shotgun-sh 0.1.0.dev1__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.

Files changed (94) hide show
  1. shotgun/__init__.py +3 -0
  2. shotgun/agents/__init__.py +1 -0
  3. shotgun/agents/agent_manager.py +196 -0
  4. shotgun/agents/common.py +295 -0
  5. shotgun/agents/config/__init__.py +13 -0
  6. shotgun/agents/config/manager.py +215 -0
  7. shotgun/agents/config/models.py +120 -0
  8. shotgun/agents/config/provider.py +91 -0
  9. shotgun/agents/history/__init__.py +5 -0
  10. shotgun/agents/history/history_processors.py +213 -0
  11. shotgun/agents/models.py +94 -0
  12. shotgun/agents/plan.py +119 -0
  13. shotgun/agents/research.py +131 -0
  14. shotgun/agents/tasks.py +122 -0
  15. shotgun/agents/tools/__init__.py +26 -0
  16. shotgun/agents/tools/codebase/__init__.py +28 -0
  17. shotgun/agents/tools/codebase/codebase_shell.py +256 -0
  18. shotgun/agents/tools/codebase/directory_lister.py +141 -0
  19. shotgun/agents/tools/codebase/file_read.py +144 -0
  20. shotgun/agents/tools/codebase/models.py +252 -0
  21. shotgun/agents/tools/codebase/query_graph.py +67 -0
  22. shotgun/agents/tools/codebase/retrieve_code.py +81 -0
  23. shotgun/agents/tools/file_management.py +130 -0
  24. shotgun/agents/tools/user_interaction.py +36 -0
  25. shotgun/agents/tools/web_search.py +69 -0
  26. shotgun/cli/__init__.py +1 -0
  27. shotgun/cli/codebase/__init__.py +5 -0
  28. shotgun/cli/codebase/commands.py +202 -0
  29. shotgun/cli/codebase/models.py +21 -0
  30. shotgun/cli/config.py +261 -0
  31. shotgun/cli/models.py +10 -0
  32. shotgun/cli/plan.py +65 -0
  33. shotgun/cli/research.py +78 -0
  34. shotgun/cli/tasks.py +71 -0
  35. shotgun/cli/utils.py +25 -0
  36. shotgun/codebase/__init__.py +12 -0
  37. shotgun/codebase/core/__init__.py +46 -0
  38. shotgun/codebase/core/change_detector.py +358 -0
  39. shotgun/codebase/core/code_retrieval.py +243 -0
  40. shotgun/codebase/core/ingestor.py +1497 -0
  41. shotgun/codebase/core/language_config.py +297 -0
  42. shotgun/codebase/core/manager.py +1554 -0
  43. shotgun/codebase/core/nl_query.py +327 -0
  44. shotgun/codebase/core/parser_loader.py +152 -0
  45. shotgun/codebase/models.py +107 -0
  46. shotgun/codebase/service.py +148 -0
  47. shotgun/logging_config.py +172 -0
  48. shotgun/main.py +73 -0
  49. shotgun/prompts/__init__.py +5 -0
  50. shotgun/prompts/agents/__init__.py +1 -0
  51. shotgun/prompts/agents/partials/codebase_understanding.j2 +79 -0
  52. shotgun/prompts/agents/partials/common_agent_system_prompt.j2 +10 -0
  53. shotgun/prompts/agents/partials/interactive_mode.j2 +8 -0
  54. shotgun/prompts/agents/plan.j2 +57 -0
  55. shotgun/prompts/agents/research.j2 +38 -0
  56. shotgun/prompts/agents/state/codebase/codebase_graphs_available.j2 +13 -0
  57. shotgun/prompts/agents/state/system_state.j2 +1 -0
  58. shotgun/prompts/agents/tasks.j2 +67 -0
  59. shotgun/prompts/codebase/__init__.py +1 -0
  60. shotgun/prompts/codebase/cypher_query_patterns.j2 +221 -0
  61. shotgun/prompts/codebase/cypher_system.j2 +28 -0
  62. shotgun/prompts/codebase/enhanced_query_context.j2 +10 -0
  63. shotgun/prompts/codebase/partials/cypher_rules.j2 +24 -0
  64. shotgun/prompts/codebase/partials/graph_schema.j2 +28 -0
  65. shotgun/prompts/codebase/partials/temporal_context.j2 +21 -0
  66. shotgun/prompts/history/__init__.py +1 -0
  67. shotgun/prompts/history/summarization.j2 +46 -0
  68. shotgun/prompts/loader.py +140 -0
  69. shotgun/prompts/user/research.j2 +5 -0
  70. shotgun/py.typed +0 -0
  71. shotgun/sdk/__init__.py +13 -0
  72. shotgun/sdk/codebase.py +195 -0
  73. shotgun/sdk/exceptions.py +17 -0
  74. shotgun/sdk/models.py +189 -0
  75. shotgun/sdk/services.py +23 -0
  76. shotgun/telemetry.py +68 -0
  77. shotgun/tui/__init__.py +0 -0
  78. shotgun/tui/app.py +49 -0
  79. shotgun/tui/components/prompt_input.py +69 -0
  80. shotgun/tui/components/spinner.py +86 -0
  81. shotgun/tui/components/splash.py +25 -0
  82. shotgun/tui/components/vertical_tail.py +28 -0
  83. shotgun/tui/screens/chat.py +415 -0
  84. shotgun/tui/screens/chat.tcss +28 -0
  85. shotgun/tui/screens/provider_config.py +221 -0
  86. shotgun/tui/screens/splash.py +31 -0
  87. shotgun/tui/styles.tcss +10 -0
  88. shotgun/utils/__init__.py +5 -0
  89. shotgun/utils/file_system_utils.py +31 -0
  90. shotgun_sh-0.1.0.dev1.dist-info/METADATA +318 -0
  91. shotgun_sh-0.1.0.dev1.dist-info/RECORD +94 -0
  92. shotgun_sh-0.1.0.dev1.dist-info/WHEEL +4 -0
  93. shotgun_sh-0.1.0.dev1.dist-info/entry_points.txt +3 -0
  94. shotgun_sh-0.1.0.dev1.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,327 @@
1
+ """Natural language to Cypher query conversion for code graphs."""
2
+
3
+ import time
4
+ from datetime import datetime
5
+ from typing import TYPE_CHECKING
6
+
7
+ from pydantic_ai.direct import model_request
8
+ from pydantic_ai.messages import (
9
+ ModelRequest,
10
+ SystemPromptPart,
11
+ TextPart,
12
+ UserPromptPart,
13
+ )
14
+
15
+ from shotgun.agents.config import get_provider_model
16
+ from shotgun.logging_config import get_logger
17
+ from shotgun.prompts import PromptLoader
18
+
19
+ if TYPE_CHECKING:
20
+ from openai import AsyncOpenAI
21
+
22
+ logger = get_logger(__name__)
23
+
24
+ # Global prompt loader instance
25
+ prompt_loader = PromptLoader()
26
+
27
+
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
+
31
+ Args:
32
+ system_prompt: The system prompt defining the behavior and context for the LLM
33
+ user_prompt: The user's natural language query
34
+ Returns:
35
+ The generated Cypher query as a string
36
+ """
37
+ model_config = get_provider_model()
38
+ query_cypher_response = await model_request(
39
+ model=model_config.pydantic_model_name,
40
+ messages=[
41
+ ModelRequest(
42
+ parts=[
43
+ SystemPromptPart(content=system_prompt),
44
+ UserPromptPart(content=user_prompt),
45
+ ]
46
+ ),
47
+ ],
48
+ )
49
+
50
+ if not query_cypher_response.parts or not query_cypher_response.parts[0]:
51
+ raise ValueError("Empty response from LLM")
52
+
53
+ message_part = query_cypher_response.parts[0]
54
+ if not isinstance(message_part, TextPart):
55
+ raise ValueError("Unexpected response part type from LLM")
56
+ cypher_query = str(message_part.content)
57
+ if not cypher_query:
58
+ raise ValueError("Empty content in LLM response")
59
+ return cypher_query
60
+
61
+
62
+ async def generate_cypher(natural_language_query: str) -> str:
63
+ """Convert a natural language query to Cypher using Shotgun's LLM client.
64
+
65
+ Args:
66
+ natural_language_query: The user's query in natural language
67
+
68
+ Returns:
69
+ Generated Cypher query
70
+ """
71
+ # Get current time for context
72
+ current_timestamp = int(time.time())
73
+ current_datetime = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
74
+
75
+ # Generate system prompt using template
76
+ system_prompt = prompt_loader.render("codebase/cypher_system.j2")
77
+
78
+ # Generate enhanced query using template
79
+ enhanced_query = prompt_loader.render(
80
+ "codebase/enhanced_query_context.j2",
81
+ current_datetime=current_datetime,
82
+ current_timestamp=current_timestamp,
83
+ natural_language_query=natural_language_query,
84
+ )
85
+
86
+ try:
87
+ cypher_query = await llm_cypher_prompt(system_prompt, enhanced_query)
88
+ cleaned_query = clean_cypher_response(cypher_query)
89
+
90
+ # Validate UNION ALL queries
91
+ is_valid, validation_error = validate_union_query(cleaned_query)
92
+ if not is_valid:
93
+ logger.warning(f"Generated query failed validation: {validation_error}")
94
+ logger.warning(f"Problematic query: {cleaned_query}")
95
+ raise ValueError(f"Generated query validation failed: {validation_error}")
96
+
97
+ return cleaned_query
98
+
99
+ except Exception as e:
100
+ raise RuntimeError(f"Failed to generate Cypher query: {e}") from e
101
+
102
+
103
+ async def generate_cypher_with_error_context(
104
+ natural_language_query: str, error_context: str = ""
105
+ ) -> str:
106
+ """Convert a natural language query to Cypher with additional error context for retry scenarios.
107
+
108
+ Args:
109
+ natural_language_query: The user's query in natural language
110
+ error_context: Additional context about previous errors to help generate better query
111
+
112
+ Returns:
113
+ Generated Cypher query
114
+ """
115
+ # Get current time for context
116
+ current_timestamp = int(time.time())
117
+ current_datetime = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
118
+
119
+ # Generate enhanced query with error context using template
120
+ enhanced_query = prompt_loader.render_string(
121
+ """Current datetime: {{ current_datetime }} (Unix timestamp: {{ current_timestamp }})
122
+
123
+ User query: {{ natural_language_query }}
124
+
125
+ ERROR CONTEXT (CRITICAL - Previous attempt failed):
126
+ {{ error_context }}
127
+
128
+ IMPORTANT: All timestamps in the database are stored as Unix timestamps (INT64). When generating time-based queries:
129
+ - For "2 minutes ago": use {{ current_timestamp - 120 }}
130
+ - For "1 hour ago": use {{ current_timestamp - 3600 }}
131
+ - For "today": use timestamps >= {{ current_timestamp - (current_timestamp % 86400) }}
132
+ - For "yesterday": use timestamps between {{ current_timestamp - 86400 - (current_timestamp % 86400) }} and {{ current_timestamp - (current_timestamp % 86400) }}
133
+ - NEVER use placeholder values like 1704067200, always calculate based on the current timestamp: {{ current_timestamp }}""",
134
+ current_datetime=current_datetime,
135
+ current_timestamp=current_timestamp,
136
+ natural_language_query=natural_language_query,
137
+ error_context=error_context,
138
+ )
139
+
140
+ try:
141
+ # Create enhanced system prompt with error recovery instructions
142
+ enhanced_system_prompt = prompt_loader.render_string(
143
+ """{{ base_system_prompt }}
144
+
145
+ **CRITICAL ERROR RECOVERY INSTRUCTIONS:**
146
+ When retrying after a UNION ALL error:
147
+ 1. Each UNION ALL branch MUST return exactly the same number of columns
148
+ 2. Column names MUST be in the same order across all branches
149
+ 3. Use explicit column aliases to ensure consistency: RETURN prop1 as name, prop2 as qualified_name, 'Type' as type
150
+ 4. If different node types have different properties, use COALESCE or NULL for missing properties
151
+ 5. Test each UNION branch separately before combining
152
+
153
+ Example of CORRECT UNION ALL:
154
+ ```cypher
155
+ MATCH (c:Class) RETURN c.name as name, c.qualified_name as qualified_name, 'Class' as type
156
+ UNION ALL
157
+ MATCH (f:Function) RETURN f.name as name, f.qualified_name as qualified_name, 'Function' as type
158
+ ```
159
+
160
+ Example of INCORRECT UNION ALL (different column counts):
161
+ ```cypher
162
+ MATCH (c:Class) RETURN c.name, c.qualified_name, c.docstring
163
+ UNION ALL
164
+ MATCH (f:Function) RETURN f.name, f.qualified_name // WRONG: missing third column
165
+ ```""",
166
+ base_system_prompt=prompt_loader.render("codebase/cypher_system.j2"),
167
+ )
168
+
169
+ cypher_query = await llm_cypher_prompt(enhanced_system_prompt, enhanced_query)
170
+ cleaned_query = clean_cypher_response(cypher_query)
171
+
172
+ # Validate UNION ALL queries
173
+ is_valid, validation_error = validate_union_query(cleaned_query)
174
+ if not is_valid:
175
+ logger.warning(f"Retry query failed validation: {validation_error}")
176
+ logger.warning(f"Problematic retry query: {cleaned_query}")
177
+ raise ValueError(f"Retry query validation failed: {validation_error}")
178
+
179
+ return cleaned_query
180
+
181
+ except Exception as e:
182
+ raise RuntimeError(
183
+ f"Failed to generate Cypher query with error context: {e}"
184
+ ) from e
185
+
186
+
187
+ async def generate_cypher_openai_async(
188
+ client: "AsyncOpenAI", natural_language_query: str, model: str = "gpt-4o"
189
+ ) -> str:
190
+ """Convert a natural language query to Cypher using async OpenAI client.
191
+
192
+ This function is for standalone usage without Shotgun's LLM infrastructure.
193
+
194
+ Args:
195
+ client: Async OpenAI client instance
196
+ natural_language_query: The user's query in natural language
197
+ model: OpenAI model to use (default: gpt-4o)
198
+
199
+ Returns:
200
+ Generated Cypher query
201
+ """
202
+ # Get current time for context
203
+ current_timestamp = int(time.time())
204
+ current_datetime = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
205
+
206
+ # Generate system prompt using template
207
+ system_prompt = prompt_loader.render("codebase/cypher_system.j2")
208
+
209
+ # Generate enhanced query using template
210
+ enhanced_query = prompt_loader.render(
211
+ "codebase/enhanced_query_context.j2",
212
+ current_datetime=current_datetime,
213
+ current_timestamp=current_timestamp,
214
+ natural_language_query=natural_language_query,
215
+ )
216
+
217
+ try:
218
+ cypher_query = await llm_cypher_prompt(system_prompt, enhanced_query)
219
+ return clean_cypher_response(cypher_query)
220
+
221
+ except Exception as e:
222
+ logger.error(f"OpenAI API error: {e}")
223
+ raise RuntimeError(f"Failed to generate Cypher query: {e}") from e
224
+
225
+
226
+ def validate_union_query(cypher_query: str) -> tuple[bool, str]:
227
+ """Validate that UNION ALL queries have matching column counts and names.
228
+
229
+ Args:
230
+ cypher_query: The Cypher query to validate
231
+
232
+ Returns:
233
+ Tuple of (is_valid, error_message)
234
+ """
235
+ query_upper = cypher_query.upper()
236
+ if "UNION ALL" not in query_upper:
237
+ return True, ""
238
+
239
+ # Split by UNION ALL and extract RETURN clauses
240
+ parts = query_upper.split("UNION ALL")
241
+ return_patterns = []
242
+
243
+ for i, part in enumerate(parts):
244
+ if "RETURN" not in part:
245
+ continue
246
+
247
+ # Extract the RETURN clause
248
+ return_start = part.rfind("RETURN")
249
+ return_clause = part[return_start + 6 :] # Skip "RETURN "
250
+
251
+ # Stop at ORDER BY, LIMIT, or end of query
252
+ for stop_word in ["ORDER BY", "LIMIT", ";"]:
253
+ if stop_word in return_clause:
254
+ return_clause = return_clause.split(stop_word)[0]
255
+
256
+ # Parse columns (basic parsing - split by comma and handle AS aliases)
257
+ columns = []
258
+ for col in return_clause.split(","):
259
+ col = col.strip()
260
+ if " AS " in col:
261
+ # Extract the alias name after AS
262
+ alias = col.split(" AS ")[-1].strip()
263
+ columns.append(alias)
264
+ else:
265
+ # Use the column name as-is (simplified)
266
+ columns.append(col.strip())
267
+
268
+ return_patterns.append((i, columns))
269
+
270
+ # Check all parts have same number of columns
271
+ if len(return_patterns) < 2:
272
+ return True, ""
273
+
274
+ first_part, first_columns = return_patterns[0]
275
+ first_count = len(first_columns)
276
+
277
+ for part_idx, columns in return_patterns[1:]:
278
+ if len(columns) != first_count:
279
+ return (
280
+ False,
281
+ f"UNION ALL part {part_idx + 1} has {len(columns)} columns, expected {first_count}. First part columns: {first_columns}, this part: {columns}",
282
+ )
283
+
284
+ return True, ""
285
+
286
+
287
+ def clean_cypher_response(response_text: str) -> str:
288
+ """Clean up common LLM formatting artifacts from a Cypher query.
289
+
290
+ Args:
291
+ response_text: Raw response from LLM
292
+
293
+ Returns:
294
+ Cleaned Cypher query
295
+ """
296
+ query = response_text.strip()
297
+
298
+ # Remove markdown code blocks
299
+ if query.startswith("```"):
300
+ lines = query.split("\n")
301
+ # Find the actual query content
302
+ start_idx = 0
303
+ end_idx = len(lines)
304
+
305
+ for i, line in enumerate(lines):
306
+ if line.startswith("```") and i == 0:
307
+ start_idx = 1
308
+ elif line.startswith("```") and i > 0:
309
+ end_idx = i
310
+ break
311
+
312
+ query = "\n".join(lines[start_idx:end_idx])
313
+
314
+ # Remove 'cypher' prefix if present
315
+ query = query.strip()
316
+ if query.lower().startswith("cypher"):
317
+ query = query[6:].strip()
318
+
319
+ # Remove backticks
320
+ query = query.replace("`", "")
321
+
322
+ # Ensure it ends with semicolon
323
+ query = query.strip()
324
+ if not query.endswith(";"):
325
+ query += ";"
326
+
327
+ return query
@@ -0,0 +1,152 @@
1
+ """Tree-sitter parser loader for code parsing."""
2
+
3
+ import sys
4
+ from collections.abc import Callable
5
+ from typing import Any
6
+
7
+ from tree_sitter import Language, Parser
8
+
9
+ from shotgun.codebase.core.language_config import LANGUAGE_CONFIGS
10
+ from shotgun.logging_config import get_logger
11
+
12
+ logger = get_logger(__name__)
13
+
14
+
15
+ def load_parsers() -> tuple[dict[str, Parser], dict[str, Any]]:
16
+ """Load available Tree-sitter parsers and compile their queries.
17
+
18
+ Returns:
19
+ Tuple of (parsers dict, queries dict)
20
+ """
21
+ parsers: dict[str, Parser] = {}
22
+ queries: dict[str, Any] = {}
23
+ available_languages = []
24
+
25
+ # Try to import available language libraries
26
+ language_loaders: dict[str, Callable[[], Any]] = {}
27
+
28
+ # Try individual language imports first
29
+ try:
30
+ import tree_sitter_python
31
+
32
+ language_loaders["python"] = lambda: tree_sitter_python.language()
33
+ available_languages.append("python")
34
+ except ImportError as e:
35
+ logger.warning(f"Failed to import tree_sitter_python: {e}")
36
+
37
+ try:
38
+ import tree_sitter_javascript
39
+
40
+ language_loaders["javascript"] = lambda: tree_sitter_javascript.language()
41
+ available_languages.append("javascript")
42
+ except ImportError as e:
43
+ logger.warning(f"Failed to import tree_sitter_javascript: {e}")
44
+
45
+ try:
46
+ import tree_sitter_typescript
47
+
48
+ language_loaders["typescript"] = (
49
+ lambda: tree_sitter_typescript.language_typescript()
50
+ )
51
+ available_languages.append("typescript")
52
+ except ImportError as e:
53
+ logger.warning(f"Failed to import tree_sitter_typescript: {e}")
54
+
55
+ try:
56
+ import tree_sitter_go
57
+
58
+ language_loaders["go"] = lambda: tree_sitter_go.language()
59
+ available_languages.append("go")
60
+ except ImportError as e:
61
+ logger.warning(f"Failed to import tree_sitter_go: {e}")
62
+
63
+ try:
64
+ import tree_sitter_rust
65
+
66
+ language_loaders["rust"] = lambda: tree_sitter_rust.language()
67
+ available_languages.append("rust")
68
+ except ImportError as e:
69
+ logger.warning(f"Failed to import tree_sitter_rust: {e}")
70
+
71
+ # If no individual imports worked, try tree_sitter_languages
72
+ if not available_languages:
73
+ try:
74
+ import tree_sitter_languages # type: ignore[import-untyped]
75
+
76
+ # Get available languages from tree_sitter_languages
77
+ for lang_name in [
78
+ "python",
79
+ "javascript",
80
+ "typescript",
81
+ "go",
82
+ "rust",
83
+ "java",
84
+ "cpp",
85
+ ]:
86
+ try:
87
+ lang = tree_sitter_languages.get_language(lang_name)
88
+ language_loaders[lang_name] = lambda lang=lang: lang # type: ignore[misc]
89
+ available_languages.append(lang_name)
90
+ except Exception as e:
91
+ logger.debug(f"Failed to load {lang_name} parser: {e}")
92
+ except ImportError:
93
+ logger.warning(
94
+ "No tree-sitter language libraries found. Install tree-sitter-languages or individual language packages."
95
+ )
96
+
97
+ logger.info(f"Available languages: {', '.join(available_languages)}")
98
+
99
+ # Create parsers for available languages
100
+ for lang_name, lang_loader in language_loaders.items():
101
+ if lang_name in LANGUAGE_CONFIGS:
102
+ try:
103
+ parser = Parser()
104
+ # Handle both function and direct language object
105
+ if callable(lang_loader):
106
+ lang_obj = lang_loader()
107
+ else:
108
+ lang_obj = lang_loader
109
+
110
+ # Create Language object if needed
111
+ if not isinstance(lang_obj, Language):
112
+ lang_obj = Language(lang_obj)
113
+
114
+ parser.language = lang_obj
115
+ parsers[lang_name] = parser
116
+
117
+ # Compile queries for this language
118
+ config = LANGUAGE_CONFIGS[lang_name]
119
+ lang_queries = {}
120
+
121
+ # Compile each query type
122
+ for query_type in [
123
+ "function_query",
124
+ "class_query",
125
+ "call_query",
126
+ "import_query",
127
+ ]:
128
+ query_text = getattr(config, query_type)
129
+ if query_text:
130
+ try:
131
+ lang_queries[query_type] = lang_obj.query(query_text)
132
+ except Exception as e:
133
+ logger.debug(
134
+ f"Failed to compile {query_type} for {lang_name}: {e}"
135
+ )
136
+
137
+ if lang_queries:
138
+ queries[lang_name] = lang_queries
139
+
140
+ logger.debug(f"Loaded parser for {lang_name}")
141
+
142
+ except Exception as e:
143
+ logger.error(f"Failed to load parser for {lang_name}: {e}")
144
+
145
+ if not parsers:
146
+ logger.error(
147
+ "No parsers could be loaded. Please install tree-sitter-languages or language-specific packages."
148
+ )
149
+ logger.error("Install with: pip install tree-sitter-languages")
150
+ sys.exit(1)
151
+
152
+ return parsers, queries
@@ -0,0 +1,107 @@
1
+ """Data models for codebase service."""
2
+
3
+ from enum import Enum
4
+ from typing import Any
5
+
6
+ from pydantic import BaseModel, Field
7
+
8
+
9
+ class GraphStatus(str, Enum):
10
+ """Status of a code knowledge graph."""
11
+
12
+ READY = "READY" # Graph is ready for queries
13
+ BUILDING = "BUILDING" # Initial build in progress
14
+ UPDATING = "UPDATING" # Update in progress
15
+ ERROR = "ERROR" # Last operation failed
16
+
17
+
18
+ class QueryType(str, Enum):
19
+ """Type of query being executed."""
20
+
21
+ NATURAL_LANGUAGE = "natural_language"
22
+ CYPHER = "cypher"
23
+
24
+
25
+ class OperationStats(BaseModel):
26
+ """Statistics for a graph operation (build/update)."""
27
+
28
+ operation_type: str = Field(..., description="Type of operation: build or update")
29
+ started_at: float = Field(..., description="Unix timestamp when operation started")
30
+ completed_at: float | None = Field(
31
+ None, description="Unix timestamp when operation completed"
32
+ )
33
+ success: bool = Field(default=False, description="Whether operation succeeded")
34
+ error: str | None = Field(None, description="Error message if operation failed")
35
+ stats: dict[str, Any] = Field(
36
+ default_factory=dict, description="Operation statistics"
37
+ )
38
+
39
+
40
+ class CodebaseGraph(BaseModel):
41
+ """Represents a code knowledge graph."""
42
+
43
+ graph_id: str = Field(..., description="Unique graph ID (hash of repo path)")
44
+ repo_path: str = Field(..., description="Absolute path to repository")
45
+ graph_path: str = Field(..., description="Path to Kuzu database")
46
+ name: str = Field(..., description="Human-readable name for the graph")
47
+ created_at: float = Field(..., description="Unix timestamp of creation")
48
+ updated_at: float = Field(..., description="Unix timestamp of last update")
49
+ schema_version: str = Field(default="1.0.0", description="Graph schema version")
50
+ build_options: dict[str, Any] = Field(
51
+ default_factory=dict, description="Build configuration"
52
+ )
53
+ language_stats: dict[str, int] = Field(
54
+ default_factory=dict, description="File count by language"
55
+ )
56
+ node_count: int = Field(default=0, description="Total number of nodes")
57
+ relationship_count: int = Field(
58
+ default=0, description="Total number of relationships"
59
+ )
60
+ node_stats: dict[str, int] = Field(
61
+ default_factory=dict, description="Node counts by type"
62
+ )
63
+ relationship_stats: dict[str, int] = Field(
64
+ default_factory=dict, description="Relationship counts by type"
65
+ )
66
+ is_watching: bool = Field(default=False, description="Whether watcher is active")
67
+ status: GraphStatus = Field(
68
+ default=GraphStatus.READY, description="Current status of the graph"
69
+ )
70
+ last_operation: OperationStats | None = Field(
71
+ None, description="Statistics from the last operation"
72
+ )
73
+ current_operation_id: str | None = Field(
74
+ None, description="ID of current in-progress operation"
75
+ )
76
+
77
+
78
+ class QueryResult(BaseModel):
79
+ """Result of a Cypher query execution."""
80
+
81
+ query: str = Field(..., description="Original query (natural language or Cypher)")
82
+ cypher_query: str | None = Field(
83
+ None, description="Generated Cypher query if from natural language"
84
+ )
85
+ results: list[dict[str, Any]] = Field(
86
+ default_factory=list, description="Query results"
87
+ )
88
+ column_names: list[str] = Field(
89
+ default_factory=list, description="Result column names"
90
+ )
91
+ row_count: int = Field(default=0, description="Number of result rows")
92
+ execution_time_ms: float = Field(
93
+ ..., description="Query execution time in milliseconds"
94
+ )
95
+ success: bool = Field(default=True, description="Whether query succeeded")
96
+ error: str | None = Field(None, description="Error message if failed")
97
+
98
+
99
+ class FileChange(BaseModel):
100
+ """Represents a file system change."""
101
+
102
+ event_type: str = Field(
103
+ ..., description="Type of change: created, modified, deleted, moved"
104
+ )
105
+ src_path: str = Field(..., description="Source file path")
106
+ dest_path: str | None = Field(None, description="Destination path for moves")
107
+ is_directory: bool = Field(default=False, description="Whether path is a directory")