Skip to main content

AI agent testing framework

Project description

Tenro

PyPI version License

Simulate everything. Trust your agents.

Verify multi-agent workflows and tool usage without burning tokens. Tenro is the modern, provider-agnostic simulation engine for testing AI agents safely.

Install

pip install tenro
# or
uv add tenro

Quick Start

# myapp/agent.py
from tenro import link_tool, link_llm

@link_tool("search")
def search(query: str) -> str:
    return external_api.search(query)

@link_llm("openai")
def call_llm(prompt: str) -> str:
    return openai.chat.completions.create(...)
# tests/test_agent.py
def test_agent(construct):
    construct.simulate_tool("search", result=["Simulated Doc"])    
    construct.simulate_llm(provider="openai", response="Done")
    
    agent.run("Hello")
    
    construct.verify_tool("search", query="Secret Docs", times=1)
    construct.verify_llm(times=1)

No mocks to configure, no expensive API calls, no flaky tests.

Why Tenro?

  • Zero API calls — Tests run instantly, no rate limits or costs
  • Full control — Simulate LLM responses, test edge cases reliably
  • Ship with confidence — Verify agent behaviour, not just their final response
  • pytest-native — Drop-in fixture or standalone, works with your existing setup
  • Provider-aware — Mocks OpenAI, Anthropic, Gemini with real response shapes

Before / After

Without Tenro With Tenro
# test_helpers.py - you write and maintain this
def mock_llm_response(content: str = None, tool_call: dict = None) -> ChatCompletion:
    if tool_call:
        message = ChatCompletionMessage(
            role="assistant", content=None,
            tool_calls=[ChatCompletionMessageToolCall(
                id="call_abc", type="function",
                function=Function(name=tool_call["name"], arguments=json.dumps(tool_call["args"]))
            )]
        )
    else:
        message = ChatCompletionMessage(role="assistant", content=content, tool_calls=None)
    return ChatCompletion(
        id="chatcmpl-123", created=0, model="gpt-5", object="chat.completion",
        choices=[Choice(index=0, finish_reason="stop", message=message)]
    )

# test_agent.py
@patch("myapp.tools.get_weather")
@patch("openai.chat.completions.create")
def test_agent(mock_llm, mock_weather):
    # Mock tool response
    mock_weather.return_value = {"temp": 72, "condition": "sunny"}

    # LLM responses: tool call → final answer
    mock_llm.side_effect = [
        mock_llm_response(tool_call={"name": "get_weather", "args": {"city": "Paris"}}),
        mock_llm_response(content="It's 72°F and sunny in Paris."),
    ]

    # Run agent
    result = my_agent.run("Weather in Paris?")

    # Verify
    assert result == "It's 72°F and sunny in Paris."
    assert mock_llm.call_count == 2
    mock_weather.assert_called_once_with(city="Paris")
def test_agent(construct):
    # Simulate tool response
    construct.simulate_tool("get_weather", result={"temp": 72, "condition": "sunny"})

    # LLM responses: tool call → final answer
    construct.simulate_llm(
        provider="openai",
        responses=[
            {"tools": [{"name": "get_weather", "arguments": {"city": "Paris"}}]},
            "It's 72°F and sunny in Paris.",
        ],
    )

    # Run agent
    my_agent.run("Weather in Paris?")

    # Verify
    construct.verify_agent("WeatherAgent", output_contains="72°F and sunny")
    construct.verify_llm(times=2)
    construct.verify_tool("get_weather", city="Paris")

No patch decorators. No helper functions to maintain. No side_effect chains. Just behavior.

How It Works

Tenro's Construct is a simulation environment for your AI agents. Link your functions with decorators, then test with full control:

from tenro import link_agent, link_llm, link_tool

@link_agent("Manager")
def manager(task: str) -> str:
    docs = search(task)
    return summarize(docs)

@link_tool("search")
def search(query: str) -> list[str]:
    return external_search_api(query)

@link_llm("openai", model="gpt-5")
def summarize(docs: list[str]) -> str:
    return openai.chat.completions.create(...)

In tests, the construct fixture intercepts these calls and applies your simulations.

LLM Provider Support

Provider Status
OpenAI
Anthropic
Gemini
Others 🚧 Coming soon

Compatibility

  • Python 3.11+
  • pytest 7.0+

Contributing

Thanks for your interest in contributing!

We are currently in the early stages of development and are focused on stabilizing the core API. Because of this, we aren't accepting external Pull Requests just yet.

However, your support is incredibly valuable to us. You can help us right now by:

  • Starring the Repository ⭐️: This helps others discover the project and lets you track when we open up for code contributions.
  • Reporting Bugs: If something breaks, let us know.
  • Suggesting Features: Have an idea on how to make this better? Tell us!
  • Asking Questions: We are happy to discuss the roadmap and usage.

Please use GitHub Issues for discussions and reports.

License

Apache 2.0

Support

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

tenro-0.1.0.tar.gz (60.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

tenro-0.1.0-py3-none-any.whl (93.1 kB view details)

Uploaded Python 3

File details

Details for the file tenro-0.1.0.tar.gz.

File metadata

  • Download URL: tenro-0.1.0.tar.gz
  • Upload date:
  • Size: 60.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.9.3

File hashes

Hashes for tenro-0.1.0.tar.gz
Algorithm Hash digest
SHA256 d923e18866cf310621f69911432428bef6cc6cb517afb730e6198f2f238eb539
MD5 2448735012d738675b2bab07cdd61991
BLAKE2b-256 d47e1148a96cbfb9e46610a5062cf96e6e282290b45f46e7585d4fa049b98ae0

See more details on using hashes here.

File details

Details for the file tenro-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: tenro-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 93.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.9.3

File hashes

Hashes for tenro-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 07eb02acb73c8cedaf303b050d37331a9fb66b77b72064d30729124f01f02e1e
MD5 97f6c94062b7ac8949a99d0ccc1508f0
BLAKE2b-256 61d98aa5b6b324697232ba35646d7a172495607dd31cccdccf7f697f42e15fe6

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page