graph_router.py 81 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051
  1. import logging
  2. import textwrap
  3. from typing import Optional, cast
  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 GraphConstructionStatus, R2RException, Workflow
  9. from core.base.abstractions import DocumentResponse, StoreType
  10. from core.base.api.models import (
  11. GenericBooleanResponse,
  12. GenericMessageResponse,
  13. WrappedBooleanResponse,
  14. WrappedCommunitiesResponse,
  15. WrappedCommunityResponse,
  16. WrappedEntitiesResponse,
  17. WrappedEntityResponse,
  18. WrappedGenericMessageResponse,
  19. WrappedGraphResponse,
  20. WrappedGraphsResponse,
  21. WrappedRelationshipResponse,
  22. WrappedRelationshipsResponse,
  23. )
  24. from core.utils import (
  25. generate_default_user_collection_id,
  26. update_settings_from_dict,
  27. )
  28. from ...abstractions import R2RProviders, R2RServices
  29. from ...config import R2RConfig
  30. from .base_router import BaseRouterV3
  31. logger = logging.getLogger()
  32. class GraphRouter(BaseRouterV3):
  33. def __init__(
  34. self,
  35. providers: R2RProviders,
  36. services: R2RServices,
  37. config: R2RConfig,
  38. ):
  39. logging.info("Initializing GraphRouter")
  40. super().__init__(providers, services, config)
  41. self._register_workflows()
  42. def _register_workflows(self):
  43. workflow_messages = {}
  44. if self.providers.orchestration.config.provider == "hatchet":
  45. workflow_messages["graph-extraction"] = (
  46. "Document extraction task queued successfully."
  47. )
  48. workflow_messages["graph-clustering"] = (
  49. "Graph enrichment task queued successfully."
  50. )
  51. workflow_messages["graph-deduplication"] = (
  52. "Entity deduplication task queued successfully."
  53. )
  54. else:
  55. workflow_messages["graph-extraction"] = (
  56. "Document entities and relationships extracted successfully."
  57. )
  58. workflow_messages["graph-clustering"] = (
  59. "Graph communities created successfully."
  60. )
  61. workflow_messages["graph-deduplication"] = (
  62. "Entity deduplication completed successfully."
  63. )
  64. self.providers.orchestration.register_workflows(
  65. Workflow.GRAPH,
  66. self.services.graph,
  67. workflow_messages,
  68. )
  69. async def _get_collection_id(
  70. self, collection_id: Optional[UUID], auth_user
  71. ) -> UUID:
  72. """Helper method to get collection ID, using default if none
  73. provided."""
  74. if collection_id is None:
  75. return generate_default_user_collection_id(auth_user.id)
  76. return collection_id
  77. def _setup_routes(self):
  78. @self.router.get(
  79. "/graphs",
  80. dependencies=[Depends(self.rate_limit_dependency)],
  81. summary="List graphs",
  82. openapi_extra={
  83. "x-codeSamples": [
  84. { # TODO: Verify
  85. "lang": "Python",
  86. "source": textwrap.dedent(
  87. """
  88. from r2r import R2RClient
  89. client = R2RClient()
  90. # when using auth, do client.login(...)
  91. response = client.graphs.list()
  92. """
  93. ),
  94. },
  95. {
  96. "lang": "JavaScript",
  97. "source": textwrap.dedent(
  98. """
  99. const { r2rClient } = require("r2r-js");
  100. const client = new r2rClient();
  101. function main() {
  102. const response = await client.graphs.list({});
  103. }
  104. main();
  105. """
  106. ),
  107. },
  108. ]
  109. },
  110. )
  111. @self.base_endpoint
  112. async def list_graphs(
  113. collection_ids: list[str] = Query(
  114. [],
  115. description="A list of graph IDs to retrieve. If not provided, all graphs will be returned.",
  116. ),
  117. offset: int = Query(
  118. 0,
  119. ge=0,
  120. description="Specifies the number of objects to skip. Defaults to 0.",
  121. ),
  122. limit: int = Query(
  123. 100,
  124. ge=1,
  125. le=1000,
  126. description="Specifies a limit on the number of objects to return, ranging between 1 and 100. Defaults to 100.",
  127. ),
  128. auth_user=Depends(self.providers.auth.auth_wrapper()),
  129. ) -> WrappedGraphsResponse:
  130. """Returns a paginated list of graphs the authenticated user has
  131. access to.
  132. Results can be filtered by providing specific graph IDs. Regular
  133. users will only see graphs they own or have access to. Superusers
  134. can see all graphs.
  135. The graphs are returned in order of last modification, with most
  136. recent first.
  137. """
  138. requesting_user_id = (
  139. None if auth_user.is_superuser else [auth_user.id]
  140. )
  141. graph_uuids = [UUID(graph_id) for graph_id in collection_ids]
  142. list_graphs_response = await self.services.graph.list_graphs(
  143. # user_ids=requesting_user_id,
  144. graph_ids=graph_uuids,
  145. offset=offset,
  146. limit=limit,
  147. )
  148. return ( # type: ignore
  149. list_graphs_response["results"],
  150. {"total_entries": list_graphs_response["total_entries"]},
  151. )
  152. @self.router.get(
  153. "/graphs/{collection_id}",
  154. dependencies=[Depends(self.rate_limit_dependency)],
  155. summary="Retrieve graph details",
  156. openapi_extra={
  157. "x-codeSamples": [
  158. {
  159. "lang": "Python",
  160. "source": textwrap.dedent("""
  161. from r2r import R2RClient
  162. client = R2RClient()
  163. # when using auth, do client.login(...)
  164. response = client.graphs.get(
  165. collection_id="d09dedb1-b2ab-48a5-b950-6e1f464d83e7"
  166. )"""),
  167. },
  168. {
  169. "lang": "JavaScript",
  170. "source": textwrap.dedent("""
  171. const { r2rClient } = require("r2r-js");
  172. const client = new r2rClient();
  173. function main() {
  174. const response = await client.graphs.retrieve({
  175. collectionId: "d09dedb1-b2ab-48a5-b950-6e1f464d83e7"
  176. });
  177. }
  178. main();
  179. """),
  180. },
  181. {
  182. "lang": "cURL",
  183. "source": textwrap.dedent("""
  184. curl -X GET "https://api.example.com/v3/graphs/d09dedb1-b2ab-48a5-b950-6e1f464d83e7" \\
  185. -H "Authorization: Bearer YOUR_API_KEY" """),
  186. },
  187. ]
  188. },
  189. )
  190. @self.base_endpoint
  191. async def get_graph(
  192. collection_id: UUID = Path(...),
  193. auth_user=Depends(self.providers.auth.auth_wrapper()),
  194. ) -> WrappedGraphResponse:
  195. """Retrieves detailed information about a specific graph by ID."""
  196. if (
  197. # not auth_user.is_superuser
  198. collection_id not in auth_user.collection_ids
  199. ):
  200. raise R2RException(
  201. "The currently authenticated user does not have access to the specified collection associated with the given graph.",
  202. 403,
  203. )
  204. list_graphs_response = await self.services.graph.list_graphs(
  205. # user_ids=None,
  206. graph_ids=[collection_id],
  207. offset=0,
  208. limit=1,
  209. )
  210. return list_graphs_response["results"][0] # type: ignore
  211. @self.router.post(
  212. "/graphs/{collection_id}/communities/build",
  213. dependencies=[Depends(self.rate_limit_dependency)],
  214. )
  215. @self.base_endpoint
  216. async def build_communities(
  217. collection_id: UUID = Path(
  218. ..., description="The unique identifier of the collection"
  219. ),
  220. graph_enrichment_settings: Optional[dict] = Body(
  221. default=None,
  222. description="Settings for the graph enrichment process.",
  223. ),
  224. run_with_orchestration: Optional[bool] = Body(True),
  225. auth_user=Depends(self.providers.auth.auth_wrapper()),
  226. ) -> WrappedGenericMessageResponse:
  227. """Creates communities in the graph by analyzing entity
  228. relationships and similarities.
  229. Communities are created through the following process:
  230. 1. Analyzes entity relationships and metadata to build a similarity graph
  231. 2. Applies advanced community detection algorithms (e.g. Leiden) to identify densely connected groups
  232. 3. Creates hierarchical community structure with multiple granularity levels
  233. 4. Generates natural language summaries and statistical insights for each community
  234. The resulting communities can be used to:
  235. - Understand high-level graph structure and organization
  236. - Identify key entity groupings and their relationships
  237. - Navigate and explore the graph at different levels of detail
  238. - Generate insights about entity clusters and their characteristics
  239. The community detection process is configurable through settings like:
  240. - Community detection algorithm parameters
  241. - Summary generation prompt
  242. """
  243. collections_overview_response = (
  244. await self.services.management.collections_overview(
  245. user_ids=[auth_user.id],
  246. collection_ids=[collection_id],
  247. offset=0,
  248. limit=1,
  249. )
  250. )["results"]
  251. if len(collections_overview_response) == 0: # type: ignore
  252. raise R2RException("Collection not found.", 404)
  253. # Check user permissions for graph
  254. if (
  255. not auth_user.is_superuser
  256. and collections_overview_response[0].owner_id != auth_user.id # type: ignore
  257. ):
  258. raise R2RException(
  259. "Only superusers can `build communities` for a graph they do not own.",
  260. 403,
  261. )
  262. # If no collection ID is provided, use the default user collection
  263. # id = generate_default_user_collection_id(auth_user.id)
  264. # Apply runtime settings overrides
  265. server_graph_enrichment_settings = (
  266. self.providers.database.config.graph_enrichment_settings
  267. )
  268. if graph_enrichment_settings:
  269. server_graph_enrichment_settings = update_settings_from_dict(
  270. server_graph_enrichment_settings, graph_enrichment_settings
  271. )
  272. workflow_input = {
  273. "collection_id": str(collection_id),
  274. "graph_enrichment_settings": server_graph_enrichment_settings.model_dump_json(),
  275. "user": auth_user.json(),
  276. }
  277. if run_with_orchestration:
  278. try:
  279. return await self.providers.orchestration.run_workflow( # type: ignore
  280. "graph-clustering", {"request": workflow_input}, {}
  281. )
  282. return GenericMessageResponse(
  283. message="Graph communities created successfully."
  284. ) # type: ignore
  285. except Exception as e: # TODO: Need to find specific error (gRPC most likely?)
  286. logger.error(
  287. f"Error running orchestrated community building: {e} \n\nAttempting to run without orchestration."
  288. )
  289. from core.main.orchestration import (
  290. simple_graph_search_results_factory,
  291. )
  292. logger.info("Running build-communities without orchestration.")
  293. simple_graph_search_results = simple_graph_search_results_factory(
  294. self.services.graph
  295. )
  296. await simple_graph_search_results["graph-clustering"](
  297. workflow_input
  298. )
  299. return { # type: ignore
  300. "message": "Graph communities created successfully.",
  301. "task_id": None,
  302. }
  303. @self.router.post(
  304. "/graphs/{collection_id}/reset",
  305. dependencies=[Depends(self.rate_limit_dependency)],
  306. summary="Reset a graph back to the initial state.",
  307. openapi_extra={
  308. "x-codeSamples": [
  309. {
  310. "lang": "Python",
  311. "source": textwrap.dedent("""
  312. from r2r import R2RClient
  313. client = R2RClient()
  314. # when using auth, do client.login(...)
  315. response = client.graphs.reset(
  316. collection_id="d09dedb1-b2ab-48a5-b950-6e1f464d83e7",
  317. )"""),
  318. },
  319. {
  320. "lang": "JavaScript",
  321. "source": textwrap.dedent("""
  322. const { r2rClient } = require("r2r-js");
  323. const client = new r2rClient();
  324. function main() {
  325. const response = await client.graphs.reset({
  326. collectionId: "d09dedb1-b2ab-48a5-b950-6e1f464d83e7"
  327. });
  328. }
  329. main();
  330. """),
  331. },
  332. {
  333. "lang": "cURL",
  334. "source": textwrap.dedent("""
  335. curl -X POST "https://api.example.com/v3/graphs/d09dedb1-b2ab-48a5-b950-6e1f464d83e7/reset" \\
  336. -H "Authorization: Bearer YOUR_API_KEY" """),
  337. },
  338. ]
  339. },
  340. )
  341. @self.base_endpoint
  342. async def reset(
  343. collection_id: UUID = Path(...),
  344. auth_user=Depends(self.providers.auth.auth_wrapper()),
  345. ) -> WrappedBooleanResponse:
  346. """Deletes a graph and all its associated data.
  347. This endpoint permanently removes the specified graph along with
  348. all entities and relationships that belong to only this graph. The
  349. original source entities and relationships extracted from
  350. underlying documents are not deleted and are managed through the
  351. document lifecycle.
  352. """
  353. if not auth_user.is_superuser:
  354. raise R2RException("Only superusers can reset a graph", 403)
  355. if (
  356. # not auth_user.is_superuser
  357. collection_id not in auth_user.collection_ids
  358. ):
  359. raise R2RException(
  360. "The currently authenticated user does not have access to the collection associated with the given graph.",
  361. 403,
  362. )
  363. await self.services.graph.reset_graph(id=collection_id)
  364. # await _pull(collection_id, auth_user)
  365. return GenericBooleanResponse(success=True) # type: ignore
  366. # update graph
  367. @self.router.post(
  368. "/graphs/{collection_id}",
  369. dependencies=[Depends(self.rate_limit_dependency)],
  370. summary="Update graph",
  371. openapi_extra={
  372. "x-codeSamples": [
  373. {
  374. "lang": "Python",
  375. "source": textwrap.dedent("""
  376. from r2r import R2RClient
  377. client = R2RClient()
  378. # when using auth, do client.login(...)
  379. response = client.graphs.update(
  380. collection_id="d09dedb1-b2ab-48a5-b950-6e1f464d83e7",
  381. graph={
  382. "name": "New Name",
  383. "description": "New Description"
  384. }
  385. )"""),
  386. },
  387. {
  388. "lang": "JavaScript",
  389. "source": textwrap.dedent("""
  390. const { r2rClient } = require("r2r-js");
  391. const client = new r2rClient();
  392. function main() {
  393. const response = await client.graphs.update({
  394. collection_id: "d09dedb1-b2ab-48a5-b950-6e1f464d83e7",
  395. name: "New Name",
  396. description: "New Description",
  397. });
  398. }
  399. main();
  400. """),
  401. },
  402. ]
  403. },
  404. )
  405. @self.base_endpoint
  406. async def update_graph(
  407. collection_id: UUID = Path(
  408. ...,
  409. description="The collection ID corresponding to the graph to update",
  410. ),
  411. name: Optional[str] = Body(
  412. None, description="The name of the graph"
  413. ),
  414. description: Optional[str] = Body(
  415. None, description="An optional description of the graph"
  416. ),
  417. auth_user=Depends(self.providers.auth.auth_wrapper()),
  418. ) -> WrappedGraphResponse:
  419. """Update an existing graphs's configuration.
  420. This endpoint allows updating the name and description of an
  421. existing collection. The user must have appropriate permissions to
  422. modify the collection.
  423. """
  424. if not auth_user.is_superuser:
  425. raise R2RException(
  426. "Only superusers can update graph details", 403
  427. )
  428. if (
  429. not auth_user.is_superuser
  430. and id not in auth_user.collection_ids
  431. ):
  432. raise R2RException(
  433. "The currently authenticated user does not have access to the collection associated with the given graph.",
  434. 403,
  435. )
  436. return await self.services.graph.update_graph( # type: ignore
  437. collection_id,
  438. name=name,
  439. description=description,
  440. )
  441. @self.router.get(
  442. "/graphs/{collection_id}/entities",
  443. dependencies=[Depends(self.rate_limit_dependency)],
  444. openapi_extra={
  445. "x-codeSamples": [
  446. {
  447. "lang": "Python",
  448. "source": textwrap.dedent("""
  449. from r2r import R2RClient
  450. client = R2RClient()
  451. # when using auth, do client.login(...)
  452. response = client.graphs.list_entities(collection_id="d09dedb1-b2ab-48a5-b950-6e1f464d83e7")
  453. """),
  454. },
  455. {
  456. "lang": "JavaScript",
  457. "source": textwrap.dedent("""
  458. const { r2rClient } = require("r2r-js");
  459. const client = new r2rClient();
  460. function main() {
  461. const response = await client.graphs.listEntities({
  462. collection_id: "d09dedb1-b2ab-48a5-b950-6e1f464d83e7",
  463. });
  464. }
  465. main();
  466. """),
  467. },
  468. ],
  469. },
  470. )
  471. @self.base_endpoint
  472. async def get_entities(
  473. collection_id: UUID = Path(
  474. ...,
  475. description="The collection ID corresponding to the graph to list entities from.",
  476. ),
  477. offset: int = Query(
  478. 0,
  479. ge=0,
  480. description="Specifies the number of objects to skip. Defaults to 0.",
  481. ),
  482. limit: int = Query(
  483. 100,
  484. ge=1,
  485. le=1000,
  486. description="Specifies a limit on the number of objects to return, ranging between 1 and 100. Defaults to 100.",
  487. ),
  488. auth_user=Depends(self.providers.auth.auth_wrapper()),
  489. ) -> WrappedEntitiesResponse:
  490. """Lists all entities in the graph with pagination support."""
  491. if (
  492. # not auth_user.is_superuser
  493. collection_id not in auth_user.collection_ids
  494. ):
  495. raise R2RException(
  496. "The currently authenticated user does not have access to the collection associated with the given graph.",
  497. 403,
  498. )
  499. entities, count = await self.services.graph.get_entities(
  500. parent_id=collection_id,
  501. offset=offset,
  502. limit=limit,
  503. )
  504. return entities, { # type: ignore
  505. "total_entries": count,
  506. }
  507. @self.router.post(
  508. "/graphs/{collection_id}/entities/export",
  509. summary="Export graph entities to CSV",
  510. dependencies=[Depends(self.rate_limit_dependency)],
  511. openapi_extra={
  512. "x-codeSamples": [
  513. {
  514. "lang": "Python",
  515. "source": textwrap.dedent("""
  516. from r2r import R2RClient
  517. client = R2RClient("http://localhost:7272")
  518. # when using auth, do client.login(...)
  519. response = client.graphs.export_entities(
  520. collection_id="b4ac4dd6-5f27-596e-a55b-7cf242ca30aa",
  521. output_path="export.csv",
  522. columns=["id", "title", "created_at"],
  523. include_header=True,
  524. )
  525. """),
  526. },
  527. {
  528. "lang": "JavaScript",
  529. "source": textwrap.dedent("""
  530. const { r2rClient } = require("r2r-js");
  531. const client = new r2rClient("http://localhost:7272");
  532. function main() {
  533. await client.graphs.exportEntities({
  534. collectionId: "b4ac4dd6-5f27-596e-a55b-7cf242ca30aa",
  535. outputPath: "export.csv",
  536. columns: ["id", "title", "created_at"],
  537. includeHeader: true,
  538. });
  539. }
  540. main();
  541. """),
  542. },
  543. {
  544. "lang": "cURL",
  545. "source": textwrap.dedent("""
  546. curl -X POST "http://127.0.0.1:7272/v3/graphs/export_entities" \
  547. -H "Authorization: Bearer YOUR_API_KEY" \
  548. -H "Content-Type: application/json" \
  549. -H "Accept: text/csv" \
  550. -d '{ "columns": ["id", "title", "created_at"], "include_header": true }' \
  551. --output export.csv
  552. """),
  553. },
  554. ]
  555. },
  556. )
  557. @self.base_endpoint
  558. async def export_entities(
  559. background_tasks: BackgroundTasks,
  560. collection_id: UUID = Path(
  561. ...,
  562. description="The ID of the collection to export entities from.",
  563. ),
  564. columns: Optional[list[str]] = Body(
  565. None, description="Specific columns to export"
  566. ),
  567. filters: Optional[dict] = Body(
  568. None, description="Filters to apply to the export"
  569. ),
  570. include_header: Optional[bool] = Body(
  571. True, description="Whether to include column headers"
  572. ),
  573. auth_user=Depends(self.providers.auth.auth_wrapper()),
  574. ) -> FileResponse:
  575. """Export documents as a downloadable CSV file."""
  576. if not auth_user.is_superuser:
  577. raise R2RException(
  578. "Only a superuser can export data.",
  579. 403,
  580. )
  581. (
  582. csv_file_path,
  583. temp_file,
  584. ) = await self.services.management.export_graph_entities(
  585. id=collection_id,
  586. columns=columns,
  587. filters=filters,
  588. include_header=include_header
  589. if include_header is not None
  590. else True,
  591. )
  592. background_tasks.add_task(temp_file.close)
  593. return FileResponse(
  594. path=csv_file_path,
  595. media_type="text/csv",
  596. filename="documents_export.csv",
  597. )
  598. @self.router.post(
  599. "/graphs/{collection_id}/entities",
  600. dependencies=[Depends(self.rate_limit_dependency)],
  601. )
  602. @self.base_endpoint
  603. async def create_entity(
  604. collection_id: UUID = Path(
  605. ...,
  606. description="The collection ID corresponding to the graph to add the entity to.",
  607. ),
  608. name: str = Body(
  609. ..., description="The name of the entity to create."
  610. ),
  611. description: str = Body(
  612. ..., description="The description of the entity to create."
  613. ),
  614. category: Optional[str] = Body(
  615. None, description="The category of the entity to create."
  616. ),
  617. metadata: Optional[dict] = Body(
  618. None, description="The metadata of the entity to create."
  619. ),
  620. auth_user=Depends(self.providers.auth.auth_wrapper()),
  621. ) -> WrappedEntityResponse:
  622. """Creates a new entity in the graph."""
  623. if (
  624. # not auth_user.is_superuser
  625. collection_id not in auth_user.collection_ids
  626. ):
  627. raise R2RException(
  628. "The currently authenticated user does not have access to the collection associated with the given graph.",
  629. 403,
  630. )
  631. return await self.services.graph.create_entity( # type: ignore
  632. name=name,
  633. description=description,
  634. parent_id=collection_id,
  635. category=category,
  636. metadata=metadata,
  637. )
  638. @self.router.post(
  639. "/graphs/{collection_id}/relationships",
  640. dependencies=[Depends(self.rate_limit_dependency)],
  641. )
  642. @self.base_endpoint
  643. async def create_relationship(
  644. collection_id: UUID = Path(
  645. ...,
  646. description="The collection ID corresponding to the graph to add the relationship to.",
  647. ),
  648. subject: str = Body(
  649. ..., description="The subject of the relationship to create."
  650. ),
  651. subject_id: UUID = Body(
  652. ...,
  653. description="The ID of the subject of the relationship to create.",
  654. ),
  655. predicate: str = Body(
  656. ..., description="The predicate of the relationship to create."
  657. ),
  658. object: str = Body(
  659. ..., description="The object of the relationship to create."
  660. ),
  661. object_id: UUID = Body(
  662. ...,
  663. description="The ID of the object of the relationship to create.",
  664. ),
  665. description: str = Body(
  666. ...,
  667. description="The description of the relationship to create.",
  668. ),
  669. weight: float = Body(
  670. 1.0, description="The weight of the relationship to create."
  671. ),
  672. metadata: Optional[dict] = Body(
  673. None, description="The metadata of the relationship to create."
  674. ),
  675. auth_user=Depends(self.providers.auth.auth_wrapper()),
  676. ) -> WrappedRelationshipResponse:
  677. """Creates a new relationship in the graph."""
  678. if not auth_user.is_superuser:
  679. raise R2RException(
  680. "Only superusers can create relationships.", 403
  681. )
  682. if (
  683. # not auth_user.is_superuser
  684. collection_id not in auth_user.collection_ids
  685. ):
  686. raise R2RException(
  687. "The currently authenticated user does not have access to the collection associated with the given graph.",
  688. 403,
  689. )
  690. return await self.services.graph.create_relationship( # type: ignore
  691. subject=subject,
  692. subject_id=subject_id,
  693. predicate=predicate,
  694. object=object,
  695. object_id=object_id,
  696. description=description,
  697. weight=weight,
  698. metadata=metadata,
  699. parent_id=collection_id,
  700. )
  701. @self.router.post(
  702. "/graphs/{collection_id}/relationships/export",
  703. summary="Export graph relationships to CSV",
  704. dependencies=[Depends(self.rate_limit_dependency)],
  705. openapi_extra={
  706. "x-codeSamples": [
  707. {
  708. "lang": "Python",
  709. "source": textwrap.dedent("""
  710. from r2r import R2RClient
  711. client = R2RClient("http://localhost:7272")
  712. # when using auth, do client.login(...)
  713. response = client.graphs.export_entities(
  714. collection_id="b4ac4dd6-5f27-596e-a55b-7cf242ca30aa",
  715. output_path="export.csv",
  716. columns=["id", "title", "created_at"],
  717. include_header=True,
  718. )
  719. """),
  720. },
  721. {
  722. "lang": "JavaScript",
  723. "source": textwrap.dedent("""
  724. const { r2rClient } = require("r2r-js");
  725. const client = new r2rClient("http://localhost:7272");
  726. function main() {
  727. await client.graphs.exportEntities({
  728. collectionId: "b4ac4dd6-5f27-596e-a55b-7cf242ca30aa",
  729. outputPath: "export.csv",
  730. columns: ["id", "title", "created_at"],
  731. includeHeader: true,
  732. });
  733. }
  734. main();
  735. """),
  736. },
  737. {
  738. "lang": "cURL",
  739. "source": textwrap.dedent("""
  740. curl -X POST "http://127.0.0.1:7272/v3/graphs/export_relationships" \
  741. -H "Authorization: Bearer YOUR_API_KEY" \
  742. -H "Content-Type: application/json" \
  743. -H "Accept: text/csv" \
  744. -d '{ "columns": ["id", "title", "created_at"], "include_header": true }' \
  745. --output export.csv
  746. """),
  747. },
  748. ]
  749. },
  750. )
  751. @self.base_endpoint
  752. async def export_relationships(
  753. background_tasks: BackgroundTasks,
  754. collection_id: UUID = Path(
  755. ...,
  756. description="The ID of the document to export entities from.",
  757. ),
  758. columns: Optional[list[str]] = Body(
  759. None, description="Specific columns to export"
  760. ),
  761. filters: Optional[dict] = Body(
  762. None, description="Filters to apply to the export"
  763. ),
  764. include_header: Optional[bool] = Body(
  765. True, description="Whether to include column headers"
  766. ),
  767. auth_user=Depends(self.providers.auth.auth_wrapper()),
  768. ) -> FileResponse:
  769. """Export documents as a downloadable CSV file."""
  770. if not auth_user.is_superuser:
  771. raise R2RException(
  772. "Only a superuser can export data.",
  773. 403,
  774. )
  775. (
  776. csv_file_path,
  777. temp_file,
  778. ) = await self.services.management.export_graph_relationships(
  779. id=collection_id,
  780. columns=columns,
  781. filters=filters,
  782. include_header=include_header
  783. if include_header is not None
  784. else True,
  785. )
  786. background_tasks.add_task(temp_file.close)
  787. return FileResponse(
  788. path=csv_file_path,
  789. media_type="text/csv",
  790. filename="documents_export.csv",
  791. )
  792. @self.router.get(
  793. "/graphs/{collection_id}/entities/{entity_id}",
  794. dependencies=[Depends(self.rate_limit_dependency)],
  795. openapi_extra={
  796. "x-codeSamples": [
  797. {
  798. "lang": "Python",
  799. "source": textwrap.dedent("""
  800. from r2r import R2RClient
  801. client = R2RClient()
  802. # when using auth, do client.login(...)
  803. response = client.graphs.get_entity(
  804. collection_id="d09dedb1-b2ab-48a5-b950-6e1f464d83e7",
  805. entity_id="d09dedb1-b2ab-48a5-b950-6e1f464d83e7"
  806. )
  807. """),
  808. },
  809. {
  810. "lang": "JavaScript",
  811. "source": textwrap.dedent("""
  812. const { r2rClient } = require("r2r-js");
  813. const client = new r2rClient();
  814. function main() {
  815. const response = await client.graphs.get_entity({
  816. collectionId: "d09dedb1-b2ab-48a5-b950-6e1f464d83e7",
  817. entityId: "d09dedb1-b2ab-48a5-b950-6e1f464d83e7"
  818. });
  819. }
  820. main();
  821. """),
  822. },
  823. ]
  824. },
  825. )
  826. @self.base_endpoint
  827. async def get_entity(
  828. collection_id: UUID = Path(
  829. ...,
  830. description="The collection ID corresponding to the graph containing the entity.",
  831. ),
  832. entity_id: UUID = Path(
  833. ..., description="The ID of the entity to retrieve."
  834. ),
  835. auth_user=Depends(self.providers.auth.auth_wrapper()),
  836. ) -> WrappedEntityResponse:
  837. """Retrieves a specific entity by its ID."""
  838. if (
  839. # not auth_user.is_superuser
  840. collection_id not in auth_user.collection_ids
  841. ):
  842. raise R2RException(
  843. "The currently authenticated user does not have access to the collection associated with the given graph.",
  844. 403,
  845. )
  846. result = await self.providers.database.graphs_handler.entities.get(
  847. parent_id=collection_id,
  848. store_type=StoreType.GRAPHS,
  849. offset=0,
  850. limit=1,
  851. entity_ids=[entity_id],
  852. )
  853. if len(result) == 0 or len(result[0]) == 0:
  854. raise R2RException("Entity not found", 404)
  855. return result[0][0]
  856. @self.router.post(
  857. "/graphs/{collection_id}/entities/{entity_id}",
  858. dependencies=[Depends(self.rate_limit_dependency)],
  859. )
  860. @self.base_endpoint
  861. async def update_entity(
  862. collection_id: UUID = Path(
  863. ...,
  864. description="The collection ID corresponding to the graph containing the entity.",
  865. ),
  866. entity_id: UUID = Path(
  867. ..., description="The ID of the entity to update."
  868. ),
  869. name: Optional[str] = Body(
  870. ..., description="The updated name of the entity."
  871. ),
  872. description: Optional[str] = Body(
  873. None, description="The updated description of the entity."
  874. ),
  875. category: Optional[str] = Body(
  876. None, description="The updated category of the entity."
  877. ),
  878. metadata: Optional[dict] = Body(
  879. None, description="The updated metadata of the entity."
  880. ),
  881. auth_user=Depends(self.providers.auth.auth_wrapper()),
  882. ) -> WrappedEntityResponse:
  883. """Updates an existing entity in the graph."""
  884. if not auth_user.is_superuser:
  885. raise R2RException(
  886. "Only superusers can update graph entities.", 403
  887. )
  888. if (
  889. # not auth_user.is_superuser
  890. collection_id not in auth_user.collection_ids
  891. ):
  892. raise R2RException(
  893. "The currently authenticated user does not have access to the collection associated with the given graph.",
  894. 403,
  895. )
  896. return await self.services.graph.update_entity( # type: ignore
  897. entity_id=entity_id,
  898. name=name,
  899. category=category,
  900. description=description,
  901. metadata=metadata,
  902. )
  903. @self.router.delete(
  904. "/graphs/{collection_id}/entities/{entity_id}",
  905. dependencies=[Depends(self.rate_limit_dependency)],
  906. summary="Remove an entity",
  907. openapi_extra={
  908. "x-codeSamples": [
  909. {
  910. "lang": "Python",
  911. "source": textwrap.dedent("""
  912. from r2r import R2RClient
  913. client = R2RClient()
  914. # when using auth, do client.login(...)
  915. response = client.graphs.remove_entity(
  916. collection_id="d09dedb1-b2ab-48a5-b950-6e1f464d83e7",
  917. entity_id="d09dedb1-b2ab-48a5-b950-6e1f464d83e7"
  918. )
  919. """),
  920. },
  921. {
  922. "lang": "JavaScript",
  923. "source": textwrap.dedent("""
  924. const { r2rClient } = require("r2r-js");
  925. const client = new r2rClient();
  926. function main() {
  927. const response = await client.graphs.removeEntity({
  928. collectionId: "d09dedb1-b2ab-48a5-b950-6e1f464d83e7",
  929. entityId: "d09dedb1-b2ab-48a5-b950-6e1f464d83e7"
  930. });
  931. }
  932. main();
  933. """),
  934. },
  935. ]
  936. },
  937. )
  938. @self.base_endpoint
  939. async def delete_entity(
  940. collection_id: UUID = Path(
  941. ...,
  942. description="The collection ID corresponding to the graph to remove the entity from.",
  943. ),
  944. entity_id: UUID = Path(
  945. ...,
  946. description="The ID of the entity to remove from the graph.",
  947. ),
  948. auth_user=Depends(self.providers.auth.auth_wrapper()),
  949. ) -> WrappedBooleanResponse:
  950. """Removes an entity from the graph."""
  951. if not auth_user.is_superuser:
  952. raise R2RException(
  953. "Only superusers can delete graph details.", 403
  954. )
  955. if (
  956. # not auth_user.is_superuser
  957. collection_id not in auth_user.collection_ids
  958. ):
  959. raise R2RException(
  960. "The currently authenticated user does not have access to the collection associated with the given graph.",
  961. 403,
  962. )
  963. await self.services.graph.delete_entity(
  964. parent_id=collection_id,
  965. entity_id=entity_id,
  966. )
  967. return GenericBooleanResponse(success=True) # type: ignore
  968. @self.router.get(
  969. "/graphs/{collection_id}/relationships",
  970. dependencies=[Depends(self.rate_limit_dependency)],
  971. description="Lists all relationships in the graph with pagination support.",
  972. openapi_extra={
  973. "x-codeSamples": [
  974. {
  975. "lang": "Python",
  976. "source": textwrap.dedent("""
  977. from r2r import R2RClient
  978. client = R2RClient()
  979. # when using auth, do client.login(...)
  980. response = client.graphs.list_relationships(collection_id="d09dedb1-b2ab-48a5-b950-6e1f464d83e7")
  981. """),
  982. },
  983. {
  984. "lang": "JavaScript",
  985. "source": textwrap.dedent("""
  986. const { r2rClient } = require("r2r-js");
  987. const client = new r2rClient();
  988. function main() {
  989. const response = await client.graphs.listRelationships({
  990. collectionId: "d09dedb1-b2ab-48a5-b950-6e1f464d83e7",
  991. });
  992. }
  993. main();
  994. """),
  995. },
  996. ],
  997. },
  998. )
  999. @self.base_endpoint
  1000. async def get_relationships(
  1001. collection_id: UUID = Path(
  1002. ...,
  1003. description="The collection ID corresponding to the graph to list relationships from.",
  1004. ),
  1005. offset: int = Query(
  1006. 0,
  1007. ge=0,
  1008. description="Specifies the number of objects to skip. Defaults to 0.",
  1009. ),
  1010. limit: int = Query(
  1011. 100,
  1012. ge=1,
  1013. le=1000,
  1014. description="Specifies a limit on the number of objects to return, ranging between 1 and 100. Defaults to 100.",
  1015. ),
  1016. auth_user=Depends(self.providers.auth.auth_wrapper()),
  1017. ) -> WrappedRelationshipsResponse:
  1018. """Lists all relationships in the graph with pagination support."""
  1019. if (
  1020. # not auth_user.is_superuser
  1021. collection_id not in auth_user.collection_ids
  1022. ):
  1023. raise R2RException(
  1024. "The currently authenticated user does not have access to the collection associated with the given graph.",
  1025. 403,
  1026. )
  1027. relationships, count = await self.services.graph.get_relationships(
  1028. parent_id=collection_id,
  1029. offset=offset,
  1030. limit=limit,
  1031. )
  1032. return relationships, { # type: ignore
  1033. "total_entries": count,
  1034. }
  1035. @self.router.get(
  1036. "/graphs/{collection_id}/relationships/{relationship_id}",
  1037. dependencies=[Depends(self.rate_limit_dependency)],
  1038. description="Retrieves a specific relationship by its ID.",
  1039. openapi_extra={
  1040. "x-codeSamples": [
  1041. {
  1042. "lang": "Python",
  1043. "source": textwrap.dedent("""
  1044. from r2r import R2RClient
  1045. client = R2RClient()
  1046. # when using auth, do client.login(...)
  1047. response = client.graphs.get_relationship(
  1048. collection_id="d09dedb1-b2ab-48a5-b950-6e1f464d83e7",
  1049. relationship_id="d09dedb1-b2ab-48a5-b950-6e1f464d83e7"
  1050. )
  1051. """),
  1052. },
  1053. {
  1054. "lang": "JavaScript",
  1055. "source": textwrap.dedent("""
  1056. const { r2rClient } = require("r2r-js");
  1057. const client = new r2rClient();
  1058. function main() {
  1059. const response = await client.graphs.getRelationship({
  1060. collectionId: "d09dedb1-b2ab-48a5-b950-6e1f464d83e7",
  1061. relationshipId: "d09dedb1-b2ab-48a5-b950-6e1f464d83e7"
  1062. });
  1063. }
  1064. main();
  1065. """),
  1066. },
  1067. ],
  1068. },
  1069. )
  1070. @self.base_endpoint
  1071. async def get_relationship(
  1072. collection_id: UUID = Path(
  1073. ...,
  1074. description="The collection ID corresponding to the graph containing the relationship.",
  1075. ),
  1076. relationship_id: UUID = Path(
  1077. ..., description="The ID of the relationship to retrieve."
  1078. ),
  1079. auth_user=Depends(self.providers.auth.auth_wrapper()),
  1080. ) -> WrappedRelationshipResponse:
  1081. """Retrieves a specific relationship by its ID."""
  1082. if (
  1083. # not auth_user.is_superuser
  1084. collection_id not in auth_user.collection_ids
  1085. ):
  1086. raise R2RException(
  1087. "The currently authenticated user does not have access to the collection associated with the given graph.",
  1088. 403,
  1089. )
  1090. results = (
  1091. await self.providers.database.graphs_handler.relationships.get(
  1092. parent_id=collection_id,
  1093. store_type=StoreType.GRAPHS,
  1094. offset=0,
  1095. limit=1,
  1096. relationship_ids=[relationship_id],
  1097. )
  1098. )
  1099. if len(results) == 0 or len(results[0]) == 0:
  1100. raise R2RException("Relationship not found", 404)
  1101. return results[0][0]
  1102. @self.router.post(
  1103. "/graphs/{collection_id}/relationships/{relationship_id}",
  1104. dependencies=[Depends(self.rate_limit_dependency)],
  1105. )
  1106. @self.base_endpoint
  1107. async def update_relationship(
  1108. collection_id: UUID = Path(
  1109. ...,
  1110. description="The collection ID corresponding to the graph containing the relationship.",
  1111. ),
  1112. relationship_id: UUID = Path(
  1113. ..., description="The ID of the relationship to update."
  1114. ),
  1115. subject: Optional[str] = Body(
  1116. ..., description="The updated subject of the relationship."
  1117. ),
  1118. subject_id: Optional[UUID] = Body(
  1119. ..., description="The updated subject ID of the relationship."
  1120. ),
  1121. predicate: Optional[str] = Body(
  1122. ..., description="The updated predicate of the relationship."
  1123. ),
  1124. object: Optional[str] = Body(
  1125. ..., description="The updated object of the relationship."
  1126. ),
  1127. object_id: Optional[UUID] = Body(
  1128. ..., description="The updated object ID of the relationship."
  1129. ),
  1130. description: Optional[str] = Body(
  1131. None,
  1132. description="The updated description of the relationship.",
  1133. ),
  1134. weight: Optional[float] = Body(
  1135. None, description="The updated weight of the relationship."
  1136. ),
  1137. metadata: Optional[dict] = Body(
  1138. None, description="The updated metadata of the relationship."
  1139. ),
  1140. auth_user=Depends(self.providers.auth.auth_wrapper()),
  1141. ) -> WrappedRelationshipResponse:
  1142. """Updates an existing relationship in the graph."""
  1143. if not auth_user.is_superuser:
  1144. raise R2RException(
  1145. "Only superusers can update graph details", 403
  1146. )
  1147. if (
  1148. # not auth_user.is_superuser
  1149. collection_id not in auth_user.collection_ids
  1150. ):
  1151. raise R2RException(
  1152. "The currently authenticated user does not have access to the collection associated with the given graph.",
  1153. 403,
  1154. )
  1155. return await self.services.graph.update_relationship( # type: ignore
  1156. relationship_id=relationship_id,
  1157. subject=subject,
  1158. subject_id=subject_id,
  1159. predicate=predicate,
  1160. object=object,
  1161. object_id=object_id,
  1162. description=description,
  1163. weight=weight,
  1164. metadata=metadata,
  1165. )
  1166. @self.router.delete(
  1167. "/graphs/{collection_id}/relationships/{relationship_id}",
  1168. dependencies=[Depends(self.rate_limit_dependency)],
  1169. description="Removes a relationship from the graph.",
  1170. openapi_extra={
  1171. "x-codeSamples": [
  1172. {
  1173. "lang": "Python",
  1174. "source": textwrap.dedent("""
  1175. from r2r import R2RClient
  1176. client = R2RClient()
  1177. # when using auth, do client.login(...)
  1178. response = client.graphs.delete_relationship(
  1179. collection_id="d09dedb1-b2ab-48a5-b950-6e1f464d83e7",
  1180. relationship_id="d09dedb1-b2ab-48a5-b950-6e1f464d83e7"
  1181. )
  1182. """),
  1183. },
  1184. {
  1185. "lang": "JavaScript",
  1186. "source": textwrap.dedent("""
  1187. const { r2rClient } = require("r2r-js");
  1188. const client = new r2rClient();
  1189. function main() {
  1190. const response = await client.graphs.deleteRelationship({
  1191. collectionId: "d09dedb1-b2ab-48a5-b950-6e1f464d83e7",
  1192. relationshipId: "d09dedb1-b2ab-48a5-b950-6e1f464d83e7"
  1193. });
  1194. }
  1195. main();
  1196. """),
  1197. },
  1198. ],
  1199. },
  1200. )
  1201. @self.base_endpoint
  1202. async def delete_relationship(
  1203. collection_id: UUID = Path(
  1204. ...,
  1205. description="The collection ID corresponding to the graph to remove the relationship from.",
  1206. ),
  1207. relationship_id: UUID = Path(
  1208. ...,
  1209. description="The ID of the relationship to remove from the graph.",
  1210. ),
  1211. auth_user=Depends(self.providers.auth.auth_wrapper()),
  1212. ) -> WrappedBooleanResponse:
  1213. """Removes a relationship from the graph."""
  1214. if not auth_user.is_superuser:
  1215. raise R2RException(
  1216. "Only superusers can delete a relationship.", 403
  1217. )
  1218. if (
  1219. not auth_user.is_superuser
  1220. and collection_id not in auth_user.collection_ids
  1221. ):
  1222. raise R2RException(
  1223. "The currently authenticated user does not have access to the collection associated with the given graph.",
  1224. 403,
  1225. )
  1226. await self.services.graph.delete_relationship(
  1227. parent_id=collection_id,
  1228. relationship_id=relationship_id,
  1229. )
  1230. return GenericBooleanResponse(success=True) # type: ignore
  1231. @self.router.post(
  1232. "/graphs/{collection_id}/communities",
  1233. dependencies=[Depends(self.rate_limit_dependency)],
  1234. summary="Create a new community",
  1235. openapi_extra={
  1236. "x-codeSamples": [
  1237. {
  1238. "lang": "Python",
  1239. "source": textwrap.dedent("""
  1240. from r2r import R2RClient
  1241. client = R2RClient()
  1242. # when using auth, do client.login(...)
  1243. response = client.graphs.create_community(
  1244. collection_id="9fbe403b-c11c-5aae-8ade-ef22980c3ad1",
  1245. name="My Community",
  1246. summary="A summary of the community",
  1247. findings=["Finding 1", "Finding 2"],
  1248. rating=5,
  1249. rating_explanation="This is a rating explanation",
  1250. )
  1251. """),
  1252. },
  1253. {
  1254. "lang": "JavaScript",
  1255. "source": textwrap.dedent("""
  1256. const { r2rClient } = require("r2r-js");
  1257. const client = new r2rClient();
  1258. function main() {
  1259. const response = await client.graphs.createCommunity({
  1260. collectionId: "9fbe403b-c11c-5aae-8ade-ef22980c3ad1",
  1261. name: "My Community",
  1262. summary: "A summary of the community",
  1263. findings: ["Finding 1", "Finding 2"],
  1264. rating: 5,
  1265. ratingExplanation: "This is a rating explanation",
  1266. });
  1267. }
  1268. main();
  1269. """),
  1270. },
  1271. ]
  1272. },
  1273. )
  1274. @self.base_endpoint
  1275. async def create_community(
  1276. collection_id: UUID = Path(
  1277. ...,
  1278. description="The collection ID corresponding to the graph to create the community in.",
  1279. ),
  1280. name: str = Body(..., description="The name of the community"),
  1281. summary: str = Body(..., description="A summary of the community"),
  1282. findings: Optional[list[str]] = Body(
  1283. default=[], description="Findings about the community"
  1284. ),
  1285. rating: Optional[float] = Body(
  1286. default=5, ge=1, le=10, description="Rating between 1 and 10"
  1287. ),
  1288. rating_explanation: Optional[str] = Body(
  1289. default="", description="Explanation for the rating"
  1290. ),
  1291. auth_user=Depends(self.providers.auth.auth_wrapper()),
  1292. ) -> WrappedCommunityResponse:
  1293. """Creates a new community in the graph.
  1294. While communities are typically built automatically via the /graphs/{id}/communities/build endpoint,
  1295. this endpoint allows you to manually create your own communities.
  1296. This can be useful when you want to:
  1297. - Define custom groupings of entities based on domain knowledge
  1298. - Add communities that weren't detected by the automatic process
  1299. - Create hierarchical organization structures
  1300. - Tag groups of entities with specific metadata
  1301. The created communities will be integrated with any existing automatically detected communities
  1302. in the graph's community structure.
  1303. """
  1304. if not auth_user.is_superuser:
  1305. raise R2RException(
  1306. "Only superusers can create a community.", 403
  1307. )
  1308. if (
  1309. not auth_user.is_superuser
  1310. and collection_id not in auth_user.collection_ids
  1311. ):
  1312. raise R2RException(
  1313. "The currently authenticated user does not have access to the collection associated with the given graph.",
  1314. 403,
  1315. )
  1316. return await self.services.graph.create_community( # type: ignore
  1317. parent_id=collection_id,
  1318. name=name,
  1319. summary=summary,
  1320. findings=findings,
  1321. rating=rating,
  1322. rating_explanation=rating_explanation,
  1323. )
  1324. @self.router.get(
  1325. "/graphs/{collection_id}/communities",
  1326. dependencies=[Depends(self.rate_limit_dependency)],
  1327. summary="List communities",
  1328. openapi_extra={
  1329. "x-codeSamples": [
  1330. {
  1331. "lang": "Python",
  1332. "source": textwrap.dedent("""
  1333. from r2r import R2RClient
  1334. client = R2RClient()
  1335. # when using auth, do client.login(...)
  1336. response = client.graphs.list_communities(collection_id="9fbe403b-c11c-5aae-8ade-ef22980c3ad1")
  1337. """),
  1338. },
  1339. {
  1340. "lang": "JavaScript",
  1341. "source": textwrap.dedent("""
  1342. const { r2rClient } = require("r2r-js");
  1343. const client = new r2rClient();
  1344. function main() {
  1345. const response = await client.graphs.listCommunities({
  1346. collectionId: "9fbe403b-c11c-5aae-8ade-ef22980c3ad1",
  1347. });
  1348. }
  1349. main();
  1350. """),
  1351. },
  1352. ]
  1353. },
  1354. )
  1355. @self.base_endpoint
  1356. async def get_communities(
  1357. collection_id: UUID = Path(
  1358. ...,
  1359. description="The collection ID corresponding to the graph to get communities for.",
  1360. ),
  1361. offset: int = Query(
  1362. 0,
  1363. ge=0,
  1364. description="Specifies the number of objects to skip. Defaults to 0.",
  1365. ),
  1366. limit: int = Query(
  1367. 100,
  1368. ge=1,
  1369. le=1000,
  1370. description="Specifies a limit on the number of objects to return, ranging between 1 and 100. Defaults to 100.",
  1371. ),
  1372. auth_user=Depends(self.providers.auth.auth_wrapper()),
  1373. ) -> WrappedCommunitiesResponse:
  1374. """Lists all communities in the graph with pagination support."""
  1375. if (
  1376. # not auth_user.is_superuser
  1377. collection_id not in auth_user.collection_ids
  1378. ):
  1379. raise R2RException(
  1380. "The currently authenticated user does not have access to the collection associated with the given graph.",
  1381. 403,
  1382. )
  1383. communities, count = await self.services.graph.get_communities(
  1384. parent_id=collection_id,
  1385. offset=offset,
  1386. limit=limit,
  1387. )
  1388. return communities, { # type: ignore
  1389. "total_entries": count,
  1390. }
  1391. @self.router.get(
  1392. "/graphs/{collection_id}/communities/{community_id}",
  1393. dependencies=[Depends(self.rate_limit_dependency)],
  1394. summary="Retrieve a community",
  1395. openapi_extra={
  1396. "x-codeSamples": [
  1397. {
  1398. "lang": "Python",
  1399. "source": textwrap.dedent("""
  1400. from r2r import R2RClient
  1401. client = R2RClient()
  1402. # when using auth, do client.login(...)
  1403. response = client.graphs.get_community(collection_id="9fbe403b-c11c-5aae-8ade-ef22980c3ad1")
  1404. """),
  1405. },
  1406. {
  1407. "lang": "JavaScript",
  1408. "source": textwrap.dedent("""
  1409. const { r2rClient } = require("r2r-js");
  1410. const client = new r2rClient();
  1411. function main() {
  1412. const response = await client.graphs.getCommunity({
  1413. collectionId: "9fbe403b-c11c-5aae-8ade-ef22980c3ad1",
  1414. });
  1415. }
  1416. main();
  1417. """),
  1418. },
  1419. ]
  1420. },
  1421. )
  1422. @self.base_endpoint
  1423. async def get_community(
  1424. collection_id: UUID = Path(
  1425. ...,
  1426. description="The ID of the collection to get communities for.",
  1427. ),
  1428. community_id: UUID = Path(
  1429. ...,
  1430. description="The ID of the community to get.",
  1431. ),
  1432. auth_user=Depends(self.providers.auth.auth_wrapper()),
  1433. ) -> WrappedCommunityResponse:
  1434. """Retrieves a specific community by its ID."""
  1435. if (
  1436. # not auth_user.is_superuser
  1437. collection_id not in auth_user.collection_ids
  1438. ):
  1439. raise R2RException(
  1440. "The currently authenticated user does not have access to the collection associated with the given graph.",
  1441. 403,
  1442. )
  1443. results = (
  1444. await self.providers.database.graphs_handler.communities.get(
  1445. parent_id=collection_id,
  1446. community_ids=[community_id],
  1447. store_type=StoreType.GRAPHS,
  1448. offset=0,
  1449. limit=1,
  1450. )
  1451. )
  1452. if len(results) == 0 or len(results[0]) == 0:
  1453. raise R2RException("Community not found", 404)
  1454. return results[0][0]
  1455. @self.router.delete(
  1456. "/graphs/{collection_id}/communities/{community_id}",
  1457. dependencies=[Depends(self.rate_limit_dependency)],
  1458. summary="Delete a community",
  1459. openapi_extra={
  1460. "x-codeSamples": [
  1461. {
  1462. "lang": "Python",
  1463. "source": textwrap.dedent("""
  1464. from r2r import R2RClient
  1465. client = R2RClient()
  1466. # when using auth, do client.login(...)
  1467. response = client.graphs.delete_community(
  1468. collection_id="d09dedb1-b2ab-48a5-b950-6e1f464d83e7",
  1469. community_id="d09dedb1-b2ab-48a5-b950-6e1f464d83e7"
  1470. )
  1471. """),
  1472. },
  1473. {
  1474. "lang": "JavaScript",
  1475. "source": textwrap.dedent("""
  1476. const { r2rClient } = require("r2r-js");
  1477. const client = new r2rClient();
  1478. function main() {
  1479. const response = await client.graphs.deleteCommunity({
  1480. collectionId: "d09dedb1-b2ab-48a5-b950-6e1f464d83e7",
  1481. communityId: "d09dedb1-b2ab-48a5-b950-6e1f464d83e7"
  1482. });
  1483. }
  1484. main();
  1485. """),
  1486. },
  1487. ]
  1488. },
  1489. )
  1490. @self.base_endpoint
  1491. async def delete_community(
  1492. collection_id: UUID = Path(
  1493. ...,
  1494. description="The collection ID corresponding to the graph to delete the community from.",
  1495. ),
  1496. community_id: UUID = Path(
  1497. ...,
  1498. description="The ID of the community to delete.",
  1499. ),
  1500. auth_user=Depends(self.providers.auth.auth_wrapper()),
  1501. ) -> WrappedBooleanResponse:
  1502. if (
  1503. not auth_user.is_superuser
  1504. and collection_id not in auth_user.graph_ids
  1505. ):
  1506. raise R2RException(
  1507. "Only superusers can delete communities", 403
  1508. )
  1509. if (
  1510. # not auth_user.is_superuser
  1511. collection_id not in auth_user.collection_ids
  1512. ):
  1513. raise R2RException(
  1514. "The currently authenticated user does not have access to the collection associated with the given graph.",
  1515. 403,
  1516. )
  1517. await self.services.graph.delete_community(
  1518. parent_id=collection_id,
  1519. community_id=community_id,
  1520. )
  1521. return GenericBooleanResponse(success=True) # type: ignore
  1522. @self.router.post(
  1523. "/graphs/{collection_id}/communities/export",
  1524. summary="Export document communities to CSV",
  1525. dependencies=[Depends(self.rate_limit_dependency)],
  1526. openapi_extra={
  1527. "x-codeSamples": [
  1528. {
  1529. "lang": "Python",
  1530. "source": textwrap.dedent("""
  1531. from r2r import R2RClient
  1532. client = R2RClient("http://localhost:7272")
  1533. # when using auth, do client.login(...)
  1534. response = client.graphs.export_communities(
  1535. collection_id="b4ac4dd6-5f27-596e-a55b-7cf242ca30aa",
  1536. output_path="export.csv",
  1537. columns=["id", "title", "created_at"],
  1538. include_header=True,
  1539. )
  1540. """),
  1541. },
  1542. {
  1543. "lang": "JavaScript",
  1544. "source": textwrap.dedent("""
  1545. const { r2rClient } = require("r2r-js");
  1546. const client = new r2rClient("http://localhost:7272");
  1547. function main() {
  1548. await client.graphs.exportCommunities({
  1549. collectionId: "b4ac4dd6-5f27-596e-a55b-7cf242ca30aa",
  1550. outputPath: "export.csv",
  1551. columns: ["id", "title", "created_at"],
  1552. includeHeader: true,
  1553. });
  1554. }
  1555. main();
  1556. """),
  1557. },
  1558. {
  1559. "lang": "cURL",
  1560. "source": textwrap.dedent("""
  1561. curl -X POST "http://127.0.0.1:7272/v3/graphs/export_communities" \
  1562. -H "Authorization: Bearer YOUR_API_KEY" \
  1563. -H "Content-Type: application/json" \
  1564. -H "Accept: text/csv" \
  1565. -d '{ "columns": ["id", "title", "created_at"], "include_header": true }' \
  1566. --output export.csv
  1567. """),
  1568. },
  1569. ]
  1570. },
  1571. )
  1572. @self.base_endpoint
  1573. async def export_communities(
  1574. background_tasks: BackgroundTasks,
  1575. collection_id: UUID = Path(
  1576. ...,
  1577. description="The ID of the document to export entities from.",
  1578. ),
  1579. columns: Optional[list[str]] = Body(
  1580. None, description="Specific columns to export"
  1581. ),
  1582. filters: Optional[dict] = Body(
  1583. None, description="Filters to apply to the export"
  1584. ),
  1585. include_header: Optional[bool] = Body(
  1586. True, description="Whether to include column headers"
  1587. ),
  1588. auth_user=Depends(self.providers.auth.auth_wrapper()),
  1589. ) -> FileResponse:
  1590. """Export documents as a downloadable CSV file."""
  1591. if not auth_user.is_superuser:
  1592. raise R2RException(
  1593. "Only a superuser can export data.",
  1594. 403,
  1595. )
  1596. (
  1597. csv_file_path,
  1598. temp_file,
  1599. ) = await self.services.management.export_graph_communities(
  1600. id=collection_id,
  1601. columns=columns,
  1602. filters=filters,
  1603. include_header=include_header
  1604. if include_header is not None
  1605. else True,
  1606. )
  1607. background_tasks.add_task(temp_file.close)
  1608. return FileResponse(
  1609. path=csv_file_path,
  1610. media_type="text/csv",
  1611. filename="documents_export.csv",
  1612. )
  1613. @self.router.post(
  1614. "/graphs/{collection_id}/communities/{community_id}",
  1615. dependencies=[Depends(self.rate_limit_dependency)],
  1616. summary="Update community",
  1617. openapi_extra={
  1618. "x-codeSamples": [
  1619. {
  1620. "lang": "Python",
  1621. "source": textwrap.dedent("""
  1622. from r2r import R2RClient
  1623. client = R2RClient()
  1624. # when using auth, do client.login(...)
  1625. response = client.graphs.update_community(
  1626. collection_id="d09dedb1-b2ab-48a5-b950-6e1f464d83e7",
  1627. community_update={
  1628. "metadata": {
  1629. "topic": "Technology",
  1630. "description": "Tech companies and products"
  1631. }
  1632. }
  1633. )"""),
  1634. },
  1635. {
  1636. "lang": "JavaScript",
  1637. "source": textwrap.dedent("""
  1638. const { r2rClient } = require("r2r-js");
  1639. const client = new r2rClient();
  1640. async function main() {
  1641. const response = await client.graphs.updateCommunity({
  1642. collectionId: "d09dedb1-b2ab-48a5-b950-6e1f464d83e7",
  1643. communityId: "d09dedb1-b2ab-48a5-b950-6e1f464d83e7",
  1644. communityUpdate: {
  1645. metadata: {
  1646. topic: "Technology",
  1647. description: "Tech companies and products"
  1648. }
  1649. }
  1650. });
  1651. }
  1652. main();
  1653. """),
  1654. },
  1655. ]
  1656. },
  1657. )
  1658. @self.base_endpoint
  1659. async def update_community(
  1660. collection_id: UUID = Path(...),
  1661. community_id: UUID = Path(...),
  1662. name: Optional[str] = Body(None),
  1663. summary: Optional[str] = Body(None),
  1664. findings: Optional[list[str]] = Body(None),
  1665. rating: Optional[float] = Body(default=None, ge=1, le=10),
  1666. rating_explanation: Optional[str] = Body(None),
  1667. auth_user=Depends(self.providers.auth.auth_wrapper()),
  1668. ) -> WrappedCommunityResponse:
  1669. """Updates an existing community in the graph."""
  1670. if (
  1671. not auth_user.is_superuser
  1672. and collection_id not in auth_user.graph_ids
  1673. ):
  1674. raise R2RException(
  1675. "Only superusers can update communities.", 403
  1676. )
  1677. if (
  1678. # not auth_user.is_superuser
  1679. collection_id not in auth_user.collection_ids
  1680. ):
  1681. raise R2RException(
  1682. "The currently authenticated user does not have access to the collection associated with the given graph.",
  1683. 403,
  1684. )
  1685. return await self.services.graph.update_community( # type: ignore
  1686. community_id=community_id,
  1687. name=name,
  1688. summary=summary,
  1689. findings=findings,
  1690. rating=rating,
  1691. rating_explanation=rating_explanation,
  1692. )
  1693. @self.router.post(
  1694. "/graphs/{collection_id}/pull",
  1695. dependencies=[Depends(self.rate_limit_dependency)],
  1696. summary="Pull latest entities to the graph",
  1697. openapi_extra={
  1698. "x-codeSamples": [
  1699. {
  1700. "lang": "Python",
  1701. "source": textwrap.dedent("""
  1702. from r2r import R2RClient
  1703. client = R2RClient()
  1704. # when using auth, do client.login(...)
  1705. response = client.graphs.pull(
  1706. collection_id="d09dedb1-b2ab-48a5-b950-6e1f464d83e7"
  1707. )"""),
  1708. },
  1709. {
  1710. "lang": "JavaScript",
  1711. "source": textwrap.dedent("""
  1712. const { r2rClient } = require("r2r-js");
  1713. const client = new r2rClient();
  1714. async function main() {
  1715. const response = await client.graphs.pull({
  1716. collection_id: "d09dedb1-b2ab-48a5-b950-6e1f464d83e7"
  1717. });
  1718. }
  1719. main();
  1720. """),
  1721. },
  1722. ]
  1723. },
  1724. )
  1725. @self.base_endpoint
  1726. async def pull(
  1727. collection_id: UUID = Path(
  1728. ..., description="The ID of the graph to initialize."
  1729. ),
  1730. force: Optional[bool] = Body(
  1731. False,
  1732. description="If true, forces a re-pull of all entities and relationships.",
  1733. ),
  1734. # document_ids: list[UUID] = Body(
  1735. # ..., description="List of document IDs to add to the graph."
  1736. # ),
  1737. auth_user=Depends(self.providers.auth.auth_wrapper()),
  1738. ) -> WrappedBooleanResponse:
  1739. """Adds documents to a graph by copying their entities and
  1740. relationships.
  1741. This endpoint:
  1742. 1. Copies document entities to the graphs_entities table
  1743. 2. Copies document relationships to the graphs_relationships table
  1744. 3. Associates the documents with the graph
  1745. When a document is added:
  1746. - Its entities and relationships are copied to graph-specific tables
  1747. - Existing entities/relationships are updated by merging their properties
  1748. - The document ID is recorded in the graph's document_ids array
  1749. Documents added to a graph will contribute their knowledge to:
  1750. - Graph analysis and querying
  1751. - Community detection
  1752. - Knowledge graph enrichment
  1753. The user must have access to both the graph and the documents being added.
  1754. """
  1755. collections_overview_response = (
  1756. await self.services.management.collections_overview(
  1757. user_ids=[auth_user.id],
  1758. collection_ids=[collection_id],
  1759. offset=0,
  1760. limit=1,
  1761. )
  1762. )["results"]
  1763. if len(collections_overview_response) == 0: # type: ignore
  1764. raise R2RException("Collection not found.", 404)
  1765. # Check user permissions for graph
  1766. if (
  1767. not auth_user.is_superuser
  1768. and collections_overview_response[0].owner_id != auth_user.id # type: ignore
  1769. ):
  1770. raise R2RException("Only superusers can `pull` a graph.", 403)
  1771. if (
  1772. # not auth_user.is_superuser
  1773. collection_id not in auth_user.collection_ids
  1774. ):
  1775. raise R2RException(
  1776. "The currently authenticated user does not have access to the collection associated with the given graph.",
  1777. 403,
  1778. )
  1779. list_graphs_response = await self.services.graph.list_graphs(
  1780. # user_ids=None,
  1781. graph_ids=[collection_id],
  1782. offset=0,
  1783. limit=1,
  1784. )
  1785. if len(list_graphs_response["results"]) == 0: # type: ignore
  1786. raise R2RException("Graph not found", 404)
  1787. collection_id = list_graphs_response["results"][0].collection_id # type: ignore
  1788. documents: list[DocumentResponse] = []
  1789. document_req = await self.providers.database.collections_handler.documents_in_collection(
  1790. collection_id, offset=0, limit=100
  1791. )
  1792. results = cast(list[DocumentResponse], document_req["results"])
  1793. documents.extend(results)
  1794. while len(results) == 100:
  1795. document_req = await self.providers.database.collections_handler.documents_in_collection(
  1796. collection_id, offset=len(documents), limit=100
  1797. )
  1798. results = cast(list[DocumentResponse], document_req["results"])
  1799. documents.extend(results)
  1800. success = False
  1801. for document in documents:
  1802. entities = (
  1803. await self.providers.database.graphs_handler.entities.get(
  1804. parent_id=document.id,
  1805. store_type=StoreType.DOCUMENTS,
  1806. offset=0,
  1807. limit=100,
  1808. )
  1809. )
  1810. has_document = (
  1811. await self.providers.database.graphs_handler.has_document(
  1812. collection_id, document.id
  1813. )
  1814. )
  1815. if has_document:
  1816. logger.info(
  1817. f"Document {document.id} is already in graph {collection_id}, skipping."
  1818. )
  1819. continue
  1820. if len(entities[0]) == 0:
  1821. if not force:
  1822. logger.warning(
  1823. f"Document {document.id} has no entities, extraction may not have been called, skipping."
  1824. )
  1825. continue
  1826. else:
  1827. logger.warning(
  1828. f"Document {document.id} has no entities, but force=True, continuing."
  1829. )
  1830. success = (
  1831. await self.providers.database.graphs_handler.add_documents(
  1832. id=collection_id,
  1833. document_ids=[document.id],
  1834. )
  1835. )
  1836. if not success:
  1837. logger.warning(
  1838. f"No documents were added to graph {collection_id}, marking as failed."
  1839. )
  1840. if success:
  1841. await self.providers.database.documents_handler.set_workflow_status(
  1842. id=collection_id,
  1843. status_type="graph_sync_status",
  1844. status=GraphConstructionStatus.SUCCESS,
  1845. )
  1846. return GenericBooleanResponse(success=success) # type: ignore