feat: 集成Hermes智能体系统,增强聊天和差旅报销功能
This commit is contained in:
44
server/src/app/api/v1/endpoints/hermes.py
Normal file
44
server/src/app/api/v1/endpoints/hermes.py
Normal 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
|
||||
@@ -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,
|
||||
|
||||
@@ -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"])
|
||||
|
||||
Reference in New Issue
Block a user