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,264 @@
|
|
|
1
|
+
"""Analytics 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="/analyze", tags=["analytics"])
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class PageRankResult(BaseModel):
|
|
12
|
+
"""PageRank result for an entity."""
|
|
13
|
+
qualified_name: str
|
|
14
|
+
name: str
|
|
15
|
+
entity_type: str
|
|
16
|
+
file_path: str
|
|
17
|
+
score: float
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class PageRankResponse(BaseModel):
|
|
21
|
+
"""PageRank response."""
|
|
22
|
+
results: list[PageRankResult]
|
|
23
|
+
damping: float
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class CommunityMember(BaseModel):
|
|
27
|
+
"""A member of a community."""
|
|
28
|
+
qualified_name: str
|
|
29
|
+
name: str
|
|
30
|
+
entity_type: str
|
|
31
|
+
file_path: str
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class Community(BaseModel):
|
|
35
|
+
"""A detected community."""
|
|
36
|
+
id: int
|
|
37
|
+
size: int
|
|
38
|
+
description: Optional[str] = None
|
|
39
|
+
members: list[CommunityMember] = Field(default_factory=list)
|
|
40
|
+
top_files: list[str] = Field(default_factory=list)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class CommunityResponse(BaseModel):
|
|
44
|
+
"""Community detection response."""
|
|
45
|
+
communities: list[Community]
|
|
46
|
+
total: int
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class AreaImportance(BaseModel):
|
|
50
|
+
"""Importance metrics for a directory/file."""
|
|
51
|
+
path: str
|
|
52
|
+
commit_count: int
|
|
53
|
+
author_count: int
|
|
54
|
+
churn: int
|
|
55
|
+
importance_score: float
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class AreasResponse(BaseModel):
|
|
59
|
+
"""Areas importance response."""
|
|
60
|
+
areas: list[AreaImportance]
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class BetweennessResult(BaseModel):
|
|
64
|
+
"""Betweenness centrality result."""
|
|
65
|
+
qualified_name: str
|
|
66
|
+
name: str
|
|
67
|
+
entity_type: str
|
|
68
|
+
score: float
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _get_analytics():
|
|
72
|
+
"""Get analytics engine."""
|
|
73
|
+
from ..analytics.engine import AnalyticsEngine
|
|
74
|
+
return AnalyticsEngine()
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@router.get("/pagerank", response_model=PageRankResponse)
|
|
78
|
+
async def get_pagerank(top: int = 20, damping: float = 0.85):
|
|
79
|
+
"""Compute PageRank scores to identify important code.
|
|
80
|
+
|
|
81
|
+
PageRank identifies code that is heavily depended upon.
|
|
82
|
+
"""
|
|
83
|
+
try:
|
|
84
|
+
engine = _get_analytics()
|
|
85
|
+
results = engine.compute_pagerank(top=top, damping=damping)
|
|
86
|
+
|
|
87
|
+
return PageRankResponse(
|
|
88
|
+
results=[
|
|
89
|
+
PageRankResult(
|
|
90
|
+
qualified_name=r.get("qualified_name", ""),
|
|
91
|
+
name=r.get("name", ""),
|
|
92
|
+
entity_type=r.get("type", ""),
|
|
93
|
+
file_path=r.get("file_path", ""),
|
|
94
|
+
score=r.get("score", 0.0),
|
|
95
|
+
)
|
|
96
|
+
for r in results
|
|
97
|
+
],
|
|
98
|
+
damping=damping,
|
|
99
|
+
)
|
|
100
|
+
except Exception as e:
|
|
101
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@router.get("/communities", response_model=CommunityResponse)
|
|
105
|
+
async def get_communities(
|
|
106
|
+
top: int = 10,
|
|
107
|
+
resolution: float = 1.0,
|
|
108
|
+
include_members: bool = False,
|
|
109
|
+
query: Optional[str] = None,
|
|
110
|
+
):
|
|
111
|
+
"""Detect code communities using Louvain algorithm.
|
|
112
|
+
|
|
113
|
+
Communities are clusters of tightly related code.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
top: Number of top communities to return
|
|
117
|
+
resolution: Louvain resolution parameter
|
|
118
|
+
include_members: Include community members in response
|
|
119
|
+
query: Filter communities by semantic query
|
|
120
|
+
"""
|
|
121
|
+
try:
|
|
122
|
+
engine = _get_analytics()
|
|
123
|
+
communities = engine.detect_communities(
|
|
124
|
+
resolution=resolution,
|
|
125
|
+
top=top,
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
result = []
|
|
129
|
+
for c in communities:
|
|
130
|
+
community = Community(
|
|
131
|
+
id=c.get("id", 0),
|
|
132
|
+
size=c.get("size", 0),
|
|
133
|
+
description=c.get("description"),
|
|
134
|
+
top_files=c.get("top_files", []),
|
|
135
|
+
)
|
|
136
|
+
if include_members:
|
|
137
|
+
community.members = [
|
|
138
|
+
CommunityMember(
|
|
139
|
+
qualified_name=m.get("qualified_name", ""),
|
|
140
|
+
name=m.get("name", ""),
|
|
141
|
+
entity_type=m.get("type", ""),
|
|
142
|
+
file_path=m.get("file_path", ""),
|
|
143
|
+
)
|
|
144
|
+
for m in c.get("members", [])
|
|
145
|
+
]
|
|
146
|
+
result.append(community)
|
|
147
|
+
|
|
148
|
+
return CommunityResponse(
|
|
149
|
+
communities=result,
|
|
150
|
+
total=len(result),
|
|
151
|
+
)
|
|
152
|
+
except Exception as e:
|
|
153
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
@router.get("/communities/{community_id}")
|
|
157
|
+
async def get_community(community_id: int):
|
|
158
|
+
"""Get details of a specific community."""
|
|
159
|
+
try:
|
|
160
|
+
engine = _get_analytics()
|
|
161
|
+
community = engine.get_community(community_id)
|
|
162
|
+
|
|
163
|
+
if not community:
|
|
164
|
+
raise HTTPException(status_code=404, detail="Community not found")
|
|
165
|
+
|
|
166
|
+
return Community(
|
|
167
|
+
id=community.get("id", community_id),
|
|
168
|
+
size=community.get("size", 0),
|
|
169
|
+
description=community.get("description"),
|
|
170
|
+
top_files=community.get("top_files", []),
|
|
171
|
+
members=[
|
|
172
|
+
CommunityMember(
|
|
173
|
+
qualified_name=m.get("qualified_name", ""),
|
|
174
|
+
name=m.get("name", ""),
|
|
175
|
+
entity_type=m.get("type", ""),
|
|
176
|
+
file_path=m.get("file_path", ""),
|
|
177
|
+
)
|
|
178
|
+
for m in community.get("members", [])
|
|
179
|
+
],
|
|
180
|
+
)
|
|
181
|
+
except HTTPException:
|
|
182
|
+
raise
|
|
183
|
+
except Exception as e:
|
|
184
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
@router.get("/areas", response_model=AreasResponse)
|
|
188
|
+
async def get_areas(
|
|
189
|
+
depth: int = 2,
|
|
190
|
+
days: int = 90,
|
|
191
|
+
top: int = 20,
|
|
192
|
+
sort: str = "importance",
|
|
193
|
+
include_files: bool = False,
|
|
194
|
+
):
|
|
195
|
+
"""Get importance metrics by directory/file.
|
|
196
|
+
|
|
197
|
+
Args:
|
|
198
|
+
depth: Directory depth to analyze
|
|
199
|
+
days: Number of days of history to consider
|
|
200
|
+
top: Number of top areas to return
|
|
201
|
+
sort: Sort by: importance, commits, churn, authors
|
|
202
|
+
include_files: Include individual files (not just directories)
|
|
203
|
+
"""
|
|
204
|
+
try:
|
|
205
|
+
engine = _get_analytics()
|
|
206
|
+
areas = engine.get_area_importance(
|
|
207
|
+
depth=depth,
|
|
208
|
+
days=days,
|
|
209
|
+
top=top,
|
|
210
|
+
sort_by=sort,
|
|
211
|
+
include_files=include_files,
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
return AreasResponse(
|
|
215
|
+
areas=[
|
|
216
|
+
AreaImportance(
|
|
217
|
+
path=a.get("path", ""),
|
|
218
|
+
commit_count=a.get("commit_count", 0),
|
|
219
|
+
author_count=a.get("author_count", 0),
|
|
220
|
+
churn=a.get("churn", 0),
|
|
221
|
+
importance_score=a.get("importance_score", 0.0),
|
|
222
|
+
)
|
|
223
|
+
for a in areas
|
|
224
|
+
]
|
|
225
|
+
)
|
|
226
|
+
except Exception as e:
|
|
227
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
@router.get("/betweenness")
|
|
231
|
+
async def get_betweenness(top: int = 20):
|
|
232
|
+
"""Compute betweenness centrality.
|
|
233
|
+
|
|
234
|
+
Identifies bridge entities that connect different parts of the codebase.
|
|
235
|
+
"""
|
|
236
|
+
try:
|
|
237
|
+
engine = _get_analytics()
|
|
238
|
+
results = engine.compute_betweenness(top=top)
|
|
239
|
+
|
|
240
|
+
return {
|
|
241
|
+
"results": [
|
|
242
|
+
BetweennessResult(
|
|
243
|
+
qualified_name=r.get("qualified_name", ""),
|
|
244
|
+
name=r.get("name", ""),
|
|
245
|
+
entity_type=r.get("type", ""),
|
|
246
|
+
score=r.get("score", 0.0),
|
|
247
|
+
)
|
|
248
|
+
for r in results
|
|
249
|
+
]
|
|
250
|
+
}
|
|
251
|
+
except Exception as e:
|
|
252
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
@router.get("/commit-importance")
|
|
256
|
+
async def get_commit_importance(top: int = 20):
|
|
257
|
+
"""Score files by commit frequency and author diversity."""
|
|
258
|
+
try:
|
|
259
|
+
engine = _get_analytics()
|
|
260
|
+
results = engine.get_commit_importance(top=top)
|
|
261
|
+
|
|
262
|
+
return {"results": results}
|
|
263
|
+
except Exception as e:
|
|
264
|
+
raise HTTPException(status_code=500, detail=str(e))
|
emdash_core/api/auth.py
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"""GitHub OAuth authentication API endpoints."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
from fastapi import APIRouter, HTTPException
|
|
8
|
+
from pydantic import BaseModel
|
|
9
|
+
|
|
10
|
+
router = APIRouter(prefix="/auth", tags=["auth"])
|
|
11
|
+
|
|
12
|
+
# Thread pool for blocking operations
|
|
13
|
+
_executor = ThreadPoolExecutor(max_workers=2)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class LoginResponse(BaseModel):
|
|
17
|
+
"""Response from login initiation."""
|
|
18
|
+
user_code: str
|
|
19
|
+
verification_uri: str
|
|
20
|
+
expires_in: int
|
|
21
|
+
interval: int
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class AuthStatus(BaseModel):
|
|
25
|
+
"""Current authentication status."""
|
|
26
|
+
authenticated: bool
|
|
27
|
+
username: Optional[str] = None
|
|
28
|
+
scope: Optional[str] = None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class LoginPollResponse(BaseModel):
|
|
32
|
+
"""Response from login polling."""
|
|
33
|
+
status: str # "pending", "success", "expired", "error"
|
|
34
|
+
username: Optional[str] = None
|
|
35
|
+
error: Optional[str] = None
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# Store active device flow states
|
|
39
|
+
_device_flows: dict[str, dict] = {}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@router.post("/login", response_model=LoginResponse)
|
|
43
|
+
async def start_login():
|
|
44
|
+
"""Start GitHub OAuth device flow.
|
|
45
|
+
|
|
46
|
+
Returns a user code that the user must enter at github.com/login/device.
|
|
47
|
+
The client should then poll /auth/login/poll until authentication completes.
|
|
48
|
+
|
|
49
|
+
Example:
|
|
50
|
+
curl -X POST http://localhost:8765/api/auth/login
|
|
51
|
+
"""
|
|
52
|
+
from ..auth.github import GitHubAuth
|
|
53
|
+
|
|
54
|
+
loop = asyncio.get_event_loop()
|
|
55
|
+
|
|
56
|
+
def _request_device_code():
|
|
57
|
+
auth = GitHubAuth()
|
|
58
|
+
return auth.request_device_code()
|
|
59
|
+
|
|
60
|
+
try:
|
|
61
|
+
response = await loop.run_in_executor(_executor, _request_device_code)
|
|
62
|
+
|
|
63
|
+
# Store device code for polling
|
|
64
|
+
_device_flows[response.user_code] = {
|
|
65
|
+
"device_code": response.device_code,
|
|
66
|
+
"interval": response.interval,
|
|
67
|
+
"expires_in": response.expires_in,
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return LoginResponse(
|
|
71
|
+
user_code=response.user_code,
|
|
72
|
+
verification_uri=response.verification_uri,
|
|
73
|
+
expires_in=response.expires_in,
|
|
74
|
+
interval=response.interval,
|
|
75
|
+
)
|
|
76
|
+
except Exception as e:
|
|
77
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@router.post("/login/poll/{user_code}", response_model=LoginPollResponse)
|
|
81
|
+
async def poll_login(user_code: str):
|
|
82
|
+
"""Poll for login completion.
|
|
83
|
+
|
|
84
|
+
After starting login, the client should poll this endpoint
|
|
85
|
+
every `interval` seconds until status is "success" or "expired".
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
user_code: The user code returned from /auth/login
|
|
89
|
+
|
|
90
|
+
Example:
|
|
91
|
+
curl -X POST http://localhost:8765/api/auth/login/poll/ABCD-1234
|
|
92
|
+
"""
|
|
93
|
+
from ..auth.github import GitHubAuth
|
|
94
|
+
|
|
95
|
+
if user_code not in _device_flows:
|
|
96
|
+
raise HTTPException(status_code=404, detail="Device flow not found")
|
|
97
|
+
|
|
98
|
+
flow = _device_flows[user_code]
|
|
99
|
+
device_code = flow["device_code"]
|
|
100
|
+
|
|
101
|
+
loop = asyncio.get_event_loop()
|
|
102
|
+
|
|
103
|
+
def _poll_for_token():
|
|
104
|
+
auth = GitHubAuth()
|
|
105
|
+
return auth.poll_for_token(device_code)
|
|
106
|
+
|
|
107
|
+
try:
|
|
108
|
+
result = await loop.run_in_executor(_executor, _poll_for_token)
|
|
109
|
+
|
|
110
|
+
if result is None:
|
|
111
|
+
# Still pending
|
|
112
|
+
return LoginPollResponse(status="pending")
|
|
113
|
+
|
|
114
|
+
if isinstance(result, str):
|
|
115
|
+
# Error message
|
|
116
|
+
if "expired" in result.lower():
|
|
117
|
+
del _device_flows[user_code]
|
|
118
|
+
return LoginPollResponse(status="expired", error=result)
|
|
119
|
+
return LoginPollResponse(status="error", error=result)
|
|
120
|
+
|
|
121
|
+
# Success - result is AuthConfig
|
|
122
|
+
del _device_flows[user_code]
|
|
123
|
+
return LoginPollResponse(
|
|
124
|
+
status="success",
|
|
125
|
+
username=result.username,
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
except Exception as e:
|
|
129
|
+
return LoginPollResponse(status="error", error=str(e))
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
@router.post("/logout")
|
|
133
|
+
async def logout():
|
|
134
|
+
"""Sign out by removing stored credentials.
|
|
135
|
+
|
|
136
|
+
Example:
|
|
137
|
+
curl -X POST http://localhost:8765/api/auth/logout
|
|
138
|
+
"""
|
|
139
|
+
from ..auth.github import GitHubAuth
|
|
140
|
+
|
|
141
|
+
loop = asyncio.get_event_loop()
|
|
142
|
+
|
|
143
|
+
def _clear_auth():
|
|
144
|
+
auth = GitHubAuth()
|
|
145
|
+
auth.clear_auth()
|
|
146
|
+
|
|
147
|
+
await loop.run_in_executor(_executor, _clear_auth)
|
|
148
|
+
return {"success": True, "message": "Logged out successfully"}
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
@router.get("/status", response_model=AuthStatus)
|
|
152
|
+
async def get_status():
|
|
153
|
+
"""Get current authentication status.
|
|
154
|
+
|
|
155
|
+
Example:
|
|
156
|
+
curl http://localhost:8765/api/auth/status
|
|
157
|
+
"""
|
|
158
|
+
from ..auth.github import get_auth_status
|
|
159
|
+
|
|
160
|
+
loop = asyncio.get_event_loop()
|
|
161
|
+
|
|
162
|
+
def _get_status():
|
|
163
|
+
return get_auth_status()
|
|
164
|
+
|
|
165
|
+
try:
|
|
166
|
+
status = await loop.run_in_executor(_executor, _get_status)
|
|
167
|
+
return AuthStatus(
|
|
168
|
+
authenticated=status.get("authenticated", False),
|
|
169
|
+
username=status.get("username"),
|
|
170
|
+
scope=status.get("scope"),
|
|
171
|
+
)
|
|
172
|
+
except Exception:
|
|
173
|
+
return AuthStatus(authenticated=False)
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""Session context endpoints."""
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
from fastapi import APIRouter
|
|
6
|
+
from pydantic import BaseModel, Field
|
|
7
|
+
|
|
8
|
+
router = APIRouter(prefix="/context", tags=["context"])
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ContextEntity(BaseModel):
|
|
12
|
+
"""An entity in the context."""
|
|
13
|
+
qualified_name: str
|
|
14
|
+
entity_type: str
|
|
15
|
+
file_path: str
|
|
16
|
+
relevance: float
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class SessionContext(BaseModel):
|
|
20
|
+
"""Current session context."""
|
|
21
|
+
entities: list[ContextEntity] = Field(default_factory=list)
|
|
22
|
+
files: list[str] = Field(default_factory=list)
|
|
23
|
+
summary: Optional[str] = None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@router.get("", response_model=SessionContext)
|
|
27
|
+
async def get_context():
|
|
28
|
+
"""Get current session context."""
|
|
29
|
+
try:
|
|
30
|
+
from ..context.service import ContextService
|
|
31
|
+
|
|
32
|
+
service = ContextService()
|
|
33
|
+
ctx = service.get_context()
|
|
34
|
+
|
|
35
|
+
return SessionContext(
|
|
36
|
+
entities=[
|
|
37
|
+
ContextEntity(
|
|
38
|
+
qualified_name=e.get("qualified_name", ""),
|
|
39
|
+
entity_type=e.get("type", ""),
|
|
40
|
+
file_path=e.get("file_path", ""),
|
|
41
|
+
relevance=e.get("relevance", 0.0),
|
|
42
|
+
)
|
|
43
|
+
for e in ctx.get("entities", [])
|
|
44
|
+
],
|
|
45
|
+
files=ctx.get("files", []),
|
|
46
|
+
summary=ctx.get("summary"),
|
|
47
|
+
)
|
|
48
|
+
except Exception:
|
|
49
|
+
return SessionContext()
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@router.delete("")
|
|
53
|
+
async def clear_context():
|
|
54
|
+
"""Clear session context."""
|
|
55
|
+
try:
|
|
56
|
+
from ..context.service import ContextService
|
|
57
|
+
|
|
58
|
+
service = ContextService()
|
|
59
|
+
service.clear()
|
|
60
|
+
|
|
61
|
+
return {"success": True}
|
|
62
|
+
except Exception as e:
|
|
63
|
+
return {"success": False, "error": str(e)}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@router.get("/prompt")
|
|
67
|
+
async def get_context_prompt():
|
|
68
|
+
"""Get context as a prompt for system prompt injection."""
|
|
69
|
+
try:
|
|
70
|
+
from ..context.service import ContextService
|
|
71
|
+
|
|
72
|
+
service = ContextService()
|
|
73
|
+
prompt = service.get_prompt()
|
|
74
|
+
|
|
75
|
+
return {"prompt": prompt}
|
|
76
|
+
except Exception:
|
|
77
|
+
return {"prompt": ""}
|
emdash_core/api/db.py
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"""Database management endpoints."""
|
|
2
|
+
|
|
3
|
+
from fastapi import APIRouter, HTTPException
|
|
4
|
+
from pydantic import BaseModel
|
|
5
|
+
|
|
6
|
+
router = APIRouter(prefix="/db", tags=["database"])
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class DbStats(BaseModel):
|
|
10
|
+
"""Database statistics."""
|
|
11
|
+
node_count: int
|
|
12
|
+
relationship_count: int
|
|
13
|
+
file_count: int
|
|
14
|
+
function_count: int
|
|
15
|
+
class_count: int
|
|
16
|
+
community_count: int
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class DbInitResponse(BaseModel):
|
|
20
|
+
"""Response from database initialization."""
|
|
21
|
+
success: bool
|
|
22
|
+
message: str
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class DbTestResponse(BaseModel):
|
|
26
|
+
"""Response from database test."""
|
|
27
|
+
connected: bool
|
|
28
|
+
database_path: str
|
|
29
|
+
message: str
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _get_connection():
|
|
33
|
+
"""Get database connection."""
|
|
34
|
+
from ..graph.connection import KuzuConnection
|
|
35
|
+
return KuzuConnection()
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@router.post("/init", response_model=DbInitResponse)
|
|
39
|
+
async def db_init():
|
|
40
|
+
"""Initialize the Kuzu database schema.
|
|
41
|
+
|
|
42
|
+
Creates all node and relationship tables required for the knowledge graph.
|
|
43
|
+
"""
|
|
44
|
+
try:
|
|
45
|
+
conn = _get_connection()
|
|
46
|
+
from ..graph.schema import SchemaManager
|
|
47
|
+
|
|
48
|
+
schema = SchemaManager(conn)
|
|
49
|
+
schema.initialize_schema()
|
|
50
|
+
|
|
51
|
+
return DbInitResponse(
|
|
52
|
+
success=True,
|
|
53
|
+
message="Database schema initialized successfully"
|
|
54
|
+
)
|
|
55
|
+
except Exception as e:
|
|
56
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@router.post("/clear", response_model=DbInitResponse)
|
|
60
|
+
async def db_clear(confirm: bool = False):
|
|
61
|
+
"""Clear all data from the database.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
confirm: Must be True to proceed with clearing
|
|
65
|
+
"""
|
|
66
|
+
if not confirm:
|
|
67
|
+
raise HTTPException(
|
|
68
|
+
status_code=400,
|
|
69
|
+
detail="Must set confirm=true to clear database"
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
try:
|
|
73
|
+
conn = _get_connection()
|
|
74
|
+
conn.clear_database()
|
|
75
|
+
|
|
76
|
+
return DbInitResponse(
|
|
77
|
+
success=True,
|
|
78
|
+
message="Database cleared successfully"
|
|
79
|
+
)
|
|
80
|
+
except Exception as e:
|
|
81
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@router.get("/stats", response_model=DbStats)
|
|
85
|
+
async def db_stats():
|
|
86
|
+
"""Get database statistics."""
|
|
87
|
+
try:
|
|
88
|
+
conn = _get_connection()
|
|
89
|
+
info = conn.get_database_info()
|
|
90
|
+
|
|
91
|
+
return DbStats(
|
|
92
|
+
node_count=info.get("node_count", 0),
|
|
93
|
+
relationship_count=info.get("relationship_count", 0),
|
|
94
|
+
file_count=info.get("file_count", 0),
|
|
95
|
+
function_count=info.get("function_count", 0),
|
|
96
|
+
class_count=info.get("class_count", 0),
|
|
97
|
+
community_count=info.get("community_count", 0),
|
|
98
|
+
)
|
|
99
|
+
except Exception as e:
|
|
100
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@router.get("/test", response_model=DbTestResponse)
|
|
104
|
+
async def db_test():
|
|
105
|
+
"""Test database connection."""
|
|
106
|
+
try:
|
|
107
|
+
conn = _get_connection()
|
|
108
|
+
# Try to connect
|
|
109
|
+
conn.connect()
|
|
110
|
+
|
|
111
|
+
return DbTestResponse(
|
|
112
|
+
connected=True,
|
|
113
|
+
database_path=str(conn.database_path),
|
|
114
|
+
message="Database connection successful"
|
|
115
|
+
)
|
|
116
|
+
except Exception as e:
|
|
117
|
+
return DbTestResponse(
|
|
118
|
+
connected=False,
|
|
119
|
+
database_path="",
|
|
120
|
+
message=str(e)
|
|
121
|
+
)
|