diff --git a/Cargo.lock b/Cargo.lock index 45c566b8..5b010c55 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1514,9 +1514,9 @@ checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" [[package]] name = "openssl" -version = "0.10.79" +version = "0.10.80" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf0b434746ee2832f4f0baf10137e1cabb18cbe6912c69e2e33263c45250f542" +checksum = "a45fa2aa886c42762255da344f0a0d313e254066c46aad76f300c3d3da62d967" dependencies = [ "bitflags", "cfg-if", @@ -1545,9 +1545,9 @@ checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" [[package]] name = "openssl-sys" -version = "0.9.115" +version = "0.9.116" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "158fe5b292746440aa6e7a7e690e55aeb72d41505e2804c23c6973ad0e9c9781" +checksum = "f28a22dc7140cda5f096e5e7724a6962ca81a7f8bfd2979f9b18c11af56318c4" dependencies = [ "cc", "libc", @@ -2321,7 +2321,7 @@ checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" [[package]] name = "simple-agent-type" -version = "0.5.1" +version = "0.5.2" dependencies = [ "async-trait", "futures-core", @@ -2334,7 +2334,7 @@ dependencies = [ [[package]] name = "simple-agents-core" -version = "0.5.1" +version = "0.5.2" dependencies = [ "async-trait", "futures-core", @@ -2350,7 +2350,7 @@ dependencies = [ [[package]] name = "simple-agents-examples" -version = "0.5.1" +version = "0.5.2" dependencies = [ "dotenv", "futures-util", @@ -2365,7 +2365,7 @@ dependencies = [ [[package]] name = "simple-agents-healing" -version = "0.5.1" +version = "0.5.2" dependencies = [ "criterion", "proptest", @@ -2380,7 +2380,7 @@ dependencies = [ [[package]] name = "simple-agents-napi" -version = "0.5.1" +version = "0.5.2" dependencies = [ "async-trait", "futures-util", @@ -2400,7 +2400,7 @@ dependencies = [ [[package]] name = "simple-agents-providers" -version = "0.5.1" +version = "0.5.2" dependencies = [ "async-trait", "bytes", @@ -2423,7 +2423,7 @@ dependencies = [ [[package]] name = "simple-agents-py" -version = "0.5.1" +version = "0.5.2" dependencies = [ "async-trait", "futures-util", @@ -2442,7 +2442,7 @@ dependencies = [ [[package]] name = "simple-agents-workflow" -version = "0.5.1" +version = "0.5.2" dependencies = [ "async-trait", "criterion", diff --git a/Cargo.toml b/Cargo.toml index afc934fb..aca83ce0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ members = ["crates/*", "examples"] resolver = "2" [workspace.package] -version = "0.5.1" +version = "0.5.2" edition = "2021" rust-version = "1.75" authors = ["SimpleAgents Contributors"] @@ -30,6 +30,6 @@ rand = "=0.8.6" futures = "=0.3.32" futures-core = "=0.3.32" futures-util = "=0.3.32" -simple-agent-type = { version = "0.5.1", path = "crates/simple-agent-type" } -simple-agents-healing = { version = "0.5.1", path = "crates/simple-agents-healing" } -simple-agents-providers = { version = "0.5.1", path = "crates/simple-agents-providers" } +simple-agent-type = { version = "0.5.2", path = "crates/simple-agent-type" } +simple-agents-healing = { version = "0.5.2", path = "crates/simple-agents-healing" } +simple-agents-providers = { version = "0.5.2", path = "crates/simple-agents-providers" } diff --git a/bindings/wasm/simple-agents-wasm/package-lock.json b/bindings/wasm/simple-agents-wasm/package-lock.json index bc3dcc2f..fd440793 100644 --- a/bindings/wasm/simple-agents-wasm/package-lock.json +++ b/bindings/wasm/simple-agents-wasm/package-lock.json @@ -1,12 +1,12 @@ { "name": "simple-agents-wasm", - "version": "0.5.1", + "version": "0.5.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "simple-agents-wasm", - "version": "0.5.1", + "version": "0.5.2", "license": "MIT OR Apache-2.0", "dependencies": { "yaml": "2.5.1" diff --git a/bindings/wasm/simple-agents-wasm/package.json b/bindings/wasm/simple-agents-wasm/package.json index ded461a4..014bc838 100644 --- a/bindings/wasm/simple-agents-wasm/package.json +++ b/bindings/wasm/simple-agents-wasm/package.json @@ -1,6 +1,6 @@ { "name": "simple-agents-wasm", - "version": "0.5.1", + "version": "0.5.2", "description": "Browser-compatible SimpleAgents client for OpenAI-compatible providers", "type": "module", "main": "index.js", diff --git a/bindings/wasm/simple-agents-wasm/rust/Cargo.lock b/bindings/wasm/simple-agents-wasm/rust/Cargo.lock index a213f449..04b1d4fd 100644 --- a/bindings/wasm/simple-agents-wasm/rust/Cargo.lock +++ b/bindings/wasm/simple-agents-wasm/rust/Cargo.lock @@ -195,7 +195,7 @@ dependencies = [ [[package]] name = "simple-agents-wasm-rust" -version = "0.5.1" +version = "0.5.2" dependencies = [ "js-sys", "serde", diff --git a/bindings/wasm/simple-agents-wasm/rust/Cargo.toml b/bindings/wasm/simple-agents-wasm/rust/Cargo.toml index 7ee22e77..4272705d 100644 --- a/bindings/wasm/simple-agents-wasm/rust/Cargo.toml +++ b/bindings/wasm/simple-agents-wasm/rust/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "simple-agents-wasm-rust" -version = "0.5.1" +version = "0.5.2" edition = "2021" license = "MIT OR Apache-2.0" diff --git a/crates/simple-agents-core/Cargo.toml b/crates/simple-agents-core/Cargo.toml index 2e1139e2..43f7ab21 100644 --- a/crates/simple-agents-core/Cargo.toml +++ b/crates/simple-agents-core/Cargo.toml @@ -22,7 +22,7 @@ tokio.workspace = true rand.workspace = true simple-agent-type = { workspace = true } -simple-agents-healing = { path = "../simple-agents-healing", version = "0.5.1" } +simple-agents-healing = { path = "../simple-agents-healing", version = "0.5.2" } [dev-dependencies] async-trait.workspace = true diff --git a/crates/simple-agents-napi/Cargo.toml b/crates/simple-agents-napi/Cargo.toml index a6a06f98..96061def 100644 --- a/crates/simple-agents-napi/Cargo.toml +++ b/crates/simple-agents-napi/Cargo.toml @@ -22,11 +22,11 @@ serde_json = { workspace = true } futures-util = { workspace = true } tracing = "=0.1.44" -simple-agents-core = { path = "../simple-agents-core", version = "0.5.1" } -simple-agents-providers = { path = "../simple-agents-providers", version = "0.5.1" } +simple-agents-core = { path = "../simple-agents-core", version = "0.5.2" } +simple-agents-providers = { path = "../simple-agents-providers", version = "0.5.2" } simple-agent-type = { workspace = true } -simple-agents-healing = { path = "../simple-agents-healing", version = "0.5.1" } -simple-agents-workflow = { path = "../simple-agents-workflow", version = "0.5.1" } +simple-agents-healing = { path = "../simple-agents-healing", version = "0.5.2" } +simple-agents-workflow = { path = "../simple-agents-workflow", version = "0.5.2" } [build-dependencies] napi-build = "=2.3.2" diff --git a/crates/simple-agents-napi/package-lock.json b/crates/simple-agents-napi/package-lock.json index 3862b3a2..41b66b91 100644 --- a/crates/simple-agents-napi/package-lock.json +++ b/crates/simple-agents-napi/package-lock.json @@ -1,12 +1,12 @@ { "name": "simple-agents-node", - "version": "0.5.1", + "version": "0.5.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "simple-agents-node", - "version": "0.5.1", + "version": "0.5.2", "devDependencies": { "@napi-rs/cli": "2.16.0", "dotenv": "16.6.1", diff --git a/crates/simple-agents-napi/package.json b/crates/simple-agents-napi/package.json index 78da396d..d9ba82e7 100644 --- a/crates/simple-agents-napi/package.json +++ b/crates/simple-agents-napi/package.json @@ -1,6 +1,6 @@ { "name": "simple-agents-node", - "version": "0.5.1", + "version": "0.5.2", "description": "Node.js bindings for SimpleAgents using napi-rs", "main": "index.js", "types": "index.d.ts", diff --git a/crates/simple-agents-providers/Cargo.toml b/crates/simple-agents-providers/Cargo.toml index eab0deed..dfe9ebb5 100644 --- a/crates/simple-agents-providers/Cargo.toml +++ b/crates/simple-agents-providers/Cargo.toml @@ -12,7 +12,7 @@ minimum-release-age = "182 days" [dependencies] simple-agent-type = { workspace = true } -simple-agents-healing = { path = "../simple-agents-healing", version = "0.5.1" } +simple-agents-healing = { path = "../simple-agents-healing", version = "0.5.2" } reqwest = { version = "=0.12.28", features = ["json", "stream", "default-tls", "http2", "gzip", "brotli", "deflate"], default-features = false } tokio = { version = "=1.52.3", features = ["full"] } futures = "=0.3.32" diff --git a/crates/simple-agents-py/Cargo.toml b/crates/simple-agents-py/Cargo.toml index 6b518371..85e7459b 100644 --- a/crates/simple-agents-py/Cargo.toml +++ b/crates/simple-agents-py/Cargo.toml @@ -23,8 +23,8 @@ futures-util = "=0.3.32" async-trait = "=0.1.89" tracing = "=0.1.44" -simple-agents-core = { path = "../simple-agents-core", version = "0.5.1" } -simple-agents-healing = { path = "../simple-agents-healing", version = "0.5.1" } -simple-agents-providers = { path = "../simple-agents-providers", version = "0.5.1" } +simple-agents-core = { path = "../simple-agents-core", version = "0.5.2" } +simple-agents-healing = { path = "../simple-agents-healing", version = "0.5.2" } +simple-agents-providers = { path = "../simple-agents-providers", version = "0.5.2" } simple-agent-type = { workspace = true } -simple-agents-workflow = { path = "../simple-agents-workflow", version = "0.5.1" } +simple-agents-workflow = { path = "../simple-agents-workflow", version = "0.5.2" } diff --git a/crates/simple-agents-py/pyproject.toml b/crates/simple-agents-py/pyproject.toml index 76303510..c1c8212b 100644 --- a/crates/simple-agents-py/pyproject.toml +++ b/crates/simple-agents-py/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "maturin" [project] name = "simple-agents-py" -version = "0.5.1" +version = "0.5.2" description = "Python bindings for SimpleAgents" readme = "README.md" requires-python = ">=3.9" diff --git a/crates/simple-agents-py/uv.lock b/crates/simple-agents-py/uv.lock index 14d42ca3..9d4ff1e2 100644 --- a/crates/simple-agents-py/uv.lock +++ b/crates/simple-agents-py/uv.lock @@ -263,7 +263,7 @@ wheels = [ [[package]] name = "simple-agents-py" -version = "0.5.1" +version = "0.5.2" source = { editable = "." } dependencies = [ { name = "pydantic" }, diff --git a/crates/simple-agents-workflow/Cargo.toml b/crates/simple-agents-workflow/Cargo.toml index b6a1977e..242bcc9d 100644 --- a/crates/simple-agents-workflow/Cargo.toml +++ b/crates/simple-agents-workflow/Cargo.toml @@ -21,7 +21,7 @@ serde_json.workspace = true serde_yaml = "=0.9.34" simple-agent-type.workspace = true simple-agents-healing.workspace = true -simple-agents-core = { path = "../simple-agents-core", version = "0.5.1" } +simple-agents-core = { path = "../simple-agents-core", version = "0.5.2" } thiserror.workspace = true tokio.workspace = true opentelemetry = { version = "=0.24.0", features = ["trace"] } diff --git a/crates/simple-agents-workflow/src/yaml_runner/client_executor.rs b/crates/simple-agents-workflow/src/yaml_runner/client_executor.rs index f75ed9d4..0a2bf849 100644 --- a/crates/simple-agents-workflow/src/yaml_runner/client_executor.rs +++ b/crates/simple-agents-workflow/src/yaml_runner/client_executor.rs @@ -88,7 +88,9 @@ impl<'a> YamlWorkflowLlmExecutor for BorrowedClientExecutor<'a> { if let Some(system_prompt) = node_system_prompt { assembled.push(Message::system(system_prompt)); } else { - assembled.push(Message::system("You execute workflow classification steps.")); + assembled.push(Message::system( + "You execute workflow classification steps.", + )); } if let Some(user_prompt) = user_input_prompt { assembled.push(Message::user(user_prompt)); diff --git a/crates/simple-agents-workflow/src/yaml_runner/tests.rs b/crates/simple-agents-workflow/src/yaml_runner/tests.rs index d05c59c8..3502b885 100644 --- a/crates/simple-agents-workflow/src/yaml_runner/tests.rs +++ b/crates/simple-agents-workflow/src/yaml_runner/tests.rs @@ -858,17 +858,13 @@ nodes: .get("user_input_prompt_bindings") .and_then(Value::as_array) .expect("bindings should be an array"); - assert!( - metadata - .get("user_input_prompt") - .and_then(Value::as_str) - .is_some_and(|value| value.contains("hello")) - ); - assert!( - metadata - .get("node_system_prompt") - .is_some_and(Value::is_null) - ); + assert!(metadata + .get("user_input_prompt") + .and_then(Value::as_str) + .is_some_and(|value| value.contains("hello"))); + assert!(metadata + .get("node_system_prompt") + .is_some_and(Value::is_null)); assert!(!bindings.is_empty()); let first_binding = &bindings[0]; @@ -1194,7 +1190,10 @@ nodes: .expect("workflow should execute"); let terminal = output.terminal_output.expect("terminal output present"); - assert_eq!(terminal.get("message_count").and_then(Value::as_u64), Some(2)); + assert_eq!( + terminal.get("message_count").and_then(Value::as_u64), + Some(2) + ); assert_eq!( terminal .get("has_user_input_prompt") @@ -1791,11 +1790,9 @@ nodes: match err { YamlWorkflowRunError::Validation { diagnostics, .. } => { - assert!( - diagnostics - .iter() - .any(|diagnostic| diagnostic.code == "missing_llm_input_source") - ); + assert!(diagnostics + .iter() + .any(|diagnostic| diagnostic.code == "missing_llm_input_source")); } other => panic!("expected validation error, got {other:?}"), } @@ -1819,7 +1816,8 @@ nodes: ok: { type: boolean } "#; - let err = serde_yaml::from_str::(yaml).expect_err("legacy prompt key should fail"); + let err = + serde_yaml::from_str::(yaml).expect_err("legacy prompt key should fail"); assert!(err.to_string().contains("unknown field `prompt`")); } @@ -1842,8 +1840,11 @@ nodes: ok: { type: boolean } "#; - let err = serde_yaml::from_str::(yaml).expect_err("legacy append flag should fail"); - assert!(err.to_string().contains("unknown field `append_prompt_as_user`")); + let err = + serde_yaml::from_str::(yaml).expect_err("legacy append flag should fail"); + assert!(err + .to_string() + .contains("unknown field `append_prompt_as_user`")); } #[tokio::test] diff --git a/examples/pyproject.toml b/examples/pyproject.toml index 6f60b4d9..7600f78a 100644 --- a/examples/pyproject.toml +++ b/examples/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "examples" -version = "0.5.1" +version = "0.5.2" description = "Add your description here" readme = "README.md" requires-python = ">=3.12" diff --git a/examples/python-test-simpleAgents/example_env.py b/examples/python-test-simpleAgents/example_env.py index a8e414c5..9bbf4a38 100644 --- a/examples/python-test-simpleAgents/example_env.py +++ b/examples/python-test-simpleAgents/example_env.py @@ -31,3 +31,8 @@ def require_env(name: str) -> str: "You can also export variables from ``examples/python-test-simpleAgents/.env`` manually." ) return value + + +def env_or_default(name: str, default: str) -> str: + load_monorepo_root_dotenv() + return os.environ.get(name, default) diff --git a/examples/python-test-simpleAgents/runners/run_email_ingestor.py b/examples/python-test-simpleAgents/runners/run_email_ingestor.py new file mode 100644 index 00000000..52bc1bbd --- /dev/null +++ b/examples/python-test-simpleAgents/runners/run_email_ingestor.py @@ -0,0 +1,159 @@ +"""Stream the email ingestor workflow (Langfuse/Jaeger tracing from repo root ``.env``). + +Setup (once):: + + cd examples/python-test-simpleAgents + uv sync + +Run (must use ``uv run``, not plain ``python``):: + + cd examples/python-test-simpleAgents + uv run python runners/run_email_ingestor.py "Please submit my expense receipts for reimbursement." + +Or interactively:: + + uv run python runners/run_email_ingestor.py + +Environment is loaded automatically from ``/.env`` (see ``example_env.py``). +Required keys: ``WORKFLOW_PROVIDER``, ``WORKFLOW_API_BASE``, ``WORKFLOW_API_KEY``. +""" + +from __future__ import annotations + +import argparse +import json +import os +import sys +from pathlib import Path + +_PACKAGE_ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(_PACKAGE_ROOT)) + +try: + from example_env import env_or_default, load_monorepo_root_dotenv, require_env + from example_paths import workflows + from simple_agents_py import Client + from simple_agents_py.workflow_request import ( + WorkflowExecutionFlags, + WorkflowExecutionRequest, + WorkflowMessage, + WorkflowRole, + WorkflowRunOptions, + WorkflowTelemetryConfig, + ) + from simple_agents_py.workflow_stream import default_on_event +except ModuleNotFoundError as exc: + if exc.name == "simple_agents_py": + raise SystemExit( + "simple_agents_py is not installed in this Python environment.\n\n" + "Install deps, then use uv run:\n" + " cd examples/python-test-simpleAgents\n" + " uv sync\n" + " uv run python runners/run_email_ingestor.py \"your email text here\"\n" + ) from exc + raise + +WORKFLOW_FILE = workflows("email-ingestor", "email-ingestor.yaml") +_REPO_ROOT = _PACKAGE_ROOT.parent.parent + + +def configure_jaeger_otel_from_env() -> bool: + """Enable OTLP export; return False only if ``JAEGER_OTEL`` is explicitly false.""" + if os.environ.get("JAEGER_OTEL", "").strip().lower() in ("0", "false", "no", "off"): + return False + + os.environ["SIMPLE_AGENTS_TRACING_ENABLED"] = "true" + if not (os.environ.get("OTEL_EXPORTER_OTLP_ENDPOINT") or "").strip(): + os.environ["OTEL_EXPORTER_OTLP_ENDPOINT"] = "http://localhost:4317" + if not (os.environ.get("OTEL_EXPORTER_OTLP_PROTOCOL") or "").strip(): + os.environ["OTEL_EXPORTER_OTLP_PROTOCOL"] = "grpc" + if not (os.environ.get("OTEL_SERVICE_NAME") or "").strip(): + os.environ["OTEL_SERVICE_NAME"] = "simple-agents-email-ingestor" + + print( + "OTLP tracing: " + f"endpoint={os.environ['OTEL_EXPORTER_OTLP_ENDPOINT']} " + f"protocol={os.environ['OTEL_EXPORTER_OTLP_PROTOCOL']} " + f"service={os.environ.get('OTEL_SERVICE_NAME', '')}", + file=sys.stderr, + ) + return True + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser( + description="Classify an incoming email string with the email-ingestor workflow.", + ) + parser.add_argument( + "email", + nargs="?", + help="Email text to classify (omit to be prompted interactively)", + ) + parser.add_argument( + "--model", + default=os.environ.get("WORKFLOW_MODEL", "").strip() + or env_or_default("CUSTOM_API_MODEL", ""), + help="Override all LLM node models (YAML default: azure/gpt-4.1-mini@eastus2). " + "Set WORKFLOW_MODEL in repo root .env when your API router rejects the @eastus2 id.", + ) + return parser.parse_args() + + +def main() -> None: + load_monorepo_root_dotenv() + telemetry_on = configure_jaeger_otel_from_env() + if not telemetry_on: + print("OTLP tracing disabled (JAEGER_OTEL=false).", file=sys.stderr) + + env_file = _REPO_ROOT / ".env" + print(f"Env file: {env_file} ({'found' if env_file.is_file() else 'missing'})", file=sys.stderr) + print(f"Workflow: {WORKFLOW_FILE}", file=sys.stderr) + + client = Client( + require_env("WORKFLOW_PROVIDER"), + api_base=require_env("WORKFLOW_API_BASE"), + api_key=require_env("WORKFLOW_API_KEY"), + ) + print(f"API base: {require_env('WORKFLOW_API_BASE')}", file=sys.stderr) + + args = parse_args() + user_input = (args.email or input("Paste incoming email text: ")).strip() + if not user_input: + raise SystemExit("No email text provided.") + + model_override = (args.model or "").strip() or None + if model_override: + print(f"Model override: {model_override}", file=sys.stderr) + else: + print("Model: azure/gpt-4.1-mini@eastus2 (from workflow YAML)", file=sys.stderr) + + workflow_options = WorkflowRunOptions( + model=model_override, + telemetry=WorkflowTelemetryConfig(enabled=True, nerdstats=True), + ) + if telemetry_on: + pass + elif model_override: + workflow_options = WorkflowRunOptions(model=model_override) + else: + workflow_options = None + + req = WorkflowExecutionRequest( + workflow_path=str(WORKFLOW_FILE), + messages=[WorkflowMessage(role=WorkflowRole.USER, content=user_input)], + execution=WorkflowExecutionFlags( + node_llm_streaming=True, + split_stream_deltas=False, + ), + workflow_options=workflow_options, + ) + + print("\n--- streaming LLM output ---", file=sys.stderr) + result = client.stream_workflow(req, on_event=default_on_event) + + print("\n\n--- final result ---") + print(json.dumps(result.to_dict(), indent=2)) + + +if __name__ == "__main__": + main() diff --git a/examples/python-test-simpleAgents/runners/run_model_router.py b/examples/python-test-simpleAgents/runners/run_model_router.py new file mode 100644 index 00000000..f9b235b4 --- /dev/null +++ b/examples/python-test-simpleAgents/runners/run_model_router.py @@ -0,0 +1,80 @@ +"""Stream a YAML workflow with live events (Pydantic request). + +From ``examples/``: ``uv sync`` (workspace member; local ``simple-agents-py``). + +LLM nodes in the YAML should use stream: true if you want token deltas. +""" + +from __future__ import annotations + +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) + +from example_env import require_env +from example_paths import workflows +from simple_agents_py import Client +from simple_agents_py.workflow_request import ( + WorkflowExecutionFlags, + WorkflowExecutionRequest, + WorkflowMessage, + WorkflowRole, +) +from simple_agents_py.workflow_stream import WorkflowStreamEvent + + +def default_on_event(event: WorkflowStreamEvent) -> None: + """Print streamed tokens to stdout; log structured snapshots to stderr. + + A ready-made ``on_event`` callback suitable for quick scripts and demos. + Pass it directly wherever a callback is accepted:: + + from simple_agents_py.workflow_stream import default_on_event + client.stream_workflow(payload, on_event=default_on_event) + + Prints ``node_stream_delta``, ``node_stream_thinking_delta``, and + ``node_stream_output_delta`` tokens inline on **stdout** (no newline between + tokens). Emits a single line per ``node_stream_snapshot`` event on **stderr** + (healing / structured JSON snapshot progress: node id, optional metadata, JSON + preview). Silently ignores ``workflow_started`` and ``workflow_completed``; + all other event types are also silently ignored by this handler. + """ + print(event) + + +workflow_file = workflows("model-router", "model-router.yaml") +# workflow_file = workflows("friendly", "friendly.yaml") + + +def main() -> None: + client = Client( + require_env("WORKFLOW_PROVIDER"), + api_base=require_env("WORKFLOW_API_BASE"), + api_key=require_env("WORKFLOW_API_KEY"), + ) + + user_input = input("Enter your Input: ") + + req = WorkflowExecutionRequest( + workflow_path=str(workflow_file), + messages=[WorkflowMessage(role=WorkflowRole.USER, content=user_input)], + execution=WorkflowExecutionFlags( + node_llm_streaming=True, + split_stream_deltas=False, + ), + ) + + result = client.stream_workflow( + req, + on_event=default_on_event, + ) + + print("\n") + import json + + print(json.dumps(result.to_dict(), indent=2)) + + +if __name__ == "__main__": + main() diff --git a/examples/python-test-simpleAgents/workflows/email-ingestor/email-ingestor.yaml b/examples/python-test-simpleAgents/workflows/email-ingestor/email-ingestor.yaml new file mode 100644 index 00000000..4f0a0fca --- /dev/null +++ b/examples/python-test-simpleAgents/workflows/email-ingestor/email-ingestor.yaml @@ -0,0 +1,656 @@ +# Email Ingestor — hierarchical classification with explicit subtype routing +# Top-level: HR | Finance | Education +# Each subtype has its own switch branch and downstream path. + +id: email-ingestor +version: 1.0.1 + +metadata: + name: "Email Ingestor" + description: "Classify incoming email text into HR, Finance, or Education with explicit per-subtype routing." + tags: [example, email-ingestor, email-classification, hr, finance, education] + +entry_node: detect_email_domain + +nodes: + - id: detect_email_domain + name: "Detect Email Domain" + node_type: + llm_call: + model: azure/gpt-4.1-mini@eastus2 + temperature: 0.2 + messages_path: input.messages + stream: true + heal: true + config: + output_schema: + type: object + properties: + domain: + type: string + enum: [hr, finance, education] + reason: + type: string + required: [domain, reason] + additionalProperties: false + user_input_prompt: | + Classify the latest user-provided email content into exactly one top-level domain. + + Return JSON only: + { + "domain": "hr" | "finance" | "education", + "reason": "short explanation" + } + + Rules: + - Use the latest user message as the email content to classify. + - Choose "hr" for employee lifecycle, hiring, interviews, termination, onboarding, or people operations. + - Choose "finance" for reimbursement, invoicing, tax, billing, payments, or accounting topics. + - Choose "education" for employee education programs, internships, college admissions for job experience, training, or academic programs. + - Pick the single best domain even if the email mentions adjacent context. + - Do not invent details not present in the email. + + - id: route_email_domain + name: "Route Email Domain" + node_type: + switch: + branches: + - condition: '$.nodes.detect_email_domain.output.domain == "hr"' + target: detect_hr_subtype + - condition: '$.nodes.detect_email_domain.output.domain == "finance"' + target: detect_finance_subtype + default: detect_education_subtype + + # --- HR: detect → route → finalize per subtype --- + + - id: detect_hr_subtype + name: "Detect HR Subtype" + node_type: + llm_call: + model: azure/gpt-4.1-mini@eastus2 + temperature: 0.2 + messages_path: input.messages + stream: true + heal: true + config: + output_schema: + type: object + properties: + hr_subtype: + type: string + enum: [employee_termination, employee_onboarding, interview_scheduling] + reason: + type: string + required: [hr_subtype, reason] + additionalProperties: false + user_input_prompt: | + The latest user message has already been classified as HR. + Classify it into exactly one HR subtype. + + Return JSON only: + { + "hr_subtype": "employee_termination" | "employee_onboarding" | "interview_scheduling", + "reason": "short explanation" + } + + Rules: + - employee_termination: resignations, exit interviews, offboarding, termination, final paycheck, separation. + - employee_onboarding: new hire setup, first-day logistics, badge/laptop provisioning, orientation, I-9, welcome emails. + - interview_scheduling: scheduling or rescheduling interviews, panel coordination, candidate availability, interview loops. + - Pick the single best subtype from the enum. + + - id: route_hr_subtype + name: "Route HR Subtype" + node_type: + switch: + branches: + - condition: '$.nodes.detect_hr_subtype.output.hr_subtype == "employee_termination"' + target: finalize_hr_employee_termination + - condition: '$.nodes.detect_hr_subtype.output.hr_subtype == "employee_onboarding"' + target: finalize_hr_employee_onboarding + - condition: '$.nodes.detect_hr_subtype.output.hr_subtype == "interview_scheduling"' + target: finalize_hr_interview_scheduling + default: finalize_hr_employee_onboarding + + - id: finalize_hr_employee_termination + name: "Finalize HR — Employee Termination" + node_type: + llm_call: + model: azure/gpt-4.1-mini@eastus2 + temperature: 0.2 + messages_path: input.messages + stream: true + heal: true + config: + output_schema: + type: object + properties: + top_level_category: + type: string + enum: [hr] + subtype: + type: string + enum: [employee_termination] + label: + type: string + enum: [hr/employee_termination] + reason: + type: string + required: [top_level_category, subtype, label, reason] + additionalProperties: false + user_input_prompt: | + Produce the final normalized classification for an HR employee-termination email. + + Return JSON only: + { + "top_level_category": "hr", + "subtype": "employee_termination", + "label": "hr/employee_termination", + "reason": "short explanation" + } + + Detected HR subtype reason: {{ nodes.detect_hr_subtype.output.reason }} + Top-level domain reason: {{ nodes.detect_email_domain.output.reason }} + + - id: finalize_hr_employee_onboarding + name: "Finalize HR — Employee Onboarding" + node_type: + llm_call: + model: azure/gpt-4.1-mini@eastus2 + temperature: 0.2 + messages_path: input.messages + stream: true + heal: true + config: + output_schema: + type: object + properties: + top_level_category: + type: string + enum: [hr] + subtype: + type: string + enum: [employee_onboarding] + label: + type: string + enum: [hr/employee_onboarding] + reason: + type: string + required: [top_level_category, subtype, label, reason] + additionalProperties: false + user_input_prompt: | + Produce the final normalized classification for an HR employee-onboarding email. + + Return JSON only: + { + "top_level_category": "hr", + "subtype": "employee_onboarding", + "label": "hr/employee_onboarding", + "reason": "short explanation" + } + + Detected HR subtype reason: {{ nodes.detect_hr_subtype.output.reason }} + Top-level domain reason: {{ nodes.detect_email_domain.output.reason }} + + - id: finalize_hr_interview_scheduling + name: "Finalize HR — Interview Scheduling" + node_type: + llm_call: + model: azure/gpt-4.1-mini@eastus2 + temperature: 0.2 + messages_path: input.messages + stream: true + heal: true + config: + output_schema: + type: object + properties: + top_level_category: + type: string + enum: [hr] + subtype: + type: string + enum: [interview_scheduling] + label: + type: string + enum: [hr/interview_scheduling] + reason: + type: string + required: [top_level_category, subtype, label, reason] + additionalProperties: false + user_input_prompt: | + Produce the final normalized classification for an HR interview-scheduling email. + + Return JSON only: + { + "top_level_category": "hr", + "subtype": "interview_scheduling", + "label": "hr/interview_scheduling", + "reason": "short explanation" + } + + Detected HR subtype reason: {{ nodes.detect_hr_subtype.output.reason }} + Top-level domain reason: {{ nodes.detect_email_domain.output.reason }} + + # --- Finance: detect → route → audit → finalize per subtype --- + + - id: detect_finance_subtype + name: "Detect Finance Subtype" + node_type: + llm_call: + model: azure/gpt-4.1-mini@eastus2 + temperature: 0.2 + messages_path: input.messages + stream: true + heal: true + config: + output_schema: + type: object + properties: + finance_subtype: + type: string + enum: [reimbursements, invoicing, tax] + reason: + type: string + required: [finance_subtype, reason] + additionalProperties: false + user_input_prompt: | + The latest user message has already been classified as finance. + Classify it into exactly one finance subtype. + + Return JSON only: + { + "finance_subtype": "reimbursements" | "invoicing" | "tax", + "reason": "short explanation" + } + + Rules: + - reimbursements: expense claims, receipt submissions, travel reimbursement, employee repayment requests. + - invoicing: vendor invoices, bills, payable notices, billing statements, supplier payment requests. + - tax: tax forms, withholding, W-2/1099, GST/VAT/sales tax, compliance, or tax review questions. + - Pick the single best subtype from the enum. + + - id: route_finance_subtype + name: "Route Finance Subtype" + node_type: + switch: + branches: + - condition: '$.nodes.detect_finance_subtype.output.finance_subtype == "reimbursements"' + target: audit_reimbursements + - condition: '$.nodes.detect_finance_subtype.output.finance_subtype == "invoicing"' + target: audit_invoicing + - condition: '$.nodes.detect_finance_subtype.output.finance_subtype == "tax"' + target: audit_tax + default: audit_reimbursements + + - id: audit_reimbursements + name: "Financial Audit — Reimbursements" + node_type: + custom_worker: + handler: financial_audit + handler_file: handlers.py + config: + payload: + finance_subtype: reimbursements + classification_reason: "{{ nodes.detect_finance_subtype.output.reason }}" + + - id: audit_invoicing + name: "Financial Audit — Invoicing" + node_type: + custom_worker: + handler: financial_audit + handler_file: handlers.py + config: + payload: + finance_subtype: invoicing + classification_reason: "{{ nodes.detect_finance_subtype.output.reason }}" + + - id: audit_tax + name: "Financial Audit — Tax" + node_type: + custom_worker: + handler: financial_audit + handler_file: handlers.py + config: + payload: + finance_subtype: tax + classification_reason: "{{ nodes.detect_finance_subtype.output.reason }}" + + - id: finalize_finance_reimbursements + name: "Finalize Finance — Reimbursements" + node_type: + llm_call: + model: azure/gpt-4.1-mini@eastus2 + temperature: 0.2 + messages_path: input.messages + stream: true + heal: true + config: + output_schema: + type: object + properties: + top_level_category: + type: string + enum: [finance] + subtype: + type: string + enum: [reimbursements] + label: + type: string + enum: [finance/reimbursements] + audit_status: + type: string + audit_message: + type: string + reason: + type: string + required: + [top_level_category, subtype, label, audit_status, audit_message, reason] + additionalProperties: false + user_input_prompt: | + Produce the final normalized classification for a finance reimbursements email. + + Return JSON only: + { + "top_level_category": "finance", + "subtype": "reimbursements", + "label": "finance/reimbursements", + "audit_status": "recorded", + "audit_message": "message from financial audit system", + "reason": "short explanation" + } + + Finance detection reason: {{ nodes.detect_finance_subtype.output.reason }} + Financial audit result: {{ nodes.audit_reimbursements.output }} + + - id: finalize_finance_invoicing + name: "Finalize Finance — Invoicing" + node_type: + llm_call: + model: azure/gpt-4.1-mini@eastus2 + temperature: 0.2 + messages_path: input.messages + stream: true + heal: true + config: + output_schema: + type: object + properties: + top_level_category: + type: string + enum: [finance] + subtype: + type: string + enum: [invoicing] + label: + type: string + enum: [finance/invoicing] + audit_status: + type: string + audit_message: + type: string + reason: + type: string + required: + [top_level_category, subtype, label, audit_status, audit_message, reason] + additionalProperties: false + user_input_prompt: | + Produce the final normalized classification for a finance invoicing email. + + Return JSON only: + { + "top_level_category": "finance", + "subtype": "invoicing", + "label": "finance/invoicing", + "audit_status": "recorded", + "audit_message": "message from financial audit system", + "reason": "short explanation" + } + + Finance detection reason: {{ nodes.detect_finance_subtype.output.reason }} + Financial audit result: {{ nodes.audit_invoicing.output }} + + - id: finalize_finance_tax + name: "Finalize Finance — Tax" + node_type: + llm_call: + model: azure/gpt-4.1-mini@eastus2 + temperature: 0.2 + messages_path: input.messages + stream: true + heal: true + config: + output_schema: + type: object + properties: + top_level_category: + type: string + enum: [finance] + subtype: + type: string + enum: [tax] + label: + type: string + enum: [finance/tax] + audit_status: + type: string + audit_message: + type: string + reason: + type: string + required: + [top_level_category, subtype, label, audit_status, audit_message, reason] + additionalProperties: false + user_input_prompt: | + Produce the final normalized classification for a finance tax email. + + Return JSON only: + { + "top_level_category": "finance", + "subtype": "tax", + "label": "finance/tax", + "audit_status": "recorded", + "audit_message": "message from financial audit system", + "reason": "short explanation" + } + + Finance detection reason: {{ nodes.detect_finance_subtype.output.reason }} + Financial audit result: {{ nodes.audit_tax.output }} + + # --- Education: detect → route → finalize per subtype --- + + - id: detect_education_subtype + name: "Detect Education Subtype" + node_type: + llm_call: + model: azure/gpt-4.1-mini@eastus2 + temperature: 0.2 + messages_path: input.messages + stream: true + heal: true + config: + output_schema: + type: object + properties: + education_subtype: + type: string + enum: + [ + employee_education_program, + internship, + college_admissions_job_exp, + ] + reason: + type: string + required: [education_subtype, reason] + additionalProperties: false + user_input_prompt: | + The latest user message has already been classified as education. + Classify it into exactly one education subtype. + + Return JSON only: + { + "education_subtype": "employee_education_program" | "internship" | "college_admissions_job_exp", + "reason": "short explanation" + } + + Rules: + - employee_education_program: tuition assistance, internal training programs, certification sponsorship, employee upskilling or education benefits. + - internship: internship applications, intern program coordination, intern onboarding or scheduling. + - college_admissions_job_exp: college admissions programs tied to job experience, co-op placements, work-study, or academic-to-career bridge programs. + - Pick the single best subtype from the enum. + + - id: route_education_subtype + name: "Route Education Subtype" + node_type: + switch: + branches: + - condition: '$.nodes.detect_education_subtype.output.education_subtype == "employee_education_program"' + target: finalize_education_employee_education_program + - condition: '$.nodes.detect_education_subtype.output.education_subtype == "internship"' + target: finalize_education_internship + - condition: '$.nodes.detect_education_subtype.output.education_subtype == "college_admissions_job_exp"' + target: finalize_education_college_admissions_job_exp + default: finalize_education_employee_education_program + + - id: finalize_education_employee_education_program + name: "Finalize Education — Employee Education Program" + node_type: + llm_call: + model: azure/gpt-4.1-mini@eastus2 + temperature: 0.2 + messages_path: input.messages + stream: true + heal: true + config: + output_schema: + type: object + properties: + top_level_category: + type: string + enum: [education] + subtype: + type: string + enum: [employee_education_program] + label: + type: string + enum: [education/employee_education_program] + reason: + type: string + required: [top_level_category, subtype, label, reason] + additionalProperties: false + user_input_prompt: | + Produce the final normalized classification for an employee education program email. + + Return JSON only: + { + "top_level_category": "education", + "subtype": "employee_education_program", + "label": "education/employee_education_program", + "reason": "short explanation" + } + + Detected education subtype reason: {{ nodes.detect_education_subtype.output.reason }} + Top-level domain reason: {{ nodes.detect_email_domain.output.reason }} + + - id: finalize_education_internship + name: "Finalize Education — Internship" + node_type: + llm_call: + model: azure/gpt-4.1-mini@eastus2 + temperature: 0.2 + messages_path: input.messages + stream: true + heal: true + config: + output_schema: + type: object + properties: + top_level_category: + type: string + enum: [education] + subtype: + type: string + enum: [internship] + label: + type: string + enum: [education/internship] + reason: + type: string + required: [top_level_category, subtype, label, reason] + additionalProperties: false + user_input_prompt: | + Produce the final normalized classification for an internship email. + + Return JSON only: + { + "top_level_category": "education", + "subtype": "internship", + "label": "education/internship", + "reason": "short explanation" + } + + Detected education subtype reason: {{ nodes.detect_education_subtype.output.reason }} + Top-level domain reason: {{ nodes.detect_email_domain.output.reason }} + + - id: finalize_education_college_admissions_job_exp + name: "Finalize Education — College Admissions Job Experience" + node_type: + llm_call: + model: azure/gpt-4.1-mini@eastus2 + temperature: 0.2 + messages_path: input.messages + stream: true + heal: true + config: + output_schema: + type: object + properties: + top_level_category: + type: string + enum: [education] + subtype: + type: string + enum: [college_admissions_job_exp] + label: + type: string + enum: [education/college_admissions_job_exp] + reason: + type: string + required: [top_level_category, subtype, label, reason] + additionalProperties: false + user_input_prompt: | + Produce the final normalized classification for a college admissions job-experience program email. + + Return JSON only: + { + "top_level_category": "education", + "subtype": "college_admissions_job_exp", + "label": "education/college_admissions_job_exp", + "reason": "short explanation" + } + + Detected education subtype reason: {{ nodes.detect_education_subtype.output.reason }} + Top-level domain reason: {{ nodes.detect_email_domain.output.reason }} + +edges: + - from: detect_email_domain + to: route_email_domain + + - from: detect_hr_subtype + to: route_hr_subtype + + - from: detect_finance_subtype + to: route_finance_subtype + + - from: audit_reimbursements + to: finalize_finance_reimbursements + + - from: audit_invoicing + to: finalize_finance_invoicing + + - from: audit_tax + to: finalize_finance_tax + + - from: detect_education_subtype + to: route_education_subtype diff --git a/examples/python-test-simpleAgents/workflows/email-ingestor/handlers.py b/examples/python-test-simpleAgents/workflows/email-ingestor/handlers.py new file mode 100644 index 00000000..ea005a0f --- /dev/null +++ b/examples/python-test-simpleAgents/workflows/email-ingestor/handlers.py @@ -0,0 +1,38 @@ +"""Custom workers for the email ingestor workflow.""" + +from __future__ import annotations + +from typing import Any + +_AUDIT_MESSAGES: dict[str, str] = { + "reimbursements": ( + "Reimbursement claim logged in financial audit system — " + "pending policy review." + ), + "invoicing": ( + "Vendor invoice captured in financial audit system — " + "routed to accounts payable queue." + ), + "tax": ( + "Tax-related correspondence recorded in financial audit system — " + "flagged for compliance review." + ), +} + + +def financial_audit( + *, context: dict[str, Any], payload: dict[str, Any] +) -> dict[str, Any]: + """Route classified finance emails into the (stub) financial audit subsystem.""" + subtype = str(payload.get("finance_subtype") or "unknown").strip().lower() + print(f"[financial_audit] hit financial audit system (subtype={subtype})") + + audit_message = _AUDIT_MESSAGES.get( + subtype, + f"Finance email ({subtype}) recorded in financial audit system.", + ) + return { + "audit_status": "recorded", + "audit_message": audit_message, + "finance_subtype": subtype, + } diff --git a/examples/python-test-simpleAgents/workflows/model-router/model-router.yaml b/examples/python-test-simpleAgents/workflows/model-router/model-router.yaml new file mode 100644 index 00000000..832399d4 --- /dev/null +++ b/examples/python-test-simpleAgents/workflows/model-router/model-router.yaml @@ -0,0 +1,563 @@ +# Model router: classify dev task → d-flash sub-tree → execute with routing summary. +# +# Local run (OpenAI-compatible router on :8080): +# cd examples/python-test-simpleAgents && uv run python runners/run_model_router.py +# +# Routing table: +# frontend → mlx-community/LFM2-8B-A1B-4bit +# backend → mlx-community/LFM2-8B-A1B-4bit +# devops → mlx-community/LFM2-8B-A1B-4bit +# architecture → mlx-community/LFM2-8B-A1B-4bit +# +# d-flash sub-tree: if chat history shows the same implementation was already +# produced, route to d_flash execute nodes; otherwise full implementation nodes. +# Terminal execute nodes return what was chosen (task, model, d-flash, mode). + +id: dev-task-model-router +version: 1.0.0 + +metadata: + name: "Dev Task Model Router" + description: "Route developer queries to Gemma/Qwen models with d-flash repeat detection" + tags: ["example", "model-router", "routing", "d-flash"] + +entry_node: classify_task_domain + +nodes: + - id: classify_task_domain + name: "Classify Task Domain" + node_type: + llm_call: + model: mlx-community/LFM2-8B-A1B-4bit + temperature: 0.2 + messages_path: input.messages + heal: true + config: + output_schema: + type: object + properties: + task_category: + type: string + enum: [frontend, backend, devops, architecture] + reason: + type: string + required: [task_category, reason] + additionalProperties: false + set_globals: + task_category: nodes.classify_task_domain.output.task_category + user_input_prompt: | + Classify the latest user message into exactly one developer task domain. + + Return JSON only: + { + "task_category": "frontend" | "backend" | "devops" | "architecture", + "reason": "short explanation" + } + + Rules: + - frontend: UI, React/Vue/CSS/HTML, components, styling, client-side state, browser APIs. + - backend: APIs, databases, services, auth, business logic, server frameworks. + - devops: CI/CD, Docker, Kubernetes, Terraform, deployment, monitoring, infra pipelines. + - architecture: system design, trade-offs, diagrams, scalability, boundaries, ADRs. + - Use the latest user message; use prior turns only for disambiguation. + - Pick the single best category. + + - id: detect_repeat_implementation + name: "Detect Repeat Implementation (d-flash gate)" + node_type: + llm_call: + model: mlx-community/LFM2-8B-A1B-4bit + temperature: 0.1 + messages_path: input.messages + heal: true + config: + output_schema: + type: object + properties: + same_implementation_in_history: + type: string + enum: ["yes", "no"] + d_flash_enabled: + type: string + enum: ["true", "false"] + matched_turn_summary: + type: string + reason: + type: string + required: + [same_implementation_in_history, d_flash_enabled, matched_turn_summary, reason] + additionalProperties: false + set_globals: + d_flash_enabled: nodes.detect_repeat_implementation.output.d_flash_enabled + user_input_prompt: | + Inspect the full conversation in the messages above. + + Task domain for this turn: {{ globals.task_category }} + Routing reason: {{ nodes.classify_task_domain.output.reason }} + + Decide whether the user is asking for essentially the SAME implementation + that was already produced in an earlier assistant turn (same feature, same + code pattern, same infra stack — not merely the same topic). + + Set d_flash_enabled="true" only when a prior assistant answer already contains + the same implementation and the user wants a repeat, tweak, or fast follow-up. + Set d_flash_enabled="false" for net-new work or materially different scope. + + Return JSON only: + { + "same_implementation_in_history": "yes" | "no", + "d_flash_enabled": "true" | "false", + "matched_turn_summary": "brief note or none", + "reason": "short explanation" + } + + - id: route_task_domain + name: "Route by Task Domain" + node_type: + switch: + branches: + - condition: '$.nodes.classify_task_domain.output.task_category == "frontend"' + target: route_d_flash_frontend + - condition: '$.nodes.classify_task_domain.output.task_category == "backend"' + target: route_d_flash_backend + - condition: '$.nodes.classify_task_domain.output.task_category == "devops"' + target: route_d_flash_devops + default: route_d_flash_architecture + + - id: route_d_flash_frontend + name: "d-flash Route (frontend)" + node_type: + switch: + branches: + - condition: '$.nodes.detect_repeat_implementation.output.same_implementation_in_history == "yes"' + target: execute_frontend_d_flash + default: execute_frontend_full + + - id: route_d_flash_backend + name: "d-flash Route (backend)" + node_type: + switch: + branches: + - condition: '$.nodes.detect_repeat_implementation.output.same_implementation_in_history == "yes"' + target: execute_backend_d_flash + default: execute_backend_full + + - id: route_d_flash_devops + name: "d-flash Route (devops)" + node_type: + switch: + branches: + - condition: '$.nodes.detect_repeat_implementation.output.same_implementation_in_history == "yes"' + target: execute_devops_d_flash + default: execute_devops_full + + - id: route_d_flash_architecture + name: "d-flash Route (architecture)" + node_type: + switch: + branches: + - condition: '$.nodes.detect_repeat_implementation.output.same_implementation_in_history == "yes"' + target: execute_architecture_d_flash + default: execute_architecture_full + + - id: execute_frontend_full + name: "Execute Frontend (full)" + node_type: + llm_call: + model: mlx-community/LFM2-8B-A1B-4bit + temperature: 0.4 + messages_path: input.messages + heal: true + stream: true + config: + output_schema: + type: object + properties: + task_category: + type: string + enum: [frontend] + selected_model: + type: string + enum: [mlx-community/LFM2-8B-A1B-4bit] + d_flash_enabled: + type: string + enum: ["false"] + implementation_mode: + type: string + enum: [full] + routing_summary: + type: string + response: + type: string + required: + [task_category, selected_model, d_flash_enabled, implementation_mode, routing_summary, response] + additionalProperties: false + user_input_prompt: | + You are a senior frontend engineer using model mlx-community/LFM2-8B-A1B-4bit. + d_flash_enabled=false — provide a complete, production-quality answer. + + Classification: {{ nodes.classify_task_domain.output }} + Repeat check: {{ nodes.detect_repeat_implementation.output }} + + Return JSON only: + { + "task_category": "frontend", + "selected_model": "mlx-community/LFM2-8B-A1B-4bit", + "d_flash_enabled": "false", + "implementation_mode": "full", + "routing_summary": "one sentence: task, model, d-flash off, full mode", + "response": "your answer to the user" + } + + - id: execute_frontend_d_flash + name: "Execute Frontend (d-flash)" + node_type: + llm_call: + model: mlx-community/LFM2-8B-A1B-4bit + temperature: 0.2 + messages_path: input.messages + heal: true + stream: true + config: + output_schema: + type: object + properties: + task_category: + type: string + enum: [frontend] + selected_model: + type: string + enum: [mlx-community/LFM2-8B-A1B-4bit] + d_flash_enabled: + type: string + enum: ["true"] + implementation_mode: + type: string + enum: [d_flash] + routing_summary: + type: string + response: + type: string + required: + [task_category, selected_model, d_flash_enabled, implementation_mode, routing_summary, response] + additionalProperties: false + user_input_prompt: | + You are a senior frontend engineer using model mlx-community/LFM2-8B-A1B-4bit. + d_flash_enabled=true — the same implementation exists in chat history. + Give a concise delta; avoid repeating unchanged code blocks. + + Classification: {{ nodes.classify_task_domain.output }} + Prior match: {{ nodes.detect_repeat_implementation.output.matched_turn_summary }} + + Return JSON only: + { + "task_category": "frontend", + "selected_model": "mlx-community/LFM2-8B-A1B-4bit", + "d_flash_enabled": "true", + "implementation_mode": "d_flash", + "routing_summary": "one sentence: task, model, d-flash on, delta mode", + "response": "your answer to the user" + } + + - id: execute_backend_full + name: "Execute Backend (full)" + node_type: + llm_call: + model: mlx-community/LFM2-8B-A1B-4bit + temperature: 0.4 + messages_path: input.messages + heal: true + stream: true + config: + output_schema: + type: object + properties: + task_category: + type: string + enum: [backend] + selected_model: + type: string + enum: [mlx-community/LFM2-8B-A1B-4bit] + d_flash_enabled: + type: string + enum: ["false"] + implementation_mode: + type: string + enum: [full] + routing_summary: + type: string + response: + type: string + required: + [task_category, selected_model, d_flash_enabled, implementation_mode, routing_summary, response] + additionalProperties: false + user_input_prompt: | + You are a senior backend engineer using model mlx-community/LFM2-8B-A1B-4bit. + d_flash_enabled=false — provide a complete, production-quality answer. + + Classification: {{ nodes.classify_task_domain.output }} + Repeat check: {{ nodes.detect_repeat_implementation.output }} + + Return JSON only: + { + "task_category": "backend", + "selected_model": "mlx-community/LFM2-8B-A1B-4bit", + "d_flash_enabled": "false", + "implementation_mode": "full", + "routing_summary": "one sentence: task, model, d-flash off, full mode", + "response": "your answer to the user" + } + + - id: execute_backend_d_flash + name: "Execute Backend (d-flash)" + node_type: + llm_call: + model: mlx-community/LFM2-8B-A1B-4bit + temperature: 0.2 + messages_path: input.messages + heal: true + stream: true + config: + output_schema: + type: object + properties: + task_category: + type: string + enum: [backend] + selected_model: + type: string + enum: [mlx-community/LFM2-8B-A1B-4bit] + d_flash_enabled: + type: string + enum: ["true"] + implementation_mode: + type: string + enum: [d_flash] + routing_summary: + type: string + response: + type: string + required: + [task_category, selected_model, d_flash_enabled, implementation_mode, routing_summary, response] + additionalProperties: false + user_input_prompt: | + You are a senior backend engineer using model mlx-community/LFM2-8B-A1B-4bit. + d_flash_enabled=true — the same implementation exists in chat history. + Give a concise delta; avoid repeating unchanged code blocks. + + Classification: {{ nodes.classify_task_domain.output }} + Prior match: {{ nodes.detect_repeat_implementation.output.matched_turn_summary }} + + Return JSON only: + { + "task_category": "backend", + "selected_model": "mlx-community/LFM2-8B-A1B-4bit", + "d_flash_enabled": "true", + "implementation_mode": "d_flash", + "routing_summary": "one sentence: task, model, d-flash on, delta mode", + "response": "your answer to the user" + } + + - id: execute_devops_full + name: "Execute DevOps (full)" + node_type: + llm_call: + model: mlx-community/LFM2-8B-A1B-4bit + temperature: 0.4 + messages_path: input.messages + heal: true + stream: true + config: + output_schema: + type: object + properties: + task_category: + type: string + enum: [devops] + selected_model: + type: string + enum: [mlx-community/LFM2-8B-A1B-4bit] + d_flash_enabled: + type: string + enum: ["false"] + implementation_mode: + type: string + enum: [full] + routing_summary: + type: string + response: + type: string + required: + [task_category, selected_model, d_flash_enabled, implementation_mode, routing_summary, response] + additionalProperties: false + user_input_prompt: | + You are a senior DevOps engineer using model mlx-community/LFM2-8B-A1B-4bit. + d_flash_enabled=false — provide a complete, production-quality answer. + + Classification: {{ nodes.classify_task_domain.output }} + Repeat check: {{ nodes.detect_repeat_implementation.output }} + + Return JSON only: + { + "task_category": "devops", + "selected_model": "mlx-community/LFM2-8B-A1B-4bit", + "d_flash_enabled": "false", + "implementation_mode": "full", + "routing_summary": "one sentence: task, model, d-flash off, full mode", + "response": "your answer to the user" + } + + - id: execute_devops_d_flash + name: "Execute DevOps (d-flash)" + node_type: + llm_call: + model: mlx-community/LFM2-8B-A1B-4bit + temperature: 0.2 + messages_path: input.messages + heal: true + stream: true + config: + output_schema: + type: object + properties: + task_category: + type: string + enum: [devops] + selected_model: + type: string + enum: [mlx-community/LFM2-8B-A1B-4bit] + d_flash_enabled: + type: string + enum: ["true"] + implementation_mode: + type: string + enum: [d_flash] + routing_summary: + type: string + response: + type: string + required: + [task_category, selected_model, d_flash_enabled, implementation_mode, routing_summary, response] + additionalProperties: false + user_input_prompt: | + You are a senior DevOps engineer using model mlx-community/LFM2-8B-A1B-4bit. + d_flash_enabled=true — the same implementation exists in chat history. + Give a concise delta; avoid repeating unchanged infra blocks. + + Classification: {{ nodes.classify_task_domain.output }} + Prior match: {{ nodes.detect_repeat_implementation.output.matched_turn_summary }} + + Return JSON only: + { + "task_category": "devops", + "selected_model": "mlx-community/LFM2-8B-A1B-4bit", + "d_flash_enabled": "true", + "implementation_mode": "d_flash", + "routing_summary": "one sentence: task, model, d-flash on, delta mode", + "response": "your answer to the user" + } + + - id: execute_architecture_full + name: "Execute Architecture (full)" + node_type: + llm_call: + model: mlx-community/LFM2-8B-A1B-4bit + temperature: 0.4 + messages_path: input.messages + heal: true + stream: true + config: + output_schema: + type: object + properties: + task_category: + type: string + enum: [architecture] + selected_model: + type: string + enum: [mlx-community/LFM2-8B-A1B-4bit] + d_flash_enabled: + type: string + enum: ["false"] + implementation_mode: + type: string + enum: [full] + routing_summary: + type: string + response: + type: string + required: + [task_category, selected_model, d_flash_enabled, implementation_mode, routing_summary, response] + additionalProperties: false + user_input_prompt: | + You are a principal architect using model mlx-community/LFM2-8B-A1B-4bit. + d_flash_enabled=false — provide a complete architecture answer with trade-offs. + + Classification: {{ nodes.classify_task_domain.output }} + Repeat check: {{ nodes.detect_repeat_implementation.output }} + + Return JSON only: + { + "task_category": "architecture", + "selected_model": "mlx-community/LFM2-8B-A1B-4bit", + "d_flash_enabled": "false", + "implementation_mode": "full", + "routing_summary": "one sentence: task, model, d-flash off, full mode", + "response": "your answer to the user" + } + + - id: execute_architecture_d_flash + name: "Execute Architecture (d-flash)" + node_type: + llm_call: + model: mlx-community/LFM2-8B-A1B-4bit + temperature: 0.2 + messages_path: input.messages + heal: true + stream: true + config: + output_schema: + type: object + properties: + task_category: + type: string + enum: [architecture] + selected_model: + type: string + enum: [mlx-community/LFM2-8B-A1B-4bit] + d_flash_enabled: + type: string + enum: ["true"] + implementation_mode: + type: string + enum: [d_flash] + routing_summary: + type: string + response: + type: string + required: + [task_category, selected_model, d_flash_enabled, implementation_mode, routing_summary, response] + additionalProperties: false + user_input_prompt: | + You are a principal architect using model mlx-community/LFM2-8B-A1B-4bit. + d_flash_enabled=true — the same design was already discussed in chat history. + Give a concise delta; highlight only design changes. + + Classification: {{ nodes.classify_task_domain.output }} + Prior match: {{ nodes.detect_repeat_implementation.output.matched_turn_summary }} + + Return JSON only: + { + "task_category": "architecture", + "selected_model": "mlx-community/LFM2-8B-A1B-4bit", + "d_flash_enabled": "true", + "implementation_mode": "d_flash", + "routing_summary": "one sentence: task, model, d-flash on, delta mode", + "response": "your answer to the user" + } + +edges: + - from: classify_task_domain + to: detect_repeat_implementation + - from: detect_repeat_implementation + to: route_task_domain diff --git a/examples/uv.lock b/examples/uv.lock index e170fcb4..ac5cc330 100644 --- a/examples/uv.lock +++ b/examples/uv.lock @@ -80,7 +80,7 @@ wheels = [ [[package]] name = "examples" -version = "0.5.1" +version = "0.5.2" source = { virtual = "." } dependencies = [ { name = "pydantic" }, @@ -489,7 +489,7 @@ wheels = [ [[package]] name = "simple-agents-py" -version = "0.5.1" +version = "0.5.2" source = { editable = "../crates/simple-agents-py" } dependencies = [ { name = "pydantic" },