Skip to content

Commit 721ff21

Browse files
committed
feat: more fixes + more plugins
1 parent 58d9d5b commit 721ff21

85 files changed

Lines changed: 11647 additions & 74 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@
33
*.eld
44
*.tgz
55
.DS_Store
6+
7+
# Environment files (contain secrets)
8+
.env
9+
.env.local
10+
.env.*.local
611
.angular
712
.next
813
.pnpm-store

README.md

Lines changed: 69 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ See: https://genkit.dev
66

77
> **Status**: Currently in active development (1.0.0-SNAPSHOT). Requires Java 21+.
88
>
9-
> **Note**: The Java SDK supports OpenAI, Google GenAI (Gemini), Anthropic (Claude), Ollama (local models), and Firebase (Firestore vector search, Cloud Functions, telemetry). See [Plugin Availability](#plugin-availability) for details.
9+
> **Note**: The Java SDK supports OpenAI, Google GenAI (Gemini), Anthropic (Claude), Ollama (local models), Firebase (Firestore vector search, Cloud Functions, telemetry), vector databases (Weaviate, PostgreSQL, Pinecone), MCP, and pre-built evaluators. See [Modules](#modules) for the full list.
1010
1111
## Installation
1212

@@ -82,6 +82,34 @@ Add the following dependencies to your Maven `pom.xml`:
8282
<artifactId>genkit-plugin-firebase</artifactId>
8383
<version>1.0.0-SNAPSHOT</version>
8484
</dependency>
85+
86+
<!-- Weaviate plugin (vector database) -->
87+
<dependency>
88+
<groupId>com.google.genkit</groupId>
89+
<artifactId>genkit-plugin-weaviate</artifactId>
90+
<version>1.0.0-SNAPSHOT</version>
91+
</dependency>
92+
93+
<!-- PostgreSQL plugin (pgvector) -->
94+
<dependency>
95+
<groupId>com.google.genkit</groupId>
96+
<artifactId>genkit-plugin-postgresql</artifactId>
97+
<version>1.0.0-SNAPSHOT</version>
98+
</dependency>
99+
100+
<!-- Pinecone plugin (vector database) -->
101+
<dependency>
102+
<groupId>com.google.genkit</groupId>
103+
<artifactId>genkit-plugin-pinecone</artifactId>
104+
<version>1.0.0-SNAPSHOT</version>
105+
</dependency>
106+
107+
<!-- Evaluators plugin (RAGAS-style metrics) -->
108+
<dependency>
109+
<groupId>com.google.genkit</groupId>
110+
<artifactId>genkit-plugin-evaluators</artifactId>
111+
<version>1.0.0-SNAPSHOT</version>
112+
</dependency>
85113
```
86114

87115
## Quick Start
@@ -344,6 +372,33 @@ EvalRunKey result = genkit.evaluate(RunEvaluationRequest.builder()
344372
.build());
345373
```
346374

375+
### Pre-built Evaluators Plugin
376+
377+
Use the evaluators plugin for RAGAS-style metrics without writing custom evaluators:
378+
379+
```java
380+
import com.google.genkit.plugins.evaluators.EvaluatorsPlugin;
381+
import com.google.genkit.plugins.evaluators.EvaluatorsPluginOptions;
382+
import com.google.genkit.plugins.evaluators.GenkitMetric;
383+
384+
Genkit genkit = Genkit.builder()
385+
.plugin(OpenAIPlugin.create())
386+
.plugin(EvaluatorsPlugin.create(
387+
EvaluatorsPluginOptions.builder()
388+
.judge("openai/gpt-4o-mini") // LLM for judging
389+
.metrics(List.of(
390+
GenkitMetric.FAITHFULNESS, // Factual accuracy against context
391+
GenkitMetric.ANSWER_RELEVANCY, // Answer pertains to question
392+
GenkitMetric.ANSWER_ACCURACY, // Matches reference answer
393+
GenkitMetric.MALICIOUSNESS, // Detects harmful content
394+
GenkitMetric.REGEX, // Pattern matching
395+
GenkitMetric.DEEP_EQUAL, // JSON deep equality
396+
GenkitMetric.JSONATA // JSONata expressions
397+
))
398+
.build()))
399+
.build();
400+
```
401+
347402
## Streaming
348403

349404
Generate responses with streaming for real-time output:
@@ -389,6 +444,10 @@ EmbedResponse response = genkit.embed("openai/text-embedding-3-small", documents
389444
| **plugins/localvec** | Local file-based vector store for development |
390445
| **plugins/mcp** | Model Context Protocol (MCP) client integration |
391446
| **plugins/firebase** | Firebase integration: Firestore vector search, Cloud Functions, telemetry |
447+
| **plugins/weaviate** | Weaviate vector database for RAG applications |
448+
| **plugins/postgresql** | PostgreSQL with pgvector for vector similarity search |
449+
| **plugins/pinecone** | Pinecone managed vector database for RAG applications |
450+
| **plugins/evaluators** | Pre-built RAGAS-style evaluators (faithfulness, relevancy, etc.) |
392451

393452

394453
## Observability
@@ -476,13 +535,17 @@ The following samples are available in `java/samples/`. See the [samples README]
476535
| **rag** | RAG application with local vector store |
477536
| **chat-session** | Multi-turn chat with session persistence |
478537
| **evaluations** | Custom evaluators and evaluation workflows |
538+
| **evaluators-plugin** | Pre-built RAGAS-style evaluators plugin demo |
479539
| **complex-io** | Complex nested types, arrays, maps in flow inputs/outputs |
480540
| **middleware** | Middleware patterns for logging, caching, rate limiting |
481541
| **multi-agent** | Multi-agent orchestration patterns |
482542
| **interrupts** | Flow interrupts and human-in-the-loop patterns |
483543
| **mcp** | Model Context Protocol (MCP) integration |
484544
| **firebase** | Firebase integration with Firestore RAG and Cloud Functions |
485545
| **spring** | Spring Boot HTTP server integration |
546+
| **weaviate** | Weaviate vector database RAG sample |
547+
| **postgresql** | PostgreSQL pgvector RAG sample |
548+
| **pinecone** | Pinecone vector database RAG sample |
486549

487550
### Running Samples
488551

@@ -614,7 +677,11 @@ com.google.genkit
614677
├── spring/ # Spring Boot HTTP server
615678
├── localvec/ # Local vector store
616679
├── mcp/ # Model Context Protocol client
617-
└── firebase/ # Firebase: Firestore RAG, Cloud Functions, telemetry
680+
├── firebase/ # Firebase: Firestore RAG, Cloud Functions, telemetry
681+
├── weaviate/ # Weaviate vector database
682+
├── postgresql/ # PostgreSQL with pgvector
683+
├── pinecone/ # Pinecone vector database
684+
└── evaluators/ # Pre-built RAGAS-style evaluators
618685
```
619686

620687
## License

ai/src/main/java/com/google/genkit/ai/GenerateAction.java

Lines changed: 191 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,10 @@
1818

1919
package com.google.genkit.ai;
2020

21+
import java.util.ArrayList;
2122
import java.util.HashMap;
2223
import java.util.List;
2324
import java.util.Map;
24-
import java.util.UUID;
2525
import java.util.function.Consumer;
2626

2727
import org.slf4j.Logger;
@@ -119,27 +119,178 @@ public ModelResponse run(ActionContext ctx, GenerateActionOptions options,
119119

120120
logger.debug("Generating with model: {}", modelKey);
121121

122-
// Create span metadata for the model call
123-
SpanMetadata spanMetadata = SpanMetadata.builder().name(modelName).type(ActionType.MODEL.getValue())
124-
.subtype("model").build();
122+
// Determine if we should return tool requests without executing them
123+
boolean returnToolRequests = Boolean.TRUE.equals(options.getReturnToolRequests());
124+
125+
// Get max turns for tool loop (default to 5)
126+
int maxTurns = options.getMaxTurns() != null ? options.getMaxTurns() : 5;
127+
int turn = 0;
125128

126129
String flowName = ctx.getFlowName();
127-
if (flowName != null) {
128-
spanMetadata.getAttributes().put("genkit:metadata:flow:name", flowName);
129-
}
130-
131-
// Run the model wrapped in a span
132-
return Tracer.runInNewSpan(ctx, spanMetadata, request, (spanCtx, req) -> {
133-
ActionContext newCtx = ctx.withSpanContext(spanCtx);
134-
String spanPath = "/generate/" + modelName;
135-
if (streamCallback != null && model.supportsStreaming()) {
136-
return ModelTelemetryHelper.runWithTelemetryStreaming(modelName, flowName, spanPath, req,
137-
r -> model.run(newCtx, r, streamCallback));
138-
} else {
139-
return ModelTelemetryHelper.runWithTelemetry(modelName, flowName, spanPath, req,
140-
r -> model.run(newCtx, r));
130+
131+
while (turn < maxTurns) {
132+
// Create span metadata for the model call
133+
SpanMetadata spanMetadata = SpanMetadata.builder().name(modelName).type(ActionType.MODEL.getValue())
134+
.subtype("model").build();
135+
136+
if (flowName != null) {
137+
spanMetadata.getAttributes().put("genkit:metadata:flow:name", flowName);
138+
}
139+
140+
final ModelRequest currentRequest = request;
141+
final String spanPath = "/generate/" + modelName;
142+
143+
// Run the model wrapped in a span
144+
ModelResponse response = Tracer.runInNewSpan(ctx, spanMetadata, request, (spanCtx, req) -> {
145+
ActionContext newCtx = ctx.withSpanContext(spanCtx);
146+
if (streamCallback != null && model.supportsStreaming()) {
147+
return ModelTelemetryHelper.runWithTelemetryStreaming(modelName, flowName, spanPath, currentRequest,
148+
r -> model.run(newCtx, r, streamCallback));
149+
} else {
150+
return ModelTelemetryHelper.runWithTelemetry(modelName, flowName, spanPath, currentRequest,
151+
r -> model.run(newCtx, r));
152+
}
153+
});
154+
155+
// Check if the model requested tool calls
156+
List<Part> toolRequestParts = extractToolRequestParts(response);
157+
158+
// If no tool requests or we should return them without executing, return
159+
// response
160+
if (toolRequestParts.isEmpty() || returnToolRequests) {
161+
return response;
162+
}
163+
164+
// Check if we have tools to execute
165+
if (options.getTools() == null || options.getTools().isEmpty()) {
166+
// No tools available, return response with tool requests
167+
return response;
168+
}
169+
170+
// Execute tools
171+
List<Part> toolResponseParts = executeTools(ctx, toolRequestParts, options.getTools());
172+
173+
// Add the assistant message with tool requests
174+
Message assistantMessage = response.getMessage();
175+
List<Message> updatedMessages = new ArrayList<>(request.getMessages());
176+
updatedMessages.add(assistantMessage);
177+
178+
// Add tool response message
179+
Message toolResponseMessage = new Message();
180+
toolResponseMessage.setRole(Role.TOOL);
181+
toolResponseMessage.setContent(toolResponseParts);
182+
updatedMessages.add(toolResponseMessage);
183+
184+
// Update request with new messages for next turn
185+
request = ModelRequest.builder().messages(updatedMessages).config(request.getConfig())
186+
.tools(request.getTools()).output(request.getOutput()).build();
187+
188+
turn++;
189+
}
190+
191+
throw new GenkitException("Max tool execution turns (" + maxTurns + ") exceeded");
192+
}
193+
194+
/**
195+
* Extracts tool request parts from a model response.
196+
*/
197+
private List<Part> extractToolRequestParts(ModelResponse response) {
198+
List<Part> toolRequestParts = new ArrayList<>();
199+
200+
if (response.getMessage() != null && response.getMessage().getContent() != null) {
201+
for (Part part : response.getMessage().getContent()) {
202+
if (part.getToolRequest() != null) {
203+
toolRequestParts.add(part);
204+
}
205+
}
206+
}
207+
208+
return toolRequestParts;
209+
}
210+
211+
/**
212+
* Executes tools and returns the response parts.
213+
*/
214+
private List<Part> executeTools(ActionContext ctx, List<Part> toolRequestParts, List<String> toolNames) {
215+
List<Part> responseParts = new ArrayList<>();
216+
217+
for (Part toolRequestPart : toolRequestParts) {
218+
ToolRequest toolRequest = toolRequestPart.getToolRequest();
219+
String toolName = toolRequest.getName();
220+
Object toolInput = toolRequest.getInput();
221+
222+
// Find the tool
223+
Tool<?, ?> tool = findTool(toolName, toolNames);
224+
if (tool == null) {
225+
// Tool not found, create an error response
226+
Part errorPart = new Part();
227+
ToolResponse errorResponse = new ToolResponse(toolRequest.getRef(), toolName,
228+
Map.of("error", "Tool not found: " + toolName));
229+
errorPart.setToolResponse(errorResponse);
230+
responseParts.add(errorPart);
231+
logger.warn("Tool not found: {}", toolName);
232+
continue;
233+
}
234+
235+
try {
236+
// Execute the tool
237+
@SuppressWarnings("unchecked")
238+
Tool<Object, Object> typedTool = (Tool<Object, Object>) tool;
239+
240+
// Convert input if necessary
241+
Object convertedInput = toolInput;
242+
if (toolInput instanceof Map && tool.getInputClass() != null
243+
&& !Map.class.isAssignableFrom(tool.getInputClass())) {
244+
convertedInput = objectMapper.convertValue(toolInput, tool.getInputClass());
245+
}
246+
247+
Object result = typedTool.run(ctx, convertedInput);
248+
249+
// Create tool response part
250+
Part responsePart = new Part();
251+
ToolResponse toolResponse = new ToolResponse(toolRequest.getRef(), toolName, result);
252+
responsePart.setToolResponse(toolResponse);
253+
responseParts.add(responsePart);
254+
255+
logger.debug("Executed tool '{}' successfully", toolName);
256+
} catch (Exception e) {
257+
logger.error("Tool execution failed for '{}': {}", toolName, e.getMessage());
258+
Part errorPart = new Part();
259+
ToolResponse errorResponse = new ToolResponse(toolRequest.getRef(), toolName,
260+
Map.of("error", "Tool execution failed: " + e.getMessage()));
261+
errorPart.setToolResponse(errorResponse);
262+
responseParts.add(errorPart);
263+
}
264+
}
265+
266+
return responseParts;
267+
}
268+
269+
/**
270+
* Finds a tool by name from the list of tool names or registry.
271+
*/
272+
private Tool<?, ?> findTool(String toolName, List<String> toolNames) {
273+
// First try to find in registry by name
274+
String toolKey = toolName.startsWith("/tool/") ? toolName : "/tool/" + toolName;
275+
Action<?, ?, ?> action = registry.lookupAction(toolKey);
276+
if (action instanceof Tool) {
277+
return (Tool<?, ?>) action;
278+
}
279+
280+
// Also try without prefix if the toolNames list includes it
281+
if (toolNames != null) {
282+
for (String name : toolNames) {
283+
String key = name.startsWith("/tool/") ? name : "/tool/" + name;
284+
if (key.equals(toolKey) || name.equals(toolName)) {
285+
action = registry.lookupAction(key);
286+
if (action instanceof Tool) {
287+
return (Tool<?, ?>) action;
288+
}
289+
}
141290
}
142-
});
291+
}
292+
293+
return null;
143294
}
144295

145296
@Override
@@ -169,9 +320,27 @@ public JsonNode runJson(ActionContext ctx, JsonNode input, Consumer<JsonNode> st
169320
@Override
170321
public ActionRunResult<JsonNode> runJsonWithTelemetry(ActionContext ctx, JsonNode input,
171322
Consumer<JsonNode> streamCallback) throws GenkitException {
172-
String traceId = UUID.randomUUID().toString();
173-
JsonNode result = runJson(ctx, input, streamCallback);
174-
return new ActionRunResult<>(result, traceId, null);
323+
// Capture trace info from within the span
324+
final String[] capturedTraceInfo = new String[2]; // [traceId, spanId]
325+
326+
SpanMetadata spanMetadata = SpanMetadata.builder().name("generate").type("util").build();
327+
328+
try {
329+
JsonNode result = Tracer.runInNewSpan(ctx, spanMetadata, input, (spanCtx, in) -> {
330+
// Capture the span context
331+
capturedTraceInfo[0] = spanCtx.getTraceId();
332+
capturedTraceInfo[1] = spanCtx.getSpanId();
333+
334+
return runJson(ctx.withSpanContext(spanCtx), in, streamCallback);
335+
});
336+
337+
return new ActionRunResult<>(result, capturedTraceInfo[0], capturedTraceInfo[1]);
338+
} catch (Exception e) {
339+
if (e instanceof GenkitException) {
340+
throw (GenkitException) e;
341+
}
342+
throw new GenkitException("Generate action failed: " + e.getMessage(), e);
343+
}
175344
}
176345

177346
@Override

0 commit comments

Comments
 (0)