mirror of
https://github.com/open-jarvis/OpenJarvis.git
synced 2026-07-31 03:12:16 +00:00
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:
co-authored by
Claude Opus 4.6
parent
bce8a2fe12
commit
869d9256fd
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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",
|
||||
]
|
||||
|
||||
@@ -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},
|
||||
|
||||
@@ -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 = ""
|
||||
|
||||
@@ -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", "")
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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] = {}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
),
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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.")
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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]")
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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
@@ -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)}")
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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]"
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -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
@@ -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")
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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.")
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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]")
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
@@ -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
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"],
|
||||
},
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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
Reference in New Issue
Block a user