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.
- examples/__init__.py +1 -0
- examples/codegen/__init__.py +5 -0
- examples/codegen/models/__init__.py +13 -0
- examples/codegen/models/schemas.py +76 -0
- examples/codegen/tests/__init__.py +1 -0
- examples/codegen/tests/test_ai_helpers.py +235 -0
- examples/codegen/tests/test_ast_analysis.py +174 -0
- examples/codegen/tests/test_code_analysis.py +134 -0
- examples/codegen/tests/test_code_context.py +301 -0
- examples/codegen/tests/test_code_nav.py +89 -0
- examples/codegen/tests/test_dependency_tools.py +119 -0
- examples/codegen/tests/test_example_tools.py +185 -0
- examples/codegen/tests/test_git_tools.py +112 -0
- examples/codegen/tests/test_impl_agent_schemas.py +193 -0
- examples/codegen/tests/test_impl_agent_v4_graph.py +94 -0
- examples/codegen/tests/test_jedi_analysis.py +226 -0
- examples/codegen/tests/test_meta_tools.py +250 -0
- examples/codegen/tests/test_plan_discovery_prompt.py +98 -0
- examples/codegen/tests/test_syntax_tools.py +85 -0
- examples/codegen/tests/test_synthesize_prompt.py +94 -0
- examples/codegen/tests/test_template_tools.py +244 -0
- examples/codegen/tools/__init__.py +80 -0
- examples/codegen/tools/ai_helpers.py +420 -0
- examples/codegen/tools/ast_analysis.py +92 -0
- examples/codegen/tools/code_context.py +180 -0
- examples/codegen/tools/code_nav.py +52 -0
- examples/codegen/tools/dependency_tools.py +120 -0
- examples/codegen/tools/example_tools.py +188 -0
- examples/codegen/tools/git_tools.py +151 -0
- examples/codegen/tools/impl_executor.py +614 -0
- examples/codegen/tools/jedi_analysis.py +311 -0
- examples/codegen/tools/meta_tools.py +202 -0
- examples/codegen/tools/syntax_tools.py +26 -0
- examples/codegen/tools/template_tools.py +356 -0
- examples/fastapi_interview.py +167 -0
- examples/npc/api/__init__.py +1 -0
- examples/npc/api/app.py +100 -0
- examples/npc/api/routes/__init__.py +5 -0
- examples/npc/api/routes/encounter.py +182 -0
- examples/npc/api/session.py +330 -0
- examples/npc/demo.py +387 -0
- examples/npc/nodes/__init__.py +5 -0
- examples/npc/nodes/image_node.py +92 -0
- examples/npc/run_encounter.py +230 -0
- examples/shared/__init__.py +0 -0
- examples/shared/replicate_tool.py +238 -0
- examples/storyboard/__init__.py +1 -0
- examples/storyboard/generate_videos.py +335 -0
- examples/storyboard/nodes/__init__.py +12 -0
- examples/storyboard/nodes/animated_character_node.py +248 -0
- examples/storyboard/nodes/animated_image_node.py +138 -0
- examples/storyboard/nodes/character_node.py +162 -0
- examples/storyboard/nodes/image_node.py +118 -0
- examples/storyboard/nodes/replicate_tool.py +49 -0
- examples/storyboard/retry_images.py +118 -0
- scripts/demo_async_executor.py +212 -0
- scripts/demo_interview_e2e.py +200 -0
- scripts/demo_streaming.py +140 -0
- scripts/run_interview_demo.py +94 -0
- scripts/test_interrupt_fix.py +26 -0
- tests/__init__.py +1 -0
- tests/conftest.py +178 -0
- tests/integration/__init__.py +1 -0
- tests/integration/test_animated_storyboard.py +63 -0
- tests/integration/test_cli_commands.py +242 -0
- tests/integration/test_colocated_prompts.py +139 -0
- tests/integration/test_map_demo.py +50 -0
- tests/integration/test_memory_demo.py +283 -0
- tests/integration/test_npc_api/__init__.py +1 -0
- tests/integration/test_npc_api/test_routes.py +357 -0
- tests/integration/test_npc_api/test_session.py +216 -0
- tests/integration/test_pipeline_flow.py +105 -0
- tests/integration/test_providers.py +163 -0
- tests/integration/test_resume.py +75 -0
- tests/integration/test_subgraph_integration.py +295 -0
- tests/integration/test_subgraph_interrupt.py +106 -0
- tests/unit/__init__.py +1 -0
- tests/unit/test_agent_nodes.py +355 -0
- tests/unit/test_async_executor.py +346 -0
- tests/unit/test_checkpointer.py +212 -0
- tests/unit/test_checkpointer_factory.py +212 -0
- tests/unit/test_cli.py +121 -0
- tests/unit/test_cli_package.py +81 -0
- tests/unit/test_compile_graph_map.py +132 -0
- tests/unit/test_conditions_routing.py +253 -0
- tests/unit/test_config.py +93 -0
- tests/unit/test_conversation_memory.py +276 -0
- tests/unit/test_database.py +145 -0
- tests/unit/test_deprecation.py +104 -0
- tests/unit/test_executor.py +172 -0
- tests/unit/test_executor_async.py +179 -0
- tests/unit/test_export.py +149 -0
- tests/unit/test_expressions.py +178 -0
- tests/unit/test_feature_brainstorm.py +194 -0
- tests/unit/test_format_prompt.py +145 -0
- tests/unit/test_generic_report.py +200 -0
- tests/unit/test_graph_commands.py +327 -0
- tests/unit/test_graph_linter.py +627 -0
- tests/unit/test_graph_loader.py +357 -0
- tests/unit/test_graph_schema.py +193 -0
- tests/unit/test_inline_schema.py +151 -0
- tests/unit/test_interrupt_node.py +182 -0
- tests/unit/test_issues.py +164 -0
- tests/unit/test_jinja2_prompts.py +85 -0
- tests/unit/test_json_extract.py +134 -0
- tests/unit/test_langsmith.py +600 -0
- tests/unit/test_langsmith_tools.py +204 -0
- tests/unit/test_llm_factory.py +109 -0
- tests/unit/test_llm_factory_async.py +118 -0
- tests/unit/test_loops.py +403 -0
- tests/unit/test_map_node.py +144 -0
- tests/unit/test_no_backward_compat.py +56 -0
- tests/unit/test_node_factory.py +348 -0
- tests/unit/test_passthrough_node.py +126 -0
- tests/unit/test_prompts.py +324 -0
- tests/unit/test_python_nodes.py +198 -0
- tests/unit/test_reliability.py +298 -0
- tests/unit/test_result_export.py +234 -0
- tests/unit/test_router.py +296 -0
- tests/unit/test_sanitize.py +99 -0
- tests/unit/test_schema_loader.py +295 -0
- tests/unit/test_shell_tools.py +229 -0
- tests/unit/test_state_builder.py +331 -0
- tests/unit/test_state_builder_map.py +104 -0
- tests/unit/test_state_config.py +197 -0
- tests/unit/test_streaming.py +307 -0
- tests/unit/test_subgraph.py +596 -0
- tests/unit/test_template.py +190 -0
- tests/unit/test_tool_call_integration.py +164 -0
- tests/unit/test_tool_call_node.py +178 -0
- tests/unit/test_tool_nodes.py +129 -0
- tests/unit/test_websearch.py +234 -0
- yamlgraph/__init__.py +35 -0
- yamlgraph/builder.py +110 -0
- yamlgraph/cli/__init__.py +159 -0
- yamlgraph/cli/__main__.py +6 -0
- yamlgraph/cli/commands.py +231 -0
- yamlgraph/cli/deprecation.py +92 -0
- yamlgraph/cli/graph_commands.py +541 -0
- yamlgraph/cli/validators.py +37 -0
- yamlgraph/config.py +67 -0
- yamlgraph/constants.py +70 -0
- yamlgraph/error_handlers.py +227 -0
- yamlgraph/executor.py +290 -0
- yamlgraph/executor_async.py +288 -0
- yamlgraph/graph_loader.py +451 -0
- yamlgraph/map_compiler.py +150 -0
- yamlgraph/models/__init__.py +36 -0
- yamlgraph/models/graph_schema.py +181 -0
- yamlgraph/models/schemas.py +124 -0
- yamlgraph/models/state_builder.py +236 -0
- yamlgraph/node_factory.py +768 -0
- yamlgraph/routing.py +87 -0
- yamlgraph/schema_loader.py +240 -0
- yamlgraph/storage/__init__.py +20 -0
- yamlgraph/storage/checkpointer.py +72 -0
- yamlgraph/storage/checkpointer_factory.py +123 -0
- yamlgraph/storage/database.py +320 -0
- yamlgraph/storage/export.py +269 -0
- yamlgraph/tools/__init__.py +1 -0
- yamlgraph/tools/agent.py +320 -0
- yamlgraph/tools/graph_linter.py +388 -0
- yamlgraph/tools/langsmith_tools.py +125 -0
- yamlgraph/tools/nodes.py +126 -0
- yamlgraph/tools/python_tool.py +179 -0
- yamlgraph/tools/shell.py +205 -0
- yamlgraph/tools/websearch.py +242 -0
- yamlgraph/utils/__init__.py +48 -0
- yamlgraph/utils/conditions.py +157 -0
- yamlgraph/utils/expressions.py +245 -0
- yamlgraph/utils/json_extract.py +104 -0
- yamlgraph/utils/langsmith.py +416 -0
- yamlgraph/utils/llm_factory.py +118 -0
- yamlgraph/utils/llm_factory_async.py +105 -0
- yamlgraph/utils/logging.py +104 -0
- yamlgraph/utils/prompts.py +171 -0
- yamlgraph/utils/sanitize.py +98 -0
- yamlgraph/utils/template.py +102 -0
- yamlgraph/utils/validators.py +181 -0
- yamlgraph-0.3.9.dist-info/METADATA +1105 -0
- yamlgraph-0.3.9.dist-info/RECORD +185 -0
- yamlgraph-0.3.9.dist-info/WHEEL +5 -0
- yamlgraph-0.3.9.dist-info/entry_points.txt +2 -0
- yamlgraph-0.3.9.dist-info/licenses/LICENSE +33 -0
- 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}
|