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,319 @@
|
|
|
1
|
+
"""Utilities for handling streaming responses from language models."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
from typing import Any, AsyncGenerator, AsyncIterable, Optional
|
|
6
|
+
|
|
7
|
+
from ..models.model import Model
|
|
8
|
+
from ..types.content import ContentBlock, Message, Messages
|
|
9
|
+
from ..types.streaming import (
|
|
10
|
+
ContentBlockDeltaEvent,
|
|
11
|
+
ContentBlockStart,
|
|
12
|
+
ContentBlockStartEvent,
|
|
13
|
+
MessageStartEvent,
|
|
14
|
+
MessageStopEvent,
|
|
15
|
+
MetadataEvent,
|
|
16
|
+
Metrics,
|
|
17
|
+
RedactContentEvent,
|
|
18
|
+
StopReason,
|
|
19
|
+
StreamEvent,
|
|
20
|
+
Usage,
|
|
21
|
+
)
|
|
22
|
+
from ..types.tools import ToolSpec, ToolUse
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def remove_blank_messages_content_text(messages: Messages) -> Messages:
|
|
28
|
+
"""Remove or replace blank text in message content.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
messages: Conversation messages to update.
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
Updated messages.
|
|
35
|
+
"""
|
|
36
|
+
removed_blank_message_content_text = False
|
|
37
|
+
replaced_blank_message_content_text = False
|
|
38
|
+
|
|
39
|
+
for message in messages:
|
|
40
|
+
# only modify assistant messages
|
|
41
|
+
if "role" in message and message["role"] != "assistant":
|
|
42
|
+
continue
|
|
43
|
+
|
|
44
|
+
if "content" in message:
|
|
45
|
+
content = message["content"]
|
|
46
|
+
has_tool_use = any("toolUse" in item for item in content)
|
|
47
|
+
|
|
48
|
+
if has_tool_use:
|
|
49
|
+
# Remove blank 'text' items for assistant messages
|
|
50
|
+
before_len = len(content)
|
|
51
|
+
content[:] = [item for item in content if "text" not in item or item["text"].strip()]
|
|
52
|
+
if not removed_blank_message_content_text and before_len != len(content):
|
|
53
|
+
removed_blank_message_content_text = True
|
|
54
|
+
else:
|
|
55
|
+
# Replace blank 'text' with '[blank text]' for assistant messages
|
|
56
|
+
for item in content:
|
|
57
|
+
if "text" in item and not item["text"].strip():
|
|
58
|
+
replaced_blank_message_content_text = True
|
|
59
|
+
item["text"] = "[blank text]"
|
|
60
|
+
|
|
61
|
+
if removed_blank_message_content_text:
|
|
62
|
+
logger.debug("removed blank message context text")
|
|
63
|
+
if replaced_blank_message_content_text:
|
|
64
|
+
logger.debug("replaced blank message context text")
|
|
65
|
+
|
|
66
|
+
return messages
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def handle_message_start(event: MessageStartEvent, message: Message) -> Message:
|
|
70
|
+
"""Handles the start of a message by setting the role in the message dictionary.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
event: A message start event.
|
|
74
|
+
message: The message dictionary being constructed.
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
Updated message dictionary with the role set.
|
|
78
|
+
"""
|
|
79
|
+
message["role"] = event["role"]
|
|
80
|
+
return message
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def handle_content_block_start(event: ContentBlockStartEvent) -> dict[str, Any]:
|
|
84
|
+
"""Handles the start of a content block by extracting tool usage information if any.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
event: Start event.
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
Dictionary with tool use id and name if tool use request, empty dictionary otherwise.
|
|
91
|
+
"""
|
|
92
|
+
start: ContentBlockStart = event["start"]
|
|
93
|
+
current_tool_use = {}
|
|
94
|
+
|
|
95
|
+
if "toolUse" in start and start["toolUse"]:
|
|
96
|
+
tool_use_data = start["toolUse"]
|
|
97
|
+
current_tool_use["toolUseId"] = tool_use_data["toolUseId"]
|
|
98
|
+
current_tool_use["name"] = tool_use_data["name"]
|
|
99
|
+
current_tool_use["input"] = ""
|
|
100
|
+
|
|
101
|
+
return current_tool_use
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def handle_content_block_delta(
|
|
105
|
+
event: ContentBlockDeltaEvent, state: dict[str, Any]
|
|
106
|
+
) -> tuple[dict[str, Any], dict[str, Any]]:
|
|
107
|
+
"""Handles content block delta updates by appending text, tool input, or reasoning content to the state.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
event: Delta event.
|
|
111
|
+
state: The current state of message processing.
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
Updated state with appended text or tool input.
|
|
115
|
+
"""
|
|
116
|
+
delta_content = event["delta"]
|
|
117
|
+
|
|
118
|
+
callback_event = {}
|
|
119
|
+
|
|
120
|
+
if "toolUse" in delta_content:
|
|
121
|
+
if "input" not in state["current_tool_use"]:
|
|
122
|
+
state["current_tool_use"]["input"] = ""
|
|
123
|
+
|
|
124
|
+
state["current_tool_use"]["input"] += delta_content["toolUse"]["input"]
|
|
125
|
+
callback_event["callback"] = {"delta": delta_content, "current_tool_use": state["current_tool_use"]}
|
|
126
|
+
|
|
127
|
+
elif "text" in delta_content:
|
|
128
|
+
state["text"] += delta_content["text"]
|
|
129
|
+
callback_event["callback"] = {"data": delta_content["text"], "delta": delta_content}
|
|
130
|
+
|
|
131
|
+
elif "reasoningContent" in delta_content:
|
|
132
|
+
if "text" in delta_content["reasoningContent"]:
|
|
133
|
+
if "reasoningText" not in state:
|
|
134
|
+
state["reasoningText"] = ""
|
|
135
|
+
|
|
136
|
+
state["reasoningText"] += delta_content["reasoningContent"]["text"]
|
|
137
|
+
callback_event["callback"] = {
|
|
138
|
+
"reasoningText": delta_content["reasoningContent"]["text"],
|
|
139
|
+
"delta": delta_content,
|
|
140
|
+
"reasoning": True,
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
elif "signature" in delta_content["reasoningContent"]:
|
|
144
|
+
if "signature" not in state:
|
|
145
|
+
state["signature"] = ""
|
|
146
|
+
|
|
147
|
+
state["signature"] += delta_content["reasoningContent"]["signature"]
|
|
148
|
+
callback_event["callback"] = {
|
|
149
|
+
"reasoning_signature": delta_content["reasoningContent"]["signature"],
|
|
150
|
+
"delta": delta_content,
|
|
151
|
+
"reasoning": True,
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return state, callback_event
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def handle_content_block_stop(state: dict[str, Any]) -> dict[str, Any]:
|
|
158
|
+
"""Handles the end of a content block by finalizing tool usage, text content, or reasoning content.
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
state: The current state of message processing.
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
Updated state with finalized content block.
|
|
165
|
+
"""
|
|
166
|
+
content: list[ContentBlock] = state["content"]
|
|
167
|
+
|
|
168
|
+
current_tool_use = state["current_tool_use"]
|
|
169
|
+
text = state["text"]
|
|
170
|
+
reasoning_text = state["reasoningText"]
|
|
171
|
+
|
|
172
|
+
if current_tool_use:
|
|
173
|
+
if "input" not in current_tool_use:
|
|
174
|
+
current_tool_use["input"] = ""
|
|
175
|
+
|
|
176
|
+
try:
|
|
177
|
+
current_tool_use["input"] = json.loads(current_tool_use["input"])
|
|
178
|
+
except ValueError:
|
|
179
|
+
current_tool_use["input"] = {}
|
|
180
|
+
|
|
181
|
+
tool_use_id = current_tool_use["toolUseId"]
|
|
182
|
+
tool_use_name = current_tool_use["name"]
|
|
183
|
+
|
|
184
|
+
tool_use = ToolUse(
|
|
185
|
+
toolUseId=tool_use_id,
|
|
186
|
+
name=tool_use_name,
|
|
187
|
+
input=current_tool_use["input"],
|
|
188
|
+
)
|
|
189
|
+
content.append({"toolUse": tool_use})
|
|
190
|
+
state["current_tool_use"] = {}
|
|
191
|
+
|
|
192
|
+
elif text:
|
|
193
|
+
content.append({"text": text})
|
|
194
|
+
state["text"] = ""
|
|
195
|
+
|
|
196
|
+
elif reasoning_text:
|
|
197
|
+
content.append(
|
|
198
|
+
{
|
|
199
|
+
"reasoningContent": {
|
|
200
|
+
"reasoningText": {
|
|
201
|
+
"text": state["reasoningText"],
|
|
202
|
+
"signature": state["signature"],
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
)
|
|
207
|
+
state["reasoningText"] = ""
|
|
208
|
+
|
|
209
|
+
return state
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def handle_message_stop(event: MessageStopEvent) -> StopReason:
|
|
213
|
+
"""Handles the end of a message by returning the stop reason.
|
|
214
|
+
|
|
215
|
+
Args:
|
|
216
|
+
event: Stop event.
|
|
217
|
+
|
|
218
|
+
Returns:
|
|
219
|
+
The reason for stopping the stream.
|
|
220
|
+
"""
|
|
221
|
+
return event["stopReason"]
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def handle_redact_content(event: RedactContentEvent, state: dict[str, Any]) -> None:
|
|
225
|
+
"""Handles redacting content from the input or output.
|
|
226
|
+
|
|
227
|
+
Args:
|
|
228
|
+
event: Redact Content Event.
|
|
229
|
+
state: The current state of message processing.
|
|
230
|
+
"""
|
|
231
|
+
if event.get("redactAssistantContentMessage") is not None:
|
|
232
|
+
state["message"]["content"] = [{"text": event["redactAssistantContentMessage"]}]
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def extract_usage_metrics(event: MetadataEvent) -> tuple[Usage, Metrics]:
|
|
236
|
+
"""Extracts usage metrics from the metadata chunk.
|
|
237
|
+
|
|
238
|
+
Args:
|
|
239
|
+
event: metadata.
|
|
240
|
+
|
|
241
|
+
Returns:
|
|
242
|
+
The extracted usage metrics and latency.
|
|
243
|
+
"""
|
|
244
|
+
usage = Usage(**event["usage"])
|
|
245
|
+
metrics = Metrics(**event["metrics"])
|
|
246
|
+
|
|
247
|
+
return usage, metrics
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
async def process_stream(chunks: AsyncIterable[StreamEvent]) -> AsyncGenerator[dict[str, Any], None]:
|
|
251
|
+
"""Processes the response stream from the API, constructing the final message and extracting usage metrics.
|
|
252
|
+
|
|
253
|
+
Args:
|
|
254
|
+
chunks: The chunks of the response stream from the model.
|
|
255
|
+
|
|
256
|
+
Yields:
|
|
257
|
+
The reason for stopping, the constructed message, and the usage metrics.
|
|
258
|
+
"""
|
|
259
|
+
stop_reason: StopReason = "end_turn"
|
|
260
|
+
|
|
261
|
+
state: dict[str, Any] = {
|
|
262
|
+
"message": {"role": "assistant", "content": []},
|
|
263
|
+
"text": "",
|
|
264
|
+
"current_tool_use": {},
|
|
265
|
+
"reasoningText": "",
|
|
266
|
+
"signature": "",
|
|
267
|
+
}
|
|
268
|
+
state["content"] = state["message"]["content"]
|
|
269
|
+
|
|
270
|
+
usage: Usage = Usage(inputTokens=0, outputTokens=0, totalTokens=0)
|
|
271
|
+
metrics: Metrics = Metrics(latencyMs=0)
|
|
272
|
+
|
|
273
|
+
async for chunk in chunks:
|
|
274
|
+
yield {"callback": {"event": chunk}}
|
|
275
|
+
|
|
276
|
+
if "messageStart" in chunk:
|
|
277
|
+
state["message"] = handle_message_start(chunk["messageStart"], state["message"])
|
|
278
|
+
elif "contentBlockStart" in chunk:
|
|
279
|
+
state["current_tool_use"] = handle_content_block_start(chunk["contentBlockStart"])
|
|
280
|
+
elif "contentBlockDelta" in chunk:
|
|
281
|
+
state, callback_event = handle_content_block_delta(chunk["contentBlockDelta"], state)
|
|
282
|
+
yield callback_event
|
|
283
|
+
elif "contentBlockStop" in chunk:
|
|
284
|
+
state = handle_content_block_stop(state)
|
|
285
|
+
elif "messageStop" in chunk:
|
|
286
|
+
stop_reason = handle_message_stop(chunk["messageStop"])
|
|
287
|
+
elif "metadata" in chunk:
|
|
288
|
+
usage, metrics = extract_usage_metrics(chunk["metadata"])
|
|
289
|
+
elif "redactContent" in chunk:
|
|
290
|
+
handle_redact_content(chunk["redactContent"], state)
|
|
291
|
+
|
|
292
|
+
yield {"stop": (stop_reason, state["message"], usage, metrics)}
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
async def stream_messages(
|
|
296
|
+
model: Model,
|
|
297
|
+
system_prompt: Optional[str],
|
|
298
|
+
messages: Messages,
|
|
299
|
+
tool_specs: list[ToolSpec],
|
|
300
|
+
) -> AsyncGenerator[dict[str, Any], None]:
|
|
301
|
+
"""Streams messages to the model and processes the response.
|
|
302
|
+
|
|
303
|
+
Args:
|
|
304
|
+
model: Model provider.
|
|
305
|
+
system_prompt: The system prompt to send.
|
|
306
|
+
messages: List of messages to send.
|
|
307
|
+
tool_specs: The list of tool specs.
|
|
308
|
+
|
|
309
|
+
Yields:
|
|
310
|
+
The reason for stopping, the final message, and the usage metrics
|
|
311
|
+
"""
|
|
312
|
+
logger.debug("model=<%s> | streaming messages", model)
|
|
313
|
+
|
|
314
|
+
messages = remove_blank_messages_content_text(messages)
|
|
315
|
+
|
|
316
|
+
chunks = model.stream(messages, tool_specs if tool_specs else None, system_prompt)
|
|
317
|
+
|
|
318
|
+
async for event in process_stream(chunks):
|
|
319
|
+
yield event
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Experimental hook functionality that has not yet reached stability."""
|
|
2
|
+
|
|
3
|
+
from .events import (
|
|
4
|
+
AfterModelInvocationEvent,
|
|
5
|
+
AfterToolInvocationEvent,
|
|
6
|
+
BeforeModelInvocationEvent,
|
|
7
|
+
BeforeToolInvocationEvent,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"BeforeToolInvocationEvent",
|
|
12
|
+
"AfterToolInvocationEvent",
|
|
13
|
+
"BeforeModelInvocationEvent",
|
|
14
|
+
"AfterModelInvocationEvent",
|
|
15
|
+
]
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""Experimental hook events emitted as part of invoking Agents.
|
|
2
|
+
|
|
3
|
+
This module defines the events that are emitted as Agents run through the lifecycle of a request.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import Any, Optional
|
|
8
|
+
|
|
9
|
+
from ...hooks import HookEvent
|
|
10
|
+
from ...types.content import Message
|
|
11
|
+
from ...types.streaming import StopReason
|
|
12
|
+
from ...types.tools import AgentTool, ToolResult, ToolUse
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class BeforeToolInvocationEvent(HookEvent):
|
|
17
|
+
"""Event triggered before a tool is invoked.
|
|
18
|
+
|
|
19
|
+
This event is fired just before the agent executes a tool, allowing hook
|
|
20
|
+
providers to inspect, modify, or replace the tool that will be executed.
|
|
21
|
+
The selected_tool can be modified by hook callbacks to change which tool
|
|
22
|
+
gets executed.
|
|
23
|
+
|
|
24
|
+
Attributes:
|
|
25
|
+
selected_tool: The tool that will be invoked. Can be modified by hooks
|
|
26
|
+
to change which tool gets executed. This may be None if tool lookup failed.
|
|
27
|
+
tool_use: The tool parameters that will be passed to selected_tool.
|
|
28
|
+
invocation_state: Keyword arguments that will be passed to the tool.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
selected_tool: Optional[AgentTool]
|
|
32
|
+
tool_use: ToolUse
|
|
33
|
+
invocation_state: dict[str, Any]
|
|
34
|
+
|
|
35
|
+
def _can_write(self, name: str) -> bool:
|
|
36
|
+
return name in ["selected_tool", "tool_use"]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass
|
|
40
|
+
class AfterToolInvocationEvent(HookEvent):
|
|
41
|
+
"""Event triggered after a tool invocation completes.
|
|
42
|
+
|
|
43
|
+
This event is fired after the agent has finished executing a tool,
|
|
44
|
+
regardless of whether the execution was successful or resulted in an error.
|
|
45
|
+
Hook providers can use this event for cleanup, logging, or post-processing.
|
|
46
|
+
|
|
47
|
+
Note: This event uses reverse callback ordering, meaning callbacks registered
|
|
48
|
+
later will be invoked first during cleanup.
|
|
49
|
+
|
|
50
|
+
Attributes:
|
|
51
|
+
selected_tool: The tool that was invoked. It may be None if tool lookup failed.
|
|
52
|
+
tool_use: The tool parameters that were passed to the tool invoked.
|
|
53
|
+
invocation_state: Keyword arguments that were passed to the tool
|
|
54
|
+
result: The result of the tool invocation. Either a ToolResult on success
|
|
55
|
+
or an Exception if the tool execution failed.
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
selected_tool: Optional[AgentTool]
|
|
59
|
+
tool_use: ToolUse
|
|
60
|
+
invocation_state: dict[str, Any]
|
|
61
|
+
result: ToolResult
|
|
62
|
+
exception: Optional[Exception] = None
|
|
63
|
+
|
|
64
|
+
def _can_write(self, name: str) -> bool:
|
|
65
|
+
return name == "result"
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def should_reverse_callbacks(self) -> bool:
|
|
69
|
+
"""True to invoke callbacks in reverse order."""
|
|
70
|
+
return True
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@dataclass
|
|
74
|
+
class BeforeModelInvocationEvent(HookEvent):
|
|
75
|
+
"""Event triggered before the model is invoked.
|
|
76
|
+
|
|
77
|
+
This event is fired just before the agent calls the model for inference,
|
|
78
|
+
allowing hook providers to inspect or modify the messages and configuration
|
|
79
|
+
that will be sent to the model.
|
|
80
|
+
|
|
81
|
+
Note: This event is not fired for invocations to structured_output.
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
pass
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@dataclass
|
|
88
|
+
class AfterModelInvocationEvent(HookEvent):
|
|
89
|
+
"""Event triggered after the model invocation completes.
|
|
90
|
+
|
|
91
|
+
This event is fired after the agent has finished calling the model,
|
|
92
|
+
regardless of whether the invocation was successful or resulted in an error.
|
|
93
|
+
Hook providers can use this event for cleanup, logging, or post-processing.
|
|
94
|
+
|
|
95
|
+
Note: This event uses reverse callback ordering, meaning callbacks registered
|
|
96
|
+
later will be invoked first during cleanup.
|
|
97
|
+
|
|
98
|
+
Note: This event is not fired for invocations to structured_output.
|
|
99
|
+
|
|
100
|
+
Attributes:
|
|
101
|
+
stop_response: The model response data if invocation was successful, None if failed.
|
|
102
|
+
exception: Exception if the model invocation failed, None if successful.
|
|
103
|
+
"""
|
|
104
|
+
|
|
105
|
+
@dataclass
|
|
106
|
+
class ModelStopResponse:
|
|
107
|
+
"""Model response data from successful invocation.
|
|
108
|
+
|
|
109
|
+
Attributes:
|
|
110
|
+
stop_reason: The reason the model stopped generating.
|
|
111
|
+
message: The generated message from the model.
|
|
112
|
+
"""
|
|
113
|
+
|
|
114
|
+
message: Message
|
|
115
|
+
stop_reason: StopReason
|
|
116
|
+
|
|
117
|
+
stop_response: Optional[ModelStopResponse] = None
|
|
118
|
+
exception: Optional[Exception] = None
|
|
119
|
+
|
|
120
|
+
@property
|
|
121
|
+
def should_reverse_callbacks(self) -> bool:
|
|
122
|
+
"""True to invoke callbacks in reverse order."""
|
|
123
|
+
return True
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""Various handlers for performing custom actions on agent state.
|
|
2
|
+
|
|
3
|
+
Examples include:
|
|
4
|
+
|
|
5
|
+
- Displaying events from the event stream
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .callback_handler import CompositeCallbackHandler, PrintingCallbackHandler, null_callback_handler
|
|
9
|
+
|
|
10
|
+
__all__ = ["CompositeCallbackHandler", "null_callback_handler", "PrintingCallbackHandler"]
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""This module provides handlers for formatting and displaying events from the agent."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class PrintingCallbackHandler:
|
|
8
|
+
"""Handler for streaming text output and tool invocations to stdout."""
|
|
9
|
+
|
|
10
|
+
def __init__(self) -> None:
|
|
11
|
+
"""Initialize handler."""
|
|
12
|
+
self.tool_count = 0
|
|
13
|
+
self.previous_tool_use = None
|
|
14
|
+
|
|
15
|
+
def __call__(self, **kwargs: Any) -> None:
|
|
16
|
+
"""Stream text output and tool invocations to stdout.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
**kwargs: Callback event data including:
|
|
20
|
+
- reasoningText (Optional[str]): Reasoning text to print if provided.
|
|
21
|
+
- data (str): Text content to stream.
|
|
22
|
+
- complete (bool): Whether this is the final chunk of a response.
|
|
23
|
+
- current_tool_use (dict): Information about the current tool being used.
|
|
24
|
+
"""
|
|
25
|
+
reasoningText = kwargs.get("reasoningText", False)
|
|
26
|
+
data = kwargs.get("data", "")
|
|
27
|
+
complete = kwargs.get("complete", False)
|
|
28
|
+
current_tool_use = kwargs.get("current_tool_use", {})
|
|
29
|
+
|
|
30
|
+
if reasoningText:
|
|
31
|
+
print(reasoningText, end="")
|
|
32
|
+
|
|
33
|
+
if data:
|
|
34
|
+
print(data, end="" if not complete else "\n")
|
|
35
|
+
|
|
36
|
+
if current_tool_use and current_tool_use.get("name"):
|
|
37
|
+
tool_name = current_tool_use.get("name", "Unknown tool")
|
|
38
|
+
if self.previous_tool_use != current_tool_use:
|
|
39
|
+
self.previous_tool_use = current_tool_use
|
|
40
|
+
self.tool_count += 1
|
|
41
|
+
print(f"\nTool #{self.tool_count}: {tool_name}")
|
|
42
|
+
|
|
43
|
+
if complete and data:
|
|
44
|
+
print("\n")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class CompositeCallbackHandler:
|
|
48
|
+
"""Class-based callback handler that combines multiple callback handlers.
|
|
49
|
+
|
|
50
|
+
This handler allows multiple callback handlers to be invoked for the same events,
|
|
51
|
+
enabling different processing or output formats for the same stream data.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
def __init__(self, *handlers: Callable) -> None:
|
|
55
|
+
"""Initialize handler."""
|
|
56
|
+
self.handlers = handlers
|
|
57
|
+
|
|
58
|
+
def __call__(self, **kwargs: Any) -> None:
|
|
59
|
+
"""Invoke all handlers in the chain."""
|
|
60
|
+
for handler in self.handlers:
|
|
61
|
+
handler(**kwargs)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def null_callback_handler(**_kwargs: Any) -> None:
|
|
65
|
+
"""Callback handler that discards all output.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
**_kwargs: Event data (ignored).
|
|
69
|
+
"""
|
|
70
|
+
return None
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Typed hook system for extending agent functionality.
|
|
2
|
+
|
|
3
|
+
This module provides a composable mechanism for building objects that can hook
|
|
4
|
+
into specific events during the agent lifecycle. The hook system enables both
|
|
5
|
+
built-in SDK components and user code to react to or modify agent behavior
|
|
6
|
+
through strongly-typed event callbacks.
|
|
7
|
+
|
|
8
|
+
Example Usage:
|
|
9
|
+
```python
|
|
10
|
+
from strands.hooks import HookProvider, HookRegistry
|
|
11
|
+
from strands.hooks.events import StartRequestEvent, EndRequestEvent
|
|
12
|
+
|
|
13
|
+
class LoggingHooks(HookProvider):
|
|
14
|
+
def register_hooks(self, registry: HookRegistry) -> None:
|
|
15
|
+
registry.add_callback(StartRequestEvent, self.log_start)
|
|
16
|
+
registry.add_callback(EndRequestEvent, self.log_end)
|
|
17
|
+
|
|
18
|
+
def log_start(self, event: StartRequestEvent) -> None:
|
|
19
|
+
print(f"Request started for {event.agent.name}")
|
|
20
|
+
|
|
21
|
+
def log_end(self, event: EndRequestEvent) -> None:
|
|
22
|
+
print(f"Request completed for {event.agent.name}")
|
|
23
|
+
|
|
24
|
+
# Use with agent
|
|
25
|
+
agent = Agent(hooks=[LoggingHooks()])
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
This replaces the older callback_handler approach with a more composable,
|
|
29
|
+
type-safe system that supports multiple subscribers per event type.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
from .events import (
|
|
33
|
+
AfterInvocationEvent,
|
|
34
|
+
AgentInitializedEvent,
|
|
35
|
+
BeforeInvocationEvent,
|
|
36
|
+
MessageAddedEvent,
|
|
37
|
+
)
|
|
38
|
+
from .registry import HookCallback, HookEvent, HookProvider, HookRegistry
|
|
39
|
+
|
|
40
|
+
__all__ = [
|
|
41
|
+
"AgentInitializedEvent",
|
|
42
|
+
"BeforeInvocationEvent",
|
|
43
|
+
"AfterInvocationEvent",
|
|
44
|
+
"MessageAddedEvent",
|
|
45
|
+
"HookEvent",
|
|
46
|
+
"HookProvider",
|
|
47
|
+
"HookCallback",
|
|
48
|
+
"HookRegistry",
|
|
49
|
+
]
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Hook events emitted as part of invoking Agents.
|
|
2
|
+
|
|
3
|
+
This module defines the events that are emitted as Agents run through the lifecycle of a request.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
|
|
8
|
+
from ..types.content import Message
|
|
9
|
+
from .registry import HookEvent
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class AgentInitializedEvent(HookEvent):
|
|
14
|
+
"""Event triggered when an agent has finished initialization.
|
|
15
|
+
|
|
16
|
+
This event is fired after the agent has been fully constructed and all
|
|
17
|
+
built-in components have been initialized. Hook providers can use this
|
|
18
|
+
event to perform setup tasks that require a fully initialized agent.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class BeforeInvocationEvent(HookEvent):
|
|
26
|
+
"""Event triggered at the beginning of a new agent request.
|
|
27
|
+
|
|
28
|
+
This event is fired before the agent begins processing a new user request,
|
|
29
|
+
before any model inference or tool execution occurs. Hook providers can
|
|
30
|
+
use this event to perform request-level setup, logging, or validation.
|
|
31
|
+
|
|
32
|
+
This event is triggered at the beginning of the following api calls:
|
|
33
|
+
- Agent.__call__
|
|
34
|
+
- Agent.stream_async
|
|
35
|
+
- Agent.structured_output
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
pass
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass
|
|
42
|
+
class AfterInvocationEvent(HookEvent):
|
|
43
|
+
"""Event triggered at the end of an agent request.
|
|
44
|
+
|
|
45
|
+
This event is fired after the agent has completed processing a request,
|
|
46
|
+
regardless of whether it completed successfully or encountered an error.
|
|
47
|
+
Hook providers can use this event for cleanup, logging, or state persistence.
|
|
48
|
+
|
|
49
|
+
Note: This event uses reverse callback ordering, meaning callbacks registered
|
|
50
|
+
later will be invoked first during cleanup.
|
|
51
|
+
|
|
52
|
+
This event is triggered at the end of the following api calls:
|
|
53
|
+
- Agent.__call__
|
|
54
|
+
- Agent.stream_async
|
|
55
|
+
- Agent.structured_output
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def should_reverse_callbacks(self) -> bool:
|
|
60
|
+
"""True to invoke callbacks in reverse order."""
|
|
61
|
+
return True
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass
|
|
65
|
+
class MessageAddedEvent(HookEvent):
|
|
66
|
+
"""Event triggered when a message is added to the agent's conversation.
|
|
67
|
+
|
|
68
|
+
This event is fired whenever the agent adds a new message to its internal
|
|
69
|
+
message history, including user messages, assistant responses, and tool
|
|
70
|
+
results. Hook providers can use this event for logging, monitoring, or
|
|
71
|
+
implementing custom message processing logic.
|
|
72
|
+
|
|
73
|
+
Note: This event is only triggered for messages added by the framework
|
|
74
|
+
itself, not for messages manually added by tools or external code.
|
|
75
|
+
|
|
76
|
+
Attributes:
|
|
77
|
+
message: The message that was added to the conversation history.
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
message: Message
|