From 1c7a17320cf50da2db36a0811bdd93e363c53a62 Mon Sep 17 00:00:00 2001 From: agentson Date: Wed, 25 Feb 2026 01:46:21 +0900 Subject: [PATCH 1/2] =?UTF-8?q?fix:=20prompt=5Foverride=20=EC=8B=9C=20pars?= =?UTF-8?q?e=5Fresponse=20=EA=B1=B4=EB=84=88=EB=9B=B0=EC=96=B4=20Missing?= =?UTF-8?q?=20fields=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): From d31a61cd0bdaefb329b183796686af60228c7a8f Mon Sep 17 00:00:00 2001 From: agentson Date: Wed, 25 Feb 2026 01:54:55 +0900 Subject: [PATCH 2/2] =?UTF-8?q?fix:=20prompt=5Foverride=20=EA=B2=BD?= =?UTF-8?q?=EB=A1=9C=20=5Ftotal=5Fdecisions=20=EB=AF=B8=EC=B9=B4=EC=9A=B4?= =?UTF-8?q?=ED=8A=B8,=20=EC=99=84=EB=A3=8C=20=EB=A1=9C=EA=B7=B8=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80,=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EB=B3=B4?= =?UTF-8?q?=EC=99=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 리뷰 지적 사항 반영: - _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 --- src/brain/gemini_client.py | 5 ++++- tests/test_brain.py | 30 ++++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/src/brain/gemini_client.py b/src/brain/gemini_client.py index dd9674b..c664eb2 100644 --- a/src/brain/gemini_client.py +++ b/src/brain/gemini_client.py @@ -445,7 +445,10 @@ class GeminiClient: # 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 + 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 ) diff --git a/tests/test_brain.py b/tests/test_brain.py index 8d2711c..c857720 100644 --- a/tests/test_brain.py +++ b/tests/test_brain.py @@ -360,6 +360,36 @@ class TestPromptOverride: # 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, + "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) + + 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 async def test_without_prompt_override_uses_build_prompt(self, settings): """Without prompt_override, decide() should use build_prompt as before."""