@drax/ai-back 3.16.0 → 3.17.1
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/controllers/AIController.js +150 -0
- package/dist/controllers/AICrudController.js +150 -0
- package/dist/controllers/AIGenericController.js +83 -0
- package/dist/factory/AiProviderFactory.js +1 -1
- package/dist/index.js +6 -1
- package/dist/permissions/AIPermissions.js +7 -0
- package/dist/routes/AIRoutes.js +10 -0
- package/package.json +4 -4
- package/src/controllers/AICrudController.ts +168 -0
- package/src/controllers/AIGenericController.ts +97 -0
- package/src/factory/AiProviderFactory.ts +1 -1
- package/src/index.ts +10 -0
- package/src/permissions/AIPermissions.ts +11 -0
- package/src/routes/AIRoutes.ts +18 -0
- package/tsconfig.tsbuildinfo +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/factory/AiProviderFactory.d.ts +1 -1
- package/types/factory/AiProviderFactory.d.ts.map +1 -1
- package/types/index.d.ts +6 -1
- package/types/index.d.ts.map +1 -1
- package/types/permissions/AIPermissions.d.ts +7 -0
- package/types/permissions/AIPermissions.d.ts.map +1 -0
- package/types/routes/AIRoutes.d.ts +4 -0
- package/types/routes/AIRoutes.d.ts.map +1 -0
|
@@ -0,0 +1,150 @@
|
|
|
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
|
+
const CrudAiFieldSchema = z.lazy(() => z.object({
|
|
6
|
+
name: z.string(),
|
|
7
|
+
type: z.string(),
|
|
8
|
+
label: z.string(),
|
|
9
|
+
hint: z.string().nullable().default(null),
|
|
10
|
+
placeholder: z.string().nullable().default(null),
|
|
11
|
+
readonly: z.boolean().nullable().default(null),
|
|
12
|
+
default: z.any().nullable().default(null),
|
|
13
|
+
enum: z.array(z.string()).nullable().default(null),
|
|
14
|
+
items: z.array(z.object({
|
|
15
|
+
title: z.string().nullable().default(null),
|
|
16
|
+
value: z.any().nullable().default(null),
|
|
17
|
+
})).nullable().default(null),
|
|
18
|
+
ref: z.string().nullable().default(null),
|
|
19
|
+
refDisplay: z.string().nullable().default(null),
|
|
20
|
+
objectFields: z.array(CrudAiFieldSchema).nullable().default(null),
|
|
21
|
+
}));
|
|
22
|
+
const PromptRequestSchema = z.object({
|
|
23
|
+
prompt: z.string().min(1),
|
|
24
|
+
operation: z.enum(["create", "edit"]).default("create"),
|
|
25
|
+
entity: z.object({
|
|
26
|
+
name: z.string(),
|
|
27
|
+
identifier: z.string().optional().nullable(),
|
|
28
|
+
}),
|
|
29
|
+
currentValues: z.record(z.string(), z.any()).default({}),
|
|
30
|
+
fields: z.array(CrudAiFieldSchema).min(1),
|
|
31
|
+
});
|
|
32
|
+
class AIController extends CommonController {
|
|
33
|
+
buildFieldValueSchema(field) {
|
|
34
|
+
switch (field.type) {
|
|
35
|
+
case "number":
|
|
36
|
+
return z.number().nullable().default(null);
|
|
37
|
+
case "boolean":
|
|
38
|
+
return z.boolean().nullable().default(null);
|
|
39
|
+
case "array.string":
|
|
40
|
+
case "array.enum":
|
|
41
|
+
case "array.ref":
|
|
42
|
+
return z.array(z.string()).nullable().default(null);
|
|
43
|
+
case "array.number":
|
|
44
|
+
return z.array(z.number()).nullable().default(null);
|
|
45
|
+
case "object":
|
|
46
|
+
if (field.objectFields && Array.isArray(field.objectFields) && field.objectFields.length > 0) {
|
|
47
|
+
return z.object(this.buildFieldShape(field.objectFields)).nullable().default(null);
|
|
48
|
+
}
|
|
49
|
+
return z.string().nullable().default(null);
|
|
50
|
+
case "record":
|
|
51
|
+
case "array.object":
|
|
52
|
+
case "array.record":
|
|
53
|
+
case "array.fullFile":
|
|
54
|
+
case "file":
|
|
55
|
+
case "fullFile":
|
|
56
|
+
return z.string().nullable().default(null);
|
|
57
|
+
case "id":
|
|
58
|
+
case "string":
|
|
59
|
+
case "longString":
|
|
60
|
+
case "date":
|
|
61
|
+
case "ref":
|
|
62
|
+
case "enum":
|
|
63
|
+
case "select":
|
|
64
|
+
case "password":
|
|
65
|
+
default:
|
|
66
|
+
return z.string().nullable().default(null);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
buildFieldShape(fields) {
|
|
70
|
+
return fields.reduce((acc, field) => {
|
|
71
|
+
acc[field.name] = this.buildFieldValueSchema(field);
|
|
72
|
+
return acc;
|
|
73
|
+
}, {});
|
|
74
|
+
}
|
|
75
|
+
buildSystemPrompt(input) {
|
|
76
|
+
return [
|
|
77
|
+
"Sos un asistente de formularios para un sistema CRUD.",
|
|
78
|
+
"Tu tarea es proponer valores JSON para completar o editar una entidad.",
|
|
79
|
+
"Debes respetar exactamente los nombres de campo entregados.",
|
|
80
|
+
"No inventes campos adicionales.",
|
|
81
|
+
"Si un campo no tiene una propuesta razonable, omitilo.",
|
|
82
|
+
"Usa tipos compatibles con JSON.",
|
|
83
|
+
"Para campos enum o select, elegí solamente valores válidos de la lista provista.",
|
|
84
|
+
"Para campos record, object sin estructura fija, array.object o array.record, devolve un string JSON serializado valido.",
|
|
85
|
+
"La respuesta debe describir sugerencias concretas, breves y aplicables.",
|
|
86
|
+
`Operacion actual: ${input.operation}.`,
|
|
87
|
+
`Entidad actual: ${input.entity.name}.`,
|
|
88
|
+
].join("\n");
|
|
89
|
+
}
|
|
90
|
+
buildUserInput(input) {
|
|
91
|
+
return JSON.stringify({
|
|
92
|
+
task: input.prompt,
|
|
93
|
+
operation: input.operation,
|
|
94
|
+
entity: input.entity,
|
|
95
|
+
currentValues: input.currentValues,
|
|
96
|
+
fields: input.fields,
|
|
97
|
+
expectedResponse: {
|
|
98
|
+
message: "string",
|
|
99
|
+
suggestions: "object with proposed values indexed by field name",
|
|
100
|
+
warnings: ["optional string warnings"],
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
async prompt(req, rep) {
|
|
105
|
+
try {
|
|
106
|
+
req.rbac.assertPermission(AIPermissions.PromptCrud);
|
|
107
|
+
const input = PromptRequestSchema.parse(req.body ?? {});
|
|
108
|
+
const aiProvider = AiProviderFactory.instance();
|
|
109
|
+
const responseSchema = z.object({
|
|
110
|
+
message: z.string().nullable().default(null),
|
|
111
|
+
suggestions: z.object(this.buildFieldShape(input.fields)).strict().default({}),
|
|
112
|
+
warnings: z.array(z.string()).default([]),
|
|
113
|
+
});
|
|
114
|
+
const response = await aiProvider.prompt({
|
|
115
|
+
systemPrompt: this.buildSystemPrompt(input),
|
|
116
|
+
userInput: this.buildUserInput(input),
|
|
117
|
+
zodSchema: responseSchema,
|
|
118
|
+
operationTitle: `crud-${input.operation}-assistant`,
|
|
119
|
+
operationGroup: "crud-form-assistant",
|
|
120
|
+
ip: req.ip,
|
|
121
|
+
userAgent: req.headers["user-agent"],
|
|
122
|
+
tenant: req.rbac?.tenantId ?? null,
|
|
123
|
+
user: req.rbac?.userId ?? null,
|
|
124
|
+
});
|
|
125
|
+
const parsedOutput = responseSchema.parse(typeof response.output === "string"
|
|
126
|
+
? JSON.parse(response.output)
|
|
127
|
+
: response.output);
|
|
128
|
+
return rep.send({
|
|
129
|
+
message: parsedOutput.message,
|
|
130
|
+
suggestions: parsedOutput.suggestions,
|
|
131
|
+
warnings: parsedOutput.warnings,
|
|
132
|
+
meta: {
|
|
133
|
+
tokens: response.tokens,
|
|
134
|
+
inputTokens: response.inputTokens,
|
|
135
|
+
outputTokens: response.outputTokens,
|
|
136
|
+
time: response.time,
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
catch (e) {
|
|
141
|
+
console.error("AIController.prompt error", e);
|
|
142
|
+
const statusCode = e?.name === "ZodError" ? 400 : 500;
|
|
143
|
+
return rep.status(statusCode).send({
|
|
144
|
+
message: e?.message || "AI prompt error",
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
export default AIController;
|
|
150
|
+
export { AIController };
|
|
@@ -0,0 +1,150 @@
|
|
|
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
|
+
const CrudAiFieldSchema = z.lazy(() => z.object({
|
|
6
|
+
name: z.string(),
|
|
7
|
+
type: z.string(),
|
|
8
|
+
label: z.string(),
|
|
9
|
+
hint: z.string().nullable().default(null),
|
|
10
|
+
placeholder: z.string().nullable().default(null),
|
|
11
|
+
readonly: z.boolean().nullable().default(null),
|
|
12
|
+
default: z.any().nullable().default(null),
|
|
13
|
+
enum: z.array(z.string()).nullable().default(null),
|
|
14
|
+
items: z.array(z.object({
|
|
15
|
+
title: z.string().nullable().default(null),
|
|
16
|
+
value: z.any().nullable().default(null),
|
|
17
|
+
})).nullable().default(null),
|
|
18
|
+
ref: z.string().nullable().default(null),
|
|
19
|
+
refDisplay: z.string().nullable().default(null),
|
|
20
|
+
objectFields: z.array(CrudAiFieldSchema).nullable().default(null),
|
|
21
|
+
}));
|
|
22
|
+
const PromptRequestSchema = z.object({
|
|
23
|
+
prompt: z.string().min(1),
|
|
24
|
+
operation: z.enum(["create", "edit"]).default("create"),
|
|
25
|
+
entity: z.object({
|
|
26
|
+
name: z.string(),
|
|
27
|
+
identifier: z.string().optional().nullable(),
|
|
28
|
+
}),
|
|
29
|
+
currentValues: z.record(z.string(), z.any()).default({}),
|
|
30
|
+
fields: z.array(CrudAiFieldSchema).min(1),
|
|
31
|
+
});
|
|
32
|
+
class AICrudController extends CommonController {
|
|
33
|
+
buildFieldValueSchema(field) {
|
|
34
|
+
switch (field.type) {
|
|
35
|
+
case "number":
|
|
36
|
+
return z.number().nullable().default(null);
|
|
37
|
+
case "boolean":
|
|
38
|
+
return z.boolean().nullable().default(null);
|
|
39
|
+
case "array.string":
|
|
40
|
+
case "array.enum":
|
|
41
|
+
case "array.ref":
|
|
42
|
+
return z.array(z.string()).nullable().default(null);
|
|
43
|
+
case "array.number":
|
|
44
|
+
return z.array(z.number()).nullable().default(null);
|
|
45
|
+
case "object":
|
|
46
|
+
if (field.objectFields && Array.isArray(field.objectFields) && field.objectFields.length > 0) {
|
|
47
|
+
return z.object(this.buildFieldShape(field.objectFields)).nullable().default(null);
|
|
48
|
+
}
|
|
49
|
+
return z.string().nullable().default(null);
|
|
50
|
+
case "record":
|
|
51
|
+
case "array.object":
|
|
52
|
+
case "array.record":
|
|
53
|
+
case "array.fullFile":
|
|
54
|
+
case "file":
|
|
55
|
+
case "fullFile":
|
|
56
|
+
return z.string().nullable().default(null);
|
|
57
|
+
case "id":
|
|
58
|
+
case "string":
|
|
59
|
+
case "longString":
|
|
60
|
+
case "date":
|
|
61
|
+
case "ref":
|
|
62
|
+
case "enum":
|
|
63
|
+
case "select":
|
|
64
|
+
case "password":
|
|
65
|
+
default:
|
|
66
|
+
return z.string().nullable().default(null);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
buildFieldShape(fields) {
|
|
70
|
+
return fields.reduce((acc, field) => {
|
|
71
|
+
acc[field.name] = this.buildFieldValueSchema(field);
|
|
72
|
+
return acc;
|
|
73
|
+
}, {});
|
|
74
|
+
}
|
|
75
|
+
buildSystemPrompt(input) {
|
|
76
|
+
return [
|
|
77
|
+
"Sos un asistente de formularios para un sistema CRUD.",
|
|
78
|
+
"Tu tarea es proponer valores JSON para completar o editar una entidad.",
|
|
79
|
+
"Debes respetar exactamente los nombres de campo entregados.",
|
|
80
|
+
"No inventes campos adicionales.",
|
|
81
|
+
"Si un campo no tiene una propuesta razonable, omitilo.",
|
|
82
|
+
"Usa tipos compatibles con JSON.",
|
|
83
|
+
"Para campos enum o select, elegí solamente valores válidos de la lista provista.",
|
|
84
|
+
"Para campos record, object sin estructura fija, array.object o array.record, devolve un string JSON serializado valido.",
|
|
85
|
+
"La respuesta debe describir sugerencias concretas, breves y aplicables.",
|
|
86
|
+
`Operacion actual: ${input.operation}.`,
|
|
87
|
+
`Entidad actual: ${input.entity.name}.`,
|
|
88
|
+
].join("\n");
|
|
89
|
+
}
|
|
90
|
+
buildUserInput(input) {
|
|
91
|
+
return JSON.stringify({
|
|
92
|
+
task: input.prompt,
|
|
93
|
+
operation: input.operation,
|
|
94
|
+
entity: input.entity,
|
|
95
|
+
currentValues: input.currentValues,
|
|
96
|
+
fields: input.fields,
|
|
97
|
+
expectedResponse: {
|
|
98
|
+
message: "string",
|
|
99
|
+
suggestions: "object with proposed values indexed by field name",
|
|
100
|
+
warnings: ["optional string warnings"],
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
async prompt(req, rep) {
|
|
105
|
+
try {
|
|
106
|
+
req.rbac.assertPermission(AIPermissions.PromptCrud);
|
|
107
|
+
const input = PromptRequestSchema.parse(req.body ?? {});
|
|
108
|
+
const aiProvider = AiProviderFactory.instance();
|
|
109
|
+
const responseSchema = z.object({
|
|
110
|
+
message: z.string().nullable().default(null),
|
|
111
|
+
suggestions: z.object(this.buildFieldShape(input.fields)).strict().default({}),
|
|
112
|
+
warnings: z.array(z.string()).default([]),
|
|
113
|
+
});
|
|
114
|
+
const response = await aiProvider.prompt({
|
|
115
|
+
systemPrompt: this.buildSystemPrompt(input),
|
|
116
|
+
userInput: this.buildUserInput(input),
|
|
117
|
+
zodSchema: responseSchema,
|
|
118
|
+
operationTitle: `crud-${input.operation}-assistant`,
|
|
119
|
+
operationGroup: "crud-form-assistant",
|
|
120
|
+
ip: req.ip,
|
|
121
|
+
userAgent: req.headers["user-agent"],
|
|
122
|
+
tenant: req.rbac?.tenantId ?? null,
|
|
123
|
+
user: req.rbac?.userId ?? null,
|
|
124
|
+
});
|
|
125
|
+
const parsedOutput = responseSchema.parse(typeof response.output === "string"
|
|
126
|
+
? JSON.parse(response.output)
|
|
127
|
+
: response.output);
|
|
128
|
+
return rep.send({
|
|
129
|
+
message: parsedOutput.message,
|
|
130
|
+
suggestions: parsedOutput.suggestions,
|
|
131
|
+
warnings: parsedOutput.warnings,
|
|
132
|
+
meta: {
|
|
133
|
+
tokens: response.tokens,
|
|
134
|
+
inputTokens: response.inputTokens,
|
|
135
|
+
outputTokens: response.outputTokens,
|
|
136
|
+
time: response.time,
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
catch (e) {
|
|
141
|
+
console.error("AIController.prompt error", e);
|
|
142
|
+
const statusCode = e?.name === "ZodError" ? 400 : 500;
|
|
143
|
+
return rep.status(statusCode).send({
|
|
144
|
+
message: e?.message || "AI prompt error",
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
export default AICrudController;
|
|
150
|
+
export { AICrudController };
|
|
@@ -0,0 +1,83 @@
|
|
|
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
|
+
const PromptImageSchema = z.object({
|
|
6
|
+
url: z.string().min(1),
|
|
7
|
+
detail: z.enum(["auto", "low", "high"]).optional(),
|
|
8
|
+
});
|
|
9
|
+
const PromptContentPartSchema = z.discriminatedUnion("type", [
|
|
10
|
+
z.object({
|
|
11
|
+
type: z.literal("text"),
|
|
12
|
+
text: z.string(),
|
|
13
|
+
}),
|
|
14
|
+
z.object({
|
|
15
|
+
type: z.literal("image"),
|
|
16
|
+
imageUrl: z.string().min(1),
|
|
17
|
+
detail: z.enum(["auto", "low", "high"]).optional(),
|
|
18
|
+
}),
|
|
19
|
+
]);
|
|
20
|
+
const PromptMessageSchema = z.object({
|
|
21
|
+
role: z.enum(["user", "assistant", "system"]),
|
|
22
|
+
content: z.union([
|
|
23
|
+
z.string(),
|
|
24
|
+
z.array(PromptContentPartSchema),
|
|
25
|
+
]),
|
|
26
|
+
});
|
|
27
|
+
const PromptMemorySchema = z.object({
|
|
28
|
+
key: z.string().min(1),
|
|
29
|
+
value: z.string(),
|
|
30
|
+
});
|
|
31
|
+
const PromptInputFileSchema = z.object({
|
|
32
|
+
filename: z.string().optional(),
|
|
33
|
+
filepath: z.string().optional(),
|
|
34
|
+
size: z.number().nullable().optional(),
|
|
35
|
+
mimetype: z.string().optional(),
|
|
36
|
+
url: z.string().optional(),
|
|
37
|
+
});
|
|
38
|
+
const GenericPromptRequestSchema = z.object({
|
|
39
|
+
systemPrompt: z.string().min(1),
|
|
40
|
+
userInput: z.string().optional(),
|
|
41
|
+
userImages: z.array(PromptImageSchema).optional(),
|
|
42
|
+
inputFiles: z.array(PromptInputFileSchema).optional(),
|
|
43
|
+
userContent: z.array(PromptContentPartSchema).optional(),
|
|
44
|
+
history: z.array(PromptMessageSchema).optional(),
|
|
45
|
+
memory: z.array(PromptMemorySchema).optional(),
|
|
46
|
+
memoryHeader: z.string().optional(),
|
|
47
|
+
knowledgeBase: z.array(z.string()).optional(),
|
|
48
|
+
knowledgeBaseHeader: z.string().optional(),
|
|
49
|
+
jsonSchema: z.record(z.string(), z.any()).or(z.array(z.any())).optional(),
|
|
50
|
+
model: z.string().optional(),
|
|
51
|
+
operationTitle: z.string().optional(),
|
|
52
|
+
operationGroup: z.string().optional(),
|
|
53
|
+
});
|
|
54
|
+
class AIGenericController extends CommonController {
|
|
55
|
+
async prompt(request, reply) {
|
|
56
|
+
try {
|
|
57
|
+
request.rbac.assertPermission(AIPermissions.Prompt);
|
|
58
|
+
const input = GenericPromptRequestSchema.parse(request.body ?? {});
|
|
59
|
+
const aiProvider = AiProviderFactory.instance();
|
|
60
|
+
const promptInput = {
|
|
61
|
+
...input,
|
|
62
|
+
operationTitle: input.operationTitle ?? "generic-ai-prompt",
|
|
63
|
+
operationGroup: input.operationGroup ?? "generic-ai-prompt",
|
|
64
|
+
ip: request.ip,
|
|
65
|
+
userAgent: request.headers["user-agent"],
|
|
66
|
+
tenant: request.rbac?.tenantId ?? null,
|
|
67
|
+
user: request.rbac?.userId ?? null,
|
|
68
|
+
};
|
|
69
|
+
const response = await aiProvider.prompt(promptInput);
|
|
70
|
+
return reply.send(response);
|
|
71
|
+
}
|
|
72
|
+
catch (e) {
|
|
73
|
+
if (e?.name === "ZodError") {
|
|
74
|
+
return reply.status(400).send({
|
|
75
|
+
message: e?.message || "AI prompt validation error",
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
this.handleError(e, reply);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
export default AIGenericController;
|
|
83
|
+
export { AIGenericController };
|
package/dist/index.js
CHANGED
|
@@ -4,11 +4,16 @@ import AILogModel from "./models/AILogModel.js";
|
|
|
4
4
|
import AILogMongoRepository from "./repository/mongo/AILogMongoRepository.js";
|
|
5
5
|
import AILogSqliteRepository from "./repository/sqlite/AILogSqliteRepository.js";
|
|
6
6
|
import { OpenAiProviderFactory } from "./factory/OpenAiProviderFactory.js";
|
|
7
|
+
import { AiProviderFactory } from "./factory/AiProviderFactory.js";
|
|
7
8
|
import AILogServiceFactory from "./factory/services/AILogServiceFactory.js";
|
|
8
9
|
import { OpenAiProvider } from "./providers/OpenAiProvider.js";
|
|
9
10
|
import { KnowledgeService } from "./services/KnowledgeService.js";
|
|
10
11
|
import { AILogService } from "./services/AILogService.js";
|
|
11
12
|
import AILogPermissions from "./permissions/AILogPermissions.js";
|
|
13
|
+
import AIPermissions from "./permissions/AIPermissions.js";
|
|
12
14
|
import AILogController from "./controllers/AILogController.js";
|
|
15
|
+
import AICrudController from "./controllers/AICrudController.js";
|
|
16
|
+
import AIGenericController from "./controllers/AIGenericController.js";
|
|
13
17
|
import AILogRoutes from "./routes/AILogRoutes.js";
|
|
14
|
-
|
|
18
|
+
import AIRoutes from "./routes/AIRoutes.js";
|
|
19
|
+
export { OpenAiConfig, AILogSchema, AILogBaseSchema, AILogModel, AILogMongoRepository, AILogSqliteRepository, OpenAiProviderFactory, AiProviderFactory, AILogServiceFactory, OpenAiProvider, KnowledgeService, AILogService, AILogPermissions, AIPermissions, AILogController, AICrudController, AIGenericController, AILogRoutes, AIRoutes };
|
|
@@ -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 };
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"publishConfig": {
|
|
4
4
|
"access": "public"
|
|
5
5
|
},
|
|
6
|
-
"version": "3.
|
|
6
|
+
"version": "3.17.1",
|
|
7
7
|
"description": "Ai utils",
|
|
8
8
|
"main": "dist/index.js",
|
|
9
9
|
"types": "types/index.d.ts",
|
|
@@ -18,8 +18,8 @@
|
|
|
18
18
|
"author": "Cristian Incarnato & Drax Team",
|
|
19
19
|
"license": "ISC",
|
|
20
20
|
"dependencies": {
|
|
21
|
-
"@drax/ai-share": "^3.
|
|
22
|
-
"@drax/crud-back": "^3.
|
|
21
|
+
"@drax/ai-share": "^3.17.0",
|
|
22
|
+
"@drax/crud-back": "^3.17.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": "
|
|
47
|
+
"gitHead": "2fc35edc0338b4bc5437346faa6787a24abcf0e5"
|
|
48
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};
|