feat: 集成Hermes智能体系统,增强聊天和差旅报销功能
This commit is contained in:
@@ -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]:
|
||||
|
||||
Reference in New Issue
Block a user