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
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"""PR review 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="/review", tags=["review"])
|
|
14
|
+
|
|
15
|
+
_executor = ThreadPoolExecutor(max_workers=2)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ReviewRequest(BaseModel):
|
|
19
|
+
"""Request for PR review."""
|
|
20
|
+
pr_number: Optional[int] = Field(default=None, description="PR number")
|
|
21
|
+
pr_url: Optional[str] = Field(default=None, description="PR URL")
|
|
22
|
+
search: Optional[str] = Field(default=None, description="Search for PR by text")
|
|
23
|
+
state: str = Field(default="open", description="PR state filter: open, closed, all")
|
|
24
|
+
model: Optional[str] = Field(default=None, description="LLM model to use")
|
|
25
|
+
post_review: bool = Field(default=False, description="Post review to GitHub")
|
|
26
|
+
verdict: bool = Field(default=False, description="Include APPROVE/REQUEST_CHANGES")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class ReviewComment(BaseModel):
|
|
30
|
+
"""A review comment."""
|
|
31
|
+
file: str
|
|
32
|
+
line: int
|
|
33
|
+
comment: str
|
|
34
|
+
severity: str # info, suggestion, warning, error
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class ReviewResponse(BaseModel):
|
|
38
|
+
"""PR review response."""
|
|
39
|
+
pr_number: int
|
|
40
|
+
pr_title: str
|
|
41
|
+
summary: str
|
|
42
|
+
comments: list[ReviewComment]
|
|
43
|
+
verdict: Optional[str] = None # APPROVE, REQUEST_CHANGES, COMMENT
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _run_review_sync(
|
|
47
|
+
pr_number: Optional[int],
|
|
48
|
+
model: Optional[str],
|
|
49
|
+
sse_handler: SSEHandler,
|
|
50
|
+
):
|
|
51
|
+
"""Run PR review synchronously."""
|
|
52
|
+
try:
|
|
53
|
+
from ..agent.code_reviewer import CodeReviewer
|
|
54
|
+
from ..agent.events import AgentEventEmitter
|
|
55
|
+
|
|
56
|
+
class SSEBridge:
|
|
57
|
+
def __init__(self, handler):
|
|
58
|
+
self._handler = handler
|
|
59
|
+
|
|
60
|
+
def handle(self, event):
|
|
61
|
+
self._handler.handle(event)
|
|
62
|
+
|
|
63
|
+
emitter = AgentEventEmitter(agent_name="Review")
|
|
64
|
+
emitter.add_handler(SSEBridge(sse_handler))
|
|
65
|
+
|
|
66
|
+
reviewer = CodeReviewer(model=model, emitter=emitter)
|
|
67
|
+
|
|
68
|
+
sse_handler.emit(EventType.PROGRESS, {
|
|
69
|
+
"step": "Fetching PR",
|
|
70
|
+
"percent": 10,
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
result = reviewer.review(pr_number=pr_number)
|
|
74
|
+
|
|
75
|
+
sse_handler.emit(EventType.RESPONSE, {
|
|
76
|
+
"pr_number": pr_number,
|
|
77
|
+
"pr_title": result.get("title", ""),
|
|
78
|
+
"summary": result.get("summary", ""),
|
|
79
|
+
"comments": result.get("comments", []),
|
|
80
|
+
"verdict": result.get("verdict"),
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
except Exception as e:
|
|
84
|
+
sse_handler.emit(EventType.ERROR, {"message": str(e)})
|
|
85
|
+
finally:
|
|
86
|
+
sse_handler.close()
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@router.post("")
|
|
90
|
+
async def review_pr(request: ReviewRequest):
|
|
91
|
+
"""Generate a PR review.
|
|
92
|
+
|
|
93
|
+
Analyzes the PR diff and generates review comments.
|
|
94
|
+
"""
|
|
95
|
+
if not request.pr_number and not request.pr_url and not request.search:
|
|
96
|
+
from fastapi import HTTPException
|
|
97
|
+
raise HTTPException(
|
|
98
|
+
status_code=400,
|
|
99
|
+
detail="Must provide pr_number, pr_url, or search"
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
sse_handler = SSEHandler(agent_name="Review")
|
|
103
|
+
|
|
104
|
+
sse_handler.emit(EventType.SESSION_START, {
|
|
105
|
+
"agent_name": "Review",
|
|
106
|
+
"pr_number": request.pr_number,
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
async def run():
|
|
110
|
+
loop = asyncio.get_event_loop()
|
|
111
|
+
await loop.run_in_executor(
|
|
112
|
+
_executor,
|
|
113
|
+
_run_review_sync,
|
|
114
|
+
request.pr_number,
|
|
115
|
+
request.model,
|
|
116
|
+
sse_handler,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
asyncio.create_task(run())
|
|
120
|
+
|
|
121
|
+
return StreamingResponse(
|
|
122
|
+
sse_handler,
|
|
123
|
+
media_type="text/event-stream",
|
|
124
|
+
headers={"Cache-Control": "no-cache", "Connection": "keep-alive"},
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@router.post("/create-profile")
|
|
129
|
+
async def create_reviewer_profile(
|
|
130
|
+
top_reviewers: int = 5,
|
|
131
|
+
top_contributors: int = 10,
|
|
132
|
+
max_prs: int = 50,
|
|
133
|
+
model: Optional[str] = None,
|
|
134
|
+
):
|
|
135
|
+
"""Create a reviewer profile by analyzing repository reviewers."""
|
|
136
|
+
sse_handler = SSEHandler(agent_name="ReviewerProfile")
|
|
137
|
+
|
|
138
|
+
async def run():
|
|
139
|
+
try:
|
|
140
|
+
from ..agent.reviewer_profile import ReviewerProfileBuilder
|
|
141
|
+
|
|
142
|
+
builder = ReviewerProfileBuilder(model=model)
|
|
143
|
+
result = builder.build(
|
|
144
|
+
top_reviewers=top_reviewers,
|
|
145
|
+
top_contributors=top_contributors,
|
|
146
|
+
max_prs=max_prs,
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
sse_handler.emit(EventType.RESPONSE, result)
|
|
150
|
+
except Exception as e:
|
|
151
|
+
sse_handler.emit(EventType.ERROR, {"message": str(e)})
|
|
152
|
+
finally:
|
|
153
|
+
sse_handler.close()
|
|
154
|
+
|
|
155
|
+
asyncio.create_task(run())
|
|
156
|
+
|
|
157
|
+
return StreamingResponse(
|
|
158
|
+
sse_handler,
|
|
159
|
+
media_type="text/event-stream",
|
|
160
|
+
headers={"Cache-Control": "no-cache", "Connection": "keep-alive"},
|
|
161
|
+
)
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Main API router combining all routes."""
|
|
2
|
+
|
|
3
|
+
from fastapi import APIRouter
|
|
4
|
+
|
|
5
|
+
from . import (
|
|
6
|
+
health,
|
|
7
|
+
agent,
|
|
8
|
+
agents,
|
|
9
|
+
auth,
|
|
10
|
+
db,
|
|
11
|
+
index,
|
|
12
|
+
search,
|
|
13
|
+
query,
|
|
14
|
+
analyze,
|
|
15
|
+
spec,
|
|
16
|
+
tasks,
|
|
17
|
+
plan,
|
|
18
|
+
team,
|
|
19
|
+
research,
|
|
20
|
+
review,
|
|
21
|
+
embed,
|
|
22
|
+
swarm,
|
|
23
|
+
rules,
|
|
24
|
+
context,
|
|
25
|
+
feature,
|
|
26
|
+
projectmd,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
api_router = APIRouter(prefix="/api")
|
|
30
|
+
|
|
31
|
+
# Health & status
|
|
32
|
+
api_router.include_router(health.router)
|
|
33
|
+
|
|
34
|
+
# Authentication
|
|
35
|
+
api_router.include_router(auth.router)
|
|
36
|
+
|
|
37
|
+
# Agent operations
|
|
38
|
+
api_router.include_router(agent.router)
|
|
39
|
+
api_router.include_router(agents.router)
|
|
40
|
+
|
|
41
|
+
# Database management
|
|
42
|
+
api_router.include_router(db.router)
|
|
43
|
+
|
|
44
|
+
# Indexing
|
|
45
|
+
api_router.include_router(index.router)
|
|
46
|
+
|
|
47
|
+
# Search & queries
|
|
48
|
+
api_router.include_router(search.router)
|
|
49
|
+
api_router.include_router(query.router)
|
|
50
|
+
|
|
51
|
+
# Analytics
|
|
52
|
+
api_router.include_router(analyze.router)
|
|
53
|
+
|
|
54
|
+
# Planning & specifications
|
|
55
|
+
api_router.include_router(spec.router)
|
|
56
|
+
api_router.include_router(tasks.router)
|
|
57
|
+
api_router.include_router(plan.router)
|
|
58
|
+
api_router.include_router(feature.router)
|
|
59
|
+
|
|
60
|
+
# Team & collaboration
|
|
61
|
+
api_router.include_router(team.router)
|
|
62
|
+
api_router.include_router(research.router)
|
|
63
|
+
api_router.include_router(review.router)
|
|
64
|
+
|
|
65
|
+
# Embeddings
|
|
66
|
+
api_router.include_router(embed.router)
|
|
67
|
+
|
|
68
|
+
# Multi-agent
|
|
69
|
+
api_router.include_router(swarm.router)
|
|
70
|
+
|
|
71
|
+
# Configuration
|
|
72
|
+
api_router.include_router(rules.router)
|
|
73
|
+
api_router.include_router(context.router)
|
|
74
|
+
|
|
75
|
+
# Documentation generation
|
|
76
|
+
api_router.include_router(projectmd.router)
|
emdash_core/api/rules.py
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""Template/rules management endpoints."""
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
from fastapi import APIRouter, HTTPException
|
|
6
|
+
from pydantic import BaseModel, Field
|
|
7
|
+
|
|
8
|
+
router = APIRouter(prefix="/rules", tags=["rules"])
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TemplateInfo(BaseModel):
|
|
12
|
+
"""Information about a template."""
|
|
13
|
+
name: str
|
|
14
|
+
source: str # built-in, global, local
|
|
15
|
+
path: Optional[str] = None
|
|
16
|
+
description: Optional[str] = None
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class TemplateContent(BaseModel):
|
|
20
|
+
"""Template content."""
|
|
21
|
+
name: str
|
|
22
|
+
content: str
|
|
23
|
+
source: str
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@router.get("/list")
|
|
27
|
+
async def list_templates():
|
|
28
|
+
"""List all templates and their active sources."""
|
|
29
|
+
templates = [
|
|
30
|
+
TemplateInfo(
|
|
31
|
+
name="spec",
|
|
32
|
+
source="built-in",
|
|
33
|
+
description="Feature specification template",
|
|
34
|
+
),
|
|
35
|
+
TemplateInfo(
|
|
36
|
+
name="tasks",
|
|
37
|
+
source="built-in",
|
|
38
|
+
description="Implementation tasks template",
|
|
39
|
+
),
|
|
40
|
+
TemplateInfo(
|
|
41
|
+
name="project",
|
|
42
|
+
source="built-in",
|
|
43
|
+
description="PROJECT.md template",
|
|
44
|
+
),
|
|
45
|
+
TemplateInfo(
|
|
46
|
+
name="focus",
|
|
47
|
+
source="built-in",
|
|
48
|
+
description="Team focus template",
|
|
49
|
+
),
|
|
50
|
+
TemplateInfo(
|
|
51
|
+
name="pr-review",
|
|
52
|
+
source="built-in",
|
|
53
|
+
description="PR review template",
|
|
54
|
+
),
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
# TODO: Check for custom templates in .emdash-rules
|
|
58
|
+
|
|
59
|
+
return {"templates": templates}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@router.get("/{template_name}", response_model=TemplateContent)
|
|
63
|
+
async def get_template(template_name: str):
|
|
64
|
+
"""Get a template's content."""
|
|
65
|
+
try:
|
|
66
|
+
from ..templates import load_template as get_template
|
|
67
|
+
|
|
68
|
+
content = get_template(template_name)
|
|
69
|
+
if not content:
|
|
70
|
+
raise HTTPException(status_code=404, detail=f"Template {template_name} not found")
|
|
71
|
+
|
|
72
|
+
return TemplateContent(
|
|
73
|
+
name=template_name,
|
|
74
|
+
content=content,
|
|
75
|
+
source="built-in", # TODO: Detect actual source
|
|
76
|
+
)
|
|
77
|
+
except HTTPException:
|
|
78
|
+
raise
|
|
79
|
+
except Exception as e:
|
|
80
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@router.post("/init")
|
|
84
|
+
async def init_templates(global_templates: bool = False, force: bool = False):
|
|
85
|
+
"""Initialize custom templates in .emdash-rules."""
|
|
86
|
+
try:
|
|
87
|
+
from pathlib import Path
|
|
88
|
+
|
|
89
|
+
# Determine target directory
|
|
90
|
+
if global_templates:
|
|
91
|
+
target = Path.home() / ".emdash-rules"
|
|
92
|
+
else:
|
|
93
|
+
target = Path.cwd() / ".emdash-rules"
|
|
94
|
+
|
|
95
|
+
if target.exists() and not force:
|
|
96
|
+
raise HTTPException(
|
|
97
|
+
status_code=400,
|
|
98
|
+
detail=f"Templates already exist at {target}. Use force=true to overwrite."
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
target.mkdir(parents=True, exist_ok=True)
|
|
102
|
+
|
|
103
|
+
# Copy built-in templates
|
|
104
|
+
from ..templates.loader import get_defaults_dir, copy_templates_to_dir, TEMPLATE_NAMES
|
|
105
|
+
|
|
106
|
+
templates_copied = copy_templates_to_dir(target, overwrite=force)
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
"success": True,
|
|
110
|
+
"path": str(target),
|
|
111
|
+
"templates": templates_copied,
|
|
112
|
+
}
|
|
113
|
+
except HTTPException:
|
|
114
|
+
raise
|
|
115
|
+
except Exception as e:
|
|
116
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""Search endpoints."""
|
|
2
|
+
|
|
3
|
+
from enum import Enum
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
from fastapi import APIRouter, HTTPException
|
|
7
|
+
from pydantic import BaseModel, Field
|
|
8
|
+
|
|
9
|
+
router = APIRouter(prefix="/search", tags=["search"])
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class SearchType(str, Enum):
|
|
13
|
+
"""Type of search to perform."""
|
|
14
|
+
SEMANTIC = "semantic"
|
|
15
|
+
TEXT = "text"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class SearchRequest(BaseModel):
|
|
19
|
+
"""Search request."""
|
|
20
|
+
repo_path: str = Field(..., description="Path to repository")
|
|
21
|
+
query: str = Field(..., description="Search query")
|
|
22
|
+
type: SearchType = Field(default=SearchType.SEMANTIC, description="Search type")
|
|
23
|
+
limit: int = Field(default=20, description="Maximum results")
|
|
24
|
+
entity_types: list[str] = Field(default_factory=list, description="Filter by types: File, Function, Class")
|
|
25
|
+
min_score: float = Field(default=0.0, description="Minimum similarity score")
|
|
26
|
+
include_importance: bool = Field(default=True, description="Include importance ranking")
|
|
27
|
+
include_snippets: bool = Field(default=True, description="Include code snippets")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class SearchResult(BaseModel):
|
|
31
|
+
"""A single search result."""
|
|
32
|
+
qualified_name: str
|
|
33
|
+
name: str
|
|
34
|
+
entity_type: str
|
|
35
|
+
file_path: str
|
|
36
|
+
line_number: Optional[int] = None
|
|
37
|
+
score: float
|
|
38
|
+
importance: Optional[float] = None
|
|
39
|
+
snippet: Optional[str] = None
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class SearchResponse(BaseModel):
|
|
43
|
+
"""Search response."""
|
|
44
|
+
results: list[SearchResult]
|
|
45
|
+
total: int
|
|
46
|
+
query: str
|
|
47
|
+
search_type: str
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _get_toolkit():
|
|
51
|
+
"""Get agent toolkit for search."""
|
|
52
|
+
from ..agent.toolkit import AgentToolkit
|
|
53
|
+
return AgentToolkit()
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@router.post("", response_model=SearchResponse)
|
|
57
|
+
async def search(request: SearchRequest):
|
|
58
|
+
"""Search the codebase.
|
|
59
|
+
|
|
60
|
+
Supports semantic search (by meaning) and text search (exact match).
|
|
61
|
+
"""
|
|
62
|
+
from pathlib import Path
|
|
63
|
+
from ..graph.connection import configure_for_repo
|
|
64
|
+
|
|
65
|
+
try:
|
|
66
|
+
# Configure database for the repo
|
|
67
|
+
repo_root = Path(request.repo_path).resolve()
|
|
68
|
+
configure_for_repo(repo_root)
|
|
69
|
+
|
|
70
|
+
toolkit = _get_toolkit()
|
|
71
|
+
|
|
72
|
+
if request.type == SearchType.SEMANTIC:
|
|
73
|
+
result = toolkit.search(
|
|
74
|
+
query=request.query,
|
|
75
|
+
limit=request.limit,
|
|
76
|
+
)
|
|
77
|
+
else:
|
|
78
|
+
result = toolkit.text_search(
|
|
79
|
+
query=request.query,
|
|
80
|
+
limit=request.limit,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
if not result.success:
|
|
84
|
+
raise HTTPException(status_code=500, detail=result.error)
|
|
85
|
+
|
|
86
|
+
# Convert results
|
|
87
|
+
results = []
|
|
88
|
+
for item in result.data.get("results", []):
|
|
89
|
+
results.append(SearchResult(
|
|
90
|
+
qualified_name=item.get("qualified_name", ""),
|
|
91
|
+
name=item.get("name", ""),
|
|
92
|
+
entity_type=item.get("type", ""),
|
|
93
|
+
file_path=item.get("file_path", ""),
|
|
94
|
+
line_number=item.get("line_number"),
|
|
95
|
+
score=item.get("score", 0.0),
|
|
96
|
+
importance=item.get("importance") if request.include_importance else None,
|
|
97
|
+
snippet=item.get("snippet") if request.include_snippets else None,
|
|
98
|
+
))
|
|
99
|
+
|
|
100
|
+
return SearchResponse(
|
|
101
|
+
results=results,
|
|
102
|
+
total=len(results),
|
|
103
|
+
query=request.query,
|
|
104
|
+
search_type=request.type.value,
|
|
105
|
+
)
|
|
106
|
+
except HTTPException:
|
|
107
|
+
raise
|
|
108
|
+
except Exception as e:
|
|
109
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@router.get("/quick")
|
|
113
|
+
async def quick_search(repo_path: str, q: str, limit: int = 10):
|
|
114
|
+
"""Quick semantic search endpoint.
|
|
115
|
+
|
|
116
|
+
Simple GET endpoint for quick searches.
|
|
117
|
+
"""
|
|
118
|
+
request = SearchRequest(repo_path=repo_path, query=q, limit=limit)
|
|
119
|
+
return await search(request)
|
emdash_core/api/spec.py
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""Feature specification 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="/spec", tags=["specification"])
|
|
14
|
+
|
|
15
|
+
_executor = ThreadPoolExecutor(max_workers=2)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class SpecRequest(BaseModel):
|
|
19
|
+
"""Request to generate a feature specification."""
|
|
20
|
+
feature: str = Field(..., description="Feature description")
|
|
21
|
+
project_md: Optional[str] = Field(default=None, description="PROJECT.md content")
|
|
22
|
+
model: Optional[str] = Field(default=None, description="LLM model to use")
|
|
23
|
+
verbose: bool = Field(default=False, description="Verbose output")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class SpecResponse(BaseModel):
|
|
27
|
+
"""Feature specification response."""
|
|
28
|
+
title: str
|
|
29
|
+
content: str
|
|
30
|
+
file_path: Optional[str] = None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _run_spec_sync(
|
|
34
|
+
feature: str,
|
|
35
|
+
model: Optional[str],
|
|
36
|
+
sse_handler: SSEHandler,
|
|
37
|
+
):
|
|
38
|
+
"""Run spec generation synchronously."""
|
|
39
|
+
try:
|
|
40
|
+
from ..agent.specification import SpecificationAgent
|
|
41
|
+
from ..agent.events import AgentEventEmitter
|
|
42
|
+
|
|
43
|
+
# Bridge events to SSE
|
|
44
|
+
class SSEBridge:
|
|
45
|
+
def __init__(self, handler):
|
|
46
|
+
self._handler = handler
|
|
47
|
+
|
|
48
|
+
def handle(self, event):
|
|
49
|
+
self._handler.handle(event)
|
|
50
|
+
|
|
51
|
+
emitter = AgentEventEmitter(agent_name="Specification")
|
|
52
|
+
emitter.add_handler(SSEBridge(sse_handler))
|
|
53
|
+
|
|
54
|
+
# Use default model if not specified
|
|
55
|
+
from ..agent.providers.factory import DEFAULT_MODEL
|
|
56
|
+
agent = SpecificationAgent(model=model or DEFAULT_MODEL, emitter=emitter, interactive=False)
|
|
57
|
+
spec = agent.generate_spec(feature)
|
|
58
|
+
|
|
59
|
+
sse_handler.emit(EventType.RESPONSE, {
|
|
60
|
+
"title": spec.title,
|
|
61
|
+
"content": spec.content,
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
except Exception as e:
|
|
65
|
+
sse_handler.emit(EventType.ERROR, {"message": str(e)})
|
|
66
|
+
finally:
|
|
67
|
+
sse_handler.close()
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@router.post("/generate")
|
|
71
|
+
async def generate_spec(request: SpecRequest):
|
|
72
|
+
"""Generate a feature specification with SSE streaming.
|
|
73
|
+
|
|
74
|
+
Returns a detailed specification for implementing a feature.
|
|
75
|
+
"""
|
|
76
|
+
sse_handler = SSEHandler(agent_name="Specification")
|
|
77
|
+
|
|
78
|
+
sse_handler.emit(EventType.SESSION_START, {
|
|
79
|
+
"agent_name": "Specification",
|
|
80
|
+
"feature": request.feature,
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
async def run():
|
|
84
|
+
loop = asyncio.get_event_loop()
|
|
85
|
+
await loop.run_in_executor(
|
|
86
|
+
_executor,
|
|
87
|
+
_run_spec_sync,
|
|
88
|
+
request.feature,
|
|
89
|
+
request.model,
|
|
90
|
+
sse_handler,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
asyncio.create_task(run())
|
|
94
|
+
|
|
95
|
+
return StreamingResponse(
|
|
96
|
+
sse_handler,
|
|
97
|
+
media_type="text/event-stream",
|
|
98
|
+
headers={"Cache-Control": "no-cache", "Connection": "keep-alive"},
|
|
99
|
+
)
|