@mushinjs/plugin-knowledge 0.0.0-stage → 0.2.0-next.20261113103537

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/index.ts"],"sourcesContent":["import { createHash } from 'node:crypto'\nimport { mkdir, readFile, rm, writeFile } from 'node:fs/promises'\nimport { join } from 'node:path'\nimport { type Context, type MessageRecord, MushinError, randomId, Service, z } from '@mushinjs/core'\nimport { type BotRole, roleAtLeast, type Session } from '@mushinjs/plugin-bots'\nimport type { Query } from '@mushinjs/plugin-database'\nimport type { FlowNode } from '@mushinjs/plugin-flows'\nimport type {} from '@mushinjs/plugin-providers'\nimport type {} from '@mushinjs/plugin-storage'\nimport { type Candidate, candidateText, LEARN_PROMPT, parseCandidates, transcript } from './learn'\nimport { searchNode } from './node'\nimport { type IndexedChunk, normalize, SearchIndex, type SearchHit } from './search'\nimport { chunkText, type DocFormat, extractText, formatOf, FORMATS, htmlToText } from './text'\n\nexport * from './learn'\nexport * from './search'\nexport * from './text'\n\ndeclare module '@mushinjs/core' {\n interface Context {\n knowledge: Knowledge\n }\n\n interface Events {\n /** Bases, documents, chunks or candidates of a base changed. */\n 'knowledge/changed'(change: { base: string }): void\n /** A document or a reindex is being processed. */\n 'knowledge/progress'(progress: KnowledgeProgress): void\n }\n}\n\ndeclare module 'minato' {\n interface Tables {\n knowledge_base: KnowledgeBase\n knowledge_doc: KnowledgeDoc\n knowledge_chunk: ChunkRow\n knowledge_candidate: CandidateRow\n }\n}\n\nexport interface LearnSettings {\n /** Pick out candidate knowledge from the chats of the base's bots. */\n enabled: boolean\n /** `group:<id>` or `private:<id>`; empty learns from every chat of the bots. */\n channels: string[]\n /** Learn once this many new messages arrived in a chat. */\n every: number\n /** Add candidates straight to the base instead of waiting for review. */\n autoAccept: boolean\n /** Most learning requests per day for this base. */\n dailyLimit: number\n /** Provider and model for learning; empty uses the bot's, then the default. */\n provider: string\n model: string\n}\n\nexport interface KnowledgeBase {\n id: string\n name: string\n description: string\n /** Embedding provider and model; empty means keyword search only. */\n provider: string\n model: string\n /** Length of the vectors; 0 until the base is embedded with its current model. */\n dimension: number\n chunkSize: number\n chunkOverlap: number\n /** Defaults for searches that do not say. */\n topK: number\n minScore: number\n /** Bots whose admins may use `/记住`, `/收录` and `/忘记`, and whose chats it learns from. */\n bots: string[]\n commands: { enabled: boolean; role: BotRole }\n learn: LearnSettings\n createdAt: number\n updatedAt: number\n}\n\n/**\n * `file`: an uploaded file. `url`: a web page, fetched again when reparsed. `text`: pasted text.\n * `notes`: entries added one at a time (`/记住`, `/收录`, the dashboard). `learned`: accepted\n * candidates from chats.\n */\nexport type DocKind = 'file' | 'url' | 'text' | 'notes' | 'learned'\nexport type DocStatus = 'pending' | 'parsing' | 'embedding' | 'ready' | 'error'\n\nexport interface KnowledgeDoc {\n id: string\n base: string\n title: string\n kind: DocKind\n /** File name or URL. */\n source: string\n format: string\n status: DocStatus\n error: string\n /** SHA-256 of the content, so the same file is not added twice. */\n hash: string\n chars: number\n chunks: number\n createdAt: number\n updatedAt: number\n}\n\ninterface ChunkRow {\n id: string\n base: string\n doc: string\n seq: number\n title: string\n text: string\n /** `Float32Array` bytes, normalized; empty when not embedded. */\n vector: ArrayBuffer\n /** Changed by hand; reparsing the document replaces it anyway. */\n edited: boolean\n /** For notes: who added it, where. */\n meta: Record<string, any>\n createdAt: number\n updatedAt: number\n}\n\nexport type KnowledgeChunk = Omit<ChunkRow, 'vector'> & { embedded: boolean }\n\nexport type CandidateStatus = 'pending' | 'accepted' | 'rejected'\n\ninterface CandidateRow {\n id: number\n base: string\n bot: string\n channel: string\n question: string\n answer: string\n status: CandidateStatus\n /** The chunk an accepted candidate became. */\n chunk: string\n createdAt: number\n updatedAt: number\n}\n\nexport type KnowledgeCandidate = CandidateRow\n\nexport interface KnowledgeProgress {\n base: string\n doc?: string\n /** `reindex` for a base being embedded again. */\n stage: DocStatus | 'reindex' | 'done'\n done: number\n total: number\n}\n\n/** A search hit with the base and document it came from. */\nexport interface KnowledgeHit extends SearchHit {\n base: string\n baseName: string\n docTitle: string\n}\n\nexport interface KnowledgeSearch {\n hits: KnowledgeHit[]\n /** Searched by meaning too (at least one base has an embedding model that answered). */\n semantic: boolean\n /** Problems that made a base fall back to keywords, such as the embedding model failing. */\n warnings: string[]\n}\n\n/** A hit as a reply's provenance and a run's state keep it. */\nexport interface KnowledgeRef {\n base: string\n doc: string\n chunk: string\n title: string\n}\n\nconst MAX_FILE_BYTES = 20 * 1024 * 1024\nconst MAX_TEXT_CHARS = 2_000_000\nconst NOTES_TITLE = '随手记'\nconst LEARNED_TITLE = '从对话学习'\nconst USAGE_KEY = 'knowledge/learn-usage'\nconst LEARN_STATE_KEY = 'knowledge/learn-state'\n/** Fewest new messages worth a learning request. */\nconst LEARN_MIN_LINES = 5\n\nconst DEFAULT_LEARN: LearnSettings = { enabled: false, channels: [], every: 50, autoAccept: false, dailyLimit: 20, provider: '', model: '' }\n\nconst learnInput = z.object({\n enabled: z.boolean().optional(),\n channels: z.array(z.string().regex(/^(group|private):\\S+$/)).optional().describe('\"group:<id>\" or \"private:<user id>\"; empty learns from every chat of the base\\'s bots'),\n every: z.number().int().min(10).max(500).optional().describe('Learn after this many new messages in a chat'),\n autoAccept: z.boolean().optional().describe('Add what is learned straight away instead of waiting for review'),\n dailyLimit: z.number().int().min(1).max(500).optional(),\n provider: z.string().optional(),\n model: z.string().optional(),\n})\n\nconst baseFields = {\n name: z.string().trim().min(1).max(60),\n description: z.string().max(500).optional(),\n provider: z.string().optional().describe('Embedding provider id (providers.embedChoices); empty for keyword search only'),\n model: z.string().optional().describe('Embedding model, e.g. text-embedding-3-small or bge-m3'),\n chunkSize: z.number().int().min(100).max(4000).optional().describe('Most characters per chunk (default 500)'),\n chunkOverlap: z.number().int().min(0).max(1000).optional().describe('Characters repeated from the previous chunk (default 50)'),\n topK: z.number().int().min(1).max(20).optional(),\n minScore: z.number().min(0).max(1).optional(),\n bots: z.array(z.string()).optional().describe('Bot ids whose admins may add knowledge from chat, and whose chats it learns from'),\n commands: z.object({ enabled: z.boolean().optional(), role: z.enum(['user', 'admin', 'master']).optional() }).optional(),\n learn: learnInput.optional(),\n}\n\nconst localDay = (date = new Date()) => `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`\nconst vectorBytes = (vector: Float32Array | undefined) => (vector ? vector.buffer.slice(vector.byteOffset, vector.byteOffset + vector.byteLength) as ArrayBuffer : new ArrayBuffer(0))\nconst vectorOf = (bytes: ArrayBuffer | Uint8Array | undefined | null) => {\n if (!bytes || !bytes.byteLength) return undefined\n const buffer = bytes instanceof ArrayBuffer ? bytes : bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength)\n return new Float32Array(buffer as ArrayBuffer)\n}\nconst hashOf = (data: Uint8Array | string) => createHash('sha256').update(data).digest('hex')\nconst errorText = (error: unknown) => (error instanceof Error ? error.message : String(error))\n\n/** The rows of a chunk as actions return them: no vector bytes. */\nfunction publicChunk({ vector, ...row }: ChunkRow): KnowledgeChunk {\n return { ...row, embedded: !!vector?.byteLength }\n}\n\n/**\n * Knowledge bases: documents split into chunks, searched by keywords and (with an embedding\n * model) by meaning. Flows search them with a node, the Agent with `kb.search`, admins add to\n * them from chat, and they can learn candidate knowledge from chats for review.\n */\nexport class Knowledge extends Service {\n static inject = ['storage', 'actions', 'llm', 'bots', 'model', 'database']\n\n private home: Context\n private indexes = new Map<string, SearchIndex>()\n private queue: Promise<unknown> = Promise.resolve()\n private abort = new AbortController()\n /** Base id → `chat key` → messages since the last learning. */\n private counters = new Map<string, number>()\n private learning = new Set<string>()\n /** Chat key → when it was last learned from, so the same messages are not read twice. */\n private learnState: Record<string, number> = {}\n private usage: { day: string; counts: Record<string, number> } = { day: '', counts: {} }\n\n constructor(ctx: Context) {\n super(ctx, 'knowledge')\n this.home = ctx\n }\n\n private get logger() {\n return this.home.logger('knowledge')\n }\n\n private get db() {\n return this.home.database\n }\n\n async *[Service.init]() {\n const model = this.home.model\n model.extend('knowledge_base', {\n id: 'string', name: 'string', description: 'string', provider: 'string', model: 'string', dimension: 'unsigned',\n chunkSize: 'unsigned', chunkOverlap: 'unsigned', topK: 'unsigned', minScore: 'double',\n bots: 'json', commands: 'json', learn: 'json', createdAt: 'double', updatedAt: 'double',\n }, { primary: 'id' })\n model.extend('knowledge_doc', {\n id: 'string', base: 'string', title: 'string', kind: 'string', source: 'string', format: 'string', status: 'string', error: 'text',\n hash: 'string', chars: 'unsigned', chunks: 'unsigned', createdAt: 'double', updatedAt: 'double',\n }, { primary: 'id', indexes: [['base']] })\n model.extend('knowledge_chunk', {\n id: 'string', base: 'string', doc: 'string', seq: 'unsigned', title: 'string', text: 'text', vector: 'binary',\n edited: 'boolean', meta: 'json', createdAt: 'double', updatedAt: 'double',\n }, { primary: 'id', indexes: [['base'], ['doc', 'seq']] })\n model.extend('knowledge_candidate', {\n id: 'unsigned', base: 'string', bot: 'string', channel: 'string', question: 'text', answer: 'text', status: 'string',\n chunk: 'string', createdAt: 'double', updatedAt: 'double',\n }, { autoInc: true, indexes: [['base', 'status']] })\n\n this.learnState = (await this.home.storage.get<Record<string, number>>(LEARN_STATE_KEY)) ?? {}\n this.usage = (await this.home.storage.get<typeof this.usage>(USAGE_KEY)) ?? { day: '', counts: {} }\n yield this.home.effect(() => () => this.abort.abort(), 'knowledge jobs')\n\n // Documents a restart interrupted start over.\n const unfinished = await this.db.get('knowledge_doc', { status: ['pending', 'parsing', 'embedding'] } as Query<KnowledgeDoc>)\n for (const doc of unfinished) this.enqueue(() => this.process(doc.id))\n\n yield* this.defineActions()\n yield* this.defineCommands()\n yield this.home.on('bots/message', (record) => this.onMessage(record))\n this.home.inject(['flows'], (ctx) => {\n ctx.flows.defineNode(searchNode(this))\n })\n this.home.inject(['agent'], (ctx) => {\n ctx.agent.defineSkill({\n id: 'answer-from-knowledge',\n name: '根据知识库回答',\n description: '先在知识库里查资料,再根据查到的内容回答,并说明出处',\n whenToUse: '问题和已经整理进知识库的资料有关(群规、产品说明、常见问题等)',\n instructions: [\n '1. 用 kb.list 看有哪些知识库;用户指定了就只查那个。',\n '2. 用 kb.search 搜索问题,必要时换几种说法再搜。',\n '3. 只根据命中的片段回答,在句末标出片段编号和标题;没查到就直接说知识库里没有,不要编。',\n '4. 发现资料过时或互相矛盾时,指出来,并建议用 kb.chunks.update 修改。',\n ].join('\\n'),\n tools: ['kb.list', 'kb.search', 'kb.chunks.list'],\n scenes: ['knowledge'],\n })\n })\n this.logger.info('knowledge bases are ready')\n }\n\n // ---- bases ----\n\n async list() {\n return this.db.get('knowledge_base', {}, { sort: { createdAt: 'asc' } })\n }\n\n async get(id: string) {\n const [base] = await this.db.get('knowledge_base', { id })\n if (!base) throw new MushinError('not_found', `Knowledge base \"${id}\" not found`)\n return base\n }\n\n /** Bases with their document, chunk and candidate counts, for the list page. */\n async summaries() {\n const bases = await this.list()\n const docs = await this.db.get('knowledge_doc', {})\n const pending = await this.db.get('knowledge_candidate', { status: 'pending' }, ['base'])\n return Promise.all(bases.map(async (base) => {\n const own = docs.filter((doc) => doc.base === base.id)\n const index = await this.index(base.id)\n return {\n ...base,\n docs: own.length,\n chunks: index.size,\n embedded: base.dimension ? index.embedded : 0,\n busy: own.some((doc) => doc.status !== 'ready' && doc.status !== 'error'),\n candidates: pending.filter((item) => item.base === base.id).length,\n }\n }))\n }\n\n /** Embeds a probe text to learn the model's vector length; fails with a readable error. */\n private async probe(provider: string, model: string) {\n try {\n const result = await this.home.llm.embed({ provider, model, texts: ['test'], signal: AbortSignal.timeout(30_000) })\n if (!result.dimension) throw new Error('the model returned an empty vector')\n return result.dimension\n } catch (error) {\n throw new MushinError('invalid_input', `The embedding model \"${model}\" did not work: ${errorText(error)}`)\n }\n }\n\n async create(input: z.infer<z.ZodObject<typeof baseFields>>) {\n const provider = input.provider ?? ''\n const model = provider ? input.model?.trim() ?? '' : ''\n if (provider && !model) throw new MushinError('invalid_input', 'Choose an embedding model, or no provider for keyword search only')\n const dimension = provider ? await this.probe(provider, model) : 0\n const now = Date.now()\n const base: KnowledgeBase = {\n id: randomId(6),\n name: input.name,\n description: input.description ?? '',\n provider,\n model,\n dimension,\n chunkSize: input.chunkSize ?? 500,\n chunkOverlap: input.chunkOverlap ?? 50,\n topK: input.topK ?? 5,\n minScore: input.minScore ?? 0.3,\n bots: input.bots ?? [],\n commands: { enabled: true, role: 'admin', ...input.commands },\n learn: { ...DEFAULT_LEARN, ...input.learn },\n createdAt: now,\n updatedAt: now,\n }\n await this.db.upsert('knowledge_base', [base])\n this.logger.info('created knowledge base %s%s', base.id, model ? ` with ${model}` : '')\n this.changed(base.id)\n return base\n }\n\n async update(id: string, patch: Partial<z.infer<z.ZodObject<typeof baseFields>>>) {\n const current = await this.get(id)\n const next: KnowledgeBase = {\n ...current,\n ...(patch.name !== undefined ? { name: patch.name } : {}),\n ...(patch.description !== undefined ? { description: patch.description } : {}),\n ...(patch.chunkSize !== undefined ? { chunkSize: patch.chunkSize } : {}),\n ...(patch.chunkOverlap !== undefined ? { chunkOverlap: patch.chunkOverlap } : {}),\n ...(patch.topK !== undefined ? { topK: patch.topK } : {}),\n ...(patch.minScore !== undefined ? { minScore: patch.minScore } : {}),\n ...(patch.bots !== undefined ? { bots: patch.bots } : {}),\n commands: { ...current.commands, ...patch.commands },\n learn: { ...DEFAULT_LEARN, ...current.learn, ...patch.learn },\n updatedAt: Date.now(),\n }\n const provider = patch.provider ?? current.provider\n const model = provider ? (patch.model ?? current.model).trim() : ''\n const modelChanged = provider !== current.provider || model !== current.model\n if (modelChanged) {\n if (provider && !model) throw new MushinError('invalid_input', 'Choose an embedding model, or no provider for keyword search only')\n next.provider = provider\n next.model = model\n // Old vectors do not fit the new model: searches use keywords until the base is embedded again.\n next.dimension = provider ? await this.probe(provider, model) : 0\n }\n await this.db.upsert('knowledge_base', [next])\n this.indexes.delete(id)\n this.changed(id)\n if (modelChanged) {\n this.logger.info('knowledge base %s now uses %s', next.id, model || 'keywords only')\n if (provider) void this.reindex(id)\n else await this.db.set('knowledge_chunk', { base: id }, { vector: new ArrayBuffer(0) })\n }\n return next\n }\n\n async remove(id: string) {\n const base = await this.get(id)\n await this.db.remove('knowledge_chunk', { base: id })\n await this.db.remove('knowledge_doc', { base: id })\n await this.db.remove('knowledge_candidate', { base: id })\n await this.db.remove('knowledge_base', { id })\n await rm(this.fileDir(id), { recursive: true, force: true })\n this.indexes.delete(id)\n this.logger.info('deleted knowledge base %s', base.id)\n this.changed(id)\n return { ok: true }\n }\n\n private changed(base: string) {\n this.home.emit('knowledge/changed', { base })\n }\n\n private progress(progress: KnowledgeProgress) {\n this.home.emit('knowledge/progress', progress)\n }\n\n // ---- documents ----\n\n private fileDir(base: string) {\n return join(this.home.storage.dir, 'knowledge', base)\n }\n\n private filePath(doc: Pick<KnowledgeDoc, 'base' | 'id' | 'format'>) {\n return join(this.fileDir(doc.base), `${doc.id}.${doc.format}`)\n }\n\n async docs(base: string) {\n await this.get(base)\n return this.db.get('knowledge_doc', { base }, { sort: { createdAt: 'asc' } })\n }\n\n async doc(id: string) {\n const [doc] = await this.db.get('knowledge_doc', { id })\n if (!doc) throw new MushinError('not_found', `Document \"${id}\" not found`)\n return doc\n }\n\n /**\n * Adds a document: a file (`data` in base64 with its `name`), pasted `text`, or a `url` to fetch.\n * It is parsed, split and embedded in the background; `knowledge/progress` reports how it goes.\n */\n async addDoc(input: { base: string; name?: string; text?: string; data?: string; url?: string }) {\n const base = await this.get(input.base)\n let kind: DocKind\n let format: DocFormat\n let bytes: Uint8Array\n let title = input.name?.trim() ?? ''\n let source = ''\n if (input.url) {\n const url = new URL(input.url)\n if (url.protocol !== 'http:' && url.protocol !== 'https:') throw new MushinError('invalid_input', 'Only http and https links can be added')\n kind = 'url'\n source = url.href\n const fetched = await this.fetchPage(url.href)\n format = fetched.format\n bytes = fetched.bytes\n title ||= fetched.title || url.hostname + url.pathname\n } else if (input.data !== undefined) {\n if (!title) throw new MushinError('invalid_input', 'Give the file\\'s name')\n const detected = formatOf(title)\n if (!detected) throw new MushinError('invalid_input', `Cannot read \"${title}\"; supported: ${FORMATS.join(', ')}`)\n kind = 'file'\n format = detected\n source = title\n bytes = Buffer.from(input.data, 'base64')\n title = title.replace(/\\.[^.]+$/, '')\n } else if (input.text !== undefined) {\n if (!input.text.trim()) throw new MushinError('invalid_input', 'The text is empty')\n kind = 'text'\n format = 'md'\n bytes = new TextEncoder().encode(input.text)\n title ||= input.text.trim().split('\\n')[0].replace(/^#+\\s*/, '').slice(0, 40)\n } else {\n throw new MushinError('invalid_input', 'Give a file (name and data), text, or a url')\n }\n if (bytes.byteLength > MAX_FILE_BYTES) throw new MushinError('invalid_input', `Files can be at most ${MAX_FILE_BYTES / 1024 / 1024} MB`)\n const hash = hashOf(bytes)\n const [same] = await this.db.get('knowledge_doc', { base: base.id, hash })\n if (same) throw new MushinError('conflict', `\"${same.title}\" in this base has the same content`)\n const now = Date.now()\n const doc: KnowledgeDoc = {\n id: randomId(6), base: base.id, title, kind, source, format, status: 'pending', error: '', hash, chars: 0, chunks: 0, createdAt: now, updatedAt: now,\n }\n await mkdir(this.fileDir(base.id), { recursive: true })\n await writeFile(this.filePath(doc), bytes)\n await this.db.upsert('knowledge_doc', [doc])\n this.logger.info('added \"%s\" to knowledge base %s', doc.title, base.id)\n this.changed(base.id)\n this.enqueue(() => this.process(doc.id))\n return doc\n }\n\n private async fetchPage(url: string) {\n let response: Response\n try {\n response = await fetch(url, { signal: AbortSignal.timeout(20_000), headers: { 'user-agent': 'Mushin.js knowledge base' } })\n } catch (error) {\n throw new MushinError('unavailable', `Could not fetch ${url}: ${errorText(error)}`)\n }\n if (!response.ok) throw new MushinError('unavailable', `${url} answered HTTP ${response.status}`)\n const bytes = new Uint8Array(await response.arrayBuffer())\n const type = response.headers.get('content-type') ?? ''\n const format: DocFormat = type.includes('pdf') ? 'pdf' : type.includes('html') || !type ? 'html' : formatOf(new URL(url).pathname) ?? 'txt'\n const title = format === 'html' ? htmlToText(new TextDecoder().decode(bytes)).title : undefined\n return { bytes, format, title }\n }\n\n async removeDoc(id: string) {\n const doc = await this.doc(id)\n await this.db.remove('knowledge_chunk', { doc: id })\n await this.db.remove('knowledge_doc', { id })\n await rm(this.filePath(doc), { force: true })\n this.indexes.delete(doc.base)\n this.changed(doc.base)\n return { ok: true }\n }\n\n /** Parses a document again from its file (a web page is fetched again); edits to its chunks are lost. */\n async reparse(id: string) {\n const doc = await this.doc(id)\n if (doc.kind === 'notes' || doc.kind === 'learned') throw new MushinError('invalid_input', 'This document is made of single entries and has nothing to parse')\n if (doc.kind === 'url') {\n const fetched = await this.fetchPage(doc.source)\n await writeFile(this.filePath({ ...doc, format: fetched.format }), fetched.bytes)\n doc.format = fetched.format\n doc.hash = hashOf(fetched.bytes)\n }\n await this.db.upsert('knowledge_doc', [{ ...doc, status: 'pending', error: '', updatedAt: Date.now() }])\n this.changed(doc.base)\n this.enqueue(() => this.process(id))\n return { ok: true }\n }\n\n private enqueue(task: () => Promise<unknown>) {\n this.queue = this.queue.catch(() => {}).then(() => (this.abort.signal.aborted ? undefined : task()))\n return this.queue\n }\n\n private async setStatus(doc: KnowledgeDoc, status: DocStatus, extra: Partial<KnowledgeDoc> = {}) {\n Object.assign(doc, { status, updatedAt: Date.now(), ...extra })\n await this.db.upsert('knowledge_doc', [doc])\n this.changed(doc.base)\n }\n\n /** Embeds texts with a base's model, `onBatch` after each batch; nothing for keyword-only bases. */\n private async embedTexts(base: KnowledgeBase, texts: string[], onBatch?: (done: number) => void) {\n if (!base.provider || !base.model || !texts.length) return undefined\n const vectors: Float32Array[] = []\n const BATCH = 32\n for (let index = 0; index < texts.length; index += BATCH) {\n const result = await this.home.llm.embed({ provider: base.provider, model: base.model, texts: texts.slice(index, index + BATCH), signal: this.abort.signal })\n vectors.push(...result.vectors.map(normalize))\n onBatch?.(Math.min(index + BATCH, texts.length))\n }\n return vectors\n }\n\n /** What a chunk's embedding reads: its title and text. */\n private embedText(chunk: { title: string; text: string }) {\n return chunk.title ? `${chunk.title}\\n${chunk.text}` : chunk.text\n }\n\n private async process(id: string) {\n const [doc] = await this.db.get('knowledge_doc', { id })\n if (!doc) return\n let base: KnowledgeBase\n try {\n base = await this.get(doc.base)\n } catch {\n return\n }\n try {\n await this.setStatus(doc, 'parsing')\n this.progress({ base: base.id, doc: id, stage: 'parsing', done: 0, total: 0 })\n const bytes = new Uint8Array(await readFile(this.filePath(doc)))\n const extracted = await extractText(doc.format as DocFormat, bytes)\n const text = extracted.text.slice(0, MAX_TEXT_CHARS)\n const pieces = chunkText(text, { size: base.chunkSize, overlap: base.chunkOverlap }, doc.title)\n if (!pieces.length) throw new MushinError('invalid_input', 'No text found in this document')\n await this.setStatus(doc, 'embedding', { chars: text.length })\n this.progress({ base: base.id, doc: id, stage: 'embedding', done: 0, total: pieces.length })\n const vectors = await this.embedTexts(base, pieces.map((piece) => this.embedText(piece)), (done) => {\n this.progress({ base: base.id, doc: id, stage: 'embedding', done, total: pieces.length })\n })\n const now = Date.now()\n const rows: ChunkRow[] = pieces.map((piece, seq) => ({\n id: randomId(8), base: base.id, doc: id, seq, title: piece.title, text: piece.text, vector: vectorBytes(vectors?.[seq]),\n edited: false, meta: {}, createdAt: now, updatedAt: now,\n }))\n await this.db.remove('knowledge_chunk', { doc: id })\n await this.db.upsert('knowledge_chunk', rows)\n this.indexes.delete(base.id)\n await this.setStatus(doc, 'ready', { chunks: rows.length, error: '' })\n this.progress({ base: base.id, doc: id, stage: 'done', done: rows.length, total: rows.length })\n this.logger.info('\"%s\" is ready: %d chunk(s)', doc.title, rows.length)\n } catch (error) {\n if (this.abort.signal.aborted) return\n this.logger.warn('reading \"%s\" failed: %s', doc.title, errorText(error))\n await this.setStatus(doc, 'error', { error: errorText(error) }).catch(() => {})\n this.progress({ base: base.id, doc: id, stage: 'error', done: 0, total: 0 })\n }\n }\n\n /** Embeds every chunk of a base again with its current model, e.g. after the model changed. */\n async reindex(id: string) {\n const base = await this.get(id)\n if (!base.provider) throw new MushinError('invalid_input', 'This base has no embedding model; it searches by keywords only')\n return this.enqueue(async () => {\n const rows = await this.db.get('knowledge_chunk', { base: id })\n this.progress({ base: id, stage: 'reindex', done: 0, total: rows.length })\n try {\n const vectors = await this.embedTexts(base, rows.map((row) => this.embedText(row)), (done) => {\n this.progress({ base: id, stage: 'reindex', done, total: rows.length })\n }) ?? []\n await this.db.upsert('knowledge_chunk', rows.map((row, index) => ({ ...row, vector: vectorBytes(vectors[index]) })))\n this.logger.info('embedded %d chunk(s) of %s again', rows.length, base.id)\n } catch (error) {\n if (!this.abort.signal.aborted) this.logger.warn('embedding %s again failed: %s', base.id, errorText(error))\n }\n this.indexes.delete(id)\n this.progress({ base: id, stage: 'done', done: rows.length, total: rows.length })\n this.changed(id)\n })\n }\n\n // ---- chunks ----\n\n async chunks(input: { base: string; doc?: string; offset?: number; limit?: number; query?: string }) {\n await this.get(input.base)\n const rows = await this.db.get('knowledge_chunk', input.doc ? { base: input.base, doc: input.doc } : { base: input.base }, { sort: { doc: 'asc', seq: 'asc' } })\n const query = input.query?.trim().toLowerCase()\n const matched = query ? rows.filter((row) => row.text.toLowerCase().includes(query) || row.title.toLowerCase().includes(query)) : rows\n const offset = input.offset ?? 0\n return { total: matched.length, chunks: matched.slice(offset, offset + (input.limit ?? 50)).map(publicChunk) }\n }\n\n private async chunkRow(id: string) {\n const [row] = await this.db.get('knowledge_chunk', { id })\n if (!row) throw new MushinError('not_found', `Chunk \"${id}\" not found`)\n return row\n }\n\n async updateChunk(id: string, patch: { title?: string; text?: string }) {\n const row = await this.chunkRow(id)\n const base = await this.get(row.base)\n const next = { ...row, ...(patch.title !== undefined ? { title: patch.title } : {}), ...(patch.text !== undefined ? { text: patch.text } : {}), edited: true, updatedAt: Date.now() }\n if (!next.text.trim()) throw new MushinError('invalid_input', 'A chunk needs text; remove it instead')\n const [vector] = await this.embedTexts(base, [this.embedText(next)]) ?? []\n next.vector = vectorBytes(vector)\n await this.db.upsert('knowledge_chunk', [next])\n this.indexes.delete(row.base)\n this.changed(row.base)\n return publicChunk(next)\n }\n\n async removeChunk(id: string) {\n const row = await this.chunkRow(id)\n await this.db.remove('knowledge_chunk', { id })\n await this.recount(row.doc)\n this.indexes.delete(row.base)\n this.changed(row.base)\n return { ok: true }\n }\n\n /** Joins a chunk with the one after it in the same document. */\n async mergeChunk(id: string) {\n const row = await this.chunkRow(id)\n const [after] = await this.db.get('knowledge_chunk', { doc: row.doc, seq: { $gt: row.seq } }, { sort: { seq: 'asc' }, limit: 1 })\n if (!after) throw new MushinError('invalid_input', 'This is the last chunk of its document')\n await this.db.remove('knowledge_chunk', { id: after.id })\n const merged = await this.updateChunk(id, { text: `${row.text}\\n\\n${after.text}` })\n await this.recount(row.doc)\n return merged\n }\n\n private async recount(docId: string) {\n const [doc] = await this.db.get('knowledge_doc', { id: docId })\n if (!doc) return\n const count = (await this.db.get('knowledge_chunk', { doc: docId }, ['id'])).length\n await this.db.upsert('knowledge_doc', [{ ...doc, chunks: count, updatedAt: Date.now() }])\n }\n\n /** The document of single entries of a kind, made the first time one is added. */\n private async entriesDoc(base: KnowledgeBase, kind: 'notes' | 'learned') {\n const [found] = await this.db.get('knowledge_doc', { base: base.id, kind })\n if (found) return found\n const now = Date.now()\n const doc: KnowledgeDoc = {\n id: randomId(6), base: base.id, title: kind === 'notes' ? NOTES_TITLE : LEARNED_TITLE, kind, source: '', format: 'md',\n status: 'ready', error: '', hash: `${kind}:${base.id}`, chars: 0, chunks: 0, createdAt: now, updatedAt: now,\n }\n await this.db.upsert('knowledge_doc', [doc])\n return doc\n }\n\n /** Adds one entry, such as a `/记住` note or an accepted candidate, as a chunk of its own. */\n async addEntry(baseId: string, entry: { text: string; title?: string; kind?: 'notes' | 'learned'; meta?: Record<string, any> }) {\n const base = await this.get(baseId)\n const text = entry.text.trim()\n if (!text) throw new MushinError('invalid_input', 'The entry is empty')\n const doc = await this.entriesDoc(base, entry.kind ?? 'notes')\n const [last] = await this.db.get('knowledge_chunk', { doc: doc.id }, { sort: { seq: 'desc' }, limit: 1 })\n const title = entry.title?.trim() ?? ''\n const [vector] = await this.embedTexts(base, [this.embedText({ title, text })]) ?? []\n const now = Date.now()\n const row: ChunkRow = {\n id: randomId(8), base: base.id, doc: doc.id, seq: (last?.seq ?? -1) + 1, title, text, vector: vectorBytes(vector),\n edited: false, meta: entry.meta ?? {}, createdAt: now, updatedAt: now,\n }\n await this.db.upsert('knowledge_chunk', [row])\n await this.recount(doc.id)\n this.indexes.delete(base.id)\n this.changed(base.id)\n return publicChunk(row)\n }\n\n /** Removes notes that contain the text; returns how many. */\n async forget(baseId: string, text: string) {\n const wanted = text.trim().toLowerCase()\n if (!wanted) return 0\n const docs = await this.db.get('knowledge_doc', { base: baseId, kind: 'notes' })\n let removed = 0\n for (const doc of docs) {\n const rows = await this.db.get('knowledge_chunk', { doc: doc.id })\n const matching = rows.filter((row) => row.text.toLowerCase().includes(wanted))\n if (!matching.length) continue\n await this.db.remove('knowledge_chunk', { id: matching.map((row) => row.id) })\n await this.recount(doc.id)\n removed += matching.length\n }\n if (removed) {\n this.indexes.delete(baseId)\n this.changed(baseId)\n }\n return removed\n }\n\n // ---- search ----\n\n private async index(base: string) {\n let index = this.indexes.get(base)\n if (index) return index\n const [record] = await this.db.get('knowledge_base', { id: base })\n const rows = await this.db.get('knowledge_chunk', { base })\n index = new SearchIndex(rows.map((row): IndexedChunk => {\n const vector = vectorOf(row.vector)\n return { id: row.id, doc: row.doc, title: row.title, text: row.text, ...(vector && record?.dimension === vector.length ? { vector } : {}) }\n }))\n this.indexes.set(base, index)\n return index\n }\n\n /**\n * Searches bases (every one when none are given) by keywords and, where the base has an\n * embedding model, by meaning. A failing embedding model falls back to keywords with a warning.\n */\n async search(input: { bases?: string[]; query: string; topK?: number; minScore?: number; signal?: AbortSignal }): Promise<KnowledgeSearch> {\n const all = await this.list()\n const bases = input.bases?.length ? input.bases.map((id) => {\n const found = all.find((base) => base.id === id || base.name === id)\n if (!found) throw new MushinError('not_found', `Knowledge base \"${id}\" not found`)\n return found\n }) : all\n const query = input.query.trim()\n const warnings: string[] = []\n if (!query || !bases.length) return { hits: [], semantic: false, warnings }\n const vectors = new Map<string, Float32Array | undefined>()\n let semantic = false\n const hits: KnowledgeHit[] = []\n const docTitles = new Map<string, string>()\n for (const base of bases) {\n let vector: Float32Array | undefined\n if (base.provider && base.model && base.dimension) {\n const key = `${base.provider}|${base.model}`\n if (!vectors.has(key)) {\n try {\n const result = await this.home.llm.embed({ provider: base.provider, model: base.model, texts: [query], signal: input.signal ?? AbortSignal.timeout(20_000) })\n vectors.set(key, normalize(result.vectors[0]))\n } catch (error) {\n if (input.signal?.aborted) throw error\n vectors.set(key, undefined)\n warnings.push(`${base.name}: the embedding model failed (${errorText(error)}); searched by keywords only`)\n }\n }\n vector = vectors.get(key)\n if (vector) semantic = true\n }\n const index = await this.index(base.id)\n for (const hit of index.search(query, vector, { topK: input.topK ?? base.topK, minScore: input.minScore ?? base.minScore })) {\n if (!docTitles.has(hit.doc)) docTitles.set(hit.doc, (await this.db.get('knowledge_doc', { id: hit.doc }, ['title']))[0]?.title ?? '')\n hits.push({ ...hit, base: base.id, baseName: base.name, docTitle: docTitles.get(hit.doc)! })\n }\n }\n const topK = input.topK ?? Math.max(...bases.map((base) => base.topK))\n hits.sort((a, b) => b.fused - a.fused || b.score - a.score)\n return { hits: hits.slice(0, topK), semantic, warnings }\n }\n\n /** Chunks and links between similar ones, for the similarity map. */\n async graph(input: { base: string; neighbors?: number; minSimilarity?: number; limit?: number }) {\n const base = await this.get(input.base)\n const index = await this.index(base.id)\n const { nodes, edges, byVector } = index.links(input)\n const docs = new Map((await this.db.get('knowledge_doc', { base: base.id })).map((doc) => [doc.id, doc.title]))\n return {\n byVector,\n total: index.size,\n nodes: nodes.map((chunk) => ({ id: chunk.id, doc: chunk.doc, docTitle: docs.get(chunk.doc) ?? '', title: chunk.title, text: chunk.text.slice(0, 300) })),\n edges,\n }\n }\n\n // ---- flows ----\n\n /** Flows that ask an LLM, for \"use in a flow\", with whether they already search this base. */\n flowChoices(base?: string) {\n const flows = this.home.get('flows')\n if (!flows) return []\n return flows.list().flatMap((summary: { id: string }) => {\n const doc = flows.get(summary.id)\n if (!doc.nodes.some((node) => node.type === 'ai.chat')) return []\n const search = doc.nodes.find((node) => node.type === 'kb.search')\n return [{\n id: doc.id,\n name: doc.name,\n bots: doc.bots,\n llmChat: doc.source?.kind === 'plugin' && doc.source.plugin === '@mushinjs/bot-plugin-llm-chat',\n uses: !!search && (!base || ((search.config.bases as string[] | undefined) ?? []).includes(base)),\n }]\n })\n }\n\n /**\n * Puts a knowledge search in front of a flow's first \"Ask the LLM\" node and passes its passages\n * to it; a flow that already has a search node gets these bases added to it.\n */\n async useInFlow(flowId: string, bases: string[]) {\n const flows = this.home.get('flows')\n if (!flows) throw new MushinError('unavailable', 'Flows are not running')\n for (const id of bases) await this.get(id)\n const doc = flows.get(flowId)\n const existing = doc.nodes.find((node) => node.type === 'kb.search')\n if (existing) {\n const merged = [...new Set([...((existing.config.bases as string[] | undefined) ?? []), ...bases])]\n const nodes = doc.nodes.map((node) => (node === existing ? { ...node, config: { ...node.config, bases: merged } } : node))\n await flows.update(flowId, { nodes })\n return { flow: flowId, node: existing.id, added: false }\n }\n const chat = doc.nodes.find((node) => node.type === 'ai.chat')\n if (!chat) throw new MushinError('invalid_input', `\"${doc.name}\" has no \"Ask the LLM\" node`)\n let id = 'kb'\n for (let n = 2; doc.nodes.some((node) => node.id === id); n++) id = `kb${n}`\n const shift = 280\n const search: FlowNode = {\n id,\n type: 'kb.search',\n label: '知识库检索',\n position: { ...chat.position },\n config: { bases, topK: 3, minScore: 0.3 },\n }\n const nodes = [\n ...doc.nodes.map((node) => {\n const moved = node.position.x >= chat.position.x ? { ...node, position: { x: node.position.x + shift, y: node.position.y } } : node\n return node === chat ? { ...moved, inputs: { ...chat.inputs, knowledge: `{{${id}.context}}` } } : moved\n }),\n search,\n ]\n const edges = [\n ...doc.edges.map((edge) => (edge.target === chat.id && edge.targetHandle === 'exec' ? { ...edge, target: id } : edge)),\n { source: id, sourceHandle: 'exec:next', target: chat.id, targetHandle: 'exec' },\n { source: id, sourceHandle: 'exec:empty', target: chat.id, targetHandle: 'exec' },\n ]\n await flows.update(flowId, { nodes, edges })\n this.logger.info('flow %s now searches knowledge before asking the LLM', doc.id)\n return { flow: flowId, node: id, added: true }\n }\n\n // ---- chat commands ----\n\n /** Bases a bot may add to from chat, oldest first. */\n private async basesFor(botId: string) {\n return (await this.list()).filter((base) => base.commands.enabled && base.bots.includes(botId))\n }\n\n private *defineCommands() {\n const bots = this.home.bots\n const hasBases = new Set<string>()\n const refresh = async () => {\n hasBases.clear()\n for (const base of await this.list()) if (base.commands.enabled) for (const bot of base.bots) hasBases.add(bot)\n }\n void refresh()\n yield this.home.on('knowledge/changed', () => void refresh())\n const only = { bots: (botId: string) => hasBases.has(botId) }\n\n yield bots.defineCommand({ name: '记住', aliases: ['remember'], description: '把一句话记进知识库', args: [{ name: '内容', type: 'text', rest: true }] }, async (session) => {\n const base = await this.allowed(session)\n if (!base) return\n const text = session.command?.rest.trim() ?? ''\n if (!text) return session.send('用法:/记住 要记住的内容')\n await this.addEntry(base.id, { text, meta: this.metaOf(session) })\n await session.send(`已记进知识库「${base.name}」`)\n }, only)\n\n yield bots.defineCommand({ name: '收录', aliases: ['collect'], description: '回复一条消息,把它收进知识库' }, async (session) => {\n const base = await this.allowed(session)\n if (!base) return\n const quoted = session.segments.find((segment) => segment.type === 'reply')?.data?.id\n if (!quoted) return session.send('请回复要收录的那条消息,再发 /收录')\n const messages = await bots.messages(session.bot.id, { channel: session.channel.id, limit: 500 })\n const message = messages.find((record) => record.messageId === String(quoted))\n const text = message ? message.segments.filter((segment) => segment.type === 'text').map((segment) => segment.data.text).join('').trim() : ''\n if (!text) return session.send('没找到那条消息的文字内容(可能太早,或者不是文字消息)')\n await this.addEntry(base.id, { text, meta: { ...this.metaOf(session), from: message!.user.id, fromName: message!.user.name ?? '' } })\n await session.send(`已收录到知识库「${base.name}」`)\n }, only)\n\n yield bots.defineCommand({ name: '忘记', aliases: ['forget'], description: '从知识库删除记住的内容', args: [{ name: '内容', type: 'text', rest: true }] }, async (session) => {\n const base = await this.allowed(session)\n if (!base) return\n const text = session.command?.rest.trim() ?? ''\n if (!text) return session.send('用法:/忘记 要删除的内容(会删除包含这段文字的记录)')\n const removed = await this.forget(base.id, text)\n await session.send(removed ? `已从知识库「${base.name}」删除 ${removed} 条` : `知识库「${base.name}」里没有用 /记住 或 /收录 记下的、包含“${text}”的内容`)\n }, only)\n }\n\n private metaOf(session: Session) {\n return { bot: session.bot.id, channel: `${session.channel.type}:${session.channel.id}`, user: session.user.id, userName: session.user.name ?? '' }\n }\n\n /** The base a chat command writes to, after checking the sender may use it; tells them when not. */\n private async allowed(session: Session) {\n const [base] = await this.basesFor(session.bot.id)\n if (!base) return\n if (!roleAtLeast(session.role() ?? 'user', base.commands.role)) {\n await session.send(base.commands.role === 'master' ? '只有主人可以修改知识库' : '需要管理员及以上才能修改知识库')\n return\n }\n return base\n }\n\n // ---- learning ----\n\n private onMessage(record: MessageRecord) {\n if (!record.segments.some((segment) => segment.type === 'text')) return\n void this.list().then((bases) => {\n const chat = `${record.channel.type}:${record.channel.id}`\n for (const base of bases) {\n const learn = { ...DEFAULT_LEARN, ...base.learn }\n if (!learn.enabled || !base.bots.includes(record.botId)) continue\n if (learn.channels.length && !learn.channels.includes(chat)) continue\n const key = `${base.id}|${record.botId}|${chat}`\n const count = (this.counters.get(key) ?? 0) + 1\n if (count < learn.every) {\n this.counters.set(key, count)\n continue\n }\n this.counters.delete(key)\n void this.learn(base.id, record.botId, chat).catch((error) => this.logger.warn('learning from %s failed: %s', chat, errorText(error)))\n }\n }, () => {})\n }\n\n private async useQuota(base: KnowledgeBase) {\n const today = localDay()\n if (this.usage.day !== today) this.usage = { day: today, counts: {} }\n const used = this.usage.counts[base.id] ?? 0\n if (used >= base.learn.dailyLimit) return false\n this.usage.counts[base.id] = used + 1\n await this.home.storage.set(USAGE_KEY, this.usage)\n return true\n }\n\n /**\n * Reads a chat's messages since it was last learned from and asks the LLM for knowledge worth\n * keeping. What it finds waits for review, or is added straight away with auto-accept.\n */\n async learn(baseId: string, botId: string, chat: string): Promise<{ candidates: KnowledgeCandidate[]; skipped?: string }> {\n const base = await this.get(baseId)\n const learn = { ...DEFAULT_LEARN, ...base.learn }\n const key = `${baseId}|${botId}|${chat}`\n if (this.learning.has(key)) return { candidates: [], skipped: 'already learning from this chat' }\n const bot = this.home.bots.get(botId)\n const [type, ...rest] = chat.split(':')\n const channelId = rest.join(':')\n const since = this.learnState[key] ?? 0\n const lines = (await this.home.bots.messages(botId, { channel: channelId, limit: Math.max(learn.every, 50) }))\n .filter((record) => record.channel.type === type && record.timestamp > since)\n const text = lines.map((record) => ({\n direction: record.direction,\n user: record.user,\n text: record.segments.filter((segment) => segment.type === 'text').map((segment) => segment.data.text).join(''),\n }))\n if (text.filter((line) => line.text.trim()).length < LEARN_MIN_LINES) return { candidates: [], skipped: 'not enough new messages' }\n if (!(await this.useQuota(base))) return { candidates: [], skipped: 'the daily learning limit is used up' }\n this.learning.add(key)\n try {\n const result = await this.home.llm.chat({\n provider: learn.provider || bot.provider || undefined,\n model: learn.model || undefined,\n maxTokens: 2000,\n signal: AbortSignal.any([this.abort.signal, AbortSignal.timeout(90_000)]),\n messages: [\n { role: 'system', content: LEARN_PROMPT },\n { role: 'user', content: transcript(text, bot.name) },\n ],\n })\n this.learnState[key] = Math.max(...lines.map((record) => record.timestamp))\n await this.home.storage.set(LEARN_STATE_KEY, this.learnState)\n const found = await this.newCandidates(base, parseCandidates(result.text))\n const now = Date.now()\n const created: KnowledgeCandidate[] = []\n for (const candidate of found) {\n const row = await this.db.create('knowledge_candidate', {\n base: baseId, bot: botId, channel: chat, question: candidate.question, answer: candidate.answer, status: 'pending', chunk: '', createdAt: now, updatedAt: now,\n })\n created.push(learn.autoAccept ? await this.accept(row.id) : row)\n }\n this.logger.info('learned %d item(s) for \"%s\" from %s', created.length, base.name, chat)\n if (created.length) this.changed(baseId)\n return { candidates: created }\n } finally {\n this.learning.delete(key)\n }\n }\n\n /** Leaves out candidates already waiting for review or already known to the base. */\n private async newCandidates(base: KnowledgeBase, found: Candidate[]) {\n const pending = new Set((await this.db.get('knowledge_candidate', { base: base.id, status: 'pending' }, ['question'])).map((row) => row.question))\n const fresh: Candidate[] = []\n for (const candidate of found) {\n if (pending.has(candidate.question)) continue\n const { hits } = await this.search({ bases: [base.id], query: candidate.question, topK: 1, minScore: 0.9 }).catch(() => ({ hits: [] }))\n if (hits.length) continue\n fresh.push(candidate)\n }\n return fresh\n }\n\n async candidates(base: string, status?: CandidateStatus) {\n await this.get(base)\n return this.db.get('knowledge_candidate', status ? { base, status } : { base }, { sort: { createdAt: 'desc' } })\n }\n\n private async candidate(id: number) {\n const [row] = await this.db.get('knowledge_candidate', { id })\n if (!row) throw new MushinError('not_found', `Candidate ${id} not found`)\n return row\n }\n\n /** Adds a candidate (as edited) to the base's \"learned from chats\" document. */\n async accept(id: number, patch: { question?: string; answer?: string } = {}) {\n const row = await this.candidate(id)\n if (row.status === 'accepted') return row\n const question = patch.question?.trim() || row.question\n const answer = patch.answer?.trim() || row.answer\n const chunk = await this.addEntry(row.base, { kind: 'learned', title: question, text: candidateText({ question, answer }), meta: { bot: row.bot, channel: row.channel, candidate: row.id } })\n const next: CandidateRow = { ...row, question, answer, status: 'accepted', chunk: chunk.id, updatedAt: Date.now() }\n await this.db.upsert('knowledge_candidate', [next])\n this.changed(row.base)\n return next\n }\n\n async reject(id: number) {\n const row = await this.candidate(id)\n const next: CandidateRow = { ...row, status: 'rejected', updatedAt: Date.now() }\n await this.db.upsert('knowledge_candidate', [next])\n this.changed(row.base)\n return next\n }\n\n // ---- actions ----\n\n private *defineActions() {\n const actions = this.home.actions\n const baseId = z.object({ base: z.string().describe('Knowledge base id from kb.list') })\n\n yield actions.define({\n name: 'kb.list',\n description: 'List knowledge bases: name, embedding model (none means keyword search only), document, chunk and pending candidate counts, linked bots and learning settings.',\n risk: 'read',\n input: z.object({}),\n handler: () => this.summaries(),\n })\n\n yield actions.define({\n name: 'kb.choices',\n description: 'Knowledge base ids and names, for pickers.',\n risk: 'read',\n input: z.object({}),\n handler: async () => (await this.list()).map((base) => ({ value: base.id, label: base.name })),\n })\n\n yield actions.define({\n name: 'kb.create',\n description: 'Create a knowledge base. With an embedding provider and model (providers.embedChoices) it searches by meaning as well as keywords; the model is tried once. Link bots to let their admins add knowledge from chat (/记住, /收录, /忘记) and to learn from their chats.',\n risk: 'write',\n input: z.object(baseFields),\n handler: (input) => this.create(input),\n })\n\n yield actions.define({\n name: 'kb.update',\n description: 'Change a knowledge base. Changing the embedding model embeds every chunk again in the background. Lists given replace the old ones.',\n risk: 'write',\n input: z.object({ id: z.string() }).extend(z.object(baseFields).partial().shape),\n handler: ({ id, ...patch }) => this.update(id, patch),\n })\n\n yield actions.define({\n name: 'kb.delete',\n description: 'Delete a knowledge base with its documents, chunks and candidates.',\n risk: 'dangerous',\n input: z.object({ id: z.string() }),\n handler: ({ id }) => this.remove(id),\n })\n\n yield actions.define({\n name: 'kb.docs.list',\n description: 'Documents of a knowledge base with their status (pending, parsing, embedding, ready, error), size and chunk count.',\n risk: 'read',\n input: baseId,\n handler: ({ base }) => this.docs(base),\n })\n\n yield actions.define({\n name: 'kb.docs.add',\n description: 'Add a document to a knowledge base: pasted text (Markdown works best: headings become chunk titles), a web page url, or a file as base64 data with its name (md, txt, html; pdf and docx need optional packages). It is split and embedded in the background.',\n risk: 'write',\n input: baseId.extend({\n name: z.string().optional().describe('Title, or the file name for data'),\n text: z.string().optional(),\n url: z.string().optional(),\n data: z.string().optional().describe('File content in base64'),\n }),\n handler: (input) => this.addDoc(input),\n })\n\n yield actions.define({\n name: 'kb.docs.remove',\n description: 'Remove a document and its chunks from its knowledge base.',\n risk: 'dangerous',\n input: z.object({ id: z.string().describe('Document id') }),\n handler: ({ id }) => this.removeDoc(id),\n })\n\n yield actions.define({\n name: 'kb.docs.reparse',\n description: 'Split a document again from its file (a web page is fetched again). Edits to its chunks are lost.',\n risk: 'write',\n input: z.object({ id: z.string() }),\n handler: ({ id }) => this.reparse(id),\n })\n\n yield actions.define({\n name: 'kb.chunks.list',\n description: 'Chunks of a knowledge base (or one document), in order, optionally only those containing some text.',\n risk: 'read',\n input: baseId.extend({\n doc: z.string().optional(),\n query: z.string().optional().describe('Only chunks containing this text'),\n offset: z.number().int().min(0).optional(),\n limit: z.number().int().min(1).max(200).optional(),\n }),\n handler: (input) => this.chunks(input),\n })\n\n yield actions.define({\n name: 'kb.chunks.add',\n description: 'Add one entry to a knowledge base (it goes in its \"随手记\" document), as /记住 does in chat.',\n risk: 'write',\n input: baseId.extend({ text: z.string().min(1), title: z.string().optional() }),\n handler: ({ base, text, title }) => this.addEntry(base, { text, title, meta: { by: 'dashboard' } }),\n })\n\n yield actions.define({\n name: 'kb.chunks.update',\n description: 'Change a chunk\\'s title or text; it is embedded again.',\n risk: 'write',\n input: z.object({ id: z.string(), title: z.string().optional(), text: z.string().optional() }),\n handler: ({ id, ...patch }) => this.updateChunk(id, patch),\n })\n\n yield actions.define({\n name: 'kb.chunks.remove',\n description: 'Remove one chunk.',\n risk: 'write',\n input: z.object({ id: z.string() }),\n handler: ({ id }) => this.removeChunk(id),\n })\n\n yield actions.define({\n name: 'kb.chunks.merge',\n description: 'Join a chunk with the next one of its document.',\n risk: 'write',\n input: z.object({ id: z.string() }),\n handler: ({ id }) => this.mergeChunk(id),\n })\n\n yield actions.define({\n name: 'kb.search',\n description: 'Search knowledge bases (all when none are given) for passages about a question. Each hit has its text, title, document, a score from 0 to 1 (the better of meaning similarity and the share of the question\\'s words it contains) and its ranks by keywords and by meaning. Answer from the hits and cite them; say so when nothing relevant is found.',\n risk: 'read',\n input: z.object({\n query: z.string().min(1),\n bases: z.array(z.string()).optional().describe('Base ids or names'),\n topK: z.number().int().min(1).max(20).optional(),\n minScore: z.number().min(0).max(1).optional(),\n }),\n handler: (input) => this.search(input),\n })\n\n yield actions.define({\n name: 'kb.reindex',\n description: 'Embed every chunk of a knowledge base again with its model, in the background.',\n risk: 'write',\n input: z.object({ id: z.string() }),\n handler: async ({ id }) => {\n void this.reindex(id)\n return { ok: true }\n },\n })\n\n yield actions.define({\n name: 'kb.graph',\n description: 'Chunks of a knowledge base and links between similar ones (by embeddings, or shared words without them), for the similarity map.',\n risk: 'read',\n input: baseId.extend({\n neighbors: z.number().int().min(1).max(10).optional(),\n minSimilarity: z.number().min(0).max(1).optional(),\n limit: z.number().int().min(10).max(2000).optional(),\n }),\n handler: (input) => this.graph(input),\n })\n\n yield actions.define({\n name: 'kb.flowChoices',\n description: 'Flows with an \"Ask the LLM\" node, which kb.useInFlow can add a knowledge search to; says which already search the base.',\n risk: 'read',\n input: z.object({ base: z.string().optional() }),\n handler: ({ base }) => this.flowChoices(base),\n })\n\n yield actions.define({\n name: 'kb.useInFlow',\n description: 'Make a flow search knowledge bases before its first \"Ask the LLM\" node and give it the passages found (a bot\\'s LLM chat flow, for example). A flow that already has a knowledge search node gets the bases added.',\n risk: 'write',\n input: z.object({ flow: z.string(), bases: z.array(z.string()).min(1) }),\n handler: ({ flow, bases }) => this.useInFlow(flow, bases),\n })\n\n yield actions.define({\n name: 'kb.candidates.list',\n description: 'Knowledge learned from chats, newest first: pending ones wait for review.',\n risk: 'read',\n input: baseId.extend({ status: z.enum(['pending', 'accepted', 'rejected']).optional() }),\n handler: ({ base, status }) => this.candidates(base, status),\n })\n\n yield actions.define({\n name: 'kb.candidates.accept',\n description: 'Add a learned candidate to its knowledge base, optionally with an edited question or answer.',\n risk: 'write',\n input: z.object({ id: z.number().int(), question: z.string().optional(), answer: z.string().optional() }),\n handler: ({ id, ...patch }) => this.accept(id, patch),\n })\n\n yield actions.define({\n name: 'kb.candidates.reject',\n description: 'Ignore a learned candidate.',\n risk: 'write',\n input: z.object({ id: z.number().int() }),\n handler: ({ id }) => this.reject(id),\n })\n\n yield actions.define({\n name: 'kb.learn',\n description: 'Learn from one chat now instead of waiting for enough messages: reads its messages since the last time and asks the LLM for knowledge worth keeping. Costs one LLM request.',\n risk: 'write',\n input: baseId.extend({ bot: z.string(), chat: z.string().regex(/^(group|private):\\S+$/).describe('\"group:<id>\" or \"private:<user id>\"') }),\n handler: ({ base, bot, chat }) => this.learn(base, bot, chat),\n })\n }\n}\n\nexport default Knowledge\n"],"mappings":"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"}
@@ -0,0 +1,20 @@
1
+ /** Turning a stretch of chat into candidate knowledge: the prompt, and reading the model's answer. */
2
+ export interface ChatLineLike {
3
+ direction: 'in' | 'out';
4
+ user: {
5
+ id: string;
6
+ name?: string;
7
+ };
8
+ text: string;
9
+ }
10
+ export interface Candidate {
11
+ question: string;
12
+ answer: string;
13
+ }
14
+ export declare const LEARN_PROMPT: string;
15
+ /** The chat as the model reads it: one `name: text` line per message. */
16
+ export declare function transcript(lines: ChatLineLike[], botName?: string): string;
17
+ /** Items from the model's answer: a JSON array, possibly in a code fence or with text around it. */
18
+ export declare function parseCandidates(answer: string): Candidate[];
19
+ /** A candidate as a knowledge chunk. */
20
+ export declare function candidateText(candidate: Candidate): string;
package/dist/learn.js ADDED
@@ -0,0 +1,43 @@
1
+ // src/learn.ts
2
+ const LEARN_PROMPT = [
3
+ "You read a stretch of a group or private chat and pick out knowledge worth keeping in the chat's knowledge base:",
4
+ "facts, rules, how-tos, answers to questions, decisions and recurring information that will help answer later questions.",
5
+ "Leave out small talk, jokes, opinions, guesses, anything still unsettled or contradicted later, and personal or private details about people.",
6
+ "Write each item as a question someone might ask and a short self-contained answer, in the language of the chat.",
7
+ "Answer with a JSON array only, like [{\"question\": \"...\", \"answer\": \"...\"}]; answer [] when there is nothing worth keeping."
8
+ ].join(" ");
9
+ /** The chat as the model reads it: one `name: text` line per message. */
10
+ function transcript(lines, botName = "bot") {
11
+ return lines.filter((line) => line.text.trim()).map((line) => `${line.direction === "out" ? botName : line.user.name || line.user.id}: ${line.text.replace(/\s+/g, " ").trim()}`).join("\n");
12
+ }
13
+ /** Items from the model's answer: a JSON array, possibly in a code fence or with text around it. */
14
+ function parseCandidates(answer) {
15
+ const start = answer.indexOf("[");
16
+ const end = answer.lastIndexOf("]");
17
+ if (start < 0 || end <= start) return [];
18
+ let items;
19
+ try {
20
+ items = JSON.parse(answer.slice(start, end + 1));
21
+ } catch {
22
+ return [];
23
+ }
24
+ if (!Array.isArray(items)) return [];
25
+ const seen = /* @__PURE__ */ new Set();
26
+ return items.flatMap((item) => {
27
+ const question = typeof item?.question === "string" ? item.question.trim() : "";
28
+ const text = typeof item?.answer === "string" ? item.answer.trim() : "";
29
+ if (!question || !text || seen.has(question)) return [];
30
+ seen.add(question);
31
+ return [{
32
+ question: question.slice(0, 300),
33
+ answer: text.slice(0, 2e3)
34
+ }];
35
+ });
36
+ }
37
+ /** A candidate as a knowledge chunk. */
38
+ function candidateText(candidate) {
39
+ return `问:${candidate.question}\n答:${candidate.answer}`;
40
+ }
41
+ export { LEARN_PROMPT, candidateText, parseCandidates, transcript };
42
+
43
+ //# sourceMappingURL=learn.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"learn.js","names":[],"sources":["../src/learn.ts"],"sourcesContent":["/** Turning a stretch of chat into candidate knowledge: the prompt, and reading the model's answer. */\n\nexport interface ChatLineLike {\n direction: 'in' | 'out'\n user: { id: string; name?: string }\n text: string\n}\n\nexport interface Candidate {\n question: string\n answer: string\n}\n\nexport const LEARN_PROMPT = [\n 'You read a stretch of a group or private chat and pick out knowledge worth keeping in the chat\\'s knowledge base:',\n 'facts, rules, how-tos, answers to questions, decisions and recurring information that will help answer later questions.',\n 'Leave out small talk, jokes, opinions, guesses, anything still unsettled or contradicted later, and personal or private details about people.',\n 'Write each item as a question someone might ask and a short self-contained answer, in the language of the chat.',\n 'Answer with a JSON array only, like [{\"question\": \"...\", \"answer\": \"...\"}]; answer [] when there is nothing worth keeping.',\n].join(' ')\n\n/** The chat as the model reads it: one `name: text` line per message. */\nexport function transcript(lines: ChatLineLike[], botName = 'bot') {\n return lines\n .filter((line) => line.text.trim())\n .map((line) => `${line.direction === 'out' ? botName : line.user.name || line.user.id}: ${line.text.replace(/\\s+/g, ' ').trim()}`)\n .join('\\n')\n}\n\n/** Items from the model's answer: a JSON array, possibly in a code fence or with text around it. */\nexport function parseCandidates(answer: string): Candidate[] {\n const start = answer.indexOf('[')\n const end = answer.lastIndexOf(']')\n if (start < 0 || end <= start) return []\n let items: unknown\n try {\n items = JSON.parse(answer.slice(start, end + 1))\n } catch {\n return []\n }\n if (!Array.isArray(items)) return []\n const seen = new Set<string>()\n return items.flatMap((item) => {\n const question = typeof item?.question === 'string' ? item.question.trim() : ''\n const text = typeof item?.answer === 'string' ? item.answer.trim() : ''\n if (!question || !text || seen.has(question)) return []\n seen.add(question)\n return [{ question: question.slice(0, 300), answer: text.slice(0, 2000) }]\n })\n}\n\n/** A candidate as a knowledge chunk. */\nexport function candidateText(candidate: Candidate) {\n return `问:${candidate.question}\\n答:${candidate.answer}`\n}\n"],"mappings":";AAaA,MAAa,eAAe;CAC1B;CACA;CACA;CACA;CACA;AACF,CAAC,CAAC,KAAK,GAAG;;AAGV,SAAgB,WAAW,OAAuB,UAAU,OAAO;CACjE,OAAO,MACJ,QAAQ,SAAS,KAAK,KAAK,KAAK,CAAC,CAAC,CAClC,KAAK,SAAS,GAAG,KAAK,cAAc,QAAQ,UAAU,KAAK,KAAK,QAAQ,KAAK,KAAK,GAAG,IAAI,KAAK,KAAK,QAAQ,QAAQ,GAAG,CAAC,CAAC,KAAK,GAAG,CAAC,CACjI,KAAK,IAAI;AACd;;AAGA,SAAgB,gBAAgB,QAA6B;CAC3D,MAAM,QAAQ,OAAO,QAAQ,GAAG;CAChC,MAAM,MAAM,OAAO,YAAY,GAAG;CAClC,IAAI,QAAQ,KAAK,OAAO,OAAO,OAAO,CAAC;CACvC,IAAI;CACJ,IAAI;EACF,QAAQ,KAAK,MAAM,OAAO,MAAM,OAAO,MAAM,CAAC,CAAC;CACjD,QAAQ;EACN,OAAO,CAAC;CACV;CACA,IAAI,CAAC,MAAM,QAAQ,KAAK,GAAG,OAAO,CAAC;CACnC,MAAM,uBAAO,IAAI,IAAY;CAC7B,OAAO,MAAM,SAAS,SAAS;EAC7B,MAAM,WAAW,OAAO,MAAM,aAAa,WAAW,KAAK,SAAS,KAAK,IAAI;EAC7E,MAAM,OAAO,OAAO,MAAM,WAAW,WAAW,KAAK,OAAO,KAAK,IAAI;EACrE,IAAI,CAAC,YAAY,CAAC,QAAQ,KAAK,IAAI,QAAQ,GAAG,OAAO,CAAC;EACtD,KAAK,IAAI,QAAQ;EACjB,OAAO,CAAC;GAAE,UAAU,SAAS,MAAM,GAAG,GAAG;GAAG,QAAQ,KAAK,MAAM,GAAG,GAAI;EAAE,CAAC;CAC3E,CAAC;AACH;;AAGA,SAAgB,cAAc,WAAsB;CAClD,OAAO,KAAK,UAAU,SAAS,MAAM,UAAU;AACjD"}
package/dist/node.js ADDED
@@ -0,0 +1,129 @@
1
+ import { passages } from "./search.js";
2
+ import { field, z } from "@mushinjs/core";
3
+ // src/node.ts
4
+ /**
5
+ * The knowledge search node: finds passages about the message, outputs them as one text for
6
+ * "Ask the LLM", and continues on `empty` when nothing scores high enough, which also tells
7
+ * whether a message is about the base's topic at all.
8
+ */
9
+ function searchNode(knowledge) {
10
+ return {
11
+ kind: "action",
12
+ type: "kb.search",
13
+ category: "ai",
14
+ title: {
15
+ en: "Search knowledge",
16
+ "zh-CN": "知识库检索"
17
+ },
18
+ description: {
19
+ en: "Finds passages about the question in knowledge bases, by keywords and by meaning. Connect \"Passages\" to the knowledge input of \"Ask the LLM\". Continues on \"empty\" when nothing scores at least the minimum, e.g. to chat normally when a message is off topic.",
20
+ "zh-CN": "在知识库里按关键词和语义查找与问题相关的片段。把“参考资料”接到“询问大模型”的参考资料输入。没有片段达到最低分数时走 empty,可用来判断消息是不是这个话题,不是就走普通闲聊。"
21
+ },
22
+ config: z.object({
23
+ bases: z.array(z.string()).default([]).meta(field("Knowledge bases", "Empty searches every base", { "zh-CN": {
24
+ title: "知识库",
25
+ description: "留空搜索全部知识库"
26
+ } }, { choices: "kb.choices" })),
27
+ topK: z.number().int().min(1).max(20).default(3).meta(field("Passages", "Most passages to return", { "zh-CN": {
28
+ title: "片段数",
29
+ description: "最多返回几段"
30
+ } })),
31
+ minScore: z.number().min(0).max(1).default(.3).meta(field("Minimum score", "From 0 to 1: the better of meaning similarity and the share of the question's words a passage contains", { "zh-CN": {
32
+ title: "最低分数",
33
+ description: "0 到 1:语义相似度和问题字词覆盖率中较高的那个"
34
+ } }))
35
+ }),
36
+ inputs: [{
37
+ name: "query",
38
+ type: "text",
39
+ title: {
40
+ en: "Question (empty: the message)",
41
+ "zh-CN": "问题(留空为收到的消息)"
42
+ }
43
+ }],
44
+ outputs: [
45
+ {
46
+ name: "context",
47
+ type: "text",
48
+ title: {
49
+ en: "Passages",
50
+ "zh-CN": "参考资料"
51
+ }
52
+ },
53
+ {
54
+ name: "top",
55
+ type: "text",
56
+ title: {
57
+ en: "Best passage",
58
+ "zh-CN": "最相关的一段"
59
+ }
60
+ },
61
+ {
62
+ name: "score",
63
+ type: "number",
64
+ title: {
65
+ en: "Best score",
66
+ "zh-CN": "最高分数"
67
+ }
68
+ },
69
+ {
70
+ name: "count",
71
+ type: "number",
72
+ title: {
73
+ en: "Passages found",
74
+ "zh-CN": "命中数"
75
+ }
76
+ },
77
+ {
78
+ name: "hits",
79
+ type: "any",
80
+ title: {
81
+ en: "Hits",
82
+ "zh-CN": "命中列表"
83
+ }
84
+ }
85
+ ],
86
+ execOut: ["next", "empty"],
87
+ async run(context) {
88
+ const { config, inputs, session, signal, logger, run } = context;
89
+ const query = String(inputs.query || session.text).trim();
90
+ const bases = config.bases ?? [];
91
+ const result = await knowledge.search({
92
+ query,
93
+ ...bases.length ? { bases } : {},
94
+ topK: Number(config.topK ?? 3),
95
+ minScore: Number(config.minScore ?? .3),
96
+ signal: AbortSignal.any([signal, AbortSignal.timeout(2e4)])
97
+ });
98
+ for (const warning of result.warnings) logger.warn(warning);
99
+ const hits = result.hits.map((hit) => ({
100
+ base: hit.baseName,
101
+ doc: hit.docTitle,
102
+ title: hit.title,
103
+ text: hit.text,
104
+ score: Math.round(hit.score * 1e3) / 1e3,
105
+ ...hit.similarity !== void 0 ? { similarity: Math.round(hit.similarity * 1e3) / 1e3 } : {},
106
+ coverage: Math.round(hit.coverage * 1e3) / 1e3
107
+ }));
108
+ run.knowledge = result.hits.map((hit) => ({
109
+ base: hit.base,
110
+ doc: hit.doc,
111
+ chunk: hit.id,
112
+ title: hit.title || hit.docTitle
113
+ }));
114
+ return {
115
+ exec: hits.length ? "next" : "empty",
116
+ outputs: {
117
+ context: passages(result.hits),
118
+ top: result.hits[0]?.text ?? "",
119
+ score: hits[0]?.score ?? 0,
120
+ count: hits.length,
121
+ hits
122
+ }
123
+ };
124
+ }
125
+ };
126
+ }
127
+ export { searchNode };
128
+
129
+ //# sourceMappingURL=node.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"node.js","names":[],"sources":["../src/node.ts"],"sourcesContent":["import { field, z } from '@mushinjs/core'\nimport type { NodeType } from '@mushinjs/plugin-flows'\nimport type { Knowledge } from './index'\nimport { passages } from './search'\n\n/**\n * The knowledge search node: finds passages about the message, outputs them as one text for\n * \"Ask the LLM\", and continues on `empty` when nothing scores high enough, which also tells\n * whether a message is about the base's topic at all.\n */\nexport function searchNode(knowledge: Knowledge): NodeType {\n return {\n kind: 'action',\n type: 'kb.search',\n category: 'ai',\n title: { en: 'Search knowledge', 'zh-CN': '知识库检索' },\n description: {\n en: 'Finds passages about the question in knowledge bases, by keywords and by meaning. Connect \"Passages\" to the knowledge input of \"Ask the LLM\". Continues on \"empty\" when nothing scores at least the minimum, e.g. to chat normally when a message is off topic.',\n 'zh-CN': '在知识库里按关键词和语义查找与问题相关的片段。把“参考资料”接到“询问大模型”的参考资料输入。没有片段达到最低分数时走 empty,可用来判断消息是不是这个话题,不是就走普通闲聊。',\n },\n config: z.object({\n bases: z.array(z.string()).default([]).meta(field('Knowledge bases', 'Empty searches every base', {\n 'zh-CN': { title: '知识库', description: '留空搜索全部知识库' },\n }, { choices: 'kb.choices' })),\n topK: z.number().int().min(1).max(20).default(3).meta(field('Passages', 'Most passages to return', {\n 'zh-CN': { title: '片段数', description: '最多返回几段' },\n })),\n minScore: z.number().min(0).max(1).default(0.3).meta(field('Minimum score', 'From 0 to 1: the better of meaning similarity and the share of the question\\'s words a passage contains', {\n 'zh-CN': { title: '最低分数', description: '0 到 1:语义相似度和问题字词覆盖率中较高的那个' },\n })),\n }),\n inputs: [{ name: 'query', type: 'text', title: { en: 'Question (empty: the message)', 'zh-CN': '问题(留空为收到的消息)' } }],\n outputs: [\n { name: 'context', type: 'text', title: { en: 'Passages', 'zh-CN': '参考资料' } },\n { name: 'top', type: 'text', title: { en: 'Best passage', 'zh-CN': '最相关的一段' } },\n { name: 'score', type: 'number', title: { en: 'Best score', 'zh-CN': '最高分数' } },\n { name: 'count', type: 'number', title: { en: 'Passages found', 'zh-CN': '命中数' } },\n { name: 'hits', type: 'any', title: { en: 'Hits', 'zh-CN': '命中列表' } },\n ],\n execOut: ['next', 'empty'],\n async run(context) {\n const { config, inputs, session, signal, logger, run } = context\n const query = String(inputs.query || session.text).trim()\n const bases = (config.bases as string[] | undefined) ?? []\n const result = await knowledge.search({\n query,\n ...(bases.length ? { bases } : {}),\n topK: Number(config.topK ?? 3),\n minScore: Number(config.minScore ?? 0.3),\n signal: AbortSignal.any([signal, AbortSignal.timeout(20_000)]),\n })\n for (const warning of result.warnings) logger.warn(warning)\n const hits = result.hits.map((hit) => ({\n base: hit.baseName, doc: hit.docTitle, title: hit.title, text: hit.text, score: Math.round(hit.score * 1000) / 1000,\n ...(hit.similarity !== undefined ? { similarity: Math.round(hit.similarity * 1000) / 1000 } : {}),\n coverage: Math.round(hit.coverage * 1000) / 1000,\n }))\n // Replies later in the run show which passages they came from.\n run.knowledge = result.hits.map((hit) => ({ base: hit.base, doc: hit.doc, chunk: hit.id, title: hit.title || hit.docTitle }))\n return {\n exec: hits.length ? 'next' : 'empty',\n outputs: {\n context: passages(result.hits),\n top: result.hits[0]?.text ?? '',\n score: hits[0]?.score ?? 0,\n count: hits.length,\n hits,\n },\n }\n },\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Hybrid search over the chunks of one knowledge base: BM25 over words (CJK text as pairs of
3
+ * characters) and cosine similarity over embeddings, fused by reciprocal rank.
4
+ */
5
+ /**
6
+ * Words of a text for keyword search: Latin words and numbers as they are, runs of CJK characters
7
+ * as overlapping pairs (`能发广告` → `能发 发广 广告`), a lone CJK character as itself. Pairs work
8
+ * without a dictionary; SQLite's trigram tokenizer cannot match two-character words at all.
9
+ */
10
+ export declare function tokenize(text: string): string[];
11
+ /** A vector scaled to length 1, so a dot product is the cosine similarity. */
12
+ export declare function normalize(vector: ArrayLike<number>): Float32Array;
13
+ export declare function dot(a: Float32Array, b: Float32Array): number;
14
+ export interface IndexedChunk {
15
+ id: string;
16
+ doc: string;
17
+ title: string;
18
+ text: string;
19
+ /** Length 1; missing until the chunk is embedded. */
20
+ vector?: Float32Array;
21
+ }
22
+ export interface SearchOptions {
23
+ topK?: number;
24
+ /** Hits whose score (the better of similarity and keyword coverage) is lower are dropped. */
25
+ minScore?: number;
26
+ }
27
+ export interface SearchHit {
28
+ id: string;
29
+ doc: string;
30
+ title: string;
31
+ text: string;
32
+ /** What `minScore` compares: the better of `similarity` and `coverage`, 0 to 1. */
33
+ score: number;
34
+ /** Cosine similarity to the question, when both are embedded. */
35
+ similarity?: number;
36
+ /** Share of the question's words found in the chunk, 0 to 1. */
37
+ coverage: number;
38
+ /** BM25 score; 0 when no word matched. */
39
+ bm25: number;
40
+ /** Place in each list (from 1), and the fused order that decides the result. */
41
+ ranks: {
42
+ keyword?: number;
43
+ vector?: number;
44
+ };
45
+ fused: number;
46
+ }
47
+ export declare class SearchIndex {
48
+ private entries;
49
+ private df;
50
+ private totalLength;
51
+ constructor(chunks?: IndexedChunk[]);
52
+ get size(): number;
53
+ /** Chunks that have a vector. */
54
+ get embedded(): number;
55
+ chunks(): IndexedChunk[];
56
+ get(id: string): IndexedChunk | undefined;
57
+ add(chunk: IndexedChunk): void;
58
+ remove(id: string): void;
59
+ private idf;
60
+ /** Searches by words, and by meaning when a normalized question vector is given. */
61
+ search(text: string, vector: Float32Array | undefined, options?: SearchOptions): SearchHit[];
62
+ /**
63
+ * Pairs of similar chunks, for the similarity map: each chunk's closest `neighbors` above
64
+ * `minSimilarity`. Uses vectors when chunks have them, shared words otherwise.
65
+ */
66
+ links(options?: {
67
+ neighbors?: number;
68
+ minSimilarity?: number;
69
+ limit?: number;
70
+ }): {
71
+ nodes: IndexedChunk[];
72
+ edges: {
73
+ source: string;
74
+ target: string;
75
+ similarity: number;
76
+ }[];
77
+ byVector: boolean;
78
+ };
79
+ }
80
+ /** Hits as numbered passages for a model: `[1] title` and the text. */
81
+ export declare function passages(hits: {
82
+ title: string;
83
+ text: string;
84
+ docTitle?: string;
85
+ }[]): string;