config.py 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258
  1. """应用配置 —— 基于 pydantic-settings 的多源配置管理(环境变量 / .env / 默认值)."""
  2. import logging
  3. import os
  4. from pathlib import Path
  5. from pydantic import Field
  6. from pydantic_settings import BaseSettings, SettingsConfigDict
  7. from dotenv import load_dotenv
  8. load_dotenv()
  9. logger = logging.getLogger(__name__)
  10. _BACKEND_ROOT = Path(__file__).resolve().parent.parent.parent
  11. _DEFAULT_DATA_DIR = str(_BACKEND_ROOT / "data")
  12. _DEFAULT_DOWNLOADS_DIR = str(_BACKEND_ROOT / "downloads" / "papers")
  13. _DEFAULT_DAILY_ARXIV_CS_CATEGORIES: tuple[str, ...] = (
  14. "cs.AI",
  15. "cs.LG",
  16. "cs.CV",
  17. "cs.CL",
  18. "cs.NE",
  19. "cs.RO",
  20. "cs.IR",
  21. "cs.HC",
  22. )
  23. class Settings(BaseSettings):
  24. """PaperGraph 全局配置,所有字段均可通过环境变量或 .env 文件覆盖."""
  25. model_config = SettingsConfigDict(env_file=".env", case_sensitive=False, extra="ignore")
  26. # ── 应用基础 ──
  27. app_name: str = "PaperGraph"
  28. app_version: str = "0.1.0"
  29. description: str = "学术文献管理系统"
  30. debug: bool = False
  31. # ── 网络与 CORS ──
  32. host: str = "0.0.0.0"
  33. port: int = 8000
  34. cors_origins: str = "http://localhost:5173,http://127.0.0.1:5173"
  35. # ── 外部 API 密钥 ──
  36. ncbi_email: str = ""
  37. ncbi_api_key: str = ""
  38. openalex_mailto: str = Field(default="", description="OpenAlex mailto 邮箱(推荐真实邮箱)")
  39. papergraph_httpx_trust_env: bool = Field(
  40. default=True,
  41. description="httpx 是否信任 HTTP_PROXY/HTTPS_PROXY 环境变量(需代理时设为 True)",
  42. )
  43. # ── Tavily Web 搜索 ──
  44. tavily_api_key: str = Field(default="", description="Tavily API key")
  45. tavily_presearch_enabled: bool = Field(default=True, description="Tavily 预搜索开关")
  46. # ── LLM 配置 ──
  47. openai_api_key: str = ""
  48. openai_base_url: str = "https://api.deepseek.com/v1"
  49. openai_model: str = Field(
  50. default="deepseek-v4-flash",
  51. description="兼容 OpenAI 的 chat 模型 ID",
  52. )
  53. # ── 存储路径 ──
  54. data_dir: str = _DEFAULT_DATA_DIR
  55. downloads_dir: str = Field(
  56. default=_DEFAULT_DOWNLOADS_DIR,
  57. description="PDF 等文件落盘目录,默认 backend/downloads/papers",
  58. )
  59. log_level: str = "INFO"
  60. # ── 每日 arXiv 推荐 ──
  61. daily_arxiv_cs_categories: str = Field(
  62. default="",
  63. description="arXiv 类目前缀,逗号分隔(如 cs.CV,cs.LG);留空使用内置默认",
  64. )
  65. # ── Agent 运行时 ──
  66. agent_runtime_default_timeout_sec: float = Field(
  67. default=20.0,
  68. ge=1.0,
  69. le=300.0,
  70. description="run_agent_task 默认超时(秒)",
  71. )
  72. agent_runtime_default_retries: int = Field(
  73. default=1,
  74. ge=0,
  75. le=5,
  76. description="默认重试次数(不含首轮)",
  77. )
  78. papergraph_intent_parse_max_retries: int = Field(
  79. default=2,
  80. ge=0,
  81. le=5,
  82. description="意图 JSON 解析或校验失败后,让模型重新生成的次数(不含首次)",
  83. )
  84. papergraph_search_recall_wall_sec: float = Field(
  85. default=25.0,
  86. ge=10.0,
  87. le=180.0,
  88. description="多源搜索 anyio 总墙时间(秒)",
  89. )
  90. papergraph_search_arxiv_fallback_wall_sec: float = Field(
  91. default=15.0,
  92. ge=3.0,
  93. le=90.0,
  94. description="arXiv 兜底搜索墙时间(秒)",
  95. )
  96. papergraph_search_recall_http_timeout_sec: float = Field(
  97. default=18.0,
  98. ge=2.0,
  99. le=60.0,
  100. description="多源召回阶段 per-request HTTP 超时(秒)",
  101. )
  102. papergraph_proceedings_supplement_enabled: bool = Field(
  103. default=True,
  104. description="DBLP/OpenAlex 主会钉年召回不足时,用会议官网 proceedings 域补召回",
  105. )
  106. papergraph_proceedings_supplement_min_candidates: int = Field(
  107. default=8,
  108. ge=1,
  109. le=40,
  110. description="候选数低于该阈值时触发 proceedings 官网补召回",
  111. )
  112. papergraph_proceedings_auto_discover: bool = Field(
  113. default=True,
  114. description="无 JSON 域名映射时,用 Tavily 按会议名+年份自动发现 proceedings 官网再检索",
  115. )
  116. papergraph_fine_rank_pipeline_wall_sec: float = Field(
  117. default=25.0,
  118. ge=10.0,
  119. le=120.0,
  120. description="检索流水线内 LLM 精排线程墙钟上限(秒)",
  121. )
  122. papergraph_recall_max_candidates: int = Field(
  123. default=24,
  124. ge=8,
  125. le=60,
  126. description="进入精排前的最大候选篇数(多源召回上限)",
  127. )
  128. papergraph_fine_rank_candidates: int = Field(default=15, ge=5, le=40)
  129. papergraph_search_http_max_attempts: int = Field(default=2, ge=1, le=5)
  130. papergraph_pipeline_parallel_presearch: bool = Field(default=True)
  131. papergraph_venue_hydrate_wall_sec: float = Field(
  132. default=3.0,
  133. ge=1.0,
  134. le=12.0,
  135. description="原文→OpenAlex 会场探测墙钟上限(秒)",
  136. )
  137. papergraph_daily_auto_refresh: bool = Field(default=True)
  138. papergraph_daily_auto_refresh_idle_sec: int = Field(default=90, ge=15, le=3600)
  139. papergraph_daily_auto_refresh_poll_sec: int = Field(default=180, ge=30, le=3600)
  140. papergraph_daily_auto_refresh_startup_grace_sec: int = Field(default=120, ge=10, le=3600)
  141. papergraph_daily_arxiv_http_timeout_sec: float = Field(
  142. default=45.0,
  143. ge=15.0,
  144. le=300.0,
  145. description="arXiv 请求读超时(秒);跨境较慢时可调高",
  146. )
  147. papergraph_daily_arxiv_http_max_attempts: int = Field(
  148. default=3,
  149. ge=1,
  150. le=10,
  151. description="arXiv 请求失败重试上限",
  152. )
  153. dblp_author_pid_min_score: float = Field(
  154. default=3.0,
  155. ge=0.0,
  156. le=10.0,
  157. description="DBLP author PID 匹配最低分数;低于此值退化为全文搜索",
  158. )
  159. openalex_author_match_min_score: float = Field(
  160. default=2.0,
  161. ge=0.0,
  162. le=10.0,
  163. description="OpenAlex author ID 匹配最低分数",
  164. )
  165. dblp_author_name_fallback_search: bool = Field(
  166. default=False,
  167. description="DBLP PID 失败后允许按 author 名回退全文搜索(通常匹配到引用者而非作者本人)",
  168. )
  169. arxiv_or_retry_on_empty: bool = Field(
  170. default=True,
  171. description="arXiv AND 查询 0 结果时自动用 OR 重试以提高经典论文召回",
  172. )
  173. # venue_topic_mismatch_keep_ratio and dblp_venue_aliases_json_path removed — LLM handles both
  174. llm_disable_proxy: bool = Field(default=False)
  175. def get_cors_origins_list(self) -> list[str]:
  176. return [origin.strip() for origin in self.cors_origins.split(',')]
  177. def get_daily_arxiv_cs_categories(self) -> list[str]:
  178. raw = (self.daily_arxiv_cs_categories or "").strip()
  179. if raw:
  180. return [x.strip() for x in raw.split(",") if x.strip()]
  181. return list(_DEFAULT_DAILY_ARXIV_CS_CATEGORIES)
  182. settings = Settings()
  183. def get_settings() -> Settings:
  184. return settings
  185. def validate_config():
  186. errors = []
  187. warnings = []
  188. try:
  189. os.makedirs(settings.data_dir, exist_ok=True)
  190. except Exception as e:
  191. errors.append(f"无法创建数据目录: {e}")
  192. llm_api_key = os.getenv("LLM_API_KEY") or os.getenv("OPENAI_API_KEY") or settings.openai_api_key
  193. if not llm_api_key:
  194. warnings.append("LLM_API_KEY未配置,AI分析功能将无法使用")
  195. if (settings.openalex_mailto or "").strip().lower() == "user@example.com":
  196. warnings.append("OPENALEX_MAILTO 配置为占位符 user@example.com,可能导致 OpenAlex 400/更严格限流(建议改为真实邮箱或留空)")
  197. if errors:
  198. error_msg = "配置错误:\n" + "\n".join(f" - {e}" for e in errors)
  199. raise ValueError(error_msg)
  200. if warnings:
  201. logger.warning("⚠️ 配置警告:")
  202. for w in warnings:
  203. logger.warning(" - %s", w)
  204. return True
  205. def print_config():
  206. logger.info("应用名称: %s", settings.app_name)
  207. logger.info("版本: %s", settings.app_version)
  208. logger.info("服务器: %s:%s", settings.host, settings.port)
  209. logger.info("调试模式: %s", ("开启" if settings.debug else "关闭"))
  210. llm_api_key = os.getenv("LLM_API_KEY") or os.getenv("OPENAI_API_KEY") or settings.openai_api_key
  211. llm_base_url = os.getenv("LLM_BASE_URL") or settings.openai_base_url
  212. llm_model = os.getenv("LLM_MODEL_ID") or settings.openai_model
  213. logger.info("LLM API Key: %s", ("已配置" if llm_api_key else "未配置"))
  214. logger.info("LLM Base URL: %s", llm_base_url)
  215. logger.info("LLM Model: %s", llm_model)
  216. logger.info("数据目录: %s", settings.data_dir)
  217. logger.info("日志级别: %s", settings.log_level)