2024-08-18 13:22:31 -04:00
|
|
|
import asyncio
|
|
|
|
|
import logging
|
2024-08-26 10:30:22 -04:00
|
|
|
import re
|
2024-08-23 08:15:44 -07:00
|
|
|
import typing
|
2024-08-22 14:26:26 -04:00
|
|
|
from collections import defaultdict
|
2024-08-18 13:22:31 -04:00
|
|
|
from datetime import datetime
|
2024-08-21 12:03:32 -04:00
|
|
|
from time import time
|
2024-08-18 13:22:31 -04:00
|
|
|
|
|
|
|
|
from neo4j import AsyncDriver
|
2024-08-22 17:24:59 -04:00
|
|
|
from neo4j import time as neo4j_time
|
2024-08-18 13:22:31 -04:00
|
|
|
|
2024-08-25 10:07:50 -07:00
|
|
|
from graphiti_core.edges import EntityEdge
|
|
|
|
|
from graphiti_core.nodes import EntityNode, EpisodicNode
|
2024-08-18 13:22:31 -04:00
|
|
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
2024-08-21 12:03:32 -04:00
|
|
|
RELEVANT_SCHEMA_LIMIT = 3
|
|
|
|
|
|
2024-08-18 13:22:31 -04:00
|
|
|
|
2024-08-23 08:15:44 -07:00
|
|
|
def parse_db_date(neo_date: neo4j_time.DateTime | None) -> datetime | None:
|
2024-08-23 14:18:45 -04:00
|
|
|
return neo_date.to_native() if neo_date else None
|
2024-08-22 17:24:59 -04:00
|
|
|
|
|
|
|
|
|
2024-08-22 14:26:26 -04:00
|
|
|
async def get_mentioned_nodes(driver: AsyncDriver, episodes: list[EpisodicNode]):
|
2024-08-23 14:18:45 -04:00
|
|
|
episode_uuids = [episode.uuid for episode in episodes]
|
|
|
|
|
records, _, _ = await driver.execute_query(
|
|
|
|
|
"""
|
2024-08-22 14:26:26 -04:00
|
|
|
MATCH (episode:Episodic)-[:MENTIONS]->(n:Entity) WHERE episode.uuid IN $uuids
|
|
|
|
|
RETURN DISTINCT
|
|
|
|
|
n.uuid As uuid,
|
|
|
|
|
n.name AS name,
|
|
|
|
|
n.created_at AS created_at,
|
|
|
|
|
n.summary AS summary
|
|
|
|
|
""",
|
2024-08-23 14:18:45 -04:00
|
|
|
uuids=episode_uuids,
|
|
|
|
|
)
|
2024-08-22 14:26:26 -04:00
|
|
|
|
2024-08-23 14:18:45 -04:00
|
|
|
nodes: list[EntityNode] = []
|
2024-08-22 14:26:26 -04:00
|
|
|
|
2024-08-23 14:18:45 -04:00
|
|
|
for record in records:
|
|
|
|
|
nodes.append(
|
|
|
|
|
EntityNode(
|
|
|
|
|
uuid=record['uuid'],
|
|
|
|
|
name=record['name'],
|
|
|
|
|
labels=['Entity'],
|
|
|
|
|
created_at=record['created_at'].to_native(),
|
|
|
|
|
summary=record['summary'],
|
|
|
|
|
)
|
|
|
|
|
)
|
2024-08-22 14:26:26 -04:00
|
|
|
|
2024-08-23 14:18:45 -04:00
|
|
|
return nodes
|
2024-08-22 14:26:26 -04:00
|
|
|
|
|
|
|
|
|
2024-08-18 13:22:31 -04:00
|
|
|
async def bfs(node_ids: list[str], driver: AsyncDriver):
|
2024-08-23 14:18:45 -04:00
|
|
|
records, _, _ = await driver.execute_query(
|
|
|
|
|
"""
|
2024-08-18 13:22:31 -04:00
|
|
|
MATCH (n WHERE n.uuid in $node_ids)-[r]->(m)
|
2024-08-22 14:26:26 -04:00
|
|
|
RETURN DISTINCT
|
2024-08-18 13:22:31 -04:00
|
|
|
n.uuid AS source_node_uuid,
|
|
|
|
|
n.name AS source_name,
|
|
|
|
|
n.summary AS source_summary,
|
|
|
|
|
m.uuid AS target_node_uuid,
|
|
|
|
|
m.name AS target_name,
|
|
|
|
|
m.summary AS target_summary,
|
|
|
|
|
r.uuid AS uuid,
|
|
|
|
|
r.created_at AS created_at,
|
|
|
|
|
r.name AS name,
|
|
|
|
|
r.fact AS fact,
|
|
|
|
|
r.fact_embedding AS fact_embedding,
|
|
|
|
|
r.episodes AS episodes,
|
|
|
|
|
r.expired_at AS expired_at,
|
|
|
|
|
r.valid_at AS valid_at,
|
|
|
|
|
r.invalid_at AS invalid_at
|
|
|
|
|
|
|
|
|
|
""",
|
2024-08-23 14:18:45 -04:00
|
|
|
node_ids=node_ids,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
context: dict[str, typing.Any] = {}
|
|
|
|
|
|
|
|
|
|
for record in records:
|
|
|
|
|
n_uuid = record['source_node_uuid']
|
|
|
|
|
if n_uuid in context:
|
|
|
|
|
context[n_uuid]['facts'].append(record['fact'])
|
|
|
|
|
else:
|
|
|
|
|
context[n_uuid] = {
|
|
|
|
|
'name': record['source_name'],
|
|
|
|
|
'summary': record['source_summary'],
|
|
|
|
|
'facts': [record['fact']],
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
m_uuid = record['target_node_uuid']
|
|
|
|
|
if m_uuid not in context:
|
|
|
|
|
context[m_uuid] = {
|
|
|
|
|
'name': record['target_name'],
|
|
|
|
|
'summary': record['target_summary'],
|
|
|
|
|
'facts': [],
|
|
|
|
|
}
|
|
|
|
|
logger.info(f'bfs search returned context: {context}')
|
|
|
|
|
return context
|
2024-08-18 13:22:31 -04:00
|
|
|
|
|
|
|
|
|
|
|
|
|
async def edge_similarity_search(
|
2024-08-23 14:18:45 -04:00
|
|
|
search_vector: list[float], driver: AsyncDriver, limit=RELEVANT_SCHEMA_LIMIT
|
2024-08-18 13:22:31 -04:00
|
|
|
) -> list[EntityEdge]:
|
2024-08-23 14:18:45 -04:00
|
|
|
# vector similarity search over embedded facts
|
|
|
|
|
records, _, _ = await driver.execute_query(
|
|
|
|
|
"""
|
2024-08-18 13:22:31 -04:00
|
|
|
CALL db.index.vector.queryRelationships("fact_embedding", 5, $search_vector)
|
|
|
|
|
YIELD relationship AS r, score
|
|
|
|
|
MATCH (n)-[r:RELATES_TO]->(m)
|
|
|
|
|
RETURN
|
|
|
|
|
r.uuid AS uuid,
|
|
|
|
|
n.uuid AS source_node_uuid,
|
|
|
|
|
m.uuid AS target_node_uuid,
|
|
|
|
|
r.created_at AS created_at,
|
|
|
|
|
r.name AS name,
|
|
|
|
|
r.fact AS fact,
|
|
|
|
|
r.fact_embedding AS fact_embedding,
|
|
|
|
|
r.episodes AS episodes,
|
|
|
|
|
r.expired_at AS expired_at,
|
|
|
|
|
r.valid_at AS valid_at,
|
|
|
|
|
r.invalid_at AS invalid_at
|
2024-08-21 12:03:32 -04:00
|
|
|
ORDER BY score DESC LIMIT $limit
|
2024-08-18 13:22:31 -04:00
|
|
|
""",
|
2024-08-23 14:18:45 -04:00
|
|
|
search_vector=search_vector,
|
|
|
|
|
limit=limit,
|
|
|
|
|
)
|
2024-08-18 13:22:31 -04:00
|
|
|
|
2024-08-23 14:18:45 -04:00
|
|
|
edges: list[EntityEdge] = []
|
2024-08-18 13:22:31 -04:00
|
|
|
|
2024-08-23 14:18:45 -04:00
|
|
|
for record in records:
|
|
|
|
|
edge = EntityEdge(
|
|
|
|
|
uuid=record['uuid'],
|
|
|
|
|
source_node_uuid=record['source_node_uuid'],
|
|
|
|
|
target_node_uuid=record['target_node_uuid'],
|
|
|
|
|
fact=record['fact'],
|
|
|
|
|
name=record['name'],
|
|
|
|
|
episodes=record['episodes'],
|
|
|
|
|
fact_embedding=record['fact_embedding'],
|
|
|
|
|
created_at=record['created_at'].to_native(),
|
|
|
|
|
expired_at=parse_db_date(record['expired_at']),
|
|
|
|
|
valid_at=parse_db_date(record['valid_at']),
|
|
|
|
|
invalid_at=parse_db_date(record['invalid_at']),
|
|
|
|
|
)
|
2024-08-18 13:22:31 -04:00
|
|
|
|
2024-08-23 14:18:45 -04:00
|
|
|
edges.append(edge)
|
2024-08-18 13:22:31 -04:00
|
|
|
|
2024-08-23 14:18:45 -04:00
|
|
|
return edges
|
2024-08-18 13:22:31 -04:00
|
|
|
|
|
|
|
|
|
|
|
|
|
async def entity_similarity_search(
|
2024-08-23 14:18:45 -04:00
|
|
|
search_vector: list[float], driver: AsyncDriver, limit=RELEVANT_SCHEMA_LIMIT
|
2024-08-18 13:22:31 -04:00
|
|
|
) -> list[EntityNode]:
|
2024-08-23 14:18:45 -04:00
|
|
|
# vector similarity search over entity names
|
|
|
|
|
records, _, _ = await driver.execute_query(
|
|
|
|
|
"""
|
2024-08-21 12:03:32 -04:00
|
|
|
CALL db.index.vector.queryNodes("name_embedding", $limit, $search_vector)
|
2024-08-18 13:22:31 -04:00
|
|
|
YIELD node AS n, score
|
|
|
|
|
RETURN
|
|
|
|
|
n.uuid As uuid,
|
|
|
|
|
n.name AS name,
|
|
|
|
|
n.created_at AS created_at,
|
|
|
|
|
n.summary AS summary
|
|
|
|
|
ORDER BY score DESC
|
|
|
|
|
""",
|
2024-08-23 14:18:45 -04:00
|
|
|
search_vector=search_vector,
|
|
|
|
|
limit=limit,
|
|
|
|
|
)
|
|
|
|
|
nodes: list[EntityNode] = []
|
2024-08-18 13:22:31 -04:00
|
|
|
|
2024-08-23 14:18:45 -04:00
|
|
|
for record in records:
|
|
|
|
|
nodes.append(
|
|
|
|
|
EntityNode(
|
|
|
|
|
uuid=record['uuid'],
|
|
|
|
|
name=record['name'],
|
|
|
|
|
labels=['Entity'],
|
|
|
|
|
created_at=record['created_at'].to_native(),
|
|
|
|
|
summary=record['summary'],
|
|
|
|
|
)
|
|
|
|
|
)
|
2024-08-18 13:22:31 -04:00
|
|
|
|
2024-08-23 14:18:45 -04:00
|
|
|
return nodes
|
2024-08-18 13:22:31 -04:00
|
|
|
|
|
|
|
|
|
2024-08-21 12:03:32 -04:00
|
|
|
async def entity_fulltext_search(
|
2024-08-23 14:18:45 -04:00
|
|
|
query: str, driver: AsyncDriver, limit=RELEVANT_SCHEMA_LIMIT
|
2024-08-21 12:03:32 -04:00
|
|
|
) -> list[EntityNode]:
|
2024-08-23 14:18:45 -04:00
|
|
|
# BM25 search to get top nodes
|
2024-08-26 10:30:22 -04:00
|
|
|
fuzzy_query = re.sub(r'[^\w\s]', '', query) + '~'
|
2024-08-23 14:18:45 -04:00
|
|
|
records, _, _ = await driver.execute_query(
|
|
|
|
|
"""
|
2024-08-18 13:22:31 -04:00
|
|
|
CALL db.index.fulltext.queryNodes("name_and_summary", $query) YIELD node, score
|
2024-08-22 14:26:26 -04:00
|
|
|
RETURN
|
2024-08-18 13:22:31 -04:00
|
|
|
node.uuid As uuid,
|
|
|
|
|
node.name AS name,
|
|
|
|
|
node.created_at AS created_at,
|
|
|
|
|
node.summary AS summary
|
|
|
|
|
ORDER BY score DESC
|
2024-08-21 12:03:32 -04:00
|
|
|
LIMIT $limit
|
2024-08-18 13:22:31 -04:00
|
|
|
""",
|
2024-08-23 14:18:45 -04:00
|
|
|
query=fuzzy_query,
|
|
|
|
|
limit=limit,
|
|
|
|
|
)
|
|
|
|
|
nodes: list[EntityNode] = []
|
2024-08-18 13:22:31 -04:00
|
|
|
|
2024-08-23 14:18:45 -04:00
|
|
|
for record in records:
|
|
|
|
|
nodes.append(
|
|
|
|
|
EntityNode(
|
|
|
|
|
uuid=record['uuid'],
|
|
|
|
|
name=record['name'],
|
|
|
|
|
labels=['Entity'],
|
|
|
|
|
created_at=record['created_at'].to_native(),
|
|
|
|
|
summary=record['summary'],
|
|
|
|
|
)
|
|
|
|
|
)
|
2024-08-18 13:22:31 -04:00
|
|
|
|
2024-08-23 14:18:45 -04:00
|
|
|
return nodes
|
2024-08-18 13:22:31 -04:00
|
|
|
|
|
|
|
|
|
2024-08-21 12:03:32 -04:00
|
|
|
async def edge_fulltext_search(
|
2024-08-23 14:18:45 -04:00
|
|
|
query: str, driver: AsyncDriver, limit=RELEVANT_SCHEMA_LIMIT
|
2024-08-21 12:03:32 -04:00
|
|
|
) -> list[EntityEdge]:
|
2024-08-23 14:18:45 -04:00
|
|
|
# fulltext search over facts
|
2024-08-26 10:30:22 -04:00
|
|
|
fuzzy_query = re.sub(r'[^\w\s]', '', query) + '~'
|
2024-08-18 13:22:31 -04:00
|
|
|
|
2024-08-23 14:18:45 -04:00
|
|
|
records, _, _ = await driver.execute_query(
|
|
|
|
|
"""
|
2024-08-18 13:22:31 -04:00
|
|
|
CALL db.index.fulltext.queryRelationships("name_and_fact", $query)
|
|
|
|
|
YIELD relationship AS r, score
|
|
|
|
|
MATCH (n:Entity)-[r]->(m:Entity)
|
2024-08-22 14:26:26 -04:00
|
|
|
RETURN
|
2024-08-18 13:22:31 -04:00
|
|
|
r.uuid AS uuid,
|
|
|
|
|
n.uuid AS source_node_uuid,
|
|
|
|
|
m.uuid AS target_node_uuid,
|
|
|
|
|
r.created_at AS created_at,
|
|
|
|
|
r.name AS name,
|
|
|
|
|
r.fact AS fact,
|
|
|
|
|
r.fact_embedding AS fact_embedding,
|
|
|
|
|
r.episodes AS episodes,
|
|
|
|
|
r.expired_at AS expired_at,
|
|
|
|
|
r.valid_at AS valid_at,
|
|
|
|
|
r.invalid_at AS invalid_at
|
2024-08-21 12:03:32 -04:00
|
|
|
ORDER BY score DESC LIMIT $limit
|
2024-08-18 13:22:31 -04:00
|
|
|
""",
|
2024-08-23 14:18:45 -04:00
|
|
|
query=fuzzy_query,
|
|
|
|
|
limit=limit,
|
|
|
|
|
)
|
2024-08-18 13:22:31 -04:00
|
|
|
|
2024-08-23 14:18:45 -04:00
|
|
|
edges: list[EntityEdge] = []
|
2024-08-18 13:22:31 -04:00
|
|
|
|
2024-08-23 14:18:45 -04:00
|
|
|
for record in records:
|
|
|
|
|
edge = EntityEdge(
|
|
|
|
|
uuid=record['uuid'],
|
|
|
|
|
source_node_uuid=record['source_node_uuid'],
|
|
|
|
|
target_node_uuid=record['target_node_uuid'],
|
|
|
|
|
fact=record['fact'],
|
|
|
|
|
name=record['name'],
|
|
|
|
|
episodes=record['episodes'],
|
|
|
|
|
fact_embedding=record['fact_embedding'],
|
|
|
|
|
created_at=record['created_at'].to_native(),
|
|
|
|
|
expired_at=parse_db_date(record['expired_at']),
|
|
|
|
|
valid_at=parse_db_date(record['valid_at']),
|
|
|
|
|
invalid_at=parse_db_date(record['invalid_at']),
|
|
|
|
|
)
|
2024-08-18 13:22:31 -04:00
|
|
|
|
2024-08-23 14:18:45 -04:00
|
|
|
edges.append(edge)
|
2024-08-18 13:22:31 -04:00
|
|
|
|
2024-08-23 14:18:45 -04:00
|
|
|
return edges
|
2024-08-18 13:22:31 -04:00
|
|
|
|
|
|
|
|
|
|
|
|
|
async def get_relevant_nodes(
|
2024-08-23 14:18:45 -04:00
|
|
|
nodes: list[EntityNode],
|
|
|
|
|
driver: AsyncDriver,
|
2024-08-18 13:22:31 -04:00
|
|
|
) -> list[EntityNode]:
|
2024-08-23 14:18:45 -04:00
|
|
|
start = time()
|
|
|
|
|
relevant_nodes: list[EntityNode] = []
|
|
|
|
|
relevant_node_uuids = set()
|
2024-08-18 13:22:31 -04:00
|
|
|
|
2024-08-23 14:18:45 -04:00
|
|
|
results = await asyncio.gather(
|
|
|
|
|
*[entity_fulltext_search(node.name, driver) for node in nodes],
|
|
|
|
|
*[
|
|
|
|
|
entity_similarity_search(node.name_embedding, driver)
|
|
|
|
|
for node in nodes
|
|
|
|
|
if node.name_embedding is not None
|
|
|
|
|
],
|
|
|
|
|
)
|
2024-08-18 13:22:31 -04:00
|
|
|
|
2024-08-23 14:18:45 -04:00
|
|
|
for result in results:
|
|
|
|
|
for node in result:
|
|
|
|
|
if node.uuid in relevant_node_uuids:
|
|
|
|
|
continue
|
2024-08-21 12:03:32 -04:00
|
|
|
|
2024-08-23 14:18:45 -04:00
|
|
|
relevant_node_uuids.add(node.uuid)
|
|
|
|
|
relevant_nodes.append(node)
|
2024-08-18 13:22:31 -04:00
|
|
|
|
2024-08-23 14:18:45 -04:00
|
|
|
end = time()
|
|
|
|
|
logger.info(f'Found relevant nodes: {relevant_node_uuids} in {(end - start) * 1000} ms')
|
2024-08-18 13:22:31 -04:00
|
|
|
|
2024-08-23 14:18:45 -04:00
|
|
|
return relevant_nodes
|
2024-08-18 13:22:31 -04:00
|
|
|
|
|
|
|
|
|
|
|
|
|
async def get_relevant_edges(
|
2024-08-23 14:18:45 -04:00
|
|
|
edges: list[EntityEdge],
|
|
|
|
|
driver: AsyncDriver,
|
2024-08-18 13:22:31 -04:00
|
|
|
) -> list[EntityEdge]:
|
2024-08-23 14:18:45 -04:00
|
|
|
start = time()
|
|
|
|
|
relevant_edges: list[EntityEdge] = []
|
|
|
|
|
relevant_edge_uuids = set()
|
2024-08-18 13:22:31 -04:00
|
|
|
|
2024-08-23 14:18:45 -04:00
|
|
|
results = await asyncio.gather(
|
|
|
|
|
*[
|
|
|
|
|
edge_similarity_search(edge.fact_embedding, driver)
|
|
|
|
|
for edge in edges
|
|
|
|
|
if edge.fact_embedding is not None
|
|
|
|
|
],
|
|
|
|
|
*[edge_fulltext_search(edge.fact, driver) for edge in edges],
|
|
|
|
|
)
|
2024-08-18 13:22:31 -04:00
|
|
|
|
2024-08-23 14:18:45 -04:00
|
|
|
for result in results:
|
|
|
|
|
for edge in result:
|
|
|
|
|
if edge.uuid in relevant_edge_uuids:
|
|
|
|
|
continue
|
2024-08-18 13:22:31 -04:00
|
|
|
|
2024-08-23 14:18:45 -04:00
|
|
|
relevant_edge_uuids.add(edge.uuid)
|
|
|
|
|
relevant_edges.append(edge)
|
2024-08-21 12:03:32 -04:00
|
|
|
|
2024-08-23 14:18:45 -04:00
|
|
|
end = time()
|
|
|
|
|
logger.info(f'Found relevant edges: {relevant_edge_uuids} in {(end - start) * 1000} ms')
|
2024-08-18 13:22:31 -04:00
|
|
|
|
2024-08-23 14:18:45 -04:00
|
|
|
return relevant_edges
|
2024-08-22 14:26:26 -04:00
|
|
|
|
|
|
|
|
|
|
|
|
|
# takes in a list of rankings of uuids
|
|
|
|
|
def rrf(results: list[list[str]], rank_const=1) -> list[str]:
|
2024-08-26 18:34:57 -04:00
|
|
|
scores: dict[str, float] = defaultdict(float)
|
2024-08-23 14:18:45 -04:00
|
|
|
for result in results:
|
|
|
|
|
for i, uuid in enumerate(result):
|
|
|
|
|
scores[uuid] += 1 / (i + rank_const)
|
2024-08-22 14:26:26 -04:00
|
|
|
|
2024-08-23 14:18:45 -04:00
|
|
|
scored_uuids = [term for term in scores.items()]
|
|
|
|
|
scored_uuids.sort(reverse=True, key=lambda term: term[1])
|
2024-08-22 14:26:26 -04:00
|
|
|
|
2024-08-23 14:18:45 -04:00
|
|
|
sorted_uuids = [term[0] for term in scored_uuids]
|
2024-08-22 14:26:26 -04:00
|
|
|
|
2024-08-23 14:18:45 -04:00
|
|
|
return sorted_uuids
|
2024-08-26 18:34:57 -04:00
|
|
|
|
|
|
|
|
|
|
|
|
|
async def node_distance_reranker(
|
|
|
|
|
driver: AsyncDriver, results: list[list[str]], center_node_uuid: str
|
|
|
|
|
) -> list[str]:
|
|
|
|
|
# use rrf as a preliminary ranker
|
|
|
|
|
sorted_uuids = rrf(results)
|
|
|
|
|
scores: dict[str, float] = {}
|
|
|
|
|
|
|
|
|
|
for uuid in sorted_uuids:
|
|
|
|
|
# Find shortest path to center node
|
|
|
|
|
records, _, _ = await driver.execute_query(
|
|
|
|
|
"""
|
|
|
|
|
MATCH (source:Entity)-[r:RELATES_TO {uuid: $edge_uuid}]->(target:Entity)
|
|
|
|
|
MATCH p = SHORTEST 1 (center:Entity)-[:RELATES_TO]-+(n:Entity)
|
|
|
|
|
WHERE center.uuid = $center_uuid AND n.uuid IN [source.uuid, target.uuid]
|
|
|
|
|
RETURN min(length(p)) AS score, source.uuid AS source_uuid, target.uuid AS target_uuid
|
|
|
|
|
""",
|
|
|
|
|
edge_uuid=uuid,
|
|
|
|
|
center_uuid=center_node_uuid,
|
|
|
|
|
)
|
|
|
|
|
distance = 0.01
|
|
|
|
|
|
|
|
|
|
for record in records:
|
|
|
|
|
if (
|
|
|
|
|
record['source_uuid'] == center_node_uuid
|
|
|
|
|
or record['target_uuid'] == center_node_uuid
|
|
|
|
|
):
|
|
|
|
|
continue
|
|
|
|
|
distance = record['score']
|
|
|
|
|
|
|
|
|
|
if uuid in scores:
|
|
|
|
|
scores[uuid] = min(1 / distance, scores[uuid])
|
|
|
|
|
else:
|
|
|
|
|
scores[uuid] = 1 / distance
|
|
|
|
|
|
|
|
|
|
# rerank on shortest distance
|
|
|
|
|
sorted_uuids.sort(reverse=True, key=lambda cur_uuid: scores[cur_uuid])
|
|
|
|
|
|
|
|
|
|
return sorted_uuids
|