fix: resolve all lint errors after merge with main

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jon Saad-Falcon
2026-03-27 13:33:42 -07:00
co-authored by Claude Opus 4.6
parent bce8a2fe12
commit 869d9256fd
335 changed files with 6653 additions and 4273 deletions
+8 -2
View File
@@ -1,10 +1,16 @@
"""Agent-to-Agent protocol — Google A2A spec implementation."""
from openjarvis.a2a.client import A2AClient
from openjarvis.a2a.protocol import A2ARequest, A2AResponse, A2ATask, AgentCard
from openjarvis.a2a.server import A2AServer
from openjarvis.a2a.tool import A2AAgentTool
__all__ = [
"A2AAgentTool", "A2AClient", "A2ARequest", "A2AResponse",
"A2AServer", "A2ATask", "AgentCard",
"A2AAgentTool",
"A2AClient",
"A2ARequest",
"A2AResponse",
"A2AServer",
"A2ATask",
"AgentCard",
]
+4
View File
@@ -22,6 +22,7 @@ class A2AClient:
def discover(self) -> AgentCard:
"""Fetch the agent card from /.well-known/agent.json."""
import httpx
resp = httpx.get(
f"{self._base_url}/.well-known/agent.json",
timeout=self._timeout,
@@ -41,6 +42,7 @@ class A2AClient:
def send_task(self, input_text: str, **kwargs: Any) -> A2ATask:
"""Send a task to the remote agent and return the result."""
import httpx
request = A2ARequest(
method="tasks/send",
params={
@@ -69,6 +71,7 @@ class A2AClient:
def get_task(self, task_id: str) -> A2ATask:
"""Get the status of a previously submitted task."""
import httpx
request = A2ARequest(
method="tasks/get",
params={"id": task_id},
@@ -90,6 +93,7 @@ class A2AClient:
def cancel_task(self, task_id: str) -> A2ATask:
"""Cancel a running task."""
import httpx
request = A2ARequest(
method="tasks/cancel",
params={"id": task_id},
+4
View File
@@ -21,6 +21,7 @@ class TaskState(str, Enum):
@dataclass(slots=True)
class AgentCard:
"""Agent discovery card served at /.well-known/agent.json."""
name: str
description: str = ""
url: str = ""
@@ -44,6 +45,7 @@ class AgentCard:
@dataclass
class A2ATask:
"""An A2A task with state machine."""
task_id: str = field(default_factory=lambda: uuid.uuid4().hex[:16])
state: TaskState = TaskState.SUBMITTED
input_text: str = ""
@@ -65,6 +67,7 @@ class A2ATask:
@dataclass(slots=True)
class A2ARequest:
"""JSON-RPC 2.0 request for A2A."""
method: str
params: Dict[str, Any] = field(default_factory=dict)
request_id: str = field(default_factory=lambda: uuid.uuid4().hex[:8])
@@ -84,6 +87,7 @@ class A2ARequest:
@dataclass(slots=True)
class A2AResponse:
"""JSON-RPC 2.0 response for A2A."""
result: Any = None
error: Optional[Dict[str, Any]] = None
request_id: str = ""
+3 -1
View File
@@ -102,7 +102,9 @@ class A2AServer:
return A2AResponse(result=task.to_dict(), request_id=req_id).to_dict()
def _handle_task_cancel(
self, params: Dict[str, Any], req_id: str,
self,
params: Dict[str, Any],
req_id: str,
) -> Dict[str, Any]:
"""Handle tasks/cancel — cancel a running task."""
task_id = params.get("id", "")
+1 -3
View File
@@ -55,9 +55,7 @@ def classify_query(text: str) -> str:
# ChannelAgent
# ---------------------------------------------------------------------------
_ESCALATION_TEMPLATE = (
"{preview}...\n\n---\nFull report ready — open in OpenJarvis:\nopenjarvis://research/{session_id}"
)
_ESCALATION_TEMPLATE = "{preview}...\n\n---\nFull report ready — open in OpenJarvis:\nopenjarvis://research/{session_id}"
_LONG_RESPONSE_THRESHOLD = 500
+1 -3
View File
@@ -35,9 +35,7 @@ _OUTPUT_END = "---OPENJARVIS_OUTPUT_END---"
_RUNNER_SRC = Path(__file__).resolve().parent / "claude_code_runner"
if not _RUNNER_SRC.exists():
_RUNNER_SRC = (
Path(__file__).resolve().parents[2]
/ "_node_modules"
/ "claude_code_runner"
Path(__file__).resolve().parents[2] / "_node_modules" / "claude_code_runner"
)
+2 -3
View File
@@ -30,6 +30,7 @@ def _tc_args(tc: dict) -> str:
return tc["function"]["arguments"]
return tc["arguments"]
DEEP_RESEARCH_SYSTEM_PROMPT = """\
/no_think
You are a deep research agent with access to a personal knowledge base \
@@ -201,9 +202,7 @@ class DeepResearchAgent(ToolUsingAgent):
# If content is empty but we have prior tool results,
# force one more generation without tools to synthesize
if not content.strip() and all_tool_results:
messages.append(
Message(role=Role.ASSISTANT, content=content)
)
messages.append(Message(role=Role.ASSISTANT, content=content))
messages.append(
Message(
role=Role.USER,
+127 -77
View File
@@ -87,7 +87,7 @@ class AgentExecutor:
agent_cls = AgentRegistry.get(agent_type)
agent = agent_cls(
engine=getattr(self._manager, '_engine', None),
engine=getattr(self._manager, "_engine", None),
system_prompt=system_prompt,
bus=self._bus,
)
@@ -113,10 +113,13 @@ class AgentExecutor:
logger.error("Agent %s not found", agent_id)
return
self._bus.publish(EventType.AGENT_TICK_START, {
"agent_id": agent_id,
"agent_name": agent["name"],
})
self._bus.publish(
EventType.AGENT_TICK_START,
{
"agent_id": agent_id,
"agent_name": agent["name"],
},
)
# Activity tracking: subscribe to tool/inference events
def _on_activity(event: Any) -> None:
@@ -131,14 +134,16 @@ class AgentExecutor:
def _on_tool_start(event: Any) -> None:
if event.data.get("agent") == agent_id:
trace_steps.append({
"type": "tool_call",
"input": {
"tool": event.data.get("tool"),
"args": event.data.get("args"),
},
"start_time": event.timestamp,
})
trace_steps.append(
{
"type": "tool_call",
"input": {
"tool": event.data.get("tool"),
"args": event.data.get("args"),
},
"start_time": event.timestamp,
}
)
def _on_tool_end(event: Any) -> None:
if event.data.get("agent") == agent_id and trace_steps:
@@ -175,8 +180,13 @@ class AgentExecutor:
if self._trace_store:
self._save_trace(
agent_id, agent, result, error_info,
tick_start, tick_duration, trace_steps,
agent_id,
agent,
result,
error_info,
tick_start,
tick_duration,
trace_steps,
)
def _run_with_retries(self, agent: dict) -> AgentResult:
@@ -193,7 +203,11 @@ class AgentExecutor:
delay = retry_delay(attempt)
logger.info(
"Agent %s tick retry %d/%d in %ds: %s",
agent["id"], attempt + 1, _MAX_RETRIES, delay, e,
agent["id"],
attempt + 1,
_MAX_RETRIES,
delay,
e,
)
time.sleep(delay)
except Exception as e:
@@ -203,7 +217,11 @@ class AgentExecutor:
delay = retry_delay(attempt)
logger.info(
"Agent %s tick retry %d/%d in %ds: %s",
agent["id"], attempt + 1, _MAX_RETRIES, delay, e,
agent["id"],
attempt + 1,
_MAX_RETRIES,
delay,
e,
)
time.sleep(delay)
@@ -225,17 +243,16 @@ class AgentExecutor:
engine = self._system.engine if self._system else None
if engine is None:
raise FatalError("No engine available in JarvisSystem")
model = config.get("model") or (
self._system.model
if self._system else ""
)
model = config.get("model") or (self._system.model if self._system else "")
if not model:
raise FatalError("No model configured for agent")
logger.info(
"Agent %s [%s]: using model=%s, engine=%s",
agent["name"], agent["id"],
model, type(engine).__name__,
agent["name"],
agent["id"],
model,
type(engine).__name__,
)
self._set_activity(agent["id"], f"Loading model {model}...")
@@ -289,7 +306,9 @@ class AgentExecutor:
if tool_instances:
logger.info(
"Agent %s: resolved %d/%d tools",
agent["name"], len(tool_instances), len(tool_names),
agent["name"],
len(tool_instances),
len(tool_names),
)
# Construct agent instance
@@ -317,7 +336,8 @@ class AgentExecutor:
self._manager.mark_message_delivered(m["id"])
logger.info(
"Agent %s: delivering %d pending message(s)",
agent["name"], len(pending),
agent["name"],
len(pending),
)
self._set_activity(
agent["id"],
@@ -325,8 +345,7 @@ class AgentExecutor:
)
else:
logger.info(
"Agent %s: no pending messages, running with "
"instruction only",
"Agent %s: no pending messages, running with instruction only",
agent["name"],
)
@@ -363,7 +382,8 @@ class AgentExecutor:
if query:
results = self._system.memory_backend.retrieve(
query, top_k=ctx_cfg.top_k,
query,
top_k=ctx_cfg.top_k,
)
memory_results = [
r for r in results if r.score >= ctx_cfg.min_score
@@ -383,7 +403,8 @@ class AgentExecutor:
self._set_activity(agent["id"], "Generating response...")
logger.info(
"Agent %s: calling agent.run() with %d chars input",
agent["name"], len(input_text),
agent["name"],
len(input_text),
)
_t0 = time.time()
result = agent_instance.run(input_text, context=agent_ctx)
@@ -392,7 +413,8 @@ class AgentExecutor:
# Qwen3.5 thinking mode consuming all tokens).
if not (result.content or "").strip():
self._set_activity(
agent["id"], "Retrying (empty response)...",
agent["id"],
"Retrying (empty response)...",
)
logger.warning(
"Agent %s: empty content, retrying once",
@@ -404,7 +426,8 @@ class AgentExecutor:
logger.info(
"Agent %s: agent.run() completed in %.1fs, "
"content_len=%d, turns=%d, tokens=%s",
agent["name"], _elapsed,
agent["name"],
_elapsed,
len(result.content or ""),
result.turns,
result.metadata.get("total_tokens", "?"),
@@ -434,7 +457,9 @@ class AgentExecutor:
"suggested_action": suggest_action(error),
"stack_trace_summary": "".join(
traceback.format_exception(type(error), error, error.__traceback__)[-3:]
)[:1000] if error.__traceback__ else "",
)[:1000]
if error.__traceback__
else "",
}
def _finalize_tick(
@@ -449,9 +474,9 @@ class AgentExecutor:
if error is None:
# Success
logger.info(
"Tick succeeded for agent %s in %.1fs, "
"response_len=%d",
agent_id, duration,
"Tick succeeded for agent %s in %.1fs, response_len=%d",
agent_id,
duration,
len(result.content or "") if result else 0,
)
self._manager.end_tick(agent_id)
@@ -466,7 +491,8 @@ class AgentExecutor:
)
in_tokens = result.metadata.get("prompt_tokens", 0)
out_tokens = result.metadata.get(
"completion_tokens", 0,
"completion_tokens",
0,
)
cost = result.metadata.get("cost", 0.0)
budget_kwargs: dict[str, Any] = {"stall_retries": 0}
@@ -481,7 +507,8 @@ class AgentExecutor:
self._manager.update_agent(agent_id, **budget_kwargs)
self._manager.update_summary_memory(
agent_id, result.content[:2000],
agent_id,
result.content[:2000],
)
self._manager.store_agent_response(agent_id, result.content[:2000])
@@ -498,49 +525,68 @@ class AgentExecutor:
exceeded = True
if exceeded:
self._manager.update_agent(agent_id, status="budget_exceeded")
self._bus.publish(EventType.AGENT_BUDGET_EXCEEDED, {
"agent_id": agent_id,
"total_cost": agent_data["total_cost"],
"total_tokens": agent_data["total_tokens"],
"max_cost": max_cost,
"max_tokens": max_tokens,
})
self._bus.publish(EventType.AGENT_TICK_END, {
"agent_id": agent_id,
"duration": duration,
"status": "ok",
})
self._bus.publish(
EventType.AGENT_BUDGET_EXCEEDED,
{
"agent_id": agent_id,
"total_cost": agent_data["total_cost"],
"total_tokens": agent_data["total_tokens"],
"max_cost": max_cost,
"max_tokens": max_tokens,
},
)
self._bus.publish(
EventType.AGENT_TICK_END,
{
"agent_id": agent_id,
"duration": duration,
"status": "ok",
},
)
elif isinstance(error, EscalateError):
logger.warning(
"Tick escalated for agent %s after %.1fs: %s",
agent_id, duration, error,
agent_id,
duration,
error,
)
self._manager.end_tick(agent_id)
self._manager.update_agent(agent_id, status="needs_attention")
self._bus.publish(EventType.AGENT_TICK_ERROR, {
"agent_id": agent_id,
"error": str(error),
"error_type": "escalate",
"duration": duration,
})
self._bus.publish(
EventType.AGENT_TICK_ERROR,
{
"agent_id": agent_id,
"error": str(error),
"error_type": "escalate",
"duration": duration,
},
)
else:
logger.error(
"Tick failed for agent %s after %.1fs: %s",
agent_id, duration, error, exc_info=error,
agent_id,
duration,
error,
exc_info=error,
)
self._manager.end_tick(agent_id)
self._manager.update_agent(agent_id, status="error")
# Write error detail to summary_memory so frontend can display it
error_msg = str(error)[:2000]
self._manager.update_summary_memory(agent_id, f"ERROR: {error_msg}")
self._bus.publish(EventType.AGENT_TICK_ERROR, {
"agent_id": agent_id,
"error": str(error),
"error_type": (
"fatal" if isinstance(error, FatalError) else "retryable_exhausted"
),
"duration": duration,
})
self._bus.publish(
EventType.AGENT_TICK_ERROR,
{
"agent_id": agent_id,
"error": str(error),
"error_type": (
"fatal"
if isinstance(error, FatalError)
else "retryable_exhausted"
),
"duration": duration,
},
)
def _save_trace(
self,
@@ -557,17 +603,19 @@ class AgentExecutor:
steps = []
for s in trace_steps:
steps.append(TraceStep(
step_type=(
StepType.TOOL_CALL
if s["type"] == "tool_call"
else StepType.GENERATE
),
input=s.get("input", {}),
output=s.get("output", {}),
duration_seconds=s.get("duration", 0),
timestamp=s.get("start_time", tick_start),
))
steps.append(
TraceStep(
step_type=(
StepType.TOOL_CALL
if s["type"] == "tool_call"
else StepType.GENERATE
),
input=s.get("input", {}),
output=s.get("output", {}),
duration_seconds=s.get("duration", 0),
timestamp=s.get("start_time", tick_start),
)
)
metadata: dict[str, Any] = {}
if error is not None:
@@ -590,5 +638,7 @@ class AgentExecutor:
self._trace_store.save(trace)
except Exception:
logger.warning(
"Failed to save trace for agent %s", agent_id, exc_info=True,
"Failed to save trace for agent %s",
agent_id,
exc_info=True,
)
+34 -43
View File
@@ -13,17 +13,19 @@ from openjarvis.core.events import EventBus, EventType
@dataclass(slots=True)
class LoopGuardConfig:
"""Configuration for the loop guard."""
enabled: bool = True
max_identical_calls: int = 3 # SHA-256 of (tool_name, arguments)
ping_pong_window: int = 6 # detect A-B-A-B cycling
poll_tool_budget: int = 5 # max calls to same polling tool
max_context_messages: int = 100 # context overflow threshold
warn_before_block: bool = True # warn on first cycle, block on second
max_identical_calls: int = 3 # SHA-256 of (tool_name, arguments)
ping_pong_window: int = 6 # detect A-B-A-B cycling
poll_tool_budget: int = 5 # max calls to same polling tool
max_context_messages: int = 100 # context overflow threshold
warn_before_block: bool = True # warn on first cycle, block on second
@dataclass(slots=True)
class LoopVerdict:
"""Result of a loop guard check."""
blocked: bool = False
reason: str = ""
warned: bool = False
@@ -54,13 +56,12 @@ class LoopGuard:
try:
from openjarvis._rust_bridge import get_rust_module
_rust = get_rust_module()
self._rust_impl = _rust.LoopGuard(
max_identical=config.max_identical_calls,
max_ping_pong=(
config.ping_pong_window // 2
if config.ping_pong_window > 1
else 2
config.ping_pong_window // 2 if config.ping_pong_window > 1 else 2
),
poll_budget=config.poll_tool_budget,
)
@@ -93,9 +94,7 @@ class LoopGuard:
def _python_check(self, tool_name: str, arguments: str) -> LoopVerdict:
"""Pure-Python fallback when Rust backend is not available."""
# 1. Hash tracking — identical calls
call_hash = hashlib.sha256(
f"{tool_name}:{arguments}".encode()
).hexdigest()[:16]
call_hash = hashlib.sha256(f"{tool_name}:{arguments}".encode()).hexdigest()[:16]
self._call_counts[call_hash] = self._call_counts.get(call_hash, 0) + 1
if self._call_counts[call_hash] > self._config.max_identical_calls:
self._emit_triggered("identical_call", tool_name)
@@ -139,12 +138,12 @@ class LoopGuard:
@staticmethod
def _is_system(msg: object) -> bool:
"""Check if a message has role == system."""
return getattr(msg, 'role', None) == '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'
return getattr(msg, "role", None) == "tool"
def compress_context(self, messages: list) -> list:
"""Apply 4-stage context overflow recovery to message list.
@@ -164,14 +163,19 @@ class LoopGuard:
for i, msg in enumerate(messages):
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,
),
name=getattr(msg, 'name', None),
))
compressed.append(
Message(
role=Role.TOOL,
content="[Tool result truncated]",
tool_call_id=getattr(
msg,
"tool_call_id",
None,
),
name=getattr(msg, "name", None),
)
)
else:
compressed.append(msg)
@@ -179,16 +183,9 @@ class LoopGuard:
return compressed
# Stage 2: Sliding window — keep system + recent
system_msgs = [
m for m in compressed if self._is_system(m)
]
non_system = [
m for m in compressed
if not self._is_system(m)
]
window_size = (
self._config.max_context_messages - len(system_msgs)
)
system_msgs = [m for m in compressed if self._is_system(m)]
non_system = [m for m in compressed if not self._is_system(m)]
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
@@ -198,24 +195,18 @@ class LoopGuard:
# Stage 3: Drop tool call/result pairs from middle
keep_start = max(
len(system_msgs), len(compressed) // 10,
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
sys_final = [
m for m in compressed if self._is_system(m)
]
tail = [
m for m in compressed
if not self._is_system(m)
]
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:
@@ -234,7 +225,7 @@ class LoopGuard:
# Check for period-2 pattern (A-B-A-B)
for period in (2, 3):
if n >= period * 2:
tail = seq[-period * 2:]
tail = seq[-period * 2 :]
pattern = tail[:period]
if all(tail[i] == pattern[i % period] for i in range(len(tail))):
return True
+1 -3
View File
@@ -284,9 +284,7 @@ class NativeOpenHandsAgent(ToolUsingAgent):
total_usage = {"prompt_tokens": 0, "completion_tokens": 0, "total_tokens": 0}
# Build OpenAI-format tool schemas for native function calling
openai_tools = (
self._executor.get_openai_tools() if self._tools else []
)
openai_tools = self._executor.get_openai_tools() if self._tools else []
# Side dict for Gemini thought_signatures (ToolCall uses slots)
_thought_sigs: dict[str, bytes] = {}
+31 -15
View File
@@ -162,7 +162,11 @@ class AgentScheduler:
for agent_id, info in due:
agent = self._manager.get_agent(agent_id)
if agent is None or agent["status"] in (
"paused", "archived", "running", "budget_exceeded", "stalled",
"paused",
"archived",
"running",
"budget_exceeded",
"stalled",
):
continue
@@ -177,11 +181,12 @@ class AgentScheduler:
if agent_id in self._agents:
if info["schedule_type"] == "cron":
self._agents[agent_id]["next_fire"] = _next_cron_fire(
str(info["schedule_value"]), now,
str(info["schedule_value"]),
now,
)
elif info["schedule_type"] == "interval":
self._agents[agent_id]["next_fire"] = (
now + float(info["schedule_value"])
self._agents[agent_id]["next_fire"] = now + float(
info["schedule_value"]
)
# Manual: stays at inf
@@ -214,22 +219,30 @@ class AgentScheduler:
self._manager.update_agent(agent["id"], status="error")
logger.warning(
"Agent %s stall retries exhausted (%d/%d), setting error",
agent["id"], current_retries, max_retries,
agent["id"],
current_retries,
max_retries,
)
else:
self._manager.end_tick(agent["id"]) # Release concurrency guard
self._manager.update_agent(
agent["id"], stall_retries=current_retries + 1,
agent["id"],
stall_retries=current_retries + 1,
)
if self._bus:
self._bus.publish(EventType.AGENT_STALL_DETECTED, {
"agent_id": agent["id"],
"last_activity_at": last_activity,
"stall_retries": current_retries + 1,
})
self._bus.publish(
EventType.AGENT_STALL_DETECTED,
{
"agent_id": agent["id"],
"last_activity_at": last_activity,
"stall_retries": current_retries + 1,
},
)
logger.warning(
"Agent %s stalled (retry %d/%d)",
agent["id"], current_retries + 1, max_retries,
agent["id"],
current_retries + 1,
max_retries,
)
# -- Learning tick counting ------------------------------------------------
@@ -258,9 +271,12 @@ class AgentScheduler:
if self._tick_counts[agent_id] >= threshold:
self._tick_counts[agent_id] = 0
if self._bus:
self._bus.publish(EventType.AGENT_LEARNING_STARTED, {
"agent_id": agent_id,
})
self._bus.publish(
EventType.AGENT_LEARNING_STARTED,
{
"agent_id": agent_id,
},
)
logger.info(
"Learning triggered for agent %s after %d ticks",
agent_id,
+3 -9
View File
@@ -64,9 +64,7 @@ class EnergyBenchmark(BaseBenchmark):
from openjarvis.telemetry.steady_state import SteadyStateDetector
detector = SteadyStateDetector()
energy_method = getattr(
energy_monitor, "energy_method", lambda: ""
)()
energy_method = getattr(energy_monitor, "energy_method", lambda: "")()
for _ in range(num_samples):
t0 = time.time()
@@ -78,9 +76,7 @@ class EnergyBenchmark(BaseBenchmark):
usage = result.get("usage", {})
tokens = usage.get("completion_tokens", 0)
energy_j = sample.energy_joules
power_w = (
energy_j / elapsed if elapsed > 0 else 0.0
)
power_w = energy_j / elapsed if elapsed > 0 else 0.0
tps = tokens / elapsed if elapsed > 0 else 0.0
ept = energy_j / tokens if tokens > 0 else 0.0
@@ -145,9 +141,7 @@ class EnergyBenchmark(BaseBenchmark):
samples=num_samples,
errors=errors,
warmup_samples=warmup_samples,
steady_state_samples=(
ss_result.steady_state_samples if ss_result else 0
),
steady_state_samples=(ss_result.steady_state_samples if ss_result else 0),
steady_state_reached=(
ss_result.steady_state_reached if ss_result else False
),
+2 -1
View File
@@ -96,7 +96,8 @@ class BlueBubblesChannel(BaseChannel):
self._publish_sent(channel, content, conversation_id)
return True
logger.warning(
"BlueBubbles API returned status %d", resp.status_code,
"BlueBubbles API returned status %d",
resp.status_code,
)
return False
except Exception:
+6 -2
View File
@@ -62,7 +62,8 @@ class DiscordChannel(BaseChannel):
import discord # noqa: F401
self._listener_thread = threading.Thread(
target=self._gateway_loop, daemon=True,
target=self._gateway_loop,
daemon=True,
)
self._listener_thread.start()
self._status = ChannelStatus.CONNECTED
@@ -107,7 +108,10 @@ class DiscordChannel(BaseChannel):
payload["message_reference"] = {"message_id": conversation_id}
resp = httpx.post(
url, json=payload, headers=headers, timeout=10.0,
url,
json=payload,
headers=headers,
timeout=10.0,
)
if resp.status_code < 300:
self._publish_sent(channel, content, conversation_id)
+6 -3
View File
@@ -86,7 +86,8 @@ class EmailChannel(BaseChannel):
if self._imap_host:
self._listener_thread = threading.Thread(
target=self._imap_poll_loop, daemon=True,
target=self._imap_poll_loop,
daemon=True,
)
self._listener_thread.start()
@@ -121,7 +122,8 @@ class EmailChannel(BaseChannel):
msg["From"] = self._username
msg["To"] = channel
msg["Subject"] = (metadata or {}).get(
"subject", "Message from OpenJarvis",
"subject",
"Message from OpenJarvis",
)
if conversation_id:
msg["In-Reply-To"] = conversation_id
@@ -166,7 +168,8 @@ class EmailChannel(BaseChannel):
try:
if self._use_tls:
imap = imaplib.IMAP4_SSL(
self._imap_host, self._imap_port,
self._imap_host,
self._imap_port,
)
else:
imap = imaplib.IMAP4(self._imap_host, self._imap_port)
+7 -4
View File
@@ -81,8 +81,7 @@ class FeishuChannel(BaseChannel):
# Obtain tenant_access_token
token_url = (
"https://open.feishu.cn/open-apis/auth/v3/"
"tenant_access_token/internal"
"https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal"
)
token_resp = httpx.post(
token_url,
@@ -119,13 +118,17 @@ class FeishuChannel(BaseChannel):
}
resp = httpx.post(
msg_url, json=payload, headers=headers, timeout=10.0,
msg_url,
json=payload,
headers=headers,
timeout=10.0,
)
if resp.status_code < 300:
self._publish_sent(channel, content, conversation_id)
return True
logger.warning(
"Feishu API returned status %d", resp.status_code,
"Feishu API returned status %d",
resp.status_code,
)
return False
except Exception:
+14 -7
View File
@@ -53,10 +53,12 @@ class GmailChannel(BaseChannel):
bus: Optional[EventBus] = None,
) -> None:
self._credentials_path = credentials_path or os.environ.get(
"GMAIL_CREDENTIALS_PATH", "",
"GMAIL_CREDENTIALS_PATH",
"",
)
self._token_path = token_path or os.environ.get(
"GMAIL_TOKEN_PATH", "",
"GMAIL_TOKEN_PATH",
"",
)
self._user_id = user_id
self._poll_interval = poll_interval
@@ -89,7 +91,8 @@ class GmailChannel(BaseChannel):
creds: Any = None
if self._token_path and os.path.exists(self._token_path):
creds = Credentials.from_authorized_user_file(
self._token_path, SCOPES,
self._token_path,
SCOPES,
)
if not creds or not creds.valid:
@@ -101,7 +104,8 @@ class GmailChannel(BaseChannel):
self._credentials_path,
):
flow = InstalledAppFlow.from_client_secrets_file(
self._credentials_path, SCOPES,
self._credentials_path,
SCOPES,
)
creds = flow.run_local_server(port=0)
else:
@@ -120,7 +124,8 @@ class GmailChannel(BaseChannel):
# Start polling thread
self._listener_thread = threading.Thread(
target=self._poll_loop, daemon=True,
target=self._poll_loop,
daemon=True,
)
self._listener_thread.start()
except ImportError:
@@ -165,7 +170,8 @@ class GmailChannel(BaseChannel):
msg["To"] = channel
msg["From"] = self._user_id
msg["Subject"] = (metadata or {}).get(
"subject", "Message from OpenJarvis",
"subject",
"Message from OpenJarvis",
)
raw = base64.urlsafe_b64encode(msg.as_bytes()).decode("utf-8")
@@ -174,7 +180,8 @@ class GmailChannel(BaseChannel):
body["threadId"] = conversation_id
self._service.users().messages().send(
userId=self._user_id, body=body,
userId=self._user_id,
body=body,
).execute()
self._publish_sent(channel, content, conversation_id)
+7 -3
View File
@@ -39,7 +39,8 @@ class GoogleChatChannel(BaseChannel):
bus: Optional[EventBus] = None,
) -> None:
self._webhook_url = webhook_url or os.environ.get(
"GOOGLE_CHAT_WEBHOOK_URL", "",
"GOOGLE_CHAT_WEBHOOK_URL",
"",
)
self._bus = bus
self._handlers: List[ChannelHandler] = []
@@ -80,13 +81,16 @@ class GoogleChatChannel(BaseChannel):
payload: Dict[str, Any] = {"text": content}
resp = httpx.post(
self._webhook_url, json=payload, timeout=10.0,
self._webhook_url,
json=payload,
timeout=10.0,
)
if resp.status_code < 300:
self._publish_sent(channel, content, conversation_id)
return True
logger.warning(
"Google Chat API returned status %d", resp.status_code,
"Google Chat API returned status %d",
resp.status_code,
)
return False
except Exception:
+6 -16
View File
@@ -20,13 +20,9 @@ from typing import Any, Dict, List
logger = logging.getLogger(__name__)
_DEFAULT_DB_PATH = str(
Path.home() / "Library" / "Messages" / "chat.db"
)
_DEFAULT_DB_PATH = str(Path.home() / "Library" / "Messages" / "chat.db")
_POLL_INTERVAL = 5
_PID_FILE = str(
Path.home() / ".openjarvis" / "imessage-agent.pid"
)
_PID_FILE = str(Path.home() / ".openjarvis" / "imessage-agent.pid")
def poll_new_messages(
@@ -37,9 +33,7 @@ def poll_new_messages(
) -> List[Dict[str, Any]]:
"""Return new incoming messages since last_rowid."""
try:
conn = sqlite3.connect(
f"file:{db_path}?mode=ro", uri=True
)
conn = sqlite3.connect(f"file:{db_path}?mode=ro", uri=True)
conn.row_factory = sqlite3.Row
except sqlite3.OperationalError:
return []
@@ -68,7 +62,7 @@ def send_imessage(chat_identifier: str, message: str) -> bool:
escaped = message.replace("\\", "\\\\").replace('"', '\\"')
script = (
f'tell application "Messages"\n'
f' set targetChat to a reference to '
f" set targetChat to a reference to "
f'chat id "{chat_identifier}"\n'
f' send "{escaped}" to targetChat\n'
f"end tell"
@@ -152,12 +146,8 @@ def run_daemon(
def _get_max_rowid(db_path: str) -> int:
"""Get the current max ROWID from chat.db."""
try:
conn = sqlite3.connect(
f"file:{db_path}?mode=ro", uri=True
)
row = conn.execute(
"SELECT MAX(ROWID) FROM message"
).fetchone()
conn = sqlite3.connect(f"file:{db_path}?mode=ro", uri=True)
row = conn.execute("SELECT MAX(ROWID) FROM message").fetchone()
conn.close()
return row[0] or 0
except sqlite3.OperationalError:
+1 -2
View File
@@ -65,8 +65,7 @@ class LineChannel(BaseChannel):
import linebot # noqa: F401
except ImportError:
raise ImportError(
"line-bot-sdk not installed. Install with: "
"uv sync --extra channel-line"
"line-bot-sdk not installed. Install with: uv sync --extra channel-line"
)
self._status = ChannelStatus.CONNECTED
+2 -6
View File
@@ -43,12 +43,8 @@ class MastodonChannel(BaseChannel):
access_token: str = "",
bus: Optional[EventBus] = None,
) -> None:
self._api_base_url = api_base_url or os.environ.get(
"MASTODON_API_BASE_URL", ""
)
self._access_token = access_token or os.environ.get(
"MASTODON_ACCESS_TOKEN", ""
)
self._api_base_url = api_base_url or os.environ.get("MASTODON_API_BASE_URL", "")
self._access_token = access_token or os.environ.get("MASTODON_ACCESS_TOKEN", "")
self._bus = bus
self._handlers: List[ChannelHandler] = []
self._status = ChannelStatus.DISCONNECTED
+6 -2
View File
@@ -94,13 +94,17 @@ class MatrixChannel(BaseChannel):
}
resp = httpx.put(
url, json=payload, headers=headers, timeout=10.0,
url,
json=payload,
headers=headers,
timeout=10.0,
)
if resp.status_code < 300:
self._publish_sent(channel, content, conversation_id)
return True
logger.warning(
"Matrix API returned status %d", resp.status_code,
"Matrix API returned status %d",
resp.status_code,
)
return False
except Exception:
+6 -2
View File
@@ -91,13 +91,17 @@ class MattermostChannel(BaseChannel):
payload["root_id"] = conversation_id
resp = httpx.post(
url, json=payload, headers=headers, timeout=10.0,
url,
json=payload,
headers=headers,
timeout=10.0,
)
if resp.status_code < 300:
self._publish_sent(channel, content, conversation_id)
return True
logger.warning(
"Mattermost API returned status %d", resp.status_code,
"Mattermost API returned status %d",
resp.status_code,
)
return False
except Exception:
+2 -5
View File
@@ -45,9 +45,7 @@ class NostrChannel(BaseChannel):
bus: Optional[EventBus] = None,
) -> None:
self._private_key = private_key or os.environ.get("NOSTR_PRIVATE_KEY", "")
relays_str = relays or os.environ.get(
"NOSTR_RELAYS", "wss://relay.damus.io"
)
relays_str = relays or os.environ.get("NOSTR_RELAYS", "wss://relay.damus.io")
self._relays = [r.strip() for r in relays_str.split(",") if r.strip()]
self._bus = bus
self._handlers: List[ChannelHandler] = []
@@ -65,8 +63,7 @@ class NostrChannel(BaseChannel):
import pynostr # noqa: F401
except ImportError:
raise ImportError(
"pynostr not installed. Install with: "
"uv sync --extra channel-nostr"
"pynostr not installed. Install with: uv sync --extra channel-nostr"
)
self._status = ChannelStatus.CONNECTED
+1 -2
View File
@@ -78,8 +78,7 @@ class RedditChannel(BaseChannel):
import praw # noqa: F401
except ImportError:
raise ImportError(
"praw not installed. Install with: "
"uv sync --extra channel-reddit"
"praw not installed. Install with: uv sync --extra channel-reddit"
)
self._status = ChannelStatus.CONNECTED
+4 -2
View File
@@ -44,7 +44,8 @@ class SignalChannel(BaseChannel):
) -> None:
self._api_url = api_url or os.environ.get("SIGNAL_API_URL", "")
self._phone_number = phone_number or os.environ.get(
"SIGNAL_PHONE_NUMBER", "",
"SIGNAL_PHONE_NUMBER",
"",
)
self._bus = bus
self._handlers: List[ChannelHandler] = []
@@ -94,7 +95,8 @@ class SignalChannel(BaseChannel):
self._publish_sent(channel, content, conversation_id)
return True
logger.warning(
"Signal API returned status %d", resp.status_code,
"Signal API returned status %d",
resp.status_code,
)
return False
except Exception:
+8 -3
View File
@@ -72,7 +72,8 @@ class SlackChannel(BaseChannel):
return
self._listener_thread = threading.Thread(
target=self._socket_mode_loop, daemon=True,
target=self._socket_mode_loop,
daemon=True,
)
self._listener_thread.start()
self._status = ChannelStatus.CONNECTED
@@ -120,7 +121,10 @@ class SlackChannel(BaseChannel):
payload["thread_ts"] = conversation_id
resp = httpx.post(
url, json=payload, headers=headers, timeout=10.0,
url,
json=payload,
headers=headers,
timeout=10.0,
)
if resp.status_code < 300:
data = resp.json()
@@ -130,7 +134,8 @@ class SlackChannel(BaseChannel):
logger.warning("Slack API error: %s", data.get("error"))
return False
logger.warning(
"Slack API returned status %d", resp.status_code,
"Slack API returned status %d",
resp.status_code,
)
return False
except Exception:
+8 -5
View File
@@ -46,9 +46,8 @@ class TeamsChannel(BaseChannel):
) -> None:
self._app_id = app_id or os.environ.get("TEAMS_APP_ID", "")
self._app_password = app_password or os.environ.get("TEAMS_APP_PASSWORD", "")
self._service_url = (
service_url
or os.environ.get("TEAMS_SERVICE_URL", "https://smba.trafficmanager.net/teams")
self._service_url = service_url or os.environ.get(
"TEAMS_SERVICE_URL", "https://smba.trafficmanager.net/teams"
)
self._bus = bus
self._handlers: List[ChannelHandler] = []
@@ -99,13 +98,17 @@ class TeamsChannel(BaseChannel):
payload["replyToId"] = conversation_id
resp = httpx.post(
url, json=payload, headers=headers, timeout=10.0,
url,
json=payload,
headers=headers,
timeout=10.0,
)
if resp.status_code < 300:
self._publish_sent(channel, content, conversation_id)
return True
logger.warning(
"Teams API returned status %d", resp.status_code,
"Teams API returned status %d",
resp.status_code,
)
return False
except Exception:
+2 -1
View File
@@ -70,7 +70,8 @@ class TelegramChannel(BaseChannel):
from telegram.ext import ApplicationBuilder # noqa: F401
self._listener_thread = threading.Thread(
target=self._poll_loop, daemon=True,
target=self._poll_loop,
daemon=True,
)
self._listener_thread.start()
self._status = ChannelStatus.CONNECTED
+5 -15
View File
@@ -45,15 +45,9 @@ class TwilioSMSChannel(BaseChannel):
phone_number: str = "",
bus: Optional[EventBus] = None,
) -> None:
self._account_sid = account_sid or os.environ.get(
"TWILIO_ACCOUNT_SID", ""
)
self._auth_token = auth_token or os.environ.get(
"TWILIO_AUTH_TOKEN", ""
)
self._phone_number = phone_number or os.environ.get(
"TWILIO_PHONE_NUMBER", ""
)
self._account_sid = account_sid or os.environ.get("TWILIO_ACCOUNT_SID", "")
self._auth_token = auth_token or os.environ.get("TWILIO_AUTH_TOKEN", "")
self._phone_number = phone_number or os.environ.get("TWILIO_PHONE_NUMBER", "")
self._bus = bus
self._client: Any = None
self._handlers: List[ChannelHandler] = []
@@ -61,9 +55,7 @@ class TwilioSMSChannel(BaseChannel):
def connect(self) -> None:
try:
self._client = _create_twilio_client(
self._account_sid, self._auth_token
)
self._client = _create_twilio_client(self._account_sid, self._auth_token)
self._status = ChannelStatus.CONNECTED
except Exception:
logger.exception("Failed to create Twilio client")
@@ -93,9 +85,7 @@ class TwilioSMSChannel(BaseChannel):
self._publish_sent(channel, content, conversation_id)
return True
except Exception:
logger.exception(
"Failed to send SMS to %s", channel
)
logger.exception("Failed to send SMS to %s", channel)
return False
def status(self) -> ChannelStatus:
+2 -5
View File
@@ -51,9 +51,7 @@ class TwitchChannel(BaseChannel):
initial_channels: str = "",
bus: Optional[EventBus] = None,
) -> None:
self._access_token = access_token or os.environ.get(
"TWITCH_ACCESS_TOKEN", ""
)
self._access_token = access_token or os.environ.get("TWITCH_ACCESS_TOKEN", "")
self._client_id = client_id or os.environ.get("TWITCH_CLIENT_ID", "")
self._nick = nick or os.environ.get("TWITCH_NICK", "")
self._initial_channels = initial_channels or os.environ.get(
@@ -75,8 +73,7 @@ class TwitchChannel(BaseChannel):
import twitchio # noqa: F401
except ImportError:
raise ImportError(
"twitchio not installed. Install with: "
"uv sync --extra channel-twitch"
"twitchio not installed. Install with: uv sync --extra channel-twitch"
)
self._status = ChannelStatus.CONNECTED
+14 -6
View File
@@ -55,15 +55,18 @@ class TwitterChannel(BaseChannel):
bus: Optional[EventBus] = None,
) -> None:
self._bearer_token = bearer_token or os.environ.get(
"TWITTER_BEARER_TOKEN", "",
"TWITTER_BEARER_TOKEN",
"",
)
self._api_key = api_key or os.environ.get("TWITTER_API_KEY", "")
self._api_secret = api_secret or os.environ.get("TWITTER_API_SECRET", "")
self._access_token = access_token or os.environ.get(
"TWITTER_ACCESS_TOKEN", "",
"TWITTER_ACCESS_TOKEN",
"",
)
self._access_secret = access_secret or os.environ.get(
"TWITTER_ACCESS_SECRET", "",
"TWITTER_ACCESS_SECRET",
"",
)
self._poll_interval = poll_interval
self._bus = bus
@@ -99,7 +102,8 @@ class TwitterChannel(BaseChannel):
if self._access_token:
self._poll_thread = threading.Thread(
target=self._poll_loop, daemon=True,
target=self._poll_loop,
daemon=True,
)
self._poll_thread.start()
except ImportError:
@@ -189,7 +193,8 @@ class TwitterChannel(BaseChannel):
continue
response = self._client.get_users_mentions(
id=user_id, **kwargs,
id=user_id,
**kwargs,
)
if response and response.data:
@@ -225,7 +230,10 @@ class TwitterChannel(BaseChannel):
self._stop_event.wait(self._poll_interval)
def _publish_sent(
self, channel: str, content: str, conversation_id: str,
self,
channel: str,
content: str,
conversation_id: str,
) -> None:
"""Publish a CHANNEL_MESSAGE_SENT event on the bus."""
if self._bus is not None:
+1 -2
View File
@@ -64,8 +64,7 @@ class ViberChannel(BaseChannel):
import viberbot # noqa: F401
except ImportError:
raise ImportError(
"viberbot not installed. Install with: "
"uv sync --extra channel-viber"
"viberbot not installed. Install with: uv sync --extra channel-viber"
)
self._status = ChannelStatus.CONNECTED
+7 -3
View File
@@ -95,14 +95,18 @@ class WebhookChannel(BaseChannel):
headers["X-Webhook-Secret"] = self._secret
resp = httpx.request(
self._method, self._url, json=payload,
headers=headers, timeout=10.0,
self._method,
self._url,
json=payload,
headers=headers,
timeout=10.0,
)
if resp.status_code < 300:
self._publish_sent(channel, content, conversation_id)
return True
logger.warning(
"Webhook returned status %d", resp.status_code,
"Webhook returned status %d",
resp.status_code,
)
return False
except Exception:
+9 -7
View File
@@ -44,7 +44,8 @@ class WhatsAppChannel(BaseChannel):
) -> None:
self._token = access_token or os.environ.get("WHATSAPP_ACCESS_TOKEN", "")
self._phone_number_id = phone_number_id or os.environ.get(
"WHATSAPP_PHONE_NUMBER_ID", "",
"WHATSAPP_PHONE_NUMBER_ID",
"",
)
self._bus = bus
self._handlers: List[ChannelHandler] = []
@@ -82,10 +83,7 @@ class WhatsAppChannel(BaseChannel):
try:
import httpx
url = (
f"https://graph.facebook.com/v21.0/"
f"{self._phone_number_id}/messages"
)
url = f"https://graph.facebook.com/v21.0/{self._phone_number_id}/messages"
headers = {
"Authorization": f"Bearer {self._token}",
"Content-Type": "application/json",
@@ -98,13 +96,17 @@ class WhatsAppChannel(BaseChannel):
}
resp = httpx.post(
url, json=payload, headers=headers, timeout=10.0,
url,
json=payload,
headers=headers,
timeout=10.0,
)
if resp.status_code < 300:
self._publish_sent(channel, content, conversation_id)
return True
logger.warning(
"WhatsApp API returned status %d", resp.status_code,
"WhatsApp API returned status %d",
resp.status_code,
)
return False
except Exception:
+10 -8
View File
@@ -109,8 +109,7 @@ class WhatsAppBaileysChannel(BaseChannel):
pkg_dst = runtime / "package.json"
pkg_src = _BRIDGE_SRC / "package.json"
if pkg_src.exists() and (
not pkg_dst.exists()
or pkg_src.stat().st_mtime > pkg_dst.stat().st_mtime
not pkg_dst.exists() or pkg_src.stat().st_mtime > pkg_dst.stat().st_mtime
):
shutil.copy2(pkg_src, pkg_dst)
@@ -169,7 +168,8 @@ class WhatsAppBaileysChannel(BaseChannel):
bufsize=1,
)
self._reader_thread = threading.Thread(
target=self._reader_loop, daemon=True,
target=self._reader_loop,
daemon=True,
)
self._reader_thread.start()
logger.info(
@@ -218,11 +218,13 @@ class WhatsAppBaileysChannel(BaseChannel):
return False
try:
self._write_command({
"type": "send",
"jid": channel,
"text": content,
})
self._write_command(
{
"type": "send",
"jid": channel,
"text": content,
}
)
self._publish_sent(channel, content, conversation_id)
return True
except Exception:
+1 -2
View File
@@ -70,8 +70,7 @@ class XMPPChannel(BaseChannel):
import slixmpp # noqa: F401
except ImportError:
raise ImportError(
"slixmpp not installed. Install with: "
"uv sync --extra channel-xmpp"
"slixmpp not installed. Install with: uv sync --extra channel-xmpp"
)
self._status = ChannelStatus.CONNECTED
+1 -2
View File
@@ -71,8 +71,7 @@ class ZulipChannel(BaseChannel):
import zulip # noqa: F401
except ImportError:
raise ImportError(
"zulip not installed. Install with: "
"uv sync --extra channel-zulip"
"zulip not installed. Install with: uv sync --extra channel-zulip"
)
self._status = ChannelStatus.CONNECTED
+1 -1
View File
@@ -15,8 +15,8 @@ from openjarvis.cli.chat_cmd import chat
from openjarvis.cli.compose_cmd import compose
from openjarvis.cli.config_cmd import config
from openjarvis.cli.connect_cmd import connect
from openjarvis.cli.deep_research_setup_cmd import deep_research_setup
from openjarvis.cli.daemon_cmd import restart, start, status, stop
from openjarvis.cli.deep_research_setup_cmd import deep_research_setup
from openjarvis.cli.doctor_cmd import doctor
from openjarvis.cli.eval_cmd import eval_group
from openjarvis.cli.feedback_cmd import feedback_group
+9 -5
View File
@@ -74,11 +74,15 @@ def _get_latest_version(current: str) -> str | None:
try:
_CACHE_PATH.parent.mkdir(parents=True, exist_ok=True)
_CACHE_PATH.write_text(json.dumps({
"last_check": time.time(),
"latest_version": latest,
"current_version": current,
}))
_CACHE_PATH.write_text(
json.dumps(
{
"last_check": time.time(),
"latest_version": latest,
"current_version": current,
}
)
)
except Exception:
pass
+3 -1
View File
@@ -70,7 +70,9 @@ _MCP_TEMPLATES = {
@click.argument("server_name")
@click.option("--key", default=None, help="API key or token for the server.")
@click.option(
"--args", "extra_args", default=None,
"--args",
"extra_args",
default=None,
help="Additional arguments (comma-separated).",
)
def add(server_name: str, key: str | None, extra_args: str | None) -> None:
+50 -45
View File
@@ -72,13 +72,19 @@ def list_agents() -> None:
tasks = mgr.list_tasks(a["id"])
bindings = mgr.list_channel_bindings(a["id"])
status_style = {
"idle": "dim", "running": "green", "paused": "yellow",
"error": "red", "archived": "dim strike",
"idle": "dim",
"running": "green",
"paused": "yellow",
"error": "red",
"archived": "dim strike",
}.get(a["status"], "")
table.add_row(
a["id"], a["name"], a["agent_type"],
a["id"],
a["name"],
a["agent_type"],
f"[{status_style}]{a['status']}[/{status_style}]",
str(len(tasks)), str(len(bindings)),
str(len(tasks)),
str(len(bindings)),
)
console.print(table)
except Exception as exc:
@@ -303,8 +309,10 @@ def templates() -> None:
table.add_column("Description", style="white")
for t in tpls:
table.add_row(
t.get("id", ""), t.get("name", ""),
t.get("source", ""), t.get("description", "")[:60],
t.get("id", ""),
t.get("name", ""),
t.get("source", ""),
t.get("description", "")[:60],
)
console.print(table)
except Exception as exc:
@@ -314,6 +322,7 @@ def templates() -> None:
def _get_system():
"""Build a JarvisSystem for CLI commands that need scheduler/executor."""
from openjarvis.system import SystemBuilder
try:
return SystemBuilder().build()
except RuntimeError as exc:
@@ -332,6 +341,7 @@ def _get_scheduler_and_executor(system=None):
def launch():
"""Interactive agent launcher."""
from openjarvis.agents.manager import AgentManager as _AM
templates = _AM.list_templates()
click.echo("Available templates:")
for i, t in enumerate(templates, 1):
@@ -363,7 +373,8 @@ def launch():
manager = _get_manager()
if template:
agent_data = manager.create_from_template(
template["id"], name=name,
template["id"],
name=name,
)
agent_config = agent_data.get("config", {})
agent_config.update(config)
@@ -374,7 +385,7 @@ def launch():
agent_data = manager.create_agent(
name=name, agent_type=agent_type, config=config
)
click.echo(f"\nAgent \"{agent_data['name']}\" created (ID: {agent_data['id']})")
click.echo(f'\nAgent "{agent_data["name"]}" created (ID: {agent_data["id"]})')
@agent.command("start")
@@ -391,7 +402,7 @@ def start_agent(agent_id):
click.echo("Scheduler not available", err=True)
raise SystemExit(1)
scheduler.register_agent(agent_id)
click.echo(f"Agent \"{agent_data['name']}\" registered with scheduler")
click.echo(f'Agent "{agent_data["name"]}" registered with scheduler')
@agent.command("stop")
@@ -408,7 +419,7 @@ def stop_agent(agent_id):
click.echo("Scheduler not available", err=True)
raise SystemExit(1)
scheduler.deregister_agent(agent_id)
click.echo(f"Agent \"{agent_data['name']}\" deregistered from scheduler")
click.echo(f'Agent "{agent_data["name"]}" deregistered from scheduler')
@agent.command("run")
@@ -423,7 +434,7 @@ def run_agent(agent_id):
if agent_data["status"] == "archived":
click.echo(f"Agent {agent_id} is archived and cannot be run", err=True)
raise SystemExit(1)
click.echo(f"Running tick for \"{agent_data['name']}\"...")
click.echo(f'Running tick for "{agent_data["name"]}"...')
_, executor, _ = _get_scheduler_and_executor()
if executor is None:
click.echo("Executor not available", err=True)
@@ -465,9 +476,7 @@ def status():
max_cost = cfg.get("max_cost", 0)
if max_cost > 0:
pct = min(100, int(a.get("total_cost", 0) / max_cost * 100))
budget = (
f"${a.get('total_cost', 0):.2f}/{max_cost:.0f} ({pct}%)"
)
budget = f"${a.get('total_cost', 0):.2f}/{max_cost:.0f} ({pct}%)"
else:
budget = f"${a.get('total_cost', 0):.2f}"
# Last seen column
@@ -491,9 +500,7 @@ def status():
@agent.command("learning")
@click.argument("agent_id")
@click.option(
"--run", "trigger_run", is_flag=True, help="Trigger manual learning run"
)
@click.option("--run", "trigger_run", is_flag=True, help="Trigger manual learning run")
def learning(agent_id, trigger_run):
"""Show learning history or trigger a manual run."""
import datetime
@@ -505,35 +512,35 @@ def learning(agent_id, trigger_run):
raise SystemExit(1)
if trigger_run:
click.echo(f"Triggering learning for \"{agent_data['name']}\"...")
click.echo(f'Triggering learning for "{agent_data["name"]}"...')
from openjarvis.core.events import EventType, get_event_bus
bus = get_event_bus()
bus.publish(
EventType.AGENT_LEARNING_STARTED, {"agent_id": agent_id}
)
bus.publish(EventType.AGENT_LEARNING_STARTED, {"agent_id": agent_id})
click.echo("Learning triggered.")
return
logs = manager.list_learning_log(agent_id)
if not logs:
click.echo(f"No learning history for \"{agent_data['name']}\"")
click.echo(f'No learning history for "{agent_data["name"]}"')
return
click.echo(f"Learning history for \"{agent_data['name']}\":")
click.echo(f'Learning history for "{agent_data["name"]}":')
for entry in logs:
ts = datetime.datetime.fromtimestamp(
entry["created_at"]
).strftime("%Y-%m-%d %H:%M")
ts = datetime.datetime.fromtimestamp(entry["created_at"]).strftime(
"%Y-%m-%d %H:%M"
)
click.echo(
f" {ts} [{entry['event_type']}] "
f"{entry.get('description', '')[:60]}"
f" {ts} [{entry['event_type']}] {entry.get('description', '')[:60]}"
)
@agent.command("trace")
@click.argument("agent_id")
@click.option(
"--run", "run_number", default=None, type=int,
"--run",
"run_number",
default=None,
type=int,
help="Specific run number",
)
@click.option("--limit", "-n", default=10, help="Number of traces to show")
@@ -551,36 +558,32 @@ def trace(agent_id, run_number, limit):
raise SystemExit(1)
config = load_config()
store = TraceStore(
config.traces.db_path or "~/.openjarvis/traces.db"
)
store = TraceStore(config.traces.db_path or "~/.openjarvis/traces.db")
traces = store.list_traces(agent=agent_id, limit=limit)
if not traces:
click.echo(f"No traces for \"{agent_data['name']}\"")
click.echo(f'No traces for "{agent_data["name"]}"')
return
if run_number is not None and 1 <= run_number <= len(traces):
t = traces[run_number - 1]
click.echo(
f"Trace #{run_number}{t.outcome} "
f"({t.total_latency_seconds:.1f}s)"
f"Trace #{run_number}{t.outcome} ({t.total_latency_seconds:.1f}s)"
)
for i, step in enumerate(t.steps, 1):
click.echo(
f" Step {i}: [{step.step_type.value}] "
f"{step.duration_seconds:.2f}s"
f" Step {i}: [{step.step_type.value}] {step.duration_seconds:.2f}s"
)
inp = str(step.input)[:80]
out = str(step.output)[:80]
click.echo(f" In: {inp}")
click.echo(f" Out: {out}")
else:
click.echo(f"Traces for \"{agent_data['name']}\":")
click.echo(f'Traces for "{agent_data["name"]}":')
for i, t in enumerate(traces, 1):
ts = datetime.datetime.fromtimestamp(
t.started_at
).strftime("%Y-%m-%d %H:%M")
ts = datetime.datetime.fromtimestamp(t.started_at).strftime(
"%Y-%m-%d %H:%M"
)
click.echo(
f" #{i} {ts} {t.outcome} "
f"{t.total_latency_seconds:.1f}s "
@@ -602,9 +605,9 @@ def logs(agent_id, limit):
raise SystemExit(1)
checkpoints = manager.list_checkpoints(agent_id)
if not checkpoints:
click.echo(f"No execution history for \"{agent_data['name']}\"")
click.echo(f'No execution history for "{agent_data["name"]}"')
return
click.echo(f"Recent runs for \"{agent_data['name']}\":")
click.echo(f'Recent runs for "{agent_data["name"]}":')
for cp in checkpoints[:limit]:
ts = datetime.datetime.fromtimestamp(cp["created_at"]).strftime(
"%Y-%m-%d %H:%M"
@@ -651,8 +654,10 @@ def watch(agent_id):
click.echo("Watching agent events... (press Ctrl+C to stop)")
bus = get_event_bus()
agent_events = [
EventType.AGENT_TICK_START, EventType.AGENT_TICK_END,
EventType.AGENT_TICK_ERROR, EventType.AGENT_BUDGET_EXCEEDED,
EventType.AGENT_TICK_START,
EventType.AGENT_TICK_END,
EventType.AGENT_TICK_ERROR,
EventType.AGENT_BUDGET_EXCEEDED,
]
def _on_event(event):
+3 -10
View File
@@ -46,16 +46,11 @@ def create_key() -> None:
content += f'\n[server.auth]\napi_key = "{key}"\n'
config_path.write_text(content)
os.chmod(
config_path, stat.S_IRUSR | stat.S_IWUSR
) # 0600
os.chmod(config_path, stat.S_IRUSR | stat.S_IWUSR) # 0600
click.echo(f"API key generated: {key}")
click.echo(f"Stored in: {config_path}")
click.echo(
"File permissions set to 0600 "
"(user-only read/write)."
)
click.echo("File permissions set to 0600 (user-only read/write).")
@auth.command("revoke-key")
@@ -71,8 +66,6 @@ def revoke_key() -> None:
click.echo("No API key found in config.")
return
content = re.sub(
r'api_key\s*=\s*"[^"]*"', 'api_key = ""', content
)
content = re.sub(r'api_key\s*=\s*"[^"]*"', 'api_key = ""', content)
config_path.write_text(content)
click.echo("API key revoked.")
+34 -17
View File
@@ -61,7 +61,7 @@ def _detect_stat_groups(metrics: dict[str, float]) -> dict[str, dict[str, float]
for key, val in metrics.items():
for pfx in _STATS_PREFIXES:
if key.startswith(pfx):
base = key[len(pfx):]
base = key[len(pfx) :]
groups.setdefault(base, {})[pfx.rstrip("_")] = val
break
return groups
@@ -152,27 +152,46 @@ def bench() -> None:
@click.option("-m", "--model", "model_name", default=None, help="Model to benchmark.")
@click.option("-e", "--engine", "engine_key", default=None, help="Engine backend.")
@click.option(
"-n", "--samples", "num_samples", default=10, type=int,
"-n",
"--samples",
"num_samples",
default=10,
type=int,
help="Number of samples per benchmark.",
)
@click.option(
"-b", "--benchmark", "benchmark_name", default=None,
"-b",
"--benchmark",
"benchmark_name",
default=None,
help="Specific benchmark to run (default: all).",
)
@click.option(
"-o", "--output", "output_path", default=None, type=click.Path(),
"-o",
"--output",
"output_path",
default=None,
type=click.Path(),
help="Write JSONL results to file.",
)
@click.option(
"--json", "output_json", is_flag=True,
"--json",
"output_json",
is_flag=True,
help="Output JSON summary to stdout.",
)
@click.option(
"-w", "--warmup", "warmup", default=0, type=int,
"-w",
"--warmup",
"warmup",
default=0,
type=int,
help="Number of warmup iterations before measurement.",
)
@click.option(
"--setup-energy", "setup_energy", is_flag=True,
"--setup-energy",
"setup_energy",
is_flag=True,
help="Run energy monitor setup script when missing (for energy benchmark).",
)
def run(
@@ -250,16 +269,12 @@ def run(
import platform
setup_script = (
Path(__file__).resolve().parents[3]
/ "scripts"
/ "setup-energy-monitor.sh"
)
is_darwin_arm = (
platform.system() == "Darwin"
and platform.machine() == "arm64"
Path(__file__).resolve().parents[3] / "scripts" / "setup-energy-monitor.sh"
)
is_darwin_arm = platform.system() == "Darwin" and platform.machine() == "arm64"
extra_hint = (
"openjarvis[energy-apple]" if is_darwin_arm
"openjarvis[energy-apple]"
if is_darwin_arm
else "openjarvis[gpu-metrics]"
if platform.system() == "Linux"
else "openjarvis[energy-all]"
@@ -314,8 +329,10 @@ def run(
f"[bold cyan]Running {len(benchmarks)} benchmark(s)...[/bold cyan]",
):
results = suite.run_all(
engine, model_name,
num_samples=num_samples, warmup_samples=warmup,
engine,
model_name,
num_samples=num_samples,
warmup_samples=warmup,
energy_monitor=energy_monitor,
)
+9 -3
View File
@@ -148,7 +148,9 @@ def channel() -> None:
@channel.command("list")
@click.option(
"--channel-type", default=None, help=_CHANNEL_TYPE_HELP,
"--channel-type",
default=None,
help=_CHANNEL_TYPE_HELP,
)
def channel_list(
channel_type: Optional[str],
@@ -186,7 +188,9 @@ def channel_list(
@click.argument("target")
@click.argument("message")
@click.option(
"--channel-type", default=None, help=_CHANNEL_TYPE_HELP,
"--channel-type",
default=None,
help=_CHANNEL_TYPE_HELP,
)
def channel_send(
target: str,
@@ -216,7 +220,9 @@ def channel_send(
@channel.command("status")
@click.option(
"--channel-type", default=None, help=_CHANNEL_TYPE_HELP,
"--channel-type",
default=None,
help=_CHANNEL_TYPE_HELP,
)
def channel_status(
channel_type: Optional[str],
+18 -15
View File
@@ -27,12 +27,14 @@ def channels_status() -> None:
if is_running():
table.add_row(
"iMessage", "[green]running[/green]",
"iMessage",
"[green]running[/green]",
"Polling chat.db",
)
else:
table.add_row(
"iMessage", "[dim]stopped[/dim]",
"iMessage",
"[dim]stopped[/dim]",
"jarvis channels imessage-start <contact>",
)
@@ -47,7 +49,8 @@ def channels_status() -> None:
help="Run in background.",
)
def imessage_start(
chat_identifier: str, background: bool,
chat_identifier: str,
background: bool,
) -> None:
"""Start the iMessage daemon for CHAT_IDENTIFIER.
@@ -61,9 +64,7 @@ def imessage_start(
console = Console()
if is_running():
console.print(
"[yellow]iMessage daemon already running.[/yellow]"
)
console.print("[yellow]iMessage daemon already running.[/yellow]")
return
if background:
@@ -71,9 +72,11 @@ def imessage_start(
proc = subprocess.Popen(
[
sys.executable, "-m",
sys.executable,
"-m",
"openjarvis.channels.imessage_daemon",
"--chat", chat_identifier,
"--chat",
chat_identifier,
],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
@@ -88,8 +91,7 @@ def imessage_start(
)
else:
console.print(
f"[green]Starting iMessage daemon[/green] "
f"— monitoring {chat_identifier}"
f"[green]Starting iMessage daemon[/green] — monitoring {chat_identifier}"
)
console.print("Press Ctrl+C to stop.\n")
@@ -117,13 +119,16 @@ def imessage_start(
KnowledgeSearchTool(retriever=retriever),
KnowledgeSQLTool(store=store),
ScanChunksTool(
store=store, engine=engine,
store=store,
engine=engine,
model="qwen3.5:4b",
),
ThinkTool(),
]
agent = DeepResearchAgent(
engine=engine, model="qwen3.5:4b", tools=tools,
engine=engine,
model="qwen3.5:4b",
tools=tools,
)
def handler(text: str) -> str:
@@ -145,6 +150,4 @@ def imessage_stop() -> None:
if stop_daemon():
console.print("[green]iMessage daemon stopped.[/green]")
else:
console.print(
"[dim]iMessage daemon is not running.[/dim]"
)
console.print("[dim]iMessage daemon is not running.[/dim]")
+3 -8
View File
@@ -162,8 +162,7 @@ def chat(
continue
elif cmd == "/model":
console.print(
f"Model: [cyan]{model}[/cyan] "
f"Engine: [cyan]{engine_name}[/cyan]"
f"Model: [cyan]{model}[/cyan] Engine: [cyan]{engine_name}[/cyan]"
)
continue
elif cmd == "/help":
@@ -183,9 +182,7 @@ def chat(
for msg in history:
role_str = msg.role if isinstance(msg.role, str) else msg.role.value
role = role_str.upper()
console.print(
f"[bold]{role}:[/bold] {msg.content[:200]}"
)
console.print(f"[bold]{role}:[/bold] {msg.content[:200]}")
continue
# Add user message
@@ -196,9 +193,7 @@ def chat(
if agent is not None:
response = agent.run(user_input)
content = (
response.content
if hasattr(response, "content")
else str(response)
response.content if hasattr(response, "content") else str(response)
)
else:
result = engine.generate(history, model=model)
+33 -12
View File
@@ -31,7 +31,10 @@ def compose() -> None:
@compose.command("list")
@click.option(
"-k", "--kind", "kind", default=None,
"-k",
"--kind",
"kind",
default=None,
type=click.Choice(["discrete", "operator"]),
help="Filter by recipe kind.",
)
@@ -203,13 +206,19 @@ def compose_run(name: str, query: tuple[str, ...], output_json: bool) -> None:
if output_json:
import json as json_mod
if isinstance(result, str):
click.echo(json_mod.dumps({"content": result}, indent=2))
else:
click.echo(json_mod.dumps({
"content": result.content,
"turns": getattr(result, "turns", 1),
}, indent=2))
click.echo(
json_mod.dumps(
{
"content": result.content,
"turns": getattr(result, "turns", 1),
},
indent=2,
)
)
else:
content = result if isinstance(result, str) else result.content
click.echo(content)
@@ -228,19 +237,33 @@ def compose_run(name: str, query: tuple[str, ...], output_json: bool) -> None:
@compose.command("bench")
@click.argument("name")
@click.option(
"-b", "--benchmark", "benchmark", default=None, multiple=True,
"-b",
"--benchmark",
"benchmark",
default=None,
multiple=True,
help="Override benchmarks (can specify multiple).",
)
@click.option(
"-n", "--max-samples", "max_samples", type=int, default=None,
"-n",
"--max-samples",
"max_samples",
type=int,
default=None,
help="Maximum samples per benchmark.",
)
@click.option(
"--judge", "judge_model", default=None,
"--judge",
"judge_model",
default=None,
help="LLM judge model override.",
)
@click.option(
"-v", "--verbose", "verbose", is_flag=True, default=False,
"-v",
"--verbose",
"verbose",
is_flag=True,
default=False,
help="Verbose logging.",
)
def compose_bench(
@@ -302,9 +325,7 @@ def compose_bench(
results_table.add_column("Errors", justify="right", style="red")
for i, rc in enumerate(run_configs, 1):
console.print(
f"\n[bold]Run {i}/{len(run_configs)}:[/bold] {rc.benchmark}"
)
console.print(f"\n[bold]Run {i}/{len(run_configs)}:[/bold] {rc.benchmark}")
try:
summary = _run_single(rc, console=console)
results_table.add_row(
+14 -31
View File
@@ -30,6 +30,7 @@ class DashboardApp:
"""Check if textual is available."""
try:
import textual # noqa: F401
return True
except ImportError:
return False
@@ -86,21 +87,16 @@ class DashboardApp:
classes="panel",
)
yield Log(
id="events-panel", classes="panel",
id="events-panel",
classes="panel",
)
yield Static(
"Telemetry\n"
"─────────\n"
"Throughput: --\n"
"Latency: --\n"
"Energy: --",
"Telemetry\n─────────\nThroughput: --\nLatency: --\nEnergy: --",
id="telemetry-panel",
classes="panel",
)
yield Static(
"Agent Activity\n"
"──────────────\n"
"No active agents.",
"Agent Activity\n──────────────\nNo active agents.",
id="agent-panel",
classes="panel",
)
@@ -113,6 +109,7 @@ class DashboardApp:
# Try to connect to event bus
try:
from openjarvis.core.events import get_event_bus
bus = get_event_bus()
def _on_event(event: Any) -> None:
@@ -124,6 +121,7 @@ class DashboardApp:
logger.debug("Event serialization failed: %s", exc)
from openjarvis.core.events import EventType
for et in EventType:
bus.subscribe(et, _on_event)
except Exception:
@@ -137,30 +135,16 @@ class DashboardApp:
lines = ["System Status", "─────────────"]
try:
from openjarvis.core.config import load_config
config = load_config()
lines.append(
f"Engine: {config.engine.default}"
)
model = (
config.intelligence.default_model
or 'auto'
)
lines.append(f"Engine: {config.engine.default}")
model = config.intelligence.default_model or "auto"
lines.append(f"Model: {model}")
backend = (
config.tools.storage.default_backend
)
backend = config.tools.storage.default_backend
lines.append(f"Memory: {backend}")
sec = (
'enabled'
if config.security.enabled
else 'disabled'
)
sec = "enabled" if config.security.enabled else "disabled"
lines.append(f"Security: {sec}")
tel = (
'enabled'
if config.telemetry.enabled
else 'disabled'
)
tel = "enabled" if config.telemetry.enabled else "disabled"
lines.append(f"Telemetry: {tel}")
except Exception:
lines.append("Config: not loaded")
@@ -175,8 +159,7 @@ def launch_dashboard(config: Optional[Any] = None) -> None:
app = DashboardApp(config=config)
if not app.available():
raise ImportError(
"TUI dashboard requires 'textual'. "
"Install with: uv sync --extra dashboard"
"TUI dashboard requires 'textual'. Install with: uv sync --extra dashboard"
)
app.run()
+50 -40
View File
@@ -57,26 +57,32 @@ def detect_local_sources(
notes_path = notes_db_path or _DEFAULT_NOTES_DB
if notes_path.exists():
sources.append({
"connector_id": "apple_notes",
"display_name": "Apple Notes",
"config": {"db_path": str(notes_path)},
})
sources.append(
{
"connector_id": "apple_notes",
"display_name": "Apple Notes",
"config": {"db_path": str(notes_path)},
}
)
imessage_path = imessage_db_path or _DEFAULT_IMESSAGE_DB
if imessage_path.exists():
sources.append({
"connector_id": "imessage",
"display_name": "iMessage",
"config": {"db_path": str(imessage_path)},
})
sources.append(
{
"connector_id": "imessage",
"display_name": "iMessage",
"config": {"db_path": str(imessage_path)},
}
)
if obsidian_vault_path and obsidian_vault_path.is_dir():
sources.append({
"connector_id": "obsidian",
"display_name": "Obsidian / Markdown",
"config": {"vault_path": str(obsidian_vault_path)},
})
sources.append(
{
"connector_id": "obsidian",
"display_name": "Obsidian / Markdown",
"config": {"vault_path": str(obsidian_vault_path)},
}
)
return sources
@@ -137,11 +143,13 @@ def detect_token_sources(
continue
if not data or not any(v for v in data.values() if v):
continue
sources.append({
"connector_id": ts["connector_id"],
"display_name": ts["display_name"],
"config": {},
})
sources.append(
{
"connector_id": ts["connector_id"],
"display_name": ts["display_name"],
"config": {},
}
)
return sources
@@ -154,8 +162,7 @@ def _prompt_connect_sources(console: Console) -> List[Dict[str, Any]]:
while True:
unconnected = [
ts for ts in _TOKEN_SOURCES
if not (cdir / ts["creds_file"]).exists()
ts for ts in _TOKEN_SOURCES if not (cdir / ts["creds_file"]).exists()
]
if not unconnected:
console.print("[dim]All token sources already connected.[/dim]")
@@ -165,10 +172,7 @@ def _prompt_connect_sources(console: Console) -> List[Dict[str, Any]]:
break
names = [ts["connector_id"] for ts in unconnected]
labels = [
f"{ts['display_name']} ({ts['connector_id']})"
for ts in unconnected
]
labels = [f"{ts['display_name']} ({ts['connector_id']})" for ts in unconnected]
console.print("Available:")
for label in labels:
console.print(f" {label}")
@@ -185,11 +189,13 @@ def _prompt_connect_sources(console: Console) -> List[Dict[str, Any]]:
connector.handle_callback(token.strip())
console.print(f" [green]{ts['display_name']}: connected![/green]")
connected.append({
"connector_id": choice,
"display_name": ts["display_name"],
"config": {},
})
connected.append(
{
"connector_id": choice,
"display_name": ts["display_name"],
"config": {},
}
)
return connected
@@ -203,27 +209,35 @@ def _instantiate_connector(connector_id: str, config: Dict[str, Any]) -> Any:
"""Lazily import and instantiate a connector by ID."""
if connector_id == "apple_notes":
from openjarvis.connectors.apple_notes import AppleNotesConnector
return AppleNotesConnector(db_path=config.get("db_path", ""))
elif connector_id == "imessage":
from openjarvis.connectors.imessage import IMessageConnector
return IMessageConnector(db_path=config.get("db_path", ""))
elif connector_id == "obsidian":
from openjarvis.connectors.obsidian import ObsidianConnector
return ObsidianConnector(vault_path=config.get("vault_path", ""))
elif connector_id == "gmail_imap":
from openjarvis.connectors.gmail_imap import GmailIMAPConnector
return GmailIMAPConnector()
elif connector_id == "outlook":
from openjarvis.connectors.outlook import OutlookConnector
return OutlookConnector()
elif connector_id == "slack":
from openjarvis.connectors.slack_connector import SlackConnector
return SlackConnector()
elif connector_id == "notion":
from openjarvis.connectors.notion import NotionConnector
return NotionConnector()
elif connector_id == "granola":
from openjarvis.connectors.granola import GranolaConnector
return GranolaConnector()
else:
msg = f"Unknown connector: {connector_id}"
@@ -277,8 +291,7 @@ def _launch_chat(store: KnowledgeStore, console: Console) -> None:
engine = OllamaEngine()
if not engine.health():
console.print(
"[red]Ollama is not running.[/red] Start it with: "
"[bold]ollama serve[/bold]"
"[red]Ollama is not running.[/red] Start it with: [bold]ollama serve[/bold]"
)
return
@@ -404,18 +417,15 @@ def deep_research_setup(obsidian_vault: Optional[str], skip_chat: bool) -> None:
for src in all_sources:
try:
connector = _instantiate_connector(
src["connector_id"], src["config"],
src["connector_id"],
src["config"],
)
pipeline = IngestionPipeline(store)
engine = SyncEngine(pipeline)
chunks = engine.sync(connector)
console.print(
f" {src['display_name']}: [green]{chunks} chunks[/green]"
)
console.print(f" {src['display_name']}: [green]{chunks} chunks[/green]")
except Exception as exc: # noqa: BLE001
console.print(
f" {src['display_name']}: [red]error: {exc}[/red]"
)
console.print(f" {src['display_name']}: [red]error: {exc}[/red]")
total = store.count()
console.print(
+125 -77
View File
@@ -28,7 +28,8 @@ KNOWN_BENCHMARKS = {
"swebench": {"category": "agentic", "description": "SWE-bench code patches"},
"swefficiency": {"category": "agentic", "description": "SWEfficiency optimization"},
"terminalbench": {
"category": "agentic", "description": "TerminalBench terminal tasks",
"category": "agentic",
"description": "TerminalBench terminal tasks",
},
"terminalbench-native": {
"category": "agentic",
@@ -98,95 +99,155 @@ def eval_list() -> None:
@eval_group.command("run")
@click.option(
"-c", "--config", "config_path", default=None, type=click.Path(),
"-c",
"--config",
"config_path",
default=None,
type=click.Path(),
help="TOML config file for suite runs.",
)
@click.option(
"-b", "--benchmark", "benchmark", default=None,
"-b",
"--benchmark",
"benchmark",
default=None,
help="Benchmark to run (e.g. supergpqa, gaia, frames, wildchat).",
)
@click.option(
"-m", "--model", "model", default=None,
"-m",
"--model",
"model",
default=None,
help="Model identifier.",
)
@click.option(
"-n", "--max-samples", "max_samples", type=int, default=None,
"-n",
"--max-samples",
"max_samples",
type=int,
default=None,
help="Maximum samples to evaluate.",
)
@click.option(
"--backend", "backend", default="jarvis-direct",
"--backend",
"backend",
default="jarvis-direct",
help="Inference backend (jarvis-direct or jarvis-agent).",
)
@click.option(
"--agent", "agent_name", default=None,
"--agent",
"agent_name",
default=None,
help="Agent name for jarvis-agent backend.",
)
@click.option(
"-e", "--engine", "engine_key", default=None,
"-e",
"--engine",
"engine_key",
default=None,
help="Engine key (ollama, vllm, cloud, ...).",
)
@click.option(
"--tools", "tools", default="",
"--tools",
"tools",
default="",
help="Comma-separated tool names.",
)
@click.option(
"--telemetry/--no-telemetry", "telemetry", default=False,
"--telemetry/--no-telemetry",
"telemetry",
default=False,
help="Enable telemetry collection during eval.",
)
@click.option(
"--gpu-metrics/--no-gpu-metrics", "gpu_metrics", default=False,
"--gpu-metrics/--no-gpu-metrics",
"gpu_metrics",
default=False,
help="Enable GPU metrics collection.",
)
@click.option(
"--seed", "seed", type=int, default=42,
"--seed",
"seed",
type=int,
default=42,
help="Random seed.",
)
@click.option(
"--temperature", "temperature", type=float, default=0.0,
"--temperature",
"temperature",
type=float,
default=0.0,
help="Generation temperature.",
)
@click.option(
"--max-tokens", "max_tokens", type=int, default=2048,
"--max-tokens",
"max_tokens",
type=int,
default=2048,
help="Max output tokens.",
)
@click.option(
"--model-filter", "model_filter", default=None,
"--model-filter",
"model_filter",
default=None,
help="Filter models by name substring (for multi-model configs).",
)
@click.option(
"-o", "--output", "output_path", default=None, type=click.Path(),
"-o",
"--output",
"output_path",
default=None,
type=click.Path(),
help="Output JSONL path.",
)
@click.option(
"--wandb-project", "wandb_project", default="",
"--wandb-project",
"wandb_project",
default="",
help="W&B project name (enables W&B tracking).",
)
@click.option(
"--wandb-entity", "wandb_entity", default="",
"--wandb-entity",
"wandb_entity",
default="",
help="W&B entity (team or user).",
)
@click.option(
"--wandb-tags", "wandb_tags", default="",
"--wandb-tags",
"wandb_tags",
default="",
help="Comma-separated W&B tags.",
)
@click.option(
"--wandb-group", "wandb_group", default="",
"--wandb-group",
"wandb_group",
default="",
help="W&B run group.",
)
@click.option(
"--sheets-id", "sheets_spreadsheet_id", default="",
"--sheets-id",
"sheets_spreadsheet_id",
default="",
help="Google Sheets spreadsheet ID.",
)
@click.option(
"--sheets-worksheet", "sheets_worksheet", default="Results",
"--sheets-worksheet",
"sheets_worksheet",
default="Results",
help="Google Sheets worksheet name.",
)
@click.option(
"--sheets-creds", "sheets_credentials_path", default="",
"--sheets-creds",
"sheets_credentials_path",
default="",
help="Path to Google service account JSON.",
)
@click.option(
"-v", "--verbose", "verbose", is_flag=True, default=False,
"-v",
"--verbose",
"verbose",
is_flag=True,
default=False,
help="Verbose logging.",
)
def eval_run(
@@ -237,13 +298,9 @@ def eval_run(
# Filter by model name substring if requested
if model_filter:
run_configs = [
rc for rc in run_configs if model_filter in rc.model
]
run_configs = [rc for rc in run_configs if model_filter in rc.model]
if not run_configs:
console.print(
f"[red]No models match filter '{model_filter}'[/red]"
)
console.print(f"[red]No models match filter '{model_filter}'[/red]")
sys.exit(1)
console.print(
@@ -257,9 +314,7 @@ def eval_run(
try:
from openjarvis.evals.cli import _run_single
except ImportError:
console.print(
"[red]Eval CLI module not available.[/red]"
)
console.print("[red]Eval CLI module not available.[/red]")
sys.exit(1)
for i, rc in enumerate(run_configs, 1):
@@ -286,9 +341,7 @@ def eval_run(
)
if benchmark not in KNOWN_BENCHMARKS:
console.print(
f"[yellow]Warning: unknown benchmark '{benchmark}'[/yellow]"
)
console.print(f"[yellow]Warning: unknown benchmark '{benchmark}'[/yellow]")
try:
from openjarvis.evals.core.types import RunConfig
@@ -299,9 +352,7 @@ def eval_run(
)
sys.exit(1)
tool_list = (
[t.strip() for t in tools.split(",") if t.strip()] if tools else []
)
tool_list = [t.strip() for t in tools.split(",") if t.strip()] if tools else []
config = RunConfig(
benchmark=benchmark,
@@ -340,9 +391,7 @@ def eval_run(
f"({summary.correct}/{summary.scored_samples})"
)
except ImportError:
console.print(
"[red]Eval CLI module not available.[/red]"
)
console.print("[red]Eval CLI module not available.[/red]")
sys.exit(1)
except Exception as exc:
console.print(f"[red]Error: {exc}[/red]")
@@ -352,7 +401,9 @@ def eval_run(
@eval_group.command("compare")
@click.argument("result_files", nargs=-1, required=True)
@click.option(
"--metric", "metric", default="accuracy",
"--metric",
"metric",
default="accuracy",
help="Metric to compare across runs (default: accuracy).",
)
def eval_compare(result_files: tuple[str, ...], metric: str) -> None:
@@ -367,13 +418,15 @@ def eval_compare(result_files: tuple[str, ...], metric: str) -> None:
if summary_path.exists():
with open(summary_path) as f:
data = json.load(f)
rows.append({
"file": path.name,
"benchmark": data.get("benchmark", "?"),
"model": data.get("model", "?"),
"value": data.get(metric, "N/A"),
"samples": data.get("total_samples", 0),
})
rows.append(
{
"file": path.name,
"benchmark": data.get("benchmark", "?"),
"model": data.get("model", "?"),
"value": data.get(metric, "N/A"),
"samples": data.get("total_samples", 0),
}
)
elif path.exists() and path.suffix == ".jsonl":
# Fall back to reading JSONL and computing metric on-the-fly
records = []
@@ -384,29 +437,25 @@ def eval_compare(result_files: tuple[str, ...], metric: str) -> None:
records.append(json.loads(line))
if records:
if metric == "accuracy":
scored = [
r for r in records
if r.get("is_correct") is not None
]
scored = [r for r in records if r.get("is_correct") is not None]
correct = [r for r in scored if r["is_correct"]]
value = (
len(correct) / len(scored) if scored else 0.0
)
value = len(correct) / len(scored) if scored else 0.0
else:
values = [
r[metric] for r in records
r[metric]
for r in records
if metric in r and r[metric] is not None
]
value = (
sum(values) / len(values) if values else "N/A"
)
rows.append({
"file": path.name,
"benchmark": records[0].get("benchmark", "?"),
"model": records[0].get("model", "?"),
"value": value,
"samples": len(records),
})
value = sum(values) / len(values) if values else "N/A"
rows.append(
{
"file": path.name,
"benchmark": records[0].get("benchmark", "?"),
"model": records[0].get("model", "?"),
"value": value,
"samples": len(records),
}
)
else:
console.print(f"[yellow]Skipping missing file: {path_str}[/yellow]")
@@ -429,8 +478,11 @@ def eval_compare(result_files: tuple[str, ...], metric: str) -> None:
val = row["value"]
val_str = f"{val:.4f}" if isinstance(val, float) else str(val)
table.add_row(
row["file"], row["benchmark"], row["model"],
val_str, str(row["samples"]),
row["file"],
row["benchmark"],
row["model"],
val_str,
str(row["samples"]),
)
console.print(table)
@@ -454,8 +506,7 @@ def eval_report(result_file: str) -> None:
console.print(f" [cyan]Model:[/cyan] {data.get('model', '?')}")
console.print(f" [cyan]Backend:[/cyan] {data.get('backend', '?')}")
console.print(
f" [cyan]Accuracy:[/cyan] "
f"[bold]{data.get('accuracy', 0.0):.4f}[/bold]"
f" [cyan]Accuracy:[/cyan] [bold]{data.get('accuracy', 0.0):.4f}[/bold]"
)
console.print(f" [cyan]Samples:[/cyan] {data.get('total_samples', 0)}")
console.print(f" [cyan]Scored:[/cyan] {data.get('scored_samples', 0)}")
@@ -466,8 +517,7 @@ def eval_report(result_file: str) -> None:
f"{data.get('mean_latency_seconds', 0.0):.4f}s (mean)"
)
console.print(
f" [cyan]Cost:[/cyan] "
f"${data.get('total_cost_usd', 0.0):.6f}"
f" [cyan]Cost:[/cyan] ${data.get('total_cost_usd', 0.0):.6f}"
)
# Per-subject breakdown
@@ -521,9 +571,7 @@ def eval_report(result_file: str) -> None:
console.print(f" [cyan]Total:[/cyan] {len(records)}")
console.print(f" [cyan]Scored:[/cyan] {len(scored)}")
console.print(f" [cyan]Correct:[/cyan] {len(correct)}")
console.print(
f" [cyan]Accuracy:[/cyan] [bold]{accuracy:.4f}[/bold]"
)
console.print(f" [cyan]Accuracy:[/cyan] [bold]{accuracy:.4f}[/bold]")
console.print(f" [cyan]Errors:[/cyan] {len(errors)}")
+24 -28
View File
@@ -18,7 +18,10 @@ def feedback_group() -> None:
@feedback_group.command("score")
@click.argument("trace_id")
@click.option(
"-s", "--score", type=float, required=True,
"-s",
"--score",
type=float,
required=True,
help="Feedback score (0.0-1.0).",
)
def feedback_score(trace_id: str, score: float) -> None:
@@ -43,13 +46,9 @@ def feedback_score(trace_id: str, score: float) -> None:
store.close()
if updated:
console.print(
f"[green]Recorded score {score} for trace {trace_id}[/green]"
)
console.print(f"[green]Recorded score {score} for trace {trace_id}[/green]")
else:
console.print(
f"[yellow]Trace '{trace_id}' not found.[/yellow]"
)
console.print(f"[yellow]Trace '{trace_id}' not found.[/yellow]")
except Exception as exc:
console.print(f"[red]Error: {exc}[/red]")
sys.exit(1)
@@ -57,16 +56,22 @@ def feedback_score(trace_id: str, score: float) -> None:
@feedback_group.command("thumbs")
@click.option(
"--last", is_flag=True, default=False,
"--last",
is_flag=True,
default=False,
help="Rate the last interaction.",
)
@click.option(
"--up/--down", "thumbs_up", default=True,
"--up/--down",
"thumbs_up",
default=True,
help="Thumbs up or down.",
)
@click.argument("trace_id", required=False)
def feedback_thumbs(
last: bool, thumbs_up: bool, trace_id: Optional[str],
last: bool,
thumbs_up: bool,
trace_id: Optional[str],
) -> None:
"""Rate a trace with thumbs up/down."""
console = Console()
@@ -97,13 +102,9 @@ def feedback_thumbs(
label = "thumbs up" if thumbs_up else "thumbs down"
if updated:
console.print(
f"[green]Recorded {label} for trace {trace_id}[/green]"
)
console.print(f"[green]Recorded {label} for trace {trace_id}[/green]")
else:
console.print(
f"[yellow]Trace '{trace_id}' not found.[/yellow]"
)
console.print(f"[yellow]Trace '{trace_id}' not found.[/yellow]")
except Exception as exc:
console.print(f"[red]Error: {exc}[/red]")
sys.exit(1)
@@ -111,7 +112,9 @@ def feedback_thumbs(
@feedback_group.command("evaluate")
@click.option(
"--since", type=str, default="7d",
"--since",
type=str,
default="7d",
help="Evaluate traces since (e.g. 7d, 24h).",
)
def feedback_evaluate(since: str) -> None:
@@ -125,15 +128,11 @@ def feedback_evaluate(since: str) -> None:
value = float(since[:-1])
seconds = value * multipliers.get(unit, 86400)
except (ValueError, IndexError):
console.print(
f"[red]Invalid duration '{since}'. Use e.g. 7d, 24h, 30m.[/red]"
)
console.print(f"[red]Invalid duration '{since}'. Use e.g. 7d, 24h, 30m.[/red]")
sys.exit(1)
since_ts = time.time() - seconds
console.print(
f"[cyan]Evaluating traces from the last {since}...[/cyan]"
)
console.print(f"[cyan]Evaluating traces from the last {since}...[/cyan]")
try:
from openjarvis.core.config import DEFAULT_CONFIG_DIR
@@ -153,8 +152,7 @@ def feedback_evaluate(since: str) -> None:
return
console.print(
"[yellow]LLM judge evaluation is not yet "
"fully implemented.[/yellow]"
"[yellow]LLM judge evaluation is not yet fully implemented.[/yellow]"
)
except Exception as exc:
console.print(f"[red]Error: {exc}[/red]")
@@ -183,9 +181,7 @@ def feedback_stats() -> None:
total = len(all_traces)
scored_count = len(scored)
mean_score = (
sum(t.feedback for t in scored) / scored_count
if scored_count > 0
else 0.0
sum(t.feedback for t in scored) / scored_count if scored_count > 0 else 0.0
)
positive = sum(1 for t in scored if t.feedback >= 0.5)
+7 -8
View File
@@ -31,27 +31,26 @@ def start(install: bool) -> None:
if plat.system() == "Darwin":
plist_path = (
Path.home()
/ "Library/LaunchAgents/com.openjarvis.gateway.plist"
Path.home() / "Library/LaunchAgents/com.openjarvis.gateway.plist"
)
generate_launchd_plist(plist_path)
click.echo(f"Wrote {plist_path}")
subprocess.run(
["launchctl", "load", str(plist_path)], check=False,
["launchctl", "load", str(plist_path)],
check=False,
)
else:
service_path = (
Path.home()
/ ".config/systemd/user/openjarvis-gateway.service"
Path.home() / ".config/systemd/user/openjarvis-gateway.service"
)
generate_systemd_service(service_path)
click.echo(f"Wrote {service_path}")
subprocess.run(
["systemctl", "--user", "daemon-reload"], check=False,
["systemctl", "--user", "daemon-reload"],
check=False,
)
subprocess.run(
["systemctl", "--user", "enable", "--now",
"openjarvis-gateway"],
["systemctl", "--user", "enable", "--now", "openjarvis-gateway"],
check=False,
)
else:
+7 -21
View File
@@ -136,9 +136,7 @@ def _install_backend(backend: str, console: Console) -> bool:
display = info["display"]
console.print()
console.print(
f"[yellow]Backend [bold]{display}[/bold] is not installed.[/yellow]"
)
console.print(f"[yellow]Backend [bold]{display}[/bold] is not installed.[/yellow]")
uv_available = shutil.which("uv") is not None
in_uv_project = os.path.exists("pyproject.toml") and os.path.exists("uv.lock")
@@ -168,9 +166,7 @@ def _install_backend(backend: str, console: Console) -> bool:
console.print(f"[bold]Running:[/bold] {install_label}")
result = subprocess.run(install_cmd)
if result.returncode != 0:
console.print(
f"[red]Installation failed (exit {result.returncode}).[/red]"
)
console.print(f"[red]Installation failed (exit {result.returncode}).[/red]")
return False
console.print(f"[green]{display} installed successfully.[/green]\n")
@@ -208,11 +204,7 @@ def _install_binary_backend(backend: str, console: Console) -> bool:
" Or from source:\n"
" https://github.com/exo-explore/exo"
),
"uzu": (
"Install Uzu:\n"
"\n"
" See https://uzu.ai for installation instructions."
),
"uzu": ("Install Uzu:\n\n See https://uzu.ai for installation instructions."),
}
console.print()
@@ -241,8 +233,7 @@ def _install_binary_backend(backend: str, console: Console) -> bool:
console.print("[green]Ollama installed successfully.[/green]\n")
return True
console.print(
"[red]Installation may have failed. "
"Check above for errors.[/red]"
"[red]Installation may have failed. Check above for errors.[/red]"
)
return False
@@ -340,9 +331,7 @@ def host(
raise SystemExit(1)
backend = detected
name = _BACKENDS[backend]["display"]
console.print(
f"Auto-detected backend: [bold cyan]{name}[/bold cyan]"
)
console.print(f"Auto-detected backend: [bold cyan]{name}[/bold cyan]")
info = _BACKENDS[backend]
@@ -381,12 +370,9 @@ def host(
console.print()
if backend != "ollama":
console.print(f"[dim]The model server will be available at {host_url}[/dim]")
console.print(
f"[dim]The model server will be available at {host_url}[/dim]"
)
console.print(
"[dim]OpenJarvis will auto-discover it. "
"Press Ctrl+C to stop.[/dim]\n"
"[dim]OpenJarvis will auto-discover it. Press Ctrl+C to stop.[/dim]\n"
)
try:
+4 -4
View File
@@ -58,12 +58,12 @@ def setup_logging(
log_dir.mkdir(parents=True, exist_ok=True)
log_file = log_dir / "cli.log"
file_handler = RotatingFileHandler(
str(log_file), maxBytes=5 * 1024 * 1024, backupCount=3,
str(log_file),
maxBytes=5 * 1024 * 1024,
backupCount=3,
)
file_handler.setLevel(logging.DEBUG)
file_fmt = logging.Formatter(
"%(asctime)s %(levelname)s %(name)s: %(message)s"
)
file_fmt = logging.Formatter("%(asctime)s %(levelname)s %(name)s: %(message)s")
file_handler.setFormatter(file_fmt)
logger.addHandler(file_handler)
+19 -6
View File
@@ -43,15 +43,21 @@ def memory() -> None:
@memory.command()
@click.argument("path")
@click.option(
"--backend", "-b", default=None,
"--backend",
"-b",
default=None,
help="Override the default memory backend.",
)
@click.option(
"--chunk-size", default=512, type=int,
"--chunk-size",
default=512,
type=int,
help="Chunk size in tokens.",
)
@click.option(
"--chunk-overlap", default=64, type=int,
"--chunk-overlap",
default=64,
type=int,
help="Overlap between chunks in tokens.",
)
def index(
@@ -108,11 +114,16 @@ def index(
@memory.command()
@click.argument("query", nargs=-1, required=True)
@click.option(
"--top-k", "-k", default=5, type=int,
"--top-k",
"-k",
default=5,
type=int,
help="Number of results to return.",
)
@click.option(
"--backend", "-b", default=None,
"--backend",
"-b",
default=None,
help="Override the default memory backend.",
)
def search(
@@ -158,7 +169,9 @@ def search(
@memory.command()
@click.option(
"--backend", "-b", default=None,
"--backend",
"-b",
default=None,
help="Override the default memory backend.",
)
def stats(backend: str | None) -> None:
+4 -9
View File
@@ -97,15 +97,11 @@ def info(model_name: str) -> None:
spec = ModelRegistry.get(model_name)
params = f"{spec.parameter_count_b}B" if spec.parameter_count_b else "unknown"
active = (
f"{spec.active_parameter_count_b}B"
if spec.active_parameter_count_b
else "-"
f"{spec.active_parameter_count_b}B" if spec.active_parameter_count_b else "-"
)
ctx_len = f"{spec.context_length:,}" if spec.context_length else "unknown"
vram = f"{spec.min_vram_gb}GB" if spec.min_vram_gb else "-"
engines_str = (
", ".join(spec.supported_engines) if spec.supported_engines else "-"
)
engines_str = ", ".join(spec.supported_engines) if spec.supported_engines else "-"
provider = spec.provider or "-"
api_key = "required" if spec.requires_api_key else "not required"
lines = [
@@ -163,6 +159,7 @@ def ollama_pull(host: str, model_name: str, console: Console) -> bool:
) as resp:
resp.raise_for_status()
import json
for line in resp.iter_lines():
if not line.strip():
continue
@@ -219,9 +216,7 @@ def hf_download(repo: str, filename: str | None, console: Console) -> bool:
@model.command()
@click.argument("model_name")
@click.option(
"--engine", default=None, help="Engine to download for."
)
@click.option("--engine", default=None, help="Engine to download for.")
def pull(model_name: str, engine: str | None) -> None:
"""Download a model."""
console = Console()
+6 -6
View File
@@ -29,10 +29,10 @@ def list_operators() -> None:
from openjarvis.operators.loader import load_operator
config = load_config()
manifests_dir = Path(
config.operators.manifests_dir
).expanduser() if hasattr(config, "operators") else (
DEFAULT_CONFIG_DIR / "operators"
manifests_dir = (
Path(config.operators.manifests_dir).expanduser()
if hasattr(config, "operators")
else (DEFAULT_CONFIG_DIR / "operators")
)
# Also check project-local operators/ directory
@@ -55,8 +55,7 @@ def list_operators() -> None:
if not found:
console.print("[dim]No operators discovered.[/dim]")
console.print(
f"[dim]Place TOML manifests in {manifests_dir} "
f"or ./operators/[/dim]"
f"[dim]Place TOML manifests in {manifests_dir} or ./operators/[/dim]"
)
return
@@ -189,6 +188,7 @@ def logs(operator_id: str, lines: int) -> None:
db_path = config.scheduler.db_path
if not db_path:
from openjarvis.core.config import DEFAULT_CONFIG_DIR
db_path = str(DEFAULT_CONFIG_DIR / "scheduler.db")
store = SchedulerStore(db_path=db_path)
+40 -24
View File
@@ -18,27 +18,43 @@ def optimize_group() -> None:
@optimize_group.command("run")
@click.option(
"-c", "--config", "config_path", type=str, default=None,
"-c",
"--config",
"config_path",
type=str,
default=None,
help="TOML config file for the optimization run.",
)
@click.option(
"-b", "--benchmark", type=str, default=None,
"-b",
"--benchmark",
type=str,
default=None,
help="Benchmark name (e.g. supergpqa, mmlu-pro).",
)
@click.option(
"-t", "--trials", type=int, default=20,
"-t",
"--trials",
type=int,
default=20,
help="Maximum number of trials.",
)
@click.option(
"--optimizer-model", type=str, default="claude-sonnet-4-6",
"--optimizer-model",
type=str,
default="claude-sonnet-4-6",
help="Model used by the LLM optimizer.",
)
@click.option(
"--max-samples", type=int, default=50,
"--max-samples",
type=int,
default=50,
help="Maximum samples per trial evaluation.",
)
@click.option(
"--output-dir", type=str, default="results/optimize/",
"--output-dir",
type=str,
default="results/optimize/",
help="Directory for trial output files.",
)
def optimize_run(
@@ -58,9 +74,7 @@ def optimize_run(
try:
from openjarvis.learning.optimize.config import load_optimize_config
except ImportError:
console.print(
"[red]Optimization framework not available.[/red]"
)
console.print("[red]Optimization framework not available.[/red]")
sys.exit(1)
try:
@@ -167,7 +181,8 @@ def optimize_run(
early_stop = 5
if data is not None:
early_stop = data.get("optimize", {}).get(
"early_stop_patience", early_stop,
"early_stop_patience",
early_stop,
)
engine = OptimizationEngine(
@@ -200,9 +215,7 @@ def optimize_run(
f"(accuracy={run.best_trial.accuracy:.4f})"
)
except ImportError as exc:
console.print(
f"[red]Missing dependency for optimization: {exc}[/red]"
)
console.print(f"[red]Missing dependency for optimization: {exc}[/red]")
sys.exit(1)
except Exception as exc:
console.print(f"[red]Optimization failed: {exc}[/red]")
@@ -321,7 +334,10 @@ def optimize_results(run_id: str) -> None:
@optimize_group.command("best")
@click.argument("run_id")
@click.option(
"-o", "--output", type=str, default=None,
"-o",
"--output",
type=str,
default=None,
help="Output recipe path (TOML).",
)
def optimize_best(run_id: str, output: Optional[str]) -> None:
@@ -350,15 +366,11 @@ def optimize_best(run_id: str, output: Optional[str]) -> None:
console.print("[yellow]No best trial found in this run.[/yellow]")
return
output_path = Path(
output or f"results/optimize/best_{run_id}.toml"
)
output_path = Path(output or f"results/optimize/best_{run_id}.toml")
engine = OptimizationEngine.__new__(OptimizationEngine)
engine.export_best_recipe(run, output_path)
console.print(
f"[green]Best recipe exported to:[/green] {output_path}"
)
console.print(f"[green]Best recipe exported to:[/green] {output_path}")
console.print(
f" Trial: {run.best_trial.trial_id} "
f"(accuracy={run.best_trial.accuracy:.4f})"
@@ -370,11 +382,16 @@ def optimize_best(run_id: str, output: Optional[str]) -> None:
@optimize_group.command("personal")
@click.argument("action", type=click.Choice(["synthesize", "run"]))
@click.option(
"--workflow", type=str, default="default",
"--workflow",
type=str,
default="default",
help="Workflow ID for personal optimization.",
)
@click.option(
"-t", "--trials", type=int, default=10,
"-t",
"--trials",
type=int,
default=10,
help="Maximum trials for personal optimization.",
)
def optimize_personal(action: str, workflow: str, trials: int) -> None:
@@ -396,8 +413,7 @@ def optimize_personal(action: str, workflow: str, trials: int) -> None:
f"workflow '{workflow}' (max {trials} trials)...[/cyan]"
)
console.print(
"[yellow]Personal optimization is not yet "
"fully implemented.[/yellow]"
"[yellow]Personal optimization is not yet fully implemented.[/yellow]"
)
+14 -13
View File
@@ -17,9 +17,9 @@ def _get_store() -> "SchedulerStore": # noqa: F821
from openjarvis.scheduler.store import SchedulerStore
config = load_config()
db_path = getattr(
getattr(config, "scheduler", None), "db_path", None
) or str(DEFAULT_CONFIG_DIR / "scheduler.db")
db_path = getattr(getattr(config, "scheduler", None), "db_path", None) or str(
DEFAULT_CONFIG_DIR / "scheduler.db"
)
return SchedulerStore(db_path)
@@ -38,13 +38,15 @@ def scheduler() -> None:
@scheduler.command("create")
@click.argument("prompt")
@click.option(
"--type", "schedule_type",
"--type",
"schedule_type",
required=True,
type=click.Choice(["cron", "interval", "once"]),
help="Schedule type.",
)
@click.option(
"--value", "schedule_value",
"--value",
"schedule_value",
required=True,
help="Schedule value (cron expr, seconds, or ISO datetime).",
)
@@ -82,7 +84,8 @@ def scheduler_create(
@scheduler.command("list")
@click.option(
"--status", default=None,
"--status",
default=None,
type=click.Choice(["active", "paused", "completed", "cancelled"]),
help="Filter by status.",
)
@@ -197,14 +200,10 @@ def scheduler_logs(task_id: str, limit: int) -> None:
table.add_column("Error", max_width=30)
for log in logs:
success_str = (
"[green]yes[/green]" if log["success"] else "[red]no[/red]"
)
success_str = "[green]yes[/green]" if log["success"] else "[red]no[/red]"
result_text = log["result"]
result_short = (
(result_text[:37] + "...")
if len(result_text) > 40
else result_text
(result_text[:37] + "...") if len(result_text) > 40 else result_text
)
table.add_row(
str(log["id"]),
@@ -221,7 +220,9 @@ def scheduler_logs(task_id: str, limit: int) -> None:
@scheduler.command("start")
@click.option(
"--poll-interval", default=60, type=int,
"--poll-interval",
default=60,
type=int,
help="Seconds between poll cycles.",
)
def scheduler_start(poll_interval: int) -> None:
+37 -35
View File
@@ -26,13 +26,18 @@ logger = logging.getLogger(__name__)
@click.command()
@click.option("--host", default=None, help="Bind address (default: config).")
@click.option(
"--port", default=None, type=int,
"--port",
default=None,
type=int,
help="Port number (default: config).",
)
@click.option("-e", "--engine", "engine_key", default=None, help="Engine backend.")
@click.option("-m", "--model", "model_name", default=None, help="Default model.")
@click.option(
"-a", "--agent", "agent_name", default=None,
"-a",
"--agent",
"agent_name",
default=None,
help="Agent for non-streaming requests (simple, orchestrator, react, openhands).",
)
def serve(
@@ -94,12 +99,14 @@ def serve(
# Apply security guardrails
from openjarvis.security import setup_security
sec = setup_security(config, engine, bus)
engine = sec.engine
# If cloud API keys are set, wrap with MultiEngine so both local
# and cloud models appear in the model list and can be used.
import os
_has_cloud = (
os.environ.get("OPENAI_API_KEY")
or os.environ.get("ANTHROPIC_API_KEY")
@@ -183,13 +190,13 @@ def serve(
if configured:
if isinstance(configured, list):
allowed = {
t.strip() for t in configured
t.strip()
for t in configured
if isinstance(t, str) and t.strip()
}
else:
allowed = {
t.strip() for t in configured.split(",")
if t.strip()
t.strip() for t in configured.split(",") if t.strip()
}
else:
allowed = _DEFAULT_TOOLS
@@ -214,6 +221,7 @@ def serve(
agent = agent_cls(engine, model_name, **agent_kwargs)
except Exception as exc:
import traceback
console.print(f"[yellow]Agent '{agent_key}' failed to load: {exc}[/yellow]")
traceback.print_exc()
@@ -254,6 +262,7 @@ def serve(
speech_backend = None
try:
from openjarvis.speech._discovery import get_speech_backend
speech_backend = get_speech_backend(config)
if speech_backend:
console.print(f" Speech: [cyan]{speech_backend.backend_id}[/cyan]")
@@ -287,6 +296,7 @@ def serve(
executor = AgentExecutor(manager=agent_manager, event_bus=bus)
from openjarvis.system import SystemBuilder
system = SystemBuilder(config).build()
executor.set_system(system)
@@ -298,7 +308,8 @@ def serve(
for ag in agent_manager.list_agents():
sched_type = ag.get("config", {}).get("schedule_type", "manual")
if sched_type in ("cron", "interval") and ag["status"] not in (
"archived", "error",
"archived",
"error",
):
agent_scheduler.register_agent(ag["id"])
agent_scheduler.start()
@@ -316,7 +327,8 @@ def serve(
mem_key = config.memory.default_backend
if MemoryRegistry.contains(mem_key):
memory_backend = MemoryRegistry.create(
mem_key, db_path=config.memory.db_path,
mem_key,
db_path=config.memory.db_path,
)
console.print(" Memory: [cyan]active[/cyan]")
except Exception as exc:
@@ -329,33 +341,21 @@ def serve(
if not api_key:
try:
import tomllib
_cfg_path = str(
__import__("pathlib").Path.home()
/ ".openjarvis" / "config.toml"
__import__("pathlib").Path.home() / ".openjarvis" / "config.toml"
)
with open(_cfg_path, "rb") as _f:
_raw = tomllib.load(_f)
api_key = (
_raw.get("server", {})
.get("auth", {})
.get("api_key", "")
)
api_key = _raw.get("server", {}).get("auth", {}).get("api_key", "")
except (FileNotFoundError, ImportError):
pass
webhook_config = {
"twilio_auth_token": _os.environ.get(
"TWILIO_AUTH_TOKEN", ""
),
"bluebubbles_password": _os.environ.get(
"BLUEBUBBLES_PASSWORD", ""
),
"whatsapp_verify_token": _os.environ.get(
"WHATSAPP_VERIFY_TOKEN", ""
),
"whatsapp_app_secret": _os.environ.get(
"WHATSAPP_APP_SECRET", ""
),
"twilio_auth_token": _os.environ.get("TWILIO_AUTH_TOKEN", ""),
"bluebubbles_password": _os.environ.get("BLUEBUBBLES_PASSWORD", ""),
"whatsapp_verify_token": _os.environ.get("WHATSAPP_VERIFY_TOKEN", ""),
"whatsapp_app_secret": _os.environ.get("WHATSAPP_APP_SECRET", ""),
}
# Wrap existing channel in ChannelBridge orchestrator
@@ -369,9 +369,7 @@ def serve(
)
session_store = SessionStore()
channels = {
channel_bridge.channel_id: channel_bridge
}
channels = {channel_bridge.channel_id: channel_bridge}
channel_bridge = ChannelBridge(
channels=channels,
session_store=session_store,
@@ -380,14 +378,17 @@ def serve(
agent_manager=agent_manager,
)
except Exception as exc:
logger.debug(
"ChannelBridge init skipped: %s", exc
)
logger.debug("ChannelBridge init skipped: %s", exc)
app = create_app(
engine, model_name, agent=agent, bus=bus,
engine_name=engine_name, agent_name=agent_key or "",
channel_bridge=channel_bridge, config=config,
engine,
model_name,
agent=agent,
bus=bus,
engine_name=engine_name,
agent_name=agent_key or "",
channel_bridge=channel_bridge,
config=config,
memory_backend=memory_backend,
speech_backend=speech_backend,
agent_manager=agent_manager,
@@ -405,4 +406,5 @@ def serve(
)
import uvicorn
uvicorn.run(app, host=bind_host, port=bind_port, log_level="info")
+23 -16
View File
@@ -27,7 +27,11 @@ def telemetry() -> None:
@telemetry.command()
@click.option(
"-n", "--top", "top_n", default=10, type=int,
"-n",
"--top",
"top_n",
default=10,
type=int,
help="Number of top models to show.",
)
def stats(top_n: int) -> None:
@@ -76,13 +80,9 @@ def stats(top_n: int) -> None:
# Per-model table
if summary.per_model:
has_energy = any(
ms.total_energy_joules > 0
for ms in summary.per_model[:top_n]
)
has_itl = any(
ms.avg_mean_itl_ms > 0
for ms in summary.per_model[:top_n]
ms.total_energy_joules > 0 for ms in summary.per_model[:top_n]
)
has_itl = any(ms.avg_mean_itl_ms > 0 for ms in summary.per_model[:top_n])
model_table = Table(title=f"Top {top_n} Models")
model_table.add_column("Model", style="cyan")
model_table.add_column("Calls", justify="right")
@@ -121,13 +121,9 @@ def stats(top_n: int) -> None:
# Per-engine table
if summary.per_engine:
has_engine_energy = any(
es.total_energy_joules > 0
for es in summary.per_engine
)
has_engine_itl = any(
es.avg_mean_itl_ms > 0
for es in summary.per_engine
es.total_energy_joules > 0 for es in summary.per_engine
)
has_engine_itl = any(es.avg_mean_itl_ms > 0 for es in summary.per_engine)
engine_table = Table(title="Engines")
engine_table.add_column("Engine", style="cyan")
engine_table.add_column("Calls", justify="right")
@@ -171,11 +167,19 @@ def stats(top_n: int) -> None:
@telemetry.command()
@click.option(
"-f", "--format", "fmt", default="json", type=click.Choice(["json", "csv"]),
"-f",
"--format",
"fmt",
default="json",
type=click.Choice(["json", "csv"]),
help="Output format.",
)
@click.option(
"-o", "--output", "output_path", default=None, type=click.Path(),
"-o",
"--output",
"output_path",
default=None,
type=click.Path(),
help="Output file path (default: stdout).",
)
def export(fmt: str, output_path: str | None) -> None:
@@ -207,7 +211,10 @@ def export(fmt: str, output_path: str | None) -> None:
@telemetry.command()
@click.option(
"-y", "--yes", "confirmed", is_flag=True,
"-y",
"--yes",
"confirmed",
is_flag=True,
help="Skip confirmation prompt.",
)
def clear(confirmed: bool) -> None:
+3 -11
View File
@@ -12,9 +12,7 @@ from openjarvis.core.config import DEFAULT_CONFIG_PATH
@click.group("tunnel", invoke_without_command=True)
@click.option(
"--port", default=8000, help="Local port to tunnel."
)
@click.option("--port", default=8000, help="Local port to tunnel.")
@click.pass_context
def tunnel(ctx: click.Context, port: int) -> None:
"""Start a Cloudflare tunnel or manage config."""
@@ -31,10 +29,7 @@ def tunnel(ctx: click.Context, port: int) -> None:
)
sys.exit(1)
click.echo(
f"Starting Cloudflare tunnel "
f"to localhost:{port}..."
)
click.echo(f"Starting Cloudflare tunnel to localhost:{port}...")
click.echo("Press Ctrl+C to stop.\n")
try:
@@ -66,10 +61,7 @@ def status() -> None:
if "public_url" in content:
for line in content.splitlines():
if "public_url" in line:
click.echo(
"Configured tunnel URL: "
f"{line.split('=', 1)[1].strip()}"
)
click.echo(f"Configured tunnel URL: {line.split('=', 1)[1].strip()}")
return
click.echo("No tunnel URL configured.")
+3 -2
View File
@@ -21,8 +21,7 @@ def _get_or_create_key() -> bytes:
from cryptography.fernet import Fernet
except ImportError:
raise ImportError(
"cryptography not installed. Install with: "
"uv sync --extra security-signing"
"cryptography not installed. Install with: uv sync --extra security-signing"
)
if _VAULT_KEY_FILE.exists():
@@ -41,6 +40,7 @@ def _load_vault() -> dict:
return {}
try:
from cryptography.fernet import Fernet
key = _get_or_create_key()
f = Fernet(key)
encrypted = _VAULT_FILE.read_bytes()
@@ -53,6 +53,7 @@ def _load_vault() -> dict:
def _save_vault(data: dict) -> None:
"""Encrypt and save vault contents."""
from cryptography.fernet import Fernet
key = _get_or_create_key()
f = Fernet(key)
plaintext = json.dumps(data).encode()
+1 -2
View File
@@ -52,8 +52,7 @@ def run(workflow_name: str, input_text: str | None) -> None:
console.print(f"[green]Workflow '{workflow_name}' started.[/green]")
# Full execution would need a JarvisSystem — just report for now
console.print(
"[dim]Note: Full workflow execution"
" requires a running system.[/dim]"
"[dim]Note: Full workflow execution requires a running system.[/dim]"
)
except ImportError:
console.print("[red]Workflow system not available.[/red]")
+2 -6
View File
@@ -26,9 +26,7 @@ _DROPBOX_API_BASE = "https://api.dropboxapi.com/2"
_DROPBOX_CONTENT_BASE = "https://content.dropboxapi.com/2"
_DROPBOX_AUTH_URL = "https://www.dropbox.com/oauth2/authorize"
_DROPBOX_SCOPES = ["files.metadata.read", "files.content.read"]
_DEFAULT_CREDENTIALS_PATH = str(
DEFAULT_CONFIG_DIR / "connectors" / "dropbox.json"
)
_DEFAULT_CREDENTIALS_PATH = str(DEFAULT_CONFIG_DIR / "connectors" / "dropbox.json")
# File extensions whose content can be downloaded and stored as text
_TEXT_EXTENSIONS = {
@@ -231,9 +229,7 @@ class DropboxConnector(BaseConnector):
entries_seen: int = 0
while True:
list_resp = _dropbox_api_list_folder(
token, path="", cursor=list_cursor
)
list_resp = _dropbox_api_list_folder(token, path="", cursor=list_cursor)
entries: List[Dict[str, Any]] = list_resp.get("entries", [])
entries_seen += len(entries)
+2 -6
View File
@@ -26,9 +26,7 @@ from openjarvis.tools._stubs import ToolSpec
# ---------------------------------------------------------------------------
_GRANOLA_API_BASE = "https://public-api.granola.ai"
_DEFAULT_CREDENTIALS_PATH = str(
DEFAULT_CONFIG_DIR / "connectors" / "granola.json"
)
_DEFAULT_CREDENTIALS_PATH = str(DEFAULT_CONFIG_DIR / "connectors" / "granola.json")
# ---------------------------------------------------------------------------
# Module-level API functions (easy to patch in tests)
@@ -122,9 +120,7 @@ def _format_note_content(note: Dict[str, Any]) -> str:
parts: List[str] = []
# Summary section
summary_markdown: str = (
note.get("summary", {}) or {}
).get("markdown", "")
summary_markdown: str = (note.get("summary", {}) or {}).get("markdown", "")
parts.append("## Summary")
parts.append(summary_markdown)
+1 -3
View File
@@ -27,9 +27,7 @@ from openjarvis.tools._stubs import ToolSpec
_NOTION_API_BASE = "https://api.notion.com/v1"
_NOTION_VERSION = "2022-06-28"
_DEFAULT_CREDENTIALS_PATH = str(
DEFAULT_CONFIG_DIR / "connectors" / "notion.json"
)
_DEFAULT_CREDENTIALS_PATH = str(DEFAULT_CONFIG_DIR / "connectors" / "notion.json")
# ---------------------------------------------------------------------------
# Module-level API functions (easy to patch in tests)
+1 -3
View File
@@ -17,9 +17,7 @@ from openjarvis.connectors.gmail_imap import GmailIMAPConnector
from openjarvis.core.config import DEFAULT_CONFIG_DIR
from openjarvis.core.registry import ConnectorRegistry
_DEFAULT_CREDENTIALS_PATH = str(
DEFAULT_CONFIG_DIR / "connectors" / "outlook.json"
)
_DEFAULT_CREDENTIALS_PATH = str(DEFAULT_CONFIG_DIR / "connectors" / "outlook.json")
@ConnectorRegistry.register("outlook")
+5 -14
View File
@@ -26,9 +26,7 @@ from openjarvis.tools._stubs import ToolSpec
_SLACK_API_BASE = "https://slack.com/api"
_SLACK_AUTH_ENDPOINT = "https://slack.com/oauth/v2/authorize"
_SLACK_SCOPES = "channels:history,channels:read,users:read"
_DEFAULT_CREDENTIALS_PATH = str(
DEFAULT_CONFIG_DIR / "connectors" / "slack.json"
)
_DEFAULT_CREDENTIALS_PATH = str(DEFAULT_CONFIG_DIR / "connectors" / "slack.json")
# ---------------------------------------------------------------------------
# Module-level API functions (easy to patch in tests)
@@ -263,9 +261,7 @@ class SlackConnector(BaseConnector):
# Step 2: paginate through channels
while True:
channels_resp = _slack_api_conversations_list(
token, cursor=channels_cursor
)
channels_resp = _slack_api_conversations_list(token, cursor=channels_cursor)
channels: List[Dict[str, Any]] = channels_resp.get("channels", [])
for channel in channels:
@@ -280,9 +276,7 @@ class SlackConnector(BaseConnector):
history_resp = _slack_api_conversations_history(
token, chan_id, cursor=history_cursor
)
messages: List[Dict[str, Any]] = history_resp.get(
"messages", []
)
messages: List[Dict[str, Any]] = history_resp.get("messages", [])
for msg in messages:
# Skip bot messages and non-content subtypes
@@ -327,9 +321,7 @@ class SlackConnector(BaseConnector):
yield doc
next_history_cursor: str = (
history_resp.get("response_metadata", {}).get(
"next_cursor", ""
)
history_resp.get("response_metadata", {}).get("next_cursor", "")
or ""
)
if not history_resp.get("has_more") or not next_history_cursor:
@@ -337,8 +329,7 @@ class SlackConnector(BaseConnector):
history_cursor = next_history_cursor
next_channels_cursor: str = (
channels_resp.get("response_metadata", {}).get("next_cursor", "")
or ""
channels_resp.get("response_metadata", {}).get("next_cursor", "") or ""
)
if not next_channels_cursor:
self._last_cursor = None
+5 -2
View File
@@ -3,6 +3,7 @@
Stores credentials in ~/.openjarvis/credentials.toml with 0o600 permissions.
Thread-safe writes via lock. Sets os.environ on save for immediate effect.
"""
from __future__ import annotations
import os
@@ -33,8 +34,10 @@ TOOL_CREDENTIALS: dict[str, list[str]] = {
"viber": ["VIBER_AUTH_TOKEN"],
"messenger": ["MESSENGER_PAGE_ACCESS_TOKEN", "MESSENGER_VERIFY_TOKEN"],
"reddit": [
"REDDIT_CLIENT_ID", "REDDIT_CLIENT_SECRET",
"REDDIT_USERNAME", "REDDIT_PASSWORD",
"REDDIT_CLIENT_ID",
"REDDIT_CLIENT_SECRET",
"REDDIT_USERNAME",
"REDDIT_PASSWORD",
],
"mastodon": ["MASTODON_ACCESS_TOKEN", "MASTODON_API_BASE_URL"],
"twitch": ["TWITCH_TOKEN", "TWITCH_CHANNEL"],
+1 -2
View File
@@ -26,8 +26,7 @@ class SessionExpiryHook:
self._executor.run_ephemeral(
agent_type="simple",
system_prompt=(
"You are a memory management agent. "
"Save important information."
"You are a memory management agent. Save important information."
),
input_text=input_text,
tools=["memory_manage", "skill_manage"],
+47 -34
View File
@@ -80,12 +80,14 @@ def health() -> JSONResponse:
@app.get("/v1/models")
def list_models() -> JSONResponse:
return JSONResponse({
"object": "list",
"data": [
{"id": MODEL_ID, "object": "model", "owned_by": "apple"},
],
})
return JSONResponse(
{
"object": "list",
"data": [
{"id": MODEL_ID, "object": "model", "owned_by": "apple"},
],
}
)
@app.post("/v1/chat/completions")
@@ -96,21 +98,25 @@ async def chat_completions(
session = apple_fm.LanguageModelSession()
if req.stream:
async def generate():
cid = f"chatcmpl-{uuid.uuid4().hex[:12]}"
async for token in session.stream_response(
prompt, max_tokens=req.max_tokens,
prompt,
max_tokens=req.max_tokens,
):
chunk = {
"id": cid,
"object": "chat.completion.chunk",
"created": int(time.time()),
"model": MODEL_ID,
"choices": [{
"index": 0,
"delta": {"content": token},
"finish_reason": None,
}],
"choices": [
{
"index": 0,
"delta": {"content": token},
"finish_reason": None,
}
],
}
yield f"data: {json.dumps(chunk)}\n\n"
final = {
@@ -118,34 +124,41 @@ async def chat_completions(
"object": "chat.completion.chunk",
"created": int(time.time()),
"model": MODEL_ID,
"choices": [{
"index": 0,
"delta": {},
"finish_reason": "stop",
}],
"choices": [
{
"index": 0,
"delta": {},
"finish_reason": "stop",
}
],
}
yield f"data: {json.dumps(final)}\n\n"
yield "data: [DONE]\n\n"
return StreamingResponse(
generate(), media_type="text/event-stream",
generate(),
media_type="text/event-stream",
)
text = await session.respond(prompt, max_tokens=req.max_tokens)
cid = f"chatcmpl-{uuid.uuid4().hex[:12]}"
return JSONResponse({
"id": cid,
"object": "chat.completion",
"created": int(time.time()),
"model": MODEL_ID,
"choices": [{
"index": 0,
"message": {"role": "assistant", "content": text},
"finish_reason": "stop",
}],
"usage": {
"prompt_tokens": 0,
"completion_tokens": 0,
"total_tokens": 0,
},
})
return JSONResponse(
{
"id": cid,
"object": "chat.completion",
"created": int(time.time()),
"model": MODEL_ID,
"choices": [
{
"index": 0,
"message": {"role": "assistant", "content": text},
"finish_reason": "stop",
}
],
"usage": {
"prompt_tokens": 0,
"completion_tokens": 0,
"total_tokens": 0,
},
}
)
+45 -33
View File
@@ -81,12 +81,14 @@ def health() -> JSONResponse:
@app.get("/v1/models")
def list_models() -> JSONResponse:
return JSONResponse({
"object": "list",
"data": [
{"id": MODEL_ID, "object": "model", "owned_by": "nexa"},
],
})
return JSONResponse(
{
"object": "list",
"data": [
{"id": MODEL_ID, "object": "model", "owned_by": "nexa"},
],
}
)
@app.post("/v1/chat/completions")
@@ -97,6 +99,7 @@ async def chat_completions(
llm = _get_llm()
if req.stream:
async def generate():
cid = f"chatcmpl-{uuid.uuid4().hex[:12]}"
for token in llm.generate(prompt, max_tokens=req.max_tokens):
@@ -105,11 +108,13 @@ async def chat_completions(
"object": "chat.completion.chunk",
"created": int(time.time()),
"model": MODEL_ID,
"choices": [{
"index": 0,
"delta": {"content": token},
"finish_reason": None,
}],
"choices": [
{
"index": 0,
"delta": {"content": token},
"finish_reason": None,
}
],
}
yield f"data: {json.dumps(chunk)}\n\n"
final = {
@@ -117,36 +122,43 @@ async def chat_completions(
"object": "chat.completion.chunk",
"created": int(time.time()),
"model": MODEL_ID,
"choices": [{
"index": 0,
"delta": {},
"finish_reason": "stop",
}],
"choices": [
{
"index": 0,
"delta": {},
"finish_reason": "stop",
}
],
}
yield f"data: {json.dumps(final)}\n\n"
yield "data: [DONE]\n\n"
return StreamingResponse(
generate(), media_type="text/event-stream",
generate(),
media_type="text/event-stream",
)
text = llm.generate(prompt, max_tokens=req.max_tokens)
if isinstance(text, list):
text = "".join(text)
cid = f"chatcmpl-{uuid.uuid4().hex[:12]}"
return JSONResponse({
"id": cid,
"object": "chat.completion",
"created": int(time.time()),
"model": MODEL_ID,
"choices": [{
"index": 0,
"message": {"role": "assistant", "content": text},
"finish_reason": "stop",
}],
"usage": {
"prompt_tokens": 0,
"completion_tokens": 0,
"total_tokens": 0,
},
})
return JSONResponse(
{
"id": cid,
"object": "chat.completion",
"created": int(time.time()),
"model": MODEL_ID,
"choices": [
{
"index": 0,
"message": {"role": "assistant", "content": text},
"finish_reason": "stop",
}
],
"usage": {
"prompt_tokens": 0,
"completion_tokens": 0,
"total_tokens": 0,
},
}
)
+29 -17
View File
@@ -139,26 +139,36 @@ class OllamaEngine(InferenceEngine):
}
# Extract timing from Ollama response (nanoseconds → seconds)
result["ttft"] = data.get("prompt_eval_duration", 0) / 1e9
result["engine_timing"] = {k: data[k] for k in
("total_duration", "load_duration", "prompt_eval_duration", "eval_duration")
if k in data}
result["engine_timing"] = {
k: data[k]
for k in (
"total_duration",
"load_duration",
"prompt_eval_duration",
"eval_duration",
)
if k in data
}
# Extract tool calls if present
raw_tool_calls = data.get("message", {}).get("tool_calls", [])
if raw_tool_calls:
tool_calls = []
for i, tc in enumerate(raw_tool_calls):
raw_args = tc.get("function", {}).get(
"arguments", "{}",
"arguments",
"{}",
)
tool_calls.append(
{
"id": tc.get("id", f"call_{i}"),
"name": tc.get("function", {}).get("name", ""),
"arguments": (
json.dumps(raw_args)
if isinstance(raw_args, dict)
else raw_args
),
}
)
tool_calls.append({
"id": tc.get("id", f"call_{i}"),
"name": tc.get("function", {}).get("name", ""),
"arguments": (
json.dumps(raw_args)
if isinstance(raw_args, dict)
else raw_args
),
})
result["tool_calls"] = tool_calls
return result
@@ -199,8 +209,7 @@ class OllamaEngine(InferenceEngine):
est_prompt = estimate_prompt_tokens(messages)
full_prompt = max(reported_prompt, est_prompt)
evaluated = (
reported_prompt if reported_prompt > 0
else full_prompt
reported_prompt if reported_prompt > 0 else full_prompt
)
comp = chunk.get("eval_count", 0)
self._last_stream_usage = {
@@ -220,11 +229,14 @@ class OllamaEngine(InferenceEngine):
resp = self._client.get("/api/tags")
resp.raise_for_status()
except (
httpx.ConnectError, httpx.TimeoutException, httpx.HTTPStatusError,
httpx.ConnectError,
httpx.TimeoutException,
httpx.HTTPStatusError,
) as exc:
logger.warning(
"Failed to list models from Ollama at %s: %s",
self._host, exc,
self._host,
exc,
)
return []
data = resp.json()
@@ -57,8 +57,11 @@ class JarvisAgentBackend(InferenceBackend):
max_tokens: int = 2048,
) -> str:
result = self.generate_full(
prompt, model=model, system=system,
temperature=temperature, max_tokens=max_tokens,
prompt,
model=model,
system=system,
temperature=temperature,
max_tokens=max_tokens,
)
return result["content"]
@@ -43,8 +43,11 @@ class JarvisDirectBackend(InferenceBackend):
max_tokens: int = 2048,
) -> str:
result = self.generate_full(
prompt, model=model, system=system,
temperature=temperature, max_tokens=max_tokens,
prompt,
model=model,
system=system,
temperature=temperature,
max_tokens=max_tokens,
)
return result["content"]
@@ -66,8 +69,10 @@ class JarvisDirectBackend(InferenceBackend):
t0 = time.monotonic()
result = self._system.engine.generate(
messages, model=model,
temperature=temperature, max_tokens=max_tokens,
messages,
model=model,
temperature=temperature,
max_tokens=max_tokens,
)
elapsed = time.monotonic() - t0
@@ -79,6 +79,7 @@ class TerminalBenchNativeBackend(InferenceBackend):
# Use built-in agent (naive uses LiteLLM)
from terminal_bench.agents.agent_name import AgentName
harness_kwargs["agent_name"] = AgentName(self._agent_name)
harness_kwargs["agent_kwargs"] = {
"llm": LiteLLM(
@@ -95,13 +96,25 @@ class TerminalBenchNativeBackend(InferenceBackend):
self._results = harness.run()
return self._results
def generate(self, prompt: str, *, model: str, system: str = "",
temperature: float = 0.0, max_tokens: int = 2048) -> str:
def generate(
self,
prompt: str,
*,
model: str,
system: str = "",
temperature: float = 0.0,
max_tokens: int = 2048,
) -> str:
return ""
def generate_full(
self, prompt: str, *, model: str, system: str = "",
temperature: float = 0.0, max_tokens: int = 2048,
self,
prompt: str,
*,
model: str,
system: str = "",
temperature: float = 0.0,
max_tokens: int = 2048,
) -> Dict[str, Any]:
return {"content": "", "usage": {}, "model": model, "latency_seconds": 0.0}
+283 -127
View File
@@ -45,7 +45,8 @@ BENCHMARKS = {
"swebench": {"category": "agentic", "description": "SWE-bench code patches"},
"swefficiency": {"category": "agentic", "description": "SWEfficiency optimization"},
"terminalbench": {
"category": "agentic", "description": "TerminalBench terminal tasks",
"category": "agentic",
"description": "TerminalBench terminal tasks",
},
"terminalbench-native": {
"category": "agentic",
@@ -140,13 +141,19 @@ def _setup_logging(verbose: bool) -> None:
)
def _build_backend(backend_name: str, engine_key: Optional[str],
agent_name: str, tools: list[str],
telemetry: bool = False, gpu_metrics: bool = False,
model: Optional[str] = None):
def _build_backend(
backend_name: str,
engine_key: Optional[str],
agent_name: str,
tools: list[str],
telemetry: bool = False,
gpu_metrics: bool = False,
model: Optional[str] = None,
):
"""Construct the appropriate backend."""
if backend_name == "jarvis-agent":
from openjarvis.evals.backends.jarvis_agent import JarvisAgentBackend
return JarvisAgentBackend(
engine_key=engine_key,
agent_name=agent_name,
@@ -157,6 +164,7 @@ def _build_backend(backend_name: str, engine_key: Optional[str],
)
else:
from openjarvis.evals.backends.jarvis_direct import JarvisDirectBackend
return JarvisDirectBackend(
engine_key=engine_key,
telemetry=telemetry,
@@ -168,104 +176,137 @@ def _build_dataset(benchmark: str, subset: str | None = None):
"""Construct the dataset provider for a benchmark."""
if benchmark == "supergpqa":
from openjarvis.evals.datasets.supergpqa import SuperGPQADataset
return SuperGPQADataset()
elif benchmark == "gpqa":
from openjarvis.evals.datasets.gpqa import GPQADataset
return GPQADataset()
elif benchmark == "mmlu-pro":
from openjarvis.evals.datasets.mmlu_pro import MMLUProDataset
return MMLUProDataset()
elif benchmark == "math500":
from openjarvis.evals.datasets.math500 import MATH500Dataset
return MATH500Dataset()
elif benchmark == "natural-reasoning":
from openjarvis.evals.datasets.natural_reasoning import NaturalReasoningDataset
return NaturalReasoningDataset()
elif benchmark == "hle":
from openjarvis.evals.datasets.hle import HLEDataset
return HLEDataset()
elif benchmark == "simpleqa":
from openjarvis.evals.datasets.simpleqa import SimpleQADataset
return SimpleQADataset()
elif benchmark == "wildchat":
from openjarvis.evals.datasets.wildchat import WildChatDataset
return WildChatDataset()
elif benchmark == "ipw":
from openjarvis.evals.datasets.ipw_mixed import IPWDataset
return IPWDataset()
elif benchmark == "gaia":
from openjarvis.evals.datasets.gaia import GAIADataset
return GAIADataset()
elif benchmark == "frames":
from openjarvis.evals.datasets.frames import FRAMESDataset
return FRAMESDataset()
elif benchmark == "swebench":
from openjarvis.evals.datasets.swebench import SWEBenchDataset
return SWEBenchDataset()
elif benchmark == "swefficiency":
from openjarvis.evals.datasets.swefficiency import SWEfficiencyDataset
return SWEfficiencyDataset()
elif benchmark == "terminalbench":
from openjarvis.evals.datasets.terminalbench import TerminalBenchDataset
return TerminalBenchDataset()
elif benchmark == "terminalbench-native":
from openjarvis.evals.datasets.terminalbench_native import (
TerminalBenchNativeDataset,
)
return TerminalBenchNativeDataset()
elif benchmark == "email_triage":
from openjarvis.evals.datasets.email_triage import EmailTriageDataset
return EmailTriageDataset()
elif benchmark == "morning_brief":
from openjarvis.evals.datasets.morning_brief import MorningBriefDataset
return MorningBriefDataset()
elif benchmark == "research_mining":
from openjarvis.evals.datasets.research_mining import ResearchMiningDataset
return ResearchMiningDataset()
elif benchmark == "knowledge_base":
from openjarvis.evals.datasets.knowledge_base import KnowledgeBaseDataset
return KnowledgeBaseDataset()
elif benchmark == "coding_task":
from openjarvis.evals.datasets.coding_task import CodingTaskDataset
return CodingTaskDataset()
elif benchmark == "loghub":
from openjarvis.evals.datasets.loghub import LogHubDataset
return LogHubDataset()
elif benchmark == "ama-bench":
from openjarvis.evals.datasets.ama_bench import AMABenchDataset
return AMABenchDataset()
elif benchmark == "lifelong-agent":
from openjarvis.evals.datasets.lifelong_agent import LifelongAgentDataset
return LifelongAgentDataset(subset=subset or "db_bench")
elif benchmark == "deepplanning":
from openjarvis.evals.datasets.deepplanning import DeepPlanningDataset
return DeepPlanningDataset()
elif benchmark == "paperarena":
from openjarvis.evals.datasets.paperarena import PaperArenaDataset
return PaperArenaDataset()
elif benchmark == "webchorearena":
from openjarvis.evals.datasets.webchorearena import WebChoreArenaDataset
return WebChoreArenaDataset()
elif benchmark == "workarena":
from openjarvis.evals.datasets.workarena import WorkArenaDataset
return WorkArenaDataset()
elif benchmark == "coding_assistant":
from openjarvis.evals.datasets.coding_assistant import CodingAssistantDataset
return CodingAssistantDataset()
elif benchmark == "security_scanner":
from openjarvis.evals.datasets.security_scanner import SecurityScannerDataset
return SecurityScannerDataset()
elif benchmark == "daily_digest":
from openjarvis.evals.datasets.daily_digest import DailyDigestDataset
return DailyDigestDataset()
elif benchmark == "doc_qa":
from openjarvis.evals.datasets.doc_qa import DocQADataset
return DocQADataset()
elif benchmark == "browser_assistant":
from openjarvis.evals.datasets.browser_assistant import BrowserAssistantDataset
return BrowserAssistantDataset()
elif benchmark == "pinchbench":
from openjarvis.evals.datasets.pinchbench import PinchBenchDataset
return PinchBenchDataset(path=subset)
else:
raise click.ClickException(f"Unknown benchmark: {benchmark}")
@@ -275,101 +316,133 @@ def _build_scorer(benchmark: str, judge_backend, judge_model: str):
"""Construct the scorer for a benchmark."""
if benchmark == "supergpqa":
from openjarvis.evals.scorers.supergpqa_mcq import SuperGPQAScorer
return SuperGPQAScorer(judge_backend, judge_model)
elif benchmark == "gpqa":
from openjarvis.evals.scorers.gpqa_mcq import GPQAScorer
return GPQAScorer(judge_backend, judge_model)
elif benchmark == "mmlu-pro":
from openjarvis.evals.scorers.mmlu_pro_mcq import MMLUProScorer
return MMLUProScorer(judge_backend, judge_model)
elif benchmark == "math500" or benchmark == "natural-reasoning":
from openjarvis.evals.scorers.reasoning_judge import ReasoningJudgeScorer
return ReasoningJudgeScorer(judge_backend, judge_model)
elif benchmark == "hle":
from openjarvis.evals.scorers.hle_judge import HLEScorer
return HLEScorer(judge_backend, judge_model)
elif benchmark == "simpleqa":
from openjarvis.evals.scorers.simpleqa_judge import SimpleQAScorer
return SimpleQAScorer(judge_backend, judge_model)
elif benchmark == "wildchat":
from openjarvis.evals.scorers.wildchat_judge import WildChatScorer
return WildChatScorer(judge_backend, judge_model)
elif benchmark == "ipw":
from openjarvis.evals.scorers.ipw_mixed import IPWMixedScorer
return IPWMixedScorer(judge_backend, judge_model)
elif benchmark == "gaia":
from openjarvis.evals.scorers.gaia_exact import GAIAScorer
return GAIAScorer(judge_backend, judge_model)
elif benchmark == "frames":
from openjarvis.evals.scorers.frames_judge import FRAMESScorer
return FRAMESScorer(judge_backend, judge_model)
elif benchmark == "swebench":
from openjarvis.evals.scorers.swebench_structural import SWEBenchScorer
return SWEBenchScorer(judge_backend, judge_model)
elif benchmark == "swefficiency":
from openjarvis.evals.scorers.swefficiency_structural import SWEfficiencyScorer
return SWEfficiencyScorer(judge_backend, judge_model)
elif benchmark == "terminalbench":
from openjarvis.evals.scorers.terminalbench_judge import TerminalBenchScorer
return TerminalBenchScorer(judge_backend, judge_model)
elif benchmark == "terminalbench-native":
from openjarvis.evals.scorers.terminalbench_native_structural import (
TerminalBenchNativeScorer,
)
return TerminalBenchNativeScorer(judge_backend, judge_model)
elif benchmark == "email_triage":
from openjarvis.evals.scorers.email_triage import EmailTriageScorer
return EmailTriageScorer(judge_backend, judge_model)
elif benchmark == "morning_brief":
from openjarvis.evals.scorers.morning_brief import MorningBriefScorer
return MorningBriefScorer(judge_backend, judge_model)
elif benchmark == "research_mining":
from openjarvis.evals.scorers.research_mining import ResearchMiningScorer
return ResearchMiningScorer(judge_backend, judge_model)
elif benchmark == "knowledge_base":
from openjarvis.evals.scorers.knowledge_base import KnowledgeBaseScorer
return KnowledgeBaseScorer(judge_backend, judge_model)
elif benchmark == "coding_task":
from openjarvis.evals.scorers.coding_task import CodingTaskScorer
return CodingTaskScorer(judge_backend, judge_model)
elif benchmark == "loghub":
from openjarvis.evals.scorers.loghub_scorer import LogHubScorer
return LogHubScorer(judge_backend, judge_model)
elif benchmark == "ama-bench":
from openjarvis.evals.scorers.ama_bench_judge import AMABenchScorer
return AMABenchScorer(judge_backend, judge_model)
elif benchmark == "lifelong-agent":
from openjarvis.evals.scorers.lifelong_agent_scorer import LifelongAgentScorer
return LifelongAgentScorer(judge_backend, judge_model)
elif benchmark == "deepplanning":
from openjarvis.evals.scorers.deepplanning_scorer import DeepPlanningScorer
return DeepPlanningScorer(judge_backend, judge_model)
elif benchmark == "paperarena":
from openjarvis.evals.scorers.paperarena_judge import PaperArenaScorer
return PaperArenaScorer(judge_backend, judge_model)
elif benchmark == "webchorearena":
from openjarvis.evals.scorers.webchorearena_scorer import WebChoreArenaScorer
return WebChoreArenaScorer(judge_backend, judge_model)
elif benchmark == "workarena":
from openjarvis.evals.scorers.workarena_scorer import WorkArenaScorer
return WorkArenaScorer(judge_backend, judge_model)
elif benchmark == "coding_assistant":
from openjarvis.evals.scorers.coding_assistant import CodingAssistantScorer
return CodingAssistantScorer(judge_backend, judge_model)
elif benchmark == "security_scanner":
from openjarvis.evals.scorers.security_scanner import SecurityScannerScorer
return SecurityScannerScorer(judge_backend, judge_model)
elif benchmark == "daily_digest":
from openjarvis.evals.scorers.daily_digest import DailyDigestScorer
return DailyDigestScorer(judge_backend, judge_model)
elif benchmark == "doc_qa":
from openjarvis.evals.scorers.doc_qa import DocQAScorer
return DocQAScorer(judge_backend, judge_model)
elif benchmark == "browser_assistant":
from openjarvis.evals.scorers.browser_assistant import BrowserAssistantScorer
return BrowserAssistantScorer(judge_backend, judge_model)
elif benchmark == "pinchbench":
from openjarvis.evals.scorers.pinchbench import PinchBenchScorer
return PinchBenchScorer(judge_backend, judge_model)
else:
raise click.ClickException(f"Unknown benchmark: {benchmark}")
@@ -384,6 +457,7 @@ def _build_judge_backend(judge_model: str, engine_key: str = "cloud"):
to use the backend rather than failing at startup.
"""
from openjarvis.evals.backends.jarvis_direct import JarvisDirectBackend
try:
return JarvisDirectBackend(engine_key=engine_key)
except RuntimeError as exc:
@@ -391,7 +465,8 @@ def _build_judge_backend(judge_model: str, engine_key: str = "cloud"):
"Judge backend (%s) unavailable: %s"
"deterministic scorers will still work; "
"LLM-judge scorers will fail when scoring.",
engine_key, exc,
engine_key,
exc,
)
return None
@@ -421,25 +496,30 @@ def _build_trackers(config) -> list:
if getattr(config, "wandb_project", ""):
try:
from openjarvis.evals.trackers.wandb_tracker import WandbTracker
trackers.append(WandbTracker(
project=config.wandb_project,
entity=getattr(config, "wandb_entity", ""),
tags=getattr(config, "wandb_tags", ""),
group=getattr(config, "wandb_group", ""),
))
trackers.append(
WandbTracker(
project=config.wandb_project,
entity=getattr(config, "wandb_entity", ""),
tags=getattr(config, "wandb_tags", ""),
group=getattr(config, "wandb_group", ""),
)
)
except ImportError as exc:
raise click.ClickException(
f"wandb not installed: {exc}\n"
"Install with: uv sync --extra eval-wandb"
f"wandb not installed: {exc}\nInstall with: uv sync --extra eval-wandb"
) from exc
if getattr(config, "sheets_spreadsheet_id", ""):
try:
from openjarvis.evals.trackers.sheets_tracker import SheetsTracker
trackers.append(SheetsTracker(
spreadsheet_id=config.sheets_spreadsheet_id,
worksheet=getattr(config, "sheets_worksheet", "Results"),
credentials_path=getattr(config, "sheets_credentials_path", ""),
))
trackers.append(
SheetsTracker(
spreadsheet_id=config.sheets_spreadsheet_id,
worksheet=getattr(config, "sheets_worksheet", "Results"),
credentials_path=getattr(config, "sheets_credentials_path", ""),
)
)
except ImportError as exc:
raise click.ClickException(
f"gspread not installed: {exc}\n"
@@ -486,7 +566,8 @@ def _run_single(config, console: Optional[Console] = None) -> object:
task = progress.add_task("Evaluating samples...", total=num_samples)
summary = runner.run(
progress_callback=lambda done, total: progress.update(
task, completed=done,
task,
completed=done,
),
)
else:
@@ -533,8 +614,7 @@ def _run_agentic(
if config.benchmark == "pinchbench" and hasattr(dataset, "set_judge"):
judge_engine = getattr(config, "judge_engine", "cloud") or "cloud"
judge_model = (
getattr(config, "judge_model", None)
or "anthropic/claude-opus-4-5"
getattr(config, "judge_model", None) or "anthropic/claude-opus-4-5"
)
judge_backend = _build_judge_backend(judge_model, engine_key=judge_engine)
dataset.set_judge(judge_backend, judge_model)
@@ -576,7 +656,8 @@ def _run_agentic(
monitor = create_energy_monitor()
if monitor is not None:
telemetry_session = TelemetrySession(
monitor=monitor, interval_ms=100,
monitor=monitor,
interval_ms=100,
)
except ImportError:
pass
@@ -650,6 +731,7 @@ def _run_agentic(
# Try HF dataset export (optional)
try:
from openjarvis.evals.core.export import export_hf_dataset
hf_path = run_dir / "hf_dataset"
export_hf_dataset(traces, hf_path)
console.print(f" [green]HF Arrow:[/green] {hf_path}")
@@ -664,6 +746,7 @@ def _run_agentic(
def _nullctx():
"""Return a no-op context manager."""
from contextlib import nullcontext
return nullcontext()
@@ -681,7 +764,8 @@ def _print_agentic_summary(console: Console, traces, config) -> None:
total_wall = sum(t.total_wall_clock_s for t in traces)
gpu_energies = [
t.total_gpu_energy_joules for t in traces
t.total_gpu_energy_joules
for t in traces
if t.total_gpu_energy_joules is not None
]
total_gpu_energy = sum(gpu_energies) if gpu_energies else None
@@ -710,7 +794,7 @@ def _print_agentic_summary(console: Console, traces, config) -> None:
table.add_row("Wall clock", f"{total_wall:.1f}s")
table.add_row(
"Avg query time",
f"{total_wall/len(traces):.1f}s" if traces else "0s",
f"{total_wall / len(traces):.1f}s" if traces else "0s",
)
if total_gpu_energy is not None:
@@ -722,7 +806,7 @@ def _print_agentic_summary(console: Console, traces, config) -> None:
if total_out_tok > 0 and total_wall > 0:
table.add_row(
"Throughput",
f"{total_out_tok/total_wall:.1f} tok/s",
f"{total_out_tok / total_wall:.1f} tok/s",
)
console.print(table)
@@ -746,18 +830,14 @@ def _run_from_config(
if model_filter:
run_configs = [rc for rc in run_configs if model_filter in rc.model]
if not run_configs:
raise click.ClickException(
f"No models match filter '{model_filter}'"
)
raise click.ClickException(f"No models match filter '{model_filter}'")
suite_name = suite.meta.name or Path(config_path).stem
# Banner + configuration
print_banner(console)
print_section(console, "Suite Configuration")
console.print(
f" [cyan]Suite:[/cyan] {suite_name}"
)
console.print(f" [cyan]Suite:[/cyan] {suite_name}")
if suite.meta.description:
console.print(f" [cyan]Description:[/cyan] {suite.meta.description}")
console.print(
@@ -802,86 +882,166 @@ def main():
@main.command()
@click.option("-c", "--config", "config_path", default=None,
type=click.Path(), help="TOML config file for suite runs")
@click.option("-b", "--benchmark", default=None,
type=click.Choice(list(BENCHMARKS.keys())),
help="Benchmark to run")
@click.option("--backend", default="jarvis-direct",
type=click.Choice(list(BACKENDS.keys())),
help="Inference backend")
@click.option("-m", "--model", default=None, help="Model identifier")
@click.option("-e", "--engine", "engine_key", default=None,
help="Engine key (ollama, vllm, cloud, ...)")
@click.option("--agent", "agent_name", default="orchestrator",
help="Agent name for jarvis-agent backend")
@click.option("--tools", default="", help="Comma-separated tool names")
@click.option("-n", "--max-samples", type=int, default=None,
help="Maximum samples to evaluate")
@click.option("-w", "--max-workers", type=int, default=4,
help="Parallel workers")
@click.option("--judge-model", default="gpt-5-mini-2025-08-07",
help="LLM judge model")
@click.option("-o", "--output", "output_path", default=None,
help="Output JSONL path")
@click.option("--seed", type=int, default=42, help="Random seed")
@click.option("--split", "dataset_split", default=None,
help="Dataset split override")
@click.option("--temperature", type=float, default=0.0,
help="Generation temperature")
@click.option("--max-tokens", type=int, default=2048,
help="Max output tokens")
@click.option("--telemetry/--no-telemetry", default=False,
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,
"-c",
"--config",
"config_path",
default=None,
type=click.Path(),
help="TOML config file for suite runs",
)
@click.option(
"-b",
"--benchmark",
default=None,
type=click.Choice(list(BENCHMARKS.keys())),
help="Benchmark to run",
)
@click.option(
"--backend",
default="jarvis-direct",
type=click.Choice(list(BACKENDS.keys())),
help="Inference backend",
)
@click.option("-m", "--model", default=None, help="Model identifier")
@click.option(
"-e",
"--engine",
"engine_key",
default=None,
help="Engine key (ollama, vllm, cloud, ...)",
)
@click.option(
"--agent",
"agent_name",
default="orchestrator",
help="Agent name for jarvis-agent backend",
)
@click.option("--tools", default="", help="Comma-separated tool names")
@click.option(
"-n", "--max-samples", type=int, default=None, help="Maximum samples to evaluate"
)
@click.option("-w", "--max-workers", type=int, default=4, help="Parallel workers")
@click.option("--judge-model", default="gpt-5-mini-2025-08-07", help="LLM judge model")
@click.option("-o", "--output", "output_path", default=None, help="Output JSONL path")
@click.option("--seed", type=int, default=42, help="Random seed")
@click.option("--split", "dataset_split", default=None, help="Dataset split override")
@click.option("--temperature", type=float, default=0.0, help="Generation temperature")
@click.option("--max-tokens", type=int, default=2048, help="Max output tokens")
@click.option(
"--telemetry/--no-telemetry",
default=False,
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,
"--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="",
help="W&B entity (team or user)")
@click.option("--wandb-tags", default="",
help="Comma-separated W&B tags")
@click.option("--wandb-group", default="",
help="W&B run group")
@click.option("--sheets-id", "sheets_spreadsheet_id", default="",
help="Google Sheets spreadsheet ID")
@click.option("--sheets-worksheet", default="Results",
help="Google Sheets worksheet name")
@click.option("--sheets-creds", "sheets_credentials_path",
default="",
help="Service account JSON path")
@click.option("--model-filter", default=None,
help="Filter models by name substring (for multi-model configs)")
@click.option("--judge-engine", default="cloud",
help="Engine key for LLM judge (default: cloud). "
"Use 'vllm' to judge locally.")
@click.option("--agentic", is_flag=True, default=False,
help="Use AgenticRunner for multi-turn agent execution")
@click.option("--episode-mode", is_flag=True, default=False,
help="Sequential episode processing with lifelong learning "
"(required for lifelong-agent and similar benchmarks)")
@click.option("--concurrency", type=int, default=1,
help="Parallel query execution (AgenticRunner only)")
@click.option("--query-timeout", type=float, default=None,
help="Per-query wall-clock timeout in seconds (AgenticRunner only)")
@click.option("--wandb-project", default="", help="W&B project name (enables tracking)")
@click.option("--wandb-entity", default="", help="W&B entity (team or user)")
@click.option("--wandb-tags", default="", help="Comma-separated W&B tags")
@click.option("--wandb-group", default="", help="W&B run group")
@click.option(
"--sheets-id",
"sheets_spreadsheet_id",
default="",
help="Google Sheets spreadsheet ID",
)
@click.option(
"--sheets-worksheet", default="Results", help="Google Sheets worksheet name"
)
@click.option(
"--sheets-creds",
"sheets_credentials_path",
default="",
help="Service account JSON path",
)
@click.option(
"--model-filter",
default=None,
help="Filter models by name substring (for multi-model configs)",
)
@click.option(
"--judge-engine",
default="cloud",
help="Engine key for LLM judge (default: cloud). Use 'vllm' to judge locally.",
)
@click.option(
"--agentic",
is_flag=True,
default=False,
help="Use AgenticRunner for multi-turn agent execution",
)
@click.option(
"--episode-mode",
is_flag=True,
default=False,
help="Sequential episode processing with lifelong learning "
"(required for lifelong-agent and similar benchmarks)",
)
@click.option(
"--concurrency",
type=int,
default=1,
help="Parallel query execution (AgenticRunner only)",
)
@click.option(
"--query-timeout",
type=float,
default=None,
help="Per-query wall-clock timeout in seconds (AgenticRunner only)",
)
@click.option("-v", "--verbose", is_flag=True, help="Verbose logging")
@click.pass_context
def run(ctx, config_path, benchmark, backend, model, engine_key, agent_name,
tools, max_samples, max_workers, judge_model, output_path, seed,
dataset_split, temperature, max_tokens, telemetry, gpu_metrics,
compact, trace_detail,
wandb_project, wandb_entity, wandb_tags, wandb_group,
sheets_spreadsheet_id, sheets_worksheet, sheets_credentials_path,
model_filter, judge_engine, agentic, episode_mode,
concurrency, query_timeout, verbose):
def run(
ctx,
config_path,
benchmark,
backend,
model,
engine_key,
agent_name,
tools,
max_samples,
max_workers,
judge_model,
output_path,
seed,
dataset_split,
temperature,
max_tokens,
telemetry,
gpu_metrics,
compact,
trace_detail,
wandb_project,
wandb_entity,
wandb_tags,
wandb_group,
sheets_spreadsheet_id,
sheets_worksheet,
sheets_credentials_path,
model_filter,
judge_engine,
agentic,
episode_mode,
concurrency,
query_timeout,
verbose,
):
"""Run a single benchmark evaluation, or a full suite from a TOML config."""
_setup_logging(verbose)
@@ -900,8 +1060,7 @@ def run(ctx, config_path, benchmark, backend, model, engine_key, agent_name,
)
if model is None:
raise click.UsageError(
"Missing option '-m' / '--model' "
"(required when --config is not provided)"
"Missing option '-m' / '--model' (required when --config is not provided)"
)
from openjarvis.evals.core.types import RunConfig
@@ -960,22 +1119,18 @@ def run(ctx, config_path, benchmark, backend, model, engine_key, agent_name,
)
if episode_mode:
console.print(
" [cyan]Mode:[/cyan] episode "
"(sequential + lifelong learning)"
" [cyan]Mode:[/cyan] episode (sequential + lifelong learning)"
)
if agentic:
# --- Agentic runner path ---
print_section(console, "Agentic Evaluation")
console.print(
f" [cyan]Concurrency:[/cyan] {concurrency}"
)
console.print(f" [cyan]Concurrency:[/cyan] {concurrency}")
if query_timeout:
console.print(
f" [cyan]Timeout:[/cyan] {query_timeout}s per query"
)
console.print(f" [cyan]Timeout:[/cyan] {query_timeout}s per query")
_run_agentic(
config, console=console,
config,
console=console,
concurrency=concurrency,
query_timeout=query_timeout,
)
@@ -1000,19 +1155,18 @@ def run(ctx, config_path, benchmark, backend, model, engine_key, agent_name,
@main.command("run-all")
@click.option("-m", "--model", required=True, help="Model identifier")
@click.option("-e", "--engine", "engine_key", default=None,
help="Engine key")
@click.option("-n", "--max-samples", type=int, default=None,
help="Max samples per benchmark")
@click.option("-w", "--max-workers", type=int, default=4,
help="Parallel workers")
@click.option("-e", "--engine", "engine_key", default=None, help="Engine key")
@click.option(
"-n", "--max-samples", type=int, default=None, help="Max samples per benchmark"
)
@click.option("-w", "--max-workers", type=int, default=4, help="Parallel workers")
@click.option("--judge-model", default="gpt-5-mini-2025-08-07", help="LLM judge model")
@click.option("--output-dir", default="results/",
help="Output directory for results")
@click.option("--output-dir", default="results/", help="Output directory for results")
@click.option("--seed", type=int, default=42, help="Random seed")
@click.option("-v", "--verbose", is_flag=True, help="Verbose logging")
def run_all(model, engine_key, max_samples, max_workers, judge_model,
output_dir, seed, verbose):
def run_all(
model, engine_key, max_samples, max_workers, judge_model, output_dir, seed, verbose
):
"""Run all benchmarks."""
_setup_logging(verbose)
@@ -1069,11 +1223,13 @@ def run_all(model, engine_key, max_samples, max_workers, judge_model,
console=console,
) as progress:
task = progress.add_task(
f"Evaluating {bench_name}...", total=max_samples,
f"Evaluating {bench_name}...",
total=max_samples,
)
summary = runner.run(
progress_callback=lambda done, total: progress.update(
task, completed=done,
task,
completed=done,
),
)
else:
+88 -82
View File
@@ -36,10 +36,7 @@ def _compute_energy_delta(
gpu_field: str = "gpu_energy_j",
) -> Optional[float]:
"""Compute energy delta from first to last reading for a field."""
values = [
getattr(s, gpu_field, None)
for s in readings
]
values = [getattr(s, gpu_field, None) for s in readings]
values = [v for v in values if v is not None and math.isfinite(v)]
if len(values) >= 2:
delta = values[-1] - values[0]
@@ -52,10 +49,7 @@ def _compute_power_avg(
power_field: str = "gpu_power_w",
) -> Optional[float]:
"""Compute average power across readings for a field."""
values = [
getattr(s, power_field, None)
for s in readings
]
values = [getattr(s, power_field, None) for s in readings]
values = [v for v in values if v is not None and math.isfinite(v)]
return statistics.mean(values) if values else None
@@ -64,9 +58,7 @@ def _compute_power_avg(
# Patch extraction helpers
# ---------------------------------------------------------------------------
_FENCED_DIFF_RE = re.compile(
r"```(?:diff|patch)\s*\n(.*?)```", re.DOTALL
)
_FENCED_DIFF_RE = re.compile(r"```(?:diff|patch)\s*\n(.*?)```", re.DOTALL)
_UNIFIED_DIFF_MARKERS = ("diff --git", "--- a/", "+++ b/", "@@ ")
@@ -160,16 +152,16 @@ class AgenticRunner:
index, record, model, self._agent, self._event_recorder
)
if self._query_timeout:
trace = await asyncio.wait_for(
fut, timeout=self._query_timeout
)
trace = await asyncio.wait_for(fut, timeout=self._query_timeout)
else:
trace = await fut
except asyncio.TimeoutError:
elapsed = time.time() - start_time
LOGGER.warning(
"Query %s timed out after %.0fs (limit=%ss)",
query_id, elapsed, self._query_timeout,
query_id,
elapsed,
self._query_timeout,
)
workload_type = getattr(record, "category", "agentic")
trace = QueryTrace(
@@ -185,12 +177,13 @@ class AgenticRunner:
self._traces.append(trace)
status = (
"TIMEOUT" if trace.timed_out
else ("OK" if trace.completed else "FAIL")
"TIMEOUT" if trace.timed_out else ("OK" if trace.completed else "FAIL")
)
LOGGER.info(
"Task %s: %s in %.1fs",
query_id, status, trace.total_wall_clock_s,
query_id,
status,
trace.total_wall_clock_s,
)
if self._run_dir:
@@ -199,7 +192,8 @@ class AgenticRunner:
if len(self._traces) % self._FLUSH_INTERVAL == 0:
LOGGER.debug(
"Processed %d/%d queries",
len(self._traces), len(work_items),
len(self._traces),
len(work_items),
)
return self._traces
@@ -213,7 +207,8 @@ class AgenticRunner:
total = len(work_items)
LOGGER.info(
"Running %d queries with concurrency=%d",
total, self._concurrency,
total,
self._concurrency,
)
result_slots: list[Optional[QueryTrace]] = [None] * total
@@ -235,19 +230,23 @@ class AgenticRunner:
fut = loop.run_in_executor(
None,
self._run_single_query_sync,
index, record, model, agent, recorder,
index,
record,
model,
agent,
recorder,
)
if self._query_timeout:
trace = await asyncio.wait_for(
fut, timeout=self._query_timeout
)
trace = await asyncio.wait_for(fut, timeout=self._query_timeout)
else:
trace = await fut
except asyncio.TimeoutError:
elapsed = time.time() - start_time
LOGGER.warning(
"Query %s timed out after %.0fs (limit=%ss)",
query_id, elapsed, self._query_timeout,
query_id,
elapsed,
self._query_timeout,
)
workload_type = getattr(record, "category", "agentic")
trace = QueryTrace(
@@ -262,12 +261,15 @@ class AgenticRunner:
)
status = (
"TIMEOUT" if trace.timed_out
"TIMEOUT"
if trace.timed_out
else ("OK" if trace.completed else "FAIL")
)
LOGGER.info(
"Task %s: %s in %.1fs",
query_id, status, trace.total_wall_clock_s,
query_id,
status,
trace.total_wall_clock_s,
)
if self._run_dir:
@@ -328,9 +330,11 @@ class AgenticRunner:
agent_bus = getattr(agent, "bus", None)
if agent_bus is not None:
for etype in (EventType.TOOL_CALL_START, EventType.TOOL_CALL_END):
def _relay(event, _etype=etype):
data = event.data if hasattr(event, "data") else {}
event_recorder.record(_etype, **data)
agent_bus.subscribe(etype, _relay)
_bus_unsubs.append((etype, _relay))
@@ -364,6 +368,7 @@ class AgenticRunner:
# Envs with reset/step/evaluate use run_agent_loop
# for multi-turn interaction with conversation history.
if task_env is not None and hasattr(task_env, "run_agent_loop"):
def _generate(prompt: str) -> str:
if hasattr(agent, "ask"):
r = agent.ask(prompt)
@@ -402,10 +407,8 @@ class AgenticRunner:
result = agent.run(record.problem)
response_text = getattr(result, "content", str(result))
result_tokens = {
"input_tokens": getattr(result, "input_tokens", 0)
or 0,
"output_tokens": getattr(result, "output_tokens", 0)
or 0,
"input_tokens": getattr(result, "input_tokens", 0) or 0,
"output_tokens": getattr(result, "output_tokens", 0) or 0,
"cost_usd": getattr(result, "cost_usd", 0.0) or 0.0,
}
else:
@@ -499,13 +502,15 @@ class AgenticRunner:
out_tok = result_tokens.get("output_tokens", 0)
cost = result_tokens.get("cost_usd", 0.0)
if not turns and (in_tok > 0 or out_tok > 0):
turns = [TurnTrace(
turn_index=0,
input_tokens=in_tok,
output_tokens=out_tok,
wall_clock_s=end_time - start_time,
cost_usd=cost if cost else None,
)]
turns = [
TurnTrace(
turn_index=0,
input_tokens=in_tok,
output_tokens=out_tok,
wall_clock_s=end_time - start_time,
cost_usd=cost if cost else None,
)
]
# Backfill tokens from result when turns have zero tokens
if turns and in_tok > 0 and out_tok > 0:
@@ -514,9 +519,7 @@ class AgenticRunner:
if total_turn_in == 0 and total_turn_out == 0:
turns[0].input_tokens = in_tok
turns[0].output_tokens = out_tok
turns[0].wall_clock_s = (
turns[0].wall_clock_s or (end_time - start_time)
)
turns[0].wall_clock_s = turns[0].wall_clock_s or (end_time - start_time)
if cost and turns[0].cost_usd is None:
turns[0].cost_usd = cost
@@ -526,6 +529,7 @@ class AgenticRunner:
turn.input_tokens > 0 or turn.output_tokens > 0
):
from openjarvis.evals.core.pricing import compute_turn_cost
turn.cost_usd = compute_turn_cost(
model, turn.input_tokens, turn.output_tokens
)
@@ -538,16 +542,10 @@ class AgenticRunner:
# Extract MBU from telemetry readings
mbu_values = [
getattr(s, "gpu_memory_bandwidth_utilization_pct", None)
for s in readings
getattr(s, "gpu_memory_bandwidth_utilization_pct", None) for s in readings
]
mbu_values = [
v for v in mbu_values
if v is not None and v >= 0
]
query_mbu_avg = (
statistics.mean(mbu_values) if mbu_values else None
)
mbu_values = [v for v in mbu_values if v is not None and v >= 0]
query_mbu_avg = statistics.mean(mbu_values) if mbu_values else None
query_mbu_max = max(mbu_values) if mbu_values else None
trace = QueryTrace(
@@ -585,10 +583,7 @@ class AgenticRunner:
"""
start_ns = int(start_s * 1e9)
end_ns = int(end_s * 1e9)
window = [
r for r in readings
if start_ns <= r.timestamp_ns <= end_ns
]
window = [r for r in readings if start_ns <= r.timestamp_ns <= end_ns]
gpu_energy = _compute_energy_delta(window, "gpu_energy_j")
cpu_energy = _compute_energy_delta(window, "cpu_energy_j")
avg_gpu_power = _compute_power_avg(window, "gpu_power_w")
@@ -629,12 +624,14 @@ class AgenticRunner:
wall_clock = 0.0
if current_turn_start is not None:
wall_clock = event.timestamp - current_turn_start
current_action_spans.append({
"action_type": "lm_inference",
"start_s": current_turn_start,
"end_s": event.timestamp,
"duration_s": wall_clock,
})
current_action_spans.append(
{
"action_type": "lm_inference",
"start_s": current_turn_start,
"end_s": event.timestamp,
"duration_s": wall_clock,
}
)
input_tokens = event.metadata.get("prompt_tokens", 0)
output_tokens = event.metadata.get("completion_tokens", 0)
@@ -645,13 +642,17 @@ class AgenticRunner:
action_breakdown = []
for span in current_action_spans:
energy = self._action_energy_from_readings(
readings, span["start_s"], span["end_s"],
readings,
span["start_s"],
span["end_s"],
)
action_breakdown.append(
{
"action_type": span["action_type"],
"duration_s": span["duration_s"],
**energy,
}
)
action_breakdown.append({
"action_type": span["action_type"],
"duration_s": span["duration_s"],
**energy,
})
turn = TurnTrace(
turn_index=current_turn_index,
@@ -683,21 +684,25 @@ class AgenticRunner:
elif etype == EventType.TOOL_CALL_END:
tool_name = event.metadata.get("tool", "unknown")
current_tools.append(tool_name)
current_tool_calls.append({
"name": tool_name,
"arguments": current_tool_args.pop(tool_name, {}),
"result": event.metadata.get("result", ""),
})
current_tool_calls.append(
{
"name": tool_name,
"arguments": current_tool_args.pop(tool_name, {}),
"result": event.metadata.get("result", ""),
}
)
start_ts = tool_start_times.pop(tool_name, None)
if start_ts is not None:
duration = event.timestamp - start_ts
current_tool_latencies[tool_name] = duration
current_action_spans.append({
"action_type": f"tool_call:{tool_name}",
"start_s": start_ts,
"end_s": event.timestamp,
"duration_s": duration,
})
current_action_spans.append(
{
"action_type": f"tool_call:{tool_name}",
"start_s": start_ts,
"end_s": event.timestamp,
"duration_s": duration,
}
)
# Synthetic turn if events but no complete LM_START/END pair
if not turns and events:
@@ -724,9 +729,7 @@ class AgenticRunner:
if not readings or not trace.turns:
return trace
has_turn_energy = any(
t.gpu_energy_joules is not None for t in trace.turns
)
has_turn_energy = any(t.gpu_energy_joules is not None for t in trace.turns)
if has_turn_energy:
return trace
@@ -770,8 +773,11 @@ class AgenticRunner:
"num_turns": trace.num_turns,
}
for key in (
"repo", "base_commit", "dataset_name",
"is_resolved", "test_results",
"repo",
"base_commit",
"dataset_name",
"is_resolved",
"test_results",
):
val = record.metadata.get(key)
if val is not None:
+88 -71
View File
@@ -35,14 +35,31 @@ VALID_BACKENDS = {"jarvis-direct", "jarvis-agent"}
# Known benchmark names (used for warnings, not hard validation)
KNOWN_BENCHMARKS = {
"supergpqa", "gpqa", "mmlu-pro", "math500", "natural-reasoning", "hle",
"simpleqa", "wildchat", "ipw",
"gaia", "frames", "swebench", "swefficiency",
"terminalbench", "terminalbench-native",
"email_triage", "morning_brief", "research_mining",
"knowledge_base", "coding_task",
"coding_assistant", "security_scanner", "daily_digest",
"doc_qa", "browser_assistant",
"supergpqa",
"gpqa",
"mmlu-pro",
"math500",
"natural-reasoning",
"hle",
"simpleqa",
"wildchat",
"ipw",
"gaia",
"frames",
"swebench",
"swefficiency",
"terminalbench",
"terminalbench-native",
"email_triage",
"morning_brief",
"research_mining",
"knowledge_base",
"coding_task",
"coding_assistant",
"security_scanner",
"daily_digest",
"doc_qa",
"browser_assistant",
"pinchbench",
}
@@ -123,36 +140,32 @@ def load_eval_config(path: str | Path) -> EvalSuiteConfig:
for m in models_raw:
if not m.get("name"):
raise EvalConfigError("Each [[models]] entry must have a 'name' field")
models.append(ModelConfig(
name=m["name"],
engine=m.get("engine"),
provider=m.get("provider"),
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
),
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)),
))
models.append(
ModelConfig(
name=m["name"],
engine=m.get("engine"),
provider=m.get("provider"),
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
),
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)),
)
)
# Parse [[benchmarks]]
benchmarks_raw = raw.get("benchmarks", [])
if not benchmarks_raw:
raise EvalConfigError(
"Config must define at least one [[benchmarks]] entry"
)
raise EvalConfigError("Config must define at least one [[benchmarks]] entry")
benchmarks: List[BenchmarkConfig] = []
for b in benchmarks_raw:
if not b.get("name"):
raise EvalConfigError(
"Each [[benchmarks]] entry must have a 'name' field"
)
raise EvalConfigError("Each [[benchmarks]] entry must have a 'name' field")
backend = b.get("backend", "jarvis-direct")
if backend not in VALID_BACKENDS:
@@ -166,18 +179,20 @@ def load_eval_config(path: str | Path) -> EvalSuiteConfig:
logger.warning("Unknown benchmark name: '%s'", bench_name)
tools_raw = b.get("tools", [])
benchmarks.append(BenchmarkConfig(
name=bench_name,
backend=backend,
max_samples=int(b["max_samples"]) if "max_samples" in b else None,
split=b.get("split"),
agent=b.get("agent"),
tools=list(tools_raw),
judge_model=b.get("judge_model"),
temperature=float(b["temperature"]) if "temperature" in b else None,
max_tokens=int(b["max_tokens"]) if "max_tokens" in b else None,
subset=b.get("subset"),
))
benchmarks.append(
BenchmarkConfig(
name=bench_name,
backend=backend,
max_samples=int(b["max_samples"]) if "max_samples" in b else None,
split=b.get("split"),
agent=b.get("agent"),
tools=list(tools_raw),
judge_model=b.get("judge_model"),
temperature=float(b["temperature"]) if "temperature" in b else None,
max_tokens=int(b["max_tokens"]) if "max_tokens" in b else None,
subset=b.get("subset"),
)
)
return EvalSuiteConfig(
meta=meta,
@@ -245,35 +260,37 @@ def expand_suite(suite: EvalSuiteConfig) -> List[RunConfig]:
# Judge engine: suite.judge.engine > "cloud"
judge_engine = suite.judge.engine or "cloud"
configs.append(RunConfig(
benchmark=bench.name,
backend=bench.backend,
model=model.name,
max_samples=bench.max_samples,
max_workers=suite.run.max_workers,
temperature=temperature,
max_tokens=max_tokens,
judge_model=judge_model,
judge_engine=judge_engine,
engine_key=model.engine,
agent_name=bench.agent,
tools=list(bench.tools),
output_path=output_path,
seed=suite.run.seed,
dataset_split=bench.split,
dataset_subset=bench.subset,
telemetry=suite.run.telemetry,
gpu_metrics=suite.run.gpu_metrics,
metadata=model_meta,
warmup_samples=suite.run.warmup_samples,
wandb_project=suite.run.wandb_project,
wandb_entity=suite.run.wandb_entity,
wandb_tags=suite.run.wandb_tags,
wandb_group=suite.run.wandb_group,
sheets_spreadsheet_id=suite.run.sheets_spreadsheet_id,
sheets_worksheet=suite.run.sheets_worksheet,
sheets_credentials_path=suite.run.sheets_credentials_path,
))
configs.append(
RunConfig(
benchmark=bench.name,
backend=bench.backend,
model=model.name,
max_samples=bench.max_samples,
max_workers=suite.run.max_workers,
temperature=temperature,
max_tokens=max_tokens,
judge_model=judge_model,
judge_engine=judge_engine,
engine_key=model.engine,
agent_name=bench.agent,
tools=list(bench.tools),
output_path=output_path,
seed=suite.run.seed,
dataset_split=bench.split,
dataset_subset=bench.subset,
telemetry=suite.run.telemetry,
gpu_metrics=suite.run.gpu_metrics,
metadata=model_meta,
warmup_samples=suite.run.warmup_samples,
wandb_project=suite.run.wandb_project,
wandb_entity=suite.run.wandb_entity,
wandb_tags=suite.run.wandb_tags,
wandb_group=suite.run.wandb_group,
sheets_spreadsheet_id=suite.run.sheets_spreadsheet_id,
sheets_worksheet=suite.run.sheets_worksheet,
sheets_credentials_path=suite.run.sheets_credentials_path,
)
)
return configs
+2 -1
View File
@@ -34,7 +34,8 @@ class DatasetProvider(ABC):
"""Return the number of loaded records."""
def create_task_env(
self, record: EvalRecord,
self,
record: EvalRecord,
) -> Optional[AbstractContextManager]:
"""Return a task environment context manager, or None."""
return None
+31 -24
View File
@@ -125,8 +125,10 @@ def print_metrics_table(console: Console, summary: RunSummary) -> None:
_add_metric_row(table, "Power (W)", summary.power_stats)
_add_metric_row(table, "GPU Util (%)", summary.gpu_utilization_stats, decimals=1)
_add_metric_row(
table, "Energy/OutTok (J)",
summary.energy_per_output_token_stats, decimals=6,
table,
"Energy/OutTok (J)",
summary.energy_per_output_token_stats,
decimals=6,
)
_add_metric_row(table, "Throughput/Watt", summary.throughput_per_watt_stats)
_add_metric_row(table, "MFU (%)", summary.mfu_stats, decimals=2)
@@ -203,35 +205,43 @@ def print_accuracy_panel(console: Console, summary: RunSummary) -> None:
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,
body,
title="[bold]Accuracy[/bold]",
border_style="green",
expand=False,
)
console.print(panel)
def print_latency_table(console: Console, summary: RunSummary) -> None:
"""Print latency, throughput, and token stats table."""
table = _stats_table("Latency & Throughput", [
("Latency (s)", summary.latency_stats, 2),
("TTFT (s)", summary.ttft_stats, 3),
("Throughput (tok/s)", summary.throughput_stats, 1),
("Avg Input Tokens", summary.input_token_stats, 1),
("Avg Output Tokens", summary.output_token_stats, 1),
])
table = _stats_table(
"Latency & Throughput",
[
("Latency (s)", summary.latency_stats, 2),
("TTFT (s)", summary.ttft_stats, 3),
("Throughput (tok/s)", summary.throughput_stats, 1),
("Avg Input Tokens", summary.input_token_stats, 1),
("Avg Output Tokens", summary.output_token_stats, 1),
],
)
if table.row_count > 0:
console.print(table)
def print_energy_table(console: Console, summary: RunSummary) -> None:
"""Print energy, efficiency, and IPJ/IPW table."""
table = _stats_table("Energy & Efficiency", [
("Energy (J)", summary.energy_stats, 1),
("Power (W)", summary.power_stats, 1),
("GPU Util (%)", summary.gpu_utilization_stats, 1),
("Energy/OutTok (J)", summary.energy_per_output_token_stats, 6),
("MFU (%)", summary.mfu_stats, 3),
("MBU (%)", summary.mbu_stats, 3),
])
table = _stats_table(
"Energy & Efficiency",
[
("Energy (J)", summary.energy_stats, 1),
("Power (W)", summary.power_stats, 1),
("GPU Util (%)", summary.gpu_utilization_stats, 1),
("Energy/OutTok (J)", summary.energy_per_output_token_stats, 6),
("MFU (%)", summary.mfu_stats, 3),
("MBU (%)", summary.mbu_stats, 3),
],
)
if table.row_count > 0:
console.print(table)
# Headline: IPW, IPJ, Total Energy
@@ -258,9 +268,7 @@ def print_trace_summary(console: Console, summary: RunSummary) -> None:
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
total_steps / summary.scored_samples if summary.scored_samples > 0 else 0
)
table = Table(
@@ -270,8 +278,7 @@ def print_trace_summary(console: Console, summary: RunSummary) -> None:
border_style="bright_blue",
title_style="bold cyan",
caption=(
f"Total Steps: {total_steps}"
f" | Avg Steps/Sample: {avg_per_sample:.1f}"
f"Total Steps: {total_steps} | Avg Steps/Sample: {avg_per_sample:.1f}"
),
)
table.add_column("Step Type", style="cyan", no_wrap=True)
+2 -1
View File
@@ -34,7 +34,8 @@ class EnvironmentProvider(ABC):
@abstractmethod
def validate(
self, record: EvalRecord,
self,
record: EvalRecord,
) -> Tuple[bool, Dict[str, Any]]:
"""Check environment state against expected outcome.
+18 -27
View File
@@ -36,12 +36,10 @@ def _compute_efficiency(
resolved = sum(1 for t in scored if t.is_resolved is True)
accuracy = resolved / len(scored) if scored else None
gpu_powers = [
t.avg_gpu_power_watts for t in traces
if t.avg_gpu_power_watts is not None
t.avg_gpu_power_watts for t in traces if t.avg_gpu_power_watts is not None
]
cpu_powers = [
t.avg_cpu_power_watts for t in traces
if t.avg_cpu_power_watts is not None
t.avg_cpu_power_watts for t in traces if t.avg_cpu_power_watts is not None
]
avg_gpu_power = statistics.mean(gpu_powers) if gpu_powers else None
avg_cpu_power = statistics.mean(cpu_powers) if cpu_powers else None
@@ -51,16 +49,8 @@ def _compute_efficiency(
"total_cpu_energy_joules": total_cpu_energy,
"avg_gpu_power_watts": avg_gpu_power,
"avg_cpu_power_watts": avg_cpu_power,
"ipj": (
accuracy / total_gpu_energy
if accuracy and total_gpu_energy
else None
),
"ipw": (
accuracy / avg_gpu_power
if accuracy and avg_gpu_power
else None
),
"ipj": (accuracy / total_gpu_energy if accuracy and total_gpu_energy else None),
"ipw": (accuracy / avg_gpu_power if accuracy and avg_gpu_power else None),
}
@@ -77,14 +67,15 @@ def _compute_normalized(
trim_count = max(1, math.floor(n * 0.05))
sorted_traces = sorted(traces, key=lambda t: t.total_wall_clock_s)
trimmed = sorted_traces[trim_count: n - trim_count]
trimmed = sorted_traces[trim_count : n - trim_count]
if not trimmed:
return None
# Recompute aggregate energy on trimmed set
gpu_energy_values = [
t.total_gpu_energy_joules for t in trimmed
t.total_gpu_energy_joules
for t in trimmed
if t.total_gpu_energy_joules is not None
]
total_gpu_energy = sum(gpu_energy_values) if gpu_energy_values else None
@@ -92,7 +83,8 @@ def _compute_normalized(
cpu_energy_values: list[float] = []
for trace in trimmed:
cpu_vals = [
turn.cpu_energy_joules for turn in trace.turns
turn.cpu_energy_joules
for turn in trace.turns
if turn.cpu_energy_joules is not None
]
if cpu_vals:
@@ -188,6 +180,7 @@ def _hardware_info_dict() -> dict[str, Any]:
"""Detect hardware and return a JSON-serializable dict."""
try:
from openjarvis.core.config import detect_hardware
hw = detect_hardware()
info: dict[str, Any] = {
"platform": hw.platform,
@@ -237,7 +230,8 @@ def export_summary_json(
total_wall_clock_s = sum(t.total_wall_clock_s for t in traces)
gpu_energy_values = [
t.total_gpu_energy_joules for t in traces
t.total_gpu_energy_joules
for t in traces
if t.total_gpu_energy_joules is not None
]
total_gpu_energy = sum(gpu_energy_values) if gpu_energy_values else None
@@ -245,7 +239,8 @@ def export_summary_json(
cpu_energy_values: list[float] = []
for trace in traces:
cpu_vals = [
turn.cpu_energy_joules for turn in trace.turns
turn.cpu_energy_joules
for turn in trace.turns
if turn.cpu_energy_joules is not None
]
if cpu_vals:
@@ -255,10 +250,7 @@ def export_summary_json(
resolved = sum(1 for t in traces if t.is_resolved is True)
unresolved = sum(1 for t in traces if t.is_resolved is False)
cost_values = [
t.total_cost_usd for t in traces
if t.total_cost_usd is not None
]
cost_values = [t.total_cost_usd for t in traces if t.total_cost_usd is not None]
total_cost = sum(cost_values) if cost_values else None
avg_turns = total_turns / total_queries if total_queries > 0 else 0
@@ -309,9 +301,7 @@ def export_summary_json(
}
accuracy = (
resolved / (resolved + unresolved)
if (resolved + unresolved) > 0
else None
resolved / (resolved + unresolved) if (resolved + unresolved) > 0 else None
)
efficiency = _compute_efficiency(traces, total_gpu_energy, total_cpu_energy)
@@ -336,7 +326,8 @@ def export_summary_json(
entry = action_totals[atype]
entry["count"] += 1
entry["total_duration_s"] += action.get(
"duration_s", 0.0,
"duration_s",
0.0,
)
gpu_e = action.get("gpu_energy_joules")
if gpu_e is not None:
+79 -81
View File
@@ -102,6 +102,7 @@ class EvalRunner:
# default iter_episodes() that wraps each record in its own episode,
# so hasattr() is always True — we must check for a real override.
from openjarvis.evals.core.dataset import DatasetProvider as _DP
try:
_overrides_episodes = (
type(self._dataset).iter_episodes is not _DP.iter_episodes
@@ -120,18 +121,20 @@ class EvalRunner:
# Detect if dataset provides task environments (e.g. PinchBench)
try:
self._has_task_env = (
type(self._dataset).create_task_env
is not _DP.create_task_env
type(self._dataset).create_task_env is not _DP.create_task_env
)
except AttributeError:
self._has_task_env = False
records = list(self._dataset.iter_records())
LOGGER.info(
"Running %s: %d samples, backend=%s, model=%s, workers=%d, "
"episode_mode=%s",
cfg.benchmark, len(records), cfg.backend, cfg.model,
cfg.max_workers, cfg.episode_mode,
"Running %s: %d samples, backend=%s, model=%s, workers=%d, episode_mode=%s",
cfg.benchmark,
len(records),
cfg.backend,
cfg.model,
cfg.max_workers,
cfg.episode_mode,
)
# --- Warmup phase (discard results) ---
@@ -155,7 +158,8 @@ class EvalRunner:
except Exception as exc:
LOGGER.warning(
"Tracker %s.on_run_start failed: %s",
type(tracker).__name__, exc,
type(tracker).__name__,
exc,
)
total = len(records)
@@ -173,9 +177,7 @@ class EvalRunner:
progress_callback(len(self._results), total)
else:
with ThreadPoolExecutor(max_workers=cfg.max_workers) as pool:
futures = {
pool.submit(self._process_one, r): r for r in records
}
futures = {pool.submit(self._process_one, r): r for r in records}
for future in as_completed(futures):
result = future.result()
self._results.append(result)
@@ -197,14 +199,16 @@ class EvalRunner:
except Exception as exc:
LOGGER.warning(
"Tracker %s.on_summary failed: %s",
type(tracker).__name__, exc,
type(tracker).__name__,
exc,
)
try:
tracker.on_run_end()
except Exception as exc:
LOGGER.warning(
"Tracker %s.on_run_end failed: %s",
type(tracker).__name__, exc,
type(tracker).__name__,
exc,
)
# Write summary JSON alongside JSONL
@@ -253,6 +257,7 @@ class EvalRunner:
if getattr(self, "_has_task_env", False):
from contextlib import nullcontext
task_env = self._dataset.create_task_env(record)
ctx = task_env if task_env is not None else nullcontext()
with ctx:
@@ -261,22 +266,23 @@ class EvalRunner:
**gen_kwargs,
)
full = full or {}
all_tool_results = list(full.get(
"tool_results", [],
))
all_tool_results = list(
full.get(
"tool_results",
[],
)
)
# Multi-session: execute remaining sessions
sessions = record.metadata.get("sessions", [])
if (
record.metadata.get("multi_session")
and len(sessions) > 1
):
if record.metadata.get("multi_session") and len(sessions) > 1:
for session in sessions[1:]:
prompt = session.get("prompt", "")
if not prompt:
continue
sfull = self._backend.generate_full(
prompt, **gen_kwargs,
prompt,
**gen_kwargs,
)
sfull = sfull or {}
all_tool_results.extend(
@@ -287,7 +293,8 @@ class EvalRunner:
# Score INSIDE context so workspace files still exist
content = full.get("content", "")
is_correct, scoring_meta = self._scorer.score(
record, content,
record,
content,
)
else:
full = self._backend.generate_full(
@@ -297,7 +304,8 @@ class EvalRunner:
full = full or {}
content = full.get("content", "")
is_correct, scoring_meta = self._scorer.score(
record, content,
record,
content,
)
usage = full.get("usage", {})
@@ -340,9 +348,7 @@ class EvalRunner:
# Extract derived and ITL metrics from _telemetry dict
_telem = full.get("_telemetry", {})
energy_per_out_tok = _telem.get(
"energy_per_output_token_joules", 0.0
)
energy_per_out_tok = _telem.get("energy_per_output_token_joules", 0.0)
throughput_per_w = _telem.get("throughput_per_watt", 0.0)
mean_itl = _telem.get("mean_itl_ms", 0.0)
@@ -406,9 +412,9 @@ class EvalRunner:
# default implementation that returns None, so hasattr() is always True —
# we must check for a real override to avoid calling env.reset() on None.
from openjarvis.evals.core.dataset import DatasetProvider
has_task_env = (
type(self._dataset).create_task_env
is not DatasetProvider.create_task_env
type(self._dataset).create_task_env is not DatasetProvider.create_task_env
)
for episode in self._dataset.iter_episodes():
@@ -417,12 +423,14 @@ class EvalRunner:
for record in episode:
if has_task_env:
result = self._process_interactive(
record, successful_examples,
record,
successful_examples,
)
else:
# Inject prior examples into prompt, then single-shot
augmented = self._inject_examples(
record, successful_examples,
record,
successful_examples,
)
result = self._process_one(augmented)
@@ -437,9 +445,13 @@ class EvalRunner:
"answer": result.model_answer,
}
# Attach full interaction history if recorded
interaction = result.scoring_metadata.get(
"_interaction_history",
) if result.scoring_metadata else None
interaction = (
result.scoring_metadata.get(
"_interaction_history",
)
if result.scoring_metadata
else None
)
if interaction:
example["interaction_history"] = interaction
successful_examples.append(example)
@@ -482,8 +494,7 @@ class EvalRunner:
else:
# Fallback: problem + answer summary
example_text += (
f"Task: {ex['problem'][:800]}\n"
f"Solution: {ex['answer'][:800]}\n\n"
f"Task: {ex['problem'][:800]}\nSolution: {ex['answer'][:800]}\n\n"
)
example_text += "## Current Task\n\n"
@@ -549,9 +560,7 @@ class EvalRunner:
# PreviousSampleUtilizationCallback which replays the complete
# chat history from prior successful sessions.
if prior_examples:
examples_text = (
"Here are examples of previously completed tasks:\n\n"
)
examples_text = "Here are examples of previously completed tasks:\n\n"
for i, ex in enumerate(prior_examples, 1):
history = ex.get("interaction_history")
if history and isinstance(history, list):
@@ -573,10 +582,12 @@ class EvalRunner:
f"Solution: {ex['answer'][:800]}\n\n"
)
messages.append({"role": "user", "content": examples_text})
messages.append({
"role": "assistant",
"content": "I've reviewed the examples. Ready.",
})
messages.append(
{
"role": "assistant",
"content": "I've reviewed the examples. Ready.",
}
)
# Initial task message — always use the full problem text which
# contains the system prompt, schema, AND task instruction.
@@ -648,7 +659,8 @@ class EvalRunner:
except Exception as exc:
LOGGER.error(
"Interactive processing failed for %s: %s",
record.record_id, exc,
record.record_id,
exc,
)
return EvalResult(
record_id=record.record_id,
@@ -720,15 +732,18 @@ class EvalRunner:
except Exception as exc:
LOGGER.error(
"Failed to serialize result %s to JSON: %s — writing error record",
result.record_id, exc,
result.record_id,
exc,
)
line = json.dumps(
{
"record_id": result.record_id,
"benchmark": self._config.benchmark,
"model": self._config.model,
"is_correct": result.is_correct,
"error": f"serialization_error: {exc}",
}
)
line = json.dumps({
"record_id": result.record_id,
"benchmark": self._config.benchmark,
"model": self._config.model,
"is_correct": result.is_correct,
"error": f"serialization_error: {exc}",
})
self._output_file.write(line + "\n")
self._output_file.flush()
@@ -739,7 +754,8 @@ class EvalRunner:
except Exception as exc:
LOGGER.warning(
"Tracker %s.on_result failed: %s",
type(tracker).__name__, exc,
type(tracker).__name__,
exc,
)
def _resolve_output_path(self) -> Optional[Path]:
@@ -802,12 +818,10 @@ class EvalRunner:
energy_vals = [r.energy_joules for r in results if r.energy_joules > 0]
power_vals = [r.power_watts for r in results if r.power_watts > 0]
gpu_util_vals = [
r.gpu_utilization_pct for r in results
if r.gpu_utilization_pct > 0
r.gpu_utilization_pct for r in results if r.gpu_utilization_pct > 0
]
throughput_vals = [
r.throughput_tok_per_sec for r in results
if r.throughput_tok_per_sec > 0
r.throughput_tok_per_sec for r in results if r.throughput_tok_per_sec > 0
]
mfu_vals = [r.mfu_pct for r in results if r.mfu_pct > 0]
mbu_vals = [r.mbu_pct for r in results if r.mbu_pct > 0]
@@ -818,15 +832,11 @@ class EvalRunner:
for r in results
if r.energy_per_output_token_joules > 0
]
tpw_vals = [
r.throughput_per_watt
for r in results if r.throughput_per_watt > 0
]
tpw_vals = [r.throughput_per_watt for r in results if r.throughput_per_watt > 0]
itl_vals = [r.mean_itl_ms for r in results if r.mean_itl_ms > 0]
input_tok_vals = [r.prompt_tokens for r in results if r.prompt_tokens > 0]
output_tok_vals = [
r.completion_tokens for r in results
if r.completion_tokens > 0
r.completion_tokens for r in results if r.completion_tokens > 0
]
total_energy = sum(r.energy_joules for r in results)
@@ -839,19 +849,14 @@ class EvalRunner:
"accuracy": round(accuracy, 4),
"total_energy_joules": round(total_energy, 6),
"avg_power_watts": round(avg_power, 4),
"ipj": (
round(accuracy / total_energy, 6)
if total_energy > 0 else None
),
"ipw": (
round(accuracy / avg_power, 6)
if avg_power > 0 else None
),
"ipj": (round(accuracy / total_energy, 6) if total_energy > 0 else None),
"ipw": (round(accuracy / avg_power, 6) if avg_power > 0 else None),
}
# Compute normalized statistics (trim 5% outliers by latency)
normalized_stats, normalized_eff = _compute_normalized_stats(
results, accuracy,
results,
accuracy,
)
return RunSummary(
@@ -911,7 +916,7 @@ def _compute_normalized_stats(
trim_count = max(1, math.floor(n * 0.05))
sorted_results = sorted(results, key=lambda r: r.latency_seconds)
trimmed = sorted_results[trim_count: n - trim_count]
trimmed = sorted_results[trim_count : n - trim_count]
if not trimmed:
return None, None
@@ -924,8 +929,7 @@ def _compute_normalized_stats(
t_energy_vals = [r.energy_joules for r in trimmed if r.energy_joules > 0]
t_power_vals = [r.power_watts for r in trimmed if r.power_watts > 0]
t_throughput_vals = [
r.throughput_tok_per_sec for r in trimmed
if r.throughput_tok_per_sec > 0
r.throughput_tok_per_sec for r in trimmed if r.throughput_tok_per_sec > 0
]
t_mbu_vals = [r.mbu_pct for r in trimmed if r.mbu_pct > 0]
@@ -951,14 +955,8 @@ def _compute_normalized_stats(
"accuracy": round(t_accuracy, 4),
"total_energy_joules": round(t_total_energy, 6),
"avg_power_watts": round(t_avg_power, 4),
"ipj": (
round(t_accuracy / t_total_energy, 6)
if t_total_energy > 0 else None
),
"ipw": (
round(t_accuracy / t_avg_power, 6)
if t_avg_power > 0 else None
),
"ipj": (round(t_accuracy / t_total_energy, 6) if t_total_energy > 0 else None),
"ipw": (round(t_accuracy / t_avg_power, 6) if t_avg_power > 0 else None),
}
return norm_stats, norm_eff
+3 -1
View File
@@ -16,7 +16,9 @@ class Scorer(ABC):
@abstractmethod
def score(
self, record: EvalRecord, model_answer: str,
self,
record: EvalRecord,
model_answer: str,
) -> Tuple[Optional[bool], Dict[str, Any]]:
"""Score a model answer against the reference.
+33 -31
View File
@@ -115,8 +115,7 @@ class QueryTrace:
@property
def total_gpu_energy_joules(self) -> Optional[float]:
values = [
t.gpu_energy_joules for t in self.turns
if t.gpu_energy_joules is not None
t.gpu_energy_joules for t in self.turns if t.gpu_energy_joules is not None
]
if values:
return sum(values)
@@ -125,8 +124,7 @@ class QueryTrace:
@property
def total_cpu_energy_joules(self) -> Optional[float]:
values = [
t.cpu_energy_joules for t in self.turns
if t.cpu_energy_joules is not None
t.cpu_energy_joules for t in self.turns if t.cpu_energy_joules is not None
]
if values:
return sum(values)
@@ -146,7 +144,8 @@ class QueryTrace:
def avg_gpu_power_watts(self) -> Optional[float]:
"""Mean GPU power across turns; falls back to query-level power."""
values = [
t.gpu_power_avg_watts for t in self.turns
t.gpu_power_avg_watts
for t in self.turns
if t.gpu_power_avg_watts is not None
]
if values:
@@ -157,7 +156,8 @@ class QueryTrace:
def avg_cpu_power_watts(self) -> Optional[float]:
"""Mean CPU power across turns; falls back to query-level power."""
values = [
t.cpu_power_avg_watts for t in self.turns
t.cpu_power_avg_watts
for t in self.turns
if t.cpu_power_avg_watts is not None
]
if values:
@@ -246,31 +246,33 @@ class QueryTrace:
rows = []
for trace in traces:
rows.append({
"query_id": trace.query_id,
"workload_type": trace.workload_type,
"query_text": trace.query_text,
"response_text": trace.response_text,
"num_turns": trace.num_turns,
"total_input_tokens": trace.total_input_tokens,
"total_output_tokens": trace.total_output_tokens,
"total_tool_calls": trace.total_tool_calls,
"total_wall_clock_s": trace.total_wall_clock_s,
"total_gpu_energy_joules": trace.total_gpu_energy_joules,
"total_cpu_energy_joules": trace.total_cpu_energy_joules,
"total_tokens": trace.total_tokens,
"total_cost_usd": trace.total_cost_usd,
"avg_gpu_power_watts": trace.avg_gpu_power_watts,
"avg_cpu_power_watts": trace.avg_cpu_power_watts,
"throughput_tokens_per_sec": trace.throughput_tokens_per_sec,
"energy_per_token_joules": trace.energy_per_token_joules,
"completed": trace.completed,
"timed_out": trace.timed_out,
"is_resolved": trace.is_resolved,
"query_mbu_avg_pct": trace.query_mbu_avg_pct,
"query_mbu_max_pct": trace.query_mbu_max_pct,
"trace_json": json.dumps(trace.to_dict()),
})
rows.append(
{
"query_id": trace.query_id,
"workload_type": trace.workload_type,
"query_text": trace.query_text,
"response_text": trace.response_text,
"num_turns": trace.num_turns,
"total_input_tokens": trace.total_input_tokens,
"total_output_tokens": trace.total_output_tokens,
"total_tool_calls": trace.total_tool_calls,
"total_wall_clock_s": trace.total_wall_clock_s,
"total_gpu_energy_joules": trace.total_gpu_energy_joules,
"total_cpu_energy_joules": trace.total_cpu_energy_joules,
"total_tokens": trace.total_tokens,
"total_cost_usd": trace.total_cost_usd,
"avg_gpu_power_watts": trace.avg_gpu_power_watts,
"avg_cpu_power_watts": trace.avg_cpu_power_watts,
"throughput_tokens_per_sec": trace.throughput_tokens_per_sec,
"energy_per_token_joules": trace.energy_per_token_joules,
"completed": trace.completed,
"timed_out": trace.timed_out,
"is_resolved": trace.is_resolved,
"query_mbu_avg_pct": trace.query_mbu_avg_pct,
"query_mbu_max_pct": trace.query_mbu_max_pct,
"trace_json": json.dumps(trace.to_dict()),
}
)
return Dataset.from_list(rows)
+43 -34
View File
@@ -130,7 +130,8 @@ class AMABenchDataset(DatasetProvider):
return [dict(row) for row in rows]
def _row_to_episode(
self, row: Dict[str, Any],
self,
row: Dict[str, Any],
) -> List[EvalRecord]:
"""Convert one AMA-Bench episode row to EvalRecord(s)."""
episode_id = str(row.get("episode_id", "")).strip()
@@ -139,11 +140,15 @@ class AMABenchDataset(DatasetProvider):
trajectory = row.get("trajectory")
if not isinstance(trajectory, list):
raise ValueError(f"AMA-Bench episode {episode_id}: trajectory must be a list")
raise ValueError(
f"AMA-Bench episode {episode_id}: trajectory must be a list"
)
qa_pairs = row.get("qa_pairs")
if not isinstance(qa_pairs, list) or not qa_pairs:
raise ValueError(f"AMA-Bench episode {episode_id}: qa_pairs must be a non-empty list")
raise ValueError(
f"AMA-Bench episode {episode_id}: qa_pairs must be a non-empty list"
)
task = str(row.get("task", "")).strip()
domain = str(row.get("domain", "")).strip() or "general"
@@ -159,12 +164,15 @@ class AMABenchDataset(DatasetProvider):
if len(trajectory_text) > max_chars:
original_len = len(trajectory_text)
trajectory_text = self._truncate_trajectory_text(
trajectory_text, max_chars,
trajectory_text,
max_chars,
)
LOGGER.info(
"AMA-Bench episode %s: trajectory truncated from %d to %d chars "
"(first 50%% + last 50%% of budget kept per Appendix B)",
episode_id, original_len, len(trajectory_text),
episode_id,
original_len,
len(trajectory_text),
)
records: List[EvalRecord] = []
@@ -194,31 +202,33 @@ class AMABenchDataset(DatasetProvider):
f"## Question\n{question}"
)
records.append(EvalRecord(
record_id=(
f"ama-{episode_id}-{q_uuid}"
if q_uuid
else f"ama-{episode_id}-q{question_index}"
),
problem=problem,
reference=answer,
category="agentic",
subject=subject,
metadata={
"episode_id": episode_id,
"task": task,
"task_type": task_type,
"source": source,
"domain": domain,
"success": success,
"num_turns": num_turns,
"total_tokens": total_tokens,
"question_index": question_index,
"question_uuid": q_uuid,
"question_type": q_type,
"capability": subject,
},
))
records.append(
EvalRecord(
record_id=(
f"ama-{episode_id}-{q_uuid}"
if q_uuid
else f"ama-{episode_id}-q{question_index}"
),
problem=problem,
reference=answer,
category="agentic",
subject=subject,
metadata={
"episode_id": episode_id,
"task": task,
"task_type": task_type,
"source": source,
"domain": domain,
"success": success,
"num_turns": num_turns,
"total_tokens": total_tokens,
"question_index": question_index,
"question_uuid": q_uuid,
"question_type": q_type,
"capability": subject,
},
)
)
return records
@@ -240,7 +250,8 @@ class AMABenchDataset(DatasetProvider):
@staticmethod
def _truncate_trajectory_text(
trajectory_text: str, max_chars: int,
trajectory_text: str,
max_chars: int,
) -> str:
"""Truncate formatted trajectory text by keeping first 50% + last 50%
of the character budget, discarding the middle.
@@ -254,9 +265,7 @@ class AMABenchDataset(DatasetProvider):
if budget_each <= 0:
return trajectory_text[:max_chars]
return (
trajectory_text[:budget_each]
+ separator
+ trajectory_text[-budget_each:]
trajectory_text[:budget_each] + separator + trajectory_text[-budget_each:]
)
@@ -81,7 +81,10 @@ _EASY_TASKS: List[Dict[str, Any]] = [
{
"question": "How many attention heads does GPT-4 use (as reported in public documentation)?",
"expected_facts": [
{"fact": "architecture details not publicly disclosed by OpenAI", "type": "semantic"},
{
"fact": "architecture details not publicly disclosed by OpenAI",
"type": "semantic",
},
],
},
{
@@ -116,9 +119,18 @@ _MEDIUM_TASKS: List[Dict[str, Any]] = [
{
"question": "Compare vLLM and TensorRT-LLM for serving large language models.",
"expected_facts": [
{"fact": "vLLM uses PagedAttention for memory efficiency", "type": "semantic"},
{"fact": "TensorRT-LLM optimizes for NVIDIA GPUs specifically", "type": "semantic"},
{"fact": "vLLM is easier to set up, TensorRT-LLM has higher throughput on supported hardware", "type": "semantic"},
{
"fact": "vLLM uses PagedAttention for memory efficiency",
"type": "semantic",
},
{
"fact": "TensorRT-LLM optimizes for NVIDIA GPUs specifically",
"type": "semantic",
},
{
"fact": "vLLM is easier to set up, TensorRT-LLM has higher throughput on supported hardware",
"type": "semantic",
},
],
},
{
@@ -133,8 +145,14 @@ _MEDIUM_TASKS: List[Dict[str, Any]] = [
"question": "Compare PostgreSQL and MySQL for JSON document storage.",
"expected_facts": [
{"fact": "PostgreSQL has JSONB with GIN indexes", "type": "semantic"},
{"fact": "MySQL has JSON type with generated columns for indexing", "type": "semantic"},
{"fact": "PostgreSQL JSONB is generally more feature-rich for JSON operations", "type": "semantic"},
{
"fact": "MySQL has JSON type with generated columns for indexing",
"type": "semantic",
},
{
"fact": "PostgreSQL JSONB is generally more feature-rich for JSON operations",
"type": "semantic",
},
],
},
{
@@ -142,15 +160,24 @@ _MEDIUM_TASKS: List[Dict[str, Any]] = [
"expected_facts": [
{"fact": "Podman is daemonless", "type": "semantic"},
{"fact": "Podman runs rootless by default", "type": "semantic"},
{"fact": "Docker uses a client-server architecture with dockerd", "type": "semantic"},
{
"fact": "Docker uses a client-server architecture with dockerd",
"type": "semantic",
},
],
},
{
"question": "Compare Terraform and Pulumi for infrastructure as code.",
"expected_facts": [
{"fact": "Terraform uses HCL, Pulumi uses general-purpose languages", "type": "semantic"},
{
"fact": "Terraform uses HCL, Pulumi uses general-purpose languages",
"type": "semantic",
},
{"fact": "both support multiple cloud providers", "type": "semantic"},
{"fact": "Pulumi has native support for Python, TypeScript, Go, C#", "type": "semantic"},
{
"fact": "Pulumi has native support for Python, TypeScript, Go, C#",
"type": "semantic",
},
],
},
{
@@ -164,9 +191,15 @@ _MEDIUM_TASKS: List[Dict[str, Any]] = [
{
"question": "Compare FastAPI, Flask, and Django for building REST APIs.",
"expected_facts": [
{"fact": "FastAPI is async-first with automatic OpenAPI docs", "type": "semantic"},
{
"fact": "FastAPI is async-first with automatic OpenAPI docs",
"type": "semantic",
},
{"fact": "Flask is lightweight and flexible", "type": "semantic"},
{"fact": "Django includes ORM, admin, and batteries-included approach", "type": "semantic"},
{
"fact": "Django includes ORM, admin, and batteries-included approach",
"type": "semantic",
},
],
},
{
@@ -187,33 +220,63 @@ _HARD_TASKS: List[Dict[str, Any]] = [
"question": "What is the current state of the art for code generation benchmarks (HumanEval, MBPP)? Which models lead and what are their scores?",
"expected_facts": [
{"fact": "recent top models score 90%+ on HumanEval", "type": "semantic"},
{"fact": "MBPP scores are generally lower than HumanEval", "type": "semantic"},
{
"fact": "MBPP scores are generally lower than HumanEval",
"type": "semantic",
},
{"fact": "mentions specific model names and scores", "type": "semantic"},
],
},
{
"question": "Explain the trade-offs between different attention mechanisms: multi-head attention, grouped-query attention, and multi-query attention.",
"expected_facts": [
{"fact": "MHA uses separate K,V heads per attention head", "type": "semantic"},
{"fact": "GQA groups multiple query heads to share K,V heads", "type": "semantic"},
{"fact": "MQA uses single K,V head for all query heads", "type": "semantic"},
{
"fact": "MHA uses separate K,V heads per attention head",
"type": "semantic",
},
{
"fact": "GQA groups multiple query heads to share K,V heads",
"type": "semantic",
},
{
"fact": "MQA uses single K,V head for all query heads",
"type": "semantic",
},
{"fact": "GQA balances quality and inference speed", "type": "semantic"},
],
},
{
"question": "What are the latest developments in mixture-of-experts (MoE) architectures for LLMs? Compare Mixtral, DeepSeek-V2, and Grok-1.",
"expected_facts": [
{"fact": "Mixtral 8x7B routes to 2 of 8 experts per token", "type": "semantic"},
{"fact": "DeepSeek-V2 uses fine-grained expert segmentation", "type": "semantic"},
{"fact": "MoE reduces compute cost per token vs dense models", "type": "semantic"},
{
"fact": "Mixtral 8x7B routes to 2 of 8 experts per token",
"type": "semantic",
},
{
"fact": "DeepSeek-V2 uses fine-grained expert segmentation",
"type": "semantic",
},
{
"fact": "MoE reduces compute cost per token vs dense models",
"type": "semantic",
},
],
},
{
"question": "How do different quantization methods (GPTQ, AWQ, GGUF/GGML, bitsandbytes) compare for LLM inference?",
"expected_facts": [
{"fact": "GPTQ uses post-training quantization with calibration data", "type": "semantic"},
{"fact": "AWQ preserves salient weights for better quality", "type": "semantic"},
{"fact": "GGUF is the llama.cpp format for CPU and mixed inference", "type": "semantic"},
{
"fact": "GPTQ uses post-training quantization with calibration data",
"type": "semantic",
},
{
"fact": "AWQ preserves salient weights for better quality",
"type": "semantic",
},
{
"fact": "GGUF is the llama.cpp format for CPU and mixed inference",
"type": "semantic",
},
{"fact": "bitsandbytes supports QLoRA for fine-tuning", "type": "semantic"},
],
},
@@ -221,17 +284,35 @@ _HARD_TASKS: List[Dict[str, Any]] = [
"question": "What are the security implications of running LLMs in production? Cover prompt injection, data exfiltration, and model poisoning.",
"expected_facts": [
{"fact": "prompt injection can bypass system prompts", "type": "semantic"},
{"fact": "data exfiltration via tool use or function calling", "type": "semantic"},
{"fact": "model poisoning through training data contamination", "type": "semantic"},
{"fact": "mitigations include input filtering and output scanning", "type": "semantic"},
{
"fact": "data exfiltration via tool use or function calling",
"type": "semantic",
},
{
"fact": "model poisoning through training data contamination",
"type": "semantic",
},
{
"fact": "mitigations include input filtering and output scanning",
"type": "semantic",
},
],
},
{
"question": "Compare the architectures and capabilities of major embedding models: OpenAI ada-002/3, Cohere embed-v3, and open-source alternatives like BGE and E5.",
"expected_facts": [
{"fact": "OpenAI text-embedding-3 supports Matryoshka dimensions", "type": "semantic"},
{"fact": "Cohere embed-v3 supports multiple input types", "type": "semantic"},
{"fact": "BGE and E5 are open-source alternatives with competitive performance", "type": "semantic"},
{
"fact": "OpenAI text-embedding-3 supports Matryoshka dimensions",
"type": "semantic",
},
{
"fact": "Cohere embed-v3 supports multiple input types",
"type": "semantic",
},
{
"fact": "BGE and E5 are open-source alternatives with competitive performance",
"type": "semantic",
},
],
},
{
@@ -246,10 +327,22 @@ _HARD_TASKS: List[Dict[str, Any]] = [
{
"question": "How do different RAG (Retrieval-Augmented Generation) strategies compare? Cover naive RAG, advanced RAG with re-ranking, and GraphRAG.",
"expected_facts": [
{"fact": "naive RAG does simple similarity search then generate", "type": "semantic"},
{"fact": "re-ranking improves retrieval precision with cross-encoder", "type": "semantic"},
{"fact": "GraphRAG uses knowledge graphs for structured retrieval", "type": "semantic"},
{"fact": "hybrid search combines dense and sparse retrieval", "type": "semantic"},
{
"fact": "naive RAG does simple similarity search then generate",
"type": "semantic",
},
{
"fact": "re-ranking improves retrieval precision with cross-encoder",
"type": "semantic",
},
{
"fact": "GraphRAG uses knowledge graphs for structured retrieval",
"type": "semantic",
},
{
"fact": "hybrid search combines dense and sparse retrieval",
"type": "semantic",
},
],
},
{
@@ -257,17 +350,32 @@ _HARD_TASKS: List[Dict[str, Any]] = [
"expected_facts": [
{"fact": "LoRA adds low-rank adapter matrices", "type": "semantic"},
{"fact": "QLoRA combines quantization with LoRA", "type": "semantic"},
{"fact": "DoRA decomposes weights into magnitude and direction", "type": "semantic"},
{"fact": "full fine-tuning updates all parameters but needs more memory", "type": "semantic"},
{
"fact": "DoRA decomposes weights into magnitude and direction",
"type": "semantic",
},
{
"fact": "full fine-tuning updates all parameters but needs more memory",
"type": "semantic",
},
],
},
{
"question": "Explain the evolution of position encoding in transformers: absolute, RoPE, ALiBi, and YaRN. What are the trade-offs for long-context extension?",
"expected_facts": [
{"fact": "absolute position embeddings are fixed length", "type": "semantic"},
{"fact": "RoPE encodes relative position through rotation", "type": "semantic"},
{
"fact": "absolute position embeddings are fixed length",
"type": "semantic",
},
{
"fact": "RoPE encodes relative position through rotation",
"type": "semantic",
},
{"fact": "ALiBi adds linear bias to attention scores", "type": "semantic"},
{"fact": "YaRN extends context through interpolation of RoPE", "type": "semantic"},
{
"fact": "YaRN extends context through interpolation of RoPE",
"type": "semantic",
},
],
},
]
@@ -305,29 +413,27 @@ class BrowserAssistantDataset(DatasetProvider):
)
exact_facts = [
f["fact"] for f in task["expected_facts"]
if f["type"] == "exact"
f["fact"] for f in task["expected_facts"] if f["type"] == "exact"
]
semantic_facts = [
f["fact"] for f in task["expected_facts"]
if f["type"] == "semantic"
f["fact"] for f in task["expected_facts"] if f["type"] == "semantic"
]
self._records.append(EvalRecord(
record_id=f"browser-assistant-{idx:03d}",
problem=prompt,
reference="; ".join(
f["fact"] for f in task["expected_facts"]
),
category="agentic",
subject=diff,
metadata={
"question": task["question"],
"expected_facts": task["expected_facts"],
"exact_facts": exact_facts,
"semantic_facts": semantic_facts,
},
))
self._records.append(
EvalRecord(
record_id=f"browser-assistant-{idx:03d}",
problem=prompt,
reference="; ".join(f["fact"] for f in task["expected_facts"]),
category="agentic",
subject=diff,
metadata={
"question": task["question"],
"expected_facts": task["expected_facts"],
"exact_facts": exact_facts,
"semantic_facts": semantic_facts,
},
)
)
def iter_records(self) -> Iterable[EvalRecord]:
return iter(self._records)
+247 -49
View File
@@ -64,7 +64,13 @@ _EASY_TASKS: List[Dict[str, Any]] = [
"def test_empty_page():\n"
" assert paginate(list(range(10)), 5, 3) == []\n"
),
"bugs": [{"description": "Off-by-one: end = start + per_page - 1 should be start + per_page", "file": "solution.py", "line": 3}],
"bugs": [
{
"description": "Off-by-one: end = start + per_page - 1 should be start + per_page",
"file": "solution.py",
"line": 3,
}
],
"originally_failing_tests": ["test_first_page", "test_second_page"],
"originally_passing_tests": ["test_last_page", "test_empty_page"],
},
@@ -91,20 +97,20 @@ _EASY_TASKS: List[Dict[str, Any]] = [
"def test_empty():\n"
" assert is_palindrome('') is True\n"
),
"bugs": [{"description": "Unnecessary len(s) > 1 check rejects single chars and empty strings", "file": "solution.py", "line": 3}],
"bugs": [
{
"description": "Unnecessary len(s) > 1 check rejects single chars and empty strings",
"file": "solution.py",
"line": 3,
}
],
"originally_failing_tests": ["test_single_char", "test_empty"],
"originally_passing_tests": ["test_racecar", "test_hello"],
},
{
"bug_report": "The celsius_to_fahrenheit function returns wrong values.",
"buggy_code": (
"def celsius_to_fahrenheit(c):\n"
" return c * 9 / 5 + 23\n"
),
"fixed_code": (
"def celsius_to_fahrenheit(c):\n"
" return c * 9 / 5 + 32\n"
),
"buggy_code": ("def celsius_to_fahrenheit(c):\n return c * 9 / 5 + 23\n"),
"fixed_code": ("def celsius_to_fahrenheit(c):\n return c * 9 / 5 + 32\n"),
"test_code": (
"from solution import celsius_to_fahrenheit\n\n"
"def test_freezing():\n"
@@ -114,7 +120,13 @@ _EASY_TASKS: List[Dict[str, Any]] = [
"def test_body_temp():\n"
" assert abs(celsius_to_fahrenheit(37) - 98.6) < 0.01\n"
),
"bugs": [{"description": "Wrong constant: +23 should be +32", "file": "solution.py", "line": 2}],
"bugs": [
{
"description": "Wrong constant: +23 should be +32",
"file": "solution.py",
"line": 2,
}
],
"originally_failing_tests": ["test_freezing", "test_boiling", "test_body_temp"],
"originally_passing_tests": [],
},
@@ -151,7 +163,13 @@ _EASY_TASKS: List[Dict[str, Any]] = [
"def test_deeply_nested():\n"
" assert flatten([[[1]], [[2]], [[3]]]) == [1, 2, 3]\n"
),
"bugs": [{"description": "append should be extend for recursive flattening", "file": "solution.py", "line": 5}],
"bugs": [
{
"description": "append should be extend for recursive flattening",
"file": "solution.py",
"line": 5,
}
],
"originally_failing_tests": ["test_nested", "test_deeply_nested"],
"originally_passing_tests": ["test_flat", "test_empty"],
},
@@ -194,9 +212,19 @@ _EASY_TASKS: List[Dict[str, Any]] = [
"def test_not_found():\n"
" assert binary_search([1, 3, 5, 7, 9], 4) == -1\n"
),
"bugs": [{"description": "lo starts at 1 instead of 0, skipping first element", "file": "solution.py", "line": 2}],
"bugs": [
{
"description": "lo starts at 1 instead of 0, skipping first element",
"file": "solution.py",
"line": 2,
}
],
"originally_failing_tests": ["test_find_first"],
"originally_passing_tests": ["test_find_middle", "test_find_last", "test_not_found"],
"originally_passing_tests": [
"test_find_middle",
"test_find_last",
"test_not_found",
],
},
{
"bug_report": "The clamp function doesn't work when value equals min_val.",
@@ -231,9 +259,21 @@ _EASY_TASKS: List[Dict[str, Any]] = [
"def test_at_max():\n"
" assert clamp(10, 0, 10) == 10\n"
),
"bugs": [{"description": "Actually no bug — this is a control task to test false positive rate", "file": "solution.py", "line": 0}],
"bugs": [
{
"description": "Actually no bug — this is a control task to test false positive rate",
"file": "solution.py",
"line": 0,
}
],
"originally_failing_tests": [],
"originally_passing_tests": ["test_below", "test_above", "test_within", "test_at_min", "test_at_max"],
"originally_passing_tests": [
"test_below",
"test_above",
"test_within",
"test_at_min",
"test_at_max",
],
},
{
"bug_report": "The count_vowels function misses uppercase vowels.",
@@ -264,7 +304,13 @@ _EASY_TASKS: List[Dict[str, Any]] = [
"def test_empty():\n"
" assert count_vowels('') == 0\n"
),
"bugs": [{"description": "Does not handle uppercase — should lowercase first", "file": "solution.py", "line": 3}],
"bugs": [
{
"description": "Does not handle uppercase — should lowercase first",
"file": "solution.py",
"line": 3,
}
],
"originally_failing_tests": ["test_uppercase", "test_mixed"],
"originally_passing_tests": ["test_lowercase", "test_empty"],
},
@@ -303,7 +349,13 @@ _EASY_TASKS: List[Dict[str, Any]] = [
"def test_two_elements():\n"
" assert max_profit([1, 5]) == 4\n"
),
"bugs": [{"description": "Subtraction order reversed: min_price - price should be price - min_price", "file": "solution.py", "line": 7}],
"bugs": [
{
"description": "Subtraction order reversed: min_price - price should be price - min_price",
"file": "solution.py",
"line": 7,
}
],
"originally_failing_tests": ["test_basic", "test_two_elements"],
"originally_passing_tests": ["test_declining", "test_single"],
},
@@ -333,7 +385,13 @@ _EASY_TASKS: List[Dict[str, Any]] = [
"def test_already_title():\n"
" assert title_case('Hello') == 'Hello'\n"
),
"bugs": [{"description": "Does not handle hyphenated words", "file": "solution.py", "line": 2}],
"bugs": [
{
"description": "Does not handle hyphenated words",
"file": "solution.py",
"line": 2,
}
],
"originally_failing_tests": ["test_hyphenated"],
"originally_passing_tests": ["test_simple", "test_already_title"],
},
@@ -364,7 +422,13 @@ _EASY_TASKS: List[Dict[str, Any]] = [
"def test_type_error():\n"
" assert safe_divide(10, 'a') == 0\n"
),
"bugs": [{"description": "Catches TypeError but not ZeroDivisionError", "file": "solution.py", "line": 4}],
"bugs": [
{
"description": "Catches TypeError but not ZeroDivisionError",
"file": "solution.py",
"line": 4,
}
],
"originally_failing_tests": ["test_zero_div", "test_zero_div_custom"],
"originally_passing_tests": ["test_normal", "test_type_error"],
},
@@ -438,8 +502,16 @@ _MEDIUM_TASKS: List[Dict[str, Any]] = [
" assert c.get(1) == 10\n"
),
"bugs": [
{"description": "order.pop() removes last (MRU) instead of order.pop(0) for LRU", "file": "solution.py", "line": 17},
{"description": "get/put don't remove old position before re-appending", "file": "solution.py", "line": 9},
{
"description": "order.pop() removes last (MRU) instead of order.pop(0) for LRU",
"file": "solution.py",
"line": 17,
},
{
"description": "get/put don't remove old position before re-appending",
"file": "solution.py",
"line": 9,
},
],
"originally_failing_tests": ["test_basic", "test_update"],
"originally_passing_tests": [],
@@ -488,7 +560,13 @@ _MEDIUM_TASKS: List[Dict[str, Any]] = [
"def test_duplicates():\n"
" assert merge_sorted([1, 2], [2, 3]) == [1, 2, 2, 3]\n"
),
"bugs": [{"description": "Duplicate result.extend(a[i:]) on line 13 causes extra elements", "file": "solution.py", "line": 13}],
"bugs": [
{
"description": "Duplicate result.extend(a[i:]) on line 13 causes extra elements",
"file": "solution.py",
"line": 13,
}
],
"originally_failing_tests": ["test_basic", "test_duplicates"],
"originally_passing_tests": ["test_one_empty", "test_both_empty"],
},
@@ -535,7 +613,13 @@ _MEDIUM_TASKS: List[Dict[str, Any]] = [
" b = [[4], [5], [6]]\n"
" assert matrix_multiply(a, b) == [[32]]\n"
),
"bugs": [{"description": "Wrong index: b[j][k] should be b[k][j]", "file": "solution.py", "line": 10}],
"bugs": [
{
"description": "Wrong index: b[j][k] should be b[k][j]",
"file": "solution.py",
"line": 10,
}
],
"originally_failing_tests": ["test_basic", "test_non_square"],
"originally_passing_tests": ["test_identity"],
},
@@ -582,7 +666,13 @@ _MEDIUM_TASKS: List[Dict[str, Any]] = [
"def test_no_repeats():\n"
" assert run_length_encode('abc') == 'a1b1c1'\n"
),
"bugs": [{"description": "In else branch, appends s[i] (next char) instead of s[i-1] (current char)", "file": "solution.py", "line": 10}],
"bugs": [
{
"description": "In else branch, appends s[i] (next char) instead of s[i-1] (current char)",
"file": "solution.py",
"line": 10,
}
],
"originally_failing_tests": ["test_basic", "test_no_repeats"],
"originally_passing_tests": ["test_single", "test_empty"],
},
@@ -627,9 +717,20 @@ _MEDIUM_TASKS: List[Dict[str, Any]] = [
"def test_extra_close():\n"
" assert validate_brackets(')') is False\n"
),
"bugs": [{"description": "Does not check that closing bracket matches the top of stack", "file": "solution.py", "line": 10}],
"bugs": [
{
"description": "Does not check that closing bracket matches the top of stack",
"file": "solution.py",
"line": 10,
}
],
"originally_failing_tests": ["test_mismatched"],
"originally_passing_tests": ["test_valid", "test_unclosed", "test_empty", "test_extra_close"],
"originally_passing_tests": [
"test_valid",
"test_unclosed",
"test_empty",
"test_extra_close",
],
},
{
"bug_report": "The group_by function loses entries when multiple items have the same key.",
@@ -660,7 +761,13 @@ _MEDIUM_TASKS: List[Dict[str, Any]] = [
"def test_empty():\n"
" assert group_by([], lambda x: x) == {}\n"
),
"bugs": [{"description": "Overwrites group with [item] instead of appending to list", "file": "solution.py", "line": 5}],
"bugs": [
{
"description": "Overwrites group with [item] instead of appending to list",
"file": "solution.py",
"line": 5,
}
],
"originally_failing_tests": ["test_basic", "test_strings"],
"originally_passing_tests": ["test_empty"],
},
@@ -697,8 +804,18 @@ _MEDIUM_TASKS: List[Dict[str, Any]] = [
"def test_window_too_large():\n"
" assert moving_average([1], 5) == []\n"
),
"bugs": [{"description": "Off-by-one in range: should be len(data) - window + 1", "file": "solution.py", "line": 5}],
"originally_failing_tests": ["test_basic", "test_window_equals_length", "test_window_one"],
"bugs": [
{
"description": "Off-by-one in range: should be len(data) - window + 1",
"file": "solution.py",
"line": 5,
}
],
"originally_failing_tests": [
"test_basic",
"test_window_equals_length",
"test_window_one",
],
"originally_passing_tests": ["test_window_too_large"],
},
{
@@ -763,7 +880,13 @@ _MEDIUM_TASKS: List[Dict[str, Any]] = [
" t = Trie()\n"
" assert t.search('hello') is False\n"
),
"bugs": [{"description": "search returns False instead of node.is_end", "file": "solution.py", "line": 24}],
"bugs": [
{
"description": "search returns False instead of node.is_end",
"file": "solution.py",
"line": 24,
}
],
"originally_failing_tests": ["test_insert_search"],
"originally_passing_tests": ["test_search_missing", "test_search_not_inserted"],
},
@@ -803,7 +926,13 @@ _MEDIUM_TASKS: List[Dict[str, Any]] = [
" expected = {'a': {'b': {'c': 3, 'd': 2}}}\n"
" assert deep_merge(base, over) == expected\n"
),
"bugs": [{"description": "Shallow merge: overwrites nested dicts instead of recursively merging", "file": "solution.py", "line": 4}],
"bugs": [
{
"description": "Shallow merge: overwrites nested dicts instead of recursively merging",
"file": "solution.py",
"line": 4,
}
],
"originally_failing_tests": ["test_nested", "test_deeply_nested"],
"originally_passing_tests": ["test_flat", "test_override"],
},
@@ -864,9 +993,18 @@ _MEDIUM_TASKS: List[Dict[str, Any]] = [
" debounced(2) # fires after wait\n"
" assert len(calls) == 2\n"
),
"bugs": [{"description": "Missing time check: calls func every time instead of only after wait_seconds", "file": "solution.py", "line": 9}],
"bugs": [
{
"description": "Missing time check: calls func every time instead of only after wait_seconds",
"file": "solution.py",
"line": 9,
}
],
"originally_failing_tests": ["test_debounce_suppresses"],
"originally_passing_tests": ["test_debounce_fires_first", "test_debounce_fires_after_wait"],
"originally_passing_tests": [
"test_debounce_fires_first",
"test_debounce_fires_after_wait",
],
},
]
@@ -908,16 +1046,19 @@ _HARD_TASKS: List[Dict[str, Any]] = [
" # 19.99 * 1.0875 * 0.95 = 20.6496...\n"
" assert financial_round(19.99, 0.0875, 0.05) == 20.65\n"
),
"bugs": [{"description": "Float arithmetic causes precision errors — should use Decimal", "file": "solution.py", "line": 2}],
"bugs": [
{
"description": "Float arithmetic causes precision errors — should use Decimal",
"file": "solution.py",
"line": 2,
}
],
"originally_failing_tests": ["test_precision", "test_rounding_boundary"],
"originally_passing_tests": ["test_basic", "test_no_discount"],
},
{
"bug_report": "The csv_parser doesn't handle quoted fields with commas inside.",
"buggy_code": (
"def parse_csv_line(line):\n"
" return line.split(',')\n"
),
"buggy_code": ("def parse_csv_line(line):\n return line.split(',')\n"),
"fixed_code": (
"import csv\nimport io\n\n"
"def parse_csv_line(line):\n"
@@ -935,7 +1076,13 @@ _HARD_TASKS: List[Dict[str, Any]] = [
"def test_empty_fields():\n"
" assert parse_csv_line('a,,c') == ['a', '', 'c']\n"
),
"bugs": [{"description": "Naive split(',') doesn't handle quoted fields", "file": "solution.py", "line": 2}],
"bugs": [
{
"description": "Naive split(',') doesn't handle quoted fields",
"file": "solution.py",
"line": 2,
}
],
"originally_failing_tests": ["test_quoted_comma"],
"originally_passing_tests": ["test_simple", "test_empty_fields"],
},
@@ -1003,7 +1150,13 @@ _HARD_TASKS: List[Dict[str, Any]] = [
" except RuntimeError:\n"
" pass\n"
),
"bugs": [{"description": "Success path doesn't return immediately and sleeps unnecessarily; runs all attempts", "file": "solution.py", "line": 8}],
"bugs": [
{
"description": "Success path doesn't return immediately and sleeps unnecessarily; runs all attempts",
"file": "solution.py",
"line": 8,
}
],
"originally_failing_tests": ["test_succeeds_first_try"],
"originally_passing_tests": ["test_exhausts_retries"],
},
@@ -1040,7 +1193,13 @@ _HARD_TASKS: List[Dict[str, Any]] = [
" result = parse_url_params('http://example.com?data=a=b')\n"
" assert result['data'] == 'a=b'\n"
),
"bugs": [{"description": "split('=') breaks on values containing '='; doesn't decode URL encoding", "file": "solution.py", "line": 7}],
"bugs": [
{
"description": "split('=') breaks on values containing '='; doesn't decode URL encoding",
"file": "solution.py",
"line": 7,
}
],
"originally_failing_tests": ["test_encoded_value", "test_value_with_equals"],
"originally_passing_tests": ["test_basic", "test_no_params"],
},
@@ -1093,8 +1252,17 @@ _HARD_TASKS: List[Dict[str, Any]] = [
" time.sleep(0.1)\n"
" assert rl.allow() is True\n"
),
"bugs": [{"description": "Never removes old requests; always appends even when over limit", "file": "solution.py", "line": 10}],
"originally_failing_tests": ["test_blocks_over_limit", "test_allows_after_window"],
"bugs": [
{
"description": "Never removes old requests; always appends even when over limit",
"file": "solution.py",
"line": 10,
}
],
"originally_failing_tests": [
"test_blocks_over_limit",
"test_allows_after_window",
],
"originally_passing_tests": ["test_allows_within_limit"],
},
{
@@ -1148,7 +1316,13 @@ _HARD_TASKS: List[Dict[str, Any]] = [
" greet(name='Alice', greeting='hey')\n"
" assert len(calls) == 2\n"
),
"bugs": [{"description": "Wrapper doesn't accept or cache kwargs", "file": "solution.py", "line": 3}],
"bugs": [
{
"description": "Wrapper doesn't accept or cache kwargs",
"file": "solution.py",
"line": 3,
}
],
"originally_failing_tests": ["test_kwargs", "test_different_kwargs"],
"originally_passing_tests": ["test_positional"],
},
@@ -1194,7 +1368,13 @@ _HARD_TASKS: List[Dict[str, Any]] = [
" ee = EventEmitter()\n"
" ee.emit('missing') # should not raise\n"
),
"bugs": [{"description": "on() overwrites listener instead of appending to list", "file": "solution.py", "line": 6}],
"bugs": [
{
"description": "on() overwrites listener instead of appending to list",
"file": "solution.py",
"line": 6,
}
],
"originally_failing_tests": ["test_multiple_listeners"],
"originally_passing_tests": ["test_single_listener", "test_no_listeners"],
},
@@ -1240,7 +1420,13 @@ _HARD_TASKS: List[Dict[str, Any]] = [
" result = json_flatten(obj)\n"
" assert result == {'a.b.c[0]': 1, 'a.b.c[1]': 2}\n"
),
"bugs": [{"description": "Does not handle list values, only dicts", "file": "solution.py", "line": 5}],
"bugs": [
{
"description": "Does not handle list values, only dicts",
"file": "solution.py",
"line": 5,
}
],
"originally_failing_tests": ["test_with_array", "test_deeply_nested"],
"originally_passing_tests": ["test_flat", "test_nested"],
},
@@ -1295,7 +1481,13 @@ _HARD_TASKS: List[Dict[str, Any]] = [
" assert len(results) == 2\n"
" assert all(r[0] == 'error' for r in results)\n"
),
"bugs": [{"description": "Silently swallows exceptions instead of recording them", "file": "solution.py", "line": 9}],
"bugs": [
{
"description": "Silently swallows exceptions instead of recording them",
"file": "solution.py",
"line": 9,
}
],
"originally_failing_tests": ["test_one_fails", "test_all_fail"],
"originally_passing_tests": ["test_all_succeed"],
},
@@ -1354,7 +1546,13 @@ _HARD_TASKS: List[Dict[str, Any]] = [
" with pytest.raises(ValueError, match='Cycle'):\n"
" topological_sort(graph)\n"
),
"bugs": [{"description": "No cycle detection — infinite recursion on cyclic graphs", "file": "solution.py", "line": 5}],
"bugs": [
{
"description": "No cycle detection — infinite recursion on cyclic graphs",
"file": "solution.py",
"line": 5,
}
],
"originally_failing_tests": ["test_cycle_detection"],
"originally_passing_tests": ["test_linear", "test_diamond"],
},
+14 -12
View File
@@ -200,7 +200,7 @@ _TASKS = [
{
"spec": "Implement a function that serializes and deserializes a binary tree. The tree node has val, left, right attributes.",
"signature": "def serialize(root) -> str\ndef deserialize(data: str) -> TreeNode",
"examples": 'serialize a tree [1,2,3,null,null,4,5] -> some string\ndeserialize that string -> same tree',
"examples": "serialize a tree [1,2,3,null,null,4,5] -> some string\ndeserialize that string -> same tree",
"test_cases": "class TreeNode:\n def __init__(self, val=0, left=None, right=None):\n self.val, self.left, self.right = val, left, right\nroot = TreeNode(1, TreeNode(2), TreeNode(3, TreeNode(4), TreeNode(5)))\ns = serialize(root)\nnew_root = deserialize(s)\nassert new_root.val == 1\nassert new_root.left.val == 2\nassert new_root.right.val == 3\nassert new_root.right.left.val == 4",
"reference": "class TreeNode:\n def __init__(self, val=0, left=None, right=None):\n self.val, self.left, self.right = val, left, right\ndef serialize(root):\n if not root: return 'null'\n return f'{root.val},{serialize(root.left)},{serialize(root.right)}'\ndef deserialize(data):\n vals = iter(data.split(','))\n def build():\n v = next(vals)\n if v == 'null': return None\n node = TreeNode(int(v))\n node.left = build()\n node.right = build()\n return node\n return build()",
},
@@ -260,17 +260,19 @@ class CodingTaskDataset(DatasetProvider):
signature=task["signature"],
examples=task["examples"],
)
self._records.append(EvalRecord(
record_id=f"coding-task-{idx}",
problem=prompt,
reference=task["reference"],
category="use-case",
subject="coding_task",
metadata={
"test_cases": task["test_cases"],
"signature": task["signature"],
},
))
self._records.append(
EvalRecord(
record_id=f"coding-task-{idx}",
problem=prompt,
reference=task["reference"],
category="use-case",
subject="coding_task",
metadata={
"test_cases": task["test_cases"],
"signature": task["signature"],
},
)
)
def iter_records(self) -> Iterable[EvalRecord]:
return iter(self._records)
+290 -55
View File
@@ -97,7 +97,12 @@ _EASY_TASKS: List[Dict[str, Any]] = [
"todos": "- Finalize Q4 roadmap draft\n- Send release notes for v3.2",
"messages": "- Slack from @eng-lead: 'v3.2 deployed to staging, all green'\n- Email from VP: 'Need roadmap by EOD Monday'",
"news_interests": "Fintech regulations, payment APIs",
"must_mention": ["stakeholder demo", "v3.2 staging", "Q4 roadmap", "EOD Monday deadline"],
"must_mention": [
"stakeholder demo",
"v3.2 staging",
"Q4 roadmap",
"EOD Monday deadline",
],
"priority_order": ["stakeholder demo", "v3.2 staging", "Q4 roadmap"],
},
{
@@ -119,7 +124,11 @@ _EASY_TASKS: List[Dict[str, Any]] = [
"todos": "- Fix crash in offline mode (Sentry issue #891)\n- Implement push notification deep links",
"messages": "- Slack from QA: 'Sentry #891 affecting 2% of users on iOS 17'\n- App Store Connect: 'v4.1 approved for release'",
"news_interests": "Swift, iOS",
"must_mention": ["Sentry #891 crash", "v4.1 approved", "push notification deep links"],
"must_mention": [
"Sentry #891 crash",
"v4.1 approved",
"push notification deep links",
],
"priority_order": ["Sentry #891 crash", "v4.1 approved"],
},
{
@@ -141,7 +150,11 @@ _EASY_TASKS: List[Dict[str, Any]] = [
"todos": "- Run regression suite for v5.0\n- Update test plan for payment module",
"messages": "- Jira: 'RELEASE-50: v5.0 release candidate tagged'\n- Slack from dev: 'Payment module refactored, old tests may break'",
"news_interests": "Test automation, Playwright",
"must_mention": ["v5.0 regression suite", "payment module tests", "release readiness"],
"must_mention": [
"v5.0 regression suite",
"payment module tests",
"release readiness",
],
"priority_order": ["v5.0 regression suite", "payment module tests"],
},
{
@@ -152,7 +165,11 @@ _EASY_TASKS: List[Dict[str, Any]] = [
"todos": "- Write post-mortem for Wednesday's outage\n- Update runbook for database failover",
"messages": "- Datadog: 'API p99 latency back to normal (12ms)'\n- Manager: 'Post-mortem draft needed before Monday'",
"news_interests": "SRE practices, observability",
"must_mention": ["post-mortem draft", "API latency normal", "capacity planning"],
"must_mention": [
"post-mortem draft",
"API latency normal",
"capacity planning",
],
"priority_order": ["post-mortem draft", "capacity planning"],
},
]
@@ -170,8 +187,18 @@ _MEDIUM_TASKS: List[Dict[str, Any]] = [
"todos": "- Finalize RFC for webhook retry redesign\n- Review perf regression in checkout API\n- Respond to security review findings",
"messages": "- Slack from @payments-lead: 'Webhook failures up 3x since Friday deploy'\n- Email from VP Eng: 'RFC deadline extended to Wednesday'\n- Jira INFRA-2847: 'Checkout API p99 jumped from 200ms to 800ms'",
"news_interests": "distributed systems, payment processing",
"must_mention": ["webhook failures 3x", "INFRA-2847 checkout latency", "RFC deadline Wednesday", "architecture review", "security review findings"],
"priority_order": ["webhook failures 3x", "INFRA-2847 checkout latency", "RFC deadline"],
"must_mention": [
"webhook failures 3x",
"INFRA-2847 checkout latency",
"RFC deadline Wednesday",
"architecture review",
"security review findings",
],
"priority_order": [
"webhook failures 3x",
"INFRA-2847 checkout latency",
"RFC deadline",
],
},
{
"role": "ML Engineer",
@@ -181,7 +208,13 @@ _MEDIUM_TASKS: List[Dict[str, Any]] = [
"todos": "- Investigate 5% CTR drop in product recommendations\n- Prepare training data for v3 model\n- Review feature store PR from junior engineer",
"messages": "- Slack from product: 'Recommendation CTR dropped on mobile — exec visibility'\n- Airflow alert: 'DAG user_features failed at 03:00, 2 retries exhausted'\n- Email from research: 'New embedding approach shows 12% improvement in offline eval'",
"news_interests": "recommendation systems, transformer architectures",
"must_mention": ["CTR drop", "Airflow DAG failure", "v3 model training data", "A/B test review", "new embedding approach"],
"must_mention": [
"CTR drop",
"Airflow DAG failure",
"v3 model training data",
"A/B test review",
"new embedding approach",
],
"priority_order": ["CTR drop", "Airflow DAG failure", "A/B test review"],
},
{
@@ -192,8 +225,18 @@ _MEDIUM_TASKS: List[Dict[str, Any]] = [
"todos": "- Complete service mesh POC documentation\n- Fix flaky integration tests in CI pipeline\n- Review IAM policy changes for staging env",
"messages": "- Slack from SRE: 'CI pipeline blocked — flaky test_payment_flow failing 40% of runs'\n- Email from CTO: 'Need vendor recommendation by Friday for budget approval'\n- GitHub: '3 new Dependabot PRs — 1 critical (lodash prototype pollution)'",
"news_interests": "service mesh, platform engineering",
"must_mention": ["CI flaky test", "vendor recommendation Friday", "lodash critical CVE", "migration planning", "IAM policy review"],
"priority_order": ["CI flaky test", "lodash critical CVE", "vendor recommendation"],
"must_mention": [
"CI flaky test",
"vendor recommendation Friday",
"lodash critical CVE",
"migration planning",
"IAM policy review",
],
"priority_order": [
"CI flaky test",
"lodash critical CVE",
"vendor recommendation",
],
},
{
"role": "Tech Lead",
@@ -203,8 +246,18 @@ _MEDIUM_TASKS: List[Dict[str, Any]] = [
"todos": "- Audit PHI access logs for September\n- Design API for lab results integration\n- Mentor new hire on HIPAA data handling",
"messages": "- Compliance team: 'September PHI audit due Oct 15 — need your section by Oct 10'\n- PM: 'Patient portal launch moved up to Nov 1'\n- QA: 'Found PHI leak in staging logs — ticket SEC-445'",
"news_interests": "healthcare APIs, HIPAA compliance",
"must_mention": ["PHI leak SEC-445", "audit due Oct 10", "patient portal Nov 1", "lab results API", "HIPAA compliance review"],
"priority_order": ["PHI leak SEC-445", "audit due Oct 10", "patient portal Nov 1"],
"must_mention": [
"PHI leak SEC-445",
"audit due Oct 10",
"patient portal Nov 1",
"lab results API",
"HIPAA compliance review",
],
"priority_order": [
"PHI leak SEC-445",
"audit due Oct 10",
"patient portal Nov 1",
],
},
{
"role": "Backend Lead",
@@ -214,8 +267,18 @@ _MEDIUM_TASKS: List[Dict[str, Any]] = [
"todos": "- Sign off on deploy for order management refactor\n- Review load test results (target: 10x normal traffic)\n- Update on-call rotation for holiday season",
"messages": "- Load test results: 'System handles 7x but OOM at 8x on order-service'\n- Slack from payments: 'New Stripe API version — migration needed by Dec 1'\n- PagerDuty: 'Memory leak alert on cart-service, auto-scaled to 8 pods'",
"news_interests": "e-commerce scaling, distributed systems",
"must_mention": ["OOM at 8x load", "cart-service memory leak", "Stripe migration Dec 1", "Black Friday capacity", "deploy review"],
"priority_order": ["OOM at 8x load", "cart-service memory leak", "Black Friday capacity"],
"must_mention": [
"OOM at 8x load",
"cart-service memory leak",
"Stripe migration Dec 1",
"Black Friday capacity",
"deploy review",
],
"priority_order": [
"OOM at 8x load",
"cart-service memory leak",
"Black Friday capacity",
],
},
{
"role": "Data Engineer",
@@ -225,8 +288,17 @@ _MEDIUM_TASKS: List[Dict[str, Any]] = [
"todos": "- Debug Spark job OOM on customer_360 pipeline\n- Implement CDC for new orders table\n- Review dbt model changes from analytics team",
"messages": "- Airflow: 'customer_360 DAG failed — Spark executor OOM'\n- Marketing: 'Attribution numbers look off by 15% since last week'\n- Slack from manager: 'Quarterly data infrastructure review next Tuesday, prep needed'",
"news_interests": "data engineering, Apache Spark",
"must_mention": ["Spark OOM customer_360", "attribution 15% discrepancy", "CDC orders table", "infrastructure review prep"],
"priority_order": ["Spark OOM", "attribution discrepancy", "infrastructure review"],
"must_mention": [
"Spark OOM customer_360",
"attribution 15% discrepancy",
"CDC orders table",
"infrastructure review prep",
],
"priority_order": [
"Spark OOM",
"attribution discrepancy",
"infrastructure review",
],
},
{
"role": "iOS Lead",
@@ -236,8 +308,18 @@ _MEDIUM_TASKS: List[Dict[str, Any]] = [
"todos": "- Fix crash in background refresh (Sentry #1204)\n- Prepare submission for iOS 18 compatibility update\n- Review accessibility audit findings",
"messages": "- App Store Connect: 'v6.2.1 rejected — missing privacy manifest for tracking'\n- Sentry: '#1204 crash rate increased to 0.8% (was 0.2%)'\n- Apple developer news: 'Privacy manifest deadline extended to Oct 31'",
"news_interests": "iOS development, SwiftUI",
"must_mention": ["v6.2.1 rejected", "privacy manifest deadline Oct 31", "Sentry #1204 crash", "accessibility audit", "SwiftUI migration"],
"priority_order": ["v6.2.1 rejected", "Sentry #1204 crash", "privacy manifest deadline"],
"must_mention": [
"v6.2.1 rejected",
"privacy manifest deadline Oct 31",
"Sentry #1204 crash",
"accessibility audit",
"SwiftUI migration",
],
"priority_order": [
"v6.2.1 rejected",
"Sentry #1204 crash",
"privacy manifest deadline",
],
},
{
"role": "VP of Engineering",
@@ -247,7 +329,13 @@ _MEDIUM_TASKS: List[Dict[str, Any]] = [
"todos": "- Finalize engineering section of board deck\n- Approve Q1 hiring plan (8 headcount)\n- Review SOC 2 Type II readiness",
"messages": "- CEO: 'Board meeting moved to Oct 20, deck due Oct 17'\n- HR: 'Director candidate strong — competing offer from FAANG, need to move fast'\n- CTO: 'SOC 2 auditor found 2 medium findings, remediation needed'",
"news_interests": "engineering leadership, scaling teams",
"must_mention": ["board deck due Oct 17", "director candidate competing offer", "SOC 2 findings", "Q1 hiring plan", "engineering all-hands"],
"must_mention": [
"board deck due Oct 17",
"director candidate competing offer",
"SOC 2 findings",
"Q1 hiring plan",
"engineering all-hands",
],
"priority_order": ["director candidate", "board deck Oct 17", "SOC 2 findings"],
},
{
@@ -258,8 +346,18 @@ _MEDIUM_TASKS: List[Dict[str, Any]] = [
"todos": "- Fix video player buffering on slow connections\n- Implement quiz auto-save feature\n- Update staging with latest migrations",
"messages": "- Support: '15 teachers reported video playback issues today'\n- PM: 'Quiz module launch is hard deadline — Nov 15'\n- Slack from DevOps: 'Staging DB needs migration, currently 3 behind'",
"news_interests": "video streaming, education technology",
"must_mention": ["video playback issues", "quiz module Nov 15", "staging migrations behind", "feature demo", "tech debt review"],
"priority_order": ["video playback issues", "quiz module Nov 15", "staging migrations"],
"must_mention": [
"video playback issues",
"quiz module Nov 15",
"staging migrations behind",
"feature demo",
"tech debt review",
],
"priority_order": [
"video playback issues",
"quiz module Nov 15",
"staging migrations",
],
},
{
"role": "Infrastructure Engineer",
@@ -269,8 +367,18 @@ _MEDIUM_TASKS: List[Dict[str, Any]] = [
"todos": "- Prepare DR test runbook for Q4 drill\n- Migrate legacy monitoring to Grafana\n- Review firewall rule change requests",
"messages": "- AWS: 'Scheduled maintenance us-east-1: Oct 25, 02:00-06:00 UTC'\n- Compliance: 'Annual DR test must complete before Dec 31'\n- Slack from SRE: 'Grafana migration 60% complete, need help with alert rules'",
"news_interests": "cloud infrastructure, disaster recovery",
"must_mention": ["AWS maintenance Oct 25", "DR test before Dec 31", "Grafana migration 60%", "firewall rule review", "change advisory board"],
"priority_order": ["AWS maintenance Oct 25", "DR test planning", "Grafana migration"],
"must_mention": [
"AWS maintenance Oct 25",
"DR test before Dec 31",
"Grafana migration 60%",
"firewall rule review",
"change advisory board",
],
"priority_order": [
"AWS maintenance Oct 25",
"DR test planning",
"Grafana migration",
],
},
]
@@ -287,8 +395,21 @@ _HARD_TASKS: List[Dict[str, Any]] = [
"todos": "- Complete post-mortem document for payment outage (5h downtime, $2.3M impact)\n- Implement circuit breaker for payment gateway\n- Review and merge hotfix for connection pool exhaustion\n- Update incident response playbook with lessons learned",
"messages": "- PagerDuty: 'CRITICAL — Payment success rate dropped to 45% at 02:14, recovered at 07:30'\n- CEO: 'Need customer communication plan by noon'\n- VP Eng: 'Board wants root cause by Wednesday'\n- Slack from payments team: 'Connection pool fix in PR #891, needs urgent review'\n- Datadog: '3 new anomaly alerts on order-service since 08:00'\n- Customer success: '47 enterprise customers opened tickets about Friday outage'",
"news_interests": "SRE, payment systems, incident management",
"must_mention": ["payment outage $2.3M", "PR #891 connection pool", "customer communication noon", "board root cause Wednesday", "new anomaly alerts", "47 enterprise tickets", "post-mortem"],
"priority_order": ["customer communication noon", "PR #891 connection pool", "anomaly alerts", "board root cause Wednesday"],
"must_mention": [
"payment outage $2.3M",
"PR #891 connection pool",
"customer communication noon",
"board root cause Wednesday",
"new anomaly alerts",
"47 enterprise tickets",
"post-mortem",
],
"priority_order": [
"customer communication noon",
"PR #891 connection pool",
"anomaly alerts",
"board root cause Wednesday",
],
},
{
"role": "CISO",
@@ -298,8 +419,21 @@ _HARD_TASKS: List[Dict[str, Any]] = [
"todos": "- Coordinate response to detected data exfiltration attempt\n- Prepare board briefing on security incident\n- Review forensic analysis from IR team\n- Determine notification obligations under GDPR/CCPA",
"messages": "- SOC: 'Detected unusual data transfer — 2.3GB to external IP from staging-db-02 at 01:47'\n- IR team: 'Attack vector identified: compromised service account via phished credentials'\n- Legal: 'If PII confirmed, 72h GDPR notification clock starts now'\n- CEO: 'No external communication until legal approves messaging'\n- AWS GuardDuty: 'UnauthorizedAccess:IAMUser/MaliciousIPCaller on staging account'\n- HR: 'Phished employee identified, account disabled'",
"news_interests": "cybersecurity, data breach response, compliance",
"must_mention": ["2.3GB exfiltration", "phished credentials", "GDPR 72h notification", "board briefing", "legal messaging approval", "GuardDuty alert", "compromised service account"],
"priority_order": ["exfiltration incident", "GDPR notification", "board briefing", "legal approval"],
"must_mention": [
"2.3GB exfiltration",
"phished credentials",
"GDPR 72h notification",
"board briefing",
"legal messaging approval",
"GuardDuty alert",
"compromised service account",
],
"priority_order": [
"exfiltration incident",
"GDPR notification",
"board briefing",
"legal approval",
],
},
{
"role": "Engineering Director",
@@ -309,8 +443,20 @@ _HARD_TASKS: List[Dict[str, Any]] = [
"todos": "- Resolve broken Salesforce integration (blocking $4M deal)\n- Prepare Q1 roadmap proposal for exec review\n- Address team morale issues in Platform team (3 resignations in 2 months)\n- Review compensation adjustments for retention",
"messages": "- Salesforce partner: 'API breaking change in v58 — our integration returns 400s since midnight'\n- Sales VP: '$4M Acme deal closes Friday, integration must work by Thursday EOD'\n- HR: 'Exit interview themes: lack of growth, below-market comp'\n- CTO: 'Q1 roadmap proposal due Friday, include AI strategy'\n- Slack from platform-lead: 'Two more engineers gave notice yesterday'\n- GitHub: 'Salesforce integration fix in draft PR, needs API key rotation'",
"news_interests": "engineering management, API integrations",
"must_mention": ["Salesforce API breaking change", "$4M deal Thursday deadline", "platform team resignations", "Q1 roadmap Friday", "compensation adjustments", "API key rotation"],
"priority_order": ["Salesforce integration", "$4M deal deadline", "platform team resignations", "Q1 roadmap"],
"must_mention": [
"Salesforce API breaking change",
"$4M deal Thursday deadline",
"platform team resignations",
"Q1 roadmap Friday",
"compensation adjustments",
"API key rotation",
],
"priority_order": [
"Salesforce integration",
"$4M deal deadline",
"platform team resignations",
"Q1 roadmap",
],
},
{
"role": "Lead SRE",
@@ -320,8 +466,21 @@ _HARD_TASKS: List[Dict[str, Any]] = [
"todos": "- Coordinate failover to backup CDN\n- Estimate customer impact (MAU affected, SLA credits)\n- Prepare status page communications\n- Document timeline for incident report",
"messages": "- CDN provider: 'Global edge outage affecting EU and APAC, ETA 4h for full recovery'\n- Monitoring: 'Video start failures at 78% (normal: 2%), 3.2M users affected'\n- Customer success: 'ESPN, Netflix, Disney+ all escalating — SLA breach imminent'\n- CEO: 'Status page must update every 30 minutes'\n- Finance: 'Estimated SLA credits: $850K if not resolved by noon'\n- Engineering: 'Backup CDN can handle 60% of traffic, degraded quality'",
"news_interests": "CDN, video streaming, incident management",
"must_mention": ["CDN outage EU APAC", "3.2M users affected", "SLA credits $850K", "backup CDN 60%", "status page updates 30min", "ETA 4h recovery", "ESPN Netflix Disney escalation"],
"priority_order": ["CDN failover", "status page updates", "customer escalations", "SLA credit estimate"],
"must_mention": [
"CDN outage EU APAC",
"3.2M users affected",
"SLA credits $850K",
"backup CDN 60%",
"status page updates 30min",
"ETA 4h recovery",
"ESPN Netflix Disney escalation",
],
"priority_order": [
"CDN failover",
"status page updates",
"customer escalations",
"SLA credit estimate",
],
},
{
"role": "CTO",
@@ -331,8 +490,20 @@ _HARD_TASKS: List[Dict[str, Any]] = [
"todos": "- Address GPU cost overrun ($180K over budget this quarter)\n- Prepare technical due diligence materials for Series B\n- Evaluate build vs buy for vector database\n- Respond to acquisition interest from BigTech",
"messages": "- CFO: 'GPU costs 40% over budget — need cost reduction plan by Monday'\n- Lead investor: 'Series B term sheet ready, need tech DD materials by next Friday'\n- BigTech BD: 'Acquisition conversation request — CEO says explore quietly'\n- AWS: 'Reserved instance commitment expires Nov 30'\n- ML team: 'New model achieves SOTA on benchmark but requires 2x GPU'\n- VP Eng: 'Team burnout is real — 60h average weeks for past month'",
"news_interests": "AI infrastructure, venture capital, GPU optimization",
"must_mention": ["GPU cost overrun $180K", "Series B DD materials", "acquisition interest", "RI expiration Nov 30", "SOTA model 2x GPU", "team burnout 60h weeks"],
"priority_order": ["GPU cost reduction Monday", "Series B DD", "acquisition exploration", "team burnout"],
"must_mention": [
"GPU cost overrun $180K",
"Series B DD materials",
"acquisition interest",
"RI expiration Nov 30",
"SOTA model 2x GPU",
"team burnout 60h weeks",
],
"priority_order": [
"GPU cost reduction Monday",
"Series B DD",
"acquisition exploration",
"team burnout",
],
},
{
"role": "Principal Engineer",
@@ -342,8 +513,20 @@ _HARD_TASKS: List[Dict[str, Any]] = [
"todos": "- Investigate 50ms latency spike in order matching engine\n- Review SEC Rule 15c3-5 compliance for new algorithm\n- Design circuit breaker for third-party market data feeds\n- Prepare tech brief on microsecond timestamping upgrade",
"messages": "- Trading desk: 'Order matching 50ms slower since Friday deploy — losing $40K/day in slippage'\n- Compliance: 'SEC audit next month, need algorithm documentation by Nov 20'\n- Vendor: 'Market data feed v3 deprecating Dec 15, migration required'\n- CTO: 'Board approved microsecond upgrade budget, need timeline'\n- Monitoring: 'Memory leak detected in risk engine — growing 50MB/hour'\n- DevOps: 'Friday deploy rollback available but needs 2h maintenance window'",
"news_interests": "low-latency systems, financial technology",
"must_mention": ["order matching latency $40K/day", "SEC audit Nov 20", "market data feed migration Dec 15", "memory leak risk engine", "deploy rollback option", "microsecond upgrade timeline"],
"priority_order": ["order matching latency", "memory leak risk engine", "SEC audit documentation", "market data migration"],
"must_mention": [
"order matching latency $40K/day",
"SEC audit Nov 20",
"market data feed migration Dec 15",
"memory leak risk engine",
"deploy rollback option",
"microsecond upgrade timeline",
],
"priority_order": [
"order matching latency",
"memory leak risk engine",
"SEC audit documentation",
"market data migration",
],
},
{
"role": "VP Platform",
@@ -353,8 +536,21 @@ _HARD_TASKS: List[Dict[str, Any]] = [
"todos": "- Coordinate DDoS mitigation (attack started 22:00 last night)\n- Prepare customer impact assessment\n- Evaluate WAF vendor proposals (Cloudflare vs Akamai)\n- Draft risk committee presentation",
"messages": "- NOC: 'DDoS attack ongoing — 340Gbps, Cloudflare mitigating 95% but 5% getting through'\n- Fortune 500 client: 'Our SLA guarantees 99.99%, currently at 99.2% for November'\n- Legal: 'Three customers sent breach of contract notices'\n- Cloudflare TAM: 'Can upgrade to Enterprise+ with 1Tbps mitigation, $50K/month'\n- CFO: 'Board wants cyber insurance claim assessment'\n- CISO: 'Attack signature matches known botnet, FBI notified'",
"news_interests": "DDoS mitigation, cloud security, SaaS operations",
"must_mention": ["DDoS 340Gbps", "Fortune 500 SLA breach", "breach of contract notices", "Cloudflare upgrade $50K", "FBI notified", "cyber insurance claim", "risk committee prep"],
"priority_order": ["DDoS mitigation", "Fortune 500 SLA", "breach of contract", "cyber insurance"],
"must_mention": [
"DDoS 340Gbps",
"Fortune 500 SLA breach",
"breach of contract notices",
"Cloudflare upgrade $50K",
"FBI notified",
"cyber insurance claim",
"risk committee prep",
],
"priority_order": [
"DDoS mitigation",
"Fortune 500 SLA",
"breach of contract",
"cyber insurance",
],
},
{
"role": "Head of ML",
@@ -364,8 +560,20 @@ _HARD_TASKS: List[Dict[str, Any]] = [
"todos": "- Investigate false negative in pedestrian detection model\n- Prepare safety data package for NHTSA\n- Review validation results for model v4.2\n- Coordinate OTA update for affected vehicles",
"messages": "- Safety team: 'Vehicle #4892 near-miss incident — pedestrian not detected at dusk'\n- NHTSA: 'Requesting incident data within 48 hours per Standing General Order'\n- ML validation: 'v4.2 shows 0.3% regression in low-light pedestrian detection'\n- Legal: 'Do not communicate externally until safety review complete'\n- Fleet ops: '12,000 vehicles on v4.1, OTA update requires 72h rollout'\n- PR: 'Reuters reporter asking about safety incident reports'",
"news_interests": "autonomous vehicles, ML safety, computer vision",
"must_mention": ["pedestrian detection near-miss", "NHTSA 48h data request", "v4.2 low-light regression", "12000 vehicles OTA", "Reuters inquiry", "legal communication hold"],
"priority_order": ["NHTSA data request", "pedestrian detection investigation", "OTA update planning", "legal hold"],
"must_mention": [
"pedestrian detection near-miss",
"NHTSA 48h data request",
"v4.2 low-light regression",
"12000 vehicles OTA",
"Reuters inquiry",
"legal communication hold",
],
"priority_order": [
"NHTSA data request",
"pedestrian detection investigation",
"OTA update planning",
"legal hold",
],
},
{
"role": "VP Engineering",
@@ -375,8 +583,21 @@ _HARD_TASKS: List[Dict[str, Any]] = [
"todos": "- Resolve core banking system freeze (customer transactions blocked)\n- Prepare OCC examination response materials\n- Coordinate Oracle emergency support (database deadlock)\n- Assess customer impact and communication plan",
"messages": "- NOC: 'Core banking frozen at 06:15 — Oracle RAC deadlock, 450K transactions queued'\n- OCC examiner: 'Examination starts Monday, technology resilience focus area'\n- Customer ops: '2,300 customers called about failed transactions'\n- Oracle support: 'Severity 1 case opened, engineer assigned ETA 2h'\n- CFO: 'Regulatory fine risk if transactions not processed by EOD'\n- CISO: 'No security incident indicated, pure technology failure'",
"news_interests": "banking technology, database systems, regulatory compliance",
"must_mention": ["core banking freeze", "450K queued transactions", "OCC examination Monday", "Oracle RAC deadlock", "2300 customer calls", "regulatory fine risk EOD", "Oracle engineer ETA 2h"],
"priority_order": ["Oracle deadlock resolution", "regulatory fine risk", "customer communication", "OCC examination prep"],
"must_mention": [
"core banking freeze",
"450K queued transactions",
"OCC examination Monday",
"Oracle RAC deadlock",
"2300 customer calls",
"regulatory fine risk EOD",
"Oracle engineer ETA 2h",
],
"priority_order": [
"Oracle deadlock resolution",
"regulatory fine risk",
"customer communication",
"OCC examination prep",
],
},
{
"role": "Director of Engineering",
@@ -386,8 +607,20 @@ _HARD_TASKS: List[Dict[str, Any]] = [
"todos": "- Finalize FDA 510(k) pre-submission package\n- Review clinical trial results for diagnostic AI accuracy\n- Address model drift detected in production deployment\n- Prepare for Monday's IRB ethics review",
"messages": "- FDA liaison: 'Pre-submission meeting confirmed Nov 21, final materials due Nov 18'\n- Clinical team: 'Trial results: 94.2% sensitivity, 91.8% specificity — below 95% target for sensitivity'\n- ML ops: 'Model drift alert — AUC dropped from 0.96 to 0.89 over 30 days'\n- Legal: 'IRB flagged consent form language, revision needed before Monday'\n- Research lead: 'Promising approach to improve sensitivity — needs 2 weeks'\n- Quality: 'CAPA-2847 open: production model version mismatch between regions'",
"news_interests": "medical AI, FDA regulation, clinical trials",
"must_mention": ["FDA materials due Nov 18", "sensitivity below target 94.2%", "model drift AUC 0.89", "IRB consent revision Monday", "CAPA-2847 version mismatch", "2-week sensitivity improvement"],
"priority_order": ["model drift", "IRB consent revision", "FDA materials", "sensitivity improvement decision"],
"must_mention": [
"FDA materials due Nov 18",
"sensitivity below target 94.2%",
"model drift AUC 0.89",
"IRB consent revision Monday",
"CAPA-2847 version mismatch",
"2-week sensitivity improvement",
],
"priority_order": [
"model drift",
"IRB consent revision",
"FDA materials",
"sensitivity improvement decision",
],
},
]
@@ -429,19 +662,21 @@ class DailyDigestDataset(DatasetProvider):
news_interests=task["news_interests"],
)
self._records.append(EvalRecord(
record_id=f"daily-digest-{idx:03d}",
problem=prompt,
reference="; ".join(task["must_mention"]),
category="agentic",
subject=diff,
metadata={
"role": task["role"],
"company": task["company"],
"must_mention": task["must_mention"],
"priority_order": task["priority_order"],
},
))
self._records.append(
EvalRecord(
record_id=f"daily-digest-{idx:03d}",
problem=prompt,
reference="; ".join(task["must_mention"]),
category="agentic",
subject=diff,
metadata={
"role": task["role"],
"company": task["company"],
"must_mention": task["must_mention"],
"priority_order": task["priority_order"],
},
)
)
def iter_records(self) -> Iterable[EvalRecord]:
return iter(self._records)
+10 -12
View File
@@ -110,15 +110,16 @@ class DeepPlanningDataset(DatasetProvider):
from datasets import load_dataset
except ImportError:
raise ImportError(
"datasets required for DeepPlanning. "
"Install with: pip install datasets"
"datasets required for DeepPlanning. Install with: pip install datasets"
)
logger.info("Downloading Qwen/DeepPlanning from HuggingFace...")
# Loading triggers the download even though we don't use the result
load_dataset("Qwen/DeepPlanning", split="train")
def _extract_cases(
self, tar_path: Path, level: int,
self,
tar_path: Path,
level: int,
) -> List[EvalRecord]:
"""Extract shopping cases from a tar.gz archive."""
records: List[EvalRecord] = []
@@ -152,7 +153,10 @@ class DeepPlanningDataset(DatasetProvider):
products_text = pf.read().decode("utf-8").strip()
rec = self._case_to_record(
data, level, case_name, products_text,
data,
level,
case_name,
products_text,
)
if rec:
records.append(rec)
@@ -185,15 +189,9 @@ class DeepPlanningDataset(DatasetProvider):
reference = "; ".join(ref_parts) if ref_parts else ""
# Count constraints
n_constraints = sum(
len(m.get("features", [])) for m in meta_info
)
n_constraints = sum(len(m.get("features", [])) for m in meta_info)
difficulty = (
"easy" if level == 1
else "medium" if level == 2
else "hard"
)
difficulty = "easy" if level == 1 else "medium" if level == 2 else "hard"
# Build product catalog section
catalog_section = ""
File diff suppressed because it is too large Load Diff
+15 -16
View File
@@ -307,23 +307,22 @@ class EmailTriageDataset(DatasetProvider):
date=email["date"],
body=email["body"],
)
reference = (
f"urgency: {email['urgency']}\n"
f"category: {email['category']}"
reference = f"urgency: {email['urgency']}\ncategory: {email['category']}"
self._records.append(
EvalRecord(
record_id=f"email-triage-{idx}",
problem=prompt,
reference=reference,
category="use-case",
subject="email_triage",
metadata={
"urgency": email["urgency"],
"category": email["category"],
"sender": email["sender"],
"subject_line": email["subject"],
},
)
)
self._records.append(EvalRecord(
record_id=f"email-triage-{idx}",
problem=prompt,
reference=reference,
category="use-case",
subject="email_triage",
metadata={
"urgency": email["urgency"],
"category": email["category"],
"sender": email["sender"],
"subject_line": email["subject"],
},
))
def iter_records(self) -> Iterable[EvalRecord]:
return iter(self._records)

Some files were not shown because too many files have changed in this diff Show More