@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.
- package/dist/config/OpenAiConfig.js +1 -0
- package/dist/controllers/AIController.js +150 -0
- package/dist/controllers/AICrudController.js +150 -0
- package/dist/controllers/AIGenericController.js +83 -0
- package/dist/controllers/AILogController.js +18 -0
- package/dist/factory/AiProviderFactory.js +1 -1
- package/dist/factory/OpenAiProviderFactory.js +2 -1
- package/dist/factory/services/AILogServiceFactory.js +30 -0
- package/dist/index.js +14 -1
- package/dist/interfaces/IAILog.js +1 -0
- package/dist/interfaces/IAILogRepository.js +1 -0
- package/dist/models/AILogModel.js +50 -0
- package/dist/permissions/AILogPermissions.js +10 -0
- package/dist/permissions/AIPermissions.js +7 -0
- package/dist/providers/OpenAiProvider.js +176 -26
- package/dist/repository/mongo/AILogMongoRepository.js +13 -0
- package/dist/repository/sqlite/AILogSqliteRepository.js +45 -0
- package/dist/routes/AILogRoutes.js +21 -0
- package/dist/routes/AIRoutes.js +10 -0
- package/dist/schemas/AILogSchema.js +44 -0
- package/dist/services/AILogService.js +9 -0
- package/package.json +8 -2
- package/src/config/OpenAiConfig.ts +1 -0
- package/src/controllers/AICrudController.ts +168 -0
- package/src/controllers/AIGenericController.ts +97 -0
- package/src/controllers/AILogController.ts +29 -0
- package/src/factory/AiProviderFactory.ts +1 -1
- package/src/factory/OpenAiProviderFactory.ts +4 -2
- package/src/factory/services/AILogServiceFactory.ts +41 -0
- package/src/index.ts +47 -1
- package/src/interfaces/IAILogRepository.ts +11 -0
- package/src/interfaces/IAIProvider.ts +48 -2
- package/src/models/AILogModel.ts +65 -0
- package/src/permissions/AILogPermissions.ts +14 -0
- package/src/permissions/AIPermissions.ts +11 -0
- package/src/providers/OpenAiProvider.ts +231 -29
- package/src/repository/mongo/AILogMongoRepository.ts +22 -0
- package/src/repository/sqlite/AILogSqliteRepository.ts +53 -0
- package/src/routes/AILogRoutes.ts +38 -0
- package/src/routes/AIRoutes.ts +18 -0
- package/src/schemas/AILogSchema.ts +52 -0
- package/src/services/AILogService.ts +20 -0
- package/test/OpenAiProvider.test.ts +91 -0
- package/tsconfig.tsbuildinfo +1 -1
- package/types/config/OpenAiConfig.d.ts +2 -1
- package/types/config/OpenAiConfig.d.ts.map +1 -1
- package/types/controllers/AIController.d.ts +25 -0
- package/types/controllers/AIController.d.ts.map +1 -0
- package/types/controllers/AICrudController.d.ts +25 -0
- package/types/controllers/AICrudController.d.ts.map +1 -0
- package/types/controllers/AIGenericController.d.ts +7 -0
- package/types/controllers/AIGenericController.d.ts.map +1 -0
- package/types/controllers/AILogController.d.ts +8 -0
- package/types/controllers/AILogController.d.ts.map +1 -0
- package/types/factory/AiProviderFactory.d.ts +1 -1
- package/types/factory/AiProviderFactory.d.ts.map +1 -1
- package/types/factory/OpenAiProviderFactory.d.ts.map +1 -1
- package/types/factory/services/AILogServiceFactory.d.ts +8 -0
- package/types/factory/services/AILogServiceFactory.d.ts.map +1 -0
- package/types/index.d.ts +17 -3
- package/types/index.d.ts.map +1 -1
- package/types/interfaces/IAILog.d.ts +77 -0
- package/types/interfaces/IAILog.d.ts.map +1 -0
- package/types/interfaces/IAILogRepository.d.ts +6 -0
- package/types/interfaces/IAILogRepository.d.ts.map +1 -0
- package/types/interfaces/IAIProvider.d.ts +32 -2
- package/types/interfaces/IAIProvider.d.ts.map +1 -1
- package/types/models/AILogModel.d.ts +15 -0
- package/types/models/AILogModel.d.ts.map +1 -0
- package/types/permissions/AILogPermissions.d.ts +10 -0
- package/types/permissions/AILogPermissions.d.ts.map +1 -0
- package/types/permissions/AIPermissions.d.ts +7 -0
- package/types/permissions/AIPermissions.d.ts.map +1 -0
- package/types/providers/OpenAiProvider.d.ts +71 -2
- package/types/providers/OpenAiProvider.d.ts.map +1 -1
- package/types/repository/mongo/AILogMongoRepository.d.ts +9 -0
- package/types/repository/mongo/AILogMongoRepository.d.ts.map +1 -0
- package/types/repository/sqlite/AILogSqliteRepository.d.ts +23 -0
- package/types/repository/sqlite/AILogSqliteRepository.d.ts.map +1 -0
- package/types/routes/AILogRoutes.d.ts +4 -0
- package/types/routes/AILogRoutes.d.ts.map +1 -0
- package/types/routes/AIRoutes.d.ts +4 -0
- package/types/routes/AIRoutes.d.ts.map +1 -0
- package/types/schemas/AILogSchema.d.ts +81 -0
- package/types/schemas/AILogSchema.d.ts.map +1 -0
- package/types/services/AILogService.d.ts +10 -0
- 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
|
-
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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.
|
|
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": "
|
|
47
|
+
"gitHead": "d8a056b087d67157bc8c2e684b7e6696399d06a4"
|
|
42
48
|
}
|
|
@@ -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};
|