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/agent.py
ADDED
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
"""Agent chat endpoint with SSE streaming."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import sys
|
|
5
|
+
import uuid
|
|
6
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
from fastapi import APIRouter, HTTPException
|
|
11
|
+
from fastapi.responses import StreamingResponse
|
|
12
|
+
|
|
13
|
+
from ..config import get_config
|
|
14
|
+
from ..models.agent import AgentChatRequest, AgentMode
|
|
15
|
+
from ..sse.stream import SSEHandler, EventType
|
|
16
|
+
|
|
17
|
+
router = APIRouter(prefix="/agent", tags=["agent"])
|
|
18
|
+
|
|
19
|
+
# Thread pool for running blocking agent code
|
|
20
|
+
_executor = ThreadPoolExecutor(max_workers=4)
|
|
21
|
+
|
|
22
|
+
# Active sessions (in-memory for now)
|
|
23
|
+
_sessions: dict[str, dict] = {}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _ensure_emdash_importable():
|
|
27
|
+
"""Ensure the emdash_core package is importable.
|
|
28
|
+
|
|
29
|
+
This is now a no-op since we use emdash_core directly.
|
|
30
|
+
"""
|
|
31
|
+
pass # emdash_core is already in the package
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _run_agent_sync(
|
|
35
|
+
message: str,
|
|
36
|
+
model: str,
|
|
37
|
+
max_iterations: int,
|
|
38
|
+
sse_handler: SSEHandler,
|
|
39
|
+
session_id: str,
|
|
40
|
+
images: list = None,
|
|
41
|
+
):
|
|
42
|
+
"""Run the agent synchronously (in thread pool).
|
|
43
|
+
|
|
44
|
+
This function runs in a background thread and emits events
|
|
45
|
+
to the SSE handler for streaming to the client.
|
|
46
|
+
"""
|
|
47
|
+
try:
|
|
48
|
+
_ensure_emdash_importable()
|
|
49
|
+
|
|
50
|
+
# Import agent components from emdash_core
|
|
51
|
+
from ..agent.runner import AgentRunner
|
|
52
|
+
from ..agent.events import AgentEventEmitter
|
|
53
|
+
|
|
54
|
+
# Create an emitter that forwards to SSE handler
|
|
55
|
+
class SSEBridgeHandler:
|
|
56
|
+
"""Bridges AgentEventEmitter to SSEHandler."""
|
|
57
|
+
|
|
58
|
+
def __init__(self, sse_handler: SSEHandler):
|
|
59
|
+
self._sse = sse_handler
|
|
60
|
+
|
|
61
|
+
def handle(self, event):
|
|
62
|
+
"""Forward event to SSE handler."""
|
|
63
|
+
self._sse.handle(event)
|
|
64
|
+
|
|
65
|
+
# Create agent with event emitter
|
|
66
|
+
emitter = AgentEventEmitter(agent_name="Emdash Code")
|
|
67
|
+
emitter.add_handler(SSEBridgeHandler(sse_handler))
|
|
68
|
+
|
|
69
|
+
runner = AgentRunner(
|
|
70
|
+
model=model,
|
|
71
|
+
verbose=True,
|
|
72
|
+
max_iterations=max_iterations,
|
|
73
|
+
emitter=emitter,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
# Convert image data if provided
|
|
77
|
+
agent_images = None
|
|
78
|
+
if images:
|
|
79
|
+
from ..agent.providers.base import ImageContent
|
|
80
|
+
agent_images = [
|
|
81
|
+
ImageContent(data=img.data, format=img.format)
|
|
82
|
+
for img in images
|
|
83
|
+
]
|
|
84
|
+
|
|
85
|
+
# Run the agent
|
|
86
|
+
response = runner.run(message, images=agent_images)
|
|
87
|
+
|
|
88
|
+
# Store session state
|
|
89
|
+
_sessions[session_id] = {
|
|
90
|
+
"runner": runner,
|
|
91
|
+
"message_count": 1,
|
|
92
|
+
"model": model,
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return response
|
|
96
|
+
|
|
97
|
+
except Exception as e:
|
|
98
|
+
# Emit error event
|
|
99
|
+
sse_handler.emit(EventType.ERROR, {
|
|
100
|
+
"message": str(e),
|
|
101
|
+
"details": None,
|
|
102
|
+
})
|
|
103
|
+
raise
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
async def _run_agent_async(
|
|
107
|
+
request: AgentChatRequest,
|
|
108
|
+
sse_handler: SSEHandler,
|
|
109
|
+
session_id: str,
|
|
110
|
+
):
|
|
111
|
+
"""Run agent in thread pool and stream events."""
|
|
112
|
+
config = get_config()
|
|
113
|
+
|
|
114
|
+
# Get model from request or config
|
|
115
|
+
model = request.model or config.default_model
|
|
116
|
+
max_iterations = request.options.max_iterations
|
|
117
|
+
|
|
118
|
+
# Emit session start
|
|
119
|
+
sse_handler.emit(EventType.SESSION_START, {
|
|
120
|
+
"agent_name": "Emdash Code",
|
|
121
|
+
"model": model,
|
|
122
|
+
"session_id": session_id,
|
|
123
|
+
"query": request.message,
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
loop = asyncio.get_event_loop()
|
|
127
|
+
|
|
128
|
+
try:
|
|
129
|
+
# Run agent in thread pool
|
|
130
|
+
await loop.run_in_executor(
|
|
131
|
+
_executor,
|
|
132
|
+
_run_agent_sync,
|
|
133
|
+
request.message,
|
|
134
|
+
model,
|
|
135
|
+
max_iterations,
|
|
136
|
+
sse_handler,
|
|
137
|
+
session_id,
|
|
138
|
+
request.images,
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
# Emit session end
|
|
142
|
+
sse_handler.emit(EventType.SESSION_END, {
|
|
143
|
+
"success": True,
|
|
144
|
+
"session_id": session_id,
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
except Exception as e:
|
|
148
|
+
sse_handler.emit(EventType.SESSION_END, {
|
|
149
|
+
"success": False,
|
|
150
|
+
"error": str(e),
|
|
151
|
+
"session_id": session_id,
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
finally:
|
|
155
|
+
sse_handler.close()
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
@router.post("/chat")
|
|
159
|
+
async def agent_chat(request: AgentChatRequest):
|
|
160
|
+
"""Start an agent chat session with SSE streaming.
|
|
161
|
+
|
|
162
|
+
The response is a Server-Sent Events stream containing:
|
|
163
|
+
- session_start: Initial session info
|
|
164
|
+
- tool_start: When a tool begins execution
|
|
165
|
+
- tool_result: When a tool completes
|
|
166
|
+
- thinking: Agent reasoning messages
|
|
167
|
+
- response/partial_response: Agent text output
|
|
168
|
+
- clarification: When agent needs user input
|
|
169
|
+
- error/warning: Error messages
|
|
170
|
+
- session_end: Session completion
|
|
171
|
+
|
|
172
|
+
Example:
|
|
173
|
+
curl -N -X POST http://localhost:8765/api/agent/chat \\
|
|
174
|
+
-H "Content-Type: application/json" \\
|
|
175
|
+
-d '{"message": "Find authentication code"}'
|
|
176
|
+
"""
|
|
177
|
+
# Generate or use provided session ID
|
|
178
|
+
session_id = request.session_id or str(uuid.uuid4())
|
|
179
|
+
|
|
180
|
+
# Create SSE handler
|
|
181
|
+
sse_handler = SSEHandler(agent_name="Emdash Code")
|
|
182
|
+
|
|
183
|
+
# Start agent in background
|
|
184
|
+
asyncio.create_task(_run_agent_async(request, sse_handler, session_id))
|
|
185
|
+
|
|
186
|
+
# Return SSE stream
|
|
187
|
+
return StreamingResponse(
|
|
188
|
+
sse_handler,
|
|
189
|
+
media_type="text/event-stream",
|
|
190
|
+
headers={
|
|
191
|
+
"Cache-Control": "no-cache",
|
|
192
|
+
"Connection": "keep-alive",
|
|
193
|
+
"X-Session-ID": session_id,
|
|
194
|
+
},
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
@router.post("/chat/{session_id}/continue")
|
|
199
|
+
async def continue_chat(session_id: str, request: AgentChatRequest):
|
|
200
|
+
"""Continue an existing chat session.
|
|
201
|
+
|
|
202
|
+
This allows multi-turn conversations by reusing the session state.
|
|
203
|
+
"""
|
|
204
|
+
if session_id not in _sessions:
|
|
205
|
+
raise HTTPException(
|
|
206
|
+
status_code=404,
|
|
207
|
+
detail=f"Session {session_id} not found"
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
session = _sessions[session_id]
|
|
211
|
+
runner = session.get("runner")
|
|
212
|
+
if not runner:
|
|
213
|
+
raise HTTPException(
|
|
214
|
+
status_code=400,
|
|
215
|
+
detail="Session has no active runner"
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
# Create SSE handler
|
|
219
|
+
sse_handler = SSEHandler(agent_name="Emdash Code")
|
|
220
|
+
|
|
221
|
+
async def _continue_session():
|
|
222
|
+
sse_handler.emit(EventType.SESSION_START, {
|
|
223
|
+
"agent_name": "Emdash Code",
|
|
224
|
+
"model": session["model"],
|
|
225
|
+
"session_id": session_id,
|
|
226
|
+
"query": request.message,
|
|
227
|
+
"continued": True,
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
loop = asyncio.get_event_loop()
|
|
231
|
+
|
|
232
|
+
try:
|
|
233
|
+
# Wire up SSE handler to runner's emitter for this request
|
|
234
|
+
from ..agent.events import AgentEventEmitter
|
|
235
|
+
|
|
236
|
+
class SSEBridgeHandler:
|
|
237
|
+
def __init__(self, sse_handler: SSEHandler):
|
|
238
|
+
self._sse = sse_handler
|
|
239
|
+
|
|
240
|
+
def handle(self, event):
|
|
241
|
+
self._sse.handle(event)
|
|
242
|
+
|
|
243
|
+
# Create fresh emitter with new SSE handler
|
|
244
|
+
emitter = AgentEventEmitter(agent_name="Emdash Code")
|
|
245
|
+
emitter.add_handler(SSEBridgeHandler(sse_handler))
|
|
246
|
+
runner.emitter = emitter
|
|
247
|
+
|
|
248
|
+
# Continue conversation in thread pool
|
|
249
|
+
await loop.run_in_executor(
|
|
250
|
+
_executor,
|
|
251
|
+
lambda: runner.chat(request.message),
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
session["message_count"] += 1
|
|
255
|
+
|
|
256
|
+
sse_handler.emit(EventType.SESSION_END, {
|
|
257
|
+
"success": True,
|
|
258
|
+
"session_id": session_id,
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
except Exception as e:
|
|
262
|
+
sse_handler.emit(EventType.ERROR, {
|
|
263
|
+
"message": str(e),
|
|
264
|
+
})
|
|
265
|
+
sse_handler.emit(EventType.SESSION_END, {
|
|
266
|
+
"success": False,
|
|
267
|
+
"error": str(e),
|
|
268
|
+
"session_id": session_id,
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
finally:
|
|
272
|
+
sse_handler.close()
|
|
273
|
+
|
|
274
|
+
asyncio.create_task(_continue_session())
|
|
275
|
+
|
|
276
|
+
return StreamingResponse(
|
|
277
|
+
sse_handler,
|
|
278
|
+
media_type="text/event-stream",
|
|
279
|
+
headers={
|
|
280
|
+
"Cache-Control": "no-cache",
|
|
281
|
+
"Connection": "keep-alive",
|
|
282
|
+
"X-Session-ID": session_id,
|
|
283
|
+
},
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
@router.get("/sessions")
|
|
288
|
+
async def list_sessions():
|
|
289
|
+
"""List active chat sessions."""
|
|
290
|
+
return {
|
|
291
|
+
"sessions": [
|
|
292
|
+
{
|
|
293
|
+
"session_id": sid,
|
|
294
|
+
"model": data.get("model"),
|
|
295
|
+
"message_count": data.get("message_count", 0),
|
|
296
|
+
}
|
|
297
|
+
for sid, data in _sessions.items()
|
|
298
|
+
]
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
@router.delete("/sessions/{session_id}")
|
|
303
|
+
async def delete_session(session_id: str):
|
|
304
|
+
"""Delete a chat session."""
|
|
305
|
+
if session_id in _sessions:
|
|
306
|
+
del _sessions[session_id]
|
|
307
|
+
return {"deleted": True}
|
|
308
|
+
raise HTTPException(status_code=404, detail="Session not found")
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"""Agent management endpoints."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
from fastapi import APIRouter, HTTPException
|
|
7
|
+
from pydantic import BaseModel, Field
|
|
8
|
+
|
|
9
|
+
from ..config import get_config
|
|
10
|
+
|
|
11
|
+
router = APIRouter(prefix="/agents", tags=["agents"])
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class AgentInfo(BaseModel):
|
|
15
|
+
"""Agent information."""
|
|
16
|
+
name: str
|
|
17
|
+
path: str
|
|
18
|
+
exists: bool
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class AgentListResponse(BaseModel):
|
|
22
|
+
"""Response for listing agents."""
|
|
23
|
+
agents: list[AgentInfo]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class CreateAgentRequest(BaseModel):
|
|
27
|
+
"""Request to create an agent."""
|
|
28
|
+
name: str = Field(..., description="Agent name")
|
|
29
|
+
description: Optional[str] = Field(default=None, description="Agent description")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class CreateAgentResponse(BaseModel):
|
|
33
|
+
"""Response from creating an agent."""
|
|
34
|
+
success: bool
|
|
35
|
+
name: str
|
|
36
|
+
path: Optional[str] = None
|
|
37
|
+
error: Optional[str] = None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _get_agents_dir() -> Path:
|
|
41
|
+
"""Get the agents directory for the current repo."""
|
|
42
|
+
config = get_config()
|
|
43
|
+
if config.repo_root:
|
|
44
|
+
return Path(config.repo_root) / ".emdash" / "agents"
|
|
45
|
+
return Path.cwd() / ".emdash" / "agents"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@router.get("", response_model=AgentListResponse)
|
|
49
|
+
async def list_agents():
|
|
50
|
+
"""List all configured agents.
|
|
51
|
+
|
|
52
|
+
Returns agents from the .emdash/agents directory.
|
|
53
|
+
"""
|
|
54
|
+
agents_dir = _get_agents_dir()
|
|
55
|
+
agents = []
|
|
56
|
+
|
|
57
|
+
if agents_dir.exists():
|
|
58
|
+
for agent_file in agents_dir.glob("*.md"):
|
|
59
|
+
agents.append(AgentInfo(
|
|
60
|
+
name=agent_file.stem,
|
|
61
|
+
path=str(agent_file),
|
|
62
|
+
exists=True,
|
|
63
|
+
))
|
|
64
|
+
|
|
65
|
+
return AgentListResponse(agents=agents)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@router.post("", response_model=CreateAgentResponse)
|
|
69
|
+
async def create_agent(request: CreateAgentRequest):
|
|
70
|
+
"""Create a new agent configuration.
|
|
71
|
+
|
|
72
|
+
Creates an agent markdown file in .emdash/agents/
|
|
73
|
+
"""
|
|
74
|
+
agents_dir = _get_agents_dir()
|
|
75
|
+
agents_dir.mkdir(parents=True, exist_ok=True)
|
|
76
|
+
|
|
77
|
+
agent_file = agents_dir / f"{request.name}.md"
|
|
78
|
+
|
|
79
|
+
if agent_file.exists():
|
|
80
|
+
raise HTTPException(
|
|
81
|
+
status_code=409,
|
|
82
|
+
detail=f"Agent '{request.name}' already exists"
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
# Create agent template
|
|
86
|
+
description = request.description or f"Custom agent: {request.name}"
|
|
87
|
+
content = f"""# {request.name}
|
|
88
|
+
|
|
89
|
+
{description}
|
|
90
|
+
|
|
91
|
+
## Instructions
|
|
92
|
+
|
|
93
|
+
Define the agent's behavior and capabilities here.
|
|
94
|
+
|
|
95
|
+
## Tools
|
|
96
|
+
|
|
97
|
+
List the tools this agent can use:
|
|
98
|
+
- search
|
|
99
|
+
- read_file
|
|
100
|
+
- write_file
|
|
101
|
+
|
|
102
|
+
## Examples
|
|
103
|
+
|
|
104
|
+
Provide example interactions here.
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
try:
|
|
108
|
+
agent_file.write_text(content)
|
|
109
|
+
return CreateAgentResponse(
|
|
110
|
+
success=True,
|
|
111
|
+
name=request.name,
|
|
112
|
+
path=str(agent_file),
|
|
113
|
+
)
|
|
114
|
+
except Exception as e:
|
|
115
|
+
return CreateAgentResponse(
|
|
116
|
+
success=False,
|
|
117
|
+
name=request.name,
|
|
118
|
+
error=str(e),
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@router.get("/{name}", response_model=AgentInfo)
|
|
123
|
+
async def get_agent(name: str):
|
|
124
|
+
"""Get a specific agent's information."""
|
|
125
|
+
agents_dir = _get_agents_dir()
|
|
126
|
+
agent_file = agents_dir / f"{name}.md"
|
|
127
|
+
|
|
128
|
+
if not agent_file.exists():
|
|
129
|
+
raise HTTPException(
|
|
130
|
+
status_code=404,
|
|
131
|
+
detail=f"Agent '{name}' not found"
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
return AgentInfo(
|
|
135
|
+
name=name,
|
|
136
|
+
path=str(agent_file),
|
|
137
|
+
exists=True,
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
@router.delete("/{name}")
|
|
142
|
+
async def delete_agent(name: str):
|
|
143
|
+
"""Delete an agent configuration."""
|
|
144
|
+
agents_dir = _get_agents_dir()
|
|
145
|
+
agent_file = agents_dir / f"{name}.md"
|
|
146
|
+
|
|
147
|
+
if not agent_file.exists():
|
|
148
|
+
raise HTTPException(
|
|
149
|
+
status_code=404,
|
|
150
|
+
detail=f"Agent '{name}' not found"
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
agent_file.unlink()
|
|
154
|
+
return {"deleted": True, "name": name}
|