@things-factory/ai-client-base 10.1.245 → 10.1.247
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/agent/ai-client-for.d.ts +5 -0
- package/dist-server/service/agent/ai-client-for.js +15 -2
- package/dist-server/service/agent/ai-client-for.js.map +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +5 -5
- package/server/service/agent/ai-client-for.test.ts +20 -2
- package/server/service/agent/ai-client-for.ts +14 -1
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.247",
|
|
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.247",
|
|
28
28
|
"@things-factory/env": "^10.1.149",
|
|
29
|
-
"@things-factory/setting-base": "^10.1.
|
|
30
|
-
"@things-factory/shell": "^10.1.
|
|
29
|
+
"@things-factory/setting-base": "^10.1.247",
|
|
30
|
+
"@things-factory/shell": "^10.1.247",
|
|
31
31
|
"openai": "^4.0.0"
|
|
32
32
|
},
|
|
33
|
-
"gitHead": "
|
|
33
|
+
"gitHead": "3c672029b2ef4a09f3370d37e5d225392856a602"
|
|
34
34
|
}
|
|
@@ -4,15 +4,22 @@
|
|
|
4
4
|
* design/ai/agent-settings.md §4).
|
|
5
5
|
*/
|
|
6
6
|
const saved: any[] = []
|
|
7
|
+
/* How long a usage row takes to write: 0, or slower than the answer it records. */
|
|
8
|
+
const mockWrite = { ms: 0 }
|
|
7
9
|
|
|
8
10
|
jest.mock('@things-factory/shell', () => ({
|
|
9
11
|
...jest.requireActual('@things-factory/shell'),
|
|
10
|
-
getRepository: jest.fn(() => ({
|
|
12
|
+
getRepository: jest.fn(() => ({
|
|
13
|
+
save: async (rows: any[]) => {
|
|
14
|
+
if (mockWrite.ms) await new Promise(resolve => setTimeout(resolve, mockWrite.ms))
|
|
15
|
+
saved.push(...rows)
|
|
16
|
+
}
|
|
17
|
+
}))
|
|
11
18
|
}))
|
|
12
19
|
jest.mock('./agent-directory', () => ({ readAiAgentDirectory: jest.fn() }))
|
|
13
20
|
|
|
14
21
|
import { readAiAgentDirectory } from './agent-directory'
|
|
15
|
-
import { aiClientFor, hourOf, outcomeOf, pickAgent } from './ai-client-for'
|
|
22
|
+
import { aiClientFor, aiUsageRecorded, hourOf, outcomeOf, pickAgent } from './ai-client-for'
|
|
16
23
|
import { registerAIProvider } from '../factory'
|
|
17
24
|
|
|
18
25
|
const entry = (id: string, extra: any = {}) => ({
|
|
@@ -49,6 +56,7 @@ const flush = () => new Promise(resolve => setImmediate(resolve))
|
|
|
49
56
|
beforeEach(() => {
|
|
50
57
|
saved.length = 0
|
|
51
58
|
replies = []
|
|
59
|
+
mockWrite.ms = 0
|
|
52
60
|
})
|
|
53
61
|
|
|
54
62
|
describe('which agent answers', () => {
|
|
@@ -115,6 +123,16 @@ describe('usage', () => {
|
|
|
115
123
|
expect(saved[0].creator).toBeUndefined()
|
|
116
124
|
})
|
|
117
125
|
|
|
126
|
+
/* A script closing its database after its last call (figure-ai-scenarios-agent.cjs) waits for the rows first. */
|
|
127
|
+
it('the rows still being written are waited for: none is left to a database closed after the last answer', async () => {
|
|
128
|
+
mockWrite.ms = 30
|
|
129
|
+
const client = await aiClientFor({ domain: { id: 'plant' } as any })
|
|
130
|
+
await client.chatWithTools!([], [], {})
|
|
131
|
+
expect(saved).toHaveLength(0)
|
|
132
|
+
await aiUsageRecorded()
|
|
133
|
+
expect(saved).toHaveLength(2)
|
|
134
|
+
})
|
|
135
|
+
|
|
118
136
|
it('the caller\'s own usage listener still hears every request', async () => {
|
|
119
137
|
const heard: any[] = []
|
|
120
138
|
const client = await aiClientFor({ domain: { id: 'plant' } as any })
|
|
@@ -32,6 +32,17 @@ export interface AgentClient extends AIClient {
|
|
|
32
32
|
readonly agent: { id: string; name: string; provider: string; model: string }
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
/* The usage rows still being written: a request's answer does not wait for its row. */
|
|
36
|
+
const recording = new Set<Promise<unknown>>()
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Once every usage row asked for so far is written (or failed and said so). For a process that closes its database
|
|
40
|
+
* after its last call -- a script running scenarios -- so the last rows are not lost to a closed handle.
|
|
41
|
+
*/
|
|
42
|
+
export async function aiUsageRecorded(): Promise<void> {
|
|
43
|
+
while (recording.size) await Promise.allSettled([...recording])
|
|
44
|
+
}
|
|
45
|
+
|
|
35
46
|
export async function aiClientFor(request: AiClientRequest): Promise<AgentClient> {
|
|
36
47
|
if (_testing) return _testing(request)
|
|
37
48
|
if (!request.domain) throw new Error('aiClientFor needs the domain the call is made in')
|
|
@@ -133,9 +144,11 @@ function meter(client: AIClient, entry: AiAgentEntry, request: AiClientRequest):
|
|
|
133
144
|
}))
|
|
134
145
|
/* Outside the caller's transaction: a rolled-back write still spent the tokens. By name, so the decorated entity
|
|
135
146
|
stays out of this module's imports. */
|
|
136
|
-
getRepository<AiUsage>('AiUsage')
|
|
147
|
+
const written: Promise<unknown> = getRepository<AiUsage>('AiUsage')
|
|
137
148
|
.save(rows as any)
|
|
138
149
|
.catch(error => console.error('[ai-client-base] could not record AI usage:', error?.message ?? error))
|
|
150
|
+
.finally(() => recording.delete(written))
|
|
151
|
+
recording.add(written)
|
|
139
152
|
}
|
|
140
153
|
|
|
141
154
|
const metered: AgentClient = {
|