mirror of
https://github.com/Litlyx/litlyx
synced 2025-12-10 07:48:37 +01:00
Add advanced ai
This commit is contained in:
121
dashboard/components/analyst/ComposableChart.vue
Normal file
121
dashboard/components/analyst/ComposableChart.vue
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { ChartData, ChartOptions } from 'chart.js';
|
||||||
|
import { useLineChart, LineChart } from 'vue-chart-3';
|
||||||
|
import * as datefns from 'date-fns';
|
||||||
|
|
||||||
|
registerChartComponents();
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
labels: string[],
|
||||||
|
title:string,
|
||||||
|
datasets: {
|
||||||
|
points: number[],
|
||||||
|
color: string,
|
||||||
|
type: string,
|
||||||
|
name:string
|
||||||
|
}[]
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const chartOptions = ref<ChartOptions<'line'>>({
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
interaction: {
|
||||||
|
intersect: false,
|
||||||
|
mode: 'nearest',
|
||||||
|
axis: 'x',
|
||||||
|
includeInvisible: true
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
y: {
|
||||||
|
ticks: { display: true },
|
||||||
|
grid: {
|
||||||
|
display: true,
|
||||||
|
drawBorder: false,
|
||||||
|
color: '#CCCCCC22',
|
||||||
|
// borderDash: [5, 10]
|
||||||
|
},
|
||||||
|
},
|
||||||
|
x: {
|
||||||
|
ticks: { display: true },
|
||||||
|
grid: {
|
||||||
|
display: true,
|
||||||
|
drawBorder: false,
|
||||||
|
color: '#CCCCCC22',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
legend: { display: true },
|
||||||
|
title: {
|
||||||
|
display: true,
|
||||||
|
text: props.title
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
enabled: true,
|
||||||
|
backgroundColor: 'rgba(0, 0, 0, 0.8)',
|
||||||
|
titleFont: { size: 16, weight: 'bold' },
|
||||||
|
bodyFont: { size: 14 },
|
||||||
|
padding: 10,
|
||||||
|
cornerRadius: 4,
|
||||||
|
boxPadding: 10,
|
||||||
|
caretPadding: 20,
|
||||||
|
yAlign: 'bottom',
|
||||||
|
xAlign: 'center',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const chartData = ref<ChartData<'line'>>({
|
||||||
|
labels: props.labels.map(e => {
|
||||||
|
return datefns.format(new Date(e), 'dd/MM');
|
||||||
|
}),
|
||||||
|
datasets: props.datasets.map(e => ({
|
||||||
|
data: e.points,
|
||||||
|
label: e.name,
|
||||||
|
backgroundColor: [e.color + '77'],
|
||||||
|
borderColor: e.color,
|
||||||
|
borderWidth: 4,
|
||||||
|
fill: true,
|
||||||
|
tension: 0.45,
|
||||||
|
pointRadius: 0,
|
||||||
|
pointHoverRadius: 10,
|
||||||
|
hoverBackgroundColor: e.color,
|
||||||
|
hoverBorderColor: 'white',
|
||||||
|
hoverBorderWidth: 2,
|
||||||
|
type: e.type
|
||||||
|
} as any))
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
const { lineChartProps, lineChartRef } = useLineChart({ chartData: chartData, options: chartOptions });
|
||||||
|
|
||||||
|
function createGradient(startColor: string) {
|
||||||
|
const c = document.createElement('canvas');
|
||||||
|
const ctx = c.getContext("2d");
|
||||||
|
let gradient: any = `${startColor}22`;
|
||||||
|
if (ctx) {
|
||||||
|
gradient = ctx.createLinearGradient(0, 25, 0, 300);
|
||||||
|
gradient.addColorStop(0, `${startColor}99`);
|
||||||
|
gradient.addColorStop(0.35, `${startColor}66`);
|
||||||
|
gradient.addColorStop(1, `${startColor}22`);
|
||||||
|
} else {
|
||||||
|
console.warn('Cannot get context for gradient');
|
||||||
|
}
|
||||||
|
|
||||||
|
return gradient;
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
|
||||||
|
chartData.value.datasets.forEach(dataset => {
|
||||||
|
dataset.backgroundColor = [createGradient(dataset.borderColor as string)]
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<LineChart ref="lineChartRef" v-bind="lineChartProps"> </LineChart>
|
||||||
|
</template>
|
||||||
110
dashboard/components/analyst/LineChart.vue
Normal file
110
dashboard/components/analyst/LineChart.vue
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { ChartData, ChartOptions } from 'chart.js';
|
||||||
|
import { useLineChart, LineChart } from 'vue-chart-3';
|
||||||
|
registerChartComponents();
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
data: any[],
|
||||||
|
labels: string[]
|
||||||
|
color: string,
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const chartOptions = ref<ChartOptions<'line'>>({
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
interaction: {
|
||||||
|
intersect: false,
|
||||||
|
mode: 'nearest',
|
||||||
|
axis: 'x',
|
||||||
|
includeInvisible: true
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
y: {
|
||||||
|
ticks: { display: true },
|
||||||
|
grid: {
|
||||||
|
display: true,
|
||||||
|
drawBorder: false,
|
||||||
|
color: '#CCCCCC22',
|
||||||
|
// borderDash: [5, 10]
|
||||||
|
},
|
||||||
|
},
|
||||||
|
x: {
|
||||||
|
ticks: { display: true },
|
||||||
|
grid: {
|
||||||
|
display: true,
|
||||||
|
drawBorder: false,
|
||||||
|
color: '#CCCCCC22',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
legend: { display: false },
|
||||||
|
title: { display: false },
|
||||||
|
tooltip: {
|
||||||
|
enabled: true,
|
||||||
|
backgroundColor: 'rgba(0, 0, 0, 0.8)',
|
||||||
|
titleFont: { size: 16, weight: 'bold' },
|
||||||
|
bodyFont: { size: 14 },
|
||||||
|
padding: 10,
|
||||||
|
cornerRadius: 4,
|
||||||
|
boxPadding: 10,
|
||||||
|
caretPadding: 20,
|
||||||
|
yAlign: 'bottom',
|
||||||
|
xAlign: 'center',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
const chartData = ref<ChartData<'line'>>({
|
||||||
|
labels: props.labels,
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
data: props.data,
|
||||||
|
backgroundColor: [props.color + '77'],
|
||||||
|
borderColor: props.color,
|
||||||
|
borderWidth: 4,
|
||||||
|
fill: true,
|
||||||
|
tension: 0.45,
|
||||||
|
pointRadius: 0,
|
||||||
|
pointHoverRadius: 10,
|
||||||
|
hoverBackgroundColor: props.color,
|
||||||
|
hoverBorderColor: 'white',
|
||||||
|
hoverBorderWidth: 2,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
const { lineChartProps, lineChartRef } = useLineChart({ chartData: chartData, options: chartOptions });
|
||||||
|
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
|
||||||
|
const c = document.createElement('canvas');
|
||||||
|
const ctx = c.getContext("2d");
|
||||||
|
let gradient: any = `${props.color}22`;
|
||||||
|
if (ctx) {
|
||||||
|
gradient = ctx.createLinearGradient(0, 25, 0, 300);
|
||||||
|
gradient.addColorStop(0, `${props.color}99`);
|
||||||
|
gradient.addColorStop(0.35, `${props.color}66`);
|
||||||
|
gradient.addColorStop(1, `${props.color}22`);
|
||||||
|
} else {
|
||||||
|
console.warn('Cannot get context for gradient');
|
||||||
|
}
|
||||||
|
|
||||||
|
chartData.value.datasets[0].backgroundColor = [gradient];
|
||||||
|
|
||||||
|
watch(props, () => {
|
||||||
|
chartData.value.labels = props.labels;
|
||||||
|
chartData.value.datasets[0].data = props.data;
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<LineChart ref="lineChartRef" v-bind="lineChartProps"> </LineChart>
|
||||||
|
</template>
|
||||||
@@ -73,12 +73,17 @@ watch(activeProject, () => {
|
|||||||
|
|
||||||
|
|
||||||
const entries: SettingsTemplateEntry[] = [
|
const entries: SettingsTemplateEntry[] = [
|
||||||
|
// { id: 'info', title: 'Billing informations', text: 'Manage billing informations for this project' },
|
||||||
{ id: 'plan', title: 'Current plan', text: 'Manage current plat for this project' },
|
{ id: 'plan', title: 'Current plan', text: 'Manage current plat for this project' },
|
||||||
{ id: 'usage', title: 'Usage', text: 'Show usage of current project' },
|
{ id: 'usage', title: 'Usage', text: 'Show usage of current project' },
|
||||||
{ id: 'invoices', title: 'Invoices', text: 'Manage invoices of current project' },
|
{ id: 'invoices', title: 'Invoices', text: 'Manage invoices of current project' },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
const currentBillingInfo = ref<any>({
|
||||||
|
address: ''
|
||||||
|
});
|
||||||
|
|
||||||
const { visible } = usePricingDrawer();
|
const { visible } = usePricingDrawer();
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
@@ -114,6 +119,9 @@ const { visible } = usePricingDrawer();
|
|||||||
<div class="poppins font-semibold text-[2rem]"> €
|
<div class="poppins font-semibold text-[2rem]"> €
|
||||||
{{ getPremiumPrice(planData.premium_type) }} </div>
|
{{ getPremiumPrice(planData.premium_type) }} </div>
|
||||||
<div class="poppins text-text-sub mt-2"> per month </div>
|
<div class="poppins text-text-sub mt-2"> per month </div>
|
||||||
|
<div class="flex items-center ml-2">
|
||||||
|
<i class="far fa-info-circle text-[.8rem]"></i>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ const sections: Section[] = [
|
|||||||
label: 'Slack support', icon: 'fab fa-slack',
|
label: 'Slack support', icon: 'fab fa-slack',
|
||||||
premiumOnly: true,
|
premiumOnly: true,
|
||||||
action() {
|
action() {
|
||||||
if (isGuest) return;
|
if (isGuest.value === true) return;
|
||||||
if (isPremium.value === true) {
|
if (isPremium.value === true) {
|
||||||
window.open('https://join.slack.com/t/litlyx/shared_invite/zt-2q3oawn29-hZlu_fBUBlc4052Ooe3FZg', '_blank');
|
window.open('https://join.slack.com/t/litlyx/shared_invite/zt-2q3oawn29-hZlu_fBUBlc4052Ooe3FZg', '_blank');
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
"nodemailer": "^6.9.13",
|
"nodemailer": "^6.9.13",
|
||||||
"nuxt": "^3.11.2",
|
"nuxt": "^3.11.2",
|
||||||
"nuxt-vue3-google-signin": "^0.0.11",
|
"nuxt-vue3-google-signin": "^0.0.11",
|
||||||
"openai": "^4.47.1",
|
"openai": "^4.61.0",
|
||||||
"pdfkit": "^0.15.0",
|
"pdfkit": "^0.15.0",
|
||||||
"primevue": "^3.52.0",
|
"primevue": "^3.52.0",
|
||||||
"redis": "^4.6.13",
|
"redis": "^4.6.13",
|
||||||
|
|||||||
@@ -1,10 +1,15 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
definePageMeta({ layout: 'dashboard' });
|
|
||||||
|
|
||||||
|
definePageMeta({ layout: 'dashboard' });
|
||||||
|
|
||||||
const activeProject = useActiveProject();
|
const activeProject = useActiveProject();
|
||||||
|
|
||||||
const { data: chatsList, refresh: reloadChatsList } = useFetch(`/api/ai/${activeProject.value?._id}/chats_list`, signHeaders());
|
const { data: chatsList, refresh: reloadChatsList } = useFetch(`/api/ai/${activeProject.value?._id}/chats_list`, {
|
||||||
|
...signHeaders(),
|
||||||
|
transform: (data) => {
|
||||||
|
return data?.toReversed();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const { data: chatsRemaining, refresh: reloadChatsRemaining } = useFetch(`/api/ai/${activeProject.value?._id}/chats_remaining`, signHeaders());
|
const { data: chatsRemaining, refresh: reloadChatsRemaining } = useFetch(`/api/ai/${activeProject.value?._id}/chats_remaining`, signHeaders());
|
||||||
|
|
||||||
@@ -12,7 +17,7 @@ const currentText = ref<string>("");
|
|||||||
const loading = ref<boolean>(false);
|
const loading = ref<boolean>(false);
|
||||||
|
|
||||||
const currentChatId = ref<string>("");
|
const currentChatId = ref<string>("");
|
||||||
const currentChatMessages = ref<any[]>([]);
|
const currentChatMessages = ref<{ role: string, content: string, charts?: any[] }[]>([]);
|
||||||
|
|
||||||
const scroller = ref<HTMLDivElement | null>(null);
|
const scroller = ref<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
@@ -39,7 +44,7 @@ async function sendMessage() {
|
|||||||
body: JSON.stringify(body),
|
body: JSON.stringify(body),
|
||||||
...signHeaders({ 'Content-Type': 'application/json' })
|
...signHeaders({ 'Content-Type': 'application/json' })
|
||||||
});
|
});
|
||||||
currentChatMessages.value.push({ role: 'assistant', content: res });
|
currentChatMessages.value.push({ role: 'assistant', content: res.content || 'nocontent', charts: res.charts.map(e => JSON.parse(e)) });
|
||||||
|
|
||||||
await reloadChatsRemaining();
|
await reloadChatsRemaining();
|
||||||
await reloadChatsList();
|
await reloadChatsList();
|
||||||
@@ -75,7 +80,8 @@ async function openChat(chat_id?: string) {
|
|||||||
currentChatId.value = chat_id;
|
currentChatId.value = chat_id;
|
||||||
const messages = await $fetch(`/api/ai/${activeProject.value._id}/${chat_id}/get_messages`, signHeaders());
|
const messages = await $fetch(`/api/ai/${activeProject.value._id}/${chat_id}/get_messages`, signHeaders());
|
||||||
if (!messages) return;
|
if (!messages) return;
|
||||||
currentChatMessages.value = messages;
|
|
||||||
|
currentChatMessages.value = messages.map(e => ({ ...e, charts: e.charts.map(k => JSON.parse(k)) })) as any;
|
||||||
setTimeout(() => scrollToBottom(), 1);
|
setTimeout(() => scrollToBottom(), 1);
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -146,14 +152,15 @@ async function deleteChat(chat_id: string) {
|
|||||||
|
|
||||||
<div ref="scroller" class="flex flex-col w-full gap-6 px-6 xl:px-28 overflow-y-auto pb-20">
|
<div ref="scroller" class="flex flex-col w-full gap-6 px-6 xl:px-28 overflow-y-auto pb-20">
|
||||||
|
|
||||||
<div class="flex w-full" v-for="message of currentChatMessages">
|
<div class="flex w-full flex-col" v-for="message of currentChatMessages">
|
||||||
|
|
||||||
<div class="flex justify-end w-full poppins text-[1.1rem]" v-if="message.role === 'user'">
|
<div class="flex justify-end w-full poppins text-[1.1rem]" v-if="message.role === 'user'">
|
||||||
<div class="bg-lyx-widget-light px-5 py-3 rounded-lg">
|
<div class="bg-lyx-widget-light px-5 py-3 rounded-lg">
|
||||||
{{ message.content }}
|
{{ message.content }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-3 justify-start w-full poppins text-[1.1rem]"
|
<div class="flex items-center gap-3 justify-start w-full poppins text-[1.1rem]"
|
||||||
v-if="message.role === 'assistant'">
|
v-if="message.role === 'assistant' && message.content">
|
||||||
<div class="flex items-center justify-center shrink-0">
|
<div class="flex items-center justify-center shrink-0">
|
||||||
<img class="h-[3.5rem] w-auto" :src="'analyst.png'">
|
<img class="h-[3.5rem] w-auto" :src="'analyst.png'">
|
||||||
</div>
|
</div>
|
||||||
@@ -161,6 +168,15 @@ async function deleteChat(chat_id: string) {
|
|||||||
class="max-w-[70%] text-text/90 whitespace-pre-wrap">
|
class="max-w-[70%] text-text/90 whitespace-pre-wrap">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-if="message.charts && message.charts.length > 0"
|
||||||
|
class="flex items-center gap-3 justify-start w-full poppins text-[1.1rem] flex-col mt-4">
|
||||||
|
<div v-for="chart of message.charts" class="w-full">
|
||||||
|
<AnalystComposableChart :datasets="chart.datasets" :labels="chart.labels" :title="chart.title">
|
||||||
|
</AnalystComposableChart>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="loading"
|
<div v-if="loading"
|
||||||
@@ -228,7 +244,9 @@ async function deleteChat(chat_id: string) {
|
|||||||
|
|
||||||
<div class="overflow-y-auto">
|
<div class="overflow-y-auto">
|
||||||
<div class="flex flex-col gap-2 px-2">
|
<div class="flex flex-col gap-2 px-2">
|
||||||
<div :class="{ '!bg-accent/60': chat._id.toString() === currentChatId }" class="flex rounded-lg items-center gap-4 w-full px-4 bg-lyx-widget-lighter hover:bg-lyx-widget" v-for="chat of chatsList?.toReversed()">
|
<div :class="{ '!bg-accent/60': chat._id.toString() === currentChatId }"
|
||||||
|
class="flex rounded-lg items-center gap-4 w-full px-4 bg-lyx-widget-lighter hover:bg-lyx-widget"
|
||||||
|
v-for="chat of chatsList">
|
||||||
<i @click="deleteChat(chat._id.toString())"
|
<i @click="deleteChat(chat._id.toString())"
|
||||||
class="far fa-trash hover:text-gray-300 cursor-pointer"></i>
|
class="far fa-trash hover:text-gray-300 cursor-pointer"></i>
|
||||||
<div @click="openChat(chat._id.toString())"
|
<div @click="openChat(chat._id.toString())"
|
||||||
|
|||||||
73
dashboard/pnpm-lock.yaml
generated
73
dashboard/pnpm-lock.yaml
generated
@@ -48,8 +48,8 @@ importers:
|
|||||||
specifier: ^0.0.11
|
specifier: ^0.0.11
|
||||||
version: 0.0.11(@types/node@20.12.12)(rollup@4.18.0)(typescript@5.4.2)(vite@5.2.12(@types/node@20.12.12)(sass@1.77.2)(terser@5.31.0))(vue@3.4.27(typescript@5.4.2))
|
version: 0.0.11(@types/node@20.12.12)(rollup@4.18.0)(typescript@5.4.2)(vite@5.2.12(@types/node@20.12.12)(sass@1.77.2)(terser@5.31.0))(vue@3.4.27(typescript@5.4.2))
|
||||||
openai:
|
openai:
|
||||||
specifier: ^4.47.1
|
specifier: ^4.61.0
|
||||||
version: 4.47.2(encoding@0.1.13)
|
version: 4.61.0(encoding@0.1.13)
|
||||||
pdfkit:
|
pdfkit:
|
||||||
specifier: ^0.15.0
|
specifier: ^0.15.0
|
||||||
version: 0.15.0
|
version: 0.15.0
|
||||||
@@ -1189,6 +1189,9 @@ packages:
|
|||||||
'@types/pdfkit@0.13.4':
|
'@types/pdfkit@0.13.4':
|
||||||
resolution: {integrity: sha512-ixGNDHYJCCKuamY305wbfYSphZ2WPe8FPkjn8oF4fHV+PgPV4V+hecPh2VOS2h4RNtpSB3zQcR4sCpNvvrEb1A==}
|
resolution: {integrity: sha512-ixGNDHYJCCKuamY305wbfYSphZ2WPe8FPkjn8oF4fHV+PgPV4V+hecPh2VOS2h4RNtpSB3zQcR4sCpNvvrEb1A==}
|
||||||
|
|
||||||
|
'@types/qs@6.9.16':
|
||||||
|
resolution: {integrity: sha512-7i+zxXdPD0T4cKDuxCUXJ4wHcsJLwENa6Z3dCu8cfCK743OGy5Nu1RmAGqDPsoTDINVEcdXKRvR/zre+P2Ku1A==}
|
||||||
|
|
||||||
'@types/resize-observer-browser@0.1.11':
|
'@types/resize-observer-browser@0.1.11':
|
||||||
resolution: {integrity: sha512-cNw5iH8JkMkb3QkCoe7DaZiawbDQEUX8t7iuQaRTyLOyQCR2h+ibBD4GJt7p5yhUHrlOeL7ZtbxNHeipqNsBzQ==}
|
resolution: {integrity: sha512-cNw5iH8JkMkb3QkCoe7DaZiawbDQEUX8t7iuQaRTyLOyQCR2h+ibBD4GJt7p5yhUHrlOeL7ZtbxNHeipqNsBzQ==}
|
||||||
|
|
||||||
@@ -1428,20 +1431,20 @@ packages:
|
|||||||
'@vue/reactivity@3.4.27':
|
'@vue/reactivity@3.4.27':
|
||||||
resolution: {integrity: sha512-kK0g4NknW6JX2yySLpsm2jlunZJl2/RJGZ0H9ddHdfBVHcNzxmQ0sS0b09ipmBoQpY8JM2KmUw+a6sO8Zo+zIA==}
|
resolution: {integrity: sha512-kK0g4NknW6JX2yySLpsm2jlunZJl2/RJGZ0H9ddHdfBVHcNzxmQ0sS0b09ipmBoQpY8JM2KmUw+a6sO8Zo+zIA==}
|
||||||
|
|
||||||
'@vue/reactivity@3.5.4':
|
'@vue/reactivity@3.5.5':
|
||||||
resolution: {integrity: sha512-HKKbEuP7tYSGCq4e4nK6ZW6l5hyG66OUetefBp4budUyjvAYsnQDf+bgFzg2RAgnH0CInyqXwD9y47jwJEHrQw==}
|
resolution: {integrity: sha512-V4tTWElZQhT73PSK3Wnax9R9m4qvMX+LeKHnfylZc6SLh4Jc5/BPakp6e3zEhKWi5AN8TDzRkGnLkp8OqycYng==}
|
||||||
|
|
||||||
'@vue/runtime-core@3.4.27':
|
'@vue/runtime-core@3.4.27':
|
||||||
resolution: {integrity: sha512-7aYA9GEbOOdviqVvcuweTLe5Za4qBZkUY7SvET6vE8kyypxVgaT1ixHLg4urtOlrApdgcdgHoTZCUuTGap/5WA==}
|
resolution: {integrity: sha512-7aYA9GEbOOdviqVvcuweTLe5Za4qBZkUY7SvET6vE8kyypxVgaT1ixHLg4urtOlrApdgcdgHoTZCUuTGap/5WA==}
|
||||||
|
|
||||||
'@vue/runtime-core@3.5.4':
|
'@vue/runtime-core@3.5.5':
|
||||||
resolution: {integrity: sha512-f3ek2sTA0AFu0n+w+kCtz567Euqqa3eHewvo4klwS7mWfSj/A+UmYTwsnUFo35KeyAFY60JgrCGvEBsu1n/3LA==}
|
resolution: {integrity: sha512-2/CFaRN17jgsXy4MpigWFBCAMmLkXPb4CjaHrndglwYSra7ajvkH2cat21dscuXaH91G8fXAeg5gCyxWJ+wCRA==}
|
||||||
|
|
||||||
'@vue/runtime-dom@3.4.27':
|
'@vue/runtime-dom@3.4.27':
|
||||||
resolution: {integrity: sha512-ScOmP70/3NPM+TW9hvVAz6VWWtZJqkbdf7w6ySsws+EsqtHvkhxaWLecrTorFxsawelM5Ys9FnDEMt6BPBDS0Q==}
|
resolution: {integrity: sha512-ScOmP70/3NPM+TW9hvVAz6VWWtZJqkbdf7w6ySsws+EsqtHvkhxaWLecrTorFxsawelM5Ys9FnDEMt6BPBDS0Q==}
|
||||||
|
|
||||||
'@vue/runtime-dom@3.5.4':
|
'@vue/runtime-dom@3.5.5':
|
||||||
resolution: {integrity: sha512-ofyc0w6rbD5KtjhP1i9hGOKdxGpvmuB1jprP7Djlj0X7R5J/oLwuNuE98GJ8WW31Hu2VxQHtk/LYTAlW8xrJdw==}
|
resolution: {integrity: sha512-0bQGgCuL+4Muz5PsCLgF4Ata9BTdhHi5VjsxtTDyI0Wy4MgoSvBGaA6bDc7W7CGgZOyirf9LNeetMYHQ05pgpw==}
|
||||||
|
|
||||||
'@vue/server-renderer@3.4.27':
|
'@vue/server-renderer@3.4.27':
|
||||||
resolution: {integrity: sha512-dlAMEuvmeA3rJsOMJ2J1kXU7o7pOxgsNHVr9K8hB3ImIkSuBrIdy0vF66h8gf8Tuinf1TK3mPAz2+2sqyf3KzA==}
|
resolution: {integrity: sha512-dlAMEuvmeA3rJsOMJ2J1kXU7o7pOxgsNHVr9K8hB3ImIkSuBrIdy0vF66h8gf8Tuinf1TK3mPAz2+2sqyf3KzA==}
|
||||||
@@ -1451,8 +1454,8 @@ packages:
|
|||||||
'@vue/shared@3.4.27':
|
'@vue/shared@3.4.27':
|
||||||
resolution: {integrity: sha512-DL3NmY2OFlqmYYrzp39yi3LDkKxa5vZVwxWdQ3rG0ekuWscHraeIbnI8t+aZK7qhYqEqWKTUdijadunb9pnrgA==}
|
resolution: {integrity: sha512-DL3NmY2OFlqmYYrzp39yi3LDkKxa5vZVwxWdQ3rG0ekuWscHraeIbnI8t+aZK7qhYqEqWKTUdijadunb9pnrgA==}
|
||||||
|
|
||||||
'@vue/shared@3.5.4':
|
'@vue/shared@3.5.5':
|
||||||
resolution: {integrity: sha512-L2MCDD8l7yC62Te5UUyPVpmexhL9ipVnYRw9CsWfm/BGRL5FwDX4a25bcJ/OJSD3+Hx+k/a8LDKcG2AFdJV3BA==}
|
resolution: {integrity: sha512-0KyMXyEgnmFAs6rNUL+6eUHtUCqCaNrVd+AW3MX3LyA0Yry5SA0Km03CDKiOua1x1WWnIr+W9+S0GMFoSDWERQ==}
|
||||||
|
|
||||||
'@vueuse/components@10.10.0':
|
'@vueuse/components@10.10.0':
|
||||||
resolution: {integrity: sha512-HiA10NQ9HJAGnju+8ZK4TyA8LIc0a6BnJmVWDa/k+TRhaYCVacSDU04k0BQ2otV+gghUDdwu98upf6TDRXpoeg==}
|
resolution: {integrity: sha512-HiA10NQ9HJAGnju+8ZK4TyA8LIc0a6BnJmVWDa/k+TRhaYCVacSDU04k0BQ2otV+gghUDdwu98upf6TDRXpoeg==}
|
||||||
@@ -3704,9 +3707,14 @@ packages:
|
|||||||
resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==}
|
resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
openai@4.47.2:
|
openai@4.61.0:
|
||||||
resolution: {integrity: sha512-E3Wq9mYdDSLajmcJm9RO/lCegTKrQ7ilAkMbhob4UgGhTjHwIHI+mXNDNPl5+sGIUp2iVUkpoi772FjYa7JlqA==}
|
resolution: {integrity: sha512-xkygRBRLIUumxzKGb1ug05pWmJROQsHkGuj/N6Jiw2dj0dI19JvbFpErSZKmJ/DA+0IvpcugZqCAyk8iLpyM6Q==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
peerDependencies:
|
||||||
|
zod: ^3.23.8
|
||||||
|
peerDependenciesMeta:
|
||||||
|
zod:
|
||||||
|
optional: true
|
||||||
|
|
||||||
openapi-typescript@6.7.6:
|
openapi-typescript@6.7.6:
|
||||||
resolution: {integrity: sha512-c/hfooPx+RBIOPM09GSxABOZhYPblDoyaGhqBkD/59vtpN21jEuWKDlM0KYTvqJVlSYjKs0tBcIdeXKChlSPtw==}
|
resolution: {integrity: sha512-c/hfooPx+RBIOPM09GSxABOZhYPblDoyaGhqBkD/59vtpN21jEuWKDlM0KYTvqJVlSYjKs0tBcIdeXKChlSPtw==}
|
||||||
@@ -5084,10 +5092,6 @@ packages:
|
|||||||
typescript:
|
typescript:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
web-streams-polyfill@3.3.3:
|
|
||||||
resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==}
|
|
||||||
engines: {node: '>= 8'}
|
|
||||||
|
|
||||||
web-streams-polyfill@4.0.0-beta.3:
|
web-streams-polyfill@4.0.0-beta.3:
|
||||||
resolution: {integrity: sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==}
|
resolution: {integrity: sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==}
|
||||||
engines: {node: '>= 14'}
|
engines: {node: '>= 14'}
|
||||||
@@ -6547,7 +6551,7 @@ snapshots:
|
|||||||
|
|
||||||
'@types/node-fetch@2.6.11':
|
'@types/node-fetch@2.6.11':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 18.19.33
|
'@types/node': 20.12.12
|
||||||
form-data: 4.0.0
|
form-data: 4.0.0
|
||||||
|
|
||||||
'@types/node@18.19.33':
|
'@types/node@18.19.33':
|
||||||
@@ -6566,6 +6570,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 20.12.12
|
'@types/node': 20.12.12
|
||||||
|
|
||||||
|
'@types/qs@6.9.16': {}
|
||||||
|
|
||||||
'@types/resize-observer-browser@0.1.11': {}
|
'@types/resize-observer-browser@0.1.11': {}
|
||||||
|
|
||||||
'@types/resolve@1.20.2': {}
|
'@types/resolve@1.20.2': {}
|
||||||
@@ -6995,7 +7001,7 @@ snapshots:
|
|||||||
'@volar/language-core': 1.11.1
|
'@volar/language-core': 1.11.1
|
||||||
'@volar/source-map': 1.11.1
|
'@volar/source-map': 1.11.1
|
||||||
'@vue/compiler-dom': 3.4.27
|
'@vue/compiler-dom': 3.4.27
|
||||||
'@vue/shared': 3.5.4
|
'@vue/shared': 3.5.5
|
||||||
computeds: 0.0.1
|
computeds: 0.0.1
|
||||||
minimatch: 9.0.4
|
minimatch: 9.0.4
|
||||||
muggle-string: 0.3.1
|
muggle-string: 0.3.1
|
||||||
@@ -7008,19 +7014,19 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@vue/shared': 3.4.27
|
'@vue/shared': 3.4.27
|
||||||
|
|
||||||
'@vue/reactivity@3.5.4':
|
'@vue/reactivity@3.5.5':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@vue/shared': 3.5.4
|
'@vue/shared': 3.5.5
|
||||||
|
|
||||||
'@vue/runtime-core@3.4.27':
|
'@vue/runtime-core@3.4.27':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@vue/reactivity': 3.4.27
|
'@vue/reactivity': 3.4.27
|
||||||
'@vue/shared': 3.4.27
|
'@vue/shared': 3.4.27
|
||||||
|
|
||||||
'@vue/runtime-core@3.5.4':
|
'@vue/runtime-core@3.5.5':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@vue/reactivity': 3.5.4
|
'@vue/reactivity': 3.5.5
|
||||||
'@vue/shared': 3.5.4
|
'@vue/shared': 3.5.5
|
||||||
|
|
||||||
'@vue/runtime-dom@3.4.27':
|
'@vue/runtime-dom@3.4.27':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -7028,11 +7034,11 @@ snapshots:
|
|||||||
'@vue/shared': 3.4.27
|
'@vue/shared': 3.4.27
|
||||||
csstype: 3.1.3
|
csstype: 3.1.3
|
||||||
|
|
||||||
'@vue/runtime-dom@3.5.4':
|
'@vue/runtime-dom@3.5.5':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@vue/reactivity': 3.5.4
|
'@vue/reactivity': 3.5.5
|
||||||
'@vue/runtime-core': 3.5.4
|
'@vue/runtime-core': 3.5.5
|
||||||
'@vue/shared': 3.5.4
|
'@vue/shared': 3.5.5
|
||||||
csstype: 3.1.3
|
csstype: 3.1.3
|
||||||
|
|
||||||
'@vue/server-renderer@3.4.27(vue@3.4.27(typescript@5.4.2))':
|
'@vue/server-renderer@3.4.27(vue@3.4.27(typescript@5.4.2))':
|
||||||
@@ -7043,7 +7049,7 @@ snapshots:
|
|||||||
|
|
||||||
'@vue/shared@3.4.27': {}
|
'@vue/shared@3.4.27': {}
|
||||||
|
|
||||||
'@vue/shared@3.5.4': {}
|
'@vue/shared@3.5.5': {}
|
||||||
|
|
||||||
'@vueuse/components@10.10.0(vue@3.4.27(typescript@5.4.2))':
|
'@vueuse/components@10.10.0(vue@3.4.27(typescript@5.4.2))':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -9585,16 +9591,17 @@ snapshots:
|
|||||||
is-docker: 2.2.1
|
is-docker: 2.2.1
|
||||||
is-wsl: 2.2.0
|
is-wsl: 2.2.0
|
||||||
|
|
||||||
openai@4.47.2(encoding@0.1.13):
|
openai@4.61.0(encoding@0.1.13):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 18.19.33
|
'@types/node': 18.19.33
|
||||||
'@types/node-fetch': 2.6.11
|
'@types/node-fetch': 2.6.11
|
||||||
|
'@types/qs': 6.9.16
|
||||||
abort-controller: 3.0.0
|
abort-controller: 3.0.0
|
||||||
agentkeepalive: 4.5.0
|
agentkeepalive: 4.5.0
|
||||||
form-data-encoder: 1.7.2
|
form-data-encoder: 1.7.2
|
||||||
formdata-node: 4.4.1
|
formdata-node: 4.4.1
|
||||||
node-fetch: 2.7.0(encoding@0.1.13)
|
node-fetch: 2.7.0(encoding@0.1.13)
|
||||||
web-streams-polyfill: 3.3.3
|
qs: 6.12.1
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- encoding
|
- encoding
|
||||||
|
|
||||||
@@ -11035,8 +11042,8 @@ snapshots:
|
|||||||
|
|
||||||
vue-chart-3@3.1.8(chart.js@3.9.1)(vue@3.4.27(typescript@5.4.2)):
|
vue-chart-3@3.1.8(chart.js@3.9.1)(vue@3.4.27(typescript@5.4.2)):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@vue/runtime-core': 3.5.4
|
'@vue/runtime-core': 3.5.5
|
||||||
'@vue/runtime-dom': 3.5.4
|
'@vue/runtime-dom': 3.5.5
|
||||||
chart.js: 3.9.1
|
chart.js: 3.9.1
|
||||||
csstype: 3.1.3
|
csstype: 3.1.3
|
||||||
lodash-es: 4.17.21
|
lodash-es: 4.17.21
|
||||||
@@ -11105,8 +11112,6 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
typescript: 5.4.2
|
typescript: 5.4.2
|
||||||
|
|
||||||
web-streams-polyfill@3.3.3: {}
|
|
||||||
|
|
||||||
web-streams-polyfill@4.0.0-beta.3: {}
|
web-streams-polyfill@4.0.0-beta.3: {}
|
||||||
|
|
||||||
webidl-conversions@3.0.1: {}
|
webidl-conversions@3.0.1: {}
|
||||||
|
|||||||
30
dashboard/server/ai/Plugin.ts
Normal file
30
dashboard/server/ai/Plugin.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
67
dashboard/server/ai/functions/AI_ComposableChart.ts
Normal file
67
dashboard/server/ai/functions/AI_ComposableChart.ts
Normal 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();
|
||||||
80
dashboard/server/ai/functions/AI_Events.ts
Normal file
80
dashboard/server/ai/functions/AI_Events.ts
Normal 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();
|
||||||
|
|
||||||
87
dashboard/server/ai/functions/AI_Visits.ts
Normal file
87
dashboard/server/ai/functions/AI_Visits.ts
Normal 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();
|
||||||
@@ -1,8 +1,5 @@
|
|||||||
import { getUserProjectFromId } from "~/server/LIVE_DEMO_DATA";
|
import { getUserProjectFromId } from "~/server/LIVE_DEMO_DATA";
|
||||||
import { AiChatModel } from "@schema/ai/AiChatSchema";
|
import { AiChatModel } from "@schema/ai/AiChatSchema";
|
||||||
import { sendMessageOnChat } from "~/server/services/AiService";
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export default defineEventHandler(async event => {
|
export default defineEventHandler(async event => {
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { getUserProjectFromId } from "~/server/LIVE_DEMO_DATA";
|
import { getUserProjectFromId } from "~/server/LIVE_DEMO_DATA";
|
||||||
import { AiChatModel } from "@schema/ai/AiChatSchema";
|
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 => {
|
export default defineEventHandler(async event => {
|
||||||
|
|
||||||
@@ -19,11 +18,14 @@ export default defineEventHandler(async event => {
|
|||||||
const chat = await AiChatModel.findOne({ _id: chat_id, project_id });
|
const chat = await AiChatModel.findOne({ _id: chat_id, project_id });
|
||||||
if (!chat) return;
|
if (!chat) return;
|
||||||
|
|
||||||
const messages = chat.messages.filter(e => {
|
return (chat.messages as OpenAI.Chat.Completions.ChatCompletionMessageParam[])
|
||||||
return (e.role == 'user' || (e.role == 'assistant' && e.content != undefined))
|
.filter(e => e.role === 'assistant' || e.role === 'user')
|
||||||
}).map(e => {
|
.map(e => {
|
||||||
return { role: e.role, content: e.content }
|
const charts = getChartsInMessage(e);
|
||||||
});
|
const content = e.content;
|
||||||
|
return { role: e.role, content, charts }
|
||||||
return messages;
|
})
|
||||||
|
.filter(e=>{
|
||||||
|
return e.charts.length > 0 || e.content
|
||||||
|
})
|
||||||
});
|
});
|
||||||
@@ -23,5 +23,6 @@ export default defineEventHandler(async event => {
|
|||||||
if (chatsRemaining <= 0) return setResponseStatus(event, 400, 'CHAT_LIMIT_REACHED');
|
if (chatsRemaining <= 0) return setResponseStatus(event, 400, 'CHAT_LIMIT_REACHED');
|
||||||
|
|
||||||
const response = await sendMessageOnChat(text, project._id.toString(), chat_id);
|
const response = await sendMessageOnChat(text, project._id.toString(), chat_id);
|
||||||
return response || 'Error getting response';
|
|
||||||
|
return response;
|
||||||
});
|
});
|
||||||
@@ -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 };
|
|
||||||
}
|
|
||||||
@@ -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 };
|
|
||||||
}
|
|
||||||
@@ -8,11 +8,6 @@ const config = useRuntimeConfig();
|
|||||||
let connection: mongoose.Mongoose;
|
let connection: mongoose.Mongoose;
|
||||||
|
|
||||||
|
|
||||||
let anomalyMinutesCount = 0;
|
|
||||||
function anomalyCheck() {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export default async () => {
|
export default async () => {
|
||||||
|
|
||||||
console.log('[SERVER] Initializing');
|
console.log('[SERVER] Initializing');
|
||||||
|
|||||||
@@ -1,59 +1,35 @@
|
|||||||
|
|
||||||
import { getVisitsCountFromDateRange } from '~/server/api/ai/functions/AI_Visits';
|
|
||||||
|
|
||||||
import OpenAI from "openai";
|
import OpenAI from "openai";
|
||||||
import { AiChatModel } from '@schema/ai/AiChatSchema';
|
import { AiChatModel } from '@schema/ai/AiChatSchema';
|
||||||
import { AI_EventsFunctions, AI_EventsTools } from '../api/ai/functions/AI_Events';
|
|
||||||
import { ProjectCountModel } from '@schema/ProjectsCounts';
|
import { ProjectCountModel } from '@schema/ProjectsCounts';
|
||||||
import { ProjectLimitModel } from '@schema/ProjectsLimits';
|
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 { AI_ORG, AI_PROJECT, AI_KEY } = useRuntimeConfig();
|
||||||
|
|
||||||
|
const OPENAI_MODEL: OpenAI.Chat.ChatModel = 'gpt-4o-mini';
|
||||||
|
|
||||||
const openai = new OpenAI({
|
const openai = new OpenAI({
|
||||||
organization: AI_ORG,
|
organization: AI_ORG,
|
||||||
project: AI_PROJECT,
|
project: AI_PROJECT,
|
||||||
apiKey: AI_KEY
|
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[] = [
|
const tools: OpenAI.Chat.Completions.ChatCompletionTool[] = [
|
||||||
get_visits_count_Schema,
|
...AiVisitsInstance.getTools(),
|
||||||
...AI_EventsTools
|
...AiEventsInstance.getTools(),
|
||||||
|
...AiComposableChartInstance.getTools()
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
const functions: any = {
|
const functions: any = {
|
||||||
get_current_date: async ({ }) => {
|
...AiVisitsInstance.getHandlers(),
|
||||||
return new Date().toISOString();
|
...AiEventsInstance.getHandlers(),
|
||||||
},
|
...AiComposableChartInstance.getHandlers()
|
||||||
get_visits_count: async ({ pid, from, to }: any) => {
|
|
||||||
return await getVisitsCountFromDateRange(pid, from, to);
|
|
||||||
},
|
|
||||||
...AI_EventsFunctions
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -81,6 +57,14 @@ async function setChatTitle(title: string, chat_id?: string) {
|
|||||||
await AiChatModel.updateOne({ _id: chat_id }, { title });
|
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) {
|
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);
|
await setChatTitle(text.substring(0, 110), chat_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
const userMessage: OpenAI.Chat.Completions.ChatCompletionMessageParam = {
|
const userMessage: OpenAI.Chat.Completions.ChatCompletionMessageParam = { role: 'user', content: text }
|
||||||
role: 'user', content: text
|
|
||||||
}
|
|
||||||
messages.push(userMessage);
|
messages.push(userMessage);
|
||||||
await addMessageToChat(userMessage, chat_id);
|
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;
|
const chartsData: string[][] = [];
|
||||||
let toolCalls = responseMessage.tool_calls;
|
|
||||||
|
|
||||||
await addMessageToChat(responseMessage, chat_id);
|
while ((response.choices[0].message.tool_calls?.length || 0) > 0) {
|
||||||
messages.push(responseMessage);
|
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) {
|
for (const toolCall of response.choices[0].message.tool_calls) {
|
||||||
console.log({ toolCalls: toolCalls.length });
|
const functionName = toolCall.function.name;
|
||||||
for (const toolCall of toolCalls) {
|
console.log('Calling tool function', functionName);
|
||||||
const functionName = toolCall.function.name;
|
const functionToCall = functions[functionName];
|
||||||
const functionToCall = functions[functionName];
|
const functionArgs = JSON.parse(toolCall.function.arguments);
|
||||||
const functionArgs = JSON.parse(toolCall.function.arguments);
|
const functionResponse = await functionToCall({ project_id: pid, ...functionArgs });
|
||||||
console.log('CALLING FUNCTION', functionName, 'WITH PARAMS', functionArgs);
|
messages.push({ tool_call_id: toolCall.id, role: "tool", content: JSON.stringify(functionResponse) });
|
||||||
const functionResponse = await functionToCall({ pid, ...functionArgs });
|
await addMessageToChat({ tool_call_id: toolCall.id, role: "tool", content: JSON.stringify(functionResponse) }, chat_id);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
response = await openai.chat.completions.create({ model: 'gpt-4o', messages, n: 1, tools });
|
response = await openai.chat.completions.create({ model: OPENAI_MODEL, messages, n: 1, tools });
|
||||||
responseMessage = response.choices[0].message;
|
|
||||||
toolCalls = responseMessage.tool_calls;
|
|
||||||
|
|
||||||
await addMessageToChat(responseMessage, chat_id);
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
await addMessageToChat(response.choices[0].message, chat_id);
|
||||||
await ProjectLimitModel.updateOne({ project_id: pid }, { $inc: { ai_messages: 1 } })
|
await ProjectLimitModel.updateOne({ project_id: pid }, { $inc: { ai_messages: 1 } })
|
||||||
|
return { content: response.choices[0].message.content, charts: chartsData.filter(e => e.length > 0).flat() };
|
||||||
return responseMessage.content;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { getPlanFromId, getPlanFromTag } from '@data/PREMIUM';
|
import { getPlanFromId, getPlanFromTag, PREMIUM_TAG } from '@data/PREMIUM';
|
||||||
import Stripe from 'stripe';
|
import Stripe from 'stripe';
|
||||||
|
|
||||||
class StripeService {
|
class StripeService {
|
||||||
@@ -133,9 +133,23 @@ class StripeService {
|
|||||||
return deleted;
|
return deleted;
|
||||||
}
|
}
|
||||||
|
|
||||||
async createOneTimeCoupon() {
|
async createStripeCode(plan: PREMIUM_TAG) {
|
||||||
if (this.disabledMode) return;
|
if (this.disabledMode) return;
|
||||||
if (!this.stripe) throw Error('Stripe not initialized');
|
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) {
|
async createOneTimeSubscriptionDummy(customer_id: string, planId: number) {
|
||||||
|
|||||||
Reference in New Issue
Block a user