Ask, chat & live sources
How a question becomes a cited answer — and how the UI stays calm while the workflow runs.
Ask topology#
Graph drafting and live research run as true parallel arms after seed search. Whether live research runs depends on the question and available graph evidence, not on the first draft.
Two callers, one retrieve path:
- Doso reasons —
POST /v1/chat/completions(effort: "fast" | "expert") orPOST /v1/search. Retrieve + draft. Direct clients. No second model. - Caller reasons —
POST /v1/ask/evidence. Same retrieve pack, no LLM. The client writes the answer.
Ask effort on completions:
- fast (default) — seed → 1-hop → passages → one draft. No rewrite, live, or skeptics.
- expert — the topology below (live + skeptics). The product UI (
POST /v1/ui-chat) uses this.
Expert topology:
- Rewrite — resolve follow-ups when history exists.
- Seed search — entities (+ document/owner boosts). Document boost prefers embedded passage retrieval, then token-AND title/text search.
- Parallel arm A (graph) —
fanout_neighbors→ focus evidence →worker_draft_graph. - Parallel arm B (live) —
route_live_need(question, seeds)→ Firecrawl/X → ingest candidates → reseed neighbors when tools are on and the question is not personal-only. - Merge — if live produced evidence,
worker_draft_refined; else keep the graph draft. - Skeptics — on by default. The
ASK_SKEPTICSbootstrap setting defaults totrue; the shipped Settings → Tools → Enable LLM skeptics (default on) control can change it at runtime. Three fresh-context lenses use majority vote to filter citations. - Evidence focus — owner edges for first-person / personal contact asks; rank by predicate strength + question-token overlap + recency.
- Document arm — retrieve ~900-character library chunks by embedding (OpenAI-compatible
/embeddings), not as a second memory. If no embed provider or empty index, fall back to title/textILIKEplus a token window. Chat / live-ask / staging docs stay out of this index. - Ingest bind — the same embedding family is a filter on the way in. Extracted names bind to an existing entity of the same type only when they are aliases of the same referent (high cosine and token overlap). A hypernym (
AI) does not absorbAI frontier race. Sibling topics stay distinct. The graph remains the memory. - Ask reads — verified triples by default; Settings toggle Include candidate triples in ask uses
status=active.
Bootstrap env: FIRECRAWL_API_KEY, TOOLS_X_SEARCH, TOOLS_FIRECRAWL, ASK_SKEPTICS, ASK_INCLUDE_CANDIDATES, LLM_EMBED_PROVIDER. Settings owns the runtime values after bootstrap.
What the user sees#
| Layer | Content |
|---|---|
| Trace (always collapsed until you open it) | Workflow nodes: plan, draft, search, ingest, refine |
| Answer bubble | Final answer (after live sources when available) |
| Earlier draft | Collapsed; fast graph-only preview if it differed |
| Citations | Collapsed; one fact per (src, dst), prose form |
| Sources | Collapsed; Firecrawl / X URLs |
Status chatter (“Gathering live sources…”, “Updated from graph”) is not written into the answer body. Progress belongs in the trace.
Facts & citations (no “code” in the answer)#
Internal storage is still S–P–O triples. For LLM context and the Citations drawer we serialize as prose:
1Elizabeth Stone is CPTO of Netflixnot (Elizabeth Stone)-[is CPTO of]->(Netflix).
Rules:
- Skip triples with empty subject/object.
- Dedupe citations to one predicate per (src, dst) (prefer shorter).
FormatFactconjugates forYou/I(You work at…, notYou works at…);named→Your name is….- Skip bare
mentioned/ cc noise in note serialization (name-drops are not grounding). - Draft prompts: natural prose; state each relationship once; never echo a citation bullet into the answer.
- Citation and source metadata travel separately from answer prose so a client parse miss cannot expose internal formatting.
- Duplicate citation echoes are removed before the answer is shown.
Memories is a groomed first-person projection — see Memory Groom. The list contains one card per subject with structural titles immediately; multi-fact cards may be compacted into a clearer summary. Soft contact and glue relations are hidden. Ontology is write-side vocabulary, not the visibility gate. Forget supersedes the underlying triples.
The graph is the knowledge source of truth. Memories and the entity wiki are read models over verified graph relations, not separate places to author facts. Opening an entity in Knowledge compiles its description, grouped relations, provenance sources, and an optional cached summary from its verified one-hop neighborhood. See Entity wiki & edge statements.
Source-aware extraction hints prefer people, organizations, events, and useful thread relationships over incidental name-drops.
Chat persistence#
- Conversations + messages in the configured SQLite or Postgres store (
/v1/conversations). - OpenAI-compatible streaming:
POST /v1/chat/completions(SSE). Still Chat Completions, not the Responses API.stream: trueforwards Grok thinking asdelta.reasoning_contentand the drafted prose asdelta.content(JSONanswerfield, not the raw object). Non-stream requests are unchanged. - Web / Tauri:
useChat→ RustPOST /v1/ui-chat(AI SDK UI message stream:reasoning-deltathentext-delta). - Workflow events: SSE
GET /v1/events+ per-messagemeta.workflowfor History replay.
Chat memory (self-facts)#
User chat turns can write into the graph — not only live web/X.
- Memory shares (
LooksLikeMemoryShare: durable statements, not questions) runRememberFromChatbefore the reply, then take an acknowledge path — a short woven reply, not ask-against-empty / “I don’t have enough yet”, and not a fact dump. - Other turns may still remember async after the user message lands.
- A cheap gate skips pure questions / acks; keeps first-person statements (
My name is…,I live…, …), named-entity attributes (Pixel's breed is…), and corrections (below). Borderline statements that miss the heuristic get a small LLM classify (save: true|false) before remember. - Creates an idempotent document
chat://conversations/{id}/messages/{id}withmeta.source=chat(legacyuser_asserted+origin=chatstill treated as chat). After ingest, candidates from that doc are promoted to verified so Memories and ask see them. - Chat extract maps I/me → a single owner PERSON (
props.role=owner). NoOWNER_NAMEenv — identity is graph-only. Pet/person attributes use predicates likebreed,age,birthday. - Placeholder owner is
"You"until anamedfact renames them. - First-person asks (
what's my name?) always seed the owner entity. GET /v1/owner→{ exists, named, canonical_name }for onboarding UI.
Onboarding: empty chat + unnamed owner shows a short prompt (name / live / work). Answers go through the same remember path.
Corrections (edit the graph)#
Chat is not append-only for owner facts. When the user retracts or equates entities, extract emits a corrections array and assemble mutates the graph:
| User says | Graph effect |
|---|---|
I don't work at Northstar Studio | reject active employment-like edges between owner ↔ Northstar Studio (works_at, founder, …) |
ExampleWorks is just Example Labs | merge absorb → keep; absorb name becomes an alias |
Example Labs is the trading name of Example Labs LLC | merge (keep legal / absorb trading name per extract guidance) |
There's no Example Labs Ltd | merge Example Labs Ltd → Example Labs (or alias-only if the former was never a node) |
Also: negated predicates in relations (does_not_work_at) are treated as rejects, not new edges.
Owner supersede still replaces prior values for identity/home (named, lives_in, …) but not works_at globally (multiple employers are allowed — retract specific edges via correction).
Configured aliases can also be supplied through DOSO_ENTITY_ALIASES. No
organization aliases are built into Doso.
Library#
Document list shows ingest status and a small stone source badge when meta.source is set (gmail, calendar/gcal, imessage, firecrawl, x, chat).