agentkit-sdk-python 0.7.2__py3-none-any.whl → 0.7.4__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.
Files changed (30) hide show
  1. agentkit/auth/model_login.py +87 -105
  2. agentkit/frameworks/__init__.py +29 -0
  3. agentkit/frameworks/_common.py +136 -0
  4. agentkit/frameworks/langchain.py +170 -0
  5. agentkit/frameworks/langgraph.py +349 -0
  6. agentkit/frameworks/serving/__init__.py +6 -0
  7. agentkit/frameworks/serving/fastapi_mount.py +65 -0
  8. agentkit/frameworks/serving/langserve.py +502 -0
  9. agentkit/toolkit/cli/sandbox/a2a_client.py +407 -0
  10. agentkit/toolkit/cli/sandbox/agentkit_client.py +248 -0
  11. agentkit/toolkit/cli/sandbox/cli.py +5 -0
  12. agentkit/toolkit/cli/sandbox/cli_create.py +219 -165
  13. agentkit/toolkit/cli/sandbox/cli_exec.py +2 -70
  14. agentkit/toolkit/cli/sandbox/cli_file.py +1 -1
  15. agentkit/toolkit/cli/sandbox/cli_get.py +2 -6
  16. agentkit/toolkit/cli/sandbox/cli_invoke.py +375 -0
  17. agentkit/toolkit/cli/sandbox/cli_model_login.py +57 -70
  18. agentkit/toolkit/cli/sandbox/cli_mount.py +1 -1
  19. agentkit/toolkit/cli/sandbox/env_config.py +798 -0
  20. agentkit/toolkit/cli/sandbox/model_config.py +90 -29
  21. agentkit/toolkit/cli/sandbox/session_create.py +94 -168
  22. agentkit/toolkit/cli/sandbox/session_sync.py +1 -1
  23. agentkit/toolkit/cli/sandbox/tool_resolve.py +37 -73
  24. agentkit/version.py +1 -1
  25. {agentkit_sdk_python-0.7.2.dist-info → agentkit_sdk_python-0.7.4.dist-info}/METADATA +8 -1
  26. {agentkit_sdk_python-0.7.2.dist-info → agentkit_sdk_python-0.7.4.dist-info}/RECORD +30 -19
  27. {agentkit_sdk_python-0.7.2.dist-info → agentkit_sdk_python-0.7.4.dist-info}/WHEEL +0 -0
  28. {agentkit_sdk_python-0.7.2.dist-info → agentkit_sdk_python-0.7.4.dist-info}/entry_points.txt +0 -0
  29. {agentkit_sdk_python-0.7.2.dist-info → agentkit_sdk_python-0.7.4.dist-info}/licenses/LICENSE +0 -0
  30. {agentkit_sdk_python-0.7.2.dist-info → agentkit_sdk_python-0.7.4.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,502 @@
1
+ """LangServe-like HTTP compatibility routes for migrated LangChain agents."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import inspect
7
+ import json
8
+ import logging
9
+ from collections.abc import AsyncGenerator
10
+ from typing import Any
11
+ from uuid import uuid4
12
+
13
+ from fastapi import FastAPI, HTTPException, Request
14
+ from fastapi.encoders import jsonable_encoder
15
+ from fastapi.responses import JSONResponse, StreamingResponse
16
+
17
+ from agentkit.frameworks._common import (
18
+ UnsupportedFrameworkAgentError,
19
+ chunk_to_text,
20
+ is_input_shape_error,
21
+ maybe_await,
22
+ )
23
+
24
+ logger = logging.getLogger(__name__)
25
+
26
+ try:
27
+ from langchain_core.messages import HumanMessage
28
+ except ImportError: # pragma: no cover - optional dependency.
29
+ HumanMessage = None # type: ignore[assignment]
30
+
31
+
32
+ def _normalize_prefix(prefix: str) -> str:
33
+ if prefix == "/":
34
+ return ""
35
+ if prefix and not prefix.startswith("/"):
36
+ raise ValueError("LangServe compatibility prefix must start with '/'.")
37
+ return prefix.rstrip("/")
38
+
39
+
40
+ def _jsonable(value: Any) -> Any:
41
+ try:
42
+ return jsonable_encoder(value)
43
+ except Exception:
44
+ return json.loads(json.dumps(value, ensure_ascii=False, default=str))
45
+
46
+
47
+ async def _request_json(request: Request) -> Any:
48
+ try:
49
+ return await request.json()
50
+ except Exception as exc:
51
+ raise HTTPException(status_code=400, detail="Request body must be valid JSON.") from exc
52
+
53
+
54
+ def _extract_input(payload: Any) -> Any:
55
+ if isinstance(payload, dict) and "input" in payload:
56
+ return payload["input"]
57
+ return payload
58
+
59
+
60
+ def _request_options(payload: Any) -> tuple[Any, dict[str, Any]]:
61
+ if not isinstance(payload, dict):
62
+ return None, {}
63
+ config = payload.get("config")
64
+ kwargs = payload.get("kwargs")
65
+ return config, kwargs if isinstance(kwargs, dict) else {}
66
+
67
+
68
+ def _input_candidates(value: Any, input_key: str) -> list[Any]:
69
+ candidates: list[Any] = []
70
+ if isinstance(value, dict):
71
+ candidates.append(value)
72
+ elif input_key != "input":
73
+ candidates.append({input_key: value})
74
+ candidates.append(value)
75
+ else:
76
+ candidates.append(value)
77
+ candidates.append({input_key: value})
78
+
79
+ if isinstance(value, str) and HumanMessage is not None:
80
+ message = HumanMessage(content=value)
81
+ candidates.extend(([message], {"messages": [message]}))
82
+
83
+ deduped: list[Any] = []
84
+ for candidate in candidates:
85
+ if not any(candidate == existing for existing in deduped):
86
+ deduped.append(candidate)
87
+ return deduped
88
+
89
+
90
+ def _method_kwargs(method: Any, config: Any, extra_kwargs: dict[str, Any]) -> dict[str, Any]:
91
+ if config is None and not extra_kwargs:
92
+ return {}
93
+
94
+ try:
95
+ signature = inspect.signature(method)
96
+ except (TypeError, ValueError):
97
+ return {"config": config, **extra_kwargs} if config is not None else dict(extra_kwargs)
98
+
99
+ parameters = signature.parameters
100
+ accepts_kwargs = any(param.kind == inspect.Parameter.VAR_KEYWORD for param in parameters.values())
101
+ call_kwargs: dict[str, Any] = {}
102
+ if config is not None and (accepts_kwargs or "config" in parameters):
103
+ call_kwargs["config"] = config
104
+ for key, value in extra_kwargs.items():
105
+ if accepts_kwargs or key in parameters:
106
+ call_kwargs[key] = value
107
+ return call_kwargs
108
+
109
+
110
+ async def _call_with_candidates(
111
+ runnable: Any,
112
+ method_name: str,
113
+ value: Any,
114
+ *,
115
+ input_key: str,
116
+ config: Any = None,
117
+ extra_kwargs: dict[str, Any] | None = None,
118
+ ) -> Any:
119
+ method = getattr(runnable, method_name, None)
120
+ if not callable(method):
121
+ raise UnsupportedFrameworkAgentError(f"LangChain entry does not expose {method_name}.")
122
+
123
+ call_kwargs = _method_kwargs(method, config, extra_kwargs or {})
124
+ last_error: Exception | None = None
125
+ for candidate in _input_candidates(value, input_key):
126
+ try:
127
+ return await maybe_await(method(candidate, **call_kwargs))
128
+ except Exception as exc:
129
+ if not is_input_shape_error(exc):
130
+ raise
131
+ last_error = exc
132
+ if last_error is not None:
133
+ raise last_error
134
+ raise UnsupportedFrameworkAgentError(f"LangChain entry did not accept any {method_name} input shape.")
135
+
136
+
137
+ async def _invoke(
138
+ runnable: Any,
139
+ value: Any,
140
+ *,
141
+ input_key: str,
142
+ config: Any = None,
143
+ extra_kwargs: dict[str, Any] | None = None,
144
+ ) -> Any:
145
+ if callable(getattr(runnable, "ainvoke", None)):
146
+ return await _call_with_candidates(
147
+ runnable,
148
+ "ainvoke",
149
+ value,
150
+ input_key=input_key,
151
+ config=config,
152
+ extra_kwargs=extra_kwargs,
153
+ )
154
+ if callable(getattr(runnable, "invoke", None)):
155
+ return await _call_with_candidates(
156
+ runnable,
157
+ "invoke",
158
+ value,
159
+ input_key=input_key,
160
+ config=config,
161
+ extra_kwargs=extra_kwargs,
162
+ )
163
+ if callable(runnable):
164
+ call_kwargs = _method_kwargs(runnable, config, extra_kwargs or {})
165
+ last_error: Exception | None = None
166
+ for candidate in _input_candidates(value, input_key):
167
+ try:
168
+ return await maybe_await(runnable(candidate, **call_kwargs))
169
+ except Exception as exc:
170
+ if not is_input_shape_error(exc):
171
+ raise
172
+ last_error = exc
173
+ if last_error is not None:
174
+ raise last_error
175
+ raise UnsupportedFrameworkAgentError(
176
+ "LangChain entry must expose ainvoke/invoke or be callable for LangServe compatibility."
177
+ )
178
+
179
+
180
+ async def _stream_from_method(
181
+ method: Any,
182
+ value: Any,
183
+ *,
184
+ input_key: str,
185
+ config: Any = None,
186
+ extra_kwargs: dict[str, Any] | None = None,
187
+ ) -> AsyncGenerator[Any, None]:
188
+ call_kwargs = _method_kwargs(method, config, extra_kwargs or {})
189
+ last_error: Exception | None = None
190
+ for candidate in _input_candidates(value, input_key):
191
+ emitted = False
192
+ try:
193
+ stream = method(candidate, **call_kwargs)
194
+ if inspect.isawaitable(stream):
195
+ stream = await stream
196
+ if hasattr(stream, "__aiter__"):
197
+ async for chunk in stream:
198
+ emitted = True
199
+ yield chunk
200
+ else:
201
+ for chunk in stream:
202
+ emitted = True
203
+ yield chunk
204
+ return
205
+ except Exception as exc:
206
+ if emitted:
207
+ raise
208
+ if not is_input_shape_error(exc):
209
+ raise
210
+ last_error = exc
211
+ if last_error is not None:
212
+ raise last_error
213
+
214
+
215
+ def _event_stream_kwargs(method: Any) -> dict[str, str]:
216
+ try:
217
+ signature = inspect.signature(method)
218
+ except (TypeError, ValueError):
219
+ return {}
220
+ version = signature.parameters.get("version")
221
+ if version is not None and version.default is inspect.Parameter.empty:
222
+ return {"version": "v2"}
223
+ return {}
224
+
225
+
226
+ async def _stream_from_method_with_candidates(
227
+ method: Any,
228
+ value: Any,
229
+ *,
230
+ input_key: str,
231
+ kwargs: dict[str, Any] | None = None,
232
+ config: Any = None,
233
+ extra_kwargs: dict[str, Any] | None = None,
234
+ ) -> AsyncGenerator[Any, None]:
235
+ call_kwargs = {**_method_kwargs(method, config, extra_kwargs or {}), **(kwargs or {})}
236
+ last_error: Exception | None = None
237
+ for candidate in _input_candidates(value, input_key):
238
+ emitted = False
239
+ try:
240
+ stream = method(candidate, **call_kwargs)
241
+ if inspect.isawaitable(stream):
242
+ stream = await stream
243
+ if hasattr(stream, "__aiter__"):
244
+ async for chunk in stream:
245
+ emitted = True
246
+ yield chunk
247
+ else:
248
+ for chunk in stream:
249
+ emitted = True
250
+ yield chunk
251
+ return
252
+ except Exception as exc:
253
+ if emitted:
254
+ raise
255
+ if not is_input_shape_error(exc):
256
+ raise
257
+ last_error = exc
258
+ if last_error is not None:
259
+ raise last_error
260
+
261
+
262
+ async def _stream_chunks(
263
+ runnable: Any,
264
+ value: Any,
265
+ *,
266
+ input_key: str,
267
+ config: Any = None,
268
+ extra_kwargs: dict[str, Any] | None = None,
269
+ ) -> AsyncGenerator[Any, None]:
270
+ astream = getattr(runnable, "astream", None)
271
+ if callable(astream):
272
+ async for chunk in _stream_from_method(
273
+ astream,
274
+ value,
275
+ input_key=input_key,
276
+ config=config,
277
+ extra_kwargs=extra_kwargs,
278
+ ):
279
+ yield chunk
280
+ return
281
+
282
+ stream = getattr(runnable, "stream", None)
283
+ if callable(stream):
284
+ async for chunk in _stream_from_method(
285
+ stream,
286
+ value,
287
+ input_key=input_key,
288
+ config=config,
289
+ extra_kwargs=extra_kwargs,
290
+ ):
291
+ yield chunk
292
+ return
293
+
294
+ yield await _invoke(runnable, value, input_key=input_key, config=config, extra_kwargs=extra_kwargs)
295
+
296
+
297
+ async def _batch(
298
+ runnable: Any,
299
+ values: list[Any],
300
+ *,
301
+ input_key: str,
302
+ config: Any = None,
303
+ extra_kwargs: dict[str, Any] | None = None,
304
+ ) -> list[Any]:
305
+ abatch = getattr(runnable, "abatch", None)
306
+ if callable(abatch):
307
+ try:
308
+ return await maybe_await(abatch(values, **_method_kwargs(abatch, config, extra_kwargs or {})))
309
+ except Exception:
310
+ logger.debug("LangServe compat abatch failed; falling back to per-item invoke.", exc_info=True)
311
+
312
+ batch = getattr(runnable, "batch", None)
313
+ if callable(batch):
314
+ try:
315
+ return await maybe_await(batch(values, **_method_kwargs(batch, config, extra_kwargs or {})))
316
+ except Exception:
317
+ logger.debug("LangServe compat batch failed; falling back to per-item invoke.", exc_info=True)
318
+
319
+ return await asyncio.gather(
320
+ *[_invoke(runnable, value, input_key=input_key, config=config, extra_kwargs=extra_kwargs) for value in values]
321
+ )
322
+
323
+
324
+ def _sse(event: str, data: Any) -> str:
325
+ return f"event: {event}\ndata: {json.dumps(_jsonable(data), ensure_ascii=False)}\n\n"
326
+
327
+
328
+ def _route_path(prefix: str, path: str) -> str:
329
+ return f"{prefix}{path}" if prefix else path
330
+
331
+
332
+ def _promote_route(app: FastAPI, endpoint: Any, path: str) -> None:
333
+ routes = app.router.routes
334
+ route_index = next(
335
+ (
336
+ index
337
+ for index, route in enumerate(routes)
338
+ if getattr(route, "endpoint", None) is endpoint and getattr(route, "path", None) == path
339
+ ),
340
+ None,
341
+ )
342
+ if route_index is None:
343
+ return
344
+
345
+ route = routes.pop(route_index)
346
+ insert_at = len(routes)
347
+ for index, existing in enumerate(routes):
348
+ existing_path = getattr(existing, "path", None)
349
+ if existing_path == path or existing_path in {"", "/"}:
350
+ insert_at = index
351
+ break
352
+ routes.insert(insert_at, route)
353
+
354
+
355
+ def attach_langserve_compat_routes(
356
+ app: FastAPI,
357
+ runnable: Any,
358
+ *,
359
+ input_key: str = "input",
360
+ prefix: str = "",
361
+ ) -> None:
362
+ """Attach a conservative LangServe-like HTTP contract to a FastAPI app."""
363
+
364
+ normalized_prefix = _normalize_prefix(prefix)
365
+ logger.info("Attaching LangServe compatibility routes at prefix %s", normalized_prefix or "/")
366
+
367
+ async def invoke(request: Request) -> JSONResponse:
368
+ payload = await _request_json(request)
369
+ config, extra_kwargs = _request_options(payload)
370
+ run_id = str(uuid4())
371
+ try:
372
+ output = await _invoke(
373
+ runnable,
374
+ _extract_input(payload),
375
+ input_key=input_key,
376
+ config=config,
377
+ extra_kwargs=extra_kwargs,
378
+ )
379
+ except UnsupportedFrameworkAgentError as exc:
380
+ logger.info("LangServe compat invoke rejected unsupported runnable: %s", exc)
381
+ raise HTTPException(status_code=422, detail=str(exc)) from exc
382
+ except Exception:
383
+ logger.exception("LangServe compat invoke failed.")
384
+ raise
385
+ return JSONResponse({"output": _jsonable(output), "metadata": {"run_id": run_id}})
386
+
387
+ async def batch(request: Request) -> JSONResponse:
388
+ payload = await _request_json(request)
389
+ config, extra_kwargs = _request_options(payload)
390
+ raw_inputs = payload.get("inputs") if isinstance(payload, dict) else None
391
+ if not isinstance(raw_inputs, list):
392
+ raise HTTPException(status_code=400, detail="Batch request body must include an 'inputs' list.")
393
+ outputs = await _batch(runnable, raw_inputs, input_key=input_key, config=config, extra_kwargs=extra_kwargs)
394
+ return JSONResponse(
395
+ {
396
+ "output": _jsonable(outputs),
397
+ "metadata": {"run_ids": [str(uuid4()) for _ in outputs]},
398
+ }
399
+ )
400
+
401
+ async def stream(request: Request) -> StreamingResponse:
402
+ payload = await _request_json(request)
403
+ value = _extract_input(payload)
404
+ config, extra_kwargs = _request_options(payload)
405
+
406
+ async def events() -> AsyncGenerator[str, None]:
407
+ try:
408
+ async for chunk in _stream_chunks(
409
+ runnable,
410
+ value,
411
+ input_key=input_key,
412
+ config=config,
413
+ extra_kwargs=extra_kwargs,
414
+ ):
415
+ yield _sse("data", chunk)
416
+ yield _sse("end", {})
417
+ except Exception as exc:
418
+ logger.exception("LangServe compat stream failed.")
419
+ yield _sse("error", {"message": str(exc)})
420
+
421
+ return StreamingResponse(events(), media_type="text/event-stream")
422
+
423
+ async def stream_events(request: Request) -> StreamingResponse:
424
+ payload = await _request_json(request)
425
+ value = _extract_input(payload)
426
+ config, extra_kwargs = _request_options(payload)
427
+ astream_events = getattr(runnable, "astream_events", None)
428
+
429
+ async def events() -> AsyncGenerator[str, None]:
430
+ try:
431
+ if callable(astream_events):
432
+ async for event in _stream_from_method_with_candidates(
433
+ astream_events,
434
+ value,
435
+ input_key=input_key,
436
+ kwargs=_event_stream_kwargs(astream_events),
437
+ config=config,
438
+ extra_kwargs=extra_kwargs,
439
+ ):
440
+ yield _sse("data", event)
441
+ else:
442
+ async for chunk in _stream_chunks(
443
+ runnable,
444
+ value,
445
+ input_key=input_key,
446
+ config=config,
447
+ extra_kwargs=extra_kwargs,
448
+ ):
449
+ yield _sse(
450
+ "data",
451
+ {
452
+ "event": "on_chain_stream",
453
+ "data": {"chunk": _jsonable(chunk), "text": chunk_to_text(chunk)},
454
+ },
455
+ )
456
+ yield _sse("end", {})
457
+ except Exception as exc:
458
+ logger.exception("LangServe compat stream_events failed.")
459
+ yield _sse("error", {"message": str(exc)})
460
+
461
+ return StreamingResponse(events(), media_type="text/event-stream")
462
+
463
+ async def stream_log(request: Request) -> StreamingResponse:
464
+ payload = await _request_json(request)
465
+ value = _extract_input(payload)
466
+ config, extra_kwargs = _request_options(payload)
467
+ astream_log = getattr(runnable, "astream_log", None)
468
+ if not callable(astream_log):
469
+ logger.info("LangServe compat /stream_log requested but runnable does not expose astream_log.")
470
+ raise HTTPException(
471
+ status_code=501,
472
+ detail="stream_log requires a runnable exposing astream_log; this compatibility layer does not synthesize LangServe logs.",
473
+ )
474
+
475
+ async def events() -> AsyncGenerator[str, None]:
476
+ try:
477
+ async for event in _stream_from_method_with_candidates(
478
+ astream_log,
479
+ value,
480
+ input_key=input_key,
481
+ config=config,
482
+ extra_kwargs=extra_kwargs,
483
+ ):
484
+ yield _sse("data", event)
485
+ yield _sse("end", {})
486
+ except Exception as exc:
487
+ logger.exception("LangServe compat stream_log failed.")
488
+ yield _sse("error", {"message": str(exc)})
489
+
490
+ return StreamingResponse(events(), media_type="text/event-stream")
491
+
492
+ routes: list[tuple[str, Any]] = [
493
+ ("/invoke", invoke),
494
+ ("/batch", batch),
495
+ ("/stream", stream),
496
+ ("/stream_events", stream_events),
497
+ ("/stream_log", stream_log),
498
+ ]
499
+ for path, endpoint in routes:
500
+ full_path = _route_path(normalized_prefix, path)
501
+ app.add_api_route(full_path, endpoint, methods=["POST"])
502
+ _promote_route(app, endpoint, full_path)