@drax/ai-back 3.41.0 → 3.42.0
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/agents/DraxAgent.js +1 -1
- package/dist/config/ElevenLabsTTSConfig.js +10 -0
- package/dist/controllers/AICrudController.js +1 -1
- package/dist/controllers/AIGenericController.js +1 -1
- package/dist/controllers/TTSGenericController.js +61 -0
- package/dist/factory/ElevenLabsTTSProviderFactory.js +13 -0
- package/dist/factory/TTSProviderFactory.js +27 -0
- package/dist/factory/ai/AiProviderFactory.js +30 -0
- package/dist/factory/ai/DeepSeekAiProviderFactory.js +14 -0
- package/dist/factory/ai/GoogleAiProviderFactory.js +14 -0
- package/dist/factory/ai/OllamaAiProviderFactory.js +14 -0
- package/dist/factory/ai/OpenAiProviderFactory.js +14 -0
- package/dist/factory/tts/ElevenLabsTTSProviderFactory.js +13 -0
- package/dist/factory/tts/TTSProviderFactory.js +27 -0
- package/dist/index.js +22 -13
- package/dist/interfaces/ITTSProvider.js +1 -0
- package/dist/permissions/TTSPermissions.js +6 -0
- package/dist/providers/ElevenLabsTTSProvider.js +108 -0
- package/dist/providers/ai/DeepSeekAiProvider.js +34 -0
- package/dist/providers/ai/GoogleAiProvider.js +367 -0
- package/dist/providers/ai/OllamaAiProvider.js +342 -0
- package/dist/providers/ai/OpenAiProvider.js +302 -0
- package/dist/providers/tts/ElevenLabsTTSProvider.js +108 -0
- package/dist/routes/TTSRoutes.js +8 -0
- package/dist/schemas/TTSRequestSchema.js +24 -0
- package/dist/services/TTSGenericService.js +21 -0
- package/package.json +2 -2
- package/src/agents/DraxAgent.ts +1 -1
- package/src/config/ElevenLabsTTSConfig.ts +13 -0
- package/src/controllers/AICrudController.ts +1 -1
- package/src/controllers/AIGenericController.ts +1 -1
- package/src/controllers/TTSGenericController.ts +70 -0
- package/src/factory/{AiProviderFactory.ts → ai/AiProviderFactory.ts} +3 -3
- package/src/factory/ai/DeepSeekAiProviderFactory.ts +27 -0
- package/src/factory/{GoogleAiProviderFactory.ts → ai/GoogleAiProviderFactory.ts} +4 -4
- package/src/factory/{OllamaAiProviderFactory.ts → ai/OllamaAiProviderFactory.ts} +4 -4
- package/src/factory/{OpenAiProviderFactory.ts → ai/OpenAiProviderFactory.ts} +4 -4
- package/src/factory/tts/ElevenLabsTTSProviderFactory.ts +26 -0
- package/src/factory/tts/TTSProviderFactory.ts +42 -0
- package/src/index.ts +52 -11
- package/src/interfaces/ITTSProvider.ts +47 -0
- package/src/permissions/AIPermissions.ts +0 -1
- package/src/permissions/TTSPermissions.ts +8 -0
- package/src/providers/{DeepSeekProvider.ts → ai/DeepSeekAiProvider.ts} +5 -5
- package/src/providers/{GoogleAiProvider.ts → ai/GoogleAiProvider.ts} +2 -2
- package/src/providers/{OllamaAiProvider.ts → ai/OllamaAiProvider.ts} +2 -2
- package/src/providers/{OpenAiProvider.ts → ai/OpenAiProvider.ts} +2 -2
- package/src/providers/tts/ElevenLabsTTSProvider.ts +132 -0
- package/src/routes/TTSRoutes.ts +13 -0
- package/src/schemas/TTSRequestSchema.ts +38 -0
- package/src/services/TTSGenericService.ts +41 -0
- package/test/DeepSeekProvider.test.ts +4 -4
- package/tsconfig.tsbuildinfo +1 -1
- package/types/config/ElevenLabsTTSConfig.d.ts +10 -0
- package/types/config/ElevenLabsTTSConfig.d.ts.map +1 -0
- package/types/controllers/TTSGenericController.d.ts +11 -0
- package/types/controllers/TTSGenericController.d.ts.map +1 -0
- package/types/factory/ElevenLabsTTSProviderFactory.d.ts +8 -0
- package/types/factory/ElevenLabsTTSProviderFactory.d.ts.map +1 -0
- package/types/factory/TTSProviderFactory.d.ts +15 -0
- package/types/factory/TTSProviderFactory.d.ts.map +1 -0
- package/types/factory/ai/AiProviderFactory.d.ts +8 -0
- package/types/factory/ai/AiProviderFactory.d.ts.map +1 -0
- package/types/factory/ai/DeepSeekAiProviderFactory.d.ts +8 -0
- package/types/factory/ai/DeepSeekAiProviderFactory.d.ts.map +1 -0
- package/types/factory/ai/GoogleAiProviderFactory.d.ts +8 -0
- package/types/factory/ai/GoogleAiProviderFactory.d.ts.map +1 -0
- package/types/factory/ai/OllamaAiProviderFactory.d.ts +8 -0
- package/types/factory/ai/OllamaAiProviderFactory.d.ts.map +1 -0
- package/types/factory/ai/OpenAiProviderFactory.d.ts +8 -0
- package/types/factory/ai/OpenAiProviderFactory.d.ts.map +1 -0
- package/types/factory/tts/ElevenLabsTTSProviderFactory.d.ts +8 -0
- package/types/factory/tts/ElevenLabsTTSProviderFactory.d.ts.map +1 -0
- package/types/factory/tts/TTSProviderFactory.d.ts +15 -0
- package/types/factory/tts/TTSProviderFactory.d.ts.map +1 -0
- package/types/index.d.ts +24 -11
- package/types/index.d.ts.map +1 -1
- package/types/interfaces/ITTSProvider.d.ts +39 -0
- package/types/interfaces/ITTSProvider.d.ts.map +1 -0
- package/types/permissions/TTSPermissions.d.ts +6 -0
- package/types/permissions/TTSPermissions.d.ts.map +1 -0
- package/types/providers/ElevenLabsTTSProvider.d.ts +38 -0
- package/types/providers/ElevenLabsTTSProvider.d.ts.map +1 -0
- package/types/providers/ai/DeepSeekAiProvider.d.ts +24 -0
- package/types/providers/ai/DeepSeekAiProvider.d.ts.map +1 -0
- package/types/providers/ai/GoogleAiProvider.d.ts +63 -0
- package/types/providers/ai/GoogleAiProvider.d.ts.map +1 -0
- package/types/providers/ai/OllamaAiProvider.d.ts +78 -0
- package/types/providers/ai/OllamaAiProvider.d.ts.map +1 -0
- package/types/providers/ai/OpenAiProvider.d.ts +97 -0
- package/types/providers/ai/OpenAiProvider.d.ts.map +1 -0
- package/types/providers/tts/ElevenLabsTTSProvider.d.ts +38 -0
- package/types/providers/tts/ElevenLabsTTSProvider.d.ts.map +1 -0
- package/types/routes/TTSRoutes.d.ts +4 -0
- package/types/routes/TTSRoutes.d.ts.map +1 -0
- package/types/schemas/TTSRequestSchema.d.ts +37 -0
- package/types/schemas/TTSRequestSchema.d.ts.map +1 -0
- package/types/services/TTSGenericService.d.ts +17 -0
- package/types/services/TTSGenericService.d.ts.map +1 -0
- package/src/factory/DeepSeekProviderFactory.ts +0 -27
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
import { GoogleGenAI } from "@google/genai";
|
|
2
|
+
import { toJSONSchema } from "zod";
|
|
3
|
+
class GoogleAiProvider {
|
|
4
|
+
constructor(apiKey, model, visionModel, aiLogService) {
|
|
5
|
+
if (!apiKey) {
|
|
6
|
+
throw new Error("Google AI apiKey required");
|
|
7
|
+
}
|
|
8
|
+
if (!model) {
|
|
9
|
+
throw new Error("Google AI model required");
|
|
10
|
+
}
|
|
11
|
+
this._apiKey = apiKey;
|
|
12
|
+
this._model = model;
|
|
13
|
+
this._visionModel = visionModel;
|
|
14
|
+
this._aiLogService = aiLogService;
|
|
15
|
+
}
|
|
16
|
+
get model() {
|
|
17
|
+
if (!this._model) {
|
|
18
|
+
throw new Error("Google AI model not found");
|
|
19
|
+
}
|
|
20
|
+
return this._model;
|
|
21
|
+
}
|
|
22
|
+
get client() {
|
|
23
|
+
if (!this._client) {
|
|
24
|
+
this._client = new GoogleGenAI({
|
|
25
|
+
apiKey: this._apiKey,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
return this._client;
|
|
29
|
+
}
|
|
30
|
+
get visionModel() {
|
|
31
|
+
return this._visionModel;
|
|
32
|
+
}
|
|
33
|
+
buildUserContent(input) {
|
|
34
|
+
if (input.userContent && input.userContent.length > 0) {
|
|
35
|
+
return this.mapContentParts(input.userContent);
|
|
36
|
+
}
|
|
37
|
+
if (input.userImages && input.userImages.length > 0) {
|
|
38
|
+
const content = [];
|
|
39
|
+
if (input.userInput) {
|
|
40
|
+
content.push({ text: input.userInput });
|
|
41
|
+
}
|
|
42
|
+
content.push(...input.userImages.map(image => this.mapImageUrl(image.url)));
|
|
43
|
+
return content;
|
|
44
|
+
}
|
|
45
|
+
return input.userInput ? [{ text: input.userInput }] : [{ text: "" }];
|
|
46
|
+
}
|
|
47
|
+
mapContentParts(content) {
|
|
48
|
+
return content.map(part => {
|
|
49
|
+
if (part.type === 'text') {
|
|
50
|
+
return {
|
|
51
|
+
text: part.text
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
return this.mapImageUrl(part.imageUrl);
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
mapImageUrl(url) {
|
|
58
|
+
const dataUrlMatch = url.match(/^data:([^;,]+);base64,(.+)$/);
|
|
59
|
+
if (dataUrlMatch) {
|
|
60
|
+
return {
|
|
61
|
+
inlineData: {
|
|
62
|
+
mimeType: dataUrlMatch[1],
|
|
63
|
+
data: dataUrlMatch[2],
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
fileData: {
|
|
69
|
+
fileUri: url,
|
|
70
|
+
mimeType: this.inferImageMimeType(url),
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
inferImageMimeType(url) {
|
|
75
|
+
const normalizedUrl = url.split("?")[0].toLowerCase();
|
|
76
|
+
if (normalizedUrl.endsWith(".png")) {
|
|
77
|
+
return "image/png";
|
|
78
|
+
}
|
|
79
|
+
if (normalizedUrl.endsWith(".webp")) {
|
|
80
|
+
return "image/webp";
|
|
81
|
+
}
|
|
82
|
+
if (normalizedUrl.endsWith(".gif")) {
|
|
83
|
+
return "image/gif";
|
|
84
|
+
}
|
|
85
|
+
if (normalizedUrl.endsWith(".bmp")) {
|
|
86
|
+
return "image/bmp";
|
|
87
|
+
}
|
|
88
|
+
if (normalizedUrl.endsWith(".heic")) {
|
|
89
|
+
return "image/heic";
|
|
90
|
+
}
|
|
91
|
+
if (normalizedUrl.endsWith(".heif")) {
|
|
92
|
+
return "image/heif";
|
|
93
|
+
}
|
|
94
|
+
return "image/jpeg";
|
|
95
|
+
}
|
|
96
|
+
mapHistory(history = []) {
|
|
97
|
+
return history.map(message => {
|
|
98
|
+
const parts = typeof message.content === 'string'
|
|
99
|
+
? [{ text: message.content }]
|
|
100
|
+
: this.mapContentParts(message.content);
|
|
101
|
+
if (message.role === "assistant") {
|
|
102
|
+
return {
|
|
103
|
+
role: "model",
|
|
104
|
+
parts,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
if (message.role === "system") {
|
|
108
|
+
return {
|
|
109
|
+
role: "user",
|
|
110
|
+
parts: [
|
|
111
|
+
{ text: "[SYSTEM]" },
|
|
112
|
+
...parts,
|
|
113
|
+
],
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
return {
|
|
117
|
+
role: "user",
|
|
118
|
+
parts,
|
|
119
|
+
};
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
hasImageInput(input) {
|
|
123
|
+
if (input.userImages && input.userImages.length > 0) {
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
if (input.userContent?.some(part => part.type === 'image')) {
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
return input.history?.some(message => Array.isArray(message.content) && message.content.some(part => part.type === 'image')) ?? false;
|
|
130
|
+
}
|
|
131
|
+
serializePromptInput(input, systemPrompt) {
|
|
132
|
+
return JSON.stringify({
|
|
133
|
+
systemPrompt,
|
|
134
|
+
history: input.history,
|
|
135
|
+
userInput: input.userInput,
|
|
136
|
+
userContent: input.userContent,
|
|
137
|
+
memory: input.memory,
|
|
138
|
+
knowledgeBase: input.knowledgeBase,
|
|
139
|
+
tools: input.tools?.map(tool => ({
|
|
140
|
+
name: tool.name,
|
|
141
|
+
description: tool.description,
|
|
142
|
+
parameters: tool.parameters,
|
|
143
|
+
})),
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
serializePromptOutput(output) {
|
|
147
|
+
if (typeof output === "string") {
|
|
148
|
+
return output;
|
|
149
|
+
}
|
|
150
|
+
if (output === null || output === undefined) {
|
|
151
|
+
return undefined;
|
|
152
|
+
}
|
|
153
|
+
return JSON.stringify(output);
|
|
154
|
+
}
|
|
155
|
+
buildLogPayload(input, params) {
|
|
156
|
+
return {
|
|
157
|
+
provider: "googleai",
|
|
158
|
+
model: params.model,
|
|
159
|
+
operationTitle: input.operationTitle,
|
|
160
|
+
operationGroup: input.operationGroup,
|
|
161
|
+
ip: input.ip,
|
|
162
|
+
userAgent: input.userAgent,
|
|
163
|
+
input: this.serializePromptInput(input, params.systemPrompt),
|
|
164
|
+
inputImages: input.userImages?.map(image => ({
|
|
165
|
+
url: image.url,
|
|
166
|
+
})) ?? input.userContent
|
|
167
|
+
?.filter(part => part.type === "image")
|
|
168
|
+
.map(part => ({
|
|
169
|
+
url: part.imageUrl,
|
|
170
|
+
})),
|
|
171
|
+
inputFiles: input.inputFiles,
|
|
172
|
+
inputTokens: params.inputTokens,
|
|
173
|
+
outputTokens: params.outputTokens,
|
|
174
|
+
tokens: params.tokens,
|
|
175
|
+
startedAt: params.startedAt,
|
|
176
|
+
endedAt: params.endedAt,
|
|
177
|
+
responseTime: params.endedAt ? `${params.endedAt.getTime() - params.startedAt.getTime()}ms` : undefined,
|
|
178
|
+
output: this.serializePromptOutput(params.output),
|
|
179
|
+
success: params.success,
|
|
180
|
+
errorMessage: params.errorMessage,
|
|
181
|
+
tenant: input.tenant,
|
|
182
|
+
user: input.user,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
async registerPromptLog(input, params) {
|
|
186
|
+
if (!this._aiLogService) {
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
try {
|
|
190
|
+
await this._aiLogService.create(this.buildLogPayload(input, params));
|
|
191
|
+
}
|
|
192
|
+
catch (e) {
|
|
193
|
+
console.error("Error registerPromptLog", {
|
|
194
|
+
name: e?.name,
|
|
195
|
+
message: e?.message,
|
|
196
|
+
stack: e?.stack,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
async generateEmbedding({ text, model = "text-embedding-004" }) {
|
|
201
|
+
const response = await this.client.models.embedContent({
|
|
202
|
+
model,
|
|
203
|
+
contents: text,
|
|
204
|
+
});
|
|
205
|
+
return response.embeddings?.[0]?.values ?? [];
|
|
206
|
+
}
|
|
207
|
+
mapTools(tools = []) {
|
|
208
|
+
if (tools.length === 0) {
|
|
209
|
+
return [];
|
|
210
|
+
}
|
|
211
|
+
return [{
|
|
212
|
+
functionDeclarations: tools.map(tool => ({
|
|
213
|
+
name: tool.name,
|
|
214
|
+
description: tool.description,
|
|
215
|
+
parametersJsonSchema: tool.parameters ?? {
|
|
216
|
+
type: "object",
|
|
217
|
+
properties: {},
|
|
218
|
+
additionalProperties: false,
|
|
219
|
+
},
|
|
220
|
+
}))
|
|
221
|
+
}];
|
|
222
|
+
}
|
|
223
|
+
buildResponseConfig(input, systemPrompt) {
|
|
224
|
+
const config = {
|
|
225
|
+
systemInstruction: systemPrompt,
|
|
226
|
+
};
|
|
227
|
+
const responseJsonSchema = this.normalizeResponseJsonSchema(input);
|
|
228
|
+
if (responseJsonSchema) {
|
|
229
|
+
config.responseMimeType = "application/json";
|
|
230
|
+
config.responseJsonSchema = responseJsonSchema;
|
|
231
|
+
}
|
|
232
|
+
if (input.tools && input.tools.length > 0) {
|
|
233
|
+
config.tools = this.mapTools(input.tools);
|
|
234
|
+
}
|
|
235
|
+
return config;
|
|
236
|
+
}
|
|
237
|
+
normalizeResponseJsonSchema(input) {
|
|
238
|
+
if (input.zodSchema) {
|
|
239
|
+
return toJSONSchema(input.zodSchema, {
|
|
240
|
+
target: "draft-7",
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
if (!input.jsonSchema) {
|
|
244
|
+
return undefined;
|
|
245
|
+
}
|
|
246
|
+
const jsonSchema = input.jsonSchema;
|
|
247
|
+
if (jsonSchema.type === "json_schema" && jsonSchema.json_schema?.schema) {
|
|
248
|
+
return jsonSchema.json_schema.schema;
|
|
249
|
+
}
|
|
250
|
+
return jsonSchema;
|
|
251
|
+
}
|
|
252
|
+
async buildToolResponseParts(functionCalls = [], tools = []) {
|
|
253
|
+
const parts = [];
|
|
254
|
+
for (const functionCall of functionCalls) {
|
|
255
|
+
const toolName = functionCall.name;
|
|
256
|
+
const tool = tools.find(t => t.name === toolName);
|
|
257
|
+
if (!tool) {
|
|
258
|
+
throw new Error(`Tool not found: ${toolName}`);
|
|
259
|
+
}
|
|
260
|
+
const output = await tool.execute(functionCall.args ?? {});
|
|
261
|
+
parts.push({
|
|
262
|
+
functionResponse: {
|
|
263
|
+
id: functionCall.id,
|
|
264
|
+
name: toolName,
|
|
265
|
+
response: {
|
|
266
|
+
output,
|
|
267
|
+
},
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
return parts;
|
|
272
|
+
}
|
|
273
|
+
buildModelFunctionCallContent(functionCalls = []) {
|
|
274
|
+
return {
|
|
275
|
+
role: "model",
|
|
276
|
+
parts: functionCalls.map(functionCall => ({
|
|
277
|
+
functionCall,
|
|
278
|
+
}))
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
async prompt(input) {
|
|
282
|
+
if (!input.systemPrompt) {
|
|
283
|
+
throw new Error("systemPrompt required");
|
|
284
|
+
}
|
|
285
|
+
let systemPrompt = input.systemPrompt;
|
|
286
|
+
if (input.memory && input.memory.length > 0) {
|
|
287
|
+
systemPrompt += `\n\n ${input.memoryHeader ?? '[MEMORIA]'}\n ${input.memory.map(m => `${m.key}: ${m.value}`).join('\n')}`;
|
|
288
|
+
}
|
|
289
|
+
if (input.knowledgeBase && input.knowledgeBase.length > 0) {
|
|
290
|
+
systemPrompt += `\n\n${input.knowledgeBaseHeader ?? '[BASE DE CONOCIMIENTO]'}\n ${input.knowledgeBase.join('\n')}`;
|
|
291
|
+
}
|
|
292
|
+
const userInput = this.buildUserContent(input);
|
|
293
|
+
const model = input.model ?? (this.hasImageInput(input) ? this.visionModel ?? this.model : this.model);
|
|
294
|
+
const startedAt = new Date();
|
|
295
|
+
const startTime = performance.now();
|
|
296
|
+
let tokens = 0;
|
|
297
|
+
let inputTokens = 0;
|
|
298
|
+
let outputTokens = 0;
|
|
299
|
+
try {
|
|
300
|
+
const contents = [
|
|
301
|
+
...this.mapHistory(input.history),
|
|
302
|
+
{ role: 'user', parts: userInput },
|
|
303
|
+
];
|
|
304
|
+
const tools = input.tools ?? [];
|
|
305
|
+
const maxIterations = input.toolMaxIterations ?? 5;
|
|
306
|
+
let output;
|
|
307
|
+
for (let iteration = 0; iteration < maxIterations; iteration++) {
|
|
308
|
+
const response = await this.client.models.generateContent({
|
|
309
|
+
model,
|
|
310
|
+
contents,
|
|
311
|
+
config: this.buildResponseConfig(input, systemPrompt),
|
|
312
|
+
});
|
|
313
|
+
tokens += response.usageMetadata?.totalTokenCount ?? 0;
|
|
314
|
+
inputTokens += response.usageMetadata?.promptTokenCount ?? 0;
|
|
315
|
+
outputTokens += response.usageMetadata?.candidatesTokenCount ?? 0;
|
|
316
|
+
const functionCalls = response.functionCalls ?? [];
|
|
317
|
+
if (functionCalls.length === 0) {
|
|
318
|
+
output = response.text;
|
|
319
|
+
break;
|
|
320
|
+
}
|
|
321
|
+
contents.push(response.candidates?.[0]?.content ?? this.buildModelFunctionCallContent(functionCalls));
|
|
322
|
+
contents.push({
|
|
323
|
+
role: "user",
|
|
324
|
+
parts: await this.buildToolResponseParts(functionCalls, tools),
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
if (output === undefined) {
|
|
328
|
+
throw new Error(`Tool max iterations reached: ${maxIterations}`);
|
|
329
|
+
}
|
|
330
|
+
const endTime = performance.now();
|
|
331
|
+
const time = endTime - startTime;
|
|
332
|
+
const endedAt = new Date();
|
|
333
|
+
await this.registerPromptLog(input, {
|
|
334
|
+
model,
|
|
335
|
+
systemPrompt,
|
|
336
|
+
startedAt,
|
|
337
|
+
endedAt,
|
|
338
|
+
inputTokens,
|
|
339
|
+
outputTokens,
|
|
340
|
+
tokens,
|
|
341
|
+
output,
|
|
342
|
+
success: true,
|
|
343
|
+
});
|
|
344
|
+
return {
|
|
345
|
+
output,
|
|
346
|
+
tokens,
|
|
347
|
+
inputTokens,
|
|
348
|
+
outputTokens,
|
|
349
|
+
time
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
catch (e) {
|
|
353
|
+
const endedAt = new Date();
|
|
354
|
+
await this.registerPromptLog(input, {
|
|
355
|
+
model,
|
|
356
|
+
systemPrompt,
|
|
357
|
+
startedAt,
|
|
358
|
+
endedAt,
|
|
359
|
+
success: false,
|
|
360
|
+
errorMessage: e?.message,
|
|
361
|
+
});
|
|
362
|
+
throw e;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
export default GoogleAiProvider;
|
|
367
|
+
export { GoogleAiProvider };
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
import { toJSONSchema } from "zod";
|
|
2
|
+
class OllamaAiProvider {
|
|
3
|
+
constructor(baseUrl, model, visionModel, embeddingModel, aiLogService) {
|
|
4
|
+
if (!baseUrl) {
|
|
5
|
+
throw new Error("Ollama AI baseUrl required");
|
|
6
|
+
}
|
|
7
|
+
if (!model) {
|
|
8
|
+
throw new Error("Ollama AI model required");
|
|
9
|
+
}
|
|
10
|
+
this._baseUrl = baseUrl.replace(/\/+$/, "");
|
|
11
|
+
this._model = model;
|
|
12
|
+
this._visionModel = visionModel;
|
|
13
|
+
this._embeddingModel = embeddingModel;
|
|
14
|
+
this._aiLogService = aiLogService;
|
|
15
|
+
}
|
|
16
|
+
get model() {
|
|
17
|
+
if (!this._model) {
|
|
18
|
+
throw new Error("Ollama AI model not found");
|
|
19
|
+
}
|
|
20
|
+
return this._model;
|
|
21
|
+
}
|
|
22
|
+
get visionModel() {
|
|
23
|
+
return this._visionModel;
|
|
24
|
+
}
|
|
25
|
+
get embeddingModel() {
|
|
26
|
+
return this._embeddingModel ?? this.model;
|
|
27
|
+
}
|
|
28
|
+
async post(path, body) {
|
|
29
|
+
const response = await fetch(`${this._baseUrl}${path}`, {
|
|
30
|
+
method: "POST",
|
|
31
|
+
headers: {
|
|
32
|
+
"Content-Type": "application/json",
|
|
33
|
+
},
|
|
34
|
+
body: JSON.stringify(body),
|
|
35
|
+
});
|
|
36
|
+
if (!response.ok) {
|
|
37
|
+
const errorText = await response.text();
|
|
38
|
+
throw new Error(`Ollama AI request failed (${response.status}): ${errorText}`);
|
|
39
|
+
}
|
|
40
|
+
return await response.json();
|
|
41
|
+
}
|
|
42
|
+
async buildUserMessage(input) {
|
|
43
|
+
if (input.userContent && input.userContent.length > 0) {
|
|
44
|
+
return await this.mapContentPartsToMessage(input.userContent);
|
|
45
|
+
}
|
|
46
|
+
if (input.userImages && input.userImages.length > 0) {
|
|
47
|
+
return {
|
|
48
|
+
role: "user",
|
|
49
|
+
content: input.userInput ?? "",
|
|
50
|
+
images: await Promise.all(input.userImages.map(image => this.imageUrlToBase64(image.url))),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
role: "user",
|
|
55
|
+
content: input.userInput ?? "",
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
async mapContentPartsToMessage(content, role = "user") {
|
|
59
|
+
const text = [];
|
|
60
|
+
const images = [];
|
|
61
|
+
for (const part of content) {
|
|
62
|
+
if (part.type === "text") {
|
|
63
|
+
text.push(part.text);
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
images.push(await this.imageUrlToBase64(part.imageUrl));
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
role,
|
|
70
|
+
content: text.join("\n"),
|
|
71
|
+
...(images.length > 0 ? { images } : {}),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
async imageUrlToBase64(url) {
|
|
75
|
+
const dataUrlMatch = url.match(/^data:[^;,]+;base64,(.+)$/);
|
|
76
|
+
if (dataUrlMatch) {
|
|
77
|
+
return dataUrlMatch[1];
|
|
78
|
+
}
|
|
79
|
+
const response = await fetch(url);
|
|
80
|
+
if (!response.ok) {
|
|
81
|
+
throw new Error(`Ollama AI image request failed (${response.status}): ${url}`);
|
|
82
|
+
}
|
|
83
|
+
const buffer = Buffer.from(await response.arrayBuffer());
|
|
84
|
+
return buffer.toString("base64");
|
|
85
|
+
}
|
|
86
|
+
async mapHistory(history = []) {
|
|
87
|
+
const messages = [];
|
|
88
|
+
for (const message of history) {
|
|
89
|
+
if (typeof message.content === "string") {
|
|
90
|
+
messages.push({
|
|
91
|
+
role: message.role,
|
|
92
|
+
content: message.content,
|
|
93
|
+
});
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
messages.push(await this.mapContentPartsToMessage(message.content, message.role));
|
|
97
|
+
}
|
|
98
|
+
return messages;
|
|
99
|
+
}
|
|
100
|
+
hasImageInput(input) {
|
|
101
|
+
if (input.userImages && input.userImages.length > 0) {
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
if (input.userContent?.some(part => part.type === 'image')) {
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
return input.history?.some(message => Array.isArray(message.content) && message.content.some(part => part.type === 'image')) ?? false;
|
|
108
|
+
}
|
|
109
|
+
serializePromptInput(input, systemPrompt) {
|
|
110
|
+
return JSON.stringify({
|
|
111
|
+
systemPrompt,
|
|
112
|
+
history: input.history,
|
|
113
|
+
userInput: input.userInput,
|
|
114
|
+
userContent: input.userContent,
|
|
115
|
+
memory: input.memory,
|
|
116
|
+
knowledgeBase: input.knowledgeBase,
|
|
117
|
+
tools: input.tools?.map(tool => ({
|
|
118
|
+
name: tool.name,
|
|
119
|
+
description: tool.description,
|
|
120
|
+
parameters: tool.parameters,
|
|
121
|
+
})),
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
serializePromptOutput(output) {
|
|
125
|
+
if (typeof output === "string") {
|
|
126
|
+
return output;
|
|
127
|
+
}
|
|
128
|
+
if (output === null || output === undefined) {
|
|
129
|
+
return undefined;
|
|
130
|
+
}
|
|
131
|
+
return JSON.stringify(output);
|
|
132
|
+
}
|
|
133
|
+
buildLogPayload(input, params) {
|
|
134
|
+
return {
|
|
135
|
+
provider: "ollamaai",
|
|
136
|
+
model: params.model,
|
|
137
|
+
operationTitle: input.operationTitle,
|
|
138
|
+
operationGroup: input.operationGroup,
|
|
139
|
+
ip: input.ip,
|
|
140
|
+
userAgent: input.userAgent,
|
|
141
|
+
input: this.serializePromptInput(input, params.systemPrompt),
|
|
142
|
+
inputImages: input.userImages?.map(image => ({
|
|
143
|
+
url: image.url,
|
|
144
|
+
})) ?? input.userContent
|
|
145
|
+
?.filter(part => part.type === "image")
|
|
146
|
+
.map(part => ({
|
|
147
|
+
url: part.imageUrl,
|
|
148
|
+
})),
|
|
149
|
+
inputFiles: input.inputFiles,
|
|
150
|
+
inputTokens: params.inputTokens,
|
|
151
|
+
outputTokens: params.outputTokens,
|
|
152
|
+
tokens: params.tokens,
|
|
153
|
+
startedAt: params.startedAt,
|
|
154
|
+
endedAt: params.endedAt,
|
|
155
|
+
responseTime: params.endedAt ? `${params.endedAt.getTime() - params.startedAt.getTime()}ms` : undefined,
|
|
156
|
+
output: this.serializePromptOutput(params.output),
|
|
157
|
+
success: params.success,
|
|
158
|
+
errorMessage: params.errorMessage,
|
|
159
|
+
tenant: input.tenant,
|
|
160
|
+
user: input.user,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
async registerPromptLog(input, params) {
|
|
164
|
+
if (!this._aiLogService) {
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
try {
|
|
168
|
+
await this._aiLogService.create(this.buildLogPayload(input, params));
|
|
169
|
+
}
|
|
170
|
+
catch (e) {
|
|
171
|
+
console.error("Error registerPromptLog", {
|
|
172
|
+
name: e?.name,
|
|
173
|
+
message: e?.message,
|
|
174
|
+
stack: e?.stack,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
async generateEmbedding({ text, model }) {
|
|
179
|
+
const response = await this.post("/api/embed", {
|
|
180
|
+
model: model ?? this.embeddingModel,
|
|
181
|
+
input: text,
|
|
182
|
+
});
|
|
183
|
+
return response.embeddings?.[0] ?? response.embedding ?? [];
|
|
184
|
+
}
|
|
185
|
+
mapTools(tools = []) {
|
|
186
|
+
return tools.map(tool => ({
|
|
187
|
+
type: "function",
|
|
188
|
+
function: {
|
|
189
|
+
name: tool.name,
|
|
190
|
+
description: tool.description,
|
|
191
|
+
parameters: tool.parameters ?? {
|
|
192
|
+
type: "object",
|
|
193
|
+
properties: {},
|
|
194
|
+
additionalProperties: false,
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
}));
|
|
198
|
+
}
|
|
199
|
+
normalizeResponseFormat(input) {
|
|
200
|
+
if (input.zodSchema) {
|
|
201
|
+
return toJSONSchema(input.zodSchema, {
|
|
202
|
+
target: "draft-7",
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
if (!input.jsonSchema) {
|
|
206
|
+
return undefined;
|
|
207
|
+
}
|
|
208
|
+
const jsonSchema = input.jsonSchema;
|
|
209
|
+
if (jsonSchema.type === "json_schema" && jsonSchema.json_schema?.schema) {
|
|
210
|
+
return jsonSchema.json_schema.schema;
|
|
211
|
+
}
|
|
212
|
+
return jsonSchema;
|
|
213
|
+
}
|
|
214
|
+
parseToolArguments(args) {
|
|
215
|
+
if (!args) {
|
|
216
|
+
return {};
|
|
217
|
+
}
|
|
218
|
+
if (typeof args === "object") {
|
|
219
|
+
return args;
|
|
220
|
+
}
|
|
221
|
+
try {
|
|
222
|
+
return JSON.parse(args);
|
|
223
|
+
}
|
|
224
|
+
catch (e) {
|
|
225
|
+
throw new Error(`Invalid tool arguments: ${args}`);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
serializeToolOutput(output) {
|
|
229
|
+
if (typeof output === "string") {
|
|
230
|
+
return output;
|
|
231
|
+
}
|
|
232
|
+
if (output === undefined) {
|
|
233
|
+
return "";
|
|
234
|
+
}
|
|
235
|
+
return JSON.stringify(output);
|
|
236
|
+
}
|
|
237
|
+
async buildToolMessages(toolCalls = [], tools = []) {
|
|
238
|
+
const toolMessages = [];
|
|
239
|
+
for (const toolCall of toolCalls) {
|
|
240
|
+
const toolName = toolCall.function?.name;
|
|
241
|
+
const tool = tools.find(t => t.name === toolName);
|
|
242
|
+
if (!tool) {
|
|
243
|
+
throw new Error(`Tool not found: ${toolName}`);
|
|
244
|
+
}
|
|
245
|
+
const args = this.parseToolArguments(toolCall.function?.arguments);
|
|
246
|
+
const output = await tool.execute(args);
|
|
247
|
+
toolMessages.push({
|
|
248
|
+
role: "tool",
|
|
249
|
+
name: toolName,
|
|
250
|
+
content: this.serializeToolOutput(output),
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
return toolMessages;
|
|
254
|
+
}
|
|
255
|
+
async prompt(input) {
|
|
256
|
+
if (!input.systemPrompt) {
|
|
257
|
+
throw new Error("systemPrompt required");
|
|
258
|
+
}
|
|
259
|
+
let systemPrompt = input.systemPrompt;
|
|
260
|
+
if (input.memory && input.memory.length > 0) {
|
|
261
|
+
systemPrompt += `\n\n ${input.memoryHeader ?? '[MEMORIA]'}\n ${input.memory.map(m => `${m.key}: ${m.value}`).join('\n')}`;
|
|
262
|
+
}
|
|
263
|
+
if (input.knowledgeBase && input.knowledgeBase.length > 0) {
|
|
264
|
+
systemPrompt += `\n\n${input.knowledgeBaseHeader ?? '[BASE DE CONOCIMIENTO]'}\n ${input.knowledgeBase.join('\n')}`;
|
|
265
|
+
}
|
|
266
|
+
const model = input.model ?? (this.hasImageInput(input) ? this.visionModel ?? this.model : this.model);
|
|
267
|
+
const startedAt = new Date();
|
|
268
|
+
const startTime = performance.now();
|
|
269
|
+
let tokens = 0;
|
|
270
|
+
let inputTokens = 0;
|
|
271
|
+
let outputTokens = 0;
|
|
272
|
+
try {
|
|
273
|
+
const messages = [
|
|
274
|
+
{ role: 'system', content: systemPrompt },
|
|
275
|
+
...await this.mapHistory(input.history),
|
|
276
|
+
await this.buildUserMessage(input),
|
|
277
|
+
];
|
|
278
|
+
const tools = input.tools ?? [];
|
|
279
|
+
const maxIterations = input.toolMaxIterations ?? 5;
|
|
280
|
+
const responseFormat = this.normalizeResponseFormat(input);
|
|
281
|
+
let output;
|
|
282
|
+
for (let iteration = 0; iteration < maxIterations; iteration++) {
|
|
283
|
+
const response = await this.post("/api/chat", {
|
|
284
|
+
model,
|
|
285
|
+
messages,
|
|
286
|
+
stream: false,
|
|
287
|
+
...(responseFormat ? { format: responseFormat } : {}),
|
|
288
|
+
...(tools.length > 0 ? { tools: this.mapTools(tools) } : {}),
|
|
289
|
+
});
|
|
290
|
+
inputTokens += response.prompt_eval_count ?? 0;
|
|
291
|
+
outputTokens += response.eval_count ?? 0;
|
|
292
|
+
tokens += (response.prompt_eval_count ?? 0) + (response.eval_count ?? 0);
|
|
293
|
+
const message = response.message ?? {};
|
|
294
|
+
const toolCalls = message.tool_calls ?? [];
|
|
295
|
+
if (toolCalls.length === 0) {
|
|
296
|
+
output = message.content ?? response.response ?? "";
|
|
297
|
+
break;
|
|
298
|
+
}
|
|
299
|
+
messages.push(message);
|
|
300
|
+
messages.push(...await this.buildToolMessages(toolCalls, tools));
|
|
301
|
+
}
|
|
302
|
+
if (output === undefined) {
|
|
303
|
+
throw new Error(`Tool max iterations reached: ${maxIterations}`);
|
|
304
|
+
}
|
|
305
|
+
const endTime = performance.now();
|
|
306
|
+
const time = endTime - startTime;
|
|
307
|
+
const endedAt = new Date();
|
|
308
|
+
await this.registerPromptLog(input, {
|
|
309
|
+
model,
|
|
310
|
+
systemPrompt,
|
|
311
|
+
startedAt,
|
|
312
|
+
endedAt,
|
|
313
|
+
inputTokens,
|
|
314
|
+
outputTokens,
|
|
315
|
+
tokens,
|
|
316
|
+
output,
|
|
317
|
+
success: true,
|
|
318
|
+
});
|
|
319
|
+
return {
|
|
320
|
+
output,
|
|
321
|
+
tokens,
|
|
322
|
+
inputTokens,
|
|
323
|
+
outputTokens,
|
|
324
|
+
time
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
catch (e) {
|
|
328
|
+
const endedAt = new Date();
|
|
329
|
+
await this.registerPromptLog(input, {
|
|
330
|
+
model,
|
|
331
|
+
systemPrompt,
|
|
332
|
+
startedAt,
|
|
333
|
+
endedAt,
|
|
334
|
+
success: false,
|
|
335
|
+
errorMessage: e?.message,
|
|
336
|
+
});
|
|
337
|
+
throw e;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
export default OllamaAiProvider;
|
|
342
|
+
export { OllamaAiProvider };
|