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,38 @@
1
+ """Agent prompts module.
2
+
3
+ Centralized location for all agent system prompts and workflow patterns.
4
+ """
5
+
6
+ from .workflow import (
7
+ WORKFLOW_PATTERNS,
8
+ EXPLORATION_STRATEGY,
9
+ OUTPUT_GUIDELINES,
10
+ EFFICIENCY_RULES,
11
+ EXPLORATION_OUTPUT_FORMAT,
12
+ PLAN_TEMPLATE,
13
+ SIZING_GUIDELINES,
14
+ )
15
+ from .main_agent import (
16
+ BASE_SYSTEM_PROMPT,
17
+ build_system_prompt,
18
+ build_tools_section,
19
+ )
20
+ from .subagents import SUBAGENT_PROMPTS, get_subagent_prompt
21
+
22
+ __all__ = [
23
+ # Workflow patterns
24
+ "WORKFLOW_PATTERNS",
25
+ "EXPLORATION_STRATEGY",
26
+ "OUTPUT_GUIDELINES",
27
+ "EFFICIENCY_RULES",
28
+ "EXPLORATION_OUTPUT_FORMAT",
29
+ "PLAN_TEMPLATE",
30
+ "SIZING_GUIDELINES",
31
+ # Main agent
32
+ "BASE_SYSTEM_PROMPT",
33
+ "build_system_prompt",
34
+ "build_tools_section",
35
+ # Sub-agents
36
+ "SUBAGENT_PROMPTS",
37
+ "get_subagent_prompt",
38
+ ]
@@ -0,0 +1,104 @@
1
+ """Main agent system prompt.
2
+
3
+ The primary prompt for the orchestrating agent that manages sub-agents
4
+ and handles complex multi-step tasks.
5
+ """
6
+
7
+ from .workflow import (
8
+ WORKFLOW_PATTERNS,
9
+ EXPLORATION_STRATEGY,
10
+ OUTPUT_GUIDELINES,
11
+ )
12
+
13
+ # Base system prompt template with placeholder for tools
14
+ BASE_SYSTEM_PROMPT = """You are a code exploration and implementation assistant. You orchestrate focused sub-agents for exploration while maintaining the high-level view.
15
+
16
+ {tools_section}
17
+ """ + WORKFLOW_PATTERNS + EXPLORATION_STRATEGY + OUTPUT_GUIDELINES
18
+
19
+
20
+ def build_system_prompt(toolkit) -> str:
21
+ """Build the complete system prompt with dynamic tool descriptions.
22
+
23
+ Args:
24
+ toolkit: The agent toolkit with registered tools
25
+
26
+ Returns:
27
+ Complete system prompt string
28
+ """
29
+ tools_section = build_tools_section(toolkit)
30
+ return BASE_SYSTEM_PROMPT.format(tools_section=tools_section)
31
+
32
+
33
+ def build_tools_section(toolkit) -> str:
34
+ """Build the tools section of the system prompt from registered tools.
35
+
36
+ Args:
37
+ toolkit: The agent toolkit with registered tools
38
+
39
+ Returns:
40
+ Formatted string with tool descriptions grouped by category
41
+ """
42
+ from ..tools.base import ToolCategory
43
+
44
+ # Group tools by category
45
+ tools_by_category: dict[str, list[tuple[str, str]]] = {}
46
+
47
+ for tool in toolkit._tools.values():
48
+ # Get category name
49
+ if hasattr(tool, 'category'):
50
+ category = tool.category.value if isinstance(tool.category, ToolCategory) else str(tool.category)
51
+ else:
52
+ category = "other"
53
+
54
+ # Get tool name and description
55
+ name = tool.name
56
+ description = tool.description
57
+
58
+ # Clean up description - take first sentence or first 150 chars
59
+ if description:
60
+ # Remove [server_name] prefix if present (from MCP tools)
61
+ if description.startswith("["):
62
+ description = description.split("]", 1)[-1].strip()
63
+ # Take first sentence
64
+ first_sentence = description.split(".")[0] + "."
65
+ if len(first_sentence) > 150:
66
+ first_sentence = description[:147] + "..."
67
+ description = first_sentence
68
+ else:
69
+ description = "No description available."
70
+
71
+ if category not in tools_by_category:
72
+ tools_by_category[category] = []
73
+ tools_by_category[category].append((name, description))
74
+
75
+ # Build formatted section
76
+ lines = ["## Available Tools\n"]
77
+
78
+ # Define category display order and titles
79
+ category_titles = {
80
+ "search": "Search & Discovery",
81
+ "traversal": "Graph Traversal",
82
+ "analytics": "Analytics",
83
+ "planning": "Planning",
84
+ "history": "History",
85
+ "other": "Other Tools",
86
+ }
87
+
88
+ # Sort categories by predefined order
89
+ category_order = ["search", "traversal", "analytics", "planning", "history", "other"]
90
+ sorted_categories = sorted(
91
+ tools_by_category.keys(),
92
+ key=lambda c: category_order.index(c) if c in category_order else 999
93
+ )
94
+
95
+ for category in sorted_categories:
96
+ tools = tools_by_category[category]
97
+ title = category_titles.get(category, category.title())
98
+
99
+ lines.append(f"### {title}")
100
+ for name, desc in sorted(tools):
101
+ lines.append(f"- **{name}**: {desc}")
102
+ lines.append("")
103
+
104
+ return "\n".join(lines)
@@ -0,0 +1,131 @@
1
+ """Sub-agent system prompts.
2
+
3
+ Prompts for specialized sub-agents that handle focused tasks like
4
+ exploration, planning, command execution, and research.
5
+ """
6
+
7
+ from .workflow import (
8
+ EFFICIENCY_RULES,
9
+ EXPLORATION_OUTPUT_FORMAT,
10
+ PLAN_TEMPLATE,
11
+ SIZING_GUIDELINES,
12
+ )
13
+
14
+ # Explore agent prompt
15
+ EXPLORE_PROMPT = f"""You are a fast, focused codebase explorer. Your job is to find specific information and return structured results.
16
+
17
+ ## Your Mission
18
+ Find and report: files, functions, classes, patterns, or code snippets relevant to the task. You have limited turns, so be efficient.
19
+
20
+ ## Strategy
21
+
22
+ ### Breadth-First for Discovery
23
+ When looking for something you're not sure exists:
24
+ 1. glob to find candidate files by name/extension
25
+ 2. grep with multiple keywords to find occurrences
26
+ 3. Read the most promising files
27
+
28
+ ### Depth-First for Understanding
29
+ When you have a specific target:
30
+ 1. Go directly to the file
31
+ 2. Read the relevant sections
32
+ 3. Follow imports/dependencies as needed
33
+
34
+ {EFFICIENCY_RULES}
35
+
36
+ {EXPLORATION_OUTPUT_FORMAT}
37
+
38
+ ## Constraints
39
+ - You are read-only - cannot modify files
40
+ - Focus on the specific task, don't go on tangents
41
+ - Be concise - the main agent needs your results, not your process"""
42
+
43
+ # Plan agent prompt
44
+ PLAN_PROMPT = f"""You are a software architect. Your job is to understand a codebase and design clear implementation plans.
45
+
46
+ ## Your Mission
47
+ Explore the codebase, understand patterns and conventions, then write a concrete implementation plan.
48
+
49
+ ## Approach
50
+
51
+ ### 1. Understand Context (use 30-40% of your turns)
52
+ - Find similar features/patterns in the codebase
53
+ - Understand the architecture and conventions
54
+ - Identify files that will need changes
55
+ - Note any constraints or dependencies
56
+
57
+ ### 2. Design the Solution
58
+ - Follow existing patterns when possible
59
+ - Break into clear, ordered steps
60
+ - Identify risks and edge cases
61
+ - Consider error handling
62
+
63
+ ### 3. Write the Plan
64
+ {PLAN_TEMPLATE}
65
+
66
+ ## Constraints
67
+ - You can only write to .emdash/plans/*.md
68
+ - Focus on actionable steps, not theory
69
+ - Reference specific files and line numbers
70
+ - Keep plans focused and concrete
71
+ {SIZING_GUIDELINES}"""
72
+
73
+ # Bash agent prompt
74
+ BASH_PROMPT = """You are a command executor. Run shell commands and report results clearly.
75
+
76
+ ## Guidelines
77
+ - Show the command you're running
78
+ - Report full output (or summarize if very long)
79
+ - Explain what happened
80
+ - Warn about destructive operations before running
81
+
82
+ ## Safety
83
+ - Never run commands that could cause data loss without warning
84
+ - Be cautious with sudo, rm -rf, force pushes
85
+ - Prefer dry-run flags when available for destructive operations
86
+
87
+ ## Output
88
+ Report: command run, output received, what it means."""
89
+
90
+ # Research agent prompt
91
+ RESEARCH_PROMPT = """You are a documentation researcher. Find authoritative information from the web and official docs.
92
+
93
+ ## Guidelines
94
+ - Prefer official documentation over blog posts
95
+ - Cite sources with URLs
96
+ - Include relevant code examples
97
+ - Note version-specific information
98
+ - Cross-reference multiple sources for accuracy
99
+
100
+ ## Output
101
+ - Answer the specific question asked
102
+ - Provide context for when/why to use the information
103
+ - Include links for further reading"""
104
+
105
+ # Registry of all sub-agent prompts
106
+ SUBAGENT_PROMPTS = {
107
+ "Explore": EXPLORE_PROMPT,
108
+ "Plan": PLAN_PROMPT,
109
+ "Bash": BASH_PROMPT,
110
+ "Research": RESEARCH_PROMPT,
111
+ }
112
+
113
+
114
+ def get_subagent_prompt(subagent_type: str) -> str:
115
+ """Get the system prompt for a sub-agent type.
116
+
117
+ Args:
118
+ subagent_type: Type of agent (e.g., "Explore", "Plan")
119
+
120
+ Returns:
121
+ System prompt string
122
+
123
+ Raises:
124
+ ValueError: If agent type is not known
125
+ """
126
+ if subagent_type not in SUBAGENT_PROMPTS:
127
+ available = list(SUBAGENT_PROMPTS.keys())
128
+ raise ValueError(
129
+ f"Unknown agent type: {subagent_type}. Available: {available}"
130
+ )
131
+ return SUBAGENT_PROMPTS[subagent_type]
@@ -0,0 +1,136 @@
1
+ """Shared workflow patterns for agents.
2
+
3
+ These patterns can be embedded in different agent prompts to ensure
4
+ consistent behavior across agent types.
5
+ """
6
+
7
+ # Core workflow for tackling complex tasks
8
+ WORKFLOW_PATTERNS = """
9
+ ## Workflow for Complex Tasks
10
+
11
+ ### 1. Understand Before Acting
12
+ - Read code before modifying it
13
+ - Ask clarifying questions when requirements are ambiguous
14
+ - Search for similar patterns already in the codebase
15
+
16
+ ### 2. Break Down Hard Problems
17
+ When facing a task you don't immediately know how to solve:
18
+
19
+ a) **Decompose**: Split into smaller, concrete sub-tasks
20
+ b) **Explore**: Use sub-agents to gather context (can run in parallel)
21
+ c) **Plan**: Write out your approach before implementing
22
+ d) **Execute**: Work through tasks one at a time
23
+ e) **Validate**: Check your work against requirements
24
+
25
+ ### 3. Use Sub-Agents Strategically
26
+ Spawn sub-agents via the `task` tool when you need:
27
+ - **Explore**: Find files, patterns, or understand code structure
28
+ - **Plan**: Design implementation approach for complex features
29
+
30
+ Guidelines:
31
+ - Launch multiple Explore agents in parallel for independent searches
32
+ - Use sub-agents for focused work that would clutter your context
33
+ - Prefer sub-agents over doing 5+ search operations yourself
34
+
35
+ ### 4. Track Progress
36
+ For multi-step tasks, mentally track what's done and what's next.
37
+ Update the user on progress for long-running work.
38
+ """
39
+
40
+ # Exploration strategy for code navigation
41
+ EXPLORATION_STRATEGY = """
42
+ ## Exploration Strategy
43
+
44
+ ### Start Broad, Then Focus
45
+ 1. Understand project structure (list_files on key directories)
46
+ 2. Find relevant files (glob for patterns, grep for keywords)
47
+ 3. Read key files to understand patterns
48
+ 4. Deep dive into specific areas
49
+
50
+ ### Tool Selection
51
+ - **glob** searches file NAMES/PATHS → glob("**/*.py")
52
+ - **grep** searches file CONTENTS → grep("authenticate")
53
+ - **semantic_search** finds conceptually related code
54
+ - Use local tools for the LOCAL codebase
55
+ - Use GitHub/MCP tools for REMOTE repositories, PRs, issues
56
+
57
+ ### When Stuck
58
+ 1. Step back - what are you actually trying to find?
59
+ 2. Try alternative search terms
60
+ 3. Look at imports/dependencies for clues
61
+ 4. Ask the user for clarification
62
+ """
63
+
64
+ # Output formatting guidelines
65
+ OUTPUT_GUIDELINES = """
66
+ ## Output Guidelines
67
+ - Cite specific files and line numbers
68
+ - Show relevant code snippets
69
+ - Be concise but thorough
70
+ - Explain your reasoning for complex decisions
71
+ - NEVER provide time estimates (hours, days, weeks). Use complexity sizing: S/M/L/XL
72
+ """
73
+
74
+ # Efficiency rules for sub-agents with limited turns
75
+ EFFICIENCY_RULES = """
76
+ ## Efficiency Rules
77
+ - If you find what you need, STOP - don't keep searching
78
+ - If 3 searches return nothing, try different terms or report "not found"
79
+ - Read only the parts of files you need (use offset/limit for large files)
80
+ - Don't read entire files when you only need a specific function
81
+ """
82
+
83
+ # Structured output format for exploration results
84
+ EXPLORATION_OUTPUT_FORMAT = """
85
+ ## Output Format
86
+ Structure your final response as:
87
+
88
+ **Summary**: 1-2 sentence answer to the task
89
+
90
+ **Key Findings**:
91
+ - `file:line` - Description of what you found
92
+ - `file:line` - Another finding
93
+
94
+ **Files Explored**: [list of files you read]
95
+
96
+ **Confidence**: high/medium/low
97
+ """
98
+
99
+ # Plan template for Plan agents
100
+ PLAN_TEMPLATE = """
101
+ ## Plan Template
102
+ Use `write_plan` to save your plan. Structure it as:
103
+
104
+ ```markdown
105
+ # [Feature Name] Implementation Plan
106
+
107
+ ## Summary
108
+ Brief description of what this plan accomplishes.
109
+
110
+ ## Files to Modify
111
+ - `path/to/file.py` - What changes
112
+
113
+ ## Files to Create (if any)
114
+ - `path/to/new.py` - Purpose
115
+
116
+ ## Implementation Steps
117
+ 1. **Step name**: Detailed description
118
+ - Specific changes to make
119
+ - Code patterns to follow (reference existing code)
120
+
121
+ 2. **Step name**: ...
122
+
123
+ ## Edge Cases & Error Handling
124
+ - Case: How to handle
125
+
126
+ ## Open Questions
127
+ Things that need clarification.
128
+ ```
129
+ """
130
+
131
+ # Sizing guidelines (no time estimates)
132
+ SIZING_GUIDELINES = """
133
+ ## Sizing (No Time Estimates)
134
+ - NEVER include time estimates (no hours, days, weeks, sprints, timelines)
135
+ - Use complexity sizing instead: S (small), M (medium), L (large), XL (extra large)
136
+ """
@@ -0,0 +1,34 @@
1
+ """LLM provider abstraction layer using OpenAI SDK."""
2
+
3
+ from .base import LLMProvider, LLMResponse, ToolCall
4
+ from .models import ChatModel, ChatModelSpec
5
+ from .openai_provider import OpenAIProvider
6
+ from .transformers_provider import TransformersProvider
7
+ from .factory import (
8
+ get_provider,
9
+ get_default_model,
10
+ get_default_api_key,
11
+ list_available_models,
12
+ DEFAULT_MODEL,
13
+ DEFAULT_API_KEY_ENV,
14
+ )
15
+
16
+ __all__ = [
17
+ # Base
18
+ "LLMProvider",
19
+ "LLMResponse",
20
+ "ToolCall",
21
+ # Models
22
+ "ChatModel",
23
+ "ChatModelSpec",
24
+ # Providers
25
+ "OpenAIProvider",
26
+ "TransformersProvider",
27
+ # Factory
28
+ "get_provider",
29
+ "get_default_model",
30
+ "get_default_api_key",
31
+ "list_available_models",
32
+ "DEFAULT_MODEL",
33
+ "DEFAULT_API_KEY_ENV",
34
+ ]
@@ -0,0 +1,143 @@
1
+ """Base classes for LLM providers."""
2
+
3
+ from abc import ABC, abstractmethod
4
+ from dataclasses import dataclass, field
5
+ from typing import Any, Optional, Union
6
+
7
+
8
+ @dataclass
9
+ class ToolCall:
10
+ """Represents a tool call from the LLM."""
11
+
12
+ id: str
13
+ name: str
14
+ arguments: str # JSON string of arguments
15
+
16
+
17
+ @dataclass
18
+ class ImageContent:
19
+ """Represents an image for vision-capable models."""
20
+
21
+ image_data: bytes # Raw image bytes
22
+ format: str = "png" # Image format (png, jpeg, gif)
23
+
24
+ @property
25
+ def base64_url(self) -> str:
26
+ """Get base64 data URL for the image."""
27
+ import base64
28
+ encoded = base64.b64encode(self.image_data).decode("utf-8")
29
+ return f"data:image/{self.format};base64,{encoded}"
30
+
31
+
32
+ @dataclass
33
+ class LLMResponse:
34
+ """Unified response from any LLM provider."""
35
+
36
+ content: Optional[str] = None
37
+ tool_calls: list[ToolCall] = field(default_factory=list)
38
+ raw: Any = None # Original provider response
39
+ stop_reason: Optional[str] = None
40
+ input_tokens: int = 0 # Tokens in the request
41
+ output_tokens: int = 0 # Tokens in the response
42
+
43
+
44
+ class LLMProvider(ABC):
45
+ """Abstract base class for LLM providers."""
46
+
47
+ def __init__(self, model: str):
48
+ self.model = model
49
+
50
+ @abstractmethod
51
+ def chat(
52
+ self,
53
+ messages: list[dict],
54
+ tools: Optional[list[dict]] = None,
55
+ system: Optional[str] = None,
56
+ reasoning: bool = False,
57
+ images: Optional[list[ImageContent]] = None,
58
+ ) -> LLMResponse:
59
+ """Send a chat completion request.
60
+
61
+ Args:
62
+ messages: List of message dicts with 'role' and 'content'
63
+ tools: Optional list of tool schemas
64
+ system: Optional system prompt (will be prepended or handled per provider)
65
+ reasoning: Enable reasoning mode (for models that support it)
66
+ images: Optional list of images for vision-capable models
67
+
68
+ Returns:
69
+ LLMResponse with content and/or tool calls
70
+ """
71
+ pass
72
+
73
+ @abstractmethod
74
+ def get_context_limit(self) -> int:
75
+ """Get the context window size for this model."""
76
+ pass
77
+
78
+ @abstractmethod
79
+ def get_max_image_size(self) -> int:
80
+ """Get maximum image size in bytes for this model."""
81
+ return 5 * 1024 * 1024 # Default 5MB
82
+
83
+ @abstractmethod
84
+ def supports_vision(self) -> bool:
85
+ """Check if this model supports image input."""
86
+ return False
87
+
88
+ @abstractmethod
89
+ def format_tool_result(self, tool_call_id: str, result: str) -> dict:
90
+ """Format a tool result message for this provider.
91
+
92
+ Args:
93
+ tool_call_id: ID of the tool call being responded to
94
+ result: JSON string result from the tool
95
+
96
+ Returns:
97
+ Message dict in provider's expected format
98
+ """
99
+ pass
100
+
101
+ @abstractmethod
102
+ def format_assistant_message(self, response: LLMResponse) -> dict:
103
+ """Format an assistant response to add back to messages.
104
+
105
+ Args:
106
+ response: The LLMResponse from a chat call
107
+
108
+ Returns:
109
+ Message dict in provider's expected format
110
+ """
111
+ pass
112
+
113
+ def format_content_with_images(
114
+ self,
115
+ text: str,
116
+ images: Optional[list[ImageContent]] = None
117
+ ) -> Union[str, list[dict]]:
118
+ """Format message content with optional images.
119
+
120
+ For vision models, returns a list of content blocks.
121
+ For non-vision models, returns text only (images stripped).
122
+
123
+ Args:
124
+ text: Text content
125
+ images: Optional list of images
126
+
127
+ Returns:
128
+ Content formatted for this provider
129
+ """
130
+ if not images:
131
+ return text
132
+
133
+ if self.supports_vision():
134
+ content = [{"type": "text", "text": text}]
135
+ for img in images:
136
+ content.append({
137
+ "type": "image_url",
138
+ "image_url": {"url": img.base64_url}
139
+ })
140
+ return content
141
+
142
+ # Non-vision model: strip images, warn
143
+ return text
@@ -0,0 +1,80 @@
1
+ """Factory for creating LLM providers."""
2
+
3
+ import os
4
+ from typing import Union
5
+
6
+ from .base import LLMProvider
7
+ from .models import ChatModel
8
+ from .openai_provider import OpenAIProvider
9
+ from .transformers_provider import TransformersProvider
10
+
11
+
12
+ # ═══════════════════════════════════════════════════════════════════════════════
13
+ # Configuration - Single source of truth
14
+ # ═══════════════════════════════════════════════════════════════════════════════
15
+
16
+ # Default model alias
17
+ DEFAULT_MODEL = "fireworks:accounts/fireworks/models/minimax-m2p1"
18
+
19
+ # Default API key environment variable (used by default model)
20
+ DEFAULT_API_KEY_ENV = "FIREWORKS_API_KEY"
21
+
22
+
23
+ # ═══════════════════════════════════════════════════════════════════════════════
24
+ # Factory functions
25
+ # ═══════════════════════════════════════════════════════════════════════════════
26
+
27
+
28
+ def get_provider(model: Union[str, ChatModel] = DEFAULT_MODEL) -> LLMProvider:
29
+ """
30
+ Get an LLM provider for the specified model.
31
+
32
+ Uses OpenAI SDK with provider-specific base URLs for OpenAI, Anthropic, and Fireworks.
33
+ For local models, uses HuggingFace Transformers.
34
+
35
+ Args:
36
+ model: Model specification - ChatModel enum or alias
37
+ Examples:
38
+ - ChatModel.ANTHROPIC_CLAUDE_HAIKU_4
39
+ - "haiku", "sonnet", "opus"
40
+ - "gpt-4o-mini"
41
+ - "minimax"
42
+ - "local:Qwen/Qwen2.5-1.5B-Instruct"
43
+ - "transformers:microsoft/Phi-3-mini-4k-instruct"
44
+
45
+ Returns:
46
+ LLMProvider instance
47
+
48
+ Raises:
49
+ ValueError: If model string not recognized
50
+ """
51
+ # Handle local/transformers prefix for local models
52
+ if isinstance(model, str) and (model.startswith("local:") or model.startswith("transformers:")):
53
+ return TransformersProvider(model)
54
+
55
+ # Handle ChatModel enum directly
56
+ if isinstance(model, ChatModel):
57
+ return OpenAIProvider(model)
58
+
59
+ # Try to parse as ChatModel
60
+ parsed = ChatModel.from_string(model)
61
+ if parsed:
62
+ return OpenAIProvider(parsed)
63
+
64
+ # Assume it's a raw model string
65
+ return OpenAIProvider(model)
66
+
67
+
68
+ def get_default_model() -> ChatModel:
69
+ """Get the default model."""
70
+ return ChatModel.from_string(DEFAULT_MODEL) or ChatModel.get_default()
71
+
72
+
73
+ def get_default_api_key() -> str | None:
74
+ """Get the default API key from environment."""
75
+ return os.environ.get(DEFAULT_API_KEY_ENV)
76
+
77
+
78
+ def list_available_models() -> list[dict]:
79
+ """List all available models."""
80
+ return ChatModel.list_all()