Files
zpark-ems/core/backend/app/services/llm_service.py
Du Wenbo f0f13faf00 feat: v2.0 — maintenance module, AI analysis, station power fix
- Add full 检修维护中心 (6.4): 3-type work orders (消缺/巡检/抄表),
  asset management, warehouse, work plans, billing settlement
- Add AI智能分析 tab with LLM-powered diagnostics (StepFun + ZhipuAI)
- Add AI模型配置 settings page (provider, temperature, prompts)
- Fix station power accuracy: use API station total (station_power)
  instead of inverter-level computation — eliminates timing gaps
- Add 7 new DB models, 4 new API routers, 5 new frontend pages
- Migrations: 009 (maintenance expansion) + 010 (AI analysis)
- Version bump: 1.6.1 → 2.0.0

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 21:16:03 +08:00

108 lines
3.9 KiB
Python

"""LLM service for AI-powered analysis using OpenAI-compatible APIs (StepFun, ZhipuAI)."""
import logging
from openai import AsyncOpenAI
logger = logging.getLogger(__name__)
async def get_llm_client(settings: dict, use_fallback: bool = False) -> tuple[AsyncOpenAI, str]:
"""Create an OpenAI-compatible client based on settings.
Returns (client, model_name) tuple.
"""
if use_fallback:
base_url = settings.get("ai_fallback_api_base_url", "")
api_key = settings.get("ai_fallback_api_key", "")
model = settings.get("ai_fallback_model_name", "codegeex-4")
else:
base_url = settings.get("ai_api_base_url", "")
api_key = settings.get("ai_api_key", "")
model = settings.get("ai_model_name", "step-2-16k")
client = AsyncOpenAI(base_url=base_url, api_key=api_key, timeout=30.0)
return client, model
async def chat_completion(
messages: list[dict],
settings: dict,
temperature: float | None = None,
max_tokens: int | None = None,
) -> str:
"""Send a chat completion request with automatic fallback."""
temp = temperature or float(settings.get("ai_temperature", "0.7"))
tokens = max_tokens or int(settings.get("ai_max_tokens", "2000"))
# Try primary
try:
client, model = await get_llm_client(settings, use_fallback=False)
response = await client.chat.completions.create(
model=model,
messages=messages,
temperature=temp,
max_tokens=tokens,
)
return response.choices[0].message.content or ""
except Exception as e:
logger.warning(f"Primary LLM failed: {e}")
# Try fallback if enabled
if settings.get("ai_fallback_enabled") == "true":
try:
client, model = await get_llm_client(settings, use_fallback=True)
response = await client.chat.completions.create(
model=model,
messages=messages,
temperature=temp,
max_tokens=tokens,
)
return response.choices[0].message.content or ""
except Exception as e2:
logger.error(f"Fallback LLM also failed: {e2}")
raise Exception(f"All LLM providers failed. Primary: {e}, Fallback: {e2}")
else:
raise
async def test_connection(settings: dict) -> dict:
"""Test connection to the configured LLM provider."""
results = {"primary": {"status": "unknown"}, "fallback": {"status": "unknown"}}
# Test primary
try:
client, model = await get_llm_client(settings, use_fallback=False)
response = await client.chat.completions.create(
model=model,
messages=[{"role": "user", "content": "你好,请回复'连接成功'"}],
max_tokens=20,
temperature=0,
)
reply = response.choices[0].message.content or ""
results["primary"] = {
"status": "success",
"model": model,
"reply": reply[:100],
}
except Exception as e:
results["primary"] = {"status": "error", "error": str(e)[:200]}
# Test fallback
if settings.get("ai_fallback_enabled") == "true":
try:
client, model = await get_llm_client(settings, use_fallback=True)
response = await client.chat.completions.create(
model=model,
messages=[{"role": "user", "content": "你好,请回复'连接成功'"}],
max_tokens=20,
temperature=0,
)
reply = response.choices[0].message.content or ""
results["fallback"] = {
"status": "success",
"model": model,
"reply": reply[:100],
}
except Exception as e:
results["fallback"] = {"status": "error", "error": str(e)[:200]}
return results