310 lines
9.2 KiB
Python
310 lines
9.2 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 ..model.model_config import (
|
||
VALID_TIER_ALIASES,
|
||
build_provider_runtime,
|
||
build_tier_runtime,
|
||
build_routing_config_snapshot,
|
||
find_matching_rule,
|
||
get_provider_defaults,
|
||
get_tier_defaults,
|
||
load_model_config,
|
||
rule_matches_function,
|
||
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", "dashscope", "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()
|
||
CONTROL_PLANE_DEFAULT_LOW_MODEL = str(TIER_DEFAULTS.get("low", {}).get("model") or "qwen-plus")
|
||
CONTROL_PLANE_DEFAULT_HIGH_MODEL = str(TIER_DEFAULTS.get("high", {}).get("model") or "GLM-5")
|
||
CONTROL_PLANE_DASHSCOPE_BASE_URL = str(PROVIDER_DEFAULTS.get("dashscope", {}).get("base_url") or "https://dashscope.aliyuncs.com/compatible-mode/v1")
|
||
PLATFORM_RUNTIME_TENANT_ID = int(os.getenv("PMFLOW_PLATFORM_RUNTIME_TENANT_ID", "1") or "1")
|
||
PROVIDER_ALIASES = {
|
||
"deep-seek": "deepseek",
|
||
"local": "ollama",
|
||
}
|
||
PROMPT_PROFILE_CACHE_TTL_SECONDS = 60.0
|
||
BOOL_TRUE_VALUES = {"1", "true", "yes", "y", "on", "是"}
|
||
CONTROL_PLANE_ROUTING_CACHE_TTL_SECONDS = 30.0
|
||
CONTROL_PLANE_AGENTS_DEFAULT_TIMEOUT_SECONDS = 120
|
||
CONTROL_PLANE_AGENTS_TIMEOUT_SECONDS_MAX = 120
|
||
DEFAULT_TIER_MODEL_OVERRIDES = {
|
||
tier: str(spec.get("model") or "")
|
||
for tier, spec in TIER_DEFAULTS.items()
|
||
}
|
||
LOW_TO_HIGH_ESCALATION_FALLBACK_MARKERS = (
|
||
"不知道",
|
||
"不清楚",
|
||
"无法回答",
|
||
"无法确定",
|
||
"无法判断",
|
||
"信息不足",
|
||
"缺少信息",
|
||
"没有足够信息",
|
||
"请提供更多信息",
|
||
"抱歉",
|
||
"不能提供",
|
||
"无法访问",
|
||
"as an ai",
|
||
"i'm sorry",
|
||
"cannot answer",
|
||
"don't know",
|
||
)
|
||
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",
|
||
}
|
||
CAPABILITY_SLOT_INTEGRATION_TYPE = "ai_capability_slots"
|
||
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",
|
||
"provider_billing_blocked",
|
||
"arrearage",
|
||
"overdue-payment",
|
||
"account is in good standing",
|
||
)
|
||
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 = {
|
||
"pmflow_semantic_context_decision",
|
||
}
|
||
DDL_API_GUARDRAIL_SYSTEM_PROMPT = """
|
||
硬性约束(最高优先级):
|
||
1. 禁止直接执行或建议执行数据库 DDL SQL(CREATE/ALTER/DROP/TRUNCATE/RENAME/COMMENT/GRANT/REVOKE)。
|
||
2. 所有数据库 DDL 变更必须通过受控接口或迁移流程(例如 Alembic migration)。
|
||
3. 若输入要求直接改库执行 DDL:
|
||
- `pmflow_semantic_context_decision` 必须拒绝执行型决策,并在结构化结果里保留 `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",
|
||
},
|
||
},
|
||
"pmflow_semantic_context_decision": {
|
||
"required_keys": (
|
||
"topic_relation",
|
||
"mode_decision",
|
||
"draft_patch",
|
||
"assistant_reply",
|
||
"requires_confirmation",
|
||
"requested_actions",
|
||
"confidence",
|
||
),
|
||
"type_checks": {
|
||
"topic_relation": "str",
|
||
"mode_decision": "str",
|
||
"draft_patch": "dict",
|
||
"assistant_reply": "str",
|
||
"requires_confirmation": "bool",
|
||
"requested_actions": "list",
|
||
"confidence": "number",
|
||
},
|
||
"optional_type_checks": {
|
||
"uncertainties": ("list",),
|
||
},
|
||
},
|
||
"external_query_plan": {
|
||
"required_keys": (
|
||
"plan_version",
|
||
"query_kind",
|
||
"answer_contract",
|
||
"freshness_required",
|
||
"topic",
|
||
"search_queries",
|
||
"result_limit",
|
||
"requires_multi_source",
|
||
"clarify_on_ambiguity",
|
||
),
|
||
"type_checks": {
|
||
"plan_version": "str",
|
||
"query_kind": "str",
|
||
"answer_contract": "str",
|
||
"freshness_required": "bool",
|
||
"topic": "str",
|
||
"search_queries": "list",
|
||
"result_limit": "number",
|
||
"requires_multi_source": "bool",
|
||
"clarify_on_ambiguity": "bool",
|
||
},
|
||
"optional_type_checks": {
|
||
"subject_entities": ("list",),
|
||
"preferred_domains": ("list",),
|
||
"location_hint": ("str_or_none",),
|
||
"time_range_hint": ("str_or_none",),
|
||
},
|
||
},
|
||
"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",),
|
||
},
|
||
},
|
||
"pmflow_launch_draft_v1": {
|
||
"required_keys": (
|
||
"title",
|
||
"one_liner",
|
||
"target_user",
|
||
"problem",
|
||
"solution",
|
||
"requirements",
|
||
"tasks",
|
||
"risks",
|
||
"decisions",
|
||
"confidence",
|
||
"evidence",
|
||
"warnings",
|
||
),
|
||
"type_checks": {
|
||
"title": "str",
|
||
"one_liner": "str",
|
||
"target_user": "str",
|
||
"problem": "str",
|
||
"solution": "str",
|
||
"requirements": "list",
|
||
"tasks": "list",
|
||
"risks": "list",
|
||
"decisions": "list",
|
||
"confidence": "number",
|
||
"evidence": "list",
|
||
"warnings": "list",
|
||
},
|
||
},
|
||
"pmflow_change_review_extraction_v1": {
|
||
"required_keys": (
|
||
"change_summary",
|
||
"proposed_items",
|
||
"evidence",
|
||
"missing_info",
|
||
"warnings",
|
||
"confidence",
|
||
),
|
||
"type_checks": {
|
||
"change_summary": "str",
|
||
"proposed_items": "list",
|
||
"evidence": "list",
|
||
"missing_info": "list",
|
||
"warnings": "list",
|
||
"confidence": "number",
|
||
},
|
||
},
|
||
}
|