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.
- package/app/backend/.env.example +9 -0
- package/app/backend/.smartkanban/evidence/8b383839-cbec-45af-86ee-c7708d075cbe/bddf2ed5-2e21-4d46-a62b-10b87f1642a6_patch.txt +195 -0
- package/app/backend/.smartkanban/evidence/8b383839-cbec-45af-86ee-c7708d075cbe/bddf2ed5-2e21-4d46-a62b-10b87f1642a6_stat.txt +6 -0
- package/app/backend/CURL_EXAMPLES.md +335 -0
- package/app/backend/ENV_SETUP.md +65 -0
- package/app/backend/alembic/env.py +71 -0
- package/app/backend/alembic/script.py.mako +28 -0
- package/app/backend/alembic/versions/001_initial_schema.py +104 -0
- package/app/backend/alembic/versions/002_add_jobs_table.py +52 -0
- package/app/backend/alembic/versions/003_add_workspace_table.py +48 -0
- package/app/backend/alembic/versions/004_add_evidence_table.py +56 -0
- package/app/backend/alembic/versions/005_add_verification_commands.py +32 -0
- package/app/backend/alembic/versions/006_add_planner_lock_table.py +39 -0
- package/app/backend/alembic/versions/007_add_revision_review_tables.py +126 -0
- package/app/backend/alembic/versions/008_add_revision_idempotency_and_traceability.py +52 -0
- package/app/backend/alembic/versions/009_add_job_health_fields.py +46 -0
- package/app/backend/alembic/versions/010_add_review_comment_line_content.py +36 -0
- package/app/backend/alembic/versions/011_add_analysis_cache.py +47 -0
- package/app/backend/alembic/versions/012_add_boards_table.py +102 -0
- package/app/backend/alembic/versions/013_add_ticket_blocking.py +45 -0
- package/app/backend/alembic/versions/014_add_agent_sessions.py +220 -0
- package/app/backend/alembic/versions/015_add_ticket_sort_order.py +33 -0
- package/app/backend/alembic/versions/03220f0b93ae_add_pr_fields_to_ticket.py +49 -0
- package/app/backend/alembic/versions/0c2d89fff3b1_seed_board_configs_from_yaml.py +206 -0
- package/app/backend/alembic/versions/3348e5cf54c1_add_merge_checklist_table.py +67 -0
- package/app/backend/alembic/versions/357c780ee445_add_goal_status.py +34 -0
- package/app/backend/alembic/versions/553340b7e26c_add_autonomy_fields_to_goal.py +65 -0
- package/app/backend/alembic/versions/774dc335c679_merge_migration_heads.py +23 -0
- package/app/backend/alembic/versions/7b307e847cbd_merge_heads.py +23 -0
- package/app/backend/alembic/versions/82ecd978cc70_add_missing_indexes.py +48 -0
- package/app/backend/alembic/versions/8ef5054dc280_add_normalized_log_entries.py +173 -0
- package/app/backend/alembic/versions/8f3e2bd8ea3b_merge_migration_heads.py +23 -0
- package/app/backend/alembic/versions/9d17f0698d3b_add_config_column_to_boards_table.py +30 -0
- package/app/backend/alembic/versions/add_agent_conversation_history.py +72 -0
- package/app/backend/alembic/versions/add_job_variant.py +34 -0
- package/app/backend/alembic/versions/add_performance_indexes.py +95 -0
- package/app/backend/alembic/versions/add_repos_and_board_repos.py +174 -0
- package/app/backend/alembic/versions/add_session_id_to_jobs.py +27 -0
- package/app/backend/alembic/versions/add_sqlite_backend_tables.py +104 -0
- package/app/backend/alembic/versions/b10fb0b62240_add_diff_content_to_revisions.py +34 -0
- package/app/backend/alembic.ini +89 -0
- package/app/backend/app/__init__.py +3 -0
- package/app/backend/app/data_dir.py +85 -0
- package/app/backend/app/database.py +70 -0
- package/app/backend/app/database_sync.py +64 -0
- package/app/backend/app/dependencies/__init__.py +5 -0
- package/app/backend/app/dependencies/auth.py +80 -0
- package/app/backend/app/dependencies.py +43 -0
- package/app/backend/app/exceptions.py +178 -0
- package/app/backend/app/executors/__init__.py +1 -0
- package/app/backend/app/executors/adapters/__init__.py +1 -0
- package/app/backend/app/executors/adapters/aider.py +152 -0
- package/app/backend/app/executors/adapters/amazon_q.py +103 -0
- package/app/backend/app/executors/adapters/amp.py +123 -0
- package/app/backend/app/executors/adapters/claude.py +177 -0
- package/app/backend/app/executors/adapters/cline.py +127 -0
- package/app/backend/app/executors/adapters/codex.py +167 -0
- package/app/backend/app/executors/adapters/copilot.py +202 -0
- package/app/backend/app/executors/adapters/cursor.py +87 -0
- package/app/backend/app/executors/adapters/droid.py +123 -0
- package/app/backend/app/executors/adapters/gemini.py +132 -0
- package/app/backend/app/executors/adapters/goose.py +131 -0
- package/app/backend/app/executors/adapters/opencode.py +123 -0
- package/app/backend/app/executors/adapters/qwen.py +123 -0
- package/app/backend/app/executors/plugins/__init__.py +1 -0
- package/app/backend/app/executors/registry.py +202 -0
- package/app/backend/app/executors/spec.py +226 -0
- package/app/backend/app/main.py +486 -0
- package/app/backend/app/middleware/__init__.py +13 -0
- package/app/backend/app/middleware/idempotency.py +426 -0
- package/app/backend/app/middleware/rate_limit.py +312 -0
- package/app/backend/app/middleware/security_headers.py +43 -0
- package/app/backend/app/middleware/timeout.py +37 -0
- package/app/backend/app/models/__init__.py +56 -0
- package/app/backend/app/models/agent_conversation_history.py +56 -0
- package/app/backend/app/models/agent_session.py +127 -0
- package/app/backend/app/models/analysis_cache.py +49 -0
- package/app/backend/app/models/base.py +9 -0
- package/app/backend/app/models/board.py +79 -0
- package/app/backend/app/models/board_repo.py +68 -0
- package/app/backend/app/models/cost_budget.py +42 -0
- package/app/backend/app/models/enums.py +40 -0
- package/app/backend/app/models/evidence.py +132 -0
- package/app/backend/app/models/goal.py +102 -0
- package/app/backend/app/models/idempotency_entry.py +30 -0
- package/app/backend/app/models/job.py +163 -0
- package/app/backend/app/models/job_queue.py +39 -0
- package/app/backend/app/models/kv_store.py +28 -0
- package/app/backend/app/models/merge_checklist.py +87 -0
- package/app/backend/app/models/normalized_log.py +100 -0
- package/app/backend/app/models/planner_lock.py +43 -0
- package/app/backend/app/models/rate_limit_entry.py +25 -0
- package/app/backend/app/models/repo.py +66 -0
- package/app/backend/app/models/review_comment.py +91 -0
- package/app/backend/app/models/review_summary.py +69 -0
- package/app/backend/app/models/revision.py +130 -0
- package/app/backend/app/models/ticket.py +223 -0
- package/app/backend/app/models/ticket_event.py +83 -0
- package/app/backend/app/models/user.py +47 -0
- package/app/backend/app/models/workspace.py +71 -0
- package/app/backend/app/redis_client.py +119 -0
- package/app/backend/app/routers/__init__.py +29 -0
- package/app/backend/app/routers/agents.py +296 -0
- package/app/backend/app/routers/auth.py +94 -0
- package/app/backend/app/routers/board.py +885 -0
- package/app/backend/app/routers/dashboard.py +351 -0
- package/app/backend/app/routers/debug.py +528 -0
- package/app/backend/app/routers/evidence.py +96 -0
- package/app/backend/app/routers/executors.py +324 -0
- package/app/backend/app/routers/goals.py +574 -0
- package/app/backend/app/routers/jobs.py +448 -0
- package/app/backend/app/routers/maintenance.py +172 -0
- package/app/backend/app/routers/merge.py +360 -0
- package/app/backend/app/routers/planner.py +537 -0
- package/app/backend/app/routers/pull_requests.py +382 -0
- package/app/backend/app/routers/repos.py +263 -0
- package/app/backend/app/routers/revisions.py +939 -0
- package/app/backend/app/routers/settings.py +267 -0
- package/app/backend/app/routers/tickets.py +2003 -0
- package/app/backend/app/routers/webhooks.py +143 -0
- package/app/backend/app/routers/websocket.py +249 -0
- package/app/backend/app/schemas/__init__.py +109 -0
- package/app/backend/app/schemas/board.py +87 -0
- package/app/backend/app/schemas/common.py +33 -0
- package/app/backend/app/schemas/evidence.py +87 -0
- package/app/backend/app/schemas/goal.py +90 -0
- package/app/backend/app/schemas/job.py +97 -0
- package/app/backend/app/schemas/merge.py +139 -0
- package/app/backend/app/schemas/planner.py +500 -0
- package/app/backend/app/schemas/repo.py +187 -0
- package/app/backend/app/schemas/review.py +137 -0
- package/app/backend/app/schemas/revision.py +114 -0
- package/app/backend/app/schemas/ticket.py +238 -0
- package/app/backend/app/schemas/ticket_event.py +72 -0
- package/app/backend/app/schemas/workspace.py +19 -0
- package/app/backend/app/services/__init__.py +31 -0
- package/app/backend/app/services/agent_memory_service.py +223 -0
- package/app/backend/app/services/agent_registry.py +346 -0
- package/app/backend/app/services/agent_session_manager.py +318 -0
- package/app/backend/app/services/agent_session_service.py +219 -0
- package/app/backend/app/services/agent_tools.py +379 -0
- package/app/backend/app/services/auth_service.py +98 -0
- package/app/backend/app/services/autonomy_service.py +380 -0
- package/app/backend/app/services/board_repo_service.py +201 -0
- package/app/backend/app/services/board_service.py +326 -0
- package/app/backend/app/services/cleanup_service.py +1085 -0
- package/app/backend/app/services/config_service.py +908 -0
- package/app/backend/app/services/context_gatherer.py +557 -0
- package/app/backend/app/services/cost_tracking_service.py +293 -0
- package/app/backend/app/services/cursor_log_normalizer.py +536 -0
- package/app/backend/app/services/delivery_pipeline.py +440 -0
- package/app/backend/app/services/executor_service.py +634 -0
- package/app/backend/app/services/git_host/__init__.py +11 -0
- package/app/backend/app/services/git_host/factory.py +87 -0
- package/app/backend/app/services/git_host/github.py +270 -0
- package/app/backend/app/services/git_host/gitlab.py +194 -0
- package/app/backend/app/services/git_host/protocol.py +75 -0
- package/app/backend/app/services/git_merge_simple.py +346 -0
- package/app/backend/app/services/git_ops.py +384 -0
- package/app/backend/app/services/github_service.py +233 -0
- package/app/backend/app/services/goal_service.py +113 -0
- package/app/backend/app/services/job_service.py +423 -0
- package/app/backend/app/services/job_watchdog_service.py +424 -0
- package/app/backend/app/services/langchain_adapter.py +122 -0
- package/app/backend/app/services/llm_provider_clients.py +351 -0
- package/app/backend/app/services/llm_service.py +285 -0
- package/app/backend/app/services/log_normalizer.py +342 -0
- package/app/backend/app/services/log_stream_service.py +276 -0
- package/app/backend/app/services/merge_checklist_service.py +264 -0
- package/app/backend/app/services/merge_service.py +784 -0
- package/app/backend/app/services/orchestrator_log.py +84 -0
- package/app/backend/app/services/planner_service.py +1662 -0
- package/app/backend/app/services/planner_tick_sync.py +1040 -0
- package/app/backend/app/services/queued_message_service.py +156 -0
- package/app/backend/app/services/reliability_wrapper.py +389 -0
- package/app/backend/app/services/repo_discovery_service.py +318 -0
- package/app/backend/app/services/review_service.py +334 -0
- package/app/backend/app/services/revision_service.py +389 -0
- package/app/backend/app/services/safe_autopilot.py +510 -0
- package/app/backend/app/services/sqlite_worker.py +372 -0
- package/app/backend/app/services/task_dispatch.py +135 -0
- package/app/backend/app/services/ticket_generation_service.py +1781 -0
- package/app/backend/app/services/ticket_service.py +486 -0
- package/app/backend/app/services/udar_planner_service.py +1007 -0
- package/app/backend/app/services/webhook_service.py +126 -0
- package/app/backend/app/services/workspace_service.py +465 -0
- package/app/backend/app/services/worktree_file_service.py +92 -0
- package/app/backend/app/services/worktree_validator.py +213 -0
- package/app/backend/app/sqlite_kv.py +278 -0
- package/app/backend/app/state_machine.py +128 -0
- package/app/backend/app/templates/__init__.py +5 -0
- package/app/backend/app/templates/registry.py +243 -0
- package/app/backend/app/utils/__init__.py +5 -0
- package/app/backend/app/utils/artifact_reader.py +87 -0
- package/app/backend/app/utils/circuit_breaker.py +229 -0
- package/app/backend/app/utils/db_retry.py +136 -0
- package/app/backend/app/utils/ignored_fields.py +123 -0
- package/app/backend/app/utils/validators.py +54 -0
- package/app/backend/app/websocket/__init__.py +5 -0
- package/app/backend/app/websocket/manager.py +179 -0
- package/app/backend/app/websocket/state_tracker.py +113 -0
- package/app/backend/app/worker.py +3190 -0
- package/app/backend/calculator_tickets.json +40 -0
- package/app/backend/canary_tests.sh +591 -0
- package/app/backend/celerybeat-schedule +0 -0
- package/app/backend/celerybeat-schedule-shm +0 -0
- package/app/backend/celerybeat-schedule-wal +0 -0
- package/app/backend/logs/.gitkeep +3 -0
- package/app/backend/multiplication_division_implementation_tickets.json +55 -0
- package/app/backend/multiplication_division_tickets.json +42 -0
- package/app/backend/pyproject.toml +45 -0
- package/app/backend/requirements-dev.txt +8 -0
- package/app/backend/requirements.txt +20 -0
- package/app/backend/run.sh +30 -0
- package/app/backend/run_with_logs.sh +10 -0
- package/app/backend/scientific_calculator_tickets.json +40 -0
- package/app/backend/scripts/extract_openapi.py +21 -0
- package/app/backend/scripts/seed_demo.py +187 -0
- package/app/backend/setup_demo_review.py +302 -0
- package/app/backend/test_actual_parse.py +41 -0
- package/app/backend/test_agent_streaming.py +61 -0
- package/app/backend/test_parse.py +51 -0
- package/app/backend/test_streaming.py +51 -0
- package/app/backend/test_subprocess_streaming.py +50 -0
- package/app/backend/tests/__init__.py +1 -0
- package/app/backend/tests/conftest.py +46 -0
- package/app/backend/tests/test_auth.py +341 -0
- package/app/backend/tests/test_autonomy_service.py +391 -0
- package/app/backend/tests/test_cleanup_service_safety.py +417 -0
- package/app/backend/tests/test_middleware.py +279 -0
- package/app/backend/tests/test_planner_providers.py +290 -0
- package/app/backend/tests/test_planner_unblock.py +183 -0
- package/app/backend/tests/test_revision_invariants.py +618 -0
- package/app/backend/tests/test_sqlite_kv.py +290 -0
- package/app/backend/tests/test_sqlite_worker.py +353 -0
- package/app/backend/tests/test_task_dispatch.py +100 -0
- package/app/backend/tests/test_ticket_validation.py +304 -0
- package/app/backend/tests/test_udar_agent.py +693 -0
- package/app/backend/tests/test_webhook_service.py +184 -0
- package/app/backend/tickets_output.json +59 -0
- package/app/backend/user_management_tickets.json +50 -0
- package/app/backend/uvicorn.log +0 -0
- package/app/draft.yaml +313 -0
- package/app/frontend/dist/assets/index-LcjCczu5.js +155 -0
- package/app/frontend/dist/assets/index-_FP_279e.css +1 -0
- package/app/frontend/dist/index.html +14 -0
- package/app/frontend/dist/vite.svg +1 -0
- package/app/frontend/package.json +101 -0
- package/bin/cli.js +527 -0
- 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)
|