@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,14 +1,23 @@
1
1
  import OpenAI from "openai";
2
- import type {ZodTypeAny} from "zod"
3
2
  import { zodResponseFormat } from "openai/helpers/zod";
4
- import type {IAIProvider, IPromptParams, IPromptResponse} from "../interfaces/IAIProvider";
3
+ import type {
4
+ IAIProvider,
5
+ IPromptContentPart,
6
+ IPromptMessage,
7
+ IPromptParams,
8
+ IPromptResponse
9
+ } from "../interfaces/IAIProvider";
10
+ import type {AILogService} from "../services/AILogService";
11
+ import type {IAILogBase} from "@drax/ai-share";
5
12
 
6
13
  class OpenAiProvider implements IAIProvider{
7
14
  protected _apiKey: string
8
15
  protected _model: any
16
+ protected _visionModel?: string
9
17
  protected _client: any
18
+ protected _aiLogService?: AILogService
10
19
 
11
- constructor(apiKey: string, model: string) {
20
+ constructor(apiKey: string, model: string, visionModel?: string, aiLogService?: AILogService) {
12
21
 
13
22
  if (!apiKey) {
14
23
  throw new Error("OpenAI apiKey required")
@@ -19,6 +28,8 @@ class OpenAiProvider implements IAIProvider{
19
28
 
20
29
  this._apiKey = apiKey
21
30
  this._model = model
31
+ this._visionModel = visionModel
32
+ this._aiLogService = aiLogService
22
33
  }
23
34
 
24
35
  get model(){
@@ -38,6 +49,170 @@ class OpenAiProvider implements IAIProvider{
38
49
  return this._client
39
50
  }
40
51
 
52
+ protected get visionModel(){
53
+ return this._visionModel
54
+ }
55
+
56
+ protected buildUserContent(input: IPromptParams): string | Array<{type: 'text', text: string} | {type: 'image_url', image_url: {url: string, detail?: 'auto' | 'low' | 'high'}}> {
57
+ if(input.userContent && input.userContent.length > 0){
58
+ return this.mapContentParts(input.userContent)
59
+ }
60
+
61
+ if(input.userImages && input.userImages.length > 0){
62
+ const content: Array<{type: 'text', text: string} | {type: 'image_url', image_url: {url: string, detail?: 'auto' | 'low' | 'high'}}> = []
63
+
64
+ if(input.userInput){
65
+ content.push({type: 'text', text: input.userInput})
66
+ }
67
+
68
+ content.push(...input.userImages.map(image => ({
69
+ type: 'image_url' as const,
70
+ image_url: {
71
+ url: image.url,
72
+ ...(image.detail ? {detail: image.detail} : {}),
73
+ }
74
+ })))
75
+
76
+ return content
77
+ }
78
+
79
+ return input.userInput ?? ""
80
+ }
81
+
82
+ protected mapContentParts(content: IPromptContentPart[]){
83
+ return content.map(part => {
84
+ if(part.type === 'text'){
85
+ return {
86
+ type: 'text' as const,
87
+ text: part.text
88
+ }
89
+ }
90
+
91
+ return {
92
+ type: 'image_url' as const,
93
+ image_url: {
94
+ url: part.imageUrl,
95
+ ...(part.detail ? {detail: part.detail} : {}),
96
+ }
97
+ }
98
+ })
99
+ }
100
+
101
+ protected mapHistory(history: IPromptMessage[] = []){
102
+ return history.map(message => ({
103
+ role: message.role,
104
+ content: typeof message.content === 'string'
105
+ ? message.content
106
+ : this.mapContentParts(message.content)
107
+ }))
108
+ }
109
+
110
+ protected hasImageInput(input: IPromptParams){
111
+ if(input.userImages && input.userImages.length > 0){
112
+ return true
113
+ }
114
+
115
+ if(input.userContent?.some(part => part.type === 'image')){
116
+ return true
117
+ }
118
+
119
+ return input.history?.some(message =>
120
+ Array.isArray(message.content) && message.content.some(part => part.type === 'image')
121
+ ) ?? false
122
+ }
123
+
124
+ protected serializePromptInput(input: IPromptParams, systemPrompt: string){
125
+ return JSON.stringify({
126
+ systemPrompt,
127
+ history: input.history,
128
+ userInput: input.userInput,
129
+ userContent: input.userContent,
130
+ memory: input.memory,
131
+ knowledgeBase: input.knowledgeBase,
132
+ })
133
+ }
134
+
135
+ protected serializePromptOutput(output: unknown){
136
+ if (typeof output === "string") {
137
+ return output
138
+ }
139
+
140
+ if (output === null || output === undefined) {
141
+ return undefined
142
+ }
143
+
144
+ return JSON.stringify(output)
145
+ }
146
+
147
+ protected buildLogPayload(input: IPromptParams, params: {
148
+ model: string,
149
+ systemPrompt: string,
150
+ startedAt: Date,
151
+ endedAt?: Date,
152
+ inputTokens?: number,
153
+ outputTokens?: number,
154
+ tokens?: number,
155
+ output?: unknown,
156
+ success: boolean,
157
+ errorMessage?: string,
158
+ }): IAILogBase {
159
+ return {
160
+ provider: "openai",
161
+ model: params.model,
162
+ operationTitle: input.operationTitle,
163
+ operationGroup: input.operationGroup,
164
+ ip: input.ip,
165
+ userAgent: input.userAgent,
166
+ input: this.serializePromptInput(input, params.systemPrompt),
167
+ inputImages: input.userImages?.map(image => ({
168
+ url: image.url,
169
+ })) ?? input.userContent
170
+ ?.filter(part => part.type === "image")
171
+ .map(part => ({
172
+ url: part.imageUrl,
173
+ })),
174
+ inputFiles: input.inputFiles,
175
+ inputTokens: params.inputTokens,
176
+ outputTokens: params.outputTokens,
177
+ tokens: params.tokens,
178
+ startedAt: params.startedAt,
179
+ endedAt: params.endedAt,
180
+ responseTime: params.endedAt ? `${params.endedAt.getTime() - params.startedAt.getTime()}ms` : undefined,
181
+ output: this.serializePromptOutput(params.output),
182
+ success: params.success,
183
+ errorMessage: params.errorMessage,
184
+ tenant: input.tenant,
185
+ user: input.user,
186
+ }
187
+ }
188
+
189
+ protected async registerPromptLog(input: IPromptParams, params: {
190
+ model: string,
191
+ systemPrompt: string,
192
+ startedAt: Date,
193
+ endedAt?: Date,
194
+ inputTokens?: number,
195
+ outputTokens?: number,
196
+ tokens?: number,
197
+ output?: unknown,
198
+ success: boolean,
199
+ errorMessage?: string,
200
+ }){
201
+ if(!this._aiLogService){
202
+ return
203
+ }
204
+
205
+ try{
206
+ await this._aiLogService.create(this.buildLogPayload(input, params))
207
+ }catch(e: any){
208
+ console.error("Error registerPromptLog", {
209
+ name: e?.name,
210
+ message: e?.message,
211
+ stack: e?.stack,
212
+ })
213
+ }
214
+ }
215
+
41
216
  async generateEmbedding({text, model="text-embedding-ada-002"}: {text:string,model:string }): Promise<number[]> {
42
217
  const response = await this.client.embeddings.create({
43
218
  model: model,
@@ -52,8 +227,6 @@ class OpenAiProvider implements IAIProvider{
52
227
  throw new Error("systemPrompt required")
53
228
  }
54
229
 
55
- const model = input.model ?? this.model
56
-
57
230
  let systemPrompt = input.systemPrompt
58
231
 
59
232
  if(input.memory && input.memory.length > 0){
@@ -65,37 +238,66 @@ class OpenAiProvider implements IAIProvider{
65
238
  }
66
239
 
67
240
 
68
- let userInput = input.userInput
69
-
241
+ const userInput = this.buildUserContent(input)
242
+ const model = input.model ?? (this.hasImageInput(input) ? this.visionModel ?? this.model : this.model)
243
+ const startedAt = new Date()
70
244
  const startTime = performance.now()
71
245
 
72
- const chatCompletion = await this.client.chat.completions.create({
73
- messages: [
74
- {role: 'system', content: systemPrompt},
75
- ...(input.history ? input.history : []),
76
- {role: 'user', content: userInput},
77
- ],
246
+ try {
247
+ const chatCompletion = await this.client.chat.completions.create({
248
+ messages: [
249
+ {role: 'system', content: systemPrompt},
250
+ ...this.mapHistory(input.history),
251
+ {role: 'user', content: userInput},
252
+ ],
78
253
 
79
- ...(input.zodSchema ? {response_format: zodResponseFormat(input.zodSchema, "event")} : {}),
80
- ...(input.jsonSchema ? {response_format: input.jsonSchema} : {}),
81
- model: model,
82
- });
254
+ ...(input.zodSchema ? {response_format: zodResponseFormat(input.zodSchema, "event")} : {}),
255
+ ...(input.jsonSchema ? {response_format: input.jsonSchema} : {}),
256
+ model: model,
257
+ });
83
258
 
84
259
 
85
- const output = chatCompletion.choices[0].message.content
86
- const tokens = chatCompletion.usage.total_tokens
87
- const inputTokens = chatCompletion.usage.prompt_tokens
88
- const outputTokens = chatCompletion.usage.completion_tokens
260
+ const output = chatCompletion.choices[0].message.content
261
+ const tokens = chatCompletion.usage.total_tokens
262
+ const inputTokens = chatCompletion.usage.prompt_tokens
263
+ const outputTokens = chatCompletion.usage.completion_tokens
89
264
 
90
- const endTime = performance.now()
91
- const time = endTime - startTime
265
+ const endTime = performance.now()
266
+ const time = endTime - startTime
267
+ const endedAt = new Date()
92
268
 
93
- return {
94
- output,
95
- tokens,
96
- inputTokens,
97
- outputTokens,
98
- time
269
+ await this.registerPromptLog(input, {
270
+ model,
271
+ systemPrompt,
272
+ startedAt,
273
+ endedAt,
274
+ inputTokens,
275
+ outputTokens,
276
+ tokens,
277
+ output,
278
+ success: true,
279
+ })
280
+
281
+ return {
282
+ output,
283
+ tokens,
284
+ inputTokens,
285
+ outputTokens,
286
+ time
287
+ }
288
+ } catch (e: any) {
289
+ const endedAt = new Date()
290
+
291
+ await this.registerPromptLog(input, {
292
+ model,
293
+ systemPrompt,
294
+ startedAt,
295
+ endedAt,
296
+ success: false,
297
+ errorMessage: e?.message,
298
+ })
299
+
300
+ throw e
99
301
  }
100
302
  }
101
303
 
@@ -0,0 +1,22 @@
1
+
2
+ import {AbstractMongoRepository} from "@drax/crud-back";
3
+ import {AILogModel} from "../../models/AILogModel.js";
4
+ import type {IAILogRepository} from '../../interfaces/IAILogRepository'
5
+ import type {IAILog, IAILogBase} from "@drax/ai-share";
6
+
7
+
8
+ class AILogMongoRepository extends AbstractMongoRepository<IAILog, IAILogBase, IAILogBase> implements IAILogRepository {
9
+
10
+ constructor() {
11
+ super();
12
+ this._model = AILogModel;
13
+ this._searchFields = ['provider', 'model', 'operationTitle', 'operationGroup', 'ip', 'userAgent', 'input', 'output', 'errorMessage'];
14
+ this._populateFields = ['tenant', 'user'];
15
+ this._lean = true
16
+ }
17
+
18
+ }
19
+
20
+ export default AILogMongoRepository
21
+ export {AILogMongoRepository}
22
+
@@ -0,0 +1,53 @@
1
+
2
+ import {AbstractSqliteRepository} from "@drax/crud-back";
3
+ import type {IAILogRepository} from '../../interfaces/IAILogRepository'
4
+ import type {IAILog, IAILogBase} from "@drax/ai-share";
5
+ import {SqliteTableField} from "@drax/common-back";
6
+
7
+ class AILogSqliteRepository extends AbstractSqliteRepository<IAILog, IAILogBase, IAILogBase> implements IAILogRepository {
8
+
9
+ protected db: any;
10
+ protected tableName: string = 'AILog';
11
+ protected dataBaseFile: string;
12
+ protected searchFields: string[] = ['provider', 'model', 'operationTitle', 'operationGroup', 'ip', 'userAgent', 'input', 'output', 'errorMessage'];
13
+ protected booleanFields: string[] = ['success'];
14
+ protected jsonFields: string[] = ['inputImages', 'inputFiles'];
15
+ protected identifier: string = '_id';
16
+ protected populateFields = [
17
+ { field: 'tenant', table: 'tenant', identifier: '_id' },
18
+ { field: 'user', table: 'user', identifier: '_id' }
19
+ ]
20
+ protected verbose: boolean = false;
21
+ protected tableFields: SqliteTableField[] = [
22
+ {name: "provider", type: "TEXT", unique: false, primary: false},
23
+ {name: "model", type: "TEXT", unique: false, primary: false},
24
+ {name: "operationTitle", type: "TEXT", unique: false, primary: false},
25
+ {name: "operationGroup", type: "TEXT", unique: false, primary: false},
26
+ {name: "ip", type: "TEXT", unique: false, primary: false},
27
+ {name: "userAgent", type: "TEXT", unique: false, primary: false},
28
+ {name: "input", type: "TEXT", unique: false, primary: false},
29
+ {name: "inputImages", type: "TEXT", unique: false, primary: false},
30
+ {name: "inputFiles", type: "TEXT", unique: false, primary: false},
31
+ {name: "inputTokens", type: "INTEGER", unique: false, primary: false},
32
+ {name: "inputTokens", type: "TEXT", unique: false, primary: false},
33
+ {name: "outputTokens", type: "INTEGER", unique: false, primary: false},
34
+ {name: "outputTokens", type: "TEXT", unique: false, primary: false},
35
+ {name: "tokens", type: "INTEGER", unique: false, primary: false},
36
+ {name: "tokens", type: "TEXT", unique: false, primary: false},
37
+ {name: "startedAt", type: "TEXT", unique: false, primary: false},
38
+ {name: "endedAt", type: "TEXT", unique: false, primary: false},
39
+ {name: "responseTime", type: "TEXT", unique: false, primary: false},
40
+ {name: "output", type: "TEXT", unique: false, primary: false},
41
+ {name: "success", type: "TEXT", unique: false, primary: false},
42
+ {name: "statusCode", type: "INTEGER", unique: false, primary: false},
43
+ {name: "statusCode", type: "TEXT", unique: false, primary: false},
44
+ {name: "errorMessage", type: "TEXT", unique: false, primary: false},
45
+ {name: "tenant", type: "TEXT", unique: false, primary: false},
46
+ {name: "user", type: "TEXT", unique: false, primary: false}
47
+ ]
48
+
49
+ }
50
+
51
+ export default AILogSqliteRepository
52
+ export {AILogSqliteRepository}
53
+
@@ -0,0 +1,38 @@
1
+
2
+ import AILogController from "../controllers/AILogController.js";
3
+ import {CrudSchemaBuilder} from "@drax/crud-back";
4
+ import {AILogSchema, AILogBaseSchema} from '../schemas/AILogSchema.js'
5
+
6
+ async function AILogFastifyRoutes(fastify, options) {
7
+
8
+ const controller: AILogController = new AILogController()
9
+ const schemas = new CrudSchemaBuilder(AILogSchema, AILogBaseSchema,AILogBaseSchema, 'AILog', 'openapi-3.0', ['ai']);
10
+
11
+ fastify.get('/api/ailog', {schema: schemas.paginateSchema}, (req,rep) => controller.paginate(req,rep))
12
+
13
+ fastify.get('/api/ailog/find', {schema: schemas.findSchema}, (req,rep) => controller.find(req,rep))
14
+
15
+ fastify.get('/api/ailog/search', {schema: schemas.searchSchema}, (req,rep) => controller.search(req,rep))
16
+
17
+ fastify.get('/api/ailog/:id', {schema: schemas.findByIdSchema}, (req,rep) => controller.findById(req,rep))
18
+
19
+ fastify.get('/api/ailog/find-one', {schema: schemas.findOneSchema}, (req,rep) => controller.findOne(req,rep))
20
+
21
+ fastify.get('/api/ailog/group-by', {schema: schemas.groupBySchema}, (req,rep) => controller.groupBy(req,rep))
22
+
23
+ fastify.post('/api/ailog', {schema: schemas.createSchema}, (req,rep) =>controller.create(req,rep))
24
+
25
+ fastify.put('/api/ailog/:id', {schema: schemas.updateSchema}, (req,rep) =>controller.update(req,rep))
26
+
27
+ fastify.patch('/api/ailog/:id', {schema: schemas.updateSchema}, (req,rep) =>controller.updatePartial(req,rep))
28
+
29
+ fastify.delete('/api/ailog/:id', {schema: schemas.deleteSchema}, (req,rep) =>controller.delete(req,rep))
30
+
31
+ fastify.get('/api/ailog/export', (req,rep) =>controller.export(req,rep))
32
+
33
+ fastify.post('/api/ailog/import', (req,rep) => controller.import(req,rep))
34
+
35
+ }
36
+
37
+ export default AILogFastifyRoutes;
38
+ export {AILogFastifyRoutes}
@@ -0,0 +1,18 @@
1
+
2
+ import AICrudController from "../controllers/AICrudController.js";
3
+ import AIGenericController from "../controllers/AIGenericController.js";
4
+
5
+
6
+ async function AiFastifyRoutes(fastify, options) {
7
+
8
+ const crudController: AICrudController = new AICrudController()
9
+ const genericController: AIGenericController = new AIGenericController()
10
+
11
+ fastify.post('/api/ai/prompt/crud', (req,rep) => crudController.prompt(req,rep))
12
+ fastify.post('/api/ai/prompt/generic', (req,rep) => genericController.prompt(req,rep))
13
+
14
+
15
+ }
16
+
17
+ export default AiFastifyRoutes;
18
+ export {AiFastifyRoutes}
@@ -0,0 +1,52 @@
1
+ import {z} from 'zod';
2
+
3
+
4
+ const AILogBaseSchema = z.object({
5
+ provider: z.string().optional(),
6
+ model: z.string().optional(),
7
+ operationTitle: z.string().optional(),
8
+ operationGroup: z.string().optional(),
9
+ ip: z.string().optional(),
10
+ userAgent: z.string().optional(),
11
+ input: z.string().optional(),
12
+ inputImages: z.array(
13
+ z.object({
14
+ filename: z.string().optional(),
15
+ filepath: z.string().optional(),
16
+ size: z.number().nullable().optional(),
17
+ mimetype: z.string().optional(),
18
+ url: z.string().optional()
19
+ })
20
+ ).optional(),
21
+ inputFiles: z.array(
22
+ z.object({
23
+ filename: z.string().optional(),
24
+ filepath: z.string().optional(),
25
+ size: z.number().nullable().optional(),
26
+ mimetype: z.string().optional(),
27
+ url: z.string().optional()
28
+ })
29
+ ).optional(),
30
+ inputTokens: z.number().nullable().optional(),
31
+ outputTokens: z.number().nullable().optional(),
32
+ tokens: z.number().nullable().optional(),
33
+ startedAt: z.coerce.date().nullable().optional(),
34
+ endedAt: z.coerce.date().nullable().optional(),
35
+ responseTime: z.string().optional(),
36
+ output: z.string().optional(),
37
+ success: z.boolean().optional(),
38
+ statusCode: z.number().nullable().optional(),
39
+ errorMessage: z.string().optional(),
40
+ tenant: z.coerce.string().optional().nullable(),
41
+ user: z.coerce.string().optional().nullable()
42
+ });
43
+
44
+ const AILogSchema = AILogBaseSchema
45
+ .extend({
46
+ _id: z.coerce.string(),
47
+ tenant: z.object({_id: z.coerce.string(), name: z.string()}).nullable().optional(),
48
+ user: z.object({_id: z.coerce.string(), username: z.string()}).nullable().optional()
49
+ })
50
+
51
+ export default AILogSchema;
52
+ export {AILogSchema, AILogBaseSchema}
@@ -0,0 +1,20 @@
1
+
2
+ import type{IAILogRepository} from "../interfaces/IAILogRepository";
3
+ import type {IAILogBase, IAILog} from "@drax/ai-share";
4
+ import {AbstractService} from "@drax/crud-back";
5
+ import type {ZodObject, ZodRawShape} from "zod";
6
+
7
+ class AILogService extends AbstractService<IAILog, IAILogBase, IAILogBase> {
8
+
9
+
10
+ constructor(AILogRepository: IAILogRepository, baseSchema?: ZodObject<ZodRawShape>, fullSchema?: ZodObject<ZodRawShape>) {
11
+ super(AILogRepository, baseSchema, fullSchema);
12
+
13
+ this._validateOutput = true
14
+
15
+ }
16
+
17
+ }
18
+
19
+ export default AILogService
20
+ export {AILogService}
@@ -6,6 +6,97 @@ import {IPromptMemory, IPromptMessage} from "../src/interfaces/IAIProvider";
6
6
 
7
7
  describe('OpenAi Test', () => {
8
8
 
9
+ test('OpenAi prompt supports image inputs and vision model fallback', async () => {
10
+ let request: any
11
+
12
+ class MockedOpenAiProvider extends OpenAiProvider {
13
+ constructor() {
14
+ super('test-key', 'gpt-4.1-mini', 'gpt-4o-mini')
15
+ this._client = {
16
+ chat: {
17
+ completions: {
18
+ create: async (payload: any) => {
19
+ request = payload
20
+ return {
21
+ choices: [{message: {content: '{"name":"invoice"}'}}],
22
+ usage: {
23
+ total_tokens: 10,
24
+ prompt_tokens: 7,
25
+ completion_tokens: 3
26
+ }
27
+ }
28
+ }
29
+ }
30
+ }
31
+ }
32
+ }
33
+ }
34
+
35
+ const openAi = new MockedOpenAiProvider()
36
+
37
+ const r = await openAi.prompt({
38
+ systemPrompt: 'Extract invoice data',
39
+ userInput: 'Read this invoice',
40
+ userImages: [{url: 'data:image/png;base64,abc123', detail: 'high'}]
41
+ })
42
+
43
+ expect(r.output).toBe('{"name":"invoice"}')
44
+ expect(request.model).toBe('gpt-4o-mini')
45
+ expect(request.messages[1]).toEqual({
46
+ role: 'user',
47
+ content: [
48
+ {type: 'text', text: 'Read this invoice'},
49
+ {
50
+ type: 'image_url',
51
+ image_url: {
52
+ url: 'data:image/png;base64,abc123',
53
+ detail: 'high'
54
+ }
55
+ }
56
+ ]
57
+ })
58
+ })
59
+
60
+ test('OpenAi prompt keeps default model for text-only inputs', async () => {
61
+ let request: any
62
+
63
+ class MockedOpenAiProvider extends OpenAiProvider {
64
+ constructor() {
65
+ super('test-key', 'gpt-4.1-mini', 'gpt-4o-mini')
66
+ this._client = {
67
+ chat: {
68
+ completions: {
69
+ create: async (payload: any) => {
70
+ request = payload
71
+ return {
72
+ choices: [{message: {content: 'Buenos Aires'}}],
73
+ usage: {
74
+ total_tokens: 8,
75
+ prompt_tokens: 6,
76
+ completion_tokens: 2
77
+ }
78
+ }
79
+ }
80
+ }
81
+ }
82
+ }
83
+ }
84
+ }
85
+
86
+ const openAi = new MockedOpenAiProvider()
87
+
88
+ await openAi.prompt({
89
+ systemPrompt: 'You are an AI assistant.',
90
+ userInput: 'What is the capital of Argentina?'
91
+ })
92
+
93
+ expect(request.model).toBe('gpt-4.1-mini')
94
+ expect(request.messages[1]).toEqual({
95
+ role: 'user',
96
+ content: 'What is the capital of Argentina?'
97
+ })
98
+ })
99
+
9
100
  test('OpenAi Factory', () => {
10
101
  const openAi = OpenAiProviderFactory.instance()
11
102
  expect(openAi).toBeInstanceOf(OpenAiProvider)