From ddfa01f4a90adab4dac3f7a498b4cd4da2744c35 Mon Sep 17 00:00:00 2001 From: ogzhanolguncu Date: Tue, 30 Apr 2024 17:27:27 +0300 Subject: [PATCH] init --- bun.lockb | Bin 172168 -> 218686 bytes package.json | 6 +- src/clients/ratelimiter/index.ts | 42 +++++++ src/clients/redis/index.test.ts | 65 +++++++++++ src/clients/redis/index.ts | 84 ++++++++++++++ src/clients/vector/index.test.ts | 65 +++++++++++ src/clients/vector/index.ts | 89 +++++++++++++++ src/error/internal.ts | 6 + src/error/model.ts | 6 + src/index.ts | 183 +++++++++++++++++++++++++++++++ src/prompts.ts | 18 +++ src/redis-custom-history.ts | 122 +++++++++++++++++++++ src/types.ts | 1 + src/utils.ts | 19 ++++ 14 files changed, 705 insertions(+), 1 deletion(-) create mode 100644 src/clients/ratelimiter/index.ts create mode 100644 src/clients/redis/index.test.ts create mode 100644 src/clients/redis/index.ts create mode 100644 src/clients/vector/index.test.ts create mode 100644 src/clients/vector/index.ts create mode 100644 src/error/internal.ts create mode 100644 src/error/model.ts create mode 100644 src/index.ts create mode 100644 src/prompts.ts create mode 100644 src/redis-custom-history.ts create mode 100644 src/types.ts create mode 100644 src/utils.ts diff --git a/bun.lockb b/bun.lockb index 70ad316aa68ee4220bcbae338f120b20f68fecd9..68b7b2a3be77625b61b6dd5b6ff225e7fa4dd03c 100755 GIT binary patch delta 68601 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 delta 38716 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 QkdUCIpU5-Ff2ce64^o*7g8%>k diff --git a/package.json b/package.json index 7a33ee7..6cfc1f0 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,10 @@ "vitest": "latest" }, "dependencies": { - "@upstash/vector": "^1.0.7" + "@langchain/community": "^0.0.50", + "@langchain/core": "^0.1.58", + "@langchain/openai": "^0.0.28", + "@upstash/sdk": "0.0.19-alpha", + "ai": "^3.0.35" } } diff --git a/src/clients/ratelimiter/index.ts b/src/clients/ratelimiter/index.ts new file mode 100644 index 0000000..deb3846 --- /dev/null +++ b/src/clients/ratelimiter/index.ts @@ -0,0 +1,42 @@ +import { Ratelimit } from "@upstash/sdk"; + +import type { Redis, Upstash } from "@upstash/sdk"; +import { InternalUpstashError } from "../../error/internal"; + +const DEFAULT_RATELIMITER_NAME = "@upstash-rag-chat-ratelimit"; +const MAX_ALLOWED_CHAT_REQUEST = 10; + +export class RatelimiterClientConstructor { + private redisClient?: Redis; + private ratelimiterClient?: Ratelimit; + private sdkClient: Upstash; + + constructor(sdkClient: Upstash, redisClient?: Redis) { + this.redisClient = redisClient; + this.sdkClient = sdkClient; + } + + public async getRatelimiterClient(): Promise { + if (!this.ratelimiterClient) { + try { + await this.initializeRatelimiterClient(); + } catch (error) { + console.error("Failed to initialize Ratelimiter client:", error); + return undefined; + } + } + return this.ratelimiterClient; + } + + private initializeRatelimiterClient = async () => { + if (!this.redisClient) + throw new InternalUpstashError("Redis client is in missing in initializeRatelimiterClient!"); + + const ratelimiter = await this.sdkClient.newRatelimitClient(this.redisClient, { + limiter: Ratelimit.tokenBucket(MAX_ALLOWED_CHAT_REQUEST, "1d", MAX_ALLOWED_CHAT_REQUEST), + prefix: DEFAULT_RATELIMITER_NAME, + }); + + this.ratelimiterClient = ratelimiter; + }; +} diff --git a/src/clients/redis/index.test.ts b/src/clients/redis/index.test.ts new file mode 100644 index 0000000..c9d5117 --- /dev/null +++ b/src/clients/redis/index.test.ts @@ -0,0 +1,65 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +import { Upstash } from "@upstash/sdk"; +import { describe, expect, test } from "bun:test"; +import { DEFAULT_REDIS_CONFIG, DEFAULT_REDIS_DB_NAME, RedisClientConstructor } from "."; + +const upstashSDK = new Upstash({ + email: process.env.UPSTASH_EMAIL!, + token: process.env.UPSTASH_TOKEN!, +}); + +describe("Redis Client", () => { + test( + "Initialize client without db name", + async () => { + const constructor = new RedisClientConstructor({ + sdkClient: upstashSDK, + }); + const redisClient = await constructor.getRedisClient(); + + expect(redisClient).toBeTruthy(); + + await upstashSDK.deleteRedisDatabase(DEFAULT_REDIS_DB_NAME); + }, + { timeout: 10_000 } + ); + + test( + "Initialize client with db name", + async () => { + const constructor = new RedisClientConstructor({ + sdkClient: upstashSDK, + redisDbNameOrInstance: "test-name", + }); + const redisClient = await constructor.getRedisClient(); + + expect(redisClient).toBeTruthy(); + + await upstashSDK.deleteRedisDatabase("test-name"); + }, + { timeout: 10_000 } + ); + + test( + "Initialize client with existing instance", + async () => { + const dbName = DEFAULT_REDIS_CONFIG.name + "suffix"; + const redisInstance = await upstashSDK.createRedisDatabase({ + ...DEFAULT_REDIS_CONFIG, + name: dbName, + }); + const existingRedisClient = await upstashSDK.newRedisClient(redisInstance.database_name); + + const constructor = new RedisClientConstructor({ + sdkClient: upstashSDK, + redisDbNameOrInstance: existingRedisClient, + }); + const redisClient = await constructor.getRedisClient(); + + expect(redisClient).toBeTruthy(); + + await upstashSDK.deleteRedisDatabase(dbName); + }, + { timeout: 10_000 } + ); +}); diff --git a/src/clients/redis/index.ts b/src/clients/redis/index.ts new file mode 100644 index 0000000..d0876ac --- /dev/null +++ b/src/clients/redis/index.ts @@ -0,0 +1,84 @@ +import type { CreateCommandPayload, Upstash } from "@upstash/sdk"; + +import { Redis } from "@upstash/sdk"; +import type { PreferredRegions } from "../../types"; + +export const DEFAULT_REDIS_DB_NAME = "upstash-rag-chat-redis"; + +export const DEFAULT_REDIS_CONFIG: CreateCommandPayload = { + name: DEFAULT_REDIS_DB_NAME, + tls: true, + region: "us-east-1", + eviction: false, +}; + +type Config = { + sdkClient: Upstash; + redisDbNameOrInstance?: string | Redis; + preferredRegion?: PreferredRegions; +}; + +export class RedisClientConstructor { + private redisDbNameOrInstance?: string | Redis; + private preferredRegion?: PreferredRegions; + private sdkClient: Upstash; + private redisClient?: Redis; + + constructor({ sdkClient, preferredRegion, redisDbNameOrInstance }: Config) { + this.redisDbNameOrInstance = redisDbNameOrInstance; + this.sdkClient = sdkClient; + this.preferredRegion = preferredRegion ?? "us-east-1"; + } + + public async getRedisClient(): Promise { + if (!this.redisClient) { + try { + await this.initializeRedisClient(); + } catch (error) { + console.error("Failed to initialize Redis client:", error); + return undefined; + } + } + return this.redisClient; + } + + private initializeRedisClient = async () => { + const { redisDbNameOrInstance } = this; + + // Direct Redis instance provided + if (redisDbNameOrInstance instanceof Redis) { + this.redisClient = redisDbNameOrInstance; + return; + } + + // Redis name provided + if (typeof redisDbNameOrInstance === "string") { + await this.createRedisClientByName(redisDbNameOrInstance); + return; + } + + // No specific Redis information provided, using default configuration + await this.createRedisClientByDefaultConfig(); + }; + + private createRedisClientByName = async (redisDbName: string) => { + try { + const redis = await this.sdkClient.getRedisDatabase(redisDbName); + this.redisClient = await this.sdkClient.newRedisClient(redis.database_name); + } catch { + await this.createRedisClientByDefaultConfig(redisDbName); + } + }; + + private createRedisClientByDefaultConfig = async (redisDbName?: string) => { + const redisDatabase = await this.sdkClient.getOrCreateRedisDatabase({ + ...DEFAULT_REDIS_CONFIG, + name: redisDbName ?? DEFAULT_REDIS_CONFIG.name, + region: this.preferredRegion ?? DEFAULT_REDIS_CONFIG.region, + }); + + if (redisDatabase?.database_name) { + this.redisClient = await this.sdkClient.newRedisClient(redisDatabase.database_name); + } + }; +} diff --git a/src/clients/vector/index.test.ts b/src/clients/vector/index.test.ts new file mode 100644 index 0000000..250900b --- /dev/null +++ b/src/clients/vector/index.test.ts @@ -0,0 +1,65 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +import { Upstash } from "@upstash/sdk"; +import { describe, expect, test } from "bun:test"; +import { DEFAULT_VECTOR_DB_NAME, VectorClientConstructor, DEFAULT_VECTOR_CONFIG } from "."; + +const upstashSDK = new Upstash({ + email: process.env.UPSTASH_EMAIL!, + token: process.env.UPSTASH_TOKEN!, +}); + +describe("Redis Client", () => { + test( + "Initialize client without index name", + async () => { + const constructor = new VectorClientConstructor({ + sdkClient: upstashSDK, + }); + const vectorClient = await constructor.getVectorClient(); + + expect(vectorClient).toBeTruthy(); + + await upstashSDK.deleteVectorIndex(DEFAULT_VECTOR_DB_NAME); + }, + { timeout: 10_000 } + ); + + test( + "Initialize client with db name", + async () => { + const constructor = new VectorClientConstructor({ + sdkClient: upstashSDK, + indexNameOrInstance: "test-name", + }); + const redisClient = await constructor.getVectorClient(); + + expect(redisClient).toBeTruthy(); + + await upstashSDK.deleteVectorIndex("test-name"); + }, + { timeout: 10_000 } + ); + + test( + "Initialize client with existing instance", + async () => { + const indexName = DEFAULT_VECTOR_CONFIG.name + "suffix"; + const vectorInstance = await upstashSDK.createVectorIndex({ + ...DEFAULT_VECTOR_CONFIG, + name: indexName, + }); + const existingVectorClient = await upstashSDK.newVectorClient(vectorInstance.name); + + const constructor = new VectorClientConstructor({ + sdkClient: upstashSDK, + indexNameOrInstance: existingVectorClient, + }); + const vectorClient = await constructor.getVectorClient(); + + expect(vectorClient).toBeTruthy(); + + await upstashSDK.deleteVectorIndex(indexName); + }, + { timeout: 10_000 } + ); +}); diff --git a/src/clients/vector/index.ts b/src/clients/vector/index.ts new file mode 100644 index 0000000..e9bd01a --- /dev/null +++ b/src/clients/vector/index.ts @@ -0,0 +1,89 @@ +import type { CreateIndexPayload, Upstash } from "@upstash/sdk"; +import { Index } from "@upstash/sdk"; + +import type { PreferredRegions } from "../../types"; +import { InternalUpstashError } from "../../error/internal"; + +export const DEFAULT_VECTOR_DB_NAME = "upstash-rag-chat-vector"; + +export const DEFAULT_VECTOR_CONFIG: CreateIndexPayload = { + name: DEFAULT_VECTOR_DB_NAME, + similarity_function: "EUCLIDEAN", + embedding_model: "MXBAI_EMBED_LARGE_V1", + region: "us-east-1", + type: "payg", +}; + +type Config = { + sdkClient: Upstash; + indexNameOrInstance?: string | Index; + preferredRegion?: PreferredRegions; +}; + +export class VectorClientConstructor { + private indexNameOrInstance?: string | Index; + private preferredRegion?: PreferredRegions; + private sdkClient: Upstash; + private vectorClient?: Index; + + constructor({ sdkClient, preferredRegion, indexNameOrInstance: indexNameOrInstance }: Config) { + this.indexNameOrInstance = indexNameOrInstance; + this.sdkClient = sdkClient; + this.preferredRegion = preferredRegion ?? "us-east-1"; + } + + public async getVectorClient(): Promise { + if (!this.vectorClient) { + try { + await this.initializeVectorClient(); + } catch (error) { + console.error("Failed to initialize Vector client:", error); + return undefined; + } + } + return this.vectorClient; + } + + private initializeVectorClient = async () => { + const { indexNameOrInstance } = this; + + // Direct Vector instance provided + if (indexNameOrInstance instanceof Index) { + this.vectorClient = indexNameOrInstance; + return; + } + + // Vector name provided + if (typeof indexNameOrInstance === "string") { + await this.createVectorClientByName(indexNameOrInstance); + return; + } + + // No specific Vector information provided, using default configuration + await this.createVectorClientByDefaultConfig(); + }; + + private createVectorClientByName = async (indexName: string) => { + try { + const index = await this.sdkClient.getVectorIndexByName(indexName); + if (!index) throw new InternalUpstashError("Index is missing!"); + + this.vectorClient = await this.sdkClient.newVectorClient(index.name); + } catch { + await this.createVectorClientByDefaultConfig(indexName); + } + }; + + private createVectorClientByDefaultConfig = async (indexName?: string) => { + const index = await this.sdkClient.getOrCreateIndex({ + ...DEFAULT_VECTOR_CONFIG, + name: indexName ?? DEFAULT_VECTOR_CONFIG.name, + region: this.preferredRegion ?? DEFAULT_VECTOR_CONFIG.region, + }); + + if (index?.name) { + const client = await this.sdkClient.newVectorClient(index.name); + this.vectorClient = client; + } + }; +} diff --git a/src/error/internal.ts b/src/error/internal.ts new file mode 100644 index 0000000..b115f3c --- /dev/null +++ b/src/error/internal.ts @@ -0,0 +1,6 @@ +export class InternalUpstashError extends Error { + constructor(message: string) { + super(message); + this.name = "InternalUpstashError"; + } +} diff --git a/src/error/model.ts b/src/error/model.ts new file mode 100644 index 0000000..179966f --- /dev/null +++ b/src/error/model.ts @@ -0,0 +1,6 @@ +export class UpstashModelError extends Error { + constructor(message: string) { + super(message); + this.name = "UpstashModelError"; + } +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..8fb9838 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,183 @@ +import type { BaseLanguageModelInterface } from "@langchain/core/language_models/base"; +import type { PromptTemplate } from "@langchain/core/prompts"; +import type { Index, Ratelimit, Redis, UpstashConfig } from "@upstash/sdk"; +import type { Callbacks } from "@langchain/core/callbacks/manager"; +import type { BaseMessage } from "@langchain/core/messages"; +import { RunnableSequence, RunnableWithMessageHistory } from "@langchain/core/runnables"; +import { Upstash } from "@upstash/sdk"; +import { LangChainStream, StreamingTextResponse } from "ai"; + +import { RatelimiterClientConstructor } from "./clients/ratelimiter"; +import { RedisClientConstructor } from "./clients/redis"; +import { VectorClientConstructor } from "./clients/vector"; +import { InternalUpstashError } from "./error/internal"; +import { UpstashModelError } from "./error/model"; +import { QA_TEMPLATE } from "./prompts"; +import { redisChatMessagesHistory } from "./redis-custom-history"; +import type { PreferredRegions } from "./types"; +import { formatChatHistory, formatFacts, sanitizeQuestion } from "./utils"; + +const SIMILARITY_THRESHOLD = 0.5; + +type CustomInputValues = { chat_history?: BaseMessage[]; question: string; context: string }; + +type RAGChatConfigCommon = { + model: BaseLanguageModelInterface; + template?: PromptTemplate; + umbrellaConfig: Omit; + preferredRegion?: PreferredRegions; +}; + +export type RAGChatConfig = ( + | { + vector?: Index; + redis?: string; + ratelimit?: Ratelimit; + } + | { + vector?: string; + redis?: Redis; + ratelimit?: Ratelimit; + } +) & + RAGChatConfigCommon; + +export class RAGChat { + private sdkClient: Upstash; + private config?: RAGChatConfig; + + //CLIENTS + private vectorClient?: Index; + private redisClient?: Redis; + private ratelimiterClient?: Ratelimit; + + constructor(email: string, token: string, config?: RAGChatConfig) { + this.sdkClient = new Upstash({ email, token, ...config?.umbrellaConfig }); + this.config = config; + } + + private async getFactsFromVector( + question: string, + similarityThreshold = SIMILARITY_THRESHOLD + ): Promise { + if (!this.vectorClient) + throw new InternalUpstashError("vectorClient is missing in getFactsFromVector"); + + const index = this.vectorClient; + const result = await index.query<{ value: string }>({ + data: question, + topK: 5, + includeMetadata: true, + includeVectors: false, + }); + + const allValuesUndefined = result.every((embedding) => embedding.metadata?.value === undefined); + if (allValuesUndefined) { + throw new TypeError(` + Query to the vector store returned ${result.length} vectors but none had "value" field in their metadata. + Text of your vectors should be in the "value" field in the metadata for the RAG Chat. + `); + } + + const facts = result + .filter((x) => x.score >= similarityThreshold) + .map((embedding, index) => `- Context Item ${index}: ${embedding.metadata?.value ?? ""}`); + return formatFacts(facts); + } + + chat = async ( + input: string, + chatOptions: { stream: boolean; sessionId: string; includeHistory?: number } + ) => { + await this.initializeClients(); + + const question = sanitizeQuestion(input); + const facts = await this.getFactsFromVector(question); + + const { stream, sessionId, includeHistory } = chatOptions; + + if (stream) { + return this.chainCallStreaming(question, facts, sessionId, includeHistory); + } + + return this.chainCall({ sessionId, includeHistory }, question, facts); + }; + + private chainCallStreaming = ( + question: string, + facts: string, + sessionId: string, + includeHistory?: number + ) => { + const { stream, handlers } = LangChainStream(); + void this.chainCall({ sessionId, includeHistory }, question, facts, [handlers]); + return new StreamingTextResponse(stream, {}); + }; + + private chainCall( + chatOptions: { sessionId: string; includeHistory?: number }, + question: string, + facts: string, + handlers?: Callbacks + ) { + if (!this.config?.model) throw new UpstashModelError("Model is missing!"); + + const formattedHistoryChain = RunnableSequence.from([ + { + chat_history: (input) => formatChatHistory(input.chat_history ?? []), + question: (input) => input.question, + context: (input) => input.context, + }, + this.config.template ?? QA_TEMPLATE, + this.config.model, + ]); + + if (!this.redisClient) throw new InternalUpstashError("redisClient is missing in chat"); + const redis = this.redisClient; + + const chainWithMessageHistory = new RunnableWithMessageHistory({ + runnable: formattedHistoryChain, + getMessageHistory: (sessionId: string) => + redisChatMessagesHistory({ + sessionId, + redis, + length: chatOptions.includeHistory, + }), + inputMessagesKey: "question", + historyMessagesKey: "chat_history", + }); + + return chainWithMessageHistory.invoke( + { + question, + context: facts, + }, + { + callbacks: handlers ?? undefined, + configurable: { sessionId: chatOptions.sessionId }, + } + ); + } + + private async initializeClients() { + if (!this.vectorClient) + this.vectorClient = await new VectorClientConstructor({ + sdkClient: this.sdkClient, + indexNameOrInstance: this.config?.vector, + preferredRegion: this.config?.preferredRegion, + }).getVectorClient(); + + if (!this.redisClient) + this.redisClient = await new RedisClientConstructor({ + sdkClient: this.sdkClient, + redisDbNameOrInstance: this.config?.redis, + preferredRegion: this.config?.preferredRegion, + }).getRedisClient(); + + if (!this.ratelimiterClient) + this.ratelimiterClient = await new RatelimiterClientConstructor( + this.sdkClient, + this.redisClient + ).getRatelimiterClient(); + } +} diff --git a/src/prompts.ts b/src/prompts.ts new file mode 100644 index 0000000..7e55632 --- /dev/null +++ b/src/prompts.ts @@ -0,0 +1,18 @@ +import { PromptTemplate } from "@langchain/core/prompts"; + +export const QA_TEMPLATE = + PromptTemplate.fromTemplate(`You are a friendly AI assistant augmented with an Upstash Vector Store. +To help you answer the questions, a context will be provided. This context is generated by querying the vector store with the user question. +Answer the question at the end using only the information available in the context and chat history. +If the answer is not available in the chat history or context, do not answer the question and politely let the user know that you can only answer if the answer is available in context or the chat history. + +------------- +Chat history: +{chat_history} +------------- +Context: +{context} +------------- + +Question: {question} +Helpful answer:`); diff --git a/src/redis-custom-history.ts b/src/redis-custom-history.ts new file mode 100644 index 0000000..956a880 --- /dev/null +++ b/src/redis-custom-history.ts @@ -0,0 +1,122 @@ +/* eslint-disable @typescript-eslint/no-magic-numbers */ +/* eslint-disable @typescript-eslint/no-unnecessary-condition */ +import { BaseListChatMessageHistory } from "@langchain/core/chat_history"; +import type { BaseMessage, StoredMessage } from "@langchain/core/messages"; +import { + mapChatMessagesToStoredMessages, + mapStoredMessagesToChatMessages, +} from "@langchain/core/messages"; +import { Redis, type RedisConfigNodejs } from "@upstash/redis"; + +//REFER HERE: https://github.com/langchain-ai/langchainjs/blob/main/libs/langchain-community/src/stores/message/upstash_redis.ts +/** + * Type definition for the input parameters required to initialize an + * instance of the UpstashRedisChatMessageHistory class. + */ +export type CustomUpstashRedisChatMessageHistoryInput = { + sessionId: string; + sessionTTL?: number; + config?: RedisConfigNodejs; + client?: Redis; + topLevelChatHistoryLength?: number; +}; + +/** + * Class used to store chat message history in Redis. It provides methods + * to add, get, and clear messages. + */ +export class CustomUpstashRedisChatMessageHistory extends BaseListChatMessageHistory { + lc_namespace = ["langchain", "stores", "message", "upstash_redis"]; + + get lc_secrets() { + return { + "config.url": "UPSTASH_REDIS_REST_URL", + "config.token": "UPSTASH_REDIS_REST_TOKEN", + }; + } + + public client: Redis; + + private sessionId: string; + + private sessionTTL?: number; + private topLevelChatHistoryLength?: number; + + constructor(fields: CustomUpstashRedisChatMessageHistoryInput) { + super(fields); + const { sessionId, sessionTTL, config, client, topLevelChatHistoryLength } = fields; + if (client) { + this.client = client; + } else if (config) { + this.client = new Redis(config); + } else { + throw new Error( + `Upstash Redis message stores require either a config object or a pre-configured client.` + ); + } + this.sessionId = sessionId; + this.sessionTTL = sessionTTL; + this.topLevelChatHistoryLength = topLevelChatHistoryLength; + } + + /** + * Retrieves the chat messages from the Redis database. + * @returns An array of BaseMessage instances representing the chat history. + */ + async getMessages(chatHistoryLength?: number): Promise { + const length = chatHistoryLength ?? this.topLevelChatHistoryLength ?? [0, -1]; + + const rawStoredMessages: StoredMessage[] = await this.client.lrange( + this.sessionId, + typeof length === "number" ? 0 : length[0], + typeof length === "number" ? length : length[1] + ); + + const orderedMessages = rawStoredMessages.reverse(); + const previousMessages = orderedMessages.filter( + (x): x is StoredMessage => x.type !== undefined && x.data.content !== undefined + ); + return mapStoredMessagesToChatMessages(previousMessages); + } + + /** + * Adds a new message to the chat history in the Redis database. + * @param message The message to be added to the chat history. + * @returns Promise resolving to void. + */ + async addMessage(message: BaseMessage): Promise { + const messageToAdd = mapChatMessagesToStoredMessages([message]); + await this.client.lpush(this.sessionId, JSON.stringify(messageToAdd[0])); + if (this.sessionTTL) { + await this.client.expire(this.sessionId, this.sessionTTL); + } + } + + /** + * Deletes all messages from the chat history in the Redis database. + * @returns Promise resolving to void. + */ + async clear(): Promise { + await this.client.del(this.sessionId); + } +} + +const DAY_IN_SECONDS = 86_400; +const TOP_6 = 5; + +export const redisChatMessagesHistory = ({ + length = TOP_6, + sessionId, + redis, +}: { + sessionId: string; + length?: number; + redis: Redis; +}) => { + return new CustomUpstashRedisChatMessageHistory({ + sessionId, + sessionTTL: DAY_IN_SECONDS, + topLevelChatHistoryLength: length, + client: redis, + }); +}; diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..c0e75f8 --- /dev/null +++ b/src/types.ts @@ -0,0 +1 @@ +export type PreferredRegions = "eu-west-1" | "us-east-1"; diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000..3c5ac68 --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,19 @@ +import type { BaseMessage } from "@langchain/core/messages"; + +export const sanitizeQuestion = (question: string) => { + return question.trim().replaceAll("\n", " "); +}; + +export const formatFacts = (facts: string[]): string => { + return facts.join("\n"); +}; + +export const formatChatHistory = (chatHistory: BaseMessage[]) => { + const formattedDialogueTurns = chatHistory.map((dialogueTurn) => + dialogueTurn._getType() === "human" + ? `Human: ${dialogueTurn.content}` + : `Assistant: ${dialogueTurn.content}` + ); + + return formatFacts(formattedDialogueTurns); +};