shotgun-sh 0.2.8.dev2__py3-none-any.whl → 0.2.17__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 +354 -46
- shotgun/agents/common.py +14 -8
- shotgun/agents/config/constants.py +0 -6
- shotgun/agents/config/manager.py +66 -35
- shotgun/agents/config/models.py +41 -1
- shotgun/agents/config/provider.py +33 -5
- 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 +2 -0
- shotgun/agents/conversation_manager.py +35 -19
- shotgun/agents/export.py +2 -2
- shotgun/agents/history/compaction.py +9 -4
- shotgun/agents/history/history_processors.py +113 -5
- shotgun/agents/history/token_counting/anthropic.py +17 -1
- shotgun/agents/history/token_counting/base.py +14 -3
- shotgun/agents/history/token_counting/openai.py +11 -1
- shotgun/agents/history/token_counting/sentencepiece_counter.py +8 -0
- shotgun/agents/history/token_counting/tokenizer_cache.py +3 -1
- shotgun/agents/history/token_counting/utils.py +0 -3
- shotgun/agents/plan.py +2 -2
- shotgun/agents/research.py +3 -3
- shotgun/agents/specify.py +2 -2
- shotgun/agents/tasks.py +2 -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 +27 -7
- shotgun/agents/tools/registry.py +217 -0
- shotgun/agents/tools/web_search/__init__.py +8 -8
- shotgun/agents/tools/web_search/anthropic.py +8 -2
- shotgun/agents/tools/web_search/gemini.py +7 -1
- shotgun/agents/tools/web_search/openai.py +7 -1
- shotgun/agents/tools/web_search/utils.py +2 -2
- shotgun/agents/usage_manager.py +16 -11
- shotgun/api_endpoints.py +7 -3
- shotgun/build_constants.py +3 -3
- shotgun/cli/clear.py +53 -0
- shotgun/cli/compact.py +186 -0
- shotgun/cli/config.py +8 -5
- shotgun/cli/context.py +111 -0
- shotgun/cli/export.py +1 -1
- shotgun/cli/feedback.py +4 -2
- shotgun/cli/models.py +1 -0
- 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 +16 -2
- shotgun/codebase/core/change_detector.py +5 -3
- shotgun/codebase/core/code_retrieval.py +4 -2
- shotgun/codebase/core/ingestor.py +10 -8
- shotgun/codebase/core/manager.py +13 -4
- shotgun/codebase/core/nl_query.py +1 -1
- shotgun/exceptions.py +32 -0
- shotgun/logging_config.py +18 -27
- shotgun/main.py +73 -11
- shotgun/posthog_telemetry.py +37 -28
- shotgun/prompts/agents/partials/common_agent_system_prompt.j2 +3 -2
- shotgun/sentry_telemetry.py +163 -16
- shotgun/settings.py +238 -0
- shotgun/telemetry.py +10 -33
- shotgun/tui/app.py +243 -43
- 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/containers.py +91 -0
- shotgun/tui/dependencies.py +39 -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 +1254 -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 +78 -2
- 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 +115 -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 +4 -4
- shotgun/tui/screens/github_issue.py +102 -0
- shotgun/tui/screens/model_picker.py +49 -24
- shotgun/tui/screens/onboarding.py +431 -0
- shotgun/tui/screens/pipx_migration.py +153 -0
- shotgun/tui/screens/provider_config.py +50 -27
- shotgun/tui/screens/shotgun_auth.py +2 -2
- shotgun/tui/screens/welcome.py +14 -11
- 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 +263 -0
- shotgun/utils/file_system_utils.py +22 -2
- shotgun/utils/marketing.py +110 -0
- shotgun/utils/update_checker.py +69 -14
- shotgun_sh-0.2.17.dist-info/METADATA +465 -0
- shotgun_sh-0.2.17.dist-info/RECORD +194 -0
- {shotgun_sh-0.2.8.dev2.dist-info → shotgun_sh-0.2.17.dist-info}/entry_points.txt +1 -0
- {shotgun_sh-0.2.8.dev2.dist-info → shotgun_sh-0.2.17.dist-info}/licenses/LICENSE +1 -1
- shotgun/tui/screens/chat.py +0 -996
- shotgun/tui/screens/chat_screen/history.py +0 -335
- shotgun_sh-0.2.8.dev2.dist-info/METADATA +0 -126
- shotgun_sh-0.2.8.dev2.dist-info/RECORD +0 -155
- {shotgun_sh-0.2.8.dev2.dist-info → shotgun_sh-0.2.17.dist-info}/WHEEL +0 -0
shotgun/sentry_telemetry.py
CHANGED
|
@@ -1,14 +1,132 @@
|
|
|
1
1
|
"""Sentry observability setup for Shotgun."""
|
|
2
2
|
|
|
3
|
-
import
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any
|
|
4
5
|
|
|
5
6
|
from shotgun import __version__
|
|
6
7
|
from shotgun.logging_config import get_early_logger
|
|
8
|
+
from shotgun.settings import settings
|
|
7
9
|
|
|
8
10
|
# Use early logger to prevent automatic StreamHandler creation
|
|
9
11
|
logger = get_early_logger(__name__)
|
|
10
12
|
|
|
11
13
|
|
|
14
|
+
def _scrub_path(path: str) -> str:
|
|
15
|
+
"""Scrub sensitive information from file paths.
|
|
16
|
+
|
|
17
|
+
Removes home directory and current working directory prefixes to prevent
|
|
18
|
+
leaking usernames that might be part of the path.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
path: The file path to scrub
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
The scrubbed path with sensitive prefixes removed
|
|
25
|
+
"""
|
|
26
|
+
if not path:
|
|
27
|
+
return path
|
|
28
|
+
|
|
29
|
+
try:
|
|
30
|
+
# Get home and cwd as Path objects for comparison
|
|
31
|
+
home = Path.home()
|
|
32
|
+
cwd = Path.cwd()
|
|
33
|
+
|
|
34
|
+
# Convert path to Path object
|
|
35
|
+
path_obj = Path(path)
|
|
36
|
+
|
|
37
|
+
# Try to make path relative to cwd first (most common case)
|
|
38
|
+
try:
|
|
39
|
+
relative_to_cwd = path_obj.relative_to(cwd)
|
|
40
|
+
return str(relative_to_cwd)
|
|
41
|
+
except ValueError:
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
# Try to replace home directory with ~
|
|
45
|
+
try:
|
|
46
|
+
relative_to_home = path_obj.relative_to(home)
|
|
47
|
+
return f"~/{relative_to_home}"
|
|
48
|
+
except ValueError:
|
|
49
|
+
pass
|
|
50
|
+
|
|
51
|
+
# If path is absolute but not under cwd or home, just return filename
|
|
52
|
+
if path_obj.is_absolute():
|
|
53
|
+
return path_obj.name
|
|
54
|
+
|
|
55
|
+
# Return as-is if already relative
|
|
56
|
+
return path
|
|
57
|
+
|
|
58
|
+
except Exception:
|
|
59
|
+
# If anything goes wrong, return the original path
|
|
60
|
+
# Better to leak a path than break error reporting
|
|
61
|
+
return path
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _scrub_sensitive_paths(event: dict[str, Any]) -> None:
|
|
65
|
+
"""Scrub sensitive paths from Sentry event data.
|
|
66
|
+
|
|
67
|
+
Modifies the event in-place to remove:
|
|
68
|
+
- Home directory paths (might contain usernames)
|
|
69
|
+
- Current working directory paths (might contain usernames)
|
|
70
|
+
- Server name/hostname
|
|
71
|
+
- Paths in sys.argv
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
event: The Sentry event dictionary to scrub
|
|
75
|
+
"""
|
|
76
|
+
extra = event.get("extra", {})
|
|
77
|
+
if "sys.argv" in extra:
|
|
78
|
+
argv = extra["sys.argv"]
|
|
79
|
+
if isinstance(argv, list):
|
|
80
|
+
extra["sys.argv"] = [
|
|
81
|
+
_scrub_path(arg) if isinstance(arg, str) else arg for arg in argv
|
|
82
|
+
]
|
|
83
|
+
|
|
84
|
+
# Scrub server name if present
|
|
85
|
+
if "server_name" in event:
|
|
86
|
+
event["server_name"] = ""
|
|
87
|
+
|
|
88
|
+
# Scrub contexts that might contain paths
|
|
89
|
+
if "contexts" in event:
|
|
90
|
+
contexts = event["contexts"]
|
|
91
|
+
# Remove runtime context if it has CWD
|
|
92
|
+
if "runtime" in contexts:
|
|
93
|
+
if "cwd" in contexts["runtime"]:
|
|
94
|
+
del contexts["runtime"]["cwd"]
|
|
95
|
+
# Scrub sys.argv to remove paths
|
|
96
|
+
if "sys.argv" in contexts["runtime"]:
|
|
97
|
+
argv = contexts["runtime"]["sys.argv"]
|
|
98
|
+
if isinstance(argv, list):
|
|
99
|
+
contexts["runtime"]["sys.argv"] = [
|
|
100
|
+
_scrub_path(arg) if isinstance(arg, str) else arg
|
|
101
|
+
for arg in argv
|
|
102
|
+
]
|
|
103
|
+
|
|
104
|
+
# Scrub exception stack traces
|
|
105
|
+
if "exception" in event and "values" in event["exception"]:
|
|
106
|
+
for exception in event["exception"]["values"]:
|
|
107
|
+
if "stacktrace" in exception and "frames" in exception["stacktrace"]:
|
|
108
|
+
for frame in exception["stacktrace"]["frames"]:
|
|
109
|
+
# Scrub file paths
|
|
110
|
+
if "abs_path" in frame:
|
|
111
|
+
frame["abs_path"] = _scrub_path(frame["abs_path"])
|
|
112
|
+
if "filename" in frame:
|
|
113
|
+
frame["filename"] = _scrub_path(frame["filename"])
|
|
114
|
+
|
|
115
|
+
# Scrub local variables that might contain paths
|
|
116
|
+
if "vars" in frame:
|
|
117
|
+
for var_name, var_value in frame["vars"].items():
|
|
118
|
+
if isinstance(var_value, str):
|
|
119
|
+
frame["vars"][var_name] = _scrub_path(var_value)
|
|
120
|
+
|
|
121
|
+
# Scrub breadcrumbs that might contain paths
|
|
122
|
+
if "breadcrumbs" in event and "values" in event["breadcrumbs"]:
|
|
123
|
+
for breadcrumb in event["breadcrumbs"]["values"]:
|
|
124
|
+
if "data" in breadcrumb:
|
|
125
|
+
for key, value in breadcrumb["data"].items():
|
|
126
|
+
if isinstance(value, str):
|
|
127
|
+
breadcrumb["data"][key] = _scrub_path(value)
|
|
128
|
+
|
|
129
|
+
|
|
12
130
|
def setup_sentry_observability() -> bool:
|
|
13
131
|
"""Set up Sentry observability for error tracking.
|
|
14
132
|
|
|
@@ -23,48 +141,77 @@ def setup_sentry_observability() -> bool:
|
|
|
23
141
|
logger.debug("Sentry is already initialized, skipping")
|
|
24
142
|
return True
|
|
25
143
|
|
|
26
|
-
#
|
|
27
|
-
dsn =
|
|
28
|
-
try:
|
|
29
|
-
from shotgun import build_constants
|
|
30
|
-
|
|
31
|
-
dsn = build_constants.SENTRY_DSN
|
|
32
|
-
logger.debug("Using Sentry DSN from build constants")
|
|
33
|
-
except ImportError:
|
|
34
|
-
# Fallback to environment variable (development)
|
|
35
|
-
dsn = os.getenv("SENTRY_DSN", "")
|
|
36
|
-
if dsn:
|
|
37
|
-
logger.debug("Using Sentry DSN from environment variable")
|
|
144
|
+
# Get DSN from settings (handles build constants + env vars automatically)
|
|
145
|
+
dsn = settings.telemetry.sentry_dsn
|
|
38
146
|
|
|
39
147
|
if not dsn:
|
|
40
148
|
logger.debug("No Sentry DSN configured, skipping Sentry initialization")
|
|
41
149
|
return False
|
|
42
150
|
|
|
43
|
-
logger.debug("
|
|
151
|
+
logger.debug("Using Sentry DSN from settings, proceeding with setup")
|
|
44
152
|
|
|
45
153
|
# Determine environment based on version
|
|
46
|
-
# Dev versions contain "dev", "rc", "alpha",
|
|
154
|
+
# Dev versions contain "dev", "rc", "alpha", "beta"
|
|
47
155
|
if any(marker in __version__ for marker in ["dev", "rc", "alpha", "beta"]):
|
|
48
156
|
environment = "development"
|
|
49
157
|
else:
|
|
50
158
|
environment = "production"
|
|
51
159
|
|
|
160
|
+
def before_send(event: Any, hint: dict[str, Any]) -> Any:
|
|
161
|
+
"""Filter out user-actionable errors and scrub sensitive paths.
|
|
162
|
+
|
|
163
|
+
User-actionable errors (like context size limits) are expected conditions
|
|
164
|
+
that users need to resolve, not bugs that need tracking.
|
|
165
|
+
|
|
166
|
+
Also scrubs sensitive information like usernames from file paths and
|
|
167
|
+
working directories to protect user privacy.
|
|
168
|
+
"""
|
|
169
|
+
|
|
170
|
+
log_record = hint.get("log_record")
|
|
171
|
+
if log_record:
|
|
172
|
+
# Scrub pathname using the helper function
|
|
173
|
+
log_record.pathname = _scrub_path(log_record.pathname)
|
|
174
|
+
|
|
175
|
+
# Scrub traceback text if it exists
|
|
176
|
+
if hasattr(log_record, "exc_text") and isinstance(
|
|
177
|
+
log_record.exc_text, str
|
|
178
|
+
):
|
|
179
|
+
# Replace home directory in traceback text
|
|
180
|
+
home = Path.home()
|
|
181
|
+
log_record.exc_text = log_record.exc_text.replace(str(home), "~")
|
|
182
|
+
|
|
183
|
+
if "exc_info" in hint:
|
|
184
|
+
_, exc_value, _ = hint["exc_info"]
|
|
185
|
+
from shotgun.exceptions import ErrorNotPickedUpBySentry
|
|
186
|
+
|
|
187
|
+
if isinstance(exc_value, ErrorNotPickedUpBySentry):
|
|
188
|
+
# Don't send to Sentry - this is user-actionable, not a bug
|
|
189
|
+
return None
|
|
190
|
+
|
|
191
|
+
# Scrub sensitive paths from the event
|
|
192
|
+
_scrub_sensitive_paths(event)
|
|
193
|
+
return event
|
|
194
|
+
|
|
52
195
|
# Initialize Sentry
|
|
53
196
|
sentry_sdk.init(
|
|
54
197
|
dsn=dsn,
|
|
55
198
|
release=f"shotgun-sh@{__version__}",
|
|
56
199
|
environment=environment,
|
|
57
200
|
send_default_pii=False, # Privacy-first: never send PII
|
|
201
|
+
server_name="", # Privacy: don't send hostname (may contain username)
|
|
58
202
|
traces_sample_rate=0.1 if environment == "production" else 1.0,
|
|
59
203
|
profiles_sample_rate=0.1 if environment == "production" else 1.0,
|
|
204
|
+
before_send=before_send,
|
|
60
205
|
)
|
|
61
206
|
|
|
62
207
|
# Set user context with anonymous shotgun instance ID from config
|
|
63
208
|
try:
|
|
209
|
+
import asyncio
|
|
210
|
+
|
|
64
211
|
from shotgun.agents.config import get_config_manager
|
|
65
212
|
|
|
66
213
|
config_manager = get_config_manager()
|
|
67
|
-
shotgun_instance_id = config_manager.get_shotgun_instance_id()
|
|
214
|
+
shotgun_instance_id = asyncio.run(config_manager.get_shotgun_instance_id())
|
|
68
215
|
sentry_sdk.set_user({"id": shotgun_instance_id})
|
|
69
216
|
logger.debug("Sentry user context set with anonymous ID")
|
|
70
217
|
except Exception as 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()
|
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
|
|
@@ -75,12 +50,14 @@ def setup_logfire_observability() -> bool:
|
|
|
75
50
|
|
|
76
51
|
# Set user context using baggage for all logs and spans
|
|
77
52
|
try:
|
|
53
|
+
import asyncio
|
|
54
|
+
|
|
78
55
|
from opentelemetry import baggage, context
|
|
79
56
|
|
|
80
57
|
from shotgun.agents.config import get_config_manager
|
|
81
58
|
|
|
82
59
|
config_manager = get_config_manager()
|
|
83
|
-
shotgun_instance_id = config_manager.get_shotgun_instance_id()
|
|
60
|
+
shotgun_instance_id = asyncio.run(config_manager.get_shotgun_instance_id())
|
|
84
61
|
|
|
85
62
|
# Set shotgun_instance_id as baggage in global context - this will be included in all logs/spans
|
|
86
63
|
ctx = baggage.set_baggage("shotgun_instance_id", shotgun_instance_id)
|