shotgun-sh 0.1.16.dev2__py3-none-any.whl → 0.2.1__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/common.py +4 -5
- shotgun/agents/config/constants.py +23 -6
- shotgun/agents/config/manager.py +239 -76
- shotgun/agents/config/models.py +74 -84
- shotgun/agents/config/provider.py +174 -85
- shotgun/agents/history/compaction.py +1 -1
- shotgun/agents/history/history_processors.py +18 -9
- shotgun/agents/history/token_counting/__init__.py +31 -0
- shotgun/agents/history/token_counting/anthropic.py +89 -0
- shotgun/agents/history/token_counting/base.py +67 -0
- shotgun/agents/history/token_counting/openai.py +80 -0
- shotgun/agents/history/token_counting/sentencepiece_counter.py +119 -0
- shotgun/agents/history/token_counting/tokenizer_cache.py +90 -0
- shotgun/agents/history/token_counting/utils.py +147 -0
- shotgun/agents/history/token_estimation.py +12 -12
- shotgun/agents/llm.py +62 -0
- shotgun/agents/models.py +2 -2
- shotgun/agents/tools/web_search/__init__.py +42 -15
- shotgun/agents/tools/web_search/anthropic.py +54 -50
- shotgun/agents/tools/web_search/gemini.py +31 -20
- shotgun/agents/tools/web_search/openai.py +4 -4
- shotgun/build_constants.py +2 -2
- shotgun/cli/config.py +34 -63
- shotgun/cli/feedback.py +4 -2
- shotgun/cli/models.py +2 -2
- shotgun/codebase/core/ingestor.py +47 -8
- shotgun/codebase/core/manager.py +7 -3
- shotgun/codebase/models.py +4 -4
- shotgun/llm_proxy/__init__.py +16 -0
- shotgun/llm_proxy/clients.py +39 -0
- shotgun/llm_proxy/constants.py +8 -0
- shotgun/main.py +6 -0
- shotgun/posthog_telemetry.py +15 -11
- shotgun/sentry_telemetry.py +3 -3
- shotgun/shotgun_web/__init__.py +19 -0
- shotgun/shotgun_web/client.py +138 -0
- shotgun/shotgun_web/constants.py +17 -0
- shotgun/shotgun_web/models.py +47 -0
- shotgun/telemetry.py +7 -4
- shotgun/tui/app.py +26 -8
- shotgun/tui/screens/chat.py +2 -8
- shotgun/tui/screens/chat_screen/command_providers.py +118 -11
- shotgun/tui/screens/chat_screen/history.py +3 -1
- shotgun/tui/screens/feedback.py +2 -2
- shotgun/tui/screens/model_picker.py +327 -0
- shotgun/tui/screens/provider_config.py +118 -28
- shotgun/tui/screens/shotgun_auth.py +295 -0
- shotgun/tui/screens/welcome.py +176 -0
- shotgun/utils/env_utils.py +12 -0
- {shotgun_sh-0.1.16.dev2.dist-info → shotgun_sh-0.2.1.dist-info}/METADATA +2 -2
- {shotgun_sh-0.1.16.dev2.dist-info → shotgun_sh-0.2.1.dist-info}/RECORD +54 -37
- shotgun/agents/history/token_counting.py +0 -429
- {shotgun_sh-0.1.16.dev2.dist-info → shotgun_sh-0.2.1.dist-info}/WHEEL +0 -0
- {shotgun_sh-0.1.16.dev2.dist-info → shotgun_sh-0.2.1.dist-info}/entry_points.txt +0 -0
- {shotgun_sh-0.1.16.dev2.dist-info → shotgun_sh-0.2.1.dist-info}/licenses/LICENSE +0 -0
shotgun/build_constants.py
CHANGED
|
@@ -12,8 +12,8 @@ POSTHOG_API_KEY = ''
|
|
|
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 = ''
|
|
16
|
+
LOGFIRE_TOKEN = ''
|
|
17
17
|
|
|
18
18
|
# Build metadata
|
|
19
19
|
BUILD_TIME_ENV = "production" if SENTRY_DSN else "development"
|
shotgun/cli/config.py
CHANGED
|
@@ -9,6 +9,7 @@ from rich.table import Table
|
|
|
9
9
|
|
|
10
10
|
from shotgun.agents.config import ProviderType, get_config_manager
|
|
11
11
|
from shotgun.logging_config import get_logger
|
|
12
|
+
from shotgun.utils.env_utils import is_shotgun_account_enabled
|
|
12
13
|
|
|
13
14
|
logger = get_logger(__name__)
|
|
14
15
|
console = Console()
|
|
@@ -43,11 +44,11 @@ def init(
|
|
|
43
44
|
console.print()
|
|
44
45
|
|
|
45
46
|
# Initialize with defaults
|
|
46
|
-
|
|
47
|
+
config_manager.initialize()
|
|
47
48
|
|
|
48
|
-
# Ask for
|
|
49
|
+
# Ask for provider
|
|
49
50
|
provider_choices = ["openai", "anthropic", "google"]
|
|
50
|
-
console.print("Choose your
|
|
51
|
+
console.print("Choose your AI provider:")
|
|
51
52
|
for i, provider in enumerate(provider_choices, 1):
|
|
52
53
|
console.print(f" {i}. {provider}")
|
|
53
54
|
|
|
@@ -55,7 +56,7 @@ def init(
|
|
|
55
56
|
try:
|
|
56
57
|
choice = typer.prompt("Enter choice (1-3)", type=int)
|
|
57
58
|
if 1 <= choice <= 3:
|
|
58
|
-
|
|
59
|
+
provider = ProviderType(provider_choices[choice - 1])
|
|
59
60
|
break
|
|
60
61
|
else:
|
|
61
62
|
console.print(
|
|
@@ -65,7 +66,6 @@ def init(
|
|
|
65
66
|
console.print("❌ Please enter a valid number.", style="red")
|
|
66
67
|
|
|
67
68
|
# Ask for API key for the selected provider
|
|
68
|
-
provider = config.default_provider
|
|
69
69
|
console.print(f"\n🔑 Setting up {provider.upper()} API key...")
|
|
70
70
|
|
|
71
71
|
api_key = typer.prompt(
|
|
@@ -75,9 +75,9 @@ def init(
|
|
|
75
75
|
)
|
|
76
76
|
|
|
77
77
|
if api_key:
|
|
78
|
+
# update_provider will automatically set selected_model for first provider
|
|
78
79
|
config_manager.update_provider(provider, api_key=api_key)
|
|
79
80
|
|
|
80
|
-
config_manager.save()
|
|
81
81
|
console.print(
|
|
82
82
|
f"\n✅ [bold green]Configuration saved to {config_manager.config_path}[/bold green]"
|
|
83
83
|
)
|
|
@@ -98,16 +98,12 @@ def set(
|
|
|
98
98
|
str | None,
|
|
99
99
|
typer.Option("--api-key", "-k", help="API key for the provider"),
|
|
100
100
|
] = None,
|
|
101
|
-
default: Annotated[
|
|
102
|
-
bool,
|
|
103
|
-
typer.Option("--default", "-d", help="Set this provider as default"),
|
|
104
|
-
] = False,
|
|
105
101
|
) -> None:
|
|
106
102
|
"""Set configuration for a specific provider."""
|
|
107
103
|
config_manager = get_config_manager()
|
|
108
104
|
|
|
109
|
-
# If no API key provided via option
|
|
110
|
-
if api_key is None
|
|
105
|
+
# If no API key provided via option, prompt for it
|
|
106
|
+
if api_key is None:
|
|
111
107
|
api_key = typer.prompt(
|
|
112
108
|
f"Enter your {provider.upper()} API key",
|
|
113
109
|
hide_input=True,
|
|
@@ -118,11 +114,6 @@ def set(
|
|
|
118
114
|
if api_key:
|
|
119
115
|
config_manager.update_provider(provider, api_key=api_key)
|
|
120
116
|
|
|
121
|
-
if default:
|
|
122
|
-
config = config_manager.load()
|
|
123
|
-
config.default_provider = provider
|
|
124
|
-
config_manager.save(config)
|
|
125
|
-
|
|
126
117
|
console.print(f"✅ Configuration updated for {provider}")
|
|
127
118
|
|
|
128
119
|
except Exception as e:
|
|
@@ -130,41 +121,6 @@ def set(
|
|
|
130
121
|
raise typer.Exit(1) from e
|
|
131
122
|
|
|
132
123
|
|
|
133
|
-
@app.command()
|
|
134
|
-
def set_default(
|
|
135
|
-
provider: Annotated[
|
|
136
|
-
ProviderType,
|
|
137
|
-
typer.Argument(
|
|
138
|
-
help="AI provider to set as default (openai, anthropic, google)"
|
|
139
|
-
),
|
|
140
|
-
],
|
|
141
|
-
) -> None:
|
|
142
|
-
"""Set the default AI provider without modifying API keys."""
|
|
143
|
-
config_manager = get_config_manager()
|
|
144
|
-
|
|
145
|
-
try:
|
|
146
|
-
config = config_manager.load()
|
|
147
|
-
|
|
148
|
-
# Check if the provider has an API key configured
|
|
149
|
-
provider_config = getattr(config, provider.value)
|
|
150
|
-
if not provider_config.api_key:
|
|
151
|
-
console.print(
|
|
152
|
-
f"⚠️ Warning: {provider.upper()} does not have an API key configured.",
|
|
153
|
-
style="yellow",
|
|
154
|
-
)
|
|
155
|
-
console.print(f"Use 'shotgun config set {provider}' to configure it.")
|
|
156
|
-
|
|
157
|
-
# Set as default
|
|
158
|
-
config.default_provider = provider
|
|
159
|
-
config_manager.save(config)
|
|
160
|
-
|
|
161
|
-
console.print(f"✅ Default provider set to: {provider}")
|
|
162
|
-
|
|
163
|
-
except Exception as e:
|
|
164
|
-
console.print(f"❌ Failed to set default provider: {e}", style="red")
|
|
165
|
-
raise typer.Exit(1) from e
|
|
166
|
-
|
|
167
|
-
|
|
168
124
|
@app.command()
|
|
169
125
|
def get(
|
|
170
126
|
provider: Annotated[
|
|
@@ -201,16 +157,23 @@ def _show_full_config(config: Any) -> None:
|
|
|
201
157
|
table.add_column("Setting", style="cyan")
|
|
202
158
|
table.add_column("Value", style="white")
|
|
203
159
|
|
|
204
|
-
#
|
|
205
|
-
|
|
160
|
+
# Selected model
|
|
161
|
+
selected_model = config.selected_model or "None (will auto-detect)"
|
|
162
|
+
table.add_row("Selected Model", f"[bold]{selected_model}[/bold]")
|
|
206
163
|
table.add_row("", "") # Separator
|
|
207
164
|
|
|
208
165
|
# Provider configurations
|
|
209
|
-
|
|
166
|
+
providers_to_show = [
|
|
210
167
|
("OpenAI", config.openai),
|
|
211
168
|
("Anthropic", config.anthropic),
|
|
212
169
|
("Google", config.google),
|
|
213
|
-
]
|
|
170
|
+
]
|
|
171
|
+
|
|
172
|
+
# Only show Shotgun Account if feature flag is enabled
|
|
173
|
+
if is_shotgun_account_enabled():
|
|
174
|
+
providers_to_show.append(("Shotgun Account", config.shotgun))
|
|
175
|
+
|
|
176
|
+
for provider_name, provider_config in providers_to_show:
|
|
214
177
|
table.add_row(f"[bold]{provider_name}[/bold]", "")
|
|
215
178
|
|
|
216
179
|
# API Key
|
|
@@ -231,6 +194,8 @@ def _show_provider_config(provider: ProviderType, config: Any) -> None:
|
|
|
231
194
|
provider_config = config.anthropic
|
|
232
195
|
elif provider_str == "google":
|
|
233
196
|
provider_config = config.google
|
|
197
|
+
elif provider_str == "shotgun":
|
|
198
|
+
provider_config = config.shotgun
|
|
234
199
|
else:
|
|
235
200
|
console.print(f"❌ Unknown provider: {provider}", style="red")
|
|
236
201
|
return
|
|
@@ -248,7 +213,13 @@ def _show_provider_config(provider: ProviderType, config: Any) -> None:
|
|
|
248
213
|
|
|
249
214
|
def _mask_secrets(data: dict[str, Any]) -> None:
|
|
250
215
|
"""Mask secrets in configuration data."""
|
|
251
|
-
|
|
216
|
+
providers = ["openai", "anthropic", "google"]
|
|
217
|
+
|
|
218
|
+
# Only mask shotgun if feature flag is enabled
|
|
219
|
+
if is_shotgun_account_enabled():
|
|
220
|
+
providers.append("shotgun")
|
|
221
|
+
|
|
222
|
+
for provider in providers:
|
|
252
223
|
if provider in data and isinstance(data[provider], dict):
|
|
253
224
|
if "api_key" in data[provider] and data[provider]["api_key"]:
|
|
254
225
|
data[provider]["api_key"] = _mask_value(data[provider]["api_key"])
|
|
@@ -262,14 +233,14 @@ def _mask_value(value: str) -> str:
|
|
|
262
233
|
|
|
263
234
|
|
|
264
235
|
@app.command()
|
|
265
|
-
def
|
|
266
|
-
"""Get the anonymous
|
|
236
|
+
def get_shotgun_instance_id() -> None:
|
|
237
|
+
"""Get the anonymous shotgun instance ID from configuration."""
|
|
267
238
|
config_manager = get_config_manager()
|
|
268
239
|
|
|
269
240
|
try:
|
|
270
|
-
|
|
271
|
-
console.print(f"[green]
|
|
241
|
+
shotgun_instance_id = config_manager.get_shotgun_instance_id()
|
|
242
|
+
console.print(f"[green]Shotgun Instance ID:[/green] {shotgun_instance_id}")
|
|
272
243
|
except Exception as e:
|
|
273
|
-
logger.error(f"Error getting
|
|
274
|
-
console.print(f"❌ Failed to get
|
|
244
|
+
logger.error(f"Error getting shotgun instance ID: {e}")
|
|
245
|
+
console.print(f"❌ Failed to get shotgun instance ID: {str(e)}", style="red")
|
|
275
246
|
raise typer.Exit(1) from e
|
shotgun/cli/feedback.py
CHANGED
|
@@ -30,7 +30,7 @@ def send_feedback(
|
|
|
30
30
|
"""Initialize Shotgun configuration."""
|
|
31
31
|
config_manager = get_config_manager()
|
|
32
32
|
config_manager.load()
|
|
33
|
-
|
|
33
|
+
shotgun_instance_id = config_manager.get_shotgun_instance_id()
|
|
34
34
|
|
|
35
35
|
if not description:
|
|
36
36
|
console.print(
|
|
@@ -39,7 +39,9 @@ def send_feedback(
|
|
|
39
39
|
)
|
|
40
40
|
raise typer.Exit(1)
|
|
41
41
|
|
|
42
|
-
feedback = Feedback(
|
|
42
|
+
feedback = Feedback(
|
|
43
|
+
kind=kind, description=description, shotgun_instance_id=shotgun_instance_id
|
|
44
|
+
)
|
|
43
45
|
|
|
44
46
|
submit_feedback_survey(feedback)
|
|
45
47
|
|
shotgun/cli/models.py
CHANGED
|
@@ -18,15 +18,12 @@ from shotgun.logging_config import get_logger
|
|
|
18
18
|
logger = get_logger(__name__)
|
|
19
19
|
|
|
20
20
|
|
|
21
|
-
#
|
|
22
|
-
|
|
21
|
+
# Directories that should never be traversed during indexing
|
|
22
|
+
BASE_IGNORE_DIRECTORIES = {
|
|
23
23
|
".git",
|
|
24
24
|
"venv",
|
|
25
25
|
".venv",
|
|
26
26
|
"__pycache__",
|
|
27
|
-
"node_modules",
|
|
28
|
-
"build",
|
|
29
|
-
"dist",
|
|
30
27
|
".eggs",
|
|
31
28
|
".pytest_cache",
|
|
32
29
|
".mypy_cache",
|
|
@@ -36,6 +33,46 @@ IGNORE_PATTERNS = {
|
|
|
36
33
|
".vscode",
|
|
37
34
|
}
|
|
38
35
|
|
|
36
|
+
# Well-known build output directories to skip when determining source files
|
|
37
|
+
BUILD_ARTIFACT_DIRECTORIES = {
|
|
38
|
+
"node_modules",
|
|
39
|
+
".next",
|
|
40
|
+
".nuxt",
|
|
41
|
+
".vite",
|
|
42
|
+
".yarn",
|
|
43
|
+
".svelte-kit",
|
|
44
|
+
".output",
|
|
45
|
+
".turbo",
|
|
46
|
+
".parcel-cache",
|
|
47
|
+
".vercel",
|
|
48
|
+
".serverless",
|
|
49
|
+
"build",
|
|
50
|
+
"dist",
|
|
51
|
+
"out",
|
|
52
|
+
"tmp",
|
|
53
|
+
"coverage",
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
# Default ignore patterns combines base directories and build artifacts
|
|
57
|
+
IGNORE_PATTERNS = BASE_IGNORE_DIRECTORIES | BUILD_ARTIFACT_DIRECTORIES
|
|
58
|
+
|
|
59
|
+
# Directory prefixes that should always be ignored
|
|
60
|
+
IGNORED_DIRECTORY_PREFIXES = (".",)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def should_ignore_directory(name: str, ignore_patterns: set[str] | None = None) -> bool:
|
|
64
|
+
"""Return True if the directory name should be ignored."""
|
|
65
|
+
patterns = IGNORE_PATTERNS if ignore_patterns is None else ignore_patterns
|
|
66
|
+
if name in patterns:
|
|
67
|
+
return True
|
|
68
|
+
return name.startswith(IGNORED_DIRECTORY_PREFIXES)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def is_path_ignored(path: Path, ignore_patterns: set[str] | None = None) -> bool:
|
|
72
|
+
"""Return True if any part of the path should be ignored."""
|
|
73
|
+
patterns = IGNORE_PATTERNS if ignore_patterns is None else ignore_patterns
|
|
74
|
+
return any(should_ignore_directory(part, patterns) for part in path.parts)
|
|
75
|
+
|
|
39
76
|
|
|
40
77
|
class Ingestor:
|
|
41
78
|
"""Handles all communication and ingestion with the Kuzu database."""
|
|
@@ -607,7 +644,9 @@ class SimpleGraphBuilder:
|
|
|
607
644
|
"""First pass: Walk directory to find packages and folders."""
|
|
608
645
|
dir_count = 0
|
|
609
646
|
for root_str, dirs, _ in os.walk(self.repo_path, topdown=True):
|
|
610
|
-
dirs[:] = [
|
|
647
|
+
dirs[:] = [
|
|
648
|
+
d for d in dirs if not should_ignore_directory(d, self.ignore_dirs)
|
|
649
|
+
]
|
|
611
650
|
root = Path(root_str)
|
|
612
651
|
relative_root = root.relative_to(self.repo_path)
|
|
613
652
|
|
|
@@ -740,7 +779,7 @@ class SimpleGraphBuilder:
|
|
|
740
779
|
root = Path(root_str)
|
|
741
780
|
|
|
742
781
|
# Skip ignored directories
|
|
743
|
-
if
|
|
782
|
+
if is_path_ignored(root, self.ignore_dirs):
|
|
744
783
|
continue
|
|
745
784
|
|
|
746
785
|
for filename in files:
|
|
@@ -757,7 +796,7 @@ class SimpleGraphBuilder:
|
|
|
757
796
|
root = Path(root_str)
|
|
758
797
|
|
|
759
798
|
# Skip ignored directories
|
|
760
|
-
if
|
|
799
|
+
if is_path_ignored(root, self.ignore_dirs):
|
|
761
800
|
continue
|
|
762
801
|
|
|
763
802
|
for filename in files:
|
shotgun/codebase/core/manager.py
CHANGED
|
@@ -51,9 +51,13 @@ class CodebaseFileHandler(FileSystemEventHandler):
|
|
|
51
51
|
self.pending_changes: list[FileChange] = []
|
|
52
52
|
self._lock = anyio.Lock()
|
|
53
53
|
# Import default ignore patterns from ingestor
|
|
54
|
-
from shotgun.codebase.core.ingestor import
|
|
54
|
+
from shotgun.codebase.core.ingestor import (
|
|
55
|
+
IGNORE_PATTERNS,
|
|
56
|
+
should_ignore_directory,
|
|
57
|
+
)
|
|
55
58
|
|
|
56
59
|
self.ignore_patterns = ignore_patterns or IGNORE_PATTERNS
|
|
60
|
+
self._should_ignore_directory = should_ignore_directory
|
|
57
61
|
|
|
58
62
|
def on_any_event(self, event: FileSystemEvent) -> None:
|
|
59
63
|
"""Handle any file system event."""
|
|
@@ -71,7 +75,7 @@ class CodebaseFileHandler(FileSystemEventHandler):
|
|
|
71
75
|
|
|
72
76
|
# Check if any parent directory should be ignored
|
|
73
77
|
for parent in path.parents:
|
|
74
|
-
if parent.name
|
|
78
|
+
if self._should_ignore_directory(parent.name, self.ignore_patterns):
|
|
75
79
|
logger.debug(
|
|
76
80
|
f"Ignoring file in ignored directory: {parent.name} - path: {src_path_str}"
|
|
77
81
|
)
|
|
@@ -106,7 +110,7 @@ class CodebaseFileHandler(FileSystemEventHandler):
|
|
|
106
110
|
)
|
|
107
111
|
dest_path = Path(dest_path_str)
|
|
108
112
|
for parent in dest_path.parents:
|
|
109
|
-
if parent.name
|
|
113
|
+
if self._should_ignore_directory(parent.name, self.ignore_patterns):
|
|
110
114
|
logger.debug(
|
|
111
115
|
f"Ignoring move to ignored directory: {parent.name} - dest_path: {dest_path_str}"
|
|
112
116
|
)
|
shotgun/codebase/models.py
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
"""Data models for codebase service."""
|
|
2
2
|
|
|
3
3
|
from collections.abc import Callable
|
|
4
|
-
from enum import
|
|
4
|
+
from enum import StrEnum
|
|
5
5
|
from typing import Any
|
|
6
6
|
|
|
7
7
|
from pydantic import BaseModel, Field
|
|
8
8
|
|
|
9
9
|
|
|
10
|
-
class GraphStatus(
|
|
10
|
+
class GraphStatus(StrEnum):
|
|
11
11
|
"""Status of a code knowledge graph."""
|
|
12
12
|
|
|
13
13
|
READY = "READY" # Graph is ready for queries
|
|
@@ -16,14 +16,14 @@ class GraphStatus(str, Enum):
|
|
|
16
16
|
ERROR = "ERROR" # Last operation failed
|
|
17
17
|
|
|
18
18
|
|
|
19
|
-
class QueryType(
|
|
19
|
+
class QueryType(StrEnum):
|
|
20
20
|
"""Type of query being executed."""
|
|
21
21
|
|
|
22
22
|
NATURAL_LANGUAGE = "natural_language"
|
|
23
23
|
CYPHER = "cypher"
|
|
24
24
|
|
|
25
25
|
|
|
26
|
-
class ProgressPhase(
|
|
26
|
+
class ProgressPhase(StrEnum):
|
|
27
27
|
"""Phase of codebase indexing progress."""
|
|
28
28
|
|
|
29
29
|
STRUCTURE = "structure" # Identifying packages and folders
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""LiteLLM proxy client utilities and configuration."""
|
|
2
|
+
|
|
3
|
+
from .clients import create_anthropic_proxy_client, create_litellm_provider
|
|
4
|
+
from .constants import (
|
|
5
|
+
LITELLM_PROXY_ANTHROPIC_BASE,
|
|
6
|
+
LITELLM_PROXY_BASE_URL,
|
|
7
|
+
LITELLM_PROXY_OPENAI_BASE,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"LITELLM_PROXY_BASE_URL",
|
|
12
|
+
"LITELLM_PROXY_ANTHROPIC_BASE",
|
|
13
|
+
"LITELLM_PROXY_OPENAI_BASE",
|
|
14
|
+
"create_litellm_provider",
|
|
15
|
+
"create_anthropic_proxy_client",
|
|
16
|
+
]
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Client creation utilities for LiteLLM proxy."""
|
|
2
|
+
|
|
3
|
+
from anthropic import Anthropic
|
|
4
|
+
from pydantic_ai.providers.litellm import LiteLLMProvider
|
|
5
|
+
|
|
6
|
+
from .constants import LITELLM_PROXY_ANTHROPIC_BASE, LITELLM_PROXY_BASE_URL
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def create_litellm_provider(api_key: str) -> LiteLLMProvider:
|
|
10
|
+
"""Create LiteLLM provider for Shotgun Account.
|
|
11
|
+
|
|
12
|
+
Args:
|
|
13
|
+
api_key: Shotgun API key
|
|
14
|
+
|
|
15
|
+
Returns:
|
|
16
|
+
Configured LiteLLM provider pointing to Shotgun's proxy
|
|
17
|
+
"""
|
|
18
|
+
return LiteLLMProvider(
|
|
19
|
+
api_base=LITELLM_PROXY_BASE_URL,
|
|
20
|
+
api_key=api_key,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def create_anthropic_proxy_client(api_key: str) -> Anthropic:
|
|
25
|
+
"""Create Anthropic client configured for LiteLLM proxy.
|
|
26
|
+
|
|
27
|
+
This client will proxy token counting requests through the
|
|
28
|
+
LiteLLM proxy to Anthropic's actual token counting API.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
api_key: Shotgun API key
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
Anthropic client configured to use LiteLLM proxy
|
|
35
|
+
"""
|
|
36
|
+
return Anthropic(
|
|
37
|
+
api_key=api_key,
|
|
38
|
+
base_url=LITELLM_PROXY_ANTHROPIC_BASE,
|
|
39
|
+
)
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""LiteLLM proxy constants and configuration."""
|
|
2
|
+
|
|
3
|
+
# Shotgun's LiteLLM proxy base URL
|
|
4
|
+
LITELLM_PROXY_BASE_URL = "https://litellm-701197220809.us-east1.run.app"
|
|
5
|
+
|
|
6
|
+
# Provider-specific endpoints
|
|
7
|
+
LITELLM_PROXY_ANTHROPIC_BASE = f"{LITELLM_PROXY_BASE_URL}/anthropic"
|
|
8
|
+
LITELLM_PROXY_OPENAI_BASE = LITELLM_PROXY_BASE_URL
|
shotgun/main.py
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
"""Main CLI application for shotgun."""
|
|
2
2
|
|
|
3
|
+
# NOTE: These are before we import any Google library to stop the noisy gRPC logs.
|
|
4
|
+
import os # noqa: I001
|
|
5
|
+
|
|
6
|
+
os.environ["GRPC_VERBOSITY"] = "ERROR"
|
|
7
|
+
os.environ["GLOG_minloglevel"] = "2"
|
|
8
|
+
|
|
3
9
|
import logging
|
|
4
10
|
|
|
5
11
|
# CRITICAL: Add NullHandler to root logger before ANY other imports.
|
shotgun/posthog_telemetry.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""PostHog analytics setup for Shotgun."""
|
|
2
2
|
|
|
3
|
-
from enum import
|
|
3
|
+
from enum import StrEnum
|
|
4
4
|
from typing import Any
|
|
5
5
|
|
|
6
6
|
import posthog
|
|
@@ -51,14 +51,14 @@ def setup_posthog_observability() -> bool:
|
|
|
51
51
|
# Store the client for later use
|
|
52
52
|
_posthog_client = posthog
|
|
53
53
|
|
|
54
|
-
# Set user context with anonymous
|
|
54
|
+
# Set user context with anonymous shotgun instance ID from config
|
|
55
55
|
try:
|
|
56
56
|
config_manager = get_config_manager()
|
|
57
|
-
|
|
57
|
+
shotgun_instance_id = config_manager.get_shotgun_instance_id()
|
|
58
58
|
|
|
59
59
|
# Identify the user in PostHog
|
|
60
60
|
posthog.identify( # type: ignore[attr-defined]
|
|
61
|
-
distinct_id=
|
|
61
|
+
distinct_id=shotgun_instance_id,
|
|
62
62
|
properties={
|
|
63
63
|
"version": __version__,
|
|
64
64
|
"environment": environment,
|
|
@@ -69,7 +69,9 @@ def setup_posthog_observability() -> bool:
|
|
|
69
69
|
posthog.disabled = False
|
|
70
70
|
posthog.personal_api_key = None # Not needed for event tracking
|
|
71
71
|
|
|
72
|
-
logger.debug(
|
|
72
|
+
logger.debug(
|
|
73
|
+
"PostHog user identified with anonymous ID: %s", shotgun_instance_id
|
|
74
|
+
)
|
|
73
75
|
except Exception as e:
|
|
74
76
|
logger.warning("Failed to set user context: %s", e)
|
|
75
77
|
|
|
@@ -99,9 +101,9 @@ def track_event(event_name: str, properties: dict[str, Any] | None = None) -> No
|
|
|
99
101
|
return
|
|
100
102
|
|
|
101
103
|
try:
|
|
102
|
-
# Get
|
|
104
|
+
# Get shotgun instance ID for tracking
|
|
103
105
|
config_manager = get_config_manager()
|
|
104
|
-
|
|
106
|
+
shotgun_instance_id = config_manager.get_shotgun_instance_id()
|
|
105
107
|
|
|
106
108
|
# Add version and environment to properties
|
|
107
109
|
if properties is None:
|
|
@@ -116,7 +118,7 @@ def track_event(event_name: str, properties: dict[str, Any] | None = None) -> No
|
|
|
116
118
|
|
|
117
119
|
# Track the event using PostHog's capture method
|
|
118
120
|
_posthog_client.capture(
|
|
119
|
-
distinct_id=
|
|
121
|
+
distinct_id=shotgun_instance_id, event=event_name, properties=properties
|
|
120
122
|
)
|
|
121
123
|
logger.debug("Tracked PostHog event: %s", event_name)
|
|
122
124
|
except Exception as e:
|
|
@@ -137,7 +139,7 @@ def shutdown() -> None:
|
|
|
137
139
|
_posthog_client = None
|
|
138
140
|
|
|
139
141
|
|
|
140
|
-
class FeedbackKind(
|
|
142
|
+
class FeedbackKind(StrEnum):
|
|
141
143
|
BUG = "bug"
|
|
142
144
|
FEATURE = "feature"
|
|
143
145
|
OTHER = "other"
|
|
@@ -146,7 +148,7 @@ class FeedbackKind(str, Enum):
|
|
|
146
148
|
class Feedback(BaseModel):
|
|
147
149
|
kind: FeedbackKind
|
|
148
150
|
description: str
|
|
149
|
-
|
|
151
|
+
shotgun_instance_id: str
|
|
150
152
|
|
|
151
153
|
|
|
152
154
|
SURVEY_ID = "01999f81-9486-0000-4fa6-9632959f92f3"
|
|
@@ -178,7 +180,9 @@ def submit_feedback_survey(feedback: Feedback) -> None:
|
|
|
178
180
|
],
|
|
179
181
|
f"$survey_response_{Q_KIND_ID}": feedback.kind,
|
|
180
182
|
f"$survey_response_{Q_DESCRIPTION_ID}": feedback.description,
|
|
181
|
-
"
|
|
183
|
+
"selected_model": config.selected_model.value
|
|
184
|
+
if config.selected_model
|
|
185
|
+
else None,
|
|
182
186
|
"config_version": config.config_version,
|
|
183
187
|
"last_10_messages": last_10_messages, # last 10 messages
|
|
184
188
|
},
|
shotgun/sentry_telemetry.py
CHANGED
|
@@ -59,13 +59,13 @@ def setup_sentry_observability() -> bool:
|
|
|
59
59
|
profiles_sample_rate=0.1 if environment == "production" else 1.0,
|
|
60
60
|
)
|
|
61
61
|
|
|
62
|
-
# Set user context with anonymous
|
|
62
|
+
# Set user context with anonymous shotgun instance ID from config
|
|
63
63
|
try:
|
|
64
64
|
from shotgun.agents.config import get_config_manager
|
|
65
65
|
|
|
66
66
|
config_manager = get_config_manager()
|
|
67
|
-
|
|
68
|
-
sentry_sdk.set_user({"id":
|
|
67
|
+
shotgun_instance_id = config_manager.get_shotgun_instance_id()
|
|
68
|
+
sentry_sdk.set_user({"id": shotgun_instance_id})
|
|
69
69
|
logger.debug("Sentry user context set with anonymous ID")
|
|
70
70
|
except Exception as e:
|
|
71
71
|
logger.warning("Failed to set Sentry user context: %s", e)
|
|
@@ -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
|
+
]
|