yamlgraph 0.3.9__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.
Files changed (185) hide show
  1. examples/__init__.py +1 -0
  2. examples/codegen/__init__.py +5 -0
  3. examples/codegen/models/__init__.py +13 -0
  4. examples/codegen/models/schemas.py +76 -0
  5. examples/codegen/tests/__init__.py +1 -0
  6. examples/codegen/tests/test_ai_helpers.py +235 -0
  7. examples/codegen/tests/test_ast_analysis.py +174 -0
  8. examples/codegen/tests/test_code_analysis.py +134 -0
  9. examples/codegen/tests/test_code_context.py +301 -0
  10. examples/codegen/tests/test_code_nav.py +89 -0
  11. examples/codegen/tests/test_dependency_tools.py +119 -0
  12. examples/codegen/tests/test_example_tools.py +185 -0
  13. examples/codegen/tests/test_git_tools.py +112 -0
  14. examples/codegen/tests/test_impl_agent_schemas.py +193 -0
  15. examples/codegen/tests/test_impl_agent_v4_graph.py +94 -0
  16. examples/codegen/tests/test_jedi_analysis.py +226 -0
  17. examples/codegen/tests/test_meta_tools.py +250 -0
  18. examples/codegen/tests/test_plan_discovery_prompt.py +98 -0
  19. examples/codegen/tests/test_syntax_tools.py +85 -0
  20. examples/codegen/tests/test_synthesize_prompt.py +94 -0
  21. examples/codegen/tests/test_template_tools.py +244 -0
  22. examples/codegen/tools/__init__.py +80 -0
  23. examples/codegen/tools/ai_helpers.py +420 -0
  24. examples/codegen/tools/ast_analysis.py +92 -0
  25. examples/codegen/tools/code_context.py +180 -0
  26. examples/codegen/tools/code_nav.py +52 -0
  27. examples/codegen/tools/dependency_tools.py +120 -0
  28. examples/codegen/tools/example_tools.py +188 -0
  29. examples/codegen/tools/git_tools.py +151 -0
  30. examples/codegen/tools/impl_executor.py +614 -0
  31. examples/codegen/tools/jedi_analysis.py +311 -0
  32. examples/codegen/tools/meta_tools.py +202 -0
  33. examples/codegen/tools/syntax_tools.py +26 -0
  34. examples/codegen/tools/template_tools.py +356 -0
  35. examples/fastapi_interview.py +167 -0
  36. examples/npc/api/__init__.py +1 -0
  37. examples/npc/api/app.py +100 -0
  38. examples/npc/api/routes/__init__.py +5 -0
  39. examples/npc/api/routes/encounter.py +182 -0
  40. examples/npc/api/session.py +330 -0
  41. examples/npc/demo.py +387 -0
  42. examples/npc/nodes/__init__.py +5 -0
  43. examples/npc/nodes/image_node.py +92 -0
  44. examples/npc/run_encounter.py +230 -0
  45. examples/shared/__init__.py +0 -0
  46. examples/shared/replicate_tool.py +238 -0
  47. examples/storyboard/__init__.py +1 -0
  48. examples/storyboard/generate_videos.py +335 -0
  49. examples/storyboard/nodes/__init__.py +12 -0
  50. examples/storyboard/nodes/animated_character_node.py +248 -0
  51. examples/storyboard/nodes/animated_image_node.py +138 -0
  52. examples/storyboard/nodes/character_node.py +162 -0
  53. examples/storyboard/nodes/image_node.py +118 -0
  54. examples/storyboard/nodes/replicate_tool.py +49 -0
  55. examples/storyboard/retry_images.py +118 -0
  56. scripts/demo_async_executor.py +212 -0
  57. scripts/demo_interview_e2e.py +200 -0
  58. scripts/demo_streaming.py +140 -0
  59. scripts/run_interview_demo.py +94 -0
  60. scripts/test_interrupt_fix.py +26 -0
  61. tests/__init__.py +1 -0
  62. tests/conftest.py +178 -0
  63. tests/integration/__init__.py +1 -0
  64. tests/integration/test_animated_storyboard.py +63 -0
  65. tests/integration/test_cli_commands.py +242 -0
  66. tests/integration/test_colocated_prompts.py +139 -0
  67. tests/integration/test_map_demo.py +50 -0
  68. tests/integration/test_memory_demo.py +283 -0
  69. tests/integration/test_npc_api/__init__.py +1 -0
  70. tests/integration/test_npc_api/test_routes.py +357 -0
  71. tests/integration/test_npc_api/test_session.py +216 -0
  72. tests/integration/test_pipeline_flow.py +105 -0
  73. tests/integration/test_providers.py +163 -0
  74. tests/integration/test_resume.py +75 -0
  75. tests/integration/test_subgraph_integration.py +295 -0
  76. tests/integration/test_subgraph_interrupt.py +106 -0
  77. tests/unit/__init__.py +1 -0
  78. tests/unit/test_agent_nodes.py +355 -0
  79. tests/unit/test_async_executor.py +346 -0
  80. tests/unit/test_checkpointer.py +212 -0
  81. tests/unit/test_checkpointer_factory.py +212 -0
  82. tests/unit/test_cli.py +121 -0
  83. tests/unit/test_cli_package.py +81 -0
  84. tests/unit/test_compile_graph_map.py +132 -0
  85. tests/unit/test_conditions_routing.py +253 -0
  86. tests/unit/test_config.py +93 -0
  87. tests/unit/test_conversation_memory.py +276 -0
  88. tests/unit/test_database.py +145 -0
  89. tests/unit/test_deprecation.py +104 -0
  90. tests/unit/test_executor.py +172 -0
  91. tests/unit/test_executor_async.py +179 -0
  92. tests/unit/test_export.py +149 -0
  93. tests/unit/test_expressions.py +178 -0
  94. tests/unit/test_feature_brainstorm.py +194 -0
  95. tests/unit/test_format_prompt.py +145 -0
  96. tests/unit/test_generic_report.py +200 -0
  97. tests/unit/test_graph_commands.py +327 -0
  98. tests/unit/test_graph_linter.py +627 -0
  99. tests/unit/test_graph_loader.py +357 -0
  100. tests/unit/test_graph_schema.py +193 -0
  101. tests/unit/test_inline_schema.py +151 -0
  102. tests/unit/test_interrupt_node.py +182 -0
  103. tests/unit/test_issues.py +164 -0
  104. tests/unit/test_jinja2_prompts.py +85 -0
  105. tests/unit/test_json_extract.py +134 -0
  106. tests/unit/test_langsmith.py +600 -0
  107. tests/unit/test_langsmith_tools.py +204 -0
  108. tests/unit/test_llm_factory.py +109 -0
  109. tests/unit/test_llm_factory_async.py +118 -0
  110. tests/unit/test_loops.py +403 -0
  111. tests/unit/test_map_node.py +144 -0
  112. tests/unit/test_no_backward_compat.py +56 -0
  113. tests/unit/test_node_factory.py +348 -0
  114. tests/unit/test_passthrough_node.py +126 -0
  115. tests/unit/test_prompts.py +324 -0
  116. tests/unit/test_python_nodes.py +198 -0
  117. tests/unit/test_reliability.py +298 -0
  118. tests/unit/test_result_export.py +234 -0
  119. tests/unit/test_router.py +296 -0
  120. tests/unit/test_sanitize.py +99 -0
  121. tests/unit/test_schema_loader.py +295 -0
  122. tests/unit/test_shell_tools.py +229 -0
  123. tests/unit/test_state_builder.py +331 -0
  124. tests/unit/test_state_builder_map.py +104 -0
  125. tests/unit/test_state_config.py +197 -0
  126. tests/unit/test_streaming.py +307 -0
  127. tests/unit/test_subgraph.py +596 -0
  128. tests/unit/test_template.py +190 -0
  129. tests/unit/test_tool_call_integration.py +164 -0
  130. tests/unit/test_tool_call_node.py +178 -0
  131. tests/unit/test_tool_nodes.py +129 -0
  132. tests/unit/test_websearch.py +234 -0
  133. yamlgraph/__init__.py +35 -0
  134. yamlgraph/builder.py +110 -0
  135. yamlgraph/cli/__init__.py +159 -0
  136. yamlgraph/cli/__main__.py +6 -0
  137. yamlgraph/cli/commands.py +231 -0
  138. yamlgraph/cli/deprecation.py +92 -0
  139. yamlgraph/cli/graph_commands.py +541 -0
  140. yamlgraph/cli/validators.py +37 -0
  141. yamlgraph/config.py +67 -0
  142. yamlgraph/constants.py +70 -0
  143. yamlgraph/error_handlers.py +227 -0
  144. yamlgraph/executor.py +290 -0
  145. yamlgraph/executor_async.py +288 -0
  146. yamlgraph/graph_loader.py +451 -0
  147. yamlgraph/map_compiler.py +150 -0
  148. yamlgraph/models/__init__.py +36 -0
  149. yamlgraph/models/graph_schema.py +181 -0
  150. yamlgraph/models/schemas.py +124 -0
  151. yamlgraph/models/state_builder.py +236 -0
  152. yamlgraph/node_factory.py +768 -0
  153. yamlgraph/routing.py +87 -0
  154. yamlgraph/schema_loader.py +240 -0
  155. yamlgraph/storage/__init__.py +20 -0
  156. yamlgraph/storage/checkpointer.py +72 -0
  157. yamlgraph/storage/checkpointer_factory.py +123 -0
  158. yamlgraph/storage/database.py +320 -0
  159. yamlgraph/storage/export.py +269 -0
  160. yamlgraph/tools/__init__.py +1 -0
  161. yamlgraph/tools/agent.py +320 -0
  162. yamlgraph/tools/graph_linter.py +388 -0
  163. yamlgraph/tools/langsmith_tools.py +125 -0
  164. yamlgraph/tools/nodes.py +126 -0
  165. yamlgraph/tools/python_tool.py +179 -0
  166. yamlgraph/tools/shell.py +205 -0
  167. yamlgraph/tools/websearch.py +242 -0
  168. yamlgraph/utils/__init__.py +48 -0
  169. yamlgraph/utils/conditions.py +157 -0
  170. yamlgraph/utils/expressions.py +245 -0
  171. yamlgraph/utils/json_extract.py +104 -0
  172. yamlgraph/utils/langsmith.py +416 -0
  173. yamlgraph/utils/llm_factory.py +118 -0
  174. yamlgraph/utils/llm_factory_async.py +105 -0
  175. yamlgraph/utils/logging.py +104 -0
  176. yamlgraph/utils/prompts.py +171 -0
  177. yamlgraph/utils/sanitize.py +98 -0
  178. yamlgraph/utils/template.py +102 -0
  179. yamlgraph/utils/validators.py +181 -0
  180. yamlgraph-0.3.9.dist-info/METADATA +1105 -0
  181. yamlgraph-0.3.9.dist-info/RECORD +185 -0
  182. yamlgraph-0.3.9.dist-info/WHEEL +5 -0
  183. yamlgraph-0.3.9.dist-info/entry_points.txt +2 -0
  184. yamlgraph-0.3.9.dist-info/licenses/LICENSE +33 -0
  185. yamlgraph-0.3.9.dist-info/top_level.txt +4 -0
@@ -0,0 +1,311 @@
1
+ """Jedi-based code analysis tools for cross-file reference tracking.
2
+
3
+ Provides semantic analysis capabilities using jedi:
4
+ - find_references: All usages of a symbol across project
5
+ - get_callers: Functions that call a given function
6
+ - get_callees: Functions called by a given function
7
+
8
+ Requires: pip install jedi (optional dependency)
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import ast
14
+ import logging
15
+ from pathlib import Path
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+ # Try to import jedi - graceful degradation if not available
20
+ try:
21
+ import jedi
22
+
23
+ JEDI_AVAILABLE = True
24
+ except ImportError:
25
+ jedi = None # type: ignore[assignment]
26
+ JEDI_AVAILABLE = False
27
+
28
+
29
+ def find_references(
30
+ file_path: str,
31
+ symbol_name: str,
32
+ line: int,
33
+ column: int = 0,
34
+ project_path: str | None = None,
35
+ ) -> list[dict] | dict:
36
+ """Find all references to a symbol across the project.
37
+
38
+ Args:
39
+ file_path: Path to file containing the symbol definition
40
+ symbol_name: Name of the symbol to find references for
41
+ line: Line number where symbol is defined (1-indexed)
42
+ column: Column offset (default: 0)
43
+ project_path: Root directory for cross-file analysis
44
+
45
+ Returns:
46
+ List of reference dicts with file, line, column, type.
47
+ Or error dict if file not found or jedi unavailable.
48
+ """
49
+ # Validate line argument - handle placeholder strings like 'TBD' or '<dynamic>'
50
+ try:
51
+ line = int(line)
52
+ except (ValueError, TypeError):
53
+ return {
54
+ "error": f"Invalid line number: {line!r}. "
55
+ "Use get_structure first to get actual line numbers."
56
+ }
57
+
58
+ if line <= 0:
59
+ return {
60
+ "error": f"Invalid line number: {line}. "
61
+ "Line numbers must be >= 1. Use get_structure first."
62
+ }
63
+
64
+ if not JEDI_AVAILABLE:
65
+ return {"error": "jedi not installed. Run: pip install jedi"}
66
+
67
+ path = Path(file_path)
68
+ if not path.exists():
69
+ return {"error": f"File not found: {file_path}"}
70
+
71
+ try:
72
+ source = path.read_text()
73
+
74
+ # Create jedi project for cross-file analysis
75
+ project = None
76
+ if project_path:
77
+ project = jedi.Project(path=project_path)
78
+
79
+ script = jedi.Script(source, path=path, project=project)
80
+
81
+ # Find the position of the symbol on the given line
82
+ # Try to find the symbol in the line to get correct column
83
+ lines = source.splitlines()
84
+ if 0 < line <= len(lines):
85
+ line_text = lines[line - 1]
86
+ if symbol_name in line_text:
87
+ column = line_text.find(symbol_name)
88
+
89
+ # Get references
90
+ references = script.get_references(line=line, column=column)
91
+
92
+ results = []
93
+ for ref in references:
94
+ ref_type = "usage"
95
+ if ref.is_definition():
96
+ ref_type = "definition"
97
+
98
+ results.append(
99
+ {
100
+ "file": str(ref.module_path) if ref.module_path else file_path,
101
+ "line": ref.line,
102
+ "column": ref.column,
103
+ "type": ref_type,
104
+ "name": ref.name,
105
+ }
106
+ )
107
+
108
+ return results
109
+
110
+ except Exception as e:
111
+ logger.warning(f"jedi analysis failed: {e}")
112
+ return {"error": str(e)}
113
+
114
+
115
+ def get_callers(
116
+ file_path: str,
117
+ function_name: str,
118
+ line: int,
119
+ project_path: str | None = None,
120
+ ) -> list[dict] | dict:
121
+ """Find all functions that call a given function.
122
+
123
+ Args:
124
+ file_path: Path to file containing the function
125
+ function_name: Name of the function to find callers for
126
+ line: Line number where function is defined (1-indexed)
127
+ project_path: Root directory for cross-file analysis
128
+
129
+ Returns:
130
+ List of caller dicts with file, line, caller name.
131
+ Or error dict if file not found.
132
+ """
133
+ # Validate line argument - handle placeholder strings like 'TBD' or '<dynamic>'
134
+ try:
135
+ line = int(line)
136
+ except (ValueError, TypeError):
137
+ return {
138
+ "error": f"Invalid line number: {line!r}. "
139
+ "Use get_structure first to get actual line numbers."
140
+ }
141
+
142
+ if line <= 0:
143
+ return {
144
+ "error": f"Invalid line number: {line}. "
145
+ "Line numbers must be >= 1. Use get_structure first."
146
+ }
147
+
148
+ if not JEDI_AVAILABLE:
149
+ return {"error": "jedi not installed. Run: pip install jedi"}
150
+
151
+ path = Path(file_path)
152
+ if not path.exists():
153
+ return {"error": f"File not found: {file_path}"}
154
+
155
+ try:
156
+ # Get all references to the function
157
+ refs = find_references(
158
+ file_path, function_name, line, project_path=project_path
159
+ )
160
+
161
+ if isinstance(refs, dict) and "error" in refs:
162
+ return refs
163
+
164
+ # Filter to only usages (not definitions)
165
+ callers = []
166
+ for ref in refs:
167
+ if ref.get("type") == "usage":
168
+ # Try to find which function contains this call
169
+ caller_info = _find_enclosing_function(
170
+ ref.get("file", file_path),
171
+ ref.get("line", 0),
172
+ )
173
+ if caller_info:
174
+ callers.append(
175
+ {
176
+ "file": ref.get("file"),
177
+ "line": ref.get("line"),
178
+ "caller": caller_info.get("name"),
179
+ "caller_line": caller_info.get("line"),
180
+ }
181
+ )
182
+
183
+ return callers
184
+
185
+ except Exception as e:
186
+ logger.warning(f"get_callers failed: {e}")
187
+ return {"error": str(e)}
188
+
189
+
190
+ def get_callees(
191
+ file_path: str,
192
+ function_name: str,
193
+ line: int,
194
+ project_path: str | None = None,
195
+ ) -> list[dict] | dict:
196
+ """Find all functions called by a given function.
197
+
198
+ Args:
199
+ file_path: Path to file containing the function
200
+ function_name: Name of the function to analyze
201
+ line: Line number where function is defined (1-indexed)
202
+ project_path: Root directory for cross-file analysis
203
+
204
+ Returns:
205
+ List of callee dicts with name, line, file.
206
+ Or error dict if file not found.
207
+ """
208
+ # Validate line argument - handle placeholder strings like 'TBD' or '<dynamic>'
209
+ try:
210
+ line = int(line)
211
+ except (ValueError, TypeError):
212
+ return {
213
+ "error": f"Invalid line number: {line!r}. "
214
+ "Use get_structure first to get actual line numbers."
215
+ }
216
+
217
+ if line <= 0:
218
+ return {
219
+ "error": f"Invalid line number: {line}. "
220
+ "Line numbers must be >= 1. Use get_structure first."
221
+ }
222
+
223
+ if not JEDI_AVAILABLE:
224
+ return {"error": "jedi not installed. Run: pip install jedi"}
225
+
226
+ path = Path(file_path)
227
+ if not path.exists():
228
+ return {"error": f"File not found: {file_path}"}
229
+
230
+ try:
231
+ source = path.read_text()
232
+ tree = ast.parse(source)
233
+
234
+ # Find the function AST node
235
+ func_node = None
236
+ for node in ast.walk(tree):
237
+ if (
238
+ isinstance(node, ast.FunctionDef)
239
+ and node.name == function_name
240
+ and (
241
+ node.lineno == line
242
+ or node.lineno <= line <= (node.end_lineno or line)
243
+ )
244
+ ):
245
+ func_node = node
246
+ break
247
+
248
+ if not func_node:
249
+ return []
250
+
251
+ # Find all function calls within the function body
252
+ callees = []
253
+ for node in ast.walk(func_node):
254
+ if isinstance(node, ast.Call):
255
+ callee_name = None
256
+ if isinstance(node.func, ast.Name):
257
+ callee_name = node.func.id
258
+ elif isinstance(node.func, ast.Attribute):
259
+ callee_name = node.func.attr
260
+
261
+ if callee_name:
262
+ callees.append(
263
+ {
264
+ "callee": callee_name,
265
+ "line": node.lineno,
266
+ "file": str(path),
267
+ }
268
+ )
269
+
270
+ return callees
271
+
272
+ except Exception as e:
273
+ logger.warning(f"get_callees failed: {e}")
274
+ return {"error": str(e)}
275
+
276
+
277
+ def _find_enclosing_function(file_path: str, line: int) -> dict | None:
278
+ """Find the function that contains a given line.
279
+
280
+ Args:
281
+ file_path: Path to file
282
+ line: Line number to find enclosing function for
283
+
284
+ Returns:
285
+ Dict with function name and line, or None if not found.
286
+ """
287
+ try:
288
+ path = Path(file_path)
289
+ if not path.exists():
290
+ return None
291
+
292
+ source = path.read_text()
293
+ tree = ast.parse(source)
294
+
295
+ # Find the innermost function containing this line
296
+ best_match = None
297
+ for node in ast.walk(tree):
298
+ if (
299
+ isinstance(node, ast.FunctionDef)
300
+ and node.lineno <= line <= (node.end_lineno or line)
301
+ and (best_match is None or node.lineno > best_match.lineno)
302
+ ):
303
+ best_match = node
304
+
305
+ if best_match:
306
+ return {"name": best_match.name, "line": best_match.lineno}
307
+
308
+ return None
309
+
310
+ except Exception:
311
+ return None
@@ -0,0 +1,202 @@
1
+ """YAMLGraph meta-template tools for implementation agent.
2
+
3
+ Extract patterns from existing graph and prompt YAML files.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import re
9
+ from pathlib import Path
10
+
11
+ import yaml
12
+
13
+
14
+ def extract_graph_template(graph_path: str) -> dict:
15
+ """Extract reusable patterns from a graph YAML file.
16
+
17
+ Args:
18
+ graph_path: Path to the graph YAML file
19
+
20
+ Returns:
21
+ dict with:
22
+ - node_types: List of node types used
23
+ - edge_patterns: List of edge pattern types
24
+ - state_fields: List of state field definitions
25
+ - tool_patterns: List of tool type patterns
26
+ or dict with 'error' key if failed
27
+ """
28
+ path = Path(graph_path)
29
+ if not path.exists():
30
+ return {"error": f"File not found: {graph_path}"}
31
+
32
+ try:
33
+ content = path.read_text()
34
+ data = yaml.safe_load(content)
35
+ except yaml.YAMLError as e:
36
+ return {"error": f"YAML parse error: {e}"}
37
+
38
+ if not isinstance(data, dict):
39
+ return {"error": "Invalid graph format: expected dict"}
40
+
41
+ result = {
42
+ "node_types": [],
43
+ "edge_patterns": [],
44
+ "state_fields": [],
45
+ "tool_patterns": [],
46
+ }
47
+
48
+ # Extract node types
49
+ nodes = data.get("nodes", {})
50
+ node_types_seen = set()
51
+ for _node_name, node_config in nodes.items():
52
+ if isinstance(node_config, dict):
53
+ node_type = node_config.get("type", "unknown")
54
+ node_types_seen.add(node_type)
55
+
56
+ result["node_types"] = sorted(node_types_seen)
57
+
58
+ # Extract edge patterns
59
+ edges = data.get("edges", [])
60
+ has_conditional = False
61
+ has_sequential = False
62
+
63
+ for edge in edges:
64
+ if isinstance(edge, str):
65
+ has_sequential = True
66
+ elif isinstance(edge, dict):
67
+ # Check for conditional edges
68
+ for edge_def in edge.values():
69
+ if isinstance(edge_def, dict) and "condition" in edge_def:
70
+ has_conditional = True
71
+
72
+ # Also check string edges for condition syntax
73
+ for edge in edges:
74
+ if isinstance(edge, str) and ":" in edge and "condition" in str(edges):
75
+ has_conditional = True
76
+
77
+ if has_sequential:
78
+ result["edge_patterns"].append("sequential")
79
+ if has_conditional:
80
+ result["edge_patterns"].append("conditional")
81
+
82
+ # Extract state fields
83
+ state = data.get("state", {})
84
+ if isinstance(state, dict):
85
+ for field_name, field_type in state.items():
86
+ result["state_fields"].append({"name": field_name, "type": str(field_type)})
87
+
88
+ # Extract tool patterns
89
+ tools = data.get("tools", {})
90
+ tool_types_seen = set()
91
+ for _tool_name, tool_config in tools.items():
92
+ if isinstance(tool_config, dict):
93
+ tool_type = tool_config.get("type", "unknown")
94
+ tool_types_seen.add(tool_type)
95
+
96
+ result["tool_patterns"] = sorted(tool_types_seen)
97
+
98
+ return result
99
+
100
+
101
+ def extract_prompt_template(prompt_path: str) -> dict:
102
+ """Extract patterns from a prompt YAML file.
103
+
104
+ Args:
105
+ prompt_path: Path to the prompt YAML file
106
+
107
+ Returns:
108
+ dict with:
109
+ - system_structure: Analysis of system prompt sections
110
+ - variables: List of variable names found
111
+ - schema_patterns: Schema field patterns if present
112
+ - jinja_patterns: Jinja2 constructs used
113
+ or dict with 'error' key if failed
114
+ """
115
+ path = Path(prompt_path)
116
+ if not path.exists():
117
+ return {"error": f"File not found: {prompt_path}"}
118
+
119
+ try:
120
+ content = path.read_text()
121
+ data = yaml.safe_load(content)
122
+ except yaml.YAMLError as e:
123
+ return {"error": f"YAML parse error: {e}"}
124
+
125
+ if not isinstance(data, dict):
126
+ return {"error": "Invalid prompt format: expected dict"}
127
+
128
+ result = {
129
+ "system_structure": {},
130
+ "variables": [],
131
+ "schema_patterns": [],
132
+ "jinja_patterns": [],
133
+ }
134
+
135
+ # Extract system prompt structure
136
+ system = data.get("system", "")
137
+ if system:
138
+ sections = _extract_sections(system)
139
+ result["system_structure"] = {"sections": sections, "length": len(system)}
140
+
141
+ # Extract variables from all text fields
142
+ variables = set()
143
+ for key in ["system", "user"]:
144
+ text = data.get(key, "")
145
+ if text:
146
+ # Find {variable} patterns (simple format)
147
+ simple_vars = re.findall(r"\{(\w+)\}", str(text))
148
+ variables.update(simple_vars)
149
+
150
+ # Find {{ variable }} patterns (Jinja2)
151
+ jinja_vars = re.findall(r"\{\{\s*(\w+)", str(text))
152
+ variables.update(jinja_vars)
153
+
154
+ result["variables"] = sorted(variables)
155
+
156
+ # Extract schema patterns
157
+ schema = data.get("schema", {})
158
+ if isinstance(schema, dict):
159
+ properties = schema.get("properties", {})
160
+ for prop_name, prop_def in properties.items():
161
+ prop_type = (
162
+ prop_def.get("type", "unknown")
163
+ if isinstance(prop_def, dict)
164
+ else "unknown"
165
+ )
166
+ result["schema_patterns"].append({"name": prop_name, "type": prop_type})
167
+
168
+ # Extract Jinja patterns
169
+ jinja_constructs = set()
170
+ full_text = str(data.get("system", "")) + str(data.get("user", ""))
171
+
172
+ if "{%" in full_text:
173
+ if "{% if" in full_text:
174
+ jinja_constructs.add("if")
175
+ if "{% for" in full_text:
176
+ jinja_constructs.add("for")
177
+ if "{% endif" in full_text:
178
+ jinja_constructs.add("endif")
179
+ if "{% endfor" in full_text:
180
+ jinja_constructs.add("endfor")
181
+ if "{% else" in full_text:
182
+ jinja_constructs.add("else")
183
+ if "{% elif" in full_text:
184
+ jinja_constructs.add("elif")
185
+
186
+ result["jinja_patterns"] = sorted(jinja_constructs)
187
+
188
+ return result
189
+
190
+
191
+ def _extract_sections(text: str) -> list[str]:
192
+ """Extract section headers from text (## or ### style)."""
193
+ sections = []
194
+ for line in text.splitlines():
195
+ line = line.strip()
196
+ if line.startswith("##"):
197
+ # Remove ## prefix and clean
198
+ section = line.lstrip("#").strip()
199
+ if section:
200
+ sections.append(section)
201
+
202
+ return sections
@@ -0,0 +1,26 @@
1
+ """Syntax validation tools for implementation agent.
2
+
3
+ Provides syntax checking for proposed code changes.
4
+ """
5
+
6
+ import ast
7
+
8
+
9
+ def syntax_check(code: str) -> dict:
10
+ """Check if Python code is syntactically valid.
11
+
12
+ Args:
13
+ code: Python code string to check
14
+
15
+ Returns:
16
+ dict with 'valid' boolean. If invalid, includes 'error' string
17
+ with line number and description.
18
+ """
19
+ try:
20
+ ast.parse(code)
21
+ return {"valid": True}
22
+ except SyntaxError as e:
23
+ error_msg = f"line {e.lineno}: {e.msg}"
24
+ if e.text:
25
+ error_msg += f" (near: {e.text.strip()[:50]})"
26
+ return {"valid": False, "error": error_msg}