feat: 新增预算费控模型与报销审批流引擎
后端新增预算费控服务和报销单审批流模块,引入申请人费用画像 算法,优化知识库 RAG 运行时和同步逻辑,完善报销单工作流常 量和明细同步,更新差旅报销规则电子表格,前端新增预算分析 组件和数字员工模型,完善审批对话框和洞察面板交互,优化侧 边栏和顶栏样式,补充单元测试。
This commit is contained in:
@@ -86,7 +86,6 @@ class UserAgentKnowledgeMixin(UserAgentKnowledgeHelpersMixin):
|
||||
*,
|
||||
citations: list[UserAgentCitation],
|
||||
) -> str | None:
|
||||
return None
|
||||
if payload.ontology.scenario != "knowledge":
|
||||
return None
|
||||
if str(payload.tool_payload.get("result_type") or "").strip() != "knowledge_search":
|
||||
@@ -130,7 +129,10 @@ class UserAgentKnowledgeMixin(UserAgentKnowledgeHelpersMixin):
|
||||
primary_heading = self._format_knowledge_heading_label(
|
||||
str(primary_item.get("heading") or "").strip()
|
||||
)
|
||||
primary_lines = self._collect_direct_knowledge_answer_lines(ordered_evidence_items)
|
||||
primary_lines = self._collect_direct_knowledge_answer_lines(
|
||||
ordered_evidence_items,
|
||||
query_terms=query_terms,
|
||||
)
|
||||
|
||||
lines: list[str] = []
|
||||
if user_name:
|
||||
@@ -139,20 +141,42 @@ class UserAgentKnowledgeMixin(UserAgentKnowledgeHelpersMixin):
|
||||
if primary_heading:
|
||||
source_prefix = f"{source_prefix}({primary_heading})"
|
||||
|
||||
conclusion_lines: list[str] = []
|
||||
evidence_lines: list[str] = []
|
||||
if str(primary_item.get("kind") or "") == "table":
|
||||
lines.append(f"{source_prefix},当前能直接确认的是:")
|
||||
lines.append(self._extract_relevant_table_preview(str(primary_item.get("content") or ""), query_terms))
|
||||
table_content = str(primary_item.get("content") or "")
|
||||
if self._question_requests_broad_knowledge_table(question):
|
||||
table_preview = table_content.strip()
|
||||
else:
|
||||
table_preview = self._extract_relevant_table_preview(
|
||||
table_content,
|
||||
query_terms,
|
||||
preferred_terms=self._build_knowledge_table_preferred_terms(payload),
|
||||
)
|
||||
table_summary = self._summarize_knowledge_table_preview(table_preview)
|
||||
conclusion_lines.append(f"{source_prefix},{table_summary}")
|
||||
evidence_lines.append(table_preview)
|
||||
else:
|
||||
if not primary_lines:
|
||||
lines.append(
|
||||
summary = self._summarize_knowledge_evidence_content(primary_item, query_terms)
|
||||
conclusion_lines.append(
|
||||
f"{source_prefix},当前能直接确认的是:"
|
||||
f"{self._summarize_knowledge_evidence_content(primary_item, query_terms)}"
|
||||
f"{summary}"
|
||||
)
|
||||
elif len(primary_lines) == 1:
|
||||
lines.append(f"{source_prefix},当前能直接确认的是:{primary_lines[0].strip()}")
|
||||
conclusion_lines.append(f"{source_prefix},当前能直接确认的是:{primary_lines[0].strip()}")
|
||||
evidence_lines.extend(primary_lines)
|
||||
else:
|
||||
lines.append(f"{source_prefix},当前能直接确认的是:")
|
||||
lines.extend(primary_lines)
|
||||
subject = self._build_knowledge_answer_subject(question, primary_heading)
|
||||
summary = self._summarize_knowledge_lines_conclusion(
|
||||
primary_lines,
|
||||
heading=subject,
|
||||
)
|
||||
if summary:
|
||||
conclusion_lines.append(f"{source_prefix},{summary}")
|
||||
else:
|
||||
conclusion_lines.append(f"{source_prefix},当前能直接确认的是:")
|
||||
evidence_lines.extend(primary_lines)
|
||||
|
||||
notes: list[str] = []
|
||||
location_note = self._build_missing_location_grounding_note(question, evidence_items)
|
||||
@@ -161,14 +185,64 @@ class UserAgentKnowledgeMixin(UserAgentKnowledgeHelpersMixin):
|
||||
if self._question_requires_explicit_condition(question) and not self._answer_evidence_has_numeric_or_condition(evidence_items):
|
||||
notes.append("当前命中的证据更偏规则说明或流程约束,还没有直接给出可立即套用的数值或完整条件。")
|
||||
|
||||
self._append_markdown_section(lines, "结论", conclusion_lines)
|
||||
self._append_markdown_section(lines, "依据", evidence_lines)
|
||||
if notes:
|
||||
lines.append("")
|
||||
lines.append("说明:")
|
||||
lines.extend(f"- {note}" for note in notes)
|
||||
self._append_markdown_section(lines, "说明", [f"- {note}" for note in notes])
|
||||
|
||||
return "\n".join(line for line in lines if line is not None).strip()
|
||||
|
||||
|
||||
@staticmethod
|
||||
def _append_markdown_section(lines: list[str], title: str, body_lines: list[str]) -> None:
|
||||
cleaned = [str(line or "").rstrip() for line in body_lines if str(line or "").strip()]
|
||||
if not cleaned:
|
||||
return
|
||||
if lines and lines[-1] != "":
|
||||
lines.append("")
|
||||
lines.append(f"## {title}")
|
||||
lines.append("")
|
||||
lines.extend(cleaned)
|
||||
|
||||
|
||||
@staticmethod
|
||||
def _build_knowledge_answer_subject(question: str, heading: str = "") -> str:
|
||||
clean_heading = str(heading or "").strip()
|
||||
if clean_heading and not any(
|
||||
marker in clean_heading
|
||||
for marker in ("问答线索补充", "结构化表格补充", "重点章节摘录", "章节导航")
|
||||
):
|
||||
return clean_heading
|
||||
|
||||
normalized = re.sub(r"\s+", "", str(question or "").strip())
|
||||
normalized = re.sub(r"[??。.!!]+$", "", normalized)
|
||||
normalized = re.sub(r"(是什么|有哪些|是多少|如何|怎么|吗|呢)$", "", normalized)
|
||||
return normalized.strip("::,,。.")
|
||||
|
||||
|
||||
@staticmethod
|
||||
def _build_knowledge_table_preferred_terms(payload: UserAgentRequest) -> list[str]:
|
||||
terms: list[str] = []
|
||||
context = payload.context_json or {}
|
||||
for key in ("grade", "position", "job_grade", "rank", "level"):
|
||||
value = str(context.get(key) or "").strip()
|
||||
if value and value not in terms:
|
||||
terms.append(value)
|
||||
|
||||
grade_match = re.fullmatch(r"[Pp](\d{1,2})", str(context.get("grade") or "").strip())
|
||||
if grade_match:
|
||||
grade = int(grade_match.group(1))
|
||||
for start in range(max(0, grade - 4), grade + 1):
|
||||
for end in range(grade, min(12, grade + 4) + 1):
|
||||
if start >= end:
|
||||
continue
|
||||
for separator in ("~", "~", "-", "至"):
|
||||
term = f"P{start}{separator}P{end}"
|
||||
if term not in terms:
|
||||
terms.append(term)
|
||||
return terms
|
||||
|
||||
|
||||
@staticmethod
|
||||
def _resolve_knowledge_question(payload: UserAgentRequest) -> str:
|
||||
return str(payload.context_json.get("user_input_text") or payload.message or "").strip()
|
||||
@@ -484,6 +558,8 @@ class UserAgentKnowledgeMixin(UserAgentKnowledgeHelpersMixin):
|
||||
def _collect_direct_knowledge_answer_lines(
|
||||
self,
|
||||
ordered_evidence_items: list[dict[str, Any]],
|
||||
*,
|
||||
query_terms: list[str] | None = None,
|
||||
) -> list[str]:
|
||||
if not ordered_evidence_items:
|
||||
return []
|
||||
@@ -509,8 +585,18 @@ class UserAgentKnowledgeMixin(UserAgentKnowledgeHelpersMixin):
|
||||
lines: list[str] = []
|
||||
seen: set[str] = set()
|
||||
for item in related_items:
|
||||
rendered = self._render_knowledge_evidence_text(item)
|
||||
for line in rendered.splitlines():
|
||||
item_kind = str(item.get("kind") or "").strip()
|
||||
item_content = str(item.get("content") or "")
|
||||
if item_kind == "paragraph" or self._has_inline_numbered_knowledge_items(item_content):
|
||||
rendered = self._focus_knowledge_segment_content(
|
||||
item_content,
|
||||
query_terms or [],
|
||||
)
|
||||
rendered_lines = self._split_inline_numbered_knowledge_items(rendered)
|
||||
else:
|
||||
rendered = self._render_knowledge_evidence_text(item)
|
||||
rendered_lines = rendered.splitlines()
|
||||
for line in rendered_lines:
|
||||
normalized = str(line or "").strip()
|
||||
if not normalized or normalized in seen:
|
||||
continue
|
||||
@@ -573,13 +659,21 @@ class UserAgentKnowledgeMixin(UserAgentKnowledgeHelpersMixin):
|
||||
or "相关制度"
|
||||
).strip()
|
||||
user_name = str(payload.context_json.get("name") or "").strip()
|
||||
prefix = f"{user_name},您好。\n" if user_name else ""
|
||||
answer_lines: list[str] = []
|
||||
if user_name:
|
||||
answer_lines.append(f"{user_name},您好。")
|
||||
if not hits:
|
||||
return (
|
||||
f"{prefix}我已经从《{title}》中检索到与你这次问题相关的制度依据,"
|
||||
"但本次答案生成环节暂时没有成功返回。请稍后重试一次;如果仍然失败,"
|
||||
"建议先检查主对话模型的连通性。"
|
||||
self._append_markdown_section(
|
||||
answer_lines,
|
||||
"结论",
|
||||
[f"当前没有拿到可用于回答这个问题的《{title}》知识库命中。"],
|
||||
)
|
||||
self._append_markdown_section(
|
||||
answer_lines,
|
||||
"说明",
|
||||
["- 我不会用相似主题或外部常识硬凑答案;请补充更具体的关键词后再试一次。"],
|
||||
)
|
||||
return "\n".join(answer_lines).strip()
|
||||
|
||||
evidence_lines: list[str] = []
|
||||
for item in evidence_items[:3]:
|
||||
@@ -614,19 +708,28 @@ class UserAgentKnowledgeMixin(UserAgentKnowledgeHelpersMixin):
|
||||
evidence_lines.append(f"- **《{item_title}》**:{excerpt}")
|
||||
|
||||
if not evidence_lines:
|
||||
return (
|
||||
f"{prefix}当前《{title}》里可用于回答的关键条款还不够明确。"
|
||||
"请补充费用类型、适用地区、职级或具体业务场景,我再继续帮你缩小范围。"
|
||||
self._append_markdown_section(
|
||||
answer_lines,
|
||||
"结论",
|
||||
[f"当前《{title}》里可用于回答这个问题的关键条款还不够明确。"],
|
||||
)
|
||||
self._append_markdown_section(
|
||||
answer_lines,
|
||||
"说明",
|
||||
["- 请补充费用类型、适用地区、职级或具体业务场景,我再继续帮你缩小范围。"],
|
||||
)
|
||||
return "\n".join(answer_lines).strip()
|
||||
|
||||
return "\n".join(
|
||||
[
|
||||
f"{prefix}我先根据当前制度依据给出可以确认的部分。",
|
||||
"",
|
||||
"**依据**:",
|
||||
*evidence_lines,
|
||||
"",
|
||||
"**说明**:以上只使用当前命中的知识库证据;没有在证据中出现的适用条件或金额,我不会替你默认补齐。",
|
||||
]
|
||||
).strip()
|
||||
self._append_markdown_section(
|
||||
answer_lines,
|
||||
"结论",
|
||||
["我先根据当前制度依据给出可以确认的部分。"],
|
||||
)
|
||||
self._append_markdown_section(answer_lines, "依据", evidence_lines)
|
||||
self._append_markdown_section(
|
||||
answer_lines,
|
||||
"说明",
|
||||
["- 以上只使用当前命中的知识库证据;没有在证据中出现的适用条件或金额,我不会替你默认补齐。"],
|
||||
)
|
||||
return "\n".join(answer_lines).strip()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user