algen-agent-runtime 0.1.0a1__py3-none-any.whl
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.
- algen_agent_runtime/__init__.py +50 -0
- algen_agent_runtime/analytics/__init__.py +15 -0
- algen_agent_runtime/analytics/contracts.py +210 -0
- algen_agent_runtime/analytics/engine.py +397 -0
- algen_agent_runtime/analytics/handlers.py +167 -0
- algen_agent_runtime/analytics/stores.py +74 -0
- algen_agent_runtime/api/app.py +631 -0
- algen_agent_runtime/api/dependencies.py +86 -0
- algen_agent_runtime/approvals/service.py +194 -0
- algen_agent_runtime/artifacts/__init__.py +7 -0
- algen_agent_runtime/artifacts/service.py +83 -0
- algen_agent_runtime/cache/__init__.py +15 -0
- algen_agent_runtime/cache/contracts.py +49 -0
- algen_agent_runtime/cache/service.py +233 -0
- algen_agent_runtime/cache/stores.py +145 -0
- algen_agent_runtime/communications/__init__.py +25 -0
- algen_agent_runtime/communications/email.py +319 -0
- algen_agent_runtime/config/registry.py +35 -0
- algen_agent_runtime/config/settings.py +497 -0
- algen_agent_runtime/context/builder.py +203 -0
- algen_agent_runtime/conversations/__init__.py +55 -0
- algen_agent_runtime/conversations/contracts.py +206 -0
- algen_agent_runtime/conversations/feedback.py +237 -0
- algen_agent_runtime/conversations/followups.py +105 -0
- algen_agent_runtime/conversations/presentation.py +48 -0
- algen_agent_runtime/conversations/service.py +637 -0
- algen_agent_runtime/conversations/stores.py +336 -0
- algen_agent_runtime/conversations/visualization.py +55 -0
- algen_agent_runtime/distributed/__init__.py +12 -0
- algen_agent_runtime/distributed/contracts.py +44 -0
- algen_agent_runtime/distributed/queue.py +182 -0
- algen_agent_runtime/distributed/worker.py +50 -0
- algen_agent_runtime/evaluation/__init__.py +17 -0
- algen_agent_runtime/evaluation/contracts.py +51 -0
- algen_agent_runtime/evaluation/runner.py +102 -0
- algen_agent_runtime/events/bus.py +172 -0
- algen_agent_runtime/events/contracts.py +63 -0
- algen_agent_runtime/exceptions/__init__.py +1 -0
- algen_agent_runtime/exceptions/errors.py +51 -0
- algen_agent_runtime/frameworks/__init__.py +31 -0
- algen_agent_runtime/frameworks/adapters.py +281 -0
- algen_agent_runtime/frameworks/contracts.py +101 -0
- algen_agent_runtime/governance/__init__.py +25 -0
- algen_agent_runtime/governance/contracts.py +77 -0
- algen_agent_runtime/governance/engine.py +126 -0
- algen_agent_runtime/governance/executor.py +124 -0
- algen_agent_runtime/methods/__init__.py +16 -0
- algen_agent_runtime/methods/contracts.py +75 -0
- algen_agent_runtime/methods/registry.py +139 -0
- algen_agent_runtime/model_services/__init__.py +25 -0
- algen_agent_runtime/model_services/contracts.py +76 -0
- algen_agent_runtime/model_services/http.py +68 -0
- algen_agent_runtime/model_services/registry.py +126 -0
- algen_agent_runtime/models/base.py +242 -0
- algen_agent_runtime/models/providers/adapters.py +409 -0
- algen_agent_runtime/models/providers/local_transformers.py +58 -0
- algen_agent_runtime/models/providers/mock.py +88 -0
- algen_agent_runtime/models/providers/openai_compatible.py +447 -0
- algen_agent_runtime/observability/setup.py +55 -0
- algen_agent_runtime/observability/traccia_adapter.py +384 -0
- algen_agent_runtime/observability/trace_levels.py +54 -0
- algen_agent_runtime/orchestration/container.py +698 -0
- algen_agent_runtime/persistence/memory.py +164 -0
- algen_agent_runtime/persistence/migrations/001_initial.sql +75 -0
- algen_agent_runtime/persistence/migrations/002_conversations.sql +32 -0
- algen_agent_runtime/persistence/migrations/003_analytical_execution.sql +32 -0
- algen_agent_runtime/persistence/migrations/004_conversation_feedback.sql +13 -0
- algen_agent_runtime/persistence/migrations/005_algen_agent_runtime_rename.sql +54 -0
- algen_agent_runtime/persistence/migrations/006_artifact_lifecycle.sql +33 -0
- algen_agent_runtime/persistence/migrations/007_s3_artifacts.sql +21 -0
- algen_agent_runtime/persistence/migrations/008_workflow_checkpoints.sql +18 -0
- algen_agent_runtime/persistence/postgres.py +412 -0
- algen_agent_runtime/persistence/redis.py +87 -0
- algen_agent_runtime/persistence/s3.py +245 -0
- algen_agent_runtime/persistence/tool_executions.py +49 -0
- algen_agent_runtime/planning/contracts.py +54 -0
- algen_agent_runtime/planning/planners.py +92 -0
- algen_agent_runtime/plugins/sdk.py +66 -0
- algen_agent_runtime/policies/contracts.py +40 -0
- algen_agent_runtime/policies/engine.py +446 -0
- algen_agent_runtime/policies/instrumentation.py +65 -0
- algen_agent_runtime/py.typed +1 -0
- algen_agent_runtime/responses/composer.py +35 -0
- algen_agent_runtime/retrieval/contracts.py +60 -0
- algen_agent_runtime/retrieval/ingestion.py +52 -0
- algen_agent_runtime/retrieval/memory.py +206 -0
- algen_agent_runtime/retrieval/registry.py +24 -0
- algen_agent_runtime/retrieval/vector_stores.py +610 -0
- algen_agent_runtime/runtime/client.py +32 -0
- algen_agent_runtime/runtime/runtime.py +1250 -0
- algen_agent_runtime/runtime/state_machine.py +77 -0
- algen_agent_runtime/security/documents.py +358 -0
- algen_agent_runtime/security/network.py +44 -0
- algen_agent_runtime/security/redaction.py +23 -0
- algen_agent_runtime/semantics/__init__.py +82 -0
- algen_agent_runtime/semantics/compiler.py +192 -0
- algen_agent_runtime/semantics/contracts.py +404 -0
- algen_agent_runtime/semantics/layer.py +390 -0
- algen_agent_runtime/semantics/planner.py +99 -0
- algen_agent_runtime/semantics/production_compiler.py +324 -0
- algen_agent_runtime/tools/adapters.py +95 -0
- algen_agent_runtime/tools/builtin.py +112 -0
- algen_agent_runtime/tools/contracts.py +104 -0
- algen_agent_runtime/tools/executor.py +193 -0
- algen_agent_runtime/tools/registry.py +24 -0
- algen_agent_runtime/tools/subagent.py +48 -0
- algen_agent_runtime/types/contracts.py +413 -0
- algen_agent_runtime/types/interfaces.py +145 -0
- algen_agent_runtime/verification/analytical.py +95 -0
- algen_agent_runtime/verification/verifiers.py +130 -0
- algen_agent_runtime/workflows/__init__.py +59 -0
- algen_agent_runtime/workflows/contracts.py +338 -0
- algen_agent_runtime/workflows/engine.py +1443 -0
- algen_agent_runtime/workflows/store.py +118 -0
- algen_agent_runtime-0.1.0a1.dist-info/METADATA +480 -0
- algen_agent_runtime-0.1.0a1.dist-info/RECORD +255 -0
- algen_agent_runtime-0.1.0a1.dist-info/WHEEL +4 -0
- algen_agent_runtime-0.1.0a1.dist-info/entry_points.txt +2 -0
- algen_agent_runtime-0.1.0a1.dist-info/licenses/LICENSE +201 -0
- examples/__init__.py +37 -0
- examples/case_study_responsible_hiring/README.md +86 -0
- examples/case_study_responsible_hiring/__init__.py +1 -0
- examples/case_study_responsible_hiring/app.py +20 -0
- examples/case_study_responsible_hiring/application/__init__.py +1 -0
- examples/case_study_responsible_hiring/application/contracts.py +132 -0
- examples/case_study_responsible_hiring/application/document_pipeline.py +30 -0
- examples/case_study_responsible_hiring/application/repositories.py +140 -0
- examples/case_study_responsible_hiring/application/workflow.py +473 -0
- examples/case_study_responsible_hiring/application/workflow_hooks.py +66 -0
- examples/case_study_responsible_hiring/config/agent.yaml +318 -0
- examples/case_study_responsible_hiring/dashboard.py +195 -0
- examples/case_study_responsible_hiring/data/template_corpus.md +18 -0
- examples/case_study_responsible_hiring/database/migrations/001_hiring_schema.sql +27 -0
- examples/case_study_responsible_hiring/ui/index.html +424 -0
- examples/case_study_teaching_assistant/DEMO_GUIDE.md +48 -0
- examples/case_study_teaching_assistant/Dockerfile +22 -0
- examples/case_study_teaching_assistant/README.md +111 -0
- examples/case_study_teaching_assistant/__init__.py +1 -0
- examples/case_study_teaching_assistant/app.py +63 -0
- examples/case_study_teaching_assistant/application/__init__.py +12 -0
- examples/case_study_teaching_assistant/application/handler.py +114 -0
- examples/case_study_teaching_assistant/application/policies.py +89 -0
- examples/case_study_teaching_assistant/application/tools.py +253 -0
- examples/case_study_teaching_assistant/application/workflow_hooks.py +11 -0
- examples/case_study_teaching_assistant/config/agent.yaml +204 -0
- examples/case_study_teaching_assistant/config/ecs.yaml +37 -0
- examples/case_study_teaching_assistant/dashboard.py +70 -0
- examples/case_study_teaching_assistant/deploy/README.md +29 -0
- examples/case_study_teaching_assistant/deploy/ecs-task-definition.example.json +46 -0
- examples/case_study_teaching_assistant/ui/index.html +55 -0
- examples/pattern_approval_workflow/README.md +13 -0
- examples/pattern_approval_workflow/__init__.py +1 -0
- examples/pattern_approval_workflow/agent.yaml +71 -0
- examples/pattern_approval_workflow/app.py +24 -0
- examples/pattern_approval_workflow/hooks.py +53 -0
- examples/pattern_approval_workflow/manifest.yaml +23 -0
- examples/pattern_child_workflow/README.md +9 -0
- examples/pattern_child_workflow/__init__.py +1 -0
- examples/pattern_child_workflow/agent.yaml +69 -0
- examples/pattern_child_workflow/app.py +21 -0
- examples/pattern_child_workflow/hooks.py +20 -0
- examples/pattern_child_workflow/manifest.yaml +21 -0
- examples/pattern_evaluation_gate/README.md +12 -0
- examples/pattern_evaluation_gate/__init__.py +1 -0
- examples/pattern_evaluation_gate/agent.yaml +36 -0
- examples/pattern_evaluation_gate/app.py +51 -0
- examples/pattern_evaluation_gate/hooks.py +23 -0
- examples/pattern_evaluation_gate/manifest.yaml +22 -0
- examples/pattern_governed_research/README.md +11 -0
- examples/pattern_governed_research/__init__.py +1 -0
- examples/pattern_governed_research/agent.yaml +57 -0
- examples/pattern_governed_research/app.py +24 -0
- examples/pattern_governed_research/hooks.py +53 -0
- examples/pattern_governed_research/manifest.yaml +23 -0
- examples/pattern_langgraph_governance/README.md +8 -0
- examples/pattern_langgraph_governance/__init__.py +1 -0
- examples/pattern_langgraph_governance/agent.yaml +36 -0
- examples/pattern_langgraph_governance/app.py +24 -0
- examples/pattern_langgraph_governance/hooks.py +44 -0
- examples/pattern_langgraph_governance/manifest.yaml +26 -0
- examples/pattern_multi_agent_fanout/README.md +11 -0
- examples/pattern_multi_agent_fanout/__init__.py +1 -0
- examples/pattern_multi_agent_fanout/agent.yaml +76 -0
- examples/pattern_multi_agent_fanout/app.py +21 -0
- examples/pattern_multi_agent_fanout/hooks.py +69 -0
- examples/pattern_multi_agent_fanout/manifest.yaml +22 -0
- examples/pattern_provider_fallback/README.md +65 -0
- examples/pattern_provider_fallback/__init__.py +1 -0
- examples/pattern_provider_fallback/agent.yaml +25 -0
- examples/pattern_provider_fallback/app.py +122 -0
- examples/pattern_provider_fallback/manifest.yaml +29 -0
- examples/pattern_text_to_sql/README.md +52 -0
- examples/pattern_text_to_sql/__init__.py +1 -0
- examples/pattern_text_to_sql/advanced/README.md +42 -0
- examples/pattern_text_to_sql/advanced/__init__.py +1 -0
- examples/pattern_text_to_sql/advanced/agent.yaml +84 -0
- examples/pattern_text_to_sql/advanced/app.py +75 -0
- examples/pattern_text_to_sql/advanced/knowledge.py +69 -0
- examples/pattern_text_to_sql/advanced/migrations/001_schema_embeddings.sql +24 -0
- examples/pattern_text_to_sql/agent.yaml +138 -0
- examples/pattern_text_to_sql/app.py +228 -0
- examples/pattern_text_to_sql/manifest.yaml +52 -0
- examples/pattern_text_to_sql/schema.sql +29 -0
- examples/plugins.py +38 -0
- examples/providers.yaml +101 -0
- examples/quickstart_approval/README.md +9 -0
- examples/quickstart_approval/__init__.py +1 -0
- examples/quickstart_approval/agent.yaml +58 -0
- examples/quickstart_approval/app.py +24 -0
- examples/quickstart_approval/hooks.py +43 -0
- examples/quickstart_approval/manifest.yaml +22 -0
- examples/quickstart_cloud_chat/README.md +11 -0
- examples/quickstart_cloud_chat/__init__.py +1 -0
- examples/quickstart_cloud_chat/agent.yaml +28 -0
- examples/quickstart_cloud_chat/app.py +33 -0
- examples/quickstart_cloud_chat/manifest.yaml +26 -0
- examples/quickstart_local_chat/README.md +70 -0
- examples/quickstart_local_chat/__init__.py +1 -0
- examples/quickstart_local_chat/agent.yaml +31 -0
- examples/quickstart_local_chat/app.py +80 -0
- examples/quickstart_local_chat/manifest.yaml +34 -0
- examples/quickstart_rag/README.md +24 -0
- examples/quickstart_rag/__init__.py +1 -0
- examples/quickstart_rag/agent.yaml +103 -0
- examples/quickstart_rag/app.py +63 -0
- examples/quickstart_rag/manifest.yaml +37 -0
- examples/quickstart_rag/tests/test_smoke.py +42 -0
- examples/quickstart_tool/README.md +11 -0
- examples/quickstart_tool/__init__.py +1 -0
- examples/quickstart_tool/agent.yaml +42 -0
- examples/quickstart_tool/app.py +22 -0
- examples/quickstart_tool/hooks.py +29 -0
- examples/quickstart_tool/manifest.yaml +22 -0
- examples/reference_customer_support/README.md +15 -0
- examples/reference_customer_support/__init__.py +1 -0
- examples/reference_customer_support/app.py +24 -0
- examples/reference_customer_support/application/__init__.py +1 -0
- examples/reference_customer_support/application/workflow.py +73 -0
- examples/reference_customer_support/config/agent.yaml +98 -0
- examples/reference_customer_support/manifest.yaml +31 -0
- examples/reference_incident_response/README.md +13 -0
- examples/reference_incident_response/__init__.py +1 -0
- examples/reference_incident_response/app.py +24 -0
- examples/reference_incident_response/application/__init__.py +1 -0
- examples/reference_incident_response/application/workflow.py +70 -0
- examples/reference_incident_response/config/agent.yaml +109 -0
- examples/reference_incident_response/manifest.yaml +23 -0
- examples/reference_invoice_exceptions/README.md +15 -0
- examples/reference_invoice_exceptions/__init__.py +1 -0
- examples/reference_invoice_exceptions/app.py +24 -0
- examples/reference_invoice_exceptions/application/__init__.py +1 -0
- examples/reference_invoice_exceptions/application/workflow.py +88 -0
- examples/reference_invoice_exceptions/config/agent.yaml +97 -0
- examples/reference_invoice_exceptions/manifest.yaml +26 -0
- examples/workflow_cli.py +107 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Composable, provider-neutral agent runtime."""
|
|
2
|
+
|
|
3
|
+
from algen_agent_runtime.artifacts import (
|
|
4
|
+
ArtifactLifecycleService,
|
|
5
|
+
ArtifactScanner,
|
|
6
|
+
ArtifactScanResult,
|
|
7
|
+
)
|
|
8
|
+
from algen_agent_runtime.cache import CacheContext, CachePolicy, CacheScope, CacheService
|
|
9
|
+
from algen_agent_runtime.retrieval.contracts import (
|
|
10
|
+
RetrievalQuery,
|
|
11
|
+
RetrievedDocument,
|
|
12
|
+
SourceDocument,
|
|
13
|
+
)
|
|
14
|
+
from algen_agent_runtime.retrieval.memory import HashingEmbedder, InMemoryRetriever
|
|
15
|
+
from algen_agent_runtime.runtime.client import AlgenAgentRuntimeClient
|
|
16
|
+
from algen_agent_runtime.runtime.runtime import AgentRuntime
|
|
17
|
+
from algen_agent_runtime.types.contracts import (
|
|
18
|
+
AgentDefinition,
|
|
19
|
+
Artifact,
|
|
20
|
+
ArtifactDescriptor,
|
|
21
|
+
ArtifactStatus,
|
|
22
|
+
Citation,
|
|
23
|
+
RunRequest,
|
|
24
|
+
RunResult,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
"AgentDefinition",
|
|
29
|
+
"AgentRuntime",
|
|
30
|
+
"AlgenAgentRuntimeClient",
|
|
31
|
+
"Artifact",
|
|
32
|
+
"ArtifactDescriptor",
|
|
33
|
+
"ArtifactLifecycleService",
|
|
34
|
+
"ArtifactScanResult",
|
|
35
|
+
"ArtifactScanner",
|
|
36
|
+
"ArtifactStatus",
|
|
37
|
+
"CacheContext",
|
|
38
|
+
"CachePolicy",
|
|
39
|
+
"CacheScope",
|
|
40
|
+
"CacheService",
|
|
41
|
+
"Citation",
|
|
42
|
+
"HashingEmbedder",
|
|
43
|
+
"InMemoryRetriever",
|
|
44
|
+
"RetrievalQuery",
|
|
45
|
+
"RetrievedDocument",
|
|
46
|
+
"RunRequest",
|
|
47
|
+
"RunResult",
|
|
48
|
+
"SourceDocument",
|
|
49
|
+
]
|
|
50
|
+
__version__ = "0.1.0a1"
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from algen_agent_runtime.analytics.contracts import * # noqa: F403
|
|
2
|
+
from algen_agent_runtime.analytics.engine import AnalyticalGraphEngine, AnalyticalNodeRegistry
|
|
3
|
+
from algen_agent_runtime.analytics.handlers import builtin_handlers
|
|
4
|
+
from algen_agent_runtime.analytics.stores import (
|
|
5
|
+
InMemoryAnalyticalGraphStore,
|
|
6
|
+
PostgresAnalyticalGraphStore,
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"AnalyticalGraphEngine",
|
|
11
|
+
"AnalyticalNodeRegistry",
|
|
12
|
+
"InMemoryAnalyticalGraphStore",
|
|
13
|
+
"PostgresAnalyticalGraphStore",
|
|
14
|
+
"builtin_handlers",
|
|
15
|
+
]
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from enum import StrEnum
|
|
5
|
+
from typing import Any, Protocol
|
|
6
|
+
from uuid import uuid4
|
|
7
|
+
|
|
8
|
+
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
|
9
|
+
|
|
10
|
+
from algen_agent_runtime.types.contracts import utc_now
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class AnalyticalNodeKind(StrEnum):
|
|
14
|
+
SEMANTIC_QUERY = "semantic_query"
|
|
15
|
+
JOIN_RESULTS = "join_results"
|
|
16
|
+
COMPARE_PERIODS = "compare_periods"
|
|
17
|
+
CALCULATE = "calculate"
|
|
18
|
+
MODEL_CALL = "model_call"
|
|
19
|
+
RANK = "rank"
|
|
20
|
+
VERIFY = "verify"
|
|
21
|
+
COMPOSE = "compose"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class NodeStatus(StrEnum):
|
|
25
|
+
PENDING = "pending"
|
|
26
|
+
RUNNING = "running"
|
|
27
|
+
COMPLETED = "completed"
|
|
28
|
+
FAILED = "failed"
|
|
29
|
+
SKIPPED = "skipped"
|
|
30
|
+
CANCELLED = "cancelled"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class GraphStatus(StrEnum):
|
|
34
|
+
PENDING = "pending"
|
|
35
|
+
RUNNING = "running"
|
|
36
|
+
COMPLETED = "completed"
|
|
37
|
+
FAILED = "failed"
|
|
38
|
+
CANCELLED = "cancelled"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class NodeRetryPolicy(BaseModel):
|
|
42
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
43
|
+
maximum_attempts: int = Field(default=1, ge=1, le=10)
|
|
44
|
+
initial_backoff_seconds: float = Field(default=0.1, ge=0, le=60)
|
|
45
|
+
maximum_backoff_seconds: float = Field(default=5, ge=0, le=300)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class NodeCachePolicy(BaseModel):
|
|
49
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
50
|
+
enabled: bool = False
|
|
51
|
+
policy_name: str = "analytical_nodes"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class NodeBudget(BaseModel):
|
|
55
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
56
|
+
timeout_seconds: float = Field(default=30, gt=0, le=3600)
|
|
57
|
+
maximum_rows: int = Field(default=10_000, ge=1, le=1_000_000)
|
|
58
|
+
maximum_bytes: int = Field(default=10_000_000, ge=1)
|
|
59
|
+
maximum_cost_usd: float | None = Field(default=None, ge=0)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class ResultReference(BaseModel):
|
|
63
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
64
|
+
node_id: str
|
|
65
|
+
path: str | None = None
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class AnalyticalNode(BaseModel):
|
|
69
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
70
|
+
id: str = Field(pattern=r"^[a-zA-Z][a-zA-Z0-9_.-]*$")
|
|
71
|
+
kind: AnalyticalNodeKind
|
|
72
|
+
dependencies: tuple[str, ...] = ()
|
|
73
|
+
inputs: dict[str, ResultReference] = Field(default_factory=dict)
|
|
74
|
+
configuration: dict[str, Any] = Field(default_factory=dict)
|
|
75
|
+
input_schema: dict[str, Any] = Field(default_factory=lambda: {"type": "object"})
|
|
76
|
+
output_schema: dict[str, Any] = Field(default_factory=dict)
|
|
77
|
+
retry: NodeRetryPolicy = Field(default_factory=NodeRetryPolicy)
|
|
78
|
+
cache: NodeCachePolicy = Field(default_factory=NodeCachePolicy)
|
|
79
|
+
budget: NodeBudget = Field(default_factory=NodeBudget)
|
|
80
|
+
concurrency_key: str | None = None
|
|
81
|
+
|
|
82
|
+
@model_validator(mode="after")
|
|
83
|
+
def validate_references(self) -> AnalyticalNode:
|
|
84
|
+
referenced = {value.node_id for value in self.inputs.values()}
|
|
85
|
+
if not referenced.issubset(set(self.dependencies)):
|
|
86
|
+
raise ValueError("every input result reference must be declared as a dependency")
|
|
87
|
+
if self.id in self.dependencies:
|
|
88
|
+
raise ValueError("a node cannot depend on itself")
|
|
89
|
+
return self
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class AnalyticalGraph(BaseModel):
|
|
93
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
94
|
+
id: str = Field(default_factory=lambda: str(uuid4()))
|
|
95
|
+
name: str
|
|
96
|
+
version: str
|
|
97
|
+
nodes: tuple[AnalyticalNode, ...] = Field(min_length=1)
|
|
98
|
+
maximum_concurrency: int = Field(default=4, ge=1, le=128)
|
|
99
|
+
maximum_nodes: int = Field(default=100, ge=1, le=1000)
|
|
100
|
+
timeout_seconds: float = Field(default=300, gt=0, le=86_400)
|
|
101
|
+
metadata: dict[str, Any] = Field(default_factory=dict)
|
|
102
|
+
|
|
103
|
+
@model_validator(mode="after")
|
|
104
|
+
def validate_dag(self) -> AnalyticalGraph:
|
|
105
|
+
if len(self.nodes) > self.maximum_nodes:
|
|
106
|
+
raise ValueError("graph exceeds maximum_nodes")
|
|
107
|
+
ids = [node.id for node in self.nodes]
|
|
108
|
+
if len(ids) != len(set(ids)):
|
|
109
|
+
raise ValueError("graph node identifiers must be unique")
|
|
110
|
+
known = set(ids)
|
|
111
|
+
for node in self.nodes:
|
|
112
|
+
missing = set(node.dependencies) - known
|
|
113
|
+
if missing:
|
|
114
|
+
raise ValueError(f"node {node.id!r} has unknown dependencies {sorted(missing)}")
|
|
115
|
+
visiting: set[str] = set()
|
|
116
|
+
visited: set[str] = set()
|
|
117
|
+
dependencies = {node.id: node.dependencies for node in self.nodes}
|
|
118
|
+
|
|
119
|
+
def visit(node_id: str) -> None:
|
|
120
|
+
if node_id in visiting:
|
|
121
|
+
raise ValueError("analytical graph contains a dependency cycle")
|
|
122
|
+
if node_id in visited:
|
|
123
|
+
return
|
|
124
|
+
visiting.add(node_id)
|
|
125
|
+
for dependency in dependencies[node_id]:
|
|
126
|
+
visit(dependency)
|
|
127
|
+
visiting.remove(node_id)
|
|
128
|
+
visited.add(node_id)
|
|
129
|
+
|
|
130
|
+
for node_id in ids:
|
|
131
|
+
visit(node_id)
|
|
132
|
+
return self
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class ResultProvenance(BaseModel):
|
|
136
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
137
|
+
source_ids: tuple[str, ...] = ()
|
|
138
|
+
source_columns: tuple[str, ...] = ()
|
|
139
|
+
upstream_result_ids: tuple[str, ...] = ()
|
|
140
|
+
semantic_layer_digest: str | None = None
|
|
141
|
+
metric_versions: dict[str, str] = Field(default_factory=dict)
|
|
142
|
+
method_version: str | None = None
|
|
143
|
+
model_version: str | None = None
|
|
144
|
+
query_fingerprint: str | None = None
|
|
145
|
+
freshness_observed_at: datetime | None = None
|
|
146
|
+
lineage: dict[str, Any] = Field(default_factory=dict)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
class AnalyticalResult(BaseModel):
|
|
150
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
151
|
+
id: str = Field(default_factory=lambda: str(uuid4()))
|
|
152
|
+
node_id: str
|
|
153
|
+
value: Any
|
|
154
|
+
schema_: dict[str, Any] = Field(default_factory=dict, alias="schema")
|
|
155
|
+
row_count: int | None = Field(default=None, ge=0)
|
|
156
|
+
byte_count: int = Field(default=0, ge=0)
|
|
157
|
+
cost_usd: float = Field(default=0, ge=0)
|
|
158
|
+
provenance: ResultProvenance = Field(default_factory=ResultProvenance)
|
|
159
|
+
warnings: tuple[str, ...] = ()
|
|
160
|
+
created_at: datetime = Field(default_factory=utc_now)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
class NodeExecution(BaseModel):
|
|
164
|
+
model_config = ConfigDict(extra="forbid")
|
|
165
|
+
node_id: str
|
|
166
|
+
status: NodeStatus = NodeStatus.PENDING
|
|
167
|
+
attempts: int = 0
|
|
168
|
+
result_id: str | None = None
|
|
169
|
+
error: str | None = None
|
|
170
|
+
started_at: datetime | None = None
|
|
171
|
+
completed_at: datetime | None = None
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
class AnalyticalGraphState(BaseModel):
|
|
175
|
+
model_config = ConfigDict(extra="forbid")
|
|
176
|
+
id: str
|
|
177
|
+
tenant_id: str
|
|
178
|
+
user_id: str
|
|
179
|
+
graph: AnalyticalGraph
|
|
180
|
+
status: GraphStatus = GraphStatus.PENDING
|
|
181
|
+
nodes: dict[str, NodeExecution] = Field(default_factory=dict)
|
|
182
|
+
results: dict[str, AnalyticalResult] = Field(default_factory=dict)
|
|
183
|
+
version: int = 0
|
|
184
|
+
created_at: datetime = Field(default_factory=utc_now)
|
|
185
|
+
updated_at: datetime = Field(default_factory=utc_now)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
class GraphExecutionContext(BaseModel):
|
|
189
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
190
|
+
tenant_id: str
|
|
191
|
+
user_id: str
|
|
192
|
+
session_id: str | None = None
|
|
193
|
+
run_id: str | None = None
|
|
194
|
+
authorization_fingerprint: str | None = None
|
|
195
|
+
attributes: dict[str, Any] = Field(default_factory=dict)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
class AnalyticalNodeHandler(Protocol):
|
|
199
|
+
async def __call__(
|
|
200
|
+
self,
|
|
201
|
+
node: AnalyticalNode,
|
|
202
|
+
inputs: dict[str, Any],
|
|
203
|
+
context: GraphExecutionContext,
|
|
204
|
+
) -> AnalyticalResult: ...
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
class AnalyticalGraphStore(Protocol):
|
|
208
|
+
async def create(self, state: AnalyticalGraphState) -> None: ...
|
|
209
|
+
async def get(self, graph_id: str, tenant_id: str) -> AnalyticalGraphState | None: ...
|
|
210
|
+
async def save(self, state: AnalyticalGraphState, expected_version: int) -> None: ...
|
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import json
|
|
5
|
+
from collections import defaultdict
|
|
6
|
+
from collections.abc import Mapping
|
|
7
|
+
from time import monotonic
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
import jsonschema
|
|
11
|
+
from opentelemetry import metrics, trace
|
|
12
|
+
|
|
13
|
+
from algen_agent_runtime.analytics.contracts import (
|
|
14
|
+
AnalyticalGraph,
|
|
15
|
+
AnalyticalGraphState,
|
|
16
|
+
AnalyticalGraphStore,
|
|
17
|
+
AnalyticalNode,
|
|
18
|
+
AnalyticalNodeHandler,
|
|
19
|
+
AnalyticalNodeKind,
|
|
20
|
+
AnalyticalResult,
|
|
21
|
+
GraphExecutionContext,
|
|
22
|
+
GraphStatus,
|
|
23
|
+
NodeExecution,
|
|
24
|
+
NodeStatus,
|
|
25
|
+
ResultProvenance,
|
|
26
|
+
)
|
|
27
|
+
from algen_agent_runtime.cache import CacheContext, CacheService
|
|
28
|
+
from algen_agent_runtime.exceptions.errors import BudgetExceededError, ConfigurationError
|
|
29
|
+
from algen_agent_runtime.types.contracts import utc_now
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class AnalyticalNodeRegistry:
|
|
33
|
+
def __init__(self) -> None:
|
|
34
|
+
self._handlers: dict[AnalyticalNodeKind, AnalyticalNodeHandler] = {}
|
|
35
|
+
|
|
36
|
+
def register(self, kind: AnalyticalNodeKind, handler: AnalyticalNodeHandler) -> None:
|
|
37
|
+
if kind in self._handlers:
|
|
38
|
+
raise ConfigurationError(f"handler for analytical node {kind.value!r} is registered")
|
|
39
|
+
self._handlers[kind] = handler
|
|
40
|
+
|
|
41
|
+
def get(self, kind: AnalyticalNodeKind) -> AnalyticalNodeHandler:
|
|
42
|
+
try:
|
|
43
|
+
return self._handlers[kind]
|
|
44
|
+
except KeyError as exc:
|
|
45
|
+
raise ConfigurationError(f"no handler for analytical node {kind.value!r}") from exc
|
|
46
|
+
|
|
47
|
+
def kinds(self) -> tuple[AnalyticalNodeKind, ...]:
|
|
48
|
+
return tuple(sorted(self._handlers, key=str))
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class AnalyticalGraphEngine:
|
|
52
|
+
"""Checkpointed DAG executor that keeps each node result independently addressable."""
|
|
53
|
+
|
|
54
|
+
def __init__(
|
|
55
|
+
self,
|
|
56
|
+
registry: AnalyticalNodeRegistry,
|
|
57
|
+
store: AnalyticalGraphStore,
|
|
58
|
+
*,
|
|
59
|
+
cache: CacheService | None = None,
|
|
60
|
+
maximum_concurrency: int = 8,
|
|
61
|
+
maximum_node_timeout_seconds: float = 30,
|
|
62
|
+
maximum_graph_timeout_seconds: float = 300,
|
|
63
|
+
) -> None:
|
|
64
|
+
self._registry = registry
|
|
65
|
+
self._store = store
|
|
66
|
+
self._cache = cache
|
|
67
|
+
self._maximum_concurrency = maximum_concurrency
|
|
68
|
+
self._maximum_node_timeout_seconds = maximum_node_timeout_seconds
|
|
69
|
+
self._maximum_graph_timeout_seconds = maximum_graph_timeout_seconds
|
|
70
|
+
self._tracer = trace.get_tracer("algen_agent_runtime.analytics")
|
|
71
|
+
meter = metrics.get_meter("algen_agent_runtime.analytics")
|
|
72
|
+
self._node_duration = meter.create_histogram(
|
|
73
|
+
"algen_agent_runtime.analytics.node.duration", unit="ms"
|
|
74
|
+
)
|
|
75
|
+
self._node_runs = meter.create_counter("algen_agent_runtime.analytics.node.executions")
|
|
76
|
+
self._locks: defaultdict[str, asyncio.Lock] = defaultdict(asyncio.Lock)
|
|
77
|
+
self._checkpoint_locks: defaultdict[str, asyncio.Lock] = defaultdict(asyncio.Lock)
|
|
78
|
+
self._concurrency_locks: defaultdict[str, asyncio.Lock] = defaultdict(asyncio.Lock)
|
|
79
|
+
|
|
80
|
+
def register_handler(
|
|
81
|
+
self,
|
|
82
|
+
kind: AnalyticalNodeKind,
|
|
83
|
+
handler: AnalyticalNodeHandler,
|
|
84
|
+
) -> None:
|
|
85
|
+
"""Register an application-owned node handler on the composed graph engine."""
|
|
86
|
+
self._registry.register(kind, handler)
|
|
87
|
+
|
|
88
|
+
def handler_kinds(self) -> tuple[AnalyticalNodeKind, ...]:
|
|
89
|
+
"""Return node kinds available in this composed engine."""
|
|
90
|
+
return self._registry.kinds()
|
|
91
|
+
|
|
92
|
+
async def run(
|
|
93
|
+
self, graph: AnalyticalGraph, context: GraphExecutionContext
|
|
94
|
+
) -> AnalyticalGraphState:
|
|
95
|
+
state = AnalyticalGraphState(
|
|
96
|
+
id=graph.id,
|
|
97
|
+
tenant_id=context.tenant_id,
|
|
98
|
+
user_id=context.user_id,
|
|
99
|
+
graph=graph,
|
|
100
|
+
nodes={node.id: NodeExecution(node_id=node.id) for node in graph.nodes},
|
|
101
|
+
)
|
|
102
|
+
await self._store.create(state)
|
|
103
|
+
return await self._drive(state, context)
|
|
104
|
+
|
|
105
|
+
async def resume(self, graph_id: str, context: GraphExecutionContext) -> AnalyticalGraphState:
|
|
106
|
+
state = await self._store.get(graph_id, context.tenant_id)
|
|
107
|
+
if state is None:
|
|
108
|
+
raise ConfigurationError(f"analytical graph {graph_id!r} was not found")
|
|
109
|
+
if state.user_id != context.user_id:
|
|
110
|
+
raise ConfigurationError("analytical graph belongs to another user")
|
|
111
|
+
for execution in state.nodes.values():
|
|
112
|
+
if execution.status == NodeStatus.RUNNING:
|
|
113
|
+
execution.status = NodeStatus.PENDING
|
|
114
|
+
execution.error = "recovered after an interrupted worker lease"
|
|
115
|
+
if state.status == GraphStatus.RUNNING:
|
|
116
|
+
state.status = GraphStatus.PENDING
|
|
117
|
+
return await self._drive(state, context)
|
|
118
|
+
|
|
119
|
+
async def _drive(
|
|
120
|
+
self, state: AnalyticalGraphState, context: GraphExecutionContext
|
|
121
|
+
) -> AnalyticalGraphState:
|
|
122
|
+
async with self._locks[state.id]:
|
|
123
|
+
with self._tracer.start_as_current_span(
|
|
124
|
+
"analytical_graph.run",
|
|
125
|
+
attributes={
|
|
126
|
+
"analytics.graph.id": state.id,
|
|
127
|
+
"analytics.graph.name": state.graph.name,
|
|
128
|
+
"analytics.graph.version": state.graph.version,
|
|
129
|
+
"tenant.id": context.tenant_id,
|
|
130
|
+
},
|
|
131
|
+
):
|
|
132
|
+
state.status = GraphStatus.RUNNING
|
|
133
|
+
await self._checkpoint(state)
|
|
134
|
+
try:
|
|
135
|
+
async with asyncio.timeout(
|
|
136
|
+
min(state.graph.timeout_seconds, self._maximum_graph_timeout_seconds)
|
|
137
|
+
):
|
|
138
|
+
await self._schedule(state, context)
|
|
139
|
+
except TimeoutError:
|
|
140
|
+
state.status = GraphStatus.FAILED
|
|
141
|
+
for execution in state.nodes.values():
|
|
142
|
+
if execution.status in {NodeStatus.PENDING, NodeStatus.RUNNING}:
|
|
143
|
+
execution.status = NodeStatus.CANCELLED
|
|
144
|
+
execution.error = "graph timeout exhausted"
|
|
145
|
+
await self._checkpoint(state)
|
|
146
|
+
raise BudgetExceededError("analytical graph timeout exhausted") from None
|
|
147
|
+
except asyncio.CancelledError:
|
|
148
|
+
state.status = GraphStatus.CANCELLED
|
|
149
|
+
await self._checkpoint(state)
|
|
150
|
+
raise
|
|
151
|
+
except Exception:
|
|
152
|
+
state.status = GraphStatus.FAILED
|
|
153
|
+
await self._checkpoint(state)
|
|
154
|
+
raise
|
|
155
|
+
state.status = GraphStatus.COMPLETED
|
|
156
|
+
await self._checkpoint(state)
|
|
157
|
+
return state
|
|
158
|
+
|
|
159
|
+
async def _schedule(self, state: AnalyticalGraphState, context: GraphExecutionContext) -> None:
|
|
160
|
+
nodes = {node.id: node for node in state.graph.nodes}
|
|
161
|
+
semaphore = asyncio.Semaphore(
|
|
162
|
+
min(state.graph.maximum_concurrency, self._maximum_concurrency)
|
|
163
|
+
)
|
|
164
|
+
while True:
|
|
165
|
+
incomplete = {
|
|
166
|
+
node_id
|
|
167
|
+
for node_id, execution in state.nodes.items()
|
|
168
|
+
if execution.status != NodeStatus.COMPLETED
|
|
169
|
+
}
|
|
170
|
+
if not incomplete:
|
|
171
|
+
return
|
|
172
|
+
failed = [
|
|
173
|
+
node_id
|
|
174
|
+
for node_id in incomplete
|
|
175
|
+
if state.nodes[node_id].status in {NodeStatus.FAILED, NodeStatus.CANCELLED}
|
|
176
|
+
]
|
|
177
|
+
if failed:
|
|
178
|
+
raise RuntimeError(f"analytical graph failed at nodes {sorted(failed)}")
|
|
179
|
+
ready = [
|
|
180
|
+
nodes[node_id]
|
|
181
|
+
for node_id in incomplete
|
|
182
|
+
if state.nodes[node_id].status == NodeStatus.PENDING
|
|
183
|
+
and all(
|
|
184
|
+
state.nodes[item].status == NodeStatus.COMPLETED
|
|
185
|
+
for item in nodes[node_id].dependencies
|
|
186
|
+
)
|
|
187
|
+
]
|
|
188
|
+
if not ready:
|
|
189
|
+
raise RuntimeError("analytical graph has no runnable nodes")
|
|
190
|
+
|
|
191
|
+
async def execute(node: AnalyticalNode) -> None:
|
|
192
|
+
async with semaphore:
|
|
193
|
+
if node.concurrency_key:
|
|
194
|
+
async with self._concurrency_locks[node.concurrency_key]:
|
|
195
|
+
await self._execute_node(state, node, context)
|
|
196
|
+
else:
|
|
197
|
+
await self._execute_node(state, node, context)
|
|
198
|
+
|
|
199
|
+
outcomes = await asyncio.gather(
|
|
200
|
+
*(execute(node) for node in ready), return_exceptions=True
|
|
201
|
+
)
|
|
202
|
+
errors = [item for item in outcomes if isinstance(item, BaseException)]
|
|
203
|
+
if errors:
|
|
204
|
+
raise errors[0]
|
|
205
|
+
|
|
206
|
+
async def _execute_node(
|
|
207
|
+
self,
|
|
208
|
+
state: AnalyticalGraphState,
|
|
209
|
+
node: AnalyticalNode,
|
|
210
|
+
context: GraphExecutionContext,
|
|
211
|
+
) -> None:
|
|
212
|
+
execution = state.nodes[node.id]
|
|
213
|
+
execution.status = NodeStatus.RUNNING
|
|
214
|
+
execution.started_at = utc_now()
|
|
215
|
+
await self._checkpoint(state)
|
|
216
|
+
inputs = self._resolve_inputs(state, node)
|
|
217
|
+
jsonschema.validate(inputs, node.input_schema)
|
|
218
|
+
handler = self._registry.get(node.kind)
|
|
219
|
+
started = monotonic()
|
|
220
|
+
attributes = {
|
|
221
|
+
"analytics.graph.id": state.id,
|
|
222
|
+
"analytics.node.id": node.id,
|
|
223
|
+
"analytics.node.kind": node.kind.value,
|
|
224
|
+
}
|
|
225
|
+
with self._tracer.start_as_current_span(
|
|
226
|
+
f"analytical_node.{node.kind.value}", attributes=attributes
|
|
227
|
+
) as span:
|
|
228
|
+
try:
|
|
229
|
+
cached = await self._cached_result(state, node, context, inputs)
|
|
230
|
+
if cached is not None:
|
|
231
|
+
result = cached
|
|
232
|
+
span.set_attribute("cache.hit", True)
|
|
233
|
+
else:
|
|
234
|
+
result = await self._invoke(handler, node, inputs, context, execution)
|
|
235
|
+
await self._validate_result(node, result)
|
|
236
|
+
await self._store_result_cache(state, node, context, inputs, result)
|
|
237
|
+
span.set_attribute("cache.hit", False)
|
|
238
|
+
if result.node_id != node.id:
|
|
239
|
+
result = result.model_copy(update={"node_id": node.id})
|
|
240
|
+
self._enforce_result_budget(node, result)
|
|
241
|
+
state.results[node.id] = result
|
|
242
|
+
execution.result_id = result.id
|
|
243
|
+
execution.status = NodeStatus.COMPLETED
|
|
244
|
+
execution.completed_at = utc_now()
|
|
245
|
+
self._node_runs.add(1, {**attributes, "outcome": "completed"})
|
|
246
|
+
except BaseException as exc:
|
|
247
|
+
execution.status = NodeStatus.FAILED
|
|
248
|
+
execution.error = f"{type(exc).__name__}: {exc}"
|
|
249
|
+
execution.completed_at = utc_now()
|
|
250
|
+
span.record_exception(exc)
|
|
251
|
+
self._node_runs.add(1, {**attributes, "outcome": "failed"})
|
|
252
|
+
raise
|
|
253
|
+
finally:
|
|
254
|
+
self._node_duration.record((monotonic() - started) * 1000, attributes)
|
|
255
|
+
await self._checkpoint(state)
|
|
256
|
+
|
|
257
|
+
async def _invoke(
|
|
258
|
+
self,
|
|
259
|
+
handler: AnalyticalNodeHandler,
|
|
260
|
+
node: AnalyticalNode,
|
|
261
|
+
inputs: dict[str, Any],
|
|
262
|
+
context: GraphExecutionContext,
|
|
263
|
+
execution: NodeExecution,
|
|
264
|
+
) -> AnalyticalResult:
|
|
265
|
+
last_error: BaseException | None = None
|
|
266
|
+
for attempt in range(1, node.retry.maximum_attempts + 1):
|
|
267
|
+
execution.attempts = attempt
|
|
268
|
+
try:
|
|
269
|
+
async with asyncio.timeout(
|
|
270
|
+
min(node.budget.timeout_seconds, self._maximum_node_timeout_seconds)
|
|
271
|
+
):
|
|
272
|
+
return await handler(node, inputs, context)
|
|
273
|
+
except asyncio.CancelledError:
|
|
274
|
+
raise
|
|
275
|
+
except BaseException as exc:
|
|
276
|
+
last_error = exc
|
|
277
|
+
if attempt < node.retry.maximum_attempts:
|
|
278
|
+
delay = min(
|
|
279
|
+
node.retry.maximum_backoff_seconds,
|
|
280
|
+
node.retry.initial_backoff_seconds * (2 ** (attempt - 1)),
|
|
281
|
+
)
|
|
282
|
+
await asyncio.sleep(delay)
|
|
283
|
+
assert last_error is not None
|
|
284
|
+
raise last_error
|
|
285
|
+
|
|
286
|
+
@staticmethod
|
|
287
|
+
def _resolve_inputs(state: AnalyticalGraphState, node: AnalyticalNode) -> dict[str, Any]:
|
|
288
|
+
resolved: dict[str, Any] = {}
|
|
289
|
+
for name, reference in node.inputs.items():
|
|
290
|
+
result = state.results[reference.node_id]
|
|
291
|
+
value = result.value
|
|
292
|
+
if reference.path:
|
|
293
|
+
for part in reference.path.split("."):
|
|
294
|
+
if isinstance(value, Mapping):
|
|
295
|
+
value = value[part]
|
|
296
|
+
elif isinstance(value, list):
|
|
297
|
+
value = value[int(part)]
|
|
298
|
+
else:
|
|
299
|
+
raise ConfigurationError(
|
|
300
|
+
f"result path {reference.path!r} cannot traverse {type(value).__name__}"
|
|
301
|
+
)
|
|
302
|
+
resolved[name] = value
|
|
303
|
+
return resolved
|
|
304
|
+
|
|
305
|
+
@staticmethod
|
|
306
|
+
async def _validate_result(node: AnalyticalNode, result: AnalyticalResult) -> None:
|
|
307
|
+
if node.output_schema:
|
|
308
|
+
jsonschema.validate(result.value, node.output_schema)
|
|
309
|
+
|
|
310
|
+
@staticmethod
|
|
311
|
+
def _enforce_result_budget(node: AnalyticalNode, result: AnalyticalResult) -> None:
|
|
312
|
+
encoded = json.dumps(result.value, default=str).encode()
|
|
313
|
+
if len(encoded) > node.budget.maximum_bytes:
|
|
314
|
+
raise BudgetExceededError(f"node {node.id!r} exceeded result byte budget")
|
|
315
|
+
if result.row_count is not None and result.row_count > node.budget.maximum_rows:
|
|
316
|
+
raise BudgetExceededError(f"node {node.id!r} exceeded result row budget")
|
|
317
|
+
if (
|
|
318
|
+
node.budget.maximum_cost_usd is not None
|
|
319
|
+
and result.cost_usd > node.budget.maximum_cost_usd
|
|
320
|
+
):
|
|
321
|
+
raise BudgetExceededError(f"node {node.id!r} exceeded result cost budget")
|
|
322
|
+
|
|
323
|
+
async def _cached_result(
|
|
324
|
+
self,
|
|
325
|
+
state: AnalyticalGraphState,
|
|
326
|
+
node: AnalyticalNode,
|
|
327
|
+
context: GraphExecutionContext,
|
|
328
|
+
inputs: dict[str, Any],
|
|
329
|
+
) -> AnalyticalResult | None:
|
|
330
|
+
if self._cache is None or not node.cache.enabled:
|
|
331
|
+
return None
|
|
332
|
+
value = await self._cache.get_json(
|
|
333
|
+
node.cache.policy_name,
|
|
334
|
+
f"analytical-node.{state.graph.name}.{node.kind.value}",
|
|
335
|
+
self._cache_material(state, node, inputs),
|
|
336
|
+
self._cache_context(context),
|
|
337
|
+
)
|
|
338
|
+
return AnalyticalResult.model_validate(value) if value is not None else None
|
|
339
|
+
|
|
340
|
+
async def _store_result_cache(
|
|
341
|
+
self,
|
|
342
|
+
state: AnalyticalGraphState,
|
|
343
|
+
node: AnalyticalNode,
|
|
344
|
+
context: GraphExecutionContext,
|
|
345
|
+
inputs: dict[str, Any],
|
|
346
|
+
result: AnalyticalResult,
|
|
347
|
+
) -> None:
|
|
348
|
+
if self._cache is None or not node.cache.enabled:
|
|
349
|
+
return
|
|
350
|
+
await self._cache.set_json(
|
|
351
|
+
node.cache.policy_name,
|
|
352
|
+
f"analytical-node.{state.graph.name}.{node.kind.value}",
|
|
353
|
+
self._cache_material(state, node, inputs),
|
|
354
|
+
result.model_dump(mode="json", by_alias=True),
|
|
355
|
+
self._cache_context(context),
|
|
356
|
+
tags=(f"graph:{state.graph.name}",),
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
@staticmethod
|
|
360
|
+
def _cache_material(
|
|
361
|
+
state: AnalyticalGraphState, node: AnalyticalNode, inputs: dict[str, Any]
|
|
362
|
+
) -> dict[str, Any]:
|
|
363
|
+
return {
|
|
364
|
+
"graph_version": state.graph.version,
|
|
365
|
+
"node": node.model_dump(mode="json"),
|
|
366
|
+
"inputs": inputs,
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
@staticmethod
|
|
370
|
+
def _cache_context(context: GraphExecutionContext) -> CacheContext:
|
|
371
|
+
return CacheContext(
|
|
372
|
+
tenant_id=context.tenant_id,
|
|
373
|
+
user_id=context.user_id,
|
|
374
|
+
session_id=context.session_id,
|
|
375
|
+
run_id=context.run_id,
|
|
376
|
+
authorization_fingerprint=context.authorization_fingerprint,
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
async def _checkpoint(self, state: AnalyticalGraphState) -> None:
|
|
380
|
+
async with self._checkpoint_locks[state.id]:
|
|
381
|
+
state.updated_at = utc_now()
|
|
382
|
+
await self._store.save(state, state.version)
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
async def passthrough_handler(
|
|
386
|
+
node: AnalyticalNode, inputs: dict[str, Any], context: GraphExecutionContext
|
|
387
|
+
) -> AnalyticalResult:
|
|
388
|
+
del context
|
|
389
|
+
value = inputs if inputs else node.configuration.get("value")
|
|
390
|
+
return AnalyticalResult(
|
|
391
|
+
node_id=node.id,
|
|
392
|
+
value=value,
|
|
393
|
+
row_count=len(value) if isinstance(value, list) else None,
|
|
394
|
+
provenance=ResultProvenance(
|
|
395
|
+
upstream_result_ids=tuple(reference.node_id for reference in node.inputs.values())
|
|
396
|
+
),
|
|
397
|
+
)
|