emdash-core 0.1.7__py3-none-any.whl
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.
- emdash_core/__init__.py +3 -0
- emdash_core/agent/__init__.py +37 -0
- emdash_core/agent/agents.py +225 -0
- emdash_core/agent/code_reviewer.py +476 -0
- emdash_core/agent/compaction.py +143 -0
- emdash_core/agent/context_manager.py +140 -0
- emdash_core/agent/events.py +338 -0
- emdash_core/agent/handlers.py +224 -0
- emdash_core/agent/inprocess_subagent.py +377 -0
- emdash_core/agent/mcp/__init__.py +50 -0
- emdash_core/agent/mcp/client.py +346 -0
- emdash_core/agent/mcp/config.py +302 -0
- emdash_core/agent/mcp/manager.py +496 -0
- emdash_core/agent/mcp/tool_factory.py +213 -0
- emdash_core/agent/prompts/__init__.py +38 -0
- emdash_core/agent/prompts/main_agent.py +104 -0
- emdash_core/agent/prompts/subagents.py +131 -0
- emdash_core/agent/prompts/workflow.py +136 -0
- emdash_core/agent/providers/__init__.py +34 -0
- emdash_core/agent/providers/base.py +143 -0
- emdash_core/agent/providers/factory.py +80 -0
- emdash_core/agent/providers/models.py +220 -0
- emdash_core/agent/providers/openai_provider.py +463 -0
- emdash_core/agent/providers/transformers_provider.py +217 -0
- emdash_core/agent/research/__init__.py +81 -0
- emdash_core/agent/research/agent.py +143 -0
- emdash_core/agent/research/controller.py +254 -0
- emdash_core/agent/research/critic.py +428 -0
- emdash_core/agent/research/macros.py +469 -0
- emdash_core/agent/research/planner.py +449 -0
- emdash_core/agent/research/researcher.py +436 -0
- emdash_core/agent/research/state.py +523 -0
- emdash_core/agent/research/synthesizer.py +594 -0
- emdash_core/agent/reviewer_profile.py +475 -0
- emdash_core/agent/rules.py +123 -0
- emdash_core/agent/runner.py +601 -0
- emdash_core/agent/session.py +262 -0
- emdash_core/agent/spec_schema.py +66 -0
- emdash_core/agent/specification.py +479 -0
- emdash_core/agent/subagent.py +397 -0
- emdash_core/agent/subagent_prompts.py +13 -0
- emdash_core/agent/toolkit.py +482 -0
- emdash_core/agent/toolkits/__init__.py +64 -0
- emdash_core/agent/toolkits/base.py +96 -0
- emdash_core/agent/toolkits/explore.py +47 -0
- emdash_core/agent/toolkits/plan.py +55 -0
- emdash_core/agent/tools/__init__.py +141 -0
- emdash_core/agent/tools/analytics.py +436 -0
- emdash_core/agent/tools/base.py +131 -0
- emdash_core/agent/tools/coding.py +484 -0
- emdash_core/agent/tools/github_mcp.py +592 -0
- emdash_core/agent/tools/history.py +13 -0
- emdash_core/agent/tools/modes.py +153 -0
- emdash_core/agent/tools/plan.py +206 -0
- emdash_core/agent/tools/plan_write.py +135 -0
- emdash_core/agent/tools/search.py +412 -0
- emdash_core/agent/tools/spec.py +341 -0
- emdash_core/agent/tools/task.py +262 -0
- emdash_core/agent/tools/task_output.py +204 -0
- emdash_core/agent/tools/tasks.py +454 -0
- emdash_core/agent/tools/traversal.py +588 -0
- emdash_core/agent/tools/web.py +179 -0
- emdash_core/analytics/__init__.py +5 -0
- emdash_core/analytics/engine.py +1286 -0
- emdash_core/api/__init__.py +5 -0
- emdash_core/api/agent.py +308 -0
- emdash_core/api/agents.py +154 -0
- emdash_core/api/analyze.py +264 -0
- emdash_core/api/auth.py +173 -0
- emdash_core/api/context.py +77 -0
- emdash_core/api/db.py +121 -0
- emdash_core/api/embed.py +131 -0
- emdash_core/api/feature.py +143 -0
- emdash_core/api/health.py +93 -0
- emdash_core/api/index.py +162 -0
- emdash_core/api/plan.py +110 -0
- emdash_core/api/projectmd.py +210 -0
- emdash_core/api/query.py +320 -0
- emdash_core/api/research.py +122 -0
- emdash_core/api/review.py +161 -0
- emdash_core/api/router.py +76 -0
- emdash_core/api/rules.py +116 -0
- emdash_core/api/search.py +119 -0
- emdash_core/api/spec.py +99 -0
- emdash_core/api/swarm.py +223 -0
- emdash_core/api/tasks.py +109 -0
- emdash_core/api/team.py +120 -0
- emdash_core/auth/__init__.py +17 -0
- emdash_core/auth/github.py +389 -0
- emdash_core/config.py +74 -0
- emdash_core/context/__init__.py +52 -0
- emdash_core/context/models.py +50 -0
- emdash_core/context/providers/__init__.py +11 -0
- emdash_core/context/providers/base.py +74 -0
- emdash_core/context/providers/explored_areas.py +183 -0
- emdash_core/context/providers/touched_areas.py +360 -0
- emdash_core/context/registry.py +73 -0
- emdash_core/context/reranker.py +199 -0
- emdash_core/context/service.py +260 -0
- emdash_core/context/session.py +352 -0
- emdash_core/core/__init__.py +104 -0
- emdash_core/core/config.py +454 -0
- emdash_core/core/exceptions.py +55 -0
- emdash_core/core/models.py +265 -0
- emdash_core/core/review_config.py +57 -0
- emdash_core/db/__init__.py +67 -0
- emdash_core/db/auth.py +134 -0
- emdash_core/db/models.py +91 -0
- emdash_core/db/provider.py +222 -0
- emdash_core/db/providers/__init__.py +5 -0
- emdash_core/db/providers/supabase.py +452 -0
- emdash_core/embeddings/__init__.py +24 -0
- emdash_core/embeddings/indexer.py +534 -0
- emdash_core/embeddings/models.py +192 -0
- emdash_core/embeddings/providers/__init__.py +7 -0
- emdash_core/embeddings/providers/base.py +112 -0
- emdash_core/embeddings/providers/fireworks.py +141 -0
- emdash_core/embeddings/providers/openai.py +104 -0
- emdash_core/embeddings/registry.py +146 -0
- emdash_core/embeddings/service.py +215 -0
- emdash_core/graph/__init__.py +26 -0
- emdash_core/graph/builder.py +134 -0
- emdash_core/graph/connection.py +692 -0
- emdash_core/graph/schema.py +416 -0
- emdash_core/graph/writer.py +667 -0
- emdash_core/ingestion/__init__.py +7 -0
- emdash_core/ingestion/change_detector.py +150 -0
- emdash_core/ingestion/git/__init__.py +5 -0
- emdash_core/ingestion/git/commit_analyzer.py +196 -0
- emdash_core/ingestion/github/__init__.py +6 -0
- emdash_core/ingestion/github/pr_fetcher.py +296 -0
- emdash_core/ingestion/github/task_extractor.py +100 -0
- emdash_core/ingestion/orchestrator.py +540 -0
- emdash_core/ingestion/parsers/__init__.py +10 -0
- emdash_core/ingestion/parsers/base_parser.py +66 -0
- emdash_core/ingestion/parsers/call_graph_builder.py +121 -0
- emdash_core/ingestion/parsers/class_extractor.py +154 -0
- emdash_core/ingestion/parsers/function_extractor.py +202 -0
- emdash_core/ingestion/parsers/import_analyzer.py +119 -0
- emdash_core/ingestion/parsers/python_parser.py +123 -0
- emdash_core/ingestion/parsers/registry.py +72 -0
- emdash_core/ingestion/parsers/ts_ast_parser.js +313 -0
- emdash_core/ingestion/parsers/typescript_parser.py +278 -0
- emdash_core/ingestion/repository.py +346 -0
- emdash_core/models/__init__.py +38 -0
- emdash_core/models/agent.py +68 -0
- emdash_core/models/index.py +77 -0
- emdash_core/models/query.py +113 -0
- emdash_core/planning/__init__.py +7 -0
- emdash_core/planning/agent_api.py +413 -0
- emdash_core/planning/context_builder.py +265 -0
- emdash_core/planning/feature_context.py +232 -0
- emdash_core/planning/feature_expander.py +646 -0
- emdash_core/planning/llm_explainer.py +198 -0
- emdash_core/planning/similarity.py +509 -0
- emdash_core/planning/team_focus.py +821 -0
- emdash_core/server.py +153 -0
- emdash_core/sse/__init__.py +5 -0
- emdash_core/sse/stream.py +196 -0
- emdash_core/swarm/__init__.py +17 -0
- emdash_core/swarm/merge_agent.py +383 -0
- emdash_core/swarm/session_manager.py +274 -0
- emdash_core/swarm/swarm_runner.py +226 -0
- emdash_core/swarm/task_definition.py +137 -0
- emdash_core/swarm/worker_spawner.py +319 -0
- emdash_core/swarm/worktree_manager.py +278 -0
- emdash_core/templates/__init__.py +10 -0
- emdash_core/templates/defaults/agent-builder.md.template +82 -0
- emdash_core/templates/defaults/focus.md.template +115 -0
- emdash_core/templates/defaults/pr-review-enhanced.md.template +309 -0
- emdash_core/templates/defaults/pr-review.md.template +80 -0
- emdash_core/templates/defaults/project.md.template +85 -0
- emdash_core/templates/defaults/research_critic.md.template +112 -0
- emdash_core/templates/defaults/research_planner.md.template +85 -0
- emdash_core/templates/defaults/research_synthesizer.md.template +128 -0
- emdash_core/templates/defaults/reviewer.md.template +81 -0
- emdash_core/templates/defaults/spec.md.template +41 -0
- emdash_core/templates/defaults/tasks.md.template +78 -0
- emdash_core/templates/loader.py +296 -0
- emdash_core/utils/__init__.py +45 -0
- emdash_core/utils/git.py +84 -0
- emdash_core/utils/image.py +502 -0
- emdash_core/utils/logger.py +51 -0
- emdash_core-0.1.7.dist-info/METADATA +35 -0
- emdash_core-0.1.7.dist-info/RECORD +187 -0
- emdash_core-0.1.7.dist-info/WHEEL +4 -0
- emdash_core-0.1.7.dist-info/entry_points.txt +3 -0
emdash_core/api/swarm.py
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
"""Swarm (multi-agent) endpoints with SSE streaming."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
from fastapi import APIRouter, HTTPException
|
|
8
|
+
from fastapi.responses import StreamingResponse
|
|
9
|
+
from pydantic import BaseModel, Field
|
|
10
|
+
|
|
11
|
+
from ..sse.stream import SSEHandler, EventType
|
|
12
|
+
|
|
13
|
+
router = APIRouter(prefix="/swarm", tags=["swarm"])
|
|
14
|
+
|
|
15
|
+
_executor = ThreadPoolExecutor(max_workers=1)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class SwarmRequest(BaseModel):
|
|
19
|
+
"""Request to run swarm."""
|
|
20
|
+
tasks: list[str] = Field(..., description="List of tasks to run in parallel")
|
|
21
|
+
model: Optional[str] = Field(default=None, description="LLM model")
|
|
22
|
+
workers: int = Field(default=3, description="Number of parallel workers")
|
|
23
|
+
timeout: int = Field(default=300, description="Timeout per task in seconds")
|
|
24
|
+
base_branch: Optional[str] = Field(default=None, description="Base branch")
|
|
25
|
+
auto_merge: bool = Field(default=True, description="Auto-merge completed branches")
|
|
26
|
+
llm_merge: bool = Field(default=False, description="Use LLM for merge conflicts")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class SwarmStatus(BaseModel):
|
|
30
|
+
"""Status of swarm execution."""
|
|
31
|
+
is_running: bool
|
|
32
|
+
tasks_total: int
|
|
33
|
+
tasks_completed: int
|
|
34
|
+
tasks_failed: int
|
|
35
|
+
current_tasks: list[str]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class SwarmSession(BaseModel):
|
|
39
|
+
"""An active swarm session."""
|
|
40
|
+
id: str
|
|
41
|
+
task: str
|
|
42
|
+
status: str # running, completed, failed
|
|
43
|
+
branch: Optional[str] = None
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _run_swarm_sync(
|
|
47
|
+
tasks: list[str],
|
|
48
|
+
model: Optional[str],
|
|
49
|
+
workers: int,
|
|
50
|
+
sse_handler: SSEHandler,
|
|
51
|
+
):
|
|
52
|
+
"""Run swarm synchronously."""
|
|
53
|
+
import sys
|
|
54
|
+
from pathlib import Path
|
|
55
|
+
|
|
56
|
+
repo_root = Path(__file__).parent.parent.parent.parent.parent
|
|
57
|
+
if str(repo_root) not in sys.path:
|
|
58
|
+
sys.path.insert(0, str(repo_root))
|
|
59
|
+
|
|
60
|
+
try:
|
|
61
|
+
from ..swarm.swarm_runner import SwarmRunner
|
|
62
|
+
from ..agent.events import AgentEventEmitter
|
|
63
|
+
|
|
64
|
+
class SSEBridge:
|
|
65
|
+
def __init__(self, handler):
|
|
66
|
+
self._handler = handler
|
|
67
|
+
|
|
68
|
+
def handle(self, event):
|
|
69
|
+
self._handler.handle(event)
|
|
70
|
+
|
|
71
|
+
emitter = AgentEventEmitter(agent_name="Swarm")
|
|
72
|
+
emitter.add_handler(SSEBridge(sse_handler))
|
|
73
|
+
|
|
74
|
+
runner = SwarmRunner(
|
|
75
|
+
repo_root=repo_root,
|
|
76
|
+
model=model or "gpt-4o-mini",
|
|
77
|
+
max_workers=workers,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
sse_handler.emit(EventType.PROGRESS, {
|
|
81
|
+
"step": f"Starting {len(tasks)} tasks with {workers} workers",
|
|
82
|
+
"percent": 0,
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
state = runner.run(tasks)
|
|
86
|
+
|
|
87
|
+
# Count results from swarm state
|
|
88
|
+
from ..swarm.task_definition import TaskStatus
|
|
89
|
+
completed = sum(1 for t in state.tasks if t.status == TaskStatus.COMPLETED)
|
|
90
|
+
failed = sum(1 for t in state.tasks if t.status == TaskStatus.FAILED)
|
|
91
|
+
|
|
92
|
+
sse_handler.emit(EventType.RESPONSE, {
|
|
93
|
+
"completed": completed,
|
|
94
|
+
"failed": failed,
|
|
95
|
+
"results": [{"slug": t.slug, "status": t.status.value} for t in state.tasks],
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
except Exception as e:
|
|
99
|
+
sse_handler.emit(EventType.ERROR, {"message": str(e)})
|
|
100
|
+
finally:
|
|
101
|
+
sse_handler.close()
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@router.post("/run")
|
|
105
|
+
async def run_swarm(request: SwarmRequest):
|
|
106
|
+
"""Run multiple agents in parallel on separate tasks.
|
|
107
|
+
|
|
108
|
+
Each task runs in its own git worktree branch.
|
|
109
|
+
"""
|
|
110
|
+
if not request.tasks:
|
|
111
|
+
raise HTTPException(status_code=400, detail="No tasks provided")
|
|
112
|
+
|
|
113
|
+
sse_handler = SSEHandler(agent_name="Swarm")
|
|
114
|
+
|
|
115
|
+
sse_handler.emit(EventType.SESSION_START, {
|
|
116
|
+
"agent_name": "Swarm",
|
|
117
|
+
"task_count": len(request.tasks),
|
|
118
|
+
"workers": request.workers,
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
async def run():
|
|
122
|
+
loop = asyncio.get_event_loop()
|
|
123
|
+
await loop.run_in_executor(
|
|
124
|
+
_executor,
|
|
125
|
+
_run_swarm_sync,
|
|
126
|
+
request.tasks,
|
|
127
|
+
request.model,
|
|
128
|
+
request.workers,
|
|
129
|
+
sse_handler,
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
asyncio.create_task(run())
|
|
133
|
+
|
|
134
|
+
return StreamingResponse(
|
|
135
|
+
sse_handler,
|
|
136
|
+
media_type="text/event-stream",
|
|
137
|
+
headers={"Cache-Control": "no-cache", "Connection": "keep-alive"},
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
@router.get("/status", response_model=SwarmStatus)
|
|
142
|
+
async def get_swarm_status():
|
|
143
|
+
"""Get status of current swarm execution."""
|
|
144
|
+
# TODO: Implement actual status tracking
|
|
145
|
+
return SwarmStatus(
|
|
146
|
+
is_running=False,
|
|
147
|
+
tasks_total=0,
|
|
148
|
+
tasks_completed=0,
|
|
149
|
+
tasks_failed=0,
|
|
150
|
+
current_tasks=[],
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
@router.get("/sessions")
|
|
155
|
+
async def get_swarm_sessions():
|
|
156
|
+
"""List active swarm sessions."""
|
|
157
|
+
# TODO: Implement session tracking
|
|
158
|
+
return {"sessions": []}
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
@router.post("/cleanup")
|
|
162
|
+
async def cleanup_swarm(force: bool = False):
|
|
163
|
+
"""Clean up all swarm worktrees and branches."""
|
|
164
|
+
try:
|
|
165
|
+
from pathlib import Path
|
|
166
|
+
from ..swarm.swarm_runner import SwarmRunner
|
|
167
|
+
|
|
168
|
+
repo_root = Path(__file__).parent.parent.parent.parent.parent
|
|
169
|
+
runner = SwarmRunner.load(repo_root)
|
|
170
|
+
|
|
171
|
+
if runner:
|
|
172
|
+
cleaned = runner.cleanup()
|
|
173
|
+
else:
|
|
174
|
+
# No active swarm, just cleanup orphaned worktrees
|
|
175
|
+
from ..swarm.worktree_manager import WorktreeManager
|
|
176
|
+
manager = WorktreeManager(repo_root)
|
|
177
|
+
cleaned = manager.cleanup_all()
|
|
178
|
+
|
|
179
|
+
return {
|
|
180
|
+
"success": True,
|
|
181
|
+
"cleaned_worktrees": cleaned,
|
|
182
|
+
}
|
|
183
|
+
except Exception as e:
|
|
184
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
@router.post("/merge")
|
|
188
|
+
async def merge_swarm_branches(
|
|
189
|
+
llm_merge: bool = False,
|
|
190
|
+
target: Optional[str] = None,
|
|
191
|
+
):
|
|
192
|
+
"""Merge all completed task branches."""
|
|
193
|
+
try:
|
|
194
|
+
from pathlib import Path
|
|
195
|
+
from ..swarm.swarm_runner import SwarmRunner
|
|
196
|
+
|
|
197
|
+
repo_root = Path(__file__).parent.parent.parent.parent.parent
|
|
198
|
+
runner = SwarmRunner.load(repo_root)
|
|
199
|
+
|
|
200
|
+
if not runner:
|
|
201
|
+
return {
|
|
202
|
+
"success": False,
|
|
203
|
+
"error": "No active swarm found",
|
|
204
|
+
"merged_branches": [],
|
|
205
|
+
"conflicts": [],
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
results = runner.merge_completed(
|
|
209
|
+
use_llm=llm_merge,
|
|
210
|
+
target_branch=target,
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
merged = [r.task_id for r in results if r.success]
|
|
214
|
+
failed = [{"task_id": r.task_id, "conflicts": r.conflicts, "error": r.error_message}
|
|
215
|
+
for r in results if not r.success]
|
|
216
|
+
|
|
217
|
+
return {
|
|
218
|
+
"success": len(failed) == 0,
|
|
219
|
+
"merged_tasks": merged,
|
|
220
|
+
"failed": failed,
|
|
221
|
+
}
|
|
222
|
+
except Exception as e:
|
|
223
|
+
raise HTTPException(status_code=500, detail=str(e))
|
emdash_core/api/tasks.py
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""Task generation endpoints with SSE streaming."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
from fastapi import APIRouter
|
|
8
|
+
from fastapi.responses import StreamingResponse
|
|
9
|
+
from pydantic import BaseModel, Field
|
|
10
|
+
|
|
11
|
+
from ..sse.stream import SSEHandler, EventType
|
|
12
|
+
|
|
13
|
+
router = APIRouter(prefix="/tasks", tags=["tasks"])
|
|
14
|
+
|
|
15
|
+
_executor = ThreadPoolExecutor(max_workers=2)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class TasksRequest(BaseModel):
|
|
19
|
+
"""Request to generate implementation tasks."""
|
|
20
|
+
spec_name: Optional[str] = Field(default=None, description="Specification name")
|
|
21
|
+
spec_content: Optional[str] = Field(default=None, description="Specification content")
|
|
22
|
+
project_md: Optional[str] = Field(default=None, description="PROJECT.md content")
|
|
23
|
+
model: Optional[str] = Field(default=None, description="LLM model to use")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class Task(BaseModel):
|
|
27
|
+
"""A single implementation task."""
|
|
28
|
+
id: int
|
|
29
|
+
title: str
|
|
30
|
+
description: str
|
|
31
|
+
files: list[str] = Field(default_factory=list)
|
|
32
|
+
dependencies: list[int] = Field(default_factory=list)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class TasksResponse(BaseModel):
|
|
36
|
+
"""Task generation response."""
|
|
37
|
+
tasks: list[Task]
|
|
38
|
+
total: int
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _run_tasks_sync(
|
|
42
|
+
spec_content: Optional[str],
|
|
43
|
+
model: Optional[str],
|
|
44
|
+
sse_handler: SSEHandler,
|
|
45
|
+
):
|
|
46
|
+
"""Run task generation synchronously."""
|
|
47
|
+
import sys
|
|
48
|
+
from pathlib import Path
|
|
49
|
+
|
|
50
|
+
try:
|
|
51
|
+
# Note: ImplementationAgent is actually ImplementationPlanAgent in the module
|
|
52
|
+
# The generate_tasks method needs to be added or this endpoint refactored
|
|
53
|
+
from ..agent.events import AgentEventEmitter
|
|
54
|
+
|
|
55
|
+
class SSEBridge:
|
|
56
|
+
def __init__(self, handler):
|
|
57
|
+
self._handler = handler
|
|
58
|
+
|
|
59
|
+
def handle(self, event):
|
|
60
|
+
self._handler.handle(event)
|
|
61
|
+
|
|
62
|
+
emitter = AgentEventEmitter(agent_name="Tasks")
|
|
63
|
+
emitter.add_handler(SSEBridge(sse_handler))
|
|
64
|
+
|
|
65
|
+
agent = ImplementationAgent(model=model, emitter=emitter)
|
|
66
|
+
result = agent.generate_tasks(spec_content or "")
|
|
67
|
+
|
|
68
|
+
tasks = result.get("tasks", [])
|
|
69
|
+
sse_handler.emit(EventType.RESPONSE, {
|
|
70
|
+
"tasks": tasks,
|
|
71
|
+
"total": len(tasks),
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
except Exception as e:
|
|
75
|
+
sse_handler.emit(EventType.ERROR, {"message": str(e)})
|
|
76
|
+
finally:
|
|
77
|
+
sse_handler.close()
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@router.post("/generate")
|
|
81
|
+
async def generate_tasks(request: TasksRequest):
|
|
82
|
+
"""Generate implementation tasks from a specification.
|
|
83
|
+
|
|
84
|
+
Returns a list of tasks with dependencies for implementing the spec.
|
|
85
|
+
"""
|
|
86
|
+
sse_handler = SSEHandler(agent_name="Tasks")
|
|
87
|
+
|
|
88
|
+
sse_handler.emit(EventType.SESSION_START, {
|
|
89
|
+
"agent_name": "Tasks",
|
|
90
|
+
"spec_name": request.spec_name,
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
async def run():
|
|
94
|
+
loop = asyncio.get_event_loop()
|
|
95
|
+
await loop.run_in_executor(
|
|
96
|
+
_executor,
|
|
97
|
+
_run_tasks_sync,
|
|
98
|
+
request.spec_content,
|
|
99
|
+
request.model,
|
|
100
|
+
sse_handler,
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
asyncio.create_task(run())
|
|
104
|
+
|
|
105
|
+
return StreamingResponse(
|
|
106
|
+
sse_handler,
|
|
107
|
+
media_type="text/event-stream",
|
|
108
|
+
headers={"Cache-Control": "no-cache", "Connection": "keep-alive"},
|
|
109
|
+
)
|
emdash_core/api/team.py
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""Team analytics endpoints with SSE streaming."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
from fastapi import APIRouter
|
|
8
|
+
from fastapi.responses import StreamingResponse
|
|
9
|
+
from pydantic import BaseModel, Field
|
|
10
|
+
|
|
11
|
+
from ..sse.stream import SSEHandler, EventType
|
|
12
|
+
|
|
13
|
+
router = APIRouter(prefix="/team", tags=["team"])
|
|
14
|
+
|
|
15
|
+
_executor = ThreadPoolExecutor(max_workers=2)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class TeamFocusRequest(BaseModel):
|
|
19
|
+
"""Request for team focus analysis."""
|
|
20
|
+
days: int = Field(default=7, description="Number of days to analyze")
|
|
21
|
+
model: Optional[str] = Field(default=None, description="LLM model for summaries")
|
|
22
|
+
include_graph: bool = Field(default=True, description="Include graph analysis")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class AuthorFocus(BaseModel):
|
|
26
|
+
"""Focus area for an author."""
|
|
27
|
+
author: str
|
|
28
|
+
email: str
|
|
29
|
+
commit_count: int
|
|
30
|
+
files_touched: int
|
|
31
|
+
areas: list[str]
|
|
32
|
+
recent_work: str
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class TeamFocusResponse(BaseModel):
|
|
36
|
+
"""Team focus response."""
|
|
37
|
+
period_days: int
|
|
38
|
+
authors: list[AuthorFocus]
|
|
39
|
+
summary: Optional[str] = None
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _run_team_focus_sync(
|
|
43
|
+
days: int,
|
|
44
|
+
model: Optional[str],
|
|
45
|
+
include_graph: bool,
|
|
46
|
+
sse_handler: SSEHandler,
|
|
47
|
+
):
|
|
48
|
+
"""Run team focus analysis synchronously."""
|
|
49
|
+
try:
|
|
50
|
+
from ..planning.team_focus import TeamFocusAnalyzer
|
|
51
|
+
|
|
52
|
+
analyzer = TeamFocusAnalyzer(model=model)
|
|
53
|
+
|
|
54
|
+
sse_handler.emit(EventType.PROGRESS, {
|
|
55
|
+
"step": "Analyzing git history",
|
|
56
|
+
"percent": 20,
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
result = analyzer.analyze(days=days, include_graph=include_graph)
|
|
60
|
+
|
|
61
|
+
sse_handler.emit(EventType.PROGRESS, {
|
|
62
|
+
"step": "Generating summary",
|
|
63
|
+
"percent": 80,
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
authors = [
|
|
67
|
+
AuthorFocus(
|
|
68
|
+
author=a.get("name", ""),
|
|
69
|
+
email=a.get("email", ""),
|
|
70
|
+
commit_count=a.get("commit_count", 0),
|
|
71
|
+
files_touched=a.get("files_touched", 0),
|
|
72
|
+
areas=a.get("areas", []),
|
|
73
|
+
recent_work=a.get("summary", ""),
|
|
74
|
+
)
|
|
75
|
+
for a in result.get("authors", [])
|
|
76
|
+
]
|
|
77
|
+
|
|
78
|
+
sse_handler.emit(EventType.RESPONSE, {
|
|
79
|
+
"period_days": days,
|
|
80
|
+
"authors": [a.model_dump() for a in authors],
|
|
81
|
+
"summary": result.get("summary"),
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
except Exception as e:
|
|
85
|
+
sse_handler.emit(EventType.ERROR, {"message": str(e)})
|
|
86
|
+
finally:
|
|
87
|
+
sse_handler.close()
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@router.post("/focus")
|
|
91
|
+
async def get_team_focus(request: TeamFocusRequest):
|
|
92
|
+
"""Get team's recent focus and work-in-progress.
|
|
93
|
+
|
|
94
|
+
Analyzes git history to understand what each team member is working on.
|
|
95
|
+
"""
|
|
96
|
+
sse_handler = SSEHandler(agent_name="TeamFocus")
|
|
97
|
+
|
|
98
|
+
sse_handler.emit(EventType.SESSION_START, {
|
|
99
|
+
"agent_name": "TeamFocus",
|
|
100
|
+
"days": request.days,
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
async def run():
|
|
104
|
+
loop = asyncio.get_event_loop()
|
|
105
|
+
await loop.run_in_executor(
|
|
106
|
+
_executor,
|
|
107
|
+
_run_team_focus_sync,
|
|
108
|
+
request.days,
|
|
109
|
+
request.model,
|
|
110
|
+
request.include_graph,
|
|
111
|
+
sse_handler,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
asyncio.create_task(run())
|
|
115
|
+
|
|
116
|
+
return StreamingResponse(
|
|
117
|
+
sse_handler,
|
|
118
|
+
media_type="text/event-stream",
|
|
119
|
+
headers={"Cache-Control": "no-cache", "Connection": "keep-alive"},
|
|
120
|
+
)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""GitHub OAuth authentication for EmDash."""
|
|
2
|
+
|
|
3
|
+
from .github import (
|
|
4
|
+
GitHubAuth,
|
|
5
|
+
AuthConfig,
|
|
6
|
+
get_github_token,
|
|
7
|
+
is_authenticated,
|
|
8
|
+
get_auth_status,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"GitHubAuth",
|
|
13
|
+
"AuthConfig",
|
|
14
|
+
"get_github_token",
|
|
15
|
+
"is_authenticated",
|
|
16
|
+
"get_auth_status",
|
|
17
|
+
]
|