@drax/ai-back 3.0.0 → 3.17.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 (87) hide show
  1. package/dist/config/OpenAiConfig.js +1 -0
  2. package/dist/controllers/AIController.js +150 -0
  3. package/dist/controllers/AICrudController.js +150 -0
  4. package/dist/controllers/AIGenericController.js +83 -0
  5. package/dist/controllers/AILogController.js +18 -0
  6. package/dist/factory/AiProviderFactory.js +1 -1
  7. package/dist/factory/OpenAiProviderFactory.js +2 -1
  8. package/dist/factory/services/AILogServiceFactory.js +30 -0
  9. package/dist/index.js +14 -1
  10. package/dist/interfaces/IAILog.js +1 -0
  11. package/dist/interfaces/IAILogRepository.js +1 -0
  12. package/dist/models/AILogModel.js +50 -0
  13. package/dist/permissions/AILogPermissions.js +10 -0
  14. package/dist/permissions/AIPermissions.js +7 -0
  15. package/dist/providers/OpenAiProvider.js +176 -26
  16. package/dist/repository/mongo/AILogMongoRepository.js +13 -0
  17. package/dist/repository/sqlite/AILogSqliteRepository.js +45 -0
  18. package/dist/routes/AILogRoutes.js +21 -0
  19. package/dist/routes/AIRoutes.js +10 -0
  20. package/dist/schemas/AILogSchema.js +44 -0
  21. package/dist/services/AILogService.js +9 -0
  22. package/package.json +8 -2
  23. package/src/config/OpenAiConfig.ts +1 -0
  24. package/src/controllers/AICrudController.ts +168 -0
  25. package/src/controllers/AIGenericController.ts +97 -0
  26. package/src/controllers/AILogController.ts +29 -0
  27. package/src/factory/AiProviderFactory.ts +1 -1
  28. package/src/factory/OpenAiProviderFactory.ts +4 -2
  29. package/src/factory/services/AILogServiceFactory.ts +41 -0
  30. package/src/index.ts +47 -1
  31. package/src/interfaces/IAILogRepository.ts +11 -0
  32. package/src/interfaces/IAIProvider.ts +48 -2
  33. package/src/models/AILogModel.ts +65 -0
  34. package/src/permissions/AILogPermissions.ts +14 -0
  35. package/src/permissions/AIPermissions.ts +11 -0
  36. package/src/providers/OpenAiProvider.ts +231 -29
  37. package/src/repository/mongo/AILogMongoRepository.ts +22 -0
  38. package/src/repository/sqlite/AILogSqliteRepository.ts +53 -0
  39. package/src/routes/AILogRoutes.ts +38 -0
  40. package/src/routes/AIRoutes.ts +18 -0
  41. package/src/schemas/AILogSchema.ts +52 -0
  42. package/src/services/AILogService.ts +20 -0
  43. package/test/OpenAiProvider.test.ts +91 -0
  44. package/tsconfig.tsbuildinfo +1 -1
  45. package/types/config/OpenAiConfig.d.ts +2 -1
  46. package/types/config/OpenAiConfig.d.ts.map +1 -1
  47. package/types/controllers/AIController.d.ts +25 -0
  48. package/types/controllers/AIController.d.ts.map +1 -0
  49. package/types/controllers/AICrudController.d.ts +25 -0
  50. package/types/controllers/AICrudController.d.ts.map +1 -0
  51. package/types/controllers/AIGenericController.d.ts +7 -0
  52. package/types/controllers/AIGenericController.d.ts.map +1 -0
  53. package/types/controllers/AILogController.d.ts +8 -0
  54. package/types/controllers/AILogController.d.ts.map +1 -0
  55. package/types/factory/AiProviderFactory.d.ts +1 -1
  56. package/types/factory/AiProviderFactory.d.ts.map +1 -1
  57. package/types/factory/OpenAiProviderFactory.d.ts.map +1 -1
  58. package/types/factory/services/AILogServiceFactory.d.ts +8 -0
  59. package/types/factory/services/AILogServiceFactory.d.ts.map +1 -0
  60. package/types/index.d.ts +17 -3
  61. package/types/index.d.ts.map +1 -1
  62. package/types/interfaces/IAILog.d.ts +77 -0
  63. package/types/interfaces/IAILog.d.ts.map +1 -0
  64. package/types/interfaces/IAILogRepository.d.ts +6 -0
  65. package/types/interfaces/IAILogRepository.d.ts.map +1 -0
  66. package/types/interfaces/IAIProvider.d.ts +32 -2
  67. package/types/interfaces/IAIProvider.d.ts.map +1 -1
  68. package/types/models/AILogModel.d.ts +15 -0
  69. package/types/models/AILogModel.d.ts.map +1 -0
  70. package/types/permissions/AILogPermissions.d.ts +10 -0
  71. package/types/permissions/AILogPermissions.d.ts.map +1 -0
  72. package/types/permissions/AIPermissions.d.ts +7 -0
  73. package/types/permissions/AIPermissions.d.ts.map +1 -0
  74. package/types/providers/OpenAiProvider.d.ts +71 -2
  75. package/types/providers/OpenAiProvider.d.ts.map +1 -1
  76. package/types/repository/mongo/AILogMongoRepository.d.ts +9 -0
  77. package/types/repository/mongo/AILogMongoRepository.d.ts.map +1 -0
  78. package/types/repository/sqlite/AILogSqliteRepository.d.ts +23 -0
  79. package/types/repository/sqlite/AILogSqliteRepository.d.ts.map +1 -0
  80. package/types/routes/AILogRoutes.d.ts +4 -0
  81. package/types/routes/AILogRoutes.d.ts.map +1 -0
  82. package/types/routes/AIRoutes.d.ts +4 -0
  83. package/types/routes/AIRoutes.d.ts.map +1 -0
  84. package/types/schemas/AILogSchema.d.ts +81 -0
  85. package/types/schemas/AILogSchema.d.ts.map +1 -0
  86. package/types/services/AILogService.d.ts +10 -0
  87. package/types/services/AILogService.d.ts.map +1 -0
@@ -1,7 +1,7 @@
1
1
  import OpenAI from "openai";
2
2
  import { zodResponseFormat } from "openai/helpers/zod";
3
3
  class OpenAiProvider {
4
- constructor(apiKey, model) {
4
+ constructor(apiKey, model, visionModel, aiLogService) {
5
5
  if (!apiKey) {
6
6
  throw new Error("OpenAI apiKey required");
7
7
  }
@@ -10,6 +10,8 @@ class OpenAiProvider {
10
10
  }
11
11
  this._apiKey = apiKey;
12
12
  this._model = model;
13
+ this._visionModel = visionModel;
14
+ this._aiLogService = aiLogService;
13
15
  }
14
16
  get model() {
15
17
  if (!this._model) {
@@ -25,6 +27,127 @@ class OpenAiProvider {
25
27
  }
26
28
  return this._client;
27
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({ type: 'text', text: input.userInput });
41
+ }
42
+ content.push(...input.userImages.map(image => ({
43
+ type: 'image_url',
44
+ image_url: {
45
+ url: image.url,
46
+ ...(image.detail ? { detail: image.detail } : {}),
47
+ }
48
+ })));
49
+ return content;
50
+ }
51
+ return input.userInput ?? "";
52
+ }
53
+ mapContentParts(content) {
54
+ return content.map(part => {
55
+ if (part.type === 'text') {
56
+ return {
57
+ type: 'text',
58
+ text: part.text
59
+ };
60
+ }
61
+ return {
62
+ type: 'image_url',
63
+ image_url: {
64
+ url: part.imageUrl,
65
+ ...(part.detail ? { detail: part.detail } : {}),
66
+ }
67
+ };
68
+ });
69
+ }
70
+ mapHistory(history = []) {
71
+ return history.map(message => ({
72
+ role: message.role,
73
+ content: typeof message.content === 'string'
74
+ ? message.content
75
+ : this.mapContentParts(message.content)
76
+ }));
77
+ }
78
+ hasImageInput(input) {
79
+ if (input.userImages && input.userImages.length > 0) {
80
+ return true;
81
+ }
82
+ if (input.userContent?.some(part => part.type === 'image')) {
83
+ return true;
84
+ }
85
+ return input.history?.some(message => Array.isArray(message.content) && message.content.some(part => part.type === 'image')) ?? false;
86
+ }
87
+ serializePromptInput(input, systemPrompt) {
88
+ return JSON.stringify({
89
+ systemPrompt,
90
+ history: input.history,
91
+ userInput: input.userInput,
92
+ userContent: input.userContent,
93
+ memory: input.memory,
94
+ knowledgeBase: input.knowledgeBase,
95
+ });
96
+ }
97
+ serializePromptOutput(output) {
98
+ if (typeof output === "string") {
99
+ return output;
100
+ }
101
+ if (output === null || output === undefined) {
102
+ return undefined;
103
+ }
104
+ return JSON.stringify(output);
105
+ }
106
+ buildLogPayload(input, params) {
107
+ return {
108
+ provider: "openai",
109
+ model: params.model,
110
+ operationTitle: input.operationTitle,
111
+ operationGroup: input.operationGroup,
112
+ ip: input.ip,
113
+ userAgent: input.userAgent,
114
+ input: this.serializePromptInput(input, params.systemPrompt),
115
+ inputImages: input.userImages?.map(image => ({
116
+ url: image.url,
117
+ })) ?? input.userContent
118
+ ?.filter(part => part.type === "image")
119
+ .map(part => ({
120
+ url: part.imageUrl,
121
+ })),
122
+ inputFiles: input.inputFiles,
123
+ inputTokens: params.inputTokens,
124
+ outputTokens: params.outputTokens,
125
+ tokens: params.tokens,
126
+ startedAt: params.startedAt,
127
+ endedAt: params.endedAt,
128
+ responseTime: params.endedAt ? `${params.endedAt.getTime() - params.startedAt.getTime()}ms` : undefined,
129
+ output: this.serializePromptOutput(params.output),
130
+ success: params.success,
131
+ errorMessage: params.errorMessage,
132
+ tenant: input.tenant,
133
+ user: input.user,
134
+ };
135
+ }
136
+ async registerPromptLog(input, params) {
137
+ if (!this._aiLogService) {
138
+ return;
139
+ }
140
+ try {
141
+ await this._aiLogService.create(this.buildLogPayload(input, params));
142
+ }
143
+ catch (e) {
144
+ console.error("Error registerPromptLog", {
145
+ name: e?.name,
146
+ message: e?.message,
147
+ stack: e?.stack,
148
+ });
149
+ }
150
+ }
28
151
  async generateEmbedding({ text, model = "text-embedding-ada-002" }) {
29
152
  const response = await this.client.embeddings.create({
30
153
  model: model,
@@ -36,7 +159,6 @@ class OpenAiProvider {
36
159
  if (!input.systemPrompt) {
37
160
  throw new Error("systemPrompt required");
38
161
  }
39
- const model = input.model ?? this.model;
40
162
  let systemPrompt = input.systemPrompt;
41
163
  if (input.memory && input.memory.length > 0) {
42
164
  systemPrompt += `\n\n ${input.memoryHeader ?? '[MEMORIA]'}\n ${input.memory.map(m => `${m.key}: ${m.value}`).join('\n')}`;
@@ -44,31 +166,59 @@ class OpenAiProvider {
44
166
  if (input.knowledgeBase && input.knowledgeBase.length > 0) {
45
167
  systemPrompt += `\n\n${input.knowledgeBaseHeader ?? '[BASE DE CONOCIMIENTO]'}\n ${input.knowledgeBase.join('\n')}`;
46
168
  }
47
- let userInput = input.userInput;
169
+ const userInput = this.buildUserContent(input);
170
+ const model = input.model ?? (this.hasImageInput(input) ? this.visionModel ?? this.model : this.model);
171
+ const startedAt = new Date();
48
172
  const startTime = performance.now();
49
- const chatCompletion = await this.client.chat.completions.create({
50
- messages: [
51
- { role: 'system', content: systemPrompt },
52
- ...(input.history ? input.history : []),
53
- { role: 'user', content: userInput },
54
- ],
55
- ...(input.zodSchema ? { response_format: zodResponseFormat(input.zodSchema, "event") } : {}),
56
- ...(input.jsonSchema ? { response_format: input.jsonSchema } : {}),
57
- model: model,
58
- });
59
- const output = chatCompletion.choices[0].message.content;
60
- const tokens = chatCompletion.usage.total_tokens;
61
- const inputTokens = chatCompletion.usage.prompt_tokens;
62
- const outputTokens = chatCompletion.usage.completion_tokens;
63
- const endTime = performance.now();
64
- const time = endTime - startTime;
65
- return {
66
- output,
67
- tokens,
68
- inputTokens,
69
- outputTokens,
70
- time
71
- };
173
+ 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;
188
+ const endTime = performance.now();
189
+ const time = endTime - startTime;
190
+ const endedAt = new Date();
191
+ await this.registerPromptLog(input, {
192
+ model,
193
+ systemPrompt,
194
+ startedAt,
195
+ endedAt,
196
+ inputTokens,
197
+ outputTokens,
198
+ tokens,
199
+ output,
200
+ success: true,
201
+ });
202
+ return {
203
+ output,
204
+ tokens,
205
+ inputTokens,
206
+ outputTokens,
207
+ time
208
+ };
209
+ }
210
+ catch (e) {
211
+ const endedAt = new Date();
212
+ await this.registerPromptLog(input, {
213
+ model,
214
+ systemPrompt,
215
+ startedAt,
216
+ endedAt,
217
+ success: false,
218
+ errorMessage: e?.message,
219
+ });
220
+ throw e;
221
+ }
72
222
  }
73
223
  }
74
224
  export default OpenAiProvider;
@@ -0,0 +1,13 @@
1
+ import { AbstractMongoRepository } from "@drax/crud-back";
2
+ import { AILogModel } from "../../models/AILogModel.js";
3
+ class AILogMongoRepository extends AbstractMongoRepository {
4
+ constructor() {
5
+ super();
6
+ this._model = AILogModel;
7
+ this._searchFields = ['provider', 'model', 'operationTitle', 'operationGroup', 'ip', 'userAgent', 'input', 'output', 'errorMessage'];
8
+ this._populateFields = ['tenant', 'user'];
9
+ this._lean = true;
10
+ }
11
+ }
12
+ export default AILogMongoRepository;
13
+ export { AILogMongoRepository };
@@ -0,0 +1,45 @@
1
+ import { AbstractSqliteRepository } from "@drax/crud-back";
2
+ class AILogSqliteRepository extends AbstractSqliteRepository {
3
+ constructor() {
4
+ super(...arguments);
5
+ this.tableName = 'AILog';
6
+ this.searchFields = ['provider', 'model', 'operationTitle', 'operationGroup', 'ip', 'userAgent', 'input', 'output', 'errorMessage'];
7
+ this.booleanFields = ['success'];
8
+ this.jsonFields = ['inputImages', 'inputFiles'];
9
+ this.identifier = '_id';
10
+ this.populateFields = [
11
+ { field: 'tenant', table: 'tenant', identifier: '_id' },
12
+ { field: 'user', table: 'user', identifier: '_id' }
13
+ ];
14
+ this.verbose = false;
15
+ this.tableFields = [
16
+ { name: "provider", type: "TEXT", unique: false, primary: false },
17
+ { name: "model", type: "TEXT", unique: false, primary: false },
18
+ { name: "operationTitle", type: "TEXT", unique: false, primary: false },
19
+ { name: "operationGroup", type: "TEXT", unique: false, primary: false },
20
+ { name: "ip", type: "TEXT", unique: false, primary: false },
21
+ { name: "userAgent", type: "TEXT", unique: false, primary: false },
22
+ { name: "input", type: "TEXT", unique: false, primary: false },
23
+ { name: "inputImages", type: "TEXT", unique: false, primary: false },
24
+ { name: "inputFiles", type: "TEXT", unique: false, primary: false },
25
+ { name: "inputTokens", type: "INTEGER", unique: false, primary: false },
26
+ { name: "inputTokens", type: "TEXT", unique: false, primary: false },
27
+ { name: "outputTokens", type: "INTEGER", unique: false, primary: false },
28
+ { name: "outputTokens", type: "TEXT", unique: false, primary: false },
29
+ { name: "tokens", type: "INTEGER", unique: false, primary: false },
30
+ { name: "tokens", type: "TEXT", unique: false, primary: false },
31
+ { name: "startedAt", type: "TEXT", unique: false, primary: false },
32
+ { name: "endedAt", type: "TEXT", unique: false, primary: false },
33
+ { name: "responseTime", type: "TEXT", unique: false, primary: false },
34
+ { name: "output", type: "TEXT", unique: false, primary: false },
35
+ { name: "success", type: "TEXT", unique: false, primary: false },
36
+ { name: "statusCode", type: "INTEGER", unique: false, primary: false },
37
+ { name: "statusCode", type: "TEXT", unique: false, primary: false },
38
+ { name: "errorMessage", type: "TEXT", unique: false, primary: false },
39
+ { name: "tenant", type: "TEXT", unique: false, primary: false },
40
+ { name: "user", type: "TEXT", unique: false, primary: false }
41
+ ];
42
+ }
43
+ }
44
+ export default AILogSqliteRepository;
45
+ export { AILogSqliteRepository };
@@ -0,0 +1,21 @@
1
+ import AILogController from "../controllers/AILogController.js";
2
+ import { CrudSchemaBuilder } from "@drax/crud-back";
3
+ import { AILogSchema, AILogBaseSchema } from '../schemas/AILogSchema.js';
4
+ async function AILogFastifyRoutes(fastify, options) {
5
+ const controller = new AILogController();
6
+ const schemas = new CrudSchemaBuilder(AILogSchema, AILogBaseSchema, AILogBaseSchema, 'AILog', 'openapi-3.0', ['ai']);
7
+ fastify.get('/api/ailog', { schema: schemas.paginateSchema }, (req, rep) => controller.paginate(req, rep));
8
+ fastify.get('/api/ailog/find', { schema: schemas.findSchema }, (req, rep) => controller.find(req, rep));
9
+ fastify.get('/api/ailog/search', { schema: schemas.searchSchema }, (req, rep) => controller.search(req, rep));
10
+ fastify.get('/api/ailog/:id', { schema: schemas.findByIdSchema }, (req, rep) => controller.findById(req, rep));
11
+ fastify.get('/api/ailog/find-one', { schema: schemas.findOneSchema }, (req, rep) => controller.findOne(req, rep));
12
+ fastify.get('/api/ailog/group-by', { schema: schemas.groupBySchema }, (req, rep) => controller.groupBy(req, rep));
13
+ fastify.post('/api/ailog', { schema: schemas.createSchema }, (req, rep) => controller.create(req, rep));
14
+ fastify.put('/api/ailog/:id', { schema: schemas.updateSchema }, (req, rep) => controller.update(req, rep));
15
+ fastify.patch('/api/ailog/:id', { schema: schemas.updateSchema }, (req, rep) => controller.updatePartial(req, rep));
16
+ fastify.delete('/api/ailog/:id', { schema: schemas.deleteSchema }, (req, rep) => controller.delete(req, rep));
17
+ fastify.get('/api/ailog/export', (req, rep) => controller.export(req, rep));
18
+ fastify.post('/api/ailog/import', (req, rep) => controller.import(req, rep));
19
+ }
20
+ export default AILogFastifyRoutes;
21
+ export { AILogFastifyRoutes };
@@ -0,0 +1,10 @@
1
+ import AICrudController from "../controllers/AICrudController.js";
2
+ import AIGenericController from "../controllers/AIGenericController.js";
3
+ async function AiFastifyRoutes(fastify, options) {
4
+ const crudController = new AICrudController();
5
+ const genericController = new AIGenericController();
6
+ fastify.post('/api/ai/prompt/crud', (req, rep) => crudController.prompt(req, rep));
7
+ fastify.post('/api/ai/prompt/generic', (req, rep) => genericController.prompt(req, rep));
8
+ }
9
+ export default AiFastifyRoutes;
10
+ export { AiFastifyRoutes };
@@ -0,0 +1,44 @@
1
+ import { z } from 'zod';
2
+ const AILogBaseSchema = z.object({
3
+ provider: z.string().optional(),
4
+ model: z.string().optional(),
5
+ operationTitle: z.string().optional(),
6
+ operationGroup: z.string().optional(),
7
+ ip: z.string().optional(),
8
+ userAgent: z.string().optional(),
9
+ input: z.string().optional(),
10
+ inputImages: z.array(z.object({
11
+ filename: z.string().optional(),
12
+ filepath: z.string().optional(),
13
+ size: z.number().nullable().optional(),
14
+ mimetype: z.string().optional(),
15
+ url: z.string().optional()
16
+ })).optional(),
17
+ inputFiles: z.array(z.object({
18
+ filename: z.string().optional(),
19
+ filepath: z.string().optional(),
20
+ size: z.number().nullable().optional(),
21
+ mimetype: z.string().optional(),
22
+ url: z.string().optional()
23
+ })).optional(),
24
+ inputTokens: z.number().nullable().optional(),
25
+ outputTokens: z.number().nullable().optional(),
26
+ tokens: z.number().nullable().optional(),
27
+ startedAt: z.coerce.date().nullable().optional(),
28
+ endedAt: z.coerce.date().nullable().optional(),
29
+ responseTime: z.string().optional(),
30
+ output: z.string().optional(),
31
+ success: z.boolean().optional(),
32
+ statusCode: z.number().nullable().optional(),
33
+ errorMessage: z.string().optional(),
34
+ tenant: z.coerce.string().optional().nullable(),
35
+ user: z.coerce.string().optional().nullable()
36
+ });
37
+ const AILogSchema = AILogBaseSchema
38
+ .extend({
39
+ _id: z.coerce.string(),
40
+ tenant: z.object({ _id: z.coerce.string(), name: z.string() }).nullable().optional(),
41
+ user: z.object({ _id: z.coerce.string(), username: z.string() }).nullable().optional()
42
+ });
43
+ export default AILogSchema;
44
+ export { AILogSchema, AILogBaseSchema };
@@ -0,0 +1,9 @@
1
+ import { AbstractService } from "@drax/crud-back";
2
+ class AILogService extends AbstractService {
3
+ constructor(AILogRepository, baseSchema, fullSchema) {
4
+ super(AILogRepository, baseSchema, fullSchema);
5
+ this._validateOutput = true;
6
+ }
7
+ }
8
+ export default AILogService;
9
+ export { AILogService };
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "3.0.0",
6
+ "version": "3.17.0",
7
7
  "description": "Ai utils",
8
8
  "main": "dist/index.js",
9
9
  "types": "types/index.d.ts",
@@ -17,6 +17,12 @@
17
17
  },
18
18
  "author": "Cristian Incarnato & Drax Team",
19
19
  "license": "ISC",
20
+ "dependencies": {
21
+ "@drax/ai-share": "^3.17.0",
22
+ "@drax/crud-back": "^3.17.0",
23
+ "mongoose": "^8.23.0",
24
+ "mongoose-paginate-v2": "^1.8.3"
25
+ },
20
26
  "peerDependencies": {
21
27
  "jsdom": "^26.0.0",
22
28
  "office-text-extractor": "^3.0.3",
@@ -38,5 +44,5 @@
38
44
  "typescript": "^5.9.3",
39
45
  "vitest": "^3.0.8"
40
46
  },
41
- "gitHead": "63ae718b24ea25ae80b1a9a5dfb84a3abbb95199"
47
+ "gitHead": "d8a056b087d67157bc8c2e684b7e6696399d06a4"
42
48
  }
@@ -3,6 +3,7 @@ enum OpenAiConfig {
3
3
 
4
4
  OpenAiApiKey = "OPENAI_API_KEY",
5
5
  OpenAiModel = "OPENAI_MODEL",
6
+ OpenAiVisionModel = "OPENAI_VISION_MODEL",
6
7
 
7
8
  }
8
9
 
@@ -0,0 +1,168 @@
1
+ import {z} from "zod";
2
+ import {CommonController} from "@drax/common-back";
3
+ import AiProviderFactory from "../factory/AiProviderFactory.js";
4
+ import AIPermissions from "../permissions/AIPermissions.js";
5
+
6
+ const CrudAiFieldSchema: z.ZodType<any> = z.lazy(() => z.object({
7
+ name: z.string(),
8
+ type: z.string(),
9
+ label: z.string(),
10
+ hint: z.string().nullable().default(null),
11
+ placeholder: z.string().nullable().default(null),
12
+ readonly: z.boolean().nullable().default(null),
13
+ default: z.any().nullable().default(null),
14
+ enum: z.array(z.string()).nullable().default(null),
15
+ items: z.array(z.object({
16
+ title: z.string().nullable().default(null),
17
+ value: z.any().nullable().default(null),
18
+ })).nullable().default(null),
19
+ ref: z.string().nullable().default(null),
20
+ refDisplay: z.string().nullable().default(null),
21
+ objectFields: z.array(CrudAiFieldSchema).nullable().default(null),
22
+ }))
23
+
24
+ const PromptRequestSchema = z.object({
25
+ prompt: z.string().min(1),
26
+ operation: z.enum(["create", "edit"]).default("create"),
27
+ entity: z.object({
28
+ name: z.string(),
29
+ identifier: z.string().optional().nullable(),
30
+ }),
31
+ currentValues: z.record(z.string(), z.any()).default({}),
32
+ fields: z.array(CrudAiFieldSchema).min(1),
33
+ })
34
+
35
+ class AICrudController extends CommonController {
36
+
37
+ protected buildFieldValueSchema(field: any): z.ZodTypeAny {
38
+ switch (field.type) {
39
+ case "number":
40
+ return z.number().nullable().default(null)
41
+ case "boolean":
42
+ return z.boolean().nullable().default(null)
43
+ case "array.string":
44
+ case "array.enum":
45
+ case "array.ref":
46
+ return z.array(z.string()).nullable().default(null)
47
+ case "array.number":
48
+ return z.array(z.number()).nullable().default(null)
49
+ case "object":
50
+ if (field.objectFields && Array.isArray(field.objectFields) && field.objectFields.length > 0) {
51
+ return z.object(this.buildFieldShape(field.objectFields)).nullable().default(null)
52
+ }
53
+ return z.string().nullable().default(null)
54
+ case "record":
55
+ case "array.object":
56
+ case "array.record":
57
+ case "array.fullFile":
58
+ case "file":
59
+ case "fullFile":
60
+ return z.string().nullable().default(null)
61
+ case "id":
62
+ case "string":
63
+ case "longString":
64
+ case "date":
65
+ case "ref":
66
+ case "enum":
67
+ case "select":
68
+ case "password":
69
+ default:
70
+ return z.string().nullable().default(null)
71
+ }
72
+ }
73
+
74
+ protected buildFieldShape(fields: any[]): Record<string, z.ZodTypeAny> {
75
+ return fields.reduce((acc, field) => {
76
+ acc[field.name] = this.buildFieldValueSchema(field)
77
+ return acc
78
+ }, {} as Record<string, z.ZodTypeAny>)
79
+ }
80
+
81
+ protected buildSystemPrompt(input: z.infer<typeof PromptRequestSchema>) {
82
+ return [
83
+ "Sos un asistente de formularios para un sistema CRUD.",
84
+ "Tu tarea es proponer valores JSON para completar o editar una entidad.",
85
+ "Debes respetar exactamente los nombres de campo entregados.",
86
+ "No inventes campos adicionales.",
87
+ "Si un campo no tiene una propuesta razonable, omitilo.",
88
+ "Usa tipos compatibles con JSON.",
89
+ "Para campos enum o select, elegí solamente valores válidos de la lista provista.",
90
+ "Para campos record, object sin estructura fija, array.object o array.record, devolve un string JSON serializado valido.",
91
+ "La respuesta debe describir sugerencias concretas, breves y aplicables.",
92
+ `Operacion actual: ${input.operation}.`,
93
+ `Entidad actual: ${input.entity.name}.`,
94
+ ].join("\n")
95
+ }
96
+
97
+ protected buildUserInput(input: z.infer<typeof PromptRequestSchema>) {
98
+ return JSON.stringify({
99
+ task: input.prompt,
100
+ operation: input.operation,
101
+ entity: input.entity,
102
+ currentValues: input.currentValues,
103
+ fields: input.fields,
104
+ expectedResponse: {
105
+ message: "string",
106
+ suggestions: "object with proposed values indexed by field name",
107
+ warnings: ["optional string warnings"],
108
+ }
109
+ })
110
+ }
111
+
112
+ async prompt(req, rep) {
113
+ try {
114
+ req.rbac.assertPermission(AIPermissions.PromptCrud)
115
+
116
+ const input = PromptRequestSchema.parse(req.body ?? {})
117
+ const aiProvider = AiProviderFactory.instance()
118
+
119
+ const responseSchema = z.object({
120
+ message: z.string().nullable().default(null),
121
+ suggestions: z.object(this.buildFieldShape(input.fields)).strict().default({}),
122
+ warnings: z.array(z.string()).default([]),
123
+ })
124
+
125
+ const response = await aiProvider.prompt({
126
+ systemPrompt: this.buildSystemPrompt(input),
127
+ userInput: this.buildUserInput(input),
128
+ zodSchema: responseSchema,
129
+ operationTitle: `crud-${input.operation}-assistant`,
130
+ operationGroup: "crud-form-assistant",
131
+ ip: req.ip,
132
+ userAgent: req.headers["user-agent"],
133
+ tenant: req.rbac?.tenantId ?? null,
134
+ user: req.rbac?.userId ?? null,
135
+ })
136
+
137
+ const parsedOutput = responseSchema.parse(
138
+ typeof response.output === "string"
139
+ ? JSON.parse(response.output)
140
+ : response.output
141
+ )
142
+
143
+ return rep.send({
144
+ message: parsedOutput.message,
145
+ suggestions: parsedOutput.suggestions,
146
+ warnings: parsedOutput.warnings,
147
+ meta: {
148
+ tokens: response.tokens,
149
+ inputTokens: response.inputTokens,
150
+ outputTokens: response.outputTokens,
151
+ time: response.time,
152
+ }
153
+ })
154
+ } catch (e: any) {
155
+ console.error("AIController.prompt error", e)
156
+
157
+ const statusCode = e?.name === "ZodError" ? 400 : 500
158
+
159
+ return rep.status(statusCode).send({
160
+ message: e?.message || "AI prompt error",
161
+ })
162
+ }
163
+ }
164
+
165
+ }
166
+
167
+ export default AICrudController;
168
+ export {AICrudController};