agentrun-sdk 0.1.2__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.
Potentially problematic release.
This version of agentrun-sdk might be problematic. Click here for more details.
- agentrun_operation_sdk/cli/__init__.py +1 -0
- agentrun_operation_sdk/cli/cli.py +19 -0
- agentrun_operation_sdk/cli/common.py +21 -0
- agentrun_operation_sdk/cli/runtime/__init__.py +1 -0
- agentrun_operation_sdk/cli/runtime/commands.py +203 -0
- agentrun_operation_sdk/client/client.py +75 -0
- agentrun_operation_sdk/operations/runtime/__init__.py +8 -0
- agentrun_operation_sdk/operations/runtime/configure.py +101 -0
- agentrun_operation_sdk/operations/runtime/launch.py +82 -0
- agentrun_operation_sdk/operations/runtime/models.py +31 -0
- agentrun_operation_sdk/services/runtime.py +152 -0
- agentrun_operation_sdk/utils/logging_config.py +72 -0
- agentrun_operation_sdk/utils/runtime/config.py +94 -0
- agentrun_operation_sdk/utils/runtime/container.py +280 -0
- agentrun_operation_sdk/utils/runtime/entrypoint.py +203 -0
- agentrun_operation_sdk/utils/runtime/schema.py +56 -0
- agentrun_sdk/__init__.py +7 -0
- agentrun_sdk/agent/__init__.py +25 -0
- agentrun_sdk/agent/agent.py +696 -0
- agentrun_sdk/agent/agent_result.py +46 -0
- agentrun_sdk/agent/conversation_manager/__init__.py +26 -0
- agentrun_sdk/agent/conversation_manager/conversation_manager.py +88 -0
- agentrun_sdk/agent/conversation_manager/null_conversation_manager.py +46 -0
- agentrun_sdk/agent/conversation_manager/sliding_window_conversation_manager.py +179 -0
- agentrun_sdk/agent/conversation_manager/summarizing_conversation_manager.py +252 -0
- agentrun_sdk/agent/state.py +97 -0
- agentrun_sdk/event_loop/__init__.py +9 -0
- agentrun_sdk/event_loop/event_loop.py +499 -0
- agentrun_sdk/event_loop/streaming.py +319 -0
- agentrun_sdk/experimental/__init__.py +4 -0
- agentrun_sdk/experimental/hooks/__init__.py +15 -0
- agentrun_sdk/experimental/hooks/events.py +123 -0
- agentrun_sdk/handlers/__init__.py +10 -0
- agentrun_sdk/handlers/callback_handler.py +70 -0
- agentrun_sdk/hooks/__init__.py +49 -0
- agentrun_sdk/hooks/events.py +80 -0
- agentrun_sdk/hooks/registry.py +247 -0
- agentrun_sdk/models/__init__.py +10 -0
- agentrun_sdk/models/anthropic.py +432 -0
- agentrun_sdk/models/bedrock.py +649 -0
- agentrun_sdk/models/litellm.py +225 -0
- agentrun_sdk/models/llamaapi.py +438 -0
- agentrun_sdk/models/mistral.py +539 -0
- agentrun_sdk/models/model.py +95 -0
- agentrun_sdk/models/ollama.py +357 -0
- agentrun_sdk/models/openai.py +436 -0
- agentrun_sdk/models/sagemaker.py +598 -0
- agentrun_sdk/models/writer.py +449 -0
- agentrun_sdk/multiagent/__init__.py +22 -0
- agentrun_sdk/multiagent/a2a/__init__.py +15 -0
- agentrun_sdk/multiagent/a2a/executor.py +148 -0
- agentrun_sdk/multiagent/a2a/server.py +252 -0
- agentrun_sdk/multiagent/base.py +92 -0
- agentrun_sdk/multiagent/graph.py +555 -0
- agentrun_sdk/multiagent/swarm.py +656 -0
- agentrun_sdk/py.typed +1 -0
- agentrun_sdk/session/__init__.py +18 -0
- agentrun_sdk/session/file_session_manager.py +216 -0
- agentrun_sdk/session/repository_session_manager.py +152 -0
- agentrun_sdk/session/s3_session_manager.py +272 -0
- agentrun_sdk/session/session_manager.py +73 -0
- agentrun_sdk/session/session_repository.py +51 -0
- agentrun_sdk/telemetry/__init__.py +21 -0
- agentrun_sdk/telemetry/config.py +194 -0
- agentrun_sdk/telemetry/metrics.py +476 -0
- agentrun_sdk/telemetry/metrics_constants.py +15 -0
- agentrun_sdk/telemetry/tracer.py +563 -0
- agentrun_sdk/tools/__init__.py +17 -0
- agentrun_sdk/tools/decorator.py +569 -0
- agentrun_sdk/tools/executor.py +137 -0
- agentrun_sdk/tools/loader.py +152 -0
- agentrun_sdk/tools/mcp/__init__.py +13 -0
- agentrun_sdk/tools/mcp/mcp_agent_tool.py +99 -0
- agentrun_sdk/tools/mcp/mcp_client.py +423 -0
- agentrun_sdk/tools/mcp/mcp_instrumentation.py +322 -0
- agentrun_sdk/tools/mcp/mcp_types.py +63 -0
- agentrun_sdk/tools/registry.py +607 -0
- agentrun_sdk/tools/structured_output.py +421 -0
- agentrun_sdk/tools/tools.py +217 -0
- agentrun_sdk/tools/watcher.py +136 -0
- agentrun_sdk/types/__init__.py +5 -0
- agentrun_sdk/types/collections.py +23 -0
- agentrun_sdk/types/content.py +188 -0
- agentrun_sdk/types/event_loop.py +48 -0
- agentrun_sdk/types/exceptions.py +81 -0
- agentrun_sdk/types/guardrails.py +254 -0
- agentrun_sdk/types/media.py +89 -0
- agentrun_sdk/types/session.py +152 -0
- agentrun_sdk/types/streaming.py +201 -0
- agentrun_sdk/types/tools.py +258 -0
- agentrun_sdk/types/traces.py +5 -0
- agentrun_sdk-0.1.2.dist-info/METADATA +51 -0
- agentrun_sdk-0.1.2.dist-info/RECORD +115 -0
- agentrun_sdk-0.1.2.dist-info/WHEEL +5 -0
- agentrun_sdk-0.1.2.dist-info/entry_points.txt +2 -0
- agentrun_sdk-0.1.2.dist-info/top_level.txt +3 -0
- agentrun_wrapper/__init__.py +11 -0
- agentrun_wrapper/_utils/__init__.py +6 -0
- agentrun_wrapper/_utils/endpoints.py +16 -0
- agentrun_wrapper/identity/__init__.py +5 -0
- agentrun_wrapper/identity/auth.py +211 -0
- agentrun_wrapper/memory/__init__.py +6 -0
- agentrun_wrapper/memory/client.py +1697 -0
- agentrun_wrapper/memory/constants.py +103 -0
- agentrun_wrapper/memory/controlplane.py +626 -0
- agentrun_wrapper/py.typed +1 -0
- agentrun_wrapper/runtime/__init__.py +13 -0
- agentrun_wrapper/runtime/app.py +473 -0
- agentrun_wrapper/runtime/context.py +34 -0
- agentrun_wrapper/runtime/models.py +25 -0
- agentrun_wrapper/services/__init__.py +1 -0
- agentrun_wrapper/services/identity.py +192 -0
- agentrun_wrapper/tools/__init__.py +6 -0
- agentrun_wrapper/tools/browser_client.py +325 -0
- agentrun_wrapper/tools/code_interpreter_client.py +186 -0
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
"""File-based session manager for local filesystem storage."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
import os
|
|
6
|
+
import shutil
|
|
7
|
+
import tempfile
|
|
8
|
+
from typing import Any, Optional, cast
|
|
9
|
+
|
|
10
|
+
from ..types.exceptions import SessionException
|
|
11
|
+
from ..types.session import Session, SessionAgent, SessionMessage
|
|
12
|
+
from .repository_session_manager import RepositorySessionManager
|
|
13
|
+
from .session_repository import SessionRepository
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
SESSION_PREFIX = "session_"
|
|
18
|
+
AGENT_PREFIX = "agent_"
|
|
19
|
+
MESSAGE_PREFIX = "message_"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class FileSessionManager(RepositorySessionManager, SessionRepository):
|
|
23
|
+
"""File-based session manager for local filesystem storage.
|
|
24
|
+
|
|
25
|
+
Creates the following filesystem structure for the session storage:
|
|
26
|
+
```bash
|
|
27
|
+
/<sessions_dir>/
|
|
28
|
+
└── session_<session_id>/
|
|
29
|
+
├── session.json # Session metadata
|
|
30
|
+
└── agents/
|
|
31
|
+
└── agent_<agent_id>/
|
|
32
|
+
├── agent.json # Agent metadata
|
|
33
|
+
└── messages/
|
|
34
|
+
├── message_<id1>.json
|
|
35
|
+
└── message_<id2>.json
|
|
36
|
+
```
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def __init__(self, session_id: str, storage_dir: Optional[str] = None, **kwargs: Any):
|
|
40
|
+
"""Initialize FileSession with filesystem storage.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
session_id: ID for the session
|
|
44
|
+
storage_dir: Directory for local filesystem storage (defaults to temp dir)
|
|
45
|
+
**kwargs: Additional keyword arguments for future extensibility.
|
|
46
|
+
"""
|
|
47
|
+
self.storage_dir = storage_dir or os.path.join(tempfile.gettempdir(), "strands/sessions")
|
|
48
|
+
os.makedirs(self.storage_dir, exist_ok=True)
|
|
49
|
+
|
|
50
|
+
super().__init__(session_id=session_id, session_repository=self)
|
|
51
|
+
|
|
52
|
+
def _get_session_path(self, session_id: str) -> str:
|
|
53
|
+
"""Get session directory path."""
|
|
54
|
+
return os.path.join(self.storage_dir, f"{SESSION_PREFIX}{session_id}")
|
|
55
|
+
|
|
56
|
+
def _get_agent_path(self, session_id: str, agent_id: str) -> str:
|
|
57
|
+
"""Get agent directory path."""
|
|
58
|
+
session_path = self._get_session_path(session_id)
|
|
59
|
+
return os.path.join(session_path, "agents", f"{AGENT_PREFIX}{agent_id}")
|
|
60
|
+
|
|
61
|
+
def _get_message_path(self, session_id: str, agent_id: str, message_id: int) -> str:
|
|
62
|
+
"""Get message file path.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
session_id: ID of the session
|
|
66
|
+
agent_id: ID of the agent
|
|
67
|
+
message_id: Index of the message
|
|
68
|
+
Returns:
|
|
69
|
+
The filename for the message
|
|
70
|
+
"""
|
|
71
|
+
agent_path = self._get_agent_path(session_id, agent_id)
|
|
72
|
+
return os.path.join(agent_path, "messages", f"{MESSAGE_PREFIX}{message_id}.json")
|
|
73
|
+
|
|
74
|
+
def _read_file(self, path: str) -> dict[str, Any]:
|
|
75
|
+
"""Read JSON file."""
|
|
76
|
+
try:
|
|
77
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
78
|
+
return cast(dict[str, Any], json.load(f))
|
|
79
|
+
except json.JSONDecodeError as e:
|
|
80
|
+
raise SessionException(f"Invalid JSON in file {path}: {str(e)}") from e
|
|
81
|
+
|
|
82
|
+
def _write_file(self, path: str, data: dict[str, Any]) -> None:
|
|
83
|
+
"""Write JSON file."""
|
|
84
|
+
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
85
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
86
|
+
json.dump(data, f, indent=2, ensure_ascii=False)
|
|
87
|
+
|
|
88
|
+
def create_session(self, session: Session, **kwargs: Any) -> Session:
|
|
89
|
+
"""Create a new session."""
|
|
90
|
+
session_dir = self._get_session_path(session.session_id)
|
|
91
|
+
if os.path.exists(session_dir):
|
|
92
|
+
raise SessionException(f"Session {session.session_id} already exists")
|
|
93
|
+
|
|
94
|
+
# Create directory structure
|
|
95
|
+
os.makedirs(session_dir, exist_ok=True)
|
|
96
|
+
os.makedirs(os.path.join(session_dir, "agents"), exist_ok=True)
|
|
97
|
+
|
|
98
|
+
# Write session file
|
|
99
|
+
session_file = os.path.join(session_dir, "session.json")
|
|
100
|
+
session_dict = session.to_dict()
|
|
101
|
+
self._write_file(session_file, session_dict)
|
|
102
|
+
|
|
103
|
+
return session
|
|
104
|
+
|
|
105
|
+
def read_session(self, session_id: str, **kwargs: Any) -> Optional[Session]:
|
|
106
|
+
"""Read session data."""
|
|
107
|
+
session_file = os.path.join(self._get_session_path(session_id), "session.json")
|
|
108
|
+
if not os.path.exists(session_file):
|
|
109
|
+
return None
|
|
110
|
+
|
|
111
|
+
session_data = self._read_file(session_file)
|
|
112
|
+
return Session.from_dict(session_data)
|
|
113
|
+
|
|
114
|
+
def delete_session(self, session_id: str, **kwargs: Any) -> None:
|
|
115
|
+
"""Delete session and all associated data."""
|
|
116
|
+
session_dir = self._get_session_path(session_id)
|
|
117
|
+
if not os.path.exists(session_dir):
|
|
118
|
+
raise SessionException(f"Session {session_id} does not exist")
|
|
119
|
+
|
|
120
|
+
shutil.rmtree(session_dir)
|
|
121
|
+
|
|
122
|
+
def create_agent(self, session_id: str, session_agent: SessionAgent, **kwargs: Any) -> None:
|
|
123
|
+
"""Create a new agent in the session."""
|
|
124
|
+
agent_id = session_agent.agent_id
|
|
125
|
+
|
|
126
|
+
agent_dir = self._get_agent_path(session_id, agent_id)
|
|
127
|
+
os.makedirs(agent_dir, exist_ok=True)
|
|
128
|
+
os.makedirs(os.path.join(agent_dir, "messages"), exist_ok=True)
|
|
129
|
+
|
|
130
|
+
agent_file = os.path.join(agent_dir, "agent.json")
|
|
131
|
+
session_data = session_agent.to_dict()
|
|
132
|
+
self._write_file(agent_file, session_data)
|
|
133
|
+
|
|
134
|
+
def read_agent(self, session_id: str, agent_id: str, **kwargs: Any) -> Optional[SessionAgent]:
|
|
135
|
+
"""Read agent data."""
|
|
136
|
+
agent_file = os.path.join(self._get_agent_path(session_id, agent_id), "agent.json")
|
|
137
|
+
if not os.path.exists(agent_file):
|
|
138
|
+
return None
|
|
139
|
+
|
|
140
|
+
agent_data = self._read_file(agent_file)
|
|
141
|
+
return SessionAgent.from_dict(agent_data)
|
|
142
|
+
|
|
143
|
+
def update_agent(self, session_id: str, session_agent: SessionAgent, **kwargs: Any) -> None:
|
|
144
|
+
"""Update agent data."""
|
|
145
|
+
agent_id = session_agent.agent_id
|
|
146
|
+
previous_agent = self.read_agent(session_id=session_id, agent_id=agent_id)
|
|
147
|
+
if previous_agent is None:
|
|
148
|
+
raise SessionException(f"Agent {agent_id} in session {session_id} does not exist")
|
|
149
|
+
|
|
150
|
+
session_agent.created_at = previous_agent.created_at
|
|
151
|
+
agent_file = os.path.join(self._get_agent_path(session_id, agent_id), "agent.json")
|
|
152
|
+
self._write_file(agent_file, session_agent.to_dict())
|
|
153
|
+
|
|
154
|
+
def create_message(self, session_id: str, agent_id: str, session_message: SessionMessage, **kwargs: Any) -> None:
|
|
155
|
+
"""Create a new message for the agent."""
|
|
156
|
+
message_file = self._get_message_path(
|
|
157
|
+
session_id,
|
|
158
|
+
agent_id,
|
|
159
|
+
session_message.message_id,
|
|
160
|
+
)
|
|
161
|
+
session_dict = session_message.to_dict()
|
|
162
|
+
self._write_file(message_file, session_dict)
|
|
163
|
+
|
|
164
|
+
def read_message(self, session_id: str, agent_id: str, message_id: int, **kwargs: Any) -> Optional[SessionMessage]:
|
|
165
|
+
"""Read message data."""
|
|
166
|
+
message_path = self._get_message_path(session_id, agent_id, message_id)
|
|
167
|
+
if not os.path.exists(message_path):
|
|
168
|
+
return None
|
|
169
|
+
message_data = self._read_file(message_path)
|
|
170
|
+
return SessionMessage.from_dict(message_data)
|
|
171
|
+
|
|
172
|
+
def update_message(self, session_id: str, agent_id: str, session_message: SessionMessage, **kwargs: Any) -> None:
|
|
173
|
+
"""Update message data."""
|
|
174
|
+
message_id = session_message.message_id
|
|
175
|
+
previous_message = self.read_message(session_id=session_id, agent_id=agent_id, message_id=message_id)
|
|
176
|
+
if previous_message is None:
|
|
177
|
+
raise SessionException(f"Message {message_id} does not exist")
|
|
178
|
+
|
|
179
|
+
# Preserve the original created_at timestamp
|
|
180
|
+
session_message.created_at = previous_message.created_at
|
|
181
|
+
message_file = self._get_message_path(session_id, agent_id, message_id)
|
|
182
|
+
self._write_file(message_file, session_message.to_dict())
|
|
183
|
+
|
|
184
|
+
def list_messages(
|
|
185
|
+
self, session_id: str, agent_id: str, limit: Optional[int] = None, offset: int = 0, **kwargs: Any
|
|
186
|
+
) -> list[SessionMessage]:
|
|
187
|
+
"""List messages for an agent with pagination."""
|
|
188
|
+
messages_dir = os.path.join(self._get_agent_path(session_id, agent_id), "messages")
|
|
189
|
+
if not os.path.exists(messages_dir):
|
|
190
|
+
raise SessionException(f"Messages directory missing from agent: {agent_id} in session {session_id}")
|
|
191
|
+
|
|
192
|
+
# Read all message files, and record the index
|
|
193
|
+
message_index_files: list[tuple[int, str]] = []
|
|
194
|
+
for filename in os.listdir(messages_dir):
|
|
195
|
+
if filename.startswith(MESSAGE_PREFIX) and filename.endswith(".json"):
|
|
196
|
+
# Extract index from message_<index>.json format
|
|
197
|
+
index = int(filename[len(MESSAGE_PREFIX) : -5]) # Remove prefix and .json suffix
|
|
198
|
+
message_index_files.append((index, filename))
|
|
199
|
+
|
|
200
|
+
# Sort by index and extract just the filenames
|
|
201
|
+
message_files = [f for _, f in sorted(message_index_files)]
|
|
202
|
+
|
|
203
|
+
# Apply pagination to filenames
|
|
204
|
+
if limit is not None:
|
|
205
|
+
message_files = message_files[offset : offset + limit]
|
|
206
|
+
else:
|
|
207
|
+
message_files = message_files[offset:]
|
|
208
|
+
|
|
209
|
+
# Load only the message files
|
|
210
|
+
messages: list[SessionMessage] = []
|
|
211
|
+
for filename in message_files:
|
|
212
|
+
file_path = os.path.join(messages_dir, filename)
|
|
213
|
+
message_data = self._read_file(file_path)
|
|
214
|
+
messages.append(SessionMessage.from_dict(message_data))
|
|
215
|
+
|
|
216
|
+
return messages
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"""Repository session manager implementation."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import TYPE_CHECKING, Any, Optional
|
|
5
|
+
|
|
6
|
+
from ..agent.state import AgentState
|
|
7
|
+
from ..types.content import Message
|
|
8
|
+
from ..types.exceptions import SessionException
|
|
9
|
+
from ..types.session import (
|
|
10
|
+
Session,
|
|
11
|
+
SessionAgent,
|
|
12
|
+
SessionMessage,
|
|
13
|
+
SessionType,
|
|
14
|
+
)
|
|
15
|
+
from .session_manager import SessionManager
|
|
16
|
+
from .session_repository import SessionRepository
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from ..agent.agent import Agent
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class RepositorySessionManager(SessionManager):
|
|
25
|
+
"""Session manager for persisting agents in a SessionRepository."""
|
|
26
|
+
|
|
27
|
+
def __init__(self, session_id: str, session_repository: SessionRepository, **kwargs: Any):
|
|
28
|
+
"""Initialize the RepositorySessionManager.
|
|
29
|
+
|
|
30
|
+
If no session with the specified session_id exists yet, it will be created
|
|
31
|
+
in the session_repository.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
session_id: ID to use for the session. A new session with this id will be created if it does
|
|
35
|
+
not exist in the repository yet
|
|
36
|
+
session_repository: Underlying session repository to use to store the sessions state.
|
|
37
|
+
**kwargs: Additional keyword arguments for future extensibility.
|
|
38
|
+
|
|
39
|
+
"""
|
|
40
|
+
self.session_repository = session_repository
|
|
41
|
+
self.session_id = session_id
|
|
42
|
+
session = session_repository.read_session(session_id)
|
|
43
|
+
# Create a session if it does not exist yet
|
|
44
|
+
if session is None:
|
|
45
|
+
logger.debug("session_id=<%s> | session not found, creating new session", self.session_id)
|
|
46
|
+
session = Session(session_id=session_id, session_type=SessionType.AGENT)
|
|
47
|
+
session_repository.create_session(session)
|
|
48
|
+
|
|
49
|
+
self.session = session
|
|
50
|
+
|
|
51
|
+
# Keep track of the latest message of each agent in case we need to redact it.
|
|
52
|
+
self._latest_agent_message: dict[str, Optional[SessionMessage]] = {}
|
|
53
|
+
|
|
54
|
+
def append_message(self, message: Message, agent: "Agent", **kwargs: Any) -> None:
|
|
55
|
+
"""Append a message to the agent's session.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
message: Message to add to the agent in the session
|
|
59
|
+
agent: Agent to append the message to
|
|
60
|
+
**kwargs: Additional keyword arguments for future extensibility.
|
|
61
|
+
"""
|
|
62
|
+
# Calculate the next index (0 if this is the first message, otherwise increment the previous index)
|
|
63
|
+
latest_agent_message = self._latest_agent_message[agent.agent_id]
|
|
64
|
+
if latest_agent_message:
|
|
65
|
+
next_index = latest_agent_message.message_id + 1
|
|
66
|
+
else:
|
|
67
|
+
next_index = 0
|
|
68
|
+
|
|
69
|
+
session_message = SessionMessage.from_message(message, next_index)
|
|
70
|
+
self._latest_agent_message[agent.agent_id] = session_message
|
|
71
|
+
self.session_repository.create_message(self.session_id, agent.agent_id, session_message)
|
|
72
|
+
|
|
73
|
+
def redact_latest_message(self, redact_message: Message, agent: "Agent", **kwargs: Any) -> None:
|
|
74
|
+
"""Redact the latest message appended to the session.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
redact_message: New message to use that contains the redact content
|
|
78
|
+
agent: Agent to apply the message redaction to
|
|
79
|
+
**kwargs: Additional keyword arguments for future extensibility.
|
|
80
|
+
"""
|
|
81
|
+
latest_agent_message = self._latest_agent_message[agent.agent_id]
|
|
82
|
+
if latest_agent_message is None:
|
|
83
|
+
raise SessionException("No message to redact.")
|
|
84
|
+
latest_agent_message.redact_message = redact_message
|
|
85
|
+
return self.session_repository.update_message(self.session_id, agent.agent_id, latest_agent_message)
|
|
86
|
+
|
|
87
|
+
def sync_agent(self, agent: "Agent", **kwargs: Any) -> None:
|
|
88
|
+
"""Serialize and update the agent into the session repository.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
agent: Agent to sync to the session.
|
|
92
|
+
**kwargs: Additional keyword arguments for future extensibility.
|
|
93
|
+
"""
|
|
94
|
+
self.session_repository.update_agent(
|
|
95
|
+
self.session_id,
|
|
96
|
+
SessionAgent.from_agent(agent),
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
def initialize(self, agent: "Agent", **kwargs: Any) -> None:
|
|
100
|
+
"""Initialize an agent with a session.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
agent: Agent to initialize from the session
|
|
104
|
+
**kwargs: Additional keyword arguments for future extensibility.
|
|
105
|
+
"""
|
|
106
|
+
if agent.agent_id in self._latest_agent_message:
|
|
107
|
+
raise SessionException("The `agent_id` of an agent must be unique in a session.")
|
|
108
|
+
self._latest_agent_message[agent.agent_id] = None
|
|
109
|
+
|
|
110
|
+
session_agent = self.session_repository.read_agent(self.session_id, agent.agent_id)
|
|
111
|
+
|
|
112
|
+
if session_agent is None:
|
|
113
|
+
logger.debug(
|
|
114
|
+
"agent_id=<%s> | session_id=<%s> | creating agent",
|
|
115
|
+
agent.agent_id,
|
|
116
|
+
self.session_id,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
session_agent = SessionAgent.from_agent(agent)
|
|
120
|
+
self.session_repository.create_agent(self.session_id, session_agent)
|
|
121
|
+
# Initialize messages with sequential indices
|
|
122
|
+
session_message = None
|
|
123
|
+
for i, message in enumerate(agent.messages):
|
|
124
|
+
session_message = SessionMessage.from_message(message, i)
|
|
125
|
+
self.session_repository.create_message(self.session_id, agent.agent_id, session_message)
|
|
126
|
+
self._latest_agent_message[agent.agent_id] = session_message
|
|
127
|
+
else:
|
|
128
|
+
logger.debug(
|
|
129
|
+
"agent_id=<%s> | session_id=<%s> | restoring agent",
|
|
130
|
+
agent.agent_id,
|
|
131
|
+
self.session_id,
|
|
132
|
+
)
|
|
133
|
+
agent.state = AgentState(session_agent.state)
|
|
134
|
+
|
|
135
|
+
# Restore the conversation manager to its previous state, and get the optional prepend messages
|
|
136
|
+
prepend_messages = agent.conversation_manager.restore_from_session(session_agent.conversation_manager_state)
|
|
137
|
+
|
|
138
|
+
if prepend_messages is None:
|
|
139
|
+
prepend_messages = []
|
|
140
|
+
|
|
141
|
+
# List the messages currently in the session, using an offset of the messages previously removed
|
|
142
|
+
# by the conversation manager.
|
|
143
|
+
session_messages = self.session_repository.list_messages(
|
|
144
|
+
session_id=self.session_id,
|
|
145
|
+
agent_id=agent.agent_id,
|
|
146
|
+
offset=agent.conversation_manager.removed_message_count,
|
|
147
|
+
)
|
|
148
|
+
if len(session_messages) > 0:
|
|
149
|
+
self._latest_agent_message[agent.agent_id] = session_messages[-1]
|
|
150
|
+
|
|
151
|
+
# Restore the agents messages array including the optional prepend messages
|
|
152
|
+
agent.messages = prepend_messages + [session_message.to_message() for session_message in session_messages]
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
"""S3-based session manager for cloud storage."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
from typing import Any, Dict, List, Optional, cast
|
|
6
|
+
|
|
7
|
+
import boto3
|
|
8
|
+
from botocore.config import Config as BotocoreConfig
|
|
9
|
+
from botocore.exceptions import ClientError
|
|
10
|
+
|
|
11
|
+
from ..types.exceptions import SessionException
|
|
12
|
+
from ..types.session import Session, SessionAgent, SessionMessage
|
|
13
|
+
from .repository_session_manager import RepositorySessionManager
|
|
14
|
+
from .session_repository import SessionRepository
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
SESSION_PREFIX = "session_"
|
|
19
|
+
AGENT_PREFIX = "agent_"
|
|
20
|
+
MESSAGE_PREFIX = "message_"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class S3SessionManager(RepositorySessionManager, SessionRepository):
|
|
24
|
+
"""S3-based session manager for cloud storage.
|
|
25
|
+
|
|
26
|
+
Creates the following filesystem structure for the session storage:
|
|
27
|
+
```bash
|
|
28
|
+
/<sessions_dir>/
|
|
29
|
+
└── session_<session_id>/
|
|
30
|
+
├── session.json # Session metadata
|
|
31
|
+
└── agents/
|
|
32
|
+
└── agent_<agent_id>/
|
|
33
|
+
├── agent.json # Agent metadata
|
|
34
|
+
└── messages/
|
|
35
|
+
├── message_<id1>.json
|
|
36
|
+
└── message_<id2>.json
|
|
37
|
+
```
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def __init__(
|
|
41
|
+
self,
|
|
42
|
+
session_id: str,
|
|
43
|
+
bucket: str,
|
|
44
|
+
prefix: str = "",
|
|
45
|
+
boto_session: Optional[boto3.Session] = None,
|
|
46
|
+
boto_client_config: Optional[BotocoreConfig] = None,
|
|
47
|
+
region_name: Optional[str] = None,
|
|
48
|
+
**kwargs: Any,
|
|
49
|
+
):
|
|
50
|
+
"""Initialize S3SessionManager with S3 storage.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
session_id: ID for the session
|
|
54
|
+
bucket: S3 bucket name (required)
|
|
55
|
+
prefix: S3 key prefix for storage organization
|
|
56
|
+
boto_session: Optional boto3 session
|
|
57
|
+
boto_client_config: Optional boto3 client configuration
|
|
58
|
+
region_name: AWS region for S3 storage
|
|
59
|
+
**kwargs: Additional keyword arguments for future extensibility.
|
|
60
|
+
"""
|
|
61
|
+
self.bucket = bucket
|
|
62
|
+
self.prefix = prefix
|
|
63
|
+
|
|
64
|
+
session = boto_session or boto3.Session(region_name=region_name)
|
|
65
|
+
|
|
66
|
+
# Add strands-agents to the request user agent
|
|
67
|
+
if boto_client_config:
|
|
68
|
+
existing_user_agent = getattr(boto_client_config, "user_agent_extra", None)
|
|
69
|
+
# Append 'strands-agents' to existing user_agent_extra or set it if not present
|
|
70
|
+
if existing_user_agent:
|
|
71
|
+
new_user_agent = f"{existing_user_agent} strands-agents"
|
|
72
|
+
else:
|
|
73
|
+
new_user_agent = "strands-agents"
|
|
74
|
+
client_config = boto_client_config.merge(BotocoreConfig(user_agent_extra=new_user_agent))
|
|
75
|
+
else:
|
|
76
|
+
client_config = BotocoreConfig(user_agent_extra="strands-agents")
|
|
77
|
+
|
|
78
|
+
self.client = session.client(service_name="s3", config=client_config)
|
|
79
|
+
super().__init__(session_id=session_id, session_repository=self)
|
|
80
|
+
|
|
81
|
+
def _get_session_path(self, session_id: str) -> str:
|
|
82
|
+
"""Get session S3 prefix."""
|
|
83
|
+
return f"{self.prefix}/{SESSION_PREFIX}{session_id}/"
|
|
84
|
+
|
|
85
|
+
def _get_agent_path(self, session_id: str, agent_id: str) -> str:
|
|
86
|
+
"""Get agent S3 prefix."""
|
|
87
|
+
session_path = self._get_session_path(session_id)
|
|
88
|
+
return f"{session_path}agents/{AGENT_PREFIX}{agent_id}/"
|
|
89
|
+
|
|
90
|
+
def _get_message_path(self, session_id: str, agent_id: str, message_id: int) -> str:
|
|
91
|
+
"""Get message S3 key.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
session_id: ID of the session
|
|
95
|
+
agent_id: ID of the agent
|
|
96
|
+
message_id: Index of the message
|
|
97
|
+
**kwargs: Additional keyword arguments for future extensibility.
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
The key for the message
|
|
101
|
+
"""
|
|
102
|
+
agent_path = self._get_agent_path(session_id, agent_id)
|
|
103
|
+
return f"{agent_path}messages/{MESSAGE_PREFIX}{message_id}.json"
|
|
104
|
+
|
|
105
|
+
def _read_s3_object(self, key: str) -> Optional[Dict[str, Any]]:
|
|
106
|
+
"""Read JSON object from S3."""
|
|
107
|
+
try:
|
|
108
|
+
response = self.client.get_object(Bucket=self.bucket, Key=key)
|
|
109
|
+
content = response["Body"].read().decode("utf-8")
|
|
110
|
+
return cast(dict[str, Any], json.loads(content))
|
|
111
|
+
except ClientError as e:
|
|
112
|
+
if e.response["Error"]["Code"] == "NoSuchKey":
|
|
113
|
+
return None
|
|
114
|
+
else:
|
|
115
|
+
raise SessionException(f"S3 error reading {key}: {e}") from e
|
|
116
|
+
except json.JSONDecodeError as e:
|
|
117
|
+
raise SessionException(f"Invalid JSON in S3 object {key}: {e}") from e
|
|
118
|
+
|
|
119
|
+
def _write_s3_object(self, key: str, data: Dict[str, Any]) -> None:
|
|
120
|
+
"""Write JSON object to S3."""
|
|
121
|
+
try:
|
|
122
|
+
content = json.dumps(data, indent=2, ensure_ascii=False)
|
|
123
|
+
self.client.put_object(
|
|
124
|
+
Bucket=self.bucket, Key=key, Body=content.encode("utf-8"), ContentType="application/json"
|
|
125
|
+
)
|
|
126
|
+
except ClientError as e:
|
|
127
|
+
raise SessionException(f"Failed to write S3 object {key}: {e}") from e
|
|
128
|
+
|
|
129
|
+
def create_session(self, session: Session, **kwargs: Any) -> Session:
|
|
130
|
+
"""Create a new session in S3."""
|
|
131
|
+
session_key = f"{self._get_session_path(session.session_id)}session.json"
|
|
132
|
+
|
|
133
|
+
# Check if session already exists
|
|
134
|
+
try:
|
|
135
|
+
self.client.head_object(Bucket=self.bucket, Key=session_key)
|
|
136
|
+
raise SessionException(f"Session {session.session_id} already exists")
|
|
137
|
+
except ClientError as e:
|
|
138
|
+
if e.response["Error"]["Code"] != "404":
|
|
139
|
+
raise SessionException(f"S3 error checking session existence: {e}") from e
|
|
140
|
+
|
|
141
|
+
# Write session object
|
|
142
|
+
session_dict = session.to_dict()
|
|
143
|
+
self._write_s3_object(session_key, session_dict)
|
|
144
|
+
return session
|
|
145
|
+
|
|
146
|
+
def read_session(self, session_id: str, **kwargs: Any) -> Optional[Session]:
|
|
147
|
+
"""Read session data from S3."""
|
|
148
|
+
session_key = f"{self._get_session_path(session_id)}session.json"
|
|
149
|
+
session_data = self._read_s3_object(session_key)
|
|
150
|
+
if session_data is None:
|
|
151
|
+
return None
|
|
152
|
+
return Session.from_dict(session_data)
|
|
153
|
+
|
|
154
|
+
def delete_session(self, session_id: str, **kwargs: Any) -> None:
|
|
155
|
+
"""Delete session and all associated data from S3."""
|
|
156
|
+
session_prefix = self._get_session_path(session_id)
|
|
157
|
+
try:
|
|
158
|
+
paginator = self.client.get_paginator("list_objects_v2")
|
|
159
|
+
pages = paginator.paginate(Bucket=self.bucket, Prefix=session_prefix)
|
|
160
|
+
|
|
161
|
+
objects_to_delete = []
|
|
162
|
+
for page in pages:
|
|
163
|
+
if "Contents" in page:
|
|
164
|
+
objects_to_delete.extend([{"Key": obj["Key"]} for obj in page["Contents"]])
|
|
165
|
+
|
|
166
|
+
if not objects_to_delete:
|
|
167
|
+
raise SessionException(f"Session {session_id} does not exist")
|
|
168
|
+
|
|
169
|
+
# Delete objects in batches
|
|
170
|
+
for i in range(0, len(objects_to_delete), 1000):
|
|
171
|
+
batch = objects_to_delete[i : i + 1000]
|
|
172
|
+
self.client.delete_objects(Bucket=self.bucket, Delete={"Objects": batch})
|
|
173
|
+
|
|
174
|
+
except ClientError as e:
|
|
175
|
+
raise SessionException(f"S3 error deleting session {session_id}: {e}") from e
|
|
176
|
+
|
|
177
|
+
def create_agent(self, session_id: str, session_agent: SessionAgent, **kwargs: Any) -> None:
|
|
178
|
+
"""Create a new agent in S3."""
|
|
179
|
+
agent_id = session_agent.agent_id
|
|
180
|
+
agent_dict = session_agent.to_dict()
|
|
181
|
+
agent_key = f"{self._get_agent_path(session_id, agent_id)}agent.json"
|
|
182
|
+
self._write_s3_object(agent_key, agent_dict)
|
|
183
|
+
|
|
184
|
+
def read_agent(self, session_id: str, agent_id: str, **kwargs: Any) -> Optional[SessionAgent]:
|
|
185
|
+
"""Read agent data from S3."""
|
|
186
|
+
agent_key = f"{self._get_agent_path(session_id, agent_id)}agent.json"
|
|
187
|
+
agent_data = self._read_s3_object(agent_key)
|
|
188
|
+
if agent_data is None:
|
|
189
|
+
return None
|
|
190
|
+
return SessionAgent.from_dict(agent_data)
|
|
191
|
+
|
|
192
|
+
def update_agent(self, session_id: str, session_agent: SessionAgent, **kwargs: Any) -> None:
|
|
193
|
+
"""Update agent data in S3."""
|
|
194
|
+
agent_id = session_agent.agent_id
|
|
195
|
+
previous_agent = self.read_agent(session_id=session_id, agent_id=agent_id)
|
|
196
|
+
if previous_agent is None:
|
|
197
|
+
raise SessionException(f"Agent {agent_id} in session {session_id} does not exist")
|
|
198
|
+
|
|
199
|
+
# Preserve creation timestamp
|
|
200
|
+
session_agent.created_at = previous_agent.created_at
|
|
201
|
+
agent_key = f"{self._get_agent_path(session_id, agent_id)}agent.json"
|
|
202
|
+
self._write_s3_object(agent_key, session_agent.to_dict())
|
|
203
|
+
|
|
204
|
+
def create_message(self, session_id: str, agent_id: str, session_message: SessionMessage, **kwargs: Any) -> None:
|
|
205
|
+
"""Create a new message in S3."""
|
|
206
|
+
message_id = session_message.message_id
|
|
207
|
+
message_dict = session_message.to_dict()
|
|
208
|
+
message_key = self._get_message_path(session_id, agent_id, message_id)
|
|
209
|
+
self._write_s3_object(message_key, message_dict)
|
|
210
|
+
|
|
211
|
+
def read_message(self, session_id: str, agent_id: str, message_id: int, **kwargs: Any) -> Optional[SessionMessage]:
|
|
212
|
+
"""Read message data from S3."""
|
|
213
|
+
message_key = self._get_message_path(session_id, agent_id, message_id)
|
|
214
|
+
message_data = self._read_s3_object(message_key)
|
|
215
|
+
if message_data is None:
|
|
216
|
+
return None
|
|
217
|
+
return SessionMessage.from_dict(message_data)
|
|
218
|
+
|
|
219
|
+
def update_message(self, session_id: str, agent_id: str, session_message: SessionMessage, **kwargs: Any) -> None:
|
|
220
|
+
"""Update message data in S3."""
|
|
221
|
+
message_id = session_message.message_id
|
|
222
|
+
previous_message = self.read_message(session_id=session_id, agent_id=agent_id, message_id=message_id)
|
|
223
|
+
if previous_message is None:
|
|
224
|
+
raise SessionException(f"Message {message_id} does not exist")
|
|
225
|
+
|
|
226
|
+
# Preserve creation timestamp
|
|
227
|
+
session_message.created_at = previous_message.created_at
|
|
228
|
+
message_key = self._get_message_path(session_id, agent_id, message_id)
|
|
229
|
+
self._write_s3_object(message_key, session_message.to_dict())
|
|
230
|
+
|
|
231
|
+
def list_messages(
|
|
232
|
+
self, session_id: str, agent_id: str, limit: Optional[int] = None, offset: int = 0, **kwargs: Any
|
|
233
|
+
) -> List[SessionMessage]:
|
|
234
|
+
"""List messages for an agent with pagination from S3."""
|
|
235
|
+
messages_prefix = f"{self._get_agent_path(session_id, agent_id)}messages/"
|
|
236
|
+
try:
|
|
237
|
+
paginator = self.client.get_paginator("list_objects_v2")
|
|
238
|
+
pages = paginator.paginate(Bucket=self.bucket, Prefix=messages_prefix)
|
|
239
|
+
|
|
240
|
+
# Collect all message keys and extract their indices
|
|
241
|
+
message_index_keys: list[tuple[int, str]] = []
|
|
242
|
+
for page in pages:
|
|
243
|
+
if "Contents" in page:
|
|
244
|
+
for obj in page["Contents"]:
|
|
245
|
+
key = obj["Key"]
|
|
246
|
+
if key.endswith(".json") and MESSAGE_PREFIX in key:
|
|
247
|
+
# Extract the filename part from the full S3 key
|
|
248
|
+
filename = key.split("/")[-1]
|
|
249
|
+
# Extract index from message_<index>.json format
|
|
250
|
+
index = int(filename[len(MESSAGE_PREFIX) : -5]) # Remove prefix and .json suffix
|
|
251
|
+
message_index_keys.append((index, key))
|
|
252
|
+
|
|
253
|
+
# Sort by index and extract just the keys
|
|
254
|
+
message_keys = [k for _, k in sorted(message_index_keys)]
|
|
255
|
+
|
|
256
|
+
# Apply pagination to keys before loading content
|
|
257
|
+
if limit is not None:
|
|
258
|
+
message_keys = message_keys[offset : offset + limit]
|
|
259
|
+
else:
|
|
260
|
+
message_keys = message_keys[offset:]
|
|
261
|
+
|
|
262
|
+
# Load only the required message objects
|
|
263
|
+
messages: List[SessionMessage] = []
|
|
264
|
+
for key in message_keys:
|
|
265
|
+
message_data = self._read_s3_object(key)
|
|
266
|
+
if message_data:
|
|
267
|
+
messages.append(SessionMessage.from_dict(message_data))
|
|
268
|
+
|
|
269
|
+
return messages
|
|
270
|
+
|
|
271
|
+
except ClientError as e:
|
|
272
|
+
raise SessionException(f"S3 error reading messages: {e}") from e
|