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,356 @@
|
|
|
1
|
+
"""Template extraction tools for implementation agent.
|
|
2
|
+
|
|
3
|
+
Extract reusable templates from existing code for consistent new code generation.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import ast
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import TYPE_CHECKING
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def extract_function_template(file_path: str, function_name: str) -> dict:
|
|
17
|
+
"""Extract reusable function template.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
file_path: Path to the Python file
|
|
21
|
+
function_name: Name of the function to extract
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
dict with 'template' string showing reusable pattern
|
|
25
|
+
or dict with 'error' key if failed
|
|
26
|
+
"""
|
|
27
|
+
path = Path(file_path)
|
|
28
|
+
if not path.exists():
|
|
29
|
+
return {"error": f"File not found: {file_path}"}
|
|
30
|
+
|
|
31
|
+
try:
|
|
32
|
+
source = path.read_text()
|
|
33
|
+
tree = ast.parse(source)
|
|
34
|
+
except SyntaxError as e:
|
|
35
|
+
return {"error": f"Syntax error: {e}"}
|
|
36
|
+
|
|
37
|
+
# Find the function
|
|
38
|
+
target = None
|
|
39
|
+
for node in ast.walk(tree):
|
|
40
|
+
if isinstance(node, ast.FunctionDef) and node.name == function_name:
|
|
41
|
+
target = node
|
|
42
|
+
break
|
|
43
|
+
|
|
44
|
+
if target is None:
|
|
45
|
+
return {"error": f"Function '{function_name}' not found in {file_path}"}
|
|
46
|
+
|
|
47
|
+
# Extract template
|
|
48
|
+
lines = source.splitlines()
|
|
49
|
+
template_parts = []
|
|
50
|
+
|
|
51
|
+
# Build signature line
|
|
52
|
+
sig = _build_signature(target)
|
|
53
|
+
template_parts.append(sig)
|
|
54
|
+
|
|
55
|
+
# Extract docstring
|
|
56
|
+
docstring = ast.get_docstring(target)
|
|
57
|
+
if docstring:
|
|
58
|
+
template_parts.append(f' """{docstring}"""')
|
|
59
|
+
|
|
60
|
+
# Extract body structure
|
|
61
|
+
body_template = _extract_body_structure(target, lines)
|
|
62
|
+
template_parts.append(body_template)
|
|
63
|
+
|
|
64
|
+
return {"template": "\n".join(template_parts)}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _build_signature(func: ast.FunctionDef) -> str:
|
|
68
|
+
"""Build function signature with type hints."""
|
|
69
|
+
args = []
|
|
70
|
+
for arg in func.args.args:
|
|
71
|
+
if arg.annotation:
|
|
72
|
+
ann = _get_annotation(arg.annotation)
|
|
73
|
+
args.append(f"{arg.arg}: {ann}")
|
|
74
|
+
else:
|
|
75
|
+
args.append(arg.arg)
|
|
76
|
+
|
|
77
|
+
# Handle defaults
|
|
78
|
+
defaults = func.args.defaults
|
|
79
|
+
num_defaults = len(defaults)
|
|
80
|
+
if num_defaults > 0:
|
|
81
|
+
for i, default in enumerate(defaults):
|
|
82
|
+
idx = len(args) - num_defaults + i
|
|
83
|
+
default_val = _get_value(default)
|
|
84
|
+
args[idx] = f"{args[idx]} = {default_val}"
|
|
85
|
+
|
|
86
|
+
ret = ""
|
|
87
|
+
if func.returns:
|
|
88
|
+
ret = f" -> {_get_annotation(func.returns)}"
|
|
89
|
+
|
|
90
|
+
return f"def {func.name}({', '.join(args)}){ret}:"
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _get_annotation(node: ast.expr) -> str:
|
|
94
|
+
"""Extract type annotation as string."""
|
|
95
|
+
if isinstance(node, ast.Name):
|
|
96
|
+
return node.id
|
|
97
|
+
elif isinstance(node, ast.Constant):
|
|
98
|
+
return repr(node.value)
|
|
99
|
+
elif isinstance(node, ast.Subscript):
|
|
100
|
+
value = _get_annotation(node.value)
|
|
101
|
+
slice_val = _get_annotation(node.slice)
|
|
102
|
+
return f"{value}[{slice_val}]"
|
|
103
|
+
elif isinstance(node, ast.Attribute):
|
|
104
|
+
return f"{_get_annotation(node.value)}.{node.attr}"
|
|
105
|
+
elif isinstance(node, ast.BinOp) and isinstance(node.op, ast.BitOr):
|
|
106
|
+
left = _get_annotation(node.left)
|
|
107
|
+
right = _get_annotation(node.right)
|
|
108
|
+
return f"{left} | {right}"
|
|
109
|
+
return "..."
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _get_value(node: ast.expr) -> str:
|
|
113
|
+
"""Extract default value as string."""
|
|
114
|
+
if isinstance(node, ast.Constant):
|
|
115
|
+
return repr(node.value)
|
|
116
|
+
elif isinstance(node, ast.Name):
|
|
117
|
+
return node.id
|
|
118
|
+
elif isinstance(node, ast.List):
|
|
119
|
+
return "[]"
|
|
120
|
+
elif isinstance(node, ast.Dict):
|
|
121
|
+
return "{}"
|
|
122
|
+
return "..."
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _extract_body_structure(func: ast.FunctionDef, lines: list[str]) -> str:
|
|
126
|
+
"""Extract body structure as template."""
|
|
127
|
+
body_parts = []
|
|
128
|
+
|
|
129
|
+
for node in func.body:
|
|
130
|
+
# Skip docstring
|
|
131
|
+
if (
|
|
132
|
+
isinstance(node, ast.Expr)
|
|
133
|
+
and isinstance(node.value, ast.Constant)
|
|
134
|
+
and isinstance(node.value.value, str)
|
|
135
|
+
):
|
|
136
|
+
continue
|
|
137
|
+
|
|
138
|
+
if isinstance(node, ast.Try):
|
|
139
|
+
body_parts.append(" try:")
|
|
140
|
+
body_parts.append(" {try_block}")
|
|
141
|
+
for handler in node.handlers:
|
|
142
|
+
exc = _get_annotation(handler.type) if handler.type else "Exception"
|
|
143
|
+
body_parts.append(f" except {exc}:")
|
|
144
|
+
body_parts.append(" {except_block}")
|
|
145
|
+
elif isinstance(node, ast.If):
|
|
146
|
+
body_parts.append(" if {condition}:")
|
|
147
|
+
body_parts.append(" {if_block}")
|
|
148
|
+
elif isinstance(node, ast.For):
|
|
149
|
+
body_parts.append(" for {item} in {iterable}:")
|
|
150
|
+
body_parts.append(" {loop_block}")
|
|
151
|
+
elif isinstance(node, ast.Return):
|
|
152
|
+
if node.value:
|
|
153
|
+
ret_val = _format_return(node.value)
|
|
154
|
+
body_parts.append(f" return {ret_val}")
|
|
155
|
+
else:
|
|
156
|
+
body_parts.append(" return")
|
|
157
|
+
else:
|
|
158
|
+
# Generic statement
|
|
159
|
+
stmt_lines = lines[node.lineno - 1 : node.end_lineno]
|
|
160
|
+
for line in stmt_lines:
|
|
161
|
+
body_parts.append(line)
|
|
162
|
+
|
|
163
|
+
if not body_parts:
|
|
164
|
+
body_parts.append(" pass")
|
|
165
|
+
|
|
166
|
+
return "\n".join(body_parts)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _format_return(node: ast.expr) -> str:
|
|
170
|
+
"""Format return value for template."""
|
|
171
|
+
if isinstance(node, ast.Dict):
|
|
172
|
+
items = []
|
|
173
|
+
for k, v in zip(node.keys, node.values, strict=False):
|
|
174
|
+
key = _get_value(k) if k else "..."
|
|
175
|
+
val = "{result}" if isinstance(v, ast.Name) else _get_value(v)
|
|
176
|
+
items.append(f"{key}: {val}")
|
|
177
|
+
return "{" + ", ".join(items) + "}"
|
|
178
|
+
elif isinstance(node, ast.Call):
|
|
179
|
+
return "{function_call}"
|
|
180
|
+
return "{result}"
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def extract_class_template(file_path: str, class_name: str) -> dict:
|
|
184
|
+
"""Extract reusable class template.
|
|
185
|
+
|
|
186
|
+
Args:
|
|
187
|
+
file_path: Path to the Python file
|
|
188
|
+
class_name: Name of the class to extract
|
|
189
|
+
|
|
190
|
+
Returns:
|
|
191
|
+
dict with 'template' string showing reusable pattern
|
|
192
|
+
or dict with 'error' key if failed
|
|
193
|
+
"""
|
|
194
|
+
path = Path(file_path)
|
|
195
|
+
if not path.exists():
|
|
196
|
+
return {"error": f"File not found: {file_path}"}
|
|
197
|
+
|
|
198
|
+
try:
|
|
199
|
+
source = path.read_text()
|
|
200
|
+
tree = ast.parse(source)
|
|
201
|
+
except SyntaxError as e:
|
|
202
|
+
return {"error": f"Syntax error: {e}"}
|
|
203
|
+
|
|
204
|
+
# Find the class
|
|
205
|
+
target = None
|
|
206
|
+
for node in ast.walk(tree):
|
|
207
|
+
if isinstance(node, ast.ClassDef) and node.name == class_name:
|
|
208
|
+
target = node
|
|
209
|
+
break
|
|
210
|
+
|
|
211
|
+
if target is None:
|
|
212
|
+
return {"error": f"Class '{class_name}' not found in {file_path}"}
|
|
213
|
+
|
|
214
|
+
template_parts = []
|
|
215
|
+
|
|
216
|
+
# Class signature with bases
|
|
217
|
+
bases = [_get_annotation(b) for b in target.bases]
|
|
218
|
+
if bases:
|
|
219
|
+
template_parts.append(f"class {class_name}({', '.join(bases)}):")
|
|
220
|
+
else:
|
|
221
|
+
template_parts.append(f"class {class_name}:")
|
|
222
|
+
|
|
223
|
+
# Docstring
|
|
224
|
+
docstring = ast.get_docstring(target)
|
|
225
|
+
if docstring:
|
|
226
|
+
template_parts.append(f' """{docstring}"""')
|
|
227
|
+
|
|
228
|
+
# Class variables
|
|
229
|
+
for node in target.body:
|
|
230
|
+
if isinstance(node, ast.AnnAssign) and isinstance(node.target, ast.Name):
|
|
231
|
+
name = node.target.id
|
|
232
|
+
ann = _get_annotation(node.annotation)
|
|
233
|
+
if node.value:
|
|
234
|
+
val = _get_value(node.value)
|
|
235
|
+
template_parts.append(f" {name}: {ann} = {val}")
|
|
236
|
+
else:
|
|
237
|
+
template_parts.append(f" {name}: {ann}")
|
|
238
|
+
|
|
239
|
+
# Methods (signatures only)
|
|
240
|
+
for node in target.body:
|
|
241
|
+
if isinstance(node, ast.FunctionDef):
|
|
242
|
+
sig = _build_signature(node)
|
|
243
|
+
template_parts.append("")
|
|
244
|
+
template_parts.append(f" {sig}")
|
|
245
|
+
# Method docstring
|
|
246
|
+
method_doc = ast.get_docstring(node)
|
|
247
|
+
if method_doc:
|
|
248
|
+
short_doc = method_doc.split("\n")[0]
|
|
249
|
+
template_parts.append(f' """{short_doc}"""')
|
|
250
|
+
template_parts.append(" ...")
|
|
251
|
+
|
|
252
|
+
return {"template": "\n".join(template_parts)}
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def extract_test_template(test_file: str, target_module: str) -> dict:
|
|
256
|
+
"""Extract test patterns for reuse.
|
|
257
|
+
|
|
258
|
+
Args:
|
|
259
|
+
test_file: Path to the test file
|
|
260
|
+
target_module: Module being tested
|
|
261
|
+
|
|
262
|
+
Returns:
|
|
263
|
+
dict with 'template' string showing test patterns
|
|
264
|
+
or dict with 'error' key if failed
|
|
265
|
+
"""
|
|
266
|
+
path = Path(test_file)
|
|
267
|
+
if not path.exists():
|
|
268
|
+
return {"error": f"File not found: {test_file}"}
|
|
269
|
+
|
|
270
|
+
try:
|
|
271
|
+
source = path.read_text()
|
|
272
|
+
tree = ast.parse(source)
|
|
273
|
+
except SyntaxError as e:
|
|
274
|
+
return {"error": f"Syntax error: {e}"}
|
|
275
|
+
|
|
276
|
+
template_parts = []
|
|
277
|
+
lines = source.splitlines()
|
|
278
|
+
|
|
279
|
+
# Find imports
|
|
280
|
+
imports = []
|
|
281
|
+
for node in ast.walk(tree):
|
|
282
|
+
if isinstance(node, ast.Import):
|
|
283
|
+
for alias in node.names:
|
|
284
|
+
if alias.name == "pytest":
|
|
285
|
+
imports.append("import pytest")
|
|
286
|
+
elif isinstance(node, ast.ImportFrom) and node.module and "mock" in node.module:
|
|
287
|
+
names = ", ".join(a.name for a in node.names)
|
|
288
|
+
imports.append(f"from {node.module} import {names}")
|
|
289
|
+
|
|
290
|
+
if imports:
|
|
291
|
+
template_parts.extend(imports)
|
|
292
|
+
template_parts.append("")
|
|
293
|
+
|
|
294
|
+
# Find fixtures
|
|
295
|
+
for node in ast.walk(tree):
|
|
296
|
+
if isinstance(node, ast.FunctionDef):
|
|
297
|
+
for decorator in node.decorator_list:
|
|
298
|
+
if isinstance(decorator, ast.Attribute) and decorator.attr == "fixture":
|
|
299
|
+
template_parts.append("@pytest.fixture")
|
|
300
|
+
sig = _build_signature(node)
|
|
301
|
+
template_parts.append(f"{sig}")
|
|
302
|
+
template_parts.append(" {fixture_implementation}")
|
|
303
|
+
template_parts.append("")
|
|
304
|
+
elif isinstance(decorator, ast.Name) and "fixture" in decorator.id:
|
|
305
|
+
template_parts.append(f"@{decorator.id}")
|
|
306
|
+
sig = _build_signature(node)
|
|
307
|
+
template_parts.append(f"{sig}")
|
|
308
|
+
template_parts.append(" {fixture_implementation}")
|
|
309
|
+
template_parts.append("")
|
|
310
|
+
|
|
311
|
+
# Find test classes
|
|
312
|
+
for node in ast.walk(tree):
|
|
313
|
+
if isinstance(node, ast.ClassDef) and node.name.startswith("Test"):
|
|
314
|
+
template_parts.append(f"class {node.name}:")
|
|
315
|
+
docstring = ast.get_docstring(node)
|
|
316
|
+
if docstring:
|
|
317
|
+
template_parts.append(f' """{docstring}"""')
|
|
318
|
+
template_parts.append("")
|
|
319
|
+
|
|
320
|
+
# Extract test method patterns
|
|
321
|
+
for method in node.body:
|
|
322
|
+
if isinstance(method, ast.FunctionDef) and method.name.startswith(
|
|
323
|
+
"test_"
|
|
324
|
+
):
|
|
325
|
+
sig = _build_signature(method)
|
|
326
|
+
template_parts.append(f" {sig}")
|
|
327
|
+
template_parts.append(" {test_implementation}")
|
|
328
|
+
template_parts.append("")
|
|
329
|
+
break # Just use first test class as pattern
|
|
330
|
+
|
|
331
|
+
# Find standalone test functions with patch
|
|
332
|
+
for node in ast.walk(tree):
|
|
333
|
+
if isinstance(node, ast.FunctionDef) and node.name.startswith("test_"):
|
|
334
|
+
# Check for patch usage in body
|
|
335
|
+
func_source = "\n".join(lines[node.lineno - 1 : node.end_lineno])
|
|
336
|
+
if "patch" in func_source:
|
|
337
|
+
template_parts.append("def {test_name}():")
|
|
338
|
+
template_parts.append(' with patch("{module}.{target}") as mock:')
|
|
339
|
+
template_parts.append(" mock.return_value = {mock_value}")
|
|
340
|
+
template_parts.append(" {test_implementation}")
|
|
341
|
+
break
|
|
342
|
+
|
|
343
|
+
if not template_parts:
|
|
344
|
+
# Provide minimal template
|
|
345
|
+
template_parts = [
|
|
346
|
+
"import pytest",
|
|
347
|
+
f"from {target_module} import {{symbols}}",
|
|
348
|
+
"",
|
|
349
|
+
"class Test{{ClassName}}:",
|
|
350
|
+
' """Tests for {{symbol}}."""',
|
|
351
|
+
"",
|
|
352
|
+
" def test_basic(self):",
|
|
353
|
+
" {test_implementation}",
|
|
354
|
+
]
|
|
355
|
+
|
|
356
|
+
return {"template": "\n".join(template_parts)}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"""FastAPI example with async graph execution.
|
|
2
|
+
|
|
3
|
+
Demonstrates:
|
|
4
|
+
- Async graph loading and compilation
|
|
5
|
+
- Interrupt handling via HTTP endpoints
|
|
6
|
+
- Thread-based session management
|
|
7
|
+
|
|
8
|
+
Run:
|
|
9
|
+
pip install fastapi uvicorn
|
|
10
|
+
uvicorn examples.fastapi_interview:app --reload
|
|
11
|
+
|
|
12
|
+
Usage:
|
|
13
|
+
# Start a new session
|
|
14
|
+
curl -X POST http://localhost:8000/chat/session1 -d '{"message": "start"}'
|
|
15
|
+
|
|
16
|
+
# Resume after interrupt (provide user's name)
|
|
17
|
+
curl -X POST http://localhost:8000/chat/session1/resume -d '{"answer": "Alice"}'
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
import logging
|
|
21
|
+
from contextlib import asynccontextmanager
|
|
22
|
+
from typing import Any
|
|
23
|
+
|
|
24
|
+
from fastapi import FastAPI, HTTPException
|
|
25
|
+
from langgraph.types import Command
|
|
26
|
+
from pydantic import BaseModel
|
|
27
|
+
|
|
28
|
+
from yamlgraph.executor_async import load_and_compile_async, run_graph_async
|
|
29
|
+
|
|
30
|
+
logger = logging.getLogger(__name__)
|
|
31
|
+
|
|
32
|
+
# Global compiled graph (loaded once at startup)
|
|
33
|
+
_app = None
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@asynccontextmanager
|
|
37
|
+
async def lifespan(app: FastAPI):
|
|
38
|
+
"""Load graph at startup."""
|
|
39
|
+
global _app
|
|
40
|
+
try:
|
|
41
|
+
_app = await load_and_compile_async("graphs/interview-demo.yaml")
|
|
42
|
+
logger.info("✅ Graph loaded and compiled")
|
|
43
|
+
except FileNotFoundError:
|
|
44
|
+
logger.warning("⚠️ interview-demo.yaml not found, using mock mode")
|
|
45
|
+
_app = None
|
|
46
|
+
yield
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
app = FastAPI(
|
|
50
|
+
title="YAMLGraph Interview Demo",
|
|
51
|
+
description="Async interview graph with interrupt support",
|
|
52
|
+
lifespan=lifespan,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class ChatRequest(BaseModel):
|
|
57
|
+
"""Request to start or continue chat."""
|
|
58
|
+
|
|
59
|
+
message: str = "start"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class ResumeRequest(BaseModel):
|
|
63
|
+
"""Request to resume after interrupt."""
|
|
64
|
+
|
|
65
|
+
answer: str
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class ChatResponse(BaseModel):
|
|
69
|
+
"""Response from chat endpoint."""
|
|
70
|
+
|
|
71
|
+
status: str
|
|
72
|
+
question: str | None = None
|
|
73
|
+
response: str | None = None
|
|
74
|
+
state: dict[str, Any] | None = None
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@app.post("/chat/{thread_id}", response_model=ChatResponse)
|
|
78
|
+
async def chat(thread_id: str, request: ChatRequest) -> ChatResponse:
|
|
79
|
+
"""Start or continue a chat session.
|
|
80
|
+
|
|
81
|
+
If graph hits an interrupt, returns status="waiting" with the question.
|
|
82
|
+
Otherwise returns status="complete" with the response.
|
|
83
|
+
"""
|
|
84
|
+
if _app is None:
|
|
85
|
+
raise HTTPException(status_code=503, detail="Graph not loaded")
|
|
86
|
+
|
|
87
|
+
config = {"configurable": {"thread_id": thread_id}}
|
|
88
|
+
|
|
89
|
+
try:
|
|
90
|
+
result = await run_graph_async(
|
|
91
|
+
_app,
|
|
92
|
+
initial_state={"input": request.message},
|
|
93
|
+
config=config,
|
|
94
|
+
)
|
|
95
|
+
except Exception as e:
|
|
96
|
+
logger.error(f"Graph execution error: {e}")
|
|
97
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
98
|
+
|
|
99
|
+
# Check for interrupt
|
|
100
|
+
if "__interrupt__" in result:
|
|
101
|
+
interrupt_value = result["__interrupt__"][0].value
|
|
102
|
+
question = interrupt_value.get("question") or interrupt_value.get(
|
|
103
|
+
"prompt", str(interrupt_value)
|
|
104
|
+
)
|
|
105
|
+
return ChatResponse(
|
|
106
|
+
status="waiting",
|
|
107
|
+
question=question,
|
|
108
|
+
state={k: v for k, v in result.items() if not k.startswith("_")},
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
return ChatResponse(
|
|
112
|
+
status="complete",
|
|
113
|
+
response=result.get("greeting") or result.get("response"),
|
|
114
|
+
state={k: v for k, v in result.items() if not k.startswith("_")},
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@app.post("/chat/{thread_id}/resume", response_model=ChatResponse)
|
|
119
|
+
async def resume(thread_id: str, request: ResumeRequest) -> ChatResponse:
|
|
120
|
+
"""Resume a paused chat session with user's answer.
|
|
121
|
+
|
|
122
|
+
Uses LangGraph's Command(resume=...) to continue execution.
|
|
123
|
+
"""
|
|
124
|
+
if _app is None:
|
|
125
|
+
raise HTTPException(status_code=503, detail="Graph not loaded")
|
|
126
|
+
|
|
127
|
+
config = {"configurable": {"thread_id": thread_id}}
|
|
128
|
+
|
|
129
|
+
try:
|
|
130
|
+
result = await run_graph_async(
|
|
131
|
+
_app,
|
|
132
|
+
initial_state=Command(resume=request.answer),
|
|
133
|
+
config=config,
|
|
134
|
+
)
|
|
135
|
+
except Exception as e:
|
|
136
|
+
logger.error(f"Graph resume error: {e}")
|
|
137
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
138
|
+
|
|
139
|
+
# Check for another interrupt
|
|
140
|
+
if "__interrupt__" in result:
|
|
141
|
+
interrupt_value = result["__interrupt__"][0].value
|
|
142
|
+
question = interrupt_value.get("question") or interrupt_value.get(
|
|
143
|
+
"prompt", str(interrupt_value)
|
|
144
|
+
)
|
|
145
|
+
return ChatResponse(
|
|
146
|
+
status="waiting",
|
|
147
|
+
question=question,
|
|
148
|
+
state={k: v for k, v in result.items() if not k.startswith("_")},
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
return ChatResponse(
|
|
152
|
+
status="complete",
|
|
153
|
+
response=result.get("greeting") or result.get("response"),
|
|
154
|
+
state={k: v for k, v in result.items() if not k.startswith("_")},
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
@app.get("/health")
|
|
159
|
+
async def health():
|
|
160
|
+
"""Health check endpoint."""
|
|
161
|
+
return {"status": "ok", "graph_loaded": _app is not None}
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
if __name__ == "__main__":
|
|
165
|
+
import uvicorn
|
|
166
|
+
|
|
167
|
+
uvicorn.run(app, host="0.0.0.0", port=8000)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""NPC Encounter Web API."""
|
examples/npc/api/app.py
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""NPC Encounter Web UI - FastAPI Application.
|
|
2
|
+
|
|
3
|
+
HTMX-powered web interface for running NPC encounters.
|
|
4
|
+
Uses YAMLGraph for encounter logic with session persistence.
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
uvicorn examples.npc.api.app:app --reload
|
|
8
|
+
|
|
9
|
+
# Or with custom port
|
|
10
|
+
uvicorn examples.npc.api.app:app --reload --port 8080
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import logging
|
|
14
|
+
import uuid
|
|
15
|
+
from contextlib import asynccontextmanager
|
|
16
|
+
|
|
17
|
+
from fastapi import FastAPI, Request
|
|
18
|
+
from fastapi.responses import HTMLResponse
|
|
19
|
+
from fastapi.staticfiles import StaticFiles
|
|
20
|
+
from fastapi.templating import Jinja2Templates
|
|
21
|
+
|
|
22
|
+
from examples.npc.api.routes.encounter import router as encounter_router
|
|
23
|
+
|
|
24
|
+
# Configure logging
|
|
25
|
+
logging.basicConfig(
|
|
26
|
+
level=logging.INFO,
|
|
27
|
+
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
|
|
28
|
+
)
|
|
29
|
+
logger = logging.getLogger(__name__)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@asynccontextmanager
|
|
33
|
+
async def lifespan(app: FastAPI):
|
|
34
|
+
"""Application lifecycle handler."""
|
|
35
|
+
logger.info("🚀 Starting NPC Encounter Web UI...")
|
|
36
|
+
logger.info("📍 Templates: examples/npc/api/templates")
|
|
37
|
+
logger.info("📦 Static files: examples/npc/api/static")
|
|
38
|
+
yield
|
|
39
|
+
logger.info("👋 Shutting down NPC Encounter Web UI...")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# Create FastAPI app
|
|
43
|
+
app = FastAPI(
|
|
44
|
+
title="NPC Encounter Web UI",
|
|
45
|
+
description="HTMX-powered web interface for running NPC encounters",
|
|
46
|
+
version="0.1.0",
|
|
47
|
+
lifespan=lifespan,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
# Templates
|
|
51
|
+
templates = Jinja2Templates(directory="examples/npc/api/templates")
|
|
52
|
+
|
|
53
|
+
# Include routers
|
|
54
|
+
app.include_router(encounter_router)
|
|
55
|
+
|
|
56
|
+
# Mount static files (create directory if needed)
|
|
57
|
+
try:
|
|
58
|
+
app.mount(
|
|
59
|
+
"/static", StaticFiles(directory="examples/npc/api/static"), name="static"
|
|
60
|
+
)
|
|
61
|
+
except RuntimeError:
|
|
62
|
+
# Static directory doesn't exist, that's fine
|
|
63
|
+
logger.warning("⚠️ Static directory not found, skipping static mount")
|
|
64
|
+
|
|
65
|
+
# Mount outputs directory for generated images
|
|
66
|
+
try:
|
|
67
|
+
app.mount("/outputs", StaticFiles(directory="outputs"), name="outputs")
|
|
68
|
+
logger.info("📁 Mounted outputs directory for generated images")
|
|
69
|
+
except RuntimeError:
|
|
70
|
+
logger.warning("⚠️ Outputs directory not found, skipping outputs mount")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@app.get("/", response_class=HTMLResponse)
|
|
74
|
+
async def index(request: Request):
|
|
75
|
+
"""Render the main encounter page."""
|
|
76
|
+
# Generate a unique session ID for this encounter
|
|
77
|
+
session_id = str(uuid.uuid4())[:8]
|
|
78
|
+
|
|
79
|
+
return templates.TemplateResponse(
|
|
80
|
+
request=request,
|
|
81
|
+
name="index.html",
|
|
82
|
+
context={"session_id": session_id},
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@app.get("/health")
|
|
87
|
+
async def health():
|
|
88
|
+
"""Health check endpoint."""
|
|
89
|
+
return {"status": "healthy", "service": "npc-encounter-api"}
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
if __name__ == "__main__":
|
|
93
|
+
import uvicorn
|
|
94
|
+
|
|
95
|
+
uvicorn.run(
|
|
96
|
+
"examples.npc.api.app:app",
|
|
97
|
+
host="0.0.0.0",
|
|
98
|
+
port=8000,
|
|
99
|
+
reload=True,
|
|
100
|
+
)
|