shotgun-sh 0.1.14__py3-none-any.whl → 0.2.11__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.

Files changed (143) hide show
  1. shotgun/agents/agent_manager.py +715 -75
  2. shotgun/agents/common.py +80 -75
  3. shotgun/agents/config/constants.py +21 -10
  4. shotgun/agents/config/manager.py +322 -97
  5. shotgun/agents/config/models.py +114 -84
  6. shotgun/agents/config/provider.py +232 -88
  7. shotgun/agents/context_analyzer/__init__.py +28 -0
  8. shotgun/agents/context_analyzer/analyzer.py +471 -0
  9. shotgun/agents/context_analyzer/constants.py +9 -0
  10. shotgun/agents/context_analyzer/formatter.py +115 -0
  11. shotgun/agents/context_analyzer/models.py +212 -0
  12. shotgun/agents/conversation_history.py +125 -2
  13. shotgun/agents/conversation_manager.py +57 -19
  14. shotgun/agents/export.py +6 -7
  15. shotgun/agents/history/compaction.py +10 -5
  16. shotgun/agents/history/context_extraction.py +93 -6
  17. shotgun/agents/history/history_processors.py +129 -12
  18. shotgun/agents/history/token_counting/__init__.py +31 -0
  19. shotgun/agents/history/token_counting/anthropic.py +127 -0
  20. shotgun/agents/history/token_counting/base.py +78 -0
  21. shotgun/agents/history/token_counting/openai.py +90 -0
  22. shotgun/agents/history/token_counting/sentencepiece_counter.py +127 -0
  23. shotgun/agents/history/token_counting/tokenizer_cache.py +92 -0
  24. shotgun/agents/history/token_counting/utils.py +144 -0
  25. shotgun/agents/history/token_estimation.py +12 -12
  26. shotgun/agents/llm.py +62 -0
  27. shotgun/agents/models.py +59 -4
  28. shotgun/agents/plan.py +6 -7
  29. shotgun/agents/research.py +7 -8
  30. shotgun/agents/specify.py +6 -7
  31. shotgun/agents/tasks.py +6 -7
  32. shotgun/agents/tools/__init__.py +0 -2
  33. shotgun/agents/tools/codebase/codebase_shell.py +6 -0
  34. shotgun/agents/tools/codebase/directory_lister.py +6 -0
  35. shotgun/agents/tools/codebase/file_read.py +11 -2
  36. shotgun/agents/tools/codebase/query_graph.py +6 -0
  37. shotgun/agents/tools/codebase/retrieve_code.py +6 -0
  38. shotgun/agents/tools/file_management.py +82 -16
  39. shotgun/agents/tools/registry.py +217 -0
  40. shotgun/agents/tools/web_search/__init__.py +55 -16
  41. shotgun/agents/tools/web_search/anthropic.py +76 -51
  42. shotgun/agents/tools/web_search/gemini.py +50 -27
  43. shotgun/agents/tools/web_search/openai.py +26 -17
  44. shotgun/agents/tools/web_search/utils.py +2 -2
  45. shotgun/agents/usage_manager.py +164 -0
  46. shotgun/api_endpoints.py +15 -0
  47. shotgun/cli/clear.py +53 -0
  48. shotgun/cli/compact.py +186 -0
  49. shotgun/cli/config.py +41 -67
  50. shotgun/cli/context.py +111 -0
  51. shotgun/cli/export.py +1 -1
  52. shotgun/cli/feedback.py +50 -0
  53. shotgun/cli/models.py +3 -2
  54. shotgun/cli/plan.py +1 -1
  55. shotgun/cli/research.py +1 -1
  56. shotgun/cli/specify.py +1 -1
  57. shotgun/cli/tasks.py +1 -1
  58. shotgun/cli/update.py +16 -2
  59. shotgun/codebase/core/change_detector.py +5 -3
  60. shotgun/codebase/core/code_retrieval.py +4 -2
  61. shotgun/codebase/core/ingestor.py +57 -16
  62. shotgun/codebase/core/manager.py +20 -7
  63. shotgun/codebase/core/nl_query.py +1 -1
  64. shotgun/codebase/models.py +4 -4
  65. shotgun/exceptions.py +32 -0
  66. shotgun/llm_proxy/__init__.py +19 -0
  67. shotgun/llm_proxy/clients.py +44 -0
  68. shotgun/llm_proxy/constants.py +15 -0
  69. shotgun/logging_config.py +18 -27
  70. shotgun/main.py +91 -12
  71. shotgun/posthog_telemetry.py +81 -10
  72. shotgun/prompts/agents/export.j2 +18 -1
  73. shotgun/prompts/agents/partials/common_agent_system_prompt.j2 +5 -1
  74. shotgun/prompts/agents/partials/interactive_mode.j2 +24 -7
  75. shotgun/prompts/agents/plan.j2 +1 -1
  76. shotgun/prompts/agents/research.j2 +1 -1
  77. shotgun/prompts/agents/specify.j2 +270 -3
  78. shotgun/prompts/agents/state/system_state.j2 +4 -0
  79. shotgun/prompts/agents/tasks.j2 +1 -1
  80. shotgun/prompts/loader.py +2 -2
  81. shotgun/prompts/tools/web_search.j2 +14 -0
  82. shotgun/sentry_telemetry.py +27 -18
  83. shotgun/settings.py +238 -0
  84. shotgun/shotgun_web/__init__.py +19 -0
  85. shotgun/shotgun_web/client.py +138 -0
  86. shotgun/shotgun_web/constants.py +21 -0
  87. shotgun/shotgun_web/models.py +47 -0
  88. shotgun/telemetry.py +24 -36
  89. shotgun/tui/app.py +251 -23
  90. shotgun/tui/commands/__init__.py +1 -1
  91. shotgun/tui/components/context_indicator.py +179 -0
  92. shotgun/tui/components/mode_indicator.py +70 -0
  93. shotgun/tui/components/status_bar.py +48 -0
  94. shotgun/tui/containers.py +91 -0
  95. shotgun/tui/dependencies.py +39 -0
  96. shotgun/tui/protocols.py +45 -0
  97. shotgun/tui/screens/chat/__init__.py +5 -0
  98. shotgun/tui/screens/chat/chat.tcss +54 -0
  99. shotgun/tui/screens/chat/chat_screen.py +1234 -0
  100. shotgun/tui/screens/chat/codebase_index_prompt_screen.py +64 -0
  101. shotgun/tui/screens/chat/codebase_index_selection.py +12 -0
  102. shotgun/tui/screens/chat/help_text.py +40 -0
  103. shotgun/tui/screens/chat/prompt_history.py +48 -0
  104. shotgun/tui/screens/chat.tcss +11 -0
  105. shotgun/tui/screens/chat_screen/command_providers.py +226 -11
  106. shotgun/tui/screens/chat_screen/history/__init__.py +22 -0
  107. shotgun/tui/screens/chat_screen/history/agent_response.py +66 -0
  108. shotgun/tui/screens/chat_screen/history/chat_history.py +116 -0
  109. shotgun/tui/screens/chat_screen/history/formatters.py +115 -0
  110. shotgun/tui/screens/chat_screen/history/partial_response.py +43 -0
  111. shotgun/tui/screens/chat_screen/history/user_question.py +42 -0
  112. shotgun/tui/screens/confirmation_dialog.py +151 -0
  113. shotgun/tui/screens/feedback.py +193 -0
  114. shotgun/tui/screens/github_issue.py +102 -0
  115. shotgun/tui/screens/model_picker.py +352 -0
  116. shotgun/tui/screens/onboarding.py +431 -0
  117. shotgun/tui/screens/pipx_migration.py +153 -0
  118. shotgun/tui/screens/provider_config.py +156 -39
  119. shotgun/tui/screens/shotgun_auth.py +295 -0
  120. shotgun/tui/screens/welcome.py +198 -0
  121. shotgun/tui/services/__init__.py +5 -0
  122. shotgun/tui/services/conversation_service.py +184 -0
  123. shotgun/tui/state/__init__.py +7 -0
  124. shotgun/tui/state/processing_state.py +185 -0
  125. shotgun/tui/utils/mode_progress.py +14 -7
  126. shotgun/tui/widgets/__init__.py +5 -0
  127. shotgun/tui/widgets/widget_coordinator.py +262 -0
  128. shotgun/utils/datetime_utils.py +77 -0
  129. shotgun/utils/env_utils.py +13 -0
  130. shotgun/utils/file_system_utils.py +22 -2
  131. shotgun/utils/marketing.py +110 -0
  132. shotgun/utils/update_checker.py +69 -14
  133. shotgun_sh-0.2.11.dist-info/METADATA +130 -0
  134. shotgun_sh-0.2.11.dist-info/RECORD +194 -0
  135. {shotgun_sh-0.1.14.dist-info → shotgun_sh-0.2.11.dist-info}/entry_points.txt +1 -0
  136. {shotgun_sh-0.1.14.dist-info → shotgun_sh-0.2.11.dist-info}/licenses/LICENSE +1 -1
  137. shotgun/agents/history/token_counting.py +0 -429
  138. shotgun/agents/tools/user_interaction.py +0 -37
  139. shotgun/tui/screens/chat.py +0 -797
  140. shotgun/tui/screens/chat_screen/history.py +0 -350
  141. shotgun_sh-0.1.14.dist-info/METADATA +0 -466
  142. shotgun_sh-0.1.14.dist-info/RECORD +0 -133
  143. {shotgun_sh-0.1.14.dist-info → shotgun_sh-0.2.11.dist-info}/WHEEL +0 -0
@@ -0,0 +1,217 @@
1
+ """Tool category registry using decorators for automatic registration.
2
+
3
+ This module provides a decorator-based system for categorizing tools used by agents.
4
+ Tools can be decorated with @register_tool to automatically register their category,
5
+ which is then used by the context analyzer to break down token usage by tool type.
6
+
7
+ It also provides a display registry system for tool formatting in the TUI, allowing
8
+ tools to declare how they should be displayed when streaming.
9
+ """
10
+
11
+ from collections.abc import Callable
12
+ from enum import StrEnum
13
+ from typing import TypeVar, overload
14
+
15
+ import sentry_sdk
16
+ from pydantic import BaseModel
17
+
18
+ from shotgun.logging_config import get_logger
19
+
20
+ logger = get_logger(__name__)
21
+
22
+ # Type variable for decorated functions
23
+ F = TypeVar("F", bound=Callable[..., object])
24
+
25
+
26
+ class ToolCategory(StrEnum):
27
+ """Categories for agent tools used in context analysis."""
28
+
29
+ CODEBASE_UNDERSTANDING = "codebase_understanding"
30
+ ARTIFACT_MANAGEMENT = "artifact_management"
31
+ WEB_RESEARCH = "web_research"
32
+ AGENT_RESPONSE = "agent_response"
33
+ UNKNOWN = "unknown"
34
+
35
+
36
+ class ToolDisplayConfig(BaseModel):
37
+ """Configuration for how a tool should be displayed in the TUI.
38
+
39
+ Attributes:
40
+ display_text: Text to show (e.g., "Reading file", "Querying code")
41
+ key_arg: Primary argument to extract from tool args for display
42
+ hide: Whether to completely hide this tool call from the UI
43
+ """
44
+
45
+ display_text: str
46
+ key_arg: str
47
+ hide: bool = False
48
+
49
+
50
+ # Global registry mapping tool names to categories
51
+ _TOOL_REGISTRY: dict[str, ToolCategory] = {}
52
+
53
+ # Global registry mapping tool names to display configs
54
+ _TOOL_DISPLAY_REGISTRY: dict[str, ToolDisplayConfig] = {}
55
+
56
+
57
+ @overload
58
+ def register_tool(
59
+ category: ToolCategory,
60
+ display_text: str,
61
+ key_arg: str,
62
+ ) -> Callable[[F], F]: ...
63
+
64
+
65
+ @overload
66
+ def register_tool(
67
+ category: ToolCategory,
68
+ display_text: str,
69
+ key_arg: str,
70
+ *,
71
+ hide: bool,
72
+ ) -> Callable[[F], F]: ...
73
+
74
+
75
+ def register_tool(
76
+ category: ToolCategory,
77
+ display_text: str,
78
+ key_arg: str,
79
+ *,
80
+ hide: bool = False,
81
+ ) -> Callable[[F], F]:
82
+ """Decorator to register a tool's category and display configuration.
83
+
84
+ Args:
85
+ category: The ToolCategory enum value for this tool
86
+ display_text: Text to show (e.g., "Reading file", "Querying code")
87
+ key_arg: Primary argument name to extract for display (e.g., "query", "filename")
88
+ hide: Whether to hide this tool call completely from the UI (default: False)
89
+
90
+ Returns:
91
+ Decorator function that registers the tool and returns it unchanged
92
+
93
+ Display Format:
94
+ - When key_arg value is missing: Shows just display_text (e.g., "Reading file")
95
+ - When key_arg value is present: Shows "display_text: key_arg_value" (e.g., "Reading file: foo.py")
96
+
97
+ Example:
98
+ @register_tool(
99
+ category=ToolCategory.CODEBASE_UNDERSTANDING,
100
+ display_text="Querying code",
101
+ key_arg="query",
102
+ )
103
+ async def query_graph(ctx: RunContext[AgentDeps], query: str) -> str:
104
+ ...
105
+ """
106
+
107
+ def decorator(func: F) -> F:
108
+ tool_name = func.__name__
109
+ _TOOL_REGISTRY[tool_name] = category
110
+ logger.debug(f"Registered tool '{tool_name}' as category '{category.value}'")
111
+
112
+ # Register display config
113
+ config = ToolDisplayConfig(
114
+ display_text=display_text,
115
+ key_arg=key_arg,
116
+ hide=hide,
117
+ )
118
+ _TOOL_DISPLAY_REGISTRY[tool_name] = config
119
+ logger.debug(f"Registered display config for tool '{tool_name}'")
120
+
121
+ return func
122
+
123
+ return decorator
124
+
125
+
126
+ # Backwards compatibility alias
127
+ tool_category = register_tool
128
+
129
+
130
+ def get_tool_category(tool_name: str) -> ToolCategory:
131
+ """Get category for a tool, logging unknown tools to Sentry.
132
+
133
+ Args:
134
+ tool_name: Name of the tool to look up
135
+
136
+ Returns:
137
+ ToolCategory enum value for the tool, or UNKNOWN if not registered
138
+ """
139
+ category = _TOOL_REGISTRY.get(tool_name)
140
+
141
+ if category is None:
142
+ logger.warning(f"Unknown tool encountered in context analysis: {tool_name}")
143
+ sentry_sdk.capture_message(
144
+ f"Unknown tool in context analysis: {tool_name}",
145
+ level="warning",
146
+ extras={"tool_name": tool_name},
147
+ )
148
+ return ToolCategory.UNKNOWN
149
+
150
+ return category
151
+
152
+
153
+ def register_special_tool(tool_name: str, category: ToolCategory) -> None:
154
+ """Register a special tool that doesn't have a decorator.
155
+
156
+ Used for tools like 'final_result' that aren't actual Python functions
157
+ but need to be categorized.
158
+
159
+ Args:
160
+ tool_name: Name of the special tool
161
+ category: Category to assign to this tool
162
+ """
163
+ _TOOL_REGISTRY[tool_name] = category
164
+ logger.debug(
165
+ f"Registered special tool '{tool_name}' as category '{category.value}'"
166
+ )
167
+
168
+
169
+ def get_tool_display_config(tool_name: str) -> ToolDisplayConfig | None:
170
+ """Get display configuration for a tool.
171
+
172
+ Args:
173
+ tool_name: Name of the tool to look up
174
+
175
+ Returns:
176
+ ToolDisplayConfig for the tool, or None if not registered
177
+ """
178
+ return _TOOL_DISPLAY_REGISTRY.get(tool_name)
179
+
180
+
181
+ def register_tool_display(
182
+ tool_name: str,
183
+ display_text: str,
184
+ key_arg: str,
185
+ *,
186
+ hide: bool = False,
187
+ ) -> None:
188
+ """Register a display config for a special tool that doesn't have a decorator.
189
+
190
+ Used for tools like 'final_result' or builtin tools that aren't actual Python functions.
191
+
192
+ Args:
193
+ tool_name: Name of the special tool
194
+ display_text: Text to show (e.g., "Reading file", "Querying code")
195
+ key_arg: Primary argument name to extract for display
196
+ hide: Whether to hide this tool call completely
197
+ """
198
+ config = ToolDisplayConfig(
199
+ display_text=display_text,
200
+ key_arg=key_arg,
201
+ hide=hide,
202
+ )
203
+ _TOOL_DISPLAY_REGISTRY[tool_name] = config
204
+ logger.debug(f"Registered display config for special tool '{tool_name}'")
205
+
206
+
207
+ # Register special tools that don't have decorators
208
+ register_special_tool("final_result", ToolCategory.AGENT_RESPONSE)
209
+ register_tool_display("final_result", display_text="", key_arg="", hide=True)
210
+
211
+ # Register builtin tools (tools that come from Pydantic AI or model providers)
212
+ # These don't have Python function definitions but need display formatting
213
+ register_tool_display(
214
+ "web_search",
215
+ display_text="Searching",
216
+ key_arg="query",
217
+ )
@@ -1,13 +1,17 @@
1
1
  """Web search tools for Pydantic AI agents.
2
2
 
3
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
4
+ - OpenAI: Uses Responses API with web_search tool (BYOK only)
5
+ - Anthropic: Uses Messages API with web_search_20250305 tool (BYOK only)
6
+ - Gemini: Uses grounding with Google Search via Pydantic AI (Shotgun Account and BYOK)
7
+
8
+ Shotgun Account: Only Gemini web search is available
9
+ BYOK: All tools work with direct provider API keys
7
10
  """
8
11
 
9
- from collections.abc import Callable
12
+ from collections.abc import Awaitable, Callable
10
13
 
14
+ from shotgun.agents.config import get_config_manager
11
15
  from shotgun.agents.config.models import ProviderType
12
16
  from shotgun.logging_config import get_logger
13
17
 
@@ -18,29 +22,64 @@ from .utils import is_provider_available
18
22
 
19
23
  logger = get_logger(__name__)
20
24
 
21
- # Type alias for web search tools
22
- WebSearchTool = Callable[[str], str]
25
+ # Type alias for web search tools (all now async)
26
+ WebSearchTool = Callable[[str], Awaitable[str]]
23
27
 
24
28
 
25
- def get_available_web_search_tools() -> list[WebSearchTool]:
29
+ async def get_available_web_search_tools() -> list[WebSearchTool]:
26
30
  """Get list of available web search tools based on configured API keys.
27
31
 
32
+ Works with both Shotgun Account (via LiteLLM proxy) and BYOK (individual provider keys).
33
+
34
+ Available tools:
35
+ - Gemini: Available for both Shotgun Account and BYOK
36
+ - Anthropic: BYOK only (uses Messages API with web search)
37
+ - OpenAI: BYOK only (uses Responses API not compatible with LiteLLM proxy)
38
+
28
39
  Returns:
29
40
  List of web search tool functions that have API keys configured
30
41
  """
31
42
  tools: list[WebSearchTool] = []
32
43
 
33
- if is_provider_available(ProviderType.OPENAI):
34
- logger.debug("✅ OpenAI web search tool available")
35
- tools.append(openai_web_search_tool)
44
+ # Check if using Shotgun Account
45
+ config_manager = get_config_manager()
46
+ config = await config_manager.load()
47
+ has_shotgun_key = config.shotgun.api_key is not None
48
+
49
+ if has_shotgun_key:
50
+ logger.debug("🔑 Shotgun Account - only Gemini web search available")
51
+
52
+ # Gemini: Only search tool available for Shotgun Account
53
+ if await is_provider_available(ProviderType.GOOGLE):
54
+ logger.debug("✅ Gemini web search tool available")
55
+ tools.append(gemini_web_search_tool)
56
+
57
+ # Anthropic: Not available for Shotgun Account (Gemini-only for Shotgun)
58
+ if await is_provider_available(ProviderType.ANTHROPIC):
59
+ logger.debug(
60
+ "⚠️ Anthropic web search requires BYOK (Shotgun Account uses Gemini only)"
61
+ )
62
+
63
+ # OpenAI: Not available for Shotgun Account (Responses API incompatible with proxy)
64
+ if await is_provider_available(ProviderType.OPENAI):
65
+ logger.debug(
66
+ "⚠️ OpenAI web search requires BYOK (Responses API not supported via proxy)"
67
+ )
68
+ else:
69
+ # BYOK mode: Load all available tools based on individual provider keys
70
+ logger.debug("🔑 BYOK mode - checking all provider web search tools")
71
+
72
+ if await is_provider_available(ProviderType.OPENAI):
73
+ logger.debug("✅ OpenAI web search tool available")
74
+ tools.append(openai_web_search_tool)
36
75
 
37
- if is_provider_available(ProviderType.ANTHROPIC):
38
- logger.debug("✅ Anthropic web search tool available")
39
- tools.append(anthropic_web_search_tool)
76
+ if await is_provider_available(ProviderType.ANTHROPIC):
77
+ logger.debug("✅ Anthropic web search tool available")
78
+ tools.append(anthropic_web_search_tool)
40
79
 
41
- if is_provider_available(ProviderType.GOOGLE):
42
- logger.debug("✅ Gemini web search tool available")
43
- tools.append(gemini_web_search_tool)
80
+ if await is_provider_available(ProviderType.GOOGLE):
81
+ logger.debug("✅ Gemini web search tool available")
82
+ tools.append(gemini_web_search_tool)
44
83
 
45
84
  if not tools:
46
85
  logger.warning("⚠️ No web search tools available - no API keys configured")
@@ -1,20 +1,35 @@
1
1
  """Anthropic web search tool implementation."""
2
2
 
3
- import anthropic
4
3
  from opentelemetry import trace
4
+ from pydantic_ai.messages import ModelMessage, ModelRequest, TextPart
5
+ from pydantic_ai.settings import ModelSettings
5
6
 
6
7
  from shotgun.agents.config import get_provider_model
8
+ from shotgun.agents.config.constants import MEDIUM_TEXT_8K_TOKENS
7
9
  from shotgun.agents.config.models import ProviderType
10
+ from shotgun.agents.llm import shotgun_model_request
11
+ from shotgun.agents.tools.registry import ToolCategory, register_tool
8
12
  from shotgun.logging_config import get_logger
13
+ from shotgun.prompts import PromptLoader
14
+ from shotgun.utils.datetime_utils import get_datetime_context
9
15
 
10
16
  logger = get_logger(__name__)
11
17
 
18
+ # Global prompt loader instance
19
+ prompt_loader = PromptLoader()
12
20
 
13
- def anthropic_web_search_tool(query: str) -> str:
14
- """Perform a web search using Anthropic's Claude API with streaming.
21
+
22
+ @register_tool(
23
+ category=ToolCategory.WEB_RESEARCH,
24
+ display_text="Searching web",
25
+ key_arg="query",
26
+ )
27
+ async def anthropic_web_search_tool(query: str) -> str:
28
+ """Perform a web search using Anthropic's Claude API.
15
29
 
16
30
  This tool uses Anthropic's web search capabilities to find current information
17
- about the given query. Results are streamed for faster response times.
31
+ about the given query. Works with both Shotgun API keys (via LiteLLM proxy)
32
+ and direct Anthropic API keys (BYOK).
18
33
 
19
34
  Args:
20
35
  query: The search query
@@ -27,49 +42,59 @@ def anthropic_web_search_tool(query: str) -> str:
27
42
  span = trace.get_current_span()
28
43
  span.set_attribute("input.value", f"**Query:** {query}\n")
29
44
 
30
- logger.debug("📡 Executing Anthropic web search with streaming prompt: %s", query)
45
+ logger.debug("📡 Executing Anthropic web search with prompt: %s", query)
31
46
 
32
- # Get API key from centralized configuration
47
+ # Get model configuration (supports both Shotgun and BYOK)
33
48
  try:
34
- model_config = get_provider_model(ProviderType.ANTHROPIC)
35
- api_key = model_config.api_key
49
+ model_config = await get_provider_model(ProviderType.ANTHROPIC)
36
50
  except ValueError as e:
37
51
  error_msg = f"Anthropic API key not configured: {str(e)}"
38
52
  logger.error("❌ %s", error_msg)
39
53
  span.set_attribute("output.value", f"**Error:**\n {error_msg}\n")
40
54
  return error_msg
41
55
 
42
- client = anthropic.Anthropic(api_key=api_key)
56
+ # Get datetime context for the search prompt
57
+ dt_context = get_datetime_context()
58
+
59
+ # Render search prompt from template
60
+ search_prompt = prompt_loader.render(
61
+ "tools/web_search.j2",
62
+ query=query,
63
+ current_datetime=dt_context.datetime_formatted,
64
+ timezone_name=dt_context.timezone_name,
65
+ utc_offset=dt_context.utc_offset,
66
+ )
43
67
 
44
- # Use the Messages API with web search tool and streaming
68
+ # Build the request messages
69
+ messages: list[ModelMessage] = [ModelRequest.user_text_prompt(search_prompt)]
70
+
71
+ # Use the Messages API with web search tool
45
72
  try:
46
- result_text = ""
47
-
48
- with client.messages.stream(
49
- model="claude-3-5-sonnet-latest",
50
- max_tokens=8192, # Maximum for Claude 3.5 Sonnet
51
- messages=[{"role": "user", "content": f"Search for: {query}"}],
52
- tools=[
53
- {
54
- "type": "web_search_20250305",
55
- "name": "web_search",
56
- }
57
- ],
58
- tool_choice={"type": "tool", "name": "web_search"},
59
- ) as stream:
60
- logger.debug("🌊 Started streaming Anthropic web search response")
61
-
62
- for event in stream:
63
- if event.type == "content_block_delta":
64
- if hasattr(event.delta, "text"):
65
- result_text += event.delta.text
66
- elif event.type == "message_start":
67
- logger.debug("🚀 Streaming started")
68
- elif event.type == "message_stop":
69
- logger.debug("✅ Streaming completed")
70
-
71
- if not result_text:
72
- result_text = "No content returned from search"
73
+ response = await shotgun_model_request(
74
+ model_config=model_config,
75
+ messages=messages,
76
+ model_settings=ModelSettings(
77
+ max_tokens=MEDIUM_TEXT_8K_TOKENS,
78
+ # Enable Anthropic web search tool
79
+ extra_body={
80
+ "tools": [
81
+ {
82
+ "type": "web_search_20250305",
83
+ "name": "web_search",
84
+ }
85
+ ],
86
+ "tool_choice": {"type": "tool", "name": "web_search"},
87
+ },
88
+ ),
89
+ )
90
+
91
+ # Extract text from response
92
+ result_text = "No content returned from search"
93
+ if response.parts:
94
+ for part in response.parts:
95
+ if isinstance(part, TextPart):
96
+ result_text = part.content
97
+ break
73
98
 
74
99
  logger.debug("📄 Anthropic web search result: %d characters", len(result_text))
75
100
  logger.debug(
@@ -88,9 +113,8 @@ def anthropic_web_search_tool(query: str) -> str:
88
113
  return error_msg
89
114
 
90
115
 
91
- def main() -> None:
116
+ async def main() -> None:
92
117
  """Main function for testing the Anthropic web search tool."""
93
- import os
94
118
  import sys
95
119
 
96
120
  from shotgun.logging_config import setup_logger
@@ -110,24 +134,23 @@ def main() -> None:
110
134
  # Join all arguments as the search query
111
135
  query = " ".join(sys.argv[1:])
112
136
 
113
- print("🔍 Testing Anthropic Web Search with streaming")
137
+ print("🔍 Testing Anthropic Web Search")
114
138
  print(f"📝 Query: {query}")
115
139
  print("=" * 60)
116
140
 
117
141
  # Check if API key is available
118
- if not (
119
- os.getenv("ANTHROPIC_API_KEY")
120
- or (
121
- callable(get_provider_model)
122
- and get_provider_model(ProviderType.ANTHROPIC).api_key
123
- )
124
- ):
125
- print(" Error: ANTHROPIC_API_KEY environment variable not set")
126
- print(" Please set it with: export ANTHROPIC_API_KEY=your_key_here")
142
+ try:
143
+ if callable(get_provider_model):
144
+ model_config = await get_provider_model(ProviderType.ANTHROPIC)
145
+ if not model_config.api_key:
146
+ raise ValueError("No API key configured")
147
+ except (ValueError, Exception):
148
+ print("❌ Error: Anthropic API key not configured")
149
+ print(" Please set it in your config file")
127
150
  sys.exit(1)
128
151
 
129
152
  try:
130
- result = anthropic_web_search_tool(query)
153
+ result = await anthropic_web_search_tool(query)
131
154
  print(f"✅ Search completed! Result length: {len(result)} characters")
132
155
  print("=" * 60)
133
156
  print("📄 RESULTS:")
@@ -141,4 +164,6 @@ def main() -> None:
141
164
 
142
165
 
143
166
  if __name__ == "__main__":
144
- main()
167
+ import asyncio
168
+
169
+ asyncio.run(main())
@@ -1,20 +1,35 @@
1
1
  """Gemini web search tool implementation."""
2
2
 
3
- import google.generativeai as genai
4
3
  from opentelemetry import trace
4
+ from pydantic_ai.messages import ModelMessage, ModelRequest
5
+ from pydantic_ai.settings import ModelSettings
5
6
 
6
7
  from shotgun.agents.config import get_provider_model
7
- from shotgun.agents.config.models import ProviderType
8
+ from shotgun.agents.config.constants import MEDIUM_TEXT_8K_TOKENS
9
+ from shotgun.agents.config.models import ModelName
10
+ from shotgun.agents.llm import shotgun_model_request
11
+ from shotgun.agents.tools.registry import ToolCategory, register_tool
8
12
  from shotgun.logging_config import get_logger
13
+ from shotgun.prompts import PromptLoader
14
+ from shotgun.utils.datetime_utils import get_datetime_context
9
15
 
10
16
  logger = get_logger(__name__)
11
17
 
18
+ # Global prompt loader instance
19
+ prompt_loader = PromptLoader()
12
20
 
13
- def gemini_web_search_tool(query: str) -> str:
21
+
22
+ @register_tool(
23
+ category=ToolCategory.WEB_RESEARCH,
24
+ display_text="Searching web",
25
+ key_arg="query",
26
+ )
27
+ async def gemini_web_search_tool(query: str) -> str:
14
28
  """Perform a web search using Google's Gemini API with grounding.
15
29
 
16
30
  This tool uses Gemini's Google Search grounding to find current information
17
- about the given query.
31
+ about the given query. Works with both Shotgun API keys (via LiteLLM proxy)
32
+ and direct Gemini API keys (BYOK).
18
33
 
19
34
  Args:
20
35
  query: The search query
@@ -29,44 +44,52 @@ def gemini_web_search_tool(query: str) -> str:
29
44
 
30
45
  logger.debug("📡 Executing Gemini web search with prompt: %s", query)
31
46
 
32
- # Get API key from centralized configuration
47
+ # Get model configuration (supports both Shotgun and BYOK)
33
48
  try:
34
- model_config = get_provider_model(ProviderType.GOOGLE)
35
- api_key = model_config.api_key
49
+ model_config = await get_provider_model(ModelName.GEMINI_2_5_FLASH)
36
50
  except ValueError as e:
37
51
  error_msg = f"Gemini API key not configured: {str(e)}"
38
52
  logger.error("❌ %s", error_msg)
39
53
  span.set_attribute("output.value", f"**Error:**\n {error_msg}\n")
40
54
  return error_msg
41
55
 
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]
56
+ # Get datetime context for the search prompt
57
+ dt_context = get_datetime_context()
47
58
 
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:
59
+ # Render search prompt from template
60
+ search_prompt = prompt_loader.render(
61
+ "tools/web_search.j2",
62
+ query=query,
63
+ current_datetime=dt_context.datetime_formatted,
64
+ timezone_name=dt_context.timezone_name,
65
+ utc_offset=dt_context.utc_offset,
66
+ )
50
67
 
51
- Query: {query}
68
+ # Build the request messages
69
+ messages: list[ModelMessage] = [ModelRequest.user_text_prompt(search_prompt)]
52
70
 
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
71
+ # Generate response using Pydantic AI with Google Search grounding
60
72
  try:
61
- response = model.generate_content(
62
- search_prompt,
63
- generation_config=genai.GenerationConfig( # type: ignore[attr-defined]
73
+ response = await shotgun_model_request(
74
+ model_config=model_config,
75
+ messages=messages,
76
+ model_settings=ModelSettings(
64
77
  temperature=0.3,
65
- max_output_tokens=8192,
78
+ max_tokens=MEDIUM_TEXT_8K_TOKENS,
79
+ # Enable Google Search grounding for Gemini
80
+ extra_body={"tools": [{"googleSearch": {}}]},
66
81
  ),
67
82
  )
68
83
 
69
- result_text = response.text or "No content returned from search"
84
+ # Extract text from response
85
+ from pydantic_ai.messages import TextPart
86
+
87
+ result_text = "No content returned from search"
88
+ if response.parts:
89
+ for part in response.parts:
90
+ if isinstance(part, TextPart):
91
+ result_text = part.content
92
+ break
70
93
 
71
94
  logger.debug("📄 Gemini web search result: %d characters", len(result_text))
72
95
  logger.debug(