diff --git a/src/agents/domain-agent.ts b/src/agents/domain-agent.ts index 0e4b159..fc549e5 100644 --- a/src/agents/domain-agent.ts +++ b/src/agents/domain-agent.ts @@ -188,6 +188,281 @@ export function addMessageToSession( } } +// Domain Expert System Prompt +const DOMAIN_EXPERT_PROMPT = `당신은 10년 경력의 도메인 컨설턴트입니다. + +전문 분야: +- 브랜딩에 적합한 도메인 선택 조언 +- SEO 관점의 도메인 추천 +- 가격 대비 가치 분석 +- TLD 선택 가이드 (.com, .net, .io, .kr 등) + +행동 지침: +1. 불필요한 프리미엄 도메인 추천 자제 +2. 실용적이고 합리적인 선택 유도 +3. 사용자의 예산과 용도를 먼저 파악 +4. 명확하지 않은 요청은 질문으로 확인 + +응답 형식: +- 짧고 명확하게 답변 +- 불필요한 인사말이나 서론 없이 바로 본론 +- 가격 정보는 항상 원화(₩)로 표시 + +특수 지시: +- 도메인과 무관한 메시지가 들어오면 반드시 "__PASSTHROUGH__"만 응답 +- 세션 종료가 필요하면 "__SESSION_END__"를 응답 끝에 추가`; + +// Domain Tools for Function Calling +const DOMAIN_TOOLS = [ + { + type: 'function' as const, + function: { + name: 'check_domain', + description: '도메인 가용성 및 가격 확인', + parameters: { + type: 'object', + properties: { + domain: { type: 'string', description: '확인할 도메인 (예: example.com)' } + }, + required: ['domain'] + } + } + }, + { + type: 'function' as const, + function: { + name: 'search_suggestions', + description: '키워드 기반 도메인 추천 검색', + parameters: { + type: 'object', + properties: { + keywords: { type: 'string', description: '도메인 추천을 위한 키워드' } + }, + required: ['keywords'] + } + } + }, + { + type: 'function' as const, + function: { + name: 'get_whois', + description: '도메인 WHOIS 정보 조회', + parameters: { + type: 'object', + properties: { + domain: { type: 'string', description: 'WHOIS 조회할 도메인' } + }, + required: ['domain'] + } + } + }, + { + type: 'function' as const, + function: { + name: 'get_price', + description: 'TLD별 도메인 가격 조회', + parameters: { + type: 'object', + properties: { + tld: { type: 'string', description: '가격 확인할 TLD (예: com, net, io)' } + }, + required: ['tld'] + } + } + }, + { + type: 'function' as const, + function: { + name: 'register_domain', + description: '도메인 등록 요청 (잔액 확인 후 결제 진행)', + parameters: { + type: 'object', + properties: { + domain: { type: 'string', description: '등록할 도메인' } + }, + required: ['domain'] + } + } + }, + { + type: 'function' as const, + function: { + name: 'set_nameservers', + description: '도메인 네임서버 변경', + parameters: { + type: 'object', + properties: { + domain: { type: 'string', description: '변경할 도메인' }, + nameservers: { + type: 'array', + items: { type: 'string' }, + description: '설정할 네임서버 목록' + } + }, + required: ['domain', 'nameservers'] + } + } + } +]; + +// OpenAI API response types +interface OpenAIToolCall { + id: string; + type: 'function'; + function: { + name: string; + arguments: string; + }; +} + +interface OpenAIMessage { + role: 'assistant'; + content: string | null; + tool_calls?: OpenAIToolCall[]; +} + +interface OpenAIAPIResponse { + choices: Array<{ + message: OpenAIMessage; + finish_reason: string; + }>; +} + +/** + * Domain Expert AI 호출 (Function Calling 지원) + * + * @param session - DomainSession + * @param userMessage - 사용자 메시지 + * @param env - Environment + * @returns AI 응답 및 tool_calls (있을 경우) + */ +async function callDomainExpertAI( + session: DomainSession, + userMessage: string, + env: Env +): Promise<{ response: string; toolCalls?: Array<{ name: string; arguments: Record }> }> { + if (!env.OPENAI_API_KEY) { + throw new Error('OPENAI_API_KEY not configured'); + } + + const { getOpenAIUrl } = await import('../utils/api-urls'); + + // Build conversation history + const conversationHistory = session.messages.map(m => ({ + role: m.role === 'user' ? 'user' as const : 'assistant' as const, + content: m.content, + })); + + const systemPrompt = `${DOMAIN_EXPERT_PROMPT} + +## 현재 수집된 정보 +${JSON.stringify(session.collected_info, null, 2)} + +## 대화 흐름 +1. 키워드 파악: "어떤 서비스 이름이나 키워드로 찾으시나요?" +2. 용도 파악: "어떤 용도로 사용하실 건가요? (예: 쇼핑몰, 블로그, 회사 홈페이지)" +3. 예산 확인 (선택): "예산 범위가 있으신가요?" +4. 정보가 충분하면 search_suggestions 도구로 추천 + +## 도구 사용 가이드 +- 키워드가 파악되면 즉시 search_suggestions 호출 +- 특정 도메인 확인 요청 → check_domain +- WHOIS 정보 요청 → get_whois +- TLD 가격 문의 → get_price +- 등록 요청 → register_domain +- 네임서버 변경 → set_nameservers`; + + try { + const messages: Array<{ + role: string; + content: string | null; + tool_calls?: OpenAIToolCall[]; + tool_call_id?: string; + name?: string + }> = [ + { role: 'system', content: systemPrompt }, + ...conversationHistory, + { role: 'user', content: userMessage }, + ]; + + const MAX_TOOL_CALLS = 3; + let toolCallCount = 0; + + // Loop to handle tool calls + while (toolCallCount < MAX_TOOL_CALLS) { + const requestBody = { + model: 'gpt-4o-mini', + messages, + tools: DOMAIN_TOOLS, + tool_choice: 'auto', + max_tokens: 800, + temperature: 0.7, + }; + + const response = await fetch(getOpenAIUrl(env), { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${env.OPENAI_API_KEY}`, + }, + body: JSON.stringify(requestBody), + }); + + if (!response.ok) { + const error = await response.text(); + throw new Error(`OpenAI API error: ${response.status} - ${error}`); + } + + const data = await response.json() as OpenAIAPIResponse; + const assistantMessage = data.choices[0].message; + + // Check if AI wants to call tools + if (assistantMessage.tool_calls && assistantMessage.tool_calls.length > 0) { + logger.info('도구 호출 요청', { + tools: assistantMessage.tool_calls.map(tc => tc.function.name), + }); + + // Return tool calls to be executed by caller + const toolCalls = assistantMessage.tool_calls.map(tc => ({ + name: tc.function.name, + arguments: JSON.parse(tc.function.arguments) as Record, + })); + + return { + response: assistantMessage.content || '', + toolCalls, + }; + } + + // No tool calls - return final response + const aiResponse = assistantMessage.content || ''; + logger.info('AI 응답', { response: aiResponse.slice(0, 200) }); + + // Check for special markers + if (aiResponse.includes('__PASSTHROUGH__')) { + return { response: '__PASSTHROUGH__' }; + } + + // Check for session end marker + const sessionEnd = aiResponse.includes('__SESSION_END__'); + const cleanResponse = aiResponse.replace('__SESSION_END__', '').trim(); + + return { + response: sessionEnd ? `${cleanResponse}\n\n[세션 종료]` : cleanResponse, + }; + } + + // Max tool calls reached + logger.warn('최대 도구 호출 횟수 도달', { toolCallCount }); + return { + response: '분석이 완료되었습니다. 추천을 진행하겠습니다.', + }; + } catch (error) { + logger.error('Domain Expert AI 호출 실패', error as Error); + throw error; + } +} + /** * 도메인 추천 상담 처리 (메인 함수) *