conversations_router.py 23 KB

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