@things-factory/ai-client-base 10.1.213 → 10.1.215
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/assistant/tool-budget.js +28 -6
- package/dist-server/service/assistant/tool-budget.js.map +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +4 -4
- package/server/service/assistant/tool-budget.test.ts +11 -0
- package/server/service/assistant/tool-budget.ts +29 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@things-factory/ai-client-base",
|
|
3
|
-
"version": "10.1.
|
|
3
|
+
"version": "10.1.215",
|
|
4
4
|
"main": "dist-server/index.js",
|
|
5
5
|
"things-factory": true,
|
|
6
6
|
"author": "heartyoh",
|
|
@@ -24,11 +24,11 @@
|
|
|
24
24
|
"dependencies": {
|
|
25
25
|
"@anthropic-ai/sdk": "^0.40.0",
|
|
26
26
|
"@google/generative-ai": "^0.24.0",
|
|
27
|
-
"@things-factory/auth-base": "^10.1.
|
|
27
|
+
"@things-factory/auth-base": "^10.1.215",
|
|
28
28
|
"@things-factory/env": "^10.1.149",
|
|
29
|
-
"@things-factory/setting-base": "^10.1.
|
|
29
|
+
"@things-factory/setting-base": "^10.1.215",
|
|
30
30
|
"@things-factory/shell": "^10.1.208",
|
|
31
31
|
"openai": "^4.0.0"
|
|
32
32
|
},
|
|
33
|
-
"gitHead": "
|
|
33
|
+
"gitHead": "7de5e80eb1f4e2e268746aee39f820e7563f454d"
|
|
34
34
|
}
|
|
@@ -154,6 +154,17 @@ describe('a scope larger than one call carries', () => {
|
|
|
154
154
|
expect(JSON.parse(seen[1].messages.at(-1).content[0].content)).toMatchObject({ opened: ['readList150'], alreadyOffered: ['readList3'] })
|
|
155
155
|
})
|
|
156
156
|
|
|
157
|
+
it('★ a wrong name among right ones opens the right ones, and says what the wrong one most likely meant', async () => {
|
|
158
|
+
const categories = registerLists(165)
|
|
159
|
+
const { client, seen } = recordingClient([reply(undefined, [TC(OPEN_TOOLS, { names: ['readList150', 'queryList151'] })]), reply(undefined, [TC(OPEN_TOOLS, { names: ['queryList151'] })]), reply('done')])
|
|
160
|
+
await registryAssistantChat(input({ client, toolCategories: categories, front: { categories: ['list-3'] } }))
|
|
161
|
+
|
|
162
|
+
expect(JSON.parse(seen[1].messages.at(-1).content[0].content)).toMatchObject({ opened: ['readList150'], notOffered: ['queryList151'], suggestions: { queryList151: ['readList151'] } })
|
|
163
|
+
expect(names(seen[1].tools)).toContain('readList150')
|
|
164
|
+
/* Only wrong names: refused, still with what each most likely meant. */
|
|
165
|
+
expect(JSON.parse(seen[2].messages.at(-1).content[0].content)).toMatchObject({ rejected: true, error: 'tool-not-offered', suggestions: { queryList151: ['readList151'] } })
|
|
166
|
+
})
|
|
167
|
+
|
|
157
168
|
it('★ a tool called before it is opened does not run - the model is told to open it', async () => {
|
|
158
169
|
const categories = registerLists(165)
|
|
159
170
|
const { client, seen } = recordingClient([reply(undefined, [TC('readList150')]), reply('done')])
|
|
@@ -120,11 +120,32 @@ export function toolOfferState(scope: ToolScope, front: ToolFront = {}) {
|
|
|
120
120
|
const offered = new Map<string, ToolSpec>([...plan.offered, ...(opening ? [opening] : [])].map(spec => [spec.name, spec]))
|
|
121
121
|
const categoryOf = new Map<string, ScopedCategory>(scope.categories.flatMap(category => category.specs.map(spec => [spec.name, category] as [string, ScopedCategory])))
|
|
122
122
|
|
|
123
|
+
/*
|
|
124
|
+
* The tools of this scope a wrong name most likely meant - the same name after its verb (`readOfficeExpenditureRequest`
|
|
125
|
+
* for `queryOfficeExpenditureRequest`). Only this scope's: a tool the person may not use is never named.
|
|
126
|
+
*/
|
|
127
|
+
const meant = (name: string) => {
|
|
128
|
+
const stem = name.replace(/^[a-z]+/, '')
|
|
129
|
+
return stem ? [...openable.keys(), ...offered.keys()].filter(one => one !== name && one.replace(/^[a-z]+/, '') === stem) : []
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/*
|
|
133
|
+
* A wrong name among right ones does not hold the right ones back: asked for a card, the model opened the card tool with
|
|
134
|
+
* a read it named wrong, got nothing open, and told the person the tools were not there (2026-10-25). The right ones
|
|
135
|
+
* open; each wrong one is said with what it most likely meant.
|
|
136
|
+
*/
|
|
123
137
|
function open(names: string[]) {
|
|
124
|
-
const unknown = names.filter(name => !openable.has(name) && !offered.has(name))
|
|
125
|
-
|
|
126
|
-
|
|
138
|
+
const unknown = [...new Set(names.filter(name => !openable.has(name) && !offered.has(name)))]
|
|
139
|
+
const suggestions = Object.fromEntries(unknown.map(name => [name, meant(name)]).filter(([, near]) => near.length))
|
|
140
|
+
const notInList = unknown.length
|
|
141
|
+
? `${unknown.join(', ')} ${unknown.length > 1 ? 'are' : 'is'} not in the list of tools you can open` +
|
|
142
|
+
(Object.keys(suggestions).length ? ` - ${Object.entries(suggestions).map(([name, near]) => `for ${name} use ${(near as string[]).join(' or ')}`).join('; ')}` : '') +
|
|
143
|
+
'. Use the exact names from the list.'
|
|
144
|
+
: ''
|
|
145
|
+
if (unknown.length && unknown.length === new Set(names).size) {
|
|
146
|
+
return { rejected: true, error: 'tool-not-offered', names: unknown, ...(Object.keys(suggestions).length ? { suggestions } : {}), message: `${notInList} Call ${OPEN_TOOLS} again now.` }
|
|
127
147
|
}
|
|
148
|
+
names = names.filter(name => !unknown.includes(name))
|
|
128
149
|
const fresh = [...new Set(names)].filter(name => !offered.has(name))
|
|
129
150
|
const already = [...new Set(names)].filter(name => offered.has(name) && name !== OPEN_TOOLS)
|
|
130
151
|
const room = MAX_OFFERED_TOOLS - offered.size
|
|
@@ -138,13 +159,15 @@ export function toolOfferState(scope: ToolScope, front: ToolFront = {}) {
|
|
|
138
159
|
}
|
|
139
160
|
for (const name of fresh) offered.set(name, openable.get(name)!)
|
|
140
161
|
const guidance = [...new Set(fresh.map(name => categoryOf.get(name)?.guidance).filter(Boolean))]
|
|
162
|
+
const said = already.length
|
|
163
|
+
? `${already.join(', ')} ${already.length > 1 ? 'are' : 'is'} already open - call ${already.length > 1 ? 'them' : 'it'} now. ${fresh.length ? 'The others are open from your next step on.' : ''}`.trim()
|
|
164
|
+
: 'these tools are open from your next step on. Call them as the question needs.'
|
|
141
165
|
return {
|
|
142
166
|
opened: fresh,
|
|
143
167
|
...(already.length ? { alreadyOffered: already } : {}),
|
|
168
|
+
...(unknown.length ? { notOffered: unknown, ...(Object.keys(suggestions).length ? { suggestions } : {}) } : {}),
|
|
144
169
|
...(guidance.length ? { guidance: guidance.join('\n') } : {}),
|
|
145
|
-
message:
|
|
146
|
-
? `${already.join(', ')} ${already.length > 1 ? 'are' : 'is'} already open - call ${already.length > 1 ? 'them' : 'it'} now. ${fresh.length ? 'The others are open from your next step on.' : ''}`.trim()
|
|
147
|
-
: 'these tools are open from your next step on. Call them as the question needs.'
|
|
170
|
+
message: notInList ? `${said} ${notInList} Open it now and go on.` : said
|
|
148
171
|
}
|
|
149
172
|
}
|
|
150
173
|
|