fix(chat): simplify header cards and llm fallback

This commit is contained in:
2026-06-04 22:42:39 +08:00
parent fecaee0b03
commit 5a6e7698e4
4 changed files with 87 additions and 50 deletions

View File

@@ -1,7 +1,7 @@
from dataclasses import dataclass
import json
import os
from urllib.error import URLError
from urllib.error import HTTPError, URLError
from urllib.request import Request, urlopen
@@ -75,12 +75,28 @@ class OpenAICompatibleProvider:
if response_format:
payload["response_format"] = response_format
try:
data = _post_json(
base_url=self.base_url,
endpoint="chat/completions",
api_key=self.api_key,
payload=payload,
)
try:
data = _post_json(
base_url=self.base_url,
endpoint="chat/completions",
api_key=self.api_key,
payload=payload,
)
except RuntimeError as exc:
# 部分 OpenAI 兼容供应商或模型不支持 response_format。
# 保留结构化优先,遇到 400 时退回普通对话,避免演示链路被接口能力差异阻断。
if not response_format or "HTTP Error 400" not in str(exc):
raise
fallback_payload = {
"model": self.model_name,
"messages": messages,
}
data = _post_json(
base_url=self.base_url,
endpoint="chat/completions",
api_key=self.api_key,
payload=fallback_payload,
)
choice = data.get("choices", [{}])[0]
content = choice.get("message", {}).get("content", "")
return LLMResponse(
@@ -197,5 +213,11 @@ def _post_json(base_url: str, endpoint: str, api_key: str, payload: dict) -> dic
try:
with urlopen(request, timeout=60) as response:
return json.loads(response.read().decode("utf-8"))
except HTTPError as exc:
error_body = exc.read().decode("utf-8", errors="ignore")
error_detail = f"{exc}"
if error_body:
error_detail = f"{error_detail} {error_body}"
raise RuntimeError(f"OpenAI 兼容接口调用失败:{error_detail}") from exc
except URLError as exc:
raise RuntimeError(f"OpenAI 兼容接口调用失败:{exc}") from exc

View File

@@ -359,33 +359,6 @@
{% endif %}
</section>
{% if conversation %}
<section class="grid-2">
<article class="panel">
<h2 class="section-title">顶部对话上下文</h2>
<p class="section-copy">进入会话后,先用当前批次、产品和风险状态快速建立审核上下文。</p>
<ul class="detail-list">
<li class="detail-item"><strong>批次编号</strong><div>{{ conversation_context.batch_id }}</div></li>
<li class="detail-item"><strong>产品名称</strong><div>{{ conversation_context.product_name|default:"未识别产品名称" }}</div></li>
<li class="detail-item"><strong>当前流程类型</strong><div>{{ conversation_context.workflow_type }}</div></li>
<li class="detail-item"><strong>当前审核阶段</strong><div>{{ conversation_context.task_status }}</div></li>
<li class="detail-item"><strong>当前最高风险等级</strong><div>{{ conversation_context.highest_risk_level }}</div></li>
<li class="detail-item"><strong>是否允许正式导出</strong><div>{{ conversation_context.export_allowed }}</div></li>
</ul>
</article>
<article class="panel">
<h2 class="section-title">推荐提问模板</h2>
<p class="section-copy">用这些提问模板快速进入目录汇总、完整性检查、字段抽取和风险分析。</p>
<div class="button-row">
{% for item in prompt_templates %}
<span class="pill pill-accent">{{ item }}</span>
{% endfor %}
</div>
</article>
</section>
{% endif %}
<section class="chat-layout">
<input class="history-toggle" id="history-collapsed" type="checkbox">
<aside class="panel history-panel">

View File

@@ -526,7 +526,7 @@ def test_chat_page_shows_upload_entry_and_dynamic_context_cards(client, db):
assert "飞书通知 / 待处理" in content
def test_chat_page_shows_top_context_and_recommended_prompts(client, db):
def test_chat_page_keeps_prompts_inside_chat_thread_without_top_cards(client, db):
batch, conversation = _create_conversation_with_batch()
conversation.task_status = "processing"
conversation.node_results = [
@@ -545,13 +545,10 @@ def test_chat_page_shows_top_context_and_recommended_prompts(client, db):
content = response.content.decode("utf-8")
assert response.status_code == 200
assert "顶部对话上下文" in content
assert "当前流程类型" in content
assert "registration" in content
assert "当前审核阶段" in content
assert "顶部对话上下文" not in content
assert "推荐提问模板" not in content
assert "对话区与节点导航" in content
assert "处理中" in content
assert "当前最高风险等级" in content
assert "推荐提问模板" in content
assert "请汇总当前资料包的章节点、页数和目录覆盖情况" in content
assert "请给出当前资料包的高风险项、责任人和整改建议" in content
@@ -583,16 +580,12 @@ def test_chat_page_explicitly_shows_batch_product_stage_and_export_context(clien
assert f"批次:{batch.batch_id}" in content
assert f"产品:{batch.product_name}" in content
assert "阶段:处理中" in content
assert "批次编号" in content
assert batch.batch_id in content
assert "产品名称" in content
assert batch.product_name in content
assert "当前审核阶段" in content
assert "处理中" in content
assert "当前最高风险等级" in content
assert ">高<" in content
assert "批次编号" not in content
assert "当前审核阶段" not in content
assert "最高风险等级" in content
assert "" in content
assert "是否允许正式导出" in content
assert ">否<" in content
assert "" in content
def test_chat_page_shows_overview_card_from_conversation_summary(client, db):

View File

@@ -5,6 +5,8 @@ from agent_core.llm_provider import (
create_llm_provider,
get_runtime_llm_config,
)
from urllib.error import HTTPError
from io import BytesIO
def test_create_llm_provider_requires_api_key_for_openai_compatible():
@@ -72,6 +74,53 @@ def test_openai_compatible_provider_posts_chat_completion(monkeypatch):
assert captured["headers"]["Authorization"] == "Bearer sk-test"
def test_openai_compatible_provider_falls_back_when_response_format_is_rejected(monkeypatch):
captured_bodies = []
class FakeResponse:
def __enter__(self):
return self
def __exit__(self, exc_type, exc, traceback):
return False
def read(self):
return b'{"choices":[{"message":{"content":"fallback ok"}}],"model":"demo-model"}'
def fake_urlopen(request, timeout):
body = request.data.decode("utf-8")
captured_bodies.append(body)
if len(captured_bodies) == 1:
raise HTTPError(
request.full_url,
400,
"Bad Request",
hdrs=None,
fp=BytesIO(b'{"error":{"message":"response_format is not supported"}}'),
)
return FakeResponse()
monkeypatch.setattr("agent_core.llm_provider.urlopen", fake_urlopen)
provider = create_llm_provider(
{
"LLM_PROVIDER": "openai_compatible",
"LLM_API_KEY": "sk-test",
"LLM_BASE_URL": "https://api.siliconflow.cn/v1",
"LLM_MODEL": "demo-model",
}
)
response = provider.generate(
[{"role": "user", "content": "hello"}],
response_format={"type": "json_object"},
)
assert response.success is True
assert response.content == "fallback ok"
assert '"response_format"' in captured_bodies[0]
assert '"response_format"' not in captured_bodies[1]
def test_embedding_provider_uses_openai_compatible_embeddings(monkeypatch):
class FakeResponse:
def __enter__(self):