edda-framework 0.14.1__tar.gz → 0.15.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {edda_framework-0.14.1 → edda_framework-0.15.0}/PKG-INFO +13 -1
- {edda_framework-0.14.1 → edda_framework-0.15.0}/README.md +8 -0
- edda_framework-0.15.0/docs/integrations/llamaindex.md +213 -0
- edda_framework-0.15.0/docs/integrations/pydantic-graph.md +256 -0
- edda_framework-0.15.0/edda/integrations/graph/__init__.py +58 -0
- edda_framework-0.15.0/edda/integrations/graph/context.py +81 -0
- edda_framework-0.15.0/edda/integrations/graph/exceptions.py +9 -0
- edda_framework-0.15.0/edda/integrations/graph/graph.py +385 -0
- edda_framework-0.15.0/edda/integrations/graph/nodes.py +144 -0
- edda_framework-0.15.0/edda/integrations/llamaindex/__init__.py +51 -0
- edda_framework-0.15.0/edda/integrations/llamaindex/events.py +160 -0
- edda_framework-0.15.0/edda/integrations/llamaindex/exceptions.py +15 -0
- edda_framework-0.15.0/edda/integrations/llamaindex/workflow.py +306 -0
- edda_framework-0.15.0/examples/graph/__init__.py +1 -0
- edda_framework-0.15.0/examples/graph/demo_graph.py +198 -0
- edda_framework-0.15.0/examples/graph/demo_graph_sleep.py +172 -0
- edda_framework-0.15.0/examples/graph/demo_graph_wait_event.py +202 -0
- edda_framework-0.15.0/examples/llamaindex/__init__.py +1 -0
- edda_framework-0.15.0/examples/llamaindex/demo_workflow.py +143 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/pyproject.toml +12 -1
- edda_framework-0.15.0/tests/test_graph_integration.py +420 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/uv.lock +468 -2
- {edda_framework-0.14.1 → edda_framework-0.15.0}/.github/workflows/ci.yml +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/.github/workflows/docs.yml +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/.github/workflows/release.yml +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/.gitignore +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/.gitmodules +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/.python-version +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/Justfile +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/LICENSE +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/demo_app.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/docs/api/reference.md +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/docs/core-features/durable-execution/replay.md +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/docs/core-features/events/cloudevents-http-binding.md +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/docs/core-features/events/postgres-notify.md +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/docs/core-features/events/wait-event.md +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/docs/core-features/hooks.md +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/docs/core-features/messages.md +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/docs/core-features/retry.md +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/docs/core-features/saga-compensation.md +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/docs/core-features/transactional-outbox.md +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/docs/core-features/workflows-activities.md +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/docs/examples/ecommerce.md +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/docs/examples/events.md +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/docs/examples/fastapi-integration.md +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/docs/examples/saga.md +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/docs/examples/simple.md +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/docs/getting-started/concepts.md +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/docs/getting-started/first-workflow.md +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/docs/getting-started/installation.md +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/docs/getting-started/quick-start.md +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/docs/index.md +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/docs/integrations/mcp.md +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/docs/integrations/mirascope.md +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/docs/integrations/opentelemetry.md +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/docs/integrations/pydantic-rpc.md +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/docs/viewer-ui/images/cloudevents-cli-trigger.png +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/docs/viewer-ui/images/compensation-execution.png +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/docs/viewer-ui/images/detail-page-loan-approval.png +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/docs/viewer-ui/images/detail-page-match-case.png +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/docs/viewer-ui/images/nested-pydantic-form.png +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/docs/viewer-ui/images/start-workflow-form-pydantic.png +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/docs/viewer-ui/images/wait-event-visualization.png +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/docs/viewer-ui/images/workflow-list-view.png +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/docs/viewer-ui/images/workflow-selection-dropdown.png +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/docs/viewer-ui/setup.md +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/docs/viewer-ui/visualization.md +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/edda/__init__.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/edda/activity.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/edda/app.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/edda/channels.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/edda/compensation.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/edda/context.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/edda/exceptions.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/edda/hooks.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/edda/integrations/__init__.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/edda/integrations/mcp/__init__.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/edda/integrations/mcp/decorators.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/edda/integrations/mcp/server.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/edda/integrations/mirascope/__init__.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/edda/integrations/mirascope/agent.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/edda/integrations/mirascope/call.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/edda/integrations/mirascope/decorator.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/edda/integrations/mirascope/types.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/edda/integrations/opentelemetry/__init__.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/edda/integrations/opentelemetry/hooks.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/edda/locking.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/edda/migrations/mysql/20251217000000_initial_schema.sql +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/edda/migrations/postgresql/20251217000000_initial_schema.sql +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/edda/migrations/sqlite/20251217000000_initial_schema.sql +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/edda/outbox/__init__.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/edda/outbox/relayer.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/edda/outbox/transactional.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/edda/pydantic_utils.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/edda/replay.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/edda/retry.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/edda/serialization/__init__.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/edda/serialization/base.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/edda/serialization/json.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/edda/storage/__init__.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/edda/storage/migrations.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/edda/storage/models.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/edda/storage/notify_base.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/edda/storage/pg_notify.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/edda/storage/protocol.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/edda/storage/sqlalchemy_storage.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/edda/viewer_ui/__init__.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/edda/viewer_ui/app.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/edda/viewer_ui/components.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/edda/viewer_ui/data_service.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/edda/viewer_ui/theme.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/edda/visualizer/__init__.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/edda/visualizer/ast_analyzer.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/edda/visualizer/mermaid_generator.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/edda/workflow.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/edda/wsgi.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/examples/__init__.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/examples/cancellable_workflow.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/examples/compensation_workflow.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/examples/event_waiting_app.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/examples/event_waiting_workflow.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/examples/event_waiting_workflow_complete.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/examples/long_running_loop.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/examples/mcp/README.md +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/examples/mcp/order_processing_mcp.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/examples/mcp/prompts_example.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/examples/mcp/remote_server_example.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/examples/mcp/simple_mcp_server.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/examples/message_passing.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/examples/mirascope/__init__.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/examples/mirascope/durable_agent.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/examples/mirascope/multi_turn.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/examples/mirascope/simple_call.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/examples/mirascope/with_tools.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/examples/observability_with_logfire.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/examples/observability_with_opentelemetry.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/examples/pydantic_rpc_integration.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/examples/pydantic_saga.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/examples/retry_example.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/examples/retry_with_compensation.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/examples/simple_workflow.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/examples/typeddict_example.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/examples/with_outbox.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/schema/.dbmate.yml +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/schema/.git +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/schema/.gitignore +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/schema/LICENSE +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/schema/README.md +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/schema/docs/column-values.md +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/tests/__init__.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/tests/conftest.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/tests/integrations/__init__.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/tests/integrations/mcp/__init__.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/tests/integrations/mcp/test_cancel.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/tests/integrations/mcp/test_integration.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/tests/integrations/mcp/test_jsonrpc.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/tests/integrations/mcp/test_prompts.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/tests/integrations/mcp/test_server.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/tests/integrations/mirascope/__init__.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/tests/integrations/mirascope/test_agent.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/tests/integrations/mirascope/test_call.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/tests/integrations/mirascope/test_decorator.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/tests/integrations/mirascope/test_types.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/tests/integrations/opentelemetry/__init__.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/tests/integrations/opentelemetry/test_hooks.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/tests/test_activity.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/tests/test_activity_retry.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/tests/test_activity_sync.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/tests/test_app.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/tests/test_ast_analyzer.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/tests/test_atomic_wait_event.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/tests/test_auto_migration.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/tests/test_binary_data.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/tests/test_channel_competing.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/tests/test_channel_direct.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/tests/test_channel_mode_locking.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/tests/test_channel_transactional.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/tests/test_cloudevents_http_binding.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/tests/test_compensation.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/tests/test_compensation_crash_recovery.py.wip +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/tests/test_concurrent_outbox.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/tests/test_context.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/tests/test_cross_language_channel.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/tests/test_ctx_session.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/tests/test_distributed_event_delivery.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/tests/test_events.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/tests/test_instance_id_routing.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/tests/test_lock_race_condition.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/tests/test_lock_timeout_customization.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/tests/test_locking.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/tests/test_message_cleanup.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/tests/test_message_delivery_lock.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/tests/test_messages.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/tests/test_migrations_integration.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/tests/test_multidb_storage.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/tests/test_outbox.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/tests/test_pg_notify.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/tests/test_polling_optimization.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/tests/test_pydantic_activity.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/tests/test_pydantic_enum.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/tests/test_pydantic_events.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/tests/test_pydantic_saga.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/tests/test_pydantic_utils.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/tests/test_receive_timeout.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/tests/test_received_event.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/tests/test_recur.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/tests/test_recur_cleanup.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/tests/test_replay.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/tests/test_retry_policy.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/tests/test_saga_parameter_extraction.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/tests/test_serialization.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/tests/test_skip_locked.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/tests/test_stale_workflow_recovery.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/tests/test_storage.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/tests/test_storage_mysql.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/tests/test_storage_postgresql.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/tests/test_transactions.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/tests/test_viewer_pagination.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/tests/test_viewer_pydantic_form.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/tests/test_viewer_start_saga.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/tests/test_wait_timer.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/tests/test_workflow.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/tests/test_workflow_auto_register.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/tests/test_workflow_cancellation.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/tests/test_workflow_resumption.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/viewer_app.py +0 -0
- {edda_framework-0.14.1 → edda_framework-0.15.0}/zensical.toml +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: edda-framework
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.15.0
|
|
4
4
|
Summary: Lightweight Durable Execution Framework
|
|
5
5
|
Project-URL: Homepage, https://github.com/i2y/edda
|
|
6
6
|
Project-URL: Documentation, https://github.com/i2y/edda#readme
|
|
@@ -42,6 +42,10 @@ Requires-Dist: starlette>=0.40.0; extra == 'dev'
|
|
|
42
42
|
Requires-Dist: testcontainers[mysql]>=4.0.0; extra == 'dev'
|
|
43
43
|
Requires-Dist: testcontainers[postgres]>=4.0.0; extra == 'dev'
|
|
44
44
|
Requires-Dist: tsuno>=0.1.3; extra == 'dev'
|
|
45
|
+
Provides-Extra: graph
|
|
46
|
+
Requires-Dist: pydantic-graph>=0.1.0; extra == 'graph'
|
|
47
|
+
Provides-Extra: llamaindex
|
|
48
|
+
Requires-Dist: llama-index-core>=0.12.0; extra == 'llamaindex'
|
|
45
49
|
Provides-Extra: mcp
|
|
46
50
|
Requires-Dist: mcp>=1.22.0; extra == 'mcp'
|
|
47
51
|
Provides-Extra: mirascope
|
|
@@ -97,6 +101,8 @@ For detailed documentation, visit [https://i2y.github.io/edda/](https://i2y.gith
|
|
|
97
101
|
- ⚡ **Instant Notifications**: PostgreSQL LISTEN/NOTIFY for near-instant event delivery (optional)
|
|
98
102
|
- 🤖 **MCP Integration**: Expose durable workflows as AI tools via Model Context Protocol
|
|
99
103
|
- 🧠 **Mirascope Integration**: Durable LLM calls
|
|
104
|
+
- 🦙 **LlamaIndex Integration**: Make LlamaIndex Workflows durable with crash recovery
|
|
105
|
+
- 📊 **pydantic-graph Integration**: Durable graph-based workflows (experimental)
|
|
100
106
|
- 🌍 **ASGI/WSGI Support**: Deploy with your preferred server (uvicorn, gunicorn, uWSGI)
|
|
101
107
|
|
|
102
108
|
## Use Cases
|
|
@@ -233,6 +239,12 @@ uv add edda-framework --extra viewer
|
|
|
233
239
|
# With PostgreSQL instant notifications (LISTEN/NOTIFY)
|
|
234
240
|
uv add edda-framework --extra postgres-notify
|
|
235
241
|
|
|
242
|
+
# With LlamaIndex Workflow integration
|
|
243
|
+
uv add edda-framework --extra llamaindex
|
|
244
|
+
|
|
245
|
+
# With pydantic-graph integration (experimental)
|
|
246
|
+
uv add edda-framework --extra graph
|
|
247
|
+
|
|
236
248
|
# All extras (PostgreSQL, MySQL, Viewer UI)
|
|
237
249
|
uv add edda-framework --extra postgresql --extra mysql --extra viewer
|
|
238
250
|
```
|
|
@@ -32,6 +32,8 @@ For detailed documentation, visit [https://i2y.github.io/edda/](https://i2y.gith
|
|
|
32
32
|
- ⚡ **Instant Notifications**: PostgreSQL LISTEN/NOTIFY for near-instant event delivery (optional)
|
|
33
33
|
- 🤖 **MCP Integration**: Expose durable workflows as AI tools via Model Context Protocol
|
|
34
34
|
- 🧠 **Mirascope Integration**: Durable LLM calls
|
|
35
|
+
- 🦙 **LlamaIndex Integration**: Make LlamaIndex Workflows durable with crash recovery
|
|
36
|
+
- 📊 **pydantic-graph Integration**: Durable graph-based workflows (experimental)
|
|
35
37
|
- 🌍 **ASGI/WSGI Support**: Deploy with your preferred server (uvicorn, gunicorn, uWSGI)
|
|
36
38
|
|
|
37
39
|
## Use Cases
|
|
@@ -168,6 +170,12 @@ uv add edda-framework --extra viewer
|
|
|
168
170
|
# With PostgreSQL instant notifications (LISTEN/NOTIFY)
|
|
169
171
|
uv add edda-framework --extra postgres-notify
|
|
170
172
|
|
|
173
|
+
# With LlamaIndex Workflow integration
|
|
174
|
+
uv add edda-framework --extra llamaindex
|
|
175
|
+
|
|
176
|
+
# With pydantic-graph integration (experimental)
|
|
177
|
+
uv add edda-framework --extra graph
|
|
178
|
+
|
|
171
179
|
# All extras (PostgreSQL, MySQL, Viewer UI)
|
|
172
180
|
uv add edda-framework --extra postgresql --extra mysql --extra viewer
|
|
173
181
|
```
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
# LlamaIndex Workflow Integration
|
|
2
|
+
|
|
3
|
+
Edda provides integration with [LlamaIndex Workflow](https://docs.llamaindex.ai/en/stable/module_guides/workflow/), making your LlamaIndex Workflows **durable**. Each workflow step is automatically recorded as an Edda Activity, enabling crash recovery and deterministic replay.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
LlamaIndex Workflow is an event-driven orchestration framework for building complex AI pipelines. Edda's integration adds:
|
|
8
|
+
|
|
9
|
+
- **Crash Recovery**: If your workflow crashes, completed steps are replayed from cache
|
|
10
|
+
- **Durable Timers**: Use `DurableSleepEvent` for timers that survive crashes
|
|
11
|
+
- **Durable Event Waiting**: Use `DurableWaitEvent` to wait for external events durably
|
|
12
|
+
- **Step-level Durability**: Each `@step` becomes a durable Edda Activity
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
pip install 'edda-framework[llamaindex]'
|
|
18
|
+
|
|
19
|
+
# Or using uv
|
|
20
|
+
uv add edda-framework --extra llamaindex
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Quick Start
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
from llama_index.core.workflow import Workflow, step, StartEvent, StopEvent, Event
|
|
27
|
+
from edda import EddaApp, workflow, WorkflowContext
|
|
28
|
+
from edda.integrations.llamaindex import DurableWorkflowRunner
|
|
29
|
+
|
|
30
|
+
# Define events
|
|
31
|
+
class OrderReceivedEvent(Event):
|
|
32
|
+
order_id: str
|
|
33
|
+
amount: float
|
|
34
|
+
|
|
35
|
+
class ProcessingCompleteEvent(Event):
|
|
36
|
+
order_id: str
|
|
37
|
+
status: str
|
|
38
|
+
|
|
39
|
+
# Define LlamaIndex Workflow
|
|
40
|
+
class OrderWorkflow(Workflow):
|
|
41
|
+
@step
|
|
42
|
+
async def receive_order(self, ctx, ev: StartEvent) -> OrderReceivedEvent:
|
|
43
|
+
print(f"Received order: {ev.order_id}")
|
|
44
|
+
return OrderReceivedEvent(order_id=ev.order_id, amount=ev.amount)
|
|
45
|
+
|
|
46
|
+
@step
|
|
47
|
+
async def process_order(self, ctx, ev: OrderReceivedEvent) -> ProcessingCompleteEvent:
|
|
48
|
+
print(f"Processing order {ev.order_id}...")
|
|
49
|
+
return ProcessingCompleteEvent(order_id=ev.order_id, status="processed")
|
|
50
|
+
|
|
51
|
+
@step
|
|
52
|
+
async def complete_order(self, ctx, ev: ProcessingCompleteEvent) -> StopEvent:
|
|
53
|
+
return StopEvent(result={
|
|
54
|
+
"order_id": ev.order_id,
|
|
55
|
+
"status": ev.status,
|
|
56
|
+
"message": "Order completed successfully",
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
# Create durable runner
|
|
60
|
+
runner = DurableWorkflowRunner(OrderWorkflow)
|
|
61
|
+
|
|
62
|
+
# Wrap in Edda workflow
|
|
63
|
+
@workflow
|
|
64
|
+
async def order_workflow(ctx: WorkflowContext, order_id: str, amount: float) -> dict:
|
|
65
|
+
result = await runner.run(ctx, order_id=order_id, amount=amount)
|
|
66
|
+
return result
|
|
67
|
+
|
|
68
|
+
# Run the workflow
|
|
69
|
+
async def main():
|
|
70
|
+
app = EddaApp(service_name="order-service", db_url="sqlite:///app.db")
|
|
71
|
+
await app.initialize()
|
|
72
|
+
|
|
73
|
+
instance_id = await order_workflow.start(order_id="ORD-001", amount=99.99)
|
|
74
|
+
print(f"Started workflow: {instance_id}")
|
|
75
|
+
|
|
76
|
+
await app.shutdown()
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## How It Works
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
LlamaIndex Workflow Steps
|
|
83
|
+
│
|
|
84
|
+
▼
|
|
85
|
+
┌──────────────────────┐
|
|
86
|
+
│ DurableWorkflowRunner│ ← Wraps your Workflow class
|
|
87
|
+
└──────────┬───────────┘
|
|
88
|
+
│
|
|
89
|
+
▼
|
|
90
|
+
┌──────────────────────┐
|
|
91
|
+
│ For each @step: │
|
|
92
|
+
│ - Serialize event │
|
|
93
|
+
│ - Run as Activity │
|
|
94
|
+
│ - Deserialize result│
|
|
95
|
+
└──────────┬───────────┘
|
|
96
|
+
│
|
|
97
|
+
▼
|
|
98
|
+
┌──────────────────────┐
|
|
99
|
+
│ Edda @activity │ ← Each step is durable
|
|
100
|
+
│ (_run_step) │
|
|
101
|
+
└──────────────────────┘
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
**On Replay**: If the workflow crashes and restarts, completed steps return cached results from the Edda history. Steps are not re-executed.
|
|
105
|
+
|
|
106
|
+
## Durable Sleep
|
|
107
|
+
|
|
108
|
+
Use `DurableSleepEvent` for timers that survive crashes:
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
from edda.integrations.llamaindex import DurableSleepEvent
|
|
112
|
+
|
|
113
|
+
class RateLimitedWorkflow(Workflow):
|
|
114
|
+
@step
|
|
115
|
+
async def check_rate_limit(self, ctx, ev: StartEvent) -> DurableSleepEvent | ProcessEvent:
|
|
116
|
+
if is_rate_limited():
|
|
117
|
+
# Sleep for 60 seconds (durable timer)
|
|
118
|
+
return DurableSleepEvent(
|
|
119
|
+
seconds=60,
|
|
120
|
+
resume_data={"retry_count": ev.retry_count + 1}
|
|
121
|
+
)
|
|
122
|
+
return ProcessEvent(data=ev.data)
|
|
123
|
+
|
|
124
|
+
@step
|
|
125
|
+
async def handle_resume(self, ctx, ev: ResumeEvent) -> ProcessEvent:
|
|
126
|
+
# Called after DurableSleepEvent completes
|
|
127
|
+
retry_count = ev.data.get("retry_count", 0)
|
|
128
|
+
return ProcessEvent(data={"retried": retry_count})
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## Durable Wait for External Events
|
|
132
|
+
|
|
133
|
+
Use `DurableWaitEvent` to wait for external events:
|
|
134
|
+
|
|
135
|
+
```python
|
|
136
|
+
from edda.integrations.llamaindex import DurableWaitEvent, ResumeEvent
|
|
137
|
+
|
|
138
|
+
class ApprovalWorkflow(Workflow):
|
|
139
|
+
@step
|
|
140
|
+
async def request_approval(self, ctx, ev: StartEvent) -> DurableWaitEvent:
|
|
141
|
+
# Wait for approval event (up to 1 hour)
|
|
142
|
+
return DurableWaitEvent(
|
|
143
|
+
event_type=f"approval.{ev.request_id}",
|
|
144
|
+
timeout_seconds=3600,
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
@step
|
|
148
|
+
async def process_approval(self, ctx, ev: ResumeEvent) -> StopEvent:
|
|
149
|
+
# Called when external event arrives
|
|
150
|
+
approved = ev.data.get("approved", False)
|
|
151
|
+
return StopEvent(result={"approved": approved})
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
To send the external event:
|
|
155
|
+
|
|
156
|
+
```python
|
|
157
|
+
from edda import send_event
|
|
158
|
+
|
|
159
|
+
await send_event(
|
|
160
|
+
event_type="approval.REQ-123",
|
|
161
|
+
source="approval-service",
|
|
162
|
+
data={"approved": True},
|
|
163
|
+
)
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
## API Reference
|
|
167
|
+
|
|
168
|
+
### DurableWorkflowRunner
|
|
169
|
+
|
|
170
|
+
```python
|
|
171
|
+
from edda.integrations.llamaindex import DurableWorkflowRunner
|
|
172
|
+
|
|
173
|
+
runner = DurableWorkflowRunner(MyWorkflow)
|
|
174
|
+
result = await runner.run(ctx, **start_event_kwargs)
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
| Parameter | Type | Description |
|
|
178
|
+
|-----------|------|-------------|
|
|
179
|
+
| `workflow_class` | `type[Workflow]` | LlamaIndex Workflow class (not instance) |
|
|
180
|
+
|
|
181
|
+
### DurableSleepEvent
|
|
182
|
+
|
|
183
|
+
```python
|
|
184
|
+
DurableSleepEvent(
|
|
185
|
+
seconds: float, # Duration to sleep
|
|
186
|
+
resume_data: dict = None, # Data passed to ResumeEvent
|
|
187
|
+
)
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### DurableWaitEvent
|
|
191
|
+
|
|
192
|
+
```python
|
|
193
|
+
DurableWaitEvent(
|
|
194
|
+
event_type: str, # Event type to wait for
|
|
195
|
+
timeout_seconds: float = None, # Optional timeout
|
|
196
|
+
)
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### ResumeEvent
|
|
200
|
+
|
|
201
|
+
Returned after `DurableSleepEvent` or `DurableWaitEvent` completes:
|
|
202
|
+
|
|
203
|
+
```python
|
|
204
|
+
class ResumeEvent:
|
|
205
|
+
data: dict # Contains resume_data or received event data
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
## Related Documentation
|
|
209
|
+
|
|
210
|
+
- [Workflows and Activities](../core-features/workflows-activities.md) - Core concepts of durable execution
|
|
211
|
+
- [Event Waiting](../core-features/events/wait-event.md) - wait_event() function
|
|
212
|
+
- [LlamaIndex Workflow Docs](https://docs.llamaindex.ai/en/stable/module_guides/workflow/) - Official LlamaIndex documentation
|
|
213
|
+
- [Examples](https://github.com/i2y/edda/tree/main/examples/llamaindex) - Complete working examples
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
# pydantic-graph Integration (Experimental)
|
|
2
|
+
|
|
3
|
+
> **Status: Experimental**
|
|
4
|
+
> This integration is functional but has known limitations with type hints. The API may change in future versions.
|
|
5
|
+
|
|
6
|
+
Edda provides integration with [pydantic-graph](https://ai.pydantic.dev/pydantic-graph/), making graph-based workflows **durable**. Each node execution is automatically recorded as an Edda Activity, enabling crash recovery and deterministic replay.
|
|
7
|
+
|
|
8
|
+
## Overview
|
|
9
|
+
|
|
10
|
+
pydantic-graph is a graph-based workflow library that uses dataclass nodes and type-safe transitions. Edda's integration adds:
|
|
11
|
+
|
|
12
|
+
- **Crash Recovery**: If your workflow crashes, completed nodes are replayed from cache
|
|
13
|
+
- **Durable Timers**: Use `Sleep` marker for timers that survive crashes
|
|
14
|
+
- **Durable Event Waiting**: Use `WaitForEvent` marker to wait for external events durably
|
|
15
|
+
- **Node-level Durability**: Each node execution becomes a durable Edda Activity
|
|
16
|
+
|
|
17
|
+
## Known Limitations
|
|
18
|
+
|
|
19
|
+
**Type hint limitation**: The `Sleep` and `WaitForEvent` markers cannot be included in node return type annotations due to pydantic-graph's strict type validation. You need to use `# type: ignore` comments:
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
# This works at runtime but requires type: ignore
|
|
23
|
+
async def run(self, ctx: DurableGraphContext) -> "NextNode": # type: ignore[override]
|
|
24
|
+
return Sleep(seconds=60, next_node=NextNode())
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
If you need clean type hints for graph visualization, consider the [LlamaIndex Workflow integration](./llamaindex.md) instead.
|
|
28
|
+
|
|
29
|
+
## Installation
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pip install 'edda-framework[graph]'
|
|
33
|
+
|
|
34
|
+
# Or using uv
|
|
35
|
+
uv add edda-framework --extra graph
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Quick Start
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
from dataclasses import dataclass
|
|
42
|
+
from pydantic_graph import BaseNode, End, Graph
|
|
43
|
+
from edda import EddaApp, workflow, WorkflowContext
|
|
44
|
+
from edda.integrations.graph import DurableGraph, DurableGraphContext
|
|
45
|
+
|
|
46
|
+
# Define state
|
|
47
|
+
@dataclass
|
|
48
|
+
class OrderState:
|
|
49
|
+
order_id: str | None = None
|
|
50
|
+
total: float = 0.0
|
|
51
|
+
status: str = "pending"
|
|
52
|
+
|
|
53
|
+
# Define nodes
|
|
54
|
+
@dataclass
|
|
55
|
+
class ValidateOrderNode(BaseNode[OrderState, None, dict]):
|
|
56
|
+
order_id: str
|
|
57
|
+
amount: float
|
|
58
|
+
|
|
59
|
+
async def run(self, ctx: DurableGraphContext) -> "ProcessPaymentNode":
|
|
60
|
+
ctx.state.order_id = self.order_id
|
|
61
|
+
ctx.state.total = self.amount
|
|
62
|
+
ctx.state.status = "validated"
|
|
63
|
+
return ProcessPaymentNode()
|
|
64
|
+
|
|
65
|
+
@dataclass
|
|
66
|
+
class ProcessPaymentNode(BaseNode[OrderState, None, dict]):
|
|
67
|
+
async def run(self, ctx: DurableGraphContext) -> End[dict]:
|
|
68
|
+
ctx.state.status = "paid"
|
|
69
|
+
return End({
|
|
70
|
+
"order_id": ctx.state.order_id,
|
|
71
|
+
"total": ctx.state.total,
|
|
72
|
+
"status": ctx.state.status,
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
# Create graph and durable wrapper
|
|
76
|
+
graph = Graph(nodes=[ValidateOrderNode, ProcessPaymentNode])
|
|
77
|
+
durable_graph = DurableGraph(graph)
|
|
78
|
+
|
|
79
|
+
# Wrap in Edda workflow
|
|
80
|
+
@workflow
|
|
81
|
+
async def order_workflow(ctx: WorkflowContext, order_id: str, amount: float) -> dict:
|
|
82
|
+
return await durable_graph.run(
|
|
83
|
+
ctx,
|
|
84
|
+
start_node=ValidateOrderNode(order_id=order_id, amount=amount),
|
|
85
|
+
state=OrderState(),
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
# Run the workflow
|
|
89
|
+
async def main():
|
|
90
|
+
app = EddaApp(service_name="order-service", db_url="sqlite:///app.db")
|
|
91
|
+
await app.initialize()
|
|
92
|
+
|
|
93
|
+
instance_id = await order_workflow.start(order_id="ORD-001", amount=99.99)
|
|
94
|
+
print(f"Started workflow: {instance_id}")
|
|
95
|
+
|
|
96
|
+
await app.shutdown()
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## How It Works
|
|
100
|
+
|
|
101
|
+
```
|
|
102
|
+
pydantic-graph Nodes
|
|
103
|
+
│
|
|
104
|
+
▼
|
|
105
|
+
┌──────────────────────┐
|
|
106
|
+
│ DurableGraph │ ← Wraps your Graph
|
|
107
|
+
└──────────┬───────────┘
|
|
108
|
+
│
|
|
109
|
+
▼
|
|
110
|
+
┌──────────────────────┐
|
|
111
|
+
│ For each node: │
|
|
112
|
+
│ - Serialize state │
|
|
113
|
+
│ - Run as Activity │
|
|
114
|
+
│ - Deserialize result│
|
|
115
|
+
└──────────┬───────────┘
|
|
116
|
+
│
|
|
117
|
+
▼
|
|
118
|
+
┌──────────────────────┐
|
|
119
|
+
│ Edda @activity │ ← Each node is durable
|
|
120
|
+
│ (_run_graph_node) │
|
|
121
|
+
└──────────────────────┘
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
**On Replay**: If the workflow crashes and restarts, completed nodes return cached results from the Edda history. Nodes are not re-executed.
|
|
125
|
+
|
|
126
|
+
## Durable Sleep
|
|
127
|
+
|
|
128
|
+
Use the `Sleep` marker for timers that survive crashes:
|
|
129
|
+
|
|
130
|
+
```python
|
|
131
|
+
from edda.integrations.graph import Sleep
|
|
132
|
+
|
|
133
|
+
@dataclass
|
|
134
|
+
class RateLimitNode(BaseNode[MyState, None, str]):
|
|
135
|
+
async def run(self, ctx: DurableGraphContext) -> "RetryNode": # type: ignore[override]
|
|
136
|
+
if is_rate_limited():
|
|
137
|
+
# Sleep for 60 seconds (durable timer)
|
|
138
|
+
return Sleep(seconds=60, next_node=RetryNode())
|
|
139
|
+
return RetryNode()
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## Durable Wait for External Events
|
|
143
|
+
|
|
144
|
+
Use the `WaitForEvent` marker to wait for external events:
|
|
145
|
+
|
|
146
|
+
```python
|
|
147
|
+
from edda.integrations.graph import WaitForEvent, ReceivedEvent
|
|
148
|
+
|
|
149
|
+
@dataclass
|
|
150
|
+
class WaitForApprovalNode(BaseNode[OrderState, None, str]):
|
|
151
|
+
async def run(self, ctx: DurableGraphContext) -> "ProcessApprovalNode": # type: ignore[override]
|
|
152
|
+
return WaitForEvent(
|
|
153
|
+
event_type=f"approval.{ctx.state.order_id}",
|
|
154
|
+
next_node=ProcessApprovalNode(),
|
|
155
|
+
timeout_seconds=3600,
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
@dataclass
|
|
159
|
+
class ProcessApprovalNode(BaseNode[OrderState, None, str]):
|
|
160
|
+
async def run(self, ctx: DurableGraphContext) -> End[str]:
|
|
161
|
+
# Access the received event via ctx.last_event
|
|
162
|
+
event: ReceivedEvent = ctx.last_event
|
|
163
|
+
if event.data.get("approved"):
|
|
164
|
+
return End("approved")
|
|
165
|
+
return End("rejected")
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
To send the external event:
|
|
169
|
+
|
|
170
|
+
```python
|
|
171
|
+
from edda import send_event
|
|
172
|
+
|
|
173
|
+
await send_event(
|
|
174
|
+
event_type="approval.ORD-123",
|
|
175
|
+
source="approval-service",
|
|
176
|
+
data={"approved": True},
|
|
177
|
+
)
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
## DurableGraphContext
|
|
181
|
+
|
|
182
|
+
The `DurableGraphContext` provides access to state, dependencies, and received events:
|
|
183
|
+
|
|
184
|
+
```python
|
|
185
|
+
@dataclass
|
|
186
|
+
class MyNode(BaseNode[MyState, MyDeps, str]):
|
|
187
|
+
async def run(self, ctx: DurableGraphContext) -> End[str]:
|
|
188
|
+
# Access state (mutable)
|
|
189
|
+
ctx.state.counter += 1
|
|
190
|
+
|
|
191
|
+
# Access dependencies (read-only)
|
|
192
|
+
api_key = ctx.deps.api_key
|
|
193
|
+
|
|
194
|
+
# Access last received event (after WaitForEvent)
|
|
195
|
+
if ctx.last_event:
|
|
196
|
+
data = ctx.last_event.data
|
|
197
|
+
|
|
198
|
+
# Access Edda WorkflowContext for advanced operations
|
|
199
|
+
edda_ctx = ctx.workflow_ctx
|
|
200
|
+
|
|
201
|
+
return End("done")
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
## API Reference
|
|
205
|
+
|
|
206
|
+
### DurableGraph
|
|
207
|
+
|
|
208
|
+
```python
|
|
209
|
+
from edda.integrations.graph import DurableGraph
|
|
210
|
+
|
|
211
|
+
durable = DurableGraph(graph)
|
|
212
|
+
result = await durable.run(ctx, start_node=MyNode(), state=MyState(), deps=MyDeps())
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
| Parameter | Type | Description |
|
|
216
|
+
|-----------|------|-------------|
|
|
217
|
+
| `graph` | `Graph` | pydantic-graph Graph instance |
|
|
218
|
+
|
|
219
|
+
### Sleep
|
|
220
|
+
|
|
221
|
+
```python
|
|
222
|
+
Sleep(
|
|
223
|
+
seconds: int, # Duration to sleep
|
|
224
|
+
next_node: NextT, # Node to continue with after sleep
|
|
225
|
+
)
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
### WaitForEvent
|
|
229
|
+
|
|
230
|
+
```python
|
|
231
|
+
WaitForEvent(
|
|
232
|
+
event_type: str, # Event type to wait for
|
|
233
|
+
next_node: NextT, # Node to continue with after event
|
|
234
|
+
timeout_seconds: int | None, # Optional timeout
|
|
235
|
+
)
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
### ReceivedEvent
|
|
239
|
+
|
|
240
|
+
Available via `ctx.last_event` after `WaitForEvent` completes:
|
|
241
|
+
|
|
242
|
+
```python
|
|
243
|
+
@dataclass
|
|
244
|
+
class ReceivedEvent:
|
|
245
|
+
event_type: str # The event type that was received
|
|
246
|
+
data: dict[str, Any] # Event payload
|
|
247
|
+
metadata: dict[str, Any] # Event metadata
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
## Related Documentation
|
|
251
|
+
|
|
252
|
+
- [Workflows and Activities](../core-features/workflows-activities.md) - Core concepts of durable execution
|
|
253
|
+
- [Event Waiting](../core-features/events/wait-event.md) - wait_event() function
|
|
254
|
+
- [LlamaIndex Integration](./llamaindex.md) - Alternative with better type hint support
|
|
255
|
+
- [pydantic-graph Documentation](https://ai.pydantic.dev/pydantic-graph/) - Official pydantic-graph docs
|
|
256
|
+
- [Examples](https://github.com/i2y/edda/tree/main/examples/graph) - Complete working examples
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Durable Graph Integration for Edda.
|
|
3
|
+
|
|
4
|
+
This module provides integration between pydantic-graph and Edda's durable
|
|
5
|
+
execution framework, making pydantic-graph execution crash-recoverable and
|
|
6
|
+
supporting durable wait operations.
|
|
7
|
+
|
|
8
|
+
Example:
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from pydantic_graph import BaseNode, Graph, End
|
|
11
|
+
from edda import workflow, WorkflowContext
|
|
12
|
+
from edda.integrations.graph import DurableGraph, DurableGraphContext
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class MyState:
|
|
16
|
+
counter: int = 0
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class IncrementNode(BaseNode[MyState, None, int]):
|
|
20
|
+
async def run(self, ctx: DurableGraphContext) -> "CheckNode":
|
|
21
|
+
ctx.state.counter += 1
|
|
22
|
+
return CheckNode()
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class CheckNode(BaseNode[MyState, None, int]):
|
|
26
|
+
async def run(self, ctx: DurableGraphContext) -> IncrementNode | End[int]:
|
|
27
|
+
if ctx.state.counter >= 5:
|
|
28
|
+
return End(ctx.state.counter)
|
|
29
|
+
return IncrementNode()
|
|
30
|
+
|
|
31
|
+
graph = Graph(nodes=[IncrementNode, CheckNode])
|
|
32
|
+
durable = DurableGraph(graph)
|
|
33
|
+
|
|
34
|
+
@workflow
|
|
35
|
+
async def counter_workflow(ctx: WorkflowContext) -> int:
|
|
36
|
+
return await durable.run(
|
|
37
|
+
ctx,
|
|
38
|
+
start_node=IncrementNode(),
|
|
39
|
+
state=MyState(),
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
Installation:
|
|
43
|
+
pip install 'edda-framework[graph]'
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
from .context import DurableGraphContext
|
|
47
|
+
from .exceptions import GraphExecutionError
|
|
48
|
+
from .graph import DurableGraph
|
|
49
|
+
from .nodes import ReceivedEvent, Sleep, WaitForEvent
|
|
50
|
+
|
|
51
|
+
__all__ = [
|
|
52
|
+
"DurableGraph",
|
|
53
|
+
"DurableGraphContext",
|
|
54
|
+
"GraphExecutionError",
|
|
55
|
+
"ReceivedEvent",
|
|
56
|
+
"Sleep",
|
|
57
|
+
"WaitForEvent",
|
|
58
|
+
]
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""DurableGraphContext - bridges pydantic-graph and Edda contexts."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from typing import TYPE_CHECKING, Generic, TypeVar
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from edda.context import WorkflowContext
|
|
10
|
+
|
|
11
|
+
from .nodes import ReceivedEvent
|
|
12
|
+
|
|
13
|
+
StateT = TypeVar("StateT")
|
|
14
|
+
DepsT = TypeVar("DepsT")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class DurableGraphContext(Generic[StateT, DepsT]):
|
|
19
|
+
"""
|
|
20
|
+
Context that bridges pydantic-graph and Edda.
|
|
21
|
+
|
|
22
|
+
Provides access to:
|
|
23
|
+
- pydantic-graph's state and deps via properties
|
|
24
|
+
- last_event: The most recent event received via WaitForEvent
|
|
25
|
+
|
|
26
|
+
This context is passed to node's run() method when executing
|
|
27
|
+
via DurableGraph.
|
|
28
|
+
|
|
29
|
+
For durable wait operations (wait_event, sleep), use the WaitForEvent
|
|
30
|
+
and Sleep marker nodes instead of calling methods directly:
|
|
31
|
+
|
|
32
|
+
from edda.integrations.graph import WaitForEvent, Sleep
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class MyNode(BaseNode[MyState, None, str]):
|
|
36
|
+
async def run(self, ctx: DurableGraphContext) -> WaitForEvent[NextNode]:
|
|
37
|
+
# Return a marker to wait for an event
|
|
38
|
+
return WaitForEvent(
|
|
39
|
+
event_type="payment.completed",
|
|
40
|
+
next_node=NextNode(),
|
|
41
|
+
timeout_seconds=3600,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
@dataclass
|
|
45
|
+
class NextNode(BaseNode[MyState, None, str]):
|
|
46
|
+
async def run(self, ctx: DurableGraphContext) -> End[str]:
|
|
47
|
+
# Access the received event
|
|
48
|
+
event = ctx.last_event
|
|
49
|
+
return End(event.data.get("status", "unknown"))
|
|
50
|
+
|
|
51
|
+
Attributes:
|
|
52
|
+
state: The graph state object (mutable, shared across nodes)
|
|
53
|
+
deps: The dependencies object (immutable)
|
|
54
|
+
last_event: The most recent event received via WaitForEvent (or None)
|
|
55
|
+
workflow_ctx: The Edda WorkflowContext
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
_state: StateT
|
|
59
|
+
_deps: DepsT
|
|
60
|
+
workflow_ctx: WorkflowContext
|
|
61
|
+
last_event: ReceivedEvent | None = field(default=None)
|
|
62
|
+
|
|
63
|
+
@property
|
|
64
|
+
def state(self) -> StateT:
|
|
65
|
+
"""Get the graph state object."""
|
|
66
|
+
return self._state
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def deps(self) -> DepsT:
|
|
70
|
+
"""Get the dependencies object."""
|
|
71
|
+
return self._deps
|
|
72
|
+
|
|
73
|
+
@property
|
|
74
|
+
def instance_id(self) -> str:
|
|
75
|
+
"""Get the workflow instance ID."""
|
|
76
|
+
return self.workflow_ctx.instance_id
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
def is_replaying(self) -> bool:
|
|
80
|
+
"""Check if the workflow is currently replaying."""
|
|
81
|
+
return self.workflow_ctx.is_replaying
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""Exceptions for durable graph integration."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class GraphExecutionError(Exception):
|
|
5
|
+
"""Raised when a graph node execution fails."""
|
|
6
|
+
|
|
7
|
+
def __init__(self, message: str, node_name: str | None = None) -> None:
|
|
8
|
+
self.node_name = node_name
|
|
9
|
+
super().__init__(message)
|