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,473 @@
|
|
|
1
|
+
"""Bedrock AgentCore base implementation.
|
|
2
|
+
|
|
3
|
+
Provides a Starlette-based web server that wraps user functions as HTTP endpoints.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import asyncio
|
|
7
|
+
import contextvars
|
|
8
|
+
import inspect
|
|
9
|
+
import json
|
|
10
|
+
import logging
|
|
11
|
+
import threading
|
|
12
|
+
import time
|
|
13
|
+
import uuid
|
|
14
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
15
|
+
from typing import Any, Callable, Dict, Optional
|
|
16
|
+
|
|
17
|
+
from starlette.applications import Starlette
|
|
18
|
+
from starlette.responses import JSONResponse, Response, StreamingResponse
|
|
19
|
+
from starlette.routing import Route
|
|
20
|
+
|
|
21
|
+
from .context import BedrockAgentCoreContext, RequestContext
|
|
22
|
+
from .models import (
|
|
23
|
+
ACCESS_TOKEN_HEADER,
|
|
24
|
+
SESSION_HEADER,
|
|
25
|
+
TASK_ACTION_CLEAR_FORCED_STATUS,
|
|
26
|
+
TASK_ACTION_FORCE_BUSY,
|
|
27
|
+
TASK_ACTION_FORCE_HEALTHY,
|
|
28
|
+
TASK_ACTION_JOB_STATUS,
|
|
29
|
+
TASK_ACTION_PING_STATUS,
|
|
30
|
+
PingStatus,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
# Request context for logging
|
|
34
|
+
request_id_context: contextvars.ContextVar[Optional[str]] = contextvars.ContextVar("request_id", default=None)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class RequestContextFormatter(logging.Formatter):
|
|
38
|
+
"""Custom formatter that includes request ID in log messages."""
|
|
39
|
+
|
|
40
|
+
def format(self, record):
|
|
41
|
+
"""Format log record with request ID context."""
|
|
42
|
+
request_id = request_id_context.get()
|
|
43
|
+
if request_id:
|
|
44
|
+
record.request_id = f"[{request_id}] "
|
|
45
|
+
else:
|
|
46
|
+
record.request_id = ""
|
|
47
|
+
return super().format(record)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class BedrockAgentCoreApp(Starlette):
|
|
51
|
+
"""Bedrock AgentCore application class that extends Starlette for AI agent deployment."""
|
|
52
|
+
|
|
53
|
+
def __init__(self, debug: bool = False):
|
|
54
|
+
"""Initialize Bedrock AgentCore application.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
debug: Enable debug actions for task management (default: False)
|
|
58
|
+
"""
|
|
59
|
+
self.handlers: Dict[str, Callable] = {}
|
|
60
|
+
self._ping_handler: Optional[Callable] = None
|
|
61
|
+
self._active_tasks: Dict[int, Dict[str, Any]] = {}
|
|
62
|
+
self._task_counter_lock: threading.Lock = threading.Lock()
|
|
63
|
+
self._forced_ping_status: Optional[PingStatus] = None
|
|
64
|
+
self._last_status_update_time: float = time.time()
|
|
65
|
+
self._invocation_executor = ThreadPoolExecutor(max_workers=2, thread_name_prefix="invocation")
|
|
66
|
+
self._invocation_semaphore = asyncio.Semaphore(2)
|
|
67
|
+
|
|
68
|
+
routes = [
|
|
69
|
+
Route("/invocations", self._handle_invocation, methods=["POST"]),
|
|
70
|
+
Route("/ping", self._handle_ping, methods=["GET"]),
|
|
71
|
+
]
|
|
72
|
+
super().__init__(routes=routes)
|
|
73
|
+
self.debug = debug # Set after super().__init__ to avoid override
|
|
74
|
+
|
|
75
|
+
self.logger = logging.getLogger("bedrock_agentcore.app")
|
|
76
|
+
if not self.logger.handlers:
|
|
77
|
+
handler = logging.StreamHandler()
|
|
78
|
+
formatter = RequestContextFormatter("%(asctime)s - %(name)s - %(levelname)s - %(request_id)s%(message)s")
|
|
79
|
+
handler.setFormatter(formatter)
|
|
80
|
+
self.logger.addHandler(handler)
|
|
81
|
+
self.logger.setLevel(logging.INFO)
|
|
82
|
+
|
|
83
|
+
def entrypoint(self, func: Callable) -> Callable:
|
|
84
|
+
"""Decorator to register a function as the main entrypoint.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
func: The function to register as entrypoint
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
The decorated function with added serve method
|
|
91
|
+
"""
|
|
92
|
+
self.handlers["main"] = func
|
|
93
|
+
func.run = lambda port=8080, host=None: self.run(port, host)
|
|
94
|
+
return func
|
|
95
|
+
|
|
96
|
+
def ping(self, func: Callable) -> Callable:
|
|
97
|
+
"""Decorator to register a custom ping status handler.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
func: The function to register as ping status handler
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
The decorated function
|
|
104
|
+
"""
|
|
105
|
+
self._ping_handler = func
|
|
106
|
+
return func
|
|
107
|
+
|
|
108
|
+
def async_task(self, func: Callable) -> Callable:
|
|
109
|
+
"""Decorator to track async tasks for ping status.
|
|
110
|
+
|
|
111
|
+
When a function is decorated with @async_task, it will:
|
|
112
|
+
- Set ping status to HEALTHY_BUSY while running
|
|
113
|
+
- Revert to HEALTHY when complete
|
|
114
|
+
"""
|
|
115
|
+
if not asyncio.iscoroutinefunction(func):
|
|
116
|
+
raise ValueError("@async_task can only be applied to async functions")
|
|
117
|
+
|
|
118
|
+
async def wrapper(*args, **kwargs):
|
|
119
|
+
task_id = self.add_async_task(func.__name__)
|
|
120
|
+
|
|
121
|
+
try:
|
|
122
|
+
self.logger.debug("Starting async task: %s", func.__name__)
|
|
123
|
+
start_time = time.time()
|
|
124
|
+
result = await func(*args, **kwargs)
|
|
125
|
+
duration = time.time() - start_time
|
|
126
|
+
self.logger.info("Async task completed: %s (%.3fs)", func.__name__, duration)
|
|
127
|
+
return result
|
|
128
|
+
except Exception as e:
|
|
129
|
+
duration = time.time() - start_time
|
|
130
|
+
self.logger.error(
|
|
131
|
+
"Async task failed: %s (%.3fs) - %s: %s", func.__name__, duration, type(e).__name__, e
|
|
132
|
+
)
|
|
133
|
+
raise
|
|
134
|
+
finally:
|
|
135
|
+
self.complete_async_task(task_id)
|
|
136
|
+
|
|
137
|
+
wrapper.__name__ = func.__name__
|
|
138
|
+
return wrapper
|
|
139
|
+
|
|
140
|
+
def get_current_ping_status(self) -> PingStatus:
|
|
141
|
+
"""Get current ping status (forced > custom > automatic)."""
|
|
142
|
+
current_status = None
|
|
143
|
+
|
|
144
|
+
if self._forced_ping_status is not None:
|
|
145
|
+
current_status = self._forced_ping_status
|
|
146
|
+
elif self._ping_handler:
|
|
147
|
+
try:
|
|
148
|
+
result = self._ping_handler()
|
|
149
|
+
if isinstance(result, str):
|
|
150
|
+
current_status = PingStatus(result)
|
|
151
|
+
else:
|
|
152
|
+
current_status = result
|
|
153
|
+
except Exception as e:
|
|
154
|
+
self.logger.warning(
|
|
155
|
+
"Custom ping handler failed, falling back to automatic: %s: %s", type(e).__name__, e
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
if current_status is None:
|
|
159
|
+
current_status = PingStatus.HEALTHY_BUSY if self._active_tasks else PingStatus.HEALTHY
|
|
160
|
+
if not hasattr(self, "_last_known_status") or self._last_known_status != current_status:
|
|
161
|
+
self._last_known_status = current_status
|
|
162
|
+
self._last_status_update_time = time.time()
|
|
163
|
+
|
|
164
|
+
return current_status
|
|
165
|
+
|
|
166
|
+
def force_ping_status(self, status: PingStatus):
|
|
167
|
+
"""Force ping status to a specific value."""
|
|
168
|
+
self._forced_ping_status = status
|
|
169
|
+
|
|
170
|
+
def clear_forced_ping_status(self):
|
|
171
|
+
"""Clear forced status and resume automatic."""
|
|
172
|
+
self._forced_ping_status = None
|
|
173
|
+
|
|
174
|
+
def get_async_task_info(self) -> Dict[str, Any]:
|
|
175
|
+
"""Get info about running async tasks."""
|
|
176
|
+
running_jobs = []
|
|
177
|
+
for t in self._active_tasks.values():
|
|
178
|
+
try:
|
|
179
|
+
running_jobs.append(
|
|
180
|
+
{"name": t.get("name", "unknown"), "duration": time.time() - t.get("start_time", time.time())}
|
|
181
|
+
)
|
|
182
|
+
except Exception as e:
|
|
183
|
+
self.logger.warning("Caught exception, continuing...: %s", e)
|
|
184
|
+
continue
|
|
185
|
+
|
|
186
|
+
return {"active_count": len(self._active_tasks), "running_jobs": running_jobs}
|
|
187
|
+
|
|
188
|
+
def add_async_task(self, name: str, metadata: Optional[Dict] = None) -> int:
|
|
189
|
+
"""Register an async task for interactive health tracking.
|
|
190
|
+
|
|
191
|
+
This method provides granular control over async task lifecycle,
|
|
192
|
+
allowing developers to interactively start tracking tasks for health monitoring.
|
|
193
|
+
Use this when you need precise control over when tasks begin and end.
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
name: Human-readable task name for monitoring
|
|
197
|
+
metadata: Optional additional task metadata
|
|
198
|
+
|
|
199
|
+
Returns:
|
|
200
|
+
Task ID for tracking and completion
|
|
201
|
+
|
|
202
|
+
Example:
|
|
203
|
+
task_id = app.add_async_task("file_processing", {"file": "data.csv"})
|
|
204
|
+
# ... do background work ...
|
|
205
|
+
app.complete_async_task(task_id)
|
|
206
|
+
"""
|
|
207
|
+
with self._task_counter_lock:
|
|
208
|
+
task_id = hash(str(uuid.uuid4())) # Generate truly unique hash-based ID
|
|
209
|
+
|
|
210
|
+
# Register task start with same structure as @async_task decorator
|
|
211
|
+
task_info = {"name": name, "start_time": time.time()}
|
|
212
|
+
if metadata:
|
|
213
|
+
task_info["metadata"] = metadata
|
|
214
|
+
|
|
215
|
+
self._active_tasks[task_id] = task_info
|
|
216
|
+
|
|
217
|
+
self.logger.info("Async task started: %s (ID: %s)", name, task_id)
|
|
218
|
+
return task_id
|
|
219
|
+
|
|
220
|
+
def complete_async_task(self, task_id: int) -> bool:
|
|
221
|
+
"""Mark an async task as complete for interactive health tracking.
|
|
222
|
+
|
|
223
|
+
This method provides granular control over async task lifecycle,
|
|
224
|
+
allowing developers to interactively complete tasks for health monitoring.
|
|
225
|
+
Call this when your background work finishes.
|
|
226
|
+
|
|
227
|
+
Args:
|
|
228
|
+
task_id: Task ID returned from add_async_task
|
|
229
|
+
|
|
230
|
+
Returns:
|
|
231
|
+
True if task was found and completed, False otherwise
|
|
232
|
+
|
|
233
|
+
Example:
|
|
234
|
+
task_id = app.add_async_task("file_processing")
|
|
235
|
+
# ... do background work ...
|
|
236
|
+
completed = app.complete_async_task(task_id)
|
|
237
|
+
"""
|
|
238
|
+
with self._task_counter_lock:
|
|
239
|
+
task_info = self._active_tasks.pop(task_id, None)
|
|
240
|
+
if task_info:
|
|
241
|
+
task_name = task_info.get("name", "unknown")
|
|
242
|
+
duration = time.time() - task_info.get("start_time", time.time())
|
|
243
|
+
|
|
244
|
+
self.logger.info("Async task completed: %s (ID: %s, Duration: %.2fs)", task_name, task_id, duration)
|
|
245
|
+
return True
|
|
246
|
+
else:
|
|
247
|
+
self.logger.warning("Attempted to complete unknown task ID: %s", task_id)
|
|
248
|
+
return False
|
|
249
|
+
|
|
250
|
+
def _build_request_context(self, request) -> RequestContext:
|
|
251
|
+
"""Build request context and setup auth if present."""
|
|
252
|
+
try:
|
|
253
|
+
agent_identity_token = request.headers.get(ACCESS_TOKEN_HEADER) or request.headers.get(
|
|
254
|
+
ACCESS_TOKEN_HEADER.lower()
|
|
255
|
+
)
|
|
256
|
+
if agent_identity_token:
|
|
257
|
+
BedrockAgentCoreContext.set_workload_access_token(agent_identity_token)
|
|
258
|
+
session_id = request.headers.get(SESSION_HEADER) or request.headers.get(SESSION_HEADER.lower())
|
|
259
|
+
return RequestContext(session_id=session_id)
|
|
260
|
+
except Exception as e:
|
|
261
|
+
self.logger.warning("Failed to build request context: %s: %s", type(e).__name__, e)
|
|
262
|
+
return RequestContext(session_id=None)
|
|
263
|
+
|
|
264
|
+
def _takes_context(self, handler: Callable) -> bool:
|
|
265
|
+
try:
|
|
266
|
+
params = list(inspect.signature(handler).parameters.keys())
|
|
267
|
+
return len(params) >= 2 and params[1] == "context"
|
|
268
|
+
except Exception:
|
|
269
|
+
return False
|
|
270
|
+
|
|
271
|
+
async def _handle_invocation(self, request):
|
|
272
|
+
request_id = str(uuid.uuid4())[:8]
|
|
273
|
+
request_id_context.set(request_id)
|
|
274
|
+
start_time = time.time()
|
|
275
|
+
|
|
276
|
+
try:
|
|
277
|
+
payload = await request.json()
|
|
278
|
+
self.logger.debug("Processing invocation request")
|
|
279
|
+
|
|
280
|
+
if self.debug:
|
|
281
|
+
task_response = self._handle_task_action(payload)
|
|
282
|
+
if task_response:
|
|
283
|
+
duration = time.time() - start_time
|
|
284
|
+
self.logger.info("Debug action completed (%.3fs)", duration)
|
|
285
|
+
return task_response
|
|
286
|
+
|
|
287
|
+
handler = self.handlers.get("main")
|
|
288
|
+
if not handler:
|
|
289
|
+
self.logger.error("No entrypoint defined")
|
|
290
|
+
return JSONResponse({"error": "No entrypoint defined"}, status_code=500)
|
|
291
|
+
|
|
292
|
+
request_context = self._build_request_context(request)
|
|
293
|
+
takes_context = self._takes_context(handler)
|
|
294
|
+
|
|
295
|
+
handler_name = handler.__name__ if hasattr(handler, "__name__") else "unknown"
|
|
296
|
+
self.logger.debug("Invoking handler: %s", handler_name)
|
|
297
|
+
result = await self._invoke_handler(handler, request_context, takes_context, payload)
|
|
298
|
+
|
|
299
|
+
duration = time.time() - start_time
|
|
300
|
+
if inspect.isgenerator(result):
|
|
301
|
+
self.logger.info("Returning streaming response (generator) (%.3fs)", duration)
|
|
302
|
+
return StreamingResponse(self._sync_stream_with_error_handling(result), media_type="text/event-stream")
|
|
303
|
+
elif inspect.isasyncgen(result):
|
|
304
|
+
self.logger.info("Returning streaming response (async generator) (%.3fs)", duration)
|
|
305
|
+
return StreamingResponse(self._stream_with_error_handling(result), media_type="text/event-stream")
|
|
306
|
+
|
|
307
|
+
self.logger.info("Invocation completed successfully (%.3fs)", duration)
|
|
308
|
+
# Use safe serialization for consistency with streaming paths
|
|
309
|
+
safe_json_string = self._safe_serialize_to_json_string(result)
|
|
310
|
+
return Response(safe_json_string, media_type="application/json")
|
|
311
|
+
|
|
312
|
+
except json.JSONDecodeError as e:
|
|
313
|
+
duration = time.time() - start_time
|
|
314
|
+
self.logger.warning("Invalid JSON in request (%.3fs): %s", duration, e)
|
|
315
|
+
return JSONResponse({"error": "Invalid JSON", "details": str(e)}, status_code=400)
|
|
316
|
+
except Exception as e:
|
|
317
|
+
duration = time.time() - start_time
|
|
318
|
+
self.logger.exception("Invocation failed (%.3fs)", duration)
|
|
319
|
+
return JSONResponse({"error": str(e)}, status_code=500)
|
|
320
|
+
|
|
321
|
+
def _handle_ping(self, request):
|
|
322
|
+
try:
|
|
323
|
+
status = self.get_current_ping_status()
|
|
324
|
+
self.logger.debug("Ping request - status: %s", status.value)
|
|
325
|
+
return JSONResponse({"status": status.value, "time_of_last_update": int(self._last_status_update_time)})
|
|
326
|
+
except Exception as e:
|
|
327
|
+
self.logger.error("Ping endpoint failed: %s: %s", type(e).__name__, e)
|
|
328
|
+
return JSONResponse({"status": PingStatus.HEALTHY.value, "time_of_last_update": int(time.time())})
|
|
329
|
+
|
|
330
|
+
def run(self, port: int = 8080, host: Optional[str] = None):
|
|
331
|
+
"""Start the Bedrock AgentCore server.
|
|
332
|
+
|
|
333
|
+
Args:
|
|
334
|
+
port: Port to serve on, defaults to 8080
|
|
335
|
+
host: Host to bind to, auto-detected if None
|
|
336
|
+
"""
|
|
337
|
+
import os
|
|
338
|
+
|
|
339
|
+
import uvicorn
|
|
340
|
+
|
|
341
|
+
if host is None:
|
|
342
|
+
if os.path.exists("/.dockerenv") or os.environ.get("DOCKER_CONTAINER"):
|
|
343
|
+
host = "0.0.0.0" # nosec B104 - Docker needs this to expose the port
|
|
344
|
+
else:
|
|
345
|
+
host = "127.0.0.1"
|
|
346
|
+
uvicorn.run(self, host=host, port=port)
|
|
347
|
+
|
|
348
|
+
async def _invoke_handler(self, handler, request_context, takes_context, payload):
|
|
349
|
+
if self._invocation_semaphore.locked():
|
|
350
|
+
return JSONResponse({"error": "Server busy - maximum concurrent requests reached"}, status_code=503)
|
|
351
|
+
|
|
352
|
+
async with self._invocation_semaphore:
|
|
353
|
+
try:
|
|
354
|
+
args = (payload, request_context) if takes_context else (payload,)
|
|
355
|
+
if asyncio.iscoroutinefunction(handler):
|
|
356
|
+
return await handler(*args)
|
|
357
|
+
else:
|
|
358
|
+
loop = asyncio.get_event_loop()
|
|
359
|
+
return await loop.run_in_executor(self._invocation_executor, handler, *args)
|
|
360
|
+
except Exception as e:
|
|
361
|
+
handler_name = getattr(handler, "__name__", "unknown")
|
|
362
|
+
self.logger.error("Handler '%s' execution failed: %s: %s", handler_name, type(e).__name__, e)
|
|
363
|
+
raise
|
|
364
|
+
|
|
365
|
+
def _handle_task_action(self, payload: dict) -> Optional[JSONResponse]:
|
|
366
|
+
"""Handle task management actions if present in payload."""
|
|
367
|
+
action = payload.get("_agent_core_app_action")
|
|
368
|
+
if not action:
|
|
369
|
+
return None
|
|
370
|
+
|
|
371
|
+
self.logger.debug("Processing debug action: %s", action)
|
|
372
|
+
|
|
373
|
+
try:
|
|
374
|
+
actions = {
|
|
375
|
+
TASK_ACTION_PING_STATUS: lambda: JSONResponse(
|
|
376
|
+
{
|
|
377
|
+
"status": self.get_current_ping_status().value,
|
|
378
|
+
"time_of_last_update": int(self._last_status_update_time),
|
|
379
|
+
}
|
|
380
|
+
),
|
|
381
|
+
TASK_ACTION_JOB_STATUS: lambda: JSONResponse(self.get_async_task_info()),
|
|
382
|
+
TASK_ACTION_FORCE_HEALTHY: lambda: (
|
|
383
|
+
self.force_ping_status(PingStatus.HEALTHY),
|
|
384
|
+
self.logger.info("Ping status forced to Healthy"),
|
|
385
|
+
JSONResponse({"forced_status": "Healthy"}),
|
|
386
|
+
)[2],
|
|
387
|
+
TASK_ACTION_FORCE_BUSY: lambda: (
|
|
388
|
+
self.force_ping_status(PingStatus.HEALTHY_BUSY),
|
|
389
|
+
self.logger.info("Ping status forced to HealthyBusy"),
|
|
390
|
+
JSONResponse({"forced_status": "HealthyBusy"}),
|
|
391
|
+
)[2],
|
|
392
|
+
TASK_ACTION_CLEAR_FORCED_STATUS: lambda: (
|
|
393
|
+
self.clear_forced_ping_status(),
|
|
394
|
+
self.logger.info("Forced ping status cleared"),
|
|
395
|
+
JSONResponse({"forced_status": "Cleared"}),
|
|
396
|
+
)[2],
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
if action in actions:
|
|
400
|
+
response = actions[action]()
|
|
401
|
+
self.logger.debug("Debug action '%s' completed successfully", action)
|
|
402
|
+
return response
|
|
403
|
+
|
|
404
|
+
self.logger.warning("Unknown debug action requested: %s", action)
|
|
405
|
+
return JSONResponse({"error": f"Unknown action: {action}"}, status_code=400)
|
|
406
|
+
|
|
407
|
+
except Exception as e:
|
|
408
|
+
self.logger.error("Debug action '%s' failed: %s: %s", action, type(e).__name__, e)
|
|
409
|
+
return JSONResponse({"error": "Debug action failed", "details": str(e)}, status_code=500)
|
|
410
|
+
|
|
411
|
+
async def _stream_with_error_handling(self, generator):
|
|
412
|
+
"""Wrap async generator to handle errors and convert to SSE format."""
|
|
413
|
+
try:
|
|
414
|
+
async for value in generator:
|
|
415
|
+
yield self._convert_to_sse(value)
|
|
416
|
+
except Exception as e:
|
|
417
|
+
self.logger.error("Error in async streaming: %s: %s", type(e).__name__, e)
|
|
418
|
+
error_event = {
|
|
419
|
+
"error": str(e),
|
|
420
|
+
"error_type": type(e).__name__,
|
|
421
|
+
"message": "An error occurred during streaming",
|
|
422
|
+
}
|
|
423
|
+
yield self._convert_to_sse(error_event)
|
|
424
|
+
|
|
425
|
+
def _safe_serialize_to_json_string(self, obj):
|
|
426
|
+
"""Safely serialize object directly to JSON string with progressive fallback handling.
|
|
427
|
+
|
|
428
|
+
This method eliminates double JSON encoding by returning the JSON string directly,
|
|
429
|
+
avoiding the test-then-encode pattern that leads to redundant json.dumps() calls.
|
|
430
|
+
Used by both streaming and non-streaming responses for consistent behavior.
|
|
431
|
+
|
|
432
|
+
Returns:
|
|
433
|
+
str: JSON string representation of the object
|
|
434
|
+
"""
|
|
435
|
+
try:
|
|
436
|
+
# First attempt: direct JSON serialization with Unicode support
|
|
437
|
+
return json.dumps(obj, ensure_ascii=False)
|
|
438
|
+
except (TypeError, ValueError, UnicodeEncodeError):
|
|
439
|
+
try:
|
|
440
|
+
# Second attempt: convert to string, then JSON encode the string
|
|
441
|
+
return json.dumps(str(obj), ensure_ascii=False)
|
|
442
|
+
except Exception as e:
|
|
443
|
+
# Final fallback: JSON encode error object with ASCII fallback for problematic Unicode
|
|
444
|
+
self.logger.warning("Failed to serialize object: %s: %s", type(e).__name__, e)
|
|
445
|
+
error_obj = {"error": "Serialization failed", "original_type": type(obj).__name__}
|
|
446
|
+
return json.dumps(error_obj, ensure_ascii=False)
|
|
447
|
+
|
|
448
|
+
def _convert_to_sse(self, obj) -> bytes:
|
|
449
|
+
"""Convert object to Server-Sent Events format using safe serialization.
|
|
450
|
+
|
|
451
|
+
Args:
|
|
452
|
+
obj: Object to convert to SSE format
|
|
453
|
+
|
|
454
|
+
Returns:
|
|
455
|
+
bytes: SSE-formatted data ready for streaming
|
|
456
|
+
"""
|
|
457
|
+
json_string = self._safe_serialize_to_json_string(obj)
|
|
458
|
+
sse_data = f"data: {json_string}\n\n"
|
|
459
|
+
return sse_data.encode("utf-8")
|
|
460
|
+
|
|
461
|
+
def _sync_stream_with_error_handling(self, generator):
|
|
462
|
+
"""Wrap sync generator to handle errors and convert to SSE format."""
|
|
463
|
+
try:
|
|
464
|
+
for value in generator:
|
|
465
|
+
yield self._convert_to_sse(value)
|
|
466
|
+
except Exception as e:
|
|
467
|
+
self.logger.error("Error in sync streaming: %s: %s", type(e).__name__, e)
|
|
468
|
+
error_event = {
|
|
469
|
+
"error": str(e),
|
|
470
|
+
"error_type": type(e).__name__,
|
|
471
|
+
"message": "An error occurred during streaming",
|
|
472
|
+
}
|
|
473
|
+
yield self._convert_to_sse(error_event)
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Request context models for Bedrock AgentCore Server.
|
|
2
|
+
|
|
3
|
+
Contains metadata extracted from HTTP requests that handlers can optionally access.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from contextvars import ContextVar
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
from pydantic import BaseModel, Field
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class RequestContext(BaseModel):
|
|
13
|
+
"""Request context containing metadata from HTTP requests."""
|
|
14
|
+
|
|
15
|
+
session_id: Optional[str] = Field(None)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class BedrockAgentCoreContext:
|
|
19
|
+
"""Context manager for Bedrock AgentCore."""
|
|
20
|
+
|
|
21
|
+
_workload_access_token: ContextVar[str] = ContextVar("workload_access_token")
|
|
22
|
+
|
|
23
|
+
@classmethod
|
|
24
|
+
def set_workload_access_token(cls, token: str):
|
|
25
|
+
"""Set the workload access token in the context."""
|
|
26
|
+
cls._workload_access_token.set(token)
|
|
27
|
+
|
|
28
|
+
@classmethod
|
|
29
|
+
def get_workload_access_token(cls) -> Optional[str]:
|
|
30
|
+
"""Get the workload access token from the context."""
|
|
31
|
+
try:
|
|
32
|
+
return cls._workload_access_token.get()
|
|
33
|
+
except LookupError:
|
|
34
|
+
return None
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Models for BedrockAgentCore runtime.
|
|
2
|
+
|
|
3
|
+
Contains data models and enums used throughout the runtime system.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from enum import Enum
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class PingStatus(str, Enum):
|
|
10
|
+
"""Ping status enum for health check responses."""
|
|
11
|
+
|
|
12
|
+
HEALTHY = "Healthy"
|
|
13
|
+
HEALTHY_BUSY = "HealthyBusy"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# Header constants
|
|
17
|
+
SESSION_HEADER = "X-Amzn-Bedrock-AgentCore-Runtime-Session-Id"
|
|
18
|
+
ACCESS_TOKEN_HEADER = "WorkloadAccessToken" # nosec
|
|
19
|
+
|
|
20
|
+
# Task action constants
|
|
21
|
+
TASK_ACTION_PING_STATUS = "ping_status"
|
|
22
|
+
TASK_ACTION_JOB_STATUS = "job_status"
|
|
23
|
+
TASK_ACTION_FORCE_HEALTHY = "force_healthy"
|
|
24
|
+
TASK_ACTION_FORCE_BUSY = "force_busy"
|
|
25
|
+
TASK_ACTION_CLEAR_FORCED_STATUS = "clear_forced_status"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""External service integrations for BedrockAgentCore Runtime SDK."""
|