@things-factory/ai-assistant 10.1.112 → 10.1.114

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.
Files changed (49) hide show
  1. package/client/components/assistant-chat.ts +19 -0
  2. package/client/utils/open-page.ts +21 -0
  3. package/dist-client/components/assistant-chat.d.ts +2 -0
  4. package/dist-client/components/assistant-chat.js +19 -0
  5. package/dist-client/components/assistant-chat.js.map +1 -1
  6. package/dist-client/tsconfig.tsbuildinfo +1 -1
  7. package/dist-client/utils/open-page.d.ts +5 -0
  8. package/dist-client/utils/open-page.js +19 -0
  9. package/dist-client/utils/open-page.js.map +1 -0
  10. package/dist-server/service/assistant-mode/resolve.d.ts +1 -0
  11. package/dist-server/service/assistant-mode/resolve.js +2 -1
  12. package/dist-server/service/assistant-mode/resolve.js.map +1 -1
  13. package/dist-server/service/assistant-mode/types.d.ts +2 -0
  14. package/dist-server/service/assistant-mode/types.js.map +1 -1
  15. package/dist-server/service/chat-session/attachment-tool-result.d.ts +28 -0
  16. package/dist-server/service/chat-session/attachment-tool-result.js +9 -0
  17. package/dist-server/service/chat-session/attachment-tool-result.js.map +1 -0
  18. package/dist-server/service/chat-session/attachment-tools.js +4 -5
  19. package/dist-server/service/chat-session/attachment-tools.js.map +1 -1
  20. package/dist-server/service/data-read/list-read.d.ts +36 -0
  21. package/dist-server/service/data-read/list-read.js +64 -0
  22. package/dist-server/service/data-read/list-read.js.map +1 -0
  23. package/dist-server/service/data-read/read-target.d.ts +109 -0
  24. package/dist-server/service/data-read/read-target.js +189 -0
  25. package/dist-server/service/data-read/read-target.js.map +1 -0
  26. package/dist-server/service/data-read/read-tools.d.ts +228 -0
  27. package/dist-server/service/data-read/read-tools.js +197 -0
  28. package/dist-server/service/data-read/read-tools.js.map +1 -0
  29. package/dist-server/service/index.d.ts +3 -0
  30. package/dist-server/service/index.js +7 -1
  31. package/dist-server/service/index.js.map +1 -1
  32. package/dist-server/tsconfig.tsbuildinfo +1 -1
  33. package/package.json +5 -5
  34. package/server/service/assistant-mode/resolve.ts +3 -2
  35. package/server/service/assistant-mode/types.ts +2 -0
  36. package/server/service/chat-session/attachment-tool-result.test.ts +26 -0
  37. package/server/service/chat-session/attachment-tool-result.ts +18 -0
  38. package/server/service/chat-session/attachment-tools.ts +4 -5
  39. package/server/service/data-read/data-read.test.ts +229 -0
  40. package/server/service/data-read/list-read.ts +98 -0
  41. package/server/service/data-read/read-target.ts +230 -0
  42. package/server/service/data-read/read-tools.ts +212 -0
  43. package/server/service/index.ts +4 -0
  44. package/test/open-page.test.ts +31 -0
  45. package/translations/en.json +14 -0
  46. package/translations/ja.json +14 -0
  47. package/translations/ko.json +14 -0
  48. package/translations/ms.json +14 -0
  49. package/translations/zh.json +14 -0
@@ -0,0 +1,189 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.readCategoryName = exports.screenToolName = exports.queryToolName = exports.READ_PAGE = exports.ROWS_FOR_MODEL = exports.MAX_READ_ROWS = void 0;
4
+ exports.declareReadTarget = declareReadTarget;
5
+ exports.readTargetOf = readTargetOf;
6
+ exports.clearReadTargets = clearReadTargets;
7
+ exports.readCriteria = readCriteria;
8
+ exports.filtersOf = filtersOf;
9
+ exports.rowOf = rowOf;
10
+ exports.readRows = readRows;
11
+ exports.sumsOf = sumsOf;
12
+ exports.groupsOf = groupsOf;
13
+ exports.screenAddress = screenAddress;
14
+ /*
15
+ * AI 조회 - an application's list, read through its own query as the person (operato-application/design/ai/read-targets.md).
16
+ *
17
+ * The application declares what it lets the assistant read: its list query, the conditions the model may set and
18
+ * how each becomes one of the query's filters, the columns to show, and the screen that lists the same records.
19
+ * The framework reads the rows through that query with the person's request state - the same @privilege and
20
+ * domain as the screen - counts and sums them itself, and puts a long result on the AI 작업판 as the query returned
21
+ * it. The model is told the count, the sums and a few rows; it never copies a table.
22
+ *
23
+ * Pure but for the GraphQL runner, which comes in as an argument - the rules are tested without a server.
24
+ */
25
+ const entry_target_js_1 = require("../data-entry/entry-target.js");
26
+ /** At most this many rows are read - the AI 작업판 table's own limit (workspace-blocks MAX_TABLE_ROWS). */
27
+ exports.MAX_READ_ROWS = 1000;
28
+ /** Rows the model is shown; the rest stay on the card. */
29
+ exports.ROWS_FOR_MODEL = 20;
30
+ /** A page of the list query. */
31
+ exports.READ_PAGE = 200;
32
+ const registry = new Map();
33
+ const pascalOf = (key) => key
34
+ .split(/[^a-zA-Z0-9]+/)
35
+ .filter(Boolean)
36
+ .map(part => part[0].toUpperCase() + part.slice(1))
37
+ .join('');
38
+ const queryToolName = (key) => `query${pascalOf(key)}`;
39
+ exports.queryToolName = queryToolName;
40
+ const screenToolName = (key) => `open${pascalOf(key)}Screen`;
41
+ exports.screenToolName = screenToolName;
42
+ const readCategoryName = (key) => `data-read:${key}`;
43
+ exports.readCategoryName = readCategoryName;
44
+ function declareReadTarget(target) {
45
+ if (!/^[a-z0-9-]+\.[a-z0-9-]+$/.test(target.key))
46
+ throw new Error(`read target key '${target.key}' is not <app>.<record>`);
47
+ if (registry.has(target.key))
48
+ throw new Error(`read target '${target.key}' is declared twice`);
49
+ (0, entry_target_js_1.rootFieldOf)(target.query, 'query');
50
+ if (!target.columns.length)
51
+ throw new Error(`read target '${target.key}' shows no column`);
52
+ const names = new Set();
53
+ for (const criterion of target.criteria) {
54
+ if (criterion.name === 'id')
55
+ throw new Error(`read target '${target.key}' declares 'id' - every target takes it`);
56
+ if (criterion.name === 'text' && target.search)
57
+ throw new Error(`read target '${target.key}' declares 'text' and search - search is the text condition`);
58
+ if (names.has(criterion.name))
59
+ throw new Error(`read target '${target.key}' names the condition '${criterion.name}' twice`);
60
+ names.add(criterion.name);
61
+ if (criterion.type === 'enum' && !criterion.values?.length)
62
+ throw new Error(`condition '${criterion.name}' of '${target.key}' lists no values`);
63
+ }
64
+ if (target.search)
65
+ names.add('text');
66
+ for (const taken of target.screen?.takes ?? [])
67
+ if (!names.has(taken))
68
+ throw new Error(`the screen of '${target.key}' takes '${taken}', which is no condition`);
69
+ registry.set(target.key, target);
70
+ }
71
+ function readTargetOf(key) {
72
+ return key ? registry.get(key) : undefined;
73
+ }
74
+ function clearReadTargets() {
75
+ registry.clear();
76
+ }
77
+ /** The conditions the model set, read by their declared types; a value that does not fit is said, never guessed at. */
78
+ function readCriteria(target, args) {
79
+ const criteria = {};
80
+ const refused = [];
81
+ /* One record by its id - every target takes it (the id a list answer or a find tool gave). */
82
+ if (typeof args?.id === 'string' && args.id.trim())
83
+ criteria.id = args.id.trim();
84
+ if (target.search && typeof args?.text === 'string' && args.text.trim())
85
+ criteria.text = args.text.trim();
86
+ for (const criterion of target.criteria) {
87
+ const value = args?.[criterion.name];
88
+ if (value === undefined || value === null || value === '')
89
+ continue;
90
+ const { read, code } = (0, entry_target_js_1.readFieldValue)(criterion, value);
91
+ if (read === undefined)
92
+ refused.push({ name: criterion.name, code: code ?? 'unreadable' });
93
+ else
94
+ criteria[criterion.name] = read;
95
+ }
96
+ return { criteria, refused };
97
+ }
98
+ function filtersOf(target, criteria) {
99
+ return [
100
+ ...(typeof criteria.id === 'string' ? [{ name: 'id', operator: 'eq', value: criteria.id }] : []),
101
+ ...target.criteria.flatMap(criterion => (criterion.name in criteria ? [criterion.filter(criteria[criterion.name])].flat() : []))
102
+ ];
103
+ }
104
+ const valueAt = (row, path) => path.split('.').reduce((at, part) => (at === null || at === undefined ? undefined : at[part]), row);
105
+ /** One row as the columns say - a number column as a number, a missing value as null. */
106
+ function rowOf(target, item) {
107
+ return Object.fromEntries([
108
+ /* The id names the record to the model - for one record's detail, a change, or its screen. */
109
+ ...(item?.id ? [['id', String(item.id)]] : []),
110
+ ...target.columns.map(column => {
111
+ const raw = valueAt(item, column.path ?? column.name);
112
+ if (raw === undefined || raw === null || raw === '')
113
+ return [column.name, null];
114
+ if (column.type === 'number') {
115
+ const number = Number(raw);
116
+ return [column.name, Number.isFinite(number) ? number : null];
117
+ }
118
+ return [column.name, String(raw)];
119
+ })
120
+ ]);
121
+ }
122
+ /**
123
+ * Reads the rows through the application's query, page by page, up to MAX_READ_ROWS. `complete` is false when the
124
+ * list holds more - a sum of the rows read is then not the sum of the list, and is never said as one.
125
+ */
126
+ async function readRows(target, filters, run, search) {
127
+ const rows = [];
128
+ let total = 0;
129
+ for (let page = 1; rows.length < exports.MAX_READ_ROWS; page++) {
130
+ const answer = await run(target.query, {
131
+ filters,
132
+ ...(search ? { search } : {}),
133
+ pagination: { page, limit: exports.READ_PAGE },
134
+ ...(target.sortings ? { sortings: target.sortings } : {})
135
+ });
136
+ if (answer.errors?.length)
137
+ throw new Error(answer.errors.map(one => one.message).join('; '));
138
+ const read = target.rowsOf(answer.data);
139
+ total = Number(read?.total) || 0;
140
+ const items = read?.rows ?? [];
141
+ rows.push(...items.slice(0, exports.MAX_READ_ROWS - rows.length).map(item => rowOf(target, item)));
142
+ if (items.length < exports.READ_PAGE || rows.length >= total)
143
+ break;
144
+ }
145
+ return { rows, total, complete: rows.length >= total };
146
+ }
147
+ /** The sums of the number columns marked total - the server's, never the model's. */
148
+ function sumsOf(target, rows) {
149
+ const sums = {};
150
+ for (const column of target.columns.filter(one => one.total && one.type === 'number')) {
151
+ sums[column.name] = Math.round(rows.reduce((sum, row) => sum + (typeof row[column.name] === 'number' ? row[column.name] : 0), 0) * 100) / 100;
152
+ }
153
+ return sums;
154
+ }
155
+ /** The rows grouped by one column - a date column by its month - each with its count and sums, largest first. */
156
+ function groupsOf(target, rows, by) {
157
+ const column = target.columns.find(one => one.name === by);
158
+ if (!column)
159
+ return undefined;
160
+ const groups = new Map();
161
+ for (const row of rows) {
162
+ const value = row[by];
163
+ const key = value === null || value === undefined ? '' : column.type === 'date' ? String(value).slice(0, 7) : String(value);
164
+ groups.set(key, [...(groups.get(key) ?? []), row]);
165
+ }
166
+ const list = [...groups].map(([key, members]) => ({ key, count: members.length, sums: sumsOf(target, members) }));
167
+ const first = target.columns.find(one => one.total)?.name;
168
+ return column.type === 'date'
169
+ ? list.sort((a, b) => a.key.localeCompare(b.key))
170
+ : list.sort((a, b) => (first ? Math.abs(b.sums[first] ?? 0) - Math.abs(a.sums[first] ?? 0) : b.count - a.count));
171
+ }
172
+ /** The address of the target's list screen with the conditions it takes, and the conditions it does not. */
173
+ function screenAddress(target, criteria) {
174
+ const screen = target.screen;
175
+ if (!screen)
176
+ return undefined;
177
+ const params = new URLSearchParams();
178
+ for (const [name, value] of Object.entries(screen.params(criteria)))
179
+ if (value !== undefined && value !== null && value !== '')
180
+ params.set(name, String(value));
181
+ const given = Object.keys(criteria);
182
+ const query = params.toString();
183
+ return {
184
+ href: query ? `${screen.route}?${query}` : screen.route,
185
+ applied: given.filter(name => screen.takes.includes(name)),
186
+ notApplied: given.filter(name => !screen.takes.includes(name))
187
+ };
188
+ }
189
+ //# sourceMappingURL=read-target.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"read-target.js","sourceRoot":"","sources":["../../../server/service/data-read/read-target.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * AI 조회 - an application's list, read through its own query as the person (operato-application/design/ai/read-targets.md).\n *\n * The application declares what it lets the assistant read: its list query, the conditions the model may set and\n * how each becomes one of the query's filters, the columns to show, and the screen that lists the same records.\n * The framework reads the rows through that query with the person's request state - the same @privilege and\n * domain as the screen - counts and sums them itself, and puts a long result on the AI 작업판 as the query returned\n * it. The model is told the count, the sums and a few rows; it never copies a table.\n *\n * Pure but for the GraphQL runner, which comes in as an argument - the rules are tested without a server.\n */\nimport { readFieldValue, rootFieldOf, type EntryField } from '../data-entry/entry-target.js'\nimport type { GraphqlRunner } from '../data-entry/entry-update.js'\n\nexport interface ReadFilter {\n name: string\n operator: string\n value: unknown\n}\n\n/** A condition the model may set - read with the entry fields' rules, turned into the query's filters by the application. */\nexport interface ReadCriterion extends Omit<EntryField, 'required' | 'reference' | 'total'> {\n filter: (value: any) => ReadFilter | ReadFilter[]\n}\n\nexport interface ReadColumn {\n /** The key in the workspace table. */\n name: string\n /** Where the value is in a row - a dotted path into the query's item (`account.name`). Defaults to `name`. */\n path?: string\n type: 'string' | 'number' | 'date'\n labelKey?: string\n /** A number column the server sums. */\n total?: boolean\n}\n\nexport interface ReadScreen {\n /** The application's route of the list screen (`cash-transaction-list`). */\n route: string\n /** The conditions the screen takes, as its address's query arguments. A condition left out is said on the card. */\n params: (criteria: Record<string, any>) => Record<string, string | number | undefined>\n /** The conditions `params` carries - the card names the others as not applied on the screen. */\n takes: string[]\n labelKey?: string\n}\n\nexport interface ReadTarget {\n /** `<app>.<record>` - the tools are named after it. */\n key: string\n /** i18n key naming the records (「은행 입출금」). */\n labelKey?: string\n /** What the records are, for the model. */\n description: string\n /**\n * The list query - takes `$filters: [Filter!]`, `$pagination: Pagination` and `$sortings: [Sorting!]`, and\n * `$search: String` when `search` is declared.\n */\n query: string\n /**\n * The list takes ListParam's free-text term, matched by the resolver across its own searchable fields - the\n * condition `text`. Say what it finds in, for the model.\n */\n search?: { description: string }\n criteria: ReadCriterion[]\n columns: ReadColumn[]\n rowsOf: (data: any) => { rows: any[]; total: number }\n /** Orders the rows; the query's own order when left out. */\n sortings?: { name: string; desc?: boolean }[]\n screen?: ReadScreen\n}\n\n/** At most this many rows are read - the AI 작업판 table's own limit (workspace-blocks MAX_TABLE_ROWS). */\nexport const MAX_READ_ROWS = 1000\n/** Rows the model is shown; the rest stay on the card. */\nexport const ROWS_FOR_MODEL = 20\n/** A page of the list query. */\nexport const READ_PAGE = 200\n\nconst registry = new Map<string, ReadTarget>()\n\nconst pascalOf = (key: string) =>\n key\n .split(/[^a-zA-Z0-9]+/)\n .filter(Boolean)\n .map(part => part[0].toUpperCase() + part.slice(1))\n .join('')\n\nexport const queryToolName = (key: string) => `query${pascalOf(key)}`\nexport const screenToolName = (key: string) => `open${pascalOf(key)}Screen`\nexport const readCategoryName = (key: string) => `data-read:${key}`\n\nexport function declareReadTarget(target: ReadTarget): void {\n if (!/^[a-z0-9-]+\\.[a-z0-9-]+$/.test(target.key)) throw new Error(`read target key '${target.key}' is not <app>.<record>`)\n if (registry.has(target.key)) throw new Error(`read target '${target.key}' is declared twice`)\n rootFieldOf(target.query, 'query')\n if (!target.columns.length) throw new Error(`read target '${target.key}' shows no column`)\n const names = new Set<string>()\n for (const criterion of target.criteria) {\n if (criterion.name === 'id') throw new Error(`read target '${target.key}' declares 'id' - every target takes it`)\n if (criterion.name === 'text' && target.search) throw new Error(`read target '${target.key}' declares 'text' and search - search is the text condition`)\n if (names.has(criterion.name)) throw new Error(`read target '${target.key}' names the condition '${criterion.name}' twice`)\n names.add(criterion.name)\n if (criterion.type === 'enum' && !criterion.values?.length) throw new Error(`condition '${criterion.name}' of '${target.key}' lists no values`)\n }\n if (target.search) names.add('text')\n for (const taken of target.screen?.takes ?? []) if (!names.has(taken)) throw new Error(`the screen of '${target.key}' takes '${taken}', which is no condition`)\n registry.set(target.key, target)\n}\n\nexport function readTargetOf(key: string | undefined): ReadTarget | undefined {\n return key ? registry.get(key) : undefined\n}\n\nexport function clearReadTargets(): void {\n registry.clear()\n}\n\n/** The conditions the model set, read by their declared types; a value that does not fit is said, never guessed at. */\nexport function readCriteria(target: ReadTarget, args: any): { criteria: Record<string, unknown>; refused: { name: string; code: string }[] } {\n const criteria: Record<string, unknown> = {}\n const refused: { name: string; code: string }[] = []\n /* One record by its id - every target takes it (the id a list answer or a find tool gave). */\n if (typeof args?.id === 'string' && args.id.trim()) criteria.id = args.id.trim()\n if (target.search && typeof args?.text === 'string' && args.text.trim()) criteria.text = args.text.trim()\n for (const criterion of target.criteria) {\n const value = args?.[criterion.name]\n if (value === undefined || value === null || value === '') continue\n const { read, code } = readFieldValue(criterion as EntryField, value)\n if (read === undefined) refused.push({ name: criterion.name, code: code ?? 'unreadable' })\n else criteria[criterion.name] = read\n }\n return { criteria, refused }\n}\n\nexport function filtersOf(target: ReadTarget, criteria: Record<string, unknown>): ReadFilter[] {\n return [\n ...(typeof criteria.id === 'string' ? [{ name: 'id', operator: 'eq', value: criteria.id }] : []),\n ...target.criteria.flatMap(criterion => (criterion.name in criteria ? [criterion.filter(criteria[criterion.name])].flat() : []))\n ]\n}\n\nconst valueAt = (row: any, path: string) => path.split('.').reduce((at, part) => (at === null || at === undefined ? undefined : at[part]), row)\n\n/** One row as the columns say - a number column as a number, a missing value as null. */\nexport function rowOf(target: ReadTarget, item: any): Record<string, string | number | null> {\n return Object.fromEntries([\n /* The id names the record to the model - for one record's detail, a change, or its screen. */\n ...(item?.id ? [['id', String(item.id)] as [string, string]] : []),\n ...target.columns.map(column => {\n const raw = valueAt(item, column.path ?? column.name)\n if (raw === undefined || raw === null || raw === '') return [column.name, null]\n if (column.type === 'number') {\n const number = Number(raw)\n return [column.name, Number.isFinite(number) ? number : null]\n }\n return [column.name, String(raw)]\n })\n ])\n}\n\n/**\n * Reads the rows through the application's query, page by page, up to MAX_READ_ROWS. `complete` is false when the\n * list holds more - a sum of the rows read is then not the sum of the list, and is never said as one.\n */\nexport async function readRows(target: ReadTarget, filters: ReadFilter[], run: GraphqlRunner, search?: string): Promise<{ rows: Record<string, any>[]; total: number; complete: boolean }> {\n const rows: Record<string, any>[] = []\n let total = 0\n for (let page = 1; rows.length < MAX_READ_ROWS; page++) {\n const answer = await run(target.query, {\n filters,\n ...(search ? { search } : {}),\n pagination: { page, limit: READ_PAGE },\n ...(target.sortings ? { sortings: target.sortings } : {})\n })\n if (answer.errors?.length) throw new Error(answer.errors.map(one => one.message).join('; '))\n const read = target.rowsOf(answer.data)\n total = Number(read?.total) || 0\n const items = read?.rows ?? []\n rows.push(...items.slice(0, MAX_READ_ROWS - rows.length).map(item => rowOf(target, item)))\n if (items.length < READ_PAGE || rows.length >= total) break\n }\n return { rows, total, complete: rows.length >= total }\n}\n\nexport interface ReadGroup {\n key: string\n count: number\n sums: Record<string, number>\n}\n\n/** The sums of the number columns marked total - the server's, never the model's. */\nexport function sumsOf(target: ReadTarget, rows: Record<string, any>[]): Record<string, number> {\n const sums: Record<string, number> = {}\n for (const column of target.columns.filter(one => one.total && one.type === 'number')) {\n sums[column.name] = Math.round(rows.reduce((sum, row) => sum + (typeof row[column.name] === 'number' ? row[column.name] : 0), 0) * 100) / 100\n }\n return sums\n}\n\n/** The rows grouped by one column - a date column by its month - each with its count and sums, largest first. */\nexport function groupsOf(target: ReadTarget, rows: Record<string, any>[], by: string): ReadGroup[] | undefined {\n const column = target.columns.find(one => one.name === by)\n if (!column) return undefined\n const groups = new Map<string, Record<string, any>[]>()\n for (const row of rows) {\n const value = row[by]\n const key = value === null || value === undefined ? '' : column.type === 'date' ? String(value).slice(0, 7) : String(value)\n groups.set(key, [...(groups.get(key) ?? []), row])\n }\n const list = [...groups].map(([key, members]) => ({ key, count: members.length, sums: sumsOf(target, members) }))\n const first = target.columns.find(one => one.total)?.name\n return column.type === 'date'\n ? list.sort((a, b) => a.key.localeCompare(b.key))\n : list.sort((a, b) => (first ? Math.abs(b.sums[first] ?? 0) - Math.abs(a.sums[first] ?? 0) : b.count - a.count))\n}\n\n/** The address of the target's list screen with the conditions it takes, and the conditions it does not. */\nexport function screenAddress(target: ReadTarget, criteria: Record<string, unknown>): { href: string; applied: string[]; notApplied: string[] } | undefined {\n const screen = target.screen\n if (!screen) return undefined\n const params = new URLSearchParams()\n for (const [name, value] of Object.entries(screen.params(criteria))) if (value !== undefined && value !== null && value !== '') params.set(name, String(value))\n const given = Object.keys(criteria)\n const query = params.toString()\n return {\n href: query ? `${screen.route}?${query}` : screen.route,\n applied: given.filter(name => screen.takes.includes(name)),\n notApplied: given.filter(name => !screen.takes.includes(name))\n }\n}\n"]}
@@ -0,0 +1,228 @@
1
+ import { type ToolCategory } from '@things-factory/ai-client-base';
2
+ import type { GraphqlRunner } from '../data-entry/entry-update.js';
3
+ import { type ReadTarget } from './read-target.js';
4
+ export declare function queryRecords(target: ReadTarget, args: any, context: any, run: GraphqlRunner): Promise<{
5
+ rejected: boolean;
6
+ error: string;
7
+ refused: {
8
+ name: string;
9
+ code: string;
10
+ }[];
11
+ message: string;
12
+ } | {
13
+ rejected: boolean;
14
+ error: string;
15
+ message: string;
16
+ refused?: undefined;
17
+ } | {
18
+ record: any;
19
+ message: string;
20
+ rowsNotShown?: number;
21
+ rows: any;
22
+ moreGroups?: number;
23
+ groupBy: any;
24
+ groups: import("./read-target.js").ReadGroup[];
25
+ target: string;
26
+ criteria: Record<string, unknown>;
27
+ total: any;
28
+ read: any;
29
+ complete: any;
30
+ sums: Record<string, number>;
31
+ rejected?: undefined;
32
+ error?: undefined;
33
+ refused?: undefined;
34
+ } | {
35
+ record: any;
36
+ message: string;
37
+ rowsNotShown?: number;
38
+ rows: any;
39
+ groupBy: any;
40
+ groupRefused: string;
41
+ target: string;
42
+ criteria: Record<string, unknown>;
43
+ total: any;
44
+ read: any;
45
+ complete: any;
46
+ sums: Record<string, number>;
47
+ rejected?: undefined;
48
+ error?: undefined;
49
+ refused?: undefined;
50
+ } | {
51
+ record: any;
52
+ message: string;
53
+ rowsNotShown?: number;
54
+ rows: any;
55
+ target: string;
56
+ criteria: Record<string, unknown>;
57
+ total: any;
58
+ read: any;
59
+ complete: any;
60
+ sums: Record<string, number>;
61
+ rejected?: undefined;
62
+ error?: undefined;
63
+ refused?: undefined;
64
+ } | {
65
+ message: string;
66
+ rowsNotShown?: number;
67
+ rows: any;
68
+ moreGroups?: number;
69
+ groupBy: any;
70
+ groups: import("./read-target.js").ReadGroup[];
71
+ target: string;
72
+ criteria: Record<string, unknown>;
73
+ total: any;
74
+ read: any;
75
+ complete: any;
76
+ sums: Record<string, number>;
77
+ rejected?: undefined;
78
+ error?: undefined;
79
+ refused?: undefined;
80
+ } | {
81
+ message: string;
82
+ rowsNotShown?: number;
83
+ rows: any;
84
+ groupBy: any;
85
+ groupRefused: string;
86
+ target: string;
87
+ criteria: Record<string, unknown>;
88
+ total: any;
89
+ read: any;
90
+ complete: any;
91
+ sums: Record<string, number>;
92
+ rejected?: undefined;
93
+ error?: undefined;
94
+ refused?: undefined;
95
+ } | {
96
+ message: string;
97
+ rowsNotShown?: number;
98
+ rows: any;
99
+ target: string;
100
+ criteria: Record<string, unknown>;
101
+ total: any;
102
+ read: any;
103
+ complete: any;
104
+ sums: Record<string, number>;
105
+ rejected?: undefined;
106
+ error?: undefined;
107
+ refused?: undefined;
108
+ } | {
109
+ forScreen: {
110
+ workspace: import("../workspace/workspace-blocks.js").Workspace;
111
+ contents: Record<string, number>;
112
+ };
113
+ message: string;
114
+ rowsNotShown?: number;
115
+ rows: any;
116
+ moreGroups?: number;
117
+ groupBy: any;
118
+ groups: import("./read-target.js").ReadGroup[];
119
+ target: string;
120
+ criteria: Record<string, unknown>;
121
+ total: any;
122
+ read: any;
123
+ complete: any;
124
+ sums: Record<string, number>;
125
+ proposed: boolean;
126
+ kind: string;
127
+ icon: string;
128
+ label: any;
129
+ rejected?: undefined;
130
+ error?: undefined;
131
+ refused?: undefined;
132
+ } | {
133
+ forScreen: {
134
+ workspace: import("../workspace/workspace-blocks.js").Workspace;
135
+ contents: Record<string, number>;
136
+ };
137
+ message: string;
138
+ rowsNotShown?: number;
139
+ rows: any;
140
+ groupBy: any;
141
+ groupRefused: string;
142
+ target: string;
143
+ criteria: Record<string, unknown>;
144
+ total: any;
145
+ read: any;
146
+ complete: any;
147
+ sums: Record<string, number>;
148
+ proposed: boolean;
149
+ kind: string;
150
+ icon: string;
151
+ label: any;
152
+ rejected?: undefined;
153
+ error?: undefined;
154
+ refused?: undefined;
155
+ } | {
156
+ forScreen: {
157
+ workspace: import("../workspace/workspace-blocks.js").Workspace;
158
+ contents: Record<string, number>;
159
+ };
160
+ message: string;
161
+ rowsNotShown?: number;
162
+ rows: any;
163
+ target: string;
164
+ criteria: Record<string, unknown>;
165
+ total: any;
166
+ read: any;
167
+ complete: any;
168
+ sums: Record<string, number>;
169
+ proposed: boolean;
170
+ kind: string;
171
+ icon: string;
172
+ label: any;
173
+ rejected?: undefined;
174
+ error?: undefined;
175
+ refused?: undefined;
176
+ }>;
177
+ export declare function openScreen(target: ReadTarget, args: any, context: any): {
178
+ rejected: boolean;
179
+ error: string;
180
+ refused: {
181
+ name: string;
182
+ code: string;
183
+ }[];
184
+ message: string;
185
+ proposed?: undefined;
186
+ kind?: undefined;
187
+ icon?: undefined;
188
+ label?: undefined;
189
+ target?: undefined;
190
+ href?: undefined;
191
+ applied?: undefined;
192
+ notApplied?: undefined;
193
+ notAppliedLabels?: undefined;
194
+ } | {
195
+ rejected: boolean;
196
+ error: string;
197
+ message: string;
198
+ refused?: undefined;
199
+ proposed?: undefined;
200
+ kind?: undefined;
201
+ icon?: undefined;
202
+ label?: undefined;
203
+ target?: undefined;
204
+ href?: undefined;
205
+ applied?: undefined;
206
+ notApplied?: undefined;
207
+ notAppliedLabels?: undefined;
208
+ } | {
209
+ proposed: boolean;
210
+ kind: string;
211
+ icon: string;
212
+ label: string;
213
+ target: string;
214
+ href: string;
215
+ applied: string[];
216
+ notApplied: string[];
217
+ notAppliedLabels: string[];
218
+ message: string;
219
+ rejected?: undefined;
220
+ error?: undefined;
221
+ refused?: undefined;
222
+ };
223
+ export declare function readCategoryOf(target: ReadTarget, run?: (context: any) => GraphqlRunner): ToolCategory;
224
+ /**
225
+ * What an application calls at boot to let the assistant read one of its lists. A mode opens it by naming the key
226
+ * in `readTargets`.
227
+ */
228
+ export declare function registerReadTarget(target: ReadTarget): void;
@@ -0,0 +1,197 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.queryRecords = queryRecords;
4
+ exports.openScreen = openScreen;
5
+ exports.readCategoryOf = readCategoryOf;
6
+ exports.registerReadTarget = registerReadTarget;
7
+ /*
8
+ * The tools of an AI 조회 target (operato-application/design/ai/read-targets.md §4, §5).
9
+ *
10
+ * query<X> reads the rows through the application's list query as the person, and tells the model the count, the
11
+ * sums, the groups and a few rows. A longer result goes on the AI 작업판 as a proposal the server builds from the rows
12
+ * themselves - the table is never written by the model. open<X>Screen reads nothing: it hands the card the address
13
+ * of the application's list screen with the conditions it takes.
14
+ */
15
+ const ai_client_base_1 = require("@things-factory/ai-client-base");
16
+ const entry_target_js_1 = require("../data-entry/entry-target.js");
17
+ const entry_tools_js_1 = require("../data-entry/entry-tools.js");
18
+ const workspace_blocks_js_1 = require("../workspace/workspace-blocks.js");
19
+ const read_target_js_1 = require("./read-target.js");
20
+ /** The reader's words for a key - the key's own default when the translator has none. */
21
+ const wordsOf = (context) => (key, fallback) => {
22
+ if (!key || typeof context?.t !== 'function')
23
+ return fallback;
24
+ try {
25
+ const said = context.t(key);
26
+ return said && said !== key ? String(said) : fallback;
27
+ }
28
+ catch {
29
+ return fallback;
30
+ }
31
+ };
32
+ function criterionSchema(criterion) {
33
+ /* One type per field - Gemini's function schema refuses a list of types (2026-10-01). */
34
+ const type = criterion.type === 'number' ? 'number' : criterion.type === 'integer' ? 'integer' : criterion.type === 'boolean' ? 'boolean' : 'string';
35
+ const said = [criterion.description, criterion.type === 'date' ? 'YYYY-MM-DD' : undefined, criterion.type === 'enum' ? `one of: ${criterion.values.join(', ')}` : undefined]
36
+ .filter(Boolean)
37
+ .join('. ');
38
+ return { type, ...(said ? { description: said } : {}) };
39
+ }
40
+ async function queryRecords(target, args, context, run) {
41
+ const { criteria, refused } = (0, read_target_js_1.readCriteria)(target, args);
42
+ if (refused.length) {
43
+ return { rejected: true, error: 'condition-unreadable', refused, message: `these conditions do not fit their type: ${refused.map(one => `${one.name} (${one.code})`).join(', ')}. Fix them and call again.` };
44
+ }
45
+ let read;
46
+ try {
47
+ read = await (0, read_target_js_1.readRows)(target, (0, read_target_js_1.filtersOf)(target, criteria), run, typeof criteria.text === 'string' && target.search ? criteria.text : undefined);
48
+ }
49
+ catch (error) {
50
+ return { rejected: true, error: 'query-failed', message: `the list could not be read: ${error?.message ?? error}` };
51
+ }
52
+ const { rows, total, complete } = read;
53
+ const sums = (0, read_target_js_1.sumsOf)(target, rows);
54
+ const groupBy = typeof args?.groupBy === 'string' && args.groupBy ? args.groupBy : undefined;
55
+ const groups = groupBy ? (0, read_target_js_1.groupsOf)(target, rows, groupBy) : undefined;
56
+ const words = wordsOf(context);
57
+ const label = words(target.labelKey, target.key);
58
+ const said = {
59
+ target: target.key,
60
+ criteria,
61
+ total,
62
+ read: rows.length,
63
+ complete,
64
+ sums,
65
+ ...(groupBy ? (groups ? { groupBy, groups: groups.slice(0, 50), ...(groups.length > 50 ? { moreGroups: groups.length - 50 } : {}) } : { groupBy, groupRefused: 'no such column' }) : {}),
66
+ rows: rows.slice(0, read_target_js_1.ROWS_FOR_MODEL),
67
+ ...(rows.length > read_target_js_1.ROWS_FOR_MODEL ? { rowsNotShown: rows.length - read_target_js_1.ROWS_FOR_MODEL } : {})
68
+ };
69
+ const incomplete = complete
70
+ ? ''
71
+ : ` Only ${rows.length} of ${total} rows were read; the sums and groups are of those rows, not of the list - say so, and ask the person to narrow the conditions.`;
72
+ if (rows.length === 1 && args?.show !== 'workspace') {
73
+ return { ...said, record: rows[0], message: 'one record - answer from its fields; say a field it does not have as not recorded.' };
74
+ }
75
+ if (rows.length <= read_target_js_1.ROWS_FOR_MODEL && args?.show !== 'workspace') {
76
+ return { ...said, message: `${rows.length} rows of ${total}.${incomplete} Answer from these rows; every number you say comes from them.` };
77
+ }
78
+ /* The workspace is built here from the rows as read - the model never copies the table. */
79
+ const columns = target.columns.map(column => ({ name: column.name, label: words(column.labelKey, column.name), type: column.type }));
80
+ const totalColumns = target.columns.filter(one => one.total);
81
+ const title = typeof args?.title === 'string' && args.title.trim() ? args.title.trim() : `${label} · ${rows.length}`;
82
+ const { workspace } = (0, workspace_blocks_js_1.normalizeWorkspace)({
83
+ name: typeof args?.name === 'string' && args.name ? args.name : target.key.replace(/\./g, '-'),
84
+ title,
85
+ ...(complete ? {} : { summary: `${total} / ${rows.length}` }),
86
+ blocks: [
87
+ {
88
+ kind: 'metrics',
89
+ items: [{ label: words('ai-assistant.read.count', 'rows'), value: String(rows.length), ...(complete ? {} : { note: `/ ${total}` }) }, ...totalColumns.map(column => ({ label: words(column.labelKey, column.name), value: String(sums[column.name]) }))]
90
+ },
91
+ ...(groups
92
+ ? [
93
+ {
94
+ kind: 'table',
95
+ caption: words(target.columns.find(one => one.name === groupBy)?.labelKey, groupBy),
96
+ columns: [{ name: 'key', label: words(target.columns.find(one => one.name === groupBy)?.labelKey, groupBy), type: 'string' }, { name: 'count', label: words('ai-assistant.read.count', 'rows'), type: 'number' }, ...totalColumns.map(column => ({ name: column.name, label: words(column.labelKey, column.name), type: 'number' }))],
97
+ rows: groups.map(group => ({ key: group.key, count: group.count, ...group.sums }))
98
+ }
99
+ ]
100
+ : []),
101
+ { kind: 'table', caption: label, columns, rows }
102
+ ]
103
+ });
104
+ return {
105
+ proposed: true,
106
+ kind: 'workspace',
107
+ icon: 'table_view',
108
+ label: title,
109
+ ...said,
110
+ forScreen: { workspace, contents: (0, workspace_blocks_js_1.workspaceContents)(workspace) },
111
+ message: `${rows.length} rows of ${total} are on the AI 작업판 as read - the person opens it from the card.${incomplete} Say the point in one or two lines from the sums, groups and rows above; do not write the table out.`
112
+ };
113
+ }
114
+ function openScreen(target, args, context) {
115
+ const { criteria, refused } = (0, read_target_js_1.readCriteria)(target, args);
116
+ if (refused.length)
117
+ return { rejected: true, error: 'condition-unreadable', refused, message: 'fix the conditions and call again.' };
118
+ const address = (0, read_target_js_1.screenAddress)(target, criteria);
119
+ if (!address)
120
+ return { rejected: true, error: 'no-screen', message: 'this list has no screen to open.' };
121
+ const words = wordsOf(context);
122
+ return {
123
+ proposed: true,
124
+ kind: 'open-page',
125
+ icon: 'open_in_new',
126
+ label: words(target.screen.labelKey ?? target.labelKey, target.key),
127
+ target: target.key,
128
+ href: address.href,
129
+ applied: address.applied,
130
+ notApplied: address.notApplied,
131
+ /* The card says them in the reader's words. */
132
+ notAppliedLabels: address.notApplied.map(name => words(target.criteria.find(one => one.name === name)?.labelKey ?? (name === 'text' || name === 'id' ? `ai-assistant.read.${name}` : undefined), name)),
133
+ message: 'the card opens the screen when the person presses it.' +
134
+ (address.notApplied.length ? ` The screen does not take ${address.notApplied.join(', ')} - say so.` : '')
135
+ };
136
+ }
137
+ function readCategoryOf(target, run = entry_tools_js_1.runAsCaller) {
138
+ const door = (0, entry_target_js_1.rootFieldOf)(target.query, 'query');
139
+ const conditions = {
140
+ ...Object.fromEntries(target.criteria.map(criterion => [criterion.name, criterionSchema(criterion)])),
141
+ ...(target.search ? { text: { type: 'string', description: `words to find - ${target.search.description}` } } : {})
142
+ };
143
+ const specs = [
144
+ {
145
+ kind: 'read',
146
+ name: (0, read_target_js_1.queryToolName)(target.key),
147
+ /* The person must be able to read the list on its screen - the tool is not a second way in. */
148
+ doors: [door],
149
+ description: `Read ${target.description} Set the conditions the question names. Returns the count, the sums, ` +
150
+ `groups when groupBy is given (a date column groups by month) and the first ${read_target_js_1.ROWS_FOR_MODEL} rows; more rows go on the AI 작업판 as read.`,
151
+ schema: {
152
+ type: 'object',
153
+ properties: {
154
+ id: { type: 'string', description: 'One record by its id, from an earlier answer - its detail.' },
155
+ ...conditions,
156
+ groupBy: { type: 'string', description: `One column to group by: ${target.columns.map(one => one.name).join(', ')}.` },
157
+ show: { type: 'string', enum: ['answer', 'workspace'], description: 'workspace to put even a short result on the AI 작업판.' },
158
+ title: { type: 'string', description: 'The workspace title, in the person\'s language.' },
159
+ name: { type: 'string', description: 'The workspace key; the same key again makes a new version.' }
160
+ }
161
+ },
162
+ builder: (args, context) => queryRecords(target, args, context, run(context))
163
+ },
164
+ ...(target.screen
165
+ ? [
166
+ {
167
+ kind: 'read',
168
+ name: (0, read_target_js_1.screenToolName)(target.key),
169
+ doors: [door],
170
+ description: `Offer the person the screen that lists ${target.description} with these conditions. Reads nothing. The screen takes: ${target.screen.takes.join(', ')}.`,
171
+ schema: { type: 'object', properties: conditions },
172
+ builder: (args, context) => openScreen(target, args, context)
173
+ }
174
+ ]
175
+ : [])
176
+ ];
177
+ return {
178
+ name: (0, read_target_js_1.readCategoryName)(target.key),
179
+ description: `data read: ${target.key}`,
180
+ guidance: [
181
+ `- To answer from ${target.key} records - a list, or one record found by its name, code or id - call ${(0, read_target_js_1.queryToolName)(target.key)} with the conditions the question names. Every number you say comes from its result - never add up rows yourself; use its sums and groups.`,
182
+ `- A list, a summary by item or month, or a comparison: call it with groupBy, and let a long result go on the AI 작업판 - then answer in one or two lines.`,
183
+ `- When the result says complete: false, say how many of how many were read and ask to narrow - never call partial sums the total.`,
184
+ ...(target.screen ? [`- When the person wants to work on the records themselves (edit, sort, export), call ${(0, read_target_js_1.screenToolName)(target.key)}; say which conditions the screen does not take.`] : [])
185
+ ].join('\n'),
186
+ specs
187
+ };
188
+ }
189
+ /**
190
+ * What an application calls at boot to let the assistant read one of its lists. A mode opens it by naming the key
191
+ * in `readTargets`.
192
+ */
193
+ function registerReadTarget(target) {
194
+ (0, read_target_js_1.declareReadTarget)(target);
195
+ (0, ai_client_base_1.registerToolCategory)(readCategoryOf(target));
196
+ }
197
+ //# sourceMappingURL=read-tools.js.map