- lookup_docs 도구로 React, OpenAI 등 공식 문서 실시간 조회 - README에 Context7 연동 기능 문서화 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
326 lines
9.2 KiB
TypeScript
326 lines
9.2 KiB
TypeScript
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'],
|
|
},
|
|
},
|
|
},
|
|
{
|
|
type: 'function',
|
|
function: {
|
|
name: 'lookup_docs',
|
|
description: '프로그래밍 라이브러리의 공식 문서를 조회합니다. React, OpenAI, Cloudflare Workers 등의 최신 문서와 코드 예제를 검색할 수 있습니다.',
|
|
parameters: {
|
|
type: 'object',
|
|
properties: {
|
|
library: {
|
|
type: 'string',
|
|
description: '라이브러리 이름 (예: react, openai, cloudflare-workers, next.js)',
|
|
},
|
|
query: {
|
|
type: 'string',
|
|
description: '찾고 싶은 내용 (예: hooks 사용법, API 호출 방법)',
|
|
},
|
|
},
|
|
required: ['library', 'query'],
|
|
},
|
|
},
|
|
},
|
|
];
|
|
|
|
// 도구 실행
|
|
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}`;
|
|
}
|
|
}
|
|
|
|
case 'lookup_docs': {
|
|
const library = args.library;
|
|
const query = args.query;
|
|
try {
|
|
// Context7 REST API 직접 호출
|
|
// 1. 라이브러리 검색
|
|
const searchUrl = `https://context7.com/api/v2/libs/search?libraryName=${encodeURIComponent(library)}&query=${encodeURIComponent(query)}`;
|
|
const searchResponse = await fetch(searchUrl);
|
|
const searchData = await searchResponse.json() as any;
|
|
|
|
if (!searchData.libraries?.length) {
|
|
return `📚 "${library}" 라이브러리를 찾을 수 없습니다.`;
|
|
}
|
|
|
|
const libraryId = searchData.libraries[0].id;
|
|
|
|
// 2. 문서 조회
|
|
const docsUrl = `https://context7.com/api/v2/context?libraryId=${encodeURIComponent(libraryId)}&query=${encodeURIComponent(query)}`;
|
|
const docsResponse = await fetch(docsUrl);
|
|
const docsData = await docsResponse.json() as any;
|
|
|
|
if (docsData.error) {
|
|
return `📚 문서 조회 실패: ${docsData.message || docsData.error}`;
|
|
}
|
|
|
|
const content = docsData.context || docsData.content || JSON.stringify(docsData, null, 2);
|
|
return `📚 ${library} 문서 (${query}):\n\n${content.slice(0, 1500)}`;
|
|
} catch (error) {
|
|
return `📚 문서 조회 중 오류: ${String(error)}`;
|
|
}
|
|
}
|
|
|
|
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 || '프로필 생성 실패';
|
|
}
|