@things-factory/ai-assistant 10.1.107 → 10.1.109
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/client/components/assistant-chat.ts +108 -2
- package/client/components/assistant-dock.ts +4 -4
- package/client/components/assistant-mode.ts +1 -1
- package/client/components/entry-review.ts +23 -3
- package/client/pages/assistant-workspace-page.ts +82 -2
- package/client/utils/attachment-policy.ts +22 -3
- package/client/utils/entry-hash.ts +35 -0
- package/client/utils/entry-proposal.ts +10 -3
- package/client/utils/entry-workspace.ts +70 -0
- package/client/utils/pasted-table.ts +145 -0
- package/client/utils/spreadsheet-text.ts +125 -0
- package/dist-client/components/assistant-chat.d.ts +2 -0
- package/dist-client/components/assistant-chat.js +105 -2
- package/dist-client/components/assistant-chat.js.map +1 -1
- package/dist-client/components/assistant-dock.js +4 -4
- package/dist-client/components/assistant-dock.js.map +1 -1
- package/dist-client/components/assistant-mode.d.ts +2 -1
- package/dist-client/components/assistant-mode.js.map +1 -1
- package/dist-client/components/entry-review.d.ts +2 -0
- package/dist-client/components/entry-review.js +30 -3
- package/dist-client/components/entry-review.js.map +1 -1
- package/dist-client/pages/assistant-workspace-page.d.ts +8 -0
- package/dist-client/pages/assistant-workspace-page.js +98 -2
- package/dist-client/pages/assistant-workspace-page.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-client/utils/attachment-policy.d.ts +7 -1
- package/dist-client/utils/attachment-policy.js +14 -2
- package/dist-client/utils/attachment-policy.js.map +1 -1
- package/dist-client/utils/entry-hash.d.ts +2 -0
- package/dist-client/utils/entry-hash.js +36 -0
- package/dist-client/utils/entry-hash.js.map +1 -0
- package/dist-client/utils/entry-proposal.d.ts +1 -1
- package/dist-client/utils/entry-proposal.js +9 -2
- package/dist-client/utils/entry-proposal.js.map +1 -1
- package/dist-client/utils/entry-workspace.d.ts +20 -0
- package/dist-client/utils/entry-workspace.js +68 -0
- package/dist-client/utils/entry-workspace.js.map +1 -0
- package/dist-client/utils/pasted-table.d.ts +34 -0
- package/dist-client/utils/pasted-table.js +150 -0
- package/dist-client/utils/pasted-table.js.map +1 -0
- package/dist-client/utils/spreadsheet-text.d.ts +48 -0
- package/dist-client/utils/spreadsheet-text.js +110 -0
- package/dist-client/utils/spreadsheet-text.js.map +1 -0
- package/dist-server/service/assistant-chat-resolver.js +1 -1
- package/dist-server/service/assistant-chat-resolver.js.map +1 -1
- package/dist-server/service/assistant-mode/assistant-mode-resolver.js +1 -1
- package/dist-server/service/assistant-mode/assistant-mode-resolver.js.map +1 -1
- package/dist-server/service/chat-session/attachment-policy.d.ts +5 -2
- package/dist-server/service/chat-session/attachment-policy.js +10 -1
- package/dist-server/service/chat-session/attachment-policy.js.map +1 -1
- package/dist-server/service/chat-session/attachment-tools.js +3 -1
- package/dist-server/service/chat-session/attachment-tools.js.map +1 -1
- package/dist-server/service/chat-session/session-attachments.d.ts +3 -2
- package/dist-server/service/chat-session/session-attachments.js +5 -4
- package/dist-server/service/chat-session/session-attachments.js.map +1 -1
- package/dist-server/service/data-entry/entry-apply.d.ts +2 -0
- package/dist-server/service/data-entry/entry-apply.js +4 -0
- package/dist-server/service/data-entry/entry-apply.js.map +1 -1
- package/dist-server/service/data-entry/entry-hash.d.ts +2 -0
- package/dist-server/service/data-entry/entry-hash.js +39 -0
- package/dist-server/service/data-entry/entry-hash.js.map +1 -0
- package/dist-server/service/data-entry/entry-resolver.d.ts +1 -1
- package/dist-server/service/data-entry/entry-resolver.js +5 -3
- package/dist-server/service/data-entry/entry-resolver.js.map +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -2
- package/server/service/assistant-chat-resolver.ts +2 -2
- package/server/service/assistant-mode/assistant-mode-resolver.ts +1 -1
- package/server/service/chat-session/attachment-policy.ts +18 -2
- package/server/service/chat-session/attachment-tools.ts +3 -1
- package/server/service/chat-session/session-attachments.ts +5 -4
- package/server/service/data-entry/data-entry.test.ts +10 -0
- package/server/service/data-entry/entry-apply.ts +5 -0
- package/server/service/data-entry/entry-hash.ts +35 -0
- package/server/service/data-entry/entry-resolver.ts +4 -2
- package/test/entry-card-styles.test.ts +18 -0
- package/test/entry-workspace.test.ts +89 -0
- package/test/pasted-table.test.ts +147 -0
- package/test/spreadsheet-text.test.ts +92 -0
- package/translations/en.json +12 -1
- package/translations/ja.json +12 -1
- package/translations/ko.json +12 -1
- package/translations/ms.json +12 -1
- package/translations/zh.json +12 -1
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* A spreadsheet read into text the conversation can carry (design/ai/shared-dock.md §9-5).
|
|
3
|
+
*
|
|
4
|
+
* The door takes images and text only; a mode that opens 'spreadsheet' has the screen read each .xlsx sheet into
|
|
5
|
+
* CSV before sending, so the model reads the sheet and the entry card can name where a row came from.
|
|
6
|
+
*
|
|
7
|
+
* - Read with exceljs. The old binary .xls is not read - it is refused by name (exceljs cannot open it, and the
|
|
8
|
+
* npm SheetJS that can has unfixed flaws; a file a person uploads is not opened with it).
|
|
9
|
+
* - A cell is its value - a formula as its computed result, a date as ISO, rich text as its text.
|
|
10
|
+
* - Every line carries the sheet's own row number. Empty rows are left out, never renumbered, so "sheet · row" on a
|
|
11
|
+
* card is the row the person sees in Excel.
|
|
12
|
+
* - The limit is on the text after reading - a small .xlsx can be a long text. A sheet over it is refused, never cut:
|
|
13
|
+
* a cut ledger is read as the whole ledger.
|
|
14
|
+
*/
|
|
15
|
+
const XLSX_TYPE = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
|
|
16
|
+
const XLS_TYPE = 'application/vnd.ms-excel';
|
|
17
|
+
/** A spreadsheet by its type, or by its name when the browser did not know the type. */
|
|
18
|
+
export function spreadsheetKindOf(file) {
|
|
19
|
+
const type = (file.type || '').toLowerCase();
|
|
20
|
+
const name = (file.name || '').toLowerCase();
|
|
21
|
+
if (type === XLSX_TYPE || name.endsWith('.xlsx'))
|
|
22
|
+
return 'xlsx';
|
|
23
|
+
if (type === XLS_TYPE || name.endsWith('.xls'))
|
|
24
|
+
return 'xls';
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
/** The picker's `accept` for spreadsheets. */
|
|
28
|
+
export const SPREADSHEET_ACCEPT = ['.xlsx', XLSX_TYPE];
|
|
29
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
30
|
+
/** One cell as text - what Excel shows as the value, not how it is stored. */
|
|
31
|
+
export function cellText(value) {
|
|
32
|
+
if (value === null || value === undefined)
|
|
33
|
+
return '';
|
|
34
|
+
if (value instanceof Date) {
|
|
35
|
+
if (isNaN(value.getTime()))
|
|
36
|
+
return '';
|
|
37
|
+
const day = `${value.getUTCFullYear()}-${pad(value.getUTCMonth() + 1)}-${pad(value.getUTCDate())}`;
|
|
38
|
+
const h = value.getUTCHours();
|
|
39
|
+
const m = value.getUTCMinutes();
|
|
40
|
+
const s = value.getUTCSeconds();
|
|
41
|
+
return h || m || s ? `${day}T${pad(h)}:${pad(m)}:${pad(s)}` : day;
|
|
42
|
+
}
|
|
43
|
+
if (typeof value === 'object') {
|
|
44
|
+
const v = value;
|
|
45
|
+
if ('result' in v)
|
|
46
|
+
return cellText(v.result);
|
|
47
|
+
if ('formula' in v || 'sharedFormula' in v)
|
|
48
|
+
return '';
|
|
49
|
+
if (Array.isArray(v.richText))
|
|
50
|
+
return v.richText.map((part) => part?.text ?? '').join('');
|
|
51
|
+
if ('text' in v)
|
|
52
|
+
return cellText(v.text);
|
|
53
|
+
if ('error' in v)
|
|
54
|
+
return String(v.error);
|
|
55
|
+
return '';
|
|
56
|
+
}
|
|
57
|
+
return String(value);
|
|
58
|
+
}
|
|
59
|
+
/** One CSV field - quoted when it holds a comma, a quote or a line break. */
|
|
60
|
+
export const csvField = (text) => (/[",\n\r]/.test(text) ? `"${text.replace(/"/g, '""')}"` : text);
|
|
61
|
+
/** One sheet as CSV: a line naming the file and sheet, then every non-empty row led by its own row number. */
|
|
62
|
+
export function sheetText(fileName, sheetName, rows) {
|
|
63
|
+
const lines = rows
|
|
64
|
+
.map(row => ({ number: row.number, cells: row.cells.map(cellText) }))
|
|
65
|
+
.filter(row => row.cells.some(cell => cell.trim() !== ''))
|
|
66
|
+
.map(row => {
|
|
67
|
+
let cells = row.cells;
|
|
68
|
+
while (cells.length && cells[cells.length - 1] === '')
|
|
69
|
+
cells = cells.slice(0, -1);
|
|
70
|
+
return [String(row.number), ...cells].map(csvField).join(',');
|
|
71
|
+
});
|
|
72
|
+
return [`# ${fileName} · sheet "${sheetName}" · the first column is the sheet's own row number`, ...lines].join('\n') + '\n';
|
|
73
|
+
}
|
|
74
|
+
/** The file name a sheet travels under - "거래내역.xlsx" sheet "9월" → "거래내역 - 9월.csv". */
|
|
75
|
+
export function sheetFileName(fileName, sheetName) {
|
|
76
|
+
return `${fileName.replace(/\.xlsx?$/i, '')} - ${sheetName.replace(/[\\/:*?"<>|]/g, '_')}.csv`;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Every non-empty sheet of an .xlsx as text, or why not. `load` is exceljs's Workbook - passed in so the screen
|
|
80
|
+
* loads the library only when a spreadsheet is attached.
|
|
81
|
+
*/
|
|
82
|
+
export async function readSpreadsheet(data, fileName, maxChars, Workbook) {
|
|
83
|
+
const workbook = new Workbook();
|
|
84
|
+
try {
|
|
85
|
+
await workbook.xlsx.load(data);
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return { refusal: { code: 'attachment-sheet-unreadable', name: fileName } };
|
|
89
|
+
}
|
|
90
|
+
const files = [];
|
|
91
|
+
for (const sheet of workbook.worksheets) {
|
|
92
|
+
const rows = [];
|
|
93
|
+
sheet.eachRow({ includeEmpty: false }, (row, number) => {
|
|
94
|
+
const values = Array.isArray(row.values) ? row.values.slice(1) : [];
|
|
95
|
+
rows.push({ number, cells: values });
|
|
96
|
+
});
|
|
97
|
+
if (!rows.length)
|
|
98
|
+
continue;
|
|
99
|
+
const text = sheetText(fileName, sheet.name, rows);
|
|
100
|
+
const name = sheetFileName(fileName, sheet.name);
|
|
101
|
+
if (text.length > maxChars)
|
|
102
|
+
return { refusal: { code: 'attachment-sheet-too-long', name: `${fileName} · ${sheet.name}`, maxChars } };
|
|
103
|
+
files.push({ name, text });
|
|
104
|
+
}
|
|
105
|
+
/* A workbook with nothing in it is said, not dropped: the person would think it was sent. */
|
|
106
|
+
if (!files.length)
|
|
107
|
+
return { refusal: { code: 'attachment-sheet-empty', name: fileName } };
|
|
108
|
+
return { files };
|
|
109
|
+
}
|
|
110
|
+
//# sourceMappingURL=spreadsheet-text.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"spreadsheet-text.js","sourceRoot":"","sources":["../../client/utils/spreadsheet-text.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * A spreadsheet read into text the conversation can carry (design/ai/shared-dock.md §9-5).\n *\n * The door takes images and text only; a mode that opens 'spreadsheet' has the screen read each .xlsx sheet into\n * CSV before sending, so the model reads the sheet and the entry card can name where a row came from.\n *\n * - Read with exceljs. The old binary .xls is not read - it is refused by name (exceljs cannot open it, and the\n * npm SheetJS that can has unfixed flaws; a file a person uploads is not opened with it).\n * - A cell is its value - a formula as its computed result, a date as ISO, rich text as its text.\n * - Every line carries the sheet's own row number. Empty rows are left out, never renumbered, so \"sheet · row\" on a\n * card is the row the person sees in Excel.\n * - The limit is on the text after reading - a small .xlsx can be a long text. A sheet over it is refused, never cut:\n * a cut ledger is read as the whole ledger.\n */\n\nexport type SpreadsheetKind = 'xlsx' | 'xls'\n\nconst XLSX_TYPE = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'\nconst XLS_TYPE = 'application/vnd.ms-excel'\n\n/** A spreadsheet by its type, or by its name when the browser did not know the type. */\nexport function spreadsheetKindOf(file: { type?: string; name?: string }): SpreadsheetKind | null {\n const type = (file.type || '').toLowerCase()\n const name = (file.name || '').toLowerCase()\n if (type === XLSX_TYPE || name.endsWith('.xlsx')) return 'xlsx'\n if (type === XLS_TYPE || name.endsWith('.xls')) return 'xls'\n return null\n}\n\n/** The picker's `accept` for spreadsheets. */\nexport const SPREADSHEET_ACCEPT = ['.xlsx', XLSX_TYPE]\n\nconst pad = (n: number) => String(n).padStart(2, '0')\n\n/** One cell as text - what Excel shows as the value, not how it is stored. */\nexport function cellText(value: unknown): string {\n if (value === null || value === undefined) return ''\n if (value instanceof Date) {\n if (isNaN(value.getTime())) return ''\n const day = `${value.getUTCFullYear()}-${pad(value.getUTCMonth() + 1)}-${pad(value.getUTCDate())}`\n const h = value.getUTCHours()\n const m = value.getUTCMinutes()\n const s = value.getUTCSeconds()\n return h || m || s ? `${day}T${pad(h)}:${pad(m)}:${pad(s)}` : day\n }\n if (typeof value === 'object') {\n const v = value as any\n if ('result' in v) return cellText(v.result)\n if ('formula' in v || 'sharedFormula' in v) return ''\n if (Array.isArray(v.richText)) return v.richText.map((part: any) => part?.text ?? '').join('')\n if ('text' in v) return cellText(v.text)\n if ('error' in v) return String(v.error)\n return ''\n }\n return String(value)\n}\n\n/** One CSV field - quoted when it holds a comma, a quote or a line break. */\nexport const csvField = (text: string) => (/[\",\\n\\r]/.test(text) ? `\"${text.replace(/\"/g, '\"\"')}\"` : text)\n\nexport interface SheetRow {\n /** The row number in the sheet - 1 for Excel's first row. */\n number: number\n cells: unknown[]\n}\n\n/** One sheet as CSV: a line naming the file and sheet, then every non-empty row led by its own row number. */\nexport function sheetText(fileName: string, sheetName: string, rows: SheetRow[]): string {\n const lines = rows\n .map(row => ({ number: row.number, cells: row.cells.map(cellText) }))\n .filter(row => row.cells.some(cell => cell.trim() !== ''))\n .map(row => {\n let cells = row.cells\n while (cells.length && cells[cells.length - 1] === '') cells = cells.slice(0, -1)\n return [String(row.number), ...cells].map(csvField).join(',')\n })\n return [`# ${fileName} · sheet \"${sheetName}\" · the first column is the sheet's own row number`, ...lines].join('\\n') + '\\n'\n}\n\nexport interface SheetFile {\n name: string\n text: string\n}\n\nexport type SpreadsheetRefusal = { code: 'attachment-xls-not-read'; name: string } | { code: 'attachment-sheet-too-long'; name: string; maxChars: number } | { code: 'attachment-sheet-unreadable'; name: string }\n | { code: 'attachment-sheet-empty'; name: string }\n\n/** The file name a sheet travels under - \"거래내역.xlsx\" sheet \"9월\" → \"거래내역 - 9월.csv\". */\nexport function sheetFileName(fileName: string, sheetName: string): string {\n return `${fileName.replace(/\\.xlsx?$/i, '')} - ${sheetName.replace(/[\\\\/:*?\"<>|]/g, '_')}.csv`\n}\n\n/**\n * Every non-empty sheet of an .xlsx as text, or why not. `load` is exceljs's Workbook - passed in so the screen\n * loads the library only when a spreadsheet is attached.\n */\nexport async function readSpreadsheet(\n data: ArrayBuffer | Uint8Array,\n fileName: string,\n maxChars: number,\n Workbook: new () => any\n): Promise<{ files: SheetFile[] } | { refusal: SpreadsheetRefusal }> {\n const workbook = new Workbook()\n try {\n await workbook.xlsx.load(data)\n } catch {\n return { refusal: { code: 'attachment-sheet-unreadable', name: fileName } }\n }\n const files: SheetFile[] = []\n for (const sheet of workbook.worksheets) {\n const rows: SheetRow[] = []\n sheet.eachRow({ includeEmpty: false }, (row: any, number: number) => {\n const values = Array.isArray(row.values) ? row.values.slice(1) : []\n rows.push({ number, cells: values })\n })\n if (!rows.length) continue\n const text = sheetText(fileName, sheet.name, rows)\n const name = sheetFileName(fileName, sheet.name)\n if (text.length > maxChars) return { refusal: { code: 'attachment-sheet-too-long', name: `${fileName} · ${sheet.name}`, maxChars } }\n files.push({ name, text })\n }\n /* A workbook with nothing in it is said, not dropped: the person would think it was sent. */\n if (!files.length) return { refusal: { code: 'attachment-sheet-empty', name: fileName } }\n return { files }\n}\n"]}
|
|
@@ -423,7 +423,7 @@ let AssistantChatResolver = class AssistantChatResolver {
|
|
|
423
423
|
if (last.role === 'user' && typeof last.content === 'string') {
|
|
424
424
|
const parts = [{ type: 'text', text: last.content }];
|
|
425
425
|
for (const row of attachmentRows) {
|
|
426
|
-
const file = await (0, session_attachments_js_1.attachmentForModel)(row);
|
|
426
|
+
const file = await (0, session_attachments_js_1.attachmentForModel)(row, (0, attachment_policy_js_1.textCharsOf)((0, registry_js_1.getAssistantMode)(input.mode?.kind ?? '')?.attachments));
|
|
427
427
|
if (file.type === 'image') {
|
|
428
428
|
parts.push({ type: 'image', source: { kind: 'base64', data: file.data, mediaType: file.mediaType } });
|
|
429
429
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"assistant-chat-resolver.js","sourceRoot":"","sources":["../../server/service/assistant-chat-resolver.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * The neutral door into a conversation.\n *\n * ── Why this exists (2026-09-11) ───────────────────────────────────────────────\n * The chat component moved here from `board-ai`, but the only mutation it could call stayed\n * behind. operato-figure mounted the dock, registered four tools, and got\n * `Cannot query field \"boardAIChat\" on type \"Mutation\"` — it does not load board-ai. The twin has\n * the same shape and it does not show: its space deliberation runs through board-ai's door purely\n * because the board authoring half of the same product already loaded that package.\n *\n * ── Why the board resolver was not moved ───────────────────────────────────────\n * `board-ai-resolver.ts` is 692 lines and the larger part of it is board authoring itself:\n * `currentBoard` as a BoardModel, PatchEntry, board patch broadcasts, refid injection for\n * component mentions. Moving that here would teach this package the board document, which is the\n * dependency we just cut, pointing the other way.\n *\n * So the door moved and the editor did not. `boardAIChat` stays in board-ai for board authoring;\n * this one is for a surface that asks questions and runs registered tools.\n *\n * ── What this is not ───────────────────────────────────────────────────────────\n * It has no document, so it takes no document and returns no patch. Tools that write to a\n * document are not offered here — `registryAssistantChat` enforces that, and it decides what may\n * be dispatched from the same list it offers, so the two cannot drift apart.\n */\nimport { Arg, Ctx, Directive, Field, InputType, Mutation, ObjectType, Resolver } from 'type-graphql'\nimport { GraphQLJSON } from 'graphql-scalars'\nimport '@things-factory/auth-base'\n\nimport { config } from '@things-factory/env'\nimport { getRepository } from '@things-factory/shell'\nimport {\n createAIClient,\n findToolSpec,\n getDefaultAIClient,\n registryAssistantChat,\n type AIClient\n} from '@things-factory/ai-client-base'\n\nimport { ChatSession } from './chat-session/chat-session.js'\nimport { assertStationSession } from './chat-session/station-session-guard.js'\nimport { ChatMessage } from './chat-message/chat-message.js'\nimport { buildLlmHistory } from './chat-message/llm-history.js'\nimport { splitByCap } from './chat-message/history-summary.js'\nimport { foldSessionHistory } from './chat-message/fold-session-history.js'\nimport { groundingSourcesFor } from './grounding-sources.js'\nimport { publishChatMessage } from './chat-message/chat-message-publish.js'\nimport { publishSessionActivity } from './chat-session/session-activity-publish.js'\nimport { activityPatch, autoSessionName } from './chat-session/session-inbox.js'\nimport { toolAccessFor } from './tool-access.js'\nimport { ChatSessionParticipant } from './chat-session-participant/chat-session-participant.js'\nimport { doorsForAnswer } from './chat-message/message-doors.js'\nimport { judgeSessionKind } from './chat-session/session-kind.js'\nimport { readerGate } from './chat-message/reader-gate.js'\nimport { inviteMentionedUsers } from './chat-session-participant/mention-invite.js'\nimport { buildInviteNotification } from './chat-session-participant/invite-notification.js'\nimport { isSessionParticipant } from './chat-session-participant/participant-lookup.js'\nimport { assertMayWriteSession } from './chat-session/session-write-gate.js'\nimport { attachmentHistoryLine, refuseAttachments } from './chat-session/attachment-policy.js'\nimport { attachmentForModel, describeAttachment, findChatAttachment } from './chat-session/session-attachments.js'\nimport { getAssistantMode } from './assistant-mode/registry.js'\nimport { resolveModeSettings } from './assistant-mode/resolve.js'\nimport { requestTranslator } from './request-translator.js'\n\n/**\n * How many stored messages reach the model.\n *\n * A deliberation runs for days. Without a cap the prompt grows without bound and the oldest turns\n * bury the recent ones. Kept the same as the board conversation's cap so the two surfaces of one\n * product do not remember differently.\n */\nconst LLM_HISTORY_MAX_TURNS = 40\n\n@InputType()\nclass AssistantMessageInput {\n @Field({ description: \"Role: 'user' or 'assistant'\" })\n role: string\n\n @Field({ description: 'Message content' })\n content: string\n}\n\n@InputType({ description: 'Which conversation this is, and what it is about.' })\nclass AssistantModeInput {\n @Field({ description: 'A mode kind the app server registered — `<app>.<surface>`, e.g. \"twin.space\".' })\n kind: string\n\n @Field({\n nullable: true,\n description:\n 'What this conversation is about — the space, the station, the model being edited. With `kind` it is the conversation\\'s lifetime: when either changes the screen closes the conversation and starts another.'\n })\n target?: string\n}\n\n@InputType({ description: 'One turn of a conversation that has no document of its own.' })\nclass AssistantChatInput {\n @Field({\n nullable: true,\n description: 'ChatSession id. Omit for a one-off question with no persistence.'\n })\n sessionId?: string\n\n @Field(() => [AssistantMessageInput], {\n description:\n 'Conversation, newest last. For a persisted session only the last user message is appended; the rest is read from storage.'\n })\n messages: AssistantMessageInput[]\n\n @Field(() => AssistantModeInput, {\n description:\n 'Which conversation this is. The persona, the tool categories, the model and the limits come from the mode the app server registered (ADR-0061 decision 7) — a caller choosing its own tool categories would be a caller choosing its own authority.'\n })\n mode: AssistantModeInput\n\n @Field(() => GraphQLJSON, {\n nullable: true,\n description:\n 'What this surface is looking at (e.g. { figureId } while modelling). Tools read it so the model never has to ask the user for identifiers. Client-supplied — never used for authorization; a tool verifies the scope belongs to the caller tenant.'\n })\n hostContext?: any\n\n @Field({\n nullable: true,\n description:\n 'Last message to keep from the stored history. The client keeps its own truncation when a user edits a message, so pass the last kept id to hide the retracted span from the model.'\n })\n truncateAfterMessageId?: string\n\n @Field(() => [String], {\n nullable: true,\n description: 'Files stored with this conversation by attachToConversation, sent with this turn.'\n })\n attachmentIds?: string[]\n\n}\n\n@ObjectType()\nclass AssistantChatOutput {\n @Field({ description: 'The reply, including the notice when the loop stopped early.' })\n reply: string\n\n @Field({ description: 'AI client identifier (provider:model).' })\n clientId: string\n\n @Field({ nullable: true, description: 'Echo of the session id, when persisted.' })\n sessionId?: string\n\n @Field({ nullable: true, description: 'Stored ChatMessage id of the user input.' })\n userMessageId?: string\n\n @Field({ nullable: true, description: 'Stored ChatMessage id of the reply.' })\n assistantMessageId?: string\n\n @Field(() => GraphQLJSON, {\n nullable: true,\n description:\n 'Set when this turn did not fit: { omitted, summarized } — how many earlier messages the model did not see, and whether a summary stood in for them. Without it the person watches the conversation lose its beginning in silence, while the model is told.'\n })\n historyFolded?: any\n\n @Field(() => GraphQLJSON, {\n nullable: true,\n description: 'Tool usages during the turn — {name, arguments, result, kind}. The surface shows them foldably.'\n })\n toolUsages?: any\n\n @Field(() => [String], {\n nullable: true,\n description:\n 'Tool names offered to the model this turn. A surface that expected a category and sees it missing here knows the registration did not happen, rather than reading a vague answer as a refusal.'\n })\n offeredTools?: string[]\n\n @Field(() => GraphQLJSON, {\n nullable: true,\n description:\n 'Actions a tool recorded without executing — each carries what the tool returned alongside `proposed: true`. The surface renders a confirm button; executing is the user\\'s act.'\n })\n proposals?: any\n\n @Field(() => GraphQLJSON, {\n nullable: true,\n description:\n 'Identifiers in the reply that appear in nothing the model received. Hallucination candidates surfaced to the user; the reply itself is not altered. Null when the answer is grounded.'\n })\n groundingWarnings?: any\n}\n\nfunction resolveAIClient(): AIClient | undefined {\n const existing = getDefaultAIClient()\n if (existing) return existing\n const cfg = config.get('aiClient', null) as any\n if (cfg && cfg.provider) return createAIClient(cfg)\n return undefined\n}\n\n@Resolver()\nexport class AssistantChatResolver {\n /*\n * No `@transaction`, for the same reason the board conversation has none: a model call takes\n * seconds to tens of seconds, and holding a write lock for that long collides with everything\n * else on SQLite. Writes are short and around the call. For a conversation, a partly stored\n * history is safer than a lost one.\n */\n @Mutation(() => AssistantChatOutput, {\n description:\n 'Ask the assistant a question on a surface that has no document of its own. Tools come from the registry; with a sessionId the conversation is persisted.'\n })\n /*\n * `ai-assistant:mutation` — the same door as the board conversation's, which moved here on\n * 2026-09-10. No `legacy` alias: this operation is new, so no one was ever passing under an old\n * name, and declaring an alias would open a second name that nothing needs.\n */\n @Directive('@privilege(category: \"ai-assistant\", privilege: \"mutation\", domainOwnerGranted: true)')\n async assistantChat(@Arg('input') input: AssistantChatInput, @Ctx() context: any): Promise<AssistantChatOutput> {\n const client = resolveAIClient()\n if (!client) {\n throw new Error(\n 'No AI client configured. Set config.aiClient = { provider, model, apiKey } or call setDefaultAIClient(...).'\n )\n }\n\n /*\n * Read the declaration before anything is stored. A kind this server does not have is a\n * mismatched deployment, and the person should see that instead of a conversation with no\n * persona and no tools.\n */\n const t = requestTranslator(context)\n const settings = resolveModeSettings(input.mode, t)\n\n const { domain, user } = context.state\n const sessionRepo = getRepository(ChatSession)\n const messageRepo = getRepository(ChatMessage)\n\n let session: ChatSession | undefined\n if (input.sessionId) {\n const found = await sessionRepo.findOneBy({ id: input.sessionId, domain: { id: domain.id } as any })\n if (!found) throw new Error(`ChatSession ${input.sessionId} not found`)\n /*\n * A private conversation is written by its people only - and here more than anywhere: the answer is\n * built on this conversation's history, so an outsider's question would read it back to them.\n */\n await assertMayWriteSession({\n session: found as any,\n userId: user?.id,\n isParticipant: () => isSessionParticipant(domain.id, found.id!, user?.id),\n t: (key, options) => t(key, options as any)\n })\n if (found.anchorType === 'figure' && input.hostContext?.figureId !== found.anchorId) {\n throw new Error(\n context.t('ai-assistant.text.other-figure-history', {\n defaultValue: 'this conversation belongs to another figure. open the conversation of the model you have open.'\n })\n )\n }\n if (found.anchorType === 'application' && (input.hostContext?.app !== found.anchorId || input.hostContext?.figureId)) {\n throw new Error(\n context.t('ai-assistant.text.application-vs-model-history', {\n defaultValue: 'this conversation is about the application, not about a model. open the one you want.'\n })\n )\n }\n /*\n * 이 세션이 **이 대화의** 세션인가 — ADR-0072 결정 3. 다른 대화의 세션에 말을 붙이면 그 이력이\n * 이 대화의 프롬프트로 들어가고, 화면은 줄이 사라져 새 대화처럼 보이므로 보낸 사람은 모른다.\n */\n const verdict = judgeSessionKind((found as any).kind, input.mode.kind)\n if (verdict.action === 'refuse') {\n throw new Error(\n t('ai-assistant.text.session-kind-mismatch', {\n ...verdict.params,\n defaultValue: 'this conversation is {sessionKind}, and you are writing as {requested}. open the conversation you mean.'\n })\n )\n }\n if (verdict.action === 'stamp') {\n /* 칸이 서기 전에 난 세션 — 요청한 대화로 한 번 찍는다. 다음부터는 위 규칙 하나만 남는다. */\n await sessionRepo.update(found.id!, { kind: verdict.kind } as any)\n ;(found as any).kind = verdict.kind\n }\n session = found\n }\n\n assertStationSession(session, input.hostContext, t)\n\n const lastUserMessage = input.messages[input.messages.length - 1]\n let userMessageId: string | undefined\n\n /*\n * Files sent with this turn (design/ai/shared-dock.md §9). They were stored with this conversation by\n * attachToConversation; one that is not this conversation's is refused, never read. The policy is the\n * mode's - checked again here, since the door is the authority, not the screen.\n */\n const attachmentRows: any[] = []\n if (input.attachmentIds?.length) {\n if (!session) throw new Error(t('ai-assistant.text.attachments-need-session', { defaultValue: 'attachments need a saved conversation.' }))\n for (const id of input.attachmentIds) {\n const row = await findChatAttachment(domain.id, session.id!, id)\n if (!row) throw new Error(t('ai-assistant.text.attachment-not-in-session', { defaultValue: 'an attachment does not belong to this conversation.' }))\n attachmentRows.push(row)\n }\n const refusal = refuseAttachments(\n getAssistantMode(input.mode.kind)?.attachments,\n attachmentRows.map(row => ({ name: row.name, type: row.mimetype, size: Number(row.size) }))\n )\n if (refusal) throw new Error(t(`ai-assistant.text.${refusal.code}`, { ...(refusal as any), defaultValue: refusal.code }))\n }\n\n if (session && lastUserMessage?.role === 'user') {\n const previousLast = await messageRepo.findOne({\n where: { session: { id: session.id } as any },\n order: { createdAt: 'DESC' }\n })\n const saved = await messageRepo.save({\n session: { id: session.id } as any,\n role: 'user',\n content: lastUserMessage.content,\n /*\n * This door is the one people call to ask the assistant, so a turn stored here was for the\n * assistant (ADR-0070 A15 decision ①). The value comes from which door was called, never\n * from the request — two sources for one fact can disagree.\n */\n addressee: 'assistant',\n attachments: attachmentRows.length ? JSON.stringify(attachmentRows.map(describeAttachment)) : undefined,\n creator: user,\n updater: user,\n parentMessage: previousLast ? ({ id: previousLast.id } as any) : undefined\n } as any)\n userMessageId = saved.id\n\n /*\n * Session activity is denormalised for the list: most-recent ordering and another\n * participant's unread badge read it. Best effort — a conversation must not fail because a\n * list sorts one position late.\n */\n try {\n const patch: any = activityPatch(lastUserMessage.content)\n if (!session.name) patch.name = autoSessionName(lastUserMessage.content)\n await sessionRepo.update(session.id!, patch)\n } catch {\n /* noop */\n }\n\n /*\n * 사람이 `@이름` 으로 부른 사람은 이 대화에 들어온다(ADR-0071 결정 1). 이 문은 **사람의 턴**을\n * 저장하는 문이라 그 일을 한다 — 모델의 답이나 도구 결과는 아무도 초대하지 않는다.\n */\n const invite = await inviteMentionedUsers({\n content: lastUserMessage.content,\n sessionId: session.id!,\n domainId: domain.id,\n inviter: user,\n participants: getRepository(ChatSessionParticipant),\n visibility: (session as any).visibility\n })\n if (invite.invited.length > 0) {\n const payload = buildInviteNotification(invite.invited, undefined, user, (session as any)?.name)\n try {\n if (payload) await (context.state as any)?.notify?.(payload)\n } catch {\n /* 편지가 못 갔다고 방에 들어온 사실이 취소되지는 않는다. */\n }\n }\n\n publishChatMessage({ domainId: domain.id, sessionId: session.id, message: saved } as any)\n publishSessionActivity({\n domainId: domain.id,\n sessionId: session.id!,\n kind: 'message',\n anchorType: (session as any).anchorType,\n anchorId: (session as any).anchorId,\n preview: lastUserMessage.content,\n actorId: user?.id\n })\n }\n\n /*\n * 이번 턴에 상한 밖으로 밀려난 것이 있는가 — 화면이 「대화가 길어졌다」를 알릴 근거다.\n *\n * 이 문은 그 사실을 내지 않았다. 대화창에는 그 안내를 그리는 자리가 있고(`historyFolded`), 보드\n * 문만 그 값을 채웠다. 그래서 앱의 대화는 앞부분이 잘려 나가도 **사람에게는 아무 말도 하지 않았다** —\n * 모델에게는 「N 개 생략」을 적어 보내면서.\n */\n const readRows = session\n ? await messageRepo.find({\n where: { session: { id: session.id } as any },\n order: { createdAt: 'ASC' },\n relations: ['creator']\n })\n : []\n /*\n * 모델이 읽는 이력도 **부른 사람** 기준으로 가린다 — ADR-0073 판정 1. 가리지 않으면, 문을 한 번도\n * 안 지난 사람이 \"아까 그 수 다시 말해 줘\" 한 마디로 값을 가져간다. 모델이 옮겨 적는 순간 그 값은\n * 문 없는 새 답이 된다. 가린 자리를 지우지 않고 한 줄로 남기는 이유도 같다 — 지우면 모델이 그런\n * 말이 없었다고 답하고, 그것은 거짓말이다.\n */\n const storedRows = await readerGate(context)(readRows)\n const capped = splitByCap(\n storedRows.map(row => ({ id: row.id, role: row.role, content: row.content ?? '' })),\n LLM_HISTORY_MAX_TURNS\n )\n const historyFolded =\n capped.dropped.length > 0\n ? { omitted: capped.dropped.length, summarized: Boolean((session as any)?.lastSummary) }\n : undefined\n\n const conversation = session\n ? buildLlmHistory(\n /*\n * The model reads who spoke and who they spoke to. The entity carries the speaker as a\n * relation and the addressee as a column; the history wants them as plain fields, and\n * without this mapping every line arrived unattributed — a room of people read as one\n * voice (ADR-0070 A15 decision ②).\n */\n storedRows.map(message => ({\n id: message.id,\n role: message.role,\n /*\n * A file sent in an earlier turn is not sent again - one line names it, and the model opens it\n * with readChatAttachment when it needs to (cost is a usage axis, ADR-0061 decision 6).\n */\n content:\n message.id !== userMessageId && (message as any).attachmentsJson?.length\n ? [message.content, ...(message as any).attachmentsJson.map(attachmentHistoryLine)].join('\\n')\n : message.content,\n senderId: (message as any).creator?.id,\n senderName: (message as any).creator?.name,\n addressee: (message as any).addressee\n })),\n {\n maxTurns: LLM_HISTORY_MAX_TURNS,\n truncateAfterMessageId: input.truncateAfterMessageId,\n /* The message just stored is this turn's question — truncation must never drop it. */\n keepMessageIds: userMessageId ? [userMessageId] : undefined,\n summary: (session as any).lastSummary\n ? { text: (session as any).lastSummary, upToMessageId: (session as any).summaryUpToMessageId }\n : undefined\n }\n )\n : input.messages.map(message => ({ role: message.role as any, content: message.content }))\n\n if (attachmentRows.length && conversation.length > 0) {\n const last = conversation[conversation.length - 1]\n if (last.role === 'user' && typeof last.content === 'string') {\n const parts: any[] = [{ type: 'text', text: last.content }]\n for (const row of attachmentRows) {\n const file = await attachmentForModel(row)\n if (file.type === 'image') {\n parts.push({ type: 'image', source: { kind: 'base64', data: file.data, mediaType: file.mediaType } })\n } else {\n parts.push({\n type: 'text',\n text: `[attachment ${row.id}: ${file.name}${file.truncated ? ' - cut, the file is longer' : ''}]\\n${file.text}`\n })\n }\n }\n last.content = parts as any\n }\n } else if (input.hostContext?.image?.data && conversation.length > 0) {\n /* The screen-image path (figure's rendered view) - not a file of the conversation. */\n const last = conversation[conversation.length - 1]\n if (last.role === 'user' && typeof last.content === 'string') {\n const raw = input.hostContext.image\n const rawData = typeof raw.data === 'string' ? raw.data.replace(/^data:image\\/[a-zA-Z]+;base64,/, '') : raw.data\n const buf = typeof rawData === 'string' ? Buffer.from(rawData, 'base64') : Buffer.from(rawData)\n last.content = [\n { type: 'text', text: last.content },\n { type: 'image', source: { kind: 'base64', data: buf, mediaType: raw.mediaType || 'image/png' } }\n ] as any\n }\n }\n\n const result = await registryAssistantChat({\n client,\n messages: conversation as any,\n systemPrompt: settings.systemPrompt,\n toolCategories: settings.toolCategories,\n /* 도구가 「어느 대화에서 불렸나」를 안다 — 서버의 값이다(ADR-0061 결정 3). */\n mode: { kind: input.mode.kind, ...(input.mode.target ? { target: input.mode.target } : {}) },\n toolAccess: toolAccessFor(context),\n /* The stop notice is a sentence a person reads — the server writes it in their language (ADR-0054). */\n t,\n hostContext: input.hostContext,\n /*\n * 근거는 도구 결과만이 아니다 — 시스템 프롬프트 · 이 대화의 말 · 화면이 보낸 스냅숏도 근거다\n * (`ai-client-base/utils/grounding`). 이 문은 그 셋을 넘기지 않아, **사람이 방금 타이핑한\n * 식별자**를 되받아 적은 답이 「근거 없음」으로 표시됐다. 그런 경고가 몇 번 나오면 사람이 경고\n * 자체를 안 보게 된다.\n */\n groundingSources: groundingSourcesFor({\n systemPrompt: settings.systemPrompt,\n messages: conversation as any,\n hostContext: input.hostContext\n }),\n state: context.state,\n model: settings.model,\n temperature: settings.temperature,\n maxTokens: settings.maxTokens,\n requireToolOnFirstTurn: settings.requireToolOnFirstTurn\n })\n\n /*\n * 이 답이 지나온 문 — ADR-0073 판정 1. 행에도, 목록 미리보기에도 같은 값이 간다. 한쪽만 적으면\n * 가려진 답이 목록에는 그대로 뜬다.\n */\n const answerDoors = doorsForAnswer(\n (result.loop.toolUsages ?? []).map((usage: any) => usage?.name).filter(Boolean),\n name => (findToolSpec(name, settings.toolCategories) as any)?.doors\n )\n\n let assistantMessageId: string | undefined\n if (session) {\n const stored = await messageRepo.save({\n session: { id: session.id } as any,\n role: 'assistant',\n content: result.reply,\n /*\n * 이 답이 지나온 문을 함께 적는다 — ADR-0073 판정 1. 방은 도메인 안에서 공개이므로, 이 문장이\n * 실어 나르는 값을 나중에 읽는 사람이 열 수 있는지 판정할 근거가 행에 있어야 한다. 적지 않으면\n * 빈 목록이 되고, 빈 목록은 누구나 읽는다는 뜻이다.\n */\n doors: answerDoors,\n toolUsages: result.loop.toolUsages?.length ? JSON.stringify(result.loop.toolUsages) : undefined,\n proposals: result.loop.proposals?.length ? JSON.stringify(result.loop.proposals) : undefined,\n groundingWarnings: result.groundingWarnings?.length ? JSON.stringify(result.groundingWarnings) : undefined,\n creator: user,\n updater: user,\n parentMessage: userMessageId ? ({ id: userMessageId } as any) : undefined\n })\n assistantMessageId = stored.id\n\n publishChatMessage({ domainId: domain.id, sessionId: session.id, message: stored } as any)\n /* The reply is session activity too. Leaving it out shows other participants \"nothing new\". */\n publishSessionActivity({\n domainId: domain.id,\n sessionId: session.id!,\n kind: 'message',\n anchorType: (session as any).anchorType,\n anchorId: (session as any).anchorId,\n preview: result.reply,\n actorId: user?.id\n })\n\n await sessionRepo.update(session.id!, {\n aiClientId: client.id,\n updater: user,\n ...activityPatch(result.reply, new Date(), answerDoors)\n } as any)\n\n /*\n * 상한 밖으로 밀려난 앞부분을 요약으로 접는다 — **답을 돌려준 뒤**에 한다(사용자 대기 시간에\n * 요약 호출을 얹지 않는다). 기다리지 않으므로 실패해도 대화에는 영향이 없다: 요약이 없으면 다음\n * 턴이 「N 개 생략」으로 정직하게 알린다.\n *\n * 이 문에는 이 호출이 없었다. 조각은 다 있었는데(상한 · 생략 알림 · `lastSummary` · 순수 규칙)\n * 부르는 쪽이 보드 제품 하나뿐이어서, 앱의 대화는 며칠이 지나도 앞부분을 요약하지 않고 매 턴\n * 「생략했다」고만 말했다 — 협의가 길어질수록 처음에 합의한 것이 먼저 사라진다.\n */\n void foldSessionHistory({\n sessionId: session.id!,\n domainId: domain.id,\n client,\n maxTurns: LLM_HISTORY_MAX_TURNS,\n locale: context.state?.lng\n })\n }\n\n return {\n reply: result.reply,\n clientId: client.id,\n sessionId: session?.id,\n userMessageId,\n assistantMessageId,\n toolUsages: result.loop.toolUsages?.length ? result.loop.toolUsages : null,\n offeredTools: result.offeredTools,\n proposals: result.loop.proposals?.length ? result.loop.proposals : null,\n groundingWarnings: result.groundingWarnings?.length ? result.groundingWarnings : null,\n historyFolded: historyFolded ?? null\n }\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"assistant-chat-resolver.js","sourceRoot":"","sources":["../../server/service/assistant-chat-resolver.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * The neutral door into a conversation.\n *\n * ── Why this exists (2026-09-11) ───────────────────────────────────────────────\n * The chat component moved here from `board-ai`, but the only mutation it could call stayed\n * behind. operato-figure mounted the dock, registered four tools, and got\n * `Cannot query field \"boardAIChat\" on type \"Mutation\"` — it does not load board-ai. The twin has\n * the same shape and it does not show: its space deliberation runs through board-ai's door purely\n * because the board authoring half of the same product already loaded that package.\n *\n * ── Why the board resolver was not moved ───────────────────────────────────────\n * `board-ai-resolver.ts` is 692 lines and the larger part of it is board authoring itself:\n * `currentBoard` as a BoardModel, PatchEntry, board patch broadcasts, refid injection for\n * component mentions. Moving that here would teach this package the board document, which is the\n * dependency we just cut, pointing the other way.\n *\n * So the door moved and the editor did not. `boardAIChat` stays in board-ai for board authoring;\n * this one is for a surface that asks questions and runs registered tools.\n *\n * ── What this is not ───────────────────────────────────────────────────────────\n * It has no document, so it takes no document and returns no patch. Tools that write to a\n * document are not offered here — `registryAssistantChat` enforces that, and it decides what may\n * be dispatched from the same list it offers, so the two cannot drift apart.\n */\nimport { Arg, Ctx, Directive, Field, InputType, Mutation, ObjectType, Resolver } from 'type-graphql'\nimport { GraphQLJSON } from 'graphql-scalars'\nimport '@things-factory/auth-base'\n\nimport { config } from '@things-factory/env'\nimport { getRepository } from '@things-factory/shell'\nimport {\n createAIClient,\n findToolSpec,\n getDefaultAIClient,\n registryAssistantChat,\n type AIClient\n} from '@things-factory/ai-client-base'\n\nimport { ChatSession } from './chat-session/chat-session.js'\nimport { assertStationSession } from './chat-session/station-session-guard.js'\nimport { ChatMessage } from './chat-message/chat-message.js'\nimport { buildLlmHistory } from './chat-message/llm-history.js'\nimport { splitByCap } from './chat-message/history-summary.js'\nimport { foldSessionHistory } from './chat-message/fold-session-history.js'\nimport { groundingSourcesFor } from './grounding-sources.js'\nimport { publishChatMessage } from './chat-message/chat-message-publish.js'\nimport { publishSessionActivity } from './chat-session/session-activity-publish.js'\nimport { activityPatch, autoSessionName } from './chat-session/session-inbox.js'\nimport { toolAccessFor } from './tool-access.js'\nimport { ChatSessionParticipant } from './chat-session-participant/chat-session-participant.js'\nimport { doorsForAnswer } from './chat-message/message-doors.js'\nimport { judgeSessionKind } from './chat-session/session-kind.js'\nimport { readerGate } from './chat-message/reader-gate.js'\nimport { inviteMentionedUsers } from './chat-session-participant/mention-invite.js'\nimport { buildInviteNotification } from './chat-session-participant/invite-notification.js'\nimport { isSessionParticipant } from './chat-session-participant/participant-lookup.js'\nimport { assertMayWriteSession } from './chat-session/session-write-gate.js'\nimport { attachmentHistoryLine, refuseAttachments, textCharsOf } from './chat-session/attachment-policy.js'\nimport { attachmentForModel, describeAttachment, findChatAttachment } from './chat-session/session-attachments.js'\nimport { getAssistantMode } from './assistant-mode/registry.js'\nimport { resolveModeSettings } from './assistant-mode/resolve.js'\nimport { requestTranslator } from './request-translator.js'\n\n/**\n * How many stored messages reach the model.\n *\n * A deliberation runs for days. Without a cap the prompt grows without bound and the oldest turns\n * bury the recent ones. Kept the same as the board conversation's cap so the two surfaces of one\n * product do not remember differently.\n */\nconst LLM_HISTORY_MAX_TURNS = 40\n\n@InputType()\nclass AssistantMessageInput {\n @Field({ description: \"Role: 'user' or 'assistant'\" })\n role: string\n\n @Field({ description: 'Message content' })\n content: string\n}\n\n@InputType({ description: 'Which conversation this is, and what it is about.' })\nclass AssistantModeInput {\n @Field({ description: 'A mode kind the app server registered — `<app>.<surface>`, e.g. \"twin.space\".' })\n kind: string\n\n @Field({\n nullable: true,\n description:\n 'What this conversation is about — the space, the station, the model being edited. With `kind` it is the conversation\\'s lifetime: when either changes the screen closes the conversation and starts another.'\n })\n target?: string\n}\n\n@InputType({ description: 'One turn of a conversation that has no document of its own.' })\nclass AssistantChatInput {\n @Field({\n nullable: true,\n description: 'ChatSession id. Omit for a one-off question with no persistence.'\n })\n sessionId?: string\n\n @Field(() => [AssistantMessageInput], {\n description:\n 'Conversation, newest last. For a persisted session only the last user message is appended; the rest is read from storage.'\n })\n messages: AssistantMessageInput[]\n\n @Field(() => AssistantModeInput, {\n description:\n 'Which conversation this is. The persona, the tool categories, the model and the limits come from the mode the app server registered (ADR-0061 decision 7) — a caller choosing its own tool categories would be a caller choosing its own authority.'\n })\n mode: AssistantModeInput\n\n @Field(() => GraphQLJSON, {\n nullable: true,\n description:\n 'What this surface is looking at (e.g. { figureId } while modelling). Tools read it so the model never has to ask the user for identifiers. Client-supplied — never used for authorization; a tool verifies the scope belongs to the caller tenant.'\n })\n hostContext?: any\n\n @Field({\n nullable: true,\n description:\n 'Last message to keep from the stored history. The client keeps its own truncation when a user edits a message, so pass the last kept id to hide the retracted span from the model.'\n })\n truncateAfterMessageId?: string\n\n @Field(() => [String], {\n nullable: true,\n description: 'Files stored with this conversation by attachToConversation, sent with this turn.'\n })\n attachmentIds?: string[]\n\n}\n\n@ObjectType()\nclass AssistantChatOutput {\n @Field({ description: 'The reply, including the notice when the loop stopped early.' })\n reply: string\n\n @Field({ description: 'AI client identifier (provider:model).' })\n clientId: string\n\n @Field({ nullable: true, description: 'Echo of the session id, when persisted.' })\n sessionId?: string\n\n @Field({ nullable: true, description: 'Stored ChatMessage id of the user input.' })\n userMessageId?: string\n\n @Field({ nullable: true, description: 'Stored ChatMessage id of the reply.' })\n assistantMessageId?: string\n\n @Field(() => GraphQLJSON, {\n nullable: true,\n description:\n 'Set when this turn did not fit: { omitted, summarized } — how many earlier messages the model did not see, and whether a summary stood in for them. Without it the person watches the conversation lose its beginning in silence, while the model is told.'\n })\n historyFolded?: any\n\n @Field(() => GraphQLJSON, {\n nullable: true,\n description: 'Tool usages during the turn — {name, arguments, result, kind}. The surface shows them foldably.'\n })\n toolUsages?: any\n\n @Field(() => [String], {\n nullable: true,\n description:\n 'Tool names offered to the model this turn. A surface that expected a category and sees it missing here knows the registration did not happen, rather than reading a vague answer as a refusal.'\n })\n offeredTools?: string[]\n\n @Field(() => GraphQLJSON, {\n nullable: true,\n description:\n 'Actions a tool recorded without executing — each carries what the tool returned alongside `proposed: true`. The surface renders a confirm button; executing is the user\\'s act.'\n })\n proposals?: any\n\n @Field(() => GraphQLJSON, {\n nullable: true,\n description:\n 'Identifiers in the reply that appear in nothing the model received. Hallucination candidates surfaced to the user; the reply itself is not altered. Null when the answer is grounded.'\n })\n groundingWarnings?: any\n}\n\nfunction resolveAIClient(): AIClient | undefined {\n const existing = getDefaultAIClient()\n if (existing) return existing\n const cfg = config.get('aiClient', null) as any\n if (cfg && cfg.provider) return createAIClient(cfg)\n return undefined\n}\n\n@Resolver()\nexport class AssistantChatResolver {\n /*\n * No `@transaction`, for the same reason the board conversation has none: a model call takes\n * seconds to tens of seconds, and holding a write lock for that long collides with everything\n * else on SQLite. Writes are short and around the call. For a conversation, a partly stored\n * history is safer than a lost one.\n */\n @Mutation(() => AssistantChatOutput, {\n description:\n 'Ask the assistant a question on a surface that has no document of its own. Tools come from the registry; with a sessionId the conversation is persisted.'\n })\n /*\n * `ai-assistant:mutation` — the same door as the board conversation's, which moved here on\n * 2026-09-10. No `legacy` alias: this operation is new, so no one was ever passing under an old\n * name, and declaring an alias would open a second name that nothing needs.\n */\n @Directive('@privilege(category: \"ai-assistant\", privilege: \"mutation\", domainOwnerGranted: true)')\n async assistantChat(@Arg('input') input: AssistantChatInput, @Ctx() context: any): Promise<AssistantChatOutput> {\n const client = resolveAIClient()\n if (!client) {\n throw new Error(\n 'No AI client configured. Set config.aiClient = { provider, model, apiKey } or call setDefaultAIClient(...).'\n )\n }\n\n /*\n * Read the declaration before anything is stored. A kind this server does not have is a\n * mismatched deployment, and the person should see that instead of a conversation with no\n * persona and no tools.\n */\n const t = requestTranslator(context)\n const settings = resolveModeSettings(input.mode, t)\n\n const { domain, user } = context.state\n const sessionRepo = getRepository(ChatSession)\n const messageRepo = getRepository(ChatMessage)\n\n let session: ChatSession | undefined\n if (input.sessionId) {\n const found = await sessionRepo.findOneBy({ id: input.sessionId, domain: { id: domain.id } as any })\n if (!found) throw new Error(`ChatSession ${input.sessionId} not found`)\n /*\n * A private conversation is written by its people only - and here more than anywhere: the answer is\n * built on this conversation's history, so an outsider's question would read it back to them.\n */\n await assertMayWriteSession({\n session: found as any,\n userId: user?.id,\n isParticipant: () => isSessionParticipant(domain.id, found.id!, user?.id),\n t: (key, options) => t(key, options as any)\n })\n if (found.anchorType === 'figure' && input.hostContext?.figureId !== found.anchorId) {\n throw new Error(\n context.t('ai-assistant.text.other-figure-history', {\n defaultValue: 'this conversation belongs to another figure. open the conversation of the model you have open.'\n })\n )\n }\n if (found.anchorType === 'application' && (input.hostContext?.app !== found.anchorId || input.hostContext?.figureId)) {\n throw new Error(\n context.t('ai-assistant.text.application-vs-model-history', {\n defaultValue: 'this conversation is about the application, not about a model. open the one you want.'\n })\n )\n }\n /*\n * 이 세션이 **이 대화의** 세션인가 — ADR-0072 결정 3. 다른 대화의 세션에 말을 붙이면 그 이력이\n * 이 대화의 프롬프트로 들어가고, 화면은 줄이 사라져 새 대화처럼 보이므로 보낸 사람은 모른다.\n */\n const verdict = judgeSessionKind((found as any).kind, input.mode.kind)\n if (verdict.action === 'refuse') {\n throw new Error(\n t('ai-assistant.text.session-kind-mismatch', {\n ...verdict.params,\n defaultValue: 'this conversation is {sessionKind}, and you are writing as {requested}. open the conversation you mean.'\n })\n )\n }\n if (verdict.action === 'stamp') {\n /* 칸이 서기 전에 난 세션 — 요청한 대화로 한 번 찍는다. 다음부터는 위 규칙 하나만 남는다. */\n await sessionRepo.update(found.id!, { kind: verdict.kind } as any)\n ;(found as any).kind = verdict.kind\n }\n session = found\n }\n\n assertStationSession(session, input.hostContext, t)\n\n const lastUserMessage = input.messages[input.messages.length - 1]\n let userMessageId: string | undefined\n\n /*\n * Files sent with this turn (design/ai/shared-dock.md §9). They were stored with this conversation by\n * attachToConversation; one that is not this conversation's is refused, never read. The policy is the\n * mode's - checked again here, since the door is the authority, not the screen.\n */\n const attachmentRows: any[] = []\n if (input.attachmentIds?.length) {\n if (!session) throw new Error(t('ai-assistant.text.attachments-need-session', { defaultValue: 'attachments need a saved conversation.' }))\n for (const id of input.attachmentIds) {\n const row = await findChatAttachment(domain.id, session.id!, id)\n if (!row) throw new Error(t('ai-assistant.text.attachment-not-in-session', { defaultValue: 'an attachment does not belong to this conversation.' }))\n attachmentRows.push(row)\n }\n const refusal = refuseAttachments(\n getAssistantMode(input.mode.kind)?.attachments,\n attachmentRows.map(row => ({ name: row.name, type: row.mimetype, size: Number(row.size) }))\n )\n if (refusal) throw new Error(t(`ai-assistant.text.${refusal.code}`, { ...(refusal as any), defaultValue: refusal.code }))\n }\n\n if (session && lastUserMessage?.role === 'user') {\n const previousLast = await messageRepo.findOne({\n where: { session: { id: session.id } as any },\n order: { createdAt: 'DESC' }\n })\n const saved = await messageRepo.save({\n session: { id: session.id } as any,\n role: 'user',\n content: lastUserMessage.content,\n /*\n * This door is the one people call to ask the assistant, so a turn stored here was for the\n * assistant (ADR-0070 A15 decision ①). The value comes from which door was called, never\n * from the request — two sources for one fact can disagree.\n */\n addressee: 'assistant',\n attachments: attachmentRows.length ? JSON.stringify(attachmentRows.map(describeAttachment)) : undefined,\n creator: user,\n updater: user,\n parentMessage: previousLast ? ({ id: previousLast.id } as any) : undefined\n } as any)\n userMessageId = saved.id\n\n /*\n * Session activity is denormalised for the list: most-recent ordering and another\n * participant's unread badge read it. Best effort — a conversation must not fail because a\n * list sorts one position late.\n */\n try {\n const patch: any = activityPatch(lastUserMessage.content)\n if (!session.name) patch.name = autoSessionName(lastUserMessage.content)\n await sessionRepo.update(session.id!, patch)\n } catch {\n /* noop */\n }\n\n /*\n * 사람이 `@이름` 으로 부른 사람은 이 대화에 들어온다(ADR-0071 결정 1). 이 문은 **사람의 턴**을\n * 저장하는 문이라 그 일을 한다 — 모델의 답이나 도구 결과는 아무도 초대하지 않는다.\n */\n const invite = await inviteMentionedUsers({\n content: lastUserMessage.content,\n sessionId: session.id!,\n domainId: domain.id,\n inviter: user,\n participants: getRepository(ChatSessionParticipant),\n visibility: (session as any).visibility\n })\n if (invite.invited.length > 0) {\n const payload = buildInviteNotification(invite.invited, undefined, user, (session as any)?.name)\n try {\n if (payload) await (context.state as any)?.notify?.(payload)\n } catch {\n /* 편지가 못 갔다고 방에 들어온 사실이 취소되지는 않는다. */\n }\n }\n\n publishChatMessage({ domainId: domain.id, sessionId: session.id, message: saved } as any)\n publishSessionActivity({\n domainId: domain.id,\n sessionId: session.id!,\n kind: 'message',\n anchorType: (session as any).anchorType,\n anchorId: (session as any).anchorId,\n preview: lastUserMessage.content,\n actorId: user?.id\n })\n }\n\n /*\n * 이번 턴에 상한 밖으로 밀려난 것이 있는가 — 화면이 「대화가 길어졌다」를 알릴 근거다.\n *\n * 이 문은 그 사실을 내지 않았다. 대화창에는 그 안내를 그리는 자리가 있고(`historyFolded`), 보드\n * 문만 그 값을 채웠다. 그래서 앱의 대화는 앞부분이 잘려 나가도 **사람에게는 아무 말도 하지 않았다** —\n * 모델에게는 「N 개 생략」을 적어 보내면서.\n */\n const readRows = session\n ? await messageRepo.find({\n where: { session: { id: session.id } as any },\n order: { createdAt: 'ASC' },\n relations: ['creator']\n })\n : []\n /*\n * 모델이 읽는 이력도 **부른 사람** 기준으로 가린다 — ADR-0073 판정 1. 가리지 않으면, 문을 한 번도\n * 안 지난 사람이 \"아까 그 수 다시 말해 줘\" 한 마디로 값을 가져간다. 모델이 옮겨 적는 순간 그 값은\n * 문 없는 새 답이 된다. 가린 자리를 지우지 않고 한 줄로 남기는 이유도 같다 — 지우면 모델이 그런\n * 말이 없었다고 답하고, 그것은 거짓말이다.\n */\n const storedRows = await readerGate(context)(readRows)\n const capped = splitByCap(\n storedRows.map(row => ({ id: row.id, role: row.role, content: row.content ?? '' })),\n LLM_HISTORY_MAX_TURNS\n )\n const historyFolded =\n capped.dropped.length > 0\n ? { omitted: capped.dropped.length, summarized: Boolean((session as any)?.lastSummary) }\n : undefined\n\n const conversation = session\n ? buildLlmHistory(\n /*\n * The model reads who spoke and who they spoke to. The entity carries the speaker as a\n * relation and the addressee as a column; the history wants them as plain fields, and\n * without this mapping every line arrived unattributed — a room of people read as one\n * voice (ADR-0070 A15 decision ②).\n */\n storedRows.map(message => ({\n id: message.id,\n role: message.role,\n /*\n * A file sent in an earlier turn is not sent again - one line names it, and the model opens it\n * with readChatAttachment when it needs to (cost is a usage axis, ADR-0061 decision 6).\n */\n content:\n message.id !== userMessageId && (message as any).attachmentsJson?.length\n ? [message.content, ...(message as any).attachmentsJson.map(attachmentHistoryLine)].join('\\n')\n : message.content,\n senderId: (message as any).creator?.id,\n senderName: (message as any).creator?.name,\n addressee: (message as any).addressee\n })),\n {\n maxTurns: LLM_HISTORY_MAX_TURNS,\n truncateAfterMessageId: input.truncateAfterMessageId,\n /* The message just stored is this turn's question — truncation must never drop it. */\n keepMessageIds: userMessageId ? [userMessageId] : undefined,\n summary: (session as any).lastSummary\n ? { text: (session as any).lastSummary, upToMessageId: (session as any).summaryUpToMessageId }\n : undefined\n }\n )\n : input.messages.map(message => ({ role: message.role as any, content: message.content }))\n\n if (attachmentRows.length && conversation.length > 0) {\n const last = conversation[conversation.length - 1]\n if (last.role === 'user' && typeof last.content === 'string') {\n const parts: any[] = [{ type: 'text', text: last.content }]\n for (const row of attachmentRows) {\n const file = await attachmentForModel(row, textCharsOf(getAssistantMode(input.mode?.kind ?? '')?.attachments))\n if (file.type === 'image') {\n parts.push({ type: 'image', source: { kind: 'base64', data: file.data, mediaType: file.mediaType } })\n } else {\n parts.push({\n type: 'text',\n text: `[attachment ${row.id}: ${file.name}${file.truncated ? ' - cut, the file is longer' : ''}]\\n${file.text}`\n })\n }\n }\n last.content = parts as any\n }\n } else if (input.hostContext?.image?.data && conversation.length > 0) {\n /* The screen-image path (figure's rendered view) - not a file of the conversation. */\n const last = conversation[conversation.length - 1]\n if (last.role === 'user' && typeof last.content === 'string') {\n const raw = input.hostContext.image\n const rawData = typeof raw.data === 'string' ? raw.data.replace(/^data:image\\/[a-zA-Z]+;base64,/, '') : raw.data\n const buf = typeof rawData === 'string' ? Buffer.from(rawData, 'base64') : Buffer.from(rawData)\n last.content = [\n { type: 'text', text: last.content },\n { type: 'image', source: { kind: 'base64', data: buf, mediaType: raw.mediaType || 'image/png' } }\n ] as any\n }\n }\n\n const result = await registryAssistantChat({\n client,\n messages: conversation as any,\n systemPrompt: settings.systemPrompt,\n toolCategories: settings.toolCategories,\n /* 도구가 「어느 대화에서 불렸나」를 안다 — 서버의 값이다(ADR-0061 결정 3). */\n mode: { kind: input.mode.kind, ...(input.mode.target ? { target: input.mode.target } : {}) },\n toolAccess: toolAccessFor(context),\n /* The stop notice is a sentence a person reads — the server writes it in their language (ADR-0054). */\n t,\n hostContext: input.hostContext,\n /*\n * 근거는 도구 결과만이 아니다 — 시스템 프롬프트 · 이 대화의 말 · 화면이 보낸 스냅숏도 근거다\n * (`ai-client-base/utils/grounding`). 이 문은 그 셋을 넘기지 않아, **사람이 방금 타이핑한\n * 식별자**를 되받아 적은 답이 「근거 없음」으로 표시됐다. 그런 경고가 몇 번 나오면 사람이 경고\n * 자체를 안 보게 된다.\n */\n groundingSources: groundingSourcesFor({\n systemPrompt: settings.systemPrompt,\n messages: conversation as any,\n hostContext: input.hostContext\n }),\n state: context.state,\n model: settings.model,\n temperature: settings.temperature,\n maxTokens: settings.maxTokens,\n requireToolOnFirstTurn: settings.requireToolOnFirstTurn\n })\n\n /*\n * 이 답이 지나온 문 — ADR-0073 판정 1. 행에도, 목록 미리보기에도 같은 값이 간다. 한쪽만 적으면\n * 가려진 답이 목록에는 그대로 뜬다.\n */\n const answerDoors = doorsForAnswer(\n (result.loop.toolUsages ?? []).map((usage: any) => usage?.name).filter(Boolean),\n name => (findToolSpec(name, settings.toolCategories) as any)?.doors\n )\n\n let assistantMessageId: string | undefined\n if (session) {\n const stored = await messageRepo.save({\n session: { id: session.id } as any,\n role: 'assistant',\n content: result.reply,\n /*\n * 이 답이 지나온 문을 함께 적는다 — ADR-0073 판정 1. 방은 도메인 안에서 공개이므로, 이 문장이\n * 실어 나르는 값을 나중에 읽는 사람이 열 수 있는지 판정할 근거가 행에 있어야 한다. 적지 않으면\n * 빈 목록이 되고, 빈 목록은 누구나 읽는다는 뜻이다.\n */\n doors: answerDoors,\n toolUsages: result.loop.toolUsages?.length ? JSON.stringify(result.loop.toolUsages) : undefined,\n proposals: result.loop.proposals?.length ? JSON.stringify(result.loop.proposals) : undefined,\n groundingWarnings: result.groundingWarnings?.length ? JSON.stringify(result.groundingWarnings) : undefined,\n creator: user,\n updater: user,\n parentMessage: userMessageId ? ({ id: userMessageId } as any) : undefined\n })\n assistantMessageId = stored.id\n\n publishChatMessage({ domainId: domain.id, sessionId: session.id, message: stored } as any)\n /* The reply is session activity too. Leaving it out shows other participants \"nothing new\". */\n publishSessionActivity({\n domainId: domain.id,\n sessionId: session.id!,\n kind: 'message',\n anchorType: (session as any).anchorType,\n anchorId: (session as any).anchorId,\n preview: result.reply,\n actorId: user?.id\n })\n\n await sessionRepo.update(session.id!, {\n aiClientId: client.id,\n updater: user,\n ...activityPatch(result.reply, new Date(), answerDoors)\n } as any)\n\n /*\n * 상한 밖으로 밀려난 앞부분을 요약으로 접는다 — **답을 돌려준 뒤**에 한다(사용자 대기 시간에\n * 요약 호출을 얹지 않는다). 기다리지 않으므로 실패해도 대화에는 영향이 없다: 요약이 없으면 다음\n * 턴이 「N 개 생략」으로 정직하게 알린다.\n *\n * 이 문에는 이 호출이 없었다. 조각은 다 있었는데(상한 · 생략 알림 · `lastSummary` · 순수 규칙)\n * 부르는 쪽이 보드 제품 하나뿐이어서, 앱의 대화는 며칠이 지나도 앞부분을 요약하지 않고 매 턴\n * 「생략했다」고만 말했다 — 협의가 길어질수록 처음에 합의한 것이 먼저 사라진다.\n */\n void foldSessionHistory({\n sessionId: session.id!,\n domainId: domain.id,\n client,\n maxTurns: LLM_HISTORY_MAX_TURNS,\n locale: context.state?.lng\n })\n }\n\n return {\n reply: result.reply,\n clientId: client.id,\n sessionId: session?.id,\n userMessageId,\n assistantMessageId,\n toolUsages: result.loop.toolUsages?.length ? result.loop.toolUsages : null,\n offeredTools: result.offeredTools,\n proposals: result.loop.proposals?.length ? result.loop.proposals : null,\n groundingWarnings: result.groundingWarnings?.length ? result.groundingWarnings : null,\n historyFolded: historyFolded ?? null\n }\n }\n}\n"]}
|
|
@@ -75,7 +75,7 @@ tslib_1.__decorate([
|
|
|
75
75
|
tslib_1.__decorate([
|
|
76
76
|
(0, type_graphql_1.Field)(() => graphql_scalars_1.GraphQLJSON, {
|
|
77
77
|
nullable: true,
|
|
78
|
-
description: "What this conversation accepts as attachments: { kinds: ('image'|'text')[], maxBytes, maxFiles }. Null: none."
|
|
78
|
+
description: "What this conversation accepts as attachments: { kinds: ('image'|'text'|'spreadsheet')[], maxBytes, maxFiles, maxChars }. Null: none."
|
|
79
79
|
}),
|
|
80
80
|
tslib_1.__metadata("design:type", Object)
|
|
81
81
|
], AssistantModeView.prototype, "attachments", void 0);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"assistant-mode-resolver.js","sourceRoot":"","sources":["../../../server/service/assistant-mode/assistant-mode-resolver.ts"],"names":[],"mappings":";;;;AAAA;;;;;;;;GAQG;AACH,+CAAsF;AACtF,qDAA6C;AAC7C,qCAAkC;AAElC,6CAAgD;AAChD,+CAAwD;AACxD,oEAA4D;AAG5D,IAAM,sBAAsB,GAA5B,MAAM,sBAAsB;CAS3B,CAAA;AAPC;IADC,IAAA,oBAAK,EAAC,EAAE,WAAW,EAAE,+BAA+B,EAAE,CAAC;;oDAC5C;AAGZ;IADC,IAAA,oBAAK,EAAC,EAAE,WAAW,EAAE,8BAA8B,EAAE,CAAC;;2DACpC;AAGnB;IADC,IAAA,oBAAK,EAAC,EAAE,WAAW,EAAE,uCAAuC,EAAE,CAAC;;wDAChD;AARZ,sBAAsB;IAD3B,IAAA,yBAAU,EAAC,EAAE,WAAW,EAAE,iDAAiD,EAAE,CAAC;GACzE,sBAAsB,CAS3B;AAGD,IAAM,iBAAiB,GAAvB,MAAM,iBAAiB;CAiCtB,CAAA;AA/BC;IADC,IAAA,oBAAK,EAAC,EAAE,WAAW,EAAE,+BAA+B,EAAE,CAAC;;+CAC5C;AAGZ;IADC,IAAA,oBAAK,EAAC,GAAG,EAAE,CAAC,CAAC,sBAAsB,CAAC,EAAE,EAAE,WAAW,EAAE,kBAAkB,EAAE,CAAC;;yDACnC;AAGxC;IADC,IAAA,oBAAK,EAAC,GAAG,EAAE,CAAC,6BAAW,EAAE,EAAE,WAAW,EAAE,kBAAkB,EAAE,CAAC;;yDACzC;AAGrB;IADC,IAAA,oBAAK,EAAC,GAAG,EAAE,CAAC,6BAAW,EAAE,EAAE,WAAW,EAAE,8DAA8D,EAAE,CAAC;;0DACpF;AAGtB;IADC,IAAA,oBAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,4DAA4D,EAAE,CAAC;;sDACjF;AAGpB;IADC,IAAA,oBAAK,EAAC,GAAG,EAAE,CAAC,6BAAW,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,+CAA+C,EAAE,CAAC;;gDAChE;AAG3C;IADC,IAAA,oBAAK,EAAC,GAAG,EAAE,CAAC,6BAAW,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,0DAA0D,EAAE,CAAC;sCAC3G,KAAK;mDAAoC;AAGpD;IADC,IAAA,oBAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,uCAAuC,EAAE,CAAC;;uDAC3D;AAGrB;IADC,IAAA,oBAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,yDAAyD,EAAE,CAAC;;uDAC7E;AAMrB;IAJC,IAAA,oBAAK,EAAC,GAAG,EAAE,CAAC,6BAAW,EAAE;QACxB,QAAQ,EAAE,IAAI;QACd,WAAW,EAAE
|
|
1
|
+
{"version":3,"file":"assistant-mode-resolver.js","sourceRoot":"","sources":["../../../server/service/assistant-mode/assistant-mode-resolver.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * The screen asks what its conversation offers.\n *\n * The `/` sentences, the `@` catalog and the two notices used to travel from the browser into the\n * request, which meant the screen and the door could describe the same conversation differently.\n * They are declared once, in the mode, as i18n keys — and this query hands them over already\n * written in the request's locale (ADR-0054, and the same rule A11 applied to the stop notice).\n * The persona, the tool categories and the limits stay on the server: nothing here returns them.\n */\nimport { Arg, Ctx, Directive, Field, ObjectType, Query, Resolver } from 'type-graphql'\nimport { GraphQLJSON } from 'graphql-scalars'\nimport '@things-factory/auth-base'\n\nimport { assistantModeView } from './resolve.js'\nimport { assistantModesForSubject } from './registry.js'\nimport { requestTranslator } from '../request-translator.js'\n\n@ObjectType({ description: 'One `/` sentence, in the words of this request.' })\nclass AssistantModeSlashView {\n @Field({ description: 'Short name shown in the list.' })\n name: string\n\n @Field({ description: 'One line about what it does.' })\n description: string\n\n @Field({ description: 'The sentence that lands in the input.' })\n template: string\n}\n\n@ObjectType({ description: 'What a conversation offers on screen.' })\nclass AssistantModeView {\n @Field({ description: 'The mode kind this describes.' })\n kind: string\n\n @Field(() => [AssistantModeSlashView], { description: 'What `/` offers.' })\n slashTemplates: AssistantModeSlashView[]\n\n @Field(() => GraphQLJSON, { description: 'What `@` offers.' })\n catalogEntries: any[]\n\n @Field(() => GraphQLJSON, { description: 'Follow-up chips for a proposal that arrived without its own.' })\n proposalChoices: any[]\n\n @Field({ nullable: true, description: 'What the empty input says — what this conversation is for.' })\n placeholder?: string\n\n @Field(() => GraphQLJSON, { nullable: true, description: 'Title and one line for an empty conversation.' })\n intro?: { title: string; subtitle: string }\n\n @Field(() => GraphQLJSON, { nullable: true, description: 'Example sentences this conversation can actually answer.' })\n examples?: Array<{ label: string; items: string[] }>\n\n @Field({ nullable: true, description: 'The line under an empty conversation.' })\n footerNotice?: string\n\n @Field({ nullable: true, description: 'The line shown once the host reports a proposal staged.' })\n stagedNotice?: string\n\n @Field(() => GraphQLJSON, {\n nullable: true,\n description: \"What this conversation accepts as attachments: { kinds: ('image'|'text'|'spreadsheet')[], maxBytes, maxFiles, maxChars }. Null: none.\"\n })\n attachments?: any\n}\n\n@ObjectType({ description: 'A conversation a screen may open for what it is showing.' })\nclass AssistantModeChoice {\n @Field({ description: 'The mode kind to open.' })\n kind: string\n\n @Field({ description: 'What to call it where a person chooses — already in the words of this request.' })\n label: string\n\n @Field({ description: 'Whether this is the one to open when the person has not chosen.' })\n isDefault: boolean\n}\n\n@Resolver()\nexport class AssistantModeResolver {\n /*\n * 이 화면이 보여 주는 것으로 열 수 있는 대화들 — ADR-0072 A17 ①.\n *\n * 화면은 자기가 무엇을 보여 주는지(`subject`)만 안다. 그 위에 어느 대화가 서는지는 앱 서버가\n * 선언한다. 화면이 종류 목록을 손으로 들고 있으면, 대화를 하나 더 세운 날 그 화면만 모른 채로 남는다.\n *\n * 하나만 오면 고르개를 내지 않는다 — 고를 것이 없는 고르개는 자리만 차지한다.\n */\n @Query(() => [AssistantModeChoice], {\n description:\n 'Conversations that stand for this kind of subject, the default first. A screen showing one subject opens the head of this list and offers the rest; a single entry means there is nothing to choose.'\n })\n @Directive('@privilege(category: \"ai-assistant\", privilege: \"query\", domainOwnerGranted: true)')\n async assistantModes(@Arg('subject') subject: string, @Ctx() context: any): Promise<AssistantModeChoice[]> {\n const t = requestTranslator(context)\n\n return assistantModesForSubject(subject).map(mode => ({\n kind: mode.kind,\n /*\n * 이름이 없는 대화는 종류를 그대로 쓴다. 부팅 보고가 이미 그것을 경고로 말하므로 여기서 숨기지\n * 않는다 — 화면에 `twin.space` 가 뜨면 그것이 빠진 선언을 가리킨다.\n */\n label: mode.labelKey ? t(mode.labelKey) : mode.kind,\n isDefault: (mode.defaultFor ?? []).includes(subject)\n }))\n }\n @Query(() => AssistantModeView, {\n description: 'What this conversation offers on screen — `/` sentences, the `@` catalog and the notices, translated.'\n })\n /* Reading what a conversation offers is reading, and the same door the session list uses. */\n @Directive('@privilege(category: \"ai-assistant\", privilege: \"query\", domainOwnerGranted: true)')\n async assistantMode(@Arg('kind') kind: string, @Ctx() context: any): Promise<AssistantModeView> {\n return assistantModeView(kind, requestTranslator(context)) as AssistantModeView\n }\n}\n"]}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
export type AttachmentKind = 'image' | 'text';
|
|
1
|
+
export type AttachmentKind = 'image' | 'text' | 'spreadsheet';
|
|
2
2
|
export interface AttachmentPolicy {
|
|
3
3
|
kinds: AttachmentKind[];
|
|
4
4
|
/** Per file. */
|
|
5
5
|
maxBytes?: number;
|
|
6
6
|
/** Per message. */
|
|
7
7
|
maxFiles?: number;
|
|
8
|
+
/** Per text file, the characters the model reads - a sheet read into text longer than this is refused, not cut. */
|
|
9
|
+
maxChars?: number;
|
|
8
10
|
}
|
|
9
11
|
export declare const DEFAULT_MAX_BYTES: number;
|
|
10
12
|
export declare const DEFAULT_MAX_FILES = 5;
|
|
@@ -29,7 +31,8 @@ export type AttachmentRefusal = {
|
|
|
29
31
|
code: 'too-many-attachments';
|
|
30
32
|
maxFiles: number;
|
|
31
33
|
};
|
|
32
|
-
/**
|
|
34
|
+
/** The characters of a text file the model reads under this policy. */
|
|
35
|
+
export declare function textCharsOf(policy: AttachmentPolicy | null | undefined): number;
|
|
33
36
|
export declare function refuseAttachments(policy: AttachmentPolicy | null | undefined, files: {
|
|
34
37
|
name?: string;
|
|
35
38
|
type?: string;
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
10
|
exports.TEXT_CHARS_FOR_MODEL = exports.DEFAULT_MAX_FILES = exports.DEFAULT_MAX_BYTES = void 0;
|
|
11
11
|
exports.attachmentKindOf = attachmentKindOf;
|
|
12
|
+
exports.textCharsOf = textCharsOf;
|
|
12
13
|
exports.refuseAttachments = refuseAttachments;
|
|
13
14
|
exports.attachmentHistoryLine = attachmentHistoryLine;
|
|
14
15
|
exports.DEFAULT_MAX_BYTES = 5 * 1024 * 1024;
|
|
@@ -30,6 +31,14 @@ function attachmentKindOf(file) {
|
|
|
30
31
|
return null;
|
|
31
32
|
}
|
|
32
33
|
/** `null` when every file may go; otherwise the first reason, by name - said before sending, never cut silently. */
|
|
34
|
+
/** A kind the policy takes - text also when it takes spreadsheets, which arrive as the text of their sheets. */
|
|
35
|
+
function takes(policy, kind) {
|
|
36
|
+
return policy.kinds.includes(kind) || (kind === 'text' && policy.kinds.includes('spreadsheet'));
|
|
37
|
+
}
|
|
38
|
+
/** The characters of a text file the model reads under this policy. */
|
|
39
|
+
function textCharsOf(policy) {
|
|
40
|
+
return policy?.maxChars ?? exports.TEXT_CHARS_FOR_MODEL;
|
|
41
|
+
}
|
|
33
42
|
function refuseAttachments(policy, files) {
|
|
34
43
|
if (!files.length)
|
|
35
44
|
return null;
|
|
@@ -41,7 +50,7 @@ function refuseAttachments(policy, files) {
|
|
|
41
50
|
const maxBytes = policy.maxBytes ?? exports.DEFAULT_MAX_BYTES;
|
|
42
51
|
for (const file of files) {
|
|
43
52
|
const kind = attachmentKindOf(file);
|
|
44
|
-
if (!kind || !policy
|
|
53
|
+
if (!kind || !takes(policy, kind)) {
|
|
45
54
|
return { code: 'attachment-kind-not-accepted', name: file.name || '', kinds: policy.kinds };
|
|
46
55
|
}
|
|
47
56
|
if ((file.size ?? 0) > maxBytes)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"attachment-policy.js","sourceRoot":"","sources":["../../../server/service/chat-session/attachment-policy.ts"],"names":[],"mappings":";AAAA;;;;;;GAMG;;;
|
|
1
|
+
{"version":3,"file":"attachment-policy.js","sourceRoot":"","sources":["../../../server/service/chat-session/attachment-policy.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * What a conversation accepts as attachments - declared by its mode (design/ai/shared-dock.md §9-1).\n *\n * Pure, so the rule is tested alone and the screen can say \"no\" before anything is sent. A mode that\n * declares nothing accepts nothing: the chat once took .csv and .json, carried them, and no code ever\n * showed them to the model - the person got an answer about a file it had never seen (AI standard §7).\n */\n\n/*\n * 'spreadsheet' is a kind a mode opens, never a file the door receives: the screen reads each .xlsx sheet into CSV\n * text before sending (client/utils/spreadsheet-text.ts), so a mode that takes spreadsheets takes that text.\n */\nexport type AttachmentKind = 'image' | 'text' | 'spreadsheet'\n\nexport interface AttachmentPolicy {\n kinds: AttachmentKind[]\n /** Per file. */\n maxBytes?: number\n /** Per message. */\n maxFiles?: number\n /** Per text file, the characters the model reads - a sheet read into text longer than this is refused, not cut. */\n maxChars?: number\n}\n\nexport const DEFAULT_MAX_BYTES = 5 * 1024 * 1024\nexport const DEFAULT_MAX_FILES = 5\n/** How much of a text file reaches the model in the turn it was sent. */\nexport const TEXT_CHARS_FOR_MODEL = 20_000\n\nconst TEXT_TYPES = ['text/plain', 'text/csv', 'text/markdown', 'application/json']\nconst TEXT_EXTENSIONS = ['.txt', '.csv', '.md', '.json']\n\n/** Which kind a file is, from its type - and its name when the browser did not know the type. */\nexport function attachmentKindOf(file: { type?: string; name?: string }): AttachmentKind | null {\n const type = (file.type || '').toLowerCase()\n if (type.startsWith('image/')) return 'image'\n if (TEXT_TYPES.includes(type)) return 'text'\n const name = (file.name || '').toLowerCase()\n if (TEXT_EXTENSIONS.some(ext => name.endsWith(ext))) return 'text'\n return null\n}\n\nexport type AttachmentRefusal =\n | { code: 'attachments-not-accepted' }\n | { code: 'attachment-kind-not-accepted'; name: string; kinds: AttachmentKind[] }\n | { code: 'attachment-too-large'; name: string; maxBytes: number }\n | { code: 'too-many-attachments'; maxFiles: number }\n\n/** `null` when every file may go; otherwise the first reason, by name - said before sending, never cut silently. */\n/** A kind the policy takes - text also when it takes spreadsheets, which arrive as the text of their sheets. */\nfunction takes(policy: AttachmentPolicy, kind: AttachmentKind): boolean {\n return policy.kinds.includes(kind) || (kind === 'text' && policy.kinds.includes('spreadsheet'))\n}\n\n/** The characters of a text file the model reads under this policy. */\nexport function textCharsOf(policy: AttachmentPolicy | null | undefined): number {\n return policy?.maxChars ?? TEXT_CHARS_FOR_MODEL\n}\n\nexport function refuseAttachments(\n policy: AttachmentPolicy | null | undefined,\n files: { name?: string; type?: string; size?: number }[]\n): AttachmentRefusal | null {\n if (!files.length) return null\n if (!policy || !policy.kinds?.length) return { code: 'attachments-not-accepted' }\n\n const maxFiles = policy.maxFiles ?? DEFAULT_MAX_FILES\n if (files.length > maxFiles) return { code: 'too-many-attachments', maxFiles }\n\n const maxBytes = policy.maxBytes ?? DEFAULT_MAX_BYTES\n for (const file of files) {\n const kind = attachmentKindOf(file)\n if (!kind || !takes(policy, kind)) {\n return { code: 'attachment-kind-not-accepted', name: file.name || '', kinds: policy.kinds }\n }\n if ((file.size ?? 0) > maxBytes) return { code: 'attachment-too-large', name: file.name || '', maxBytes }\n }\n return null\n}\n\n/** The line history keeps for a file once its turn is past - the model reads the file again only by asking. */\nexport function attachmentHistoryLine(file: { id: string; name?: string; mimetype?: string; size?: number | string }): string {\n return `[attachment ${file.id}: ${file.name || 'unnamed'} (${file.mimetype || 'unknown type'}, ${file.size ?? '?'} bytes) - readChatAttachment to open it]`\n}\n"]}
|
|
@@ -18,6 +18,8 @@ const chat_session_js_1 = require("./chat-session.js");
|
|
|
18
18
|
const session_visibility_js_1 = require("./session-visibility.js");
|
|
19
19
|
const participant_lookup_js_1 = require("../chat-session-participant/participant-lookup.js");
|
|
20
20
|
const session_attachments_js_1 = require("./session-attachments.js");
|
|
21
|
+
const attachment_policy_js_1 = require("./attachment-policy.js");
|
|
22
|
+
const registry_js_1 = require("../assistant-mode/registry.js");
|
|
21
23
|
exports.CHAT_ATTACHMENT_CATEGORY = 'chat-attachments';
|
|
22
24
|
const readChatAttachment = {
|
|
23
25
|
kind: 'read',
|
|
@@ -44,7 +46,7 @@ const readChatAttachment = {
|
|
|
44
46
|
/* Same answer as absent - saying "it exists but is private" would itself leak. */
|
|
45
47
|
return { absent: 'not-found', attachmentId: id, message: 'there is no such attachment in a conversation here.' };
|
|
46
48
|
}
|
|
47
|
-
const file = await (0, session_attachments_js_1.attachmentForModel)(row);
|
|
49
|
+
const file = await (0, session_attachments_js_1.attachmentForModel)(row, (0, attachment_policy_js_1.textCharsOf)((0, registry_js_1.getAssistantMode)(session.kind || '')?.attachments));
|
|
48
50
|
return file.type === 'image'
|
|
49
51
|
? { attachmentId: id, name: file.name, type: 'image', note: 'images are shown to you only in the turn they were sent; ask the person to send it again to look at it.' }
|
|
50
52
|
: { attachmentId: id, name: file.name, type: 'text', text: file.text, ...(file.truncated ? { note: 'the file is longer; this is its beginning.' } : {}) };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"attachment-tools.js","sourceRoot":"","sources":["../../../server/service/chat-session/attachment-tools.ts"],"names":[],"mappings":";;;AAAA;;;;;;;;;GASG;AACH,mEAAuG;AACvG,qEAA4D;AAC5D,iDAAqD;AAErD,uDAA+C;AAC/C,mEAAwD;AACxD,6FAAwF;AACxF,qEAAkF;
|
|
1
|
+
{"version":3,"file":"attachment-tools.js","sourceRoot":"","sources":["../../../server/service/chat-session/attachment-tools.ts"],"names":[],"mappings":";;;AAAA;;;;;;;;;GASG;AACH,mEAAuG;AACvG,qEAA4D;AAC5D,iDAAqD;AAErD,uDAA+C;AAC/C,mEAAwD;AACxD,6FAAwF;AACxF,qEAAkF;AAClF,iEAAoD;AACpD,+DAAgE;AAEnD,QAAA,wBAAwB,GAAG,kBAAkB,CAAA;AAE1D,MAAM,kBAAkB,GAAa;IACnC,IAAI,EAAE,MAAM;IACZ,IAAI,EAAE,oBAAoB;IAC1B,KAAK,EAAE,CAAC,oBAAoB,CAAC;IAC7B,WAAW,EACT,uHAAuH;QACvH,2HAA2H;IAC7H,MAAM,EAAE;QACN,IAAI,EAAE,QAAQ;QACd,UAAU,EAAE,EAAE,YAAY,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,gDAAgD,EAAE,EAAE;QAC/G,QAAQ,EAAE,CAAC,cAAc,CAAC;KAC3B;IACD,OAAO,EAAE,KAAK,EAAE,IAAS,EAAE,OAAY,EAAE,EAAE;QACzC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,OAAO,EAAE,KAAK,IAAI,EAAE,CAAA;QAC7C,MAAM,EAAE,GAAG,OAAO,IAAI,EAAE,YAAY,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,YAAY,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAA;QACjF,IAAI,CAAC,MAAM,EAAE,EAAE,IAAI,CAAC,EAAE;YAAE,OAAO,EAAE,MAAM,EAAE,eAAe,EAAE,OAAO,EAAE,6BAA6B,EAAE,CAAA;QAElG,MAAM,GAAG,GAAQ,MAAM,IAAA,qBAAa,EAAC,4BAAU,CAAC,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,MAAM,CAAC,EAAE,EAAE,EAAE,OAAO,EAAE,4CAAmB,EAAS,EAAE,CAAC,CAAA;QAC3I,MAAM,OAAO,GAAQ,GAAG,IAAI,CAAC,MAAM,IAAA,qBAAa,EAAC,6BAAW,CAAC,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE,EAAE,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,MAAM,CAAC,EAAE,EAAE,EAAS,EAAE,CAAC,CAAC,CAAA;QACtI,IAAI,CAAC,GAAG,IAAI,CAAC,OAAO;YAAE,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,YAAY,EAAE,EAAE,EAAE,OAAO,EAAE,qDAAqD,EAAE,CAAA;QAEtI,MAAM,WAAW,GAAG,MAAM,IAAA,4CAAoB,EAAC,MAAM,CAAC,EAAE,EAAE,OAAO,CAAC,EAAE,EAAE,IAAI,EAAE,EAAE,CAAC,CAAA;QAC/E,IAAI,CAAC,IAAA,sCAAc,EAAC,OAAO,CAAC,UAAU,EAAE,WAAW,CAAC,EAAE,CAAC;YACrD,kFAAkF;YAClF,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,YAAY,EAAE,EAAE,EAAE,OAAO,EAAE,qDAAqD,EAAE,CAAA;QAClH,CAAC;QAED,MAAM,IAAI,GAAG,MAAM,IAAA,2CAAkB,EAAC,GAAG,EAAE,IAAA,kCAAW,EAAC,IAAA,8BAAgB,EAAC,OAAO,CAAC,IAAI,IAAI,EAAE,CAAC,EAAE,WAAW,CAAC,CAAC,CAAA;QAC1G,OAAO,IAAI,CAAC,IAAI,KAAK,OAAO;YAC1B,CAAC,CAAC,EAAE,YAAY,EAAE,EAAE,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,yGAAyG,EAAE;YACvK,CAAC,CAAC,EAAE,YAAY,EAAE,EAAE,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,4CAA4C,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAA;IAC7J,CAAC;CACU,CAAA;AAEA,QAAA,sBAAsB,GAAiB;IAClD,IAAI,EAAE,gCAAwB;IAC9B,QAAQ,EACN,wLAAwL;IAC1L,KAAK,EAAE,CAAC,kBAAkB,CAAC;CAC5B,CAAA;AAED,IAAA,qCAAoB,EAAC,8BAAsB,CAAC,CAAA","sourcesContent":["/*\n * readChatAttachment - the model opens a file sent in an earlier turn (design/ai/shared-dock.md §9-4).\n *\n * A file reaches the model in full only in the turn it was sent; later turns carry one line naming it.\n * When the model needs it again it asks here. The caller must be able to read the conversation the file\n * belongs to - the same gate as reading its messages (ADR-0073 decision 1) - so a file id from elsewhere\n * opens nothing.\n *\n * The category is opened for every mode that declares an attachment policy (resolve.ts).\n */\nimport { registerToolCategory, type ToolCategory, type ToolSpec } from '@things-factory/ai-client-base'\nimport { Attachment } from '@things-factory/attachment-base'\nimport { getRepository } from '@things-factory/shell'\n\nimport { ChatSession } from './chat-session.js'\nimport { mayReadSession } from './session-visibility.js'\nimport { isSessionParticipant } from '../chat-session-participant/participant-lookup.js'\nimport { attachmentForModel, CHAT_ATTACHMENT_REF } from './session-attachments.js'\nimport { textCharsOf } from './attachment-policy.js'\nimport { getAssistantMode } from '../assistant-mode/registry.js'\n\nexport const CHAT_ATTACHMENT_CATEGORY = 'chat-attachments'\n\nconst readChatAttachment: ToolSpec = {\n kind: 'read',\n name: 'readChatAttachment',\n doors: ['Query.chatMessages'],\n description:\n 'Open a file that was sent earlier in this conversation. History names such files as \"[attachment <id>: <name> ...]\"; ' +\n 'pass that id. An image comes back as a description request, a text file as its text (cut when long - the answer says so).',\n schema: {\n type: 'object',\n properties: { attachmentId: { type: 'string', description: 'The id from the \"[attachment <id>: ...]\" line.' } },\n required: ['attachmentId']\n },\n builder: async (args: any, context: any) => {\n const { domain, user } = context?.state ?? {}\n const id = typeof args?.attachmentId === 'string' ? args.attachmentId.trim() : ''\n if (!domain?.id || !id) return { absent: 'no-attachment', message: 'no attachment id was given.' }\n\n const row: any = await getRepository(Attachment).findOne({ where: { id, domain: { id: domain.id }, refType: CHAT_ATTACHMENT_REF } as any })\n const session: any = row && (await getRepository(ChatSession).findOne({ where: { id: row.refBy, domain: { id: domain.id } } as any }))\n if (!row || !session) return { absent: 'not-found', attachmentId: id, message: 'there is no such attachment in a conversation here.' }\n\n const participant = await isSessionParticipant(domain.id, session.id, user?.id)\n if (!mayReadSession(session.visibility, participant)) {\n /* Same answer as absent - saying \"it exists but is private\" would itself leak. */\n return { absent: 'not-found', attachmentId: id, message: 'there is no such attachment in a conversation here.' }\n }\n\n const file = await attachmentForModel(row, textCharsOf(getAssistantMode(session.kind || '')?.attachments))\n return file.type === 'image'\n ? { attachmentId: id, name: file.name, type: 'image', note: 'images are shown to you only in the turn they were sent; ask the person to send it again to look at it.' }\n : { attachmentId: id, name: file.name, type: 'text', text: file.text, ...(file.truncated ? { note: 'the file is longer; this is its beginning.' } : {}) }\n }\n} as ToolSpec\n\nexport const chatAttachmentCategory: ToolCategory = {\n name: CHAT_ATTACHMENT_CATEGORY,\n guidance:\n '- A line \"[attachment <id>: <name> ...]\" names a file sent in an earlier turn. Call readChatAttachment with that id before answering about its contents; never guess what a file says.',\n specs: [readChatAttachment]\n}\n\nregisterToolCategory(chatAttachmentCategory)\n"]}
|
|
@@ -11,10 +11,11 @@ export declare function saveChatAttachments(sessionId: string, files: any[], con
|
|
|
11
11
|
/** One file of the conversation - `undefined` when it is not this conversation's. */
|
|
12
12
|
export declare function findChatAttachment(domainId: string, sessionId: string, attachmentId: string): Promise<any | undefined>;
|
|
13
13
|
/**
|
|
14
|
-
* What the model gets for a file: an image as bytes, a text file as its text (cut at
|
|
14
|
+
* What the model gets for a file: an image as bytes, a text file as its text (cut at the mode's maxChars -
|
|
15
|
+
* TEXT_CHARS_FOR_MODEL when it declares none -
|
|
15
16
|
* and the cut is said), anything else as a line that names it.
|
|
16
17
|
*/
|
|
17
|
-
export declare function attachmentForModel(row: any): Promise<{
|
|
18
|
+
export declare function attachmentForModel(row: any, maxChars?: number): Promise<{
|
|
18
19
|
type: 'image';
|
|
19
20
|
data: Buffer;
|
|
20
21
|
mediaType: string;
|
|
@@ -38,18 +38,19 @@ async function findChatAttachment(domainId, sessionId, attachmentId) {
|
|
|
38
38
|
})) ?? undefined);
|
|
39
39
|
}
|
|
40
40
|
/**
|
|
41
|
-
* What the model gets for a file: an image as bytes, a text file as its text (cut at
|
|
41
|
+
* What the model gets for a file: an image as bytes, a text file as its text (cut at the mode's maxChars -
|
|
42
|
+
* TEXT_CHARS_FOR_MODEL when it declares none -
|
|
42
43
|
* and the cut is said), anything else as a line that names it.
|
|
43
44
|
*/
|
|
44
|
-
async function attachmentForModel(row) {
|
|
45
|
+
async function attachmentForModel(row, maxChars = attachment_policy_js_1.TEXT_CHARS_FOR_MODEL) {
|
|
45
46
|
const kind = (0, attachment_policy_js_1.attachmentKindOf)({ type: row.mimetype, name: row.name });
|
|
46
47
|
if (kind === 'image') {
|
|
47
48
|
const data = await attachment_base_1.STORAGE.readFile(row.path);
|
|
48
49
|
return { type: 'image', data: Buffer.isBuffer(data) ? data : Buffer.from(data), mediaType: row.mimetype, name: row.name };
|
|
49
50
|
}
|
|
50
51
|
const content = String(await attachment_base_1.STORAGE.readFile(row.path, 'utf8'));
|
|
51
|
-
const truncated = content.length >
|
|
52
|
-
return { type: 'text', text: truncated ? content.slice(0,
|
|
52
|
+
const truncated = content.length > maxChars;
|
|
53
|
+
return { type: 'text', text: truncated ? content.slice(0, maxChars) : content, name: row.name, truncated };
|
|
53
54
|
}
|
|
54
55
|
/** Removes these files from storage and their rows. */
|
|
55
56
|
async function removeAttachmentRows(ids) {
|