shotgun-sh 0.1.0.dev2__py3-none-any.whl → 0.1.0.dev4__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 +2 -1
- shotgun/agents/config/models.py +62 -57
- shotgun/agents/config/provider.py +97 -17
- shotgun/agents/research.py +13 -2
- shotgun/agents/tools/__init__.py +10 -2
- shotgun/agents/tools/web_search/__init__.py +60 -0
- shotgun/agents/tools/web_search/anthropic.py +86 -0
- shotgun/agents/tools/web_search/gemini.py +85 -0
- shotgun/agents/tools/{web_search.py → web_search/openai.py} +23 -8
- shotgun/agents/tools/web_search/utils.py +20 -0
- shotgun/cli/research.py +3 -7
- shotgun/codebase/core/nl_query.py +2 -1
- shotgun/codebase/core/parser_loader.py +4 -28
- shotgun/main.py +10 -5
- shotgun/telemetry.py +20 -44
- shotgun/tui/screens/chat.py +22 -4
- shotgun/tui/screens/chat.tcss +4 -0
- {shotgun_sh-0.1.0.dev2.dist-info → shotgun_sh-0.1.0.dev4.dist-info}/METADATA +5 -6
- {shotgun_sh-0.1.0.dev2.dist-info → shotgun_sh-0.1.0.dev4.dist-info}/RECORD +22 -18
- {shotgun_sh-0.1.0.dev2.dist-info → shotgun_sh-0.1.0.dev4.dist-info}/WHEEL +0 -0
- {shotgun_sh-0.1.0.dev2.dist-info → shotgun_sh-0.1.0.dev4.dist-info}/entry_points.txt +0 -0
- {shotgun_sh-0.1.0.dev2.dist-info → shotgun_sh-0.1.0.dev4.dist-info}/licenses/LICENSE +0 -0
shotgun/agents/common.py
CHANGED
|
@@ -170,7 +170,8 @@ def create_base_agent(
|
|
|
170
170
|
provider_name.upper(),
|
|
171
171
|
model_config.name,
|
|
172
172
|
)
|
|
173
|
-
|
|
173
|
+
# Use the Model instance directly (has API key baked in)
|
|
174
|
+
model = model_config.model_instance
|
|
174
175
|
|
|
175
176
|
# Create deps with model config and codebase service
|
|
176
177
|
codebase_service = get_codebase_service()
|
shotgun/agents/config/models.py
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
from enum import Enum
|
|
4
4
|
|
|
5
|
-
from pydantic import BaseModel, Field, SecretStr
|
|
5
|
+
from pydantic import BaseModel, Field, PrivateAttr, SecretStr
|
|
6
|
+
from pydantic_ai.models import Model
|
|
6
7
|
|
|
7
8
|
|
|
8
9
|
class ProviderType(str, Enum):
|
|
@@ -13,17 +14,42 @@ class ProviderType(str, Enum):
|
|
|
13
14
|
GOOGLE = "google"
|
|
14
15
|
|
|
15
16
|
|
|
17
|
+
class ModelSpec(BaseModel):
|
|
18
|
+
"""Static specification for a model - just metadata."""
|
|
19
|
+
|
|
20
|
+
name: str # Model identifier (e.g., "gpt-5", "claude-opus-4-1")
|
|
21
|
+
provider: ProviderType
|
|
22
|
+
max_input_tokens: int
|
|
23
|
+
max_output_tokens: int
|
|
24
|
+
|
|
25
|
+
|
|
16
26
|
class ModelConfig(BaseModel):
|
|
17
|
-
"""
|
|
27
|
+
"""A fully configured model with API key and settings."""
|
|
18
28
|
|
|
19
29
|
name: str # Model identifier (e.g., "gpt-5", "claude-opus-4-1")
|
|
20
30
|
provider: ProviderType
|
|
21
31
|
max_input_tokens: int
|
|
22
32
|
max_output_tokens: int
|
|
33
|
+
api_key: str
|
|
34
|
+
_model_instance: Model | None = PrivateAttr(default=None)
|
|
35
|
+
|
|
36
|
+
class Config:
|
|
37
|
+
arbitrary_types_allowed = True
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def model_instance(self) -> Model:
|
|
41
|
+
"""Lazy load the Model instance."""
|
|
42
|
+
if self._model_instance is None:
|
|
43
|
+
from .provider import get_or_create_model
|
|
44
|
+
|
|
45
|
+
self._model_instance = get_or_create_model(
|
|
46
|
+
self.provider, self.name, self.api_key
|
|
47
|
+
)
|
|
48
|
+
return self._model_instance
|
|
23
49
|
|
|
24
50
|
@property
|
|
25
51
|
def pydantic_model_name(self) -> str:
|
|
26
|
-
"""Compute the full Pydantic AI model identifier."""
|
|
52
|
+
"""Compute the full Pydantic AI model identifier. For backward compatibility."""
|
|
27
53
|
provider_prefix = {
|
|
28
54
|
ProviderType.OPENAI: "openai",
|
|
29
55
|
ProviderType.ANTHROPIC: "anthropic",
|
|
@@ -32,60 +58,39 @@ class ModelConfig(BaseModel):
|
|
|
32
58
|
return f"{provider_prefix[self.provider]}:{self.name}"
|
|
33
59
|
|
|
34
60
|
|
|
35
|
-
#
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
)
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
provider=ProviderType.GOOGLE,
|
|
69
|
-
max_input_tokens=1_000_000,
|
|
70
|
-
max_output_tokens=64_000,
|
|
71
|
-
)
|
|
72
|
-
|
|
73
|
-
# List of all available models
|
|
74
|
-
AVAILABLE_MODELS = [
|
|
75
|
-
GPT_5,
|
|
76
|
-
GPT_4O,
|
|
77
|
-
CLAUDE_OPUS_4_1,
|
|
78
|
-
CLAUDE_3_5_SONNET,
|
|
79
|
-
GEMINI_2_5_PRO,
|
|
80
|
-
]
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
def get_model_by_name(name: str) -> ModelConfig:
|
|
84
|
-
"""Find a model configuration by name."""
|
|
85
|
-
for model in AVAILABLE_MODELS:
|
|
86
|
-
if model.name == name:
|
|
87
|
-
return model
|
|
88
|
-
raise ValueError(f"Model '{name}' not found")
|
|
61
|
+
# Model specifications registry (static metadata)
|
|
62
|
+
MODEL_SPECS: dict[str, ModelSpec] = {
|
|
63
|
+
"gpt-5": ModelSpec(
|
|
64
|
+
name="gpt-5",
|
|
65
|
+
provider=ProviderType.OPENAI,
|
|
66
|
+
max_input_tokens=400_000,
|
|
67
|
+
max_output_tokens=128_000,
|
|
68
|
+
),
|
|
69
|
+
"gpt-4o": ModelSpec(
|
|
70
|
+
name="gpt-4o",
|
|
71
|
+
provider=ProviderType.OPENAI,
|
|
72
|
+
max_input_tokens=128_000,
|
|
73
|
+
max_output_tokens=16_000,
|
|
74
|
+
),
|
|
75
|
+
"claude-opus-4-1": ModelSpec(
|
|
76
|
+
name="claude-opus-4-1",
|
|
77
|
+
provider=ProviderType.ANTHROPIC,
|
|
78
|
+
max_input_tokens=200_000,
|
|
79
|
+
max_output_tokens=32_000,
|
|
80
|
+
),
|
|
81
|
+
"claude-3-5-sonnet-latest": ModelSpec(
|
|
82
|
+
name="claude-3-5-sonnet-latest",
|
|
83
|
+
provider=ProviderType.ANTHROPIC,
|
|
84
|
+
max_input_tokens=200_000,
|
|
85
|
+
max_output_tokens=20_000,
|
|
86
|
+
),
|
|
87
|
+
"gemini-2.5-pro": ModelSpec(
|
|
88
|
+
name="gemini-2.5-pro",
|
|
89
|
+
provider=ProviderType.GOOGLE,
|
|
90
|
+
max_input_tokens=1_000_000,
|
|
91
|
+
max_output_tokens=64_000,
|
|
92
|
+
),
|
|
93
|
+
}
|
|
89
94
|
|
|
90
95
|
|
|
91
96
|
class OpenAIConfig(BaseModel):
|
|
@@ -3,23 +3,73 @@
|
|
|
3
3
|
import os
|
|
4
4
|
|
|
5
5
|
from pydantic import SecretStr
|
|
6
|
+
from pydantic_ai.models import Model
|
|
7
|
+
from pydantic_ai.models.anthropic import AnthropicModel
|
|
8
|
+
from pydantic_ai.models.google import GoogleModel
|
|
9
|
+
from pydantic_ai.models.openai import OpenAIChatModel
|
|
10
|
+
from pydantic_ai.providers.anthropic import AnthropicProvider
|
|
11
|
+
from pydantic_ai.providers.google import GoogleProvider
|
|
12
|
+
from pydantic_ai.providers.openai import OpenAIProvider
|
|
6
13
|
|
|
7
14
|
from shotgun.logging_config import get_logger
|
|
8
15
|
|
|
9
16
|
from .manager import get_config_manager
|
|
10
|
-
from .models import ModelConfig, ProviderType
|
|
17
|
+
from .models import MODEL_SPECS, ModelConfig, ProviderType
|
|
11
18
|
|
|
12
19
|
logger = get_logger(__name__)
|
|
13
20
|
|
|
21
|
+
# Global cache for Model instances (singleton pattern)
|
|
22
|
+
_model_cache: dict[tuple[ProviderType, str, str], Model] = {}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def get_or_create_model(provider: ProviderType, model_name: str, api_key: str) -> Model:
|
|
26
|
+
"""Get or create a singleton Model instance.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
provider: Provider type
|
|
30
|
+
model_name: Name of the model
|
|
31
|
+
api_key: API key for the provider
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
Cached or newly created Model instance
|
|
35
|
+
|
|
36
|
+
Raises:
|
|
37
|
+
ValueError: If provider is not supported
|
|
38
|
+
"""
|
|
39
|
+
cache_key = (provider, model_name, api_key)
|
|
40
|
+
|
|
41
|
+
if cache_key not in _model_cache:
|
|
42
|
+
logger.debug("Creating new %s model instance: %s", provider.value, model_name)
|
|
43
|
+
|
|
44
|
+
if provider == ProviderType.OPENAI:
|
|
45
|
+
openai_provider = OpenAIProvider(api_key=api_key)
|
|
46
|
+
_model_cache[cache_key] = OpenAIChatModel(
|
|
47
|
+
model_name, provider=openai_provider
|
|
48
|
+
)
|
|
49
|
+
elif provider == ProviderType.ANTHROPIC:
|
|
50
|
+
anthropic_provider = AnthropicProvider(api_key=api_key)
|
|
51
|
+
_model_cache[cache_key] = AnthropicModel(
|
|
52
|
+
model_name, provider=anthropic_provider
|
|
53
|
+
)
|
|
54
|
+
elif provider == ProviderType.GOOGLE:
|
|
55
|
+
google_provider = GoogleProvider(api_key=api_key)
|
|
56
|
+
_model_cache[cache_key] = GoogleModel(model_name, provider=google_provider)
|
|
57
|
+
else:
|
|
58
|
+
raise ValueError(f"Unsupported provider: {provider}")
|
|
59
|
+
else:
|
|
60
|
+
logger.debug("Reusing cached %s model instance: %s", provider.value, model_name)
|
|
61
|
+
|
|
62
|
+
return _model_cache[cache_key]
|
|
63
|
+
|
|
14
64
|
|
|
15
65
|
def get_provider_model(provider: ProviderType | None = None) -> ModelConfig:
|
|
16
|
-
"""Get
|
|
66
|
+
"""Get a fully configured ModelConfig with API key and Model instance.
|
|
17
67
|
|
|
18
68
|
Args:
|
|
19
69
|
provider: Provider to get model for. If None, uses default provider
|
|
20
70
|
|
|
21
71
|
Returns:
|
|
22
|
-
ModelConfig with
|
|
72
|
+
ModelConfig with API key configured and lazy Model instance
|
|
23
73
|
|
|
24
74
|
Raises:
|
|
25
75
|
ValueError: If provider is not configured properly or model not found
|
|
@@ -41,11 +91,21 @@ def get_provider_model(provider: ProviderType | None = None) -> ModelConfig:
|
|
|
41
91
|
raise ValueError(
|
|
42
92
|
"OpenAI API key not configured. Set via environment variable OPENAI_API_KEY or config."
|
|
43
93
|
)
|
|
44
|
-
# Set the API key in environment if not already there
|
|
45
|
-
if "OPENAI_API_KEY" not in os.environ:
|
|
46
|
-
os.environ["OPENAI_API_KEY"] = api_key
|
|
47
94
|
|
|
48
|
-
|
|
95
|
+
# Get model spec
|
|
96
|
+
model_name = config.openai.model_name
|
|
97
|
+
if model_name not in MODEL_SPECS:
|
|
98
|
+
raise ValueError(f"Model '{model_name}' not found")
|
|
99
|
+
spec = MODEL_SPECS[model_name]
|
|
100
|
+
|
|
101
|
+
# Create fully configured ModelConfig
|
|
102
|
+
return ModelConfig(
|
|
103
|
+
name=spec.name,
|
|
104
|
+
provider=spec.provider,
|
|
105
|
+
max_input_tokens=spec.max_input_tokens,
|
|
106
|
+
max_output_tokens=spec.max_output_tokens,
|
|
107
|
+
api_key=api_key,
|
|
108
|
+
)
|
|
49
109
|
|
|
50
110
|
elif provider_enum == ProviderType.ANTHROPIC:
|
|
51
111
|
api_key = _get_api_key(config.anthropic.api_key, "ANTHROPIC_API_KEY")
|
|
@@ -53,23 +113,43 @@ def get_provider_model(provider: ProviderType | None = None) -> ModelConfig:
|
|
|
53
113
|
raise ValueError(
|
|
54
114
|
"Anthropic API key not configured. Set via environment variable ANTHROPIC_API_KEY or config."
|
|
55
115
|
)
|
|
56
|
-
# Set the API key in environment if not already there
|
|
57
|
-
if "ANTHROPIC_API_KEY" not in os.environ:
|
|
58
|
-
os.environ["ANTHROPIC_API_KEY"] = api_key
|
|
59
116
|
|
|
60
|
-
|
|
117
|
+
# Get model spec
|
|
118
|
+
model_name = config.anthropic.model_name
|
|
119
|
+
if model_name not in MODEL_SPECS:
|
|
120
|
+
raise ValueError(f"Model '{model_name}' not found")
|
|
121
|
+
spec = MODEL_SPECS[model_name]
|
|
122
|
+
|
|
123
|
+
# Create fully configured ModelConfig
|
|
124
|
+
return ModelConfig(
|
|
125
|
+
name=spec.name,
|
|
126
|
+
provider=spec.provider,
|
|
127
|
+
max_input_tokens=spec.max_input_tokens,
|
|
128
|
+
max_output_tokens=spec.max_output_tokens,
|
|
129
|
+
api_key=api_key,
|
|
130
|
+
)
|
|
61
131
|
|
|
62
132
|
elif provider_enum == ProviderType.GOOGLE:
|
|
63
|
-
api_key = _get_api_key(config.google.api_key, "
|
|
133
|
+
api_key = _get_api_key(config.google.api_key, "GEMINI_API_KEY")
|
|
64
134
|
if not api_key:
|
|
65
135
|
raise ValueError(
|
|
66
|
-
"
|
|
136
|
+
"Gemini API key not configured. Set via environment variable GEMINI_API_KEY or config."
|
|
67
137
|
)
|
|
68
|
-
# Set the API key in environment if not already there
|
|
69
|
-
if "GOOGLE_API_KEY" not in os.environ:
|
|
70
|
-
os.environ["GOOGLE_API_KEY"] = api_key
|
|
71
138
|
|
|
72
|
-
|
|
139
|
+
# Get model spec
|
|
140
|
+
model_name = config.google.model_name
|
|
141
|
+
if model_name not in MODEL_SPECS:
|
|
142
|
+
raise ValueError(f"Model '{model_name}' not found")
|
|
143
|
+
spec = MODEL_SPECS[model_name]
|
|
144
|
+
|
|
145
|
+
# Create fully configured ModelConfig
|
|
146
|
+
return ModelConfig(
|
|
147
|
+
name=spec.name,
|
|
148
|
+
provider=spec.provider,
|
|
149
|
+
max_input_tokens=spec.max_input_tokens,
|
|
150
|
+
max_output_tokens=spec.max_output_tokens,
|
|
151
|
+
api_key=api_key,
|
|
152
|
+
)
|
|
73
153
|
|
|
74
154
|
else:
|
|
75
155
|
raise ValueError(f"Unsupported provider: {provider_enum}")
|
shotgun/agents/research.py
CHANGED
|
@@ -23,7 +23,7 @@ from .common import (
|
|
|
23
23
|
run_agent,
|
|
24
24
|
)
|
|
25
25
|
from .models import AgentDeps, AgentRuntimeOptions
|
|
26
|
-
from .tools import
|
|
26
|
+
from .tools import get_available_web_search_tools
|
|
27
27
|
|
|
28
28
|
logger = get_logger(__name__)
|
|
29
29
|
|
|
@@ -60,11 +60,22 @@ def create_research_agent(
|
|
|
60
60
|
Tuple of (Configured Pydantic AI agent for research tasks, Agent dependencies)
|
|
61
61
|
"""
|
|
62
62
|
logger.debug("Initializing research agent")
|
|
63
|
+
|
|
64
|
+
# Get available web search tools based on configured API keys
|
|
65
|
+
web_search_tools = get_available_web_search_tools()
|
|
66
|
+
if web_search_tools:
|
|
67
|
+
logger.info(
|
|
68
|
+
"Research agent configured with %d web search tool(s)",
|
|
69
|
+
len(web_search_tools),
|
|
70
|
+
)
|
|
71
|
+
else:
|
|
72
|
+
logger.warning("Research agent configured without web search tools")
|
|
73
|
+
|
|
63
74
|
agent, deps = create_base_agent(
|
|
64
75
|
_build_research_agent_system_prompt,
|
|
65
76
|
agent_runtime_options,
|
|
66
77
|
load_codebase_understanding_tools=True,
|
|
67
|
-
additional_tools=
|
|
78
|
+
additional_tools=web_search_tools,
|
|
68
79
|
provider=provider,
|
|
69
80
|
)
|
|
70
81
|
return agent, deps
|
shotgun/agents/tools/__init__.py
CHANGED
|
@@ -9,10 +9,18 @@ from .codebase import (
|
|
|
9
9
|
)
|
|
10
10
|
from .file_management import append_file, read_file, write_file
|
|
11
11
|
from .user_interaction import ask_user
|
|
12
|
-
from .web_search import
|
|
12
|
+
from .web_search import (
|
|
13
|
+
anthropic_web_search_tool,
|
|
14
|
+
gemini_web_search_tool,
|
|
15
|
+
get_available_web_search_tools,
|
|
16
|
+
openai_web_search_tool,
|
|
17
|
+
)
|
|
13
18
|
|
|
14
19
|
__all__ = [
|
|
15
|
-
"
|
|
20
|
+
"openai_web_search_tool",
|
|
21
|
+
"anthropic_web_search_tool",
|
|
22
|
+
"gemini_web_search_tool",
|
|
23
|
+
"get_available_web_search_tools",
|
|
16
24
|
"ask_user",
|
|
17
25
|
"read_file",
|
|
18
26
|
"write_file",
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""Web search tools for Pydantic AI agents.
|
|
2
|
+
|
|
3
|
+
Provides web search capabilities for multiple LLM providers:
|
|
4
|
+
- OpenAI: Uses Responses API with web_search tool
|
|
5
|
+
- Anthropic: Uses Messages API with web_search_20250305 tool
|
|
6
|
+
- Gemini: Uses grounding with Google Search
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from collections.abc import Callable
|
|
10
|
+
|
|
11
|
+
from shotgun.agents.config.models import ProviderType
|
|
12
|
+
from shotgun.logging_config import get_logger
|
|
13
|
+
|
|
14
|
+
from .anthropic import anthropic_web_search_tool
|
|
15
|
+
from .gemini import gemini_web_search_tool
|
|
16
|
+
from .openai import openai_web_search_tool
|
|
17
|
+
from .utils import is_provider_available
|
|
18
|
+
|
|
19
|
+
logger = get_logger(__name__)
|
|
20
|
+
|
|
21
|
+
# Type alias for web search tools
|
|
22
|
+
WebSearchTool = Callable[[str], str]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def get_available_web_search_tools() -> list[WebSearchTool]:
|
|
26
|
+
"""Get list of available web search tools based on configured API keys.
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
List of web search tool functions that have API keys configured
|
|
30
|
+
"""
|
|
31
|
+
tools: list[WebSearchTool] = []
|
|
32
|
+
|
|
33
|
+
if is_provider_available(ProviderType.OPENAI):
|
|
34
|
+
logger.debug("✅ OpenAI web search tool available")
|
|
35
|
+
tools.append(openai_web_search_tool)
|
|
36
|
+
|
|
37
|
+
if is_provider_available(ProviderType.ANTHROPIC):
|
|
38
|
+
logger.debug("✅ Anthropic web search tool available")
|
|
39
|
+
tools.append(anthropic_web_search_tool)
|
|
40
|
+
|
|
41
|
+
if is_provider_available(ProviderType.GOOGLE):
|
|
42
|
+
logger.debug("✅ Gemini web search tool available")
|
|
43
|
+
tools.append(gemini_web_search_tool)
|
|
44
|
+
|
|
45
|
+
if not tools:
|
|
46
|
+
logger.warning("⚠️ No web search tools available - no API keys configured")
|
|
47
|
+
else:
|
|
48
|
+
logger.info("🔍 %d web search tool(s) available", len(tools))
|
|
49
|
+
|
|
50
|
+
return tools
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
__all__ = [
|
|
54
|
+
"openai_web_search_tool",
|
|
55
|
+
"anthropic_web_search_tool",
|
|
56
|
+
"gemini_web_search_tool",
|
|
57
|
+
"get_available_web_search_tools",
|
|
58
|
+
"is_provider_available",
|
|
59
|
+
"WebSearchTool",
|
|
60
|
+
]
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""Anthropic web search tool implementation."""
|
|
2
|
+
|
|
3
|
+
import anthropic
|
|
4
|
+
from opentelemetry import trace
|
|
5
|
+
|
|
6
|
+
from shotgun.agents.config import get_provider_model
|
|
7
|
+
from shotgun.agents.config.models import ProviderType
|
|
8
|
+
from shotgun.logging_config import get_logger
|
|
9
|
+
|
|
10
|
+
logger = get_logger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def anthropic_web_search_tool(query: str) -> str:
|
|
14
|
+
"""Perform a web search using Anthropic's Claude API.
|
|
15
|
+
|
|
16
|
+
This tool uses Anthropic's web search capabilities to find current information
|
|
17
|
+
about the given query.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
query: The search query
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
Search results as a formatted string
|
|
24
|
+
"""
|
|
25
|
+
logger.debug("🔧 Invoking Anthropic web_search_tool with query: %s", query)
|
|
26
|
+
|
|
27
|
+
span = trace.get_current_span()
|
|
28
|
+
span.set_attribute("input.value", f"**Query:** {query}\n")
|
|
29
|
+
|
|
30
|
+
logger.debug("📡 Executing Anthropic web search with prompt: %s", query)
|
|
31
|
+
|
|
32
|
+
# Get API key from centralized configuration
|
|
33
|
+
try:
|
|
34
|
+
model_config = get_provider_model(ProviderType.ANTHROPIC)
|
|
35
|
+
api_key = model_config.api_key
|
|
36
|
+
except ValueError as e:
|
|
37
|
+
error_msg = f"Anthropic API key not configured: {str(e)}"
|
|
38
|
+
logger.error("❌ %s", error_msg)
|
|
39
|
+
span.set_attribute("output.value", f"**Error:**\n {error_msg}\n")
|
|
40
|
+
return error_msg
|
|
41
|
+
|
|
42
|
+
client = anthropic.Anthropic(api_key=api_key)
|
|
43
|
+
|
|
44
|
+
# Use the Messages API with web search tool
|
|
45
|
+
try:
|
|
46
|
+
response = client.messages.create(
|
|
47
|
+
model="claude-3-5-sonnet-latest",
|
|
48
|
+
max_tokens=8192, # Increased from 4096 for more comprehensive results
|
|
49
|
+
messages=[{"role": "user", "content": f"Search for: {query}"}],
|
|
50
|
+
tools=[
|
|
51
|
+
{
|
|
52
|
+
"type": "web_search_20250305",
|
|
53
|
+
"name": "web_search",
|
|
54
|
+
}
|
|
55
|
+
],
|
|
56
|
+
tool_choice={"type": "tool", "name": "web_search"},
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
# Extract the search results from the response
|
|
60
|
+
result_text = ""
|
|
61
|
+
if hasattr(response, "content") and response.content:
|
|
62
|
+
for content in response.content:
|
|
63
|
+
if hasattr(content, "text"):
|
|
64
|
+
result_text += content.text
|
|
65
|
+
elif hasattr(content, "tool_use") and content.tool_use:
|
|
66
|
+
# Handle tool use response
|
|
67
|
+
result_text += f"Search performed for: {query}\n"
|
|
68
|
+
|
|
69
|
+
if not result_text:
|
|
70
|
+
result_text = "No content returned from search"
|
|
71
|
+
|
|
72
|
+
logger.debug("📄 Anthropic web search result: %d characters", len(result_text))
|
|
73
|
+
logger.debug(
|
|
74
|
+
"🔍 Result preview: %s...",
|
|
75
|
+
result_text[:100] if result_text else "No result",
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
span.set_attribute("output.value", f"**Results:**\n {result_text}\n")
|
|
79
|
+
|
|
80
|
+
return result_text
|
|
81
|
+
except Exception as e:
|
|
82
|
+
error_msg = f"Error performing Anthropic web search: {str(e)}"
|
|
83
|
+
logger.error("❌ Anthropic web search failed: %s", str(e))
|
|
84
|
+
logger.debug("💥 Full error details: %s", error_msg)
|
|
85
|
+
span.set_attribute("output.value", f"**Error:**\n {error_msg}\n")
|
|
86
|
+
return error_msg
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""Gemini web search tool implementation."""
|
|
2
|
+
|
|
3
|
+
import google.generativeai as genai
|
|
4
|
+
from opentelemetry import trace
|
|
5
|
+
|
|
6
|
+
from shotgun.agents.config import get_provider_model
|
|
7
|
+
from shotgun.agents.config.models import ProviderType
|
|
8
|
+
from shotgun.logging_config import get_logger
|
|
9
|
+
|
|
10
|
+
logger = get_logger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def gemini_web_search_tool(query: str) -> str:
|
|
14
|
+
"""Perform a web search using Google's Gemini API with grounding.
|
|
15
|
+
|
|
16
|
+
This tool uses Gemini's Google Search grounding to find current information
|
|
17
|
+
about the given query.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
query: The search query
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
Search results as a formatted string
|
|
24
|
+
"""
|
|
25
|
+
logger.debug("🔧 Invoking Gemini web_search_tool with query: %s", query)
|
|
26
|
+
|
|
27
|
+
span = trace.get_current_span()
|
|
28
|
+
span.set_attribute("input.value", f"**Query:** {query}\n")
|
|
29
|
+
|
|
30
|
+
logger.debug("📡 Executing Gemini web search with prompt: %s", query)
|
|
31
|
+
|
|
32
|
+
# Get API key from centralized configuration
|
|
33
|
+
try:
|
|
34
|
+
model_config = get_provider_model(ProviderType.GOOGLE)
|
|
35
|
+
api_key = model_config.api_key
|
|
36
|
+
except ValueError as e:
|
|
37
|
+
error_msg = f"Gemini API key not configured: {str(e)}"
|
|
38
|
+
logger.error("❌ %s", error_msg)
|
|
39
|
+
span.set_attribute("output.value", f"**Error:**\n {error_msg}\n")
|
|
40
|
+
return error_msg
|
|
41
|
+
|
|
42
|
+
genai.configure(api_key=api_key) # type: ignore[attr-defined]
|
|
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
|
|
49
|
+
search_prompt = f"""Please provide current and accurate information about the following query:
|
|
50
|
+
|
|
51
|
+
Query: {query}
|
|
52
|
+
|
|
53
|
+
Instructions:
|
|
54
|
+
- Provide comprehensive, factual information
|
|
55
|
+
- Include relevant details and context
|
|
56
|
+
- Focus on current and recent information
|
|
57
|
+
- Be specific and accurate in your response"""
|
|
58
|
+
|
|
59
|
+
# Generate response using the model's knowledge
|
|
60
|
+
try:
|
|
61
|
+
response = model.generate_content(
|
|
62
|
+
search_prompt,
|
|
63
|
+
generation_config=genai.GenerationConfig( # type: ignore[attr-defined]
|
|
64
|
+
temperature=0.3,
|
|
65
|
+
max_output_tokens=8192, # Explicit limit for comprehensive results
|
|
66
|
+
),
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
result_text = response.text or "No content returned from search"
|
|
70
|
+
|
|
71
|
+
logger.debug("📄 Gemini web search result: %d characters", len(result_text))
|
|
72
|
+
logger.debug(
|
|
73
|
+
"🔍 Result preview: %s...",
|
|
74
|
+
result_text[:100] if result_text else "No result",
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
span.set_attribute("output.value", f"**Results:**\n {result_text}\n")
|
|
78
|
+
|
|
79
|
+
return result_text
|
|
80
|
+
except Exception as e:
|
|
81
|
+
error_msg = f"Error performing Gemini web search: {str(e)}"
|
|
82
|
+
logger.error("❌ Gemini web search failed: %s", str(e))
|
|
83
|
+
logger.debug("💥 Full error details: %s", error_msg)
|
|
84
|
+
span.set_attribute("output.value", f"**Error:**\n {error_msg}\n")
|
|
85
|
+
return error_msg
|
|
@@ -1,14 +1,16 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""OpenAI web search tool implementation."""
|
|
2
2
|
|
|
3
3
|
from openai import OpenAI
|
|
4
4
|
from opentelemetry import trace
|
|
5
5
|
|
|
6
|
+
from shotgun.agents.config import get_provider_model
|
|
7
|
+
from shotgun.agents.config.models import ProviderType
|
|
6
8
|
from shotgun.logging_config import get_logger
|
|
7
9
|
|
|
8
10
|
logger = get_logger(__name__)
|
|
9
11
|
|
|
10
12
|
|
|
11
|
-
def
|
|
13
|
+
def openai_web_search_tool(query: str) -> str:
|
|
12
14
|
"""Perform a web search and return results.
|
|
13
15
|
|
|
14
16
|
This tool uses OpenAI's web search capabilities to find current information
|
|
@@ -20,27 +22,40 @@ def web_search_tool(query: str) -> str:
|
|
|
20
22
|
Returns:
|
|
21
23
|
Search results as a formatted string
|
|
22
24
|
"""
|
|
23
|
-
logger.debug("🔧 Invoking web_search_tool with query: %s", query)
|
|
25
|
+
logger.debug("🔧 Invoking OpenAI web_search_tool with query: %s", query)
|
|
24
26
|
|
|
25
27
|
span = trace.get_current_span()
|
|
26
28
|
span.set_attribute("input.value", f"**Query:** {query}\n")
|
|
27
29
|
|
|
28
30
|
try:
|
|
29
|
-
logger.debug("📡 Executing web search with prompt: %s", query)
|
|
31
|
+
logger.debug("📡 Executing OpenAI web search with prompt: %s", query)
|
|
30
32
|
|
|
31
|
-
|
|
33
|
+
# Get API key from centralized configuration
|
|
34
|
+
try:
|
|
35
|
+
model_config = get_provider_model(ProviderType.OPENAI)
|
|
36
|
+
api_key = model_config.api_key
|
|
37
|
+
except ValueError as e:
|
|
38
|
+
error_msg = f"OpenAI API key not configured: {str(e)}"
|
|
39
|
+
logger.error("❌ %s", error_msg)
|
|
40
|
+
span.set_attribute("output.value", f"**Error:**\n {error_msg}\n")
|
|
41
|
+
return error_msg
|
|
42
|
+
|
|
43
|
+
client = OpenAI(api_key=api_key)
|
|
32
44
|
response = client.responses.create( # type: ignore[call-overload]
|
|
33
45
|
model="gpt-5-mini",
|
|
34
46
|
input=[
|
|
35
47
|
{"role": "user", "content": [{"type": "input_text", "text": query}]}
|
|
36
48
|
],
|
|
37
|
-
text={
|
|
38
|
-
|
|
49
|
+
text={
|
|
50
|
+
"format": {"type": "text"},
|
|
51
|
+
"verbosity": "high",
|
|
52
|
+
}, # Increased from medium
|
|
53
|
+
reasoning={"effort": "high", "summary": "auto"}, # Increased from medium
|
|
39
54
|
tools=[
|
|
40
55
|
{
|
|
41
56
|
"type": "web_search",
|
|
42
57
|
"user_location": {"type": "approximate"},
|
|
43
|
-
"search_context_size": "
|
|
58
|
+
"search_context_size": "high", # Increased from low for more context
|
|
44
59
|
}
|
|
45
60
|
],
|
|
46
61
|
store=False,
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""Utility functions for web search tools."""
|
|
2
|
+
|
|
3
|
+
from shotgun.agents.config import get_provider_model
|
|
4
|
+
from shotgun.agents.config.models import ProviderType
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def is_provider_available(provider: ProviderType) -> bool:
|
|
8
|
+
"""Check if a provider has API key configured.
|
|
9
|
+
|
|
10
|
+
Args:
|
|
11
|
+
provider: The provider to check
|
|
12
|
+
|
|
13
|
+
Returns:
|
|
14
|
+
True if the provider has valid credentials configured (from config or env)
|
|
15
|
+
"""
|
|
16
|
+
try:
|
|
17
|
+
get_provider_model(provider)
|
|
18
|
+
return True
|
|
19
|
+
except ValueError:
|
|
20
|
+
return False
|
shotgun/cli/research.py
CHANGED
|
@@ -9,7 +9,6 @@ from shotgun.agents.config import ProviderType
|
|
|
9
9
|
from shotgun.agents.models import AgentRuntimeOptions
|
|
10
10
|
from shotgun.agents.research import (
|
|
11
11
|
create_research_agent,
|
|
12
|
-
get_research_history,
|
|
13
12
|
run_research_agent,
|
|
14
13
|
)
|
|
15
14
|
from shotgun.logging_config import get_logger
|
|
@@ -70,9 +69,6 @@ async def async_research(
|
|
|
70
69
|
result = await run_research_agent(agent, query, deps)
|
|
71
70
|
|
|
72
71
|
# Display results
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
logger.info("📄 Full research saved to: .shotgun/research.md")
|
|
77
|
-
logger.debug("📚 Research history:")
|
|
78
|
-
logger.debug("%s", get_research_history())
|
|
72
|
+
print("✅ Research Complete!")
|
|
73
|
+
print("📋 Findings:")
|
|
74
|
+
print(result.output)
|
|
@@ -35,8 +35,9 @@ async def llm_cypher_prompt(system_prompt: str, user_prompt: str) -> str:
|
|
|
35
35
|
The generated Cypher query as a string
|
|
36
36
|
"""
|
|
37
37
|
model_config = get_provider_model()
|
|
38
|
+
# Use the Model instance directly (has API key baked in)
|
|
38
39
|
query_cypher_response = await model_request(
|
|
39
|
-
model=model_config.
|
|
40
|
+
model=model_config.model_instance,
|
|
40
41
|
messages=[
|
|
41
42
|
ModelRequest(
|
|
42
43
|
parts=[
|
|
@@ -68,32 +68,6 @@ def load_parsers() -> tuple[dict[str, Parser], dict[str, Any]]:
|
|
|
68
68
|
except ImportError as e:
|
|
69
69
|
logger.warning(f"Failed to import tree_sitter_rust: {e}")
|
|
70
70
|
|
|
71
|
-
# If no individual imports worked, try tree_sitter_languages
|
|
72
|
-
if not available_languages:
|
|
73
|
-
try:
|
|
74
|
-
import tree_sitter_languages # type: ignore[import-untyped]
|
|
75
|
-
|
|
76
|
-
# Get available languages from tree_sitter_languages
|
|
77
|
-
for lang_name in [
|
|
78
|
-
"python",
|
|
79
|
-
"javascript",
|
|
80
|
-
"typescript",
|
|
81
|
-
"go",
|
|
82
|
-
"rust",
|
|
83
|
-
"java",
|
|
84
|
-
"cpp",
|
|
85
|
-
]:
|
|
86
|
-
try:
|
|
87
|
-
lang = tree_sitter_languages.get_language(lang_name)
|
|
88
|
-
language_loaders[lang_name] = lambda lang=lang: lang # type: ignore[misc]
|
|
89
|
-
available_languages.append(lang_name)
|
|
90
|
-
except Exception as e:
|
|
91
|
-
logger.debug(f"Failed to load {lang_name} parser: {e}")
|
|
92
|
-
except ImportError:
|
|
93
|
-
logger.warning(
|
|
94
|
-
"No tree-sitter language libraries found. Install tree-sitter-languages or individual language packages."
|
|
95
|
-
)
|
|
96
|
-
|
|
97
71
|
logger.info(f"Available languages: {', '.join(available_languages)}")
|
|
98
72
|
|
|
99
73
|
# Create parsers for available languages
|
|
@@ -144,9 +118,11 @@ def load_parsers() -> tuple[dict[str, Parser], dict[str, Any]]:
|
|
|
144
118
|
|
|
145
119
|
if not parsers:
|
|
146
120
|
logger.error(
|
|
147
|
-
"No parsers could be loaded. Please install tree-sitter
|
|
121
|
+
"No parsers could be loaded. Please install language-specific tree-sitter packages."
|
|
122
|
+
)
|
|
123
|
+
logger.error(
|
|
124
|
+
"Install with: pip install tree-sitter-python tree-sitter-javascript tree-sitter-typescript tree-sitter-go tree-sitter-rust"
|
|
148
125
|
)
|
|
149
|
-
logger.error("Install with: pip install tree-sitter-languages")
|
|
150
126
|
sys.exit(1)
|
|
151
127
|
|
|
152
128
|
return parsers, queries
|
shotgun/main.py
CHANGED
|
@@ -8,7 +8,8 @@ from dotenv import load_dotenv
|
|
|
8
8
|
from shotgun.agents.config import get_config_manager
|
|
9
9
|
from shotgun.cli import codebase, config, plan, research, tasks
|
|
10
10
|
from shotgun.logging_config import configure_root_logger, get_logger
|
|
11
|
-
from shotgun.telemetry import
|
|
11
|
+
from shotgun.telemetry import setup_logfire_observability
|
|
12
|
+
from shotgun.tui import app as tui_app
|
|
12
13
|
|
|
13
14
|
# Load environment variables from .env file
|
|
14
15
|
load_dotenv()
|
|
@@ -25,13 +26,12 @@ except Exception as e:
|
|
|
25
26
|
logger.debug("Configuration initialization warning: %s", e)
|
|
26
27
|
|
|
27
28
|
# Initialize telemetry
|
|
28
|
-
|
|
29
|
-
logger.debug("
|
|
29
|
+
_logfire_enabled = setup_logfire_observability()
|
|
30
|
+
logger.debug("Logfire observability enabled: %s", _logfire_enabled)
|
|
30
31
|
|
|
31
32
|
app = typer.Typer(
|
|
32
33
|
name="shotgun",
|
|
33
34
|
help="Shotgun - AI-powered CLI tool for research, planning, and task management",
|
|
34
|
-
no_args_is_help=True,
|
|
35
35
|
rich_markup_mode="rich",
|
|
36
36
|
)
|
|
37
37
|
|
|
@@ -52,8 +52,9 @@ def version_callback(value: bool) -> None:
|
|
|
52
52
|
raise typer.Exit()
|
|
53
53
|
|
|
54
54
|
|
|
55
|
-
@app.callback()
|
|
55
|
+
@app.callback(invoke_without_command=True)
|
|
56
56
|
def main(
|
|
57
|
+
ctx: typer.Context,
|
|
57
58
|
version: Annotated[
|
|
58
59
|
bool,
|
|
59
60
|
typer.Option(
|
|
@@ -67,6 +68,10 @@ def main(
|
|
|
67
68
|
) -> None:
|
|
68
69
|
"""Shotgun - AI-powered CLI tool."""
|
|
69
70
|
logger.debug("Starting shotgun CLI application")
|
|
71
|
+
if ctx.invoked_subcommand is None and not ctx.resilient_parsing:
|
|
72
|
+
logger.debug("Launching shotgun TUI application")
|
|
73
|
+
tui_app.run()
|
|
74
|
+
raise typer.Exit()
|
|
70
75
|
|
|
71
76
|
|
|
72
77
|
if __name__ == "__main__":
|
shotgun/telemetry.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""Observability setup for Logfire."""
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
4
|
import os
|
|
@@ -6,63 +6,39 @@ import os
|
|
|
6
6
|
logger = logging.getLogger(__name__)
|
|
7
7
|
|
|
8
8
|
|
|
9
|
-
def
|
|
10
|
-
"""Set up
|
|
11
|
-
|
|
12
|
-
Supports both local Phoenix and cloud Phoenix (Arize) configurations.
|
|
9
|
+
def setup_logfire_observability() -> bool:
|
|
10
|
+
"""Set up Logfire observability if enabled.
|
|
13
11
|
|
|
14
12
|
Returns:
|
|
15
|
-
True if
|
|
13
|
+
True if Logfire was successfully set up, False otherwise
|
|
16
14
|
"""
|
|
17
|
-
# Check if
|
|
18
|
-
if os.getenv("
|
|
19
|
-
logger.debug("
|
|
15
|
+
# Check if Logfire observability is enabled
|
|
16
|
+
if os.getenv("LOGFIRE_ENABLED", "false").lower() not in ("true", "1", "yes"):
|
|
17
|
+
logger.debug("Logfire observability disabled via LOGFIRE_ENABLED env var")
|
|
20
18
|
return False
|
|
21
19
|
|
|
22
20
|
try:
|
|
23
|
-
|
|
24
|
-
phoenix_collector_endpoint = os.getenv("PHOENIX_COLLECTOR_ENDPOINT")
|
|
25
|
-
phoenix_api_key = os.getenv("PHOENIX_API_KEY")
|
|
21
|
+
import logfire
|
|
26
22
|
|
|
27
|
-
|
|
23
|
+
# Check for Logfire token
|
|
24
|
+
logfire_token = os.getenv("LOGFIRE_TOKEN")
|
|
25
|
+
if not logfire_token:
|
|
26
|
+
logger.warning("LOGFIRE_TOKEN not set, Logfire observability disabled")
|
|
28
27
|
return False
|
|
29
28
|
|
|
30
|
-
#
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
from openinference.instrumentation.pydantic_ai import (
|
|
34
|
-
OpenInferenceSpanProcessor,
|
|
35
|
-
)
|
|
36
|
-
from opentelemetry import trace
|
|
37
|
-
from opentelemetry.exporter.otlp.proto.http.trace_exporter import (
|
|
38
|
-
OTLPSpanExporter,
|
|
39
|
-
)
|
|
40
|
-
from opentelemetry.sdk.trace import TracerProvider
|
|
41
|
-
from opentelemetry.sdk.trace.export import BatchSpanProcessor
|
|
42
|
-
|
|
43
|
-
# Set up tracer provider
|
|
44
|
-
tracer_provider = TracerProvider()
|
|
45
|
-
trace.set_tracer_provider(tracer_provider)
|
|
46
|
-
|
|
47
|
-
# Set up OTLP exporter for cloud Phoenix
|
|
48
|
-
# Phoenix cloud expects Authorization header, not api_key
|
|
49
|
-
otlp_exporter = OTLPSpanExporter(
|
|
50
|
-
endpoint=phoenix_collector_endpoint,
|
|
51
|
-
headers={"authorization": f"Bearer {phoenix_api_key}"},
|
|
52
|
-
)
|
|
29
|
+
# Configure Logfire
|
|
30
|
+
logfire.configure(token=logfire_token)
|
|
53
31
|
|
|
54
|
-
#
|
|
55
|
-
|
|
56
|
-
tracer_provider.add_span_processor(BatchSpanProcessor(otlp_exporter))
|
|
32
|
+
# Instrument Pydantic AI for better observability
|
|
33
|
+
logfire.instrument_pydantic_ai()
|
|
57
34
|
|
|
58
|
-
logger.debug("
|
|
59
|
-
logger.debug("
|
|
60
|
-
logger.debug("API key configured: %s", "Yes" if phoenix_api_key else "No")
|
|
35
|
+
logger.debug("Logfire observability configured successfully")
|
|
36
|
+
logger.debug("Token configured: %s", "Yes" if logfire_token else "No")
|
|
61
37
|
return True
|
|
62
38
|
|
|
63
39
|
except ImportError as e:
|
|
64
|
-
logger.warning("
|
|
40
|
+
logger.warning("Logfire not available: %s", e)
|
|
65
41
|
return False
|
|
66
42
|
except Exception as e:
|
|
67
|
-
logger.warning("Failed to setup
|
|
43
|
+
logger.warning("Failed to setup Logfire observability: %s", e)
|
|
68
44
|
return False
|
shotgun/tui/screens/chat.py
CHANGED
|
@@ -146,7 +146,7 @@ class StatusBar(Widget):
|
|
|
146
146
|
"""
|
|
147
147
|
|
|
148
148
|
def render(self) -> str:
|
|
149
|
-
return """[$foreground-muted]
|
|
149
|
+
return """[$foreground-muted][bold $text]enter[/] to send • [bold $text]ctrl+p[/] command palette • [bold $text]shift+tab[/] cycle modes • /help for commands[/]"""
|
|
150
150
|
|
|
151
151
|
|
|
152
152
|
class ModeIndicator(Widget):
|
|
@@ -284,12 +284,13 @@ class ChatScreen(Screen[None]):
|
|
|
284
284
|
|
|
285
285
|
BINDINGS = [
|
|
286
286
|
("ctrl+p", "command_palette", "Command Palette"),
|
|
287
|
+
("shift+tab", "toggle_mode", "Toggle mode"),
|
|
287
288
|
]
|
|
288
289
|
|
|
289
290
|
COMMANDS = {AgentModeProvider, ProviderSetupProvider}
|
|
290
291
|
|
|
291
292
|
value = reactive("")
|
|
292
|
-
mode = reactive(AgentType.RESEARCH
|
|
293
|
+
mode = reactive(AgentType.RESEARCH)
|
|
293
294
|
history: PromptHistory = PromptHistory()
|
|
294
295
|
messages = reactive(list[ModelMessage]())
|
|
295
296
|
working = reactive(False)
|
|
@@ -314,13 +315,19 @@ class ChatScreen(Screen[None]):
|
|
|
314
315
|
|
|
315
316
|
def watch_mode(self, new_mode: AgentType) -> None:
|
|
316
317
|
"""React to mode changes by updating the agent manager."""
|
|
317
|
-
|
|
318
|
+
|
|
319
|
+
if self.is_mounted:
|
|
318
320
|
self.agent_manager.set_agent(new_mode)
|
|
319
321
|
|
|
322
|
+
mode_indicator = self.query_one(ModeIndicator)
|
|
323
|
+
mode_indicator.mode = new_mode
|
|
324
|
+
mode_indicator.refresh()
|
|
325
|
+
|
|
320
326
|
def watch_working(self, is_working: bool) -> None:
|
|
321
327
|
"""Show or hide the spinner based on working state."""
|
|
322
328
|
if self.is_mounted:
|
|
323
329
|
spinner = self.query_one("#spinner")
|
|
330
|
+
spinner.set_classes("" if is_working else "hidden")
|
|
324
331
|
spinner.display = is_working
|
|
325
332
|
|
|
326
333
|
def watch_messages(self, messages: list[ModelMessage]) -> None:
|
|
@@ -340,6 +347,13 @@ class ChatScreen(Screen[None]):
|
|
|
340
347
|
question_display.update("")
|
|
341
348
|
question_display.display = False
|
|
342
349
|
|
|
350
|
+
def action_toggle_mode(self) -> None:
|
|
351
|
+
modes = [AgentType.RESEARCH, AgentType.PLAN, AgentType.TASKS]
|
|
352
|
+
self.mode = modes[(modes.index(self.mode) + 1) % len(modes)]
|
|
353
|
+
self.agent_manager.set_agent(self.mode)
|
|
354
|
+
# whoops it actually changes focus. Let's be brutal for now
|
|
355
|
+
self.call_later(lambda: self.query_one(PromptInput).focus())
|
|
356
|
+
|
|
343
357
|
@work
|
|
344
358
|
async def add_question_listener(self) -> None:
|
|
345
359
|
while True:
|
|
@@ -355,7 +369,11 @@ class ChatScreen(Screen[None]):
|
|
|
355
369
|
yield Markdown(markdown="", id="question-display")
|
|
356
370
|
yield self.agent_manager
|
|
357
371
|
with Container(id="footer"):
|
|
358
|
-
yield Spinner(
|
|
372
|
+
yield Spinner(
|
|
373
|
+
text="Processing...",
|
|
374
|
+
id="spinner",
|
|
375
|
+
classes="" if self.working else "hidden",
|
|
376
|
+
)
|
|
359
377
|
yield StatusBar()
|
|
360
378
|
yield PromptInput(
|
|
361
379
|
text=self.value,
|
shotgun/tui/screens/chat.tcss
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: shotgun-sh
|
|
3
|
-
Version: 0.1.0.
|
|
3
|
+
Version: 0.1.0.dev4
|
|
4
4
|
Summary: AI-powered research, planning, and task management CLI tool
|
|
5
5
|
Project-URL: Homepage, https://shotgun.sh/
|
|
6
6
|
Project-URL: Repository, https://github.com/shotgun-sh/shotgun
|
|
@@ -22,20 +22,19 @@ Classifier: Programming Language :: Python :: 3.12
|
|
|
22
22
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
23
23
|
Classifier: Topic :: Utilities
|
|
24
24
|
Requires-Python: >=3.10
|
|
25
|
+
Requires-Dist: anthropic>=0.39.0
|
|
26
|
+
Requires-Dist: google-generativeai>=0.8.0
|
|
25
27
|
Requires-Dist: httpx>=0.27.0
|
|
26
28
|
Requires-Dist: jinja2>=3.1.0
|
|
27
29
|
Requires-Dist: kuzu>=0.7.0
|
|
28
|
-
Requires-Dist:
|
|
29
|
-
Requires-Dist:
|
|
30
|
-
Requires-Dist: opentelemetry-exporter-otlp
|
|
31
|
-
Requires-Dist: opentelemetry-sdk
|
|
30
|
+
Requires-Dist: logfire[pydantic-ai]>=2.0.0
|
|
31
|
+
Requires-Dist: openai>=1.0.0
|
|
32
32
|
Requires-Dist: pydantic-ai>=0.0.14
|
|
33
33
|
Requires-Dist: rich>=13.0.0
|
|
34
34
|
Requires-Dist: textual-dev>=1.7.0
|
|
35
35
|
Requires-Dist: textual>=6.1.0
|
|
36
36
|
Requires-Dist: tree-sitter-go>=0.23.0
|
|
37
37
|
Requires-Dist: tree-sitter-javascript>=0.23.0
|
|
38
|
-
Requires-Dist: tree-sitter-languages>=1.10.0
|
|
39
38
|
Requires-Dist: tree-sitter-python>=0.23.0
|
|
40
39
|
Requires-Dist: tree-sitter-rust>=0.23.0
|
|
41
40
|
Requires-Dist: tree-sitter-typescript>=0.23.0
|
|
@@ -1,25 +1,24 @@
|
|
|
1
1
|
shotgun/__init__.py,sha256=5pveArOu7XFzA-uGyx58sIlsoja3-yr25JiMg6LaoGY,55
|
|
2
2
|
shotgun/logging_config.py,sha256=EJL2kpwH8-zRtpKit3_BbpeUxbRR-wen3MaNXuHCoD4,5600
|
|
3
|
-
shotgun/main.py,sha256=
|
|
3
|
+
shotgun/main.py,sha256=SOwxw49hbWlSe_APstonRU3uB34fIToYfx-GFGmXolE,2291
|
|
4
4
|
shotgun/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
-
shotgun/telemetry.py,sha256=
|
|
5
|
+
shotgun/telemetry.py,sha256=42mItPr4uZ5NtMF--WbRCy2DX8o_cN_zz4se3E3ULLA,1346
|
|
6
6
|
shotgun/agents/__init__.py,sha256=8Jzv1YsDuLyNPFJyckSr_qI4ehTVeDyIMDW4omsfPGc,25
|
|
7
7
|
shotgun/agents/agent_manager.py,sha256=1Aof8btrV2Ei3V3bNn8bLzdBiBsJ-sjaQq0t7bnwbpo,6564
|
|
8
|
-
shotgun/agents/common.py,sha256=
|
|
8
|
+
shotgun/agents/common.py,sha256=xNlbvwt68de2588930iI4JnNVyPMGsUEwf_ySFXttFE,9439
|
|
9
9
|
shotgun/agents/models.py,sha256=EY84zpsZW1g-K6-JHVDJoO6R_Q6ZM7YKyUtZO1j0Gmo,2453
|
|
10
10
|
shotgun/agents/plan.py,sha256=mSkaMfavZ1-WNJ-Yk-8ivbNtmbOz-TZI4oO8vIXXu5Y,3537
|
|
11
|
-
shotgun/agents/research.py,sha256=
|
|
11
|
+
shotgun/agents/research.py,sha256=zDoS1BLVn7Wvcc4VAoYWOZ33IhtaHCY_CIlvEFiD1_A,4089
|
|
12
12
|
shotgun/agents/tasks.py,sha256=8miXmOB6zp-9jTZgzpgs-109M5BWAg0n5-TD5D8tm1c,3648
|
|
13
13
|
shotgun/agents/config/__init__.py,sha256=Fl8K_81zBpm-OfOW27M_WWLSFdaHHek6lWz95iDREjQ,318
|
|
14
14
|
shotgun/agents/config/manager.py,sha256=d5Eb0TK6Fwez9XN3f33_meGxs2iP7lukwaAyJM5e4fQ,7821
|
|
15
|
-
shotgun/agents/config/models.py,sha256=
|
|
16
|
-
shotgun/agents/config/provider.py,sha256=
|
|
15
|
+
shotgun/agents/config/models.py,sha256=TqVtmCDqlSRxc3ZK53O-nJDTTwT_eS2TWvC1RmSin28,3518
|
|
16
|
+
shotgun/agents/config/provider.py,sha256=tN__agB1MiaNL49HTDUz1QzvOXTegLRoJ9xQKoklK-k,5820
|
|
17
17
|
shotgun/agents/history/__init__.py,sha256=XFQj2a6fxDqVg0Q3juvN9RjV_RJbgvFZtQOCOjVJyp4,147
|
|
18
18
|
shotgun/agents/history/history_processors.py,sha256=U0HiJM4zVLfwUnlZ6_2YoXNRteedMxwmr52KE4jky5w,7036
|
|
19
|
-
shotgun/agents/tools/__init__.py,sha256=
|
|
19
|
+
shotgun/agents/tools/__init__.py,sha256=QaN80IqWvB5qEcjHqri1-PYvYlO74vdhcwLugoEdblo,772
|
|
20
20
|
shotgun/agents/tools/file_management.py,sha256=Lua9KZ_zZMbKl8mLO9EIkfjQwjjCnJ3E3R97GzjO460,3985
|
|
21
21
|
shotgun/agents/tools/user_interaction.py,sha256=7l0OY8EdgO-9gkKy-yOv0V0P_Uzzfk0jMU39d4XN1xM,1087
|
|
22
|
-
shotgun/agents/tools/web_search.py,sha256=BCCjHPDh83cHjDJbT78fTBd4s1qg2kxbkzDQcljLk2o,2268
|
|
23
22
|
shotgun/agents/tools/codebase/__init__.py,sha256=ceAGkK006NeOYaIJBLQsw7Q46sAyCRK9PYDs8feMQVw,661
|
|
24
23
|
shotgun/agents/tools/codebase/codebase_shell.py,sha256=2zEq8YXzdcYttYAfKso_JGRqXHyy3xgAuWlf0unopFg,8635
|
|
25
24
|
shotgun/agents/tools/codebase/directory_lister.py,sha256=MCLGDEc0F-4J8-UrquxdJrIQYs5xzYgws65mjf7Q7X4,4724
|
|
@@ -27,11 +26,16 @@ shotgun/agents/tools/codebase/file_read.py,sha256=mqS04CI9OEmWiSHjL5SPUCUgPzoQa3
|
|
|
27
26
|
shotgun/agents/tools/codebase/models.py,sha256=8eR3_8DQiBNgB2twu0aC_evIJbugN9KW3gtxMZdGYCE,10087
|
|
28
27
|
shotgun/agents/tools/codebase/query_graph.py,sha256=ffm8kTZap0KwPTtae5hvYLy_AQDjpDHUcx0ui9nX2OQ,2136
|
|
29
28
|
shotgun/agents/tools/codebase/retrieve_code.py,sha256=yhWCiam6Dgs9Pyx0mVVzsC4KhQb2NmP5DEToOj3q1Vw,2899
|
|
29
|
+
shotgun/agents/tools/web_search/__init__.py,sha256=Sj1tVokrCsJiLRWWTq0zrAolMHEGntRIYnqiyFi8L2E,1840
|
|
30
|
+
shotgun/agents/tools/web_search/anthropic.py,sha256=v6R4z_c5L_YbBX3FPNWYUKXUBgFQwTxEgHvtGDlqlgk,3092
|
|
31
|
+
shotgun/agents/tools/web_search/gemini.py,sha256=RB7AeGDBvjlsA2RLCI8ErxZh3gOPtA0rhxtW28NyOeE,3025
|
|
32
|
+
shotgun/agents/tools/web_search/openai.py,sha256=ItpV3IquamYJ13ZNUHYjXrSsgOROD51jDd2mwnz0KCE,2972
|
|
33
|
+
shotgun/agents/tools/web_search/utils.py,sha256=GLJ5QV9bT2ubFMuFN7caMN7tK9OTJ0R3GD57B-tCMF0,532
|
|
30
34
|
shotgun/cli/__init__.py,sha256=_F1uW2g87y4bGFxz8Gp8u7mq2voHp8vQIUtCmm8Tojo,40
|
|
31
35
|
shotgun/cli/config.py,sha256=_eKnKG8ySLNDRavw5EC3mXB3u0UKDoqSSj5ZVd6KYkY,8267
|
|
32
36
|
shotgun/cli/models.py,sha256=LoajeEK7MEDUSnZXb1Li-dbhXqne812YZglx-LcVpiQ,181
|
|
33
37
|
shotgun/cli/plan.py,sha256=q3nMrNK0J1HAhnfFFqllcHXN_grmzZMd1FBPcCikNvA,2186
|
|
34
|
-
shotgun/cli/research.py,sha256=
|
|
38
|
+
shotgun/cli/research.py,sha256=cmJa1ejxsnK_eqB1KsPQtqsq0cIMSS0Z8R2tJamHu8k,2212
|
|
35
39
|
shotgun/cli/tasks.py,sha256=VdZM0tObUALRg2e_r9YwZYdrG6rYe1wXo5LubXdJ4qw,2289
|
|
36
40
|
shotgun/cli/utils.py,sha256=umVWXDx8pelovMk-nT8B7m0c39AKY9hHsuAMnbw_Hcg,732
|
|
37
41
|
shotgun/cli/codebase/__init__.py,sha256=rKdvx33p0i_BYbNkz5_4DCFgEMwzOOqLi9f5p7XTLKM,73
|
|
@@ -46,8 +50,8 @@ shotgun/codebase/core/code_retrieval.py,sha256=_JVyyQKHDFm3dxOOua1mw9eIIOHIVz3-I
|
|
|
46
50
|
shotgun/codebase/core/ingestor.py,sha256=zMjadeqDOEr2v3vhTS25Jvx0WsLPXpgwquZfbdiz57o,59810
|
|
47
51
|
shotgun/codebase/core/language_config.py,sha256=vsqHyuFnumRPRBV1lMOxWKNOIiClO6FyfKQR0fGrtl4,8934
|
|
48
52
|
shotgun/codebase/core/manager.py,sha256=5GlJKykDGvnb6nTr9w3kyCPTL4OQgmBoesnWr28wvTg,55419
|
|
49
|
-
shotgun/codebase/core/nl_query.py,sha256=
|
|
50
|
-
shotgun/codebase/core/parser_loader.py,sha256=
|
|
53
|
+
shotgun/codebase/core/nl_query.py,sha256=ZQRVc9qBNEqPwdPIHmgePCKgBZrWzqos8Xd1dALRMYY,11377
|
|
54
|
+
shotgun/codebase/core/parser_loader.py,sha256=LZRrDS8Sp518jIu3tQW-BxdwJ86lnsTteI478ER9Td8,4278
|
|
51
55
|
shotgun/prompts/__init__.py,sha256=RswUm0HMdfm2m2YKUwUsEdRIwoczdbI7zlucoEvHYRo,132
|
|
52
56
|
shotgun/prompts/loader.py,sha256=jy24-E02pCSmz2651aCT2NgHfRrHAGMYvKrD6gs0Er8,4424
|
|
53
57
|
shotgun/prompts/agents/__init__.py,sha256=YRIJMbzpArojNX1BP5gfxxois334z_GQga8T-xyWMbY,39
|
|
@@ -81,14 +85,14 @@ shotgun/tui/components/prompt_input.py,sha256=Ss-htqraHZAPaehGE4x86ij0veMjc4Ugad
|
|
|
81
85
|
shotgun/tui/components/spinner.py,sha256=ovTDeaJ6FD6chZx_Aepia6R3UkPOVJ77EKHfRmn39MY,2427
|
|
82
86
|
shotgun/tui/components/splash.py,sha256=vppy9vEIEvywuUKRXn2y11HwXSRkQZHLYoVjhDVdJeU,1267
|
|
83
87
|
shotgun/tui/components/vertical_tail.py,sha256=kkCH0WjAh54jDvRzIaOffRZXUKn_zHFZ_ichfUpgzaE,1071
|
|
84
|
-
shotgun/tui/screens/chat.py,sha256=
|
|
85
|
-
shotgun/tui/screens/chat.tcss,sha256=
|
|
88
|
+
shotgun/tui/screens/chat.py,sha256=FLzBeh-UJGloAnhMlQx_XDiREWDx640vI58ux_YeVe4,14428
|
|
89
|
+
shotgun/tui/screens/chat.tcss,sha256=MV7-HhXSpBxIsSbB57RugNeM0wOpqMpIVke7qCf4-yQ,312
|
|
86
90
|
shotgun/tui/screens/provider_config.py,sha256=A_tvDHF5KLP5PV60LjMJ_aoOdT3TjI6_g04UIUqGPqM,7126
|
|
87
91
|
shotgun/tui/screens/splash.py,sha256=E2MsJihi3c9NY1L28o_MstDxGwrCnnV7zdq00MrGAsw,706
|
|
88
92
|
shotgun/utils/__init__.py,sha256=WinIEp9oL2iMrWaDkXz2QX4nYVPAm8C9aBSKTeEwLtE,198
|
|
89
93
|
shotgun/utils/file_system_utils.py,sha256=KQCxgkspb1CR8VE1n66q7-oT6O7MmV_edCXFEEO-CNY,871
|
|
90
|
-
shotgun_sh-0.1.0.
|
|
91
|
-
shotgun_sh-0.1.0.
|
|
92
|
-
shotgun_sh-0.1.0.
|
|
93
|
-
shotgun_sh-0.1.0.
|
|
94
|
-
shotgun_sh-0.1.0.
|
|
94
|
+
shotgun_sh-0.1.0.dev4.dist-info/METADATA,sha256=B8s8d8TmfgScM6mfMfHuhoHq6a-y0nfcb81mGsxCGrQ,7701
|
|
95
|
+
shotgun_sh-0.1.0.dev4.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
96
|
+
shotgun_sh-0.1.0.dev4.dist-info/entry_points.txt,sha256=zMC2AP_RmTKW4s4FlQRdap3AzzPOUvByudp8cALAiVY,71
|
|
97
|
+
shotgun_sh-0.1.0.dev4.dist-info/licenses/LICENSE,sha256=YebsZl590zCHrF_acCU5pmNt0pnAfD2DmAnevJPB1tY,1065
|
|
98
|
+
shotgun_sh-0.1.0.dev4.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|