feat: add domain agent AI call functions
- Add domain expert system prompt - Add domain tools for function calling - Add callDomainExpertAI function Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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<string, unknown> }> }> {
|
||||
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<string, unknown>,
|
||||
}));
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 도메인 추천 상담 처리 (메인 함수)
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user