shotgun-sh 0.2.11.dev2__py3-none-any.whl → 0.2.11.dev7__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/agent_manager.py +194 -28
- shotgun/agents/common.py +14 -8
- shotgun/agents/config/manager.py +64 -33
- shotgun/agents/config/models.py +25 -1
- shotgun/agents/config/provider.py +2 -2
- shotgun/agents/context_analyzer/analyzer.py +2 -24
- shotgun/agents/conversation_manager.py +35 -19
- shotgun/agents/export.py +2 -2
- shotgun/agents/history/history_processors.py +99 -3
- shotgun/agents/history/token_counting/anthropic.py +17 -1
- shotgun/agents/history/token_counting/base.py +14 -3
- shotgun/agents/history/token_counting/openai.py +11 -1
- shotgun/agents/history/token_counting/sentencepiece_counter.py +8 -0
- shotgun/agents/history/token_counting/tokenizer_cache.py +3 -1
- shotgun/agents/history/token_counting/utils.py +0 -3
- shotgun/agents/plan.py +2 -2
- shotgun/agents/research.py +3 -3
- shotgun/agents/specify.py +2 -2
- shotgun/agents/tasks.py +2 -2
- shotgun/agents/tools/codebase/file_read.py +5 -2
- shotgun/agents/tools/file_management.py +11 -7
- shotgun/agents/tools/web_search/__init__.py +8 -8
- shotgun/agents/tools/web_search/anthropic.py +2 -2
- shotgun/agents/tools/web_search/gemini.py +1 -1
- shotgun/agents/tools/web_search/openai.py +1 -1
- shotgun/agents/tools/web_search/utils.py +2 -2
- shotgun/agents/usage_manager.py +16 -11
- shotgun/cli/clear.py +2 -1
- shotgun/cli/compact.py +3 -3
- shotgun/cli/config.py +8 -5
- shotgun/cli/context.py +2 -2
- shotgun/cli/export.py +1 -1
- shotgun/cli/feedback.py +4 -2
- shotgun/cli/plan.py +1 -1
- shotgun/cli/research.py +1 -1
- shotgun/cli/specify.py +1 -1
- shotgun/cli/tasks.py +1 -1
- shotgun/codebase/core/change_detector.py +5 -3
- shotgun/codebase/core/code_retrieval.py +4 -2
- shotgun/codebase/core/ingestor.py +10 -8
- shotgun/codebase/core/manager.py +3 -3
- shotgun/codebase/core/nl_query.py +1 -1
- shotgun/exceptions.py +32 -0
- shotgun/logging_config.py +10 -17
- shotgun/main.py +3 -1
- shotgun/posthog_telemetry.py +14 -4
- shotgun/sentry_telemetry.py +22 -2
- shotgun/telemetry.py +3 -1
- shotgun/tui/app.py +71 -65
- shotgun/tui/components/context_indicator.py +43 -0
- shotgun/tui/containers.py +15 -17
- shotgun/tui/dependencies.py +2 -2
- shotgun/tui/screens/chat/chat_screen.py +164 -40
- shotgun/tui/screens/chat/help_text.py +16 -15
- shotgun/tui/screens/chat_screen/command_providers.py +10 -0
- shotgun/tui/screens/feedback.py +4 -4
- shotgun/tui/screens/github_issue.py +102 -0
- shotgun/tui/screens/model_picker.py +21 -20
- shotgun/tui/screens/onboarding.py +431 -0
- shotgun/tui/screens/provider_config.py +50 -27
- shotgun/tui/screens/shotgun_auth.py +2 -2
- shotgun/tui/screens/welcome.py +14 -11
- shotgun/tui/services/conversation_service.py +16 -14
- shotgun/tui/utils/mode_progress.py +14 -7
- shotgun/tui/widgets/widget_coordinator.py +15 -0
- shotgun/utils/file_system_utils.py +19 -0
- shotgun/utils/marketing.py +110 -0
- {shotgun_sh-0.2.11.dev2.dist-info → shotgun_sh-0.2.11.dev7.dist-info}/METADATA +2 -1
- {shotgun_sh-0.2.11.dev2.dist-info → shotgun_sh-0.2.11.dev7.dist-info}/RECORD +72 -68
- {shotgun_sh-0.2.11.dev2.dist-info → shotgun_sh-0.2.11.dev7.dist-info}/WHEEL +0 -0
- {shotgun_sh-0.2.11.dev2.dist-info → shotgun_sh-0.2.11.dev7.dist-info}/entry_points.txt +0 -0
- {shotgun_sh-0.2.11.dev2.dist-info → shotgun_sh-0.2.11.dev7.dist-info}/licenses/LICENSE +0 -0
|
@@ -6,6 +6,8 @@ These tools are restricted to the .shotgun directory for security.
|
|
|
6
6
|
from pathlib import Path
|
|
7
7
|
from typing import Literal
|
|
8
8
|
|
|
9
|
+
import aiofiles
|
|
10
|
+
import aiofiles.os
|
|
9
11
|
from pydantic_ai import RunContext
|
|
10
12
|
|
|
11
13
|
from shotgun.agents.models import AgentDeps, AgentType, FileOperationType
|
|
@@ -181,10 +183,11 @@ async def read_file(ctx: RunContext[AgentDeps], filename: str) -> str:
|
|
|
181
183
|
try:
|
|
182
184
|
file_path = _validate_shotgun_path(filename)
|
|
183
185
|
|
|
184
|
-
if not
|
|
186
|
+
if not await aiofiles.os.path.exists(file_path):
|
|
185
187
|
raise FileNotFoundError(f"File not found: {filename}")
|
|
186
188
|
|
|
187
|
-
|
|
189
|
+
async with aiofiles.open(file_path, encoding="utf-8") as f:
|
|
190
|
+
content = await f.read()
|
|
188
191
|
logger.debug("📄 Read %d characters from %s", len(content), filename)
|
|
189
192
|
return content
|
|
190
193
|
|
|
@@ -233,21 +236,22 @@ async def write_file(
|
|
|
233
236
|
else:
|
|
234
237
|
operation = (
|
|
235
238
|
FileOperationType.CREATED
|
|
236
|
-
if not
|
|
239
|
+
if not await aiofiles.os.path.exists(file_path)
|
|
237
240
|
else FileOperationType.UPDATED
|
|
238
241
|
)
|
|
239
242
|
|
|
240
243
|
# Ensure parent directory exists
|
|
241
|
-
file_path.parent
|
|
244
|
+
await aiofiles.os.makedirs(file_path.parent, exist_ok=True)
|
|
242
245
|
|
|
243
246
|
# Write content
|
|
244
247
|
if mode == "a":
|
|
245
|
-
with open(file_path, "a", encoding="utf-8") as f:
|
|
246
|
-
f.write(content)
|
|
248
|
+
async with aiofiles.open(file_path, "a", encoding="utf-8") as f:
|
|
249
|
+
await f.write(content)
|
|
247
250
|
logger.debug("📄 Appended %d characters to %s", len(content), filename)
|
|
248
251
|
result = f"Successfully appended {len(content)} characters to {filename}"
|
|
249
252
|
else:
|
|
250
|
-
|
|
253
|
+
async with aiofiles.open(file_path, "w", encoding="utf-8") as f:
|
|
254
|
+
await f.write(content)
|
|
251
255
|
logger.debug("📄 Wrote %d characters to %s", len(content), filename)
|
|
252
256
|
result = f"Successfully wrote {len(content)} characters to {filename}"
|
|
253
257
|
|
|
@@ -26,7 +26,7 @@ logger = get_logger(__name__)
|
|
|
26
26
|
WebSearchTool = Callable[[str], Awaitable[str]]
|
|
27
27
|
|
|
28
28
|
|
|
29
|
-
def get_available_web_search_tools() -> list[WebSearchTool]:
|
|
29
|
+
async def get_available_web_search_tools() -> list[WebSearchTool]:
|
|
30
30
|
"""Get list of available web search tools based on configured API keys.
|
|
31
31
|
|
|
32
32
|
Works with both Shotgun Account (via LiteLLM proxy) and BYOK (individual provider keys).
|
|
@@ -43,25 +43,25 @@ def get_available_web_search_tools() -> list[WebSearchTool]:
|
|
|
43
43
|
|
|
44
44
|
# Check if using Shotgun Account
|
|
45
45
|
config_manager = get_config_manager()
|
|
46
|
-
config = config_manager.load()
|
|
46
|
+
config = await config_manager.load()
|
|
47
47
|
has_shotgun_key = config.shotgun.api_key is not None
|
|
48
48
|
|
|
49
49
|
if has_shotgun_key:
|
|
50
50
|
logger.debug("🔑 Shotgun Account - only Gemini web search available")
|
|
51
51
|
|
|
52
52
|
# Gemini: Only search tool available for Shotgun Account
|
|
53
|
-
if is_provider_available(ProviderType.GOOGLE):
|
|
53
|
+
if await is_provider_available(ProviderType.GOOGLE):
|
|
54
54
|
logger.debug("✅ Gemini web search tool available")
|
|
55
55
|
tools.append(gemini_web_search_tool)
|
|
56
56
|
|
|
57
57
|
# Anthropic: Not available for Shotgun Account (Gemini-only for Shotgun)
|
|
58
|
-
if is_provider_available(ProviderType.ANTHROPIC):
|
|
58
|
+
if await is_provider_available(ProviderType.ANTHROPIC):
|
|
59
59
|
logger.debug(
|
|
60
60
|
"⚠️ Anthropic web search requires BYOK (Shotgun Account uses Gemini only)"
|
|
61
61
|
)
|
|
62
62
|
|
|
63
63
|
# OpenAI: Not available for Shotgun Account (Responses API incompatible with proxy)
|
|
64
|
-
if is_provider_available(ProviderType.OPENAI):
|
|
64
|
+
if await is_provider_available(ProviderType.OPENAI):
|
|
65
65
|
logger.debug(
|
|
66
66
|
"⚠️ OpenAI web search requires BYOK (Responses API not supported via proxy)"
|
|
67
67
|
)
|
|
@@ -69,15 +69,15 @@ def get_available_web_search_tools() -> list[WebSearchTool]:
|
|
|
69
69
|
# BYOK mode: Load all available tools based on individual provider keys
|
|
70
70
|
logger.debug("🔑 BYOK mode - checking all provider web search tools")
|
|
71
71
|
|
|
72
|
-
if is_provider_available(ProviderType.OPENAI):
|
|
72
|
+
if await is_provider_available(ProviderType.OPENAI):
|
|
73
73
|
logger.debug("✅ OpenAI web search tool available")
|
|
74
74
|
tools.append(openai_web_search_tool)
|
|
75
75
|
|
|
76
|
-
if is_provider_available(ProviderType.ANTHROPIC):
|
|
76
|
+
if await is_provider_available(ProviderType.ANTHROPIC):
|
|
77
77
|
logger.debug("✅ Anthropic web search tool available")
|
|
78
78
|
tools.append(anthropic_web_search_tool)
|
|
79
79
|
|
|
80
|
-
if is_provider_available(ProviderType.GOOGLE):
|
|
80
|
+
if await is_provider_available(ProviderType.GOOGLE):
|
|
81
81
|
logger.debug("✅ Gemini web search tool available")
|
|
82
82
|
tools.append(gemini_web_search_tool)
|
|
83
83
|
|
|
@@ -46,7 +46,7 @@ async def anthropic_web_search_tool(query: str) -> str:
|
|
|
46
46
|
|
|
47
47
|
# Get model configuration (supports both Shotgun and BYOK)
|
|
48
48
|
try:
|
|
49
|
-
model_config = get_provider_model(ProviderType.ANTHROPIC)
|
|
49
|
+
model_config = await get_provider_model(ProviderType.ANTHROPIC)
|
|
50
50
|
except ValueError as e:
|
|
51
51
|
error_msg = f"Anthropic API key not configured: {str(e)}"
|
|
52
52
|
logger.error("❌ %s", error_msg)
|
|
@@ -141,7 +141,7 @@ async def main() -> None:
|
|
|
141
141
|
# Check if API key is available
|
|
142
142
|
try:
|
|
143
143
|
if callable(get_provider_model):
|
|
144
|
-
model_config = get_provider_model(ProviderType.ANTHROPIC)
|
|
144
|
+
model_config = await get_provider_model(ProviderType.ANTHROPIC)
|
|
145
145
|
if not model_config.api_key:
|
|
146
146
|
raise ValueError("No API key configured")
|
|
147
147
|
except (ValueError, Exception):
|
|
@@ -46,7 +46,7 @@ async def gemini_web_search_tool(query: str) -> str:
|
|
|
46
46
|
|
|
47
47
|
# Get model configuration (supports both Shotgun and BYOK)
|
|
48
48
|
try:
|
|
49
|
-
model_config = get_provider_model(ModelName.GEMINI_2_5_FLASH)
|
|
49
|
+
model_config = await get_provider_model(ModelName.GEMINI_2_5_FLASH)
|
|
50
50
|
except ValueError as e:
|
|
51
51
|
error_msg = f"Gemini API key not configured: {str(e)}"
|
|
52
52
|
logger.error("❌ %s", error_msg)
|
|
@@ -43,7 +43,7 @@ async def openai_web_search_tool(query: str) -> str:
|
|
|
43
43
|
|
|
44
44
|
# Get API key from centralized configuration
|
|
45
45
|
try:
|
|
46
|
-
model_config = get_provider_model(ProviderType.OPENAI)
|
|
46
|
+
model_config = await get_provider_model(ProviderType.OPENAI)
|
|
47
47
|
api_key = model_config.api_key
|
|
48
48
|
except ValueError as e:
|
|
49
49
|
error_msg = f"OpenAI API key not configured: {str(e)}"
|
|
@@ -4,7 +4,7 @@ from shotgun.agents.config import get_provider_model
|
|
|
4
4
|
from shotgun.agents.config.models import ProviderType
|
|
5
5
|
|
|
6
6
|
|
|
7
|
-
def is_provider_available(provider: ProviderType) -> bool:
|
|
7
|
+
async def is_provider_available(provider: ProviderType) -> bool:
|
|
8
8
|
"""Check if a provider has API key configured.
|
|
9
9
|
|
|
10
10
|
Args:
|
|
@@ -14,7 +14,7 @@ def is_provider_available(provider: ProviderType) -> bool:
|
|
|
14
14
|
True if the provider has valid credentials configured (from config or env)
|
|
15
15
|
"""
|
|
16
16
|
try:
|
|
17
|
-
get_provider_model(provider)
|
|
17
|
+
await get_provider_model(provider)
|
|
18
18
|
return True
|
|
19
19
|
except ValueError:
|
|
20
20
|
return False
|
shotgun/agents/usage_manager.py
CHANGED
|
@@ -6,6 +6,8 @@ from logging import getLogger
|
|
|
6
6
|
from pathlib import Path
|
|
7
7
|
from typing import TypeAlias
|
|
8
8
|
|
|
9
|
+
import aiofiles
|
|
10
|
+
import aiofiles.os
|
|
9
11
|
from genai_prices import calc_price
|
|
10
12
|
from pydantic import BaseModel, Field
|
|
11
13
|
from pydantic_ai import RunUsage
|
|
@@ -48,9 +50,10 @@ class SessionUsageManager:
|
|
|
48
50
|
self._model_providers: dict[ModelName, ProviderType] = {}
|
|
49
51
|
self._usage_log: list[UsageLogEntry] = []
|
|
50
52
|
self._usage_path: Path = get_shotgun_home() / "usage.json"
|
|
51
|
-
|
|
53
|
+
# Note: restore_usage_state needs to be called asynchronously after init
|
|
54
|
+
# Caller should use: manager = SessionUsageManager(); await manager.restore_usage_state()
|
|
52
55
|
|
|
53
|
-
def add_usage(
|
|
56
|
+
async def add_usage(
|
|
54
57
|
self, usage: RunUsage, *, model_name: ModelName, provider: ProviderType
|
|
55
58
|
) -> None:
|
|
56
59
|
self.usage[model_name] += usage
|
|
@@ -58,7 +61,7 @@ class SessionUsageManager:
|
|
|
58
61
|
self._usage_log.append(
|
|
59
62
|
UsageLogEntry(model_name=model_name, usage=usage, provider=provider)
|
|
60
63
|
)
|
|
61
|
-
self.persist_usage_state()
|
|
64
|
+
await self.persist_usage_state()
|
|
62
65
|
|
|
63
66
|
def get_usage_report(self) -> dict[ModelName, RunUsage]:
|
|
64
67
|
return self.usage.copy()
|
|
@@ -78,7 +81,7 @@ class SessionUsageManager:
|
|
|
78
81
|
def build_usage_hint(self) -> str | None:
|
|
79
82
|
return format_usage_hint(self.get_usage_breakdown())
|
|
80
83
|
|
|
81
|
-
def persist_usage_state(self) -> None:
|
|
84
|
+
async def persist_usage_state(self) -> None:
|
|
82
85
|
state = UsageState(
|
|
83
86
|
usage=dict(self.usage.items()),
|
|
84
87
|
model_providers=self._model_providers.copy(),
|
|
@@ -86,23 +89,25 @@ class SessionUsageManager:
|
|
|
86
89
|
)
|
|
87
90
|
|
|
88
91
|
try:
|
|
89
|
-
self._usage_path.parent
|
|
90
|
-
|
|
91
|
-
|
|
92
|
+
await aiofiles.os.makedirs(self._usage_path.parent, exist_ok=True)
|
|
93
|
+
json_content = json.dumps(state.model_dump(mode="json"), indent=2)
|
|
94
|
+
async with aiofiles.open(self._usage_path, "w", encoding="utf-8") as f:
|
|
95
|
+
await f.write(json_content)
|
|
92
96
|
logger.debug("Usage state persisted to %s", self._usage_path)
|
|
93
97
|
except Exception as exc:
|
|
94
98
|
logger.error(
|
|
95
99
|
"Failed to persist usage state to %s: %s", self._usage_path, exc
|
|
96
100
|
)
|
|
97
101
|
|
|
98
|
-
def restore_usage_state(self) -> None:
|
|
99
|
-
if not
|
|
102
|
+
async def restore_usage_state(self) -> None:
|
|
103
|
+
if not await aiofiles.os.path.exists(self._usage_path):
|
|
100
104
|
logger.debug("No usage state file found at %s", self._usage_path)
|
|
101
105
|
return
|
|
102
106
|
|
|
103
107
|
try:
|
|
104
|
-
with self._usage_path
|
|
105
|
-
|
|
108
|
+
async with aiofiles.open(self._usage_path, encoding="utf-8") as f:
|
|
109
|
+
content = await f.read()
|
|
110
|
+
data = json.loads(content)
|
|
106
111
|
|
|
107
112
|
state = UsageState.model_validate(data)
|
|
108
113
|
except Exception as exc:
|
shotgun/cli/clear.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""Clear command for shotgun CLI."""
|
|
2
2
|
|
|
3
|
+
import asyncio
|
|
3
4
|
from pathlib import Path
|
|
4
5
|
|
|
5
6
|
import typer
|
|
@@ -37,7 +38,7 @@ def clear() -> None:
|
|
|
37
38
|
|
|
38
39
|
# Clear the conversation
|
|
39
40
|
manager = ConversationManager(conversation_file)
|
|
40
|
-
manager.clear()
|
|
41
|
+
asyncio.run(manager.clear())
|
|
41
42
|
|
|
42
43
|
console.print(
|
|
43
44
|
"[green]✓[/green] Conversation cleared successfully", style="bold"
|
shotgun/cli/compact.py
CHANGED
|
@@ -79,7 +79,7 @@ async def compact_conversation() -> dict[str, Any]:
|
|
|
79
79
|
|
|
80
80
|
# Load conversation
|
|
81
81
|
manager = ConversationManager(conversation_file)
|
|
82
|
-
conversation = manager.load()
|
|
82
|
+
conversation = await manager.load()
|
|
83
83
|
|
|
84
84
|
if not conversation:
|
|
85
85
|
raise ValueError("Conversation file is empty or corrupted")
|
|
@@ -91,7 +91,7 @@ async def compact_conversation() -> dict[str, Any]:
|
|
|
91
91
|
raise ValueError("No agent messages found in conversation")
|
|
92
92
|
|
|
93
93
|
# Get model config
|
|
94
|
-
model_config = get_provider_model()
|
|
94
|
+
model_config = await get_provider_model()
|
|
95
95
|
|
|
96
96
|
# Calculate before metrics
|
|
97
97
|
original_message_count = len(agent_messages)
|
|
@@ -133,7 +133,7 @@ async def compact_conversation() -> dict[str, Any]:
|
|
|
133
133
|
|
|
134
134
|
# Save compacted conversation
|
|
135
135
|
conversation.set_agent_messages(compacted_messages)
|
|
136
|
-
manager.save(conversation)
|
|
136
|
+
await manager.save(conversation)
|
|
137
137
|
|
|
138
138
|
logger.info(
|
|
139
139
|
f"Compacted conversation: {original_message_count} → {compacted_message_count} messages "
|
shotgun/cli/config.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""Configuration management CLI commands."""
|
|
2
2
|
|
|
3
|
+
import asyncio
|
|
3
4
|
import json
|
|
4
5
|
from typing import Annotated, Any
|
|
5
6
|
|
|
@@ -44,7 +45,7 @@ def init(
|
|
|
44
45
|
console.print()
|
|
45
46
|
|
|
46
47
|
# Initialize with defaults
|
|
47
|
-
config_manager.initialize()
|
|
48
|
+
asyncio.run(config_manager.initialize())
|
|
48
49
|
|
|
49
50
|
# Ask for provider
|
|
50
51
|
provider_choices = ["openai", "anthropic", "google"]
|
|
@@ -76,7 +77,7 @@ def init(
|
|
|
76
77
|
|
|
77
78
|
if api_key:
|
|
78
79
|
# update_provider will automatically set selected_model for first provider
|
|
79
|
-
config_manager.update_provider(provider, api_key=api_key)
|
|
80
|
+
asyncio.run(config_manager.update_provider(provider, api_key=api_key))
|
|
80
81
|
|
|
81
82
|
console.print(
|
|
82
83
|
f"\n✅ [bold green]Configuration saved to {config_manager.config_path}[/bold green]"
|
|
@@ -84,7 +85,7 @@ def init(
|
|
|
84
85
|
console.print("🎯 You can now use Shotgun with your configured provider!")
|
|
85
86
|
|
|
86
87
|
else:
|
|
87
|
-
config_manager.initialize()
|
|
88
|
+
asyncio.run(config_manager.initialize())
|
|
88
89
|
console.print(f"✅ Configuration initialized at {config_manager.config_path}")
|
|
89
90
|
|
|
90
91
|
|
|
@@ -112,7 +113,7 @@ def set(
|
|
|
112
113
|
|
|
113
114
|
try:
|
|
114
115
|
if api_key:
|
|
115
|
-
config_manager.update_provider(provider, api_key=api_key)
|
|
116
|
+
asyncio.run(config_manager.update_provider(provider, api_key=api_key))
|
|
116
117
|
|
|
117
118
|
console.print(f"✅ Configuration updated for {provider}")
|
|
118
119
|
|
|
@@ -133,8 +134,10 @@ def get(
|
|
|
133
134
|
] = False,
|
|
134
135
|
) -> None:
|
|
135
136
|
"""Display current configuration."""
|
|
137
|
+
import asyncio
|
|
138
|
+
|
|
136
139
|
config_manager = get_config_manager()
|
|
137
|
-
config = config_manager.load()
|
|
140
|
+
config = asyncio.run(config_manager.load())
|
|
138
141
|
|
|
139
142
|
if json_output:
|
|
140
143
|
# Convert to dict and mask secrets
|
shotgun/cli/context.py
CHANGED
|
@@ -79,7 +79,7 @@ async def analyze_context() -> ContextAnalysisOutput:
|
|
|
79
79
|
|
|
80
80
|
# Load conversation
|
|
81
81
|
manager = ConversationManager(conversation_file)
|
|
82
|
-
conversation = manager.load()
|
|
82
|
+
conversation = await manager.load()
|
|
83
83
|
|
|
84
84
|
if not conversation:
|
|
85
85
|
raise ValueError("Conversation file is empty or corrupted")
|
|
@@ -91,7 +91,7 @@ async def analyze_context() -> ContextAnalysisOutput:
|
|
|
91
91
|
raise ValueError("No agent messages found in conversation")
|
|
92
92
|
|
|
93
93
|
# Get model config (use default provider settings)
|
|
94
|
-
model_config = get_provider_model()
|
|
94
|
+
model_config = await get_provider_model()
|
|
95
95
|
|
|
96
96
|
# Debug: Log the model being used
|
|
97
97
|
logger.debug(f"Using model: {model_config.name.value}")
|
shotgun/cli/export.py
CHANGED
|
@@ -63,7 +63,7 @@ def export(
|
|
|
63
63
|
)
|
|
64
64
|
|
|
65
65
|
# Create the export agent with deps and provider
|
|
66
|
-
agent, deps = create_export_agent(agent_runtime_options, provider)
|
|
66
|
+
agent, deps = asyncio.run(create_export_agent(agent_runtime_options, provider))
|
|
67
67
|
|
|
68
68
|
# Start export process
|
|
69
69
|
logger.info("🎯 Starting export...")
|
shotgun/cli/feedback.py
CHANGED
|
@@ -28,9 +28,11 @@ def send_feedback(
|
|
|
28
28
|
],
|
|
29
29
|
) -> None:
|
|
30
30
|
"""Initialize Shotgun configuration."""
|
|
31
|
+
import asyncio
|
|
32
|
+
|
|
31
33
|
config_manager = get_config_manager()
|
|
32
|
-
config_manager.load()
|
|
33
|
-
shotgun_instance_id = config_manager.get_shotgun_instance_id()
|
|
34
|
+
asyncio.run(config_manager.load())
|
|
35
|
+
shotgun_instance_id = asyncio.run(config_manager.get_shotgun_instance_id())
|
|
34
36
|
|
|
35
37
|
if not description:
|
|
36
38
|
console.print(
|
shotgun/cli/plan.py
CHANGED
|
@@ -55,7 +55,7 @@ def plan(
|
|
|
55
55
|
)
|
|
56
56
|
|
|
57
57
|
# Create the plan agent with deps and provider
|
|
58
|
-
agent, deps = create_plan_agent(agent_runtime_options, provider)
|
|
58
|
+
agent, deps = asyncio.run(create_plan_agent(agent_runtime_options, provider))
|
|
59
59
|
|
|
60
60
|
# Start planning process
|
|
61
61
|
logger.info("🎯 Starting planning...")
|
shotgun/cli/research.py
CHANGED
|
@@ -73,7 +73,7 @@ async def async_research(
|
|
|
73
73
|
agent_runtime_options = AgentRuntimeOptions(interactive_mode=not non_interactive)
|
|
74
74
|
|
|
75
75
|
# Create the research agent with deps and provider
|
|
76
|
-
agent, deps = create_research_agent(agent_runtime_options, provider)
|
|
76
|
+
agent, deps = await create_research_agent(agent_runtime_options, provider)
|
|
77
77
|
|
|
78
78
|
# Start research process
|
|
79
79
|
logger.info("🔬 Starting research...")
|
shotgun/cli/specify.py
CHANGED
|
@@ -51,7 +51,7 @@ def specify(
|
|
|
51
51
|
)
|
|
52
52
|
|
|
53
53
|
# Create the specify agent with deps and provider
|
|
54
|
-
agent, deps = create_specify_agent(agent_runtime_options, provider)
|
|
54
|
+
agent, deps = asyncio.run(create_specify_agent(agent_runtime_options, provider))
|
|
55
55
|
|
|
56
56
|
# Start specification process
|
|
57
57
|
logger.info("📋 Starting specification generation...")
|
shotgun/cli/tasks.py
CHANGED
|
@@ -60,7 +60,7 @@ def tasks(
|
|
|
60
60
|
)
|
|
61
61
|
|
|
62
62
|
# Create the tasks agent with deps and provider
|
|
63
|
-
agent, deps = create_tasks_agent(agent_runtime_options, provider)
|
|
63
|
+
agent, deps = asyncio.run(create_tasks_agent(agent_runtime_options, provider))
|
|
64
64
|
|
|
65
65
|
# Start task creation process
|
|
66
66
|
logger.info("🎯 Starting task creation...")
|
|
@@ -6,6 +6,7 @@ from enum import Enum
|
|
|
6
6
|
from pathlib import Path
|
|
7
7
|
from typing import Any, cast
|
|
8
8
|
|
|
9
|
+
import aiofiles
|
|
9
10
|
import kuzu
|
|
10
11
|
|
|
11
12
|
from shotgun.logging_config import get_logger
|
|
@@ -301,7 +302,7 @@ class ChangeDetector:
|
|
|
301
302
|
# Direct substring match
|
|
302
303
|
return pattern in filepath
|
|
303
304
|
|
|
304
|
-
def _calculate_file_hash(self, filepath: Path) -> str:
|
|
305
|
+
async def _calculate_file_hash(self, filepath: Path) -> str:
|
|
305
306
|
"""Calculate hash of file contents.
|
|
306
307
|
|
|
307
308
|
Args:
|
|
@@ -311,8 +312,9 @@ class ChangeDetector:
|
|
|
311
312
|
SHA256 hash of file contents
|
|
312
313
|
"""
|
|
313
314
|
try:
|
|
314
|
-
with open(filepath, "rb") as f:
|
|
315
|
-
|
|
315
|
+
async with aiofiles.open(filepath, "rb") as f:
|
|
316
|
+
content = await f.read()
|
|
317
|
+
return hashlib.sha256(content).hexdigest()
|
|
316
318
|
except Exception as e:
|
|
317
319
|
logger.error(f"Failed to calculate hash for {filepath}: {e}")
|
|
318
320
|
return ""
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from pathlib import Path
|
|
4
4
|
from typing import TYPE_CHECKING
|
|
5
5
|
|
|
6
|
+
import aiofiles
|
|
6
7
|
from pydantic import BaseModel
|
|
7
8
|
|
|
8
9
|
from shotgun.logging_config import get_logger
|
|
@@ -141,8 +142,9 @@ async def retrieve_code_by_qualified_name(
|
|
|
141
142
|
|
|
142
143
|
# Read the file and extract the snippet
|
|
143
144
|
try:
|
|
144
|
-
with
|
|
145
|
-
|
|
145
|
+
async with aiofiles.open(full_path, encoding="utf-8") as f:
|
|
146
|
+
content = await f.read()
|
|
147
|
+
all_lines = content.splitlines(keepends=True)
|
|
146
148
|
|
|
147
149
|
# Extract the relevant lines (1-indexed to 0-indexed)
|
|
148
150
|
snippet_lines = all_lines[start_line - 1 : end_line]
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""Kuzu graph ingestor for building code knowledge graphs."""
|
|
2
2
|
|
|
3
|
+
import asyncio
|
|
3
4
|
import hashlib
|
|
4
5
|
import os
|
|
5
6
|
import time
|
|
@@ -8,6 +9,7 @@ from collections import defaultdict
|
|
|
8
9
|
from pathlib import Path
|
|
9
10
|
from typing import Any
|
|
10
11
|
|
|
12
|
+
import aiofiles
|
|
11
13
|
import kuzu
|
|
12
14
|
from tree_sitter import Node, Parser, QueryCursor
|
|
13
15
|
|
|
@@ -619,7 +621,7 @@ class SimpleGraphBuilder:
|
|
|
619
621
|
# Don't let progress callback errors crash the build
|
|
620
622
|
logger.debug(f"Progress callback error: {e}")
|
|
621
623
|
|
|
622
|
-
def run(self) -> None:
|
|
624
|
+
async def run(self) -> None:
|
|
623
625
|
"""Run the three-pass graph building process."""
|
|
624
626
|
logger.info(f"Building graph for project: {self.project_name}")
|
|
625
627
|
|
|
@@ -629,7 +631,7 @@ class SimpleGraphBuilder:
|
|
|
629
631
|
|
|
630
632
|
# Pass 2: Definitions
|
|
631
633
|
logger.info("Pass 2: Processing files and extracting definitions...")
|
|
632
|
-
self._process_files()
|
|
634
|
+
await self._process_files()
|
|
633
635
|
|
|
634
636
|
# Pass 3: Relationships
|
|
635
637
|
logger.info("Pass 3: Processing relationships (calls, imports)...")
|
|
@@ -771,7 +773,7 @@ class SimpleGraphBuilder:
|
|
|
771
773
|
phase_complete=True,
|
|
772
774
|
)
|
|
773
775
|
|
|
774
|
-
def _process_files(self) -> None:
|
|
776
|
+
async def _process_files(self) -> None:
|
|
775
777
|
"""Second pass: Process files and extract definitions."""
|
|
776
778
|
# First pass: Count total files
|
|
777
779
|
total_files = 0
|
|
@@ -807,7 +809,7 @@ class SimpleGraphBuilder:
|
|
|
807
809
|
lang_config = get_language_config(ext)
|
|
808
810
|
|
|
809
811
|
if lang_config and lang_config.name in self.parsers:
|
|
810
|
-
self._process_single_file(filepath, lang_config.name)
|
|
812
|
+
await self._process_single_file(filepath, lang_config.name)
|
|
811
813
|
file_count += 1
|
|
812
814
|
|
|
813
815
|
# Report progress after each file
|
|
@@ -832,7 +834,7 @@ class SimpleGraphBuilder:
|
|
|
832
834
|
phase_complete=True,
|
|
833
835
|
)
|
|
834
836
|
|
|
835
|
-
def _process_single_file(self, filepath: Path, language: str) -> None:
|
|
837
|
+
async def _process_single_file(self, filepath: Path, language: str) -> None:
|
|
836
838
|
"""Process a single file."""
|
|
837
839
|
relative_path = filepath.relative_to(self.repo_path)
|
|
838
840
|
relative_path_str = str(relative_path).replace(os.sep, "/")
|
|
@@ -873,8 +875,8 @@ class SimpleGraphBuilder:
|
|
|
873
875
|
|
|
874
876
|
# Parse file
|
|
875
877
|
try:
|
|
876
|
-
with open(filepath, "rb") as f:
|
|
877
|
-
content = f.read()
|
|
878
|
+
async with aiofiles.open(filepath, "rb") as f:
|
|
879
|
+
content = await f.read()
|
|
878
880
|
|
|
879
881
|
parser = self.parsers[language]
|
|
880
882
|
tree = parser.parse(content)
|
|
@@ -1636,7 +1638,7 @@ class CodebaseIngestor:
|
|
|
1636
1638
|
)
|
|
1637
1639
|
if self.project_name:
|
|
1638
1640
|
builder.project_name = self.project_name
|
|
1639
|
-
builder.run()
|
|
1641
|
+
asyncio.run(builder.run())
|
|
1640
1642
|
|
|
1641
1643
|
logger.info(f"Graph successfully created at: {self.db_path}")
|
|
1642
1644
|
|
shotgun/codebase/core/manager.py
CHANGED
|
@@ -769,7 +769,7 @@ class CodebaseGraphManager:
|
|
|
769
769
|
|
|
770
770
|
lang_config = get_language_config(full_path.suffix)
|
|
771
771
|
if lang_config and lang_config.name in parsers:
|
|
772
|
-
builder._process_single_file(full_path, lang_config.name)
|
|
772
|
+
await builder._process_single_file(full_path, lang_config.name)
|
|
773
773
|
stats["nodes_modified"] += 1 # Approximate
|
|
774
774
|
|
|
775
775
|
# Process additions
|
|
@@ -784,7 +784,7 @@ class CodebaseGraphManager:
|
|
|
784
784
|
|
|
785
785
|
lang_config = get_language_config(full_path.suffix)
|
|
786
786
|
if lang_config and lang_config.name in parsers:
|
|
787
|
-
builder._process_single_file(full_path, lang_config.name)
|
|
787
|
+
await builder._process_single_file(full_path, lang_config.name)
|
|
788
788
|
stats["nodes_added"] += 1 # Approximate
|
|
789
789
|
|
|
790
790
|
# Flush all pending operations
|
|
@@ -1751,7 +1751,7 @@ class CodebaseGraphManager:
|
|
|
1751
1751
|
)
|
|
1752
1752
|
|
|
1753
1753
|
# Build the graph
|
|
1754
|
-
builder.run()
|
|
1754
|
+
asyncio.run(builder.run())
|
|
1755
1755
|
|
|
1756
1756
|
# Run build in thread pool
|
|
1757
1757
|
await anyio.to_thread.run_sync(_build_graph)
|
|
@@ -34,7 +34,7 @@ async def llm_cypher_prompt(
|
|
|
34
34
|
Returns:
|
|
35
35
|
CypherGenerationResponse with cypher_query, can_generate flag, and reason if not
|
|
36
36
|
"""
|
|
37
|
-
model_config = get_provider_model()
|
|
37
|
+
model_config = await get_provider_model()
|
|
38
38
|
|
|
39
39
|
# Create an agent with structured output for Cypher generation
|
|
40
40
|
cypher_agent = Agent(
|
shotgun/exceptions.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""General exceptions for Shotgun application."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class ErrorNotPickedUpBySentry(Exception): # noqa: N818
|
|
5
|
+
"""Base for user-actionable errors that shouldn't be sent to Sentry.
|
|
6
|
+
|
|
7
|
+
These errors represent expected user conditions requiring action
|
|
8
|
+
rather than bugs that need tracking.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ContextSizeLimitExceeded(ErrorNotPickedUpBySentry):
|
|
13
|
+
"""Raised when conversation context exceeds the model's limits.
|
|
14
|
+
|
|
15
|
+
This is a user-actionable error - they need to either:
|
|
16
|
+
1. Switch to a larger context model
|
|
17
|
+
2. Switch to a larger model, compact their conversation, then switch back
|
|
18
|
+
3. Clear the conversation and start fresh
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(self, model_name: str, max_tokens: int):
|
|
22
|
+
"""Initialize the exception.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
model_name: Name of the model whose limit was exceeded
|
|
26
|
+
max_tokens: Maximum tokens allowed by the model
|
|
27
|
+
"""
|
|
28
|
+
self.model_name = model_name
|
|
29
|
+
self.max_tokens = max_tokens
|
|
30
|
+
super().__init__(
|
|
31
|
+
f"Context too large for {model_name} (limit: {max_tokens:,} tokens)"
|
|
32
|
+
)
|