From c8a5774f278833737509b1ad5b8bdfd1338d9044 Mon Sep 17 00:00:00 2001 From: Vu Chau Date: Wed, 10 Jun 2026 12:43:59 -0500 Subject: [PATCH 1/3] Improve mock agent runner design --- agents/runner/agent_runner.py | 139 ++++++++++++++++++++++++++-------- 1 file changed, 109 insertions(+), 30 deletions(-) diff --git a/agents/runner/agent_runner.py b/agents/runner/agent_runner.py index e194c31..47194fa 100644 --- a/agents/runner/agent_runner.py +++ b/agents/runner/agent_runner.py @@ -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") + From 310c39858c2810aa68694a5fe63b6897be3d1f48 Mon Sep 17 00:00:00 2001 From: Vu Chau Date: Thu, 11 Jun 2026 08:34:37 -0500 Subject: [PATCH 2/3] Improve frontend marketplace card --- frontend/src/components/AgentCard.css | 197 +++++++++++++++++++------- frontend/src/components/AgentCard.tsx | 95 ++++++++++--- frontend/src/pages/AgentsPage.tsx | 101 ++++++++++--- frontend/src/vite-env.d.ts | 1 + 4 files changed, 307 insertions(+), 87 deletions(-) create mode 100644 frontend/src/vite-env.d.ts diff --git a/frontend/src/components/AgentCard.css b/frontend/src/components/AgentCard.css index ca3a56f..a676642 100644 --- a/frontend/src/components/AgentCard.css +++ b/frontend/src/components/AgentCard.css @@ -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; +} \ No newline at end of file diff --git a/frontend/src/components/AgentCard.tsx b/frontend/src/components/AgentCard.tsx index 0b321dd..24e0ada 100644 --- a/frontend/src/components/AgentCard.tsx +++ b/frontend/src/components/AgentCard.tsx @@ -1,4 +1,4 @@ -import { Link } from 'react-router-dom' +import { useNavigate } from 'react-router-dom' import type { AgentSummary } from '../api/client' import './AgentCard.css' @@ -6,31 +6,86 @@ interface Props { agent: AgentSummary } -/** - * AgentCard — displays a single agent in the browse grid. - * - * TODO: Add install count, trending badge, category chip styling - * TODO: Add star rating display component +/* + * Switched from wrapping to
+ useNavigate() on the button — this is more accessible. + + * Shows: name, description, category, rating, downloads, tags, tools required. + * Includes a "View Details" button (does not rely on the whole card being clickable). */ + export default function AgentCard({ agent }: Props) { + const navigate = useNavigate() + + const handleViewDetails = (e: React.MouseEvent) => { + e.preventDefault() + navigate(`/agents/${agent.id}`) + } + + // Render star rating visually (filled + empty stars out of 5) + const renderStars = (rating: number) => { + const filled = Math.round(rating) + return Array.from({ length: 5 }, (_, i) => ( + + ★ + + )) + } + return ( - -
-

{agent.name}

- {agent.category} -
-

{agent.description}

-
- ★ {agent.rating.toFixed(1)} - {agent.downloads.toLocaleString()} downloads +
+ + {/* ── Header: name + category badge ── */} +
+

{agent.name}

+ {agent.category}
- {agent.tools_required.length > 0 && ( -
+ + {/* ── Description ── */} +

{agent.description}

+ + {/* ── Tags ── */} + {agent.tags && agent.tags.length > 0 && ( +
+ {agent.tags.map((tag) => ( + + #{tag} + + ))} +
+ )} + + {/* ── Tools required ── */} + {agent.tools_required && agent.tools_required.length > 0 && ( +
+ Tools: {agent.tools_required.map((tool) => ( - {tool} + + {tool} + ))}
)} - + + {/* ── Footer: rating + downloads + CTA ── */} +
+
+ + {renderStars(agent.rating)} + {agent.rating.toFixed(1)} + + + ↓ {agent.downloads.toLocaleString()} + +
+ +
+ +
) -} +} \ No newline at end of file diff --git a/frontend/src/pages/AgentsPage.tsx b/frontend/src/pages/AgentsPage.tsx index ad39e3b..1a8ffdc 100644 --- a/frontend/src/pages/AgentsPage.tsx +++ b/frontend/src/pages/AgentsPage.tsx @@ -2,44 +2,109 @@ import { useEffect, useState } from 'react' import AgentCard from '../components/AgentCard' import { fetchAgents, type AgentSummary } from '../api/client' +/* + * Mock data — used when the backend is not running. + * Satisfies acceptance criteria: "Agent card displays real data from backend OR mock data." + * + * All fields match the AgentSummary interface in api/client.ts: + * id, name, description, category, rating, downloads, tags, tools_required + * + * TODO: Remove when backend /agents endpoint is stable. + */ +const MOCK_AGENTS: AgentSummary[] = [ + { + id: 'email-summarizer', + name: 'Email Summarizer', + description: + 'Reads your inbox and generates concise bullet-point summaries for each thread, so you never miss an action item.', + category: 'Productivity', + rating: 4.7, + downloads: 12843, + tags: ['email', 'summarization', 'inbox'], + tools_required: ['fetch_emails', 'summarize_text'], + }, + { + id: 'github-issue-triage', + name: 'GitHub Issue Triage', + description: + 'Scans open GitHub issues, classifies them by priority and type, and recommends which to fix first based on impact.', + category: 'Developer Tools', + rating: 4.5, + downloads: 8201, + tags: ['github', 'triage', 'issues'], + tools_required: ['fetch_github_issues', 'triage_issues'], + }, + { + id: 'meeting-notes', + name: 'Meeting Notes', + description: + 'Transcribes recorded meetings and extracts a structured list of action items, owners, and due dates automatically.', + category: 'Productivity', + rating: 4.3, + downloads: 5670, + tags: ['meetings', 'transcription', 'action-items'], + tools_required: ['transcribe_audio', 'extract_action_items'], + }, + { + id: 'weather-agent', + name: 'Weather Agent', + description: + 'Answers natural-language weather questions for any city. Returns current conditions, humidity, and wind speed.', + category: 'Information', + rating: 4.1, + downloads: 3120, + tags: ['weather', 'geocoding', 'real-time'], + tools_required: ['geocode_tool', 'weather_lookup_tool', 'response_format_tool'], + }, +] + /** * AgentsPage — browse all marketplace agents. * + * Falls back to MOCK_AGENTS when the backend is unavailable, + * so the card UI can be reviewed without a running server. + * * TODO: Add category filter, search, and trending sort * TODO: Connect to trending endpoint when data-science implements it */ export default function AgentsPage() { const [agents, setAgents] = useState([]) const [loading, setLoading] = useState(true) - const [error, setError] = useState(null) + const [usingMock, setUsingMock] = useState(false) useEffect(() => { fetchAgents() - .then(setAgents) - .catch((e) => setError(e.message)) + .then((data) => { + setAgents(data) + setUsingMock(false) + }) + .catch(() => { + // Backend not running — fall back to mock data so the card is always visible + setAgents(MOCK_AGENTS) + setUsingMock(true) + }) .finally(() => setLoading(false)) }, []) - if (loading) return

Loading agents...

- if (error) { - return ( -
-

Browse Agents

-
- Could not load agents from API ({error}). Start the backend with{' '} - uvicorn app.main:app --reload from the backend folder. -
-

Showing placeholder message — implement offline fallback in a future ticket.

-
- ) - } + if (loading) return

Loading agents…

return (

Browse Agents

-

+ +

{agents.length} agents available

+ + {/* Mock data notice — visible only when backend is offline */} + {usingMock && ( +
+ Backend not detected — showing mock data. Start the backend with{' '} + uvicorn app.main:app --reload from the backend/ folder + to load live agents. +
+ )} +
{agents.map((agent) => ( @@ -47,4 +112,4 @@ export default function AgentsPage() {
) -} +} \ No newline at end of file diff --git a/frontend/src/vite-env.d.ts b/frontend/src/vite-env.d.ts new file mode 100644 index 0000000..e332379 --- /dev/null +++ b/frontend/src/vite-env.d.ts @@ -0,0 +1 @@ +declare module '*.css' \ No newline at end of file From 66173b1b24585bbeba8722124afd3b0bf4b29b2b Mon Sep 17 00:00:00 2001 From: Vu Chau Date: Thu, 11 Jun 2026 09:56:11 -0500 Subject: [PATCH 3/3] Improved response with more detailed status --- backend/app/routers/runs.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/backend/app/routers/runs.py b/backend/app/routers/runs.py index df21434..9a09ad2 100644 --- a/backend/app/routers/runs.py +++ b/backend/app/routers/runs.py @@ -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, )