chore: import upstream snapshot with attribution
CF: Deploy Dev Docs / deploy (push) Has been cancelled
Sync Labels / build (push) Has been cancelled
tests / unit tests (macos-latest) (push) Has been cancelled
tests / unit tests (windows-latest) (push) Has been cancelled
tests / unit tests (ubuntu-latest) (push) Has been cancelled
CF: Deploy Dev Docs / deploy (push) Has been cancelled
Sync Labels / build (push) Has been cancelled
tests / unit tests (macos-latest) (push) Has been cancelled
tests / unit tests (windows-latest) (push) Has been cancelled
tests / unit tests (ubuntu-latest) (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,53 @@
|
||||
---
|
||||
title: "Javascript: Pre & Post Processing"
|
||||
type: docs
|
||||
weight: 2
|
||||
description: >
|
||||
How to add pre- and post- processing to your Agents using JS.
|
||||
sample_filters: ["Pre & Post Processing", "JavaScript", "ADK", "LangChain"]
|
||||
is_sample: true
|
||||
---
|
||||
|
||||
## Prerequisites
|
||||
|
||||
This tutorial assumes that you have set up MCP Toolbox with a basic agent as described in the [local quickstart](../../../getting-started/local_quickstart_js.md).
|
||||
|
||||
This guide demonstrates how to implement these patterns in your Toolbox applications.
|
||||
|
||||
## Implementation
|
||||
|
||||
{{< tabpane persist=header >}}
|
||||
{{% tab header="ADK" text=true %}}
|
||||
The following example demonstrates how to use the `beforeToolCallback` and `afterToolCallback` hooks in the ADK `LlmAgent` to implement pre and post processing logic.
|
||||
|
||||
{{< include "adk/agent.js" "js" >}}
|
||||
|
||||
You can also add model-level (`beforeModelCallback`, `afterModelCallback`) and agent-level (`beforeAgentCallback`, `afterAgentCallback`) hooks to intercept messages at different stages of the execution loop.
|
||||
|
||||
For more information, see the [ADK Callbacks documentation](https://google.github.io/adk-docs/callbacks/types-of-callbacks/).
|
||||
{{% /tab %}}
|
||||
{{% tab header="Langchain" text=true %}}
|
||||
The following example demonstrates how to use `ToolboxClient` with LangChain's middleware to implement pre and post processing for tool calls.
|
||||
|
||||
{{< include "langchain/agent.js" "js" >}}
|
||||
|
||||
You can also use the `wrapModelCall` hook to intercept messages before and after model calls.
|
||||
You can also use [node-style hooks](https://docs.langchain.com/oss/javascript/langchain/middleware/custom#node-style-hooks) to intercept messages at the agent and model level.
|
||||
See the [LangChain Middleware documentation](https://docs.langchain.com/oss/javascript/langchain/middleware/custom#tool-call-monitoring) for details on these additional hook types.
|
||||
|
||||
{{% /tab %}}
|
||||
{{< /tabpane >}}
|
||||
|
||||
## Results
|
||||
|
||||
The output should look similar to the following.
|
||||
|
||||
{{< notice note >}}
|
||||
The exact responses may vary due to the non-deterministic nature of LLMs and differences between orchestration frameworks.
|
||||
{{< /notice >}}
|
||||
|
||||
```
|
||||
AI: Booking Confirmed! You earned 500 Loyalty Points with this stay.
|
||||
|
||||
AI: Error: Maximum stay duration is 14 days.
|
||||
```
|
||||
@@ -0,0 +1,103 @@
|
||||
import { InMemoryRunner, LlmAgent, LogLevel } from '@google/adk';
|
||||
import { ToolboxClient } from '@toolbox-sdk/adk';
|
||||
|
||||
process.env.GOOGLE_GENAI_API_KEY = process.env.GOOGLE_API_KEY || 'your-api-key'; // Replace it with your API key
|
||||
|
||||
const systemPrompt = `
|
||||
You're a helpful hotel assistant. You handle hotel searching, booking and
|
||||
cancellations. When the user searches for a hotel, mention it's name, id,
|
||||
location and price tier. Always mention hotel ids while performing any
|
||||
searches. This is very important for any operations. For any bookings or
|
||||
cancellations, please provide the appropriate confirmation. Be sure to
|
||||
update checkin or checkout dates if mentioned by the user.
|
||||
Don't ask for confirmations from the user.
|
||||
`;
|
||||
|
||||
// Pre-Processing
|
||||
function enforeBusinessRules({tool, args}) {
|
||||
const name = tool.name;
|
||||
console.log(`POLICY CHECK: Intercepting '${name}'`);
|
||||
|
||||
if (name === "update-hotel" && args.checkin_date && args.checkout_date) {
|
||||
try {
|
||||
const start = new Date(args.checkin_date);
|
||||
const end = new Date(args.checkout_date);
|
||||
const duration = (end - start) / (1000 * 60 * 60 * 24); // days
|
||||
|
||||
if (duration > 14) {
|
||||
console.log("BLOCKED: Stay too long");
|
||||
return "Error: Maximum stay duration is 14 days.";
|
||||
}
|
||||
} catch (e) {
|
||||
// Ignore invalid dates
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Post-Processing
|
||||
function enrichResponse({tool, response}) {
|
||||
const name = tool.name;
|
||||
console.log(`ENRICHING RESPONSE: Intercepting '${name}'`);
|
||||
if (name === "book-hotel") {
|
||||
let content = response;
|
||||
if (response && typeof response === "object") {
|
||||
content = response.content;
|
||||
}
|
||||
if (typeof content === "string" && !content.includes("Error")) {
|
||||
const loyaltyBonus = 500;
|
||||
const enrichedContent = `Booking Confirmed!\n You earned ${loyaltyBonus} Loyalty Points with this stay.\n\nSystem Details: ${content}`;
|
||||
|
||||
if (response && typeof response === "object") {
|
||||
return { ...response, content: enrichedContent };
|
||||
}
|
||||
return enrichedContent;
|
||||
}
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
async function runTurn(runner, userId, sessionId, prompt) {
|
||||
console.log(`\nUSER: '${prompt}'`);
|
||||
const content = { role: 'user', parts: [{ text: prompt }] };
|
||||
const stream = runner.runAsync({ userId, sessionId, newMessage: content });
|
||||
|
||||
let fullText = "";
|
||||
for await (const chunk of stream) {
|
||||
if (chunk.content && chunk.content.parts) {
|
||||
fullText += chunk.content.parts.map(p => p.text || "").join("");
|
||||
}
|
||||
}
|
||||
|
||||
console.log("-".repeat(50));
|
||||
console.log(`AI: ${fullText}`);
|
||||
}
|
||||
|
||||
export async function main() {
|
||||
const userId = 'test_user';
|
||||
const client = new ToolboxClient('http://127.0.0.1:5000');
|
||||
const tools = await client.loadToolset("my-toolset");
|
||||
|
||||
const rootAgent = new LlmAgent({
|
||||
name: 'hotel_agent',
|
||||
model: 'gemini-3-flash-preview',
|
||||
description: 'Agent for hotel bookings and administration.',
|
||||
instruction: systemPrompt,
|
||||
tools: tools,
|
||||
// Add any pre- and post- processing callbacks
|
||||
beforeToolCallback: enforeBusinessRules,
|
||||
afterToolCallback: enrichResponse
|
||||
});
|
||||
|
||||
const appName = rootAgent.name;
|
||||
const runner = new InMemoryRunner({ agent: rootAgent, appName, logLevel: LogLevel.ERROR });
|
||||
const session = await runner.sessionService.createSession({ appName, userId });
|
||||
|
||||
// Turn 1: Booking
|
||||
await runTurn(runner, userId, session.id, "Book hotel with id 3.");
|
||||
|
||||
// Turn 2: Policy Violation
|
||||
await runTurn(runner, userId, session.id, "Update my hotel with id 3 with checkin date 2025-01-18 and checkout date 2025-02-10");
|
||||
}
|
||||
|
||||
main();
|
||||
+8085
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "adk",
|
||||
"version": "1.0.0",
|
||||
"description": "ADK.js sample for pre/post processing",
|
||||
"type": "module",
|
||||
"main": "agent.js",
|
||||
"scripts": {
|
||||
"start": "node agent.js"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@google/adk": "^1.2.0",
|
||||
"@toolbox-sdk/adk": "^0.3.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
// Copyright 2026 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { describe, test, before, after } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
|
||||
const ORCH_NAME = process.env.ORCH_NAME;
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const orchDir = path.join(__dirname, ORCH_NAME);
|
||||
const agentPath = path.join(orchDir, "agent.js");
|
||||
|
||||
const { main: runAgent } = await import(agentPath);
|
||||
|
||||
const GOLDEN_KEYWORDS = [
|
||||
"AI:",
|
||||
"Loyalty Points",
|
||||
"POLICY CHECK: Intercepting 'update-hotel'"
|
||||
];
|
||||
|
||||
describe(`${ORCH_NAME} Pre/Post Processing Agent`, () => {
|
||||
let capturedOutput = [];
|
||||
let capturedErrors = [];
|
||||
let originalLog;
|
||||
let originalError;
|
||||
|
||||
before(() => {
|
||||
originalLog = console.log;
|
||||
originalError = console.error;
|
||||
|
||||
console.log = (...args) => {
|
||||
const msg = args.map(a => (typeof a === 'object' ? JSON.stringify(a, null, 2) : String(a))).join(' ');
|
||||
capturedOutput.push(msg);
|
||||
};
|
||||
|
||||
console.error = (...args) => {
|
||||
const msg = args.map(a => (typeof a === 'object' ? JSON.stringify(a, null, 2) : String(a))).join(' ');
|
||||
capturedErrors.push(msg);
|
||||
};
|
||||
});
|
||||
|
||||
after(() => {
|
||||
console.log = originalLog;
|
||||
console.error = originalError;
|
||||
});
|
||||
|
||||
test("runs without errors and outputContainsRequiredKeywords", async () => {
|
||||
capturedOutput = [];
|
||||
capturedErrors = [];
|
||||
|
||||
await runAgent();
|
||||
|
||||
const actualErrors = capturedErrors.filter(err => !err.includes('Warning'));
|
||||
|
||||
assert.equal(
|
||||
actualErrors.length,
|
||||
0,
|
||||
`Script produced stderr: ${actualErrors.join("\n")}`
|
||||
);
|
||||
|
||||
const actualOutput = capturedOutput.join("\n");
|
||||
|
||||
assert.ok(
|
||||
actualOutput.length > 0,
|
||||
"Assertion Failed: Script ran successfully but produced no output."
|
||||
);
|
||||
|
||||
const missingKeywords = [];
|
||||
|
||||
for (const keyword of GOLDEN_KEYWORDS) {
|
||||
if (!actualOutput.includes(keyword)) {
|
||||
missingKeywords.push(keyword);
|
||||
}
|
||||
}
|
||||
|
||||
assert.ok(
|
||||
missingKeywords.length === 0,
|
||||
`Assertion Failed: The following keywords were missing from the output: [${missingKeywords.join(", ")}]`
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,109 @@
|
||||
import { ToolboxClient } from "@toolbox-sdk/core";
|
||||
import { ChatGoogleGenerativeAI } from "@langchain/google-genai";
|
||||
import { createAgent, createMiddleware, ToolMessage } from "langchain";
|
||||
import { tool } from "@langchain/core/tools";
|
||||
import { fileURLToPath } from "url";
|
||||
import process from "process";
|
||||
|
||||
const systemPrompt = `
|
||||
You're a helpful hotel assistant. You handle hotel searching, booking and
|
||||
cancellations. When the user searches for a hotel, mention it's name, id,
|
||||
location and price tier. Always mention hotel ids while performing any
|
||||
searches. This is very important for any operations. For any bookings or
|
||||
cancellations, please provide the appropriate confirmation. Be sure to
|
||||
update checkin or checkout dates if mentioned by the user.
|
||||
Don't ask for confirmations from the user.
|
||||
`;
|
||||
|
||||
const GOOGLE_API_KEY = process.env.GOOGLE_API_KEY || 'your-api-key'; // Replace it with your API key
|
||||
|
||||
const businessRulesMiddleware = createMiddleware({
|
||||
name: "BusinessRules",
|
||||
wrapToolCall: async (request, handler) => {
|
||||
const toolName = request.toolCall.name;
|
||||
const toolArgs = request.toolCall.args;
|
||||
console.log(`POLICY CHECK: Intercepting '${toolName}' running with args ${JSON.stringify(toolArgs)}`);
|
||||
if (toolName === "update-hotel" && toolArgs.checkin_date && toolArgs.checkout_date) {
|
||||
try {
|
||||
const start = new Date(toolArgs.checkin_date);
|
||||
const end = new Date(toolArgs.checkout_date);
|
||||
const duration = (end - start) / (1000 * 60 * 60 * 24); // days
|
||||
|
||||
if (duration > 14) {
|
||||
console.log("BLOCKED: Stay too long");
|
||||
return ToolMessage({content:'Error: Maximum stay duration is 14 days.', status:"error"})
|
||||
}
|
||||
} catch (e) {
|
||||
// Ignore invalid dates
|
||||
}
|
||||
}
|
||||
return handler(request);
|
||||
}
|
||||
});
|
||||
|
||||
const enrichmentMiddleware = createMiddleware({
|
||||
name: "Enrichment",
|
||||
wrapToolCall: async (request, handler) => {
|
||||
const result = await handler(request);
|
||||
const toolName = request.toolCall.name;
|
||||
|
||||
let content = result;
|
||||
if (typeof result === 'object' && result !== null && result.content) {
|
||||
content = result.content;
|
||||
}
|
||||
if (toolName === "book-hotel" && typeof content === 'string' && !content.includes("Error")) {
|
||||
const loyaltyBonus = 500;
|
||||
const enrichedContent = `Booking Confirmed!\n You earned ${loyaltyBonus} Loyalty Points with this stay.\n\nSystem Details: ${content}`;
|
||||
if (typeof result === 'object' && result !== null) {
|
||||
result.content = enrichedContent;
|
||||
return result;
|
||||
}
|
||||
return enrichedContent;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
});
|
||||
|
||||
const queries = [
|
||||
"Book hotel with id 3.",
|
||||
"Update my hotel with id 3 with checkin date 2025-01-18 and checkout date 2025-02-10"
|
||||
];
|
||||
|
||||
async function main() {
|
||||
const client = new ToolboxClient("http://127.0.0.1:5000");
|
||||
const rawTools = await client.loadToolset("my-toolset");
|
||||
const tools = rawTools
|
||||
.map(t => tool(t, {
|
||||
name: t.getName(),
|
||||
description: t.getDescription(),
|
||||
schema: t.getParamSchema()
|
||||
}));
|
||||
|
||||
const model = new ChatGoogleGenerativeAI({
|
||||
model: "gemini-3-flash-preview",
|
||||
});
|
||||
|
||||
const agent = createAgent({
|
||||
model: model,
|
||||
tools: tools,
|
||||
systemPrompt: systemPrompt,
|
||||
middleware: [businessRulesMiddleware, enrichmentMiddleware]
|
||||
});
|
||||
|
||||
for (const query of queries) {
|
||||
console.log(`\nUSER: '${query}'`);
|
||||
const result = await agent.invoke({
|
||||
messages: [
|
||||
{ role: "user", content: query},
|
||||
],
|
||||
});
|
||||
console.log("-".repeat(50));
|
||||
console.log(`AI: ${result.messages[result.messages.length-1].content}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
||||
main();
|
||||
}
|
||||
|
||||
export { main };
|
||||
+1005
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "langchain",
|
||||
"version": "1.0.0",
|
||||
"description": "LangChain.js sample for pre/post processing",
|
||||
"type": "module",
|
||||
"main": "agent.js",
|
||||
"scripts": {
|
||||
"start": "node agent.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@langchain/core": "^1.1.26",
|
||||
"@langchain/google-genai": "^2.1.19",
|
||||
"@langchain/google-vertexai": "^2.1.19",
|
||||
"@toolbox-sdk/core": "^1.0.0",
|
||||
"langchain": "^1.2.25",
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC"
|
||||
}
|
||||
Reference in New Issue
Block a user