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:
kappa
2026-01-14 13:00:44 +09:00
commit 1e71e035e7
15 changed files with 2272 additions and 0 deletions

272
src/openai-service.ts Normal file
View File

@@ -0,0 +1,272 @@
import { Env } from './types';
interface OpenAIMessage {
role: 'system' | 'user' | 'assistant' | 'tool';
content: string | null;
tool_calls?: ToolCall[];
tool_call_id?: string;
}
interface ToolCall {
id: string;
type: 'function';
function: {
name: string;
arguments: string;
};
}
interface OpenAIResponse {
choices: {
message: OpenAIMessage;
finish_reason: string;
}[];
}
// 사용 가능한 도구 정의
const tools = [
{
type: 'function',
function: {
name: 'get_weather',
description: '특정 도시의 현재 날씨 정보를 가져옵니다',
parameters: {
type: 'object',
properties: {
city: {
type: 'string',
description: '도시 이름 (예: Seoul, Tokyo, New York)',
},
},
required: ['city'],
},
},
},
{
type: 'function',
function: {
name: 'search_web',
description: '웹에서 정보를 검색합니다',
parameters: {
type: 'object',
properties: {
query: {
type: 'string',
description: '검색 쿼리',
},
},
required: ['query'],
},
},
},
{
type: 'function',
function: {
name: 'get_current_time',
description: '현재 시간을 가져옵니다',
parameters: {
type: 'object',
properties: {
timezone: {
type: 'string',
description: '타임존 (예: Asia/Seoul, UTC)',
},
},
required: [],
},
},
},
{
type: 'function',
function: {
name: 'calculate',
description: '수학 계산을 수행합니다',
parameters: {
type: 'object',
properties: {
expression: {
type: 'string',
description: '계산할 수식 (예: 2+2, 100*5)',
},
},
required: ['expression'],
},
},
},
];
// 도구 실행
async function executeTool(name: string, args: Record<string, string>): Promise<string> {
switch (name) {
case 'get_weather': {
const city = args.city || 'Seoul';
try {
const response = await fetch(
`https://wttr.in/${encodeURIComponent(city)}?format=j1`
);
const data = await response.json() as any;
const current = data.current_condition[0];
return `🌤 ${city} 날씨
온도: ${current.temp_C}°C (체감 ${current.FeelsLikeC}°C)
상태: ${current.weatherDesc[0].value}
습도: ${current.humidity}%
풍속: ${current.windspeedKmph} km/h`;
} catch (error) {
return `날씨 정보를 가져올 수 없습니다: ${city}`;
}
}
case 'search_web': {
// 간단한 DuckDuckGo Instant Answer API
const query = args.query;
try {
const response = await fetch(
`https://api.duckduckgo.com/?q=${encodeURIComponent(query)}&format=json&no_html=1`
);
const data = await response.json() as any;
if (data.Abstract) {
return `🔍 검색 결과: ${query}\n\n${data.Abstract}\n\n출처: ${data.AbstractSource}`;
} else if (data.RelatedTopics?.length > 0) {
const topics = data.RelatedTopics.slice(0, 3)
.filter((t: any) => t.Text)
.map((t: any) => `${t.Text}`)
.join('\n');
return `🔍 관련 정보: ${query}\n\n${topics}`;
}
return `"${query}"에 대한 즉시 답변을 찾을 수 없습니다. 더 구체적인 질문을 해주세요.`;
} catch (error) {
return `검색 중 오류가 발생했습니다.`;
}
}
case 'get_current_time': {
const timezone = args.timezone || 'Asia/Seoul';
try {
const now = new Date();
const formatted = now.toLocaleString('ko-KR', { timeZone: timezone });
return `🕐 현재 시간 (${timezone}): ${formatted}`;
} catch (error) {
return `시간 정보를 가져올 수 없습니다.`;
}
}
case 'calculate': {
const expression = args.expression;
try {
// 안전한 수식 계산 (기본 연산만)
const sanitized = expression.replace(/[^0-9+\-*/().% ]/g, '');
const result = Function('"use strict"; return (' + sanitized + ')')();
return `🔢 계산 결과: ${expression} = ${result}`;
} catch (error) {
return `계산할 수 없는 수식입니다: ${expression}`;
}
}
default:
return `알 수 없는 도구: ${name}`;
}
}
// OpenAI API 호출
async function callOpenAI(
apiKey: string,
messages: OpenAIMessage[],
useTools: boolean = true
): Promise<OpenAIResponse> {
const response = await fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${apiKey}`,
},
body: JSON.stringify({
model: 'gpt-4o-mini',
messages,
tools: useTools ? tools : undefined,
tool_choice: useTools ? 'auto' : undefined,
max_tokens: 1000,
}),
});
if (!response.ok) {
const error = await response.text();
throw new Error(`OpenAI API error: ${response.status} - ${error}`);
}
return response.json();
}
// 메인 응답 생성 함수
export async function generateOpenAIResponse(
env: Env,
userMessage: string,
systemPrompt: string,
recentContext: { role: 'user' | 'assistant'; content: string }[]
): Promise<string> {
if (!env.OPENAI_API_KEY) {
throw new Error('OPENAI_API_KEY not configured');
}
const messages: OpenAIMessage[] = [
{ role: 'system', content: systemPrompt },
...recentContext.map((m) => ({
role: m.role as 'user' | 'assistant',
content: m.content,
})),
{ role: 'user', content: userMessage },
];
// 첫 번째 호출
let response = await callOpenAI(env.OPENAI_API_KEY, messages);
let assistantMessage = response.choices[0].message;
// Function Calling 처리 (최대 3회 반복)
let iterations = 0;
while (assistantMessage.tool_calls && iterations < 3) {
iterations++;
// 도구 호출 결과 수집
const toolResults: OpenAIMessage[] = [];
for (const toolCall of assistantMessage.tool_calls) {
const args = JSON.parse(toolCall.function.arguments);
const result = await executeTool(toolCall.function.name, args);
toolResults.push({
role: 'tool',
tool_call_id: toolCall.id,
content: result,
});
}
// 대화에 추가
messages.push({
role: 'assistant',
content: assistantMessage.content,
tool_calls: assistantMessage.tool_calls,
});
messages.push(...toolResults);
// 다시 호출
response = await callOpenAI(env.OPENAI_API_KEY, messages, false);
assistantMessage = response.choices[0].message;
}
return assistantMessage.content || '응답을 생성할 수 없습니다.';
}
// 프로필 생성용 (도구 없이)
export async function generateProfileWithOpenAI(
env: Env,
prompt: string
): Promise<string> {
if (!env.OPENAI_API_KEY) {
throw new Error('OPENAI_API_KEY not configured');
}
const response = await callOpenAI(
env.OPENAI_API_KEY,
[{ role: 'user', content: prompt }],
false
);
return response.choices[0].message.content || '프로필 생성 실패';
}