shotgun-sh 0.1.15.dev2__py3-none-any.whl → 0.2.1.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/common.py +4 -5
- shotgun/agents/config/constants.py +21 -5
- shotgun/agents/config/manager.py +147 -39
- shotgun/agents/config/models.py +59 -86
- shotgun/agents/config/provider.py +164 -61
- 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 +46 -40
- shotgun/agents/tools/web_search/gemini.py +31 -20
- shotgun/agents/tools/web_search/openai.py +4 -4
- shotgun/cli/config.py +14 -55
- shotgun/cli/feedback.py +1 -1
- shotgun/cli/models.py +2 -2
- 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 +5 -3
- shotgun/tui/app.py +3 -1
- shotgun/tui/screens/chat_screen/command_providers.py +20 -0
- shotgun/tui/screens/model_picker.py +214 -0
- shotgun/tui/screens/provider_config.py +39 -26
- {shotgun_sh-0.1.15.dev2.dist-info → shotgun_sh-0.2.1.dev1.dist-info}/METADATA +2 -2
- {shotgun_sh-0.1.15.dev2.dist-info → shotgun_sh-0.2.1.dev1.dist-info}/RECORD +39 -28
- shotgun/agents/history/token_counting.py +0 -429
- {shotgun_sh-0.1.15.dev2.dist-info → shotgun_sh-0.2.1.dev1.dist-info}/WHEEL +0 -0
- {shotgun_sh-0.1.15.dev2.dist-info → shotgun_sh-0.2.1.dev1.dist-info}/entry_points.txt +0 -0
- {shotgun_sh-0.1.15.dev2.dist-info → shotgun_sh-0.2.1.dev1.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,20 +1,24 @@
|
|
|
1
1
|
"""Anthropic web search tool implementation."""
|
|
2
2
|
|
|
3
|
-
import anthropic
|
|
4
3
|
from opentelemetry import trace
|
|
4
|
+
from pydantic_ai.messages import ModelMessage, ModelRequest, TextPart
|
|
5
|
+
from pydantic_ai.settings import ModelSettings
|
|
5
6
|
|
|
6
7
|
from shotgun.agents.config import get_provider_model
|
|
8
|
+
from shotgun.agents.config.constants import MEDIUM_TEXT_8K_TOKENS
|
|
7
9
|
from shotgun.agents.config.models import ProviderType
|
|
10
|
+
from shotgun.agents.llm import shotgun_model_request
|
|
8
11
|
from shotgun.logging_config import get_logger
|
|
9
12
|
|
|
10
13
|
logger = get_logger(__name__)
|
|
11
14
|
|
|
12
15
|
|
|
13
|
-
def anthropic_web_search_tool(query: str) -> str:
|
|
14
|
-
"""Perform a web search using Anthropic's Claude API
|
|
16
|
+
async def anthropic_web_search_tool(query: str) -> str:
|
|
17
|
+
"""Perform a web search using Anthropic's Claude API.
|
|
15
18
|
|
|
16
19
|
This tool uses Anthropic's web search capabilities to find current information
|
|
17
|
-
about the given query.
|
|
20
|
+
about the given query. Works with both Shotgun API keys (via LiteLLM proxy)
|
|
21
|
+
and direct Anthropic API keys (BYOK).
|
|
18
22
|
|
|
19
23
|
Args:
|
|
20
24
|
query: The search query
|
|
@@ -27,49 +31,49 @@ def anthropic_web_search_tool(query: str) -> str:
|
|
|
27
31
|
span = trace.get_current_span()
|
|
28
32
|
span.set_attribute("input.value", f"**Query:** {query}\n")
|
|
29
33
|
|
|
30
|
-
logger.debug("📡 Executing Anthropic web search with
|
|
34
|
+
logger.debug("📡 Executing Anthropic web search with prompt: %s", query)
|
|
31
35
|
|
|
32
|
-
# Get
|
|
36
|
+
# Get model configuration (supports both Shotgun and BYOK)
|
|
33
37
|
try:
|
|
34
38
|
model_config = get_provider_model(ProviderType.ANTHROPIC)
|
|
35
|
-
api_key = model_config.api_key
|
|
36
39
|
except ValueError as e:
|
|
37
40
|
error_msg = f"Anthropic API key not configured: {str(e)}"
|
|
38
41
|
logger.error("❌ %s", error_msg)
|
|
39
42
|
span.set_attribute("output.value", f"**Error:**\n {error_msg}\n")
|
|
40
43
|
return error_msg
|
|
41
44
|
|
|
42
|
-
|
|
45
|
+
# Build the request messages
|
|
46
|
+
messages: list[ModelMessage] = [
|
|
47
|
+
ModelRequest.user_text_prompt(f"Search for: {query}")
|
|
48
|
+
]
|
|
43
49
|
|
|
44
|
-
# Use the Messages API with web search tool
|
|
50
|
+
# Use the Messages API with web search tool
|
|
45
51
|
try:
|
|
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
|
-
if not result_text:
|
|
72
|
-
result_text = "No content returned from search"
|
|
52
|
+
response = await shotgun_model_request(
|
|
53
|
+
model_config=model_config,
|
|
54
|
+
messages=messages,
|
|
55
|
+
model_settings=ModelSettings(
|
|
56
|
+
max_tokens=MEDIUM_TEXT_8K_TOKENS,
|
|
57
|
+
# Enable Anthropic web search tool
|
|
58
|
+
extra_body={
|
|
59
|
+
"tools": [
|
|
60
|
+
{
|
|
61
|
+
"type": "web_search_20250305",
|
|
62
|
+
"name": "web_search",
|
|
63
|
+
}
|
|
64
|
+
],
|
|
65
|
+
"tool_choice": {"type": "tool", "name": "web_search"},
|
|
66
|
+
},
|
|
67
|
+
),
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
# Extract text from response
|
|
71
|
+
result_text = "No content returned from search"
|
|
72
|
+
if response.parts:
|
|
73
|
+
for part in response.parts:
|
|
74
|
+
if isinstance(part, TextPart):
|
|
75
|
+
result_text = part.content
|
|
76
|
+
break
|
|
73
77
|
|
|
74
78
|
logger.debug("📄 Anthropic web search result: %d characters", len(result_text))
|
|
75
79
|
logger.debug(
|
|
@@ -88,7 +92,7 @@ def anthropic_web_search_tool(query: str) -> str:
|
|
|
88
92
|
return error_msg
|
|
89
93
|
|
|
90
94
|
|
|
91
|
-
def main() -> None:
|
|
95
|
+
async def main() -> None:
|
|
92
96
|
"""Main function for testing the Anthropic web search tool."""
|
|
93
97
|
import os
|
|
94
98
|
import sys
|
|
@@ -110,7 +114,7 @@ def main() -> None:
|
|
|
110
114
|
# Join all arguments as the search query
|
|
111
115
|
query = " ".join(sys.argv[1:])
|
|
112
116
|
|
|
113
|
-
print("🔍 Testing Anthropic Web Search
|
|
117
|
+
print("🔍 Testing Anthropic Web Search")
|
|
114
118
|
print(f"📝 Query: {query}")
|
|
115
119
|
print("=" * 60)
|
|
116
120
|
|
|
@@ -127,7 +131,7 @@ def main() -> None:
|
|
|
127
131
|
sys.exit(1)
|
|
128
132
|
|
|
129
133
|
try:
|
|
130
|
-
result = anthropic_web_search_tool(query)
|
|
134
|
+
result = await anthropic_web_search_tool(query)
|
|
131
135
|
print(f"✅ Search completed! Result length: {len(result)} characters")
|
|
132
136
|
print("=" * 60)
|
|
133
137
|
print("📄 RESULTS:")
|
|
@@ -141,4 +145,6 @@ def main() -> None:
|
|
|
141
145
|
|
|
142
146
|
|
|
143
147
|
if __name__ == "__main__":
|
|
144
|
-
|
|
148
|
+
import asyncio
|
|
149
|
+
|
|
150
|
+
asyncio.run(main())
|
|
@@ -1,20 +1,24 @@
|
|
|
1
1
|
"""Gemini web search tool implementation."""
|
|
2
2
|
|
|
3
|
-
import google.generativeai as genai
|
|
4
3
|
from opentelemetry import trace
|
|
4
|
+
from pydantic_ai.messages import ModelMessage, ModelRequest
|
|
5
|
+
from pydantic_ai.settings import ModelSettings
|
|
5
6
|
|
|
6
7
|
from shotgun.agents.config import get_provider_model
|
|
7
|
-
from shotgun.agents.config.
|
|
8
|
+
from shotgun.agents.config.constants import MEDIUM_TEXT_8K_TOKENS
|
|
9
|
+
from shotgun.agents.config.models import ModelName
|
|
10
|
+
from shotgun.agents.llm import shotgun_model_request
|
|
8
11
|
from shotgun.logging_config import get_logger
|
|
9
12
|
|
|
10
13
|
logger = get_logger(__name__)
|
|
11
14
|
|
|
12
15
|
|
|
13
|
-
def gemini_web_search_tool(query: str) -> str:
|
|
16
|
+
async def gemini_web_search_tool(query: str) -> str:
|
|
14
17
|
"""Perform a web search using Google's Gemini API with grounding.
|
|
15
18
|
|
|
16
19
|
This tool uses Gemini's Google Search grounding to find current information
|
|
17
|
-
about the given query.
|
|
20
|
+
about the given query. Works with both Shotgun API keys (via LiteLLM proxy)
|
|
21
|
+
and direct Gemini API keys (BYOK).
|
|
18
22
|
|
|
19
23
|
Args:
|
|
20
24
|
query: The search query
|
|
@@ -29,23 +33,16 @@ def gemini_web_search_tool(query: str) -> str:
|
|
|
29
33
|
|
|
30
34
|
logger.debug("📡 Executing Gemini web search with prompt: %s", query)
|
|
31
35
|
|
|
32
|
-
# Get
|
|
36
|
+
# Get model configuration (supports both Shotgun and BYOK)
|
|
33
37
|
try:
|
|
34
|
-
model_config = get_provider_model(
|
|
35
|
-
api_key = model_config.api_key
|
|
38
|
+
model_config = get_provider_model(ModelName.GEMINI_2_5_FLASH)
|
|
36
39
|
except ValueError as e:
|
|
37
40
|
error_msg = f"Gemini API key not configured: {str(e)}"
|
|
38
41
|
logger.error("❌ %s", error_msg)
|
|
39
42
|
span.set_attribute("output.value", f"**Error:**\n {error_msg}\n")
|
|
40
43
|
return error_msg
|
|
41
44
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
# Create model without built-in tools to avoid conflict with Pydantic AI
|
|
45
|
-
# Using prompt-based search approach instead
|
|
46
|
-
model = genai.GenerativeModel("gemini-2.5-pro") # type: ignore[attr-defined]
|
|
47
|
-
|
|
48
|
-
# Create a search-optimized prompt that leverages Gemini's knowledge
|
|
45
|
+
# Create a search-optimized prompt
|
|
49
46
|
search_prompt = f"""Please provide current and accurate information about the following query:
|
|
50
47
|
|
|
51
48
|
Query: {query}
|
|
@@ -56,17 +53,31 @@ Instructions:
|
|
|
56
53
|
- Focus on current and recent information
|
|
57
54
|
- Be specific and accurate in your response"""
|
|
58
55
|
|
|
59
|
-
#
|
|
56
|
+
# Build the request messages
|
|
57
|
+
messages: list[ModelMessage] = [ModelRequest.user_text_prompt(search_prompt)]
|
|
58
|
+
|
|
59
|
+
# Generate response using Pydantic AI with Google Search grounding
|
|
60
60
|
try:
|
|
61
|
-
response =
|
|
62
|
-
|
|
63
|
-
|
|
61
|
+
response = await shotgun_model_request(
|
|
62
|
+
model_config=model_config,
|
|
63
|
+
messages=messages,
|
|
64
|
+
model_settings=ModelSettings(
|
|
64
65
|
temperature=0.3,
|
|
65
|
-
|
|
66
|
+
max_tokens=MEDIUM_TEXT_8K_TOKENS,
|
|
67
|
+
# Enable Google Search grounding for Gemini
|
|
68
|
+
extra_body={"tools": [{"googleSearch": {}}]},
|
|
66
69
|
),
|
|
67
70
|
)
|
|
68
71
|
|
|
69
|
-
|
|
72
|
+
# Extract text from response
|
|
73
|
+
from pydantic_ai.messages import TextPart
|
|
74
|
+
|
|
75
|
+
result_text = "No content returned from search"
|
|
76
|
+
if response.parts:
|
|
77
|
+
for part in response.parts:
|
|
78
|
+
if isinstance(part, TextPart):
|
|
79
|
+
result_text = part.content
|
|
80
|
+
break
|
|
70
81
|
|
|
71
82
|
logger.debug("📄 Gemini web search result: %d characters", len(result_text))
|
|
72
83
|
logger.debug(
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""OpenAI web search tool implementation."""
|
|
2
2
|
|
|
3
|
-
from openai import
|
|
3
|
+
from openai import AsyncOpenAI
|
|
4
4
|
from opentelemetry import trace
|
|
5
5
|
|
|
6
6
|
from shotgun.agents.config import get_provider_model
|
|
@@ -10,7 +10,7 @@ from shotgun.logging_config import get_logger
|
|
|
10
10
|
logger = get_logger(__name__)
|
|
11
11
|
|
|
12
12
|
|
|
13
|
-
def openai_web_search_tool(query: str) -> str:
|
|
13
|
+
async def openai_web_search_tool(query: str) -> str:
|
|
14
14
|
"""Perform a web search and return results.
|
|
15
15
|
|
|
16
16
|
This tool uses OpenAI's web search capabilities to find current information
|
|
@@ -54,8 +54,8 @@ Instructions:
|
|
|
54
54
|
ALWAYS PROVIDE THE SOURCES (urls) TO BACK UP THE INFORMATION YOU PROVIDE.
|
|
55
55
|
"""
|
|
56
56
|
|
|
57
|
-
client =
|
|
58
|
-
response = client.responses.create( # type: ignore[call-overload]
|
|
57
|
+
client = AsyncOpenAI(api_key=api_key)
|
|
58
|
+
response = await client.responses.create( # type: ignore[call-overload]
|
|
59
59
|
model="gpt-5-mini",
|
|
60
60
|
input=[
|
|
61
61
|
{"role": "user", "content": [{"type": "input_text", "text": prompt}]}
|
shotgun/cli/config.py
CHANGED
|
@@ -43,11 +43,11 @@ def init(
|
|
|
43
43
|
console.print()
|
|
44
44
|
|
|
45
45
|
# Initialize with defaults
|
|
46
|
-
|
|
46
|
+
config_manager.initialize()
|
|
47
47
|
|
|
48
|
-
# Ask for
|
|
48
|
+
# Ask for provider
|
|
49
49
|
provider_choices = ["openai", "anthropic", "google"]
|
|
50
|
-
console.print("Choose your
|
|
50
|
+
console.print("Choose your AI provider:")
|
|
51
51
|
for i, provider in enumerate(provider_choices, 1):
|
|
52
52
|
console.print(f" {i}. {provider}")
|
|
53
53
|
|
|
@@ -55,7 +55,7 @@ def init(
|
|
|
55
55
|
try:
|
|
56
56
|
choice = typer.prompt("Enter choice (1-3)", type=int)
|
|
57
57
|
if 1 <= choice <= 3:
|
|
58
|
-
|
|
58
|
+
provider = ProviderType(provider_choices[choice - 1])
|
|
59
59
|
break
|
|
60
60
|
else:
|
|
61
61
|
console.print(
|
|
@@ -65,7 +65,6 @@ def init(
|
|
|
65
65
|
console.print("❌ Please enter a valid number.", style="red")
|
|
66
66
|
|
|
67
67
|
# Ask for API key for the selected provider
|
|
68
|
-
provider = config.default_provider
|
|
69
68
|
console.print(f"\n🔑 Setting up {provider.upper()} API key...")
|
|
70
69
|
|
|
71
70
|
api_key = typer.prompt(
|
|
@@ -75,9 +74,9 @@ def init(
|
|
|
75
74
|
)
|
|
76
75
|
|
|
77
76
|
if api_key:
|
|
77
|
+
# update_provider will automatically set selected_model for first provider
|
|
78
78
|
config_manager.update_provider(provider, api_key=api_key)
|
|
79
79
|
|
|
80
|
-
config_manager.save()
|
|
81
80
|
console.print(
|
|
82
81
|
f"\n✅ [bold green]Configuration saved to {config_manager.config_path}[/bold green]"
|
|
83
82
|
)
|
|
@@ -98,16 +97,12 @@ def set(
|
|
|
98
97
|
str | None,
|
|
99
98
|
typer.Option("--api-key", "-k", help="API key for the provider"),
|
|
100
99
|
] = None,
|
|
101
|
-
default: Annotated[
|
|
102
|
-
bool,
|
|
103
|
-
typer.Option("--default", "-d", help="Set this provider as default"),
|
|
104
|
-
] = False,
|
|
105
100
|
) -> None:
|
|
106
101
|
"""Set configuration for a specific provider."""
|
|
107
102
|
config_manager = get_config_manager()
|
|
108
103
|
|
|
109
|
-
# If no API key provided via option
|
|
110
|
-
if api_key is None
|
|
104
|
+
# If no API key provided via option, prompt for it
|
|
105
|
+
if api_key is None:
|
|
111
106
|
api_key = typer.prompt(
|
|
112
107
|
f"Enter your {provider.upper()} API key",
|
|
113
108
|
hide_input=True,
|
|
@@ -118,11 +113,6 @@ def set(
|
|
|
118
113
|
if api_key:
|
|
119
114
|
config_manager.update_provider(provider, api_key=api_key)
|
|
120
115
|
|
|
121
|
-
if default:
|
|
122
|
-
config = config_manager.load()
|
|
123
|
-
config.default_provider = provider
|
|
124
|
-
config_manager.save(config)
|
|
125
|
-
|
|
126
116
|
console.print(f"✅ Configuration updated for {provider}")
|
|
127
117
|
|
|
128
118
|
except Exception as e:
|
|
@@ -130,41 +120,6 @@ def set(
|
|
|
130
120
|
raise typer.Exit(1) from e
|
|
131
121
|
|
|
132
122
|
|
|
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
123
|
@app.command()
|
|
169
124
|
def get(
|
|
170
125
|
provider: Annotated[
|
|
@@ -201,8 +156,9 @@ def _show_full_config(config: Any) -> None:
|
|
|
201
156
|
table.add_column("Setting", style="cyan")
|
|
202
157
|
table.add_column("Value", style="white")
|
|
203
158
|
|
|
204
|
-
#
|
|
205
|
-
|
|
159
|
+
# Selected model
|
|
160
|
+
selected_model = config.selected_model or "None (will auto-detect)"
|
|
161
|
+
table.add_row("Selected Model", f"[bold]{selected_model}[/bold]")
|
|
206
162
|
table.add_row("", "") # Separator
|
|
207
163
|
|
|
208
164
|
# Provider configurations
|
|
@@ -210,6 +166,7 @@ def _show_full_config(config: Any) -> None:
|
|
|
210
166
|
("OpenAI", config.openai),
|
|
211
167
|
("Anthropic", config.anthropic),
|
|
212
168
|
("Google", config.google),
|
|
169
|
+
("Shotgun Account", config.shotgun),
|
|
213
170
|
]:
|
|
214
171
|
table.add_row(f"[bold]{provider_name}[/bold]", "")
|
|
215
172
|
|
|
@@ -231,6 +188,8 @@ def _show_provider_config(provider: ProviderType, config: Any) -> None:
|
|
|
231
188
|
provider_config = config.anthropic
|
|
232
189
|
elif provider_str == "google":
|
|
233
190
|
provider_config = config.google
|
|
191
|
+
elif provider_str == "shotgun":
|
|
192
|
+
provider_config = config.shotgun
|
|
234
193
|
else:
|
|
235
194
|
console.print(f"❌ Unknown provider: {provider}", style="red")
|
|
236
195
|
return
|
|
@@ -248,7 +207,7 @@ def _show_provider_config(provider: ProviderType, config: Any) -> None:
|
|
|
248
207
|
|
|
249
208
|
def _mask_secrets(data: dict[str, Any]) -> None:
|
|
250
209
|
"""Mask secrets in configuration data."""
|
|
251
|
-
for provider in ["openai", "anthropic", "google"]:
|
|
210
|
+
for provider in ["openai", "anthropic", "google", "shotgun"]:
|
|
252
211
|
if provider in data and isinstance(data[provider], dict):
|
|
253
212
|
if "api_key" in data[provider] and data[provider]["api_key"]:
|
|
254
213
|
data[provider]["api_key"] = _mask_value(data[provider]["api_key"])
|
shotgun/cli/feedback.py
CHANGED
shotgun/cli/models.py
CHANGED
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
|
|
@@ -137,7 +137,7 @@ def shutdown() -> None:
|
|
|
137
137
|
_posthog_client = None
|
|
138
138
|
|
|
139
139
|
|
|
140
|
-
class FeedbackKind(
|
|
140
|
+
class FeedbackKind(StrEnum):
|
|
141
141
|
BUG = "bug"
|
|
142
142
|
FEATURE = "feature"
|
|
143
143
|
OTHER = "other"
|
|
@@ -178,7 +178,9 @@ def submit_feedback_survey(feedback: Feedback) -> None:
|
|
|
178
178
|
],
|
|
179
179
|
f"$survey_response_{Q_KIND_ID}": feedback.kind,
|
|
180
180
|
f"$survey_response_{Q_DESCRIPTION_ID}": feedback.description,
|
|
181
|
-
"
|
|
181
|
+
"selected_model": config.selected_model.value
|
|
182
|
+
if config.selected_model
|
|
183
|
+
else None,
|
|
182
184
|
"config_version": config.config_version,
|
|
183
185
|
"last_10_messages": last_10_messages, # last 10 messages
|
|
184
186
|
},
|
shotgun/tui/app.py
CHANGED
|
@@ -14,6 +14,7 @@ from shotgun.utils.update_checker import perform_auto_update_async
|
|
|
14
14
|
from .screens.chat import ChatScreen
|
|
15
15
|
from .screens.directory_setup import DirectorySetupScreen
|
|
16
16
|
from .screens.feedback import FeedbackScreen
|
|
17
|
+
from .screens.model_picker import ModelPickerScreen
|
|
17
18
|
from .screens.provider_config import ProviderConfigScreen
|
|
18
19
|
|
|
19
20
|
logger = get_logger(__name__)
|
|
@@ -23,6 +24,7 @@ class ShotgunApp(App[None]):
|
|
|
23
24
|
SCREENS = {
|
|
24
25
|
"chat": ChatScreen,
|
|
25
26
|
"provider_config": ProviderConfigScreen,
|
|
27
|
+
"model_picker": ModelPickerScreen,
|
|
26
28
|
"directory_setup": DirectorySetupScreen,
|
|
27
29
|
"feedback": FeedbackScreen,
|
|
28
30
|
}
|
|
@@ -106,7 +108,7 @@ class ShotgunApp(App[None]):
|
|
|
106
108
|
def handle_feedback(feedback: Feedback | None) -> None:
|
|
107
109
|
if feedback is not None:
|
|
108
110
|
submit_feedback_survey(feedback)
|
|
109
|
-
self.notify("Feedback sent. Thank you!")
|
|
111
|
+
self.notify("✅ Feedback sent. Thank you!")
|
|
110
112
|
|
|
111
113
|
self.push_screen("feedback", callback=handle_feedback)
|
|
112
114
|
|
|
@@ -109,15 +109,25 @@ class ProviderSetupProvider(Provider):
|
|
|
109
109
|
"""Show the provider configuration screen."""
|
|
110
110
|
self.chat_screen.app.push_screen("provider_config")
|
|
111
111
|
|
|
112
|
+
def open_model_picker(self) -> None:
|
|
113
|
+
"""Show the model picker screen."""
|
|
114
|
+
self.chat_screen.app.push_screen("model_picker")
|
|
115
|
+
|
|
112
116
|
async def discover(self) -> AsyncGenerator[DiscoveryHit, None]:
|
|
113
117
|
yield DiscoveryHit(
|
|
114
118
|
"Open Provider Setup",
|
|
115
119
|
self.open_provider_config,
|
|
116
120
|
help="⚙️ Manage API keys for available providers",
|
|
117
121
|
)
|
|
122
|
+
yield DiscoveryHit(
|
|
123
|
+
"Select AI Model",
|
|
124
|
+
self.open_model_picker,
|
|
125
|
+
help="🤖 Choose which AI model to use",
|
|
126
|
+
)
|
|
118
127
|
|
|
119
128
|
async def search(self, query: str) -> AsyncGenerator[Hit, None]:
|
|
120
129
|
matcher = self.matcher(query)
|
|
130
|
+
|
|
121
131
|
title = "Open Provider Setup"
|
|
122
132
|
score = matcher.match(title)
|
|
123
133
|
if score > 0:
|
|
@@ -128,6 +138,16 @@ class ProviderSetupProvider(Provider):
|
|
|
128
138
|
help="⚙️ Manage API keys for available providers",
|
|
129
139
|
)
|
|
130
140
|
|
|
141
|
+
title = "Select AI Model"
|
|
142
|
+
score = matcher.match(title)
|
|
143
|
+
if score > 0:
|
|
144
|
+
yield Hit(
|
|
145
|
+
score,
|
|
146
|
+
matcher.highlight(title),
|
|
147
|
+
self.open_model_picker,
|
|
148
|
+
help="🤖 Choose which AI model to use",
|
|
149
|
+
)
|
|
150
|
+
|
|
131
151
|
|
|
132
152
|
class CodebaseCommandProvider(Provider):
|
|
133
153
|
"""Command palette entries for codebase management."""
|