feat(ai_agent): 完善AI适配器和服务功能

- 添加OpenClaw和Hermes健康检查接口
- 实现OpenClaw工具调用功能
- 重构AI适配器使用常量定义
- 增加AI技能系统支持
- 更新配置文件中的默认模型提供者设置

refactor(scheduler): 使用常量替换硬编码值

- 将硬编码的actor值替换为ActorValue常量
- 将receive_id_type替换为FeishuReceiveIdType枚举

refactor(audit): 统一审计日志常量使用

- 将硬编码的actor、source、risk_level等值替换为对应常量
- 更新审核服务中的状态和操作常量引用

refactor(approvals): 标准化审批模块常量使用

- 将applicant默认值替换为ActorValue.API常量
- 使用ApprovalStatus常量替代硬编码状态值
- 更新审核操作常量引用
```
This commit is contained in:
2026-07-06 00:02:03 +08:00
parent d82116d637
commit aa81fc5321
36 changed files with 2328 additions and 337 deletions

View File

@@ -2,7 +2,22 @@ from typing import Any
from sqlalchemy.orm import Session
from app.modules.ai_agent.adapters import get_adapter
from app.core.constants import ActorValue
from app.core.config import get_settings
from app.modules.ai_agent.adapters import HermesAdapter, OpenClawAdapter, get_adapter
from app.modules.ai_agent.constants import (
AIDefault,
AIProviderName,
AIRequestKey,
AIResponseKey,
)
from app.modules.ai_agent.skills import AISkillId, get_ai_skill
from app.modules.audit.constants import (
AuditAction,
AuditRiskLevel,
AuditSource,
AuditTargetType,
)
from app.modules.audit.schemas import AuditLogCreate
from app.modules.audit.service import AuditService
@@ -18,29 +33,109 @@ class AIService:
self,
prompt: str,
context: dict[str, Any] | None = None,
actor: str = "api",
source: str = "api",
actor: str = ActorValue.API,
source: str = AuditSource.API,
) -> dict[str, Any]:
adapter = get_adapter()
result = adapter.ask(prompt, context or {})
response = {
"provider": adapter.provider_name,
"answer": result["answer"],
"raw": result.get("raw", {}),
AIResponseKey.PROVIDER: adapter.provider_name,
AIResponseKey.ANSWER: result[AIResponseKey.ANSWER],
AIResponseKey.RAW: result.get(AIResponseKey.RAW, {}),
}
self.audit.log(
AuditLogCreate(
actor=actor,
source=source,
action="ai.ask",
target_type="ai",
risk_level="medium",
request_payload={"prompt": prompt, "context": context or {}},
action=AuditAction.AI_ASK,
target_type=AuditTargetType.AI,
risk_level=AuditRiskLevel.MEDIUM,
request_payload={
AIRequestKey.PROMPT: prompt,
AIRequestKey.CONTEXT: context or {},
},
response_payload=response,
)
)
return response
def run_skill(
self,
skill_id: AISkillId | str,
context: dict[str, Any] | None = None,
variables: dict[str, Any] | None = None,
actor: str = ActorValue.API,
) -> dict[str, Any]:
skill = get_ai_skill(skill_id)
return self.ask(
skill.render(variables),
context=context or {},
actor=actor,
source=skill.source,
)
def provider_health(self, actor: str = ActorValue.API) -> dict[str, Any]:
settings = get_settings()
openclaw = self._health_result(OpenClawAdapter(settings).health)
hermes = self._health_result(HermesAdapter(settings).health)
response = {
"model_provider": settings.model_provider,
AIProviderName.OPENCLAW: openclaw,
AIProviderName.HERMES: hermes,
}
self.audit.log(
AuditLogCreate(
actor=actor,
source=AuditSource.API,
action=AuditAction.AI_PROVIDER_HEALTH,
target_type=AuditTargetType.AI,
risk_level=AuditRiskLevel.LOW,
response_payload=response,
)
)
return response
def invoke_openclaw_tool(
self,
tool: str,
action: str = AIDefault.ACTION_JSON,
args: dict[str, Any] | None = None,
session_key: str = AIDefault.SESSION_KEY_MAIN,
actor: str = ActorValue.API,
) -> dict[str, Any]:
result = OpenClawAdapter(get_settings()).invoke_tool(tool, action, args or {}, session_key)
response = {AIResponseKey.PROVIDER: AIProviderName.OPENCLAW, AIResponseKey.RESULT: result}
self.audit.log(
AuditLogCreate(
actor=actor,
source=AuditSource.OPENCLAW,
action=AuditAction.OPENCLAW_TOOLS_INVOKE,
target_type=AuditTargetType.OPENCLAW_TOOL,
target_id=tool,
risk_level=AuditRiskLevel.HIGH,
request_payload={
"tool": tool,
"action": action,
"args": args or {},
"session_key": session_key,
},
response_payload=result,
)
)
return response
@staticmethod
def _health_result(check: Any) -> dict[str, Any]:
try:
return check()
except Exception as exc:
# Health checks should report failures, not mask the other provider.
return {
AIResponseKey.OK: False,
AIResponseKey.ERROR: str(exc),
AIResponseKey.TYPE: type(exc).__name__,
}
def draft_policy(
self,
title: str,
@@ -48,12 +143,15 @@ class AIService:
requirements: list[str],
actor: str,
) -> dict[str, Any]:
prompt = (
f"Draft a company policy in Chinese. Title: {title}. Type: {policy_type}. "
"Include purpose, scope, roles, process, approval rules, audit rules, and KPI linkage. "
f"Requirements: {requirements}"
return self.run_skill(
AISkillId.DRAFT_POLICY,
variables={
"title": title,
"policy_type": policy_type,
"requirements": requirements,
},
actor=actor,
)
return self.ask(prompt, actor=actor, source="policy")
def draft_investment_research(
self,
@@ -61,10 +159,11 @@ class AIService:
risk_preference: str,
actor: str,
) -> dict[str, Any]:
prompt = (
"Create an investment research memo in Chinese. Do not give direct trading "
"instructions. Include thesis, risks, data needed, position sizing constraints, "
"and human approval checklist. "
f"Topic: {symbol_or_topic}. Risk preference: {risk_preference}."
return self.run_skill(
AISkillId.INVESTMENT_RESEARCH,
variables={
"symbol_or_topic": symbol_or_topic,
"risk_preference": risk_preference,
},
actor=actor,
)
return self.ask(prompt, actor=actor, source="investment")