diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 00000000..09b55a30
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,50 @@
+name: CI
+
+on:
+ push:
+ branches: [main]
+ pull_request:
+ branches: [main]
+
+concurrency:
+ group: ci-${{ github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ lint:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Set up Python
+ uses: actions/setup-python@v5
+ with:
+ python-version: "3.12"
+
+ - name: Install uv
+ uses: astral-sh/setup-uv@v4
+
+ - name: Install dependencies
+ run: uv sync --extra dev
+
+ - name: Ruff check
+ run: uv run ruff check src/ tests/
+
+ test:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Set up Python
+ uses: actions/setup-python@v5
+ with:
+ python-version: "3.12"
+
+ - name: Install uv
+ uses: astral-sh/setup-uv@v4
+
+ - name: Install dependencies
+ run: uv sync --extra dev
+
+ - name: Run tests
+ run: uv run pytest tests/ -v --tb=short
diff --git a/pyproject.toml b/pyproject.toml
index 555b19bd..d80d07db 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -11,7 +11,6 @@ requires-python = ">=3.10"
dependencies = [
"click>=8",
"httpx>=0.27",
- "pynvml>=13.0.1",
"rich>=13",
"tomli>=2.0; python_version < '3.11'",
]
diff --git a/src/openjarvis/agents/_stubs.py b/src/openjarvis/agents/_stubs.py
index 70eec81a..7ff0fa08 100644
--- a/src/openjarvis/agents/_stubs.py
+++ b/src/openjarvis/agents/_stubs.py
@@ -183,7 +183,10 @@ class BaseAgent(ABC):
begins directly with reasoning text followed by ````.
"""
# Full ... blocks
- text = re.sub(r".*?\s*", "", text, flags=re.DOTALL | re.IGNORECASE)
+ text = re.sub(
+ r".*?\s*", "", text,
+ flags=re.DOTALL | re.IGNORECASE,
+ )
# Leading content before a bare (no opening tag)
text = re.sub(r"^.*?\s*", "", text, flags=re.DOTALL | re.IGNORECASE)
return text.strip()
diff --git a/src/openjarvis/agents/loop_guard.py b/src/openjarvis/agents/loop_guard.py
index c96ae84d..e0609248 100644
--- a/src/openjarvis/agents/loop_guard.py
+++ b/src/openjarvis/agents/loop_guard.py
@@ -115,6 +115,16 @@ class LoopGuard:
"""Check whether an agent response indicates a loop. Reserved for future use."""
return LoopVerdict()
+ @staticmethod
+ def _is_system(msg: object) -> bool:
+ """Check if a message has role == system."""
+ return getattr(msg, 'role', None) == 'system'
+
+ @staticmethod
+ def _is_tool(msg: object) -> bool:
+ """Check if a message has role == tool."""
+ return getattr(msg, 'role', None) == 'tool'
+
def compress_context(self, messages: list) -> list:
"""Apply 4-stage context overflow recovery to message list.
@@ -131,17 +141,14 @@ class LoopGuard:
threshold = len(messages) // 2
compressed = []
for i, msg in enumerate(messages):
- if (
- i < threshold
- and hasattr(msg, 'role')
- and str(getattr(msg, 'role', '')) == 'tool'
- ):
- # Replace with truncated version
+ if i < threshold and self._is_tool(msg):
from openjarvis.core.types import Message, Role
compressed.append(Message(
role=Role.TOOL,
content="[Tool result truncated]",
- tool_call_id=getattr(msg, 'tool_call_id', None),
+ tool_call_id=getattr(
+ msg, 'tool_call_id', None,
+ ),
name=getattr(msg, 'name', None),
))
else:
@@ -150,20 +157,17 @@ class LoopGuard:
if len(compressed) <= self._config.max_context_messages:
return compressed
- # Stage 2: Sliding window — keep system messages + recent window
+ # Stage 2: Sliding window — keep system + recent
system_msgs = [
- m for m in compressed
- if hasattr(m, 'role')
- and str(getattr(m, 'role', '')) == 'system'
+ m for m in compressed if self._is_system(m)
]
non_system = [
m for m in compressed
- if not (
- hasattr(m, 'role')
- and str(getattr(m, 'role', '')) == 'system'
- )
+ if not self._is_system(m)
]
- window_size = self._config.max_context_messages - len(system_msgs)
+ window_size = (
+ self._config.max_context_messages - len(system_msgs)
+ )
if len(non_system) > window_size:
non_system = non_system[-window_size:]
compressed = system_msgs + non_system
@@ -172,16 +176,26 @@ class LoopGuard:
return compressed
# Stage 3: Drop tool call/result pairs from middle
- # Keep first 10% and last 50%
- keep_start = max(len(system_msgs), len(compressed) // 10)
+ keep_start = max(
+ len(system_msgs), len(compressed) // 10,
+ )
keep_end = len(compressed) // 2
- compressed = compressed[:keep_start] + compressed[-keep_end:]
+ compressed = (
+ compressed[:keep_start] + compressed[-keep_end:]
+ )
if len(compressed) <= self._config.max_context_messages:
return compressed
- # Stage 4: Extreme — system + last 2 exchanges (4 messages)
- return system_msgs + non_system[-4:]
+ # Stage 4: Extreme — system + last 2 exchanges
+ sys_final = [
+ m for m in compressed if self._is_system(m)
+ ]
+ tail = [
+ m for m in compressed
+ if not self._is_system(m)
+ ]
+ return sys_final + tail[-4:]
def reset(self) -> None:
"""Reset all tracking state."""
diff --git a/src/openjarvis/agents/native_react.py b/src/openjarvis/agents/native_react.py
index 527eecfe..1c10ddf1 100644
--- a/src/openjarvis/agents/native_react.py
+++ b/src/openjarvis/agents/native_react.py
@@ -110,6 +110,9 @@ class NativeReActAgent(ToolUsingAgent):
for _turn in range(self._max_turns):
turns += 1
+ if self._loop_guard:
+ messages = self._loop_guard.compress_context(messages)
+
result = self._generate(messages)
content = result.get("content", "")
diff --git a/src/openjarvis/agents/operative.py b/src/openjarvis/agents/operative.py
index cef77e4c..a98f3f8d 100644
--- a/src/openjarvis/agents/operative.py
+++ b/src/openjarvis/agents/operative.py
@@ -105,6 +105,9 @@ class OperativeAgent(ToolUsingAgent):
for _turn in range(self._max_turns):
turns += 1
+ if self._loop_guard:
+ messages = self._loop_guard.compress_context(messages)
+
gen_kwargs: dict[str, Any] = {}
if openai_tools:
gen_kwargs["tools"] = openai_tools
diff --git a/src/openjarvis/agents/orchestrator.py b/src/openjarvis/agents/orchestrator.py
index 90ef0964..7e9bd5b1 100644
--- a/src/openjarvis/agents/orchestrator.py
+++ b/src/openjarvis/agents/orchestrator.py
@@ -101,6 +101,9 @@ class OrchestratorAgent(ToolUsingAgent):
for _turn in range(self._max_turns):
turns += 1
+ if self._loop_guard:
+ messages = self._loop_guard.compress_context(messages)
+
result = self._generate(messages)
content = result.get("content", "")
@@ -217,6 +220,9 @@ class OrchestratorAgent(ToolUsingAgent):
for _turn in range(self._max_turns):
turns += 1
+ if self._loop_guard:
+ messages = self._loop_guard.compress_context(messages)
+
# Build generate kwargs
gen_kwargs: dict[str, Any] = {}
if openai_tools:
diff --git a/src/openjarvis/cli/bench_cmd.py b/src/openjarvis/cli/bench_cmd.py
index c520f436..adf56bc6 100644
--- a/src/openjarvis/cli/bench_cmd.py
+++ b/src/openjarvis/cli/bench_cmd.py
@@ -4,6 +4,7 @@ from __future__ import annotations
import json as json_mod
import sys
+from typing import TYPE_CHECKING
import click
from rich.console import Console
@@ -11,8 +12,6 @@ from rich.panel import Panel
from rich.rule import Rule
from rich.table import Table
-from typing import TYPE_CHECKING
-
from openjarvis.core.config import load_config
if TYPE_CHECKING:
diff --git a/src/openjarvis/cli/init_cmd.py b/src/openjarvis/cli/init_cmd.py
index f489f285..c5ede301 100644
--- a/src/openjarvis/cli/init_cmd.py
+++ b/src/openjarvis/cli/init_cmd.py
@@ -2,11 +2,12 @@
from __future__ import annotations
+from pathlib import Path
+from typing import Optional
+
import click
from rich.console import Console
from rich.panel import Panel
-from pathlib import Path
-from typing import Optional
from openjarvis.core.config import (
DEFAULT_CONFIG_DIR,
diff --git a/src/openjarvis/cli/quickstart_cmd.py b/src/openjarvis/cli/quickstart_cmd.py
index 936f4bd2..ee70a08d 100644
--- a/src/openjarvis/cli/quickstart_cmd.py
+++ b/src/openjarvis/cli/quickstart_cmd.py
@@ -17,12 +17,11 @@ from openjarvis.core.config import (
def _check_engine_health(engine_key: str) -> bool:
"""Return True if the recommended engine is reachable."""
try:
+ import openjarvis.engine # noqa: F401 — trigger registration
from openjarvis.core.config import load_config
from openjarvis.core.registry import EngineRegistry
from openjarvis.engine import _discovery
- import openjarvis.engine # noqa: F401 — trigger registration
-
config = load_config()
if engine_key not in EngineRegistry.keys():
return False
@@ -86,7 +85,10 @@ def quickstart(force: bool) -> None:
console.print()
console.print("[bold cyan][2/5][/bold cyan] Writing config...")
if DEFAULT_CONFIG_PATH.exists() and not force:
- console.print(f" [dim]Config already exists at {DEFAULT_CONFIG_PATH} (skip)[/dim]")
+ console.print(
+ f" [dim]Config already exists at"
+ f" {DEFAULT_CONFIG_PATH} (skip)[/dim]"
+ )
else:
toml_content = generate_default_toml(hw)
DEFAULT_CONFIG_DIR.mkdir(parents=True, exist_ok=True)
@@ -100,7 +102,7 @@ def quickstart(force: bool) -> None:
console.print(f" [red bold]Engine '{engine_key}' is not reachable.[/red bold]")
console.print()
console.print(f" Start the {engine_key} server and try again.")
- console.print(f" Run [bold]jarvis doctor[/bold] for detailed diagnostics.")
+ console.print(" Run [bold]jarvis doctor[/bold] for detailed diagnostics.")
raise SystemExit(1)
console.print(f" [green]Engine '{engine_key}' is healthy.[/green]")
@@ -120,4 +122,7 @@ def quickstart(force: bool) -> None:
console.print(f" [green]Response:[/green] {response[:200]}")
console.print()
- console.print("[bold green]Setup complete![/bold green] Try: [bold]jarvis ask \"Hello\"[/bold]")
+ console.print(
+ '[bold green]Setup complete![/bold green]'
+ ' Try: [bold]jarvis ask "Hello"[/bold]'
+ )
diff --git a/src/openjarvis/cli/serve.py b/src/openjarvis/cli/serve.py
index d9c84f4d..63791d3d 100644
--- a/src/openjarvis/cli/serve.py
+++ b/src/openjarvis/cli/serve.py
@@ -150,7 +150,9 @@ def serve(
tools = []
for name in ToolRegistry.keys():
tool_cls = ToolRegistry.get(name)
- if isinstance(tool_cls, type) and issubclass(tool_cls, BaseTool):
+ if isinstance(tool_cls, type) and issubclass(
+ tool_cls, BaseTool
+ ):
tools.append(tool_cls())
elif isinstance(tool_cls, BaseTool):
tools.append(tool_cls)
diff --git a/src/openjarvis/core/types.py b/src/openjarvis/core/types.py
index 3da6bba3..f2f970cd 100644
--- a/src/openjarvis/core/types.py
+++ b/src/openjarvis/core/types.py
@@ -84,6 +84,8 @@ class Conversation:
def window(self, n: int) -> List[Message]:
"""Return the last *n* messages."""
+ if n <= 0:
+ return []
return self.messages[-n:]
diff --git a/src/openjarvis/engine/_openai_compat.py b/src/openjarvis/engine/_openai_compat.py
index 4e8358f8..9442718a 100644
--- a/src/openjarvis/engine/_openai_compat.py
+++ b/src/openjarvis/engine/_openai_compat.py
@@ -57,7 +57,15 @@ class _OpenAICompatibleEngine(InferenceEngine):
f"{self.engine_id} engine not reachable at {self._host}"
) from exc
data = resp.json()
- choice = data["choices"][0]
+ choices = data.get("choices", [])
+ if not choices:
+ return {
+ "content": "",
+ "usage": data.get("usage", {}),
+ "model": data.get("model", model),
+ "finish_reason": "error",
+ }
+ choice = choices[0]
usage = data.get("usage", {})
result: Dict[str, Any] = {
"content": choice["message"].get("content") or "",
diff --git a/src/openjarvis/engine/ollama.py b/src/openjarvis/engine/ollama.py
index f8b41f7e..c31d2d85 100644
--- a/src/openjarvis/engine/ollama.py
+++ b/src/openjarvis/engine/ollama.py
@@ -92,14 +92,21 @@ class OllamaEngine(InferenceEngine):
# Extract tool calls if present
raw_tool_calls = data.get("message", {}).get("tool_calls", [])
if raw_tool_calls:
- result["tool_calls"] = [
- {
+ tool_calls = []
+ for i, tc in enumerate(raw_tool_calls):
+ raw_args = tc.get("function", {}).get(
+ "arguments", "{}",
+ )
+ tool_calls.append({
"id": tc.get("id", f"call_{i}"),
"name": tc.get("function", {}).get("name", ""),
- "arguments": tc.get("function", {}).get("arguments", "{}"),
- }
- for i, tc in enumerate(raw_tool_calls)
- ]
+ "arguments": (
+ json.dumps(raw_args)
+ if isinstance(raw_args, dict)
+ else raw_args
+ ),
+ })
+ result["tool_calls"] = tool_calls
return result
async def stream(
diff --git a/src/openjarvis/evals/cli.py b/src/openjarvis/evals/cli.py
index 7395bfd1..11b35741 100644
--- a/src/openjarvis/evals/cli.py
+++ b/src/openjarvis/evals/cli.py
@@ -21,7 +21,6 @@ from openjarvis.evals.core.display import (
print_banner,
print_completion,
print_full_results,
- print_metrics_table,
print_run_header,
print_section,
print_subject_table,
@@ -392,8 +391,14 @@ def main():
help="Enable telemetry collection during eval")
@click.option("--gpu-metrics/--no-gpu-metrics", default=False,
help="Enable GPU metrics collection")
-@click.option("--compact", is_flag=True, default=False, help="Dense single-table output")
-@click.option("--trace-detail", is_flag=True, default=False, help="Full per-step trace listing")
+@click.option(
+ "--compact", is_flag=True, default=False,
+ help="Dense single-table output",
+)
+@click.option(
+ "--trace-detail", is_flag=True, default=False,
+ help="Full per-step trace listing",
+)
@click.option("--wandb-project", default="",
help="W&B project name (enables tracking)")
@click.option("--wandb-entity", default="",
diff --git a/src/openjarvis/evals/core/config.py b/src/openjarvis/evals/core/config.py
index af09a9de..3c1329ba 100644
--- a/src/openjarvis/evals/core/config.py
+++ b/src/openjarvis/evals/core/config.py
@@ -124,7 +124,11 @@ def load_eval_config(path: str | Path) -> EvalSuiteConfig:
temperature=float(m["temperature"]) if "temperature" in m else None,
max_tokens=int(m["max_tokens"]) if "max_tokens" in m else None,
param_count_b=float(m.get("param_count_b", 0.0)),
- active_params_b=float(m["active_params_b"]) if "active_params_b" in m else None,
+ active_params_b=(
+ float(m["active_params_b"])
+ if "active_params_b" in m
+ else None
+ ),
gpu_peak_tflops=float(m.get("gpu_peak_tflops", 0.0)),
gpu_peak_bandwidth_gb_s=float(m.get("gpu_peak_bandwidth_gb_s", 0.0)),
num_gpus=int(m.get("num_gpus", 1)),
diff --git a/src/openjarvis/evals/core/display.py b/src/openjarvis/evals/core/display.py
index e2efc9d9..cb91c5aa 100644
--- a/src/openjarvis/evals/core/display.py
+++ b/src/openjarvis/evals/core/display.py
@@ -152,7 +152,10 @@ def print_metrics_table(console: Console, summary: RunSummary) -> None:
console.print(headline)
-def _stats_table(title: str, rows: list[tuple[str, Optional[MetricStats], int]]) -> Table:
+def _stats_table(
+ title: str,
+ rows: list[tuple[str, Optional[MetricStats], int]],
+) -> Table:
"""Build a stats table with Avg/Median/Min/Max/Std columns."""
table = Table(
title=f"[bold]{title}[/bold]",
@@ -192,7 +195,10 @@ def print_accuracy_panel(console: Console, summary: RunSummary) -> None:
scored = int(stats.get("scored", 0))
lines.append(f" {subj:<20s} {acc:.1%} ({correct}/{scored})")
body = "\n".join(lines)
- panel = Panel(body, title="[bold]Accuracy[/bold]", border_style="green", expand=False)
+ panel = Panel(
+ body, title="[bold]Accuracy[/bold]",
+ border_style="green", expand=False,
+ )
console.print(panel)
@@ -244,7 +250,11 @@ def print_trace_summary(console: Console, summary: RunSummary) -> None:
if not sts:
return
total_steps = sum(s.get("count", 0) for s in sts.values())
- avg_per_sample = total_steps / summary.scored_samples if summary.scored_samples > 0 else 0
+ avg_per_sample = (
+ total_steps / summary.scored_samples
+ if summary.scored_samples > 0
+ else 0
+ )
table = Table(
title="[bold]Agentic Trace Summary[/bold]",
@@ -252,7 +262,10 @@ def print_trace_summary(console: Console, summary: RunSummary) -> None:
header_style="bold bright_white",
border_style="bright_blue",
title_style="bold cyan",
- caption=f"Total Steps: {total_steps} | Avg Steps/Sample: {avg_per_sample:.1f}",
+ caption=(
+ f"Total Steps: {total_steps}"
+ f" | Avg Steps/Sample: {avg_per_sample:.1f}"
+ ),
)
table.add_column("Step Type", style="cyan", no_wrap=True)
table.add_column("Count", justify="right")
diff --git a/src/openjarvis/evals/tests/test_config.py b/src/openjarvis/evals/tests/test_config.py
index 84d187a9..e03e038c 100644
--- a/src/openjarvis/evals/tests/test_config.py
+++ b/src/openjarvis/evals/tests/test_config.py
@@ -19,7 +19,6 @@ from openjarvis.evals.core.types import (
RunConfig,
)
-
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
@@ -197,7 +196,10 @@ class TestLoadEvalConfig:
[[models]]
name = "qwen3:8b"
""")
- with pytest.raises(EvalConfigError, match="at least one \\[\\[benchmarks\\]\\]"):
+ with pytest.raises(
+ EvalConfigError,
+ match="at least one \\[\\[benchmarks\\]\\]",
+ ):
load_eval_config(p)
def test_model_without_name_raises(self, tmp_path):
@@ -275,7 +277,10 @@ class TestLoadEvalConfig:
benchmarks = []
""")
- with pytest.raises(EvalConfigError, match="at least one \\[\\[benchmarks\\]\\]"):
+ with pytest.raises(
+ EvalConfigError,
+ match="at least one \\[\\[benchmarks\\]\\]",
+ ):
load_eval_config(p)
def test_model_hardware_params(self, tmp_path):
@@ -339,7 +344,13 @@ class TestLoadEvalConfig:
class TestExampleConfigs:
- @pytest.fixture(params=["minimal.toml", "single-run.toml", "full-suite.toml", "glm-4.7-flash-openhands.toml", "glm-4.7-flash-openhands-remaining.toml"])
+ @pytest.fixture(params=[
+ "minimal.toml",
+ "single-run.toml",
+ "full-suite.toml",
+ "glm-4.7-flash-openhands.toml",
+ "glm-4.7-flash-openhands-remaining.toml",
+ ])
def example_config(self, request):
configs_dir = Path(__file__).resolve().parent.parent / "configs"
return configs_dir / request.param
@@ -597,6 +608,7 @@ class TestExpandSuite:
class TestCLIConfig:
def test_run_missing_benchmark_and_config(self):
from click.testing import CliRunner
+
from openjarvis.evals.cli import main
runner = CliRunner()
@@ -606,6 +618,7 @@ class TestCLIConfig:
def test_run_missing_model_and_config(self):
from click.testing import CliRunner
+
from openjarvis.evals.cli import main
runner = CliRunner()
@@ -615,6 +628,7 @@ class TestCLIConfig:
def test_run_config_file_not_found(self):
from click.testing import CliRunner
+
from openjarvis.evals.cli import main
runner = CliRunner()
@@ -630,6 +644,7 @@ class TestCLIConfig:
from unittest.mock import patch
from click.testing import CliRunner
+
from openjarvis.evals.cli import main
p = _write_toml(tmp_path, """\
diff --git a/src/openjarvis/evals/tests/test_display.py b/src/openjarvis/evals/tests/test_display.py
index 49b24e97..74d84d1d 100644
--- a/src/openjarvis/evals/tests/test_display.py
+++ b/src/openjarvis/evals/tests/test_display.py
@@ -8,11 +8,11 @@ from rich.console import Console
from openjarvis.evals.core.display import (
print_accuracy_panel,
+ print_compact_table,
print_energy_table,
+ print_full_results,
print_latency_table,
print_trace_summary,
- print_compact_table,
- print_full_results,
)
from openjarvis.evals.core.types import MetricStats, RunSummary
diff --git a/src/openjarvis/evals/tests/test_runner.py b/src/openjarvis/evals/tests/test_runner.py
index 80b65e17..38ea7580 100644
--- a/src/openjarvis/evals/tests/test_runner.py
+++ b/src/openjarvis/evals/tests/test_runner.py
@@ -381,7 +381,10 @@ class TestRunnerTokenStats:
def test_summary_has_total_input_output_tokens(self, tmp_path):
"""RunSummary should include total token counts."""
records = [
- EvalRecord(record_id=f"r{i}", problem=f"q{i}", reference="a", category="test")
+ EvalRecord(
+ record_id=f"r{i}", problem=f"q{i}",
+ reference="a", category="test",
+ )
for i in range(3)
]
output_path = tmp_path / "results.jsonl"
diff --git a/src/openjarvis/intelligence/model_catalog.py b/src/openjarvis/intelligence/model_catalog.py
index 33c50c53..6d4a5366 100644
--- a/src/openjarvis/intelligence/model_catalog.py
+++ b/src/openjarvis/intelligence/model_catalog.py
@@ -117,7 +117,10 @@ BUILTIN_MODELS: List[ModelSpec] = [
provider="teichai",
metadata={
"architecture": "moe",
- "hf_repo": "TeichAI/GLM-4.7-Flash-Claude-Opus-4.5-High-Reasoning-Distill-GGUF",
+ "hf_repo": (
+ "TeichAI/GLM-4.7-Flash-Claude-"
+ "Opus-4.5-High-Reasoning-Distill-GGUF"
+ ),
"teacher": "Claude Opus 4.5",
"quantization": "GGUF Q4_K_M / Q8_0",
"license": "apache-2.0",
diff --git a/src/openjarvis/security/guardrails.py b/src/openjarvis/security/guardrails.py
index 3b4a2d9c..aa7f4dc1 100644
--- a/src/openjarvis/security/guardrails.py
+++ b/src/openjarvis/security/guardrails.py
@@ -172,20 +172,22 @@ class GuardrailsEngine(InferenceEngine):
"""Scan input, call wrapped engine, scan output."""
# Scan input messages
if self._scan_input:
- for msg in messages:
+ processed = list(messages)
+ for i, msg in enumerate(processed):
if msg.content:
result = self._scan_text(msg.content)
if not result.clean:
- msg = Message(
+ processed[i] = Message(
role=msg.role,
content=self._handle_findings(
- msg.content, result, "input"
+ msg.content, result, "input",
),
name=msg.name,
tool_calls=msg.tool_calls,
tool_call_id=msg.tool_call_id,
metadata=msg.metadata,
)
+ messages = processed
# Call wrapped engine
response = self._engine.generate(
diff --git a/src/openjarvis/server/api_routes.py b/src/openjarvis/server/api_routes.py
index 5df86265..00c281fb 100644
--- a/src/openjarvis/server/api_routes.py
+++ b/src/openjarvis/server/api_routes.py
@@ -238,7 +238,6 @@ async def telemetry_stats(request: Request):
async def telemetry_energy(request: Request):
"""Get energy monitoring data."""
try:
- from dataclasses import asdict
from openjarvis.core.config import DEFAULT_CONFIG_DIR
from openjarvis.telemetry.aggregator import TelemetryAggregator
diff --git a/src/openjarvis/server/dashboard.py b/src/openjarvis/server/dashboard.py
index 73d28145..16398f42 100644
--- a/src/openjarvis/server/dashboard.py
+++ b/src/openjarvis/server/dashboard.py
@@ -266,7 +266,9 @@ function fmtNum(n) {
}
function fmtDollar(n) {
- if (n >= 1000) return '$' + n.toLocaleString('en-US', {minimumFractionDigits: 2, maximumFractionDigits: 2});
+ if (n >= 1000) return '$' + n.toLocaleString(
+ 'en-US', {minimumFractionDigits: 2,
+ maximumFractionDigits: 2});
if (n >= 1) return '$' + n.toFixed(2);
if (n >= 0.01) return '$' + n.toFixed(3);
if (n > 0) return '$' + n.toFixed(4);
@@ -292,30 +294,44 @@ async function refresh() {
const d = await resp.json();
document.getElementById('total-calls').textContent = fmtNum(d.total_calls);
- document.getElementById('prompt-tokens').textContent = fmtNum(d.total_prompt_tokens);
- document.getElementById('completion-tokens').textContent = fmtNum(d.total_completion_tokens);
- document.getElementById('total-tokens').textContent = fmtNum(d.total_tokens);
+ document.getElementById('prompt-tokens')
+ .textContent = fmtNum(d.total_prompt_tokens);
+ document.getElementById('completion-tokens')
+ .textContent = fmtNum(d.total_completion_tokens);
+ document.getElementById('total-tokens')
+ .textContent = fmtNum(d.total_tokens);
const providerMap = {};
- (d.per_provider || []).forEach(p => { providerMap[p.provider] = p; });
+ (d.per_provider || []).forEach(p => {
+ providerMap[p.provider] = p;
+ });
// OpenAI / GPT 5.2
const oa = providerMap['gpt-5.2'] || {};
- document.getElementById('save-openai').textContent = fmtDollar(oa.total_cost || 0);
- document.getElementById('save-openai-in').textContent = fmtDollar(oa.input_cost || 0);
- document.getElementById('save-openai-out').textContent = fmtDollar(oa.output_cost || 0);
+ document.getElementById('save-openai')
+ .textContent = fmtDollar(oa.total_cost || 0);
+ document.getElementById('save-openai-in')
+ .textContent = fmtDollar(oa.input_cost || 0);
+ document.getElementById('save-openai-out')
+ .textContent = fmtDollar(oa.output_cost || 0);
// Anthropic / Claude Opus 4.6
const an = providerMap['claude-opus-4.6'] || {};
- document.getElementById('save-anthropic').textContent = fmtDollar(an.total_cost || 0);
- document.getElementById('save-anthropic-in').textContent = fmtDollar(an.input_cost || 0);
- document.getElementById('save-anthropic-out').textContent = fmtDollar(an.output_cost || 0);
+ document.getElementById('save-anthropic')
+ .textContent = fmtDollar(an.total_cost || 0);
+ document.getElementById('save-anthropic-in')
+ .textContent = fmtDollar(an.input_cost || 0);
+ document.getElementById('save-anthropic-out')
+ .textContent = fmtDollar(an.output_cost || 0);
// Google / Gemini 3.1 Pro
const go = providerMap['gemini-3.1-pro'] || {};
- document.getElementById('save-google').textContent = fmtDollar(go.total_cost || 0);
- document.getElementById('save-google-in').textContent = fmtDollar(go.input_cost || 0);
- document.getElementById('save-google-out').textContent = fmtDollar(go.output_cost || 0);
+ document.getElementById('save-google')
+ .textContent = fmtDollar(go.total_cost || 0);
+ document.getElementById('save-google-in')
+ .textContent = fmtDollar(go.input_cost || 0);
+ document.getElementById('save-google-out')
+ .textContent = fmtDollar(go.output_cost || 0);
// Energy / FLOPs (use GPT 5.2 as reference)
const ej = oa.energy_joules || 0;
@@ -323,15 +339,25 @@ async function refresh() {
const fl = oa.flops || 0;
const co2 = (eWh / 1000) * 390; // grams CO2 (US grid avg 0.39 kg/kWh)
- document.getElementById('energy-joules').innerHTML = fmtEnergy(ej) + ' ';
- document.getElementById('energy-kwh').textContent =
- (eWh / 1000).toFixed(4) + ' kWh of cloud datacenter energy avoided';
- document.getElementById('flops-val').innerHTML = fmt(fl) + ' ';
- document.getElementById('flops-sub').textContent = 'cloud compute operations not needed';
- document.getElementById('co2-val').innerHTML = fmtCO2(co2) + ' ';
+ document.getElementById('energy-joules')
+ .innerHTML = fmtEnergy(ej) +
+ ' ';
+ document.getElementById('energy-kwh')
+ .textContent = (eWh / 1000).toFixed(4) +
+ ' kWh of cloud datacenter energy avoided';
+ document.getElementById('flops-val')
+ .innerHTML = fmt(fl) +
+ ' ';
+ document.getElementById('flops-sub')
+ .textContent =
+ 'cloud compute operations not needed';
+ document.getElementById('co2-val')
+ .innerHTML = fmtCO2(co2) +
+ ' ';
} catch (e) {
- document.getElementById('status-text').textContent = 'Connection error — retrying...';
+ document.getElementById('status-text')
+ .textContent = 'Connection error — retrying...';
}
}
diff --git a/src/openjarvis/system.py b/src/openjarvis/system.py
index 0fbc6917..dd9625e6 100644
--- a/src/openjarvis/system.py
+++ b/src/openjarvis/system.py
@@ -264,16 +264,20 @@ class JarvisSystem:
"""Release resources."""
if self.scheduler and hasattr(self.scheduler, "stop"):
self.scheduler.stop()
- if self.scheduler_store and hasattr(self.scheduler_store, "close"):
- self.scheduler_store.close()
- if self.engine and hasattr(self.engine, "close"):
- self.engine.close()
- if self.gpu_monitor and hasattr(self.gpu_monitor, "close"):
- self.gpu_monitor.close()
- if self.telemetry_store and hasattr(self.telemetry_store, "close"):
- self.telemetry_store.close()
- if self.trace_store and hasattr(self.trace_store, "close"):
- self.trace_store.close()
+ for resource in (
+ self.scheduler_store,
+ self.engine,
+ self.gpu_monitor,
+ self.telemetry_store,
+ self.trace_store,
+ self.memory_backend,
+ self.session_store,
+ self.channel_backend,
+ self.workflow_engine,
+ self.container_runner,
+ ):
+ if resource and hasattr(resource, "close"):
+ resource.close()
def __enter__(self) -> JarvisSystem:
return self
@@ -372,55 +376,56 @@ class SystemBuilder:
# Resolve model
model = self._resolve_model(config, engine)
- # Wrap with InstrumentedEngine if telemetry enabled
+ # Compute telemetry_enabled once
telemetry_enabled = (
self._telemetry if self._telemetry is not None
else config.telemetry.enabled
)
gpu_monitor = None
energy_monitor = None
- if telemetry_enabled:
- from openjarvis.telemetry.instrumented_engine import InstrumentedEngine
+ if telemetry_enabled and config.telemetry.gpu_metrics:
+ # Try new multi-vendor EnergyMonitor first
+ try:
+ from openjarvis.telemetry.energy_monitor import (
+ create_energy_monitor,
+ )
- if config.telemetry.gpu_metrics:
- # Try new multi-vendor EnergyMonitor first
+ energy_monitor = create_energy_monitor(
+ poll_interval_ms=config.telemetry.gpu_poll_interval_ms,
+ prefer_vendor=config.telemetry.energy_vendor or None,
+ )
+ except ImportError:
+ pass
+
+ # Fall back to legacy GpuMonitor
+ if energy_monitor is None:
try:
- from openjarvis.telemetry.energy_monitor import (
- create_energy_monitor,
- )
+ from openjarvis.telemetry.gpu_monitor import GpuMonitor
- energy_monitor = create_energy_monitor(
- poll_interval_ms=config.telemetry.gpu_poll_interval_ms,
- prefer_vendor=config.telemetry.energy_vendor or None,
- )
+ if GpuMonitor.available():
+ gpu_monitor = GpuMonitor(
+ poll_interval_ms=config.telemetry.gpu_poll_interval_ms,
+ )
except ImportError:
pass
- # Fall back to legacy GpuMonitor if EnergyMonitor not available
- if energy_monitor is None:
- try:
- from openjarvis.telemetry.gpu_monitor import GpuMonitor
+ # Apply security guardrails FIRST (innermost wrapper)
+ engine = self._apply_security(config, engine, bus)
+
+ # Then wrap with InstrumentedEngine (outermost wrapper)
+ if telemetry_enabled:
+ from openjarvis.telemetry.instrumented_engine import (
+ InstrumentedEngine,
+ )
- if GpuMonitor.available():
- gpu_monitor = GpuMonitor(
- poll_interval_ms=config.telemetry.gpu_poll_interval_ms,
- )
- except ImportError:
- pass
engine = InstrumentedEngine(
engine, bus,
gpu_monitor=gpu_monitor,
energy_monitor=energy_monitor,
)
- # Apply security guardrails to engine
- engine = self._apply_security(config, engine, bus)
-
- # Set up telemetry
+ # Set up telemetry store
telemetry_store = None
- telemetry_enabled = (
- self._telemetry if self._telemetry is not None else config.telemetry.enabled
- )
if telemetry_enabled:
telemetry_store = self._setup_telemetry(config, bus)
diff --git a/src/openjarvis/tools/web_search.py b/src/openjarvis/tools/web_search.py
index e8c7838c..0b01f8cc 100644
--- a/src/openjarvis/tools/web_search.py
+++ b/src/openjarvis/tools/web_search.py
@@ -85,10 +85,16 @@ class WebSearchTool(BaseTool):
resp.raise_for_status()
content_type = resp.headers.get("content-type", "")
if "application/pdf" in content_type:
- return f"[This URL points to a PDF file which cannot be read directly. URL: {url}]"
+ return (
+ "[This URL points to a PDF file which"
+ f" cannot be read directly. URL: {url}]"
+ )
html = resp.text
# Strip script/style tags and their contents
- html = _re.sub(r"<(script|style)[^>]*>.*?\1>", "", html, flags=_re.DOTALL | _re.IGNORECASE)
+ html = _re.sub(
+ r"<(script|style)[^>]*>.*?\1>", "", html,
+ flags=_re.DOTALL | _re.IGNORECASE,
+ )
# Strip HTML tags
text = _re.sub(r"<[^>]+>", " ", html)
# Collapse whitespace
diff --git a/tests/agents/test_loop_guard.py b/tests/agents/test_loop_guard.py
index 1c08711d..df2d3641 100644
--- a/tests/agents/test_loop_guard.py
+++ b/tests/agents/test_loop_guard.py
@@ -90,6 +90,31 @@ class TestLoopGuard:
result = guard.compress_context(messages)
assert len(result) <= 10
+ def test_context_compression_stage4_uses_current_state(self):
+ """Stage 4 should derive from compressed state."""
+ from openjarvis.core.types import Message, Role
+ guard, _ = self._make_guard(max_context_messages=5)
+ messages = [
+ Message(role=Role.SYSTEM, content="sys"),
+ ] + [
+ Message(role=Role.USER, content=f"msg {i}")
+ for i in range(100)
+ ] + [
+ Message(
+ role=Role.TOOL,
+ content=f"result {i}",
+ tool_call_id=f"t{i}",
+ )
+ for i in range(100)
+ ]
+ result = guard.compress_context(messages)
+ assert len(result) == 5
+ system_count = sum(
+ 1 for m in result
+ if getattr(m, 'role', None) == 'system'
+ )
+ assert system_count == 1
+
def test_check_response_returns_unblocked(self):
guard, _ = self._make_guard()
v = guard.check_response("some content")
diff --git a/tests/agents/test_rlm.py b/tests/agents/test_rlm.py
index 78afc308..1e1c280d 100644
--- a/tests/agents/test_rlm.py
+++ b/tests/agents/test_rlm.py
@@ -196,15 +196,27 @@ class TestRLMSubLMCalls:
# Third call: root LM gets REPL output, returns final (no code)
engine.generate.side_effect = [
{
- "content": "```python\nresult = llm_query('What is 2+2?')\nFINAL(result)\n```",
- "usage": {"prompt_tokens": 5, "completion_tokens": 10, "total_tokens": 15},
+ "content": (
+ "```python\n"
+ "result = llm_query('What is 2+2?')\n"
+ "FINAL(result)\n```"
+ ),
+ "usage": {
+ "prompt_tokens": 5,
+ "completion_tokens": 10,
+ "total_tokens": 15,
+ },
"model": "test-model",
"finish_reason": "stop",
},
# Sub-LM response for llm_query
{
"content": "4",
- "usage": {"prompt_tokens": 3, "completion_tokens": 1, "total_tokens": 4},
+ "usage": {
+ "prompt_tokens": 3,
+ "completion_tokens": 1,
+ "total_tokens": 4,
+ },
"model": "test-model",
"finish_reason": "stop",
},
@@ -224,15 +236,28 @@ class TestRLMMultiTurn:
engine.generate.side_effect = [
# Turn 1: code that sets a variable
{
- "content": "```python\nx = 10\nprint(f'x = {x}')\n```",
- "usage": {"prompt_tokens": 5, "completion_tokens": 10, "total_tokens": 15},
+ "content": (
+ "```python\n"
+ "x = 10\nprint(f'x = {x}')\n```"
+ ),
+ "usage": {
+ "prompt_tokens": 5,
+ "completion_tokens": 10,
+ "total_tokens": 15,
+ },
"model": "test-model",
"finish_reason": "stop",
},
# Turn 2: code that uses the variable and terminates
{
- "content": "```python\ny = x * 2\nFINAL(y)\n```",
- "usage": {"prompt_tokens": 20, "completion_tokens": 10, "total_tokens": 30},
+ "content": (
+ "```python\ny = x * 2\nFINAL(y)\n```"
+ ),
+ "usage": {
+ "prompt_tokens": 20,
+ "completion_tokens": 10,
+ "total_tokens": 30,
+ },
"model": "test-model",
"finish_reason": "stop",
},
@@ -309,8 +334,16 @@ class TestRLMSubLMWithTools:
engine.generate.side_effect = [
# Root LM: code that calls llm_query
{
- "content": "```python\nresult = llm_query('Calculate 2+2')\nFINAL(result)\n```",
- "usage": {"prompt_tokens": 5, "completion_tokens": 10, "total_tokens": 15},
+ "content": (
+ "```python\n"
+ "result = llm_query('Calculate 2+2')\n"
+ "FINAL(result)\n```"
+ ),
+ "usage": {
+ "prompt_tokens": 5,
+ "completion_tokens": 10,
+ "total_tokens": 15,
+ },
"model": "test-model",
"finish_reason": "stop",
},
@@ -318,16 +351,28 @@ class TestRLMSubLMWithTools:
{
"content": "",
"tool_calls": [
- {"id": "sub_0", "name": "calculator", "arguments": '{"expression":"2+2"}'},
+ {
+ "id": "sub_0",
+ "name": "calculator",
+ "arguments": '{"expression":"2+2"}',
+ },
],
- "usage": {"prompt_tokens": 3, "completion_tokens": 5, "total_tokens": 8},
+ "usage": {
+ "prompt_tokens": 3,
+ "completion_tokens": 5,
+ "total_tokens": 8,
+ },
"model": "test-model",
"finish_reason": "tool_calls",
},
# Sub-LM follow-up after tool result
{
"content": "The answer is 4.",
- "usage": {"prompt_tokens": 10, "completion_tokens": 5, "total_tokens": 15},
+ "usage": {
+ "prompt_tokens": 10,
+ "completion_tokens": 5,
+ "total_tokens": 15,
+ },
"model": "test-model",
"finish_reason": "stop",
},
@@ -345,14 +390,22 @@ class TestRLMBlockedCode:
# Code with blocked pattern
{
"content": "```python\nos.system('ls')\n```",
- "usage": {"prompt_tokens": 5, "completion_tokens": 10, "total_tokens": 15},
+ "usage": {
+ "prompt_tokens": 5,
+ "completion_tokens": 10,
+ "total_tokens": 15,
+ },
"model": "test-model",
"finish_reason": "stop",
},
# After error feedback, model gives direct answer
{
"content": "I apologize, let me answer directly.",
- "usage": {"prompt_tokens": 15, "completion_tokens": 5, "total_tokens": 20},
+ "usage": {
+ "prompt_tokens": 15,
+ "completion_tokens": 5,
+ "total_tokens": 20,
+ },
"model": "test-model",
"finish_reason": "stop",
},
diff --git a/tests/channels/test_bluebubbles.py b/tests/channels/test_bluebubbles.py
index b927e881..bb4f9664 100644
--- a/tests/channels/test_bluebubbles.py
+++ b/tests/channels/test_bluebubbles.py
@@ -42,14 +42,25 @@ class TestInit:
assert ch._password == "test-pass"
def test_env_var_fallback(self):
- with patch.dict(os.environ, {"BLUEBUBBLES_URL": "http://env:1234", "BLUEBUBBLES_PASSWORD": "env-pass"}):
+ env = {
+ "BLUEBUBBLES_URL": "http://env:1234",
+ "BLUEBUBBLES_PASSWORD": "env-pass",
+ }
+ with patch.dict(os.environ, env):
ch = BlueBubblesChannel()
assert ch._url == "http://env:1234"
assert ch._password == "env-pass"
def test_constructor_overrides_env(self):
- with patch.dict(os.environ, {"BLUEBUBBLES_URL": "http://env:1234", "BLUEBUBBLES_PASSWORD": "env-pass"}):
- ch = BlueBubblesChannel(url="http://explicit:1234", password="explicit-pass")
+ env = {
+ "BLUEBUBBLES_URL": "http://env:1234",
+ "BLUEBUBBLES_PASSWORD": "env-pass",
+ }
+ with patch.dict(os.environ, env):
+ ch = BlueBubblesChannel(
+ url="http://explicit:1234",
+ password="explicit-pass",
+ )
assert ch._url == "http://explicit:1234"
assert ch._password == "explicit-pass"
@@ -98,7 +109,11 @@ class TestSend:
def test_send_publishes_event(self):
bus = EventBus(record_history=True)
- ch = BlueBubblesChannel(url="http://localhost:1234", password="test-pass", bus=bus)
+ ch = BlueBubblesChannel(
+ url="http://localhost:1234",
+ password="test-pass",
+ bus=bus,
+ )
mock_response = MagicMock()
mock_response.status_code = 200
diff --git a/tests/channels/test_feishu.py b/tests/channels/test_feishu.py
index 9ecdf150..a647a333 100644
--- a/tests/channels/test_feishu.py
+++ b/tests/channels/test_feishu.py
@@ -42,13 +42,21 @@ class TestInit:
assert ch._app_secret == "test-secret"
def test_env_var_fallback(self):
- with patch.dict(os.environ, {"FEISHU_APP_ID": "env-id", "FEISHU_APP_SECRET": "env-secret"}):
+ env = {
+ "FEISHU_APP_ID": "env-id",
+ "FEISHU_APP_SECRET": "env-secret",
+ }
+ with patch.dict(os.environ, env):
ch = FeishuChannel()
assert ch._app_id == "env-id"
assert ch._app_secret == "env-secret"
def test_constructor_overrides_env(self):
- with patch.dict(os.environ, {"FEISHU_APP_ID": "env-id", "FEISHU_APP_SECRET": "env-secret"}):
+ env = {
+ "FEISHU_APP_ID": "env-id",
+ "FEISHU_APP_SECRET": "env-secret",
+ }
+ with patch.dict(os.environ, env):
ch = FeishuChannel(app_id="explicit-id", app_secret="explicit-secret")
assert ch._app_id == "explicit-id"
assert ch._app_secret == "explicit-secret"
diff --git a/tests/channels/test_irc_channel.py b/tests/channels/test_irc_channel.py
index f07d7589..cc0392c8 100644
--- a/tests/channels/test_irc_channel.py
+++ b/tests/channels/test_irc_channel.py
@@ -63,7 +63,11 @@ class TestInit:
"IRC_NICK": "envbot",
"IRC_PASSWORD": "envpass",
}):
- ch = IRCChannel(server="irc.explicit.com", nick="explicit", password="explicit-pass")
+ ch = IRCChannel(
+ server="irc.explicit.com",
+ nick="explicit",
+ password="explicit-pass",
+ )
assert ch._server == "irc.explicit.com"
assert ch._nick == "explicit"
assert ch._password == "explicit-pass"
@@ -96,7 +100,12 @@ class TestSend:
def test_send_publishes_event(self):
bus = EventBus(record_history=True)
- ch = IRCChannel(server="irc.example.com", nick="jarvis", password="pass123", bus=bus)
+ ch = IRCChannel(
+ server="irc.example.com",
+ nick="jarvis",
+ password="pass123",
+ bus=bus,
+ )
mock_sock = MagicMock()
with patch("socket.socket", return_value=mock_sock):
diff --git a/tests/channels/test_matrix_channel.py b/tests/channels/test_matrix_channel.py
index 24ec00f9..298641af 100644
--- a/tests/channels/test_matrix_channel.py
+++ b/tests/channels/test_matrix_channel.py
@@ -25,7 +25,10 @@ class TestRegistration:
assert ChannelRegistry.contains("matrix")
def test_channel_id(self):
- ch = MatrixChannel(homeserver="https://matrix.example.com", access_token="test-token")
+ ch = MatrixChannel(
+ homeserver="https://matrix.example.com",
+ access_token="test-token",
+ )
assert ch.channel_id == "matrix"
@@ -37,26 +40,43 @@ class TestInit:
assert ch._status == ChannelStatus.DISCONNECTED
def test_constructor_param(self):
- ch = MatrixChannel(homeserver="https://matrix.example.com", access_token="test-token")
+ ch = MatrixChannel(
+ homeserver="https://matrix.example.com",
+ access_token="test-token",
+ )
assert ch._homeserver == "https://matrix.example.com"
assert ch._access_token == "test-token"
def test_env_var_fallback(self):
- with patch.dict(os.environ, {"MATRIX_HOMESERVER": "https://env.example.com", "MATRIX_ACCESS_TOKEN": "env-token"}):
+ env = {
+ "MATRIX_HOMESERVER": "https://env.example.com",
+ "MATRIX_ACCESS_TOKEN": "env-token",
+ }
+ with patch.dict(os.environ, env):
ch = MatrixChannel()
assert ch._homeserver == "https://env.example.com"
assert ch._access_token == "env-token"
def test_constructor_overrides_env(self):
- with patch.dict(os.environ, {"MATRIX_HOMESERVER": "https://env.example.com", "MATRIX_ACCESS_TOKEN": "env-token"}):
- ch = MatrixChannel(homeserver="https://explicit.example.com", access_token="explicit-token")
+ env = {
+ "MATRIX_HOMESERVER": "https://env.example.com",
+ "MATRIX_ACCESS_TOKEN": "env-token",
+ }
+ with patch.dict(os.environ, env):
+ ch = MatrixChannel(
+ homeserver="https://explicit.example.com",
+ access_token="explicit-token",
+ )
assert ch._homeserver == "https://explicit.example.com"
assert ch._access_token == "explicit-token"
class TestSend:
def test_send_success(self):
- ch = MatrixChannel(homeserver="https://matrix.example.com", access_token="test-token")
+ ch = MatrixChannel(
+ homeserver="https://matrix.example.com",
+ access_token="test-token",
+ )
mock_response = MagicMock()
mock_response.status_code = 200
@@ -71,7 +91,10 @@ class TestSend:
assert "!room123:example.com" in url
def test_send_failure(self):
- ch = MatrixChannel(homeserver="https://matrix.example.com", access_token="test-token")
+ ch = MatrixChannel(
+ homeserver="https://matrix.example.com",
+ access_token="test-token",
+ )
mock_response = MagicMock()
mock_response.status_code = 400
@@ -82,7 +105,10 @@ class TestSend:
assert result is False
def test_send_exception(self):
- ch = MatrixChannel(homeserver="https://matrix.example.com", access_token="test-token")
+ ch = MatrixChannel(
+ homeserver="https://matrix.example.com",
+ access_token="test-token",
+ )
with patch("httpx.put", side_effect=ConnectionError("refused")):
result = ch.send("!room123:example.com", "Hello!")
@@ -95,7 +121,11 @@ class TestSend:
def test_send_publishes_event(self):
bus = EventBus(record_history=True)
- ch = MatrixChannel(homeserver="https://matrix.example.com", access_token="test-token", bus=bus)
+ ch = MatrixChannel(
+ homeserver="https://matrix.example.com",
+ access_token="test-token",
+ bus=bus,
+ )
mock_response = MagicMock()
mock_response.status_code = 200
@@ -109,13 +139,19 @@ class TestSend:
class TestListChannels:
def test_list_channels(self):
- ch = MatrixChannel(homeserver="https://matrix.example.com", access_token="test-token")
+ ch = MatrixChannel(
+ homeserver="https://matrix.example.com",
+ access_token="test-token",
+ )
assert ch.list_channels() == ["matrix"]
class TestStatus:
def test_disconnected_initially(self):
- ch = MatrixChannel(homeserver="https://matrix.example.com", access_token="test-token")
+ ch = MatrixChannel(
+ homeserver="https://matrix.example.com",
+ access_token="test-token",
+ )
assert ch.status() == ChannelStatus.DISCONNECTED
def test_no_homeserver_connect_error(self):
@@ -126,7 +162,10 @@ class TestStatus:
class TestOnMessage:
def test_on_message(self):
- ch = MatrixChannel(homeserver="https://matrix.example.com", access_token="test-token")
+ ch = MatrixChannel(
+ homeserver="https://matrix.example.com",
+ access_token="test-token",
+ )
handler = MagicMock()
ch.on_message(handler)
assert handler in ch._handlers
@@ -134,7 +173,10 @@ class TestOnMessage:
class TestDisconnect:
def test_disconnect(self):
- ch = MatrixChannel(homeserver="https://matrix.example.com", access_token="test-token")
+ ch = MatrixChannel(
+ homeserver="https://matrix.example.com",
+ access_token="test-token",
+ )
ch._status = ChannelStatus.CONNECTED
ch.disconnect()
assert ch.status() == ChannelStatus.DISCONNECTED
diff --git a/tests/channels/test_mattermost.py b/tests/channels/test_mattermost.py
index 3ae6cc3f..dd7d2fa0 100644
--- a/tests/channels/test_mattermost.py
+++ b/tests/channels/test_mattermost.py
@@ -42,14 +42,25 @@ class TestInit:
assert ch._token == "test-token"
def test_env_var_fallback(self):
- with patch.dict(os.environ, {"MATTERMOST_URL": "https://env.example.com", "MATTERMOST_TOKEN": "env-token"}):
+ env = {
+ "MATTERMOST_URL": "https://env.example.com",
+ "MATTERMOST_TOKEN": "env-token",
+ }
+ with patch.dict(os.environ, env):
ch = MattermostChannel()
assert ch._url == "https://env.example.com"
assert ch._token == "env-token"
def test_constructor_overrides_env(self):
- with patch.dict(os.environ, {"MATTERMOST_URL": "https://env.example.com", "MATTERMOST_TOKEN": "env-token"}):
- ch = MattermostChannel(url="https://explicit.example.com", token="explicit-token")
+ env = {
+ "MATTERMOST_URL": "https://env.example.com",
+ "MATTERMOST_TOKEN": "env-token",
+ }
+ with patch.dict(os.environ, env):
+ ch = MattermostChannel(
+ url="https://explicit.example.com",
+ token="explicit-token",
+ )
assert ch._url == "https://explicit.example.com"
assert ch._token == "explicit-token"
@@ -108,7 +119,11 @@ class TestSend:
def test_send_publishes_event(self):
bus = EventBus(record_history=True)
- ch = MattermostChannel(url="https://mattermost.example.com", token="test-token", bus=bus)
+ ch = MattermostChannel(
+ url="https://mattermost.example.com",
+ token="test-token",
+ bus=bus,
+ )
mock_response = MagicMock()
mock_response.status_code = 200
diff --git a/tests/channels/test_signal_channel.py b/tests/channels/test_signal_channel.py
index 8d8fae6e..38f6303d 100644
--- a/tests/channels/test_signal_channel.py
+++ b/tests/channels/test_signal_channel.py
@@ -55,7 +55,10 @@ class TestInit:
"SIGNAL_API_URL": "http://env-signal:8080",
"SIGNAL_PHONE_NUMBER": "+9876543210",
}):
- ch = SignalChannel(api_url="http://explicit:8080", phone_number="+1111111111")
+ ch = SignalChannel(
+ api_url="http://explicit:8080",
+ phone_number="+1111111111",
+ )
assert ch._api_url == "http://explicit:8080"
assert ch._phone_number == "+1111111111"
@@ -97,7 +100,11 @@ class TestSend:
def test_send_publishes_event(self):
bus = EventBus(record_history=True)
- ch = SignalChannel(api_url="http://localhost:8080", phone_number="+1234567890", bus=bus)
+ ch = SignalChannel(
+ api_url="http://localhost:8080",
+ phone_number="+1234567890",
+ bus=bus,
+ )
mock_response = MagicMock()
mock_response.status_code = 200
diff --git a/tests/channels/test_teams.py b/tests/channels/test_teams.py
index 86fbce08..498fcc09 100644
--- a/tests/channels/test_teams.py
+++ b/tests/channels/test_teams.py
@@ -42,13 +42,21 @@ class TestInit:
assert ch._app_password == "test-pass"
def test_env_var_fallback(self):
- with patch.dict(os.environ, {"TEAMS_APP_ID": "env-id", "TEAMS_APP_PASSWORD": "env-pass"}):
+ env = {
+ "TEAMS_APP_ID": "env-id",
+ "TEAMS_APP_PASSWORD": "env-pass",
+ }
+ with patch.dict(os.environ, env):
ch = TeamsChannel()
assert ch._app_id == "env-id"
assert ch._app_password == "env-pass"
def test_constructor_overrides_env(self):
- with patch.dict(os.environ, {"TEAMS_APP_ID": "env-id", "TEAMS_APP_PASSWORD": "env-pass"}):
+ env = {
+ "TEAMS_APP_ID": "env-id",
+ "TEAMS_APP_PASSWORD": "env-pass",
+ }
+ with patch.dict(os.environ, env):
ch = TeamsChannel(app_id="explicit-id", app_password="explicit-pass")
assert ch._app_id == "explicit-id"
assert ch._app_password == "explicit-pass"
diff --git a/tests/channels/test_whatsapp.py b/tests/channels/test_whatsapp.py
index 161e72da..3b0a2a30 100644
--- a/tests/channels/test_whatsapp.py
+++ b/tests/channels/test_whatsapp.py
@@ -55,7 +55,10 @@ class TestInit:
"WHATSAPP_ACCESS_TOKEN": "env-token",
"WHATSAPP_PHONE_NUMBER_ID": "env-id",
}):
- ch = WhatsAppChannel(access_token="explicit-token", phone_number_id="explicit-id")
+ ch = WhatsAppChannel(
+ access_token="explicit-token",
+ phone_number_id="explicit-id",
+ )
assert ch._token == "explicit-token"
assert ch._phone_number_id == "explicit-id"
@@ -101,7 +104,11 @@ class TestSend:
def test_send_publishes_event(self):
bus = EventBus(record_history=True)
- ch = WhatsAppChannel(access_token="test-token", phone_number_id="12345", bus=bus)
+ ch = WhatsAppChannel(
+ access_token="test-token",
+ phone_number_id="12345",
+ bus=bus,
+ )
mock_response = MagicMock()
mock_response.status_code = 200
diff --git a/tests/channels/test_whatsapp_baileys.py b/tests/channels/test_whatsapp_baileys.py
index 544dba5c..72e64035 100644
--- a/tests/channels/test_whatsapp_baileys.py
+++ b/tests/channels/test_whatsapp_baileys.py
@@ -276,7 +276,13 @@ class TestReaderLoop:
lines = [
json.dumps({"type": "status", "status": "connected"}) + "\n",
- json.dumps({"type": "message", "jid": "j", "sender": "s", "text": "t", "message_id": "m"}) + "\n",
+ json.dumps({
+ "type": "message",
+ "jid": "j",
+ "sender": "s",
+ "text": "t",
+ "message_id": "m",
+ }) + "\n",
]
mock_proc = MagicMock()
diff --git a/tests/cli/test_quickstart.py b/tests/cli/test_quickstart.py
index 3b1e5541..5317a5eb 100644
--- a/tests/cli/test_quickstart.py
+++ b/tests/cli/test_quickstart.py
@@ -31,11 +31,31 @@ class TestQuickstartCommand:
patch("openjarvis.cli.quickstart_cmd.detect_hardware", return_value=hw),
patch("openjarvis.cli.quickstart_cmd.DEFAULT_CONFIG_PATH", config_path),
patch("openjarvis.cli.quickstart_cmd.DEFAULT_CONFIG_DIR", tmp_path),
- patch("openjarvis.cli.quickstart_cmd.generate_default_toml", return_value="[engine]\n"),
- patch("openjarvis.cli.quickstart_cmd.recommend_engine", return_value="ollama"),
- patch("openjarvis.cli.quickstart_cmd._check_engine_health", return_value=True),
- patch("openjarvis.cli.quickstart_cmd._check_model_available", return_value=True),
- patch("openjarvis.cli.quickstart_cmd._test_query", return_value="Hello!"),
+ patch(
+ "openjarvis.cli.quickstart_cmd"
+ ".generate_default_toml",
+ return_value="[engine]\n",
+ ),
+ patch(
+ "openjarvis.cli.quickstart_cmd"
+ ".recommend_engine",
+ return_value="ollama",
+ ),
+ patch(
+ "openjarvis.cli.quickstart_cmd"
+ "._check_engine_health",
+ return_value=True,
+ ),
+ patch(
+ "openjarvis.cli.quickstart_cmd"
+ "._check_model_available",
+ return_value=True,
+ ),
+ patch(
+ "openjarvis.cli.quickstart_cmd"
+ "._test_query",
+ return_value="Hello!",
+ ),
):
runner = CliRunner()
result = runner.invoke(cli, ["quickstart"])
@@ -58,16 +78,39 @@ class TestQuickstartCommand:
patch("openjarvis.cli.quickstart_cmd.detect_hardware", return_value=hw),
patch("openjarvis.cli.quickstart_cmd.DEFAULT_CONFIG_PATH", config_path),
patch("openjarvis.cli.quickstart_cmd.DEFAULT_CONFIG_DIR", tmp_path),
- patch("openjarvis.cli.quickstart_cmd.generate_default_toml", return_value="[engine]\n"),
- patch("openjarvis.cli.quickstart_cmd.recommend_engine", return_value="ollama"),
- patch("openjarvis.cli.quickstart_cmd._check_engine_health", return_value=True),
- patch("openjarvis.cli.quickstart_cmd._check_model_available", return_value=True),
- patch("openjarvis.cli.quickstart_cmd._test_query", return_value="Hello!"),
+ patch(
+ "openjarvis.cli.quickstart_cmd"
+ ".generate_default_toml",
+ return_value="[engine]\n",
+ ),
+ patch(
+ "openjarvis.cli.quickstart_cmd"
+ ".recommend_engine",
+ return_value="ollama",
+ ),
+ patch(
+ "openjarvis.cli.quickstart_cmd"
+ "._check_engine_health",
+ return_value=True,
+ ),
+ patch(
+ "openjarvis.cli.quickstart_cmd"
+ "._check_model_available",
+ return_value=True,
+ ),
+ patch(
+ "openjarvis.cli.quickstart_cmd"
+ "._test_query",
+ return_value="Hello!",
+ ),
):
runner = CliRunner()
result = runner.invoke(cli, ["quickstart"])
assert result.exit_code == 0
- assert "already exists" in result.output.lower() or "skip" in result.output.lower()
+ assert (
+ "already exists" in result.output.lower()
+ or "skip" in result.output.lower()
+ )
def test_force_regenerates_config(self, tmp_path):
"""--force should regenerate config even if it exists."""
@@ -84,10 +127,26 @@ class TestQuickstartCommand:
patch("openjarvis.cli.quickstart_cmd.detect_hardware", return_value=hw),
patch("openjarvis.cli.quickstart_cmd.DEFAULT_CONFIG_PATH", config_path),
patch("openjarvis.cli.quickstart_cmd.DEFAULT_CONFIG_DIR", tmp_path),
- patch("openjarvis.cli.quickstart_cmd.generate_default_toml", return_value="[engine]\nnew = true\n"),
- patch("openjarvis.cli.quickstart_cmd.recommend_engine", return_value="ollama"),
- patch("openjarvis.cli.quickstart_cmd._check_engine_health", return_value=True),
- patch("openjarvis.cli.quickstart_cmd._check_model_available", return_value=True),
+ patch(
+ "openjarvis.cli.quickstart_cmd"
+ ".generate_default_toml",
+ return_value="[engine]\nnew = true\n",
+ ),
+ patch(
+ "openjarvis.cli.quickstart_cmd"
+ ".recommend_engine",
+ return_value="ollama",
+ ),
+ patch(
+ "openjarvis.cli.quickstart_cmd"
+ "._check_engine_health",
+ return_value=True,
+ ),
+ patch(
+ "openjarvis.cli.quickstart_cmd"
+ "._check_model_available",
+ return_value=True,
+ ),
patch("openjarvis.cli.quickstart_cmd._test_query", return_value="Hello!"),
):
runner = CliRunner()
@@ -109,11 +168,26 @@ class TestQuickstartCommand:
patch("openjarvis.cli.quickstart_cmd.detect_hardware", return_value=hw),
patch("openjarvis.cli.quickstart_cmd.DEFAULT_CONFIG_PATH", config_path),
patch("openjarvis.cli.quickstart_cmd.DEFAULT_CONFIG_DIR", tmp_path),
- patch("openjarvis.cli.quickstart_cmd.generate_default_toml", return_value="[engine]\n"),
- patch("openjarvis.cli.quickstart_cmd.recommend_engine", return_value="ollama"),
- patch("openjarvis.cli.quickstart_cmd._check_engine_health", return_value=False),
+ patch(
+ "openjarvis.cli.quickstart_cmd"
+ ".generate_default_toml",
+ return_value="[engine]\n",
+ ),
+ patch(
+ "openjarvis.cli.quickstart_cmd"
+ ".recommend_engine",
+ return_value="ollama",
+ ),
+ patch(
+ "openjarvis.cli.quickstart_cmd"
+ "._check_engine_health",
+ return_value=False,
+ ),
):
runner = CliRunner()
result = runner.invoke(cli, ["quickstart"])
assert result.exit_code == 1
- assert "engine" in result.output.lower() or "not reachable" in result.output.lower()
+ assert (
+ "engine" in result.output.lower()
+ or "not reachable" in result.output.lower()
+ )
diff --git a/tests/core/test_types.py b/tests/core/test_types.py
index 9a40d291..2d80ed34 100644
--- a/tests/core/test_types.py
+++ b/tests/core/test_types.py
@@ -54,6 +54,12 @@ class TestConversation:
assert len(conv.messages) == 3
assert [m.content for m in conv.window(2)] == ["b", "c"]
+ def test_window_zero_returns_empty(self) -> None:
+ conv = Conversation()
+ conv.add(Message(role=Role.USER, content="a"))
+ conv.add(Message(role=Role.ASSISTANT, content="b"))
+ assert conv.window(0) == []
+
def test_max_messages(self) -> None:
conv = Conversation(max_messages=2)
for i in range(5):
diff --git a/tests/engine/test_openai_compat.py b/tests/engine/test_openai_compat.py
index 59ab94a4..148594fd 100644
--- a/tests/engine/test_openai_compat.py
+++ b/tests/engine/test_openai_compat.py
@@ -46,6 +46,33 @@ class TestOpenAICompatGenerate:
assert result["content"] == "4"
assert result["usage"]["total_tokens"] == 9
+ def test_empty_choices_returns_graceful_fallback(
+ self, engine: VLLMEngine,
+ ) -> None:
+ with respx.mock:
+ respx.post(
+ "http://testhost:8000/v1/chat/completions"
+ ).mock(
+ return_value=httpx.Response(
+ 200,
+ json={
+ "choices": [],
+ "usage": {
+ "prompt_tokens": 5,
+ "completion_tokens": 0,
+ "total_tokens": 5,
+ },
+ "model": "test",
+ },
+ )
+ )
+ result = engine.generate(
+ [Message(role=Role.USER, content="hi")],
+ model="test",
+ )
+ assert result["content"] == ""
+ assert result["finish_reason"] == "error"
+
def test_generate_connection_error(self, engine: VLLMEngine) -> None:
with respx.mock:
respx.post("http://testhost:8000/v1/chat/completions").mock(
diff --git a/tests/engine/test_openai_compat_tools.py b/tests/engine/test_openai_compat_tools.py
index 4b50e2c1..18857748 100644
--- a/tests/engine/test_openai_compat_tools.py
+++ b/tests/engine/test_openai_compat_tools.py
@@ -203,6 +203,38 @@ class TestOllamaToolCalls:
)
assert "tools" in captured["body"]
+ def test_dict_arguments_serialized_to_json(self, respx_mock):
+ """Ollama returns arguments as dict — engine must serialize."""
+ respx_mock.post(
+ "http://localhost:11434/api/chat"
+ ).mock(
+ return_value=httpx.Response(200, json={
+ "message": {
+ "content": "",
+ "tool_calls": [{
+ "function": {
+ "name": "calculator",
+ "arguments": {"expression": "3*3"},
+ },
+ }],
+ },
+ "model": "test",
+ "prompt_eval_count": 5,
+ "eval_count": 3,
+ })
+ )
+ engine = OllamaEngine()
+ result = engine.generate(
+ [Message(role=Role.USER, content="3*3")],
+ model="test",
+ )
+ assert "tool_calls" in result
+ tc = result["tool_calls"][0]
+ assert isinstance(tc["arguments"], str)
+ assert json.loads(tc["arguments"]) == {
+ "expression": "3*3",
+ }
+
def test_no_tools_no_tools_key(self, respx_mock):
captured = {}
diff --git a/tests/evals/test_benchmark_datasets.py b/tests/evals/test_benchmark_datasets.py
index 4dd32311..a381a78c 100644
--- a/tests/evals/test_benchmark_datasets.py
+++ b/tests/evals/test_benchmark_datasets.py
@@ -238,12 +238,14 @@ class TestCLIFactories:
def test_build_dataset_unknown(self) -> None:
import click
+
from openjarvis.evals.cli import _build_dataset
with pytest.raises(click.ClickException, match="Unknown benchmark"):
_build_dataset("nonexistent")
def test_build_scorer_unknown(self) -> None:
import click
+
from openjarvis.evals.cli import _build_scorer
with pytest.raises(click.ClickException, match="Unknown benchmark"):
_build_scorer("nonexistent", _mock_backend(), "test-model")
diff --git a/tests/evals/test_display.py b/tests/evals/test_display.py
index 119d9054..320f9660 100644
--- a/tests/evals/test_display.py
+++ b/tests/evals/test_display.py
@@ -5,6 +5,8 @@ from __future__ import annotations
from io import StringIO
from pathlib import Path
+from rich.console import Console
+
from openjarvis.evals.core.display import (
print_banner,
print_completion,
@@ -15,7 +17,6 @@ from openjarvis.evals.core.display import (
print_suite_summary,
)
from openjarvis.evals.core.types import MetricStats, RunSummary
-from rich.console import Console
def _make_console() -> tuple[Console, StringIO]:
diff --git a/tests/learning/test_learning_api.py b/tests/learning/test_learning_api.py
index 93d058f3..6724af9b 100644
--- a/tests/learning/test_learning_api.py
+++ b/tests/learning/test_learning_api.py
@@ -2,10 +2,15 @@
from __future__ import annotations
-from fastapi import FastAPI
-from starlette.testclient import TestClient
+import pytest
-from openjarvis.server.api_routes import learning_router
+fastapi = pytest.importorskip("fastapi")
+starlette = pytest.importorskip("starlette")
+
+from fastapi import FastAPI # noqa: E402
+from starlette.testclient import TestClient # noqa: E402
+
+from openjarvis.server.api_routes import learning_router # noqa: E402
def _make_app() -> FastAPI:
diff --git a/tests/operators/test_operator_recipes.py b/tests/operators/test_operator_recipes.py
index 2b642bb7..455f2762 100644
--- a/tests/operators/test_operator_recipes.py
+++ b/tests/operators/test_operator_recipes.py
@@ -6,7 +6,14 @@ import pytest
from openjarvis.operators.loader import load_operator
-_OPERATORS_DIR = Path(__file__).parent.parent.parent / "src" / "openjarvis" / "recipes" / "data" / "operators"
+_OPERATORS_DIR = (
+ Path(__file__).parent.parent.parent
+ / "src"
+ / "openjarvis"
+ / "recipes"
+ / "data"
+ / "operators"
+)
class TestResearcherOperator:
diff --git a/tests/scheduler/test_scheduler.py b/tests/scheduler/test_scheduler.py
index e0ffe3c4..abdddeda 100644
--- a/tests/scheduler/test_scheduler.py
+++ b/tests/scheduler/test_scheduler.py
@@ -51,7 +51,10 @@ class TestScheduledTask:
def test_defaults(self):
task = ScheduledTask(
- id="x", prompt="p", schedule_type="once", schedule_value="2026-01-01T00:00:00"
+ id="x",
+ prompt="p",
+ schedule_type="once",
+ schedule_value="2026-01-01T00:00:00",
)
assert task.context_mode == "isolated"
assert task.status == "active"
diff --git a/tests/security/test_guardrails.py b/tests/security/test_guardrails.py
index 4b0d9176..5eee6dc8 100644
--- a/tests/security/test_guardrails.py
+++ b/tests/security/test_guardrails.py
@@ -123,6 +123,25 @@ class TestGuardrailsEngineInputScanning:
assert len(alerts) >= 1
assert any(a.data.get("direction") == "input" for a in alerts)
+ def test_redact_input_modifies_messages_sent_to_engine(
+ self,
+ ) -> None:
+ """REDACT mode on input must send redacted messages."""
+ mock = _make_mock_engine("OK")
+ ge = GuardrailsEngine(
+ mock, mode=RedactionMode.REDACT, scan_input=True,
+ )
+
+ secret = "my key sk-abc123def456ghi789jkl012"
+ messages = [Message(role=Role.USER, content=secret)]
+ ge.generate(messages, model="test")
+
+ # Engine should receive redacted content
+ call_args = mock.generate.call_args
+ sent_messages = call_args[0][0]
+ assert "sk-abc123" not in sent_messages[0].content
+ assert "[REDACTED:" in sent_messages[0].content
+
def test_scan_input_disabled(self) -> None:
"""Input messages are not scanned when scan_input=False."""
bus = EventBus(record_history=True)
diff --git a/tests/server/test_speech_routes.py b/tests/server/test_speech_routes.py
index cb6f452f..f4dfd00a 100644
--- a/tests/server/test_speech_routes.py
+++ b/tests/server/test_speech_routes.py
@@ -1,14 +1,14 @@
"""Tests for speech API endpoints."""
+from unittest.mock import MagicMock
+
import pytest
fastapi = pytest.importorskip("fastapi")
-from unittest.mock import MagicMock
+from fastapi.testclient import TestClient # noqa: E402
-from fastapi.testclient import TestClient
-
-from openjarvis.speech._stubs import TranscriptionResult
+from openjarvis.speech._stubs import TranscriptionResult # noqa: E402
@pytest.fixture
@@ -29,6 +29,7 @@ def mock_speech_backend():
@pytest.fixture
def app_with_speech(mock_speech_backend):
from fastapi import FastAPI
+
from openjarvis.server.api_routes import speech_router
app = FastAPI()
@@ -71,6 +72,7 @@ def test_health_endpoint(client):
def test_health_no_backend():
from fastapi import FastAPI
from fastapi.testclient import TestClient
+
from openjarvis.server.api_routes import speech_router
app = FastAPI()
diff --git a/tests/skills/test_bundled_skills.py b/tests/skills/test_bundled_skills.py
index 860165b9..44940e4a 100644
--- a/tests/skills/test_bundled_skills.py
+++ b/tests/skills/test_bundled_skills.py
@@ -9,7 +9,13 @@ import pytest
from openjarvis.skills.loader import load_skill
# Resolve the skills/builtin/ directory relative to the project root.
-BUILTIN_DIR = Path(__file__).resolve().parents[2] / "src" / "openjarvis" / "skills" / "data"
+BUILTIN_DIR = (
+ Path(__file__).resolve().parents[2]
+ / "src"
+ / "openjarvis"
+ / "skills"
+ / "data"
+)
# Collect all TOML files once so parametrized IDs are readable.
_toml_files = sorted(BUILTIN_DIR.glob("*.toml")) if BUILTIN_DIR.is_dir() else []
diff --git a/tests/telemetry/test_energy_apple.py b/tests/telemetry/test_energy_apple.py
index 89599a52..143d8e58 100644
--- a/tests/telemetry/test_energy_apple.py
+++ b/tests/telemetry/test_energy_apple.py
@@ -65,6 +65,7 @@ class TestEnergyMethod:
from openjarvis.telemetry.energy_apple import AppleEnergyMonitor
monitor = AppleEnergyMonitor.__new__(AppleEnergyMonitor)
+ monitor._zeus_ok = True
assert monitor.energy_method() == "zeus"
@@ -91,7 +92,8 @@ class TestSampleComponentBreakdown:
monitor = AppleEnergyMonitor.__new__(AppleEnergyMonitor)
monitor._poll_interval_ms = 50
monitor._monitor = mock_zeus_monitor
- monitor._initialized = True
+ monitor._zeus_ok = True
+ monitor._chip_name = "M1"
with monitor.sample() as result:
time.sleep(0.01)
@@ -123,7 +125,8 @@ class TestSampleComponentBreakdown:
monitor = AppleEnergyMonitor.__new__(AppleEnergyMonitor)
monitor._poll_interval_ms = 50
monitor._monitor = mock_zeus_monitor
- monitor._initialized = True
+ monitor._zeus_ok = True
+ monitor._chip_name = "M1"
with monitor.sample() as result:
pass
@@ -145,15 +148,19 @@ class TestSampleUninitialized:
monitor = AppleEnergyMonitor.__new__(AppleEnergyMonitor)
monitor._poll_interval_ms = 50
monitor._monitor = None
- monitor._initialized = False
+ monitor._zeus_ok = False
+ monitor._chip_name = "Apple Silicon"
+ monitor._tdp_watts = 20.0
with monitor.sample() as result:
pass
- assert result.energy_joules == 0.0
- assert result.cpu_energy_joules == 0.0
- assert result.gpu_energy_joules == 0.0
- assert result.dram_energy_joules == 0.0
- assert result.ane_energy_joules == 0.0
+ # CPU-time fallback produces small but non-zero estimates
+ assert result.energy_joules >= 0.0
+ assert result.cpu_energy_joules >= 0.0
+ assert result.gpu_energy_joules >= 0.0
+ assert result.dram_energy_joules >= 0.0
+ assert result.ane_energy_joules >= 0.0
assert result.duration_seconds >= 0
assert result.vendor == "apple"
+ assert result.energy_method == "cpu_time_estimate"
diff --git a/tests/telemetry/test_store_tokens_per_joule.py b/tests/telemetry/test_store_tokens_per_joule.py
index 3f3d3971..e6143297 100644
--- a/tests/telemetry/test_store_tokens_per_joule.py
+++ b/tests/telemetry/test_store_tokens_per_joule.py
@@ -7,8 +7,8 @@ import time
import pytest
from openjarvis.core.types import TelemetryRecord
-from openjarvis.telemetry.store import TelemetryStore
from openjarvis.telemetry.aggregator import TelemetryAggregator
+from openjarvis.telemetry.store import TelemetryStore
class TestTokensPerJouleStorage:
diff --git a/tests/templates/test_agent_templates.py b/tests/templates/test_agent_templates.py
index ac7be777..8264edd3 100644
--- a/tests/templates/test_agent_templates.py
+++ b/tests/templates/test_agent_templates.py
@@ -12,7 +12,13 @@ from openjarvis.templates.agent_templates import (
load_template,
)
-TEMPLATES_DIR = Path(__file__).resolve().parents[2] / "src" / "openjarvis" / "templates" / "data"
+TEMPLATES_DIR = (
+ Path(__file__).resolve().parents[2]
+ / "src"
+ / "openjarvis"
+ / "templates"
+ / "data"
+)
VALID_AGENT_TYPES = {"simple", "orchestrator", "native_react"}
diff --git a/tests/test_llm_optimizer.py b/tests/test_llm_optimizer.py
index 22088a0d..30fd514f 100644
--- a/tests/test_llm_optimizer.py
+++ b/tests/test_llm_optimizer.py
@@ -21,7 +21,6 @@ from openjarvis.optimize.types import (
TrialResult,
)
-
# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------
@@ -322,7 +321,10 @@ class TestProposeNext:
assert "What is 2+2?" in prompt
def test_empty_history(self) -> None:
- response = '```json\n{"params": {"agent.type": "simple"}, "reasoning": "start simple"}\n```'
+ response = (
+ '```json\n{"params": {"agent.type": "simple"},'
+ ' "reasoning": "start simple"}\n```'
+ )
backend = _make_mock_backend(response)
opt = LLMOptimizer(
search_space=_make_search_space(),
@@ -638,7 +640,7 @@ class TestFormatHistory:
assert "[FRONTIER]" in result
# Only t1 should be marked
lines = result.split("\n")
- frontier_lines = [l for l in lines if "[FRONTIER]" in l]
+ frontier_lines = [line for line in lines if "[FRONTIER]" in line]
assert len(frontier_lines) == 1
assert "t1" in frontier_lines[0]
@@ -957,7 +959,11 @@ class TestProposeMerge:
"""Tests for LLMOptimizer.propose_merge."""
def test_includes_candidates_in_prompt(self) -> None:
- response = '```json\n{"params": {"agent.type": "orchestrator"}, "reasoning": "merged"}\n```'
+ response = (
+ '```json\n{"params": {"agent.type":'
+ ' "orchestrator"}, "reasoning":'
+ ' "merged"}\n```'
+ )
backend = _make_mock_backend(response)
opt = LLMOptimizer(
search_space=_make_search_space(),
diff --git a/tests/test_optimize_store.py b/tests/test_optimize_store.py
index fa6fcfde..b0a53615 100644
--- a/tests/test_optimize_store.py
+++ b/tests/test_optimize_store.py
@@ -2,8 +2,6 @@
from __future__ import annotations
-import json
-
from openjarvis.optimize.store import OptimizationStore
from openjarvis.optimize.types import (
OptimizationRun,
@@ -361,7 +359,8 @@ class TestClose:
class TestNewFieldsPersistence:
- """Tests for sample_scores, structured_feedback, and pareto_frontier_ids roundtrip."""
+ """Tests for sample_scores, structured_feedback,
+ and pareto_frontier_ids roundtrip."""
def test_sample_scores_roundtrip(self, tmp_path) -> None:
store = OptimizationStore(tmp_path / "opt.db")
diff --git a/tests/test_optimize_types.py b/tests/test_optimize_types.py
index 28d9668b..4fd2cafa 100644
--- a/tests/test_optimize_types.py
+++ b/tests/test_optimize_types.py
@@ -3,7 +3,6 @@
from __future__ import annotations
from openjarvis.optimize.types import (
- ObjectiveSpec,
OptimizationRun,
SampleScore,
SearchDimension,
diff --git a/tests/test_optimizer_engine.py b/tests/test_optimizer_engine.py
index 141bc3a2..10be2aa1 100644
--- a/tests/test_optimizer_engine.py
+++ b/tests/test_optimizer_engine.py
@@ -2,8 +2,7 @@
from __future__ import annotations
-from pathlib import Path
-from unittest.mock import MagicMock, patch
+from unittest.mock import MagicMock
try:
import tomllib
@@ -147,7 +146,9 @@ class TestOptimizationEngineRun:
)
optimizer.propose_initial.return_value = initial_config
optimizer.propose_next.return_value = second_config
- optimizer.analyze_trial.return_value = TrialFeedback(summary_text="analysis text")
+ optimizer.analyze_trial.return_value = TrialFeedback(
+ summary_text="analysis text",
+ )
optimizer.optimizer_model = "test-model"
runner.run_trial.return_value = _sample_trial_result(
@@ -213,7 +214,9 @@ class TestOptimizationEngineRun:
optimizer.propose_initial.return_value = TrialConfig(
trial_id="t1", params={},
)
- optimizer.analyze_trial.return_value = TrialFeedback(summary_text="detailed analysis")
+ optimizer.analyze_trial.return_value = TrialFeedback(
+ summary_text="detailed analysis",
+ )
optimizer.optimizer_model = "m"
runner.run_trial.return_value = _sample_trial_result("t1", 0.8)
runner.benchmark = "b"
@@ -733,7 +736,7 @@ class TestTargetedAndMerge:
trial_runner=runner,
max_trials=4,
)
- run = engine.run()
+ engine.run()
# After trial 2 (trial_num=3, > 2), targeted should be used
assert optimizer.propose_targeted.called
@@ -779,7 +782,7 @@ class TestTargetedAndMerge:
trial_runner=runner,
max_trials=6,
)
- run = engine.run()
+ engine.run()
# Merge should be triggered at trial_num=5
assert optimizer.propose_merge.called
diff --git a/tests/test_pareto.py b/tests/test_pareto.py
index 1cfa7f23..c0542daf 100644
--- a/tests/test_pareto.py
+++ b/tests/test_pareto.py
@@ -2,9 +2,7 @@
from __future__ import annotations
-from typing import Any, Dict, Optional
-
-import pytest
+from typing import Any, Dict
from openjarvis.evals.core.types import MetricStats, RunSummary
from openjarvis.optimize.optimizer import compute_pareto_frontier
@@ -14,7 +12,6 @@ from openjarvis.optimize.types import (
TrialResult,
)
-
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
diff --git a/tests/test_personal_synthesizer.py b/tests/test_personal_synthesizer.py
index ba6dd1c5..85dd3406 100644
--- a/tests/test_personal_synthesizer.py
+++ b/tests/test_personal_synthesizer.py
@@ -2,7 +2,6 @@
from __future__ import annotations
-import tempfile
from pathlib import Path
from typing import Any, Dict
from unittest.mock import MagicMock
@@ -20,7 +19,6 @@ from openjarvis.optimize.personal.synthesizer import (
)
from openjarvis.traces.store import TraceStore
-
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
@@ -121,24 +119,38 @@ class TestPersonalBenchmarkSynthesizer:
def test_grouping_by_query_class(self, trace_store: TraceStore) -> None:
"""Same agent + same query prefix -> same group, so only one sample."""
- trace_store.save(
- _make_trace(trace_id="t1", query="What is 2+2?", agent="simple", feedback=0.8),
- )
- trace_store.save(
- _make_trace(trace_id="t2", query="What is 2+2?", agent="simple", feedback=0.95),
- )
+ trace_store.save(_make_trace(
+ trace_id="t1",
+ query="What is 2+2?",
+ agent="simple",
+ feedback=0.8,
+ ))
+ trace_store.save(_make_trace(
+ trace_id="t2",
+ query="What is 2+2?",
+ agent="simple",
+ feedback=0.95,
+ ))
synth = PersonalBenchmarkSynthesizer(trace_store)
bm = synth.synthesize()
# Should collapse into one sample (same group)
assert len(bm.samples) == 1
def test_picks_highest_feedback_per_group(self, trace_store: TraceStore) -> None:
- trace_store.save(
- _make_trace(trace_id="t1", query="Tell me a joke", agent="simple", feedback=0.7, result="bad joke"),
- )
- trace_store.save(
- _make_trace(trace_id="t2", query="Tell me a joke", agent="simple", feedback=0.99, result="great joke"),
- )
+ trace_store.save(_make_trace(
+ trace_id="t1",
+ query="Tell me a joke",
+ agent="simple",
+ feedback=0.7,
+ result="bad joke",
+ ))
+ trace_store.save(_make_trace(
+ trace_id="t2",
+ query="Tell me a joke",
+ agent="simple",
+ feedback=0.99,
+ result="great joke",
+ ))
synth = PersonalBenchmarkSynthesizer(trace_store)
bm = synth.synthesize()
assert len(bm.samples) == 1
@@ -150,9 +162,12 @@ class TestPersonalBenchmarkSynthesizer:
trace_store.save(
_make_trace(trace_id="t1", query="Hello", agent="simple", feedback=0.9),
)
- trace_store.save(
- _make_trace(trace_id="t2", query="Hello", agent="orchestrator", feedback=0.8),
- )
+ trace_store.save(_make_trace(
+ trace_id="t2",
+ query="Hello",
+ agent="orchestrator",
+ feedback=0.8,
+ ))
synth = PersonalBenchmarkSynthesizer(trace_store)
bm = synth.synthesize()
assert len(bm.samples) == 2
diff --git a/tests/test_rust_bridge.py b/tests/test_rust_bridge.py
index f48152e9..74102ae2 100644
--- a/tests/test_rust_bridge.py
+++ b/tests/test_rust_bridge.py
@@ -3,9 +3,6 @@
from __future__ import annotations
import json
-import os
-
-import pytest
class TestGetRustModule:
diff --git a/tests/test_system.py b/tests/test_system.py
index 54e6fd87..4e54f877 100644
--- a/tests/test_system.py
+++ b/tests/test_system.py
@@ -356,6 +356,48 @@ class TestJarvisSystemClose:
system.close()
scheduler.stop.assert_called_once()
+ def test_close_with_memory_backend(self):
+ engine = MagicMock()
+ mem = MagicMock()
+ system = JarvisSystem(
+ config=JarvisConfig(),
+ bus=EventBus(),
+ engine=engine,
+ engine_key="mock",
+ model="test",
+ memory_backend=mem,
+ )
+ system.close()
+ mem.close.assert_called_once()
+
+ def test_close_with_session_store(self):
+ engine = MagicMock()
+ sess = MagicMock()
+ system = JarvisSystem(
+ config=JarvisConfig(),
+ bus=EventBus(),
+ engine=engine,
+ engine_key="mock",
+ model="test",
+ session_store=sess,
+ )
+ system.close()
+ sess.close.assert_called_once()
+
+ def test_close_with_workflow_engine(self):
+ engine = MagicMock()
+ wf = MagicMock()
+ system = JarvisSystem(
+ config=JarvisConfig(),
+ bus=EventBus(),
+ engine=engine,
+ engine_key="mock",
+ model="test",
+ workflow_engine=wf,
+ )
+ system.close()
+ wf.close.assert_called_once()
+
def test_system_fields_default_none(self):
engine = MagicMock()
system = JarvisSystem(
diff --git a/tests/test_trace_judge.py b/tests/test_trace_judge.py
index d807def4..8a74edff 100644
--- a/tests/test_trace_judge.py
+++ b/tests/test_trace_judge.py
@@ -7,7 +7,6 @@ from unittest.mock import MagicMock
from openjarvis.core.types import StepType, Trace, TraceStep
from openjarvis.optimize.feedback.judge import TraceJudge, _parse_score
-
# ---------------------------------------------------------------------------
# _parse_score unit tests
# ---------------------------------------------------------------------------
@@ -92,7 +91,9 @@ class TestScoreTrace:
def test_returns_score_and_feedback(self) -> None:
backend = MagicMock()
- backend.generate.return_value = "Score: 0.85\nGood reasoning and correct answer."
+ backend.generate.return_value = (
+ "Score: 0.85\nGood reasoning and correct answer."
+ )
judge = TraceJudge(backend=backend, model="judge-model")
trace = _make_trace()
diff --git a/tests/traces/test_analyzer_energy.py b/tests/traces/test_analyzer_energy.py
index f11585eb..7c269535 100644
--- a/tests/traces/test_analyzer_energy.py
+++ b/tests/traces/test_analyzer_energy.py
@@ -7,7 +7,7 @@ import time
import pytest
from openjarvis.core.types import StepType, Trace, TraceStep
-from openjarvis.traces.analyzer import StepTypeStats, TraceAnalyzer, TraceSummary
+from openjarvis.traces.analyzer import StepTypeStats, TraceAnalyzer
from openjarvis.traces.store import TraceStore
@@ -71,8 +71,14 @@ class TestTraceSummaryEnergyFields:
def test_step_type_stats(self, tmp_path):
store = TraceStore(db_path=tmp_path / "traces.db")
trace = _make_trace([
- _gen_step(energy=10.0, duration=2.0, prompt_tokens=100, completion_tokens=50),
- _gen_step(energy=20.0, duration=4.0, prompt_tokens=80, completion_tokens=40),
+ _gen_step(
+ energy=10.0, duration=2.0,
+ prompt_tokens=100, completion_tokens=50,
+ ),
+ _gen_step(
+ energy=20.0, duration=4.0,
+ prompt_tokens=80, completion_tokens=40,
+ ),
_tool_step(duration=0.5),
_tool_step(duration=1.5),
])
diff --git a/uv.lock b/uv.lock
index 230605aa..9863bc35 100644
--- a/uv.lock
+++ b/uv.lock
@@ -562,14 +562,24 @@ sdist = { url = "https://files.pythonhosted.org/packages/92/88/b8527e1b00c1811db
wheels = [
{ url = "https://files.pythonhosted.org/packages/6a/80/ea4ead0c5d52a9828692e7df20f0eafe8d26e671ce4883a0a146bb91049e/caio-0.9.25-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ca6c8ecda611478b6016cb94d23fd3eb7124852b985bdec7ecaad9f3116b9619", size = 36836, upload-time = "2025-12-26T15:22:04.662Z" },
{ url = "https://files.pythonhosted.org/packages/17/b9/36715c97c873649d1029001578f901b50250916295e3dddf20c865438865/caio-0.9.25-cp310-cp310-manylinux2010_x86_64.manylinux2014_x86_64.manylinux_2_12_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:db9b5681e4af8176159f0d6598e73b2279bb661e718c7ac23342c550bd78c241", size = 79695, upload-time = "2025-12-26T15:22:18.818Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/ab/07080ecb1adb55a02cbd8ec0126aa8e43af343ffabb6a71125b42670e9a1/caio-0.9.25-cp310-cp310-manylinux_2_34_aarch64.whl", hash = "sha256:bf61d7d0c4fd10ffdd98ca47f7e8db4d7408e74649ffaf4bef40b029ada3c21b", size = 79457, upload-time = "2026-03-04T22:08:16.024Z" },
+ { url = "https://files.pythonhosted.org/packages/88/95/dd55757bb671eb4c376e006c04e83beb413486821f517792ea603ef216e9/caio-0.9.25-cp310-cp310-manylinux_2_34_x86_64.whl", hash = "sha256:ab52e5b643f8bbd64a0605d9412796cd3464cb8ca88593b13e95a0f0b10508ae", size = 77705, upload-time = "2026-03-04T22:08:17.202Z" },
{ url = "https://files.pythonhosted.org/packages/ec/90/543f556fcfcfa270713eef906b6352ab048e1e557afec12925c991dc93c2/caio-0.9.25-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d6956d9e4a27021c8bd6c9677f3a59eb1d820cc32d0343cea7961a03b1371965", size = 36839, upload-time = "2025-12-26T15:21:40.267Z" },
{ url = "https://files.pythonhosted.org/packages/51/3b/36f3e8ec38dafe8de4831decd2e44c69303d2a3892d16ceda42afed44e1b/caio-0.9.25-cp311-cp311-manylinux2010_x86_64.manylinux2014_x86_64.manylinux_2_12_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bf84bfa039f25ad91f4f52944452a5f6f405e8afab4d445450978cd6241d1478", size = 80255, upload-time = "2025-12-26T15:22:20.271Z" },
+ { url = "https://files.pythonhosted.org/packages/df/ce/65e64867d928e6aff1b4f0e12dba0ef6d5bf412c240dc1df9d421ac10573/caio-0.9.25-cp311-cp311-manylinux_2_34_aarch64.whl", hash = "sha256:ae3d62587332bce600f861a8de6256b1014d6485cfd25d68c15caf1611dd1f7c", size = 80052, upload-time = "2026-03-04T22:08:20.402Z" },
+ { url = "https://files.pythonhosted.org/packages/46/90/e278863c47e14ec58309aa2e38a45882fbe67b4cc29ec9bc8f65852d3e45/caio-0.9.25-cp311-cp311-manylinux_2_34_x86_64.whl", hash = "sha256:fc220b8533dcf0f238a6b1a4a937f92024c71e7b10b5a2dfc1c73604a25709bc", size = 78273, upload-time = "2026-03-04T22:08:21.368Z" },
{ url = "https://files.pythonhosted.org/packages/d3/25/79c98ebe12df31548ba4eaf44db11b7cad6b3e7b4203718335620939083c/caio-0.9.25-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fb7ff95af4c31ad3f03179149aab61097a71fd85e05f89b4786de0359dffd044", size = 36983, upload-time = "2025-12-26T15:21:36.075Z" },
{ url = "https://files.pythonhosted.org/packages/a3/2b/21288691f16d479945968a0a4f2856818c1c5be56881d51d4dac9b255d26/caio-0.9.25-cp312-cp312-manylinux2010_x86_64.manylinux2014_x86_64.manylinux_2_12_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:97084e4e30dfa598449d874c4d8e0c8d5ea17d2f752ef5e48e150ff9d240cd64", size = 82012, upload-time = "2025-12-26T15:22:20.983Z" },
+ { url = "https://files.pythonhosted.org/packages/03/c4/8a1b580875303500a9c12b9e0af58cb82e47f5bcf888c2457742a138273c/caio-0.9.25-cp312-cp312-manylinux_2_34_aarch64.whl", hash = "sha256:4fa69eba47e0f041b9d4f336e2ad40740681c43e686b18b191b6c5f4c5544bfb", size = 81502, upload-time = "2026-03-04T22:08:22.381Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/1c/0fe770b8ffc8362c48134d1592d653a81a3d8748d764bec33864db36319d/caio-0.9.25-cp312-cp312-manylinux_2_34_x86_64.whl", hash = "sha256:6bebf6f079f1341d19f7386db9b8b1f07e8cc15ae13bfdaff573371ba0575d69", size = 80200, upload-time = "2026-03-04T22:08:23.382Z" },
{ url = "https://files.pythonhosted.org/packages/31/57/5e6ff127e6f62c9f15d989560435c642144aa4210882f9494204bc892305/caio-0.9.25-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d6c2a3411af97762a2b03840c3cec2f7f728921ff8adda53d7ea2315a8563451", size = 36979, upload-time = "2025-12-26T15:21:35.484Z" },
{ url = "https://files.pythonhosted.org/packages/a3/9f/f21af50e72117eb528c422d4276cbac11fb941b1b812b182e0a9c70d19c5/caio-0.9.25-cp313-cp313-manylinux2010_x86_64.manylinux2014_x86_64.manylinux_2_12_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0998210a4d5cd5cb565b32ccfe4e53d67303f868a76f212e002a8554692870e6", size = 81900, upload-time = "2025-12-26T15:22:21.919Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/12/c39ae2a4037cb10ad5eb3578eb4d5f8c1a2575c62bba675f3406b7ef0824/caio-0.9.25-cp313-cp313-manylinux_2_34_aarch64.whl", hash = "sha256:1a177d4777141b96f175fe2c37a3d96dec7911ed9ad5f02bac38aaa1c936611f", size = 81523, upload-time = "2026-03-04T22:08:25.187Z" },
+ { url = "https://files.pythonhosted.org/packages/22/59/f8f2e950eb4f1a5a3883e198dca514b9d475415cb6cd7b78b9213a0dd45a/caio-0.9.25-cp313-cp313-manylinux_2_34_x86_64.whl", hash = "sha256:9ed3cfb28c0e99fec5e208c934e5c157d0866aa9c32aa4dc5e9b6034af6286b7", size = 80243, upload-time = "2026-03-04T22:08:26.449Z" },
{ url = "https://files.pythonhosted.org/packages/69/ca/a08fdc7efdcc24e6a6131a93c85be1f204d41c58f474c42b0670af8c016b/caio-0.9.25-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fab6078b9348e883c80a5e14b382e6ad6aabbc4429ca034e76e730cf464269db", size = 36978, upload-time = "2025-12-26T15:21:41.055Z" },
{ url = "https://files.pythonhosted.org/packages/5e/6c/d4d24f65e690213c097174d26eda6831f45f4734d9d036d81790a27e7b78/caio-0.9.25-cp314-cp314-manylinux2010_x86_64.manylinux2014_x86_64.manylinux_2_12_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:44a6b58e52d488c75cfaa5ecaa404b2b41cc965e6c417e03251e868ecd5b6d77", size = 81832, upload-time = "2025-12-26T15:22:22.757Z" },
+ { url = "https://files.pythonhosted.org/packages/87/a4/e534cf7d2d0e8d880e25dd61e8d921ffcfe15bd696734589826f5a2df727/caio-0.9.25-cp314-cp314-manylinux_2_34_aarch64.whl", hash = "sha256:628a630eb7fb22381dd8e3c8ab7f59e854b9c806639811fc3f4310c6bd711d79", size = 81565, upload-time = "2026-03-04T22:08:27.483Z" },
+ { url = "https://files.pythonhosted.org/packages/3f/ed/bf81aeac1d290017e5e5ac3e880fd56ee15e50a6d0353986799d1bc5cfd5/caio-0.9.25-cp314-cp314-manylinux_2_34_x86_64.whl", hash = "sha256:0ba16aa605ccb174665357fc729cf500679c2d94d5f1458a6f0d5ca48f2060a7", size = 80071, upload-time = "2026-03-04T22:08:28.751Z" },
{ url = "https://files.pythonhosted.org/packages/86/93/1f76c8d1bafe3b0614e06b2195784a3765bbf7b0a067661af9e2dd47fc33/caio-0.9.25-py3-none-any.whl", hash = "sha256:06c0bb02d6b929119b1cfbe1ca403c768b2013a369e2db46bfa2a5761cf82e40", size = 19087, upload-time = "2025-12-26T15:22:00.221Z" },
]
@@ -2637,14 +2647,14 @@ name = "mlx-lm"
version = "0.30.7"
source = { registry = "https://pypi.org/simple" }
dependencies = [
- { name = "jinja2", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'emscripten' and sys_platform != 'win32')" },
+ { name = "jinja2", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')" },
{ name = "mlx", marker = "sys_platform == 'darwin'" },
- { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
- { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and sys_platform != 'emscripten' and sys_platform != 'win32'" },
- { name = "protobuf", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'emscripten' and sys_platform != 'win32')" },
- { name = "pyyaml", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'emscripten' and sys_platform != 'win32')" },
- { name = "sentencepiece", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'emscripten' and sys_platform != 'win32')" },
- { name = "transformers", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'emscripten' and sys_platform != 'win32')" },
+ { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' and sys_platform != 'linux'" },
+ { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32'" },
+ { name = "protobuf", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')" },
+ { name = "pyyaml", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')" },
+ { name = "sentencepiece", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')" },
+ { name = "transformers", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')" },
]
sdist = { url = "https://files.pythonhosted.org/packages/66/0d/56542e2ae13ec6f542d3977d7cff89a205d4f6c5122e0ce23f33265f61c9/mlx_lm-0.30.7.tar.gz", hash = "sha256:e5f31ac58d9f2381f28e1ba639ff903e64f7cff1bdc245c0bc97f72264be329c", size = 275764, upload-time = "2026-02-12T18:41:11.86Z" }
wheels = [
@@ -3320,7 +3330,6 @@ source = { editable = "." }
dependencies = [
{ name = "click" },
{ name = "httpx" },
- { name = "pynvml" },
{ name = "rich" },
{ name = "tomli", marker = "python_full_version < '3.11'" },
]
@@ -3508,7 +3517,6 @@ requires-dist = [
{ name = "pydantic", marker = "extra == 'server'", specifier = ">=2.0" },
{ name = "pymessenger", marker = "extra == 'channel-messenger'", specifier = ">=0.0.7" },
{ name = "pynostr", marker = "extra == 'channel-nostr'", specifier = ">=0.6" },
- { name = "pynvml", specifier = ">=13.0.1" },
{ name = "pynvml", marker = "extra == 'energy-all'", specifier = ">=12.0" },
{ name = "pynvml", marker = "extra == 'gpu-metrics'", specifier = ">=12.0" },
{ name = "pytest", marker = "extra == 'dev'", specifier = ">=8" },