agnt5 0.2.8a2__cp310-abi3-manylinux_2_34_x86_64.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.
Potentially problematic release.
This version of agnt5 might be problematic. Click here for more details.
- agnt5/__init__.py +87 -0
- agnt5/_compat.py +16 -0
- agnt5/_core.abi3.so +0 -0
- agnt5/_retry_utils.py +169 -0
- agnt5/_schema_utils.py +312 -0
- agnt5/_telemetry.py +167 -0
- agnt5/agent.py +956 -0
- agnt5/client.py +724 -0
- agnt5/context.py +84 -0
- agnt5/entity.py +697 -0
- agnt5/exceptions.py +46 -0
- agnt5/function.py +314 -0
- agnt5/lm.py +705 -0
- agnt5/tool.py +418 -0
- agnt5/tracing.py +196 -0
- agnt5/types.py +110 -0
- agnt5/version.py +19 -0
- agnt5/worker.py +1151 -0
- agnt5/workflow.py +596 -0
- agnt5-0.2.8a2.dist-info/METADATA +25 -0
- agnt5-0.2.8a2.dist-info/RECORD +22 -0
- agnt5-0.2.8a2.dist-info/WHEEL +4 -0
agnt5/worker.py
ADDED
|
@@ -0,0 +1,1151 @@
|
|
|
1
|
+
"""Worker implementation for AGNT5 SDK."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import contextvars
|
|
7
|
+
import logging
|
|
8
|
+
from typing import Any, Dict, List, Optional
|
|
9
|
+
|
|
10
|
+
from .function import FunctionRegistry
|
|
11
|
+
from .workflow import WorkflowRegistry
|
|
12
|
+
from ._telemetry import setup_module_logger
|
|
13
|
+
|
|
14
|
+
logger = setup_module_logger(__name__)
|
|
15
|
+
|
|
16
|
+
# Context variable to store trace metadata for propagation to LM calls
|
|
17
|
+
# This allows Rust LM layer to access traceparent without explicit parameter passing
|
|
18
|
+
_trace_metadata: contextvars.ContextVar[Dict[str, str]] = contextvars.ContextVar(
|
|
19
|
+
'_trace_metadata', default={}
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Worker:
|
|
24
|
+
"""AGNT5 Worker for registering and running functions/workflows with the coordinator.
|
|
25
|
+
|
|
26
|
+
The Worker class manages the lifecycle of your service, including:
|
|
27
|
+
- Registration with the AGNT5 coordinator
|
|
28
|
+
- Automatic discovery of @function and @workflow decorated handlers
|
|
29
|
+
- Message handling and execution
|
|
30
|
+
- Health monitoring
|
|
31
|
+
|
|
32
|
+
Example:
|
|
33
|
+
```python
|
|
34
|
+
from agnt5 import Worker, function
|
|
35
|
+
|
|
36
|
+
@function
|
|
37
|
+
async def process_data(ctx: Context, data: str) -> dict:
|
|
38
|
+
return {"result": data.upper()}
|
|
39
|
+
|
|
40
|
+
async def main():
|
|
41
|
+
worker = Worker(
|
|
42
|
+
service_name="data-processor",
|
|
43
|
+
service_version="1.0.0",
|
|
44
|
+
coordinator_endpoint="http://localhost:34186"
|
|
45
|
+
)
|
|
46
|
+
await worker.run()
|
|
47
|
+
|
|
48
|
+
if __name__ == "__main__":
|
|
49
|
+
asyncio.run(main())
|
|
50
|
+
```
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
def __init__(
|
|
54
|
+
self,
|
|
55
|
+
service_name: str,
|
|
56
|
+
service_version: str = "1.0.0",
|
|
57
|
+
coordinator_endpoint: Optional[str] = None,
|
|
58
|
+
runtime: str = "standalone",
|
|
59
|
+
metadata: Optional[Dict[str, str]] = None,
|
|
60
|
+
functions: Optional[List] = None,
|
|
61
|
+
workflows: Optional[List] = None,
|
|
62
|
+
entities: Optional[List] = None,
|
|
63
|
+
agents: Optional[List] = None,
|
|
64
|
+
tools: Optional[List] = None,
|
|
65
|
+
auto_register: bool = False,
|
|
66
|
+
auto_register_paths: Optional[List[str]] = None,
|
|
67
|
+
pyproject_path: Optional[str] = None,
|
|
68
|
+
):
|
|
69
|
+
"""Initialize a new Worker with explicit or automatic component registration.
|
|
70
|
+
|
|
71
|
+
The Worker supports two registration modes:
|
|
72
|
+
|
|
73
|
+
**Explicit Mode (default, production):**
|
|
74
|
+
- Register workflows/agents explicitly, their dependencies are auto-included
|
|
75
|
+
- Optionally register standalone functions/tools for direct API invocation
|
|
76
|
+
|
|
77
|
+
**Auto-Registration Mode (development):**
|
|
78
|
+
- Automatically discovers all decorated components in source paths
|
|
79
|
+
- Reads source paths from pyproject.toml or uses explicit paths
|
|
80
|
+
- No need to maintain import lists
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
service_name: Unique name for this service
|
|
84
|
+
service_version: Version string (semantic versioning recommended)
|
|
85
|
+
coordinator_endpoint: Coordinator endpoint URL (default: from env AGNT5_COORDINATOR_ENDPOINT)
|
|
86
|
+
runtime: Runtime type - "standalone", "docker", "kubernetes", etc.
|
|
87
|
+
metadata: Optional service-level metadata
|
|
88
|
+
functions: List of @function decorated handlers (explicit mode)
|
|
89
|
+
workflows: List of @workflow decorated handlers (explicit mode)
|
|
90
|
+
entities: List of Entity classes (explicit mode)
|
|
91
|
+
agents: List of Agent instances (explicit mode)
|
|
92
|
+
tools: List of Tool instances (explicit mode)
|
|
93
|
+
auto_register: Enable automatic component discovery (default: False)
|
|
94
|
+
auto_register_paths: Explicit source paths to scan (overrides pyproject.toml discovery)
|
|
95
|
+
pyproject_path: Path to pyproject.toml (default: current directory)
|
|
96
|
+
|
|
97
|
+
Example (explicit mode - production):
|
|
98
|
+
```python
|
|
99
|
+
from agnt5 import Worker
|
|
100
|
+
from my_service import greet_user, order_fulfillment, ShoppingCart, analyst_agent
|
|
101
|
+
|
|
102
|
+
worker = Worker(
|
|
103
|
+
service_name="my-service",
|
|
104
|
+
workflows=[order_fulfillment],
|
|
105
|
+
entities=[ShoppingCart],
|
|
106
|
+
agents=[analyst_agent],
|
|
107
|
+
functions=[greet_user],
|
|
108
|
+
)
|
|
109
|
+
await worker.run()
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Example (auto-register mode - development):
|
|
113
|
+
```python
|
|
114
|
+
from agnt5 import Worker
|
|
115
|
+
|
|
116
|
+
worker = Worker(
|
|
117
|
+
service_name="my-service",
|
|
118
|
+
auto_register=True, # Discovers from pyproject.toml
|
|
119
|
+
)
|
|
120
|
+
await worker.run()
|
|
121
|
+
```
|
|
122
|
+
"""
|
|
123
|
+
self.service_name = service_name
|
|
124
|
+
self.service_version = service_version
|
|
125
|
+
self.coordinator_endpoint = coordinator_endpoint
|
|
126
|
+
self.runtime = runtime
|
|
127
|
+
self.metadata = metadata or {}
|
|
128
|
+
|
|
129
|
+
# Import Rust worker
|
|
130
|
+
try:
|
|
131
|
+
from ._core import PyWorker, PyWorkerConfig, PyComponentInfo
|
|
132
|
+
self._PyWorker = PyWorker
|
|
133
|
+
self._PyWorkerConfig = PyWorkerConfig
|
|
134
|
+
self._PyComponentInfo = PyComponentInfo
|
|
135
|
+
except ImportError as e:
|
|
136
|
+
raise ImportError(
|
|
137
|
+
f"Failed to import Rust core worker: {e}. "
|
|
138
|
+
"Make sure agnt5 is properly installed with: pip install agnt5"
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
# Create Rust worker config
|
|
142
|
+
self._rust_config = self._PyWorkerConfig(
|
|
143
|
+
service_name=service_name,
|
|
144
|
+
service_version=service_version,
|
|
145
|
+
service_type=runtime,
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
# Create Rust worker instance
|
|
149
|
+
self._rust_worker = self._PyWorker(self._rust_config)
|
|
150
|
+
|
|
151
|
+
# Create worker-scoped entity state manager
|
|
152
|
+
from .entity import EntityStateManager
|
|
153
|
+
self._entity_state_manager = EntityStateManager()
|
|
154
|
+
|
|
155
|
+
# Component registration: auto-discover or explicit
|
|
156
|
+
if auto_register:
|
|
157
|
+
# Auto-registration mode: discover from source paths
|
|
158
|
+
if auto_register_paths:
|
|
159
|
+
source_paths = auto_register_paths
|
|
160
|
+
logger.info(f"Auto-registration with explicit paths: {source_paths}")
|
|
161
|
+
else:
|
|
162
|
+
source_paths = self._discover_source_paths(pyproject_path)
|
|
163
|
+
logger.info(f"Auto-registration with discovered paths: {source_paths}")
|
|
164
|
+
|
|
165
|
+
# Auto-discover components (will populate _explicit_components)
|
|
166
|
+
self._auto_discover_components(source_paths)
|
|
167
|
+
else:
|
|
168
|
+
# Explicit registration from constructor kwargs
|
|
169
|
+
self._explicit_components = {
|
|
170
|
+
'functions': list(functions or []),
|
|
171
|
+
'workflows': list(workflows or []),
|
|
172
|
+
'entities': list(entities or []),
|
|
173
|
+
'agents': list(agents or []),
|
|
174
|
+
'tools': list(tools or []),
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
# Count explicitly registered components
|
|
178
|
+
total_explicit = sum(len(v) for v in self._explicit_components.values())
|
|
179
|
+
logger.info(
|
|
180
|
+
f"Worker initialized: {service_name} v{service_version} (runtime: {runtime}), "
|
|
181
|
+
f"{total_explicit} components explicitly registered"
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
def register_components(
|
|
185
|
+
self,
|
|
186
|
+
functions=None,
|
|
187
|
+
workflows=None,
|
|
188
|
+
entities=None,
|
|
189
|
+
agents=None,
|
|
190
|
+
tools=None,
|
|
191
|
+
):
|
|
192
|
+
"""Register additional components after Worker initialization.
|
|
193
|
+
|
|
194
|
+
This method allows incremental registration of components after the Worker
|
|
195
|
+
has been created. Useful for conditional or dynamic component registration.
|
|
196
|
+
|
|
197
|
+
Args:
|
|
198
|
+
functions: List of functions decorated with @function
|
|
199
|
+
workflows: List of workflows decorated with @workflow
|
|
200
|
+
entities: List of entity classes
|
|
201
|
+
agents: List of agent instances
|
|
202
|
+
tools: List of tool instances
|
|
203
|
+
|
|
204
|
+
Example:
|
|
205
|
+
```python
|
|
206
|
+
worker = Worker(service_name="my-service")
|
|
207
|
+
|
|
208
|
+
# Register conditionally
|
|
209
|
+
if feature_enabled:
|
|
210
|
+
worker.register_components(workflows=[advanced_workflow])
|
|
211
|
+
```
|
|
212
|
+
"""
|
|
213
|
+
if functions:
|
|
214
|
+
self._explicit_components['functions'].extend(functions)
|
|
215
|
+
logger.debug(f"Incrementally registered {len(functions)} functions")
|
|
216
|
+
|
|
217
|
+
if workflows:
|
|
218
|
+
self._explicit_components['workflows'].extend(workflows)
|
|
219
|
+
logger.debug(f"Incrementally registered {len(workflows)} workflows")
|
|
220
|
+
|
|
221
|
+
if entities:
|
|
222
|
+
self._explicit_components['entities'].extend(entities)
|
|
223
|
+
logger.debug(f"Incrementally registered {len(entities)} entities")
|
|
224
|
+
|
|
225
|
+
if agents:
|
|
226
|
+
self._explicit_components['agents'].extend(agents)
|
|
227
|
+
logger.debug(f"Incrementally registered {len(agents)} agents")
|
|
228
|
+
|
|
229
|
+
if tools:
|
|
230
|
+
self._explicit_components['tools'].extend(tools)
|
|
231
|
+
logger.debug(f"Incrementally registered {len(tools)} tools")
|
|
232
|
+
|
|
233
|
+
total = sum(len(v) for v in self._explicit_components.values())
|
|
234
|
+
logger.info(f"Total components now registered: {total}")
|
|
235
|
+
|
|
236
|
+
def _discover_source_paths(self, pyproject_path: Optional[str] = None) -> List[str]:
|
|
237
|
+
"""Discover source paths from pyproject.toml.
|
|
238
|
+
|
|
239
|
+
Reads pyproject.toml to find package source directories using:
|
|
240
|
+
- Hatch: [tool.hatch.build.targets.wheel] packages
|
|
241
|
+
- Maturin: [tool.maturin] python-source
|
|
242
|
+
- Fallback: ["src"] if not found
|
|
243
|
+
|
|
244
|
+
Args:
|
|
245
|
+
pyproject_path: Path to pyproject.toml (default: current directory)
|
|
246
|
+
|
|
247
|
+
Returns:
|
|
248
|
+
List of directory paths to scan (e.g., ["src/agnt5_benchmark"])
|
|
249
|
+
"""
|
|
250
|
+
from pathlib import Path
|
|
251
|
+
|
|
252
|
+
# Python 3.11+ has tomllib in stdlib
|
|
253
|
+
try:
|
|
254
|
+
import tomllib
|
|
255
|
+
except ImportError:
|
|
256
|
+
logger.error("tomllib not available (Python 3.11+ required for auto-registration)")
|
|
257
|
+
return ["src"]
|
|
258
|
+
|
|
259
|
+
# Determine pyproject.toml location
|
|
260
|
+
if pyproject_path:
|
|
261
|
+
pyproject_file = Path(pyproject_path)
|
|
262
|
+
else:
|
|
263
|
+
# Look in current directory
|
|
264
|
+
pyproject_file = Path.cwd() / "pyproject.toml"
|
|
265
|
+
|
|
266
|
+
if not pyproject_file.exists():
|
|
267
|
+
logger.warning(
|
|
268
|
+
f"pyproject.toml not found at {pyproject_file}, "
|
|
269
|
+
f"defaulting to 'src/' directory"
|
|
270
|
+
)
|
|
271
|
+
return ["src"]
|
|
272
|
+
|
|
273
|
+
# Parse pyproject.toml
|
|
274
|
+
try:
|
|
275
|
+
with open(pyproject_file, "rb") as f:
|
|
276
|
+
config = tomllib.load(f)
|
|
277
|
+
except Exception as e:
|
|
278
|
+
logger.error(f"Failed to parse pyproject.toml: {e}")
|
|
279
|
+
return ["src"]
|
|
280
|
+
|
|
281
|
+
# Extract source paths based on build system
|
|
282
|
+
source_paths = []
|
|
283
|
+
|
|
284
|
+
# Try Hatch configuration
|
|
285
|
+
if "tool" in config and "hatch" in config["tool"]:
|
|
286
|
+
hatch_config = config["tool"]["hatch"]
|
|
287
|
+
if "build" in hatch_config and "targets" in hatch_config["build"]:
|
|
288
|
+
wheel_config = hatch_config["build"]["targets"].get("wheel", {})
|
|
289
|
+
packages = wheel_config.get("packages", [])
|
|
290
|
+
source_paths.extend(packages)
|
|
291
|
+
|
|
292
|
+
# Try Maturin configuration
|
|
293
|
+
if not source_paths and "tool" in config and "maturin" in config["tool"]:
|
|
294
|
+
maturin_config = config["tool"]["maturin"]
|
|
295
|
+
python_source = maturin_config.get("python-source")
|
|
296
|
+
if python_source:
|
|
297
|
+
source_paths.append(python_source)
|
|
298
|
+
|
|
299
|
+
# Fallback to src/
|
|
300
|
+
if not source_paths:
|
|
301
|
+
logger.info("No source paths in pyproject.toml, defaulting to 'src/'")
|
|
302
|
+
source_paths = ["src"]
|
|
303
|
+
|
|
304
|
+
logger.info(f"Discovered source paths from pyproject.toml: {source_paths}")
|
|
305
|
+
return source_paths
|
|
306
|
+
|
|
307
|
+
def _auto_discover_components(self, source_paths: List[str]) -> None:
|
|
308
|
+
"""Auto-discover components by importing all Python files in source paths.
|
|
309
|
+
|
|
310
|
+
Args:
|
|
311
|
+
source_paths: List of directory paths to scan
|
|
312
|
+
"""
|
|
313
|
+
import importlib.util
|
|
314
|
+
import sys
|
|
315
|
+
from pathlib import Path
|
|
316
|
+
|
|
317
|
+
logger.info(f"Auto-discovering components in paths: {source_paths}")
|
|
318
|
+
|
|
319
|
+
total_modules = 0
|
|
320
|
+
|
|
321
|
+
for source_path in source_paths:
|
|
322
|
+
path = Path(source_path)
|
|
323
|
+
|
|
324
|
+
if not path.exists():
|
|
325
|
+
logger.warning(f"Source path does not exist: {source_path}")
|
|
326
|
+
continue
|
|
327
|
+
|
|
328
|
+
# Recursively find all .py files
|
|
329
|
+
for py_file in path.rglob("*.py"):
|
|
330
|
+
# Skip __pycache__ and test files
|
|
331
|
+
if "__pycache__" in str(py_file) or py_file.name.startswith("test_"):
|
|
332
|
+
continue
|
|
333
|
+
|
|
334
|
+
# Convert path to module name
|
|
335
|
+
# e.g., src/agnt5_benchmark/functions.py -> agnt5_benchmark.functions
|
|
336
|
+
relative_path = py_file.relative_to(path.parent)
|
|
337
|
+
module_parts = list(relative_path.parts[:-1]) # Remove .py extension part
|
|
338
|
+
module_parts.append(relative_path.stem) # Add filename without .py
|
|
339
|
+
module_name = ".".join(module_parts)
|
|
340
|
+
|
|
341
|
+
# Import module (triggers decorators)
|
|
342
|
+
try:
|
|
343
|
+
if module_name in sys.modules:
|
|
344
|
+
logger.debug(f"Module already imported: {module_name}")
|
|
345
|
+
else:
|
|
346
|
+
spec = importlib.util.spec_from_file_location(module_name, py_file)
|
|
347
|
+
if spec and spec.loader:
|
|
348
|
+
module = importlib.util.module_from_spec(spec)
|
|
349
|
+
sys.modules[module_name] = module
|
|
350
|
+
spec.loader.exec_module(module)
|
|
351
|
+
logger.debug(f"Auto-imported: {module_name}")
|
|
352
|
+
total_modules += 1
|
|
353
|
+
except Exception as e:
|
|
354
|
+
logger.warning(f"Failed to import {module_name}: {e}")
|
|
355
|
+
|
|
356
|
+
logger.info(f"Auto-imported {total_modules} modules")
|
|
357
|
+
|
|
358
|
+
# Collect components from registries
|
|
359
|
+
from .agent import AgentRegistry
|
|
360
|
+
from .entity import EntityRegistry
|
|
361
|
+
from .tool import ToolRegistry
|
|
362
|
+
|
|
363
|
+
# Extract actual objects from registries
|
|
364
|
+
functions = [cfg.handler for cfg in FunctionRegistry.all().values()]
|
|
365
|
+
workflows = [cfg.handler for cfg in WorkflowRegistry.all().values()]
|
|
366
|
+
entities = [et.entity_class for et in EntityRegistry.all().values()]
|
|
367
|
+
agents = list(AgentRegistry.all().values())
|
|
368
|
+
tools = list(ToolRegistry.all().values())
|
|
369
|
+
|
|
370
|
+
self._explicit_components = {
|
|
371
|
+
'functions': functions,
|
|
372
|
+
'workflows': workflows,
|
|
373
|
+
'entities': entities,
|
|
374
|
+
'agents': agents,
|
|
375
|
+
'tools': tools,
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
logger.info(
|
|
379
|
+
f"Auto-discovered components: "
|
|
380
|
+
f"{len(functions)} functions, "
|
|
381
|
+
f"{len(workflows)} workflows, "
|
|
382
|
+
f"{len(entities)} entities, "
|
|
383
|
+
f"{len(agents)} agents, "
|
|
384
|
+
f"{len(tools)} tools"
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
def _discover_components(self):
|
|
388
|
+
"""Discover explicit components and auto-include their dependencies.
|
|
389
|
+
|
|
390
|
+
Hybrid approach:
|
|
391
|
+
- Explicitly registered workflows/agents are processed
|
|
392
|
+
- Functions called by workflows are auto-included (TODO: implement)
|
|
393
|
+
- Tools used by agents are auto-included
|
|
394
|
+
- Standalone functions/tools can be explicitly registered
|
|
395
|
+
|
|
396
|
+
Returns:
|
|
397
|
+
List of PyComponentInfo instances for all components
|
|
398
|
+
"""
|
|
399
|
+
components = []
|
|
400
|
+
import json
|
|
401
|
+
|
|
402
|
+
# Import registries
|
|
403
|
+
from .entity import EntityRegistry
|
|
404
|
+
from .tool import ToolRegistry
|
|
405
|
+
|
|
406
|
+
# Track all components (explicit + auto-included)
|
|
407
|
+
all_functions = set(self._explicit_components['functions'])
|
|
408
|
+
all_tools = set(self._explicit_components['tools'])
|
|
409
|
+
|
|
410
|
+
# Auto-include agent tool dependencies
|
|
411
|
+
for agent in self._explicit_components['agents']:
|
|
412
|
+
if hasattr(agent, 'tools') and agent.tools:
|
|
413
|
+
# Agent.tools is a dict of {tool_name: tool_instance}
|
|
414
|
+
all_tools.update(agent.tools.values())
|
|
415
|
+
logger.debug(
|
|
416
|
+
f"Auto-included {len(agent.tools)} tools from agent '{agent.name}'"
|
|
417
|
+
)
|
|
418
|
+
|
|
419
|
+
# Log registration summary
|
|
420
|
+
explicit_func_count = len(self._explicit_components['functions'])
|
|
421
|
+
explicit_tool_count = len(self._explicit_components['tools'])
|
|
422
|
+
auto_func_count = len(all_functions) - explicit_func_count
|
|
423
|
+
auto_tool_count = len(all_tools) - explicit_tool_count
|
|
424
|
+
|
|
425
|
+
logger.info(
|
|
426
|
+
f"Component registration summary: "
|
|
427
|
+
f"{len(all_functions)} functions ({explicit_func_count} explicit, {auto_func_count} auto-included), "
|
|
428
|
+
f"{len(self._explicit_components['workflows'])} workflows, "
|
|
429
|
+
f"{len(self._explicit_components['entities'])} entities, "
|
|
430
|
+
f"{len(self._explicit_components['agents'])} agents, "
|
|
431
|
+
f"{len(all_tools)} tools ({explicit_tool_count} explicit, {auto_tool_count} auto-included)"
|
|
432
|
+
)
|
|
433
|
+
|
|
434
|
+
# Process functions (explicit + auto-included)
|
|
435
|
+
for func in all_functions:
|
|
436
|
+
config = FunctionRegistry.get(func.__name__)
|
|
437
|
+
if not config:
|
|
438
|
+
logger.warning(f"Function '{func.__name__}' not found in FunctionRegistry")
|
|
439
|
+
continue
|
|
440
|
+
|
|
441
|
+
input_schema_str = json.dumps(config.input_schema) if config.input_schema else None
|
|
442
|
+
output_schema_str = json.dumps(config.output_schema) if config.output_schema else None
|
|
443
|
+
metadata = config.metadata if config.metadata else {}
|
|
444
|
+
|
|
445
|
+
component_info = self._PyComponentInfo(
|
|
446
|
+
name=config.name,
|
|
447
|
+
component_type="function",
|
|
448
|
+
metadata=metadata,
|
|
449
|
+
config={},
|
|
450
|
+
input_schema=input_schema_str,
|
|
451
|
+
output_schema=output_schema_str,
|
|
452
|
+
definition=None,
|
|
453
|
+
)
|
|
454
|
+
components.append(component_info)
|
|
455
|
+
|
|
456
|
+
# Process workflows
|
|
457
|
+
for workflow in self._explicit_components['workflows']:
|
|
458
|
+
config = WorkflowRegistry.get(workflow.__name__)
|
|
459
|
+
if not config:
|
|
460
|
+
logger.warning(f"Workflow '{workflow.__name__}' not found in WorkflowRegistry")
|
|
461
|
+
continue
|
|
462
|
+
|
|
463
|
+
input_schema_str = json.dumps(config.input_schema) if config.input_schema else None
|
|
464
|
+
output_schema_str = json.dumps(config.output_schema) if config.output_schema else None
|
|
465
|
+
metadata = config.metadata if config.metadata else {}
|
|
466
|
+
|
|
467
|
+
component_info = self._PyComponentInfo(
|
|
468
|
+
name=config.name,
|
|
469
|
+
component_type="workflow",
|
|
470
|
+
metadata=metadata,
|
|
471
|
+
config={},
|
|
472
|
+
input_schema=input_schema_str,
|
|
473
|
+
output_schema=output_schema_str,
|
|
474
|
+
definition=None,
|
|
475
|
+
)
|
|
476
|
+
components.append(component_info)
|
|
477
|
+
|
|
478
|
+
# Process entities
|
|
479
|
+
for entity_class in self._explicit_components['entities']:
|
|
480
|
+
entity_type = EntityRegistry.get(entity_class.__name__)
|
|
481
|
+
if not entity_type:
|
|
482
|
+
logger.warning(f"Entity '{entity_class.__name__}' not found in EntityRegistry")
|
|
483
|
+
continue
|
|
484
|
+
|
|
485
|
+
# Build complete entity definition with state schema and method schemas
|
|
486
|
+
entity_definition = entity_type.build_entity_definition()
|
|
487
|
+
definition_str = json.dumps(entity_definition)
|
|
488
|
+
|
|
489
|
+
# Keep minimal metadata for backward compatibility
|
|
490
|
+
metadata_dict = {
|
|
491
|
+
"methods": json.dumps(list(entity_type._method_schemas.keys())),
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
component_info = self._PyComponentInfo(
|
|
495
|
+
name=entity_type.name,
|
|
496
|
+
component_type="entity",
|
|
497
|
+
metadata=metadata_dict,
|
|
498
|
+
config={},
|
|
499
|
+
input_schema=None, # Entities don't have single input/output schemas
|
|
500
|
+
output_schema=None,
|
|
501
|
+
definition=definition_str, # Complete entity definition with state and methods
|
|
502
|
+
)
|
|
503
|
+
components.append(component_info)
|
|
504
|
+
logger.debug(f"Registered entity '{entity_type.name}' with definition")
|
|
505
|
+
|
|
506
|
+
# Process agents
|
|
507
|
+
from .agent import AgentRegistry
|
|
508
|
+
|
|
509
|
+
for agent in self._explicit_components['agents']:
|
|
510
|
+
# Register agent in AgentRegistry for execution lookup
|
|
511
|
+
AgentRegistry.register(agent)
|
|
512
|
+
logger.debug(f"Registered agent '{agent.name}' in AgentRegistry for execution")
|
|
513
|
+
|
|
514
|
+
input_schema_str = json.dumps(agent.input_schema) if hasattr(agent, 'input_schema') and agent.input_schema else None
|
|
515
|
+
output_schema_str = json.dumps(agent.output_schema) if hasattr(agent, 'output_schema') and agent.output_schema else None
|
|
516
|
+
|
|
517
|
+
metadata_dict = agent.metadata if hasattr(agent, 'metadata') else {}
|
|
518
|
+
if hasattr(agent, 'tools'):
|
|
519
|
+
metadata_dict["tools"] = json.dumps(list(agent.tools.keys()))
|
|
520
|
+
|
|
521
|
+
component_info = self._PyComponentInfo(
|
|
522
|
+
name=agent.name,
|
|
523
|
+
component_type="agent",
|
|
524
|
+
metadata=metadata_dict,
|
|
525
|
+
config={},
|
|
526
|
+
input_schema=input_schema_str,
|
|
527
|
+
output_schema=output_schema_str,
|
|
528
|
+
definition=None,
|
|
529
|
+
)
|
|
530
|
+
components.append(component_info)
|
|
531
|
+
|
|
532
|
+
# Process tools (explicit + auto-included)
|
|
533
|
+
for tool in all_tools:
|
|
534
|
+
input_schema_str = json.dumps(tool.input_schema) if hasattr(tool, 'input_schema') and tool.input_schema else None
|
|
535
|
+
output_schema_str = json.dumps(tool.output_schema) if hasattr(tool, 'output_schema') and tool.output_schema else None
|
|
536
|
+
|
|
537
|
+
component_info = self._PyComponentInfo(
|
|
538
|
+
name=tool.name,
|
|
539
|
+
component_type="tool",
|
|
540
|
+
metadata={},
|
|
541
|
+
config={},
|
|
542
|
+
input_schema=input_schema_str,
|
|
543
|
+
output_schema=output_schema_str,
|
|
544
|
+
definition=None,
|
|
545
|
+
)
|
|
546
|
+
components.append(component_info)
|
|
547
|
+
|
|
548
|
+
logger.info(f"Discovered {len(components)} total components")
|
|
549
|
+
return components
|
|
550
|
+
|
|
551
|
+
def _create_message_handler(self):
|
|
552
|
+
"""Create the message handler that will be called by Rust worker."""
|
|
553
|
+
|
|
554
|
+
def handle_message(request):
|
|
555
|
+
"""Handle incoming execution requests - returns coroutine for Rust to await."""
|
|
556
|
+
# Extract request details
|
|
557
|
+
component_name = request.component_name
|
|
558
|
+
component_type = request.component_type
|
|
559
|
+
input_data = request.input_data
|
|
560
|
+
|
|
561
|
+
logger.debug(
|
|
562
|
+
f"Handling {component_type} request: {component_name}, input size: {len(input_data)} bytes"
|
|
563
|
+
)
|
|
564
|
+
|
|
565
|
+
# Import all registries
|
|
566
|
+
from .tool import ToolRegistry
|
|
567
|
+
from .entity import EntityRegistry
|
|
568
|
+
from .agent import AgentRegistry
|
|
569
|
+
|
|
570
|
+
# Route based on component type and return coroutines
|
|
571
|
+
if component_type == "tool":
|
|
572
|
+
tool = ToolRegistry.get(component_name)
|
|
573
|
+
if tool:
|
|
574
|
+
logger.debug(f"Found tool: {component_name}")
|
|
575
|
+
# Return coroutine, don't await it
|
|
576
|
+
return self._execute_tool(tool, input_data, request)
|
|
577
|
+
|
|
578
|
+
elif component_type == "entity":
|
|
579
|
+
entity_type = EntityRegistry.get(component_name)
|
|
580
|
+
if entity_type:
|
|
581
|
+
logger.debug(f"Found entity: {component_name}")
|
|
582
|
+
# Return coroutine, don't await it
|
|
583
|
+
return self._execute_entity(entity_type, input_data, request)
|
|
584
|
+
|
|
585
|
+
elif component_type == "agent":
|
|
586
|
+
agent = AgentRegistry.get(component_name)
|
|
587
|
+
if agent:
|
|
588
|
+
logger.debug(f"Found agent: {component_name}")
|
|
589
|
+
# Return coroutine, don't await it
|
|
590
|
+
return self._execute_agent(agent, input_data, request)
|
|
591
|
+
|
|
592
|
+
elif component_type == "workflow":
|
|
593
|
+
workflow_config = WorkflowRegistry.get(component_name)
|
|
594
|
+
if workflow_config:
|
|
595
|
+
logger.debug(f"Found workflow: {component_name}")
|
|
596
|
+
# Return coroutine, don't await it
|
|
597
|
+
return self._execute_workflow(workflow_config, input_data, request)
|
|
598
|
+
|
|
599
|
+
elif component_type == "function":
|
|
600
|
+
function_config = FunctionRegistry.get(component_name)
|
|
601
|
+
if function_config:
|
|
602
|
+
logger.info(f"🔥 WORKER: Received request for function: {component_name}")
|
|
603
|
+
# Return coroutine, don't await it
|
|
604
|
+
return self._execute_function(function_config, input_data, request)
|
|
605
|
+
|
|
606
|
+
# Not found - need to return an async error response
|
|
607
|
+
error_msg = f"Component '{component_name}' of type '{component_type}' not found"
|
|
608
|
+
logger.error(error_msg)
|
|
609
|
+
|
|
610
|
+
# Create async wrapper for error response
|
|
611
|
+
async def error_response():
|
|
612
|
+
return self._create_error_response(request, error_msg)
|
|
613
|
+
|
|
614
|
+
return error_response()
|
|
615
|
+
|
|
616
|
+
return handle_message
|
|
617
|
+
|
|
618
|
+
async def _execute_function(self, config, input_data: bytes, request):
|
|
619
|
+
"""Execute a function handler (supports both regular and streaming functions)."""
|
|
620
|
+
import json
|
|
621
|
+
import inspect
|
|
622
|
+
import time
|
|
623
|
+
from .context import Context
|
|
624
|
+
from ._core import PyExecuteComponentResponse
|
|
625
|
+
|
|
626
|
+
exec_start = time.time()
|
|
627
|
+
logger.info(f"🔥 WORKER: Executing function {config.name}")
|
|
628
|
+
|
|
629
|
+
try:
|
|
630
|
+
# Parse input data
|
|
631
|
+
input_dict = json.loads(input_data.decode("utf-8")) if input_data else {}
|
|
632
|
+
|
|
633
|
+
# Store trace metadata in contextvar for LM calls to access
|
|
634
|
+
# The Rust worker injects traceparent into request.metadata for trace propagation
|
|
635
|
+
if hasattr(request, 'metadata') and request.metadata:
|
|
636
|
+
_trace_metadata.set(dict(request.metadata))
|
|
637
|
+
logger.debug(f"Trace metadata stored: traceparent={request.metadata.get('traceparent', 'N/A')}")
|
|
638
|
+
|
|
639
|
+
# Create context with runtime_context for trace correlation
|
|
640
|
+
ctx = Context(
|
|
641
|
+
run_id=f"{self.service_name}:{config.name}",
|
|
642
|
+
runtime_context=request.runtime_context,
|
|
643
|
+
)
|
|
644
|
+
|
|
645
|
+
# Create span for function execution with trace linking
|
|
646
|
+
from ._core import create_span
|
|
647
|
+
|
|
648
|
+
with create_span(
|
|
649
|
+
config.name,
|
|
650
|
+
"function",
|
|
651
|
+
request.runtime_context,
|
|
652
|
+
{
|
|
653
|
+
"function.name": config.name,
|
|
654
|
+
"service.name": self.service_name,
|
|
655
|
+
},
|
|
656
|
+
) as span:
|
|
657
|
+
# Execute function
|
|
658
|
+
if input_dict:
|
|
659
|
+
result = config.handler(ctx, **input_dict)
|
|
660
|
+
else:
|
|
661
|
+
result = config.handler(ctx)
|
|
662
|
+
|
|
663
|
+
# Debug: Log what type result is
|
|
664
|
+
logger.info(f"🔥 WORKER: Function result type: {type(result).__name__}, isasyncgen: {inspect.isasyncgen(result)}, iscoroutine: {inspect.iscoroutine(result)}")
|
|
665
|
+
|
|
666
|
+
# Note: Removed flush_telemetry_py() call here - it was causing 2-second blocking delay!
|
|
667
|
+
# The batch span processor handles flushing automatically with 5s timeout
|
|
668
|
+
# We only need to flush on worker shutdown, not after each function execution
|
|
669
|
+
|
|
670
|
+
# Check if result is an async generator (streaming function)
|
|
671
|
+
if inspect.isasyncgen(result):
|
|
672
|
+
# Streaming function - return list of responses
|
|
673
|
+
# Rust bridge will send each response separately to coordinator
|
|
674
|
+
responses = []
|
|
675
|
+
chunk_index = 0
|
|
676
|
+
|
|
677
|
+
async for chunk in result:
|
|
678
|
+
# Serialize chunk
|
|
679
|
+
chunk_data = json.dumps(chunk).encode("utf-8")
|
|
680
|
+
|
|
681
|
+
responses.append(PyExecuteComponentResponse(
|
|
682
|
+
invocation_id=request.invocation_id,
|
|
683
|
+
success=True,
|
|
684
|
+
output_data=chunk_data,
|
|
685
|
+
state_update=None,
|
|
686
|
+
error_message=None,
|
|
687
|
+
metadata=None,
|
|
688
|
+
is_chunk=True,
|
|
689
|
+
done=False,
|
|
690
|
+
chunk_index=chunk_index,
|
|
691
|
+
))
|
|
692
|
+
chunk_index += 1
|
|
693
|
+
|
|
694
|
+
# Add final "done" marker
|
|
695
|
+
responses.append(PyExecuteComponentResponse(
|
|
696
|
+
invocation_id=request.invocation_id,
|
|
697
|
+
success=True,
|
|
698
|
+
output_data=b"",
|
|
699
|
+
state_update=None,
|
|
700
|
+
error_message=None,
|
|
701
|
+
metadata=None,
|
|
702
|
+
is_chunk=True,
|
|
703
|
+
done=True,
|
|
704
|
+
chunk_index=chunk_index,
|
|
705
|
+
))
|
|
706
|
+
|
|
707
|
+
logger.debug(f"Streaming function produced {len(responses)} chunks")
|
|
708
|
+
return responses
|
|
709
|
+
else:
|
|
710
|
+
# Regular function - await and return single response
|
|
711
|
+
if inspect.iscoroutine(result):
|
|
712
|
+
result = await result
|
|
713
|
+
|
|
714
|
+
# Serialize result
|
|
715
|
+
output_data = json.dumps(result).encode("utf-8")
|
|
716
|
+
|
|
717
|
+
return PyExecuteComponentResponse(
|
|
718
|
+
invocation_id=request.invocation_id,
|
|
719
|
+
success=True,
|
|
720
|
+
output_data=output_data,
|
|
721
|
+
state_update=None,
|
|
722
|
+
error_message=None,
|
|
723
|
+
metadata=None,
|
|
724
|
+
is_chunk=False,
|
|
725
|
+
done=True,
|
|
726
|
+
chunk_index=0,
|
|
727
|
+
)
|
|
728
|
+
|
|
729
|
+
except Exception as e:
|
|
730
|
+
# Include exception type for better error messages
|
|
731
|
+
error_msg = f"{type(e).__name__}: {str(e)}"
|
|
732
|
+
logger.error(f"Function execution failed: {error_msg}", exc_info=True)
|
|
733
|
+
return PyExecuteComponentResponse(
|
|
734
|
+
invocation_id=request.invocation_id,
|
|
735
|
+
success=False,
|
|
736
|
+
output_data=b"",
|
|
737
|
+
state_update=None,
|
|
738
|
+
error_message=error_msg,
|
|
739
|
+
metadata=None,
|
|
740
|
+
is_chunk=False,
|
|
741
|
+
done=True,
|
|
742
|
+
chunk_index=0,
|
|
743
|
+
)
|
|
744
|
+
|
|
745
|
+
async def _execute_workflow(self, config, input_data: bytes, request):
|
|
746
|
+
"""Execute a workflow handler with automatic replay support."""
|
|
747
|
+
import json
|
|
748
|
+
from .workflow import WorkflowEntity, WorkflowContext
|
|
749
|
+
from .entity import _get_state_manager
|
|
750
|
+
from ._core import PyExecuteComponentResponse
|
|
751
|
+
|
|
752
|
+
try:
|
|
753
|
+
# Parse input data
|
|
754
|
+
input_dict = json.loads(input_data.decode("utf-8")) if input_data else {}
|
|
755
|
+
|
|
756
|
+
# Parse replay data from request metadata for crash recovery
|
|
757
|
+
completed_steps = {}
|
|
758
|
+
initial_state = {}
|
|
759
|
+
|
|
760
|
+
if hasattr(request, 'metadata') and request.metadata:
|
|
761
|
+
# Parse completed steps for replay
|
|
762
|
+
if "completed_steps" in request.metadata:
|
|
763
|
+
completed_steps_json = request.metadata["completed_steps"]
|
|
764
|
+
if completed_steps_json:
|
|
765
|
+
try:
|
|
766
|
+
completed_steps = json.loads(completed_steps_json)
|
|
767
|
+
logger.info(f"🔄 Replaying workflow with {len(completed_steps)} cached steps")
|
|
768
|
+
except json.JSONDecodeError:
|
|
769
|
+
logger.warning("Failed to parse completed_steps from metadata")
|
|
770
|
+
|
|
771
|
+
# Parse initial workflow state for replay
|
|
772
|
+
if "workflow_state" in request.metadata:
|
|
773
|
+
workflow_state_json = request.metadata["workflow_state"]
|
|
774
|
+
if workflow_state_json:
|
|
775
|
+
try:
|
|
776
|
+
initial_state = json.loads(workflow_state_json)
|
|
777
|
+
logger.info(f"🔄 Loaded workflow state: {len(initial_state)} keys")
|
|
778
|
+
except json.JSONDecodeError:
|
|
779
|
+
logger.warning("Failed to parse workflow_state from metadata")
|
|
780
|
+
|
|
781
|
+
# Create WorkflowEntity for state management
|
|
782
|
+
workflow_entity = WorkflowEntity(run_id=f"{self.service_name}:{config.name}")
|
|
783
|
+
|
|
784
|
+
# Load replay data into entity if provided
|
|
785
|
+
if completed_steps:
|
|
786
|
+
workflow_entity._completed_steps = completed_steps
|
|
787
|
+
logger.debug(f"Loaded {len(completed_steps)} completed steps into workflow entity")
|
|
788
|
+
|
|
789
|
+
if initial_state:
|
|
790
|
+
# Load initial state into entity's state manager
|
|
791
|
+
state_manager = _get_state_manager()
|
|
792
|
+
state_manager._states[workflow_entity._state_key] = initial_state
|
|
793
|
+
logger.debug(f"Loaded initial state with {len(initial_state)} keys into workflow entity")
|
|
794
|
+
|
|
795
|
+
# Create WorkflowContext with entity and runtime_context for trace correlation
|
|
796
|
+
ctx = WorkflowContext(
|
|
797
|
+
workflow_entity=workflow_entity,
|
|
798
|
+
run_id=f"{self.service_name}:{config.name}",
|
|
799
|
+
runtime_context=request.runtime_context,
|
|
800
|
+
)
|
|
801
|
+
|
|
802
|
+
# Create span for workflow execution with trace linking
|
|
803
|
+
from ._core import create_span
|
|
804
|
+
|
|
805
|
+
with create_span(
|
|
806
|
+
config.name,
|
|
807
|
+
"workflow",
|
|
808
|
+
request.runtime_context,
|
|
809
|
+
{
|
|
810
|
+
"workflow.name": config.name,
|
|
811
|
+
"service.name": self.service_name,
|
|
812
|
+
},
|
|
813
|
+
) as span:
|
|
814
|
+
# Execute workflow
|
|
815
|
+
if input_dict:
|
|
816
|
+
result = await config.handler(ctx, **input_dict)
|
|
817
|
+
else:
|
|
818
|
+
result = await config.handler(ctx)
|
|
819
|
+
|
|
820
|
+
# Note: Removed flush_telemetry_py() call here - it was causing 2-second blocking delay!
|
|
821
|
+
# The batch span processor handles flushing automatically with 5s timeout
|
|
822
|
+
|
|
823
|
+
# Serialize result
|
|
824
|
+
output_data = json.dumps(result).encode("utf-8")
|
|
825
|
+
|
|
826
|
+
# Collect workflow execution metadata for durability
|
|
827
|
+
metadata = {}
|
|
828
|
+
|
|
829
|
+
# Add step events to metadata (for workflow durability)
|
|
830
|
+
# Access _step_events from the workflow entity, not the context
|
|
831
|
+
step_events = ctx._workflow_entity._step_events
|
|
832
|
+
if step_events:
|
|
833
|
+
metadata["step_events"] = json.dumps(step_events)
|
|
834
|
+
logger.debug(f"Workflow has {len(step_events)} recorded steps")
|
|
835
|
+
|
|
836
|
+
# Add final state snapshot to metadata (if state was used)
|
|
837
|
+
# Check if _state was initialized without triggering property getter
|
|
838
|
+
if hasattr(ctx, '_workflow_entity') and ctx._workflow_entity._state is not None:
|
|
839
|
+
if ctx._workflow_entity._state.has_changes():
|
|
840
|
+
state_snapshot = ctx._workflow_entity._state.get_state_snapshot()
|
|
841
|
+
metadata["workflow_state"] = json.dumps(state_snapshot)
|
|
842
|
+
logger.debug(f"Workflow state snapshot: {state_snapshot}")
|
|
843
|
+
|
|
844
|
+
logger.info(f"Workflow completed successfully with {len(step_events)} steps")
|
|
845
|
+
|
|
846
|
+
return PyExecuteComponentResponse(
|
|
847
|
+
invocation_id=request.invocation_id,
|
|
848
|
+
success=True,
|
|
849
|
+
output_data=output_data,
|
|
850
|
+
state_update=None, # Not used for workflows (use metadata instead)
|
|
851
|
+
error_message=None,
|
|
852
|
+
metadata=metadata if metadata else None, # Include step events + state
|
|
853
|
+
is_chunk=False,
|
|
854
|
+
done=True,
|
|
855
|
+
chunk_index=0,
|
|
856
|
+
)
|
|
857
|
+
|
|
858
|
+
except Exception as e:
|
|
859
|
+
# Include exception type for better error messages
|
|
860
|
+
error_msg = f"{type(e).__name__}: {str(e)}"
|
|
861
|
+
logger.error(f"Workflow execution failed: {error_msg}", exc_info=True)
|
|
862
|
+
return PyExecuteComponentResponse(
|
|
863
|
+
invocation_id=request.invocation_id,
|
|
864
|
+
success=False,
|
|
865
|
+
output_data=b"",
|
|
866
|
+
state_update=None,
|
|
867
|
+
error_message=error_msg,
|
|
868
|
+
metadata=None,
|
|
869
|
+
is_chunk=False,
|
|
870
|
+
done=True,
|
|
871
|
+
chunk_index=0,
|
|
872
|
+
)
|
|
873
|
+
|
|
874
|
+
async def _execute_tool(self, tool, input_data: bytes, request):
|
|
875
|
+
"""Execute a tool handler."""
|
|
876
|
+
import json
|
|
877
|
+
from .context import Context
|
|
878
|
+
from ._core import PyExecuteComponentResponse
|
|
879
|
+
|
|
880
|
+
try:
|
|
881
|
+
# Parse input data
|
|
882
|
+
input_dict = json.loads(input_data.decode("utf-8")) if input_data else {}
|
|
883
|
+
|
|
884
|
+
# Create context with runtime_context for trace correlation
|
|
885
|
+
ctx = Context(
|
|
886
|
+
run_id=f"{self.service_name}:{tool.name}",
|
|
887
|
+
runtime_context=request.runtime_context,
|
|
888
|
+
)
|
|
889
|
+
|
|
890
|
+
# Execute tool
|
|
891
|
+
result = await tool.invoke(ctx, **input_dict)
|
|
892
|
+
|
|
893
|
+
# Serialize result
|
|
894
|
+
output_data = json.dumps(result).encode("utf-8")
|
|
895
|
+
|
|
896
|
+
return PyExecuteComponentResponse(
|
|
897
|
+
invocation_id=request.invocation_id,
|
|
898
|
+
success=True,
|
|
899
|
+
output_data=output_data,
|
|
900
|
+
state_update=None,
|
|
901
|
+
error_message=None,
|
|
902
|
+
metadata=None,
|
|
903
|
+
is_chunk=False,
|
|
904
|
+
done=True,
|
|
905
|
+
chunk_index=0,
|
|
906
|
+
)
|
|
907
|
+
|
|
908
|
+
except Exception as e:
|
|
909
|
+
# Include exception type for better error messages
|
|
910
|
+
error_msg = f"{type(e).__name__}: {str(e)}"
|
|
911
|
+
logger.error(f"Tool execution failed: {error_msg}", exc_info=True)
|
|
912
|
+
return PyExecuteComponentResponse(
|
|
913
|
+
invocation_id=request.invocation_id,
|
|
914
|
+
success=False,
|
|
915
|
+
output_data=b"",
|
|
916
|
+
state_update=None,
|
|
917
|
+
error_message=error_msg,
|
|
918
|
+
metadata=None,
|
|
919
|
+
is_chunk=False,
|
|
920
|
+
done=True,
|
|
921
|
+
chunk_index=0,
|
|
922
|
+
)
|
|
923
|
+
|
|
924
|
+
async def _execute_entity(self, entity_type, input_data: bytes, request):
|
|
925
|
+
"""Execute an entity method."""
|
|
926
|
+
import json
|
|
927
|
+
from .context import Context
|
|
928
|
+
from .entity import EntityType, Entity, _entity_state_manager_ctx
|
|
929
|
+
from ._core import PyExecuteComponentResponse
|
|
930
|
+
|
|
931
|
+
# Set entity state manager in context for Entity instances to access
|
|
932
|
+
_entity_state_manager_ctx.set(self._entity_state_manager)
|
|
933
|
+
|
|
934
|
+
try:
|
|
935
|
+
# Parse input data
|
|
936
|
+
input_dict = json.loads(input_data.decode("utf-8")) if input_data else {}
|
|
937
|
+
|
|
938
|
+
# Extract entity key and method name from input
|
|
939
|
+
entity_key = input_dict.pop("key", None)
|
|
940
|
+
method_name = input_dict.pop("method", None)
|
|
941
|
+
|
|
942
|
+
if not entity_key:
|
|
943
|
+
raise ValueError("Entity invocation requires 'key' parameter")
|
|
944
|
+
if not method_name:
|
|
945
|
+
raise ValueError("Entity invocation requires 'method' parameter")
|
|
946
|
+
|
|
947
|
+
# Load state from platform if provided in request metadata
|
|
948
|
+
state_key = (entity_type.name, entity_key)
|
|
949
|
+
if hasattr(request, 'metadata') and request.metadata:
|
|
950
|
+
if "entity_state" in request.metadata:
|
|
951
|
+
platform_state_json = request.metadata["entity_state"]
|
|
952
|
+
platform_version = int(request.metadata.get("state_version", "0"))
|
|
953
|
+
|
|
954
|
+
# Load platform state into state manager
|
|
955
|
+
self._entity_state_manager.load_state_from_platform(
|
|
956
|
+
state_key,
|
|
957
|
+
platform_state_json,
|
|
958
|
+
platform_version
|
|
959
|
+
)
|
|
960
|
+
logger.info(
|
|
961
|
+
f"Loaded entity state from platform: {entity_type.name}/{entity_key} "
|
|
962
|
+
f"(version {platform_version})"
|
|
963
|
+
)
|
|
964
|
+
|
|
965
|
+
# Create entity instance using the stored class reference
|
|
966
|
+
entity_instance = entity_type.entity_class(key=entity_key)
|
|
967
|
+
|
|
968
|
+
# Get method
|
|
969
|
+
if not hasattr(entity_instance, method_name):
|
|
970
|
+
raise ValueError(f"Entity '{entity_type.name}' has no method '{method_name}'")
|
|
971
|
+
|
|
972
|
+
method = getattr(entity_instance, method_name)
|
|
973
|
+
|
|
974
|
+
# Execute method
|
|
975
|
+
result = await method(**input_dict)
|
|
976
|
+
|
|
977
|
+
# Serialize result
|
|
978
|
+
output_data = json.dumps(result).encode("utf-8")
|
|
979
|
+
|
|
980
|
+
# Capture entity state after execution with version tracking
|
|
981
|
+
state_dict, expected_version, new_version = \
|
|
982
|
+
self._entity_state_manager.get_state_for_persistence(state_key)
|
|
983
|
+
|
|
984
|
+
metadata = {}
|
|
985
|
+
if state_dict:
|
|
986
|
+
# Serialize state as JSON string for platform persistence
|
|
987
|
+
state_json = json.dumps(state_dict)
|
|
988
|
+
# Pass in metadata for Worker Coordinator to publish
|
|
989
|
+
metadata = {
|
|
990
|
+
"entity_state": state_json,
|
|
991
|
+
"entity_type": entity_type.name,
|
|
992
|
+
"entity_key": entity_key,
|
|
993
|
+
"expected_version": str(expected_version),
|
|
994
|
+
"new_version": str(new_version),
|
|
995
|
+
}
|
|
996
|
+
logger.info(
|
|
997
|
+
f"Captured entity state: {entity_type.name}/{entity_key} "
|
|
998
|
+
f"(version {expected_version} → {new_version})"
|
|
999
|
+
)
|
|
1000
|
+
|
|
1001
|
+
return PyExecuteComponentResponse(
|
|
1002
|
+
invocation_id=request.invocation_id,
|
|
1003
|
+
success=True,
|
|
1004
|
+
output_data=output_data,
|
|
1005
|
+
state_update=None, # TODO: Use structured StateUpdate object
|
|
1006
|
+
error_message=None,
|
|
1007
|
+
metadata=metadata, # Include state in metadata for Worker Coordinator
|
|
1008
|
+
is_chunk=False,
|
|
1009
|
+
done=True,
|
|
1010
|
+
chunk_index=0,
|
|
1011
|
+
)
|
|
1012
|
+
|
|
1013
|
+
except Exception as e:
|
|
1014
|
+
# Include exception type for better error messages
|
|
1015
|
+
error_msg = f"{type(e).__name__}: {str(e)}"
|
|
1016
|
+
logger.error(f"Entity execution failed: {error_msg}", exc_info=True)
|
|
1017
|
+
return PyExecuteComponentResponse(
|
|
1018
|
+
invocation_id=request.invocation_id,
|
|
1019
|
+
success=False,
|
|
1020
|
+
output_data=b"",
|
|
1021
|
+
state_update=None,
|
|
1022
|
+
error_message=error_msg,
|
|
1023
|
+
metadata=None,
|
|
1024
|
+
is_chunk=False,
|
|
1025
|
+
done=True,
|
|
1026
|
+
chunk_index=0,
|
|
1027
|
+
)
|
|
1028
|
+
|
|
1029
|
+
async def _execute_agent(self, agent, input_data: bytes, request):
|
|
1030
|
+
"""Execute an agent."""
|
|
1031
|
+
import json
|
|
1032
|
+
from .context import Context
|
|
1033
|
+
from ._core import PyExecuteComponentResponse
|
|
1034
|
+
|
|
1035
|
+
try:
|
|
1036
|
+
# Parse input data
|
|
1037
|
+
input_dict = json.loads(input_data.decode("utf-8")) if input_data else {}
|
|
1038
|
+
|
|
1039
|
+
# Extract user message
|
|
1040
|
+
user_message = input_dict.get("message", "")
|
|
1041
|
+
if not user_message:
|
|
1042
|
+
raise ValueError("Agent invocation requires 'message' parameter")
|
|
1043
|
+
|
|
1044
|
+
# Create context with runtime_context for trace correlation
|
|
1045
|
+
ctx = Context(
|
|
1046
|
+
run_id=f"{self.service_name}:{agent.name}",
|
|
1047
|
+
runtime_context=request.runtime_context,
|
|
1048
|
+
)
|
|
1049
|
+
|
|
1050
|
+
# Execute agent
|
|
1051
|
+
agent_result = await agent.run(user_message, context=ctx)
|
|
1052
|
+
|
|
1053
|
+
# Build response
|
|
1054
|
+
result = {
|
|
1055
|
+
"output": agent_result.output,
|
|
1056
|
+
"tool_calls": agent_result.tool_calls,
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
# Serialize result
|
|
1060
|
+
output_data = json.dumps(result).encode("utf-8")
|
|
1061
|
+
|
|
1062
|
+
return PyExecuteComponentResponse(
|
|
1063
|
+
invocation_id=request.invocation_id,
|
|
1064
|
+
success=True,
|
|
1065
|
+
output_data=output_data,
|
|
1066
|
+
state_update=None,
|
|
1067
|
+
error_message=None,
|
|
1068
|
+
metadata=None,
|
|
1069
|
+
is_chunk=False,
|
|
1070
|
+
done=True,
|
|
1071
|
+
chunk_index=0,
|
|
1072
|
+
)
|
|
1073
|
+
|
|
1074
|
+
except Exception as e:
|
|
1075
|
+
# Include exception type for better error messages
|
|
1076
|
+
error_msg = f"{type(e).__name__}: {str(e)}"
|
|
1077
|
+
logger.error(f"Agent execution failed: {error_msg}", exc_info=True)
|
|
1078
|
+
return PyExecuteComponentResponse(
|
|
1079
|
+
invocation_id=request.invocation_id,
|
|
1080
|
+
success=False,
|
|
1081
|
+
output_data=b"",
|
|
1082
|
+
state_update=None,
|
|
1083
|
+
error_message=error_msg,
|
|
1084
|
+
metadata=None,
|
|
1085
|
+
is_chunk=False,
|
|
1086
|
+
done=True,
|
|
1087
|
+
chunk_index=0,
|
|
1088
|
+
)
|
|
1089
|
+
|
|
1090
|
+
def _create_error_response(self, request, error_message: str):
|
|
1091
|
+
"""Create an error response."""
|
|
1092
|
+
from ._core import PyExecuteComponentResponse
|
|
1093
|
+
|
|
1094
|
+
return PyExecuteComponentResponse(
|
|
1095
|
+
invocation_id=request.invocation_id,
|
|
1096
|
+
success=False,
|
|
1097
|
+
output_data=b"",
|
|
1098
|
+
state_update=None,
|
|
1099
|
+
error_message=error_message,
|
|
1100
|
+
metadata=None,
|
|
1101
|
+
is_chunk=False,
|
|
1102
|
+
done=True,
|
|
1103
|
+
chunk_index=0,
|
|
1104
|
+
)
|
|
1105
|
+
|
|
1106
|
+
async def run(self):
|
|
1107
|
+
"""Run the worker (register and start message loop).
|
|
1108
|
+
|
|
1109
|
+
This method will:
|
|
1110
|
+
1. Discover all registered @function and @workflow handlers
|
|
1111
|
+
2. Register with the coordinator
|
|
1112
|
+
3. Create a shared Python event loop for all function executions
|
|
1113
|
+
4. Enter the message processing loop
|
|
1114
|
+
5. Block until shutdown
|
|
1115
|
+
|
|
1116
|
+
This is the main entry point for your worker service.
|
|
1117
|
+
"""
|
|
1118
|
+
logger.info(f"Starting worker: {self.service_name}")
|
|
1119
|
+
|
|
1120
|
+
# Discover components
|
|
1121
|
+
components = self._discover_components()
|
|
1122
|
+
|
|
1123
|
+
# Set components on Rust worker
|
|
1124
|
+
self._rust_worker.set_components(components)
|
|
1125
|
+
|
|
1126
|
+
# Set metadata
|
|
1127
|
+
if self.metadata:
|
|
1128
|
+
self._rust_worker.set_service_metadata(self.metadata)
|
|
1129
|
+
|
|
1130
|
+
# Get the current event loop to pass to Rust for concurrent Python async execution
|
|
1131
|
+
# This allows Rust to execute Python async functions on the same event loop
|
|
1132
|
+
# without spawn_blocking overhead, enabling true concurrency
|
|
1133
|
+
loop = asyncio.get_running_loop()
|
|
1134
|
+
logger.info("Passing Python event loop to Rust worker for concurrent execution")
|
|
1135
|
+
|
|
1136
|
+
# Set event loop on Rust worker
|
|
1137
|
+
self._rust_worker.set_event_loop(loop)
|
|
1138
|
+
|
|
1139
|
+
# Set message handler
|
|
1140
|
+
handler = self._create_message_handler()
|
|
1141
|
+
self._rust_worker.set_message_handler(handler)
|
|
1142
|
+
|
|
1143
|
+
# Initialize worker
|
|
1144
|
+
self._rust_worker.initialize()
|
|
1145
|
+
|
|
1146
|
+
logger.info("Worker registered successfully, entering message loop...")
|
|
1147
|
+
|
|
1148
|
+
# Run worker (this will block until shutdown)
|
|
1149
|
+
await self._rust_worker.run()
|
|
1150
|
+
|
|
1151
|
+
logger.info("Worker shutdown complete")
|