adk-code-mode 0.1.0__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.
- adk_code_mode/__about__.py +4 -0
- adk_code_mode/__init__.py +22 -0
- adk_code_mode/_artifact_tools.py +152 -0
- adk_code_mode/callback.py +81 -0
- adk_code_mode/executor.py +607 -0
- adk_code_mode/output.py +97 -0
- adk_code_mode/py.typed +1 -0
- adk_code_mode/runtime/__init__.py +9 -0
- adk_code_mode/runtime/base.py +88 -0
- adk_code_mode/runtime/docker.py +331 -0
- adk_code_mode/runtime/protocol.py +193 -0
- adk_code_mode/tools/__init__.py +4 -0
- adk_code_mode/tools/catalog.py +95 -0
- adk_code_mode/tools/dispatcher.py +290 -0
- adk_code_mode/tools/namespacing.py +200 -0
- adk_code_mode/tools/normaliser.py +69 -0
- adk_code_mode/tools/stubs.py +446 -0
- adk_code_mode/workspace/__init__.py +8 -0
- adk_code_mode/workspace/files.py +44 -0
- adk_code_mode-0.1.0.dist-info/METADATA +351 -0
- adk_code_mode-0.1.0.dist-info/RECORD +23 -0
- adk_code_mode-0.1.0.dist-info/WHEEL +4 -0
- adk_code_mode-0.1.0.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2025-present A2A Net <hello@a2anet.com>
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
from adk_code_mode.__about__ import __version__
|
|
5
|
+
from adk_code_mode.callback import code_mode_before_model_callback
|
|
6
|
+
from adk_code_mode.executor import (
|
|
7
|
+
CODE_MODE_SYSTEM_INSTRUCTION,
|
|
8
|
+
ArtifactsSavedCallback,
|
|
9
|
+
CodeModeCodeExecutor,
|
|
10
|
+
)
|
|
11
|
+
from adk_code_mode.runtime import DockerRuntime, SandboxHandle, SandboxRuntime
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"ArtifactsSavedCallback",
|
|
15
|
+
"CODE_MODE_SYSTEM_INSTRUCTION",
|
|
16
|
+
"CodeModeCodeExecutor",
|
|
17
|
+
"DockerRuntime",
|
|
18
|
+
"SandboxHandle",
|
|
19
|
+
"SandboxRuntime",
|
|
20
|
+
"__version__",
|
|
21
|
+
"code_mode_before_model_callback",
|
|
22
|
+
]
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2025-present A2A Net <hello@a2anet.com>
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
"""Built-in ``save_artifact`` / ``load_artifact`` / ``list_artifacts`` tools.
|
|
5
|
+
|
|
6
|
+
These are regular ``FunctionTool`` instances injected at the front of
|
|
7
|
+
``CodeModeCodeExecutor.tools`` (set ``include_artifact_tools=False`` to opt out).
|
|
8
|
+
They appear in the rendered catalog as top-level tools; the model writes
|
|
9
|
+
``from tools import save_artifact, load_artifact, list_artifacts``.
|
|
10
|
+
|
|
11
|
+
The wire format is JSON. Binary content is base64 over the wire — the model
|
|
12
|
+
encodes it before calling ``save_artifact`` and decodes it after
|
|
13
|
+
``load_artifact``. Loaded artifacts return a flat ``{"kind", "data",
|
|
14
|
+
"mime_type"}`` dict so the model dispatches on ``kind`` without importing
|
|
15
|
+
host-side types.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import base64
|
|
21
|
+
from typing import Any
|
|
22
|
+
|
|
23
|
+
from google.adk.tools.function_tool import FunctionTool
|
|
24
|
+
from google.adk.tools.tool_context import ToolContext
|
|
25
|
+
from google.genai import types as genai_types
|
|
26
|
+
|
|
27
|
+
_TEXT_MIME_PREFIXES = ("text/",)
|
|
28
|
+
_TEXT_MIME_TYPES = {"application/json", "application/xml", "application/javascript"}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _is_text_mime(mime_type: str | None) -> bool:
|
|
32
|
+
if not mime_type:
|
|
33
|
+
return False
|
|
34
|
+
if mime_type.startswith(_TEXT_MIME_PREFIXES):
|
|
35
|
+
return True
|
|
36
|
+
if mime_type in _TEXT_MIME_TYPES:
|
|
37
|
+
return True
|
|
38
|
+
return mime_type.endswith("+json") or mime_type.endswith("+xml")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
async def save_artifact(
|
|
42
|
+
*,
|
|
43
|
+
filename: str,
|
|
44
|
+
content: str,
|
|
45
|
+
mime_type: str | None = None,
|
|
46
|
+
tool_context: ToolContext,
|
|
47
|
+
) -> int:
|
|
48
|
+
"""Save an artifact to the session. Returns the new version number.
|
|
49
|
+
|
|
50
|
+
For text or JSON, pass ``content`` as a string and set ``mime_type``:
|
|
51
|
+
|
|
52
|
+
save_artifact(
|
|
53
|
+
filename="report.json",
|
|
54
|
+
content=json.dumps({"status": "ok"}),
|
|
55
|
+
mime_type="application/json",
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
For binary, base64-encode the bytes and set the binary mime type:
|
|
59
|
+
|
|
60
|
+
save_artifact(
|
|
61
|
+
filename="image.png",
|
|
62
|
+
content=base64.b64encode(image_bytes).decode("ascii"),
|
|
63
|
+
mime_type="image/png",
|
|
64
|
+
)
|
|
65
|
+
"""
|
|
66
|
+
if _is_text_mime(mime_type):
|
|
67
|
+
part = genai_types.Part(
|
|
68
|
+
inline_data=genai_types.Blob(
|
|
69
|
+
data=content.encode("utf-8"),
|
|
70
|
+
mime_type=mime_type or "text/plain",
|
|
71
|
+
)
|
|
72
|
+
)
|
|
73
|
+
else:
|
|
74
|
+
part = genai_types.Part(
|
|
75
|
+
inline_data=genai_types.Blob(
|
|
76
|
+
data=base64.b64decode(content),
|
|
77
|
+
mime_type=mime_type or "application/octet-stream",
|
|
78
|
+
)
|
|
79
|
+
)
|
|
80
|
+
return await tool_context.save_artifact(filename, part)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
async def load_artifact(
|
|
84
|
+
*,
|
|
85
|
+
filename: str,
|
|
86
|
+
version: int | None = None,
|
|
87
|
+
tool_context: ToolContext,
|
|
88
|
+
) -> dict[str, Any] | None:
|
|
89
|
+
"""Load an artifact by filename. Returns ``None`` if not found.
|
|
90
|
+
|
|
91
|
+
Returned dict shape:
|
|
92
|
+
|
|
93
|
+
{
|
|
94
|
+
"kind": "text" | "bytes",
|
|
95
|
+
"data": <str>, # text: the string; bytes: base64-encoded
|
|
96
|
+
"mime_type": <str | None>,
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
Example:
|
|
100
|
+
|
|
101
|
+
result = load_artifact(filename="report.json")
|
|
102
|
+
if result is None:
|
|
103
|
+
return
|
|
104
|
+
if result["kind"] == "text" and result["mime_type"] == "application/json":
|
|
105
|
+
payload = json.loads(result["data"])
|
|
106
|
+
elif result["kind"] == "bytes":
|
|
107
|
+
blob = base64.b64decode(result["data"])
|
|
108
|
+
"""
|
|
109
|
+
part = await tool_context.load_artifact(filename, version=version)
|
|
110
|
+
if part is None:
|
|
111
|
+
return None
|
|
112
|
+
inline = part.inline_data
|
|
113
|
+
if inline is not None:
|
|
114
|
+
mime_type = inline.mime_type
|
|
115
|
+
data = inline.data or b""
|
|
116
|
+
if isinstance(data, str):
|
|
117
|
+
data = data.encode("utf-8")
|
|
118
|
+
if _is_text_mime(mime_type):
|
|
119
|
+
return {
|
|
120
|
+
"kind": "text",
|
|
121
|
+
"data": data.decode("utf-8"),
|
|
122
|
+
"mime_type": mime_type,
|
|
123
|
+
}
|
|
124
|
+
return {
|
|
125
|
+
"kind": "bytes",
|
|
126
|
+
"data": base64.b64encode(data).decode("ascii"),
|
|
127
|
+
"mime_type": mime_type,
|
|
128
|
+
}
|
|
129
|
+
if part.text is not None:
|
|
130
|
+
return {"kind": "text", "data": part.text, "mime_type": None}
|
|
131
|
+
return {"kind": "text", "data": "", "mime_type": None}
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
async def list_artifacts(*, tool_context: ToolContext) -> list[str]:
|
|
135
|
+
"""Return the filenames of artifacts visible to this session.
|
|
136
|
+
|
|
137
|
+
Example:
|
|
138
|
+
|
|
139
|
+
for filename in list_artifacts():
|
|
140
|
+
print(filename)
|
|
141
|
+
"""
|
|
142
|
+
return await tool_context.list_artifacts()
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
ARTIFACT_TOOLS: tuple[FunctionTool, ...] = (
|
|
146
|
+
FunctionTool(save_artifact),
|
|
147
|
+
FunctionTool(load_artifact),
|
|
148
|
+
FunctionTool(list_artifacts),
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
__all__ = ["ARTIFACT_TOOLS", "list_artifacts", "load_artifact", "save_artifact"]
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2025-present A2A Net <hello@a2anet.com>
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
"""``code_mode_before_model_callback``: inject the tool catalog into the prompt.
|
|
5
|
+
|
|
6
|
+
Wire the result of :func:`code_mode_before_model_callback` as
|
|
7
|
+
``before_model_callback`` on an ``LlmAgent``. On every model turn, the
|
|
8
|
+
callback:
|
|
9
|
+
|
|
10
|
+
1. Resolves the tools (including any ``BaseToolset`` instances) for the
|
|
11
|
+
current invocation, using the live ``CallbackContext``.
|
|
12
|
+
2. Renders the catalog — signatures + docstrings for every tool —
|
|
13
|
+
falling back to a short overflow message when the rendered catalog
|
|
14
|
+
would exceed ``max_catalog_chars``.
|
|
15
|
+
3. Appends ``\\n\\n<tools>\\n…\\n</tools>`` to
|
|
16
|
+
``llm_request.config.system_instruction``.
|
|
17
|
+
4. Caches the resolved tools on the executor so the follow-up
|
|
18
|
+
``execute_code`` call reuses them instead of re-resolving toolsets.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import logging
|
|
24
|
+
from typing import TYPE_CHECKING, Any, Awaitable, Callable
|
|
25
|
+
|
|
26
|
+
from adk_code_mode.tools import namespacing, normaliser
|
|
27
|
+
from adk_code_mode.tools.catalog import render_catalog, render_overflow_catalog
|
|
28
|
+
|
|
29
|
+
if TYPE_CHECKING:
|
|
30
|
+
from adk_code_mode.executor import CodeModeCodeExecutor
|
|
31
|
+
|
|
32
|
+
logger = logging.getLogger("adk_code_mode.callback")
|
|
33
|
+
|
|
34
|
+
_TOOLS_OPEN = "<tools>"
|
|
35
|
+
_TOOLS_CLOSE = "</tools>"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def code_mode_before_model_callback(
|
|
39
|
+
executor: "CodeModeCodeExecutor",
|
|
40
|
+
) -> Callable[..., Awaitable[None]]:
|
|
41
|
+
"""Build a ``before_model_callback`` bound to ``executor``."""
|
|
42
|
+
|
|
43
|
+
async def _callback(
|
|
44
|
+
callback_context: Any,
|
|
45
|
+
llm_request: Any,
|
|
46
|
+
**_unused: Any,
|
|
47
|
+
) -> None:
|
|
48
|
+
resolved = await normaliser.resolve(list(executor.tools), callback_context)
|
|
49
|
+
|
|
50
|
+
executor._record_resolved_tools(callback_context.invocation_id, resolved)
|
|
51
|
+
|
|
52
|
+
ns_tools = namespacing.build(resolved)
|
|
53
|
+
catalog = render_catalog(ns_tools)
|
|
54
|
+
if len(catalog) > executor.max_catalog_chars:
|
|
55
|
+
catalog = render_overflow_catalog()
|
|
56
|
+
block = f"{_TOOLS_OPEN}\n{catalog}\n{_TOOLS_CLOSE}"
|
|
57
|
+
|
|
58
|
+
_append_system_instruction(llm_request.config, block)
|
|
59
|
+
|
|
60
|
+
return _callback
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _append_system_instruction(config: Any, block: str) -> None:
|
|
64
|
+
"""Append ``block`` to ``config.system_instruction`` in place.
|
|
65
|
+
|
|
66
|
+
Handles the two shapes ADK exposes: ``None`` or ``str``, and a list of
|
|
67
|
+
items (typically strings or ``Part`` objects). The new block is always
|
|
68
|
+
appended as a final string; non-string items in a list are preserved
|
|
69
|
+
as-is.
|
|
70
|
+
"""
|
|
71
|
+
existing = config.system_instruction
|
|
72
|
+
if existing is None:
|
|
73
|
+
config.system_instruction = block
|
|
74
|
+
return
|
|
75
|
+
if isinstance(existing, str):
|
|
76
|
+
config.system_instruction = f"{existing}\n\n{block}"
|
|
77
|
+
return
|
|
78
|
+
config.system_instruction = [*existing, block]
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
__all__ = ["code_mode_before_model_callback"]
|