diff --git a/.gitignore b/.gitignore index b450be4..b64188c 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,7 @@ .demo.log .demo.pid .boot.log + +# Generated by `npm run build:docs` — regenerated on every full build +# (prebuild) and on demand during dev. Don't commit the artifact. +/public/docs-index.json diff --git a/app/app.css b/app/app.css index 12f2064..6dfa9da 100644 --- a/app/app.css +++ b/app/app.css @@ -33,6 +33,7 @@ @source "../../lib-code-ui/src"; @source "../../lib-diagram-ui/src"; @source "../../lib-onboarding-ui/src"; +@source "../../lib-lexical-rag-ui/src"; @source "../../lib-notification-ui/src"; /* CREMA:SOURCES */ diff --git a/app/lib/admin-tools.ts b/app/lib/admin-tools.ts index 59cfd98..6260fd2 100644 --- a/app/lib/admin-tools.ts +++ b/app/lib/admin-tools.ts @@ -15,9 +15,13 @@ import { suspendTenant, type Tenant, } from "~/lib/arcadia/tenants" -import { searchDocs } from "~/lib/docs-search" +import { createRAGClient } from "@crema/lexical-rag-ui" import { BLOCK_INDEX, getBlockSchema } from "~/lib/block-schemas" +// Lazy singleton — first tool call fetches /docs-index.json, subsequent +// calls reuse the parsed MiniSearch instance. +const docsClient = createRAGClient("/docs-index.json") + export type ToolCall = { name: string args: Record @@ -253,8 +257,22 @@ const TOOLS: ToolDef[] = [ 10, Math.max(1, typeof args.limit === "number" ? args.limit : 5), ) - const hits = await searchDocs(query, limit) - return { query, count: hits.length, hits } + const hits = await docsClient.search(query, { limit }) + // Tool-shape parity with the previous searchDocs() return: collapse + // tags[] back to category for now so the agent's prior expectations + // and any cached examples still parse cleanly. + return { + query, + count: hits.length, + hits: hits.map((h) => ({ + id: h.id, + title: h.title, + sourcePath: h.sourcePath, + category: h.tags[0] ?? "", + excerpt: h.excerpt, + score: h.score, + })), + } }, }, { diff --git a/app/routes/ai.tsx b/app/routes/ai.tsx index 5b43392..ca08361 100644 --- a/app/routes/ai.tsx +++ b/app/routes/ai.tsx @@ -69,7 +69,19 @@ import { Avatar, AvatarFallback } from "~/components/ui/avatar" import { pageTitle } from "~/lib/page-meta" import { useArcadiaClient } from "@crema/arcadia-client" import type { Message as LLMMessage, ToolCall } from "@crema/llm-ui" -import type { DocHit } from "~/lib/docs-search" +// Shape of a single hit returned by the `search_docs` tool. Defined here +// rather than imported from the lib because the tool wrapper in +// admin-tools.ts intentionally collapses the lib's `tags[]` back to a +// single `category` for tool-response stability — this type matches +// what the model actually sees. +type DocHit = { + id: string + title: string + sourcePath: string + category: string + excerpt: string + score: number +} import { AgentAvatar, ToolCallCard, diff --git a/package-lock.json b/package-lock.json index 627de6b..3e1d867 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "d3-geo": "^3.1.1", "isbot": "^5.1.36", "lucide-react": "^1.8.0", + "minisearch": "^7.2.0", "motion": "^12.38.0", "openapi-fetch": "^0.17.0", "phoenix": "^1.8.5", @@ -7264,6 +7265,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/minisearch": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/minisearch/-/minisearch-7.2.0.tgz", + "integrity": "sha512-dqT2XBYUOZOiC5t2HRnwADjhNS2cecp9u+TJRiJ1Qp/f5qjkeT5APcGPjHw+bz89Ms8Jp+cG4AlE+QZ/QnDglg==", + "license": "MIT" + }, "node_modules/morgan": { "version": "1.10.1", "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.1.tgz", diff --git a/package.json b/package.json index 057d148..bbb614a 100644 --- a/package.json +++ b/package.json @@ -3,13 +3,15 @@ "private": true, "type": "module", "scripts": { + "prebuild": "npm run build:docs", "build": "react-router build", "dev": "react-router dev", "start": "react-router-serve ./build/server/index.js", "typecheck": "react-router typegen && tsc", "test": "vitest run", "test:watch": "vitest", - "sync-libs": "node scripts/sync-libs.mjs" + "sync-libs": "node scripts/sync-libs.mjs", + "build:docs": "node scripts/build-docs-index.mjs" }, "dependencies": { "@base-ui/react": "^1.4.0", @@ -21,6 +23,7 @@ "d3-geo": "^3.1.1", "isbot": "^5.1.36", "lucide-react": "^1.8.0", + "minisearch": "^7.2.0", "motion": "^12.38.0", "openapi-fetch": "^0.17.0", "phoenix": "^1.8.5", diff --git a/scripts/build-docs-index.mjs b/scripts/build-docs-index.mjs new file mode 100644 index 0000000..9f0cc99 --- /dev/null +++ b/scripts/build-docs-index.mjs @@ -0,0 +1,39 @@ +#!/usr/bin/env node +// Build the /docs-index.json bundle consumed by @crema/lexical-rag-ui at +// runtime. Thin wrapper — all engine logic lives in the lib's builder.ts; +// this file owns the per-app config (which arcadia-app docs to index and +// how to tag them). +// +// Run: npm run build:docs +// +// Allowlist is intentional. Excluded files are aspirational/stale and +// would poison answers (TODO lists, design docs for unshipped features, +// sub-app READMEs that aren't part of arcadia-core). To add a file, +// append to SOURCES below — don't auto-discover. + +import { resolve, dirname } from "node:path" +import { fileURLToPath } from "node:url" + +import MiniSearch from "minisearch" +import { buildIndex } from "../../lib-lexical-rag-ui/src/builder.mjs" + +const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..") +const ARCADIA = resolve(ROOT, "../reference/arcadia-app") +const OUT = resolve(ROOT, "public/docs-index.json") + +const SOURCES = [ + { path: "README.md", tags: ["core"] }, + { path: "docs/ARCADIA.md", tags: ["core"] }, + { path: "docs/MODULAR_MONOLITH.md", tags: ["core"] }, + { path: "apps/arcadia_core/README.md", tags: ["core"] }, + { path: "DEPLOY.md", tags: ["ops"] }, + { path: "DEV_DEPLOY.md", tags: ["ops"] }, + { path: "DEV_SETUP.md", tags: ["ops"] }, +] + +buildIndex({ + miniSearch: MiniSearch, + rootDir: ARCADIA, + outPath: OUT, + sources: SOURCES, +}) diff --git a/tsconfig.json b/tsconfig.json index 0d09a40..099a58f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -62,6 +62,8 @@ "@crema/diagram-ui/*": ["../lib-diagram-ui/src/*"], "@crema/onboarding-ui": ["../lib-onboarding-ui/src/index.tsx"], "@crema/onboarding-ui/*": ["../lib-onboarding-ui/src/*"], + "@crema/lexical-rag-ui": ["../lib-lexical-rag-ui/src/index.tsx"], + "@crema/lexical-rag-ui/*": ["../lib-lexical-rag-ui/src/*"], "// CREMA:PATHS": [""], "react": ["./node_modules/@types/react"], "react/*": ["./node_modules/@types/react/*"], @@ -69,6 +71,7 @@ "react-dom/*": ["./node_modules/@types/react-dom/*"], "clsx": ["./node_modules/clsx"], "tailwind-merge": ["./node_modules/tailwind-merge"], + "minisearch": ["./node_modules/minisearch"], "lucide-react": ["./node_modules/lucide-react"], "openapi-fetch": ["./node_modules/openapi-fetch"], "openapi-fetch/*": ["./node_modules/openapi-fetch/*"],