edda-framework 0.14.0__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.0 → edda_framework-0.15.0}/PKG-INFO +13 -1
- {edda_framework-0.14.0 → 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.14.0 → edda_framework-0.15.0}/edda/app.py +6 -21
- 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.14.0 → edda_framework-0.15.0}/edda/locking.py +12 -37
- 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.0 → edda_framework-0.15.0}/pyproject.toml +12 -1
- {edda_framework-0.14.0 → edda_framework-0.15.0}/tests/test_cross_language_channel.py +637 -0
- edda_framework-0.15.0/tests/test_graph_integration.py +420 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/tests/test_stale_workflow_recovery.py +0 -3
- {edda_framework-0.14.0 → edda_framework-0.15.0}/uv.lock +468 -2
- {edda_framework-0.14.0 → edda_framework-0.15.0}/.github/workflows/ci.yml +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/.github/workflows/docs.yml +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/.github/workflows/release.yml +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/.gitignore +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/.gitmodules +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/.python-version +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/Justfile +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/LICENSE +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/demo_app.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/docs/api/reference.md +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/docs/core-features/durable-execution/replay.md +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/docs/core-features/events/cloudevents-http-binding.md +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/docs/core-features/events/postgres-notify.md +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/docs/core-features/events/wait-event.md +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/docs/core-features/hooks.md +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/docs/core-features/messages.md +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/docs/core-features/retry.md +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/docs/core-features/saga-compensation.md +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/docs/core-features/transactional-outbox.md +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/docs/core-features/workflows-activities.md +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/docs/examples/ecommerce.md +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/docs/examples/events.md +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/docs/examples/fastapi-integration.md +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/docs/examples/saga.md +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/docs/examples/simple.md +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/docs/getting-started/concepts.md +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/docs/getting-started/first-workflow.md +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/docs/getting-started/installation.md +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/docs/getting-started/quick-start.md +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/docs/index.md +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/docs/integrations/mcp.md +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/docs/integrations/mirascope.md +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/docs/integrations/opentelemetry.md +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/docs/integrations/pydantic-rpc.md +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/docs/viewer-ui/images/cloudevents-cli-trigger.png +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/docs/viewer-ui/images/compensation-execution.png +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/docs/viewer-ui/images/detail-page-loan-approval.png +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/docs/viewer-ui/images/detail-page-match-case.png +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/docs/viewer-ui/images/nested-pydantic-form.png +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/docs/viewer-ui/images/start-workflow-form-pydantic.png +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/docs/viewer-ui/images/wait-event-visualization.png +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/docs/viewer-ui/images/workflow-list-view.png +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/docs/viewer-ui/images/workflow-selection-dropdown.png +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/docs/viewer-ui/setup.md +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/docs/viewer-ui/visualization.md +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/edda/__init__.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/edda/activity.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/edda/channels.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/edda/compensation.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/edda/context.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/edda/exceptions.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/edda/hooks.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/edda/integrations/__init__.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/edda/integrations/mcp/__init__.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/edda/integrations/mcp/decorators.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/edda/integrations/mcp/server.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/edda/integrations/mirascope/__init__.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/edda/integrations/mirascope/agent.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/edda/integrations/mirascope/call.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/edda/integrations/mirascope/decorator.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/edda/integrations/mirascope/types.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/edda/integrations/opentelemetry/__init__.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/edda/integrations/opentelemetry/hooks.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/edda/migrations/mysql/20251217000000_initial_schema.sql +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/edda/migrations/postgresql/20251217000000_initial_schema.sql +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/edda/migrations/sqlite/20251217000000_initial_schema.sql +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/edda/outbox/__init__.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/edda/outbox/relayer.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/edda/outbox/transactional.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/edda/pydantic_utils.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/edda/replay.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/edda/retry.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/edda/serialization/__init__.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/edda/serialization/base.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/edda/serialization/json.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/edda/storage/__init__.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/edda/storage/migrations.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/edda/storage/models.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/edda/storage/notify_base.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/edda/storage/pg_notify.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/edda/storage/protocol.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/edda/storage/sqlalchemy_storage.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/edda/viewer_ui/__init__.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/edda/viewer_ui/app.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/edda/viewer_ui/components.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/edda/viewer_ui/data_service.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/edda/viewer_ui/theme.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/edda/visualizer/__init__.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/edda/visualizer/ast_analyzer.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/edda/visualizer/mermaid_generator.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/edda/workflow.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/edda/wsgi.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/examples/__init__.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/examples/cancellable_workflow.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/examples/compensation_workflow.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/examples/event_waiting_app.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/examples/event_waiting_workflow.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/examples/event_waiting_workflow_complete.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/examples/long_running_loop.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/examples/mcp/README.md +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/examples/mcp/order_processing_mcp.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/examples/mcp/prompts_example.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/examples/mcp/remote_server_example.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/examples/mcp/simple_mcp_server.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/examples/message_passing.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/examples/mirascope/__init__.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/examples/mirascope/durable_agent.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/examples/mirascope/multi_turn.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/examples/mirascope/simple_call.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/examples/mirascope/with_tools.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/examples/observability_with_logfire.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/examples/observability_with_opentelemetry.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/examples/pydantic_rpc_integration.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/examples/pydantic_saga.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/examples/retry_example.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/examples/retry_with_compensation.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/examples/simple_workflow.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/examples/typeddict_example.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/examples/with_outbox.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/schema/.dbmate.yml +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/schema/.git +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/schema/.gitignore +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/schema/LICENSE +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/schema/README.md +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/schema/docs/column-values.md +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/tests/__init__.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/tests/conftest.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/tests/integrations/__init__.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/tests/integrations/mcp/__init__.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/tests/integrations/mcp/test_cancel.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/tests/integrations/mcp/test_integration.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/tests/integrations/mcp/test_jsonrpc.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/tests/integrations/mcp/test_prompts.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/tests/integrations/mcp/test_server.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/tests/integrations/mirascope/__init__.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/tests/integrations/mirascope/test_agent.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/tests/integrations/mirascope/test_call.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/tests/integrations/mirascope/test_decorator.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/tests/integrations/mirascope/test_types.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/tests/integrations/opentelemetry/__init__.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/tests/integrations/opentelemetry/test_hooks.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/tests/test_activity.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/tests/test_activity_retry.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/tests/test_activity_sync.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/tests/test_app.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/tests/test_ast_analyzer.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/tests/test_atomic_wait_event.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/tests/test_auto_migration.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/tests/test_binary_data.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/tests/test_channel_competing.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/tests/test_channel_direct.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/tests/test_channel_mode_locking.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/tests/test_channel_transactional.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/tests/test_cloudevents_http_binding.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/tests/test_compensation.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/tests/test_compensation_crash_recovery.py.wip +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/tests/test_concurrent_outbox.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/tests/test_context.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/tests/test_ctx_session.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/tests/test_distributed_event_delivery.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/tests/test_events.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/tests/test_instance_id_routing.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/tests/test_lock_race_condition.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/tests/test_lock_timeout_customization.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/tests/test_locking.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/tests/test_message_cleanup.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/tests/test_message_delivery_lock.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/tests/test_messages.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/tests/test_migrations_integration.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/tests/test_multidb_storage.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/tests/test_outbox.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/tests/test_pg_notify.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/tests/test_polling_optimization.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/tests/test_pydantic_activity.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/tests/test_pydantic_enum.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/tests/test_pydantic_events.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/tests/test_pydantic_saga.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/tests/test_pydantic_utils.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/tests/test_receive_timeout.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/tests/test_received_event.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/tests/test_recur.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/tests/test_recur_cleanup.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/tests/test_replay.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/tests/test_retry_policy.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/tests/test_saga_parameter_extraction.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/tests/test_serialization.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/tests/test_skip_locked.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/tests/test_storage.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/tests/test_storage_mysql.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/tests/test_storage_postgresql.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/tests/test_transactions.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/tests/test_viewer_pagination.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/tests/test_viewer_pydantic_form.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/tests/test_viewer_start_saga.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/tests/test_wait_timer.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/tests/test_workflow.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/tests/test_workflow_auto_register.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/tests/test_workflow_cancellation.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/tests/test_workflow_resumption.py +0 -0
- {edda_framework-0.14.0 → edda_framework-0.15.0}/viewer_app.py +0 -0
- {edda_framework-0.14.0 → 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
|
|
@@ -583,7 +583,6 @@ class EddaApp:
|
|
|
583
583
|
auto_resume_stale_workflows_periodically(
|
|
584
584
|
self.storage,
|
|
585
585
|
self.replay_engine,
|
|
586
|
-
self.worker_id,
|
|
587
586
|
interval=60,
|
|
588
587
|
),
|
|
589
588
|
name="leader_stale_workflow_resume",
|
|
@@ -628,7 +627,6 @@ class EddaApp:
|
|
|
628
627
|
auto_resume_stale_workflows_periodically(
|
|
629
628
|
self.storage,
|
|
630
629
|
self.replay_engine,
|
|
631
|
-
self.worker_id,
|
|
632
630
|
interval=60,
|
|
633
631
|
),
|
|
634
632
|
name="leader_stale_workflow_resume",
|
|
@@ -1411,7 +1409,8 @@ class EddaApp:
|
|
|
1411
1409
|
from growing indefinitely with orphaned messages (messages that were
|
|
1412
1410
|
published but never received by any subscriber).
|
|
1413
1411
|
|
|
1414
|
-
|
|
1412
|
+
Important: This task should only be run by a single worker (e.g., via leader
|
|
1413
|
+
election). It does not perform its own distributed coordination.
|
|
1415
1414
|
|
|
1416
1415
|
Args:
|
|
1417
1416
|
interval: Cleanup interval in seconds (default: 3600 = 1 hour)
|
|
@@ -1422,27 +1421,13 @@ class EddaApp:
|
|
|
1422
1421
|
"""
|
|
1423
1422
|
while True:
|
|
1424
1423
|
try:
|
|
1425
|
-
# Add jitter to prevent thundering herd
|
|
1424
|
+
# Add jitter to prevent thundering herd
|
|
1426
1425
|
jitter = random.uniform(0, interval * 0.3)
|
|
1427
1426
|
await asyncio.sleep(interval + jitter)
|
|
1428
1427
|
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
worker_id=self.worker_id,
|
|
1433
|
-
timeout_seconds=interval,
|
|
1434
|
-
)
|
|
1435
|
-
|
|
1436
|
-
if not lock_acquired:
|
|
1437
|
-
# Another pod is handling this task
|
|
1438
|
-
continue
|
|
1439
|
-
|
|
1440
|
-
try:
|
|
1441
|
-
deleted_count = await self.storage.cleanup_old_channel_messages(retention_days)
|
|
1442
|
-
if deleted_count > 0:
|
|
1443
|
-
logger.info("Cleaned up %d old channel messages", deleted_count)
|
|
1444
|
-
finally:
|
|
1445
|
-
await self.storage.release_system_lock("cleanup_old_messages", self.worker_id)
|
|
1428
|
+
deleted_count = await self.storage.cleanup_old_channel_messages(retention_days)
|
|
1429
|
+
if deleted_count > 0:
|
|
1430
|
+
logger.info("Cleaned up %d old channel messages", deleted_count)
|
|
1446
1431
|
except Exception as e:
|
|
1447
1432
|
logger.error("Error cleaning up old messages: %s", e, exc_info=True)
|
|
1448
1433
|
|
|
@@ -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
|
+
]
|