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.
Files changed (255) hide show
  1. algen_agent_runtime/__init__.py +50 -0
  2. algen_agent_runtime/analytics/__init__.py +15 -0
  3. algen_agent_runtime/analytics/contracts.py +210 -0
  4. algen_agent_runtime/analytics/engine.py +397 -0
  5. algen_agent_runtime/analytics/handlers.py +167 -0
  6. algen_agent_runtime/analytics/stores.py +74 -0
  7. algen_agent_runtime/api/app.py +631 -0
  8. algen_agent_runtime/api/dependencies.py +86 -0
  9. algen_agent_runtime/approvals/service.py +194 -0
  10. algen_agent_runtime/artifacts/__init__.py +7 -0
  11. algen_agent_runtime/artifacts/service.py +83 -0
  12. algen_agent_runtime/cache/__init__.py +15 -0
  13. algen_agent_runtime/cache/contracts.py +49 -0
  14. algen_agent_runtime/cache/service.py +233 -0
  15. algen_agent_runtime/cache/stores.py +145 -0
  16. algen_agent_runtime/communications/__init__.py +25 -0
  17. algen_agent_runtime/communications/email.py +319 -0
  18. algen_agent_runtime/config/registry.py +35 -0
  19. algen_agent_runtime/config/settings.py +497 -0
  20. algen_agent_runtime/context/builder.py +203 -0
  21. algen_agent_runtime/conversations/__init__.py +55 -0
  22. algen_agent_runtime/conversations/contracts.py +206 -0
  23. algen_agent_runtime/conversations/feedback.py +237 -0
  24. algen_agent_runtime/conversations/followups.py +105 -0
  25. algen_agent_runtime/conversations/presentation.py +48 -0
  26. algen_agent_runtime/conversations/service.py +637 -0
  27. algen_agent_runtime/conversations/stores.py +336 -0
  28. algen_agent_runtime/conversations/visualization.py +55 -0
  29. algen_agent_runtime/distributed/__init__.py +12 -0
  30. algen_agent_runtime/distributed/contracts.py +44 -0
  31. algen_agent_runtime/distributed/queue.py +182 -0
  32. algen_agent_runtime/distributed/worker.py +50 -0
  33. algen_agent_runtime/evaluation/__init__.py +17 -0
  34. algen_agent_runtime/evaluation/contracts.py +51 -0
  35. algen_agent_runtime/evaluation/runner.py +102 -0
  36. algen_agent_runtime/events/bus.py +172 -0
  37. algen_agent_runtime/events/contracts.py +63 -0
  38. algen_agent_runtime/exceptions/__init__.py +1 -0
  39. algen_agent_runtime/exceptions/errors.py +51 -0
  40. algen_agent_runtime/frameworks/__init__.py +31 -0
  41. algen_agent_runtime/frameworks/adapters.py +281 -0
  42. algen_agent_runtime/frameworks/contracts.py +101 -0
  43. algen_agent_runtime/governance/__init__.py +25 -0
  44. algen_agent_runtime/governance/contracts.py +77 -0
  45. algen_agent_runtime/governance/engine.py +126 -0
  46. algen_agent_runtime/governance/executor.py +124 -0
  47. algen_agent_runtime/methods/__init__.py +16 -0
  48. algen_agent_runtime/methods/contracts.py +75 -0
  49. algen_agent_runtime/methods/registry.py +139 -0
  50. algen_agent_runtime/model_services/__init__.py +25 -0
  51. algen_agent_runtime/model_services/contracts.py +76 -0
  52. algen_agent_runtime/model_services/http.py +68 -0
  53. algen_agent_runtime/model_services/registry.py +126 -0
  54. algen_agent_runtime/models/base.py +242 -0
  55. algen_agent_runtime/models/providers/adapters.py +409 -0
  56. algen_agent_runtime/models/providers/local_transformers.py +58 -0
  57. algen_agent_runtime/models/providers/mock.py +88 -0
  58. algen_agent_runtime/models/providers/openai_compatible.py +447 -0
  59. algen_agent_runtime/observability/setup.py +55 -0
  60. algen_agent_runtime/observability/traccia_adapter.py +384 -0
  61. algen_agent_runtime/observability/trace_levels.py +54 -0
  62. algen_agent_runtime/orchestration/container.py +698 -0
  63. algen_agent_runtime/persistence/memory.py +164 -0
  64. algen_agent_runtime/persistence/migrations/001_initial.sql +75 -0
  65. algen_agent_runtime/persistence/migrations/002_conversations.sql +32 -0
  66. algen_agent_runtime/persistence/migrations/003_analytical_execution.sql +32 -0
  67. algen_agent_runtime/persistence/migrations/004_conversation_feedback.sql +13 -0
  68. algen_agent_runtime/persistence/migrations/005_algen_agent_runtime_rename.sql +54 -0
  69. algen_agent_runtime/persistence/migrations/006_artifact_lifecycle.sql +33 -0
  70. algen_agent_runtime/persistence/migrations/007_s3_artifacts.sql +21 -0
  71. algen_agent_runtime/persistence/migrations/008_workflow_checkpoints.sql +18 -0
  72. algen_agent_runtime/persistence/postgres.py +412 -0
  73. algen_agent_runtime/persistence/redis.py +87 -0
  74. algen_agent_runtime/persistence/s3.py +245 -0
  75. algen_agent_runtime/persistence/tool_executions.py +49 -0
  76. algen_agent_runtime/planning/contracts.py +54 -0
  77. algen_agent_runtime/planning/planners.py +92 -0
  78. algen_agent_runtime/plugins/sdk.py +66 -0
  79. algen_agent_runtime/policies/contracts.py +40 -0
  80. algen_agent_runtime/policies/engine.py +446 -0
  81. algen_agent_runtime/policies/instrumentation.py +65 -0
  82. algen_agent_runtime/py.typed +1 -0
  83. algen_agent_runtime/responses/composer.py +35 -0
  84. algen_agent_runtime/retrieval/contracts.py +60 -0
  85. algen_agent_runtime/retrieval/ingestion.py +52 -0
  86. algen_agent_runtime/retrieval/memory.py +206 -0
  87. algen_agent_runtime/retrieval/registry.py +24 -0
  88. algen_agent_runtime/retrieval/vector_stores.py +610 -0
  89. algen_agent_runtime/runtime/client.py +32 -0
  90. algen_agent_runtime/runtime/runtime.py +1250 -0
  91. algen_agent_runtime/runtime/state_machine.py +77 -0
  92. algen_agent_runtime/security/documents.py +358 -0
  93. algen_agent_runtime/security/network.py +44 -0
  94. algen_agent_runtime/security/redaction.py +23 -0
  95. algen_agent_runtime/semantics/__init__.py +82 -0
  96. algen_agent_runtime/semantics/compiler.py +192 -0
  97. algen_agent_runtime/semantics/contracts.py +404 -0
  98. algen_agent_runtime/semantics/layer.py +390 -0
  99. algen_agent_runtime/semantics/planner.py +99 -0
  100. algen_agent_runtime/semantics/production_compiler.py +324 -0
  101. algen_agent_runtime/tools/adapters.py +95 -0
  102. algen_agent_runtime/tools/builtin.py +112 -0
  103. algen_agent_runtime/tools/contracts.py +104 -0
  104. algen_agent_runtime/tools/executor.py +193 -0
  105. algen_agent_runtime/tools/registry.py +24 -0
  106. algen_agent_runtime/tools/subagent.py +48 -0
  107. algen_agent_runtime/types/contracts.py +413 -0
  108. algen_agent_runtime/types/interfaces.py +145 -0
  109. algen_agent_runtime/verification/analytical.py +95 -0
  110. algen_agent_runtime/verification/verifiers.py +130 -0
  111. algen_agent_runtime/workflows/__init__.py +59 -0
  112. algen_agent_runtime/workflows/contracts.py +338 -0
  113. algen_agent_runtime/workflows/engine.py +1443 -0
  114. algen_agent_runtime/workflows/store.py +118 -0
  115. algen_agent_runtime-0.1.0a1.dist-info/METADATA +480 -0
  116. algen_agent_runtime-0.1.0a1.dist-info/RECORD +255 -0
  117. algen_agent_runtime-0.1.0a1.dist-info/WHEEL +4 -0
  118. algen_agent_runtime-0.1.0a1.dist-info/entry_points.txt +2 -0
  119. algen_agent_runtime-0.1.0a1.dist-info/licenses/LICENSE +201 -0
  120. examples/__init__.py +37 -0
  121. examples/case_study_responsible_hiring/README.md +86 -0
  122. examples/case_study_responsible_hiring/__init__.py +1 -0
  123. examples/case_study_responsible_hiring/app.py +20 -0
  124. examples/case_study_responsible_hiring/application/__init__.py +1 -0
  125. examples/case_study_responsible_hiring/application/contracts.py +132 -0
  126. examples/case_study_responsible_hiring/application/document_pipeline.py +30 -0
  127. examples/case_study_responsible_hiring/application/repositories.py +140 -0
  128. examples/case_study_responsible_hiring/application/workflow.py +473 -0
  129. examples/case_study_responsible_hiring/application/workflow_hooks.py +66 -0
  130. examples/case_study_responsible_hiring/config/agent.yaml +318 -0
  131. examples/case_study_responsible_hiring/dashboard.py +195 -0
  132. examples/case_study_responsible_hiring/data/template_corpus.md +18 -0
  133. examples/case_study_responsible_hiring/database/migrations/001_hiring_schema.sql +27 -0
  134. examples/case_study_responsible_hiring/ui/index.html +424 -0
  135. examples/case_study_teaching_assistant/DEMO_GUIDE.md +48 -0
  136. examples/case_study_teaching_assistant/Dockerfile +22 -0
  137. examples/case_study_teaching_assistant/README.md +111 -0
  138. examples/case_study_teaching_assistant/__init__.py +1 -0
  139. examples/case_study_teaching_assistant/app.py +63 -0
  140. examples/case_study_teaching_assistant/application/__init__.py +12 -0
  141. examples/case_study_teaching_assistant/application/handler.py +114 -0
  142. examples/case_study_teaching_assistant/application/policies.py +89 -0
  143. examples/case_study_teaching_assistant/application/tools.py +253 -0
  144. examples/case_study_teaching_assistant/application/workflow_hooks.py +11 -0
  145. examples/case_study_teaching_assistant/config/agent.yaml +204 -0
  146. examples/case_study_teaching_assistant/config/ecs.yaml +37 -0
  147. examples/case_study_teaching_assistant/dashboard.py +70 -0
  148. examples/case_study_teaching_assistant/deploy/README.md +29 -0
  149. examples/case_study_teaching_assistant/deploy/ecs-task-definition.example.json +46 -0
  150. examples/case_study_teaching_assistant/ui/index.html +55 -0
  151. examples/pattern_approval_workflow/README.md +13 -0
  152. examples/pattern_approval_workflow/__init__.py +1 -0
  153. examples/pattern_approval_workflow/agent.yaml +71 -0
  154. examples/pattern_approval_workflow/app.py +24 -0
  155. examples/pattern_approval_workflow/hooks.py +53 -0
  156. examples/pattern_approval_workflow/manifest.yaml +23 -0
  157. examples/pattern_child_workflow/README.md +9 -0
  158. examples/pattern_child_workflow/__init__.py +1 -0
  159. examples/pattern_child_workflow/agent.yaml +69 -0
  160. examples/pattern_child_workflow/app.py +21 -0
  161. examples/pattern_child_workflow/hooks.py +20 -0
  162. examples/pattern_child_workflow/manifest.yaml +21 -0
  163. examples/pattern_evaluation_gate/README.md +12 -0
  164. examples/pattern_evaluation_gate/__init__.py +1 -0
  165. examples/pattern_evaluation_gate/agent.yaml +36 -0
  166. examples/pattern_evaluation_gate/app.py +51 -0
  167. examples/pattern_evaluation_gate/hooks.py +23 -0
  168. examples/pattern_evaluation_gate/manifest.yaml +22 -0
  169. examples/pattern_governed_research/README.md +11 -0
  170. examples/pattern_governed_research/__init__.py +1 -0
  171. examples/pattern_governed_research/agent.yaml +57 -0
  172. examples/pattern_governed_research/app.py +24 -0
  173. examples/pattern_governed_research/hooks.py +53 -0
  174. examples/pattern_governed_research/manifest.yaml +23 -0
  175. examples/pattern_langgraph_governance/README.md +8 -0
  176. examples/pattern_langgraph_governance/__init__.py +1 -0
  177. examples/pattern_langgraph_governance/agent.yaml +36 -0
  178. examples/pattern_langgraph_governance/app.py +24 -0
  179. examples/pattern_langgraph_governance/hooks.py +44 -0
  180. examples/pattern_langgraph_governance/manifest.yaml +26 -0
  181. examples/pattern_multi_agent_fanout/README.md +11 -0
  182. examples/pattern_multi_agent_fanout/__init__.py +1 -0
  183. examples/pattern_multi_agent_fanout/agent.yaml +76 -0
  184. examples/pattern_multi_agent_fanout/app.py +21 -0
  185. examples/pattern_multi_agent_fanout/hooks.py +69 -0
  186. examples/pattern_multi_agent_fanout/manifest.yaml +22 -0
  187. examples/pattern_provider_fallback/README.md +65 -0
  188. examples/pattern_provider_fallback/__init__.py +1 -0
  189. examples/pattern_provider_fallback/agent.yaml +25 -0
  190. examples/pattern_provider_fallback/app.py +122 -0
  191. examples/pattern_provider_fallback/manifest.yaml +29 -0
  192. examples/pattern_text_to_sql/README.md +52 -0
  193. examples/pattern_text_to_sql/__init__.py +1 -0
  194. examples/pattern_text_to_sql/advanced/README.md +42 -0
  195. examples/pattern_text_to_sql/advanced/__init__.py +1 -0
  196. examples/pattern_text_to_sql/advanced/agent.yaml +84 -0
  197. examples/pattern_text_to_sql/advanced/app.py +75 -0
  198. examples/pattern_text_to_sql/advanced/knowledge.py +69 -0
  199. examples/pattern_text_to_sql/advanced/migrations/001_schema_embeddings.sql +24 -0
  200. examples/pattern_text_to_sql/agent.yaml +138 -0
  201. examples/pattern_text_to_sql/app.py +228 -0
  202. examples/pattern_text_to_sql/manifest.yaml +52 -0
  203. examples/pattern_text_to_sql/schema.sql +29 -0
  204. examples/plugins.py +38 -0
  205. examples/providers.yaml +101 -0
  206. examples/quickstart_approval/README.md +9 -0
  207. examples/quickstart_approval/__init__.py +1 -0
  208. examples/quickstart_approval/agent.yaml +58 -0
  209. examples/quickstart_approval/app.py +24 -0
  210. examples/quickstart_approval/hooks.py +43 -0
  211. examples/quickstart_approval/manifest.yaml +22 -0
  212. examples/quickstart_cloud_chat/README.md +11 -0
  213. examples/quickstart_cloud_chat/__init__.py +1 -0
  214. examples/quickstart_cloud_chat/agent.yaml +28 -0
  215. examples/quickstart_cloud_chat/app.py +33 -0
  216. examples/quickstart_cloud_chat/manifest.yaml +26 -0
  217. examples/quickstart_local_chat/README.md +70 -0
  218. examples/quickstart_local_chat/__init__.py +1 -0
  219. examples/quickstart_local_chat/agent.yaml +31 -0
  220. examples/quickstart_local_chat/app.py +80 -0
  221. examples/quickstart_local_chat/manifest.yaml +34 -0
  222. examples/quickstart_rag/README.md +24 -0
  223. examples/quickstart_rag/__init__.py +1 -0
  224. examples/quickstart_rag/agent.yaml +103 -0
  225. examples/quickstart_rag/app.py +63 -0
  226. examples/quickstart_rag/manifest.yaml +37 -0
  227. examples/quickstart_rag/tests/test_smoke.py +42 -0
  228. examples/quickstart_tool/README.md +11 -0
  229. examples/quickstart_tool/__init__.py +1 -0
  230. examples/quickstart_tool/agent.yaml +42 -0
  231. examples/quickstart_tool/app.py +22 -0
  232. examples/quickstart_tool/hooks.py +29 -0
  233. examples/quickstart_tool/manifest.yaml +22 -0
  234. examples/reference_customer_support/README.md +15 -0
  235. examples/reference_customer_support/__init__.py +1 -0
  236. examples/reference_customer_support/app.py +24 -0
  237. examples/reference_customer_support/application/__init__.py +1 -0
  238. examples/reference_customer_support/application/workflow.py +73 -0
  239. examples/reference_customer_support/config/agent.yaml +98 -0
  240. examples/reference_customer_support/manifest.yaml +31 -0
  241. examples/reference_incident_response/README.md +13 -0
  242. examples/reference_incident_response/__init__.py +1 -0
  243. examples/reference_incident_response/app.py +24 -0
  244. examples/reference_incident_response/application/__init__.py +1 -0
  245. examples/reference_incident_response/application/workflow.py +70 -0
  246. examples/reference_incident_response/config/agent.yaml +109 -0
  247. examples/reference_incident_response/manifest.yaml +23 -0
  248. examples/reference_invoice_exceptions/README.md +15 -0
  249. examples/reference_invoice_exceptions/__init__.py +1 -0
  250. examples/reference_invoice_exceptions/app.py +24 -0
  251. examples/reference_invoice_exceptions/application/__init__.py +1 -0
  252. examples/reference_invoice_exceptions/application/workflow.py +88 -0
  253. examples/reference_invoice_exceptions/config/agent.yaml +97 -0
  254. examples/reference_invoice_exceptions/manifest.yaml +26 -0
  255. 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
+ )