fix: fallback chat history when ai config missing

This commit is contained in:
2026-06-21 19:15:21 +08:00
parent 17f5ada9f2
commit 278bc28416
2 changed files with 224 additions and 11 deletions
+183 -10
View File
@@ -630,6 +630,183 @@ class RemoteSharedAIPlatformClient:
class UnavailableSharedAIPlatformClient:
def __init__(self, reason: str, db: Optional[Session] = None):
self.reason = reason
self.db = db
def stream_answer(
self,
current_user: Dict[str, Any],
conversation_id: Optional[str],
message: str,
review_context: Optional[Any] = None,
template_key: Optional[str] = None,
) -> Iterable[str]:
yield self._format_event(
{
"type": "error",
"message": f"模型服务未配置或暂不可用:{self.reason}。请管理员到“大模型设置”核对参数,并完成后端/共享 AI 服务配置后重试。",
}
)
def list_conversations(self, current_user: Dict[str, Any], page: int, page_size: int) -> dict:
if self.db is None:
return self._empty_conversations(page, page_size)
normalized_page = max(int(page or 1), 1)
normalized_page_size = max(min(int(page_size or 20), 100), 1)
repository = ConversationRepository(self.db)
tenant_id = str(current_user.get("tenant_id") or "")
user_id = str(current_user.get("id") or "")
items = repository.list_paginated(tenant_id, user_id, normalized_page, normalized_page_size)
return {
"items": [
{
"id": item.id,
"title": item.title,
"message_count": item.message_count,
"last_message_at": item.last_message_at.isoformat() if item.last_message_at else None,
}
for item in items
],
"page": normalized_page,
"page_size": normalized_page_size,
"total": repository.count(tenant_id, user_id),
"source": "local_fallback",
"warning": self._history_warning(),
}
def get_conversation_detail(self, current_user: Dict[str, Any], conversation_id: str) -> dict:
if self.db is None:
raise HTTPException(status_code=503, detail=self._history_warning())
tenant_id = str(current_user.get("tenant_id") or "")
user_id = str(current_user.get("id") or "")
conversation = ConversationRepository(self.db).get_by_id_for_user(conversation_id, tenant_id, user_id)
if conversation is None:
raise HTTPException(status_code=404, detail="conversation not found")
messages = MessageRepository(self.db).list_by_conversation(conversation.id)
return {
"id": conversation.id,
"title": conversation.title,
"messages": [
{
"id": message.id,
"role": message.role,
"content": message.content,
"sources": message.sources_json or [],
"created_at": message.created_at.isoformat(),
}
for message in messages
if message.tenant_id == tenant_id
],
"source": "local_fallback",
"warning": self._history_warning(),
}
def list_knowledge_bases(self, current_user: Dict[str, Any], page: int, page_size: int) -> dict:
return {
"items": [],
"page": max(int(page or 1), 1),
"page_size": max(min(int(page_size or 20), 100), 1),
"total": 0,
"warning": self._unavailable_message(),
}
def list_knowledge_base_versions(self, current_user: Dict[str, Any], knowledge_base_id: str) -> list[dict]:
return []
def list_documents(self, current_user: Dict[str, Any], knowledge_base_id: str, page: int, page_size: int) -> dict:
return {
"items": [],
"page": max(int(page or 1), 1),
"page_size": max(min(int(page_size or 20), 100), 1),
"total": 0,
"warning": self._unavailable_message(),
}
def create_knowledge_base(self, current_user: Dict[str, Any], name: str, description: Optional[str]) -> dict:
self._raise_unavailable()
def publish_knowledge_base(self, current_user: Dict[str, Any], knowledge_base_id: str) -> dict:
self._raise_unavailable()
def unpublish_knowledge_base(self, current_user: Dict[str, Any], knowledge_base_id: str) -> dict:
self._raise_unavailable()
def rollback_knowledge_base(
self,
current_user: Dict[str, Any],
knowledge_base_id: str,
target_version_label: str,
change_summary: Optional[str] = None,
) -> dict:
self._raise_unavailable()
def upload_document(
self,
current_user: Dict[str, Any],
knowledge_base_id: str,
file_name: str,
mime_type: Optional[str],
content: bytes,
) -> dict:
self._raise_unavailable()
def retry_document(
self,
current_user: Dict[str, Any],
knowledge_base_id: str,
document_id: str,
file_name: Optional[str] = None,
mime_type: Optional[str] = None,
content: Optional[bytes] = None,
) -> dict:
self._raise_unavailable()
def run_legal_review(self, current_user: Dict[str, Any], payload: Dict[str, Any]) -> dict:
self._raise_unavailable()
def extract_document_text(self, current_user: Dict[str, Any], *, file_name: str, mime_type: Optional[str], content: bytes) -> dict:
self._raise_unavailable()
def classify_pdf_source(self, current_user: Dict[str, Any], *, content: bytes) -> dict:
self._raise_unavailable()
def enhance_contract_generation(
self,
current_user: Dict[str, Any],
*,
prompt_runtime: Optional[Dict[str, Any]],
prompt: str,
draft_content: str,
metadata: Optional[Dict[str, Any]] = None,
) -> dict:
self._raise_unavailable()
def _empty_conversations(self, page: int, page_size: int) -> dict:
return {
"items": [],
"page": max(int(page or 1), 1),
"page_size": max(min(int(page_size or 20), 100), 1),
"total": 0,
"source": "local_fallback",
"warning": self._history_warning(),
}
def _history_warning(self) -> str:
return f"共享 AI 会话历史暂不可用,已切换为本地历史记录。原因:{self.reason}"
def _unavailable_message(self) -> str:
return f"共享 AI 服务未配置或暂不可用:{self.reason}"
def _raise_unavailable(self):
raise HTTPException(status_code=503, detail=self._unavailable_message())
@staticmethod
def _format_event(payload: Dict[str, object]) -> str:
return f"data: {json.dumps(payload, ensure_ascii=False)}\n\n"
def get_shared_ai_platform_client(db: Session = Depends(get_db_session)) -> SharedAIPlatformClient:
return build_shared_ai_platform_client(db)
@@ -637,18 +814,14 @@ def get_shared_ai_platform_client(db: Session = Depends(get_db_session)) -> Shar
def build_shared_ai_platform_client(db: Session) -> SharedAIPlatformClient:
base_url = settings.shared_ai_platform_base_url.strip()
if not base_url:
raise HTTPException(
status_code=503,
detail="shared-ai-platform base URL is required for AI features but is not configured",
)
api_key = settings.shared_ai_platform_api_key.strip()
missing_parts = []
if not base_url:
missing_parts.append("SHARED_AI_PLATFORM_BASE_URL")
if not api_key:
raise HTTPException(
status_code=503,
detail="shared-ai-platform API key is required for AI features but is not configured",
)
missing_parts.append("SHARED_AI_PLATFORM_API_KEY")
if missing_parts:
return UnavailableSharedAIPlatformClient(f"缺少 {', '.join(missing_parts)}", db=db)
return RemoteSharedAIPlatformClient(
base_url=base_url,
@@ -6,7 +6,8 @@ import httpx
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from app.clients.shared_ai_platform import RemoteSharedAIPlatformClient
from app.clients.shared_ai_platform import RemoteSharedAIPlatformClient, build_shared_ai_platform_client
from app.core.config import settings
from app.db import Base
from app.models.conversation import Conversation
from app.models.message import Message
@@ -123,6 +124,45 @@ class SharedAiPlatformSdkTestCase(unittest.TestCase):
class RemoteSharedAiPlatformClientStreamTestCase(unittest.TestCase):
def test_missing_shared_ai_config_uses_local_history_fallback(self):
original_base_url = settings.shared_ai_platform_base_url
original_api_key = settings.shared_ai_platform_api_key
settings.shared_ai_platform_base_url = ""
settings.shared_ai_platform_api_key = ""
engine = create_engine("sqlite:///:memory:", future=True)
Base.metadata.create_all(engine)
session = sessionmaker(bind=engine, future=True)()
try:
now = datetime.now(timezone.utc)
session.add(
Conversation(
id="conv-local-config-missing",
tenant_id="tenant-1",
user_id="user-1",
title="Local fallback",
message_count=1,
last_message_at=now,
created_at=now,
updated_at=now,
)
)
session.commit()
client = build_shared_ai_platform_client(session)
payload = client.list_conversations({"id": "user-1", "tenant_id": "tenant-1", "role": "admin"}, 1, 20)
chunks = list(client.stream_answer({"id": "user-1", "tenant_id": "tenant-1", "role": "admin"}, None, "hello"))
self.assertEqual(payload["source"], "local_fallback")
self.assertEqual(payload["items"][0]["id"], "conv-local-config-missing")
self.assertIn("SHARED_AI_PLATFORM_BASE_URL", payload["warning"])
self.assertIn("SHARED_AI_PLATFORM_API_KEY", payload["warning"])
self.assertIn("模型服务未配置", chunks[0])
finally:
session.close()
engine.dispose()
settings.shared_ai_platform_base_url = original_base_url
settings.shared_ai_platform_api_key = original_api_key
def test_remote_stream_ignores_start_and_usage_and_maps_delta(self):
calls = []