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:
elpresidank 2026-04-05 21:09:49 -05:00
commit 74cc8a4685
1216 changed files with 116347 additions and 0 deletions

493
test-dialog-flow.py Normal file
View 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()