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,454 @@
|
|
|
1
|
+
"""Configuration management for EmDash."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, Field
|
|
8
|
+
from dotenv import load_dotenv
|
|
9
|
+
|
|
10
|
+
from .exceptions import ConfigurationError
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def get_config_dir() -> Path:
|
|
14
|
+
"""Get the emdash config directory (~/.config/emdash)."""
|
|
15
|
+
return Path.home() / ".config" / "emdash"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def get_user_config_path() -> Path:
|
|
19
|
+
"""Get the user config file path (~/.config/emdash/config)."""
|
|
20
|
+
return get_config_dir() / "config"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def get_local_env_path() -> Path:
|
|
24
|
+
"""Get the .env file path in the current working directory."""
|
|
25
|
+
return Path.cwd() / ".env"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def load_env_files() -> None:
|
|
29
|
+
"""Load environment variables from config files.
|
|
30
|
+
|
|
31
|
+
Load order (later files override earlier):
|
|
32
|
+
1. ~/.config/emdash/config (user-level defaults)
|
|
33
|
+
2. .env in current working directory (project-level overrides)
|
|
34
|
+
"""
|
|
35
|
+
# Load user-level config first (lower priority)
|
|
36
|
+
user_config = get_user_config_path()
|
|
37
|
+
if user_config.exists():
|
|
38
|
+
load_dotenv(user_config, override=False)
|
|
39
|
+
|
|
40
|
+
# Load local .env from current working directory (higher priority)
|
|
41
|
+
local_env = get_local_env_path()
|
|
42
|
+
if local_env.exists():
|
|
43
|
+
load_dotenv(local_env, override=True)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# Load environment variables from config files
|
|
47
|
+
load_env_files()
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class KuzuConfig(BaseModel):
|
|
51
|
+
"""Kuzu embedded database configuration."""
|
|
52
|
+
|
|
53
|
+
database_path: str = Field(default=".emdash/index/kuzu_db")
|
|
54
|
+
read_only: bool = Field(default=False)
|
|
55
|
+
|
|
56
|
+
@classmethod
|
|
57
|
+
def from_env(cls) -> "KuzuConfig":
|
|
58
|
+
"""Load configuration from environment variables."""
|
|
59
|
+
return cls(
|
|
60
|
+
database_path=os.getenv("KUZU_DATABASE_PATH", ".emdash/index/kuzu_db"),
|
|
61
|
+
read_only=os.getenv("KUZU_READ_ONLY", "false").lower() == "true",
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class IngestionConfig(BaseModel):
|
|
66
|
+
"""Configuration for repository ingestion."""
|
|
67
|
+
|
|
68
|
+
max_workers: int = Field(default=4, ge=1, le=16)
|
|
69
|
+
batch_size: int = Field(default=1000, ge=100, le=10000)
|
|
70
|
+
git_depth: Optional[int] = Field(default=None)
|
|
71
|
+
ast_only: bool = Field(default=False, description="Skip Layer B (git) and Layer C (analytics)")
|
|
72
|
+
ignore_patterns: list[str] = Field(
|
|
73
|
+
default_factory=lambda: [
|
|
74
|
+
"__pycache__",
|
|
75
|
+
"*.pyc",
|
|
76
|
+
"*.pyo",
|
|
77
|
+
".git",
|
|
78
|
+
".venv",
|
|
79
|
+
"venv",
|
|
80
|
+
"env",
|
|
81
|
+
"node_modules",
|
|
82
|
+
".tox",
|
|
83
|
+
".pytest_cache",
|
|
84
|
+
"*.egg-info",
|
|
85
|
+
# Build outputs - minified files slow down indexing significantly
|
|
86
|
+
"dist",
|
|
87
|
+
"build",
|
|
88
|
+
".next",
|
|
89
|
+
".nuxt",
|
|
90
|
+
".output",
|
|
91
|
+
"_astro",
|
|
92
|
+
"*.min.js",
|
|
93
|
+
"*.min.css",
|
|
94
|
+
"*.bundle.js",
|
|
95
|
+
"*.chunk.js",
|
|
96
|
+
# Other common excludes
|
|
97
|
+
"coverage",
|
|
98
|
+
".nyc_output",
|
|
99
|
+
"*.map",
|
|
100
|
+
]
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
@classmethod
|
|
104
|
+
def from_env(cls) -> "IngestionConfig":
|
|
105
|
+
"""Load configuration from environment variables."""
|
|
106
|
+
git_depth = os.getenv("GIT_DEPTH")
|
|
107
|
+
return cls(
|
|
108
|
+
max_workers=int(os.getenv("MAX_WORKERS", "4")),
|
|
109
|
+
batch_size=int(os.getenv("BATCH_SIZE", "1000")),
|
|
110
|
+
git_depth=int(git_depth) if git_depth else None,
|
|
111
|
+
ast_only=os.getenv("AST_ONLY", "false").lower() == "true",
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class AnalyticsConfig(BaseModel):
|
|
116
|
+
"""Configuration for graph analytics."""
|
|
117
|
+
|
|
118
|
+
pagerank_iterations: int = Field(default=20, ge=5, le=100)
|
|
119
|
+
pagerank_damping: float = Field(default=0.85, ge=0.0, le=1.0)
|
|
120
|
+
clustering_algorithm: str = Field(default="louvain")
|
|
121
|
+
|
|
122
|
+
@classmethod
|
|
123
|
+
def from_env(cls) -> "AnalyticsConfig":
|
|
124
|
+
"""Load configuration from environment variables."""
|
|
125
|
+
return cls(
|
|
126
|
+
pagerank_iterations=int(os.getenv("PAGERANK_ITERATIONS", "20")),
|
|
127
|
+
pagerank_damping=float(os.getenv("PAGERANK_DAMPING", "0.85")),
|
|
128
|
+
clustering_algorithm=os.getenv("CLUSTERING_ALGORITHM", "louvain"),
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class GitHubConfig(BaseModel):
|
|
133
|
+
"""Configuration for GitHub API access."""
|
|
134
|
+
|
|
135
|
+
token: Optional[str] = Field(default=None)
|
|
136
|
+
|
|
137
|
+
@classmethod
|
|
138
|
+
def from_env(cls) -> "GitHubConfig":
|
|
139
|
+
"""Load configuration from environment variables."""
|
|
140
|
+
return cls(token=os.getenv("GITHUB_TOKEN"))
|
|
141
|
+
|
|
142
|
+
@property
|
|
143
|
+
def is_available(self) -> bool:
|
|
144
|
+
"""Check if GitHub token is configured."""
|
|
145
|
+
return self.token is not None and len(self.token) > 0
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
class OpenAIConfig(BaseModel):
|
|
149
|
+
"""Configuration for OpenAI API (embeddings and chat)."""
|
|
150
|
+
|
|
151
|
+
api_key: Optional[str] = Field(default=None)
|
|
152
|
+
model: str = Field(default="text-embedding-3-small")
|
|
153
|
+
dimensions: int = Field(default=1536)
|
|
154
|
+
batch_size: int = Field(default=100, ge=1, le=2000)
|
|
155
|
+
|
|
156
|
+
@classmethod
|
|
157
|
+
def from_env(cls) -> "OpenAIConfig":
|
|
158
|
+
"""Load configuration from environment variables."""
|
|
159
|
+
return cls(
|
|
160
|
+
api_key=os.getenv("OPENAI_API_KEY"),
|
|
161
|
+
model=os.getenv("OPENAI_EMBEDDING_MODEL", "text-embedding-3-small"),
|
|
162
|
+
dimensions=int(os.getenv("OPENAI_EMBEDDING_DIMENSIONS", "1536")),
|
|
163
|
+
batch_size=int(os.getenv("OPENAI_BATCH_SIZE", "100")),
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
@property
|
|
167
|
+
def is_available(self) -> bool:
|
|
168
|
+
"""Check if OpenAI API key is configured."""
|
|
169
|
+
return self.api_key is not None and len(self.api_key) > 0
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
class AnthropicConfig(BaseModel):
|
|
173
|
+
"""Configuration for Anthropic API (chat)."""
|
|
174
|
+
|
|
175
|
+
api_key: Optional[str] = Field(default=None)
|
|
176
|
+
|
|
177
|
+
@classmethod
|
|
178
|
+
def from_env(cls) -> "AnthropicConfig":
|
|
179
|
+
"""Load configuration from environment variables."""
|
|
180
|
+
return cls(
|
|
181
|
+
api_key=os.getenv("ANTHROPIC_API_KEY"),
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
@property
|
|
185
|
+
def is_available(self) -> bool:
|
|
186
|
+
"""Check if Anthropic API key is configured."""
|
|
187
|
+
return self.api_key is not None and len(self.api_key) > 0
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
class FireworksConfig(BaseModel):
|
|
191
|
+
"""Configuration for Fireworks AI API (embeddings)."""
|
|
192
|
+
|
|
193
|
+
api_key: Optional[str] = Field(default=None)
|
|
194
|
+
batch_size: int = Field(default=100, ge=1, le=500)
|
|
195
|
+
|
|
196
|
+
@classmethod
|
|
197
|
+
def from_env(cls) -> "FireworksConfig":
|
|
198
|
+
"""Load configuration from environment variables."""
|
|
199
|
+
return cls(
|
|
200
|
+
api_key=os.getenv("FIREWORKS_API_KEY"),
|
|
201
|
+
batch_size=int(os.getenv("FIREWORKS_BATCH_SIZE", "100")),
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
@property
|
|
205
|
+
def is_available(self) -> bool:
|
|
206
|
+
"""Check if Fireworks API key is configured."""
|
|
207
|
+
return self.api_key is not None and len(self.api_key) > 0
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
class ContextConfig(BaseModel):
|
|
211
|
+
"""Configuration for session context providers."""
|
|
212
|
+
|
|
213
|
+
providers: list[str] = Field(
|
|
214
|
+
default_factory=lambda: ["touched_areas"],
|
|
215
|
+
description="Comma-separated list of context provider names",
|
|
216
|
+
)
|
|
217
|
+
min_score: float = Field(
|
|
218
|
+
default=0.3,
|
|
219
|
+
ge=0.0,
|
|
220
|
+
le=1.0,
|
|
221
|
+
description="Minimum score threshold to include context items",
|
|
222
|
+
)
|
|
223
|
+
decay_factor: float = Field(
|
|
224
|
+
default=0.8,
|
|
225
|
+
ge=0.0,
|
|
226
|
+
le=1.0,
|
|
227
|
+
description="Factor to multiply existing scores on new touch",
|
|
228
|
+
)
|
|
229
|
+
neighbor_depth: int = Field(
|
|
230
|
+
default=2,
|
|
231
|
+
ge=1,
|
|
232
|
+
le=5,
|
|
233
|
+
description="Number of hops for AST neighbor traversal",
|
|
234
|
+
)
|
|
235
|
+
max_items: int = Field(
|
|
236
|
+
default=50,
|
|
237
|
+
ge=1,
|
|
238
|
+
le=200,
|
|
239
|
+
description="Maximum number of context items to include",
|
|
240
|
+
)
|
|
241
|
+
enabled: bool = Field(
|
|
242
|
+
default=True,
|
|
243
|
+
description="Whether session context is enabled",
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
@classmethod
|
|
247
|
+
def from_env(cls) -> "ContextConfig":
|
|
248
|
+
"""Load configuration from environment variables."""
|
|
249
|
+
providers_str = os.getenv("CONTEXT_PROVIDERS", "touched_areas,explored_areas")
|
|
250
|
+
return cls(
|
|
251
|
+
providers=[p.strip() for p in providers_str.split(",") if p.strip()],
|
|
252
|
+
min_score=float(os.getenv("CONTEXT_MIN_SCORE", "0.5")),
|
|
253
|
+
decay_factor=float(os.getenv("CONTEXT_DECAY_FACTOR", "0.8")),
|
|
254
|
+
neighbor_depth=int(os.getenv("CONTEXT_NEIGHBOR_DEPTH", "2")),
|
|
255
|
+
max_items=int(os.getenv("CONTEXT_MAX_ITEMS", "50")),
|
|
256
|
+
enabled=os.getenv("CONTEXT_ENABLED", "true").lower() == "true",
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
class AgentConfig(BaseModel):
|
|
261
|
+
"""Configuration for agent behavior."""
|
|
262
|
+
|
|
263
|
+
max_context_messages: int = Field(
|
|
264
|
+
default=25,
|
|
265
|
+
ge=5,
|
|
266
|
+
le=100,
|
|
267
|
+
description="Maximum number of previous messages to send to LLM (excludes system prompt)",
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
max_iterations: int = Field(
|
|
271
|
+
default=50,
|
|
272
|
+
ge=10,
|
|
273
|
+
le=200,
|
|
274
|
+
description="Maximum tool call iterations before stopping",
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
tool_max_output_tokens: int = Field(
|
|
278
|
+
default=25000,
|
|
279
|
+
ge=1000,
|
|
280
|
+
le=100000,
|
|
281
|
+
description="Maximum tokens for tool output (estimated at ~4 chars/token)",
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
@classmethod
|
|
285
|
+
def from_env(cls) -> "AgentConfig":
|
|
286
|
+
"""Load configuration from environment variables."""
|
|
287
|
+
return cls(
|
|
288
|
+
max_context_messages=int(os.getenv("EMDASH_MAX_CONTEXT_MESSAGES", "25")),
|
|
289
|
+
max_iterations=int(os.getenv("EMDASH_MAX_ITERATIONS", "50")),
|
|
290
|
+
tool_max_output_tokens=int(os.getenv("EMDASH_TOOL_MAX_OUTPUT", "25000")),
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
class MCPConfig(BaseModel):
|
|
295
|
+
"""Configuration for MCP (Model Context Protocol) servers."""
|
|
296
|
+
|
|
297
|
+
github_token: Optional[str] = Field(default=None)
|
|
298
|
+
github_repo: Optional[str] = Field(default=None) # Format: "owner/repo"
|
|
299
|
+
server_mode: str = Field(default="local") # "local" | "docker"
|
|
300
|
+
toolsets: list[str] = Field(
|
|
301
|
+
default_factory=lambda: ["repos", "pull_requests", "issues", "code_security"]
|
|
302
|
+
)
|
|
303
|
+
binary_path: str = Field(default="github-mcp-server")
|
|
304
|
+
read_only: bool = Field(default=True)
|
|
305
|
+
timeout: int = Field(default=30)
|
|
306
|
+
|
|
307
|
+
@staticmethod
|
|
308
|
+
def _get_gh_cli_token() -> Optional[str]:
|
|
309
|
+
"""Get GitHub token from gh CLI if available (supports browser auth)."""
|
|
310
|
+
import subprocess
|
|
311
|
+
try:
|
|
312
|
+
result = subprocess.run(
|
|
313
|
+
["gh", "auth", "token"],
|
|
314
|
+
capture_output=True,
|
|
315
|
+
text=True,
|
|
316
|
+
timeout=5,
|
|
317
|
+
)
|
|
318
|
+
if result.returncode == 0 and result.stdout.strip():
|
|
319
|
+
return result.stdout.strip()
|
|
320
|
+
except (subprocess.TimeoutExpired, FileNotFoundError, Exception):
|
|
321
|
+
pass
|
|
322
|
+
return None
|
|
323
|
+
|
|
324
|
+
@staticmethod
|
|
325
|
+
def _get_repo_from_git() -> Optional[str]:
|
|
326
|
+
"""Auto-detect owner/repo from git remote origin URL."""
|
|
327
|
+
import subprocess
|
|
328
|
+
import re
|
|
329
|
+
try:
|
|
330
|
+
result = subprocess.run(
|
|
331
|
+
["git", "remote", "get-url", "origin"],
|
|
332
|
+
capture_output=True,
|
|
333
|
+
text=True,
|
|
334
|
+
timeout=5,
|
|
335
|
+
)
|
|
336
|
+
if result.returncode == 0 and result.stdout.strip():
|
|
337
|
+
url = result.stdout.strip()
|
|
338
|
+
# Handle SSH: git@github.com:owner/repo.git
|
|
339
|
+
ssh_match = re.search(r"git@github\.com[:/](.+?)/(.+?)(?:\.git)?$", url)
|
|
340
|
+
if ssh_match:
|
|
341
|
+
return f"{ssh_match.group(1)}/{ssh_match.group(2)}"
|
|
342
|
+
# Handle HTTPS: https://github.com/owner/repo.git
|
|
343
|
+
https_match = re.search(r"github\.com/(.+?)/(.+?)(?:\.git)?$", url)
|
|
344
|
+
if https_match:
|
|
345
|
+
return f"{https_match.group(1)}/{https_match.group(2)}"
|
|
346
|
+
except (subprocess.TimeoutExpired, FileNotFoundError, Exception):
|
|
347
|
+
pass
|
|
348
|
+
return None
|
|
349
|
+
|
|
350
|
+
@classmethod
|
|
351
|
+
def from_env(cls) -> "MCPConfig":
|
|
352
|
+
"""Load configuration from environment variables.
|
|
353
|
+
|
|
354
|
+
Token priority:
|
|
355
|
+
1. GITHUB_TOKEN env var
|
|
356
|
+
2. GITHUB_PERSONAL_ACCESS_TOKEN env var
|
|
357
|
+
3. gh CLI auth token (supports browser auth for private repos)
|
|
358
|
+
"""
|
|
359
|
+
toolsets_str = os.getenv("MCP_TOOLSETS", "repos,pull_requests,issues,code_security")
|
|
360
|
+
|
|
361
|
+
# Try env vars first, then fall back to gh CLI
|
|
362
|
+
github_token = (
|
|
363
|
+
os.getenv("GITHUB_TOKEN") or
|
|
364
|
+
os.getenv("GITHUB_PERSONAL_ACCESS_TOKEN") or
|
|
365
|
+
MCPConfig._get_gh_cli_token()
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
# Auto-detect repo from git remote if not set in env
|
|
369
|
+
github_repo = os.getenv("GITHUB_REPO") or MCPConfig._get_repo_from_git()
|
|
370
|
+
|
|
371
|
+
return cls(
|
|
372
|
+
github_token=github_token,
|
|
373
|
+
github_repo=github_repo,
|
|
374
|
+
server_mode=os.getenv("MCP_SERVER_MODE", "local"),
|
|
375
|
+
toolsets=toolsets_str.split(",") if toolsets_str else [],
|
|
376
|
+
binary_path=os.getenv("MCP_BINARY_PATH", "github-mcp-server"),
|
|
377
|
+
read_only=os.getenv("MCP_READ_ONLY", "true").lower() == "true",
|
|
378
|
+
timeout=int(os.getenv("MCP_TIMEOUT", "30")),
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
@property
|
|
382
|
+
def is_available(self) -> bool:
|
|
383
|
+
"""Check if MCP is properly configured."""
|
|
384
|
+
return self.github_token is not None and len(self.github_token) > 0
|
|
385
|
+
|
|
386
|
+
@property
|
|
387
|
+
def repo_owner(self) -> Optional[str]:
|
|
388
|
+
"""Get the owner from github_repo (e.g., 'wix-private' from 'wix-private/picasso')."""
|
|
389
|
+
if self.github_repo and "/" in self.github_repo:
|
|
390
|
+
return self.github_repo.split("/")[0]
|
|
391
|
+
return None
|
|
392
|
+
|
|
393
|
+
@property
|
|
394
|
+
def repo_name(self) -> Optional[str]:
|
|
395
|
+
"""Get the repo name from github_repo (e.g., 'picasso' from 'wix-private/picasso')."""
|
|
396
|
+
if self.github_repo and "/" in self.github_repo:
|
|
397
|
+
return self.github_repo.split("/")[1]
|
|
398
|
+
return None
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
class EmDashConfig(BaseModel):
|
|
402
|
+
"""Main configuration for EmDash."""
|
|
403
|
+
|
|
404
|
+
kuzu: KuzuConfig = Field(default_factory=KuzuConfig)
|
|
405
|
+
ingestion: IngestionConfig = Field(default_factory=IngestionConfig)
|
|
406
|
+
analytics: AnalyticsConfig = Field(default_factory=AnalyticsConfig)
|
|
407
|
+
github: GitHubConfig = Field(default_factory=GitHubConfig)
|
|
408
|
+
openai: OpenAIConfig = Field(default_factory=OpenAIConfig)
|
|
409
|
+
anthropic: AnthropicConfig = Field(default_factory=AnthropicConfig)
|
|
410
|
+
fireworks: FireworksConfig = Field(default_factory=FireworksConfig)
|
|
411
|
+
mcp: MCPConfig = Field(default_factory=MCPConfig)
|
|
412
|
+
context: ContextConfig = Field(default_factory=ContextConfig)
|
|
413
|
+
agent: AgentConfig = Field(default_factory=AgentConfig)
|
|
414
|
+
log_level: str = Field(default="WARNING")
|
|
415
|
+
|
|
416
|
+
@classmethod
|
|
417
|
+
def from_env(cls) -> "EmDashConfig":
|
|
418
|
+
"""Load full configuration from environment variables."""
|
|
419
|
+
return cls(
|
|
420
|
+
kuzu=KuzuConfig.from_env(),
|
|
421
|
+
ingestion=IngestionConfig.from_env(),
|
|
422
|
+
analytics=AnalyticsConfig.from_env(),
|
|
423
|
+
github=GitHubConfig.from_env(),
|
|
424
|
+
openai=OpenAIConfig.from_env(),
|
|
425
|
+
anthropic=AnthropicConfig.from_env(),
|
|
426
|
+
fireworks=FireworksConfig.from_env(),
|
|
427
|
+
mcp=MCPConfig.from_env(),
|
|
428
|
+
context=ContextConfig.from_env(),
|
|
429
|
+
agent=AgentConfig.from_env(),
|
|
430
|
+
log_level=os.getenv("LOG_LEVEL", "WARNING"),
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
def validate_kuzu(self) -> None:
|
|
434
|
+
"""Validate Kuzu configuration."""
|
|
435
|
+
if not self.kuzu.database_path:
|
|
436
|
+
raise ConfigurationError("Kuzu database path is required")
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
# Global configuration instance
|
|
440
|
+
_config: Optional[EmDashConfig] = None
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
def get_config() -> EmDashConfig:
|
|
444
|
+
"""Get the global configuration instance."""
|
|
445
|
+
global _config
|
|
446
|
+
if _config is None:
|
|
447
|
+
_config = EmDashConfig.from_env()
|
|
448
|
+
return _config
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
def set_config(config: EmDashConfig) -> None:
|
|
452
|
+
"""Set the global configuration instance."""
|
|
453
|
+
global _config
|
|
454
|
+
_config = config
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""Custom exceptions for EmDash."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class EmDashException(Exception):
|
|
5
|
+
"""Base exception for all EmDash errors."""
|
|
6
|
+
pass
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ConfigurationError(EmDashException):
|
|
10
|
+
"""Raised when configuration is invalid or missing."""
|
|
11
|
+
pass
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class DatabaseConnectionError(EmDashException):
|
|
15
|
+
"""Raised when database connection fails."""
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class RepositoryError(EmDashException):
|
|
20
|
+
"""Raised when repository operations fail."""
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ParsingError(EmDashException):
|
|
25
|
+
"""Raised when code parsing fails."""
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class GraphBuildError(EmDashException):
|
|
30
|
+
"""Raised when graph construction fails."""
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class QueryError(EmDashException):
|
|
35
|
+
"""Raised when graph queries fail."""
|
|
36
|
+
pass
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class AnalyticsError(EmDashException):
|
|
40
|
+
"""Raised when analytics computation fails."""
|
|
41
|
+
pass
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class ContextLengthError(EmDashException):
|
|
45
|
+
"""Raised when context length exceeds model's limit."""
|
|
46
|
+
|
|
47
|
+
def __init__(
|
|
48
|
+
self,
|
|
49
|
+
message: str,
|
|
50
|
+
tokens_requested: int = 0,
|
|
51
|
+
tokens_limit: int = 0,
|
|
52
|
+
):
|
|
53
|
+
super().__init__(message)
|
|
54
|
+
self.tokens_requested = tokens_requested
|
|
55
|
+
self.tokens_limit = tokens_limit
|