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> {
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);
}