NVIDIA Object-Oriented Agents (NOOA)
Skill by ara.so — AI Agent Skills collection.
NVIDIA Object-Oriented Agents (NOOA) is a model-agnostic Python framework for building reliable AI agents using standard object-oriented programming. Unlike traditional agent frameworks that separate prompts, tools, and workflows, NOOA unifies these concepts into Python classes where:
- Agents are Python objects with typed fields (state) and methods (capabilities)
- Methods with
...bodies become LLM-driven generation methods - Regular methods remain deterministic Python code
- Type annotations define contracts with automatic validation
- Docstrings serve as prompts
- The LLM acts by writing Python code in a REPL with access to
selfand imports
Installation
Core Framework
# Using uv (recommended)
uv add nooa
# Using pip
pip install nooa
Optional Packages
# CLI tools and trace viewer
uv add nooa-cli
# or as extra: uv add "nooa[cli]"
# Long-term memory subsystem
uv add nooa-memory
# or as extra: uv add "nooa[memory]"
# Benchmarking tools
uv add nooa-bench
# or as extra: uv add "nooa[bench]"
# Multiple extras at once
uv add "nooa[cli,memory,bench]"
From Source
# Latest development
uv add "nooa @ git+https://github.com/NVIDIA-NeMo/labs-OO-Agents.git@main"
# Pinned release
uv add "nooa @ git+https://github.com/NVIDIA-NeMo/labs-OO-Agents.git@v0.0.7"
Safety Note
NOOA agents can execute LLM-generated code. Always run in a sandboxed environment (container, VM, or NVIDIA OpenShell). Built-in AST validation and module deny-lists are defense-in-depth, not containment boundaries.
Quick Start
1. Configure an LLM Client
from nooa.unifiedllm.registry import get_llm_client
# Anthropic (requires ANTHROPIC_API_KEY env var)
llm = get_llm_client("claude-haiku-4-5")
# OpenAI (requires OPENAI_API_KEY env var)
llm = get_llm_client("gpt-5-mini")
# Local Ollama (no key required)
llm = get_llm_client(
"ollama_chat/qwen3:1.7b",
api_base="http://localhost:11434"
)
# Local vLLM (no key required)
llm = get_llm_client(
"hosted_vllm/Qwen/Qwen3-1.7B",
api_base="http://localhost:8000/v1"
)
2. Create Your First Agent
import asyncio
from nooa import Agent
class FeedbackAgent(Agent, llm=llm):
"""You are an agent specializing in analyzing customer feedback."""
async def analyze_feedback(self, text: str) -> str:
"""Analyze customer feedback for sentiment and key topics in one sentence."""
...
async def main():
agent = FeedbackAgent()
result = await agent.analyze_feedback("Great product, but shipping was slow")
print(result)
asyncio.run(main())
Core Concepts
Generation Methods (LLM-Driven)
Methods with ... bodies are implemented by the LLM at runtime:
class SupportAgent(Agent, llm=llm):
"""You are a customer support agent."""
async def triage(self, message: str) -> str:
"""Classify this support message and suggest next steps."""
...
The method name, parameters, return type, and docstring all contribute to the prompt.
Deterministic Methods (Regular Python)
Regular Python methods provide tools and logic:
from datetime import datetime, timedelta
class OrderAgent(Agent, llm=llm):
"""You help manage customer orders."""
def is_refund_eligible(self, order_date: datetime) -> bool:
"""Check if order is within 30-day refund window."""
days_since = (datetime.now() - order_date).days
return days_since <= 30
async def process_refund_request(self, order_id: str, order_date: datetime) -> str:
"""Decide whether to approve refund and explain why."""
...
The LLM can call self.is_refund_eligible() when implementing process_refund_request().
Typed State
Fields on the agent class hold state:
from dataclasses import dataclass
from typing import List
@dataclass
class Order:
id: str
total: float
delivered: bool
days_since_delivery: int
class SupportAgent(Agent, llm=llm):
"""You are a support agent with access to order database."""
order_db: dict[str, Order] # Typed state
def get_order(self, order_id: str) -> Order | None:
return self.order_db.get(order_id)
async def handle_inquiry(self, order_id: str, question: str) -> str:
"""Answer customer question about their order."""
...
Structured Output with Pydantic
Use Pydantic models for typed, validated returns:
from pydantic import BaseModel, Field
class Ticket(BaseModel):
category: str = Field(description="Support category: refund, shipping, technical")
priority: int = Field(ge=1, le=5, description="Priority from 1 (low) to 5 (critical)")
summary: str = Field(description="One-sentence summary")
class SupportAgent(Agent, llm=llm):
"""You create support tickets from customer messages."""
async def create_ticket(self, message: str) -> Ticket:
"""Create a structured support ticket."""
...
# Usage
async def main():
agent = SupportAgent()
ticket = await agent.create_ticket("My order never arrived and I need it urgently!")
print(f"Category: {ticket.category}, Priority: {ticket.priority}")
print(f"Summary: {ticket.summary}")
Advanced Patterns
Context Blocks for Runtime Information
Use Context to provide runtime information without cluttering the agent class:
from nooa import Agent, Context
class ResearchAgent(Agent, llm=llm):
"""You are a research assistant."""
async def answer_question(self, question: str) -> str:
"""Answer the question using available context."""
...
async def main():
agent = ResearchAgent()
with Context.user_message("The meeting is scheduled for 3 PM today."):
result = await agent.answer_question("When is the meeting?")
print(result) # Will use the context provided
Multiple Generation Strategies
Agents can use different orchestration strategies:
from nooa import Agent
# ReAct strategy (default): iterative think-act loops
class ReactAgent(Agent, llm=llm, strategy="react"):
"""You solve problems step by step."""
async def solve(self, problem: str) -> str:
"""Solve this problem."""
...
# Code-first strategy: generates Python code to execute
class CodeAgent(Agent, llm=llm, strategy="code"):
"""You solve problems by writing Python code."""
async def calculate(self, expression: str) -> float:
"""Calculate the result."""
...
Progressive Disclosure with doc()
Use doc() to provide detailed information only when the LLM requests it:
from nooa import Agent, doc
class AnalyticsAgent(Agent, llm=llm):
"""You analyze sales data."""
def get_sales_schema(self) -> str:
return doc("""
Sales database schema:
- orders table: id, customer_id, total, date
- customers table: id, name, email, segment
- products table: id, name, category, price
""")
async def query_sales(self, question: str) -> str:
"""Answer questions about sales data. Use get_sales_schema() if you need schema details."""
...
The documentation in doc() is only shown to the LLM when it calls get_sales_schema().
Agent Composition
Agents can delegate to other agents:
class EmailAgent(Agent, llm=llm):
"""You write professional emails."""
async def compose(self, topic: str, recipient: str) -> str:
"""Write an email."""
...
class CommunicationAgent(Agent, llm=llm):
"""You manage customer communications."""
email_agent: EmailAgent
async def send_update(self, customer_name: str, order_status: str) -> str:
"""Compose and send an order status update."""
email = await self.email_agent.compose(
topic=f"Order Status Update: {order_status}",
recipient=customer_name
)
# Send email logic here
return email
# Usage
async def main():
agent = CommunicationAgent(email_agent=EmailAgent())
result = await agent.send_update("John Doe", "Shipped")
Event System
Agents can emit and respond to events:
from nooa import Agent
class MonitorAgent(Agent, llm=llm):
"""You monitor system health."""
async def on_error(self, error_message: str) -> str:
"""Handle an error event."""
...
async def main():
agent = MonitorAgent()
# Emit an event
await agent.emit("error", error_message="Database connection failed")
Long-Term Memory
With nooa-memory installed:
from nooa import Agent
from nooa_memory import MemoryManager
class AssistantAgent(Agent, llm=llm):
"""You are a helpful assistant with memory."""
memory: MemoryManager
async def chat(self, message: str) -> str:
"""Chat with the user, remembering previous conversations."""
# Memory is automatically managed
...
# Usage
async def main():
memory = MemoryManager(llm=llm)
agent = AssistantAgent(memory=memory)
response1 = await agent.chat("My name is Alice")
response2 = await agent.chat("What's my name?") # Will remember "Alice"
CLI Tools
Start Trace Viewer
# Start development trace viewer (http://localhost:5001)
uv run nooa start-dev
# Or with custom port
uv run nooa start-dev --port 8080
View Traces
All agent execution is automatically traced (LLM calls, code execution, method invocations) when the viewer is running. Open http://localhost:5001 to browse traces with parent-child span relationships.
Configuration
Environment Variables
# For Anthropic models
export ANTHROPIC_API_KEY=your_key_here
# For OpenAI models
export OPENAI_API_KEY=your_key_here
# For custom API endpoints (Ollama, vLLM)
# Pass api_base directly to get_llm_client()
LLM Client Options
from nooa.unifiedllm.registry import get_llm_client
llm = get_llm_client(
"claude-haiku-4-5",
temperature=0.7, # Control randomness
max_tokens=4096, # Maximum response length
timeout=60.0, # Request timeout in seconds
)
Real-World Example: Research Assistant
import asyncio
from typing import List
from pydantic import BaseModel, Field
from nooa import Agent, Context, doc
class Source(BaseModel):
title: str
summary: str
relevance: int = Field(ge=1, le=5, description="Relevance score 1-5")
class ResearchReport(BaseModel):
topic: str
key_findings: List[str]
sources: List[Source]
conclusion: str
class ResearchAgent(Agent, llm=llm):
"""You are a research assistant who gathers and synthesizes information."""
search_history: List[str] = []
def record_search(self, query: str) -> None:
"""Record a search query in history."""
self.search_history.append(query)
print(f"Searching: {query}")
def get_research_guidelines(self) -> str:
return doc("""
Research best practices:
1. Use multiple diverse sources
2. Verify claims across sources
3. Note conflicting information
4. Prioritize recent, authoritative sources
5. Clearly distinguish facts from opinions
""")
async def research_topic(self, topic: str, depth: str = "comprehensive") -> ResearchReport:
"""
Research a topic and produce a structured report.
Args:
topic: The research topic
depth: "quick" for overview, "comprehensive" for detailed analysis
"""
...
async def main():
agent = ResearchAgent()
with Context.user_message("Focus on developments from the last 6 months."):
report = await agent.research_topic(
topic="Recent advances in multimodal AI models",
depth="comprehensive"
)
print(f"\n=== Research Report: {report.topic} ===")
print(f"\nKey Findings:")
for finding in report.key_findings:
print(f" - {finding}")
print(f"\nSources ({len(report.sources)}):")
for source in report.sources:
print(f" - {source.title} (relevance: {source.relevance}/5)")
print(f" {source.summary}")
print(f"\nConclusion:\n{report.conclusion}")
print(f"\nSearch history: {agent.search_history}")
asyncio.run(main())
Troubleshooting
Agent Not Calling Methods
Problem: Generation method completes without calling helper methods.
Solution: Make docstrings more directive:
# Less effective
async def analyze(self, text: str) -> str:
"""Analyze the text."""
...
# More effective
async def analyze(self, text: str) -> str:
"""
Analyze the text for sentiment and topics.
Use check_language() first to validate the input language.
"""
...
Type Validation Errors
Problem: Pydantic validation fails on LLM output.
Solution: Add field descriptions and constraints:
class Report(BaseModel):
# Less constrained
score: int
# More constrained
score: int = Field(ge=0, le=100, description="Confidence score from 0 to 100")
Code Execution Failures
Problem: Generated code fails or is rejected by validators.
Solution:
- Ensure required imports are available to the agent
- Check that method signatures match what the LLM expects
- Review trace viewer to see exactly what code was generated
- Use
strategy="react"if code generation is problematic
Memory Issues
Problem: Agent doesn't remember previous interactions.
Solution: Install and configure nooa-memory:
from nooa_memory import MemoryManager
agent = MyAgent(memory=MemoryManager(llm=llm))
Trace Viewer Not Working
Problem: Traces not appearing in viewer.
Solution:
- Ensure
nooa-cliis installed:uv add nooa-cli - Start viewer before running agent:
uv run nooa start-dev - Check viewer is running:
curl http://localhost:5001
Local Model Issues
Problem: Local Ollama/vLLM models not responding.
Solution:
# For Ollama, ensure it's running
ollama serve
ollama pull qwen3:1.7b
# For vLLM, check the endpoint
curl http://localhost:8000/v1/models
# Verify api_base in get_llm_client
llm = get_llm_client(
"ollama_chat/qwen3:1.7b",
api_base="http://localhost:11434" # Must match Ollama port
)
Testing Agents
import pytest
from nooa import Agent
class CalculatorAgent(Agent, llm=llm):
"""You perform calculations."""
async def add(self, a: float, b: float) -> float:
"""Add two numbers."""
...
@pytest.mark.asyncio
async def test_calculator_addition():
agent = CalculatorAgent()
result = await agent.add(2.5, 3.5)
assert abs(result - 6.0) < 0.01 # Allow for floating point precision
@pytest.mark.asyncio
async def test_calculator_with_mock_llm():
# Use a mock LLM for deterministic testing
from unittest.mock import AsyncMock
mock_llm = AsyncMock()
mock_llm.generate.return_value = "6.0"
agent = CalculatorAgent(llm=mock_llm)
result = await agent.add(2.0, 4.0)
assert result == 6.0