responses.py 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604
  1. from typing import Any, Literal, Optional
  2. from pydantic import BaseModel, Field
  3. from shared.abstractions import (
  4. AggregateSearchResult,
  5. ChunkSearchResult,
  6. GraphSearchResult,
  7. LLMChatCompletion,
  8. Message,
  9. WebPageSearchResult,
  10. )
  11. from shared.api.models.base import R2RResults
  12. from shared.api.models.management.responses import DocumentResponse
  13. from ....abstractions import R2RSerializable
  14. class CitationSpan(R2RSerializable):
  15. """Represents a single occurrence of a citation in text."""
  16. start_index: int = Field(
  17. ..., description="Starting character index of the citation"
  18. )
  19. end_index: int = Field(
  20. ..., description="Ending character index of the citation"
  21. )
  22. context_start: int = Field(
  23. ..., description="Starting index of the surrounding context"
  24. )
  25. context_end: int = Field(
  26. ..., description="Ending index of the surrounding context"
  27. )
  28. class Citation(R2RSerializable):
  29. """
  30. Represents a citation reference in the RAG response.
  31. The first time a citation appears, it includes the full payload.
  32. Subsequent appearances only include the citation ID and span information.
  33. """
  34. # Basic identification
  35. id: str = Field(
  36. ..., description="The short ID of the citation (e.g., 'e41ac2d')"
  37. )
  38. object: str = Field(
  39. "citation", description="The type of object, always 'citation'"
  40. )
  41. # Optimize payload delivery
  42. is_new: bool = Field(
  43. True,
  44. description="Whether this is the first occurrence of this citation",
  45. )
  46. # Position information
  47. span: Optional[CitationSpan] = Field(
  48. None, description="Position of this citation occurrence in the text"
  49. )
  50. # Source information - only included for first occurrence
  51. source_type: Optional[str] = Field(
  52. None, description="Type of source: 'chunk', 'graph', 'web', or 'doc'"
  53. )
  54. # Full payload - only included for first occurrence
  55. payload: (
  56. ChunkSearchResult
  57. | GraphSearchResult
  58. | WebPageSearchResult
  59. | DocumentResponse
  60. | dict[str, Any]
  61. | None
  62. ) = Field(
  63. None,
  64. description="The complete source object (only included for new citations)",
  65. )
  66. class Config:
  67. extra = "ignore"
  68. json_schema_extra = {
  69. "example": {
  70. "id": "e41ac2d",
  71. "object": "citation",
  72. "is_new": True,
  73. "span": {
  74. "start_index": 120,
  75. "end_index": 129,
  76. "context_start": 80,
  77. "context_end": 180,
  78. },
  79. "source_type": "chunk",
  80. "payload": {
  81. "id": "e41ac2d1-full-id",
  82. "text": "The study found significant improvements...",
  83. "metadata": {"title": "Research Paper"},
  84. },
  85. }
  86. }
  87. # class Citation(R2RSerializable):
  88. # """Represents a single citation reference in the RAG response.
  89. # Combines both bracket metadata (start/end offsets, snippet range) and the
  90. # mapped source fields (id, doc ID, chunk text, etc.).
  91. # """
  92. # # Bracket references
  93. # id: str = Field(..., description="The ID of the citation object")
  94. # object: str = Field(
  95. # ...,
  96. # description="The type of object, e.g. `citation`",
  97. # )
  98. # payload: (
  99. # ChunkSearchResult
  100. # | GraphSearchResult
  101. # | WebPageSearchResult
  102. # | DocumentResponse
  103. # | None
  104. # ) = Field(
  105. # ..., description="The object payload and it's corresponding type"
  106. # )
  107. # class Config:
  108. # extra = "ignore" # This tells Pydantic to ignore extra fields
  109. # json_schema_extra = {
  110. # "example": {
  111. # "id": "cit.abcd123",
  112. # "object": "citation",
  113. # "payload": "ChunkSearchResult(...)",
  114. # }
  115. # }
  116. class RAGResponse(R2RSerializable):
  117. generated_answer: str = Field(
  118. ..., description="The generated completion from the RAG process"
  119. )
  120. search_results: AggregateSearchResult = Field(
  121. ..., description="The search results used for the RAG process"
  122. )
  123. citations: Optional[list[Citation]] = Field(
  124. None,
  125. description="Structured citation metadata, if you do citation extraction.",
  126. )
  127. metadata: dict = Field(
  128. default_factory=dict,
  129. description="Additional data returned by the LLM provider",
  130. )
  131. completion: str = Field(
  132. ...,
  133. description="The generated completion from the RAG process",
  134. # deprecated=True,
  135. )
  136. class Config:
  137. json_schema_extra = {
  138. "example": {
  139. "generated_answer": "The capital of France is Paris.",
  140. "search_results": {
  141. "chunk_search_results": [
  142. {
  143. "index": 1,
  144. "start_index": 25,
  145. "end_index": 28,
  146. "uri": "https://example.com/doc1",
  147. "title": "example_document_1.pdf",
  148. "license": "CC-BY-4.0",
  149. }
  150. ],
  151. "graph_search_results": [
  152. {
  153. "content": {
  154. "id": "3f3d47f3-8baf-58eb-8bc2-0171fb1c6e09",
  155. "name": "Entity Name",
  156. "description": "Entity Description",
  157. "metadata": {},
  158. },
  159. "result_type": "entity",
  160. "chunk_ids": [
  161. "c68dc72e-fc23-5452-8f49-d7bd46088a96"
  162. ],
  163. "metadata": {
  164. "associated_query": "What is the capital of France?"
  165. },
  166. }
  167. ],
  168. "web_search_results": [
  169. {
  170. "title": "Page Title",
  171. "link": "https://example.com/page",
  172. "snippet": "Page snippet",
  173. "position": 1,
  174. "date": "2021-01-01",
  175. "sitelinks": [
  176. {
  177. "title": "Sitelink Title",
  178. "link": "https://example.com/sitelink",
  179. }
  180. ],
  181. }
  182. ],
  183. "document_search_results": [
  184. {
  185. "document": {
  186. "id": "3f3d47f3-8baf-58eb-8bc2-0171fb1c6e09",
  187. "title": "Document Title",
  188. "chunks": ["Chunk 1", "Chunk 2"],
  189. "metadata": {},
  190. },
  191. }
  192. ],
  193. },
  194. "citations": [
  195. {
  196. "index": 1,
  197. "rawIndex": 9,
  198. "startIndex": 393,
  199. "endIndex": 396,
  200. "snippetStartIndex": 320,
  201. "snippetEndIndex": 418,
  202. "sourceType": "chunk",
  203. "id": "e760bb76-1c6e-52eb-910d-0ce5b567011b",
  204. "document_id": "e43864f5-a36f-548e-aacd-6f8d48b30c7f",
  205. "owner_id": "2acb499e-8428-543b-bd85-0d9098718220",
  206. "collection_ids": [
  207. "122fdf6a-e116-546b-a8f6-e4cb2e2c0a09"
  208. ],
  209. "score": 0.64,
  210. "text": "Document Title: DeepSeek_R1.pdf\n\nText: could achieve an accuracy of ...",
  211. "metadata": {
  212. "title": "DeepSeek_R1.pdf",
  213. "license": "CC-BY-4.0",
  214. "chunk_order": 68,
  215. "document_type": "pdf",
  216. },
  217. }
  218. ],
  219. "metadata": {
  220. "id": "chatcmpl-example123",
  221. "choices": [
  222. {
  223. "finish_reason": "stop",
  224. "index": 0,
  225. "message": {"role": "assistant"},
  226. }
  227. ],
  228. },
  229. "completion": "TO BE DEPRECATED",
  230. }
  231. }
  232. class AgentResponse(R2RSerializable):
  233. messages: list[Message] = Field(..., description="Agent response messages")
  234. conversation_id: str = Field(
  235. ..., description="The conversation ID for the RAG agent response"
  236. )
  237. class Config:
  238. json_schema_extra = {
  239. "example": {
  240. "messages": [
  241. {
  242. "role": "assistant",
  243. "content": """Aristotle (384–322 BC) was an Ancient
  244. Greek philosopher and polymath whose contributions
  245. have had a profound impact on various fields of
  246. knowledge.
  247. Here are some key points about his life and work:
  248. \n\n1. **Early Life**: Aristotle was born in 384 BC in
  249. Stagira, Chalcidice, which is near modern-day
  250. Thessaloniki, Greece. His father, Nicomachus, was the
  251. personal physician to King Amyntas of Macedon, which
  252. exposed Aristotle to medical and biological knowledge
  253. from a young age [C].\n\n2. **Education and Career**:
  254. After the death of his parents, Aristotle was sent to
  255. Athens to study at Plato's Academy, where he remained
  256. for about 20 years. After Plato's death, Aristotle
  257. left Athens and eventually became the tutor of
  258. Alexander the Great [C].
  259. \n\n3. **Philosophical Contributions**: Aristotle
  260. founded the Lyceum in Athens, where he established the
  261. Peripatetic school of philosophy. His works cover a
  262. wide range of subjects, including metaphysics, ethics,
  263. politics, logic, biology, and aesthetics. His writings
  264. laid the groundwork for many modern scientific and
  265. philosophical inquiries [A].\n\n4. **Legacy**:
  266. Aristotle's influence extends beyond philosophy to the
  267. natural sciences, linguistics, economics, and
  268. psychology. His method of systematic observation and
  269. analysis has been foundational to the development of
  270. modern science [A].\n\nAristotle's comprehensive
  271. approach to knowledge and his systematic methodology
  272. have earned him a lasting legacy as one of the
  273. greatest philosophers of all time.\n\nSources:
  274. \n- [A] Aristotle's broad range of writings and
  275. influence on modern science.\n- [C] Details about
  276. Aristotle's early life and education.""",
  277. "name": None,
  278. "function_call": None,
  279. "tool_calls": None,
  280. "metadata": {
  281. "citations": [
  282. {
  283. "index": 1,
  284. "rawIndex": 9,
  285. "startIndex": 393,
  286. "endIndex": 396,
  287. "snippetStartIndex": 320,
  288. "snippetEndIndex": 418,
  289. "sourceType": "chunk",
  290. "id": "e760bb76-1c6e-52eb-910d-0ce5b567011b",
  291. "document_id": """
  292. e43864f5-a36f-548e-aacd-6f8d48b30c7f
  293. """,
  294. "owner_id": """
  295. 2acb499e-8428-543b-bd85-0d9098718220
  296. """,
  297. "collection_ids": [
  298. "122fdf6a-e116-546b-a8f6-e4cb2e2c0a09"
  299. ],
  300. "score": 0.64,
  301. "text": """
  302. Document Title: DeepSeek_R1.pdf
  303. \n\nText: could achieve an accuracy of ...
  304. """,
  305. "metadata": {
  306. "title": "DeepSeek_R1.pdf",
  307. "license": "CC-BY-4.0",
  308. "chunk_order": 68,
  309. "document_type": "pdf",
  310. },
  311. }
  312. ],
  313. "aggregated_search_results": {
  314. "chunk_search_results": [
  315. {
  316. "id": "3f3d47f3-8baf-58eb-8bc2-0171fb1c6e09",
  317. "document_id": "3e157b3a-8469-51db-90d9-52e7d896b49b",
  318. "owner_id": "2acb499e-8428-543b-bd85-0d9098718220",
  319. "collection_ids": [],
  320. "score": 0.23943702876567796,
  321. "text": "Example text from the document",
  322. "metadata": {
  323. "title": "example_document.pdf",
  324. "associated_query": "What is the capital of France?",
  325. },
  326. }
  327. ],
  328. "graph_search_results": [
  329. {
  330. "content": {
  331. "id": "3f3d47f3-8baf-58eb-8bc2-0171fb1c6e09",
  332. "name": "Entity Name",
  333. "description": "Entity Description",
  334. "metadata": {},
  335. },
  336. "result_type": "entity",
  337. "chunk_ids": [
  338. "c68dc72e-fc23-5452-8f49-d7bd46088a96"
  339. ],
  340. "metadata": {
  341. "associated_query": "What is the capital of France?"
  342. },
  343. }
  344. ],
  345. "web_search_results": [
  346. {
  347. "title": "Page Title",
  348. "link": "https://example.com/page",
  349. "snippet": "Page snippet",
  350. "position": 1,
  351. "date": "2021-01-01",
  352. "sitelinks": [
  353. {
  354. "title": "Sitelink Title",
  355. "link": "https://example.com/sitelink",
  356. }
  357. ],
  358. }
  359. ],
  360. "document_search_results": [
  361. {
  362. "document": {
  363. "id": "3f3d47f3-8baf-58eb-8bc2-0171fb1c6e09",
  364. "title": "Document Title",
  365. "chunks": ["Chunk 1", "Chunk 2"],
  366. "metadata": {},
  367. },
  368. }
  369. ],
  370. },
  371. },
  372. },
  373. ],
  374. "conversation_id": "a32b4c5d-6e7f-8a9b-0c1d-2e3f4a5b6c7d",
  375. }
  376. }
  377. class DocumentSearchResult(BaseModel):
  378. document_id: str = Field(
  379. ...,
  380. description="The document ID",
  381. )
  382. metadata: Optional[dict] = Field(
  383. None,
  384. description="The metadata of the document",
  385. )
  386. score: float = Field(
  387. ...,
  388. description="The score of the document",
  389. )
  390. # A generic base model for SSE events
  391. class SSEEventBase(BaseModel):
  392. event: str
  393. data: Any
  394. # Model for the search results event
  395. class SearchResultsData(BaseModel):
  396. id: str
  397. object: str
  398. data: AggregateSearchResult
  399. class SearchResultsEvent(SSEEventBase):
  400. event: Literal["search_results"]
  401. data: SearchResultsData
  402. class DeltaPayload(BaseModel):
  403. value: str
  404. annotations: list[Any]
  405. # Model for message events (partial tokens)
  406. class MessageDelta(BaseModel):
  407. type: str
  408. payload: DeltaPayload
  409. class Delta(BaseModel):
  410. content: list[MessageDelta]
  411. class MessageData(BaseModel):
  412. id: str
  413. object: str
  414. delta: Delta
  415. class MessageEvent(SSEEventBase):
  416. event: Literal["message"]
  417. data: MessageData
  418. # Update CitationSpan model for SSE events
  419. class CitationSpanData(BaseModel):
  420. start: int = Field(
  421. ..., description="Starting character index of the citation"
  422. )
  423. end: int = Field(..., description="Ending character index of the citation")
  424. context_start: Optional[int] = Field(
  425. None, description="Starting index of surrounding context"
  426. )
  427. context_end: Optional[int] = Field(
  428. None, description="Ending index of surrounding context"
  429. )
  430. # Update CitationData model
  431. class CitationData(BaseModel):
  432. id: str = Field(
  433. ..., description="The short ID of the citation (e.g., 'e41ac2d')"
  434. )
  435. object: str = Field(
  436. "citation", description="The type of object, always 'citation'"
  437. )
  438. # New fields from the enhanced Citation model
  439. is_new: Optional[bool] = Field(
  440. None,
  441. description="Whether this is the first occurrence of this citation",
  442. )
  443. span: Optional[CitationSpanData] = Field(
  444. None, description="Position of this citation occurrence in the text"
  445. )
  446. source_type: Optional[str] = Field(
  447. None, description="Type of source: 'chunk', 'graph', 'web', or 'doc'"
  448. )
  449. # Optional payload field, only for first occurrence
  450. payload: Optional[Any] = Field(
  451. None,
  452. description="The complete source object (only included for new citations)",
  453. )
  454. # For backward compatibility, maintain the existing fields
  455. class Config:
  456. populate_by_name = True
  457. extra = "ignore"
  458. # CitationEvent remains the same, but now using the updated CitationData
  459. class CitationEvent(SSEEventBase):
  460. event: Literal["citation"]
  461. data: CitationData
  462. # Model for the final answer event
  463. class FinalAnswerData(BaseModel):
  464. generated_answer: str
  465. citations: list[Citation] # refine if you have a citation model
  466. class FinalAnswerEvent(SSEEventBase):
  467. event: Literal["final_answer"]
  468. data: FinalAnswerData
  469. # "tool_call" event
  470. class ToolCallData(BaseModel):
  471. tool_call_id: str
  472. name: str
  473. arguments: Any # If JSON arguments, use dict[str, Any], or str if needed
  474. class ToolCallEvent(SSEEventBase):
  475. event: Literal["tool_call"]
  476. data: ToolCallData
  477. # "tool_result" event
  478. class ToolResultData(BaseModel):
  479. tool_call_id: str
  480. role: Literal["tool", "function"]
  481. content: str
  482. class ToolResultEvent(SSEEventBase):
  483. event: Literal["tool_result"]
  484. data: ToolResultData
  485. # Optionally, define a fallback model for unrecognized events
  486. class UnknownEvent(SSEEventBase):
  487. pass
  488. # 1) Define a new ThinkingEvent type
  489. class ThinkingData(BaseModel):
  490. id: str
  491. object: str
  492. delta: Delta
  493. class ThinkingEvent(SSEEventBase):
  494. event: str = "thinking"
  495. data: ThinkingData
  496. # Create a union type for all RAG events
  497. RAGEvent = (
  498. SearchResultsEvent
  499. | MessageEvent
  500. | CitationEvent
  501. | FinalAnswerEvent
  502. | UnknownEvent
  503. | ToolCallEvent
  504. | ToolResultEvent
  505. | ToolResultData
  506. | ToolResultEvent
  507. )
  508. AgentEvent = (
  509. ThinkingEvent
  510. | SearchResultsEvent
  511. | MessageEvent
  512. | CitationEvent
  513. | FinalAnswerEvent
  514. | ToolCallEvent
  515. | ToolResultEvent
  516. | UnknownEvent
  517. )
  518. WrappedCompletionResponse = R2RResults[LLMChatCompletion]
  519. # Create wrapped versions of the responses
  520. WrappedVectorSearchResponse = R2RResults[list[ChunkSearchResult]]
  521. WrappedSearchResponse = R2RResults[AggregateSearchResult]
  522. # FIXME: This is returning DocumentResponse, but should be DocumentSearchResult
  523. WrappedDocumentSearchResponse = R2RResults[list[DocumentResponse]]
  524. WrappedRAGResponse = R2RResults[RAGResponse]
  525. WrappedAgentResponse = R2RResults[AgentResponse]
  526. WrappedLLMChatCompletion = R2RResults[LLMChatCompletion]
  527. WrappedEmbeddingResponse = R2RResults[list[float]]