shotgun-sh 0.1.14__py3-none-any.whl → 0.2.11__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of shotgun-sh might be problematic. Click here for more details.
- shotgun/agents/agent_manager.py +715 -75
- shotgun/agents/common.py +80 -75
- shotgun/agents/config/constants.py +21 -10
- shotgun/agents/config/manager.py +322 -97
- shotgun/agents/config/models.py +114 -84
- shotgun/agents/config/provider.py +232 -88
- shotgun/agents/context_analyzer/__init__.py +28 -0
- shotgun/agents/context_analyzer/analyzer.py +471 -0
- shotgun/agents/context_analyzer/constants.py +9 -0
- shotgun/agents/context_analyzer/formatter.py +115 -0
- shotgun/agents/context_analyzer/models.py +212 -0
- shotgun/agents/conversation_history.py +125 -2
- shotgun/agents/conversation_manager.py +57 -19
- shotgun/agents/export.py +6 -7
- shotgun/agents/history/compaction.py +10 -5
- shotgun/agents/history/context_extraction.py +93 -6
- shotgun/agents/history/history_processors.py +129 -12
- shotgun/agents/history/token_counting/__init__.py +31 -0
- shotgun/agents/history/token_counting/anthropic.py +127 -0
- shotgun/agents/history/token_counting/base.py +78 -0
- shotgun/agents/history/token_counting/openai.py +90 -0
- shotgun/agents/history/token_counting/sentencepiece_counter.py +127 -0
- shotgun/agents/history/token_counting/tokenizer_cache.py +92 -0
- shotgun/agents/history/token_counting/utils.py +144 -0
- shotgun/agents/history/token_estimation.py +12 -12
- shotgun/agents/llm.py +62 -0
- shotgun/agents/models.py +59 -4
- shotgun/agents/plan.py +6 -7
- shotgun/agents/research.py +7 -8
- shotgun/agents/specify.py +6 -7
- shotgun/agents/tasks.py +6 -7
- shotgun/agents/tools/__init__.py +0 -2
- shotgun/agents/tools/codebase/codebase_shell.py +6 -0
- shotgun/agents/tools/codebase/directory_lister.py +6 -0
- shotgun/agents/tools/codebase/file_read.py +11 -2
- shotgun/agents/tools/codebase/query_graph.py +6 -0
- shotgun/agents/tools/codebase/retrieve_code.py +6 -0
- shotgun/agents/tools/file_management.py +82 -16
- shotgun/agents/tools/registry.py +217 -0
- shotgun/agents/tools/web_search/__init__.py +55 -16
- shotgun/agents/tools/web_search/anthropic.py +76 -51
- shotgun/agents/tools/web_search/gemini.py +50 -27
- shotgun/agents/tools/web_search/openai.py +26 -17
- shotgun/agents/tools/web_search/utils.py +2 -2
- shotgun/agents/usage_manager.py +164 -0
- shotgun/api_endpoints.py +15 -0
- shotgun/cli/clear.py +53 -0
- shotgun/cli/compact.py +186 -0
- shotgun/cli/config.py +41 -67
- shotgun/cli/context.py +111 -0
- shotgun/cli/export.py +1 -1
- shotgun/cli/feedback.py +50 -0
- shotgun/cli/models.py +3 -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/cli/update.py +16 -2
- shotgun/codebase/core/change_detector.py +5 -3
- shotgun/codebase/core/code_retrieval.py +4 -2
- shotgun/codebase/core/ingestor.py +57 -16
- shotgun/codebase/core/manager.py +20 -7
- shotgun/codebase/core/nl_query.py +1 -1
- shotgun/codebase/models.py +4 -4
- shotgun/exceptions.py +32 -0
- shotgun/llm_proxy/__init__.py +19 -0
- shotgun/llm_proxy/clients.py +44 -0
- shotgun/llm_proxy/constants.py +15 -0
- shotgun/logging_config.py +18 -27
- shotgun/main.py +91 -12
- shotgun/posthog_telemetry.py +81 -10
- shotgun/prompts/agents/export.j2 +18 -1
- shotgun/prompts/agents/partials/common_agent_system_prompt.j2 +5 -1
- shotgun/prompts/agents/partials/interactive_mode.j2 +24 -7
- shotgun/prompts/agents/plan.j2 +1 -1
- shotgun/prompts/agents/research.j2 +1 -1
- shotgun/prompts/agents/specify.j2 +270 -3
- shotgun/prompts/agents/state/system_state.j2 +4 -0
- shotgun/prompts/agents/tasks.j2 +1 -1
- shotgun/prompts/loader.py +2 -2
- shotgun/prompts/tools/web_search.j2 +14 -0
- shotgun/sentry_telemetry.py +27 -18
- shotgun/settings.py +238 -0
- shotgun/shotgun_web/__init__.py +19 -0
- shotgun/shotgun_web/client.py +138 -0
- shotgun/shotgun_web/constants.py +21 -0
- shotgun/shotgun_web/models.py +47 -0
- shotgun/telemetry.py +24 -36
- shotgun/tui/app.py +251 -23
- shotgun/tui/commands/__init__.py +1 -1
- shotgun/tui/components/context_indicator.py +179 -0
- shotgun/tui/components/mode_indicator.py +70 -0
- shotgun/tui/components/status_bar.py +48 -0
- shotgun/tui/containers.py +91 -0
- shotgun/tui/dependencies.py +39 -0
- shotgun/tui/protocols.py +45 -0
- shotgun/tui/screens/chat/__init__.py +5 -0
- shotgun/tui/screens/chat/chat.tcss +54 -0
- shotgun/tui/screens/chat/chat_screen.py +1234 -0
- shotgun/tui/screens/chat/codebase_index_prompt_screen.py +64 -0
- shotgun/tui/screens/chat/codebase_index_selection.py +12 -0
- shotgun/tui/screens/chat/help_text.py +40 -0
- shotgun/tui/screens/chat/prompt_history.py +48 -0
- shotgun/tui/screens/chat.tcss +11 -0
- shotgun/tui/screens/chat_screen/command_providers.py +226 -11
- shotgun/tui/screens/chat_screen/history/__init__.py +22 -0
- shotgun/tui/screens/chat_screen/history/agent_response.py +66 -0
- shotgun/tui/screens/chat_screen/history/chat_history.py +116 -0
- shotgun/tui/screens/chat_screen/history/formatters.py +115 -0
- shotgun/tui/screens/chat_screen/history/partial_response.py +43 -0
- shotgun/tui/screens/chat_screen/history/user_question.py +42 -0
- shotgun/tui/screens/confirmation_dialog.py +151 -0
- shotgun/tui/screens/feedback.py +193 -0
- shotgun/tui/screens/github_issue.py +102 -0
- shotgun/tui/screens/model_picker.py +352 -0
- shotgun/tui/screens/onboarding.py +431 -0
- shotgun/tui/screens/pipx_migration.py +153 -0
- shotgun/tui/screens/provider_config.py +156 -39
- shotgun/tui/screens/shotgun_auth.py +295 -0
- shotgun/tui/screens/welcome.py +198 -0
- shotgun/tui/services/__init__.py +5 -0
- shotgun/tui/services/conversation_service.py +184 -0
- shotgun/tui/state/__init__.py +7 -0
- shotgun/tui/state/processing_state.py +185 -0
- shotgun/tui/utils/mode_progress.py +14 -7
- shotgun/tui/widgets/__init__.py +5 -0
- shotgun/tui/widgets/widget_coordinator.py +262 -0
- shotgun/utils/datetime_utils.py +77 -0
- shotgun/utils/env_utils.py +13 -0
- shotgun/utils/file_system_utils.py +22 -2
- shotgun/utils/marketing.py +110 -0
- shotgun/utils/update_checker.py +69 -14
- shotgun_sh-0.2.11.dist-info/METADATA +130 -0
- shotgun_sh-0.2.11.dist-info/RECORD +194 -0
- {shotgun_sh-0.1.14.dist-info → shotgun_sh-0.2.11.dist-info}/entry_points.txt +1 -0
- {shotgun_sh-0.1.14.dist-info → shotgun_sh-0.2.11.dist-info}/licenses/LICENSE +1 -1
- shotgun/agents/history/token_counting.py +0 -429
- shotgun/agents/tools/user_interaction.py +0 -37
- shotgun/tui/screens/chat.py +0 -797
- shotgun/tui/screens/chat_screen/history.py +0 -350
- shotgun_sh-0.1.14.dist-info/METADATA +0 -466
- shotgun_sh-0.1.14.dist-info/RECORD +0 -133
- {shotgun_sh-0.1.14.dist-info → shotgun_sh-0.2.11.dist-info}/WHEEL +0 -0
|
@@ -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
|
|
|
@@ -18,15 +20,12 @@ from shotgun.logging_config import get_logger
|
|
|
18
20
|
logger = get_logger(__name__)
|
|
19
21
|
|
|
20
22
|
|
|
21
|
-
#
|
|
22
|
-
|
|
23
|
+
# Directories that should never be traversed during indexing
|
|
24
|
+
BASE_IGNORE_DIRECTORIES = {
|
|
23
25
|
".git",
|
|
24
26
|
"venv",
|
|
25
27
|
".venv",
|
|
26
28
|
"__pycache__",
|
|
27
|
-
"node_modules",
|
|
28
|
-
"build",
|
|
29
|
-
"dist",
|
|
30
29
|
".eggs",
|
|
31
30
|
".pytest_cache",
|
|
32
31
|
".mypy_cache",
|
|
@@ -36,6 +35,46 @@ IGNORE_PATTERNS = {
|
|
|
36
35
|
".vscode",
|
|
37
36
|
}
|
|
38
37
|
|
|
38
|
+
# Well-known build output directories to skip when determining source files
|
|
39
|
+
BUILD_ARTIFACT_DIRECTORIES = {
|
|
40
|
+
"node_modules",
|
|
41
|
+
".next",
|
|
42
|
+
".nuxt",
|
|
43
|
+
".vite",
|
|
44
|
+
".yarn",
|
|
45
|
+
".svelte-kit",
|
|
46
|
+
".output",
|
|
47
|
+
".turbo",
|
|
48
|
+
".parcel-cache",
|
|
49
|
+
".vercel",
|
|
50
|
+
".serverless",
|
|
51
|
+
"build",
|
|
52
|
+
"dist",
|
|
53
|
+
"out",
|
|
54
|
+
"tmp",
|
|
55
|
+
"coverage",
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
# Default ignore patterns combines base directories and build artifacts
|
|
59
|
+
IGNORE_PATTERNS = BASE_IGNORE_DIRECTORIES | BUILD_ARTIFACT_DIRECTORIES
|
|
60
|
+
|
|
61
|
+
# Directory prefixes that should always be ignored
|
|
62
|
+
IGNORED_DIRECTORY_PREFIXES = (".",)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def should_ignore_directory(name: str, ignore_patterns: set[str] | None = None) -> bool:
|
|
66
|
+
"""Return True if the directory name should be ignored."""
|
|
67
|
+
patterns = IGNORE_PATTERNS if ignore_patterns is None else ignore_patterns
|
|
68
|
+
if name in patterns:
|
|
69
|
+
return True
|
|
70
|
+
return name.startswith(IGNORED_DIRECTORY_PREFIXES)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def is_path_ignored(path: Path, ignore_patterns: set[str] | None = None) -> bool:
|
|
74
|
+
"""Return True if any part of the path should be ignored."""
|
|
75
|
+
patterns = IGNORE_PATTERNS if ignore_patterns is None else ignore_patterns
|
|
76
|
+
return any(should_ignore_directory(part, patterns) for part in path.parts)
|
|
77
|
+
|
|
39
78
|
|
|
40
79
|
class Ingestor:
|
|
41
80
|
"""Handles all communication and ingestion with the Kuzu database."""
|
|
@@ -582,7 +621,7 @@ class SimpleGraphBuilder:
|
|
|
582
621
|
# Don't let progress callback errors crash the build
|
|
583
622
|
logger.debug(f"Progress callback error: {e}")
|
|
584
623
|
|
|
585
|
-
def run(self) -> None:
|
|
624
|
+
async def run(self) -> None:
|
|
586
625
|
"""Run the three-pass graph building process."""
|
|
587
626
|
logger.info(f"Building graph for project: {self.project_name}")
|
|
588
627
|
|
|
@@ -592,7 +631,7 @@ class SimpleGraphBuilder:
|
|
|
592
631
|
|
|
593
632
|
# Pass 2: Definitions
|
|
594
633
|
logger.info("Pass 2: Processing files and extracting definitions...")
|
|
595
|
-
self._process_files()
|
|
634
|
+
await self._process_files()
|
|
596
635
|
|
|
597
636
|
# Pass 3: Relationships
|
|
598
637
|
logger.info("Pass 3: Processing relationships (calls, imports)...")
|
|
@@ -607,7 +646,9 @@ class SimpleGraphBuilder:
|
|
|
607
646
|
"""First pass: Walk directory to find packages and folders."""
|
|
608
647
|
dir_count = 0
|
|
609
648
|
for root_str, dirs, _ in os.walk(self.repo_path, topdown=True):
|
|
610
|
-
dirs[:] = [
|
|
649
|
+
dirs[:] = [
|
|
650
|
+
d for d in dirs if not should_ignore_directory(d, self.ignore_dirs)
|
|
651
|
+
]
|
|
611
652
|
root = Path(root_str)
|
|
612
653
|
relative_root = root.relative_to(self.repo_path)
|
|
613
654
|
|
|
@@ -732,7 +773,7 @@ class SimpleGraphBuilder:
|
|
|
732
773
|
phase_complete=True,
|
|
733
774
|
)
|
|
734
775
|
|
|
735
|
-
def _process_files(self) -> None:
|
|
776
|
+
async def _process_files(self) -> None:
|
|
736
777
|
"""Second pass: Process files and extract definitions."""
|
|
737
778
|
# First pass: Count total files
|
|
738
779
|
total_files = 0
|
|
@@ -740,7 +781,7 @@ class SimpleGraphBuilder:
|
|
|
740
781
|
root = Path(root_str)
|
|
741
782
|
|
|
742
783
|
# Skip ignored directories
|
|
743
|
-
if
|
|
784
|
+
if is_path_ignored(root, self.ignore_dirs):
|
|
744
785
|
continue
|
|
745
786
|
|
|
746
787
|
for filename in files:
|
|
@@ -757,7 +798,7 @@ class SimpleGraphBuilder:
|
|
|
757
798
|
root = Path(root_str)
|
|
758
799
|
|
|
759
800
|
# Skip ignored directories
|
|
760
|
-
if
|
|
801
|
+
if is_path_ignored(root, self.ignore_dirs):
|
|
761
802
|
continue
|
|
762
803
|
|
|
763
804
|
for filename in files:
|
|
@@ -768,7 +809,7 @@ class SimpleGraphBuilder:
|
|
|
768
809
|
lang_config = get_language_config(ext)
|
|
769
810
|
|
|
770
811
|
if lang_config and lang_config.name in self.parsers:
|
|
771
|
-
self._process_single_file(filepath, lang_config.name)
|
|
812
|
+
await self._process_single_file(filepath, lang_config.name)
|
|
772
813
|
file_count += 1
|
|
773
814
|
|
|
774
815
|
# Report progress after each file
|
|
@@ -793,7 +834,7 @@ class SimpleGraphBuilder:
|
|
|
793
834
|
phase_complete=True,
|
|
794
835
|
)
|
|
795
836
|
|
|
796
|
-
def _process_single_file(self, filepath: Path, language: str) -> None:
|
|
837
|
+
async def _process_single_file(self, filepath: Path, language: str) -> None:
|
|
797
838
|
"""Process a single file."""
|
|
798
839
|
relative_path = filepath.relative_to(self.repo_path)
|
|
799
840
|
relative_path_str = str(relative_path).replace(os.sep, "/")
|
|
@@ -834,8 +875,8 @@ class SimpleGraphBuilder:
|
|
|
834
875
|
|
|
835
876
|
# Parse file
|
|
836
877
|
try:
|
|
837
|
-
with open(filepath, "rb") as f:
|
|
838
|
-
content = f.read()
|
|
878
|
+
async with aiofiles.open(filepath, "rb") as f:
|
|
879
|
+
content = await f.read()
|
|
839
880
|
|
|
840
881
|
parser = self.parsers[language]
|
|
841
882
|
tree = parser.parse(content)
|
|
@@ -1597,7 +1638,7 @@ class CodebaseIngestor:
|
|
|
1597
1638
|
)
|
|
1598
1639
|
if self.project_name:
|
|
1599
1640
|
builder.project_name = self.project_name
|
|
1600
|
-
builder.run()
|
|
1641
|
+
asyncio.run(builder.run())
|
|
1601
1642
|
|
|
1602
1643
|
logger.info(f"Graph successfully created at: {self.db_path}")
|
|
1603
1644
|
|
shotgun/codebase/core/manager.py
CHANGED
|
@@ -51,9 +51,13 @@ class CodebaseFileHandler(FileSystemEventHandler):
|
|
|
51
51
|
self.pending_changes: list[FileChange] = []
|
|
52
52
|
self._lock = anyio.Lock()
|
|
53
53
|
# Import default ignore patterns from ingestor
|
|
54
|
-
from shotgun.codebase.core.ingestor import
|
|
54
|
+
from shotgun.codebase.core.ingestor import (
|
|
55
|
+
IGNORE_PATTERNS,
|
|
56
|
+
should_ignore_directory,
|
|
57
|
+
)
|
|
55
58
|
|
|
56
59
|
self.ignore_patterns = ignore_patterns or IGNORE_PATTERNS
|
|
60
|
+
self._should_ignore_directory = should_ignore_directory
|
|
57
61
|
|
|
58
62
|
def on_any_event(self, event: FileSystemEvent) -> None:
|
|
59
63
|
"""Handle any file system event."""
|
|
@@ -71,7 +75,7 @@ class CodebaseFileHandler(FileSystemEventHandler):
|
|
|
71
75
|
|
|
72
76
|
# Check if any parent directory should be ignored
|
|
73
77
|
for parent in path.parents:
|
|
74
|
-
if parent.name
|
|
78
|
+
if self._should_ignore_directory(parent.name, self.ignore_patterns):
|
|
75
79
|
logger.debug(
|
|
76
80
|
f"Ignoring file in ignored directory: {parent.name} - path: {src_path_str}"
|
|
77
81
|
)
|
|
@@ -106,7 +110,7 @@ class CodebaseFileHandler(FileSystemEventHandler):
|
|
|
106
110
|
)
|
|
107
111
|
dest_path = Path(dest_path_str)
|
|
108
112
|
for parent in dest_path.parents:
|
|
109
|
-
if parent.name
|
|
113
|
+
if self._should_ignore_directory(parent.name, self.ignore_patterns):
|
|
110
114
|
logger.debug(
|
|
111
115
|
f"Ignoring move to ignored directory: {parent.name} - dest_path: {dest_path_str}"
|
|
112
116
|
)
|
|
@@ -367,7 +371,16 @@ class CodebaseGraphManager:
|
|
|
367
371
|
)
|
|
368
372
|
import shutil
|
|
369
373
|
|
|
370
|
-
|
|
374
|
+
# Handle both files and directories (kuzu v0.11.2+ uses files)
|
|
375
|
+
if graph_path.is_file():
|
|
376
|
+
graph_path.unlink() # Delete file
|
|
377
|
+
# Also delete WAL file if it exists
|
|
378
|
+
wal_path = graph_path.with_suffix(graph_path.suffix + ".wal")
|
|
379
|
+
if wal_path.exists():
|
|
380
|
+
wal_path.unlink()
|
|
381
|
+
logger.debug(f"Deleted WAL file: {wal_path}")
|
|
382
|
+
else:
|
|
383
|
+
shutil.rmtree(graph_path) # Delete directory
|
|
371
384
|
|
|
372
385
|
# Import the builder from local core module
|
|
373
386
|
from shotgun.codebase.core import CodebaseIngestor
|
|
@@ -756,7 +769,7 @@ class CodebaseGraphManager:
|
|
|
756
769
|
|
|
757
770
|
lang_config = get_language_config(full_path.suffix)
|
|
758
771
|
if lang_config and lang_config.name in parsers:
|
|
759
|
-
builder._process_single_file(full_path, lang_config.name)
|
|
772
|
+
await builder._process_single_file(full_path, lang_config.name)
|
|
760
773
|
stats["nodes_modified"] += 1 # Approximate
|
|
761
774
|
|
|
762
775
|
# Process additions
|
|
@@ -771,7 +784,7 @@ class CodebaseGraphManager:
|
|
|
771
784
|
|
|
772
785
|
lang_config = get_language_config(full_path.suffix)
|
|
773
786
|
if lang_config and lang_config.name in parsers:
|
|
774
|
-
builder._process_single_file(full_path, lang_config.name)
|
|
787
|
+
await builder._process_single_file(full_path, lang_config.name)
|
|
775
788
|
stats["nodes_added"] += 1 # Approximate
|
|
776
789
|
|
|
777
790
|
# Flush all pending operations
|
|
@@ -1738,7 +1751,7 @@ class CodebaseGraphManager:
|
|
|
1738
1751
|
)
|
|
1739
1752
|
|
|
1740
1753
|
# Build the graph
|
|
1741
|
-
builder.run()
|
|
1754
|
+
asyncio.run(builder.run())
|
|
1742
1755
|
|
|
1743
1756
|
# Run build in thread pool
|
|
1744
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/codebase/models.py
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
"""Data models for codebase service."""
|
|
2
2
|
|
|
3
3
|
from collections.abc import Callable
|
|
4
|
-
from enum import
|
|
4
|
+
from enum import StrEnum
|
|
5
5
|
from typing import Any
|
|
6
6
|
|
|
7
7
|
from pydantic import BaseModel, Field
|
|
8
8
|
|
|
9
9
|
|
|
10
|
-
class GraphStatus(
|
|
10
|
+
class GraphStatus(StrEnum):
|
|
11
11
|
"""Status of a code knowledge graph."""
|
|
12
12
|
|
|
13
13
|
READY = "READY" # Graph is ready for queries
|
|
@@ -16,14 +16,14 @@ class GraphStatus(str, Enum):
|
|
|
16
16
|
ERROR = "ERROR" # Last operation failed
|
|
17
17
|
|
|
18
18
|
|
|
19
|
-
class QueryType(
|
|
19
|
+
class QueryType(StrEnum):
|
|
20
20
|
"""Type of query being executed."""
|
|
21
21
|
|
|
22
22
|
NATURAL_LANGUAGE = "natural_language"
|
|
23
23
|
CYPHER = "cypher"
|
|
24
24
|
|
|
25
25
|
|
|
26
|
-
class ProgressPhase(
|
|
26
|
+
class ProgressPhase(StrEnum):
|
|
27
27
|
"""Phase of codebase indexing progress."""
|
|
28
28
|
|
|
29
29
|
STRUCTURE = "structure" # Identifying packages and folders
|
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
|
+
)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""LiteLLM proxy client utilities and configuration."""
|
|
2
|
+
|
|
3
|
+
from .clients import (
|
|
4
|
+
create_anthropic_proxy_provider,
|
|
5
|
+
create_litellm_provider,
|
|
6
|
+
)
|
|
7
|
+
from .constants import (
|
|
8
|
+
LITELLM_PROXY_ANTHROPIC_BASE,
|
|
9
|
+
LITELLM_PROXY_BASE_URL,
|
|
10
|
+
LITELLM_PROXY_OPENAI_BASE,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"LITELLM_PROXY_BASE_URL",
|
|
15
|
+
"LITELLM_PROXY_ANTHROPIC_BASE",
|
|
16
|
+
"LITELLM_PROXY_OPENAI_BASE",
|
|
17
|
+
"create_litellm_provider",
|
|
18
|
+
"create_anthropic_proxy_provider",
|
|
19
|
+
]
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""Client creation utilities for LiteLLM proxy."""
|
|
2
|
+
|
|
3
|
+
from pydantic_ai.providers.anthropic import AnthropicProvider
|
|
4
|
+
from pydantic_ai.providers.litellm import LiteLLMProvider
|
|
5
|
+
|
|
6
|
+
from .constants import LITELLM_PROXY_ANTHROPIC_BASE, LITELLM_PROXY_BASE_URL
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def create_litellm_provider(api_key: str) -> LiteLLMProvider:
|
|
10
|
+
"""Create LiteLLM provider for Shotgun Account.
|
|
11
|
+
|
|
12
|
+
Args:
|
|
13
|
+
api_key: Shotgun API key
|
|
14
|
+
|
|
15
|
+
Returns:
|
|
16
|
+
Configured LiteLLM provider pointing to Shotgun's proxy
|
|
17
|
+
"""
|
|
18
|
+
return LiteLLMProvider(
|
|
19
|
+
api_base=LITELLM_PROXY_BASE_URL,
|
|
20
|
+
api_key=api_key,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def create_anthropic_proxy_provider(api_key: str) -> AnthropicProvider:
|
|
25
|
+
"""Create Anthropic provider configured for LiteLLM proxy.
|
|
26
|
+
|
|
27
|
+
This provider uses native Anthropic API format while routing through
|
|
28
|
+
the LiteLLM proxy. This preserves Anthropic-specific features like
|
|
29
|
+
tool_choice and web search.
|
|
30
|
+
|
|
31
|
+
The provider's .client attribute provides access to the async Anthropic
|
|
32
|
+
client (AsyncAnthropic), which should be used for all API operations
|
|
33
|
+
including token counting.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
api_key: Shotgun API key
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
AnthropicProvider configured to use LiteLLM proxy /anthropic endpoint
|
|
40
|
+
"""
|
|
41
|
+
return AnthropicProvider(
|
|
42
|
+
api_key=api_key,
|
|
43
|
+
base_url=LITELLM_PROXY_ANTHROPIC_BASE,
|
|
44
|
+
)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""LiteLLM proxy constants and configuration."""
|
|
2
|
+
|
|
3
|
+
# Import from centralized API endpoints module
|
|
4
|
+
from shotgun.api_endpoints import (
|
|
5
|
+
LITELLM_PROXY_ANTHROPIC_BASE,
|
|
6
|
+
LITELLM_PROXY_BASE_URL,
|
|
7
|
+
LITELLM_PROXY_OPENAI_BASE,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
# Re-export for backward compatibility
|
|
11
|
+
__all__ = [
|
|
12
|
+
"LITELLM_PROXY_BASE_URL",
|
|
13
|
+
"LITELLM_PROXY_ANTHROPIC_BASE",
|
|
14
|
+
"LITELLM_PROXY_OPENAI_BASE",
|
|
15
|
+
]
|
shotgun/logging_config.py
CHANGED
|
@@ -2,12 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
4
|
import logging.handlers
|
|
5
|
-
import os
|
|
6
5
|
import sys
|
|
6
|
+
from datetime import datetime, timezone
|
|
7
7
|
from pathlib import Path
|
|
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,21 +70,16 @@ 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:
|
|
76
77
|
return logger
|
|
77
78
|
|
|
78
|
-
# Get log level from
|
|
79
|
-
|
|
80
|
-
if env_level not in ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]:
|
|
81
|
-
env_level = "INFO"
|
|
79
|
+
# Get log level from settings (already validated and uppercased)
|
|
80
|
+
log_level = settings.logging.log_level
|
|
82
81
|
|
|
83
|
-
logger.setLevel(getattr(logging,
|
|
82
|
+
logger.setLevel(getattr(logging, log_level))
|
|
84
83
|
|
|
85
84
|
# Default format string
|
|
86
85
|
if format_string is None:
|
|
@@ -102,13 +101,13 @@ def setup_logger(
|
|
|
102
101
|
# Check if console logging is enabled (default: off)
|
|
103
102
|
# Force console logging OFF if Logfire is enabled in dev build
|
|
104
103
|
console_logging_enabled = (
|
|
105
|
-
|
|
104
|
+
settings.logging.logging_to_console and not is_logfire_dev_build
|
|
106
105
|
)
|
|
107
106
|
|
|
108
107
|
if console_logging_enabled:
|
|
109
108
|
# Create console handler
|
|
110
109
|
console_handler = logging.StreamHandler(sys.stdout)
|
|
111
|
-
console_handler.setLevel(getattr(logging,
|
|
110
|
+
console_handler.setLevel(getattr(logging, log_level))
|
|
112
111
|
|
|
113
112
|
# Use colored formatter for console
|
|
114
113
|
console_formatter = ColoredFormatter(format_string, datefmt="%H:%M:%S")
|
|
@@ -118,26 +117,21 @@ def setup_logger(
|
|
|
118
117
|
logger.addHandler(console_handler)
|
|
119
118
|
|
|
120
119
|
# Check if file logging is enabled (default: on)
|
|
121
|
-
file_logging_enabled =
|
|
120
|
+
file_logging_enabled = settings.logging.logging_to_file
|
|
122
121
|
|
|
123
122
|
if file_logging_enabled:
|
|
124
123
|
try:
|
|
125
|
-
# Create file handler with
|
|
124
|
+
# Create file handler with ISO8601 timestamp for each run
|
|
126
125
|
log_dir = get_log_directory()
|
|
127
|
-
log_file = log_dir / "shotgun.log"
|
|
126
|
+
log_file = log_dir / f"shotgun-{_RUN_TIMESTAMP}.log"
|
|
128
127
|
|
|
129
|
-
# Use
|
|
130
|
-
file_handler = logging.
|
|
128
|
+
# Use regular FileHandler - each run gets its own isolated log file
|
|
129
|
+
file_handler = logging.FileHandler(
|
|
131
130
|
filename=log_file,
|
|
132
|
-
when="midnight", # Rotate at midnight
|
|
133
|
-
interval=1, # Every 1 day
|
|
134
|
-
backupCount=7, # Keep 7 days of logs
|
|
135
131
|
encoding="utf-8",
|
|
136
132
|
)
|
|
137
133
|
|
|
138
|
-
|
|
139
|
-
# Note: We'll use TimedRotatingFileHandler which handles both time and size
|
|
140
|
-
file_handler.setLevel(getattr(logging, env_level))
|
|
134
|
+
file_handler.setLevel(getattr(logging, log_level))
|
|
141
135
|
|
|
142
136
|
# Use standard formatter for file (no colors)
|
|
143
137
|
file_formatter = logging.Formatter(
|
|
@@ -191,10 +185,7 @@ def get_logger(name: str) -> logging.Logger:
|
|
|
191
185
|
logger = logging.getLogger(name)
|
|
192
186
|
|
|
193
187
|
# Check if we have a file handler already
|
|
194
|
-
has_file_handler = any(
|
|
195
|
-
isinstance(h, logging.handlers.TimedRotatingFileHandler)
|
|
196
|
-
for h in logger.handlers
|
|
197
|
-
)
|
|
188
|
+
has_file_handler = any(isinstance(h, logging.FileHandler) for h in logger.handlers)
|
|
198
189
|
|
|
199
190
|
# If no file handler, set up the logger (will add file handler)
|
|
200
191
|
if not has_file_handler:
|
shotgun/main.py
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
"""Main CLI application for shotgun."""
|
|
2
2
|
|
|
3
|
+
# NOTE: These are before we import any Google library to stop the noisy gRPC logs.
|
|
4
|
+
import os # noqa: I001
|
|
5
|
+
|
|
6
|
+
os.environ["GRPC_VERBOSITY"] = "ERROR"
|
|
7
|
+
os.environ["GLOG_minloglevel"] = "2"
|
|
8
|
+
|
|
3
9
|
import logging
|
|
4
10
|
|
|
5
11
|
# CRITICAL: Add NullHandler to root logger before ANY other imports.
|
|
@@ -16,7 +22,20 @@ from dotenv import load_dotenv
|
|
|
16
22
|
|
|
17
23
|
from shotgun import __version__
|
|
18
24
|
from shotgun.agents.config import get_config_manager
|
|
19
|
-
from shotgun.cli import
|
|
25
|
+
from shotgun.cli import (
|
|
26
|
+
clear,
|
|
27
|
+
codebase,
|
|
28
|
+
compact,
|
|
29
|
+
config,
|
|
30
|
+
context,
|
|
31
|
+
export,
|
|
32
|
+
feedback,
|
|
33
|
+
plan,
|
|
34
|
+
research,
|
|
35
|
+
specify,
|
|
36
|
+
tasks,
|
|
37
|
+
update,
|
|
38
|
+
)
|
|
20
39
|
from shotgun.logging_config import configure_root_logger, get_logger
|
|
21
40
|
from shotgun.posthog_telemetry import setup_posthog_observability
|
|
22
41
|
from shotgun.sentry_telemetry import setup_sentry_observability
|
|
@@ -37,8 +56,10 @@ logger.debug("Logfire observability enabled: %s", _logfire_enabled)
|
|
|
37
56
|
|
|
38
57
|
# Initialize configuration
|
|
39
58
|
try:
|
|
59
|
+
import asyncio
|
|
60
|
+
|
|
40
61
|
config_manager = get_config_manager()
|
|
41
|
-
config_manager.load() # Ensure config is loaded at startup
|
|
62
|
+
asyncio.run(config_manager.load()) # Ensure config is loaded at startup
|
|
42
63
|
except Exception as e:
|
|
43
64
|
logger.debug("Configuration initialization warning: %s", e)
|
|
44
65
|
|
|
@@ -62,12 +83,16 @@ app.add_typer(config.app, name="config", help="Manage Shotgun configuration")
|
|
|
62
83
|
app.add_typer(
|
|
63
84
|
codebase.app, name="codebase", help="Manage and query code knowledge graphs"
|
|
64
85
|
)
|
|
86
|
+
app.add_typer(context.app, name="context", help="Analyze conversation context usage")
|
|
87
|
+
app.add_typer(compact.app, name="compact", help="Compact conversation history")
|
|
88
|
+
app.add_typer(clear.app, name="clear", help="Clear conversation history")
|
|
65
89
|
app.add_typer(research.app, name="research", help="Perform research with agentic loops")
|
|
66
90
|
app.add_typer(plan.app, name="plan", help="Generate structured plans")
|
|
67
91
|
app.add_typer(specify.app, name="specify", help="Generate comprehensive specifications")
|
|
68
92
|
app.add_typer(tasks.app, name="tasks", help="Generate task lists with agentic approach")
|
|
69
93
|
app.add_typer(export.app, name="export", help="Export artifacts to various formats")
|
|
70
94
|
app.add_typer(update.app, name="update", help="Check for and install updates")
|
|
95
|
+
app.add_typer(feedback.app, name="feedback", help="Send us feedback")
|
|
71
96
|
|
|
72
97
|
|
|
73
98
|
def version_callback(value: bool) -> None:
|
|
@@ -108,6 +133,41 @@ def main(
|
|
|
108
133
|
help="Continue previous TUI conversation",
|
|
109
134
|
),
|
|
110
135
|
] = False,
|
|
136
|
+
web: Annotated[
|
|
137
|
+
bool,
|
|
138
|
+
typer.Option(
|
|
139
|
+
"--web",
|
|
140
|
+
help="Serve TUI as web application",
|
|
141
|
+
),
|
|
142
|
+
] = False,
|
|
143
|
+
port: Annotated[
|
|
144
|
+
int,
|
|
145
|
+
typer.Option(
|
|
146
|
+
"--port",
|
|
147
|
+
help="Port for web server (only used with --web)",
|
|
148
|
+
),
|
|
149
|
+
] = 8000,
|
|
150
|
+
host: Annotated[
|
|
151
|
+
str,
|
|
152
|
+
typer.Option(
|
|
153
|
+
"--host",
|
|
154
|
+
help="Host address for web server (only used with --web)",
|
|
155
|
+
),
|
|
156
|
+
] = "localhost",
|
|
157
|
+
public_url: Annotated[
|
|
158
|
+
str | None,
|
|
159
|
+
typer.Option(
|
|
160
|
+
"--public-url",
|
|
161
|
+
help="Public URL if behind proxy (only used with --web)",
|
|
162
|
+
),
|
|
163
|
+
] = None,
|
|
164
|
+
force_reindex: Annotated[
|
|
165
|
+
bool,
|
|
166
|
+
typer.Option(
|
|
167
|
+
"--force-reindex",
|
|
168
|
+
help="Force re-indexing of codebase (ignores existing index)",
|
|
169
|
+
),
|
|
170
|
+
] = False,
|
|
111
171
|
) -> None:
|
|
112
172
|
"""Shotgun - AI-powered CLI tool."""
|
|
113
173
|
logger.debug("Starting shotgun CLI application")
|
|
@@ -117,16 +177,35 @@ def main(
|
|
|
117
177
|
perform_auto_update_async(no_update_check=no_update_check)
|
|
118
178
|
|
|
119
179
|
if ctx.invoked_subcommand is None and not ctx.resilient_parsing:
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
180
|
+
if web:
|
|
181
|
+
logger.debug("Launching shotgun TUI as web application")
|
|
182
|
+
try:
|
|
183
|
+
tui_app.serve(
|
|
184
|
+
host=host,
|
|
185
|
+
port=port,
|
|
186
|
+
public_url=public_url,
|
|
187
|
+
no_update_check=no_update_check,
|
|
188
|
+
continue_session=continue_session,
|
|
189
|
+
force_reindex=force_reindex,
|
|
190
|
+
)
|
|
191
|
+
finally:
|
|
192
|
+
# Ensure PostHog is shut down cleanly even if server exits unexpectedly
|
|
193
|
+
from shotgun.posthog_telemetry import shutdown
|
|
194
|
+
|
|
195
|
+
shutdown()
|
|
196
|
+
else:
|
|
197
|
+
logger.debug("Launching shotgun TUI application")
|
|
198
|
+
try:
|
|
199
|
+
tui_app.run(
|
|
200
|
+
no_update_check=no_update_check,
|
|
201
|
+
continue_session=continue_session,
|
|
202
|
+
force_reindex=force_reindex,
|
|
203
|
+
)
|
|
204
|
+
finally:
|
|
205
|
+
# Ensure PostHog is shut down cleanly even if TUI exits unexpectedly
|
|
206
|
+
from shotgun.posthog_telemetry import shutdown
|
|
207
|
+
|
|
208
|
+
shutdown()
|
|
130
209
|
raise typer.Exit()
|
|
131
210
|
|
|
132
211
|
# For CLI commands, register PostHog shutdown handler
|