Files
ai-contract-platform/backend/app/clients/shared_ai_platform.py
T

833 lines
34 KiB
Python

import inspect
import json
from typing import Any, Dict, Iterable, Mapping, Optional, Protocol
from fastapi import Depends, HTTPException
from sqlalchemy.orm import Session
from app.core.config import settings
from app.db import get_db_session
from app.repositories.conversation_repository import ConversationRepository
from app.repositories.message_repository import MessageRepository
from app.services.contract_content_normalizer import normalize_contract_content
from shared_ai_platform_sdk import RequestContext as SharedAIRequestContext
from shared_ai_platform_sdk import SharedAIPlatformClient as SharedAIPlatformSdkClient
from shared_ai_platform_sdk import SharedAIPlatformError
REMOTE_CHAT_TEMPLATE_KEYS = {
"general-assistant.chat",
"internal-knowledge.chat",
"site-chat.customer-service",
}
class SharedAIPlatformClient(Protocol):
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]: ...
def list_knowledge_bases(self, current_user: Dict[str, Any], page: int, page_size: int) -> dict: ...
def list_conversations(self, current_user: Dict[str, Any], page: int, page_size: int) -> dict: ...
def get_conversation_detail(self, current_user: Dict[str, Any], conversation_id: str) -> dict: ...
def run_legal_review(self, current_user: Dict[str, Any], payload: Dict[str, Any]) -> dict: ...
def extract_document_text(self, current_user: Dict[str, Any], *, file_name: str, mime_type: Optional[str], content: bytes) -> dict: ...
def classify_pdf_source(self, current_user: Dict[str, Any], *, content: bytes) -> dict: ...
def create_knowledge_base(self, current_user: Dict[str, Any], name: str, description: Optional[str]) -> dict: ...
def list_knowledge_base_versions(self, current_user: Dict[str, Any], knowledge_base_id: str) -> list[dict]: ...
def publish_knowledge_base(self, current_user: Dict[str, Any], knowledge_base_id: str) -> dict: ...
def unpublish_knowledge_base(self, current_user: Dict[str, Any], knowledge_base_id: str) -> dict: ...
def rollback_knowledge_base(
self,
current_user: Dict[str, Any],
knowledge_base_id: str,
target_version_label: str,
change_summary: Optional[str] = None,
) -> dict: ...
def list_documents(self, current_user: Dict[str, Any], knowledge_base_id: str, page: int, page_size: int) -> dict: ...
def upload_document(
self,
current_user: Dict[str, Any],
knowledge_base_id: str,
file_name: str,
mime_type: Optional[str],
content: bytes,
) -> dict: ...
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: ...
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: ...
class RemoteSharedAIPlatformClient:
def __init__(self, base_url: str, app_id: str, api_key: str, timeout_seconds: float, db: Optional[Session] = None):
self.base_url = base_url.rstrip("/")
self.app_id = app_id
self.api_key = api_key
self.timeout_seconds = timeout_seconds
self.db = db
self.sdk = SharedAIPlatformSdkClient(
base_url=self.base_url,
api_key=self.api_key,
app_id=self.app_id,
timeout_seconds=self.timeout_seconds,
api_prefix=settings.shared_ai_platform_api_prefix,
)
def _context(self, current_user: Dict[str, Any]) -> SharedAIRequestContext:
return SharedAIRequestContext(
tenant_id=current_user.get("tenant_id", ""),
user_id=current_user.get("id", ""),
user_role=current_user.get("role", "admin"),
)
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]:
review_context_payload = self._normalize_review_context(review_context)
remote_template_key = self._resolve_remote_chat_template_key(template_key)
def _event_stream():
attempt_template_keys = [remote_template_key]
if remote_template_key:
# Some shared-ai deployments only have generic chat templates.
# If the template-key-specific stream dies before sending any
# payload, retry once without the explicit template key.
attempt_template_keys.append(None)
for attempt_index, active_template_key in enumerate(attempt_template_keys):
emitted_payload = False
try:
for event in self.sdk.stream_conversation(
self._context(current_user),
message=message,
chat_context=review_context_payload or {},
conversation_id=conversation_id,
template_key=active_template_key,
):
if event.event_type in {"delta", "text"}:
emitted_payload = True
yield self._format_event({"type": "text", "content": event.payload.get("content", "")})
elif event.event_type == "sources":
emitted_payload = True
yield self._format_event({"type": "sources", "sources": event.payload.get("sources", [])})
elif event.event_type == "end":
emitted_payload = True
yield self._format_event(
{
"type": "end",
"conversation_id": event.payload.get("conversation_id"),
}
)
elif event.event_type == "error":
if attempt_index == 0 and template_key and not emitted_payload:
break
yield self._format_event(
{
"type": "error",
"message": event.payload.get("message", "shared ai platform request failed"),
}
)
return
elif event.event_type in {"start", "usage"}:
continue
else:
return
except SharedAIPlatformError as exc:
if attempt_index == 0 and remote_template_key and not emitted_payload:
continue
yield self._format_event({"type": "error", "message": self._build_stream_error_message(exc)})
return
except Exception as exc: # pragma: no cover - protects remote stream regressions
if attempt_index == 0 and remote_template_key and not emitted_payload:
continue
yield self._format_event({"type": "error", "message": self._build_stream_error_message(exc)})
return
yield self._format_event(
{
"type": "error",
"message": "共享 AI 问答暂时不可用,请稍后重试。",
}
)
return _event_stream()
@staticmethod
def _build_stream_error_message(exc: Exception) -> str:
detail = str(exc).strip()
if detail:
return f"共享 AI 问答暂时不可用,请稍后重试。{detail}"
return "共享 AI 问答暂时不可用,请稍后重试。"
@staticmethod
def _resolve_remote_chat_template_key(template_key: Optional[str]) -> Optional[str]:
normalized = str(template_key or "").strip()
if normalized in REMOTE_CHAT_TEMPLATE_KEYS:
return normalized
return None
def list_knowledge_bases(self, current_user: Dict[str, Any], page: int, page_size: int) -> dict:
context = self._context(current_user)
method = self.sdk.list_knowledge_bases
normalized_page = max(int(page or 1), 1)
normalized_page_size = max(min(int(page_size or 20), 100), 1)
try:
signature = inspect.signature(method)
accepts_pagination = (
"page" in signature.parameters
or "page_size" in signature.parameters
or any(parameter.kind == inspect.Parameter.VAR_KEYWORD for parameter in signature.parameters.values())
)
except (TypeError, ValueError):
accepts_pagination = True
if accepts_pagination:
try:
payload = method(context, page=normalized_page, page_size=normalized_page_size)
except TypeError as exc:
if "unexpected keyword argument" not in str(exc):
raise
payload = method(context)
else:
payload = method(context)
return self._paginate_remote_knowledge_bases_payload(
self._apply_remote_knowledge_defaults(payload),
normalized_page,
normalized_page_size,
)
def list_conversations(self, current_user: Dict[str, Any], page: int, page_size: int) -> dict:
try:
return self.sdk.list_conversations(self._context(current_user), page=page, page_size=page_size)
except Exception as exc:
return self._fallback_list_conversations(current_user, page, page_size, exc)
def get_conversation_detail(self, current_user: Dict[str, Any], conversation_id: str) -> dict:
try:
return self.sdk.get_conversation_detail(self._context(current_user), conversation_id)
except Exception as exc:
return self._fallback_get_conversation_detail(current_user, conversation_id, exc)
def run_legal_review(self, current_user: Dict[str, Any], payload: Dict[str, Any]) -> dict:
return self.sdk.run_legal_review(
self._context(current_user),
title=payload.get("title") or "文本输入合同",
text=payload.get("text") or "",
contract_type=payload.get("contract_type"),
review_as=payload.get("review_as") or "party_a",
provider=payload.get("provider"),
)
def extract_document_text(self, current_user: Dict[str, Any], *, file_name: str, mime_type: Optional[str], content: bytes) -> dict:
return self.sdk.extract_document_text(
self._context(current_user),
file_name=file_name,
mime_type=mime_type,
content=content,
)
def classify_pdf_source(self, current_user: Dict[str, Any], *, content: bytes) -> dict:
return self.sdk.classify_pdf_source(
self._context(current_user),
content=content,
)
def create_knowledge_base(self, current_user: Dict[str, Any], name: str, description: Optional[str]) -> dict:
payload = self.sdk.create_knowledge_base(
self._context(current_user),
name=name,
description=description,
retrieval_mode="hybrid",
)
if "retrieval_mode" not in payload:
payload["retrieval_mode"] = "hybrid"
return payload
def list_knowledge_base_versions(self, current_user: Dict[str, Any], knowledge_base_id: str) -> list[dict]:
try:
return self.sdk.list_knowledge_base_versions(self._context(current_user), knowledge_base_id)
except SharedAIPlatformError as exc:
if exc.status_code in (401, 403):
raise
return self._fallback_knowledge_base_versions(current_user, knowledge_base_id, exc)
except Exception as exc:
return self._fallback_knowledge_base_versions(current_user, knowledge_base_id, exc)
def publish_knowledge_base(self, current_user: Dict[str, Any], knowledge_base_id: str) -> dict:
payload = self.sdk.publish_knowledge_base(self._context(current_user), knowledge_base_id)
if "retrieval_mode" not in payload:
payload["retrieval_mode"] = "hybrid"
return payload
def unpublish_knowledge_base(self, current_user: Dict[str, Any], knowledge_base_id: str) -> dict:
payload = self.sdk.unpublish_knowledge_base(self._context(current_user), knowledge_base_id)
if "retrieval_mode" not in payload:
payload["retrieval_mode"] = "hybrid"
return payload
def rollback_knowledge_base(
self,
current_user: Dict[str, Any],
knowledge_base_id: str,
target_version_label: str,
change_summary: Optional[str] = None,
) -> dict:
payload = self.sdk.rollback_knowledge_base(
self._context(current_user),
knowledge_base_id,
target_version_label=target_version_label,
change_summary=change_summary,
)
if "retrieval_mode" not in payload:
payload["retrieval_mode"] = "hybrid"
return payload
def list_documents(self, current_user: Dict[str, Any], knowledge_base_id: str, page: int, page_size: int) -> dict:
payload = self.sdk.list_documents(self._context(current_user), knowledge_base_id, page=page, page_size=page_size)
return self._normalize_remote_knowledge_documents_payload(payload)
def upload_document(
self,
current_user: Dict[str, Any],
knowledge_base_id: str,
file_name: str,
mime_type: Optional[str],
content: bytes,
) -> dict:
payload = self.sdk.upload_document(
self._context(current_user),
knowledge_base_id,
file_name=file_name,
mime_type=mime_type,
content=content,
)
return self._normalize_remote_knowledge_document_item(payload)
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:
payload = self.sdk.retry_document(
self._context(current_user),
knowledge_base_id,
document_id,
file_name=file_name,
mime_type=mime_type,
content=content,
)
return self._normalize_remote_knowledge_document_item(payload)
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:
template_key = str((prompt_runtime or {}).get("template_key") or "").strip() or None
payload = dict(metadata or {})
payload["draft_content"] = draft_content
payload["prompt_version"] = (prompt_runtime or {}).get("prompt_version")
payload["prompt_key"] = (prompt_runtime or {}).get("prompt_key")
payload["prompt_type"] = (prompt_runtime or {}).get("prompt_type")
stream = self.stream_answer(
current_user,
None,
prompt,
review_context=payload,
template_key=template_key,
)
text_parts: list[str] = []
for chunk in stream:
if not isinstance(chunk, str):
continue
for line in chunk.splitlines():
if not line.startswith("data:"):
continue
raw = line[5:].strip()
if not raw:
continue
try:
event = json.loads(raw)
except json.JSONDecodeError:
continue
if not isinstance(event, Mapping):
continue
if event.get("type") == "text":
text_parts.append(str(event.get("content") or ""))
elif event.get("type") == "error":
raise RuntimeError(str(event.get("message") or "shared ai enhancement failed"))
raw_content = "".join(text_parts).strip()
if not raw_content:
raise RuntimeError("shared ai enhancement returned empty content")
structured_output = self._extract_structured_output(raw_content)
content = self._extract_enhanced_contract_content(structured_output, raw_content)
return {
"content": content,
"raw_content": raw_content,
"structured_output": structured_output,
"provider": "shared-ai-platform",
"model": (prompt_runtime or {}).get("model"),
"prompt_key": (prompt_runtime or {}).get("prompt_key"),
"prompt_version": (prompt_runtime or {}).get("prompt_version"),
}
@staticmethod
def _extract_enhanced_contract_content(structured_output: Optional[Dict[str, Any]], raw_content: str) -> str:
if not isinstance(structured_output, Mapping):
return normalize_contract_content(raw_content)
for key in ("html", "content", "text", "body", "markdown", "document"):
value = structured_output.get(key)
if isinstance(value, str) and value.strip():
return normalize_contract_content(value)
return normalize_contract_content(raw_content)
@staticmethod
def _extract_structured_output(raw_content: str) -> Optional[Dict[str, Any]]:
cleaned = str(raw_content or "").strip()
if not cleaned:
return None
if cleaned.startswith("```"):
cleaned = cleaned.removeprefix("```json").removeprefix("```").strip()
if cleaned.endswith("```"):
cleaned = cleaned[:-3].strip()
try:
parsed = json.loads(cleaned)
except json.JSONDecodeError:
return None
return parsed if isinstance(parsed, dict) else None
def _format_event(self, payload: Dict[str, object]) -> str:
return f"data: {json.dumps(payload, ensure_ascii=False)}\n\n"
def _apply_remote_knowledge_defaults(self, payload: dict) -> dict:
items = payload.get("items")
if not isinstance(items, list):
return payload
for item in items:
if isinstance(item, dict) and not item.get("retrieval_mode"):
item["retrieval_mode"] = "hybrid"
items.sort(key=self._knowledge_base_sort_key, reverse=True)
return payload
def _paginate_remote_knowledge_bases_payload(self, payload: dict, page: int, page_size: int) -> dict:
items = payload.get("items") if isinstance(payload, Mapping) else None
if not isinstance(items, list):
return payload
normalized = dict(payload)
total = int(normalized.get("total") or len(items))
source_page_size = int(normalized.get("page_size") or len(items) or page_size)
source_page = int(normalized.get("page") or 1)
if source_page == page and source_page_size == page_size:
normalized["total"] = total
normalized["page"] = page
normalized["page_size"] = page_size
return normalized
if total == len(items):
start = (page - 1) * page_size
normalized["items"] = items[start : start + page_size]
normalized["total"] = total
normalized["page"] = page
normalized["page_size"] = page_size
return normalized
@staticmethod
def _knowledge_base_sort_key(item: Any) -> str:
if not isinstance(item, Mapping):
return ""
return str(item.get("updated_at") or item.get("created_at") or "")
def _fallback_knowledge_base_versions(self, current_user: Dict[str, Any], knowledge_base_id: str, exc: Exception) -> list[dict]:
try:
payload = self.list_knowledge_bases(current_user, 1, 100)
except Exception as list_exc:
raise HTTPException(status_code=503, detail=self._build_knowledge_base_versions_error_message(exc)) from list_exc
items = payload.get("items") if isinstance(payload, Mapping) else []
knowledge_base = next((item for item in items if isinstance(item, Mapping) and item.get("id") == knowledge_base_id), None)
if not knowledge_base:
raise HTTPException(status_code=404, detail="知识库不存在或无权访问")
version_label = str(knowledge_base.get("current_version_label") or "current")
timestamp = knowledge_base.get("published_at") or knowledge_base.get("updated_at") or knowledge_base.get("created_at")
return [
{
"id": f"{knowledge_base_id}-{version_label}",
"knowledge_base_id": knowledge_base_id,
"version_label": version_label,
"name": knowledge_base.get("name") or "",
"description": knowledge_base.get("description"),
"retrieval_mode": knowledge_base.get("retrieval_mode") or "hybrid",
"status": knowledge_base.get("status") or "active",
"published_at": knowledge_base.get("published_at"),
"published_by": knowledge_base.get("published_by"),
"change_summary": self._build_knowledge_base_versions_error_message(exc),
"created_at": timestamp,
}
]
def _normalize_remote_knowledge_documents_payload(self, payload: dict) -> dict:
items = payload.get("items")
if not isinstance(items, list):
return payload
payload["items"] = [self._normalize_remote_knowledge_document_item(item) for item in items]
return payload
def _normalize_remote_knowledge_document_item(self, payload: dict) -> dict:
metadata = payload.get("metadata_json")
if not isinstance(metadata, Mapping):
metadata = payload.get("metadata")
metadata = metadata if isinstance(metadata, Mapping) else {}
normalized = dict(payload)
source_type = normalized.get("source_type") or metadata.get("source_type")
authority = normalized.get("authority") or metadata.get("authority") or metadata.get("issuer")
effective_date = normalized.get("effective_date") or metadata.get("effective_date")
region = normalized.get("region") or metadata.get("region") or metadata.get("jurisdiction")
tags = normalized.get("tags")
if tags is None:
tags = normalized.get("tags_json")
if tags is None:
tags = metadata.get("tags")
normalized["source_type"] = source_type
normalized["authority"] = authority
normalized["effective_date"] = effective_date
normalized["region"] = region
normalized["tags"] = list(tags) if isinstance(tags, list) else None
if normalized["tags"] is not None and normalized.get("tags_json") is None:
normalized["tags_json"] = normalized["tags"]
if metadata and normalized.get("metadata_json") is None:
normalized["metadata_json"] = dict(metadata)
return normalized
def _normalize_review_context(self, review_context: Any) -> Optional[Dict[str, Any]]:
if review_context is None:
return None
if isinstance(review_context, Mapping):
return dict(review_context)
if hasattr(review_context, "model_dump"):
return review_context.model_dump(mode="json", by_alias=True, exclude_none=True)
return review_context
def _fallback_list_conversations(self, current_user: Dict[str, Any], page: int, page_size: int, exc: Exception) -> dict:
if self.db is None:
raise HTTPException(status_code=503, detail=self._build_history_error_message(exc)) from exc
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._build_history_error_message(exc),
}
def _fallback_get_conversation_detail(self, current_user: Dict[str, Any], conversation_id: str, exc: Exception) -> dict:
if self.db is None:
raise HTTPException(status_code=503, detail=self._build_history_error_message(exc)) from exc
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=503, detail=self._build_history_error_message(exc)) from exc
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._build_history_error_message(exc),
}
@staticmethod
def _build_history_error_message(exc: Exception) -> str:
detail = str(exc).strip()
if detail:
return f"共享 AI 会话历史暂时不可用,已切换为本地历史记录。原因:{detail[:200]}"
return "共享 AI 会话历史暂时不可用,已切换为本地历史记录。"
@staticmethod
def _build_knowledge_base_versions_error_message(exc: Exception) -> str:
detail = str(exc).strip()
if detail:
return f"共享 AI 版本历史暂时不可用,已展示当前知识库快照。原因:{detail[:200]}"
return "共享 AI 版本历史暂时不可用,已展示当前知识库快照。"
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)
def build_shared_ai_platform_client(db: Session) -> SharedAIPlatformClient:
base_url = settings.shared_ai_platform_base_url.strip()
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:
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,
app_id=settings.shared_ai_platform_app_id,
api_key=api_key,
timeout_seconds=settings.shared_ai_platform_timeout_seconds,
db=db,
)