From 1c7a17320cf50da2db36a0811bdd93e363c53a62 Mon Sep 17 00:00:00 2001 From: agentson Date: Wed, 25 Feb 2026 01:46:21 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20prompt=5Foverride=20=EC=8B=9C=20parse=5F?= =?UTF-8?q?response=20=EA=B1=B4=EB=84=88=EB=9B=B0=EC=96=B4=20Missing=20fie?= =?UTF-8?q?lds=20=EA=B2=BD=EA=B3=A0=20=EC=A0=9C=EA=B1=B0=20(#247)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/brain/gemini_client.py | 9 +++++++++ tests/test_brain.py | 37 +++++++++++++++++++++---------------- 2 files changed, 30 insertions(+), 16 deletions(-) diff --git a/src/brain/gemini_client.py b/src/brain/gemini_client.py index d6a3f2b..dd9674b 100644 --- a/src/brain/gemini_client.py +++ b/src/brain/gemini_client.py @@ -441,6 +441,15 @@ 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: + self._total_decisions += 1 + return TradeDecision( + action="HOLD", confidence=0, rationale=raw, token_count=token_count + ) + decision = self.parse_response(raw) self._total_decisions += 1 diff --git a/tests/test_brain.py b/tests/test_brain.py index 1103d6e..8d2711c 100644 --- a/tests/test_brain.py +++ b/tests/test_brain.py @@ -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,35 +326,39 @@ 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, - ) as mock_generate: - market_data = { - "stock_code": "PLANNER", - "current_price": 0, - "prompt_override": custom_prompt, - } - await client.decide(market_data) + ): + 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) - 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 - ) - assert actual_prompt == custom_prompt + # 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_without_prompt_override_uses_build_prompt(self, settings):