emdash-core 0.1.33__py3-none-any.whl → 0.1.37__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/agent/agents.py +84 -23
- emdash_core/agent/hooks.py +419 -0
- emdash_core/agent/inprocess_subagent.py +44 -9
- emdash_core/agent/prompts/main_agent.py +35 -0
- emdash_core/agent/prompts/subagents.py +24 -8
- emdash_core/agent/prompts/workflow.py +37 -23
- emdash_core/agent/runner/agent_runner.py +12 -0
- emdash_core/agent/runner/context.py +28 -9
- emdash_core/agent/toolkits/__init__.py +117 -18
- emdash_core/agent/toolkits/base.py +87 -2
- emdash_core/agent/toolkits/explore.py +18 -0
- emdash_core/agent/toolkits/plan.py +18 -0
- emdash_core/agent/tools/task.py +11 -4
- emdash_core/api/agent.py +154 -3
- emdash_core/ingestion/repository.py +17 -198
- emdash_core/models/agent.py +4 -0
- {emdash_core-0.1.33.dist-info → emdash_core-0.1.37.dist-info}/METADATA +3 -1
- {emdash_core-0.1.33.dist-info → emdash_core-0.1.37.dist-info}/RECORD +20 -19
- {emdash_core-0.1.33.dist-info → emdash_core-0.1.37.dist-info}/WHEEL +0 -0
- {emdash_core-0.1.33.dist-info → emdash_core-0.1.37.dist-info}/entry_points.txt +0 -0
|
@@ -5,12 +5,16 @@ The main agent (in plan mode) writes the plan to .emdash/<feature>.md.
|
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
from pathlib import Path
|
|
8
|
+
from typing import TYPE_CHECKING, Optional
|
|
8
9
|
|
|
9
10
|
from .base import BaseToolkit
|
|
10
11
|
from ..tools.coding import ReadFileTool, ListFilesTool
|
|
11
12
|
from ..tools.search import SemanticSearchTool, GrepTool, GlobTool
|
|
12
13
|
from ...utils.logger import log
|
|
13
14
|
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from ..agents import AgentMCPServerConfig
|
|
17
|
+
|
|
14
18
|
|
|
15
19
|
class PlanToolkit(BaseToolkit):
|
|
16
20
|
"""Read-only toolkit for Plan subagent.
|
|
@@ -24,6 +28,7 @@ class PlanToolkit(BaseToolkit):
|
|
|
24
28
|
- glob: Find files by pattern
|
|
25
29
|
- grep: Search file contents
|
|
26
30
|
- semantic_search: AI-powered code search
|
|
31
|
+
- MCP server tools (if configured)
|
|
27
32
|
"""
|
|
28
33
|
|
|
29
34
|
TOOLS = [
|
|
@@ -34,6 +39,19 @@ class PlanToolkit(BaseToolkit):
|
|
|
34
39
|
"semantic_search",
|
|
35
40
|
]
|
|
36
41
|
|
|
42
|
+
def __init__(
|
|
43
|
+
self,
|
|
44
|
+
repo_root: Path,
|
|
45
|
+
mcp_servers: Optional[list["AgentMCPServerConfig"]] = None,
|
|
46
|
+
):
|
|
47
|
+
"""Initialize the plan toolkit.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
repo_root: Root directory of the repository
|
|
51
|
+
mcp_servers: Optional MCP server configurations for this agent
|
|
52
|
+
"""
|
|
53
|
+
super().__init__(repo_root, mcp_servers=mcp_servers)
|
|
54
|
+
|
|
37
55
|
def _register_tools(self) -> None:
|
|
38
56
|
"""Register read-only exploration tools."""
|
|
39
57
|
# All read-only exploration tools
|
emdash_core/agent/tools/task.py
CHANGED
|
@@ -87,11 +87,15 @@ Multiple sub-agents can be launched in parallel."""
|
|
|
87
87
|
suggestions=["Provide a clear task description in 'prompt'"],
|
|
88
88
|
)
|
|
89
89
|
|
|
90
|
-
available_types = list_agent_types()
|
|
90
|
+
available_types = list_agent_types(self.repo_root)
|
|
91
|
+
log.info(f"TaskTool: repo_root={self.repo_root}, available_types={available_types}")
|
|
91
92
|
if subagent_type not in available_types:
|
|
92
93
|
return ToolResult.error_result(
|
|
93
94
|
f"Unknown agent type: {subagent_type}",
|
|
94
|
-
suggestions=[
|
|
95
|
+
suggestions=[
|
|
96
|
+
f"Available types: {available_types}",
|
|
97
|
+
f"Searched in: {self.repo_root / '.emdash' / 'agents'}",
|
|
98
|
+
],
|
|
95
99
|
)
|
|
96
100
|
|
|
97
101
|
# Log current mode for debugging
|
|
@@ -254,6 +258,9 @@ Multiple sub-agents can be launched in parallel."""
|
|
|
254
258
|
|
|
255
259
|
def get_schema(self) -> dict:
|
|
256
260
|
"""Get OpenAI function schema."""
|
|
261
|
+
# Get available agent types dynamically (includes custom agents)
|
|
262
|
+
available_types = list_agent_types(self.repo_root)
|
|
263
|
+
|
|
257
264
|
return self._make_schema(
|
|
258
265
|
properties={
|
|
259
266
|
"description": {
|
|
@@ -266,8 +273,8 @@ Multiple sub-agents can be launched in parallel."""
|
|
|
266
273
|
},
|
|
267
274
|
"subagent_type": {
|
|
268
275
|
"type": "string",
|
|
269
|
-
"enum":
|
|
270
|
-
"description": "Type of specialized agent",
|
|
276
|
+
"enum": available_types,
|
|
277
|
+
"description": f"Type of specialized agent. Available: {', '.join(available_types)}",
|
|
271
278
|
"default": "Explore",
|
|
272
279
|
},
|
|
273
280
|
"model_tier": {
|
emdash_core/api/agent.py
CHANGED
|
@@ -95,6 +95,7 @@ def _run_agent_sync(
|
|
|
95
95
|
images: list = None,
|
|
96
96
|
plan_mode: bool = False,
|
|
97
97
|
use_sdk: bool = None,
|
|
98
|
+
history: list = None,
|
|
98
99
|
):
|
|
99
100
|
"""Run the agent synchronously (in thread pool).
|
|
100
101
|
|
|
@@ -103,6 +104,9 @@ def _run_agent_sync(
|
|
|
103
104
|
|
|
104
105
|
For Claude models with use_sdk=True, uses the Anthropic Agent SDK.
|
|
105
106
|
For other models, uses the standard AgentRunner with OpenAI-compatible API.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
history: Optional list of previous messages to pre-populate conversation
|
|
106
110
|
"""
|
|
107
111
|
try:
|
|
108
112
|
_ensure_emdash_importable()
|
|
@@ -140,6 +144,12 @@ def _run_agent_sync(
|
|
|
140
144
|
emitter = AgentEventEmitter(agent_name="Emdash Code")
|
|
141
145
|
emitter.add_handler(SSEBridgeHandler(sse_handler))
|
|
142
146
|
|
|
147
|
+
# Add hook handler for user-defined hooks
|
|
148
|
+
from ..agent.hooks import HookHandler, get_hook_manager
|
|
149
|
+
hook_manager = get_hook_manager()
|
|
150
|
+
hook_manager.set_session_id(session_id)
|
|
151
|
+
emitter.add_handler(HookHandler(hook_manager))
|
|
152
|
+
|
|
143
153
|
# Use SDK for Claude models if enabled
|
|
144
154
|
if use_sdk and is_claude_model(model):
|
|
145
155
|
return _run_sdk_agent(
|
|
@@ -152,17 +162,23 @@ def _run_agent_sync(
|
|
|
152
162
|
)
|
|
153
163
|
|
|
154
164
|
# Standard path: use AgentRunner with OpenAI-compatible API
|
|
165
|
+
# Get repo_root from config (set by server on startup)
|
|
166
|
+
from pathlib import Path
|
|
167
|
+
from ..config import get_config
|
|
168
|
+
from ..utils.logger import log
|
|
169
|
+
config = get_config()
|
|
170
|
+
repo_root = Path(config.repo_root) if config.repo_root else Path.cwd()
|
|
171
|
+
log.info(f"Agent API: config.repo_root={config.repo_root}, resolved repo_root={repo_root}")
|
|
172
|
+
|
|
155
173
|
# Create toolkit with plan_mode if requested
|
|
156
174
|
# When in plan mode, generate a plan file path so write_to_file is available
|
|
157
175
|
plan_file_path = None
|
|
158
176
|
if plan_mode:
|
|
159
|
-
from pathlib import Path
|
|
160
|
-
repo_root = Path.cwd()
|
|
161
177
|
plan_file_path = str(repo_root / ".emdash" / "plan.md")
|
|
162
178
|
# Ensure .emdash directory exists
|
|
163
179
|
(repo_root / ".emdash").mkdir(exist_ok=True)
|
|
164
180
|
|
|
165
|
-
toolkit = AgentToolkit(plan_mode=plan_mode, plan_file_path=plan_file_path)
|
|
181
|
+
toolkit = AgentToolkit(repo_root=repo_root, plan_mode=plan_mode, plan_file_path=plan_file_path)
|
|
166
182
|
|
|
167
183
|
runner = AgentRunner(
|
|
168
184
|
toolkit=toolkit,
|
|
@@ -172,6 +188,11 @@ def _run_agent_sync(
|
|
|
172
188
|
emitter=emitter,
|
|
173
189
|
)
|
|
174
190
|
|
|
191
|
+
# Inject pre-loaded conversation history if provided
|
|
192
|
+
if history:
|
|
193
|
+
runner._messages = list(history)
|
|
194
|
+
log.info(f"Injected {len(history)} messages from saved session")
|
|
195
|
+
|
|
175
196
|
# Store session state BEFORE running (so it exists even if interrupted)
|
|
176
197
|
_sessions[session_id] = {
|
|
177
198
|
"runner": runner,
|
|
@@ -180,6 +201,35 @@ def _run_agent_sync(
|
|
|
180
201
|
"plan_mode": plan_mode,
|
|
181
202
|
}
|
|
182
203
|
|
|
204
|
+
# Set up autosave callback if enabled via env var
|
|
205
|
+
import os
|
|
206
|
+
import json
|
|
207
|
+
if os.environ.get("EMDASH_SESSION_AUTOSAVE", "").lower() == "true":
|
|
208
|
+
sessions_dir = repo_root / ".emdash" / "sessions"
|
|
209
|
+
sessions_dir.mkdir(parents=True, exist_ok=True)
|
|
210
|
+
autosave_path = sessions_dir / "_autosave.json"
|
|
211
|
+
|
|
212
|
+
def autosave_callback(messages):
|
|
213
|
+
"""Save messages to autosave file after each iteration."""
|
|
214
|
+
try:
|
|
215
|
+
# Limit to last 10 messages
|
|
216
|
+
trimmed = messages[-10:] if len(messages) > 10 else messages
|
|
217
|
+
autosave_data = {
|
|
218
|
+
"name": "_autosave",
|
|
219
|
+
"messages": trimmed,
|
|
220
|
+
"model": model,
|
|
221
|
+
"mode": "plan" if plan_mode else "code",
|
|
222
|
+
"session_id": session_id,
|
|
223
|
+
}
|
|
224
|
+
with open(autosave_path, "w") as f:
|
|
225
|
+
json.dump(autosave_data, f, indent=2, default=str)
|
|
226
|
+
log.debug(f"Autosaved {len(trimmed)} messages to {autosave_path}")
|
|
227
|
+
except Exception as e:
|
|
228
|
+
log.debug(f"Autosave failed: {e}")
|
|
229
|
+
|
|
230
|
+
runner._on_iteration_callback = autosave_callback
|
|
231
|
+
log.info("Session autosave enabled")
|
|
232
|
+
|
|
183
233
|
# Convert image data if provided
|
|
184
234
|
agent_images = None
|
|
185
235
|
if images:
|
|
@@ -239,6 +289,8 @@ async def _run_agent_async(
|
|
|
239
289
|
session_id,
|
|
240
290
|
request.images,
|
|
241
291
|
plan_mode,
|
|
292
|
+
None, # use_sdk (auto-detect)
|
|
293
|
+
request.history, # Pre-loaded conversation history
|
|
242
294
|
)
|
|
243
295
|
|
|
244
296
|
# Emit session end
|
|
@@ -411,6 +463,48 @@ async def delete_session(session_id: str):
|
|
|
411
463
|
raise HTTPException(status_code=404, detail="Session not found")
|
|
412
464
|
|
|
413
465
|
|
|
466
|
+
@router.get("/chat/{session_id}/export")
|
|
467
|
+
async def export_session(session_id: str, limit: int = 10):
|
|
468
|
+
"""Export session messages for persistence.
|
|
469
|
+
|
|
470
|
+
Args:
|
|
471
|
+
session_id: The session ID
|
|
472
|
+
limit: Maximum number of messages to return (default 10)
|
|
473
|
+
|
|
474
|
+
Returns:
|
|
475
|
+
JSON with messages array and metadata
|
|
476
|
+
"""
|
|
477
|
+
if session_id not in _sessions:
|
|
478
|
+
raise HTTPException(status_code=404, detail="Session not found")
|
|
479
|
+
|
|
480
|
+
session = _sessions[session_id]
|
|
481
|
+
runner = session.get("runner")
|
|
482
|
+
|
|
483
|
+
if not runner:
|
|
484
|
+
return {
|
|
485
|
+
"session_id": session_id,
|
|
486
|
+
"messages": [],
|
|
487
|
+
"message_count": 0,
|
|
488
|
+
"model": session.get("model"),
|
|
489
|
+
"mode": "plan" if session.get("plan_mode") else "code",
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
# Get messages from runner
|
|
493
|
+
messages = getattr(runner, "_messages", [])
|
|
494
|
+
|
|
495
|
+
# Trim to limit (most recent)
|
|
496
|
+
if len(messages) > limit:
|
|
497
|
+
messages = messages[-limit:]
|
|
498
|
+
|
|
499
|
+
return {
|
|
500
|
+
"session_id": session_id,
|
|
501
|
+
"messages": messages,
|
|
502
|
+
"message_count": len(messages),
|
|
503
|
+
"model": session.get("model"),
|
|
504
|
+
"mode": "plan" if session.get("plan_mode") else "code",
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
|
|
414
508
|
@router.get("/chat/{session_id}/plan")
|
|
415
509
|
async def get_pending_plan(session_id: str):
|
|
416
510
|
"""Get the pending plan for a session, if any.
|
|
@@ -860,3 +954,60 @@ async def reject_plan_mode(session_id: str, feedback: str = ""):
|
|
|
860
954
|
"X-Session-ID": session_id,
|
|
861
955
|
},
|
|
862
956
|
)
|
|
957
|
+
|
|
958
|
+
|
|
959
|
+
@router.get("/chat/{session_id}/todos")
|
|
960
|
+
async def get_todos(session_id: str):
|
|
961
|
+
"""Get the current todo list for a session.
|
|
962
|
+
|
|
963
|
+
Returns the agent's task list including status of each item.
|
|
964
|
+
"""
|
|
965
|
+
if session_id not in _sessions:
|
|
966
|
+
raise HTTPException(status_code=404, detail="Session not found")
|
|
967
|
+
|
|
968
|
+
# Get todos from TaskState singleton
|
|
969
|
+
from ..agent.tools.tasks import TaskState
|
|
970
|
+
state = TaskState.get_instance()
|
|
971
|
+
|
|
972
|
+
todos = state.get_all_tasks()
|
|
973
|
+
|
|
974
|
+
# Count by status
|
|
975
|
+
pending = sum(1 for t in todos if t["status"] == "pending")
|
|
976
|
+
in_progress = sum(1 for t in todos if t["status"] == "in_progress")
|
|
977
|
+
completed = sum(1 for t in todos if t["status"] == "completed")
|
|
978
|
+
|
|
979
|
+
return {
|
|
980
|
+
"session_id": session_id,
|
|
981
|
+
"todos": todos,
|
|
982
|
+
"summary": {
|
|
983
|
+
"total": len(todos),
|
|
984
|
+
"pending": pending,
|
|
985
|
+
"in_progress": in_progress,
|
|
986
|
+
"completed": completed,
|
|
987
|
+
},
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
|
|
991
|
+
@router.post("/chat/{session_id}/todos")
|
|
992
|
+
async def add_todo(session_id: str, title: str, description: str = ""):
|
|
993
|
+
"""Add a new todo item to the agent's task list.
|
|
994
|
+
|
|
995
|
+
This allows users to inject tasks for the agent to work on.
|
|
996
|
+
"""
|
|
997
|
+
if session_id not in _sessions:
|
|
998
|
+
raise HTTPException(status_code=404, detail="Session not found")
|
|
999
|
+
|
|
1000
|
+
if not title or not title.strip():
|
|
1001
|
+
raise HTTPException(status_code=400, detail="Title is required")
|
|
1002
|
+
|
|
1003
|
+
# Add todo via TaskState singleton
|
|
1004
|
+
from ..agent.tools.tasks import TaskState
|
|
1005
|
+
state = TaskState.get_instance()
|
|
1006
|
+
|
|
1007
|
+
task = state.add_task(title=title.strip(), description=description.strip())
|
|
1008
|
+
|
|
1009
|
+
return {
|
|
1010
|
+
"session_id": session_id,
|
|
1011
|
+
"task": task.to_dict(),
|
|
1012
|
+
"total_tasks": len(state.tasks),
|
|
1013
|
+
}
|
|
@@ -1,15 +1,9 @@
|
|
|
1
|
-
"""Repository management
|
|
1
|
+
"""Repository management for local Git repositories."""
|
|
2
2
|
|
|
3
|
-
import hashlib
|
|
4
|
-
import os
|
|
5
|
-
import shutil
|
|
6
|
-
from datetime import datetime
|
|
7
3
|
from pathlib import Path
|
|
8
4
|
from typing import Optional
|
|
9
|
-
from urllib.parse import urlparse
|
|
10
5
|
|
|
11
|
-
import
|
|
12
|
-
from git import Repo, GitCommandError
|
|
6
|
+
from git import Repo
|
|
13
7
|
|
|
14
8
|
from ..core.exceptions import RepositoryError
|
|
15
9
|
from ..core.models import RepositoryEntity
|
|
@@ -17,44 +11,37 @@ from ..utils.logger import log
|
|
|
17
11
|
|
|
18
12
|
|
|
19
13
|
class RepositoryManager:
|
|
20
|
-
"""Manages Git repository operations
|
|
14
|
+
"""Manages local Git repository operations."""
|
|
21
15
|
|
|
22
|
-
def __init__(self
|
|
23
|
-
"""Initialize repository manager.
|
|
24
|
-
|
|
25
|
-
Args:
|
|
26
|
-
cache_dir: Directory to cache cloned repositories.
|
|
27
|
-
Defaults to ~/.emdash/repos
|
|
28
|
-
"""
|
|
29
|
-
if cache_dir is None:
|
|
30
|
-
cache_dir = Path.home() / ".emdash" / "repos"
|
|
31
|
-
|
|
32
|
-
self.cache_dir = cache_dir
|
|
33
|
-
self.cache_dir.mkdir(parents=True, exist_ok=True)
|
|
16
|
+
def __init__(self):
|
|
17
|
+
"""Initialize repository manager."""
|
|
18
|
+
pass
|
|
34
19
|
|
|
35
20
|
def get_or_clone(
|
|
36
21
|
self,
|
|
37
22
|
repo_path: str,
|
|
38
23
|
skip_commit_count: bool = False
|
|
39
24
|
) -> tuple[Repo, RepositoryEntity]:
|
|
40
|
-
"""Get a repository
|
|
25
|
+
"""Get a local repository.
|
|
41
26
|
|
|
42
27
|
Args:
|
|
43
|
-
repo_path:
|
|
44
|
-
skip_commit_count: Whether to skip counting commits
|
|
28
|
+
repo_path: Local path to repository
|
|
29
|
+
skip_commit_count: Whether to skip counting commits (unused, kept for API compatibility)
|
|
45
30
|
|
|
46
31
|
Returns:
|
|
47
32
|
Tuple of (git.Repo, RepositoryEntity)
|
|
48
33
|
|
|
49
34
|
Raises:
|
|
50
|
-
RepositoryError: If repository cannot be accessed
|
|
35
|
+
RepositoryError: If repository cannot be accessed or path doesn't exist
|
|
51
36
|
"""
|
|
52
|
-
#
|
|
53
|
-
if Path(repo_path).exists():
|
|
54
|
-
|
|
37
|
+
# Only support local paths
|
|
38
|
+
if not Path(repo_path).exists():
|
|
39
|
+
raise RepositoryError(
|
|
40
|
+
f"Repository path does not exist: {repo_path}. "
|
|
41
|
+
"Remote repository URLs are not supported - please provide a local path."
|
|
42
|
+
)
|
|
55
43
|
|
|
56
|
-
|
|
57
|
-
return self._clone_or_fetch(repo_path, skip_commit_count)
|
|
44
|
+
return self._open_local_repo(repo_path)
|
|
58
45
|
|
|
59
46
|
def _open_local_repo(self, path: str) -> tuple[Repo, RepositoryEntity]:
|
|
60
47
|
"""Open a local repository.
|
|
@@ -88,164 +75,6 @@ class RepositoryManager:
|
|
|
88
75
|
except Exception as e:
|
|
89
76
|
raise RepositoryError(f"Failed to open local repository {path}: {e}")
|
|
90
77
|
|
|
91
|
-
def _clone_or_fetch(
|
|
92
|
-
self,
|
|
93
|
-
url: str,
|
|
94
|
-
skip_commit_count: bool
|
|
95
|
-
) -> tuple[Repo, RepositoryEntity]:
|
|
96
|
-
"""Clone a repository or fetch updates if already cloned.
|
|
97
|
-
|
|
98
|
-
Args:
|
|
99
|
-
url: Repository URL
|
|
100
|
-
skip_commit_count: Whether to skip counting commits
|
|
101
|
-
|
|
102
|
-
Returns:
|
|
103
|
-
Tuple of (git.Repo, RepositoryEntity)
|
|
104
|
-
"""
|
|
105
|
-
# Generate cache path from URL
|
|
106
|
-
cache_path = self._get_cache_path(url)
|
|
107
|
-
|
|
108
|
-
if cache_path.exists():
|
|
109
|
-
log.info(f"Repository already cached at {cache_path}")
|
|
110
|
-
return self._fetch_updates(cache_path, url, skip_commit_count)
|
|
111
|
-
else:
|
|
112
|
-
log.info(f"Cloning repository: {url}")
|
|
113
|
-
return self._clone_repo(url, cache_path, skip_commit_count)
|
|
114
|
-
|
|
115
|
-
def _clone_repo(
|
|
116
|
-
self,
|
|
117
|
-
url: str,
|
|
118
|
-
cache_path: Path,
|
|
119
|
-
skip_commit_count: bool
|
|
120
|
-
) -> tuple[Repo, RepositoryEntity]:
|
|
121
|
-
"""Clone a repository.
|
|
122
|
-
|
|
123
|
-
Args:
|
|
124
|
-
url: Repository URL
|
|
125
|
-
cache_path: Path to clone into
|
|
126
|
-
skip_commit_count: Whether to skip counting commits
|
|
127
|
-
|
|
128
|
-
Returns:
|
|
129
|
-
Tuple of (git.Repo, RepositoryEntity)
|
|
130
|
-
"""
|
|
131
|
-
try:
|
|
132
|
-
repo = Repo.clone_from(url, cache_path, depth=None)
|
|
133
|
-
log.info(f"Successfully cloned {url}")
|
|
134
|
-
|
|
135
|
-
entity = self._create_repository_entity(
|
|
136
|
-
repo,
|
|
137
|
-
url,
|
|
138
|
-
skip_commit_count=skip_commit_count
|
|
139
|
-
)
|
|
140
|
-
return repo, entity
|
|
141
|
-
|
|
142
|
-
except GitCommandError as e:
|
|
143
|
-
raise RepositoryError(f"Failed to clone repository {url}: {e}")
|
|
144
|
-
except Exception as e:
|
|
145
|
-
raise RepositoryError(f"Unexpected error cloning {url}: {e}")
|
|
146
|
-
|
|
147
|
-
def _fetch_updates(
|
|
148
|
-
self,
|
|
149
|
-
cache_path: Path,
|
|
150
|
-
url: str,
|
|
151
|
-
skip_commit_count: bool
|
|
152
|
-
) -> tuple[Repo, RepositoryEntity]:
|
|
153
|
-
"""Fetch updates for an existing repository.
|
|
154
|
-
|
|
155
|
-
Args:
|
|
156
|
-
cache_path: Path to cached repository
|
|
157
|
-
url: Repository URL
|
|
158
|
-
skip_commit_count: Whether to skip counting commits
|
|
159
|
-
|
|
160
|
-
Returns:
|
|
161
|
-
Tuple of (git.Repo, RepositoryEntity)
|
|
162
|
-
"""
|
|
163
|
-
try:
|
|
164
|
-
repo = Repo(cache_path)
|
|
165
|
-
|
|
166
|
-
log.info("Fetching updates from remote...")
|
|
167
|
-
repo.remotes.origin.fetch()
|
|
168
|
-
|
|
169
|
-
# Pull latest changes
|
|
170
|
-
repo.remotes.origin.pull()
|
|
171
|
-
|
|
172
|
-
log.info("Repository updated successfully")
|
|
173
|
-
|
|
174
|
-
entity = self._create_repository_entity(
|
|
175
|
-
repo,
|
|
176
|
-
url,
|
|
177
|
-
skip_commit_count=skip_commit_count
|
|
178
|
-
)
|
|
179
|
-
return repo, entity
|
|
180
|
-
|
|
181
|
-
except GitCommandError as e:
|
|
182
|
-
raise RepositoryError(f"Failed to fetch updates for {url}: {e}")
|
|
183
|
-
except Exception as e:
|
|
184
|
-
raise RepositoryError(f"Unexpected error fetching updates: {e}")
|
|
185
|
-
|
|
186
|
-
def _create_repository_entity(
|
|
187
|
-
self,
|
|
188
|
-
repo: Repo,
|
|
189
|
-
url: str,
|
|
190
|
-
skip_commit_count: bool = False
|
|
191
|
-
) -> RepositoryEntity:
|
|
192
|
-
"""Create a RepositoryEntity from a git.Repo.
|
|
193
|
-
|
|
194
|
-
Args:
|
|
195
|
-
repo: Git repository
|
|
196
|
-
url: Repository URL
|
|
197
|
-
skip_commit_count: Whether to skip counting commits
|
|
198
|
-
|
|
199
|
-
Returns:
|
|
200
|
-
RepositoryEntity
|
|
201
|
-
"""
|
|
202
|
-
# Parse URL to extract owner and name
|
|
203
|
-
parsed = urlparse(url)
|
|
204
|
-
path_parts = parsed.path.strip("/").split("/")
|
|
205
|
-
|
|
206
|
-
if len(path_parts) >= 2:
|
|
207
|
-
owner = path_parts[-2]
|
|
208
|
-
repo_name = path_parts[-1].replace(".git", "")
|
|
209
|
-
else:
|
|
210
|
-
owner = None
|
|
211
|
-
repo_name = path_parts[-1].replace(".git", "") if path_parts else "unknown"
|
|
212
|
-
|
|
213
|
-
commit_count = 0
|
|
214
|
-
if not skip_commit_count:
|
|
215
|
-
try:
|
|
216
|
-
commit_count = sum(1 for _ in repo.iter_commits())
|
|
217
|
-
except Exception:
|
|
218
|
-
commit_count = 0
|
|
219
|
-
|
|
220
|
-
return RepositoryEntity(
|
|
221
|
-
url=url,
|
|
222
|
-
name=repo_name,
|
|
223
|
-
owner=owner,
|
|
224
|
-
default_branch=repo.active_branch.name if repo.active_branch else "main",
|
|
225
|
-
last_ingested=None,
|
|
226
|
-
ingestion_status="pending",
|
|
227
|
-
commit_count=commit_count,
|
|
228
|
-
)
|
|
229
|
-
|
|
230
|
-
def _get_cache_path(self, url: str) -> Path:
|
|
231
|
-
"""Get the cache path for a repository URL.
|
|
232
|
-
|
|
233
|
-
Args:
|
|
234
|
-
url: Repository URL
|
|
235
|
-
|
|
236
|
-
Returns:
|
|
237
|
-
Path to cache directory
|
|
238
|
-
"""
|
|
239
|
-
# Create a unique directory name from URL
|
|
240
|
-
url_hash = hashlib.md5(url.encode()).hexdigest()[:12]
|
|
241
|
-
|
|
242
|
-
# Extract repo name from URL
|
|
243
|
-
parsed = urlparse(url)
|
|
244
|
-
path_parts = parsed.path.strip("/").split("/")
|
|
245
|
-
repo_name = path_parts[-1].replace(".git", "")
|
|
246
|
-
|
|
247
|
-
return self.cache_dir / f"{repo_name}_{url_hash}"
|
|
248
|
-
|
|
249
78
|
def _get_origin_url(self, repo: Repo) -> Optional[str]:
|
|
250
79
|
"""Get the origin URL of a repository.
|
|
251
80
|
|
|
@@ -334,13 +163,3 @@ class RepositoryManager:
|
|
|
334
163
|
This is a convenience wrapper around get_source_files() for backward compatibility.
|
|
335
164
|
"""
|
|
336
165
|
return self.get_source_files(repo, ['.py'], ignore_patterns)
|
|
337
|
-
|
|
338
|
-
def clear_cache(self):
|
|
339
|
-
"""Clear all cached repositories."""
|
|
340
|
-
log.warning("Clearing repository cache...")
|
|
341
|
-
|
|
342
|
-
if self.cache_dir.exists():
|
|
343
|
-
shutil.rmtree(self.cache_dir)
|
|
344
|
-
self.cache_dir.mkdir(parents=True, exist_ok=True)
|
|
345
|
-
|
|
346
|
-
log.info("Cache cleared successfully")
|
emdash_core/models/agent.py
CHANGED
|
@@ -51,6 +51,10 @@ class AgentChatRequest(BaseModel):
|
|
|
51
51
|
default_factory=list,
|
|
52
52
|
description="Images for vision-capable models"
|
|
53
53
|
)
|
|
54
|
+
history: list[dict] = Field(
|
|
55
|
+
default_factory=list,
|
|
56
|
+
description="Pre-loaded conversation history from saved session"
|
|
57
|
+
)
|
|
54
58
|
options: AgentChatOptions = Field(
|
|
55
59
|
default_factory=AgentChatOptions,
|
|
56
60
|
description="Agent options"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: emdash-core
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.37
|
|
4
4
|
Summary: EmDash Core - FastAPI server for code intelligence
|
|
5
5
|
Author: Em Dash Team
|
|
6
6
|
Requires-Python: >=3.10,<4.0
|
|
@@ -13,6 +13,7 @@ Classifier: Programming Language :: Python :: 3.14
|
|
|
13
13
|
Requires-Dist: astroid (>=3.0.1,<4.0.0)
|
|
14
14
|
Requires-Dist: beautifulsoup4 (>=4.12.0)
|
|
15
15
|
Requires-Dist: claude-agent-sdk (>=0.1.19)
|
|
16
|
+
Requires-Dist: cmake (>=3.25.0)
|
|
16
17
|
Requires-Dist: duckduckgo-search (>=6.0.0)
|
|
17
18
|
Requires-Dist: fastapi (>=0.109.0)
|
|
18
19
|
Requires-Dist: gitpython (>=3.1.40,<4.0.0)
|
|
@@ -28,6 +29,7 @@ Requires-Dist: pydantic-settings (>=2.0.0,<3.0.0)
|
|
|
28
29
|
Requires-Dist: pygithub (>=2.1.1,<3.0.0)
|
|
29
30
|
Requires-Dist: python-dotenv (>=1.0.0,<2.0.0)
|
|
30
31
|
Requires-Dist: python-louvain (>=0.16,<0.17)
|
|
32
|
+
Requires-Dist: pyyaml (>=6.0,<7.0)
|
|
31
33
|
Requires-Dist: sentence-transformers (>=2.2.0)
|
|
32
34
|
Requires-Dist: sse-starlette (>=2.0.0)
|
|
33
35
|
Requires-Dist: supabase (>=2.0.0)
|