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.

Files changed (94) hide show
  1. shotgun/__init__.py +3 -0
  2. shotgun/agents/__init__.py +1 -0
  3. shotgun/agents/agent_manager.py +196 -0
  4. shotgun/agents/common.py +295 -0
  5. shotgun/agents/config/__init__.py +13 -0
  6. shotgun/agents/config/manager.py +215 -0
  7. shotgun/agents/config/models.py +120 -0
  8. shotgun/agents/config/provider.py +91 -0
  9. shotgun/agents/history/__init__.py +5 -0
  10. shotgun/agents/history/history_processors.py +213 -0
  11. shotgun/agents/models.py +94 -0
  12. shotgun/agents/plan.py +119 -0
  13. shotgun/agents/research.py +131 -0
  14. shotgun/agents/tasks.py +122 -0
  15. shotgun/agents/tools/__init__.py +26 -0
  16. shotgun/agents/tools/codebase/__init__.py +28 -0
  17. shotgun/agents/tools/codebase/codebase_shell.py +256 -0
  18. shotgun/agents/tools/codebase/directory_lister.py +141 -0
  19. shotgun/agents/tools/codebase/file_read.py +144 -0
  20. shotgun/agents/tools/codebase/models.py +252 -0
  21. shotgun/agents/tools/codebase/query_graph.py +67 -0
  22. shotgun/agents/tools/codebase/retrieve_code.py +81 -0
  23. shotgun/agents/tools/file_management.py +130 -0
  24. shotgun/agents/tools/user_interaction.py +36 -0
  25. shotgun/agents/tools/web_search.py +69 -0
  26. shotgun/cli/__init__.py +1 -0
  27. shotgun/cli/codebase/__init__.py +5 -0
  28. shotgun/cli/codebase/commands.py +202 -0
  29. shotgun/cli/codebase/models.py +21 -0
  30. shotgun/cli/config.py +261 -0
  31. shotgun/cli/models.py +10 -0
  32. shotgun/cli/plan.py +65 -0
  33. shotgun/cli/research.py +78 -0
  34. shotgun/cli/tasks.py +71 -0
  35. shotgun/cli/utils.py +25 -0
  36. shotgun/codebase/__init__.py +12 -0
  37. shotgun/codebase/core/__init__.py +46 -0
  38. shotgun/codebase/core/change_detector.py +358 -0
  39. shotgun/codebase/core/code_retrieval.py +243 -0
  40. shotgun/codebase/core/ingestor.py +1497 -0
  41. shotgun/codebase/core/language_config.py +297 -0
  42. shotgun/codebase/core/manager.py +1554 -0
  43. shotgun/codebase/core/nl_query.py +327 -0
  44. shotgun/codebase/core/parser_loader.py +152 -0
  45. shotgun/codebase/models.py +107 -0
  46. shotgun/codebase/service.py +148 -0
  47. shotgun/logging_config.py +172 -0
  48. shotgun/main.py +73 -0
  49. shotgun/prompts/__init__.py +5 -0
  50. shotgun/prompts/agents/__init__.py +1 -0
  51. shotgun/prompts/agents/partials/codebase_understanding.j2 +79 -0
  52. shotgun/prompts/agents/partials/common_agent_system_prompt.j2 +10 -0
  53. shotgun/prompts/agents/partials/interactive_mode.j2 +8 -0
  54. shotgun/prompts/agents/plan.j2 +57 -0
  55. shotgun/prompts/agents/research.j2 +38 -0
  56. shotgun/prompts/agents/state/codebase/codebase_graphs_available.j2 +13 -0
  57. shotgun/prompts/agents/state/system_state.j2 +1 -0
  58. shotgun/prompts/agents/tasks.j2 +67 -0
  59. shotgun/prompts/codebase/__init__.py +1 -0
  60. shotgun/prompts/codebase/cypher_query_patterns.j2 +221 -0
  61. shotgun/prompts/codebase/cypher_system.j2 +28 -0
  62. shotgun/prompts/codebase/enhanced_query_context.j2 +10 -0
  63. shotgun/prompts/codebase/partials/cypher_rules.j2 +24 -0
  64. shotgun/prompts/codebase/partials/graph_schema.j2 +28 -0
  65. shotgun/prompts/codebase/partials/temporal_context.j2 +21 -0
  66. shotgun/prompts/history/__init__.py +1 -0
  67. shotgun/prompts/history/summarization.j2 +46 -0
  68. shotgun/prompts/loader.py +140 -0
  69. shotgun/prompts/user/research.j2 +5 -0
  70. shotgun/py.typed +0 -0
  71. shotgun/sdk/__init__.py +13 -0
  72. shotgun/sdk/codebase.py +195 -0
  73. shotgun/sdk/exceptions.py +17 -0
  74. shotgun/sdk/models.py +189 -0
  75. shotgun/sdk/services.py +23 -0
  76. shotgun/telemetry.py +68 -0
  77. shotgun/tui/__init__.py +0 -0
  78. shotgun/tui/app.py +49 -0
  79. shotgun/tui/components/prompt_input.py +69 -0
  80. shotgun/tui/components/spinner.py +86 -0
  81. shotgun/tui/components/splash.py +25 -0
  82. shotgun/tui/components/vertical_tail.py +28 -0
  83. shotgun/tui/screens/chat.py +415 -0
  84. shotgun/tui/screens/chat.tcss +28 -0
  85. shotgun/tui/screens/provider_config.py +221 -0
  86. shotgun/tui/screens/splash.py +31 -0
  87. shotgun/tui/styles.tcss +10 -0
  88. shotgun/utils/__init__.py +5 -0
  89. shotgun/utils/file_system_utils.py +31 -0
  90. shotgun_sh-0.1.0.dev1.dist-info/METADATA +318 -0
  91. shotgun_sh-0.1.0.dev1.dist-info/RECORD +94 -0
  92. shotgun_sh-0.1.0.dev1.dist-info/WHEEL +4 -0
  93. shotgun_sh-0.1.0.dev1.dist-info/entry_points.txt +3 -0
  94. shotgun_sh-0.1.0.dev1.dist-info/licenses/LICENSE +21 -0
shotgun/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """Shotgun CLI package."""
2
+
3
+ __version__ = "0.1.0.dev1"
@@ -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
+ )
@@ -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
+ ]