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.
- agentkit/auth/model_login.py +87 -105
- agentkit/frameworks/__init__.py +29 -0
- agentkit/frameworks/_common.py +136 -0
- agentkit/frameworks/langchain.py +170 -0
- agentkit/frameworks/langgraph.py +349 -0
- agentkit/frameworks/serving/__init__.py +6 -0
- agentkit/frameworks/serving/fastapi_mount.py +65 -0
- agentkit/frameworks/serving/langserve.py +502 -0
- agentkit/toolkit/cli/sandbox/a2a_client.py +407 -0
- agentkit/toolkit/cli/sandbox/agentkit_client.py +248 -0
- agentkit/toolkit/cli/sandbox/cli.py +5 -0
- agentkit/toolkit/cli/sandbox/cli_create.py +219 -165
- agentkit/toolkit/cli/sandbox/cli_exec.py +2 -70
- agentkit/toolkit/cli/sandbox/cli_file.py +1 -1
- agentkit/toolkit/cli/sandbox/cli_get.py +2 -6
- agentkit/toolkit/cli/sandbox/cli_invoke.py +375 -0
- agentkit/toolkit/cli/sandbox/cli_model_login.py +57 -70
- agentkit/toolkit/cli/sandbox/cli_mount.py +1 -1
- agentkit/toolkit/cli/sandbox/env_config.py +798 -0
- agentkit/toolkit/cli/sandbox/model_config.py +90 -29
- agentkit/toolkit/cli/sandbox/session_create.py +94 -168
- agentkit/toolkit/cli/sandbox/session_sync.py +1 -1
- agentkit/toolkit/cli/sandbox/tool_resolve.py +37 -73
- agentkit/version.py +1 -1
- {agentkit_sdk_python-0.7.2.dist-info → agentkit_sdk_python-0.7.4.dist-info}/METADATA +8 -1
- {agentkit_sdk_python-0.7.2.dist-info → agentkit_sdk_python-0.7.4.dist-info}/RECORD +30 -19
- {agentkit_sdk_python-0.7.2.dist-info → agentkit_sdk_python-0.7.4.dist-info}/WHEEL +0 -0
- {agentkit_sdk_python-0.7.2.dist-info → agentkit_sdk_python-0.7.4.dist-info}/entry_points.txt +0 -0
- {agentkit_sdk_python-0.7.2.dist-info → agentkit_sdk_python-0.7.4.dist-info}/licenses/LICENSE +0 -0
- {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)
|