shotgun-sh 0.2.11.dev1__py3-none-any.whl → 0.2.11.dev5__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 +150 -27
- 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/token_counting/anthropic.py +17 -1
- shotgun/agents/history/token_counting/base.py +14 -3
- shotgun/agents/history/token_counting/openai.py +8 -0
- 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/logging_config.py +10 -17
- shotgun/main.py +3 -1
- shotgun/posthog_telemetry.py +14 -4
- shotgun/sentry_telemetry.py +3 -1
- 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 +110 -18
- 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.dev1.dist-info → shotgun_sh-0.2.11.dev5.dist-info}/METADATA +2 -1
- {shotgun_sh-0.2.11.dev1.dist-info → shotgun_sh-0.2.11.dev5.dist-info}/RECORD +70 -67
- {shotgun_sh-0.2.11.dev1.dist-info → shotgun_sh-0.2.11.dev5.dist-info}/WHEEL +0 -0
- {shotgun_sh-0.2.11.dev1.dist-info → shotgun_sh-0.2.11.dev5.dist-info}/entry_points.txt +0 -0
- {shotgun_sh-0.2.11.dev1.dist-info → shotgun_sh-0.2.11.dev5.dist-info}/licenses/LICENSE +0 -0
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/logging_config.py
CHANGED
|
@@ -3,11 +3,15 @@
|
|
|
3
3
|
import logging
|
|
4
4
|
import logging.handlers
|
|
5
5
|
import sys
|
|
6
|
+
from datetime import datetime, timezone
|
|
6
7
|
from pathlib import Path
|
|
7
8
|
|
|
8
9
|
from shotgun.settings import settings
|
|
9
10
|
from shotgun.utils.env_utils import is_truthy
|
|
10
11
|
|
|
12
|
+
# Generate a single timestamp for this run to be used across all loggers
|
|
13
|
+
_RUN_TIMESTAMP = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
|
|
14
|
+
|
|
11
15
|
|
|
12
16
|
def get_log_directory() -> Path:
|
|
13
17
|
"""Get the log directory path, creating it if necessary.
|
|
@@ -66,10 +70,7 @@ def setup_logger(
|
|
|
66
70
|
logger = logging.getLogger(name)
|
|
67
71
|
|
|
68
72
|
# Check if we already have a file handler
|
|
69
|
-
has_file_handler = any(
|
|
70
|
-
isinstance(h, logging.handlers.TimedRotatingFileHandler)
|
|
71
|
-
for h in logger.handlers
|
|
72
|
-
)
|
|
73
|
+
has_file_handler = any(isinstance(h, logging.FileHandler) for h in logger.handlers)
|
|
73
74
|
|
|
74
75
|
# If we already have a file handler, just return the logger
|
|
75
76
|
if has_file_handler:
|
|
@@ -120,21 +121,16 @@ def setup_logger(
|
|
|
120
121
|
|
|
121
122
|
if file_logging_enabled:
|
|
122
123
|
try:
|
|
123
|
-
# Create file handler with
|
|
124
|
+
# Create file handler with ISO8601 timestamp for each run
|
|
124
125
|
log_dir = get_log_directory()
|
|
125
|
-
log_file = log_dir / "shotgun.log"
|
|
126
|
+
log_file = log_dir / f"shotgun-{_RUN_TIMESTAMP}.log"
|
|
126
127
|
|
|
127
|
-
# Use
|
|
128
|
-
file_handler = logging.
|
|
128
|
+
# Use regular FileHandler - each run gets its own isolated log file
|
|
129
|
+
file_handler = logging.FileHandler(
|
|
129
130
|
filename=log_file,
|
|
130
|
-
when="midnight", # Rotate at midnight
|
|
131
|
-
interval=1, # Every 1 day
|
|
132
|
-
backupCount=7, # Keep 7 days of logs
|
|
133
131
|
encoding="utf-8",
|
|
134
132
|
)
|
|
135
133
|
|
|
136
|
-
# Also set max file size (10MB) using RotatingFileHandler as fallback
|
|
137
|
-
# Note: We'll use TimedRotatingFileHandler which handles both time and size
|
|
138
134
|
file_handler.setLevel(getattr(logging, log_level))
|
|
139
135
|
|
|
140
136
|
# Use standard formatter for file (no colors)
|
|
@@ -189,10 +185,7 @@ def get_logger(name: str) -> logging.Logger:
|
|
|
189
185
|
logger = logging.getLogger(name)
|
|
190
186
|
|
|
191
187
|
# Check if we have a file handler already
|
|
192
|
-
has_file_handler = any(
|
|
193
|
-
isinstance(h, logging.handlers.TimedRotatingFileHandler)
|
|
194
|
-
for h in logger.handlers
|
|
195
|
-
)
|
|
188
|
+
has_file_handler = any(isinstance(h, logging.FileHandler) for h in logger.handlers)
|
|
196
189
|
|
|
197
190
|
# If no file handler, set up the logger (will add file handler)
|
|
198
191
|
if not has_file_handler:
|
shotgun/main.py
CHANGED
|
@@ -56,8 +56,10 @@ logger.debug("Logfire observability enabled: %s", _logfire_enabled)
|
|
|
56
56
|
|
|
57
57
|
# Initialize configuration
|
|
58
58
|
try:
|
|
59
|
+
import asyncio
|
|
60
|
+
|
|
59
61
|
config_manager = get_config_manager()
|
|
60
|
-
config_manager.load() # Ensure config is loaded at startup
|
|
62
|
+
asyncio.run(config_manager.load()) # Ensure config is loaded at startup
|
|
61
63
|
except Exception as e:
|
|
62
64
|
logger.debug("Configuration initialization warning: %s", e)
|
|
63
65
|
|
shotgun/posthog_telemetry.py
CHANGED
|
@@ -59,8 +59,10 @@ def setup_posthog_observability() -> bool:
|
|
|
59
59
|
|
|
60
60
|
# Set user context with anonymous shotgun instance ID from config
|
|
61
61
|
try:
|
|
62
|
+
import asyncio
|
|
63
|
+
|
|
62
64
|
config_manager = get_config_manager()
|
|
63
|
-
shotgun_instance_id = config_manager.get_shotgun_instance_id()
|
|
65
|
+
shotgun_instance_id = asyncio.run(config_manager.get_shotgun_instance_id())
|
|
64
66
|
|
|
65
67
|
# Identify the user in PostHog
|
|
66
68
|
posthog.identify( # type: ignore[attr-defined]
|
|
@@ -107,9 +109,11 @@ def track_event(event_name: str, properties: dict[str, Any] | None = None) -> No
|
|
|
107
109
|
return
|
|
108
110
|
|
|
109
111
|
try:
|
|
112
|
+
import asyncio
|
|
113
|
+
|
|
110
114
|
# Get shotgun instance ID for tracking
|
|
111
115
|
config_manager = get_config_manager()
|
|
112
|
-
shotgun_instance_id = config_manager.get_shotgun_instance_id()
|
|
116
|
+
shotgun_instance_id = asyncio.run(config_manager.get_shotgun_instance_id())
|
|
113
117
|
|
|
114
118
|
# Add version and environment to properties
|
|
115
119
|
if properties is None:
|
|
@@ -168,10 +172,16 @@ def submit_feedback_survey(feedback: Feedback) -> None:
|
|
|
168
172
|
logger.debug("PostHog not initialized, skipping feedback survey")
|
|
169
173
|
return
|
|
170
174
|
|
|
175
|
+
import asyncio
|
|
176
|
+
|
|
171
177
|
config_manager = get_config_manager()
|
|
172
|
-
config = config_manager.load()
|
|
178
|
+
config = asyncio.run(config_manager.load())
|
|
173
179
|
conversation_manager = ConversationManager()
|
|
174
|
-
conversation =
|
|
180
|
+
conversation = None
|
|
181
|
+
try:
|
|
182
|
+
conversation = asyncio.run(conversation_manager.load())
|
|
183
|
+
except Exception as e:
|
|
184
|
+
logger.debug(f"Failed to load conversation history: {e}")
|
|
175
185
|
last_10_messages = []
|
|
176
186
|
if conversation is not None:
|
|
177
187
|
last_10_messages = conversation.get_agent_messages()[:10]
|
shotgun/sentry_telemetry.py
CHANGED
|
@@ -50,10 +50,12 @@ def setup_sentry_observability() -> bool:
|
|
|
50
50
|
|
|
51
51
|
# Set user context with anonymous shotgun instance ID from config
|
|
52
52
|
try:
|
|
53
|
+
import asyncio
|
|
54
|
+
|
|
53
55
|
from shotgun.agents.config import get_config_manager
|
|
54
56
|
|
|
55
57
|
config_manager = get_config_manager()
|
|
56
|
-
shotgun_instance_id = config_manager.get_shotgun_instance_id()
|
|
58
|
+
shotgun_instance_id = asyncio.run(config_manager.get_shotgun_instance_id())
|
|
57
59
|
sentry_sdk.set_user({"id": shotgun_instance_id})
|
|
58
60
|
logger.debug("Sentry user context set with anonymous ID")
|
|
59
61
|
except Exception as e:
|
shotgun/telemetry.py
CHANGED
|
@@ -50,12 +50,14 @@ def setup_logfire_observability() -> bool:
|
|
|
50
50
|
|
|
51
51
|
# Set user context using baggage for all logs and spans
|
|
52
52
|
try:
|
|
53
|
+
import asyncio
|
|
54
|
+
|
|
53
55
|
from opentelemetry import baggage, context
|
|
54
56
|
|
|
55
57
|
from shotgun.agents.config import get_config_manager
|
|
56
58
|
|
|
57
59
|
config_manager = get_config_manager()
|
|
58
|
-
shotgun_instance_id = config_manager.get_shotgun_instance_id()
|
|
60
|
+
shotgun_instance_id = asyncio.run(config_manager.get_shotgun_instance_id())
|
|
59
61
|
|
|
60
62
|
# Set shotgun_instance_id as baggage in global context - this will be included in all logs/spans
|
|
61
63
|
ctx = baggage.set_baggage("shotgun_instance_id", shotgun_instance_id)
|