feat: add Server Expert AI with search/docs tools for trend-aware recommendations
- Add server-agent.ts with 30-year senior architect persona - Implement KV-based session management for multi-turn conversations - Add search_trends (Brave Search) and lookup_framework_docs (Context7) tools - Function Calling support with max 3 tool calls per request - Auto-infer tech stack and expected users from use case/scale - Prohibit competitor provider mentions (AWS, GCP, Azure, etc.) - Simplify main AI system prompt, delegate complex logic to expert AI Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -93,6 +93,25 @@ export async function generateOpenAIResponse(
|
||||
telegramUserId?: string,
|
||||
db?: D1Database
|
||||
): Promise<string> {
|
||||
// Check if server consultation session is active
|
||||
if (telegramUserId && env.SESSION_KV) {
|
||||
try {
|
||||
const { getServerSession, processServerConsultation } = await import('./server-agent');
|
||||
const session = await getServerSession(env.SESSION_KV, telegramUserId);
|
||||
|
||||
if (session && session.status !== 'completed') {
|
||||
logger.info('Active server session detected, routing to consultation', {
|
||||
userId: telegramUserId,
|
||||
status: session.status
|
||||
});
|
||||
return await processServerConsultation(userMessage, session, env);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Session check failed, continuing with normal flow', error as Error);
|
||||
// Continue with normal flow if session check fails
|
||||
}
|
||||
}
|
||||
|
||||
if (!env.OPENAI_API_KEY) {
|
||||
throw new Error('OPENAI_API_KEY not configured');
|
||||
}
|
||||
|
||||
457
src/server-agent.ts
Normal file
457
src/server-agent.ts
Normal file
@@ -0,0 +1,457 @@
|
||||
/**
|
||||
* Server Expert Agent - 서버 전문가 AI 상담 시스템
|
||||
*
|
||||
* 기능:
|
||||
* - 대화형 서버 추천 상담
|
||||
* - 세션 기반 정보 수집
|
||||
* - 충분한 정보 수집 시 자동 추천
|
||||
* - Brave Search / Context7 도구로 최신 트렌드 반영
|
||||
*/
|
||||
|
||||
import type { Env, ServerSession } from './types';
|
||||
import { createLogger } from './utils/logger';
|
||||
import { executeSearchWeb, executeLookupDocs } from './tools/search-tool';
|
||||
|
||||
const logger = createLogger('server-agent');
|
||||
|
||||
// KV Session Management
|
||||
const SESSION_TTL = 3600; // 1 hour
|
||||
const SESSION_KEY_PREFIX = 'server_session:';
|
||||
|
||||
export async function getServerSession(
|
||||
kv: KVNamespace,
|
||||
userId: string
|
||||
): Promise<ServerSession | null> {
|
||||
try {
|
||||
const key = `${SESSION_KEY_PREFIX}${userId}`;
|
||||
const data = await kv.get(key, 'json');
|
||||
|
||||
if (!data) {
|
||||
logger.info('세션 없음', { userId });
|
||||
return null;
|
||||
}
|
||||
|
||||
logger.info('세션 조회 성공', { userId, status: (data as ServerSession).status });
|
||||
return data as ServerSession;
|
||||
} catch (error) {
|
||||
logger.error('세션 조회 실패', error as Error, { userId });
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function saveServerSession(
|
||||
kv: KVNamespace,
|
||||
userId: string,
|
||||
session: ServerSession
|
||||
): Promise<void> {
|
||||
try {
|
||||
const key = `${SESSION_KEY_PREFIX}${userId}`;
|
||||
session.updatedAt = Date.now();
|
||||
|
||||
await kv.put(key, JSON.stringify(session), {
|
||||
expirationTtl: SESSION_TTL,
|
||||
});
|
||||
|
||||
logger.info('세션 저장 성공', { userId, status: session.status });
|
||||
} catch (error) {
|
||||
logger.error('세션 저장 실패', error as Error, { userId });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteServerSession(
|
||||
kv: KVNamespace,
|
||||
userId: string
|
||||
): Promise<void> {
|
||||
try {
|
||||
const key = `${SESSION_KEY_PREFIX}${userId}`;
|
||||
await kv.delete(key);
|
||||
logger.info('세션 삭제 성공', { userId });
|
||||
} catch (error) {
|
||||
logger.error('세션 삭제 실패', error as Error, { userId });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Server Expert AI Tools
|
||||
const serverExpertTools = [
|
||||
{
|
||||
type: 'function' as const,
|
||||
function: {
|
||||
name: 'search_trends',
|
||||
description: '최신 기술 트렌드, 서버 요구사항, 프레임워크 인기도를 검색합니다. 예: "2024 WordPress server requirements", "Next.js hosting best practices"',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
query: {
|
||||
type: 'string',
|
||||
description: '검색 쿼리 (영문 권장, 기술 키워드 포함)',
|
||||
},
|
||||
},
|
||||
required: ['query'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'function' as const,
|
||||
function: {
|
||||
name: 'lookup_framework_docs',
|
||||
description: '프레임워크/라이브러리 공식 문서에서 서버 요구사항, 배포 가이드, 권장 환경을 조회합니다.',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
library: {
|
||||
type: 'string',
|
||||
description: '라이브러리/프레임워크 이름 (예: nextjs, laravel, django, wordpress)',
|
||||
},
|
||||
topic: {
|
||||
type: 'string',
|
||||
description: '조회할 주제 (예: deployment requirements, production setup, server specs)',
|
||||
},
|
||||
},
|
||||
required: ['library', 'topic'],
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
// Execute server expert tool
|
||||
async function executeServerExpertTool(
|
||||
toolName: string,
|
||||
args: Record<string, unknown>,
|
||||
env: Env
|
||||
): Promise<string> {
|
||||
logger.info('도구 실행', { toolName, args });
|
||||
|
||||
switch (toolName) {
|
||||
case 'search_trends': {
|
||||
const result = await executeSearchWeb({ query: args.query as string }, env);
|
||||
return result;
|
||||
}
|
||||
case 'lookup_framework_docs': {
|
||||
const result = await executeLookupDocs({
|
||||
library: args.library as string,
|
||||
query: args.topic as string,
|
||||
}, env);
|
||||
return result;
|
||||
}
|
||||
default:
|
||||
return `알 수 없는 도구: ${toolName}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Tech stack inference from use case
|
||||
function inferTechStack(useCase: string): string[] {
|
||||
const useCaseLower = useCase.toLowerCase();
|
||||
|
||||
if (/블로그|blog|wordpress/.test(useCaseLower)) {
|
||||
return ['wordpress'];
|
||||
}
|
||||
if (/쇼핑몰|이커머스|ecommerce|shop|store/.test(useCaseLower)) {
|
||||
return ['ecommerce'];
|
||||
}
|
||||
if (/커뮤니티|게시판|forum|community/.test(useCaseLower)) {
|
||||
return ['php', 'mysql'];
|
||||
}
|
||||
if (/api|백엔드|backend/.test(useCaseLower)) {
|
||||
return ['nodejs', 'express'];
|
||||
}
|
||||
|
||||
return ['web']; // Default
|
||||
}
|
||||
|
||||
// Expected users inference from scale
|
||||
function inferExpectedUsers(scale: string): number {
|
||||
if (scale === 'personal') return 100;
|
||||
if (scale === 'business') return 500;
|
||||
return 100; // Default to personal
|
||||
}
|
||||
|
||||
// OpenAI API 응답 타입
|
||||
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;
|
||||
}>;
|
||||
}
|
||||
|
||||
// OpenAI 호출 (서버 전문가 AI with Function Calling)
|
||||
async function callServerExpertAI(
|
||||
env: Env,
|
||||
session: ServerSession,
|
||||
userMessage: string
|
||||
): Promise<{ action: 'question' | 'recommend'; message: string; collectedInfo: ServerSession['collectedInfo'] }> {
|
||||
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 = `당신은 30년 경력의 시니어 클라우드 아키텍트입니다.
|
||||
|
||||
## 전문성 (30년 경력)
|
||||
- 서버 엔지니어: Linux, Windows Server, 가상화, 컨테이너 마스터
|
||||
- 네트워크 엔지니어: 로드밸런싱, CDN, DNS, 보안 설계 전문
|
||||
- 클라우드 아키텍트: 모든 클라우드 플랫폼 경험
|
||||
- 수천 개의 서버 구축 경험으로 용도만 들으면 최적 스펙을 바로 판단 가능
|
||||
|
||||
## 성격
|
||||
- 따뜻하고 친근하지만 전문적인 어조
|
||||
- 비기술자도 이해하기 쉽게 설명
|
||||
- 고객의 예산과 상황을 항상 배려
|
||||
- 불필요한 기술 용어 사용 자제
|
||||
|
||||
## 금지 사항 (절대 위반 금지)
|
||||
- AWS, GCP, Azure, Vultr, Linode, DigitalOcean 등 다른 클라우드 프로바이더 언급 금지
|
||||
- 경쟁사 서비스 추천 금지
|
||||
- 우리 서비스(Anvil)만 추천
|
||||
- "다른 곳도 고려해보세요" 같은 멘트 금지
|
||||
|
||||
## 도구 사용 가이드 (적극적으로 활용할 것)
|
||||
- 고객이 특정 프레임워크/기술을 언급하면 (예: Next.js, Laravel, Django, Astro, Bun, Rust 등) → 반드시 lookup_framework_docs 호출하여 최신 공식 권장 스펙 확인
|
||||
- "최신", "트렌드", "2024", "2025", "요즘" 등 시의성 있는 키워드 → 반드시 search_trends 호출
|
||||
- 블로그, 쇼핑몰 같은 일반적 용도는 경험으로 바로 답변
|
||||
- 도구 결과를 자연스럽게 메시지에 포함 (예: "공식 문서에 따르면...")
|
||||
|
||||
## 대화 흐름
|
||||
1. 용도 파악: "어떤 서비스를 운영하실 건가요? (예: 블로그, 쇼핑몰, 커뮤니티)"
|
||||
2. 규모 파악: "개인용인가요, 사업용인가요?"
|
||||
3. 정보가 충분하면 즉시 추천 (추가 질문 없이)
|
||||
|
||||
## 핵심 규칙 (반드시 준수)
|
||||
- 기술 스택, 동시접속자 수, 트래픽 패턴은 절대 묻지 않음 (30년 경험으로 알아서 추론)
|
||||
- "모르겠어요", "아무거나", "글쎄요" → 즉시 action="recommend" (기본값: 개인용 웹서비스)
|
||||
- 용도+규모 한번에 말하면 → 즉시 action="recommend"
|
||||
- 용도만 말해도 → 개인용으로 가정하고 action="recommend" 가능
|
||||
- 질문은 최대 2번까지, 그 이후는 무조건 action="recommend"
|
||||
|
||||
## 추론 규칙 (30년 경험 기반)
|
||||
- 블로그 → WordPress, 1GB RAM이면 충분
|
||||
- 쇼핑몰 → 2GB+ RAM, DB 분리 고려
|
||||
- 커뮤니티 → PHP+MySQL, 트래픽에 따라 2~4GB
|
||||
- 게임서버 → 고사양 CPU, 낮은 레이턴시 리전
|
||||
- 규모: personal→100명, business→500명
|
||||
|
||||
## 현재 수집된 정보
|
||||
${JSON.stringify(session.collectedInfo, null, 2)}
|
||||
|
||||
## 응답 형식 (반드시 JSON만 반환, 다른 텍스트 절대 금지)
|
||||
{
|
||||
"action": "question" | "recommend",
|
||||
"message": "사용자에게 보여줄 메시지 (도구에서 얻은 정보를 자연스럽게 포함)",
|
||||
"collectedInfo": {
|
||||
"useCase": "용도 (없으면 '웹서비스')",
|
||||
"scale": "personal 또는 business (없으면 'personal')"
|
||||
}
|
||||
}
|
||||
|
||||
중요: 정보가 부족해도 기본값으로 action="recommend" 하세요. 30년 경험이면 충분합니다.`;
|
||||
|
||||
try {
|
||||
// Messages array that we'll build up with tool results
|
||||
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 response = await fetch(getOpenAIUrl(env), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${env.OPENAI_API_KEY}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: 'gpt-4o-mini',
|
||||
messages,
|
||||
tools: serverExpertTools,
|
||||
tool_choice: 'auto',
|
||||
max_tokens: 800,
|
||||
temperature: 0.7,
|
||||
}),
|
||||
});
|
||||
|
||||
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),
|
||||
});
|
||||
|
||||
// Add assistant message with tool calls
|
||||
messages.push({
|
||||
role: 'assistant',
|
||||
content: assistantMessage.content,
|
||||
tool_calls: assistantMessage.tool_calls,
|
||||
});
|
||||
|
||||
// Execute each tool and add results
|
||||
for (const toolCall of assistantMessage.tool_calls) {
|
||||
const args = JSON.parse(toolCall.function.arguments);
|
||||
const result = await executeServerExpertTool(toolCall.function.name, args, env);
|
||||
|
||||
messages.push({
|
||||
role: 'tool',
|
||||
tool_call_id: toolCall.id,
|
||||
name: toolCall.function.name,
|
||||
content: result,
|
||||
});
|
||||
|
||||
toolCallCount++;
|
||||
}
|
||||
|
||||
// Continue loop to get AI's response with tool results
|
||||
continue;
|
||||
}
|
||||
|
||||
// No tool calls - parse the final response
|
||||
const aiResponse = assistantMessage.content || '';
|
||||
logger.info('AI 응답', { response: aiResponse.slice(0, 200), toolCallCount });
|
||||
|
||||
// JSON 파싱 (마크다운 코드 블록 제거)
|
||||
const jsonMatch = aiResponse.match(/```(?:json)?\s*(\{[\s\S]*?\})\s*```/) ||
|
||||
aiResponse.match(/(\{[\s\S]*\})/);
|
||||
|
||||
if (!jsonMatch) {
|
||||
logger.error('JSON 파싱 실패', new Error('No JSON found'), { response: aiResponse });
|
||||
throw new Error('AI 응답 형식 오류');
|
||||
}
|
||||
|
||||
const parsed = JSON.parse(jsonMatch[1]);
|
||||
|
||||
// Validate response structure
|
||||
if (!parsed.action || !parsed.message) {
|
||||
throw new Error('Invalid AI response structure');
|
||||
}
|
||||
|
||||
return {
|
||||
action: parsed.action,
|
||||
message: parsed.message,
|
||||
collectedInfo: parsed.collectedInfo || session.collectedInfo,
|
||||
};
|
||||
}
|
||||
|
||||
// Max tool calls reached, force a recommendation
|
||||
logger.warn('최대 도구 호출 횟수 도달', { toolCallCount });
|
||||
return {
|
||||
action: 'recommend',
|
||||
message: '분석이 완료되었습니다. 최적의 서버를 추천해 드리겠습니다.',
|
||||
collectedInfo: session.collectedInfo,
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error('Server Expert AI 호출 실패', error as Error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Main consultation processing
|
||||
export async function processServerConsultation(
|
||||
userMessage: string,
|
||||
session: ServerSession,
|
||||
env: Env
|
||||
): Promise<string> {
|
||||
try {
|
||||
logger.info('상담 처리 시작', {
|
||||
userId: session.telegramUserId,
|
||||
message: userMessage.slice(0, 50),
|
||||
status: session.status
|
||||
});
|
||||
|
||||
// Add user message to history
|
||||
session.messages.push({ role: 'user', content: userMessage });
|
||||
|
||||
// Call Server Expert AI
|
||||
const aiResult = await callServerExpertAI(env, session, userMessage);
|
||||
|
||||
// Update collected info
|
||||
session.collectedInfo = { ...session.collectedInfo, ...aiResult.collectedInfo };
|
||||
|
||||
// Add AI response to history
|
||||
session.messages.push({ role: 'assistant', content: aiResult.message });
|
||||
|
||||
if (aiResult.action === 'recommend') {
|
||||
// Mark session as recommending
|
||||
session.status = 'recommending';
|
||||
await saveServerSession(env.SESSION_KV, session.telegramUserId, session);
|
||||
|
||||
// Call recommendation API
|
||||
logger.info('추천 API 호출', { collectedInfo: session.collectedInfo });
|
||||
|
||||
const { executeServerAction } = await import('./tools/server-tool');
|
||||
|
||||
const techStack = session.collectedInfo.useCase
|
||||
? inferTechStack(session.collectedInfo.useCase)
|
||||
: ['web'];
|
||||
|
||||
const expectedUsers = session.collectedInfo.scale
|
||||
? inferExpectedUsers(session.collectedInfo.scale)
|
||||
: 100;
|
||||
|
||||
const recommendation = await executeServerAction(
|
||||
'recommend',
|
||||
{
|
||||
tech_stack: techStack,
|
||||
expected_users: expectedUsers,
|
||||
use_case: session.collectedInfo.useCase || '웹 서비스',
|
||||
region_preference: session.collectedInfo.regionPreference,
|
||||
budget_limit: session.collectedInfo.budgetLimit,
|
||||
lang: 'ko',
|
||||
},
|
||||
env,
|
||||
session.telegramUserId
|
||||
);
|
||||
|
||||
// Mark session as completed and delete
|
||||
session.status = 'completed';
|
||||
await deleteServerSession(env.SESSION_KV, session.telegramUserId);
|
||||
|
||||
return `${aiResult.message}\n\n${recommendation}`;
|
||||
} else {
|
||||
// Continue gathering information
|
||||
session.status = 'gathering';
|
||||
await saveServerSession(env.SESSION_KV, session.telegramUserId, session);
|
||||
|
||||
return aiResult.message;
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('상담 처리 실패', error as Error, { userId: session.telegramUserId });
|
||||
|
||||
// Clean up session on error
|
||||
await deleteServerSession(env.SESSION_KV, session.telegramUserId);
|
||||
|
||||
return '죄송합니다. 서버 추천 중 오류가 발생했습니다.\n다시 시도하려면 "서버 추천"이라고 말씀해주세요.';
|
||||
}
|
||||
}
|
||||
@@ -383,7 +383,9 @@ ${integratedProfile}
|
||||
- 날씨, 시간, 계산 요청은 제공된 도구를 사용하세요.
|
||||
- 최신 정보, 실시간 데이터, 뉴스, 특정 사실 확인이 필요한 질문은 반드시 search_web 도구로 검색하세요. 자체 지식으로 답변하지 마세요.
|
||||
- 예치금, 입금, 충전, 잔액, 계좌 관련 요청은 반드시 manage_deposit 도구를 사용하세요. 금액 제한이나 규칙을 직접 판단하지 마세요.
|
||||
- 서버, VPS, 클라우드, 호스팅, 인스턴스 관련 요청은 반드시 manage_server 도구를 사용하세요. 서버 추천 시 기술 스택과 예상 사용자 수를 확인하세요. 이전 대화에 서버 추천 결과가 있어도 항상 새로 도구를 호출하세요(가격/재고 변동).
|
||||
- 서버, VPS, 클라우드, 호스팅 관련 요청:
|
||||
• 첫 요청: manage_server(action="start_consultation")을 호출하여 상담 시작
|
||||
• 서버 상담 중인 메시지는 자동으로 전문가 AI에게 전달됨 (추가 처리 불필요)
|
||||
- 도메인 추천, 도메인 제안, 도메인 아이디어 요청은 반드시 suggest_domains 도구를 사용하세요. 직접 도메인을 나열하지 마세요.
|
||||
- 도메인/TLD 가격 조회(".com 가격", ".io 가격" 등)는 manage_domain 도구의 action=price를 사용하세요.
|
||||
- 기타 도메인 관련 요청(조회, 등록, 네임서버, WHOIS 등)은 manage_domain 도구를 사용하세요.
|
||||
|
||||
@@ -56,7 +56,8 @@ const SuggestDomainsArgsSchema = z.object({
|
||||
});
|
||||
|
||||
const ManageServerArgsSchema = z.object({
|
||||
action: z.enum(['recommend', 'order', 'start', 'stop', 'delete', 'list']),
|
||||
action: z.enum(['recommend', 'order', 'start', 'stop', 'delete', 'list',
|
||||
'start_consultation', 'continue_consultation', 'cancel_consultation']),
|
||||
tech_stack: z.array(z.string().min(1).max(100)).max(20).optional(),
|
||||
expected_users: z.number().int().positive().optional(),
|
||||
use_case: z.string().min(1).max(500).optional(),
|
||||
@@ -67,6 +68,7 @@ const ManageServerArgsSchema = z.object({
|
||||
server_id: z.string().min(1).max(100).optional(),
|
||||
region_code: z.string().min(1).max(50).optional(),
|
||||
label: z.string().min(1).max(100).optional(),
|
||||
message: z.string().min(1).max(500).optional(), // For continue_consultation
|
||||
});
|
||||
|
||||
// All tools array (used by OpenAI API)
|
||||
|
||||
@@ -136,32 +136,33 @@ export const manageServerTool = {
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'manage_server',
|
||||
description: '클라우드 서버 관리 및 추천. "서버", "VPS", "클라우드", "호스팅" 등의 키워드가 포함되면 사용하세요.',
|
||||
description: '클라우드 서버 관리 및 추천. 서버/VPS/클라우드/호스팅 관련 요청 시 사용. 상담 시작: start_consultation',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
action: {
|
||||
type: 'string',
|
||||
enum: ['recommend', 'order', 'start', 'stop', 'delete', 'list'],
|
||||
description: 'recommend: 서버 추천, order: 서버 신청 (준비 중), start: 서버 켜기 (준비 중), stop: 서버 끄기 (준비 중), delete: 서버 해지 (준비 중), list: 내 서버 목록 (준비 중)',
|
||||
enum: ['recommend', 'order', 'start', 'stop', 'delete', 'list',
|
||||
'start_consultation', 'continue_consultation', 'cancel_consultation'],
|
||||
description: 'start_consultation: 서버 추천 상담 시작, continue_consultation: 상담 계속, recommend: 직접 추천',
|
||||
},
|
||||
tech_stack: {
|
||||
type: 'array',
|
||||
items: { type: 'string' },
|
||||
description: '사용할 기술 스택 (예: ["nodejs", "nginx", "postgresql"]). recommend action에서 필수',
|
||||
description: '기술 스택. 용도에서 추론 (블로그→wordpress, 쇼핑몰→ecommerce, 커뮤니티→php,mysql). 모르면 ["web"]',
|
||||
},
|
||||
expected_users: {
|
||||
type: 'number',
|
||||
description: '예상 동시 접속자 수. recommend action에서 필수',
|
||||
description: '예상 사용자 수. 모르면 개인용=100, 사업용=500 사용',
|
||||
},
|
||||
use_case: {
|
||||
type: 'string',
|
||||
description: '사용 목적 설명 (예: "웹사이트 호스팅", "API 서버"). recommend action에서 필수',
|
||||
description: '용도 (예: "블로그", "쇼핑몰", "커뮤니티")',
|
||||
},
|
||||
traffic_pattern: {
|
||||
type: 'string',
|
||||
enum: ['steady', 'spiky', 'growing'],
|
||||
description: '트래픽 패턴. steady: 일정한 트래픽, spiky: 순간 급증, growing: 점진적 성장. recommend action에서 선택',
|
||||
description: '생략 가능. 기본값: steady',
|
||||
},
|
||||
region_preference: {
|
||||
type: 'array',
|
||||
@@ -189,6 +190,10 @@ export const manageServerTool = {
|
||||
type: 'string',
|
||||
description: '서버 라벨 (예: "myapp-prod"). order action에서 필수',
|
||||
},
|
||||
message: {
|
||||
type: 'string',
|
||||
description: '사용자 메시지. continue_consultation action에서 필수',
|
||||
},
|
||||
},
|
||||
required: ['action'],
|
||||
},
|
||||
@@ -314,7 +319,7 @@ function formatRecommendations(data: RecommendResponse): string {
|
||||
}
|
||||
|
||||
// 서버 작업 직접 실행
|
||||
async function executeServerAction(
|
||||
export async function executeServerAction(
|
||||
action: string,
|
||||
args: {
|
||||
tech_stack?: string[];
|
||||
@@ -327,6 +332,7 @@ async function executeServerAction(
|
||||
server_id?: string;
|
||||
region_code?: string;
|
||||
label?: string;
|
||||
message?: string;
|
||||
},
|
||||
env?: Env,
|
||||
telegramUserId?: string
|
||||
@@ -338,6 +344,76 @@ async function executeServerAction(
|
||||
});
|
||||
|
||||
switch (action) {
|
||||
case 'start_consultation': {
|
||||
// Import session functions
|
||||
const { saveServerSession } = await import('../server-agent');
|
||||
|
||||
if (!telegramUserId) {
|
||||
return '🚫 사용자 인증이 필요합니다.';
|
||||
}
|
||||
|
||||
if (!env?.SESSION_KV) {
|
||||
return '🚫 세션 저장소가 설정되지 않았습니다.';
|
||||
}
|
||||
|
||||
const session: import('../types').ServerSession = {
|
||||
telegramUserId,
|
||||
status: 'gathering',
|
||||
collectedInfo: {},
|
||||
messages: [],
|
||||
createdAt: Date.now(),
|
||||
updatedAt: Date.now(),
|
||||
};
|
||||
|
||||
await saveServerSession(env.SESSION_KV, telegramUserId, session);
|
||||
|
||||
logger.info('상담 세션 생성', { userId: maskUserId(telegramUserId) });
|
||||
|
||||
return '안녕하세요! 서버 추천을 도와드리겠습니다. 😊\n\n어떤 서비스를 운영하실 건가요?\n예: 블로그, 쇼핑몰, 커뮤니티, API 서버 등';
|
||||
}
|
||||
|
||||
case 'continue_consultation': {
|
||||
const { getServerSession, processServerConsultation } = await import('../server-agent');
|
||||
|
||||
if (!telegramUserId) {
|
||||
return '🚫 사용자 인증이 필요합니다.';
|
||||
}
|
||||
|
||||
if (!env?.SESSION_KV) {
|
||||
return '🚫 세션 저장소가 설정되지 않았습니다.';
|
||||
}
|
||||
|
||||
if (!args.message) {
|
||||
return '🚫 메시지가 필요합니다.';
|
||||
}
|
||||
|
||||
const session = await getServerSession(env.SESSION_KV, telegramUserId);
|
||||
if (!session) {
|
||||
return '세션이 만료되었습니다. 다시 시작하려면 "서버 추천"이라고 말씀해주세요.';
|
||||
}
|
||||
|
||||
const result = await processServerConsultation(args.message, session, env);
|
||||
return result;
|
||||
}
|
||||
|
||||
case 'cancel_consultation': {
|
||||
const { deleteServerSession } = await import('../server-agent');
|
||||
|
||||
if (!telegramUserId) {
|
||||
return '🚫 사용자 인증이 필요합니다.';
|
||||
}
|
||||
|
||||
if (!env?.SESSION_KV) {
|
||||
return '🚫 세션 저장소가 설정되지 않았습니다.';
|
||||
}
|
||||
|
||||
await deleteServerSession(env.SESSION_KV, telegramUserId);
|
||||
|
||||
logger.info('상담 세션 취소', { userId: maskUserId(telegramUserId) });
|
||||
|
||||
return '상담이 취소되었습니다. 다시 시작하려면 "서버 추천"이라고 말씀해주세요.';
|
||||
}
|
||||
|
||||
case 'recommend': {
|
||||
const { tech_stack, expected_users, use_case, traffic_pattern, region_preference, budget_limit, lang } = args;
|
||||
|
||||
@@ -438,6 +514,7 @@ export async function executeManageServer(
|
||||
server_id?: string;
|
||||
region_code?: string;
|
||||
label?: string;
|
||||
message?: string;
|
||||
},
|
||||
env?: Env,
|
||||
telegramUserId?: string
|
||||
|
||||
21
src/types.ts
21
src/types.ts
@@ -204,7 +204,10 @@ export interface ManageServerArgs {
|
||||
| "start"
|
||||
| "stop"
|
||||
| "delete"
|
||||
| "list";
|
||||
| "list"
|
||||
| "start_consultation"
|
||||
| "continue_consultation"
|
||||
| "cancel_consultation";
|
||||
tech_stack?: string[];
|
||||
expected_users?: number;
|
||||
use_case?: string;
|
||||
@@ -215,6 +218,22 @@ export interface ManageServerArgs {
|
||||
server_id?: string;
|
||||
region_code?: string;
|
||||
label?: string;
|
||||
message?: string; // For continue_consultation
|
||||
}
|
||||
|
||||
// Server Consultation Session
|
||||
export interface ServerSession {
|
||||
telegramUserId: string;
|
||||
status: 'gathering' | 'recommending' | 'completed';
|
||||
collectedInfo: {
|
||||
useCase?: string;
|
||||
scale?: 'personal' | 'business';
|
||||
budgetLimit?: number;
|
||||
regionPreference?: string[];
|
||||
};
|
||||
messages: Array<{ role: 'user' | 'assistant'; content: string }>;
|
||||
createdAt: number;
|
||||
updatedAt: number;
|
||||
}
|
||||
|
||||
// Deposit Agent 결과 타입
|
||||
|
||||
Reference in New Issue
Block a user