flatagents 0.4.1__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.
- flatagents/__init__.py +136 -0
- flatagents/actions.py +239 -0
- flatagents/assets/__init__.py +0 -0
- flatagents/assets/flatagent.d.ts +189 -0
- flatagents/assets/flatagent.schema.json +210 -0
- flatagents/assets/flatagent.slim.d.ts +52 -0
- flatagents/assets/flatmachine.d.ts +363 -0
- flatagents/assets/flatmachine.schema.json +515 -0
- flatagents/assets/flatmachine.slim.d.ts +94 -0
- flatagents/backends.py +222 -0
- flatagents/baseagent.py +814 -0
- flatagents/execution.py +462 -0
- flatagents/expressions/__init__.py +60 -0
- flatagents/expressions/cel.py +101 -0
- flatagents/expressions/simple.py +166 -0
- flatagents/flatagent.py +735 -0
- flatagents/flatmachine.py +1176 -0
- flatagents/gcp/__init__.py +25 -0
- flatagents/gcp/firestore.py +227 -0
- flatagents/hooks.py +380 -0
- flatagents/locking.py +69 -0
- flatagents/monitoring.py +373 -0
- flatagents/persistence.py +200 -0
- flatagents/utils.py +46 -0
- flatagents/validation.py +141 -0
- flatagents-0.4.1.dist-info/METADATA +310 -0
- flatagents-0.4.1.dist-info/RECORD +28 -0
- flatagents-0.4.1.dist-info/WHEEL +4 -0
flatagents/backends.py
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Result backends for FlatMachine inter-machine communication.
|
|
3
|
+
|
|
4
|
+
Result backends handle the storage and retrieval of machine execution results,
|
|
5
|
+
enabling machines to read outputs from peer machines they launched.
|
|
6
|
+
|
|
7
|
+
URI Scheme: flatagents://{execution_id}/[checkpoint|result]
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import asyncio
|
|
11
|
+
import logging
|
|
12
|
+
from abc import ABC, abstractmethod
|
|
13
|
+
from dataclasses import dataclass, asdict
|
|
14
|
+
from typing import Any, Dict, Optional, Protocol, runtime_checkable
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def make_uri(execution_id: str, path: str = "result") -> str:
|
|
20
|
+
"""Generate a FlatAgents URI for a given execution and path.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
execution_id: Unique execution identifier
|
|
24
|
+
path: URI path component (default: "result")
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
URI string in format flatagents://{execution_id}/{path}
|
|
28
|
+
"""
|
|
29
|
+
return f"flatagents://{execution_id}/{path}"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def parse_uri(uri: str) -> tuple[str, str]:
|
|
33
|
+
"""Parse a FlatAgents URI into execution_id and path.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
uri: URI in format flatagents://{execution_id}/{path}
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
Tuple of (execution_id, path)
|
|
40
|
+
|
|
41
|
+
Raises:
|
|
42
|
+
ValueError: If URI format is invalid
|
|
43
|
+
"""
|
|
44
|
+
if not uri.startswith("flatagents://"):
|
|
45
|
+
raise ValueError(f"Invalid FlatAgents URI: {uri}")
|
|
46
|
+
|
|
47
|
+
rest = uri[len("flatagents://"):]
|
|
48
|
+
parts = rest.split("/", 1)
|
|
49
|
+
|
|
50
|
+
if len(parts) == 1:
|
|
51
|
+
return parts[0], "result"
|
|
52
|
+
return parts[0], parts[1]
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass
|
|
56
|
+
class LaunchIntent:
|
|
57
|
+
"""
|
|
58
|
+
Launch intent for outbox pattern.
|
|
59
|
+
Recorded in checkpoint before launching to ensure exactly-once semantics.
|
|
60
|
+
"""
|
|
61
|
+
execution_id: str
|
|
62
|
+
machine: str
|
|
63
|
+
input: Dict[str, Any]
|
|
64
|
+
launched: bool = False
|
|
65
|
+
|
|
66
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
67
|
+
return asdict(self)
|
|
68
|
+
|
|
69
|
+
@classmethod
|
|
70
|
+
def from_dict(cls, data: Dict[str, Any]) -> "LaunchIntent":
|
|
71
|
+
return cls(**data)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@runtime_checkable
|
|
75
|
+
class ResultBackend(Protocol):
|
|
76
|
+
"""
|
|
77
|
+
Protocol for result backends.
|
|
78
|
+
|
|
79
|
+
Result backends store and retrieve machine execution results,
|
|
80
|
+
enabling inter-machine communication.
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
async def write(self, uri: str, data: Any) -> None:
|
|
84
|
+
"""Write data to a URI.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
uri: FlatAgents URI (flatagents://{execution_id}/{path})
|
|
88
|
+
data: Data to store (will be serialized)
|
|
89
|
+
"""
|
|
90
|
+
...
|
|
91
|
+
|
|
92
|
+
async def read(self, uri: str, block: bool = True, timeout: Optional[float] = None) -> Any:
|
|
93
|
+
"""Read data from a URI.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
uri: FlatAgents URI
|
|
97
|
+
block: If True, wait for data to be available
|
|
98
|
+
timeout: Maximum seconds to wait (None = forever, only used if block=True)
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
The stored data, or None if not found and block=False
|
|
102
|
+
|
|
103
|
+
Raises:
|
|
104
|
+
TimeoutError: If timeout expires while waiting
|
|
105
|
+
"""
|
|
106
|
+
...
|
|
107
|
+
|
|
108
|
+
async def exists(self, uri: str) -> bool:
|
|
109
|
+
"""Check if data exists at a URI.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
uri: FlatAgents URI
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
True if data exists, False otherwise
|
|
116
|
+
"""
|
|
117
|
+
...
|
|
118
|
+
|
|
119
|
+
async def delete(self, uri: str) -> None:
|
|
120
|
+
"""Delete data at a URI.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
uri: FlatAgents URI
|
|
124
|
+
"""
|
|
125
|
+
...
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class InMemoryResultBackend:
|
|
129
|
+
"""
|
|
130
|
+
In-memory result backend for local execution.
|
|
131
|
+
|
|
132
|
+
Stores results in memory with asyncio Event-based blocking reads.
|
|
133
|
+
Suitable for single-process execution where machines run in the same process.
|
|
134
|
+
"""
|
|
135
|
+
|
|
136
|
+
def __init__(self):
|
|
137
|
+
self._store: Dict[str, Any] = {}
|
|
138
|
+
self._events: Dict[str, asyncio.Event] = {}
|
|
139
|
+
self._lock = asyncio.Lock()
|
|
140
|
+
|
|
141
|
+
def _get_key(self, uri: str) -> str:
|
|
142
|
+
"""Convert URI to storage key."""
|
|
143
|
+
execution_id, path = parse_uri(uri)
|
|
144
|
+
return f"{execution_id}/{path}"
|
|
145
|
+
|
|
146
|
+
def _get_event(self, key: str) -> asyncio.Event:
|
|
147
|
+
"""Get or create an event for a key."""
|
|
148
|
+
if key not in self._events:
|
|
149
|
+
self._events[key] = asyncio.Event()
|
|
150
|
+
return self._events[key]
|
|
151
|
+
|
|
152
|
+
async def write(self, uri: str, data: Any) -> None:
|
|
153
|
+
"""Write data to a URI."""
|
|
154
|
+
key = self._get_key(uri)
|
|
155
|
+
async with self._lock:
|
|
156
|
+
self._store[key] = data
|
|
157
|
+
event = self._get_event(key)
|
|
158
|
+
event.set()
|
|
159
|
+
logger.debug(f"ResultBackend: wrote to {uri}")
|
|
160
|
+
|
|
161
|
+
async def read(self, uri: str, block: bool = True, timeout: Optional[float] = None) -> Any:
|
|
162
|
+
"""Read data from a URI."""
|
|
163
|
+
key = self._get_key(uri)
|
|
164
|
+
|
|
165
|
+
if not block:
|
|
166
|
+
return self._store.get(key)
|
|
167
|
+
|
|
168
|
+
event = self._get_event(key)
|
|
169
|
+
|
|
170
|
+
# Check if already available
|
|
171
|
+
if key in self._store:
|
|
172
|
+
return self._store[key]
|
|
173
|
+
|
|
174
|
+
# Wait for data
|
|
175
|
+
try:
|
|
176
|
+
await asyncio.wait_for(event.wait(), timeout=timeout)
|
|
177
|
+
except asyncio.TimeoutError:
|
|
178
|
+
raise TimeoutError(f"Timeout waiting for result at {uri}")
|
|
179
|
+
|
|
180
|
+
return self._store.get(key)
|
|
181
|
+
|
|
182
|
+
async def exists(self, uri: str) -> bool:
|
|
183
|
+
"""Check if data exists at a URI."""
|
|
184
|
+
key = self._get_key(uri)
|
|
185
|
+
return key in self._store
|
|
186
|
+
|
|
187
|
+
async def delete(self, uri: str) -> None:
|
|
188
|
+
"""Delete data at a URI."""
|
|
189
|
+
key = self._get_key(uri)
|
|
190
|
+
async with self._lock:
|
|
191
|
+
self._store.pop(key, None)
|
|
192
|
+
if key in self._events:
|
|
193
|
+
del self._events[key]
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
# Singleton for shared in-memory backend
|
|
197
|
+
_default_backend: Optional[InMemoryResultBackend] = None
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def get_default_result_backend() -> InMemoryResultBackend:
|
|
201
|
+
"""Get the default shared in-memory result backend."""
|
|
202
|
+
global _default_backend
|
|
203
|
+
if _default_backend is None:
|
|
204
|
+
_default_backend = InMemoryResultBackend()
|
|
205
|
+
return _default_backend
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def reset_default_result_backend() -> None:
|
|
209
|
+
"""Reset the default result backend (for testing)."""
|
|
210
|
+
global _default_backend
|
|
211
|
+
_default_backend = None
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
__all__ = [
|
|
215
|
+
"ResultBackend",
|
|
216
|
+
"InMemoryResultBackend",
|
|
217
|
+
"LaunchIntent",
|
|
218
|
+
"make_uri",
|
|
219
|
+
"parse_uri",
|
|
220
|
+
"get_default_result_backend",
|
|
221
|
+
"reset_default_result_backend",
|
|
222
|
+
]
|