feat: 新增预算费控模型与报销审批流引擎
后端新增预算费控服务和报销单审批流模块,引入申请人费用画像 算法,优化知识库 RAG 运行时和同步逻辑,完善报销单工作流常 量和明细同步,更新差旅报销规则电子表格,前端新增预算分析 组件和数字员工模型,完善审批对话框和洞察面板交互,优化侧 边栏和顶栏样式,补充单元测试。
This commit is contained in:
@@ -4,8 +4,9 @@ import os
|
||||
import re
|
||||
import socket
|
||||
import threading
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from typing import Any, Callable
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
@@ -89,8 +90,10 @@ STRUCTURED_APPENDIX_LEADING_MARKERS = (
|
||||
)
|
||||
STRUCTURED_APPENDIX_LEADING_WINDOW = 220
|
||||
_runtime_lock = threading.RLock()
|
||||
_runtime_instances: dict[int, _LightRagRuntime] = {}
|
||||
_runtime_signatures: dict[int, tuple[Any, ...]] = {}
|
||||
_runtime_executor = ThreadPoolExecutor(max_workers=1, thread_name_prefix="knowledge-rag-runtime")
|
||||
_runtime_instances: dict[str, _LightRagRuntime] = {}
|
||||
_runtime_signatures: dict[str, tuple[Any, ...]] = {}
|
||||
_RUNTIME_CACHE_KEY = "lightrag"
|
||||
|
||||
|
||||
class KnowledgeRagService:
|
||||
@@ -133,21 +136,26 @@ class KnowledgeRagService:
|
||||
|
||||
runtime_hits: list[dict[str, Any]] = []
|
||||
runtime_references: list[str] = []
|
||||
try:
|
||||
runtime = self._get_runtime()
|
||||
raw = runtime.query_data(rewritten_query, conversation_history=conversation_history)
|
||||
data = raw.get("data") if isinstance(raw, dict) else {}
|
||||
chunks = list(data.get("chunks") or []) if isinstance(data, dict) else []
|
||||
entities = list(data.get("entities") or []) if isinstance(data, dict) else []
|
||||
runtime_references = list(data.get("references") or []) if isinstance(data, dict) else []
|
||||
runtime_hits = self._build_hits_from_query_data(
|
||||
query=rewritten_query,
|
||||
chunks=chunks,
|
||||
entities=entities,
|
||||
limit=limit,
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.warning("Knowledge query failed: %s", exc)
|
||||
if not local_result.confident:
|
||||
try:
|
||||
raw = self._run_runtime_operation(
|
||||
lambda runtime: runtime.query_data(
|
||||
rewritten_query,
|
||||
conversation_history=conversation_history,
|
||||
)
|
||||
)
|
||||
data = raw.get("data") if isinstance(raw, dict) else {}
|
||||
chunks = list(data.get("chunks") or []) if isinstance(data, dict) else []
|
||||
entities = list(data.get("entities") or []) if isinstance(data, dict) else []
|
||||
runtime_references = list(data.get("references") or []) if isinstance(data, dict) else []
|
||||
runtime_hits = self._build_hits_from_query_data(
|
||||
query=rewritten_query,
|
||||
chunks=chunks,
|
||||
entities=entities,
|
||||
limit=limit,
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.warning("Knowledge query failed: %s", exc)
|
||||
|
||||
all_hits: dict[str, dict[str, Any]] = {}
|
||||
for hit in local_result.hits:
|
||||
@@ -189,7 +197,7 @@ class KnowledgeRagService:
|
||||
],
|
||||
"raw_references": runtime_references,
|
||||
"metadata": {
|
||||
"retrieval_strategy": "fusion",
|
||||
"retrieval_strategy": "fusion" if runtime_hits else "local_text_chunks",
|
||||
"local_total_chunks": local_result.total_chunks,
|
||||
"local_best_score": local_result.best_score,
|
||||
},
|
||||
@@ -244,14 +252,17 @@ class KnowledgeRagService:
|
||||
file_paths: list[str] = []
|
||||
document_summaries: list[dict[str, Any]] = []
|
||||
|
||||
runtime = self._get_runtime()
|
||||
existing_statuses = runtime.get_document_statuses(normalized_ids)
|
||||
existing_statuses = self._run_runtime_operation(
|
||||
lambda runtime: runtime.get_document_statuses(normalized_ids)
|
||||
)
|
||||
|
||||
for document_id in normalized_ids:
|
||||
entry = knowledge_service.get_document_entry(document_id)
|
||||
if force and document_id in existing_statuses:
|
||||
try:
|
||||
runtime.delete_document(document_id)
|
||||
self._run_runtime_operation(
|
||||
lambda runtime, target_id=document_id: runtime.delete_document(target_id)
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.warning(
|
||||
"Delete existing LightRAG document failed doc_id=%s: %s", document_id, exc
|
||||
@@ -277,13 +288,17 @@ class KnowledgeRagService:
|
||||
)
|
||||
)
|
||||
|
||||
track_id = runtime.insert_documents(
|
||||
texts=texts,
|
||||
document_ids=normalized_ids,
|
||||
file_paths=file_paths,
|
||||
track_id = self._run_runtime_operation(
|
||||
lambda runtime: runtime.insert_documents(
|
||||
texts=texts,
|
||||
document_ids=normalized_ids,
|
||||
file_paths=file_paths,
|
||||
)
|
||||
)
|
||||
|
||||
statuses = runtime.get_document_statuses(normalized_ids)
|
||||
statuses = self._run_runtime_operation(
|
||||
lambda runtime: runtime.get_document_statuses(normalized_ids)
|
||||
)
|
||||
succeeded_document_ids: list[str] = []
|
||||
failed_documents: list[dict[str, str]] = []
|
||||
summary_by_id = {
|
||||
@@ -344,7 +359,9 @@ class KnowledgeRagService:
|
||||
if not target_ids:
|
||||
return {}
|
||||
try:
|
||||
statuses = self._get_runtime().get_document_statuses(target_ids)
|
||||
statuses = self._run_runtime_operation(
|
||||
lambda runtime: runtime.get_document_statuses(target_ids)
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.warning("Load LightRAG document statuses failed: %s", exc)
|
||||
return {}
|
||||
@@ -358,16 +375,40 @@ class KnowledgeRagService:
|
||||
if not normalized_id:
|
||||
return
|
||||
try:
|
||||
self._get_runtime().delete_document(normalized_id)
|
||||
self._run_runtime_operation(
|
||||
lambda runtime: runtime.delete_document(normalized_id)
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.warning("Delete LightRAG document ignored doc_id=%s: %s", normalized_id, exc)
|
||||
|
||||
def _get_runtime(self) -> _LightRagRuntime:
|
||||
def _run_runtime_operation(self, operation: Callable[[_LightRagRuntime], Any]) -> Any:
|
||||
signature, runtime_kwargs = self._build_runtime_signature()
|
||||
thread_id = threading.get_ident()
|
||||
return _runtime_executor.submit(
|
||||
self._execute_runtime_operation,
|
||||
signature,
|
||||
runtime_kwargs,
|
||||
operation,
|
||||
).result()
|
||||
|
||||
def _execute_runtime_operation(
|
||||
self,
|
||||
signature: tuple[Any, ...],
|
||||
runtime_kwargs: dict[str, Any],
|
||||
operation: Callable[[_LightRagRuntime], Any],
|
||||
) -> Any:
|
||||
return operation(self._get_runtime(signature=signature, runtime_kwargs=runtime_kwargs))
|
||||
|
||||
def _get_runtime(
|
||||
self,
|
||||
*,
|
||||
signature: tuple[Any, ...] | None = None,
|
||||
runtime_kwargs: dict[str, Any] | None = None,
|
||||
) -> _LightRagRuntime:
|
||||
if signature is None or runtime_kwargs is None:
|
||||
signature, runtime_kwargs = self._build_runtime_signature()
|
||||
with _runtime_lock:
|
||||
runtime = _runtime_instances.get(thread_id)
|
||||
if runtime is not None and _runtime_signatures.get(thread_id) == signature:
|
||||
runtime = _runtime_instances.get(_RUNTIME_CACHE_KEY)
|
||||
if runtime is not None and _runtime_signatures.get(_RUNTIME_CACHE_KEY) == signature:
|
||||
return runtime
|
||||
|
||||
if runtime is not None:
|
||||
@@ -377,8 +418,8 @@ class KnowledgeRagService:
|
||||
logger.warning("Finalize previous LightRAG runtime failed: %s", exc)
|
||||
|
||||
runtime = _LightRagRuntime(**runtime_kwargs)
|
||||
_runtime_instances[thread_id] = runtime
|
||||
_runtime_signatures[thread_id] = signature
|
||||
_runtime_instances[_RUNTIME_CACHE_KEY] = runtime
|
||||
_runtime_signatures[_RUNTIME_CACHE_KEY] = signature
|
||||
return runtime
|
||||
|
||||
def _build_runtime_signature(self) -> tuple[tuple[Any, ...], dict[str, Any]]:
|
||||
@@ -633,6 +674,10 @@ class KnowledgeRagService:
|
||||
|
||||
|
||||
def shutdown_knowledge_rag_runtime() -> None:
|
||||
_runtime_executor.submit(_shutdown_runtime_instances).result()
|
||||
|
||||
|
||||
def _shutdown_runtime_instances() -> None:
|
||||
with _runtime_lock:
|
||||
for runtime in list(_runtime_instances.values()):
|
||||
try:
|
||||
|
||||
Reference in New Issue
Block a user