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