Memory System: - Add category-based memory storage (company, tech, role, location, server) - Silent background saving via saveMemorySilently() - Category-based overwrite (same category replaces old memory) - Server-related pattern detection (AWS, GCP, k8s, traffic info) - Memory management tool (list, delete) Troubleshoot Agent: - Session-based troubleshooting conversation (KV, 1h TTL) - 20-year DevOps/SRE expert persona - Support for server/infra, domain/DNS, code/deploy, network, database issues - Internal tools: search_solution (Brave), lookup_docs (Context7) - Auto-trigger on error-related keywords - Session completion and cancellation support Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
272 lines
10 KiB
TypeScript
272 lines
10 KiB
TypeScript
// Tool Registry - All tools exported from here
|
|
import { z } from 'zod';
|
|
import { createLogger } from '../utils/logger';
|
|
|
|
const logger = createLogger('tools');
|
|
|
|
import { weatherTool, executeWeather } from './weather-tool';
|
|
import { searchWebTool, lookupDocsTool, executeSearchWeb, executeLookupDocs } from './search-tool';
|
|
import { manageDomainTool, suggestDomainsTool, executeManageDomain, executeSuggestDomains } from './domain-tool';
|
|
import { manageDepositTool, executeManageDeposit } from './deposit-tool';
|
|
import { manageServerTool, executeManageServer } from './server-tool';
|
|
import { manageTroubleshootTool, executeManageTroubleshoot } from './troubleshoot-tool';
|
|
import { getCurrentTimeTool, calculateTool, executeGetCurrentTime, executeCalculate } from './utility-tools';
|
|
import { redditSearchTool, executeRedditSearch } from './reddit-tool';
|
|
import type { Env } from '../types';
|
|
|
|
// Zod validation schemas for tool arguments
|
|
const DOMAIN_REGEX = /^[a-zA-Z0-9][a-zA-Z0-9.-]{0,251}[a-zA-Z0-9]?\.[a-zA-Z]{2,}$/;
|
|
|
|
const ManageDomainArgsSchema = z.object({
|
|
action: z.enum(['register', 'check', 'whois', 'list', 'info', 'get_ns', 'set_ns', 'price', 'cheapest']),
|
|
domain: z.string().max(253).regex(DOMAIN_REGEX, 'Invalid domain format').optional(),
|
|
nameservers: z.array(z.string().max(255)).max(10).optional(),
|
|
tld: z.string().max(20).optional(),
|
|
});
|
|
|
|
const ManageDepositArgsSchema = z.object({
|
|
action: z.enum(['balance', 'account', 'request', 'history', 'cancel', 'pending', 'confirm', 'reject']),
|
|
depositor_name: z.string().max(100).optional(),
|
|
amount: z.number().positive().max(100_000_000).optional(), // 1억원 상한
|
|
transaction_id: z.number().int().positive().optional(),
|
|
limit: z.number().int().positive().max(100).optional(),
|
|
});
|
|
|
|
const SearchWebArgsSchema = z.object({
|
|
query: z.string().min(1).max(500),
|
|
});
|
|
|
|
const GetWeatherArgsSchema = z.object({
|
|
city: z.string().min(1).max(100),
|
|
});
|
|
|
|
const CalculateArgsSchema = z.object({
|
|
expression: z.string().min(1).max(200),
|
|
});
|
|
|
|
const GetCurrentTimeArgsSchema = z.object({
|
|
timezone: z.string().max(50).optional(),
|
|
});
|
|
|
|
const LookupDocsArgsSchema = z.object({
|
|
library: z.string().min(1).max(100),
|
|
query: z.string().min(1).max(500),
|
|
});
|
|
|
|
const SuggestDomainsArgsSchema = z.object({
|
|
keywords: z.string().min(1).max(500),
|
|
});
|
|
|
|
const RedditSearchArgsSchema = z.object({
|
|
query: z.string().min(1).max(500),
|
|
limit: z.number().int().positive().max(25).optional(),
|
|
sort: z.enum(['hot', 'new', 'top', 'relevance']).optional(),
|
|
});
|
|
|
|
const ManageServerArgsSchema = z.object({
|
|
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(),
|
|
traffic_pattern: z.enum(['steady', 'burst', 'high']).optional(),
|
|
region_preference: z.array(z.string().min(1).max(50)).max(10).optional(),
|
|
budget_limit: z.number().positive().optional(),
|
|
lang: z.enum(['ko', 'en']).optional(),
|
|
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
|
|
});
|
|
|
|
const ManageTroubleshootArgsSchema = z.object({
|
|
action: z.enum(['start', 'cancel']),
|
|
});
|
|
|
|
// All tools array (used by OpenAI API)
|
|
export const tools = [
|
|
weatherTool,
|
|
searchWebTool,
|
|
getCurrentTimeTool,
|
|
calculateTool,
|
|
lookupDocsTool,
|
|
manageDomainTool,
|
|
manageDepositTool,
|
|
manageServerTool,
|
|
manageTroubleshootTool,
|
|
suggestDomainsTool,
|
|
redditSearchTool,
|
|
];
|
|
|
|
// Tool categories for dynamic loading (auto-generated from tool definitions)
|
|
export const TOOL_CATEGORIES: Record<string, string[]> = {
|
|
domain: [manageDomainTool.function.name, suggestDomainsTool.function.name],
|
|
deposit: [manageDepositTool.function.name],
|
|
server: [manageServerTool.function.name],
|
|
troubleshoot: [manageTroubleshootTool.function.name],
|
|
weather: [weatherTool.function.name],
|
|
search: [searchWebTool.function.name, lookupDocsTool.function.name],
|
|
reddit: [redditSearchTool.function.name],
|
|
utility: [getCurrentTimeTool.function.name, calculateTool.function.name],
|
|
};
|
|
|
|
// Category detection patterns
|
|
export const CATEGORY_PATTERNS: Record<string, RegExp> = {
|
|
domain: /도메인|네임서버|whois|dns|tld|등록|\.com|\.net|\.io|\.kr|\.org/i,
|
|
deposit: /입금|충전|잔액|계좌|예치금|송금|돈/i,
|
|
server: /서버|VPS|클라우드|호스팅|인스턴스|linode|vultr/i,
|
|
troubleshoot: /문제|에러|오류|안[돼되]|느려|트러블|장애|버그|실패|안\s*됨/i,
|
|
weather: /날씨|기온|비|눈|맑|흐림|더워|추워/i,
|
|
search: /검색|찾아|뭐야|뉴스|최신/i,
|
|
reddit: /레딧|reddit|서브레딧|subreddit/i,
|
|
};
|
|
|
|
// Message-based tool selection
|
|
export function selectToolsForMessage(message: string): typeof tools {
|
|
const selectedCategories = new Set<string>(['utility']); // 항상 포함
|
|
|
|
for (const [category, pattern] of Object.entries(CATEGORY_PATTERNS)) {
|
|
if (pattern.test(message)) {
|
|
selectedCategories.add(category);
|
|
}
|
|
}
|
|
|
|
// 패턴 매칭 없으면 전체 도구 사용 (폴백)
|
|
if (selectedCategories.size === 1) { // utility만 있으면 폴백
|
|
logger.info('패턴 매칭 없음 → 전체 도구 사용');
|
|
return tools;
|
|
}
|
|
|
|
const selectedNames = new Set(
|
|
[...selectedCategories].flatMap(cat => TOOL_CATEGORIES[cat] || [])
|
|
);
|
|
|
|
const selectedTools = tools.filter(t => selectedNames.has(t.function.name));
|
|
|
|
logger.info('도구 선택 완료', {
|
|
message,
|
|
categories: [...selectedCategories].join(', '),
|
|
selectedTools: selectedTools.map(t => t.function.name).join(', ')
|
|
});
|
|
|
|
return selectedTools;
|
|
}
|
|
|
|
// Tool execution dispatcher with validation
|
|
export async function executeTool(
|
|
name: string,
|
|
args: Record<string, unknown>,
|
|
env?: Env,
|
|
telegramUserId?: string,
|
|
db?: D1Database
|
|
): Promise<string> {
|
|
try {
|
|
switch (name) {
|
|
case 'get_weather': {
|
|
const result = GetWeatherArgsSchema.safeParse(args);
|
|
if (!result.success) {
|
|
logger.error('Invalid weather args', new Error(result.error.message), { args });
|
|
return `❌ Invalid arguments: ${result.error.issues.map(e => e.message).join(', ')}`;
|
|
}
|
|
return executeWeather(result.data, env);
|
|
}
|
|
|
|
case 'search_web': {
|
|
const result = SearchWebArgsSchema.safeParse(args);
|
|
if (!result.success) {
|
|
logger.error('Invalid search args', new Error(result.error.message), { args });
|
|
return `❌ Invalid arguments: ${result.error.issues.map(e => e.message).join(', ')}`;
|
|
}
|
|
return executeSearchWeb(result.data, env);
|
|
}
|
|
|
|
case 'lookup_docs': {
|
|
const result = LookupDocsArgsSchema.safeParse(args);
|
|
if (!result.success) {
|
|
logger.error('Invalid lookup_docs args', new Error(result.error.message), { args });
|
|
return `❌ Invalid arguments: ${result.error.issues.map(e => e.message).join(', ')}`;
|
|
}
|
|
return executeLookupDocs(result.data, env);
|
|
}
|
|
|
|
case 'get_current_time': {
|
|
const result = GetCurrentTimeArgsSchema.safeParse(args);
|
|
if (!result.success) {
|
|
logger.error('Invalid time args', new Error(result.error.message), { args });
|
|
return `❌ Invalid arguments: ${result.error.issues.map(e => e.message).join(', ')}`;
|
|
}
|
|
return executeGetCurrentTime(result.data);
|
|
}
|
|
|
|
case 'calculate': {
|
|
const result = CalculateArgsSchema.safeParse(args);
|
|
if (!result.success) {
|
|
logger.error('Invalid calculate args', new Error(result.error.message), { args });
|
|
return `❌ Invalid arguments: ${result.error.issues.map(e => e.message).join(', ')}`;
|
|
}
|
|
return executeCalculate(result.data);
|
|
}
|
|
|
|
case 'manage_domain': {
|
|
const result = ManageDomainArgsSchema.safeParse(args);
|
|
if (!result.success) {
|
|
logger.error('Invalid domain args', new Error(result.error.message), { args });
|
|
return `❌ Invalid arguments: ${result.error.issues.map(e => e.message).join(', ')}`;
|
|
}
|
|
return executeManageDomain(result.data, env, telegramUserId, db);
|
|
}
|
|
|
|
case 'suggest_domains': {
|
|
const result = SuggestDomainsArgsSchema.safeParse(args);
|
|
if (!result.success) {
|
|
logger.error('Invalid suggest_domains args', new Error(result.error.message), { args });
|
|
return `❌ Invalid arguments: ${result.error.issues.map(e => e.message).join(', ')}`;
|
|
}
|
|
return executeSuggestDomains(result.data, env);
|
|
}
|
|
|
|
case 'manage_deposit': {
|
|
const result = ManageDepositArgsSchema.safeParse(args);
|
|
if (!result.success) {
|
|
logger.error('Invalid deposit args', new Error(result.error.message), { args });
|
|
return `❌ Invalid arguments: ${result.error.issues.map(e => e.message).join(', ')}`;
|
|
}
|
|
return executeManageDeposit(result.data, env, telegramUserId, db);
|
|
}
|
|
|
|
case 'manage_server': {
|
|
const result = ManageServerArgsSchema.safeParse(args);
|
|
if (!result.success) {
|
|
logger.error('Invalid server args', new Error(result.error.message), { args });
|
|
return `❌ Invalid arguments: ${result.error.issues.map(e => e.message).join(', ')}`;
|
|
}
|
|
return executeManageServer(result.data, env, telegramUserId);
|
|
}
|
|
|
|
case 'search_reddit': {
|
|
const result = RedditSearchArgsSchema.safeParse(args);
|
|
if (!result.success) {
|
|
logger.error('Invalid reddit args', new Error(result.error.message), { args });
|
|
return `❌ Invalid arguments: ${result.error.issues.map(e => e.message).join(', ')}`;
|
|
}
|
|
return executeRedditSearch(result.data, env);
|
|
}
|
|
|
|
case 'manage_troubleshoot': {
|
|
const result = ManageTroubleshootArgsSchema.safeParse(args);
|
|
if (!result.success) {
|
|
logger.error('Invalid troubleshoot args', new Error(result.error.message), { args });
|
|
return `❌ Invalid arguments: ${result.error.issues.map(e => e.message).join(', ')}`;
|
|
}
|
|
return executeManageTroubleshoot(result.data, env, telegramUserId);
|
|
}
|
|
|
|
default:
|
|
return `알 수 없는 도구: ${name}`;
|
|
}
|
|
} catch (error) {
|
|
logger.error('Tool execution error', error as Error, { name, args });
|
|
return `⚠️ 도구 실행 중 오류가 발생했습니다.`;
|
|
}
|
|
}
|