fix(chat): simplify header cards and llm fallback
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
from urllib.error import URLError
|
from urllib.error import HTTPError, URLError
|
||||||
from urllib.request import Request, urlopen
|
from urllib.request import Request, urlopen
|
||||||
|
|
||||||
|
|
||||||
@@ -75,12 +75,28 @@ class OpenAICompatibleProvider:
|
|||||||
if response_format:
|
if response_format:
|
||||||
payload["response_format"] = response_format
|
payload["response_format"] = response_format
|
||||||
try:
|
try:
|
||||||
data = _post_json(
|
try:
|
||||||
base_url=self.base_url,
|
data = _post_json(
|
||||||
endpoint="chat/completions",
|
base_url=self.base_url,
|
||||||
api_key=self.api_key,
|
endpoint="chat/completions",
|
||||||
payload=payload,
|
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]
|
choice = data.get("choices", [{}])[0]
|
||||||
content = choice.get("message", {}).get("content", "")
|
content = choice.get("message", {}).get("content", "")
|
||||||
return LLMResponse(
|
return LLMResponse(
|
||||||
@@ -197,5 +213,11 @@ def _post_json(base_url: str, endpoint: str, api_key: str, payload: dict) -> dic
|
|||||||
try:
|
try:
|
||||||
with urlopen(request, timeout=60) as response:
|
with urlopen(request, timeout=60) as response:
|
||||||
return json.loads(response.read().decode("utf-8"))
|
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:
|
except URLError as exc:
|
||||||
raise RuntimeError(f"OpenAI 兼容接口调用失败:{exc}") from exc
|
raise RuntimeError(f"OpenAI 兼容接口调用失败:{exc}") from exc
|
||||||
|
|||||||
@@ -359,33 +359,6 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</section>
|
</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">
|
<section class="chat-layout">
|
||||||
<input class="history-toggle" id="history-collapsed" type="checkbox">
|
<input class="history-toggle" id="history-collapsed" type="checkbox">
|
||||||
<aside class="panel history-panel">
|
<aside class="panel history-panel">
|
||||||
|
|||||||
@@ -526,7 +526,7 @@ def test_chat_page_shows_upload_entry_and_dynamic_context_cards(client, db):
|
|||||||
assert "飞书通知 / 待处理" in content
|
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()
|
batch, conversation = _create_conversation_with_batch()
|
||||||
conversation.task_status = "processing"
|
conversation.task_status = "processing"
|
||||||
conversation.node_results = [
|
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")
|
content = response.content.decode("utf-8")
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert "顶部对话上下文" in content
|
assert "顶部对话上下文" not in content
|
||||||
assert "当前流程类型" in content
|
assert "推荐提问模板" not in content
|
||||||
assert "registration" in content
|
assert "对话区与节点导航" in content
|
||||||
assert "当前审核阶段" in content
|
|
||||||
assert "处理中" in content
|
assert "处理中" 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.batch_id}" in content
|
||||||
assert f"产品:{batch.product_name}" in content
|
assert f"产品:{batch.product_name}" in content
|
||||||
assert "阶段:处理中" in content
|
assert "阶段:处理中" in content
|
||||||
assert "批次编号" in content
|
assert "批次编号" not in content
|
||||||
assert batch.batch_id in content
|
assert "当前审核阶段" not in content
|
||||||
assert "产品名称" in content
|
assert "最高风险等级" in content
|
||||||
assert batch.product_name in content
|
assert "高" in content
|
||||||
assert "当前审核阶段" in content
|
|
||||||
assert "处理中" in content
|
|
||||||
assert "当前最高风险等级" 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):
|
def test_chat_page_shows_overview_card_from_conversation_summary(client, db):
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ from agent_core.llm_provider import (
|
|||||||
create_llm_provider,
|
create_llm_provider,
|
||||||
get_runtime_llm_config,
|
get_runtime_llm_config,
|
||||||
)
|
)
|
||||||
|
from urllib.error import HTTPError
|
||||||
|
from io import BytesIO
|
||||||
|
|
||||||
|
|
||||||
def test_create_llm_provider_requires_api_key_for_openai_compatible():
|
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"
|
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):
|
def test_embedding_provider_uses_openai_compatible_embeddings(monkeypatch):
|
||||||
class FakeResponse:
|
class FakeResponse:
|
||||||
def __enter__(self):
|
def __enter__(self):
|
||||||
|
|||||||
Reference in New Issue
Block a user