add llm functionality

This commit is contained in:
Matthias Nannt
2024-08-21 16:35:33 +02:00
parent da124bbbbe
commit a69df3baf0
22 changed files with 394 additions and 129 deletions

View File

@@ -1,20 +1,20 @@
"use server";
import { getEmailTemplateHtml } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/lib/emailTemplate";
import { generateText } from "ai";
import { customAlphabet } from "nanoid";
import { AuthOptions } from "next-auth";
import { z } from "zod";
import { sendEmbedSurveyPreviewEmail } from "@formbricks/email";
import { authenticatedActionClient } from "@formbricks/lib/actionClient";
import { checkAuthorization } from "@formbricks/lib/actionClient/utils";
import { authOptions } from "@formbricks/lib/authOptions";
import { getEmbeddingsByTypeAndReferenceId } from "@formbricks/lib/embedding/service";
import { getQuestionResponseReferenceId } from "@formbricks/lib/embedding/utils";
import { llmModel } from "@formbricks/lib/ai";
import { clusterDocuments } from "@formbricks/lib/document/kmeans";
import { getDocumentsByTypeAndReferenceId } from "@formbricks/lib/document/service";
import { getQuestionResponseReferenceId } from "@formbricks/lib/document/utils";
import { getOrganizationIdFromSurveyId } from "@formbricks/lib/organization/utils";
import { canUserAccessSurvey } from "@formbricks/lib/survey/auth";
import { getSurvey, updateSurvey } from "@formbricks/lib/survey/service";
import { ZId } from "@formbricks/types/environment";
import { AuthorizationError, ResourceNotFoundError } from "@formbricks/types/errors";
import { ResourceNotFoundError } from "@formbricks/types/errors";
const ZSendEmbedSurveyPreviewEmailAction = z.object({
surveyId: ZId,
@@ -160,12 +160,37 @@ export const getOpenTextSummaryAction = authenticatedActionClient
rules: ["survey", "read"],
});
const embeddings = await getEmbeddingsByTypeAndReferenceId(
const survey = await getSurvey(parsedInput.surveyId);
if (!survey) {
throw new ResourceNotFoundError("Survey", parsedInput.surveyId);
}
const documents = await getDocumentsByTypeAndReferenceId(
"questionResponse",
getQuestionResponseReferenceId(parsedInput.surveyId, parsedInput.questionId)
);
console.log(embeddings);
const topics = await clusterDocuments(documents, 3);
return;
const question = survey.questions.find((q) => q.id === parsedInput.questionId);
const prompt = `You are an AI research assistant and answer the question: "${question?.headline.default}". Please provide a short summary sentence and provide 3 bullet points for insights you got from these samples:\n${topics.map((t) => t.centralDocument.text).join("\n")}`;
try {
const { text } = await generateText({
model: llmModel,
prompt,
});
return text;
} catch (error) {
console.error(error);
throw new Error("Failed to generate summary");
}
/* return `Users report mixed experiences with the app's performance, with particular concerns about the dashboard's load time and afternoon slowdowns.
### Insights:
1. **Dashboard Performance**: The most common feedback is that the dashboard is slow to load, impacting user experience.
2. **Afternoon Slowdown**: Several users notice that the app slows down in the afternoon, while it runs smoothly in the morning.
3. **Varied Experiences**: Some users do not experience any performance issues, indicating that the problem may not be universal.`; */
});

View File

@@ -1,12 +1,15 @@
import { getOpenTextSummaryAction } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/actions";
import Markdown from "markdown-to-jsx";
import Link from "next/link";
import { useState } from "react";
import { getPersonIdentifier } from "@formbricks/lib/person/utils";
import { timeSince } from "@formbricks/lib/time";
import { TAttributeClass } from "@formbricks/types/attribute-classes";
import { TSurvey, TSurveyQuestionSummaryOpenText } from "@formbricks/types/surveys/types";
import { Alert, AlertDescription, AlertTitle } from "@formbricks/ui/Alert";
import { PersonAvatar } from "@formbricks/ui/Avatars";
import { Button } from "@formbricks/ui/Button";
import { LoadingSpinner } from "@formbricks/ui/LoadingSpinner";
import { QuestionSummaryHeader } from "./QuestionSummaryHeader";
interface OpenTextSummaryProps {
@@ -14,6 +17,7 @@ interface OpenTextSummaryProps {
environmentId: string;
survey: TSurvey;
attributeClasses: TAttributeClass[];
isAiEnabled: boolean;
}
export const OpenTextSummary = ({
@@ -21,8 +25,11 @@ export const OpenTextSummary = ({
environmentId,
survey,
attributeClasses,
isAiEnabled,
}: OpenTextSummaryProps) => {
const [visibleResponses, setVisibleResponses] = useState(10);
const [isLoadingAiSummary, setIsLoadingAiSummary] = useState(false);
const [aiSummary, setAiSummary] = useState<string | null>(null);
const handleLoadMore = () => {
// Increase the number of visible responses by 10, not exceeding the total number of responses
@@ -32,8 +39,19 @@ export const OpenTextSummary = ({
};
const getOpenTextSummary = async () => {
setIsLoadingAiSummary(true);
// This function is not implemented yet
await getOpenTextSummaryAction(survey.id, questionSummary.question.id);
const res = await getOpenTextSummaryAction({
surveyId: survey.id,
questionId: questionSummary.question.id,
});
const openTextSummary = res?.data;
if (openTextSummary) {
setAiSummary(openTextSummary);
} else {
setAiSummary("No summary available");
}
setIsLoadingAiSummary(false);
};
return (
@@ -43,7 +61,32 @@ export const OpenTextSummary = ({
survey={survey}
attributeClasses={attributeClasses}
/>
<Button onClick={() => getOpenTextSummary()}>Create Summary</Button>
<div className="p-4">
{isAiEnabled && (
<>
<Alert variant="info">
<AlertTitle> AI Summary</AlertTitle>
{isLoadingAiSummary && <LoadingSpinner />}
{!isLoadingAiSummary && aiSummary && (
<>
<hr className="my-4 text-slate-200" />
<AlertDescription>
<Markdown>{aiSummary}</Markdown>
</AlertDescription>
</>
)}
<hr className="my-4 text-slate-200" />
{questionSummary.responseCount < 10 ? (
<p className="text-sm">This question needs at least 10 responses to access AI summaries</p>
) : (
<Button onClick={() => getOpenTextSummary()} disabled={isLoadingAiSummary}>
Generate Summary
</Button>
)}
</Alert>
</>
)}
</div>
<div className="">
<div className="grid h-10 grid-cols-4 items-center border-y border-slate-200 bg-slate-100 text-sm font-bold text-slate-600">
<div className="pl-4 md:pl-6">User</div>

View File

@@ -37,6 +37,7 @@ interface SummaryListProps {
survey: TSurvey;
totalResponseCount: number;
attributeClasses: TAttributeClass[];
isAiEnabled: boolean;
}
export const SummaryList = ({
@@ -46,6 +47,7 @@ export const SummaryList = ({
survey,
totalResponseCount,
attributeClasses,
isAiEnabled,
}: SummaryListProps) => {
const { setSelectedFilter, selectedFilter } = useResponseFilter();
const widgetSetupCompleted =
@@ -129,6 +131,7 @@ export const SummaryList = ({
environmentId={environment.id}
survey={survey}
attributeClasses={attributeClasses}
isAiEnabled={isAiEnabled}
/>
);
}

View File

@@ -46,6 +46,7 @@ interface SummaryPageProps {
user?: TUser;
totalResponseCount: number;
attributeClasses: TAttributeClass[];
isAiEnabled: boolean;
}
export const SummaryPage = ({
@@ -55,6 +56,7 @@ export const SummaryPage = ({
webAppUrl,
totalResponseCount,
attributeClasses,
isAiEnabled,
}: SummaryPageProps) => {
const params = useParams();
const sharingKey = params.sharingKey as string;
@@ -161,6 +163,7 @@ export const SummaryPage = ({
environment={environment}
totalResponseCount={totalResponseCount}
attributeClasses={attributeClasses}
isAiEnabled={isAiEnabled}
/>
</>
);

View File

@@ -5,7 +5,7 @@ import { getServerSession } from "next-auth";
import { notFound } from "next/navigation";
import { getAttributeClasses } from "@formbricks/lib/attributeClass/service";
import { authOptions } from "@formbricks/lib/authOptions";
import { WEBAPP_URL } from "@formbricks/lib/constants";
import { IS_FORMBRICKS_CLOUD, WEBAPP_URL } from "@formbricks/lib/constants";
import { getEnvironment } from "@formbricks/lib/environment/service";
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
import { getAccessFlags } from "@formbricks/lib/membership/utils";
@@ -60,6 +60,9 @@ const Page = async ({ params }) => {
const totalResponseCount = await getResponseCountBySurveyId(params.surveyId);
const { isViewer } = getAccessFlags(currentUserMembership?.role);
const isAiEnabled =
IS_FORMBRICKS_CLOUD &&
(organization.billing.plan === "scale" || organization.billing.plan === "enterprise");
return (
<PageContentWrapper>
@@ -89,6 +92,7 @@ const Page = async ({ params }) => {
user={user}
totalResponseCount={totalResponseCount}
attributeClasses={attributeClasses}
isAiEnabled={isAiEnabled}
/>
</PageContentWrapper>
);

View File

@@ -5,10 +5,9 @@ import { headers } from "next/headers";
import { prisma } from "@formbricks/database";
import { sendResponseFinishedEmail } from "@formbricks/email";
import { embeddingsModel } from "@formbricks/lib/ai";
import { IS_AI_ENABLED, IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants";
import { CRON_SECRET } from "@formbricks/lib/constants";
import { createEmbedding } from "@formbricks/lib/embedding/service";
import { getQuestionResponseReferenceId } from "@formbricks/lib/embedding/utils";
import { CRON_SECRET, IS_AI_ENABLED, IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants";
import { createDocument } from "@formbricks/lib/document/service";
import { getQuestionResponseReferenceId } from "@formbricks/lib/document/utils";
import { getIntegrations } from "@formbricks/lib/integration/service";
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
@@ -167,17 +166,11 @@ export const POST = async (request: Request) => {
// generate embeddings for all open text question responses for enterprise and scale plans
const hasSurveyOpenTextQuestions = survey.questions.some((question) => question.type === "openText");
console.log("hasSurveyOpenTextQuestions", hasSurveyOpenTextQuestions);
console.log("is Cloud", hasSurveyOpenTextQuestions && IS_FORMBRICKS_CLOUD && IS_AI_ENABLED);
if (hasSurveyOpenTextQuestions && IS_FORMBRICKS_CLOUD && IS_AI_ENABLED) {
const organization = await getOrganizationByEnvironmentId(environmentId);
if (!organization) {
throw new Error("Organization not found");
}
console.log(
"valid billing plan",
organization.billing.plan === "enterprise" || organization.billing.plan === "scale"
);
if (organization.billing.plan === "enterprise" || organization.billing.plan === "scale") {
for (const question of survey.questions) {
if (question.type === "openText") {
@@ -186,13 +179,17 @@ export const POST = async (request: Request) => {
if (!isQuestionAnswered) {
continue;
}
const text = `${question.headline.default} Answer: ${response.data[question.id]}`;
const { embedding } = await embed({
model: embeddingsModel,
value: `${question.headline.default} Answer: ${response.data[question.id]}`,
value: text,
});
await createEmbedding({
console.log("creating embedding for question response", question.id);
await createDocument({
environmentId,
referenceId: getQuestionResponseReferenceId(survey.id, question.id),
type: "questionResponse",
text,
vector: embedding,
});
}

View File

@@ -56,6 +56,7 @@ const Page = async ({ params }) => {
webAppUrl={WEBAPP_URL}
totalResponseCount={totalResponseCount}
attributeClasses={attributeClasses}
isAiEnabled={false} // Disable AI for sharing page for now
/>
</PageContentWrapper>
</div>

View File

@@ -44,6 +44,7 @@
"lodash": "^4.17.21",
"lru-cache": "^11.0.0",
"lucide-react": "^0.427.0",
"markdown-to-jsx": "^7.5.0",
"mime": "^4.0.4",
"next": "14.2.5",
"next-safe-action": "^7.6.2",

View File

@@ -1,24 +0,0 @@
-- CreateExtension
CREATE EXTENSION IF NOT EXISTS "vector";
-- CreateEnum
CREATE TYPE "EmbeddingType" AS ENUM ('questionResponse');
-- CreateTable
CREATE TABLE "Embedding" (
"id" TEXT NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
"environmentId" TEXT NOT NULL,
"type" "EmbeddingType" NOT NULL,
"referenceId" TEXT NOT NULL,
"vector" vector(512),
CONSTRAINT "Embedding_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE INDEX "Embedding_type_referenceId_idx" ON "Embedding"("type", "referenceId");
-- AddForeignKey
ALTER TABLE "Embedding" ADD CONSTRAINT "Embedding_environmentId_fkey" FOREIGN KEY ("environmentId") REFERENCES "Environment"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@@ -0,0 +1,25 @@
-- CreateExtension
CREATE EXTENSION IF NOT EXISTS "vector";
-- CreateEnum
CREATE TYPE "DocumentType" AS ENUM ('questionResponse');
-- CreateTable
CREATE TABLE "Document" (
"id" TEXT NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
"environmentId" TEXT NOT NULL,
"type" "DocumentType" NOT NULL,
"referenceId" TEXT NOT NULL,
"text" TEXT NOT NULL,
"vector" vector(512),
CONSTRAINT "Document_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE INDEX "Document_type_referenceId_idx" ON "Document"("type", "referenceId");
-- AddForeignKey
ALTER TABLE "Document" ADD CONSTRAINT "Document_environmentId_fkey" FOREIGN KEY ("environmentId") REFERENCES "Environment"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@@ -415,7 +415,7 @@ model Environment {
tags Tag[]
segments Segment[]
integration Integration[]
embeddings Embedding[]
documents Document[]
@@index([productId])
}
@@ -661,18 +661,19 @@ model SurveyLanguage {
@@index([languageId])
}
enum EmbeddingType {
enum DocumentType {
questionResponse
}
model Embedding {
model Document {
id String @id @default(cuid())
createdAt DateTime @default(now()) @map(name: "created_at")
updatedAt DateTime @updatedAt @map(name: "updated_at")
environmentId String
environment Environment @relation(fields: [environmentId], references: [id], onDelete: Cascade)
type EmbeddingType
type DocumentType
referenceId String
text String
vector Unsupported("vector(512)")?
@@index([type, referenceId])

View File

@@ -1,12 +1,14 @@
import { createAzure } from "@ai-sdk/azure";
import { env } from "./env";
const azure = createAzure({
resourceName: env.AI_AZURE_RESSOURCE_NAME, // Azure resource name
apiKey: env.AI_AZURE_API_KEY, // Azure API key
});
export const llmModel = createAzure({
resourceName: env.AI_AZURE_LLM_RESSOURCE_NAME, // Azure resource name
apiKey: env.AI_AZURE_LLM_API_KEY, // Azure API key
})(env.AI_AZURE_LLM_DEPLOYMENT_ID || "llm");
export const llmModel = azure(env.AI_AZURE_LLM_DEPLOYMENT_ID || "llm");
export const embeddingsModel = azure.embedding(env.AI_AZURE_EMBEDDINGS_DEPLOYMENT_ID || "embeddings", {
export const embeddingsModel = createAzure({
resourceName: env.AI_AZURE_EMBEDDINGS_RESSOURCE_NAME, // Azure resource name
apiKey: env.AI_AZURE_EMBEDDINGS_API_KEY, // Azure API key
}).embedding(env.AI_AZURE_EMBEDDINGS_DEPLOYMENT_ID || "embeddings", {
dimensions: 512,
});

View File

@@ -211,7 +211,10 @@ export const BILLING_LIMITS = {
} as const;
export const IS_AI_ENABLED = !!(
env.AI_AZURE_RESSOURCE_NAME &&
env.AI_AZURE_API_KEY &&
env.AI_AZURE_EMBEDDINGS_DEPLOYMENT_ID
env.AI_AZURE_EMBEDDINGS_API_KEY &&
env.AI_AZURE_EMBEDDINGS_DEPLOYMENT_ID &&
env.AI_AZURE_EMBEDDINGS_RESSOURCE_NAME &&
env.AI_AZURE_LLM_API_KEY &&
env.AI_AZURE_LLM_DEPLOYMENT_ID &&
env.AI_AZURE_LLM_RESSOURCE_NAME
);

View File

@@ -6,13 +6,13 @@ interface RevalidateProps {
referenceId?: string;
}
export const embeddingCache = {
export const documentCache = {
tag: {
byId(id: string) {
return `embeddings-${id}`;
return `documents-${id}`;
},
byTypeAndReferenceId(type: string, id: string) {
return `embeddings-${type}-${id}`;
return `documents-${type}-${id}`;
},
},
revalidate({ id, type, referenceId }: RevalidateProps): void {

View File

@@ -0,0 +1,132 @@
import { TDocument } from "@formbricks/types/documents";
class KMeans {
private k: number;
private maxIterations: number;
private centroids: number[][];
constructor(k: number, maxIterations: number = 100) {
this.k = k;
this.maxIterations = maxIterations;
}
fit(documents: TDocument[]): number[] {
// Extract the vectors from the documents
const points = documents.map((document) => document.vector);
// Initialize centroids randomly
this.centroids = this.initializeCentroids(points);
for (let i = 0; i < this.maxIterations; i++) {
const clusters: TDocument[][] = Array.from({ length: this.k }, () => []);
// Assign documents to nearest centroid
for (const document of documents) {
const closestCentroidIndex = this.getClosestCentroidIndex(document.vector);
clusters[closestCentroidIndex].push(document);
}
// Update centroids
const newCentroids = clusters.map((cluster) =>
cluster.length > 0
? this.calculateCentroid(cluster.map((d) => d.vector))
: this.centroids[clusters.indexOf(cluster)]
);
// Check for convergence
if (this.hasConverged(newCentroids)) {
break;
}
this.centroids = newCentroids;
}
// Assign final clusters
return documents.map((document) => this.getClosestCentroidIndex(document.vector));
}
private initializeCentroids(points: number[][]): number[][] {
const centroids: number[][] = [];
const used = new Set<number>();
while (centroids.length < this.k) {
const index = Math.floor(Math.random() * points.length);
if (!used.has(index)) {
centroids.push([...points[index]]);
used.add(index);
}
}
return centroids;
}
private getClosestCentroidIndex(point: number[]): number {
let minDistance = Infinity;
let closestIndex = 0;
for (let i = 0; i < this.centroids.length; i++) {
const distance = this.euclideanDistance(point, this.centroids[i]);
if (distance < minDistance) {
minDistance = distance;
closestIndex = i;
}
}
return closestIndex;
}
private calculateCentroid(cluster: number[][]): number[] {
const dimensions = cluster[0].length;
const centroid = new Array(dimensions).fill(0);
for (const point of cluster) {
for (let i = 0; i < dimensions; i++) {
centroid[i] += point[i];
}
}
return centroid.map((sum) => sum / cluster.length);
}
private euclideanDistance(a: number[], b: number[]): number {
return Math.sqrt(a.reduce((sum, val, i) => sum + Math.pow(val - b[i], 2), 0));
}
private hasConverged(newCentroids: number[][]): boolean {
return newCentroids.every((centroid, i) => this.euclideanDistance(centroid, this.centroids[i]) < 1e-6);
}
}
export async function clusterDocuments(documents: TDocument[], numTopics: number) {
// 2. Perform clustering
const kmeans = new KMeans(numTopics);
const clusterAssignments = kmeans.fit(documents);
// 3. Analyze clusters
const clusters: TDocument[][] = Array.from({ length: numTopics }, () => []);
documents.forEach((document, index) => {
clusters[clusterAssignments[index]].push(document);
});
// 4. Find central documents and extract responses
const topics = await Promise.all(
clusters.map(async (cluster, index) => {
const centroid = kmeans["centroids"][index];
const centralDocument = cluster.reduce(
(central, document) => {
const distance = kmeans["euclideanDistance"](document.vector, centroid);
return distance < central.distance ? { document, distance } : central;
},
{ document: cluster[0], distance: Infinity }
).document;
return {
topicId: index,
centralDocument: centralDocument,
size: cluster.length,
};
})
);
return topics;
}

View File

@@ -4,48 +4,50 @@ import { cache as reactCache } from "react";
import { prisma } from "@formbricks/database";
import { ZString } from "@formbricks/types/common";
import {
TEmbedding,
TEmbeddingCreateInput,
ZEmbeddingCreateInput,
ZEmbeddingType,
} from "@formbricks/types/embedding";
TDocument,
TDocumentCreateInput,
ZDocumentCreateInput,
ZDocumentType,
} from "@formbricks/types/documents";
import { DatabaseError } from "@formbricks/types/errors";
import { cache } from "../cache";
import { validateInputs } from "../utils/validate";
import { embeddingCache } from "./cache";
import { documentCache } from "./cache";
export type TPrismaEmbedding = Omit<TEmbedding, "vector"> & {
export type TPrismaDocument = Omit<TDocument, "vector"> & {
vector: string;
};
export const createEmbedding = async (embeddingInput: TEmbeddingCreateInput): Promise<TEmbedding> => {
validateInputs([embeddingInput, ZEmbeddingCreateInput]);
export const createDocument = async (documentInput: TDocumentCreateInput): Promise<TDocument> => {
validateInputs([documentInput, ZDocumentCreateInput]);
try {
const { vector, ...data } = embeddingInput;
const { vector, ...data } = documentInput;
const prismaEmbedding = await prisma.embedding.create({
const prismaDocument = await prisma.document.create({
data,
});
const embedding = {
...prismaEmbedding,
const document = {
...prismaDocument,
vector,
};
// update vector
const vectorString = `[${vector.join(",")}]`;
await prisma.$executeRaw`
UPDATE "Embedding"
UPDATE "Document"
SET "vector" = ${vectorString}::vector(512)
WHERE "id" = ${embedding.id};
WHERE "id" = ${document.id};
`;
embeddingCache.revalidate({
referenceId: embedding.referenceId,
documentCache.revalidate({
id: document.id,
type: document.type,
referenceId: document.referenceId,
});
return embedding;
return document;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
@@ -54,39 +56,40 @@ export const createEmbedding = async (embeddingInput: TEmbeddingCreateInput): Pr
}
};
export const getEmbeddingsByTypeAndReferenceId = reactCache(
(type: string, referenceId: string): Promise<TEmbedding[]> =>
export const getDocumentsByTypeAndReferenceId = reactCache(
(type: string, referenceId: string): Promise<TDocument[]> =>
cache(
async () => {
validateInputs([type, ZEmbeddingType], [referenceId, ZString]);
validateInputs([type, ZDocumentType], [referenceId, ZString]);
try {
const prismaEmbeddings: TPrismaEmbedding[] = await prisma.$queryRaw`
const prismaDocuments: TPrismaDocument[] = await prisma.$queryRaw`
SELECT
id,
created_at AS "createdAt",
updated_at AS "updatedAt",
type,
text,
"referenceId",
vector::text
FROM "Embedding" e
WHERE e."type" = ${type}::"EmbeddingType"
FROM "Document" e
WHERE e."type" = ${type}::"DocumentType"
AND e."referenceId" = ${referenceId}
`;
const embeddings = prismaEmbeddings.map((prismaEmbedding) => {
// Convert the string representation of the embedding back to an array of numbers
const vector = prismaEmbedding.vector
const documents = prismaDocuments.map((prismaDocument) => {
// Convert the string representation of the vector back to an array of numbers
const vector = prismaDocument.vector
.slice(1, -1) // Remove the surrounding square brackets
.split(",") // Split the string into an array of strings
.map(Number); // Convert each string to a number
return {
...prismaEmbedding,
...prismaDocument,
vector,
};
});
return embeddings;
return documents;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
console.error(error);
@@ -95,9 +98,9 @@ export const getEmbeddingsByTypeAndReferenceId = reactCache(
throw error;
}
},
[`getEmbeddingsByTypeAndReferenceId-${type}-${referenceId}`],
[`getDocumentsByTypeAndReferenceId-${type}-${referenceId}`],
{
tags: [embeddingCache.tag.byTypeAndReferenceId(type, referenceId)],
tags: [documentCache.tag.byTypeAndReferenceId(type, referenceId)],
}
)()
);

View File

@@ -7,10 +7,12 @@ export const env = createEnv({
* Will throw if you access these variables on the client.
*/
server: {
AI_AZURE_API_KEY: z.string().optional(),
AI_AZURE_EMBEDDINGS_API_KEY: z.string().optional(),
AI_AZURE_LLM_API_KEY: z.string().optional(),
AI_AZURE_EMBEDDINGS_DEPLOYMENT_ID: z.string().optional(),
AI_AZURE_LLM_DEPLOYMENT_ID: z.string().optional(),
AI_AZURE_RESSOURCE_NAME: z.string().optional(),
AI_AZURE_EMBEDDINGS_RESSOURCE_NAME: z.string().optional(),
AI_AZURE_LLM_RESSOURCE_NAME: z.string().optional(),
AIRTABLE_CLIENT_ID: z.string().optional(),
AZUREAD_CLIENT_ID: z.string().optional(),
AZUREAD_CLIENT_SECRET: z.string().optional(),
@@ -117,10 +119,12 @@ export const env = createEnv({
* 💡 You'll get type errors if not all variables from `server` & `client` are included here.
*/
runtimeEnv: {
AI_AZURE_API_KEY: process.env.AI_AZURE_API_KEY,
AI_AZURE_EMBEDDINGS_API_KEY: process.env.AI_AZURE_EMBEDDINGS_API_KEY,
AI_AZURE_LLM_API_KEY: process.env.AI_AZURE_LLM_API_KEY,
AI_AZURE_EMBEDDINGS_DEPLOYMENT_ID: process.env.AI_AZURE_EMBEDDINGS_DEPLOYMENT_ID,
AI_AZURE_LLM_DEPLOYMENT_ID: process.env.AI_AZURE_LLM_DEPLOYMENT_ID,
AI_AZURE_RESSOURCE_NAME: process.env.AI_AZURE_RESSOURCE_NAME,
AI_AZURE_EMBEDDINGS_RESSOURCE_NAME: process.env.AI_AZURE_EMBEDDINGS_RESSOURCE_NAME,
AI_AZURE_LLM_RESSOURCE_NAME: process.env.AI_AZURE_LLM_RESSOURCE_NAME,
AIRTABLE_CLIENT_ID: process.env.AIRTABLE_CLIENT_ID,
AZUREAD_CLIENT_ID: process.env.AZUREAD_CLIENT_ID,
AZUREAD_CLIENT_SECRET: process.env.AZUREAD_CLIENT_SECRET,

View File

@@ -0,0 +1,28 @@
import { z } from "zod";
import { ZId } from "./environment";
export const ZDocumentType = z.enum(["questionResponse"]);
export type TDocumentType = z.infer<typeof ZDocumentType>;
export const ZDocument = z.object({
environmentId: ZId,
referenceId: z.string(),
createdAt: z.date(),
updatedAt: z.date(),
type: ZDocumentType,
text: z.string(),
vector: z.array(z.number()).length(512),
});
export type TDocument = z.infer<typeof ZDocument>;
export const ZDocumentCreateInput = z.object({
environmentId: ZId,
type: ZDocumentType,
referenceId: z.string(),
text: z.string(),
vector: z.array(z.number()).length(512),
});
export type TDocumentCreateInput = z.infer<typeof ZDocumentCreateInput>;

View File

@@ -1,21 +0,0 @@
import { z } from "zod";
export const ZEmbeddingType = z.enum(["questionResponse"]);
export const ZEmbedding = z.object({
referenceId: z.string(),
createdAt: z.date(),
updatedAt: z.date(),
type: ZEmbeddingType,
vector: z.array(z.number()).length(512),
});
export type TEmbedding = z.infer<typeof ZEmbedding>;
export const ZEmbeddingCreateInput = z.object({
type: ZEmbeddingType,
referenceId: z.string(),
vector: z.array(z.number()).length(512),
});
export type TEmbeddingCreateInput = z.infer<typeof ZEmbeddingCreateInput>;

47
pnpm-lock.yaml generated
View File

@@ -380,7 +380,7 @@ importers:
version: 0.6.2
'@vercel/speed-insights':
specifier: ^1.0.12
version: 1.0.12(next@14.2.5(@opentelemetry/api@1.9.0)(@playwright/test@1.45.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)
version: 1.0.12(next@14.2.5(@opentelemetry/api@1.9.0)(@playwright/test@1.45.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)(svelte@4.2.18)(vue@3.4.38)
bcryptjs:
specifier: ^2.4.3
version: 2.4.3
@@ -411,6 +411,9 @@ importers:
lucide-react:
specifier: ^0.427.0
version: 0.427.0(react@18.3.1)
markdown-to-jsx:
specifier: ^7.5.0
version: 7.5.0(react@18.3.1)
mime:
specifier: ^4.0.4
version: 4.0.4
@@ -546,13 +549,13 @@ importers:
devDependencies:
'@trivago/prettier-plugin-sort-imports':
specifier: ^4.3.0
version: 4.3.0(prettier@3.3.3)
version: 4.3.0(@vue/compiler-sfc@3.4.38)(prettier@3.3.3)
prettier:
specifier: ^3.3.3
version: 3.3.3
prettier-plugin-tailwindcss:
specifier: ^0.6.6
version: 0.6.6(@trivago/prettier-plugin-sort-imports@4.3.0(prettier@3.3.3))(prettier@3.3.3)
version: 0.6.6(@trivago/prettier-plugin-sort-imports@4.3.0(@vue/compiler-sfc@3.4.38)(prettier@3.3.3))(prettier@3.3.3)
packages/config-tailwind:
devDependencies:
@@ -8785,6 +8788,12 @@ packages:
peerDependencies:
react: '>= 0.14.0'
markdown-to-jsx@7.5.0:
resolution: {integrity: sha512-RrBNcMHiFPcz/iqIj0n3wclzHXjwS7mzjBNWecKKVhNTIxQepIix6Il/wZCn2Cg5Y1ow2Qi84+eJrryFRWBEWw==}
engines: {node: '>= 10'}
peerDependencies:
react: '>= 0.14.0'
marked@7.0.4:
resolution: {integrity: sha512-t8eP0dXRJMtMvBojtkcsA7n48BkauktUKzfkPSCq85ZMTJ0v76Rke4DYz01omYpPTUh4p/f7HePgRo3ebG8+QQ==}
engines: {node: '>= 16'}
@@ -17610,7 +17619,7 @@ snapshots:
dependencies:
'@testing-library/dom': 10.1.0
'@trivago/prettier-plugin-sort-imports@4.3.0(prettier@3.3.3)':
'@trivago/prettier-plugin-sort-imports@4.3.0(@vue/compiler-sfc@3.4.38)(prettier@3.3.3)':
dependencies:
'@babel/generator': 7.17.7
'@babel/parser': 7.24.7
@@ -17619,6 +17628,8 @@ snapshots:
javascript-natural-sort: 0.7.1
lodash: 4.17.21
prettier: 3.3.3
optionalDependencies:
'@vue/compiler-sfc': 3.4.38
transitivePeerDependencies:
- supports-color
@@ -18169,10 +18180,12 @@ snapshots:
satori: 0.10.9
yoga-wasm-web: 0.3.3
'@vercel/speed-insights@1.0.12(next@14.2.5(@opentelemetry/api@1.9.0)(@playwright/test@1.45.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)':
'@vercel/speed-insights@1.0.12(next@14.2.5(@opentelemetry/api@1.9.0)(@playwright/test@1.45.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)(svelte@4.2.18)(vue@3.4.38)':
optionalDependencies:
next: 14.2.5(@opentelemetry/api@1.9.0)(@playwright/test@1.45.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
react: 18.3.1
svelte: 4.2.18
vue: 3.4.38
'@vercel/style-guide@6.0.0(@next/eslint-plugin-next@14.2.5)(eslint@8.57.0)(prettier@3.3.3)(typescript@5.5.4)(vitest@2.0.5)':
dependencies:
@@ -18361,6 +18374,13 @@ snapshots:
'@vue/shared': 3.4.38
vue: 3.4.38(typescript@5.5.4)
'@vue/server-renderer@3.4.38(vue@3.4.38)':
dependencies:
'@vue/compiler-ssr': 3.4.38
'@vue/shared': 3.4.38
vue: 3.4.38
optional: true
'@vue/shared@3.4.29': {}
'@vue/shared@3.4.38': {}
@@ -21869,6 +21889,10 @@ snapshots:
dependencies:
react: 18.3.1
markdown-to-jsx@7.5.0(react@18.3.1):
dependencies:
react: 18.3.1
marked@7.0.4: {}
md-to-react-email@5.0.2(react@18.3.1):
@@ -23225,11 +23249,11 @@ snapshots:
optionalDependencies:
prettier: 3.3.3
prettier-plugin-tailwindcss@0.6.6(@trivago/prettier-plugin-sort-imports@4.3.0(prettier@3.3.3))(prettier@3.3.3):
prettier-plugin-tailwindcss@0.6.6(@trivago/prettier-plugin-sort-imports@4.3.0(@vue/compiler-sfc@3.4.38)(prettier@3.3.3))(prettier@3.3.3):
dependencies:
prettier: 3.3.3
optionalDependencies:
'@trivago/prettier-plugin-sort-imports': 4.3.0(prettier@3.3.3)
'@trivago/prettier-plugin-sort-imports': 4.3.0(@vue/compiler-sfc@3.4.38)(prettier@3.3.3)
prettier@2.8.8: {}
@@ -25525,6 +25549,15 @@ snapshots:
semver: 7.6.2
typescript: 5.5.4
vue@3.4.38:
dependencies:
'@vue/compiler-dom': 3.4.38
'@vue/compiler-sfc': 3.4.38
'@vue/runtime-dom': 3.4.38
'@vue/server-renderer': 3.4.38(vue@3.4.38)
'@vue/shared': 3.4.38
optional: true
vue@3.4.38(typescript@5.5.4):
dependencies:
'@vue/compiler-dom': 3.4.38

View File

@@ -51,10 +51,12 @@
"dependsOn": ["^build"],
"outputs": ["dist/**", ".next/**"],
"env": [
"AI_AZURE_API_KEY",
"AI_AZURE_EMBEDDINGS_API_KEY",
"AI_AZURE_LLM_API_KEY",
"AI_AZURE_EMBEDDINGS_DEPLOYMENT_ID",
"AI_AZURE_EMBEDDINGS_RESSOURCE_NAME",
"AI_AZURE_LLM_DEPLOYMENT_ID",
"AI_AZURE_RESSOURCE_NAME",
"AI_AZURE_LLM_RESSOURCE_NAME",
"AIRTABLE_CLIENT_ID",
"ASSET_PREFIX_URL",
"AZUREAD_CLIENT_ID",