From ea86b537eb11f4c12829ea735b977f400f507c6b Mon Sep 17 00:00:00 2001 From: amstrongzyf <201840057@smail.nju.edu.cn> Date: Mon, 7 Jul 2025 14:18:56 +0800 Subject: [PATCH] fix: support experimental support for Deepseek models and update docs about configuration (#1024) * fix qlib running bug for deepseek and add configuration docs about deepseek --- README.md | 20 +++- docs/installation_and_configuration.rst | 20 ++++ .../coder/factor_coder/evolving_strategy.py | 33 ++++-- rdagent/oai/backend/base.py | 107 ++++++++++++++++-- rdagent/scenarios/qlib/prompts.yaml | 2 +- 5 files changed, 162 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index b96d1d58..3031efa3 100644 --- a/README.md +++ b/README.md @@ -153,7 +153,9 @@ Ensure the current user can run Docker commands **without using sudo**. You can You can set your Chat Model and Embedding Model in the following ways: -- **Using LiteLLM (Default)**: We now support LiteLLM as a backend for integration with multiple LLM providers. You can configure in two ways: + > **🔥 Attention**: We now provide experimental support for **DeepSeek** models! You can use DeepSeek's official API for cost-effective and high-performance inference. See the configuration example below for DeepSeek setup. + +- **Using LiteLLM (Default)**: We now support LiteLLM as a backend for integration with multiple LLM providers. You can configure in multiple ways: **Option 1: Unified API base for both models** ```bash @@ -185,6 +187,22 @@ Ensure the current user can run Docker commands **without using sudo**. You can LITELLM_PROXY_API_BASE=https://api.siliconflow.cn/v1 ``` + **Configuration Example: DeepSeek Setup**: + + >Since many users encounter configuration errors when setting up DeepSeek. Here's a complete working example for DeepSeek Setup: + ```bash + cat << EOF > .env + # CHAT MODEL: Using DeepSeek Official API + CHAT_MODEL=deepseek/deepseek-chat + DEEPSEEK_API_KEY= + + # EMBEDDING MODEL: Using SiliconFlow for embedding since deepseek has no embedding model. + # Note: embedding requires litellm_proxy prefix + EMBEDDING_MODEL=litellm_proxy/BAAI/bge-m3 + LITELLM_PROXY_API_KEY= + LITELLM_PROXY_API_BASE=https://api.siliconflow.cn/v1 + ``` + Notice: If you are using reasoning models that include thought processes in their responses (such as \ tags), you need to set the following environment variable: ```bash REASONING_THINK_RM=True diff --git a/docs/installation_and_configuration.rst b/docs/installation_and_configuration.rst index f5b630cc..4f607177 100644 --- a/docs/installation_and_configuration.rst +++ b/docs/installation_and_configuration.rst @@ -16,6 +16,9 @@ Ensure the current user can run Docker commands **without using sudo**. You can LiteLLM Backend Configuration (Default) ======================================= +.. note:: + 🔥 **Attention**: We now provide experimental support for **DeepSeek** models! You can use DeepSeek's official API for cost-effective and high-performance inference. See the configuration example below for DeepSeek setup. + Option 1: Unified API base for both models ------------------------------------------ @@ -48,6 +51,23 @@ Option 2: Separate API bases for Chat and Embedding models LITELLM_PROXY_API_KEY= LITELLM_PROXY_API_BASE=https://api.siliconflow.cn/v1 +Configuration Example: DeepSeek Setup +------------------------------------- + +Many users encounter configuration errors when setting up DeepSeek. Here's a complete working example: + + .. code-block:: Properties + + # CHAT MODEL: Using DeepSeek Official API + CHAT_MODEL=deepseek/deepseek-chat + DEEPSEEK_API_KEY= + + # EMBEDDING MODEL: Using SiliconFlow for embedding since DeepSeek has no embedding model. + # Note: embedding requires litellm_proxy prefix + EMBEDDING_MODEL=litellm_proxy/BAAI/bge-m3 + LITELLM_PROXY_API_KEY= + LITELLM_PROXY_API_BASE=https://api.siliconflow.cn/v1 + Necessary parameters include: - `CHAT_MODEL`: The model name of the chat model. diff --git a/rdagent/components/coder/factor_coder/evolving_strategy.py b/rdagent/components/coder/factor_coder/evolving_strategy.py index e1d93c51..4d79afe5 100644 --- a/rdagent/components/coder/factor_coder/evolving_strategy.py +++ b/rdagent/components/coder/factor_coder/evolving_strategy.py @@ -1,6 +1,7 @@ from __future__ import annotations import json +import re from typing import Dict from rdagent.components.coder.CoSTEER.evaluators import CoSTEERSingleFeedback @@ -136,18 +137,28 @@ class FactorMultiProcessEvolvingStrategy(MultiProcessEvolvingStrategy): queried_similar_error_knowledge_to_render = queried_similar_error_knowledge_to_render[:-1] for _ in range(10): try: - code = json.loads( - APIBackend( - use_chat_cache=FACTOR_COSTEER_SETTINGS.coder_use_cache - ).build_messages_and_create_chat_completion( - user_prompt=user_prompt, - system_prompt=system_prompt, - json_mode=True, - json_target_type=Dict[str, str], - ) - )["code"] + response = APIBackend( + use_chat_cache=FACTOR_COSTEER_SETTINGS.coder_use_cache + ).build_messages_and_create_chat_completion( + user_prompt=user_prompt, + system_prompt=system_prompt, + json_mode=True, + json_target_type=Dict[str, str], + ) + + try: + code = json.loads(response)["code"] + except json.decoder.JSONDecodeError: + # extract python code block + match = re.search(r"```python(.*?)```", response, re.DOTALL) + if match: + code = match.group(1).strip() + else: + raise # continue to retry + return code - except json.decoder.JSONDecodeError: + + except (json.decoder.JSONDecodeError, KeyError): pass else: return "" # return empty code if failed to get code after 10 attempts diff --git a/rdagent/oai/backend/base.py b/rdagent/oai/backend/base.py index 049a34e3..41a8ab29 100644 --- a/rdagent/oai/backend/base.py +++ b/rdagent/oai/backend/base.py @@ -1,15 +1,17 @@ from __future__ import annotations +import io import json import re import sqlite3 import time +import tokenize import uuid from abc import ABC, abstractmethod from copy import deepcopy from datetime import datetime from pathlib import Path -from typing import Any, Optional, cast +from typing import Any, Callable, List, Optional, Tuple, cast import pytz from pydantic import BaseModel, TypeAdapter @@ -31,6 +33,100 @@ except ImportError: openai_imported = False +class JSONParser: + """JSON parser supporting multiple strategies""" + + def __init__(self) -> None: + self.strategies: List[Callable[[str], str]] = [ + self._direct_parse, + self._extract_from_code_block, + self._fix_python_syntax, + self._extract_with_fix_combined, + ] + + def parse(self, content: str) -> str: + """Parse JSON content, automatically trying multiple strategies""" + original_content = content + + for strategy in self.strategies: + try: + return strategy(original_content) + except json.JSONDecodeError: + continue + + # All strategies failed + raise json.JSONDecodeError("Failed to parse JSON after all attempts", original_content, 0) + + def _direct_parse(self, content: str) -> str: + """Strategy 1: Direct parsing (including handling extra data)""" + try: + json.loads(content) + return content + except json.JSONDecodeError as e: + if "Extra data" in str(e): + return self._extract_first_json(content) + raise + + def _extract_from_code_block(self, content: str) -> str: + """Strategy 2: Extract JSON from code block""" + match = re.search(r"```json\s*(.*?)\s*```", content, re.DOTALL) + if not match: + raise json.JSONDecodeError("No JSON code block found", content, 0) + + json_content = match.group(1).strip() + return self._direct_parse(json_content) + + def _fix_python_syntax(self, content: str) -> str: + """Strategy 3: Fix Python syntax before parsing""" + fixed = self._fix_python_booleans(content) + return self._direct_parse(fixed) + + def _extract_with_fix_combined(self, content: str) -> str: + """Strategy 4: Combined strategy - fix Python syntax first, then extract the first JSON object""" + fixed = self._fix_python_booleans(content) + + # Try to extract code block from the fixed content + match = re.search(r"```json\s*(.*?)\s*```", fixed, re.DOTALL) + if match: + fixed = match.group(1).strip() + + return self._direct_parse(fixed) + + @staticmethod + def _fix_python_booleans(json_str: str) -> str: + """Safely fix Python-style booleans to JSON standard format using tokenize""" + replacements = {"True": "true", "False": "false", "None": "null"} + + try: + out = [] + io_string = io.StringIO(json_str) + tokens = tokenize.generate_tokens(io_string.readline) + + for toknum, tokval, _, _, _ in tokens: + if toknum == tokenize.NAME and tokval in replacements: + out.append(replacements[tokval]) + else: + out.append(tokval) + + result = "".join(out) + # Validate if the result is valid JSON + json.loads(result) + return result + + except (tokenize.TokenError, json.JSONDecodeError): + # If tokenize fails, fallback to regex method + for python_val, json_val in replacements.items(): + json_str = re.sub(rf"\\b{python_val}\\b", json_val, json_str) + return json_str + + @staticmethod + def _extract_first_json(response: str) -> str: + """Extract the first complete JSON object, ignoring extra content""" + decoder = json.JSONDecoder() + obj, _ = decoder.raw_decode(response) + return json.dumps(obj) + + class SQliteLazyCache(SingletonBaseClass): def __init__(self, cache_location: str) -> None: super().__init__() @@ -482,12 +578,9 @@ class APIBackend(ABC): # 3) format checking if json_mode: - try: - json.loads(all_response) - except json.decoder.JSONDecodeError: - match = re.search(r"```json(.*)```", all_response, re.DOTALL) - all_response = match.groups()[0] if match else all_response - json.loads(all_response) + parser = JSONParser() + all_response = parser.parse(all_response) + if json_target_type is not None: TypeAdapter(json_target_type).validate_json(all_response) if (response_format := kwargs.get("response_format")) is not None: diff --git a/rdagent/scenarios/qlib/prompts.yaml b/rdagent/scenarios/qlib/prompts.yaml index e53aa1af..b1a147af 100644 --- a/rdagent/scenarios/qlib/prompts.yaml +++ b/rdagent/scenarios/qlib/prompts.yaml @@ -78,7 +78,7 @@ hypothesis_output_format_with_action: |- The output should follow JSON format. The schema is as follows: { "action": "If `hypothesis_specification` provides the action you need to take, please follow "hypothesis_specification" to choose the action. Otherwise, based on previous experimental results, suggest the action you believe is most appropriate at the moment. It should be one of [`factor`, `model`].", - "hypothesis": "The new hypothesis generated based on the information provided.", + "hypothesis": "The new hypothesis generated based on the information provided,should be a string.", "reason": "The reason why you generate this hypothesis. It should be comprehensive and logical. It should cover the other keys below and extend them. Limit in two or three sentences.", }