12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147 |
- import logging
- import textwrap
- from typing import Optional
- from uuid import UUID
- from fastapi import Body, Depends, Path, Query
- from fastapi.background import BackgroundTasks
- from fastapi.responses import FileResponse
- from core.base import KGEnrichmentStatus, R2RException, Workflow
- from core.base.abstractions import KGRunType, StoreType
- from core.base.api.models import (
- GenericBooleanResponse,
- WrappedBooleanResponse,
- WrappedCommunitiesResponse,
- WrappedCommunityResponse,
- WrappedEntitiesResponse,
- WrappedEntityResponse,
- WrappedGraphResponse,
- WrappedGraphsResponse,
- WrappedRelationshipResponse,
- WrappedRelationshipsResponse,
- )
- from core.utils import (
- generate_default_user_collection_id,
- update_settings_from_dict,
- )
- from ...abstractions import R2RProviders, R2RServices
- from .base_router import BaseRouterV3
- logger = logging.getLogger()
- class GraphRouter(BaseRouterV3):
- def __init__(
- self,
- providers: R2RProviders,
- services: R2RServices,
- ):
- super().__init__(providers, services)
- self._register_workflows()
- def _register_workflows(self):
- workflow_messages = {}
- if self.providers.orchestration.config.provider == "hatchet":
- workflow_messages["extract-triples"] = (
- "Document extraction task queued successfully."
- )
- workflow_messages["build-communities"] = (
- "Graph enrichment task queued successfully."
- )
- else:
- workflow_messages["extract-triples"] = (
- "Document entities and relationships extracted successfully."
- )
- workflow_messages["build-communities"] = (
- "Graph communities created successfully."
- )
- self.providers.orchestration.register_workflows(
- Workflow.KG,
- self.services.graph,
- workflow_messages,
- )
- async def _get_collection_id(
- self, collection_id: Optional[UUID], auth_user
- ) -> UUID:
- """Helper method to get collection ID, using default if none provided"""
- if collection_id is None:
- return generate_default_user_collection_id(auth_user.id)
- return collection_id
- def _setup_routes(self):
- @self.router.get(
- "/graphs",
- dependencies=[Depends(self.rate_limit_dependency)],
- summary="List graphs",
- openapi_extra={
- "x-codeSamples": [
- { # TODO: Verify
- "lang": "Python",
- "source": textwrap.dedent(
- """
- from r2r import R2RClient
- client = R2RClient()
- # when using auth, do client.login(...)
- response = client.graphs.list()
- """
- ),
- },
- {
- "lang": "JavaScript",
- "source": textwrap.dedent(
- """
- const { r2rClient } = require("r2r-js");
- const client = new r2rClient();
- function main() {
- const response = await client.graphs.list({});
- }
- main();
- """
- ),
- },
- ]
- },
- )
- @self.base_endpoint
- async def list_graphs(
- collection_ids: list[str] = Query(
- [],
- description="A list of graph IDs to retrieve. If not provided, all graphs will be returned.",
- ),
- offset: int = Query(
- 0,
- ge=0,
- description="Specifies the number of objects to skip. Defaults to 0.",
- ),
- limit: int = Query(
- 100,
- ge=1,
- le=1000,
- description="Specifies a limit on the number of objects to return, ranging between 1 and 100. Defaults to 100.",
- ),
- auth_user=Depends(self.providers.auth.auth_wrapper()),
- ) -> WrappedGraphsResponse:
- """
- Returns a paginated list of graphs the authenticated user has access to.
- Results can be filtered by providing specific graph IDs. Regular users will only see
- graphs they own or have access to. Superusers can see all graphs.
- The graphs are returned in order of last modification, with most recent first.
- """
- requesting_user_id = (
- None if auth_user.is_superuser else [auth_user.id]
- )
- graph_uuids = [UUID(graph_id) for graph_id in collection_ids]
- list_graphs_response = await self.services.graph.list_graphs(
- # user_ids=requesting_user_id,
- graph_ids=graph_uuids,
- offset=offset,
- limit=limit,
- )
- return ( # type: ignore
- list_graphs_response["results"],
- {"total_entries": list_graphs_response["total_entries"]},
- )
- @self.router.get(
- "/graphs/{collection_id}",
- dependencies=[Depends(self.rate_limit_dependency)],
- summary="Retrieve graph details",
- openapi_extra={
- "x-codeSamples": [
- {
- "lang": "Python",
- "source": textwrap.dedent(
- """
- from r2r import R2RClient
- client = R2RClient()
- # when using auth, do client.login(...)
- response = client.graphs.get(
- collection_id="d09dedb1-b2ab-48a5-b950-6e1f464d83e7"
- )"""
- ),
- },
- {
- "lang": "JavaScript",
- "source": textwrap.dedent(
- """
- const { r2rClient } = require("r2r-js");
- const client = new r2rClient();
- function main() {
- const response = await client.graphs.retrieve({
- collectionId: "d09dedb1-b2ab-48a5-b950-6e1f464d83e7"
- });
- }
- main();
- """
- ),
- },
- {
- "lang": "cURL",
- "source": textwrap.dedent(
- """
- curl -X GET "https://api.example.com/v3/graphs/d09dedb1-b2ab-48a5-b950-6e1f464d83e7" \\
- -H "Authorization: Bearer YOUR_API_KEY" """
- ),
- },
- ]
- },
- )
- @self.base_endpoint
- async def get_graph(
- collection_id: UUID = Path(...),
- auth_user=Depends(self.providers.auth.auth_wrapper()),
- ) -> WrappedGraphResponse:
- """
- Retrieves detailed information about a specific graph by ID.
- """
- if (
- # not auth_user.is_superuser
- collection_id
- not in auth_user.collection_ids
- ):
- raise R2RException(
- "The currently authenticated user does not have access to the specified collection associated with the given graph.",
- 403,
- )
- list_graphs_response = await self.services.graph.list_graphs(
- # user_ids=None,
- graph_ids=[collection_id],
- offset=0,
- limit=1,
- )
- return list_graphs_response["results"][0] # type: ignore
- @self.router.post(
- "/graphs/{collection_id}/communities/build",
- dependencies=[Depends(self.rate_limit_dependency)],
- )
- @self.base_endpoint
- async def build_communities(
- collection_id: UUID = Path(
- ..., description="The unique identifier of the collection"
- ),
- run_type: Optional[KGRunType] = Body(
- default=KGRunType.ESTIMATE,
- description="Run type for the graph enrichment process.",
- ),
- graph_enrichment_settings: Optional[dict] = Body(
- default=None,
- description="Settings for the graph enrichment process.",
- ),
- run_with_orchestration: Optional[bool] = Body(True),
- auth_user=Depends(self.providers.auth.auth_wrapper()),
- ):
- """
- Creates communities in the graph by analyzing entity relationships and similarities.
- Communities are created through the following process:
- 1. Analyzes entity relationships and metadata to build a similarity graph
- 2. Applies advanced community detection algorithms (e.g. Leiden) to identify densely connected groups
- 3. Creates hierarchical community structure with multiple granularity levels
- 4. Generates natural language summaries and statistical insights for each community
- The resulting communities can be used to:
- - Understand high-level graph structure and organization
- - Identify key entity groupings and their relationships
- - Navigate and explore the graph at different levels of detail
- - Generate insights about entity clusters and their characteristics
- The community detection process is configurable through settings like:
- - Community detection algorithm parameters
- - Summary generation prompt
- """
- if not auth_user.is_superuser:
- raise R2RException(
- "Only superusers can build communities", 403
- )
- if (
- # not auth_user.is_superuser
- collection_id
- not in auth_user.collection_ids
- ):
- raise R2RException(
- "The currently authenticated user does not have access to the collection associated with the given graph.",
- 403,
- )
- # If no collection ID is provided, use the default user collection
- # id = generate_default_user_collection_id(auth_user.id)
- # If no run type is provided, default to estimate
- if not run_type:
- run_type = KGRunType.ESTIMATE
- # Apply runtime settings overrides
- server_graph_enrichment_settings = (
- self.providers.database.config.graph_enrichment_settings
- )
- if graph_enrichment_settings:
- server_graph_enrichment_settings = update_settings_from_dict(
- server_graph_enrichment_settings, graph_enrichment_settings
- )
- workflow_input = {
- "collection_id": str(collection_id),
- "graph_enrichment_settings": server_graph_enrichment_settings.model_dump_json(),
- "user": auth_user.json(),
- }
- if run_with_orchestration:
- return await self.providers.orchestration.run_workflow( # type: ignore
- "build-communities", {"request": workflow_input}, {}
- )
- else:
- from core.main.orchestration import simple_kg_factory
- logger.info("Running build-communities without orchestration.")
- simple_kg = simple_kg_factory(self.services.graph)
- await simple_kg["build-communities"](workflow_input)
- return {
- "message": "Graph communities created successfully.",
- "task_id": None,
- }
- @self.router.post(
- "/graphs/{collection_id}/reset",
- dependencies=[Depends(self.rate_limit_dependency)],
- summary="Reset a graph back to the initial state.",
- openapi_extra={
- "x-codeSamples": [
- {
- "lang": "Python",
- "source": textwrap.dedent(
- """
- from r2r import R2RClient
- client = R2RClient()
- # when using auth, do client.login(...)
- response = client.graphs.reset(
- collection_id="d09dedb1-b2ab-48a5-b950-6e1f464d83e7",
- )"""
- ),
- },
- {
- "lang": "JavaScript",
- "source": textwrap.dedent(
- """
- const { r2rClient } = require("r2r-js");
- const client = new r2rClient();
- function main() {
- const response = await client.graphs.reset({
- collectionId: "d09dedb1-b2ab-48a5-b950-6e1f464d83e7"
- });
- }
- main();
- """
- ),
- },
- {
- "lang": "cURL",
- "source": textwrap.dedent(
- """
- curl -X POST "https://api.example.com/v3/graphs/d09dedb1-b2ab-48a5-b950-6e1f464d83e7/reset" \\
- -H "Authorization: Bearer YOUR_API_KEY" """
- ),
- },
- ]
- },
- )
- @self.base_endpoint
- async def reset(
- collection_id: UUID = Path(...),
- auth_user=Depends(self.providers.auth.auth_wrapper()),
- ) -> WrappedBooleanResponse:
- """
- Deletes a graph and all its associated data.
- This endpoint permanently removes the specified graph along with all
- entities and relationships that belong to only this graph.
- The original source entities and relationships extracted from underlying documents are not deleted
- and are managed through the document lifecycle.
- """
- if not auth_user.is_superuser:
- raise R2RException("Only superusers can reset a graph", 403)
- if (
- # not auth_user.is_superuser
- collection_id
- not in auth_user.collection_ids
- ):
- raise R2RException(
- "The currently authenticated user does not have access to the collection associated with the given graph.",
- 403,
- )
- await self.services.graph.reset_graph_v3(id=collection_id)
- # await _pull(collection_id, auth_user)
- return GenericBooleanResponse(success=True) # type: ignore
- # update graph
- @self.router.post(
- "/graphs/{collection_id}",
- dependencies=[Depends(self.rate_limit_dependency)],
- summary="Update graph",
- openapi_extra={
- "x-codeSamples": [
- {
- "lang": "Python",
- "source": textwrap.dedent(
- """
- from r2r import R2RClient
- client = R2RClient()
- # when using auth, do client.login(...)
- response = client.graphs.update(
- collection_id="d09dedb1-b2ab-48a5-b950-6e1f464d83e7",
- graph={
- "name": "New Name",
- "description": "New Description"
- }
- )"""
- ),
- },
- {
- "lang": "JavaScript",
- "source": textwrap.dedent(
- """
- const { r2rClient } = require("r2r-js");
- const client = new r2rClient();
- function main() {
- const response = await client.graphs.update({
- collection_id: "d09dedb1-b2ab-48a5-b950-6e1f464d83e7",
- name: "New Name",
- description: "New Description",
- });
- }
- main();
- """
- ),
- },
- ]
- },
- )
- @self.base_endpoint
- async def update_graph(
- collection_id: UUID = Path(
- ...,
- description="The collection ID corresponding to the graph to update",
- ),
- name: Optional[str] = Body(
- None, description="The name of the graph"
- ),
- description: Optional[str] = Body(
- None, description="An optional description of the graph"
- ),
- auth_user=Depends(self.providers.auth.auth_wrapper()),
- ):
- """
- Update an existing graphs's configuration.
- This endpoint allows updating the name and description of an existing collection.
- The user must have appropriate permissions to modify the collection.
- """
- if not auth_user.is_superuser:
- raise R2RException(
- "Only superusers can update graph details", 403
- )
- if (
- not auth_user.is_superuser
- and id not in auth_user.collection_ids
- ):
- raise R2RException(
- "The currently authenticated user does not have access to the collection associated with the given graph.",
- 403,
- )
- return await self.services.graph.update_graph( # type: ignore
- collection_id,
- name=name,
- description=description,
- )
- @self.router.get(
- "/graphs/{collection_id}/entities",
- dependencies=[Depends(self.rate_limit_dependency)],
- openapi_extra={
- "x-codeSamples": [
- {
- "lang": "Python",
- "source": textwrap.dedent(
- """
- from r2r import R2RClient
- client = R2RClient()
- # when using auth, do client.login(...)
- response = client.graphs.list_entities(collection_id="d09dedb1-b2ab-48a5-b950-6e1f464d83e7")
- """
- ),
- },
- {
- "lang": "JavaScript",
- "source": textwrap.dedent(
- """
- const { r2rClient } = require("r2r-js");
- const client = new r2rClient();
- function main() {
- const response = await client.graphs.listEntities({
- collection_id: "d09dedb1-b2ab-48a5-b950-6e1f464d83e7",
- });
- }
- main();
- """
- ),
- },
- ],
- },
- )
- @self.base_endpoint
- async def get_entities(
- collection_id: UUID = Path(
- ...,
- description="The collection ID corresponding to the graph to list entities from.",
- ),
- offset: int = Query(
- 0,
- ge=0,
- description="Specifies the number of objects to skip. Defaults to 0.",
- ),
- limit: int = Query(
- 100,
- ge=1,
- le=1000,
- description="Specifies a limit on the number of objects to return, ranging between 1 and 100. Defaults to 100.",
- ),
- auth_user=Depends(self.providers.auth.auth_wrapper()),
- ) -> WrappedEntitiesResponse:
- """Lists all entities in the graph with pagination support."""
- if (
- # not auth_user.is_superuser
- collection_id
- not in auth_user.collection_ids
- ):
- raise R2RException(
- "The currently authenticated user does not have access to the collection associated with the given graph.",
- 403,
- )
- entities, count = await self.services.graph.get_entities(
- parent_id=collection_id,
- offset=offset,
- limit=limit,
- )
- return entities, { # type: ignore
- "total_entries": count,
- }
- @self.router.post(
- "/graphs/{collection_id}/entities/export",
- summary="Export graph entities to CSV",
- dependencies=[Depends(self.rate_limit_dependency)],
- openapi_extra={
- "x-codeSamples": [
- {
- "lang": "Python",
- "source": textwrap.dedent(
- """
- from r2r import R2RClient
- client = R2RClient("http://localhost:7272")
- # when using auth, do client.login(...)
- response = client.graphs.export_entities(
- collection_id="b4ac4dd6-5f27-596e-a55b-7cf242ca30aa",
- output_path="export.csv",
- columns=["id", "title", "created_at"],
- include_header=True,
- )
- """
- ),
- },
- {
- "lang": "JavaScript",
- "source": textwrap.dedent(
- """
- const { r2rClient } = require("r2r-js");
- const client = new r2rClient("http://localhost:7272");
- function main() {
- await client.graphs.exportEntities({
- collectionId: "b4ac4dd6-5f27-596e-a55b-7cf242ca30aa",
- outputPath: "export.csv",
- columns: ["id", "title", "created_at"],
- includeHeader: true,
- });
- }
- main();
- """
- ),
- },
- {
- "lang": "CLI",
- "source": textwrap.dedent(
- """
- """
- ),
- },
- {
- "lang": "cURL",
- "source": textwrap.dedent(
- """
- curl -X POST "http://127.0.0.1:7272/v3/graphs/export_entities" \
- -H "Authorization: Bearer YOUR_API_KEY" \
- -H "Content-Type: application/json" \
- -H "Accept: text/csv" \
- -d '{ "columns": ["id", "title", "created_at"], "include_header": true }' \
- --output export.csv
- """
- ),
- },
- ]
- },
- )
- @self.base_endpoint
- async def export_entities(
- background_tasks: BackgroundTasks,
- collection_id: UUID = Path(
- ...,
- description="The ID of the collection to export entities from.",
- ),
- columns: Optional[list[str]] = Body(
- None, description="Specific columns to export"
- ),
- filters: Optional[dict] = Body(
- None, description="Filters to apply to the export"
- ),
- include_header: Optional[bool] = Body(
- True, description="Whether to include column headers"
- ),
- auth_user=Depends(self.providers.auth.auth_wrapper()),
- ) -> FileResponse:
- """
- Export documents as a downloadable CSV file.
- """
- if not auth_user.is_superuser:
- raise R2RException(
- "Only a superuser can export data.",
- 403,
- )
- csv_file_path, temp_file = (
- await self.services.management.export_graph_entities(
- id=collection_id,
- columns=columns,
- filters=filters,
- include_header=include_header,
- )
- )
- background_tasks.add_task(temp_file.close)
- return FileResponse(
- path=csv_file_path,
- media_type="text/csv",
- filename="documents_export.csv",
- )
- @self.router.post(
- "/graphs/{collection_id}/entities",
- dependencies=[Depends(self.rate_limit_dependency)],
- )
- @self.base_endpoint
- async def create_entity(
- collection_id: UUID = Path(
- ...,
- description="The collection ID corresponding to the graph to add the entity to.",
- ),
- name: str = Body(
- ..., description="The name of the entity to create."
- ),
- description: str = Body(
- ..., description="The description of the entity to create."
- ),
- category: Optional[str] = Body(
- None, description="The category of the entity to create."
- ),
- metadata: Optional[dict] = Body(
- None, description="The metadata of the entity to create."
- ),
- auth_user=Depends(self.providers.auth.auth_wrapper()),
- ) -> WrappedEntityResponse:
- """Creates a new entity in the graph."""
- if (
- # not auth_user.is_superuser
- collection_id
- not in auth_user.collection_ids
- ):
- raise R2RException(
- "The currently authenticated user does not have access to the collection associated with the given graph.",
- 403,
- )
- return await self.services.graph.create_entity(
- name=name,
- description=description,
- parent_id=collection_id,
- category=category,
- metadata=metadata,
- )
- @self.router.post(
- "/graphs/{collection_id}/relationships",
- dependencies=[Depends(self.rate_limit_dependency)],
- )
- @self.base_endpoint
- async def create_relationship(
- collection_id: UUID = Path(
- ...,
- description="The collection ID corresponding to the graph to add the relationship to.",
- ),
- subject: str = Body(
- ..., description="The subject of the relationship to create."
- ),
- subject_id: UUID = Body(
- ...,
- description="The ID of the subject of the relationship to create.",
- ),
- predicate: str = Body(
- ..., description="The predicate of the relationship to create."
- ),
- object: str = Body(
- ..., description="The object of the relationship to create."
- ),
- object_id: UUID = Body(
- ...,
- description="The ID of the object of the relationship to create.",
- ),
- description: str = Body(
- ...,
- description="The description of the relationship to create.",
- ),
- weight: float = Body(
- 1.0, description="The weight of the relationship to create."
- ),
- metadata: Optional[dict] = Body(
- None, description="The metadata of the relationship to create."
- ),
- auth_user=Depends(self.providers.auth.auth_wrapper()),
- ) -> WrappedRelationshipResponse:
- """Creates a new relationship in the graph."""
- if not auth_user.is_superuser:
- raise R2RException(
- "Only superusers can create relationships.", 403
- )
- if (
- # not auth_user.is_superuser
- collection_id
- not in auth_user.collection_ids
- ):
- raise R2RException(
- "The currently authenticated user does not have access to the collection associated with the given graph.",
- 403,
- )
- return await self.services.graph.create_relationship(
- subject=subject,
- subject_id=subject_id,
- predicate=predicate,
- object=object,
- object_id=object_id,
- description=description,
- weight=weight,
- metadata=metadata,
- parent_id=collection_id,
- )
- @self.router.post(
- "/graphs/{collection_id}/relationships/export",
- summary="Export graph relationships to CSV",
- dependencies=[Depends(self.rate_limit_dependency)],
- openapi_extra={
- "x-codeSamples": [
- {
- "lang": "Python",
- "source": textwrap.dedent(
- """
- from r2r import R2RClient
- client = R2RClient("http://localhost:7272")
- # when using auth, do client.login(...)
- response = client.graphs.export_entities(
- collection_id="b4ac4dd6-5f27-596e-a55b-7cf242ca30aa",
- output_path="export.csv",
- columns=["id", "title", "created_at"],
- include_header=True,
- )
- """
- ),
- },
- {
- "lang": "JavaScript",
- "source": textwrap.dedent(
- """
- const { r2rClient } = require("r2r-js");
- const client = new r2rClient("http://localhost:7272");
- function main() {
- await client.graphs.exportEntities({
- collectionId: "b4ac4dd6-5f27-596e-a55b-7cf242ca30aa",
- outputPath: "export.csv",
- columns: ["id", "title", "created_at"],
- includeHeader: true,
- });
- }
- main();
- """
- ),
- },
- {
- "lang": "CLI",
- "source": textwrap.dedent(
- """
- """
- ),
- },
- {
- "lang": "cURL",
- "source": textwrap.dedent(
- """
- curl -X POST "http://127.0.0.1:7272/v3/graphs/export_relationships" \
- -H "Authorization: Bearer YOUR_API_KEY" \
- -H "Content-Type: application/json" \
- -H "Accept: text/csv" \
- -d '{ "columns": ["id", "title", "created_at"], "include_header": true }' \
- --output export.csv
- """
- ),
- },
- ]
- },
- )
- @self.base_endpoint
- async def export_relationships(
- background_tasks: BackgroundTasks,
- collection_id: UUID = Path(
- ...,
- description="The ID of the document to export entities from.",
- ),
- columns: Optional[list[str]] = Body(
- None, description="Specific columns to export"
- ),
- filters: Optional[dict] = Body(
- None, description="Filters to apply to the export"
- ),
- include_header: Optional[bool] = Body(
- True, description="Whether to include column headers"
- ),
- auth_user=Depends(self.providers.auth.auth_wrapper()),
- ) -> FileResponse:
- """
- Export documents as a downloadable CSV file.
- """
- if not auth_user.is_superuser:
- raise R2RException(
- "Only a superuser can export data.",
- 403,
- )
- csv_file_path, temp_file = (
- await self.services.management.export_graph_relationships(
- id=collection_id,
- columns=columns,
- filters=filters,
- include_header=include_header,
- )
- )
- background_tasks.add_task(temp_file.close)
- return FileResponse(
- path=csv_file_path,
- media_type="text/csv",
- filename="documents_export.csv",
- )
- @self.router.get(
- "/graphs/{collection_id}/entities/{entity_id}",
- dependencies=[Depends(self.rate_limit_dependency)],
- openapi_extra={
- "x-codeSamples": [
- {
- "lang": "Python",
- "source": textwrap.dedent(
- """
- from r2r import R2RClient
- client = R2RClient()
- # when using auth, do client.login(...)
- response = client.graphs.get_entity(
- collection_id="d09dedb1-b2ab-48a5-b950-6e1f464d83e7",
- entity_id="d09dedb1-b2ab-48a5-b950-6e1f464d83e7"
- )
- """
- ),
- },
- {
- "lang": "JavaScript",
- "source": textwrap.dedent(
- """
- const { r2rClient } = require("r2r-js");
- const client = new r2rClient();
- function main() {
- const response = await client.graphs.get_entity({
- collectionId: "d09dedb1-b2ab-48a5-b950-6e1f464d83e7",
- entityId: "d09dedb1-b2ab-48a5-b950-6e1f464d83e7"
- });
- }
- main();
- """
- ),
- },
- ]
- },
- )
- @self.base_endpoint
- async def get_entity(
- collection_id: UUID = Path(
- ...,
- description="The collection ID corresponding to the graph containing the entity.",
- ),
- entity_id: UUID = Path(
- ..., description="The ID of the entity to retrieve."
- ),
- auth_user=Depends(self.providers.auth.auth_wrapper()),
- ) -> WrappedEntityResponse:
- """Retrieves a specific entity by its ID."""
- if (
- # not auth_user.is_superuser
- collection_id
- not in auth_user.collection_ids
- ):
- raise R2RException(
- "The currently authenticated user does not have access to the collection associated with the given graph.",
- 403,
- )
- result = await self.providers.database.graphs_handler.entities.get(
- parent_id=collection_id,
- store_type=StoreType.GRAPHS,
- offset=0,
- limit=1,
- entity_ids=[entity_id],
- )
- if len(result) == 0 or len(result[0]) == 0:
- raise R2RException("Entity not found", 404)
- return result[0][0]
- @self.router.post(
- "/graphs/{collection_id}/entities/{entity_id}",
- dependencies=[Depends(self.rate_limit_dependency)],
- )
- @self.base_endpoint
- async def update_entity(
- collection_id: UUID = Path(
- ...,
- description="The collection ID corresponding to the graph containing the entity.",
- ),
- entity_id: UUID = Path(
- ..., description="The ID of the entity to update."
- ),
- name: Optional[str] = Body(
- ..., description="The updated name of the entity."
- ),
- description: Optional[str] = Body(
- None, description="The updated description of the entity."
- ),
- category: Optional[str] = Body(
- None, description="The updated category of the entity."
- ),
- metadata: Optional[dict] = Body(
- None, description="The updated metadata of the entity."
- ),
- auth_user=Depends(self.providers.auth.auth_wrapper()),
- ) -> WrappedEntityResponse:
- """Updates an existing entity in the graph."""
- if not auth_user.is_superuser:
- raise R2RException(
- "Only superusers can update graph entities.", 403
- )
- if (
- # not auth_user.is_superuser
- collection_id
- not in auth_user.collection_ids
- ):
- raise R2RException(
- "The currently authenticated user does not have access to the collection associated with the given graph.",
- 403,
- )
- return await self.services.graph.update_entity(
- entity_id=entity_id,
- name=name,
- category=category,
- description=description,
- metadata=metadata,
- )
- @self.router.delete(
- "/graphs/{collection_id}/entities/{entity_id}",
- dependencies=[Depends(self.rate_limit_dependency)],
- summary="Remove an entity",
- openapi_extra={
- "x-codeSamples": [
- {
- "lang": "Python",
- "source": textwrap.dedent(
- """
- from r2r import R2RClient
- client = R2RClient()
- # when using auth, do client.login(...)
- response = client.graphs.remove_entity(
- collection_id="d09dedb1-b2ab-48a5-b950-6e1f464d83e7",
- entity_id="d09dedb1-b2ab-48a5-b950-6e1f464d83e7"
- )
- """
- ),
- },
- {
- "lang": "JavaScript",
- "source": textwrap.dedent(
- """
- const { r2rClient } = require("r2r-js");
- const client = new r2rClient();
- function main() {
- const response = await client.graphs.removeEntity({
- collectionId: "d09dedb1-b2ab-48a5-b950-6e1f464d83e7",
- entityId: "d09dedb1-b2ab-48a5-b950-6e1f464d83e7"
- });
- }
- main();
- """
- ),
- },
- ]
- },
- )
- @self.base_endpoint
- async def delete_entity(
- collection_id: UUID = Path(
- ...,
- description="The collection ID corresponding to the graph to remove the entity from.",
- ),
- entity_id: UUID = Path(
- ...,
- description="The ID of the entity to remove from the graph.",
- ),
- auth_user=Depends(self.providers.auth.auth_wrapper()),
- ) -> WrappedBooleanResponse:
- """Removes an entity from the graph."""
- if not auth_user.is_superuser:
- raise R2RException(
- "Only superusers can delete graph details.", 403
- )
- if (
- # not auth_user.is_superuser
- collection_id
- not in auth_user.collection_ids
- ):
- raise R2RException(
- "The currently authenticated user does not have access to the collection associated with the given graph.",
- 403,
- )
- await self.services.graph.delete_entity(
- parent_id=collection_id,
- entity_id=entity_id,
- )
- return GenericBooleanResponse(success=True) # type: ignore
- @self.router.get(
- "/graphs/{collection_id}/relationships",
- dependencies=[Depends(self.rate_limit_dependency)],
- description="Lists all relationships in the graph with pagination support.",
- openapi_extra={
- "x-codeSamples": [
- {
- "lang": "Python",
- "source": textwrap.dedent(
- """
- from r2r import R2RClient
- client = R2RClient()
- # when using auth, do client.login(...)
- response = client.graphs.list_relationships(collection_id="d09dedb1-b2ab-48a5-b950-6e1f464d83e7")
- """
- ),
- },
- {
- "lang": "JavaScript",
- "source": textwrap.dedent(
- """
- const { r2rClient } = require("r2r-js");
- const client = new r2rClient();
- function main() {
- const response = await client.graphs.listRelationships({
- collectionId: "d09dedb1-b2ab-48a5-b950-6e1f464d83e7",
- });
- }
- main();
- """
- ),
- },
- ],
- },
- )
- @self.base_endpoint
- async def get_relationships(
- collection_id: UUID = Path(
- ...,
- description="The collection ID corresponding to the graph to list relationships from.",
- ),
- offset: int = Query(
- 0,
- ge=0,
- description="Specifies the number of objects to skip. Defaults to 0.",
- ),
- limit: int = Query(
- 100,
- ge=1,
- le=1000,
- description="Specifies a limit on the number of objects to return, ranging between 1 and 100. Defaults to 100.",
- ),
- auth_user=Depends(self.providers.auth.auth_wrapper()),
- ) -> WrappedRelationshipsResponse:
- """
- Lists all relationships in the graph with pagination support.
- """
- if (
- # not auth_user.is_superuser
- collection_id
- not in auth_user.collection_ids
- ):
- raise R2RException(
- "The currently authenticated user does not have access to the collection associated with the given graph.",
- 403,
- )
- relationships, count = await self.services.graph.get_relationships(
- parent_id=collection_id,
- offset=offset,
- limit=limit,
- )
- return relationships, { # type: ignore
- "total_entries": count,
- }
- @self.router.get(
- "/graphs/{collection_id}/relationships/{relationship_id}",
- dependencies=[Depends(self.rate_limit_dependency)],
- description="Retrieves a specific relationship by its ID.",
- openapi_extra={
- "x-codeSamples": [
- {
- "lang": "Python",
- "source": textwrap.dedent(
- """
- from r2r import R2RClient
- client = R2RClient()
- # when using auth, do client.login(...)
- response = client.graphs.get_relationship(
- collection_id="d09dedb1-b2ab-48a5-b950-6e1f464d83e7",
- relationship_id="d09dedb1-b2ab-48a5-b950-6e1f464d83e7"
- )
- """
- ),
- },
- {
- "lang": "JavaScript",
- "source": textwrap.dedent(
- """
- const { r2rClient } = require("r2r-js");
- const client = new r2rClient();
- function main() {
- const response = await client.graphs.getRelationship({
- collectionId: "d09dedb1-b2ab-48a5-b950-6e1f464d83e7",
- relationshipId: "d09dedb1-b2ab-48a5-b950-6e1f464d83e7"
- });
- }
- main();
- """
- ),
- },
- ],
- },
- )
- @self.base_endpoint
- async def get_relationship(
- collection_id: UUID = Path(
- ...,
- description="The collection ID corresponding to the graph containing the relationship.",
- ),
- relationship_id: UUID = Path(
- ..., description="The ID of the relationship to retrieve."
- ),
- auth_user=Depends(self.providers.auth.auth_wrapper()),
- ) -> WrappedRelationshipResponse:
- """Retrieves a specific relationship by its ID."""
- if (
- # not auth_user.is_superuser
- collection_id
- not in auth_user.collection_ids
- ):
- raise R2RException(
- "The currently authenticated user does not have access to the collection associated with the given graph.",
- 403,
- )
- results = (
- await self.providers.database.graphs_handler.relationships.get(
- parent_id=collection_id,
- store_type=StoreType.GRAPHS,
- offset=0,
- limit=1,
- relationship_ids=[relationship_id],
- )
- )
- if len(results) == 0 or len(results[0]) == 0:
- raise R2RException("Relationship not found", 404)
- return results[0][0]
- @self.router.post(
- "/graphs/{collection_id}/relationships/{relationship_id}",
- dependencies=[Depends(self.rate_limit_dependency)],
- )
- @self.base_endpoint
- async def update_relationship(
- collection_id: UUID = Path(
- ...,
- description="The collection ID corresponding to the graph containing the relationship.",
- ),
- relationship_id: UUID = Path(
- ..., description="The ID of the relationship to update."
- ),
- subject: Optional[str] = Body(
- ..., description="The updated subject of the relationship."
- ),
- subject_id: Optional[UUID] = Body(
- ..., description="The updated subject ID of the relationship."
- ),
- predicate: Optional[str] = Body(
- ..., description="The updated predicate of the relationship."
- ),
- object: Optional[str] = Body(
- ..., description="The updated object of the relationship."
- ),
- object_id: Optional[UUID] = Body(
- ..., description="The updated object ID of the relationship."
- ),
- description: Optional[str] = Body(
- None,
- description="The updated description of the relationship.",
- ),
- weight: Optional[float] = Body(
- None, description="The updated weight of the relationship."
- ),
- metadata: Optional[dict] = Body(
- None, description="The updated metadata of the relationship."
- ),
- auth_user=Depends(self.providers.auth.auth_wrapper()),
- ) -> WrappedRelationshipResponse:
- """Updates an existing relationship in the graph."""
- if not auth_user.is_superuser:
- raise R2RException(
- "Only superusers can update graph details", 403
- )
- if (
- # not auth_user.is_superuser
- collection_id
- not in auth_user.collection_ids
- ):
- raise R2RException(
- "The currently authenticated user does not have access to the collection associated with the given graph.",
- 403,
- )
- return await self.services.graph.update_relationship(
- relationship_id=relationship_id,
- subject=subject,
- subject_id=subject_id,
- predicate=predicate,
- object=object,
- object_id=object_id,
- description=description,
- weight=weight,
- metadata=metadata,
- )
- @self.router.delete(
- "/graphs/{collection_id}/relationships/{relationship_id}",
- dependencies=[Depends(self.rate_limit_dependency)],
- description="Removes a relationship from the graph.",
- openapi_extra={
- "x-codeSamples": [
- {
- "lang": "Python",
- "source": textwrap.dedent(
- """
- from r2r import R2RClient
- client = R2RClient()
- # when using auth, do client.login(...)
- response = client.graphs.delete_relationship(
- collection_id="d09dedb1-b2ab-48a5-b950-6e1f464d83e7",
- relationship_id="d09dedb1-b2ab-48a5-b950-6e1f464d83e7"
- )
- """
- ),
- },
- {
- "lang": "JavaScript",
- "source": textwrap.dedent(
- """
- const { r2rClient } = require("r2r-js");
- const client = new r2rClient();
- function main() {
- const response = await client.graphs.deleteRelationship({
- collectionId: "d09dedb1-b2ab-48a5-b950-6e1f464d83e7",
- relationshipId: "d09dedb1-b2ab-48a5-b950-6e1f464d83e7"
- });
- }
- main();
- """
- ),
- },
- ],
- },
- )
- @self.base_endpoint
- async def delete_relationship(
- collection_id: UUID = Path(
- ...,
- description="The collection ID corresponding to the graph to remove the relationship from.",
- ),
- relationship_id: UUID = Path(
- ...,
- description="The ID of the relationship to remove from the graph.",
- ),
- auth_user=Depends(self.providers.auth.auth_wrapper()),
- ) -> WrappedBooleanResponse:
- """Removes a relationship from the graph."""
- if not auth_user.is_superuser:
- raise R2RException(
- "Only superusers can delete a relationship.", 403
- )
- if (
- not auth_user.is_superuser
- and collection_id not in auth_user.collection_ids
- ):
- raise R2RException(
- "The currently authenticated user does not have access to the collection associated with the given graph.",
- 403,
- )
- await self.services.graph.delete_relationship(
- parent_id=collection_id,
- relationship_id=relationship_id,
- )
- return GenericBooleanResponse(success=True) # type: ignore
- @self.router.post(
- "/graphs/{collection_id}/communities",
- dependencies=[Depends(self.rate_limit_dependency)],
- summary="Create a new community",
- openapi_extra={
- "x-codeSamples": [
- {
- "lang": "Python",
- "source": textwrap.dedent(
- """
- from r2r import R2RClient
- client = R2RClient()
- # when using auth, do client.login(...)
- response = client.graphs.create_community(
- collection_id="9fbe403b-c11c-5aae-8ade-ef22980c3ad1",
- name="My Community",
- summary="A summary of the community",
- findings=["Finding 1", "Finding 2"],
- rating=5,
- rating_explanation="This is a rating explanation",
- )
- """
- ),
- },
- {
- "lang": "JavaScript",
- "source": textwrap.dedent(
- """
- const { r2rClient } = require("r2r-js");
- const client = new r2rClient();
- function main() {
- const response = await client.graphs.createCommunity({
- collectionId: "9fbe403b-c11c-5aae-8ade-ef22980c3ad1",
- name: "My Community",
- summary: "A summary of the community",
- findings: ["Finding 1", "Finding 2"],
- rating: 5,
- ratingExplanation: "This is a rating explanation",
- });
- }
- main();
- """
- ),
- },
- ]
- },
- )
- @self.base_endpoint
- async def create_community(
- collection_id: UUID = Path(
- ...,
- description="The collection ID corresponding to the graph to create the community in.",
- ),
- name: str = Body(..., description="The name of the community"),
- summary: str = Body(..., description="A summary of the community"),
- findings: Optional[list[str]] = Body(
- default=[], description="Findings about the community"
- ),
- rating: Optional[float] = Body(
- default=5, ge=1, le=10, description="Rating between 1 and 10"
- ),
- rating_explanation: Optional[str] = Body(
- default="", description="Explanation for the rating"
- ),
- auth_user=Depends(self.providers.auth.auth_wrapper()),
- ) -> WrappedCommunityResponse:
- """
- Creates a new community in the graph.
- While communities are typically built automatically via the /graphs/{id}/communities/build endpoint,
- this endpoint allows you to manually create your own communities.
- This can be useful when you want to:
- - Define custom groupings of entities based on domain knowledge
- - Add communities that weren't detected by the automatic process
- - Create hierarchical organization structures
- - Tag groups of entities with specific metadata
- The created communities will be integrated with any existing automatically detected communities
- in the graph's community structure.
- """
- if not auth_user.is_superuser:
- raise R2RException(
- "Only superusers can create a community.", 403
- )
- if (
- not auth_user.is_superuser
- and collection_id not in auth_user.collection_ids
- ):
- raise R2RException(
- "The currently authenticated user does not have access to the collection associated with the given graph.",
- 403,
- )
- return await self.services.graph.create_community(
- parent_id=collection_id,
- name=name,
- summary=summary,
- findings=findings,
- rating=rating,
- rating_explanation=rating_explanation,
- )
- @self.router.get(
- "/graphs/{collection_id}/communities",
- dependencies=[Depends(self.rate_limit_dependency)],
- summary="List communities",
- openapi_extra={
- "x-codeSamples": [
- {
- "lang": "Python",
- "source": textwrap.dedent(
- """
- from r2r import R2RClient
- client = R2RClient()
- # when using auth, do client.login(...)
- response = client.graphs.list_communities(collection_id="9fbe403b-c11c-5aae-8ade-ef22980c3ad1")
- """
- ),
- },
- {
- "lang": "JavaScript",
- "source": textwrap.dedent(
- """
- const { r2rClient } = require("r2r-js");
- const client = new r2rClient();
- function main() {
- const response = await client.graphs.listCommunities({
- collectionId: "9fbe403b-c11c-5aae-8ade-ef22980c3ad1",
- });
- }
- main();
- """
- ),
- },
- ]
- },
- )
- @self.base_endpoint
- async def get_communities(
- collection_id: UUID = Path(
- ...,
- description="The collection ID corresponding to the graph to get communities for.",
- ),
- offset: int = Query(
- 0,
- ge=0,
- description="Specifies the number of objects to skip. Defaults to 0.",
- ),
- limit: int = Query(
- 100,
- ge=1,
- le=1000,
- description="Specifies a limit on the number of objects to return, ranging between 1 and 100. Defaults to 100.",
- ),
- auth_user=Depends(self.providers.auth.auth_wrapper()),
- ) -> WrappedCommunitiesResponse:
- """
- Lists all communities in the graph with pagination support.
- """
- if (
- # not auth_user.is_superuser
- collection_id
- not in auth_user.collection_ids
- ):
- raise R2RException(
- "The currently authenticated user does not have access to the collection associated with the given graph.",
- 403,
- )
- communities, count = await self.services.graph.get_communities(
- parent_id=collection_id,
- offset=offset,
- limit=limit,
- )
- return communities, { # type: ignore
- "total_entries": count,
- }
- @self.router.get(
- "/graphs/{collection_id}/communities/{community_id}",
- dependencies=[Depends(self.rate_limit_dependency)],
- summary="Retrieve a community",
- openapi_extra={
- "x-codeSamples": [
- {
- "lang": "Python",
- "source": textwrap.dedent(
- """
- from r2r import R2RClient
- client = R2RClient()
- # when using auth, do client.login(...)
- response = client.graphs.get_community(collection_id="9fbe403b-c11c-5aae-8ade-ef22980c3ad1")
- """
- ),
- },
- {
- "lang": "JavaScript",
- "source": textwrap.dedent(
- """
- const { r2rClient } = require("r2r-js");
- const client = new r2rClient();
- function main() {
- const response = await client.graphs.getCommunity({
- collectionId: "9fbe403b-c11c-5aae-8ade-ef22980c3ad1",
- });
- }
- main();
- """
- ),
- },
- ]
- },
- )
- @self.base_endpoint
- async def get_community(
- collection_id: UUID = Path(
- ...,
- description="The ID of the collection to get communities for.",
- ),
- community_id: UUID = Path(
- ...,
- description="The ID of the community to get.",
- ),
- auth_user=Depends(self.providers.auth.auth_wrapper()),
- ) -> WrappedCommunityResponse:
- """
- Retrieves a specific community by its ID.
- """
- if (
- # not auth_user.is_superuser
- collection_id
- not in auth_user.collection_ids
- ):
- raise R2RException(
- "The currently authenticated user does not have access to the collection associated with the given graph.",
- 403,
- )
- results = (
- await self.providers.database.graphs_handler.communities.get(
- parent_id=collection_id,
- community_ids=[community_id],
- store_type=StoreType.GRAPHS,
- offset=0,
- limit=1,
- )
- )
- if len(results) == 0 or len(results[0]) == 0:
- raise R2RException("Community not found", 404)
- return results[0][0]
- @self.router.delete(
- "/graphs/{collection_id}/communities/{community_id}",
- dependencies=[Depends(self.rate_limit_dependency)],
- summary="Delete a community",
- openapi_extra={
- "x-codeSamples": [
- {
- "lang": "Python",
- "source": textwrap.dedent(
- """
- from r2r import R2RClient
- client = R2RClient()
- # when using auth, do client.login(...)
- response = client.graphs.delete_community(
- collection_id="d09dedb1-b2ab-48a5-b950-6e1f464d83e7",
- community_id="d09dedb1-b2ab-48a5-b950-6e1f464d83e7"
- )
- """
- ),
- },
- {
- "lang": "JavaScript",
- "source": textwrap.dedent(
- """
- const { r2rClient } = require("r2r-js");
- const client = new r2rClient();
- function main() {
- const response = await client.graphs.deleteCommunity({
- collectionId: "d09dedb1-b2ab-48a5-b950-6e1f464d83e7",
- communityId: "d09dedb1-b2ab-48a5-b950-6e1f464d83e7"
- });
- }
- main();
- """
- ),
- },
- ]
- },
- )
- @self.base_endpoint
- async def delete_community(
- collection_id: UUID = Path(
- ...,
- description="The collection ID corresponding to the graph to delete the community from.",
- ),
- community_id: UUID = Path(
- ...,
- description="The ID of the community to delete.",
- ),
- auth_user=Depends(self.providers.auth.auth_wrapper()),
- ):
- if (
- not auth_user.is_superuser
- and collection_id not in auth_user.graph_ids
- ):
- raise R2RException(
- "Only superusers can delete communities", 403
- )
- if (
- # not auth_user.is_superuser
- collection_id
- not in auth_user.collection_ids
- ):
- raise R2RException(
- "The currently authenticated user does not have access to the collection associated with the given graph.",
- 403,
- )
- await self.services.graph.delete_community(
- parent_id=collection_id,
- community_id=community_id,
- )
- return GenericBooleanResponse(success=True) # type: ignore
- @self.router.post(
- "/graphs/{collection_id}/communities/export",
- summary="Export document communities to CSV",
- dependencies=[Depends(self.rate_limit_dependency)],
- openapi_extra={
- "x-codeSamples": [
- {
- "lang": "Python",
- "source": textwrap.dedent(
- """
- from r2r import R2RClient
- client = R2RClient("http://localhost:7272")
- # when using auth, do client.login(...)
- response = client.graphs.export_communities(
- collection_id="b4ac4dd6-5f27-596e-a55b-7cf242ca30aa",
- output_path="export.csv",
- columns=["id", "title", "created_at"],
- include_header=True,
- )
- """
- ),
- },
- {
- "lang": "JavaScript",
- "source": textwrap.dedent(
- """
- const { r2rClient } = require("r2r-js");
- const client = new r2rClient("http://localhost:7272");
- function main() {
- await client.graphs.exportCommunities({
- collectionId: "b4ac4dd6-5f27-596e-a55b-7cf242ca30aa",
- outputPath: "export.csv",
- columns: ["id", "title", "created_at"],
- includeHeader: true,
- });
- }
- main();
- """
- ),
- },
- {
- "lang": "CLI",
- "source": textwrap.dedent(
- """
- """
- ),
- },
- {
- "lang": "cURL",
- "source": textwrap.dedent(
- """
- curl -X POST "http://127.0.0.1:7272/v3/graphs/export_communities" \
- -H "Authorization: Bearer YOUR_API_KEY" \
- -H "Content-Type: application/json" \
- -H "Accept: text/csv" \
- -d '{ "columns": ["id", "title", "created_at"], "include_header": true }' \
- --output export.csv
- """
- ),
- },
- ]
- },
- )
- @self.base_endpoint
- async def export_relationships(
- background_tasks: BackgroundTasks,
- collection_id: UUID = Path(
- ...,
- description="The ID of the document to export entities from.",
- ),
- columns: Optional[list[str]] = Body(
- None, description="Specific columns to export"
- ),
- filters: Optional[dict] = Body(
- None, description="Filters to apply to the export"
- ),
- include_header: Optional[bool] = Body(
- True, description="Whether to include column headers"
- ),
- auth_user=Depends(self.providers.auth.auth_wrapper()),
- ) -> FileResponse:
- """
- Export documents as a downloadable CSV file.
- """
- if not auth_user.is_superuser:
- raise R2RException(
- "Only a superuser can export data.",
- 403,
- )
- csv_file_path, temp_file = (
- await self.services.management.export_graph_communities(
- id=collection_id,
- columns=columns,
- filters=filters,
- include_header=include_header,
- )
- )
- background_tasks.add_task(temp_file.close)
- return FileResponse(
- path=csv_file_path,
- media_type="text/csv",
- filename="documents_export.csv",
- )
- @self.router.post(
- "/graphs/{collection_id}/communities/{community_id}",
- dependencies=[Depends(self.rate_limit_dependency)],
- summary="Update community",
- openapi_extra={
- "x-codeSamples": [
- {
- "lang": "Python",
- "source": textwrap.dedent(
- """
- from r2r import R2RClient
- client = R2RClient()
- # when using auth, do client.login(...)
- response = client.graphs.update_community(
- collection_id="d09dedb1-b2ab-48a5-b950-6e1f464d83e7",
- community_update={
- "metadata": {
- "topic": "Technology",
- "description": "Tech companies and products"
- }
- }
- )"""
- ),
- },
- {
- "lang": "JavaScript",
- "source": textwrap.dedent(
- """
- const { r2rClient } = require("r2r-js");
- const client = new r2rClient();
- async function main() {
- const response = await client.graphs.updateCommunity({
- collectionId: "d09dedb1-b2ab-48a5-b950-6e1f464d83e7",
- communityId: "d09dedb1-b2ab-48a5-b950-6e1f464d83e7",
- communityUpdate: {
- metadata: {
- topic: "Technology",
- description: "Tech companies and products"
- }
- }
- });
- }
- main();
- """
- ),
- },
- ]
- },
- )
- @self.base_endpoint
- async def update_community(
- collection_id: UUID = Path(...),
- community_id: UUID = Path(...),
- name: Optional[str] = Body(None),
- summary: Optional[str] = Body(None),
- findings: Optional[list[str]] = Body(None),
- rating: Optional[float] = Body(default=None, ge=1, le=10),
- rating_explanation: Optional[str] = Body(None),
- auth_user=Depends(self.providers.auth.auth_wrapper()),
- ) -> WrappedCommunityResponse:
- """
- Updates an existing community in the graph.
- """
- if (
- not auth_user.is_superuser
- and collection_id not in auth_user.graph_ids
- ):
- raise R2RException(
- "Only superusers can update communities.", 403
- )
- if (
- # not auth_user.is_superuser
- collection_id
- not in auth_user.collection_ids
- ):
- raise R2RException(
- "The currently authenticated user does not have access to the collection associated with the given graph.",
- 403,
- )
- return await self.services.graph.update_community(
- community_id=community_id,
- name=name,
- summary=summary,
- findings=findings,
- rating=rating,
- rating_explanation=rating_explanation,
- )
- @self.router.post(
- "/graphs/{collection_id}/pull",
- dependencies=[Depends(self.rate_limit_dependency)],
- summary="Pull latest entities to the graph",
- openapi_extra={
- "x-codeSamples": [
- {
- "lang": "Python",
- "source": textwrap.dedent(
- """
- from r2r import R2RClient
- client = R2RClient()
- # when using auth, do client.login(...)
- response = client.graphs.pull(
- collection_id="d09dedb1-b2ab-48a5-b950-6e1f464d83e7"
- )"""
- ),
- },
- {
- "lang": "JavaScript",
- "source": textwrap.dedent(
- """
- const { r2rClient } = require("r2r-js");
- const client = new r2rClient();
- async function main() {
- const response = await client.graphs.pull({
- collection_id: "d09dedb1-b2ab-48a5-b950-6e1f464d83e7"
- });
- }
- main();
- """
- ),
- },
- ]
- },
- )
- @self.base_endpoint
- async def pull(
- collection_id: UUID = Path(
- ..., description="The ID of the graph to initialize."
- ),
- force: Optional[bool] = Body(
- False,
- description="If true, forces a re-pull of all entities and relationships.",
- ),
- # document_ids: list[UUID] = Body(
- # ..., description="List of document IDs to add to the graph."
- # ),
- auth_user=Depends(self.providers.auth.auth_wrapper()),
- ) -> WrappedBooleanResponse:
- """
- Adds documents to a graph by copying their entities and relationships.
- This endpoint:
- 1. Copies document entities to the graphs_entities table
- 2. Copies document relationships to the graphs_relationships table
- 3. Associates the documents with the graph
- When a document is added:
- - Its entities and relationships are copied to graph-specific tables
- - Existing entities/relationships are updated by merging their properties
- - The document ID is recorded in the graph's document_ids array
- Documents added to a graph will contribute their knowledge to:
- - Graph analysis and querying
- - Community detection
- - Knowledge graph enrichment
- The user must have access to both the graph and the documents being added.
- """
- # Check user permissions for graph
- if not auth_user.is_superuser:
- raise R2RException("Only superusers can `pull` a graph.", 403)
- if (
- # not auth_user.is_superuser
- collection_id
- not in auth_user.collection_ids
- ):
- raise R2RException(
- "The currently authenticated user does not have access to the collection associated with the given graph.",
- 403,
- )
- list_graphs_response = await self.services.graph.list_graphs(
- # user_ids=None,
- graph_ids=[collection_id],
- offset=0,
- limit=1,
- )
- if len(list_graphs_response["results"]) == 0:
- raise R2RException("Graph not found", 404)
- collection_id = list_graphs_response["results"][0].collection_id
- documents = []
- document_req = (
- await self.providers.database.collections_handler.documents_in_collection(
- collection_id, offset=0, limit=100
- )
- )["results"]
- documents.extend(document_req)
- while len(document_req) == 100:
- document_req = (
- await self.providers.database.collections_handler.documents_in_collection(
- collection_id, offset=len(documents), limit=100
- )
- )["results"]
- documents.extend(document_req)
- success = False
- for document in documents:
- # TODO - Add better checks for user permissions
- if (
- not auth_user.is_superuser
- and document.id
- not in auth_user.document_ids # TODO - extend to include checks on collections
- ):
- raise R2RException(
- f"The currently authenticated user does not have access to document {document.id}",
- 403,
- )
- entities = (
- await self.providers.database.graphs_handler.entities.get(
- parent_id=document.id,
- store_type=StoreType.DOCUMENTS,
- offset=0,
- limit=100,
- )
- )
- has_document = (
- await self.providers.database.graphs_handler.has_document(
- collection_id, document.id
- )
- )
- if has_document:
- logger.info(
- f"Document {document.id} is already in graph {collection_id}, skipping."
- )
- continue
- if len(entities[0]) == 0:
- if not force:
- logger.warning(
- f"Document {document.id} has no entities, extraction may not have been called, skipping."
- )
- continue
- else:
- logger.warning(
- f"Document {document.id} has no entities, but force=True, continuing."
- )
- success = (
- await self.providers.database.graphs_handler.add_documents(
- id=collection_id,
- document_ids=[document.id],
- )
- )
- if not success:
- logger.warning(
- f"No documents were added to graph {collection_id}, marking as failed."
- )
- if success:
- await self.providers.database.documents_handler.set_workflow_status(
- id=collection_id,
- status_type="graph_sync_status",
- status=KGEnrichmentStatus.SUCCESS,
- )
- return GenericBooleanResponse(success=success) # type: ignore
|