From 2a258605f246806fb36c05a6a9bd4917716d30a5 Mon Sep 17 00:00:00 2001 From: kappa Date: Tue, 27 Jan 2026 11:36:07 +0900 Subject: [PATCH] fix: Server Expert AI JSON parsing and numeric type conversion - Add response_format: { type: 'json_object' } for review mode to force JSON response - Convert expectedDau and expectedConcurrent from string to number before API call - Add enhanced KV session debugging with key names in logs Fixes: - AI returning plain text instead of JSON in review mode - 400 error from recommend API due to string values in expected_users Co-Authored-By: Claude Opus 4.5 --- src/server-agent.ts | 54 ++++++++++++++++++++++++++++++--------------- 1 file changed, 36 insertions(+), 18 deletions(-) diff --git a/src/server-agent.ts b/src/server-agent.ts index 6d3ae59..9c21daf 100644 --- a/src/server-agent.ts +++ b/src/server-agent.ts @@ -25,17 +25,18 @@ export async function getServerSession( ): Promise { try { const key = `${SESSION_KEY_PREFIX}${userId}`; + logger.info('세션 조회 시도', { userId, key }); const data = await kv.get(key, 'json'); if (!data) { - logger.info('세션 없음', { userId }); + logger.info('세션 없음', { userId, key }); return null; } - logger.info('세션 조회 성공', { userId, status: (data as ServerSession).status }); + logger.info('세션 조회 성공', { userId, key, status: (data as ServerSession).status }); return data as ServerSession; } catch (error) { - logger.error('세션 조회 실패', error as Error, { userId }); + logger.error('세션 조회 실패', error as Error, { userId, key: `${SESSION_KEY_PREFIX}${userId}` }); return null; } } @@ -49,13 +50,16 @@ export async function saveServerSession( const key = `${SESSION_KEY_PREFIX}${userId}`; session.updatedAt = Date.now(); - await kv.put(key, JSON.stringify(session), { + const sessionData = JSON.stringify(session); + logger.info('세션 저장 시도', { userId, key, status: session.status, dataLength: sessionData.length }); + + await kv.put(key, sessionData, { expirationTtl: SESSION_TTL, }); - logger.info('세션 저장 성공', { userId, status: session.status }); + logger.info('세션 저장 성공', { userId, key, status: session.status }); } catch (error) { - logger.error('세션 저장 실패', error as Error, { userId }); + logger.error('세션 저장 실패', error as Error, { userId, key: `${SESSION_KEY_PREFIX}${userId}` }); throw error; } } @@ -370,20 +374,31 @@ ${JSON.stringify(session.collectedInfo, null, 2)} // Loop to handle tool calls while (toolCallCount < MAX_TOOL_CALLS) { + // 검토 모드에서는 도구 없이 JSON 응답만 요청 + const requestBody = isReviewMode + ? { + model: 'gpt-4o-mini', + messages, + response_format: { type: 'json_object' }, + max_tokens: 500, + temperature: 0.5, + } + : { + model: 'gpt-4o-mini', + messages, + tools: serverExpertTools, + tool_choice: 'auto', + max_tokens: 800, + temperature: 0.7, + }; + const response = await fetch(getOpenAIUrl(env), { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${env.OPENAI_API_KEY}`, }, - body: JSON.stringify({ - model: 'gpt-4o-mini', - messages, - tools: serverExpertTools, - tool_choice: 'auto', - max_tokens: 800, - temperature: 0.7, - }), + body: JSON.stringify(requestBody), }); if (!response.ok) { @@ -570,11 +585,14 @@ export async function processServerConsultation( // 동시접속자 우선 사용, 없으면 scale 기반 추론 let expectedUsers = 10; // Default - if (session.collectedInfo.expectedConcurrent) { - expectedUsers = session.collectedInfo.expectedConcurrent; - } else if (session.collectedInfo.expectedDau) { + const concurrent = Number(session.collectedInfo.expectedConcurrent) || 0; + const dau = Number(session.collectedInfo.expectedDau) || 0; + + if (concurrent > 0) { + expectedUsers = concurrent; + } else if (dau > 0) { // DAU가 있으면 10% 비율로 동시접속자 계산 - expectedUsers = Math.ceil(session.collectedInfo.expectedDau * 0.1); + expectedUsers = Math.ceil(dau * 0.1); } else if (session.collectedInfo.scale) { expectedUsers = inferExpectedUsers(session.collectedInfo.scale); }