agnt5 0.3.0a8__cp310-abi3-manylinux_2_17_x86_64.manylinux2014_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 +119 -0
- agnt5/_compat.py +16 -0
- agnt5/_core.abi3.so +0 -0
- agnt5/_retry_utils.py +172 -0
- agnt5/_schema_utils.py +312 -0
- agnt5/_sentry.py +515 -0
- agnt5/_telemetry.py +191 -0
- agnt5/agent/__init__.py +48 -0
- agnt5/agent/context.py +458 -0
- agnt5/agent/core.py +1793 -0
- agnt5/agent/decorator.py +112 -0
- agnt5/agent/handoff.py +105 -0
- agnt5/agent/registry.py +68 -0
- agnt5/agent/result.py +39 -0
- agnt5/checkpoint.py +246 -0
- agnt5/client.py +1478 -0
- agnt5/context.py +210 -0
- agnt5/entity.py +1230 -0
- agnt5/events.py +566 -0
- agnt5/exceptions.py +102 -0
- agnt5/function.py +325 -0
- agnt5/lm.py +1033 -0
- agnt5/memory.py +521 -0
- agnt5/tool.py +657 -0
- agnt5/tracing.py +196 -0
- agnt5/types.py +110 -0
- agnt5/version.py +19 -0
- agnt5/worker.py +1982 -0
- agnt5/workflow.py +1584 -0
- agnt5-0.3.0a8.dist-info/METADATA +26 -0
- agnt5-0.3.0a8.dist-info/RECORD +32 -0
- agnt5-0.3.0a8.dist-info/WHEEL +5 -0
agnt5/agent/__init__.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Agent module - AI agents with streaming execution.
|
|
2
|
+
|
|
3
|
+
This module provides the core agent primitives for building AI-powered
|
|
4
|
+
applications with tool orchestration and multi-agent collaboration.
|
|
5
|
+
|
|
6
|
+
Example:
|
|
7
|
+
```python
|
|
8
|
+
from agnt5.agent import Agent, AgentResult, handoff
|
|
9
|
+
|
|
10
|
+
# Create an agent
|
|
11
|
+
agent = Agent(
|
|
12
|
+
name="researcher",
|
|
13
|
+
model="openai/gpt-4o",
|
|
14
|
+
instructions="You are a research assistant.",
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
# Streaming execution (recommended)
|
|
18
|
+
async for event in agent.run("Find recent AI papers"):
|
|
19
|
+
if event.event_type == EventType.LM_MESSAGE_DELTA:
|
|
20
|
+
print(event.data, end="") # data is raw content string for deltas
|
|
21
|
+
|
|
22
|
+
# Non-streaming execution
|
|
23
|
+
result = await agent.run_sync("Find recent AI papers")
|
|
24
|
+
print(result.output)
|
|
25
|
+
```
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
# Import from split modules
|
|
29
|
+
from .context import AgentContext
|
|
30
|
+
from .result import AgentResult
|
|
31
|
+
from .handoff import Handoff, handoff
|
|
32
|
+
from .registry import AgentRegistry
|
|
33
|
+
from .core import Agent
|
|
34
|
+
from .decorator import agent
|
|
35
|
+
|
|
36
|
+
__all__ = [
|
|
37
|
+
# Core classes
|
|
38
|
+
"Agent",
|
|
39
|
+
"AgentContext",
|
|
40
|
+
"AgentResult",
|
|
41
|
+
# Handoff support
|
|
42
|
+
"Handoff",
|
|
43
|
+
"handoff",
|
|
44
|
+
# Registry
|
|
45
|
+
"AgentRegistry",
|
|
46
|
+
# Decorator
|
|
47
|
+
"agent",
|
|
48
|
+
]
|
agnt5/agent/context.py
ADDED
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
"""Agent execution context with conversation state management."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import time
|
|
5
|
+
from typing import Any, Dict, List, Optional, TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from ..context import Context
|
|
8
|
+
from ..lm import Message
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
pass
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class AgentContext(Context):
|
|
17
|
+
"""
|
|
18
|
+
Context for agent execution with conversation state management.
|
|
19
|
+
|
|
20
|
+
Extends base Context with:
|
|
21
|
+
- State management via EntityStateManager
|
|
22
|
+
- Conversation history persistence
|
|
23
|
+
- Context inheritance (child agents share parent's state)
|
|
24
|
+
|
|
25
|
+
Three initialization modes:
|
|
26
|
+
1. Standalone: Creates own state manager (playground testing)
|
|
27
|
+
2. Inherit WorkflowContext: Shares parent's state manager
|
|
28
|
+
3. Inherit parent AgentContext: Shares parent's state manager
|
|
29
|
+
|
|
30
|
+
Example:
|
|
31
|
+
```python
|
|
32
|
+
# Standalone agent with conversation history
|
|
33
|
+
ctx = AgentContext(run_id="session-1", agent_name="tutor")
|
|
34
|
+
result = await agent.run_sync("Hello", context=ctx)
|
|
35
|
+
result = await agent.run_sync("Continue", context=ctx) # Remembers previous message
|
|
36
|
+
|
|
37
|
+
# Agent in workflow - shares workflow state
|
|
38
|
+
@workflow
|
|
39
|
+
async def research_workflow(ctx: WorkflowContext):
|
|
40
|
+
agent_result = await research_agent.run_sync("Find AI trends", context=ctx)
|
|
41
|
+
# Agent has access to workflow state via inherited context
|
|
42
|
+
```
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
def __init__(
|
|
46
|
+
self,
|
|
47
|
+
run_id: str,
|
|
48
|
+
agent_name: str,
|
|
49
|
+
session_id: Optional[str] = None,
|
|
50
|
+
state_manager: Optional[Any] = None,
|
|
51
|
+
parent_context: Optional[Context] = None,
|
|
52
|
+
attempt: int = 0,
|
|
53
|
+
runtime_context: Optional[Any] = None,
|
|
54
|
+
is_streaming: bool = False,
|
|
55
|
+
tenant_id: Optional[str] = None,
|
|
56
|
+
):
|
|
57
|
+
"""
|
|
58
|
+
Initialize agent context.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
run_id: Unique execution identifier
|
|
62
|
+
agent_name: Name of the agent
|
|
63
|
+
session_id: Session identifier for conversation history (default: run_id)
|
|
64
|
+
state_manager: Optional state manager (for context inheritance)
|
|
65
|
+
parent_context: Parent context to inherit state from
|
|
66
|
+
attempt: Retry attempt number
|
|
67
|
+
runtime_context: RuntimeContext for trace correlation
|
|
68
|
+
is_streaming: Whether this is a streaming request (for real-time SSE log delivery)
|
|
69
|
+
tenant_id: Tenant identifier for multi-tenant deployments
|
|
70
|
+
"""
|
|
71
|
+
# Inherit is_streaming and tenant_id from parent context if not explicitly provided
|
|
72
|
+
if parent_context and not is_streaming:
|
|
73
|
+
is_streaming = getattr(parent_context, '_is_streaming', False)
|
|
74
|
+
if parent_context and not tenant_id:
|
|
75
|
+
tenant_id = getattr(parent_context, '_tenant_id', None)
|
|
76
|
+
|
|
77
|
+
super().__init__(run_id, attempt, runtime_context, is_streaming, tenant_id)
|
|
78
|
+
|
|
79
|
+
self._agent_name = agent_name
|
|
80
|
+
self._session_id = session_id or run_id
|
|
81
|
+
self.parent_context = parent_context # Store for context chain traversal
|
|
82
|
+
|
|
83
|
+
# Determine state adapter based on parent context
|
|
84
|
+
from ..entity import EntityStateAdapter, _get_state_adapter
|
|
85
|
+
|
|
86
|
+
if state_manager:
|
|
87
|
+
# Explicit state adapter provided (parameter name kept for backward compat)
|
|
88
|
+
self._state_adapter = state_manager
|
|
89
|
+
elif parent_context:
|
|
90
|
+
# Try to inherit state adapter from parent
|
|
91
|
+
try:
|
|
92
|
+
# Check if parent is WorkflowContext or AgentContext
|
|
93
|
+
if hasattr(parent_context, '_workflow_entity'):
|
|
94
|
+
# WorkflowContext - get state adapter from worker context
|
|
95
|
+
self._state_adapter = _get_state_adapter()
|
|
96
|
+
elif hasattr(parent_context, '_state_adapter'):
|
|
97
|
+
# Parent AgentContext - share state adapter
|
|
98
|
+
self._state_adapter = parent_context._state_adapter
|
|
99
|
+
elif hasattr(parent_context, '_state_manager'):
|
|
100
|
+
# Backward compatibility: parent has old _state_manager
|
|
101
|
+
self._state_adapter = parent_context._state_manager
|
|
102
|
+
else:
|
|
103
|
+
# FunctionContext or base Context - create new state adapter
|
|
104
|
+
self._state_adapter = EntityStateAdapter()
|
|
105
|
+
except RuntimeError:
|
|
106
|
+
# _get_state_adapter() failed (not in worker context) - create standalone
|
|
107
|
+
self._state_adapter = EntityStateAdapter()
|
|
108
|
+
else:
|
|
109
|
+
# Try to get from worker context first
|
|
110
|
+
try:
|
|
111
|
+
self._state_adapter = _get_state_adapter()
|
|
112
|
+
except RuntimeError:
|
|
113
|
+
# Standalone - create new state adapter
|
|
114
|
+
self._state_adapter = EntityStateAdapter()
|
|
115
|
+
|
|
116
|
+
# Conversation key for state storage (used for in-memory state)
|
|
117
|
+
self._conversation_key = f"agent:{agent_name}:{self._session_id}:messages"
|
|
118
|
+
# Entity key for database persistence (without :messages suffix to match API expectations)
|
|
119
|
+
self._entity_key = f"agent:{agent_name}:{self._session_id}"
|
|
120
|
+
|
|
121
|
+
# Determine storage mode: "workflow" if parent is WorkflowContext, else "standalone"
|
|
122
|
+
self._storage_mode = "standalone" # Default mode
|
|
123
|
+
self._workflow_entity = None
|
|
124
|
+
|
|
125
|
+
if parent_context and hasattr(parent_context, '_workflow_entity'):
|
|
126
|
+
# Agent is running within a workflow - store conversation in workflow state
|
|
127
|
+
self._storage_mode = "workflow"
|
|
128
|
+
self._workflow_entity = parent_context._workflow_entity
|
|
129
|
+
logger.debug(
|
|
130
|
+
f"Agent '{agent_name}' using workflow storage mode "
|
|
131
|
+
f"(workflow entity: {self._workflow_entity.key})"
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
@property
|
|
135
|
+
def state(self):
|
|
136
|
+
"""
|
|
137
|
+
Get state interface for agent state management.
|
|
138
|
+
|
|
139
|
+
Note: This is a simplified in-memory state interface for agent-specific data.
|
|
140
|
+
Conversation history is managed separately via get_conversation_history() and
|
|
141
|
+
save_conversation_history() which use the Rust-backed persistence layer.
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
Dict-like object for state operations
|
|
145
|
+
|
|
146
|
+
Example:
|
|
147
|
+
# Store agent-specific data (in-memory only)
|
|
148
|
+
ctx.state["research_results"] = data
|
|
149
|
+
ctx.state["iteration_count"] = 5
|
|
150
|
+
"""
|
|
151
|
+
# Simple dict-based state for agent-specific data
|
|
152
|
+
# This is in-memory only and not persisted to platform
|
|
153
|
+
if not hasattr(self, '_agent_state'):
|
|
154
|
+
self._agent_state = {}
|
|
155
|
+
return self._agent_state
|
|
156
|
+
|
|
157
|
+
@property
|
|
158
|
+
def session_id(self) -> str:
|
|
159
|
+
"""Get session identifier for this agent context."""
|
|
160
|
+
return self._session_id
|
|
161
|
+
|
|
162
|
+
async def get_conversation_history(self) -> List[Message]:
|
|
163
|
+
"""
|
|
164
|
+
Retrieve conversation history from state, loading from database if needed.
|
|
165
|
+
|
|
166
|
+
Uses the EntityStateAdapter which delegates to Rust core for cache-first loading.
|
|
167
|
+
If running within a workflow, loads from workflow entity state instead.
|
|
168
|
+
|
|
169
|
+
Returns:
|
|
170
|
+
List of Message objects from conversation history
|
|
171
|
+
"""
|
|
172
|
+
if self._storage_mode == "workflow":
|
|
173
|
+
return await self._load_from_workflow_state()
|
|
174
|
+
else:
|
|
175
|
+
return await self._load_from_entity_storage()
|
|
176
|
+
|
|
177
|
+
async def _load_from_workflow_state(self) -> List[Message]:
|
|
178
|
+
"""Load conversation history from workflow entity state."""
|
|
179
|
+
key = f"agent.{self._agent_name}"
|
|
180
|
+
agent_data = self._workflow_entity.state.get(key, {})
|
|
181
|
+
messages_data = agent_data.get("messages", [])
|
|
182
|
+
|
|
183
|
+
# Convert dict representations back to Message objects
|
|
184
|
+
return self._convert_dicts_to_messages(messages_data)
|
|
185
|
+
|
|
186
|
+
async def _load_from_entity_storage(self) -> List[Message]:
|
|
187
|
+
"""Load conversation history from AgentSession entity (standalone mode)."""
|
|
188
|
+
entity_type = "AgentSession"
|
|
189
|
+
entity_key = self._entity_key
|
|
190
|
+
|
|
191
|
+
# Load session data via adapter (Rust handles cache + platform load)
|
|
192
|
+
# Use session scope with session_id for proper entity isolation
|
|
193
|
+
session_data = await self._state_adapter.load_state(
|
|
194
|
+
entity_type,
|
|
195
|
+
entity_key,
|
|
196
|
+
scope="session",
|
|
197
|
+
scope_id=self._session_id,
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
# Extract messages from session object
|
|
201
|
+
if isinstance(session_data, dict) and "messages" in session_data:
|
|
202
|
+
# New format with session metadata
|
|
203
|
+
messages_data = session_data["messages"]
|
|
204
|
+
elif isinstance(session_data, list):
|
|
205
|
+
# Old format - just messages array
|
|
206
|
+
messages_data = session_data
|
|
207
|
+
else:
|
|
208
|
+
# No messages found
|
|
209
|
+
messages_data = []
|
|
210
|
+
|
|
211
|
+
# Convert dict representations back to Message objects
|
|
212
|
+
return self._convert_dicts_to_messages(messages_data)
|
|
213
|
+
|
|
214
|
+
def _convert_dicts_to_messages(self, messages_data: list) -> List[Message]:
|
|
215
|
+
"""Convert list of message dicts to Message objects."""
|
|
216
|
+
from ..lm import MessageRole
|
|
217
|
+
|
|
218
|
+
messages = []
|
|
219
|
+
for msg_dict in messages_data:
|
|
220
|
+
if isinstance(msg_dict, dict):
|
|
221
|
+
role = msg_dict.get("role", "user")
|
|
222
|
+
content = msg_dict.get("content", "")
|
|
223
|
+
if role == "user":
|
|
224
|
+
messages.append(Message.user(content))
|
|
225
|
+
elif role == "assistant":
|
|
226
|
+
messages.append(Message.assistant(content))
|
|
227
|
+
else:
|
|
228
|
+
# Generic message - create with MessageRole enum
|
|
229
|
+
msg_role = (
|
|
230
|
+
MessageRole(role)
|
|
231
|
+
if role in ("user", "assistant", "system")
|
|
232
|
+
else MessageRole.USER
|
|
233
|
+
)
|
|
234
|
+
msg = Message(role=msg_role, content=content)
|
|
235
|
+
messages.append(msg)
|
|
236
|
+
else:
|
|
237
|
+
# Already a Message object
|
|
238
|
+
messages.append(msg_dict)
|
|
239
|
+
|
|
240
|
+
return messages
|
|
241
|
+
|
|
242
|
+
async def save_conversation_history(self, messages: List[Message]) -> None:
|
|
243
|
+
"""
|
|
244
|
+
Save conversation history to state and persist to database.
|
|
245
|
+
|
|
246
|
+
Uses the EntityStateAdapter which delegates to Rust core for version-checked saves.
|
|
247
|
+
If running within a workflow, saves to workflow entity state instead.
|
|
248
|
+
|
|
249
|
+
Args:
|
|
250
|
+
messages: List of Message objects to persist
|
|
251
|
+
"""
|
|
252
|
+
if self._storage_mode == "workflow":
|
|
253
|
+
await self._save_to_workflow_state(messages)
|
|
254
|
+
else:
|
|
255
|
+
await self._save_to_entity_storage(messages)
|
|
256
|
+
|
|
257
|
+
async def _save_to_workflow_state(self, messages: List[Message]) -> None:
|
|
258
|
+
"""Save conversation history to workflow entity state."""
|
|
259
|
+
# Convert Message objects to dict for JSON serialization
|
|
260
|
+
messages_data = []
|
|
261
|
+
for msg in messages:
|
|
262
|
+
messages_data.append({
|
|
263
|
+
"role": msg.role.value if hasattr(msg.role, 'value') else str(msg.role),
|
|
264
|
+
"content": msg.content,
|
|
265
|
+
"timestamp": time.time()
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
# Build agent data structure
|
|
269
|
+
key = f"agent.{self._agent_name}"
|
|
270
|
+
current_data = self._workflow_entity.state.get(key, {})
|
|
271
|
+
now = time.time()
|
|
272
|
+
|
|
273
|
+
agent_data = {
|
|
274
|
+
"session_id": self._session_id,
|
|
275
|
+
"agent_name": self._agent_name,
|
|
276
|
+
"created_at": current_data.get("created_at", now),
|
|
277
|
+
"last_message_time": now,
|
|
278
|
+
"message_count": len(messages_data),
|
|
279
|
+
"messages": messages_data,
|
|
280
|
+
"metadata": getattr(self, '_custom_metadata', {})
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
# Store in workflow state (WorkflowEntity handles persistence)
|
|
284
|
+
self._workflow_entity.state.set(key, agent_data)
|
|
285
|
+
logger.info(f"Saved conversation to workflow state: {key} ({len(messages_data)} messages)")
|
|
286
|
+
|
|
287
|
+
async def _save_to_entity_storage(self, messages: List[Message]) -> None:
|
|
288
|
+
"""Save conversation history to AgentSession entity (standalone mode)."""
|
|
289
|
+
# Convert Message objects to dict for JSON serialization
|
|
290
|
+
messages_data = []
|
|
291
|
+
for msg in messages:
|
|
292
|
+
messages_data.append({
|
|
293
|
+
"role": msg.role.value if hasattr(msg.role, 'value') else str(msg.role),
|
|
294
|
+
"content": msg.content,
|
|
295
|
+
"timestamp": time.time() # Add timestamp for each message
|
|
296
|
+
})
|
|
297
|
+
|
|
298
|
+
entity_type = "AgentSession"
|
|
299
|
+
entity_key = self._entity_key
|
|
300
|
+
|
|
301
|
+
# Load current state with version for optimistic locking
|
|
302
|
+
# Use session scope with session_id for proper entity isolation
|
|
303
|
+
current_state, current_version = await self._state_adapter.load_with_version(
|
|
304
|
+
entity_type,
|
|
305
|
+
entity_key,
|
|
306
|
+
scope="session",
|
|
307
|
+
scope_id=self._session_id,
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
# Build session object with metadata
|
|
311
|
+
now = time.time()
|
|
312
|
+
|
|
313
|
+
# Get custom metadata from instance variable or preserve from loaded state
|
|
314
|
+
custom_metadata = getattr(self, '_custom_metadata', current_state.get("metadata", {}))
|
|
315
|
+
|
|
316
|
+
session_data = {
|
|
317
|
+
"session_id": self._session_id,
|
|
318
|
+
"agent_name": self._agent_name,
|
|
319
|
+
"created_at": current_state.get("created_at", now), # Preserve existing or set new
|
|
320
|
+
"last_message_time": now,
|
|
321
|
+
"message_count": len(messages_data),
|
|
322
|
+
"messages": messages_data,
|
|
323
|
+
"metadata": custom_metadata # Save custom metadata
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
# Save to platform via adapter (Rust handles optimistic locking)
|
|
327
|
+
# Use session scope with session_id for proper entity isolation
|
|
328
|
+
try:
|
|
329
|
+
new_version = await self._state_adapter.save_state(
|
|
330
|
+
entity_type,
|
|
331
|
+
entity_key,
|
|
332
|
+
session_data,
|
|
333
|
+
current_version,
|
|
334
|
+
scope="session",
|
|
335
|
+
scope_id=self._session_id,
|
|
336
|
+
)
|
|
337
|
+
logger.info(
|
|
338
|
+
f"Persisted conversation history: {entity_key} "
|
|
339
|
+
f"(version {current_version} -> {new_version})"
|
|
340
|
+
)
|
|
341
|
+
except Exception as e:
|
|
342
|
+
logger.error(f"Failed to persist conversation history to database: {e}")
|
|
343
|
+
# Don't fail - conversation is still in memory for this execution
|
|
344
|
+
|
|
345
|
+
async def get_metadata(self) -> Dict[str, Any]:
|
|
346
|
+
"""
|
|
347
|
+
Get conversation session metadata.
|
|
348
|
+
|
|
349
|
+
Returns session metadata including:
|
|
350
|
+
- created_at: Timestamp of first message (float, Unix timestamp)
|
|
351
|
+
- last_activity: Timestamp of last message (float, Unix timestamp)
|
|
352
|
+
- message_count: Number of messages in conversation (int)
|
|
353
|
+
- custom: Dict of user-provided custom metadata
|
|
354
|
+
|
|
355
|
+
Returns:
|
|
356
|
+
Dictionary with metadata. If no conversation exists yet, returns defaults.
|
|
357
|
+
|
|
358
|
+
Example:
|
|
359
|
+
```python
|
|
360
|
+
metadata = await context.get_metadata()
|
|
361
|
+
print(f"Session created: {metadata['created_at']}")
|
|
362
|
+
print(f"User ID: {metadata['custom'].get('user_id')}")
|
|
363
|
+
```
|
|
364
|
+
"""
|
|
365
|
+
if self._storage_mode == "workflow":
|
|
366
|
+
return await self._get_metadata_from_workflow()
|
|
367
|
+
else:
|
|
368
|
+
return await self._get_metadata_from_entity()
|
|
369
|
+
|
|
370
|
+
async def _get_metadata_from_workflow(self) -> Dict[str, Any]:
|
|
371
|
+
"""Get metadata from workflow entity state."""
|
|
372
|
+
key = f"agent.{self._agent_name}"
|
|
373
|
+
agent_data = self._workflow_entity.state.get(key, {})
|
|
374
|
+
|
|
375
|
+
if not agent_data:
|
|
376
|
+
# No conversation exists yet - return defaults
|
|
377
|
+
return {
|
|
378
|
+
"created_at": None,
|
|
379
|
+
"last_activity": None,
|
|
380
|
+
"message_count": 0,
|
|
381
|
+
"custom": getattr(self, '_custom_metadata', {})
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
messages = agent_data.get("messages", [])
|
|
385
|
+
return {
|
|
386
|
+
"created_at": agent_data.get("created_at"),
|
|
387
|
+
"last_activity": agent_data.get("last_message_time"),
|
|
388
|
+
"message_count": len(messages),
|
|
389
|
+
"custom": agent_data.get("metadata", {})
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
async def _get_metadata_from_entity(self) -> Dict[str, Any]:
|
|
393
|
+
"""Get metadata from AgentSession entity (standalone mode)."""
|
|
394
|
+
entity_type = "AgentSession"
|
|
395
|
+
entity_key = self._entity_key
|
|
396
|
+
|
|
397
|
+
# Load session data with session scope
|
|
398
|
+
session_data = await self._state_adapter.load_state(
|
|
399
|
+
entity_type,
|
|
400
|
+
entity_key,
|
|
401
|
+
scope="session",
|
|
402
|
+
scope_id=self._session_id,
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
if not session_data:
|
|
406
|
+
# No conversation exists yet - return defaults
|
|
407
|
+
return {
|
|
408
|
+
"created_at": None,
|
|
409
|
+
"last_activity": None,
|
|
410
|
+
"message_count": 0,
|
|
411
|
+
"custom": getattr(self, '_custom_metadata', {})
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
messages = session_data.get("messages", [])
|
|
415
|
+
|
|
416
|
+
# Derive timestamps from messages if available
|
|
417
|
+
created_at = session_data.get("created_at")
|
|
418
|
+
last_activity = session_data.get("last_message_time")
|
|
419
|
+
|
|
420
|
+
return {
|
|
421
|
+
"created_at": created_at,
|
|
422
|
+
"last_activity": last_activity,
|
|
423
|
+
"message_count": len(messages),
|
|
424
|
+
"custom": session_data.get("metadata", {})
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
def update_metadata(self, **kwargs) -> None:
|
|
428
|
+
"""
|
|
429
|
+
Update custom session metadata.
|
|
430
|
+
|
|
431
|
+
Metadata will be persisted alongside conversation history on next save.
|
|
432
|
+
Use this to store application-specific data like user_id, preferences, etc.
|
|
433
|
+
|
|
434
|
+
Args:
|
|
435
|
+
**kwargs: Key-value pairs to store as metadata
|
|
436
|
+
|
|
437
|
+
Example:
|
|
438
|
+
```python
|
|
439
|
+
# Store user identification and preferences
|
|
440
|
+
context.update_metadata(
|
|
441
|
+
user_id="user-123",
|
|
442
|
+
subscription_tier="premium",
|
|
443
|
+
preferences={"theme": "dark", "language": "en"}
|
|
444
|
+
)
|
|
445
|
+
|
|
446
|
+
# Later retrieve it
|
|
447
|
+
metadata = await context.get_metadata()
|
|
448
|
+
user_id = metadata["custom"]["user_id"]
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
Note:
|
|
452
|
+
- Metadata is merged with existing metadata (doesn't replace)
|
|
453
|
+
- Changes persist on next save_conversation_history() call
|
|
454
|
+
- Use simple JSON-serializable types (str, int, float, dict, list)
|
|
455
|
+
"""
|
|
456
|
+
if not hasattr(self, '_custom_metadata'):
|
|
457
|
+
self._custom_metadata = {}
|
|
458
|
+
self._custom_metadata.update(kwargs)
|