"""
Copyright 2026 Huawei Technologies Co., Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
import re
import requests
from typing import Optional, List
from backend.models.param_config import (
LLMProviderConfig,
LLMGenerationConfig,
LLMPromptConfig,
)
from backend.models.constants import (
DEFAULT_LLM_MODEL,
DEFAULT_MAX_TOKENS,
DEFAULT_TEMPERATURE,
DEFAULT_NUM_QUESTIONS,
DEFAULT_LOCAL_API_URL,
HTTP_OK,
HTTP_REQUEST_CONNECT_TIMEOUT,
HTTP_REQUEST_READ_TIMEOUT,
)
from backend.utils.logger import init_logger
logger = init_logger(__name__)
OPENAI_API_URL = "https://api.openai.com/v1/chat/completions"
PROVIDER_OPENAI = "openai"
PROVIDER_OLLAMA = "ollama"
DEFAULT_SYSTEM_PROMPT = "Generate questions based on the content"
DEFAULT_ANSWER_PROMPT = "Answer the question based on the given context"
DEFAULT_COT_PROMPT = "Think step by step and show your reasoning process before answering:"
NUMBERED_LIST_PATTERN = r'^\s*\d+\.\s*'
PARENTHESIS_NUMBER_PATTERN = r'^\s*\(\d+\)\s*'
Q_PREFIX_PATTERN = r'^\s*Q\d+:\s*'
class LLMService:
"""
语言模型提供商类,支持多种大语言模型提供商
Attributes:
provider_config: LLM提供商配置
generation_config: LLM生成配置
prompt_config: LLM提示词配置
"""
def __init__(
self,
provider_config: Optional[LLMProviderConfig] = None,
generation_config: Optional[LLMGenerationConfig] = None,
prompt_config: Optional[LLMPromptConfig] = None,
):
"""
初始化语言模型提供商
Args:
provider_config: LLM提供商配置
generation_config: LLM生成配置
prompt_config: LLM提示词配置
"""
provider_config = provider_config or LLMProviderConfig()
generation_config = generation_config or LLMGenerationConfig()
prompt_config = prompt_config or LLMPromptConfig()
self.provider = provider_config.provider
self.api_key = provider_config.api_key
self.model_name = provider_config.model_name
self.llm_api = provider_config.llm_api
self.max_tokens = generation_config.max_tokens
self.temperature = generation_config.temperature
self.system_prompt = prompt_config.system_prompt
self.answer_prompt = prompt_config.answer_prompt
self.chain_of_thought_prompt = prompt_config.chain_of_thought_prompt
@staticmethod
def _clean_question_line(line: str) -> str:
"""
清理问题行,移除编号前缀
Args:
line: 原始行文本
Returns:
str: 清理后的文本
"""
cleaned = re.sub(NUMBERED_LIST_PATTERN, '', line)
cleaned = re.sub(PARENTHESIS_NUMBER_PATTERN, '', cleaned)
cleaned = re.sub(Q_PREFIX_PATTERN, '', cleaned)
cleaned = cleaned.strip('- ')
return cleaned
def generate_questions(
self,
text: str,
num_questions: int = DEFAULT_NUM_QUESTIONS,
system_prompt: Optional[str] = None,
) -> List[str]:
"""
生成问题列表
Args:
text: 要分析的文本
num_questions: 生成的问题数量 (默认3)
system_prompt: 自定义系统提示词 (默认None)
Returns:
List[str]: 生成的问题列表
"""
prompt = system_prompt or self.system_prompt
full_prompt = f"{prompt} text: \\n\\n{text}\n\n请生成{num_questions}个相关问题:"
response = self._call_llm(full_prompt)
return self._parse_questions(response, num_questions)
def generate_answer(
self,
question: str,
context: str,
system_prompt: Optional[str] = None,
) -> str:
"""
生成回答
Args:
question: 要回答的问题
context: 相关上下文
system_prompt: 自定义回答提示词 (默认None)
Returns:
str: 生成的回答
"""
prompt = system_prompt or self.answer_prompt
full_prompt = (
f"{prompt}\n\nContext: {context}\n\nQuestion: {question}\n\nAnswer:"
)
return self._call_llm(full_prompt)
def generate_chain_of_thought(
self,
question: str,
answer: str,
context: str,
system_prompt: Optional[str] = None,
) -> str:
"""
生成思维链
Args:
question: 问题
answer: 答案
context: 相关上下文
system_prompt: 自定义思维链提示词 (默认None)
Returns:
str: 生成的思维链
"""
prompt = system_prompt or self.chain_of_thought_prompt
full_prompt = (
f"{prompt}\n\nContext: {context}\n\n"
f"Question: {question}\n\nAnswer: {answer}\n\nChain of thought:"
)
return self._call_llm(full_prompt)
def _call_llm(self, prompt: str) -> str:
"""
调用LLM获取响应
Args:
prompt: 发送给LLM的提示词
Returns:
str: LLM生成的响应文本
Raises:
Exception: API调用失败时抛出
ValueError: 不支持的提供商时抛出
"""
if self.provider == PROVIDER_OPENAI:
return self._call_openai(prompt)
elif self.provider == PROVIDER_OLLAMA:
return self._call_ollama(prompt)
else:
raise ValueError(f"不支持的LLM提供商: {self.provider}")
def _call_openai(self, prompt: str) -> str:
"""
调用OpenAI API
Args:
prompt: 提示词
Returns:
str: 响应文本
Raises:
Exception: API调用失败时抛出
"""
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {self.api_key}",
}
data = {
"model": self.model_name,
"messages": [{"role": "user", "content": prompt}],
"stream": False,
"temperature": self.temperature,
"max_tokens": self.max_tokens,
}
url = self.llm_api or OPENAI_API_URL
response = requests.post(
url,
headers=headers,
json=data,
timeout=(HTTP_REQUEST_CONNECT_TIMEOUT, HTTP_REQUEST_READ_TIMEOUT)
)
if response.status_code != HTTP_OK:
raise Exception(
f"OpenAI API请求失败,状态码: {response.status_code}, "
f"响应: {response.text}"
)
response_data = response.json()
return response_data["choices"][0]["message"]["content"].strip()
def _call_ollama(self, prompt: str) -> str:
"""
调用Ollama API
Args:
prompt: 提示词
Returns:
str: 响应文本
Raises:
Exception: API调用失败时抛出
"""
headers = {"Content-Type": "application/json"}
data = {
"model": self.model_name,
"messages": [{"role": "user", "content": prompt}],
"options": {
"temperature": self.temperature,
"num_predict": self.max_tokens,
},
}
url = self.llm_api or DEFAULT_LOCAL_API_URL
response = requests.post(
url,
headers=headers,
json=data,
timeout=(HTTP_REQUEST_CONNECT_TIMEOUT, HTTP_REQUEST_READ_TIMEOUT)
)
if response.status_code != HTTP_OK:
raise Exception(
f"Ollama API请求失败,状态码: {response.status_code}, "
f"响应: {response.text}"
)
response_data = response.json()
return response_data["message"]["content"].strip()
def _parse_questions(self, response: str, num_questions: int) -> List[str]:
"""
解析问题列表
Args:
response: LLM返回的原始响应
num_questions: 期望的问题数量
Returns:
List[str]: 清理后的问题列表,最多包含num_questions个问题
"""
lines = response.splitlines()
questions = []
for line in lines:
if not line.strip():
continue
cleaned_line = self._clean_question_line(line)
if cleaned_line:
questions.append(cleaned_line)
if len(questions) >= num_questions:
break
return questions