feat: implement processDomainConsultation main handler
- Add full conversation flow with session management - Handle tool call execution - Support __PASSTHROUGH__ and __SESSION_END__ markers - Add hasDomainSession helper for routing - Export executeDomainAction from domain-tool.ts Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
234
docs/plans/2026-02-05-agent-refactoring-design.md
Normal file
234
docs/plans/2026-02-05-agent-refactoring-design.md
Normal file
@@ -0,0 +1,234 @@
|
|||||||
|
# 도메인/예치금 에이전트 리팩토링 설계
|
||||||
|
|
||||||
|
> 작성일: 2026-02-05
|
||||||
|
> 상태: 설계 완료, 구현 대기
|
||||||
|
|
||||||
|
## 개요
|
||||||
|
|
||||||
|
기존 직접 코드 처리 방식의 `domain-tool.ts`와 `deposit-agent.ts`를 `server-agent.ts` 패턴의 세션 기반 AI 에이전트로 리팩토링한다.
|
||||||
|
|
||||||
|
## 요구사항
|
||||||
|
|
||||||
|
| 항목 | 도메인 에이전트 | 예치금 에이전트 |
|
||||||
|
|------|----------------|----------------|
|
||||||
|
| **트리거** | 모든 도메인 요청 | 모든 예치금 요청 |
|
||||||
|
| **의도 파악** | AI가 판단 | AI가 판단 |
|
||||||
|
| **세션** | 작업 단위 (추천/등록/NS변경) | 입금 신고만 |
|
||||||
|
| **즉시 응답** | 가격, WHOIS, 목록 조회 | 잔액, 내역 조회 |
|
||||||
|
| **리팩토링** | domain-tool.ts 통합 | deposit-agent.ts 통합 |
|
||||||
|
|
||||||
|
## 아키텍처
|
||||||
|
|
||||||
|
```
|
||||||
|
메인 AI (openai-service.ts)
|
||||||
|
│
|
||||||
|
├─ 도메인 키워드 감지 → Domain Agent (domain-agent.ts)
|
||||||
|
│ ├─ 세션 있음? → 세션 컨텍스트로 처리
|
||||||
|
│ ├─ 세션 필요? → 세션 생성 후 상담
|
||||||
|
│ └─ 즉시 응답 가능? → 바로 실행
|
||||||
|
│
|
||||||
|
└─ 예치금 키워드 감지 → Deposit Agent (deposit-agent.ts)
|
||||||
|
├─ 세션 있음? → 입금 신고 흐름 계속
|
||||||
|
├─ 입금 신고? → 세션 생성 후 정보 수집
|
||||||
|
└─ 조회 요청? → 바로 실행
|
||||||
|
|
||||||
|
__PASSTHROUGH__: 무관한 메시지는 메인 AI로 반환
|
||||||
|
```
|
||||||
|
|
||||||
|
## 도메인 에이전트 상세
|
||||||
|
|
||||||
|
### 세션 상태
|
||||||
|
```typescript
|
||||||
|
type DomainSessionStatus =
|
||||||
|
| 'gathering' // 정보 수집 중 (추천용 키워드, 용도)
|
||||||
|
| 'suggesting' // 추천 결과 표시 중
|
||||||
|
| 'confirming' // 등록 확인 대기
|
||||||
|
| 'setting_ns' // 네임서버 변경 확인 대기
|
||||||
|
| 'completed'; // 완료
|
||||||
|
```
|
||||||
|
|
||||||
|
### 작업별 흐름
|
||||||
|
|
||||||
|
| 작업 | 세션 | 흐름 |
|
||||||
|
|------|------|------|
|
||||||
|
| **추천** | ✅ | gathering → suggesting → confirming → completed |
|
||||||
|
| **등록** | ✅ | confirming → completed (잔액 확인 → 결제) |
|
||||||
|
| **NS 변경** | ✅ | setting_ns → completed (위험 작업이라 확인) |
|
||||||
|
| **가격 조회** | ❌ | 즉시 응답 |
|
||||||
|
| **WHOIS** | ❌ | 즉시 응답 |
|
||||||
|
| **내 도메인 목록** | ❌ | 즉시 응답 |
|
||||||
|
| **도메인 정보** | ❌ | 즉시 응답 |
|
||||||
|
|
||||||
|
### AI 페르소나
|
||||||
|
```
|
||||||
|
"10년 경력의 도메인 컨설턴트. 브랜딩, SEO, 가격 대비 가치를 고려한 조언 제공.
|
||||||
|
불필요한 프리미엄 도메인 추천 자제, 실용적인 선택 유도."
|
||||||
|
```
|
||||||
|
|
||||||
|
### 전용 도구
|
||||||
|
- `check_domain`: 가용성 + 가격 확인
|
||||||
|
- `search_suggestions`: 키워드 기반 도메인 추천
|
||||||
|
- `get_whois`: WHOIS 조회
|
||||||
|
- `get_price`: TLD별 가격 조회
|
||||||
|
- `register_domain`: 도메인 등록 (잔액 차감)
|
||||||
|
- `set_nameservers`: 네임서버 변경
|
||||||
|
|
||||||
|
## 예치금 에이전트 상세
|
||||||
|
|
||||||
|
### 세션 상태
|
||||||
|
```typescript
|
||||||
|
type DepositSessionStatus =
|
||||||
|
| 'collecting_amount' // 금액 수집 중
|
||||||
|
| 'collecting_name' // 입금자명 수집 중
|
||||||
|
| 'confirming' // 입금 신고 확인 대기
|
||||||
|
| 'completed'; // 완료
|
||||||
|
```
|
||||||
|
|
||||||
|
### 작업별 흐름
|
||||||
|
|
||||||
|
| 작업 | 세션 | 흐름 |
|
||||||
|
|------|------|------|
|
||||||
|
| **입금 신고** | ✅ | collecting_amount → collecting_name → confirming → completed |
|
||||||
|
| **잔액 조회** | ❌ | 즉시 응답 |
|
||||||
|
| **계좌 안내** | ❌ | 즉시 응답 |
|
||||||
|
| **거래 내역** | ❌ | 즉시 응답 |
|
||||||
|
| **거래 취소** | ❌ | 즉시 응답 (본인 pending만) |
|
||||||
|
| **관리자 기능** | ❌ | 즉시 응답 (pending/confirm/reject) |
|
||||||
|
|
||||||
|
### 스마트 파싱
|
||||||
|
```
|
||||||
|
"홍길동 5만원 입금" → 금액 + 입금자명 한번에 파싱 → confirming 직행
|
||||||
|
"충전할게" → "얼마를 충전하시겠어요?" → collecting_amount
|
||||||
|
"3만원" → "입금자명을 알려주세요" → collecting_name
|
||||||
|
"홍길동" → confirming → 자동매칭 시도
|
||||||
|
```
|
||||||
|
|
||||||
|
### AI 페르소나
|
||||||
|
```
|
||||||
|
"친절한 금융 상담사. 입금 절차를 명확하게 안내하고,
|
||||||
|
자동 매칭 결과를 즉시 알려줌. 오입금 방지를 위해 확인 절차 진행."
|
||||||
|
```
|
||||||
|
|
||||||
|
### 전용 도구
|
||||||
|
- `get_balance`: 잔액 조회
|
||||||
|
- `get_account_info`: 계좌 안내
|
||||||
|
- `request_deposit`: 입금 신고 및 자동 매칭
|
||||||
|
- `get_transactions`: 거래 내역
|
||||||
|
- `cancel_transaction`: 대기 중 거래 취소
|
||||||
|
|
||||||
|
## 데이터베이스 스키마
|
||||||
|
|
||||||
|
### domain_sessions
|
||||||
|
```sql
|
||||||
|
CREATE TABLE domain_sessions (
|
||||||
|
user_id TEXT PRIMARY KEY,
|
||||||
|
status TEXT NOT NULL,
|
||||||
|
collected_info TEXT,
|
||||||
|
target_domain TEXT,
|
||||||
|
messages TEXT,
|
||||||
|
created_at INTEGER NOT NULL,
|
||||||
|
updated_at INTEGER NOT NULL,
|
||||||
|
expires_at INTEGER NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_domain_sessions_expires ON domain_sessions(expires_at);
|
||||||
|
```
|
||||||
|
|
||||||
|
### deposit_sessions
|
||||||
|
```sql
|
||||||
|
CREATE TABLE deposit_sessions (
|
||||||
|
user_id TEXT PRIMARY KEY,
|
||||||
|
status TEXT NOT NULL,
|
||||||
|
collected_info TEXT,
|
||||||
|
messages TEXT,
|
||||||
|
created_at INTEGER NOT NULL,
|
||||||
|
updated_at INTEGER NOT NULL,
|
||||||
|
expires_at INTEGER NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_deposit_sessions_expires ON deposit_sessions(expires_at);
|
||||||
|
```
|
||||||
|
|
||||||
|
## 파일 구조 변경
|
||||||
|
|
||||||
|
### Before
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├── openai-service.ts
|
||||||
|
├── deposit-agent.ts
|
||||||
|
├── server-agent.ts
|
||||||
|
├── tools/
|
||||||
|
│ ├── domain-tool.ts
|
||||||
|
│ ├── deposit-tool.ts
|
||||||
|
│ ├── server-tool.ts
|
||||||
|
│ └── ...
|
||||||
|
```
|
||||||
|
|
||||||
|
### After
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├── openai-service.ts
|
||||||
|
├── agents/
|
||||||
|
│ ├── domain-agent.ts
|
||||||
|
│ ├── deposit-agent.ts
|
||||||
|
│ └── server-agent.ts
|
||||||
|
├── tools/
|
||||||
|
│ ├── search-tool.ts
|
||||||
|
│ ├── weather-tool.ts
|
||||||
|
│ └── utility-tools.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
## 구현 순서
|
||||||
|
|
||||||
|
### Phase 1: 기반 작업
|
||||||
|
1. `migrations/006_add_agent_sessions.sql` 작성
|
||||||
|
2. `src/agents/` 디렉토리 생성
|
||||||
|
3. `server-agent.ts` → `src/agents/`로 이동
|
||||||
|
4. 타입 정의 (`types.ts`에 세션 타입 추가)
|
||||||
|
|
||||||
|
### Phase 2: 도메인 에이전트
|
||||||
|
1. `src/agents/domain-agent.ts` 생성
|
||||||
|
2. 기존 `domain-tool.ts` 로직 통합
|
||||||
|
3. `openai-service.ts` 수정
|
||||||
|
4. `tools/domain-tool.ts` 삭제
|
||||||
|
|
||||||
|
### Phase 3: 예치금 에이전트
|
||||||
|
1. `src/agents/deposit-agent.ts` 리팩토링
|
||||||
|
2. `openai-service.ts` 수정
|
||||||
|
3. `tools/deposit-tool.ts` 삭제
|
||||||
|
|
||||||
|
### Phase 4: 정리
|
||||||
|
1. `tools/server-tool.ts` → `agents/` 통합
|
||||||
|
2. import 경로 정리
|
||||||
|
3. 테스트 및 문서 업데이트
|
||||||
|
|
||||||
|
## 테스트 계획
|
||||||
|
|
||||||
|
### 도메인 에이전트
|
||||||
|
| 시나리오 | 예상 결과 |
|
||||||
|
|----------|----------|
|
||||||
|
| "도메인 추천해줘" | 세션 시작 → 용도 질문 |
|
||||||
|
| "커피숍 도메인" | 키워드 수집 → 추천 결과 |
|
||||||
|
| "1번 등록" | 잔액 확인 → 등록 확인 |
|
||||||
|
| ".com 가격" | 즉시 응답 (세션 없음) |
|
||||||
|
| "example.com WHOIS" | 즉시 응답 (세션 없음) |
|
||||||
|
| 세션 중 "날씨 알려줘" | `__PASSTHROUGH__` → 메인 AI |
|
||||||
|
|
||||||
|
### 예치금 에이전트
|
||||||
|
| 시나리오 | 예상 결과 |
|
||||||
|
|----------|----------|
|
||||||
|
| "충전할게" | 세션 시작 → 금액 질문 |
|
||||||
|
| "5만원" | 입금자명 질문 |
|
||||||
|
| "홍길동" | 확인 → 자동매칭 시도 |
|
||||||
|
| "홍길동 5만원 입금" | 한번에 파싱 → 바로 확인 |
|
||||||
|
| "잔액" | 즉시 응답 (세션 없음) |
|
||||||
|
| "내역" | 즉시 응답 (세션 없음) |
|
||||||
|
|
||||||
|
## 위험 요소 및 대응
|
||||||
|
|
||||||
|
| 위험 | 대응 |
|
||||||
|
|------|------|
|
||||||
|
| 기존 기능 회귀 | 기존 테스트 유지 + 세션 테스트 추가 |
|
||||||
|
| 세션 충돌 | user_id 기반 단일 세션 보장 |
|
||||||
|
| AI 오판단 | `__PASSTHROUGH__` 폴백, 명확한 시스템 프롬프트 |
|
||||||
|
| 마이그레이션 실패 | 로컬 테스트 후 프로덕션 적용 |
|
||||||
1093
docs/plans/2026-02-05-agent-refactoring-implementation.md
Normal file
1093
docs/plans/2026-02-05-agent-refactoring-implementation.md
Normal file
File diff suppressed because it is too large
Load Diff
@@ -593,10 +593,93 @@ export async function processDomainConsultation(
|
|||||||
userMessage: string,
|
userMessage: string,
|
||||||
env: Env
|
env: Env
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
// TODO: Implement in Task 8
|
const startTime = Date.now();
|
||||||
logger.info('도메인 상담 처리 요청 (미구현)', {
|
logger.info('도메인 상담 시작', { userId, message: userMessage.substring(0, 100) });
|
||||||
userId,
|
|
||||||
message: userMessage.slice(0, 50),
|
try {
|
||||||
});
|
// 1. Check for existing session
|
||||||
|
let session = await getDomainSession(db, userId);
|
||||||
|
|
||||||
|
// 2. Create new session if none exists and message seems domain-related
|
||||||
|
// (For first call, we always try to process - AI will return __PASSTHROUGH__ if not relevant)
|
||||||
|
if (!session) {
|
||||||
|
session = createDomainSession(userId, 'gathering');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Add user message to session
|
||||||
|
addMessageToSession(session, 'user', userMessage);
|
||||||
|
|
||||||
|
// 4. Call AI to get response and possible tool calls
|
||||||
|
const aiResult = await callDomainExpertAI(session, userMessage, env);
|
||||||
|
|
||||||
|
// 5. Handle __PASSTHROUGH__ - not domain related
|
||||||
|
if (aiResult.response === '__PASSTHROUGH__' || aiResult.response.includes('__PASSTHROUGH__')) {
|
||||||
|
logger.info('도메인 상담 패스스루', { userId });
|
||||||
|
// Don't save session if passthrough
|
||||||
return '__PASSTHROUGH__';
|
return '__PASSTHROUGH__';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 6. Execute tool calls if any
|
||||||
|
let toolResults: string[] = [];
|
||||||
|
if (aiResult.toolCalls && aiResult.toolCalls.length > 0) {
|
||||||
|
for (const toolCall of aiResult.toolCalls) {
|
||||||
|
const result = await executeDomainToolCall(
|
||||||
|
toolCall.name,
|
||||||
|
toolCall.arguments,
|
||||||
|
env,
|
||||||
|
userId,
|
||||||
|
db
|
||||||
|
);
|
||||||
|
toolResults.push(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7. Build final response
|
||||||
|
let finalResponse = aiResult.response;
|
||||||
|
if (toolResults.length > 0) {
|
||||||
|
// If we have tool results, include them in the response
|
||||||
|
// The AI response might be a summary, but tool results have the actual data
|
||||||
|
finalResponse = toolResults.join('\n\n');
|
||||||
|
if (aiResult.response && !aiResult.response.includes('__SESSION_END__')) {
|
||||||
|
finalResponse = aiResult.response + '\n\n' + finalResponse;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 8. Handle __SESSION_END__ - session complete
|
||||||
|
if (finalResponse.includes('__SESSION_END__')) {
|
||||||
|
logger.info('도메인 상담 세션 종료', { userId });
|
||||||
|
await deleteDomainSession(db, userId);
|
||||||
|
finalResponse = finalResponse.replace('__SESSION_END__', '').trim();
|
||||||
|
return finalResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 9. Add assistant response to session and save
|
||||||
|
addMessageToSession(session, 'assistant', finalResponse);
|
||||||
|
session.updated_at = Date.now();
|
||||||
|
await saveDomainSession(db, session);
|
||||||
|
|
||||||
|
logger.info('도메인 상담 완료', {
|
||||||
|
userId,
|
||||||
|
duration: Date.now() - startTime,
|
||||||
|
hasToolCalls: aiResult.toolCalls?.length || 0
|
||||||
|
});
|
||||||
|
|
||||||
|
return finalResponse;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('도메인 상담 오류', error as Error, { userId });
|
||||||
|
return '죄송합니다. 도메인 상담 중 오류가 발생했습니다. 잠시 후 다시 시도해주세요.';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 도메인 세션 존재 여부 확인 (라우팅용)
|
||||||
|
*
|
||||||
|
* @param db - D1 Database
|
||||||
|
* @param userId - Telegram User ID
|
||||||
|
* @returns true if active session exists, false otherwise
|
||||||
|
*/
|
||||||
|
export async function hasDomainSession(db: D1Database, userId: string): Promise<boolean> {
|
||||||
|
const session = await getDomainSession(db, userId);
|
||||||
|
return session !== null && !isSessionExpired(session);
|
||||||
|
}
|
||||||
|
|||||||
@@ -470,7 +470,7 @@ async function callNamecheapApi(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 도메인 작업 직접 실행 (Agent 없이 코드로 처리)
|
// 도메인 작업 직접 실행 (Agent 없이 코드로 처리)
|
||||||
async function executeDomainAction(
|
export async function executeDomainAction(
|
||||||
action: string,
|
action: string,
|
||||||
args: { domain?: string; nameservers?: string[]; tld?: string },
|
args: { domain?: string; nameservers?: string[]; tld?: string },
|
||||||
allowedDomains: string[],
|
allowedDomains: string[],
|
||||||
|
|||||||
Reference in New Issue
Block a user