-
Notifications
You must be signed in to change notification settings - Fork 47
Expand file tree
/
Copy pathservice.ts
More file actions
75 lines (60 loc) · 2.48 KB
/
service.ts
File metadata and controls
75 lines (60 loc) · 2.48 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
import { ConfigService } from "src/config/service";
import { LoggerService } from "src/logger/service";
import { Service } from "typedi";
import { targetConstructorToSchema } from "class-validator-jsonschema";
import { FetchService } from "src/fetch/service";
import { ClassConstructor, plainToClass } from "class-transformer";
import { validateSync } from "class-validator";
import { AiPromptRepository } from "./repository";
import { captureException } from "@sentry/node";
type AIChat = { role: "user" | "system"; content: string };
type OpenAIResponse = {
choices: Array<{ message: AIChat }>;
};
@Service()
export class AIService {
constructor(
private readonly configService: ConfigService,
private readonly loggerService: LoggerService,
private readonly fetchService: FetchService,
private readonly aiPromptRepository: AiPromptRepository,
) {}
public query = async <T extends object>(
payload: AIChat[],
ResponseDto: ClassConstructor<T>,
): Promise<T> => {
const schema = targetConstructorToSchema(ResponseDto);
const payloadWithValidationPrompt: AIChat[] = [
{
role: "system",
content: `system response must strictly follow the schema:\n${JSON.stringify(schema)}`,
},
...payload,
];
const cachedResponse = await this.aiPromptRepository.findPromptResponse(
payloadWithValidationPrompt,
);
if (cachedResponse) return JSON.parse(cachedResponse.response);
const body = { model: "gpt-4o", messages: payloadWithValidationPrompt };
this.loggerService.logger.info("Cached response not found, querying AI...");
// todo-zm: change to captureEvent
captureException("AI Query", { tags: { type: "CRON" }, extra: { body } });
const { OPENAI_KEY } = this.configService.env();
const res = await this.fetchService.post<OpenAIResponse>(
"https://api.openai.com/v1/chat/completions",
{ headers: { Authorization: `Bearer ${OPENAI_KEY}` }, body },
);
const chatResponseUnchecked = JSON.parse(res.choices[0].message.content) as T;
const output = plainToClass(ResponseDto, chatResponseUnchecked);
const errors = validateSync(output);
if (errors.length > 0)
throw new Error(
`⚠️ Errors in AI response in the following keys:${errors.reduce(
(pV, cV) => (pV += "\n" + cV.property + " : " + JSON.stringify(cV.constraints)),
"",
)}`,
);
await this.aiPromptRepository.insert(payloadWithValidationPrompt, output);
return output;
};
}