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

@@ -0,0 +1,44 @@
from __future__ import annotations
from typing import Annotated
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from app.api.deps import get_db
from app.api.v1.endpoints.settings import require_hermes_agent_token
from app.schemas.common import ErrorResponse
from app.schemas.hermes import HermesCallbackRead, HermesCallbackWrite
from app.services.hermes_callbacks import HermesCallbackService
router = APIRouter(prefix="/hermes")
DbSession = Annotated[Session, Depends(get_db)]
@router.post(
"/callback",
response_model=HermesCallbackRead,
dependencies=[Depends(require_hermes_agent_token)],
summary="接收 Hermes 通用回调",
description="所有 Hermes 任务统一通过该入口回传进度或完成结果,服务端依据 type 分发到对应业务处理器。",
responses={
status.HTTP_400_BAD_REQUEST: {
"model": ErrorResponse,
"description": "Hermes 回调载荷不合法或任务类型暂不支持。",
},
status.HTTP_404_NOT_FOUND: {
"model": ErrorResponse,
"description": "回调引用的 AgentRun 不存在。",
},
},
)
def handle_hermes_callback(
payload: HermesCallbackWrite,
db: DbSession,
) -> HermesCallbackRead:
try:
return HermesCallbackService(db).handle_callback(payload)
except LookupError as exc:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc
except ValueError as exc:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)) from exc

View File

@@ -1,6 +1,6 @@
from __future__ import annotations
from datetime import UTC, datetime
from datetime import UTC, datetime, timedelta
from typing import Annotated
from fastapi import APIRouter, Body, Depends, HTTPException, Query, status
@@ -26,7 +26,11 @@ from app.schemas.knowledge import (
LlmWikiSummaryUpdateWrite,
)
from app.services.agent_runs import AgentRunService
from app.services.knowledge import KNOWLEDGE_INGEST_STATUS_SYNCING, KnowledgeService
from app.services.knowledge import (
KNOWLEDGE_INGEST_STATUS_FAILED,
KNOWLEDGE_INGEST_STATUS_SYNCING,
KnowledgeService,
)
from app.services.llm_wiki import LlmWikiService
from app.services.llm_wiki_tasks import llm_wiki_task_manager
@@ -169,6 +173,78 @@ def sync_llm_wiki(
for item in knowledge_service.list_folder_documents(folder=payload.folder)
if str(item.get("id") or "").strip() and (not requested_ids or str(item.get("id") or "").strip() in requested_ids)
]
active_run = None
for item in run_service.list_runs(
agent=AgentName.HERMES.value,
status=AgentRunStatus.RUNNING.value,
limit=100,
):
if item.route_json.get("job_type") != "llm_wiki_sync":
continue
if item.route_json.get("folder") != payload.folder:
continue
heartbeat_raw = str(item.route_json.get("heartbeat_at") or "").strip()
heartbeat_at = None
if heartbeat_raw:
try:
heartbeat_at = datetime.fromisoformat(heartbeat_raw)
except ValueError:
heartbeat_at = None
last_seen_at = heartbeat_at or item.started_at
if last_seen_at.tzinfo is None:
last_seen_at = last_seen_at.replace(tzinfo=UTC)
if datetime.now(UTC) - last_seen_at > timedelta(minutes=30):
stale_document_ids = [
str(document_id).strip()
for document_id in list(item.route_json.get("requested_document_ids") or [])
if str(document_id).strip()
]
if stale_document_ids:
knowledge_service.set_document_ingest_statuses(
stale_document_ids,
status_code=KNOWLEDGE_INGEST_STATUS_FAILED,
agent_run_id=item.run_id,
)
run_service.merge_route_json(
item.run_id,
{
"phase": "stale_failed",
"heartbeat_at": datetime.now(UTC).isoformat(),
},
status=AgentRunStatus.FAILED.value,
result_summary="Hermes 归纳任务长时间无心跳,已自动标记为失败。",
error_message="Hermes callback heartbeat timed out.",
finished_at=datetime.now(UTC),
)
continue
if (
not target_document_ids
or not list(item.route_json.get("requested_document_ids") or [])
or bool(
set(target_document_ids)
& {
str(document_id).strip()
for document_id in list(item.route_json.get("requested_document_ids") or [])
if str(document_id).strip()
}
)
):
active_run = item
break
if active_run is not None:
return LlmWikiSyncTaskRead(
ok=True,
agent_run_id=active_run.run_id,
folder=payload.folder,
document_ids=[
str(item).strip()
for item in list(active_run.route_json.get("requested_document_ids") or target_document_ids)
if str(item).strip()
],
queued_at=active_run.started_at,
status=active_run.status,
summary="已有 Hermes 归纳任务正在执行,已复用当前任务而不是重复创建。",
)
task_asset = db.scalar(
select(AgentAsset).where(AgentAsset.code == "task.hermes.llm_wiki_rule_formation")
)
@@ -186,6 +262,8 @@ def sync_llm_wiki(
"folder": payload.folder,
"force": payload.force,
"requested_document_ids": target_document_ids,
"requested_by_username": current_user.username,
"requested_by_name": current_user.name,
"progress": {
"total_documents": len(target_document_ids),
"completed_documents": 0,

View File

@@ -7,6 +7,7 @@ from app.api.v1.endpoints.auth import router as auth_router
from app.api.v1.endpoints.bootstrap import router as bootstrap_router
from app.api.v1.endpoints.employees import router as employees_router
from app.api.v1.endpoints.health import router as health_router
from app.api.v1.endpoints.hermes import router as hermes_router
from app.api.v1.endpoints.knowledge import router as knowledge_router
from app.api.v1.endpoints.ocr import router as ocr_router
from app.api.v1.endpoints.ontology import router as ontology_router
@@ -17,6 +18,7 @@ from app.api.v1.endpoints.system_logs import router as system_logs_router
router = APIRouter()
router.include_router(health_router, tags=["health"])
router.include_router(hermes_router, tags=["hermes"])
router.include_router(bootstrap_router, tags=["bootstrap"])
router.include_router(auth_router, tags=["auth"])
router.include_router(agent_assets_router, tags=["agent-assets"])