@multiplayer-app/ai-agent-node 0.0.1
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/.env.example +45 -0
- package/README.md +611 -0
- package/config.example.json +73 -0
- package/dist/config.d.ts +35 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +44 -0
- package/dist/config.js.map +1 -0
- package/dist/helpers/AIHelper.d.ts +23 -0
- package/dist/helpers/AIHelper.d.ts.map +1 -0
- package/dist/helpers/AIHelper.js +326 -0
- package/dist/helpers/AIHelper.js.map +1 -0
- package/dist/helpers/AIHelper.test.d.ts +2 -0
- package/dist/helpers/AIHelper.test.d.ts.map +1 -0
- package/dist/helpers/AIHelper.test.js +332 -0
- package/dist/helpers/AIHelper.test.js.map +1 -0
- package/dist/helpers/ConfigHelper.d.ts +20 -0
- package/dist/helpers/ConfigHelper.d.ts.map +1 -0
- package/dist/helpers/ConfigHelper.js +118 -0
- package/dist/helpers/ConfigHelper.js.map +1 -0
- package/dist/helpers/ContextLimiter.d.ts +82 -0
- package/dist/helpers/ContextLimiter.d.ts.map +1 -0
- package/dist/helpers/ContextLimiter.js +165 -0
- package/dist/helpers/ContextLimiter.js.map +1 -0
- package/dist/helpers/FileHelper.d.ts +31 -0
- package/dist/helpers/FileHelper.d.ts.map +1 -0
- package/dist/helpers/FileHelper.js +175 -0
- package/dist/helpers/FileHelper.js.map +1 -0
- package/dist/helpers/SetupHelper.d.ts +5 -0
- package/dist/helpers/SetupHelper.d.ts.map +1 -0
- package/dist/helpers/SetupHelper.js +32 -0
- package/dist/helpers/SetupHelper.js.map +1 -0
- package/dist/helpers/index.d.ts +6 -0
- package/dist/helpers/index.d.ts.map +1 -0
- package/dist/helpers/index.js +6 -0
- package/dist/helpers/index.js.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +17 -0
- package/dist/index.js.map +1 -0
- package/dist/libs/index.d.ts +4 -0
- package/dist/libs/index.d.ts.map +1 -0
- package/dist/libs/index.js +4 -0
- package/dist/libs/index.js.map +1 -0
- package/dist/libs/kafka/config.d.ts +5 -0
- package/dist/libs/kafka/config.d.ts.map +1 -0
- package/dist/libs/kafka/config.js +5 -0
- package/dist/libs/kafka/config.js.map +1 -0
- package/dist/libs/kafka/consumer.d.ts +16 -0
- package/dist/libs/kafka/consumer.d.ts.map +1 -0
- package/dist/libs/kafka/consumer.js +126 -0
- package/dist/libs/kafka/consumer.js.map +1 -0
- package/dist/libs/kafka/index.d.ts +3 -0
- package/dist/libs/kafka/index.d.ts.map +1 -0
- package/dist/libs/kafka/index.js +3 -0
- package/dist/libs/kafka/index.js.map +1 -0
- package/dist/libs/kafka/kafka.d.ts +3 -0
- package/dist/libs/kafka/kafka.d.ts.map +1 -0
- package/dist/libs/kafka/kafka.js +24 -0
- package/dist/libs/kafka/kafka.js.map +1 -0
- package/dist/libs/kafka/producer.d.ts +11 -0
- package/dist/libs/kafka/producer.d.ts.map +1 -0
- package/dist/libs/kafka/producer.js +44 -0
- package/dist/libs/kafka/producer.js.map +1 -0
- package/dist/libs/logger/config.d.ts +5 -0
- package/dist/libs/logger/config.d.ts.map +1 -0
- package/dist/libs/logger/config.js +6 -0
- package/dist/libs/logger/config.js.map +1 -0
- package/dist/libs/logger/index.d.ts +10 -0
- package/dist/libs/logger/index.d.ts.map +1 -0
- package/dist/libs/logger/index.js +20 -0
- package/dist/libs/logger/index.js.map +1 -0
- package/dist/libs/logger/kafkajs-logger-creator.d.ts +12 -0
- package/dist/libs/logger/kafkajs-logger-creator.d.ts.map +1 -0
- package/dist/libs/logger/kafkajs-logger-creator.js +29 -0
- package/dist/libs/logger/kafkajs-logger-creator.js.map +1 -0
- package/dist/libs/logger/logger.d.ts +42 -0
- package/dist/libs/logger/logger.d.ts.map +1 -0
- package/dist/libs/logger/logger.js +44 -0
- package/dist/libs/logger/logger.js.map +1 -0
- package/dist/libs/s3/config.d.ts +7 -0
- package/dist/libs/s3/config.d.ts.map +1 -0
- package/dist/libs/s3/config.js +7 -0
- package/dist/libs/s3/config.js.map +1 -0
- package/dist/libs/s3/index.d.ts +4 -0
- package/dist/libs/s3/index.d.ts.map +1 -0
- package/dist/libs/s3/index.js +4 -0
- package/dist/libs/s3/index.js.map +1 -0
- package/dist/libs/s3/s3.lib.d.ts +25 -0
- package/dist/libs/s3/s3.lib.d.ts.map +1 -0
- package/dist/libs/s3/s3.lib.js +202 -0
- package/dist/libs/s3/s3.lib.js.map +1 -0
- package/dist/processors/ChatProcessor.d.ts +66 -0
- package/dist/processors/ChatProcessor.d.ts.map +1 -0
- package/dist/processors/ChatProcessor.js +610 -0
- package/dist/processors/ChatProcessor.js.map +1 -0
- package/dist/processors/ModelsProcessor.d.ts +11 -0
- package/dist/processors/ModelsProcessor.d.ts.map +1 -0
- package/dist/processors/ModelsProcessor.js +30 -0
- package/dist/processors/ModelsProcessor.js.map +1 -0
- package/dist/processors/index.d.ts +3 -0
- package/dist/processors/index.d.ts.map +1 -0
- package/dist/processors/index.js +3 -0
- package/dist/processors/index.js.map +1 -0
- package/dist/services/AIService.d.ts +48 -0
- package/dist/services/AIService.d.ts.map +1 -0
- package/dist/services/AIService.js +196 -0
- package/dist/services/AIService.js.map +1 -0
- package/dist/services/InternalEventsHandler.d.ts +21 -0
- package/dist/services/InternalEventsHandler.d.ts.map +1 -0
- package/dist/services/InternalEventsHandler.js +56 -0
- package/dist/services/InternalEventsHandler.js.map +1 -0
- package/dist/services/KafkaService.d.ts +35 -0
- package/dist/services/KafkaService.d.ts.map +1 -0
- package/dist/services/KafkaService.js +120 -0
- package/dist/services/KafkaService.js.map +1 -0
- package/dist/services/ModelFetcher.d.ts +54 -0
- package/dist/services/ModelFetcher.d.ts.map +1 -0
- package/dist/services/ModelFetcher.js +247 -0
- package/dist/services/ModelFetcher.js.map +1 -0
- package/dist/services/RedisService.d.ts +90 -0
- package/dist/services/RedisService.d.ts.map +1 -0
- package/dist/services/RedisService.js +236 -0
- package/dist/services/RedisService.js.map +1 -0
- package/dist/services/SocketService.d.ts +39 -0
- package/dist/services/SocketService.d.ts.map +1 -0
- package/dist/services/SocketService.js +128 -0
- package/dist/services/SocketService.js.map +1 -0
- package/dist/services/index.d.ts +7 -0
- package/dist/services/index.d.ts.map +1 -0
- package/dist/services/index.js +7 -0
- package/dist/services/index.js.map +1 -0
- package/dist/store/AgentStore.d.ts +48 -0
- package/dist/store/AgentStore.d.ts.map +1 -0
- package/dist/store/AgentStore.js +98 -0
- package/dist/store/AgentStore.js.map +1 -0
- package/dist/store/ArtifactStore.d.ts +13 -0
- package/dist/store/ArtifactStore.d.ts.map +1 -0
- package/dist/store/ArtifactStore.js +27 -0
- package/dist/store/ArtifactStore.js.map +1 -0
- package/dist/store/ConfigStore.d.ts +89 -0
- package/dist/store/ConfigStore.d.ts.map +1 -0
- package/dist/store/ConfigStore.js +214 -0
- package/dist/store/ConfigStore.js.map +1 -0
- package/dist/store/ConfigStore.test.d.ts +2 -0
- package/dist/store/ConfigStore.test.d.ts.map +1 -0
- package/dist/store/ConfigStore.test.js +259 -0
- package/dist/store/ConfigStore.test.js.map +1 -0
- package/dist/store/ModelStore.d.ts +44 -0
- package/dist/store/ModelStore.d.ts.map +1 -0
- package/dist/store/ModelStore.js +81 -0
- package/dist/store/ModelStore.js.map +1 -0
- package/dist/store/ModelStore.test.d.ts +2 -0
- package/dist/store/ModelStore.test.d.ts.map +1 -0
- package/dist/store/ModelStore.test.js +390 -0
- package/dist/store/ModelStore.test.js.map +1 -0
- package/dist/store/index.d.ts +5 -0
- package/dist/store/index.d.ts.map +1 -0
- package/dist/store/index.js +5 -0
- package/dist/store/index.js.map +1 -0
- package/dist/tools/generateChartTool.d.ts +24 -0
- package/dist/tools/generateChartTool.d.ts.map +1 -0
- package/dist/tools/generateChartTool.js +124 -0
- package/dist/tools/generateChartTool.js.map +1 -0
- package/dist/tools/proposeFormValuesTool.d.ts +35 -0
- package/dist/tools/proposeFormValuesTool.d.ts.map +1 -0
- package/dist/tools/proposeFormValuesTool.js +56 -0
- package/dist/tools/proposeFormValuesTool.js.map +1 -0
- package/package.json +71 -0
- package/src/config.ts +46 -0
- package/src/helpers/AIHelper.test.ts +375 -0
- package/src/helpers/AIHelper.ts +353 -0
- package/src/helpers/ConfigHelper.ts +130 -0
- package/src/helpers/ContextLimiter.ts +228 -0
- package/src/helpers/FileHelper.ts +197 -0
- package/src/helpers/SetupHelper.ts +35 -0
- package/src/helpers/index.ts +5 -0
- package/src/index.ts +18 -0
- package/src/libs/index.ts +3 -0
- package/src/libs/kafka/config.ts +4 -0
- package/src/libs/kafka/consumer.ts +161 -0
- package/src/libs/kafka/index.ts +2 -0
- package/src/libs/kafka/kafka.ts +27 -0
- package/src/libs/kafka/producer.ts +48 -0
- package/src/libs/logger/config.ts +4 -0
- package/src/libs/logger/index.ts +21 -0
- package/src/libs/logger/kafkajs-logger-creator.ts +28 -0
- package/src/libs/logger/logger.ts +60 -0
- package/src/libs/s3/config.ts +7 -0
- package/src/libs/s3/index.ts +3 -0
- package/src/libs/s3/s3.lib.ts +284 -0
- package/src/processors/ChatProcessor.ts +713 -0
- package/src/processors/ModelsProcessor.ts +34 -0
- package/src/processors/index.ts +2 -0
- package/src/services/AIService.ts +241 -0
- package/src/services/InternalEventsHandler.ts +61 -0
- package/src/services/KafkaService.ts +142 -0
- package/src/services/ModelFetcher.ts +286 -0
- package/src/services/RedisService.ts +285 -0
- package/src/services/SocketService.ts +153 -0
- package/src/services/index.ts +6 -0
- package/src/store/AgentStore.ts +138 -0
- package/src/store/ArtifactStore.ts +29 -0
- package/src/store/ConfigStore.test.ts +314 -0
- package/src/store/ConfigStore.ts +239 -0
- package/src/store/ModelStore.test.ts +473 -0
- package/src/store/ModelStore.ts +93 -0
- package/src/store/index.ts +4 -0
- package/src/tools/generateChartTool.ts +131 -0
- package/src/tools/proposeFormValuesTool.ts +67 -0
- package/tsconfig.json +24 -0
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
import { AdditionalContext, AgentAttachmentType, AgentConfig, AgentMessage, MessageRole } from "@multiplayer-app/ai-agent-types";
|
|
2
|
+
import {ModelMessage, ToolModelMessage, ToolResultPart, streamText, UserContent} from "ai";
|
|
3
|
+
import { AIService, GenerateTextOptions, StreamTextOptions } from "../services";
|
|
4
|
+
import { ModelStore } from "../store";
|
|
5
|
+
import { logger } from "../libs/logger";
|
|
6
|
+
import { FilePart, ImagePart, ReasoningPart, TextPart, ToolCallPart} from "@ai-sdk/provider-utils";
|
|
7
|
+
import { ConfigStore } from "../store";
|
|
8
|
+
import { isImage, prepareImageForVisionAPI, extractTextFromDocument } from "./FileHelper";
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
export class AIHelper {
|
|
12
|
+
private static async convertUserMessage(msg: Pick<AgentMessage, 'attachments' | 'content'>, ignoreFileAttachments: boolean): Promise<Array<TextPart | ImagePart | FilePart>> {
|
|
13
|
+
const userContent: UserContent = [{
|
|
14
|
+
type: "text",
|
|
15
|
+
text: msg.content
|
|
16
|
+
}];
|
|
17
|
+
const atts = msg.attachments ?? [];
|
|
18
|
+
if (!atts.length) {
|
|
19
|
+
return userContent;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const lines: string[] = [];
|
|
23
|
+
|
|
24
|
+
for (const att of atts) {
|
|
25
|
+
if (att.type !== AgentAttachmentType.Context) {
|
|
26
|
+
if (ignoreFileAttachments) {
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
if (isImage(att)) {
|
|
30
|
+
const image = await prepareImageForVisionAPI(att)
|
|
31
|
+
if(image) {
|
|
32
|
+
userContent.push({
|
|
33
|
+
type: "image",
|
|
34
|
+
image: image.image_url.url,
|
|
35
|
+
mediaType: att.mimeType as string,
|
|
36
|
+
})
|
|
37
|
+
}
|
|
38
|
+
} else {
|
|
39
|
+
const fileText = await extractTextFromDocument(att)
|
|
40
|
+
if(fileText) {
|
|
41
|
+
userContent.push({
|
|
42
|
+
type: "text",
|
|
43
|
+
text: `${att.type}: ${att.name}(${att.mimeType})\n ${fileText}`
|
|
44
|
+
})
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
}
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const md = att.metadata as any;
|
|
52
|
+
const kind = typeof md?.kind === 'string' ? md.kind : 'unknown';
|
|
53
|
+
lines.push(`- context:${kind}: ${att.name}`);
|
|
54
|
+
|
|
55
|
+
if (kind === 'webSnippet') {
|
|
56
|
+
const url = typeof md?.source?.url === 'string' ? md.source.url : att.url;
|
|
57
|
+
const title = typeof md?.title === 'string' ? md.title : undefined;
|
|
58
|
+
const selectedText = typeof md?.selectedText === 'string' ? md.selectedText : '';
|
|
59
|
+
if (url) lines.push(` url: ${url}`);
|
|
60
|
+
if (title) lines.push(` title: ${title}`);
|
|
61
|
+
if (selectedText) {
|
|
62
|
+
const clipped = selectedText.length > 4000 ? `${selectedText.slice(0, 4000)}…` : selectedText;
|
|
63
|
+
lines.push(' BEGIN_UNTRUSTED_WEB_SNIPPET');
|
|
64
|
+
lines.push(clipped);
|
|
65
|
+
lines.push(' END_UNTRUSTED_WEB_SNIPPET');
|
|
66
|
+
}
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (kind === 'formField') {
|
|
71
|
+
const formId = typeof md?.formId === 'string' ? md.formId : undefined;
|
|
72
|
+
const fieldName = typeof md?.fieldName === 'string' ? md.fieldName : undefined;
|
|
73
|
+
const fieldLabel = typeof md?.fieldLabel === 'string' ? md.fieldLabel : undefined;
|
|
74
|
+
const value = typeof md?.value === 'string' ? md.value : '';
|
|
75
|
+
const inputType = typeof md?.inputType === 'string' ? md.inputType : undefined;
|
|
76
|
+
const options = Array.isArray(md?.options) ? md.options : [];
|
|
77
|
+
if (formId) lines.push(` formId: ${formId}`);
|
|
78
|
+
if (fieldName) lines.push(` field: ${fieldLabel ? `${fieldLabel} (${fieldName})` : fieldName}`);
|
|
79
|
+
if (inputType) lines.push(` inputType: ${inputType}`);
|
|
80
|
+
if ((inputType === 'select' || inputType === 'radio') && options.length) {
|
|
81
|
+
const rendered = options
|
|
82
|
+
.slice(0, 30)
|
|
83
|
+
.map((o: any) => {
|
|
84
|
+
const v = typeof o?.value === 'string' ? o.value : '';
|
|
85
|
+
const l = typeof o?.label === 'string' ? o.label : '';
|
|
86
|
+
if (!v && !l) return '';
|
|
87
|
+
return l && v ? `${l} (${v})` : (l || v);
|
|
88
|
+
})
|
|
89
|
+
.filter(Boolean)
|
|
90
|
+
.join(', ');
|
|
91
|
+
if (rendered) lines.push(` options: ${rendered}${options.length > 30 ? '…' : ''}`);
|
|
92
|
+
}
|
|
93
|
+
if (value) lines.push(` value: ${value.length > 1000 ? `${value.slice(0, 1000)}…` : value}`);
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (kind === 'formSnapshot') {
|
|
98
|
+
const formId = typeof md?.formId === 'string' ? md.formId : undefined;
|
|
99
|
+
const formName = typeof md?.formName === 'string' ? md.formName : undefined;
|
|
100
|
+
const fields = Array.isArray(md?.fields) ? md.fields : [];
|
|
101
|
+
if (formId) lines.push(` formId: ${formId}`);
|
|
102
|
+
if (formName) lines.push(` formName: ${formName}`);
|
|
103
|
+
lines.push(' fields:');
|
|
104
|
+
for (const f of fields.slice(0, 50)) {
|
|
105
|
+
const name = typeof f?.name === 'string' ? f.name : 'unknown';
|
|
106
|
+
const label = typeof f?.label === 'string' ? f.label : undefined;
|
|
107
|
+
const value = typeof f?.value === 'string' ? f.value : '';
|
|
108
|
+
const inputType = typeof f?.inputType === 'string' ? f.inputType : undefined;
|
|
109
|
+
const options = Array.isArray(f?.options) ? f.options : [];
|
|
110
|
+
const clipped = value.length > 1000 ? `${value.slice(0, 1000)}…` : value;
|
|
111
|
+
const meta: string[] = [];
|
|
112
|
+
if (inputType) meta.push(`type=${inputType}`);
|
|
113
|
+
if ((inputType === 'select' || inputType === 'radio') && options.length) {
|
|
114
|
+
const opts = options
|
|
115
|
+
.slice(0, 20)
|
|
116
|
+
.map((o: any) => typeof o?.value === 'string' ? o.value : '')
|
|
117
|
+
.filter(Boolean)
|
|
118
|
+
.join('|');
|
|
119
|
+
if (opts) meta.push(`options=${opts}${options.length > 20 ? '|…' : ''}`);
|
|
120
|
+
}
|
|
121
|
+
lines.push(` - ${label ? `${label} (${name})` : name}${meta.length ? ` [${meta.join(', ')}]` : ''}: ${clipped}`);
|
|
122
|
+
}
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Custom kind: prefer summary, fall back to minimal JSON.
|
|
127
|
+
const title = typeof md?.title === 'string' ? md.title : undefined;
|
|
128
|
+
const summary = typeof md?.summary === 'string' ? md.summary : undefined;
|
|
129
|
+
if (title) lines.push(` title: ${title}`);
|
|
130
|
+
|
|
131
|
+
if (summary) {
|
|
132
|
+
lines.push(` summary: ${summary.length > 4000 ? `${summary.slice(0, 4000)}…` : summary}`);
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if (md?.data !== undefined) {
|
|
136
|
+
const json = JSON.stringify(md.data);
|
|
137
|
+
lines.push(` data: ${json.length > 2000 ? `${json.slice(0, 2000)}…` : json}`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (lines.length) {
|
|
142
|
+
userContent.push({
|
|
143
|
+
type: "text",
|
|
144
|
+
text: `Context attachments (non-authoritative; user message is primary):\n ${lines.join('\n')}`
|
|
145
|
+
})
|
|
146
|
+
}
|
|
147
|
+
return userContent;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
private static getAgentConfigs(contextKey: string): AgentConfig[] {
|
|
151
|
+
const agents = ConfigStore.getInstance().getAgentsForContext(contextKey);
|
|
152
|
+
if (!agents.length) {
|
|
153
|
+
throw new Error(`Agent configs not found for context key: ${contextKey}`);
|
|
154
|
+
}
|
|
155
|
+
return agents;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
static async getAgentOptions(params: {
|
|
159
|
+
contextKey?: string,
|
|
160
|
+
messages?: ModelMessage[],
|
|
161
|
+
modelId?: string,
|
|
162
|
+
context?: AdditionalContext,
|
|
163
|
+
agentName?: string,
|
|
164
|
+
}, options: Omit<GenerateTextOptions, 'messages' | 'system'> = {}): Promise<Omit<GenerateTextOptions, 'messages'> & {name: string}> {
|
|
165
|
+
const { contextKey, messages, modelId, context, agentName } = params;
|
|
166
|
+
let agentConfig = ConfigStore.getInstance().getAgentConfigByName(agentName);
|
|
167
|
+
if (contextKey && messages) {
|
|
168
|
+
agentConfig = await AIHelper.selectAgentConfig(contextKey, messages);
|
|
169
|
+
}
|
|
170
|
+
const aiService = new AIService(ModelStore.getInstance());
|
|
171
|
+
const agentOptions = aiService.getAgentOptions(agentConfig, modelId, context);
|
|
172
|
+
return {
|
|
173
|
+
...agentOptions,
|
|
174
|
+
...options,
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
private static async selectAgentConfig(contextKey: string, messages: ModelMessage[]): Promise<AgentConfig> {
|
|
179
|
+
const configs = AIHelper.getAgentConfigs(contextKey)
|
|
180
|
+
if (configs.length === 1) {
|
|
181
|
+
return configs[0];
|
|
182
|
+
}
|
|
183
|
+
const aiService = new AIService(ModelStore.getInstance());
|
|
184
|
+
const systemPrompt = `Select the most appropriate agent for the given user prompt.
|
|
185
|
+
Return only the agent name, no quotes or extra text.
|
|
186
|
+
Available agents: ${
|
|
187
|
+
JSON.stringify(configs.map(config => ({
|
|
188
|
+
name: config.name,
|
|
189
|
+
description: config.description,
|
|
190
|
+
tools: config.tools.map(({data}) => data.title) })))
|
|
191
|
+
}`;
|
|
192
|
+
const agentName = await aiService.generateText({
|
|
193
|
+
messages,
|
|
194
|
+
system: systemPrompt,
|
|
195
|
+
temperature: 0.7,
|
|
196
|
+
maxOutputTokens: 2000
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
const agent = configs.find(config => config.name === agentName) as AgentConfig;
|
|
200
|
+
if (!agent) {
|
|
201
|
+
// fallback, agent not found, grabbing first from the list
|
|
202
|
+
return configs[0];
|
|
203
|
+
}
|
|
204
|
+
return agent;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
static async getAssistantResponse(messages: Pick<AgentMessage, 'role' | 'content' | 'attachments'>[], signal?: AbortSignal, options: Omit<GenerateTextOptions, 'messages'> = {}): Promise<string> {
|
|
208
|
+
try {
|
|
209
|
+
const aiService = new AIService(ModelStore.getInstance());
|
|
210
|
+
const processedOptions = this.processToolChoice(options, messages);
|
|
211
|
+
|
|
212
|
+
return aiService.generateText({
|
|
213
|
+
temperature: 0.7,
|
|
214
|
+
maxOutputTokens: 2000,
|
|
215
|
+
...processedOptions,
|
|
216
|
+
messages: await AIHelper.convertMessages(messages),
|
|
217
|
+
signal,
|
|
218
|
+
});
|
|
219
|
+
} catch (error) {
|
|
220
|
+
logger.error('AI assistant response generation failed:', error);
|
|
221
|
+
throw error;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
private static processToolChoice(
|
|
226
|
+
options: Omit<StreamTextOptions, 'messages'> | undefined,
|
|
227
|
+
agentMessages: Pick<AgentMessage, 'role' | 'content' | 'toolCalls'>[]
|
|
228
|
+
): Omit<StreamTextOptions, 'messages'> {
|
|
229
|
+
if (!options?.toolChoice) {
|
|
230
|
+
return options || {};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Find the latest assistant message
|
|
234
|
+
const latestAssistantMessage = [...agentMessages]
|
|
235
|
+
.reverse()
|
|
236
|
+
.find(msg => msg.role === MessageRole.Assistant);
|
|
237
|
+
|
|
238
|
+
// Handle 'required' toolChoice: if any tool was called, set to 'auto'
|
|
239
|
+
if (options.toolChoice === 'required') {
|
|
240
|
+
const anyToolWasCalled = (latestAssistantMessage?.toolCalls?.length ?? 0) > 0;
|
|
241
|
+
if (anyToolWasCalled) {
|
|
242
|
+
return {
|
|
243
|
+
...options,
|
|
244
|
+
toolChoice: 'auto' as const
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
return options;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Handle specific tool toolChoice: if that specific tool was called, set to 'auto'
|
|
251
|
+
if (typeof options.toolChoice === 'object' && options.toolChoice.type === 'tool') {
|
|
252
|
+
const toolName = options.toolChoice.toolName;
|
|
253
|
+
const toolWasCalled = latestAssistantMessage?.toolCalls?.some(
|
|
254
|
+
toolCall => toolCall.name === toolName
|
|
255
|
+
) ?? false;
|
|
256
|
+
|
|
257
|
+
if (toolWasCalled) {
|
|
258
|
+
return {
|
|
259
|
+
...options,
|
|
260
|
+
toolChoice: 'auto' as const
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return options;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
static async streamAssistantResponse(
|
|
269
|
+
agentMessages: Pick<AgentMessage, 'role' | 'content' | 'toolCalls' | 'attachments'>[],
|
|
270
|
+
signal?: AbortSignal,
|
|
271
|
+
options?: Omit<StreamTextOptions, 'messages'>): Promise<ReturnType<typeof streamText>> {
|
|
272
|
+
const aiService = new AIService(ModelStore.getInstance());
|
|
273
|
+
const messages = await this.convertMessages(agentMessages)
|
|
274
|
+
const processedOptions = this.processToolChoice(options, agentMessages);
|
|
275
|
+
|
|
276
|
+
return aiService.streamText({
|
|
277
|
+
messages,
|
|
278
|
+
temperature: 0.7,
|
|
279
|
+
maxOutputTokens: 2000,
|
|
280
|
+
signal,
|
|
281
|
+
...processedOptions
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
static async convertMessages(agentMessages: Pick<AgentMessage, 'role' | 'content' | 'toolCalls' | 'attachments'>[], ignoreToolsAndAttachments = false): Promise<ModelMessage[]> {
|
|
286
|
+
const result: ModelMessage[] = [];
|
|
287
|
+
|
|
288
|
+
for (const msg of agentMessages) {
|
|
289
|
+
if (msg.role === MessageRole.User) {
|
|
290
|
+
result.push({
|
|
291
|
+
role: 'user',
|
|
292
|
+
content: await this.convertUserMessage(msg, ignoreToolsAndAttachments)
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
if (msg.role === MessageRole.Assistant) {
|
|
296
|
+
const assistantMessage = {
|
|
297
|
+
role: 'assistant' as const,
|
|
298
|
+
content: [] as Array<TextPart | FilePart | ReasoningPart | ToolCallPart | ToolResultPart>
|
|
299
|
+
}
|
|
300
|
+
if (msg.content.trim().length) {
|
|
301
|
+
assistantMessage.content.push({type: 'text', text: msg.content})
|
|
302
|
+
}
|
|
303
|
+
const toolMessage: ToolModelMessage = {
|
|
304
|
+
role: 'tool',
|
|
305
|
+
content: []
|
|
306
|
+
}
|
|
307
|
+
if(!ignoreToolsAndAttachments) {
|
|
308
|
+
msg.toolCalls?.forEach((toolCall) => {
|
|
309
|
+
if (toolCall.output) {
|
|
310
|
+
assistantMessage.content.push({
|
|
311
|
+
type: 'tool-call',
|
|
312
|
+
toolCallId: toolCall.id,
|
|
313
|
+
toolName: toolCall.name,
|
|
314
|
+
input: toolCall.input
|
|
315
|
+
})
|
|
316
|
+
toolMessage.content.push({
|
|
317
|
+
type: 'tool-result',
|
|
318
|
+
toolCallId: toolCall.id,
|
|
319
|
+
toolName: toolCall.name,
|
|
320
|
+
output: {
|
|
321
|
+
type: 'text',
|
|
322
|
+
value: JSON.stringify(toolCall.output)
|
|
323
|
+
}
|
|
324
|
+
})
|
|
325
|
+
}
|
|
326
|
+
})
|
|
327
|
+
}
|
|
328
|
+
if (assistantMessage.content.length) {
|
|
329
|
+
result.push(assistantMessage)
|
|
330
|
+
if (toolMessage.content.length) {
|
|
331
|
+
result.push(toolMessage)
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
return result;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
static async generateTitleForMessage(contextKey: string, messages: Pick<AgentMessage, 'role' | 'content'>[]): Promise<string> {
|
|
341
|
+
try {
|
|
342
|
+
const aiService = new AIService(ModelStore.getInstance());
|
|
343
|
+
const systemPrompt = `Generate descriptive title 5 words maximum for a chat conversation. Return only the title, no quotes or extra text. If user question is opaque, assume it is related to ${contextKey} topic.`;
|
|
344
|
+
return aiService.generateText({
|
|
345
|
+
messages: await AIHelper.convertMessages(messages, true),
|
|
346
|
+
system: systemPrompt,
|
|
347
|
+
});
|
|
348
|
+
} catch (error) {
|
|
349
|
+
logger.warn('AI title generation failed, falling back to simple title generation', { error });
|
|
350
|
+
return `${contextKey} session ${new Date().toISOString()}`;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { AgentAppConfig, AgentTool } from '@multiplayer-app/ai-agent-types';
|
|
3
|
+
import { AgentToolType } from '@multiplayer-app/ai-agent-types';
|
|
4
|
+
import { logger } from '../libs/logger';
|
|
5
|
+
|
|
6
|
+
export class ConfigHelper {
|
|
7
|
+
|
|
8
|
+
static loadConfig(rawConfig: Record<string, any>): AgentAppConfig {
|
|
9
|
+
try {
|
|
10
|
+
// Convert to AgentAppConfig format
|
|
11
|
+
return ConfigHelper.convertToAgentAppConfig(rawConfig);
|
|
12
|
+
} catch (error) {
|
|
13
|
+
if (error instanceof SyntaxError) {
|
|
14
|
+
logger.error(`Failed to parse config.json: ${error.message}`);
|
|
15
|
+
throw new Error(`Invalid JSON in config file: ${error.message}`);
|
|
16
|
+
}
|
|
17
|
+
logger.error(`Failed to load config: ${error}`);
|
|
18
|
+
throw error;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
private static jsonSchemaToZod (schema: any): z.ZodSchema<any> | null {
|
|
23
|
+
if (!schema) return null;
|
|
24
|
+
return z.fromJSONSchema(schema);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Converts raw JSON config to AgentAppConfig format
|
|
29
|
+
* Validates structure and converts JSON schemas to Zod schemas
|
|
30
|
+
*/
|
|
31
|
+
private static convertToAgentAppConfig(rawConfig: any): AgentAppConfig {
|
|
32
|
+
// Convert agents, transforming JSON schemas to Zod schemas
|
|
33
|
+
const config: AgentAppConfig = rawConfig.agents.reduce((config: AgentAppConfig, agent: any) => {
|
|
34
|
+
agent.contextKeys.forEach((contextKey: string) => {
|
|
35
|
+
if (!config[contextKey]) {
|
|
36
|
+
config[contextKey] = [];
|
|
37
|
+
}
|
|
38
|
+
config[contextKey].push({
|
|
39
|
+
name: agent.name,
|
|
40
|
+
description: agent.description,
|
|
41
|
+
defaultModel: agent.defaultModel,
|
|
42
|
+
systemPrompt: agent.systemPrompt,
|
|
43
|
+
temperature: agent.temperature || 0.1,
|
|
44
|
+
maxTokens: agent.maxTokens || 2000,
|
|
45
|
+
tools: agent.tools.map((tool: any) => ConfigHelper.convertTool(tool)),
|
|
46
|
+
outputSchema: agent.outputSchema ? ConfigHelper.jsonSchemaToZod(agent.outputSchema) as any : undefined
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
return config;
|
|
50
|
+
}, {} as AgentAppConfig);
|
|
51
|
+
|
|
52
|
+
return config
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Converts a tool from JSON format (with JSON schemas) to AgentTool format (with Zod schemas)
|
|
57
|
+
*/
|
|
58
|
+
private static convertTool(tool: any): AgentTool {
|
|
59
|
+
if (tool.type === AgentToolType.WEB_SEARCH) {
|
|
60
|
+
return {
|
|
61
|
+
type: AgentToolType.WEB_SEARCH,
|
|
62
|
+
data: {
|
|
63
|
+
title: AgentToolType.WEB_SEARCH,
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (tool.type === AgentToolType.API_TOOL) {
|
|
69
|
+
return {
|
|
70
|
+
type: AgentToolType.API_TOOL,
|
|
71
|
+
data: {
|
|
72
|
+
title: tool.data.title,
|
|
73
|
+
description: tool.data.description,
|
|
74
|
+
headersToPass: tool.data.headersToPass,
|
|
75
|
+
method: tool.data.method,
|
|
76
|
+
url: tool.data.url,
|
|
77
|
+
body: tool.data.body ? ConfigHelper.jsonSchemaToZod(tool.data.body) as any : undefined,
|
|
78
|
+
queryParams: tool.data.queryParams ? ConfigHelper.jsonSchemaToZod(tool.data.queryParams) as any : undefined,
|
|
79
|
+
needsApproval: tool.data.needsApproval ?? false,
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
throw new Error(`Unknown tool type: ${tool.type}`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Validates the AgentAppConfig structure
|
|
90
|
+
* @throws Error if validation fails
|
|
91
|
+
*/
|
|
92
|
+
private static validateConfig(config: AgentAppConfig): void {
|
|
93
|
+
if (!config.agents || !Array.isArray(config.agents)) {
|
|
94
|
+
throw new Error('Config must have an "agents" array');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (!config.contexts || typeof config.contexts !== 'object') {
|
|
98
|
+
throw new Error('Config must have a "contexts" object');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Validate agents
|
|
102
|
+
const agentNames = new Set<string>();
|
|
103
|
+
for (const agent of config.agents) {
|
|
104
|
+
if (!agent.name) {
|
|
105
|
+
throw new Error('All agents must have a "name" field');
|
|
106
|
+
}
|
|
107
|
+
if (agentNames.has(agent.name)) {
|
|
108
|
+
throw new Error(`Duplicate agent name found: ${agent.name}`);
|
|
109
|
+
}
|
|
110
|
+
agentNames.add(agent.name);
|
|
111
|
+
|
|
112
|
+
if (!agent.systemPrompt) {
|
|
113
|
+
throw new Error(`Agent "${agent.name}" must have a "systemPrompt" field`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Validate contexts reference valid agents
|
|
118
|
+
for (const [contextKey, contextAgentNames] of Object.entries(config.contexts)) {
|
|
119
|
+
if (!Array.isArray(contextAgentNames)) {
|
|
120
|
+
throw new Error(`Context "${contextKey}" must have an array of agent names`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
for (const agentName of contextAgentNames) {
|
|
124
|
+
if (!agentNames.has(agentName)) {
|
|
125
|
+
throw new Error(`Context "${contextKey}" references unknown agent: ${agentName}`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|