Files
yawiii-ComfyUI-Prompt-Assis…/services/llm.py
T

343 lines
15 KiB
Python

import json
import os
import sys
from typing import Optional, Dict, Any, List, Callable
import asyncio
from openai import AsyncOpenAI
from openai.types.chat import ChatCompletion
import httpx
from .error_util import format_api_error
class LLMService:
_provider_base_urls = {
'openai': None, # 使用默认
'siliconflow': 'https://api.siliconflow.cn/v1',
'zhipu': 'https://open.bigmodel.cn/api/paas/v4',
'custom': None # 使用配置中的自定义URL
}
@classmethod
def get_openai_client(cls, api_key: str, provider: str) -> AsyncOpenAI:
"""获取OpenAI客户端"""
# 否则创建新客户端
base_url = None
# 如果是自定义提供商,从配置中获取base_url
if provider == 'custom':
from ..config_manager import config_manager
config = config_manager.get_llm_config()
if 'providers' in config and 'custom' in config['providers']:
base_url = config['providers']['custom'].get('base_url')
# 确保base_url不以/chat/completions结尾,避免路径重复
if base_url and base_url.endswith('/chat/completions'):
base_url = base_url.rstrip('/chat/completions')
else:
base_url = cls._provider_base_urls.get(provider)
# 创建简化的httpx客户端,不使用HTTP/2,避免额外依赖
http_client = httpx.AsyncClient(
timeout=httpx.Timeout(15.0) # 设置超时时间,稍微增加以适应网络延迟
)
kwargs = {
"api_key": api_key,
"http_client": http_client,
"max_retries": 2 # 设置最大重试次数
}
if base_url:
# 确保base_url末尾没有斜杠
base_url = base_url.rstrip('/')
kwargs["base_url"] = base_url
# 添加调试日志
from ..server import PREFIX
print(f"{PREFIX} 创建OpenAI客户端 | 提供商:{provider} | 基础URL:{base_url}")
# 创建客户端并缓存
client = AsyncOpenAI(**kwargs)
return client
@staticmethod
def _get_config() -> Dict[str, Any]:
"""获取LLM配置"""
from ..config_manager import config_manager
config = config_manager.get_llm_config()
current_provider = config.get('provider')
# 获取实际配置
if 'providers' in config and current_provider in config['providers']:
provider_config = config['providers'][current_provider]
return {
'provider': current_provider,
'model': provider_config.get('model', ''),
'base_url': provider_config.get('base_url', ''),
'api_key': provider_config.get('api_key', ''),
'temperature': provider_config.get('temperature', 0.7),
'top_p': provider_config.get('top_p', 0.9),
'max_tokens': provider_config.get('max_tokens', 2000)
}
else:
# 兼容旧版配置格式
return config
@staticmethod
async def expand_prompt(prompt: str, request_id: Optional[str] = None, stream_callback: Optional[Callable[[str], None]] = None) -> Dict[str, Any]:
"""
使用大语言模型扩写提示词,自动判断用户输入语言,并设置大语言模型回答语言。
支持流式输出以提高响应速度。
参数:
prompt: 要扩写的提示词
request_id: 请求ID
stream_callback: 流式输出的回调函数
返回:
包含扩写结果的字典
"""
try:
# 获取配置
config = LLMService._get_config()
api_key = config.get('api_key')
model = config.get('model')
provider = config.get('provider', 'unknown')
temperature = config.get('temperature', 0.7)
top_p = config.get('top_p', 0.9)
max_tokens = config.get('max_tokens', 2000)
if not api_key:
return {"success": False, "error": "请先配置大语言模型 API密钥"}
if not model:
return {"success": False, "error": "未配置模型名称"}
# 从server.py导入颜色常量和前缀
from ..server import PREFIX, ERROR_PREFIX
# 获取提供商显示名称
provider_display_name = {
'zhipu': '智谱',
'siliconflow': '硅基流动',
'openai': 'OpenAI',
'custom': '自定义'
}.get(provider, provider)
print(f"{PREFIX} LLM扩写请求 | 服务:{provider_display_name} | ID:{request_id} | 内容:{prompt[:30]}...")
# 加载系统提示词
from ..config_manager import config_manager
system_prompts = config_manager.get_system_prompts()
if not system_prompts or 'expand_prompts' not in system_prompts:
return {"success": False, "error": "扩写系统提示词加载失败"}
# 获取激活的提示词ID
active_prompt_id = system_prompts.get('active_prompts', {}).get('expand', 'expand_default')
# 获取对应的提示词
if active_prompt_id not in system_prompts['expand_prompts']:
# 如果找不到激活的提示词,尝试使用第一个可用的提示词
if len(system_prompts['expand_prompts']) > 0:
active_prompt_id = list(system_prompts['expand_prompts'].keys())[0]
else:
return {"success": False, "error": "未找到可用的扩写系统提示词"}
system_message = system_prompts['expand_prompts'][active_prompt_id]
# 输出使用的提示词名称
prompt_name = system_message.get('name', active_prompt_id)
print(f"{PREFIX} 使用扩写提示词: {prompt_name} | ID:{active_prompt_id}")
# 判断用户输入语言
def is_chinese(text):
return any('\u4e00' <= char <= '\u9fff' for char in text)
if is_chinese(prompt):
lang_message = {"role": "system", "content": "请用中文回答"}
else:
lang_message = {"role": "system", "content": "Please answer in English."}
# 构建消息
messages = [
lang_message,
system_message,
{"role": "user", "content": prompt}
]
# 使用OpenAI SDK
client = LLMService.get_openai_client(api_key, provider)
try:
# 添加调试信息
print(f"{PREFIX} 调用LLM API | 服务:{provider_display_name} | 模型:{model}")
# 使用配置中的参数
stream = await client.chat.completions.create(
model=model,
messages=[{"role": m["role"], "content": m["content"]} for m in messages],
temperature=temperature,
top_p=top_p,
max_tokens=max_tokens,
stream=True,
# 添加响应格式参数,减少不必要的token
response_format={"type": "text"}
)
full_content = ""
async for chunk in stream:
if chunk.choices[0].delta.content:
content = chunk.choices[0].delta.content
full_content += content
if stream_callback:
stream_callback(content)
# 输出结构化成功日志
print(f"{PREFIX} LLM扩写成功 | 服务:{provider_display_name} | 请求ID:{request_id} | 结果字符数:{len(full_content)}")
return {
"success": True,
"data": {
"original": prompt,
"expanded": full_content
}
}
except asyncio.CancelledError:
print(f"{PREFIX} LLM扩写任务在服务层被取消 | ID:{request_id}")
return {"success": False, "error": "请求已取消", "cancelled": True}
except Exception as e:
return {"success": False, "error": format_api_error(e, provider_display_name)}
except asyncio.CancelledError:
from ..server import PREFIX
print(f"{PREFIX} LLM扩写任务在服务层被取消 | ID:{request_id}")
return {"success": False, "error": "请求已取消", "cancelled": True}
except Exception as e:
# 从server.py导入颜色常量和前缀
from ..server import ERROR_PREFIX
print(f"{ERROR_PREFIX} LLM扩写请求失败 | 错误:{str(e)}")
return {"success": False, "error": str(e)}
@staticmethod
async def translate(text: str, from_lang: str = 'auto', to_lang: str = 'zh', request_id: Optional[str] = None, is_auto: bool = False, stream_callback: Optional[Callable[[str], None]] = None) -> Dict[str, Any]:
"""
使用大语言模型翻译文本,自动设置提示词语言和输出语言。
支持流式输出以提高响应速度。
参数:
text: 要翻译的文本
from_lang: 源语言 (默认为auto自动检测)
to_lang: 目标语言 (默认为zh中文)
request_id: 请求ID
is_auto: 是否为工作流自动翻译
stream_callback: 流式输出的回调函数
返回:
包含翻译结果的字典
"""
try:
# 获取配置
config = LLMService._get_config()
api_key = config.get('api_key')
model = config.get('model')
provider = config.get('provider', 'unknown')
temperature = config.get('temperature', 0.7)
top_p = config.get('top_p', 0.9)
max_tokens = config.get('max_tokens', 2000)
if not api_key:
return {"success": False, "error": "请先配置大语言模型 API密钥"}
if not model:
return {"success": False, "error": "未配置模型名称"}
# 从server.py导入颜色常量和前缀
from ..server import PREFIX, AUTO_TRANSLATE_PREFIX
# 获取提供商显示名称
provider_display_name = {
'zhipu': '智谱',
'siliconflow': '硅基流动',
'openai': 'OpenAI',
'custom': '自定义'
}.get(provider, provider)
# 使用统一的前缀
prefix = AUTO_TRANSLATE_PREFIX if is_auto else PREFIX
print(f"{prefix} {'工作流自动翻译' if is_auto else '翻译请求'} | 服务:{provider_display_name}翻译 | 请求ID:{request_id} | 原文长度:{len(text)} | 方向:{from_lang}->{to_lang}")
# 加载系统提示词
from ..config_manager import config_manager
system_prompts = config_manager.get_system_prompts()
if not system_prompts or 'translate_prompts' not in system_prompts or 'ZH' not in system_prompts['translate_prompts']:
return {"success": False, "error": "翻译系统提示词加载失败"}
system_message = system_prompts['translate_prompts']['ZH']
# 动态替换提示词中的{src_lang}和{dst_lang}
lang_map = {'zh': '中文', 'en': '英文', 'auto': '原文'}
src_lang = lang_map.get(from_lang, from_lang)
dst_lang = lang_map.get(to_lang, to_lang)
sys_msg_content = system_message['content'].replace('{src_lang}', src_lang).replace('{dst_lang}', dst_lang)
sys_msg = {"role": "system", "content": sys_msg_content}
# 设置输出语言
if to_lang == 'en':
lang_message = {"role": "system", "content": "Please answer in English."}
else:
lang_message = {"role": "system", "content": "请用中文回答"}
# 构建消息
messages = [
lang_message,
sys_msg,
{"role": "user", "content": text}
]
# 使用OpenAI SDK
client = LLMService.get_openai_client(api_key, provider)
try:
# 添加调试信息
print(f"{PREFIX} 调用LLM API | 服务:{provider_display_name} | 模型:{model}")
# 使用配置中的参数
stream = await client.chat.completions.create(
model=model,
messages=[{"role": m["role"], "content": m["content"]} for m in messages],
temperature=temperature,
top_p=top_p,
max_tokens=max_tokens,
stream=True,
# 添加响应格式参数,减少不必要的token
response_format={"type": "text"}
)
full_content = ""
async for chunk in stream:
if chunk.choices[0].delta.content:
content = chunk.choices[0].delta.content
full_content += content
if stream_callback:
stream_callback(content)
# 输出结构化成功日志
prefix = AUTO_TRANSLATE_PREFIX if is_auto else PREFIX
print(f"{prefix} {'工作流翻译完成' if is_auto else '翻译完成'} | 服务:{provider_display_name}翻译 | 请求ID:{request_id} | 结果字符数:{len(full_content)}")
return {
"success": True,
"data": {
"from": from_lang,
"to": to_lang,
"original": text,
"translated": full_content
}
}
except asyncio.CancelledError:
print(f"{prefix} {'工作流翻译' if is_auto else '翻译'}任务在服务层被取消 | ID:{request_id}")
return {"success": False, "error": "请求已取消", "cancelled": True}
except Exception as e:
return {"success": False, "error": format_api_error(e, provider_display_name)}
except asyncio.CancelledError:
from ..server import PREFIX, AUTO_TRANSLATE_PREFIX
prefix = AUTO_TRANSLATE_PREFIX if is_auto else PREFIX
print(f"{prefix} {'工作流翻译' if is_auto else '翻译'}任务在服务层被取消 | ID:{request_id}")
return {"success": False, "error": "请求已取消", "cancelled": True}
except Exception as e:
return {"success": False, "error": str(e)}