662 lines
24 KiB
Python
662 lines
24 KiB
Python
"""
|
||
PMFlow AI 核心能力封装
|
||
|
||
技术栈:LangChain 0.3+、OpenAI SDK、Redis
|
||
- 封装统一 LLM 路由入口调用(流式/非流式)
|
||
- Prompt 集中在 config/ai_prompts
|
||
- AI 调用日志写入 ai_call_logs
|
||
- Redis 结果缓存(1 小时 TTL)
|
||
- 指数退避重试
|
||
"""
|
||
|
||
import hashlib
|
||
import json
|
||
import os
|
||
import time
|
||
from datetime import datetime
|
||
from typing import Any, AsyncGenerator, Optional, Union
|
||
from sqlalchemy import and_, select
|
||
|
||
# LangChain & OpenAI
|
||
try:
|
||
from langchain_openai import ChatOpenAI
|
||
from langchain_core.messages import HumanMessage, AIMessage, SystemMessage
|
||
from langchain_core.language_models.chat_models import BaseChatModel
|
||
LANGCHAIN_AVAILABLE = True
|
||
except ImportError:
|
||
LANGCHAIN_AVAILABLE = False
|
||
BaseChatModel = object
|
||
|
||
# Redis
|
||
try:
|
||
import redis.asyncio as redis
|
||
REDIS_AVAILABLE = True
|
||
except ImportError:
|
||
REDIS_AVAILABLE = False
|
||
|
||
# Token 定价 (GPT-4o-mini, 2024)
|
||
INPUT_PRICE_PER_1K = 0.00015
|
||
OUTPUT_PRICE_PER_1K = 0.0006
|
||
CACHE_TTL_SECONDS = 3600 # 1 小时
|
||
PROMPT_PROFILE_CACHE_TTL_SECONDS = 60
|
||
DEFAULT_OPENAI_BASE_URL = "https://api.edgefn.net/v1"
|
||
DEFAULT_OPENAI_MODEL = "MiniMax-M2.5"
|
||
DEFAULT_GLM_MODEL = "GLM-4.7"
|
||
|
||
from ..config.ai_prompts import build_default_role_prompt_bundle
|
||
from ..database import AsyncSessionLocal
|
||
from ..models import Integration
|
||
|
||
|
||
def _t2s(v: Union[str, int]) -> str:
|
||
"""统一转为 str"""
|
||
return str(v) if v is not None else ""
|
||
|
||
|
||
def _hash_input(fn: str, *args, **kwargs) -> str:
|
||
"""生成缓存 key"""
|
||
payload = json.dumps({"fn": fn, "args": args, "kwargs": kwargs}, sort_keys=True, default=str)
|
||
return hashlib.sha256(payload.encode()).hexdigest()
|
||
|
||
|
||
def _estimate_cost(prompt_tokens: int, completion_tokens: int) -> float:
|
||
return (prompt_tokens * INPUT_PRICE_PER_1K + completion_tokens * OUTPUT_PRICE_PER_1K) / 1000
|
||
|
||
|
||
def _tenant_to_int(tenant_id: Union[str, int]) -> int:
|
||
try:
|
||
return int(tenant_id)
|
||
except (TypeError, ValueError):
|
||
return 1
|
||
|
||
|
||
class AIService:
|
||
"""PMFlow AI 服务"""
|
||
|
||
def __init__(
|
||
self,
|
||
api_key: Optional[str] = None,
|
||
model: Optional[str] = None,
|
||
base_url: Optional[str] = None,
|
||
redis_url: Optional[str] = None,
|
||
max_retries: int = 3,
|
||
):
|
||
runtime = self._resolve_llm_runtime(
|
||
api_key=api_key,
|
||
model=model,
|
||
base_url=base_url,
|
||
)
|
||
self.api_key = runtime["api_key"]
|
||
self.model = runtime["model"]
|
||
self.base_url = runtime["base_url"]
|
||
self.redis_url = redis_url or os.getenv("REDIS_URL")
|
||
self.max_retries = max_retries
|
||
self._llm: Optional[BaseChatModel] = None
|
||
self._redis: Optional[Any] = None
|
||
self._prompt_profile_cache: dict[int, tuple[float, tuple[str, str]]] = {}
|
||
|
||
@staticmethod
|
||
def _is_env_true(name: str, default: bool) -> bool:
|
||
raw = str(os.getenv(name, str(default)) or "").strip().lower()
|
||
if not raw:
|
||
return default
|
||
return raw in {"1", "true", "yes", "y", "on", "是"}
|
||
|
||
def _resolve_llm_runtime(
|
||
self,
|
||
*,
|
||
api_key: Optional[str],
|
||
model: Optional[str],
|
||
base_url: Optional[str],
|
||
) -> dict[str, str]:
|
||
routing_enabled = self._is_env_true("OPENCLAW_ROUTING_ENABLED", True)
|
||
direct_emergency_enabled = self._is_env_true("LLM_DIRECT_EMERGENCY_ENABLED", False)
|
||
if routing_enabled and not direct_emergency_enabled:
|
||
return {
|
||
"api_key": str(api_key or os.getenv("OPENAI_API_KEY", "") or "").strip(),
|
||
"model": str(
|
||
model
|
||
or os.getenv("LLM_ROUTING_HIGH_MODEL")
|
||
or os.getenv("OPENAI_MODEL")
|
||
or DEFAULT_OPENAI_MODEL
|
||
or ""
|
||
).strip(),
|
||
"base_url": str(base_url or os.getenv("OPENAI_BASE_URL", DEFAULT_OPENAI_BASE_URL) or "").strip(),
|
||
}
|
||
|
||
provider = str(os.getenv("LLM_PROVIDER", "openai") or "openai").strip().lower()
|
||
if provider == "glm":
|
||
return {
|
||
"api_key": str(api_key or os.getenv("GLM_API_KEY", "") or "").strip(),
|
||
"model": str(model or os.getenv("GLM_MODEL", DEFAULT_GLM_MODEL) or "").strip(),
|
||
"base_url": str(base_url or os.getenv("GLM_BASE_URL", DEFAULT_OPENAI_BASE_URL) or "").strip(),
|
||
}
|
||
if provider == "deepseek":
|
||
return {
|
||
"api_key": str(api_key or os.getenv("DEEPSEEK_API_KEY", "") or "").strip(),
|
||
"model": str(model or os.getenv("DEEPSEEK_MODEL", "deepseek-chat") or "").strip(),
|
||
"base_url": str(base_url or os.getenv("DEEPSEEK_BASE_URL", "https://api.deepseek.com/v1") or "").strip(),
|
||
}
|
||
if provider == "ollama":
|
||
return {
|
||
"api_key": str(api_key or "").strip(),
|
||
"model": str(model or os.getenv("OLLAMA_MODEL", "llama3:latest") or "").strip(),
|
||
"base_url": str(
|
||
base_url
|
||
or os.getenv("OLLAMA_BASE_URL")
|
||
or os.getenv("OLLAMA_HOST", "http://127.0.0.1:11434")
|
||
or ""
|
||
).strip(),
|
||
}
|
||
|
||
return {
|
||
"api_key": str(api_key or os.getenv("OPENAI_API_KEY", "") or "").strip(),
|
||
"model": str(model or os.getenv("OPENAI_MODEL", DEFAULT_OPENAI_MODEL) or "").strip(),
|
||
"base_url": str(base_url or os.getenv("OPENAI_BASE_URL", DEFAULT_OPENAI_BASE_URL) or "").strip(),
|
||
}
|
||
|
||
def _get_llm(self) -> BaseChatModel:
|
||
if not LANGCHAIN_AVAILABLE:
|
||
raise RuntimeError("请安装 langchain-openai: pip install langchain-openai langchain-core")
|
||
if not self.api_key:
|
||
raise RuntimeError("请设置统一路由所需 API Key 环境变量")
|
||
if self._llm is None:
|
||
self._llm = ChatOpenAI(
|
||
model=self.model,
|
||
api_key=self.api_key,
|
||
base_url=self.base_url,
|
||
temperature=0.3,
|
||
)
|
||
return self._llm
|
||
|
||
async def _get_redis(self):
|
||
if not REDIS_AVAILABLE or not self.redis_url:
|
||
return None
|
||
if self._redis is None:
|
||
self._redis = redis.from_url(self.redis_url, decode_responses=True)
|
||
return self._redis
|
||
|
||
async def _cache_get(self, key: str) -> Optional[str]:
|
||
r = await self._get_redis()
|
||
if r is None:
|
||
return None
|
||
try:
|
||
return await r.get(key)
|
||
except Exception:
|
||
return None
|
||
|
||
async def _cache_set(self, key: str, value: str, ttl: int = CACHE_TTL_SECONDS):
|
||
r = await self._get_redis()
|
||
if r is None:
|
||
return
|
||
try:
|
||
await r.setex(key, ttl, value)
|
||
except Exception:
|
||
pass
|
||
|
||
async def _resolve_active_system_prompt(
|
||
self,
|
||
*,
|
||
tenant_id: Union[str, int],
|
||
db=None,
|
||
) -> tuple[str, str]:
|
||
tenant_int = _tenant_to_int(tenant_id)
|
||
now = time.time()
|
||
cached = self._prompt_profile_cache.get(tenant_int)
|
||
if cached and cached[0] > now:
|
||
return cached[1]
|
||
|
||
role_name = ""
|
||
system_prompt = ""
|
||
try:
|
||
async def _load_profile(session) -> tuple[str, str]:
|
||
row = (
|
||
await session.execute(
|
||
select(Integration).where(
|
||
and_(
|
||
Integration.tenant_id == int(tenant_int),
|
||
Integration.type == "ai_prompt_profiles",
|
||
)
|
||
)
|
||
)
|
||
).scalar_one_or_none()
|
||
bundle = row.config if row is not None and isinstance(row.config, dict) else {}
|
||
active_key = str(bundle.get("active_role_key") or "").strip()
|
||
roles = bundle.get("roles") if isinstance(bundle.get("roles"), list) else []
|
||
selected = None
|
||
for item in roles:
|
||
if not isinstance(item, dict):
|
||
continue
|
||
if str(item.get("role_key") or "").strip() == active_key:
|
||
selected = item
|
||
break
|
||
if selected is None:
|
||
for item in roles:
|
||
if isinstance(item, dict) and bool(item.get("enabled", True)):
|
||
selected = item
|
||
break
|
||
if not isinstance(selected, dict):
|
||
return ("", "")
|
||
return (
|
||
str(selected.get("role_name") or "").strip(),
|
||
str(selected.get("system_prompt") or "").strip(),
|
||
)
|
||
|
||
if db is not None:
|
||
role_name, system_prompt = await _load_profile(db)
|
||
else:
|
||
async with AsyncSessionLocal() as session:
|
||
role_name, system_prompt = await _load_profile(session)
|
||
except Exception:
|
||
role_name = ""
|
||
system_prompt = ""
|
||
|
||
if len(system_prompt) < 20:
|
||
fallback = build_default_role_prompt_bundle()
|
||
active_key = str(fallback.get("active_role_key") or "").strip()
|
||
roles = fallback.get("roles") if isinstance(fallback.get("roles"), list) else []
|
||
selected = None
|
||
for row in roles:
|
||
if not isinstance(row, dict):
|
||
continue
|
||
if str(row.get("role_key") or "").strip() == active_key:
|
||
selected = row
|
||
break
|
||
if selected is None and roles and isinstance(roles[0], dict):
|
||
selected = roles[0]
|
||
if isinstance(selected, dict):
|
||
role_name = str(selected.get("role_name") or role_name or "默认角色").strip()
|
||
system_prompt = str(selected.get("system_prompt") or "").strip()
|
||
|
||
result = (role_name, system_prompt)
|
||
self._prompt_profile_cache[tenant_int] = (now + PROMPT_PROFILE_CACHE_TTL_SECONDS, result)
|
||
return result
|
||
|
||
async def _log_call(
|
||
self,
|
||
db,
|
||
tenant_id: str,
|
||
function_name: str,
|
||
input_hash: str,
|
||
status: str,
|
||
prompt_tokens: int = 0,
|
||
completion_tokens: int = 0,
|
||
latency_ms: Optional[int] = None,
|
||
error_message: Optional[str] = None,
|
||
):
|
||
try:
|
||
from ..models import AICallLog
|
||
|
||
total = prompt_tokens + completion_tokens
|
||
cost = _estimate_cost(prompt_tokens, completion_tokens)
|
||
log = AICallLog(
|
||
tenant_id=_t2s(tenant_id),
|
||
function_name=function_name,
|
||
input_hash=input_hash,
|
||
status=status,
|
||
prompt_tokens=prompt_tokens,
|
||
completion_tokens=completion_tokens,
|
||
total_tokens=total,
|
||
cost_usd=cost,
|
||
latency_ms=latency_ms,
|
||
error_message=error_message,
|
||
)
|
||
db.add(log)
|
||
await db.commit()
|
||
except Exception:
|
||
pass
|
||
|
||
async def _invoke_with_retry(
|
||
self,
|
||
prompt: str,
|
||
db=None,
|
||
tenant_id: str = "default",
|
||
function_name: str = "unknown",
|
||
use_cache: bool = True,
|
||
cache_key: Optional[str] = None,
|
||
project_id: Optional[int] = None,
|
||
) -> str:
|
||
"""带缓存、重试、日志的 LLM 调用"""
|
||
input_hash = cache_key or _hash_input(function_name, prompt)
|
||
|
||
if use_cache and db is None:
|
||
cached = await self._cache_get(input_hash)
|
||
if cached:
|
||
return cached
|
||
|
||
# 预算前置检查(仅在有 db 会话时启用)
|
||
if db is not None:
|
||
try:
|
||
from .budget_enforcement_service import BudgetEnforcementService
|
||
|
||
tenant_int = _tenant_to_int(tenant_id)
|
||
# 预估:按 prompt 长度粗略估算,保守给定 completion 预算
|
||
estimated_prompt_tokens = max(len(prompt) // 4, 1)
|
||
estimated_completion_tokens = 1200
|
||
estimated_cost = _estimate_cost(estimated_prompt_tokens, estimated_completion_tokens)
|
||
|
||
budget_service = BudgetEnforcementService(db)
|
||
budget_check = await budget_service.check_budget_before_ai_call(
|
||
tenant_id=tenant_int,
|
||
estimated_cost_usd=estimated_cost,
|
||
project_id=project_id,
|
||
)
|
||
if not budget_check.get("allowed", True):
|
||
raise RuntimeError("预算超限,AI 调用被阻止(hard limit)")
|
||
except RuntimeError:
|
||
raise
|
||
except Exception:
|
||
# 预算系统异常不阻断主流程,避免影响现有可用性
|
||
pass
|
||
|
||
llm = self._get_llm()
|
||
_, system_prompt = await self._resolve_active_system_prompt(
|
||
tenant_id=tenant_id,
|
||
db=db,
|
||
)
|
||
last_err = None
|
||
for attempt in range(self.max_retries):
|
||
try:
|
||
t0 = time.perf_counter()
|
||
messages = [HumanMessage(content=prompt)]
|
||
if system_prompt:
|
||
messages = [SystemMessage(content=system_prompt), *messages]
|
||
resp = await llm.ainvoke(messages)
|
||
latency_ms = int((time.perf_counter() - t0) * 1000)
|
||
text = resp.content if hasattr(resp, "content") else str(resp)
|
||
pt = getattr(resp, "response_metadata", None) or {}
|
||
usage = (pt or {}).get("usage", {}) or (getattr(resp, "response_metadata", None) or {}).get("usage", {})
|
||
prompt_tokens = usage.get("input_tokens", 0) or usage.get("prompt_tokens", 0)
|
||
completion_tokens = usage.get("output_tokens", 0) or usage.get("completion_tokens", 0)
|
||
|
||
if db:
|
||
await self._log_call(
|
||
db, tenant_id, function_name, input_hash, "success",
|
||
prompt_tokens, completion_tokens, latency_ms,
|
||
)
|
||
# 按实际 token 成本累计预算
|
||
try:
|
||
from .budget_enforcement_service import BudgetEnforcementService
|
||
|
||
actual_cost = _estimate_cost(prompt_tokens, completion_tokens)
|
||
await BudgetEnforcementService(db).update_budget_usage_after_ai_call(
|
||
tenant_id=_tenant_to_int(tenant_id),
|
||
actual_cost_usd=actual_cost,
|
||
project_id=project_id,
|
||
)
|
||
except Exception:
|
||
pass
|
||
if use_cache:
|
||
await self._cache_set(input_hash, text)
|
||
return text
|
||
except Exception as e:
|
||
last_err = e
|
||
wait = 2 ** attempt
|
||
time.sleep(wait)
|
||
|
||
if db:
|
||
await self._log_call(
|
||
db, tenant_id, function_name, input_hash, "failed",
|
||
error_message=str(last_err),
|
||
)
|
||
raise last_err
|
||
|
||
def _parse_json(self, text: str) -> dict:
|
||
text = text.strip()
|
||
for p in ("```json", "```"):
|
||
if text.startswith(p):
|
||
text = text[len(p):]
|
||
if text.endswith("```"):
|
||
text = text[:-3]
|
||
return json.loads(text.strip())
|
||
|
||
# ========================================================================
|
||
# 核心函数
|
||
# ========================================================================
|
||
|
||
async def generate_charter_from_signal(
|
||
self,
|
||
signal_content: str,
|
||
tenant_id: Union[str, int] = "default",
|
||
db=None,
|
||
) -> dict:
|
||
"""从市场/模型信号生成需求宪章草案"""
|
||
from ..config.ai_prompts import PROMPT_GENERATE_CHARTER
|
||
|
||
tid = _t2s(tenant_id)
|
||
prompt = PROMPT_GENERATE_CHARTER.format(signal_content=signal_content)
|
||
text = await self._invoke_with_retry(
|
||
prompt, db=db, tenant_id=tid, function_name="generate_charter_from_signal",
|
||
)
|
||
return self._parse_json(text)
|
||
|
||
async def decompose_tasks_from_charter(
|
||
self,
|
||
charter_content: str,
|
||
project_id: Union[str, int],
|
||
tenant_id: Union[str, int] = "default",
|
||
db=None,
|
||
) -> dict:
|
||
"""从宪章拆解任务,自动识别所需技能标签,返回带技能要求的任务列表"""
|
||
from ..config.ai_prompts import PROMPT_DECOMPOSE_TASKS
|
||
|
||
tid = _t2s(tenant_id)
|
||
prompt = PROMPT_DECOMPOSE_TASKS.format(
|
||
charter_content=charter_content,
|
||
project_id=_t2s(project_id),
|
||
)
|
||
text = await self._invoke_with_retry(
|
||
prompt, db=db, tenant_id=tid, function_name="decompose_tasks_from_charter",
|
||
)
|
||
return self._parse_json(text)
|
||
|
||
async def recommend_assignees_for_task(
|
||
self,
|
||
task_content: str,
|
||
required_skill_tag_ids: list,
|
||
project_id: Union[str, int],
|
||
tenant_id: Union[str, int] = "default",
|
||
db=None,
|
||
candidates_json: Optional[str] = None,
|
||
) -> dict:
|
||
"""基于人员能力图谱、负载、熟练度,推荐 Top3 人员"""
|
||
from ..config.ai_prompts import PROMPT_RECOMMEND_ASSIGNEES
|
||
|
||
tid = _t2s(tenant_id)
|
||
candidates = candidates_json or "[]"
|
||
prompt = PROMPT_RECOMMEND_ASSIGNEES.format(
|
||
task_content=task_content,
|
||
required_skill_tag_ids=json.dumps(required_skill_tag_ids),
|
||
candidates_json=candidates,
|
||
)
|
||
text = await self._invoke_with_retry(
|
||
prompt, db=db, tenant_id=tid, function_name="recommend_assignees_for_task",
|
||
)
|
||
return self._parse_json(text)
|
||
|
||
async def update_user_skill_scores(
|
||
self,
|
||
task_id: Union[str, int],
|
||
tenant_id: Union[str, int] = "default",
|
||
db=None,
|
||
quality_score: float = 0.0,
|
||
efficiency_score: float = 0.0,
|
||
assignee_ids: Optional[list] = None,
|
||
skill_tag_ids: Optional[list] = None,
|
||
) -> dict:
|
||
"""任务完成后,根据质量/效率评分更新相关人员技能分"""
|
||
from sqlalchemy import select
|
||
from ..models import UserSkillBinding
|
||
from uuid import UUID
|
||
|
||
def to_uuid(v):
|
||
if v is None:
|
||
return None
|
||
if isinstance(v, UUID):
|
||
return v
|
||
s = str(v)
|
||
try:
|
||
return UUID(s) if len(s) == 36 else None
|
||
except ValueError:
|
||
return None
|
||
|
||
if db is None:
|
||
return {"updated_count": 0, "updates": []}
|
||
|
||
tid = _t2s(tenant_id)
|
||
updates = []
|
||
if assignee_ids and skill_tag_ids:
|
||
for uid in assignee_ids:
|
||
for sid in skill_tag_ids:
|
||
uid_u = to_uuid(uid)
|
||
sid_u = to_uuid(sid)
|
||
if not uid_u or not sid_u:
|
||
continue
|
||
try:
|
||
r = await db.execute(
|
||
select(UserSkillBinding).where(
|
||
UserSkillBinding.tenant_id == tid,
|
||
UserSkillBinding.user_id == uid_u,
|
||
UserSkillBinding.skill_tag_id == sid_u,
|
||
)
|
||
)
|
||
b = r.scalar_one_or_none()
|
||
if b:
|
||
b.quality_score = min(100, max(0, (b.quality_score or 0) + quality_score))
|
||
b.efficiency_score = min(100, max(0, (b.efficiency_score or 0) + efficiency_score))
|
||
b.last_used_at = datetime.utcnow()
|
||
await db.commit()
|
||
updates.append({
|
||
"user_id": str(uid),
|
||
"skill_tag_id": str(sid),
|
||
"quality_score_delta": quality_score,
|
||
"efficiency_score_delta": efficiency_score,
|
||
"last_used_at": b.last_used_at.isoformat(),
|
||
})
|
||
except Exception:
|
||
pass
|
||
|
||
return {"updated_count": len(updates), "updates": updates}
|
||
|
||
async def calculate_alignment_score(
|
||
self,
|
||
user_id: Union[str, int],
|
||
truth_node_id: Union[str, int],
|
||
tenant_id: Union[str, int] = "default",
|
||
db=None,
|
||
truth_content: str = "",
|
||
user_context: str = "",
|
||
) -> dict:
|
||
"""计算用户对真理节点的认知对齐度(0-100)"""
|
||
from ..config.ai_prompts import PROMPT_ALIGNMENT_SCORE
|
||
from sqlalchemy import select
|
||
from ..models import TruthNode
|
||
|
||
tid = _t2s(tenant_id)
|
||
if db and not truth_content:
|
||
r = await db.execute(
|
||
select(TruthNode).where(
|
||
TruthNode.id == truth_node_id,
|
||
TruthNode.tenant_id == tid,
|
||
)
|
||
)
|
||
tn = r.scalar_one_or_none()
|
||
if tn:
|
||
truth_content = tn.content or tn.title or ""
|
||
|
||
prompt = PROMPT_ALIGNMENT_SCORE.format(
|
||
user_id=_t2s(user_id),
|
||
truth_node_id=_t2s(truth_node_id),
|
||
truth_content=truth_content or "(无内容)",
|
||
user_context=user_context or "(无)",
|
||
)
|
||
text = await self._invoke_with_retry(
|
||
prompt, db=db, tenant_id=tid, function_name="calculate_alignment_score",
|
||
)
|
||
data = self._parse_json(text)
|
||
data["user_id"] = _t2s(user_id)
|
||
data["truth_node_id"] = _t2s(truth_node_id)
|
||
return data
|
||
|
||
async def detect_cognitive_conflicts(
|
||
self,
|
||
truth_node_id: Union[str, int],
|
||
tenant_id: Union[str, int] = "default",
|
||
db=None,
|
||
truth_content: str = "",
|
||
user_viewpoints: str = "",
|
||
) -> dict:
|
||
"""识别真理节点下的隐性认知冲突"""
|
||
from ..config.ai_prompts import PROMPT_COGNITIVE_CONFLICTS
|
||
from sqlalchemy import select
|
||
from ..models import TruthNode
|
||
|
||
tid = _t2s(tenant_id)
|
||
if db and not truth_content:
|
||
r = await db.execute(
|
||
select(TruthNode).where(
|
||
TruthNode.id == truth_node_id,
|
||
TruthNode.tenant_id == tid,
|
||
)
|
||
)
|
||
tn = r.scalar_one_or_none()
|
||
if tn:
|
||
truth_content = tn.content or tn.title or ""
|
||
|
||
prompt = PROMPT_COGNITIVE_CONFLICTS.format(
|
||
truth_node_id=_t2s(truth_node_id),
|
||
truth_content=truth_content or "(无内容)",
|
||
user_viewpoints=user_viewpoints or "(无)",
|
||
)
|
||
text = await self._invoke_with_retry(
|
||
prompt, db=db, tenant_id=tid, function_name="detect_cognitive_conflicts",
|
||
)
|
||
data = self._parse_json(text)
|
||
data["truth_node_id"] = _t2s(truth_node_id)
|
||
data["total_count"] = len(data.get("conflicts", []))
|
||
return data
|
||
|
||
async def evaluate_model_requirement(
|
||
self,
|
||
charter_content: str,
|
||
model_version_id: Union[str, int],
|
||
tenant_id: Union[str, int] = "default",
|
||
db=None,
|
||
model_info: str = "",
|
||
) -> dict:
|
||
"""评估需求与模型的匹配度"""
|
||
from ..config.ai_prompts import PROMPT_EVALUATE_MODEL_REQUIREMENT
|
||
|
||
tid = _t2s(tenant_id)
|
||
prompt = PROMPT_EVALUATE_MODEL_REQUIREMENT.format(
|
||
charter_content=charter_content,
|
||
model_version_id=_t2s(model_version_id),
|
||
model_info=model_info or "(未提供)",
|
||
)
|
||
text = await self._invoke_with_retry(
|
||
prompt, db=db, tenant_id=tid, function_name="evaluate_model_requirement",
|
||
)
|
||
return self._parse_json(text)
|
||
|
||
# ========================================================================
|
||
# 流式调用
|
||
# ========================================================================
|
||
|
||
async def stream_chat(self, prompt: str) -> AsyncGenerator[str, None]:
|
||
"""流式生成"""
|
||
llm = self._get_llm()
|
||
async for chunk in llm.astream([HumanMessage(content=prompt)]):
|
||
if hasattr(chunk, "content") and chunk.content:
|
||
yield chunk.content
|
||
|
||
|
||
# 单例
|
||
_ai_service: Optional[AIService] = None
|
||
|
||
|
||
def get_ai_service() -> AIService:
|
||
global _ai_service
|
||
if _ai_service is None:
|
||
_ai_service = AIService()
|
||
return _ai_service
|