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

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 

7 

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 

22 

23router = APIRouter() 

24 

25 

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") 

37 

38 try: 

39 # Generate API key 

40 api_key = create_api_key() 

41 api_key_hash = hash_api_key(api_key) 

42 

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 ) 

51 

52 db.add(agent) 

53 await db.commit() 

54 await db.refresh(agent) 

55 

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)}") 

70 

71 

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) 

79 

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() 

95 

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 

98 

99 result = await db.execute(query) 

100 agents = result.scalars().all() 

101 

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 ] 

113 

114 

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() 

125 

126 if not agent: 

127 raise HTTPException(status_code=404, detail="Agent not found") 

128 

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() 

137 

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 )) 

147 

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 ) 

158 

159 

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) 

171 

172 await db.commit() 

173 await db.refresh(agent) 

174 

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)}")