Spaces:
Running
Running
Upgrade DeepSeek client and improve API reliability
#1
by GuXiaopeng - opened
- app.py +14 -10
- config.py +27 -1
- core/pipeline_agent.py +498 -124
app.py
CHANGED
|
@@ -94,15 +94,19 @@ def clean_trailing_references(text, keywords=("reference", "references", "refere
|
|
| 94 |
def clean_markdown_headings(text):
|
| 95 |
return re.sub(r'(?m)^#+\s+(.*)$', r'**\1**', text)
|
| 96 |
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 101 |
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
except: pass
|
| 106 |
|
| 107 |
WECHAT_BLOCKER_JS = """<script>try{var ua=navigator.userAgent.toLowerCase();if(ua.indexOf('micromessenger')!==-1){var parentDoc=window.parent.document;if(!parentDoc.getElementById('wx-blocker')){var overlay=parentDoc.createElement('div');overlay.id='wx-blocker';overlay.style.cssText='position:fixed;top:0;left:0;width:100vw;height:100vh;background:#1a365d;z-index:9999999;color:white;display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;font-family:sans-serif;padding:30px;box-sizing:border-box;';overlay.innerHTML='<h2 style="font-size:26px;margin-bottom:15px;font-weight:bold;">环境受限 / Restricted</h2><p style="font-size:18px;line-height:1.6;color:#e2e8f0;max-width:100%;">为了保障最终双语 ZIP 压缩包正常下载,<b>本系统禁止在微信内直接运行</b>。<br><br>👉 <b>请点击右上角 [···] 菜单,选择“在浏览器打开” (Open in Browser)</b></p>';parentDoc.body.appendChild(overlay);}}}catch(e){}</script>"""
|
| 108 |
components.html(WECHAT_BLOCKER_JS, height=0, width=0)
|
|
@@ -788,11 +792,11 @@ if st.session_state.start_pipeline or st.session_state.resume_pipeline:
|
|
| 788 |
update_heartbeat()
|
| 789 |
st.write(f"📡 已发起 DeepSeek API 请求...")
|
| 790 |
|
| 791 |
-
target_model = "deepseek-
|
| 792 |
st.write(f"✍️ 核心推导由 R1 完成,当前节点使用 {target_model} 高速执笔...")
|
| 793 |
|
| 794 |
en_draft_clean = re.sub(r'<think>.*?</think>', '', en_ph.write_stream(
|
| 795 |
-
stream_with_heartbeat(agent.stream_deepseek(sys_prompt_m3, base_user, max_tokens=6000, model_name=target_model))
|
| 796 |
), flags=re.DOTALL).strip()
|
| 797 |
|
| 798 |
status.update(label=f"✅ 【{display_name}】撰写完毕!", state="complete", expanded=False)
|
|
|
|
| 94 |
def clean_markdown_headings(text):
|
| 95 |
return re.sub(r'(?m)^#+\s+(.*)$', r'**\1**', text)
|
| 96 |
|
| 97 |
+
def _read_runtime_secret(name):
|
| 98 |
+
# Read a Hugging Face/Streamlit secret and remove copied whitespace.
|
| 99 |
+
value = os.environ.get(name, "")
|
| 100 |
+
if not value:
|
| 101 |
+
try:
|
| 102 |
+
value = st.secrets.get(name, "")
|
| 103 |
+
except Exception:
|
| 104 |
+
value = ""
|
| 105 |
+
return str(value or "").strip()
|
| 106 |
|
| 107 |
+
|
| 108 |
+
global_ds_key = _read_runtime_secret("DEEPSEEK_API_KEY")
|
| 109 |
+
global_s2_key = _read_runtime_secret("S2_API_KEY")
|
|
|
|
| 110 |
|
| 111 |
WECHAT_BLOCKER_JS = """<script>try{var ua=navigator.userAgent.toLowerCase();if(ua.indexOf('micromessenger')!==-1){var parentDoc=window.parent.document;if(!parentDoc.getElementById('wx-blocker')){var overlay=parentDoc.createElement('div');overlay.id='wx-blocker';overlay.style.cssText='position:fixed;top:0;left:0;width:100vw;height:100vh;background:#1a365d;z-index:9999999;color:white;display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;font-family:sans-serif;padding:30px;box-sizing:border-box;';overlay.innerHTML='<h2 style="font-size:26px;margin-bottom:15px;font-weight:bold;">环境受限 / Restricted</h2><p style="font-size:18px;line-height:1.6;color:#e2e8f0;max-width:100%;">为了保障最终双语 ZIP 压缩包正常下载,<b>本系统禁止在微信内直接运行</b>。<br><br>👉 <b>请点击右上角 [···] 菜单,选择“在浏览器打开” (Open in Browser)</b></p>';parentDoc.body.appendChild(overlay);}}}catch(e){}</script>"""
|
| 112 |
components.html(WECHAT_BLOCKER_JS, height=0, width=0)
|
|
|
|
| 792 |
update_heartbeat()
|
| 793 |
st.write(f"📡 已发起 DeepSeek API 请求...")
|
| 794 |
|
| 795 |
+
target_model = "deepseek-v4-flash"
|
| 796 |
st.write(f"✍️ 核心推导由 R1 完成,当前节点使用 {target_model} 高速执笔...")
|
| 797 |
|
| 798 |
en_draft_clean = re.sub(r'<think>.*?</think>', '', en_ph.write_stream(
|
| 799 |
+
stream_with_heartbeat(agent.stream_deepseek(sys_prompt_m3, base_user, max_tokens=6000, model_name=target_model, thinking=False))
|
| 800 |
), flags=re.DOTALL).strip()
|
| 801 |
|
| 802 |
status.update(label=f"✅ 【{display_name}】撰写完毕!", state="complete", expanded=False)
|
config.py
CHANGED
|
@@ -22,7 +22,33 @@ def load_config():
|
|
| 22 |
},
|
| 23 |
|
| 24 |
# --- API 节点配置 ---
|
| 25 |
-
"deepseek_api_url": os.environ.get(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
"openalex_api_url": os.environ.get("OPENALEX_API_URL", "https://api.openalex.org/works"),
|
| 27 |
"s2_api_url": os.environ.get("S2_API_URL", "https://api.semanticscholar.org/graph/v1"),
|
| 28 |
|
|
|
|
| 22 |
},
|
| 23 |
|
| 24 |
# --- API 节点配置 ---
|
| 25 |
+
"deepseek_api_url": os.environ.get(
|
| 26 |
+
"DEEPSEEK_API_URL",
|
| 27 |
+
"https://api.deepseek.com/chat/completions",
|
| 28 |
+
),
|
| 29 |
+
"deepseek_default_model": os.environ.get(
|
| 30 |
+
"DEEPSEEK_DEFAULT_MODEL",
|
| 31 |
+
"deepseek-v4-flash",
|
| 32 |
+
),
|
| 33 |
+
"deepseek_fallback_model": os.environ.get(
|
| 34 |
+
"DEEPSEEK_FALLBACK_MODEL",
|
| 35 |
+
"deepseek-v4-flash",
|
| 36 |
+
),
|
| 37 |
+
"deepseek_default_thinking": os.environ.get(
|
| 38 |
+
"DEEPSEEK_DEFAULT_THINKING",
|
| 39 |
+
"true",
|
| 40 |
+
).strip().lower() in {"1", "true", "yes", "on"},
|
| 41 |
+
"deepseek_reasoning_effort": os.environ.get(
|
| 42 |
+
"DEEPSEEK_REASONING_EFFORT",
|
| 43 |
+
"high",
|
| 44 |
+
),
|
| 45 |
+
"deepseek_max_retries": int(
|
| 46 |
+
os.environ.get("DEEPSEEK_MAX_RETRIES", "3")
|
| 47 |
+
),
|
| 48 |
+
"deepseek_show_reasoning": os.environ.get(
|
| 49 |
+
"DEEPSEEK_SHOW_REASONING",
|
| 50 |
+
"false",
|
| 51 |
+
).strip().lower() in {"1", "true", "yes", "on"},
|
| 52 |
"openalex_api_url": os.environ.get("OPENALEX_API_URL", "https://api.openalex.org/works"),
|
| 53 |
"s2_api_url": os.environ.get("S2_API_URL", "https://api.semanticscholar.org/graph/v1"),
|
| 54 |
|
core/pipeline_agent.py
CHANGED
|
@@ -25,9 +25,31 @@ except ImportError:
|
|
| 25 |
|
| 26 |
from config import APP_CONFIG
|
| 27 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
class SSciPipelineAgent:
|
| 29 |
def __init__(self, ds_key, s2_key=""):
|
| 30 |
-
self.ds_key = ds_key
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
try:
|
| 32 |
self.s2_key = st.secrets.get("S2_API_KEY", s2_key)
|
| 33 |
except:
|
|
@@ -55,6 +77,24 @@ class SSciPipelineAgent:
|
|
| 55 |
self.chunk_words_cyrillic = APP_CONFIG["chunk_words_cyrillic"]
|
| 56 |
|
| 57 |
self.deepseek_api_url = APP_CONFIG["deepseek_api_url"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 58 |
self.openalex_api_url = APP_CONFIG["openalex_api_url"]
|
| 59 |
self.s2_api_url = APP_CONFIG["s2_api_url"]
|
| 60 |
self.timeouts = APP_CONFIG["api_timeouts"]
|
|
@@ -255,138 +295,472 @@ class SSciPipelineAgent:
|
|
| 255 |
return self._truncate_context(full, max_tokens)
|
| 256 |
return full
|
| 257 |
|
| 258 |
-
def
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 264 |
self.ds_calls += 1
|
| 265 |
-
|
| 266 |
-
if
|
| 267 |
-
current_model =
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 281 |
start_time = time.time()
|
|
|
|
| 282 |
try:
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 301 |
elapsed = time.time() - start_time
|
| 302 |
-
logging.info(
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
|
| 316 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 317 |
self.ds_calls += 1
|
| 318 |
-
|
| 319 |
-
if
|
| 320 |
-
current_model =
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
|
| 328 |
-
|
| 329 |
-
|
| 330 |
-
|
| 331 |
-
|
| 332 |
-
|
| 333 |
-
|
| 334 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 335 |
start_time = time.time()
|
| 336 |
-
|
| 337 |
try:
|
| 338 |
-
with requests.post(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 339 |
if response.status_code != 200:
|
| 340 |
-
|
| 341 |
-
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
|
| 347 |
-
|
| 348 |
-
|
| 349 |
-
|
| 350 |
-
|
| 351 |
-
|
| 352 |
-
|
| 353 |
-
|
| 354 |
-
|
| 355 |
-
|
| 356 |
-
|
| 357 |
-
|
| 358 |
-
|
| 359 |
-
|
| 360 |
-
|
| 361 |
-
|
| 362 |
-
|
| 363 |
-
|
| 364 |
-
|
| 365 |
-
|
| 366 |
-
|
| 367 |
-
|
| 368 |
-
|
| 369 |
-
|
| 370 |
-
|
| 371 |
-
|
| 372 |
-
|
| 373 |
-
|
| 374 |
-
|
| 375 |
-
|
| 376 |
-
|
| 377 |
-
|
| 378 |
-
|
| 379 |
-
|
| 380 |
-
|
| 381 |
-
|
| 382 |
-
|
| 383 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 384 |
raise
|
| 385 |
-
except
|
| 386 |
-
|
| 387 |
-
|
| 388 |
-
|
| 389 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 390 |
|
| 391 |
def parse_user_core_literature(self, user_lit_text):
|
| 392 |
if not user_lit_text.strip():
|
|
|
|
| 25 |
|
| 26 |
from config import APP_CONFIG
|
| 27 |
|
| 28 |
+
|
| 29 |
+
class DeepSeekAPIError(RuntimeError):
|
| 30 |
+
pass
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
class DeepSeekAuthenticationError(DeepSeekAPIError):
|
| 34 |
+
pass
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
class DeepSeekBalanceError(DeepSeekAPIError):
|
| 38 |
+
pass
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
class DeepSeekRequestError(DeepSeekAPIError):
|
| 42 |
+
pass
|
| 43 |
+
|
| 44 |
+
|
| 45 |
class SSciPipelineAgent:
|
| 46 |
def __init__(self, ds_key, s2_key=""):
|
| 47 |
+
self.ds_key = str(ds_key or "").strip()
|
| 48 |
+
if not self.ds_key:
|
| 49 |
+
raise DeepSeekAuthenticationError(
|
| 50 |
+
"DEEPSEEK_API_KEY is empty. Configure it in Hugging Face "
|
| 51 |
+
"Space Settings > Variables and secrets."
|
| 52 |
+
)
|
| 53 |
try:
|
| 54 |
self.s2_key = st.secrets.get("S2_API_KEY", s2_key)
|
| 55 |
except:
|
|
|
|
| 77 |
self.chunk_words_cyrillic = APP_CONFIG["chunk_words_cyrillic"]
|
| 78 |
|
| 79 |
self.deepseek_api_url = APP_CONFIG["deepseek_api_url"]
|
| 80 |
+
self.deepseek_default_model = str(
|
| 81 |
+
APP_CONFIG.get("deepseek_default_model", "deepseek-v4-flash")
|
| 82 |
+
).strip()
|
| 83 |
+
self.deepseek_fallback_model = str(
|
| 84 |
+
APP_CONFIG.get("deepseek_fallback_model", "deepseek-v4-flash")
|
| 85 |
+
).strip()
|
| 86 |
+
self.deepseek_default_thinking = bool(
|
| 87 |
+
APP_CONFIG.get("deepseek_default_thinking", True)
|
| 88 |
+
)
|
| 89 |
+
self.deepseek_reasoning_effort = str(
|
| 90 |
+
APP_CONFIG.get("deepseek_reasoning_effort", "high")
|
| 91 |
+
).strip().lower()
|
| 92 |
+
self.deepseek_max_retries = max(
|
| 93 |
+
1, int(APP_CONFIG.get("deepseek_max_retries", 3))
|
| 94 |
+
)
|
| 95 |
+
self.deepseek_show_reasoning = bool(
|
| 96 |
+
APP_CONFIG.get("deepseek_show_reasoning", False)
|
| 97 |
+
)
|
| 98 |
self.openalex_api_url = APP_CONFIG["openalex_api_url"]
|
| 99 |
self.s2_api_url = APP_CONFIG["s2_api_url"]
|
| 100 |
self.timeouts = APP_CONFIG["api_timeouts"]
|
|
|
|
| 295 |
return self._truncate_context(full, max_tokens)
|
| 296 |
return full
|
| 297 |
|
| 298 |
+
def _resolve_deepseek_model(self, model_name=None, thinking=None):
|
| 299 |
+
# Legacy names are accepted only as internal aliases.
|
| 300 |
+
requested = str(
|
| 301 |
+
model_name or self.deepseek_default_model
|
| 302 |
+
).strip().lower()
|
| 303 |
+
|
| 304 |
+
legacy_aliases = {
|
| 305 |
+
"deepseek-chat": ("deepseek-v4-flash", False),
|
| 306 |
+
"deepseek-reasoner": ("deepseek-v4-flash", True),
|
| 307 |
+
}
|
| 308 |
+
if requested in legacy_aliases:
|
| 309 |
+
public_model, alias_thinking = legacy_aliases[requested]
|
| 310 |
+
if thinking is None:
|
| 311 |
+
thinking = alias_thinking
|
| 312 |
+
return public_model, bool(thinking)
|
| 313 |
+
|
| 314 |
+
allowed_models = {"deepseek-v4-flash", "deepseek-v4-pro"}
|
| 315 |
+
if requested not in allowed_models:
|
| 316 |
+
raise DeepSeekRequestError(
|
| 317 |
+
f"Unsupported DeepSeek model: {requested!r}. "
|
| 318 |
+
"Use 'deepseek-v4-flash' or 'deepseek-v4-pro'."
|
| 319 |
+
)
|
| 320 |
+
|
| 321 |
+
if thinking is None:
|
| 322 |
+
thinking = self.deepseek_default_thinking
|
| 323 |
+
|
| 324 |
+
return requested, bool(thinking)
|
| 325 |
+
|
| 326 |
+
def _build_deepseek_payload(
|
| 327 |
+
self,
|
| 328 |
+
system_prompt,
|
| 329 |
+
user_prompt,
|
| 330 |
+
*,
|
| 331 |
+
model_name,
|
| 332 |
+
thinking,
|
| 333 |
+
max_tokens,
|
| 334 |
+
temp,
|
| 335 |
+
stream,
|
| 336 |
+
reasoning_effort,
|
| 337 |
+
):
|
| 338 |
+
effort = str(
|
| 339 |
+
reasoning_effort or self.deepseek_reasoning_effort
|
| 340 |
+
).strip().lower()
|
| 341 |
+
if effort not in {"high", "max"}:
|
| 342 |
+
effort = "high"
|
| 343 |
+
|
| 344 |
+
payload = {
|
| 345 |
+
"model": model_name,
|
| 346 |
+
"messages": [
|
| 347 |
+
{"role": "system", "content": str(system_prompt)},
|
| 348 |
+
{"role": "user", "content": str(user_prompt)},
|
| 349 |
+
],
|
| 350 |
+
"max_tokens": int(max_tokens),
|
| 351 |
+
"stream": bool(stream),
|
| 352 |
+
"thinking": {
|
| 353 |
+
"type": "enabled" if thinking else "disabled"
|
| 354 |
+
},
|
| 355 |
+
}
|
| 356 |
+
|
| 357 |
+
if thinking:
|
| 358 |
+
payload["reasoning_effort"] = effort
|
| 359 |
+
else:
|
| 360 |
+
payload["temperature"] = max(
|
| 361 |
+
0.0, min(float(temp), 2.0)
|
| 362 |
+
)
|
| 363 |
+
|
| 364 |
+
return payload
|
| 365 |
+
|
| 366 |
+
@staticmethod
|
| 367 |
+
def _deepseek_headers(api_key):
|
| 368 |
+
return {
|
| 369 |
+
"Authorization": f"Bearer {api_key}",
|
| 370 |
+
"Content-Type": "application/json",
|
| 371 |
+
"Accept": "application/json",
|
| 372 |
+
"User-Agent": "SmartPaper/DeepSeekV4",
|
| 373 |
+
}
|
| 374 |
+
|
| 375 |
+
@staticmethod
|
| 376 |
+
def _safe_response_text(response, limit=600):
|
| 377 |
+
try:
|
| 378 |
+
return response.text[:limit]
|
| 379 |
+
except Exception:
|
| 380 |
+
return ""
|
| 381 |
+
|
| 382 |
+
def _raise_for_deepseek_status(self, response):
|
| 383 |
+
status = int(response.status_code)
|
| 384 |
+
detail = self._safe_response_text(response)
|
| 385 |
+
|
| 386 |
+
if status == 401:
|
| 387 |
+
raise DeepSeekAuthenticationError(
|
| 388 |
+
"DeepSeek authentication failed (401). "
|
| 389 |
+
"Check DEEPSEEK_API_KEY in Hugging Face Space Secrets."
|
| 390 |
+
)
|
| 391 |
+
if status == 402:
|
| 392 |
+
raise DeepSeekBalanceError(
|
| 393 |
+
"DeepSeek account balance is insufficient (402)."
|
| 394 |
+
)
|
| 395 |
+
if status == 400:
|
| 396 |
+
raise DeepSeekRequestError(
|
| 397 |
+
f"DeepSeek rejected the request format (400): {detail}"
|
| 398 |
+
)
|
| 399 |
+
if status == 404:
|
| 400 |
+
raise DeepSeekRequestError(
|
| 401 |
+
f"DeepSeek model or endpoint was not found (404): {detail}"
|
| 402 |
+
)
|
| 403 |
+
if status == 422:
|
| 404 |
+
raise DeepSeekRequestError(
|
| 405 |
+
f"DeepSeek request parameters are invalid (422): {detail}"
|
| 406 |
+
)
|
| 407 |
+
|
| 408 |
+
if status in {408, 409, 425, 429, 500, 502, 503, 504}:
|
| 409 |
+
raise requests.exceptions.HTTPError(
|
| 410 |
+
f"Transient DeepSeek API failure ({status}): {detail}",
|
| 411 |
+
response=response,
|
| 412 |
+
)
|
| 413 |
+
|
| 414 |
+
raise DeepSeekRequestError(
|
| 415 |
+
f"Unexpected DeepSeek API status {status}: {detail}"
|
| 416 |
+
)
|
| 417 |
+
|
| 418 |
+
@staticmethod
|
| 419 |
+
def _retry_delay(attempt_index):
|
| 420 |
+
base = min(2 ** attempt_index, 8)
|
| 421 |
+
return base + random.uniform(0.25, 1.25)
|
| 422 |
+
|
| 423 |
+
@staticmethod
|
| 424 |
+
def _parse_sse_line(raw_line):
|
| 425 |
+
if raw_line is None:
|
| 426 |
+
return None
|
| 427 |
+
if isinstance(raw_line, bytes):
|
| 428 |
+
line = raw_line.decode("utf-8", errors="replace").strip()
|
| 429 |
+
else:
|
| 430 |
+
line = str(raw_line).strip()
|
| 431 |
+
|
| 432 |
+
if not line or not line.startswith("data:"):
|
| 433 |
+
return None
|
| 434 |
+
|
| 435 |
+
data = line[5:].strip()
|
| 436 |
+
if not data:
|
| 437 |
+
return None
|
| 438 |
+
if data == "[DONE]":
|
| 439 |
+
return {"done": True}
|
| 440 |
+
|
| 441 |
+
try:
|
| 442 |
+
return json.loads(data)
|
| 443 |
+
except json.JSONDecodeError:
|
| 444 |
+
logging.warning(
|
| 445 |
+
"⚠️ Ignored malformed DeepSeek SSE chunk: %s",
|
| 446 |
+
data[:160],
|
| 447 |
+
)
|
| 448 |
+
return None
|
| 449 |
+
|
| 450 |
+
def _attempt_plan(self, requested_model, requested_thinking):
|
| 451 |
+
total = self.deepseek_max_retries
|
| 452 |
+
for attempt in range(total):
|
| 453 |
+
model = requested_model
|
| 454 |
+
thinking = requested_thinking
|
| 455 |
+
|
| 456 |
+
if attempt == total - 1:
|
| 457 |
+
if model != self.deepseek_fallback_model:
|
| 458 |
+
model = self.deepseek_fallback_model
|
| 459 |
+
if thinking:
|
| 460 |
+
thinking = False
|
| 461 |
+
|
| 462 |
+
yield attempt, model, thinking
|
| 463 |
+
|
| 464 |
+
def call_deepseek(
|
| 465 |
+
self,
|
| 466 |
+
system_prompt,
|
| 467 |
+
user_prompt,
|
| 468 |
+
max_tokens=8000,
|
| 469 |
+
temp=0.3,
|
| 470 |
+
model_name="deepseek-reasoner",
|
| 471 |
+
thinking=None,
|
| 472 |
+
reasoning_effort=None,
|
| 473 |
+
):
|
| 474 |
+
requested_model, requested_thinking = (
|
| 475 |
+
self._resolve_deepseek_model(model_name, thinking)
|
| 476 |
+
)
|
| 477 |
+
tokens_est = self._count_tokens(str(user_prompt))
|
| 478 |
+
|
| 479 |
+
logging.info(
|
| 480 |
+
"🚀 [DeepSeek 静态请求] 模型=%s | Thinking=%s | "
|
| 481 |
+
"MaxTokens=%s | 输入估算=%s tokens",
|
| 482 |
+
requested_model,
|
| 483 |
+
requested_thinking,
|
| 484 |
+
max_tokens,
|
| 485 |
+
tokens_est,
|
| 486 |
+
)
|
| 487 |
+
|
| 488 |
+
last_error = None
|
| 489 |
+
|
| 490 |
+
for attempt, current_model, current_thinking in self._attempt_plan(
|
| 491 |
+
requested_model, requested_thinking
|
| 492 |
+
):
|
| 493 |
+
with self.lock:
|
| 494 |
self.ds_calls += 1
|
| 495 |
+
|
| 496 |
+
if (
|
| 497 |
+
current_model != requested_model
|
| 498 |
+
or current_thinking != requested_thinking
|
| 499 |
+
):
|
| 500 |
+
logging.warning(
|
| 501 |
+
"⚠️ 最后一次尝试降级:模型=%s | Thinking=%s",
|
| 502 |
+
current_model,
|
| 503 |
+
current_thinking,
|
| 504 |
+
)
|
| 505 |
+
|
| 506 |
+
payload = self._build_deepseek_payload(
|
| 507 |
+
system_prompt,
|
| 508 |
+
user_prompt,
|
| 509 |
+
model_name=current_model,
|
| 510 |
+
thinking=current_thinking,
|
| 511 |
+
max_tokens=max_tokens,
|
| 512 |
+
temp=temp,
|
| 513 |
+
stream=False,
|
| 514 |
+
reasoning_effort=reasoning_effort,
|
| 515 |
+
)
|
| 516 |
start_time = time.time()
|
| 517 |
+
|
| 518 |
try:
|
| 519 |
+
response = requests.post(
|
| 520 |
+
self.deepseek_api_url,
|
| 521 |
+
headers=self._deepseek_headers(self.ds_key),
|
| 522 |
+
json=payload,
|
| 523 |
+
timeout=self.timeouts["standard"],
|
| 524 |
+
)
|
| 525 |
+
|
| 526 |
+
if response.status_code != 200:
|
| 527 |
+
self._raise_for_deepseek_status(response)
|
| 528 |
+
|
| 529 |
+
try:
|
| 530 |
+
result = response.json()
|
| 531 |
+
except ValueError as exc:
|
| 532 |
+
raise DeepSeekAPIError(
|
| 533 |
+
"DeepSeek returned a non-JSON success response."
|
| 534 |
+
) from exc
|
| 535 |
+
|
| 536 |
+
choices = result.get("choices") or []
|
| 537 |
+
if not choices:
|
| 538 |
+
raise DeepSeekAPIError(
|
| 539 |
+
"DeepSeek returned no choices."
|
| 540 |
+
)
|
| 541 |
+
|
| 542 |
+
message = choices[0].get("message") or {}
|
| 543 |
+
content = str(message.get("content") or "").strip()
|
| 544 |
+
|
| 545 |
+
if not content:
|
| 546 |
+
finish_reason = choices[0].get("finish_reason")
|
| 547 |
+
raise DeepSeekAPIError(
|
| 548 |
+
"DeepSeek returned empty final content. "
|
| 549 |
+
f"finish_reason={finish_reason!r}"
|
| 550 |
+
)
|
| 551 |
+
|
| 552 |
elapsed = time.time() - start_time
|
| 553 |
+
logging.info(
|
| 554 |
+
"✅ [静态请求返回] 耗时=%.2fs | 输出长度=%s 字符",
|
| 555 |
+
elapsed,
|
| 556 |
+
len(content),
|
| 557 |
+
)
|
| 558 |
+
return content
|
| 559 |
+
|
| 560 |
+
except (
|
| 561 |
+
DeepSeekAuthenticationError,
|
| 562 |
+
DeepSeekBalanceError,
|
| 563 |
+
DeepSeekRequestError,
|
| 564 |
+
):
|
| 565 |
+
raise
|
| 566 |
+
except (
|
| 567 |
+
requests.exceptions.Timeout,
|
| 568 |
+
requests.exceptions.ConnectionError,
|
| 569 |
+
requests.exceptions.HTTPError,
|
| 570 |
+
DeepSeekAPIError,
|
| 571 |
+
) as exc:
|
| 572 |
+
last_error = exc
|
| 573 |
+
is_last = attempt == self.deepseek_max_retries - 1
|
| 574 |
+
logging.error(
|
| 575 |
+
"❌ DeepSeek 尝试 %s/%s 失败: %s",
|
| 576 |
+
attempt + 1,
|
| 577 |
+
self.deepseek_max_retries,
|
| 578 |
+
exc,
|
| 579 |
+
)
|
| 580 |
+
if is_last:
|
| 581 |
+
break
|
| 582 |
+
time.sleep(self._retry_delay(attempt))
|
| 583 |
+
|
| 584 |
+
raise DeepSeekAPIError(
|
| 585 |
+
f"DeepSeek API failed after "
|
| 586 |
+
f"{self.deepseek_max_retries} attempts: {last_error}"
|
| 587 |
+
)
|
| 588 |
+
|
| 589 |
+
def stream_deepseek(
|
| 590 |
+
self,
|
| 591 |
+
system_prompt,
|
| 592 |
+
user_prompt,
|
| 593 |
+
max_tokens=8000,
|
| 594 |
+
model_name="deepseek-reasoner",
|
| 595 |
+
thinking=None,
|
| 596 |
+
reasoning_effort=None,
|
| 597 |
+
temp=0.3,
|
| 598 |
+
):
|
| 599 |
+
requested_model, requested_thinking = (
|
| 600 |
+
self._resolve_deepseek_model(model_name, thinking)
|
| 601 |
+
)
|
| 602 |
+
tokens_est = self._count_tokens(str(user_prompt))
|
| 603 |
+
|
| 604 |
+
logging.info(
|
| 605 |
+
"🚀 [DeepSeek 流式请求] 模型=%s | Thinking=%s | "
|
| 606 |
+
"MaxTokens=%s | 输入估算=%s tokens",
|
| 607 |
+
requested_model,
|
| 608 |
+
requested_thinking,
|
| 609 |
+
max_tokens,
|
| 610 |
+
tokens_est,
|
| 611 |
+
)
|
| 612 |
+
|
| 613 |
+
last_error = None
|
| 614 |
+
|
| 615 |
+
for attempt, current_model, current_thinking in self._attempt_plan(
|
| 616 |
+
requested_model, requested_thinking
|
| 617 |
+
):
|
| 618 |
+
with self.lock:
|
| 619 |
self.ds_calls += 1
|
| 620 |
+
|
| 621 |
+
if (
|
| 622 |
+
current_model != requested_model
|
| 623 |
+
or current_thinking != requested_thinking
|
| 624 |
+
):
|
| 625 |
+
logging.warning(
|
| 626 |
+
"⚠️ 最后一次尝试降级:模型=%s | Thinking=%s",
|
| 627 |
+
current_model,
|
| 628 |
+
current_thinking,
|
| 629 |
+
)
|
| 630 |
+
|
| 631 |
+
payload = self._build_deepseek_payload(
|
| 632 |
+
system_prompt,
|
| 633 |
+
user_prompt,
|
| 634 |
+
model_name=current_model,
|
| 635 |
+
thinking=current_thinking,
|
| 636 |
+
max_tokens=max_tokens,
|
| 637 |
+
temp=temp,
|
| 638 |
+
stream=True,
|
| 639 |
+
reasoning_effort=reasoning_effort,
|
| 640 |
+
)
|
| 641 |
+
|
| 642 |
+
emitted_content = False
|
| 643 |
+
reasoning_open = False
|
| 644 |
+
reasoning_chunks = 0
|
| 645 |
start_time = time.time()
|
| 646 |
+
|
| 647 |
try:
|
| 648 |
+
with requests.post(
|
| 649 |
+
self.deepseek_api_url,
|
| 650 |
+
headers=self._deepseek_headers(self.ds_key),
|
| 651 |
+
json=payload,
|
| 652 |
+
stream=True,
|
| 653 |
+
timeout=self.timeouts["stream"],
|
| 654 |
+
) as response:
|
| 655 |
if response.status_code != 200:
|
| 656 |
+
self._raise_for_deepseek_status(response)
|
| 657 |
+
|
| 658 |
+
for raw_line in response.iter_lines(
|
| 659 |
+
decode_unicode=True
|
| 660 |
+
):
|
| 661 |
+
event = self._parse_sse_line(raw_line)
|
| 662 |
+
if not event:
|
| 663 |
+
continue
|
| 664 |
+
if event.get("done"):
|
| 665 |
+
break
|
| 666 |
+
|
| 667 |
+
choices = event.get("choices") or []
|
| 668 |
+
if not choices:
|
| 669 |
+
continue
|
| 670 |
+
|
| 671 |
+
delta = choices[0].get("delta") or {}
|
| 672 |
+
reasoning_content = (
|
| 673 |
+
delta.get("reasoning_content") or ""
|
| 674 |
+
)
|
| 675 |
+
content = delta.get("content") or ""
|
| 676 |
+
|
| 677 |
+
if reasoning_content:
|
| 678 |
+
reasoning_chunks += 1
|
| 679 |
+
if not reasoning_open:
|
| 680 |
+
reasoning_open = True
|
| 681 |
+
logging.info(
|
| 682 |
+
"🧠 DeepSeek thinking started."
|
| 683 |
+
)
|
| 684 |
+
if self.deepseek_show_reasoning:
|
| 685 |
+
yield "\n<think>\n"
|
| 686 |
+
|
| 687 |
+
if self.deepseek_show_reasoning:
|
| 688 |
+
yield str(reasoning_content)
|
| 689 |
+
else:
|
| 690 |
+
yield ""
|
| 691 |
+
|
| 692 |
+
if reasoning_chunks % 25 == 0:
|
| 693 |
+
logging.info(
|
| 694 |
+
"⏳ DeepSeek thinking heartbeat: "
|
| 695 |
+
"%.1fs, %s chunks",
|
| 696 |
+
time.time() - start_time,
|
| 697 |
+
reasoning_chunks,
|
| 698 |
+
)
|
| 699 |
+
|
| 700 |
+
if content:
|
| 701 |
+
if (
|
| 702 |
+
reasoning_open
|
| 703 |
+
and self.deepseek_show_reasoning
|
| 704 |
+
):
|
| 705 |
+
yield "\n</think>\n\n"
|
| 706 |
+
reasoning_open = False
|
| 707 |
+
emitted_content = True
|
| 708 |
+
yield str(content)
|
| 709 |
+
|
| 710 |
+
if (
|
| 711 |
+
reasoning_open
|
| 712 |
+
and self.deepseek_show_reasoning
|
| 713 |
+
):
|
| 714 |
+
yield "\n</think>\n"
|
| 715 |
+
|
| 716 |
+
if not emitted_content:
|
| 717 |
+
raise DeepSeekAPIError(
|
| 718 |
+
"DeepSeek stream ended without final content."
|
| 719 |
+
)
|
| 720 |
+
|
| 721 |
+
logging.info(
|
| 722 |
+
"✅ [流式生成结束] 总耗时=%.1fs",
|
| 723 |
+
time.time() - start_time,
|
| 724 |
+
)
|
| 725 |
+
return
|
| 726 |
+
|
| 727 |
+
except GeneratorExit:
|
| 728 |
raise
|
| 729 |
+
except (
|
| 730 |
+
DeepSeekAuthenticationError,
|
| 731 |
+
DeepSeekBalanceError,
|
| 732 |
+
DeepSeekRequestError,
|
| 733 |
+
):
|
| 734 |
+
raise
|
| 735 |
+
except (
|
| 736 |
+
requests.exceptions.Timeout,
|
| 737 |
+
requests.exceptions.ConnectionError,
|
| 738 |
+
requests.exceptions.HTTPError,
|
| 739 |
+
DeepSeekAPIError,
|
| 740 |
+
) as exc:
|
| 741 |
+
last_error = exc
|
| 742 |
+
logging.error(
|
| 743 |
+
"❌ DeepSeek 流式尝试 %s/%s 失败: %s",
|
| 744 |
+
attempt + 1,
|
| 745 |
+
self.deepseek_max_retries,
|
| 746 |
+
exc,
|
| 747 |
+
)
|
| 748 |
+
|
| 749 |
+
if emitted_content:
|
| 750 |
+
raise DeepSeekAPIError(
|
| 751 |
+
"DeepSeek stream was interrupted after partial "
|
| 752 |
+
"content had already been emitted."
|
| 753 |
+
) from exc
|
| 754 |
+
|
| 755 |
+
is_last = attempt == self.deepseek_max_retries - 1
|
| 756 |
+
if is_last:
|
| 757 |
+
break
|
| 758 |
+
time.sleep(self._retry_delay(attempt))
|
| 759 |
+
|
| 760 |
+
raise DeepSeekAPIError(
|
| 761 |
+
f"DeepSeek streaming failed after "
|
| 762 |
+
f"{self.deepseek_max_retries} attempts: {last_error}"
|
| 763 |
+
)
|
| 764 |
|
| 765 |
def parse_user_core_literature(self, user_lit_text):
|
| 766 |
if not user_lit_text.strip():
|