shotgun-sh 0.2.11.dev7__py3-none-any.whl → 0.2.23.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/agents/agent_manager.py +25 -11
- shotgun/agents/config/README.md +89 -0
- shotgun/agents/config/__init__.py +10 -1
- shotgun/agents/config/manager.py +287 -32
- shotgun/agents/config/models.py +26 -1
- shotgun/agents/config/provider.py +27 -0
- shotgun/agents/config/streaming_test.py +119 -0
- shotgun/agents/error/__init__.py +11 -0
- shotgun/agents/error/models.py +19 -0
- shotgun/agents/history/token_counting/anthropic.py +8 -0
- shotgun/agents/runner.py +230 -0
- shotgun/build_constants.py +1 -1
- shotgun/cli/context.py +43 -0
- 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/specify.py +20 -19
- shotgun/cli/tasks.py +34 -34
- shotgun/exceptions.py +323 -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 +2 -0
- shotgun/posthog_telemetry.py +18 -25
- shotgun/prompts/agents/partials/common_agent_system_prompt.j2 +3 -2
- shotgun/sdk/codebase.py +14 -3
- shotgun/sentry_telemetry.py +140 -2
- shotgun/settings.py +5 -0
- shotgun/tui/app.py +35 -10
- shotgun/tui/screens/chat/chat_screen.py +192 -91
- shotgun/tui/screens/chat/codebase_index_prompt_screen.py +62 -11
- shotgun/tui/screens/chat_screen/command_providers.py +3 -2
- shotgun/tui/screens/chat_screen/hint_message.py +76 -1
- shotgun/tui/screens/chat_screen/history/chat_history.py +37 -2
- 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 +8 -1
- shotgun/tui/screens/pipx_migration.py +12 -6
- shotgun/tui/screens/provider_config.py +25 -8
- shotgun/tui/screens/shotgun_auth.py +0 -10
- shotgun/tui/screens/welcome.py +32 -0
- shotgun/tui/widgets/widget_coordinator.py +3 -2
- shotgun_sh-0.2.23.dev1.dist-info/METADATA +472 -0
- {shotgun_sh-0.2.11.dev7.dist-info → shotgun_sh-0.2.23.dev1.dist-info}/RECORD +50 -42
- shotgun_sh-0.2.11.dev7.dist-info/METADATA +0 -130
- {shotgun_sh-0.2.11.dev7.dist-info → shotgun_sh-0.2.23.dev1.dist-info}/WHEEL +0 -0
- {shotgun_sh-0.2.11.dev7.dist-info → shotgun_sh-0.2.23.dev1.dist-info}/entry_points.txt +0 -0
- {shotgun_sh-0.2.11.dev7.dist-info → shotgun_sh-0.2.23.dev1.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""Utility for testing streaming capability of OpenAI models."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
|
|
7
|
+
logger = logging.getLogger(__name__)
|
|
8
|
+
|
|
9
|
+
# Maximum number of attempts to test streaming capability
|
|
10
|
+
MAX_STREAMING_TEST_ATTEMPTS = 3
|
|
11
|
+
|
|
12
|
+
# Timeout for each streaming test attempt (in seconds)
|
|
13
|
+
STREAMING_TEST_TIMEOUT = 10.0
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
async def check_streaming_capability(
|
|
17
|
+
api_key: str, model_name: str, max_attempts: int = MAX_STREAMING_TEST_ATTEMPTS
|
|
18
|
+
) -> bool:
|
|
19
|
+
"""Check if the given OpenAI model supports streaming with this API key.
|
|
20
|
+
|
|
21
|
+
Retries multiple times to handle transient network issues. Only returns False
|
|
22
|
+
if streaming definitively fails after all retry attempts.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
api_key: The OpenAI API key to test
|
|
26
|
+
model_name: The model name (e.g., "gpt-5", "gpt-5-mini")
|
|
27
|
+
max_attempts: Maximum number of attempts (default: 3)
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
True if streaming is supported, False if it definitively fails
|
|
31
|
+
"""
|
|
32
|
+
url = "https://api.openai.com/v1/chat/completions"
|
|
33
|
+
headers = {
|
|
34
|
+
"Authorization": f"Bearer {api_key}",
|
|
35
|
+
"Content-Type": "application/json",
|
|
36
|
+
}
|
|
37
|
+
# GPT-5 family uses max_completion_tokens instead of max_tokens
|
|
38
|
+
payload = {
|
|
39
|
+
"model": model_name,
|
|
40
|
+
"messages": [{"role": "user", "content": "test"}],
|
|
41
|
+
"stream": True,
|
|
42
|
+
"max_completion_tokens": 10,
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
last_error = None
|
|
46
|
+
|
|
47
|
+
for attempt in range(1, max_attempts + 1):
|
|
48
|
+
logger.debug(
|
|
49
|
+
f"Streaming test attempt {attempt}/{max_attempts} for {model_name}"
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
try:
|
|
53
|
+
async with httpx.AsyncClient(timeout=STREAMING_TEST_TIMEOUT) as client:
|
|
54
|
+
async with client.stream(
|
|
55
|
+
"POST", url, json=payload, headers=headers
|
|
56
|
+
) as response:
|
|
57
|
+
# Check if we get a successful response
|
|
58
|
+
if response.status_code != 200:
|
|
59
|
+
last_error = f"HTTP {response.status_code}"
|
|
60
|
+
logger.warning(
|
|
61
|
+
f"Streaming test attempt {attempt} failed for {model_name}: {last_error}"
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
# For definitive errors (403 Forbidden, 404 Not Found), don't retry
|
|
65
|
+
if response.status_code in (403, 404):
|
|
66
|
+
logger.info(
|
|
67
|
+
f"Streaming definitively unsupported for {model_name} (HTTP {response.status_code})"
|
|
68
|
+
)
|
|
69
|
+
return False
|
|
70
|
+
|
|
71
|
+
# For other errors, retry
|
|
72
|
+
continue
|
|
73
|
+
|
|
74
|
+
# Try to read at least one chunk from the stream
|
|
75
|
+
try:
|
|
76
|
+
async for _ in response.aiter_bytes():
|
|
77
|
+
# Successfully received streaming data
|
|
78
|
+
logger.info(
|
|
79
|
+
f"Streaming test passed for {model_name} (attempt {attempt})"
|
|
80
|
+
)
|
|
81
|
+
return True
|
|
82
|
+
except Exception as e:
|
|
83
|
+
last_error = str(e)
|
|
84
|
+
logger.warning(
|
|
85
|
+
f"Streaming test attempt {attempt} failed for {model_name} while reading stream: {e}"
|
|
86
|
+
)
|
|
87
|
+
continue
|
|
88
|
+
|
|
89
|
+
except httpx.TimeoutException:
|
|
90
|
+
last_error = "timeout"
|
|
91
|
+
logger.warning(
|
|
92
|
+
f"Streaming test attempt {attempt} timed out for {model_name}"
|
|
93
|
+
)
|
|
94
|
+
continue
|
|
95
|
+
except httpx.HTTPStatusError as e:
|
|
96
|
+
last_error = str(e)
|
|
97
|
+
logger.warning(
|
|
98
|
+
f"Streaming test attempt {attempt} failed for {model_name}: {e}"
|
|
99
|
+
)
|
|
100
|
+
continue
|
|
101
|
+
except Exception as e:
|
|
102
|
+
last_error = str(e)
|
|
103
|
+
logger.warning(
|
|
104
|
+
f"Streaming test attempt {attempt} failed for {model_name} with unexpected error: {e}"
|
|
105
|
+
)
|
|
106
|
+
continue
|
|
107
|
+
|
|
108
|
+
# If we got here without reading any chunks, streaming didn't work
|
|
109
|
+
last_error = "no data received"
|
|
110
|
+
logger.warning(
|
|
111
|
+
f"Streaming test attempt {attempt} failed for {model_name}: no data received"
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
# All attempts exhausted
|
|
115
|
+
logger.error(
|
|
116
|
+
f"Streaming test failed for {model_name} after {max_attempts} attempts. "
|
|
117
|
+
f"Last error: {last_error}. Assuming streaming is NOT supported."
|
|
118
|
+
)
|
|
119
|
+
return False
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Pydantic models for agent error handling."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class AgentErrorContext(BaseModel):
|
|
9
|
+
"""Context information needed to classify and handle agent errors.
|
|
10
|
+
|
|
11
|
+
Attributes:
|
|
12
|
+
exception: The exception that was raised
|
|
13
|
+
is_shotgun_account: Whether the user is using a Shotgun Account
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
17
|
+
|
|
18
|
+
exception: Any = Field(...)
|
|
19
|
+
is_shotgun_account: bool
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""Anthropic token counting using official client."""
|
|
2
2
|
|
|
3
3
|
import logfire
|
|
4
|
+
from anthropic import APIStatusError
|
|
4
5
|
from pydantic_ai.messages import ModelMessage
|
|
5
6
|
|
|
6
7
|
from shotgun.agents.config.models import KeyProvider
|
|
@@ -103,6 +104,13 @@ class AnthropicTokenCounter(TokenCounter):
|
|
|
103
104
|
exception_type=type(e).__name__,
|
|
104
105
|
exception_message=str(e),
|
|
105
106
|
)
|
|
107
|
+
|
|
108
|
+
# Re-raise API errors directly so they can be classified by the runner
|
|
109
|
+
# This allows proper error classification for BYOK users (authentication, rate limits, etc.)
|
|
110
|
+
if isinstance(e, APIStatusError):
|
|
111
|
+
raise
|
|
112
|
+
|
|
113
|
+
# Only wrap library-level errors in RuntimeError
|
|
106
114
|
raise RuntimeError(
|
|
107
115
|
f"Anthropic token counting API failed for {self.model_name}: {type(e).__name__}: {str(e)}"
|
|
108
116
|
) from e
|
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
|
shotgun/build_constants.py
CHANGED
|
@@ -8,7 +8,7 @@ DO NOT EDIT MANUALLY.
|
|
|
8
8
|
SENTRY_DSN = 'https://2818a6d165c64eccc94cfd51ce05d6aa@o4506813296738304.ingest.us.sentry.io/4510045952409600'
|
|
9
9
|
|
|
10
10
|
# PostHog configuration embedded at build time (empty strings if not provided)
|
|
11
|
-
POSTHOG_API_KEY = ''
|
|
11
|
+
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)
|
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
|
|
|
@@ -16,6 +17,7 @@ from shotgun.agents.context_analyzer import (
|
|
|
16
17
|
)
|
|
17
18
|
from shotgun.agents.conversation_manager 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())
|