支持自定义API接口

This commit is contained in:
xiamuceer
2025-10-30 16:53:50 +08:00
parent fe974d1524
commit 3aefdd433d
16 changed files with 1143 additions and 97 deletions
+84 -22
View File
@@ -2,7 +2,7 @@
from typing import Optional, AsyncGenerator, List, Dict, Any
from openai import AsyncOpenAI
from anthropic import AsyncAnthropic
from app.config import settings
from app.config import settings as app_settings
from app.logger import get_logger
import httpx
@@ -10,12 +10,37 @@ logger = get_logger(__name__)
class AIService:
"""AI服务统一接口"""
"""AI服务统一接口 - 支持从用户设置或全局配置初始化"""
def __init__(self):
"""初始化AI客户端(优化并发性能)"""
def __init__(
self,
api_provider: Optional[str] = None,
api_key: Optional[str] = None,
api_base_url: Optional[str] = None,
default_model: Optional[str] = None,
default_temperature: Optional[float] = None,
default_max_tokens: Optional[int] = None
):
"""
初始化AI客户端(优化并发性能)
Args:
api_provider: API提供商 (openai/anthropic),为None时使用全局配置
api_key: API密钥,为None时使用全局配置
api_base_url: API基础URL,为None时使用全局配置
default_model: 默认模型,为None时使用全局配置
default_temperature: 默认温度,为None时使用全局配置
default_max_tokens: 默认最大tokens,为None时使用全局配置
"""
# 保存用户设置或使用全局配置
self.api_provider = api_provider or app_settings.default_ai_provider
self.default_model = default_model or app_settings.default_model
self.default_temperature = default_temperature or app_settings.default_temperature
self.default_max_tokens = default_max_tokens or app_settings.default_max_tokens
# 初始化OpenAI客户端
if settings.openai_api_key:
openai_key = api_key if api_provider == "openai" else app_settings.openai_api_key
if openai_key:
# 创建自定义的httpx客户端来避免proxies参数问题
try:
# 配置连接池限制,支持高并发
@@ -43,12 +68,14 @@ class AIService:
)
client_kwargs = {
"api_key": settings.openai_api_key,
"api_key": openai_key,
"http_client": http_client
}
if settings.openai_base_url:
client_kwargs["base_url"] = settings.openai_base_url
# 优先使用用户提供的base_url,否则使用全局配置
base_url = api_base_url if api_provider == "openai" else app_settings.openai_base_url
if base_url:
client_kwargs["base_url"] = base_url
self.openai_client = AsyncOpenAI(**client_kwargs)
logger.info("✅ OpenAI客户端初始化成功")
@@ -62,7 +89,8 @@ class AIService:
logger.warning("OpenAI API key未配置")
# 初始化Anthropic客户端
if settings.anthropic_api_key:
anthropic_key = api_key if api_provider == "anthropic" else app_settings.anthropic_api_key
if anthropic_key:
try:
# 为Anthropic设置相同的超时和连接池配置
limits = httpx.Limits(
@@ -82,12 +110,14 @@ class AIService:
)
client_kwargs = {
"api_key": settings.anthropic_api_key,
"api_key": anthropic_key,
"http_client": http_client
}
if settings.anthropic_base_url:
client_kwargs["base_url"] = settings.anthropic_base_url
# 优先使用用户提供的base_url,否则使用全局配置
base_url = api_base_url if api_provider == "anthropic" else app_settings.anthropic_base_url
if base_url:
client_kwargs["base_url"] = base_url
self.anthropic_client = AsyncAnthropic(**client_kwargs)
logger.info("✅ Anthropic客户端初始化成功")
@@ -123,10 +153,10 @@ class AIService:
Returns:
生成的文本
"""
provider = provider or settings.default_ai_provider
model = model or settings.default_model
temperature = temperature or settings.default_temperature
max_tokens = max_tokens or settings.default_max_tokens
provider = provider or self.api_provider
model = model or self.default_model
temperature = temperature or self.default_temperature
max_tokens = max_tokens or self.default_max_tokens
if provider == "openai":
return await self._generate_openai(
@@ -162,10 +192,10 @@ class AIService:
Yields:
生成的文本片段
"""
provider = provider or settings.default_ai_provider
model = model or settings.default_model
temperature = temperature or settings.default_temperature
max_tokens = max_tokens or settings.default_max_tokens
provider = provider or self.api_provider
model = model or self.default_model
temperature = temperature or self.default_temperature
max_tokens = max_tokens or self.default_max_tokens
if provider == "openai":
async for chunk in self._generate_openai_stream(
@@ -359,5 +389,37 @@ class AIService:
raise
# 创建全局AI服务实例
ai_service = AIService()
# 创建全局AI服务实例(使用环境变量配置,用于向后兼容)
ai_service = AIService()
def create_user_ai_service(
api_provider: str,
api_key: str,
api_base_url: str,
model_name: str,
temperature: float,
max_tokens: int
) -> AIService:
"""
根据用户设置创建AI服务实例
Args:
api_provider: API提供商
api_key: API密钥
api_base_url: API基础URL
model_name: 模型名称
temperature: 温度参数
max_tokens: 最大tokens
Returns:
AIService实例
"""
return AIService(
api_provider=api_provider,
api_key=api_key,
api_base_url=api_base_url,
default_model=model_name,
default_temperature=temperature,
default_max_tokens=max_tokens
)
+48 -12
View File
@@ -26,7 +26,14 @@ class PromptService:
- 为故事发展提供支撑
- 具有独特性和吸引力
**重要:你必须只返回纯JSON格式,不要包含任何markdown标记、代码块标记或其他说明文字。**
**重要格式要求**
1. 只返回纯JSON格式,不要包含任何markdown标记、代码块标记或其他说明文字
2. 不要在JSON字符串值中使用中文引号(""''),请使用英文引号或直接省略引号
3. 专有名词和强调内容可以使用【】或《》标记,不要用引号
**正确示例**
- ✅ "距离【大灾变】爆发""距离大灾变爆发"
- ❌ "距离"大灾变"爆发" (会导致JSON解析失败)
请严格按照以下JSON格式返回(每个字段为200-300字的文本描述):
{{
@@ -36,7 +43,10 @@ class PromptService:
"rules": "世界规则的详细描述,包括运行法则、特殊设定、社会规则、权力结构"
}}
再次强调:只返回纯JSON对象,不要有```json```这样的标记,不要有任何额外的文字说明。"""
再次强调:
1. 只返回纯JSON对象,不要有```json```这样的标记
2. 文本中不要使用中文引号(""),使用【】或《》代替
3. 不要有任何额外的文字说明"""
# 批量角色生成提示词
CHARACTERS_BATCH_GENERATION = """你是一位专业的角色设定师。请根据以下世界观和要求,生成{count}个立体丰满的角色和组织:
@@ -67,7 +77,10 @@ class PromptService:
- 组织要有存在的合理性
- 所有实体要为故事服务
**重要:你必须只返回纯JSON数组格式,不要包含任何markdown标记、代码块标记或其他说明文字。**
**重要格式要求**
1. 只返回纯JSON数组格式,不要包含任何markdown标记、代码块标记或其他说明文字
2. 不要在JSON字符串值中使用中文引号(""''),请使用英文引号或【】《》标记
3. 专有名词和强调内容使用【】或《》,不要用引号
请严格按照以下JSON数组格式返回(每个角色为数组中的一个对象):
[
@@ -134,7 +147,8 @@ class PromptService:
再次强调:
1. 只返回纯JSON数组,不要有```json```这样的标记
2. 数组中必须精确包含{count}个对象
3. 不要引用任何本批次中不存在的角色或组织名称"""
3. 不要引用任何本批次中不存在的角色或组织名称
4. 文本描述中不要使用中文引号(""),改用【】或《》"""
# 完整大纲生成提示词
COMPLETE_OUTLINE_GENERATION = """你是一位经验丰富的小说作家和编剧。请根据以下信息生成完整的{chapter_count}章小说大纲:
@@ -166,7 +180,10 @@ class PromptService:
- 节奏把控:有张有弛
- 视角统一:采用{narrative_perspective}视角叙事
**重要:你必须只返回纯JSON数组格式,不要包含任何markdown标记、代码块标记或其他说明文字。**
**重要格式要求**
1. 只返回纯JSON数组格式,不要包含任何markdown标记、代码块标记或其他说明文字
2. 不要在JSON字符串值中使用中文引号(""''),请使用【】或《》标记
3. 专有名词、书名、事件名使用【】或《》
请严格按照以下JSON数组格式返回(共{chapter_count}个章节对象):
[
@@ -192,7 +209,10 @@ class PromptService:
}}
]
再次强调:只返回纯JSON数组,不要有```json```这样的标记,不要有任何额外的文字说明。数组中要包含{chapter_count}个章节对象。"""
再次强调:
1. 只返回纯JSON数组,不要有```json```这样的标记
2. 数组中要包含{chapter_count}个章节对象
3. 文本中不要使用中文引号(""),改用【】或《》"""
# 大纲续写提示词
OUTLINE_CONTINUE_GENERATION = """你是一位经验丰富的小说作家和编剧。请基于以下信息续写小说大纲:
@@ -232,7 +252,10 @@ class PromptService:
- 保持与已有章节相同的风格和详细程度
- 推进角色成长和情节发展
**重要:你必须只返回纯JSON数组格式,不要包含任何markdown标记、代码块标记或其他说明文字。**
**重要格式要求**
1. 只返回纯JSON数组格式,不要包含任何markdown标记、代码块标记或其他说明文字
2. 不要在JSON字符串值中使用中文引号(""''),请使用【】或《》
3. 文本描述中的专有名词使用【】标记
请严格按照以下JSON数组格式返回(共{chapter_count}个章节对象):
[
@@ -262,7 +285,8 @@ class PromptService:
1. 只返回纯JSON数组,不要有```json```这样的标记
2. 数组中要包含{chapter_count}个章节对象
3. 每个summary必须是100-200字的详细描述
4. 确保字段结构与已有章节完全一致"""
4. 确保字段结构与已有章节完全一致
5. 文本中不要使用中文引号(""),改用【】或《》"""
# AI去味提示词(核心特色功能)
AI_DENOISING = """你是一位追求自然写作风格的编辑。你的任务是将AI生成的文本改写得更像人类作家的手笔。
@@ -431,7 +455,10 @@ class PromptService:
4. 情节的递进和冲突升级
5. 角色的成长弧线
**重要:你必须只返回纯JSON格式,不要包含任何markdown标记、代码块标记或其他说明文字。**
**重要格式要求**
1. 只返回纯JSON格式,不要包含任何markdown标记、代码块标记或其他说明文字
2. 不要在JSON字符串值中使用中文引号(""''),改用【】或《》
3. 专有名词和强调内容使用【】标记
请严格按照以下JSON格式返回:
{{
@@ -444,7 +471,10 @@ class PromptService:
]
}}
再次强调:只返回纯JSON对象,不要有```json```这样的标记,不要有任何额外的文字说明。"""
再次强调:
1. 只返回纯JSON对象,不要有```json```这样的标记
2. 文本中不要使用中文引号(""),改用【】或《》
3. 不要有任何额外的文字说明"""
# 单个角色生成提示词
SINGLE_CHARACTER_GENERATION = """你是一位专业的角色设定师。请根据以下信息创建一个立体饱满的小说角色。
@@ -487,7 +517,10 @@ class PromptService:
- 特殊技能或知识
- 符合世界观设定
**你必须只返回纯JSON格式,不要包含任何markdown标记、代码块标记或其他说明文字。**
**重要格式要求:**
1. 只返回纯JSON格式,不要包含任何markdown标记、代码块标记或其他说明文字
2. 不要在JSON字符串值中使用中文引号(""''),改用【】或《》
3. 文本描述中的专有名词使用【】标记
请严格按照以下JSON格式返回:
{{
@@ -543,7 +576,10 @@ class PromptService:
- 配角要有独特性,不能是工具人
- 所有设定要为故事服务
再次强调:只返回纯JSON对象,不要有```json```这样的标记,不要有任何额外的文字说明。"""
再次强调:
1. 只返回纯JSON对象,不要有```json```这样的标记
2. 文本中不要使用中文引号(""),改用【】或《》
3. 不要有任何额外的文字说明"""
@staticmethod
def format_prompt(template: str, **kwargs) -> str: