Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 13

Post-processing Tool Responses

Recommended resources

Introduction

Welcome to the next module! In our previous lessons, we mastered pre-tool hooks, learning how to validate inputs and enforce business logic before a tool executes. We saw how to apply these hooks to individual tools or globally across your entire toolset.

Now, we shift our focus to the other side of the process: what happens after a tool runs but before its output is sent back to the AI model. This is the domain of post-tool patterns. Your experience with Rasa Pro has likely shown you the value of manipulating the data returned from an action—perhaps to format it for the user, update the conversation's state, or trigger a follow-up.

In this lesson, we will implement the most direct form of a post-tool hook: transforming a tool's response data from within the endpoint itself. You will learn how to enrich the data returned by your get_order_details tool, adding new, contextual information that makes the response more useful for the language model.


1. Why Transform a Tool's Response?

When an AI model calls one of your tools, the tool's primary job is to fetch or process data. However, the raw output from your internal systems (like a database record) might not be in the ideal format for the AI.

Here’s why you might want to transform or enrich a response before returning it:

  • Adding Context: You can add a human-readable summary. Instead of just returning {"status": "shipped"}, you could add a field like {"summary": "The order was shipped successfully and is on its way."}. This gives the model clearer language to use in its reply.
  • Data Formatting: You might convert timestamps into a more friendly format or combine multiple fields into a single, coherent string.
  • Redaction: You could remove sensitive information (like internal notes or specific financial details) that the model doesn't need to see.
  • Adding Actionable Flags: You could add a flag like {"requires_follow_up": true} to guide the model's next steps.

This process is a "post-hook" because it happens after the main business logic (fetching the data) is complete. In FastAPI, the most straightforward way to implement this is directly inside the endpoint function.


2. Gaining Control Over the Response in FastAPI

By default, FastAPI performs a helpful bit of magic: you return a Pydantic model or a dictionary, and it automatically converts it into a JSON response, validating it against the response_model you defined in the decorator.

However, this automatic process can sometimes be too rigid. If you add a new key to your dictionary that isn't in the response_model, FastAPI will filter it out to ensure the response strictly adheres to the schema.

To implement our post-hook, we need to bypass this filtering. We can do this by constructing and returning a Response object ourselves. This tells FastAPI, "Don't worry about the conversion; I'll handle the final response format myself."

The documentation for FastAPI explains this concept clearly.

Return a Response Directly - FastAPI

The official FastAPI documentation, 'Return a Response Directly', explains how to take manual control over the response object. This is the key technique for our response transformation pattern.

Please read the introduction and the first two main sections: 'Return a Response' and 'Using the jsonable_encoder in a Response'. Focus on understanding why returning a Response or JSONResponse object directly bypasses FastAPI's default data conversion and validation.

The key takeaways from that reading are:

  1. You can return an instance of JSONResponse from an endpoint.
  2. When you do, FastAPI sends it directly, bypassing the response_model validation.
  3. If your data is in a Pydantic object, you should first convert it to a dictionary using jsonable_encoder.

3. Practical Exercise: Enriching the get_order_details Response

Let's apply this knowledge. We will modify the get_order_details tool. Currently, it returns data like {"order_id": "ORD12345", "status": "shipped", ...}. We'll enrich this by adding a new summary field that provides a clear, natural-language description of the order.

1. Import Necessary Components

First, at the top of your main.py file, you need to import JSONResponse. You may also need jsonable_encoder if you were working with Pydantic objects, but since our endpoint already produces a dictionary, we'll only need JSONResponse for now. Also, we will update the function signature to correctly type-hint the return value.




# main.py
import os
from typing import Optional, Dict, Any




# Add JSONResponse to your imports from fastapi
from fastapi import FastAPI, Depends, HTTPException, Request
from fastapi.responses import JSONResponse 
from pydantic import BaseModel



# ... rest of your imports

2. Modify the get_order_details Endpoint

Now, let's rewrite the get_order_details function. Instead of returning the dictionary directly, we will:

  1. Store the order data in a variable.
  2. Add our new summary key to this dictionary.
  3. Wrap the final, enriched dictionary in a JSONResponse and return that.
  4. Update the function's return type hint from -> Dict[str, Any] to -> JSONResponse.

Here is the updated code for the endpoint. Replace the existing get_order_details function in main.py with this new version.




# main.py




# ... (keep your Pydantic models and other endpoints)

@app.get(
    "/orders/{order_id}",
    operation_id="get_order_details",
    description="Fetches detailed information for a specific order by its ID.",
    tags=["orders"],
    response_model=Order,
)
async def get_order_details(order_id: str) -> JSONResponse: # <-- Note the new return type
    """
    Example endpoint to simulate fetching order data.
    In a real application, this would query a database.
    """



    # Placeholder data
    if order_id == "ORD12345":



        # 1. Get the original data
        order_data = {
            "order_id": order_id,
            "customer_id": 101,
            "status": "shipped",
            "total_amount": 199.99,
            "currency": "USD",
        }




        # 2. Enrich the data (our "post-hook" logic)
        order_data["summary"] = f"Order {order_data['order_id']} for ${order_data['total_amount']} has been shipped."
        



        # 3. Return a JSONResponse directly, bypassing response_model filtering
        return JSONResponse(content=order_data)

    raise HTTPException(status_code=404, detail="Order not found")

Notice we are keeping response_model=Order in the decorator. While the JSONResponse bypasses its filtering, it's still excellent practice to keep it for OpenAPI/MCP documentation purposes. It tells developers and the AI what the base structure of the response will look like.

3. Verify the Transformation

  1. Make sure your FastAPI server is running (uv run uvicorn main:app --reload).
  2. Open the MCP Inspector, connect to your server (http://localhost:8000/mcp), and add your Authorization header.
  3. Find the get_order_details tool and execute it with the order_id set to ORD12345.
  4. Examine the response payload. You should see the new summary field included in the JSON output, right alongside the other order details!

This is what your result should look like in the MCP Inspector. Notice the new "summary" field in the response, which was added by our post-tool logic.

You have successfully implemented a post-tool hook to enrich the data your tool returns to the AI.


Conclusion

In this lesson, you've learned a fundamental pattern for post-tool processing in FastAPI. By taking manual control of the response and returning a JSONResponse object, you can freely transform, enrich, or redact data after your main business logic has run.

Key Takeaways:

  • Post-tool hooks are executed after the primary tool logic but before the response is sent to the AI.
  • They are useful for adding context, formatting data, or redacting sensitive information.
  • In FastAPI, the simplest way to implement a post-hook for a specific tool is to add logic within the endpoint function just before the return statement.
  • Returning a JSONResponse object gives you full control over the final output and bypasses the response_model's filtering, allowing you to add new fields dynamically.

This pattern gives you precise control over the information your tools provide, enabling you to build more effective and context-aware interactions with AI models.

Next Up: We've transformed a response, but the effect is temporary, lasting only for a single tool call. What if you need to remember information between tool calls? In our next lesson, we will tackle this by designing a simple state manager, which is directly analogous to the "slot-setting" you're familiar with from conversational AI frameworks like Rasa.