feat(py): generate middleware#5253
Conversation
There was a problem hiding this comment.
Code Review
This pull request implements a robust middleware system for Genkit, enabling interception and modification of model generation, API calls, and tool executions. It introduces the genkit-plugin-middleware package, providing standard middleware for retries, fallbacks, tool approval, skills, and filesystem operations. Core generation logic was refactored to handle middleware normalization and per-call scoping. Feedback identifies redundant model copies in the asynchronous generation methods and a design conflict between validation tests and the normalization implementation. Furthermore, the reviewer noted blocking synchronous I/O in asynchronous tool implementations and issues with the jitter calculation in the retry middleware that could cause delays to exceed configured maximums.
| # FunctionResponse.response must be a dict, not a raw value. | ||
| output = tool_output if isinstance(tool_output, dict) else {'result': tool_output} | ||
|
|
||
| # --- Primary path: ToolResponse.content (set by MultipartToolResponse.content) --- |
There was a problem hiding this comment.
add a more descriptive comment
| is covariant: ``list[Tool]`` or ``list[str]`` are both assignable to | ||
| ``Sequence[str | Tool]``, but not to ``list[str | Tool]``. | ||
| """ | ||
| registry = await registry_with_inline_tools(self.registry, tools) |
There was a problem hiding this comment.
child_registry = registry.new_child()
await register_tools(child_registry, tools)
refs = register_middleware(child_registry, middleware) # unnamed middleware get auto-generated names
| raise GenkitError( | ||
| status='NOT_FOUND', | ||
| message=( | ||
| f'No middleware named "{entry.name}" is registered on this app. ' |
There was a problem hiding this comment.
"A middleware with the name "{entry.name}" cannot be found. Please make sure the middleware is registered correctly via the @ai.middleware(...) decorator if you defined one inline, or pass a middleware instance directly."
Python middleware for
generate()—use=[...]Adds a middleware system for Python that lets you intercept and wrap
generate()calls at three granularities: the full generate iteration, each model API call, and each tool execution. Replaces the old ModelMiddleware abstraction.What it does
Middleware is applied per-
generate()call via ause=[...]parameter. Each entry in the list wraps the call in a chain — first entry is outermost (runs first/last). Three hooks are available:wrap_generate— wraps each iteration of the tool loop (model call + tool resolution). Runs once per agentic turn.wrap_model— wraps each raw model API call. Use for retry, fallback, logging latency.wrap_tool— wraps each individual tool execution. Use for approval gates, sandboxing, error enrichment.tools()— contribute extra tools dynamically pergenerate()call (e.g. skills libraries, sandboxed filesystem ops). Tools are scoped to the call and don't pollute the root registry.Defining middleware inline (app developers)
Subclass
BaseMiddleware(a Pydantic model) — config fields and hook overrides in one class. Pass instances directly inuse=[...]:Dev UI integration
Plugin middleware (e.g. anything registered via
middleware_bundle()or a custommiddleware_plugin(...)) is automatically available in the Dev UI — no extra step needed.For middleware you define yourself in app code, call
ai.define_middleware()to publish it to the registry so the Dev UI can discover it by name:Once registered, the middleware shows up on the Model Runner page in the Dev UI, where you can mix-and-match middleware and set config values interactively. When you run a generate call from there, the Dev UI passes a
MiddlewareRef— a name plus a config dict — intogenerate_action. The framework resolves that ref against the registry, instantiates the middleware class with the provided config (cls(**config)), and runs the chain exactly as it would inline. TheMiddlewareRefwire format is what makes dynamic, config-driven dispatch possible in the Dev UI case without requiring code changes.Pre-packaging middleware through a plugin (plugin authors)
Use
new_middlewareto build aMiddlewareDescfrom aBaseMiddlewaresubclass, then wrap them withmiddleware_pluginto produce a standardPluginforplugins=[...]:mylib/middleware.py (plugin author):
app.py (app developer):