Skip to content

Commit c1a1d41

Browse files
authored
Merge branch 'master' into feat/redis-rate-limit-series
2 parents 37e1711 + 0f8dad9 commit c1a1d41

File tree

21 files changed

+291
-23
lines changed

21 files changed

+291
-23
lines changed

package.json

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "hawk.api",
3-
"version": "1.2.24",
3+
"version": "1.2.26",
44
"main": "index.ts",
55
"license": "BUSL-1.1",
66
"scripts": {
@@ -34,12 +34,13 @@
3434
"typescript": "^4.7.4"
3535
},
3636
"dependencies": {
37+
"@ai-sdk/openai": "^2.0.64",
3738
"@amplitude/node": "^1.10.0",
3839
"@graphql-tools/merge": "^8.3.1",
3940
"@graphql-tools/schema": "^8.5.1",
4041
"@graphql-tools/utils": "^8.9.0",
4142
"@hawk.so/nodejs": "^3.1.1",
42-
"@hawk.so/types": "^0.1.33",
43+
"@hawk.so/types": "^0.1.37",
4344
"@n1ru4l/json-patch-plus": "^0.2.0",
4445
"@types/amqp-connection-manager": "^2.0.4",
4546
"@types/bson": "^4.0.5",
@@ -53,9 +54,9 @@
5354
"@types/mongodb": "^3.6.20",
5455
"@types/morgan": "^1.9.10",
5556
"@types/node": "^16.11.46",
56-
"@types/node-fetch": "^2.5.4",
5757
"@types/safe-regex": "^1.1.6",
5858
"@types/uuid": "^8.3.4",
59+
"ai": "^5.0.89",
5960
"amqp-connection-manager": "^3.1.0",
6061
"amqplib": "^0.5.5",
6162
"apollo-server-express": "^3.10.0",
@@ -86,6 +87,7 @@
8687
"redis": "^4.7.0",
8788
"safe-regex": "^2.1.0",
8889
"ts-node-dev": "^2.0.0",
89-
"uuid": "^8.3.2"
90+
"uuid": "^8.3.2",
91+
"zod": "^3.25.76"
9092
}
9193
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { EventAddons, EventData } from '@hawk.so/types';
2+
import { generateText } from 'ai';
3+
import { openai } from '@ai-sdk/openai';
4+
import { eventSolvingInput } from './inputs/eventSolving';
5+
import { ctoInstruction } from './instructions/cto';
6+
7+
/**
8+
* Interface for interacting with Vercel AI Gateway
9+
*/
10+
class VercelAIApi {
11+
/**
12+
* Model ID to use for generating suggestions
13+
*/
14+
private readonly modelId: string;
15+
16+
constructor() {
17+
/**
18+
* @todo make it dynamic, get from project settings
19+
*/
20+
this.modelId = 'gpt-4o';
21+
}
22+
23+
/**
24+
* Generate AI suggestion for the event
25+
*
26+
* @param {EventData<EventAddons>} payload - event data
27+
* @returns {Promise<string>} AI suggestion for the event
28+
* @todo add defence against invalid prompt injection
29+
*/
30+
public async generateSuggestion(payload: EventData<EventAddons>) {
31+
const { text } = await generateText({
32+
model: openai(this.modelId),
33+
system: ctoInstruction,
34+
prompt: eventSolvingInput(payload),
35+
});
36+
37+
return text;
38+
}
39+
}
40+
41+
export const vercelAIApi = new VercelAIApi();
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { EventData, EventAddons } from '@hawk.so/types';
2+
3+
export const eventSolvingInput = (payload: EventData<EventAddons>) => `
4+
Payload: ${JSON.stringify(payload)}
5+
`;
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
export const ctoInstruction = `Ты технический директор ИТ компании, тебе нужно пояснить ошибку и предложить решение.
2+
3+
Предоставь ответ в следующем формате:
4+
5+
1. Описание проблемы
6+
2. Решение проблемы
7+
3. Описание того, как можно предотвратить подобную ошибку в будущем
8+
9+
Ответь на русском языке.`;

src/models/eventsFactory.js

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,12 @@ class EventsFactory extends Factory {
292292
const searchFilter = search.trim().length > 0
293293
? {
294294
$or: [
295+
{
296+
'repetition.delta': {
297+
$regex: escapedSearch,
298+
$options: 'i',
299+
},
300+
},
295301
{
296302
'event.payload.title': {
297303
$regex: escapedSearch,
@@ -640,7 +646,6 @@ class EventsFactory extends Factory {
640646
/**
641647
* Returns Event repetitions
642648
*
643-
* @param {string|ObjectID} eventId - Event's id, could be repetitionId in case when we want to get repetitions portion by one repetition
644649
* @param {string|ObjectID} originalEventId - id of the original event
645650
* @param {Number} limit - count limitations
646651
* @param {Number} cursor - pointer to the next repetition
@@ -735,7 +740,7 @@ class EventsFactory extends Factory {
735740
/**
736741
* If originalEventId equals repetitionId than user wants to get first repetition which is original event
737742
*/
738-
if (repetitionId === originalEventId) {
743+
if (repetitionId.toString() === originalEventId.toString()) {
739744
const originalEvent = await this.eventsDataLoader.load(originalEventId);
740745

741746
/**

src/models/notify.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,11 @@ class Notify {
9090
endpoint: 'slack',
9191
minPeriod: 60,
9292
},
93+
loop: {
94+
isEnabled: true,
95+
endpoint: 'loop',
96+
minPeriod: 60,
97+
},
9398
telegram: {
9499
isEnabled: true,
95100
endpoint: 'telegram',

src/rabbitmq.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export enum Queues {
2727
Email = 'sender/email',
2828
Telegram = 'notify/telegram',
2929
Slack = 'notify/slack',
30+
Loop = 'notify/loop',
3031
Limiter = 'cron-tasks/limiter',
3132
}
3233

@@ -81,6 +82,14 @@ export const WorkerPaths: Record<string, WorkerPath> = {
8182
queue: Queues.Slack,
8283
},
8384

85+
/**
86+
* Path to loop worker
87+
*/
88+
Loop: {
89+
exchange: Exchanges.Notify,
90+
queue: Queues.Loop,
91+
},
92+
8493
/**
8594
* Path to limiter worker
8695
*/

src/resolvers/event.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
const getEventsFactory = require('./helpers/eventsFactory').default;
22
const sendPersonalNotification = require('../utils/personalNotifications').default;
3+
const { aiService } = require('../services/ai');
34

45
/**
56
* See all types and fields here {@see ../typeDefs/event.graphql}
@@ -89,6 +90,20 @@ module.exports = {
8990
return factory.getEventDailyChart(groupHash, days, timezoneOffset);
9091
},
9192

93+
/**
94+
* Return AI suggestion for the event
95+
*
96+
* @param {string} projectId - event's project
97+
* @param {string} eventId - event id
98+
* @param {string} originalEventId - original event id
99+
* @returns {Promise<string>} AI suggestion for the event
100+
*/
101+
async aiSuggestion({ projectId, _id: eventId, originalEventId }, _args, context) {
102+
const factory = getEventsFactory(context, projectId);
103+
104+
return aiService.generateSuggestion(factory, eventId, originalEventId);
105+
},
106+
92107
/**
93108
* Return release data for the event
94109
*

src/resolvers/project.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,11 @@ module.exports = {
9090
endpoint: '',
9191
minPeriod: 60,
9292
},
93+
loop: {
94+
isEnabled: false,
95+
endpoint: '',
96+
minPeriod: 60,
97+
},
9398
},
9499
}, true);
95100

src/resolvers/projectNotifications.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,12 @@ function validateNotificationsRuleChannels(channels: NotificationsChannelsDBSche
114114
}
115115
}
116116

117+
if (channels.loop!.isEnabled) {
118+
if (!/^https:\/\/.+\/hooks\/.+$/.test(channels.loop!.endpoint)) {
119+
return 'Invalid loop endpoint passed';
120+
}
121+
}
122+
117123
if (channels.telegram!.isEnabled) {
118124
if (!/^https:\/\/notify\.bot\.codex\.so\/u\/[A-Za-z0-9]+$/.test(channels.telegram!.endpoint)) {
119125
return 'Invalid telegram endpoint passed';

0 commit comments

Comments
 (0)