mirror of
https://github.com/trustgraph-ai/trustgraph.git
synced 2026-07-01 17:39:39 +02:00
Squashed 'ai-context/trustgraph-templates/' content from commit 42a5fd1b
git-subtree-dir: ai-context/trustgraph-templates git-subtree-split: 42a5fd1b678f32be378062e30451e2052ccb95dd
This commit is contained in:
commit
74cc8a4685
1216 changed files with 116347 additions and 0 deletions
493
test-dialog-flow.py
Normal file
493
test-dialog-flow.py
Normal file
|
|
@ -0,0 +1,493 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test harness for dialog flow - walks through selecting all default options,
|
||||
produces the resulting state object, and runs the JSONata transform.
|
||||
|
||||
Supports a test matrix mode where each field is tested with all its options
|
||||
while other fields use defaults.
|
||||
"""
|
||||
|
||||
import yaml
|
||||
import json
|
||||
import argparse
|
||||
import zipfile
|
||||
import io
|
||||
from pathlib import Path
|
||||
import jsonata
|
||||
import requests
|
||||
|
||||
|
||||
RESOURCES_DIR = Path(__file__).parent / "trustgraph_configurator/resources/dialog"
|
||||
|
||||
|
||||
def load_flow():
|
||||
"""Load the dialog flow YAML file."""
|
||||
with open(RESOURCES_DIR / "trustgraph-flow.yaml") as f:
|
||||
return yaml.safe_load(f)
|
||||
|
||||
|
||||
def load_jsonata_transform():
|
||||
"""Load the JSONata transform file."""
|
||||
with open(RESOURCES_DIR / "trustgraph-output.jsonata") as f:
|
||||
return f.read()
|
||||
|
||||
|
||||
def run_transform(state, transform_expr):
|
||||
"""Run the JSONata transform on the state object."""
|
||||
expr = jsonata.Jsonata(transform_expr)
|
||||
return expr.evaluate(state)
|
||||
|
||||
|
||||
def call_config_service(config):
|
||||
"""
|
||||
Call the configuration service to generate a deployment package.
|
||||
Returns (success, message, zip_contents) tuple.
|
||||
|
||||
The API expects just the templates array, not the full config object.
|
||||
"""
|
||||
url = config["api_url"]
|
||||
templates = config["templates"]
|
||||
|
||||
try:
|
||||
response = requests.post(
|
||||
url,
|
||||
json=templates,
|
||||
headers={"Content-Type": "application/json"},
|
||||
timeout=30
|
||||
)
|
||||
|
||||
if response.status_code != 200:
|
||||
return False, f"HTTP {response.status_code}: {response.text[:200]}", None
|
||||
|
||||
# Verify it's a valid ZIP
|
||||
try:
|
||||
zip_data = io.BytesIO(response.content)
|
||||
with zipfile.ZipFile(zip_data, 'r') as zf:
|
||||
file_list = zf.namelist()
|
||||
return True, f"ZIP with {len(file_list)} files", file_list
|
||||
except zipfile.BadZipFile:
|
||||
return False, "Response is not a valid ZIP file", None
|
||||
|
||||
except requests.exceptions.ConnectionError:
|
||||
return False, "Connection refused - is the service running?", None
|
||||
except requests.exceptions.Timeout:
|
||||
return False, "Request timed out", None
|
||||
except Exception as e:
|
||||
return False, f"Error: {e}", None
|
||||
|
||||
|
||||
def get_all_options(step):
|
||||
"""Get all possible values for a step's input."""
|
||||
input_def = step.get("input", {})
|
||||
input_type = input_def.get("type")
|
||||
|
||||
if input_type == "select":
|
||||
return [opt["value"] for opt in input_def.get("options", [])]
|
||||
elif input_type == "toggle":
|
||||
return [True, False]
|
||||
elif input_type == "number":
|
||||
# For numbers, just test default, min, and max
|
||||
default = input_def.get("default")
|
||||
min_val = input_def.get("min")
|
||||
max_val = input_def.get("max")
|
||||
values = []
|
||||
if min_val is not None:
|
||||
values.append(min_val)
|
||||
if default is not None and default not in values:
|
||||
values.append(default)
|
||||
if max_val is not None and max_val not in values:
|
||||
values.append(max_val)
|
||||
return values
|
||||
|
||||
return []
|
||||
|
||||
|
||||
def get_default_value(step):
|
||||
"""Get the default value for a step's input."""
|
||||
input_def = step.get("input", {})
|
||||
input_type = input_def.get("type")
|
||||
|
||||
if input_type == "select":
|
||||
options = input_def.get("options", [])
|
||||
# Find recommended option, or use first
|
||||
for opt in options:
|
||||
if opt.get("recommended"):
|
||||
return opt["value"]
|
||||
return options[0]["value"] if options else None
|
||||
|
||||
elif input_type == "number":
|
||||
return input_def.get("default")
|
||||
|
||||
elif input_type == "toggle":
|
||||
return input_def.get("default", False)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def evaluate_condition(condition, state):
|
||||
"""
|
||||
Evaluate a simple condition against the state.
|
||||
Supports: "key = value", "key = true/false", "key < 'version'"
|
||||
"""
|
||||
if not condition:
|
||||
return True
|
||||
|
||||
# Handle equality: "ocr.enabled = true"
|
||||
if " = " in condition:
|
||||
key, value = condition.split(" = ", 1)
|
||||
key = key.strip()
|
||||
value = value.strip()
|
||||
|
||||
# Get nested key
|
||||
state_value = state.get(key)
|
||||
|
||||
# Parse value
|
||||
if value == "true":
|
||||
return state_value is True
|
||||
elif value == "false":
|
||||
return state_value is False
|
||||
else:
|
||||
return str(state_value) == value
|
||||
|
||||
# Handle less-than for version comparisons: "version < '1.6.0'"
|
||||
if " < " in condition:
|
||||
key, value = condition.split(" < ", 1)
|
||||
key = key.strip()
|
||||
value = value.strip().strip("'\"")
|
||||
state_value = state.get(key, "")
|
||||
return str(state_value) < value
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def get_next_step(step, state):
|
||||
"""Determine the next step based on transitions and current state."""
|
||||
transitions = step.get("transitions", [])
|
||||
|
||||
for trans in transitions:
|
||||
when = trans.get("when")
|
||||
if when:
|
||||
if evaluate_condition(when, state):
|
||||
return trans.get("next")
|
||||
else:
|
||||
# Unconditional transition
|
||||
return trans.get("next")
|
||||
|
||||
return None # Terminal state
|
||||
|
||||
|
||||
def walk_flow(flow_data, overrides=None, verbose=True):
|
||||
"""
|
||||
Walk through the flow, return the state object.
|
||||
|
||||
Args:
|
||||
flow_data: The parsed dialog flow YAML
|
||||
overrides: Dict of {state_key: value} to override defaults
|
||||
verbose: Whether to print progress
|
||||
"""
|
||||
state = {}
|
||||
overrides = overrides or {}
|
||||
steps = flow_data.get("steps", {})
|
||||
current = flow_data.get("flow", {}).get("start")
|
||||
visited_steps = []
|
||||
|
||||
if verbose:
|
||||
print(f"Starting at: {current}")
|
||||
print("-" * 60)
|
||||
|
||||
while current:
|
||||
step = steps.get(current)
|
||||
if not step:
|
||||
if verbose:
|
||||
print(f"ERROR: Step '{current}' not found!")
|
||||
break
|
||||
|
||||
visited_steps.append(current)
|
||||
title = step.get("title", current)
|
||||
state_key = step.get("state_key")
|
||||
|
||||
# Get value - use override if present, otherwise default
|
||||
if state_key:
|
||||
if state_key in overrides:
|
||||
value = overrides[state_key]
|
||||
else:
|
||||
value = get_default_value(step)
|
||||
state[state_key] = value
|
||||
|
||||
if verbose:
|
||||
is_override = state_key in overrides
|
||||
marker = " [OVERRIDE]" if is_override else ""
|
||||
print(f"Step: {current}")
|
||||
print(f" Title: {title}")
|
||||
print(f" State key: {state_key} = {value}{marker}")
|
||||
else:
|
||||
if verbose:
|
||||
print(f"Step: {current}")
|
||||
print(f" Title: {title}")
|
||||
print(f" (no state key - review/terminal step)")
|
||||
|
||||
# Get next step
|
||||
next_step = get_next_step(step, state)
|
||||
if verbose:
|
||||
if next_step:
|
||||
print(f" -> Next: {next_step}")
|
||||
else:
|
||||
print(f" -> Terminal state")
|
||||
print()
|
||||
|
||||
current = next_step
|
||||
|
||||
return state, visited_steps
|
||||
|
||||
|
||||
def collect_fields_for_path(flow_data, overrides=None):
|
||||
"""
|
||||
Collect all fields and their possible values for a given path.
|
||||
Returns a list of (step_name, state_key, options, default_value) tuples.
|
||||
"""
|
||||
fields = []
|
||||
steps = flow_data.get("steps", {})
|
||||
|
||||
# Walk with given overrides to find the path
|
||||
_, visited = walk_flow(flow_data, overrides=overrides, verbose=False)
|
||||
|
||||
for step_name in visited:
|
||||
step = steps.get(step_name, {})
|
||||
state_key = step.get("state_key")
|
||||
if state_key:
|
||||
options = get_all_options(step)
|
||||
default = get_default_value(step)
|
||||
if len(options) > 1: # Only include fields with choices
|
||||
fields.append((step_name, state_key, options, default))
|
||||
|
||||
return fields
|
||||
|
||||
|
||||
def collect_all_fields(flow_data):
|
||||
"""
|
||||
Collect all fields from the baseline (default) path.
|
||||
"""
|
||||
return collect_fields_for_path(flow_data, overrides=None)
|
||||
|
||||
|
||||
def run_single_test(flow_data, transform_expr, overrides, description, test_num, results, call_api=False):
|
||||
"""Run a single test case and record the result."""
|
||||
print("-" * 70)
|
||||
print(f"Test {test_num}: {description}")
|
||||
print("-" * 70)
|
||||
|
||||
state, _ = walk_flow(flow_data, overrides=overrides, verbose=False)
|
||||
|
||||
try:
|
||||
config = run_transform(state, transform_expr)
|
||||
result = {
|
||||
"test": test_num,
|
||||
"description": description,
|
||||
"overrides": overrides,
|
||||
"state": state,
|
||||
"config": config
|
||||
}
|
||||
|
||||
print(f"State: {json.dumps(state, indent=2)}")
|
||||
print(f"Templates: {[t['name'] for t in config['templates']]}")
|
||||
|
||||
# Optionally call the configuration service
|
||||
if call_api:
|
||||
success, message, files = call_config_service(config)
|
||||
result["api_success"] = success
|
||||
result["api_message"] = message
|
||||
if files:
|
||||
result["api_files"] = files
|
||||
|
||||
if success:
|
||||
print(f"API: OK - {message}")
|
||||
else:
|
||||
print(f"API: FAILED - {message}")
|
||||
result["error"] = f"API: {message}"
|
||||
|
||||
results.append(result)
|
||||
|
||||
except Exception as e:
|
||||
results.append({
|
||||
"test": test_num,
|
||||
"description": description,
|
||||
"overrides": overrides,
|
||||
"state": state,
|
||||
"error": str(e)
|
||||
})
|
||||
print(f"State: {json.dumps(state, indent=2)}")
|
||||
print(f"ERROR: {e}")
|
||||
print()
|
||||
|
||||
return test_num + 1
|
||||
|
||||
|
||||
def run_test_matrix(flow_data, transform_expr, call_api=False):
|
||||
"""
|
||||
Run the test matrix - for each field, try all values while others use defaults.
|
||||
When a toggle enables conditional fields, also test all options of those fields.
|
||||
"""
|
||||
baseline_fields = collect_all_fields(flow_data)
|
||||
baseline_keys = {f[1] for f in baseline_fields}
|
||||
|
||||
print("=" * 70)
|
||||
print("TEST MATRIX" + (" (with API validation)" if call_api else ""))
|
||||
print("=" * 70)
|
||||
print()
|
||||
print(f"Found {len(baseline_fields)} fields with multiple options on baseline path:")
|
||||
for step_name, state_key, options, default in baseline_fields:
|
||||
print(f" - {state_key}: {len(options)} options (default: {default})")
|
||||
print()
|
||||
|
||||
results = []
|
||||
test_num = 1
|
||||
|
||||
# First, run the baseline (all defaults)
|
||||
test_num = run_single_test(
|
||||
flow_data, transform_expr,
|
||||
overrides={},
|
||||
description="BASELINE (all defaults)",
|
||||
test_num=test_num,
|
||||
results=results,
|
||||
call_api=call_api
|
||||
)
|
||||
|
||||
# For each field, try each non-default value
|
||||
for step_name, state_key, options, default in baseline_fields:
|
||||
for option in options:
|
||||
if option == default:
|
||||
continue # Skip default, already tested in baseline
|
||||
|
||||
overrides = {state_key: option}
|
||||
test_num = run_single_test(
|
||||
flow_data, transform_expr,
|
||||
overrides=overrides,
|
||||
description=f"{state_key} = {option}",
|
||||
test_num=test_num,
|
||||
results=results,
|
||||
call_api=call_api
|
||||
)
|
||||
|
||||
# Check if this override unlocks new fields (conditional paths)
|
||||
unlocked_fields = collect_fields_for_path(flow_data, overrides=overrides)
|
||||
unlocked_keys = {f[1] for f in unlocked_fields}
|
||||
new_keys = unlocked_keys - baseline_keys
|
||||
|
||||
if new_keys:
|
||||
# Test all non-default options of the newly unlocked fields
|
||||
for uf_step, uf_key, uf_options, uf_default in unlocked_fields:
|
||||
if uf_key not in new_keys:
|
||||
continue
|
||||
for uf_option in uf_options:
|
||||
if uf_option == uf_default:
|
||||
continue # Default already tested above
|
||||
|
||||
combined_overrides = {state_key: option, uf_key: uf_option}
|
||||
test_num = run_single_test(
|
||||
flow_data, transform_expr,
|
||||
overrides=combined_overrides,
|
||||
description=f"{state_key} = {option}, {uf_key} = {uf_option}",
|
||||
test_num=test_num,
|
||||
results=results,
|
||||
call_api=call_api
|
||||
)
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Test harness for dialog flow configuration"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--matrix", "-m",
|
||||
action="store_true",
|
||||
help="Run test matrix (each field with all options)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--api", "-a",
|
||||
action="store_true",
|
||||
help="Call the configuration service API to validate each config"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--summary", "-s",
|
||||
action="store_true",
|
||||
help="Show summary only (with --matrix)"
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
flow_data = load_flow()
|
||||
transform_expr = load_jsonata_transform()
|
||||
|
||||
if args.matrix:
|
||||
results = run_test_matrix(flow_data, transform_expr, call_api=args.api)
|
||||
|
||||
# Summary
|
||||
print("=" * 70)
|
||||
print("SUMMARY")
|
||||
print("=" * 70)
|
||||
print()
|
||||
passed = [r for r in results if "error" not in r]
|
||||
failed = [r for r in results if "error" in r]
|
||||
print(f"Total tests: {len(results)}")
|
||||
print(f"Passed: {len(passed)}")
|
||||
print(f"Failed: {len(failed)}")
|
||||
|
||||
if args.api:
|
||||
api_ok = [r for r in results if r.get("api_success")]
|
||||
api_fail = [r for r in results if "api_success" in r and not r["api_success"]]
|
||||
print(f"API OK: {len(api_ok)}")
|
||||
print(f"API Failed: {len(api_fail)}")
|
||||
|
||||
if failed:
|
||||
print()
|
||||
print("Failed tests:")
|
||||
for r in failed:
|
||||
print(f" - Test {r['test']}: {r['description']}")
|
||||
print(f" Error: {r['error']}")
|
||||
else:
|
||||
print("=" * 60)
|
||||
print("Dialog Flow Test Harness - Default Options")
|
||||
print("=" * 60)
|
||||
print()
|
||||
|
||||
state, _ = walk_flow(flow_data)
|
||||
|
||||
print("=" * 60)
|
||||
print("Final State Object:")
|
||||
print("=" * 60)
|
||||
print()
|
||||
print(json.dumps(state, indent=2))
|
||||
|
||||
# Run JSONata transform
|
||||
print()
|
||||
print("=" * 60)
|
||||
print("Running JSONata Transform...")
|
||||
print("=" * 60)
|
||||
print()
|
||||
|
||||
config = run_transform(state, transform_expr)
|
||||
|
||||
print("=" * 60)
|
||||
print("Configuration Object (output of transform):")
|
||||
print("=" * 60)
|
||||
print()
|
||||
print(json.dumps(config, indent=2))
|
||||
|
||||
# Optionally call the API
|
||||
if args.api:
|
||||
print()
|
||||
print("=" * 60)
|
||||
print("Calling Configuration Service...")
|
||||
print("=" * 60)
|
||||
print()
|
||||
success, message, files = call_config_service(config)
|
||||
if success:
|
||||
print(f"OK: {message}")
|
||||
print(f"Files: {files}")
|
||||
else:
|
||||
print(f"FAILED: {message}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Loading…
Add table
Add a link
Reference in a new issue