companionbot 0.11.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ai/claude.js +79 -65
- package/dist/cli/main.js +16 -1
- package/dist/config/constants.js +123 -49
- package/dist/config/index.js +39 -0
- package/dist/health/index.js +3 -1
- package/dist/memory/benchmark.js +114 -0
- package/dist/memory/embeddings.js +67 -5
- package/dist/memory/ftsIndex.js +288 -68
- package/dist/memory/ftsIndex.optimized.js +368 -0
- package/dist/memory/ftsIndex.original.js +148 -0
- package/dist/memory/hybridSearch.js +147 -41
- package/dist/memory/hybridSearch.optimized.js +209 -0
- package/dist/memory/hybridSearch.original.js +146 -0
- package/dist/memory/index.js +2 -2
- package/dist/memory/vectorStore.js +333 -160
- package/dist/memory/vectorStore.optimized.js +492 -0
- package/dist/memory/vectorStore.original.js +350 -0
- package/dist/session/state.js +5 -3
- package/dist/telegram/bot.js +42 -12
- package/dist/telegram/handlers/commands.js +14 -44
- package/dist/telegram/handlers/messages.js +241 -65
- package/dist/telegram/utils/cache.js +35 -0
- package/dist/telegram/utils/index.js +1 -1
- package/dist/telegram/utils/prompt.js +126 -189
- package/dist/tools/compress.js +141 -0
- package/dist/tools/definitions.js +313 -0
- package/dist/tools/index.js +11 -2
- package/dist/tools/timeout.js +78 -0
- package/dist/utils/index.js +1 -0
- package/dist/utils/retry.js +288 -0
- package/dist/warmup.js +120 -0
- package/package.json +1 -1
- package/templates/AGENTS.md +27 -184
package/dist/ai/claude.js
CHANGED
|
@@ -1,48 +1,21 @@
|
|
|
1
1
|
import Anthropic, { APIError } from "@anthropic-ai/sdk";
|
|
2
2
|
import { tools, executeTool } from "../tools/index.js";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
? parseInt(retryAfter) * 1000
|
|
20
|
-
: BASE_RETRY_DELAY_MS * Math.pow(2, attempt);
|
|
21
|
-
console.log(`[RateLimit] 429 received, waiting ${delay}ms (attempt ${attempt + 1}/${retries})`);
|
|
22
|
-
await sleep(delay);
|
|
23
|
-
continue;
|
|
24
|
-
}
|
|
25
|
-
// 서버 에러 (500+)
|
|
26
|
-
if (error.status >= 500) {
|
|
27
|
-
const delay = BASE_RETRY_DELAY_MS * Math.pow(2, attempt);
|
|
28
|
-
console.log(`[ServerError] ${error.status}, waiting ${delay}ms (attempt ${attempt + 1}/${retries})`);
|
|
29
|
-
await sleep(delay);
|
|
30
|
-
continue;
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
// 일반 Error 처리
|
|
34
|
-
if (error instanceof Error) {
|
|
35
|
-
lastError = error;
|
|
36
|
-
}
|
|
37
|
-
else {
|
|
38
|
-
lastError = new Error(String(error));
|
|
39
|
-
}
|
|
40
|
-
// 다른 에러는 바로 throw
|
|
41
|
-
throw error;
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
throw lastError;
|
|
45
|
-
}
|
|
3
|
+
import { MAX_TOOL_ITERATIONS, TOOL_INPUT_SUMMARY_LENGTH, TOOL_OUTPUT_SUMMARY_LENGTH, } from "../utils/constants.js";
|
|
4
|
+
import { withRetry, withTimeout, } from "../utils/retry.js";
|
|
5
|
+
import { getToolTimeout } from "../tools/timeout.js";
|
|
6
|
+
import { compressToolResult } from "../tools/compress.js";
|
|
7
|
+
// API 호출 타임아웃 (2분) - Claude의 긴 응답 시간 고려
|
|
8
|
+
const API_TIMEOUT_MS = 120000;
|
|
9
|
+
// 재시도 설정
|
|
10
|
+
const API_RETRY_OPTIONS = {
|
|
11
|
+
maxRetries: 3,
|
|
12
|
+
initialDelayMs: 1000,
|
|
13
|
+
maxDelayMs: 30000,
|
|
14
|
+
onRetry: (attempt, error, delay) => {
|
|
15
|
+
const errMsg = error instanceof Error ? error.message : String(error);
|
|
16
|
+
console.log(`[API Retry] Attempt ${attempt}, waiting ${delay}ms: ${errMsg.slice(0, 100)}`);
|
|
17
|
+
},
|
|
18
|
+
};
|
|
46
19
|
let anthropic = null;
|
|
47
20
|
function getClient() {
|
|
48
21
|
if (!anthropic) {
|
|
@@ -163,31 +136,57 @@ export async function chat(messages, systemPrompt, modelId = "sonnet", thinkingL
|
|
|
163
136
|
return params;
|
|
164
137
|
};
|
|
165
138
|
let response;
|
|
166
|
-
response = await withRetry(() => client.messages.create(buildRequestParams()));
|
|
139
|
+
response = await withRetry(() => withTimeout(() => client.messages.create(buildRequestParams()), API_TIMEOUT_MS, "API 응답 시간 초과"), API_RETRY_OPTIONS);
|
|
167
140
|
// Tool use 루프 - Claude가 도구 사용을 멈출 때까지 반복
|
|
168
141
|
let iterations = 0;
|
|
169
142
|
while (response.stop_reason === "tool_use" && iterations < MAX_TOOL_ITERATIONS) {
|
|
170
143
|
iterations++;
|
|
171
144
|
const toolUseBlocks = response.content.filter((block) => block.type === "tool_use");
|
|
172
|
-
// 도구 실행
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
145
|
+
// 도구 병렬 실행 (성능 최적화)
|
|
146
|
+
console.log(`[Tool] Executing ${toolUseBlocks.length} tool(s) in parallel`);
|
|
147
|
+
const toolExecutions = await Promise.all(toolUseBlocks.map(async (toolUse) => {
|
|
148
|
+
const startTime = Date.now();
|
|
149
|
+
console.log(`[Tool] ${toolUse.name}:`, JSON.stringify(toolUse.input).slice(0, 200));
|
|
150
|
+
try {
|
|
151
|
+
// 도구별 타임아웃 적용
|
|
152
|
+
const timeout = getToolTimeout(toolUse.name);
|
|
153
|
+
const result = await Promise.race([
|
|
154
|
+
executeTool(toolUse.name, toolUse.input),
|
|
155
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error(`Tool ${toolUse.name} timed out after ${timeout}ms`)), timeout)),
|
|
156
|
+
]);
|
|
157
|
+
const elapsed = Date.now() - startTime;
|
|
158
|
+
console.log(`[Tool] ${toolUse.name} completed in ${elapsed}ms`);
|
|
159
|
+
// 스마트 결과 압축
|
|
160
|
+
const compressedResult = compressToolResult(toolUse.name, result);
|
|
161
|
+
return {
|
|
162
|
+
toolUse,
|
|
163
|
+
result: compressedResult,
|
|
164
|
+
success: true,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
catch (error) {
|
|
168
|
+
const elapsed = Date.now() - startTime;
|
|
169
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
170
|
+
console.error(`[Tool] ${toolUse.name} failed after ${elapsed}ms:`, errorMsg);
|
|
171
|
+
return {
|
|
172
|
+
toolUse,
|
|
173
|
+
result: `Error: ${errorMsg}`,
|
|
174
|
+
success: false,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
}));
|
|
178
|
+
// 결과 수집
|
|
179
|
+
const toolResults = toolExecutions.map((exec) => ({
|
|
180
|
+
type: "tool_result",
|
|
181
|
+
tool_use_id: exec.toolUse.id,
|
|
182
|
+
content: exec.result,
|
|
183
|
+
}));
|
|
184
|
+
// 도구 사용 기록
|
|
185
|
+
for (const exec of toolExecutions) {
|
|
187
186
|
toolsUsed.push({
|
|
188
|
-
name: toolUse.name,
|
|
189
|
-
input: JSON.stringify(toolUse.input).slice(0, TOOL_INPUT_SUMMARY_LENGTH),
|
|
190
|
-
output:
|
|
187
|
+
name: exec.toolUse.name,
|
|
188
|
+
input: JSON.stringify(exec.toolUse.input).slice(0, TOOL_INPUT_SUMMARY_LENGTH),
|
|
189
|
+
output: exec.result.slice(0, TOOL_OUTPUT_SUMMARY_LENGTH),
|
|
191
190
|
});
|
|
192
191
|
}
|
|
193
192
|
// 어시스턴트 메시지와 도구 결과 추가
|
|
@@ -200,7 +199,7 @@ export async function chat(messages, systemPrompt, modelId = "sonnet", thinkingL
|
|
|
200
199
|
content: toolResults,
|
|
201
200
|
});
|
|
202
201
|
// 다음 응답 요청 (도구 루프에서도 thinking 유지)
|
|
203
|
-
response = await withRetry(() => client.messages.create(buildRequestParams()));
|
|
202
|
+
response = await withRetry(() => withTimeout(() => client.messages.create(buildRequestParams()), API_TIMEOUT_MS, "API 응답 시간 초과"), API_RETRY_OPTIONS);
|
|
204
203
|
}
|
|
205
204
|
// 반복 횟수 초과 시 경고
|
|
206
205
|
if (iterations >= MAX_TOOL_ITERATIONS) {
|
|
@@ -226,8 +225,12 @@ export async function chat(messages, systemPrompt, modelId = "sonnet", thinkingL
|
|
|
226
225
|
* 스트리밍 중 에러 발생 시 적절한 에러 메시지를 반환하거나 예외를 전파함
|
|
227
226
|
*/
|
|
228
227
|
export async function chatSmart(messages, systemPrompt, modelId, thinkingLevel = "medium", onChunk) {
|
|
228
|
+
// 콜백 정규화
|
|
229
|
+
const callbacks = typeof onChunk === 'function'
|
|
230
|
+
? { onChunk }
|
|
231
|
+
: (onChunk ?? {});
|
|
229
232
|
// 스트리밍 콜백이 없으면 그냥 일반 chat 사용
|
|
230
|
-
if (!onChunk) {
|
|
233
|
+
if (!callbacks.onChunk) {
|
|
231
234
|
const result = await chat(messages, systemPrompt, modelId, thinkingLevel);
|
|
232
235
|
return { text: result.text, usedTools: result.toolsUsed.length > 0, toolsUsed: result.toolsUsed };
|
|
233
236
|
}
|
|
@@ -279,7 +282,7 @@ export async function chatSmart(messages, systemPrompt, modelId, thinkingLevel =
|
|
|
279
282
|
streamingStarted = true;
|
|
280
283
|
accumulated += text;
|
|
281
284
|
try {
|
|
282
|
-
await onChunk(text, accumulated);
|
|
285
|
+
await callbacks.onChunk(text, accumulated);
|
|
283
286
|
}
|
|
284
287
|
catch (err) {
|
|
285
288
|
// editMessageText 실패 등은 무시하고 계속
|
|
@@ -293,6 +296,17 @@ export async function chatSmart(messages, systemPrompt, modelId, thinkingLevel =
|
|
|
293
296
|
// 주의: chat()은 내부에서 withRetry를 사용하므로 여기서 추가 재시도 불필요
|
|
294
297
|
if (stopReason === "tool_use") {
|
|
295
298
|
console.log("[Stream] Tool use detected, falling back to chat()");
|
|
299
|
+
// 도구 이름 추출하여 콜백 호출
|
|
300
|
+
const toolUseBlocks = finalMessage.content.filter((block) => block.type === "tool_use");
|
|
301
|
+
const toolNames = toolUseBlocks.map(t => t.name);
|
|
302
|
+
if (callbacks.onToolStart && toolNames.length > 0) {
|
|
303
|
+
try {
|
|
304
|
+
await callbacks.onToolStart(toolNames);
|
|
305
|
+
}
|
|
306
|
+
catch (err) {
|
|
307
|
+
console.warn("[Stream] Tool start callback error (ignored):", err);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
296
310
|
const result = await chat(messages, systemPrompt, modelId, thinkingLevel);
|
|
297
311
|
return { text: result.text, usedTools: true, toolsUsed: result.toolsUsed };
|
|
298
312
|
}
|
package/dist/cli/main.js
CHANGED
|
@@ -6,6 +6,7 @@ import { createBot } from "../telegram/bot.js";
|
|
|
6
6
|
import { cleanupHeartbeats } from "../heartbeat/index.js";
|
|
7
7
|
import { cleanupBriefings } from "../briefing/index.js";
|
|
8
8
|
import { cleanupReminders } from "../reminders/index.js";
|
|
9
|
+
import { preloadEmbeddingModel, preloadVectorStore } from "../memory/index.js";
|
|
9
10
|
function createPrompt() {
|
|
10
11
|
return readline.createInterface({
|
|
11
12
|
input: process.stdin,
|
|
@@ -233,7 +234,21 @@ async function main() {
|
|
|
233
234
|
}
|
|
234
235
|
// 4. 환경변수 설정
|
|
235
236
|
process.env.ANTHROPIC_API_KEY = apiKey;
|
|
236
|
-
// 5.
|
|
237
|
+
// 5. 🚀 사전 로딩 (첫 응답 속도 개선)
|
|
238
|
+
console.log(`
|
|
239
|
+
╔═══════════════════════════════════════════════════════════════╗
|
|
240
|
+
║ ⏳ 시스템 사전 로딩... ║
|
|
241
|
+
╚═══════════════════════════════════════════════════════════════╝
|
|
242
|
+
`);
|
|
243
|
+
const preloadStart = Date.now();
|
|
244
|
+
// 임베딩 모델 + 벡터 저장소 병렬 로딩
|
|
245
|
+
await Promise.all([
|
|
246
|
+
preloadEmbeddingModel(),
|
|
247
|
+
preloadVectorStore(),
|
|
248
|
+
]);
|
|
249
|
+
console.log(` ✓ 사전 로딩 완료 (${Date.now() - preloadStart}ms)
|
|
250
|
+
`);
|
|
251
|
+
// 6. 봇 시작
|
|
237
252
|
console.log(`
|
|
238
253
|
╔═══════════════════════════════════════════════════════════════╗
|
|
239
254
|
║ 🚀 봇 시작! ║
|
package/dist/config/constants.js
CHANGED
|
@@ -1,105 +1,179 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 전역 상수 설정
|
|
3
|
-
*
|
|
4
3
|
* 하드코딩된 매직 넘버들을 한 곳에서 관리
|
|
5
|
-
* 환경변수로 오버라이드 가능
|
|
6
4
|
*/
|
|
7
|
-
// 환경변수에서 숫자 읽기 (기본값 사용)
|
|
8
|
-
function envInt(key, defaultValue) {
|
|
9
|
-
const val = process.env[key];
|
|
10
|
-
if (!val)
|
|
11
|
-
return defaultValue;
|
|
12
|
-
const parsed = parseInt(val, 10);
|
|
13
|
-
return isNaN(parsed) ? defaultValue : parsed;
|
|
14
|
-
}
|
|
15
|
-
function envFloat(key, defaultValue) {
|
|
16
|
-
const val = process.env[key];
|
|
17
|
-
if (!val)
|
|
18
|
-
return defaultValue;
|
|
19
|
-
const parsed = parseFloat(val);
|
|
20
|
-
return isNaN(parsed) ? defaultValue : parsed;
|
|
21
|
-
}
|
|
22
5
|
// ============================================
|
|
23
6
|
// 세션 관련 설정
|
|
24
7
|
// ============================================
|
|
25
8
|
export const SESSION = {
|
|
26
9
|
/** 최대 동시 세션 수 (LRU 정리) */
|
|
27
|
-
MAX_SESSIONS:
|
|
28
|
-
/** 세션 TTL (밀리초) -
|
|
29
|
-
TTL_MS:
|
|
10
|
+
MAX_SESSIONS: 100,
|
|
11
|
+
/** 세션 TTL (밀리초) - 24시간 */
|
|
12
|
+
TTL_MS: 24 * 60 * 60 * 1000,
|
|
30
13
|
/** 메모리에 로드할 최대 히스토리 메시지 수 */
|
|
31
|
-
MAX_HISTORY_LOAD:
|
|
14
|
+
MAX_HISTORY_LOAD: 50,
|
|
32
15
|
};
|
|
33
16
|
// ============================================
|
|
34
17
|
// 토큰/컨텍스트 관련 설정
|
|
35
18
|
// ============================================
|
|
36
19
|
export const TOKENS = {
|
|
37
20
|
/** Claude 최대 컨텍스트 토큰 */
|
|
38
|
-
MAX_CONTEXT:
|
|
21
|
+
MAX_CONTEXT: 100000,
|
|
39
22
|
/** 히스토리 토큰 한도 */
|
|
40
|
-
MAX_HISTORY:
|
|
23
|
+
MAX_HISTORY: 40000,
|
|
41
24
|
/** 이 이상이면 자동 요약 시작 */
|
|
42
|
-
SUMMARY_THRESHOLD:
|
|
25
|
+
SUMMARY_THRESHOLD: 25000,
|
|
43
26
|
/** 핀 맥락 최대 토큰 */
|
|
44
|
-
MAX_PINNED:
|
|
27
|
+
MAX_PINNED: 5000,
|
|
45
28
|
/** 자동 압축 시작 비율 (0.35 = 35%) */
|
|
46
|
-
COMPACTION_THRESHOLD:
|
|
29
|
+
COMPACTION_THRESHOLD: 0.35,
|
|
47
30
|
/** compact 스킵 기준 토큰 */
|
|
48
|
-
COMPACT_MIN_TOKENS:
|
|
31
|
+
COMPACT_MIN_TOKENS: 5000,
|
|
49
32
|
};
|
|
50
33
|
// ============================================
|
|
51
34
|
// 메시지 관련 설정
|
|
52
35
|
// ============================================
|
|
53
36
|
export const MESSAGES = {
|
|
54
37
|
/** 트리밍 시 최소 유지할 최근 메시지 수 */
|
|
55
|
-
MIN_RECENT:
|
|
38
|
+
MIN_RECENT: 6,
|
|
56
39
|
/** compact 시 유지할 최근 메시지 수 */
|
|
57
|
-
KEEP_ON_COMPACT:
|
|
40
|
+
KEEP_ON_COMPACT: 4,
|
|
58
41
|
/** 최대 요약 청크 수 */
|
|
59
|
-
MAX_SUMMARY_CHUNKS:
|
|
42
|
+
MAX_SUMMARY_CHUNKS: 3,
|
|
60
43
|
/** 검색 기본 결과 수 */
|
|
61
|
-
SEARCH_LIMIT:
|
|
44
|
+
SEARCH_LIMIT: 10,
|
|
62
45
|
/** 히스토리 로드 기본 limit */
|
|
63
|
-
HISTORY_LOAD_LIMIT:
|
|
46
|
+
HISTORY_LOAD_LIMIT: 100,
|
|
64
47
|
};
|
|
65
48
|
// ============================================
|
|
66
49
|
// 메모리/벡터 저장소 설정
|
|
67
50
|
// ============================================
|
|
68
51
|
export const MEMORY = {
|
|
69
|
-
/** 벡터 캐시 TTL (밀리초) -
|
|
70
|
-
CACHE_TTL_MS:
|
|
52
|
+
/** 벡터 캐시 TTL (밀리초) - 5분 */
|
|
53
|
+
CACHE_TTL_MS: 5 * 60 * 1000,
|
|
71
54
|
/** 최소 청크 길이 (이하는 무시) */
|
|
72
|
-
MIN_CHUNK_LENGTH:
|
|
55
|
+
MIN_CHUNK_LENGTH: 20,
|
|
73
56
|
/** 최대 청크 길이 (초과 시 분할) */
|
|
74
|
-
MAX_CHUNK_LENGTH:
|
|
57
|
+
MAX_CHUNK_LENGTH: 500,
|
|
75
58
|
/** 로드할 최근 메모리 파일 일수 */
|
|
76
|
-
RECENT_DAYS:
|
|
59
|
+
RECENT_DAYS: 30,
|
|
77
60
|
/** 벡터 검색 기본 topK */
|
|
78
|
-
SEARCH_TOP_K:
|
|
61
|
+
SEARCH_TOP_K: 5,
|
|
79
62
|
/** 벡터 검색 최소 유사도 점수 */
|
|
80
|
-
MIN_SIMILARITY:
|
|
63
|
+
MIN_SIMILARITY: 0.3,
|
|
81
64
|
/** /memory 명령어 표시 일수 */
|
|
82
|
-
DISPLAY_DAYS:
|
|
65
|
+
DISPLAY_DAYS: 7,
|
|
83
66
|
/** /memory 최대 표시 길이 */
|
|
84
|
-
MAX_DISPLAY_LENGTH:
|
|
67
|
+
MAX_DISPLAY_LENGTH: 2000,
|
|
85
68
|
};
|
|
86
69
|
// ============================================
|
|
87
70
|
// 텔레그램/UI 관련 설정
|
|
88
71
|
// ============================================
|
|
89
72
|
export const TELEGRAM = {
|
|
90
|
-
/** 스트리밍 업데이트 간격 (밀리초) */
|
|
91
|
-
STREAM_UPDATE_INTERVAL_MS:
|
|
92
|
-
/**
|
|
93
|
-
|
|
73
|
+
/** 스트리밍 업데이트 간격 - 적응형 (밀리초) */
|
|
74
|
+
STREAM_UPDATE_INTERVAL_MS: 500, // 레거시 호환용
|
|
75
|
+
/** 스트리밍 적응형 간격 설정 */
|
|
76
|
+
STREAM_INTERVAL: {
|
|
77
|
+
/** 첫 번째 업데이트 (즉시) */
|
|
78
|
+
FIRST_MS: 0,
|
|
79
|
+
/** 초기 빠른 업데이트 (처음 5회) */
|
|
80
|
+
FAST_MS: 200,
|
|
81
|
+
/** 이후 일반 간격 */
|
|
82
|
+
NORMAL_MS: 400,
|
|
83
|
+
/** 빠른 업데이트 횟수 */
|
|
84
|
+
FAST_COUNT: 5,
|
|
85
|
+
},
|
|
86
|
+
/** 텔레그램 메시지 최대 길이 */
|
|
87
|
+
MAX_MESSAGE_LENGTH: 4096,
|
|
88
|
+
/** 최대 이미지 크기 (바이트) - 10MB */
|
|
89
|
+
MAX_IMAGE_SIZE: 10 * 1024 * 1024,
|
|
94
90
|
/** URL 처리 최대 개수 */
|
|
95
|
-
MAX_URL_FETCH:
|
|
91
|
+
MAX_URL_FETCH: 3,
|
|
96
92
|
/** 캘린더 미리보기 이벤트 수 */
|
|
97
|
-
CALENDAR_PREVIEW_COUNT:
|
|
93
|
+
CALENDAR_PREVIEW_COUNT: 3,
|
|
94
|
+
/** 스트리밍 UI 아이콘 */
|
|
95
|
+
STREAM_ICONS: {
|
|
96
|
+
THINKING: "💭",
|
|
97
|
+
TYPING: "▌",
|
|
98
|
+
TOOL: "🔧",
|
|
99
|
+
DONE: "",
|
|
100
|
+
},
|
|
101
|
+
/** Typing indicator 자동 갱신 간격 (밀리초) - 텔레그램은 5초 후 만료 */
|
|
102
|
+
TYPING_REFRESH_MS: 4000,
|
|
103
|
+
/** 도구별 친화적 상태 메시지 */
|
|
104
|
+
TOOL_STATUS_MESSAGES: {
|
|
105
|
+
// 검색/정보 조회
|
|
106
|
+
web_search: { icon: "🔍", text: "웹에서 검색하는 중", estimate: "5-10초" },
|
|
107
|
+
web_fetch: { icon: "📄", text: "웹페이지 읽는 중", estimate: "3-5초" },
|
|
108
|
+
get_weather: { icon: "🌤️", text: "날씨 확인 중", estimate: "2-3초" },
|
|
109
|
+
memory_search: { icon: "🧠", text: "기억 검색 중", estimate: "1-2초" },
|
|
110
|
+
memory_reindex: { icon: "🧠", text: "기억 재색인 중", estimate: "10-30초" },
|
|
111
|
+
// 파일 작업
|
|
112
|
+
read_file: { icon: "📖", text: "파일 읽는 중", estimate: "1초" },
|
|
113
|
+
write_file: { icon: "✍️", text: "파일 쓰는 중", estimate: "1초" },
|
|
114
|
+
edit_file: { icon: "✏️", text: "파일 수정 중", estimate: "1초" },
|
|
115
|
+
list_directory: { icon: "📁", text: "폴더 살펴보는 중", estimate: "1초" },
|
|
116
|
+
// 명령어 실행
|
|
117
|
+
run_command: { icon: "⚡", text: "명령어 실행 중", estimate: "변동" },
|
|
118
|
+
list_sessions: { icon: "📋", text: "세션 목록 확인 중", estimate: "1초" },
|
|
119
|
+
get_session_log: { icon: "📜", text: "로그 가져오는 중", estimate: "1초" },
|
|
120
|
+
kill_session: { icon: "🛑", text: "세션 종료 중", estimate: "1초" },
|
|
121
|
+
// 일정/리마인더
|
|
122
|
+
get_calendar_events: { icon: "📅", text: "일정 확인 중", estimate: "2-3초" },
|
|
123
|
+
add_calendar_event: { icon: "📅", text: "일정 추가 중", estimate: "2-3초" },
|
|
124
|
+
delete_calendar_event: { icon: "📅", text: "일정 삭제 중", estimate: "2초" },
|
|
125
|
+
set_reminder: { icon: "⏰", text: "알림 설정 중", estimate: "1초" },
|
|
126
|
+
list_reminders: { icon: "⏰", text: "알림 목록 확인 중", estimate: "1초" },
|
|
127
|
+
cancel_reminder: { icon: "⏰", text: "알림 취소 중", estimate: "1초" },
|
|
128
|
+
// 브리핑/하트비트
|
|
129
|
+
control_briefing: { icon: "☀️", text: "브리핑 설정 중", estimate: "1초" },
|
|
130
|
+
send_briefing_now: { icon: "☀️", text: "브리핑 준비 중", estimate: "5-10초" },
|
|
131
|
+
control_heartbeat: { icon: "💓", text: "하트비트 설정 중", estimate: "1초" },
|
|
132
|
+
run_heartbeat_check: { icon: "💓", text: "체크 실행 중", estimate: "3-5초" },
|
|
133
|
+
// 서브에이전트
|
|
134
|
+
spawn_agent: { icon: "🤖", text: "서브에이전트 생성 중", estimate: "2-3초" },
|
|
135
|
+
list_agents: { icon: "🤖", text: "에이전트 목록 확인 중", estimate: "1초" },
|
|
136
|
+
cancel_agent: { icon: "🤖", text: "에이전트 취소 중", estimate: "1초" },
|
|
137
|
+
// Cron
|
|
138
|
+
add_cron: { icon: "🕐", text: "예약 작업 추가 중", estimate: "1초" },
|
|
139
|
+
list_crons: { icon: "🕐", text: "예약 작업 확인 중", estimate: "1초" },
|
|
140
|
+
remove_cron: { icon: "🕐", text: "예약 작업 삭제 중", estimate: "1초" },
|
|
141
|
+
toggle_cron: { icon: "🕐", text: "예약 작업 설정 중", estimate: "1초" },
|
|
142
|
+
run_cron: { icon: "🕐", text: "예약 작업 실행 중", estimate: "변동" },
|
|
143
|
+
// 기타
|
|
144
|
+
change_model: { icon: "🔄", text: "모델 변경 중", estimate: "1초" },
|
|
145
|
+
save_memory: { icon: "💾", text: "기억 저장 중", estimate: "1초" },
|
|
146
|
+
save_persona: { icon: "✨", text: "페르소나 저장 중", estimate: "2초" },
|
|
147
|
+
},
|
|
98
148
|
};
|
|
99
149
|
// ============================================
|
|
100
150
|
// 보안/토큰 관련 설정
|
|
101
151
|
// ============================================
|
|
102
152
|
export const SECURITY = {
|
|
103
|
-
/** 리셋 토큰 만료 시간 (밀리초) -
|
|
104
|
-
RESET_TOKEN_TTL_MS:
|
|
153
|
+
/** 리셋 토큰 만료 시간 (밀리초) - 1분 */
|
|
154
|
+
RESET_TOKEN_TTL_MS: 60000,
|
|
155
|
+
};
|
|
156
|
+
// ============================================
|
|
157
|
+
// API/네트워크 설정
|
|
158
|
+
// ============================================
|
|
159
|
+
export const API = {
|
|
160
|
+
/** Claude API 타임아웃 (밀리초) - 2분 */
|
|
161
|
+
TIMEOUT_MS: 120000,
|
|
162
|
+
/** 최대 재시도 횟수 */
|
|
163
|
+
MAX_RETRIES: 3,
|
|
164
|
+
/** 초기 재시도 대기 시간 (밀리초) */
|
|
165
|
+
INITIAL_RETRY_DELAY_MS: 1000,
|
|
166
|
+
/** 최대 재시도 대기 시간 (밀리초) */
|
|
167
|
+
MAX_RETRY_DELAY_MS: 30000,
|
|
168
|
+
/** 재시도 백오프 배수 */
|
|
169
|
+
BACKOFF_MULTIPLIER: 2,
|
|
170
|
+
};
|
|
171
|
+
// ============================================
|
|
172
|
+
// 메모리 검색 타임아웃 설정
|
|
173
|
+
// ============================================
|
|
174
|
+
export const SEARCH = {
|
|
175
|
+
/** 전체 검색 타임아웃 (밀리초) */
|
|
176
|
+
TIMEOUT_MS: 5000,
|
|
177
|
+
/** 임베딩 생성 타임아웃 (밀리초) */
|
|
178
|
+
EMBED_TIMEOUT_MS: 3000,
|
|
105
179
|
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 설정 파일 로더
|
|
3
|
+
* config.json에서 사용자 설정을 읽음
|
|
4
|
+
*/
|
|
5
|
+
import { readFileSync, existsSync } from "fs";
|
|
6
|
+
import { resolve, dirname } from "path";
|
|
7
|
+
import { fileURLToPath } from "url";
|
|
8
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
const CONFIG_PATH = resolve(__dirname, "../../config.json");
|
|
10
|
+
const DEFAULT_CONFIG = {
|
|
11
|
+
thinking: "medium",
|
|
12
|
+
};
|
|
13
|
+
let cachedConfig = null;
|
|
14
|
+
export function loadConfig() {
|
|
15
|
+
if (cachedConfig)
|
|
16
|
+
return cachedConfig;
|
|
17
|
+
if (!existsSync(CONFIG_PATH)) {
|
|
18
|
+
console.log("[Config] config.json not found, using defaults");
|
|
19
|
+
cachedConfig = DEFAULT_CONFIG;
|
|
20
|
+
return cachedConfig;
|
|
21
|
+
}
|
|
22
|
+
try {
|
|
23
|
+
const raw = readFileSync(CONFIG_PATH, "utf-8");
|
|
24
|
+
const parsed = JSON.parse(raw);
|
|
25
|
+
cachedConfig = {
|
|
26
|
+
thinking: parsed.thinking ?? DEFAULT_CONFIG.thinking,
|
|
27
|
+
};
|
|
28
|
+
console.log(`[Config] Loaded: thinking=${cachedConfig.thinking}`);
|
|
29
|
+
return cachedConfig;
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
console.error("[Config] Failed to load config.json:", error);
|
|
33
|
+
cachedConfig = DEFAULT_CONFIG;
|
|
34
|
+
return cachedConfig;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
export function getConfig() {
|
|
38
|
+
return cachedConfig ?? loadConfig();
|
|
39
|
+
}
|
package/dist/health/index.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* 봇의 상태를 추적하고 모니터링합니다.
|
|
4
4
|
* @module health
|
|
5
5
|
*/
|
|
6
|
+
import { getWarmupStatus } from "../warmup.js";
|
|
6
7
|
let startTime = Date.now();
|
|
7
8
|
let lastActivity = Date.now();
|
|
8
9
|
let messageCount = 0;
|
|
@@ -36,7 +37,8 @@ export function getHealthStatus() {
|
|
|
36
37
|
lastActivity,
|
|
37
38
|
messageCount,
|
|
38
39
|
errorCount,
|
|
39
|
-
isHealthy
|
|
40
|
+
isHealthy,
|
|
41
|
+
warmup: getWarmupStatus(),
|
|
40
42
|
};
|
|
41
43
|
}
|
|
42
44
|
/**
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 메모리 검색 벤치마크
|
|
3
|
+
*
|
|
4
|
+
* 사용법: npx tsx src/memory/benchmark.ts
|
|
5
|
+
*/
|
|
6
|
+
import { performance } from "perf_hooks";
|
|
7
|
+
// 현재 구현
|
|
8
|
+
import * as currentVector from "./vectorStore.js";
|
|
9
|
+
import * as currentFts from "./ftsIndex.js";
|
|
10
|
+
import * as currentHybrid from "./hybridSearch.js";
|
|
11
|
+
// 최적화 구현 (주석 해제하여 비교)
|
|
12
|
+
// import * as optimizedVector from "./vectorStore.optimized.js";
|
|
13
|
+
// import * as optimizedFts from "./ftsIndex.optimized.js";
|
|
14
|
+
// import * as optimizedHybrid from "./hybridSearch.optimized.js";
|
|
15
|
+
const TEST_QUERIES = [
|
|
16
|
+
"오늘 무슨 일이 있었어?",
|
|
17
|
+
"지난주 회의 내용",
|
|
18
|
+
"프로젝트 마감일",
|
|
19
|
+
"API 키 설정",
|
|
20
|
+
"에러 해결 방법",
|
|
21
|
+
"CompanionBot feature",
|
|
22
|
+
"일정 확인",
|
|
23
|
+
"메모리 검색 최적화",
|
|
24
|
+
"테스트 코드 작성",
|
|
25
|
+
"버그 수정",
|
|
26
|
+
];
|
|
27
|
+
async function benchmark(name, fn, iterations = 10) {
|
|
28
|
+
const times = [];
|
|
29
|
+
// 워밍업
|
|
30
|
+
await fn();
|
|
31
|
+
await fn();
|
|
32
|
+
for (let i = 0; i < iterations; i++) {
|
|
33
|
+
const start = performance.now();
|
|
34
|
+
await fn();
|
|
35
|
+
const end = performance.now();
|
|
36
|
+
times.push(end - start);
|
|
37
|
+
}
|
|
38
|
+
times.sort((a, b) => a - b);
|
|
39
|
+
const avgMs = times.reduce((a, b) => a + b, 0) / times.length;
|
|
40
|
+
const p95Idx = Math.floor(times.length * 0.95);
|
|
41
|
+
return {
|
|
42
|
+
name,
|
|
43
|
+
avgMs: Math.round(avgMs * 100) / 100,
|
|
44
|
+
minMs: Math.round(times[0] * 100) / 100,
|
|
45
|
+
maxMs: Math.round(times[times.length - 1] * 100) / 100,
|
|
46
|
+
p95Ms: Math.round(times[p95Idx] * 100) / 100,
|
|
47
|
+
ops: Math.round(1000 / avgMs),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
function printResult(result) {
|
|
51
|
+
console.log(`
|
|
52
|
+
${result.name}:
|
|
53
|
+
Average: ${result.avgMs}ms
|
|
54
|
+
Min: ${result.minMs}ms
|
|
55
|
+
Max: ${result.maxMs}ms
|
|
56
|
+
P95: ${result.p95Ms}ms
|
|
57
|
+
Ops/sec: ${result.ops}
|
|
58
|
+
`);
|
|
59
|
+
}
|
|
60
|
+
async function main() {
|
|
61
|
+
console.log("=".repeat(60));
|
|
62
|
+
console.log("Memory Search Benchmark");
|
|
63
|
+
console.log("=".repeat(60));
|
|
64
|
+
console.log(`\nTest queries: ${TEST_QUERIES.length}`);
|
|
65
|
+
console.log(`Iterations per query: 10\n`);
|
|
66
|
+
// 1. 벡터 검색 벤치마크
|
|
67
|
+
console.log("\n--- Vector Search ---");
|
|
68
|
+
for (const query of TEST_QUERIES.slice(0, 3)) {
|
|
69
|
+
const result = await benchmark(`Vector search: "${query.slice(0, 20)}..."`, async () => {
|
|
70
|
+
return currentHybrid.searchVector(query, 5, 0.3);
|
|
71
|
+
}, 10);
|
|
72
|
+
printResult(result);
|
|
73
|
+
}
|
|
74
|
+
// 2. FTS 검색 벤치마크
|
|
75
|
+
console.log("\n--- FTS Keyword Search ---");
|
|
76
|
+
for (const query of TEST_QUERIES.slice(0, 3)) {
|
|
77
|
+
const result = await benchmark(`FTS search: "${query.slice(0, 20)}..."`, async () => {
|
|
78
|
+
return currentFts.searchKeyword(query, 10);
|
|
79
|
+
}, 10);
|
|
80
|
+
printResult(result);
|
|
81
|
+
}
|
|
82
|
+
// 3. 하이브리드 검색 벤치마크
|
|
83
|
+
console.log("\n--- Hybrid Search ---");
|
|
84
|
+
for (const query of TEST_QUERIES.slice(0, 3)) {
|
|
85
|
+
const result = await benchmark(`Hybrid search: "${query.slice(0, 20)}..."`, async () => {
|
|
86
|
+
return currentHybrid.hybridSearch(query, { topK: 5 });
|
|
87
|
+
}, 10);
|
|
88
|
+
printResult(result);
|
|
89
|
+
}
|
|
90
|
+
// 4. 청크 로딩 벤치마크
|
|
91
|
+
console.log("\n--- Chunk Loading ---");
|
|
92
|
+
// 캐시 무효화 후 로딩 시간
|
|
93
|
+
currentVector.invalidateCache();
|
|
94
|
+
const loadResult = await benchmark("Load all chunks (cold)", async () => {
|
|
95
|
+
currentVector.invalidateCache();
|
|
96
|
+
return currentVector.loadAllMemoryChunks();
|
|
97
|
+
}, 5);
|
|
98
|
+
printResult(loadResult);
|
|
99
|
+
// 캐시된 로딩 시간
|
|
100
|
+
const cachedLoadResult = await benchmark("Load all chunks (cached)", async () => {
|
|
101
|
+
return currentVector.loadAllMemoryChunks();
|
|
102
|
+
}, 10);
|
|
103
|
+
printResult(cachedLoadResult);
|
|
104
|
+
// 5. 통계
|
|
105
|
+
console.log("\n--- Statistics ---");
|
|
106
|
+
const chunks = await currentVector.loadAllMemoryChunks();
|
|
107
|
+
const ftsCount = currentFts.getDocumentCount();
|
|
108
|
+
console.log(`Total chunks in vector store: ${chunks.length}`);
|
|
109
|
+
console.log(`Total documents in FTS: ${ftsCount}`);
|
|
110
|
+
console.log("\n" + "=".repeat(60));
|
|
111
|
+
console.log("Benchmark complete");
|
|
112
|
+
console.log("=".repeat(60));
|
|
113
|
+
}
|
|
114
|
+
main().catch(console.error);
|