diff --git a/dashboard/server/ai/Plugin.ts b/dashboard/server/ai/Plugin.ts index 354482d..9a04f22 100644 --- a/dashboard/server/ai/Plugin.ts +++ b/dashboard/server/ai/Plugin.ts @@ -3,12 +3,13 @@ import type OpenAI from 'openai' export type AIPlugin_TTool = (OpenAI.Chat.Completions.ChatCompletionTool & { function: { name: T } }); -export type AIPlugin_TFunction = (...args: any[]) => any; + +export type AIPlugin_TFunction = (...args: any[]) => any; type AIPlugin_Constructor = { [Key in Items[number]]: { tool: AIPlugin_TTool, - handler: AIPlugin_TFunction + handler: AIPlugin_TFunction } } diff --git a/dashboard/server/ai/functions/AI_Billing.ts b/dashboard/server/ai/functions/AI_Billing.ts new file mode 100644 index 0000000..f276156 --- /dev/null +++ b/dashboard/server/ai/functions/AI_Billing.ts @@ -0,0 +1,127 @@ + +import { ProjectLimitModel } from "@schema/project/ProjectsLimits"; +import { AIPlugin } from "../Plugin"; +import { MAX_LOG_LIMIT_PERCENT } from "@data/broker/Limits"; +import { ProjectModel } from "@schema/project/ProjectSchema"; +import StripeService from "~/server/services/StripeService"; +import { InvoiceData } from "~/server/api/pay/invoices"; + +export class AiBilling extends AIPlugin<[ + 'getBillingInfo', + 'getLimits', + 'getInvoices' +]> { + + constructor() { + super({ + + 'getInvoices': { + handler: async (data: { project_id: string }) => { + + const project = await ProjectModel.findOne({ _id: data.project_id }); + if (!project) return { error: 'Project not found' }; + const invoices = await StripeService.getInvoices(project.customer_id); + if (!invoices) return []; + + return invoices?.data.map(e => { + const result: InvoiceData = { + link: e.invoice_pdf || '', + id: e.number || '', + date: e.created * 1000, + status: e.status || 'NO_STATUS', + cost: e.amount_due + } + return result; + }); + }, + tool: { + type: 'function', + function: { + name: 'getInvoices', + description: 'Gets the invoices of the user project', + parameters: {} + } + } + }, + + 'getBillingInfo': { + handler: async (data: { project_id: string }) => { + + const project = await ProjectModel.findOne({ _id: data.project_id }); + if (!project) return { error: 'Project not found' }; + + if (project.subscription_id === 'onetime') { + + const projectLimits = await ProjectLimitModel.findOne({ project_id: data.project_id }); + if (!projectLimits) return { error: 'Limits not found' } + + const result = { + premium: project.premium, + premium_type: project.premium_type, + billing_start_at: projectLimits.billing_start_at, + billing_expire_at: projectLimits.billing_expire_at, + limit: projectLimits.limit, + count: projectLimits.events + projectLimits.visits, + subscription_status: StripeService.isDisabled() ? 'Disabled mode' : ('One time payment') + } + + return result; + } + + const subscription = await StripeService.getSubscription(project.subscription_id); + + const projectLimits = await ProjectLimitModel.findOne({ project_id: data.project_id }); + if (!projectLimits) return { error: 'Limits not found' } + + + const result = { + premium: project.premium, + premium_type: project.premium_type, + billing_start_at: projectLimits.billing_start_at, + billing_expire_at: projectLimits.billing_expire_at, + limit: projectLimits.limit, + count: projectLimits.events + projectLimits.visits, + subscription_status: StripeService.isDisabled() ? 'Disabled mode' : (subscription?.status ?? '?') + } + + return result; + }, + tool: { + type: 'function', + function: { + name: 'getBillingInfo', + description: 'Gets the informations about the billing of the user project, limits, count, subscription_status, is premium, premium type, billing start at, billing expire at', + parameters: {} + } + } + }, + + 'getLimits': { + handler: async (data: { project_id: string }) => { + const projectLimits = await ProjectLimitModel.findOne({ project_id: data.project_id }); + if (!projectLimits) return { error: 'Project limits not found' }; + const TOTAL_COUNT = projectLimits.events + projectLimits.visits; + const COUNT_LIMIT = projectLimits.limit; + return { + total: TOTAL_COUNT, + limit: COUNT_LIMIT, + limited: TOTAL_COUNT > COUNT_LIMIT * MAX_LOG_LIMIT_PERCENT, + percent: Math.round(100 / COUNT_LIMIT * TOTAL_COUNT) + } + }, + tool: { + type: 'function', + function: { + name: 'getLimits', + description: 'Gets the informations about the limits of the user project', + parameters: {} + } + } + }, + + + }) + } +} + +export const AiBillingInstance = new AiBilling(); \ No newline at end of file diff --git a/dashboard/server/ai/functions/AI_Snapshots.ts b/dashboard/server/ai/functions/AI_Snapshots.ts new file mode 100644 index 0000000..d7e65b1 --- /dev/null +++ b/dashboard/server/ai/functions/AI_Snapshots.ts @@ -0,0 +1,78 @@ + +import { AIPlugin } from "../Plugin"; +import { ProjectModel } from "@schema/project/ProjectSchema"; +import { ProjectSnapshotModel } from "@schema/project/ProjectSnapshot"; + +export class AiSnapshot extends AIPlugin<[ + 'getSnapshots', + 'createSnapshot', +]> { + + constructor() { + super({ + + 'getSnapshots': { + handler: async (data: { project_id: string }) => { + const snapshots = await ProjectSnapshotModel.find({ project_id: data.project_id }); + return snapshots.map(e => e.toJSON()); + }, + tool: { + type: 'function', + function: { + name: 'getSnapshots', + description: 'Gets the snapshots list', + parameters: {} + } + } + }, + + 'createSnapshot': { + handler: async (data: { project_id: string, from: string, to: string, color: string, name: string }) => { + + if (!data.name) return { error: 'SnapshotName too short' } + if (data.name.length == 0) return { error: 'SnapshotName too short' } + + if (!data.from) return { error: 'from is required' } + if (!data.to) return { error: 'to is required' } + if (!data.color) return { error: 'color is required' } + + const project = await ProjectModel.findById(data.project_id); + if (!project) return { error: 'Project not found' } + + + const newSnapshot = await ProjectSnapshotModel.create({ + name: data.name, + from: new Date(data.from), + to: new Date(data.to), + color: data.color, + project_id: data.project_id + }); + + return newSnapshot.id; + + + }, + tool: { + type: 'function', + function: { + name: 'createSnapshot', + description: 'Create a snapshot', + parameters: { + type: 'object', + properties: { + from: { type: 'string', description: 'ISO string of start date' }, + to: { type: 'string', description: 'ISO string of end date' }, + color: { type: 'string', description: 'Color of the snapshot in HEX' }, + name: { type: 'string', description: 'Name of the snapshot' } + }, + required: ['from', 'to', 'color', 'name'] + } + } + } + }, + + }) + } +} + +export const AiSnapshotInstance = new AiSnapshot(); \ No newline at end of file diff --git a/dashboard/server/api/snapshot/create.post.ts b/dashboard/server/api/snapshot/create.post.ts index d15d481..3c657e6 100644 --- a/dashboard/server/api/snapshot/create.post.ts +++ b/dashboard/server/api/snapshot/create.post.ts @@ -1,7 +1,6 @@ import { ProjectModel } from "@schema/project/ProjectSchema"; import { ProjectSnapshotModel } from "@schema/project/ProjectSnapshot"; - export default defineEventHandler(async event => { const data = await getRequestData(event, { requireSchema: false, allowGuests: true, requireRange: false }); diff --git a/dashboard/server/services/AiService.ts b/dashboard/server/services/AiService.ts index 18b4b40..6e7772d 100644 --- a/dashboard/server/services/AiService.ts +++ b/dashboard/server/services/AiService.ts @@ -7,6 +7,8 @@ import { ProjectLimitModel } from '@schema/project/ProjectsLimits'; import { AiEventsInstance } from '../ai/functions/AI_Events'; import { AiVisitsInstance } from '../ai/functions/AI_Visits'; import { AiSessionsInstance } from '../ai/functions/AI_Sessions'; +import { AiBillingInstance } from '../ai/functions/AI_Billing'; +import { AiSnapshotInstance } from '../ai/functions/AI_Snapshots'; import { AiComposableChartInstance } from '../ai/functions/AI_ComposableChart'; const { AI_KEY, AI_ORG, AI_PROJECT } = useRuntimeConfig(); @@ -19,6 +21,8 @@ const tools: OpenAI.Chat.Completions.ChatCompletionTool[] = [ ...AiVisitsInstance.getTools(), ...AiEventsInstance.getTools(), ...AiSessionsInstance.getTools(), + ...AiBillingInstance.getTools(), + ...AiSnapshotInstance.getTools(), ...AiComposableChartInstance.getTools(), ] @@ -27,6 +31,8 @@ const functions: any = { ...AiVisitsInstance.getHandlers(), ...AiEventsInstance.getHandlers(), ...AiSessionsInstance.getHandlers(), + ...AiBillingInstance.getHandlers(), + ...AiSnapshotInstance.getHandlers(), ...AiComposableChartInstance.getHandlers() } @@ -190,7 +196,7 @@ export async function sendMessageOnChat(text: string, pid: string, time_offset: } else { const roleMessage: OpenAI.Chat.Completions.ChatCompletionMessageParam = { role: 'system', - content: "You are an AI Data Analyst and Growth Hacker specialized in helping users analyze data collected within Litlyx and providing strategies to grow their website, app, or business. Your scope is strictly limited to data creation, visualization, and growth-related advice. If a user asks something outside this domain, politely inform them that you are not designed to answer such questions. Today ISO date is " + new Date().toISOString() + "take this in count when the user ask relative dates" + content: "You are an AI Data Analyst and Growth Hacker specialized in helping users analyze data collected within Litlyx and providing strategies to grow their website, app, or business. Your scope is strictly limited to data creation, visualization, and growth-related advice. If a user asks something outside this domain, politely inform them that you are not designed to answer such questions. Today ISO date is " + new Date().toISOString() + " take this in count when the user ask relative dates" } messages.push(roleMessage); await addMessageToChat(roleMessage, chat_id);