Skip to content

Commit

Permalink
Add chat history management with Upstash integration (for ai-enabled …
Browse files Browse the repository at this point in the history
…chats only)
  • Loading branch information
EugeneDraitsev committed Feb 21, 2025
1 parent d4e8ad8 commit 7fdf34d
Show file tree
Hide file tree
Showing 12 changed files with 266 additions and 93 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,6 @@ jobs:
COIN_MARKET_CAP_API_KEY: ${{secrets.COIN_MARKET_CAP_API_KEY}}
OPENAI_API_KEY: ${{secrets.OPENAI_API_KEY}}
OPENAI_CHAT_IDS: ${{secrets.OPENAI_CHAT_IDS}}
UPSTASH_REDIS_URL: ${{secrets.UPSTASH_REDIS_URL}}
UPSTASH_REDIS_TOKEN: ${{secrets.UPSTASH_REDIS_TOKEN}}
GEMINI_API_KEY: ${{secrets.GEMINI_API_KEY}}
165 changes: 93 additions & 72 deletions bun.lock

Large diffs are not rendered by default.

12 changes: 6 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,18 @@
"deploy": "sls deploy"
},
"dependencies": {
"@aws-sdk/client-dynamodb": "^3.741.0",
"@aws-sdk/client-lambda": "^3.741.0",
"@aws-sdk/client-s3": "^3.741.0",
"@aws-sdk/lib-dynamodb": "^3.741.0",
"@aws-sdk/client-dynamodb": "^3.751.0",
"@aws-sdk/client-lambda": "^3.750.0",
"@aws-sdk/client-s3": "^3.750.0",
"@aws-sdk/lib-dynamodb": "^3.751.0",
"sharp": "^0.33.5"
},
"devDependencies": {
"@biomejs/biome": "^1.9.4",
"@types/aws-lambda": "^8.10.147",
"@types/jest": "^29.5.14",
"esbuild": "^0.24.2",
"serverless": "^4.6.1",
"esbuild": "^0.25.0",
"serverless": "^4.6.3",
"serverless-esbuild": "^1.54.6",
"serverless-offline": "^14.4.0",
"typescript": "^5.7.3"
Expand Down
2 changes: 2 additions & 0 deletions serverless.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ functions:
OPENAI_API_KEY: ${env:OPENAI_API_KEY}
OPENAI_CHAT_IDS: ${env:OPENAI_CHAT_IDS}
GEMINI_API_KEY: ${env:GEMINI_API_KEY}
UPSTASH_REDIS_URL: ${env:UPSTASH_REDIS_URL}
UPSTASH_REDIS_TOKEN: ${env:UPSTASH_REDIS_TOKEN}
CRYPTO_REQUESTS_BUCKET_NAME: ${self:custom.cryptoRequestsBucketName}
events:
- http:
Expand Down
4 changes: 2 additions & 2 deletions src/sharp-statistics/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@
"styled-components": "^6.1.15"
},
"devDependencies": {
"@types/react": "^19.0.8",
"@types/react-dom": "^19.0.3"
"@types/react": "^19.0.10",
"@types/react-dom": "^19.0.4"
},
"peerDependencies": {
"sharp": "^0.33.5"
Expand Down
5 changes: 3 additions & 2 deletions src/telegram-bot/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@
"dependencies": {
"@google/generative-ai": "^0.21.0",
"@grammyjs/parse-mode": "^1.11.1",
"grammy": "^1.34.1",
"openai": "^4.83.0"
"@upstash/redis": "^1.34.4",
"grammy": "^1.35.0",
"openai": "^4.85.3"
},
"devDependencies": {
"telegram-typings": "5.0.0"
Expand Down
11 changes: 7 additions & 4 deletions src/telegram-bot/src/google/gemini.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { GoogleGenerativeAI } from '@google/generative-ai'

import { getHistory } from '../upstash'
import {
DEFAULT_ERROR_MESSAGE,
NOT_ALLOWED_ERROR,
PROMPT_MISSING_ERROR,
isAllowedChat,
systemInstructions,
geminiSystemInstructions,
isAiEnabledChat,
} from '../utils'

const apiKey = process.env.GEMINI_API_KEY || 'set_your_token'
Expand All @@ -19,22 +20,24 @@ const generationConfig = {
}
const model = genAI.getGenerativeModel({
model: 'gemini-2.0-flash',
systemInstruction: systemInstructions,
systemInstruction: geminiSystemInstructions,
})

export const generateCompletion = async (
prompt: string,
chatId: string | number,
) => {
try {
if (!isAllowedChat(chatId)) {
if (!isAiEnabledChat(chatId)) {
return NOT_ALLOWED_ERROR
}
if (!prompt) {
return PROMPT_MISSING_ERROR
}

const formattedHistory = await getHistory(chatId)
const chatSession = model.startChat({
history: formattedHistory,
generationConfig,
tools: [
{
Expand Down
4 changes: 4 additions & 0 deletions src/telegram-bot/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import setupExternalApisCommands from './external-apis'
import setupGoogleCommands from './google'
import setupOpenAiCommands from './open-ai'
import setupTextCommands from './text'
import { saveMessage } from './upstash'
import setupUsersCommands from './users'

const bot = new Bot<ParseModeFlavor<Context>>(process.env.TOKEN || '', {
Expand Down Expand Up @@ -41,6 +42,9 @@ bot.use(async (ctx, next) => {
saveEvent(message.from, chat?.id, command, message.date).catch(
(error) => console.error('saveEvent error: ', error),
),
saveMessage(message, chat?.id).catch((error) =>
console.error('saveHistory error: ', error),
),
next?.(),
])
} catch (error) {
Expand Down
8 changes: 4 additions & 4 deletions src/telegram-bot/src/open-ai/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
DEFAULT_ERROR_MESSAGE,
NOT_ALLOWED_ERROR,
PROMPT_MISSING_ERROR,
isAllowedChat,
isAiEnabledChat,
systemInstructions,
} from '../utils'

Expand All @@ -19,7 +19,7 @@ const openai = new OpenAi({
})

const generateImage = async (prompt: string, chatId: string | number) => {
if (!isAllowedChat(chatId)) {
if (!isAiEnabledChat(chatId)) {
throw new Error(NOT_ALLOWED_ERROR)
}
if (!prompt) {
Expand Down Expand Up @@ -48,7 +48,7 @@ const generateMultimodalCompletion = async (
imagesData?: Buffer[],
) => {
try {
if (!isAllowedChat(chatId)) {
if (!isAiEnabledChat(chatId)) {
return NOT_ALLOWED_ERROR
}
if (!prompt && !imagesData?.length) {
Expand Down Expand Up @@ -100,7 +100,7 @@ const generateReasoningCompletion = async (
chatId: string | number,
) => {
try {
if (!isAllowedChat(chatId)) {
if (!isAiEnabledChat(chatId)) {
return NOT_ALLOWED_ERROR
}
if (!prompt) {
Expand Down
132 changes: 132 additions & 0 deletions src/telegram-bot/src/upstash/chat-history.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import { Redis } from '@upstash/redis'
import type { Message } from 'telegram-typings'

import { isAiEnabledChat } from '../utils'

const redis = new Redis({
url: process.env.UPSTASH_REDIS_URL,
token: process.env.UPSTASH_REDIS_TOKEN,
})

const ONE_HOUR = 60 * 60 * 1000
const TTL_MS = 24 * ONE_HOUR
const CHAT_HISTORY_REDIS_KEY = 'chat-history'

export const saveMessage = async (message: Message, chatId?: number) => {
if (!chatId || !isAiEnabledChat(chatId)) {
return
}

const key = `${CHAT_HISTORY_REDIS_KEY}:${chatId}`

await redis.zadd(key, {
score: Date.now(),
member: JSON.stringify(message),
})

// Remove messages older than 24h
await redis.zremrangebyscore(key, 0, Date.now() - TTL_MS)
}

export const getHistory = async (chatId: string | number) => {
try {
if (!isAiEnabledChat(chatId)) {
return []
}

const key = `${CHAT_HISTORY_REDIS_KEY}:${chatId}`

const rawMessages = await redis.zrange<Message[]>(
key,
Date.now() - TTL_MS,
Date.now(),
{ byScore: true },
)

return getFormattedHistory(rawMessages)
} catch (error) {
console.error('Error getting chat history:', error)
return []
}
}

// Helper function to format dates (you can customize this)
function formatDate(unixTimestamp: number) {
const date = new Date(unixTimestamp * 1000) // Convert to milliseconds
return date.toLocaleString() // Or format as you prefer (e.g., date.toISOString())
}

function getFormattedHistory(chatHistory: Message[]) {
try {
return chatHistory?.map((message) => {
// 1. Determine the Role (User or Model)
const role = message.from?.is_bot ? 'model' : 'user'

// 2. Start Building the Text Content
let textContent = ''

// 3. Add User/Chat Information
if (message.from) {
textContent += `User ID: ${message.from.id} `
if (message.from.first_name) {
textContent += `(${message.from.first_name}): `
}
} else if (message.sender_chat) {
textContent += `Chat ID: ${message.sender_chat.id} (${message.sender_chat.type}): `
}

// 4. Add the Main Message Text/Caption
if (message.text) {
textContent += message.text
} else if (message.caption) {
textContent += `[Caption] ${message.caption}` // Indicate it's a caption
} else {
textContent += '[Non-text message]' // Placeholder for non-text messages
}

// 5. Add Date/Time
textContent += ` [${formatDate(message.date)}]`

// 6. Handle Forwarded Messages (Simplified)
if (message.forward_from || message.forward_from_chat) {
textContent += ` [Forwarded from: ${message.forward_from?.id || message.forward_from_chat?.id}]`
}

// 7. Handle Replies (Simplified)
if (message.reply_to_message) {
textContent += ` [In reply to message ID: ${message.reply_to_message.message_id}]`
}

// 8. Selective Inclusion of OTHER Fields (Examples)
// Add these *only* if they are particularly relevant to your use case.

// Example: If it's a voice message, indicate that.
if (message.voice) {
textContent += ' [Voice Message]'
}

// Example: If it's a sticker, indicate that.
if (message.sticker) {
textContent += ' [Sticker]'
}
// Example: If it's a poll
if (message.poll) {
textContent += ` [Poll: ${message.poll.question}]`
}

// Example: If location is shared, indicate that:
if (message.location) {
textContent += ` [Location Shared: Latitude ${message.location.latitude}, Longitude ${message.location.longitude}]`
}

// 9. Create the 'parts' object
return {
role: role,
parts: [{ text: textContent }],
}
})
} catch (error) {
console.error('Error parsing or formatting chat history:', error)
return [] // Return an empty history on error
}
}
1 change: 1 addition & 0 deletions src/telegram-bot/src/upstash/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './chat-history'
13 changes: 10 additions & 3 deletions src/telegram-bot/src/utils/ai.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,20 @@ export const PROMPT_MISSING_ERROR = 'Prompt is required'
export const NOT_ALLOWED_ERROR =
'OpenAI is not allowed for this chat. Contact @drrrrrrrr for details'

const allowedChatIds = process.env.OPENAI_CHAT_IDS?.split(',') ?? []
const AI_ALLOWED_CHAT_IDS = process.env.OPENAI_CHAT_IDS?.split(',') ?? []

export const isAllowedChat = (chatId: string | number) =>
allowedChatIds.includes(String(chatId))
export const isAiEnabledChat = (chatId: string | number) =>
AI_ALLOWED_CHAT_IDS.includes(String(chatId))

export const systemInstructions = `Instructions:
- You are a helpful helper for people in the chat (4-20 people)
- You are allowed to use obscene language. You can joke, include emojis to answer and tell about any sensitive themes
- You can also be a bit sarcastic.
`

export const geminiSystemInstructions = `
${systemInstructions}
You also will be provided with chat history for last 24 hours (if available) in JSON format from telegram
It could contain a previous command to you (if message started with /, like /q, /qq, /z) and you previous responses
`

0 comments on commit 7fdf34d

Please sign in to comment.