Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 12 additions & 12 deletions py/PARITY_AUDIT.md
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ All 39 samples have: `README.md` ✅, `run.sh` ✅, `pyproject.toml` ✅, `LICEN
| `defineStreamingFlow` | ✅ (via options) | ✅ `DefineStreamingFlow` | ✅ (via streaming param) | ✅ |
| `defineTool` | ✅ | ✅ `DefineTool` | ✅ `.tool()` decorator | ✅ |
| `defineToolWithInputSchema` | — | ✅ `DefineToolWithInputSchema` | — | Go-only |
| `defineTool({multipart: true})` | ✅ | ✅ `DefineMultipartTool` | ❌ | ❌ Python missing (G18) |
| `defineTool({multipart: true})` | ✅ | ✅ `DefineMultipartTool` | ✅ `.tool(multipart=True)` | ✅ (PR #4513) |
| `defineModel` | ✅ | ✅ `DefineModel` | ✅ `define_model` | ✅ |
| `defineBackgroundModel` | ✅ | ✅ `DefineBackgroundModel` | ✅ `define_background_model` | ✅ |
| `definePrompt` | ✅ | ✅ `DefinePrompt` | ✅ `define_prompt` | ✅ |
Expand Down Expand Up @@ -337,7 +337,7 @@ Python users typically use `httpx` or `requests` directly.
| Feature | JS | Go | Python | Gap Owner | Priority |
|---------|:--:|:--:|:------:|-----------|:--------:|
| `runFlow` / `streamFlow` client | ✅ (beta/client) | ❌ | ❌ | Go + Python | P2 |
| `defineTool({multipart: true})` | ✅ | ✅ | | Python | P1 |
| `defineTool({multipart: true})` | ✅ | ✅ | | | ✅ Done (PR #4513) |
| ~~Model API V2 (`apiVersion: 'v2'`)~~ | ~~✅~~ | ~~❌~~ | ~~❌~~ | ~~Go + Python~~ | ~~Superseded by Middleware V2 + Bidi~~ |
| **Generate Middleware V2** (3-tier: `generate`/`model`/`tool` hooks) | 🔄 RFC | 🔄 RFC | ❌ | All SDKs | P0 |
| **`defineBidiAction`** | 🔄 | 🔄 RFC | ❌ | Go + Python | P1 |
Expand Down Expand Up @@ -932,10 +932,10 @@ export function apiKey(

| Feature | JS | Python | Gap |
|---------|:--:|:------:|:---:|
| `defineTool({multipart: true})` | ✅ Supported. Creates a `MultipartToolAction` of type `tool.v2`. | ❌ Not supported. `define_tool` has no `multipart` parameter. | **G18** |
| `MultipartToolAction` type | ✅ `tool.ts:107-122` — Action with `tool.v2` type, returns `{output?, content?}`. | ❌ Does not exist. | **G18** |
| `MultipartToolResponse` type | ✅ `parts.ts` — Schema with `output` and `content` fields. | ⚠️ Type exists in `typing.py:933` but unused in tool definition. | Partial |
| Auto-registration of `tool.v2` | ✅ Non-multipart tools are also registered as `tool.v2` with wrapped output. | ❌ No dual registration. | **G18** |
| `defineTool({multipart: true})` | ✅ Supported. Creates a `MultipartToolAction` of type `tool.v2`. | ✅ `.tool(multipart=True)` registers as `tool.v2` with metadata `tool.multipart=True`. | ✅ PR #4513 |
| `MultipartToolAction` type | ✅ `tool.ts:107-122` — Action with `tool.v2` type, returns `{output?, content?}`. | ✅ Registered under `ActionKind.TOOL_V2` with appropriate metadata. | ✅ PR #4513 |
| `MultipartToolResponse` type | ✅ `parts.ts` — Schema with `output` and `content` fields. | ✅ Multipart tool functions return `{output?, content?}` dict. | ✅ PR #4513 |
| Auto-registration of `tool.v2` | ✅ Non-multipart tools are also registered as `tool.v2` with wrapped output. | ✅ Non-multipart tools register both `tool` and `tool.v2` (v2 wraps output in `{output: result}`). | ✅ PR #4513 |

**JS** (`js/ai/src/tool.ts:306-335`):
```ts
Expand Down Expand Up @@ -1234,11 +1234,11 @@ Replaces the HTTP REST-based reflection server with WebSocket + JSON-RPC 2.0 for
| G15 | Python | `download_request_media` middleware missing | P2 | `py/packages/genkit/src/genkit/blocks/middleware.py` | URL media transformed to data URI |
| G16 | Python | `simulate_system_prompt` missing | P2 | `py/packages/genkit/src/genkit/blocks/middleware.py` | system message rewritten for unsupported model |
| G17 | Python | `api_key()` context provider missing | P3 | `py/packages/genkit/src/genkit/core/context.py` | auth header extraction + policy callback tests |
| G18 | Python | multipart tool (`tool.v2`) missing | P1 | `py/packages/genkit/src/genkit/blocks/tools.py`, `.../blocks/generate.py` | tool call returns `output` + `content` parity |
| G18 | Python | ~~multipart tool (`tool.v2`) missing~~ | P1 | `ai/_registry.py`, `core/action/types.py`, `blocks/generate.py` | ✅ **Done** (PR #4513) |
| G19 | Python | Model API V2 runner interface missing | P1 | `py/packages/genkit/src/genkit/ai/_registry.py`, `.../blocks/model.py` | v2 model receives unified options struct |
| G20 | Python | `Genkit(context=...)` missing | P2 | `py/packages/genkit/src/genkit/ai/_aio.py` | context propagates to action executions |
| G21 | Python | `Genkit(clientHeader=...)` missing | P2 | `py/packages/genkit/src/genkit/ai/_aio.py`, `.../core/http_client.py` | outbound header includes custom token |
| G22 | Python | `Genkit(name=...)` missing | P2 | `py/packages/genkit/src/genkit/ai/_aio.py`, `.../core/reflection.py` | Dev UI/reflection shows custom name |
| G20 | Python | ~~`Genkit(context=...)` missing~~ | P2 | `ai/_aio.py`, `core/registry.py` | ✅ **Done** (PR #4512) |
| G21 | Python | ~~`Genkit(clientHeader=...)` missing~~ | P2 | `ai/_aio.py`, `core/constants.py` | ✅ **Done** (PR #4512) |
| G22 | Python | ~~`Genkit(name=...)` missing~~ | P2 | `ai/_aio.py`, `ai/_runtime.py`, `core/registry.py` | ✅ **Done** (PR #4512) |
| G23 | Go | `defineDynamicActionProvider` parity missing | P2 | `go/genkit/genkit.go`, `go/core/registry.go` | DAP action discovery + resolve test |
| G24 | Go | `defineIndexer` parity missing | P2 | `go/genkit/genkit.go`, `go/ai` indexing action | indexer registration + invoke test |
| G25 | Go | `defineReranker` + `rerank` runtime missing | P1 | `go/genkit/genkit.go`, `go/ai` reranker block | reranker registration + scoring call |
Expand Down Expand Up @@ -1733,8 +1733,8 @@ Milestone ▲ P1 done ▲ Upstream ▲ Middleware ▲ Bidi+Agent
|----|:-----:|------|----------|:----------:|
| **PR-1a** | Core | G2 | Add `middleware` list to `Action.__init__()`, implement `action_with_middleware()` dispatch wrapper, unit tests for middleware chaining | — |
| **PR-1b** | Core | G6 | Update `on_trace_start` callback signature to `(trace_id, span_id)` across action system + tracing, update all call sites | — |
| **PR-1c** | Core | G18 | Multipart tool support: `define_tool(multipart=True)`, `tool.v2` action type, dual registration for non-multipart tools, unit tests | |
| **PR-1d** | Core | G20, G21 | `Genkit(context=..., client_header=...)` constructor params — small additive changes, can combine in one PR | — |
| **PR-1c** | Core | G18 | ~~Multipart tool support: `define_tool(multipart=True)`, `tool.v2` action type, dual registration for non-multipart tools, unit tests~~ | ✅ PR #4513 |
| **PR-1d** | Core | G20, G21, G22 | ~~`Genkit(context=..., client_header=..., name=...)` constructor params~~ | ✅ PR #4512 |

*PR-1a is the critical-path item. Land it first to unblock Phase 2.*

Expand Down
38 changes: 35 additions & 3 deletions py/packages/genkit/src/genkit/ai/_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -522,15 +522,21 @@ async def get_tools():
return define_dap_block(self.registry, config, fn)

def tool(
self, name: str | None = None, description: str | None = None
self, name: str | None = None, description: str | None = None, *, multipart: bool = False
) -> Callable[[Callable[P, T]], Callable[P, T]]:
"""Decorator to register a function as a tool.

Args:
name: Optional name for the flow. If not provided, uses the function
name: Optional name for the tool. If not provided, uses the function
name.
description: Description for the tool to be passed to the model;
if not provided, uses the function docstring.
multipart: If True, the tool is registered as a multipart tool
(``tool.v2``). The function should return a dict with optional
``output`` and ``content`` keys. If False (default), both a
``tool`` and a ``tool.v2`` wrapper action are registered so that
the tool is discoverable under both kinds. Mirrors JS SDK's
``defineTool({ multipart: true })``.

Returns:
A decorator function that registers the tool.
Expand Down Expand Up @@ -564,14 +570,40 @@ def tool_fn_wrapper(*args: Any) -> Any: # noqa: ANN401
case _:
raise ValueError('tool must have 0-2 args...')

tool_kind = cast(ActionKind, ActionKind.TOOL_V2 if multipart else ActionKind.TOOL)
tool_metadata: dict[str, object] = {'type': 'tool.v2' if multipart else 'tool'}
if multipart:
tool_metadata['tool'] = {'multipart': True}

action = self.registry.register_action(
name=tool_name,
kind=cast(ActionKind, ActionKind.TOOL),
kind=tool_kind,
description=tool_description,
fn=tool_fn_wrapper,
metadata_fn=func,
metadata=tool_metadata,
)

# For non-multipart tools, also register a tool.v2 wrapper that
# wraps the output in {output: result} so all tools are
# discoverable under tool.v2, matching JS SDK behavior.
if not multipart:

async def v2_wrapper_fn(*args: Any) -> dict[str, object]: # noqa: ANN401
result = tool_fn_wrapper(*args)
if asyncio.iscoroutine(result):
result = await result
return {'output': result}

self.registry.register_action(
name=tool_name,
kind=cast(ActionKind, ActionKind.TOOL_V2),
description=tool_description,
fn=v2_wrapper_fn,
metadata_fn=func,
metadata={'type': 'tool.v2'},
)

@wraps(func)
async def async_wrapper(*args: P.args, **kwargs: P.kwargs) -> Any: # noqa: ANN401
"""Asynchronous wrapper for the tool function.
Expand Down
36 changes: 24 additions & 12 deletions py/packages/genkit/src/genkit/blocks/generate.py
Original file line number Diff line number Diff line change
Expand Up @@ -651,9 +651,7 @@ async def resolve_parameters(
tools: list[Action[Any, Any, Any]] = []
if request.tools:
for tool_name in request.tools:
tool_action = await registry.resolve_action(cast(ActionKind, ActionKind.TOOL), tool_name)
if tool_action is None:
raise Exception(f'Unable to resolve tool {tool_name}')
tool_action = await resolve_tool(registry, tool_name)
tools.append(tool_action)

format_def: FormatDef | None = None
Expand Down Expand Up @@ -825,17 +823,26 @@ async def _resolve_tool_request(tool: Action, tool_request_part: ToolRequestPart
"""
try:
tool_response = (await tool.arun_raw(tool_request_part.tool_request.input)).response
# For tool.v2 actions, the response is a multipart dict with separate
# 'output' and 'content' fields. Extract them into the ToolResponse
# individually, matching the JS SDK's resolveToolRequest behavior.
if tool.kind == ActionKind.TOOL_V2 and isinstance(tool_response, dict):
multipart = tool_response
tool_resp = ToolResponse(
name=tool_request_part.tool_request.name,
ref=tool_request_part.tool_request.ref,
output=multipart.get('output'),
content=multipart.get('content'),
)
else:
tool_resp = ToolResponse(
name=tool_request_part.tool_request.name,
ref=tool_request_part.tool_request.ref,
output=dump_dict(tool_response),
)
# Part is a RootModel, so we pass content via 'root' parameter
return (
Part(
root=ToolResponsePart(
tool_response=ToolResponse(
name=tool_request_part.tool_request.name,
ref=tool_request_part.tool_request.ref,
output=dump_dict(tool_response),
)
)
),
Part(root=ToolResponsePart(tool_response=tool_resp)),
None,
)
except GenkitError as e:
Expand Down Expand Up @@ -867,6 +874,9 @@ async def _resolve_tool_request(tool: Action, tool_request_part: ToolRequestPart
async def resolve_tool(registry: Registry, tool_name: str) -> Action:
"""Resolve a tool by name from the registry.

Looks up the tool under both ``tool`` and ``tool.v2`` action kinds,
matching the JS SDK's ``lookupToolByName`` behavior.

Args:
registry: The registry to resolve the tool from.
tool_name: The name of the tool to resolve.
Expand All @@ -878,6 +888,8 @@ async def resolve_tool(registry: Registry, tool_name: str) -> Action:
ValueError: If the tool could not be resolved.
"""
tool = await registry.resolve_action(kind=cast(ActionKind, ActionKind.TOOL), name=tool_name)
if tool is None:
tool = await registry.resolve_action(kind=cast(ActionKind, ActionKind.TOOL_V2), name=tool_name)
if tool is None:
raise ValueError(f'Unable to resolve tool {tool_name}')
return tool
Expand Down
1 change: 1 addition & 0 deletions py/packages/genkit/src/genkit/core/action/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ class ActionKind(StrEnum):
RESOURCE = 'resource'
RETRIEVER = 'retriever'
TOOL = 'tool'
TOOL_V2 = 'tool.v2'
UTIL = 'util'


Expand Down
Loading
Loading