Build AI agents using NVIDIA Object-Oriented Agents framework with Python classes, typed methods, and LLM-driven generation
triggers
["create an AI agent with NVIDIA OO Agents","build a nooa agent with typed methods","use NVIDIA Object-Oriented Agents framework","implement generation methods in nooa","setup nooa agent with tools and state","add LLM-driven methods to Python agent","trace and debug nooa agent execution","create typed agent workflows with NVIDIA OO Agents"]
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 self and 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]"
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
classFeedbackAgent(Agent, llm=llm):
"""You are an agent specializing in analyzing customer feedback."""asyncdefanalyze_feedback(self, text: str) -> str:
"""Analyze customer feedback for sentiment and key topics in one sentence."""
...
asyncdefmain():
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:
classSupportAgent(Agent, llm=llm):
"""You are a customer support agent."""asyncdeftriage(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
classOrderAgent(Agent, llm=llm):
"""You help manage customer orders."""defis_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 <= 30asyncdefprocess_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 importList@dataclassclassOrder:
id: str
total: float
delivered: bool
days_since_delivery: intclassSupportAgent(Agent, llm=llm):
"""You are a support agent with access to order database."""
order_db: dict[str, Order] # Typed statedefget_order(self, order_id: str) -> Order | None:
returnself.order_db.get(order_id)
asyncdefhandle_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
classTicket(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")
classSupportAgent(Agent, llm=llm):
"""You create support tickets from customer messages."""asyncdefcreate_ticket(self, message: str) -> Ticket:
"""Create a structured support ticket."""
...
# Usageasyncdefmain():
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
classResearchAgent(Agent, llm=llm):
"""You are a research assistant."""asyncdefanswer_question(self, question: str) -> str:
"""Answer the question using available context."""
...
asyncdefmain():
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 loopsclassReactAgent(Agent, llm=llm, strategy="react"):
"""You solve problems step by step."""asyncdefsolve(self, problem: str) -> str:
"""Solve this problem."""
...
# Code-first strategy: generates Python code to executeclassCodeAgent(Agent, llm=llm, strategy="code"):
"""You solve problems by writing Python code."""asyncdefcalculate(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
classAnalyticsAgent(Agent, llm=llm):
"""You analyze sales data."""defget_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
""")
asyncdefquery_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:
classEmailAgent(Agent, llm=llm):
"""You write professional emails."""asyncdefcompose(self, topic: str, recipient: str) -> str:
"""Write an email."""
...
classCommunicationAgent(Agent, llm=llm):
"""You manage customer communications."""
email_agent: EmailAgent
asyncdefsend_update(self, customer_name: str, order_status: str) -> str:
"""Compose and send an order status update."""
email = awaitself.email_agent.compose(
topic=f"Order Status Update: {order_status}",
recipient=customer_name
)
# Send email logic herereturn email
# Usageasyncdefmain():
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
classMonitorAgent(Agent, llm=llm):
"""You monitor system health."""asyncdefon_error(self, error_message: str) -> str:
"""Handle an error event."""
...
asyncdefmain():
agent = MonitorAgent()
# Emit an eventawait agent.emit("error", error_message="Database connection failed")
Long-Term Memory
With nooa-memory installed:
from nooa import Agent
from nooa_memory import MemoryManager
classAssistantAgent(Agent, llm=llm):
"""You are a helpful assistant with memory."""
memory: MemoryManager
asyncdefchat(self, message: str) -> str:
"""Chat with the user, remembering previous conversations."""# Memory is automatically managed
...
# Usageasyncdefmain():
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 modelsexport ANTHROPIC_API_KEY=your_key_here
# For OpenAI modelsexport 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 importListfrom pydantic import BaseModel, Field
from nooa import Agent, Context, doc
classSource(BaseModel):
title: str
summary: str
relevance: int = Field(ge=1, le=5, description="Relevance score 1-5")
classResearchReport(BaseModel):
topic: str
key_findings: List[str]
sources: List[Source]
conclusion: strclassResearchAgent(Agent, llm=llm):
"""You are a research assistant who gathers and synthesizes information."""
search_history: List[str] = []
defrecord_search(self, query: str) -> None:
"""Record a search query in history."""self.search_history.append(query)
print(f"Searching: {query}")
defget_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
""")
asyncdefresearch_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
"""
...
asyncdefmain():
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 effectiveasyncdefanalyze(self, text: str) -> str:
"""Analyze the text."""
...
# More effectiveasyncdefanalyze(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:
classReport(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
from nooa_memory import MemoryManager
agent = MyAgent(memory=MemoryManager(llm=llm))
Trace Viewer Not Working
Problem: Traces not appearing in viewer.
Solution:
Ensure nooa-cli is 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
classCalculatorAgent(Agent, llm=llm):
"""You perform calculations."""asyncdefadd(self, a: float, b: float) -> float:
"""Add two numbers."""
...
@pytest.mark.asyncioasyncdeftest_calculator_addition():
agent = CalculatorAgent()
result = await agent.add(2.5, 3.5)
assertabs(result - 6.0) < 0.01# Allow for floating point precision@pytest.mark.asyncioasyncdeftest_calculator_with_mock_llm():
# Use a mock LLM for deterministic testingfrom 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