How to attach Python code execution to an Azure AI Foundry Agent after it generates a payload?

Summary

The user’s goal to attach Python code execution to an Azure AI Foundry Agent after it generates a payload is a standard architectural pattern. However, a critical misunderstanding exists regarding how agentic workflows handle control flow. The agent itself is an orchestrator, not an execution environment for arbitrary Python scripts. You do not “attach” Python code to the agent; you register tools that the agent decides to invoke. If the agent generates a JSON payload as its final output (a “text” message), the execution flow stops there. To continue execution with custom Python logic, the agent must be configured to recognize the JSON as a trigger to call a specific Function Tool.

Root Cause

The root cause of the confusion is a separation of concerns between the Agent (Orchestrator) and the Execution Host (Your Python Application).

  1. Output Termination: By default, when an Azure AI Agent generates a structured payload, it views this as the completion of the task. It returns the payload to the client (AIProjectClient). It does not automatically feed that payload back into a different execution context (like a Python script) unless explicitly directed by a tool definition.
  2. Tool vs. Code: The user is looking for a “hook” to run arbitrary Python code. Azure AI Foundry does not support arbitrary code execution on the server side for security reasons. The supported mechanism is Function Calling.
  3. The “Loop” Gap: The user’s flow implies: Agent Generates JSON -> Magic happens -> Python Logic runs. The Magic is missing. The correct flow is: Agent Generates JSON -> Agent decides to call execute_request tool -> Azure AI Foundry sends a request to your host application -> Your host application runs the Python code -> Your host application returns a result to the agent.

Why This Happens in Real Systems

This pattern is a classic Orchestration vs. Choreography anti-pattern or misunderstanding in distributed systems.

  • LLM Statelessness: The Azure AI Agent is stateless regarding your specific Python environment. It does not have access to your local libraries, database connections, or private APIs.
  • Security Boundaries: To maintain security, the AI Runtime operates in a sandbox. It communicates with the “outside world” (your Python code) strictly through defined protocols (HTTP requests for OpenAPI or Function Tools).
  • The “Driver” Seat: The LLM sits in the driver’s seat. It decides which tool to use. If you have a Python script ready to run, you must give the LLM a “方向盘” (steering wheel) in the form of a Tool Definition. If the LLM hallucinates or decides to answer with text instead of calling the tool, the code never runs.

Real-World Impact

If this architectural gap is not addressed:

  • Orphaned Data: The agent generates the JSON payload, but the downstream business logic (e.g., booking the flight, sending the quote) never triggers. You end up parsing text strings in your client application, leading to brittle code.
  • Loss of Agency: You lose the ability to let the agent handle retries or errors returned by your Python code. If your Python API fails, the agent cannot automatically retry unless it receives that failure signal and decides to call the tool again.
  • Complexity Explosion: Developers often try to parse the agent’s text output manually (response.text.find("JSON")...). This is unreliable. It breaks as soon as the LLM adds “Here is your result: ” to the output.

Example or Code

To achieve the user’s goal, you must implement the Azure AI Foundry Function Tool pattern. You run a local HTTP server (or deploy an Azure Function) that the Agent can call.

1. The User’s Python Logic (The “Attached” Code)
This runs on your machine/server, not inside Azure.

# This is your custom logic that you want to run
def process_quote_request(payload: dict) -> str:
    # Example: Custom validation, API call, or batching
    if "items" not in payload:
        return "Error: Missing items"

    # Simulate an API call
    total_price = sum(item.get("price", 0) for item in payload["items"])
    return f"Processed Quote: Total ${total_price}"

2. The Azure AI Agent Setup (The Host)
You must define the tool and tell the agent to use it.

from azure.ai.projects import AIProjectClient
from azure.identity import DefaultAzureCredential

# 1. Define the Function Tool definition
# This tells Azure what the function does so the LLM can understand it
function_tool = {
    "name": "process_quote",
    "description": "Processes a quote request JSON and returns a calculated total.",
    "parameters": {
        "type": "object",
        "properties": {
            "items": {
                "type": "array",
                "description": "List of items with prices",
                "items": {
                    "type": "object",
                    "properties": {
                        "name": {"type": "string"},
                        "price": {"type": "number"}
                    }
                }
            }
        },
        "required": ["items"]
    }
}

# 2. Create the agent with the tool
project_client = AIProjectClient(endpoint="...", credential=DefaultAzureCredential())
agent = project_client.agents.create_agent(
    model="gpt-4o",
    name="QuoteAgent",
    instructions="You generate a quote request JSON. If the user asks for a quote, generate the JSON and immediately call the 'process_quote' tool with that data.",
    tools=[function_tool] # <--- The critical link
)

# 3. Handle the tool execution (The "Callback")
# In a real scenario, you need a mechanism to receive the tool call request.
# Azure AI Agents SDK usually requires you to poll for `requires_action` status.
# When status is 'requires_action', you execute your python logic and submit the output back.

# Pseudo-code for the execution loop:
# while True:
#    run = project_client.agents.runs.get(thread_id, run_id)
#    if run.status == 'requires_action':
#        tool_call = run.required_action.submit_tool_outputs.tool_calls[0]
#        if tool_call.function.name == "process_quote":
#            # Deserialize arguments
#            args = json.loads(tool_call.function.arguments)
#            # RUN YOUR PYTHON CODE
#            result = process_quote_request(args)
#            # Submit result back to agent
#            project_client.agents.runs.submit_tool_outputs(
#                thread_id, run_id, tool_outputs=[{"tool_call_id": tool_call.id, "output": result}]
#            )

How Senior Engineers Fix It

Senior engineers solve this by treating the Agent as a State Machine and the Python Code as a Microservice.

  1. Implement the Function Tool: Instead of asking the agent to “generate JSON,” the senior engineer instructs the agent to “call the create_quote tool with the required data.” This forces the JSON generation to happen inside the tool call arguments.
  2. Decouple the Logic: The Python code is deployed as an Azure Function or a containerized API. This ensures the code is scalable and accessible to the agent.
  3. Result Handling: The engineer sets up a robust “Tool Output Submission” loop. If the Python code returns an error, the senior engineer ensures that error message is sent back to the LLM so it can attempt to correct the JSON and call the tool again.
  4. Instruction Engineering: The prompts are written specifically to discourage “chatty” responses. The prompt says: “You must use the tool to process the data. Do not return a text summary unless the tool fails.”

Why Juniors Miss It

  • Illusion of Continuity: Juniors often expect the Agent to behave like a continuous script (e.g., “I write Python, so the Agent runs Python”). They don’t realize the Agent is a brain that makes API calls, not a runtime environment.
  • Ignoring the “Action Required” State: They see the agent generate a JSON and stop, not realizing they need to check the run status for requires_action and programmatically trigger the local Python function.
  • Over-reliance on Text Parsing: They try to hack the system by asking the agent to output text, then copying that text into a Python variable. This introduces latency and parsing errors. They miss the structured tool calling feature provided by the SDK.