diff --git a/ai/src/main/java/com/google/genkit/ai/GenerateAction.java b/ai/src/main/java/com/google/genkit/ai/GenerateAction.java index fb193f689..e7ce6b0a3 100644 --- a/ai/src/main/java/com/google/genkit/ai/GenerateAction.java +++ b/ai/src/main/java/com/google/genkit/ai/GenerateAction.java @@ -20,6 +20,13 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.genkit.ai.middleware.GenerateNext; +import com.google.genkit.ai.middleware.GenerateParams; +import com.google.genkit.ai.middleware.GenerationMiddleware; +import com.google.genkit.ai.middleware.ModelNext; +import com.google.genkit.ai.middleware.ModelParams; +import com.google.genkit.ai.middleware.ToolNext; +import com.google.genkit.ai.middleware.ToolParams; import com.google.genkit.ai.telemetry.ModelTelemetryHelper; import com.google.genkit.core.*; import com.google.genkit.core.tracing.SpanMetadata; @@ -94,42 +101,62 @@ public ModelResponse run( throw new GenkitException("GenerateActionOptions cannot be null"); } + // Resolve middleware names from the registry. The Dev UI's Middleware panel sends + // selected middleware as a list of names in `options.use`. Each name is looked up + // in the "middleware" value bucket (registered via Genkit.Builder.middleware(...)). + // Mirrors the JS SDK's resolveMiddleware() in js/ai/src/generate/action.ts. + final List middlewares = resolveMiddlewares(options.getUse()); + + // Core: run the full tool loop (possibly streaming). model.run() inside is wrapped + // by the wrapModel chain; tool execution is wrapped by the wrapTool chain. + GenerateNext core = + (gctx, gparams) -> + runIterations(gctx, gparams.getRequest(), gparams.getOnChunk(), middlewares); + + // Outermost: wrapGenerate chain + GenerateNext chain = chainGenerate(middlewares, core); + + int initialMsgIdx = options.getMessages() != null ? options.getMessages().size() : 0; + return chain.apply(ctx, new GenerateParams(options, 0, initialMsgIdx, streamCallback)); + } + + /** + * Executes the tool-call loop. Each iteration wraps the model call in the {@code wrapModel} + * middleware chain and tool execution in the {@code wrapTool} chain. + */ + private ModelResponse runIterations( + ActionContext ctx, + GenerateActionOptions options, + Consumer streamCallback, + List middlewares) + throws GenkitException { + String modelName = options.getModel(); if (modelName == null || modelName.isEmpty()) { throw new GenkitException("Model name is required"); } - // Resolve the model action key String modelKey = resolveModelKey(modelName); - - // Look up the model in the registry Action action = registry.lookupAction(modelKey); if (action == null) { throw new GenkitException("Model not found: " + modelName + " (key: " + modelKey + ")"); } - if (!(action instanceof Model)) { throw new GenkitException("Action is not a model: " + modelKey); } + final Model model = (Model) action; - Model model = (Model) action; - - // Build the model request from the options ModelRequest request = buildModelRequest(options); logger.debug("Generating with model: {}", modelKey); - // Determine if we should return tool requests without executing them boolean returnToolRequests = Boolean.TRUE.equals(options.getReturnToolRequests()); - - // Get max turns for tool loop (default to 5) int maxTurns = options.getMaxTurns() != null ? options.getMaxTurns() : 5; int turn = 0; String flowName = ctx.getFlowName(); while (turn < maxTurns) { - // Create span metadata for the model call SpanMetadata spanMetadata = SpanMetadata.builder() .name(modelName) @@ -144,7 +171,7 @@ public ModelResponse run( final ModelRequest currentRequest = request; final String spanPath = "/generate/" + modelName; - // Run the model wrapped in a span + // Run the model wrapped in a span and through the wrapModel middleware chain. ModelResponse response = Tracer.runInNewSpan( ctx, @@ -152,49 +179,46 @@ public ModelResponse run( request, (spanCtx, req) -> { ActionContext newCtx = ctx.withSpanContext(spanCtx); - if (streamCallback != null && model.supportsStreaming()) { - return ModelTelemetryHelper.runWithTelemetryStreaming( - modelName, - flowName, - spanPath, - currentRequest, - r -> model.run(newCtx, r, streamCallback)); - } else { - return ModelTelemetryHelper.runWithTelemetry( - modelName, flowName, spanPath, currentRequest, r -> model.run(newCtx, r)); - } + ModelNext modelCore = + (mctx, mparams) -> { + ModelRequest mreq = mparams.getRequest(); + Consumer sc = mparams.getStreamCallback(); + if (sc != null && model.supportsStreaming()) { + return ModelTelemetryHelper.runWithTelemetryStreaming( + modelName, flowName, spanPath, mreq, r -> model.run(mctx, r, sc)); + } else { + return ModelTelemetryHelper.runWithTelemetry( + modelName, flowName, spanPath, mreq, r -> model.run(mctx, r)); + } + }; + ModelNext wrappedModel = chainModel(middlewares, modelCore); + return wrappedModel.apply(newCtx, new ModelParams(currentRequest, streamCallback)); }); // Check if the model requested tool calls List toolRequestParts = extractToolRequestParts(response); - // If no tool requests or we should return them without executing, return - // response if (toolRequestParts.isEmpty() || returnToolRequests) { return response; } - // Check if we have tools to execute if (options.getTools() == null || options.getTools().isEmpty()) { - // No tools available, return response with tool requests return response; } - // Execute tools - List toolResponseParts = executeTools(ctx, toolRequestParts, options.getTools()); + // Execute tools through the wrapTool chain + List toolResponseParts = + executeTools(ctx, toolRequestParts, options.getTools(), middlewares); - // Add the assistant message with tool requests Message assistantMessage = response.getMessage(); List updatedMessages = new ArrayList<>(request.getMessages()); updatedMessages.add(assistantMessage); - // Add tool response message Message toolResponseMessage = new Message(); toolResponseMessage.setRole(Role.TOOL); toolResponseMessage.setContent(toolResponseParts); updatedMessages.add(toolResponseMessage); - // Update request with new messages for next turn request = ModelRequest.builder() .messages(updatedMessages) @@ -209,6 +233,80 @@ public ModelResponse run( throw new GenkitException("Max tool execution turns (" + maxTurns + ") exceeded"); } + /** + * Resolves middleware references to fresh per-call middleware instances by looking them up in the + * registry's {@code "middleware"} value bucket. Accepts either bare JSON strings or objects with + * a {@code name} field (the shape the Dev UI's Middleware panel sends). Unknown names are logged + * and skipped. + */ + private List resolveMiddlewares(List refs) { + if (refs == null || refs.isEmpty()) { + return List.of(); + } + List resolved = new ArrayList<>(refs.size()); + for (JsonNode ref : refs) { + if (ref == null || ref.isNull()) continue; + String name; + if (ref.isTextual()) { + name = ref.asText(); + } else if (ref.isObject() && ref.hasNonNull("name")) { + name = ref.get("name").asText(); + } else { + logger.warn("Unrecognized middleware reference shape: {}", ref); + continue; + } + if (name == null || name.isEmpty()) continue; + Object value = registry.lookupValue("middleware", name); + if (value instanceof GenerationMiddleware) { + // Use a fresh instance per call so middleware state is per-invocation. + resolved.add(((GenerationMiddleware) value).newInstance()); + } else { + logger.warn( + "Middleware '{}' was requested but is not registered. " + + "Register via Genkit.Builder.middleware(...).", + name); + } + } + return resolved; + } + + /** Chains wrapGenerate hooks. First middleware is outermost. */ + private static GenerateNext chainGenerate( + List middlewares, GenerateNext core) { + if (middlewares.isEmpty()) return core; + GenerateNext current = core; + for (int i = middlewares.size() - 1; i >= 0; i--) { + final GenerationMiddleware mw = middlewares.get(i); + final GenerateNext next = current; + current = (ctx, params) -> mw.wrapGenerate(ctx, params, next); + } + return current; + } + + /** Chains wrapModel hooks. First middleware is outermost. */ + private static ModelNext chainModel(List middlewares, ModelNext core) { + if (middlewares.isEmpty()) return core; + ModelNext current = core; + for (int i = middlewares.size() - 1; i >= 0; i--) { + final GenerationMiddleware mw = middlewares.get(i); + final ModelNext next = current; + current = (ctx, params) -> mw.wrapModel(ctx, params, next); + } + return current; + } + + /** Chains wrapTool hooks. First middleware is outermost. */ + private static ToolNext chainTool(List middlewares, ToolNext core) { + if (middlewares.isEmpty()) return core; + ToolNext current = core; + for (int i = middlewares.size() - 1; i >= 0; i--) { + final GenerationMiddleware mw = middlewares.get(i); + final ToolNext next = current; + current = (ctx, params) -> mw.wrapTool(ctx, params, next); + } + return current; + } + /** Extracts tool request parts from a model response. */ private List extractToolRequestParts(ModelResponse response) { List toolRequestParts = new ArrayList<>(); @@ -224,20 +322,45 @@ private List extractToolRequestParts(ModelResponse response) { return toolRequestParts; } - /** Executes tools and returns the response parts. */ + /** Executes tools through the wrapTool middleware chain and returns the response parts. */ private List executeTools( - ActionContext ctx, List toolRequestParts, List toolNames) { + ActionContext ctx, + List toolRequestParts, + List toolNames, + List middlewares) { List responseParts = new ArrayList<>(); + // Core tool invocation — runs after all wrapTool middleware + ToolNext toolCore = + (tctx, tparams) -> { + Tool tool = tparams.getTool(); + ToolRequest toolReq = tparams.getRequest(); + Object toolInput = toolReq.getInput(); + + // Convert input if necessary + if (toolInput instanceof Map + && tool.getInputClass() != null + && !Map.class.isAssignableFrom(tool.getInputClass())) { + toolInput = objectMapper.convertValue(toolInput, tool.getInputClass()); + } + + @SuppressWarnings("unchecked") + Tool typedTool = (Tool) tool; + Object result = typedTool.run(tctx, toolInput); + + Part responsePart = new Part(); + responsePart.setToolResponse( + new ToolResponse(toolReq.getRef(), toolReq.getName(), result)); + return responsePart; + }; + ToolNext wrappedTool = chainTool(middlewares, toolCore); + for (Part toolRequestPart : toolRequestParts) { ToolRequest toolRequest = toolRequestPart.getToolRequest(); String toolName = toolRequest.getName(); - Object toolInput = toolRequest.getInput(); - // Find the tool Tool tool = findTool(toolName, toolNames); if (tool == null) { - // Tool not found, create an error response Part errorPart = new Part(); ToolResponse errorResponse = new ToolResponse( @@ -249,26 +372,8 @@ private List executeTools( } try { - // Execute the tool - @SuppressWarnings("unchecked") - Tool typedTool = (Tool) tool; - - // Convert input if necessary - Object convertedInput = toolInput; - if (toolInput instanceof Map - && tool.getInputClass() != null - && !Map.class.isAssignableFrom(tool.getInputClass())) { - convertedInput = objectMapper.convertValue(toolInput, tool.getInputClass()); - } - - Object result = typedTool.run(ctx, convertedInput); - - // Create tool response part - Part responsePart = new Part(); - ToolResponse toolResponse = new ToolResponse(toolRequest.getRef(), toolName, result); - responsePart.setToolResponse(toolResponse); + Part responsePart = wrappedTool.apply(ctx, new ToolParams(toolRequestPart, tool)); responseParts.add(responsePart); - logger.debug("Executed tool '{}' successfully", toolName); } catch (Exception e) { logger.error("Tool execution failed for '{}': {}", toolName, e.getMessage()); diff --git a/ai/src/main/java/com/google/genkit/ai/GenerateActionOptions.java b/ai/src/main/java/com/google/genkit/ai/GenerateActionOptions.java index e3ebd52fb..f301a37f2 100644 --- a/ai/src/main/java/com/google/genkit/ai/GenerateActionOptions.java +++ b/ai/src/main/java/com/google/genkit/ai/GenerateActionOptions.java @@ -20,6 +20,7 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.JsonNode; import java.util.ArrayList; import java.util.List; @@ -81,6 +82,23 @@ public class GenerateActionOptions { @JsonProperty("stepName") private String stepName; + /** + * Middleware references. In the JS SDK, this is a list of {@code ModelMiddleware} functions or + * name strings. The Dev UI populates this field with the names of middlewares the user has + * selected in the Middleware panel (which correspond to middlewares registered via {@code + * Genkit.Builder.middleware(...)} under the {@code "middleware"} value bucket). + * + *

At runtime, {@link GenerateAction#run} resolves each name via {@code + * registry.lookupValue("middleware", name)} and dispatches the {@code wrapGenerate}/{@code + * wrapModel}/{@code wrapTool} hooks around the model invocation. + */ + /** + * Each element may be a JSON string (middleware name) or a JSON object with a {@code name} field, + * matching the shape sent by the Dev UI's Middleware panel. + */ + @JsonProperty("use") + private List use; + /** Default constructor for JSON deserialization. */ public GenerateActionOptions() {} @@ -109,6 +127,7 @@ public GenerateActionOptions withMessages(List newMessages) { copy.returnToolRequests = this.returnToolRequests; copy.maxTurns = this.maxTurns; copy.stepName = this.stepName; + copy.use = this.use; return copy; } @@ -216,4 +235,12 @@ public String getStepName() { public void setStepName(String stepName) { this.stepName = stepName; } + + public List getUse() { + return use; + } + + public void setUse(List use) { + this.use = use; + } } diff --git a/core/src/main/java/com/google/genkit/core/DefaultRegistry.java b/core/src/main/java/com/google/genkit/core/DefaultRegistry.java index 88465de93..774e73e23 100644 --- a/core/src/main/java/com/google/genkit/core/DefaultRegistry.java +++ b/core/src/main/java/com/google/genkit/core/DefaultRegistry.java @@ -35,6 +35,7 @@ public class DefaultRegistry implements Registry { private final Map> actions = new ConcurrentHashMap<>(); private final Map plugins = new ConcurrentHashMap<>(); private final Map values = new ConcurrentHashMap<>(); + private final Map> valuesByType = new ConcurrentHashMap<>(); private final Map> schemas = new ConcurrentHashMap<>(); private final Map partials = new ConcurrentHashMap<>(); private final Map helpers = new ConcurrentHashMap<>(); @@ -90,6 +91,16 @@ public void registerValue(String name, Object value) { logger.debug("Registered value: {}", name); } + @Override + public void registerValue(String type, String name, Object value) { + Map bucket = valuesByType.computeIfAbsent(type, t -> new ConcurrentHashMap<>()); + if (bucket.containsKey(name)) { + throw new IllegalStateException("Value already registered: " + type + "/" + name); + } + bucket.put(name, value); + logger.debug("Registered value: {}/{}", type, name); + } + @Override public void registerSchema(String name, Map schema) { if (schemas.containsKey(name)) { @@ -126,6 +137,16 @@ public Object lookupValue(String name) { return value; } + @Override + public Object lookupValue(String type, String name) { + Map bucket = valuesByType.get(type); + Object value = bucket != null ? bucket.get(name) : null; + if (value == null && parent != null) { + value = parent.lookupValue(type, name); + } + return value; + } + @Override public Map lookupSchema(String name) { Map schema = schemas.get(name); @@ -252,6 +273,19 @@ public Map listValues() { return allValues; } + @Override + public Map listValues(String type) { + Map allValues = new LinkedHashMap<>(); + if (parent != null) { + allValues.putAll(parent.listValues(type)); + } + Map bucket = valuesByType.get(type); + if (bucket != null) { + allValues.putAll(bucket); + } + return allValues; + } + @Override public void registerPartial(String name, String source) { partials.put(name, source); diff --git a/core/src/main/java/com/google/genkit/core/Registry.java b/core/src/main/java/com/google/genkit/core/Registry.java index 431fdf8c2..3150f443c 100644 --- a/core/src/main/java/com/google/genkit/core/Registry.java +++ b/core/src/main/java/com/google/genkit/core/Registry.java @@ -72,7 +72,7 @@ public interface Registry { void registerAction(String key, Action action); /** - * Records an arbitrary value in the registry. + * Records an arbitrary value in the registry under the default type bucket. * * @param name the value name * @param value the value to register @@ -80,6 +80,20 @@ public interface Registry { */ void registerValue(String name, Object value); + /** + * Records an arbitrary value in the registry under the given type bucket. + * + *

This mirrors the JS reflection API which keys values by {@code (type, name)} (e.g. {@code + * type="middleware"}, {@code type="defaultModel"}). Values registered here are exposed via the + * {@code /api/values?type=...} reflection endpoint and surfaced in the Genkit Dev UI. + * + * @param type the value type bucket (e.g. {@code "middleware"}) + * @param name the value name + * @param value the value to register + * @throws IllegalStateException if a value with the same (type, name) is already registered + */ + void registerValue(String type, String name, Object value); + /** * Records a JSON schema in the registry. * @@ -127,6 +141,16 @@ public interface Registry { */ Object lookupValue(String name); + /** + * Returns the value for the given type bucket and name. It first checks the current registry, + * then falls back to the parent if not found. + * + * @param type the value type bucket (e.g. {@code "middleware"}) + * @param name the value name + * @return the value, or null if not found + */ + Object lookupValue(String type, String name); + /** * Returns a JSON schema for the given name. It first checks the current registry, then falls back * to the parent if not found. @@ -190,12 +214,21 @@ default void registerAction(ActionType type, Action action) { List listPlugins(); /** - * Returns a map of all registered values. + * Returns a map of all registered values in the default type bucket. * * @return map of all registered values */ Map listValues(); + /** + * Returns a map of all registered values under the given type bucket. This includes values from + * both the current registry and its parent hierarchy. + * + * @param type the value type bucket (e.g. {@code "middleware"}) + * @return map of values keyed by name, or empty map if none + */ + Map listValues(String type); + /** * Registers a partial template for use with prompts. * diff --git a/docs/src/content/docs/middleware.md b/docs/src/content/docs/middleware.md index 89c945724..b12693ed0 100644 --- a/docs/src/content/docs/middleware.md +++ b/docs/src/content/docs/middleware.md @@ -314,6 +314,27 @@ ModelResponse response = genkit.generate( Middleware order matters — the **first** middleware listed is **outermost** (runs first on the way in, last on the way out). +### Using middleware from the Dev UI + +To make middleware selectable from the **Middleware** panel in the Genkit Dev UI, register it with the `Genkit` builder via `.middleware(...)`: + +```java +GenerationMiddleware modelLogging = new ModelLoggingMiddleware(); +GenerationMiddleware timing = new GenerateTimingMiddleware(); +GenerationMiddleware toolMonitor = new ToolMonitorMiddleware(); + +Genkit genkit = Genkit.builder() + .plugin(OpenAIPlugin.create()) + .middleware(modelLogging, timing, toolMonitor) + .build(); +``` + +Each registered middleware appears in the Dev UI Middleware panel by `name()`. When you select one or more middlewares in the panel and run a model from the **Models** runner, the Dev UI invokes the `/util/generate` action with the selected middleware names in the `use` field. The action resolves each name back to the registered middleware via the registry and runs it through the same `wrapGenerate` / `wrapModel` / `wrapTool` chain that applies to programmatic `generate()` calls. + +A fresh middleware instance (via `newInstance()`) is created per Dev UI invocation, so per-request state (counters, timers) is isolated just as it is for code-driven calls. + +> **Note:** `.middleware(...)` only controls Dev UI visibility. Middleware attached programmatically with `GenerateOptions.builder().use(...)` does not need to be registered with the builder. + ### Multi-hook middleware A single middleware can implement all three hooks to observe every stage: diff --git a/genkit/src/main/java/com/google/genkit/Genkit.java b/genkit/src/main/java/com/google/genkit/Genkit.java index 67314e07e..8741d50a9 100644 --- a/genkit/src/main/java/com/google/genkit/Genkit.java +++ b/genkit/src/main/java/com/google/genkit/Genkit.java @@ -678,6 +678,10 @@ private ModelResponse generateInternal( int maxTurns = options.getMaxTurns() != null ? options.getMaxTurns() : 5; + // Auto-register any middleware passed via .use(...) so it shows up in the Dev UI + // Middleware panel. Registration is idempotent (last write wins for a given name). + registerMiddlewareForDevUi(options.getUse()); + // Create fresh middleware instances for this invocation List middlewares = createMiddlewareInstances(options.getUse()); @@ -859,6 +863,26 @@ private List createMiddlewareInstances(List use) { + if (use == null || use.isEmpty()) { + return; + } + for (GenerationMiddleware mw : use) { + if (mw == null) continue; + String name = mw.name(); + if (name == null || name.isEmpty()) continue; + if (registry.lookupValue("middleware", name) == null) { + registry.registerValue("middleware", name, mw); + } + } + } + /** * Converts {@link GenerateOptions} to a high-level {@link GenerateActionOptions}. * @@ -2424,6 +2448,7 @@ public EvalStore getEvalStore() { /** Builder for Genkit. */ public static class Builder { private final List plugins = new ArrayList<>(); + private final List middlewares = new ArrayList<>(); private GenkitOptions options = GenkitOptions.builder().build(); /** @@ -2448,6 +2473,22 @@ public Builder plugin(Plugin plugin) { return this; } + /** + * Optional: pre-registers one or more generation middlewares so they show up in the Genkit Dev + * UI Middleware panel before any flow has executed. This is a UX convenience only — + * middlewares are also auto-registered the first time they appear in a {@code + * GenerateOptions.use(...)} call, so production code does not need to declare them here. + * + * @param middlewares the middlewares to pre-register + * @return this builder + */ + public Builder middleware(GenerationMiddleware... middlewares) { + for (GenerationMiddleware mw : middlewares) { + this.middlewares.add(mw); + } + return this; + } + /** * Enables dev mode. * @@ -2479,6 +2520,9 @@ public Genkit build() { Genkit genkit = new Genkit(options); genkit.plugins.addAll(plugins); genkit.init(); + // Pre-register any middlewares declared via .middleware(...) so the Dev UI + // Middleware panel can list them before any generate() call runs. + genkit.registerMiddlewareForDevUi(middlewares); return genkit; } } diff --git a/genkit/src/main/java/com/google/genkit/ReflectionServer.java b/genkit/src/main/java/com/google/genkit/ReflectionServer.java index d03d7be9e..05bbe4636 100644 --- a/genkit/src/main/java/com/google/genkit/ReflectionServer.java +++ b/genkit/src/main/java/com/google/genkit/ReflectionServer.java @@ -272,6 +272,16 @@ public boolean handle(Request request, Response response, Callback callback) thr } else if (target.startsWith("/api/actions/")) { String actionKey = target.substring("/api/actions/".length()); result = handleGetAction(actionKey); + } else if ("/api/values".equals(target) && "GET".equals(method)) { + String query = request.getHttpURI().getQuery(); + String type = parseQueryParam(query, "type"); + if (type == null) { + status = 400; + result = + createErrorResponse(3, "Query parameter \"type\" is required.", null); // 3=INVALID + } else { + result = handleListValuesByType(type); + } } else if ("/api/notify".equals(target) && "POST".equals(method)) { String body = readRequestBody(request); result = handleNotify(body); @@ -464,6 +474,60 @@ private String handleGetAction(String actionKey) { return JsonUtils.toJson(actionInfo); } + /** + * Parses a query string and returns the value for the given parameter name, or null if absent. + */ + private String parseQueryParam(String query, String name) { + if (query == null || query.isEmpty()) return null; + for (String pair : query.split("&")) { + int eq = pair.indexOf('='); + String k = eq >= 0 ? pair.substring(0, eq) : pair; + String v = eq >= 0 ? pair.substring(eq + 1) : ""; + if (k.equals(name)) { + try { + return java.net.URLDecoder.decode(v, java.nio.charset.StandardCharsets.UTF_8); + } catch (Exception e) { + return v; + } + } + } + return null; + } + + /** + * Handles {@code GET /api/values?type=}. Returns a map of name -> value JSON + * representation for all values registered under the given type bucket. Mirrors the JS + * reflection API used by the Dev UI Middleware panel (type=middleware) and default-model + * indicator (type=defaultModel). + */ + private String handleListValuesByType(String type) { + Map values = registry.listValues(type); + Map mapped = new HashMap<>(); + if (values != null) { + for (Map.Entry e : values.entrySet()) { + mapped.put(e.getKey(), serializeValue(e.getValue(), e.getKey())); + } + } + return JsonUtils.toJson(mapped); + } + + /** + * Serializes a registered value for the reflection API. Special-cases known interfaces (e.g. + * {@link com.google.genkit.ai.middleware.GenerationMiddleware}) which can't be JSON-serialized + * directly. + */ + private Object serializeValue(Object value, String fallbackName) { + if (value == null) return null; + if (value instanceof com.google.genkit.ai.middleware.GenerationMiddleware) { + com.google.genkit.ai.middleware.GenerationMiddleware mw = + (com.google.genkit.ai.middleware.GenerationMiddleware) value; + Map json = new HashMap<>(); + json.put("name", mw.name() != null ? mw.name() : fallbackName); + return json; + } + return value; + } + private String handleRunAction(String body) throws GenkitException { JsonNode requestNode = JsonUtils.parseJson(body); diff --git a/genkit/src/main/java/com/google/genkit/ReflectionServerV2.java b/genkit/src/main/java/com/google/genkit/ReflectionServerV2.java index 7f8a20058..8d16a3397 100644 --- a/genkit/src/main/java/com/google/genkit/ReflectionServerV2.java +++ b/genkit/src/main/java/com/google/genkit/ReflectionServerV2.java @@ -386,9 +386,28 @@ private void handleListActions(String requestId) { private void handleListValues(String requestId, JsonNode params) { if (requestId == null) return; - // Currently no values to list for Java runtime + String type = + (params != null && params.hasNonNull("type")) ? params.get("type").asText() : null; + Map values = new HashMap<>(); + if (type != null) { + Map raw = registry.listValues(type); + if (raw != null) { + for (Map.Entry e : raw.entrySet()) { + Object v = e.getValue(); + if (v instanceof com.google.genkit.ai.middleware.GenerationMiddleware) { + com.google.genkit.ai.middleware.GenerationMiddleware mw = + (com.google.genkit.ai.middleware.GenerationMiddleware) v; + Map json = new HashMap<>(); + json.put("name", mw.name() != null ? mw.name() : e.getKey()); + values.put(e.getKey(), json); + } else { + values.put(e.getKey(), v); + } + } + } + } Map result = new HashMap<>(); - result.put("values", new HashMap<>()); + result.put("values", values); sendResponse(requestId, result); } diff --git a/samples/middleware-v2/README.md b/samples/middleware-v2/README.md index 5726e101c..f1dbb7678 100644 --- a/samples/middleware-v2/README.md +++ b/samples/middleware-v2/README.md @@ -130,6 +130,21 @@ ModelResponse response = genkit.generate( .build()); ``` +## Using middleware from the Dev UI + +Register middleware with the `Genkit` builder so they appear in the Dev UI **Middleware** panel: + +```java +Genkit genkit = Genkit.builder() + .plugin(OpenAIPlugin.create()) + .middleware(new MyMiddleware(), new AnotherMiddleware()) + .build(); +``` + +In the Dev UI, open the Middleware panel, tick one or more middlewares, then run any model from the **Models** runner. The Dev UI sends the selected middleware names in the `use` field of the `/util/generate` action, which resolves them from the registry and dispatches the full `wrapGenerate` / `wrapModel` / `wrapTool` chain — middleware logs will appear in the server console. + +`.middleware(...)` only controls Dev UI visibility; programmatic `GenerateOptions.builder().use(...)` calls do not require registration. + ## Architecture V2 middleware wraps the generation pipeline at three levels: diff --git a/samples/middleware-v2/src/main/java/com/google/genkit/samples/MiddlewareV2Sample.java b/samples/middleware-v2/src/main/java/com/google/genkit/samples/MiddlewareV2Sample.java index 476eb2e04..de63dd5a0 100644 --- a/samples/middleware-v2/src/main/java/com/google/genkit/samples/MiddlewareV2Sample.java +++ b/samples/middleware-v2/src/main/java/com/google/genkit/samples/MiddlewareV2Sample.java @@ -236,19 +236,22 @@ public Part wrapTool(ActionContext ctx, ToolParams params, ToolNext next) public static void main(String[] args) throws Exception { JettyPlugin jetty = new JettyPlugin(JettyPluginOptions.builder().port(8080).build()); + // Instantiate middleware (templates — newInstance() is called per generate()) + GenerationMiddleware modelLogging = new ModelLoggingMiddleware(); + GenerationMiddleware generateTiming = new GenerateTimingMiddleware(); + GenerationMiddleware toolMonitor = new ToolMonitorMiddleware(); + GenerationMiddleware fullObservability = new FullObservabilityMiddleware(); + Genkit genkit = Genkit.builder() .options(GenkitOptions.builder().devMode(true).reflectionPort(3100).build()) .plugin(OpenAIPlugin.create()) .plugin(jetty) + // Register middlewares so they show up in the Dev UI Middleware panel. + // (Middleware is still applied per generate() call via GenerateOptions.use(...).) + .middleware(modelLogging, generateTiming, toolMonitor, fullObservability) .build(); - // Instantiate middleware (templates — newInstance() is called per generate()) - GenerationMiddleware modelLogging = new ModelLoggingMiddleware(); - GenerationMiddleware generateTiming = new GenerateTimingMiddleware(); - GenerationMiddleware toolMonitor = new ToolMonitorMiddleware(); - GenerationMiddleware fullObservability = new FullObservabilityMiddleware(); - // Define a simple tool so the WrapTool hook gets exercised @SuppressWarnings("unchecked") Tool, Map> weatherTool =