mirror of
https://github.com/open-jarvis/OpenJarvis.git
synced 2026-07-30 19:02:16 +00:00
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:
@@ -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
|
||||
@@ -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'",
|
||||
]
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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", "")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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]'
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:]
|
||||
|
||||
|
||||
|
||||
@@ -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 "",
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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="",
|
||||
|
||||
@@ -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)),
|
||||
|
||||
@@ -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,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, """\
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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",
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
)
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 = {}
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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]:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 []
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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"}
|
||||
|
||||
|
||||
@@ -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,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")
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from openjarvis.optimize.types import (
|
||||
ObjectiveSpec,
|
||||
OptimizationRun,
|
||||
SampleScore,
|
||||
SearchDimension,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@@ -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,9 +3,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
class TestGetRustModule:
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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),
|
||||
])
|
||||
|
||||
@@ -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" },
|
||||
|
||||
Reference in New Issue
Block a user