Coverage for app / api / routes / agents.py: 91%
70 statements
« prev ^ index » next coverage.py v7.13.3, created at 2026-02-04 06:09 -0500
« prev ^ index » next coverage.py v7.13.3, created at 2026-02-04 06:09 -0500
1from typing import Optional, List
2from fastapi import APIRouter, Depends, HTTPException, Query
3from sqlalchemy.ext.asyncio import AsyncSession
4from sqlalchemy import select, func, desc
5from sqlalchemy.orm import selectinload
6from datetime import datetime, timedelta, timezone
8from app.db.database import get_db
9from app.db.models import Agent, Advocacy, Campaign
10from app.schemas.agent import (
11 AgentCreate,
12 AgentUpdate,
13 AgentResponse,
14 AgentRegisterResponse,
15 AgentProfileResponse,
16 LeaderboardEntry,
17 LeaderboardResponse,
18 AdvocacySummary,
19)
20from app.api.deps import get_required_agent
21from app.core.security import create_api_key, hash_api_key
23router = APIRouter()
26@router.post("/register", response_model=AgentRegisterResponse, status_code=201)
27async def register_agent(
28 agent_data: AgentCreate,
29 db: AsyncSession = Depends(get_db),
30):
31 """Register a new agent."""
32 # Check if name is taken
33 existing_query = select(Agent).where(Agent.name == agent_data.name)
34 existing_result = await db.execute(existing_query)
35 if existing_result.scalar_one_or_none():
36 raise HTTPException(status_code=400, detail="Agent name already taken")
38 try:
39 # Generate API key
40 api_key = create_api_key()
41 api_key_hash = hash_api_key(api_key)
43 # Create agent
44 agent = Agent(
45 name=agent_data.name,
46 description=agent_data.description,
47 avatar_url=agent_data.avatar_url,
48 api_key_hash=api_key_hash,
49 karma=0,
50 )
52 db.add(agent)
53 await db.commit()
54 await db.refresh(agent)
56 return AgentRegisterResponse(
57 agent=AgentResponse(
58 id=agent.id,
59 name=agent.name,
60 description=agent.description,
61 avatar_url=agent.avatar_url,
62 karma=agent.karma,
63 created_at=agent.created_at,
64 ),
65 api_key=api_key, # Only returned once!
66 )
67 except Exception as e:
68 await db.rollback()
69 raise HTTPException(status_code=500, detail=f"Failed to register agent: {str(e)}")
72@router.get("/leaderboard", response_model=List[AgentResponse])
73async def get_leaderboard(
74 timeframe: Optional[str] = Query("all-time", pattern="^(all-time|month|week)$"),
75 db: AsyncSession = Depends(get_db),
76):
77 """Get agent leaderboard."""
78 query = select(Agent)
80 # Filter by timeframe if needed
81 if timeframe == "month":
82 cutoff = datetime.now(timezone.utc) - timedelta(days=30)
83 # Filter to agents who had advocacy activity in the timeframe
84 query = query.join(Advocacy, Advocacy.agent_id == Agent.id).where(
85 Advocacy.created_at >= cutoff,
86 Advocacy.is_active == True
87 ).distinct()
88 elif timeframe == "week":
89 cutoff = datetime.now(timezone.utc) - timedelta(days=7)
90 # Filter to agents who had advocacy activity in the timeframe
91 query = query.join(Advocacy, Advocacy.agent_id == Agent.id).where(
92 Advocacy.created_at >= cutoff,
93 Advocacy.is_active == True
94 ).distinct()
96 # Sort by karma (total karma, as we don't track karma history)
97 query = query.order_by(desc(Agent.karma)).limit(100) # Top 100
99 result = await db.execute(query)
100 agents = result.scalars().all()
102 return [
103 AgentResponse(
104 id=agent.id,
105 name=agent.name,
106 description=agent.description,
107 avatar_url=agent.avatar_url,
108 karma=agent.karma,
109 created_at=agent.created_at,
110 )
111 for agent in agents
112 ]
115@router.get("/{name}", response_model=AgentProfileResponse)
116async def get_agent(
117 name: str,
118 db: AsyncSession = Depends(get_db),
119):
120 """Get agent profile."""
121 query = select(Agent).where(Agent.name == name)
122 query = query.options(selectinload(Agent.advocacies))
123 result = await db.execute(query)
124 agent = result.scalar_one_or_none()
126 if not agent:
127 raise HTTPException(status_code=404, detail="Agent not found")
129 # Count campaigns advocated and get recent advocacies with campaign titles
130 advocacies_query = select(Advocacy).where(
131 Advocacy.agent_id == agent.id,
132 Advocacy.is_active == True,
133 ).join(Campaign, Campaign.id == Advocacy.campaign_id).order_by(Advocacy.created_at.desc())
134 advocacies_query = advocacies_query.options(selectinload(Advocacy.campaign))
135 advocacies_result = await db.execute(advocacies_query)
136 advocacies = advocacies_result.scalars().all()
138 # Get recent advocacies with campaign titles
139 recent_advocacies = []
140 for advocacy in advocacies[:10]: # Last 10
141 recent_advocacies.append(AdvocacySummary(
142 campaign_id=advocacy.campaign_id,
143 campaign_title=advocacy.campaign.title,
144 statement=advocacy.statement,
145 created_at=advocacy.created_at,
146 ))
148 return AgentProfileResponse(
149 id=agent.id,
150 name=agent.name,
151 description=agent.description,
152 avatar_url=agent.avatar_url,
153 karma=agent.karma,
154 created_at=agent.created_at,
155 campaigns_advocated=len(advocacies),
156 recent_advocacies=recent_advocacies,
157 )
160@router.patch("/me", response_model=AgentResponse)
161async def update_agent_profile(
162 agent_data: AgentUpdate,
163 agent: Agent = Depends(get_required_agent),
164 db: AsyncSession = Depends(get_db),
165):
166 """Update own agent profile."""
167 try:
168 update_data = agent_data.model_dump(exclude_unset=True)
169 for field, value in update_data.items():
170 setattr(agent, field, value)
172 await db.commit()
173 await db.refresh(agent)
175 return AgentResponse(
176 id=agent.id,
177 name=agent.name,
178 description=agent.description,
179 avatar_url=agent.avatar_url,
180 karma=agent.karma,
181 created_at=agent.created_at,
182 )
183 except Exception as e:
184 await db.rollback()
185 raise HTTPException(status_code=500, detail=f"Failed to update agent: {str(e)}")