Integrations › AutoGen
autogen v0.4+

Keep real keys private with AutoGen.

Keep service keys out of your agent’s conversation. Your software uses the key and returns only the fields the agent needs. Microsoft AutoGen v0.4 (autogen-agentchat) defines tools as plain async Python functions passed directly to AssistantAgent. This integration pattern resolves the credential inside the tool boundary, makes the privileged HTTPS call, and returns only a response containing only approved fields (a data transfer object, or DTO) to the agent. The key never enters the message history the model sees.

Jump to code ↓

Before you start

Python: AssistantAgent + async tool function

Show code : ~32 lines, python
# pip install autogen-agentchat autogen-ext[openai] deny-sh requests
import asyncio, os, requests, json
from autogen_agentchat.agents import AssistantAgent
from autogen_ext.models.openai import OpenAIChatCompletionClient
from deny_sh import vault_get

async def get_invoice(id: str) -> str:
    """Look up a Stripe invoice by id and return status and amount due."""
    # resolve the credential INSIDE the tool boundary
    stripe_key = vault_get("stripe-prod", os.environ["VAULT_PW"])
    r = requests.get(
        f"https://api.stripe.com/v1/invoices/{id}",
        headers={"Authorization": f"Bearer {stripe_key}"},
    )
    body = r.json()
    # narrowed DTO: never return raw upstream response bodies
    return json.dumps({
        "id": body.get("id"),
        "amount_due": body.get("amount_due"),
        "status": body.get("status"),
    })

model_client = OpenAIChatCompletionClient(model="gpt-4o")

agent = AssistantAgent(
    name="billing_agent",
    model_client=model_client,
    tools=[get_invoice],
    system_message="You are a billing assistant. Use tools to look up invoices.",
)

async def main():
    result = await agent.run(task="Look up invoice in_1Abc123XYZ")
    print(result.messages[-1].content)

asyncio.run(main())

Why the key stays private

The Stripe key is resolved and consumed entirely inside get_invoice's scope. AutoGen's AssistantAgent infers the tool schema from the function signature and docstring : it sees the input args (id: str) and the narrowed return string. The OpenAIChatCompletionClient sees the same. The vault sends encrypted data to your server. Your server decrypts it and sends the key to Stripe, not to the model. The narrowed DTO prevents leaking raw upstream response headers or error stacks into the model's message history.

Streaming variant with run_stream

For long-running agentic tasks, swap agent.run() for agent.run_stream() and consume the async iterator. The deniable shape is identical : credential resolution stays inside the tool boundary regardless of streaming mode.

Show code : ~16 lines, python
from autogen_agentchat.base import TaskResult
from autogen_agentchat.messages import AgentEvent, ChatMessage

async def main_stream():
    async for event in agent.run_stream(task="Look up invoice in_1Abc123XYZ"):
        if isinstance(event, (AgentEvent, ChatMessage)):
            print(f"[{event.source}] {event.content}", flush=True)
        elif isinstance(event, TaskResult):
            print(f"\nFinal: {event.messages[-1].content}")
            break

asyncio.run(main_stream())

Multiple customers: factory pattern

For per-customer agent platforms, build a factory that captures each tenant's vault password and deny.sh API key in a closure. Construct a fresh AssistantAgent per request so tool functions are scoped to that tenant only : no shared credential fallback is possible.

Show code : ~22 lines, python
def build_agent_for_tenant(tenant_pw: str, tenant_api_key: str) -> AssistantAgent:
    async def get_invoice(id: str) -> str:
        """Look up a Stripe invoice for this tenant by id."""
        # tenant_pw and tenant_api_key captured in closure :  never shared
        stripe_key = vault_get(
            "stripe-prod",
            tenant_pw,
            api_key=tenant_api_key,
        )
        r = requests.get(
            f"https://api.stripe.com/v1/invoices/{id}",
            headers={"Authorization": f"Bearer {stripe_key}"},
        )
        body = r.json()
        return json.dumps({
            "id": body.get("id"),
            "amount_due": body.get("amount_due"),
            "status": body.get("status"),
        })

    return AssistantAgent(
        name="billing_agent",
        model_client=OpenAIChatCompletionClient(model="gpt-4o"),
        tools=[get_invoice],
        system_message="You are a billing assistant.",
    )

# per-request: resolve tenant credentials from your own store, then build
agent = build_agent_for_tenant(tenant_pw=pw, tenant_api_key=key)
result = await agent.run(task="Look up invoice in_1Abc123XYZ")

Each tenant's vault entries are cryptographically isolated. A compromise of one tenant's vault password exposes only that tenant's labels : not any other tenant's credentials or the platform operator's own keys.

Troubleshooting

Common integration problems. Expand an item for the technical fix.

ModuleNotFoundError: No module named 'autogen_agentchat'

You are importing from the old v0.2 package (import autogen / from autogen import ConversableAgent), which installs as pyautogen. AutoGen v0.4 restructured into separate packages and the top-level autogen namespace no longer ships AssistantAgent or the new agent-chat primitives.

Fix: pip install autogen-agentchat autogen-ext[openai]. Update all imports to from autogen_agentchat.agents import AssistantAgent and from autogen_ext.models.openai import OpenAIChatCompletionClient. Remove any pyautogen or old autogen pins from your requirements file to avoid conflicts.

ModuleNotFoundError: No module named 'autogen_ext'

autogen-agentchat was installed but the model-client extension package was not. OpenAIChatCompletionClient lives in autogen-ext, which is a separate install with an optional dependency group for OpenAI.

Fix: pip install "autogen-ext[openai]". The [openai] extra pulls in openai>=1.3 and the required async transport. If you need Anthropic or Azure instead, use [anthropic] or [azure] respectively.

Agent never calls the tool : loops on text replies instead

AssistantAgent infers the tool call schema from the function's type hints and docstring. If either is missing, AutoGen cannot generate a valid JSON schema for the tool and the model never receives a usable tool definition : so it answers in plain text.

Fix: ensure every tool function has (a) typed parameters (id: str), (b) a typed return annotation (-> str), and (c) a non-empty docstring. The docstring becomes the tool's description field; vague or absent descriptions cause the model to ignore the tool even when it is registered.

KeyError: 'VAULT_PW' or KeyError: 'DENY_API_KEY'

The tool function reads os.environ["VAULT_PW"] at call time. If the environment variable is not set in the process running the agent, the lookup raises KeyError the first time the model invokes the tool.

Fix: set VAULT_PW and DENY_API_KEY in your server environment (Fly secrets, Render env vars, a .env loaded with python-dotenv before agent construction). Never pass them as part of system_message or task : that puts the value inside the model’s conversation context and defeats the deniable shape.

vault_get error: label_not_found when the vault entry exists

A missing label may return vault_label_not_found. Do not assume a wrong vault password returns invalid_password. Check the label, account scope and password separately. The vault is not the core real-and-decoy scheme. If you are in a multi-tenant setup, the most common cause is passing the platform operator's vault password instead of the tenant-specific password captured in the closure.

Fix: confirm the password passed to vault_get is the same one used in vault_store for that label. If the password has been rotated, all labels stored under the old password must be re-stored under the new one : old ciphertexts will not decrypt with a new password regardless of the label name matching.

Production checklist