schemas.py 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233
  1. """Pydantic API 模型 —— 请求/响应 schema 定义."""
  2. from __future__ import annotations
  3. from datetime import datetime
  4. from enum import Enum
  5. from typing import Any
  6. from pydantic import BaseModel, Field
  7. class PaperSource(str, Enum):
  8. ARXIV = "arxiv"
  9. OPENALEX = "openalex"
  10. DBLP = "dblp"
  11. TAVILY = "tavily"
  12. UNKNOWN = "unknown"
  13. class ReadStatus(str, Enum):
  14. UNREAD = "unread"
  15. READING = "reading"
  16. READ = "read"
  17. class FeedbackActionEnum(str, Enum):
  18. CLICK = "click"
  19. SAVE = "save"
  20. SKIP = "skip"
  21. IGNORE = "ignore"
  22. READ = "read"
  23. class BaseAPIResponse(BaseModel):
  24. success: bool
  25. message: str | None = None
  26. class Author(BaseModel):
  27. name: str
  28. affiliation: str | None = None
  29. email: str | None = None
  30. orcid: str | None = None
  31. db_id: int | None = Field(default=None, description="本地 authors 表 id,用于区分同名")
  32. class Paper(BaseModel):
  33. id: int | None = None
  34. title: str
  35. authors: list[Author] = Field(default_factory=list)
  36. abstract: str | None = None
  37. doi: str | None = None
  38. pmid: str | None = None
  39. arxiv_id: str | None = None
  40. pmc_id: str | None = None
  41. journal: str | None = None
  42. venue_type: str | None = Field(default=None, description="会议/期刊类型:conference 或 journal")
  43. year: int | None = None
  44. volume: str | None = None
  45. issue: str | None = None
  46. pages: str | None = None
  47. publisher: str | None = None
  48. pdf_url: str | None = None
  49. source_url: str | None = None
  50. local_pdf_path: str | None = Field(default=None, description="本地 PDF 相对路径")
  51. keywords: list[str] = Field(default_factory=list)
  52. mesh_terms: list[str] = Field(default_factory=list)
  53. references: list[str] = Field(default_factory=list)
  54. citations: int = 0
  55. source: PaperSource = PaperSource.UNKNOWN
  56. relevance_score: float = 0.0
  57. notes: str | None = None
  58. tags: list[str] = Field(default_factory=list)
  59. category: str | None = Field(
  60. default=None,
  61. description="文献库领域(保存时由大模型或手写)",
  62. )
  63. rating: int | None = None
  64. read_status: ReadStatus = ReadStatus.UNREAD
  65. importance: str = "normal"
  66. created_at: datetime | None = None
  67. updated_at: datetime | None = None
  68. class PapersResponse(BaseAPIResponse):
  69. total: int
  70. papers: list[Paper] = Field(default_factory=list)
  71. class LibraryCategoryFolder(BaseModel):
  72. category: str
  73. folder: str
  74. count: int
  75. children: list[dict[str, Any]] = Field(default_factory=list)
  76. class LibraryCategoriesResponse(BaseAPIResponse):
  77. store_root: str = "文献库"
  78. folders: list[LibraryCategoryFolder] = Field(default_factory=list)
  79. class SavePapersRequest(BaseModel):
  80. papers: list[Paper]
  81. download_pdfs: bool = Field(default=False, description="保存后下载 PDF")
  82. llm_classify: bool = Field(default=True, description="大模型划分 category")
  83. class SavePapersResponse(BaseAPIResponse):
  84. added: int
  85. updated: int = 0
  86. ids: list[int] = Field(default_factory=list)
  87. pdf_downloaded: int = 0
  88. llm_classified: int = 0
  89. class DailyPaperPickHint(BaseModel):
  90. identity_key: str = Field(description="身份键,如 arxiv:2401.0001")
  91. pick_kind: str = Field(description="personalized | general")
  92. explanation: str = Field(default="", description="入选理由")
  93. class DailyPapersRequest(BaseModel):
  94. days_back: int = Field(default=5, ge=0, le=30, description="arXiv 最近 N 天")
  95. arxiv_max_results: int = Field(default=20, ge=10, le=50, description="arXiv 候选数")
  96. arxiv_categories: list[str] | None = Field(default=None, description="arXiv 分类过滤")
  97. personalized_k: int = Field(default=20, ge=0, le=40, description="个性化推荐条数")
  98. library_limit: int = Field(default=800, ge=50, le=3000, description="库内候选上限")
  99. force_refresh: bool = Field(default=False, description="忽略缓存强制刷新")
  100. use_llm_rank: bool = Field(default=False, description="是否启用 LLM 精排")
  101. rerank_recall_max: int = Field(default=24, ge=8, le=60, description="精排前召回候选上限")
  102. use_llm_theme_keywords: bool = Field(default=True, description="LLM 生成主题标签")
  103. class DailyPapersResponse(BaseAPIResponse):
  104. date_key: str
  105. arxiv_latest_total: int
  106. arxiv_selected_total: int
  107. personalized_total: int
  108. arxiv_latest: list[Paper] = Field(default_factory=list)
  109. arxiv_selected: list[Paper] = Field(default_factory=list)
  110. personalized: list[Paper] = Field(default_factory=list)
  111. memory_keywords_used: list[str] = Field(default_factory=list, description="偏好词摘要")
  112. strategy_explanation: str = Field(default="", description="推荐策略摘要(≤2 行中文)")
  113. personalized_theme_keywords: list[str] = Field(default_factory=list, description="个性化列表主题标签")
  114. general_theme_keywords: list[str] = Field(default_factory=list, description="精选列表主题标签")
  115. personalized_pick_hints: list[DailyPaperPickHint] = Field(default_factory=list)
  116. general_pick_hints: list[DailyPaperPickHint] = Field(default_factory=list)
  117. class UpdatePaperRequest(BaseModel):
  118. notes: str | None = None
  119. tags: list[str] | None = None
  120. category: str | None = None
  121. rating: int | None = None
  122. read_status: ReadStatus | None = None
  123. importance: str | None = None
  124. class UpdatePaperResponse(BaseAPIResponse):
  125. updated_fields: list[str] = Field(default_factory=list)
  126. class DeletePaperResponse(BaseAPIResponse):
  127. pass
  128. class GraphNode(BaseModel):
  129. id: str
  130. type: str
  131. label: str
  132. paper_id: int | None = None
  133. year: int | None = None
  134. category: str | None = None
  135. journal: str | None = None
  136. venue_type: str | None = None
  137. weight: float = 1.0
  138. class GraphEdge(BaseModel):
  139. source: str
  140. target: str
  141. type: str
  142. weight: float = 1.0
  143. evidence: str | None = None
  144. class LibraryGraphResponse(BaseAPIResponse):
  145. nodes: list[GraphNode] = Field(default_factory=list)
  146. edges: list[GraphEdge] = Field(default_factory=list)
  147. class PaperReaderOpeningRequest(BaseModel):
  148. paper_id: int = Field(..., ge=1)
  149. class PaperReaderOpeningResponse(BaseAPIResponse):
  150. opening: str
  151. pdf_parsing: bool = False
  152. class PaperReaderChatRequest(BaseModel):
  153. paper_id: int = Field(..., ge=1)
  154. messages: list[dict[str, str]] = Field(default_factory=list)
  155. user_message: str = Field(..., min_length=1, max_length=12000)
  156. class PaperReaderChatResponse(BaseAPIResponse):
  157. reply: str
  158. pdf_parsing: bool = False
  159. related_papers: list[Paper] = Field(default_factory=list)
  160. related_hints: list[dict[str, Any]] = Field(default_factory=list)
  161. kg_edges: list[dict[str, Any]] = Field(default_factory=list)
  162. class PaperReaderHistoryItem(BaseModel):
  163. role: str
  164. content: str
  165. created_at: int
  166. class PaperReaderHistoryResponse(BaseAPIResponse):
  167. paper_id: int
  168. turns: list[PaperReaderHistoryItem] = Field(default_factory=list)
  169. class ReadingLogRequest(BaseModel):
  170. paper_id: int = Field(..., ge=1)
  171. duration_sec: int = Field(..., ge=1, le=60 * 60 * 24, description="本次阅读停留时长(秒)")
  172. client_ts: int | None = Field(default=None, description="客户端时间戳(秒);缺省则服务端按当前时间落在当天")
  173. class ReadingCalendarItem(BaseModel):
  174. date: str = Field(..., description="YYYY-MM-DD")
  175. seconds: int = 0
  176. sessions: int = 0
  177. class ReadingCalendarResponse(BaseAPIResponse):
  178. days: int = 180
  179. items: list[ReadingCalendarItem] = Field(default_factory=list)
  180. class DailyRecommendFeedbackRequest(BaseModel):
  181. identity_key: str = Field(..., description="论文身份标识(如 arxiv:2401.0001 / doi:xxx / title_hash:xxx)")
  182. title: str | None = Field(default=None, description="论文标题")
  183. action: FeedbackActionEnum = Field(..., description="用户动作")
  184. source_list: str | None = Field(default=None, description="推荐来源: personalized 或 general")
  185. score_at_recommend: float | None = Field(default=None, description="推荐时的匹配分数")
  186. keywords: list[str] | None = Field(default=None, description="论文关键词")
  187. category: str | None = Field(default=None, description="论文分类")
  188. journal: str | None = Field(default=None, description="论文期刊/会议(用于负反馈建模)")
  189. source: str | None = Field(default=None, description="数据源(用于负反馈建模)")
  190. class DailyRecommendFeedbackResponse(BaseAPIResponse):
  191. pass