diff --git a/agents/manifests/github_issue_triage_agent.json b/agents/manifests/github_issue_triage_agent.json index e665530..f73a0a1 100644 --- a/agents/manifests/github_issue_triage_agent.json +++ b/agents/manifests/github_issue_triage_agent.json @@ -1,45 +1,116 @@ { - "id": "github_issue_triage", - "name": "GitHub Issue Triage Agent", - "description": "Analyzes open GitHub issues, suggests labels, priority, and assignees.", - "category": "Developer Tools", - "creator": "AgentStore Team", - "version": "1.0.0", - "status": "published", - "rating": 4.7, - "downloads": 980, - "installs": 720, - "runs": 3100, - "tags": ["github", "developer", "triage", "issues"], - "tools_required": ["github_reader"], - "permissions_required": ["github.read"], - "inputs": { - "type": "object", - "properties": { - "repo": { "type": "string", "description": "owner/repo" }, - "state": { "type": "string", "enum": ["open", "closed", "all"] } - } - }, - "outputs": { - "type": "object", - "properties": { - "triaged_issues": { - "type": "array", - "items": { - "type": "object", - "properties": { - "issue_number": { "type": "integer" }, - "suggested_labels": { "type": "array" }, - "priority": { "type": "string" }, - "suggested_assignee": { "type": "string" } - } + "id": "github_issue_triage", + "name": "GitHub Issue Triage Agent", + "description": "Reads open issues from any GitHub repository and automatically classifies each one by type (bug, enhancement, question, docs), assigns a priority level (high, medium, low), and recommends the best team or individual to assign it to — based on labels, keywords, and past assignee patterns. Saves hours of manual backlog grooming for engineering teams.", + "category": "Developer Tools", + "creator": "AgentStore Team", + "version": "1.0.0", + "status": "published", + "rating": 4.7, + "downloads": 980, + "installs": 720, + "runs": 3100, + "tags": [ + "github", + "developer-tools", + "triage", + "issues", + "backlog", + "open-source", + "project-management", + "automation" + ], + "tools_required": ["github_reader"], + "permissions_required": ["github.read"], + "permissions_explanation": { + "github.read": "Read-only access to public or private repository issues and labels. The agent never writes to GitHub, creates labels, or modifies any issue — it only reads and classifies." + }, + "inputs": { + "type": "object", + "required": ["repo"], + "properties": { + "repo": { + "type": "string", + "description": "The GitHub repository to triage in owner/repo format. Example: 'microsoft/vscode' or 'myorg/myrepo'." + }, + "state": { + "type": "string", + "enum": ["open", "closed", "all"], + "default": "open", + "description": "Which issues to fetch. Use 'open' for active backlog triage (recommended), 'closed' for historical review, or 'all' for both." + }, + "max_issues": { + "type": "integer", + "default": 50, + "description": "Maximum number of issues to triage in one run. Increase for large backlogs (up to 200). Defaults to 50." + }, + "label_filter": { + "type": "array", + "items": { "type": "string" }, + "description": "Optional list of existing GitHub labels to filter by before triaging. Example: ['needs-triage', 'bug']. Leave empty to triage all issues." + } } - } - } - }, - "example_use_case": "Automatically triage new issues in an open-source repo every morning.", - "example_prompts": [ - "Triage all open issues in myorg/myrepo", - "Which issues need urgent attention?" - ] -} + }, + "outputs": { + "type": "object", + "properties": { + "triaged_issues": { + "type": "array", + "description": "One entry per issue containing the classification results.", + "items": { + "type": "object", + "properties": { + "issue_number": { + "type": "integer", + "description": "The GitHub issue number (e.g. #42)." + }, + "title": { + "type": "string", + "description": "The original issue title." + }, + "issue_type": { + "type": "string", + "enum": ["bug", "enhancement", "question", "docs", "chore", "unknown"], + "description": "Classified issue type based on title, body, and existing labels." + }, + "suggested_labels": { + "type": "array", + "items": { "type": "string" }, + "description": "New labels the agent recommends applying. Example: ['bug', 'frontend', 'good-first-issue']." + }, + "priority": { + "type": "string", + "enum": ["high", "medium", "low"], + "description": "Suggested fix priority. 'high' = crash/data loss/security. 'medium' = broken feature. 'low' = nice-to-have." + }, + "suggested_assignee": { + "type": "string", + "description": "GitHub username recommended for this issue based on past ownership of similar areas. May be empty if no clear match." + }, + "reasoning": { + "type": "string", + "description": "One-sentence explanation of why this priority and assignee were suggested." + } + } + } + }, + "triage_summary": { + "type": "object", + "description": "Aggregate stats across all triaged issues.", + "properties": { + "total_triaged": { "type": "integer", "description": "Total number of issues processed." }, + "high_priority_count": { "type": "integer", "description": "Number of issues classified as high priority." }, + "by_type": { "type": "object", "description": "Breakdown of issue count by type. Example: { 'bug': 5, 'enhancement': 3 }." } + } + } + } + }, + "example_use_case": "A maintainer of an open-source repo runs this agent every Monday morning on the previous week's new issues. The agent classifies each issue as bug or enhancement, flags 3 high-priority crashes, and recommends assignees — turning 30 minutes of manual triage into a 10-second review.", + "example_prompts": [ + "Triage all open issues in myorg/myrepo and tell me which are highest priority", + "Which open issues in microsoft/vscode are bugs that need urgent attention?", + "Classify and assign the 20 newest issues in our repo", + "Find all untracked enhancement requests in myorg/backend and suggest labels", + "Give me a triage summary for techx/agentstore — how many bugs vs feature requests?" + ] +} \ No newline at end of file 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") + 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, ) 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