Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.25",
"version": "1.2.26",
"main": "index.ts",
"license": "BUSL-1.1",
"scripts": {
Expand Down Expand Up @@ -34,6 +34,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 @@ -53,9 +54,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 @@ -86,6 +87,7 @@
"redis": "^4.7.0",
"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 @@ -691,7 +691,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.getEventDailyChart(groupHash, days, timezoneOffset);
},

/**
* 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/gateway@2.0.7":
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/provider-utils@3.0.16":
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/provider@2.0.0":
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 @@ -845,6 +878,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 @@ -1183,14 +1221,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 @@ -1339,6 +1369,11 @@
semver "^7.3.2"
tsutils "^3.17.1"

"@vercel/oidc@3.0.3":
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 @@ -1397,6 +1432,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 @@ -2967,6 +3012,11 @@ events@1.1.1:
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 @@ -4570,6 +4620,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 @@ -7215,3 +7270,8 @@ yn@3.1.1:
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