agentkit-sdk-python 0.7.2__py3-none-any.whl → 0.7.4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. agentkit/auth/model_login.py +87 -105
  2. agentkit/frameworks/__init__.py +29 -0
  3. agentkit/frameworks/_common.py +136 -0
  4. agentkit/frameworks/langchain.py +170 -0
  5. agentkit/frameworks/langgraph.py +349 -0
  6. agentkit/frameworks/serving/__init__.py +6 -0
  7. agentkit/frameworks/serving/fastapi_mount.py +65 -0
  8. agentkit/frameworks/serving/langserve.py +502 -0
  9. agentkit/toolkit/cli/sandbox/a2a_client.py +407 -0
  10. agentkit/toolkit/cli/sandbox/agentkit_client.py +248 -0
  11. agentkit/toolkit/cli/sandbox/cli.py +5 -0
  12. agentkit/toolkit/cli/sandbox/cli_create.py +219 -165
  13. agentkit/toolkit/cli/sandbox/cli_exec.py +2 -70
  14. agentkit/toolkit/cli/sandbox/cli_file.py +1 -1
  15. agentkit/toolkit/cli/sandbox/cli_get.py +2 -6
  16. agentkit/toolkit/cli/sandbox/cli_invoke.py +375 -0
  17. agentkit/toolkit/cli/sandbox/cli_model_login.py +57 -70
  18. agentkit/toolkit/cli/sandbox/cli_mount.py +1 -1
  19. agentkit/toolkit/cli/sandbox/env_config.py +798 -0
  20. agentkit/toolkit/cli/sandbox/model_config.py +90 -29
  21. agentkit/toolkit/cli/sandbox/session_create.py +94 -168
  22. agentkit/toolkit/cli/sandbox/session_sync.py +1 -1
  23. agentkit/toolkit/cli/sandbox/tool_resolve.py +37 -73
  24. agentkit/version.py +1 -1
  25. {agentkit_sdk_python-0.7.2.dist-info → agentkit_sdk_python-0.7.4.dist-info}/METADATA +8 -1
  26. {agentkit_sdk_python-0.7.2.dist-info → agentkit_sdk_python-0.7.4.dist-info}/RECORD +30 -19
  27. {agentkit_sdk_python-0.7.2.dist-info → agentkit_sdk_python-0.7.4.dist-info}/WHEEL +0 -0
  28. {agentkit_sdk_python-0.7.2.dist-info → agentkit_sdk_python-0.7.4.dist-info}/entry_points.txt +0 -0
  29. {agentkit_sdk_python-0.7.2.dist-info → agentkit_sdk_python-0.7.4.dist-info}/licenses/LICENSE +0 -0
  30. {agentkit_sdk_python-0.7.2.dist-info → agentkit_sdk_python-0.7.4.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,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)