Files
telegram-bot-workers/src/tools/index.ts
kappa 860e36a688 feat: add memory system and troubleshoot agent
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>
2026-01-27 14:28:22 +09:00

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 `⚠️ 도구 실행 중 오류가 발생했습니다.`;
}
}