MicroFish/backend/app/config.py

242 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
配置管理
统一从项目根目录的 .env 文件加载配置
"""
import os
from dotenv import load_dotenv
# 加载项目根目录的 .env 文件
# 路径: MiroFish/.env (相对于 backend/app/config.py)
project_root_env = os.path.join(os.path.dirname(__file__), '../../.env')
if os.path.exists(project_root_env):
load_dotenv(project_root_env, override=True)
else:
# 如果根目录没有 .env尝试加载环境变量用于生产环境
load_dotenv(override=True)
class Config:
"""Flask配置类"""
# Flask配置
SECRET_KEY = os.environ.get('SECRET_KEY', 'mirofish-secret-key')
DEBUG = os.environ.get('FLASK_DEBUG', 'True').lower() == 'true'
# JSON配置 - 禁用ASCII转义让中文直接显示而不是 \uXXXX 格式)
JSON_AS_ASCII = False
# LLM配置统一使用OpenAI格式
LLM_API_KEY = os.environ.get('LLM_API_KEY')
LLM_BASE_URL = os.environ.get('LLM_BASE_URL', 'https://api.openai.com/v1')
LLM_MODEL_NAME = os.environ.get('LLM_MODEL_NAME', 'gpt-4o-mini')
# Zep配置
ZEP_MODE = os.environ.get('ZEP_MODE', 'cloud').lower()
ZEP_API_KEY = os.environ.get('ZEP_API_KEY')
ZEP_BASE_URL = os.environ.get('ZEP_BASE_URL')
OPENZEP_EMBEDDER_API_KEY = os.environ.get('OPENZEP_EMBEDDER_API_KEY') or None
OPENZEP_EMBEDDER_BASE_URL = os.environ.get('OPENZEP_EMBEDDER_BASE_URL') or None
OPENZEP_EMBEDDER_MODEL = os.environ.get('OPENZEP_EMBEDDER_MODEL') or None
# 图后端配置
GRAPH_BACKEND = os.environ.get('GRAPH_BACKEND', 'graphiti').lower()
GRAPH_SEARCH_RERANKER = os.environ.get('GRAPH_SEARCH_RERANKER', 'rrf').strip() or None
GRAPH_SEARCH_APP_RERANKER = (os.environ.get('GRAPH_SEARCH_APP_RERANKER', 'embedding_rrf').strip().lower() or 'embedding_rrf')
GRAPH_SEARCH_APP_RERANK_FUSION_K = max(1, int(os.environ.get('GRAPH_SEARCH_APP_RERANK_FUSION_K', '60')))
GRAPH_SEARCH_APP_SEMANTIC_WEIGHT = max(0.0, float(os.environ.get('GRAPH_SEARCH_APP_SEMANTIC_WEIGHT', '2.0')))
GRAPH_SEARCH_APP_EMBEDDER_API_KEY = os.environ.get('GRAPH_SEARCH_APP_EMBEDDER_API_KEY') or None
GRAPH_SEARCH_APP_EMBEDDER_BASE_URL = os.environ.get('GRAPH_SEARCH_APP_EMBEDDER_BASE_URL') or None
GRAPH_SEARCH_APP_EMBEDDER_MODEL = os.environ.get('GRAPH_SEARCH_APP_EMBEDDER_MODEL') or None
GRAPH_SEARCH_APP_EMBED_BATCH_SIZE = max(1, int(os.environ.get('GRAPH_SEARCH_APP_EMBED_BATCH_SIZE', '32')))
GRAPH_SEARCH_APP_RERANKER_API_KEY = os.environ.get('GRAPH_SEARCH_APP_RERANKER_API_KEY') or None
GRAPH_SEARCH_APP_RERANKER_BASE_URL = os.environ.get('GRAPH_SEARCH_APP_RERANKER_BASE_URL') or None
GRAPH_SEARCH_APP_RERANKER_MODEL = os.environ.get('GRAPH_SEARCH_APP_RERANKER_MODEL') or None
GRAPH_SEARCH_APP_RERANKER_PROVIDER = (os.environ.get('GRAPH_SEARCH_APP_RERANKER_PROVIDER', 'auto').strip().lower() or 'auto')
GRAPH_SEARCH_APP_RERANKER_TIMEOUT = max(1.0, float(os.environ.get('GRAPH_SEARCH_APP_RERANKER_TIMEOUT', '20')))
GRAPH_SEARCH_INCLUDE_NODES = os.environ.get('GRAPH_SEARCH_INCLUDE_NODES', 'true').lower() == 'true'
GRAPH_SEARCH_EDGE_LIMIT_MULTIPLIER = max(1, int(os.environ.get('GRAPH_SEARCH_EDGE_LIMIT_MULTIPLIER', '2')))
GRAPH_SEARCH_NODE_LIMIT_MULTIPLIER = max(1, int(os.environ.get('GRAPH_SEARCH_NODE_LIMIT_MULTIPLIER', '1')))
GRAPH_SEARCH_NODE_SUMMARY_LIMIT = max(1, int(os.environ.get('GRAPH_SEARCH_NODE_SUMMARY_LIMIT', '5')))
GRAPH_SEARCH_EXPAND_EDGES_FROM_NODES = os.environ.get('GRAPH_SEARCH_EXPAND_EDGES_FROM_NODES', 'true').lower() == 'true'
GRAPH_SEARCH_NODE_EDGE_EXPANSION_LIMIT = max(0, int(os.environ.get('GRAPH_SEARCH_NODE_EDGE_EXPANSION_LIMIT', '2')))
GRAPH_SEARCH_NODE_EDGE_PER_NODE_LIMIT = max(1, int(os.environ.get('GRAPH_SEARCH_NODE_EDGE_PER_NODE_LIMIT', '8')))
GRAPHITI_URI = os.environ.get('GRAPHITI_URI')
GRAPHITI_USER = os.environ.get('GRAPHITI_USER', 'neo4j')
GRAPHITI_PASSWORD = os.environ.get('GRAPHITI_PASSWORD')
GRAPHITI_DATABASE = os.environ.get('GRAPHITI_DATABASE', 'neo4j')
GRAPHITI_LLM_API_KEY = os.environ.get('GRAPHITI_LLM_API_KEY') or LLM_API_KEY
GRAPHITI_LLM_BASE_URL = os.environ.get('GRAPHITI_LLM_BASE_URL') or LLM_BASE_URL
GRAPHITI_LLM_MODEL = os.environ.get('GRAPHITI_LLM_MODEL') or LLM_MODEL_NAME
GRAPHITI_LLM_SMALL_MODEL = os.environ.get('GRAPHITI_LLM_SMALL_MODEL') or GRAPHITI_LLM_MODEL
GRAPHITI_LLM_CLIENT_MODE = os.environ.get('GRAPHITI_LLM_CLIENT_MODE', 'openai').lower()
GRAPHITI_LLM_MAX_TOKENS = max(1024, int(os.environ.get('GRAPHITI_LLM_MAX_TOKENS', '16384')))
GRAPHITI_EMBEDDER_API_KEY = os.environ.get('GRAPHITI_EMBEDDER_API_KEY') or GRAPHITI_LLM_API_KEY
GRAPHITI_EMBEDDER_BASE_URL = os.environ.get('GRAPHITI_EMBEDDER_BASE_URL') or GRAPHITI_LLM_BASE_URL
GRAPHITI_EMBEDDER_MODEL = os.environ.get('GRAPHITI_EMBEDDER_MODEL', 'qwen3-embedding:8b')
GRAPHITI_EMBEDDER_DIM = max(128, int(os.environ.get('GRAPHITI_EMBEDDER_DIM', '1024')))
GRAPHITI_RERANKER_API_KEY = os.environ.get('GRAPHITI_RERANKER_API_KEY') or GRAPHITI_LLM_API_KEY
GRAPHITI_RERANKER_BASE_URL = os.environ.get('GRAPHITI_RERANKER_BASE_URL') or GRAPHITI_LLM_BASE_URL
GRAPHITI_RERANKER_MODEL = os.environ.get('GRAPHITI_RERANKER_MODEL') or GRAPHITI_LLM_MODEL
GRAPHITI_ENABLE_CROSS_ENCODER = os.environ.get('GRAPHITI_ENABLE_CROSS_ENCODER', 'false').lower() == 'true'
GRAPHITI_MAX_COROUTINES = max(1, int(os.environ.get('GRAPHITI_MAX_COROUTINES', '20')))
# 文件上传配置
MAX_CONTENT_LENGTH = 50 * 1024 * 1024 # 50MB
UPLOAD_FOLDER = os.path.join(os.path.dirname(__file__), '../uploads')
ALLOWED_EXTENSIONS = {'pdf', 'md', 'txt', 'markdown'}
# 文本处理配置
DEFAULT_CHUNK_SIZE = 500 # 默认切块大小
DEFAULT_CHUNK_OVERLAP = 50 # 默认重叠大小
# OASIS模拟配置
OASIS_DEFAULT_MAX_ROUNDS = int(os.environ.get('OASIS_DEFAULT_MAX_ROUNDS', '10'))
OASIS_SIMULATION_DATA_DIR = os.path.join(os.path.dirname(__file__), '../uploads/simulations')
# OASIS平台可用动作配置
OASIS_TWITTER_ACTIONS = [
'CREATE_POST', 'LIKE_POST', 'REPOST', 'FOLLOW', 'DO_NOTHING', 'QUOTE_POST'
]
OASIS_REDDIT_ACTIONS = [
'LIKE_POST', 'DISLIKE_POST', 'CREATE_POST', 'CREATE_COMMENT',
'LIKE_COMMENT', 'DISLIKE_COMMENT', 'SEARCH_POSTS', 'SEARCH_USER',
'TREND', 'REFRESH', 'DO_NOTHING', 'FOLLOW', 'MUTE'
]
# Report Agent配置
REPORT_AGENT_MAX_TOOL_CALLS = int(os.environ.get('REPORT_AGENT_MAX_TOOL_CALLS', '5'))
REPORT_AGENT_MAX_REFLECTION_ROUNDS = int(os.environ.get('REPORT_AGENT_MAX_REFLECTION_ROUNDS', '2'))
REPORT_AGENT_TEMPERATURE = float(os.environ.get('REPORT_AGENT_TEMPERATURE', '0.5'))
# Experimental Memory (Spike S1)
USE_EXPERIMENTAL_MEMORY = os.environ.get('USE_EXPERIMENTAL_MEMORY', 'False').lower() == 'true'
DATA_DIR = os.path.join(os.path.dirname(__file__), '../data')
@classmethod
def use_openzep(cls):
"""是否启用 OpenZep / 自定义 Zep endpoint。"""
return cls.ZEP_MODE == 'openzep' or bool(cls.ZEP_BASE_URL)
@classmethod
def is_zep_configured(cls, api_key=None):
"""检查 Zep/OpenZep 是否已完成最小配置。"""
resolved_api_key = cls.ZEP_API_KEY if api_key is None else api_key
if cls.use_openzep():
return bool(cls.ZEP_BASE_URL)
return bool(resolved_api_key)
@classmethod
def get_zep_client_kwargs(cls, api_key=None):
"""生成 Zep 客户端初始化参数。"""
resolved_api_key = cls.ZEP_API_KEY if api_key is None else api_key
kwargs = {}
# OpenZep 场景允许关闭鉴权;此时不要传空字符串 api_key
# 否则 zep sdk 会构造出非法的 `Api-Key:` 请求头httpx 会直接拒绝。
if resolved_api_key:
kwargs['api_key'] = resolved_api_key
if cls.ZEP_BASE_URL:
kwargs['base_url'] = cls.ZEP_BASE_URL
return kwargs
@classmethod
def get_zep_config_errors(cls, api_key=None):
"""返回 Zep/OpenZep 的配置错误。"""
if cls.is_zep_configured(api_key=api_key):
return []
if cls.use_openzep():
return ["ZEP_BASE_URL 未配置"]
return ["ZEP_API_KEY 未配置"]
@classmethod
def get_graph_search_embedder_config(cls):
"""返回 app-side 图搜索语义重排使用的 embedding 配置。"""
api_key = cls.GRAPH_SEARCH_APP_EMBEDDER_API_KEY
base_url = cls.GRAPH_SEARCH_APP_EMBEDDER_BASE_URL
model = cls.GRAPH_SEARCH_APP_EMBEDDER_MODEL
backend = (cls.GRAPH_BACKEND or 'graphiti').lower()
if backend == 'graphiti':
api_key = api_key or cls.GRAPHITI_EMBEDDER_API_KEY
base_url = base_url or cls.GRAPHITI_EMBEDDER_BASE_URL
model = model or cls.GRAPHITI_EMBEDDER_MODEL
elif cls.use_openzep():
api_key = api_key or cls.OPENZEP_EMBEDDER_API_KEY
base_url = base_url or cls.OPENZEP_EMBEDDER_BASE_URL
model = model or cls.OPENZEP_EMBEDDER_MODEL
if model and not api_key:
api_key = 'ollama'
return {
'api_key': api_key,
'base_url': base_url,
'model': model,
}
@classmethod
def get_graph_search_reranker_config(cls):
"""返回 app-side 图搜索交叉编码重排使用的 reranker 配置。"""
api_key = cls.GRAPH_SEARCH_APP_RERANKER_API_KEY
base_url = cls.GRAPH_SEARCH_APP_RERANKER_BASE_URL
model = cls.GRAPH_SEARCH_APP_RERANKER_MODEL
provider = cls.GRAPH_SEARCH_APP_RERANKER_PROVIDER
graphiti_reranker_base_url = os.environ.get('GRAPHITI_RERANKER_BASE_URL') or None
if graphiti_reranker_base_url:
api_key = api_key or (os.environ.get('GRAPHITI_RERANKER_API_KEY') or None)
base_url = base_url or graphiti_reranker_base_url
model = model or (os.environ.get('GRAPHITI_RERANKER_MODEL') or None)
return {
'api_key': api_key,
'base_url': base_url,
'model': model,
'provider': provider,
'timeout': cls.GRAPH_SEARCH_APP_RERANKER_TIMEOUT,
}
@classmethod
def get_graphiti_config_errors(cls):
"""返回 Graphiti + Neo4j 的配置错误。"""
errors = []
if not cls.GRAPHITI_URI:
errors.append("GRAPHITI_URI 未配置")
if not cls.GRAPHITI_DATABASE:
errors.append("GRAPHITI_DATABASE 未配置")
if not cls.GRAPHITI_LLM_MODEL:
errors.append("GRAPHITI_LLM_MODEL 未配置")
if not cls.GRAPHITI_EMBEDDER_MODEL:
errors.append("GRAPHITI_EMBEDDER_MODEL 未配置")
return errors
@classmethod
def get_graph_backend_config_errors(cls, api_key=None):
"""根据当前 GRAPH_BACKEND 返回对应的配置错误。"""
backend = (cls.GRAPH_BACKEND or 'graphiti').lower()
if backend in {'zep', 'openzep'}:
return cls.get_zep_config_errors(api_key=api_key)
if backend == 'graphiti':
return cls.get_graphiti_config_errors()
return [f"不支持的 GRAPH_BACKEND: {backend}"]
@classmethod
def is_graph_backend_configured(cls, api_key=None):
return len(cls.get_graph_backend_config_errors(api_key=api_key)) == 0
@classmethod
def validate(cls):
"""验证必要配置"""
errors = []
if not cls.LLM_API_KEY:
errors.append("LLM_API_KEY 未配置")
errors.extend(cls.get_graph_backend_config_errors())
return errors