# Ombra

**Minimal code, maximum automation for AI workflows.**

Write regular Python functions. Add two decorators. Get FastAPI endpoints, execution history, and time travel debugging automatically.

Ombra solves the biggest pain point in AI engineering: **debugging complex chains.** Instead of re-running expensive/slow LLM calls from scratch every time you tweak a prompt, Ombra lets you "time travel" back to any step, edit its inputs, and replay from there using cached results for everything upstream.

## Features

- ⚡ **Instant API**: Your Python functions become production-ready FastAPI endpoints.
- 🔄 **Time Travel**: Resume execution from any step. Tweaked a prompt in step 3? Replay from step 3, reusing results from steps 1 & 2.
- 📊 **Visual Dashboard**: See exactly what happened, when, and why. Inspect inputs/outputs for every step.
- 💾 **Automatic Persistence**: All execution history, inputs, and outputs are saved to SQLite.
- 🤖 **LLM Integration**: Built-in `call_llm` helper that handles multimodal inputs (like PDFs) seamlessly.

## Quick Start

### Installation

```bash
pip install ombra
# or
uv add ombra
```

Requires Python ≥3.12.

### Your First Workflow: PDF Analysis & Email Drafting

Let's build a real-world example: A workflow that takes a PDF, analyzes it using Gemini, and drafts a response email.

#### 1. Define Data Models (`models.py`)
Define structured inputs/outputs using Pydantic.

```python
from pydantic import BaseModel

class DocumentAnalysis(BaseModel):
    """Structured analysis of a document."""
    summary: str
    key_entities: list[str]
    sentiment: str
    requires_action: bool

class EmailDraft(BaseModel):
    """Proposed email draft based on analysis."""
    subject: str
    body: str
    recipient_role: str
```

#### 2. Create Steps (`steps.py`)
Write async functions decorated with `@step()`. Use `call_llm` for AI tasks.

```python
from ombra import step, File, call_llm, Model, Message, Role
from models import DocumentAnalysis, EmailDraft

@step()
async def analyze_document(file: File) -> DocumentAnalysis:
    """Sends PDF to Gemini for analysis."""
    prompt = """
    Analyze the attached PDF. Provide a summary, key entities, sentiment, 
    and if action is required.
    """

    response = await call_llm(
        model=Model.GEMINI_2_5_FLASH_LITE,
        messages=[
            Message(role=Role.USER, content=[prompt, file]) # Native file support!
        ],
        response_format=DocumentAnalysis # Structured Output!
    )
    
    # Ombra + LiteLLM guarantees the response matches your Pydantic model
    return DocumentAnalysis.model_validate_json(response.content)

@step()
async def draft_response_email(analysis: DocumentAnalysis) -> EmailDraft:
    """Drafts an email based on the analysis."""
    prompt = f"""
    Draft a response based on this analysis:
    Summary: {analysis.summary}
    Sentiment: {analysis.sentiment}
    """
    
    response = await call_llm(
        model=Model.GEMINI_2_5_FLASH_LITE,
        messages=[Message(role=Role.USER, content=prompt)],
        response_format=EmailDraft # Structured Output!
    )
    
    return EmailDraft.model_validate_json(response.content)
```

#### 3. Connect the Workflow (`workflow.py`)
Decorate your main function with `@workflow`.

```python
from ombra import workflow, File
from models import EmailDraft
from steps import analyze_document, draft_response_email

@workflow(name="process_document", description="Analyze PDF and draft email")
async def process_document_workflow(file: File) -> EmailDraft:
    # Step 1: Analyze the PDF
    analysis = await analyze_document(file)
    
    # Step 2: Draft an email based on the analysis
    email_draft = await draft_response_email(analysis)
    
    return email_draft
```

### Run It

Start the development server pointing to your code:

```bash
uv run ombra dev --workflows-dir .
```

You'll see:
```text
INFO:     🚀 Starting Ombra Dev Server at http://127.0.0.1:8000
...
INFO:     ✨ Registered workflow: process_document
INFO:     
INFO:     📖 Run Workflows:    http://localhost:8000/docs
INFO:     👀 View Executions:  http://localhost:8000/workflows
INFO:     
```

### Use It

1.  **Execute**: Go to `http://localhost:8000/docs`, find `POST /workflows/process_document/execute`, upload a PDF, and run it.
2.  **Visualize**: Go to `http://localhost:8000/workflows` to see the execution live. You'll see the `analyze_document` step completing, followed by `draft_response_email`.
3.  **Time Travel**: 
    -   Let's say you don't like the email tone in Step 2. 
    -   Instead of re-uploading the PDF and re-running the expensive Step 1, just click **"Resume"** on the `draft_response_email` step in the UI.
    -   You can even edit the inputs (e.g., change the prompt logic in your code) and replay just that step!

## Core Concepts

### `@step()`
- Wraps any `async` function.
- Automatically serializes and persists inputs/outputs to SQLite.
- Handles nesting (steps calling steps).
- Supports file handling natively.

### `@workflow()`
- Wraps the entry point function.
- Automatically generates a FastAPI route based on type hints.
- Manages the execution context.

### `call_llm`
- Built-in wrapper around standard LLM APIs.
- **Multimodal**: Pass text and `File` objects directly in the `content` list. Ombra handles the base64 conversion and formatting for you.
- **Structured Outputs**: Pass a Pydantic model to `response_format` to guarantee valid JSON outputs matching your schema.
- **Visualized**: Shows token usage and model details directly in the execution dashboard.

## CLI Commands

- `ombra dev`: Starts the dev server with hot reloading.
  - `--workflows-dir`: Specify where your workflow files are (defaults to current dir).
- `ombra serve`: Starts a production-ready server.

## License

MIT
