shotgun-sh 0.1.9__py3-none-any.whl → 0.2.11__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 +761 -52
- shotgun/agents/common.py +80 -75
- shotgun/agents/config/constants.py +21 -10
- shotgun/agents/config/manager.py +322 -97
- shotgun/agents/config/models.py +114 -84
- shotgun/agents/config/provider.py +232 -88
- shotgun/agents/context_analyzer/__init__.py +28 -0
- shotgun/agents/context_analyzer/analyzer.py +471 -0
- shotgun/agents/context_analyzer/constants.py +9 -0
- shotgun/agents/context_analyzer/formatter.py +115 -0
- shotgun/agents/context_analyzer/models.py +212 -0
- shotgun/agents/conversation_history.py +125 -2
- shotgun/agents/conversation_manager.py +57 -19
- shotgun/agents/export.py +6 -7
- shotgun/agents/history/compaction.py +23 -3
- shotgun/agents/history/context_extraction.py +93 -6
- shotgun/agents/history/history_processors.py +179 -11
- shotgun/agents/history/token_counting/__init__.py +31 -0
- shotgun/agents/history/token_counting/anthropic.py +127 -0
- shotgun/agents/history/token_counting/base.py +78 -0
- shotgun/agents/history/token_counting/openai.py +90 -0
- shotgun/agents/history/token_counting/sentencepiece_counter.py +127 -0
- shotgun/agents/history/token_counting/tokenizer_cache.py +92 -0
- shotgun/agents/history/token_counting/utils.py +144 -0
- shotgun/agents/history/token_estimation.py +12 -12
- shotgun/agents/llm.py +62 -0
- shotgun/agents/models.py +59 -4
- shotgun/agents/plan.py +6 -7
- shotgun/agents/research.py +7 -8
- shotgun/agents/specify.py +6 -7
- shotgun/agents/tasks.py +6 -7
- shotgun/agents/tools/__init__.py +0 -2
- shotgun/agents/tools/codebase/codebase_shell.py +6 -0
- shotgun/agents/tools/codebase/directory_lister.py +6 -0
- shotgun/agents/tools/codebase/file_read.py +11 -2
- shotgun/agents/tools/codebase/query_graph.py +6 -0
- shotgun/agents/tools/codebase/retrieve_code.py +6 -0
- shotgun/agents/tools/file_management.py +82 -16
- shotgun/agents/tools/registry.py +217 -0
- shotgun/agents/tools/web_search/__init__.py +55 -16
- shotgun/agents/tools/web_search/anthropic.py +76 -51
- shotgun/agents/tools/web_search/gemini.py +50 -27
- shotgun/agents/tools/web_search/openai.py +26 -17
- shotgun/agents/tools/web_search/utils.py +2 -2
- shotgun/agents/usage_manager.py +164 -0
- shotgun/api_endpoints.py +15 -0
- shotgun/cli/clear.py +53 -0
- shotgun/cli/codebase/commands.py +71 -2
- shotgun/cli/compact.py +186 -0
- shotgun/cli/config.py +41 -67
- shotgun/cli/context.py +111 -0
- shotgun/cli/export.py +1 -1
- shotgun/cli/feedback.py +50 -0
- shotgun/cli/models.py +3 -2
- shotgun/cli/plan.py +1 -1
- shotgun/cli/research.py +1 -1
- shotgun/cli/specify.py +1 -1
- shotgun/cli/tasks.py +1 -1
- shotgun/cli/update.py +18 -5
- shotgun/codebase/core/change_detector.py +5 -3
- shotgun/codebase/core/code_retrieval.py +4 -2
- shotgun/codebase/core/ingestor.py +169 -19
- shotgun/codebase/core/manager.py +177 -13
- shotgun/codebase/core/nl_query.py +1 -1
- shotgun/codebase/models.py +28 -3
- shotgun/codebase/service.py +14 -2
- shotgun/exceptions.py +32 -0
- shotgun/llm_proxy/__init__.py +19 -0
- shotgun/llm_proxy/clients.py +44 -0
- shotgun/llm_proxy/constants.py +15 -0
- shotgun/logging_config.py +18 -27
- shotgun/main.py +91 -4
- shotgun/posthog_telemetry.py +87 -40
- shotgun/prompts/agents/export.j2 +18 -1
- shotgun/prompts/agents/partials/common_agent_system_prompt.j2 +5 -1
- shotgun/prompts/agents/partials/interactive_mode.j2 +24 -7
- shotgun/prompts/agents/plan.j2 +1 -1
- shotgun/prompts/agents/research.j2 +1 -1
- shotgun/prompts/agents/specify.j2 +270 -3
- shotgun/prompts/agents/state/system_state.j2 +4 -0
- shotgun/prompts/agents/tasks.j2 +1 -1
- shotgun/prompts/codebase/partials/cypher_rules.j2 +13 -0
- shotgun/prompts/loader.py +2 -2
- shotgun/prompts/tools/web_search.j2 +14 -0
- shotgun/sdk/codebase.py +60 -2
- shotgun/sentry_telemetry.py +28 -21
- shotgun/settings.py +238 -0
- shotgun/shotgun_web/__init__.py +19 -0
- shotgun/shotgun_web/client.py +138 -0
- shotgun/shotgun_web/constants.py +21 -0
- shotgun/shotgun_web/models.py +47 -0
- shotgun/telemetry.py +24 -36
- shotgun/tui/app.py +275 -23
- shotgun/tui/commands/__init__.py +1 -1
- shotgun/tui/components/context_indicator.py +179 -0
- shotgun/tui/components/mode_indicator.py +70 -0
- shotgun/tui/components/status_bar.py +48 -0
- shotgun/tui/components/vertical_tail.py +6 -0
- shotgun/tui/containers.py +91 -0
- shotgun/tui/dependencies.py +39 -0
- shotgun/tui/filtered_codebase_service.py +46 -0
- shotgun/tui/protocols.py +45 -0
- shotgun/tui/screens/chat/__init__.py +5 -0
- shotgun/tui/screens/chat/chat.tcss +54 -0
- shotgun/tui/screens/chat/chat_screen.py +1234 -0
- shotgun/tui/screens/chat/codebase_index_prompt_screen.py +64 -0
- shotgun/tui/screens/chat/codebase_index_selection.py +12 -0
- shotgun/tui/screens/chat/help_text.py +40 -0
- shotgun/tui/screens/chat/prompt_history.py +48 -0
- shotgun/tui/screens/chat.tcss +11 -0
- shotgun/tui/screens/chat_screen/command_providers.py +226 -11
- shotgun/tui/screens/chat_screen/history/__init__.py +22 -0
- shotgun/tui/screens/chat_screen/history/agent_response.py +66 -0
- shotgun/tui/screens/chat_screen/history/chat_history.py +116 -0
- shotgun/tui/screens/chat_screen/history/formatters.py +115 -0
- shotgun/tui/screens/chat_screen/history/partial_response.py +43 -0
- shotgun/tui/screens/chat_screen/history/user_question.py +42 -0
- shotgun/tui/screens/confirmation_dialog.py +151 -0
- shotgun/tui/screens/feedback.py +193 -0
- shotgun/tui/screens/github_issue.py +102 -0
- shotgun/tui/screens/model_picker.py +352 -0
- shotgun/tui/screens/onboarding.py +431 -0
- shotgun/tui/screens/pipx_migration.py +153 -0
- shotgun/tui/screens/provider_config.py +156 -39
- shotgun/tui/screens/shotgun_auth.py +295 -0
- shotgun/tui/screens/welcome.py +198 -0
- shotgun/tui/services/__init__.py +5 -0
- shotgun/tui/services/conversation_service.py +184 -0
- shotgun/tui/state/__init__.py +7 -0
- shotgun/tui/state/processing_state.py +185 -0
- shotgun/tui/utils/mode_progress.py +14 -7
- shotgun/tui/widgets/__init__.py +5 -0
- shotgun/tui/widgets/widget_coordinator.py +262 -0
- shotgun/utils/datetime_utils.py +77 -0
- shotgun/utils/env_utils.py +13 -0
- shotgun/utils/file_system_utils.py +22 -2
- shotgun/utils/marketing.py +110 -0
- shotgun/utils/source_detection.py +16 -0
- shotgun/utils/update_checker.py +73 -21
- shotgun_sh-0.2.11.dist-info/METADATA +130 -0
- shotgun_sh-0.2.11.dist-info/RECORD +194 -0
- {shotgun_sh-0.1.9.dist-info → shotgun_sh-0.2.11.dist-info}/entry_points.txt +1 -0
- {shotgun_sh-0.1.9.dist-info → shotgun_sh-0.2.11.dist-info}/licenses/LICENSE +1 -1
- shotgun/agents/history/token_counting.py +0 -429
- shotgun/agents/tools/user_interaction.py +0 -37
- shotgun/tui/screens/chat.py +0 -818
- shotgun/tui/screens/chat_screen/history.py +0 -222
- shotgun_sh-0.1.9.dist-info/METADATA +0 -466
- shotgun_sh-0.1.9.dist-info/RECORD +0 -131
- {shotgun_sh-0.1.9.dist-info → shotgun_sh-0.2.11.dist-info}/WHEEL +0 -0
shotgun/sentry_telemetry.py
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
"""Sentry observability setup for Shotgun."""
|
|
2
2
|
|
|
3
|
-
import
|
|
3
|
+
from typing import Any
|
|
4
4
|
|
|
5
|
+
from shotgun import __version__
|
|
5
6
|
from shotgun.logging_config import get_early_logger
|
|
7
|
+
from shotgun.settings import settings
|
|
6
8
|
|
|
7
9
|
# Use early logger to prevent automatic StreamHandler creation
|
|
8
10
|
logger = get_early_logger(__name__)
|
|
@@ -22,35 +24,37 @@ def setup_sentry_observability() -> bool:
|
|
|
22
24
|
logger.debug("Sentry is already initialized, skipping")
|
|
23
25
|
return True
|
|
24
26
|
|
|
25
|
-
#
|
|
26
|
-
dsn =
|
|
27
|
-
try:
|
|
28
|
-
from shotgun import build_constants
|
|
29
|
-
|
|
30
|
-
dsn = build_constants.SENTRY_DSN
|
|
31
|
-
logger.debug("Using Sentry DSN from build constants")
|
|
32
|
-
except ImportError:
|
|
33
|
-
# Fallback to environment variable (development)
|
|
34
|
-
dsn = os.getenv("SENTRY_DSN", "")
|
|
35
|
-
if dsn:
|
|
36
|
-
logger.debug("Using Sentry DSN from environment variable")
|
|
27
|
+
# Get DSN from settings (handles build constants + env vars automatically)
|
|
28
|
+
dsn = settings.telemetry.sentry_dsn
|
|
37
29
|
|
|
38
30
|
if not dsn:
|
|
39
31
|
logger.debug("No Sentry DSN configured, skipping Sentry initialization")
|
|
40
32
|
return False
|
|
41
33
|
|
|
42
|
-
logger.debug("
|
|
43
|
-
|
|
44
|
-
# Get version for release tracking
|
|
45
|
-
from shotgun import __version__
|
|
34
|
+
logger.debug("Using Sentry DSN from settings, proceeding with setup")
|
|
46
35
|
|
|
47
36
|
# Determine environment based on version
|
|
48
|
-
# Dev versions contain "dev", "rc", "alpha",
|
|
37
|
+
# Dev versions contain "dev", "rc", "alpha", "beta"
|
|
49
38
|
if any(marker in __version__ for marker in ["dev", "rc", "alpha", "beta"]):
|
|
50
39
|
environment = "development"
|
|
51
40
|
else:
|
|
52
41
|
environment = "production"
|
|
53
42
|
|
|
43
|
+
def before_send(event: Any, hint: dict[str, Any]) -> Any:
|
|
44
|
+
"""Filter out user-actionable errors from Sentry.
|
|
45
|
+
|
|
46
|
+
User-actionable errors (like context size limits) are expected conditions
|
|
47
|
+
that users need to resolve, not bugs that need tracking.
|
|
48
|
+
"""
|
|
49
|
+
if "exc_info" in hint:
|
|
50
|
+
exc_type, exc_value, tb = hint["exc_info"]
|
|
51
|
+
from shotgun.exceptions import ErrorNotPickedUpBySentry
|
|
52
|
+
|
|
53
|
+
if isinstance(exc_value, ErrorNotPickedUpBySentry):
|
|
54
|
+
# Don't send to Sentry - this is user-actionable, not a bug
|
|
55
|
+
return None
|
|
56
|
+
return event
|
|
57
|
+
|
|
54
58
|
# Initialize Sentry
|
|
55
59
|
sentry_sdk.init(
|
|
56
60
|
dsn=dsn,
|
|
@@ -59,15 +63,18 @@ def setup_sentry_observability() -> bool:
|
|
|
59
63
|
send_default_pii=False, # Privacy-first: never send PII
|
|
60
64
|
traces_sample_rate=0.1 if environment == "production" else 1.0,
|
|
61
65
|
profiles_sample_rate=0.1 if environment == "production" else 1.0,
|
|
66
|
+
before_send=before_send,
|
|
62
67
|
)
|
|
63
68
|
|
|
64
|
-
# Set user context with anonymous
|
|
69
|
+
# Set user context with anonymous shotgun instance ID from config
|
|
65
70
|
try:
|
|
71
|
+
import asyncio
|
|
72
|
+
|
|
66
73
|
from shotgun.agents.config import get_config_manager
|
|
67
74
|
|
|
68
75
|
config_manager = get_config_manager()
|
|
69
|
-
|
|
70
|
-
sentry_sdk.set_user({"id":
|
|
76
|
+
shotgun_instance_id = asyncio.run(config_manager.get_shotgun_instance_id())
|
|
77
|
+
sentry_sdk.set_user({"id": shotgun_instance_id})
|
|
71
78
|
logger.debug("Sentry user context set with anonymous ID")
|
|
72
79
|
except Exception as e:
|
|
73
80
|
logger.warning("Failed to set Sentry user context: %s", e)
|
shotgun/settings.py
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
"""Centralized application settings using Pydantic Settings.
|
|
2
|
+
|
|
3
|
+
All environment variables use the SHOTGUN_ prefix to avoid conflicts with other tools.
|
|
4
|
+
Settings are loaded with the following priority:
|
|
5
|
+
1. Environment variables (highest priority)
|
|
6
|
+
2. Build constants (embedded at build time)
|
|
7
|
+
3. Default values (lowest priority)
|
|
8
|
+
|
|
9
|
+
Example usage:
|
|
10
|
+
from shotgun.settings import settings
|
|
11
|
+
|
|
12
|
+
# Access telemetry settings
|
|
13
|
+
if settings.telemetry.sentry_dsn:
|
|
14
|
+
sentry_sdk.init(dsn=settings.telemetry.sentry_dsn)
|
|
15
|
+
|
|
16
|
+
# Access logging settings
|
|
17
|
+
logger.setLevel(settings.logging.log_level)
|
|
18
|
+
|
|
19
|
+
# Access API settings
|
|
20
|
+
response = httpx.get(settings.api.web_base_url)
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from typing import Any
|
|
24
|
+
|
|
25
|
+
from pydantic import Field, field_validator
|
|
26
|
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _get_build_constant(name: str, default: Any = None) -> Any:
|
|
30
|
+
"""Get a value from build_constants.py, falling back to default.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
name: The constant name to retrieve (e.g., "SENTRY_DSN")
|
|
34
|
+
default: Default value if constant not found
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
The constant value, or default if not found/import fails
|
|
38
|
+
"""
|
|
39
|
+
try:
|
|
40
|
+
from shotgun import build_constants
|
|
41
|
+
|
|
42
|
+
return getattr(build_constants, name, default)
|
|
43
|
+
except ImportError:
|
|
44
|
+
return default
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class TelemetrySettings(BaseSettings):
|
|
48
|
+
"""Telemetry and observability settings.
|
|
49
|
+
|
|
50
|
+
These settings control error tracking (Sentry), analytics (PostHog),
|
|
51
|
+
and observability (Logfire) integrations.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
sentry_dsn: str = Field(
|
|
55
|
+
default_factory=lambda: _get_build_constant("SENTRY_DSN", ""),
|
|
56
|
+
description="Sentry DSN for error tracking",
|
|
57
|
+
)
|
|
58
|
+
posthog_api_key: str = Field(
|
|
59
|
+
default_factory=lambda: _get_build_constant("POSTHOG_API_KEY", ""),
|
|
60
|
+
description="PostHog API key for analytics",
|
|
61
|
+
)
|
|
62
|
+
posthog_project_id: str = Field(
|
|
63
|
+
default_factory=lambda: _get_build_constant("POSTHOG_PROJECT_ID", ""),
|
|
64
|
+
description="PostHog project ID",
|
|
65
|
+
)
|
|
66
|
+
logfire_enabled: bool = Field(
|
|
67
|
+
default_factory=lambda: _get_build_constant("LOGFIRE_ENABLED", False),
|
|
68
|
+
description="Enable Logfire observability (dev builds only)",
|
|
69
|
+
)
|
|
70
|
+
logfire_token: str = Field(
|
|
71
|
+
default_factory=lambda: _get_build_constant("LOGFIRE_TOKEN", ""),
|
|
72
|
+
description="Logfire authentication token",
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
model_config = SettingsConfigDict(
|
|
76
|
+
env_prefix="SHOTGUN_",
|
|
77
|
+
env_file=".env",
|
|
78
|
+
env_file_encoding="utf-8",
|
|
79
|
+
extra="ignore",
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
@field_validator("logfire_enabled", mode="before")
|
|
83
|
+
@classmethod
|
|
84
|
+
def parse_bool(cls, v: Any) -> bool:
|
|
85
|
+
"""Parse boolean values from strings (matches is_truthy behavior)."""
|
|
86
|
+
if isinstance(v, bool):
|
|
87
|
+
return v
|
|
88
|
+
if isinstance(v, str):
|
|
89
|
+
return v.lower() in ("true", "1", "yes")
|
|
90
|
+
return bool(v)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class LoggingSettings(BaseSettings):
|
|
94
|
+
"""Logging configuration settings.
|
|
95
|
+
|
|
96
|
+
Controls log level, console output, and file logging behavior.
|
|
97
|
+
"""
|
|
98
|
+
|
|
99
|
+
log_level: str = Field(
|
|
100
|
+
default="INFO",
|
|
101
|
+
description="Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL)",
|
|
102
|
+
)
|
|
103
|
+
logging_to_console: bool = Field(
|
|
104
|
+
default=False,
|
|
105
|
+
description="Enable console logging output",
|
|
106
|
+
)
|
|
107
|
+
logging_to_file: bool = Field(
|
|
108
|
+
default=True,
|
|
109
|
+
description="Enable file logging output",
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
model_config = SettingsConfigDict(
|
|
113
|
+
env_prefix="SHOTGUN_",
|
|
114
|
+
env_file=".env",
|
|
115
|
+
env_file_encoding="utf-8",
|
|
116
|
+
extra="ignore",
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
@field_validator("log_level")
|
|
120
|
+
@classmethod
|
|
121
|
+
def validate_log_level(cls, v: str) -> str:
|
|
122
|
+
"""Validate log level is one of the allowed values."""
|
|
123
|
+
v = v.upper()
|
|
124
|
+
valid_levels = ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
|
|
125
|
+
if v not in valid_levels:
|
|
126
|
+
return "INFO" # Default to INFO if invalid
|
|
127
|
+
return v
|
|
128
|
+
|
|
129
|
+
@field_validator("logging_to_console", "logging_to_file", mode="before")
|
|
130
|
+
@classmethod
|
|
131
|
+
def parse_bool(cls, v: Any) -> bool:
|
|
132
|
+
"""Parse boolean values from strings (matches is_truthy behavior)."""
|
|
133
|
+
if isinstance(v, bool):
|
|
134
|
+
return v
|
|
135
|
+
if isinstance(v, str):
|
|
136
|
+
return v.lower() in ("true", "1", "yes")
|
|
137
|
+
return bool(v)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class ApiSettings(BaseSettings):
|
|
141
|
+
"""API endpoint settings.
|
|
142
|
+
|
|
143
|
+
Configuration for Shotgun backend services.
|
|
144
|
+
"""
|
|
145
|
+
|
|
146
|
+
web_base_url: str = Field(
|
|
147
|
+
default="https://api-219702594231.us-east4.run.app",
|
|
148
|
+
description="Shotgun Web API base URL (authentication/subscription)",
|
|
149
|
+
)
|
|
150
|
+
account_llm_base_url: str = Field(
|
|
151
|
+
default="https://litellm-219702594231.us-east4.run.app",
|
|
152
|
+
description="Shotgun's LiteLLM proxy base URL (AI model requests)",
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
model_config = SettingsConfigDict(
|
|
156
|
+
env_prefix="SHOTGUN_",
|
|
157
|
+
env_file=".env",
|
|
158
|
+
env_file_encoding="utf-8",
|
|
159
|
+
extra="ignore",
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
class DevelopmentSettings(BaseSettings):
|
|
164
|
+
"""Development and testing settings.
|
|
165
|
+
|
|
166
|
+
These settings are primarily used for testing and development purposes.
|
|
167
|
+
"""
|
|
168
|
+
|
|
169
|
+
home: str | None = Field(
|
|
170
|
+
default=None,
|
|
171
|
+
description="Override Shotgun home directory (for testing)",
|
|
172
|
+
)
|
|
173
|
+
pipx_simulate: bool = Field(
|
|
174
|
+
default=False,
|
|
175
|
+
description="Simulate pipx installation (for testing)",
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
model_config = SettingsConfigDict(
|
|
179
|
+
env_prefix="SHOTGUN_",
|
|
180
|
+
env_file=".env",
|
|
181
|
+
env_file_encoding="utf-8",
|
|
182
|
+
extra="ignore",
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
@field_validator("pipx_simulate", mode="before")
|
|
186
|
+
@classmethod
|
|
187
|
+
def parse_bool(cls, v: Any) -> bool:
|
|
188
|
+
"""Parse boolean values from strings (matches is_truthy behavior)."""
|
|
189
|
+
if isinstance(v, bool):
|
|
190
|
+
return v
|
|
191
|
+
if isinstance(v, str):
|
|
192
|
+
return v.lower() in ("true", "1", "yes")
|
|
193
|
+
return bool(v)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
class Settings(BaseSettings):
|
|
197
|
+
"""Main application settings with SHOTGUN_ prefix.
|
|
198
|
+
|
|
199
|
+
This is the main settings class that composes all other settings groups.
|
|
200
|
+
Access settings via the global `settings` singleton instance.
|
|
201
|
+
|
|
202
|
+
Example:
|
|
203
|
+
from shotgun.settings import settings
|
|
204
|
+
|
|
205
|
+
# Telemetry settings
|
|
206
|
+
settings.telemetry.sentry_dsn
|
|
207
|
+
settings.telemetry.posthog_api_key
|
|
208
|
+
settings.telemetry.logfire_enabled
|
|
209
|
+
|
|
210
|
+
# Logging settings
|
|
211
|
+
settings.logging.log_level
|
|
212
|
+
settings.logging.logging_to_console
|
|
213
|
+
|
|
214
|
+
# API settings
|
|
215
|
+
settings.api.web_base_url
|
|
216
|
+
settings.api.account_llm_base_url
|
|
217
|
+
|
|
218
|
+
# Development settings
|
|
219
|
+
settings.dev.home
|
|
220
|
+
settings.dev.pipx_simulate
|
|
221
|
+
"""
|
|
222
|
+
|
|
223
|
+
telemetry: TelemetrySettings = Field(default_factory=TelemetrySettings)
|
|
224
|
+
logging: LoggingSettings = Field(default_factory=LoggingSettings)
|
|
225
|
+
api: ApiSettings = Field(default_factory=ApiSettings)
|
|
226
|
+
dev: DevelopmentSettings = Field(default_factory=DevelopmentSettings)
|
|
227
|
+
|
|
228
|
+
model_config = SettingsConfigDict(
|
|
229
|
+
env_prefix="SHOTGUN_",
|
|
230
|
+
env_file=".env",
|
|
231
|
+
env_file_encoding="utf-8",
|
|
232
|
+
extra="ignore",
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
# Global settings singleton
|
|
237
|
+
# Import this in your modules: from shotgun.settings import settings
|
|
238
|
+
settings = Settings()
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Shotgun Web API client for subscription and authentication."""
|
|
2
|
+
|
|
3
|
+
from .client import ShotgunWebClient, check_token_status, create_unification_token
|
|
4
|
+
from .models import (
|
|
5
|
+
TokenCreateRequest,
|
|
6
|
+
TokenCreateResponse,
|
|
7
|
+
TokenStatus,
|
|
8
|
+
TokenStatusResponse,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"ShotgunWebClient",
|
|
13
|
+
"create_unification_token",
|
|
14
|
+
"check_token_status",
|
|
15
|
+
"TokenCreateRequest",
|
|
16
|
+
"TokenCreateResponse",
|
|
17
|
+
"TokenStatus",
|
|
18
|
+
"TokenStatusResponse",
|
|
19
|
+
]
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"""HTTP client for Shotgun Web API."""
|
|
2
|
+
|
|
3
|
+
import httpx
|
|
4
|
+
|
|
5
|
+
from shotgun.logging_config import get_logger
|
|
6
|
+
|
|
7
|
+
from .constants import (
|
|
8
|
+
SHOTGUN_WEB_BASE_URL,
|
|
9
|
+
UNIFICATION_TOKEN_CREATE_PATH,
|
|
10
|
+
UNIFICATION_TOKEN_STATUS_PATH,
|
|
11
|
+
)
|
|
12
|
+
from .models import (
|
|
13
|
+
TokenCreateRequest,
|
|
14
|
+
TokenCreateResponse,
|
|
15
|
+
TokenStatusResponse,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
logger = get_logger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ShotgunWebClient:
|
|
22
|
+
"""HTTP client for Shotgun Web API."""
|
|
23
|
+
|
|
24
|
+
def __init__(self, base_url: str | None = None, timeout: float = 10.0):
|
|
25
|
+
"""Initialize Shotgun Web client.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
base_url: Base URL for Shotgun Web API. If None, uses SHOTGUN_WEB_BASE_URL
|
|
29
|
+
timeout: Request timeout in seconds
|
|
30
|
+
"""
|
|
31
|
+
self.base_url = base_url or SHOTGUN_WEB_BASE_URL
|
|
32
|
+
self.timeout = timeout
|
|
33
|
+
|
|
34
|
+
def create_unification_token(self, shotgun_instance_id: str) -> TokenCreateResponse:
|
|
35
|
+
"""Create a unification token for CLI authentication.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
shotgun_instance_id: UUID for this shotgun instance
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
Token creation response with token and auth URL
|
|
42
|
+
|
|
43
|
+
Raises:
|
|
44
|
+
httpx.HTTPError: If request fails
|
|
45
|
+
"""
|
|
46
|
+
url = f"{self.base_url}{UNIFICATION_TOKEN_CREATE_PATH}"
|
|
47
|
+
request_data = TokenCreateRequest(shotgun_instance_id=shotgun_instance_id)
|
|
48
|
+
|
|
49
|
+
logger.debug("Creating unification token for instance %s", shotgun_instance_id)
|
|
50
|
+
|
|
51
|
+
try:
|
|
52
|
+
response = httpx.post(
|
|
53
|
+
url,
|
|
54
|
+
json=request_data.model_dump(),
|
|
55
|
+
timeout=self.timeout,
|
|
56
|
+
)
|
|
57
|
+
response.raise_for_status()
|
|
58
|
+
|
|
59
|
+
data = response.json()
|
|
60
|
+
result = TokenCreateResponse.model_validate(data)
|
|
61
|
+
|
|
62
|
+
logger.info(
|
|
63
|
+
"Successfully created unification token, expires in %d seconds",
|
|
64
|
+
result.expires_in_seconds,
|
|
65
|
+
)
|
|
66
|
+
return result
|
|
67
|
+
|
|
68
|
+
except httpx.HTTPError as e:
|
|
69
|
+
logger.error("Failed to create unification token: %s", e)
|
|
70
|
+
raise
|
|
71
|
+
|
|
72
|
+
def check_token_status(self, token: str) -> TokenStatusResponse:
|
|
73
|
+
"""Check token status and get keys if completed.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
token: Unification token to check
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
Token status response with status and keys (if completed)
|
|
80
|
+
|
|
81
|
+
Raises:
|
|
82
|
+
httpx.HTTPStatusError: If token not found (404) or expired (410)
|
|
83
|
+
httpx.HTTPError: For other request failures
|
|
84
|
+
"""
|
|
85
|
+
url = f"{self.base_url}{UNIFICATION_TOKEN_STATUS_PATH.format(token=token)}"
|
|
86
|
+
|
|
87
|
+
logger.debug("Checking status for token %s...", token[:8])
|
|
88
|
+
|
|
89
|
+
try:
|
|
90
|
+
response = httpx.get(url, timeout=self.timeout)
|
|
91
|
+
response.raise_for_status()
|
|
92
|
+
|
|
93
|
+
data = response.json()
|
|
94
|
+
result = TokenStatusResponse.model_validate(data)
|
|
95
|
+
|
|
96
|
+
logger.debug("Token status: %s", result.status)
|
|
97
|
+
return result
|
|
98
|
+
|
|
99
|
+
except httpx.HTTPStatusError as e:
|
|
100
|
+
if e.response.status_code == 404:
|
|
101
|
+
logger.error("Token not found: %s", token[:8])
|
|
102
|
+
elif e.response.status_code == 410:
|
|
103
|
+
logger.error("Token expired: %s", token[:8])
|
|
104
|
+
raise
|
|
105
|
+
except httpx.HTTPError as e:
|
|
106
|
+
logger.error("Failed to check token status: %s", e)
|
|
107
|
+
raise
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
# Convenience functions for standalone use
|
|
111
|
+
def create_unification_token(shotgun_instance_id: str) -> TokenCreateResponse:
|
|
112
|
+
"""Create a unification token.
|
|
113
|
+
|
|
114
|
+
Convenience function that creates a client and calls create_unification_token.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
shotgun_instance_id: UUID for this shotgun instance
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
Token creation response
|
|
121
|
+
"""
|
|
122
|
+
client = ShotgunWebClient()
|
|
123
|
+
return client.create_unification_token(shotgun_instance_id)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def check_token_status(token: str) -> TokenStatusResponse:
|
|
127
|
+
"""Check token status.
|
|
128
|
+
|
|
129
|
+
Convenience function that creates a client and calls check_token_status.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
token: Unification token to check
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
Token status response
|
|
136
|
+
"""
|
|
137
|
+
client = ShotgunWebClient()
|
|
138
|
+
return client.check_token_status(token)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Constants for Shotgun Web API."""
|
|
2
|
+
|
|
3
|
+
# Import from centralized API endpoints module
|
|
4
|
+
from shotgun.api_endpoints import SHOTGUN_WEB_BASE_URL
|
|
5
|
+
|
|
6
|
+
# API endpoints
|
|
7
|
+
UNIFICATION_TOKEN_CREATE_PATH = "/api/unification/token/create" # noqa: S105
|
|
8
|
+
UNIFICATION_TOKEN_STATUS_PATH = "/api/unification/token/{token}/status" # noqa: S105
|
|
9
|
+
|
|
10
|
+
# Polling configuration
|
|
11
|
+
DEFAULT_POLL_INTERVAL_SECONDS = 3
|
|
12
|
+
DEFAULT_TOKEN_TIMEOUT_SECONDS = 1800 # 30 minutes
|
|
13
|
+
|
|
14
|
+
# Re-export for backward compatibility
|
|
15
|
+
__all__ = [
|
|
16
|
+
"SHOTGUN_WEB_BASE_URL",
|
|
17
|
+
"UNIFICATION_TOKEN_CREATE_PATH",
|
|
18
|
+
"UNIFICATION_TOKEN_STATUS_PATH",
|
|
19
|
+
"DEFAULT_POLL_INTERVAL_SECONDS",
|
|
20
|
+
"DEFAULT_TOKEN_TIMEOUT_SECONDS",
|
|
21
|
+
]
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Pydantic models for Shotgun Web API."""
|
|
2
|
+
|
|
3
|
+
from enum import StrEnum
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, Field
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class TokenStatus(StrEnum):
|
|
9
|
+
"""Token status enum matching API specification."""
|
|
10
|
+
|
|
11
|
+
PENDING = "pending"
|
|
12
|
+
COMPLETED = "completed"
|
|
13
|
+
AWAITING_PAYMENT = "awaiting_payment"
|
|
14
|
+
EXPIRED = "expired"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class TokenCreateRequest(BaseModel):
|
|
18
|
+
"""Request model for creating a unification token."""
|
|
19
|
+
|
|
20
|
+
shotgun_instance_id: str = Field(
|
|
21
|
+
description="CLI-provided UUID for shotgun instance"
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class TokenCreateResponse(BaseModel):
|
|
26
|
+
"""Response model for token creation."""
|
|
27
|
+
|
|
28
|
+
token: str = Field(description="Secure authentication token")
|
|
29
|
+
auth_url: str = Field(description="Web authentication URL for user to complete")
|
|
30
|
+
expires_in_seconds: int = Field(description="Token expiration time in seconds")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class TokenStatusResponse(BaseModel):
|
|
34
|
+
"""Response model for token status check."""
|
|
35
|
+
|
|
36
|
+
status: TokenStatus = Field(description="Current token status")
|
|
37
|
+
supabase_key: str | None = Field(
|
|
38
|
+
default=None,
|
|
39
|
+
description="Supabase user JWT (only returned when status=completed)",
|
|
40
|
+
)
|
|
41
|
+
litellm_key: str | None = Field(
|
|
42
|
+
default=None,
|
|
43
|
+
description="LiteLLM virtual key (only returned when status=completed)",
|
|
44
|
+
)
|
|
45
|
+
message: str | None = Field(
|
|
46
|
+
default=None, description="Human-readable status message"
|
|
47
|
+
)
|
shotgun/telemetry.py
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
"""Observability setup for Logfire."""
|
|
2
2
|
|
|
3
|
-
import os
|
|
4
|
-
|
|
5
3
|
from shotgun.logging_config import get_early_logger
|
|
6
|
-
from shotgun.
|
|
4
|
+
from shotgun.settings import settings
|
|
7
5
|
|
|
8
6
|
# Use early logger to prevent automatic StreamHandler creation
|
|
9
7
|
logger = get_early_logger(__name__)
|
|
@@ -15,36 +13,13 @@ def setup_logfire_observability() -> bool:
|
|
|
15
13
|
Returns:
|
|
16
14
|
True if Logfire was successfully set up, False otherwise
|
|
17
15
|
"""
|
|
18
|
-
#
|
|
19
|
-
logfire_enabled =
|
|
20
|
-
logfire_token =
|
|
21
|
-
|
|
22
|
-
try:
|
|
23
|
-
from shotgun.build_constants import LOGFIRE_ENABLED, LOGFIRE_TOKEN
|
|
24
|
-
|
|
25
|
-
# Use build constants if they're not empty
|
|
26
|
-
if LOGFIRE_ENABLED:
|
|
27
|
-
logfire_enabled = LOGFIRE_ENABLED
|
|
28
|
-
if LOGFIRE_TOKEN:
|
|
29
|
-
logfire_token = LOGFIRE_TOKEN
|
|
30
|
-
except ImportError:
|
|
31
|
-
# No build constants available
|
|
32
|
-
pass
|
|
33
|
-
|
|
34
|
-
# Fall back to environment variables if not set from build constants
|
|
35
|
-
if not logfire_enabled:
|
|
36
|
-
logfire_enabled = os.getenv("LOGFIRE_ENABLED", "false")
|
|
37
|
-
if not logfire_token:
|
|
38
|
-
logfire_token = os.getenv("LOGFIRE_TOKEN")
|
|
39
|
-
|
|
40
|
-
# Allow environment variable to override and disable Logfire
|
|
41
|
-
env_override = os.getenv("LOGFIRE_ENABLED")
|
|
42
|
-
if env_override and is_falsy(env_override):
|
|
43
|
-
logfire_enabled = env_override
|
|
16
|
+
# Get Logfire configuration from settings (handles build constants + env vars)
|
|
17
|
+
logfire_enabled = settings.telemetry.logfire_enabled
|
|
18
|
+
logfire_token = settings.telemetry.logfire_token
|
|
44
19
|
|
|
45
20
|
# Check if Logfire observability is enabled
|
|
46
|
-
if not
|
|
47
|
-
logger.debug("Logfire observability disabled
|
|
21
|
+
if not logfire_enabled:
|
|
22
|
+
logger.debug("Logfire observability disabled")
|
|
48
23
|
return False
|
|
49
24
|
|
|
50
25
|
try:
|
|
@@ -52,7 +27,7 @@ def setup_logfire_observability() -> bool:
|
|
|
52
27
|
|
|
53
28
|
# Check for Logfire token
|
|
54
29
|
if not logfire_token:
|
|
55
|
-
logger.warning("
|
|
30
|
+
logger.warning("Logfire token not set, Logfire observability disabled")
|
|
56
31
|
return False
|
|
57
32
|
|
|
58
33
|
# Configure Logfire
|
|
@@ -65,19 +40,32 @@ def setup_logfire_observability() -> bool:
|
|
|
65
40
|
# Instrument Pydantic AI for better observability
|
|
66
41
|
logfire.instrument_pydantic_ai()
|
|
67
42
|
|
|
43
|
+
# Add LogfireLoggingHandler to root logger so logfire logs also go to file
|
|
44
|
+
import logging
|
|
45
|
+
|
|
46
|
+
root_logger = logging.getLogger()
|
|
47
|
+
logfire_handler = logfire.LogfireLoggingHandler()
|
|
48
|
+
root_logger.addHandler(logfire_handler)
|
|
49
|
+
logger.debug("Added LogfireLoggingHandler to root logger for file integration")
|
|
50
|
+
|
|
68
51
|
# Set user context using baggage for all logs and spans
|
|
69
52
|
try:
|
|
53
|
+
import asyncio
|
|
54
|
+
|
|
70
55
|
from opentelemetry import baggage, context
|
|
71
56
|
|
|
72
57
|
from shotgun.agents.config import get_config_manager
|
|
73
58
|
|
|
74
59
|
config_manager = get_config_manager()
|
|
75
|
-
|
|
60
|
+
shotgun_instance_id = asyncio.run(config_manager.get_shotgun_instance_id())
|
|
76
61
|
|
|
77
|
-
# Set
|
|
78
|
-
ctx = baggage.set_baggage("
|
|
62
|
+
# Set shotgun_instance_id as baggage in global context - this will be included in all logs/spans
|
|
63
|
+
ctx = baggage.set_baggage("shotgun_instance_id", shotgun_instance_id)
|
|
79
64
|
context.attach(ctx)
|
|
80
|
-
logger.debug(
|
|
65
|
+
logger.debug(
|
|
66
|
+
"Logfire user context set with shotgun_instance_id: %s",
|
|
67
|
+
shotgun_instance_id,
|
|
68
|
+
)
|
|
81
69
|
except Exception as e:
|
|
82
70
|
logger.warning("Failed to set Logfire user context: %s", e)
|
|
83
71
|
|