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,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)
|