PMFlow/backend/app/services/llm_service/shared_part_01.py

299 lines
9.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""统一 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 ..model.model_config import (
build_provider_runtime,
build_tier_runtime,
get_provider_defaults,
get_tier_defaults,
load_model_config,
resolve_model_config_path,
)
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"}
MODEL_CONFIG_BUNDLE = load_model_config()
PROVIDER_DEFAULTS: dict[str, dict[str, str]] = get_provider_defaults()
TIER_DEFAULTS: dict[str, dict[str, str]] = get_tier_defaults()
OPENCLAW_DEFAULT_LOW_MODEL = str(TIER_DEFAULTS.get("low", {}).get("model") or "qwen-plus")
OPENCLAW_DEFAULT_HIGH_MODEL = str(TIER_DEFAULTS.get("high", {}).get("model") or "GLM-5")
OPENCLAW_DEFAULT_SAFETY_MODEL = str(TIER_DEFAULTS.get("safety", {}).get("model") or OPENCLAW_DEFAULT_LOW_MODEL)
OPENCLAW_DASHSCOPE_BASE_URL = str(PROVIDER_DEFAULTS.get("glm", {}).get("base_url") or "https://dashscope.aliyuncs.com/compatible-mode/v1")
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 = {
tier: str(spec.get("model") or "")
for tier, spec in TIER_DEFAULTS.items()
}
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",),
},
},
"generate_task_breakdown": {
"required_keys": ("tasks",),
"type_checks": {
"tasks": "list",
},
"optional_type_checks": {
"detected_skills": ("list",),
"source_context": ("dict_or_none",),
},
},
"sstc_ai_breakdown": {
"required_keys": ("tasks",),
"type_checks": {
"tasks": "list",
},
"optional_type_checks": {
"detected_skills": ("list",),
"source_context": ("dict_or_none",),
},
},
}