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,19 @@
|
||||
# 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.
|
||||
|
||||
|
||||
# This file makes the 'pre_post_processing/python' directory a Python package.
|
||||
|
||||
# You can include any package-level initialization logic here if needed.
|
||||
# For now, this file is empty.
|
||||
@@ -0,0 +1,50 @@
|
||||
---
|
||||
title: "Python: Pre & Post Processing"
|
||||
type: docs
|
||||
weight: 1
|
||||
description: >
|
||||
How to add pre- and post- processing to your Agents using Python.
|
||||
sample_filters: ["Pre & Post Processing", "Python", "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.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 `ToolboxToolset` with ADK's pre and post processing hooks to implement pre and post processing for tool calls.
|
||||
|
||||
{{< include "adk/agent.py" "python">}}
|
||||
|
||||
You can also add model-level (`before_model_callback`, `after_model_callback`) and agent-level (`before_agent_callback`, `after_agent_callback`) 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.py" "python" >}}
|
||||
|
||||
You can also add model-level (`wrap_model`) and agent-level (`before_agent`, `after_agent`) hooks to intercept messages at different stages of the execution loop. See the [LangChain Middleware documentation](https://docs.langchain.com/oss/python/langchain/middleware/custom#wrap-style-hooks) 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,137 @@
|
||||
import asyncio
|
||||
from datetime import datetime
|
||||
from typing import Any, Dict, Optional
|
||||
from copy import deepcopy
|
||||
|
||||
from google.adk import Agent
|
||||
from google.adk.apps import App
|
||||
from google.adk.runners import Runner
|
||||
from google.adk.sessions.in_memory_session_service import InMemorySessionService
|
||||
from google.adk.tools.tool_context import ToolContext
|
||||
from google.genai import types
|
||||
from toolbox_adk import CredentialStrategy, ToolboxToolset, ToolboxTool
|
||||
|
||||
SYSTEM_PROMPT = """
|
||||
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
|
||||
async def enfore_business_rules(
|
||||
tool: ToolboxTool, args: Dict[str, Any], tool_context: ToolContext
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
Callback fired before a tool is executed.
|
||||
Enforces business logic: Max stay duration is 14 days.
|
||||
"""
|
||||
tool_name = tool.name
|
||||
print(f"POLICY CHECK: Intercepting '{tool_name}'")
|
||||
|
||||
if tool_name == "update-hotel" and "checkin_date" in args and "checkout_date" in args:
|
||||
start = datetime.fromisoformat(args["checkin_date"])
|
||||
end = datetime.fromisoformat(args["checkout_date"])
|
||||
duration = (end - start).days
|
||||
|
||||
if duration > 14:
|
||||
print("BLOCKED: Stay too long")
|
||||
return {"result": "Error: Maximum stay duration is 14 days."}
|
||||
return None
|
||||
|
||||
|
||||
# Post processing
|
||||
async def enrich_response(
|
||||
tool: ToolboxTool,
|
||||
args: Dict[str, Any],
|
||||
tool_context: ToolContext,
|
||||
tool_response: Any,
|
||||
) -> Optional[Any]:
|
||||
"""
|
||||
Callback fired after a tool execution.
|
||||
Enriches response for successful bookings.
|
||||
"""
|
||||
if isinstance(tool_response, dict):
|
||||
result = tool_response.get("result", "")
|
||||
elif isinstance(tool_response, str):
|
||||
result = tool_response
|
||||
else:
|
||||
return None
|
||||
|
||||
tool_name = tool.name
|
||||
if isinstance(result, str) and "Error" not in result:
|
||||
if tool_name == "book-hotel":
|
||||
loyalty_bonus = 500
|
||||
enriched_result = f"Booking Confirmed!\n You earned {loyalty_bonus} Loyalty Points with this stay.\n\nSystem Details: {result}"
|
||||
|
||||
if isinstance(tool_response, dict):
|
||||
modified_response = deepcopy(tool_response)
|
||||
modified_response["result"] = enriched_result
|
||||
return modified_response
|
||||
else:
|
||||
return enriched_result
|
||||
return None
|
||||
|
||||
|
||||
async def run_chat_turn(
|
||||
runner: Runner, session_id: str, user_id: str, message_text: str
|
||||
):
|
||||
"""Executes a single chat turn and prints the interaction."""
|
||||
print(f"\nUSER: '{message_text}'")
|
||||
response_text = ""
|
||||
async for event in runner.run_async(
|
||||
user_id=user_id,
|
||||
session_id=session_id,
|
||||
new_message=types.Content(role="user", parts=[types.Part(text=message_text)]),
|
||||
):
|
||||
if event.content and event.content.parts:
|
||||
for part in event.content.parts:
|
||||
if part.text:
|
||||
response_text += part.text
|
||||
|
||||
print(f"AI: {response_text}")
|
||||
|
||||
|
||||
async def main():
|
||||
toolset = ToolboxToolset(
|
||||
server_url="http://127.0.0.1:5000",
|
||||
toolset_name="my-toolset",
|
||||
credentials=CredentialStrategy.toolbox_identity(),
|
||||
)
|
||||
tools = await toolset.get_tools()
|
||||
root_agent = Agent(
|
||||
name="root_agent",
|
||||
model="gemini-3-flash-preview",
|
||||
instruction=SYSTEM_PROMPT,
|
||||
tools=tools,
|
||||
# add any pre and post processing callbacks
|
||||
before_tool_callback=enfore_business_rules,
|
||||
after_tool_callback=enrich_response,
|
||||
)
|
||||
app = App(root_agent=root_agent, name="my_agent")
|
||||
runner = Runner(app=app, session_service=InMemorySessionService())
|
||||
session_id = "test-session"
|
||||
user_id = "test-user"
|
||||
await runner.session_service.create_session(
|
||||
app_name=app.name, user_id=user_id, session_id=session_id
|
||||
)
|
||||
|
||||
# First turn: Successful booking
|
||||
await run_chat_turn(runner, session_id, user_id, "Book hotel with id 3.")
|
||||
print("-" * 50)
|
||||
# Second turn: Policy violation (stay > 14 days)
|
||||
await run_chat_turn(
|
||||
runner,
|
||||
session_id,
|
||||
user_id,
|
||||
"Update hotel with id 5 with checkin date 2025-01-18 and checkout date 2025-02-10",
|
||||
)
|
||||
await toolset.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,2 @@
|
||||
google-adk[toolbox]==1.28.1
|
||||
google-genai==2.3.0
|
||||
@@ -0,0 +1,51 @@
|
||||
# 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 asyncio
|
||||
import importlib
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
ORCH_NAME = os.environ.get("ORCH_NAME")
|
||||
module_path = f"python.{ORCH_NAME}.agent"
|
||||
agent = importlib.import_module(module_path)
|
||||
|
||||
GOLDEN_KEYWORDS = [
|
||||
"AI:",
|
||||
"Loyalty Points",
|
||||
"POLICY CHECK: Intercepting 'update-hotel'",
|
||||
]
|
||||
|
||||
# --- Execution Tests ---
|
||||
class TestExecution:
|
||||
"""Test framework execution and output validation."""
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def script_output(self, capsys):
|
||||
"""Run the agent function and return its output."""
|
||||
asyncio.run(agent.main())
|
||||
return capsys.readouterr()
|
||||
|
||||
def test_script_runs_without_errors(self, script_output):
|
||||
"""Test that the script runs and produces no stderr."""
|
||||
assert script_output.err == "", f"Script produced stderr: {script_output.err}"
|
||||
|
||||
def test_keywords_in_output(self, script_output):
|
||||
"""Test that expected keywords are present in the script's output."""
|
||||
output = script_output.out
|
||||
print(f"\nAgent Output:\n{output}\n")
|
||||
missing_keywords = [kw for kw in GOLDEN_KEYWORDS if kw not in output]
|
||||
assert not missing_keywords, f"Missing keywords in output: {missing_keywords}"
|
||||
@@ -0,0 +1,118 @@
|
||||
import asyncio
|
||||
from datetime import datetime
|
||||
|
||||
from langchain.agents import create_agent
|
||||
from langchain.agents.middleware import wrap_tool_call
|
||||
from langchain_core.messages import ToolMessage
|
||||
from langchain_google_genai import ChatGoogleGenerativeAI
|
||||
from toolbox_langchain import ToolboxClient
|
||||
|
||||
system_prompt = """
|
||||
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
|
||||
@wrap_tool_call
|
||||
async def enforce_business_rules(request, handler):
|
||||
"""
|
||||
Business Logic Validation:
|
||||
Enforces max stay duration (e.g., max 14 days).
|
||||
"""
|
||||
tool_call = request.tool_call
|
||||
name = tool_call["name"]
|
||||
args = tool_call["args"]
|
||||
|
||||
print(f"POLICY CHECK: Intercepting '{name}'")
|
||||
|
||||
if name == "update-hotel":
|
||||
if "checkin_date" in args and "checkout_date" in args:
|
||||
try:
|
||||
start = datetime.fromisoformat(args["checkin_date"])
|
||||
end = datetime.fromisoformat(args["checkout_date"])
|
||||
duration = (end - start).days
|
||||
|
||||
if duration > 14:
|
||||
print("BLOCKED: Stay too long")
|
||||
return ToolMessage(
|
||||
content="Error: Maximum stay duration is 14 days.",
|
||||
tool_call_id=tool_call["id"],
|
||||
)
|
||||
except ValueError:
|
||||
pass # Ignore invalid date formats
|
||||
|
||||
# PRE: Code here runs BEFORE the tool execution
|
||||
|
||||
# EXEC: Execute the tool (or next middleware)
|
||||
result = await handler(request)
|
||||
|
||||
# POST: Code here runs AFTER the tool execution
|
||||
return result
|
||||
|
||||
|
||||
# Post processing
|
||||
@wrap_tool_call
|
||||
async def enrich_response(request, handler):
|
||||
"""
|
||||
Post-Processing & Enrichment:
|
||||
Adds loyalty points information to successful bookings.
|
||||
Standardizes output format.
|
||||
"""
|
||||
# PRE: Code here runs BEFORE the tool execution
|
||||
|
||||
# EXEC: Execute the tool (or next middleware)
|
||||
result = await handler(request)
|
||||
|
||||
# POST: Code here runs AFTER the tool execution
|
||||
if isinstance(result, ToolMessage):
|
||||
content = str(result.content)
|
||||
tool_name = request.tool_call["name"]
|
||||
|
||||
if tool_name == "book-hotel" and "Error" not in content:
|
||||
loyalty_bonus = 500
|
||||
result.content = f"Booking Confirmed!\n You earned {loyalty_bonus} Loyalty Points with this stay.\n\nSystem Details: {content}"
|
||||
|
||||
return result
|
||||
|
||||
|
||||
async def main():
|
||||
async with ToolboxClient("http://127.0.0.1:5000") as client:
|
||||
tools = await client.aload_toolset("my-toolset")
|
||||
model = ChatGoogleGenerativeAI(model="gemini-3-flash-preview")
|
||||
agent = create_agent(
|
||||
system_prompt=system_prompt,
|
||||
model=model,
|
||||
tools=tools,
|
||||
# add any pre and post processing methods
|
||||
middleware=[enforce_business_rules, enrich_response],
|
||||
)
|
||||
# Test post-processing
|
||||
user_input = "Book hotel with id 3."
|
||||
print(f"\n[INPUT] User: {user_input}")
|
||||
response = await agent.ainvoke(
|
||||
{"messages": [{"role": "user", "content": user_input}]}
|
||||
)
|
||||
|
||||
print("-" * 50)
|
||||
last_ai_msg = response["messages"][-1].content
|
||||
print(f"[OUTPUT] AI: {last_ai_msg}")
|
||||
|
||||
# Test Pre-processing
|
||||
print("-" * 50)
|
||||
user_input = "Update my hotel with id 3 with checkin date 2025-01-18 and checkout date 2025-02-20."
|
||||
print(f"\n[INPUT] User: {user_input}")
|
||||
response = await agent.ainvoke(
|
||||
{"messages": [{"role": "user", "content": user_input}]}
|
||||
)
|
||||
last_ai_msg = response["messages"][-1].content
|
||||
print(f"[OUTPUT] AI: {last_ai_msg}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
langchain==1.3.9
|
||||
langchain-google-genai==4.2.1
|
||||
toolbox-langchain==1.0.0
|
||||
Reference in New Issue
Block a user