shotgun-sh 0.4.0.dev1__py3-none-any.whl → 0.6.2__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.
- shotgun/agents/agent_manager.py +307 -8
- shotgun/agents/cancellation.py +103 -0
- shotgun/agents/common.py +12 -0
- shotgun/agents/config/README.md +0 -1
- shotgun/agents/config/manager.py +10 -7
- shotgun/agents/config/models.py +5 -27
- shotgun/agents/config/provider.py +44 -27
- shotgun/agents/conversation/history/token_counting/base.py +51 -9
- shotgun/agents/file_read.py +176 -0
- shotgun/agents/messages.py +15 -3
- shotgun/agents/models.py +24 -1
- shotgun/agents/router/models.py +8 -0
- shotgun/agents/router/tools/delegation_tools.py +55 -1
- shotgun/agents/router/tools/plan_tools.py +88 -7
- shotgun/agents/runner.py +17 -2
- shotgun/agents/tools/__init__.py +8 -0
- shotgun/agents/tools/codebase/directory_lister.py +27 -39
- shotgun/agents/tools/codebase/file_read.py +26 -35
- shotgun/agents/tools/codebase/query_graph.py +9 -0
- shotgun/agents/tools/codebase/retrieve_code.py +9 -0
- shotgun/agents/tools/file_management.py +32 -2
- shotgun/agents/tools/file_read_tools/__init__.py +7 -0
- shotgun/agents/tools/file_read_tools/multimodal_file_read.py +167 -0
- shotgun/agents/tools/markdown_tools/__init__.py +62 -0
- shotgun/agents/tools/markdown_tools/insert_section.py +148 -0
- shotgun/agents/tools/markdown_tools/models.py +86 -0
- shotgun/agents/tools/markdown_tools/remove_section.py +114 -0
- shotgun/agents/tools/markdown_tools/replace_section.py +119 -0
- shotgun/agents/tools/markdown_tools/utils.py +453 -0
- shotgun/agents/tools/registry.py +44 -6
- shotgun/agents/tools/web_search/openai.py +42 -23
- shotgun/attachments/__init__.py +41 -0
- shotgun/attachments/errors.py +60 -0
- shotgun/attachments/models.py +107 -0
- shotgun/attachments/parser.py +257 -0
- shotgun/attachments/processor.py +193 -0
- shotgun/build_constants.py +4 -7
- shotgun/cli/clear.py +2 -2
- shotgun/cli/codebase/commands.py +181 -65
- shotgun/cli/compact.py +2 -2
- shotgun/cli/context.py +2 -2
- shotgun/cli/error_handler.py +2 -2
- shotgun/cli/run.py +90 -0
- shotgun/cli/spec/backup.py +2 -1
- shotgun/codebase/__init__.py +2 -0
- shotgun/codebase/benchmarks/__init__.py +35 -0
- shotgun/codebase/benchmarks/benchmark_runner.py +309 -0
- shotgun/codebase/benchmarks/exporters.py +119 -0
- shotgun/codebase/benchmarks/formatters/__init__.py +49 -0
- shotgun/codebase/benchmarks/formatters/base.py +34 -0
- shotgun/codebase/benchmarks/formatters/json_formatter.py +106 -0
- shotgun/codebase/benchmarks/formatters/markdown.py +136 -0
- shotgun/codebase/benchmarks/models.py +129 -0
- shotgun/codebase/core/__init__.py +4 -0
- shotgun/codebase/core/call_resolution.py +91 -0
- shotgun/codebase/core/change_detector.py +11 -6
- shotgun/codebase/core/errors.py +159 -0
- shotgun/codebase/core/extractors/__init__.py +23 -0
- shotgun/codebase/core/extractors/base.py +138 -0
- shotgun/codebase/core/extractors/factory.py +63 -0
- shotgun/codebase/core/extractors/go/__init__.py +7 -0
- shotgun/codebase/core/extractors/go/extractor.py +122 -0
- shotgun/codebase/core/extractors/javascript/__init__.py +7 -0
- shotgun/codebase/core/extractors/javascript/extractor.py +132 -0
- shotgun/codebase/core/extractors/protocol.py +109 -0
- shotgun/codebase/core/extractors/python/__init__.py +7 -0
- shotgun/codebase/core/extractors/python/extractor.py +141 -0
- shotgun/codebase/core/extractors/rust/__init__.py +7 -0
- shotgun/codebase/core/extractors/rust/extractor.py +139 -0
- shotgun/codebase/core/extractors/types.py +15 -0
- shotgun/codebase/core/extractors/typescript/__init__.py +7 -0
- shotgun/codebase/core/extractors/typescript/extractor.py +92 -0
- shotgun/codebase/core/gitignore.py +252 -0
- shotgun/codebase/core/ingestor.py +644 -354
- shotgun/codebase/core/kuzu_compat.py +119 -0
- shotgun/codebase/core/language_config.py +239 -0
- shotgun/codebase/core/manager.py +256 -46
- shotgun/codebase/core/metrics_collector.py +310 -0
- shotgun/codebase/core/metrics_types.py +347 -0
- shotgun/codebase/core/parallel_executor.py +424 -0
- shotgun/codebase/core/work_distributor.py +254 -0
- shotgun/codebase/core/worker.py +768 -0
- shotgun/codebase/indexing_state.py +86 -0
- shotgun/codebase/models.py +94 -0
- shotgun/codebase/service.py +13 -0
- shotgun/exceptions.py +9 -9
- shotgun/main.py +3 -16
- shotgun/posthog_telemetry.py +165 -24
- shotgun/prompts/agents/file_read.j2 +48 -0
- shotgun/prompts/agents/partials/common_agent_system_prompt.j2 +19 -47
- shotgun/prompts/agents/partials/content_formatting.j2 +12 -33
- shotgun/prompts/agents/partials/interactive_mode.j2 +9 -32
- shotgun/prompts/agents/partials/router_delegation_mode.j2 +21 -22
- shotgun/prompts/agents/plan.j2 +14 -0
- shotgun/prompts/agents/router.j2 +531 -258
- shotgun/prompts/agents/specify.j2 +14 -0
- shotgun/prompts/agents/state/codebase/codebase_graphs_available.j2 +14 -1
- shotgun/prompts/agents/state/system_state.j2 +13 -11
- shotgun/prompts/agents/tasks.j2 +14 -0
- shotgun/settings.py +49 -10
- shotgun/tui/app.py +149 -18
- shotgun/tui/commands/__init__.py +9 -1
- shotgun/tui/components/attachment_bar.py +87 -0
- shotgun/tui/components/prompt_input.py +25 -28
- shotgun/tui/components/status_bar.py +14 -7
- shotgun/tui/dependencies.py +3 -8
- shotgun/tui/protocols.py +18 -0
- shotgun/tui/screens/chat/chat.tcss +15 -0
- shotgun/tui/screens/chat/chat_screen.py +766 -235
- shotgun/tui/screens/chat/codebase_index_prompt_screen.py +8 -4
- shotgun/tui/screens/chat_screen/attachment_hint.py +40 -0
- shotgun/tui/screens/chat_screen/command_providers.py +0 -10
- shotgun/tui/screens/chat_screen/history/chat_history.py +54 -14
- shotgun/tui/screens/chat_screen/history/formatters.py +22 -0
- shotgun/tui/screens/chat_screen/history/user_question.py +25 -3
- shotgun/tui/screens/database_locked_dialog.py +219 -0
- shotgun/tui/screens/database_timeout_dialog.py +158 -0
- shotgun/tui/screens/kuzu_error_dialog.py +135 -0
- shotgun/tui/screens/model_picker.py +1 -3
- shotgun/tui/screens/models.py +11 -0
- shotgun/tui/state/processing_state.py +19 -0
- shotgun/tui/widgets/widget_coordinator.py +18 -0
- shotgun/utils/file_system_utils.py +4 -1
- {shotgun_sh-0.4.0.dev1.dist-info → shotgun_sh-0.6.2.dist-info}/METADATA +87 -34
- {shotgun_sh-0.4.0.dev1.dist-info → shotgun_sh-0.6.2.dist-info}/RECORD +128 -79
- shotgun/cli/export.py +0 -81
- shotgun/cli/plan.py +0 -73
- shotgun/cli/research.py +0 -93
- shotgun/cli/specify.py +0 -70
- shotgun/cli/tasks.py +0 -78
- shotgun/sentry_telemetry.py +0 -232
- shotgun/tui/screens/onboarding.py +0 -584
- {shotgun_sh-0.4.0.dev1.dist-info → shotgun_sh-0.6.2.dist-info}/WHEEL +0 -0
- {shotgun_sh-0.4.0.dev1.dist-info → shotgun_sh-0.6.2.dist-info}/entry_points.txt +0 -0
- {shotgun_sh-0.4.0.dev1.dist-info → shotgun_sh-0.6.2.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""State tracking for codebase indexing operations."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from typing import ClassVar
|
|
5
|
+
|
|
6
|
+
from shotgun.logging_config import get_logger
|
|
7
|
+
|
|
8
|
+
logger = get_logger(__name__)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class IndexingState:
|
|
12
|
+
"""Tracks which graph_ids are currently being indexed.
|
|
13
|
+
|
|
14
|
+
This is a simple state container that tools can check to determine
|
|
15
|
+
if a graph is available or currently being built. This prevents
|
|
16
|
+
race conditions on Windows where Kuzu uses exclusive file locking.
|
|
17
|
+
|
|
18
|
+
Uses class-level state so all service instances share the same state.
|
|
19
|
+
This is similar to how CodebaseGraphManager shares connections.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
# Error message for when tools try to access a graph being indexed
|
|
23
|
+
INDEXING_IN_PROGRESS_ERROR = (
|
|
24
|
+
"This codebase is currently being indexed. "
|
|
25
|
+
"Please wait for indexing to complete before accessing it."
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
# Class-level state shared across all instances
|
|
29
|
+
_active_graphs: ClassVar[set[str]] = set()
|
|
30
|
+
_lock: ClassVar[asyncio.Lock | None] = None
|
|
31
|
+
|
|
32
|
+
@classmethod
|
|
33
|
+
def _get_lock(cls) -> asyncio.Lock:
|
|
34
|
+
"""Get or create the class-level lock."""
|
|
35
|
+
if cls._lock is None:
|
|
36
|
+
cls._lock = asyncio.Lock()
|
|
37
|
+
return cls._lock
|
|
38
|
+
|
|
39
|
+
async def start(self, graph_id: str) -> None:
|
|
40
|
+
"""Mark a graph as being indexed.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
graph_id: The graph ID that is starting to be indexed
|
|
44
|
+
"""
|
|
45
|
+
lock = self._get_lock()
|
|
46
|
+
async with lock:
|
|
47
|
+
self._active_graphs.add(graph_id)
|
|
48
|
+
logger.debug(f"Indexing started for graph: {graph_id}")
|
|
49
|
+
|
|
50
|
+
async def complete(self, graph_id: str) -> None:
|
|
51
|
+
"""Mark a graph as finished indexing.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
graph_id: The graph ID that finished indexing
|
|
55
|
+
"""
|
|
56
|
+
lock = self._get_lock()
|
|
57
|
+
async with lock:
|
|
58
|
+
self._active_graphs.discard(graph_id)
|
|
59
|
+
logger.debug(f"Indexing completed for graph: {graph_id}")
|
|
60
|
+
|
|
61
|
+
def is_active(self, graph_id: str) -> bool:
|
|
62
|
+
"""Check if a specific graph is currently being indexed.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
graph_id: The graph ID to check
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
True if the graph is currently being indexed
|
|
69
|
+
"""
|
|
70
|
+
return graph_id in self._active_graphs
|
|
71
|
+
|
|
72
|
+
def has_active(self) -> bool:
|
|
73
|
+
"""Check if any graph is currently being indexed.
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
True if any graph is being indexed
|
|
77
|
+
"""
|
|
78
|
+
return len(self._active_graphs) > 0
|
|
79
|
+
|
|
80
|
+
def get_active_ids(self) -> set[str]:
|
|
81
|
+
"""Get set of graph IDs currently being indexed.
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
Copy of the set of graph IDs being indexed
|
|
85
|
+
"""
|
|
86
|
+
return self._active_graphs.copy()
|
shotgun/codebase/models.py
CHANGED
|
@@ -16,6 +16,15 @@ class GraphStatus(StrEnum):
|
|
|
16
16
|
ERROR = "ERROR" # Last operation failed
|
|
17
17
|
|
|
18
18
|
|
|
19
|
+
class IgnoreReason(StrEnum):
|
|
20
|
+
"""Reason why a file or directory was ignored during indexing."""
|
|
21
|
+
|
|
22
|
+
HARDCODED = (
|
|
23
|
+
"hardcoded" # Matched hardcoded ignore patterns (venv, node_modules, etc.)
|
|
24
|
+
)
|
|
25
|
+
GITIGNORE = "gitignore" # Matched .gitignore pattern
|
|
26
|
+
|
|
27
|
+
|
|
19
28
|
class QueryType(StrEnum):
|
|
20
29
|
"""Type of query being executed."""
|
|
21
30
|
|
|
@@ -33,6 +42,55 @@ class ProgressPhase(StrEnum):
|
|
|
33
42
|
FLUSH_RELATIONSHIPS = "flush_relationships" # Flushing relationships to database
|
|
34
43
|
|
|
35
44
|
|
|
45
|
+
class NodeLabel(StrEnum):
|
|
46
|
+
"""Node type labels for the code knowledge graph."""
|
|
47
|
+
|
|
48
|
+
PROJECT = "Project" # Top-level project node
|
|
49
|
+
PACKAGE = "Package" # Python package/namespace
|
|
50
|
+
FOLDER = "Folder" # Directory structure
|
|
51
|
+
FILE = "File" # Source file
|
|
52
|
+
MODULE = "Module" # Python module
|
|
53
|
+
CLASS = "Class" # Class definition
|
|
54
|
+
FUNCTION = "Function" # Function definition
|
|
55
|
+
METHOD = "Method" # Method definition (inside a class)
|
|
56
|
+
FILE_METADATA = "FileMetadata" # File tracking metadata (hash, mtime)
|
|
57
|
+
EXTERNAL_PACKAGE = "ExternalPackage" # External dependency
|
|
58
|
+
DELETION_LOG = "DeletionLog" # Deletion audit trail
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class RelationshipType(StrEnum):
|
|
62
|
+
"""Relationship types for the code knowledge graph."""
|
|
63
|
+
|
|
64
|
+
# Containment relationships (used with suffixes _PKG, _FOLDER in tables)
|
|
65
|
+
CONTAINS_PACKAGE = "CONTAINS_PACKAGE" # Container to Package
|
|
66
|
+
CONTAINS_FOLDER = "CONTAINS_FOLDER" # Container to Folder
|
|
67
|
+
CONTAINS_FILE = "CONTAINS_FILE" # Container to File
|
|
68
|
+
CONTAINS_MODULE = "CONTAINS_MODULE" # Container to Module
|
|
69
|
+
|
|
70
|
+
# Definition relationships
|
|
71
|
+
DEFINES = "DEFINES" # Module to Class
|
|
72
|
+
DEFINES_FUNC = "DEFINES_FUNC" # Module to Function
|
|
73
|
+
DEFINES_METHOD = "DEFINES_METHOD" # Class to Method
|
|
74
|
+
|
|
75
|
+
# Call relationships
|
|
76
|
+
CALLS = "CALLS" # Function to Function
|
|
77
|
+
CALLS_FM = "CALLS_FM" # Function to Method
|
|
78
|
+
CALLS_MF = "CALLS_MF" # Method to Function
|
|
79
|
+
CALLS_MM = "CALLS_MM" # Method to Method
|
|
80
|
+
|
|
81
|
+
# Tracking relationships (FileMetadata to entity)
|
|
82
|
+
TRACKS_MODULE = "TRACKS_Module" # FileMetadata to Module
|
|
83
|
+
TRACKS_CLASS = "TRACKS_Class" # FileMetadata to Class
|
|
84
|
+
TRACKS_FUNCTION = "TRACKS_Function" # FileMetadata to Function
|
|
85
|
+
TRACKS_METHOD = "TRACKS_Method" # FileMetadata to Method
|
|
86
|
+
|
|
87
|
+
# Other relationships
|
|
88
|
+
INHERITS = "INHERITS" # Child Class to Parent Class
|
|
89
|
+
OVERRIDES = "OVERRIDES" # Method to Method (override)
|
|
90
|
+
IMPORTS = "IMPORTS" # Module to Module
|
|
91
|
+
DEPENDS_ON_EXTERNAL = "DEPENDS_ON_EXTERNAL" # Project to ExternalPackage
|
|
92
|
+
|
|
93
|
+
|
|
36
94
|
class IndexProgress(BaseModel):
|
|
37
95
|
"""Progress information for codebase indexing."""
|
|
38
96
|
|
|
@@ -49,6 +107,42 @@ class IndexProgress(BaseModel):
|
|
|
49
107
|
ProgressCallback = Callable[[IndexProgress], None]
|
|
50
108
|
|
|
51
109
|
|
|
110
|
+
class GitignoreStats(BaseModel):
|
|
111
|
+
"""Statistics from gitignore pattern matching."""
|
|
112
|
+
|
|
113
|
+
gitignore_files_loaded: int = Field(
|
|
114
|
+
default=0, description="Number of .gitignore files loaded"
|
|
115
|
+
)
|
|
116
|
+
patterns_loaded: int = Field(default=0, description="Total patterns loaded")
|
|
117
|
+
files_checked: int = Field(default=0, description="Number of paths checked")
|
|
118
|
+
files_ignored: int = Field(
|
|
119
|
+
default=0, description="Number of paths ignored by gitignore"
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class IndexingStats(BaseModel):
|
|
124
|
+
"""Statistics from codebase indexing."""
|
|
125
|
+
|
|
126
|
+
dirs_scanned: int = Field(default=0, description="Directories scanned")
|
|
127
|
+
dirs_ignored_hardcoded: int = Field(
|
|
128
|
+
default=0, description="Directories ignored by hardcoded patterns"
|
|
129
|
+
)
|
|
130
|
+
dirs_ignored_gitignore: int = Field(
|
|
131
|
+
default=0, description="Directories ignored by gitignore"
|
|
132
|
+
)
|
|
133
|
+
files_scanned: int = Field(default=0, description="Files scanned")
|
|
134
|
+
files_ignored_hardcoded: int = Field(
|
|
135
|
+
default=0, description="Files ignored by hardcoded patterns"
|
|
136
|
+
)
|
|
137
|
+
files_ignored_gitignore: int = Field(
|
|
138
|
+
default=0, description="Files ignored by gitignore"
|
|
139
|
+
)
|
|
140
|
+
files_ignored_no_parser: int = Field(
|
|
141
|
+
default=0, description="Files ignored due to no parser available"
|
|
142
|
+
)
|
|
143
|
+
files_processed: int = Field(default=0, description="Files successfully processed")
|
|
144
|
+
|
|
145
|
+
|
|
52
146
|
class OperationStats(BaseModel):
|
|
53
147
|
"""Statistics for a graph operation (build/update)."""
|
|
54
148
|
|
shotgun/codebase/service.py
CHANGED
|
@@ -7,6 +7,7 @@ from typing import Any
|
|
|
7
7
|
from shotgun.codebase.core.cypher_models import CypherGenerationNotPossibleError
|
|
8
8
|
from shotgun.codebase.core.manager import CodebaseGraphManager
|
|
9
9
|
from shotgun.codebase.core.nl_query import generate_cypher
|
|
10
|
+
from shotgun.codebase.indexing_state import IndexingState
|
|
10
11
|
from shotgun.codebase.models import CodebaseGraph, QueryResult, QueryType
|
|
11
12
|
from shotgun.logging_config import get_logger
|
|
12
13
|
|
|
@@ -28,6 +29,18 @@ class CodebaseService:
|
|
|
28
29
|
self.storage_dir = storage_dir
|
|
29
30
|
self.storage_dir.mkdir(parents=True, exist_ok=True)
|
|
30
31
|
self.manager = CodebaseGraphManager(storage_dir)
|
|
32
|
+
self.indexing = IndexingState()
|
|
33
|
+
|
|
34
|
+
def compute_graph_id(self, repo_path: str | Path) -> str:
|
|
35
|
+
"""Compute graph_id for a repo path without creating the graph.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
repo_path: Path to the repository
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
The graph_id that would be used for this repo path
|
|
42
|
+
"""
|
|
43
|
+
return self.manager.generate_graph_id(str(repo_path))
|
|
31
44
|
|
|
32
45
|
async def list_graphs(self) -> list[CodebaseGraph]:
|
|
33
46
|
"""List all existing graphs.
|
shotgun/exceptions.py
CHANGED
|
@@ -7,8 +7,8 @@ SHOTGUN_SIGNUP_URL = "https://shotgun.sh"
|
|
|
7
7
|
SHOTGUN_CONTACT_EMAIL = "contact@shotgun.sh"
|
|
8
8
|
|
|
9
9
|
|
|
10
|
-
class
|
|
11
|
-
"""Base for user-actionable errors that shouldn't be sent to
|
|
10
|
+
class UserActionableError(Exception): # noqa: N818
|
|
11
|
+
"""Base for user-actionable errors that shouldn't be sent to telemetry.
|
|
12
12
|
|
|
13
13
|
These errors represent expected user conditions requiring action
|
|
14
14
|
rather than bugs that need tracking.
|
|
@@ -37,7 +37,7 @@ class ErrorNotPickedUpBySentry(Exception): # noqa: N818
|
|
|
37
37
|
# ============================================================================
|
|
38
38
|
|
|
39
39
|
|
|
40
|
-
class AgentCancelledException(
|
|
40
|
+
class AgentCancelledException(UserActionableError): # noqa: N818
|
|
41
41
|
"""Raised when user cancels an agent operation."""
|
|
42
42
|
|
|
43
43
|
def __init__(self) -> None:
|
|
@@ -53,7 +53,7 @@ class AgentCancelledException(ErrorNotPickedUpBySentry):
|
|
|
53
53
|
return "⚠️ Operation cancelled by user"
|
|
54
54
|
|
|
55
55
|
|
|
56
|
-
class ContextSizeLimitExceeded(
|
|
56
|
+
class ContextSizeLimitExceeded(UserActionableError): # noqa: N818
|
|
57
57
|
"""Raised when conversation context exceeds the model's limits.
|
|
58
58
|
|
|
59
59
|
This is a user-actionable error - they need to either:
|
|
@@ -81,7 +81,7 @@ class ContextSizeLimitExceeded(ErrorNotPickedUpBySentry):
|
|
|
81
81
|
f"⚠️ **Context too large for {self.model_name}**\n\n"
|
|
82
82
|
f"Your conversation history exceeds this model's limit ({self.max_tokens:,} tokens).\n\n"
|
|
83
83
|
f"**Choose an action:**\n\n"
|
|
84
|
-
f"1. Switch to a larger model (
|
|
84
|
+
f"1. Switch to a larger model (`/` → Change Model)\n"
|
|
85
85
|
f"2. Switch to a larger model, compact (`/compact`), then switch back to {self.model_name}\n"
|
|
86
86
|
f"3. Clear conversation (`/clear`)\n"
|
|
87
87
|
)
|
|
@@ -103,7 +103,7 @@ class ContextSizeLimitExceeded(ErrorNotPickedUpBySentry):
|
|
|
103
103
|
# ============================================================================
|
|
104
104
|
|
|
105
105
|
|
|
106
|
-
class ShotgunAccountException(
|
|
106
|
+
class ShotgunAccountException(UserActionableError): # noqa: N818
|
|
107
107
|
"""Base class for Shotgun Account service errors.
|
|
108
108
|
|
|
109
109
|
TUI will check isinstance() of this class to show contact email UI.
|
|
@@ -216,7 +216,7 @@ class ShotgunRateLimitException(ShotgunAccountException):
|
|
|
216
216
|
# ============================================================================
|
|
217
217
|
|
|
218
218
|
|
|
219
|
-
class BYOKAPIException(
|
|
219
|
+
class BYOKAPIException(UserActionableError): # noqa: N818
|
|
220
220
|
"""Base class for BYOK API errors.
|
|
221
221
|
|
|
222
222
|
All BYOK errors suggest using Shotgun Account to avoid the issue.
|
|
@@ -313,7 +313,7 @@ class BYOKGenericAPIException(BYOKAPIException):
|
|
|
313
313
|
# ============================================================================
|
|
314
314
|
|
|
315
315
|
|
|
316
|
-
class GenericAPIStatusException(
|
|
316
|
+
class GenericAPIStatusException(UserActionableError): # noqa: N818
|
|
317
317
|
"""Raised for generic API status errors that don't fit other categories."""
|
|
318
318
|
|
|
319
319
|
def __init__(self, message: str):
|
|
@@ -334,7 +334,7 @@ class GenericAPIStatusException(ErrorNotPickedUpBySentry):
|
|
|
334
334
|
return f"⚠️ AI service error: {self.api_message}"
|
|
335
335
|
|
|
336
336
|
|
|
337
|
-
class UnknownAgentException(
|
|
337
|
+
class UnknownAgentException(UserActionableError): # noqa: N818
|
|
338
338
|
"""Raised for unknown/unclassified agent errors."""
|
|
339
339
|
|
|
340
340
|
def __init__(self, original_exception: Exception):
|
shotgun/main.py
CHANGED
|
@@ -28,18 +28,13 @@ from shotgun.cli import (
|
|
|
28
28
|
compact,
|
|
29
29
|
config,
|
|
30
30
|
context,
|
|
31
|
-
export,
|
|
32
31
|
feedback,
|
|
33
|
-
|
|
34
|
-
research,
|
|
32
|
+
run,
|
|
35
33
|
spec,
|
|
36
|
-
specify,
|
|
37
|
-
tasks,
|
|
38
34
|
update,
|
|
39
35
|
)
|
|
40
36
|
from shotgun.logging_config import configure_root_logger, get_logger
|
|
41
37
|
from shotgun.posthog_telemetry import setup_posthog_observability
|
|
42
|
-
from shotgun.sentry_telemetry import setup_sentry_observability
|
|
43
38
|
from shotgun.telemetry import setup_logfire_observability
|
|
44
39
|
from shotgun.tui import app as tui_app
|
|
45
40
|
from shotgun.utils.update_checker import perform_auto_update_async
|
|
@@ -66,11 +61,7 @@ try:
|
|
|
66
61
|
except Exception as e:
|
|
67
62
|
logger.debug("Configuration initialization warning: %s", e)
|
|
68
63
|
|
|
69
|
-
# Initialize
|
|
70
|
-
_sentry_enabled = setup_sentry_observability()
|
|
71
|
-
logger.debug("Sentry observability enabled: %s", _sentry_enabled)
|
|
72
|
-
|
|
73
|
-
# Initialize PostHog analytics
|
|
64
|
+
# Initialize PostHog analytics (includes exception tracking)
|
|
74
65
|
_posthog_enabled = setup_posthog_observability()
|
|
75
66
|
logger.debug("PostHog analytics enabled: %s", _posthog_enabled)
|
|
76
67
|
|
|
@@ -89,11 +80,7 @@ app.add_typer(
|
|
|
89
80
|
app.add_typer(context.app, name="context", help="Analyze conversation context usage")
|
|
90
81
|
app.add_typer(compact.app, name="compact", help="Compact conversation history")
|
|
91
82
|
app.add_typer(clear.app, name="clear", help="Clear conversation history")
|
|
92
|
-
app.add_typer(
|
|
93
|
-
app.add_typer(plan.app, name="plan", help="Generate structured plans")
|
|
94
|
-
app.add_typer(specify.app, name="specify", help="Generate comprehensive specifications")
|
|
95
|
-
app.add_typer(tasks.app, name="tasks", help="Generate task lists with agentic approach")
|
|
96
|
-
app.add_typer(export.app, name="export", help="Export artifacts to various formats")
|
|
83
|
+
app.add_typer(run.app, name="run", help="Run a prompt using the Router agent")
|
|
97
84
|
app.add_typer(update.app, name="update", help="Check for and install updates")
|
|
98
85
|
app.add_typer(feedback.app, name="feedback", help="Send us feedback")
|
|
99
86
|
app.add_typer(spec.app, name="spec", help="Manage shared specifications")
|
shotgun/posthog_telemetry.py
CHANGED
|
@@ -1,34 +1,86 @@
|
|
|
1
1
|
"""PostHog analytics setup for Shotgun."""
|
|
2
2
|
|
|
3
|
+
import platform
|
|
3
4
|
from enum import StrEnum
|
|
4
5
|
from typing import Any
|
|
5
6
|
|
|
6
|
-
import
|
|
7
|
+
from posthog import Posthog
|
|
7
8
|
from pydantic import BaseModel
|
|
8
9
|
|
|
9
10
|
from shotgun import __version__
|
|
10
11
|
from shotgun.agents.config import get_config_manager
|
|
11
12
|
from shotgun.agents.conversation import ConversationManager
|
|
13
|
+
from shotgun.exceptions import UserActionableError
|
|
12
14
|
from shotgun.logging_config import get_early_logger
|
|
13
15
|
from shotgun.settings import settings
|
|
14
16
|
|
|
15
17
|
# Use early logger to prevent automatic StreamHandler creation
|
|
16
18
|
logger = get_early_logger(__name__)
|
|
17
19
|
|
|
20
|
+
|
|
21
|
+
def _get_environment() -> str:
|
|
22
|
+
"""Determine environment from version string.
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
'development' for dev/rc/alpha/beta versions, 'production' otherwise
|
|
26
|
+
"""
|
|
27
|
+
if any(marker in __version__ for marker in ["dev", "rc", "alpha", "beta"]):
|
|
28
|
+
return "development"
|
|
29
|
+
return "production"
|
|
30
|
+
|
|
31
|
+
|
|
18
32
|
# Global PostHog client instance
|
|
19
|
-
_posthog_client = None
|
|
33
|
+
_posthog_client: Posthog | None = None
|
|
20
34
|
|
|
21
|
-
# Cache
|
|
35
|
+
# Cache user context to avoid async calls during event tracking
|
|
22
36
|
_shotgun_instance_id: str | None = None
|
|
37
|
+
_user_context: dict[str, Any] = {}
|
|
38
|
+
|
|
39
|
+
# Store original exception hook
|
|
40
|
+
_original_excepthook: Any = None
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _install_exception_hook() -> None:
|
|
44
|
+
"""Install custom exception hook to capture unhandled exceptions with full context."""
|
|
45
|
+
import sys
|
|
46
|
+
|
|
47
|
+
global _original_excepthook
|
|
48
|
+
|
|
49
|
+
# Store original excepthook
|
|
50
|
+
_original_excepthook = sys.excepthook
|
|
51
|
+
|
|
52
|
+
def custom_excepthook(
|
|
53
|
+
exc_type: type[BaseException],
|
|
54
|
+
exc_value: BaseException,
|
|
55
|
+
exc_traceback: Any,
|
|
56
|
+
) -> None:
|
|
57
|
+
"""Custom exception hook that captures exceptions to PostHog."""
|
|
58
|
+
# Only capture Exception subclasses (not KeyboardInterrupt, SystemExit, etc.)
|
|
59
|
+
if isinstance(exc_value, Exception):
|
|
60
|
+
capture_exception(exc_value)
|
|
61
|
+
|
|
62
|
+
# Flush PostHog to ensure exception is sent before process exits
|
|
63
|
+
if _posthog_client is not None:
|
|
64
|
+
try:
|
|
65
|
+
_posthog_client.flush() # type: ignore[no-untyped-call]
|
|
66
|
+
except Exception: # noqa: S110 - intentionally silent during crash
|
|
67
|
+
pass
|
|
68
|
+
|
|
69
|
+
# Call original excepthook to maintain normal behavior
|
|
70
|
+
if _original_excepthook is not None:
|
|
71
|
+
_original_excepthook(exc_type, exc_value, exc_traceback)
|
|
72
|
+
|
|
73
|
+
sys.excepthook = custom_excepthook
|
|
74
|
+
logger.debug("Installed custom exception hook for PostHog")
|
|
23
75
|
|
|
24
76
|
|
|
25
77
|
def setup_posthog_observability() -> bool:
|
|
26
|
-
"""Set up PostHog analytics for usage tracking.
|
|
78
|
+
"""Set up PostHog analytics for usage tracking and exception capture.
|
|
27
79
|
|
|
28
80
|
Returns:
|
|
29
81
|
True if PostHog was successfully set up, False otherwise
|
|
30
82
|
"""
|
|
31
|
-
global _posthog_client, _shotgun_instance_id
|
|
83
|
+
global _posthog_client, _shotgun_instance_id, _user_context
|
|
32
84
|
|
|
33
85
|
try:
|
|
34
86
|
# Check if PostHog is already initialized
|
|
@@ -46,27 +98,49 @@ def setup_posthog_observability() -> bool:
|
|
|
46
98
|
|
|
47
99
|
logger.debug("Using PostHog API key from settings")
|
|
48
100
|
|
|
49
|
-
|
|
50
|
-
# Dev versions contain "dev", "rc", "alpha", or "beta"
|
|
51
|
-
if any(marker in __version__ for marker in ["dev", "rc", "alpha", "beta"]):
|
|
52
|
-
environment = "development"
|
|
53
|
-
else:
|
|
54
|
-
environment = "production"
|
|
101
|
+
environment = _get_environment()
|
|
55
102
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
103
|
+
def on_error(e: Exception, batch: list[dict[str, Any]]) -> None:
|
|
104
|
+
"""Handle PostHog errors."""
|
|
105
|
+
logger.warning("PostHog error: %s", e)
|
|
59
106
|
|
|
60
|
-
#
|
|
61
|
-
_posthog_client =
|
|
107
|
+
# Initialize PostHog client (we use custom exception hook instead of autocapture)
|
|
108
|
+
_posthog_client = Posthog(
|
|
109
|
+
project_api_key=api_key,
|
|
110
|
+
host="https://us.i.posthog.com",
|
|
111
|
+
on_error=on_error,
|
|
112
|
+
)
|
|
62
113
|
|
|
63
|
-
# Cache
|
|
114
|
+
# Cache user context for later use (avoids async issues in exception capture)
|
|
64
115
|
try:
|
|
65
116
|
import asyncio
|
|
66
117
|
|
|
67
118
|
config_manager = get_config_manager()
|
|
68
119
|
_shotgun_instance_id = asyncio.run(config_manager.get_shotgun_instance_id())
|
|
69
120
|
|
|
121
|
+
# Load config to get account type and model info
|
|
122
|
+
config = asyncio.run(config_manager.load())
|
|
123
|
+
|
|
124
|
+
# Cache user context for exception tracking
|
|
125
|
+
is_shotgun_account = config.shotgun.has_valid_account
|
|
126
|
+
_user_context["account_type"] = "shotgun" if is_shotgun_account else "byok"
|
|
127
|
+
_user_context["selected_model"] = (
|
|
128
|
+
config.selected_model.value if config.selected_model else None
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
# Set user properties for tracking
|
|
132
|
+
_posthog_client.capture(
|
|
133
|
+
distinct_id=_shotgun_instance_id,
|
|
134
|
+
event="$identify",
|
|
135
|
+
properties={
|
|
136
|
+
"$set": {
|
|
137
|
+
"app_version": __version__,
|
|
138
|
+
"environment": environment,
|
|
139
|
+
"account_type": _user_context["account_type"],
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
)
|
|
143
|
+
|
|
70
144
|
logger.debug(
|
|
71
145
|
"PostHog initialized with shotgun instance ID: %s",
|
|
72
146
|
_shotgun_instance_id,
|
|
@@ -75,6 +149,9 @@ def setup_posthog_observability() -> bool:
|
|
|
75
149
|
logger.warning("Failed to load shotgun instance ID: %s", e)
|
|
76
150
|
# Continue anyway - we'll try to get it during event tracking
|
|
77
151
|
|
|
152
|
+
# Install custom exception hook to capture unhandled exceptions with full context
|
|
153
|
+
_install_exception_hook()
|
|
154
|
+
|
|
78
155
|
logger.debug(
|
|
79
156
|
"PostHog analytics configured successfully (environment: %s, version: %s)",
|
|
80
157
|
environment,
|
|
@@ -112,12 +189,7 @@ def track_event(event_name: str, properties: dict[str, Any] | None = None) -> No
|
|
|
112
189
|
if properties is None:
|
|
113
190
|
properties = {}
|
|
114
191
|
properties["version"] = __version__
|
|
115
|
-
|
|
116
|
-
# Determine environment
|
|
117
|
-
if any(marker in __version__ for marker in ["dev", "rc", "alpha", "beta"]):
|
|
118
|
-
properties["environment"] = "development"
|
|
119
|
-
else:
|
|
120
|
-
properties["environment"] = "production"
|
|
192
|
+
properties["environment"] = _get_environment()
|
|
121
193
|
|
|
122
194
|
# Track the event using PostHog's capture method
|
|
123
195
|
_posthog_client.capture(
|
|
@@ -128,13 +200,82 @@ def track_event(event_name: str, properties: dict[str, Any] | None = None) -> No
|
|
|
128
200
|
logger.warning("Failed to track PostHog event '%s': %s", event_name, e)
|
|
129
201
|
|
|
130
202
|
|
|
203
|
+
def capture_exception(
|
|
204
|
+
exception: Exception,
|
|
205
|
+
properties: dict[str, Any] | None = None,
|
|
206
|
+
) -> None:
|
|
207
|
+
"""Manually capture an exception in PostHog.
|
|
208
|
+
|
|
209
|
+
Uses the PostHog SDK's built-in capture_exception method which properly
|
|
210
|
+
formats the exception with stack traces, fingerprinting, and all required
|
|
211
|
+
fields for PostHog's Error Tracking system.
|
|
212
|
+
|
|
213
|
+
Note: UserActionableError exceptions are filtered out as they represent
|
|
214
|
+
expected user conditions, not bugs.
|
|
215
|
+
|
|
216
|
+
Args:
|
|
217
|
+
exception: The exception to capture
|
|
218
|
+
properties: Optional additional properties
|
|
219
|
+
"""
|
|
220
|
+
global _posthog_client, _shotgun_instance_id
|
|
221
|
+
|
|
222
|
+
if _posthog_client is None:
|
|
223
|
+
logger.debug("PostHog not initialized, skipping exception capture")
|
|
224
|
+
return
|
|
225
|
+
|
|
226
|
+
# Filter out user-actionable errors - these are expected conditions
|
|
227
|
+
if isinstance(exception, UserActionableError):
|
|
228
|
+
logger.debug(
|
|
229
|
+
"Skipping UserActionableError in PostHog exception capture: %s",
|
|
230
|
+
type(exception).__name__,
|
|
231
|
+
)
|
|
232
|
+
return
|
|
233
|
+
|
|
234
|
+
try:
|
|
235
|
+
if _shotgun_instance_id is None:
|
|
236
|
+
logger.warning(
|
|
237
|
+
"Shotgun instance ID not available, skipping exception capture"
|
|
238
|
+
)
|
|
239
|
+
return
|
|
240
|
+
|
|
241
|
+
# Build properties with app/user context
|
|
242
|
+
event_properties: dict[str, Any] = {
|
|
243
|
+
# App info
|
|
244
|
+
"version": __version__,
|
|
245
|
+
"environment": _get_environment(),
|
|
246
|
+
# System info
|
|
247
|
+
"python_version": platform.python_version(),
|
|
248
|
+
"os": platform.system(),
|
|
249
|
+
"os_version": platform.release(),
|
|
250
|
+
# User context
|
|
251
|
+
"shotgun_instance_id": _shotgun_instance_id,
|
|
252
|
+
"account_type": _user_context.get("account_type"),
|
|
253
|
+
"selected_model": _user_context.get("selected_model"),
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
# Add custom properties
|
|
257
|
+
if properties:
|
|
258
|
+
event_properties.update(properties)
|
|
259
|
+
|
|
260
|
+
# Use the SDK's built-in capture_exception method which properly
|
|
261
|
+
# formats the exception with stack traces, fingerprinting, etc.
|
|
262
|
+
_posthog_client.capture_exception(
|
|
263
|
+
exception,
|
|
264
|
+
distinct_id=_shotgun_instance_id,
|
|
265
|
+
properties=event_properties,
|
|
266
|
+
)
|
|
267
|
+
logger.debug("Captured exception in PostHog: %s", type(exception).__name__)
|
|
268
|
+
except Exception as e:
|
|
269
|
+
logger.warning("Failed to capture exception in PostHog: %s", e)
|
|
270
|
+
|
|
271
|
+
|
|
131
272
|
def shutdown() -> None:
|
|
132
273
|
"""Shutdown PostHog client and flush any pending events."""
|
|
133
274
|
global _posthog_client
|
|
134
275
|
|
|
135
276
|
if _posthog_client is not None:
|
|
136
277
|
try:
|
|
137
|
-
_posthog_client.shutdown()
|
|
278
|
+
_posthog_client.shutdown() # type: ignore[no-untyped-call]
|
|
138
279
|
logger.debug("PostHog client shutdown successfully")
|
|
139
280
|
except Exception as e:
|
|
140
281
|
logger.warning("Error shutting down PostHog: %s", e)
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
You are a File Reading Agent - a lightweight, focused assistant for finding and reading files.
|
|
2
|
+
|
|
3
|
+
## YOUR PURPOSE
|
|
4
|
+
|
|
5
|
+
Your job is to:
|
|
6
|
+
1. **Search** for files matching the user's description
|
|
7
|
+
2. **Read** file contents (including PDFs and images)
|
|
8
|
+
3. **Summarize** what you found
|
|
9
|
+
4. **Return** the file paths so they can be loaded into context
|
|
10
|
+
|
|
11
|
+
## TOOLS AVAILABLE
|
|
12
|
+
|
|
13
|
+
- `directory_lister` - List contents of a directory
|
|
14
|
+
- `file_read` - Read text file contents
|
|
15
|
+
- `read_file` - Read file by path
|
|
16
|
+
- `multimodal_file_read` - Verify PDFs and images exist and get their absolute paths
|
|
17
|
+
|
|
18
|
+
## WORKFLOW
|
|
19
|
+
|
|
20
|
+
1. **Understand the request** - What file is the user looking for?
|
|
21
|
+
2. **Search systematically** - Use directory_lister to explore, then read candidates
|
|
22
|
+
3. **For PDFs/images** - Use `multimodal_file_read` to verify they exist and get the absolute path
|
|
23
|
+
4. **Identify the right file** - Confirm you found what was requested
|
|
24
|
+
5. **Return results** - Include the absolute file path in `files_found` so the Router can load it
|
|
25
|
+
|
|
26
|
+
## OUTPUT FORMAT
|
|
27
|
+
|
|
28
|
+
When you find the file(s):
|
|
29
|
+
- Provide a brief summary of what you found
|
|
30
|
+
- **Always include absolute file paths in the `files_found` field** of your response
|
|
31
|
+
- This allows the Router to load the files into its context
|
|
32
|
+
|
|
33
|
+
Example response:
|
|
34
|
+
```json
|
|
35
|
+
{
|
|
36
|
+
"response": "Found the user stories document at /home/user/docs/user_stories_v2.pdf. It contains 3 user stories about authentication, profile management, and notifications.",
|
|
37
|
+
"files_found": ["/home/user/docs/user_stories_v2.pdf"]
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## IMPORTANT NOTES
|
|
42
|
+
|
|
43
|
+
- You are a **read-only** agent - you cannot create or modify files
|
|
44
|
+
- Use `multimodal_file_read` for PDFs and images to verify they exist and get absolute paths
|
|
45
|
+
- The Router will load the actual file content using the paths you return in `files_found`
|
|
46
|
+
- Be efficient - search systematically, don't read every file
|
|
47
|
+
- Return file paths as **absolute paths** for reliability
|
|
48
|
+
- If you can't find the file, say so clearly and suggest alternatives
|