@things-factory/ai-assistant 10.1.154 → 10.1.155
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/bootstrap.ts +27 -0
- package/client/components/assistant-chat.ts +229 -178
- package/client/components/assistant-dock.ts +53 -48
- package/client/components/assistant-session-manager.ts +82 -100
- package/client/components/assistant-session-toolbar.ts +11 -3
- package/client/components/chat-input-builder.ts +8 -2
- package/client/settings/agent-settings-model.ts +164 -0
- package/client/settings/ai-agent-setting-let.ts +684 -0
- package/client/settings/ai-usage-setting-let.ts +252 -0
- package/client/settings/usage-model.ts +46 -0
- package/client/setup-assistant-dock.ts +2 -3
- package/client/utils/agent-choice.ts +56 -0
- package/client/utils/assistant-navigation.test.ts +14 -8
- package/client/utils/assistant-navigation.ts +8 -3
- package/client/utils/composer-frame.ts +28 -2
- package/client/utils/menu-room.ts +33 -0
- package/dist-client/bootstrap.d.ts +8 -0
- package/dist-client/bootstrap.js +26 -0
- package/dist-client/bootstrap.js.map +1 -0
- package/dist-client/components/assistant-chat.d.ts +32 -4
- package/dist-client/components/assistant-chat.js +234 -175
- package/dist-client/components/assistant-chat.js.map +1 -1
- package/dist-client/components/assistant-dock.d.ts +6 -8
- package/dist-client/components/assistant-dock.js +52 -60
- package/dist-client/components/assistant-dock.js.map +1 -1
- package/dist-client/components/assistant-session-manager.d.ts +8 -7
- package/dist-client/components/assistant-session-manager.js +84 -124
- package/dist-client/components/assistant-session-manager.js.map +1 -1
- package/dist-client/components/assistant-session-toolbar.d.ts +1 -0
- package/dist-client/components/assistant-session-toolbar.js +10 -3
- package/dist-client/components/assistant-session-toolbar.js.map +1 -1
- package/dist-client/components/chat-input-builder.d.ts +4 -0
- package/dist-client/components/chat-input-builder.js +4 -2
- package/dist-client/components/chat-input-builder.js.map +1 -1
- package/dist-client/settings/agent-settings-model.d.ts +93 -0
- package/dist-client/settings/agent-settings-model.js +95 -0
- package/dist-client/settings/agent-settings-model.js.map +1 -0
- package/dist-client/settings/ai-agent-setting-let.d.ts +51 -0
- package/dist-client/settings/ai-agent-setting-let.js +672 -0
- package/dist-client/settings/ai-agent-setting-let.js.map +1 -0
- package/dist-client/settings/ai-usage-setting-let.d.ts +26 -0
- package/dist-client/settings/ai-usage-setting-let.js +276 -0
- package/dist-client/settings/ai-usage-setting-let.js.map +1 -0
- package/dist-client/settings/usage-model.d.ts +32 -0
- package/dist-client/settings/usage-model.js +25 -0
- package/dist-client/settings/usage-model.js.map +1 -0
- package/dist-client/setup-assistant-dock.d.ts +0 -1
- package/dist-client/setup-assistant-dock.js +0 -1
- package/dist-client/setup-assistant-dock.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-client/utils/agent-choice.d.ts +24 -0
- package/dist-client/utils/agent-choice.js +38 -0
- package/dist-client/utils/agent-choice.js.map +1 -0
- package/dist-client/utils/assistant-navigation.d.ts +8 -3
- package/dist-client/utils/assistant-navigation.js +7 -2
- package/dist-client/utils/assistant-navigation.js.map +1 -1
- package/dist-client/utils/assistant-navigation.test.js +14 -8
- package/dist-client/utils/assistant-navigation.test.js.map +1 -1
- package/dist-client/utils/composer-frame.d.ts +23 -2
- package/dist-client/utils/composer-frame.js +20 -2
- package/dist-client/utils/composer-frame.js.map +1 -1
- package/dist-client/utils/menu-room.d.ts +13 -0
- package/dist-client/utils/menu-room.js +26 -0
- package/dist-client/utils/menu-room.js.map +1 -0
- package/dist-server/service/ai-agent-resolver.d.ts +82 -0
- package/dist-server/service/ai-agent-resolver.js +417 -0
- package/dist-server/service/ai-agent-resolver.js.map +1 -0
- package/dist-server/service/assistant-capability-resolver.d.ts +1 -1
- package/dist-server/service/assistant-capability-resolver.js +7 -7
- package/dist-server/service/assistant-capability-resolver.js.map +1 -1
- package/dist-server/service/assistant-chat-resolver.d.ts +2 -0
- package/dist-server/service/assistant-chat-resolver.js +32 -15
- package/dist-server/service/assistant-chat-resolver.js.map +1 -1
- package/dist-server/service/assistant-mode/resolve.d.ts +0 -1
- package/dist-server/service/assistant-mode/resolve.js +0 -1
- package/dist-server/service/assistant-mode/resolve.js.map +1 -1
- package/dist-server/service/assistant-mode/types.d.ts +1 -2
- package/dist-server/service/assistant-mode/types.js.map +1 -1
- package/dist-server/service/chat-session/chat-session.d.ts +2 -0
- package/dist-server/service/chat-session/chat-session.js +5 -0
- package/dist-server/service/chat-session/chat-session.js.map +1 -1
- package/dist-server/service/index.d.ts +2 -1
- package/dist-server/service/index.js +3 -0
- package/dist-server/service/index.js.map +1 -1
- package/dist-server/service/testing/mode-conformance.js +0 -1
- package/dist-server/service/testing/mode-conformance.js.map +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +9 -7
- package/server/service/ai-agent-resolver.ts +330 -0
- package/server/service/assistant-capability-resolver.ts +7 -7
- package/server/service/assistant-chat-resolver.ts +32 -24
- package/server/service/assistant-mode/resolve.test.ts +0 -2
- package/server/service/assistant-mode/resolve.ts +0 -2
- package/server/service/assistant-mode/types.ts +1 -2
- package/server/service/chat-session/chat-session.ts +5 -0
- package/server/service/index.ts +3 -0
- package/server/service/testing/mode-conformance.ts +0 -1
- package/server/tool-registry-boot-report.test.ts +2 -0
- package/test/ai-agent-settings.test.ts +123 -0
- package/test/assistant-dock.test.ts +3 -2
- package/test/assistant-session-manager.test.ts +10 -4
- package/test/assistant-session-toolbar.test.ts +8 -0
- package/test/composer-frame.test.ts +40 -2
- package/test/menu-room.test.ts +33 -0
- package/test/open-doors.test.ts +3 -1
- package/test/session-discovery.test.ts +7 -6
- package/things-factory.config.js +2 -0
- package/translations/en.json +78 -12
- package/translations/es.json +78 -12
- package/translations/ja.json +78 -12
- package/translations/ko.json +78 -12
- package/translations/ms.json +78 -12
- package/translations/zh.json +78 -12
- package/client/components/chat-triggers.test.ts +0 -72
- package/client/components/chat-triggers.ts +0 -62
- package/dist-client/components/chat-triggers.d.ts +0 -16
- package/dist-client/components/chat-triggers.js +0 -42
- package/dist-client/components/chat-triggers.js.map +0 -1
- package/dist-client/components/chat-triggers.test.d.ts +0 -1
- package/dist-client/components/chat-triggers.test.js +0 -61
- package/dist-client/components/chat-triggers.test.js.map +0 -1
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.155",
|
|
4
4
|
"main": "dist-server/index.js",
|
|
5
5
|
"things-factory": true,
|
|
6
6
|
"author": "heartyoh",
|
|
@@ -29,16 +29,18 @@
|
|
|
29
29
|
"@material/web": "^2.0.0",
|
|
30
30
|
"@operato/graphql": "^10.0.0",
|
|
31
31
|
"@operato/i18n": "^10.0.0",
|
|
32
|
+
"@operato/input": "^10.0.0",
|
|
32
33
|
"@operato/layout": "^10.20.0",
|
|
33
34
|
"@operato/popup": "^10.16.0",
|
|
34
35
|
"@operato/shell": "^10.0.0",
|
|
35
36
|
"@operato/styles": "^10.0.0",
|
|
36
|
-
"@things-factory/ai-client-base": "^10.1.
|
|
37
|
-
"@things-factory/apptool-base": "^10.1.
|
|
38
|
-
"@things-factory/attachment-base": "^10.1.
|
|
39
|
-
"@things-factory/auth-base": "^10.1.
|
|
37
|
+
"@things-factory/ai-client-base": "^10.1.155",
|
|
38
|
+
"@things-factory/apptool-base": "^10.1.155",
|
|
39
|
+
"@things-factory/attachment-base": "^10.1.155",
|
|
40
|
+
"@things-factory/auth-base": "^10.1.155",
|
|
40
41
|
"@things-factory/env": "^10.1.149",
|
|
41
|
-
"@things-factory/
|
|
42
|
+
"@things-factory/setting-base": "^10.1.155",
|
|
43
|
+
"@things-factory/shell": "^10.1.155",
|
|
42
44
|
"dompurify": "^3.0.0",
|
|
43
45
|
"exceljs": "4.2.0",
|
|
44
46
|
"graphql-scalars": "^1.22.0",
|
|
@@ -52,5 +54,5 @@
|
|
|
52
54
|
"copyfiles": "^2.4.1",
|
|
53
55
|
"rimraf": "^5.0.0"
|
|
54
56
|
},
|
|
55
|
-
"gitHead": "
|
|
57
|
+
"gitHead": "0211c9c0764a26c10151fdc7d60094fc334905ff"
|
|
56
58
|
}
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The doors of a domain's AI agents (design/ai/agent-settings.md).
|
|
3
|
+
*
|
|
4
|
+
* The settings card reads and saves the agents of the domain it is in; the dock reads the agents a person may pick.
|
|
5
|
+
* Everything here is a thin door over ai-client-base, which owns the agents, their keys and their usage. A key never
|
|
6
|
+
* leaves the server: reads carry a hint of this domain's own key and nothing of a key from above.
|
|
7
|
+
*/
|
|
8
|
+
import { Arg, Ctx, Directive, Field, ID, InputType, Int, Mutation, ObjectType, Query, Resolver, UseMiddleware } from 'type-graphql'
|
|
9
|
+
import '@things-factory/auth-base'
|
|
10
|
+
import { SpeakRefusals } from '@things-factory/shell'
|
|
11
|
+
import {
|
|
12
|
+
type AiAgentEntry,
|
|
13
|
+
capabilityOf,
|
|
14
|
+
listProviderModels,
|
|
15
|
+
readAgentUsersBelow,
|
|
16
|
+
readAiAgentDirectory,
|
|
17
|
+
saveAiAgents,
|
|
18
|
+
summarizeAiUsage
|
|
19
|
+
} from '@things-factory/ai-client-base'
|
|
20
|
+
|
|
21
|
+
@ObjectType({ description: 'A domain, by id and name.' })
|
|
22
|
+
class AiAgentDomain {
|
|
23
|
+
@Field(() => ID, { description: 'Domain id.' })
|
|
24
|
+
id: string
|
|
25
|
+
|
|
26
|
+
@Field({ description: 'Domain name.' })
|
|
27
|
+
name: string
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
@ObjectType({ description: 'One AI agent as the settings card shows it. The key itself is never here.' })
|
|
31
|
+
class AiAgentView {
|
|
32
|
+
@Field(() => ID, { description: 'Agent id.' })
|
|
33
|
+
id: string
|
|
34
|
+
|
|
35
|
+
@Field({ description: 'The name a person picks it by.' })
|
|
36
|
+
name: string
|
|
37
|
+
|
|
38
|
+
@Field({ description: 'openai, anthropic or gemini.' })
|
|
39
|
+
provider: string
|
|
40
|
+
|
|
41
|
+
@Field({ description: 'The model it answers with.' })
|
|
42
|
+
model: string
|
|
43
|
+
|
|
44
|
+
@Field({ nullable: true, description: 'A gateway or proxy the provider is reached through.' })
|
|
45
|
+
baseURL?: string
|
|
46
|
+
|
|
47
|
+
@Field({ nullable: true, description: 'OpenAI organization.' })
|
|
48
|
+
organization?: string
|
|
49
|
+
|
|
50
|
+
@Field({ description: 'Off takes it out of this domain and every domain below.' })
|
|
51
|
+
active: boolean
|
|
52
|
+
|
|
53
|
+
@Field(() => AiAgentDomain, { description: 'The domain that declared it.' })
|
|
54
|
+
owner: AiAgentDomain
|
|
55
|
+
|
|
56
|
+
@Field({ description: 'Declared by a domain above. Read-only here.' })
|
|
57
|
+
inherited: boolean
|
|
58
|
+
|
|
59
|
+
@Field({ description: 'An inherited agent this domain does not use.' })
|
|
60
|
+
turnedOff: boolean
|
|
61
|
+
|
|
62
|
+
@Field({ description: 'own, inherited, overridden or missing - where the key that answers comes from.' })
|
|
63
|
+
keyState: string
|
|
64
|
+
|
|
65
|
+
@Field(() => AiAgentDomain, { nullable: true, description: 'The domain whose key answers.' })
|
|
66
|
+
keyDomain?: AiAgentDomain
|
|
67
|
+
|
|
68
|
+
@Field({ nullable: true, description: 'A hint of this domain\'s own key, such as ••••abcd. Never given for a key from above.' })
|
|
69
|
+
keyHint?: string
|
|
70
|
+
|
|
71
|
+
@Field({ description: 'Active, used here, and has a key - a person can pick it.' })
|
|
72
|
+
usable: boolean
|
|
73
|
+
|
|
74
|
+
@Field({ description: 'The agent a person gets before picking one.' })
|
|
75
|
+
isDefault: boolean
|
|
76
|
+
|
|
77
|
+
@Field(() => [AiAgentDomain], { description: 'For this domain\'s own agents: the domains below that use it.' })
|
|
78
|
+
usedBelow: AiAgentDomain[]
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
@ObjectType({ description: 'A domain\'s AI agents and how its AI stands.' })
|
|
82
|
+
class AiAgentSettings {
|
|
83
|
+
@Field(() => [AiAgentDomain], { description: 'This domain and the domains above it, closest first.' })
|
|
84
|
+
chain: AiAgentDomain[]
|
|
85
|
+
|
|
86
|
+
@Field(() => [AiAgentView], { description: 'This domain\'s agents, then each domain above\'s, closest first.' })
|
|
87
|
+
agents: AiAgentView[]
|
|
88
|
+
|
|
89
|
+
@Field({ nullable: true, description: 'The agent a person gets before picking one.' })
|
|
90
|
+
defaultAgentId?: string
|
|
91
|
+
|
|
92
|
+
@Field({ description: 'own, inherited, first or none - how the default was decided.' })
|
|
93
|
+
defaultFrom: string
|
|
94
|
+
|
|
95
|
+
@Field({ description: 'configured or not-configured.' })
|
|
96
|
+
state: string
|
|
97
|
+
|
|
98
|
+
@Field({ nullable: true, description: 'Why it is not configured: no-agent, no-agent-key or no-usable-agent.' })
|
|
99
|
+
reason?: string
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
@ObjectType({ description: 'An AI agent a person may pick in the dock.' })
|
|
103
|
+
class AiAgentChoice {
|
|
104
|
+
@Field(() => ID, { description: 'Agent id - what assistantChat takes as agentId.' })
|
|
105
|
+
id: string
|
|
106
|
+
|
|
107
|
+
@Field({ description: 'The name a person picks it by.' })
|
|
108
|
+
name: string
|
|
109
|
+
|
|
110
|
+
@Field({ description: 'openai, anthropic or gemini.' })
|
|
111
|
+
provider: string
|
|
112
|
+
|
|
113
|
+
@Field({ description: 'The model it answers with.' })
|
|
114
|
+
model: string
|
|
115
|
+
|
|
116
|
+
@Field({ description: 'The agent a person gets before picking one.' })
|
|
117
|
+
isDefault: boolean
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
@InputType({ description: 'One of this domain\'s own agents as the settings card saves it.' })
|
|
121
|
+
class AiAgentInput {
|
|
122
|
+
@Field(() => ID, { nullable: true, description: 'Empty for a new agent.' })
|
|
123
|
+
id?: string
|
|
124
|
+
|
|
125
|
+
@Field({ description: 'The name a person picks it by.' })
|
|
126
|
+
name: string
|
|
127
|
+
|
|
128
|
+
@Field({ description: 'openai, anthropic or gemini.' })
|
|
129
|
+
provider: string
|
|
130
|
+
|
|
131
|
+
@Field({ description: 'A model from aiProviderModels.' })
|
|
132
|
+
model: string
|
|
133
|
+
|
|
134
|
+
@Field({ nullable: true, description: 'A gateway or proxy the provider is reached through.' })
|
|
135
|
+
baseURL?: string
|
|
136
|
+
|
|
137
|
+
@Field({ nullable: true, description: 'OpenAI organization.' })
|
|
138
|
+
organization?: string
|
|
139
|
+
|
|
140
|
+
@Field({ description: 'Off takes it out of this domain and every domain below.' })
|
|
141
|
+
active: boolean
|
|
142
|
+
|
|
143
|
+
@Field({ nullable: true, description: 'A new API key. Left out, the stored key stays.' })
|
|
144
|
+
apiKey?: string
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
@InputType({ description: 'This domain\'s key for an agent it inherits.' })
|
|
148
|
+
class InheritedKeyInput {
|
|
149
|
+
@Field(() => ID, { description: 'The inherited agent.' })
|
|
150
|
+
agentId: string
|
|
151
|
+
|
|
152
|
+
@Field({ nullable: true, description: 'A key of this domain to answer the agent with.' })
|
|
153
|
+
apiKey?: string
|
|
154
|
+
|
|
155
|
+
@Field({ nullable: true, description: 'Go back to the key of the domain above.' })
|
|
156
|
+
clear?: boolean
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
@InputType({ description: 'A domain\'s agents as the settings card saves them.' })
|
|
160
|
+
class AiAgentsInput {
|
|
161
|
+
@Field(() => [AiAgentInput], { description: 'The whole list of this domain\'s own agents. One left out is deleted.' })
|
|
162
|
+
agents: AiAgentInput[]
|
|
163
|
+
|
|
164
|
+
@Field({ nullable: true, description: 'This domain\'s default - its own agent or an inherited one.' })
|
|
165
|
+
defaultAgentId?: string
|
|
166
|
+
|
|
167
|
+
@Field(() => [String], { nullable: true, description: 'Inherited agents this domain does not use.' })
|
|
168
|
+
turnedOff?: string[]
|
|
169
|
+
|
|
170
|
+
@Field(() => [InheritedKeyInput], { nullable: true, description: 'This domain\'s keys for inherited agents.' })
|
|
171
|
+
inheritedKeys?: InheritedKeyInput[]
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
@ObjectType({ description: 'AI usage of one bucket, agent and person.' })
|
|
175
|
+
class AiUsageBucket {
|
|
176
|
+
@Field({ description: 'The hour, day or month in the domain\'s time zone.' })
|
|
177
|
+
bucket: string
|
|
178
|
+
|
|
179
|
+
@Field({ nullable: true, description: 'Agent id.' })
|
|
180
|
+
agentId?: string
|
|
181
|
+
|
|
182
|
+
@Field({ nullable: true, description: 'Agent name. Empty for a deleted agent.' })
|
|
183
|
+
agentName?: string
|
|
184
|
+
|
|
185
|
+
@Field({ description: 'openai, anthropic or gemini.' })
|
|
186
|
+
provider: string
|
|
187
|
+
|
|
188
|
+
@Field({ description: 'The model the provider reported.' })
|
|
189
|
+
model: string
|
|
190
|
+
|
|
191
|
+
@Field({ description: 'The domain the calls were made in.' })
|
|
192
|
+
domainId: string
|
|
193
|
+
|
|
194
|
+
@Field({ description: 'Its name.' })
|
|
195
|
+
domainName: string
|
|
196
|
+
|
|
197
|
+
@Field({ nullable: true, description: 'The person. Empty for a domain below and for calls with no person.' })
|
|
198
|
+
userId?: string
|
|
199
|
+
|
|
200
|
+
@Field({ nullable: true, description: 'The person\'s name.' })
|
|
201
|
+
userName?: string
|
|
202
|
+
|
|
203
|
+
@Field(() => Int, { description: 'Provider requests.' })
|
|
204
|
+
calls: number
|
|
205
|
+
|
|
206
|
+
@Field(() => Int, { description: 'Input tokens.' })
|
|
207
|
+
inputTokens: number
|
|
208
|
+
|
|
209
|
+
@Field(() => Int, { description: 'Output tokens.' })
|
|
210
|
+
outputTokens: number
|
|
211
|
+
|
|
212
|
+
@Field(() => Int, { description: 'Requests that failed.' })
|
|
213
|
+
failures: number
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
@Resolver()
|
|
217
|
+
@UseMiddleware(SpeakRefusals)
|
|
218
|
+
export class AiAgentResolver {
|
|
219
|
+
@Query(() => AiAgentSettings, { description: 'This domain\'s AI agents, the ones it inherits, and how its AI stands.' })
|
|
220
|
+
@Directive('@privilege(category: "setting", privilege: "query", domainOwnerGranted: true)')
|
|
221
|
+
async aiAgentSettings(@Ctx() context: ResolverContext): Promise<AiAgentSettings> {
|
|
222
|
+
const { domain } = context.state
|
|
223
|
+
const directory = await readAiAgentDirectory(domain)
|
|
224
|
+
const nameOf = new Map(directory.chain.map(one => [one.id, one.name]))
|
|
225
|
+
const ownIds = directory.entries.filter(one => !one.inherited).map(one => one.spec.id)
|
|
226
|
+
const usedBelow = await readAgentUsersBelow(domain, ownIds)
|
|
227
|
+
const capability = capabilityOf(directory)
|
|
228
|
+
|
|
229
|
+
return {
|
|
230
|
+
chain: directory.chain,
|
|
231
|
+
agents: directory.entries.map(entry => agentView(entry, directory.defaultAgentId, nameOf, usedBelow.get(entry.spec.id) ?? [])),
|
|
232
|
+
defaultAgentId: directory.defaultAgentId,
|
|
233
|
+
defaultFrom: directory.defaultFrom,
|
|
234
|
+
state: capability.state,
|
|
235
|
+
reason: capability.reason
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
@Query(() => [AiAgentChoice], { description: 'The AI agents a person may pick in the dock - usable ones only.' })
|
|
240
|
+
@Directive('@privilege(category: "ai-assistant", privilege: "query", domainOwnerGranted: true)')
|
|
241
|
+
async aiAgentChoices(@Ctx() context: ResolverContext): Promise<AiAgentChoice[]> {
|
|
242
|
+
const directory = await readAiAgentDirectory(context.state.domain)
|
|
243
|
+
return directory.entries
|
|
244
|
+
.filter(one => one.usable)
|
|
245
|
+
.map(one => ({
|
|
246
|
+
id: one.spec.id,
|
|
247
|
+
name: one.spec.name,
|
|
248
|
+
provider: one.spec.provider,
|
|
249
|
+
model: one.spec.model,
|
|
250
|
+
isDefault: one.spec.id === directory.defaultAgentId
|
|
251
|
+
}))
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
@Query(() => [String], {
|
|
255
|
+
description:
|
|
256
|
+
'The conversation models a provider offers to a key - a typed key, or the stored key of one of this domain\'s agents. A model is only ever picked from this list.'
|
|
257
|
+
})
|
|
258
|
+
@Directive('@privilege(category: "setting", privilege: "query", domainOwnerGranted: true)')
|
|
259
|
+
async aiProviderModels(
|
|
260
|
+
@Arg('provider') provider: string,
|
|
261
|
+
@Arg('apiKey', { nullable: true }) apiKey: string,
|
|
262
|
+
@Arg('agentId', { nullable: true }) agentId: string,
|
|
263
|
+
@Arg('baseURL', { nullable: true }) baseURL: string,
|
|
264
|
+
@Arg('organization', { nullable: true }) organization: string,
|
|
265
|
+
@Ctx() context: ResolverContext
|
|
266
|
+
): Promise<string[]> {
|
|
267
|
+
let key = apiKey?.trim()
|
|
268
|
+
if (!key && agentId) {
|
|
269
|
+
const directory = await readAiAgentDirectory(context.state.domain)
|
|
270
|
+
key = directory.entries.find(one => one.spec.id === agentId && !one.inherited)?.apiKey
|
|
271
|
+
}
|
|
272
|
+
return listProviderModels({ provider, apiKey: key ?? '', baseURL, organization })
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
@Mutation(() => Boolean, {
|
|
276
|
+
description: 'Saves this domain\'s AI agents and their keys in one transaction. Read aiAgentSettings again for how they now stand.'
|
|
277
|
+
})
|
|
278
|
+
@Directive('@transaction')
|
|
279
|
+
@Directive('@privilege(category: "setting", privilege: "mutation", domainOwnerGranted: true)')
|
|
280
|
+
async saveAiAgents(@Arg('input', () => AiAgentsInput) input: AiAgentsInput, @Ctx() context: ResolverContext): Promise<boolean> {
|
|
281
|
+
const { tx, domain, user } = context.state
|
|
282
|
+
await saveAiAgents(input, { tx, domain, user })
|
|
283
|
+
return true
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
@Query(() => [AiUsageBucket], {
|
|
287
|
+
description:
|
|
288
|
+
'AI usage per bucket, agent and person, in this domain\'s time zone. With includeDescendants, what the domains below did with this domain\'s agents is added - per domain, without their people.'
|
|
289
|
+
})
|
|
290
|
+
@Directive('@privilege(category: "setting", privilege: "query", domainOwnerGranted: true)')
|
|
291
|
+
async aiUsageSummary(
|
|
292
|
+
@Arg('from') from: Date,
|
|
293
|
+
@Arg('to') to: Date,
|
|
294
|
+
@Arg('bucket', { defaultValue: 'day' }) bucket: string,
|
|
295
|
+
@Arg('agentIds', () => [String], { nullable: true }) agentIds: string[],
|
|
296
|
+
@Arg('userIds', () => [String], { nullable: true }) userIds: string[],
|
|
297
|
+
@Arg('includeDescendants', { nullable: true }) includeDescendants: boolean,
|
|
298
|
+
@Ctx() context: ResolverContext
|
|
299
|
+
): Promise<AiUsageBucket[]> {
|
|
300
|
+
const size = bucket === 'hour' || bucket === 'month' ? bucket : 'day'
|
|
301
|
+
return summarizeAiUsage(context.state.domain, { from, to, bucket: size, agentIds, userIds, includeDescendants: !!includeDescendants })
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function agentView(
|
|
306
|
+
entry: AiAgentEntry,
|
|
307
|
+
defaultAgentId: string | undefined,
|
|
308
|
+
nameOf: Map<string, string>,
|
|
309
|
+
usedBelow: { id: string; name: string }[]
|
|
310
|
+
): AiAgentView {
|
|
311
|
+
const domainOf = (id?: string) => (id ? { id, name: nameOf.get(id) ?? id } : undefined)
|
|
312
|
+
return {
|
|
313
|
+
id: entry.spec.id,
|
|
314
|
+
name: entry.spec.name,
|
|
315
|
+
provider: entry.spec.provider,
|
|
316
|
+
model: entry.spec.model,
|
|
317
|
+
baseURL: entry.spec.baseURL,
|
|
318
|
+
organization: entry.spec.organization,
|
|
319
|
+
active: entry.spec.active,
|
|
320
|
+
owner: domainOf(entry.ownerDomainId)!,
|
|
321
|
+
inherited: entry.inherited,
|
|
322
|
+
turnedOff: entry.turnedOff,
|
|
323
|
+
keyState: entry.keyState,
|
|
324
|
+
keyDomain: domainOf(entry.keyDomainId),
|
|
325
|
+
keyHint: entry.keyHint,
|
|
326
|
+
usable: entry.usable,
|
|
327
|
+
isDefault: entry.spec.id === defaultAgentId,
|
|
328
|
+
usedBelow: entry.inherited ? [] : usedBelow
|
|
329
|
+
}
|
|
330
|
+
}
|
|
@@ -29,18 +29,18 @@ class AiRegistrationWarning {
|
|
|
29
29
|
message: string
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
@ObjectType({ description: "Whether this
|
|
32
|
+
@ObjectType({ description: "Whether this domain has an AI, and what the registrations are missing." })
|
|
33
33
|
class AiCapabilityView {
|
|
34
34
|
@Field({ description: "'configured' or 'not-configured'. A screen that has not asked yet knows a third state of its own: it does not know." })
|
|
35
35
|
state: string
|
|
36
36
|
|
|
37
|
-
@Field({ nullable: true, description: '
|
|
37
|
+
@Field({ nullable: true, description: 'The provider of the domain\'s default AI agent.' })
|
|
38
38
|
provider?: string
|
|
39
39
|
|
|
40
|
-
@Field({ nullable: true, description: 'The
|
|
40
|
+
@Field({ nullable: true, description: 'The model of the domain\'s default AI agent.' })
|
|
41
41
|
model?: string
|
|
42
42
|
|
|
43
|
-
@Field({ nullable: true, description: 'Why it is not configured: no-
|
|
43
|
+
@Field({ nullable: true, description: 'Why it is not configured: no-agent · no-agent-key · no-usable-agent.' })
|
|
44
44
|
reason?: string
|
|
45
45
|
|
|
46
46
|
@Field(() => [AiRegistrationWarning], { description: 'Tools with no declared privilege, and modes offering work their tools cannot do.' })
|
|
@@ -50,11 +50,11 @@ class AiCapabilityView {
|
|
|
50
50
|
@Resolver()
|
|
51
51
|
export class AssistantCapabilityResolver {
|
|
52
52
|
@Query(() => AiCapabilityView, {
|
|
53
|
-
description: 'Whether this
|
|
53
|
+
description: 'Whether this domain has a usable AI agent, and which registrations are incomplete. Read this before offering to ask the assistant — do not find out by failing.'
|
|
54
54
|
})
|
|
55
55
|
@Directive('@privilege(category: "ai-assistant", privilege: "query", domainOwnerGranted: true)')
|
|
56
|
-
async aiCapability(@Ctx()
|
|
57
|
-
const capability = aiCapability()
|
|
56
|
+
async aiCapability(@Ctx() context: any): Promise<AiCapabilityView> {
|
|
57
|
+
const capability = await aiCapability(context.state.domain)
|
|
58
58
|
const warnings = [...getToolRegistryWarnings(), ...getAssistantModeWarnings()].map(warning => ({
|
|
59
59
|
code: warning.code,
|
|
60
60
|
params: warning.params,
|
|
@@ -26,15 +26,8 @@ import { Arg, Ctx, Directive, Field, InputType, Mutation, ObjectType, Resolver }
|
|
|
26
26
|
import { GraphQLJSON } from 'graphql-scalars'
|
|
27
27
|
import '@things-factory/auth-base'
|
|
28
28
|
|
|
29
|
-
import {
|
|
30
|
-
import {
|
|
31
|
-
import {
|
|
32
|
-
createAIClient,
|
|
33
|
-
findToolSpec,
|
|
34
|
-
getDefaultAIClient,
|
|
35
|
-
registryAssistantChat,
|
|
36
|
-
type AIClient
|
|
37
|
-
} from '@things-factory/ai-client-base'
|
|
29
|
+
import { getRepository, rethrowSpoken } from '@things-factory/shell'
|
|
30
|
+
import { aiClientFor, findToolSpec, registryAssistantChat, type AgentClient } from '@things-factory/ai-client-base'
|
|
38
31
|
|
|
39
32
|
import { ChatSession } from './chat-session/chat-session.js'
|
|
40
33
|
import { assertStationSession } from './chat-session/station-session-guard.js'
|
|
@@ -132,6 +125,12 @@ class AssistantChatInput {
|
|
|
132
125
|
})
|
|
133
126
|
attachmentIds?: string[]
|
|
134
127
|
|
|
128
|
+
@Field({
|
|
129
|
+
nullable: true,
|
|
130
|
+
description:
|
|
131
|
+
'The AI agent the person picked for this turn, from aiAgentChoices. Omitted takes the domain default. An agent this domain cannot use is refused - the provider, the model and the key are always the server\'s.'
|
|
132
|
+
})
|
|
133
|
+
agentId?: string
|
|
135
134
|
}
|
|
136
135
|
|
|
137
136
|
@ObjectType()
|
|
@@ -142,6 +141,9 @@ class AssistantChatOutput {
|
|
|
142
141
|
@Field({ description: 'AI client identifier (provider:model).' })
|
|
143
142
|
clientId: string
|
|
144
143
|
|
|
144
|
+
@Field({ nullable: true, description: 'The AI agent that answered.' })
|
|
145
|
+
agentId?: string
|
|
146
|
+
|
|
145
147
|
@Field({ nullable: true, description: 'Echo of the session id, when persisted.' })
|
|
146
148
|
sessionId?: string
|
|
147
149
|
|
|
@@ -186,14 +188,6 @@ class AssistantChatOutput {
|
|
|
186
188
|
groundingWarnings?: any
|
|
187
189
|
}
|
|
188
190
|
|
|
189
|
-
function resolveAIClient(): AIClient | undefined {
|
|
190
|
-
const existing = getDefaultAIClient()
|
|
191
|
-
if (existing) return existing
|
|
192
|
-
const cfg = config.get('aiClient', null) as any
|
|
193
|
-
if (cfg && cfg.provider) return createAIClient(cfg)
|
|
194
|
-
return undefined
|
|
195
|
-
}
|
|
196
|
-
|
|
197
191
|
@Resolver()
|
|
198
192
|
export class AssistantChatResolver {
|
|
199
193
|
/*
|
|
@@ -213,12 +207,26 @@ export class AssistantChatResolver {
|
|
|
213
207
|
*/
|
|
214
208
|
@Directive('@privilege(category: "ai-assistant", privilege: "mutation", domainOwnerGranted: true)')
|
|
215
209
|
async assistantChat(@Arg('input') input: AssistantChatInput, @Ctx() context: any): Promise<AssistantChatOutput> {
|
|
216
|
-
const
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
210
|
+
const { domain, user } = context.state
|
|
211
|
+
|
|
212
|
+
/*
|
|
213
|
+
* The domain's agent - the one picked, or the default (design/ai/agent-settings.md §4). Asked before anything is
|
|
214
|
+
* stored: a domain with no AI, or an agent it cannot use, is refused as itself.
|
|
215
|
+
*/
|
|
216
|
+
let client: AgentClient
|
|
217
|
+
try {
|
|
218
|
+
client = await aiClientFor({
|
|
219
|
+
domain,
|
|
220
|
+
user,
|
|
221
|
+
agentId: input.agentId,
|
|
222
|
+
modeKind: input.mode.kind,
|
|
223
|
+
sessionId: input.sessionId
|
|
224
|
+
})
|
|
225
|
+
} catch (error) {
|
|
226
|
+
rethrowSpoken(error, context)
|
|
221
227
|
}
|
|
228
|
+
/* A tool that asks a model of its own within this turn answers with the same agent (ToolCallContext.state). */
|
|
229
|
+
context.state.aiAgentId = client.agent.id
|
|
222
230
|
|
|
223
231
|
/*
|
|
224
232
|
* Read the declaration before anything is stored. A kind this server does not have is a
|
|
@@ -228,7 +236,6 @@ export class AssistantChatResolver {
|
|
|
228
236
|
const t = requestTranslator(context)
|
|
229
237
|
const settings = resolveModeSettings(input.mode, t)
|
|
230
238
|
|
|
231
|
-
const { domain, user } = context.state
|
|
232
239
|
const sessionRepo = getRepository(ChatSession)
|
|
233
240
|
const messageRepo = getRepository(ChatMessage)
|
|
234
241
|
|
|
@@ -493,7 +500,6 @@ export class AssistantChatResolver {
|
|
|
493
500
|
hostContext: input.hostContext
|
|
494
501
|
}),
|
|
495
502
|
state: context.state,
|
|
496
|
-
model: settings.model,
|
|
497
503
|
temperature: settings.temperature,
|
|
498
504
|
maxTokens: settings.maxTokens,
|
|
499
505
|
requireToolOnFirstTurn: settings.requireToolOnFirstTurn
|
|
@@ -543,6 +549,7 @@ export class AssistantChatResolver {
|
|
|
543
549
|
|
|
544
550
|
await sessionRepo.update(session.id!, {
|
|
545
551
|
aiClientId: client.id,
|
|
552
|
+
agentId: client.agent.id,
|
|
546
553
|
updater: user,
|
|
547
554
|
...activityPatch(result.reply, new Date(), answerDoors)
|
|
548
555
|
} as any)
|
|
@@ -568,6 +575,7 @@ export class AssistantChatResolver {
|
|
|
568
575
|
return {
|
|
569
576
|
reply: result.reply,
|
|
570
577
|
clientId: client.id,
|
|
578
|
+
agentId: client.agent.id,
|
|
571
579
|
sessionId: session?.id,
|
|
572
580
|
userMessageId,
|
|
573
581
|
assistantMessageId,
|
|
@@ -19,7 +19,6 @@ beforeEach(() => {
|
|
|
19
19
|
systemPrompt: 'you answer about one space',
|
|
20
20
|
toolCategories: ['twin-ops'],
|
|
21
21
|
requireToolOnFirstTurn: true,
|
|
22
|
-
model: 'claude-sonnet-5',
|
|
23
22
|
maxTokens: 2048,
|
|
24
23
|
temperature: 0.2,
|
|
25
24
|
slashTemplates: [{ name: 'summarize', descriptionKey: 'twin.slash.summarize', templateKey: 'twin.slash.summarize.template' }],
|
|
@@ -37,7 +36,6 @@ it('★ the settings come from the registered mode', () => {
|
|
|
37
36
|
/* Every conversation may put a large result on the AI 작업판 (design/ai/workspace.md). */
|
|
38
37
|
toolCategories: ['twin-ops', 'assistant-workspace'],
|
|
39
38
|
requireToolOnFirstTurn: true,
|
|
40
|
-
model: 'claude-sonnet-5',
|
|
41
39
|
temperature: 0.2,
|
|
42
40
|
maxTokens: 2048
|
|
43
41
|
})
|
|
@@ -38,7 +38,6 @@ export interface ModeSettings {
|
|
|
38
38
|
systemPrompt: string
|
|
39
39
|
toolCategories?: string[]
|
|
40
40
|
requireToolOnFirstTurn?: boolean
|
|
41
|
-
model?: string
|
|
42
41
|
temperature?: number
|
|
43
42
|
maxTokens?: number
|
|
44
43
|
}
|
|
@@ -70,7 +69,6 @@ export function resolveModeSettings(ref: AssistantModeRef, t: Translate): ModeSe
|
|
|
70
69
|
/* A mode that takes attachments can open the files sent in earlier turns (design/ai/shared-dock.md §9-4). */
|
|
71
70
|
toolCategories: modeToolCategories(mode),
|
|
72
71
|
requireToolOnFirstTurn: mode.requireToolOnFirstTurn,
|
|
73
|
-
model: mode.model,
|
|
74
72
|
temperature: mode.temperature,
|
|
75
73
|
maxTokens: mode.maxTokens
|
|
76
74
|
}
|
|
@@ -62,8 +62,7 @@ export interface AssistantModeSpec {
|
|
|
62
62
|
toolCategories?: string[]
|
|
63
63
|
/** Make the model read before it answers — for surfaces whose subject is live state. */
|
|
64
64
|
requireToolOnFirstTurn?: boolean
|
|
65
|
-
/**
|
|
66
|
-
model?: string
|
|
65
|
+
/** Limits are policy, not a request field. The model is the domain's AI agent's (design/ai/agent-settings.md). */
|
|
67
66
|
temperature?: number
|
|
68
67
|
maxTokens?: number
|
|
69
68
|
/** What `/` offers. Declared as keys; the server translates. */
|
|
@@ -206,6 +206,11 @@ export class ChatSession {
|
|
|
206
206
|
@Field({ nullable: true })
|
|
207
207
|
aiClientId?: string
|
|
208
208
|
|
|
209
|
+
/** The agent that answered the last turn. Every turn's agent is in the AI usage rows. */
|
|
210
|
+
@Column({ nullable: true })
|
|
211
|
+
@Field({ nullable: true, description: 'The AI agent that answered the last turn.' })
|
|
212
|
+
agentId?: string
|
|
213
|
+
|
|
209
214
|
@CreateDateColumn()
|
|
210
215
|
@Field({ nullable: true })
|
|
211
216
|
createdAt?: Date
|
package/server/service/index.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { ChatSessionResolver } from './chat-session-resolver.js'
|
|
|
2
2
|
import { AssistantChatResolver } from './assistant-chat-resolver.js'
|
|
3
3
|
import { AssistantModeResolver } from './assistant-mode/assistant-mode-resolver.js'
|
|
4
4
|
import { AssistantCapabilityResolver } from './assistant-capability-resolver.js'
|
|
5
|
+
import { AiAgentResolver } from './ai-agent-resolver.js'
|
|
5
6
|
import { MySessionActivitySubscription } from './chat-session/session-activity-subscription.js'
|
|
6
7
|
import { ChatMessageSubscription } from './chat-message/chat-message-subscription.js'
|
|
7
8
|
import { entities as ChatSessionEntities } from './chat-session/index.js'
|
|
@@ -72,6 +73,8 @@ export const schema = {
|
|
|
72
73
|
AssistantModeResolver,
|
|
73
74
|
/* 「이 배포에 AI 가 있나 · 무엇이 선언 없이 등록됐나」 — 창구 하나(표준 A7-a). */
|
|
74
75
|
AssistantCapabilityResolver,
|
|
76
|
+
/* A domain's AI agents - the settings card and the dock's picker (design/ai/agent-settings.md). */
|
|
77
|
+
AiAgentResolver,
|
|
75
78
|
ChatMessageSubscription,
|
|
76
79
|
/* 내가 참여한 모든 세션의 활동 — 창을 닫아 둔 표면이 배지·목록을 갱신할 수 있게. */
|
|
77
80
|
MySessionActivitySubscription,
|
|
@@ -48,7 +48,6 @@ export async function runRegisteredMode(
|
|
|
48
48
|
systemPrompt: settings.systemPrompt,
|
|
49
49
|
toolCategories: settings.toolCategories,
|
|
50
50
|
requireToolOnFirstTurn: (requiredToolOnFirstTurn = Boolean(settings.requireToolOnFirstTurn)),
|
|
51
|
-
model: settings.model,
|
|
52
51
|
temperature: settings.temperature,
|
|
53
52
|
maxTokens: settings.maxTokens,
|
|
54
53
|
hostContext: options.hostContext,
|
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
*/
|
|
10
10
|
jest.mock('@things-factory/env', () => ({ logger: { warn: jest.fn() } }))
|
|
11
11
|
jest.mock('@things-factory/auth-base', () => ({ privilegeOfField: () => ({ known: false }) }))
|
|
12
|
+
/* ai-client-base keeps the AI usage table since 2026-10-12, so its root brings shell too - the same stand-in. */
|
|
13
|
+
jest.mock('@things-factory/ai-client-base', () => ({ getToolRegistryWarnings: () => [] }))
|
|
12
14
|
|
|
13
15
|
import { formatToolRegistryWarnings } from './tool-registry-boot-report'
|
|
14
16
|
|