Microsoft Agent Framework
Test your Microsoft Agent Framework agents without API keys. MAF wraps standard provider SDKs — point them at aimock and every LLM call returns deterministic fixture responses.
Quick Start
MAF wraps standard provider SDKs. Point them at aimock and every call goes through your fixtures instead of the real API.
from agent_framework import Agent, AgentRuntime
from agent_framework_openai import OpenAIChatClient
import asyncio
# Start aimock first: npx @copilotkit/aimock --fixtures ./fixtures
client = OpenAIChatClient(
model="gpt-4o",
base_url="http://localhost:4010/v1",
api_key="test",
)
agent = Agent(
name="Researcher",
instructions="You are a helpful research assistant.",
model_client=client,
)
async def main():
runtime = AgentRuntime()
runtime.register(agent)
response = await runtime.send_message(agent, "What are the latest AI testing frameworks?")
print(response)
asyncio.run(main())
using Microsoft.Extensions.AI;
using OpenAI;
// Start aimock first: npx @copilotkit/aimock --fixtures ./fixtures
var openaiClient = new OpenAIClient(
new ApiKeyCredential("test"),
new OpenAIClientOptions
{
Endpoint = new Uri("http://localhost:4010/v1")
}
);
var agent = openaiClient
.GetChatClient("gpt-4o")
.AsIChatClient();
var response = await agent.GetResponseAsync("What are the latest AI testing frameworks?");
Console.WriteLine(response.Text);
This works with any MAF provider that wraps an OpenAI-compatible API. For Azure OpenAI, see the Azure OpenAI guide.
base_url —
OPENAI_BASE_URL works when agents use the default OpenAI provider, but
passing base_url explicitly to OpenAIChatClient is more reliable
and recommended.
Anthropic Provider
For Claude-backed agents, inject a pre-configured Anthropic client. Note that aimock
serves the Claude Messages API at the root path, not under /v1.
from anthropic import AsyncAnthropic
from agent_framework_anthropic import AnthropicClient
anthropic_client = AsyncAnthropic(
api_key="test",
base_url="http://localhost:4010",
)
client = AnthropicClient(
model="claude-sonnet-4-6",
anthropic_client=anthropic_client,
)
using Anthropic;
var client = new AnthropicClient(new APIAuthentication("test"))
{
BaseUrl = "http://localhost:4010"
};
Pass this client as the model_client parameter when creating
your agent, exactly as in the OpenAI example above. See
Claude Messages for provider-specific fixture details.
With aimock-pytest
The aimock-pytest plugin starts and stops the server automatically per test,
so you never need to manage a background process.
pip install aimock-pytest
dotnet add package Microsoft.Extensions.AI
import os, pytest
@pytest.fixture(autouse=True)
def mock_llm(aimock):
"""aimock-pytest provides the `aimock` fixture automatically.
It starts a fresh server for each test that requests it (function-scoped).
You must set OPENAI_BASE_URL yourself so MAF agents route to aimock."""
os.environ["OPENAI_BASE_URL"] = aimock.url + "/v1"
os.environ["OPENAI_API_KEY"] = "test"
aimock.load_fixtures("./fixtures/maf-agents.json")
yield aimock
from agent_framework import Agent, AgentRuntime
from agent_framework_openai import OpenAIChatClient
import pytest
@pytest.mark.asyncio
async def test_researcher_agent(aimock):
client = OpenAIChatClient(
model="gpt-4o",
base_url=aimock.url + "/v1",
api_key="test",
)
agent = Agent(
name="Researcher",
instructions="You are a helpful research assistant.",
model_client=client,
)
runtime = AgentRuntime()
runtime.register(agent)
response = await runtime.send_message(agent, "Summarize recent AI breakthroughs")
assert "AI" in str(response)
using Microsoft.Extensions.AI;
using OpenAI;
using Xunit;
public class AgentTests : IAsyncLifetime
{
private Process? _aimock;
public Task InitializeAsync()
{
_aimock = Process.Start(new ProcessStartInfo
{
FileName = "npx",
Arguments = "@copilotkit/aimock --fixtures ./fixtures",
RedirectStandardOutput = true,
});
return Task.Delay(1000); // wait for server startup
}
public Task DisposeAsync()
{
_aimock?.Kill();
return Task.CompletedTask;
}
[Fact]
public async Task ResearcherAgent_ReturnsFixtureResponse()
{
var openaiClient = new OpenAIClient(
new ApiKeyCredential("test"),
new OpenAIClientOptions
{
Endpoint = new Uri("http://localhost:4010/v1")
}
);
var agent = openaiClient
.GetChatClient("gpt-4o")
.AsIChatClient();
var response = await agent.GetResponseAsync(
"Summarize recent AI breakthroughs"
);
Assert.Contains("AI", response.Text);
}
}
Multi-Agent Workflows
MAF supports multi-agent orchestration where agents communicate with each other. Each
agent makes independent LLM calls — aimock matches each by the
userMessage field, so you can write fixtures that target each agent's prompt
pattern independently.
{
"fixtures": [
{
"match": { "userMessage": "research" },
"response": {
"content": "Based on my research, the key findings are:\n\n1. LLM testing with fixture-based mocks eliminates flaky tests caused by non-deterministic API responses.\n2. Proxy recording captures real interactions for replay in CI without API keys.\n3. Multi-agent frameworks like MAF benefit most because each agent multiplies the number of LLM calls per run."
}
},
{
"match": { "userMessage": "summarize" },
"response": {
"content": "# Summary: LLM Testing Best Practices\n\nFixture-based mocking brings determinism to AI-powered applications. By replacing real API calls with recorded responses, teams ship faster with confidence.\n\n## Key Takeaway\n\nEvery agent in a MAF runtime makes independent LLM calls. Without mocking, a multi-agent workflow means multiple sources of non-determinism per run. With aimock, every call returns the exact same response every time."
}
}
]
}
import asyncio
from agent_framework import Agent, AgentRuntime
from agent_framework_openai import OpenAIChatClient
client = OpenAIChatClient(
model="gpt-4o",
base_url="http://localhost:4010/v1",
api_key="test",
)
researcher = Agent(
name="Researcher",
instructions="You research topics thoroughly and report findings.",
model_client=client,
)
summarizer = Agent(
name="Summarizer",
instructions="You summarize research into concise reports.",
model_client=client,
)
async def main():
runtime = AgentRuntime()
runtime.register(researcher)
runtime.register(summarizer)
research = await runtime.send_message(researcher, "Research LLM testing best practices")
summary = await runtime.send_message(summarizer, f"Summarize the following findings: {research}")
print(summary)
asyncio.run(main())
using Microsoft.Extensions.AI;
using OpenAI;
var openaiClient = new OpenAIClient(
new ApiKeyCredential("test"),
new OpenAIClientOptions
{
Endpoint = new Uri("http://localhost:4010/v1")
}
);
var agent = openaiClient
.GetChatClient("gpt-4o")
.AsIChatClient();
var research = await agent.GetResponseAsync(
"Research LLM testing best practices"
);
var summary = await agent.GetResponseAsync(
$"Summarize the following findings: {research.Text}"
);
Console.WriteLine(summary.Text);
The researcher's prompt contains "research", matching the first fixture. The summarizer's prompt contains "summarize", matching the second. Each agent gets its own deterministic response. See Fixtures and Sequential Responses for the full matching reference.
Tool Calls
MAF agents can use tools via function calling. When an agent invokes a tool, the framework
sends a chat completion with tool_choice and expects a tool-call response.
aimock fixtures handle this with the toolCalls response field.
{
"fixtures": [
{
"match": { "userMessage": "search", "sequenceIndex": 0 },
"response": {
"toolCalls": [
{
"name": "web_search",
"arguments": { "query": "LLM testing frameworks 2025" }
}
]
}
},
{
"match": { "userMessage": "search", "sequenceIndex": 1 },
"response": {
"content": "Based on the search results, the top LLM testing frameworks are aimock, promptfoo, and deepeval."
}
}
]
}
import asyncio
from agent_framework import Agent, AgentRuntime, FunctionTool
from agent_framework_openai import OpenAIChatClient
def web_search(query: str) -> str:
"""Search the web for information."""
return f"Results for: {query}"
client = OpenAIChatClient(
model="gpt-4o",
base_url="http://localhost:4010/v1",
api_key="test",
)
researcher = Agent(
name="Researcher",
instructions="Search the web for information when asked.",
model_client=client,
tools=[FunctionTool(web_search)],
)
async def main():
runtime = AgentRuntime()
runtime.register(researcher)
result = await runtime.send_message(researcher, "Search for the latest LLM testing frameworks")
print(result)
asyncio.run(main())
using Microsoft.Extensions.AI;
using OpenAI;
var openaiClient = new OpenAIClient(
new ApiKeyCredential("test"),
new OpenAIClientOptions
{
Endpoint = new Uri("http://localhost:4010/v1")
}
);
var agent = openaiClient
.GetChatClient("gpt-4o")
.AsIChatClient();
// aimock returns tool call fixtures — the client processes them
// just like real OpenAI tool call responses
var response = await agent.GetResponseAsync(
"Search for the latest LLM testing frameworks"
);
Console.WriteLine(response.Text);
The first fixture triggers the tool call. After MAF processes the tool result and sends it back to the LLM, the second fixture matches the follow-up message and returns the final answer.
CI with GitHub Action
Use the CopilotKit/aimock GitHub Action to run aimock as a background service
in your CI pipeline.
name: Test MAF Agents
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.11"
- uses: CopilotKit/aimock@v1
with:
fixtures: ./fixtures
- run: pip install agent-framework agent-framework-openai pytest pytest-asyncio aimock-pytest
- run: pytest
env:
OPENAI_BASE_URL: http://127.0.0.1:4010/v1
OPENAI_API_KEY: test
name: Test MAF Agents
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
with:
dotnet-version: "8.0"
- uses: CopilotKit/aimock@v1
with:
fixtures: ./fixtures
- run: dotnet test
env:
OPENAI_BASE_URL: http://127.0.0.1:4010/v1
OPENAI_API_KEY: test
The action starts aimock on port 4010, loads your fixtures, and keeps the server running for the duration of the job. No real API keys needed.
Record & Replay
Record a full agent session against a real LLM, then replay it deterministically in tests and CI. This is especially useful for capturing complex multi-agent interactions.
# Start aimock in record mode — unmatched requests go to OpenAI
npx @copilotkit/aimock --fixtures ./fixtures \
--record \
--provider-openai https://api.openai.com
# Run your agent with the real API key (proxied through aimock)
export OPENAI_BASE_URL=http://localhost:4010/v1
export OPENAI_API_KEY=sk-your-real-key
python agent.py
# New fixtures appear in ./fixtures/recorded/
# Commit them to your repo for deterministic replay
On subsequent runs without --record, aimock replays the recorded fixtures.
Every agent in the runtime gets the exact same response it received during the original
recording, making your tests fully reproducible. See
Record & Replay for the full reference.