conversations_router.py 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737
  1. import logging
  2. import textwrap
  3. from typing import Optional
  4. from uuid import UUID
  5. from fastapi import Body, Depends, Path, Query
  6. from fastapi.background import BackgroundTasks
  7. from fastapi.responses import FileResponse
  8. from core.base import Message, R2RException
  9. from core.base.api.models import (
  10. GenericBooleanResponse,
  11. WrappedBooleanResponse,
  12. WrappedConversationMessagesResponse,
  13. WrappedConversationResponse,
  14. WrappedConversationsResponse,
  15. WrappedMessageResponse,
  16. )
  17. from ...abstractions import R2RProviders, R2RServices
  18. from ...config import R2RConfig
  19. from .base_router import BaseRouterV3
  20. logger = logging.getLogger()
  21. class ConversationsRouter(BaseRouterV3):
  22. def __init__(
  23. self, providers: R2RProviders, services: R2RServices, config: R2RConfig
  24. ):
  25. logging.info("Initializing ConversationsRouter")
  26. super().__init__(providers, services, config)
  27. def _setup_routes(self):
  28. @self.router.post(
  29. "/conversations",
  30. summary="Create a new conversation",
  31. dependencies=[Depends(self.rate_limit_dependency)],
  32. openapi_extra={
  33. "x-codeSamples": [
  34. {
  35. "lang": "Python",
  36. "source": textwrap.dedent("""
  37. from r2r import R2RClient
  38. client = R2RClient()
  39. # when using auth, do client.login(...)
  40. result = client.conversations.create()
  41. """),
  42. },
  43. {
  44. "lang": "JavaScript",
  45. "source": textwrap.dedent("""
  46. const { r2rClient } = require("r2r-js");
  47. const client = new r2rClient();
  48. function main() {
  49. const response = await client.conversations.create();
  50. }
  51. main();
  52. """),
  53. },
  54. {
  55. "lang": "cURL",
  56. "source": textwrap.dedent("""
  57. curl -X POST "https://api.example.com/v3/conversations" \\
  58. -H "Authorization: Bearer YOUR_API_KEY"
  59. """),
  60. },
  61. ]
  62. },
  63. )
  64. @self.base_endpoint
  65. async def create_conversation(
  66. name: Optional[str] = Body(
  67. None, description="The name of the conversation", embed=True
  68. ),
  69. auth_user=Depends(self.providers.auth.auth_wrapper()),
  70. ) -> WrappedConversationResponse:
  71. """Create a new conversation.
  72. This endpoint initializes a new conversation for the authenticated
  73. user.
  74. """
  75. user_id = auth_user.id
  76. return await self.services.management.create_conversation( # type: ignore
  77. user_id=user_id,
  78. name=name,
  79. )
  80. @self.router.get(
  81. "/conversations",
  82. summary="List conversations",
  83. dependencies=[Depends(self.rate_limit_dependency)],
  84. openapi_extra={
  85. "x-codeSamples": [
  86. {
  87. "lang": "Python",
  88. "source": textwrap.dedent("""
  89. from r2r import R2RClient
  90. client = R2RClient()
  91. # when using auth, do client.login(...)
  92. result = client.conversations.list(
  93. offset=0,
  94. limit=10,
  95. )
  96. """),
  97. },
  98. {
  99. "lang": "JavaScript",
  100. "source": textwrap.dedent("""
  101. const { r2rClient } = require("r2r-js");
  102. const client = new r2rClient();
  103. function main() {
  104. const response = await client.conversations.list();
  105. }
  106. main();
  107. """),
  108. },
  109. {
  110. "lang": "cURL",
  111. "source": textwrap.dedent("""
  112. curl -X GET "https://api.example.com/v3/conversations?offset=0&limit=10" \\
  113. -H "Authorization: Bearer YOUR_API_KEY"
  114. """),
  115. },
  116. ]
  117. },
  118. )
  119. @self.base_endpoint
  120. async def list_conversations(
  121. ids: list[str] = Query(
  122. [],
  123. description="A list of conversation IDs to retrieve. If not provided, all conversations will be returned.",
  124. ),
  125. offset: int = Query(
  126. 0,
  127. ge=0,
  128. description="Specifies the number of objects to skip. Defaults to 0.",
  129. ),
  130. limit: int = Query(
  131. 100,
  132. ge=1,
  133. le=1000,
  134. description="Specifies a limit on the number of objects to return, ranging between 1 and 100. Defaults to 100.",
  135. ),
  136. auth_user=Depends(self.providers.auth.auth_wrapper()),
  137. ) -> WrappedConversationsResponse:
  138. """List conversations with pagination and sorting options.
  139. This endpoint returns a paginated list of conversations for the
  140. authenticated user.
  141. """
  142. requesting_user_id = (
  143. None if auth_user.is_superuser else [auth_user.id]
  144. )
  145. conversation_uuids = [
  146. UUID(conversation_id) for conversation_id in ids
  147. ]
  148. conversations_response = (
  149. await self.services.management.conversations_overview(
  150. offset=offset,
  151. limit=limit,
  152. conversation_ids=conversation_uuids,
  153. user_ids=requesting_user_id,
  154. )
  155. )
  156. return conversations_response["results"], { # type: ignore
  157. "total_entries": conversations_response["total_entries"]
  158. }
  159. @self.router.post(
  160. "/conversations/export",
  161. summary="Export conversations to CSV",
  162. dependencies=[Depends(self.rate_limit_dependency)],
  163. openapi_extra={
  164. "x-codeSamples": [
  165. {
  166. "lang": "Python",
  167. "source": textwrap.dedent("""
  168. from r2r import R2RClient
  169. client = R2RClient("http://localhost:7272")
  170. # when using auth, do client.login(...)
  171. response = client.conversations.export(
  172. output_path="export.csv",
  173. columns=["id", "created_at"],
  174. include_header=True,
  175. )
  176. """),
  177. },
  178. {
  179. "lang": "JavaScript",
  180. "source": textwrap.dedent("""
  181. const { r2rClient } = require("r2r-js");
  182. const client = new r2rClient("http://localhost:7272");
  183. function main() {
  184. await client.conversations.export({
  185. outputPath: "export.csv",
  186. columns: ["id", "created_at"],
  187. includeHeader: true,
  188. });
  189. }
  190. main();
  191. """),
  192. },
  193. {
  194. "lang": "cURL",
  195. "source": textwrap.dedent("""
  196. curl -X POST "http://127.0.0.1:7272/v3/conversations/export" \
  197. -H "Authorization: Bearer YOUR_API_KEY" \
  198. -H "Content-Type: application/json" \
  199. -H "Accept: text/csv" \
  200. -d '{ "columns": ["id", "created_at"], "include_header": true }' \
  201. --output export.csv
  202. """),
  203. },
  204. ]
  205. },
  206. )
  207. @self.base_endpoint
  208. async def export_conversations(
  209. background_tasks: BackgroundTasks,
  210. columns: Optional[list[str]] = Body(
  211. None, description="Specific columns to export"
  212. ),
  213. filters: Optional[dict] = Body(
  214. None, description="Filters to apply to the export"
  215. ),
  216. include_header: Optional[bool] = Body(
  217. True, description="Whether to include column headers"
  218. ),
  219. auth_user=Depends(self.providers.auth.auth_wrapper()),
  220. ) -> FileResponse:
  221. """Export conversations as a downloadable CSV file."""
  222. if not auth_user.is_superuser:
  223. raise R2RException(
  224. "Only a superuser can export data.",
  225. 403,
  226. )
  227. (
  228. csv_file_path,
  229. temp_file,
  230. ) = await self.services.management.export_conversations(
  231. columns=columns,
  232. filters=filters,
  233. include_header=include_header
  234. if include_header is not None
  235. else True,
  236. )
  237. background_tasks.add_task(temp_file.close)
  238. return FileResponse(
  239. path=csv_file_path,
  240. media_type="text/csv",
  241. filename="documents_export.csv",
  242. )
  243. @self.router.post(
  244. "/conversations/export_messages",
  245. summary="Export messages to CSV",
  246. dependencies=[Depends(self.rate_limit_dependency)],
  247. openapi_extra={
  248. "x-codeSamples": [
  249. {
  250. "lang": "Python",
  251. "source": textwrap.dedent("""
  252. from r2r import R2RClient
  253. client = R2RClient("http://localhost:7272")
  254. # when using auth, do client.login(...)
  255. response = client.conversations.export_messages(
  256. output_path="export.csv",
  257. columns=["id", "created_at"],
  258. include_header=True,
  259. )
  260. """),
  261. },
  262. {
  263. "lang": "JavaScript",
  264. "source": textwrap.dedent("""
  265. const { r2rClient } = require("r2r-js");
  266. const client = new r2rClient("http://localhost:7272");
  267. function main() {
  268. await client.conversations.exportMessages({
  269. outputPath: "export.csv",
  270. columns: ["id", "created_at"],
  271. includeHeader: true,
  272. });
  273. }
  274. main();
  275. """),
  276. },
  277. {
  278. "lang": "cURL",
  279. "source": textwrap.dedent("""
  280. curl -X POST "http://127.0.0.1:7272/v3/conversations/export_messages" \
  281. -H "Authorization: Bearer YOUR_API_KEY" \
  282. -H "Content-Type: application/json" \
  283. -H "Accept: text/csv" \
  284. -d '{ "columns": ["id", "created_at"], "include_header": true }' \
  285. --output export.csv
  286. """),
  287. },
  288. ]
  289. },
  290. )
  291. @self.base_endpoint
  292. async def export_messages(
  293. background_tasks: BackgroundTasks,
  294. columns: Optional[list[str]] = Body(
  295. None, description="Specific columns to export"
  296. ),
  297. filters: Optional[dict] = Body(
  298. None, description="Filters to apply to the export"
  299. ),
  300. include_header: Optional[bool] = Body(
  301. True, description="Whether to include column headers"
  302. ),
  303. auth_user=Depends(self.providers.auth.auth_wrapper()),
  304. ) -> FileResponse:
  305. """Export conversations as a downloadable CSV file."""
  306. if not auth_user.is_superuser:
  307. raise R2RException(
  308. "Only a superuser can export data.",
  309. 403,
  310. )
  311. (
  312. csv_file_path,
  313. temp_file,
  314. ) = await self.services.management.export_messages(
  315. columns=columns,
  316. filters=filters,
  317. include_header=include_header
  318. if include_header is not None
  319. else True,
  320. )
  321. background_tasks.add_task(temp_file.close)
  322. return FileResponse(
  323. path=csv_file_path,
  324. media_type="text/csv",
  325. filename="documents_export.csv",
  326. )
  327. @self.router.get(
  328. "/conversations/{id}",
  329. summary="Get conversation details",
  330. dependencies=[Depends(self.rate_limit_dependency)],
  331. openapi_extra={
  332. "x-codeSamples": [
  333. {
  334. "lang": "Python",
  335. "source": textwrap.dedent("""
  336. from r2r import R2RClient
  337. client = R2RClient()
  338. # when using auth, do client.login(...)
  339. result = client.conversations.get(
  340. "123e4567-e89b-12d3-a456-426614174000"
  341. )
  342. """),
  343. },
  344. {
  345. "lang": "JavaScript",
  346. "source": textwrap.dedent("""
  347. const { r2rClient } = require("r2r-js");
  348. const client = new r2rClient();
  349. function main() {
  350. const response = await client.conversations.retrieve({
  351. id: "123e4567-e89b-12d3-a456-426614174000",
  352. });
  353. }
  354. main();
  355. """),
  356. },
  357. {
  358. "lang": "cURL",
  359. "source": textwrap.dedent("""
  360. curl -X GET "https://api.example.com/v3/conversations/123e4567-e89b-12d3-a456-426614174000" \\
  361. -H "Authorization: Bearer YOUR_API_KEY"
  362. """),
  363. },
  364. ]
  365. },
  366. )
  367. @self.base_endpoint
  368. async def get_conversation(
  369. id: UUID = Path(
  370. ..., description="The unique identifier of the conversation"
  371. ),
  372. auth_user=Depends(self.providers.auth.auth_wrapper()),
  373. ) -> WrappedConversationMessagesResponse:
  374. """Get details of a specific conversation.
  375. This endpoint retrieves detailed information about a single
  376. conversation identified by its UUID.
  377. """
  378. requesting_user_id = (
  379. None if auth_user.is_superuser else [auth_user.id]
  380. )
  381. conversation = await self.services.management.get_conversation(
  382. conversation_id=id,
  383. user_ids=requesting_user_id,
  384. )
  385. return conversation # type: ignore
  386. @self.router.post(
  387. "/conversations/{id}",
  388. summary="Update conversation",
  389. dependencies=[Depends(self.rate_limit_dependency)],
  390. openapi_extra={
  391. "x-codeSamples": [
  392. {
  393. "lang": "Python",
  394. "source": textwrap.dedent("""
  395. from r2r import R2RClient
  396. client = R2RClient()
  397. # when using auth, do client.login(...)
  398. result = client.conversations.update("123e4567-e89b-12d3-a456-426614174000", "new_name")
  399. """),
  400. },
  401. {
  402. "lang": "JavaScript",
  403. "source": textwrap.dedent("""
  404. const { r2rClient } = require("r2r-js");
  405. const client = new r2rClient();
  406. function main() {
  407. const response = await client.conversations.update({
  408. id: "123e4567-e89b-12d3-a456-426614174000",
  409. name: "new_name",
  410. });
  411. }
  412. main();
  413. """),
  414. },
  415. {
  416. "lang": "cURL",
  417. "source": textwrap.dedent("""
  418. curl -X POST "https://api.example.com/v3/conversations/123e4567-e89b-12d3-a456-426614174000" \
  419. -H "Authorization: Bearer YOUR_API_KEY" \
  420. -H "Content-Type: application/json" \
  421. -d '{"name": "new_name"}'
  422. """),
  423. },
  424. ]
  425. },
  426. )
  427. @self.base_endpoint
  428. async def update_conversation(
  429. id: UUID = Path(
  430. ...,
  431. description="The unique identifier of the conversation to delete",
  432. ),
  433. name: str = Body(
  434. ...,
  435. description="The updated name for the conversation",
  436. embed=True,
  437. ),
  438. auth_user=Depends(self.providers.auth.auth_wrapper()),
  439. ) -> WrappedConversationResponse:
  440. """Update an existing conversation.
  441. This endpoint updates the name of an existing conversation
  442. identified by its UUID.
  443. """
  444. return await self.services.management.update_conversation( # type: ignore
  445. conversation_id=id,
  446. name=name,
  447. )
  448. @self.router.delete(
  449. "/conversations/{id}",
  450. summary="Delete conversation",
  451. dependencies=[Depends(self.rate_limit_dependency)],
  452. openapi_extra={
  453. "x-codeSamples": [
  454. {
  455. "lang": "Python",
  456. "source": textwrap.dedent("""
  457. from r2r import R2RClient
  458. client = R2RClient()
  459. # when using auth, do client.login(...)
  460. result = client.conversations.delete("123e4567-e89b-12d3-a456-426614174000")
  461. """),
  462. },
  463. {
  464. "lang": "JavaScript",
  465. "source": textwrap.dedent("""
  466. const { r2rClient } = require("r2r-js");
  467. const client = new r2rClient();
  468. function main() {
  469. const response = await client.conversations.delete({
  470. id: "123e4567-e89b-12d3-a456-426614174000",
  471. });
  472. }
  473. main();
  474. """),
  475. },
  476. {
  477. "lang": "cURL",
  478. "source": textwrap.dedent("""
  479. curl -X DELETE "https://api.example.com/v3/conversations/123e4567-e89b-12d3-a456-426614174000" \\
  480. -H "Authorization: Bearer YOUR_API_KEY"
  481. """),
  482. },
  483. ]
  484. },
  485. )
  486. @self.base_endpoint
  487. async def delete_conversation(
  488. id: UUID = Path(
  489. ...,
  490. description="The unique identifier of the conversation to delete",
  491. ),
  492. auth_user=Depends(self.providers.auth.auth_wrapper()),
  493. ) -> WrappedBooleanResponse:
  494. """Delete an existing conversation.
  495. This endpoint deletes a conversation identified by its UUID.
  496. """
  497. requesting_user_id = (
  498. None if auth_user.is_superuser else [auth_user.id]
  499. )
  500. await self.services.management.delete_conversation(
  501. conversation_id=id,
  502. user_ids=requesting_user_id,
  503. )
  504. return GenericBooleanResponse(success=True) # type: ignore
  505. @self.router.post(
  506. "/conversations/{id}/messages",
  507. summary="Add message to conversation",
  508. dependencies=[Depends(self.rate_limit_dependency)],
  509. openapi_extra={
  510. "x-codeSamples": [
  511. {
  512. "lang": "Python",
  513. "source": textwrap.dedent("""
  514. from r2r import R2RClient
  515. client = R2RClient()
  516. # when using auth, do client.login(...)
  517. result = client.conversations.add_message(
  518. "123e4567-e89b-12d3-a456-426614174000",
  519. content="Hello, world!",
  520. role="user",
  521. parent_id="parent_message_id",
  522. metadata={"key": "value"}
  523. )
  524. """),
  525. },
  526. {
  527. "lang": "JavaScript",
  528. "source": textwrap.dedent("""
  529. const { r2rClient } = require("r2r-js");
  530. const client = new r2rClient();
  531. function main() {
  532. const response = await client.conversations.addMessage({
  533. id: "123e4567-e89b-12d3-a456-426614174000",
  534. content: "Hello, world!",
  535. role: "user",
  536. parentId: "parent_message_id",
  537. });
  538. }
  539. main();
  540. """),
  541. },
  542. {
  543. "lang": "cURL",
  544. "source": textwrap.dedent("""
  545. curl -X POST "https://api.example.com/v3/conversations/123e4567-e89b-12d3-a456-426614174000/messages" \\
  546. -H "Authorization: Bearer YOUR_API_KEY" \\
  547. -H "Content-Type: application/json" \\
  548. -d '{"content": "Hello, world!", "parent_id": "parent_message_id", "metadata": {"key": "value"}}'
  549. """),
  550. },
  551. ]
  552. },
  553. )
  554. @self.base_endpoint
  555. async def add_message(
  556. id: UUID = Path(
  557. ..., description="The unique identifier of the conversation"
  558. ),
  559. content: str = Body(
  560. ..., description="The content of the message to add"
  561. ),
  562. role: str = Body(
  563. ..., description="The role of the message to add"
  564. ),
  565. parent_id: Optional[UUID] = Body(
  566. None, description="The ID of the parent message, if any"
  567. ),
  568. metadata: Optional[dict[str, str]] = Body(
  569. None, description="Additional metadata for the message"
  570. ),
  571. auth_user=Depends(self.providers.auth.auth_wrapper()),
  572. ) -> WrappedMessageResponse:
  573. """Add a new message to a conversation.
  574. This endpoint adds a new message to an existing conversation.
  575. """
  576. if content == "":
  577. raise R2RException("Content cannot be empty", status_code=400)
  578. if role not in ["user", "assistant", "system"]:
  579. raise R2RException("Invalid role", status_code=400)
  580. message = Message(role=role, content=content)
  581. return await self.services.management.add_message( # type: ignore
  582. conversation_id=id,
  583. content=message,
  584. parent_id=parent_id,
  585. metadata=metadata,
  586. )
  587. @self.router.post(
  588. "/conversations/{id}/messages/{message_id}",
  589. summary="Update message in conversation",
  590. dependencies=[Depends(self.rate_limit_dependency)],
  591. openapi_extra={
  592. "x-codeSamples": [
  593. {
  594. "lang": "Python",
  595. "source": textwrap.dedent("""
  596. from r2r import R2RClient
  597. client = R2RClient()
  598. # when using auth, do client.login(...)
  599. result = client.conversations.update_message(
  600. "123e4567-e89b-12d3-a456-426614174000",
  601. "message_id_to_update",
  602. content="Updated content"
  603. )
  604. """),
  605. },
  606. {
  607. "lang": "JavaScript",
  608. "source": textwrap.dedent("""
  609. const { r2rClient } = require("r2r-js");
  610. const client = new r2rClient();
  611. function main() {
  612. const response = await client.conversations.updateMessage({
  613. id: "123e4567-e89b-12d3-a456-426614174000",
  614. messageId: "message_id_to_update",
  615. content: "Updated content",
  616. });
  617. }
  618. main();
  619. """),
  620. },
  621. {
  622. "lang": "cURL",
  623. "source": textwrap.dedent("""
  624. curl -X POST "https://api.example.com/v3/conversations/123e4567-e89b-12d3-a456-426614174000/messages/message_id_to_update" \\
  625. -H "Authorization: Bearer YOUR_API_KEY" \\
  626. -H "Content-Type: application/json" \\
  627. -d '{"content": "Updated content"}'
  628. """),
  629. },
  630. ]
  631. },
  632. )
  633. @self.base_endpoint
  634. async def update_message(
  635. id: UUID = Path(
  636. ..., description="The unique identifier of the conversation"
  637. ),
  638. message_id: UUID = Path(
  639. ..., description="The ID of the message to update"
  640. ),
  641. content: Optional[str] = Body(
  642. None, description="The new content for the message"
  643. ),
  644. metadata: Optional[dict[str, str]] = Body(
  645. None, description="Additional metadata for the message"
  646. ),
  647. auth_user=Depends(self.providers.auth.auth_wrapper()),
  648. ) -> WrappedMessageResponse:
  649. """Update an existing message in a conversation.
  650. This endpoint updates the content of an existing message in a
  651. conversation.
  652. """
  653. return await self.services.management.edit_message( # type: ignore
  654. message_id=message_id,
  655. new_content=content,
  656. additional_metadata=metadata,
  657. )