testing
This commit is contained in:
@@ -1,295 +1,46 @@
|
|||||||
import json
|
import json
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from wf_core import (
|
from wf_core import RuntimeContext, execute_workflow, resume_workflow
|
||||||
END,
|
from wf_core.demo_workflow import build_demo_registry, build_demo_workflow
|
||||||
RuntimeContext,
|
|
||||||
Workflow,
|
|
||||||
execute_workflow,
|
|
||||||
resume_workflow,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
workflow = Workflow.model_validate(
|
def print_run(label: str, run: object) -> None:
|
||||||
{
|
print(f"{label}:")
|
||||||
"name": "drive_summary_demo",
|
print(json.dumps(run.to_dict(), indent=2))
|
||||||
"input_schema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"folder_id": {"type": "string"},
|
|
||||||
"should_email": {"type": "boolean"},
|
|
||||||
},
|
|
||||||
"required": ["folder_id", "should_email"],
|
|
||||||
},
|
|
||||||
"state_schema": {
|
|
||||||
"fields": {
|
|
||||||
"folder_id": {"type": "string"},
|
|
||||||
"should_email": {"type": "boolean"},
|
|
||||||
"documents": {"type": "array", "merge_strategy": "replace"},
|
|
||||||
"item_summaries": {"type": "array", "merge_strategy": "append"},
|
|
||||||
"summary": {"type": "string", "merge_strategy": "replace"},
|
|
||||||
"approved": {"type": "boolean", "merge_strategy": "replace"},
|
|
||||||
"approval_comment": {"type": "string", "merge_strategy": "replace"},
|
|
||||||
"email_status": {"type": "string", "merge_strategy": "replace"},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"output_schema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"summary": {"type": "string"},
|
|
||||||
"email_status": {"type": "string"},
|
|
||||||
},
|
|
||||||
"required": ["summary", "email_status"],
|
|
||||||
},
|
|
||||||
"node_defs": [
|
|
||||||
{
|
|
||||||
"name": "drive_list_files",
|
|
||||||
"input_schema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {"folder_id": {"type": "string"}},
|
|
||||||
"required": ["folder_id"],
|
|
||||||
},
|
|
||||||
"output_schema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {"documents": {"type": "array"}},
|
|
||||||
"required": ["documents"],
|
|
||||||
},
|
|
||||||
"outcomes": ["ok"],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "summarize_document",
|
|
||||||
"input_schema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {"document": {"type": "string"}},
|
|
||||||
"required": ["document"],
|
|
||||||
},
|
|
||||||
"output_schema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {"item_summary": {"type": "string"}},
|
|
||||||
"required": ["item_summary"],
|
|
||||||
},
|
|
||||||
"outcomes": ["ok"],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "combine_summaries",
|
|
||||||
"input_schema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {"item_summaries": {"type": "array"}},
|
|
||||||
"required": ["item_summaries"],
|
|
||||||
},
|
|
||||||
"output_schema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {"summary": {"type": "string"}},
|
|
||||||
"required": ["summary"],
|
|
||||||
},
|
|
||||||
"outcomes": ["ok"],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "send_email",
|
|
||||||
"input_schema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {"summary": {"type": "string"}},
|
|
||||||
"required": ["summary"],
|
|
||||||
},
|
|
||||||
"output_schema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {"email_status": {"type": "string"}},
|
|
||||||
"required": ["email_status"],
|
|
||||||
},
|
|
||||||
"outcomes": ["sent"],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "mark_email_skipped",
|
|
||||||
"input_schema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {},
|
|
||||||
},
|
|
||||||
"output_schema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {"email_status": {"type": "string"}},
|
|
||||||
"required": ["email_status"],
|
|
||||||
},
|
|
||||||
"outcomes": ["ok"],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"start": "list_files",
|
|
||||||
"nodes": [
|
|
||||||
{
|
|
||||||
"id": "list_files",
|
|
||||||
"type": "node",
|
|
||||||
"node": "drive_list_files",
|
|
||||||
"desc": "List files from a Google Drive folder",
|
|
||||||
"in_map": {"input.folder_id": "folder_id"},
|
|
||||||
"out_map": {"documents": "state.documents"},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "summarize_each",
|
|
||||||
"type": "foreach",
|
|
||||||
"over": "state.documents",
|
|
||||||
"as": "document",
|
|
||||||
"mode": "serial",
|
|
||||||
"on_item_error": "fail",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "summarize_one",
|
|
||||||
"type": "node",
|
|
||||||
"node": "summarize_document",
|
|
||||||
"desc": "Summarize one document",
|
|
||||||
"in_map": {"context.document": "document"},
|
|
||||||
"out_map": {"item_summary": "state.item_summaries"},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "combine_summaries",
|
|
||||||
"type": "node",
|
|
||||||
"node": "combine_summaries",
|
|
||||||
"desc": "Combine item summaries into one final summary",
|
|
||||||
"in_map": {"state.item_summaries": "item_summaries"},
|
|
||||||
"out_map": {"summary": "state.summary"},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "should_email",
|
|
||||||
"type": "condition",
|
|
||||||
"check": {
|
|
||||||
"op": "eq",
|
|
||||||
"left": {"path": "state.should_email"},
|
|
||||||
"right": {"value": True},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "send_email",
|
|
||||||
"type": "node",
|
|
||||||
"node": "send_email",
|
|
||||||
"desc": "Send the summary by email",
|
|
||||||
"in_map": {"state.summary": "summary"},
|
|
||||||
"out_map": {"email_status": "state.email_status"},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "approve_email",
|
|
||||||
"type": "interrupt",
|
|
||||||
"kind": "approval",
|
|
||||||
"request_map": {
|
|
||||||
"state.summary": "summary",
|
|
||||||
"input.folder_id": "folder_id",
|
|
||||||
},
|
|
||||||
"out_map": {
|
|
||||||
"approved": "state.approved",
|
|
||||||
"comment": "state.approval_comment",
|
|
||||||
},
|
|
||||||
"outcomes": ["submitted", "cancelled"],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "skip_email",
|
|
||||||
"type": "node",
|
|
||||||
"node": "mark_email_skipped",
|
|
||||||
"desc": "Record that email delivery was skipped",
|
|
||||||
"out_map": {"email_status": "state.email_status"},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"edges": [
|
|
||||||
{"from": "list_files", "outcome": "ok", "to": "summarize_each"},
|
|
||||||
{"from": "summarize_each", "outcome": "loop", "to": "summarize_one"},
|
|
||||||
{"from": "summarize_each", "outcome": "done", "to": "combine_summaries"},
|
|
||||||
{"from": "summarize_one", "outcome": "ok", "to": END},
|
|
||||||
{"from": "combine_summaries", "outcome": "ok", "to": "should_email"},
|
|
||||||
{"from": "should_email", "outcome": "true", "to": "approve_email"},
|
|
||||||
{"from": "should_email", "outcome": "false", "to": "skip_email"},
|
|
||||||
{"from": "approve_email", "outcome": "submitted", "to": "send_email"},
|
|
||||||
{"from": "approve_email", "outcome": "cancelled", "to": "skip_email"},
|
|
||||||
{"from": "send_email", "outcome": "sent", "to": END},
|
|
||||||
{"from": "skip_email", "outcome": "ok", "to": END},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
report = workflow.validate_structure()
|
|
||||||
report.raise_for_errors()
|
|
||||||
|
|
||||||
print("Workflow is structurally valid.", file=sys.stderr)
|
|
||||||
|
|
||||||
|
|
||||||
def drive_list_files(
|
def main() -> None:
|
||||||
payload: dict[str, object], ctx: RuntimeContext
|
workflow = build_demo_workflow()
|
||||||
) -> dict[str, object]:
|
registry = build_demo_registry()
|
||||||
folder_id = payload["folder_id"]
|
|
||||||
return {
|
|
||||||
"outcome": "ok",
|
|
||||||
"output": {
|
|
||||||
"documents": [
|
|
||||||
f"{folder_id}/meeting-notes.md",
|
|
||||||
f"{folder_id}/weekly-report.md",
|
|
||||||
]
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
|
report = workflow.validate_structure()
|
||||||
|
report.raise_for_errors()
|
||||||
|
print("Workflow is structurally valid.", file=sys.stderr)
|
||||||
|
|
||||||
def summarize_documents(
|
interrupted_run = execute_workflow(
|
||||||
payload: dict[str, object], ctx: RuntimeContext
|
|
||||||
) -> dict[str, object]:
|
|
||||||
document = payload["document"]
|
|
||||||
return {
|
|
||||||
"outcome": "ok",
|
|
||||||
"output": {"item_summary": f"Summary of {document}"},
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def combine_summaries(
|
|
||||||
payload: dict[str, object], ctx: RuntimeContext
|
|
||||||
) -> dict[str, object]:
|
|
||||||
item_summaries = payload["item_summaries"]
|
|
||||||
return {
|
|
||||||
"outcome": "ok",
|
|
||||||
"output": {"summary": " | ".join(item_summaries)},
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def send_email(payload: dict[str, object], ctx: RuntimeContext) -> dict[str, object]:
|
|
||||||
return {
|
|
||||||
"outcome": "sent",
|
|
||||||
"output": {"email_status": f"sent: {payload['summary']}"},
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def mark_email_skipped(
|
|
||||||
payload: dict[str, object], ctx: RuntimeContext
|
|
||||||
) -> dict[str, object]:
|
|
||||||
return {
|
|
||||||
"outcome": "ok",
|
|
||||||
"output": {"email_status": "skipped"},
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
registry = {
|
|
||||||
"drive_list_files": drive_list_files,
|
|
||||||
"summarize_document": summarize_documents,
|
|
||||||
"combine_summaries": combine_summaries,
|
|
||||||
"send_email": send_email,
|
|
||||||
"mark_email_skipped": mark_email_skipped,
|
|
||||||
}
|
|
||||||
|
|
||||||
workflow.validate_structure().raise_for_errors()
|
|
||||||
|
|
||||||
print("Interrupting run:")
|
|
||||||
interrupted_run = execute_workflow(
|
|
||||||
workflow,
|
workflow,
|
||||||
{"folder_id": "demo-folder", "should_email": True},
|
{"folder_id": "demo-folder", "should_email": True},
|
||||||
registry,
|
registry,
|
||||||
)
|
)
|
||||||
print(json.dumps(interrupted_run.to_dict(), indent=2))
|
print_run("Interrupting run", interrupted_run)
|
||||||
|
|
||||||
print("Resumed run:")
|
resumed_run = resume_workflow(
|
||||||
resumed_run = resume_workflow(
|
|
||||||
workflow,
|
workflow,
|
||||||
interrupted_run,
|
interrupted_run,
|
||||||
registry,
|
registry,
|
||||||
resume_payload={"approved": True, "comment": "Looks good to send."},
|
resume_payload={"approved": True, "comment": "Looks good to send."},
|
||||||
resume_outcome="submitted",
|
resume_outcome="submitted",
|
||||||
)
|
)
|
||||||
print(json.dumps(resumed_run.to_dict(), indent=2))
|
print_run("Resumed run", resumed_run)
|
||||||
|
|
||||||
print("Non-interrupt run:")
|
non_interrupt_run = execute_workflow(
|
||||||
non_interrupt_run = execute_workflow(
|
|
||||||
workflow,
|
workflow,
|
||||||
{"folder_id": "demo-folder", "should_email": False},
|
{"folder_id": "demo-folder", "should_email": False},
|
||||||
registry,
|
registry,
|
||||||
)
|
)
|
||||||
print(json.dumps(non_interrupt_run.to_dict(), indent=2))
|
print_run("Non-interrupt run", non_interrupt_run)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
|||||||
@@ -8,3 +8,8 @@ dependencies = [
|
|||||||
"mcp[cli,rich]>=1.27.0",
|
"mcp[cli,rich]>=1.27.0",
|
||||||
"pydantic>=2.13.3",
|
"pydantic>=2.13.3",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[dependency-groups]
|
||||||
|
dev = [
|
||||||
|
"pytest>=8.4.0",
|
||||||
|
]
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
if str(ROOT) not in sys.path:
|
||||||
|
sys.path.insert(0, str(ROOT))
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from wf_core import (
|
||||||
|
END,
|
||||||
|
FrameStatus,
|
||||||
|
RunStatus,
|
||||||
|
execute_workflow,
|
||||||
|
resume_workflow,
|
||||||
|
step_workflow,
|
||||||
|
)
|
||||||
|
from wf_core.demo_workflow import build_demo_registry, build_demo_workflow
|
||||||
|
from wf_core.run_factory import create_run_state
|
||||||
|
|
||||||
|
|
||||||
|
def test_interrupt_then_resume_to_send_email() -> None:
|
||||||
|
workflow = build_demo_workflow()
|
||||||
|
registry = build_demo_registry()
|
||||||
|
|
||||||
|
interrupted_run = execute_workflow(
|
||||||
|
workflow,
|
||||||
|
{"folder_id": "demo-folder", "should_email": True},
|
||||||
|
registry,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert interrupted_run.status == RunStatus.INTERRUPTED
|
||||||
|
assert interrupted_run.current_node_id == "approve_email"
|
||||||
|
assert interrupted_run.interrupt is not None
|
||||||
|
assert interrupted_run.interrupt.kind == "approval"
|
||||||
|
assert interrupted_run.state["summary"].startswith("Summary of demo-folder/")
|
||||||
|
|
||||||
|
resumed_run = resume_workflow(
|
||||||
|
workflow,
|
||||||
|
interrupted_run,
|
||||||
|
registry,
|
||||||
|
resume_payload={"approved": True, "comment": "Looks good to send."},
|
||||||
|
resume_outcome="submitted",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert resumed_run.status == RunStatus.COMPLETED
|
||||||
|
assert resumed_run.current_node_id == END
|
||||||
|
assert resumed_run.output["email_status"].startswith("sent:")
|
||||||
|
assert resumed_run.state["approved"] is True
|
||||||
|
assert resumed_run.state["approval_comment"] == "Looks good to send."
|
||||||
|
|
||||||
|
|
||||||
|
def test_non_interrupt_path_skips_email() -> None:
|
||||||
|
workflow = build_demo_workflow()
|
||||||
|
registry = build_demo_registry()
|
||||||
|
|
||||||
|
run = execute_workflow(
|
||||||
|
workflow,
|
||||||
|
{"folder_id": "demo-folder", "should_email": False},
|
||||||
|
registry,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert run.status == RunStatus.COMPLETED
|
||||||
|
assert run.output["email_status"] == "skipped"
|
||||||
|
assert run.interrupt is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_stepwise_execution_reaches_interrupt() -> None:
|
||||||
|
workflow = build_demo_workflow()
|
||||||
|
registry = build_demo_registry()
|
||||||
|
run = create_run_state(
|
||||||
|
workflow,
|
||||||
|
{"folder_id": "demo-folder", "should_email": True},
|
||||||
|
)
|
||||||
|
|
||||||
|
workflow.validate_structure().raise_for_errors()
|
||||||
|
|
||||||
|
while run.status not in {RunStatus.INTERRUPTED, RunStatus.COMPLETED}:
|
||||||
|
step_workflow(workflow, run, registry)
|
||||||
|
|
||||||
|
assert run.status == RunStatus.INTERRUPTED
|
||||||
|
assert run.current_node_id == "approve_email"
|
||||||
|
assert any(entry.step_type == "foreach" for entry in run.trace)
|
||||||
|
assert len(run.trace) == 9
|
||||||
|
|
||||||
|
|
||||||
|
def test_foreach_stress_with_many_documents() -> None:
|
||||||
|
workflow = build_demo_workflow()
|
||||||
|
registry = build_demo_registry()
|
||||||
|
|
||||||
|
document_count = 25
|
||||||
|
|
||||||
|
def many_files(payload: dict[str, object], ctx: object) -> dict[str, object]:
|
||||||
|
folder_id = payload["folder_id"]
|
||||||
|
return {
|
||||||
|
"outcome": "ok",
|
||||||
|
"output": {
|
||||||
|
"documents": [
|
||||||
|
f"{folder_id}/doc-{index:02d}.md" for index in range(document_count)
|
||||||
|
]
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
registry["drive_list_files"] = many_files
|
||||||
|
|
||||||
|
run = execute_workflow(
|
||||||
|
workflow,
|
||||||
|
{"folder_id": "bulk-folder", "should_email": False},
|
||||||
|
registry,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert run.status == RunStatus.COMPLETED
|
||||||
|
assert len(run.state["documents"]) == document_count
|
||||||
|
assert len(run.state["item_summaries"]) == document_count
|
||||||
|
assert len(
|
||||||
|
[
|
||||||
|
frame
|
||||||
|
for frame in run.frames.values()
|
||||||
|
if frame.kind == "foreach_iteration"
|
||||||
|
and frame.status == FrameStatus.COMPLETED
|
||||||
|
]
|
||||||
|
) == document_count
|
||||||
|
assert len([entry for entry in run.trace if entry.step_type == "foreach"]) == (
|
||||||
|
document_count + 1
|
||||||
|
)
|
||||||
@@ -212,6 +212,15 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/5d/13/ad7d7ca3808a898b4612b6fe93cde56b53f3034dcde235acb1f0e1df24c6/idna-3.13-py3-none-any.whl", hash = "sha256:892ea0cde124a99ce773decba204c5552b69c3c67ffd5f232eb7696135bc8bb3", size = 68629, upload-time = "2026-04-22T16:42:40.909Z" },
|
{ url = "https://files.pythonhosted.org/packages/5d/13/ad7d7ca3808a898b4612b6fe93cde56b53f3034dcde235acb1f0e1df24c6/idna-3.13-py3-none-any.whl", hash = "sha256:892ea0cde124a99ce773decba204c5552b69c3c67ffd5f232eb7696135bc8bb3", size = 68629, upload-time = "2026-04-22T16:42:40.909Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "iniconfig"
|
||||||
|
version = "2.3.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "jsonschema"
|
name = "jsonschema"
|
||||||
version = "4.26.0"
|
version = "4.26.0"
|
||||||
@@ -248,12 +257,20 @@ dependencies = [
|
|||||||
{ name = "pydantic" },
|
{ name = "pydantic" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[package.dev-dependencies]
|
||||||
|
dev = [
|
||||||
|
{ name = "pytest" },
|
||||||
|
]
|
||||||
|
|
||||||
[package.metadata]
|
[package.metadata]
|
||||||
requires-dist = [
|
requires-dist = [
|
||||||
{ name = "mcp", extras = ["cli", "rich"], specifier = ">=1.27.0" },
|
{ name = "mcp", extras = ["cli", "rich"], specifier = ">=1.27.0" },
|
||||||
{ name = "pydantic", specifier = ">=2.13.3" },
|
{ name = "pydantic", specifier = ">=2.13.3" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[package.metadata.requires-dev]
|
||||||
|
dev = [{ name = "pytest", specifier = ">=8.4.0" }]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "markdown-it-py"
|
name = "markdown-it-py"
|
||||||
version = "4.0.0"
|
version = "4.0.0"
|
||||||
@@ -309,6 +326,24 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
|
{ url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "packaging"
|
||||||
|
version = "26.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pluggy"
|
||||||
|
version = "1.6.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pycparser"
|
name = "pycparser"
|
||||||
version = "3.0"
|
version = "3.0"
|
||||||
@@ -411,6 +446,22 @@ crypto = [
|
|||||||
{ name = "cryptography" },
|
{ name = "cryptography" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pytest"
|
||||||
|
version = "9.0.3"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||||
|
{ name = "iniconfig" },
|
||||||
|
{ name = "packaging" },
|
||||||
|
{ name = "pluggy" },
|
||||||
|
{ name = "pygments" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "python-dotenv"
|
name = "python-dotenv"
|
||||||
version = "1.2.2"
|
version = "1.2.2"
|
||||||
|
|||||||
@@ -0,0 +1,266 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Callable
|
||||||
|
|
||||||
|
from .model import Workflow
|
||||||
|
from .run_state import RuntimeContext
|
||||||
|
from .tokens import END
|
||||||
|
|
||||||
|
DemoHandler = Callable[[dict[str, object], RuntimeContext], dict[str, object]]
|
||||||
|
|
||||||
|
|
||||||
|
def build_demo_workflow() -> Workflow:
|
||||||
|
return Workflow.model_validate(
|
||||||
|
{
|
||||||
|
"name": "drive_summary_demo",
|
||||||
|
"input_schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"folder_id": {"type": "string"},
|
||||||
|
"should_email": {"type": "boolean"},
|
||||||
|
},
|
||||||
|
"required": ["folder_id", "should_email"],
|
||||||
|
},
|
||||||
|
"state_schema": {
|
||||||
|
"fields": {
|
||||||
|
"folder_id": {"type": "string"},
|
||||||
|
"should_email": {"type": "boolean"},
|
||||||
|
"documents": {"type": "array", "merge_strategy": "replace"},
|
||||||
|
"item_summaries": {"type": "array", "merge_strategy": "append"},
|
||||||
|
"summary": {"type": "string", "merge_strategy": "replace"},
|
||||||
|
"approved": {"type": "boolean", "merge_strategy": "replace"},
|
||||||
|
"approval_comment": {
|
||||||
|
"type": "string",
|
||||||
|
"merge_strategy": "replace",
|
||||||
|
},
|
||||||
|
"email_status": {"type": "string", "merge_strategy": "replace"},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"output_schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"summary": {"type": "string"},
|
||||||
|
"email_status": {"type": "string"},
|
||||||
|
},
|
||||||
|
"required": ["summary", "email_status"],
|
||||||
|
},
|
||||||
|
"node_defs": [
|
||||||
|
{
|
||||||
|
"name": "drive_list_files",
|
||||||
|
"input_schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {"folder_id": {"type": "string"}},
|
||||||
|
"required": ["folder_id"],
|
||||||
|
},
|
||||||
|
"output_schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {"documents": {"type": "array"}},
|
||||||
|
"required": ["documents"],
|
||||||
|
},
|
||||||
|
"outcomes": ["ok"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "summarize_document",
|
||||||
|
"input_schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {"document": {"type": "string"}},
|
||||||
|
"required": ["document"],
|
||||||
|
},
|
||||||
|
"output_schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {"item_summary": {"type": "string"}},
|
||||||
|
"required": ["item_summary"],
|
||||||
|
},
|
||||||
|
"outcomes": ["ok"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "combine_summaries",
|
||||||
|
"input_schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {"item_summaries": {"type": "array"}},
|
||||||
|
"required": ["item_summaries"],
|
||||||
|
},
|
||||||
|
"output_schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {"summary": {"type": "string"}},
|
||||||
|
"required": ["summary"],
|
||||||
|
},
|
||||||
|
"outcomes": ["ok"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "send_email",
|
||||||
|
"input_schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {"summary": {"type": "string"}},
|
||||||
|
"required": ["summary"],
|
||||||
|
},
|
||||||
|
"output_schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {"email_status": {"type": "string"}},
|
||||||
|
"required": ["email_status"],
|
||||||
|
},
|
||||||
|
"outcomes": ["sent"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "mark_email_skipped",
|
||||||
|
"input_schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {},
|
||||||
|
},
|
||||||
|
"output_schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {"email_status": {"type": "string"}},
|
||||||
|
"required": ["email_status"],
|
||||||
|
},
|
||||||
|
"outcomes": ["ok"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"start": "list_files",
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"id": "list_files",
|
||||||
|
"type": "node",
|
||||||
|
"node": "drive_list_files",
|
||||||
|
"desc": "List files from a Google Drive folder",
|
||||||
|
"in_map": {"input.folder_id": "folder_id"},
|
||||||
|
"out_map": {"documents": "state.documents"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "summarize_each",
|
||||||
|
"type": "foreach",
|
||||||
|
"over": "state.documents",
|
||||||
|
"as": "document",
|
||||||
|
"mode": "serial",
|
||||||
|
"on_item_error": "fail",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "summarize_one",
|
||||||
|
"type": "node",
|
||||||
|
"node": "summarize_document",
|
||||||
|
"desc": "Summarize one document",
|
||||||
|
"in_map": {"context.document": "document"},
|
||||||
|
"out_map": {"item_summary": "state.item_summaries"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "combine_summaries",
|
||||||
|
"type": "node",
|
||||||
|
"node": "combine_summaries",
|
||||||
|
"desc": "Combine item summaries into one final summary",
|
||||||
|
"in_map": {"state.item_summaries": "item_summaries"},
|
||||||
|
"out_map": {"summary": "state.summary"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "should_email",
|
||||||
|
"type": "condition",
|
||||||
|
"check": {
|
||||||
|
"op": "eq",
|
||||||
|
"left": {"path": "state.should_email"},
|
||||||
|
"right": {"value": True},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "send_email",
|
||||||
|
"type": "node",
|
||||||
|
"node": "send_email",
|
||||||
|
"desc": "Send the summary by email",
|
||||||
|
"in_map": {"state.summary": "summary"},
|
||||||
|
"out_map": {"email_status": "state.email_status"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "approve_email",
|
||||||
|
"type": "interrupt",
|
||||||
|
"kind": "approval",
|
||||||
|
"request_map": {
|
||||||
|
"state.summary": "summary",
|
||||||
|
"input.folder_id": "folder_id",
|
||||||
|
},
|
||||||
|
"out_map": {
|
||||||
|
"approved": "state.approved",
|
||||||
|
"comment": "state.approval_comment",
|
||||||
|
},
|
||||||
|
"outcomes": ["submitted", "cancelled"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "skip_email",
|
||||||
|
"type": "node",
|
||||||
|
"node": "mark_email_skipped",
|
||||||
|
"desc": "Record that email delivery was skipped",
|
||||||
|
"out_map": {"email_status": "state.email_status"},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"edges": [
|
||||||
|
{"from": "list_files", "outcome": "ok", "to": "summarize_each"},
|
||||||
|
{"from": "summarize_each", "outcome": "loop", "to": "summarize_one"},
|
||||||
|
{"from": "summarize_each", "outcome": "done", "to": "combine_summaries"},
|
||||||
|
{"from": "summarize_one", "outcome": "ok", "to": END},
|
||||||
|
{"from": "combine_summaries", "outcome": "ok", "to": "should_email"},
|
||||||
|
{"from": "should_email", "outcome": "true", "to": "approve_email"},
|
||||||
|
{"from": "should_email", "outcome": "false", "to": "skip_email"},
|
||||||
|
{"from": "approve_email", "outcome": "submitted", "to": "send_email"},
|
||||||
|
{"from": "approve_email", "outcome": "cancelled", "to": "skip_email"},
|
||||||
|
{"from": "send_email", "outcome": "sent", "to": END},
|
||||||
|
{"from": "skip_email", "outcome": "ok", "to": END},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def drive_list_files(
|
||||||
|
payload: dict[str, object], ctx: RuntimeContext
|
||||||
|
) -> dict[str, object]:
|
||||||
|
folder_id = payload["folder_id"]
|
||||||
|
return {
|
||||||
|
"outcome": "ok",
|
||||||
|
"output": {
|
||||||
|
"documents": [
|
||||||
|
f"{folder_id}/meeting-notes.md",
|
||||||
|
f"{folder_id}/weekly-report.md",
|
||||||
|
]
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def summarize_documents(
|
||||||
|
payload: dict[str, object], ctx: RuntimeContext
|
||||||
|
) -> dict[str, object]:
|
||||||
|
document = payload["document"]
|
||||||
|
return {
|
||||||
|
"outcome": "ok",
|
||||||
|
"output": {"item_summary": f"Summary of {document}"},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def combine_summaries(
|
||||||
|
payload: dict[str, object], ctx: RuntimeContext
|
||||||
|
) -> dict[str, object]:
|
||||||
|
item_summaries = payload["item_summaries"]
|
||||||
|
return {
|
||||||
|
"outcome": "ok",
|
||||||
|
"output": {"summary": " | ".join(item_summaries)},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def send_email(payload: dict[str, object], ctx: RuntimeContext) -> dict[str, object]:
|
||||||
|
return {
|
||||||
|
"outcome": "sent",
|
||||||
|
"output": {"email_status": f"sent: {payload['summary']}"},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def mark_email_skipped(
|
||||||
|
payload: dict[str, object], ctx: RuntimeContext
|
||||||
|
) -> dict[str, object]:
|
||||||
|
return {
|
||||||
|
"outcome": "ok",
|
||||||
|
"output": {"email_status": "skipped"},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def build_demo_registry() -> dict[str, DemoHandler]:
|
||||||
|
return {
|
||||||
|
"drive_list_files": drive_list_files,
|
||||||
|
"summarize_document": summarize_documents,
|
||||||
|
"combine_summaries": combine_summaries,
|
||||||
|
"send_email": send_email,
|
||||||
|
"mark_email_skipped": mark_email_skipped,
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user