companionbot 0.13.0 → 0.14.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.
Files changed (2) hide show
  1. package/dist/ai/claude.js +62 -71
  2. package/package.json +1 -1
package/dist/ai/claude.js CHANGED
@@ -23,7 +23,7 @@ function getClient() {
23
23
  }
24
24
  return anthropic;
25
25
  }
26
- // Thinking 레벨별 설정 (비율 및 최대값)
26
+ // Thinking 레벨별 설정
27
27
  export const THINKING_CONFIGS = {
28
28
  off: { ratio: 0, maxBudget: 0 },
29
29
  low: { ratio: 0.3, maxBudget: 5000 },
@@ -36,7 +36,7 @@ export const MODELS = {
36
36
  id: "claude-haiku-3-5-20241022",
37
37
  name: "Claude Haiku 3.5",
38
38
  contextWindow: 200000,
39
- supportsThinking: false, // Haiku는 thinking 미지원
39
+ supportsThinking: false,
40
40
  },
41
41
  sonnet: {
42
42
  id: "claude-sonnet-4-20250514",
@@ -51,58 +51,47 @@ export const MODELS = {
51
51
  supportsThinking: true,
52
52
  },
53
53
  };
54
- // 동적 토큰 계산을 위한 설정
55
- const MIN_OUTPUT_TOKENS = 4096; // 최소 출력 토큰
56
- const OUTPUT_BUFFER_RATIO = 0.3; // 컨텍스트의 30%를 출력용으로 예약
54
+ // 동적 토큰 계산 설정
55
+ const MIN_OUTPUT_TOKENS = 4096;
56
+ const OUTPUT_BUFFER_RATIO = 0.3;
57
57
  /**
58
58
  * 동적으로 max_tokens와 thinking budget 계산
59
- *
60
- * @param modelId 모델 ID
61
- * @param thinkingLevel thinking 레벨
62
- * @param inputTokens 현재 입력 토큰 수 (시스템 프롬프트 + 히스토리)
63
- * @returns { maxTokens, thinkingBudget }
64
59
  */
65
60
  export function calculateTokenBudgets(modelId, thinkingLevel, inputTokens) {
66
61
  const model = MODELS[modelId];
67
62
  const thinkingConfig = THINKING_CONFIGS[thinkingLevel];
68
- // Thinking 미지원 모델이거나 off인 경우
69
63
  if (!model.supportsThinking || thinkingLevel === "off") {
70
- // 간단히 고정 max_tokens 사용
71
64
  return { maxTokens: 8192, thinkingBudget: 0 };
72
65
  }
73
- // 사용 가능한 출력 토큰 계산
74
- // 컨텍스트 윈도우 - 입력 토큰 = 출력 가능 토큰
75
66
  const availableOutputTokens = model.contextWindow - inputTokens;
76
- // 최소 출력 토큰 보장
77
67
  const maxTokens = Math.max(MIN_OUTPUT_TOKENS, Math.floor(availableOutputTokens * OUTPUT_BUFFER_RATIO));
78
- // thinking budget 계산: min(레벨별 최대값, max_tokens * 비율)
79
- // API 조건: max_tokens > budget_tokens 이므로 max_tokens - 1024 로 상한 설정
80
68
  const calculatedBudget = Math.floor(maxTokens * thinkingConfig.ratio);
81
- const thinkingBudget = Math.min(thinkingConfig.maxBudget, calculatedBudget, maxTokens - 1024 // max_tokens > budget_tokens 조건 충족
82
- );
83
- // budget이 1024 미만이면 thinking 비활성화 (의미 없음)
69
+ const thinkingBudget = Math.min(thinkingConfig.maxBudget, calculatedBudget, maxTokens - 1024);
84
70
  if (thinkingBudget < 1024) {
85
71
  return { maxTokens, thinkingBudget: 0 };
86
72
  }
87
73
  return { maxTokens, thinkingBudget };
88
74
  }
75
+ /**
76
+ * Claude API 호출 (스트리밍 내부 사용, thinking 지원)
77
+ * - 스트리밍으로 호출하되 최종 응답만 반환 (사용자에게 중간 메시지 안 보냄)
78
+ * - thinking 활성화 가능
79
+ * - 도구 사용 시에는 non-streaming으로 폴백 (thinking off)
80
+ */
89
81
  export async function chat(messages, systemPrompt, modelId = "sonnet", thinkingLevel = "medium") {
90
82
  const client = getClient();
91
83
  const modelConfig = MODELS[modelId];
92
84
  const toolsUsed = [];
93
- // 메시지를 API 형식으로 변환
94
85
  const apiMessages = messages.map((m) => ({
95
86
  role: m.role,
96
87
  content: m.content,
97
88
  }));
98
- // 입력 토큰 추정 (대략적)
89
+ // 입력 토큰 추정
99
90
  const estimateInputTokens = () => {
100
91
  let total = 0;
101
- // 시스템 프롬프트
102
92
  if (systemPrompt) {
103
- total += Math.ceil(systemPrompt.length / 3); // 대략 3자당 1토큰
93
+ total += Math.ceil(systemPrompt.length / 3);
104
94
  }
105
- // 메시지들
106
95
  for (const msg of apiMessages) {
107
96
  const content = typeof msg.content === "string"
108
97
  ? msg.content
@@ -111,44 +100,65 @@ export async function chat(messages, systemPrompt, modelId = "sonnet", thinkingL
111
100
  }
112
101
  return total;
113
102
  };
114
- // 동적 토큰 budget 계산
115
103
  const inputTokens = estimateInputTokens();
116
104
  const { maxTokens, thinkingBudget } = calculateTokenBudgets(modelId, thinkingLevel, inputTokens);
117
105
  console.log(`[Chat] model=${modelId}, thinking=${thinkingLevel}, input~${inputTokens}, maxTokens=${maxTokens}, budget=${thinkingBudget}`);
118
- // API 요청 파라미터 빌드 (도구 루프에서도 동일하게 사용)
119
- const buildRequestParams = () => {
106
+ // 스트리밍 호출 (thinking 사용 가능)
107
+ const streamRequest = async () => {
120
108
  const params = {
121
109
  model: modelConfig.id,
122
110
  max_tokens: maxTokens,
123
111
  messages: apiMessages,
124
112
  tools: tools,
113
+ stream: true,
125
114
  };
126
115
  if (systemPrompt) {
127
116
  params.system = systemPrompt;
128
117
  }
129
- // thinking 활성화 (budget > 0인 경우)
118
+ // thinking 활성화
130
119
  if (thinkingBudget > 0) {
131
120
  params.thinking = {
132
121
  type: "enabled",
133
122
  budget_tokens: thinkingBudget,
134
123
  };
135
124
  }
136
- return params;
125
+ // 스트리밍하되 최종 메시지만 반환
126
+ const stream = client.messages.stream(params);
127
+ return await stream.finalMessage();
137
128
  };
129
+ // Non-streaming 호출 (도구 사용 루프용, thinking off)
130
+ const nonStreamRequest = async () => {
131
+ const params = {
132
+ model: modelConfig.id,
133
+ max_tokens: 8192,
134
+ messages: apiMessages,
135
+ tools: tools,
136
+ };
137
+ if (systemPrompt) {
138
+ params.system = systemPrompt;
139
+ }
140
+ return await client.messages.create(params);
141
+ };
142
+ // 첫 번째 호출은 스트리밍 (thinking 사용)
138
143
  let response;
139
- response = await withRetry(() => withTimeout(() => client.messages.create(buildRequestParams()), API_TIMEOUT_MS, "API 응답 시간 초과"), API_RETRY_OPTIONS);
140
- // Tool use 루프 - Claude가 도구 사용을 멈출 때까지 반복
144
+ try {
145
+ response = await withRetry(() => withTimeout(streamRequest, API_TIMEOUT_MS, "API 응답 시간 초과"), API_RETRY_OPTIONS);
146
+ }
147
+ catch (error) {
148
+ // 스트리밍 실패 시 non-streaming 폴백
149
+ console.log("[Chat] Streaming failed, falling back to non-streaming");
150
+ response = await withRetry(() => withTimeout(nonStreamRequest, API_TIMEOUT_MS, "API 응답 시간 초과"), API_RETRY_OPTIONS);
151
+ }
152
+ // Tool use 루프 (non-streaming, thinking off)
141
153
  let iterations = 0;
142
154
  while (response.stop_reason === "tool_use" && iterations < MAX_TOOL_ITERATIONS) {
143
155
  iterations++;
144
156
  const toolUseBlocks = response.content.filter((block) => block.type === "tool_use");
145
- // 도구 병렬 실행 (성능 최적화)
146
157
  console.log(`[Tool] Executing ${toolUseBlocks.length} tool(s) in parallel`);
147
158
  const toolExecutions = await Promise.all(toolUseBlocks.map(async (toolUse) => {
148
159
  const startTime = Date.now();
149
- console.log(`[Tool] ${toolUse.name}:`, JSON.stringify(toolUse.input).slice(0, 200));
160
+ console.log(`[Tool] ${toolUse.name}:`, JSON.stringify(toolUse.input).slice(0, TOOL_INPUT_SUMMARY_LENGTH));
150
161
  try {
151
- // 도구별 타임아웃 적용
152
162
  const timeout = getToolTimeout(toolUse.name);
153
163
  const result = await Promise.race([
154
164
  executeTool(toolUse.name, toolUse.input),
@@ -156,32 +166,17 @@ export async function chat(messages, systemPrompt, modelId = "sonnet", thinkingL
156
166
  ]);
157
167
  const elapsed = Date.now() - startTime;
158
168
  console.log(`[Tool] ${toolUse.name} completed in ${elapsed}ms`);
159
- // 스마트 결과 압축
160
169
  const compressedResult = compressToolResult(toolUse.name, result);
161
- return {
162
- toolUse,
163
- result: compressedResult,
164
- success: true,
165
- };
170
+ return { toolUse, result: compressedResult, success: true };
166
171
  }
167
172
  catch (error) {
168
173
  const elapsed = Date.now() - startTime;
169
174
  const errorMsg = error instanceof Error ? error.message : String(error);
170
175
  console.error(`[Tool] ${toolUse.name} failed after ${elapsed}ms:`, errorMsg);
171
- return {
172
- toolUse,
173
- result: `Error: ${errorMsg}`,
174
- success: false,
175
- };
176
+ return { toolUse, result: `Error: ${errorMsg}`, success: false };
176
177
  }
177
178
  }));
178
- // 결과 수집
179
- const toolResults = toolExecutions.map((exec) => ({
180
- type: "tool_result",
181
- tool_use_id: exec.toolUse.id,
182
- content: exec.result,
183
- }));
184
- // 도구 사용 기록
179
+ // 도구 결과 기록
185
180
  for (const exec of toolExecutions) {
186
181
  toolsUsed.push({
187
182
  name: exec.toolUse.name,
@@ -189,34 +184,30 @@ export async function chat(messages, systemPrompt, modelId = "sonnet", thinkingL
189
184
  output: exec.result.slice(0, TOOL_OUTPUT_SUMMARY_LENGTH),
190
185
  });
191
186
  }
192
- // 어시스턴트 메시지와 도구 결과 추가
187
+ // 어시스턴트 메시지 추가 (도구 호출)
193
188
  apiMessages.push({
194
189
  role: "assistant",
195
190
  content: response.content,
196
191
  });
192
+ // 도구 결과 메시지 추가
197
193
  apiMessages.push({
198
194
  role: "user",
199
- content: toolResults,
195
+ content: toolExecutions.map((exec) => ({
196
+ type: "tool_result",
197
+ tool_use_id: exec.toolUse.id,
198
+ content: exec.result,
199
+ })),
200
200
  });
201
- // 다음 응답 요청 (도구 루프에서도 thinking 유지)
202
- response = await withRetry(() => withTimeout(() => client.messages.create(buildRequestParams()), API_TIMEOUT_MS, "API 응답 시간 초과"), API_RETRY_OPTIONS);
203
- }
204
- // 반복 횟수 초과 시 경고
205
- if (iterations >= MAX_TOOL_ITERATIONS) {
206
- console.warn(`[Warning] Tool use loop reached max iterations (${MAX_TOOL_ITERATIONS})`);
207
- return { text: "도구 실행이 너무 많이 반복됐어. 다시 시도해줄래?", toolsUsed };
201
+ // 다음 API 호출 (non-streaming, thinking off - 도구 결과 처리)
202
+ response = await withRetry(() => withTimeout(nonStreamRequest, API_TIMEOUT_MS, "API 응답 시간 초과"), API_RETRY_OPTIONS);
208
203
  }
209
- // 최종 텍스트 응답 추출
210
- const textBlock = response.content.find((block) => block.type === "text");
211
- return {
212
- text: textBlock?.text ?? "응답을 생성하지 못했어. 다시 시도해줄래?",
213
- toolsUsed
214
- };
204
+ // 최종 텍스트 추출
205
+ const textBlocks = response.content.filter((block) => block.type === "text");
206
+ const text = textBlocks.map((b) => b.text).join("\n");
207
+ return { text, toolsUsed };
215
208
  }
216
209
  /**
217
- * 스마트 채팅 - chat()의 단순 래퍼
218
- *
219
- * 도구 사용 여부를 별도로 반환하여 호출자가 구분할 수 있게 함
210
+ * chat()의 간단한 래퍼 - 도구 사용 여부 반환
220
211
  */
221
212
  export async function chatSmart(messages, systemPrompt, modelId, thinkingLevel = "medium") {
222
213
  const result = await chat(messages, systemPrompt, modelId, thinkingLevel);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "companionbot",
3
- "version": "0.13.0",
3
+ "version": "0.14.0",
4
4
  "description": "AI 친구 텔레그램 봇 - Claude API 기반 개인화된 대화 상대",
5
5
  "keywords": [
6
6
  "telegram",