agentkit-sdk-python 0.7.3__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/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/version.py +1 -1
- {agentkit_sdk_python-0.7.3.dist-info → agentkit_sdk_python-0.7.4.dist-info}/METADATA +8 -1
- {agentkit_sdk_python-0.7.3.dist-info → agentkit_sdk_python-0.7.4.dist-info}/RECORD +14 -7
- {agentkit_sdk_python-0.7.3.dist-info → agentkit_sdk_python-0.7.4.dist-info}/WHEEL +0 -0
- {agentkit_sdk_python-0.7.3.dist-info → agentkit_sdk_python-0.7.4.dist-info}/entry_points.txt +0 -0
- {agentkit_sdk_python-0.7.3.dist-info → agentkit_sdk_python-0.7.4.dist-info}/licenses/LICENSE +0 -0
- {agentkit_sdk_python-0.7.3.dist-info → agentkit_sdk_python-0.7.4.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Framework adapters for running third-party agents in AgentKit apps."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from agentkit.frameworks._common import (
|
|
8
|
+
FrameworkBridgeError,
|
|
9
|
+
UnsupportedFrameworkAgentError,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"FrameworkBridgeError",
|
|
14
|
+
"LangChainAgentkitBridge",
|
|
15
|
+
"LangGraphAgentkitBridge",
|
|
16
|
+
"UnsupportedFrameworkAgentError",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def __getattr__(name: str) -> Any:
|
|
21
|
+
if name == "LangChainAgentkitBridge":
|
|
22
|
+
from agentkit.frameworks.langchain import LangChainAgentkitBridge
|
|
23
|
+
|
|
24
|
+
return LangChainAgentkitBridge
|
|
25
|
+
if name == "LangGraphAgentkitBridge":
|
|
26
|
+
from agentkit.frameworks.langgraph import LangGraphAgentkitBridge
|
|
27
|
+
|
|
28
|
+
return LangGraphAgentkitBridge
|
|
29
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"""Shared helpers for framework-to-ADK adapters."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import inspect
|
|
6
|
+
import json
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from google.adk.agents.invocation_context import InvocationContext
|
|
10
|
+
from google.adk.events import Event
|
|
11
|
+
from google.genai import types
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class FrameworkBridgeError(RuntimeError):
|
|
15
|
+
"""Base error raised by AgentKit framework adapters."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class UnsupportedFrameworkAgentError(FrameworkBridgeError):
|
|
19
|
+
"""Raised when an entry object does not expose a supported agent protocol."""
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def user_text(ctx: InvocationContext) -> str:
|
|
23
|
+
content = ctx.user_content
|
|
24
|
+
if content is None or not content.parts:
|
|
25
|
+
return ""
|
|
26
|
+
texts: list[str] = []
|
|
27
|
+
for part in content.parts:
|
|
28
|
+
text = getattr(part, "text", None)
|
|
29
|
+
if text:
|
|
30
|
+
texts.append(text)
|
|
31
|
+
return "\n".join(texts)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def json_text(value: Any) -> str:
|
|
35
|
+
try:
|
|
36
|
+
return json.dumps(value, ensure_ascii=False, default=str)
|
|
37
|
+
except Exception:
|
|
38
|
+
return str(value)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def content_to_text(content: Any) -> str:
|
|
42
|
+
if content is None:
|
|
43
|
+
return ""
|
|
44
|
+
if isinstance(content, str):
|
|
45
|
+
return content
|
|
46
|
+
if isinstance(content, dict):
|
|
47
|
+
for key in ("text", "content", "output", "answer"):
|
|
48
|
+
if key in content:
|
|
49
|
+
text = content_to_text(content[key])
|
|
50
|
+
if text:
|
|
51
|
+
return text
|
|
52
|
+
return json_text(content)
|
|
53
|
+
if isinstance(content, list):
|
|
54
|
+
return "".join(content_to_text(item) for item in content)
|
|
55
|
+
text = getattr(content, "text", None)
|
|
56
|
+
if isinstance(text, str):
|
|
57
|
+
return text
|
|
58
|
+
return str(content)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def chunk_to_text(value: Any) -> str:
|
|
62
|
+
if value is None:
|
|
63
|
+
return ""
|
|
64
|
+
if isinstance(value, str):
|
|
65
|
+
return value
|
|
66
|
+
content = getattr(value, "content", None)
|
|
67
|
+
if content is not None:
|
|
68
|
+
return content_to_text(content)
|
|
69
|
+
text = getattr(value, "text", None)
|
|
70
|
+
if isinstance(text, str):
|
|
71
|
+
return text
|
|
72
|
+
if isinstance(value, dict):
|
|
73
|
+
for key in ("output", "answer", "text", "content"):
|
|
74
|
+
if key in value:
|
|
75
|
+
text = chunk_to_text(value[key])
|
|
76
|
+
if text:
|
|
77
|
+
return text
|
|
78
|
+
messages = value.get("messages")
|
|
79
|
+
if isinstance(messages, (list, tuple)) and messages:
|
|
80
|
+
return chunk_to_text(messages[-1])
|
|
81
|
+
for nested in value.values():
|
|
82
|
+
text = chunk_to_text(nested)
|
|
83
|
+
if text:
|
|
84
|
+
return text
|
|
85
|
+
return json_text(value)
|
|
86
|
+
if isinstance(value, (list, tuple)):
|
|
87
|
+
for item in reversed(value):
|
|
88
|
+
text = chunk_to_text(item)
|
|
89
|
+
if text:
|
|
90
|
+
return text
|
|
91
|
+
return ""
|
|
92
|
+
return str(value)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def adk_event(
|
|
96
|
+
ctx: InvocationContext,
|
|
97
|
+
author: str,
|
|
98
|
+
text: str,
|
|
99
|
+
*,
|
|
100
|
+
partial: bool,
|
|
101
|
+
) -> Event:
|
|
102
|
+
return Event(
|
|
103
|
+
invocation_id=ctx.invocation_id,
|
|
104
|
+
author=author,
|
|
105
|
+
branch=ctx.branch,
|
|
106
|
+
partial=partial,
|
|
107
|
+
content=types.Content(role="model", parts=[types.Part(text=text)]),
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
async def maybe_await(value: Any) -> Any:
|
|
112
|
+
if inspect.isawaitable(value):
|
|
113
|
+
return await value
|
|
114
|
+
return value
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def is_input_shape_error(exc: Exception) -> bool:
|
|
118
|
+
message = str(exc)
|
|
119
|
+
name = exc.__class__.__name__
|
|
120
|
+
return (
|
|
121
|
+
name == "InvalidUpdateError"
|
|
122
|
+
or "Invalid input type" in message
|
|
123
|
+
or "Must be a PromptValue, str, or list of BaseMessages" in message
|
|
124
|
+
or "Expected dict" in message
|
|
125
|
+
or "string indices must be integers" in message
|
|
126
|
+
or "'str' object is not subscriptable" in message
|
|
127
|
+
or "Input to ChatPromptTemplate is missing variables" in message
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def chunk_delta(accumulated: str, text: str) -> str:
|
|
132
|
+
if not accumulated:
|
|
133
|
+
return text
|
|
134
|
+
if text.startswith(accumulated):
|
|
135
|
+
return text[len(accumulated) :]
|
|
136
|
+
return text
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"""LangChain adapter for AgentKit apps."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import AsyncGenerator
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from google.adk.agents.base_agent import BaseAgent
|
|
9
|
+
from google.adk.agents.invocation_context import InvocationContext
|
|
10
|
+
from google.adk.events import Event
|
|
11
|
+
from pydantic import PrivateAttr
|
|
12
|
+
|
|
13
|
+
from agentkit.frameworks._common import (
|
|
14
|
+
UnsupportedFrameworkAgentError,
|
|
15
|
+
adk_event,
|
|
16
|
+
chunk_delta,
|
|
17
|
+
chunk_to_text,
|
|
18
|
+
is_input_shape_error,
|
|
19
|
+
maybe_await,
|
|
20
|
+
user_text,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
try:
|
|
25
|
+
from langchain_core.messages import HumanMessage
|
|
26
|
+
except ImportError: # pragma: no cover - depends on optional packages.
|
|
27
|
+
HumanMessage = None # type: ignore[assignment]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class LangChainAgentkitBridge(BaseAgent):
|
|
31
|
+
"""Adapt a LangChain Runnable or callable to AgentKit's ADK runtime boundary."""
|
|
32
|
+
|
|
33
|
+
_runnable: Any = PrivateAttr()
|
|
34
|
+
_input_key: str = PrivateAttr(default="input")
|
|
35
|
+
|
|
36
|
+
def __init__(
|
|
37
|
+
self,
|
|
38
|
+
runnable: Any,
|
|
39
|
+
*,
|
|
40
|
+
name: str = "langchain_agent",
|
|
41
|
+
description: str = "LangChain agent adapted for AgentKit runtime",
|
|
42
|
+
input_key: str = "input",
|
|
43
|
+
) -> None:
|
|
44
|
+
super().__init__(name=name, description=description)
|
|
45
|
+
self._runnable = runnable
|
|
46
|
+
self._input_key = input_key
|
|
47
|
+
|
|
48
|
+
def _input_candidates(self, payload: dict[str, Any], text_input: str) -> list[Any]:
|
|
49
|
+
candidates: list[Any] = [payload, text_input]
|
|
50
|
+
if HumanMessage is not None:
|
|
51
|
+
message = HumanMessage(content=text_input)
|
|
52
|
+
candidates.extend(([message], {"messages": [message]}))
|
|
53
|
+
return candidates
|
|
54
|
+
|
|
55
|
+
async def _call_once(self, payload: dict[str, Any], text_input: str) -> Any:
|
|
56
|
+
candidates = self._input_candidates(payload, text_input)
|
|
57
|
+
ainvoke = getattr(self._runnable, "ainvoke", None)
|
|
58
|
+
if callable(ainvoke):
|
|
59
|
+
last_error: Exception | None = None
|
|
60
|
+
for candidate in candidates:
|
|
61
|
+
try:
|
|
62
|
+
return await ainvoke(candidate)
|
|
63
|
+
except Exception as exc:
|
|
64
|
+
if not is_input_shape_error(exc):
|
|
65
|
+
raise
|
|
66
|
+
last_error = exc
|
|
67
|
+
if last_error is not None:
|
|
68
|
+
raise last_error
|
|
69
|
+
|
|
70
|
+
invoke = getattr(self._runnable, "invoke", None)
|
|
71
|
+
if callable(invoke):
|
|
72
|
+
last_error = None
|
|
73
|
+
for candidate in candidates:
|
|
74
|
+
try:
|
|
75
|
+
return invoke(candidate)
|
|
76
|
+
except Exception as exc:
|
|
77
|
+
if not is_input_shape_error(exc):
|
|
78
|
+
raise
|
|
79
|
+
last_error = exc
|
|
80
|
+
if last_error is not None:
|
|
81
|
+
raise last_error
|
|
82
|
+
|
|
83
|
+
if callable(self._runnable):
|
|
84
|
+
last_error = None
|
|
85
|
+
for candidate in candidates:
|
|
86
|
+
try:
|
|
87
|
+
return await maybe_await(self._runnable(candidate))
|
|
88
|
+
except Exception as exc:
|
|
89
|
+
if not is_input_shape_error(exc):
|
|
90
|
+
raise
|
|
91
|
+
last_error = exc
|
|
92
|
+
if last_error is not None:
|
|
93
|
+
raise last_error
|
|
94
|
+
|
|
95
|
+
raise UnsupportedFrameworkAgentError(
|
|
96
|
+
"LangChain entry must be a Runnable-like object exposing "
|
|
97
|
+
"astream, stream, ainvoke, or invoke, or a callable that accepts the user input."
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
async def _stream_chunks(self, payload: dict[str, Any], text_input: str):
|
|
101
|
+
astream = getattr(self._runnable, "astream", None)
|
|
102
|
+
if callable(astream):
|
|
103
|
+
last_error: Exception | None = None
|
|
104
|
+
for candidate in self._input_candidates(payload, text_input):
|
|
105
|
+
emitted = False
|
|
106
|
+
try:
|
|
107
|
+
async for chunk in astream(candidate):
|
|
108
|
+
emitted = True
|
|
109
|
+
yield chunk
|
|
110
|
+
return
|
|
111
|
+
except Exception as exc:
|
|
112
|
+
if emitted:
|
|
113
|
+
raise
|
|
114
|
+
if not is_input_shape_error(exc):
|
|
115
|
+
raise
|
|
116
|
+
last_error = exc
|
|
117
|
+
if last_error is not None:
|
|
118
|
+
raise last_error
|
|
119
|
+
return
|
|
120
|
+
|
|
121
|
+
stream = getattr(self._runnable, "stream", None)
|
|
122
|
+
if callable(stream):
|
|
123
|
+
last_error = None
|
|
124
|
+
for candidate in self._input_candidates(payload, text_input):
|
|
125
|
+
emitted = False
|
|
126
|
+
try:
|
|
127
|
+
for chunk in stream(candidate):
|
|
128
|
+
emitted = True
|
|
129
|
+
yield chunk
|
|
130
|
+
return
|
|
131
|
+
except Exception as exc:
|
|
132
|
+
if emitted:
|
|
133
|
+
raise
|
|
134
|
+
if not is_input_shape_error(exc):
|
|
135
|
+
raise
|
|
136
|
+
last_error = exc
|
|
137
|
+
if last_error is not None:
|
|
138
|
+
raise last_error
|
|
139
|
+
|
|
140
|
+
async def _run_async_impl(
|
|
141
|
+
self,
|
|
142
|
+
ctx: InvocationContext,
|
|
143
|
+
) -> AsyncGenerator[Event, None]:
|
|
144
|
+
text_input = user_text(ctx)
|
|
145
|
+
payload = {self._input_key: text_input}
|
|
146
|
+
accumulated_text = ""
|
|
147
|
+
has_output = False
|
|
148
|
+
last_text = ""
|
|
149
|
+
streamed = False
|
|
150
|
+
|
|
151
|
+
async for chunk in self._stream_chunks(payload, text_input):
|
|
152
|
+
streamed = True
|
|
153
|
+
text = chunk_to_text(chunk)
|
|
154
|
+
if not text:
|
|
155
|
+
continue
|
|
156
|
+
delta = chunk_delta(accumulated_text, text)
|
|
157
|
+
if not delta:
|
|
158
|
+
continue
|
|
159
|
+
accumulated_text += delta
|
|
160
|
+
has_output = True
|
|
161
|
+
last_text = accumulated_text
|
|
162
|
+
yield adk_event(ctx, self.name, delta, partial=True)
|
|
163
|
+
|
|
164
|
+
if not streamed:
|
|
165
|
+
result = await self._call_once(payload, text_input)
|
|
166
|
+
last_text = chunk_to_text(result)
|
|
167
|
+
|
|
168
|
+
final_text = accumulated_text if has_output else last_text
|
|
169
|
+
if final_text:
|
|
170
|
+
yield adk_event(ctx, self.name, final_text, partial=False)
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
"""LangGraph adapter for AgentKit apps."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import AsyncGenerator
|
|
6
|
+
import inspect
|
|
7
|
+
import json
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
try:
|
|
11
|
+
from langchain_core.messages import HumanMessage
|
|
12
|
+
except ImportError as exc: # pragma: no cover - depends on optional packages.
|
|
13
|
+
raise ImportError(
|
|
14
|
+
"LangGraph bridge requires langgraph/langchain-core. "
|
|
15
|
+
"Install langgraph or use the requirements generated by agentkit migrate."
|
|
16
|
+
) from exc
|
|
17
|
+
|
|
18
|
+
from google.adk.agents.base_agent import BaseAgent
|
|
19
|
+
from google.adk.agents.invocation_context import InvocationContext
|
|
20
|
+
from google.adk.events import Event, EventActions
|
|
21
|
+
from google.genai import types
|
|
22
|
+
from pydantic import PrivateAttr
|
|
23
|
+
|
|
24
|
+
try:
|
|
25
|
+
from langgraph.types import Command
|
|
26
|
+
except ImportError: # pragma: no cover - older optional langgraph versions.
|
|
27
|
+
Command = None # type: ignore[assignment]
|
|
28
|
+
|
|
29
|
+
from agentkit.frameworks._common import (
|
|
30
|
+
UnsupportedFrameworkAgentError,
|
|
31
|
+
adk_event,
|
|
32
|
+
chunk_delta,
|
|
33
|
+
chunk_to_text,
|
|
34
|
+
user_text,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
LANGGRAPH_PENDING_INTERRUPT_STATE_KEY = "agentkit:langgraph:pending_interrupt"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _method_kwargs(method: Any, kwargs: dict[str, Any]) -> dict[str, Any]:
|
|
42
|
+
try:
|
|
43
|
+
signature = inspect.signature(method)
|
|
44
|
+
except (TypeError, ValueError):
|
|
45
|
+
return kwargs
|
|
46
|
+
|
|
47
|
+
parameters = signature.parameters
|
|
48
|
+
accepts_kwargs = any(param.kind == inspect.Parameter.VAR_KEYWORD for param in parameters.values())
|
|
49
|
+
return {key: value for key, value in kwargs.items() if accepts_kwargs or key in parameters}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _jsonable(value: Any) -> Any:
|
|
53
|
+
try:
|
|
54
|
+
json.dumps(value, ensure_ascii=False)
|
|
55
|
+
return value
|
|
56
|
+
except Exception:
|
|
57
|
+
return str(value)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _interrupt_payload(data: Any) -> Any | None:
|
|
61
|
+
if not isinstance(data, dict) or "__interrupt__" not in data:
|
|
62
|
+
return None
|
|
63
|
+
interrupts = data.get("__interrupt__")
|
|
64
|
+
if not isinstance(interrupts, (list, tuple)) or not interrupts:
|
|
65
|
+
return interrupts
|
|
66
|
+
values: list[dict[str, Any]] = []
|
|
67
|
+
for interrupt in interrupts:
|
|
68
|
+
values.append(
|
|
69
|
+
{
|
|
70
|
+
"id": getattr(interrupt, "id", None),
|
|
71
|
+
"value": _jsonable(getattr(interrupt, "value", interrupt)),
|
|
72
|
+
}
|
|
73
|
+
)
|
|
74
|
+
return values
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _resume_value(text_input: str) -> Any:
|
|
78
|
+
stripped = text_input.strip()
|
|
79
|
+
if stripped.startswith(("{", "[")):
|
|
80
|
+
try:
|
|
81
|
+
return json.loads(stripped)
|
|
82
|
+
except json.JSONDecodeError:
|
|
83
|
+
return text_input
|
|
84
|
+
return text_input
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class LangGraphAgentkitBridge(BaseAgent):
|
|
88
|
+
"""Adapt a compiled LangGraph graph to AgentKit's ADK runtime boundary."""
|
|
89
|
+
|
|
90
|
+
_graph: Any = PrivateAttr()
|
|
91
|
+
_input_key: str | None = PrivateAttr(default=None)
|
|
92
|
+
|
|
93
|
+
def __init__(
|
|
94
|
+
self,
|
|
95
|
+
graph: Any,
|
|
96
|
+
*,
|
|
97
|
+
name: str = "langgraph_agent",
|
|
98
|
+
description: str = "LangGraph agent adapted for AgentKit runtime",
|
|
99
|
+
input_key: str | None = None,
|
|
100
|
+
) -> None:
|
|
101
|
+
super().__init__(name=name, description=description)
|
|
102
|
+
self._graph = graph
|
|
103
|
+
self._input_key = input_key
|
|
104
|
+
|
|
105
|
+
def _input_payload(self, text_input: str) -> dict[str, Any]:
|
|
106
|
+
if self._input_key:
|
|
107
|
+
return {self._input_key: text_input}
|
|
108
|
+
return {"messages": [HumanMessage(content=text_input)]}
|
|
109
|
+
|
|
110
|
+
def _graph_config(self, ctx: InvocationContext) -> dict[str, Any] | None:
|
|
111
|
+
session = getattr(ctx, "session", None)
|
|
112
|
+
session_id = getattr(session, "id", None)
|
|
113
|
+
if not session_id:
|
|
114
|
+
return None
|
|
115
|
+
return {"configurable": {"thread_id": session_id}}
|
|
116
|
+
|
|
117
|
+
def _pending_interrupt(self, ctx: InvocationContext) -> Any | None:
|
|
118
|
+
session = getattr(ctx, "session", None)
|
|
119
|
+
state = getattr(session, "state", None)
|
|
120
|
+
if not isinstance(state, dict):
|
|
121
|
+
return None
|
|
122
|
+
pending = state.get(LANGGRAPH_PENDING_INTERRUPT_STATE_KEY)
|
|
123
|
+
if not pending:
|
|
124
|
+
return None
|
|
125
|
+
if isinstance(pending, dict) and pending.get("agent") not in {None, self.name}:
|
|
126
|
+
return None
|
|
127
|
+
return pending
|
|
128
|
+
|
|
129
|
+
def _run_payload(self, ctx: InvocationContext) -> Any:
|
|
130
|
+
text_input = user_text(ctx)
|
|
131
|
+
if self._pending_interrupt(ctx) is None:
|
|
132
|
+
return self._input_payload(text_input)
|
|
133
|
+
if Command is None:
|
|
134
|
+
raise UnsupportedFrameworkAgentError(
|
|
135
|
+
"LangGraph HITL resume requires langgraph.types.Command. "
|
|
136
|
+
"Upgrade langgraph or start a new AgentKit session."
|
|
137
|
+
)
|
|
138
|
+
return Command(resume=_resume_value(text_input))
|
|
139
|
+
|
|
140
|
+
def _interrupt_state_delta(self, ctx: InvocationContext, config: dict[str, Any] | None, interrupt: Any) -> dict[str, Any]:
|
|
141
|
+
session = getattr(ctx, "session", None)
|
|
142
|
+
session_id = getattr(session, "id", None)
|
|
143
|
+
if not session_id:
|
|
144
|
+
return {}
|
|
145
|
+
return {
|
|
146
|
+
LANGGRAPH_PENDING_INTERRUPT_STATE_KEY: {
|
|
147
|
+
"agent": self.name,
|
|
148
|
+
"thread_id": config.get("configurable", {}).get("thread_id") if config else session_id,
|
|
149
|
+
"interrupts": interrupt,
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
def _final_event(self, ctx: InvocationContext, text: str, *, clear_pending_interrupt: bool) -> Event:
|
|
154
|
+
event_kwargs: dict[str, Any] = {}
|
|
155
|
+
if clear_pending_interrupt:
|
|
156
|
+
event_kwargs["actions"] = EventActions(state_delta={LANGGRAPH_PENDING_INTERRUPT_STATE_KEY: None})
|
|
157
|
+
return Event(
|
|
158
|
+
invocation_id=ctx.invocation_id,
|
|
159
|
+
author=self.name,
|
|
160
|
+
branch=ctx.branch,
|
|
161
|
+
partial=False,
|
|
162
|
+
content=types.Content(role="model", parts=[types.Part(text=text)]),
|
|
163
|
+
**event_kwargs,
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
def _clear_pending_interrupt_event(self, ctx: InvocationContext) -> Event:
|
|
167
|
+
return Event(
|
|
168
|
+
invocation_id=ctx.invocation_id,
|
|
169
|
+
author=self.name,
|
|
170
|
+
branch=ctx.branch,
|
|
171
|
+
actions=EventActions(state_delta={LANGGRAPH_PENDING_INTERRUPT_STATE_KEY: None}),
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
async def _call_once(self, payload: Any, config: dict[str, Any] | None) -> Any:
|
|
175
|
+
ainvoke = getattr(self._graph, "ainvoke", None)
|
|
176
|
+
if callable(ainvoke):
|
|
177
|
+
return await ainvoke(payload, **_method_kwargs(ainvoke, {"config": config}))
|
|
178
|
+
|
|
179
|
+
invoke = getattr(self._graph, "invoke", None)
|
|
180
|
+
if callable(invoke):
|
|
181
|
+
return invoke(payload, **_method_kwargs(invoke, {"config": config}))
|
|
182
|
+
|
|
183
|
+
raise UnsupportedFrameworkAgentError(
|
|
184
|
+
"LangGraph entry must be a compiled graph-like object exposing "
|
|
185
|
+
"astream, stream, ainvoke, or invoke."
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
def _sync_stream(self, payload: Any, config: dict[str, Any] | None):
|
|
189
|
+
stream_fn = getattr(self._graph, "stream", None)
|
|
190
|
+
if not callable(stream_fn):
|
|
191
|
+
return None
|
|
192
|
+
try:
|
|
193
|
+
return stream_fn(
|
|
194
|
+
payload,
|
|
195
|
+
**_method_kwargs(
|
|
196
|
+
stream_fn,
|
|
197
|
+
{
|
|
198
|
+
"config": config,
|
|
199
|
+
"stream_mode": ["messages", "updates"],
|
|
200
|
+
"version": "v2",
|
|
201
|
+
},
|
|
202
|
+
),
|
|
203
|
+
)
|
|
204
|
+
except TypeError:
|
|
205
|
+
try:
|
|
206
|
+
return stream_fn(
|
|
207
|
+
payload,
|
|
208
|
+
**_method_kwargs(
|
|
209
|
+
stream_fn,
|
|
210
|
+
{
|
|
211
|
+
"config": config,
|
|
212
|
+
"stream_mode": ["messages", "updates"],
|
|
213
|
+
},
|
|
214
|
+
),
|
|
215
|
+
)
|
|
216
|
+
except TypeError:
|
|
217
|
+
return stream_fn(payload, **_method_kwargs(stream_fn, {"config": config}))
|
|
218
|
+
|
|
219
|
+
async def _stream_graph(self, payload: Any, config: dict[str, Any] | None, *, prefer_sync: bool = False):
|
|
220
|
+
if prefer_sync:
|
|
221
|
+
stream = self._sync_stream(payload, config)
|
|
222
|
+
if stream is not None:
|
|
223
|
+
for item in stream:
|
|
224
|
+
yield item
|
|
225
|
+
return
|
|
226
|
+
|
|
227
|
+
astream = getattr(self._graph, "astream", None)
|
|
228
|
+
if callable(astream):
|
|
229
|
+
try:
|
|
230
|
+
stream = astream(
|
|
231
|
+
payload,
|
|
232
|
+
**_method_kwargs(
|
|
233
|
+
astream,
|
|
234
|
+
{
|
|
235
|
+
"config": config,
|
|
236
|
+
"stream_mode": ["messages", "updates"],
|
|
237
|
+
"version": "v2",
|
|
238
|
+
},
|
|
239
|
+
),
|
|
240
|
+
)
|
|
241
|
+
except TypeError:
|
|
242
|
+
try:
|
|
243
|
+
stream = astream(
|
|
244
|
+
payload,
|
|
245
|
+
**_method_kwargs(
|
|
246
|
+
astream,
|
|
247
|
+
{
|
|
248
|
+
"config": config,
|
|
249
|
+
"stream_mode": ["messages", "updates"],
|
|
250
|
+
},
|
|
251
|
+
),
|
|
252
|
+
)
|
|
253
|
+
except TypeError:
|
|
254
|
+
stream = astream(payload, **_method_kwargs(astream, {"config": config}))
|
|
255
|
+
async for item in stream:
|
|
256
|
+
yield item
|
|
257
|
+
return
|
|
258
|
+
|
|
259
|
+
stream = self._sync_stream(payload, config)
|
|
260
|
+
if stream is not None:
|
|
261
|
+
for item in stream:
|
|
262
|
+
yield item
|
|
263
|
+
|
|
264
|
+
async def _run_async_impl(
|
|
265
|
+
self,
|
|
266
|
+
ctx: InvocationContext,
|
|
267
|
+
) -> AsyncGenerator[Event, None]:
|
|
268
|
+
config = self._graph_config(ctx)
|
|
269
|
+
resume_pending = self._pending_interrupt(ctx) is not None
|
|
270
|
+
payload = self._run_payload(ctx)
|
|
271
|
+
accumulated_text = ""
|
|
272
|
+
has_output = False
|
|
273
|
+
last_update: Any = None
|
|
274
|
+
pending_update_text = ""
|
|
275
|
+
saw_messages = False
|
|
276
|
+
streamed = False
|
|
277
|
+
|
|
278
|
+
async for item in self._stream_graph(payload, config, prefer_sync=resume_pending):
|
|
279
|
+
streamed = True
|
|
280
|
+
mode = ""
|
|
281
|
+
data = item
|
|
282
|
+
if isinstance(item, tuple) and len(item) == 2 and isinstance(item[0], str):
|
|
283
|
+
mode, data = item
|
|
284
|
+
elif isinstance(item, dict) and isinstance(item.get("type"), str):
|
|
285
|
+
mode = item["type"]
|
|
286
|
+
data = item.get("data")
|
|
287
|
+
|
|
288
|
+
if mode == "messages":
|
|
289
|
+
message = data[0] if isinstance(data, tuple) and data else data
|
|
290
|
+
text = chunk_to_text(message)
|
|
291
|
+
if not text:
|
|
292
|
+
continue
|
|
293
|
+
delta = chunk_delta(accumulated_text, text)
|
|
294
|
+
if not delta:
|
|
295
|
+
continue
|
|
296
|
+
saw_messages = True
|
|
297
|
+
accumulated_text += delta
|
|
298
|
+
has_output = True
|
|
299
|
+
yield adk_event(ctx, self.name, delta, partial=True)
|
|
300
|
+
continue
|
|
301
|
+
|
|
302
|
+
if mode == "updates":
|
|
303
|
+
interrupt = _interrupt_payload(data)
|
|
304
|
+
if interrupt is not None:
|
|
305
|
+
state_delta = self._interrupt_state_delta(ctx, config, interrupt)
|
|
306
|
+
event_kwargs: dict[str, Any] = {}
|
|
307
|
+
if state_delta:
|
|
308
|
+
event_kwargs["actions"] = EventActions(state_delta=state_delta)
|
|
309
|
+
yield Event(
|
|
310
|
+
invocation_id=ctx.invocation_id,
|
|
311
|
+
author=self.name,
|
|
312
|
+
branch=ctx.branch,
|
|
313
|
+
interrupted=True,
|
|
314
|
+
error_code="LANGGRAPH_INTERRUPT",
|
|
315
|
+
error_message="LangGraph execution interrupted and requires resume input.",
|
|
316
|
+
custom_metadata={"langgraph_interrupt": interrupt},
|
|
317
|
+
**event_kwargs,
|
|
318
|
+
)
|
|
319
|
+
return
|
|
320
|
+
last_update = data
|
|
321
|
+
if saw_messages:
|
|
322
|
+
continue
|
|
323
|
+
text = chunk_to_text(data)
|
|
324
|
+
if text:
|
|
325
|
+
pending_update_text = text
|
|
326
|
+
continue
|
|
327
|
+
|
|
328
|
+
text = chunk_to_text(data)
|
|
329
|
+
if text:
|
|
330
|
+
delta = chunk_delta(accumulated_text, text)
|
|
331
|
+
if delta:
|
|
332
|
+
accumulated_text += delta
|
|
333
|
+
has_output = True
|
|
334
|
+
yield adk_event(ctx, self.name, delta, partial=True)
|
|
335
|
+
|
|
336
|
+
if streamed:
|
|
337
|
+
if saw_messages or has_output:
|
|
338
|
+
final_text = accumulated_text
|
|
339
|
+
else:
|
|
340
|
+
final_text = pending_update_text or chunk_to_text(last_update)
|
|
341
|
+
if final_text:
|
|
342
|
+
yield adk_event(ctx, self.name, final_text, partial=True)
|
|
343
|
+
else:
|
|
344
|
+
final_text = chunk_to_text(await self._call_once(payload, config))
|
|
345
|
+
|
|
346
|
+
if final_text:
|
|
347
|
+
yield self._final_event(ctx, final_text, clear_pending_interrupt=resume_pending)
|
|
348
|
+
elif resume_pending:
|
|
349
|
+
yield self._clear_pending_interrupt_event(ctx)
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
"""Serving compatibility helpers for migrated framework agents."""
|
|
2
|
+
|
|
3
|
+
from agentkit.frameworks.serving.fastapi_mount import mount_legacy_fastapi_app
|
|
4
|
+
from agentkit.frameworks.serving.langserve import attach_langserve_compat_routes
|
|
5
|
+
|
|
6
|
+
__all__ = ["attach_langserve_compat_routes", "mount_legacy_fastapi_app"]
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""Helpers for mounting an existing FastAPI app beside AgentKit routes."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from fastapi import FastAPI
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _normalize_prefix(prefix: str) -> str:
|
|
14
|
+
if not prefix:
|
|
15
|
+
raise ValueError("legacy FastAPI mount prefix must not be empty.")
|
|
16
|
+
if not prefix.startswith("/"):
|
|
17
|
+
raise ValueError("legacy FastAPI mount prefix must start with '/'.")
|
|
18
|
+
if prefix != "/" and prefix.endswith("/"):
|
|
19
|
+
return prefix.rstrip("/")
|
|
20
|
+
return prefix
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def mount_legacy_fastapi_app(
|
|
24
|
+
app: FastAPI,
|
|
25
|
+
legacy_app: Any,
|
|
26
|
+
*,
|
|
27
|
+
prefix: str = "/legacy",
|
|
28
|
+
allow_root: bool = False,
|
|
29
|
+
) -> None:
|
|
30
|
+
"""Mount a user-owned FastAPI app without rewriting its routes."""
|
|
31
|
+
|
|
32
|
+
normalized_prefix = _normalize_prefix(prefix)
|
|
33
|
+
if normalized_prefix == "/" and not allow_root:
|
|
34
|
+
raise ValueError(
|
|
35
|
+
"mounting a legacy FastAPI app at '/' can shadow AgentKit routes; "
|
|
36
|
+
"pass allow_root=True only when you intentionally want that behavior."
|
|
37
|
+
)
|
|
38
|
+
if not isinstance(legacy_app, FastAPI):
|
|
39
|
+
raise TypeError("legacy_app must be a fastapi.FastAPI instance.")
|
|
40
|
+
|
|
41
|
+
logger.info("Mounting legacy FastAPI app at %s", normalized_prefix)
|
|
42
|
+
app.mount(normalized_prefix, legacy_app)
|
|
43
|
+
_promote_mount(app, normalized_prefix)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _promote_mount(app: FastAPI, path: str) -> None:
|
|
47
|
+
routes = app.router.routes
|
|
48
|
+
route_index = next(
|
|
49
|
+
(
|
|
50
|
+
index
|
|
51
|
+
for index, route in enumerate(routes)
|
|
52
|
+
if getattr(route, "path", None) == ("" if path == "/" else path)
|
|
53
|
+
),
|
|
54
|
+
None,
|
|
55
|
+
)
|
|
56
|
+
if route_index is None:
|
|
57
|
+
return
|
|
58
|
+
|
|
59
|
+
route = routes.pop(route_index)
|
|
60
|
+
insert_at = len(routes)
|
|
61
|
+
for index, existing in enumerate(routes):
|
|
62
|
+
if getattr(existing, "path", None) in {"", "/"}:
|
|
63
|
+
insert_at = index
|
|
64
|
+
break
|
|
65
|
+
routes.insert(insert_at, route)
|
|
@@ -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)
|
agentkit/version.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agentkit-sdk-python
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.4
|
|
4
4
|
Summary: Python SDK for transforming any AI agent into a production-ready application. Framework-agnostic primitives for runtime, memory, authentication, and tools with volcengine-managed infrastructure.
|
|
5
5
|
Author-email: Xiangrui Cheng <innsdcc@gmail.com>, Yumeng Bao <baoyumeng.123@gmail.com>, Yaozheng Fang <fangyozheng@gmail.com>, Guodong Li <cu.eric.lee@gmail.com>
|
|
6
6
|
License: Apache License
|
|
@@ -244,6 +244,13 @@ Requires-Dist: pyreadline3; sys_platform == "win32"
|
|
|
244
244
|
Provides-Extra: extensions
|
|
245
245
|
Provides-Extra: toolkit
|
|
246
246
|
Provides-Extra: dev
|
|
247
|
+
Provides-Extra: langchain
|
|
248
|
+
Requires-Dist: langchain; extra == "langchain"
|
|
249
|
+
Provides-Extra: langgraph
|
|
250
|
+
Requires-Dist: langgraph; extra == "langgraph"
|
|
251
|
+
Provides-Extra: frameworks
|
|
252
|
+
Requires-Dist: langchain; extra == "frameworks"
|
|
253
|
+
Requires-Dist: langgraph; extra == "frameworks"
|
|
247
254
|
Dynamic: license-file
|
|
248
255
|
|
|
249
256
|
<div align="center">
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
agentkit/__init__.py,sha256=l27ZMDslc3VhmmnPZJyrqVvTDoZ0LqhCtM5hw0caHcU,1021
|
|
2
2
|
agentkit/errors.py,sha256=UWlXv0ZsXd_oLTMlxuf4u9-XkTdRYeVZueE--Z3JsHU,1214
|
|
3
|
-
agentkit/version.py,sha256=
|
|
3
|
+
agentkit/version.py,sha256=S9zUkhZqaZ0llqw_JF_CXK5OsV7vd2EIpTBKb38hgCI,653
|
|
4
4
|
agentkit/apps/__init__.py,sha256=-oXTjxV3gejpJ8pffTUcUAXw0LfxKCYZJk-_hIJhtLM,1921
|
|
5
5
|
agentkit/apps/base_app.py,sha256=3hZZExL1wyTGWveJEZZoqXN086MzmkVS_WU1vyulIWg,754
|
|
6
6
|
agentkit/apps/utils.py,sha256=IzimIDmT6FS6PV9MLimPh46mq5zT-EjVmnYPxBdyEq0,1934
|
|
@@ -40,6 +40,13 @@ agentkit/client/__init__.py,sha256=WHKgNGkpYsJp0xD20drYcxAMDclwDqrfURqxEVv0V1Y,1
|
|
|
40
40
|
agentkit/client/base_agentkit_client.py,sha256=NV1m2FtboUcMdVUJL2ep75nAwDQ42roiWgl3YKaUSn8,2955
|
|
41
41
|
agentkit/client/base_iam_client.py,sha256=F3ggcZjt14CAcgDYpncF2XGr1zTGnhaOzqEJl4NGHwU,2432
|
|
42
42
|
agentkit/client/base_service_client.py,sha256=zpPUidGMDRAMDnSVjrX7BwuOp4BQdAEk0b8HoXks0TE,15049
|
|
43
|
+
agentkit/frameworks/__init__.py,sha256=NPT7kxcY7shMyA2m1QoxhXzAc9JEvDCNFyWupedTCSY,808
|
|
44
|
+
agentkit/frameworks/_common.py,sha256=0L0cyBQxtlOdi3GbQ_UgaOoHWwenztlKrxUsyvV3hXw,3859
|
|
45
|
+
agentkit/frameworks/langchain.py,sha256=Tcy2HGYqKmcugDSn3OMaD_6uxhsJtZI3o5E8Rw7UJrg,5947
|
|
46
|
+
agentkit/frameworks/langgraph.py,sha256=CHhaX6DcE8McC6Cs31gexEJHqJJ7cFiqh8D6vyQ8FVE,12770
|
|
47
|
+
agentkit/frameworks/serving/__init__.py,sha256=EaR0jzP6np8v1gyD6CYlaRoIY1KgBaFosqCmRUgMc2E,302
|
|
48
|
+
agentkit/frameworks/serving/fastapi_mount.py,sha256=LV6gJunwd7aU8MFdDYSxeaIUYWvYwMl2Am1J07ddP0A,1958
|
|
49
|
+
agentkit/frameworks/serving/langserve.py,sha256=R2R0g95LHcSFJ1BnOm2cxnULv3wQaZNUdm5Aqd-Oq5s,17238
|
|
43
50
|
agentkit/platform/__init__.py,sha256=XD1YoXWJPpLGrP410Tb_psPFKR3LUo3-47UlqayUumo,3464
|
|
44
51
|
agentkit/platform/configuration.py,sha256=lclvtiUUO1D6iwapmPygYGhNJCMfkqt4fHPBCIEqUhE,23378
|
|
45
52
|
agentkit/platform/console_urls.py,sha256=JcnjwyuxN5c4RuXcPevsxy6aInurVFtchqbibJBqjSk,1125
|
|
@@ -237,9 +244,9 @@ agentkit/utils/redact.py,sha256=RAtiggxH74eb1Da6Gg0G5SLTVWRaIE5RqYnCozSZozQ,2167
|
|
|
237
244
|
agentkit/utils/request.py,sha256=1IGxPS3BSCKk1K1_j862_v34xzLEx04CIvrLVb1MsGQ,1625
|
|
238
245
|
agentkit/utils/template_utils.py,sha256=Qjg9V6dEpjAd_yYezIIPwuDsDeeMmp6XTMn5ZECifNc,6336
|
|
239
246
|
agentkit/utils/ve_sign.py,sha256=JfKig7zni8KvdhgbLJXm3Zy_ttyzKrYGUmBY_82_A4Q,12762
|
|
240
|
-
agentkit_sdk_python-0.7.
|
|
241
|
-
agentkit_sdk_python-0.7.
|
|
242
|
-
agentkit_sdk_python-0.7.
|
|
243
|
-
agentkit_sdk_python-0.7.
|
|
244
|
-
agentkit_sdk_python-0.7.
|
|
245
|
-
agentkit_sdk_python-0.7.
|
|
247
|
+
agentkit_sdk_python-0.7.4.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
248
|
+
agentkit_sdk_python-0.7.4.dist-info/METADATA,sha256=s47zJ5ciuJGmqJoi4WcqSMqz8lUM4HWt3eAHAQ4kKGo,19880
|
|
249
|
+
agentkit_sdk_python-0.7.4.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
250
|
+
agentkit_sdk_python-0.7.4.dist-info/entry_points.txt,sha256=fhzZUsvsLXeB4mPaa0SBQiTBqFT404uDAKPaePAOUAE,58
|
|
251
|
+
agentkit_sdk_python-0.7.4.dist-info/top_level.txt,sha256=ipy8JF-QQ-V0C1oRFLxsyaW8zwrasfJ-zjAh9vgOc7U,9
|
|
252
|
+
agentkit_sdk_python-0.7.4.dist-info/RECORD,,
|
|
File without changes
|
{agentkit_sdk_python-0.7.3.dist-info → agentkit_sdk_python-0.7.4.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
{agentkit_sdk_python-0.7.3.dist-info → agentkit_sdk_python-0.7.4.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|
|
File without changes
|