shotgun-sh 0.1.4__tar.gz → 0.1.6__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.

Files changed (132) hide show
  1. {shotgun_sh-0.1.4 → shotgun_sh-0.1.6}/PKG-INFO +1 -1
  2. {shotgun_sh-0.1.4 → shotgun_sh-0.1.6}/pyproject.toml +1 -1
  3. {shotgun_sh-0.1.4 → shotgun_sh-0.1.6}/src/shotgun/agents/agent_manager.py +0 -11
  4. shotgun_sh-0.1.6/src/shotgun/codebase/core/cypher_models.py +46 -0
  5. {shotgun_sh-0.1.4 → shotgun_sh-0.1.6}/src/shotgun/codebase/core/nl_query.py +180 -39
  6. {shotgun_sh-0.1.4 → shotgun_sh-0.1.6}/src/shotgun/codebase/service.py +17 -0
  7. {shotgun_sh-0.1.4 → shotgun_sh-0.1.6}/src/shotgun/prompts/codebase/cypher_system.j2 +15 -1
  8. {shotgun_sh-0.1.4 → shotgun_sh-0.1.6}/src/shotgun/tui/screens/chat.py +5 -11
  9. {shotgun_sh-0.1.4 → shotgun_sh-0.1.6}/src/shotgun/tui/screens/chat_screen/history.py +5 -4
  10. {shotgun_sh-0.1.4 → shotgun_sh-0.1.6}/src/shotgun/utils/update_checker.py +16 -4
  11. {shotgun_sh-0.1.4 → shotgun_sh-0.1.6}/.gitignore +0 -0
  12. {shotgun_sh-0.1.4 → shotgun_sh-0.1.6}/LICENSE +0 -0
  13. {shotgun_sh-0.1.4 → shotgun_sh-0.1.6}/README.md +0 -0
  14. {shotgun_sh-0.1.4 → shotgun_sh-0.1.6}/hatch_build.py +0 -0
  15. {shotgun_sh-0.1.4 → shotgun_sh-0.1.6}/src/shotgun/__init__.py +0 -0
  16. {shotgun_sh-0.1.4 → shotgun_sh-0.1.6}/src/shotgun/agents/__init__.py +0 -0
  17. {shotgun_sh-0.1.4 → shotgun_sh-0.1.6}/src/shotgun/agents/common.py +0 -0
  18. {shotgun_sh-0.1.4 → shotgun_sh-0.1.6}/src/shotgun/agents/config/__init__.py +0 -0
  19. {shotgun_sh-0.1.4 → shotgun_sh-0.1.6}/src/shotgun/agents/config/constants.py +0 -0
  20. {shotgun_sh-0.1.4 → shotgun_sh-0.1.6}/src/shotgun/agents/config/manager.py +0 -0
  21. {shotgun_sh-0.1.4 → shotgun_sh-0.1.6}/src/shotgun/agents/config/models.py +0 -0
  22. {shotgun_sh-0.1.4 → shotgun_sh-0.1.6}/src/shotgun/agents/config/provider.py +0 -0
  23. {shotgun_sh-0.1.4 → shotgun_sh-0.1.6}/src/shotgun/agents/conversation_history.py +0 -0
  24. {shotgun_sh-0.1.4 → shotgun_sh-0.1.6}/src/shotgun/agents/conversation_manager.py +0 -0
  25. {shotgun_sh-0.1.4 → shotgun_sh-0.1.6}/src/shotgun/agents/export.py +0 -0
  26. {shotgun_sh-0.1.4 → shotgun_sh-0.1.6}/src/shotgun/agents/history/__init__.py +0 -0
  27. {shotgun_sh-0.1.4 → shotgun_sh-0.1.6}/src/shotgun/agents/history/compaction.py +0 -0
  28. {shotgun_sh-0.1.4 → shotgun_sh-0.1.6}/src/shotgun/agents/history/constants.py +0 -0
  29. {shotgun_sh-0.1.4 → shotgun_sh-0.1.6}/src/shotgun/agents/history/context_extraction.py +0 -0
  30. {shotgun_sh-0.1.4 → shotgun_sh-0.1.6}/src/shotgun/agents/history/history_building.py +0 -0
  31. {shotgun_sh-0.1.4 → shotgun_sh-0.1.6}/src/shotgun/agents/history/history_processors.py +0 -0
  32. {shotgun_sh-0.1.4 → shotgun_sh-0.1.6}/src/shotgun/agents/history/message_utils.py +0 -0
  33. {shotgun_sh-0.1.4 → shotgun_sh-0.1.6}/src/shotgun/agents/history/token_counting.py +0 -0
  34. {shotgun_sh-0.1.4 → shotgun_sh-0.1.6}/src/shotgun/agents/history/token_estimation.py +0 -0
  35. {shotgun_sh-0.1.4 → shotgun_sh-0.1.6}/src/shotgun/agents/messages.py +0 -0
  36. {shotgun_sh-0.1.4 → shotgun_sh-0.1.6}/src/shotgun/agents/models.py +0 -0
  37. {shotgun_sh-0.1.4 → shotgun_sh-0.1.6}/src/shotgun/agents/plan.py +0 -0
  38. {shotgun_sh-0.1.4 → shotgun_sh-0.1.6}/src/shotgun/agents/research.py +0 -0
  39. {shotgun_sh-0.1.4 → shotgun_sh-0.1.6}/src/shotgun/agents/specify.py +0 -0
  40. {shotgun_sh-0.1.4 → shotgun_sh-0.1.6}/src/shotgun/agents/tasks.py +0 -0
  41. {shotgun_sh-0.1.4 → shotgun_sh-0.1.6}/src/shotgun/agents/tools/__init__.py +0 -0
  42. {shotgun_sh-0.1.4 → shotgun_sh-0.1.6}/src/shotgun/agents/tools/codebase/__init__.py +0 -0
  43. {shotgun_sh-0.1.4 → shotgun_sh-0.1.6}/src/shotgun/agents/tools/codebase/codebase_shell.py +0 -0
  44. {shotgun_sh-0.1.4 → shotgun_sh-0.1.6}/src/shotgun/agents/tools/codebase/directory_lister.py +0 -0
  45. {shotgun_sh-0.1.4 → shotgun_sh-0.1.6}/src/shotgun/agents/tools/codebase/file_read.py +0 -0
  46. {shotgun_sh-0.1.4 → shotgun_sh-0.1.6}/src/shotgun/agents/tools/codebase/models.py +0 -0
  47. {shotgun_sh-0.1.4 → shotgun_sh-0.1.6}/src/shotgun/agents/tools/codebase/query_graph.py +0 -0
  48. {shotgun_sh-0.1.4 → shotgun_sh-0.1.6}/src/shotgun/agents/tools/codebase/retrieve_code.py +0 -0
  49. {shotgun_sh-0.1.4 → shotgun_sh-0.1.6}/src/shotgun/agents/tools/file_management.py +0 -0
  50. {shotgun_sh-0.1.4 → shotgun_sh-0.1.6}/src/shotgun/agents/tools/user_interaction.py +0 -0
  51. {shotgun_sh-0.1.4 → shotgun_sh-0.1.6}/src/shotgun/agents/tools/web_search/__init__.py +0 -0
  52. {shotgun_sh-0.1.4 → shotgun_sh-0.1.6}/src/shotgun/agents/tools/web_search/anthropic.py +0 -0
  53. {shotgun_sh-0.1.4 → shotgun_sh-0.1.6}/src/shotgun/agents/tools/web_search/gemini.py +0 -0
  54. {shotgun_sh-0.1.4 → shotgun_sh-0.1.6}/src/shotgun/agents/tools/web_search/openai.py +0 -0
  55. {shotgun_sh-0.1.4 → shotgun_sh-0.1.6}/src/shotgun/agents/tools/web_search/utils.py +0 -0
  56. {shotgun_sh-0.1.4 → shotgun_sh-0.1.6}/src/shotgun/build_constants.py +0 -0
  57. {shotgun_sh-0.1.4 → shotgun_sh-0.1.6}/src/shotgun/cli/__init__.py +0 -0
  58. {shotgun_sh-0.1.4 → shotgun_sh-0.1.6}/src/shotgun/cli/codebase/__init__.py +0 -0
  59. {shotgun_sh-0.1.4 → shotgun_sh-0.1.6}/src/shotgun/cli/codebase/commands.py +0 -0
  60. {shotgun_sh-0.1.4 → shotgun_sh-0.1.6}/src/shotgun/cli/codebase/models.py +0 -0
  61. {shotgun_sh-0.1.4 → shotgun_sh-0.1.6}/src/shotgun/cli/config.py +0 -0
  62. {shotgun_sh-0.1.4 → shotgun_sh-0.1.6}/src/shotgun/cli/export.py +0 -0
  63. {shotgun_sh-0.1.4 → shotgun_sh-0.1.6}/src/shotgun/cli/models.py +0 -0
  64. {shotgun_sh-0.1.4 → shotgun_sh-0.1.6}/src/shotgun/cli/plan.py +0 -0
  65. {shotgun_sh-0.1.4 → shotgun_sh-0.1.6}/src/shotgun/cli/research.py +0 -0
  66. {shotgun_sh-0.1.4 → shotgun_sh-0.1.6}/src/shotgun/cli/specify.py +0 -0
  67. {shotgun_sh-0.1.4 → shotgun_sh-0.1.6}/src/shotgun/cli/tasks.py +0 -0
  68. {shotgun_sh-0.1.4 → shotgun_sh-0.1.6}/src/shotgun/cli/update.py +0 -0
  69. {shotgun_sh-0.1.4 → shotgun_sh-0.1.6}/src/shotgun/cli/utils.py +0 -0
  70. {shotgun_sh-0.1.4 → shotgun_sh-0.1.6}/src/shotgun/codebase/__init__.py +0 -0
  71. {shotgun_sh-0.1.4 → shotgun_sh-0.1.6}/src/shotgun/codebase/core/__init__.py +0 -0
  72. {shotgun_sh-0.1.4 → shotgun_sh-0.1.6}/src/shotgun/codebase/core/change_detector.py +0 -0
  73. {shotgun_sh-0.1.4 → shotgun_sh-0.1.6}/src/shotgun/codebase/core/code_retrieval.py +0 -0
  74. {shotgun_sh-0.1.4 → shotgun_sh-0.1.6}/src/shotgun/codebase/core/ingestor.py +0 -0
  75. {shotgun_sh-0.1.4 → shotgun_sh-0.1.6}/src/shotgun/codebase/core/language_config.py +0 -0
  76. {shotgun_sh-0.1.4 → shotgun_sh-0.1.6}/src/shotgun/codebase/core/manager.py +0 -0
  77. {shotgun_sh-0.1.4 → shotgun_sh-0.1.6}/src/shotgun/codebase/core/parser_loader.py +0 -0
  78. {shotgun_sh-0.1.4 → shotgun_sh-0.1.6}/src/shotgun/codebase/models.py +0 -0
  79. {shotgun_sh-0.1.4 → shotgun_sh-0.1.6}/src/shotgun/logging_config.py +0 -0
  80. {shotgun_sh-0.1.4 → shotgun_sh-0.1.6}/src/shotgun/main.py +0 -0
  81. {shotgun_sh-0.1.4 → shotgun_sh-0.1.6}/src/shotgun/posthog_telemetry.py +0 -0
  82. {shotgun_sh-0.1.4 → shotgun_sh-0.1.6}/src/shotgun/prompts/__init__.py +0 -0
  83. {shotgun_sh-0.1.4 → shotgun_sh-0.1.6}/src/shotgun/prompts/agents/__init__.py +0 -0
  84. {shotgun_sh-0.1.4 → shotgun_sh-0.1.6}/src/shotgun/prompts/agents/export.j2 +0 -0
  85. {shotgun_sh-0.1.4 → shotgun_sh-0.1.6}/src/shotgun/prompts/agents/partials/codebase_understanding.j2 +0 -0
  86. {shotgun_sh-0.1.4 → shotgun_sh-0.1.6}/src/shotgun/prompts/agents/partials/common_agent_system_prompt.j2 +0 -0
  87. {shotgun_sh-0.1.4 → shotgun_sh-0.1.6}/src/shotgun/prompts/agents/partials/content_formatting.j2 +0 -0
  88. {shotgun_sh-0.1.4 → shotgun_sh-0.1.6}/src/shotgun/prompts/agents/partials/interactive_mode.j2 +0 -0
  89. {shotgun_sh-0.1.4 → shotgun_sh-0.1.6}/src/shotgun/prompts/agents/plan.j2 +0 -0
  90. {shotgun_sh-0.1.4 → shotgun_sh-0.1.6}/src/shotgun/prompts/agents/research.j2 +0 -0
  91. {shotgun_sh-0.1.4 → shotgun_sh-0.1.6}/src/shotgun/prompts/agents/specify.j2 +0 -0
  92. {shotgun_sh-0.1.4 → shotgun_sh-0.1.6}/src/shotgun/prompts/agents/state/codebase/codebase_graphs_available.j2 +0 -0
  93. {shotgun_sh-0.1.4 → shotgun_sh-0.1.6}/src/shotgun/prompts/agents/state/system_state.j2 +0 -0
  94. {shotgun_sh-0.1.4 → shotgun_sh-0.1.6}/src/shotgun/prompts/agents/tasks.j2 +0 -0
  95. {shotgun_sh-0.1.4 → shotgun_sh-0.1.6}/src/shotgun/prompts/codebase/__init__.py +0 -0
  96. {shotgun_sh-0.1.4 → shotgun_sh-0.1.6}/src/shotgun/prompts/codebase/cypher_query_patterns.j2 +0 -0
  97. {shotgun_sh-0.1.4 → shotgun_sh-0.1.6}/src/shotgun/prompts/codebase/enhanced_query_context.j2 +0 -0
  98. {shotgun_sh-0.1.4 → shotgun_sh-0.1.6}/src/shotgun/prompts/codebase/partials/cypher_rules.j2 +0 -0
  99. {shotgun_sh-0.1.4 → shotgun_sh-0.1.6}/src/shotgun/prompts/codebase/partials/graph_schema.j2 +0 -0
  100. {shotgun_sh-0.1.4 → shotgun_sh-0.1.6}/src/shotgun/prompts/codebase/partials/temporal_context.j2 +0 -0
  101. {shotgun_sh-0.1.4 → shotgun_sh-0.1.6}/src/shotgun/prompts/history/__init__.py +0 -0
  102. {shotgun_sh-0.1.4 → shotgun_sh-0.1.6}/src/shotgun/prompts/history/incremental_summarization.j2 +0 -0
  103. {shotgun_sh-0.1.4 → shotgun_sh-0.1.6}/src/shotgun/prompts/history/summarization.j2 +0 -0
  104. {shotgun_sh-0.1.4 → shotgun_sh-0.1.6}/src/shotgun/prompts/loader.py +0 -0
  105. {shotgun_sh-0.1.4 → shotgun_sh-0.1.6}/src/shotgun/py.typed +0 -0
  106. {shotgun_sh-0.1.4 → shotgun_sh-0.1.6}/src/shotgun/sdk/__init__.py +0 -0
  107. {shotgun_sh-0.1.4 → shotgun_sh-0.1.6}/src/shotgun/sdk/codebase.py +0 -0
  108. {shotgun_sh-0.1.4 → shotgun_sh-0.1.6}/src/shotgun/sdk/exceptions.py +0 -0
  109. {shotgun_sh-0.1.4 → shotgun_sh-0.1.6}/src/shotgun/sdk/models.py +0 -0
  110. {shotgun_sh-0.1.4 → shotgun_sh-0.1.6}/src/shotgun/sdk/services.py +0 -0
  111. {shotgun_sh-0.1.4 → shotgun_sh-0.1.6}/src/shotgun/sentry_telemetry.py +0 -0
  112. {shotgun_sh-0.1.4 → shotgun_sh-0.1.6}/src/shotgun/telemetry.py +0 -0
  113. {shotgun_sh-0.1.4 → shotgun_sh-0.1.6}/src/shotgun/tui/__init__.py +0 -0
  114. {shotgun_sh-0.1.4 → shotgun_sh-0.1.6}/src/shotgun/tui/app.py +0 -0
  115. {shotgun_sh-0.1.4 → shotgun_sh-0.1.6}/src/shotgun/tui/commands/__init__.py +0 -0
  116. {shotgun_sh-0.1.4 → shotgun_sh-0.1.6}/src/shotgun/tui/components/prompt_input.py +0 -0
  117. {shotgun_sh-0.1.4 → shotgun_sh-0.1.6}/src/shotgun/tui/components/spinner.py +0 -0
  118. {shotgun_sh-0.1.4 → shotgun_sh-0.1.6}/src/shotgun/tui/components/splash.py +0 -0
  119. {shotgun_sh-0.1.4 → shotgun_sh-0.1.6}/src/shotgun/tui/components/vertical_tail.py +0 -0
  120. {shotgun_sh-0.1.4 → shotgun_sh-0.1.6}/src/shotgun/tui/screens/chat.tcss +0 -0
  121. {shotgun_sh-0.1.4 → shotgun_sh-0.1.6}/src/shotgun/tui/screens/chat_screen/__init__.py +0 -0
  122. {shotgun_sh-0.1.4 → shotgun_sh-0.1.6}/src/shotgun/tui/screens/chat_screen/command_providers.py +0 -0
  123. {shotgun_sh-0.1.4 → shotgun_sh-0.1.6}/src/shotgun/tui/screens/chat_screen/hint_message.py +0 -0
  124. {shotgun_sh-0.1.4 → shotgun_sh-0.1.6}/src/shotgun/tui/screens/directory_setup.py +0 -0
  125. {shotgun_sh-0.1.4 → shotgun_sh-0.1.6}/src/shotgun/tui/screens/provider_config.py +0 -0
  126. {shotgun_sh-0.1.4 → shotgun_sh-0.1.6}/src/shotgun/tui/screens/splash.py +0 -0
  127. {shotgun_sh-0.1.4 → shotgun_sh-0.1.6}/src/shotgun/tui/styles.tcss +0 -0
  128. {shotgun_sh-0.1.4 → shotgun_sh-0.1.6}/src/shotgun/tui/utils/__init__.py +0 -0
  129. {shotgun_sh-0.1.4 → shotgun_sh-0.1.6}/src/shotgun/tui/utils/mode_progress.py +0 -0
  130. {shotgun_sh-0.1.4 → shotgun_sh-0.1.6}/src/shotgun/utils/__init__.py +0 -0
  131. {shotgun_sh-0.1.4 → shotgun_sh-0.1.6}/src/shotgun/utils/env_utils.py +0 -0
  132. {shotgun_sh-0.1.4 → shotgun_sh-0.1.6}/src/shotgun/utils/file_system_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: shotgun-sh
3
- Version: 0.1.4
3
+ Version: 0.1.6
4
4
  Summary: AI-powered research, planning, and task management CLI tool
5
5
  Project-URL: Homepage, https://shotgun.sh/
6
6
  Project-URL: Repository, https://github.com/shotgun-sh/shotgun
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "shotgun-sh"
3
- version = "0.1.4"
3
+ version = "0.1.6"
4
4
  description = "AI-powered research, planning, and task management CLI tool"
5
5
  readme = "README.md"
6
6
  license = { text = "MIT" }
@@ -362,17 +362,6 @@ 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)
376
365
  self._stream_state = None
377
366
 
378
367
  self.ui_message_history = original_messages + cast(
@@ -0,0 +1,46 @@
1
+ """Pydantic models and exceptions for Cypher query generation."""
2
+
3
+ from typing import Any
4
+
5
+ from pydantic import BaseModel, Field
6
+
7
+
8
+ class CypherGenerationResponse(BaseModel):
9
+ """Structured response from LLM for Cypher query generation.
10
+
11
+ This model ensures the LLM explicitly indicates whether it can generate
12
+ a valid Cypher query and provides a reason if it cannot.
13
+ """
14
+
15
+ cypher_query: str | None = Field(
16
+ default=None,
17
+ description="The generated Cypher query, or None if generation not possible",
18
+ )
19
+ can_generate_valid_cypher: bool = Field(
20
+ description="Whether a valid Cypher query can be generated for this request"
21
+ )
22
+ reason_cannot_generate: str | None = Field(
23
+ default=None,
24
+ description="Explanation why query cannot be generated (if applicable)",
25
+ )
26
+
27
+ def model_post_init(self, __context: Any) -> None:
28
+ """Validate that reason is provided when query cannot be generated."""
29
+ if not self.can_generate_valid_cypher and not self.reason_cannot_generate:
30
+ self.reason_cannot_generate = "No reason provided"
31
+ if self.can_generate_valid_cypher and not self.cypher_query:
32
+ raise ValueError(
33
+ "cypher_query must be provided when can_generate_valid_cypher is True"
34
+ )
35
+
36
+
37
+ class CypherGenerationNotPossibleError(Exception):
38
+ """Raised when LLM cannot generate valid Cypher for the query.
39
+
40
+ This typically happens when the query is conceptual rather than structural,
41
+ or when it requires interpretation beyond what can be expressed in Cypher.
42
+ """
43
+
44
+ def __init__(self, reason: str):
45
+ self.reason = reason
46
+ super().__init__(f"Cannot generate Cypher query: {reason}")
@@ -4,15 +4,13 @@ import time
4
4
  from datetime import datetime
5
5
  from typing import TYPE_CHECKING
6
6
 
7
- from pydantic_ai.messages import (
8
- ModelRequest,
9
- SystemPromptPart,
10
- TextPart,
11
- UserPromptPart,
12
- )
7
+ from pydantic_ai import Agent
13
8
 
14
9
  from shotgun.agents.config import get_provider_model
15
- from shotgun.agents.config.models import shotgun_model_request
10
+ from shotgun.codebase.core.cypher_models import (
11
+ CypherGenerationNotPossibleError,
12
+ CypherGenerationResponse,
13
+ )
16
14
  from shotgun.logging_config import get_logger
17
15
  from shotgun.prompts import PromptLoader
18
16
 
@@ -25,42 +23,52 @@ logger = get_logger(__name__)
25
23
  prompt_loader = PromptLoader()
26
24
 
27
25
 
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.
26
+ async def llm_cypher_prompt(
27
+ system_prompt: str, user_prompt: str
28
+ ) -> CypherGenerationResponse:
29
+ """Generate a Cypher query from a natural language prompt using structured output.
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
- The generated Cypher query as a string
35
+ CypherGenerationResponse with cypher_query, can_generate flag, and reason if not
36
36
  """
37
37
  model_config = get_provider_model()
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
38
+
39
+ # Create an agent with structured output for Cypher generation
40
+ cypher_agent = Agent(
41
+ model=model_config.model_instance,
42
+ output_type=CypherGenerationResponse,
43
+ retries=2,
52
44
  )
53
45
 
54
- if not query_cypher_response.parts or not query_cypher_response.parts[0]:
55
- raise ValueError("Empty response from LLM")
46
+ # Combine system and user prompts
47
+ combined_prompt = f"{system_prompt}\n\nUser Query: {user_prompt}"
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
56
63
 
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
64
+ except Exception as e:
65
+ logger.error("Failed to generate Cypher query with structured output: %s", e)
66
+ # Return a failure response
67
+ return CypherGenerationResponse(
68
+ cypher_query=None,
69
+ can_generate_valid_cypher=False,
70
+ reason_cannot_generate=f"LLM error: {str(e)}",
71
+ )
64
72
 
65
73
 
66
74
  async def generate_cypher(natural_language_query: str) -> str:
@@ -71,6 +79,10 @@ async def generate_cypher(natural_language_query: str) -> str:
71
79
 
72
80
  Returns:
73
81
  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
74
86
  """
75
87
  # Get current time for context
76
88
  current_timestamp = int(time.time())
@@ -88,8 +100,30 @@ async def generate_cypher(natural_language_query: str) -> str:
88
100
  )
89
101
 
90
102
  try:
91
- cypher_query = await llm_cypher_prompt(system_prompt, enhanced_query)
92
- cleaned_query = clean_cypher_response(cypher_query)
103
+ response = await llm_cypher_prompt(system_prompt, enhanced_query)
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}")
93
127
 
94
128
  # Validate UNION ALL queries
95
129
  is_valid, validation_error = validate_union_query(cleaned_query)
@@ -100,6 +134,8 @@ async def generate_cypher(natural_language_query: str) -> str:
100
134
 
101
135
  return cleaned_query
102
136
 
137
+ except CypherGenerationNotPossibleError:
138
+ raise # Re-raise as-is
103
139
  except Exception as e:
104
140
  raise RuntimeError(f"Failed to generate Cypher query: {e}") from e
105
141
 
@@ -170,8 +206,31 @@ MATCH (f:Function) RETURN f.name, f.qualified_name // WRONG: missing third colu
170
206
  base_system_prompt=prompt_loader.render("codebase/cypher_system.j2"),
171
207
  )
172
208
 
173
- cypher_query = await llm_cypher_prompt(enhanced_system_prompt, enhanced_query)
174
- cleaned_query = clean_cypher_response(cypher_query)
209
+ response = await llm_cypher_prompt(enhanced_system_prompt, enhanced_query)
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}")
175
234
 
176
235
  # Validate UNION ALL queries
177
236
  is_valid, validation_error = validate_union_query(cleaned_query)
@@ -182,6 +241,8 @@ MATCH (f:Function) RETURN f.name, f.qualified_name // WRONG: missing third colu
182
241
 
183
242
  return cleaned_query
184
243
 
244
+ except CypherGenerationNotPossibleError:
245
+ raise # Re-raise as-is
185
246
  except Exception as e:
186
247
  raise RuntimeError(
187
248
  f"Failed to generate Cypher query with error context: {e}"
@@ -202,6 +263,10 @@ async def generate_cypher_openai_async(
202
263
 
203
264
  Returns:
204
265
  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
205
270
  """
206
271
  # Get current time for context
207
272
  current_timestamp = int(time.time())
@@ -219,9 +284,26 @@ async def generate_cypher_openai_async(
219
284
  )
220
285
 
221
286
  try:
222
- cypher_query = await llm_cypher_prompt(system_prompt, enhanced_query)
223
- return clean_cypher_response(cypher_query)
287
+ response = await llm_cypher_prompt(system_prompt, enhanced_query)
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
+ )
224
299
 
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
225
307
  except Exception as e:
226
308
  logger.error(f"OpenAI API error: {e}")
227
309
  raise RuntimeError(f"Failed to generate Cypher query: {e}") from e
@@ -288,6 +370,65 @@ def validate_union_query(cypher_query: str) -> tuple[bool, str]:
288
370
  return True, ""
289
371
 
290
372
 
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
+
291
432
  def clean_cypher_response(response_text: str) -> str:
292
433
  """Clean up common LLM formatting artifacts from a Cypher query.
293
434
 
@@ -4,6 +4,7 @@ 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
7
8
  from shotgun.codebase.core.manager import CodebaseGraphManager
8
9
  from shotgun.codebase.core.nl_query import generate_cypher
9
10
  from shotgun.codebase.models import CodebaseGraph, QueryResult, QueryType
@@ -190,6 +191,22 @@ class CodebaseService:
190
191
  error=None,
191
192
  )
192
193
 
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
+
193
210
  except Exception as e:
194
211
  execution_time = (time.time() - start_time) * 1000
195
212
  logger.error(f"Query execution failed: {e}")
@@ -25,4 +25,18 @@ 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
- Provide only the Cypher query.
28
+ You must return a structured JSON response with the following fields:
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."
@@ -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(already_indexed=True))
415
415
  return
416
416
 
417
417
  should_index = await self.app.push_screen_wait(CodebaseIndexPromptScreen())
@@ -419,6 +419,8 @@ 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
+
422
424
  self.index_codebase_command()
423
425
 
424
426
  def watch_mode(self, new_mode: AgentType) -> None:
@@ -692,7 +694,6 @@ class ChatScreen(Screen[None]):
692
694
  timeout=8,
693
695
  )
694
696
 
695
- self.mount_hint(codebase_indexed_hint(selection.name))
696
697
  except CodebaseAlreadyIndexedError as exc:
697
698
  logger.warning(f"Codebase already indexed: {exc}")
698
699
  self.notify(str(exc), severity="warning")
@@ -796,17 +797,10 @@ class ChatScreen(Screen[None]):
796
797
  self.mode = AgentType(conversation.last_agent_model)
797
798
 
798
799
 
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:
800
+ def help_text_with_codebase(already_indexed: bool = False) -> str:
807
801
  return (
808
802
  "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"
809
- "I can help with:\n\n"
803
+ f"{'' if already_indexed else 'Once your codebase is indexed, '}I can help with:\n\n"
810
804
  "- Speccing out a new feature\n"
811
805
  "- Onboarding you onto this project\n"
812
806
  "- Helping with a refactor spec\n"
@@ -136,7 +136,8 @@ 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
- acc += " ∟ finished\n\n" # let's not show anything yet
139
+ # acc += " ∟ finished\n\n" # let's not show anything yet
140
+ pass
140
141
  elif isinstance(part, UserPromptPart):
141
142
  acc += f"**>** {part.content}\n\n"
142
143
  return acc
@@ -152,7 +153,7 @@ class AgentResponseWidget(Widget):
152
153
  if self.item is None:
153
154
  yield Markdown(markdown="")
154
155
  else:
155
- yield Markdown(markdown=f"**⏺** {self.compute_output()}")
156
+ yield Markdown(markdown=self.compute_output())
156
157
 
157
158
  def compute_output(self) -> str:
158
159
  acc = ""
@@ -160,10 +161,10 @@ class AgentResponseWidget(Widget):
160
161
  return ""
161
162
  for idx, part in enumerate(self.item.parts):
162
163
  if isinstance(part, TextPart):
163
- acc += f"{part.content}\n\n"
164
+ acc += f"**⏺** {part.content}\n\n"
164
165
  elif isinstance(part, ToolCallPart):
165
166
  parts_str = self._format_tool_call_part(part)
166
- acc += f"{part.tool_name}: " + parts_str + "\n\n"
167
+ acc += parts_str + "\n\n"
167
168
  elif isinstance(part, BuiltinToolCallPart):
168
169
  acc += f"{part.tool_name}({part.args})\n\n"
169
170
  elif isinstance(part, BuiltinToolReturnPart):
@@ -288,7 +288,12 @@ def perform_update(force: bool = False) -> tuple[bool, str]:
288
288
 
289
289
  # Verify actual installation by checking version
290
290
  update_successful = False
291
- if result.returncode == 0 or pipx_success:
291
+
292
+ # For pipx with return code 0, trust it succeeded
293
+ if method == "pipx" and result.returncode == 0:
294
+ update_successful = True
295
+ logger.debug("Pipx returned 0, trusting update succeeded")
296
+ elif result.returncode == 0 or pipx_success:
292
297
  # Give the system a moment to update the package metadata
293
298
  import time
294
299
 
@@ -360,8 +365,9 @@ def perform_update(force: bool = False) -> tuple[bool, str]:
360
365
  )
361
366
  except Exception as e:
362
367
  logger.debug(f"Version verification failed: {e}")
363
- # If verification fails, trust the return code or pipx patterns
364
- update_successful = result.returncode == 0 or pipx_success
368
+ # If verification fails but initial command succeeded, trust it
369
+ if not update_successful:
370
+ update_successful = result.returncode == 0 or pipx_success
365
371
 
366
372
  if update_successful:
367
373
  message = f"Successfully updated from {current_version} to {latest}"
@@ -374,7 +380,13 @@ def perform_update(force: bool = False) -> tuple[bool, str]:
374
380
 
375
381
  return True, message
376
382
  else:
377
- error_msg = f"Update failed: {result.stderr or result.stdout}"
383
+ # Only use stderr for error message, stdout often contains normal progress
384
+ if result.stderr:
385
+ error_msg = f"Update failed: {result.stderr}"
386
+ elif result.returncode != 0:
387
+ error_msg = f"Update failed with exit code {result.returncode}: {result.stdout or 'No output'}"
388
+ else:
389
+ error_msg = "Update verification failed but command may have succeeded"
378
390
  logger.error(error_msg)
379
391
  return False, error_msg
380
392
 
File without changes
File without changes
File without changes
File without changes