# {{projectName}} / {{appName}}

Voltro AI backend scaffold (template: **api-ai**) — a **RAG support
agent** over a docs knowledge base. It exercises the framework's AI
surface end-to-end: auto-embedded docs, a retrieval tool, a real agent
model loop, and a structured-output action.

## Boot

```bash
pnpm install                # at the repo root
pnpm --filter @{{projectName}}/{{appName}} dev
# → http://localhost:4000
# → ws://localhost:4000/ws
```

The app **boots and discovers every primitive with zero infra and no AI
key** (`store: 'memory'`). You only need a provider key to actually RUN
the agent / the summarize action — see below.

## What it demonstrates

| File | Primitive | What it shows |
|---|---|---|
| `app.config.ts` | app + env | `store: 'memory'` zero-infra boot; AI env declared in the manifest. |
| `database/schema.ts` | schema + `vectorEmbedding()` | A `docs` table that auto-embeds `body` on every write (adds the `embedding` vector column + HNSW index). |
| `tools/searchDocs.tool.tsx` | `defineTool` | A retrieval tool the agent calls: `nearestNeighbours(query, k)` over `docs`, errors → `[]`. |
| `agents/support.agent.tsx` | `defineAgent` (descriptor) | Browser-safe wire contract (`name`, `input`). |
| `agents/support.agent.server.tsx` | `defineAgentExecutor` (executor) | System prompt + `tools: { searchDocs }` + `maxSteps`; model inherited from env. |
| `actions/summarize.action.ts` / `.server.tsx` | `defineAction` + `generateObject` | Structured output — a typed `{ title, summary, keyPoints, sentiment }` object. |
| `seeds/docs.seed.ts` | `defineSeed` | Idempotent boot seed of sample docs (auto-embedded on insert). |

The framework SYNTHESIZES two routes from the agent descriptor — you
write neither:

- **`support.send`** — an action that appends the user turn and streams
  the assistant turn (delta-persisted to `agent_messages`).
- **`support.messages`** — a reactive query over `agent_messages` that
  streams the turns (including the live streaming row) to the client.

The thread tables (`agent_threads`, `agent_messages`) are auto-provided
and auto-migrated because this app ships an `*.agent.tsx` — no schema
file needed.

## AI provider — env

`@voltro/ai` reads the provider/model/key from env. All are OPTIONAL for
booting; they're only needed to RUN the model. Put them in `.env`:

```bash
AI_PROVIDER=openai                 # e.g. 'openai' | 'anthropic'
AI_MODEL=gpt-4o-mini               # any chat model the provider exposes
AI_API_KEY=sk-...                  # provider key
# @voltro/ai also accepts the provider-standard var:
#   OPENAI_API_KEY=...   /   ANTHROPIC_API_KEY=...
```

Editing `.env` hard-restarts `voltro dev` automatically. Run `voltro env`
to see the resolved manifest.

### Embedding model + dimensions

`vectorEmbedding({ from: 'body', model: 'text-embedding-3-small',
dimensions: 1536 })` in `database/schema.ts` embeds with OpenAI's
`text-embedding-3-small` (1536-wide). If you switch to a different
embedding model, change `dimensions` to match its output width (e.g.
768 / 3072) — the vector column size must line up or inserts/queries
won't align.

> `store: 'memory'` keeps the vector column but ANN falls back to a
> sequential scan (correct, just unindexed). Switch to `store:
> 'postgres'` for a real pgvector HNSW index.

## Backfilling embeddings

The seed runs on first boot. If it ran BEFORE you set an AI key, the doc
rows exist but their `embedding` column is empty (the auto-embed had no
provider). Set a key, then fill the gaps:

```bash
voltro embeddings backfill docs --text body --vector embedding
# add --model <m> if you changed the embedding model from the default
```

This (re-)embeds existing rows the `vectorEmbedding()` mixin missed —
the same command to use after a model change.

## Trying it

Once a key is set, drive the synthesized routes from a web client (or
the dashboard's invoke surface):

```ts
// mint a thread id, subscribe to the live feed, then send a turn
const threadId = `thread_${crypto.randomUUID().replace(/-/g, '')}`
const { data: messages } = useSubscription('app', ['support.messages', { threadId }], { threadId })
const send = useAction('app', 'support.send')
await send.run({ threadId, order: messages?.length ?? 0, prompt: 'How do I reset my password?' })
```

The agent calls `searchDocs`, retrieves the relevant doc, and answers
from it — the assistant bubble streams in as `support.messages` re-fires.

Drop more `*.agent.tsx`, `*.tool.tsx`, `*.action.ts`, `*.seed.ts`, or
schema files anywhere in this tree — discovery is by file convention.
