@things-factory/ai-assistant 10.1.97 → 10.1.99
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/entry-review.ts +10 -4
- package/dist-client/components/entry-review.js +8 -2
- package/dist-client/components/entry-review.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-server/service/data-entry/entry-target.d.ts +2 -0
- package/dist-server/service/data-entry/entry-target.js.map +1 -1
- package/dist-server/service/data-entry/entry-tools.d.ts +1 -0
- package/dist-server/service/data-entry/entry-tools.js +13 -3
- package/dist-server/service/data-entry/entry-tools.js.map +1 -1
- package/dist-server/service/data-entry/entry-update.d.ts +1 -0
- package/dist-server/service/data-entry/entry-update.js +1 -1
- package/dist-server/service/data-entry/entry-update.js.map +1 -1
- package/dist-server/service/workspace/workspace-blocks.js +9 -1
- package/dist-server/service/workspace/workspace-blocks.js.map +1 -1
- package/dist-server/service/workspace/workspace-tools.js +43 -1
- package/dist-server/service/workspace/workspace-tools.js.map +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/server/service/data-entry/data-entry-update.test.ts +15 -0
- package/server/service/data-entry/data-entry.test.ts +9 -0
- package/server/service/data-entry/entry-target.ts +2 -0
- package/server/service/data-entry/entry-tools.ts +13 -3
- package/server/service/data-entry/entry-update.ts +1 -1
- package/server/service/workspace/workspace-blocks.ts +8 -1
- package/server/service/workspace/workspace-tools.ts +43 -1
- package/server/service/workspace/workspace.test.ts +16 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@things-factory/ai-assistant",
|
|
3
|
-
"version": "10.1.
|
|
3
|
+
"version": "10.1.99",
|
|
4
4
|
"main": "dist-server/index.js",
|
|
5
5
|
"things-factory": true,
|
|
6
6
|
"author": "heartyoh",
|
|
@@ -51,5 +51,5 @@
|
|
|
51
51
|
"copyfiles": "^2.4.1",
|
|
52
52
|
"rimraf": "^5.0.0"
|
|
53
53
|
},
|
|
54
|
-
"gitHead": "
|
|
54
|
+
"gitHead": "15e86d38824d6d2af94043eb14a928232a8924a3"
|
|
55
55
|
}
|
|
@@ -190,3 +190,18 @@ describe('declaring and offering', () => {
|
|
|
190
190
|
expect(rows[0].issues).toEqual([{ code: 'record-not-found', blocking: true }])
|
|
191
191
|
})
|
|
192
192
|
})
|
|
193
|
+
|
|
194
|
+
describe('the tool schemas', () => {
|
|
195
|
+
/* Gemini's function schema refuses a list of types - one such field failed every office conversation (2026-10-01). */
|
|
196
|
+
const typesOf = (schema: any, path = ''): string[] =>
|
|
197
|
+
!schema || typeof schema !== 'object'
|
|
198
|
+
? []
|
|
199
|
+
: [
|
|
200
|
+
...(Array.isArray(schema.type) ? [`${path}: ${JSON.stringify(schema.type)}`] : []),
|
|
201
|
+
...Object.entries(schema).flatMap(([key, value]) => (typeof value === 'object' ? typesOf(value, `${path}/${key}`) : []))
|
|
202
|
+
]
|
|
203
|
+
|
|
204
|
+
it('★ every field has one type - no list of types anywhere in the tools', () => {
|
|
205
|
+
for (const spec of entryCategoryOf(expense, () => run).specs as any[]) expect(typesOf(spec.schema)).toEqual([])
|
|
206
|
+
})
|
|
207
|
+
})
|
|
@@ -309,3 +309,12 @@ describe('telling the application', () => {
|
|
|
309
309
|
expect(result.proposed).toBe(true)
|
|
310
310
|
})
|
|
311
311
|
})
|
|
312
|
+
|
|
313
|
+
describe('the card speaks the person\'s words', () => {
|
|
314
|
+
it('an enum field hands its value key prefix to the card - PERSONAL_CARD shows as the app\'s own word for it', async () => {
|
|
315
|
+
const target: EntryTarget = { ...expense, fields: [...expense.fields, { name: 'paidBy', type: 'enum', values: ['PERSONAL_CARD'], valueKeyPrefix: 'text.expense-payment-' }] }
|
|
316
|
+
const result: any = await proposeEntries(target, { rows: [{ values: { spentOn: '2026-09-12', amount: 1, paidBy: 'PERSONAL_CARD' }, sources: [{ where: 'x' }] }] }, { state: {} }, async () => new Map())
|
|
317
|
+
expect(result.columns.find((one: any) => one.name === 'paidBy')).toMatchObject({ valueKeyPrefix: 'text.expense-payment-' })
|
|
318
|
+
})
|
|
319
|
+
})
|
|
320
|
+
|
|
@@ -28,6 +28,8 @@ export interface EntryField {
|
|
|
28
28
|
labelKey?: string
|
|
29
29
|
/** A number field the card sums - and compares with the total the material prints (§ totals). */
|
|
30
30
|
total?: boolean
|
|
31
|
+
/** An enum's values on the card: `<prefix><value in lowercase>` - `text.expense-payment-` shows PERSONAL_CARD as 개인카드. */
|
|
32
|
+
valueKeyPrefix?: string
|
|
31
33
|
}
|
|
32
34
|
|
|
33
35
|
/** Where a row came from - which file, and where in it. Shown on the card beside the row. */
|
|
@@ -30,7 +30,11 @@ import {
|
|
|
30
30
|
} from './entry-target.js'
|
|
31
31
|
|
|
32
32
|
function fieldSchema(field: EntryField): any {
|
|
33
|
-
|
|
33
|
+
/*
|
|
34
|
+
* One type per field - Gemini's function schema (an OpenAPI 3.0 subset) refuses a list of types, and the
|
|
35
|
+
* whole request with it: "12,400" or "₩12,400" is the model's to write as 12400 (2026-10-01, office).
|
|
36
|
+
*/
|
|
37
|
+
const type = field.type === 'number' ? 'number' : field.type === 'integer' ? 'integer' : field.type === 'boolean' ? 'boolean' : 'string'
|
|
34
38
|
const said = [
|
|
35
39
|
field.description,
|
|
36
40
|
field.required ? 'required' : 'optional',
|
|
@@ -44,7 +48,13 @@ function fieldSchema(field: EntryField): any {
|
|
|
44
48
|
|
|
45
49
|
/** What the card needs besides the rows - the columns, in the target's order. */
|
|
46
50
|
function columnsOf(target: EntryTarget) {
|
|
47
|
-
return target.fields.map(field => ({
|
|
51
|
+
return target.fields.map(field => ({
|
|
52
|
+
name: field.name,
|
|
53
|
+
type: field.type,
|
|
54
|
+
required: !!field.required,
|
|
55
|
+
...(field.labelKey ? { labelKey: field.labelKey } : {}),
|
|
56
|
+
...(field.valueKeyPrefix ? { valueKeyPrefix: field.valueKeyPrefix } : {})
|
|
57
|
+
}))
|
|
48
58
|
}
|
|
49
59
|
|
|
50
60
|
export type SourceTextLoader = (attachmentIds: string[], context: any) => Promise<Map<string, string>>
|
|
@@ -205,7 +215,7 @@ export function entryCategoryOf(target: EntryTarget, run: (context: any) => Grap
|
|
|
205
215
|
description: 'The total the material itself prints, when it prints one - the card compares it with the sum of the rows. Never compute it yourself.',
|
|
206
216
|
properties: {
|
|
207
217
|
field: { type: 'string', description: `Which field it totals${target.fields.filter(field => field.total).length ? ` (${target.fields.filter(field => field.total).map(field => field.name).join(', ')})` : ''}.` },
|
|
208
|
-
value: { type:
|
|
218
|
+
value: { type: 'number' },
|
|
209
219
|
attachmentId: { type: 'string' },
|
|
210
220
|
where: { type: 'string' }
|
|
211
221
|
}
|
|
@@ -125,7 +125,7 @@ function changedColumns(target: EntryTarget, rows: ChangeRow[]) {
|
|
|
125
125
|
const used = new Set(rows.flatMap(row => Object.keys(row.values)))
|
|
126
126
|
return changeableFields(target)
|
|
127
127
|
.filter(field => used.has(field.name))
|
|
128
|
-
.map(field => ({ name: field.name, type: field.type, ...(field.labelKey ? { labelKey: field.labelKey } : {}) }))
|
|
128
|
+
.map(field => ({ name: field.name, type: field.type, ...(field.labelKey ? { labelKey: field.labelKey } : {}), ...(field.valueKeyPrefix ? { valueKeyPrefix: field.valueKeyPrefix } : {}) }))
|
|
129
129
|
}
|
|
130
130
|
|
|
131
131
|
export async function proposeChanges(target: EntryTarget, args: any, context: any, run: GraphqlRunner) {
|
|
@@ -75,8 +75,15 @@ function block(raw: any): WorkspaceBlock {
|
|
|
75
75
|
if (!columns.length) return unreadable(kind, 'no-columns')
|
|
76
76
|
const given = Array.isArray(raw.rows) ? raw.rows : []
|
|
77
77
|
/* A row is an object by column name, or an array in column order - models write both. */
|
|
78
|
+
/* A number column reads "12,400" as 12400 - cells come as text from a model told one type per field. */
|
|
79
|
+
const cell = (column: any, value: unknown) => {
|
|
80
|
+
const read = scalar(value)
|
|
81
|
+
if (column.type !== 'number' || typeof read !== 'string') return read
|
|
82
|
+
const number = Number(read.replace(/[,\s₩$€]/g, ''))
|
|
83
|
+
return read.trim() && Number.isFinite(number) ? number : read
|
|
84
|
+
}
|
|
78
85
|
const rows = given.slice(0, MAX_TABLE_ROWS).map((row: any) =>
|
|
79
|
-
Object.fromEntries(columns.map((column: any, i: number) => [column.name,
|
|
86
|
+
Object.fromEntries(columns.map((column: any, i: number) => [column.name, cell(column, Array.isArray(row) ? row[i] : row?.[column.name])]))
|
|
80
87
|
)
|
|
81
88
|
return { kind, columns, rows, ...(text(raw.caption, 300) ? { caption: text(raw.caption, 300) } : {}), ...(given.length > MAX_TABLE_ROWS ? { cut: given.length - MAX_TABLE_ROWS } : {}) }
|
|
82
89
|
}
|
|
@@ -41,9 +41,51 @@ const spec = {
|
|
|
41
41
|
name: { type: 'string', description: 'A short key, lowercase with dashes ("contracts-by-customer"). The same name again makes a new version of the same workspace.' },
|
|
42
42
|
title: { type: 'string' },
|
|
43
43
|
summary: { type: 'string', description: 'One line under the title.' },
|
|
44
|
+
/*
|
|
45
|
+
* Every field of every block is declared, each with one type: Gemini's function schema (an OpenAPI 3.0
|
|
46
|
+
* subset) refuses a list of types and may leave out a field it was not told of (2026-10-01).
|
|
47
|
+
*/
|
|
44
48
|
blocks: {
|
|
45
49
|
type: 'array',
|
|
46
|
-
items: {
|
|
50
|
+
items: {
|
|
51
|
+
type: 'object',
|
|
52
|
+
properties: {
|
|
53
|
+
kind: { type: 'string', enum: [...WORKSPACE_BLOCK_KINDS] },
|
|
54
|
+
text: { type: 'string', description: 'heading' },
|
|
55
|
+
level: { type: 'integer', description: 'heading: 1-3' },
|
|
56
|
+
markdown: { type: 'string', description: 'text' },
|
|
57
|
+
caption: { type: 'string', description: 'table, chart' },
|
|
58
|
+
unit: { type: 'string', description: 'chart' },
|
|
59
|
+
chart: { type: 'string', enum: ['bar', 'line'], description: 'chart' },
|
|
60
|
+
items: {
|
|
61
|
+
type: 'array',
|
|
62
|
+
description: 'metrics: {label,value,unit,note}; steps: {label,state,note}',
|
|
63
|
+
items: {
|
|
64
|
+
type: 'object',
|
|
65
|
+
properties: {
|
|
66
|
+
label: { type: 'string' },
|
|
67
|
+
value: { type: 'string' },
|
|
68
|
+
unit: { type: 'string' },
|
|
69
|
+
note: { type: 'string' },
|
|
70
|
+
state: { type: 'string', enum: ['done', 'doing', 'todo', 'failed'] }
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
columns: {
|
|
75
|
+
type: 'array',
|
|
76
|
+
description: 'table',
|
|
77
|
+
items: { type: 'object', properties: { name: { type: 'string' }, label: { type: 'string' }, type: { type: 'string', enum: ['number', 'date', 'string'] } } }
|
|
78
|
+
},
|
|
79
|
+
rows: { type: 'array', description: 'table: each row is its cells in column order', items: { type: 'array', items: { type: 'string' } } },
|
|
80
|
+
x: { type: 'array', description: 'chart: the labels along the bottom', items: { type: 'string' } },
|
|
81
|
+
series: {
|
|
82
|
+
type: 'array',
|
|
83
|
+
description: 'chart',
|
|
84
|
+
items: { type: 'object', properties: { name: { type: 'string' }, values: { type: 'array', items: { type: 'number' } } } }
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
required: ['kind']
|
|
88
|
+
}
|
|
47
89
|
}
|
|
48
90
|
},
|
|
49
91
|
required: ['name', 'title', 'blocks']
|
|
@@ -55,4 +55,20 @@ describe('the workspace', () => {
|
|
|
55
55
|
expect(showInWorkspace({ blocks: [{ kind: 'text', markdown: 'x' }] })).toMatchObject({ rejected: true, error: 'no-title' })
|
|
56
56
|
expect(showInWorkspace({ title: 'x', blocks: [] })).toMatchObject({ rejected: true, error: 'no-blocks' })
|
|
57
57
|
})
|
|
58
|
+
|
|
59
|
+
it('a number column reads the text a model writes - "12,400" is 12400, a word stays a word', () => {
|
|
60
|
+
const table: any = normalizeWorkspace({ title: 't', blocks: [{ kind: 'table', columns: [{ name: 'a', type: 'number' }], rows: [['12,400'], ['n/a']] }] }).workspace!.blocks[0]
|
|
61
|
+
expect(table.rows).toEqual([{ a: 12400 }, { a: 'n/a' }])
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('★ every field of the tool has one type, and every object says its fields - what Gemini accepts', () => {
|
|
65
|
+
const walk = (schema: any, path: string, out: string[]) => {
|
|
66
|
+
if (!schema || typeof schema !== 'object') return out
|
|
67
|
+
if (Array.isArray(schema.type)) out.push(`${path}: a list of types`)
|
|
68
|
+
if (schema.type === 'object' && !Object.keys(schema.properties ?? {}).length) out.push(`${path}: an object with no fields`)
|
|
69
|
+
for (const [key, value] of Object.entries(schema)) if (value && typeof value === 'object') walk(value, `${path}/${key}`, out)
|
|
70
|
+
return out
|
|
71
|
+
}
|
|
72
|
+
expect(walk((workspaceCategory.specs[0] as any).schema, '', [])).toEqual([])
|
|
73
|
+
})
|
|
58
74
|
})
|