fix: fallback chat history when ai config missing
This commit is contained in:
@@ -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 = []
|
||||
|
||||
|
||||
Reference in New Issue
Block a user