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.
Files changed (187) hide show
  1. emdash_core/__init__.py +3 -0
  2. emdash_core/agent/__init__.py +37 -0
  3. emdash_core/agent/agents.py +225 -0
  4. emdash_core/agent/code_reviewer.py +476 -0
  5. emdash_core/agent/compaction.py +143 -0
  6. emdash_core/agent/context_manager.py +140 -0
  7. emdash_core/agent/events.py +338 -0
  8. emdash_core/agent/handlers.py +224 -0
  9. emdash_core/agent/inprocess_subagent.py +377 -0
  10. emdash_core/agent/mcp/__init__.py +50 -0
  11. emdash_core/agent/mcp/client.py +346 -0
  12. emdash_core/agent/mcp/config.py +302 -0
  13. emdash_core/agent/mcp/manager.py +496 -0
  14. emdash_core/agent/mcp/tool_factory.py +213 -0
  15. emdash_core/agent/prompts/__init__.py +38 -0
  16. emdash_core/agent/prompts/main_agent.py +104 -0
  17. emdash_core/agent/prompts/subagents.py +131 -0
  18. emdash_core/agent/prompts/workflow.py +136 -0
  19. emdash_core/agent/providers/__init__.py +34 -0
  20. emdash_core/agent/providers/base.py +143 -0
  21. emdash_core/agent/providers/factory.py +80 -0
  22. emdash_core/agent/providers/models.py +220 -0
  23. emdash_core/agent/providers/openai_provider.py +463 -0
  24. emdash_core/agent/providers/transformers_provider.py +217 -0
  25. emdash_core/agent/research/__init__.py +81 -0
  26. emdash_core/agent/research/agent.py +143 -0
  27. emdash_core/agent/research/controller.py +254 -0
  28. emdash_core/agent/research/critic.py +428 -0
  29. emdash_core/agent/research/macros.py +469 -0
  30. emdash_core/agent/research/planner.py +449 -0
  31. emdash_core/agent/research/researcher.py +436 -0
  32. emdash_core/agent/research/state.py +523 -0
  33. emdash_core/agent/research/synthesizer.py +594 -0
  34. emdash_core/agent/reviewer_profile.py +475 -0
  35. emdash_core/agent/rules.py +123 -0
  36. emdash_core/agent/runner.py +601 -0
  37. emdash_core/agent/session.py +262 -0
  38. emdash_core/agent/spec_schema.py +66 -0
  39. emdash_core/agent/specification.py +479 -0
  40. emdash_core/agent/subagent.py +397 -0
  41. emdash_core/agent/subagent_prompts.py +13 -0
  42. emdash_core/agent/toolkit.py +482 -0
  43. emdash_core/agent/toolkits/__init__.py +64 -0
  44. emdash_core/agent/toolkits/base.py +96 -0
  45. emdash_core/agent/toolkits/explore.py +47 -0
  46. emdash_core/agent/toolkits/plan.py +55 -0
  47. emdash_core/agent/tools/__init__.py +141 -0
  48. emdash_core/agent/tools/analytics.py +436 -0
  49. emdash_core/agent/tools/base.py +131 -0
  50. emdash_core/agent/tools/coding.py +484 -0
  51. emdash_core/agent/tools/github_mcp.py +592 -0
  52. emdash_core/agent/tools/history.py +13 -0
  53. emdash_core/agent/tools/modes.py +153 -0
  54. emdash_core/agent/tools/plan.py +206 -0
  55. emdash_core/agent/tools/plan_write.py +135 -0
  56. emdash_core/agent/tools/search.py +412 -0
  57. emdash_core/agent/tools/spec.py +341 -0
  58. emdash_core/agent/tools/task.py +262 -0
  59. emdash_core/agent/tools/task_output.py +204 -0
  60. emdash_core/agent/tools/tasks.py +454 -0
  61. emdash_core/agent/tools/traversal.py +588 -0
  62. emdash_core/agent/tools/web.py +179 -0
  63. emdash_core/analytics/__init__.py +5 -0
  64. emdash_core/analytics/engine.py +1286 -0
  65. emdash_core/api/__init__.py +5 -0
  66. emdash_core/api/agent.py +308 -0
  67. emdash_core/api/agents.py +154 -0
  68. emdash_core/api/analyze.py +264 -0
  69. emdash_core/api/auth.py +173 -0
  70. emdash_core/api/context.py +77 -0
  71. emdash_core/api/db.py +121 -0
  72. emdash_core/api/embed.py +131 -0
  73. emdash_core/api/feature.py +143 -0
  74. emdash_core/api/health.py +93 -0
  75. emdash_core/api/index.py +162 -0
  76. emdash_core/api/plan.py +110 -0
  77. emdash_core/api/projectmd.py +210 -0
  78. emdash_core/api/query.py +320 -0
  79. emdash_core/api/research.py +122 -0
  80. emdash_core/api/review.py +161 -0
  81. emdash_core/api/router.py +76 -0
  82. emdash_core/api/rules.py +116 -0
  83. emdash_core/api/search.py +119 -0
  84. emdash_core/api/spec.py +99 -0
  85. emdash_core/api/swarm.py +223 -0
  86. emdash_core/api/tasks.py +109 -0
  87. emdash_core/api/team.py +120 -0
  88. emdash_core/auth/__init__.py +17 -0
  89. emdash_core/auth/github.py +389 -0
  90. emdash_core/config.py +74 -0
  91. emdash_core/context/__init__.py +52 -0
  92. emdash_core/context/models.py +50 -0
  93. emdash_core/context/providers/__init__.py +11 -0
  94. emdash_core/context/providers/base.py +74 -0
  95. emdash_core/context/providers/explored_areas.py +183 -0
  96. emdash_core/context/providers/touched_areas.py +360 -0
  97. emdash_core/context/registry.py +73 -0
  98. emdash_core/context/reranker.py +199 -0
  99. emdash_core/context/service.py +260 -0
  100. emdash_core/context/session.py +352 -0
  101. emdash_core/core/__init__.py +104 -0
  102. emdash_core/core/config.py +454 -0
  103. emdash_core/core/exceptions.py +55 -0
  104. emdash_core/core/models.py +265 -0
  105. emdash_core/core/review_config.py +57 -0
  106. emdash_core/db/__init__.py +67 -0
  107. emdash_core/db/auth.py +134 -0
  108. emdash_core/db/models.py +91 -0
  109. emdash_core/db/provider.py +222 -0
  110. emdash_core/db/providers/__init__.py +5 -0
  111. emdash_core/db/providers/supabase.py +452 -0
  112. emdash_core/embeddings/__init__.py +24 -0
  113. emdash_core/embeddings/indexer.py +534 -0
  114. emdash_core/embeddings/models.py +192 -0
  115. emdash_core/embeddings/providers/__init__.py +7 -0
  116. emdash_core/embeddings/providers/base.py +112 -0
  117. emdash_core/embeddings/providers/fireworks.py +141 -0
  118. emdash_core/embeddings/providers/openai.py +104 -0
  119. emdash_core/embeddings/registry.py +146 -0
  120. emdash_core/embeddings/service.py +215 -0
  121. emdash_core/graph/__init__.py +26 -0
  122. emdash_core/graph/builder.py +134 -0
  123. emdash_core/graph/connection.py +692 -0
  124. emdash_core/graph/schema.py +416 -0
  125. emdash_core/graph/writer.py +667 -0
  126. emdash_core/ingestion/__init__.py +7 -0
  127. emdash_core/ingestion/change_detector.py +150 -0
  128. emdash_core/ingestion/git/__init__.py +5 -0
  129. emdash_core/ingestion/git/commit_analyzer.py +196 -0
  130. emdash_core/ingestion/github/__init__.py +6 -0
  131. emdash_core/ingestion/github/pr_fetcher.py +296 -0
  132. emdash_core/ingestion/github/task_extractor.py +100 -0
  133. emdash_core/ingestion/orchestrator.py +540 -0
  134. emdash_core/ingestion/parsers/__init__.py +10 -0
  135. emdash_core/ingestion/parsers/base_parser.py +66 -0
  136. emdash_core/ingestion/parsers/call_graph_builder.py +121 -0
  137. emdash_core/ingestion/parsers/class_extractor.py +154 -0
  138. emdash_core/ingestion/parsers/function_extractor.py +202 -0
  139. emdash_core/ingestion/parsers/import_analyzer.py +119 -0
  140. emdash_core/ingestion/parsers/python_parser.py +123 -0
  141. emdash_core/ingestion/parsers/registry.py +72 -0
  142. emdash_core/ingestion/parsers/ts_ast_parser.js +313 -0
  143. emdash_core/ingestion/parsers/typescript_parser.py +278 -0
  144. emdash_core/ingestion/repository.py +346 -0
  145. emdash_core/models/__init__.py +38 -0
  146. emdash_core/models/agent.py +68 -0
  147. emdash_core/models/index.py +77 -0
  148. emdash_core/models/query.py +113 -0
  149. emdash_core/planning/__init__.py +7 -0
  150. emdash_core/planning/agent_api.py +413 -0
  151. emdash_core/planning/context_builder.py +265 -0
  152. emdash_core/planning/feature_context.py +232 -0
  153. emdash_core/planning/feature_expander.py +646 -0
  154. emdash_core/planning/llm_explainer.py +198 -0
  155. emdash_core/planning/similarity.py +509 -0
  156. emdash_core/planning/team_focus.py +821 -0
  157. emdash_core/server.py +153 -0
  158. emdash_core/sse/__init__.py +5 -0
  159. emdash_core/sse/stream.py +196 -0
  160. emdash_core/swarm/__init__.py +17 -0
  161. emdash_core/swarm/merge_agent.py +383 -0
  162. emdash_core/swarm/session_manager.py +274 -0
  163. emdash_core/swarm/swarm_runner.py +226 -0
  164. emdash_core/swarm/task_definition.py +137 -0
  165. emdash_core/swarm/worker_spawner.py +319 -0
  166. emdash_core/swarm/worktree_manager.py +278 -0
  167. emdash_core/templates/__init__.py +10 -0
  168. emdash_core/templates/defaults/agent-builder.md.template +82 -0
  169. emdash_core/templates/defaults/focus.md.template +115 -0
  170. emdash_core/templates/defaults/pr-review-enhanced.md.template +309 -0
  171. emdash_core/templates/defaults/pr-review.md.template +80 -0
  172. emdash_core/templates/defaults/project.md.template +85 -0
  173. emdash_core/templates/defaults/research_critic.md.template +112 -0
  174. emdash_core/templates/defaults/research_planner.md.template +85 -0
  175. emdash_core/templates/defaults/research_synthesizer.md.template +128 -0
  176. emdash_core/templates/defaults/reviewer.md.template +81 -0
  177. emdash_core/templates/defaults/spec.md.template +41 -0
  178. emdash_core/templates/defaults/tasks.md.template +78 -0
  179. emdash_core/templates/loader.py +296 -0
  180. emdash_core/utils/__init__.py +45 -0
  181. emdash_core/utils/git.py +84 -0
  182. emdash_core/utils/image.py +502 -0
  183. emdash_core/utils/logger.py +51 -0
  184. emdash_core-0.1.7.dist-info/METADATA +35 -0
  185. emdash_core-0.1.7.dist-info/RECORD +187 -0
  186. emdash_core-0.1.7.dist-info/WHEEL +4 -0
  187. 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)
@@ -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)
@@ -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
+ )