@drax/ai-back 3.29.0 → 3.31.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.
Files changed (103) hide show
  1. package/dist/agents/ChatbotTaskService.js +143 -0
  2. package/dist/agents/ChatbotTaskTools.js +756 -0
  3. package/dist/agents/DraxAgent.js +450 -0
  4. package/dist/controllers/AgentSessionController.js +18 -0
  5. package/dist/controllers/DraxAgentController.js +114 -0
  6. package/dist/factory/services/AgentSessionServiceFactory.js +30 -0
  7. package/dist/index.js +15 -1
  8. package/dist/interfaces/IAgentSession.js +1 -0
  9. package/dist/interfaces/IAgentSessionRepository.js +1 -0
  10. package/dist/interfaces/IBuilderTool.js +1 -0
  11. package/dist/interfaces/IDraxAgent.js +1 -0
  12. package/dist/interfaces/IDraxAgentController.js +1 -0
  13. package/dist/interfaces/IDraxAgentRoutes.js +1 -0
  14. package/dist/models/AgentSessionModel.js +31 -0
  15. package/dist/permissions/AgentPermissions.js +6 -0
  16. package/dist/permissions/AgentSessionPermissions.js +10 -0
  17. package/dist/repository/mongo/AgentSessionMongoRepository.js +13 -0
  18. package/dist/repository/sqlite/AgentSessionSqliteRepository.js +34 -0
  19. package/dist/routes/AgentSessionRoutes.js +21 -0
  20. package/dist/routes/ChatbotTaskRoutes.js +8 -0
  21. package/dist/routes/DraxAgentRoutes.js +9 -0
  22. package/dist/schemas/AgentSessionSchema.js +25 -0
  23. package/dist/services/AgentSessionService.js +9 -0
  24. package/dist/tools/BuilderTool.js +248 -0
  25. package/dist/tools/ToolBuilder.js +243 -0
  26. package/package.json +3 -3
  27. package/src/agents/DraxAgent.ts +574 -0
  28. package/src/controllers/AgentSessionController.ts +29 -0
  29. package/src/controllers/DraxAgentController.ts +135 -0
  30. package/src/factory/services/AgentSessionServiceFactory.ts +41 -0
  31. package/src/index.ts +58 -1
  32. package/src/interfaces/IAIProvider.ts +8 -0
  33. package/src/interfaces/IAgentSession.ts +44 -0
  34. package/src/interfaces/IAgentSessionRepository.ts +11 -0
  35. package/src/interfaces/IBuilderTool.ts +51 -0
  36. package/src/interfaces/IDraxAgent.ts +108 -0
  37. package/src/interfaces/IDraxAgentController.ts +5 -0
  38. package/src/interfaces/IDraxAgentRoutes.ts +7 -0
  39. package/src/models/AgentSessionModel.ts +46 -0
  40. package/src/permissions/AgentPermissions.ts +10 -0
  41. package/src/permissions/AgentSessionPermissions.ts +14 -0
  42. package/src/repository/mongo/AgentSessionMongoRepository.ts +22 -0
  43. package/src/repository/sqlite/AgentSessionSqliteRepository.ts +42 -0
  44. package/src/routes/AgentSessionRoutes.ts +38 -0
  45. package/src/routes/DraxAgentRoutes.ts +12 -0
  46. package/src/schemas/AgentSessionSchema.ts +30 -0
  47. package/src/services/AgentSessionService.ts +20 -0
  48. package/src/tools/BuilderTool.ts +289 -0
  49. package/test/DraxAgent.test.ts +221 -0
  50. package/test/ToolBuilder.test.ts +90 -0
  51. package/tsconfig.tsbuildinfo +1 -1
  52. package/types/agents/ChatbotTaskService.d.ts +42 -0
  53. package/types/agents/ChatbotTaskService.d.ts.map +1 -0
  54. package/types/agents/ChatbotTaskTools.d.ts +54 -0
  55. package/types/agents/ChatbotTaskTools.d.ts.map +1 -0
  56. package/types/agents/DraxAgent.d.ts +55 -0
  57. package/types/agents/DraxAgent.d.ts.map +1 -0
  58. package/types/controllers/AgentSessionController.d.ts +8 -0
  59. package/types/controllers/AgentSessionController.d.ts.map +1 -0
  60. package/types/controllers/DraxAgentController.d.ts +16 -0
  61. package/types/controllers/DraxAgentController.d.ts.map +1 -0
  62. package/types/factory/services/AgentSessionServiceFactory.d.ts +8 -0
  63. package/types/factory/services/AgentSessionServiceFactory.d.ts.map +1 -0
  64. package/types/index.d.ts +14 -2
  65. package/types/index.d.ts.map +1 -1
  66. package/types/interfaces/IAIProvider.d.ts +7 -1
  67. package/types/interfaces/IAIProvider.d.ts.map +1 -1
  68. package/types/interfaces/IAgentSession.d.ts +39 -0
  69. package/types/interfaces/IAgentSession.d.ts.map +1 -0
  70. package/types/interfaces/IAgentSessionRepository.d.ts +6 -0
  71. package/types/interfaces/IAgentSessionRepository.d.ts.map +1 -0
  72. package/types/interfaces/IBuilderTool.d.ts +26 -0
  73. package/types/interfaces/IBuilderTool.d.ts.map +1 -0
  74. package/types/interfaces/IDraxAgent.d.ts +74 -0
  75. package/types/interfaces/IDraxAgent.d.ts.map +1 -0
  76. package/types/interfaces/IDraxAgentController.d.ts +5 -0
  77. package/types/interfaces/IDraxAgentController.d.ts.map +1 -0
  78. package/types/interfaces/IDraxAgentRoutes.d.ts +6 -0
  79. package/types/interfaces/IDraxAgentRoutes.d.ts.map +1 -0
  80. package/types/models/AgentSessionModel.d.ts +15 -0
  81. package/types/models/AgentSessionModel.d.ts.map +1 -0
  82. package/types/permissions/AgentPermissions.d.ts +6 -0
  83. package/types/permissions/AgentPermissions.d.ts.map +1 -0
  84. package/types/permissions/AgentSessionPermissions.d.ts +10 -0
  85. package/types/permissions/AgentSessionPermissions.d.ts.map +1 -0
  86. package/types/repository/mongo/AgentSessionMongoRepository.d.ts +9 -0
  87. package/types/repository/mongo/AgentSessionMongoRepository.d.ts.map +1 -0
  88. package/types/repository/sqlite/AgentSessionSqliteRepository.d.ts +23 -0
  89. package/types/repository/sqlite/AgentSessionSqliteRepository.d.ts.map +1 -0
  90. package/types/routes/AgentSessionRoutes.d.ts +4 -0
  91. package/types/routes/AgentSessionRoutes.d.ts.map +1 -0
  92. package/types/routes/ChatbotTaskRoutes.d.ts +4 -0
  93. package/types/routes/ChatbotTaskRoutes.d.ts.map +1 -0
  94. package/types/routes/DraxAgentRoutes.d.ts +4 -0
  95. package/types/routes/DraxAgentRoutes.d.ts.map +1 -0
  96. package/types/schemas/AgentSessionSchema.d.ts +51 -0
  97. package/types/schemas/AgentSessionSchema.d.ts.map +1 -0
  98. package/types/services/AgentSessionService.d.ts +10 -0
  99. package/types/services/AgentSessionService.d.ts.map +1 -0
  100. package/types/tools/BuilderTool.d.ts +35 -0
  101. package/types/tools/BuilderTool.d.ts.map +1 -0
  102. package/types/tools/ToolBuilder.d.ts +47 -0
  103. package/types/tools/ToolBuilder.d.ts.map +1 -0
@@ -0,0 +1,38 @@
1
+
2
+ import AgentSessionController from "../controllers/AgentSessionController.js";
3
+ import {CrudSchemaBuilder} from "@drax/crud-back";
4
+ import {AgentSessionSchema, AgentSessionBaseSchema} from '../schemas/AgentSessionSchema.js'
5
+
6
+ async function AgentSessionRoutes(fastify, options) {
7
+
8
+ const controller: AgentSessionController = new AgentSessionController()
9
+ const schemas = new CrudSchemaBuilder(AgentSessionSchema, AgentSessionBaseSchema,AgentSessionBaseSchema, 'AgentSession', 'openapi-3.0', ['ai']);
10
+
11
+ fastify.get('/api/agentsession', {schema: schemas.paginateSchema}, (req,rep) => controller.paginate(req,rep))
12
+
13
+ fastify.get('/api/agentsession/find', {schema: schemas.findSchema}, (req,rep) => controller.find(req,rep))
14
+
15
+ fastify.get('/api/agentsession/search', {schema: schemas.searchSchema}, (req,rep) => controller.search(req,rep))
16
+
17
+ fastify.get('/api/agentsession/:id', {schema: schemas.findByIdSchema}, (req,rep) => controller.findById(req,rep))
18
+
19
+ fastify.get('/api/agentsession/find-one', {schema: schemas.findOneSchema}, (req,rep) => controller.findOne(req,rep))
20
+
21
+ fastify.get('/api/agentsession/group-by', {schema: schemas.groupBySchema}, (req,rep) => controller.groupBy(req,rep))
22
+
23
+ fastify.post('/api/agentsession', {schema: schemas.createSchema}, (req,rep) =>controller.create(req,rep))
24
+
25
+ fastify.put('/api/agentsession/:id', {schema: schemas.updateSchema}, (req,rep) =>controller.update(req,rep))
26
+
27
+ fastify.patch('/api/agentsession/:id', {schema: schemas.updateSchema}, (req,rep) =>controller.updatePartial(req,rep))
28
+
29
+ fastify.delete('/api/agentsession/:id', {schema: schemas.deleteSchema}, (req,rep) =>controller.delete(req,rep))
30
+
31
+ fastify.get('/api/agentsession/export', (req,rep) =>controller.export(req,rep))
32
+
33
+ fastify.post('/api/agentsession/import', (req,rep) => controller.import(req,rep))
34
+
35
+ }
36
+
37
+ export default AgentSessionRoutes;
38
+ export {AgentSessionRoutes}
@@ -0,0 +1,12 @@
1
+ import DraxAgentController from "../controllers/DraxAgentController.js";
2
+
3
+ async function DraxAgentRoutes(fastify, options:any) {
4
+ const controller = new DraxAgentController();
5
+ const prefix = "/api/ai/agent";
6
+
7
+ fastify.post(`${prefix}/session`, (req, rep) => controller.startSession(req, rep));
8
+ fastify.post(`${prefix}/message`, (req, rep) => controller.message(req, rep));
9
+ }
10
+
11
+ export default DraxAgentRoutes;
12
+ export {DraxAgentRoutes};
@@ -0,0 +1,30 @@
1
+ import {z} from 'zod';
2
+
3
+ const AgentSessionBaseSchema = z.object({
4
+ sessionId: z.string().min(1, 'validation.required'),
5
+ title: z.string().optional(),
6
+ lastMessageAt: z.coerce.date().nullable().optional(),
7
+ messages: z.array(
8
+ z.object({
9
+ role: z.enum(['user', 'assistant', 'system']),
10
+ content: z.string().min(1, 'validation.required'),
11
+ createdAt: z.coerce.date().nullable().optional()
12
+ })
13
+ ).optional(),
14
+ messageCount: z.number().nullable().optional(),
15
+ inputTokens: z.number().nullable().optional(),
16
+ outputTokens: z.number().nullable().optional(),
17
+ tokens: z.number().nullable().optional(),
18
+ tenant: z.coerce.string().optional().nullable(),
19
+ user: z.coerce.string().optional().nullable()
20
+ });
21
+
22
+ const AgentSessionSchema = AgentSessionBaseSchema
23
+ .extend({
24
+ _id: z.coerce.string(),
25
+ tenant: z.object({_id: z.coerce.string(), name: z.string().optional()}).nullable(),
26
+ user: z.object({_id: z.coerce.string(), username: z.string().optional()}).nullable()
27
+ })
28
+
29
+ export default AgentSessionSchema;
30
+ export {AgentSessionSchema, AgentSessionBaseSchema}
@@ -0,0 +1,20 @@
1
+
2
+ import type{IAgentSessionRepository} from "../interfaces/IAgentSessionRepository";
3
+ import type {IAgentSessionBase, IAgentSession} from "../interfaces/IAgentSession";
4
+ import {AbstractService} from "@drax/crud-back";
5
+ import type {ZodObject, ZodRawShape} from "zod";
6
+
7
+ class AgentSessionService extends AbstractService<IAgentSession, IAgentSessionBase, IAgentSessionBase> {
8
+
9
+
10
+ constructor(AgentSessionRepository: IAgentSessionRepository, baseSchema?: ZodObject<ZodRawShape>, fullSchema?: ZodObject<ZodRawShape>) {
11
+ super(AgentSessionRepository, baseSchema, fullSchema);
12
+
13
+ this._validateOutput = true
14
+
15
+ }
16
+
17
+ }
18
+
19
+ export default AgentSessionService
20
+ export {AgentSessionService}
@@ -0,0 +1,289 @@
1
+ import {z, type ZodObject, type ZodRawShape, type ZodTypeAny} from "zod";
2
+ import type {IPromptTool} from "../interfaces/IAIProvider.js";
3
+ import type {
4
+ ToolBuilderMethod,
5
+ ToolBuilderOptions,
6
+ ToolBuilderService,
7
+ ToolDefinition
8
+ } from "../interfaces/IBuilderTool.js";
9
+
10
+ const emptyParameters = {
11
+ type: "object",
12
+ properties: {},
13
+ additionalProperties: false,
14
+ };
15
+
16
+ const idSchema = z.object({
17
+ id: z.string().describe("Entity identifier"),
18
+ });
19
+
20
+ const filtersSchema = z.array(z.object({
21
+ field: z.string(),
22
+ operator: z.string(),
23
+ value: z.any(),
24
+ orGroup: z.string().optional(),
25
+ })).optional().describe("Optional Drax field filters");
26
+
27
+ const toolDefinitions: Record<ToolBuilderMethod, ToolDefinition> = {
28
+ create: {
29
+ description: entityName => `Crear un registro de ${entityName}`,
30
+ parameters: builder => builder.objectParameters(z.object({
31
+ data: builder.inputSchema.describe("Data for the entity to create"),
32
+ })),
33
+ execute: (service, args) => service.create?.(args.data),
34
+ },
35
+ update: {
36
+ description: entityName => `Reemplazar un registro de ${entityName} por id`,
37
+ parameters: builder => builder.objectParameters(z.object({
38
+ id: z.string().describe("Entity identifier"),
39
+ data: builder.inputSchema.describe("Complete replacement data"),
40
+ })),
41
+ execute: (service, args) => service.update?.(args.id, args.data),
42
+ },
43
+ updatePartial: {
44
+ description: entityName => `Actualizar parcialmente un registro de ${entityName} por id`,
45
+ parameters: builder => builder.objectParameters(z.object({
46
+ id: z.string().describe("Entity identifier"),
47
+ data: builder.partialInputSchema.describe("Partial data to update"),
48
+ })),
49
+ execute: (service, args) => service.updatePartial?.(args.id, args.data),
50
+ },
51
+ delete: {
52
+ description: entityName => `Eliminar un registro de ${entityName} por id`,
53
+ parameters: builder => builder.objectParameters(idSchema),
54
+ execute: (service, args) => service.delete?.(args.id),
55
+ },
56
+ findById: {
57
+ description: entityName => `Buscar un registro de ${entityName} por id`,
58
+ parameters: builder => builder.objectParameters(idSchema),
59
+ execute: (service, args) => service.findById?.(args.id),
60
+ },
61
+ findByIds: {
62
+ description: entityName => `Buscar multiples registros de ${entityName} por ids`,
63
+ parameters: builder => builder.objectParameters(z.object({
64
+ ids: z.array(z.string()).describe("Entity identifiers"),
65
+ })),
66
+ execute: (service, args) => service.findByIds?.(args.ids),
67
+ },
68
+ findOneBy: {
69
+ description: entityName => `Buscar un registro de ${entityName} por valor de campo`,
70
+ parameters: builder => builder.objectParameters(z.object({
71
+ field: z.string(),
72
+ value: z.any(),
73
+ filters: filtersSchema,
74
+ })),
75
+ execute: (service, args) => service.findOneBy?.(args.field, args.value, args.filters ?? []),
76
+ },
77
+ findOne: {
78
+ description: entityName => `Buscar el primer registro de ${entityName} que coincida con busqueda y filtros`,
79
+ parameters: builder => builder.objectParameters(z.object({
80
+ search: z.string().optional(),
81
+ filters: filtersSchema,
82
+ })),
83
+ execute: (service, args) => service.findOne?.({
84
+ search: args.search ?? "",
85
+ filters: args.filters ?? [],
86
+ }),
87
+ },
88
+ findBy: {
89
+ description: entityName => `Buscar registros de ${entityName} por valor de campo`,
90
+ parameters: builder => builder.objectParameters(z.object({
91
+ field: z.string(),
92
+ value: z.any(),
93
+ limit: z.number().optional(),
94
+ filters: filtersSchema,
95
+ })),
96
+ execute: (service, args) => service.findBy?.(args.field, args.value, args.limit ?? 1000, args.filters ?? []),
97
+ },
98
+ fetchAll: {
99
+ description: entityName => `Obtener todos los registros de ${entityName}`,
100
+ parameters: () => emptyParameters,
101
+ execute: service => service.fetchAll?.(),
102
+ },
103
+ search: {
104
+ description: entityName => `Buscar registros de ${entityName} por texto`,
105
+ parameters: builder => builder.objectParameters(z.object({
106
+ value: z.string().describe("Search text"),
107
+ limit: z.number().optional(),
108
+ filters: filtersSchema,
109
+ })),
110
+ execute: (service, args) => service.search?.(args.value, args.limit ?? 1000, args.filters ?? []),
111
+ },
112
+ find: {
113
+ description: entityName => `Buscar registros de ${entityName} usando opciones de listado`,
114
+ parameters: builder => builder.objectParameters(z.object({
115
+ orderBy: z.string().optional(),
116
+ order: z.union([z.enum(["asc", "desc"]), z.boolean()]).optional(),
117
+ search: z.string().optional(),
118
+ filters: filtersSchema,
119
+ limit: z.number().optional(),
120
+ })),
121
+ execute: (service, args) => service.find?.({
122
+ orderBy: args.orderBy ?? "",
123
+ order: args.order ?? false,
124
+ search: args.search ?? "",
125
+ filters: args.filters ?? [],
126
+ limit: args.limit ?? 0,
127
+ }),
128
+ },
129
+ paginate: {
130
+ description: entityName => `Paginar registros de ${entityName}`,
131
+ parameters: builder => builder.objectParameters(z.object({
132
+ page: z.number().optional(),
133
+ limit: z.number().optional(),
134
+ orderBy: z.string().optional(),
135
+ order: z.enum(["asc", "desc"]).optional(),
136
+ search: z.string().optional(),
137
+ filters: filtersSchema,
138
+ })),
139
+ execute: (service, args) => service.paginate({
140
+ page: args.page ?? 1,
141
+ limit: args.limit ?? 10,
142
+ orderBy: args.orderBy,
143
+ order: args.order ?? "asc",
144
+ search: args.search ?? "",
145
+ filters: args.filters ?? [],
146
+ }),
147
+ },
148
+ groupBy: {
149
+ description: entityName => `Agrupar registros de ${entityName} por campos`,
150
+ parameters: builder => builder.objectParameters(z.object({
151
+ fields: z.array(z.string()).optional(),
152
+ filters: filtersSchema,
153
+ dateFormat: z.enum(["year", "month", "day", "hour", "minute", "second"]).optional(),
154
+ })),
155
+ execute: (service, args) => service.groupBy?.({
156
+ fields: args.fields ?? [],
157
+ filters: args.filters ?? [],
158
+ dateFormat: args.dateFormat ?? "day",
159
+ }),
160
+ },
161
+ };
162
+
163
+ class BuilderTool<T = any, C = any, U = any> {
164
+ protected _inputSchema: ZodObject<ZodRawShape>;
165
+ protected _partialInputSchema: ZodObject<ZodRawShape>;
166
+ protected _outputSchema?: ZodObject<ZodRawShape>;
167
+
168
+ constructor(protected options: ToolBuilderOptions<T, C, U>) {
169
+ this._inputSchema = this.schemaAdapter(options.schema);
170
+ this._partialInputSchema = this._inputSchema.partial();
171
+ this._outputSchema = options.outputSchema ? this.schemaAdapter(options.outputSchema) : undefined;
172
+ }
173
+
174
+ get inputSchema() {
175
+ return this._inputSchema;
176
+ }
177
+
178
+ get partialInputSchema() {
179
+ return this._partialInputSchema;
180
+ }
181
+
182
+ get outputSchema() {
183
+ return this._outputSchema;
184
+ }
185
+
186
+ getTools(): IPromptTool[] {
187
+ return this.options.methods.map(method => this.buildTool(method));
188
+ }
189
+
190
+ getSystemPromptSection(): string {
191
+ const entityDescription = this.options.entityDescription
192
+ ? `\n${this.options.entityDescription}`
193
+ : "";
194
+
195
+ const tools = this.options.methods
196
+ .map(method => {
197
+ const definition = toolDefinitions[method];
198
+ return `- ${this.getToolName(method)}: ${definition.description(this.options.entityName)}`;
199
+ })
200
+ .join("\n");
201
+
202
+ const entitySchema = JSON.stringify(this.toJsonSchema(this._outputSchema ?? this._inputSchema));
203
+
204
+ return [
205
+ `[ENTIDAD: ${this.options.entityName}]${entityDescription}`,
206
+ `Schema JSON de la entidad: ${entitySchema}`,
207
+ "Tools disponibles:",
208
+ tools,
209
+ ].join("\n");
210
+ }
211
+
212
+ objectParameters(schema: ZodObject<ZodRawShape>) {
213
+ return this.toJsonSchema(this.schemaAdapter(schema));
214
+ }
215
+
216
+ protected buildTool(method: ToolBuilderMethod): IPromptTool {
217
+ const definition = toolDefinitions[method];
218
+ const serviceMethod = this.options.service[method];
219
+
220
+ if (typeof serviceMethod !== "function") {
221
+ throw new Error(`Tool method not available on service: ${method}`);
222
+ }
223
+
224
+ return {
225
+ name: this.getToolName(method),
226
+ description: definition.description(this.options.entityName),
227
+ parameters: definition.parameters(this),
228
+ navigation: {
229
+ entityName: this.options.entityName,
230
+ method,
231
+ basePath: this.options.navigationBasePath,
232
+ },
233
+ execute: async (args: any) => definition.execute(this.options.service, args),
234
+ };
235
+ }
236
+
237
+ protected getToolName(method: ToolBuilderMethod) {
238
+ return `${this.options.toolNamePrefix ?? this.options.entityName}_${method}`;
239
+ }
240
+
241
+ protected toJsonSchema(schema: ZodObject<ZodRawShape>) {
242
+ return z.toJSONSchema(schema, {target: "openAi"});
243
+ }
244
+
245
+ protected getTypeName(field: any): string | undefined {
246
+ return field?.constructor?.name;
247
+ }
248
+
249
+ protected fieldAdapter(field: unknown): ZodTypeAny {
250
+ const f: any = field;
251
+ const typeName = this.getTypeName(f);
252
+
253
+ if (typeof f?.unwrap === "function" && typeName === "ZodOptional") {
254
+ return this.fieldAdapter(f.unwrap()).optional();
255
+ }
256
+
257
+ if (typeof f?.unwrap === "function" && typeName === "ZodNullable") {
258
+ return this.fieldAdapter(f.unwrap()).nullable();
259
+ }
260
+
261
+ if (typeName === "ZodArray" && f?.element) {
262
+ return z.array(this.fieldAdapter(f.element));
263
+ }
264
+
265
+ if (typeName === "ZodObject" && f?.shape) {
266
+ return this.schemaAdapter(f);
267
+ }
268
+
269
+ if (typeName === "ZodDate") {
270
+ return z.iso.datetime();
271
+ }
272
+
273
+ return f as ZodTypeAny;
274
+ }
275
+
276
+ protected schemaAdapter<TObj extends ZodObject<ZodRawShape>>(schema: TObj): TObj {
277
+ const shape = (schema as any).shape as Record<string, unknown>;
278
+ const newShape: Record<string, ZodTypeAny> = {};
279
+
280
+ for (const key of Object.keys(shape)) {
281
+ newShape[key] = this.fieldAdapter(shape[key]);
282
+ }
283
+
284
+ return z.object(newShape) as unknown as TObj;
285
+ }
286
+ }
287
+
288
+ export default BuilderTool;
289
+ export {BuilderTool};
@@ -0,0 +1,221 @@
1
+ import {describe, expect, test} from "vitest";
2
+ import {DraxAgent} from "../src/agents/DraxAgent.js";
3
+ import type {IAIProvider, IPromptParams, IPromptResponse, IPromptTool} from "../src/interfaces/IAIProvider.js";
4
+
5
+ class MockProvider implements IAIProvider {
6
+ requests: IPromptParams[] = [];
7
+
8
+ async prompt(input: IPromptParams): Promise<IPromptResponse> {
9
+ this.requests.push(input);
10
+
11
+ if (input.tools && input.tools.length > 0) {
12
+ await input.tools[0].execute({value: "Ada"});
13
+ }
14
+
15
+ return {
16
+ output: "respuesta",
17
+ tokens: 10,
18
+ inputTokens: 6,
19
+ outputTokens: 4,
20
+ time: 12,
21
+ };
22
+ }
23
+ }
24
+
25
+ describe("DraxAgent", () => {
26
+ test("creates a session and sends messages through the injected provider", async () => {
27
+ const provider = new MockProvider();
28
+ const agent = DraxAgent.instance().clearSessions().configure({
29
+ provider,
30
+ systemPrompt: "Sos un asistente.",
31
+ sessionService: false,
32
+ });
33
+
34
+ const session = await agent.startSession({userId: "user-1", tenantId: "tenant-1"});
35
+ const response = await agent.sendMessage({
36
+ sessionId: session.id,
37
+ userId: "user-1",
38
+ tenantId: "tenant-1",
39
+ message: "Hola",
40
+ });
41
+
42
+ expect(response).toMatchObject({
43
+ sessionId: session.id,
44
+ message: "respuesta",
45
+ tokens: 10,
46
+ });
47
+ expect(provider.requests[0]).toMatchObject({
48
+ userInput: "Hola",
49
+ tenant: "tenant-1",
50
+ user: "user-1",
51
+ });
52
+ expect(provider.requests[0].systemPrompt).toContain("Sos un asistente.");
53
+ expect(provider.requests[0].systemPrompt).toContain("[CONTEXTO RBAC]");
54
+ expect(provider.requests[0].systemPrompt).toContain("tenantId: tenant-1");
55
+ expect(provider.requests[0].systemPrompt).toContain("userId: user-1");
56
+ expect(provider.requests[0].systemPrompt).toContain("tenant, tenantId, user, userId o createdBy");
57
+ });
58
+
59
+ test("injects builder tools, custom tools and builder prompt sections", async () => {
60
+ const calls: any[] = [];
61
+ const provider = new MockProvider();
62
+ const customTool: IPromptTool = {
63
+ name: "custom_search",
64
+ description: "Buscar datos custom",
65
+ parameters: {
66
+ type: "object",
67
+ properties: {
68
+ value: {type: "string"},
69
+ },
70
+ },
71
+ execute: async args => {
72
+ calls.push(args);
73
+ return [{name: args.value}];
74
+ },
75
+ };
76
+
77
+ const agent = DraxAgent.instance().clearSessions().configure({
78
+ provider,
79
+ systemPrompt: "Prompt base.",
80
+ sessionService: false,
81
+ toolBuilders: [{
82
+ getTools: () => [],
83
+ getSystemPromptSection: () => "[ENTIDAD: person]\nTools disponibles:",
84
+ }],
85
+ tools: [customTool],
86
+ });
87
+
88
+ await agent.sendMessage({
89
+ userId: "user-1",
90
+ message: "Busca Ada",
91
+ });
92
+
93
+ expect(provider.requests[0].systemPrompt).toContain("Prompt base.");
94
+ expect(provider.requests[0].systemPrompt).toContain("[ENTIDADES Y TOOLS]");
95
+ expect(provider.requests[0].systemPrompt).toContain("[ENTIDAD: person]");
96
+ expect(provider.requests[0].tools?.map(tool => tool.name)).toEqual(["custom_search"]);
97
+ expect(calls).toEqual([{value: "Ada"}]);
98
+ });
99
+
100
+ test("uses previous session messages as history", async () => {
101
+ const provider = new MockProvider();
102
+ const agent = DraxAgent.instance().clearSessions().configure({
103
+ provider,
104
+ systemPrompt: "Sos un asistente.",
105
+ sessionService: false,
106
+ toolBuilders: undefined,
107
+ tools: undefined,
108
+ });
109
+
110
+ const first = await agent.sendMessage({
111
+ userId: "user-1",
112
+ message: "Primer mensaje",
113
+ });
114
+ await agent.sendMessage({
115
+ sessionId: first.sessionId,
116
+ userId: "user-1",
117
+ message: "Segundo mensaje",
118
+ });
119
+
120
+ expect(provider.requests[1].history).toEqual([
121
+ {role: "user", content: "Primer mensaje"},
122
+ {role: "assistant", content: "respuesta"},
123
+ ]);
124
+ });
125
+
126
+ test("returns a navigation path from tool execution metadata", async () => {
127
+ const provider = new MockProvider();
128
+ const agent = DraxAgent.instance().clearSessions().configure({
129
+ provider,
130
+ systemPrompt: "Sos un asistente.",
131
+ sessionService: false,
132
+ toolBuilders: undefined,
133
+ tools: [{
134
+ name: "task_findOne",
135
+ description: "Buscar una tarea",
136
+ parameters: {
137
+ type: "object",
138
+ properties: {
139
+ value: {type: "string"},
140
+ },
141
+ },
142
+ navigation: {
143
+ entityName: "task",
144
+ method: "findOne",
145
+ },
146
+ execute: async () => ({id: "task-1", title: "Ada"}),
147
+ }],
148
+ });
149
+
150
+ const response = await agent.sendMessage({
151
+ userId: "user-1",
152
+ message: "Busca Ada",
153
+ });
154
+
155
+ expect(response.navigationPath).toBe("/crud/task?mode=view&id=task-1");
156
+ });
157
+
158
+ test("persists agent session creation and message updates", async () => {
159
+ const provider = new MockProvider();
160
+ const created: any[] = [];
161
+ const updates: any[] = [];
162
+ const sessionService = {
163
+ create: async (data: any) => {
164
+ created.push(data);
165
+ return {_id: "agent-session-1", ...data};
166
+ },
167
+ updatePartial: async (id: string, data: any) => {
168
+ updates.push({id, data});
169
+ return {_id: id, ...data};
170
+ },
171
+ findBy: async () => [],
172
+ findById: async () => ({
173
+ _id: "agent-session-1",
174
+ sessionId: "session-1",
175
+ inputTokens: 1,
176
+ outputTokens: 2,
177
+ tokens: 3,
178
+ }),
179
+ } as any;
180
+
181
+ const agent = DraxAgent.instance().clearSessions().configure({
182
+ provider,
183
+ systemPrompt: "Sos un asistente.",
184
+ sessionService,
185
+ toolBuilders: undefined,
186
+ tools: undefined,
187
+ });
188
+
189
+ const response = await agent.sendMessage({
190
+ sessionId: "session-1",
191
+ userId: "user-1",
192
+ tenantId: "tenant-1",
193
+ message: "Hola",
194
+ });
195
+
196
+ expect(response.sessionId).toBe("session-1");
197
+ expect(created[0]).toMatchObject({
198
+ sessionId: "session-1",
199
+ tenant: "tenant-1",
200
+ user: "user-1",
201
+ messageCount: 0,
202
+ });
203
+ expect(updates[0]).toMatchObject({
204
+ id: "agent-session-1",
205
+ data: {
206
+ sessionId: "session-1",
207
+ tenant: "tenant-1",
208
+ user: "user-1",
209
+ title: "Hola",
210
+ messageCount: 2,
211
+ inputTokens: 7,
212
+ outputTokens: 6,
213
+ tokens: 13,
214
+ },
215
+ });
216
+ expect(updates[0].data.messages).toEqual([
217
+ expect.objectContaining({role: "user", content: "Hola"}),
218
+ expect.objectContaining({role: "assistant", content: "respuesta"}),
219
+ ]);
220
+ });
221
+ });