From 8df841fdfa05624d103a71ef2e4c2686af30507e Mon Sep 17 00:00:00 2001 From: Ed_ Date: Thu, 25 Jun 2026 20:16:55 -0400 Subject: [PATCH] refactor(ai_client): migrate _send_deepseek history loop to ChatMessage (Phase 5 part 1) Phase 5: ChatMessage (part 1) Before: 6 .get('role'/'content'/'tool_calls'/'tool_call_id') sites in _send_deepseek After: 0 Delta: -6 Migrates _send_deepseek's history transformation loop from dict-style access to ChatMessage direct field access: msg = _ChatMessage.from_dict(msg_raw) msg.role (was msg.get('role')) msg.content (was msg.get('content')) msg.tool_calls (was msg.get('tool_calls') / msg['tool_calls']) msg.tool_call_id (was msg.get('tool_call_id')) The api_msg dict (output for the DeepSeek API) is constructed via direct field access. The tool_calls list is converted to dicts via tc.to_dict() (preserves the existing API payload format). Notes: - msg_raw.get('reasoning_content') is preserved as-is because reasoning_content is NOT a ChatMessage field. - Local import 'from src.openai_schemas import ChatMessage as _ChatMessage' follows the existing pattern in this file (lazy imports inside functions). Tests: 36/36 pass (test_ai_client_result, test_ai_client_tool_loop, test_deepseek_provider, test_openai_schemas). --- src/ai_client.py | 27 ++++++++++++--------------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/src/ai_client.py b/src/ai_client.py index 70282116..215ceeda 100644 --- a/src/ai_client.py +++ b/src/ai_client.py @@ -2202,29 +2202,26 @@ def _send_deepseek(md_content: str, user_message: str, base_dir: str, current_api_messages.append(sys_msg) with history.lock: - for i, msg in enumerate(history): - # Create a clean copy of the message for the API - role = msg.get("role") - api_msg = {"role": role} + from src.openai_schemas import ChatMessage as _ChatMessage + for i, msg_raw in enumerate(history): + msg = _ChatMessage.from_dict(msg_raw) + api_msg = {"role": msg.role} - content = msg.get("content") + content = msg.content if i == 0 and is_reasoner: - # Prepend system instructions to the first user message for R1 content = f"System Instructions:\n{_get_combined_system_prompt()}\n\nContext:\n{md_content}\n\n---\n\n{content}" - if role == "assistant": - # OpenAI/DeepSeek: content MUST be a string if tool_calls is absent - # If tool_calls is present, content can be null - if msg.get("tool_calls"): + if msg.role == "assistant": + if msg.tool_calls: api_msg["content"] = content or None - api_msg["tool_calls"] = msg["tool_calls"] + api_msg["tool_calls"] = [tc.to_dict() for tc in msg.tool_calls] else: api_msg["content"] = content or "" - if msg.get("reasoning_content"): - api_msg["reasoning_content"] = msg["reasoning_content"] - elif role == "tool": + if msg_raw.get("reasoning_content"): + api_msg["reasoning_content"] = msg_raw["reasoning_content"] + elif msg.role == "tool": api_msg["content"] = content or "" - api_msg["tool_call_id"] = msg.get("tool_call_id") + api_msg["tool_call_id"] = msg.tool_call_id else: api_msg["content"] = content or ""