shotgun-sh 0.1.1.dev1__tar.gz → 0.1.2__tar.gz
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_sh-0.1.1.dev1 → shotgun_sh-0.1.2}/PKG-INFO +1 -1
- {shotgun_sh-0.1.1.dev1 → shotgun_sh-0.1.2}/pyproject.toml +1 -1
- {shotgun_sh-0.1.1.dev1 → shotgun_sh-0.1.2}/src/shotgun/agents/agent_manager.py +11 -0
- {shotgun_sh-0.1.1.dev1 → shotgun_sh-0.1.2}/src/shotgun/build_constants.py +2 -2
- {shotgun_sh-0.1.1.dev1 → shotgun_sh-0.1.2}/src/shotgun/codebase/core/nl_query.py +39 -180
- {shotgun_sh-0.1.1.dev1 → shotgun_sh-0.1.2}/src/shotgun/codebase/service.py +0 -17
- {shotgun_sh-0.1.1.dev1 → shotgun_sh-0.1.2}/src/shotgun/main.py +14 -4
- {shotgun_sh-0.1.1.dev1 → shotgun_sh-0.1.2}/src/shotgun/prompts/codebase/cypher_system.j2 +1 -15
- {shotgun_sh-0.1.1.dev1 → shotgun_sh-0.1.2}/src/shotgun/tui/app.py +13 -3
- {shotgun_sh-0.1.1.dev1 → shotgun_sh-0.1.2}/src/shotgun/tui/screens/chat.py +11 -5
- {shotgun_sh-0.1.1.dev1 → shotgun_sh-0.1.2}/src/shotgun/tui/screens/chat_screen/history.py +4 -5
- {shotgun_sh-0.1.1.dev1 → shotgun_sh-0.1.2}/src/shotgun/utils/update_checker.py +202 -0
- shotgun_sh-0.1.1.dev1/src/shotgun/codebase/core/cypher_models.py +0 -46
- {shotgun_sh-0.1.1.dev1 → shotgun_sh-0.1.2}/.gitignore +0 -0
- {shotgun_sh-0.1.1.dev1 → shotgun_sh-0.1.2}/LICENSE +0 -0
- {shotgun_sh-0.1.1.dev1 → shotgun_sh-0.1.2}/README.md +0 -0
- {shotgun_sh-0.1.1.dev1 → shotgun_sh-0.1.2}/hatch_build.py +0 -0
- {shotgun_sh-0.1.1.dev1 → shotgun_sh-0.1.2}/src/shotgun/__init__.py +0 -0
- {shotgun_sh-0.1.1.dev1 → shotgun_sh-0.1.2}/src/shotgun/agents/__init__.py +0 -0
- {shotgun_sh-0.1.1.dev1 → shotgun_sh-0.1.2}/src/shotgun/agents/common.py +0 -0
- {shotgun_sh-0.1.1.dev1 → shotgun_sh-0.1.2}/src/shotgun/agents/config/__init__.py +0 -0
- {shotgun_sh-0.1.1.dev1 → shotgun_sh-0.1.2}/src/shotgun/agents/config/constants.py +0 -0
- {shotgun_sh-0.1.1.dev1 → shotgun_sh-0.1.2}/src/shotgun/agents/config/manager.py +0 -0
- {shotgun_sh-0.1.1.dev1 → shotgun_sh-0.1.2}/src/shotgun/agents/config/models.py +0 -0
- {shotgun_sh-0.1.1.dev1 → shotgun_sh-0.1.2}/src/shotgun/agents/config/provider.py +0 -0
- {shotgun_sh-0.1.1.dev1 → shotgun_sh-0.1.2}/src/shotgun/agents/conversation_history.py +0 -0
- {shotgun_sh-0.1.1.dev1 → shotgun_sh-0.1.2}/src/shotgun/agents/conversation_manager.py +0 -0
- {shotgun_sh-0.1.1.dev1 → shotgun_sh-0.1.2}/src/shotgun/agents/export.py +0 -0
- {shotgun_sh-0.1.1.dev1 → shotgun_sh-0.1.2}/src/shotgun/agents/history/__init__.py +0 -0
- {shotgun_sh-0.1.1.dev1 → shotgun_sh-0.1.2}/src/shotgun/agents/history/compaction.py +0 -0
- {shotgun_sh-0.1.1.dev1 → shotgun_sh-0.1.2}/src/shotgun/agents/history/constants.py +0 -0
- {shotgun_sh-0.1.1.dev1 → shotgun_sh-0.1.2}/src/shotgun/agents/history/context_extraction.py +0 -0
- {shotgun_sh-0.1.1.dev1 → shotgun_sh-0.1.2}/src/shotgun/agents/history/history_building.py +0 -0
- {shotgun_sh-0.1.1.dev1 → shotgun_sh-0.1.2}/src/shotgun/agents/history/history_processors.py +0 -0
- {shotgun_sh-0.1.1.dev1 → shotgun_sh-0.1.2}/src/shotgun/agents/history/message_utils.py +0 -0
- {shotgun_sh-0.1.1.dev1 → shotgun_sh-0.1.2}/src/shotgun/agents/history/token_counting.py +0 -0
- {shotgun_sh-0.1.1.dev1 → shotgun_sh-0.1.2}/src/shotgun/agents/history/token_estimation.py +0 -0
- {shotgun_sh-0.1.1.dev1 → shotgun_sh-0.1.2}/src/shotgun/agents/messages.py +0 -0
- {shotgun_sh-0.1.1.dev1 → shotgun_sh-0.1.2}/src/shotgun/agents/models.py +0 -0
- {shotgun_sh-0.1.1.dev1 → shotgun_sh-0.1.2}/src/shotgun/agents/plan.py +0 -0
- {shotgun_sh-0.1.1.dev1 → shotgun_sh-0.1.2}/src/shotgun/agents/research.py +0 -0
- {shotgun_sh-0.1.1.dev1 → shotgun_sh-0.1.2}/src/shotgun/agents/specify.py +0 -0
- {shotgun_sh-0.1.1.dev1 → shotgun_sh-0.1.2}/src/shotgun/agents/tasks.py +0 -0
- {shotgun_sh-0.1.1.dev1 → shotgun_sh-0.1.2}/src/shotgun/agents/tools/__init__.py +0 -0
- {shotgun_sh-0.1.1.dev1 → shotgun_sh-0.1.2}/src/shotgun/agents/tools/codebase/__init__.py +0 -0
- {shotgun_sh-0.1.1.dev1 → shotgun_sh-0.1.2}/src/shotgun/agents/tools/codebase/codebase_shell.py +0 -0
- {shotgun_sh-0.1.1.dev1 → shotgun_sh-0.1.2}/src/shotgun/agents/tools/codebase/directory_lister.py +0 -0
- {shotgun_sh-0.1.1.dev1 → shotgun_sh-0.1.2}/src/shotgun/agents/tools/codebase/file_read.py +0 -0
- {shotgun_sh-0.1.1.dev1 → shotgun_sh-0.1.2}/src/shotgun/agents/tools/codebase/models.py +0 -0
- {shotgun_sh-0.1.1.dev1 → shotgun_sh-0.1.2}/src/shotgun/agents/tools/codebase/query_graph.py +0 -0
- {shotgun_sh-0.1.1.dev1 → shotgun_sh-0.1.2}/src/shotgun/agents/tools/codebase/retrieve_code.py +0 -0
- {shotgun_sh-0.1.1.dev1 → shotgun_sh-0.1.2}/src/shotgun/agents/tools/file_management.py +0 -0
- {shotgun_sh-0.1.1.dev1 → shotgun_sh-0.1.2}/src/shotgun/agents/tools/user_interaction.py +0 -0
- {shotgun_sh-0.1.1.dev1 → shotgun_sh-0.1.2}/src/shotgun/agents/tools/web_search/__init__.py +0 -0
- {shotgun_sh-0.1.1.dev1 → shotgun_sh-0.1.2}/src/shotgun/agents/tools/web_search/anthropic.py +0 -0
- {shotgun_sh-0.1.1.dev1 → shotgun_sh-0.1.2}/src/shotgun/agents/tools/web_search/gemini.py +0 -0
- {shotgun_sh-0.1.1.dev1 → shotgun_sh-0.1.2}/src/shotgun/agents/tools/web_search/openai.py +0 -0
- {shotgun_sh-0.1.1.dev1 → shotgun_sh-0.1.2}/src/shotgun/agents/tools/web_search/utils.py +0 -0
- {shotgun_sh-0.1.1.dev1 → shotgun_sh-0.1.2}/src/shotgun/cli/__init__.py +0 -0
- {shotgun_sh-0.1.1.dev1 → shotgun_sh-0.1.2}/src/shotgun/cli/codebase/__init__.py +0 -0
- {shotgun_sh-0.1.1.dev1 → shotgun_sh-0.1.2}/src/shotgun/cli/codebase/commands.py +0 -0
- {shotgun_sh-0.1.1.dev1 → shotgun_sh-0.1.2}/src/shotgun/cli/codebase/models.py +0 -0
- {shotgun_sh-0.1.1.dev1 → shotgun_sh-0.1.2}/src/shotgun/cli/config.py +0 -0
- {shotgun_sh-0.1.1.dev1 → shotgun_sh-0.1.2}/src/shotgun/cli/export.py +0 -0
- {shotgun_sh-0.1.1.dev1 → shotgun_sh-0.1.2}/src/shotgun/cli/models.py +0 -0
- {shotgun_sh-0.1.1.dev1 → shotgun_sh-0.1.2}/src/shotgun/cli/plan.py +0 -0
- {shotgun_sh-0.1.1.dev1 → shotgun_sh-0.1.2}/src/shotgun/cli/research.py +0 -0
- {shotgun_sh-0.1.1.dev1 → shotgun_sh-0.1.2}/src/shotgun/cli/specify.py +0 -0
- {shotgun_sh-0.1.1.dev1 → shotgun_sh-0.1.2}/src/shotgun/cli/tasks.py +0 -0
- {shotgun_sh-0.1.1.dev1 → shotgun_sh-0.1.2}/src/shotgun/cli/update.py +0 -0
- {shotgun_sh-0.1.1.dev1 → shotgun_sh-0.1.2}/src/shotgun/cli/utils.py +0 -0
- {shotgun_sh-0.1.1.dev1 → shotgun_sh-0.1.2}/src/shotgun/codebase/__init__.py +0 -0
- {shotgun_sh-0.1.1.dev1 → shotgun_sh-0.1.2}/src/shotgun/codebase/core/__init__.py +0 -0
- {shotgun_sh-0.1.1.dev1 → shotgun_sh-0.1.2}/src/shotgun/codebase/core/change_detector.py +0 -0
- {shotgun_sh-0.1.1.dev1 → shotgun_sh-0.1.2}/src/shotgun/codebase/core/code_retrieval.py +0 -0
- {shotgun_sh-0.1.1.dev1 → shotgun_sh-0.1.2}/src/shotgun/codebase/core/ingestor.py +0 -0
- {shotgun_sh-0.1.1.dev1 → shotgun_sh-0.1.2}/src/shotgun/codebase/core/language_config.py +0 -0
- {shotgun_sh-0.1.1.dev1 → shotgun_sh-0.1.2}/src/shotgun/codebase/core/manager.py +0 -0
- {shotgun_sh-0.1.1.dev1 → shotgun_sh-0.1.2}/src/shotgun/codebase/core/parser_loader.py +0 -0
- {shotgun_sh-0.1.1.dev1 → shotgun_sh-0.1.2}/src/shotgun/codebase/models.py +0 -0
- {shotgun_sh-0.1.1.dev1 → shotgun_sh-0.1.2}/src/shotgun/logging_config.py +0 -0
- {shotgun_sh-0.1.1.dev1 → shotgun_sh-0.1.2}/src/shotgun/posthog_telemetry.py +0 -0
- {shotgun_sh-0.1.1.dev1 → shotgun_sh-0.1.2}/src/shotgun/prompts/__init__.py +0 -0
- {shotgun_sh-0.1.1.dev1 → shotgun_sh-0.1.2}/src/shotgun/prompts/agents/__init__.py +0 -0
- {shotgun_sh-0.1.1.dev1 → shotgun_sh-0.1.2}/src/shotgun/prompts/agents/export.j2 +0 -0
- {shotgun_sh-0.1.1.dev1 → shotgun_sh-0.1.2}/src/shotgun/prompts/agents/partials/codebase_understanding.j2 +0 -0
- {shotgun_sh-0.1.1.dev1 → shotgun_sh-0.1.2}/src/shotgun/prompts/agents/partials/common_agent_system_prompt.j2 +0 -0
- {shotgun_sh-0.1.1.dev1 → shotgun_sh-0.1.2}/src/shotgun/prompts/agents/partials/content_formatting.j2 +0 -0
- {shotgun_sh-0.1.1.dev1 → shotgun_sh-0.1.2}/src/shotgun/prompts/agents/partials/interactive_mode.j2 +0 -0
- {shotgun_sh-0.1.1.dev1 → shotgun_sh-0.1.2}/src/shotgun/prompts/agents/plan.j2 +0 -0
- {shotgun_sh-0.1.1.dev1 → shotgun_sh-0.1.2}/src/shotgun/prompts/agents/research.j2 +0 -0
- {shotgun_sh-0.1.1.dev1 → shotgun_sh-0.1.2}/src/shotgun/prompts/agents/specify.j2 +0 -0
- {shotgun_sh-0.1.1.dev1 → shotgun_sh-0.1.2}/src/shotgun/prompts/agents/state/codebase/codebase_graphs_available.j2 +0 -0
- {shotgun_sh-0.1.1.dev1 → shotgun_sh-0.1.2}/src/shotgun/prompts/agents/state/system_state.j2 +0 -0
- {shotgun_sh-0.1.1.dev1 → shotgun_sh-0.1.2}/src/shotgun/prompts/agents/tasks.j2 +0 -0
- {shotgun_sh-0.1.1.dev1 → shotgun_sh-0.1.2}/src/shotgun/prompts/codebase/__init__.py +0 -0
- {shotgun_sh-0.1.1.dev1 → shotgun_sh-0.1.2}/src/shotgun/prompts/codebase/cypher_query_patterns.j2 +0 -0
- {shotgun_sh-0.1.1.dev1 → shotgun_sh-0.1.2}/src/shotgun/prompts/codebase/enhanced_query_context.j2 +0 -0
- {shotgun_sh-0.1.1.dev1 → shotgun_sh-0.1.2}/src/shotgun/prompts/codebase/partials/cypher_rules.j2 +0 -0
- {shotgun_sh-0.1.1.dev1 → shotgun_sh-0.1.2}/src/shotgun/prompts/codebase/partials/graph_schema.j2 +0 -0
- {shotgun_sh-0.1.1.dev1 → shotgun_sh-0.1.2}/src/shotgun/prompts/codebase/partials/temporal_context.j2 +0 -0
- {shotgun_sh-0.1.1.dev1 → shotgun_sh-0.1.2}/src/shotgun/prompts/history/__init__.py +0 -0
- {shotgun_sh-0.1.1.dev1 → shotgun_sh-0.1.2}/src/shotgun/prompts/history/incremental_summarization.j2 +0 -0
- {shotgun_sh-0.1.1.dev1 → shotgun_sh-0.1.2}/src/shotgun/prompts/history/summarization.j2 +0 -0
- {shotgun_sh-0.1.1.dev1 → shotgun_sh-0.1.2}/src/shotgun/prompts/loader.py +0 -0
- {shotgun_sh-0.1.1.dev1 → shotgun_sh-0.1.2}/src/shotgun/py.typed +0 -0
- {shotgun_sh-0.1.1.dev1 → shotgun_sh-0.1.2}/src/shotgun/sdk/__init__.py +0 -0
- {shotgun_sh-0.1.1.dev1 → shotgun_sh-0.1.2}/src/shotgun/sdk/codebase.py +0 -0
- {shotgun_sh-0.1.1.dev1 → shotgun_sh-0.1.2}/src/shotgun/sdk/exceptions.py +0 -0
- {shotgun_sh-0.1.1.dev1 → shotgun_sh-0.1.2}/src/shotgun/sdk/models.py +0 -0
- {shotgun_sh-0.1.1.dev1 → shotgun_sh-0.1.2}/src/shotgun/sdk/services.py +0 -0
- {shotgun_sh-0.1.1.dev1 → shotgun_sh-0.1.2}/src/shotgun/sentry_telemetry.py +0 -0
- {shotgun_sh-0.1.1.dev1 → shotgun_sh-0.1.2}/src/shotgun/telemetry.py +0 -0
- {shotgun_sh-0.1.1.dev1 → shotgun_sh-0.1.2}/src/shotgun/tui/__init__.py +0 -0
- {shotgun_sh-0.1.1.dev1 → shotgun_sh-0.1.2}/src/shotgun/tui/commands/__init__.py +0 -0
- {shotgun_sh-0.1.1.dev1 → shotgun_sh-0.1.2}/src/shotgun/tui/components/prompt_input.py +0 -0
- {shotgun_sh-0.1.1.dev1 → shotgun_sh-0.1.2}/src/shotgun/tui/components/spinner.py +0 -0
- {shotgun_sh-0.1.1.dev1 → shotgun_sh-0.1.2}/src/shotgun/tui/components/splash.py +0 -0
- {shotgun_sh-0.1.1.dev1 → shotgun_sh-0.1.2}/src/shotgun/tui/components/vertical_tail.py +0 -0
- {shotgun_sh-0.1.1.dev1 → shotgun_sh-0.1.2}/src/shotgun/tui/screens/chat.tcss +0 -0
- {shotgun_sh-0.1.1.dev1 → shotgun_sh-0.1.2}/src/shotgun/tui/screens/chat_screen/__init__.py +0 -0
- {shotgun_sh-0.1.1.dev1 → shotgun_sh-0.1.2}/src/shotgun/tui/screens/chat_screen/command_providers.py +0 -0
- {shotgun_sh-0.1.1.dev1 → shotgun_sh-0.1.2}/src/shotgun/tui/screens/chat_screen/hint_message.py +0 -0
- {shotgun_sh-0.1.1.dev1 → shotgun_sh-0.1.2}/src/shotgun/tui/screens/directory_setup.py +0 -0
- {shotgun_sh-0.1.1.dev1 → shotgun_sh-0.1.2}/src/shotgun/tui/screens/provider_config.py +0 -0
- {shotgun_sh-0.1.1.dev1 → shotgun_sh-0.1.2}/src/shotgun/tui/screens/splash.py +0 -0
- {shotgun_sh-0.1.1.dev1 → shotgun_sh-0.1.2}/src/shotgun/tui/styles.tcss +0 -0
- {shotgun_sh-0.1.1.dev1 → shotgun_sh-0.1.2}/src/shotgun/tui/utils/__init__.py +0 -0
- {shotgun_sh-0.1.1.dev1 → shotgun_sh-0.1.2}/src/shotgun/tui/utils/mode_progress.py +0 -0
- {shotgun_sh-0.1.1.dev1 → shotgun_sh-0.1.2}/src/shotgun/utils/__init__.py +0 -0
- {shotgun_sh-0.1.1.dev1 → shotgun_sh-0.1.2}/src/shotgun/utils/env_utils.py +0 -0
- {shotgun_sh-0.1.1.dev1 → shotgun_sh-0.1.2}/src/shotgun/utils/file_system_utils.py +0 -0
|
@@ -362,6 +362,17 @@ class AgentManager(Widget):
|
|
|
362
362
|
**kwargs,
|
|
363
363
|
)
|
|
364
364
|
finally:
|
|
365
|
+
# If the stream ended unexpectedly without a final result, clear accumulated state.
|
|
366
|
+
# state = self._stream_state
|
|
367
|
+
# if state is not None:
|
|
368
|
+
# pending_response = state.current_response
|
|
369
|
+
# if pending_response is not None:
|
|
370
|
+
# already_recorded = (
|
|
371
|
+
# bool(state.messages) and state.messages[-1] is pending_response
|
|
372
|
+
# )
|
|
373
|
+
# if not already_recorded:
|
|
374
|
+
# self._post_partial_message(pending_response, True)
|
|
375
|
+
# state.messages.append(pending_response)
|
|
365
376
|
self._stream_state = None
|
|
366
377
|
|
|
367
378
|
self.ui_message_history = original_messages + cast(
|
|
@@ -12,8 +12,8 @@ POSTHOG_API_KEY = ''
|
|
|
12
12
|
POSTHOG_PROJECT_ID = '191396'
|
|
13
13
|
|
|
14
14
|
# Logfire configuration embedded at build time (only for dev builds)
|
|
15
|
-
LOGFIRE_ENABLED = '
|
|
16
|
-
LOGFIRE_TOKEN = '
|
|
15
|
+
LOGFIRE_ENABLED = ''
|
|
16
|
+
LOGFIRE_TOKEN = ''
|
|
17
17
|
|
|
18
18
|
# Build metadata
|
|
19
19
|
BUILD_TIME_ENV = "production" if SENTRY_DSN else "development"
|
|
@@ -4,13 +4,15 @@ import time
|
|
|
4
4
|
from datetime import datetime
|
|
5
5
|
from typing import TYPE_CHECKING
|
|
6
6
|
|
|
7
|
-
from pydantic_ai import
|
|
7
|
+
from pydantic_ai.messages import (
|
|
8
|
+
ModelRequest,
|
|
9
|
+
SystemPromptPart,
|
|
10
|
+
TextPart,
|
|
11
|
+
UserPromptPart,
|
|
12
|
+
)
|
|
8
13
|
|
|
9
14
|
from shotgun.agents.config import get_provider_model
|
|
10
|
-
from shotgun.
|
|
11
|
-
CypherGenerationNotPossibleError,
|
|
12
|
-
CypherGenerationResponse,
|
|
13
|
-
)
|
|
15
|
+
from shotgun.agents.config.models import shotgun_model_request
|
|
14
16
|
from shotgun.logging_config import get_logger
|
|
15
17
|
from shotgun.prompts import PromptLoader
|
|
16
18
|
|
|
@@ -23,52 +25,42 @@ logger = get_logger(__name__)
|
|
|
23
25
|
prompt_loader = PromptLoader()
|
|
24
26
|
|
|
25
27
|
|
|
26
|
-
async def llm_cypher_prompt(
|
|
27
|
-
|
|
28
|
-
) -> CypherGenerationResponse:
|
|
29
|
-
"""Generate a Cypher query from a natural language prompt using structured output.
|
|
28
|
+
async def llm_cypher_prompt(system_prompt: str, user_prompt: str) -> str:
|
|
29
|
+
"""Generate a Cypher query from a natural language prompt using the configured LLM provider.
|
|
30
30
|
|
|
31
31
|
Args:
|
|
32
32
|
system_prompt: The system prompt defining the behavior and context for the LLM
|
|
33
33
|
user_prompt: The user's natural language query
|
|
34
34
|
Returns:
|
|
35
|
-
|
|
35
|
+
The generated Cypher query as a string
|
|
36
36
|
"""
|
|
37
37
|
model_config = get_provider_model()
|
|
38
|
-
|
|
39
|
-
#
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
38
|
+
# Use shotgun wrapper to maximize response quality for codebase queries
|
|
39
|
+
# Limit max_tokens to 2000 for Cypher queries (they're typically 50-200 tokens)
|
|
40
|
+
# This prevents Anthropic SDK from requiring streaming for longer token limits
|
|
41
|
+
query_cypher_response = await shotgun_model_request(
|
|
42
|
+
model_config=model_config,
|
|
43
|
+
messages=[
|
|
44
|
+
ModelRequest(
|
|
45
|
+
parts=[
|
|
46
|
+
SystemPromptPart(content=system_prompt),
|
|
47
|
+
UserPromptPart(content=user_prompt),
|
|
48
|
+
]
|
|
49
|
+
),
|
|
50
|
+
],
|
|
51
|
+
max_tokens=2000, # Cypher queries are short, 2000 tokens is plenty
|
|
44
52
|
)
|
|
45
53
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
try:
|
|
50
|
-
# Run the agent to get structured response
|
|
51
|
-
result = await cypher_agent.run(combined_prompt)
|
|
52
|
-
response = result.output
|
|
53
|
-
|
|
54
|
-
# Log the structured response for debugging
|
|
55
|
-
logger.debug(
|
|
56
|
-
"Cypher generation response - can_generate: %s, query: %s, reason: %s",
|
|
57
|
-
response.can_generate_valid_cypher,
|
|
58
|
-
response.cypher_query[:50] if response.cypher_query else None,
|
|
59
|
-
response.reason_cannot_generate,
|
|
60
|
-
)
|
|
61
|
-
|
|
62
|
-
return response
|
|
54
|
+
if not query_cypher_response.parts or not query_cypher_response.parts[0]:
|
|
55
|
+
raise ValueError("Empty response from LLM")
|
|
63
56
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
)
|
|
57
|
+
message_part = query_cypher_response.parts[0]
|
|
58
|
+
if not isinstance(message_part, TextPart):
|
|
59
|
+
raise ValueError("Unexpected response part type from LLM")
|
|
60
|
+
cypher_query = str(message_part.content)
|
|
61
|
+
if not cypher_query:
|
|
62
|
+
raise ValueError("Empty content in LLM response")
|
|
63
|
+
return cypher_query
|
|
72
64
|
|
|
73
65
|
|
|
74
66
|
async def generate_cypher(natural_language_query: str) -> str:
|
|
@@ -79,10 +71,6 @@ async def generate_cypher(natural_language_query: str) -> str:
|
|
|
79
71
|
|
|
80
72
|
Returns:
|
|
81
73
|
Generated Cypher query
|
|
82
|
-
|
|
83
|
-
Raises:
|
|
84
|
-
CypherGenerationNotPossibleError: If the query cannot be converted to Cypher
|
|
85
|
-
RuntimeError: If there's an error during generation
|
|
86
74
|
"""
|
|
87
75
|
# Get current time for context
|
|
88
76
|
current_timestamp = int(time.time())
|
|
@@ -100,30 +88,8 @@ async def generate_cypher(natural_language_query: str) -> str:
|
|
|
100
88
|
)
|
|
101
89
|
|
|
102
90
|
try:
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
# Check if the LLM could generate a valid Cypher query
|
|
106
|
-
if not response.can_generate_valid_cypher:
|
|
107
|
-
logger.info(
|
|
108
|
-
"Cannot generate Cypher for query '%s': %s",
|
|
109
|
-
natural_language_query,
|
|
110
|
-
response.reason_cannot_generate,
|
|
111
|
-
)
|
|
112
|
-
raise CypherGenerationNotPossibleError(
|
|
113
|
-
response.reason_cannot_generate or "Query cannot be converted to Cypher"
|
|
114
|
-
)
|
|
115
|
-
|
|
116
|
-
if not response.cypher_query:
|
|
117
|
-
raise ValueError("LLM indicated success but provided no query")
|
|
118
|
-
|
|
119
|
-
cleaned_query = clean_cypher_response(response.cypher_query)
|
|
120
|
-
|
|
121
|
-
# Validate Cypher keywords
|
|
122
|
-
is_valid, validation_error = validate_cypher_keywords(cleaned_query)
|
|
123
|
-
if not is_valid:
|
|
124
|
-
logger.warning(f"Generated query has invalid syntax: {validation_error}")
|
|
125
|
-
logger.warning(f"Problematic query: {cleaned_query}")
|
|
126
|
-
raise ValueError(f"Generated query validation failed: {validation_error}")
|
|
91
|
+
cypher_query = await llm_cypher_prompt(system_prompt, enhanced_query)
|
|
92
|
+
cleaned_query = clean_cypher_response(cypher_query)
|
|
127
93
|
|
|
128
94
|
# Validate UNION ALL queries
|
|
129
95
|
is_valid, validation_error = validate_union_query(cleaned_query)
|
|
@@ -134,8 +100,6 @@ async def generate_cypher(natural_language_query: str) -> str:
|
|
|
134
100
|
|
|
135
101
|
return cleaned_query
|
|
136
102
|
|
|
137
|
-
except CypherGenerationNotPossibleError:
|
|
138
|
-
raise # Re-raise as-is
|
|
139
103
|
except Exception as e:
|
|
140
104
|
raise RuntimeError(f"Failed to generate Cypher query: {e}") from e
|
|
141
105
|
|
|
@@ -206,31 +170,8 @@ MATCH (f:Function) RETURN f.name, f.qualified_name // WRONG: missing third colu
|
|
|
206
170
|
base_system_prompt=prompt_loader.render("codebase/cypher_system.j2"),
|
|
207
171
|
)
|
|
208
172
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
# Check if the LLM could generate a valid Cypher query
|
|
212
|
-
if not response.can_generate_valid_cypher:
|
|
213
|
-
logger.info(
|
|
214
|
-
"Cannot generate Cypher for retry query '%s': %s",
|
|
215
|
-
natural_language_query,
|
|
216
|
-
response.reason_cannot_generate,
|
|
217
|
-
)
|
|
218
|
-
raise CypherGenerationNotPossibleError(
|
|
219
|
-
response.reason_cannot_generate
|
|
220
|
-
or "Query cannot be converted to Cypher even with error context"
|
|
221
|
-
)
|
|
222
|
-
|
|
223
|
-
if not response.cypher_query:
|
|
224
|
-
raise ValueError("LLM indicated success but provided no query on retry")
|
|
225
|
-
|
|
226
|
-
cleaned_query = clean_cypher_response(response.cypher_query)
|
|
227
|
-
|
|
228
|
-
# Validate Cypher keywords
|
|
229
|
-
is_valid, validation_error = validate_cypher_keywords(cleaned_query)
|
|
230
|
-
if not is_valid:
|
|
231
|
-
logger.warning(f"Generated query has invalid syntax: {validation_error}")
|
|
232
|
-
logger.warning(f"Problematic query: {cleaned_query}")
|
|
233
|
-
raise ValueError(f"Generated query validation failed: {validation_error}")
|
|
173
|
+
cypher_query = await llm_cypher_prompt(enhanced_system_prompt, enhanced_query)
|
|
174
|
+
cleaned_query = clean_cypher_response(cypher_query)
|
|
234
175
|
|
|
235
176
|
# Validate UNION ALL queries
|
|
236
177
|
is_valid, validation_error = validate_union_query(cleaned_query)
|
|
@@ -241,8 +182,6 @@ MATCH (f:Function) RETURN f.name, f.qualified_name // WRONG: missing third colu
|
|
|
241
182
|
|
|
242
183
|
return cleaned_query
|
|
243
184
|
|
|
244
|
-
except CypherGenerationNotPossibleError:
|
|
245
|
-
raise # Re-raise as-is
|
|
246
185
|
except Exception as e:
|
|
247
186
|
raise RuntimeError(
|
|
248
187
|
f"Failed to generate Cypher query with error context: {e}"
|
|
@@ -263,10 +202,6 @@ async def generate_cypher_openai_async(
|
|
|
263
202
|
|
|
264
203
|
Returns:
|
|
265
204
|
Generated Cypher query
|
|
266
|
-
|
|
267
|
-
Raises:
|
|
268
|
-
CypherGenerationNotPossibleError: If the query cannot be converted to Cypher
|
|
269
|
-
RuntimeError: If there's an error during generation
|
|
270
205
|
"""
|
|
271
206
|
# Get current time for context
|
|
272
207
|
current_timestamp = int(time.time())
|
|
@@ -284,26 +219,9 @@ async def generate_cypher_openai_async(
|
|
|
284
219
|
)
|
|
285
220
|
|
|
286
221
|
try:
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
# Check if the LLM could generate a valid Cypher query
|
|
290
|
-
if not response.can_generate_valid_cypher:
|
|
291
|
-
logger.info(
|
|
292
|
-
"Cannot generate Cypher for query '%s': %s",
|
|
293
|
-
natural_language_query,
|
|
294
|
-
response.reason_cannot_generate,
|
|
295
|
-
)
|
|
296
|
-
raise CypherGenerationNotPossibleError(
|
|
297
|
-
response.reason_cannot_generate or "Query cannot be converted to Cypher"
|
|
298
|
-
)
|
|
222
|
+
cypher_query = await llm_cypher_prompt(system_prompt, enhanced_query)
|
|
223
|
+
return clean_cypher_response(cypher_query)
|
|
299
224
|
|
|
300
|
-
if not response.cypher_query:
|
|
301
|
-
raise ValueError("LLM indicated success but provided no query")
|
|
302
|
-
|
|
303
|
-
return clean_cypher_response(response.cypher_query)
|
|
304
|
-
|
|
305
|
-
except CypherGenerationNotPossibleError:
|
|
306
|
-
raise # Re-raise as-is
|
|
307
225
|
except Exception as e:
|
|
308
226
|
logger.error(f"OpenAI API error: {e}")
|
|
309
227
|
raise RuntimeError(f"Failed to generate Cypher query: {e}") from e
|
|
@@ -370,65 +288,6 @@ def validate_union_query(cypher_query: str) -> tuple[bool, str]:
|
|
|
370
288
|
return True, ""
|
|
371
289
|
|
|
372
290
|
|
|
373
|
-
def validate_cypher_keywords(query: str) -> tuple[bool, str]:
|
|
374
|
-
"""Validate that a query starts with valid Kuzu Cypher keywords.
|
|
375
|
-
|
|
376
|
-
Args:
|
|
377
|
-
query: The Cypher query to validate
|
|
378
|
-
|
|
379
|
-
Returns:
|
|
380
|
-
Tuple of (is_valid, error_message)
|
|
381
|
-
"""
|
|
382
|
-
# Valid Kuzu Cypher starting keywords based on parser expectations
|
|
383
|
-
valid_cypher_keywords = {
|
|
384
|
-
"ALTER",
|
|
385
|
-
"ATTACH",
|
|
386
|
-
"BEGIN",
|
|
387
|
-
"CALL",
|
|
388
|
-
"CHECKPOINT",
|
|
389
|
-
"COMMENT",
|
|
390
|
-
"COMMIT",
|
|
391
|
-
"COPY",
|
|
392
|
-
"CREATE",
|
|
393
|
-
"DELETE",
|
|
394
|
-
"DETACH",
|
|
395
|
-
"DROP",
|
|
396
|
-
"EXPLAIN",
|
|
397
|
-
"EXPORT",
|
|
398
|
-
"FORCE",
|
|
399
|
-
"IMPORT",
|
|
400
|
-
"INSTALL",
|
|
401
|
-
"LOAD",
|
|
402
|
-
"MATCH",
|
|
403
|
-
"MERGE",
|
|
404
|
-
"OPTIONAL",
|
|
405
|
-
"PROFILE",
|
|
406
|
-
"RETURN",
|
|
407
|
-
"ROLLBACK",
|
|
408
|
-
"SET",
|
|
409
|
-
"UNWIND",
|
|
410
|
-
"UNINSTALL",
|
|
411
|
-
"UPDATE",
|
|
412
|
-
"USE",
|
|
413
|
-
"WITH",
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
query = query.strip()
|
|
417
|
-
if not query:
|
|
418
|
-
return False, "Empty query"
|
|
419
|
-
|
|
420
|
-
# Get the first word
|
|
421
|
-
first_word = query.upper().split()[0] if query else ""
|
|
422
|
-
|
|
423
|
-
if first_word not in valid_cypher_keywords:
|
|
424
|
-
return (
|
|
425
|
-
False,
|
|
426
|
-
f"Query doesn't start with valid Cypher keyword. Found: '{first_word}'",
|
|
427
|
-
)
|
|
428
|
-
|
|
429
|
-
return True, ""
|
|
430
|
-
|
|
431
|
-
|
|
432
291
|
def clean_cypher_response(response_text: str) -> str:
|
|
433
292
|
"""Clean up common LLM formatting artifacts from a Cypher query.
|
|
434
293
|
|
|
@@ -4,7 +4,6 @@ import time
|
|
|
4
4
|
from pathlib import Path
|
|
5
5
|
from typing import Any
|
|
6
6
|
|
|
7
|
-
from shotgun.codebase.core.cypher_models import CypherGenerationNotPossibleError
|
|
8
7
|
from shotgun.codebase.core.manager import CodebaseGraphManager
|
|
9
8
|
from shotgun.codebase.core.nl_query import generate_cypher
|
|
10
9
|
from shotgun.codebase.models import CodebaseGraph, QueryResult, QueryType
|
|
@@ -191,22 +190,6 @@ class CodebaseService:
|
|
|
191
190
|
error=None,
|
|
192
191
|
)
|
|
193
192
|
|
|
194
|
-
except CypherGenerationNotPossibleError as e:
|
|
195
|
-
# Handle queries that cannot be converted to Cypher
|
|
196
|
-
execution_time = (time.time() - start_time) * 1000
|
|
197
|
-
logger.info(f"Query cannot be converted to Cypher: {e.reason}")
|
|
198
|
-
|
|
199
|
-
return QueryResult(
|
|
200
|
-
query=query,
|
|
201
|
-
cypher_query=None,
|
|
202
|
-
results=[],
|
|
203
|
-
column_names=[],
|
|
204
|
-
row_count=0,
|
|
205
|
-
execution_time_ms=execution_time,
|
|
206
|
-
success=False,
|
|
207
|
-
error=f"This query cannot be converted to Cypher: {e.reason}",
|
|
208
|
-
)
|
|
209
|
-
|
|
210
193
|
except Exception as e:
|
|
211
194
|
execution_time = (time.time() - start_time) * 1000
|
|
212
195
|
logger.error(f"Query execution failed: {e}")
|
|
@@ -22,7 +22,7 @@ from shotgun.posthog_telemetry import setup_posthog_observability
|
|
|
22
22
|
from shotgun.sentry_telemetry import setup_sentry_observability
|
|
23
23
|
from shotgun.telemetry import setup_logfire_observability
|
|
24
24
|
from shotgun.tui import app as tui_app
|
|
25
|
-
from shotgun.utils.update_checker import
|
|
25
|
+
from shotgun.utils.update_checker import check_and_install_updates_async
|
|
26
26
|
|
|
27
27
|
# Load environment variables from .env file
|
|
28
28
|
load_dotenv()
|
|
@@ -52,6 +52,7 @@ logger.debug("PostHog analytics enabled: %s", _posthog_enabled)
|
|
|
52
52
|
|
|
53
53
|
# Global variable to store update notification
|
|
54
54
|
_update_notification: str | None = None
|
|
55
|
+
_update_progress: str | None = None
|
|
55
56
|
|
|
56
57
|
|
|
57
58
|
def _update_callback(notification: str) -> None:
|
|
@@ -60,6 +61,13 @@ def _update_callback(notification: str) -> None:
|
|
|
60
61
|
_update_notification = notification
|
|
61
62
|
|
|
62
63
|
|
|
64
|
+
def _update_progress_callback(progress: str) -> None:
|
|
65
|
+
"""Callback to store update progress."""
|
|
66
|
+
global _update_progress
|
|
67
|
+
_update_progress = progress
|
|
68
|
+
logger.debug(f"Update progress: {progress}")
|
|
69
|
+
|
|
70
|
+
|
|
63
71
|
app = typer.Typer(
|
|
64
72
|
name="shotgun",
|
|
65
73
|
help="Shotgun - AI-powered CLI tool for research, planning, and task management",
|
|
@@ -121,10 +129,12 @@ def main(
|
|
|
121
129
|
"""Shotgun - AI-powered CLI tool."""
|
|
122
130
|
logger.debug("Starting shotgun CLI application")
|
|
123
131
|
|
|
124
|
-
# Start async update check (non-blocking)
|
|
132
|
+
# Start async update check and install (non-blocking)
|
|
125
133
|
if not ctx.resilient_parsing:
|
|
126
|
-
|
|
127
|
-
callback=_update_callback,
|
|
134
|
+
check_and_install_updates_async(
|
|
135
|
+
callback=_update_callback,
|
|
136
|
+
no_update_check=no_update_check,
|
|
137
|
+
progress_callback=_update_progress_callback,
|
|
128
138
|
)
|
|
129
139
|
|
|
130
140
|
if ctx.invoked_subcommand is None and not ctx.resilient_parsing:
|
|
@@ -25,18 +25,4 @@ Your goal is to return appropriate properties for each node type. Common propert
|
|
|
25
25
|
{% include 'codebase/partials/temporal_context.j2' %}
|
|
26
26
|
|
|
27
27
|
**6. Output Format**
|
|
28
|
-
|
|
29
|
-
- `cypher_query`: The generated Cypher query string (or null if not possible)
|
|
30
|
-
- `can_generate_valid_cypher`: Boolean indicating if a valid Cypher query can be generated
|
|
31
|
-
- `reason_cannot_generate`: String explaining why generation isn't possible (or null if successful)
|
|
32
|
-
|
|
33
|
-
**IMPORTANT:** Some queries cannot be expressed in Cypher:
|
|
34
|
-
- Conceptual questions requiring interpretation (e.g., "What is the main purpose of this codebase?")
|
|
35
|
-
- Questions about code quality or best practices
|
|
36
|
-
- Questions requiring semantic understanding beyond structure
|
|
37
|
-
|
|
38
|
-
For these, set `can_generate_valid_cypher` to false and provide a clear explanation in `reason_cannot_generate`.
|
|
39
|
-
|
|
40
|
-
Examples:
|
|
41
|
-
- Query: "Show all classes" → can_generate_valid_cypher: true, cypher_query: "MATCH (c:Class) RETURN c.name, c.qualified_name;"
|
|
42
|
-
- Query: "What is the main purpose of this codebase?" → can_generate_valid_cypher: false, reason_cannot_generate: "This is a conceptual question requiring interpretation and analysis of the code's overall design and intent, rather than a structural query about specific code elements."
|
|
28
|
+
Provide only the Cypher query.
|
|
@@ -9,7 +9,7 @@ from shotgun.agents.config import ConfigManager, get_config_manager
|
|
|
9
9
|
from shotgun.logging_config import get_logger
|
|
10
10
|
from shotgun.tui.screens.splash import SplashScreen
|
|
11
11
|
from shotgun.utils.file_system_utils import get_shotgun_base_path
|
|
12
|
-
from shotgun.utils.update_checker import
|
|
12
|
+
from shotgun.utils.update_checker import check_and_install_updates_async
|
|
13
13
|
|
|
14
14
|
from .screens.chat import ChatScreen
|
|
15
15
|
from .screens.directory_setup import DirectorySetupScreen
|
|
@@ -37,16 +37,26 @@ class ShotgunApp(App[None]):
|
|
|
37
37
|
self.no_update_check = no_update_check
|
|
38
38
|
self.continue_session = continue_session
|
|
39
39
|
self.update_notification: str | None = None
|
|
40
|
+
self.update_progress: str | None = None
|
|
40
41
|
|
|
41
|
-
# Start async update check
|
|
42
|
+
# Start async update check and install
|
|
42
43
|
if not no_update_check:
|
|
43
|
-
|
|
44
|
+
check_and_install_updates_async(
|
|
45
|
+
callback=self._update_callback,
|
|
46
|
+
no_update_check=no_update_check,
|
|
47
|
+
progress_callback=self._update_progress_callback,
|
|
48
|
+
)
|
|
44
49
|
|
|
45
50
|
def _update_callback(self, notification: str) -> None:
|
|
46
51
|
"""Store update notification to show later."""
|
|
47
52
|
self.update_notification = notification
|
|
48
53
|
logger.debug(f"Update notification received: {notification}")
|
|
49
54
|
|
|
55
|
+
def _update_progress_callback(self, progress: str) -> None:
|
|
56
|
+
"""Store update progress."""
|
|
57
|
+
self.update_progress = progress
|
|
58
|
+
logger.debug(f"Update progress: {progress}")
|
|
59
|
+
|
|
50
60
|
def on_mount(self) -> None:
|
|
51
61
|
self.theme = "gruvbox"
|
|
52
62
|
# Track TUI startup
|
|
@@ -411,7 +411,7 @@ class ChatScreen(Screen[None]):
|
|
|
411
411
|
await self.codebase_sdk.list_codebases_for_directory()
|
|
412
412
|
).graphs
|
|
413
413
|
if accessible_graphs:
|
|
414
|
-
self.mount_hint(help_text_with_codebase(
|
|
414
|
+
self.mount_hint(help_text_with_codebase())
|
|
415
415
|
return
|
|
416
416
|
|
|
417
417
|
should_index = await self.app.push_screen_wait(CodebaseIndexPromptScreen())
|
|
@@ -419,8 +419,6 @@ class ChatScreen(Screen[None]):
|
|
|
419
419
|
self.mount_hint(help_text_empty_dir())
|
|
420
420
|
return
|
|
421
421
|
|
|
422
|
-
self.mount_hint(help_text_with_codebase(already_indexed=False))
|
|
423
|
-
|
|
424
422
|
self.index_codebase_command()
|
|
425
423
|
|
|
426
424
|
def watch_mode(self, new_mode: AgentType) -> None:
|
|
@@ -694,6 +692,7 @@ class ChatScreen(Screen[None]):
|
|
|
694
692
|
timeout=8,
|
|
695
693
|
)
|
|
696
694
|
|
|
695
|
+
self.mount_hint(codebase_indexed_hint(selection.name))
|
|
697
696
|
except CodebaseAlreadyIndexedError as exc:
|
|
698
697
|
logger.warning(f"Codebase already indexed: {exc}")
|
|
699
698
|
self.notify(str(exc), severity="warning")
|
|
@@ -797,10 +796,17 @@ class ChatScreen(Screen[None]):
|
|
|
797
796
|
self.mode = AgentType(conversation.last_agent_model)
|
|
798
797
|
|
|
799
798
|
|
|
800
|
-
def
|
|
799
|
+
def codebase_indexed_hint(codebase_name: str) -> str:
|
|
800
|
+
return (
|
|
801
|
+
f"Codebase **{codebase_name}** indexed successfully. You can now use it in your chat.\n\n"
|
|
802
|
+
+ help_text_with_codebase()
|
|
803
|
+
)
|
|
804
|
+
|
|
805
|
+
|
|
806
|
+
def help_text_with_codebase() -> str:
|
|
801
807
|
return (
|
|
802
808
|
"Howdy! Welcome to Shotgun - the context tool for software engineering. \n\nYou can research, build specs, plan, create tasks, and export context to your favorite code-gen agents.\n\n"
|
|
803
|
-
|
|
809
|
+
"I can help with:\n\n"
|
|
804
810
|
"- Speccing out a new feature\n"
|
|
805
811
|
"- Onboarding you onto this project\n"
|
|
806
812
|
"- Helping with a refactor spec\n"
|
|
@@ -136,8 +136,7 @@ class UserQuestionWidget(Widget):
|
|
|
136
136
|
if part.tool_name == "ask_user" and isinstance(part.content, dict):
|
|
137
137
|
acc += f"**>** {part.content['answer']}\n\n"
|
|
138
138
|
else:
|
|
139
|
-
|
|
140
|
-
pass
|
|
139
|
+
acc += " ∟ finished\n\n" # let's not show anything yet
|
|
141
140
|
elif isinstance(part, UserPromptPart):
|
|
142
141
|
acc += f"**>** {part.content}\n\n"
|
|
143
142
|
return acc
|
|
@@ -153,7 +152,7 @@ class AgentResponseWidget(Widget):
|
|
|
153
152
|
if self.item is None:
|
|
154
153
|
yield Markdown(markdown="")
|
|
155
154
|
else:
|
|
156
|
-
yield Markdown(markdown=self.compute_output())
|
|
155
|
+
yield Markdown(markdown=f"**⏺** {self.compute_output()}")
|
|
157
156
|
|
|
158
157
|
def compute_output(self) -> str:
|
|
159
158
|
acc = ""
|
|
@@ -161,10 +160,10 @@ class AgentResponseWidget(Widget):
|
|
|
161
160
|
return ""
|
|
162
161
|
for idx, part in enumerate(self.item.parts):
|
|
163
162
|
if isinstance(part, TextPart):
|
|
164
|
-
acc += f"
|
|
163
|
+
acc += f"{part.content}\n\n"
|
|
165
164
|
elif isinstance(part, ToolCallPart):
|
|
166
165
|
parts_str = self._format_tool_call_part(part)
|
|
167
|
-
acc += parts_str + "\n\n"
|
|
166
|
+
acc += f"{part.tool_name}: " + parts_str + "\n\n"
|
|
168
167
|
elif isinstance(part, BuiltinToolCallPart):
|
|
169
168
|
acc += f"{part.tool_name}({part.args})\n\n"
|
|
170
169
|
elif isinstance(part, BuiltinToolReturnPart):
|