@skaile/forge-editor 6.3.0 → 6.4.0
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.
- package/CHANGELOG.md +8 -0
- package/dist/adapters/nitro.d.ts +11 -3
- package/dist/adapters/nitro.js +2 -1
- package/dist/adapters/nitro.js.map +1 -1
- package/dist/code/CodeSurface.vue.d.ts +3 -3
- package/dist/document/DocumentSurface.vue.d.ts +3 -3
- package/dist/features/ai/index.d.ts +2 -2
- package/dist/features/ai/index.js +2 -2
- package/dist/features/ai/protocol/chat.d.ts +5 -0
- package/dist/features/ai/protocol/chat.js +7 -4
- package/dist/features/ai/protocol/chat.js.map +1 -1
- package/dist/features/ai/protocol/index.d.ts +2 -2
- package/dist/features/ai/protocol/index.js +2 -2
- package/dist/features/composites/components/CompositeInspector.js +8 -8
- package/dist/features/composites/vue.js +1 -1
- package/dist/markdown/MarkdownSurface.vue.d.ts +7 -7
- package/dist/server/chat-agent.d.ts +85 -0
- package/dist/server/chat-agent.js +61 -0
- package/dist/server/chat-agent.js.map +1 -0
- package/dist/server/document-tools.d.ts +22 -0
- package/dist/server/document-tools.js +190 -0
- package/dist/server/document-tools.js.map +1 -0
- package/dist/server/handlers/ai.d.ts +2 -1
- package/dist/server/handlers/ai.js +232 -121
- package/dist/server/handlers/ai.js.map +1 -1
- package/dist/server/handlers/context.d.ts +4 -1
- package/dist/server/handlers/context.js.map +1 -1
- package/dist/server/index.d.ts +7 -3
- package/dist/server/index.js +5 -2
- package/dist/server/workspace-tools.d.ts +1 -0
- package/dist/server/workspace-tools.js +8 -3
- package/dist/server/workspace-tools.js.map +1 -1
- package/dist/tree/DocumentTree.js +1 -1
- package/dist/tree/use-tree-model.js +1 -1
- package/dist/vue/AdvancedEditor.vue.d.ts +4 -4
- package/dist/vue/SimpleEditor.vue.d.ts +4 -4
- package/dist/vue/components/EditorSurface.vue.d.ts +4 -4
- package/dist/vue/compose.js +1 -1
- package/dist/vue/index.js +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ai.js","names":[],"sources":["../../../src/server/handlers/ai.ts"],"sourcesContent":["import { createHash, randomUUID } from 'node:crypto'\nimport type { JSONContent } from '@tiptap/core'\nimport { jsonSchema, type ModelMessage, tool, type ToolSet } from 'ai'\nimport { z } from 'zod'\nimport {\n AGENT_LIMITS,\n AGENT_OP_OUTCOMES,\n AGENT_OP_TYPES,\n AGENT_TOOL,\n AI_ID_PATTERN,\n AI_TASKS,\n type AgentOp,\n type AgentOpProblem,\n agentOpTarget,\n agentRetryMessage,\n agentUnreadableProblem,\n aiBlockSchema,\n checkAgentOps,\n CHAT_LIMITS,\n type BlockLines,\n type ChatToolCall,\n completeBodySchema,\n documentChatMessages,\n generateBodySchema,\n generateProblem,\n type ModelAgentOp,\n modelRefSchema,\n readBlockLines,\n runCompletion,\n runGenerate,\n} from '../../features/ai/protocol'\nimport { blockExcerptText } from '../../core/block-snippet'\nimport { AGENT_OP_ID, type ChangeSetHunk } from '../../core/change-set'\nimport type { ExtractMethod, ExtractRequest, ExtractResponse } from '../../core/backend-types'\nimport { PROPERTY_TYPES } from '../../core/properties'\nimport { aiBlockTypesOf } from '../../features/composites/schema'\nimport { ASSET_DOCUMENT_ID, ASSET_NAME } from '../../features/media/schema'\nimport {\n runTranscriptCleanup,\n TranscriptCleanupUnreadableError,\n transcriptCleanupRequestSchema,\n} from '../../integrations/transcript-ai/protocol'\nimport { STRUCTURE_MODES } from '../../settings'\nimport { HttpError } from '../errors'\nimport { BIBLIOGRAPHY_PAGES, bibliographyText, describeImage, extractBibliography, isScanned, joinPages, MAX_OCR_PAGES, MAX_TRANSCRIBE_BYTES, ocrPages, pdfPagesText, RasterError, transcribe, type TranscriptionResult } from '../extract'\nimport { DOI_PATTERN } from '../crossref'\nimport { blockExcerptOf, blockLinesOfContent } from '../document-blocks'\nimport { openPdf } from '../pdf'\nimport { changeSetOwner } from '../../adapters/change-sets/owner'\nimport { mayEditDocument } from '../readable-documents'\nimport { loadCurrentContent } from '../stored-content'\nimport { type AiHandlerContext, authenticated, enforceRateLimit, eventStream, type HandlerRequest, type HandlerResponse, json, port, readBody } from './context'\nimport { type ToolContext, toolRegistry, type ToolRegistry } from '../tools'\n\n/** What a context without `tools` offers the model: nothing beside `edit_document`. */\nconst NO_TOOLS = toolRegistry()\n\n/** Turns of the chat that may end in read-only tool calls before the model has to answer, and how many such calls one turn may run. */\nconst MAX_TOOL_ROUNDS = 5\nconst MAX_TOOL_CALLS_PER_ROUND = 8\nconst TOOL_CALLS_LIMIT_OUTPUT = `Nicht ausgeführt: höchstens ${MAX_TOOL_CALLS_PER_ROUND} Werkzeugaufrufe pro Antwort. Wiederhole diesen Aufruf, wenn du ihn noch brauchst.`\nconst EDIT_DEFERRED = `Nicht ausgeführt: In derselben Antwort fehlten noch die Ergebnisse der Lesewerkzeuge. Rufe ${AGENT_TOOL} erneut auf, wenn die Änderung noch passt.`\n\n/** The input of a tool call as the history keeps it: always an object. */\nconst toolInput = (input: unknown): Record<string, unknown> => typeof input === 'object' && input !== null && !Array.isArray(input) ? input as Record<string, unknown> : {}\n\n/** Runs one call of a registered tool and answers the text the model reads, cut to `CHAT_LIMITS.toolOutput`. A failure is that text too, so the model can go on without the tool. */\nasync function runTool(registry: ToolRegistry, context: ToolContext, call: { toolName: string, input: unknown }): Promise<string> {\n try {\n return (await registry.providers.get(call.toolName)!.handle(context, call.input)).slice(0, CHAT_LIMITS.toolOutput)\n }\n catch (error) {\n if (context.signal.aborted) throw error\n const { statusMessage, message } = error as { statusMessage?: string, message?: string }\n return `Fehler: ${statusMessage ?? message ?? 'Unbekannter Fehler'}`.slice(0, CHAT_LIMITS.toolOutput)\n }\n}\n\nconst blockId = z.string().min(1).max(100)\nconst opId = z.string().regex(AGENT_OP_ID)\nconst markdown = z.string().min(1).max(AGENT_LIMITS.markdown)\n/** A `convert` target: a `CONVERT_TARGET_IDS` entry, `typedBlock:<kind>`, or a host block id (`aiContext.blocks` with a conversion provider). */\nconst convertTarget = z.string().min(1).max(100)\n\n/** The Document an operation or a decision belongs to when it is not the open one. */\nconst documentId = z.string().min(1).max(200)\n\nconst opSchema = z.discriminatedUnion('type', [\n z.object({ op: opId, type: z.literal('update'), documentId: documentId.optional(), id: blockId, markdown }),\n z.object({ op: opId, type: z.literal('delete'), documentId: documentId.optional(), id: blockId }),\n z.object({ op: opId, type: z.literal('add'), documentId: documentId.optional(), afterId: blockId.nullable(), markdown }),\n z.object({ op: opId, type: z.literal('convert'), documentId: documentId.optional(), id: blockId, to: convertTarget }),\n])\n\nconst label = z.string().min(1).max(100)\nconst blockTypeSchema = z.object({\n // Same characters as the `::: <Block type>` fence opener accepts.\n id: z.string().regex(AI_ID_PATTERN).max(50),\n label,\n purpose: z.string().min(1).max(500).optional(),\n fields: z.array(z.object({\n key: z.string().min(1).max(50),\n label,\n type: z.enum(PROPERTY_TYPES),\n // A select can list every slot of a template (`@skaile/forge-editor-compose`), which runs past 50.\n options: z.array(z.object({ value: z.string().min(1).max(100), label })).max(500).optional(),\n })).max(20),\n})\n\n/** A read-only tool call of an earlier turn (the `tool` event the client kept): replayed to the model with its result. */\nconst toolCallSchema = z.object({\n name: z.string().min(1).max(64),\n input: z.record(z.string(), z.unknown()),\n output: z.string().max(CHAT_LIMITS.toolOutput),\n}) satisfies z.ZodType<ChatToolCall>\n\nconst messageSchema = z.discriminatedUnion('role', [\n z.object({\n role: z.literal('user'),\n content: z.string().min(1).max(CHAT_LIMITS.message),\n feedback: z.array(z.object({ op: opId, type: z.enum(AGENT_OP_TYPES), target: blockId.nullable(), outcome: z.enum(AGENT_OP_OUTCOMES), documentId: documentId.optional() })).max(500).optional(),\n }),\n z.object({\n role: z.literal('assistant'),\n content: z.string().max(CHAT_LIMITS.message),\n ops: z.array(opSchema).max(AGENT_LIMITS.ops).optional(),\n dropped: z.number().int().min(0).max(1000).optional(),\n tools: z.array(toolCallSchema).max(CHAT_LIMITS.tools).optional(),\n }).refine(m => m.content || m.ops?.length || m.dropped || m.tools?.length, 'Leere Antwort im Verlauf'),\n])\n\nexport const chatBodySchema = z.object({\n messages: z.array(messageSchema).min(1).max(CHAT_LIMITS.messages).refine(messages => messages.at(-1)!.role === 'user', 'Die letzte Nachricht muss von der Nutzerin bzw. dem Nutzer sein'),\n document: z.string().max(CHAT_LIMITS.document),\n /** The ID of the open Document: operations naming it (or no Document) are edits of the open Document, any other `documentId` aims at another Document of the workspace. */\n documentId: documentId.optional(),\n /** Block IDs edits are limited to (the selected blocks); null or missing: the whole document. */\n scope: z.array(blockId).min(1).max(AGENT_LIMITS.scope).nullish(),\n /** Number of the first new operation (`op<n>`), so IDs stay unique across the conversation. */\n nextOp: z.number().int().min(1).max(1_000_000).default(1),\n model: modelRefSchema,\n /** The editor's Block types (`blockTypesOf`), for the block model in the system prompt. Ignored when the server resolves them itself (`AiHandlerContext.blockTypes`). */\n blockTypes: z.array(blockTypeSchema).max(50),\n /** The editor's host blocks (`aiContext.blocks`), for the block model in the system prompt. */\n blocks: z.array(aiBlockSchema).max(50).default([]),\n /** Block types `convert` may turn a block into. */\n targets: z.array(convertTarget).max(100),\n /** The user's preferred structure of new content (settings `blocks.structure`). */\n structure: z.enum(STRUCTURE_MODES),\n})\n\n/**\n * The tools of one chat turn: `edit_document` (deliberately loose, so malformed calls reach `checkAgentOps` and get a precise\n * retry message) and, with `reads`, the registry's read-only tools. With `workspace` (the host keeps change sets and a mutator)\n * the operations of `edit_document` may name another Document by `documentId`.\n */\nfunction agentTools(registry: ToolRegistry, { workspace, reads }: { workspace: boolean, reads: boolean }): ToolSet {\n return {\n [AGENT_TOOL]: tool({\n description: workspace\n ? 'Schlägt Änderungen vor. Operationen am geöffneten Dokument zeigt die Nutzerin bzw. der Nutzer im Dokument zur Prüfung an; Operationen mit documentId (anderes Dokument des Arbeitsbereichs) landen in einem Änderungssatz, den die Nutzerin bzw. der Nutzer je Dokument prüft und übernimmt oder ablehnt.'\n : 'Schlägt Änderungen am Dokument vor. Jede Operation wird der Nutzerin bzw. dem Nutzer im Dokument zur Prüfung angezeigt.',\n inputSchema: jsonSchema<{ ops: unknown[] }>({\n type: 'object',\n properties: {\n ops: {\n type: 'array',\n description: 'Die Operationen in Dokumentreihenfolge.',\n items: {\n type: 'object',\n properties: {\n type: { type: 'string', enum: [...AGENT_OP_TYPES], description: 'update: Blockinhalt ersetzen; delete: Block löschen; add: Blöcke einfügen; convert: Blocktyp ändern.' },\n ...(workspace && { documentId: { type: 'string', description: 'Nur für Blöcke eines anderen Dokuments: dessen ID (zusammen mit den Block-IDs aus find_blocks oder read_block). Es muss ein Dokument sein, das die Nutzerin bzw. der Nutzer bearbeiten darf. Ohne Angabe gilt das geöffnete Dokument.' } }),\n id: { type: 'string', description: 'update/delete/convert: ID des Blocks aus dem Dokument.' },\n afterId: { type: ['string', 'null'], description: 'add (Pflicht): ID des Blocks, nach dem eingefügt wird; für den Dokumentanfang ausdrücklich null.' },\n markdown: { type: 'string', description: 'update/add: der neue Inhalt als Markdown, ohne Block-ID.' },\n to: { type: 'string', description: 'convert: der neue Blocktyp, einer der im Systemprompt erlaubten Werte (z. B. paragraph, heading2, composite:task).' },\n },\n required: ['type'],\n },\n },\n },\n required: ['ops'],\n }),\n }),\n ...(reads && registry.tools),\n }\n}\n\n/**\n * Document chat: answers the last message of `messages` about `document`, or proposes edit operations through the\n * `edit_document` tool. Operations pointing to IDs that are unknown or outside `scope`, an `add` without `afterId`, an\n * operation inside a block that another operation replaces or deletes, or converting into a type not in\n * `targets`, make the model retry (at most `AGENT_LIMITS.retries` times); what is still invalid then, or left out by a\n * corrected list, is dropped. The system prompt describes the editor's block model from `blockTypes`, `blocks` and `targets`. SSE like\n * `/api/ai/generate`: `delta` (text), `ops` (`{ ops: AgentOp[], dropped: AgentOpProblem[], retries: number }`, once,\n * before `done`), `change-set` (the stored `ChangeSet`, once, when operations aimed at other Documents), `done`, `error`.\n * Operations of the open Document (`documentId` absent or the body's) are `ops`; operations naming another Document the\n * user may edit (`mayEditDocument`) become one `ChangeSet` (`ChangeSetStore`) for the user to review per Document, when the\n * host has the `changeSets` port and a mutator; else such an operation is a `document` problem the model retries. All\n * operations share one numbering (`op<n>`).\n */\nexport async function chatPost(ctx: AiHandlerContext, request: HandlerRequest): Promise<HandlerResponse> {\n const user = await enforceRateLimit(ctx, request)\n const { messages, document, documentId: openDocumentId, scope, nextOp, model, blockTypes: clientBlockTypes, blocks, targets, structure } = await readBody(request, chatBodySchema)\n const blockTypes = ctx.blockTypes ? aiBlockTypesOf(ctx.blockTypes) : clientBlockTypes\n // Unknown preset or missing key must fail as a plain JSON error before the SSE response starts.\n const languageModel = await ctx.ai.resolveModel(model, user)\n if (!messages.at(-1)!.content.trim()) throw new HttpError(400, 'Nachricht fehlt')\n\n const lines = readBlockLines(document)\n const allowed = scope ? new Set(scope.filter(id => lines.ids.has(id))) : null\n if (allowed && !allowed.size) throw new HttpError(400, 'Die Auswahl enthält keine Blöcke des Dokuments')\n const context = { lines, allowed, targets: new Set(targets) }\n const registry = ctx.tools ?? NO_TOOLS\n // Operations may aim at other Documents only where accepting them is possible: stored change sets and the mutator that applies them.\n const changeSets = ctx.mutator ? ctx.ports.changeSets : undefined\n const workspace = changeSets !== undefined\n const editTools = agentTools(registry, { workspace, reads: false })\n const allTools = agentTools(registry, { workspace, reads: true })\n\n return eventStream(request, async (emit, signal) => {\n try {\n let conversation: ModelMessage[] = documentChatMessages(messages, document, allowed && [...allowed], { blockTypes, blocks, targets, structure }, registry.infos)\n let answered = false\n let result: { valid: ModelAgentOp[], problems: AgentOpProblem[] } | null = null\n // Problems of attempts that were retried: an operation the corrected list leaves out counts as dropped too.\n const retried: AgentOpProblem[] = []\n let retries = 0\n // Turns that ended in read-only tool calls: the server ran them and the model answers again.\n let rounds = 0\n const toolContext: ToolContext = { ports: ctx.ports, user, signal, liveContent: id => ctx.collab?.liveContent(id) ?? null, ...(openDocumentId && { documentId: openDocumentId }) }\n // The other Documents the operations name, as far as the user may edit them: loaded once per request (the model's read and its edit see the same content).\n const loaded = new Map<string, Promise<{ content: JSONContent, lines: BlockLines } | null>>()\n const documentOf = (id: string) => {\n let entry = loaded.get(id)\n if (!entry) {\n entry = (async () => {\n if (!await mayEditDocument(ctx.ports, user, id)) return null\n const content = toolContext.liveContent(id) ?? await loadCurrentContent(ctx.ports.documents, id)\n return content && { content, lines: blockLinesOfContent(content) }\n })()\n loaded.set(id, entry)\n }\n return entry\n }\n for (;;) {\n // `edit_document` calls are collected for checking; calls of the registered tools are run right after the turn.\n const calls: { toolCallId: string, input: unknown, invalid: boolean }[] = []\n const reads: { toolCallId: string, toolName: string, input: unknown }[] = []\n const retry = retries > 0\n let text = ''\n for await (const part of ctx.ai.streamChatTurn({\n model: languageModel,\n messages: conversation,\n // Retries only correct the operations: no second answer text, nothing to read.\n tools: retry || rounds >= MAX_TOOL_ROUNDS ? editTools : allTools,\n toolChoice: retry ? { type: 'tool', toolName: AGENT_TOOL } : 'auto',\n signal,\n })) {\n if (part.type === 'tool-call') {\n if (part.toolName === AGENT_TOOL) calls.push(part)\n else if (registry.providers.has(part.toolName)) reads.push(part)\n }\n else if (!retry) {\n // The text of a later turn is a new paragraph of the answer.\n if (!text && answered) emit('delta', JSON.stringify({ text: '\\n\\n' }))\n text += part.text\n answered = true\n emit('delta', JSON.stringify({ text: part.text }))\n }\n }\n\n if (reads.length && !retry && rounds < MAX_TOOL_ROUNDS) {\n rounds++\n const outputs = await Promise.all(reads.map((call, i) => i < MAX_TOOL_CALLS_PER_ROUND ? runTool(registry, toolContext, call) : TOOL_CALLS_LIMIT_OUTPUT))\n reads.forEach((call, i) => emit('tool', JSON.stringify({ name: call.toolName, input: toolInput(call.input), output: outputs[i]! } satisfies ChatToolCall)))\n conversation = [\n ...conversation,\n {\n role: 'assistant',\n content: [\n ...(text ? [{ type: 'text' as const, text }] : []),\n ...reads.map(call => ({ type: 'tool-call' as const, toolCallId: call.toolCallId, toolName: call.toolName, input: call.input ?? {} })),\n ...calls.map(call => ({ type: 'tool-call' as const, toolCallId: call.toolCallId, toolName: AGENT_TOOL, input: call.input ?? {} })),\n ],\n },\n {\n role: 'tool',\n content: [\n ...reads.map((call, i) => ({ type: 'tool-result' as const, toolCallId: call.toolCallId, toolName: call.toolName, output: { type: 'text' as const, value: outputs[i]! } })),\n // An edit proposed in the same turn was written before its context was complete: not applied.\n ...calls.map(call => ({ type: 'tool-result' as const, toolCallId: call.toolCallId, toolName: AGENT_TOOL, output: { type: 'text' as const, value: EDIT_DEFERRED } })),\n ],\n },\n ]\n continue\n }\n if (!calls.length) break\n\n const unreadable = calls.some(call => call.invalid || !Array.isArray((call.input as { ops?: unknown } | null)?.ops))\n const raw = calls.flatMap(call => (call.input as { ops?: unknown } | null)?.ops).filter(op => op !== undefined)\n const documents = new Map<string, BlockLines>()\n if (changeSets) {\n const named = new Set(raw.flatMap(op => typeof (op as { documentId?: unknown } | null)?.documentId === 'string' ? [(op as { documentId: string }).documentId.trim()] : []))\n named.delete(openDocumentId ?? '')\n // At most one Document per operation; the limit of operations bounds the loads.\n for (const id of [...named].slice(0, AGENT_LIMITS.ops)) {\n const entry = id ? await documentOf(id) : null\n if (entry) documents.set(id, entry.lines)\n }\n }\n const checked = checkAgentOps(raw, { ...context, ...(openDocumentId && { documentId: openDocumentId }), documents })\n if (unreadable) checked.problems.unshift(agentUnreadableProblem())\n result = checked\n if (!checked.problems.length || retries === AGENT_LIMITS.retries) break\n\n retried.push(...checked.problems)\n retries++\n const correction = agentRetryMessage(checked.problems, !!allowed)\n conversation = [\n ...conversation,\n {\n role: 'assistant',\n content: [\n ...(text ? [{ type: 'text' as const, text }] : []),\n ...calls.map(call => ({ type: 'tool-call' as const, toolCallId: call.toolCallId, toolName: AGENT_TOOL, input: call.input ?? {} })),\n ],\n },\n { role: 'tool', content: calls.map(call => ({ type: 'tool-result' as const, toolCallId: call.toolCallId, toolName: AGENT_TOOL, output: { type: 'text' as const, value: correction } })) },\n ]\n }\n\n if (result) {\n const all: AgentOp[] = result.valid.map((op, i) => ({ ...op, op: `op${nextOp + i}` }))\n // The open Document's operations are the editor's in-document review; the others become a change set.\n const ops = all.filter(op => op.documentId === undefined)\n const foreign = all.filter(op => op.documentId !== undefined)\n const targets = new Set(all.map(op => agentOpTarget(op)))\n const dropped = [...result.problems]\n for (const problem of retried) {\n if (problem.id !== null && !targets.has(problem.id) && !dropped.some(d => d.id === problem.id && d.type === problem.type)) dropped.push(problem)\n }\n if (!answered && !all.length) {\n emit('delta', JSON.stringify({ text: 'Ich konnte keine gültigen Änderungen vorschlagen.' }))\n }\n if (foreign.length) {\n const hunks: ChangeSetHunk[] = await Promise.all(foreign.map(async (op): Promise<ChangeSetHunk> => {\n const documentId = op.documentId!\n const target = agentOpTarget(op)\n const entry = await documentOf(documentId)\n const excerpt = target === null || !entry ? null : blockExcerptOf(entry.content, target)\n return { op: { ...op, documentId }, status: 'pending', target: excerpt && { type: excerpt.type, text: blockExcerptText(excerpt.text) } }\n }))\n emit('change-set', JSON.stringify(await changeSets!.create({ by: changeSetOwner(user), hunks })))\n }\n emit('ops', JSON.stringify({ ops, dropped, retries }))\n }\n emit('done', '{}')\n \n }\n catch (error) {\n if (!signal.aborted) {\n const { statusMessage, message } = error as { statusMessage?: string, message?: string }\n emit('error', JSON.stringify({ message: statusMessage ?? message ?? 'Unbekannter Fehler' }))\n }\n }\n })\n}\n\n// The body schemas live with the protocol (the browser-direct adapter validates with them too); the handlers keep exporting them.\nexport { completeBodySchema, generateBodySchema }\n\n/** Short inline continuation (ghost text) of the text before the caret. Plain JSON: `{ text }`, empty when nothing fits. */\nexport async function completePost(ctx: AiHandlerContext, request: HandlerRequest): Promise<HandlerResponse> {\n const user = await enforceRateLimit(ctx, request)\n const { before, model } = await readBody(request, completeBodySchema)\n const languageModel = await ctx.ai.resolveModel(model, user)\n\n // Typing on the client aborts its fetch; stop the upstream request with it.\n const abort = new AbortController()\n request.signal?.addEventListener('abort', () => abort.abort(), { once: true })\n return json({ text: await runCompletion(before, (messages, signal) => ctx.ai.streamChat({ model: languageModel, messages, signal }), abort.signal) })\n}\n\n/** Streams one text task (rewrite, translate, generate, …) as SSE: `delta` (`{ text }`), `done`, `error` (`{ message }`). */\nexport async function generatePost(ctx: AiHandlerContext, request: HandlerRequest): Promise<HandlerResponse> {\n const user = await enforceRateLimit(ctx, request)\n const { task: taskId, model, ...input } = await readBody(request, generateBodySchema)\n // Unknown preset or missing key must fail as a plain JSON error before the SSE response starts.\n const languageModel = await ctx.ai.resolveModel(model, user)\n const task = AI_TASKS[taskId]\n const problem = generateProblem(task, input)\n if (problem) throw new HttpError(400, problem)\n const users = task.needsUsers ? (await port(ctx, 'directory').listUsers()).map(u => u.username) : undefined\n\n return eventStream(request, async (emit, signal) => {\n for await (const { event, data } of runGenerate(task, input, users, (messages, streamSignal) => ctx.ai.streamChat({ model: languageModel, messages, signal: streamSignal }), signal)) {\n emit(event, JSON.stringify(data))\n }\n })\n}\n\n/**\n * The models the client may choose from (without keys) — the `models` port's `listModels` for the user, else the\n * server-defined presets — and the default preset id per model role, for the client's model profiles. Requires an\n * authenticated user through the `identity` port; not rate limited, since it calls no model.\n */\nexport async function modelsGet(ctx: AiHandlerContext, request: HandlerRequest): Promise<HandlerResponse> {\n const user = await authenticated(ctx, request)\n return json({ presets: await ctx.ai.listModels(user), roles: ctx.ai.roles })\n}\n\nconst sha256Hex = (data: string | Uint8Array): string => createHash('sha256').update(data).digest('hex')\n\n/** PDF pages read by `pdf-text` and `ocr` unless the request caps them lower. */\nconst DEFAULT_MAX_PAGES = 200\n\n/** Body schema of `extractPost`. */\nexport const extractBodySchema = z.object({\n documentId: z.string().regex(ASSET_DOCUMENT_ID),\n name: z.string().regex(ASSET_NAME).optional(),\n method: z.enum(['auto', 'transcription', 'pdf-text', 'ocr', 'description', 'bibliography']),\n model: modelRefSchema,\n bibliography: z.boolean().optional(),\n doi: z.string().regex(DOI_PATTERN).optional(),\n maxPages: z.number().int().min(1).max(500).default(DEFAULT_MAX_PAGES),\n}).refine(body => body.name !== undefined || (body.method === 'bibliography' && body.doi !== undefined), {\n message: 'name fehlt (nur eine Literaturabfrage mit DOI braucht keine Datei)',\n path: ['name'],\n}) satisfies z.ZodType<ExtractRequest>\n\n/** The method an `ExtractRequest` runs for an asset of `mimeType`: `auto` picks by type; 415 when the type does not fit the method. */\nfunction extractMethodOf(requested: ExtractMethod, mimeType: string, hasDoi: boolean): ExtractResponse['method'] {\n const audioVideo = /^(?:audio|video)\\//.test(mimeType)\n const image = mimeType.startsWith('image/')\n const pdf = mimeType === 'application/pdf'\n const fits: Record<ExtractMethod, boolean> = {\n 'auto': audioVideo || image || pdf,\n 'transcription': audioVideo,\n 'description': image,\n 'pdf-text': pdf,\n 'ocr': pdf,\n // A given DOI needs no document text, so any asset can stand for the work.\n 'bibliography': pdf || hasDoi,\n }\n if (!fits[requested]) throw new HttpError(415, `Dateityp ${mimeType} wird für diese Auswertung nicht unterstützt`)\n if (requested !== 'auto') return requested\n if (audioVideo) return 'transcription'\n return image ? 'description' : 'pdf-text'\n}\n\n/**\n * Turns a stored asset into readable content (`ExtractRequest` → `ExtractResponse`): audio/video into a transcription\n * with segment timestamps, a PDF into its text per page (OCR through the model when it has no text layer), an image\n * into a description, bibliographic data from Crossref or the model. The raw Markdown is stored as an asset of the same\n * document (`output`), with the SHA-256 of input and output. Errors: 404 unknown asset, 415 type not supported, 413\n * oversize audio, 422 unreadable PDF, 501 transcription or OCR unavailable, 502 upstream failure.\n */\nexport async function extractPost(ctx: AiHandlerContext, request: HandlerRequest): Promise<HandlerResponse> {\n const user = await enforceRateLimit(ctx, request)\n const body = await readBody(request, extractBodySchema)\n if (body.name === undefined) {\n // Only a DOI lookup comes without a file (the schema allows nothing else): no asset, no output, empty hashes.\n const { bib, warnings } = await extractBibliography({ doi: body.doi, text: '', resolveModel: () => ctx.ai.resolveModel(body.model, user), signal: request.signal })\n return json({ method: 'bibliography', markdown: '', bib, inputHash: '', outputHash: sha256Hex(''), output: null, warnings } satisfies ExtractResponse)\n }\n const assets = port(ctx, 'assets')\n const asset = await assets.loadAsset(body.documentId, body.name)\n if (!asset) throw new HttpError(404, 'Datei nicht gefunden')\n const inputHash = sha256Hex(asset.data)\n let method = extractMethodOf(body.method, asset.record.mimeType, body.doi !== undefined)\n // Closing the page aborts the client's fetch; the upstream requests stop with it.\n const { signal } = request\n const resolveModel = () => ctx.ai.resolveModel(body.model, user)\n\n const warnings: string[] = []\n let markdown = ''\n let model: string | undefined\n let segments: TranscriptionResult['segments']\n let bib: ExtractResponse['bib']\n\n if (method === 'transcription') {\n const config = ctx.ai.transcription\n if (!config?.endpoint || !config.model) throw new HttpError(501, 'Transkription nicht konfiguriert')\n if (asset.data.byteLength > MAX_TRANSCRIBE_BYTES) throw new HttpError(413, `Datei ist größer als ${MAX_TRANSCRIBE_BYTES / 1024 / 1024} MB`)\n ;({ markdown, segments } = await transcribe(config, asset, signal))\n model = config.model\n }\n else if (method === 'description') {\n markdown = await describeImage(await resolveModel(), asset.data, asset.record.mimeType, signal)\n }\n else if (asset.record.mimeType !== 'application/pdf') {\n // `bibliography` of a non-PDF asset: only reachable with a DOI.\n const found = await extractBibliography({ doi: body.doi, text: '', resolveModel, signal })\n bib = found.bib\n warnings.push(...found.warnings)\n }\n else {\n const pdf = await openPdf(asset.data)\n try {\n if (method === 'bibliography') {\n const pages = await pdfPagesText(pdf, Math.min(pdf.pageCount, BIBLIOGRAPHY_PAGES), signal)\n const found = await extractBibliography({ doi: body.doi, text: bibliographyText(pages), resolveModel, signal })\n bib = found.bib\n warnings.push(...found.warnings)\n }\n else {\n const count = Math.min(pdf.pageCount, body.maxPages)\n if (pdf.pageCount > count) warnings.push('pages-truncated')\n let pages = await pdfPagesText(pdf, count, signal)\n if (body.method === 'auto' && isScanned(pages)) method = 'ocr'\n if (method === 'ocr') {\n // One vision call per page: the OCR reads fewer pages than the text layer would.\n const ocrCount = Math.min(count, MAX_OCR_PAGES)\n try {\n pages = await ocrPages(pdf, ocrCount, resolveModel, signal)\n if (ocrCount < count) warnings.push('ocr-truncated')\n }\n catch (error) {\n if (!(error instanceof RasterError)) throw error\n // Pages cannot be rendered here: keep what the text layer has.\n if (!pages.some(Boolean)) throw new HttpError(501, 'OCR nicht verfügbar')\n warnings.push('ocr-unavailable')\n method = 'pdf-text'\n }\n }\n markdown = joinPages(pages)\n if (body.bibliography) {\n try {\n const found = await extractBibliography({ doi: body.doi, text: bibliographyText(pages), resolveModel, signal })\n bib = found.bib\n warnings.push(...found.warnings)\n }\n catch (error) {\n // The text is the result; missing bibliographic data must not lose it.\n if (signal?.aborted) throw error\n warnings.push('bibliography-failed')\n }\n }\n }\n }\n finally {\n await pdf.close()\n }\n }\n\n const outputHash = sha256Hex(markdown)\n let output: string | null = null\n if (method !== 'bibliography' && markdown) {\n const data = Buffer.from(markdown, 'utf8')\n output = `${randomUUID()}.md`\n await assets.storeAsset(body.documentId, {\n name: output,\n filename: `${asset.record.filename}.${method}.md`.slice(0, 255),\n mimeType: 'text/markdown',\n size: data.byteLength,\n createdAt: new Date().toISOString(),\n sha256: outputHash,\n data,\n })\n }\n return json({ method, markdown, ...(segments && { segments }), ...(bib && { bib }), ...(model && { model }), inputHash, outputHash, output, warnings } satisfies ExtractResponse)\n}\n\n/** Body schema of `transcriptCleanupPost`. */\nexport const transcriptCleanupBodySchema = transcriptCleanupRequestSchema\n\n/**\n * Cleans transcript segments (filler words, punctuation, casing, fragmented sentences) without touching speakers or\n * order: the model only returns a new text per segment ID. Plain JSON: `{ cleaned, failed }`.\n */\nexport async function transcriptCleanupPost(ctx: AiHandlerContext, request: HandlerRequest): Promise<HandlerResponse> {\n const user = await enforceRateLimit(ctx, request)\n const { segments, model } = await readBody(request, transcriptCleanupBodySchema)\n const languageModel = await ctx.ai.resolveModel(model, user)\n\n // Closing the menu or the page aborts the client's fetch; stop the upstream requests with it.\n const abort = new AbortController()\n request.signal?.addEventListener('abort', () => abort.abort(), { once: true })\n try {\n return json(await runTranscriptCleanup(segments, (messages, signal) => ctx.ai.streamChat({ model: languageModel, messages, signal }), abort.signal))\n }\n catch (error) {\n throw error instanceof TranscriptCleanupUnreadableError ? new HttpError(502, error.message) : error\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ai.js","names":[],"sources":["../../../src/server/handlers/ai.ts"],"sourcesContent":["import { createHash, randomUUID } from 'node:crypto'\nimport type { JSONContent } from '@tiptap/core'\nimport { jsonSchema, type ModelMessage, tool, type ToolSet } from 'ai'\nimport { z } from 'zod'\nimport {\n AGENT_LIMITS,\n AGENT_OP_OUTCOMES,\n AGENT_OP_TYPES,\n AGENT_TOOL,\n AI_ID_PATTERN,\n AI_TASKS,\n type AgentOp,\n type AgentOpProblem,\n agentOpTarget,\n agentRetryMessage,\n agentShownMessage,\n agentUnreadableProblem,\n aiBlockSchema,\n checkAgentOps,\n CHAT_LIMITS,\n type BlockLines,\n type ChatBlockModel,\n type ChatToolCall,\n completeBodySchema,\n documentChatMessages,\n documentChatSystem,\n generateBodySchema,\n generateProblem,\n type ModelAgentOp,\n modelRefSchema,\n readBlockLines,\n runCompletion,\n runGenerate,\n} from '../../features/ai/protocol'\nimport { blockExcerptText } from '../../core/block-snippet'\nimport { AGENT_OP_ID, type ChangeSetHunk } from '../../core/change-set'\nimport type { ExtractMethod, ExtractRequest, ExtractResponse } from '../../core/backend-types'\nimport { PROPERTY_TYPES } from '../../core/properties'\nimport { aiBlockTypesOf } from '../../features/composites/schema'\nimport { ASSET_DOCUMENT_ID, ASSET_NAME } from '../../features/media/schema'\nimport {\n runTranscriptCleanup,\n TranscriptCleanupUnreadableError,\n transcriptCleanupRequestSchema,\n} from '../../integrations/transcript-ai/protocol'\nimport { STRUCTURE_MODES } from '../../settings'\nimport { type ChatAgent, type ChatAgentEdit, documentEditTool } from '../chat-agent'\nimport { HttpError } from '../errors'\nimport { BIBLIOGRAPHY_PAGES, bibliographyText, describeImage, extractBibliography, isScanned, joinPages, MAX_OCR_PAGES, MAX_TRANSCRIBE_BYTES, ocrPages, pdfPagesText, RasterError, transcribe, type TranscriptionResult } from '../extract'\nimport { DOI_PATTERN } from '../crossref'\nimport { blockExcerptOf, blockLinesOfContent } from '../document-blocks'\nimport { openPdf } from '../pdf'\nimport { changeSetOwner } from '../../adapters/change-sets/owner'\nimport { mayEditDocument } from '../readable-documents'\nimport type { EditorPorts } from '../ports'\nimport { loadCurrentContent } from '../stored-content'\nimport { type AiHandlerContext, authenticated, enforceRateLimit, eventStream, type HandlerRequest, type HandlerResponse, json, port, readBody } from './context'\nimport { type ToolContext, toolRegistry, type ToolRegistry } from '../tools'\n\n/** What a context without `tools` offers the model: nothing beside `edit_document`. */\nconst NO_TOOLS = toolRegistry()\n\n/** Turns of the chat that may end in read-only tool calls before the model has to answer, and how many such calls one turn may run. */\nconst MAX_TOOL_ROUNDS = 5\nconst MAX_TOOL_CALLS_PER_ROUND = 8\nconst TOOL_CALLS_LIMIT_OUTPUT = `Nicht ausgeführt: höchstens ${MAX_TOOL_CALLS_PER_ROUND} Werkzeugaufrufe pro Antwort. Wiederhole diesen Aufruf, wenn du ihn noch brauchst.`\nconst EDIT_DEFERRED = `Nicht ausgeführt: In derselben Antwort fehlten noch die Ergebnisse der Lesewerkzeuge. Rufe ${AGENT_TOOL} erneut auf, wenn die Änderung noch passt.`\n\n/** The input of a tool call as the history keeps it: always an object. */\nconst toolInput = (input: unknown): Record<string, unknown> => typeof input === 'object' && input !== null && !Array.isArray(input) ? input as Record<string, unknown> : {}\n\n/** Runs one call of a registered tool and answers the text the model reads, cut to `CHAT_LIMITS.toolOutput`. A failure is that text too, so the model can go on without the tool. */\nasync function runTool(registry: ToolRegistry, context: ToolContext, call: { toolName: string, input: unknown }): Promise<string> {\n try {\n return (await registry.providers.get(call.toolName)!.handle(context, call.input)).slice(0, CHAT_LIMITS.toolOutput)\n }\n catch (error) {\n if (context.signal.aborted) throw error\n const { statusMessage, message } = error as { statusMessage?: string, message?: string }\n return `Fehler: ${statusMessage ?? message ?? 'Unbekannter Fehler'}`.slice(0, CHAT_LIMITS.toolOutput)\n }\n}\n\nconst blockId = z.string().min(1).max(100)\nconst opId = z.string().regex(AGENT_OP_ID)\nconst markdown = z.string().min(1).max(AGENT_LIMITS.markdown)\n/** A `convert` target: a `CONVERT_TARGET_IDS` entry, `typedBlock:<kind>`, or a host block id (`aiContext.blocks` with a conversion provider). */\nconst convertTarget = z.string().min(1).max(100)\n\n/** The Document an operation or a decision belongs to when it is not the open one. */\nconst documentId = z.string().min(1).max(200)\n\nconst opSchema = z.discriminatedUnion('type', [\n z.object({ op: opId, type: z.literal('update'), documentId: documentId.optional(), id: blockId, markdown }),\n z.object({ op: opId, type: z.literal('delete'), documentId: documentId.optional(), id: blockId }),\n z.object({ op: opId, type: z.literal('add'), documentId: documentId.optional(), afterId: blockId.nullable(), markdown }),\n z.object({ op: opId, type: z.literal('convert'), documentId: documentId.optional(), id: blockId, to: convertTarget }),\n])\n\nconst label = z.string().min(1).max(100)\nconst blockTypeSchema = z.object({\n // Same characters as the `::: <Block type>` fence opener accepts.\n id: z.string().regex(AI_ID_PATTERN).max(50),\n label,\n purpose: z.string().min(1).max(500).optional(),\n fields: z.array(z.object({\n key: z.string().min(1).max(50),\n label,\n type: z.enum(PROPERTY_TYPES),\n // A select can list every slot of a template (`@skaile/forge-editor-compose`), which runs past 50.\n options: z.array(z.object({ value: z.string().min(1).max(100), label })).max(500).optional(),\n })).max(20),\n})\n\n/** A read-only tool call of an earlier turn (the `tool` event the client kept): replayed to the model with its result. */\nconst toolCallSchema = z.object({\n name: z.string().min(1).max(64),\n input: z.record(z.string(), z.unknown()),\n output: z.string().max(CHAT_LIMITS.toolOutput),\n}) satisfies z.ZodType<ChatToolCall>\n\nconst messageSchema = z.discriminatedUnion('role', [\n z.object({\n role: z.literal('user'),\n content: z.string().min(1).max(CHAT_LIMITS.message),\n feedback: z.array(z.object({ op: opId, type: z.enum(AGENT_OP_TYPES), target: blockId.nullable(), outcome: z.enum(AGENT_OP_OUTCOMES), documentId: documentId.optional() })).max(500).optional(),\n }),\n z.object({\n role: z.literal('assistant'),\n content: z.string().max(CHAT_LIMITS.message),\n ops: z.array(opSchema).max(AGENT_LIMITS.ops).optional(),\n dropped: z.number().int().min(0).max(1000).optional(),\n tools: z.array(toolCallSchema).max(CHAT_LIMITS.tools).optional(),\n }).refine(m => m.content || m.ops?.length || m.dropped || m.tools?.length, 'Leere Antwort im Verlauf'),\n])\n\nexport const chatBodySchema = z.object({\n messages: z.array(messageSchema).min(1).max(CHAT_LIMITS.messages).refine(messages => messages.at(-1)!.role === 'user', 'Die letzte Nachricht muss von der Nutzerin bzw. dem Nutzer sein'),\n document: z.string().max(CHAT_LIMITS.document),\n /** The ID of the open Document: operations naming it (or no Document) are edits of the open Document, any other `documentId` aims at another Document of the workspace. */\n documentId: documentId.optional(),\n /** Block IDs edits are limited to (the selected blocks); null or missing: the whole document. */\n scope: z.array(blockId).min(1).max(AGENT_LIMITS.scope).nullish(),\n /** Number of the first new operation (`op<n>`), so IDs stay unique across the conversation. */\n nextOp: z.number().int().min(1).max(1_000_000).default(1),\n model: modelRefSchema,\n /** The editor's Block types (`blockTypesOf`), for the block model in the system prompt. Ignored when the server resolves them itself (`AiHandlerContext.blockTypes`). */\n blockTypes: z.array(blockTypeSchema).max(50),\n /** The editor's host blocks (`aiContext.blocks`), for the block model in the system prompt. */\n blocks: z.array(aiBlockSchema).max(50).default([]),\n /** Block types `convert` may turn a block into. */\n targets: z.array(convertTarget).max(100),\n /** The user's preferred structure of new content (settings `blocks.structure`). */\n structure: z.enum(STRUCTURE_MODES),\n})\n\n/**\n * The tools of one chat turn: `edit_document` (`documentEditTool`, deliberately loose, so malformed calls reach `checkAgentOps`\n * and get a precise retry message) and, with `reads`, the registry's read-only tools. With `workspace` (the host keeps change\n * sets and a mutator) the operations of `edit_document` may name another Document by `documentId`.\n */\nfunction agentTools(registry: ToolRegistry, { workspace, reads }: { workspace: boolean, reads: boolean }): ToolSet {\n const edit = documentEditTool({ workspace })\n return {\n [AGENT_TOOL]: tool({ description: edit.description, inputSchema: jsonSchema<{ ops: unknown[] }>(edit.inputSchema) }),\n ...(reads && registry.tools),\n }\n}\n\ntype ChatBody = z.infer<typeof chatBodySchema>\n\n/** Checks a chat request beyond its schema (400 for an empty message or a selection without blocks of the document) and answers where its operations may point (`checkAgentOps`). */\nfunction opContextOf({ messages, document, scope, targets }: ChatBody): { lines: BlockLines, allowed: Set<string> | null, targets: Set<string> } {\n if (!messages.at(-1)!.content.trim()) throw new HttpError(400, 'Nachricht fehlt')\n const lines = readBlockLines(document)\n const allowed = scope ? new Set(scope.filter(id => lines.ids.has(id))) : null\n if (allowed && !allowed.size) throw new HttpError(400, 'Die Auswahl enthält keine Blöcke des Dokuments')\n return { lines, allowed, targets: new Set(targets) }\n}\n\n/** A Document an operation names besides the open one, as the user may edit it: its content (live first) and block lines. */\ntype NamedDocument = { content: JSONContent, lines: BlockLines }\n\n/** Loads the Documents operations name, once per request (the model's read and its edit see the same content); `null` for one the user may not edit or that does not exist. */\nfunction namedDocumentLoader(ctx: AiHandlerContext, user: object, toolContext: ToolContext): (id: string) => Promise<NamedDocument | null> {\n const loaded = new Map<string, Promise<NamedDocument | null>>()\n return (id) => {\n let entry = loaded.get(id)\n if (!entry) {\n entry = (async () => {\n if (!await mayEditDocument(ctx.ports, user, id)) return null\n const content = toolContext.liveContent(id) ?? await loadCurrentContent(ctx.ports.documents, id)\n return content && { content, lines: blockLinesOfContent(content) }\n })()\n loaded.set(id, entry)\n }\n return entry\n }\n}\n\n/** The lines of the other Documents `raw` names, as far as the user may edit them; empty without `documentOf` (no change sets: such an operation is a `document` problem). */\nasync function namedDocumentLines(raw: readonly unknown[], openDocumentId: string | undefined, documentOf: ((id: string) => Promise<NamedDocument | null>) | null): Promise<Map<string, BlockLines>> {\n const documents = new Map<string, BlockLines>()\n if (!documentOf) return documents\n const named = new Set(raw.flatMap(op => typeof op === 'object' && op !== null && 'documentId' in op && typeof op.documentId === 'string' ? [op.documentId.trim()] : []))\n named.delete(openDocumentId ?? '')\n // At most one Document per operation; the limit of operations bounds the loads.\n for (const id of [...named].slice(0, AGENT_LIMITS.ops)) {\n const entry = id ? await documentOf(id) : null\n if (entry) documents.set(id, entry.lines)\n }\n return documents\n}\n\n/**\n * Streams the operations of a turn, numbered from `op<nextOp>`: the open Document's as `ops` (the editor's in-document review),\n * the others' as one stored `change-set`. Without an answer text and without a valid operation the user reads that none was possible.\n */\nasync function emitProposal(emit: (event: string, data: string) => void, proposal: {\n all: AgentOp[]\n dropped: AgentOpProblem[]\n retries: number\n answered: boolean\n user: object\n changeSets: EditorPorts['changeSets']\n documentOf: (id: string) => Promise<NamedDocument | null>\n}): Promise<void> {\n const { all, dropped, retries, answered, user, changeSets, documentOf } = proposal\n const ops = all.filter(op => op.documentId === undefined)\n const foreign = all.filter(op => op.documentId !== undefined)\n if (!answered && !all.length) {\n emit('delta', JSON.stringify({ text: 'Ich konnte keine gültigen Änderungen vorschlagen.' }))\n }\n if (foreign.length) {\n const hunks: ChangeSetHunk[] = await Promise.all(foreign.map(async (op): Promise<ChangeSetHunk> => {\n const documentId = op.documentId!\n const target = agentOpTarget(op)\n const entry = await documentOf(documentId)\n const excerpt = target === null || !entry ? null : blockExcerptOf(entry.content, target)\n return { op: { ...op, documentId }, status: 'pending', target: excerpt && { type: excerpt.type, text: blockExcerptText(excerpt.text) } }\n }))\n emit('change-set', JSON.stringify(await changeSets!.create({ by: changeSetOwner(user), hunks })))\n }\n emit('ops', JSON.stringify({ ops, dropped, retries }))\n}\n\n/** `final` and the problems of earlier attempts whose block no operation of `all` targets: an operation a corrected list leaves out counts as dropped too. */\nfunction droppedOf(all: readonly AgentOp[], final: readonly AgentOpProblem[], earlier: readonly AgentOpProblem[]): AgentOpProblem[] {\n const targets = new Set(all.map(op => agentOpTarget(op)))\n const dropped = [...final]\n for (const problem of earlier) {\n if (problem.id !== null && !targets.has(problem.id) && !dropped.some(d => d.id === problem.id && d.type === problem.type)) dropped.push(problem)\n }\n return dropped\n}\n\n/**\n * Document chat: answers the last message of `messages` about `document`, or proposes edit operations through the\n * `edit_document` tool. Operations pointing to IDs that are unknown or outside `scope`, an `add` without `afterId`, an\n * operation inside a block that another operation replaces or deletes, or converting into a type not in\n * `targets`, make the model retry (at most `AGENT_LIMITS.retries` times); what is still invalid then, or left out by a\n * corrected list, is dropped. The system prompt describes the editor's block model from `blockTypes`, `blocks` and `targets`. SSE like\n * `/api/ai/generate`: `delta` (text), `ops` (`{ ops: AgentOp[], dropped: AgentOpProblem[], retries: number }`, once,\n * before `done`), `change-set` (the stored `ChangeSet`, once, when operations aimed at other Documents), `done`, `error`.\n * Operations of the open Document (`documentId` absent or the body's) are `ops`; operations naming another Document the\n * user may edit (`mayEditDocument`) become one `ChangeSet` (`ChangeSetStore`) for the user to review per Document, when the\n * host has the `changeSets` port and a mutator; else such an operation is a `document` problem the model retries. All\n * operations share one numbering (`op<n>`). With a host agent (`AiHandlerContext.chatAgent` answers one for the user) the\n * agent answers the turn in place of the model loop (`agentChat`), with the same checks and events.\n */\nexport async function chatPost(ctx: AiHandlerContext, request: HandlerRequest): Promise<HandlerResponse> {\n const user = await enforceRateLimit(ctx, request)\n const body = await readBody(request, chatBodySchema)\n const { messages, document, documentId: openDocumentId, nextOp, model, blockTypes: clientBlockTypes, blocks, targets, structure } = body\n const blockModel: ChatBlockModel = { blockTypes: ctx.blockTypes ? aiBlockTypesOf(ctx.blockTypes) : clientBlockTypes, blocks, targets, structure }\n const registry = ctx.tools ?? NO_TOOLS\n // Operations may aim at other Documents only where accepting them is possible: stored change sets and the mutator that applies them.\n const changeSets = ctx.mutator ? ctx.ports.changeSets : undefined\n const agent = (await ctx.chatAgent?.(user)) ?? null\n if (agent) return agentChat(ctx, request, agent, { user, body, blockModel, context: opContextOf(body), registry, changeSets })\n\n // Unknown preset or missing key must fail as a plain JSON error before the SSE response starts.\n const languageModel = await ctx.ai.resolveModel(model, user)\n const context = opContextOf(body)\n const { allowed } = context\n const workspace = changeSets !== undefined\n const editTools = agentTools(registry, { workspace, reads: false })\n const allTools = agentTools(registry, { workspace, reads: true })\n\n return eventStream(request, async (emit, signal) => {\n try {\n let conversation: ModelMessage[] = documentChatMessages(messages, document, allowed && [...allowed], blockModel, registry.infos)\n let answered = false\n let result: { valid: ModelAgentOp[], problems: AgentOpProblem[] } | null = null\n // Problems of attempts that were retried: an operation the corrected list leaves out counts as dropped too.\n const retried: AgentOpProblem[] = []\n let retries = 0\n // Turns that ended in read-only tool calls: the server ran them and the model answers again.\n let rounds = 0\n const toolContext: ToolContext = { ports: ctx.ports, user, signal, liveContent: id => ctx.collab?.liveContent(id) ?? null, ...(openDocumentId && { documentId: openDocumentId }) }\n // The other Documents the operations name, as far as the user may edit them: loaded once per request (the model's read and its edit see the same content).\n const documentOf = namedDocumentLoader(ctx, user, toolContext)\n for (;;) {\n // `edit_document` calls are collected for checking; calls of the registered tools are run right after the turn.\n const calls: { toolCallId: string, input: unknown, invalid: boolean }[] = []\n const reads: { toolCallId: string, toolName: string, input: unknown }[] = []\n const retry = retries > 0\n let text = ''\n for await (const part of ctx.ai.streamChatTurn({\n model: languageModel,\n messages: conversation,\n // Retries only correct the operations: no second answer text, nothing to read.\n tools: retry || rounds >= MAX_TOOL_ROUNDS ? editTools : allTools,\n toolChoice: retry ? { type: 'tool', toolName: AGENT_TOOL } : 'auto',\n signal,\n })) {\n if (part.type === 'tool-call') {\n if (part.toolName === AGENT_TOOL) calls.push(part)\n else if (registry.providers.has(part.toolName)) reads.push(part)\n }\n else if (!retry) {\n // The text of a later turn is a new paragraph of the answer.\n if (!text && answered) emit('delta', JSON.stringify({ text: '\\n\\n' }))\n text += part.text\n answered = true\n emit('delta', JSON.stringify({ text: part.text }))\n }\n }\n\n if (reads.length && !retry && rounds < MAX_TOOL_ROUNDS) {\n rounds++\n const outputs = await Promise.all(reads.map((call, i) => i < MAX_TOOL_CALLS_PER_ROUND ? runTool(registry, toolContext, call) : TOOL_CALLS_LIMIT_OUTPUT))\n reads.forEach((call, i) => emit('tool', JSON.stringify({ name: call.toolName, input: toolInput(call.input), output: outputs[i]! } satisfies ChatToolCall)))\n conversation = [\n ...conversation,\n {\n role: 'assistant',\n content: [\n ...(text ? [{ type: 'text' as const, text }] : []),\n ...reads.map(call => ({ type: 'tool-call' as const, toolCallId: call.toolCallId, toolName: call.toolName, input: call.input ?? {} })),\n ...calls.map(call => ({ type: 'tool-call' as const, toolCallId: call.toolCallId, toolName: AGENT_TOOL, input: call.input ?? {} })),\n ],\n },\n {\n role: 'tool',\n content: [\n ...reads.map((call, i) => ({ type: 'tool-result' as const, toolCallId: call.toolCallId, toolName: call.toolName, output: { type: 'text' as const, value: outputs[i]! } })),\n // An edit proposed in the same turn was written before its context was complete: not applied.\n ...calls.map(call => ({ type: 'tool-result' as const, toolCallId: call.toolCallId, toolName: AGENT_TOOL, output: { type: 'text' as const, value: EDIT_DEFERRED } })),\n ],\n },\n ]\n continue\n }\n if (!calls.length) break\n\n const unreadable = calls.some(call => call.invalid || !Array.isArray((call.input as { ops?: unknown } | null)?.ops))\n const raw = calls.flatMap(call => (call.input as { ops?: unknown } | null)?.ops).filter(op => op !== undefined)\n const documents = await namedDocumentLines(raw, openDocumentId, changeSets ? documentOf : null)\n const checked = checkAgentOps(raw, { ...context, ...(openDocumentId && { documentId: openDocumentId }), documents })\n if (unreadable) checked.problems.unshift(agentUnreadableProblem())\n result = checked\n if (!checked.problems.length || retries === AGENT_LIMITS.retries) break\n\n retried.push(...checked.problems)\n retries++\n const correction = agentRetryMessage(checked.problems, !!allowed)\n conversation = [\n ...conversation,\n {\n role: 'assistant',\n content: [\n ...(text ? [{ type: 'text' as const, text }] : []),\n ...calls.map(call => ({ type: 'tool-call' as const, toolCallId: call.toolCallId, toolName: AGENT_TOOL, input: call.input ?? {} })),\n ],\n },\n { role: 'tool', content: calls.map(call => ({ type: 'tool-result' as const, toolCallId: call.toolCallId, toolName: AGENT_TOOL, output: { type: 'text' as const, value: correction } })) },\n ]\n }\n\n if (result) {\n const all: AgentOp[] = result.valid.map((op, i) => ({ ...op, op: `op${nextOp + i}` }))\n await emitProposal(emit, { all, dropped: droppedOf(all, result.problems, retried), retries, answered, user, changeSets, documentOf })\n }\n emit('done', '{}')\n }\n catch (error) {\n if (!signal.aborted) {\n const { statusMessage, message } = error as { statusMessage?: string, message?: string }\n emit('error', JSON.stringify({ message: statusMessage ?? message ?? 'Unbekannter Fehler' }))\n }\n }\n })\n}\n\n/**\n * The chat turn of a host's `ChatAgent`: the agent gets the request as `ChatAgentTurn` (snapshot, scope, block model, the\n * read-only tools with their context, the built-in system prompt and `edit_document`) and streams text, the tool calls it ran\n * and the operations `checkEdit` accepted. They become the built-in loop's events: `delta`, `tool`, then once the proposal of\n * all edits (`change-set` for other Documents, `ops` numbered from `nextOp`), `done`; a thrown error is `error`. `checkEdit`\n * checks each `edit_document` call against the operations of the earlier calls of the turn too (the limit, a block edited twice,\n * an operation inside a block an earlier one replaces), and numbers on from them.\n */\nfunction agentChat(ctx: AiHandlerContext, request: HandlerRequest, agent: ChatAgent, chat: {\n user: object\n body: ChatBody\n blockModel: ChatBlockModel\n context: { lines: BlockLines, allowed: Set<string> | null, targets: Set<string> }\n registry: ToolRegistry\n changeSets: EditorPorts['changeSets']\n}): HandlerResponse {\n const { user, body: { messages, document, documentId: openDocumentId, nextOp }, blockModel, context, registry, changeSets } = chat\n return eventStream(request, async (emit, signal) => {\n try {\n const toolContext: ToolContext = { ports: ctx.ports, user, signal, liveContent: id => ctx.collab?.liveContent(id) ?? null, ...(openDocumentId && { documentId: openDocumentId }) }\n const documentOf = namedDocumentLoader(ctx, user, toolContext)\n // The operations earlier `edit_document` calls of this turn had accepted, as the model wrote them (already shown for review).\n const shown: ModelAgentOp[] = []\n const checkEdit = async (input: unknown): Promise<ChatAgentEdit> => {\n const value = typeof input === 'object' && input !== null && 'ops' in input ? input.ops : undefined\n const raw = [...shown, ...(Array.isArray(value) ? value.filter(op => op !== undefined) : [])]\n const documents = await namedDocumentLines(raw, openDocumentId, changeSets ? documentOf : null)\n const checked = checkAgentOps(raw, { ...context, ...(openDocumentId && { documentId: openDocumentId }), documents })\n // The earlier operations come first and pass as before; one that a new operation now covers stays shown (the review marks the overlap).\n const covered = checked.problems.filter(problem => problem.index <= shown.length).length\n const fresh = checked.valid.slice(shown.length - covered)\n const problems = checked.problems.filter(problem => problem.index > shown.length).map(problem => ({ ...problem, index: problem.index - shown.length }))\n if (!Array.isArray(value)) problems.unshift(agentUnreadableProblem())\n const ops: AgentOp[] = fresh.map((op, i) => ({ ...op, op: `op${nextOp + shown.length + i}` }))\n shown.push(...fresh)\n const message = agentShownMessage(ops, 0)\n return { ops, problems, feedback: problems.length ? `${message}\\n${agentRetryMessage(problems, !!context.allowed, { partial: true })}` : message }\n }\n\n const proposed: AgentOp[] = []\n // The problems of each `edit_document` call the agent reported, in order.\n const attempts: AgentOpProblem[][] = []\n let answered = false\n for await (const part of agent.turn({\n user,\n ...(openDocumentId && { documentId: openDocumentId }),\n messages,\n document,\n scope: context.allowed && [...context.allowed],\n blockModel,\n tools: registry,\n toolContext,\n system: documentChatSystem(blockModel, registry.infos),\n editTool: documentEditTool({ workspace: changeSets !== undefined }),\n checkEdit,\n signal,\n })) {\n if (part.type === 'text') {\n if (!part.text) continue\n answered = true\n emit('delta', JSON.stringify({ text: part.text }))\n }\n else if (part.type === 'tool') {\n emit('tool', JSON.stringify({ name: part.call.name, input: toolInput(part.call.input), output: part.call.output.slice(0, CHAT_LIMITS.toolOutput) } satisfies ChatToolCall))\n }\n else {\n proposed.push(...part.ops)\n attempts.push(part.dropped)\n }\n }\n\n if (attempts.length) {\n const all: AgentOp[] = proposed.map((op, i) => ({ ...op, op: `op${nextOp + i}` }))\n // A call after one with problems is a retry; the last call's problems are dropped, an earlier call's only when no operation took up its block.\n const retries = attempts.slice(1).filter((_, i) => attempts[i]!.length > 0).length\n await emitProposal(emit, { all, dropped: droppedOf(all, attempts.at(-1)!, attempts.slice(0, -1).flat()), retries, answered, user, changeSets, documentOf })\n }\n emit('done', '{}')\n }\n catch (error) {\n if (!signal.aborted) {\n const { statusMessage, message } = error as { statusMessage?: string, message?: string }\n emit('error', JSON.stringify({ message: statusMessage ?? message ?? 'Unbekannter Fehler' }))\n }\n }\n })\n}\n\n// The body schemas live with the protocol (the browser-direct adapter validates with them too); the handlers keep exporting them.\nexport { completeBodySchema, generateBodySchema }\n\n/** Short inline continuation (ghost text) of the text before the caret. Plain JSON: `{ text }`, empty when nothing fits. */\nexport async function completePost(ctx: AiHandlerContext, request: HandlerRequest): Promise<HandlerResponse> {\n const user = await enforceRateLimit(ctx, request)\n const { before, model } = await readBody(request, completeBodySchema)\n const languageModel = await ctx.ai.resolveModel(model, user)\n\n // Typing on the client aborts its fetch; stop the upstream request with it.\n const abort = new AbortController()\n request.signal?.addEventListener('abort', () => abort.abort(), { once: true })\n return json({ text: await runCompletion(before, (messages, signal) => ctx.ai.streamChat({ model: languageModel, messages, signal }), abort.signal) })\n}\n\n/** Streams one text task (rewrite, translate, generate, …) as SSE: `delta` (`{ text }`), `done`, `error` (`{ message }`). */\nexport async function generatePost(ctx: AiHandlerContext, request: HandlerRequest): Promise<HandlerResponse> {\n const user = await enforceRateLimit(ctx, request)\n const { task: taskId, model, ...input } = await readBody(request, generateBodySchema)\n // Unknown preset or missing key must fail as a plain JSON error before the SSE response starts.\n const languageModel = await ctx.ai.resolveModel(model, user)\n const task = AI_TASKS[taskId]\n const problem = generateProblem(task, input)\n if (problem) throw new HttpError(400, problem)\n const users = task.needsUsers ? (await port(ctx, 'directory').listUsers()).map(u => u.username) : undefined\n\n return eventStream(request, async (emit, signal) => {\n for await (const { event, data } of runGenerate(task, input, users, (messages, streamSignal) => ctx.ai.streamChat({ model: languageModel, messages, signal: streamSignal }), signal)) {\n emit(event, JSON.stringify(data))\n }\n })\n}\n\n/**\n * The models the client may choose from (without keys) — the `models` port's `listModels` for the user, else the\n * server-defined presets — and the default preset id per model role, for the client's model profiles. Requires an\n * authenticated user through the `identity` port; not rate limited, since it calls no model.\n */\nexport async function modelsGet(ctx: AiHandlerContext, request: HandlerRequest): Promise<HandlerResponse> {\n const user = await authenticated(ctx, request)\n return json({ presets: await ctx.ai.listModels(user), roles: ctx.ai.roles })\n}\n\nconst sha256Hex = (data: string | Uint8Array): string => createHash('sha256').update(data).digest('hex')\n\n/** PDF pages read by `pdf-text` and `ocr` unless the request caps them lower. */\nconst DEFAULT_MAX_PAGES = 200\n\n/** Body schema of `extractPost`. */\nexport const extractBodySchema = z.object({\n documentId: z.string().regex(ASSET_DOCUMENT_ID),\n name: z.string().regex(ASSET_NAME).optional(),\n method: z.enum(['auto', 'transcription', 'pdf-text', 'ocr', 'description', 'bibliography']),\n model: modelRefSchema,\n bibliography: z.boolean().optional(),\n doi: z.string().regex(DOI_PATTERN).optional(),\n maxPages: z.number().int().min(1).max(500).default(DEFAULT_MAX_PAGES),\n}).refine(body => body.name !== undefined || (body.method === 'bibliography' && body.doi !== undefined), {\n message: 'name fehlt (nur eine Literaturabfrage mit DOI braucht keine Datei)',\n path: ['name'],\n}) satisfies z.ZodType<ExtractRequest>\n\n/** The method an `ExtractRequest` runs for an asset of `mimeType`: `auto` picks by type; 415 when the type does not fit the method. */\nfunction extractMethodOf(requested: ExtractMethod, mimeType: string, hasDoi: boolean): ExtractResponse['method'] {\n const audioVideo = /^(?:audio|video)\\//.test(mimeType)\n const image = mimeType.startsWith('image/')\n const pdf = mimeType === 'application/pdf'\n const fits: Record<ExtractMethod, boolean> = {\n 'auto': audioVideo || image || pdf,\n 'transcription': audioVideo,\n 'description': image,\n 'pdf-text': pdf,\n 'ocr': pdf,\n // A given DOI needs no document text, so any asset can stand for the work.\n 'bibliography': pdf || hasDoi,\n }\n if (!fits[requested]) throw new HttpError(415, `Dateityp ${mimeType} wird für diese Auswertung nicht unterstützt`)\n if (requested !== 'auto') return requested\n if (audioVideo) return 'transcription'\n return image ? 'description' : 'pdf-text'\n}\n\n/**\n * Turns a stored asset into readable content (`ExtractRequest` → `ExtractResponse`): audio/video into a transcription\n * with segment timestamps, a PDF into its text per page (OCR through the model when it has no text layer), an image\n * into a description, bibliographic data from Crossref or the model. The raw Markdown is stored as an asset of the same\n * document (`output`), with the SHA-256 of input and output. Errors: 404 unknown asset, 415 type not supported, 413\n * oversize audio, 422 unreadable PDF, 501 transcription or OCR unavailable, 502 upstream failure.\n */\nexport async function extractPost(ctx: AiHandlerContext, request: HandlerRequest): Promise<HandlerResponse> {\n const user = await enforceRateLimit(ctx, request)\n const body = await readBody(request, extractBodySchema)\n if (body.name === undefined) {\n // Only a DOI lookup comes without a file (the schema allows nothing else): no asset, no output, empty hashes.\n const { bib, warnings } = await extractBibliography({ doi: body.doi, text: '', resolveModel: () => ctx.ai.resolveModel(body.model, user), signal: request.signal })\n return json({ method: 'bibliography', markdown: '', bib, inputHash: '', outputHash: sha256Hex(''), output: null, warnings } satisfies ExtractResponse)\n }\n const assets = port(ctx, 'assets')\n const asset = await assets.loadAsset(body.documentId, body.name)\n if (!asset) throw new HttpError(404, 'Datei nicht gefunden')\n const inputHash = sha256Hex(asset.data)\n let method = extractMethodOf(body.method, asset.record.mimeType, body.doi !== undefined)\n // Closing the page aborts the client's fetch; the upstream requests stop with it.\n const { signal } = request\n const resolveModel = () => ctx.ai.resolveModel(body.model, user)\n\n const warnings: string[] = []\n let markdown = ''\n let model: string | undefined\n let segments: TranscriptionResult['segments']\n let bib: ExtractResponse['bib']\n\n if (method === 'transcription') {\n const config = ctx.ai.transcription\n if (!config?.endpoint || !config.model) throw new HttpError(501, 'Transkription nicht konfiguriert')\n if (asset.data.byteLength > MAX_TRANSCRIBE_BYTES) throw new HttpError(413, `Datei ist größer als ${MAX_TRANSCRIBE_BYTES / 1024 / 1024} MB`)\n ;({ markdown, segments } = await transcribe(config, asset, signal))\n model = config.model\n }\n else if (method === 'description') {\n markdown = await describeImage(await resolveModel(), asset.data, asset.record.mimeType, signal)\n }\n else if (asset.record.mimeType !== 'application/pdf') {\n // `bibliography` of a non-PDF asset: only reachable with a DOI.\n const found = await extractBibliography({ doi: body.doi, text: '', resolveModel, signal })\n bib = found.bib\n warnings.push(...found.warnings)\n }\n else {\n const pdf = await openPdf(asset.data)\n try {\n if (method === 'bibliography') {\n const pages = await pdfPagesText(pdf, Math.min(pdf.pageCount, BIBLIOGRAPHY_PAGES), signal)\n const found = await extractBibliography({ doi: body.doi, text: bibliographyText(pages), resolveModel, signal })\n bib = found.bib\n warnings.push(...found.warnings)\n }\n else {\n const count = Math.min(pdf.pageCount, body.maxPages)\n if (pdf.pageCount > count) warnings.push('pages-truncated')\n let pages = await pdfPagesText(pdf, count, signal)\n if (body.method === 'auto' && isScanned(pages)) method = 'ocr'\n if (method === 'ocr') {\n // One vision call per page: the OCR reads fewer pages than the text layer would.\n const ocrCount = Math.min(count, MAX_OCR_PAGES)\n try {\n pages = await ocrPages(pdf, ocrCount, resolveModel, signal)\n if (ocrCount < count) warnings.push('ocr-truncated')\n }\n catch (error) {\n if (!(error instanceof RasterError)) throw error\n // Pages cannot be rendered here: keep what the text layer has.\n if (!pages.some(Boolean)) throw new HttpError(501, 'OCR nicht verfügbar')\n warnings.push('ocr-unavailable')\n method = 'pdf-text'\n }\n }\n markdown = joinPages(pages)\n if (body.bibliography) {\n try {\n const found = await extractBibliography({ doi: body.doi, text: bibliographyText(pages), resolveModel, signal })\n bib = found.bib\n warnings.push(...found.warnings)\n }\n catch (error) {\n // The text is the result; missing bibliographic data must not lose it.\n if (signal?.aborted) throw error\n warnings.push('bibliography-failed')\n }\n }\n }\n }\n finally {\n await pdf.close()\n }\n }\n\n const outputHash = sha256Hex(markdown)\n let output: string | null = null\n if (method !== 'bibliography' && markdown) {\n const data = Buffer.from(markdown, 'utf8')\n output = `${randomUUID()}.md`\n await assets.storeAsset(body.documentId, {\n name: output,\n filename: `${asset.record.filename}.${method}.md`.slice(0, 255),\n mimeType: 'text/markdown',\n size: data.byteLength,\n createdAt: new Date().toISOString(),\n sha256: outputHash,\n data,\n })\n }\n return json({ method, markdown, ...(segments && { segments }), ...(bib && { bib }), ...(model && { model }), inputHash, outputHash, output, warnings } satisfies ExtractResponse)\n}\n\n/** Body schema of `transcriptCleanupPost`. */\nexport const transcriptCleanupBodySchema = transcriptCleanupRequestSchema\n\n/**\n * Cleans transcript segments (filler words, punctuation, casing, fragmented sentences) without touching speakers or\n * order: the model only returns a new text per segment ID. Plain JSON: `{ cleaned, failed }`.\n */\nexport async function transcriptCleanupPost(ctx: AiHandlerContext, request: HandlerRequest): Promise<HandlerResponse> {\n const user = await enforceRateLimit(ctx, request)\n const { segments, model } = await readBody(request, transcriptCleanupBodySchema)\n const languageModel = await ctx.ai.resolveModel(model, user)\n\n // Closing the menu or the page aborts the client's fetch; stop the upstream requests with it.\n const abort = new AbortController()\n request.signal?.addEventListener('abort', () => abort.abort(), { once: true })\n try {\n return json(await runTranscriptCleanup(segments, (messages, signal) => ctx.ai.streamChat({ model: languageModel, messages, signal }), abort.signal))\n }\n catch (error) {\n throw error instanceof TranscriptCleanupUnreadableError ? new HttpError(502, error.message) : error\n }\n}\n"],"mappings":"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"}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { BlockType } from "../../features/composites/block-types.js";
|
|
2
2
|
import { EditorPorts } from "../ports.js";
|
|
3
3
|
import { AiModels } from "../ai.js";
|
|
4
|
+
import { ToolRegistry } from "../tools.js";
|
|
5
|
+
import { ChatAgent } from "../chat-agent.js";
|
|
4
6
|
import { CollabServer } from "../collab.js";
|
|
5
7
|
import { DocumentMutator } from "../document-mutator.js";
|
|
6
8
|
import { RateLimiter } from "../rate-limit.js";
|
|
7
9
|
import { SpeechBudget } from "../speech-budget.js";
|
|
8
|
-
import { ToolRegistry } from "../tools.js";
|
|
9
10
|
import "../../features/composites/schema.js";
|
|
10
11
|
//#region src/server/handlers/context.d.ts
|
|
11
12
|
/** What every editor handler needs: the ports of the backend (identity first). */
|
|
@@ -31,6 +32,8 @@ export interface AiHandlerContext extends HandlerContext {
|
|
|
31
32
|
limiter: RateLimiter;
|
|
32
33
|
/** The document chat's read-only tools besides `edit_document`, built once with `toolRegistry(EditorServerOptions.tools)` (e.g. of `createWorkspaceTools()`). The model may call them; the server runs them and feeds the results back before the next turn. */
|
|
33
34
|
tools?: ToolRegistry;
|
|
35
|
+
/** The host's agent for the document chat (`EditorServerOptions.chatAgent`), resolved per request: an agent answers the turn in place of the built-in model loop, `null` keeps the loop. */
|
|
36
|
+
chatAgent?: (user: object) => ChatAgent | null | Promise<ChatAgent | null>;
|
|
34
37
|
}
|
|
35
38
|
/** A request, reduced to what the handlers read: any HTTP server maps its own request type onto it. */
|
|
36
39
|
export interface HandlerRequest {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"context.js","names":[],"sources":["../../../src/server/handlers/context.ts"],"sourcesContent":["import type { BlockType } from '../../features/composites/schema'\nimport type { AiModels } from '../ai'\nimport type { EditorPorts } from '../ports'\nimport type { CollabServer } from '../collab'\nimport type { DocumentMutator } from '../document-mutator'\nimport { HttpError } from '../errors'\nimport type { RateLimiter } from '../rate-limit'\nimport { userKey } from '../rate-limit'\nimport type { SpeechBudget } from '../speech-budget'\nimport type { ToolRegistry } from '../tools'\n\n/** What every editor handler needs: the ports of the backend (identity first). */\nexport interface HandlerContext {\n ports: EditorPorts\n /** The collaboration server of the process: documents open in it count for `tagsGet` with their unsaved content. */\n collab?: CollabServer\n /** Applies block operations to a Document (`POST /api/editor/documents/:id/ops`); without it the route answers 501. */\n mutator?: DocumentMutator\n /** What the installed server really serves (`capabilities` answers with it); without it, what `ports` imply. */\n served?: { ai?: boolean, collaboration?: boolean }\n /** The daily character budget of the speech route (`createSpeechBudget`); without it one budget of `SPEECH_CHARS_PER_DAY` per user and UTC day is kept per `ports` object. */\n speechBudget?: SpeechBudget\n /** The Block types of the host's config (`EditorServerOptions.blockTypes`). The query route reads their Property definitions to type the fields of a query; the chat describes them to the model and, when they are given, ignores the `blockTypes` the client sends. */\n blockTypes?: readonly BlockType[]\n}\n\n/** Context of the AI handlers: the configured models and the per-user limit shared by all of them. */\nexport interface AiHandlerContext extends HandlerContext {\n ai: AiModels\n limiter: RateLimiter\n /** The document chat's read-only tools besides `edit_document`, built once with `toolRegistry(EditorServerOptions.tools)` (e.g. of `createWorkspaceTools()`). The model may call them; the server runs them and feeds the results back before the next turn. */\n tools?: ToolRegistry\n}\n\n/** A request, reduced to what the handlers read: any HTTP server maps its own request type onto it. */\nexport interface HandlerRequest {\n headers: Headers\n /** Route parameters (`id`, `name`). */\n params?: Readonly<Record<string, string | undefined>>\n /** Query string values (`q`). */\n query?: Readonly<Record<string, string | undefined>>\n /** The JSON body. Lazy, so a handler authenticates (401) before it reads or validates anything. Rejects when the body is no JSON. */\n readJson?: () => Promise<unknown>\n /** The multipart body; `null` when it is none. Lazy like `readJson`. */\n readForm?: () => Promise<FormData | null>\n /** Aborts when the client goes away: handlers stop upstream requests with it. */\n signal?: AbortSignal\n}\n\n/** A response, as plain data any HTTP server can send (`new Response(body, { status, headers })` maps it 1:1). */\nexport interface HandlerResponse {\n status: number\n headers: Record<string, string>\n body: string | Uint8Array | ReadableStream<Uint8Array> | null\n}\n\nexport const json = (value: unknown, status = 200): HandlerResponse => ({\n status,\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify(value),\n})\n\nexport const text = (value: string, contentType: string, headers: Record<string, string> = {}): HandlerResponse => ({\n status: 200,\n headers: { 'content-type': contentType, ...headers },\n body: value,\n})\n\nexport const empty = (status = 204, headers: Record<string, string> = {}): HandlerResponse => ({ status, headers, body: null })\n\n/** The first language tag of the `Accept-Language` header that `Intl` knows (`de-DE,de;q=0.9` → `de-DE`, canonical); `*` and tags that are none are skipped; undefined when there is no usable one. */\nexport function acceptedLocaleOf(request: HandlerRequest): string | undefined {\n for (const part of (request.headers.get('accept-language') ?? '').split(',')) {\n const tag = part.split(';')[0]!.trim()\n if (!tag || tag === '*') continue\n try {\n return Intl.getCanonicalLocales(tag)[0]\n }\n catch { /* not a language tag: the next one */ }\n }\n return undefined\n}\n\n/**\n * Server-sent events. `run` writes events through `emit` and ends the stream by returning; `signal` aborts when the\n * client cancels the response or `request.signal` aborts. An `emit` after that is dropped.\n */\nexport function eventStream(request: HandlerRequest, run: (emit: (event: string, data: string) => void, signal: AbortSignal) => Promise<void>): HandlerResponse {\n const abort = new AbortController()\n if (request.signal?.aborted) abort.abort()\n else request.signal?.addEventListener('abort', () => abort.abort(), { once: true })\n const encoder = new TextEncoder()\n return {\n status: 200,\n headers: { 'content-type': 'text/event-stream', 'cache-control': 'no-cache, no-transform', 'x-accel-buffering': 'no' },\n body: new ReadableStream<Uint8Array>({\n start(controller) {\n const emit = (event: string, data: string) => {\n if (!abort.signal.aborted) controller.enqueue(encoder.encode(`event: ${event}\\ndata: ${data}\\n\\n`))\n }\n void run(emit, abort.signal)\n .catch((error) => { if (!abort.signal.aborted) console.error('[editor-server]', error) })\n .finally(() => { try { controller.close() } catch { /* the client cancelled */ } })\n },\n cancel() { abort.abort() },\n }),\n }\n}\n\n/** The user `ports.identity.authenticate` finds behind the request's headers; 401 without one. */\nexport async function authenticated({ ports }: HandlerContext, request: HandlerRequest): Promise<object> {\n const user = await ports.identity.authenticate({ headers: request.headers })\n if (!user) throw new HttpError(401, 'Nicht angemeldet')\n return user\n}\n\n/** The port `name` of the backend; 501 when the host serves none. The one place absent ports turn into an answer. */\nexport function port<K extends Exclude<keyof EditorPorts, 'identity' | 'documents'>>(ctx: HandlerContext, name: K): NonNullable<EditorPorts[K]> {\n const found = ctx.ports[name]\n if (found === undefined) throw new HttpError(501, `Port \"${name}\" not supported`)\n return found as NonNullable<EditorPorts[K]>\n}\n\n/** Per-user limit for the AI handlers: 401 without a user, 429 with `Retry-After` when exceeded; returns the user, for `resolveModel`. */\nexport async function enforceRateLimit(ctx: AiHandlerContext, request: HandlerRequest): Promise<object> {\n const user = await authenticated(ctx, request)\n const waitMs = ctx.limiter.hit(userKey(user))\n if (!waitMs) return user\n throw new HttpError(429, 'Zu viele KI-Anfragen, bitte kurz warten', { 'retry-after': String(Math.max(1, Math.ceil(waitMs / 1000))) })\n}\n\ninterface BodySchema<T> {\n safeParse(data: unknown): { success: true, data: T } | { success: false, error: { issues: unknown[] } }\n}\n\n/** The request's JSON body validated with `schema`; 400 for malformed JSON and for validation failures (`data`: the issues). */\nexport async function readBody<T>(request: HandlerRequest, schema: BodySchema<T>): Promise<T> {\n let raw: unknown\n try {\n raw = await request.readJson?.()\n }\n catch {\n throw new HttpError(400, 'Ungültiger JSON-Body')\n }\n const parsed = schema.safeParse(raw)\n if (!parsed.success) throw new HttpError(400, 'Validation Error', {}, parsed.error.issues)\n return parsed.data\n}\n"],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"context.js","names":[],"sources":["../../../src/server/handlers/context.ts"],"sourcesContent":["import type { BlockType } from '../../features/composites/schema'\nimport type { AiModels } from '../ai'\nimport type { ChatAgent } from '../chat-agent'\nimport type { EditorPorts } from '../ports'\nimport type { CollabServer } from '../collab'\nimport type { DocumentMutator } from '../document-mutator'\nimport { HttpError } from '../errors'\nimport type { RateLimiter } from '../rate-limit'\nimport { userKey } from '../rate-limit'\nimport type { SpeechBudget } from '../speech-budget'\nimport type { ToolRegistry } from '../tools'\n\n/** What every editor handler needs: the ports of the backend (identity first). */\nexport interface HandlerContext {\n ports: EditorPorts\n /** The collaboration server of the process: documents open in it count for `tagsGet` with their unsaved content. */\n collab?: CollabServer\n /** Applies block operations to a Document (`POST /api/editor/documents/:id/ops`); without it the route answers 501. */\n mutator?: DocumentMutator\n /** What the installed server really serves (`capabilities` answers with it); without it, what `ports` imply. */\n served?: { ai?: boolean, collaboration?: boolean }\n /** The daily character budget of the speech route (`createSpeechBudget`); without it one budget of `SPEECH_CHARS_PER_DAY` per user and UTC day is kept per `ports` object. */\n speechBudget?: SpeechBudget\n /** The Block types of the host's config (`EditorServerOptions.blockTypes`). The query route reads their Property definitions to type the fields of a query; the chat describes them to the model and, when they are given, ignores the `blockTypes` the client sends. */\n blockTypes?: readonly BlockType[]\n}\n\n/** Context of the AI handlers: the configured models and the per-user limit shared by all of them. */\nexport interface AiHandlerContext extends HandlerContext {\n ai: AiModels\n limiter: RateLimiter\n /** The document chat's read-only tools besides `edit_document`, built once with `toolRegistry(EditorServerOptions.tools)` (e.g. of `createWorkspaceTools()`). The model may call them; the server runs them and feeds the results back before the next turn. */\n tools?: ToolRegistry\n /** The host's agent for the document chat (`EditorServerOptions.chatAgent`), resolved per request: an agent answers the turn in place of the built-in model loop, `null` keeps the loop. */\n chatAgent?: (user: object) => ChatAgent | null | Promise<ChatAgent | null>\n}\n\n/** A request, reduced to what the handlers read: any HTTP server maps its own request type onto it. */\nexport interface HandlerRequest {\n headers: Headers\n /** Route parameters (`id`, `name`). */\n params?: Readonly<Record<string, string | undefined>>\n /** Query string values (`q`). */\n query?: Readonly<Record<string, string | undefined>>\n /** The JSON body. Lazy, so a handler authenticates (401) before it reads or validates anything. Rejects when the body is no JSON. */\n readJson?: () => Promise<unknown>\n /** The multipart body; `null` when it is none. Lazy like `readJson`. */\n readForm?: () => Promise<FormData | null>\n /** Aborts when the client goes away: handlers stop upstream requests with it. */\n signal?: AbortSignal\n}\n\n/** A response, as plain data any HTTP server can send (`new Response(body, { status, headers })` maps it 1:1). */\nexport interface HandlerResponse {\n status: number\n headers: Record<string, string>\n body: string | Uint8Array | ReadableStream<Uint8Array> | null\n}\n\nexport const json = (value: unknown, status = 200): HandlerResponse => ({\n status,\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify(value),\n})\n\nexport const text = (value: string, contentType: string, headers: Record<string, string> = {}): HandlerResponse => ({\n status: 200,\n headers: { 'content-type': contentType, ...headers },\n body: value,\n})\n\nexport const empty = (status = 204, headers: Record<string, string> = {}): HandlerResponse => ({ status, headers, body: null })\n\n/** The first language tag of the `Accept-Language` header that `Intl` knows (`de-DE,de;q=0.9` → `de-DE`, canonical); `*` and tags that are none are skipped; undefined when there is no usable one. */\nexport function acceptedLocaleOf(request: HandlerRequest): string | undefined {\n for (const part of (request.headers.get('accept-language') ?? '').split(',')) {\n const tag = part.split(';')[0]!.trim()\n if (!tag || tag === '*') continue\n try {\n return Intl.getCanonicalLocales(tag)[0]\n }\n catch { /* not a language tag: the next one */ }\n }\n return undefined\n}\n\n/**\n * Server-sent events. `run` writes events through `emit` and ends the stream by returning; `signal` aborts when the\n * client cancels the response or `request.signal` aborts. An `emit` after that is dropped.\n */\nexport function eventStream(request: HandlerRequest, run: (emit: (event: string, data: string) => void, signal: AbortSignal) => Promise<void>): HandlerResponse {\n const abort = new AbortController()\n if (request.signal?.aborted) abort.abort()\n else request.signal?.addEventListener('abort', () => abort.abort(), { once: true })\n const encoder = new TextEncoder()\n return {\n status: 200,\n headers: { 'content-type': 'text/event-stream', 'cache-control': 'no-cache, no-transform', 'x-accel-buffering': 'no' },\n body: new ReadableStream<Uint8Array>({\n start(controller) {\n const emit = (event: string, data: string) => {\n if (!abort.signal.aborted) controller.enqueue(encoder.encode(`event: ${event}\\ndata: ${data}\\n\\n`))\n }\n void run(emit, abort.signal)\n .catch((error) => { if (!abort.signal.aborted) console.error('[editor-server]', error) })\n .finally(() => { try { controller.close() } catch { /* the client cancelled */ } })\n },\n cancel() { abort.abort() },\n }),\n }\n}\n\n/** The user `ports.identity.authenticate` finds behind the request's headers; 401 without one. */\nexport async function authenticated({ ports }: HandlerContext, request: HandlerRequest): Promise<object> {\n const user = await ports.identity.authenticate({ headers: request.headers })\n if (!user) throw new HttpError(401, 'Nicht angemeldet')\n return user\n}\n\n/** The port `name` of the backend; 501 when the host serves none. The one place absent ports turn into an answer. */\nexport function port<K extends Exclude<keyof EditorPorts, 'identity' | 'documents'>>(ctx: HandlerContext, name: K): NonNullable<EditorPorts[K]> {\n const found = ctx.ports[name]\n if (found === undefined) throw new HttpError(501, `Port \"${name}\" not supported`)\n return found as NonNullable<EditorPorts[K]>\n}\n\n/** Per-user limit for the AI handlers: 401 without a user, 429 with `Retry-After` when exceeded; returns the user, for `resolveModel`. */\nexport async function enforceRateLimit(ctx: AiHandlerContext, request: HandlerRequest): Promise<object> {\n const user = await authenticated(ctx, request)\n const waitMs = ctx.limiter.hit(userKey(user))\n if (!waitMs) return user\n throw new HttpError(429, 'Zu viele KI-Anfragen, bitte kurz warten', { 'retry-after': String(Math.max(1, Math.ceil(waitMs / 1000))) })\n}\n\ninterface BodySchema<T> {\n safeParse(data: unknown): { success: true, data: T } | { success: false, error: { issues: unknown[] } }\n}\n\n/** The request's JSON body validated with `schema`; 400 for malformed JSON and for validation failures (`data`: the issues). */\nexport async function readBody<T>(request: HandlerRequest, schema: BodySchema<T>): Promise<T> {\n let raw: unknown\n try {\n raw = await request.readJson?.()\n }\n catch {\n throw new HttpError(400, 'Ungültiger JSON-Body')\n }\n const parsed = schema.safeParse(raw)\n if (!parsed.success) throw new HttpError(400, 'Validation Error', {}, parsed.error.issues)\n return parsed.data\n}\n"],"mappings":";;;AA2DA,MAAa,QAAQ,OAAgB,SAAS,SAA0B;CACtE;CACA,SAAS,EAAE,gBAAgB,mBAAmB;CAC9C,MAAM,KAAK,UAAU,KAAK;AAC5B;AAEA,MAAa,QAAQ,OAAe,aAAqB,UAAkC,CAAC,OAAwB;CAClH,QAAQ;CACR,SAAS;EAAE,gBAAgB;EAAa,GAAG;CAAQ;CACnD,MAAM;AACR;AAEA,MAAa,SAAS,SAAS,KAAK,UAAkC,CAAC,OAAwB;CAAE;CAAQ;CAAS,MAAM;AAAK;;AAG7H,SAAgB,iBAAiB,SAA6C;CAC5E,KAAK,MAAM,SAAS,QAAQ,QAAQ,IAAI,iBAAiB,KAAK,GAAA,CAAI,MAAM,GAAG,GAAG;EAC5E,MAAM,MAAM,KAAK,MAAM,GAAG,CAAC,CAAC,EAAE,CAAE,KAAK;EACrC,IAAI,CAAC,OAAO,QAAQ,KAAK;EACzB,IAAI;GACF,OAAO,KAAK,oBAAoB,GAAG,CAAC,CAAC;EACvC,QACM,CAAyC;CACjD;AAEF;;;;;AAMA,SAAgB,YAAY,SAAyB,KAA2G;CAC9J,MAAM,QAAQ,IAAI,gBAAgB;CAClC,IAAI,QAAQ,QAAQ,SAAS,MAAM,MAAM;MACpC,QAAQ,QAAQ,iBAAiB,eAAe,MAAM,MAAM,GAAG,EAAE,MAAM,KAAK,CAAC;CAClF,MAAM,UAAU,IAAI,YAAY;CAChC,OAAO;EACL,QAAQ;EACR,SAAS;GAAE,gBAAgB;GAAqB,iBAAiB;GAA0B,qBAAqB;EAAK;EACrH,MAAM,IAAI,eAA2B;GACnC,MAAM,YAAY;IAChB,MAAM,QAAQ,OAAe,SAAiB;KAC5C,IAAI,CAAC,MAAM,OAAO,SAAS,WAAW,QAAQ,QAAQ,OAAO,UAAU,MAAM,UAAU,KAAK,KAAK,CAAC;IACpG;IACA,IAAS,MAAM,MAAM,MAAM,CAAC,CACzB,OAAO,UAAU;KAAE,IAAI,CAAC,MAAM,OAAO,SAAS,QAAQ,MAAM,mBAAmB,KAAK;IAAE,CAAC,CAAC,CACxF,cAAc;KAAE,IAAI;MAAE,WAAW,MAAM;KAAE,QAAQ,CAA6B;IAAE,CAAC;GACtF;GACA,SAAS;IAAE,MAAM,MAAM;GAAE;EAC3B,CAAC;CACH;AACF;;AAGA,eAAsB,cAAc,EAAE,SAAyB,SAA0C;CACvG,MAAM,OAAO,MAAM,MAAM,SAAS,aAAa,EAAE,SAAS,QAAQ,QAAQ,CAAC;CAC3E,IAAI,CAAC,MAAM,MAAM,IAAI,UAAU,KAAK,kBAAkB;CACtD,OAAO;AACT;;AAGA,SAAgB,KAAqE,KAAqB,MAAsC;CAC9I,MAAM,QAAQ,IAAI,MAAM;CACxB,IAAI,UAAU,KAAA,GAAW,MAAM,IAAI,UAAU,KAAK,SAAS,KAAK,gBAAgB;CAChF,OAAO;AACT;;AAGA,eAAsB,iBAAiB,KAAuB,SAA0C;CACtG,MAAM,OAAO,MAAM,cAAc,KAAK,OAAO;CAC7C,MAAM,SAAS,IAAI,QAAQ,IAAI,QAAQ,IAAI,CAAC;CAC5C,IAAI,CAAC,QAAQ,OAAO;CACpB,MAAM,IAAI,UAAU,KAAK,2CAA2C,EAAE,eAAe,OAAO,KAAK,IAAI,GAAG,KAAK,KAAK,SAAS,GAAI,CAAC,CAAC,EAAE,CAAC;AACtI;;AAOA,eAAsB,SAAY,SAAyB,QAAmC;CAC5F,IAAI;CACJ,IAAI;EACF,MAAM,MAAM,QAAQ,WAAW;CACjC,QACM;EACJ,MAAM,IAAI,UAAU,KAAK,sBAAsB;CACjD;CACA,MAAM,SAAS,OAAO,UAAU,GAAG;CACnC,IAAI,CAAC,OAAO,SAAS,MAAM,IAAI,UAAU,KAAK,oBAAoB,CAAC,GAAG,OAAO,MAAM,MAAM;CACzF,OAAO,OAAO;AAChB"}
|
package/dist/server/index.d.ts
CHANGED
|
@@ -1,18 +1,22 @@
|
|
|
1
1
|
import { ModelPresetConfig, TranscriptionConfig } from "../config/index.js";
|
|
2
2
|
import { AuthenticateRequest } from "./auth.js";
|
|
3
|
+
import { documentChatSystem } from "../features/ai/protocol/chat.js";
|
|
3
4
|
import { ChatMessage } from "../features/ai/protocol/generate.js";
|
|
5
|
+
import "../features/ai/protocol/index.js";
|
|
4
6
|
import { AccessControl, AssetStore, BlockIndex, BlockSearchOptions, CollabStateStore, Directory, DocumentAdmin, DocumentContentStore, DocumentScope, EditorPorts, Embedder, ImageProvider, KnowledgeGraph, ModelProvider, PORT_CAPABILITY, RenderProgressReporter, Renderer, SettingsStore, SpeechProvider, ThemeStore, serverCapabilities } from "./ports.js";
|
|
5
7
|
import { BlockIndexOptions, IndexedPorts, createBlockMarkdown, withBlockIndex } from "./block-index.js";
|
|
6
8
|
import { HttpError } from "./errors.js";
|
|
7
9
|
import { AiModels, AiModelsConfig, ChatTurnPart, createAiModels, streamChat, streamChatTurn, upstreamError } from "./ai.js";
|
|
10
|
+
import { ToolContext, ToolProvider, ToolRegistry, toolRegistry } from "./tools.js";
|
|
11
|
+
import { ChatAgent, ChatAgentEdit, ChatAgentEditTool, ChatAgentPart, ChatAgentTurn, documentEditInputSchema, documentEditTool } from "./chat-agent.js";
|
|
8
12
|
import { COLLAB_FIELD, CollabConnection, CollabServer, CollabServerOptions, WebSocketLike, createCollabServer } from "./collab.js";
|
|
9
13
|
import { decodeCollabState, encodeCollabState, isStateStale } from "./collab-state.js";
|
|
10
14
|
import { DocumentMutator, DocumentMutatorOptions, MutationContext, createDocumentMutator } from "./document-mutator.js";
|
|
15
|
+
import { FIND_BLOCKS_LIMIT, READ_BLOCK_LIMIT, WorkspaceToolsOptions, createWorkspaceTools } from "./workspace-tools.js";
|
|
16
|
+
import { DocumentToolsOptions, createDocumentTools } from "./document-tools.js";
|
|
11
17
|
import { MarkdownBlocks, MarkdownBlocksOptions, createMarkdownBlocks } from "./markdown-blocks.js";
|
|
12
18
|
import { RateLimiter, RateLimiterOptions, createRateLimiter, userKey } from "./rate-limit.js";
|
|
13
19
|
import { SPEECH_CHARS_PER_DAY, SpeechBudget, SpeechBudgetOptions, createSpeechBudget } from "./speech-budget.js";
|
|
14
20
|
import { mayReadDocument, readableDocumentsOf } from "./readable-documents.js";
|
|
15
21
|
import { currentContent, listCurrentContents, loadCurrentContent } from "./stored-content.js";
|
|
16
|
-
|
|
17
|
-
import { FIND_BLOCKS_LIMIT, READ_BLOCK_LIMIT, WorkspaceToolsOptions, createWorkspaceTools } from "./workspace-tools.js";
|
|
18
|
-
export { type AccessControl, type AiModels, type AiModelsConfig, type AssetStore, type AuthenticateRequest, type BlockIndex, type BlockIndexOptions, type BlockSearchOptions, COLLAB_FIELD, type ChatMessage, type ChatTurnPart, type CollabConnection, type CollabServer, type CollabServerOptions, type CollabStateStore, type Directory, type DocumentAdmin, type DocumentContentStore, type DocumentMutator, type DocumentMutatorOptions, type DocumentScope, type EditorPorts, type Embedder, FIND_BLOCKS_LIMIT, HttpError, type ImageProvider, type IndexedPorts, type KnowledgeGraph, type MarkdownBlocks, type MarkdownBlocksOptions, type ModelPresetConfig, type ModelProvider, type MutationContext, PORT_CAPABILITY, READ_BLOCK_LIMIT, type RateLimiter, type RateLimiterOptions, type RenderProgressReporter, type Renderer, SPEECH_CHARS_PER_DAY, type SettingsStore, type SpeechBudget, type SpeechBudgetOptions, type SpeechProvider, type ThemeStore, type ToolContext, type ToolProvider, type ToolRegistry, type TranscriptionConfig, type WebSocketLike, type WorkspaceToolsOptions, createAiModels, createBlockMarkdown, createCollabServer, createDocumentMutator, createMarkdownBlocks, createRateLimiter, createSpeechBudget, createWorkspaceTools, currentContent, decodeCollabState, encodeCollabState, isStateStale, listCurrentContents, loadCurrentContent, mayReadDocument, readableDocumentsOf, serverCapabilities, streamChat, streamChatTurn, toolRegistry, upstreamError, userKey, withBlockIndex };
|
|
22
|
+
export { type AccessControl, type AiModels, type AiModelsConfig, type AssetStore, type AuthenticateRequest, type BlockIndex, type BlockIndexOptions, type BlockSearchOptions, COLLAB_FIELD, type ChatAgent, type ChatAgentEdit, type ChatAgentEditTool, type ChatAgentPart, type ChatAgentTurn, type ChatMessage, type ChatTurnPart, type CollabConnection, type CollabServer, type CollabServerOptions, type CollabStateStore, type Directory, type DocumentAdmin, type DocumentContentStore, type DocumentMutator, type DocumentMutatorOptions, type DocumentScope, type DocumentToolsOptions, type EditorPorts, type Embedder, FIND_BLOCKS_LIMIT, HttpError, type ImageProvider, type IndexedPorts, type KnowledgeGraph, type MarkdownBlocks, type MarkdownBlocksOptions, type ModelPresetConfig, type ModelProvider, type MutationContext, PORT_CAPABILITY, READ_BLOCK_LIMIT, type RateLimiter, type RateLimiterOptions, type RenderProgressReporter, type Renderer, SPEECH_CHARS_PER_DAY, type SettingsStore, type SpeechBudget, type SpeechBudgetOptions, type SpeechProvider, type ThemeStore, type ToolContext, type ToolProvider, type ToolRegistry, type TranscriptionConfig, type WebSocketLike, type WorkspaceToolsOptions, createAiModels, createBlockMarkdown, createCollabServer, createDocumentMutator, createDocumentTools, createMarkdownBlocks, createRateLimiter, createSpeechBudget, createWorkspaceTools, currentContent, decodeCollabState, documentChatSystem, documentEditInputSchema, documentEditTool, encodeCollabState, isStateStale, listCurrentContents, loadCurrentContent, mayReadDocument, readableDocumentsOf, serverCapabilities, streamChat, streamChatTurn, toolRegistry, upstreamError, userKey, withBlockIndex };
|
package/dist/server/index.js
CHANGED
|
@@ -1,15 +1,18 @@
|
|
|
1
|
+
import { documentChatSystem } from "../features/ai/protocol/chat.js";
|
|
1
2
|
import { currentContent, listCurrentContents, loadCurrentContent } from "./stored-content.js";
|
|
2
3
|
import { createBlockMarkdown, withBlockIndex } from "./block-index.js";
|
|
3
4
|
import { HttpError } from "./errors.js";
|
|
4
5
|
import { createAiModels, streamChat, streamChatTurn, upstreamError } from "./ai.js";
|
|
6
|
+
import { documentEditInputSchema, documentEditTool } from "./chat-agent.js";
|
|
5
7
|
import { decodeCollabState, encodeCollabState, isStateStale } from "./collab-state.js";
|
|
6
8
|
import { COLLAB_FIELD, createCollabServer } from "./collab.js";
|
|
7
9
|
import { createMarkdownBlocks } from "./markdown-blocks.js";
|
|
8
10
|
import { mayReadDocument, readableDocumentsOf } from "./readable-documents.js";
|
|
9
11
|
import { createDocumentMutator } from "./document-mutator.js";
|
|
12
|
+
import { FIND_BLOCKS_LIMIT, READ_BLOCK_LIMIT, createWorkspaceTools } from "./workspace-tools.js";
|
|
13
|
+
import { createDocumentTools } from "./document-tools.js";
|
|
10
14
|
import { PORT_CAPABILITY, serverCapabilities } from "./ports.js";
|
|
11
15
|
import { createRateLimiter, userKey } from "./rate-limit.js";
|
|
12
16
|
import { SPEECH_CHARS_PER_DAY, createSpeechBudget } from "./speech-budget.js";
|
|
13
17
|
import { toolRegistry } from "./tools.js";
|
|
14
|
-
|
|
15
|
-
export { COLLAB_FIELD, FIND_BLOCKS_LIMIT, HttpError, PORT_CAPABILITY, READ_BLOCK_LIMIT, SPEECH_CHARS_PER_DAY, createAiModels, createBlockMarkdown, createCollabServer, createDocumentMutator, createMarkdownBlocks, createRateLimiter, createSpeechBudget, createWorkspaceTools, currentContent, decodeCollabState, encodeCollabState, isStateStale, listCurrentContents, loadCurrentContent, mayReadDocument, readableDocumentsOf, serverCapabilities, streamChat, streamChatTurn, toolRegistry, upstreamError, userKey, withBlockIndex };
|
|
18
|
+
export { COLLAB_FIELD, FIND_BLOCKS_LIMIT, HttpError, PORT_CAPABILITY, READ_BLOCK_LIMIT, SPEECH_CHARS_PER_DAY, createAiModels, createBlockMarkdown, createCollabServer, createDocumentMutator, createDocumentTools, createMarkdownBlocks, createRateLimiter, createSpeechBudget, createWorkspaceTools, currentContent, decodeCollabState, documentChatSystem, documentEditInputSchema, documentEditTool, encodeCollabState, isStateStale, listCurrentContents, loadCurrentContent, mayReadDocument, readableDocumentsOf, serverCapabilities, streamChat, streamChatTurn, toolRegistry, upstreamError, userKey, withBlockIndex };
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { BlockMarkdown } from "../core/block-index.js";
|
|
2
2
|
import { BlockType } from "../features/composites/block-types.js";
|
|
3
3
|
import { ToolProvider } from "./tools.js";
|
|
4
|
+
import { z } from "zod";
|
|
4
5
|
import { AnyExtension } from "@tiptap/core";
|
|
5
6
|
//#region src/server/workspace-tools.d.ts
|
|
6
7
|
/** `find_blocks` answers at most this many hits unless the model asks for fewer. */
|
|
@@ -28,12 +28,17 @@ const readInput = z.object({
|
|
|
28
28
|
documentId: z.string().min(1).max(200),
|
|
29
29
|
blockId: z.string().min(1).max(100)
|
|
30
30
|
});
|
|
31
|
-
/** The parsed `input`, or the sentence that tells the model what was wrong with it. */
|
|
31
|
+
/** The parsed `input`, or the sentence that tells the model what was wrong with it (also for `createDocumentTools`). */
|
|
32
32
|
function parsed(schema, input) {
|
|
33
33
|
const result = schema.safeParse(input);
|
|
34
34
|
if (result.success) return { value: result.data };
|
|
35
35
|
return { problem: `Ungültige Eingabe: ${result.error.issues.map((issue) => `${issue.path.join(".") || "Eingabe"}: ${issue.message}`).join("; ")}` };
|
|
36
36
|
}
|
|
37
|
+
/**
|
|
38
|
+
* The answer of a read tool for a Block it cannot show: the same sentence for an unreadable Document and an unknown Block, so a
|
|
39
|
+
* tool never confirms that a Block exists somewhere the user may not look.
|
|
40
|
+
*/
|
|
41
|
+
const unreadableBlock = (documentId, blockId) => `Der Block „${blockId}“ in Dokument „${documentId}“ ist nicht lesbar oder existiert nicht.`;
|
|
37
42
|
const indexOf = ({ ports }) => {
|
|
38
43
|
if (!ports.index) throw new HttpError(501, "Dieser Arbeitsbereich hat keinen Block-Index");
|
|
39
44
|
return ports.index;
|
|
@@ -152,7 +157,7 @@ function createWorkspaceTools(options = {}) {
|
|
|
152
157
|
const checked = parsed(readInput, input);
|
|
153
158
|
if ("problem" in checked) return checked.problem;
|
|
154
159
|
const { documentId, blockId } = checked.value;
|
|
155
|
-
const notFound =
|
|
160
|
+
const notFound = unreadableBlock(documentId, blockId);
|
|
156
161
|
const index = indexOf(ctx);
|
|
157
162
|
if (!await mayReadDocument(ctx.ports, ctx.user, documentId)) return notFound;
|
|
158
163
|
const row = await index.get({
|
|
@@ -169,6 +174,6 @@ function createWorkspaceTools(options = {}) {
|
|
|
169
174
|
}];
|
|
170
175
|
}
|
|
171
176
|
//#endregion
|
|
172
|
-
export { FIND_BLOCKS_LIMIT, READ_BLOCK_LIMIT, createWorkspaceTools };
|
|
177
|
+
export { FIND_BLOCKS_LIMIT, READ_BLOCK_LIMIT, createWorkspaceTools, parsed, unreadableBlock };
|
|
173
178
|
|
|
174
179
|
//# sourceMappingURL=workspace-tools.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"workspace-tools.js","names":[],"sources":["../../src/server/workspace-tools.ts"],"sourcesContent":["// i18n-ignore-file: model-facing tool descriptions and results (German, like the chat's system prompt)\nimport type { AnyExtension } from '@tiptap/core'\nimport { jsonSchema, tool } from 'ai'\nimport { z } from 'zod'\nimport type { BlockMarkdown } from '../core/block-index'\nimport { MAX_SEARCH_LIMIT } from '../core/block-search'\nimport type { DataQuery, DataRow, FilterNode } from '../core/query'\nimport { defaultQueryEnv } from '../core/query-types'\nimport type { BlockType } from '../features/composites/block-types'\nimport { createBlockMarkdown } from './block-index'\nimport { findBlock } from './document-blocks'\nimport { HttpError } from './errors'\nimport type { BlockIndex } from './ports'\nimport { queryDefinitionsOf } from './query-definitions'\nimport { mayReadDocument, readableDocumentsOf } from './readable-documents'\nimport { loadCurrentContent } from './stored-content'\nimport type { ToolContext, ToolProvider } from './tools'\n\n// The first two read-only tools of the document chat (see `ToolProvider`): the model finds Blocks anywhere in the workspace and\n// reads them, inside what the user may read. Both go through the Block index; `read_block` serialises the Block from the\n// Document's current content, so a Block just edited in an open Document reads as it is now.\n\n/** `find_blocks` answers at most this many hits unless the model asks for fewer. */\nexport const FIND_BLOCKS_LIMIT = 10\n/** Characters of a hit's text that `find_blocks` shows. */\nconst SNIPPET_LENGTH = 240\n/** Characters of a Block `read_block` returns; a longer one is cut with a note. */\nexport const READ_BLOCK_LIMIT = 6000\n\nexport interface WorkspaceToolsOptions {\n /** The collaboration schema the Markdown is written with, see `createBlockMarkdown`. */\n schema?: () => AnyExtension[]\n /** The host's Block types (`EditorServerOptions.blockTypes`); they decide which Typed blocks keep their id in a fence. */\n blockTypes?: readonly BlockType[]\n /** The serializer itself, instead of one built from `schema` and `blockTypes`. */\n markdown?: BlockMarkdown\n}\n\nconst findInput = z.object({\n query: z.string().min(1).max(200).optional(),\n limit: z.number().int().min(1).max(MAX_SEARCH_LIMIT).optional(),\n blockType: z.string().min(1).max(100).optional(),\n // The structure of a filter is checked by `validateQuery` (through the index), whose sentences tell the model what to fix.\n filter: z.custom<FilterNode>(value => typeof value === 'object' && value !== null && !Array.isArray(value), { message: 'filter muss ein Objekt sein' }).optional(),\n}).refine(input => input.query !== undefined || input.blockType !== undefined, { message: 'query oder blockType angeben' })\n .refine(input => input.filter === undefined || input.blockType !== undefined, { message: 'filter braucht blockType', path: ['blockType'] })\nconst readInput = z.object({\n documentId: z.string().min(1).max(200),\n blockId: z.string().min(1).max(100),\n})\n\n/** The parsed `input`, or the sentence that tells the model what was wrong with it. */\nfunction parsed<T>(schema: z.ZodType<T>, input: unknown): { value: T } | { problem: string } {\n const result = schema.safeParse(input)\n if (result.success) return { value: result.data }\n return { problem: `Ungültige Eingabe: ${result.error.issues.map(issue => `${issue.path.join('.') || 'Eingabe'}: ${issue.message}`).join('; ')}` }\n}\n\nconst indexOf = ({ ports }: ToolContext): BlockIndex => {\n if (!ports.index) throw new HttpError(501, 'Dieser Arbeitsbereich hat keinen Block-Index')\n return ports.index\n}\n\nconst snippet = (text: string): string => {\n const flat = text.replace(/\\s+/g, ' ').trim()\n return flat.length > SNIPPET_LENGTH ? `${flat.slice(0, SNIPPET_LENGTH)}…` : flat\n}\n\n/**\n * The read-only workspace tools of the document chat, for `EditorServerOptions.tools` / `provideEditorServer({ tools })`:\n *\n * - `find_blocks(query, limit?)`: `BlockIndex.search` over the Documents the user may read (`readableDocumentsOf`), best\n * first, each hit with its address, Block type, title and the start of its text;\n * - `read_block(documentId, blockId)`: one Block as Markdown, from the Document's current content (live content of an open\n * Document first, else `loadCurrentContent`). An unreadable Document and an unknown Block answer the same sentence, so the\n * tool never confirms that a Block exists somewhere the user may not look.\n *\n * Both need the `index` port; without it a call answers the model that there is no index.\n */\nexport function createWorkspaceTools(options: WorkspaceToolsOptions = {}): ToolProvider[] {\n let markdown = options.markdown\n const markdownOf = (): BlockMarkdown => markdown ??= createBlockMarkdown({\n ...(options.schema && { schema: options.schema }),\n ...(options.blockTypes && { blockTypes: options.blockTypes }),\n })\n\n const findBlocks: ToolProvider = {\n name: 'find_blocks',\n tool: tool({\n description: 'Sucht Blöcke in allen Dokumenten des Arbeitsbereichs, die die Nutzerin bzw. der Nutzer lesen darf. Mit query (Titel und Text; ganze Wörter oder Wortanfänge, alle Wörter der Suche müssen vorkommen) liefert sie beste Treffer zuerst. Mit blockType (z. B. \"task\") und optional filter listet sie die Blöcke dieses Typs, deren Eigenschaften der Bedingung genügen (zusammen mit query: nur Treffer der Suche). Jeder Treffer hat Dokument-ID, Block-ID, Blocktyp, Titel, Textanfang und bei blockType die Eigenschaften (values). Den ganzen Block liest du mit read_block.',\n inputSchema: jsonSchema<{ query?: string, limit?: number, blockType?: string, filter?: FilterNode }>({\n type: 'object',\n properties: {\n query: { type: 'string', description: 'Suchbegriffe, durch Leerzeichen getrennt. Pflicht ohne blockType.' },\n limit: { type: 'integer', minimum: 1, maximum: MAX_SEARCH_LIMIT, description: `Höchstzahl der Treffer, Standard ${FIND_BLOCKS_LIMIT}.` },\n blockType: { type: 'string', description: 'Blocktyp-ID, nur Blöcke dieses Typs (Pflicht mit filter).' },\n filter: {\n type: 'object',\n description: 'Bedingung auf Eigenschaften: { \"field\": \"<Eigenschaft oder $title, $tags, $type, $id, $doc, $basket>\", \"op\": \"eq|ne|lt|lte|gt|gte|contains|startsWith|isEmpty|isNotEmpty|hasAny|hasAll|hasNone|before|after|on\", \"value\": <Wert oder Liste> } oder { \"and\": [Bedingungen] } / { \"or\": [Bedingungen] }.',\n },\n },\n }),\n }),\n async handle(ctx, input) {\n const checked = parsed(findInput, input)\n if ('problem' in checked) return checked.problem\n const { query, limit = FIND_BLOCKS_LIMIT, blockType, filter } = checked.value\n const index = indexOf(ctx)\n const scope = await readableDocumentsOf(ctx.ports, ctx.user)\n if (blockType === undefined) {\n const hits = await index.search(query!, scope, { limit })\n if (!hits.length) return `Keine Treffer für „${query}“.`\n const rows = await Promise.all(hits.map(hit => index.get(hit)))\n return JSON.stringify(hits.map(({ documentId, blockId, type, title }, i) => ({ documentId, blockId, type, title, text: snippet(rows[i]?.text ?? '') })))\n }\n const dataQuery: DataQuery = {\n v: 1,\n source: { kind: 'blocks', blockType },\n ...(filter && { filter }),\n ...(query && { search: { q: query } }),\n page: { limit },\n }\n const result = await index.query(dataQuery, scope, defaultQueryEnv(), queryDefinitionsOf(options.blockTypes, dataQuery))\n const errors = result.issues.filter(issue => issue.severity === 'error')\n if (errors.length) return `Ungültige Abfrage: ${errors.map(issue => issue.message).join(' ')}`\n if (!result.rows.length) return `Keine Blöcke vom Typ „${blockType}“ gefunden${filter ? ', die dem Filter genügen' : ''}.`\n const texts = await Promise.all(result.rows.map(row => index.get(row.ref)))\n return JSON.stringify(result.rows.map(({ ref, type, title, values }: DataRow, i) => ({ documentId: ref.documentId, blockId: ref.blockId, type, title, text: snippet(texts[i]?.text ?? ''), values })))\n },\n }\n\n const readBlock: ToolProvider = {\n name: 'read_block',\n tool: tool({\n description: 'Liest einen Block eines beliebigen lesbaren Dokuments des Arbeitsbereichs als Markdown. Dokument-ID und Block-ID stammen aus find_blocks.',\n inputSchema: jsonSchema<{ documentId: string, blockId: string }>({\n type: 'object',\n properties: {\n documentId: { type: 'string', description: 'ID des Dokuments.' },\n blockId: { type: 'string', description: 'ID des Blocks im Dokument.' },\n },\n required: ['documentId', 'blockId'],\n }),\n }),\n async handle(ctx, input) {\n const checked = parsed(readInput, input)\n if ('problem' in checked) return checked.problem\n const { documentId, blockId } = checked.value\n const notFound = `Der Block „${blockId}“ in Dokument „${documentId}“ ist nicht lesbar oder existiert nicht.`\n const index = indexOf(ctx)\n if (!await mayReadDocument(ctx.ports, ctx.user, documentId)) return notFound\n const row = await index.get({ documentId, blockId })\n if (!row) return notFound\n const content = ctx.liveContent(documentId) ?? await loadCurrentContent(ctx.ports.documents, documentId)\n const node = content && findBlock(content, blockId)\n // The index can lag a write by a moment; its text is then the best there is.\n const body = (node ? markdownOf()([node]) : row.text).trim()\n const head = `Dokument „${documentId}“, Block „${blockId}“ (${row.type}${row.title ? `, ${row.title}` : ''}):`\n return body.length > READ_BLOCK_LIMIT ? `${head}\\n\\n${body.slice(0, READ_BLOCK_LIMIT)}\\n\\n[… gekürzt, ${body.length - READ_BLOCK_LIMIT} Zeichen fehlen]` : `${head}\\n\\n${body}`\n },\n }\n\n return [findBlocks, readBlock]\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"workspace-tools.js","names":[],"sources":["../../src/server/workspace-tools.ts"],"sourcesContent":["// i18n-ignore-file: model-facing tool descriptions and results (German, like the chat's system prompt)\nimport type { AnyExtension } from '@tiptap/core'\nimport { jsonSchema, tool } from 'ai'\nimport { z } from 'zod'\nimport type { BlockMarkdown } from '../core/block-index'\nimport { MAX_SEARCH_LIMIT } from '../core/block-search'\nimport type { DataQuery, DataRow, FilterNode } from '../core/query'\nimport { defaultQueryEnv } from '../core/query-types'\nimport type { BlockType } from '../features/composites/block-types'\nimport { createBlockMarkdown } from './block-index'\nimport { findBlock } from './document-blocks'\nimport { HttpError } from './errors'\nimport type { BlockIndex } from './ports'\nimport { queryDefinitionsOf } from './query-definitions'\nimport { mayReadDocument, readableDocumentsOf } from './readable-documents'\nimport { loadCurrentContent } from './stored-content'\nimport type { ToolContext, ToolProvider } from './tools'\n\n// The first two read-only tools of the document chat (see `ToolProvider`): the model finds Blocks anywhere in the workspace and\n// reads them, inside what the user may read. Both go through the Block index; `read_block` serialises the Block from the\n// Document's current content, so a Block just edited in an open Document reads as it is now.\n\n/** `find_blocks` answers at most this many hits unless the model asks for fewer. */\nexport const FIND_BLOCKS_LIMIT = 10\n/** Characters of a hit's text that `find_blocks` shows. */\nconst SNIPPET_LENGTH = 240\n/** Characters of a Block `read_block` returns; a longer one is cut with a note. */\nexport const READ_BLOCK_LIMIT = 6000\n\nexport interface WorkspaceToolsOptions {\n /** The collaboration schema the Markdown is written with, see `createBlockMarkdown`. */\n schema?: () => AnyExtension[]\n /** The host's Block types (`EditorServerOptions.blockTypes`); they decide which Typed blocks keep their id in a fence. */\n blockTypes?: readonly BlockType[]\n /** The serializer itself, instead of one built from `schema` and `blockTypes`. */\n markdown?: BlockMarkdown\n}\n\nconst findInput = z.object({\n query: z.string().min(1).max(200).optional(),\n limit: z.number().int().min(1).max(MAX_SEARCH_LIMIT).optional(),\n blockType: z.string().min(1).max(100).optional(),\n // The structure of a filter is checked by `validateQuery` (through the index), whose sentences tell the model what to fix.\n filter: z.custom<FilterNode>(value => typeof value === 'object' && value !== null && !Array.isArray(value), { message: 'filter muss ein Objekt sein' }).optional(),\n}).refine(input => input.query !== undefined || input.blockType !== undefined, { message: 'query oder blockType angeben' })\n .refine(input => input.filter === undefined || input.blockType !== undefined, { message: 'filter braucht blockType', path: ['blockType'] })\nconst readInput = z.object({\n documentId: z.string().min(1).max(200),\n blockId: z.string().min(1).max(100),\n})\n\n/** The parsed `input`, or the sentence that tells the model what was wrong with it (also for `createDocumentTools`). */\nexport function parsed<T>(schema: z.ZodType<T>, input: unknown): { value: T } | { problem: string } {\n const result = schema.safeParse(input)\n if (result.success) return { value: result.data }\n return { problem: `Ungültige Eingabe: ${result.error.issues.map(issue => `${issue.path.join('.') || 'Eingabe'}: ${issue.message}`).join('; ')}` }\n}\n\n/**\n * The answer of a read tool for a Block it cannot show: the same sentence for an unreadable Document and an unknown Block, so a\n * tool never confirms that a Block exists somewhere the user may not look.\n */\nexport const unreadableBlock = (documentId: string, blockId: string): string => `Der Block „${blockId}“ in Dokument „${documentId}“ ist nicht lesbar oder existiert nicht.`\n\nconst indexOf = ({ ports }: ToolContext): BlockIndex => {\n if (!ports.index) throw new HttpError(501, 'Dieser Arbeitsbereich hat keinen Block-Index')\n return ports.index\n}\n\nconst snippet = (text: string): string => {\n const flat = text.replace(/\\s+/g, ' ').trim()\n return flat.length > SNIPPET_LENGTH ? `${flat.slice(0, SNIPPET_LENGTH)}…` : flat\n}\n\n/**\n * The read-only workspace tools of the document chat, for `EditorServerOptions.tools` / `provideEditorServer({ tools })`:\n *\n * - `find_blocks(query, limit?)`: `BlockIndex.search` over the Documents the user may read (`readableDocumentsOf`), best\n * first, each hit with its address, Block type, title and the start of its text;\n * - `read_block(documentId, blockId)`: one Block as Markdown, from the Document's current content (live content of an open\n * Document first, else `loadCurrentContent`). An unreadable Document and an unknown Block answer the same sentence, so the\n * tool never confirms that a Block exists somewhere the user may not look.\n *\n * Both need the `index` port; without it a call answers the model that there is no index.\n */\nexport function createWorkspaceTools(options: WorkspaceToolsOptions = {}): ToolProvider[] {\n let markdown = options.markdown\n const markdownOf = (): BlockMarkdown => markdown ??= createBlockMarkdown({\n ...(options.schema && { schema: options.schema }),\n ...(options.blockTypes && { blockTypes: options.blockTypes }),\n })\n\n const findBlocks: ToolProvider = {\n name: 'find_blocks',\n tool: tool({\n description: 'Sucht Blöcke in allen Dokumenten des Arbeitsbereichs, die die Nutzerin bzw. der Nutzer lesen darf. Mit query (Titel und Text; ganze Wörter oder Wortanfänge, alle Wörter der Suche müssen vorkommen) liefert sie beste Treffer zuerst. Mit blockType (z. B. \"task\") und optional filter listet sie die Blöcke dieses Typs, deren Eigenschaften der Bedingung genügen (zusammen mit query: nur Treffer der Suche). Jeder Treffer hat Dokument-ID, Block-ID, Blocktyp, Titel, Textanfang und bei blockType die Eigenschaften (values). Den ganzen Block liest du mit read_block.',\n inputSchema: jsonSchema<{ query?: string, limit?: number, blockType?: string, filter?: FilterNode }>({\n type: 'object',\n properties: {\n query: { type: 'string', description: 'Suchbegriffe, durch Leerzeichen getrennt. Pflicht ohne blockType.' },\n limit: { type: 'integer', minimum: 1, maximum: MAX_SEARCH_LIMIT, description: `Höchstzahl der Treffer, Standard ${FIND_BLOCKS_LIMIT}.` },\n blockType: { type: 'string', description: 'Blocktyp-ID, nur Blöcke dieses Typs (Pflicht mit filter).' },\n filter: {\n type: 'object',\n description: 'Bedingung auf Eigenschaften: { \"field\": \"<Eigenschaft oder $title, $tags, $type, $id, $doc, $basket>\", \"op\": \"eq|ne|lt|lte|gt|gte|contains|startsWith|isEmpty|isNotEmpty|hasAny|hasAll|hasNone|before|after|on\", \"value\": <Wert oder Liste> } oder { \"and\": [Bedingungen] } / { \"or\": [Bedingungen] }.',\n },\n },\n }),\n }),\n async handle(ctx, input) {\n const checked = parsed(findInput, input)\n if ('problem' in checked) return checked.problem\n const { query, limit = FIND_BLOCKS_LIMIT, blockType, filter } = checked.value\n const index = indexOf(ctx)\n const scope = await readableDocumentsOf(ctx.ports, ctx.user)\n if (blockType === undefined) {\n const hits = await index.search(query!, scope, { limit })\n if (!hits.length) return `Keine Treffer für „${query}“.`\n const rows = await Promise.all(hits.map(hit => index.get(hit)))\n return JSON.stringify(hits.map(({ documentId, blockId, type, title }, i) => ({ documentId, blockId, type, title, text: snippet(rows[i]?.text ?? '') })))\n }\n const dataQuery: DataQuery = {\n v: 1,\n source: { kind: 'blocks', blockType },\n ...(filter && { filter }),\n ...(query && { search: { q: query } }),\n page: { limit },\n }\n const result = await index.query(dataQuery, scope, defaultQueryEnv(), queryDefinitionsOf(options.blockTypes, dataQuery))\n const errors = result.issues.filter(issue => issue.severity === 'error')\n if (errors.length) return `Ungültige Abfrage: ${errors.map(issue => issue.message).join(' ')}`\n if (!result.rows.length) return `Keine Blöcke vom Typ „${blockType}“ gefunden${filter ? ', die dem Filter genügen' : ''}.`\n const texts = await Promise.all(result.rows.map(row => index.get(row.ref)))\n return JSON.stringify(result.rows.map(({ ref, type, title, values }: DataRow, i) => ({ documentId: ref.documentId, blockId: ref.blockId, type, title, text: snippet(texts[i]?.text ?? ''), values })))\n },\n }\n\n const readBlock: ToolProvider = {\n name: 'read_block',\n tool: tool({\n description: 'Liest einen Block eines beliebigen lesbaren Dokuments des Arbeitsbereichs als Markdown. Dokument-ID und Block-ID stammen aus find_blocks.',\n inputSchema: jsonSchema<{ documentId: string, blockId: string }>({\n type: 'object',\n properties: {\n documentId: { type: 'string', description: 'ID des Dokuments.' },\n blockId: { type: 'string', description: 'ID des Blocks im Dokument.' },\n },\n required: ['documentId', 'blockId'],\n }),\n }),\n async handle(ctx, input) {\n const checked = parsed(readInput, input)\n if ('problem' in checked) return checked.problem\n const { documentId, blockId } = checked.value\n const notFound = unreadableBlock(documentId, blockId)\n const index = indexOf(ctx)\n if (!await mayReadDocument(ctx.ports, ctx.user, documentId)) return notFound\n const row = await index.get({ documentId, blockId })\n if (!row) return notFound\n const content = ctx.liveContent(documentId) ?? await loadCurrentContent(ctx.ports.documents, documentId)\n const node = content && findBlock(content, blockId)\n // The index can lag a write by a moment; its text is then the best there is.\n const body = (node ? markdownOf()([node]) : row.text).trim()\n const head = `Dokument „${documentId}“, Block „${blockId}“ (${row.type}${row.title ? `, ${row.title}` : ''}):`\n return body.length > READ_BLOCK_LIMIT ? `${head}\\n\\n${body.slice(0, READ_BLOCK_LIMIT)}\\n\\n[… gekürzt, ${body.length - READ_BLOCK_LIMIT} Zeichen fehlen]` : `${head}\\n\\n${body}`\n },\n }\n\n return [findBlocks, readBlock]\n}\n"],"mappings":"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"}
|
|
@@ -5,10 +5,10 @@ import { basketById } from "../features/composites/baskets.js";
|
|
|
5
5
|
import { useUiKit } from "../vue/kit/use-ui-kit.js";
|
|
6
6
|
import { useEditorSettings } from "../vue/settings.js";
|
|
7
7
|
import { useEditorI18n } from "../vue/use-editor-i18n.js";
|
|
8
|
-
import _sfc_main$1 from "../features/composites/components/DocumentBadges.js";
|
|
9
8
|
import { DRAG_MIME, focusCrumbs, indexItems, isOutlineHeader, itemBasket } from "./tree-items.js";
|
|
10
9
|
import { refocusRow, rowPosition } from "./row-focus.js";
|
|
11
10
|
import { rowOf } from "./tree-model.js";
|
|
11
|
+
import _sfc_main$1 from "../features/composites/components/DocumentBadges.js";
|
|
12
12
|
import { useTreeModel } from "./use-tree-model.js";
|
|
13
13
|
import { Fragment, computed, createBlock, createCommentVNode, createElementBlock, createElementVNode, createSlots, createTextVNode, createVNode, defineComponent, mergeModels, normalizeClass, normalizeStyle, openBlock, renderList, renderSlot, toDisplayString, unref, useModel, withCtx, withKeys, withModifiers } from "vue";
|
|
14
14
|
//#region src/tree/DocumentTree.vue
|
|
@@ -3,10 +3,10 @@ import { useEditorSettings } from "../vue/settings.js";
|
|
|
3
3
|
import { useEditorContext } from "../vue/context.js";
|
|
4
4
|
import { useEditorI18n } from "../vue/use-editor-i18n.js";
|
|
5
5
|
import { useStoreRef } from "../vue/store-ref.js";
|
|
6
|
-
import { useFileImport } from "../features/media/vue.js";
|
|
7
6
|
import { treeView } from "./tree-view-model.js";
|
|
8
7
|
import { createTreeModel } from "./tree-model.js";
|
|
9
8
|
import { createTreeExecutor, treeEditorOf } from "./tree-host.js";
|
|
9
|
+
import { useFileImport } from "../features/media/vue.js";
|
|
10
10
|
import { computed, onScopeDispose, shallowRef, watch } from "vue";
|
|
11
11
|
//#region src/tree/use-tree-model.ts
|
|
12
12
|
/**
|
|
@@ -82,21 +82,21 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {
|
|
|
82
82
|
editor: import("vue").ComputedRef<Editor | undefined>;
|
|
83
83
|
preset: ComposedEditor<Component>;
|
|
84
84
|
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
85
|
-
settingsOpen: (tab?: string | undefined) => any;
|
|
86
85
|
"update:sidebarOpen": (value: boolean) => any;
|
|
87
86
|
"update:commentsOpen": (value: boolean) => any;
|
|
88
|
-
"update:modelValue": (content: JSONContent) => any;
|
|
89
87
|
ready: (editor: Editor) => any;
|
|
88
|
+
"update:modelValue": (content: JSONContent) => any;
|
|
90
89
|
metadataCreate: (list: string, item: MetadataItem) => any;
|
|
91
90
|
contentError: (error: Error) => any;
|
|
91
|
+
settingsOpen: (tab?: string | undefined) => any;
|
|
92
92
|
}, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
|
|
93
|
-
onSettingsOpen?: ((tab?: string | undefined) => any) | undefined;
|
|
94
93
|
"onUpdate:sidebarOpen"?: ((value: boolean) => any) | undefined;
|
|
95
94
|
"onUpdate:commentsOpen"?: ((value: boolean) => any) | undefined;
|
|
96
|
-
"onUpdate:modelValue"?: ((content: JSONContent) => any) | undefined;
|
|
97
95
|
onReady?: ((editor: Editor) => any) | undefined;
|
|
96
|
+
"onUpdate:modelValue"?: ((content: JSONContent) => any) | undefined;
|
|
98
97
|
onMetadataCreate?: ((list: string, item: MetadataItem) => any) | undefined;
|
|
99
98
|
onContentError?: ((error: Error) => any) | undefined;
|
|
99
|
+
onSettingsOpen?: ((tab?: string | undefined) => any) | undefined;
|
|
100
100
|
}>, {
|
|
101
101
|
content: ContentLevel | readonly FeatureId[];
|
|
102
102
|
ai: boolean;
|
|
@@ -41,21 +41,21 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {
|
|
|
41
41
|
editor: import("vue").ComputedRef<Editor | undefined>;
|
|
42
42
|
preset: EditorPreset<Component>;
|
|
43
43
|
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
44
|
-
settingsOpen: (tab?: string | undefined) => any;
|
|
45
44
|
"update:sidebarOpen": (value: boolean) => any;
|
|
46
45
|
"update:commentsOpen": (value: boolean) => any;
|
|
47
|
-
"update:modelValue": (content: JSONContent) => any;
|
|
48
46
|
ready: (editor: Editor) => any;
|
|
47
|
+
"update:modelValue": (content: JSONContent) => any;
|
|
49
48
|
metadataCreate: (list: string, item: MetadataItem) => any;
|
|
50
49
|
contentError: (error: Error) => any;
|
|
50
|
+
settingsOpen: (tab?: string | undefined) => any;
|
|
51
51
|
}, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
|
|
52
|
-
onSettingsOpen?: ((tab?: string | undefined) => any) | undefined;
|
|
53
52
|
"onUpdate:sidebarOpen"?: ((value: boolean) => any) | undefined;
|
|
54
53
|
"onUpdate:commentsOpen"?: ((value: boolean) => any) | undefined;
|
|
55
|
-
"onUpdate:modelValue"?: ((content: JSONContent) => any) | undefined;
|
|
56
54
|
onReady?: ((editor: Editor) => any) | undefined;
|
|
55
|
+
"onUpdate:modelValue"?: ((content: JSONContent) => any) | undefined;
|
|
57
56
|
onMetadataCreate?: ((list: string, item: MetadataItem) => any) | undefined;
|
|
58
57
|
onContentError?: ((error: Error) => any) | undefined;
|
|
58
|
+
onSettingsOpen?: ((tab?: string | undefined) => any) | undefined;
|
|
59
59
|
}>, {
|
|
60
60
|
editable: boolean;
|
|
61
61
|
settingsUi: "internal" | "external";
|