yamlgraph 0.1.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of yamlgraph might be problematic. Click here for more details.
- examples/__init__.py +1 -0
- examples/storyboard/__init__.py +1 -0
- examples/storyboard/generate_videos.py +335 -0
- examples/storyboard/nodes/__init__.py +10 -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 +238 -0
- examples/storyboard/retry_images.py +118 -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_map_demo.py +50 -0
- tests/integration/test_memory_demo.py +281 -0
- tests/integration/test_pipeline_flow.py +105 -0
- tests/integration/test_providers.py +163 -0
- tests/integration/test_resume.py +75 -0
- tests/unit/__init__.py +1 -0
- tests/unit/test_agent_nodes.py +200 -0
- tests/unit/test_checkpointer.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 +270 -0
- tests/unit/test_database.py +145 -0
- tests/unit/test_deprecation.py +104 -0
- tests/unit/test_executor.py +60 -0
- tests/unit/test_executor_async.py +179 -0
- tests/unit/test_export.py +150 -0
- tests/unit/test_expressions.py +178 -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_loader.py +299 -0
- tests/unit/test_graph_schema.py +193 -0
- tests/unit/test_inline_schema.py +151 -0
- tests/unit/test_issues.py +164 -0
- tests/unit/test_jinja2_prompts.py +85 -0
- tests/unit/test_langsmith.py +319 -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 +225 -0
- tests/unit/test_prompts.py +166 -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_template.py +190 -0
- tests/unit/test_tool_nodes.py +129 -0
- yamlgraph/__init__.py +35 -0
- yamlgraph/builder.py +110 -0
- yamlgraph/cli/__init__.py +139 -0
- yamlgraph/cli/__main__.py +6 -0
- yamlgraph/cli/commands.py +232 -0
- yamlgraph/cli/deprecation.py +92 -0
- yamlgraph/cli/graph_commands.py +382 -0
- yamlgraph/cli/validators.py +37 -0
- yamlgraph/config.py +67 -0
- yamlgraph/constants.py +66 -0
- yamlgraph/error_handlers.py +226 -0
- yamlgraph/executor.py +275 -0
- yamlgraph/executor_async.py +122 -0
- yamlgraph/graph_loader.py +337 -0
- yamlgraph/map_compiler.py +138 -0
- yamlgraph/models/__init__.py +36 -0
- yamlgraph/models/graph_schema.py +141 -0
- yamlgraph/models/schemas.py +124 -0
- yamlgraph/models/state_builder.py +236 -0
- yamlgraph/node_factory.py +240 -0
- yamlgraph/routing.py +87 -0
- yamlgraph/schema_loader.py +160 -0
- yamlgraph/storage/__init__.py +17 -0
- yamlgraph/storage/checkpointer.py +72 -0
- yamlgraph/storage/database.py +320 -0
- yamlgraph/storage/export.py +269 -0
- yamlgraph/tools/__init__.py +1 -0
- yamlgraph/tools/agent.py +235 -0
- yamlgraph/tools/nodes.py +124 -0
- yamlgraph/tools/python_tool.py +178 -0
- yamlgraph/tools/shell.py +205 -0
- yamlgraph/utils/__init__.py +47 -0
- yamlgraph/utils/conditions.py +157 -0
- yamlgraph/utils/expressions.py +111 -0
- yamlgraph/utils/langsmith.py +308 -0
- yamlgraph/utils/llm_factory.py +118 -0
- yamlgraph/utils/llm_factory_async.py +105 -0
- yamlgraph/utils/logging.py +127 -0
- yamlgraph/utils/prompts.py +116 -0
- yamlgraph/utils/sanitize.py +98 -0
- yamlgraph/utils/template.py +102 -0
- yamlgraph/utils/validators.py +181 -0
- yamlgraph-0.1.1.dist-info/METADATA +854 -0
- yamlgraph-0.1.1.dist-info/RECORD +111 -0
- yamlgraph-0.1.1.dist-info/WHEEL +5 -0
- yamlgraph-0.1.1.dist-info/entry_points.txt +2 -0
- yamlgraph-0.1.1.dist-info/licenses/LICENSE +21 -0
- yamlgraph-0.1.1.dist-info/top_level.txt +3 -0
|
@@ -0,0 +1,382 @@
|
|
|
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
|
+
"""
|
|
8
|
+
|
|
9
|
+
import sys
|
|
10
|
+
from argparse import Namespace
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
import yaml
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def parse_vars(var_list: list[str] | None) -> dict[str, str]:
|
|
17
|
+
"""Parse --var key=value arguments into a dict.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
var_list: List of "key=value" strings
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
Dict mapping keys to values
|
|
24
|
+
|
|
25
|
+
Raises:
|
|
26
|
+
ValueError: If a var doesn't contain '='
|
|
27
|
+
"""
|
|
28
|
+
if not var_list:
|
|
29
|
+
return {}
|
|
30
|
+
|
|
31
|
+
result = {}
|
|
32
|
+
for item in var_list:
|
|
33
|
+
if "=" not in item:
|
|
34
|
+
raise ValueError(f"Invalid var format: '{item}' (expected key=value)")
|
|
35
|
+
key, value = item.split("=", 1)
|
|
36
|
+
result[key] = value
|
|
37
|
+
|
|
38
|
+
return result
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _display_result(result: dict) -> None:
|
|
42
|
+
"""Display result summary to console.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
result: Graph execution result dict
|
|
46
|
+
"""
|
|
47
|
+
print("=" * 60)
|
|
48
|
+
print("RESULT")
|
|
49
|
+
print("=" * 60)
|
|
50
|
+
|
|
51
|
+
skip_keys = {"messages", "errors", "_loop_counts"}
|
|
52
|
+
for key, value in result.items():
|
|
53
|
+
if key.startswith("_") or key in skip_keys:
|
|
54
|
+
continue
|
|
55
|
+
if value is not None:
|
|
56
|
+
value_str = str(value)[:200]
|
|
57
|
+
if len(str(value)) > 200:
|
|
58
|
+
value_str += "..."
|
|
59
|
+
print(f" {key}: {value_str}")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _handle_export(graph_path: Path, result: dict) -> None:
|
|
63
|
+
"""Handle optional result export.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
graph_path: Path to the graph YAML file
|
|
67
|
+
result: Graph execution result dict
|
|
68
|
+
"""
|
|
69
|
+
from yamlgraph.storage.export import export_result
|
|
70
|
+
|
|
71
|
+
with open(graph_path) as f:
|
|
72
|
+
graph_config = yaml.safe_load(f)
|
|
73
|
+
|
|
74
|
+
export_config = graph_config.get("exports", {})
|
|
75
|
+
if export_config:
|
|
76
|
+
paths = export_result(result, export_config)
|
|
77
|
+
if paths:
|
|
78
|
+
print("\n📁 Exported:")
|
|
79
|
+
for p in paths:
|
|
80
|
+
print(f" {p}")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def cmd_graph_run(args: Namespace) -> None:
|
|
84
|
+
"""Run any graph with provided variables.
|
|
85
|
+
|
|
86
|
+
Usage:
|
|
87
|
+
yamlgraph graph run graphs/yamlgraph.yaml --var topic=AI --var style=casual
|
|
88
|
+
"""
|
|
89
|
+
from yamlgraph.graph_loader import load_and_compile
|
|
90
|
+
|
|
91
|
+
graph_path = Path(args.graph_path)
|
|
92
|
+
|
|
93
|
+
if not graph_path.exists():
|
|
94
|
+
print(f"❌ Graph file not found: {graph_path}")
|
|
95
|
+
sys.exit(1)
|
|
96
|
+
|
|
97
|
+
# Parse variables
|
|
98
|
+
try:
|
|
99
|
+
initial_state = parse_vars(args.var)
|
|
100
|
+
except ValueError as e:
|
|
101
|
+
print(f"❌ {e}")
|
|
102
|
+
sys.exit(1)
|
|
103
|
+
|
|
104
|
+
print(f"\n🚀 Running graph: {graph_path.name}")
|
|
105
|
+
if initial_state:
|
|
106
|
+
print(f" Variables: {initial_state}")
|
|
107
|
+
print()
|
|
108
|
+
|
|
109
|
+
try:
|
|
110
|
+
graph = load_and_compile(str(graph_path))
|
|
111
|
+
app = graph.compile()
|
|
112
|
+
|
|
113
|
+
# Add thread_id if provided
|
|
114
|
+
config = {}
|
|
115
|
+
if args.thread:
|
|
116
|
+
config["configurable"] = {"thread_id": args.thread}
|
|
117
|
+
initial_state["thread_id"] = args.thread
|
|
118
|
+
|
|
119
|
+
result = app.invoke(initial_state, config=config if config else None)
|
|
120
|
+
|
|
121
|
+
_display_result(result)
|
|
122
|
+
|
|
123
|
+
if args.export:
|
|
124
|
+
_handle_export(graph_path, result)
|
|
125
|
+
|
|
126
|
+
print()
|
|
127
|
+
|
|
128
|
+
except Exception as e:
|
|
129
|
+
print(f"❌ Error: {e}")
|
|
130
|
+
sys.exit(1)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def cmd_graph_list(args: Namespace) -> None:
|
|
134
|
+
"""List available graphs in graphs/ directory."""
|
|
135
|
+
graphs_dir = Path("graphs")
|
|
136
|
+
|
|
137
|
+
if not graphs_dir.exists():
|
|
138
|
+
print("❌ graphs/ directory not found")
|
|
139
|
+
return
|
|
140
|
+
|
|
141
|
+
yaml_files = sorted(graphs_dir.glob("*.yaml"))
|
|
142
|
+
|
|
143
|
+
if not yaml_files:
|
|
144
|
+
print("No graphs found in graphs/")
|
|
145
|
+
return
|
|
146
|
+
|
|
147
|
+
print(f"\n📋 Available graphs ({len(yaml_files)}):\n")
|
|
148
|
+
|
|
149
|
+
for path in yaml_files:
|
|
150
|
+
try:
|
|
151
|
+
with open(path) as f:
|
|
152
|
+
config = yaml.safe_load(f)
|
|
153
|
+
description = config.get("description", "")
|
|
154
|
+
print(f" {path.name}")
|
|
155
|
+
if description:
|
|
156
|
+
print(f" {description[:60]}")
|
|
157
|
+
except Exception:
|
|
158
|
+
print(f" {path.name} (invalid)")
|
|
159
|
+
|
|
160
|
+
print()
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def cmd_graph_info(args: Namespace) -> None:
|
|
164
|
+
"""Show information about a graph."""
|
|
165
|
+
graph_path = Path(args.graph_path)
|
|
166
|
+
|
|
167
|
+
if not graph_path.exists():
|
|
168
|
+
print(f"❌ Graph file not found: {graph_path}")
|
|
169
|
+
sys.exit(1)
|
|
170
|
+
|
|
171
|
+
try:
|
|
172
|
+
with open(graph_path) as f:
|
|
173
|
+
config = yaml.safe_load(f)
|
|
174
|
+
|
|
175
|
+
name = config.get("name", graph_path.stem)
|
|
176
|
+
description = config.get("description", "No description")
|
|
177
|
+
state_class = config.get("state_class", "default")
|
|
178
|
+
nodes = config.get("nodes", {})
|
|
179
|
+
edges = config.get("edges", [])
|
|
180
|
+
|
|
181
|
+
print(f"\n📊 Graph: {name}")
|
|
182
|
+
print(f" {description}")
|
|
183
|
+
print(f"\n State: {state_class}")
|
|
184
|
+
|
|
185
|
+
# Show nodes
|
|
186
|
+
print(f"\n Nodes ({len(nodes)}):")
|
|
187
|
+
for node_name, node_config in nodes.items():
|
|
188
|
+
node_type = node_config.get("type", "prompt")
|
|
189
|
+
print(f" - {node_name} ({node_type})")
|
|
190
|
+
|
|
191
|
+
# Show edges
|
|
192
|
+
print(f"\n Edges ({len(edges)}):")
|
|
193
|
+
for edge in edges:
|
|
194
|
+
from_node = edge.get("from", "?")
|
|
195
|
+
to_node = edge.get("to", "?")
|
|
196
|
+
condition = edge.get("condition", "")
|
|
197
|
+
if condition:
|
|
198
|
+
print(f" {from_node} → {to_node} (conditional)")
|
|
199
|
+
else:
|
|
200
|
+
print(f" {from_node} → {to_node}")
|
|
201
|
+
|
|
202
|
+
# Show required inputs if defined
|
|
203
|
+
inputs = config.get("inputs", {})
|
|
204
|
+
if inputs:
|
|
205
|
+
print(f"\n Inputs ({len(inputs)}):")
|
|
206
|
+
for input_name, input_config in inputs.items():
|
|
207
|
+
required = input_config.get("required", False)
|
|
208
|
+
default = input_config.get("default", None)
|
|
209
|
+
req_str = " (required)" if required else f" (default: {default})"
|
|
210
|
+
print(f" --var {input_name}=<value>{req_str}")
|
|
211
|
+
|
|
212
|
+
print()
|
|
213
|
+
|
|
214
|
+
except Exception as e:
|
|
215
|
+
print(f"❌ Error reading graph: {e}")
|
|
216
|
+
sys.exit(1)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _validate_required_fields(config: dict) -> tuple[list[str], list[str]]:
|
|
220
|
+
"""Validate required fields in graph config.
|
|
221
|
+
|
|
222
|
+
Args:
|
|
223
|
+
config: Parsed YAML configuration
|
|
224
|
+
|
|
225
|
+
Returns:
|
|
226
|
+
Tuple of (errors, warnings) lists
|
|
227
|
+
"""
|
|
228
|
+
errors = []
|
|
229
|
+
warnings = []
|
|
230
|
+
|
|
231
|
+
if not config.get("name"):
|
|
232
|
+
errors.append("Missing required field: name")
|
|
233
|
+
|
|
234
|
+
if not config.get("nodes"):
|
|
235
|
+
errors.append("Missing required field: nodes")
|
|
236
|
+
|
|
237
|
+
if not config.get("edges"):
|
|
238
|
+
warnings.append("No edges defined")
|
|
239
|
+
|
|
240
|
+
return errors, warnings
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def _validate_edges(edges: list[dict], node_names: set[str]) -> list[str]:
|
|
244
|
+
"""Validate edge references in graph config.
|
|
245
|
+
|
|
246
|
+
Args:
|
|
247
|
+
edges: List of edge configurations
|
|
248
|
+
node_names: Set of valid node names (including START/END)
|
|
249
|
+
|
|
250
|
+
Returns:
|
|
251
|
+
List of error messages
|
|
252
|
+
"""
|
|
253
|
+
errors = []
|
|
254
|
+
|
|
255
|
+
for i, edge in enumerate(edges):
|
|
256
|
+
from_node = edge.get("from", "")
|
|
257
|
+
to_node = edge.get("to", "")
|
|
258
|
+
|
|
259
|
+
if from_node not in node_names:
|
|
260
|
+
errors.append(f"Edge {i + 1}: unknown 'from' node '{from_node}'")
|
|
261
|
+
|
|
262
|
+
# Handle conditional edges where 'to' is a list
|
|
263
|
+
if isinstance(to_node, list):
|
|
264
|
+
for t in to_node:
|
|
265
|
+
if t not in node_names:
|
|
266
|
+
errors.append(f"Edge {i + 1}: unknown 'to' node '{t}'")
|
|
267
|
+
elif to_node not in node_names:
|
|
268
|
+
errors.append(f"Edge {i + 1}: unknown 'to' node '{to_node}'")
|
|
269
|
+
|
|
270
|
+
return errors
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def _validate_nodes(nodes: dict) -> list[str]:
|
|
274
|
+
"""Validate node configurations.
|
|
275
|
+
|
|
276
|
+
Args:
|
|
277
|
+
nodes: Dict of node_name -> node_config
|
|
278
|
+
|
|
279
|
+
Returns:
|
|
280
|
+
List of warning messages
|
|
281
|
+
"""
|
|
282
|
+
warnings = []
|
|
283
|
+
|
|
284
|
+
for node_name, node_config in nodes.items():
|
|
285
|
+
node_type = node_config.get("type", "llm")
|
|
286
|
+
if node_type == "agent" and not node_config.get("tools"):
|
|
287
|
+
warnings.append(f"Node '{node_name}': agent has no tools")
|
|
288
|
+
|
|
289
|
+
return warnings
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def _report_validation_result(
|
|
293
|
+
graph_path: Path,
|
|
294
|
+
config: dict,
|
|
295
|
+
errors: list[str],
|
|
296
|
+
warnings: list[str],
|
|
297
|
+
) -> None:
|
|
298
|
+
"""Report validation results and exit appropriately.
|
|
299
|
+
|
|
300
|
+
Args:
|
|
301
|
+
graph_path: Path to the graph file
|
|
302
|
+
config: Parsed graph configuration
|
|
303
|
+
errors: List of error messages
|
|
304
|
+
warnings: List of warning messages
|
|
305
|
+
"""
|
|
306
|
+
name = config.get("name", graph_path.stem)
|
|
307
|
+
nodes = config.get("nodes", {})
|
|
308
|
+
edges = config.get("edges", [])
|
|
309
|
+
|
|
310
|
+
if errors:
|
|
311
|
+
print(f"\n❌ {graph_path.name} ({name}) - INVALID\n")
|
|
312
|
+
for err in errors:
|
|
313
|
+
print(f" ✗ {err}")
|
|
314
|
+
for warn in warnings:
|
|
315
|
+
print(f" ⚠ {warn}")
|
|
316
|
+
print()
|
|
317
|
+
sys.exit(1)
|
|
318
|
+
elif warnings:
|
|
319
|
+
print(f"\n⚠️ {graph_path.name} ({name}) - VALID with warnings\n")
|
|
320
|
+
for warn in warnings:
|
|
321
|
+
print(f" ⚠ {warn}")
|
|
322
|
+
print()
|
|
323
|
+
else:
|
|
324
|
+
print(f"\n✅ {graph_path.name} ({name}) - VALID\n")
|
|
325
|
+
print(f" Nodes: {len(nodes)}")
|
|
326
|
+
print(f" Edges: {len(edges)}")
|
|
327
|
+
print()
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def cmd_graph_validate(args: Namespace) -> None:
|
|
331
|
+
"""Validate a graph YAML file.
|
|
332
|
+
|
|
333
|
+
Checks:
|
|
334
|
+
- File exists and is valid YAML
|
|
335
|
+
- Required fields present (name, nodes, edges)
|
|
336
|
+
- Node references are valid
|
|
337
|
+
- Edge references match existing nodes
|
|
338
|
+
"""
|
|
339
|
+
graph_path = Path(args.graph_path)
|
|
340
|
+
|
|
341
|
+
if not graph_path.exists():
|
|
342
|
+
print(f"❌ Graph file not found: {graph_path}")
|
|
343
|
+
sys.exit(1)
|
|
344
|
+
|
|
345
|
+
try:
|
|
346
|
+
with open(graph_path) as f:
|
|
347
|
+
config = yaml.safe_load(f)
|
|
348
|
+
|
|
349
|
+
# Run validations
|
|
350
|
+
errors, warnings = _validate_required_fields(config)
|
|
351
|
+
|
|
352
|
+
nodes = config.get("nodes", {})
|
|
353
|
+
edges = config.get("edges", [])
|
|
354
|
+
node_names = set(nodes.keys()) | {"START", "END"}
|
|
355
|
+
|
|
356
|
+
errors.extend(_validate_edges(edges, node_names))
|
|
357
|
+
warnings.extend(_validate_nodes(nodes))
|
|
358
|
+
|
|
359
|
+
# Report results
|
|
360
|
+
_report_validation_result(graph_path, config, errors, warnings)
|
|
361
|
+
|
|
362
|
+
except yaml.YAMLError as e:
|
|
363
|
+
print(f"❌ Invalid YAML: {e}")
|
|
364
|
+
sys.exit(1)
|
|
365
|
+
except Exception as e:
|
|
366
|
+
print(f"❌ Error validating graph: {e}")
|
|
367
|
+
sys.exit(1)
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def cmd_graph_dispatch(args: Namespace) -> None:
|
|
371
|
+
"""Dispatch to graph subcommands."""
|
|
372
|
+
if args.graph_command == "run":
|
|
373
|
+
cmd_graph_run(args)
|
|
374
|
+
elif args.graph_command == "list":
|
|
375
|
+
cmd_graph_list(args)
|
|
376
|
+
elif args.graph_command == "info":
|
|
377
|
+
cmd_graph_info(args)
|
|
378
|
+
elif args.graph_command == "validate":
|
|
379
|
+
cmd_graph_validate(args)
|
|
380
|
+
else:
|
|
381
|
+
print(f"Unknown graph command: {args.graph_command}")
|
|
382
|
+
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
|
+
]
|
yamlgraph/constants.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""Type-safe constants for YAML graph configuration.
|
|
2
|
+
|
|
3
|
+
Provides enums for node types, error handlers, and other magic strings
|
|
4
|
+
used throughout the codebase to enable static type checking and IDE support.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from enum import StrEnum
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class NodeType(StrEnum):
|
|
11
|
+
"""Valid node types in YAML graph configuration."""
|
|
12
|
+
|
|
13
|
+
LLM = "llm"
|
|
14
|
+
ROUTER = "router"
|
|
15
|
+
TOOL = "tool"
|
|
16
|
+
AGENT = "agent"
|
|
17
|
+
PYTHON = "python"
|
|
18
|
+
MAP = "map"
|
|
19
|
+
|
|
20
|
+
@classmethod
|
|
21
|
+
def requires_prompt(cls, node_type: str) -> bool:
|
|
22
|
+
"""Check if node type requires a prompt field.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
node_type: The node type string
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
True if the node type requires a prompt
|
|
29
|
+
"""
|
|
30
|
+
return node_type in (cls.LLM, cls.ROUTER)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class ErrorHandler(StrEnum):
|
|
34
|
+
"""Valid on_error handling strategies."""
|
|
35
|
+
|
|
36
|
+
SKIP = "skip" # Skip node and continue pipeline
|
|
37
|
+
RETRY = "retry" # Retry with max_retries attempts
|
|
38
|
+
FAIL = "fail" # Raise exception immediately
|
|
39
|
+
FALLBACK = "fallback" # Try fallback provider
|
|
40
|
+
|
|
41
|
+
@classmethod
|
|
42
|
+
def all_values(cls) -> set[str]:
|
|
43
|
+
"""Return all valid error handler values.
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
Set of valid error handler strings
|
|
47
|
+
"""
|
|
48
|
+
return {handler.value for handler in cls}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class EdgeType(StrEnum):
|
|
52
|
+
"""Valid edge types in graph configuration."""
|
|
53
|
+
|
|
54
|
+
SIMPLE = "simple" # Direct edge from -> to
|
|
55
|
+
CONDITIONAL = "conditional" # Edge with conditions
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class SpecialNodes(StrEnum):
|
|
59
|
+
"""Special node names with semantic meaning."""
|
|
60
|
+
|
|
61
|
+
START = "__start__"
|
|
62
|
+
END = "__end__"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
# Re-export for convenience
|
|
66
|
+
__all__ = ["NodeType", "ErrorHandler", "EdgeType", "SpecialNodes"]
|