Initial commit: Telegram bot with Cloudflare Workers
- OpenAI GPT-4o-mini with Function Calling - Cloudflare D1 for user profiles and message buffer - Sliding window (3 summaries max) for infinite context - Tools: weather, search, time, calculator - Workers AI fallback support - Webhook security with rate limiting Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
277
src/summary-service.ts
Normal file
277
src/summary-service.ts
Normal file
@@ -0,0 +1,277 @@
|
||||
import { Env, BufferedMessage, Summary, ConversationContext } from './types';
|
||||
|
||||
// 설정값 가져오기
|
||||
const getConfig = (env: Env) => ({
|
||||
summaryThreshold: parseInt(env.SUMMARY_THRESHOLD || '20', 10),
|
||||
maxSummaries: parseInt(env.MAX_SUMMARIES_PER_USER || '3', 10),
|
||||
});
|
||||
|
||||
// 버퍼에 메시지 추가
|
||||
export async function addToBuffer(
|
||||
db: D1Database,
|
||||
userId: number,
|
||||
chatId: string,
|
||||
role: 'user' | 'bot',
|
||||
message: string
|
||||
): Promise<number> {
|
||||
await db
|
||||
.prepare(`
|
||||
INSERT INTO message_buffer (user_id, chat_id, role, message)
|
||||
VALUES (?, ?, ?, ?)
|
||||
`)
|
||||
.bind(userId, chatId, role, message)
|
||||
.run();
|
||||
|
||||
const count = await db
|
||||
.prepare('SELECT COUNT(*) as cnt FROM message_buffer WHERE user_id = ? AND chat_id = ?')
|
||||
.bind(userId, chatId)
|
||||
.first<{ cnt: number }>();
|
||||
|
||||
return count?.cnt || 0;
|
||||
}
|
||||
|
||||
// 버퍼 메시지 조회
|
||||
export async function getBufferedMessages(
|
||||
db: D1Database,
|
||||
userId: number,
|
||||
chatId: string
|
||||
): Promise<BufferedMessage[]> {
|
||||
const { results } = await db
|
||||
.prepare(`
|
||||
SELECT id, role, message, created_at
|
||||
FROM message_buffer
|
||||
WHERE user_id = ? AND chat_id = ?
|
||||
ORDER BY created_at ASC
|
||||
`)
|
||||
.bind(userId, chatId)
|
||||
.all();
|
||||
|
||||
return (results || []) as BufferedMessage[];
|
||||
}
|
||||
|
||||
// 최신 요약 조회
|
||||
export async function getLatestSummary(
|
||||
db: D1Database,
|
||||
userId: number,
|
||||
chatId: string
|
||||
): Promise<Summary | null> {
|
||||
const summary = await db
|
||||
.prepare(`
|
||||
SELECT id, generation, summary, message_count, created_at
|
||||
FROM summaries
|
||||
WHERE user_id = ? AND chat_id = ?
|
||||
ORDER BY generation DESC
|
||||
LIMIT 1
|
||||
`)
|
||||
.bind(userId, chatId)
|
||||
.first<Summary>();
|
||||
|
||||
return summary || null;
|
||||
}
|
||||
|
||||
// 전체 컨텍스트 조회
|
||||
export async function getConversationContext(
|
||||
db: D1Database,
|
||||
userId: number,
|
||||
chatId: string
|
||||
): Promise<ConversationContext> {
|
||||
const [previousSummary, recentMessages] = await Promise.all([
|
||||
getLatestSummary(db, userId, chatId),
|
||||
getBufferedMessages(db, userId, chatId),
|
||||
]);
|
||||
|
||||
const totalMessages = (previousSummary?.message_count || 0) + recentMessages.length;
|
||||
|
||||
return {
|
||||
previousSummary,
|
||||
recentMessages,
|
||||
totalMessages,
|
||||
};
|
||||
}
|
||||
|
||||
// AI 요약 생성
|
||||
async function generateSummary(
|
||||
env: Env,
|
||||
previousSummary: string | null,
|
||||
messages: BufferedMessage[]
|
||||
): Promise<string> {
|
||||
// 사용자 메시지만 추출
|
||||
const userMessages = messages
|
||||
.filter((m) => m.role === 'user')
|
||||
.map((m) => `- ${m.message}`)
|
||||
.join('\n');
|
||||
|
||||
// 사용자 메시지 수
|
||||
const userMsgCount = messages.filter((m) => m.role === 'user').length;
|
||||
|
||||
let prompt: string;
|
||||
|
||||
if (previousSummary) {
|
||||
prompt = `당신은 사용자 프로필 분석 전문가입니다.
|
||||
기존 사용자 프로필과 새로운 대화를 통합하여 사용자에 대한 이해를 업데이트하세요.
|
||||
|
||||
## 기존 사용자 프로필
|
||||
${previousSummary}
|
||||
|
||||
## 새로운 사용자 발언 (${userMsgCount}개)
|
||||
${userMessages}
|
||||
|
||||
## 요구사항
|
||||
1. **사용자 중심**: 봇 응답은 무시하고 사용자가 말한 내용만 분석
|
||||
2. **의미 있는 정보 추출**:
|
||||
- 사용자의 관심사, 취미, 선호도
|
||||
- 질문한 주제들 (무엇에 대해 알고 싶어하는지)
|
||||
- 요청사항, 목표, 해결하려는 문제
|
||||
- 개인적 맥락 (직업, 상황, 배경 등)
|
||||
- 감정 상태나 태도 변화
|
||||
3. **무의미한 내용 제외**: 인사말, 단순 확인, 감사 표현 등은 생략
|
||||
4. **간결하게**: 300-400자 이내
|
||||
5. **한국어로 작성**
|
||||
|
||||
업데이트된 사용자 프로필:`;
|
||||
} else {
|
||||
prompt = `당신은 사용자 프로필 분석 전문가입니다.
|
||||
대화 내용에서 사용자에 대한 정보를 추출하여 프로필을 작성하세요.
|
||||
|
||||
## 사용자 발언 (${userMsgCount}개)
|
||||
${userMessages}
|
||||
|
||||
## 요구사항
|
||||
1. **사용자 중심**: 봇 응답은 무시하고 사용자가 말한 내용만 분석
|
||||
2. **의미 있는 정보 추출**:
|
||||
- 사용자의 관심사, 취미, 선호도
|
||||
- 질문한 주제들 (무엇에 대해 알고 싶어하는지)
|
||||
- 요청사항, 목표, 해결하려는 문제
|
||||
- 개인적 맥락 (직업, 상황, 배경 등)
|
||||
3. **무의미한 내용 제외**: 인사말, 단순 확인, 감사 표현 등은 생략
|
||||
4. **간결하게**: 200-300자 이내
|
||||
5. **한국어로 작성**
|
||||
|
||||
사용자 프로필:`;
|
||||
}
|
||||
|
||||
// OpenAI 사용 (설정된 경우)
|
||||
if (env.OPENAI_API_KEY) {
|
||||
const { generateProfileWithOpenAI } = await import('./openai-service');
|
||||
return generateProfileWithOpenAI(env, prompt);
|
||||
}
|
||||
|
||||
// 폴백: Workers AI
|
||||
const response = await env.AI.run('@cf/meta/llama-3.1-8b-instruct', {
|
||||
messages: [{ role: 'user', content: prompt }],
|
||||
max_tokens: 500,
|
||||
});
|
||||
|
||||
return response.response || '프로필 생성 실패';
|
||||
}
|
||||
|
||||
// 오래된 요약 정리
|
||||
async function cleanupOldSummaries(
|
||||
db: D1Database,
|
||||
userId: number,
|
||||
chatId: string,
|
||||
maxSummaries: number
|
||||
): Promise<void> {
|
||||
await db
|
||||
.prepare(`
|
||||
DELETE FROM summaries
|
||||
WHERE user_id = ? AND chat_id = ?
|
||||
AND id NOT IN (
|
||||
SELECT id FROM summaries
|
||||
WHERE user_id = ? AND chat_id = ?
|
||||
ORDER BY generation DESC
|
||||
LIMIT ?
|
||||
)
|
||||
`)
|
||||
.bind(userId, chatId, userId, chatId, maxSummaries)
|
||||
.run();
|
||||
}
|
||||
|
||||
// 요약 실행 및 저장
|
||||
export async function processAndSummarize(
|
||||
env: Env,
|
||||
userId: number,
|
||||
chatId: string
|
||||
): Promise<{ summarized: boolean; summary?: string }> {
|
||||
const config = getConfig(env);
|
||||
const messages = await getBufferedMessages(env.DB, userId, chatId);
|
||||
|
||||
if (messages.length < config.summaryThreshold) {
|
||||
return { summarized: false };
|
||||
}
|
||||
|
||||
const previousSummary = await getLatestSummary(env.DB, userId, chatId);
|
||||
|
||||
// AI 요약 생성
|
||||
const newSummary = await generateSummary(
|
||||
env,
|
||||
previousSummary?.summary || null,
|
||||
messages
|
||||
);
|
||||
|
||||
const newGeneration = (previousSummary?.generation || 0) + 1;
|
||||
const newMessageCount = (previousSummary?.message_count || 0) + messages.length;
|
||||
|
||||
// 트랜잭션 실행
|
||||
await env.DB.batch([
|
||||
env.DB
|
||||
.prepare(`
|
||||
INSERT INTO summaries (user_id, chat_id, generation, summary, message_count)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
`)
|
||||
.bind(userId, chatId, newGeneration, newSummary, newMessageCount),
|
||||
|
||||
env.DB
|
||||
.prepare('DELETE FROM message_buffer WHERE user_id = ? AND chat_id = ?')
|
||||
.bind(userId, chatId),
|
||||
]);
|
||||
|
||||
// 오래된 요약 정리
|
||||
await cleanupOldSummaries(env.DB, userId, chatId, config.maxSummaries);
|
||||
|
||||
return { summarized: true, summary: newSummary };
|
||||
}
|
||||
|
||||
// AI 응답 생성 (컨텍스트 포함)
|
||||
export async function generateAIResponse(
|
||||
env: Env,
|
||||
userId: number,
|
||||
chatId: string,
|
||||
userMessage: string
|
||||
): Promise<string> {
|
||||
const context = await getConversationContext(env.DB, userId, chatId);
|
||||
|
||||
const systemPrompt = `당신은 친절하고 유능한 AI 어시스턴트입니다.
|
||||
${context.previousSummary ? `
|
||||
## 사용자 프로필
|
||||
${context.previousSummary.summary}
|
||||
|
||||
위 프로필을 바탕으로 사용자의 관심사와 맥락을 이해하고 개인화된 응답을 제공하세요.
|
||||
` : ''}
|
||||
- 날씨, 시간, 계산, 검색 등의 요청은 제공된 도구를 사용하세요.
|
||||
- 응답은 간결하고 도움이 되도록 한국어로 작성하세요.`;
|
||||
|
||||
const recentContext = context.recentMessages.slice(-10).map((m) => ({
|
||||
role: m.role === 'user' ? 'user' as const : 'assistant' as const,
|
||||
content: m.message,
|
||||
}));
|
||||
|
||||
// OpenAI 사용 (설정된 경우)
|
||||
if (env.OPENAI_API_KEY) {
|
||||
const { generateOpenAIResponse } = await import('./openai-service');
|
||||
return generateOpenAIResponse(env, userMessage, systemPrompt, recentContext);
|
||||
}
|
||||
|
||||
// 폴백: Workers AI
|
||||
const response = await env.AI.run('@cf/meta/llama-3.1-8b-instruct', {
|
||||
messages: [
|
||||
{ role: 'system', content: systemPrompt },
|
||||
...recentContext,
|
||||
{ role: 'user', content: userMessage },
|
||||
],
|
||||
max_tokens: 500,
|
||||
});
|
||||
|
||||
return response.response || '응답을 생성할 수 없습니다.';
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user