Skip to content
Merged
8 changes: 5 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "hawk.api",
"version": "1.2.22",
"version": "1.2.23",
"main": "index.ts",
"license": "BUSL-1.1",
"scripts": {
Expand Down Expand Up @@ -33,6 +33,7 @@
"typescript": "^4.7.4"
},
"dependencies": {
"@ai-sdk/openai": "^2.0.64",
"@amplitude/node": "^1.10.0",
"@graphql-tools/merge": "^8.3.1",
"@graphql-tools/schema": "^8.5.1",
Expand All @@ -52,9 +53,9 @@
"@types/mongodb": "^3.6.20",
"@types/morgan": "^1.9.10",
"@types/node": "^16.11.46",
"@types/node-fetch": "^2.5.4",
"@types/safe-regex": "^1.1.6",
"@types/uuid": "^8.3.4",
"ai": "^5.0.89",
"amqp-connection-manager": "^3.1.0",
"amqplib": "^0.5.5",
"apollo-server-express": "^3.10.0",
Expand Down Expand Up @@ -84,6 +85,7 @@
"prom-client": "^15.1.3",
"safe-regex": "^2.1.0",
"ts-node-dev": "^2.0.0",
"uuid": "^8.3.2"
"uuid": "^8.3.2",
"zod": "^3.25.76"
}
}
41 changes: 41 additions & 0 deletions src/integrations/vercel-ai/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { EventAddons, EventData } from '@hawk.so/types';
import { generateText } from 'ai';
import { openai } from '@ai-sdk/openai';
import { eventSolvingInput } from './inputs/eventSolving';
import { ctoInstruction } from './instructions/cto';

/**
* Interface for interacting with Vercel AI Gateway
*/
class VercelAIApi {
/**
* Model ID to use for generating suggestions
*/
private readonly modelId: string;

constructor() {
/**
* @todo make it dynamic, get from project settings
*/
this.modelId = 'gpt-4o';
}

/**
* Generate AI suggestion for the event
*
* @param {EventData<EventAddons>} payload - event data
* @returns {Promise<string>} AI suggestion for the event
* @todo add defence against invalid prompt injection
*/
public async generateSuggestion(payload: EventData<EventAddons>) {
const { text } = await generateText({
model: openai(this.modelId),
system: ctoInstruction,
prompt: eventSolvingInput(payload),
});

return text;
}
}

export const vercelAIApi = new VercelAIApi();
5 changes: 5 additions & 0 deletions src/integrations/vercel-ai/inputs/eventSolving.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { EventData, EventAddons } from '@hawk.so/types';

export const eventSolvingInput = (payload: EventData<EventAddons>) => `
Payload: ${JSON.stringify(payload)}
`;
9 changes: 9 additions & 0 deletions src/integrations/vercel-ai/instructions/cto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export const ctoInstruction = `Ты технический директор ИТ компании, тебе нужно пояснить ошибку и предложить решение.

Предоставь ответ в следующем формате:

1. Описание проблемы
2. Решение проблемы
3. Описание того, как можно предотвратить подобную ошибку в будущем

Ответь на русском языке.`;
2 changes: 1 addition & 1 deletion src/models/eventsFactory.js
Original file line number Diff line number Diff line change
Expand Up @@ -623,7 +623,7 @@ class EventsFactory extends Factory {
/**
* If originalEventId equals repetitionId than user wants to get first repetition which is original event
*/
if (repetitionId === originalEventId) {
if (repetitionId.toString() === originalEventId.toString()) {
const originalEvent = await this.eventsDataLoader.load(originalEventId);

/**
Expand Down
15 changes: 15 additions & 0 deletions src/resolvers/event.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
const getEventsFactory = require('./helpers/eventsFactory').default;
const sendPersonalNotification = require('../utils/personalNotifications').default;
const { aiService } = require('../services/ai');

/**
* See all types and fields here {@see ../typeDefs/event.graphql}
Expand Down Expand Up @@ -89,6 +90,20 @@ module.exports = {
return factory.findChartData(days, timezoneOffset, groupHash);
},

/**
* Return AI suggestion for the event
*
* @param {string} projectId - event's project
* @param {string} eventId - event id
* @param {string} originalEventId - original event id
* @returns {Promise<string>} AI suggestion for the event
*/
async aiSuggestion({ projectId, _id: eventId, originalEventId }, _args, context) {
const factory = getEventsFactory(context, projectId);

return aiService.generateSuggestion(factory, eventId, originalEventId);
},

/**
* Return release data for the event
*
Expand Down
27 changes: 27 additions & 0 deletions src/services/ai.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { vercelAIApi } from '../integrations/vercel-ai/';
import { EventsFactoryInterface } from './types';

/**
* Service for interacting with AI
*/
export class AIService {
/**
* Generate suggestion for the event
*
* @param eventsFactory - events factory
* @param eventId - event id
* @param originalEventId - original event id
* @returns {Promise<string>} - suggestion
*/
public async generateSuggestion(eventsFactory: EventsFactoryInterface, eventId: string, originalEventId: string): Promise<string> {
const event = await eventsFactory.getEventRepetition(eventId, originalEventId);

if (!event) {
throw new Error('Event not found');
}

return vercelAIApi.generateSuggestion(event.payload);
}
}

export const aiService = new AIService();
23 changes: 23 additions & 0 deletions src/services/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { EventAddons, EventData } from '@hawk.so/types';

/**
* Event type which is returned by events factory
*/
type Event = {
_id: string;
payload: EventData<EventAddons>;
};

/**
* Interface for interacting with events factory
*/
export interface EventsFactoryInterface {
/**
* Get event repetition
*
* @param repetitionId - repetition id
* @param originalEventId - original event id
* @returns {Promise<EventData<EventAddons>>} - event repetition
*/
getEventRepetition(repetitionId: string, originalEventId: string): Promise<Event>;
}
5 changes: 5 additions & 0 deletions src/typeDefs/event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,11 @@ type Event {
"""
repetitionsPortion(cursor: String = null, limit: Int = 10): RepetitionsPortion!

"""
AI suggestion for the event
"""
aiSuggestion: String

"""
Array of users who visited event
"""
Expand Down
18 changes: 18 additions & 0 deletions src/types/vercel-ai.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
declare module 'ai' {
/**
* Minimal type for generateText used in server-side integration.
*/
export function generateText(input: {
model: any;
system?: string;
prompt: string;
}): Promise<{ text: string }>;
}

declare module '@ai-sdk/openai' {
/**
* Minimal types for OpenAI provider.
*/
export function createOpenAI(config?: { apiKey?: string }): (model: string) => any;
export const openai: (model: string) => any;
}
5 changes: 5 additions & 0 deletions src/types/zod-v4.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
declare module 'zod/v4' {
export * from 'zod';
import z from 'zod';
export default z;
}
9 changes: 6 additions & 3 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,16 @@

/* Module Resolution Options */
// "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
"baseUrl": "./", /* Base directory to resolve non-absolute module names. */
"paths": { /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
"zod/v4": ["src/types/zod-v4.d.ts"]
},
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
// "typeRoots": [], /* List of folders to include type definitions from. */
// "types": [], /* Type declaration files to be included in compilation. */
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
"esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
"esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
"skipLibCheck": true
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */

Expand Down
78 changes: 69 additions & 9 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,39 @@
# yarn lockfile v1


"@ai-sdk/[email protected]":
version "2.0.7"
resolved "https://registry.yarnpkg.com/@ai-sdk/gateway/-/gateway-2.0.7.tgz#e3b77ef01658b47a19956313fc2a36b9e5a951f3"
integrity sha512-/AI5AKi4vOK9SEb8Z1dfXkhsJ5NAfWsoJQc96B/mzn2KIrjw5occOjIwD06scuhV9xWlghCoXJT1sQD9QH/tyg==
dependencies:
"@ai-sdk/provider" "2.0.0"
"@ai-sdk/provider-utils" "3.0.16"
"@vercel/oidc" "3.0.3"

"@ai-sdk/openai@^2.0.64":
version "2.0.64"
resolved "https://registry.yarnpkg.com/@ai-sdk/openai/-/openai-2.0.64.tgz#d8746bd341c277b440d2ed54179bfe1b43e7853c"
integrity sha512-+1mqxn42uB32DPZ6kurSyGAmL3MgCaDpkYU7zNDWI4NLy3Zg97RxTsI1jBCGIqkEVvRZKJlIMYtb89OvMnq3AQ==
dependencies:
"@ai-sdk/provider" "2.0.0"
"@ai-sdk/provider-utils" "3.0.16"

"@ai-sdk/[email protected]":
version "3.0.16"
resolved "https://registry.yarnpkg.com/@ai-sdk/provider-utils/-/provider-utils-3.0.16.tgz#17b7170bf51a7a690bf0186490ce29a8ce50a961"
integrity sha512-lsWQY9aDXHitw7C1QRYIbVGmgwyT98TF3MfM8alNIXKpdJdi+W782Rzd9f1RyOfgRmZ08gJ2EYNDhWNK7RqpEA==
dependencies:
"@ai-sdk/provider" "2.0.0"
"@standard-schema/spec" "^1.0.0"
eventsource-parser "^3.0.6"

"@ai-sdk/[email protected]":
version "2.0.0"
resolved "https://registry.yarnpkg.com/@ai-sdk/provider/-/provider-2.0.0.tgz#b853c739d523b33675bc74b6c506b2c690bc602b"
integrity sha512-6o7Y2SeO9vFKB8lArHXehNuusnpddKPk7xqL7T2/b+OvXMRIXUO1rR4wcv1hAFUAT9avGZshty3Wlua/XA7TvA==
dependencies:
json-schema "^0.4.0"

"@amplitude/identify@^1.10.0":
version "1.10.0"
resolved "https://registry.yarnpkg.com/@amplitude/identify/-/identify-1.10.0.tgz#d62b8b6785c29350c368810475a6fc7b04985210"
Expand Down Expand Up @@ -725,7 +758,7 @@
resolved "https://registry.yarnpkg.com/@n1ru4l/json-patch-plus/-/json-patch-plus-0.2.0.tgz#b8fa09fd980c3460dfdc109a7c4cc5590157aa6b"
integrity sha512-pLkJy83/rVfDTyQgDSC8GeXAHEdXNHGNJrB1b7wAyGQu0iv7tpMXntKVSqj0+XKNVQbco40SZffNfVALzIt0SQ==

"@opentelemetry/api@^1.4.0":
"@opentelemetry/api@1.9.0", "@opentelemetry/api@^1.4.0":
version "1.9.0"
resolved "https://registry.yarnpkg.com/@opentelemetry/api/-/api-1.9.0.tgz#d03eba68273dc0f7509e2a3d5cba21eae10379fe"
integrity sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==
Expand Down Expand Up @@ -811,6 +844,11 @@
dependencies:
"@sinonjs/commons" "^1.7.0"

"@standard-schema/spec@^1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@standard-schema/spec/-/spec-1.0.0.tgz#f193b73dc316c4170f2e82a881da0f550d551b9c"
integrity sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==

"@tootallnate/once@1":
version "1.1.2"
resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82"
Expand Down Expand Up @@ -1149,14 +1187,6 @@
"@types/node" "*"
form-data "^4.0.0"

"@types/node-fetch@^2.5.4":
version "2.6.2"
resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.6.2.tgz#d1a9c5fd049d9415dce61571557104dec3ec81da"
integrity sha512-DHqhlq5jeESLy19TYhLakJ07kNumXWjcDdxXsLUMJZ6ue8VZJj4kLPQVE/2mdHh3xZziNF1xppu5lwmS53HR+A==
dependencies:
"@types/node" "*"
form-data "^3.0.0"

"@types/node@*":
version "18.6.2"
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.6.2.tgz#ffc5f0f099d27887c8d9067b54e55090fcd54126"
Expand Down Expand Up @@ -1305,6 +1335,11 @@
semver "^7.3.2"
tsutils "^3.17.1"

"@vercel/[email protected]":
version "3.0.3"
resolved "https://registry.yarnpkg.com/@vercel/oidc/-/oidc-3.0.3.tgz#82c2b6dd4d5c3b37dcb1189718cdeb9db402d052"
integrity sha512-yNEQvPcVrK9sIe637+I0jD6leluPxzwJKx/Haw6F4H77CdDsszUn5V3o96LPziXkSNE2B83+Z3mjqGKBK/R6Gg==

abab@^2.0.3, abab@^2.0.5:
version "2.0.6"
resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.6.tgz#41b80f2c871d19686216b82309231cfd3cb3d291"
Expand Down Expand Up @@ -1363,6 +1398,16 @@ agent-base@6:
dependencies:
debug "4"

ai@^5.0.89:
version "5.0.89"
resolved "https://registry.yarnpkg.com/ai/-/ai-5.0.89.tgz#8929fbc18f247aa9e4442836a12aa84191edf2a4"
integrity sha512-8Nq+ZojGacQrupoJEQLrTDzT5VtR3gyp5AaqFSV3tzsAXlYQ9Igb7QE3yeoEdzOk5IRfDwWL7mDCUD+oBg1hDA==
dependencies:
"@ai-sdk/gateway" "2.0.7"
"@ai-sdk/provider" "2.0.0"
"@ai-sdk/provider-utils" "3.0.16"
"@opentelemetry/api" "1.9.0"

ajv@^6.10.0, ajv@^6.10.2:
version "6.12.6"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4"
Expand Down Expand Up @@ -2928,6 +2973,11 @@ [email protected]:
resolved "https://registry.yarnpkg.com/events/-/events-1.1.1.tgz#9ebdb7635ad099c70dcc4c2a1f5004288e8bd924"
integrity sha512-kEcvvCBByWXGnZy6JUlgAp2gBIUjfCAV6P6TgT1/aaQKcmuAEC4OZTV1I4EWQLz2gxZw76atuVyvHhTxvi0Flw==

eventsource-parser@^3.0.6:
version "3.0.6"
resolved "https://registry.yarnpkg.com/eventsource-parser/-/eventsource-parser-3.0.6.tgz#292e165e34cacbc936c3c92719ef326d4aeb4e90"
integrity sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==

exec-sh@^0.3.2:
version "0.3.6"
resolved "https://registry.yarnpkg.com/exec-sh/-/exec-sh-0.3.6.tgz#ff264f9e325519a60cb5e273692943483cca63bc"
Expand Down Expand Up @@ -4526,6 +4576,11 @@ json-schema-traverse@^0.4.1:
resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660"
integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==

json-schema@^0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.4.0.tgz#f7de4cf6efab838ebaeb3236474cbba5a1930ab5"
integrity sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==

json-stable-stringify-without-jsonify@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651"
Expand Down Expand Up @@ -7154,3 +7209,8 @@ [email protected]:
version "3.1.1"
resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50"
integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==

zod@^3.25.76:
version "3.25.76"
resolved "https://registry.yarnpkg.com/zod/-/zod-3.25.76.tgz#26841c3f6fd22a6a2760e7ccb719179768471e34"
integrity sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==
Loading