feat: 集成Hermes智能体系统,增强聊天和差旅报销功能

This commit is contained in:
caoxiaozhu
2026-05-16 06:14:08 +00:00
parent 763afa0ee2
commit 212c935308
46 changed files with 8802 additions and 5372 deletions

View File

@@ -2,6 +2,7 @@ from __future__ import annotations
from dataclasses import dataclass
from datetime import UTC, datetime, timedelta
import re
from time import perf_counter
from typing import Any
@@ -37,6 +38,7 @@ from app.services.agent_conversations import AgentConversationService
from app.services.expense_claims import ExpenseClaimService
from app.services.agent_foundation import AgentFoundationService
from app.services.agent_runs import AgentRunService
from app.services.knowledge import KnowledgeService
from app.services.ontology import SemanticOntologyService
from app.services.user_agent import UserAgentService
@@ -62,6 +64,8 @@ class ExecutionOutcome:
PRIVILEGED_EXPENSE_QUERY_ROLE_CODES = {"finance"}
SELF_REFERENCE_KEYWORDS = ("我的", "我自己", "本人", "我名下", "给我查", "我提交", "我申请")
KNOWLEDGE_TRAVEL_TRIGGER_KEYWORDS = ("出差", "差旅", "报销多少钱", "能报多少", "一共可以报销", "一共能报销")
KNOWLEDGE_TRAVEL_EXPANSION_TERMS = ("差旅费", "住宿费", "出差补贴", "交通费", "酒店住宿限额标准", "出差补贴标准")
EXPENSE_QUERY_RECENT_WINDOW_DAYS = 10
EXPENSE_QUERY_PREVIEW_LIMIT = 20
EXPENSE_STATUS_LABELS = {
@@ -100,6 +104,7 @@ class OrchestratorService:
self.asset_service = AgentAssetService(db)
self.conversation_service = AgentConversationService(db)
self.expense_claim_service = ExpenseClaimService(db)
self.knowledge_service = KnowledgeService(db=db)
self.run_service = AgentRunService(db)
self.ontology_service = SemanticOntologyService(db)
self.user_agent_service = UserAgentService(db)
@@ -574,7 +579,12 @@ class OrchestratorService:
tool_name="knowledge.search",
request_json=self._build_ontology_json(ontology),
context_json=context_json,
executor=lambda: self._build_knowledge_answer(ontology, capabilities),
executor=lambda: self._build_knowledge_answer(
message=payload.message or "",
ontology=ontology,
capabilities=capabilities,
context_json=context_json,
),
fallback_factory=lambda exc: {
"message": f"知识检索暂时不可用,建议稍后重试:{exc}",
"degraded": True,
@@ -1348,18 +1358,154 @@ class OrchestratorService:
result["review_payload"] = response.review_payload.model_dump()
return result
@staticmethod
def _build_knowledge_answer(
self,
*,
message: str,
ontology: OntologyParseResult,
capabilities: dict[str, list[AgentAssetListItem | AgentAssetRead]],
context_json: dict[str, Any],
) -> dict[str, Any]:
referenced = [item.code for item in capabilities["rules"][:1]] or [
"knowledge.policy.default"
]
return {
"message": f"已路由到 User Agent占位知识结果建议先查看 {', '.join(referenced)}",
"references": referenced,
}
payload = self.knowledge_service.search_llm_wiki(message, limit=5)
expanded_query = self._build_knowledge_expanded_query(
message=message,
context_json=context_json,
)
if expanded_query and expanded_query != message:
expanded_payload = self.knowledge_service.search_llm_wiki(expanded_query, limit=5)
payload = self._merge_knowledge_search_payloads(
primary_payload=payload,
expanded_payload=expanded_payload,
original_query=message,
expanded_query=expanded_query,
)
references = [str(item).strip() for item in list(payload.get("references") or []) if str(item).strip()]
if references:
payload["references"] = references
return payload
@staticmethod
def _merge_knowledge_search_payloads(
*,
primary_payload: dict[str, Any],
expanded_payload: dict[str, Any],
original_query: str,
expanded_query: str,
) -> dict[str, Any]:
merged_by_code: dict[str, dict[str, Any]] = {}
for item in [
*list(primary_payload.get("hits") or []),
*list(expanded_payload.get("hits") or []),
]:
if not isinstance(item, dict):
continue
code = str(item.get("code") or "").strip()
if not code:
continue
existing = merged_by_code.get(code)
if existing is None or int(item.get("score") or 0) > int(existing.get("score") or 0):
merged_by_code[code] = item
if not merged_by_code:
return primary_payload
ranked_hits = sorted(
merged_by_code.values(),
key=lambda item: (
-int(item.get("score") or 0),
str(item.get("quality_status") or "") != "formal",
str(item.get("title") or ""),
),
)[:5]
merged_payload = dict(primary_payload)
merged_payload.update(
{
"query": original_query,
"expanded_query": expanded_query,
"record_count": len(ranked_hits),
"hits": ranked_hits,
"references": [
str(item.get("code") or "").strip()
for item in ranked_hits
if str(item.get("code") or "").strip()
],
}
)
return merged_payload
@staticmethod
def _build_knowledge_expanded_query(
*,
message: str,
context_json: dict[str, Any],
) -> str:
expansions: list[str] = []
normalized_message = "".join(str(message or "").split())
if normalized_message and any(keyword in normalized_message for keyword in KNOWLEDGE_TRAVEL_TRIGGER_KEYWORDS):
expansions.extend(KNOWLEDGE_TRAVEL_EXPANSION_TERMS)
location = OrchestratorService._extract_knowledge_location(message)
if location:
expansions.append(location)
grade = OrchestratorService._extract_knowledge_grade(message, context_json)
if grade:
expansions.append(grade)
history = context_json.get("conversation_history")
if not isinstance(history, list):
if not expansions:
return message
return "\n".join([message, " ".join(dict.fromkeys(expansions))])
previous_user_messages: list[str] = []
for item in reversed(history):
if not isinstance(item, dict):
continue
role = str(item.get("role") or "").strip()
content = str(item.get("content") or "").strip()
if role != "user" or not content or content == message:
continue
previous_user_messages.append(content)
if len(previous_user_messages) >= 2:
break
query_parts: list[str] = []
if previous_user_messages:
query_parts.extend(reversed(previous_user_messages))
query_parts.append(message)
if expansions:
query_parts.append(" ".join(dict.fromkeys(expansions)))
return "\n".join(query_parts)
@staticmethod
def _extract_knowledge_location(message: str) -> str:
patterns = (
r"去([\u4e00-\u9fff]{2,8})(?:出差|开会|培训)",
r"到([\u4e00-\u9fff]{2,8})(?:出差|开会|培训)",
r"在([\u4e00-\u9fff]{2,8})(?:出差|开会|培训)",
)
for pattern in patterns:
matched = re.search(pattern, str(message or ""))
if matched:
return matched.group(1)
return ""
@staticmethod
def _extract_knowledge_grade(message: str, context_json: dict[str, Any]) -> str:
matched = re.search(r"\bP[1-9]\d?\b", str(message or ""), re.IGNORECASE)
if matched:
return matched.group(0).upper()
for key in ("grade", "employee_grade", "employeeLevel", "employee_level"):
value = str(context_json.get(key) or "").strip()
if re.fullmatch(r"P[1-9]\d?", value, re.IGNORECASE):
return value.upper()
user = context_json.get("current_user")
if isinstance(user, dict):
for key in ("grade", "employee_grade", "employeeLevel", "employee_level"):
value = str(user.get(key) or "").strip()
if re.fullmatch(r"P[1-9]\d?", value, re.IGNORECASE):
return value.upper()
return ""
@staticmethod
def _build_rule_answer(ontology: OntologyParseResult) -> dict[str, Any]: