Skip to main content

A modern multi-agent AI testing framework

Project description

Tenro

PyPI version License

A modern, provider-agnostic simulation engine to safely test AI agents.

Verify multi-agent workflows and tool usage without burning tokens.

Simulate everything. Trust your agents.

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 — manual mocks, helper functions, boilerplate
# test_helpers.py - you write and maintain this
def mock_llm_response(content=None, tool_call=None):
    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_weather.return_value = {"temp": 72, "condition": "sunny"}
    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."),
    ]
    result = my_agent.run("Weather in Paris?")
    assert result == "It's 72°F and sunny in Paris."
    mock_weather.assert_called_once_with(city="Paris")

With Tenro:

def test_agent(construct):
    construct.simulate_tool("get_weather", result={"temp": 72, "condition": "sunny"})
    construct.simulate_llm(provider="openai", responses=[
        {"tools": [{"name": "get_weather", "arguments": {"city": "Paris"}}]},
        "It's 72°F and sunny in Paris.",
    ])

    my_agent.run("Weather in Paris?")

    construct.verify_agent("WeatherAgent", output_contains="72°F and sunny")
    construct.verify_tool("get_weather", city="Paris")

No mocks. No helpers. 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.

Trace Output

Enable trace visualization to debug agent execution:

Set TENRO_TRACE=true in your .env or run TENRO_TRACE=true pytest

🤖 SupportAgent
   ├─ → user: "My order #12345 hasn't arrived"
   │
   ├─ 🧠 claude-sonnet-4-5
   │     ├─ → prompt: "Help customer: My order #12345 hasn't arrived"
   │     └─ ← tool_call: lookup_order(order_id='12345')
   │
   ├─ 🔧 lookup_order
   │     ├─ → order_id='12345'
   │     └─ ← {'status': 'shipped', 'eta': '2025-01-02'}
   │
   ├─ 🧠 claude-sonnet-4-5
   │     ├─ → prompt: "Tool result: {'status': 'shipped', ...}"
   │     └─ ← "Your order has shipped and will arrive by Jan 2nd!"
   │
   └─ ← "Your order has shipped and will arrive by Jan 2nd!"

────────────────────────────────────────────────────────────────
Summary: 1 agent | 2 LLM calls | 1 tool call | Total: 1.24s

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.4.tar.gz (76.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.4-py3-none-any.whl (118.0 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: tenro-0.1.4.tar.gz
  • Upload date:
  • Size: 76.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.9.18 {"installer":{"name":"uv","version":"0.9.18","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for tenro-0.1.4.tar.gz
Algorithm Hash digest
SHA256 6c1284dc40aea7eb776b2867fd275bcf364f4bcc66b0803fc096a8ba7124d3a7
MD5 8ccb7927950dc29f4b7753d227d0c7df
BLAKE2b-256 1d25b93580ec00a68854f4c0573f3a65d1ad05b56c5cfffce0dd087983c4244c

See more details on using hashes here.

File details

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

File metadata

  • Download URL: tenro-0.1.4-py3-none-any.whl
  • Upload date:
  • Size: 118.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.9.18 {"installer":{"name":"uv","version":"0.9.18","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for tenro-0.1.4-py3-none-any.whl
Algorithm Hash digest
SHA256 ce2fcacd5b377e236f0be34b7c5689649190ce23ce083a840b170e80016167dc
MD5 1c7bc63b887fff3c167c167427151efd
BLAKE2b-256 e5af71057d3ec4e25fad1491fe8c588d610091ec57b1918617a9a8ad2a63b3a8

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