flatmachines 1.0.0__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.
- flatmachines/__init__.py +136 -0
- flatmachines/actions.py +408 -0
- flatmachines/adapters/__init__.py +38 -0
- flatmachines/adapters/flatagent.py +86 -0
- flatmachines/adapters/pi_agent_bridge.py +127 -0
- flatmachines/adapters/pi_agent_runner.mjs +99 -0
- flatmachines/adapters/smolagents.py +125 -0
- flatmachines/agents.py +144 -0
- flatmachines/assets/MACHINES.md +141 -0
- flatmachines/assets/README.md +11 -0
- flatmachines/assets/__init__.py +0 -0
- flatmachines/assets/flatagent.d.ts +219 -0
- flatmachines/assets/flatagent.schema.json +271 -0
- flatmachines/assets/flatagent.slim.d.ts +58 -0
- flatmachines/assets/flatagents-runtime.d.ts +523 -0
- flatmachines/assets/flatagents-runtime.schema.json +281 -0
- flatmachines/assets/flatagents-runtime.slim.d.ts +187 -0
- flatmachines/assets/flatmachine.d.ts +403 -0
- flatmachines/assets/flatmachine.schema.json +620 -0
- flatmachines/assets/flatmachine.slim.d.ts +106 -0
- flatmachines/assets/profiles.d.ts +140 -0
- flatmachines/assets/profiles.schema.json +93 -0
- flatmachines/assets/profiles.slim.d.ts +26 -0
- flatmachines/backends.py +222 -0
- flatmachines/distributed.py +835 -0
- flatmachines/distributed_hooks.py +351 -0
- flatmachines/execution.py +638 -0
- flatmachines/expressions/__init__.py +60 -0
- flatmachines/expressions/cel.py +101 -0
- flatmachines/expressions/simple.py +166 -0
- flatmachines/flatmachine.py +1263 -0
- flatmachines/hooks.py +381 -0
- flatmachines/locking.py +69 -0
- flatmachines/monitoring.py +505 -0
- flatmachines/persistence.py +213 -0
- flatmachines/run.py +117 -0
- flatmachines/utils.py +166 -0
- flatmachines/validation.py +79 -0
- flatmachines-1.0.0.dist-info/METADATA +390 -0
- flatmachines-1.0.0.dist-info/RECORD +41 -0
- flatmachines-1.0.0.dist-info/WHEEL +4 -0
flatmachines/__init__.py
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
__version__ = "1.0.0"
|
|
2
|
+
|
|
3
|
+
from .flatmachine import FlatMachine
|
|
4
|
+
from .hooks import MachineHooks, LoggingHooks, MetricsHooks, CompositeHooks, WebhookHooks
|
|
5
|
+
from .actions import SubprocessInvoker, launch_machine
|
|
6
|
+
from .expressions import get_expression_engine, ExpressionEngine
|
|
7
|
+
from .execution import (
|
|
8
|
+
ExecutionType,
|
|
9
|
+
DefaultExecution,
|
|
10
|
+
ParallelExecution,
|
|
11
|
+
RetryExecution,
|
|
12
|
+
MDAPVotingExecution,
|
|
13
|
+
get_execution_type,
|
|
14
|
+
)
|
|
15
|
+
from .validation import (
|
|
16
|
+
validate_flatmachine_config,
|
|
17
|
+
get_flatmachine_schema,
|
|
18
|
+
get_asset,
|
|
19
|
+
ValidationWarning,
|
|
20
|
+
)
|
|
21
|
+
from .monitoring import (
|
|
22
|
+
setup_logging,
|
|
23
|
+
get_logger,
|
|
24
|
+
get_meter,
|
|
25
|
+
AgentMonitor,
|
|
26
|
+
track_operation,
|
|
27
|
+
)
|
|
28
|
+
from .backends import (
|
|
29
|
+
ResultBackend,
|
|
30
|
+
InMemoryResultBackend,
|
|
31
|
+
LaunchIntent,
|
|
32
|
+
make_uri,
|
|
33
|
+
parse_uri,
|
|
34
|
+
get_default_result_backend,
|
|
35
|
+
reset_default_result_backend,
|
|
36
|
+
)
|
|
37
|
+
from .persistence import (
|
|
38
|
+
PersistenceBackend,
|
|
39
|
+
LocalFileBackend,
|
|
40
|
+
MemoryBackend,
|
|
41
|
+
CheckpointManager,
|
|
42
|
+
MachineSnapshot,
|
|
43
|
+
)
|
|
44
|
+
from .locking import ExecutionLock, LocalFileLock, NoOpLock
|
|
45
|
+
from .agents import (
|
|
46
|
+
AgentExecutor,
|
|
47
|
+
AgentResult,
|
|
48
|
+
AgentRef,
|
|
49
|
+
AgentAdapter,
|
|
50
|
+
AgentAdapterContext,
|
|
51
|
+
AgentAdapterRegistry,
|
|
52
|
+
normalize_agent_ref,
|
|
53
|
+
coerce_agent_result,
|
|
54
|
+
)
|
|
55
|
+
from .distributed import (
|
|
56
|
+
WorkerRegistration,
|
|
57
|
+
WorkerRecord,
|
|
58
|
+
WorkerFilter,
|
|
59
|
+
WorkItem,
|
|
60
|
+
RegistrationBackend,
|
|
61
|
+
WorkBackend,
|
|
62
|
+
WorkPool,
|
|
63
|
+
MemoryRegistrationBackend,
|
|
64
|
+
MemoryWorkBackend,
|
|
65
|
+
SQLiteRegistrationBackend,
|
|
66
|
+
SQLiteWorkBackend,
|
|
67
|
+
create_registration_backend,
|
|
68
|
+
create_work_backend,
|
|
69
|
+
)
|
|
70
|
+
from .distributed_hooks import DistributedWorkerHooks
|
|
71
|
+
|
|
72
|
+
__all__ = [
|
|
73
|
+
"__version__",
|
|
74
|
+
"FlatMachine",
|
|
75
|
+
"MachineHooks",
|
|
76
|
+
"LoggingHooks",
|
|
77
|
+
"MetricsHooks",
|
|
78
|
+
"CompositeHooks",
|
|
79
|
+
"WebhookHooks",
|
|
80
|
+
"ExpressionEngine",
|
|
81
|
+
"get_expression_engine",
|
|
82
|
+
"ExecutionType",
|
|
83
|
+
"DefaultExecution",
|
|
84
|
+
"ParallelExecution",
|
|
85
|
+
"RetryExecution",
|
|
86
|
+
"MDAPVotingExecution",
|
|
87
|
+
"get_execution_type",
|
|
88
|
+
"validate_flatmachine_config",
|
|
89
|
+
"get_flatmachine_schema",
|
|
90
|
+
"get_asset",
|
|
91
|
+
"ValidationWarning",
|
|
92
|
+
"setup_logging",
|
|
93
|
+
"get_logger",
|
|
94
|
+
"get_meter",
|
|
95
|
+
"AgentMonitor",
|
|
96
|
+
"track_operation",
|
|
97
|
+
"ResultBackend",
|
|
98
|
+
"InMemoryResultBackend",
|
|
99
|
+
"LaunchIntent",
|
|
100
|
+
"make_uri",
|
|
101
|
+
"parse_uri",
|
|
102
|
+
"get_default_result_backend",
|
|
103
|
+
"reset_default_result_backend",
|
|
104
|
+
"PersistenceBackend",
|
|
105
|
+
"LocalFileBackend",
|
|
106
|
+
"MemoryBackend",
|
|
107
|
+
"CheckpointManager",
|
|
108
|
+
"MachineSnapshot",
|
|
109
|
+
"ExecutionLock",
|
|
110
|
+
"LocalFileLock",
|
|
111
|
+
"NoOpLock",
|
|
112
|
+
"AgentExecutor",
|
|
113
|
+
"AgentResult",
|
|
114
|
+
"AgentRef",
|
|
115
|
+
"AgentAdapter",
|
|
116
|
+
"AgentAdapterContext",
|
|
117
|
+
"AgentAdapterRegistry",
|
|
118
|
+
"normalize_agent_ref",
|
|
119
|
+
"coerce_agent_result",
|
|
120
|
+
"WorkerRegistration",
|
|
121
|
+
"WorkerRecord",
|
|
122
|
+
"WorkerFilter",
|
|
123
|
+
"WorkItem",
|
|
124
|
+
"RegistrationBackend",
|
|
125
|
+
"WorkBackend",
|
|
126
|
+
"WorkPool",
|
|
127
|
+
"MemoryRegistrationBackend",
|
|
128
|
+
"MemoryWorkBackend",
|
|
129
|
+
"SQLiteRegistrationBackend",
|
|
130
|
+
"SQLiteWorkBackend",
|
|
131
|
+
"create_registration_backend",
|
|
132
|
+
"create_work_backend",
|
|
133
|
+
"SubprocessInvoker",
|
|
134
|
+
"launch_machine",
|
|
135
|
+
"DistributedWorkerHooks",
|
|
136
|
+
]
|
flatmachines/actions.py
ADDED
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from typing import Any, Dict, Optional, TYPE_CHECKING
|
|
3
|
+
import logging
|
|
4
|
+
import os
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from .flatmachine import FlatMachine
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
class Action(ABC):
|
|
12
|
+
"""
|
|
13
|
+
Base class for state actions (when state has 'action:' key).
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
@abstractmethod
|
|
17
|
+
async def execute(
|
|
18
|
+
self,
|
|
19
|
+
action_name: str,
|
|
20
|
+
context: Dict[str, Any],
|
|
21
|
+
config: Dict[str, Any]
|
|
22
|
+
) -> Dict[str, Any]:
|
|
23
|
+
"""
|
|
24
|
+
Execute the action.
|
|
25
|
+
Returns modified context.
|
|
26
|
+
"""
|
|
27
|
+
pass
|
|
28
|
+
|
|
29
|
+
class HookAction(Action):
|
|
30
|
+
"""
|
|
31
|
+
Default action: delegates to machine hooks (on_action).
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(self, hooks):
|
|
35
|
+
self.hooks = hooks
|
|
36
|
+
|
|
37
|
+
async def execute(
|
|
38
|
+
self,
|
|
39
|
+
action_name: str,
|
|
40
|
+
context: Dict[str, Any],
|
|
41
|
+
config: Dict[str, Any]
|
|
42
|
+
) -> Dict[str, Any]:
|
|
43
|
+
import asyncio
|
|
44
|
+
result = self.hooks.on_action(action_name, context)
|
|
45
|
+
if asyncio.iscoroutine(result):
|
|
46
|
+
return await result
|
|
47
|
+
return result
|
|
48
|
+
|
|
49
|
+
# -------------------------------------------------------------------------
|
|
50
|
+
# Machine Invokers (Graph Execution)
|
|
51
|
+
# -------------------------------------------------------------------------
|
|
52
|
+
|
|
53
|
+
class MachineInvoker(ABC):
|
|
54
|
+
"""
|
|
55
|
+
Interface for invoking other machines (graph execution).
|
|
56
|
+
|
|
57
|
+
See flatagents-runtime.d.ts for canonical interface definition.
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
@abstractmethod
|
|
61
|
+
async def invoke(
|
|
62
|
+
self,
|
|
63
|
+
caller_machine: 'FlatMachine',
|
|
64
|
+
target_config: Dict[str, Any],
|
|
65
|
+
input_data: Dict[str, Any],
|
|
66
|
+
execution_id: Optional[str] = None
|
|
67
|
+
) -> Dict[str, Any]:
|
|
68
|
+
"""
|
|
69
|
+
Invoke another machine and wait for result.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
caller_machine: The machine initiating the call
|
|
73
|
+
target_config: Config dict for the target machine
|
|
74
|
+
input_data: Input to pass to the target machine
|
|
75
|
+
execution_id: Optional predetermined ID (for resume support)
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
The target machine's output
|
|
79
|
+
"""
|
|
80
|
+
pass
|
|
81
|
+
|
|
82
|
+
@abstractmethod
|
|
83
|
+
async def launch(
|
|
84
|
+
self,
|
|
85
|
+
caller_machine: 'FlatMachine',
|
|
86
|
+
target_config: Dict[str, Any],
|
|
87
|
+
input_data: Dict[str, Any],
|
|
88
|
+
execution_id: str
|
|
89
|
+
) -> None:
|
|
90
|
+
"""
|
|
91
|
+
Launch a machine fire-and-forget style.
|
|
92
|
+
|
|
93
|
+
The launched machine runs independently. Results are written
|
|
94
|
+
to the result backend using the execution_id.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
caller_machine: The machine initiating the launch
|
|
98
|
+
target_config: Config dict for the target machine
|
|
99
|
+
input_data: Input to pass to the target machine
|
|
100
|
+
execution_id: The predetermined execution ID for the launched machine
|
|
101
|
+
"""
|
|
102
|
+
pass
|
|
103
|
+
|
|
104
|
+
class InlineInvoker(MachineInvoker):
|
|
105
|
+
"""
|
|
106
|
+
Default Invoker for local execution.
|
|
107
|
+
|
|
108
|
+
- invoke(): Runs target machine in same event loop, awaits result
|
|
109
|
+
- launch(): Creates background task, returns immediately
|
|
110
|
+
|
|
111
|
+
Both share the same persistence/lock backends as the caller.
|
|
112
|
+
"""
|
|
113
|
+
|
|
114
|
+
def __init__(self):
|
|
115
|
+
# Track background tasks for cleanup
|
|
116
|
+
self._background_tasks: set = set()
|
|
117
|
+
|
|
118
|
+
async def invoke(
|
|
119
|
+
self,
|
|
120
|
+
caller_machine: 'FlatMachine',
|
|
121
|
+
target_config: Dict[str, Any],
|
|
122
|
+
input_data: Dict[str, Any],
|
|
123
|
+
execution_id: Optional[str] = None
|
|
124
|
+
) -> Dict[str, Any]:
|
|
125
|
+
from .flatmachine import FlatMachine # lazy import to avoid cycle
|
|
126
|
+
import hashlib
|
|
127
|
+
|
|
128
|
+
target_name = target_config.get('data', {}).get('name', 'unknown')
|
|
129
|
+
|
|
130
|
+
# Generate execution_id if not provided
|
|
131
|
+
if not execution_id:
|
|
132
|
+
context_hash = hashlib.md5(str(sorted(input_data.items())).encode()).hexdigest()[:8]
|
|
133
|
+
execution_id = f"{caller_machine.execution_id}:peer:{target_name}:{context_hash}"
|
|
134
|
+
|
|
135
|
+
logger.info(f"Invoking peer machine: {target_name} (ID: {execution_id})")
|
|
136
|
+
|
|
137
|
+
target = FlatMachine(
|
|
138
|
+
config_dict=target_config,
|
|
139
|
+
persistence=caller_machine.persistence,
|
|
140
|
+
lock=caller_machine.lock,
|
|
141
|
+
result_backend=caller_machine.result_backend,
|
|
142
|
+
agent_registry=caller_machine.agent_registry,
|
|
143
|
+
_config_dir=caller_machine._config_dir,
|
|
144
|
+
_execution_id=execution_id,
|
|
145
|
+
_parent_execution_id=caller_machine.execution_id,
|
|
146
|
+
_profiles_dict=getattr(caller_machine, "_profiles_dict", None),
|
|
147
|
+
_profiles_file=getattr(caller_machine, "_profiles_file", None),
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
result = await target.execute(input=input_data, resume_from=execution_id)
|
|
151
|
+
|
|
152
|
+
# Aggregate stats back to caller
|
|
153
|
+
caller_machine.total_api_calls += target.total_api_calls
|
|
154
|
+
caller_machine.total_cost += target.total_cost
|
|
155
|
+
|
|
156
|
+
return result
|
|
157
|
+
|
|
158
|
+
async def launch(
|
|
159
|
+
self,
|
|
160
|
+
caller_machine: 'FlatMachine',
|
|
161
|
+
target_config: Dict[str, Any],
|
|
162
|
+
input_data: Dict[str, Any],
|
|
163
|
+
execution_id: str
|
|
164
|
+
) -> None:
|
|
165
|
+
import asyncio
|
|
166
|
+
from .flatmachine import FlatMachine
|
|
167
|
+
from .backends import make_uri
|
|
168
|
+
|
|
169
|
+
target_name = target_config.get('data', {}).get('name', 'unknown')
|
|
170
|
+
logger.info(f"Launching peer machine (fire-and-forget): {target_name} (ID: {execution_id})")
|
|
171
|
+
|
|
172
|
+
async def _execute_and_write():
|
|
173
|
+
target = FlatMachine(
|
|
174
|
+
config_dict=target_config,
|
|
175
|
+
persistence=caller_machine.persistence,
|
|
176
|
+
lock=caller_machine.lock,
|
|
177
|
+
result_backend=caller_machine.result_backend,
|
|
178
|
+
agent_registry=caller_machine.agent_registry,
|
|
179
|
+
_config_dir=caller_machine._config_dir,
|
|
180
|
+
_execution_id=execution_id,
|
|
181
|
+
_parent_execution_id=caller_machine.execution_id,
|
|
182
|
+
_profiles_dict=getattr(caller_machine, "_profiles_dict", None),
|
|
183
|
+
_profiles_file=getattr(caller_machine, "_profiles_file", None),
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
try:
|
|
187
|
+
result = await target.execute(input=input_data)
|
|
188
|
+
# Write result to backend so parent can read if needed
|
|
189
|
+
uri = make_uri(execution_id, "result")
|
|
190
|
+
await caller_machine.result_backend.write(uri, result)
|
|
191
|
+
except Exception as e:
|
|
192
|
+
uri = make_uri(execution_id, "result")
|
|
193
|
+
await caller_machine.result_backend.write(uri, {
|
|
194
|
+
"_error": str(e),
|
|
195
|
+
"_error_type": type(e).__name__
|
|
196
|
+
})
|
|
197
|
+
raise
|
|
198
|
+
|
|
199
|
+
# Create background task
|
|
200
|
+
task = asyncio.create_task(_execute_and_write())
|
|
201
|
+
caller_machine._background_tasks.add(task)
|
|
202
|
+
task.add_done_callback(caller_machine._background_tasks.discard)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
class QueueInvoker(MachineInvoker):
|
|
206
|
+
"""
|
|
207
|
+
Invoker that enqueues launches to an external queue.
|
|
208
|
+
|
|
209
|
+
For production deployments using SQS, Cloud Tasks, etc.
|
|
210
|
+
Subclass and implement _enqueue() for your queue provider.
|
|
211
|
+
"""
|
|
212
|
+
|
|
213
|
+
async def invoke(
|
|
214
|
+
self,
|
|
215
|
+
caller_machine: 'FlatMachine',
|
|
216
|
+
target_config: Dict[str, Any],
|
|
217
|
+
input_data: Dict[str, Any],
|
|
218
|
+
execution_id: Optional[str] = None
|
|
219
|
+
) -> Dict[str, Any]:
|
|
220
|
+
# For queue-based invocation, we launch and then poll for result
|
|
221
|
+
import uuid
|
|
222
|
+
from .backends import make_uri
|
|
223
|
+
|
|
224
|
+
if not execution_id:
|
|
225
|
+
execution_id = str(uuid.uuid4())
|
|
226
|
+
|
|
227
|
+
await self.launch(caller_machine, target_config, input_data, execution_id)
|
|
228
|
+
|
|
229
|
+
# Block until result is available
|
|
230
|
+
uri = make_uri(execution_id, "result")
|
|
231
|
+
return await caller_machine.result_backend.read(uri, block=True)
|
|
232
|
+
|
|
233
|
+
async def launch(
|
|
234
|
+
self,
|
|
235
|
+
caller_machine: 'FlatMachine',
|
|
236
|
+
target_config: Dict[str, Any],
|
|
237
|
+
input_data: Dict[str, Any],
|
|
238
|
+
execution_id: str
|
|
239
|
+
) -> None:
|
|
240
|
+
await self._enqueue(execution_id, target_config, input_data)
|
|
241
|
+
|
|
242
|
+
async def _enqueue(
|
|
243
|
+
self,
|
|
244
|
+
execution_id: str,
|
|
245
|
+
config: Dict[str, Any],
|
|
246
|
+
input_data: Dict[str, Any]
|
|
247
|
+
) -> None:
|
|
248
|
+
"""Override in subclass to enqueue to your queue provider."""
|
|
249
|
+
raise NotImplementedError("Subclass must implement _enqueue()")
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
class SubprocessInvoker(MachineInvoker):
|
|
253
|
+
"""
|
|
254
|
+
Invoker that launches machines as independent subprocesses.
|
|
255
|
+
|
|
256
|
+
For local distributed execution where each worker is a separate process.
|
|
257
|
+
Used by the parallelization checker to spawn worker machines.
|
|
258
|
+
|
|
259
|
+
The subprocess runs `python -m flatmachines.run` with the machine config,
|
|
260
|
+
enabling true process isolation and independent lifecycle.
|
|
261
|
+
"""
|
|
262
|
+
|
|
263
|
+
def __init__(self,
|
|
264
|
+
machine_path: Optional[str] = None,
|
|
265
|
+
working_dir: Optional[str] = None):
|
|
266
|
+
"""
|
|
267
|
+
Args:
|
|
268
|
+
machine_path: Base path for resolving machine configs
|
|
269
|
+
working_dir: Working directory for spawned processes
|
|
270
|
+
"""
|
|
271
|
+
self.machine_path = machine_path
|
|
272
|
+
self.working_dir = working_dir
|
|
273
|
+
|
|
274
|
+
async def invoke(
|
|
275
|
+
self,
|
|
276
|
+
caller_machine: 'FlatMachine',
|
|
277
|
+
target_config: Dict[str, Any],
|
|
278
|
+
input_data: Dict[str, Any],
|
|
279
|
+
execution_id: Optional[str] = None
|
|
280
|
+
) -> Dict[str, Any]:
|
|
281
|
+
"""Launch subprocess and poll for result."""
|
|
282
|
+
import uuid
|
|
283
|
+
from .backends import make_uri
|
|
284
|
+
|
|
285
|
+
if not execution_id:
|
|
286
|
+
execution_id = str(uuid.uuid4())
|
|
287
|
+
|
|
288
|
+
await self.launch(caller_machine, target_config, input_data, execution_id)
|
|
289
|
+
|
|
290
|
+
# Block until result is available
|
|
291
|
+
uri = make_uri(execution_id, "result")
|
|
292
|
+
return await caller_machine.result_backend.read(uri, block=True)
|
|
293
|
+
|
|
294
|
+
async def launch(
|
|
295
|
+
self,
|
|
296
|
+
caller_machine: 'FlatMachine',
|
|
297
|
+
target_config: Dict[str, Any],
|
|
298
|
+
input_data: Dict[str, Any],
|
|
299
|
+
execution_id: str
|
|
300
|
+
) -> None:
|
|
301
|
+
"""Launch machine as independent subprocess (fire-and-forget)."""
|
|
302
|
+
import subprocess
|
|
303
|
+
import sys
|
|
304
|
+
import json
|
|
305
|
+
import tempfile
|
|
306
|
+
import os
|
|
307
|
+
|
|
308
|
+
target_name = target_config.get('data', {}).get('name', 'unknown')
|
|
309
|
+
logger.info(f"Launching subprocess: {target_name} (ID: {execution_id})")
|
|
310
|
+
|
|
311
|
+
# Write config to temp file for subprocess to read
|
|
312
|
+
with tempfile.NamedTemporaryFile(
|
|
313
|
+
mode='w',
|
|
314
|
+
suffix='.json',
|
|
315
|
+
delete=False,
|
|
316
|
+
dir=self.working_dir
|
|
317
|
+
) as f:
|
|
318
|
+
json.dump(target_config, f)
|
|
319
|
+
config_path = f.name
|
|
320
|
+
|
|
321
|
+
# Build command
|
|
322
|
+
cmd = [
|
|
323
|
+
sys.executable, "-m", "flatmachines.run",
|
|
324
|
+
"--config", config_path,
|
|
325
|
+
"--input", json.dumps(input_data),
|
|
326
|
+
"--execution-id", execution_id,
|
|
327
|
+
]
|
|
328
|
+
|
|
329
|
+
# Add parent execution ID for lineage tracking
|
|
330
|
+
if caller_machine.execution_id:
|
|
331
|
+
cmd.extend(["--parent-id", caller_machine.execution_id])
|
|
332
|
+
|
|
333
|
+
# Spawn detached process
|
|
334
|
+
cwd = self.working_dir or caller_machine._config_dir
|
|
335
|
+
|
|
336
|
+
# Use Popen for fire-and-forget
|
|
337
|
+
subprocess.Popen(
|
|
338
|
+
cmd,
|
|
339
|
+
cwd=cwd,
|
|
340
|
+
stdout=subprocess.DEVNULL,
|
|
341
|
+
stderr=subprocess.DEVNULL,
|
|
342
|
+
start_new_session=True # Detach from parent process group
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
logger.debug(f"Subprocess launched: {' '.join(cmd)}")
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def launch_machine(
|
|
349
|
+
machine_config: str,
|
|
350
|
+
input_data: Dict[str, Any],
|
|
351
|
+
execution_id: Optional[str] = None,
|
|
352
|
+
working_dir: Optional[str] = None,
|
|
353
|
+
parent_id: Optional[str] = None
|
|
354
|
+
) -> str:
|
|
355
|
+
"""
|
|
356
|
+
Fire-and-forget machine execution via subprocess.
|
|
357
|
+
|
|
358
|
+
Standalone utility function for launching machines without an existing
|
|
359
|
+
FlatMachine context. Useful for trigger scripts and manual invocation.
|
|
360
|
+
|
|
361
|
+
Args:
|
|
362
|
+
machine_config: Path to machine YAML file
|
|
363
|
+
input_data: Input dictionary for the machine
|
|
364
|
+
execution_id: Optional predetermined execution ID
|
|
365
|
+
working_dir: Working directory for the subprocess
|
|
366
|
+
parent_id: Optional parent execution ID for lineage
|
|
367
|
+
|
|
368
|
+
Returns:
|
|
369
|
+
The execution ID of the launched machine
|
|
370
|
+
|
|
371
|
+
Example:
|
|
372
|
+
# From a trigger script
|
|
373
|
+
exec_id = launch_machine(
|
|
374
|
+
"job_worker.yml",
|
|
375
|
+
{"pool_id": "paper_analysis"},
|
|
376
|
+
working_dir="/path/to/project"
|
|
377
|
+
)
|
|
378
|
+
"""
|
|
379
|
+
import subprocess
|
|
380
|
+
import sys
|
|
381
|
+
import json
|
|
382
|
+
import uuid
|
|
383
|
+
|
|
384
|
+
if not execution_id:
|
|
385
|
+
execution_id = str(uuid.uuid4())
|
|
386
|
+
|
|
387
|
+
cmd = [
|
|
388
|
+
sys.executable, "-m", "flatmachines.run",
|
|
389
|
+
"--config", machine_config,
|
|
390
|
+
"--input", json.dumps(input_data),
|
|
391
|
+
"--execution-id", execution_id,
|
|
392
|
+
]
|
|
393
|
+
|
|
394
|
+
if parent_id:
|
|
395
|
+
cmd.extend(["--parent-id", parent_id])
|
|
396
|
+
|
|
397
|
+
subprocess.Popen(
|
|
398
|
+
cmd,
|
|
399
|
+
cwd=working_dir,
|
|
400
|
+
env=os.environ.copy(), # Pass parent environment (includes PYTHONPATH, venv)
|
|
401
|
+
stdout=subprocess.DEVNULL,
|
|
402
|
+
stderr=subprocess.DEVNULL,
|
|
403
|
+
start_new_session=True
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
logger.info(f"Launched machine subprocess: {machine_config} (ID: {execution_id})")
|
|
407
|
+
return execution_id
|
|
408
|
+
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""Adapter registry helpers for FlatMachines."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
from ..agents import AgentAdapterRegistry
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def register_builtin_adapters(registry: AgentAdapterRegistry) -> None:
|
|
11
|
+
"""Register built-in adapters if their dependencies are installed."""
|
|
12
|
+
try:
|
|
13
|
+
from .flatagent import FlatAgentAdapter
|
|
14
|
+
|
|
15
|
+
registry.register(FlatAgentAdapter())
|
|
16
|
+
except ImportError:
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
try:
|
|
20
|
+
from .smolagents import SmolagentsAdapter
|
|
21
|
+
|
|
22
|
+
registry.register(SmolagentsAdapter())
|
|
23
|
+
except ImportError:
|
|
24
|
+
pass
|
|
25
|
+
|
|
26
|
+
try:
|
|
27
|
+
from .pi_agent_bridge import PiAgentBridgeAdapter
|
|
28
|
+
|
|
29
|
+
registry.register(PiAgentBridgeAdapter())
|
|
30
|
+
except ImportError:
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def create_registry(with_builtins: bool = True) -> AgentAdapterRegistry:
|
|
35
|
+
registry = AgentAdapterRegistry()
|
|
36
|
+
if with_builtins:
|
|
37
|
+
register_builtin_adapters(registry)
|
|
38
|
+
return registry
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""FlatAgent adapter for FlatMachines."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Dict, Optional
|
|
6
|
+
|
|
7
|
+
from ..agents import AgentAdapter, AgentAdapterContext, AgentExecutor, AgentRef, AgentResult
|
|
8
|
+
|
|
9
|
+
try:
|
|
10
|
+
from flatagents.flatagent import FlatAgent
|
|
11
|
+
from flatagents.profiles import (
|
|
12
|
+
discover_profiles_file,
|
|
13
|
+
load_profiles_from_file,
|
|
14
|
+
resolve_profiles_with_fallback,
|
|
15
|
+
)
|
|
16
|
+
except ImportError as exc: # pragma: no cover - optional dependency
|
|
17
|
+
raise ImportError("flatagents is required for FlatAgentAdapter") from exc
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class FlatAgentExecutor(AgentExecutor):
|
|
21
|
+
def __init__(self, agent: FlatAgent):
|
|
22
|
+
self._agent = agent
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
def metadata(self) -> Dict[str, Any]:
|
|
26
|
+
return getattr(self._agent, "metadata", {})
|
|
27
|
+
|
|
28
|
+
async def execute(
|
|
29
|
+
self,
|
|
30
|
+
input_data: Dict[str, Any],
|
|
31
|
+
context: Optional[Dict[str, Any]] = None,
|
|
32
|
+
) -> AgentResult:
|
|
33
|
+
pre_calls = self._agent.total_api_calls
|
|
34
|
+
pre_cost = self._agent.total_cost
|
|
35
|
+
|
|
36
|
+
result = await self._agent.call(**input_data)
|
|
37
|
+
|
|
38
|
+
delta_calls = self._agent.total_api_calls - pre_calls
|
|
39
|
+
delta_cost = self._agent.total_cost - pre_cost
|
|
40
|
+
|
|
41
|
+
return AgentResult(
|
|
42
|
+
output=result.output,
|
|
43
|
+
content=result.content,
|
|
44
|
+
raw=result,
|
|
45
|
+
usage={"api_calls": delta_calls},
|
|
46
|
+
cost=delta_cost,
|
|
47
|
+
metadata=getattr(self._agent, "metadata", None),
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class FlatAgentAdapter(AgentAdapter):
|
|
52
|
+
type_name = "flatagent"
|
|
53
|
+
|
|
54
|
+
def create_executor(
|
|
55
|
+
self,
|
|
56
|
+
*,
|
|
57
|
+
agent_name: str,
|
|
58
|
+
agent_ref: AgentRef,
|
|
59
|
+
context: AgentAdapterContext,
|
|
60
|
+
) -> AgentExecutor:
|
|
61
|
+
profiles_file = discover_profiles_file(context.config_dir, context.profiles_file)
|
|
62
|
+
own_profiles = load_profiles_from_file(profiles_file) if profiles_file else None
|
|
63
|
+
profiles_dict = resolve_profiles_with_fallback(own_profiles, context.profiles_dict)
|
|
64
|
+
|
|
65
|
+
if agent_ref.ref:
|
|
66
|
+
return FlatAgentExecutor(
|
|
67
|
+
FlatAgent(
|
|
68
|
+
config_file=self._resolve_ref(agent_ref.ref, context),
|
|
69
|
+
profiles_dict=profiles_dict,
|
|
70
|
+
)
|
|
71
|
+
)
|
|
72
|
+
if agent_ref.config:
|
|
73
|
+
return FlatAgentExecutor(
|
|
74
|
+
FlatAgent(
|
|
75
|
+
config_dict=agent_ref.config,
|
|
76
|
+
profiles_dict=profiles_dict,
|
|
77
|
+
)
|
|
78
|
+
)
|
|
79
|
+
raise ValueError(f"FlatAgent reference missing ref/config for agent '{agent_name}'")
|
|
80
|
+
|
|
81
|
+
def _resolve_ref(self, ref: str, context: AgentAdapterContext) -> str:
|
|
82
|
+
import os
|
|
83
|
+
|
|
84
|
+
if os.path.isabs(ref):
|
|
85
|
+
return ref
|
|
86
|
+
return os.path.join(context.config_dir, ref)
|