feat(backend): knowledge iteration + vision + neo4j + response contract source
dependencies.py 拆分为 dependencies/ 包; 新增 vision/ragflow_ingestion/neo4j 客户端与 h5_ai_task; alembic 045 图置信度迁移; 响应契约统一收尾。
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
# =============================================================================
|
||||
# 企微IT智能服务台 — 阶段5 自动化 外部客户端包
|
||||
# =============================================================================
|
||||
# 说明:自动化引擎专用外部系统客户端集合(环境变量 AUTOMATION_* 驱动)。
|
||||
# 导出基类、异常、各系统客户端及对应 get_*_client 工厂函数。
|
||||
# =============================================================================
|
||||
|
||||
from app.core.clients.base import (
|
||||
BaseClient,
|
||||
BaseClientError,
|
||||
ClientAPIError,
|
||||
ClientAuthError,
|
||||
ClientConfigError,
|
||||
ClientConnectionError,
|
||||
)
|
||||
from app.core.clients.huorong import HuorongClient, get_huorong_client
|
||||
from app.core.clients.lianruan import LianruanClient, get_lianruan_client
|
||||
from app.core.clients.dify import DifyClient, get_dify_client
|
||||
from app.core.clients.ragflow import RagFlowClient, get_ragflow_client
|
||||
from app.core.clients.ehr import BeisenEHRClient, get_ehr_client
|
||||
|
||||
__all__ = [
|
||||
"BaseClient",
|
||||
"BaseClientError",
|
||||
"ClientConfigError",
|
||||
"ClientConnectionError",
|
||||
"ClientAuthError",
|
||||
"ClientAPIError",
|
||||
"HuorongClient",
|
||||
"get_huorong_client",
|
||||
"LianruanClient",
|
||||
"get_lianruan_client",
|
||||
"DifyClient",
|
||||
"get_dify_client",
|
||||
"RagFlowClient",
|
||||
"get_ragflow_client",
|
||||
"BeisenEHRClient",
|
||||
"get_ehr_client",
|
||||
]
|
||||
@@ -0,0 +1,184 @@
|
||||
# =============================================================================
|
||||
# 企微IT智能服务台 — 阶段5 自动化 Dify 意图识别客户端
|
||||
# =============================================================================
|
||||
# 说明:自动化引擎的意图识别客户端,调用 Dify(OpenAI 兼容代理
|
||||
# http://yw-dify.dc.servyou-it.com/dify2openai/)识别员工诉求命中的场景。
|
||||
#
|
||||
# 生产基址(任务指定):http://yw-dify.dc.servyou-it.com/dify2openai/
|
||||
#
|
||||
# 返回结构(供 IntentRouter 使用):
|
||||
# {"scenario_key": str|None, "confidence": float, "raw": str, "error": str}
|
||||
# scenario_key ∈ {password_reset, software_install, virus_dispose, terminal_locate}
|
||||
#
|
||||
# 降级:Dify 未配置或调用失败 → _fallback_intent 走关键词兜底(关键词取自
|
||||
# app.services.automation.DEFAULT_SCENARIO_CONFIGS),保证无真实环境也能闭环。
|
||||
# =============================================================================
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from app.config import settings
|
||||
from app.core.clients.base import (
|
||||
BaseClient,
|
||||
BaseClientError,
|
||||
ClientAPIError,
|
||||
ClientConfigError,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_DEFAULT_TIMEOUT = 30.0
|
||||
# 意图识别提示词:要求模型仅输出 JSON
|
||||
_SYSTEM_PROMPT = (
|
||||
"你是IT服务台意图分类器。根据用户诉求,判断其属于以下哪个场景之一,"
|
||||
"并仅输出一个 JSON 对象,不要输出任何额外文字:\n"
|
||||
'{"scenario_key": "password_reset|software_install|virus_dispose|terminal_locate|unknown", '
|
||||
'"confidence": 0.0~1.0}\n'
|
||||
"场景说明:\n"
|
||||
"- password_reset: 忘记/重置密码、账号密码相关\n"
|
||||
"- software_install: 安装/下载软件\n"
|
||||
"- virus_dispose: 病毒、木马、勒索、杀毒\n"
|
||||
"- terminal_locate: 定位/查找我的电脑或终端\n"
|
||||
"- unknown: 不属于以上任何一类\n"
|
||||
"confidence 表示你对该判断的置信度(0~1)。"
|
||||
)
|
||||
|
||||
|
||||
class DifyClient(BaseClient):
|
||||
"""Dify 意图识别客户端(OpenAI 兼容)。"""
|
||||
|
||||
system = "dify"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
base_url: str,
|
||||
api_key: str,
|
||||
timeout: float = _DEFAULT_TIMEOUT,
|
||||
audit: Any = None,
|
||||
max_retries: int = 2,
|
||||
):
|
||||
if not base_url:
|
||||
raise ClientConfigError("Dify base_url 未配置")
|
||||
if not api_key:
|
||||
raise ClientConfigError("Dify api_key 未配置")
|
||||
# 统一 base_url 形态(确保末尾含 /v1 由调用方决定,此处仅保证无尾斜杠)
|
||||
super().__init__(base_url=base_url.rstrip("/"), timeout=timeout, audit=audit, max_retries=max_retries)
|
||||
self.api_key = api_key
|
||||
|
||||
def _chat_url(self) -> str:
|
||||
"""OpenAI 兼容 chat/completions 端点。"""
|
||||
base = self.base_url
|
||||
if base.endswith("/v1"):
|
||||
return f"{base}/chat/completions"
|
||||
return f"{base}/v1/chat/completions"
|
||||
|
||||
async def detect_intent(
|
||||
self, description: str, employee_id: str = ""
|
||||
) -> Dict[str, Any]:
|
||||
"""调用 Dify 识别意图。
|
||||
|
||||
Returns:
|
||||
Dict: {scenario_key, confidence, raw, error}
|
||||
"""
|
||||
prompt = description or ""
|
||||
body = {
|
||||
"model": "dify",
|
||||
"messages": [
|
||||
{"role": "system", "content": _SYSTEM_PROMPT},
|
||||
{"role": "user", "content": prompt},
|
||||
],
|
||||
"temperature": 0,
|
||||
"response_format": {"type": "json_object"},
|
||||
"user": employee_id or "automation",
|
||||
}
|
||||
headers = {
|
||||
"Authorization": f"Bearer {self.api_key}",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
try:
|
||||
data = await self._request(
|
||||
"POST", "/v1/chat/completions", json_body=body, headers=headers
|
||||
)
|
||||
except BaseClientError as e:
|
||||
logger.warning(f"Dify 调用失败: {e.message}")
|
||||
fb = self._fallback_intent(description)
|
||||
fb["error"] = e.message
|
||||
return fb
|
||||
|
||||
# 解析 OpenAI 兼容响应
|
||||
try:
|
||||
choices = data.get("choices") or []
|
||||
content = choices[0]["message"]["content"] if choices else ""
|
||||
parsed = json.loads(content)
|
||||
scenario_key = parsed.get("scenario_key")
|
||||
confidence = float(parsed.get("confidence", 0.0))
|
||||
return {
|
||||
"scenario_key": scenario_key if scenario_key != "unknown" else None,
|
||||
"confidence": confidence,
|
||||
"raw": content,
|
||||
"error": "",
|
||||
}
|
||||
except Exception as e: # noqa: BLE001
|
||||
logger.warning(f"Dify 响应解析失败: {e}")
|
||||
fb = self._fallback_intent(description)
|
||||
fb["error"] = f"parse_error: {e}"
|
||||
return fb
|
||||
|
||||
@staticmethod
|
||||
def _fallback_intent(description: str) -> Dict[str, Any]:
|
||||
"""关键词兜底(无 Dify / 解析失败时)。
|
||||
|
||||
关键词取自 DEFAULT_SCENARIO_CONFIGS 的 trigger_conditions.keywords;
|
||||
命中即返回该场景,置信度取 0.6(恰好达到阈值,可继续编排)。
|
||||
"""
|
||||
text = (description or "").lower()
|
||||
scenario_key: Optional[str] = None
|
||||
try: # 懒加载,避免循环依赖
|
||||
from app.services.automation import DEFAULT_SCENARIO_CONFIGS
|
||||
|
||||
for key, cfg in DEFAULT_SCENARIO_CONFIGS.items():
|
||||
triggers = cfg.get("trigger_conditions") or {}
|
||||
keywords = triggers.get("keywords") or []
|
||||
if any(kw.lower() in text for kw in keywords):
|
||||
scenario_key = key
|
||||
break
|
||||
except Exception: # noqa: BLE001
|
||||
pass
|
||||
return {
|
||||
"scenario_key": scenario_key,
|
||||
"confidence": 0.6 if scenario_key else 0.0,
|
||||
"raw": "",
|
||||
"error": "fallback",
|
||||
}
|
||||
|
||||
async def test_connection(self) -> Dict[str, Any]:
|
||||
"""连接测试(轻量 chat 探测)。"""
|
||||
try:
|
||||
result = await self.detect_intent("测试连接")
|
||||
return {"success": True, "message": "Dify 可用", "scenario_key": result.get("scenario_key")}
|
||||
except BaseClientError as e:
|
||||
return {"success": False, "message": e.message}
|
||||
|
||||
|
||||
async def get_dify_client(audit: Any = None) -> Optional[DifyClient]:
|
||||
"""构建 Dify 客户端(环境变量 AUTOMATION_DIFY_* 驱动)。
|
||||
|
||||
未配置 → 返回 None(IntentRouter 自动走关键词兜底)。
|
||||
|
||||
Returns:
|
||||
Optional[DifyClient]: 配置完整时返回,否则 None。
|
||||
"""
|
||||
base_url = getattr(settings, "automation_dify_base_url", "") or ""
|
||||
api_key = getattr(settings, "automation_dify_api_key", "") or ""
|
||||
if not (base_url and api_key):
|
||||
logger.debug("Dify 未配置(AUTOMATION_DIFY_*),返回 None")
|
||||
return None
|
||||
try:
|
||||
return DifyClient(base_url=base_url, api_key=api_key, audit=audit)
|
||||
except ClientConfigError as e:
|
||||
logger.warning(f"Dify 客户端构建失败: {e.message}")
|
||||
return None
|
||||
@@ -0,0 +1,117 @@
|
||||
# =============================================================================
|
||||
# 企微IT智能服务台 — 阶段5 自动化 北森 EHR 静态映射兜底客户端
|
||||
# =============================================================================
|
||||
# 说明:自动化引擎的终端映射兜底客户端(环境变量 AUTOMATION_EHR_* 驱动)。
|
||||
# 当联软(主源)不可用或未解析到终端时,使用北森 EHR 静态映射给出
|
||||
# 员工→部门/资产 hint(注意:EHR 通常不提供火绒 client_id,仅作兜底展示)。
|
||||
#
|
||||
# 主用方法:
|
||||
# - get_terminal_by_employee(employee_id) -> dict | None
|
||||
# 返回 {employee_id, department, asset_no, hint} 或 None(无记录)。
|
||||
# =============================================================================
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from app.config import settings
|
||||
from app.core.clients.base import (
|
||||
BaseClient,
|
||||
BaseClientError,
|
||||
ClientAPIError,
|
||||
ClientAuthError,
|
||||
ClientConfigError,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_DEFAULT_TIMEOUT = 15.0
|
||||
|
||||
|
||||
class BeisenEHRClient(BaseClient):
|
||||
"""北森 EHR 静态映射兜底客户端(Bearer/API Key 认证)。"""
|
||||
|
||||
system = "ehr"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
base_url: str,
|
||||
api_key: str,
|
||||
timeout: float = _DEFAULT_TIMEOUT,
|
||||
audit: Any = None,
|
||||
max_retries: int = 1,
|
||||
):
|
||||
if not base_url:
|
||||
raise ClientConfigError("EHR base_url 未配置")
|
||||
if not api_key:
|
||||
raise ClientConfigError("EHR api_key 未配置")
|
||||
super().__init__(base_url=base_url.rstrip("/"), timeout=timeout, audit=audit, max_retries=max_retries)
|
||||
self.api_key = api_key
|
||||
|
||||
async def get_terminal_by_employee(self, employee_id: str) -> Optional[Dict[str, Any]]:
|
||||
"""按员工账号查询静态映射(兜底)。
|
||||
|
||||
Args:
|
||||
employee_id: 员工企微 UserID
|
||||
|
||||
Returns:
|
||||
Optional[Dict]: {employee_id, department, asset_no, hint} 或 None。
|
||||
默认实现为占位:未对接真实北森接口,返回基于本地静态表的 hint。
|
||||
结构上可被单元测试 mock(注入真实 client 即可)。
|
||||
"""
|
||||
headers = {
|
||||
"Authorization": f"Bearer {self.api_key}",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
# 真实环境:GET /api/v1/employee/{employee_id}/asset
|
||||
# 本地无真实环境,使用占位静态映射(可在 settings/配置中扩展)。
|
||||
try:
|
||||
data = await self._request(
|
||||
"GET",
|
||||
f"/api/v1/employee/{employee_id}/asset",
|
||||
headers=headers,
|
||||
)
|
||||
except BaseClientError as e:
|
||||
logger.warning(f"EHR 查询失败 employee={employee_id}: {e.message}")
|
||||
return None
|
||||
|
||||
emp = (data.get("data", {}) or {}).get("employee", {}) or {}
|
||||
if not emp:
|
||||
return None
|
||||
return {
|
||||
"employee_id": employee_id,
|
||||
"department": emp.get("department", ""),
|
||||
"asset_no": emp.get("asset_no", ""),
|
||||
"hint": emp.get("asset_hint", "该员工暂无终端映射,建议转人工处理"),
|
||||
}
|
||||
|
||||
async def test_connection(self) -> Dict[str, Any]:
|
||||
"""连接测试(查询根路径)。"""
|
||||
headers = {"Authorization": f"Bearer {self.api_key}", "Content-Type": "application/json"}
|
||||
try:
|
||||
await self._request("GET", "/api/v1/ping", headers=headers)
|
||||
return {"success": True, "message": "EHR 连接成功"}
|
||||
except BaseClientError as e:
|
||||
return {"success": False, "message": e.message}
|
||||
|
||||
|
||||
async def get_ehr_client(audit: Any = None) -> Optional[BeisenEHRClient]:
|
||||
"""构建 EHR 客户端(环境变量 AUTOMATION_EHR_* 驱动)。
|
||||
|
||||
未配置 → 返回 None(mapping_resolver 跳过兜底)。
|
||||
|
||||
Returns:
|
||||
Optional[BeisenEHRClient]: 配置完整时返回,否则 None。
|
||||
"""
|
||||
base_url = getattr(settings, "automation_ehr_base_url", "") or ""
|
||||
api_key = getattr(settings, "automation_ehr_api_key", "") or ""
|
||||
if not (base_url and api_key):
|
||||
logger.debug("EHR 未配置(AUTOMATION_EHR_*),返回 None")
|
||||
return None
|
||||
try:
|
||||
return BeisenEHRClient(base_url=base_url, api_key=api_key, audit=audit)
|
||||
except ClientConfigError as e:
|
||||
logger.warning(f"EHR 客户端构建失败: {e.message}")
|
||||
return None
|
||||
@@ -0,0 +1,250 @@
|
||||
# =============================================================================
|
||||
# 企微IT智能服务台 — 阶段5 自动化 联软 LV7000 客户端
|
||||
# =============================================================================
|
||||
# 说明:自动化引擎使用的联软客户端(环境变量 AUTOMATION_LIANRUAN_* 驱动)。
|
||||
# 三层认证:① IP 白名单(联软后台配置,调用自动生效)② 账号密码 ③ Token。
|
||||
# Token 经 getToken 获取,30 分钟有效,本地缓存 + 提前 5 分钟刷新。
|
||||
#
|
||||
# 主用方法(自动化映射解析):
|
||||
# - query_dev_by_params(strusername=...) 员工账号 → 终端列表(核心映射)
|
||||
# 其余方法供排障/扩展。
|
||||
#
|
||||
# 返回 items 为 dict 子类(同时支持 .get() 与属性访问),以兼容
|
||||
# mapping_resolver / action_registry 对终端信息的两种访问方式。
|
||||
# =============================================================================
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
import logging
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
import httpx
|
||||
|
||||
from app.config import settings
|
||||
from app.core.clients.base import (
|
||||
BaseClient,
|
||||
BaseClientError,
|
||||
ClientAPIError,
|
||||
ClientAuthError,
|
||||
ClientConfigError,
|
||||
ClientConnectionError,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Token 有效期(秒)与提前刷新阈值
|
||||
_TOKEN_TTL = 1800
|
||||
_TOKEN_REFRESH_EARLY = 300
|
||||
_DEFAULT_TIMEOUT = 30.0
|
||||
_DEFAULT_PAGE_SIZE = 20
|
||||
|
||||
|
||||
class _TerminalRow(dict):
|
||||
"""终端信息行:同时支持 dict.get 与属性访问。"""
|
||||
|
||||
def __init__(self, **kw: Any):
|
||||
super().__init__(**kw)
|
||||
for k, v in kw.items():
|
||||
setattr(self, k, v)
|
||||
|
||||
|
||||
class LianruanClient(BaseClient):
|
||||
"""联软 LV7000 终端安全管理客户端(自动化引擎专用)。"""
|
||||
|
||||
system = "lianruan"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
base_url: str,
|
||||
api_account: str,
|
||||
api_password: str,
|
||||
validate_key: str = "",
|
||||
timeout: float = _DEFAULT_TIMEOUT,
|
||||
audit: Any = None,
|
||||
max_retries: int = 2,
|
||||
):
|
||||
if not base_url:
|
||||
raise ClientConfigError("联软 base_url 未配置")
|
||||
if not api_account or not api_password:
|
||||
raise ClientConfigError("联软 账号/密码 未配置")
|
||||
super().__init__(base_url=base_url, timeout=timeout, audit=audit, max_retries=max_retries)
|
||||
self.api_account = api_account
|
||||
self.api_password = api_password
|
||||
self.validate_key = validate_key
|
||||
self._token: str = ""
|
||||
self._token_expire: float = 0.0
|
||||
|
||||
# ======================================================================
|
||||
# Token 管理(第三层认证)
|
||||
# ======================================================================
|
||||
async def _ensure_token(self) -> str:
|
||||
"""确保 Token 有效,过期则刷新(提前 5 分钟)。"""
|
||||
now = time.time()
|
||||
if self._token and now < self._token_expire - _TOKEN_REFRESH_EARLY:
|
||||
return self._token
|
||||
try:
|
||||
client = await self._get_client()
|
||||
params = {
|
||||
"act": "getToken",
|
||||
"apiAccount": self.api_account,
|
||||
"apiPassword": self.api_password,
|
||||
}
|
||||
if self.validate_key:
|
||||
params["validatekey"] = self.validate_key
|
||||
resp = await client.get(f"{self.base_url}/token", params=params)
|
||||
resp.raise_for_status()
|
||||
data = resp.json()
|
||||
except httpx.ConnectError as e:
|
||||
raise ClientConnectionError(f"无法连接联软: {e}")
|
||||
except httpx.TimeoutException as e:
|
||||
raise ClientConnectionError(f"联软连接超时: {e}")
|
||||
except httpx.HTTPStatusError as e:
|
||||
raise ClientAPIError(message=f"联软 Token HTTP 错误: {e}", status=e.response.status_code)
|
||||
|
||||
if data.get("status") != "SUCCESS":
|
||||
raise ClientAuthError(f"联软 Token 获取失败: {data.get('msg', '')}")
|
||||
self._token = str(data.get("data") or data.get("token") or data.get("rows") or "")
|
||||
self._token_expire = now + _TOKEN_TTL
|
||||
return self._token
|
||||
|
||||
# ======================================================================
|
||||
# 统一请求(带认证参数)
|
||||
# ======================================================================
|
||||
async def _call(
|
||||
self, path: str, act: str, params: Optional[Dict[str, Any]] = None, method: str = "GET"
|
||||
) -> Dict[str, Any]:
|
||||
"""发送联软请求(自动附带 token + apiAccount + apiPassword)。"""
|
||||
token = await self._ensure_token()
|
||||
full: Dict[str, Any] = {
|
||||
"act": act,
|
||||
"apiAccount": self.api_account,
|
||||
"apiPassword": self.api_password,
|
||||
"token": token,
|
||||
}
|
||||
if self.validate_key:
|
||||
full["validatekey"] = self.validate_key
|
||||
if params:
|
||||
full.update(params)
|
||||
|
||||
try:
|
||||
client = await self._get_client()
|
||||
if method.upper() == "POST":
|
||||
resp = await client.post(f"{self.base_url}{path}", data=full)
|
||||
else:
|
||||
resp = await client.get(f"{self.base_url}{path}", params=full)
|
||||
resp.raise_for_status()
|
||||
data = resp.json()
|
||||
except httpx.ConnectError as e:
|
||||
raise ClientConnectionError(f"无法连接联软: {e}")
|
||||
except httpx.TimeoutException as e:
|
||||
raise ClientConnectionError(f"联软连接超时: {e}")
|
||||
except httpx.HTTPStatusError as e:
|
||||
raise ClientAPIError(message=f"联软 HTTP 错误: {e}", status=e.response.status_code)
|
||||
|
||||
status = data.get("status", "")
|
||||
if status == "INVALID":
|
||||
# Token 可能失效,清除缓存以便下次刷新
|
||||
self._token = ""
|
||||
self._token_expire = 0.0
|
||||
raise ClientAuthError(f"联软认证失败(IP 白名单/Token 无效): {data.get('msg', '')}")
|
||||
if status == "ERROR":
|
||||
raise ClientAPIError(message=f"联软业务错误: {data.get('msg', '')}", data=data)
|
||||
if status == "Exceed":
|
||||
raise ClientAPIError(message=f"联软数据量超限: {data.get('msg', '')}", data=data)
|
||||
if status not in ("", "SUCCESS"):
|
||||
raise ClientAPIError(message=f"联软未知状态: {status}", data=data)
|
||||
return data
|
||||
|
||||
# ======================================================================
|
||||
# 终端设备查询(核心映射接口)
|
||||
# ======================================================================
|
||||
async def query_dev_by_params(
|
||||
self,
|
||||
strusername: str = "",
|
||||
strdevname: str = "",
|
||||
strdevip: str = "",
|
||||
strmac: str = "",
|
||||
page: int = 1,
|
||||
per_page: int = _DEFAULT_PAGE_SIZE,
|
||||
) -> Dict[str, Any]:
|
||||
"""按员工账号等参数查询终端(strusername 为映射金钥匙)。
|
||||
|
||||
Returns:
|
||||
Dict: {"items": [终端信息], "total": int}
|
||||
终端信息为 _TerminalRow(支持 .get 与属性访问)。
|
||||
"""
|
||||
params: Dict[str, Any] = {}
|
||||
if strusername:
|
||||
params["strusername"] = strusername
|
||||
if strdevname:
|
||||
params["strdevname"] = strdevname
|
||||
if strdevip:
|
||||
params["strdevip"] = strdevip
|
||||
if strmac:
|
||||
params["strmac"] = strmac
|
||||
params["page"] = str(page)
|
||||
params["rows"] = str(per_page)
|
||||
|
||||
data = await self._call("/terminal", "queryDevByParams", params)
|
||||
rows = data.get("rows", []) or []
|
||||
items = [_TerminalRow(**row) for row in rows]
|
||||
return {"items": items, "total": data.get("total", len(items))}
|
||||
|
||||
# ======================================================================
|
||||
# 扩展查询(排障/管理用)
|
||||
# ======================================================================
|
||||
async def get_dev_all_info(self, strdevname: str = "", strdevip: str = "") -> Dict[str, Any]:
|
||||
"""查询终端详情。"""
|
||||
params: Dict[str, Any] = {}
|
||||
if strdevname:
|
||||
params["strdevname"] = strdevname
|
||||
if strdevip:
|
||||
params["strdevip"] = strdevip
|
||||
return await self._call("/devallinfoshowwithpaging", "getDevAllInfo", params)
|
||||
|
||||
async def get_user_info_by_account(self, useraccount: str) -> Optional[Dict[str, Any]]:
|
||||
"""按账号查询用户信息。"""
|
||||
data = await self._call("/querydeptuser", "getUserInfoByAccount", {"useraccount": useraccount})
|
||||
rows = data.get("rows", data.get("row", []))
|
||||
if rows:
|
||||
row = rows[0] if isinstance(rows, list) else rows
|
||||
return dict(row)
|
||||
return None
|
||||
|
||||
async def test_connection(self) -> Dict[str, Any]:
|
||||
"""连接测试(取 Token)。"""
|
||||
try:
|
||||
token = await self._ensure_token()
|
||||
return {"success": bool(token), "message": "联软连接成功" if token else "Token 获取失败"}
|
||||
except BaseClientError as e:
|
||||
return {"success": False, "message": e.message}
|
||||
|
||||
|
||||
async def get_lianruan_client(db: Any = None, audit: Any = None) -> Optional[LianruanClient]:
|
||||
"""构建联软客户端(环境变量 AUTOMATION_LIANRUAN_* 驱动)。
|
||||
|
||||
必填项缺失 → 返回 None(映射解析器转 EHR 兜底)。
|
||||
|
||||
Returns:
|
||||
Optional[LianruanClient]: 配置完整时返回,否则 None。
|
||||
"""
|
||||
base_url = getattr(settings, "automation_lianruan_base_url", "") or ""
|
||||
api_account = getattr(settings, "automation_lianruan_api_account", "") or ""
|
||||
api_password = getattr(settings, "automation_lianruan_api_password", "") or ""
|
||||
validate_key = getattr(settings, "automation_lianruan_validate_key", "") or ""
|
||||
if not (base_url and api_account and api_password):
|
||||
logger.debug("联软未配置(AUTOMATION_LIANRUAN_*),返回 None")
|
||||
return None
|
||||
try:
|
||||
return LianruanClient(
|
||||
base_url=base_url,
|
||||
api_account=api_account,
|
||||
api_password=api_password,
|
||||
validate_key=validate_key,
|
||||
audit=audit,
|
||||
)
|
||||
except ClientConfigError as e:
|
||||
logger.warning(f"联软客户端构建失败: {e.message}")
|
||||
return None
|
||||
@@ -0,0 +1,139 @@
|
||||
# =============================================================================
|
||||
# 企微IT智能服务台 — 阶段5 自动化 RAGFlow 知识库检索客户端
|
||||
# =============================================================================
|
||||
# 说明:自动化引擎的知识检索客户端(环境变量 AUTOMATION_RAGFLOW_* 驱动)。
|
||||
# 基址默认内网 :9380(任务指定)。
|
||||
#
|
||||
# 检索策略默认(可在调用时覆盖):
|
||||
# - top_k=6:返回相关性最高的前 N 个片段(Top-K)
|
||||
# - truncation:单片段超长时截断到 512 字符,避免上下文溢出
|
||||
# - similarity_threshold=0.2
|
||||
#
|
||||
# 当前自动化引擎尚未在编排主链路强制调用 RAGFlow,但提供统一客户端以支撑
|
||||
# 病毒处置指引、软件安装知识等场景的后续接入(结构正确、可被 mock)。
|
||||
# =============================================================================
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from app.config import settings
|
||||
from app.core.clients.base import (
|
||||
BaseClient,
|
||||
BaseClientError,
|
||||
ClientAPIError,
|
||||
ClientAuthError,
|
||||
ClientConfigError,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_DEFAULT_TIMEOUT = 30.0
|
||||
_DEFAULT_TOP_K = 6
|
||||
_DEFAULT_SIMILARITY = 0.2
|
||||
# 单片段最大字符数(截断)
|
||||
_MAX_CHUNK_CHARS = 512
|
||||
|
||||
|
||||
class RagFlowClient(BaseClient):
|
||||
"""RAGFlow 知识检索引擎客户端(Bearer 认证)。"""
|
||||
|
||||
system = "ragflow"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
base_url: str,
|
||||
api_key: str,
|
||||
timeout: float = _DEFAULT_TIMEOUT,
|
||||
audit: Any = None,
|
||||
max_retries: int = 2,
|
||||
):
|
||||
if not base_url:
|
||||
raise ClientConfigError("RAGFlow base_url 未配置")
|
||||
if not api_key:
|
||||
raise ClientConfigError("RAGFlow api_key 未配置")
|
||||
super().__init__(base_url=base_url.rstrip("/"), timeout=timeout, audit=audit, max_retries=max_retries)
|
||||
self.api_key = api_key
|
||||
|
||||
async def retrieve(
|
||||
self,
|
||||
question: str,
|
||||
dataset_ids: Optional[List[str]] = None,
|
||||
top_k: int = _DEFAULT_TOP_K,
|
||||
similarity_threshold: float = _DEFAULT_SIMILARITY,
|
||||
) -> Dict[str, Any]:
|
||||
"""知识检索(Top-K + 截断)。
|
||||
|
||||
Args:
|
||||
question: 检索问题
|
||||
dataset_ids: 知识库 ID 列表(为空则检索全部)
|
||||
top_k: 返回片段数量上限
|
||||
similarity_threshold: 相似度阈值
|
||||
|
||||
Returns:
|
||||
Dict: {chunks: [...], total: int}
|
||||
chunks 中每个片段含 content(可能已截断)、score、document_name。
|
||||
"""
|
||||
body: Dict[str, Any] = {
|
||||
"question": question,
|
||||
"similarity_threshold": similarity_threshold,
|
||||
"top_k": top_k,
|
||||
}
|
||||
if dataset_ids:
|
||||
body["dataset_ids"] = dataset_ids
|
||||
|
||||
headers = {
|
||||
"Authorization": f"Bearer {self.api_key}",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
data = await self._request("POST", "/api/v1/retrieval", json_body=body, headers=headers)
|
||||
|
||||
raw_chunks = (data.get("data", {}) or {}).get("chunks", []) or []
|
||||
chunks: List[Dict[str, Any]] = []
|
||||
for c in raw_chunks[:top_k]:
|
||||
content = c.get("content", "") or ""
|
||||
if len(content) > _MAX_CHUNK_CHARS:
|
||||
content = content[:_MAX_CHUNK_CHARS] + "...(截断)"
|
||||
chunks.append(
|
||||
{
|
||||
"content": content,
|
||||
"score": c.get("score", 0.0),
|
||||
"document_name": c.get("document_name", ""),
|
||||
"kw": c.get("kw", ""),
|
||||
}
|
||||
)
|
||||
return {"chunks": chunks, "total": len(chunks)}
|
||||
|
||||
async def test_connection(self) -> Dict[str, Any]:
|
||||
"""连接测试(列出数据集)。"""
|
||||
headers = {"Authorization": f"Bearer {self.api_key}", "Content-Type": "application/json"}
|
||||
try:
|
||||
data = await self._request(
|
||||
"GET", "/api/v1/datasets", params={"page": 1, "page_size": 1}, headers=headers
|
||||
)
|
||||
total = (data.get("data", {}) or {}).get("total", 0)
|
||||
return {"success": True, "message": f"连接成功,共 {total} 个知识库"}
|
||||
except BaseClientError as e:
|
||||
return {"success": False, "message": e.message}
|
||||
|
||||
|
||||
async def get_ragflow_client(audit: Any = None) -> Optional[RagFlowClient]:
|
||||
"""构建 RAGFlow 客户端(环境变量 AUTOMATION_RAGFLOW_* 驱动)。
|
||||
|
||||
未配置 → 返回 None。
|
||||
|
||||
Returns:
|
||||
Optional[RagFlowClient]: 配置完整时返回,否则 None。
|
||||
"""
|
||||
base_url = getattr(settings, "automation_ragflow_base_url", "") or ""
|
||||
api_key = getattr(settings, "automation_ragflow_api_key", "") or ""
|
||||
if not (base_url and api_key):
|
||||
logger.debug("RAGFlow 未配置(AUTOMATION_RAGFLOW_*),返回 None")
|
||||
return None
|
||||
try:
|
||||
return RagFlowClient(base_url=base_url, api_key=api_key, audit=audit)
|
||||
except ClientConfigError as e:
|
||||
logger.warning(f"RAGFlow 客户端构建失败: {e.message}")
|
||||
return None
|
||||
Reference in New Issue
Block a user