emdash-core 0.1.7__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (187) hide show
  1. emdash_core/__init__.py +3 -0
  2. emdash_core/agent/__init__.py +37 -0
  3. emdash_core/agent/agents.py +225 -0
  4. emdash_core/agent/code_reviewer.py +476 -0
  5. emdash_core/agent/compaction.py +143 -0
  6. emdash_core/agent/context_manager.py +140 -0
  7. emdash_core/agent/events.py +338 -0
  8. emdash_core/agent/handlers.py +224 -0
  9. emdash_core/agent/inprocess_subagent.py +377 -0
  10. emdash_core/agent/mcp/__init__.py +50 -0
  11. emdash_core/agent/mcp/client.py +346 -0
  12. emdash_core/agent/mcp/config.py +302 -0
  13. emdash_core/agent/mcp/manager.py +496 -0
  14. emdash_core/agent/mcp/tool_factory.py +213 -0
  15. emdash_core/agent/prompts/__init__.py +38 -0
  16. emdash_core/agent/prompts/main_agent.py +104 -0
  17. emdash_core/agent/prompts/subagents.py +131 -0
  18. emdash_core/agent/prompts/workflow.py +136 -0
  19. emdash_core/agent/providers/__init__.py +34 -0
  20. emdash_core/agent/providers/base.py +143 -0
  21. emdash_core/agent/providers/factory.py +80 -0
  22. emdash_core/agent/providers/models.py +220 -0
  23. emdash_core/agent/providers/openai_provider.py +463 -0
  24. emdash_core/agent/providers/transformers_provider.py +217 -0
  25. emdash_core/agent/research/__init__.py +81 -0
  26. emdash_core/agent/research/agent.py +143 -0
  27. emdash_core/agent/research/controller.py +254 -0
  28. emdash_core/agent/research/critic.py +428 -0
  29. emdash_core/agent/research/macros.py +469 -0
  30. emdash_core/agent/research/planner.py +449 -0
  31. emdash_core/agent/research/researcher.py +436 -0
  32. emdash_core/agent/research/state.py +523 -0
  33. emdash_core/agent/research/synthesizer.py +594 -0
  34. emdash_core/agent/reviewer_profile.py +475 -0
  35. emdash_core/agent/rules.py +123 -0
  36. emdash_core/agent/runner.py +601 -0
  37. emdash_core/agent/session.py +262 -0
  38. emdash_core/agent/spec_schema.py +66 -0
  39. emdash_core/agent/specification.py +479 -0
  40. emdash_core/agent/subagent.py +397 -0
  41. emdash_core/agent/subagent_prompts.py +13 -0
  42. emdash_core/agent/toolkit.py +482 -0
  43. emdash_core/agent/toolkits/__init__.py +64 -0
  44. emdash_core/agent/toolkits/base.py +96 -0
  45. emdash_core/agent/toolkits/explore.py +47 -0
  46. emdash_core/agent/toolkits/plan.py +55 -0
  47. emdash_core/agent/tools/__init__.py +141 -0
  48. emdash_core/agent/tools/analytics.py +436 -0
  49. emdash_core/agent/tools/base.py +131 -0
  50. emdash_core/agent/tools/coding.py +484 -0
  51. emdash_core/agent/tools/github_mcp.py +592 -0
  52. emdash_core/agent/tools/history.py +13 -0
  53. emdash_core/agent/tools/modes.py +153 -0
  54. emdash_core/agent/tools/plan.py +206 -0
  55. emdash_core/agent/tools/plan_write.py +135 -0
  56. emdash_core/agent/tools/search.py +412 -0
  57. emdash_core/agent/tools/spec.py +341 -0
  58. emdash_core/agent/tools/task.py +262 -0
  59. emdash_core/agent/tools/task_output.py +204 -0
  60. emdash_core/agent/tools/tasks.py +454 -0
  61. emdash_core/agent/tools/traversal.py +588 -0
  62. emdash_core/agent/tools/web.py +179 -0
  63. emdash_core/analytics/__init__.py +5 -0
  64. emdash_core/analytics/engine.py +1286 -0
  65. emdash_core/api/__init__.py +5 -0
  66. emdash_core/api/agent.py +308 -0
  67. emdash_core/api/agents.py +154 -0
  68. emdash_core/api/analyze.py +264 -0
  69. emdash_core/api/auth.py +173 -0
  70. emdash_core/api/context.py +77 -0
  71. emdash_core/api/db.py +121 -0
  72. emdash_core/api/embed.py +131 -0
  73. emdash_core/api/feature.py +143 -0
  74. emdash_core/api/health.py +93 -0
  75. emdash_core/api/index.py +162 -0
  76. emdash_core/api/plan.py +110 -0
  77. emdash_core/api/projectmd.py +210 -0
  78. emdash_core/api/query.py +320 -0
  79. emdash_core/api/research.py +122 -0
  80. emdash_core/api/review.py +161 -0
  81. emdash_core/api/router.py +76 -0
  82. emdash_core/api/rules.py +116 -0
  83. emdash_core/api/search.py +119 -0
  84. emdash_core/api/spec.py +99 -0
  85. emdash_core/api/swarm.py +223 -0
  86. emdash_core/api/tasks.py +109 -0
  87. emdash_core/api/team.py +120 -0
  88. emdash_core/auth/__init__.py +17 -0
  89. emdash_core/auth/github.py +389 -0
  90. emdash_core/config.py +74 -0
  91. emdash_core/context/__init__.py +52 -0
  92. emdash_core/context/models.py +50 -0
  93. emdash_core/context/providers/__init__.py +11 -0
  94. emdash_core/context/providers/base.py +74 -0
  95. emdash_core/context/providers/explored_areas.py +183 -0
  96. emdash_core/context/providers/touched_areas.py +360 -0
  97. emdash_core/context/registry.py +73 -0
  98. emdash_core/context/reranker.py +199 -0
  99. emdash_core/context/service.py +260 -0
  100. emdash_core/context/session.py +352 -0
  101. emdash_core/core/__init__.py +104 -0
  102. emdash_core/core/config.py +454 -0
  103. emdash_core/core/exceptions.py +55 -0
  104. emdash_core/core/models.py +265 -0
  105. emdash_core/core/review_config.py +57 -0
  106. emdash_core/db/__init__.py +67 -0
  107. emdash_core/db/auth.py +134 -0
  108. emdash_core/db/models.py +91 -0
  109. emdash_core/db/provider.py +222 -0
  110. emdash_core/db/providers/__init__.py +5 -0
  111. emdash_core/db/providers/supabase.py +452 -0
  112. emdash_core/embeddings/__init__.py +24 -0
  113. emdash_core/embeddings/indexer.py +534 -0
  114. emdash_core/embeddings/models.py +192 -0
  115. emdash_core/embeddings/providers/__init__.py +7 -0
  116. emdash_core/embeddings/providers/base.py +112 -0
  117. emdash_core/embeddings/providers/fireworks.py +141 -0
  118. emdash_core/embeddings/providers/openai.py +104 -0
  119. emdash_core/embeddings/registry.py +146 -0
  120. emdash_core/embeddings/service.py +215 -0
  121. emdash_core/graph/__init__.py +26 -0
  122. emdash_core/graph/builder.py +134 -0
  123. emdash_core/graph/connection.py +692 -0
  124. emdash_core/graph/schema.py +416 -0
  125. emdash_core/graph/writer.py +667 -0
  126. emdash_core/ingestion/__init__.py +7 -0
  127. emdash_core/ingestion/change_detector.py +150 -0
  128. emdash_core/ingestion/git/__init__.py +5 -0
  129. emdash_core/ingestion/git/commit_analyzer.py +196 -0
  130. emdash_core/ingestion/github/__init__.py +6 -0
  131. emdash_core/ingestion/github/pr_fetcher.py +296 -0
  132. emdash_core/ingestion/github/task_extractor.py +100 -0
  133. emdash_core/ingestion/orchestrator.py +540 -0
  134. emdash_core/ingestion/parsers/__init__.py +10 -0
  135. emdash_core/ingestion/parsers/base_parser.py +66 -0
  136. emdash_core/ingestion/parsers/call_graph_builder.py +121 -0
  137. emdash_core/ingestion/parsers/class_extractor.py +154 -0
  138. emdash_core/ingestion/parsers/function_extractor.py +202 -0
  139. emdash_core/ingestion/parsers/import_analyzer.py +119 -0
  140. emdash_core/ingestion/parsers/python_parser.py +123 -0
  141. emdash_core/ingestion/parsers/registry.py +72 -0
  142. emdash_core/ingestion/parsers/ts_ast_parser.js +313 -0
  143. emdash_core/ingestion/parsers/typescript_parser.py +278 -0
  144. emdash_core/ingestion/repository.py +346 -0
  145. emdash_core/models/__init__.py +38 -0
  146. emdash_core/models/agent.py +68 -0
  147. emdash_core/models/index.py +77 -0
  148. emdash_core/models/query.py +113 -0
  149. emdash_core/planning/__init__.py +7 -0
  150. emdash_core/planning/agent_api.py +413 -0
  151. emdash_core/planning/context_builder.py +265 -0
  152. emdash_core/planning/feature_context.py +232 -0
  153. emdash_core/planning/feature_expander.py +646 -0
  154. emdash_core/planning/llm_explainer.py +198 -0
  155. emdash_core/planning/similarity.py +509 -0
  156. emdash_core/planning/team_focus.py +821 -0
  157. emdash_core/server.py +153 -0
  158. emdash_core/sse/__init__.py +5 -0
  159. emdash_core/sse/stream.py +196 -0
  160. emdash_core/swarm/__init__.py +17 -0
  161. emdash_core/swarm/merge_agent.py +383 -0
  162. emdash_core/swarm/session_manager.py +274 -0
  163. emdash_core/swarm/swarm_runner.py +226 -0
  164. emdash_core/swarm/task_definition.py +137 -0
  165. emdash_core/swarm/worker_spawner.py +319 -0
  166. emdash_core/swarm/worktree_manager.py +278 -0
  167. emdash_core/templates/__init__.py +10 -0
  168. emdash_core/templates/defaults/agent-builder.md.template +82 -0
  169. emdash_core/templates/defaults/focus.md.template +115 -0
  170. emdash_core/templates/defaults/pr-review-enhanced.md.template +309 -0
  171. emdash_core/templates/defaults/pr-review.md.template +80 -0
  172. emdash_core/templates/defaults/project.md.template +85 -0
  173. emdash_core/templates/defaults/research_critic.md.template +112 -0
  174. emdash_core/templates/defaults/research_planner.md.template +85 -0
  175. emdash_core/templates/defaults/research_synthesizer.md.template +128 -0
  176. emdash_core/templates/defaults/reviewer.md.template +81 -0
  177. emdash_core/templates/defaults/spec.md.template +41 -0
  178. emdash_core/templates/defaults/tasks.md.template +78 -0
  179. emdash_core/templates/loader.py +296 -0
  180. emdash_core/utils/__init__.py +45 -0
  181. emdash_core/utils/git.py +84 -0
  182. emdash_core/utils/image.py +502 -0
  183. emdash_core/utils/logger.py +51 -0
  184. emdash_core-0.1.7.dist-info/METADATA +35 -0
  185. emdash_core-0.1.7.dist-info/RECORD +187 -0
  186. emdash_core-0.1.7.dist-info/WHEEL +4 -0
  187. emdash_core-0.1.7.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,412 @@
1
+ """Search tools for finding code in the graph."""
2
+
3
+ import subprocess
4
+ from typing import Optional
5
+
6
+ from .base import BaseTool, ToolResult, ToolCategory
7
+ from ...utils.logger import log
8
+
9
+
10
+ class SemanticSearchTool(BaseTool):
11
+ """Search for code entities using natural language."""
12
+
13
+ name = "semantic_search"
14
+ description = """Search for code entities (functions, classes, files) using natural language.
15
+ Returns entities matching the semantic meaning of your query, ranked by relevance.
16
+ Useful for finding code related to concepts like "authentication", "database queries", etc."""
17
+ category = ToolCategory.SEARCH
18
+
19
+ def execute(
20
+ self,
21
+ query: str,
22
+ entity_types: Optional[list[str]] = None,
23
+ limit: int = 10,
24
+ min_score: float = 0.5,
25
+ ) -> ToolResult:
26
+ """Execute semantic search.
27
+
28
+ Args:
29
+ query: Natural language search query
30
+ entity_types: Optional list of types to filter (Function, Class, File)
31
+ limit: Maximum results to return
32
+ min_score: Minimum similarity score (0-1)
33
+
34
+ Returns:
35
+ ToolResult with matching entities
36
+ """
37
+ try:
38
+ from ...embeddings.indexer import EmbeddingIndexer
39
+
40
+ indexer = EmbeddingIndexer(connection=self.connection)
41
+ results = indexer.search(
42
+ query=query,
43
+ entity_types=entity_types,
44
+ limit=limit,
45
+ min_score=min_score,
46
+ )
47
+
48
+ return ToolResult.success_result(
49
+ data={
50
+ "query": query,
51
+ "results": results,
52
+ "count": len(results),
53
+ },
54
+ suggestions=self._generate_suggestions(results),
55
+ )
56
+
57
+ except Exception as e:
58
+ log.exception("Semantic search failed")
59
+ return ToolResult.error_result(
60
+ f"Search failed: {str(e)}",
61
+ suggestions=["Try a different query", "Check if embeddings are indexed"],
62
+ )
63
+
64
+ def _generate_suggestions(self, results: list) -> list[str]:
65
+ """Generate next step suggestions based on results."""
66
+ if not results:
67
+ return ["Try broader search terms", "Use text_search for exact matches"]
68
+
69
+ suggestions = []
70
+ top = results[0]
71
+
72
+ if top.get("node_type") == "Function":
73
+ suggestions.append(f"Use expand_node to see full context of {top.get('qualified_name')}")
74
+ suggestions.append(f"Use get_callers to see who calls {top.get('qualified_name')}")
75
+ elif top.get("node_type") == "Class":
76
+ suggestions.append(f"Use expand_node to see methods and relationships")
77
+ elif top.get("node_type") == "File":
78
+ suggestions.append(f"Use expand_node to see file contents and dependencies")
79
+
80
+ return suggestions
81
+
82
+ def get_schema(self) -> dict:
83
+ """Get OpenAI function schema."""
84
+ return self._make_schema(
85
+ properties={
86
+ "query": {
87
+ "type": "string",
88
+ "description": "Natural language search query",
89
+ },
90
+ "entity_types": {
91
+ "type": "array",
92
+ "items": {"type": "string", "enum": ["Function", "Class", "File"]},
93
+ "description": "Types of entities to search for",
94
+ },
95
+ "limit": {
96
+ "type": "integer",
97
+ "description": "Maximum results to return",
98
+ "default": 10,
99
+ },
100
+ "min_score": {
101
+ "type": "number",
102
+ "description": "Minimum similarity score (0-1)",
103
+ "default": 0.5,
104
+ },
105
+ },
106
+ required=["query"],
107
+ )
108
+
109
+
110
+ class TextSearchTool(BaseTool):
111
+ """Search for code by exact text match."""
112
+
113
+ name = "text_search"
114
+ description = """Search for code entities by exact text match in names.
115
+ Useful for finding specific functions, classes, or files by name.
116
+ More precise than semantic search when you know part of the name."""
117
+ category = ToolCategory.SEARCH
118
+
119
+ def execute(
120
+ self,
121
+ query: str,
122
+ entity_types: Optional[list[str]] = None,
123
+ limit: int = 10,
124
+ ) -> ToolResult:
125
+ """Execute text search.
126
+
127
+ Args:
128
+ query: Text to search for in entity names
129
+ entity_types: Optional types to filter
130
+ limit: Maximum results
131
+
132
+ Returns:
133
+ ToolResult with matching entities
134
+ """
135
+ try:
136
+ results = []
137
+
138
+ # Search in graph
139
+ cypher = """
140
+ MATCH (n)
141
+ WHERE (n:Function OR n:Class OR n:File)
142
+ AND (
143
+ n.qualified_name CONTAINS $query
144
+ OR n.name CONTAINS $query
145
+ OR n.file_path CONTAINS $query
146
+ )
147
+ RETURN n.qualified_name as qualified_name,
148
+ n.name as name,
149
+ n.file_path as file_path,
150
+ labels(n)[0] as node_type
151
+ LIMIT $limit
152
+ """
153
+
154
+ with self.connection.session() as session:
155
+ result = session.run(cypher, {"query": query, "limit": limit})
156
+ for record in result:
157
+ results.append({
158
+ "qualified_name": record["qualified_name"],
159
+ "name": record["name"],
160
+ "file_path": record["file_path"],
161
+ "node_type": record["node_type"],
162
+ })
163
+
164
+ # Filter by type if specified
165
+ if entity_types:
166
+ results = [r for r in results if r["node_type"] in entity_types]
167
+
168
+ return ToolResult.success_result(
169
+ data={
170
+ "query": query,
171
+ "results": results,
172
+ "count": len(results),
173
+ },
174
+ )
175
+
176
+ except Exception as e:
177
+ log.exception("Text search failed")
178
+ return ToolResult.error_result(f"Search failed: {str(e)}")
179
+
180
+ def get_schema(self) -> dict:
181
+ """Get OpenAI function schema."""
182
+ return self._make_schema(
183
+ properties={
184
+ "query": {
185
+ "type": "string",
186
+ "description": "Text to search for in entity names",
187
+ },
188
+ "entity_types": {
189
+ "type": "array",
190
+ "items": {"type": "string", "enum": ["Function", "Class", "File"]},
191
+ "description": "Types of entities to search for",
192
+ },
193
+ "limit": {
194
+ "type": "integer",
195
+ "description": "Maximum results to return",
196
+ "default": 10,
197
+ },
198
+ },
199
+ required=["query"],
200
+ )
201
+
202
+
203
+ class GrepTool(BaseTool):
204
+ """Search file contents using ripgrep."""
205
+
206
+ name = "grep"
207
+ description = """Search file contents using ripgrep.
208
+ Fast full-text search across all files in the repository.
209
+ Useful for finding code patterns, string literals, or specific implementations."""
210
+ category = ToolCategory.SEARCH
211
+
212
+ def execute(
213
+ self,
214
+ pattern: str,
215
+ file_pattern: Optional[str] = None,
216
+ max_results: int = 50,
217
+ context_lines: int = 2,
218
+ ) -> ToolResult:
219
+ """Execute grep search.
220
+
221
+ Args:
222
+ pattern: Regex pattern to search for
223
+ file_pattern: Optional glob pattern for files (e.g., "*.py")
224
+ max_results: Maximum number of matches
225
+ context_lines: Lines of context around matches
226
+
227
+ Returns:
228
+ ToolResult with grep matches
229
+ """
230
+ try:
231
+ cmd = ["rg", "--json", "-C", str(context_lines), "-m", str(max_results)]
232
+
233
+ if file_pattern:
234
+ cmd.extend(["-g", file_pattern])
235
+
236
+ cmd.append(pattern)
237
+
238
+ result = subprocess.run(
239
+ cmd,
240
+ capture_output=True,
241
+ text=True,
242
+ timeout=30,
243
+ )
244
+
245
+ matches = []
246
+ for line in result.stdout.strip().split("\n"):
247
+ if not line:
248
+ continue
249
+ try:
250
+ import json
251
+ data = json.loads(line)
252
+ if data.get("type") == "match":
253
+ match_data = data.get("data", {})
254
+ matches.append({
255
+ "file": match_data.get("path", {}).get("text", ""),
256
+ "line_number": match_data.get("line_number"),
257
+ "line_text": match_data.get("lines", {}).get("text", "").strip(),
258
+ })
259
+ except json.JSONDecodeError:
260
+ continue
261
+
262
+ return ToolResult.success_result(
263
+ data={
264
+ "pattern": pattern,
265
+ "matches": matches,
266
+ "count": len(matches),
267
+ },
268
+ )
269
+
270
+ except subprocess.TimeoutExpired:
271
+ return ToolResult.error_result("Grep search timed out")
272
+ except FileNotFoundError:
273
+ return ToolResult.error_result(
274
+ "ripgrep (rg) not found",
275
+ suggestions=["Install ripgrep: brew install ripgrep"],
276
+ )
277
+ except Exception as e:
278
+ log.exception("Grep search failed")
279
+ return ToolResult.error_result(f"Grep failed: {str(e)}")
280
+
281
+ def get_schema(self) -> dict:
282
+ """Get OpenAI function schema."""
283
+ return self._make_schema(
284
+ properties={
285
+ "pattern": {
286
+ "type": "string",
287
+ "description": "Regex pattern to search for",
288
+ },
289
+ "file_pattern": {
290
+ "type": "string",
291
+ "description": "Optional glob pattern for files (e.g., '*.py')",
292
+ },
293
+ "max_results": {
294
+ "type": "integer",
295
+ "description": "Maximum number of matches",
296
+ "default": 50,
297
+ },
298
+ "context_lines": {
299
+ "type": "integer",
300
+ "description": "Lines of context around matches",
301
+ "default": 2,
302
+ },
303
+ },
304
+ required=["pattern"],
305
+ )
306
+
307
+
308
+ class GlobTool(BaseTool):
309
+ """Find files by name/path patterns using glob."""
310
+
311
+ name = "glob"
312
+ description = """Find files by name or path pattern using glob syntax.
313
+ Use this to discover files matching patterns like "**/*.py" or "src/**/*.ts".
314
+ Unlike grep which searches file CONTENTS, glob searches file NAMES/PATHS.
315
+
316
+ Common patterns:
317
+ - "**/*.py" - All Python files
318
+ - "src/**/*.ts" - TypeScript files in src/
319
+ - "**/test_*.py" - Test files
320
+ - "**/*config*" - Files with 'config' in name"""
321
+ category = ToolCategory.SEARCH
322
+
323
+ def execute(
324
+ self,
325
+ pattern: str,
326
+ max_results: int = 100,
327
+ ) -> ToolResult:
328
+ """Execute glob search for files.
329
+
330
+ Args:
331
+ pattern: Glob pattern (e.g., "**/*.py", "src/**/*.ts")
332
+ max_results: Maximum number of files to return
333
+
334
+ Returns:
335
+ ToolResult with matching file paths
336
+ """
337
+ from pathlib import Path
338
+
339
+ try:
340
+ # Get current working directory
341
+ cwd = Path.cwd()
342
+
343
+ # Execute glob
344
+ matches = list(cwd.glob(pattern))
345
+
346
+ # Sort by modification time (most recent first) and limit
347
+ matches.sort(key=lambda p: p.stat().st_mtime if p.exists() else 0, reverse=True)
348
+ matches = matches[:max_results]
349
+
350
+ # Convert to relative paths
351
+ files = []
352
+ for match in matches:
353
+ if match.is_file():
354
+ try:
355
+ rel_path = match.relative_to(cwd)
356
+ files.append({
357
+ "path": str(rel_path),
358
+ "name": match.name,
359
+ "extension": match.suffix,
360
+ })
361
+ except ValueError:
362
+ files.append({
363
+ "path": str(match),
364
+ "name": match.name,
365
+ "extension": match.suffix,
366
+ })
367
+
368
+ return ToolResult.success_result(
369
+ data={
370
+ "pattern": pattern,
371
+ "files": files,
372
+ "count": len(files),
373
+ },
374
+ suggestions=self._generate_suggestions(files, pattern),
375
+ )
376
+
377
+ except Exception as e:
378
+ log.exception("Glob search failed")
379
+ return ToolResult.error_result(f"Glob failed: {str(e)}")
380
+
381
+ def _generate_suggestions(self, files: list, pattern: str) -> list[str]:
382
+ """Generate suggestions based on results."""
383
+ if not files:
384
+ return [
385
+ "No files found. Try a broader pattern.",
386
+ "Use '**/*' to match all files recursively.",
387
+ ]
388
+
389
+ suggestions = []
390
+ if len(files) > 0:
391
+ first_file = files[0]["path"]
392
+ suggestions.append(f"Use read_file to examine '{first_file}'")
393
+ suggestions.append(f"Use grep to search within these {len(files)} files")
394
+
395
+ return suggestions
396
+
397
+ def get_schema(self) -> dict:
398
+ """Get OpenAI function schema."""
399
+ return self._make_schema(
400
+ properties={
401
+ "pattern": {
402
+ "type": "string",
403
+ "description": "Glob pattern (e.g., '**/*.py', 'src/**/*.ts', '**/test_*.py')",
404
+ },
405
+ "max_results": {
406
+ "type": "integer",
407
+ "description": "Maximum number of files to return",
408
+ "default": 100,
409
+ },
410
+ },
411
+ required=["pattern"],
412
+ )