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 <noreply@anthropic.com>
This commit is contained in:
kappa
2026-01-27 11:36:07 +09:00
parent b1bbce5375
commit 2a258605f2

View File

@@ -25,17 +25,18 @@ export async function getServerSession(
): Promise<ServerSession | null> { ): Promise<ServerSession | null> {
try { try {
const key = `${SESSION_KEY_PREFIX}${userId}`; const key = `${SESSION_KEY_PREFIX}${userId}`;
logger.info('세션 조회 시도', { userId, key });
const data = await kv.get(key, 'json'); const data = await kv.get(key, 'json');
if (!data) { if (!data) {
logger.info('세션 없음', { userId }); logger.info('세션 없음', { userId, key });
return null; return null;
} }
logger.info('세션 조회 성공', { userId, status: (data as ServerSession).status }); logger.info('세션 조회 성공', { userId, key, status: (data as ServerSession).status });
return data as ServerSession; return data as ServerSession;
} catch (error) { } catch (error) {
logger.error('세션 조회 실패', error as Error, { userId }); logger.error('세션 조회 실패', error as Error, { userId, key: `${SESSION_KEY_PREFIX}${userId}` });
return null; return null;
} }
} }
@@ -49,13 +50,16 @@ export async function saveServerSession(
const key = `${SESSION_KEY_PREFIX}${userId}`; const key = `${SESSION_KEY_PREFIX}${userId}`;
session.updatedAt = Date.now(); 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, expirationTtl: SESSION_TTL,
}); });
logger.info('세션 저장 성공', { userId, status: session.status }); logger.info('세션 저장 성공', { userId, key, status: session.status });
} catch (error) { } catch (error) {
logger.error('세션 저장 실패', error as Error, { userId }); logger.error('세션 저장 실패', error as Error, { userId, key: `${SESSION_KEY_PREFIX}${userId}` });
throw error; throw error;
} }
} }
@@ -370,20 +374,31 @@ ${JSON.stringify(session.collectedInfo, null, 2)}
// Loop to handle tool calls // Loop to handle tool calls
while (toolCallCount < MAX_TOOL_CALLS) { while (toolCallCount < MAX_TOOL_CALLS) {
const response = await fetch(getOpenAIUrl(env), { // 검토 모드에서는 도구 없이 JSON 응답만 요청
method: 'POST', const requestBody = isReviewMode
headers: { ? {
'Content-Type': 'application/json', model: 'gpt-4o-mini',
'Authorization': `Bearer ${env.OPENAI_API_KEY}`, messages,
}, response_format: { type: 'json_object' },
body: JSON.stringify({ max_tokens: 500,
temperature: 0.5,
}
: {
model: 'gpt-4o-mini', model: 'gpt-4o-mini',
messages, messages,
tools: serverExpertTools, tools: serverExpertTools,
tool_choice: 'auto', tool_choice: 'auto',
max_tokens: 800, max_tokens: 800,
temperature: 0.7, 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(requestBody),
}); });
if (!response.ok) { if (!response.ok) {
@@ -570,11 +585,14 @@ export async function processServerConsultation(
// 동시접속자 우선 사용, 없으면 scale 기반 추론 // 동시접속자 우선 사용, 없으면 scale 기반 추론
let expectedUsers = 10; // Default let expectedUsers = 10; // Default
if (session.collectedInfo.expectedConcurrent) { const concurrent = Number(session.collectedInfo.expectedConcurrent) || 0;
expectedUsers = session.collectedInfo.expectedConcurrent; const dau = Number(session.collectedInfo.expectedDau) || 0;
} else if (session.collectedInfo.expectedDau) {
if (concurrent > 0) {
expectedUsers = concurrent;
} else if (dau > 0) {
// DAU가 있으면 10% 비율로 동시접속자 계산 // DAU가 있으면 10% 비율로 동시접속자 계산
expectedUsers = Math.ceil(session.collectedInfo.expectedDau * 0.1); expectedUsers = Math.ceil(dau * 0.1);
} else if (session.collectedInfo.scale) { } else if (session.collectedInfo.scale) {
expectedUsers = inferExpectedUsers(session.collectedInfo.scale); expectedUsers = inferExpectedUsers(session.collectedInfo.scale);
} }