@drax/ai-back 3.26.0 → 3.28.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/.env ADDED
@@ -0,0 +1,4 @@
1
+ #IA
2
+ OPENAI_API_KEY=sk-svcacct-Jm8f5uVnoe2Jl71Hux1vslYU3w0roJyGiV-itniyrDVw191uhGcSHgTh0O59VGAT3BlbkFJaeneUjBAPPC4pMQIVmeTL98U6cp0vnIR-wugCX-wGxxqpjB2vVRj4GRz3sjpkLQA
3
+ OPENAI_MODEL=gpt-5.4
4
+ OPENAI_VISION_MODEL=gpt-5.4
@@ -92,6 +92,11 @@ class OpenAiProvider {
92
92
  userContent: input.userContent,
93
93
  memory: input.memory,
94
94
  knowledgeBase: input.knowledgeBase,
95
+ tools: input.tools?.map(tool => ({
96
+ name: tool.name,
97
+ description: tool.description,
98
+ parameters: tool.parameters,
99
+ })),
95
100
  });
96
101
  }
97
102
  serializePromptOutput(output) {
@@ -155,6 +160,58 @@ class OpenAiProvider {
155
160
  });
156
161
  return response.data[0].embedding;
157
162
  }
163
+ mapTools(tools = []) {
164
+ return tools.map(tool => ({
165
+ type: "function",
166
+ function: {
167
+ name: tool.name,
168
+ description: tool.description,
169
+ parameters: tool.parameters ?? {
170
+ type: "object",
171
+ properties: {},
172
+ additionalProperties: false,
173
+ },
174
+ },
175
+ }));
176
+ }
177
+ parseToolArguments(args) {
178
+ if (!args) {
179
+ return {};
180
+ }
181
+ try {
182
+ return JSON.parse(args);
183
+ }
184
+ catch (e) {
185
+ throw new Error(`Invalid tool arguments: ${args}`);
186
+ }
187
+ }
188
+ serializeToolOutput(output) {
189
+ if (typeof output === "string") {
190
+ return output;
191
+ }
192
+ if (output === undefined) {
193
+ return "";
194
+ }
195
+ return JSON.stringify(output);
196
+ }
197
+ async buildToolMessages(toolCalls = [], tools = []) {
198
+ const toolMessages = [];
199
+ for (const toolCall of toolCalls) {
200
+ const toolName = toolCall.function?.name;
201
+ const tool = tools.find(t => t.name === toolName);
202
+ if (!tool) {
203
+ throw new Error(`Tool not found: ${toolName}`);
204
+ }
205
+ const args = this.parseToolArguments(toolCall.function?.arguments);
206
+ const output = await tool.execute(args);
207
+ toolMessages.push({
208
+ role: "tool",
209
+ tool_call_id: toolCall.id,
210
+ content: this.serializeToolOutput(output),
211
+ });
212
+ }
213
+ return toolMessages;
214
+ }
158
215
  async prompt(input) {
159
216
  if (!input.systemPrompt) {
160
217
  throw new Error("systemPrompt required");
@@ -170,21 +227,41 @@ class OpenAiProvider {
170
227
  const model = input.model ?? (this.hasImageInput(input) ? this.visionModel ?? this.model : this.model);
171
228
  const startedAt = new Date();
172
229
  const startTime = performance.now();
230
+ let tokens = 0;
231
+ let inputTokens = 0;
232
+ let outputTokens = 0;
173
233
  try {
174
- const chatCompletion = await this.client.chat.completions.create({
175
- messages: [
176
- { role: 'system', content: systemPrompt },
177
- ...this.mapHistory(input.history),
178
- { role: 'user', content: userInput },
179
- ],
180
- ...(input.zodSchema ? { response_format: zodResponseFormat(input.zodSchema, "event") } : {}),
181
- ...(input.jsonSchema ? { response_format: input.jsonSchema } : {}),
182
- model: model,
183
- });
184
- const output = chatCompletion.choices[0].message.content;
185
- const tokens = chatCompletion.usage.total_tokens;
186
- const inputTokens = chatCompletion.usage.prompt_tokens;
187
- const outputTokens = chatCompletion.usage.completion_tokens;
234
+ const messages = [
235
+ { role: 'system', content: systemPrompt },
236
+ ...this.mapHistory(input.history),
237
+ { role: 'user', content: userInput },
238
+ ];
239
+ const tools = input.tools ?? [];
240
+ const maxIterations = input.toolMaxIterations ?? 5;
241
+ let output;
242
+ for (let iteration = 0; iteration < maxIterations; iteration++) {
243
+ const chatCompletion = await this.client.chat.completions.create({
244
+ messages,
245
+ ...(input.zodSchema ? { response_format: zodResponseFormat(input.zodSchema, "event") } : {}),
246
+ ...(input.jsonSchema ? { response_format: input.jsonSchema } : {}),
247
+ ...(tools.length > 0 ? { tools: this.mapTools(tools) } : {}),
248
+ model: model,
249
+ });
250
+ tokens += chatCompletion.usage?.total_tokens ?? 0;
251
+ inputTokens += chatCompletion.usage?.prompt_tokens ?? 0;
252
+ outputTokens += chatCompletion.usage?.completion_tokens ?? 0;
253
+ const message = chatCompletion.choices[0].message;
254
+ const toolCalls = message.tool_calls ?? [];
255
+ if (toolCalls.length === 0) {
256
+ output = message.content;
257
+ break;
258
+ }
259
+ messages.push(message);
260
+ messages.push(...await this.buildToolMessages(toolCalls, tools));
261
+ }
262
+ if (output === undefined) {
263
+ throw new Error(`Tool max iterations reached: ${maxIterations}`);
264
+ }
188
265
  const endTime = performance.now();
189
266
  const time = endTime - startTime;
190
267
  const endedAt = new Date();
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "3.26.0",
6
+ "version": "3.28.0",
7
7
  "description": "Ai utils",
8
8
  "main": "dist/index.js",
9
9
  "types": "types/index.d.ts",
@@ -19,7 +19,7 @@
19
19
  "license": "ISC",
20
20
  "dependencies": {
21
21
  "@drax/ai-share": "^3.18.0",
22
- "@drax/crud-back": "^3.26.0",
22
+ "@drax/crud-back": "^3.28.0",
23
23
  "mongoose": "^8.23.0",
24
24
  "mongoose-paginate-v2": "^1.8.3"
25
25
  },
@@ -44,5 +44,5 @@
44
44
  "typescript": "^5.9.3",
45
45
  "vitest": "^3.0.8"
46
46
  },
47
- "gitHead": "482f91f51a9e783ac9131f557ace113f3b3404fa"
47
+ "gitHead": "f9787aec59da68ec6d84d0c5e3ee3471eb65ad07"
48
48
  }
package/src/index.ts CHANGED
@@ -27,7 +27,8 @@ import type {
27
27
  IPromptMessage,
28
28
  IPromptMemory,
29
29
  IPromptParams,
30
- IPromptResponse
30
+ IPromptResponse,
31
+ IPromptTool
31
32
  } from "./interfaces/IAIProvider.js";
32
33
 
33
34
  export type {
@@ -37,6 +38,7 @@ export type {
37
38
  IPromptParams,
38
39
  IPromptMessage,
39
40
  IPromptMemory,
41
+ IPromptTool,
40
42
  IPromptImage,
41
43
  IPromptImageDetail,
42
44
  IPromptContentPart,
@@ -32,6 +32,13 @@ interface IPromptMemory {
32
32
  value: string;
33
33
  }
34
34
 
35
+ interface IPromptTool {
36
+ name: string;
37
+ description: string;
38
+ parameters?: object;
39
+ execute: (args: any) => any | Promise<any>;
40
+ }
41
+
35
42
  interface IPromptParams {
36
43
  systemPrompt: string,
37
44
  userInput?: string,
@@ -51,6 +58,8 @@ interface IPromptParams {
51
58
  knowledgeBaseHeader?: string | '[KNOWLEDGE BASE]' | '[BASE DE CONOCIMIENTO]',
52
59
  zodSchema?: ZodSchema<any>,
53
60
  jsonSchema?: object,
61
+ tools?: IPromptTool[],
62
+ toolMaxIterations?: number,
54
63
  model?: string,
55
64
  operationTitle?: string,
56
65
  operationGroup?: string,
@@ -78,6 +87,7 @@ export type {
78
87
  IPromptResponse,
79
88
  IPromptMessage,
80
89
  IPromptMemory,
90
+ IPromptTool,
81
91
  IPromptImage,
82
92
  IPromptImageDetail,
83
93
  IPromptContentPart,
@@ -5,7 +5,8 @@ import type {
5
5
  IPromptContentPart,
6
6
  IPromptMessage,
7
7
  IPromptParams,
8
- IPromptResponse
8
+ IPromptResponse,
9
+ IPromptTool
9
10
  } from "../interfaces/IAIProvider";
10
11
  import type {AILogService} from "../services/AILogService";
11
12
  import type {IAILogBase} from "@drax/ai-share";
@@ -129,6 +130,11 @@ class OpenAiProvider implements IAIProvider{
129
130
  userContent: input.userContent,
130
131
  memory: input.memory,
131
132
  knowledgeBase: input.knowledgeBase,
133
+ tools: input.tools?.map(tool => ({
134
+ name: tool.name,
135
+ description: tool.description,
136
+ parameters: tool.parameters,
137
+ })),
132
138
  })
133
139
  }
134
140
 
@@ -221,6 +227,69 @@ class OpenAiProvider implements IAIProvider{
221
227
  return response.data[0].embedding;
222
228
  }
223
229
 
230
+ protected mapTools(tools: IPromptTool[] = []){
231
+ return tools.map(tool => ({
232
+ type: "function" as const,
233
+ function: {
234
+ name: tool.name,
235
+ description: tool.description,
236
+ parameters: tool.parameters ?? {
237
+ type: "object",
238
+ properties: {},
239
+ additionalProperties: false,
240
+ },
241
+ },
242
+ }))
243
+ }
244
+
245
+ protected parseToolArguments(args: string | undefined){
246
+ if(!args){
247
+ return {}
248
+ }
249
+
250
+ try{
251
+ return JSON.parse(args)
252
+ }catch(e){
253
+ throw new Error(`Invalid tool arguments: ${args}`)
254
+ }
255
+ }
256
+
257
+ protected serializeToolOutput(output: unknown){
258
+ if(typeof output === "string"){
259
+ return output
260
+ }
261
+
262
+ if(output === undefined){
263
+ return ""
264
+ }
265
+
266
+ return JSON.stringify(output)
267
+ }
268
+
269
+ protected async buildToolMessages(toolCalls: any[] = [], tools: IPromptTool[] = []){
270
+ const toolMessages: any[] = []
271
+
272
+ for(const toolCall of toolCalls){
273
+ const toolName = toolCall.function?.name
274
+ const tool = tools.find(t => t.name === toolName)
275
+
276
+ if(!tool){
277
+ throw new Error(`Tool not found: ${toolName}`)
278
+ }
279
+
280
+ const args = this.parseToolArguments(toolCall.function?.arguments)
281
+ const output = await tool.execute(args)
282
+
283
+ toolMessages.push({
284
+ role: "tool",
285
+ tool_call_id: toolCall.id,
286
+ content: this.serializeToolOutput(output),
287
+ })
288
+ }
289
+
290
+ return toolMessages
291
+ }
292
+
224
293
  async prompt(input: IPromptParams): Promise<IPromptResponse> {
225
294
 
226
295
  if(!input.systemPrompt){
@@ -242,25 +311,49 @@ class OpenAiProvider implements IAIProvider{
242
311
  const model = input.model ?? (this.hasImageInput(input) ? this.visionModel ?? this.model : this.model)
243
312
  const startedAt = new Date()
244
313
  const startTime = performance.now()
314
+ let tokens = 0
315
+ let inputTokens = 0
316
+ let outputTokens = 0
245
317
 
246
318
  try {
247
- const chatCompletion = await this.client.chat.completions.create({
248
- messages: [
249
- {role: 'system', content: systemPrompt},
250
- ...this.mapHistory(input.history),
251
- {role: 'user', content: userInput},
252
- ],
253
-
254
- ...(input.zodSchema ? {response_format: zodResponseFormat(input.zodSchema, "event")} : {}),
255
- ...(input.jsonSchema ? {response_format: input.jsonSchema} : {}),
256
- model: model,
257
- });
319
+ const messages: any[] = [
320
+ {role: 'system', content: systemPrompt},
321
+ ...this.mapHistory(input.history),
322
+ {role: 'user', content: userInput},
323
+ ]
324
+ const tools = input.tools ?? []
325
+ const maxIterations = input.toolMaxIterations ?? 5
326
+ let output: any
327
+
328
+ for(let iteration = 0; iteration < maxIterations; iteration++){
329
+ const chatCompletion = await this.client.chat.completions.create({
330
+ messages,
331
+
332
+ ...(input.zodSchema ? {response_format: zodResponseFormat(input.zodSchema, "event")} : {}),
333
+ ...(input.jsonSchema ? {response_format: input.jsonSchema} : {}),
334
+ ...(tools.length > 0 ? {tools: this.mapTools(tools)} : {}),
335
+ model: model,
336
+ });
337
+
338
+ tokens += chatCompletion.usage?.total_tokens ?? 0
339
+ inputTokens += chatCompletion.usage?.prompt_tokens ?? 0
340
+ outputTokens += chatCompletion.usage?.completion_tokens ?? 0
341
+
342
+ const message = chatCompletion.choices[0].message
343
+ const toolCalls = message.tool_calls ?? []
344
+
345
+ if(toolCalls.length === 0){
346
+ output = message.content
347
+ break
348
+ }
258
349
 
350
+ messages.push(message)
351
+ messages.push(...await this.buildToolMessages(toolCalls, tools))
352
+ }
259
353
 
260
- const output = chatCompletion.choices[0].message.content
261
- const tokens = chatCompletion.usage.total_tokens
262
- const inputTokens = chatCompletion.usage.prompt_tokens
263
- const outputTokens = chatCompletion.usage.completion_tokens
354
+ if(output === undefined){
355
+ throw new Error(`Tool max iterations reached: ${maxIterations}`)
356
+ }
264
357
 
265
358
  const endTime = performance.now()
266
359
  const time = endTime - startTime
@@ -1,11 +1,66 @@
1
- import {describe, test, expect} from 'vitest'
1
+ import {describe, test, expect, beforeAll, afterAll} from 'vitest'
2
2
  import {OpenAiProvider, OpenAiProviderFactory} from "../src";
3
3
  import z from "zod";
4
- import {IPromptMemory, IPromptMessage} from "../src/interfaces/IAIProvider";
4
+ import {IPromptMemory, IPromptMessage, IPromptTool} from "../src/interfaces/IAIProvider";
5
+ import {TestSetup} from "./setup/TestSetup";
6
+ import {existsSync, readFileSync} from "fs";
7
+ import * as path from "path";
8
+
9
+ function loadEnvFile(){
10
+ const envPaths = [
11
+ path.resolve(process.cwd(), ".env"),
12
+ path.resolve(process.cwd(), "packages/ai/ai-back/.env"),
13
+ ]
14
+ const envPath = envPaths.find(filePath => existsSync(filePath))
15
+
16
+ if(!envPath){
17
+ return
18
+ }
19
+
20
+ const envContent = readFileSync(envPath, "utf8")
21
+
22
+ for(const line of envContent.split(/\r?\n/)){
23
+ const trimmedLine = line.trim()
24
+
25
+ if(!trimmedLine || trimmedLine.startsWith("#")){
26
+ continue
27
+ }
28
+
29
+ const separatorIndex = trimmedLine.indexOf("=")
30
+
31
+ if(separatorIndex === -1){
32
+ continue
33
+ }
34
+
35
+ const key = trimmedLine.slice(0, separatorIndex).trim()
36
+ let value = trimmedLine.slice(separatorIndex + 1).trim()
37
+
38
+ if((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))){
39
+ value = value.slice(1, -1)
40
+ }
41
+
42
+ if(process.env[key] === undefined || process.env[key] === ""){
43
+ process.env[key] = value
44
+ }
45
+ }
46
+ }
47
+
48
+ loadEnvFile()
5
49
 
6
50
 
7
51
  describe('OpenAi Test', () => {
8
52
 
53
+ let testSetup = new TestSetup("mongo")
54
+
55
+ beforeAll(async () => {
56
+ await testSetup.setup()
57
+ })
58
+
59
+ afterAll(async () => {
60
+ await testSetup.dropAndClose()
61
+ return
62
+ })
63
+
9
64
  test('OpenAi prompt supports image inputs and vision model fallback', async () => {
10
65
  let request: any
11
66
 
@@ -97,6 +152,94 @@ describe('OpenAi Test', () => {
97
152
  })
98
153
  })
99
154
 
155
+ test('OpenAi prompt executes tools and sends tool output back to model', async () => {
156
+ const requests: any[] = []
157
+ const weatherTool: IPromptTool = {
158
+ name: 'get_weather',
159
+ description: 'Get weather for a city',
160
+ parameters: {
161
+ type: 'object',
162
+ properties: {
163
+ city: {type: 'string'}
164
+ },
165
+ required: ['city'],
166
+ additionalProperties: false
167
+ },
168
+ execute: async ({city}) => ({city, temperature: 21})
169
+ }
170
+
171
+ class MockedOpenAiProvider extends OpenAiProvider {
172
+ constructor() {
173
+ super('test-key', 'gpt-4.1-mini')
174
+ this._client = {
175
+ chat: {
176
+ completions: {
177
+ create: async (payload: any) => {
178
+ requests.push(payload)
179
+
180
+ if(requests.length === 1){
181
+ return {
182
+ choices: [{
183
+ message: {
184
+ role: 'assistant',
185
+ content: null,
186
+ tool_calls: [{
187
+ id: 'call_123',
188
+ type: 'function',
189
+ function: {
190
+ name: 'get_weather',
191
+ arguments: '{"city":"Buenos Aires"}'
192
+ }
193
+ }]
194
+ }
195
+ }],
196
+ usage: {
197
+ total_tokens: 15,
198
+ prompt_tokens: 10,
199
+ completion_tokens: 5
200
+ }
201
+ }
202
+ }
203
+
204
+ return {
205
+ choices: [{message: {role: 'assistant', content: '21 grados'}}],
206
+ usage: {
207
+ total_tokens: 9,
208
+ prompt_tokens: 7,
209
+ completion_tokens: 2
210
+ }
211
+ }
212
+ }
213
+ }
214
+ }
215
+ }
216
+ }
217
+ }
218
+
219
+ const openAi = new MockedOpenAiProvider()
220
+ const r = await openAi.prompt({
221
+ systemPrompt: 'You are an AI assistant.',
222
+ userInput: 'How is the weather in Buenos Aires?',
223
+ tools: [weatherTool]
224
+ })
225
+
226
+ expect(r.output).toBe('21 grados')
227
+ expect(r.tokens).toBe(24)
228
+ expect(requests[0].tools).toEqual([{
229
+ type: 'function',
230
+ function: {
231
+ name: 'get_weather',
232
+ description: 'Get weather for a city',
233
+ parameters: weatherTool.parameters
234
+ }
235
+ }])
236
+ expect(requests[1].messages[3]).toEqual({
237
+ role: 'tool',
238
+ tool_call_id: 'call_123',
239
+ content: '{"city":"Buenos Aires","temperature":21}'
240
+ })
241
+ })
242
+
100
243
  test('OpenAi Factory', () => {
101
244
  const openAi = OpenAiProviderFactory.instance()
102
245
  expect(openAi).toBeInstanceOf(OpenAiProvider)
@@ -0,0 +1,53 @@
1
+ import {mongoose} from '@drax/common-back';
2
+ import {MongoMemoryServer} from 'mongodb-memory-server';
3
+
4
+ class MongoInMemory {
5
+
6
+ mongoServer: MongoMemoryServer
7
+
8
+ async connect() {
9
+ this.mongoServer = await MongoMemoryServer.create();
10
+ if (this.mongoServer.state == "new") {
11
+ await this.mongoServer.start()
12
+ }
13
+ if (!mongoose.connection.readyState) {
14
+ await mongoose.connect(this.mongoServer.getUri(), {dbName: "verifyMASTER"});
15
+ }
16
+ return
17
+ }
18
+
19
+ get mongooseStatus() {
20
+ return mongoose.connection.readyState
21
+ }
22
+
23
+ get serverStatus() {
24
+ return this.mongoServer.state
25
+ }
26
+
27
+ get status() {
28
+ return mongoose.connection.readyState
29
+ }
30
+
31
+ async disconnect() {
32
+ await mongoose.disconnect();
33
+ }
34
+
35
+ async dropData() {
36
+ const collections = await mongoose.connection.listCollections()
37
+ for (let collection of collections) {
38
+ console.log(`Dropping collection: ${collection.name}`)
39
+ await mongoose.connection.dropCollection(collection.name)
40
+ }
41
+
42
+ }
43
+
44
+ async dropAndClose() {
45
+ if (this.mongoServer) {
46
+ await mongoose.connection.dropDatabase();
47
+ await mongoose.connection.close();
48
+ await this.mongoServer.stop();
49
+ }
50
+ }
51
+ }
52
+
53
+ export default MongoInMemory