march-agent 0.1.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.
- march_agent/__init__.py +52 -0
- march_agent/agent.py +341 -0
- march_agent/agent_state_client.py +149 -0
- march_agent/app.py +416 -0
- march_agent/artifact.py +58 -0
- march_agent/checkpoint_client.py +169 -0
- march_agent/checkpointer.py +16 -0
- march_agent/cli.py +139 -0
- march_agent/conversation.py +103 -0
- march_agent/conversation_client.py +86 -0
- march_agent/conversation_message.py +48 -0
- march_agent/exceptions.py +36 -0
- march_agent/extensions/__init__.py +1 -0
- march_agent/extensions/langgraph.py +526 -0
- march_agent/extensions/pydantic_ai.py +180 -0
- march_agent/gateway_client.py +506 -0
- march_agent/gateway_pb2.py +73 -0
- march_agent/gateway_pb2_grpc.py +101 -0
- march_agent/heartbeat.py +84 -0
- march_agent/memory.py +73 -0
- march_agent/memory_client.py +155 -0
- march_agent/message.py +80 -0
- march_agent/streamer.py +220 -0
- march_agent-0.1.1.dist-info/METADATA +503 -0
- march_agent-0.1.1.dist-info/RECORD +29 -0
- march_agent-0.1.1.dist-info/WHEEL +5 -0
- march_agent-0.1.1.dist-info/entry_points.txt +2 -0
- march_agent-0.1.1.dist-info/licenses/LICENSE +21 -0
- march_agent-0.1.1.dist-info/top_level.txt +1 -0
march_agent/__init__.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""March AI Agent SDK - Synchronous Python framework for building AI agents."""
|
|
2
|
+
|
|
3
|
+
__version__ = "0.3.0"
|
|
4
|
+
|
|
5
|
+
from .app import MarchAgentApp
|
|
6
|
+
from .agent import Agent
|
|
7
|
+
from .message import Message
|
|
8
|
+
from .conversation import Conversation
|
|
9
|
+
from .conversation_client import ConversationClient
|
|
10
|
+
from .conversation_message import ConversationMessage
|
|
11
|
+
from .checkpoint_client import CheckpointClient
|
|
12
|
+
from .memory import Memory
|
|
13
|
+
from .memory_client import (
|
|
14
|
+
MemoryClient,
|
|
15
|
+
MemoryMessage,
|
|
16
|
+
MemorySearchResult,
|
|
17
|
+
UserSummary,
|
|
18
|
+
)
|
|
19
|
+
from .streamer import Streamer
|
|
20
|
+
from .artifact import Artifact, ArtifactType
|
|
21
|
+
from .exceptions import (
|
|
22
|
+
MarchAgentError,
|
|
23
|
+
RegistrationError,
|
|
24
|
+
KafkaError,
|
|
25
|
+
ConfigurationError,
|
|
26
|
+
APIException,
|
|
27
|
+
HeartbeatError,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
__all__ = [
|
|
31
|
+
"MarchAgentApp",
|
|
32
|
+
"Agent",
|
|
33
|
+
"Message",
|
|
34
|
+
"Conversation",
|
|
35
|
+
"ConversationClient",
|
|
36
|
+
"ConversationMessage",
|
|
37
|
+
"CheckpointClient",
|
|
38
|
+
"Memory",
|
|
39
|
+
"MemoryClient",
|
|
40
|
+
"MemoryMessage",
|
|
41
|
+
"MemorySearchResult",
|
|
42
|
+
"UserSummary",
|
|
43
|
+
"Streamer",
|
|
44
|
+
"Artifact",
|
|
45
|
+
"ArtifactType",
|
|
46
|
+
"MarchAgentError",
|
|
47
|
+
"RegistrationError",
|
|
48
|
+
"KafkaError",
|
|
49
|
+
"ConfigurationError",
|
|
50
|
+
"APIException",
|
|
51
|
+
"HeartbeatError",
|
|
52
|
+
]
|
march_agent/agent.py
ADDED
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
"""Core agent class for handling messages."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import inspect
|
|
5
|
+
import logging
|
|
6
|
+
from typing import Callable, Optional, Dict, Any, List, Tuple, Set
|
|
7
|
+
|
|
8
|
+
from .gateway_client import GatewayClient, KafkaMessage
|
|
9
|
+
from .heartbeat import HeartbeatManager
|
|
10
|
+
from .message import Message
|
|
11
|
+
from .conversation_client import ConversationClient
|
|
12
|
+
from .memory_client import MemoryClient
|
|
13
|
+
from .streamer import Streamer
|
|
14
|
+
from .exceptions import ConfigurationError
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class SenderFilter:
|
|
20
|
+
"""Filter for matching message senders."""
|
|
21
|
+
|
|
22
|
+
def __init__(self, senders: Optional[List[str]] = None):
|
|
23
|
+
self.include: Set[str] = set()
|
|
24
|
+
self.exclude: Set[str] = set()
|
|
25
|
+
self.match_all = senders is None or len(senders) == 0
|
|
26
|
+
|
|
27
|
+
if senders:
|
|
28
|
+
for s in senders:
|
|
29
|
+
if s.startswith("~"):
|
|
30
|
+
self.exclude.add(s[1:])
|
|
31
|
+
else:
|
|
32
|
+
self.include.add(s)
|
|
33
|
+
|
|
34
|
+
def matches(self, sender: str) -> bool:
|
|
35
|
+
"""Check if sender matches this filter."""
|
|
36
|
+
if self.match_all:
|
|
37
|
+
return True
|
|
38
|
+
if sender in self.exclude:
|
|
39
|
+
return False
|
|
40
|
+
if self.include and sender not in self.include:
|
|
41
|
+
return False
|
|
42
|
+
return True
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class Agent:
|
|
46
|
+
"""Core agent class that handles messaging via the Agent Gateway."""
|
|
47
|
+
|
|
48
|
+
def __init__(
|
|
49
|
+
self,
|
|
50
|
+
name: str,
|
|
51
|
+
gateway_client: GatewayClient,
|
|
52
|
+
agent_data: Dict[str, Any],
|
|
53
|
+
heartbeat_interval: int = 60,
|
|
54
|
+
conversation_client: Optional[ConversationClient] = None,
|
|
55
|
+
memory_client: Optional[MemoryClient] = None,
|
|
56
|
+
error_message_template: str = "I encountered an error while processing your message. Please try again or contact support if the issue persists.",
|
|
57
|
+
):
|
|
58
|
+
self.name = name
|
|
59
|
+
self.gateway_client = gateway_client
|
|
60
|
+
self.agent_data = agent_data
|
|
61
|
+
self.conversation_client = conversation_client
|
|
62
|
+
self.memory_client = memory_client
|
|
63
|
+
self.heartbeat_interval = heartbeat_interval
|
|
64
|
+
|
|
65
|
+
# Error handling configuration
|
|
66
|
+
self.error_message_template: str = error_message_template
|
|
67
|
+
self.send_error_responses: bool = True
|
|
68
|
+
|
|
69
|
+
self.heartbeat_manager: Optional[HeartbeatManager] = None
|
|
70
|
+
self._message_handlers: List[Tuple[SenderFilter, Callable]] = []
|
|
71
|
+
self._running = False
|
|
72
|
+
self._initialized = False
|
|
73
|
+
|
|
74
|
+
logger.info(f"Agent '{name}' created")
|
|
75
|
+
|
|
76
|
+
def _initialize_with_gateway(self):
|
|
77
|
+
"""Initialize agent after gateway connection is established."""
|
|
78
|
+
if self._initialized:
|
|
79
|
+
return
|
|
80
|
+
|
|
81
|
+
# Start heartbeat via gateway proxy
|
|
82
|
+
self.heartbeat_manager = HeartbeatManager(
|
|
83
|
+
gateway_client=self.gateway_client,
|
|
84
|
+
agent_name=self.name,
|
|
85
|
+
interval=self.heartbeat_interval,
|
|
86
|
+
)
|
|
87
|
+
self.heartbeat_manager.start()
|
|
88
|
+
|
|
89
|
+
self._initialized = True
|
|
90
|
+
logger.info(f"Agent '{self.name}' initialized with gateway")
|
|
91
|
+
|
|
92
|
+
def on_message(self, handler_or_senders=None, *, senders: Optional[List[str]] = None):
|
|
93
|
+
"""Register message handler with optional sender filter.
|
|
94
|
+
|
|
95
|
+
Supports both:
|
|
96
|
+
@agent.on_message # catch-all
|
|
97
|
+
@agent.on_message(senders=["user"]) # filtered
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
senders: List of sender names to match. Prefix with ~ to exclude.
|
|
101
|
+
Examples: ["user"], ["agent-1", "agent-2"], ["~user"]
|
|
102
|
+
"""
|
|
103
|
+
def decorator(handler: Callable[[Message, str], None]):
|
|
104
|
+
sender_filter = SenderFilter(senders)
|
|
105
|
+
self._message_handlers.append((sender_filter, handler))
|
|
106
|
+
logger.info(f"Handler registered for agent '{self.name}' with senders={senders}")
|
|
107
|
+
return handler
|
|
108
|
+
|
|
109
|
+
# Called as @on_message (no parens) - handler_or_senders is the function
|
|
110
|
+
if callable(handler_or_senders):
|
|
111
|
+
return decorator(handler_or_senders)
|
|
112
|
+
|
|
113
|
+
# Called as @on_message(senders=[...]) - return decorator
|
|
114
|
+
return decorator
|
|
115
|
+
|
|
116
|
+
def _get_sender(self, headers: Dict[str, str]) -> str:
|
|
117
|
+
"""Determine sender from headers."""
|
|
118
|
+
return headers.get("from_", "user")
|
|
119
|
+
|
|
120
|
+
def _find_matching_handler(self, sender: str) -> Optional[Callable]:
|
|
121
|
+
"""Find first handler that matches the sender."""
|
|
122
|
+
for sender_filter, handler in self._message_handlers:
|
|
123
|
+
if sender_filter.matches(sender):
|
|
124
|
+
return handler
|
|
125
|
+
return None
|
|
126
|
+
|
|
127
|
+
def _send_error_response(
|
|
128
|
+
self,
|
|
129
|
+
message: Message,
|
|
130
|
+
error: Exception,
|
|
131
|
+
handler_name: Optional[str] = None
|
|
132
|
+
) -> None:
|
|
133
|
+
"""Send error response to user when handler fails (sync - deprecated).
|
|
134
|
+
|
|
135
|
+
Fail-safe design: wraps everything in try-except to prevent
|
|
136
|
+
cascading failures if error response itself fails.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
message: The original message that caused the error
|
|
140
|
+
error: The exception that was caught
|
|
141
|
+
handler_name: Optional name of the handler that failed
|
|
142
|
+
"""
|
|
143
|
+
if not self.send_error_responses:
|
|
144
|
+
return
|
|
145
|
+
|
|
146
|
+
try:
|
|
147
|
+
# Log detailed error for debugging with structured fields
|
|
148
|
+
handler_info = f" in handler '{handler_name}'" if handler_name else ""
|
|
149
|
+
logger.error(
|
|
150
|
+
f"Handler error{handler_info} for agent '{self.name}': {error}",
|
|
151
|
+
exc_info=True,
|
|
152
|
+
extra={
|
|
153
|
+
"agent_name": self.name,
|
|
154
|
+
"conversation_id": message.conversation_id,
|
|
155
|
+
"error_type": type(error).__name__,
|
|
156
|
+
}
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
# Create streamer with original message context
|
|
160
|
+
streamer = self.streamer(
|
|
161
|
+
original_message=message,
|
|
162
|
+
send_to="user"
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
# Send error message (stream is sync, but we can't await here)
|
|
166
|
+
streamer.stream(self.error_message_template)
|
|
167
|
+
# Note: Can't use async context manager in sync method
|
|
168
|
+
|
|
169
|
+
logger.info(
|
|
170
|
+
f"Error response sent to user for conversation {message.conversation_id}"
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
except Exception as send_error:
|
|
174
|
+
# Fail-safe: if sending error response fails, just log it
|
|
175
|
+
logger.error(
|
|
176
|
+
f"Failed to send error response: {send_error}",
|
|
177
|
+
exc_info=True,
|
|
178
|
+
extra={
|
|
179
|
+
"agent_name": self.name,
|
|
180
|
+
"conversation_id": message.conversation_id,
|
|
181
|
+
"original_error": str(error),
|
|
182
|
+
}
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
async def _send_error_response_async(
|
|
186
|
+
self,
|
|
187
|
+
message: Message,
|
|
188
|
+
error: Exception,
|
|
189
|
+
handler_name: Optional[str] = None
|
|
190
|
+
) -> None:
|
|
191
|
+
"""Send error response to user when handler fails (async).
|
|
192
|
+
|
|
193
|
+
Fail-safe design: wraps everything in try-except to prevent
|
|
194
|
+
cascading failures if error response itself fails.
|
|
195
|
+
|
|
196
|
+
Args:
|
|
197
|
+
message: The original message that caused the error
|
|
198
|
+
error: The exception that was caught
|
|
199
|
+
handler_name: Optional name of the handler that failed
|
|
200
|
+
"""
|
|
201
|
+
if not self.send_error_responses:
|
|
202
|
+
return
|
|
203
|
+
|
|
204
|
+
try:
|
|
205
|
+
# Log detailed error for debugging with structured fields
|
|
206
|
+
handler_info = f" in handler '{handler_name}'" if handler_name else ""
|
|
207
|
+
logger.error(
|
|
208
|
+
f"Handler error{handler_info} for agent '{self.name}': {error}",
|
|
209
|
+
exc_info=True,
|
|
210
|
+
extra={
|
|
211
|
+
"agent_name": self.name,
|
|
212
|
+
"conversation_id": message.conversation_id,
|
|
213
|
+
"error_type": type(error).__name__,
|
|
214
|
+
}
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
# Create streamer with original message context
|
|
218
|
+
streamer = self.streamer(
|
|
219
|
+
original_message=message,
|
|
220
|
+
send_to="user"
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
# Send error message using async context manager pattern
|
|
224
|
+
async with streamer as s:
|
|
225
|
+
s.stream(self.error_message_template)
|
|
226
|
+
|
|
227
|
+
logger.info(
|
|
228
|
+
f"Error response sent to user for conversation {message.conversation_id}"
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
except Exception as send_error:
|
|
232
|
+
# Fail-safe: if sending error response fails, just log it
|
|
233
|
+
logger.error(
|
|
234
|
+
f"Failed to send error response: {send_error}",
|
|
235
|
+
exc_info=True,
|
|
236
|
+
extra={
|
|
237
|
+
"agent_name": self.name,
|
|
238
|
+
"conversation_id": message.conversation_id,
|
|
239
|
+
"original_error": str(error),
|
|
240
|
+
}
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
async def _handle_message_async(self, kafka_msg: KafkaMessage):
|
|
244
|
+
"""Handle incoming Kafka message asynchronously."""
|
|
245
|
+
message = None
|
|
246
|
+
handler = None
|
|
247
|
+
|
|
248
|
+
try:
|
|
249
|
+
headers = kafka_msg.headers
|
|
250
|
+
body = kafka_msg.body if isinstance(kafka_msg.body, dict) else {}
|
|
251
|
+
|
|
252
|
+
sender = self._get_sender(headers)
|
|
253
|
+
handler = self._find_matching_handler(sender)
|
|
254
|
+
|
|
255
|
+
if handler is None:
|
|
256
|
+
logger.warning(f"No handler matched for sender: {sender}")
|
|
257
|
+
return
|
|
258
|
+
|
|
259
|
+
# Create message before handler execution
|
|
260
|
+
message = Message.from_kafka_message(
|
|
261
|
+
body, headers,
|
|
262
|
+
conversation_client=self.conversation_client,
|
|
263
|
+
memory_client=self.memory_client,
|
|
264
|
+
agent_name=self.name,
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
# Execute handler based on whether it's async or sync
|
|
268
|
+
if inspect.iscoroutinefunction(handler):
|
|
269
|
+
# Async handler - await it directly
|
|
270
|
+
await handler(message, sender)
|
|
271
|
+
else:
|
|
272
|
+
# Sync handler - run in thread pool for backward compatibility
|
|
273
|
+
loop = asyncio.get_event_loop()
|
|
274
|
+
await loop.run_in_executor(None, handler, message, sender)
|
|
275
|
+
|
|
276
|
+
except Exception as e:
|
|
277
|
+
# Handler execution failed - send error response to user
|
|
278
|
+
# Only send if we successfully created the message
|
|
279
|
+
if message is not None:
|
|
280
|
+
handler_name = handler.__name__ if handler and hasattr(handler, '__name__') else None
|
|
281
|
+
await self._send_error_response_async(message, e, handler_name)
|
|
282
|
+
else:
|
|
283
|
+
# Message creation failed - just log (can't respond without context)
|
|
284
|
+
logger.error(
|
|
285
|
+
f"Error processing Kafka message before handler execution: {e}",
|
|
286
|
+
exc_info=True,
|
|
287
|
+
extra={"agent_name": self.name}
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
def start_consuming(self):
|
|
291
|
+
"""Mark agent as ready to consume messages.
|
|
292
|
+
|
|
293
|
+
Note: Actual consumption is handled by the gateway client's consume loop.
|
|
294
|
+
This method just sets the agent as running and waits for shutdown.
|
|
295
|
+
"""
|
|
296
|
+
if not self._message_handlers:
|
|
297
|
+
raise ConfigurationError("No message handlers registered.")
|
|
298
|
+
|
|
299
|
+
if not self._initialized:
|
|
300
|
+
raise ConfigurationError("Agent not initialized. Call _initialize_with_gateway first.")
|
|
301
|
+
|
|
302
|
+
logger.info(f"Agent '{self.name}' ready to consume messages")
|
|
303
|
+
self._running = True
|
|
304
|
+
|
|
305
|
+
# The gateway client handles all message consumption in a single thread.
|
|
306
|
+
# We just keep this thread alive until shutdown.
|
|
307
|
+
import time
|
|
308
|
+
while self._running:
|
|
309
|
+
time.sleep(1.0)
|
|
310
|
+
|
|
311
|
+
def streamer(
|
|
312
|
+
self,
|
|
313
|
+
original_message: Message,
|
|
314
|
+
awaiting: bool = False,
|
|
315
|
+
send_to: str = "user",
|
|
316
|
+
) -> Streamer:
|
|
317
|
+
"""Create a new Streamer for streaming responses.
|
|
318
|
+
|
|
319
|
+
Args:
|
|
320
|
+
original_message: The message being responded to
|
|
321
|
+
awaiting: If True (and using context manager), sets awaiting_route on finish
|
|
322
|
+
send_to: Target for the response - "user" (default) or another agent name
|
|
323
|
+
"""
|
|
324
|
+
return Streamer(
|
|
325
|
+
agent_name=self.name,
|
|
326
|
+
original_message=original_message,
|
|
327
|
+
gateway_client=self.gateway_client,
|
|
328
|
+
conversation_client=self.conversation_client,
|
|
329
|
+
awaiting=awaiting,
|
|
330
|
+
send_to=send_to,
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
def shutdown(self):
|
|
334
|
+
"""Shutdown agent gracefully."""
|
|
335
|
+
logger.info(f"Shutting down agent '{self.name}'...")
|
|
336
|
+
self._running = False
|
|
337
|
+
|
|
338
|
+
if self.heartbeat_manager:
|
|
339
|
+
self.heartbeat_manager.stop()
|
|
340
|
+
|
|
341
|
+
logger.info(f"Agent '{self.name}' shutdown complete")
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"""HTTP client for agent state storage API."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import Dict, Any, Optional
|
|
5
|
+
import aiohttp
|
|
6
|
+
|
|
7
|
+
from .exceptions import APIException
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class AgentStateClient:
|
|
13
|
+
"""Async HTTP client for agent-state API.
|
|
14
|
+
|
|
15
|
+
This client communicates with the conversation-store's agent-state endpoints
|
|
16
|
+
to store and retrieve framework-specific state data.
|
|
17
|
+
|
|
18
|
+
Example:
|
|
19
|
+
```python
|
|
20
|
+
client = AgentStateClient("http://gateway/s/conversation-store")
|
|
21
|
+
|
|
22
|
+
# Store state
|
|
23
|
+
await client.put("conv-123", "pydantic_ai", {"messages": [...]})
|
|
24
|
+
|
|
25
|
+
# Get state
|
|
26
|
+
state = await client.get("conv-123", "pydantic_ai")
|
|
27
|
+
|
|
28
|
+
# Delete state
|
|
29
|
+
await client.delete("conv-123", "pydantic_ai")
|
|
30
|
+
```
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def __init__(self, base_url: str):
|
|
34
|
+
"""Initialize agent state client.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
base_url: Base URL for the agent-state API
|
|
38
|
+
(e.g., http://gateway/s/conversation-store)
|
|
39
|
+
"""
|
|
40
|
+
self.base_url = base_url.rstrip("/")
|
|
41
|
+
self._session: Optional[aiohttp.ClientSession] = None
|
|
42
|
+
|
|
43
|
+
async def _get_session(self) -> aiohttp.ClientSession:
|
|
44
|
+
"""Get or create aiohttp session."""
|
|
45
|
+
if self._session is None or self._session.closed:
|
|
46
|
+
timeout = aiohttp.ClientTimeout(total=30.0)
|
|
47
|
+
self._session = aiohttp.ClientSession(timeout=timeout)
|
|
48
|
+
return self._session
|
|
49
|
+
|
|
50
|
+
async def close(self):
|
|
51
|
+
"""Close the aiohttp session."""
|
|
52
|
+
if self._session and not self._session.closed:
|
|
53
|
+
await self._session.close()
|
|
54
|
+
|
|
55
|
+
async def put(
|
|
56
|
+
self,
|
|
57
|
+
conversation_id: str,
|
|
58
|
+
namespace: str,
|
|
59
|
+
state: Dict[str, Any],
|
|
60
|
+
) -> Dict[str, Any]:
|
|
61
|
+
"""Store or update agent state.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
conversation_id: Conversation identifier
|
|
65
|
+
namespace: Framework namespace (e.g., 'pydantic_ai')
|
|
66
|
+
state: State data to store (will be serialized as JSON)
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
Response with conversation_id, namespace, and created flag
|
|
70
|
+
"""
|
|
71
|
+
url = f"{self.base_url}/agent-state/{conversation_id}"
|
|
72
|
+
payload = {
|
|
73
|
+
"namespace": namespace,
|
|
74
|
+
"state": state,
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
session = await self._get_session()
|
|
78
|
+
try:
|
|
79
|
+
async with session.put(url, json=payload) as response:
|
|
80
|
+
if response.status >= 400:
|
|
81
|
+
error_text = await response.text()
|
|
82
|
+
raise APIException(
|
|
83
|
+
f"Failed to store agent state: {response.status} - {error_text}"
|
|
84
|
+
)
|
|
85
|
+
return await response.json()
|
|
86
|
+
except aiohttp.ClientError as e:
|
|
87
|
+
raise APIException(f"Failed to store agent state: {e}")
|
|
88
|
+
|
|
89
|
+
async def get(
|
|
90
|
+
self,
|
|
91
|
+
conversation_id: str,
|
|
92
|
+
namespace: str,
|
|
93
|
+
) -> Optional[Dict[str, Any]]:
|
|
94
|
+
"""Get agent state.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
conversation_id: Conversation identifier
|
|
98
|
+
namespace: Framework namespace (e.g., 'pydantic_ai')
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
AgentStateResponse dict or None if not found
|
|
102
|
+
"""
|
|
103
|
+
url = f"{self.base_url}/agent-state/{conversation_id}"
|
|
104
|
+
params = {"namespace": namespace}
|
|
105
|
+
|
|
106
|
+
session = await self._get_session()
|
|
107
|
+
try:
|
|
108
|
+
async with session.get(url, params=params) as response:
|
|
109
|
+
if response.status == 404:
|
|
110
|
+
return None
|
|
111
|
+
if response.status >= 400:
|
|
112
|
+
error_text = await response.text()
|
|
113
|
+
raise APIException(
|
|
114
|
+
f"Failed to get agent state: {response.status} - {error_text}"
|
|
115
|
+
)
|
|
116
|
+
return await response.json()
|
|
117
|
+
except aiohttp.ClientError as e:
|
|
118
|
+
raise APIException(f"Failed to get agent state: {e}")
|
|
119
|
+
|
|
120
|
+
async def delete(
|
|
121
|
+
self,
|
|
122
|
+
conversation_id: str,
|
|
123
|
+
namespace: Optional[str] = None,
|
|
124
|
+
) -> Dict[str, Any]:
|
|
125
|
+
"""Delete agent state.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
conversation_id: Conversation identifier
|
|
129
|
+
namespace: Framework namespace (optional, deletes all if not provided)
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
Response with conversation_id, namespace, and deleted count
|
|
133
|
+
"""
|
|
134
|
+
url = f"{self.base_url}/agent-state/{conversation_id}"
|
|
135
|
+
params = {}
|
|
136
|
+
if namespace:
|
|
137
|
+
params["namespace"] = namespace
|
|
138
|
+
|
|
139
|
+
session = await self._get_session()
|
|
140
|
+
try:
|
|
141
|
+
async with session.delete(url, params=params) as response:
|
|
142
|
+
if response.status >= 400:
|
|
143
|
+
error_text = await response.text()
|
|
144
|
+
raise APIException(
|
|
145
|
+
f"Failed to delete agent state: {response.status} - {error_text}"
|
|
146
|
+
)
|
|
147
|
+
return await response.json()
|
|
148
|
+
except aiohttp.ClientError as e:
|
|
149
|
+
raise APIException(f"Failed to delete agent state: {e}")
|