Metadata-Version: 2.4
Name: runora
Version: 0.1.0
Summary: An all-in-one Python AI framework for building agents, chatbots, RAG apps, and workflows
Project-URL: Homepage, https://github.com/runora-ai/runora
Project-URL: Repository, https://github.com/runora-ai/runora
Project-URL: Documentation, https://github.com/runora-ai/runora#readme
Project-URL: Bug Tracker, https://github.com/runora-ai/runora/issues
Project-URL: Changelog, https://github.com/runora-ai/runora/blob/main/CHANGELOG.md
Author-email: Runora Contributors <mahfoojali.dev@gmail.com>
License: MIT License
        
        Copyright (c) 2026 Runora Contributors
        
        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:
        
        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Software.
        
        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        SOFTWARE.
License-File: LICENSE
Keywords: agents,ai,chatbot,framework,llm,openai,rag,workflow
Classifier: Development Status :: 3 - Alpha
Classifier: Framework :: FastAPI
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.11
Requires-Dist: fastapi>=0.104
Requires-Dist: httpx>=0.25
Requires-Dist: pydantic>=2.0
Requires-Dist: python-dotenv>=1.0
Requires-Dist: rich>=13.0
Requires-Dist: typer[all]>=0.9
Requires-Dist: uvicorn[standard]>=0.24
Provides-Extra: dev
Requires-Dist: pytest-asyncio>=0.21; extra == 'dev'
Requires-Dist: pytest>=7.0; extra == 'dev'
Requires-Dist: ruff>=0.1; extra == 'dev'
Provides-Extra: openai
Requires-Dist: openai>=1.0; extra == 'openai'
Description-Content-Type: text/markdown

# Runora

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

```bash
pip install runora
```

```python
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?](#what-is-runora)
- [Why Runora?](#why-runora)
- [Installation](#installation)
- [Quickstart](#quickstart)
- [Basic chatbot](#basic-chatbot)
- [RAG docs bot](#rag-docs-bot)
- [Tool-calling agent](#tool-calling-agent)
- [Serving as an API](#serving-as-an-api)
- [Memory](#memory)
- [Evals](#evals)
- [Tracing](#tracing)
- [CLI commands](#cli-commands)
- [Environment variables](#environment-variables)
- [API key safety](#api-key-safety)
- [Development install](#development-install)
- [Publishing to PyPI](#publishing-to-pypi)
- [Roadmap](#roadmap)

---

## 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 development** — `model="mock:test"` works everywhere so you can build and test without an API key
- **Minimal dependencies** — Pydantic, FastAPI, Typer, Rich. That is all.

---

## Installation

```bash
# 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

```python
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:

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

---

## Basic chatbot

```python
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:

```bash
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.

```python
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.

```python
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):

```python
@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.

```python
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:

```bash
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:**

```bash
curl -X POST http://localhost:8787/chat \
     -H "Content-Type: application/json" \
     -d '{"message": "Hello"}'
```

```json
{
  "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:

```python
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.

```python
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:

```python
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`):

```json
{"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:**

```python
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:**

```bash
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.

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

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

```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:**

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

**Programmatic access:**

```python
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

```bash
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:

```env
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:

```env
OPENAI_API_KEY=ollama              # or any non-empty string
OPENAI_BASE_URL=http://localhost:11434/v1
```

```python
agent = Agent(name="Local", model="openai:llama3.2", instructions="...")
```

**Model string formats:**

```python
"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.

```bash
echo ".env" >> .gitignore
```

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

```env
OPENAI_API_KEY=
OPENAI_BASE_URL=
```

**If you accidentally commit a key:**

1. Rotate the key immediately at [platform.openai.com/api-keys](https://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:

```bash
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:**

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

**Run the linter / formatter:**

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

**Run a single example:**

```bash
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:**

```bash
# 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`:

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

Update `CHANGELOG.md` with the new release entry.

**2. Build the distribution packages:**

```bash
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:**

```bash
# 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:**

```bash
uv publish
```

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

```bash
UV_PUBLISH_TOKEN=pypi-AgAAA... uv publish
```

**Publish to TestPyPI first** (recommended for new releases):

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

Then verify the install from TestPyPI:

```bash
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.*
