294 lines
8.7 KiB
Python
294 lines
8.7 KiB
Python
"""统一 LLM 服务(DeepSeek / GLM / OpenAI / Ollama)。
|
||
|
||
能力:
|
||
1. 多引擎配置持久化(Integration.type=llm_engine)
|
||
2. 动态选择激活引擎
|
||
3. OpenAI-Compatible chat/completions 调用
|
||
4. 基础 AI 调用日志写入(ai_call_logs)
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import asyncio
|
||
from dataclasses import dataclass
|
||
import hashlib
|
||
import json
|
||
import logging
|
||
import os
|
||
import re
|
||
import time
|
||
from datetime import datetime, timedelta, timezone
|
||
from pathlib import Path
|
||
from typing import Any, Optional
|
||
from uuid import uuid4
|
||
|
||
import httpx
|
||
from sqlalchemy import and_, select
|
||
from sqlalchemy.exc import OperationalError
|
||
from sqlalchemy.ext.asyncio import AsyncSession
|
||
|
||
from ...config.ai_prompts import build_default_role_prompt_bundle
|
||
from ...core.database import AsyncSessionLocal
|
||
from ...models import AICallLog, Integration
|
||
from ..model.model_context_bundle import ModelContextBundleService
|
||
|
||
from ...utils.timezone import now_cn_aware
|
||
try:
|
||
import redis.asyncio as redis_async
|
||
|
||
REDIS_AVAILABLE = True
|
||
except Exception: # pragma: no cover
|
||
REDIS_AVAILABLE = False
|
||
redis_async = None
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
SUPPORTED_PROVIDERS = {"deepseek", "glm", "openai", "ollama"}
|
||
OPENCLAW_DEFAULT_LOW_MODEL = "GLM-4.7"
|
||
OPENCLAW_DEFAULT_HIGH_MODEL = "MiniMax-M2.5"
|
||
OPENCLAW_DEFAULT_SAFETY_MODEL = "deepseek-chat"
|
||
PROVIDER_DEFAULTS: dict[str, dict[str, str]] = {
|
||
"deepseek": {
|
||
"name": "DeepSeek",
|
||
"base_url": "https://api.deepseek.com/v1",
|
||
"model": "deepseek-chat",
|
||
"api_env": "DEEPSEEK_API_KEY",
|
||
},
|
||
"glm": {
|
||
"name": "GLM",
|
||
"base_url": "https://open.bigmodel.cn/api/paas/v4",
|
||
"model": "GLM-4.7",
|
||
"api_env": "GLM_API_KEY",
|
||
},
|
||
"openai": {
|
||
"name": "OpenAI",
|
||
"base_url": "https://api.openai.com/v1",
|
||
"model": "gpt-4o-mini",
|
||
"api_env": "OPENAI_API_KEY",
|
||
},
|
||
"ollama": {
|
||
"name": "Ollama",
|
||
"base_url": "http://127.0.0.1:11434",
|
||
"model": "llama3:latest",
|
||
"api_env": "",
|
||
},
|
||
}
|
||
PROVIDER_ALIASES = {
|
||
"zhipu": "glm",
|
||
"chatglm": "glm",
|
||
"deep-seek": "deepseek",
|
||
"local": "ollama",
|
||
}
|
||
PROMPT_PROFILE_CACHE_TTL_SECONDS = 60.0
|
||
BOOL_TRUE_VALUES = {"1", "true", "yes", "y", "on", "是"}
|
||
OPENCLAW_ROUTING_CACHE_TTL_SECONDS = 30.0
|
||
OPENCLAW_DEFAULT_RULES_RELATIVE_PATH = "../.openclaw/pmflow-routing-rules.json"
|
||
OPENCLAW_AGENTS_DEFAULT_TIMEOUT_SECONDS = 120
|
||
OPENCLAW_AGENTS_TIMEOUT_SECONDS_MAX = 120
|
||
DEFAULT_TIER_MODEL_OVERRIDES = {
|
||
"low": OPENCLAW_DEFAULT_LOW_MODEL,
|
||
"high": OPENCLAW_DEFAULT_HIGH_MODEL,
|
||
"safety": OPENCLAW_DEFAULT_SAFETY_MODEL,
|
||
}
|
||
DEFAULT_HIGH_TIER_HINTS = (
|
||
"classify_intent_complex",
|
||
"project_from_text",
|
||
"feishu_project_prefill",
|
||
"feishu_task_orchestration",
|
||
"feishu_project_signal_classification",
|
||
"adaptive_rag_answer_deep",
|
||
"rag_db_project_semantic_retrieval",
|
||
"analyze_food_calories",
|
||
"sstc_ai_breakdown",
|
||
"generate_task_breakdown",
|
||
"recommend_assignee",
|
||
"alignment",
|
||
"charter",
|
||
"cognitive_conflict",
|
||
"blocker",
|
||
"model_requirement",
|
||
"signal_planner_analyze",
|
||
)
|
||
DEFAULT_LOW_TIER_HINTS = (
|
||
"feishu_level_classification",
|
||
"feishu_signal_extraction",
|
||
"feishu_humanized_reply",
|
||
"signal_source_onboarding_plan",
|
||
"signal_source_ai_worker_execution",
|
||
"extract_context_insights",
|
||
"project_draft_",
|
||
"adaptive_rag_answer_normal",
|
||
"rag_intent_classification",
|
||
"classify_intent",
|
||
)
|
||
STRUCTURED_OUTPUT_FUNCTION_HINTS = (
|
||
"feishu_level_classification",
|
||
"feishu_signal_extraction",
|
||
"signal_source_onboarding_plan",
|
||
"signal_source_ai_worker_execution",
|
||
"project_draft_",
|
||
"rag_intent_classification",
|
||
"classify_intent",
|
||
"feishu_project_prefill",
|
||
"feishu_task_orchestration",
|
||
"generate_task_breakdown",
|
||
"generate_charter_from_signal",
|
||
"decompose_tasks_from_charter",
|
||
"recommend_assignees_for_task",
|
||
"evaluate_model_requirement",
|
||
"calculate_alignment_score",
|
||
"detect_cognitive_conflicts",
|
||
"rag_db_project_semantic_retrieval",
|
||
"workload_optimize_prompt",
|
||
"analyze_cost_roi",
|
||
"analyze_food_calories",
|
||
"sstc_ai_breakdown",
|
||
"sstc_charter_localize_zh",
|
||
"sstc_charter_prefill",
|
||
"signal_planner_analyze",
|
||
)
|
||
LOW_TO_HIGH_ESCALATION_FALLBACK_MARKERS = (
|
||
"不知道",
|
||
"不清楚",
|
||
"无法回答",
|
||
"无法确定",
|
||
"无法判断",
|
||
"信息不足",
|
||
"缺少信息",
|
||
"没有足够信息",
|
||
"请提供更多信息",
|
||
"抱歉",
|
||
"不能提供",
|
||
"无法访问",
|
||
"as an ai",
|
||
"i'm sorry",
|
||
"cannot answer",
|
||
"don't know",
|
||
)
|
||
FAST_LOCAL_FALLBACK_FUNCTION_HINTS = (
|
||
"signal_source_onboarding_plan",
|
||
"signal_source_ai_worker_execution",
|
||
)
|
||
FAST_LOCAL_PLANNER_FUNCTION_HINTS = (
|
||
"signal_source_onboarding_plan",
|
||
)
|
||
FAST_LOCAL_WORKER_FUNCTION_HINTS = ("signal_source_ai_worker_execution",)
|
||
FAST_LOCAL_FALLBACK_FAST_TIMEOUT_SECONDS = 20.0
|
||
FAST_LOCAL_FALLBACK_COMPLETE_TIMEOUT_SECONDS = 35.0
|
||
STRUCTURED_CACHE_TTL_SECONDS = 300
|
||
LLM_ENGINE_SETTINGS_DEFAULTS = {
|
||
"enable_local_kb_qa": False,
|
||
"local_kb_qa_scope": "kb_qa_only",
|
||
}
|
||
LOCAL_KB_QA_FUNCTION_PREFIXES = (
|
||
"adaptive_rag_answer_",
|
||
)
|
||
LOCAL_KB_QA_FUNCTION_EXACT = {
|
||
"rag_intent_classification",
|
||
"rag_db_project_semantic_retrieval",
|
||
}
|
||
STRICT_MODEL_ERROR_FUNCTION_PREFIXES = (
|
||
"adaptive_rag_answer_",
|
||
)
|
||
STRICT_MODEL_ERROR_FUNCTION_EXACT = {
|
||
"rag_db_project_semantic_retrieval",
|
||
}
|
||
FAIL_FAST_ERROR_MARKERS = (
|
||
"primary_engine_empty_response",
|
||
"high_tier_empty_response",
|
||
"llm 返回空内容",
|
||
"返回空内容",
|
||
"empty_response",
|
||
"provider_circuit_open",
|
||
)
|
||
CIRCUIT_BREAKER_FAILURE_THRESHOLD = 2
|
||
CIRCUIT_BREAKER_COOLDOWN_SECONDS = 300.0
|
||
CIRCUIT_BREAKER_EXEMPT_FUNCTION_PREFIXES = (
|
||
"project_from_text_",
|
||
"project_draft_",
|
||
)
|
||
DDL_API_GUARDRAIL_FUNCTION_EXACT = {
|
||
"classify_intent_ai_pmo_kernel",
|
||
"classify_intent_complex",
|
||
}
|
||
DDL_API_GUARDRAIL_SYSTEM_PROMPT = """
|
||
硬性约束(最高优先级):
|
||
1. 禁止直接执行或建议执行数据库 DDL SQL(CREATE/ALTER/DROP/TRUNCATE/RENAME/COMMENT/GRANT/REVOKE)。
|
||
2. 所有数据库 DDL 变更必须通过受控接口或迁移流程(例如 Alembic migration)。
|
||
3. 若输入要求直接改库执行 DDL:
|
||
- `classify_intent_ai_pmo_kernel` / `classify_intent_complex` 必须返回 `query.read_only`,并在 reasoning 包含 `ddl_api_required`。
|
||
""".strip()
|
||
STRUCTURED_OBJECT_SCHEMAS: dict[str, dict[str, Any]] = {
|
||
"ai_pmo_signal_title_refine": {
|
||
"required_keys": ("title",),
|
||
"type_checks": {
|
||
"title": "str",
|
||
},
|
||
},
|
||
"signal_source_onboarding_plan": {
|
||
"required_keys": ("should_onboard", "summary"),
|
||
"type_checks": {
|
||
"should_onboard": "bool",
|
||
"summary": "str",
|
||
},
|
||
"optional_type_checks": {
|
||
"worker_roles": ("list",),
|
||
"execution_instruction": ("str_or_none",),
|
||
"source": ("dict_or_none",),
|
||
},
|
||
},
|
||
"signal_source_ai_worker_execution": {
|
||
"required_keys": ("summary", "deliverables", "risks", "next_actions"),
|
||
"type_checks": {
|
||
"summary": "str",
|
||
"deliverables": "list",
|
||
"risks": "list",
|
||
"next_actions": "list",
|
||
},
|
||
},
|
||
"classify_intent_ai_pmo_kernel": {
|
||
"required_keys": ("confidence", "reasoning"),
|
||
"type_checks": {
|
||
"confidence": "number",
|
||
"reasoning": "str",
|
||
},
|
||
"optional_type_checks": {
|
||
"intent": ("str_or_none",),
|
||
"family": ("str_or_none",),
|
||
"action": ("str_or_none",),
|
||
"project_id": ("number_or_none",),
|
||
"requested_count": ("number_or_none",),
|
||
"requires_confirmation": ("bool",),
|
||
},
|
||
},
|
||
"classify_intent_complex": {
|
||
"required_keys": ("confidence", "reasoning"),
|
||
"type_checks": {
|
||
"confidence": "number",
|
||
"reasoning": "str",
|
||
},
|
||
"optional_type_checks": {
|
||
"intent": ("str_or_none",),
|
||
"family": ("str_or_none",),
|
||
"action": ("str_or_none",),
|
||
"project_id": ("number_or_none",),
|
||
"requested_count": ("number_or_none",),
|
||
"requires_confirmation": ("bool",),
|
||
},
|
||
},
|
||
"signal_planner_analyze": {
|
||
"required_keys": ("summary", "priority", "execution_plan", "risks", "confidence"),
|
||
"type_checks": {
|
||
"summary": "str",
|
||
"priority": "str",
|
||
"execution_plan": "list",
|
||
"risks": "list",
|
||
"confidence": "number",
|
||
},
|
||
"optional_type_checks": {
|
||
"assignee_user_id": ("number_or_none",),
|
||
"assignee_reason": ("str_or_none",),
|
||
},
|
||
},
|
||
}
|