shotgun-sh 0.1.9__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 +761 -52
- 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 +23 -3
- shotgun/agents/history/context_extraction.py +93 -6
- shotgun/agents/history/history_processors.py +179 -11
- 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/codebase/commands.py +71 -2
- 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 +18 -5
- shotgun/codebase/core/change_detector.py +5 -3
- shotgun/codebase/core/code_retrieval.py +4 -2
- shotgun/codebase/core/ingestor.py +169 -19
- shotgun/codebase/core/manager.py +177 -13
- shotgun/codebase/core/nl_query.py +1 -1
- shotgun/codebase/models.py +28 -3
- shotgun/codebase/service.py +14 -2
- 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 -4
- shotgun/posthog_telemetry.py +87 -40
- 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/codebase/partials/cypher_rules.j2 +13 -0
- shotgun/prompts/loader.py +2 -2
- shotgun/prompts/tools/web_search.j2 +14 -0
- shotgun/sdk/codebase.py +60 -2
- shotgun/sentry_telemetry.py +28 -21
- 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 +275 -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/components/vertical_tail.py +6 -0
- shotgun/tui/containers.py +91 -0
- shotgun/tui/dependencies.py +39 -0
- shotgun/tui/filtered_codebase_service.py +46 -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/source_detection.py +16 -0
- shotgun/utils/update_checker.py +73 -21
- shotgun_sh-0.2.11.dist-info/METADATA +130 -0
- shotgun_sh-0.2.11.dist-info/RECORD +194 -0
- {shotgun_sh-0.1.9.dist-info → shotgun_sh-0.2.11.dist-info}/entry_points.txt +1 -0
- {shotgun_sh-0.1.9.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 -818
- shotgun/tui/screens/chat_screen/history.py +0 -222
- shotgun_sh-0.1.9.dist-info/METADATA +0 -466
- shotgun_sh-0.1.9.dist-info/RECORD +0 -131
- {shotgun_sh-0.1.9.dist-info → shotgun_sh-0.2.11.dist-info}/WHEEL +0 -0
shotgun/codebase/models.py
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
"""Data models for codebase service."""
|
|
2
2
|
|
|
3
|
-
from
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
from enum import StrEnum
|
|
4
5
|
from typing import Any
|
|
5
6
|
|
|
6
7
|
from pydantic import BaseModel, Field
|
|
7
8
|
|
|
8
9
|
|
|
9
|
-
class GraphStatus(
|
|
10
|
+
class GraphStatus(StrEnum):
|
|
10
11
|
"""Status of a code knowledge graph."""
|
|
11
12
|
|
|
12
13
|
READY = "READY" # Graph is ready for queries
|
|
@@ -15,13 +16,37 @@ class GraphStatus(str, Enum):
|
|
|
15
16
|
ERROR = "ERROR" # Last operation failed
|
|
16
17
|
|
|
17
18
|
|
|
18
|
-
class QueryType(
|
|
19
|
+
class QueryType(StrEnum):
|
|
19
20
|
"""Type of query being executed."""
|
|
20
21
|
|
|
21
22
|
NATURAL_LANGUAGE = "natural_language"
|
|
22
23
|
CYPHER = "cypher"
|
|
23
24
|
|
|
24
25
|
|
|
26
|
+
class ProgressPhase(StrEnum):
|
|
27
|
+
"""Phase of codebase indexing progress."""
|
|
28
|
+
|
|
29
|
+
STRUCTURE = "structure" # Identifying packages and folders
|
|
30
|
+
DEFINITIONS = "definitions" # Processing files and extracting definitions
|
|
31
|
+
RELATIONSHIPS = "relationships" # Processing relationships (calls, imports)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class IndexProgress(BaseModel):
|
|
35
|
+
"""Progress information for codebase indexing."""
|
|
36
|
+
|
|
37
|
+
phase: ProgressPhase = Field(..., description="Current indexing phase")
|
|
38
|
+
phase_name: str = Field(..., description="Human-readable phase name")
|
|
39
|
+
current: int = Field(..., description="Current item count")
|
|
40
|
+
total: int | None = Field(None, description="Total items (None if unknown)")
|
|
41
|
+
phase_complete: bool = Field(
|
|
42
|
+
default=False, description="Whether this phase is complete"
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# Type alias for progress callback function
|
|
47
|
+
ProgressCallback = Callable[[IndexProgress], None]
|
|
48
|
+
|
|
49
|
+
|
|
25
50
|
class OperationStats(BaseModel):
|
|
26
51
|
"""Statistics for a graph operation (build/update)."""
|
|
27
52
|
|
shotgun/codebase/service.py
CHANGED
|
@@ -69,11 +69,19 @@ class CodebaseService:
|
|
|
69
69
|
# Otherwise, check if current directory is in the allowed list
|
|
70
70
|
elif target_path in graph.indexed_from_cwds:
|
|
71
71
|
filtered_graphs.append(graph)
|
|
72
|
+
# Also allow access if current directory IS the repository itself
|
|
73
|
+
# Use Path.resolve() for robust comparison (handles symlinks, etc.)
|
|
74
|
+
elif Path(target_path).resolve() == Path(graph.repo_path).resolve():
|
|
75
|
+
filtered_graphs.append(graph)
|
|
72
76
|
|
|
73
77
|
return filtered_graphs
|
|
74
78
|
|
|
75
79
|
async def create_graph(
|
|
76
|
-
self,
|
|
80
|
+
self,
|
|
81
|
+
repo_path: str | Path,
|
|
82
|
+
name: str,
|
|
83
|
+
indexed_from_cwd: str | None = None,
|
|
84
|
+
progress_callback: Any | None = None,
|
|
77
85
|
) -> CodebaseGraph:
|
|
78
86
|
"""Create and index a new graph from a repository.
|
|
79
87
|
|
|
@@ -81,12 +89,16 @@ class CodebaseService:
|
|
|
81
89
|
repo_path: Path to the repository to index
|
|
82
90
|
name: Human-readable name for the graph
|
|
83
91
|
indexed_from_cwd: Working directory from which indexing was initiated
|
|
92
|
+
progress_callback: Optional callback for progress reporting
|
|
84
93
|
|
|
85
94
|
Returns:
|
|
86
95
|
The created CodebaseGraph
|
|
87
96
|
"""
|
|
88
97
|
return await self.manager.build_graph(
|
|
89
|
-
str(repo_path),
|
|
98
|
+
str(repo_path),
|
|
99
|
+
name,
|
|
100
|
+
indexed_from_cwd=indexed_from_cwd,
|
|
101
|
+
progress_callback=progress_callback,
|
|
90
102
|
)
|
|
91
103
|
|
|
92
104
|
async def get_graph(self, graph_id: str) -> CodebaseGraph | None:
|
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,8 +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
|
-
|
|
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()
|
|
122
209
|
raise typer.Exit()
|
|
123
210
|
|
|
124
211
|
# For CLI commands, register PostHog shutdown handler
|
shotgun/posthog_telemetry.py
CHANGED
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
"""PostHog analytics setup for Shotgun."""
|
|
2
2
|
|
|
3
|
-
import
|
|
3
|
+
from enum import StrEnum
|
|
4
4
|
from typing import Any
|
|
5
5
|
|
|
6
|
+
import posthog
|
|
7
|
+
from pydantic import BaseModel
|
|
8
|
+
|
|
9
|
+
from shotgun import __version__
|
|
10
|
+
from shotgun.agents.config import get_config_manager
|
|
11
|
+
from shotgun.agents.conversation_manager import ConversationManager
|
|
6
12
|
from shotgun.logging_config import get_early_logger
|
|
13
|
+
from shotgun.settings import settings
|
|
7
14
|
|
|
8
15
|
# Use early logger to prevent automatic StreamHandler creation
|
|
9
16
|
logger = get_early_logger(__name__)
|
|
@@ -21,41 +28,20 @@ def setup_posthog_observability() -> bool:
|
|
|
21
28
|
global _posthog_client
|
|
22
29
|
|
|
23
30
|
try:
|
|
24
|
-
import posthog
|
|
25
|
-
|
|
26
31
|
# Check if PostHog is already initialized
|
|
27
32
|
if _posthog_client is not None:
|
|
28
33
|
logger.debug("PostHog is already initialized, skipping")
|
|
29
34
|
return True
|
|
30
35
|
|
|
31
|
-
#
|
|
32
|
-
api_key =
|
|
33
|
-
|
|
34
|
-
try:
|
|
35
|
-
from shotgun import build_constants
|
|
36
|
-
|
|
37
|
-
api_key = build_constants.POSTHOG_API_KEY
|
|
38
|
-
if api_key:
|
|
39
|
-
logger.debug("Using PostHog configuration from build constants")
|
|
40
|
-
except (ImportError, AttributeError):
|
|
41
|
-
pass
|
|
42
|
-
|
|
43
|
-
# Fallback to environment variables if build constants are empty or missing
|
|
44
|
-
if not api_key:
|
|
45
|
-
api_key = os.getenv("POSTHOG_API_KEY", "")
|
|
46
|
-
if api_key:
|
|
47
|
-
logger.debug("Using PostHog configuration from environment variables")
|
|
36
|
+
# Get API key from settings (handles build constants + env vars automatically)
|
|
37
|
+
api_key = settings.telemetry.posthog_api_key
|
|
48
38
|
|
|
39
|
+
# If no API key is available, skip PostHog initialization
|
|
49
40
|
if not api_key:
|
|
50
|
-
logger.debug(
|
|
51
|
-
"No PostHog API key configured, skipping PostHog initialization"
|
|
52
|
-
)
|
|
41
|
+
logger.debug("No PostHog API key available, skipping initialization")
|
|
53
42
|
return False
|
|
54
43
|
|
|
55
|
-
logger.debug("
|
|
56
|
-
|
|
57
|
-
# Get version for context
|
|
58
|
-
from shotgun import __version__
|
|
44
|
+
logger.debug("Using PostHog API key from settings")
|
|
59
45
|
|
|
60
46
|
# Determine environment based on version
|
|
61
47
|
# Dev versions contain "dev", "rc", "alpha", or "beta"
|
|
@@ -71,22 +57,31 @@ def setup_posthog_observability() -> bool:
|
|
|
71
57
|
# Store the client for later use
|
|
72
58
|
_posthog_client = posthog
|
|
73
59
|
|
|
74
|
-
# Set user context with anonymous
|
|
60
|
+
# Set user context with anonymous shotgun instance ID from config
|
|
75
61
|
try:
|
|
76
|
-
|
|
62
|
+
import asyncio
|
|
77
63
|
|
|
78
64
|
config_manager = get_config_manager()
|
|
79
|
-
|
|
65
|
+
shotgun_instance_id = asyncio.run(config_manager.get_shotgun_instance_id())
|
|
66
|
+
|
|
67
|
+
# Identify the user in PostHog
|
|
68
|
+
posthog.identify( # type: ignore[attr-defined]
|
|
69
|
+
distinct_id=shotgun_instance_id,
|
|
70
|
+
properties={
|
|
71
|
+
"version": __version__,
|
|
72
|
+
"environment": environment,
|
|
73
|
+
},
|
|
74
|
+
)
|
|
80
75
|
|
|
81
76
|
# Set default properties for all events
|
|
82
77
|
posthog.disabled = False
|
|
83
78
|
posthog.personal_api_key = None # Not needed for event tracking
|
|
84
79
|
|
|
85
80
|
logger.debug(
|
|
86
|
-
"PostHog user
|
|
81
|
+
"PostHog user identified with anonymous ID: %s", shotgun_instance_id
|
|
87
82
|
)
|
|
88
83
|
except Exception as e:
|
|
89
|
-
logger.warning("Failed to
|
|
84
|
+
logger.warning("Failed to set user context: %s", e)
|
|
90
85
|
|
|
91
86
|
logger.debug(
|
|
92
87
|
"PostHog analytics configured successfully (environment: %s, version: %s)",
|
|
@@ -95,9 +90,6 @@ def setup_posthog_observability() -> bool:
|
|
|
95
90
|
)
|
|
96
91
|
return True
|
|
97
92
|
|
|
98
|
-
except ImportError as e:
|
|
99
|
-
logger.error("PostHog SDK not available: %s", e)
|
|
100
|
-
return False
|
|
101
93
|
except Exception as e:
|
|
102
94
|
logger.warning("Failed to setup PostHog analytics: %s", e)
|
|
103
95
|
return False
|
|
@@ -117,12 +109,11 @@ def track_event(event_name: str, properties: dict[str, Any] | None = None) -> No
|
|
|
117
109
|
return
|
|
118
110
|
|
|
119
111
|
try:
|
|
120
|
-
|
|
121
|
-
from shotgun.agents.config import get_config_manager
|
|
112
|
+
import asyncio
|
|
122
113
|
|
|
123
|
-
# Get
|
|
114
|
+
# Get shotgun instance ID for tracking
|
|
124
115
|
config_manager = get_config_manager()
|
|
125
|
-
|
|
116
|
+
shotgun_instance_id = asyncio.run(config_manager.get_shotgun_instance_id())
|
|
126
117
|
|
|
127
118
|
# Add version and environment to properties
|
|
128
119
|
if properties is None:
|
|
@@ -137,7 +128,7 @@ def track_event(event_name: str, properties: dict[str, Any] | None = None) -> No
|
|
|
137
128
|
|
|
138
129
|
# Track the event using PostHog's capture method
|
|
139
130
|
_posthog_client.capture(
|
|
140
|
-
distinct_id=
|
|
131
|
+
distinct_id=shotgun_instance_id, event=event_name, properties=properties
|
|
141
132
|
)
|
|
142
133
|
logger.debug("Tracked PostHog event: %s", event_name)
|
|
143
134
|
except Exception as e:
|
|
@@ -156,3 +147,59 @@ def shutdown() -> None:
|
|
|
156
147
|
logger.warning("Error shutting down PostHog: %s", e)
|
|
157
148
|
finally:
|
|
158
149
|
_posthog_client = None
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
class FeedbackKind(StrEnum):
|
|
153
|
+
BUG = "bug"
|
|
154
|
+
FEATURE = "feature"
|
|
155
|
+
OTHER = "other"
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
class Feedback(BaseModel):
|
|
159
|
+
kind: FeedbackKind
|
|
160
|
+
description: str
|
|
161
|
+
shotgun_instance_id: str
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
SURVEY_ID = "01999f81-9486-0000-4fa6-9632959f92f3"
|
|
165
|
+
Q_KIND_ID = "aaa5fcc3-88ba-4c24-bcf5-1481fd5efc2b"
|
|
166
|
+
Q_DESCRIPTION_ID = "a0ed6283-5d4b-452c-9160-6768d879db8a"
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def submit_feedback_survey(feedback: Feedback) -> None:
|
|
170
|
+
global _posthog_client
|
|
171
|
+
if _posthog_client is None:
|
|
172
|
+
logger.debug("PostHog not initialized, skipping feedback survey")
|
|
173
|
+
return
|
|
174
|
+
|
|
175
|
+
import asyncio
|
|
176
|
+
|
|
177
|
+
config_manager = get_config_manager()
|
|
178
|
+
config = asyncio.run(config_manager.load())
|
|
179
|
+
conversation_manager = ConversationManager()
|
|
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}")
|
|
185
|
+
last_10_messages = []
|
|
186
|
+
if conversation is not None:
|
|
187
|
+
last_10_messages = conversation.get_agent_messages()[:10]
|
|
188
|
+
|
|
189
|
+
track_event(
|
|
190
|
+
"survey sent",
|
|
191
|
+
properties={
|
|
192
|
+
"$survey_id": SURVEY_ID,
|
|
193
|
+
"$survey_questions": [
|
|
194
|
+
{"id": Q_KIND_ID, "question": "Feedback type"},
|
|
195
|
+
{"id": Q_DESCRIPTION_ID, "question": "Feedback description"},
|
|
196
|
+
],
|
|
197
|
+
f"$survey_response_{Q_KIND_ID}": feedback.kind,
|
|
198
|
+
f"$survey_response_{Q_DESCRIPTION_ID}": feedback.description,
|
|
199
|
+
"selected_model": config.selected_model.value
|
|
200
|
+
if config.selected_model
|
|
201
|
+
else None,
|
|
202
|
+
"config_version": config.config_version,
|
|
203
|
+
"last_10_messages": last_10_messages, # last 10 messages
|
|
204
|
+
},
|
|
205
|
+
)
|