Skip to main content

An all-in-one Python AI framework for building agents, chatbots, RAG apps, and workflows

Project description

Runora

An all-in-one Python AI framework for building agents, chatbots, RAG apps, workflows, and APIs — without the overhead of heavy libraries.

pip install runora
from runora import Agent

agent = Agent(name="Bot", model="mock:test", instructions="You are helpful.")
print(agent.ask("Hello!").text)

No API key required to get started.


Table of contents


What is Runora?

Runora is a lightweight Python framework that gives you everything you need to build production-ready AI applications:

Component What it does
Agent Conversational AI with tools, memory, and a knowledge base
Tool Any Python function the agent can call autonomously
Knowledge Local RAG — chunk and retrieve .txt/.md files, no embeddings API
Memory Conversation buffer with a configurable message window
Workflow Directed acyclic pipeline connecting agents and functions
Serve One-line FastAPI wrapper — any agent becomes an HTTP API
CLI runora new, runora chat, runora serve, runora eval
Evals JSONL datasets, pass/fail metrics, saved reports
Tracing Every call traced to .runora/traces/ with cost estimation

Why Runora?

Runora LangChain bare OpenAI SDK
Lines of code for a basic agent ~5 ~20+ ~30+
No API key required ✅ mock provider
Built-in RAG (no embeddings API) needs setup
Built-in serving ✅ one call needs FastAPI
Built-in evals plugin
Built-in CLI
Import-time secrets ❌ never sometimes sometimes
Production-ready tracing needs setup

Design principles:

  • Zero secrets at import time — API keys are loaded only when ask() is first called
  • No magic — every component is a plain Python class you can read, subclass, or replace
  • Mock-first developmentmodel="mock:test" works everywhere so you can build and test without an API key
  • Minimal dependencies — Pydantic, FastAPI, Typer, Rich. That is all.

Installation

# Core (mock provider included)
pip install runora

# With OpenAI support
pip install "runora[openai]"

# With uv
uv add runora
uv add "runora[openai]"

Requires Python 3.11+.


Quickstart

from runora import Agent

# Works immediately — no API key, no setup
agent = Agent(
    name="Assistant",
    model="mock:test",
    instructions="You are a concise, helpful assistant.",
)

response = agent.ask("What is Runora?")
print(response.text)        # the answer
print(response.trace_id)    # UUID — every call is traced
print(response.cost_usd)    # 0.0 for mock; real $ for OpenAI

Swap to a real model by changing one line — no other code changes required:

agent = Agent(name="Assistant", model="openai:gpt-4.1-mini", instructions="...")

Basic chatbot

from runora import Agent
from runora.memory import Memory

agent = Agent(
    name="ChatBot",
    model="mock:test",                  # swap for "openai:gpt-4.1-mini"
    instructions="You are a friendly assistant.",
    memory=Memory(max_messages=20),     # remember the last 20 turns
)

while True:
    user_input = input("You: ").strip()
    if user_input.lower() in ("exit", "quit"):
        break
    response = agent.ask(user_input)
    print(f"Bot: {response.text}")

Or scaffold a project in one command:

runora new my-chatbot
cd my-chatbot
python app.py

RAG docs bot

Load local documents into a keyword-based retrieval index. No embeddings API needed.

from runora import Agent, Knowledge

# Load all .txt and .md files — chunked and indexed automatically
kb = Knowledge.from_folder("./docs", chunk_size=800, overlap=120)

agent = Agent(
    name="Docs Bot",
    model="openai:gpt-4.1-mini",
    instructions="Answer questions using only the provided documentation.",
    knowledge=kb,
)

response = agent.ask("How do I get started?")
print(response.text)
print("Sources:", response.sources)   # list of file paths that were retrieved

How it works:

  1. Knowledge.from_folder() reads every .txt and .md file recursively
  2. Files are split into overlapping chunks (configurable chunk_size and overlap)
  3. On each ask(), the most relevant chunks are retrieved via keyword scoring
  4. Retrieved chunks are injected into the system prompt as context
  5. Source file paths are returned in response.sources

Supported file types: .txt, .md


Tool-calling agent

Decorate any Python function with @tool and the agent can call it autonomously.

from typing import Annotated
from runora import Agent, tool

@tool
def get_order_status(
    order_id: Annotated[str, "The order ID, e.g. ORD-001"],
) -> str:
    """Look up the current status of an order."""
    # call your real database or API here
    return f"Order {order_id}: Shipped. Estimated delivery: tomorrow."

@tool
def send_email(to: str, subject: str, body: str) -> str:
    """Send an email to a customer."""
    # your email logic here
    return f"Email sent to {to}."

agent = Agent(
    name="Support Agent",
    model="openai:gpt-4.1-mini",
    instructions="You are a customer support agent. Use tools to look up orders and send emails.",
    tools=[get_order_status, send_email],
)

response = agent.ask("What's the status of order ORD-042? Email the customer if it's delayed.")
print(response.text)
print("Tool calls:", response.tool_calls)
# [{"name": "get_order_status", "args": {"order_id": "ORD-042"}, "result": "..."}]

How it works:

  • @tool inspects the function signature and generates a JSON schema from type annotations
  • On each turn, Runora appends available tool descriptions to the system prompt
  • The model emits <tool_call name="...">{"arg": "value"}</tool_call> when it wants to call a tool
  • Runora parses and executes the call, then feeds the result back to the model
  • The loop runs up to 3 iterations per ask() call

Dangerous tools (require user confirmation in future versions):

@tool(dangerous=True)
def delete_record(record_id: str) -> str:
    """Permanently delete a database record."""
    ...

Serving as an API

Turn any agent into a FastAPI HTTP server with one call.

from runora import Agent, serve

agent = Agent(
    name="API Bot",
    model="openai:gpt-4.1-mini",
    instructions="You are a helpful assistant.",
)

serve(agent, host="0.0.0.0", port=8787)

Or from the command line:

runora serve app.py --port 8787

Endpoints:

Method Path Description
POST /chat Send a message, get a response
GET /health Liveness probe
GET /agent Agent metadata (name, model, tools)
GET /traces/{id} Retrieve a trace by ID

Chat request / response:

curl -X POST http://localhost:8787/chat \
     -H "Content-Type: application/json" \
     -d '{"message": "Hello"}'
{
  "text": "Hello! How can I help you today?",
  "agent": "API Bot",
  "model": "openai:gpt-4.1-mini",
  "provider": "openai",
  "trace_id": "a1b2c3d4-...",
  "sources": [],
  "tool_calls": [],
  "cost_usd": 0.000042
}

Use create_app() to get the FastAPI instance for testing or integration:

from runora.serve import create_app
from fastapi.testclient import TestClient

app = create_app(agent)
client = TestClient(app)
resp = client.post("/chat", json={"message": "Hi"})

Memory

Agents can maintain conversation history across turns.

from runora import Agent
from runora.memory import Memory

agent = Agent(
    name="Bot",
    model="openai:gpt-4.1-mini",
    instructions="You are a helpful assistant.",
    memory=Memory(max_messages=40),    # rolling window of last 40 messages
)

agent.ask("My name is Alice.")
agent.ask("What languages do I use?")
response = agent.ask("What's my name?")
print(response.text)   # "Your name is Alice."

Persistent fact memory — store and recall named facts across sessions:

from runora import Agent, LocalMemory

mem = LocalMemory(path=".runora/memory.json")   # persisted to disk
mem.add("User is a senior Python developer.", tags=["background"])
mem.add("User prefers brief answers.", tags=["style"])

agent = Agent(
    name="Bot",
    model="openai:gpt-4.1-mini",
    instructions="You are a personalized assistant.",
    local_memory=mem,
)

response = agent.ask("How experienced am I?")
print(response.text)   # grounded in the stored facts
Class Use case Storage
Memory Conversation history (current session) In-process
LocalMemory Named facts (across sessions) JSON file

Evals

Test your agent against a dataset before shipping.

1. Create a JSONL dataset (tests/cases.jsonl):

{"input": "What is the refund policy?", "expected": "30 days"}
{"input": "How do I reset my password?", "expected_keywords": ["email", "reset", "link"]}
{"input": "What is 2 + 2?", "expected": "4"}

2. Run evals:

from runora import Agent
from runora.evals import run_eval, EvalReport, save_report

agent = Agent(name="SupportBot", model="openai:gpt-4.1-mini", instructions="...")

run = run_eval(agent, "tests/cases.jsonl", metric="contains_expected")
print(f"Pass rate: {run.pass_rate:.0%}")   # e.g. "Pass rate: 67%"

report = EvalReport.from_run(run)
save_report(report)   # saved to .runora/evals/eval_20260613T….json

3. Or from the CLI:

runora eval app.py --dataset tests/cases.jsonl --metric keyword_match --save

Available metrics:

Metric Passes when
contains_expected expected appears anywhere in the output
exact_match output equals expected exactly (case-insensitive)
keyword_match all expected_keywords appear in the output

JSONL fields:

Field Required for
input all metrics
expected contains_expected, exact_match
expected_keywords keyword_match

Tracing

Every agent.ask() call is automatically traced.

response = agent.ask("Tell me about Paris.")
print(response.trace_id)   # "a1b2c3d4-e5f6-..."

Traces are saved as JSON under .runora/traces/<trace_id>.json:

{
  "trace_id": "a1b2c3d4-...",
  "timestamp": "2026-06-13T10:00:00Z",
  "agent": "MyBot",
  "model": "openai:gpt-4.1-mini",
  "provider": "openai",
  "input": "Tell me about Paris.",
  "output": "Paris is the capital of France...",
  "tool_calls": [],
  "sources": [],
  "cost_usd": 0.000085
}

CLI — inspect traces:

runora trace last                # most recent trace
runora trace a1b2c3d4            # specific trace by ID

Programmatic access:

from runora.tracing.store import load_trace, load_last_trace

data = load_last_trace()
print(data["output"])
print(f"Cost: ${data['cost_usd']:.6f}")

Workflow traces are also saved with "type": "workflow" so they are easy to distinguish.


CLI commands

runora version                          # print installed version

runora new my-project                   # scaffold basic project
runora new my-rag  --template rag       # scaffold RAG project
runora new my-bot  --template tool      # scaffold tool agent project

runora chat app.py                      # interactive REPL with your agent
runora ask  app.py "What is Runora?"    # one-shot query

runora serve app.py                     # serve on http://127.0.0.1:8787
runora serve app.py --port 9000         # custom port
runora serve app.py --host 0.0.0.0     # expose to network

runora trace last                       # print the most recent trace
runora trace <id>                       # print a specific trace

runora eval app.py \
    --dataset tests/cases.jsonl \
    --metric keyword_match \
    --save                              # run evals and save report

runora doctor                           # environment health check

runora doctor checks:

  • Python version (≥ 3.11 required)
  • .env file exists
  • OPENAI_API_KEY is set (value never printed)
  • .runora/ directory is writable

Project templates (runora new):

Template Contents
basic (default) app.py, .env.example, README.md
rag above + docs/intro.md
tool app.py with a sample @tool

Environment variables

Create a .env file in your project root:

OPENAI_API_KEY=sk-...
OPENAI_BASE_URL=                  # optional: custom endpoint (Ollama, proxy, etc.)

Runora reads these automatically when ask() is first called. Never at import time.

Custom base URL — point Runora at any OpenAI-compatible API:

OPENAI_API_KEY=ollama              # or any non-empty string
OPENAI_BASE_URL=http://localhost:11434/v1
agent = Agent(name="Local", model="openai:llama3.2", instructions="...")

Model string formats:

"mock:test"                  # built-in mock, no key needed
"openai:gpt-4.1-mini"        # OpenAI GPT-4.1 Mini
"openai:gpt-4o"              # OpenAI GPT-4o
"gpt-4o-mini"                # shorthand — resolved to OpenAI automatically

API key safety

Runora is designed so that secrets never leak accidentally.

What Runora guarantees:

  • API keys are never read at import time — only when ask() is first called
  • API keys are never written to trace files, logs, or any output
  • The OPENAI_API_KEY is loaded once per process from the environment — it is never stored on the Agent object
  • runora doctor checks whether the key is set but never prints its value

What you must do:

Warning Never commit .env to GitHub. Add it to .gitignore immediately.

echo ".env" >> .gitignore

Your .env.example (safe to commit — no real values):

OPENAI_API_KEY=
OPENAI_BASE_URL=

If you accidentally commit a key:

  1. Rotate the key immediately at platform.openai.com/api-keys
  2. Remove it from git history: git filter-repo or BFG Repo Cleaner
  3. Force-push the cleaned history

Development install

Clone the repo and install in editable mode so changes to the source are reflected immediately without reinstalling:

git clone https://github.com/runora-ai/runora.git
cd runora

# With uv (recommended)
uv pip install -e ".[dev,openai]"

# With pip
pip install -e ".[dev,openai]"

The dev extra installs pytest, pytest-asyncio, and ruff.

Run the test suite:

pytest
# or with uv's managed Python
uv run pytest

Run the linter / formatter:

ruff check .          # lint
ruff format .         # format
ruff check --fix .    # auto-fix lint issues

Run a single example:

python examples/basic_chatbot/app.py
python examples/tool_agent/app.py
python examples/workflow_blog_writer/app.py

Publishing to PyPI

Note: These instructions are for maintainers cutting a release. End users install Runora with pip install runora.

Prerequisites:

# Install uv if you don't have it
curl -LsSf https://astral.sh/uv/install.sh | sh

# Create a PyPI account and generate an API token at:
# https://pypi.org/manage/account/token/

1. Bump the version in pyproject.toml:

[project]
version = "0.1.0"   # change to the new version

Update CHANGELOG.md with the new release entry.

2. Build the distribution packages:

uv build

This creates two files in dist/:

dist/
  runora-0.1.0-py3-none-any.whl   # wheel (fast install)
  runora-0.1.0.tar.gz             # source distribution

3. Verify the build locally before uploading:

# Install into a temporary venv and smoke-test
uv venv /tmp/runora-test
/tmp/runora-test/bin/pip install dist/runora-0.1.0-py3-none-any.whl
/tmp/runora-test/bin/python -c "from runora import Agent; print(Agent('Bot','mock:test','hi').ask('hi').text)"
/tmp/runora-test/bin/runora version

4. Publish to PyPI:

uv publish

uv publish reads your PyPI token from UV_PUBLISH_TOKEN env var or ~/.pypi/credentials. You can also pass it directly:

UV_PUBLISH_TOKEN=pypi-AgAAA... uv publish

Publish to TestPyPI first (recommended for new releases):

uv publish --index https://test.pypi.org/legacy/ \
           --index-url https://test.pypi.org/simple/

Then verify the install from TestPyPI:

pip install --index-url https://test.pypi.org/simple/ runora==0.1.0

Roadmap

v0.1.0 — current

  • Agent with sync/async ask() and agentic tool-calling loop
  • Mock provider (zero config, no API key)
  • OpenAI provider (gpt-4.1-mini, gpt-4o, and all gpt-* models)
  • @tool decorator with automatic JSON schema generation
  • Local RAG (Knowledge.from_folder) — keyword retrieval, no embeddings API
  • Conversation memory (Memory) and persistent fact memory (LocalMemory)
  • Workflow — directed acyclic pipeline of agents and functions with cycle detection
  • serve() — FastAPI wrapper with /chat, /health, /agent, /traces/{id}
  • CLI: new, chat, ask, serve, trace, doctor, eval
  • Evals: JSONL datasets, contains_expected/exact_match/keyword_match metrics
  • Tracing: JSON traces with cost estimation saved to .runora/traces/

v0.2.0 — planned

  • Streaming responses (agent.stream())
  • Anthropic / Claude provider
  • Gemini provider
  • Semantic / embedding-based retrieval (optional upgrade from keyword)
  • Async workflow execution with parallel branches
  • LLM-as-judge eval metric
  • Web UI for trace inspection (runora ui)

v0.3.0 — ideas

  • Agent-to-agent handoff (multi-agent routing)
  • Long-term vector memory (Chroma, Pinecone)
  • Structured output (Pydantic response models)
  • Function schemas from OpenAPI specs
  • Deployment to Fly.io, Railway, Render (one command)

License

MIT — free to use in personal and commercial projects.


Built with Python 3.11+. No LangChain. No magic.

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

runora-0.1.0.tar.gz (126.1 kB view details)

Uploaded Source

Built Distribution

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

runora-0.1.0-py3-none-any.whl (63.3 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: runora-0.1.0.tar.gz
  • Upload date:
  • Size: 126.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.16 {"installer":{"name":"uv","version":"0.11.16","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"26.04","id":"resolute","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for runora-0.1.0.tar.gz
Algorithm Hash digest
SHA256 937fcf66f9c700a07a72d8a2a95ff23345216584df33b3d982ecefea350a79fd
MD5 442a3ebf501c4f36c04ce578724a7b95
BLAKE2b-256 78f1341788862c4ab0a9e8b84105a7ec780d6a1b8931ba18d2929fb9cb71a3f0

See more details on using hashes here.

File details

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

File metadata

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

File hashes

Hashes for runora-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 aa2210ed2938b29e8f3a20cec0f6999138df7223c54a450b44f49b9363c8eab6
MD5 9b7c7ed370073de1f35943e2a32c53fd
BLAKE2b-256 c10fa1a92517afd266dc7d03fe02d16c3692251ddc5fed861cb602de10857384

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