shotgun-sh 0.1.0.dev1__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.
Potentially problematic release.
This version of shotgun-sh might be problematic. Click here for more details.
- shotgun/__init__.py +3 -0
- shotgun/agents/__init__.py +1 -0
- shotgun/agents/agent_manager.py +196 -0
- shotgun/agents/common.py +295 -0
- shotgun/agents/config/__init__.py +13 -0
- shotgun/agents/config/manager.py +215 -0
- shotgun/agents/config/models.py +120 -0
- shotgun/agents/config/provider.py +91 -0
- shotgun/agents/history/__init__.py +5 -0
- shotgun/agents/history/history_processors.py +213 -0
- shotgun/agents/models.py +94 -0
- shotgun/agents/plan.py +119 -0
- shotgun/agents/research.py +131 -0
- shotgun/agents/tasks.py +122 -0
- shotgun/agents/tools/__init__.py +26 -0
- shotgun/agents/tools/codebase/__init__.py +28 -0
- shotgun/agents/tools/codebase/codebase_shell.py +256 -0
- shotgun/agents/tools/codebase/directory_lister.py +141 -0
- shotgun/agents/tools/codebase/file_read.py +144 -0
- shotgun/agents/tools/codebase/models.py +252 -0
- shotgun/agents/tools/codebase/query_graph.py +67 -0
- shotgun/agents/tools/codebase/retrieve_code.py +81 -0
- shotgun/agents/tools/file_management.py +130 -0
- shotgun/agents/tools/user_interaction.py +36 -0
- shotgun/agents/tools/web_search.py +69 -0
- shotgun/cli/__init__.py +1 -0
- shotgun/cli/codebase/__init__.py +5 -0
- shotgun/cli/codebase/commands.py +202 -0
- shotgun/cli/codebase/models.py +21 -0
- shotgun/cli/config.py +261 -0
- shotgun/cli/models.py +10 -0
- shotgun/cli/plan.py +65 -0
- shotgun/cli/research.py +78 -0
- shotgun/cli/tasks.py +71 -0
- shotgun/cli/utils.py +25 -0
- shotgun/codebase/__init__.py +12 -0
- shotgun/codebase/core/__init__.py +46 -0
- shotgun/codebase/core/change_detector.py +358 -0
- shotgun/codebase/core/code_retrieval.py +243 -0
- shotgun/codebase/core/ingestor.py +1497 -0
- shotgun/codebase/core/language_config.py +297 -0
- shotgun/codebase/core/manager.py +1554 -0
- shotgun/codebase/core/nl_query.py +327 -0
- shotgun/codebase/core/parser_loader.py +152 -0
- shotgun/codebase/models.py +107 -0
- shotgun/codebase/service.py +148 -0
- shotgun/logging_config.py +172 -0
- shotgun/main.py +73 -0
- shotgun/prompts/__init__.py +5 -0
- shotgun/prompts/agents/__init__.py +1 -0
- shotgun/prompts/agents/partials/codebase_understanding.j2 +79 -0
- shotgun/prompts/agents/partials/common_agent_system_prompt.j2 +10 -0
- shotgun/prompts/agents/partials/interactive_mode.j2 +8 -0
- shotgun/prompts/agents/plan.j2 +57 -0
- shotgun/prompts/agents/research.j2 +38 -0
- shotgun/prompts/agents/state/codebase/codebase_graphs_available.j2 +13 -0
- shotgun/prompts/agents/state/system_state.j2 +1 -0
- shotgun/prompts/agents/tasks.j2 +67 -0
- shotgun/prompts/codebase/__init__.py +1 -0
- shotgun/prompts/codebase/cypher_query_patterns.j2 +221 -0
- shotgun/prompts/codebase/cypher_system.j2 +28 -0
- shotgun/prompts/codebase/enhanced_query_context.j2 +10 -0
- shotgun/prompts/codebase/partials/cypher_rules.j2 +24 -0
- shotgun/prompts/codebase/partials/graph_schema.j2 +28 -0
- shotgun/prompts/codebase/partials/temporal_context.j2 +21 -0
- shotgun/prompts/history/__init__.py +1 -0
- shotgun/prompts/history/summarization.j2 +46 -0
- shotgun/prompts/loader.py +140 -0
- shotgun/prompts/user/research.j2 +5 -0
- shotgun/py.typed +0 -0
- shotgun/sdk/__init__.py +13 -0
- shotgun/sdk/codebase.py +195 -0
- shotgun/sdk/exceptions.py +17 -0
- shotgun/sdk/models.py +189 -0
- shotgun/sdk/services.py +23 -0
- shotgun/telemetry.py +68 -0
- shotgun/tui/__init__.py +0 -0
- shotgun/tui/app.py +49 -0
- shotgun/tui/components/prompt_input.py +69 -0
- shotgun/tui/components/spinner.py +86 -0
- shotgun/tui/components/splash.py +25 -0
- shotgun/tui/components/vertical_tail.py +28 -0
- shotgun/tui/screens/chat.py +415 -0
- shotgun/tui/screens/chat.tcss +28 -0
- shotgun/tui/screens/provider_config.py +221 -0
- shotgun/tui/screens/splash.py +31 -0
- shotgun/tui/styles.tcss +10 -0
- shotgun/utils/__init__.py +5 -0
- shotgun/utils/file_system_utils.py +31 -0
- shotgun_sh-0.1.0.dev1.dist-info/METADATA +318 -0
- shotgun_sh-0.1.0.dev1.dist-info/RECORD +94 -0
- shotgun_sh-0.1.0.dev1.dist-info/WHEEL +4 -0
- shotgun_sh-0.1.0.dev1.dist-info/entry_points.txt +3 -0
- shotgun_sh-0.1.0.dev1.dist-info/licenses/LICENSE +21 -0
shotgun/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Shotgun AI Agents."""
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
"""Agent manager for coordinating multiple AI agents with shared message history."""
|
|
2
|
+
|
|
3
|
+
from enum import Enum
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from pydantic_ai import Agent, DeferredToolRequests, DeferredToolResults, UsageLimits
|
|
7
|
+
from pydantic_ai.agent import AgentRunResult
|
|
8
|
+
from pydantic_ai.messages import ModelMessage, ModelRequest
|
|
9
|
+
from textual.message import Message
|
|
10
|
+
from textual.widget import Widget
|
|
11
|
+
|
|
12
|
+
from .models import AgentDeps, AgentRuntimeOptions
|
|
13
|
+
from .plan import create_plan_agent
|
|
14
|
+
from .research import create_research_agent
|
|
15
|
+
from .tasks import create_tasks_agent
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class AgentType(Enum):
|
|
19
|
+
"""Enumeration for available agent types (for Python < 3.11)."""
|
|
20
|
+
|
|
21
|
+
RESEARCH = "research"
|
|
22
|
+
PLAN = "plan"
|
|
23
|
+
TASKS = "tasks"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class MessageHistoryUpdated(Message):
|
|
27
|
+
"""Event posted when the message history is updated."""
|
|
28
|
+
|
|
29
|
+
def __init__(self, messages: list[ModelMessage], agent_type: AgentType) -> None:
|
|
30
|
+
"""Initialize the message history updated event.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
messages: The updated message history.
|
|
34
|
+
agent_type: The type of agent that triggered the update.
|
|
35
|
+
"""
|
|
36
|
+
super().__init__()
|
|
37
|
+
self.messages = messages
|
|
38
|
+
self.agent_type = agent_type
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class AgentManager(Widget):
|
|
42
|
+
"""Manages multiple agents with shared message history."""
|
|
43
|
+
|
|
44
|
+
def __init__(
|
|
45
|
+
self,
|
|
46
|
+
deps: AgentDeps | None = None,
|
|
47
|
+
initial_type: AgentType = AgentType.RESEARCH,
|
|
48
|
+
) -> None:
|
|
49
|
+
"""Initialize the agent manager.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
deps: Optional agent dependencies. If not provided, defaults to interactive mode.
|
|
53
|
+
"""
|
|
54
|
+
super().__init__()
|
|
55
|
+
# Use provided deps or create default with interactive mode
|
|
56
|
+
self.deps = deps
|
|
57
|
+
|
|
58
|
+
if self.deps is None:
|
|
59
|
+
raise ValueError("AgentDeps must be provided to AgentManager")
|
|
60
|
+
|
|
61
|
+
# Create AgentRuntimeOptions from deps for agent creation
|
|
62
|
+
agent_runtime_options = AgentRuntimeOptions(
|
|
63
|
+
interactive_mode=self.deps.interactive_mode,
|
|
64
|
+
working_directory=self.deps.working_directory,
|
|
65
|
+
max_iterations=self.deps.max_iterations,
|
|
66
|
+
queue=self.deps.queue,
|
|
67
|
+
tasks=self.deps.tasks,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
# Initialize all agents with the same deps
|
|
71
|
+
self.research_agent, _ = create_research_agent(
|
|
72
|
+
agent_runtime_options=agent_runtime_options
|
|
73
|
+
)
|
|
74
|
+
self.plan_agent, _ = create_plan_agent(
|
|
75
|
+
agent_runtime_options=agent_runtime_options
|
|
76
|
+
)
|
|
77
|
+
self.tasks_agent, _ = create_tasks_agent(
|
|
78
|
+
agent_runtime_options=agent_runtime_options
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
# Track current active agent
|
|
82
|
+
self._current_agent_type: AgentType = initial_type
|
|
83
|
+
|
|
84
|
+
# Maintain shared message history
|
|
85
|
+
self.ui_message_history: list[ModelMessage] = []
|
|
86
|
+
self.message_history: list[ModelMessage] = []
|
|
87
|
+
|
|
88
|
+
@property
|
|
89
|
+
def current_agent(self) -> Agent[AgentDeps, str | DeferredToolRequests]:
|
|
90
|
+
"""Get the currently active agent.
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
The currently selected agent instance.
|
|
94
|
+
"""
|
|
95
|
+
return self._get_agent(self._current_agent_type)
|
|
96
|
+
|
|
97
|
+
def _get_agent(
|
|
98
|
+
self, agent_type: AgentType
|
|
99
|
+
) -> Agent[AgentDeps, str | DeferredToolRequests]:
|
|
100
|
+
"""Get agent by type.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
agent_type: The type of agent to retrieve.
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
The requested agent instance.
|
|
107
|
+
"""
|
|
108
|
+
agent_map = {
|
|
109
|
+
AgentType.RESEARCH: self.research_agent,
|
|
110
|
+
AgentType.PLAN: self.plan_agent,
|
|
111
|
+
AgentType.TASKS: self.tasks_agent,
|
|
112
|
+
}
|
|
113
|
+
return agent_map[agent_type]
|
|
114
|
+
|
|
115
|
+
def set_agent(self, agent_type: AgentType) -> None:
|
|
116
|
+
"""Set the current active agent.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
agent_type: The agent type to activate (AgentType enum or string).
|
|
120
|
+
|
|
121
|
+
Raises:
|
|
122
|
+
ValueError: If invalid agent type is provided.
|
|
123
|
+
"""
|
|
124
|
+
try:
|
|
125
|
+
self._current_agent_type = AgentType(agent_type)
|
|
126
|
+
except ValueError:
|
|
127
|
+
raise ValueError(
|
|
128
|
+
f"Invalid agent type: {agent_type}. Must be one of: {', '.join(e.value for e in AgentType)}"
|
|
129
|
+
) from None
|
|
130
|
+
|
|
131
|
+
async def run(
|
|
132
|
+
self,
|
|
133
|
+
prompt: str | None = None,
|
|
134
|
+
*,
|
|
135
|
+
deps: AgentDeps | None = None,
|
|
136
|
+
usage_limits: UsageLimits | None = None,
|
|
137
|
+
deferred_tool_results: DeferredToolResults | None = None,
|
|
138
|
+
**kwargs: Any,
|
|
139
|
+
) -> AgentRunResult[str | DeferredToolRequests]:
|
|
140
|
+
"""Run the current agent with automatic message history management.
|
|
141
|
+
|
|
142
|
+
This method wraps the agent's run method, automatically injecting the
|
|
143
|
+
shared message history and updating it after each run.
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
prompt: Optional prompt to send to the agent.
|
|
147
|
+
deps: Optional dependencies override (defaults to manager's deps).
|
|
148
|
+
usage_limits: Optional usage limits for the agent run.
|
|
149
|
+
deferred_tool_results: Optional deferred tool results for continuing a conversation.
|
|
150
|
+
**kwargs: Additional keyword arguments to pass to the agent.
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
The agent run result.
|
|
154
|
+
"""
|
|
155
|
+
# Use manager's deps if not provided
|
|
156
|
+
if deps is None:
|
|
157
|
+
deps = self.deps
|
|
158
|
+
|
|
159
|
+
# Ensure deps is not None
|
|
160
|
+
if deps is None:
|
|
161
|
+
raise ValueError("AgentDeps must be provided")
|
|
162
|
+
|
|
163
|
+
if prompt:
|
|
164
|
+
self.ui_message_history.append(ModelRequest.user_text_prompt(prompt))
|
|
165
|
+
self._post_messages_updated()
|
|
166
|
+
|
|
167
|
+
# Run the agent with the shared message history
|
|
168
|
+
result: AgentRunResult[
|
|
169
|
+
str | DeferredToolRequests
|
|
170
|
+
] = await self.current_agent.run(
|
|
171
|
+
prompt,
|
|
172
|
+
deps=deps,
|
|
173
|
+
usage_limits=usage_limits,
|
|
174
|
+
message_history=self.message_history,
|
|
175
|
+
deferred_tool_results=deferred_tool_results,
|
|
176
|
+
**kwargs,
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
# Update the shared message history with all messages from this run
|
|
180
|
+
self.ui_message_history = self.ui_message_history + [
|
|
181
|
+
mes for mes in result.new_messages() if not isinstance(mes, ModelRequest)
|
|
182
|
+
]
|
|
183
|
+
|
|
184
|
+
self.message_history = result.all_messages()
|
|
185
|
+
self._post_messages_updated()
|
|
186
|
+
|
|
187
|
+
return result
|
|
188
|
+
|
|
189
|
+
def _post_messages_updated(self) -> None:
|
|
190
|
+
# Post event to notify listeners of the message history update
|
|
191
|
+
self.post_message(
|
|
192
|
+
MessageHistoryUpdated(
|
|
193
|
+
messages=self.ui_message_history.copy(),
|
|
194
|
+
agent_type=self._current_agent_type,
|
|
195
|
+
)
|
|
196
|
+
)
|
shotgun/agents/common.py
ADDED
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
"""Common utilities for agent creation and management."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from collections.abc import Callable
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from pydantic_ai import (
|
|
9
|
+
Agent,
|
|
10
|
+
DeferredToolRequests,
|
|
11
|
+
DeferredToolResults,
|
|
12
|
+
RunContext,
|
|
13
|
+
UsageLimits,
|
|
14
|
+
)
|
|
15
|
+
from pydantic_ai.agent import AgentRunResult
|
|
16
|
+
from pydantic_ai.messages import (
|
|
17
|
+
ModelMessage,
|
|
18
|
+
ModelResponse,
|
|
19
|
+
TextPart,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
from shotgun.agents.config import ProviderType, get_config_manager, get_provider_model
|
|
23
|
+
from shotgun.logging_config import get_logger
|
|
24
|
+
from shotgun.prompts import PromptLoader
|
|
25
|
+
from shotgun.sdk.services import get_codebase_service
|
|
26
|
+
from shotgun.utils import ensure_shotgun_directory_exists
|
|
27
|
+
|
|
28
|
+
from .history import token_limit_compactor
|
|
29
|
+
from .models import AgentDeps, AgentRuntimeOptions
|
|
30
|
+
from .tools import (
|
|
31
|
+
append_file,
|
|
32
|
+
ask_user,
|
|
33
|
+
codebase_shell,
|
|
34
|
+
directory_lister,
|
|
35
|
+
file_read,
|
|
36
|
+
query_graph,
|
|
37
|
+
read_file,
|
|
38
|
+
retrieve_code,
|
|
39
|
+
write_file,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
logger = get_logger(__name__)
|
|
43
|
+
|
|
44
|
+
# Global prompt loader instance
|
|
45
|
+
prompt_loader = PromptLoader()
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def ensure_file_exists(filename: str, header: str) -> str:
|
|
49
|
+
"""Ensure a markdown file exists with proper header and return its content.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
filename: Name of the file (e.g., "research.md")
|
|
53
|
+
header: Header to add if file is empty (e.g., "# Research")
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
Current file content
|
|
57
|
+
"""
|
|
58
|
+
shotgun_dir = Path.cwd() / ".shotgun"
|
|
59
|
+
file_path = shotgun_dir / filename
|
|
60
|
+
|
|
61
|
+
try:
|
|
62
|
+
if file_path.exists():
|
|
63
|
+
content = file_path.read_text(encoding="utf-8")
|
|
64
|
+
if not content.strip():
|
|
65
|
+
# File exists but is empty, add header
|
|
66
|
+
header_content = f"{header}\n\n"
|
|
67
|
+
file_path.write_text(header_content, encoding="utf-8")
|
|
68
|
+
return header_content
|
|
69
|
+
return content
|
|
70
|
+
else:
|
|
71
|
+
# File doesn't exist, create it with header
|
|
72
|
+
shotgun_dir.mkdir(exist_ok=True)
|
|
73
|
+
header_content = f"{header}\n\n"
|
|
74
|
+
file_path.write_text(header_content, encoding="utf-8")
|
|
75
|
+
return header_content
|
|
76
|
+
except Exception as e:
|
|
77
|
+
logger.error("Failed to initialize %s: %s", filename, str(e))
|
|
78
|
+
return f"{header}\n\n"
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def register_common_tools(
|
|
82
|
+
agent: Agent[AgentDeps], additional_tools: list[Any], interactive_mode: bool
|
|
83
|
+
) -> None:
|
|
84
|
+
"""Register common tools with an agent.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
agent: The Pydantic AI agent to register tools with
|
|
88
|
+
additional_tools: List of additional tools specific to this agent
|
|
89
|
+
interactive_mode: Whether to register interactive tools
|
|
90
|
+
"""
|
|
91
|
+
logger.debug("📌 Registering tools with agent")
|
|
92
|
+
|
|
93
|
+
# Register additional tools first (agent-specific)
|
|
94
|
+
for tool in additional_tools:
|
|
95
|
+
agent.tool_plain(tool)
|
|
96
|
+
|
|
97
|
+
# Register interactive tool if enabled
|
|
98
|
+
if interactive_mode:
|
|
99
|
+
agent.tool(ask_user)
|
|
100
|
+
logger.debug("📞 User interaction tool registered")
|
|
101
|
+
else:
|
|
102
|
+
logger.debug("🚫 User interaction disabled (non-interactive mode)")
|
|
103
|
+
|
|
104
|
+
# Register common file management tools
|
|
105
|
+
agent.tool_plain(read_file)
|
|
106
|
+
agent.tool_plain(write_file)
|
|
107
|
+
agent.tool_plain(append_file)
|
|
108
|
+
|
|
109
|
+
logger.debug("✅ Tool registration complete")
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
async def add_system_status_message(
|
|
113
|
+
deps: AgentDeps,
|
|
114
|
+
message_history: list[ModelMessage] | None = None,
|
|
115
|
+
) -> list[ModelMessage]:
|
|
116
|
+
"""Add a system status message to the message history.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
deps: Agent dependencies containing runtime options
|
|
120
|
+
message_history: Existing message history
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
Updated message history with system status message prepended
|
|
124
|
+
"""
|
|
125
|
+
message_history = message_history or []
|
|
126
|
+
codebase_understanding_graphs = await deps.codebase_service.list_graphs()
|
|
127
|
+
|
|
128
|
+
system_state = prompt_loader.render(
|
|
129
|
+
"agents/state/system_state.j2",
|
|
130
|
+
codebase_understanding_graphs=codebase_understanding_graphs,
|
|
131
|
+
context="system state",
|
|
132
|
+
)
|
|
133
|
+
message_history.append(
|
|
134
|
+
ModelResponse(
|
|
135
|
+
parts=[
|
|
136
|
+
TextPart(content=system_state),
|
|
137
|
+
]
|
|
138
|
+
)
|
|
139
|
+
)
|
|
140
|
+
return message_history
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def create_base_agent(
|
|
144
|
+
system_prompt_fn: Callable[[RunContext[AgentDeps]], str],
|
|
145
|
+
agent_runtime_options: AgentRuntimeOptions,
|
|
146
|
+
load_codebase_understanding_tools: bool = True,
|
|
147
|
+
additional_tools: list[Any] | None = None,
|
|
148
|
+
provider: ProviderType | None = None,
|
|
149
|
+
) -> tuple[Agent[AgentDeps, str | DeferredToolRequests], AgentDeps]:
|
|
150
|
+
"""Create a base agent with common configuration.
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
system_prompt_fn: Function that will be decorated as system_prompt
|
|
154
|
+
agent_runtime_options: Agent runtime options for the agent
|
|
155
|
+
additional_tools: Optional list of additional tools
|
|
156
|
+
provider: Optional provider override. If None, uses configured default
|
|
157
|
+
|
|
158
|
+
Returns:
|
|
159
|
+
Tuple of (Configured Pydantic AI agent, Agent dependencies)
|
|
160
|
+
"""
|
|
161
|
+
ensure_shotgun_directory_exists()
|
|
162
|
+
|
|
163
|
+
# Get configured model or fall back to hardcoded default
|
|
164
|
+
try:
|
|
165
|
+
model_config = get_provider_model(provider)
|
|
166
|
+
config_manager = get_config_manager()
|
|
167
|
+
provider_name = provider or config_manager.load().default_provider
|
|
168
|
+
logger.debug(
|
|
169
|
+
"🤖 Creating agent with configured %s model: %s",
|
|
170
|
+
provider_name.upper(),
|
|
171
|
+
model_config.name,
|
|
172
|
+
)
|
|
173
|
+
model = model_config.pydantic_model_name
|
|
174
|
+
|
|
175
|
+
# Create deps with model config and codebase service
|
|
176
|
+
codebase_service = get_codebase_service()
|
|
177
|
+
deps = AgentDeps(
|
|
178
|
+
**agent_runtime_options.model_dump(),
|
|
179
|
+
llm_model=model_config,
|
|
180
|
+
codebase_service=codebase_service,
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
except Exception as e:
|
|
184
|
+
logger.warning("Failed to load configured model, using fallback: %s", e)
|
|
185
|
+
logger.debug("🤖 Creating agent with fallback OpenAI GPT-4o")
|
|
186
|
+
raise ValueError("Configured model is required") from e
|
|
187
|
+
|
|
188
|
+
agent = Agent(
|
|
189
|
+
model,
|
|
190
|
+
output_type=[str, DeferredToolRequests],
|
|
191
|
+
deps_type=AgentDeps,
|
|
192
|
+
instrument=True,
|
|
193
|
+
history_processors=[token_limit_compactor],
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
# Decorate the system prompt function
|
|
197
|
+
agent.system_prompt(system_prompt_fn)
|
|
198
|
+
|
|
199
|
+
# Register additional tools first (agent-specific)
|
|
200
|
+
for tool in additional_tools or []:
|
|
201
|
+
agent.tool_plain(tool)
|
|
202
|
+
|
|
203
|
+
# Register interactive tool conditionally based on deps
|
|
204
|
+
if deps.interactive_mode:
|
|
205
|
+
agent.tool(ask_user)
|
|
206
|
+
logger.debug("📞 Interactive mode enabled - ask_user tool registered")
|
|
207
|
+
|
|
208
|
+
# Register common file management tools (always available)
|
|
209
|
+
agent.tool_plain(read_file)
|
|
210
|
+
agent.tool_plain(write_file)
|
|
211
|
+
agent.tool_plain(append_file)
|
|
212
|
+
|
|
213
|
+
# Register codebase understanding tools (always available)
|
|
214
|
+
if load_codebase_understanding_tools:
|
|
215
|
+
agent.tool(query_graph)
|
|
216
|
+
agent.tool(retrieve_code)
|
|
217
|
+
agent.tool(file_read)
|
|
218
|
+
agent.tool(directory_lister)
|
|
219
|
+
agent.tool(codebase_shell)
|
|
220
|
+
logger.debug("🧠Codebase understanding tools registered")
|
|
221
|
+
else:
|
|
222
|
+
logger.debug("🚫🧠Codebase understanding tools not registered")
|
|
223
|
+
|
|
224
|
+
logger.debug("✅ Agent creation complete")
|
|
225
|
+
return agent, deps
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def create_usage_limits() -> UsageLimits:
|
|
229
|
+
"""Create reasonable usage limits for agent runs.
|
|
230
|
+
|
|
231
|
+
Returns:
|
|
232
|
+
UsageLimits configured for responsible API usage
|
|
233
|
+
"""
|
|
234
|
+
return UsageLimits(
|
|
235
|
+
request_limit=100, # Maximum number of model requests per run
|
|
236
|
+
tool_calls_limit=100, # Maximum number of successful tool calls
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def get_file_history(filename: str) -> str:
|
|
241
|
+
"""Get the history content from a file.
|
|
242
|
+
|
|
243
|
+
Args:
|
|
244
|
+
filename: Name of the file (e.g., "research.md")
|
|
245
|
+
|
|
246
|
+
Returns:
|
|
247
|
+
File content or fallback message
|
|
248
|
+
"""
|
|
249
|
+
try:
|
|
250
|
+
return read_file(filename)
|
|
251
|
+
except Exception as e:
|
|
252
|
+
logger.debug("Could not load %s history: %s", filename, str(e))
|
|
253
|
+
return f"No {filename.replace('.md', '')} history available."
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
async def run_agent(
|
|
257
|
+
agent: Agent[AgentDeps, str | DeferredToolRequests],
|
|
258
|
+
prompt: str,
|
|
259
|
+
deps: AgentDeps,
|
|
260
|
+
message_history: list[ModelMessage] | None = None,
|
|
261
|
+
usage_limits: UsageLimits | None = None,
|
|
262
|
+
) -> AgentRunResult[str | DeferredToolRequests]:
|
|
263
|
+
result = await agent.run(
|
|
264
|
+
prompt,
|
|
265
|
+
deps=deps,
|
|
266
|
+
usage_limits=usage_limits,
|
|
267
|
+
message_history=message_history,
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
messages = result.all_messages()
|
|
271
|
+
while isinstance(result.output, DeferredToolRequests):
|
|
272
|
+
logger.info("got deferred tool requests")
|
|
273
|
+
await deps.queue.join()
|
|
274
|
+
requests = result.output
|
|
275
|
+
done, _ = await asyncio.wait(deps.tasks)
|
|
276
|
+
|
|
277
|
+
task_results = [task.result() for task in done]
|
|
278
|
+
task_results_by_tool_call_id = {
|
|
279
|
+
result.tool_call_id: result.answer for result in task_results
|
|
280
|
+
}
|
|
281
|
+
logger.info("got task results", task_results_by_tool_call_id)
|
|
282
|
+
results = DeferredToolResults()
|
|
283
|
+
for call in requests.calls:
|
|
284
|
+
results.calls[call.tool_call_id] = task_results_by_tool_call_id[
|
|
285
|
+
call.tool_call_id
|
|
286
|
+
]
|
|
287
|
+
result = await agent.run(
|
|
288
|
+
deps=deps,
|
|
289
|
+
usage_limits=usage_limits,
|
|
290
|
+
message_history=messages,
|
|
291
|
+
deferred_tool_results=results,
|
|
292
|
+
)
|
|
293
|
+
messages = result.all_messages()
|
|
294
|
+
|
|
295
|
+
return result
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Configuration module for Shotgun CLI."""
|
|
2
|
+
|
|
3
|
+
from .manager import ConfigManager, get_config_manager
|
|
4
|
+
from .models import ProviderType, ShotgunConfig
|
|
5
|
+
from .provider import get_provider_model
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"ConfigManager",
|
|
9
|
+
"get_config_manager",
|
|
10
|
+
"ProviderType",
|
|
11
|
+
"ShotgunConfig",
|
|
12
|
+
"get_provider_model",
|
|
13
|
+
]
|