Files
ai-contract-platform/backend/app/clients/shared_ai_platform.py
T
yuqei 3df85fdb04 feat: land phase15 legal docs and commercial ops
Includes legal document generation workflows, prompt governance runtime wiring, legal change impact scans, operation KPI dashboards, invoice files, sales follow-up events, mobile review polish, deployment automation, and updated project docs.
2026-05-21 14:42:44 +08:00

327 lines
13 KiB
Python

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 shared_ai_platform_sdk import RequestContext as SharedAIRequestContext
from shared_ai_platform_sdk import SharedAIPlatformClient as SharedAIPlatformSdkClient
from shared_ai_platform_sdk import SharedAIPlatformError
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 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: ...
class RemoteSharedAIPlatformClient:
def __init__(self, base_url: str, app_id: str, api_key: str, timeout_seconds: float):
self.base_url = base_url.rstrip("/")
self.app_id = app_id
self.api_key = api_key
self.timeout_seconds = timeout_seconds
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 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)
def _event_stream():
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=template_key,
):
if event.event_type in {"delta", "text"}:
yield self._format_event({"type": "text", "content": event.payload.get("content", "")})
elif event.event_type == "sources":
yield self._format_event({"type": "sources", "sources": event.payload.get("sources", [])})
elif event.event_type == "end":
yield self._format_event(
{
"type": "end",
"conversation_id": event.payload.get("conversation_id"),
}
)
elif event.event_type == "error":
yield self._format_event({"type": "error", "message": event.payload.get("message", "shared ai platform request failed")})
elif event.event_type in {"start", "usage"}:
continue
except SharedAIPlatformError as exc:
raise HTTPException(status_code=exc.status_code, detail=str(exc)) from exc
return _event_stream()
def list_knowledge_bases(self, current_user: Dict[str, Any], page: int, page_size: int) -> dict:
payload = self.sdk.list_knowledge_bases(self._context(current_user), page=page, page_size=page_size)
return self._apply_remote_knowledge_defaults(payload)
def list_conversations(self, current_user: Dict[str, Any], page: int, page_size: int) -> dict:
return self.sdk.list_conversations(self._context(current_user), page=page, page_size=page_size)
def get_conversation_detail(self, current_user: Dict[str, Any], conversation_id: str) -> dict:
return self.sdk.get_conversation_detail(self._context(current_user), conversation_id)
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 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]:
return self.sdk.list_knowledge_base_versions(self._context(current_user), knowledge_base_id)
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 _context(self, current_user: Dict[str, Any]) -> SharedAIRequestContext:
return SharedAIRequestContext(
tenant_id=str(current_user.get("tenant_id") or ""),
user_id=str(current_user.get("id") or ""),
user_role=str(current_user.get("role") or "admin"),
)
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
@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 _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 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()
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()
if not api_key:
raise HTTPException(
status_code=503,
detail="shared-ai-platform API key is required for AI features but is not configured",
)
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,
)