Add advanced ai

This commit is contained in:
Emily
2024-09-16 01:03:49 +02:00
parent 4c46a36c75
commit c3904ebd55
19 changed files with 641 additions and 194 deletions

View File

@@ -0,0 +1,30 @@
import type OpenAI from 'openai'
export type AIPlugin_TTool<T extends string> = (OpenAI.Chat.Completions.ChatCompletionTool & { function: { name: T } });
export type AIPlugin_TFunction<T extends string> = (...args: any[]) => any;
type AIPlugin_Constructor<Items extends string[]> = {
[Key in Items[number]]: {
tool: AIPlugin_TTool<Key>,
handler: AIPlugin_TFunction<Key>
}
}
export abstract class AIPlugin<Items extends string[] = []> {
constructor(public functions: AIPlugin_Constructor<Items>) { }
getTools() {
const keys = Object.keys(this.functions) as Items;
return keys.map((key: Items[number]) => { return this.functions[key].tool });
}
getHandlers() {
const keys = Object.keys(this.functions) as Items;
const result: Record<string, any> = {};
keys.forEach((key: Items[number]) => {
result[key] = this.functions[key].handler;
});
return result;
}
}

View File

@@ -0,0 +1,67 @@
import { AIPlugin } from "../Plugin";
export class AiComposableChart extends AIPlugin<['createComposableChart']> {
constructor() {
super({
'createComposableChart': {
handler: (data: { labels: string, points: number[] }) => {
return { ok: true };
},
tool: {
type: 'function',
function: {
name: 'createComposableChart',
description: 'Creates a chart based on the provided datasets',
parameters: {
type: 'object',
properties: {
labels: {
type: 'array',
items: { type: 'string' },
description: 'Labels for each data point in the chart'
},
title: {
type: 'string',
description: 'Title of the chart to let user understand what is displaying, not include dates'
},
datasets: {
type: 'array',
description: 'List of datasets',
items: {
type: 'object',
properties: {
chartType: {
type: 'string',
enum: ['line', 'bar'],
description: 'The type of chart to display the dataset, either "line" or "bar"'
},
points: {
type: 'array',
items: { type: 'number' },
description: 'Numerical values for each data point in the chart'
},
color: {
type: 'string',
description: 'Color used to represent the dataset'
},
name: {
type: 'string',
description: 'Name of the dataset'
}
},
required: ['points', 'color', 'chartType', 'name'],
description: 'Data points and style information for the dataset'
}
}
},
required: ['labels', 'datasets', 'title']
}
}
}
}
})
}
}
export const AiComposableChartInstance = new AiComposableChart();

View File

@@ -0,0 +1,80 @@
import { EventModel } from "@schema/metrics/EventSchema";
import { executeTimelineAggregation, fillAndMergeTimelineAggregationV2 } from "~/server/services/TimelineService";
import { Types } from "mongoose";
import { AIPlugin, AIPlugin_TTool } from "../Plugin";
const getEventsCountTool: AIPlugin_TTool<'getEventsCount'> = {
type: 'function',
function: {
name: 'getEventsCount',
description: 'Gets the number of events received on a date range, can also specify the event name and the metadata associated',
parameters: {
type: 'object',
properties: {
from: { type: 'string', description: 'ISO string of start date including hours' },
to: { type: 'string', description: 'ISO string of end date including hours' },
name: { type: 'string', description: 'Name of the events to get' },
metadata: { type: 'object', description: 'Metadata of events to get' },
},
required: ['from', 'to']
}
}
}
const getEventsTimelineTool: AIPlugin_TTool<'getEventsTimeline'> = {
type: 'function',
function: {
name: 'getEventsTimeline',
description: 'Gets an array of date and count for events received on a date range. Should be used to create charts.',
parameters: {
type: 'object',
properties: {
from: { type: 'string', description: 'ISO string of start date including hours' },
to: { type: 'string', description: 'ISO string of end date including hours' },
},
required: ['from', 'to']
}
}
}
export class AiEvents extends AIPlugin<['getEventsCount', 'getEventsTimeline']> {
constructor() {
super({
'getEventsCount': {
handler: async (data: { project_id: string, from?: string, to?: string, name?: string, metadata?: string }) => {
const query: any = {
project_id: data.project_id,
created_at: {
$gt: data.from ? new Date(data.from).getTime() : new Date(2023).getTime(),
$lt: data.to ? new Date(data.to).getTime() : new Date().getTime(),
}
}
if (data.metadata) query.metadata = data.metadata;
if (data.name) query.name = data.name;
const result = await EventModel.countDocuments(query);
return { count: result };
},
tool: getEventsCountTool
},
'getEventsTimeline': {
handler: async (data: { project_id: string, from: string, to: string }) => {
const timelineData = await executeTimelineAggregation({
projectId: new Types.ObjectId(data.project_id) as any,
model: EventModel,
from: data.from, to: data.to, slice: 'day'
});
const timelineFilledMerged = fillAndMergeTimelineAggregationV2(timelineData, 'day', data.from, data.to);
return { data: timelineFilledMerged };
},
tool: getEventsTimelineTool
}
})
}
}
export const AiEventsInstance = new AiEvents();

View File

@@ -0,0 +1,87 @@
import { VisitModel } from "@schema/metrics/VisitSchema";
import { AdvancedTimelineAggregationOptions, executeAdvancedTimelineAggregation, executeTimelineAggregation, fillAndMergeTimelineAggregationV2 } from "~/server/services/TimelineService";
import { Types } from "mongoose";
import { AIPlugin, AIPlugin_TTool } from "../Plugin";
const getVisitsCountsTool: AIPlugin_TTool<'getVisitsCount'> = {
type: 'function',
function: {
name: 'getVisitsCount',
description: 'Gets the number of visits received on a date range',
parameters: {
type: 'object',
properties: {
from: { type: 'string', description: 'ISO string of start date including hours' },
to: { type: 'string', description: 'ISO string of end date including hours' },
website: { type: 'string', description: 'The website of the visits' },
page: { type: 'string', description: 'The page of the visit' }
},
required: ['from', 'to']
}
}
}
const getVisitsTimelineTool: AIPlugin_TTool<'getVisitsTimeline'> = {
type: 'function',
function: {
name: 'getVisitsTimeline',
description: 'Gets an array of date and count for events received on a date range. Should be used to create charts.',
parameters: {
type: 'object',
properties: {
from: { type: 'string', description: 'ISO string of start date including hours' },
to: { type: 'string', description: 'ISO string of end date including hours' },
website: { type: 'string', description: 'The website of the visits' },
page: { type: 'string', description: 'The page of the visit' }
},
required: ['from', 'to']
}
}
}
export class AiVisits extends AIPlugin<['getVisitsCount', 'getVisitsTimeline']> {
constructor() {
super({
'getVisitsCount': {
handler: async (data: { project_id: string, from?: string, to?: string, website?: string, page?: string }) => {
const query: any = {
project_id: data.project_id,
created_at: {
$gt: data.from ? new Date(data.from).getTime() : new Date(2023).getTime(),
$lt: data.to ? new Date(data.to).getTime() : new Date().getTime(),
}
}
if (data.website) query.website = data.website;
if (data.page) query.page = data.page;
const result = await VisitModel.countDocuments(query);
return { count: result };
},
tool: getVisitsCountsTool
},
'getVisitsTimeline': {
handler: async (data: { project_id: string, from: string, to: string, website?: string, page?: string }) => {
const query: AdvancedTimelineAggregationOptions & { customMatch: Record<string, any> } = {
projectId: new Types.ObjectId(data.project_id) as any,
model: VisitModel,
from: data.from, to: data.to, slice: 'day',
customMatch: {}
}
if (data.website) query.customMatch.website = data.website;
if (data.page) query.customMatch.page = data.page;
const timelineData = await executeAdvancedTimelineAggregation(query);
const timelineFilledMerged = fillAndMergeTimelineAggregationV2(timelineData, 'day', data.from, data.to);
return { data: timelineFilledMerged };
},
tool: getVisitsTimelineTool
}
})
}
}
export const AiVisitsInstance = new AiVisits();

View File

@@ -1,8 +1,5 @@
import { getUserProjectFromId } from "~/server/LIVE_DEMO_DATA";
import { AiChatModel } from "@schema/ai/AiChatSchema";
import { sendMessageOnChat } from "~/server/services/AiService";
export default defineEventHandler(async event => {

View File

@@ -1,8 +1,7 @@
import { getUserProjectFromId } from "~/server/LIVE_DEMO_DATA";
import { AiChatModel } from "@schema/ai/AiChatSchema";
import { sendMessageOnChat } from "~/server/services/AiService";
import type OpenAI from "openai";
import { getChartsInMessage } from "~/server/services/AiService";
export default defineEventHandler(async event => {
@@ -19,11 +18,14 @@ export default defineEventHandler(async event => {
const chat = await AiChatModel.findOne({ _id: chat_id, project_id });
if (!chat) return;
const messages = chat.messages.filter(e => {
return (e.role == 'user' || (e.role == 'assistant' && e.content != undefined))
}).map(e => {
return { role: e.role, content: e.content }
});
return messages;
return (chat.messages as OpenAI.Chat.Completions.ChatCompletionMessageParam[])
.filter(e => e.role === 'assistant' || e.role === 'user')
.map(e => {
const charts = getChartsInMessage(e);
const content = e.content;
return { role: e.role, content, charts }
})
.filter(e=>{
return e.charts.length > 0 || e.content
})
});

View File

@@ -23,5 +23,6 @@ export default defineEventHandler(async event => {
if (chatsRemaining <= 0) return setResponseStatus(event, 400, 'CHAT_LIMIT_REACHED');
const response = await sendMessageOnChat(text, project._id.toString(), chat_id);
return response || 'Error getting response';
return response;
});

View File

@@ -1,51 +0,0 @@
import OpenAI from "openai";
import { EventModel } from "@schema/metrics/EventSchema";
export const AI_EventsFunctions = {
getEventsCount: ({ pid, from, to, name, metadata }: any) => {
return getEventsCountForAI(pid, from, to, name, metadata);
}
}
export const getEventsCountForAIDeclaration: OpenAI.Chat.Completions.ChatCompletionTool = {
type: 'function',
function: {
name: 'getEventsCount',
description: 'Gets the number of events received on a date range, can also specify the event name and the metadata associated',
parameters: {
type: 'object',
properties: {
from: { type: 'string', description: 'ISO string of start date including hours' },
to: { type: 'string', description: 'ISO string of end date including hours' },
name: { type: 'string', description: 'Name of the events to get' },
metadata: { type: 'object', description: 'Metadata of events to get' },
},
required: ['from', 'to']
}
}
}
export const AI_EventsTools: OpenAI.Chat.Completions.ChatCompletionTool[] = [
getEventsCountForAIDeclaration
]
export async function getEventsCountForAI(project_id: string, from?: string, to?: string, name?: string, metadata?: string) {
const query: any = {
project_id,
created_at: {
$gt: from ? new Date(from).getTime() : new Date(2023).getTime(),
$lt: to ? new Date(to).getTime() : new Date().getTime(),
}
}
if (metadata) query.metadata = metadata;
if (name) query.name = name;
const result = await EventModel.countDocuments(query);
return { count: result };
}

View File

@@ -1,14 +0,0 @@
import { VisitModel } from "@schema/metrics/VisitSchema";
export async function getVisitsCountFromDateRange(project_id: string, from?: string, to?: string) {
const result = await VisitModel.countDocuments({
project_id,
created_at: {
$gt: from ? new Date(from).getTime() : new Date(2023).getTime(),
$lt: to ? new Date(to).getTime() : new Date().getTime(),
}
});
return { count: result };
}

View File

@@ -8,11 +8,6 @@ const config = useRuntimeConfig();
let connection: mongoose.Mongoose;
let anomalyMinutesCount = 0;
function anomalyCheck() {
}
export default async () => {
console.log('[SERVER] Initializing');

View File

@@ -1,59 +1,35 @@
import { getVisitsCountFromDateRange } from '~/server/api/ai/functions/AI_Visits';
import OpenAI from "openai";
import { AiChatModel } from '@schema/ai/AiChatSchema';
import { AI_EventsFunctions, AI_EventsTools } from '../api/ai/functions/AI_Events';
import { ProjectCountModel } from '@schema/ProjectsCounts';
import { ProjectLimitModel } from '@schema/ProjectsLimits';
import { AiEventsInstance } from '../ai/functions/AI_Events';
import { AiVisitsInstance } from '../ai/functions/AI_Visits';
import { AiComposableChartInstance } from '../ai/functions/AI_ComposableChart';
const { AI_ORG, AI_PROJECT, AI_KEY } = useRuntimeConfig();
const OPENAI_MODEL: OpenAI.Chat.ChatModel = 'gpt-4o-mini';
const openai = new OpenAI({
organization: AI_ORG,
project: AI_PROJECT,
apiKey: AI_KEY
});
// const get_current_date: OpenAI.Chat.Completions.ChatCompletionTool = {
// type: 'function',
// function: {
// name: 'get_current_date',
// description: 'Gets the current date as ISO string',
// }
// }
const get_visits_count_Schema: OpenAI.Chat.Completions.ChatCompletionTool = {
type: 'function',
function: {
name: 'get_visits_count',
description: 'Gets the number of visits received on a date range',
parameters: {
type: 'object',
properties: {
from: { type: 'string', description: 'ISO string of start date including hours' },
to: { type: 'string', description: 'ISO string of end date including hours' }
},
required: ['from', 'to']
}
}
}
const tools: OpenAI.Chat.Completions.ChatCompletionTool[] = [
get_visits_count_Schema,
...AI_EventsTools
...AiVisitsInstance.getTools(),
...AiEventsInstance.getTools(),
...AiComposableChartInstance.getTools()
]
const functions: any = {
get_current_date: async ({ }) => {
return new Date().toISOString();
},
get_visits_count: async ({ pid, from, to }: any) => {
return await getVisitsCountFromDateRange(pid, from, to);
},
...AI_EventsFunctions
...AiVisitsInstance.getHandlers(),
...AiEventsInstance.getHandlers(),
...AiComposableChartInstance.getHandlers()
}
@@ -81,6 +57,14 @@ async function setChatTitle(title: string, chat_id?: string) {
await AiChatModel.updateOne({ _id: chat_id }, { title });
}
export function getChartsInMessage(message: OpenAI.Chat.Completions.ChatCompletionMessageParam) {
if (message.role != 'assistant') return [];
if (!message.tool_calls) return [];
if (message.tool_calls.length == 0) return [];
return message.tool_calls.filter(e => e.function.name === 'createComposableChart').map(e => e.function.arguments);
}
export async function sendMessageOnChat(text: string, pid: string, initial_chat_id?: string) {
@@ -100,43 +84,36 @@ export async function sendMessageOnChat(text: string, pid: string, initial_chat_
await setChatTitle(text.substring(0, 110), chat_id);
}
const userMessage: OpenAI.Chat.Completions.ChatCompletionMessageParam = {
role: 'user', content: text
}
const userMessage: OpenAI.Chat.Completions.ChatCompletionMessageParam = { role: 'user', content: text }
messages.push(userMessage);
await addMessageToChat(userMessage, chat_id);
let response = await openai.chat.completions.create({ model: 'gpt-3.5-turbo', messages, n: 1, tools });
let response = await openai.chat.completions.create({ model: OPENAI_MODEL, messages, n: 1, tools });
let responseMessage = response.choices[0].message;
let toolCalls = responseMessage.tool_calls;
const chartsData: string[][] = [];
await addMessageToChat(responseMessage, chat_id);
messages.push(responseMessage);
while ((response.choices[0].message.tool_calls?.length || 0) > 0) {
await addMessageToChat(response.choices[0].message, chat_id);
messages.push(response.choices[0].message);
if (response.choices[0].message.tool_calls) {
console.log('Tools to call', response.choices[0].message.tool_calls.length);
chartsData.push(getChartsInMessage(response.choices[0].message));
if (toolCalls) {
console.log({ toolCalls: toolCalls.length });
for (const toolCall of toolCalls) {
const functionName = toolCall.function.name;
const functionToCall = functions[functionName];
const functionArgs = JSON.parse(toolCall.function.arguments);
console.log('CALLING FUNCTION', functionName, 'WITH PARAMS', functionArgs);
const functionResponse = await functionToCall({ pid, ...functionArgs });
console.log('RESPONSE FUNCTION', functionName, 'WITH VALUE', functionResponse);
messages.push({ tool_call_id: toolCall.id, role: "tool", content: JSON.stringify(functionResponse) });
await addMessageToChat({ tool_call_id: toolCall.id, role: "tool", content: JSON.stringify(functionResponse) }, chat_id);
for (const toolCall of response.choices[0].message.tool_calls) {
const functionName = toolCall.function.name;
console.log('Calling tool function', functionName);
const functionToCall = functions[functionName];
const functionArgs = JSON.parse(toolCall.function.arguments);
const functionResponse = await functionToCall({ project_id: pid, ...functionArgs });
messages.push({ tool_call_id: toolCall.id, role: "tool", content: JSON.stringify(functionResponse) });
await addMessageToChat({ tool_call_id: toolCall.id, role: "tool", content: JSON.stringify(functionResponse) }, chat_id);
}
}
response = await openai.chat.completions.create({ model: 'gpt-4o', messages, n: 1, tools });
responseMessage = response.choices[0].message;
toolCalls = responseMessage.tool_calls;
await addMessageToChat(responseMessage, chat_id);
response = await openai.chat.completions.create({ model: OPENAI_MODEL, messages, n: 1, tools });
}
await addMessageToChat(response.choices[0].message, chat_id);
await ProjectLimitModel.updateOne({ project_id: pid }, { $inc: { ai_messages: 1 } })
return responseMessage.content;
return { content: response.choices[0].message.content, charts: chartsData.filter(e => e.length > 0).flat() };
}

View File

@@ -1,4 +1,4 @@
import { getPlanFromId, getPlanFromTag } from '@data/PREMIUM';
import { getPlanFromId, getPlanFromTag, PREMIUM_TAG } from '@data/PREMIUM';
import Stripe from 'stripe';
class StripeService {
@@ -133,9 +133,23 @@ class StripeService {
return deleted;
}
async createOneTimeCoupon() {
async createStripeCode(plan: PREMIUM_TAG) {
if (this.disabledMode) return;
if (!this.stripe) throw Error('Stripe not initialized');
const INCUBATION_COUPON = 'sDD7Weh3';
if (plan === 'INCUBATION') {
await this.stripe.promotionCodes.create({
coupon: INCUBATION_COUPON,
active: true,
code: 'TESTCACCA1',
max_redemptions: 1,
})
return true;
}
return false;
}
async createOneTimeSubscriptionDummy(customer_id: string, planId: number) {