Merge pull request #12 from HazyResearch/fix/codebase-cleanup-bugs-and-lint

fix: codebase cleanup — bug fixes, test hardening, and lint
This commit is contained in:
Jon Saad-Falcon
2026-03-04 22:02:36 -08:00
committed by GitHub
63 changed files with 908 additions and 260 deletions
+50
View File
@@ -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
-1
View File
@@ -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'",
]
+4 -1
View File
@@ -183,7 +183,10 @@ class BaseAgent(ABC):
begins directly with reasoning text followed by ``</think>``.
"""
# Full <think>...</think> blocks
text = re.sub(r"<think>.*?</think>\s*", "", text, flags=re.DOTALL | re.IGNORECASE)
text = re.sub(
r"<think>.*?</think>\s*", "", text,
flags=re.DOTALL | re.IGNORECASE,
)
# Leading content before a bare </think> (no opening tag)
text = re.sub(r"^.*?</think>\s*", "", text, flags=re.DOTALL | re.IGNORECASE)
return text.strip()
+35 -21
View File
@@ -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."""
+3
View File
@@ -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", "")
+3
View File
@@ -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
+6
View File
@@ -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:
+1 -2
View File
@@ -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:
+3 -2
View File
@@ -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,
+10 -5
View File
@@ -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]'
)
+3 -1
View File
@@ -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)
+2
View File
@@ -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:]
+9 -1
View File
@@ -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 "",
+13 -6
View File
@@ -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(
+8 -3
View File
@@ -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="",
+5 -1
View File
@@ -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)),
+17 -4
View File
@@ -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")
+19 -4
View File
@@ -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, """\
+2 -2
View File
@@ -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
+4 -1
View File
@@ -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"
+4 -1
View File
@@ -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",
+5 -3
View File
@@ -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(
-1
View File
@@ -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
+47 -21
View File
@@ -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) + ' <span class="munit"></span>';
document.getElementById('energy-kwh').textContent =
(eWh / 1000).toFixed(4) + ' kWh of cloud datacenter energy avoided';
document.getElementById('flops-val').innerHTML = fmt(fl) + ' <span class="munit"></span>';
document.getElementById('flops-sub').textContent = 'cloud compute operations not needed';
document.getElementById('co2-val').innerHTML = fmtCO2(co2) + ' <span class="munit"></span>';
document.getElementById('energy-joules')
.innerHTML = fmtEnergy(ej) +
' <span class="munit"></span>';
document.getElementById('energy-kwh')
.textContent = (eWh / 1000).toFixed(4) +
' kWh of cloud datacenter energy avoided';
document.getElementById('flops-val')
.innerHTML = fmt(fl) +
' <span class="munit"></span>';
document.getElementById('flops-sub')
.textContent =
'cloud compute operations not needed';
document.getElementById('co2-val')
.innerHTML = fmtCO2(co2) +
' <span class="munit"></span>';
} catch (e) {
document.getElementById('status-text').textContent = 'Connection error — retrying...';
document.getElementById('status-text')
.textContent = 'Connection error — retrying...';
}
}
+44 -39
View File
@@ -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)
+8 -2
View File
@@ -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
+25
View File
@@ -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")
+67 -14
View File
@@ -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",
},
+19 -4
View File
@@ -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
+10 -2
View File
@@ -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"
+11 -2
View File
@@ -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):
+55 -13
View File
@@ -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
+19 -4
View File
@@ -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
+9 -2
View File
@@ -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
+10 -2
View File
@@ -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"
+9 -2
View File
@@ -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
+7 -1
View File
@@ -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()
+93 -19
View File
@@ -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()
)
+6
View File
@@ -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):
+27
View File
@@ -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(
+32
View File
@@ -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 = {}
+2
View File
@@ -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")
+2 -1
View File
@@ -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]:
+8 -3
View File
@@ -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:
+8 -1
View File
@@ -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:
+4 -1
View File
@@ -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"
+19
View File
@@ -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)
+6 -4
View File
@@ -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()
+7 -1
View File
@@ -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 []
+15 -8
View File
@@ -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"
@@ -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:
+7 -1
View File
@@ -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"}
+10 -4
View File
@@ -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(),
+2 -3
View File
@@ -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")
-1
View File
@@ -3,7 +3,6 @@
from __future__ import annotations
from openjarvis.optimize.types import (
ObjectiveSpec,
OptimizationRun,
SampleScore,
SearchDimension,
+9 -6
View File
@@ -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
+1 -4
View File
@@ -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
# ---------------------------------------------------------------------------
+32 -17
View File
@@ -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
-3
View File
@@ -3,9 +3,6 @@
from __future__ import annotations
import json
import os
import pytest
class TestGetRustModule:
+42
View File
@@ -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(
+3 -2
View File
@@ -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()
+9 -3
View File
@@ -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),
])
Generated
+17 -9
View File
@@ -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" },