Compare commits

...

2 Commits

Author SHA1 Message Date
agentson
d31a61cd0b fix: prompt_override 경로 _total_decisions 미카운트, 완료 로그 추가, 테스트 보완
Some checks failed
CI / test (pull_request) Has been cancelled
리뷰 지적 사항 반영:
- _total_decisions 카운트 제거 (플레이북 생성은 거래 결정이 아님 → 메트릭 왜곡 방지)
- "Gemini raw response received" INFO 로그 추가 (완료 추적 가능)
- test_prompt_override_takes_priority_over_optimization 신규 추가
  (enable_optimization=True 상태에서도 prompt_override 우선됨을 검증)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-25 01:54:55 +09:00
agentson
1c7a17320c fix: prompt_override 시 parse_response 건너뛰어 Missing fields 경고 제거 (#247)
Some checks failed
CI / test (pull_request) Has been cancelled
pre_market_planner처럼 prompt_override를 사용하는 호출자는 플레이북 JSON 등
TradeDecision이 아닌 raw 텍스트를 기대한다. 기존에는 parse_response를 통과시켜
항상 "Missing fields" 경고가 발생했다.

decide()에서 prompt_override 감지 시 parse_response를 건너뛰고 raw 응답을
rationale에 담아 직접 반환하도록 수정한다.
정상 응답인데 경고가 뜨는 문제가 해결된다.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-25 01:46:21 +09:00
2 changed files with 52 additions and 5 deletions

View File

@@ -441,6 +441,18 @@ class GeminiClient:
action="HOLD", confidence=0, rationale=f"API error: {exc}", token_count=token_count
)
# prompt_override callers (e.g. pre_market_planner) expect raw text back,
# not a parsed TradeDecision. Skip parse_response to avoid spurious
# "Missing fields" warnings and return the raw response directly. (#247)
if "prompt_override" in market_data:
logger.info(
"Gemini raw response received (prompt_override, tokens=%d)", token_count
)
# Not a trade decision — don't inflate _total_decisions metrics
return TradeDecision(
action="HOLD", confidence=0, rationale=raw, token_count=token_count
)
decision = self.parse_response(raw)
self._total_decisions += 1

View File

@@ -302,9 +302,10 @@ class TestPromptOverride:
client = GeminiClient(settings)
custom_prompt = "You are a playbook generator. Return JSON with scenarios."
playbook_json = '{"market_outlook": "neutral", "stocks": []}'
mock_response = MagicMock()
mock_response.text = '{"action": "HOLD", "confidence": 50, "rationale": "test"}'
mock_response.text = playbook_json
with patch.object(
client._client.aio.models,
@@ -317,7 +318,7 @@ class TestPromptOverride:
"current_price": 0,
"prompt_override": custom_prompt,
}
await client.decide(market_data)
decision = await client.decide(market_data)
# Verify the custom prompt was sent, not a built prompt
mock_generate.assert_called_once()
@@ -325,17 +326,50 @@ class TestPromptOverride:
"contents", mock_generate.call_args[0][1] if len(mock_generate.call_args[0]) > 1 else None
)
assert actual_prompt == custom_prompt
# Raw response preserved in rationale without parse_response (#247)
assert decision.rationale == playbook_json
@pytest.mark.asyncio
async def test_prompt_override_skips_optimization(self, settings):
"""prompt_override should bypass prompt optimization."""
async def test_prompt_override_skips_parse_response(self, settings):
"""prompt_override bypasses parse_response — no Missing fields warning, raw preserved."""
client = GeminiClient(settings)
client._enable_optimization = True
custom_prompt = "Custom playbook prompt"
playbook_json = '{"market_outlook": "bullish", "stocks": [{"stock_code": "AAPL"}]}'
mock_response = MagicMock()
mock_response.text = '{"action": "HOLD", "confidence": 50, "rationale": "ok"}'
mock_response.text = playbook_json
with patch.object(
client._client.aio.models,
"generate_content",
new_callable=AsyncMock,
return_value=mock_response,
):
with patch.object(client, "parse_response") as mock_parse:
market_data = {
"stock_code": "PLANNER",
"current_price": 0,
"prompt_override": custom_prompt,
}
decision = await client.decide(market_data)
# parse_response must NOT be called for prompt_override
mock_parse.assert_not_called()
# Raw playbook JSON preserved in rationale
assert decision.rationale == playbook_json
@pytest.mark.asyncio
async def test_prompt_override_takes_priority_over_optimization(self, settings):
"""prompt_override must win over enable_optimization=True."""
client = GeminiClient(settings)
client._enable_optimization = True
custom_prompt = "Explicit playbook prompt"
mock_response = MagicMock()
mock_response.text = '{"market_outlook": "neutral", "stocks": []}'
with patch.object(
client._client.aio.models,
@@ -353,6 +387,7 @@ class TestPromptOverride:
actual_prompt = mock_generate.call_args[1].get(
"contents", mock_generate.call_args[0][1] if len(mock_generate.call_args[0]) > 1 else None
)
# The custom prompt must be used, not the compressed prompt
assert actual_prompt == custom_prompt
@pytest.mark.asyncio