Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
139 changes: 109 additions & 30 deletions agents/runner/agent_runner.py
Original file line number Diff line number Diff line change
@@ -1,53 +1,132 @@
"""Mock agent runner — simulates agent execution without real API calls."""
"""
Mock agent runner — simulates agent execution without real API calls.
"""

from pathlib import Path
import json
from typing import Optional

import time
import uuid
from pathlib import Path
from typing import Optional, Any

TRACES_DIR = Path(__file__).parent.parent / "traces"

# Mock Tools (no real APIs)
def mock_fetch_emails(limit: int = 5) -> dict:
return {
"emails": [
{"from": "alice@example.com", "subject": "Q3 Budget Review", "snippet": "Please review the attached budget..."},
{"from": "bob@corp.com", "subject": "Team standup notes", "snippet": "Action items from today's standup..."},
{"from": "carol@vendor.com", "subject": "Invoice #4821", "snippet": "Please find attached invoice..."},
][:limit]
}

def mock_summarize_text(text: str) -> dict:
summary = text[:120].strip() + ("..." if len(text) > 120 else "")
return {"summary": summary, "word_count": len(text.split())}

# Tool dispatch table
TOOL_DISPATCH = {
"fetch_emails": mock_fetch_emails,
"summarize_text": mock_summarize_text,
}

# email_summarizer

def email_chain(user_input: dict, prev: dict) -> list[tuple[str, dict]]:
return [
("fetch_emails", {"limit": user_input.get("limit", 5)}),
("summarize_text", {
"text": " | ".join(
e["snippet"] for e in prev.get("fetch_emails", {}).get("emails", [])
)
}),
]

AGENT_CHAINS = {
"email_summarizer": email_chain,
}

# Final Output Builder
def build_final_output(agent_id: str, outputs: dict) -> str:
if agent_id == "email_summarizer":
emails = outputs["fetch_emails"]["emails"]
summary = outputs["summarize_text"]["summary"]
return f"Summarized {len(emails)} emails: {summary}"
return f"Agent '{agent_id}' executed successfully."

# Runner
def run_agent(agent_id: str, user_input: dict) -> dict:
"""Simulate running an agent with the given input.

TODO: Implement full simulation flow:
1. Load agent manifest
2. Identify required tools
3. Call mock tools (return canned responses)
4. Build trace steps
5. Generate final output
6. Save run history

For MVP skeleton, return a placeholder response.
"""
return {
run_id = str(uuid.uuid4())[:8]
start = time.time()
trace = []
outputs = {}

if agent_id not in AGENT_CHAINS:
return {
"agent_id": agent_id,
"status": "error",
"message": f"Agent '{agent_id}' not found.",
"input_received": user_input,
}

chain_builder = AGENT_CHAINS[agent_id]
tool_steps = chain_builder(user_input, {})

for i, (tool_id, _) in enumerate(tool_steps):
refreshed_steps = chain_builder(user_input, outputs)
_, kwargs = refreshed_steps[i]

tool_fn = TOOL_DISPATCH[tool_id]
t0 = time.time()
output = tool_fn(**kwargs)
latency = int((time.time() - t0) * 1000) + 15

outputs[tool_id] = output
trace.append({
"step": i + 1,
"tool_id": tool_id,
"input": kwargs,
"output": output,
"status": "success",
"latency_ms": latency,
})

final_output = build_final_output(agent_id, outputs)
duration = int((time.time() - start) * 1000)

result = {
"run_id": run_id,
"agent_id": agent_id,
"status": "simulated",
"message": "TODO: Implement agent runner simulation",
"input_received": user_input,
"user_input": user_input,
"final_output": final_output,
"status": "success",
"trace": trace,
"duration_ms": duration,
}

_save_trace(result)
return result

# Trace Loader
def get_trace_for_agent(agent_id: str) -> Optional[dict]:
"""Load a pre-built mock trace for an agent if one exists.

TODO: Support loading traces by run_id
TODO: Support listing all traces for an agent
"""
trace_map = {
"email_summarizer": "email_summarizer_trace.json",
"github_issue_triage": "github_issue_triage_trace.json",
"meeting_notes": "meeting_notes_trace.json",
}

trace_file = trace_map.get(agent_id)
if not trace_file:
return None

trace_path = TRACES_DIR / trace_file
if not trace_path.exists():
path = TRACES_DIR / trace_file
if not path.exists():
return None

with open(trace_path, encoding="utf-8") as f:
with open(path, encoding="utf-8") as f:
return json.load(f)

# Save Trace
def _save_trace(result: dict) -> None:
TRACES_DIR.mkdir(parents=True, exist_ok=True)
path = TRACES_DIR / f"{result['agent_id']}_{result['run_id']}.json"
path.write_text(json.dumps(result, indent=2), encoding="utf-8")

5 changes: 3 additions & 2 deletions backend/app/routers/runs.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,11 @@ def simulate_run(agent_id: str, request: RunRequest):
result = run_agent(agent_id, request.input)
trace = get_trace_for_agent(agent_id)

# Improved response with more detailed status and message based on trace and result
return RunResponse(
agent_id=agent_id,
status=result.get("status", "simulated"),
message=result.get("message", ""),
status=result.get("status") or (trace.get("status") if trace else "error"),
message=result.get("final_output", result.get("message", "")),
trace=trace,
output=trace.get("final_output") if trace else None,
)
Expand Down
197 changes: 148 additions & 49 deletions frontend/src/components/AgentCard.css
Original file line number Diff line number Diff line change
@@ -1,70 +1,169 @@
/*
Improved marketplace card — all fields visible,
"View Details" CTA, consistent with App palette
*/

.agent-card {
display: block;
background: white;
border: 1px solid #e5e7eb;
border-radius: 12px;
padding: 1.25rem;
color: inherit;
text-decoration: none;
transition: box-shadow 0.2s, border-color 0.2s;
display: flex;
flex-direction: column;
gap: 0.75rem;
background: #ffffff;
border: 1px solid #e5e7eb;
border-radius: 12px;
padding: 1.25rem;
transition: box-shadow 0.2s ease, border-color 0.2s ease;
}

.agent-card:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
border-color: #4f46e5;
text-decoration: none;
box-shadow: 0 4px 16px rgba(79, 70, 229, 0.1);
border-color: #4f46e5;
}

.agent-card__header {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 0.5rem;
}

.agent-card__name {
font-size: 1.05rem;
font-weight: 600;
color: #1a1a2e;
line-height: 1.3;
margin: 0;
}

.agent-card__category {
flex-shrink: 0;
font-size: 0.7rem;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.04em;
background: #eef2ff;
color: #4f46e5;
padding: 0.2rem 0.55rem;
border-radius: 4px;
}

.agent-card__description {
font-size: 0.875rem;
color: #6b7280;
line-height: 1.5;
margin: 0;
display: -webkit-box;
-webkit-line-clamp: 3;
line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}

.agent-card__tags {
display: flex;
flex-wrap: wrap;
gap: 0.35rem;
}

.tag {
font-size: 0.7rem;
color: #6366f1;
background: #f0f0ff;
border: 1px solid #e0e0ff;
padding: 0.15rem 0.45rem;
border-radius: 99px;
}

.agent-card__tools {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.35rem;
}

.tools-label {
font-size: 0.72rem;
font-weight: 600;
color: #9ca3af;
text-transform: uppercase;
letter-spacing: 0.04em;
margin-right: 0.1rem;
}

.tool-chip {
font-size: 0.7rem;
background: #f3f4f6;
color: #374151;
border: 1px solid #e5e7eb;
padding: 0.15rem 0.5rem;
border-radius: 4px;
font-family: ui-monospace, 'Cascadia Code', monospace;
}

.agent-card__footer {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: auto;
padding-top: 0.5rem;
border-top: 1px solid #f3f4f6;
}

.agent-card-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 0.5rem;
.agent-card__stats {
display: flex;
align-items: center;
gap: 0.9rem;
}

.agent-card-header h3 {
font-size: 1.1rem;
color: #1a1a2e;
.agent-card__rating {
display: flex;
align-items: center;
gap: 0.15rem;
}

.category {
font-size: 0.75rem;
background: #eef2ff;
color: #4f46e5;
padding: 0.2rem 0.5rem;
border-radius: 4px;
.star {
font-size: 0.85rem;
line-height: 1;
}

.description {
font-size: 0.9rem;
color: #6b7280;
margin-bottom: 0.75rem;
line-height: 1.4;
.star--filled {
color: #f59e0b;
}

.agent-card-footer {
display: flex;
justify-content: space-between;
font-size: 0.85rem;
color: #9ca3af;
margin-bottom: 0.5rem;
.star--empty {
color: #d1d5db;
}

.rating {
color: #f59e0b;
font-weight: 600;
.rating-value {
font-size: 0.8rem;
font-weight: 600;
color: #374151;
margin-left: 0.2rem;
}

.tools {
display: flex;
flex-wrap: wrap;
gap: 0.35rem;
.agent-card__downloads {
font-size: 0.8rem;
color: #9ca3af;
}

.tool-tag {
font-size: 0.7rem;
background: #f3f4f6;
color: #4b5563;
padding: 0.15rem 0.4rem;
border-radius: 3px;
.agent-card__cta {
font-size: 0.8rem;
font-weight: 600;
color: #4f46e5;
background: transparent;
border: 1.5px solid #4f46e5;
border-radius: 6px;
padding: 0.35rem 0.85rem;
cursor: pointer;
transition: background 0.15s ease, color 0.15s ease;
white-space: nowrap;
}

.agent-card__cta:hover {
background: #4f46e5;
color: #ffffff;
}

.agent-card__cta:focus-visible {
outline: 2px solid #4f46e5;
outline-offset: 2px;
}
Loading