Nuvu Agent
A developer-first Python framework for building agentic workflows with multi-provider LLM support. Removes boilerplate while standardizing how LLMs execute functions and connect to external data via MCP.
What It Does
Register Python functions as tools with a decorator, connect to remote MCP servers, and run an agent loop — the framework handles provider differences, tool schema conversion, and execution automatically.
Your Code → NuvuAgent → (OpenAI | Anthropic | Gemini) → Your Tools → Response
Installation
# Install from source
pip install -e ".[all,dev]"
# Core (includes OpenAI support)
pip install nuvu-agent
# With Anthropic (Claude) support
pip install nuvu-agent[anthropic]
# With Google Gemini support
pip install nuvu-agent[gemini]
# With FastAPI integration
pip install nuvu-agent[fastapi]
# Everything
pip install nuvu-agent[all]
Quick Start
from nuvu_agent import NuvuAgent, tool
from nuvu_agent.tools import NuvuMCPTool
# 1. Register a local tool with a decorator
@tool
def calculate_discount(price: float, percentage: float) -> float:
"""Calculates final price after applying a discount percentage."""
return price * (1 - percentage / 100)
# 2. Connect to a remote MCP server (optional)
nuvu_mcp = NuvuMCPTool(endpoint="https://mcp.nuvu.dev")
# 3. Create an agent — swap the model string to change provider
agent = NuvuAgent(
model="claude-3-5-sonnet-20241022", # or "gpt-4o" or "gemini-1.5-pro"
tools=[calculate_discount, nuvu_mcp],
system_prompt="You are a helpful data analyst using Nuvu tools.",
)
# 4. Run
response = agent.run("What is $150 with a 15% discount?")
print(response)
The provider is auto-detected from the model name. Set your API key via environment variable:
export ANTHROPIC_API_KEY="sk-ant-..." # for claude-* models
export OPENAI_API_KEY="sk-..." # for gpt-* / o1-* / o3-* models
export GOOGLE_API_KEY="..." # for gemini-* models
Core Concepts
The @tool Decorator
Convert any typed Python function into an agent tool. Type hints become the JSON schema automatically:
from nuvu_agent import tool
from typing import Literal, Optional
@tool
def search_orders(
customer: str,
status: Literal["pending", "shipped", "delivered"],
limit: int = 20,
) -> str:
"""Search orders by customer name and status."""
# Your implementation here
return f"Found orders for {customer} with status {status}"
@tool
async def fetch_price(symbol: str) -> float:
"""Fetch the current price for a stock symbol."""
# Async functions work too
...
Supported types: str, int, float, bool, list[X], Optional[X], Literal[...].
The decorator:
- Extracts function name → tool name
- Extracts docstring → tool description
- Maps type hints → JSON Schema parameters
- Wraps sync functions for async execution
- Validates arguments at runtime via Pydantic v2
NuvuMCPTool (Remote MCP Tools)
Connect to any MCP-compliant server to instantly expose its tools:
from nuvu_agent.tools import NuvuMCPTool
# Connects to MCP server, discovers tools via JSON-RPC handshake
mcp = NuvuMCPTool(
endpoint="https://mcp.nuvu.dev",
auth_token="optional-bearer-token",
)
# Pass to agent — all remote tools are available
agent = NuvuAgent(model="gpt-4o", tools=[mcp])
The MCP bridge:
- Performs the MCP
initialize+tools/listhandshake - Converts remote schemas to LLM-compatible function definitions
- Routes execution back to the server via
tools/call - Caches tool discovery (handshake happens once)
Multi-Provider Support
The same code works across providers — just change the model string:
# OpenAI
agent = NuvuAgent(model="gpt-4o", tools=[...])
# Anthropic (Claude)
agent = NuvuAgent(model="claude-3-5-sonnet-20241022", tools=[...])
# Google Gemini
agent = NuvuAgent(model="gemini-1.5-pro", tools=[...])
# Explicit provider (for custom endpoints or ambiguous model names)
agent = NuvuAgent(model="my-custom-model", provider="openai", tools=[...])
Provider detection prefixes:
| Prefix | Provider |
|---|---|
claude-* |
Anthropic |
gpt-*, o1-*, o3-*, o4-* |
OpenAI |
gemini-* |
Google Gemini |
Agent Loop
NuvuAgent.run() executes a standard agent loop:
User Prompt → LLM → [Tool Call → Execute → Result]* → Final Response
- Loops up to
max_iterationstimes (default: 10) - Tool errors are caught and fed back as observations (never crashes)
- Supports both sync (
run()) and async (arun()) execution
# Sync
response = agent.run("Analyze this data")
# Async
response = await agent.arun("Analyze this data")
Advanced: Server Integration (FastAPI)
For production deployments with streaming SSE, sessions, and multi-tenant auth, use the lower-level AgentOrchestrator:
from nuvu_agent import AgentConfig, ToolRegistry, APIToolExecutor
from nuvu_agent.session import InMemorySessionStore
from nuvu_agent.integrations.fastapi import create_agent_router
from fastapi import FastAPI
config = AgentConfig() # reads AGENT_* env vars
registry = ToolRegistry()
registry.register({
"type": "function",
"function": {
"name": "search_orders",
"description": "Search orders by customer name or status",
"parameters": {
"type": "object",
"properties": {
"customer": {"type": "string"},
"status": {"type": "string", "enum": ["pending", "shipped", "delivered"]},
},
"required": []
}
}
}, category="read")
executor = APIToolExecutor(api_base_url="http://localhost:8000")
@executor.handler("search_orders")
async def handle_search(args, context):
params = {k: v for k, v in args.items() if v}
return await executor.api_get("/api/orders", params=params, context=context)
class OrderPrompt:
def build(self, user_id, context=None):
return "You are an order management assistant. Use tools to find real data."
app = FastAPI()
router = create_agent_router(
config=config,
tool_registry=registry,
tool_executor=executor,
session_store=InMemorySessionStore(),
prompt_builder=OrderPrompt(),
)
app.include_router(router, prefix="/api/agent")
Skills (Progressive Disclosure)
Skills are modular instruction sets that load on-demand — the agent only pays the token cost when it activates a skill. This works identically across all providers since skills are injected into the system prompt.
| Level | When Loaded | What | Token Cost |
|---|---|---|---|
| 1. Metadata | Always (system prompt) | Name + description + trigger | ~100 tokens/skill |
| 2. Instructions | On demand (load_skill) |
Full SKILL.md body | 1-5K tokens |
| 3. Resources | On demand (read_skill_file) |
Additional files | As needed |
skills/
├── weather-analyzer/
│ ├── SKILL.md # Frontmatter + instructions
│ └── REFERENCE.md # Additional reference (Level 3)
└── data-analyzer/
└── SKILL.md
from nuvu_agent.skills import SkillRegistry
skill_registry = SkillRegistry(skills_root="./skills")
# Include skill metadata in system prompt
system_prompt = "You are a helpful assistant.\n\n" + skill_registry.system_prompt_section()
Knowledge Base
Index markdown documentation for domain Q&A — no vector database needed:
from nuvu_agent.knowledge import MarkdownKnowledgeBase
kb = MarkdownKnowledgeBase(docs_root="./docs")
results = kb.search("shipping policy", max_results=3)
Environment Variables
| Variable | Default | Description |
|---|---|---|
OPENAI_API_KEY |
— | API key for OpenAI models |
ANTHROPIC_API_KEY |
— | API key for Anthropic models |
GOOGLE_API_KEY |
— | API key for Gemini models |
AGENT_LLM_API_KEY |
— | API key for FastAPI orchestrator |
AGENT_LLM_BASE_URL |
https://api.openai.com/v1 |
LLM endpoint (orchestrator) |
AGENT_LLM_MODEL |
gpt-4o |
Model (orchestrator) |
AGENT_MAX_TOKENS |
4096 |
Max tokens per response |
AGENT_MAX_TOOL_ROUNDS |
10 |
Max tool-calling iterations |
AGENT_TEMPERATURE |
0.0 |
LLM temperature |
Architecture
┌─────────────────────────────────────────────────────────────────┐
│ Your Application │
└───────────────────────────────┬─────────────────────────────────┘
│
┌───────────────┴───────────────┐
│ nuvu-agent │
│ │
│ ┌─────────────────────────┐ │
│ │ NuvuAgent │ │
│ │ (simple top-level) │ │
│ └────────────┬────────────┘ │
│ │ │
│ ┌────────────▼────────────┐ │
│ │ Provider Adapters │ │
│ │ ┌───────┬──────┬────┐ │ │
│ │ │OpenAI │Anthr.│Gem.│ │ │
│ │ └───────┴──────┴────┘ │ │
│ └────────────┬────────────┘ │
│ │ │
│ ┌────────────▼────────────┐ │
│ │ Tool Execution │ │
│ │ ┌────────┬─────────┐ │ │
│ │ │ @tool │NuvuMCP │ │ │
│ │ │(local) │(remote) │ │ │
│ │ └────────┴─────────┘ │ │
│ └─────────────────────────┘ │
└───────────────────────────────┘
OpenAI-Compatible Providers
Any service that exposes an OpenAI-compatible API (vLLM, Ollama, LiteLLM, Together AI, Groq, Azure OpenAI, etc.) works out of the box — pass base_url and set provider="openai":
# Local Ollama
agent = NuvuAgent(
model="llama3",
provider="openai",
base_url="http://localhost:11434/v1",
)
# Together AI
agent = NuvuAgent(
model="meta-llama/Llama-3-70b-chat-hf",
provider="openai",
api_key="your-together-key",
base_url="https://api.together.xyz/v1",
)
# Groq
agent = NuvuAgent(
model="llama-3.1-70b-versatile",
provider="openai",
api_key="your-groq-key",
base_url="https://api.groq.com/openai/v1",
)
# Azure OpenAI
agent = NuvuAgent(
model="gpt-4o",
provider="openai",
api_key="your-azure-key",
base_url="https://your-resource.openai.azure.com/openai/deployments/gpt-4o/",
)
The provider="openai" override bypasses model-name auto-detection, and base_url routes requests to your endpoint.
Extending: Custom Providers
To add a new LLM provider, subclass BaseProvider and implement two methods:
from nuvu_agent.providers.base import BaseProvider
from nuvu_agent.schema import CanonicalTool, ProviderResponse, ToolCall, Usage
class MyCustomProvider(BaseProvider):
def __init__(self, model: str, api_key: str | None = None, **kwargs):
super().__init__(model, api_key, **kwargs)
# Initialize your SDK client here
def format_tools(self, tools: list[CanonicalTool]) -> list[dict]:
"""Convert canonical tools to your provider's wire format."""
return [
{
"name": t.name,
"description": t.description,
"parameters": t.parameters_json_schema(),
}
for t in tools
]
async def complete(self, messages, tools, *, temperature=0.0, max_tokens=4096) -> ProviderResponse:
"""Call your LLM and return a unified ProviderResponse."""
# 1. Format tools and messages for your API
# 2. Make the API call
# 3. Parse response into ProviderResponse
return ProviderResponse(
content="response text",
tool_calls=[], # list of ToolCall(id, name, arguments)
stop_reason="end_turn", # or "tool_use"
usage=Usage(prompt_tokens=0, completion_tokens=0),
)
Then pass the instance directly to NuvuAgent via the provider parameter:
from nuvu_agent import NuvuAgent, tool
@tool
def greet(name: str) -> str:
"""Say hello."""
return f"Hello, {name}!"
provider = MyCustomProvider(model="my-model", api_key="...")
agent = NuvuAgent(
model="my-model",
provider=provider, # pass instance directly — bypasses auto-detection
tools=[greet],
)
response = agent.run("Greet Alice")
The ProviderResponse contract is simple:
content: text output (orNoneif only tool calls)tool_calls: list ofToolCall(id=str, name=str, arguments=dict)stop_reason:"end_turn"(done) or"tool_use"(wants to call tools)usage: optional token counts
License
Apache License, Version 2.0
Metadata
Release files for nuvu-agent 1.0.2
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| nuvu_agent-1.0.2.tar.gz | 41.1 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| nuvu_agent-1.0.2-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 79.5 kB
Release files / nuvu_agent-1.0.2.tar.gz
| Download URL | nuvu_agent-1.0.2.tar.gz |
|---|---|
| Size | 41.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
d7c3bb7a4649644652dd8166b07de21ce2dd8b24a67f881304f146aa62fd09da
|
|
BLAKE2b-256 checksum How to use checksums |
1d40795e90e87af66a0c429ee2680a9fb619346b3cba8aec029e632a613caf5c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 3, 2026.
Transparency logRelease files / nuvu_agent-1.0.2-py3-none-any.whl
| Download URL | nuvu_agent-1.0.2-py3-none-any.whl |
|---|---|
| Size | 38.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
8f5c96cab1eb364615f408174b9d59a4fa57c101e3fc27ca80ffe2644c86b6d0
|
|
BLAKE2b-256 checksum How to use checksums |
387fbfad7e524952f09c3364d9ec0f723835c307c4348e1aec328abd956c7baa
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 3, 2026.
Transparency log