shotgun-sh 0.2.17__py3-none-any.whl → 0.3.3.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.
- shotgun/agents/agent_manager.py +28 -14
- shotgun/agents/common.py +1 -1
- shotgun/agents/config/README.md +89 -0
- shotgun/agents/config/__init__.py +10 -1
- shotgun/agents/config/manager.py +323 -53
- shotgun/agents/config/models.py +85 -21
- shotgun/agents/config/provider.py +51 -13
- shotgun/agents/config/streaming_test.py +119 -0
- shotgun/agents/context_analyzer/analyzer.py +6 -2
- shotgun/agents/conversation/__init__.py +18 -0
- shotgun/agents/conversation/filters.py +164 -0
- shotgun/agents/conversation/history/chunking.py +278 -0
- shotgun/agents/{history → conversation/history}/compaction.py +27 -1
- shotgun/agents/{history → conversation/history}/constants.py +5 -0
- shotgun/agents/conversation/history/file_content_deduplication.py +216 -0
- shotgun/agents/{history → conversation/history}/history_processors.py +267 -3
- shotgun/agents/{history → conversation/history}/token_counting/anthropic.py +8 -0
- shotgun/agents/{conversation_manager.py → conversation/manager.py} +1 -1
- shotgun/agents/{conversation_history.py → conversation/models.py} +8 -94
- shotgun/agents/error/__init__.py +11 -0
- shotgun/agents/error/models.py +19 -0
- shotgun/agents/runner.py +230 -0
- shotgun/agents/tools/web_search/openai.py +1 -1
- shotgun/build_constants.py +2 -2
- shotgun/cli/clear.py +1 -1
- shotgun/cli/compact.py +5 -3
- shotgun/cli/context.py +44 -1
- shotgun/cli/error_handler.py +24 -0
- shotgun/cli/export.py +34 -34
- shotgun/cli/plan.py +34 -34
- shotgun/cli/research.py +17 -9
- shotgun/cli/spec/__init__.py +5 -0
- shotgun/cli/spec/backup.py +81 -0
- shotgun/cli/spec/commands.py +132 -0
- shotgun/cli/spec/models.py +48 -0
- shotgun/cli/spec/pull_service.py +219 -0
- shotgun/cli/specify.py +20 -19
- shotgun/cli/tasks.py +34 -34
- shotgun/codebase/core/ingestor.py +153 -7
- shotgun/codebase/models.py +2 -0
- shotgun/exceptions.py +325 -0
- shotgun/llm_proxy/__init__.py +17 -0
- shotgun/llm_proxy/client.py +215 -0
- shotgun/llm_proxy/models.py +137 -0
- shotgun/logging_config.py +42 -0
- shotgun/main.py +4 -0
- shotgun/posthog_telemetry.py +1 -1
- shotgun/prompts/agents/partials/common_agent_system_prompt.j2 +28 -3
- shotgun/prompts/agents/partials/interactive_mode.j2 +3 -3
- shotgun/prompts/agents/plan.j2 +16 -0
- shotgun/prompts/agents/research.j2 +16 -3
- shotgun/prompts/agents/specify.j2 +54 -1
- shotgun/prompts/agents/state/system_state.j2 +0 -2
- shotgun/prompts/agents/tasks.j2 +16 -0
- shotgun/prompts/history/chunk_summarization.j2 +34 -0
- shotgun/prompts/history/combine_summaries.j2 +53 -0
- shotgun/sdk/codebase.py +14 -3
- shotgun/settings.py +5 -0
- shotgun/shotgun_web/__init__.py +67 -1
- shotgun/shotgun_web/client.py +42 -1
- shotgun/shotgun_web/constants.py +46 -0
- shotgun/shotgun_web/exceptions.py +29 -0
- shotgun/shotgun_web/models.py +390 -0
- shotgun/shotgun_web/shared_specs/__init__.py +32 -0
- shotgun/shotgun_web/shared_specs/file_scanner.py +175 -0
- shotgun/shotgun_web/shared_specs/hasher.py +83 -0
- shotgun/shotgun_web/shared_specs/models.py +71 -0
- shotgun/shotgun_web/shared_specs/upload_pipeline.py +329 -0
- shotgun/shotgun_web/shared_specs/utils.py +34 -0
- shotgun/shotgun_web/specs_client.py +703 -0
- shotgun/shotgun_web/supabase_client.py +31 -0
- shotgun/tui/app.py +73 -9
- shotgun/tui/containers.py +1 -1
- shotgun/tui/layout.py +5 -0
- shotgun/tui/screens/chat/chat_screen.py +372 -95
- shotgun/tui/screens/chat/codebase_index_prompt_screen.py +196 -17
- shotgun/tui/screens/chat_screen/command_providers.py +13 -2
- shotgun/tui/screens/chat_screen/hint_message.py +76 -1
- shotgun/tui/screens/confirmation_dialog.py +40 -0
- shotgun/tui/screens/directory_setup.py +45 -41
- shotgun/tui/screens/feedback.py +10 -3
- shotgun/tui/screens/github_issue.py +11 -2
- shotgun/tui/screens/model_picker.py +28 -8
- shotgun/tui/screens/onboarding.py +149 -0
- shotgun/tui/screens/pipx_migration.py +58 -6
- shotgun/tui/screens/provider_config.py +66 -8
- shotgun/tui/screens/shared_specs/__init__.py +21 -0
- shotgun/tui/screens/shared_specs/create_spec_dialog.py +273 -0
- shotgun/tui/screens/shared_specs/models.py +56 -0
- shotgun/tui/screens/shared_specs/share_specs_dialog.py +390 -0
- shotgun/tui/screens/shared_specs/upload_progress_screen.py +452 -0
- shotgun/tui/screens/shotgun_auth.py +110 -16
- shotgun/tui/screens/spec_pull.py +288 -0
- shotgun/tui/screens/welcome.py +123 -0
- shotgun/tui/services/conversation_service.py +5 -2
- shotgun/tui/widgets/widget_coordinator.py +1 -1
- {shotgun_sh-0.2.17.dist-info → shotgun_sh-0.3.3.dev1.dist-info}/METADATA +9 -2
- {shotgun_sh-0.2.17.dist-info → shotgun_sh-0.3.3.dev1.dist-info}/RECORD +112 -77
- {shotgun_sh-0.2.17.dist-info → shotgun_sh-0.3.3.dev1.dist-info}/WHEEL +1 -1
- /shotgun/agents/{history → conversation/history}/__init__.py +0 -0
- /shotgun/agents/{history → conversation/history}/context_extraction.py +0 -0
- /shotgun/agents/{history → conversation/history}/history_building.py +0 -0
- /shotgun/agents/{history → conversation/history}/message_utils.py +0 -0
- /shotgun/agents/{history → conversation/history}/token_counting/__init__.py +0 -0
- /shotgun/agents/{history → conversation/history}/token_counting/base.py +0 -0
- /shotgun/agents/{history → conversation/history}/token_counting/openai.py +0 -0
- /shotgun/agents/{history → conversation/history}/token_counting/sentencepiece_counter.py +0 -0
- /shotgun/agents/{history → conversation/history}/token_counting/tokenizer_cache.py +0 -0
- /shotgun/agents/{history → conversation/history}/token_counting/utils.py +0 -0
- /shotgun/agents/{history → conversation/history}/token_estimation.py +0 -0
- {shotgun_sh-0.2.17.dist-info → shotgun_sh-0.3.3.dev1.dist-info}/entry_points.txt +0 -0
- {shotgun_sh-0.2.17.dist-info → shotgun_sh-0.3.3.dev1.dist-info}/licenses/LICENSE +0 -0
shotgun/agents/runner.py
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
"""Unified agent execution with consistent error handling.
|
|
2
|
+
|
|
3
|
+
This module provides a reusable agent runner that wraps agent execution exceptions
|
|
4
|
+
in user-friendly custom exceptions that can be caught and displayed by TUI or CLI.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import asyncio
|
|
8
|
+
import logging
|
|
9
|
+
from typing import TYPE_CHECKING, NoReturn
|
|
10
|
+
|
|
11
|
+
from anthropic import APIStatusError as AnthropicAPIStatusError
|
|
12
|
+
from openai import APIStatusError as OpenAIAPIStatusError
|
|
13
|
+
from pydantic_ai.exceptions import ModelHTTPError, UnexpectedModelBehavior
|
|
14
|
+
|
|
15
|
+
from shotgun.agents.error.models import AgentErrorContext
|
|
16
|
+
from shotgun.exceptions import (
|
|
17
|
+
AgentCancelledException,
|
|
18
|
+
BudgetExceededException,
|
|
19
|
+
BYOKAuthenticationException,
|
|
20
|
+
BYOKGenericAPIException,
|
|
21
|
+
BYOKQuotaBillingException,
|
|
22
|
+
BYOKRateLimitException,
|
|
23
|
+
BYOKServiceOverloadException,
|
|
24
|
+
ContextSizeLimitExceeded,
|
|
25
|
+
GenericAPIStatusException,
|
|
26
|
+
ShotgunRateLimitException,
|
|
27
|
+
ShotgunServiceOverloadException,
|
|
28
|
+
UnknownAgentException,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
if TYPE_CHECKING:
|
|
32
|
+
from shotgun.agents.agent_manager import AgentManager
|
|
33
|
+
|
|
34
|
+
logger = logging.getLogger(__name__)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class AgentRunner:
|
|
38
|
+
"""Unified agent execution wrapper with consistent error handling.
|
|
39
|
+
|
|
40
|
+
This class wraps agent execution and converts any exceptions into
|
|
41
|
+
user-friendly custom exceptions that can be caught and displayed by the
|
|
42
|
+
calling interface (TUI or CLI).
|
|
43
|
+
|
|
44
|
+
The runner:
|
|
45
|
+
- Executes the agent
|
|
46
|
+
- Logs errors for debugging
|
|
47
|
+
- Wraps exceptions in custom exception types (AgentCancelledException,
|
|
48
|
+
BYOKRateLimitException, etc.)
|
|
49
|
+
- Lets exceptions propagate to caller for display
|
|
50
|
+
|
|
51
|
+
Example:
|
|
52
|
+
>>> runner = AgentRunner(agent_manager)
|
|
53
|
+
>>> try:
|
|
54
|
+
>>> await runner.run("Write a hello world function")
|
|
55
|
+
>>> except ContextSizeLimitExceeded as e:
|
|
56
|
+
>>> print(e.to_markdown())
|
|
57
|
+
>>> except BYOKRateLimitException as e:
|
|
58
|
+
>>> print(e.to_plain_text())
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
def __init__(self, agent_manager: "AgentManager"):
|
|
62
|
+
"""Initialize the agent runner.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
agent_manager: The agent manager to execute
|
|
66
|
+
"""
|
|
67
|
+
self.agent_manager = agent_manager
|
|
68
|
+
|
|
69
|
+
async def run(self, prompt: str) -> None:
|
|
70
|
+
"""Run the agent with the given prompt.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
prompt: The user's prompt/query
|
|
74
|
+
|
|
75
|
+
Raises:
|
|
76
|
+
Custom exceptions for different error types:
|
|
77
|
+
- AgentCancelledException: User cancelled the operation
|
|
78
|
+
- ContextSizeLimitExceeded: Context too large for model
|
|
79
|
+
- BudgetExceededException: Shotgun Account budget exceeded
|
|
80
|
+
- BYOKRateLimitException: BYOK rate limit hit
|
|
81
|
+
- BYOKQuotaBillingException: BYOK quota/billing issue
|
|
82
|
+
- BYOKAuthenticationException: BYOK authentication failed
|
|
83
|
+
- BYOKServiceOverloadException: BYOK service overloaded
|
|
84
|
+
- BYOKGenericAPIException: Generic BYOK API error
|
|
85
|
+
- ShotgunServiceOverloadException: Shotgun service overloaded
|
|
86
|
+
- ShotgunRateLimitException: Shotgun rate limit hit
|
|
87
|
+
- GenericAPIStatusException: Generic API error
|
|
88
|
+
- UnknownAgentException: Unknown/unclassified error
|
|
89
|
+
"""
|
|
90
|
+
try:
|
|
91
|
+
await self.agent_manager.run(prompt=prompt)
|
|
92
|
+
|
|
93
|
+
except asyncio.CancelledError as e:
|
|
94
|
+
# User cancelled - wrap and re-raise as our custom exception
|
|
95
|
+
context = self._create_error_context(e)
|
|
96
|
+
self._classify_and_raise(context)
|
|
97
|
+
|
|
98
|
+
except ContextSizeLimitExceeded as e:
|
|
99
|
+
# Already a custom exception - log and re-raise
|
|
100
|
+
logger.info(
|
|
101
|
+
"Context size limit exceeded",
|
|
102
|
+
extra={
|
|
103
|
+
"max_tokens": e.max_tokens,
|
|
104
|
+
"model_name": e.model_name,
|
|
105
|
+
},
|
|
106
|
+
)
|
|
107
|
+
raise
|
|
108
|
+
|
|
109
|
+
except Exception as e:
|
|
110
|
+
# Log with full stack trace to shotgun.log
|
|
111
|
+
logger.exception(
|
|
112
|
+
"Agent run failed",
|
|
113
|
+
extra={
|
|
114
|
+
"agent_mode": self.agent_manager._current_agent_type.value,
|
|
115
|
+
"error_type": type(e).__name__,
|
|
116
|
+
},
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
# Create error context and wrap/raise custom exception
|
|
120
|
+
context = self._create_error_context(e)
|
|
121
|
+
self._classify_and_raise(context)
|
|
122
|
+
|
|
123
|
+
def _create_error_context(self, exception: BaseException) -> AgentErrorContext:
|
|
124
|
+
"""Create error context from exception and agent state.
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
exception: The exception that was raised
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
AgentErrorContext with all necessary information for classification
|
|
131
|
+
"""
|
|
132
|
+
return AgentErrorContext(
|
|
133
|
+
exception=exception,
|
|
134
|
+
is_shotgun_account=self.agent_manager.deps.llm_model.is_shotgun_account,
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
def _classify_and_raise(self, context: AgentErrorContext) -> NoReturn:
|
|
138
|
+
"""Classify an exception and raise the appropriate custom exception.
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
context: Context information about the error
|
|
142
|
+
|
|
143
|
+
Raises:
|
|
144
|
+
Custom exception based on the error type
|
|
145
|
+
"""
|
|
146
|
+
exception = context.exception
|
|
147
|
+
error_name = type(exception).__name__
|
|
148
|
+
error_message = str(exception)
|
|
149
|
+
|
|
150
|
+
# Check for cancellation
|
|
151
|
+
if isinstance(exception, asyncio.CancelledError):
|
|
152
|
+
raise AgentCancelledException() from exception
|
|
153
|
+
|
|
154
|
+
# Check for context size limit exceeded
|
|
155
|
+
if isinstance(exception, ContextSizeLimitExceeded):
|
|
156
|
+
# Already the right exception type, re-raise it
|
|
157
|
+
raise exception
|
|
158
|
+
|
|
159
|
+
# Check for budget exceeded (Shotgun Account only)
|
|
160
|
+
if (
|
|
161
|
+
context.is_shotgun_account
|
|
162
|
+
and "apistatuserror" in error_name.lower()
|
|
163
|
+
and "budget" in error_message.lower()
|
|
164
|
+
and "exceeded" in error_message.lower()
|
|
165
|
+
):
|
|
166
|
+
raise BudgetExceededException(message=error_message) from exception
|
|
167
|
+
|
|
168
|
+
# Check for empty model response (e.g., model unavailable or misconfigured)
|
|
169
|
+
if isinstance(exception, UnexpectedModelBehavior):
|
|
170
|
+
raise GenericAPIStatusException(
|
|
171
|
+
"The model returned an empty response. This may indicate:\n"
|
|
172
|
+
"- The model is unavailable or misconfigured\n"
|
|
173
|
+
"- A temporary service issue\n\n"
|
|
174
|
+
"Try switching to a different model or try again later."
|
|
175
|
+
) from exception
|
|
176
|
+
|
|
177
|
+
# Detect API errors
|
|
178
|
+
is_api_error = False
|
|
179
|
+
if isinstance(exception, OpenAIAPIStatusError):
|
|
180
|
+
is_api_error = True
|
|
181
|
+
elif isinstance(exception, AnthropicAPIStatusError):
|
|
182
|
+
is_api_error = True
|
|
183
|
+
elif isinstance(exception, ModelHTTPError):
|
|
184
|
+
# pydantic_ai wraps API errors in ModelHTTPError
|
|
185
|
+
# Check for HTTP error status codes (4xx client errors)
|
|
186
|
+
if 400 <= exception.status_code < 500:
|
|
187
|
+
is_api_error = True
|
|
188
|
+
|
|
189
|
+
# BYOK user API errors
|
|
190
|
+
if not context.is_shotgun_account and is_api_error:
|
|
191
|
+
self._raise_byok_api_error(error_message, exception)
|
|
192
|
+
|
|
193
|
+
# Shotgun Account specific errors
|
|
194
|
+
if "APIStatusError" in error_name:
|
|
195
|
+
if "overload" in error_message.lower():
|
|
196
|
+
raise ShotgunServiceOverloadException(error_message) from exception
|
|
197
|
+
elif "rate" in error_message.lower():
|
|
198
|
+
raise ShotgunRateLimitException(error_message) from exception
|
|
199
|
+
else:
|
|
200
|
+
raise GenericAPIStatusException(error_message) from exception
|
|
201
|
+
|
|
202
|
+
# Unknown error - wrap in our custom exception
|
|
203
|
+
raise UnknownAgentException(exception) from exception
|
|
204
|
+
|
|
205
|
+
def _raise_byok_api_error(
|
|
206
|
+
self, error_message: str, original_exception: Exception
|
|
207
|
+
) -> NoReturn:
|
|
208
|
+
"""Classify and raise API errors for BYOK users into specific types.
|
|
209
|
+
|
|
210
|
+
Args:
|
|
211
|
+
error_message: The error message from the API
|
|
212
|
+
original_exception: The original exception
|
|
213
|
+
|
|
214
|
+
Raises:
|
|
215
|
+
Specific BYOK exception type
|
|
216
|
+
"""
|
|
217
|
+
error_lower = error_message.lower()
|
|
218
|
+
|
|
219
|
+
if "rate" in error_lower:
|
|
220
|
+
raise BYOKRateLimitException(error_message) from original_exception
|
|
221
|
+
elif "quota" in error_lower or "billing" in error_lower:
|
|
222
|
+
raise BYOKQuotaBillingException(error_message) from original_exception
|
|
223
|
+
elif "authentication" in error_lower or (
|
|
224
|
+
"invalid" in error_lower and "key" in error_lower
|
|
225
|
+
):
|
|
226
|
+
raise BYOKAuthenticationException(error_message) from original_exception
|
|
227
|
+
elif "overload" in error_lower:
|
|
228
|
+
raise BYOKServiceOverloadException(error_message) from original_exception
|
|
229
|
+
else:
|
|
230
|
+
raise BYOKGenericAPIException(error_message) from original_exception
|
|
@@ -64,7 +64,7 @@ async def openai_web_search_tool(query: str) -> str:
|
|
|
64
64
|
)
|
|
65
65
|
|
|
66
66
|
client = AsyncOpenAI(api_key=api_key)
|
|
67
|
-
response = await client.responses.create(
|
|
67
|
+
response = await client.responses.create(
|
|
68
68
|
model="gpt-5-mini",
|
|
69
69
|
input=[
|
|
70
70
|
{"role": "user", "content": [{"type": "input_text", "text": prompt}]}
|
shotgun/build_constants.py
CHANGED
|
@@ -12,8 +12,8 @@ POSTHOG_API_KEY = 'phc_KKnChzZUKeNqZDOTJ6soCBWNQSx3vjiULdwTR9H5Mcr'
|
|
|
12
12
|
POSTHOG_PROJECT_ID = '191396'
|
|
13
13
|
|
|
14
14
|
# Logfire configuration embedded at build time (only for dev builds)
|
|
15
|
-
LOGFIRE_ENABLED = ''
|
|
16
|
-
LOGFIRE_TOKEN = ''
|
|
15
|
+
LOGFIRE_ENABLED = 'true'
|
|
16
|
+
LOGFIRE_TOKEN = 'pylf_v1_us_RwZMlJm1tX6j0PL5RWWbmZpzK2hLBNtFWStNKlySfjh8'
|
|
17
17
|
|
|
18
18
|
# Build metadata
|
|
19
19
|
BUILD_TIME_ENV = "production" if SENTRY_DSN else "development"
|
shotgun/cli/clear.py
CHANGED
|
@@ -6,7 +6,7 @@ from pathlib import Path
|
|
|
6
6
|
import typer
|
|
7
7
|
from rich.console import Console
|
|
8
8
|
|
|
9
|
-
from shotgun.agents.
|
|
9
|
+
from shotgun.agents.conversation import ConversationManager
|
|
10
10
|
from shotgun.logging_config import get_logger
|
|
11
11
|
|
|
12
12
|
app = typer.Typer(
|
shotgun/cli/compact.py
CHANGED
|
@@ -10,9 +10,11 @@ from pydantic_ai.usage import RequestUsage
|
|
|
10
10
|
from rich.console import Console
|
|
11
11
|
|
|
12
12
|
from shotgun.agents.config import get_provider_model
|
|
13
|
-
from shotgun.agents.
|
|
14
|
-
from shotgun.agents.history.history_processors import token_limit_compactor
|
|
15
|
-
from shotgun.agents.history.token_estimation import
|
|
13
|
+
from shotgun.agents.conversation import ConversationManager
|
|
14
|
+
from shotgun.agents.conversation.history.history_processors import token_limit_compactor
|
|
15
|
+
from shotgun.agents.conversation.history.token_estimation import (
|
|
16
|
+
estimate_tokens_from_messages,
|
|
17
|
+
)
|
|
16
18
|
from shotgun.cli.models import OutputFormat
|
|
17
19
|
from shotgun.logging_config import get_logger
|
|
18
20
|
|
shotgun/cli/context.py
CHANGED
|
@@ -5,6 +5,7 @@ import json
|
|
|
5
5
|
from pathlib import Path
|
|
6
6
|
from typing import Annotated
|
|
7
7
|
|
|
8
|
+
import httpx
|
|
8
9
|
import typer
|
|
9
10
|
from rich.console import Console
|
|
10
11
|
|
|
@@ -14,8 +15,9 @@ from shotgun.agents.context_analyzer import (
|
|
|
14
15
|
ContextAnalyzer,
|
|
15
16
|
ContextFormatter,
|
|
16
17
|
)
|
|
17
|
-
from shotgun.agents.
|
|
18
|
+
from shotgun.agents.conversation import ConversationManager
|
|
18
19
|
from shotgun.cli.models import OutputFormat
|
|
20
|
+
from shotgun.llm_proxy import BudgetInfo, LiteLLMProxyClient
|
|
19
21
|
from shotgun.logging_config import get_logger
|
|
20
22
|
|
|
21
23
|
app = typer.Typer(
|
|
@@ -108,4 +110,45 @@ async def analyze_context() -> ContextAnalysisOutput:
|
|
|
108
110
|
markdown = ContextFormatter.format_markdown(analysis)
|
|
109
111
|
json_data = ContextFormatter.format_json(analysis)
|
|
110
112
|
|
|
113
|
+
# Add budget info for Shotgun Account users
|
|
114
|
+
if model_config.is_shotgun_account:
|
|
115
|
+
try:
|
|
116
|
+
logger.debug("Fetching budget info for Shotgun Account")
|
|
117
|
+
client = LiteLLMProxyClient(model_config.api_key)
|
|
118
|
+
budget_info = await client.get_budget_info()
|
|
119
|
+
|
|
120
|
+
# Format budget section for markdown
|
|
121
|
+
budget_markdown = _format_budget_markdown(budget_info)
|
|
122
|
+
markdown = f"{markdown}\n\n{budget_markdown}"
|
|
123
|
+
|
|
124
|
+
# Add budget info to JSON using Pydantic model
|
|
125
|
+
json_data["budget"] = budget_info.model_dump()
|
|
126
|
+
logger.debug("Successfully added budget info to context output")
|
|
127
|
+
|
|
128
|
+
except httpx.HTTPError as e:
|
|
129
|
+
logger.warning(f"Failed to fetch budget info: {e}")
|
|
130
|
+
# Don't fail the entire command if budget fetch fails
|
|
131
|
+
except Exception as e:
|
|
132
|
+
logger.warning(f"Unexpected error fetching budget info: {e}")
|
|
133
|
+
# Don't fail the entire command if budget fetch fails
|
|
134
|
+
|
|
111
135
|
return ContextAnalysisOutput(markdown=markdown, json_data=json_data)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _format_budget_markdown(budget_info: BudgetInfo) -> str:
|
|
139
|
+
"""Format budget information as markdown.
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
budget_info: BudgetInfo instance
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
Formatted markdown string
|
|
146
|
+
"""
|
|
147
|
+
source_label = "Key" if budget_info.source == "key" else "Team"
|
|
148
|
+
|
|
149
|
+
return f"""## Shotgun Account Budget
|
|
150
|
+
|
|
151
|
+
* Max Budget: ${budget_info.max_budget:.2f}
|
|
152
|
+
* Current Spend: ${budget_info.spend:.2f}
|
|
153
|
+
* Remaining: ${budget_info.remaining:.2f} ({100 - budget_info.percentage_used:.1f}%)
|
|
154
|
+
* Budget Source: {source_label}-level"""
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""CLI-specific error handling utilities.
|
|
2
|
+
|
|
3
|
+
This module provides utilities for displaying agent errors in the CLI
|
|
4
|
+
by printing formatted messages to the console.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
|
|
9
|
+
from shotgun.exceptions import ErrorNotPickedUpBySentry
|
|
10
|
+
|
|
11
|
+
console = Console(stderr=True)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def print_agent_error(exception: ErrorNotPickedUpBySentry) -> None:
|
|
15
|
+
"""Print an agent error to the console in yellow.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
exception: The error exception with formatting methods
|
|
19
|
+
"""
|
|
20
|
+
# Get plain text version for CLI
|
|
21
|
+
message = exception.to_plain_text()
|
|
22
|
+
|
|
23
|
+
# Print with yellow styling
|
|
24
|
+
console.print(message, style="yellow")
|
shotgun/cli/export.py
CHANGED
|
@@ -11,7 +11,10 @@ from shotgun.agents.export import (
|
|
|
11
11
|
run_export_agent,
|
|
12
12
|
)
|
|
13
13
|
from shotgun.agents.models import AgentRuntimeOptions
|
|
14
|
+
from shotgun.cli.error_handler import print_agent_error
|
|
15
|
+
from shotgun.exceptions import ErrorNotPickedUpBySentry
|
|
14
16
|
from shotgun.logging_config import get_logger
|
|
17
|
+
from shotgun.posthog_telemetry import track_event
|
|
15
18
|
|
|
16
19
|
app = typer.Typer(
|
|
17
20
|
name="export", help="Export artifacts to various formats with agentic approach"
|
|
@@ -45,37 +48,34 @@ def export(
|
|
|
45
48
|
|
|
46
49
|
logger.info("📤 Export Instruction: %s", instruction)
|
|
47
50
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
"
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
import traceback
|
|
80
|
-
|
|
81
|
-
logger.debug("Full traceback:\n%s", traceback.format_exc())
|
|
51
|
+
# Track export command usage
|
|
52
|
+
track_event(
|
|
53
|
+
"export_command",
|
|
54
|
+
{
|
|
55
|
+
"non_interactive": non_interactive,
|
|
56
|
+
"provider": provider.value if provider else "default",
|
|
57
|
+
},
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
# Create agent dependencies
|
|
61
|
+
agent_runtime_options = AgentRuntimeOptions(interactive_mode=not non_interactive)
|
|
62
|
+
|
|
63
|
+
# Create the export agent with deps and provider
|
|
64
|
+
agent, deps = asyncio.run(create_export_agent(agent_runtime_options, provider))
|
|
65
|
+
|
|
66
|
+
# Start export process with error handling
|
|
67
|
+
logger.info("🎯 Starting export...")
|
|
68
|
+
|
|
69
|
+
async def async_export() -> None:
|
|
70
|
+
try:
|
|
71
|
+
result = await run_export_agent(agent, instruction, deps)
|
|
72
|
+
logger.info("✅ Export Complete!")
|
|
73
|
+
logger.info("📤 Results:")
|
|
74
|
+
logger.info("%s", result.output)
|
|
75
|
+
except ErrorNotPickedUpBySentry as e:
|
|
76
|
+
print_agent_error(e)
|
|
77
|
+
except Exception as e:
|
|
78
|
+
logger.exception("Unexpected error in export command")
|
|
79
|
+
print(f"⚠️ An unexpected error occurred: {str(e)}")
|
|
80
|
+
|
|
81
|
+
asyncio.run(async_export())
|
shotgun/cli/plan.py
CHANGED
|
@@ -8,7 +8,10 @@ import typer
|
|
|
8
8
|
from shotgun.agents.config import ProviderType
|
|
9
9
|
from shotgun.agents.models import AgentRuntimeOptions
|
|
10
10
|
from shotgun.agents.plan import create_plan_agent, run_plan_agent
|
|
11
|
+
from shotgun.cli.error_handler import print_agent_error
|
|
12
|
+
from shotgun.exceptions import ErrorNotPickedUpBySentry
|
|
11
13
|
from shotgun.logging_config import get_logger
|
|
14
|
+
from shotgun.posthog_telemetry import track_event
|
|
12
15
|
|
|
13
16
|
app = typer.Typer(name="plan", help="Generate structured plans", no_args_is_help=True)
|
|
14
17
|
logger = get_logger(__name__)
|
|
@@ -37,37 +40,34 @@ def plan(
|
|
|
37
40
|
|
|
38
41
|
logger.info("📋 Planning Goal: %s", goal)
|
|
39
42
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
"
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
import traceback
|
|
72
|
-
|
|
73
|
-
logger.debug("Full traceback:\n%s", traceback.format_exc())
|
|
43
|
+
# Track plan command usage
|
|
44
|
+
track_event(
|
|
45
|
+
"plan_command",
|
|
46
|
+
{
|
|
47
|
+
"non_interactive": non_interactive,
|
|
48
|
+
"provider": provider.value if provider else "default",
|
|
49
|
+
},
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
# Create agent dependencies
|
|
53
|
+
agent_runtime_options = AgentRuntimeOptions(interactive_mode=not non_interactive)
|
|
54
|
+
|
|
55
|
+
# Create the plan agent with deps and provider
|
|
56
|
+
agent, deps = asyncio.run(create_plan_agent(agent_runtime_options, provider))
|
|
57
|
+
|
|
58
|
+
# Start planning process with error handling
|
|
59
|
+
logger.info("🎯 Starting planning...")
|
|
60
|
+
|
|
61
|
+
async def async_plan() -> None:
|
|
62
|
+
try:
|
|
63
|
+
result = await run_plan_agent(agent, goal, deps)
|
|
64
|
+
logger.info("✅ Planning Complete!")
|
|
65
|
+
logger.info("📋 Results:")
|
|
66
|
+
logger.info("%s", result.output)
|
|
67
|
+
except ErrorNotPickedUpBySentry as e:
|
|
68
|
+
print_agent_error(e)
|
|
69
|
+
except Exception as e:
|
|
70
|
+
logger.exception("Unexpected error in plan command")
|
|
71
|
+
print(f"⚠️ An unexpected error occurred: {str(e)}")
|
|
72
|
+
|
|
73
|
+
asyncio.run(async_plan())
|
shotgun/cli/research.py
CHANGED
|
@@ -11,7 +11,10 @@ from shotgun.agents.research import (
|
|
|
11
11
|
create_research_agent,
|
|
12
12
|
run_research_agent,
|
|
13
13
|
)
|
|
14
|
+
from shotgun.cli.error_handler import print_agent_error
|
|
15
|
+
from shotgun.exceptions import ErrorNotPickedUpBySentry
|
|
14
16
|
from shotgun.logging_config import get_logger
|
|
17
|
+
from shotgun.posthog_telemetry import track_event
|
|
15
18
|
|
|
16
19
|
app = typer.Typer(
|
|
17
20
|
name="research", help="Perform research with agentic loops", no_args_is_help=True
|
|
@@ -59,8 +62,6 @@ async def async_research(
|
|
|
59
62
|
) -> None:
|
|
60
63
|
"""Async wrapper for research process."""
|
|
61
64
|
# Track research command usage
|
|
62
|
-
from shotgun.posthog_telemetry import track_event
|
|
63
|
-
|
|
64
65
|
track_event(
|
|
65
66
|
"research_command",
|
|
66
67
|
{
|
|
@@ -75,11 +76,18 @@ async def async_research(
|
|
|
75
76
|
# Create the research agent with deps and provider
|
|
76
77
|
agent, deps = await create_research_agent(agent_runtime_options, provider)
|
|
77
78
|
|
|
78
|
-
# Start research process
|
|
79
|
+
# Start research process with error handling
|
|
79
80
|
logger.info("🔬 Starting research...")
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
81
|
+
try:
|
|
82
|
+
result = await run_research_agent(agent, query, deps)
|
|
83
|
+
# Display results
|
|
84
|
+
print("✅ Research Complete!")
|
|
85
|
+
print("📋 Findings:")
|
|
86
|
+
print(result.output)
|
|
87
|
+
except ErrorNotPickedUpBySentry as e:
|
|
88
|
+
# All user-actionable errors - display with plain text
|
|
89
|
+
print_agent_error(e)
|
|
90
|
+
except Exception as e:
|
|
91
|
+
# Unexpected errors that weren't wrapped (shouldn't happen)
|
|
92
|
+
logger.exception("Unexpected error in research command")
|
|
93
|
+
print(f"⚠️ An unexpected error occurred: {str(e)}")
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""Backup utility for .shotgun/ directory before pulling specs."""
|
|
2
|
+
|
|
3
|
+
import shutil
|
|
4
|
+
import zipfile
|
|
5
|
+
from datetime import datetime, timezone
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from shotgun.logging_config import get_logger
|
|
9
|
+
|
|
10
|
+
logger = get_logger(__name__)
|
|
11
|
+
|
|
12
|
+
# Backup directory location
|
|
13
|
+
BACKUP_DIR = Path.home() / ".shotgun-sh" / "backups"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
async def create_backup(shotgun_dir: Path) -> str | None:
|
|
17
|
+
"""Create a zip backup of the .shotgun/ directory.
|
|
18
|
+
|
|
19
|
+
Creates a timestamped backup at ~/.shotgun-sh/backups/{YYYYMMDD_HHMMSS}.zip.
|
|
20
|
+
Only creates backup if the directory exists and has content.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
shotgun_dir: Path to the .shotgun/ directory to backup
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
Path to the backup file as string, or None if no backup was created
|
|
27
|
+
(e.g., directory doesn't exist or is empty)
|
|
28
|
+
|
|
29
|
+
Raises:
|
|
30
|
+
Exception: If backup creation fails (caller should handle)
|
|
31
|
+
"""
|
|
32
|
+
# Check if directory exists and has content
|
|
33
|
+
if not shotgun_dir.exists():
|
|
34
|
+
logger.debug("No .shotgun/ directory to backup")
|
|
35
|
+
return None
|
|
36
|
+
|
|
37
|
+
files_to_backup = list(shotgun_dir.rglob("*"))
|
|
38
|
+
if not any(f.is_file() for f in files_to_backup):
|
|
39
|
+
logger.debug(".shotgun/ directory is empty, skipping backup")
|
|
40
|
+
return None
|
|
41
|
+
|
|
42
|
+
# Create backup directory if needed
|
|
43
|
+
BACKUP_DIR.mkdir(parents=True, exist_ok=True)
|
|
44
|
+
|
|
45
|
+
# Generate timestamp-based filename
|
|
46
|
+
timestamp = datetime.now(timezone.utc).strftime("%Y%m%d_%H%M%S")
|
|
47
|
+
backup_path = BACKUP_DIR / f"{timestamp}.zip"
|
|
48
|
+
|
|
49
|
+
logger.info("Creating backup of .shotgun/ at %s", backup_path)
|
|
50
|
+
|
|
51
|
+
# Create zip file
|
|
52
|
+
with zipfile.ZipFile(backup_path, "w", zipfile.ZIP_DEFLATED) as zipf:
|
|
53
|
+
for file_path in files_to_backup:
|
|
54
|
+
if file_path.is_file():
|
|
55
|
+
# Store with path relative to shotgun_dir
|
|
56
|
+
arcname = file_path.relative_to(shotgun_dir)
|
|
57
|
+
zipf.write(file_path, arcname)
|
|
58
|
+
logger.debug("Added to backup: %s", arcname)
|
|
59
|
+
|
|
60
|
+
logger.info("Backup created successfully: %s", backup_path)
|
|
61
|
+
return str(backup_path)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def clear_shotgun_dir(shotgun_dir: Path) -> None:
|
|
65
|
+
"""Clear all contents of the .shotgun/ directory.
|
|
66
|
+
|
|
67
|
+
Removes all files and subdirectories but keeps the .shotgun/ directory itself.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
shotgun_dir: Path to the .shotgun/ directory to clear
|
|
71
|
+
"""
|
|
72
|
+
if not shotgun_dir.exists():
|
|
73
|
+
return
|
|
74
|
+
|
|
75
|
+
for item in shotgun_dir.iterdir():
|
|
76
|
+
if item.is_dir():
|
|
77
|
+
shutil.rmtree(item)
|
|
78
|
+
else:
|
|
79
|
+
item.unlink()
|
|
80
|
+
|
|
81
|
+
logger.debug("Cleared contents of %s", shotgun_dir)
|