2025-02-06 18:35:28 +08:00
|
|
|
|
# llm_adapters.py
|
|
|
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
|
|
import logging
|
|
|
|
|
|
from typing import Optional
|
|
|
|
|
|
from langchain_openai import ChatOpenAI
|
|
|
|
|
|
|
|
|
|
|
|
def ensure_openai_base_url_has_v1(url: str) -> str:
|
|
|
|
|
|
import re
|
|
|
|
|
|
url = url.strip()
|
|
|
|
|
|
if not url:
|
|
|
|
|
|
return url
|
|
|
|
|
|
if not re.search(r'/v\d+$', url):
|
|
|
|
|
|
if '/v1' not in url:
|
|
|
|
|
|
url = url.rstrip('/') + '/v1'
|
|
|
|
|
|
return url
|
|
|
|
|
|
|
|
|
|
|
|
class BaseLLMAdapter:
|
|
|
|
|
|
"""
|
|
|
|
|
|
统一的 LLM 接口基类,为不同后端(OpenAI、Ollama、ML Studio 等)提供一致的方法签名。
|
|
|
|
|
|
"""
|
|
|
|
|
|
def invoke(self, prompt: str) -> str:
|
|
|
|
|
|
raise NotImplementedError("Subclasses must implement .invoke(prompt) method.")
|
|
|
|
|
|
|
|
|
|
|
|
class DeepSeekAdapter(BaseLLMAdapter):
|
|
|
|
|
|
"""
|
|
|
|
|
|
适配官方/OpenAI兼容接口(使用 langchain.ChatOpenAI)
|
|
|
|
|
|
"""
|
2025-02-07 18:07:20 +08:00
|
|
|
|
def __init__(self, api_key: str, base_url: str, model_name: str, max_tokens: int, temperature: float = 0.7, timeout: Optional[int] = 600):
|
2025-02-06 18:35:28 +08:00
|
|
|
|
self.base_url = ensure_openai_base_url_has_v1(base_url)
|
|
|
|
|
|
self.api_key = api_key
|
|
|
|
|
|
self.model_name = model_name
|
|
|
|
|
|
self.max_tokens = max_tokens
|
|
|
|
|
|
self.temperature = temperature
|
2025-02-07 18:07:20 +08:00
|
|
|
|
self.timeout = timeout
|
2025-02-06 18:35:28 +08:00
|
|
|
|
|
|
|
|
|
|
self._client = ChatOpenAI(
|
|
|
|
|
|
model=self.model_name,
|
|
|
|
|
|
api_key=self.api_key,
|
|
|
|
|
|
base_url=self.base_url,
|
|
|
|
|
|
max_tokens=self.max_tokens,
|
2025-02-07 18:07:20 +08:00
|
|
|
|
temperature=self.temperature,
|
|
|
|
|
|
timeout=self.timeout
|
2025-02-06 18:35:28 +08:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
def invoke(self, prompt: str) -> str:
|
|
|
|
|
|
response = self._client.invoke(prompt)
|
|
|
|
|
|
if not response:
|
|
|
|
|
|
logging.warning("No response from DeepSeekAdapter.")
|
|
|
|
|
|
return ""
|
|
|
|
|
|
return response.content
|
|
|
|
|
|
|
|
|
|
|
|
class OpenAIAdapter(BaseLLMAdapter):
|
|
|
|
|
|
"""
|
|
|
|
|
|
适配官方/OpenAI兼容接口(使用 langchain.ChatOpenAI)
|
|
|
|
|
|
"""
|
2025-02-07 18:07:20 +08:00
|
|
|
|
def __init__(self, api_key: str, base_url: str, model_name: str, max_tokens: int, temperature: float = 0.7, timeout: Optional[int] = 600):
|
2025-02-06 18:35:28 +08:00
|
|
|
|
self.base_url = ensure_openai_base_url_has_v1(base_url)
|
|
|
|
|
|
self.api_key = api_key
|
|
|
|
|
|
self.model_name = model_name
|
|
|
|
|
|
self.max_tokens = max_tokens
|
|
|
|
|
|
self.temperature = temperature
|
2025-02-07 18:07:20 +08:00
|
|
|
|
self.timeout = timeout
|
2025-02-06 18:35:28 +08:00
|
|
|
|
|
|
|
|
|
|
self._client = ChatOpenAI(
|
|
|
|
|
|
model=self.model_name,
|
|
|
|
|
|
api_key=self.api_key,
|
|
|
|
|
|
base_url=self.base_url,
|
|
|
|
|
|
max_tokens=self.max_tokens,
|
2025-02-07 18:07:20 +08:00
|
|
|
|
temperature=self.temperature,
|
|
|
|
|
|
timeout=self.timeout
|
2025-02-06 18:35:28 +08:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
def invoke(self, prompt: str) -> str:
|
|
|
|
|
|
response = self._client.invoke(prompt)
|
|
|
|
|
|
if not response:
|
|
|
|
|
|
logging.warning("No response from OpenAIAdapter.")
|
|
|
|
|
|
return ""
|
|
|
|
|
|
return response.content
|
|
|
|
|
|
|
|
|
|
|
|
class OllamaAdapter(BaseLLMAdapter):
|
|
|
|
|
|
"""
|
|
|
|
|
|
Ollama 同样有一个 OpenAI-like /v1/chat 接口,可直接使用 ChatOpenAI。
|
|
|
|
|
|
但是通常 Ollama 默认本地服务在 http://localhost:11434,如果符合OpenAI风格即可直接传参。
|
|
|
|
|
|
"""
|
2025-02-07 18:07:20 +08:00
|
|
|
|
def __init__(self, api_key: str, base_url: str, model_name: str, max_tokens: int, temperature: float = 0.7, timeout: Optional[int] = 600):
|
2025-02-06 18:35:28 +08:00
|
|
|
|
self.base_url = ensure_openai_base_url_has_v1(base_url)
|
|
|
|
|
|
self.api_key = api_key
|
|
|
|
|
|
self.model_name = model_name
|
|
|
|
|
|
self.max_tokens = max_tokens
|
|
|
|
|
|
self.temperature = temperature
|
2025-02-07 18:07:20 +08:00
|
|
|
|
self.timeout = timeout
|
2025-02-06 18:35:28 +08:00
|
|
|
|
|
|
|
|
|
|
self._client = ChatOpenAI(
|
|
|
|
|
|
model=self.model_name,
|
|
|
|
|
|
api_key=self.api_key,
|
|
|
|
|
|
base_url=self.base_url,
|
|
|
|
|
|
max_tokens=self.max_tokens,
|
2025-02-07 18:07:20 +08:00
|
|
|
|
temperature=self.temperature,
|
|
|
|
|
|
timeout=self.timeout
|
2025-02-06 18:35:28 +08:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
def invoke(self, prompt: str) -> str:
|
|
|
|
|
|
response = self._client.invoke(prompt)
|
|
|
|
|
|
if not response:
|
|
|
|
|
|
logging.warning("No response from OllamaAdapter.")
|
|
|
|
|
|
return ""
|
|
|
|
|
|
return response.content
|
|
|
|
|
|
|
|
|
|
|
|
class MLStudioAdapter(BaseLLMAdapter):
|
2025-02-07 18:07:20 +08:00
|
|
|
|
def __init__(self, api_key: str, base_url: str, model_name: str, max_tokens: int, temperature: float = 0.7, timeout: Optional[int] = 600):
|
2025-02-06 18:35:28 +08:00
|
|
|
|
self.base_url = ensure_openai_base_url_has_v1(base_url)
|
|
|
|
|
|
self.api_key = api_key
|
|
|
|
|
|
self.model_name = model_name
|
|
|
|
|
|
self.max_tokens = max_tokens
|
|
|
|
|
|
self.temperature = temperature
|
2025-02-07 18:07:20 +08:00
|
|
|
|
self.timeout = timeout
|
2025-02-06 18:35:28 +08:00
|
|
|
|
|
|
|
|
|
|
self._client = ChatOpenAI(
|
|
|
|
|
|
model=self.model_name,
|
|
|
|
|
|
api_key=self.api_key,
|
|
|
|
|
|
base_url=self.base_url,
|
|
|
|
|
|
max_tokens=self.max_tokens,
|
2025-02-07 18:07:20 +08:00
|
|
|
|
temperature=self.temperature,
|
|
|
|
|
|
timeout=self.timeout
|
2025-02-06 18:35:28 +08:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
def invoke(self, prompt: str) -> str:
|
|
|
|
|
|
response = self._client.invoke(prompt)
|
|
|
|
|
|
if not response:
|
|
|
|
|
|
logging.warning("No response from MLStudioAdapter.")
|
|
|
|
|
|
return ""
|
|
|
|
|
|
return response.content
|
|
|
|
|
|
|
|
|
|
|
|
def create_llm_adapter(
|
|
|
|
|
|
interface_format: str,
|
|
|
|
|
|
base_url: str,
|
|
|
|
|
|
model_name: str,
|
|
|
|
|
|
api_key: str,
|
2025-02-06 19:37:37 +08:00
|
|
|
|
temperature: float,
|
2025-02-07 20:59:12 +08:00
|
|
|
|
max_tokens: int,
|
|
|
|
|
|
timeout: int
|
2025-02-06 18:35:28 +08:00
|
|
|
|
) -> BaseLLMAdapter:
|
|
|
|
|
|
"""
|
|
|
|
|
|
工厂函数:根据 interface_format 返回不同的适配器实例。
|
|
|
|
|
|
"""
|
|
|
|
|
|
if interface_format.lower() == "deepseek":
|
2025-02-07 20:59:12 +08:00
|
|
|
|
return DeepSeekAdapter(api_key, base_url, model_name, max_tokens, temperature, timeout)
|
2025-02-06 18:35:28 +08:00
|
|
|
|
elif interface_format.lower() == "openai":
|
2025-02-07 20:59:12 +08:00
|
|
|
|
return OpenAIAdapter(api_key, base_url, model_name, max_tokens, temperature, timeout)
|
2025-02-06 18:35:28 +08:00
|
|
|
|
elif interface_format.lower() == "ollama":
|
2025-02-07 20:59:12 +08:00
|
|
|
|
return OllamaAdapter(api_key, base_url, model_name, max_tokens, temperature, timeout)
|
2025-02-06 18:35:28 +08:00
|
|
|
|
elif interface_format.lower() == "ml studio":
|
2025-02-07 20:59:12 +08:00
|
|
|
|
return MLStudioAdapter(api_key, base_url, model_name, max_tokens, temperature, timeout)
|
2025-02-06 18:35:28 +08:00
|
|
|
|
else:
|
|
|
|
|
|
raise ValueError(f"Unknown interface_format: {interface_format}")
|