支持自定义API接口
This commit is contained in:
@@ -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
|
||||
)
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user