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 CHANGED
@@ -170,7 +170,8 @@ def create_base_agent(
170
170
  provider_name.upper(),
171
171
  model_config.name,
172
172
  )
173
- model = model_config.pydantic_model_name
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()
@@ -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
- """Configuration for an LLM model."""
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
- # OpenAI Models
36
- GPT_5 = ModelConfig(
37
- name="gpt-5",
38
- provider=ProviderType.OPENAI,
39
- max_input_tokens=400_000,
40
- max_output_tokens=128_000,
41
- )
42
-
43
- GPT_4O = ModelConfig(
44
- name="gpt-4o",
45
- provider=ProviderType.OPENAI,
46
- max_input_tokens=128_000,
47
- max_output_tokens=16_000,
48
- )
49
-
50
- # Anthropic Models
51
- CLAUDE_OPUS_4_1 = ModelConfig(
52
- name="claude-opus-4-1",
53
- provider=ProviderType.ANTHROPIC,
54
- max_input_tokens=200_000,
55
- max_output_tokens=32_000,
56
- )
57
-
58
- CLAUDE_3_5_SONNET = ModelConfig(
59
- name="claude-3-5-sonnet-latest",
60
- provider=ProviderType.ANTHROPIC,
61
- max_input_tokens=200_000,
62
- max_output_tokens=20_000,
63
- )
64
-
65
- # Google Models
66
- GEMINI_2_5_PRO = ModelConfig(
67
- name="gemini-2.5-pro",
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, get_model_by_name
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 model configuration for the specified provider.
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 pydantic_model_name and token limits
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
- return get_model_by_name(config.openai.model_name)
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
- return get_model_by_name(config.anthropic.model_name)
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, "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
- "Google API key not configured. Set via environment variable GOOGLE_API_KEY or config."
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
- return get_model_by_name(config.google.model_name)
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}")
@@ -23,7 +23,7 @@ from .common import (
23
23
  run_agent,
24
24
  )
25
25
  from .models import AgentDeps, AgentRuntimeOptions
26
- from .tools import web_search_tool
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=[web_search_tool],
78
+ additional_tools=web_search_tools,
68
79
  provider=provider,
69
80
  )
70
81
  return agent, deps
@@ -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 web_search_tool
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
- "web_search_tool",
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
- """Web search tool for Pydantic AI agents."""
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 web_search_tool(query: str) -> str:
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
- client = OpenAI()
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={"format": {"type": "text"}, "verbosity": "medium"},
38
- reasoning={"effort": "medium", "summary": "auto"},
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": "low",
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
- logger.info("✅ Research Complete!")
74
- logger.info("📋 Findings:")
75
- logger.info("%s", result.output)
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.pydantic_model_name,
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-languages or language-specific packages."
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 setup_phoenix_observability
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
- _telemetry_enabled = setup_phoenix_observability()
29
- logger.debug("Phoenix observability enabled: %s", _telemetry_enabled)
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
- """Phoenix AI observability setup for cloud and local deployment."""
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 setup_phoenix_observability() -> bool:
10
- """Set up Phoenix AI observability if enabled.
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 Phoenix was successfully set up, False otherwise
13
+ True if Logfire was successfully set up, False otherwise
16
14
  """
17
- # Check if Phoenix observability is enabled
18
- if os.getenv("PHOENIX_ENABLED", "false").lower() not in ("true", "1", "yes"):
19
- logger.debug("Phoenix AI observability disabled via PHOENIX_ENABLED env var")
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
- # Check if using cloud Phoenix (Arize) or local Phoenix
24
- phoenix_collector_endpoint = os.getenv("PHOENIX_COLLECTOR_ENDPOINT")
25
- phoenix_api_key = os.getenv("PHOENIX_API_KEY")
21
+ import logfire
26
22
 
27
- if not phoenix_collector_endpoint or not phoenix_api_key:
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
- # Cloud Phoenix setup (Arize) - following exact docs pattern
31
- logger.debug("Setting up cloud Phoenix AI observability")
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
- # Add both span processors - OpenInference for semantics and BatchSpanProcessor for export
55
- tracer_provider.add_span_processor(OpenInferenceSpanProcessor())
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("Cloud Phoenix AI observability configured successfully")
59
- logger.debug("Endpoint: %s", phoenix_collector_endpoint)
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("Phoenix AI not available: %s", e)
40
+ logger.warning("Logfire not available: %s", e)
65
41
  return False
66
42
  except Exception as e:
67
- logger.warning("Failed to setup Phoenix AI observability: %s", e)
43
+ logger.warning("Failed to setup Logfire observability: %s", e)
68
44
  return False
@@ -146,7 +146,7 @@ class StatusBar(Widget):
146
146
  """
147
147
 
148
148
  def render(self) -> str:
149
- return """[$foreground-muted]Press [bold $text]Enter[/] to send • [bold $text]Ctrl+P[/] for command palette • /help for commands[/]"""
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, recompose=True)
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
- if hasattr(self, "agent_manager"):
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(text="Processing...", id="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,
@@ -25,4 +25,8 @@ ModeIndicator {
25
25
 
26
26
  #window {
27
27
  align: left bottom;
28
+ }
29
+
30
+ .hidden {
31
+ display: none;
28
32
  }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: shotgun-sh
3
- Version: 0.1.0.dev2
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: openinference-instrumentation-pydantic-ai
29
- Requires-Dist: opentelemetry-api
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=41ENJ7j2sNYVUSFOvry-nh_yZkUe8WwkayvzG056OTQ,2055
3
+ shotgun/main.py,sha256=SOwxw49hbWlSe_APstonRU3uB34fIToYfx-GFGmXolE,2291
4
4
  shotgun/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
- shotgun/telemetry.py,sha256=aq9wd-t2bYyCpRV927mUQdqMcL67QoZgsu3oZrVu83w,2605
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=BxeZkkMsT1gWR_EwFXo4gjatuWarT0nct1o2xeIahas,9379
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=JZKioKrl9ExiwRilCvz66orn1gKX3GO-D-EULCHy85c,3705
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=ziQoHEAL15GgVBrpS-tW2kPlHSVwn8imhRdbOunnVRY,2906
16
- shotgun/agents/config/provider.py,sha256=XZHi8_AHTG8bKjaNXnmx0hCvWSmtfNp6HCHjaedxUcg,3029
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=ArlUVsNnFxq10QTKarP5QAod6EVVD5XwIE70HyJtSmY,553
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=d0Afoi4P9q2Nl4T2eTDUGNdeB1KtDQFKFaauK_t0H8Q,2421
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=eljEFXwXnFMxvZuQFHy7K-417NnZ8S4dE25u3aUaJpw,11321
50
- shotgun/codebase/core/parser_loader.py,sha256=D2rNGNBuRbmF6OTV8isH-9l5gYYkFk0Uv7IkScXBp30,5223
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=2-A28icICOt2eZzu37U6r-HGoVtmvR5llPiyZnUS714,13682
85
- shotgun/tui/screens/chat.tcss,sha256=EJ3wGTwYTTbaVx39fHSrOZWd44grodQnmi39X-9qRC4,282
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.dev2.dist-info/METADATA,sha256=aBCHtstHlaPSwf1pZWI7C-gOHprXWc_2hjnY7pT5UzQ,7765
91
- shotgun_sh-0.1.0.dev2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
92
- shotgun_sh-0.1.0.dev2.dist-info/entry_points.txt,sha256=zMC2AP_RmTKW4s4FlQRdap3AzzPOUvByudp8cALAiVY,71
93
- shotgun_sh-0.1.0.dev2.dist-info/licenses/LICENSE,sha256=YebsZl590zCHrF_acCU5pmNt0pnAfD2DmAnevJPB1tY,1065
94
- shotgun_sh-0.1.0.dev2.dist-info/RECORD,,
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,,