# The AI assistant & Context Constructor (2.0+) `lattice gui` ships an optional assistant rail. It is **GUI-only** and inert until you configure a credential — the `latticesql` library API is unchanged. ## Connect Claude Open **Settings → User → Assistant**. The primary action is **Connect with Claude** — an Authorization-Code + PKCE flow that links your Claude Pro / Max / Enterprise **subscription**, so the assistant runs on your own account with no API key to paste or rotate. It works out of the box (the public OAuth client is built in); the panel shows **Connected with Claude** once linked, with a **Disconnect** button. The redirect is a loopback callback derived from the GUI's own origin — only a loopback `Host` is trusted, so a forged/proxied host can't redirect the authorization code elsewhere. Prefer a raw key? Expand **Advanced — use an API key instead** and paste an Anthropic API key (or set `ANTHROPIC_API_KEY` in the environment). Keys are stored encrypted in the native `secrets` entity; the env var is a fallback. Every `ANTHROPIC_OAUTH_*` value (authorize/token URL, client id, scopes, redirect) can be overridden via the environment for a non-default deployment — see [`.env.example`](../.env.example). ## Chat The rail runs a Claude tool-calling loop streamed over SSE. The model can list, read, **full-text search**, create, update, link, delete tables, and revert in the active database. **Every edit goes through the same audited, undoable mutation path as a manual edit** — it appears in the activity feed and the version history and can be reverted. The **top search box hands your query to the assistant**: type and press Enter and the query is submitted as a chat turn, which the assistant answers using its `search` (and read) tools rather than a plain text match. The assistant never sees the conversation-storage or `secrets` tables (search and `list_entities` both exclude them). When the assistant points you at a specific record — ask it to "link me to" or "open" one — it renders a **clickable object pill** inline in its answer (emitted as `[label](lattice:///)`). Clicking the pill opens that row in the GUI via the same mode-aware navigator the activity feed uses; it links the user-facing record (the contract/person/etc.) rather than an internal `files` id, and only ids it actually retrieved. **The assistant reads your organized context.** A new `get_row_context` tool lets the assistant pull a record's full rendered context — its own fields, related records, and combined summary — in a single call. It leverages the context tree Lattice maintains rather than re-stitching together raw reads, so the model can answer follow-ups like "summarize this record" or "what are the related items?" in one tool call. It falls back to direct row tools when a record hasn't been rendered yet. **Deleting a table is guarded + reversible.** The `delete_entity` tool refuses built-in tables, tables another table links to, and tables you don't own. An **empty** table is soft-deleted immediately; a **non-empty** one is **not** deleted until you decide what happens to the data — the tool reports the row count and the assistant asks, then you choose `delete_data` (soft-delete the rows too) or `move_to` another table. The physical table + rows are kept (no hard drop), so the whole thing is revertible from version history. **Adding a field to an existing table.** The `add_column` tool lets the assistant add a single column to an existing table on request ("add a priority field to projects", "add an email column"). The column is registered live, persisted, audited, and revertible. On a cloud, the per-column masking view is rebuilt so members see the new field immediately. Conversations persist in the native `chat_threads` / `chat_messages` entities; use the thread switcher to revisit them. A new thread is **named from a short AI summary** of its first exchange (e.g. "Adding New Notes About Cheese"). The assistant's **data changes are saved with each turn and replayed as activity cards** when you reopen the conversation — collapsed by type (e.g. "Deleted 19 tables", "Removed 49 rows across 9 tables"), with the operation's icon. Reads (list / get / search) change nothing, so they produce no card; only data changes appear. The activity feed is scoped to the open conversation rather than a global workspace log. The assistant **remembers what it read across turns.** Earlier tool calls and their results (including row ids) are replayed into the model's context, so a follow-up like "now update that row" reuses the id it just listed instead of guessing one. Replay is bounded to the recent turns within a size budget and is secret-redacted; set `LATTICE_CHAT_REHYDRATE=false` to disable it. Reads are also deterministically ordered, so listing the same table twice returns the same rows. The assistant **knows the record you're looking at.** When a file or row detail is open, the chat passes that record (table + id) as context, so "delete this file", "summarize this", or "share this row" act on it directly instead of asking which one. It's a hint only — every action still goes through the same permission-gated tools, so it can't reach a record you couldn't otherwise touch. **Pasted GUI links resolve to the actual record.** When you paste a local GUI link (the address bar's `…/#/fs/
/`) into the chat, the assistant resolves it deterministically to its real data in the database (via the same permission-gated read as any other access), so it can answer queries about that record without needing to fetch or guess. Resolution happens in code; the resolved data appears in context alongside the viewed record. The assistant can also **answer questions about Lattice itself.** Ask "what is private mode?" or "how do I invite a member?" and it calls the `lattice_help` tool, which searches Lattice's own documentation (these `docs/*.md` files — the single canonical source, shipped in the npm package) and answers from it rather than guessing or searching your data. ## The Context Constructor (file & text ingest) Drag files onto the rail, click the upload button, or paste text (or a URL). For each source: 1. **Referenced, not copied.** The source becomes a native `files` row that points at the original; bytes are not moved into Lattice. 2. **Extracted.** Plain text/markdown/code is read directly; documents (PDF, Word `.docx`/`.doc`, PowerPoint `.pptx`, Excel `.xlsx`, OpenDocument `.odt`/`.ods`/`.odp`, EPUB, RTF) are parsed **natively in-process** — no external CLI; **images are described by Claude vision**; **scanned/image-only PDFs** with no text layer fall back to Claude's native PDF read; a pasted **bare URL is crawled** for readable text (and the URL preserved on the row as a `cloud_ref`). Legacy binary `.xls`/`.ppt` (pre-2007) and any other binary are still referenced and marked `extraction_status='skipped'`. The parsers ship as optional dependencies, so a document just skips (rather than failing) if its parser isn't installed. 3. **Summarized** with Claude Haiku (the description fills in). 4. **Organized.** The text is classified against your existing records, and for each match the file is **linked** — **auto-creating the `files_` junction table when none exists yet**. When a source fits **nothing** (and aggressiveness is high), a new native `notes` object is **created** for it, linked back via `source_file_id`. New objects, enrichment, links, and junctions are all reversible via the version history. ### Reading a web link (`ingest_url`) You can also just **ask** the assistant to read a link: "summarize https://… for me", "save this article", "read that page". The model calls the **`ingest_url`** tool, which fetches the page, saves it as a `files` web reference (`ref_kind='cloud_ref'`, `ref_provider='web'`), summarizes it, and reports back. The saved reference follows the same sharing rules as any file (private mode → private). It is deliberately **not** a general fetch primitive — that would be an SSRF + prompt- injection hazard for an LLM-driven tool. Guardrails: - **User-provided URLs only.** The tool fetches only a URL that appears verbatim in your own message; it refuses a URL discovered inside a file, a row, or model output. - **SSRF + policy + rate limits.** Every fetch passes the SSRF guard (no private / loopback / metadata addresses), a deployment on/off + allow/block-list policy, a per-turn fetch budget, a process-wide concurrency cap, and a per-host throttle — all tunable via the `LATTICE_URL_*` env vars (see [`.env.example`](../.env.example)). - **Untrusted content.** A fetched page is treated as untrusted data end-to-end: the row is flagged `source_json.untrusted=true`, the enrichment prompts wrap its text in explicit "data, not instructions" markers, and `get_row`/`list_rows` re-wrap it when the assistant reads it back. The compact tool result never includes the raw page text. - **Optional JS rendering.** SPA pages render with headless Chromium when the optional `playwright` dependency is installed; otherwise the crawler degrades to the static extraction (one warning, no failure). Posts on x.com / twitter.com are read via their public oEmbed endpoint. This shares one `ingestUrlAsFile` path with the `/api/ingest/text` URL branch, so a pasted URL and an assistant-requested URL behave identically. ### Library API The same intelligence is a first-class, GUI-independent API (inert without an LLM client): `organizeSource`, `describeImage`, `crawlUrl`, `enrichKnowledge`, and the `summarizeText` / `classifyLinks` primitives — all importable from `latticesql`. `sharp` + `file-type` are optional, lazily-loaded deps; the crawler uses `jsdom` + `@mozilla/readability`. A transient **"Analyzing…"** row shows while ingest runs; the add/enrich/link events stream into the feed as the server materializes them. ### Structured-source import (drop a JSON / `.xlsx`) (4.2) The Context Constructor above turns _unstructured_ sources (documents, images, web pages) into a summarized, linked `files` row. **Dropping a structured source — a JSON object or an Excel `.xlsx` workbook — takes a different path:** Lattice infers a schema from it (entities, dimensions, junctions) and materializes it into real tables. Excel sheets become records (header + data-region detection); per-slice tabs that mirror a master become read-only **views** (no duplicated rows). An **as-of date** is detected (file contents → name → Excel preamble → a Claude fallback, or per-row from a date column), so re-importing a newer period keeps a **dated snapshot** beside the prior one; a re-upload is fingerprinted and matched to the tables already in the workspace, so it lands as a new snapshot rather than duplicate tables. A **recognized dataset with a confident date imports silently** as a dated snapshot (reported in the activity feed); a brand-new dataset, or a recognized one with no confident date, surfaces an **inline confirm card** that proposes the schema, the as-of date (and any per-row date column), and the mode before anything is written — applied via `POST /api/import/apply`. The same inference + materialization functions (`inferSchema`, `materializeImport`, `detectAsOf*`, `excelToRecords`, `dedupeAndDetectViews`, …) are exported from `latticesql` for library use. See [importing.md](importing.md) for the full walkthrough. ## Artifacts Ask the assistant to "write a doc / note / summary / write-up" and it calls the `create_artifact` tool: the Markdown is saved as a native `files` row (flagged `artifact_type='markdown'`, content inline in `extracted_text`), auto-opens in the viewer rendered as formatted Markdown, and shows an **✦ Artifact** badge. An artifact is an ordinary file, so it follows the **same sharing rules** — created in private mode it's owner-only; otherwise it follows the files-table default — enforced by cloud Row-Level Security. ## Schema definitions New columns and tables get a concise one-line **definition** generated automatically by a cheap, non-blocking, fail-silent model pass at creation time (it never blocks the write and never overwrites an authored value). Definitions show as hover tooltips on table headers, field labels, the sidebar, and dashboard cards; built-ins ship for the native entities. They're injected into the assistant's schema context (so a good definition improves categorization), and the assistant can author or correct one with the **`set_definition`** tool (`{ table, column?, description }` — column present ⇒ column definition, absent ⇒ table definition). ## De-duplication Uploading a **byte-identical** file is de-duplicated automatically: the copy is merged onto the original (its many-to-many links re-pointed to the survivor, then soft-deleted — recoverable from Trash / Undo), attributed to "Lattice" in the feed. No modal, no prompt. The assistant can also de-duplicate any table on request with the **`dedup`** tool (`{ table, fuzzy? }`); fuzzy-merge liberalness follows the [aggressiveness slider](#inference-aggressiveness). ## Inference Aggressiveness A single **Conservative ↔ Aggressive** slider (Settings → Assistant) tunes how much the assistant extrapolates. It maps to the model sampling temperature, how liberally the ingest classifier proposes links, and whether ingest auto-creates a missing junction (gated at ≥ 0.25) versus only suggesting it. Default: balanced (0.5). Settable via `PUT /api/assistant/aggressiveness { "value": 0..1 }`. This is a **user preference** (machine-local `~/.lattice/preferences.json`), not a workspace secret — it persists across workspaces and never appears in a workspace's Secrets object. ## Voice (optional) The composer's 🎙 mic dictates **on-device** — speech is transcribed in your browser by Whisper (WASM), so it needs no API key or setup and audio never leaves your machine. There is no voice-provider choice in the GUI; the mic is always shown when a microphone is available, and shown disabled with a tooltip when none is. **While a note is recording or transcribing, the composer is read-only** — it shows a "Listening… / Transcribing…" placeholder and the Send button is disabled — and the transcript is inserted when you stop. Keyed cloud transcription (`OPENAI_API_KEY` / Whisper or `ELEVENLABS_API_KEY`) remains available to **API** callers via `POST /api/assistant/transcribe` for backward compatibility; the GUI itself always dictates on-device. ## Cloud The assistant runs against local SQLite and any `postgres://` connection, including a Lattice cloud. On a cloud it connects as your own scoped role, so its reads and writes are confined by Postgres Row-Level Security to the rows you may see — see [cloud.md](cloud.md).