@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@things-factory/ai-assistant",
3
- "version": "10.1.97",
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": "79bebdf2ce31c5e82134757678be2cc1bb6e4dec"
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
- const type = field.type === 'number' || field.type === 'integer' ? ['number', 'string'] : field.type === 'boolean' ? ['boolean', 'string'] : 'string'
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: ['number', 'string'] },
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, scalar(Array.isArray(row) ? row[i] : row?.[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: { type: 'object', properties: { kind: { type: 'string', enum: [...WORKSPACE_BLOCK_KINDS] } }, required: ['kind'] }
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
  })