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,541 @@
|
|
|
1
|
+
"""Graph commands for universal graph runner.
|
|
2
|
+
|
|
3
|
+
Implements:
|
|
4
|
+
- graph run <path> --var key=value
|
|
5
|
+
- graph list
|
|
6
|
+
- graph info <path>
|
|
7
|
+
- graph lint <path>
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import sys
|
|
11
|
+
from argparse import Namespace
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
import yaml
|
|
15
|
+
|
|
16
|
+
from yamlgraph.config import WORKING_DIR
|
|
17
|
+
from yamlgraph.tools.graph_linter import lint_graph
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def parse_vars(var_list: list[str] | None) -> dict[str, str]:
|
|
21
|
+
"""Parse --var key=value arguments into a dict.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
var_list: List of "key=value" strings
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
Dict mapping keys to values
|
|
28
|
+
|
|
29
|
+
Raises:
|
|
30
|
+
ValueError: If a var doesn't contain '='
|
|
31
|
+
"""
|
|
32
|
+
if not var_list:
|
|
33
|
+
return {}
|
|
34
|
+
|
|
35
|
+
result = {}
|
|
36
|
+
for item in var_list:
|
|
37
|
+
if "=" not in item:
|
|
38
|
+
raise ValueError(f"Invalid var format: '{item}' (expected key=value)")
|
|
39
|
+
key, value = item.split("=", 1)
|
|
40
|
+
result[key] = value
|
|
41
|
+
|
|
42
|
+
return result
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _display_result(result: dict, truncate: bool = True) -> None:
|
|
46
|
+
"""Display result summary to console.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
result: Graph execution result dict
|
|
50
|
+
truncate: Whether to truncate long values (default: True)
|
|
51
|
+
"""
|
|
52
|
+
print("=" * 60)
|
|
53
|
+
print("RESULT")
|
|
54
|
+
print("=" * 60)
|
|
55
|
+
|
|
56
|
+
skip_keys = {"messages", "errors", "_loop_counts"}
|
|
57
|
+
for key, value in result.items():
|
|
58
|
+
if key.startswith("_") or key in skip_keys:
|
|
59
|
+
continue
|
|
60
|
+
if value is not None:
|
|
61
|
+
value_str = str(value)
|
|
62
|
+
if truncate and len(value_str) > 200:
|
|
63
|
+
value_str = value_str[:200] + "..."
|
|
64
|
+
print(f" {key}: {value_str}")
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _handle_export(graph_path: Path, result: dict) -> None:
|
|
68
|
+
"""Handle optional result export.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
graph_path: Path to the graph YAML file
|
|
72
|
+
result: Graph execution result dict
|
|
73
|
+
"""
|
|
74
|
+
from yamlgraph.storage.export import export_result
|
|
75
|
+
|
|
76
|
+
with open(graph_path) as f:
|
|
77
|
+
graph_config = yaml.safe_load(f)
|
|
78
|
+
|
|
79
|
+
export_config = graph_config.get("exports", {})
|
|
80
|
+
if export_config:
|
|
81
|
+
paths = export_result(result, export_config)
|
|
82
|
+
if paths:
|
|
83
|
+
print("\n📁 Exported:")
|
|
84
|
+
for p in paths:
|
|
85
|
+
print(f" {p}")
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def cmd_graph_run(args: Namespace) -> None:
|
|
89
|
+
"""Run any graph with provided variables.
|
|
90
|
+
|
|
91
|
+
Usage:
|
|
92
|
+
yamlgraph graph run graphs/yamlgraph.yaml --var topic=AI --var style=casual
|
|
93
|
+
"""
|
|
94
|
+
from yamlgraph.graph_loader import load_and_compile
|
|
95
|
+
|
|
96
|
+
graph_path = Path(args.graph_path)
|
|
97
|
+
|
|
98
|
+
if not graph_path.exists():
|
|
99
|
+
print(f"❌ Graph file not found: {graph_path}")
|
|
100
|
+
sys.exit(1)
|
|
101
|
+
|
|
102
|
+
# Parse variables
|
|
103
|
+
try:
|
|
104
|
+
initial_state = parse_vars(args.var)
|
|
105
|
+
except ValueError as e:
|
|
106
|
+
print(f"❌ {e}")
|
|
107
|
+
sys.exit(1)
|
|
108
|
+
|
|
109
|
+
print(f"\n🚀 Running graph: {graph_path.name}")
|
|
110
|
+
if initial_state:
|
|
111
|
+
print(f" Variables: {initial_state}")
|
|
112
|
+
print()
|
|
113
|
+
|
|
114
|
+
try:
|
|
115
|
+
graph = load_and_compile(str(graph_path))
|
|
116
|
+
app = graph.compile()
|
|
117
|
+
|
|
118
|
+
# Add thread_id if provided
|
|
119
|
+
config = {}
|
|
120
|
+
if args.thread:
|
|
121
|
+
config["configurable"] = {"thread_id": args.thread}
|
|
122
|
+
initial_state["thread_id"] = args.thread
|
|
123
|
+
|
|
124
|
+
result = app.invoke(initial_state, config=config if config else None)
|
|
125
|
+
|
|
126
|
+
_display_result(result, truncate=not getattr(args, "full", False))
|
|
127
|
+
|
|
128
|
+
if args.export:
|
|
129
|
+
_handle_export(graph_path, result)
|
|
130
|
+
|
|
131
|
+
print()
|
|
132
|
+
|
|
133
|
+
except Exception as e:
|
|
134
|
+
print(f"❌ Error: {e}")
|
|
135
|
+
sys.exit(1)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def cmd_graph_list(args: Namespace) -> None:
|
|
139
|
+
"""List available graphs in graphs/ directory."""
|
|
140
|
+
graphs_dir = Path("graphs")
|
|
141
|
+
|
|
142
|
+
if not graphs_dir.exists():
|
|
143
|
+
print("❌ graphs/ directory not found")
|
|
144
|
+
return
|
|
145
|
+
|
|
146
|
+
yaml_files = sorted(graphs_dir.glob("*.yaml"))
|
|
147
|
+
|
|
148
|
+
if not yaml_files:
|
|
149
|
+
print("No graphs found in graphs/")
|
|
150
|
+
return
|
|
151
|
+
|
|
152
|
+
print(f"\n📋 Available graphs ({len(yaml_files)}):\n")
|
|
153
|
+
|
|
154
|
+
for path in yaml_files:
|
|
155
|
+
try:
|
|
156
|
+
with open(path) as f:
|
|
157
|
+
config = yaml.safe_load(f)
|
|
158
|
+
description = config.get("description", "")
|
|
159
|
+
print(f" {path.name}")
|
|
160
|
+
if description:
|
|
161
|
+
print(f" {description[:60]}")
|
|
162
|
+
except Exception:
|
|
163
|
+
print(f" {path.name} (invalid)")
|
|
164
|
+
|
|
165
|
+
print()
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def cmd_graph_info(args: Namespace) -> None:
|
|
169
|
+
"""Show information about a graph."""
|
|
170
|
+
graph_path = Path(args.graph_path)
|
|
171
|
+
|
|
172
|
+
if not graph_path.exists():
|
|
173
|
+
print(f"❌ Graph file not found: {graph_path}")
|
|
174
|
+
sys.exit(1)
|
|
175
|
+
|
|
176
|
+
try:
|
|
177
|
+
with open(graph_path) as f:
|
|
178
|
+
config = yaml.safe_load(f)
|
|
179
|
+
|
|
180
|
+
name = config.get("name", graph_path.stem)
|
|
181
|
+
description = config.get("description", "No description")
|
|
182
|
+
state_class = config.get("state_class", "default")
|
|
183
|
+
nodes = config.get("nodes", {})
|
|
184
|
+
edges = config.get("edges", [])
|
|
185
|
+
|
|
186
|
+
print(f"\n📊 Graph: {name}")
|
|
187
|
+
print(f" {description}")
|
|
188
|
+
print(f"\n State: {state_class}")
|
|
189
|
+
|
|
190
|
+
# Show nodes
|
|
191
|
+
print(f"\n Nodes ({len(nodes)}):")
|
|
192
|
+
for node_name, node_config in nodes.items():
|
|
193
|
+
node_type = node_config.get("type", "prompt")
|
|
194
|
+
print(f" - {node_name} ({node_type})")
|
|
195
|
+
|
|
196
|
+
# Show edges
|
|
197
|
+
print(f"\n Edges ({len(edges)}):")
|
|
198
|
+
for edge in edges:
|
|
199
|
+
from_node = edge.get("from", "?")
|
|
200
|
+
to_node = edge.get("to", "?")
|
|
201
|
+
condition = edge.get("condition", "")
|
|
202
|
+
if condition:
|
|
203
|
+
print(f" {from_node} → {to_node} (conditional)")
|
|
204
|
+
else:
|
|
205
|
+
print(f" {from_node} → {to_node}")
|
|
206
|
+
|
|
207
|
+
# Show required inputs if defined
|
|
208
|
+
inputs = config.get("inputs", {})
|
|
209
|
+
if inputs:
|
|
210
|
+
print(f"\n Inputs ({len(inputs)}):")
|
|
211
|
+
for input_name, input_config in inputs.items():
|
|
212
|
+
required = input_config.get("required", False)
|
|
213
|
+
default = input_config.get("default", None)
|
|
214
|
+
req_str = " (required)" if required else f" (default: {default})"
|
|
215
|
+
print(f" --var {input_name}=<value>{req_str}")
|
|
216
|
+
|
|
217
|
+
print()
|
|
218
|
+
|
|
219
|
+
except Exception as e:
|
|
220
|
+
print(f"❌ Error reading graph: {e}")
|
|
221
|
+
sys.exit(1)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def cmd_graph_mermaid(args: Namespace) -> None:
|
|
225
|
+
"""Generate Mermaid diagram from a graph."""
|
|
226
|
+
graph_path = Path(args.graph_path)
|
|
227
|
+
|
|
228
|
+
if not graph_path.exists():
|
|
229
|
+
print(f"❌ Graph file not found: {graph_path}")
|
|
230
|
+
sys.exit(1)
|
|
231
|
+
|
|
232
|
+
try:
|
|
233
|
+
with open(graph_path) as f:
|
|
234
|
+
config = yaml.safe_load(f)
|
|
235
|
+
|
|
236
|
+
mermaid = generate_mermaid(config)
|
|
237
|
+
|
|
238
|
+
if hasattr(args, "output") and args.output:
|
|
239
|
+
output_path = Path(args.output)
|
|
240
|
+
output_path.write_text(mermaid)
|
|
241
|
+
print(f"✅ Mermaid diagram written to {output_path}")
|
|
242
|
+
else:
|
|
243
|
+
print(mermaid)
|
|
244
|
+
|
|
245
|
+
except Exception as e:
|
|
246
|
+
print(f"❌ Error generating mermaid: {e}")
|
|
247
|
+
sys.exit(1)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def generate_mermaid(config: dict) -> str:
|
|
251
|
+
"""Generate Mermaid flowchart from graph config.
|
|
252
|
+
|
|
253
|
+
Args:
|
|
254
|
+
config: Parsed YAML graph configuration
|
|
255
|
+
|
|
256
|
+
Returns:
|
|
257
|
+
Mermaid flowchart diagram as string
|
|
258
|
+
"""
|
|
259
|
+
lines = ["```mermaid", "flowchart TD"]
|
|
260
|
+
|
|
261
|
+
nodes = config.get("nodes", {})
|
|
262
|
+
edges = config.get("edges", [])
|
|
263
|
+
|
|
264
|
+
# Node shapes by type
|
|
265
|
+
node_shapes = {
|
|
266
|
+
"llm": ("[", "]"), # Rectangle
|
|
267
|
+
"prompt": ("[", "]"),
|
|
268
|
+
"tool": ("[[", "]]"), # Subroutine
|
|
269
|
+
"agent": ("{{", "}}"), # Hexagon
|
|
270
|
+
"map": ("[/", "/]"), # Parallelogram
|
|
271
|
+
"subgraph": ("[[", "]]"), # Subroutine (composition)
|
|
272
|
+
"router": ("{", "}"), # Diamond-ish
|
|
273
|
+
"interrupt": ("(", ")"), # Stadium
|
|
274
|
+
"passthrough": ("([", "])"), # Stadium
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
# Define nodes with shapes
|
|
278
|
+
for node_name, node_config in nodes.items():
|
|
279
|
+
node_type = node_config.get("type", "llm")
|
|
280
|
+
left, right = node_shapes.get(node_type, ("[", "]"))
|
|
281
|
+
# Escape quotes in node name for display
|
|
282
|
+
display_name = node_name.replace('"', "'")
|
|
283
|
+
lines.append(f' {node_name}{left}"{display_name}"{right}')
|
|
284
|
+
|
|
285
|
+
lines.append("")
|
|
286
|
+
|
|
287
|
+
# Define edges
|
|
288
|
+
for edge in edges:
|
|
289
|
+
from_node = edge.get("from", "START")
|
|
290
|
+
to_node = edge.get("to", "END")
|
|
291
|
+
condition = edge.get("condition")
|
|
292
|
+
edge_type = edge.get("type")
|
|
293
|
+
|
|
294
|
+
# Map START/END to special nodes
|
|
295
|
+
from_id = "__start__" if from_node == "START" else from_node
|
|
296
|
+
|
|
297
|
+
# Handle list of targets (conditional routing)
|
|
298
|
+
if isinstance(to_node, list):
|
|
299
|
+
for target in to_node:
|
|
300
|
+
to_id = "__end__" if target == "END" else target
|
|
301
|
+
lines.append(f" {from_id} -.-> {to_id}")
|
|
302
|
+
else:
|
|
303
|
+
to_id = "__end__" if to_node == "END" else to_node
|
|
304
|
+
|
|
305
|
+
if condition:
|
|
306
|
+
# Conditional edge with label
|
|
307
|
+
cond_label = condition if isinstance(condition, str) else "?"
|
|
308
|
+
lines.append(f" {from_id} -->|{cond_label}| {to_id}")
|
|
309
|
+
elif edge_type == "conditional":
|
|
310
|
+
lines.append(f" {from_id} -.-> {to_id}")
|
|
311
|
+
else:
|
|
312
|
+
lines.append(f" {from_id} --> {to_id}")
|
|
313
|
+
|
|
314
|
+
lines.append("```")
|
|
315
|
+
return "\n".join(lines)
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def _validate_required_fields(config: dict) -> tuple[list[str], list[str]]:
|
|
319
|
+
"""Validate required fields in graph config.
|
|
320
|
+
|
|
321
|
+
Args:
|
|
322
|
+
config: Parsed YAML configuration
|
|
323
|
+
|
|
324
|
+
Returns:
|
|
325
|
+
Tuple of (errors, warnings) lists
|
|
326
|
+
"""
|
|
327
|
+
errors = []
|
|
328
|
+
warnings = []
|
|
329
|
+
|
|
330
|
+
if not config.get("name"):
|
|
331
|
+
errors.append("Missing required field: name")
|
|
332
|
+
|
|
333
|
+
if not config.get("nodes"):
|
|
334
|
+
errors.append("Missing required field: nodes")
|
|
335
|
+
|
|
336
|
+
if not config.get("edges"):
|
|
337
|
+
warnings.append("No edges defined")
|
|
338
|
+
|
|
339
|
+
return errors, warnings
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def _validate_edges(edges: list[dict], node_names: set[str]) -> list[str]:
|
|
343
|
+
"""Validate edge references in graph config.
|
|
344
|
+
|
|
345
|
+
Args:
|
|
346
|
+
edges: List of edge configurations
|
|
347
|
+
node_names: Set of valid node names (including START/END)
|
|
348
|
+
|
|
349
|
+
Returns:
|
|
350
|
+
List of error messages
|
|
351
|
+
"""
|
|
352
|
+
errors = []
|
|
353
|
+
|
|
354
|
+
for i, edge in enumerate(edges):
|
|
355
|
+
from_node = edge.get("from", "")
|
|
356
|
+
to_node = edge.get("to", "")
|
|
357
|
+
|
|
358
|
+
if from_node not in node_names:
|
|
359
|
+
errors.append(f"Edge {i + 1}: unknown 'from' node '{from_node}'")
|
|
360
|
+
|
|
361
|
+
# Handle conditional edges where 'to' is a list
|
|
362
|
+
if isinstance(to_node, list):
|
|
363
|
+
for t in to_node:
|
|
364
|
+
if t not in node_names:
|
|
365
|
+
errors.append(f"Edge {i + 1}: unknown 'to' node '{t}'")
|
|
366
|
+
elif to_node not in node_names:
|
|
367
|
+
errors.append(f"Edge {i + 1}: unknown 'to' node '{to_node}'")
|
|
368
|
+
|
|
369
|
+
return errors
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def _validate_nodes(nodes: dict) -> list[str]:
|
|
373
|
+
"""Validate node configurations.
|
|
374
|
+
|
|
375
|
+
Args:
|
|
376
|
+
nodes: Dict of node_name -> node_config
|
|
377
|
+
|
|
378
|
+
Returns:
|
|
379
|
+
List of warning messages
|
|
380
|
+
"""
|
|
381
|
+
warnings = []
|
|
382
|
+
|
|
383
|
+
for node_name, node_config in nodes.items():
|
|
384
|
+
node_type = node_config.get("type", "llm")
|
|
385
|
+
if node_type == "agent" and not node_config.get("tools"):
|
|
386
|
+
warnings.append(f"Node '{node_name}': agent has no tools")
|
|
387
|
+
|
|
388
|
+
return warnings
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def _report_validation_result(
|
|
392
|
+
graph_path: Path,
|
|
393
|
+
config: dict,
|
|
394
|
+
errors: list[str],
|
|
395
|
+
warnings: list[str],
|
|
396
|
+
) -> None:
|
|
397
|
+
"""Report validation results and exit appropriately.
|
|
398
|
+
|
|
399
|
+
Args:
|
|
400
|
+
graph_path: Path to the graph file
|
|
401
|
+
config: Parsed graph configuration
|
|
402
|
+
errors: List of error messages
|
|
403
|
+
warnings: List of warning messages
|
|
404
|
+
"""
|
|
405
|
+
name = config.get("name", graph_path.stem)
|
|
406
|
+
nodes = config.get("nodes", {})
|
|
407
|
+
edges = config.get("edges", [])
|
|
408
|
+
|
|
409
|
+
if errors:
|
|
410
|
+
print(f"\n❌ {graph_path.name} ({name}) - INVALID\n")
|
|
411
|
+
for err in errors:
|
|
412
|
+
print(f" ✗ {err}")
|
|
413
|
+
for warn in warnings:
|
|
414
|
+
print(f" ⚠ {warn}")
|
|
415
|
+
print()
|
|
416
|
+
sys.exit(1)
|
|
417
|
+
elif warnings:
|
|
418
|
+
print(f"\n⚠️ {graph_path.name} ({name}) - VALID with warnings\n")
|
|
419
|
+
for warn in warnings:
|
|
420
|
+
print(f" ⚠ {warn}")
|
|
421
|
+
print()
|
|
422
|
+
else:
|
|
423
|
+
print(f"\n✅ {graph_path.name} ({name}) - VALID\n")
|
|
424
|
+
print(f" Nodes: {len(nodes)}")
|
|
425
|
+
print(f" Edges: {len(edges)}")
|
|
426
|
+
print()
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
def cmd_graph_validate(args: Namespace) -> None:
|
|
430
|
+
"""Validate a graph YAML file.
|
|
431
|
+
|
|
432
|
+
Checks:
|
|
433
|
+
- File exists and is valid YAML
|
|
434
|
+
- Required fields present (name, nodes, edges)
|
|
435
|
+
- Node references are valid
|
|
436
|
+
- Edge references match existing nodes
|
|
437
|
+
"""
|
|
438
|
+
graph_path = Path(args.graph_path)
|
|
439
|
+
|
|
440
|
+
if not graph_path.exists():
|
|
441
|
+
print(f"❌ Graph file not found: {graph_path}")
|
|
442
|
+
sys.exit(1)
|
|
443
|
+
|
|
444
|
+
try:
|
|
445
|
+
with open(graph_path) as f:
|
|
446
|
+
config = yaml.safe_load(f)
|
|
447
|
+
|
|
448
|
+
# Run validations
|
|
449
|
+
errors, warnings = _validate_required_fields(config)
|
|
450
|
+
|
|
451
|
+
nodes = config.get("nodes", {})
|
|
452
|
+
edges = config.get("edges", [])
|
|
453
|
+
node_names = set(nodes.keys()) | {"START", "END"}
|
|
454
|
+
|
|
455
|
+
errors.extend(_validate_edges(edges, node_names))
|
|
456
|
+
warnings.extend(_validate_nodes(nodes))
|
|
457
|
+
|
|
458
|
+
# Report results
|
|
459
|
+
_report_validation_result(graph_path, config, errors, warnings)
|
|
460
|
+
|
|
461
|
+
except yaml.YAMLError as e:
|
|
462
|
+
print(f"❌ Invalid YAML: {e}")
|
|
463
|
+
sys.exit(1)
|
|
464
|
+
except Exception as e:
|
|
465
|
+
print(f"❌ Error validating graph: {e}")
|
|
466
|
+
sys.exit(1)
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
def cmd_graph_lint(args: Namespace) -> None:
|
|
470
|
+
"""Lint graph YAML files for issues.
|
|
471
|
+
|
|
472
|
+
Checks:
|
|
473
|
+
- Missing state declarations for variables
|
|
474
|
+
- Undefined tool references
|
|
475
|
+
- Missing prompt files
|
|
476
|
+
- Unreachable nodes
|
|
477
|
+
- Invalid node types
|
|
478
|
+
"""
|
|
479
|
+
total_errors = 0
|
|
480
|
+
total_warnings = 0
|
|
481
|
+
|
|
482
|
+
for graph_path_str in args.graph_path:
|
|
483
|
+
graph_path = Path(graph_path_str)
|
|
484
|
+
|
|
485
|
+
if not graph_path.exists():
|
|
486
|
+
print(f"❌ Graph file not found: {graph_path}")
|
|
487
|
+
total_errors += 1
|
|
488
|
+
continue
|
|
489
|
+
|
|
490
|
+
try:
|
|
491
|
+
result = lint_graph(graph_path, WORKING_DIR)
|
|
492
|
+
|
|
493
|
+
errors = [i for i in result.issues if i.severity == "error"]
|
|
494
|
+
warnings = [i for i in result.issues if i.severity == "warning"]
|
|
495
|
+
|
|
496
|
+
if result.valid and not warnings:
|
|
497
|
+
print(f"✅ {graph_path.name} - No issues found")
|
|
498
|
+
else:
|
|
499
|
+
status = "❌" if errors else "⚠️"
|
|
500
|
+
print(f"{status} {graph_path.name}")
|
|
501
|
+
|
|
502
|
+
for issue in result.issues:
|
|
503
|
+
icon = "❌" if issue.severity == "error" else "⚠"
|
|
504
|
+
print(f" {icon} [{issue.code}] {issue.message}")
|
|
505
|
+
if issue.fix:
|
|
506
|
+
print(f" Fix: {issue.fix}")
|
|
507
|
+
|
|
508
|
+
total_errors += len(errors)
|
|
509
|
+
total_warnings += len(warnings)
|
|
510
|
+
|
|
511
|
+
except Exception as e:
|
|
512
|
+
print(f"❌ Error linting {graph_path}: {e}")
|
|
513
|
+
total_errors += 1
|
|
514
|
+
|
|
515
|
+
# Summary
|
|
516
|
+
print()
|
|
517
|
+
if total_errors == 0 and total_warnings == 0:
|
|
518
|
+
print("✅ All graphs passed linting")
|
|
519
|
+
else:
|
|
520
|
+
print(f"Found {total_errors} error(s) and {total_warnings} warning(s)")
|
|
521
|
+
if total_errors > 0:
|
|
522
|
+
sys.exit(1)
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
def cmd_graph_dispatch(args: Namespace) -> None:
|
|
526
|
+
"""Dispatch to graph subcommands."""
|
|
527
|
+
if args.graph_command == "run":
|
|
528
|
+
cmd_graph_run(args)
|
|
529
|
+
elif args.graph_command == "list":
|
|
530
|
+
cmd_graph_list(args)
|
|
531
|
+
elif args.graph_command == "info":
|
|
532
|
+
cmd_graph_info(args)
|
|
533
|
+
elif args.graph_command == "validate":
|
|
534
|
+
cmd_graph_validate(args)
|
|
535
|
+
elif args.graph_command == "lint":
|
|
536
|
+
cmd_graph_lint(args)
|
|
537
|
+
elif args.graph_command == "mermaid":
|
|
538
|
+
cmd_graph_mermaid(args)
|
|
539
|
+
else:
|
|
540
|
+
print(f"Unknown graph command: {args.graph_command}")
|
|
541
|
+
sys.exit(1)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""CLI validation functions.
|
|
2
|
+
|
|
3
|
+
Provides argument validation for CLI commands.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from yamlgraph.config import MAX_WORD_COUNT, MIN_WORD_COUNT
|
|
7
|
+
from yamlgraph.utils.sanitize import sanitize_topic
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def validate_run_args(args) -> bool:
|
|
11
|
+
"""Validate and sanitize run command arguments.
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
args: Parsed arguments namespace
|
|
15
|
+
|
|
16
|
+
Returns:
|
|
17
|
+
True if valid, False otherwise (prints error message)
|
|
18
|
+
"""
|
|
19
|
+
# Sanitize topic
|
|
20
|
+
result = sanitize_topic(args.topic)
|
|
21
|
+
if not result.is_safe:
|
|
22
|
+
for warning in result.warnings:
|
|
23
|
+
print(f"❌ {warning}")
|
|
24
|
+
return False
|
|
25
|
+
|
|
26
|
+
# Update args with sanitized value
|
|
27
|
+
args.topic = result.value
|
|
28
|
+
|
|
29
|
+
# Print any warnings (e.g., truncation)
|
|
30
|
+
for warning in result.warnings:
|
|
31
|
+
print(f"⚠️ {warning}")
|
|
32
|
+
|
|
33
|
+
if args.word_count < MIN_WORD_COUNT or args.word_count > MAX_WORD_COUNT:
|
|
34
|
+
print(f"❌ Word count must be between {MIN_WORD_COUNT} and {MAX_WORD_COUNT}")
|
|
35
|
+
return False
|
|
36
|
+
|
|
37
|
+
return True
|
yamlgraph/config.py
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""Centralized configuration for the yamlgraph package.
|
|
2
|
+
|
|
3
|
+
Provides paths, settings, and environment configuration
|
|
4
|
+
used across all modules.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from dotenv import load_dotenv
|
|
11
|
+
|
|
12
|
+
# Package root (yamlgraph/ directory)
|
|
13
|
+
PACKAGE_ROOT = Path(__file__).parent
|
|
14
|
+
|
|
15
|
+
# Working directory (where the user runs the CLI from)
|
|
16
|
+
WORKING_DIR = Path.cwd()
|
|
17
|
+
|
|
18
|
+
# Load environment variables from current working directory
|
|
19
|
+
# This ensures .env is found where the user runs yamlgraph, not in site-packages
|
|
20
|
+
load_dotenv(WORKING_DIR / ".env")
|
|
21
|
+
|
|
22
|
+
# Directory paths (relative to working directory)
|
|
23
|
+
PROMPTS_DIR = WORKING_DIR / "prompts"
|
|
24
|
+
GRAPHS_DIR = WORKING_DIR / "graphs"
|
|
25
|
+
OUTPUTS_DIR = WORKING_DIR / "outputs"
|
|
26
|
+
DATABASE_PATH = OUTPUTS_DIR / "yamlgraph.db"
|
|
27
|
+
|
|
28
|
+
# Default graph configuration
|
|
29
|
+
DEFAULT_GRAPH = GRAPHS_DIR / "yamlgraph.yaml"
|
|
30
|
+
|
|
31
|
+
# LLM Configuration
|
|
32
|
+
DEFAULT_TEMPERATURE = 0.7
|
|
33
|
+
DEFAULT_MAX_TOKENS = 4096
|
|
34
|
+
|
|
35
|
+
# Default models per provider (override with {PROVIDER}_MODEL env var)
|
|
36
|
+
DEFAULT_MODELS = {
|
|
37
|
+
"anthropic": os.getenv("ANTHROPIC_MODEL", "claude-haiku-4-5"),
|
|
38
|
+
"mistral": os.getenv("MISTRAL_MODEL", "mistral-large-latest"),
|
|
39
|
+
"openai": os.getenv("OPENAI_MODEL", "gpt-4o"),
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
# Retry Configuration
|
|
43
|
+
MAX_RETRIES = int(os.getenv("LLM_MAX_RETRIES", "3"))
|
|
44
|
+
RETRY_BASE_DELAY = float(os.getenv("LLM_RETRY_DELAY", "1.0")) # seconds
|
|
45
|
+
RETRY_MAX_DELAY = float(os.getenv("LLM_RETRY_MAX_DELAY", "30.0")) # seconds
|
|
46
|
+
|
|
47
|
+
# CLI Constraints - configurable via environment
|
|
48
|
+
MAX_TOPIC_LENGTH = int(os.getenv("YAMLGRAPH_MAX_TOPIC_LENGTH", "500"))
|
|
49
|
+
MAX_WORD_COUNT = int(os.getenv("YAMLGRAPH_MAX_WORD_COUNT", "5000"))
|
|
50
|
+
MIN_WORD_COUNT = int(os.getenv("YAMLGRAPH_MIN_WORD_COUNT", "50"))
|
|
51
|
+
|
|
52
|
+
# Valid styles - can be extended via environment (comma-separated)
|
|
53
|
+
_default_styles = "informative,casual,technical"
|
|
54
|
+
VALID_STYLES = tuple(os.getenv("YAMLGRAPH_VALID_STYLES", _default_styles).split(","))
|
|
55
|
+
|
|
56
|
+
# Input Sanitization Patterns
|
|
57
|
+
# Characters that could be used for prompt injection
|
|
58
|
+
DANGEROUS_PATTERNS = [
|
|
59
|
+
"ignore previous",
|
|
60
|
+
"ignore above",
|
|
61
|
+
"disregard",
|
|
62
|
+
"forget everything",
|
|
63
|
+
"new instructions",
|
|
64
|
+
"system:",
|
|
65
|
+
"<|", # Token delimiters
|
|
66
|
+
"|>",
|
|
67
|
+
]
|