agentkit-sdk-python 0.7.8__py3-none-any.whl → 0.7.10__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.
@@ -12,8 +12,10 @@
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
14
 
15
+ import inspect
15
16
  import json
16
17
  import logging
18
+ from collections.abc import AsyncIterator
17
19
  from contextlib import asynccontextmanager
18
20
  from typing import Any
19
21
 
@@ -62,6 +64,35 @@ from agentkit.apps.base_app import BaseAgentkitApp
62
64
  logger = logging.getLogger(__name__)
63
65
 
64
66
 
67
+ async def _call_lifecycle_handler(handler: Any) -> None:
68
+ result = handler()
69
+ if inspect.isawaitable(result):
70
+ await result
71
+
72
+
73
+ @asynccontextmanager
74
+ async def _run_a2a_app_lifespan(a2a_app: Any) -> AsyncIterator[None]:
75
+ router = getattr(a2a_app, "router", None)
76
+ if router is None:
77
+ raise RuntimeError("A2A server app has no router; cannot initialize lifecycle.")
78
+
79
+ lifespan_context = getattr(router, "lifespan_context", None)
80
+ if lifespan_context is not None:
81
+ async with lifespan_context(a2a_app):
82
+ yield
83
+ return
84
+
85
+ startup_handlers = tuple(getattr(router, "on_startup", ()) or ())
86
+ shutdown_handlers = tuple(getattr(router, "on_shutdown", ()) or ())
87
+ for handler in startup_handlers:
88
+ await _call_lifecycle_handler(handler)
89
+ try:
90
+ yield
91
+ finally:
92
+ for handler in shutdown_handlers:
93
+ await _call_lifecycle_handler(handler)
94
+
95
+
65
96
  class AgentKitAgentLoader(BaseAgentLoader):
66
97
  def __init__(self, agent_or_app: BaseAgent | App) -> None:
67
98
  super().__init__()
@@ -157,11 +188,10 @@ class AgentkitAgentServerApp(BaseAgentkitApp):
157
188
  async def lifespan(app: FastAPI):
158
189
  # trigger A2A server app startup
159
190
  logger.info(
160
- "Triggering A2A server app startup within API server..."
191
+ "Triggering A2A server app lifespan within API server..."
161
192
  )
162
- for handler in _a2a_server_app.router.on_startup:
163
- await handler()
164
- yield
193
+ async with _run_a2a_app_lifespan(_a2a_server_app):
194
+ yield
165
195
 
166
196
  resolved_allow_origins = resolve_agentkit_allow_origins(
167
197
  allow_origins=allow_origins,
@@ -280,78 +310,6 @@ class AgentkitAgentServerApp(BaseAgentkitApp):
280
310
  routes.insert(0, routes.pop(i))
281
311
  break
282
312
 
283
- @self.app.post("/run_sse")
284
- async def run_agent_sse(req: RunAgentRequest) -> StreamingResponse:
285
- print("my run sse !!!")
286
- # SSE endpoint
287
- session = await self.server.session_service.get_session(
288
- app_name=req.app_name,
289
- user_id=req.user_id,
290
- session_id=req.session_id,
291
- )
292
- if not session:
293
- raise HTTPException(status_code=404, detail="Session not found")
294
-
295
- # Convert the events to properly formatted SSE
296
- async def event_generator():
297
- try:
298
- stream_mode = (
299
- StreamingMode.SSE
300
- if req.streaming
301
- else StreamingMode.NONE
302
- )
303
- runner = await self.server.get_runner_async(req.app_name)
304
- async with Aclosing(
305
- runner.run_async(
306
- user_id=req.user_id,
307
- session_id=req.session_id,
308
- new_message=req.new_message,
309
- state_delta=req.state_delta,
310
- run_config=RunConfig(streaming_mode=stream_mode),
311
- invocation_id=req.invocation_id,
312
- )
313
- ) as agen:
314
- async for event in agen:
315
- # ADK Web renders artifacts from `actions.artifactDelta`
316
- # during part processing *and* during action processing
317
- # 1) the original event with `artifactDelta` cleared (content)
318
- # 2) a content-less "action-only" event carrying `artifactDelta`
319
- events_to_stream = [event]
320
- if (
321
- event.actions.artifact_delta
322
- and event.content
323
- and event.content.parts
324
- ):
325
- content_event = event.model_copy(deep=True)
326
- content_event.actions.artifact_delta = {}
327
- artifact_event = event.model_copy(deep=True)
328
- artifact_event.content = None
329
- events_to_stream = [
330
- content_event,
331
- artifact_event,
332
- ]
333
-
334
- for event_to_stream in events_to_stream:
335
- sse_event = event_to_stream.model_dump_json(
336
- exclude_none=True,
337
- by_alias=True,
338
- )
339
- logger.debug(
340
- "Generated event in agent run streaming: %s",
341
- sse_event,
342
- )
343
- yield f"data: {sse_event}\n\n"
344
- except Exception as e:
345
- logger.exception("Error in event_generator: %s", e)
346
- yield f"data: {json.dumps({'error': str(e)})}\n\n"
347
-
348
- # Returns a streaming response with the proper media type for SSE
349
-
350
- return StreamingResponse(
351
- event_generator(),
352
- media_type="text/event-stream",
353
- )
354
-
355
313
  # Attach ASGI middleware for unified telemetry across all routes
356
314
  self.app.add_middleware(AgentkitTelemetryHTTPMiddleware)
357
315
 
@@ -11,14 +11,20 @@ from agentkit.frameworks._common import (
11
11
 
12
12
  __all__ = [
13
13
  "FrameworkBridgeError",
14
+ "BedrockAgentCoreAgentkitBridge",
14
15
  "LangChainAgentkitBridge",
15
16
  "LangGraphAgentkitBridge",
17
+ "StrandsAgentkitBridge",
16
18
  "UnsupportedFrameworkAgentError",
17
19
  "load_entry_object",
18
20
  ]
19
21
 
20
22
 
21
23
  def __getattr__(name: str) -> Any:
24
+ if name == "BedrockAgentCoreAgentkitBridge":
25
+ from agentkit.frameworks.agentcore import BedrockAgentCoreAgentkitBridge
26
+
27
+ return BedrockAgentCoreAgentkitBridge
22
28
  if name == "LangChainAgentkitBridge":
23
29
  from agentkit.frameworks.langchain import LangChainAgentkitBridge
24
30
 
@@ -27,6 +33,10 @@ def __getattr__(name: str) -> Any:
27
33
  from agentkit.frameworks.langgraph import LangGraphAgentkitBridge
28
34
 
29
35
  return LangGraphAgentkitBridge
36
+ if name == "StrandsAgentkitBridge":
37
+ from agentkit.frameworks.strands import StrandsAgentkitBridge
38
+
39
+ return StrandsAgentkitBridge
30
40
  if name == "load_entry_object":
31
41
  from agentkit.frameworks.migration import load_entry_object
32
42
 
@@ -0,0 +1,415 @@
1
+ """Bedrock AgentCore Runtime adapters for AgentKit apps."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import inspect
6
+ import json
7
+ from collections.abc import AsyncGenerator, Iterable
8
+ from types import SimpleNamespace
9
+ from typing import Any
10
+ from uuid import uuid4
11
+
12
+ from fastapi import FastAPI, HTTPException, Request
13
+ from fastapi.responses import JSONResponse, Response, StreamingResponse
14
+ from google.adk.agents.base_agent import BaseAgent
15
+ from google.adk.agents.invocation_context import InvocationContext
16
+ from google.adk.events import Event
17
+ from google.genai import types
18
+
19
+ from agentkit.frameworks._common import (
20
+ UnsupportedFrameworkAgentError,
21
+ adk_event,
22
+ content_to_text,
23
+ json_text,
24
+ user_text,
25
+ )
26
+ from agentkit.frameworks.model_replacement import (
27
+ ARK_DEFAULT_BASE_URL,
28
+ apply_agentkit_model_replacement,
29
+ )
30
+
31
+
32
+ AGENTCORE_SESSION_HEADER = "X-Amzn-Bedrock-AgentCore-Runtime-Session-Id"
33
+ AGENTCORE_REQUEST_ID_HEADER = "X-Amzn-Bedrock-AgentCore-Runtime-Request-Id"
34
+ AGENTCORE_WORKLOAD_ACCESS_TOKEN_HEADER = "WorkloadAccessToken"
35
+ AGENTCORE_AUTHORIZATION_HEADER = "Authorization"
36
+
37
+
38
+ class _AgentCoreEntry:
39
+ def __init__(self, source: Any) -> None:
40
+ self.source = source
41
+ self.handler = _entrypoint_handler(source)
42
+
43
+ async def invoke(self, payload: Any, context: Any) -> Any:
44
+ args = (
45
+ (payload, context)
46
+ if _takes_context(self.handler)
47
+ else (payload,)
48
+ )
49
+ result = self.handler(*args)
50
+ if inspect.isawaitable(result):
51
+ result = await result
52
+ return result
53
+
54
+
55
+ def _entrypoint_handler(source: Any) -> Any:
56
+ handlers = getattr(source, "handlers", None)
57
+ if isinstance(handlers, dict) and callable(handlers.get("main")):
58
+ return handlers["main"]
59
+ if callable(source):
60
+ return source
61
+ raise UnsupportedFrameworkAgentError(
62
+ "Bedrock AgentCore entry must be a BedrockAgentCoreApp with an "
63
+ "@app.entrypoint handler, or a callable entrypoint function."
64
+ )
65
+
66
+
67
+ def _takes_context(handler: Any) -> bool:
68
+ try:
69
+ params = list(inspect.signature(handler).parameters.values())
70
+ except (TypeError, ValueError):
71
+ return False
72
+ if len(params) < 2:
73
+ return False
74
+ return params[1].name == "context"
75
+
76
+
77
+ def _parse_json_text(text: str) -> Any:
78
+ stripped = text.strip()
79
+ if not stripped.startswith(("{", "[")):
80
+ return None
81
+ try:
82
+ return json.loads(stripped)
83
+ except json.JSONDecodeError:
84
+ return None
85
+
86
+
87
+ def _payload_from_adk_text(text: str) -> Any:
88
+ parsed = _parse_json_text(text)
89
+ if parsed is not None:
90
+ return parsed
91
+ return {"prompt": text}
92
+
93
+
94
+ def _session_id(ctx: InvocationContext) -> str | None:
95
+ session = getattr(ctx, "session", None)
96
+ value = getattr(session, "id", None)
97
+ return str(value) if value else None
98
+
99
+
100
+ def _request_headers(
101
+ *,
102
+ request_id: str | None,
103
+ session_id: str | None,
104
+ source_headers: dict[str, str] | None = None,
105
+ ) -> dict[str, str]:
106
+ headers = dict(source_headers or {})
107
+ if request_id and not _has_header(headers, AGENTCORE_REQUEST_ID_HEADER):
108
+ headers.setdefault(AGENTCORE_REQUEST_ID_HEADER, request_id)
109
+ if session_id and not _has_header(headers, AGENTCORE_SESSION_HEADER):
110
+ headers.setdefault(AGENTCORE_SESSION_HEADER, session_id)
111
+ return headers
112
+
113
+
114
+ def _has_header(headers: dict[str, str], name: str) -> bool:
115
+ wanted = name.lower()
116
+ return any(key.lower() == wanted for key in headers)
117
+
118
+
119
+ def _get_header(headers: dict[str, str], name: str) -> str | None:
120
+ wanted = name.lower()
121
+ for key, value in headers.items():
122
+ if key.lower() == wanted:
123
+ return value
124
+ return None
125
+
126
+
127
+ def _set_agentcore_context(
128
+ *,
129
+ request_id: str | None,
130
+ session_id: str | None,
131
+ headers: dict[str, str],
132
+ ) -> None:
133
+ try:
134
+ from bedrock_agentcore.runtime.context import BedrockAgentCoreContext
135
+ except Exception:
136
+ return
137
+
138
+ BedrockAgentCoreContext.set_request_context(request_id or str(uuid4()), session_id)
139
+ if headers:
140
+ BedrockAgentCoreContext.set_request_headers(headers)
141
+ workload_token = _get_header(headers, AGENTCORE_WORKLOAD_ACCESS_TOKEN_HEADER)
142
+ if workload_token:
143
+ BedrockAgentCoreContext.set_workload_access_token(workload_token)
144
+
145
+
146
+ def _context_object(
147
+ *,
148
+ request_id: str | None,
149
+ session_id: str | None,
150
+ headers: dict[str, str],
151
+ request: Request | None = None,
152
+ ) -> Any:
153
+ _set_agentcore_context(
154
+ request_id=request_id,
155
+ session_id=session_id,
156
+ headers=headers,
157
+ )
158
+ try:
159
+ from bedrock_agentcore.runtime.context import RequestContext
160
+
161
+ return RequestContext(
162
+ session_id=session_id,
163
+ request_headers=headers or None,
164
+ request=request,
165
+ )
166
+ except Exception:
167
+ return SimpleNamespace(
168
+ session_id=session_id,
169
+ request_headers=headers or None,
170
+ request=request,
171
+ )
172
+
173
+
174
+ def _context_from_adk(ctx: InvocationContext) -> Any:
175
+ session_id = _session_id(ctx)
176
+ request_id = getattr(ctx, "invocation_id", None)
177
+ headers = _request_headers(request_id=request_id, session_id=session_id)
178
+ return _context_object(
179
+ request_id=request_id,
180
+ session_id=session_id,
181
+ headers=headers,
182
+ )
183
+
184
+
185
+ def _context_from_request(source: Any, request: Request) -> Any:
186
+ build_context = getattr(source, "_build_request_context", None)
187
+ if callable(build_context):
188
+ return build_context(request)
189
+
190
+ headers = dict(request.headers)
191
+ session_id = request.headers.get(AGENTCORE_SESSION_HEADER)
192
+ request_id = request.headers.get(AGENTCORE_REQUEST_ID_HEADER) or str(uuid4())
193
+ return _context_object(
194
+ request_id=request_id,
195
+ session_id=session_id,
196
+ headers=_request_headers(
197
+ request_id=request_id,
198
+ session_id=session_id,
199
+ source_headers=headers,
200
+ ),
201
+ request=request,
202
+ )
203
+
204
+
205
+ def _is_stream(value: Any) -> bool:
206
+ return inspect.isasyncgen(value) or inspect.isgenerator(value)
207
+
208
+
209
+ def _response_text(response: Response) -> str:
210
+ body = getattr(response, "body", b"")
211
+ if isinstance(body, bytes):
212
+ return body.decode("utf-8", errors="replace")
213
+ return content_to_text(body)
214
+
215
+
216
+ def _agentcore_chunk_text(value: Any) -> str:
217
+ if isinstance(value, Response):
218
+ return _response_text(value)
219
+ if isinstance(value, (bytes, bytearray)):
220
+ return value.decode("utf-8", errors="replace")
221
+ if isinstance(value, str):
222
+ return value
223
+ if isinstance(value, dict):
224
+ event_type = value.get("type")
225
+ if event_type == "response.output_text.delta":
226
+ return content_to_text(value.get("delta"))
227
+ if event_type == "response.thinking":
228
+ return ""
229
+ if event_type == "response.failed":
230
+ return content_to_text(value.get("message") or value.get("reason"))
231
+ if isinstance(event_type, str) and event_type.startswith("response."):
232
+ return ""
233
+ for key in ("delta", "text", "content", "output", "answer", "message", "result"):
234
+ if key in value:
235
+ text = content_to_text(value[key])
236
+ if text:
237
+ return text
238
+ return json_text(value)
239
+ return content_to_text(value)
240
+
241
+
242
+ async def _iterate_stream(value: Any) -> AsyncGenerator[Any, None]:
243
+ if inspect.isasyncgen(value):
244
+ async for item in value:
245
+ yield item
246
+ return
247
+ if inspect.isgenerator(value):
248
+ for item in value:
249
+ yield item
250
+ return
251
+ yield value
252
+
253
+
254
+ def _safe_json(value: Any) -> str:
255
+ try:
256
+ return json.dumps(value, ensure_ascii=False, default=str)
257
+ except Exception:
258
+ return json.dumps(str(value), ensure_ascii=False)
259
+
260
+
261
+ def _sse_bytes(value: Any) -> bytes:
262
+ return f"data: {_safe_json(value)}\n\n".encode("utf-8")
263
+
264
+
265
+ async def _async_sse_stream(value: Any) -> AsyncGenerator[bytes, None]:
266
+ try:
267
+ async for item in _iterate_stream(value):
268
+ yield _sse_bytes(item)
269
+ except Exception as exc:
270
+ yield _sse_bytes(
271
+ {
272
+ "error": str(exc),
273
+ "error_type": type(exc).__name__,
274
+ "message": "An error occurred during streaming",
275
+ }
276
+ )
277
+
278
+
279
+ def _json_response(value: Any) -> Response:
280
+ if isinstance(value, Response):
281
+ return value
282
+ return Response(_safe_json(value), media_type="application/json")
283
+
284
+
285
+ class BedrockAgentCoreAgentkitBridge(BaseAgent):
286
+ """Adapt a Bedrock AgentCore entrypoint to AgentKit's ADK runtime."""
287
+
288
+ def __init__(
289
+ self,
290
+ source: Any,
291
+ *,
292
+ name: str = "bedrock_agentcore_agent",
293
+ description: str = "Bedrock AgentCore entrypoint adapted for AgentKit runtime",
294
+ ) -> None:
295
+ super().__init__(name=name, description=description)
296
+ self._entry = _AgentCoreEntry(source)
297
+
298
+ async def _run_async_impl(
299
+ self,
300
+ ctx: InvocationContext,
301
+ ) -> AsyncGenerator[Event, None]:
302
+ payload = _payload_from_adk_text(user_text(ctx))
303
+ context = _context_from_adk(ctx)
304
+ result = await self._entry.invoke(payload, context)
305
+ accumulated = ""
306
+
307
+ if _is_stream(result):
308
+ async for item in _iterate_stream(result):
309
+ text = _agentcore_chunk_text(item)
310
+ if text:
311
+ accumulated += text
312
+ yield adk_event(ctx, self.name, text, partial=True)
313
+ else:
314
+ accumulated = _agentcore_chunk_text(result)
315
+
316
+ if accumulated:
317
+ yield Event(
318
+ invocation_id=ctx.invocation_id,
319
+ author=self.name,
320
+ branch=ctx.branch,
321
+ partial=False,
322
+ content=types.Content(
323
+ role="model",
324
+ parts=[types.Part(text=accumulated)],
325
+ ),
326
+ )
327
+
328
+
329
+ def _normalize_prefix(prefix: str) -> str:
330
+ if prefix == "/":
331
+ return ""
332
+ if prefix and not prefix.startswith("/"):
333
+ raise ValueError("AgentCore compatibility prefix must start with '/'.")
334
+ return prefix.rstrip("/")
335
+
336
+
337
+ def _path(prefix: str, path: str) -> str:
338
+ return f"{prefix}{path}" if prefix else path
339
+
340
+
341
+ def _promote_routes(app: FastAPI, paths: Iterable[str]) -> None:
342
+ target_paths = set(paths)
343
+ routes = app.router.routes
344
+ promoted = []
345
+ remaining = []
346
+ for route in routes:
347
+ if getattr(route, "path", None) in target_paths:
348
+ promoted.append(route)
349
+ else:
350
+ remaining.append(route)
351
+
352
+ insert_at = len(remaining)
353
+ for index, route in enumerate(remaining):
354
+ if getattr(route, "path", None) in {"", "/"}:
355
+ insert_at = index
356
+ break
357
+ routes[:] = remaining[:insert_at] + promoted + remaining[insert_at:]
358
+
359
+
360
+ def attach_bedrock_agentcore_compat_routes(
361
+ app: FastAPI,
362
+ source: Any,
363
+ *,
364
+ prefix: str = "",
365
+ ) -> None:
366
+ """Attach Bedrock AgentCore-compatible /invocations and /ping routes."""
367
+
368
+ normalized_prefix = _normalize_prefix(prefix)
369
+ entry = _AgentCoreEntry(source)
370
+ invocations_path = _path(normalized_prefix, "/invocations")
371
+ ping_path = _path(normalized_prefix, "/ping")
372
+
373
+ @app.post(invocations_path)
374
+ async def _agentcore_invocations(request: Request):
375
+ try:
376
+ payload = await request.json()
377
+ except Exception as exc:
378
+ raise HTTPException(
379
+ status_code=400,
380
+ detail="Request body must be valid JSON.",
381
+ ) from exc
382
+
383
+ task_response = None
384
+ if isinstance(payload, dict):
385
+ handle_task_action = getattr(source, "_handle_task_action", None)
386
+ if callable(handle_task_action):
387
+ task_response = handle_task_action(payload)
388
+ if task_response is not None:
389
+ return task_response
390
+
391
+ context = _context_from_request(source, request)
392
+ result = await entry.invoke(payload, context)
393
+ if _is_stream(result):
394
+ return StreamingResponse(
395
+ _async_sse_stream(result),
396
+ media_type="text/event-stream",
397
+ )
398
+ return _json_response(result)
399
+
400
+ @app.get(ping_path)
401
+ async def _agentcore_ping():
402
+ get_status = getattr(source, "get_current_ping_status", None)
403
+ if callable(get_status):
404
+ status = get_status()
405
+ return JSONResponse(
406
+ {
407
+ "status": getattr(status, "value", status),
408
+ "time_of_last_update": int(
409
+ getattr(source, "_last_status_update_time", 0)
410
+ ),
411
+ }
412
+ )
413
+ return JSONResponse({"status": "Healthy"})
414
+
415
+ _promote_routes(app, [invocations_path, ping_path])