@things-factory/ai-assistant 10.1.97 → 10.1.98
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/dist-server/service/data-entry/entry-tools.js +6 -2
- package/dist-server/service/data-entry/entry-tools.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/entry-tools.ts +6 -2
- 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.98",
|
|
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": "6321b11b694621c8cf7ba105650071eb88e9beb7"
|
|
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
|
+
})
|
|
@@ -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',
|
|
@@ -205,7 +209,7 @@ export function entryCategoryOf(target: EntryTarget, run: (context: any) => Grap
|
|
|
205
209
|
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
210
|
properties: {
|
|
207
211
|
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:
|
|
212
|
+
value: { type: 'number' },
|
|
209
213
|
attachmentId: { type: 'string' },
|
|
210
214
|
where: { type: 'string' }
|
|
211
215
|
}
|
|
@@ -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
|
})
|