Перейти к основному содержимому

The AI subsystem serves two product surfaces (plus one showcase demo) through one endpoint and one orchestrator. They are told apart by an explicit surface discriminant — not by implicit request-flag truthiness.

The two surfaces

chatbot deskbot
Role The v10r expertwhy and how the project is built The in-desk operator — does anything a user can do via the desk UI
Mode Read-only, grounded, citation-faithful Q&A Agentic, mutating, approval-gated
Route POST /api/ai/chatbot POST /api/ai/deskbot
Client composites/chatbot/Chatbot.svelte — persistent, minimizable, non-modal panel; live thread owned by the chatbot-session singleton (see ../app-shell/ai-assistant.md) chat/ChatPanel.svelte (desk panel)
Harness buildRetrievalTools()chatbotToolMeta createDeskTools()deskbotToolMeta
Tools search_catalog, search_project_docs, get_llmwiki_pages, get_rawrag_chunks, resolve_ref desk_* read/write/create/delete, desk_propose_plan, resolve_ref
System prompt SYSTEM_PROMPT (plain) DESK_SYSTEM_PROMPT (XML-tagged) + permissions + desk-context
Corpus (nRAG) System-owned (source IN docs,catalog, SYSTEM_DOCS_USER_ID) + per-user llmwiki — curated, static (catalog slice graph-seeded; docs-corpus graph tier dormant) The user's own desk files — per-user, mutable, not graph-seeded, private
Location-awareness site-awareness — the current public route as a thin server-resolved page label (public-catalog only); v1 built (dev, uncommitted), see site-awareness.md desk-awareness — live desk state (panelContext/deskLayout/activeWorkspace): which panels & files are open; live
Invariants Never emits a DeskEffect; never creates a proposal All mutations route through db/desk; write & destructive tools require human approval (proposal → approve-route), never mutating in-loop; only reversible creates run in-loop

A third value, rag-demo, drives the showcase retrieval-pipeline demo. It is not a product surface and must not dilute the chatbot.

Dispatch — the surface discriminant

ChatInput.surface: TurnSurface ('chatbot' | 'deskbot' | 'rag-demo') is the single named dispatch decision in chat-orchestrator.ts. The three per-surface routes set it explicitly:

Route Surface
POST /api/ai/chatbot orchestrateChat({ surface: 'chatbot' })
POST /api/ai/deskbot orchestrateChat({ surface: 'deskbot' })
POST /api/ai/showcase/rag derived (the rag-chat showcase toggles useLlmwiki vs. raw retrieval)

All three share one entry guard, guardAiRequest() (guard.ts), which dedups auth → aiConfigured → rate-limit → daily-budget so the routes stay thin and the rate-limit key can't drift across copies. When surface is absent it is derived from the legacy useLlmwiki/useRetrieval flags (so the bare showcase clients keep working). The retrieval surfaces additionally require a fresh user turn — resume turns degrade to the plain deskbot streaming path, where the desk tools are filtered to read-only (desk:read/desk:ask): the approved plan has already run via the deterministic approve-route replay, so the acknowledgement turn physically cannot re-mutate. Approval binds execution.

orchestrateChatInner → resolve surface →
  surface === 'chatbot'  → llmwiki-first grounded turn (+ relevance-gated system-docs prefetch)
  surface === 'rag-demo' → showcase retrieval demo
  surface === 'deskbot'  → agentic desk tool loop (default; also handles resume turns, which mount read-only desk tools only)

Harness split (zero tool overlap)

The two tool collections are physically and semantically disjoint and grow independently:

  • chatbotToolMeta — read-only retrieval tools. No scope field (uses surface-neutral ToolMeta/ToolRisk).
  • deskbotToolMeta — desk CRUD/action tools, each carrying its gating DeskToolScope (DeskToolMeta).
  • allToolMeta — the union, for admin/telemetry that needs every tool regardless of surface.

A chatbot tool never carries a desk scope; a deskbot tool always does. The /admin/ai/tools topology view derives branch from membership (name in chatbotToolMeta), not from a scope artifact.

One-door rule (deskbot mutations)

Every deskbot mutation flows through the single SSOT executeDeskToolCall() (tools/desk-execute.ts) — the one door. Write and destructive tools never mutate in the agent loop: their execute validates the target and returns a requiresApproval sentinel, which the orchestrator turns into a pending agent_proposal (a PlanCard). That mutation runs only via the proposal-approval replay (POST /api/ai/proposals/[id]/approve), which records a real approvedBy/approvedAt. Reversible creates (soft-delete-recoverable) mutate in-loop, auto-approved. For the replay to execute the approved plan, the persisted proposal payload carries each step's args (the model supplies them on desk_propose_plan); an empty-args payload would silently no-op (executeDeskToolCall(tool, {}) → "File not found"), so the approval would bind nothing.

Two tests guard distinct properties of the door. index.test.ts drift-guards tool name coverage — it fails if a mutating deskbot tool lacks a replay case, so the replay path can never silently fall behind the live tool set. desk-execute.test.ts separately guards that args round-trip — empty args must surface "File not found" (never a fake success), and real args must reach the desk mutation verbatim.

nRAG: one shared kernel, two profiles, two corpora

"nRAG" is an informal umbrella term for the whole retrieval subsystem — not a code identifier (no nRAG symbol exists; the entry point is rawrag/retrieve()). The subsystem spans both the rawrag engine and the llmwiki pointer layer — curated TLDR-with-chunk-pointers over the immutable rawrag chunks — so a definition naming only the engine is incomplete.

The retrieval engine (rawrag/retrieve() — embed → tiers → RRF fusion → drill, with the single user_id tenant-isolation filter) is shared mechanism. The two surfaces exercise it as distinct profiles:

chatbot profile deskbot profile
Corpus SYSTEM_DOCS_USER_ID (docs/catalog) + per-user llmwiki The user's own desk files
Tiers Designed 1–3 (graph tier valuable — catalog is Neo4j-seeded); live: the chatbot requests tier-1 only — tiers 2–3 unexercised by the chatbot today 1–2 (no graph — desk files aren't graph-seeded)
Grounding Injected <project-overview> system-overview anchor + relevance-gated system-docs prefetch + llmwiki + on-demand drill; post-stream citation verification desk:ask read-only tool (desk_search_knowledge); no citation chips. Read-only: excluded from hasMutatingScope/stepsForScopes/the plan gate — it never triggers plan-before-execute (only the turn's mutating tools do)
Freshness Static curated corpus Mutable — aiContext opt-in; reconciled off the hot path by the desk-rawrag-sync job (polls updatedAt), not on each save

The kernel is never forked (a duplicated user_id filter would be a cross-tenant-leak risk); the corpus boundary is purely document.userId.

Location-awareness — two profiles

Each surface knows where the user currently is, so deixis ("this", "here") resolves to their actual location. This is location-awareness — one idea, two surface-specific profiles:

  • site-awareness (chatbot) — Vely's awareness of the public route you're viewing, as a thin server-resolved page label (public-catalog routes only, never the raw path or DOM). v1 built (dev, uncommitted) — see site-awareness.md.
  • desk-awareness (deskbot) — the deskbot's awareness of your live desk state: which panels and files are open (panelContext/deskLayout/activeWorkspace), first-party and mutable. Live.

Same concept, different depth. Site-awareness and desk-awareness are two profiles of one idea — ground the user's here/this in their current location — at deliberately unequal depth. Desk-awareness is rich, first-party, and mutable (full panel and file content); site-awareness is intentionally thin — a single server-resolved route label, public-catalog routes only, never DOM-scraped. The asymmetry is by design, not a parity gap: deeper page extraction (DOM, selected text) is a standing security finding, not missing work.

The mechanism behind site-awareness is page-awareness — resolving page.route.id to a <current-page> label. "Location-awareness" is the family, "site-awareness"/"desk-awareness" the two profiles, and "page-awareness" the chatbot-side mechanism. Full design: site-awareness.md.

Status

Live: the naming + dispatch discriminant; the per-surface route split (/api/ai/chatbot · /api/ai/deskbot · /api/ai/showcase/rag) behind the shared guardAiRequest; the harness split (zero overlap); the one-door rule (plan payload carries per-step args, replayed verbatim; resume turns mount read-only desk tools — approval binds execution); the tool-layer approval gate (every write/overwrite/delete — even single-target — returns a requiresApproval sentinel and mutates only through a human-approved proposal recorded with approvedBy/approvedAt; the model-minted confirmed self-handshake is gone; shouldRequirePlan widened to mutatingScopeGranted && destructiveIntent, now soft planning guidance; a pre-image desk.file_revision snapshot makes an approved overwrite/delete recoverable); the surface analytics column (ai_surface enum on ai.conversation + conversation_step, stamped at creation, conv_step_surface_idx); the chatbot nRAG profile (relevance-gated system-docs prefetch); and the deskbot nRAG profiledesk:ask read-only grounding tool (desk_search_knowledge) over the user's own aiContext desk files, ingested via the shared kernel (source = 'desk') and kept fresh by the desk-rawrag-sync job (polls desk.file.updatedAt).

Browser-verified 2026-06-25: the chatbot's Phase-C foundation grounding is live — an injected <project-overview> system-overview anchor (loadOverview([SYSTEM_DOCS_USER_ID], PROJECT_DOCS_COLLECTION_ID)) plus tier-1-only retrieval. The anchor is the load-bearing fix for the original broad-question bug: "how do I use v10r?" now answers correctly with real /docs/... citations (5/5 functional probes green). Hierarchical docs chunking landed too, but is groundwork for future tier-2 surfaces — it does not change chatbot answers today, and the corpus conversion is partial (36/93 docs, quota-gated). See knowledge-base.md.

Planned: the physical _shared/chatbot/deskbot directory layout — all three surfaces still dispatch from one chat-orchestrator.ts. Also planned: per-surface Valibot request schemas (both routes share ChatRequestSchema today) and a declarative ToolDescriptor manifest backed by a DB ai_tool registry. And site-awareness — the chatbot's location-awareness profile (site-awareness.md, v1 built + browser-verified live, dev/uncommitted); its sibling desk-awareness already ships as the deskbot's panelContext/deskLayout injection.

← Back to Blueprint

Думаете, этот паттерн можно сделать лучше? Расскажите как.

Оставить отзыв