draft-board 0.1.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (250) hide show
  1. package/app/backend/.env.example +9 -0
  2. package/app/backend/.smartkanban/evidence/8b383839-cbec-45af-86ee-c7708d075cbe/bddf2ed5-2e21-4d46-a62b-10b87f1642a6_patch.txt +195 -0
  3. package/app/backend/.smartkanban/evidence/8b383839-cbec-45af-86ee-c7708d075cbe/bddf2ed5-2e21-4d46-a62b-10b87f1642a6_stat.txt +6 -0
  4. package/app/backend/CURL_EXAMPLES.md +335 -0
  5. package/app/backend/ENV_SETUP.md +65 -0
  6. package/app/backend/alembic/env.py +71 -0
  7. package/app/backend/alembic/script.py.mako +28 -0
  8. package/app/backend/alembic/versions/001_initial_schema.py +104 -0
  9. package/app/backend/alembic/versions/002_add_jobs_table.py +52 -0
  10. package/app/backend/alembic/versions/003_add_workspace_table.py +48 -0
  11. package/app/backend/alembic/versions/004_add_evidence_table.py +56 -0
  12. package/app/backend/alembic/versions/005_add_verification_commands.py +32 -0
  13. package/app/backend/alembic/versions/006_add_planner_lock_table.py +39 -0
  14. package/app/backend/alembic/versions/007_add_revision_review_tables.py +126 -0
  15. package/app/backend/alembic/versions/008_add_revision_idempotency_and_traceability.py +52 -0
  16. package/app/backend/alembic/versions/009_add_job_health_fields.py +46 -0
  17. package/app/backend/alembic/versions/010_add_review_comment_line_content.py +36 -0
  18. package/app/backend/alembic/versions/011_add_analysis_cache.py +47 -0
  19. package/app/backend/alembic/versions/012_add_boards_table.py +102 -0
  20. package/app/backend/alembic/versions/013_add_ticket_blocking.py +45 -0
  21. package/app/backend/alembic/versions/014_add_agent_sessions.py +220 -0
  22. package/app/backend/alembic/versions/015_add_ticket_sort_order.py +33 -0
  23. package/app/backend/alembic/versions/03220f0b93ae_add_pr_fields_to_ticket.py +49 -0
  24. package/app/backend/alembic/versions/0c2d89fff3b1_seed_board_configs_from_yaml.py +206 -0
  25. package/app/backend/alembic/versions/3348e5cf54c1_add_merge_checklist_table.py +67 -0
  26. package/app/backend/alembic/versions/357c780ee445_add_goal_status.py +34 -0
  27. package/app/backend/alembic/versions/553340b7e26c_add_autonomy_fields_to_goal.py +65 -0
  28. package/app/backend/alembic/versions/774dc335c679_merge_migration_heads.py +23 -0
  29. package/app/backend/alembic/versions/7b307e847cbd_merge_heads.py +23 -0
  30. package/app/backend/alembic/versions/82ecd978cc70_add_missing_indexes.py +48 -0
  31. package/app/backend/alembic/versions/8ef5054dc280_add_normalized_log_entries.py +173 -0
  32. package/app/backend/alembic/versions/8f3e2bd8ea3b_merge_migration_heads.py +23 -0
  33. package/app/backend/alembic/versions/9d17f0698d3b_add_config_column_to_boards_table.py +30 -0
  34. package/app/backend/alembic/versions/add_agent_conversation_history.py +72 -0
  35. package/app/backend/alembic/versions/add_job_variant.py +34 -0
  36. package/app/backend/alembic/versions/add_performance_indexes.py +95 -0
  37. package/app/backend/alembic/versions/add_repos_and_board_repos.py +174 -0
  38. package/app/backend/alembic/versions/add_session_id_to_jobs.py +27 -0
  39. package/app/backend/alembic/versions/add_sqlite_backend_tables.py +104 -0
  40. package/app/backend/alembic/versions/b10fb0b62240_add_diff_content_to_revisions.py +34 -0
  41. package/app/backend/alembic.ini +89 -0
  42. package/app/backend/app/__init__.py +3 -0
  43. package/app/backend/app/data_dir.py +85 -0
  44. package/app/backend/app/database.py +70 -0
  45. package/app/backend/app/database_sync.py +64 -0
  46. package/app/backend/app/dependencies/__init__.py +5 -0
  47. package/app/backend/app/dependencies/auth.py +80 -0
  48. package/app/backend/app/dependencies.py +43 -0
  49. package/app/backend/app/exceptions.py +178 -0
  50. package/app/backend/app/executors/__init__.py +1 -0
  51. package/app/backend/app/executors/adapters/__init__.py +1 -0
  52. package/app/backend/app/executors/adapters/aider.py +152 -0
  53. package/app/backend/app/executors/adapters/amazon_q.py +103 -0
  54. package/app/backend/app/executors/adapters/amp.py +123 -0
  55. package/app/backend/app/executors/adapters/claude.py +177 -0
  56. package/app/backend/app/executors/adapters/cline.py +127 -0
  57. package/app/backend/app/executors/adapters/codex.py +167 -0
  58. package/app/backend/app/executors/adapters/copilot.py +202 -0
  59. package/app/backend/app/executors/adapters/cursor.py +87 -0
  60. package/app/backend/app/executors/adapters/droid.py +123 -0
  61. package/app/backend/app/executors/adapters/gemini.py +132 -0
  62. package/app/backend/app/executors/adapters/goose.py +131 -0
  63. package/app/backend/app/executors/adapters/opencode.py +123 -0
  64. package/app/backend/app/executors/adapters/qwen.py +123 -0
  65. package/app/backend/app/executors/plugins/__init__.py +1 -0
  66. package/app/backend/app/executors/registry.py +202 -0
  67. package/app/backend/app/executors/spec.py +226 -0
  68. package/app/backend/app/main.py +486 -0
  69. package/app/backend/app/middleware/__init__.py +13 -0
  70. package/app/backend/app/middleware/idempotency.py +426 -0
  71. package/app/backend/app/middleware/rate_limit.py +312 -0
  72. package/app/backend/app/middleware/security_headers.py +43 -0
  73. package/app/backend/app/middleware/timeout.py +37 -0
  74. package/app/backend/app/models/__init__.py +56 -0
  75. package/app/backend/app/models/agent_conversation_history.py +56 -0
  76. package/app/backend/app/models/agent_session.py +127 -0
  77. package/app/backend/app/models/analysis_cache.py +49 -0
  78. package/app/backend/app/models/base.py +9 -0
  79. package/app/backend/app/models/board.py +79 -0
  80. package/app/backend/app/models/board_repo.py +68 -0
  81. package/app/backend/app/models/cost_budget.py +42 -0
  82. package/app/backend/app/models/enums.py +40 -0
  83. package/app/backend/app/models/evidence.py +132 -0
  84. package/app/backend/app/models/goal.py +102 -0
  85. package/app/backend/app/models/idempotency_entry.py +30 -0
  86. package/app/backend/app/models/job.py +163 -0
  87. package/app/backend/app/models/job_queue.py +39 -0
  88. package/app/backend/app/models/kv_store.py +28 -0
  89. package/app/backend/app/models/merge_checklist.py +87 -0
  90. package/app/backend/app/models/normalized_log.py +100 -0
  91. package/app/backend/app/models/planner_lock.py +43 -0
  92. package/app/backend/app/models/rate_limit_entry.py +25 -0
  93. package/app/backend/app/models/repo.py +66 -0
  94. package/app/backend/app/models/review_comment.py +91 -0
  95. package/app/backend/app/models/review_summary.py +69 -0
  96. package/app/backend/app/models/revision.py +130 -0
  97. package/app/backend/app/models/ticket.py +223 -0
  98. package/app/backend/app/models/ticket_event.py +83 -0
  99. package/app/backend/app/models/user.py +47 -0
  100. package/app/backend/app/models/workspace.py +71 -0
  101. package/app/backend/app/redis_client.py +119 -0
  102. package/app/backend/app/routers/__init__.py +29 -0
  103. package/app/backend/app/routers/agents.py +296 -0
  104. package/app/backend/app/routers/auth.py +94 -0
  105. package/app/backend/app/routers/board.py +885 -0
  106. package/app/backend/app/routers/dashboard.py +351 -0
  107. package/app/backend/app/routers/debug.py +528 -0
  108. package/app/backend/app/routers/evidence.py +96 -0
  109. package/app/backend/app/routers/executors.py +324 -0
  110. package/app/backend/app/routers/goals.py +574 -0
  111. package/app/backend/app/routers/jobs.py +448 -0
  112. package/app/backend/app/routers/maintenance.py +172 -0
  113. package/app/backend/app/routers/merge.py +360 -0
  114. package/app/backend/app/routers/planner.py +537 -0
  115. package/app/backend/app/routers/pull_requests.py +382 -0
  116. package/app/backend/app/routers/repos.py +263 -0
  117. package/app/backend/app/routers/revisions.py +939 -0
  118. package/app/backend/app/routers/settings.py +267 -0
  119. package/app/backend/app/routers/tickets.py +2003 -0
  120. package/app/backend/app/routers/webhooks.py +143 -0
  121. package/app/backend/app/routers/websocket.py +249 -0
  122. package/app/backend/app/schemas/__init__.py +109 -0
  123. package/app/backend/app/schemas/board.py +87 -0
  124. package/app/backend/app/schemas/common.py +33 -0
  125. package/app/backend/app/schemas/evidence.py +87 -0
  126. package/app/backend/app/schemas/goal.py +90 -0
  127. package/app/backend/app/schemas/job.py +97 -0
  128. package/app/backend/app/schemas/merge.py +139 -0
  129. package/app/backend/app/schemas/planner.py +500 -0
  130. package/app/backend/app/schemas/repo.py +187 -0
  131. package/app/backend/app/schemas/review.py +137 -0
  132. package/app/backend/app/schemas/revision.py +114 -0
  133. package/app/backend/app/schemas/ticket.py +238 -0
  134. package/app/backend/app/schemas/ticket_event.py +72 -0
  135. package/app/backend/app/schemas/workspace.py +19 -0
  136. package/app/backend/app/services/__init__.py +31 -0
  137. package/app/backend/app/services/agent_memory_service.py +223 -0
  138. package/app/backend/app/services/agent_registry.py +346 -0
  139. package/app/backend/app/services/agent_session_manager.py +318 -0
  140. package/app/backend/app/services/agent_session_service.py +219 -0
  141. package/app/backend/app/services/agent_tools.py +379 -0
  142. package/app/backend/app/services/auth_service.py +98 -0
  143. package/app/backend/app/services/autonomy_service.py +380 -0
  144. package/app/backend/app/services/board_repo_service.py +201 -0
  145. package/app/backend/app/services/board_service.py +326 -0
  146. package/app/backend/app/services/cleanup_service.py +1085 -0
  147. package/app/backend/app/services/config_service.py +908 -0
  148. package/app/backend/app/services/context_gatherer.py +557 -0
  149. package/app/backend/app/services/cost_tracking_service.py +293 -0
  150. package/app/backend/app/services/cursor_log_normalizer.py +536 -0
  151. package/app/backend/app/services/delivery_pipeline.py +440 -0
  152. package/app/backend/app/services/executor_service.py +634 -0
  153. package/app/backend/app/services/git_host/__init__.py +11 -0
  154. package/app/backend/app/services/git_host/factory.py +87 -0
  155. package/app/backend/app/services/git_host/github.py +270 -0
  156. package/app/backend/app/services/git_host/gitlab.py +194 -0
  157. package/app/backend/app/services/git_host/protocol.py +75 -0
  158. package/app/backend/app/services/git_merge_simple.py +346 -0
  159. package/app/backend/app/services/git_ops.py +384 -0
  160. package/app/backend/app/services/github_service.py +233 -0
  161. package/app/backend/app/services/goal_service.py +113 -0
  162. package/app/backend/app/services/job_service.py +423 -0
  163. package/app/backend/app/services/job_watchdog_service.py +424 -0
  164. package/app/backend/app/services/langchain_adapter.py +122 -0
  165. package/app/backend/app/services/llm_provider_clients.py +351 -0
  166. package/app/backend/app/services/llm_service.py +285 -0
  167. package/app/backend/app/services/log_normalizer.py +342 -0
  168. package/app/backend/app/services/log_stream_service.py +276 -0
  169. package/app/backend/app/services/merge_checklist_service.py +264 -0
  170. package/app/backend/app/services/merge_service.py +784 -0
  171. package/app/backend/app/services/orchestrator_log.py +84 -0
  172. package/app/backend/app/services/planner_service.py +1662 -0
  173. package/app/backend/app/services/planner_tick_sync.py +1040 -0
  174. package/app/backend/app/services/queued_message_service.py +156 -0
  175. package/app/backend/app/services/reliability_wrapper.py +389 -0
  176. package/app/backend/app/services/repo_discovery_service.py +318 -0
  177. package/app/backend/app/services/review_service.py +334 -0
  178. package/app/backend/app/services/revision_service.py +389 -0
  179. package/app/backend/app/services/safe_autopilot.py +510 -0
  180. package/app/backend/app/services/sqlite_worker.py +372 -0
  181. package/app/backend/app/services/task_dispatch.py +135 -0
  182. package/app/backend/app/services/ticket_generation_service.py +1781 -0
  183. package/app/backend/app/services/ticket_service.py +486 -0
  184. package/app/backend/app/services/udar_planner_service.py +1007 -0
  185. package/app/backend/app/services/webhook_service.py +126 -0
  186. package/app/backend/app/services/workspace_service.py +465 -0
  187. package/app/backend/app/services/worktree_file_service.py +92 -0
  188. package/app/backend/app/services/worktree_validator.py +213 -0
  189. package/app/backend/app/sqlite_kv.py +278 -0
  190. package/app/backend/app/state_machine.py +128 -0
  191. package/app/backend/app/templates/__init__.py +5 -0
  192. package/app/backend/app/templates/registry.py +243 -0
  193. package/app/backend/app/utils/__init__.py +5 -0
  194. package/app/backend/app/utils/artifact_reader.py +87 -0
  195. package/app/backend/app/utils/circuit_breaker.py +229 -0
  196. package/app/backend/app/utils/db_retry.py +136 -0
  197. package/app/backend/app/utils/ignored_fields.py +123 -0
  198. package/app/backend/app/utils/validators.py +54 -0
  199. package/app/backend/app/websocket/__init__.py +5 -0
  200. package/app/backend/app/websocket/manager.py +179 -0
  201. package/app/backend/app/websocket/state_tracker.py +113 -0
  202. package/app/backend/app/worker.py +3190 -0
  203. package/app/backend/calculator_tickets.json +40 -0
  204. package/app/backend/canary_tests.sh +591 -0
  205. package/app/backend/celerybeat-schedule +0 -0
  206. package/app/backend/celerybeat-schedule-shm +0 -0
  207. package/app/backend/celerybeat-schedule-wal +0 -0
  208. package/app/backend/logs/.gitkeep +3 -0
  209. package/app/backend/multiplication_division_implementation_tickets.json +55 -0
  210. package/app/backend/multiplication_division_tickets.json +42 -0
  211. package/app/backend/pyproject.toml +45 -0
  212. package/app/backend/requirements-dev.txt +8 -0
  213. package/app/backend/requirements.txt +20 -0
  214. package/app/backend/run.sh +30 -0
  215. package/app/backend/run_with_logs.sh +10 -0
  216. package/app/backend/scientific_calculator_tickets.json +40 -0
  217. package/app/backend/scripts/extract_openapi.py +21 -0
  218. package/app/backend/scripts/seed_demo.py +187 -0
  219. package/app/backend/setup_demo_review.py +302 -0
  220. package/app/backend/test_actual_parse.py +41 -0
  221. package/app/backend/test_agent_streaming.py +61 -0
  222. package/app/backend/test_parse.py +51 -0
  223. package/app/backend/test_streaming.py +51 -0
  224. package/app/backend/test_subprocess_streaming.py +50 -0
  225. package/app/backend/tests/__init__.py +1 -0
  226. package/app/backend/tests/conftest.py +46 -0
  227. package/app/backend/tests/test_auth.py +341 -0
  228. package/app/backend/tests/test_autonomy_service.py +391 -0
  229. package/app/backend/tests/test_cleanup_service_safety.py +417 -0
  230. package/app/backend/tests/test_middleware.py +279 -0
  231. package/app/backend/tests/test_planner_providers.py +290 -0
  232. package/app/backend/tests/test_planner_unblock.py +183 -0
  233. package/app/backend/tests/test_revision_invariants.py +618 -0
  234. package/app/backend/tests/test_sqlite_kv.py +290 -0
  235. package/app/backend/tests/test_sqlite_worker.py +353 -0
  236. package/app/backend/tests/test_task_dispatch.py +100 -0
  237. package/app/backend/tests/test_ticket_validation.py +304 -0
  238. package/app/backend/tests/test_udar_agent.py +693 -0
  239. package/app/backend/tests/test_webhook_service.py +184 -0
  240. package/app/backend/tickets_output.json +59 -0
  241. package/app/backend/user_management_tickets.json +50 -0
  242. package/app/backend/uvicorn.log +0 -0
  243. package/app/draft.yaml +313 -0
  244. package/app/frontend/dist/assets/index-LcjCczu5.js +155 -0
  245. package/app/frontend/dist/assets/index-_FP_279e.css +1 -0
  246. package/app/frontend/dist/index.html +14 -0
  247. package/app/frontend/dist/vite.svg +1 -0
  248. package/app/frontend/package.json +101 -0
  249. package/bin/cli.js +527 -0
  250. package/package.json +37 -0
@@ -0,0 +1,296 @@
1
+ """AI Agent management API endpoints."""
2
+
3
+ import logging
4
+
5
+ from fastapi import APIRouter, Depends, HTTPException, Query
6
+ from pydantic import BaseModel
7
+ from sqlalchemy import select
8
+ from sqlalchemy.ext.asyncio import AsyncSession
9
+
10
+ from app.database import get_db
11
+ from app.models.agent_session import AgentMessage, AgentSession
12
+ from app.services.agent_registry import (
13
+ AGENT_REGISTRY,
14
+ AgentType,
15
+ agent_registry,
16
+ )
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+ router = APIRouter(prefix="/agents", tags=["agents"])
21
+
22
+
23
+ # ============================================================================
24
+ # Response Models
25
+ # ============================================================================
26
+
27
+
28
+ class AgentInfo(BaseModel):
29
+ """Information about an AI agent."""
30
+
31
+ type: str
32
+ name: str
33
+ available: bool
34
+ supports_yolo: bool = False
35
+ supports_session_resume: bool = False
36
+ supports_mcp: bool = False
37
+ cost_per_1k_input: float | None = None
38
+ cost_per_1k_output: float | None = None
39
+ description: str = ""
40
+
41
+
42
+ class AgentListResponse(BaseModel):
43
+ """List of available agents."""
44
+
45
+ agents: list[AgentInfo]
46
+ default_agent: str = "claude"
47
+
48
+
49
+ class SessionInfo(BaseModel):
50
+ """Agent session information."""
51
+
52
+ id: str
53
+ ticket_id: str
54
+ agent_type: str
55
+ agent_session_id: str | None = None
56
+ is_active: bool
57
+ turn_count: int
58
+ total_input_tokens: int
59
+ total_output_tokens: int
60
+ estimated_cost_usd: float
61
+ last_prompt: str | None = None
62
+ created_at: str
63
+ updated_at: str
64
+ ended_at: str | None = None
65
+
66
+
67
+ class SessionListResponse(BaseModel):
68
+ """List of sessions for a ticket."""
69
+
70
+ sessions: list[SessionInfo]
71
+ total: int
72
+
73
+
74
+ class MessageInfo(BaseModel):
75
+ """Agent message information."""
76
+
77
+ id: str
78
+ role: str
79
+ content: str
80
+ input_tokens: int
81
+ output_tokens: int
82
+ tool_name: str | None = None
83
+ created_at: str
84
+
85
+
86
+ class SessionDetailResponse(BaseModel):
87
+ """Detailed session information with messages."""
88
+
89
+ session: SessionInfo
90
+ messages: list[MessageInfo]
91
+
92
+
93
+ # ============================================================================
94
+ # Agent Descriptions
95
+ # ============================================================================
96
+
97
+ AGENT_DESCRIPTIONS = {
98
+ AgentType.CLAUDE: "Anthropic's Claude Code CLI - best for complex reasoning and code generation",
99
+ AgentType.CURSOR: "Cursor IDE's agent mode - interactive, opens editor",
100
+ AgentType.AMP: "Sourcegraph's Amp agent - fast, good for quick fixes",
101
+ AgentType.AIDER: "Open-source coding assistant - free, supports multiple models",
102
+ AgentType.CODEX: "OpenAI's Codex - specialized for code completion",
103
+ AgentType.GEMINI: "Google's Gemini CLI - multimodal capabilities",
104
+ }
105
+
106
+ AGENT_DISPLAY_NAMES = {
107
+ AgentType.CLAUDE: "Claude Code",
108
+ AgentType.CURSOR: "Cursor Agent",
109
+ AgentType.AMP: "Amp",
110
+ AgentType.AIDER: "Aider",
111
+ AgentType.CODEX: "Codex",
112
+ AgentType.GEMINI: "Gemini",
113
+ }
114
+
115
+
116
+ # ============================================================================
117
+ # API Endpoints
118
+ # ============================================================================
119
+
120
+
121
+ @router.get("", response_model=AgentListResponse)
122
+ async def list_agents() -> AgentListResponse:
123
+ """List all known AI agents and their availability."""
124
+ agents = []
125
+
126
+ for agent_type in AgentType:
127
+ config = AGENT_REGISTRY.get(agent_type)
128
+ if not config:
129
+ continue
130
+
131
+ executor = agent_registry.get_executor(agent_type)
132
+ is_available = executor.is_available() if executor else False
133
+
134
+ agents.append(
135
+ AgentInfo(
136
+ type=agent_type.value,
137
+ name=AGENT_DISPLAY_NAMES.get(agent_type, agent_type.value),
138
+ available=is_available,
139
+ supports_yolo=config.supports_yolo,
140
+ supports_session_resume=config.supports_session_resume,
141
+ supports_mcp=config.supports_mcp,
142
+ cost_per_1k_input=config.cost_per_1k_input,
143
+ cost_per_1k_output=config.cost_per_1k_output,
144
+ description=AGENT_DESCRIPTIONS.get(agent_type, ""),
145
+ )
146
+ )
147
+
148
+ # Sort: available first, then by name
149
+ agents.sort(key=lambda a: (not a.available, a.name))
150
+
151
+ return AgentListResponse(agents=agents, default_agent="claude")
152
+
153
+
154
+ @router.get("/available", response_model=list[str])
155
+ async def list_available_agents() -> list[str]:
156
+ """List only the agents available on this system."""
157
+ available = agent_registry.get_available_agents()
158
+ return [a.value for a in available]
159
+
160
+
161
+ @router.get("/{agent_type}", response_model=AgentInfo)
162
+ async def get_agent(agent_type: str) -> AgentInfo:
163
+ """Get detailed information about a specific agent."""
164
+ try:
165
+ agent_enum = AgentType(agent_type)
166
+ except ValueError:
167
+ raise HTTPException(status_code=404, detail=f"Unknown agent type: {agent_type}")
168
+
169
+ config = AGENT_REGISTRY.get(agent_enum)
170
+ if not config:
171
+ raise HTTPException(
172
+ status_code=404, detail=f"Agent not configured: {agent_type}"
173
+ )
174
+
175
+ executor = agent_registry.get_executor(agent_enum)
176
+ is_available = executor.is_available() if executor else False
177
+
178
+ return AgentInfo(
179
+ type=agent_enum.value,
180
+ name=AGENT_DISPLAY_NAMES.get(agent_enum, agent_enum.value),
181
+ available=is_available,
182
+ supports_yolo=config.supports_yolo,
183
+ supports_session_resume=config.supports_session_resume,
184
+ supports_mcp=config.supports_mcp,
185
+ cost_per_1k_input=config.cost_per_1k_input,
186
+ cost_per_1k_output=config.cost_per_1k_output,
187
+ description=AGENT_DESCRIPTIONS.get(agent_enum, ""),
188
+ )
189
+
190
+
191
+ @router.get("/sessions/ticket/{ticket_id}", response_model=SessionListResponse)
192
+ async def list_ticket_sessions(
193
+ ticket_id: str,
194
+ include_ended: bool = Query(False, description="Include ended sessions"),
195
+ db: AsyncSession = Depends(get_db),
196
+ ) -> SessionListResponse:
197
+ """List all agent sessions for a ticket."""
198
+ query = select(AgentSession).where(AgentSession.ticket_id == ticket_id)
199
+
200
+ if not include_ended:
201
+ query = query.where(AgentSession.is_active)
202
+
203
+ query = query.order_by(AgentSession.created_at.desc())
204
+
205
+ result = await db.execute(query)
206
+ sessions = result.scalars().all()
207
+
208
+ return SessionListResponse(
209
+ sessions=[
210
+ SessionInfo(
211
+ id=s.id,
212
+ ticket_id=s.ticket_id,
213
+ agent_type=s.agent_type,
214
+ agent_session_id=s.agent_session_id,
215
+ is_active=s.is_active,
216
+ turn_count=s.turn_count,
217
+ total_input_tokens=s.total_input_tokens,
218
+ total_output_tokens=s.total_output_tokens,
219
+ estimated_cost_usd=s.estimated_cost_usd,
220
+ last_prompt=s.last_prompt[:200] if s.last_prompt else None,
221
+ created_at=s.created_at.isoformat(),
222
+ updated_at=s.updated_at.isoformat(),
223
+ ended_at=s.ended_at.isoformat() if s.ended_at else None,
224
+ )
225
+ for s in sessions
226
+ ],
227
+ total=len(sessions),
228
+ )
229
+
230
+
231
+ @router.get("/sessions/{session_id}", response_model=SessionDetailResponse)
232
+ async def get_session(
233
+ session_id: str, db: AsyncSession = Depends(get_db)
234
+ ) -> SessionDetailResponse:
235
+ """Get detailed session information with messages."""
236
+ result = await db.execute(select(AgentSession).where(AgentSession.id == session_id))
237
+ session = result.scalar_one_or_none()
238
+
239
+ if not session:
240
+ raise HTTPException(status_code=404, detail="Session not found")
241
+
242
+ # Load messages
243
+ msg_result = await db.execute(
244
+ select(AgentMessage)
245
+ .where(AgentMessage.session_id == session_id)
246
+ .order_by(AgentMessage.created_at)
247
+ )
248
+ messages = msg_result.scalars().all()
249
+
250
+ return SessionDetailResponse(
251
+ session=SessionInfo(
252
+ id=session.id,
253
+ ticket_id=session.ticket_id,
254
+ agent_type=session.agent_type,
255
+ agent_session_id=session.agent_session_id,
256
+ is_active=session.is_active,
257
+ turn_count=session.turn_count,
258
+ total_input_tokens=session.total_input_tokens,
259
+ total_output_tokens=session.total_output_tokens,
260
+ estimated_cost_usd=session.estimated_cost_usd,
261
+ last_prompt=session.last_prompt,
262
+ created_at=session.created_at.isoformat(),
263
+ updated_at=session.updated_at.isoformat(),
264
+ ended_at=session.ended_at.isoformat() if session.ended_at else None,
265
+ ),
266
+ messages=[
267
+ MessageInfo(
268
+ id=m.id,
269
+ role=m.role,
270
+ content=m.content,
271
+ input_tokens=m.input_tokens,
272
+ output_tokens=m.output_tokens,
273
+ tool_name=m.tool_name,
274
+ created_at=m.created_at.isoformat(),
275
+ )
276
+ for m in messages
277
+ ],
278
+ )
279
+
280
+
281
+ @router.post("/sessions/{session_id}/end")
282
+ async def end_session(session_id: str, db: AsyncSession = Depends(get_db)):
283
+ """End an active session."""
284
+ result = await db.execute(select(AgentSession).where(AgentSession.id == session_id))
285
+ session = result.scalar_one_or_none()
286
+
287
+ if not session:
288
+ raise HTTPException(status_code=404, detail="Session not found")
289
+
290
+ if not session.is_active:
291
+ raise HTTPException(status_code=400, detail="Session already ended")
292
+
293
+ session.end_session()
294
+ await db.commit()
295
+
296
+ return {"message": "Session ended", "session_id": session_id}
@@ -0,0 +1,94 @@
1
+ """Auth router: register, login, current-user profile."""
2
+
3
+ from fastapi import APIRouter, Depends, HTTPException, status
4
+ from pydantic import BaseModel, EmailStr, Field
5
+ from sqlalchemy.ext.asyncio import AsyncSession
6
+
7
+ from app.database import get_db
8
+ from app.dependencies.auth import get_current_user
9
+ from app.models.user import User
10
+ from app.services.auth_service import (
11
+ authenticate_user,
12
+ create_access_token,
13
+ create_user,
14
+ get_user_by_email,
15
+ )
16
+
17
+ router = APIRouter(prefix="/auth", tags=["auth"])
18
+
19
+
20
+ # ---- Schemas ----
21
+
22
+
23
+ class RegisterRequest(BaseModel):
24
+ email: EmailStr
25
+ password: str = Field(min_length=8, max_length=128)
26
+ display_name: str = Field(min_length=1, max_length=255)
27
+
28
+
29
+ class LoginRequest(BaseModel):
30
+ email: EmailStr
31
+ password: str
32
+
33
+
34
+ class AuthResponse(BaseModel):
35
+ access_token: str
36
+ token_type: str = "bearer"
37
+ user: "UserProfile"
38
+
39
+
40
+ class UserProfile(BaseModel):
41
+ id: str
42
+ email: str
43
+ display_name: str
44
+ is_active: bool
45
+
46
+ model_config = {"from_attributes": True}
47
+
48
+
49
+ # ---- Endpoints ----
50
+
51
+
52
+ @router.post("/register", response_model=AuthResponse, status_code=201)
53
+ async def register(body: RegisterRequest, db: AsyncSession = Depends(get_db)):
54
+ """Create a new user account and return an access token."""
55
+ existing = await get_user_by_email(db, body.email)
56
+ if existing is not None:
57
+ raise HTTPException(
58
+ status_code=status.HTTP_409_CONFLICT,
59
+ detail="Email already registered",
60
+ )
61
+
62
+ user = await create_user(db, body.email, body.password, body.display_name)
63
+ token = create_access_token(user.id, user.email)
64
+ return AuthResponse(
65
+ access_token=token,
66
+ user=UserProfile.model_validate(user),
67
+ )
68
+
69
+
70
+ @router.post("/login", response_model=AuthResponse)
71
+ async def login(body: LoginRequest, db: AsyncSession = Depends(get_db)):
72
+ """Authenticate with email/password and return an access token."""
73
+ user = await authenticate_user(db, body.email, body.password)
74
+ if user is None:
75
+ raise HTTPException(
76
+ status_code=status.HTTP_401_UNAUTHORIZED,
77
+ detail="Invalid email or password",
78
+ )
79
+ token = create_access_token(user.id, user.email)
80
+ return AuthResponse(
81
+ access_token=token,
82
+ user=UserProfile.model_validate(user),
83
+ )
84
+
85
+
86
+ @router.get("/me", response_model=UserProfile)
87
+ async def get_me(current_user: User | None = Depends(get_current_user)):
88
+ """Return the authenticated user's profile."""
89
+ if current_user is None:
90
+ raise HTTPException(
91
+ status_code=status.HTTP_401_UNAUTHORIZED,
92
+ detail="Authentication required",
93
+ )
94
+ return UserProfile.model_validate(current_user)