graphiti-core 0.2.1__tar.gz → 0.2.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of graphiti-core might be problematic. Click here for more details.
- {graphiti_core-0.2.1 → graphiti_core-0.2.2}/PKG-INFO +2 -2
- {graphiti_core-0.2.1 → graphiti_core-0.2.2}/README.md +1 -1
- {graphiti_core-0.2.1 → graphiti_core-0.2.2}/graphiti_core/graphiti.py +56 -64
- {graphiti_core-0.2.1 → graphiti_core-0.2.2}/graphiti_core/nodes.py +6 -1
- {graphiti_core-0.2.1 → graphiti_core-0.2.2}/graphiti_core/prompts/dedupe_edges.py +1 -1
- {graphiti_core-0.2.1 → graphiti_core-0.2.2}/graphiti_core/prompts/invalidate_edges.py +37 -1
- {graphiti_core-0.2.1 → graphiti_core-0.2.2}/graphiti_core/search/search_utils.py +31 -29
- {graphiti_core-0.2.1 → graphiti_core-0.2.2}/graphiti_core/utils/maintenance/edge_operations.py +97 -11
- {graphiti_core-0.2.1 → graphiti_core-0.2.2}/graphiti_core/utils/maintenance/temporal_operations.py +34 -0
- {graphiti_core-0.2.1 → graphiti_core-0.2.2}/pyproject.toml +1 -1
- {graphiti_core-0.2.1 → graphiti_core-0.2.2}/LICENSE +0 -0
- {graphiti_core-0.2.1 → graphiti_core-0.2.2}/graphiti_core/__init__.py +0 -0
- {graphiti_core-0.2.1 → graphiti_core-0.2.2}/graphiti_core/edges.py +0 -0
- {graphiti_core-0.2.1 → graphiti_core-0.2.2}/graphiti_core/helpers.py +0 -0
- {graphiti_core-0.2.1 → graphiti_core-0.2.2}/graphiti_core/llm_client/__init__.py +0 -0
- {graphiti_core-0.2.1 → graphiti_core-0.2.2}/graphiti_core/llm_client/anthropic_client.py +0 -0
- {graphiti_core-0.2.1 → graphiti_core-0.2.2}/graphiti_core/llm_client/client.py +0 -0
- {graphiti_core-0.2.1 → graphiti_core-0.2.2}/graphiti_core/llm_client/config.py +0 -0
- {graphiti_core-0.2.1 → graphiti_core-0.2.2}/graphiti_core/llm_client/groq_client.py +0 -0
- {graphiti_core-0.2.1 → graphiti_core-0.2.2}/graphiti_core/llm_client/openai_client.py +0 -0
- {graphiti_core-0.2.1 → graphiti_core-0.2.2}/graphiti_core/llm_client/utils.py +0 -0
- {graphiti_core-0.2.1 → graphiti_core-0.2.2}/graphiti_core/prompts/__init__.py +0 -0
- {graphiti_core-0.2.1 → graphiti_core-0.2.2}/graphiti_core/prompts/dedupe_nodes.py +0 -0
- {graphiti_core-0.2.1 → graphiti_core-0.2.2}/graphiti_core/prompts/extract_edge_dates.py +0 -0
- {graphiti_core-0.2.1 → graphiti_core-0.2.2}/graphiti_core/prompts/extract_edges.py +0 -0
- {graphiti_core-0.2.1 → graphiti_core-0.2.2}/graphiti_core/prompts/extract_nodes.py +0 -0
- {graphiti_core-0.2.1 → graphiti_core-0.2.2}/graphiti_core/prompts/lib.py +0 -0
- {graphiti_core-0.2.1 → graphiti_core-0.2.2}/graphiti_core/prompts/models.py +0 -0
- {graphiti_core-0.2.1 → graphiti_core-0.2.2}/graphiti_core/search/__init__.py +0 -0
- {graphiti_core-0.2.1 → graphiti_core-0.2.2}/graphiti_core/search/search.py +0 -0
- {graphiti_core-0.2.1 → graphiti_core-0.2.2}/graphiti_core/utils/__init__.py +0 -0
- {graphiti_core-0.2.1 → graphiti_core-0.2.2}/graphiti_core/utils/bulk_utils.py +0 -0
- {graphiti_core-0.2.1 → graphiti_core-0.2.2}/graphiti_core/utils/maintenance/__init__.py +0 -0
- {graphiti_core-0.2.1 → graphiti_core-0.2.2}/graphiti_core/utils/maintenance/graph_data_operations.py +0 -0
- {graphiti_core-0.2.1 → graphiti_core-0.2.2}/graphiti_core/utils/maintenance/node_operations.py +0 -0
- {graphiti_core-0.2.1 → graphiti_core-0.2.2}/graphiti_core/utils/maintenance/utils.py +0 -0
- {graphiti_core-0.2.1 → graphiti_core-0.2.2}/graphiti_core/utils/utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: graphiti-core
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: A temporal graph building library
|
|
5
5
|
License: Apache-2.0
|
|
6
6
|
Author: Paul Paliychuk
|
|
@@ -173,7 +173,7 @@ graphiti.close()
|
|
|
173
173
|
|
|
174
174
|
## Documentation
|
|
175
175
|
|
|
176
|
-
- [Guides and API documentation](https://help.getzep.com/
|
|
176
|
+
- [Guides and API documentation](https://help.getzep.com/graphiti).
|
|
177
177
|
- [Quick Start](https://help.getzep.com/graphiti/graphiti/quick-start)
|
|
178
178
|
- [Building an agent with LangChain's LangGraph and Graphiti](https://help.getzep.com/graphiti/graphiti/lang-graph-agent)
|
|
179
179
|
|
|
@@ -151,7 +151,7 @@ graphiti.close()
|
|
|
151
151
|
|
|
152
152
|
## Documentation
|
|
153
153
|
|
|
154
|
-
- [Guides and API documentation](https://help.getzep.com/
|
|
154
|
+
- [Guides and API documentation](https://help.getzep.com/graphiti).
|
|
155
155
|
- [Quick Start](https://help.getzep.com/graphiti/graphiti/quick-start)
|
|
156
156
|
- [Building an agent with LangChain's LangGraph and Graphiti](https://help.getzep.com/graphiti/graphiti/lang-graph-agent)
|
|
157
157
|
|
|
@@ -59,11 +59,6 @@ from graphiti_core.utils.maintenance.node_operations import (
|
|
|
59
59
|
extract_nodes,
|
|
60
60
|
resolve_extracted_nodes,
|
|
61
61
|
)
|
|
62
|
-
from graphiti_core.utils.maintenance.temporal_operations import (
|
|
63
|
-
extract_edge_dates,
|
|
64
|
-
invalidate_edges,
|
|
65
|
-
prepare_edges_for_invalidation,
|
|
66
|
-
)
|
|
67
62
|
|
|
68
63
|
logger = logging.getLogger(__name__)
|
|
69
64
|
|
|
@@ -293,7 +288,7 @@ class Graphiti:
|
|
|
293
288
|
*[node.generate_name_embedding(embedder) for node in extracted_nodes]
|
|
294
289
|
)
|
|
295
290
|
|
|
296
|
-
# Resolve extracted nodes with nodes already in the graph
|
|
291
|
+
# Resolve extracted nodes with nodes already in the graph and extract facts
|
|
297
292
|
existing_nodes_lists: list[list[EntityNode]] = list(
|
|
298
293
|
await asyncio.gather(
|
|
299
294
|
*[get_relevant_nodes([node], self.driver) for node in extracted_nodes]
|
|
@@ -302,22 +297,27 @@ class Graphiti:
|
|
|
302
297
|
|
|
303
298
|
logger.info(f'Extracted nodes: {[(n.name, n.uuid) for n in extracted_nodes]}')
|
|
304
299
|
|
|
305
|
-
mentioned_nodes,
|
|
306
|
-
self.llm_client, extracted_nodes, existing_nodes_lists
|
|
300
|
+
(mentioned_nodes, uuid_map), extracted_edges = await asyncio.gather(
|
|
301
|
+
resolve_extracted_nodes(self.llm_client, extracted_nodes, existing_nodes_lists),
|
|
302
|
+
extract_edges(self.llm_client, episode, extracted_nodes, previous_episodes),
|
|
307
303
|
)
|
|
308
304
|
logger.info(f'Adjusted mentioned nodes: {[(n.name, n.uuid) for n in mentioned_nodes]}')
|
|
309
305
|
nodes.extend(mentioned_nodes)
|
|
310
306
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
self.llm_client, episode, mentioned_nodes, previous_episodes
|
|
307
|
+
extracted_edges_with_resolved_pointers = resolve_edge_pointers(
|
|
308
|
+
extracted_edges, uuid_map
|
|
314
309
|
)
|
|
315
310
|
|
|
316
311
|
# calculate embeddings
|
|
317
|
-
await asyncio.gather(
|
|
312
|
+
await asyncio.gather(
|
|
313
|
+
*[
|
|
314
|
+
edge.generate_embedding(embedder)
|
|
315
|
+
for edge in extracted_edges_with_resolved_pointers
|
|
316
|
+
]
|
|
317
|
+
)
|
|
318
318
|
|
|
319
|
-
# Resolve extracted edges with edges already in the graph
|
|
320
|
-
|
|
319
|
+
# Resolve extracted edges with related edges already in the graph
|
|
320
|
+
related_edges_list: list[list[EntityEdge]] = list(
|
|
321
321
|
await asyncio.gather(
|
|
322
322
|
*[
|
|
323
323
|
get_relevant_edges(
|
|
@@ -327,74 +327,66 @@ class Graphiti:
|
|
|
327
327
|
edge.target_node_uuid,
|
|
328
328
|
RELEVANT_SCHEMA_LIMIT,
|
|
329
329
|
)
|
|
330
|
-
for edge in
|
|
330
|
+
for edge in extracted_edges_with_resolved_pointers
|
|
331
331
|
]
|
|
332
332
|
)
|
|
333
333
|
)
|
|
334
334
|
logger.info(
|
|
335
|
-
f'
|
|
335
|
+
f'Related edges lists: {[(e.name, e.uuid) for edges_lst in related_edges_list for e in edges_lst]}'
|
|
336
336
|
)
|
|
337
|
-
logger.info(
|
|
338
|
-
|
|
339
|
-
deduped_edges: list[EntityEdge] = await resolve_extracted_edges(
|
|
340
|
-
self.llm_client, extracted_edges, existing_edges_list
|
|
337
|
+
logger.info(
|
|
338
|
+
f'Extracted edges: {[(e.name, e.uuid) for e in extracted_edges_with_resolved_pointers]}'
|
|
341
339
|
)
|
|
342
340
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
341
|
+
existing_source_edges_list: list[list[EntityEdge]] = list(
|
|
342
|
+
await asyncio.gather(
|
|
343
|
+
*[
|
|
344
|
+
get_relevant_edges(
|
|
345
|
+
self.driver,
|
|
346
|
+
[edge],
|
|
347
|
+
edge.source_node_uuid,
|
|
348
|
+
None,
|
|
349
|
+
RELEVANT_SCHEMA_LIMIT,
|
|
350
|
+
)
|
|
351
|
+
for edge in extracted_edges_with_resolved_pointers
|
|
352
|
+
]
|
|
353
|
+
)
|
|
354
354
|
)
|
|
355
355
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
356
|
+
existing_target_edges_list: list[list[EntityEdge]] = list(
|
|
357
|
+
await asyncio.gather(
|
|
358
|
+
*[
|
|
359
|
+
get_relevant_edges(
|
|
360
|
+
self.driver,
|
|
361
|
+
[edge],
|
|
362
|
+
None,
|
|
363
|
+
edge.target_node_uuid,
|
|
364
|
+
RELEVANT_SCHEMA_LIMIT,
|
|
365
|
+
)
|
|
366
|
+
for edge in extracted_edges_with_resolved_pointers
|
|
367
|
+
]
|
|
368
|
+
)
|
|
369
|
+
)
|
|
366
370
|
|
|
367
|
-
|
|
368
|
-
|
|
371
|
+
existing_edges_list: list[list[EntityEdge]] = [
|
|
372
|
+
source_lst + target_lst
|
|
373
|
+
for source_lst, target_lst in zip(
|
|
374
|
+
existing_source_edges_list, existing_target_edges_list
|
|
375
|
+
)
|
|
369
376
|
]
|
|
370
377
|
|
|
371
|
-
(
|
|
372
|
-
old_edges_with_nodes_pending_invalidation,
|
|
373
|
-
new_edges_with_nodes,
|
|
374
|
-
) = prepare_edges_for_invalidation(
|
|
375
|
-
existing_edges=existing_edges, new_edges=deduped_edges, nodes=nodes
|
|
376
|
-
)
|
|
377
|
-
|
|
378
|
-
invalidated_edges = await invalidate_edges(
|
|
378
|
+
resolved_edges, invalidated_edges = await resolve_extracted_edges(
|
|
379
379
|
self.llm_client,
|
|
380
|
-
|
|
381
|
-
|
|
380
|
+
extracted_edges_with_resolved_pointers,
|
|
381
|
+
related_edges_list,
|
|
382
|
+
existing_edges_list,
|
|
382
383
|
episode,
|
|
383
384
|
previous_episodes,
|
|
384
385
|
)
|
|
385
386
|
|
|
386
|
-
|
|
387
|
-
for existing_edge in existing_edges:
|
|
388
|
-
if existing_edge.uuid == edge.uuid:
|
|
389
|
-
existing_edge.expired_at = edge.expired_at
|
|
390
|
-
for deduped_edge in deduped_edges:
|
|
391
|
-
if deduped_edge.uuid == edge.uuid:
|
|
392
|
-
deduped_edge.expired_at = edge.expired_at
|
|
393
|
-
logger.info(f'Invalidated edges: {[(e.name, e.uuid) for e in invalidated_edges]}')
|
|
394
|
-
|
|
395
|
-
entity_edges.extend(existing_edges)
|
|
387
|
+
entity_edges.extend(resolved_edges + invalidated_edges)
|
|
396
388
|
|
|
397
|
-
logger.info(f'
|
|
389
|
+
logger.info(f'Resolved edges: {[(e.name, e.uuid) for e in resolved_edges]}')
|
|
398
390
|
|
|
399
391
|
episodic_edges: list[EpisodicEdge] = build_episodic_edges(
|
|
400
392
|
mentioned_nodes,
|
|
@@ -225,7 +225,8 @@ class EntityNode(Node):
|
|
|
225
225
|
MATCH (n:Entity {uuid: $uuid})
|
|
226
226
|
RETURN
|
|
227
227
|
n.uuid As uuid,
|
|
228
|
-
n.name AS name,
|
|
228
|
+
n.name AS name,
|
|
229
|
+
n.name_embedding AS name_embedding,
|
|
229
230
|
n.created_at AS created_at,
|
|
230
231
|
n.summary AS summary
|
|
231
232
|
""",
|
|
@@ -239,6 +240,7 @@ class EntityNode(Node):
|
|
|
239
240
|
EntityNode(
|
|
240
241
|
uuid=record['uuid'],
|
|
241
242
|
name=record['name'],
|
|
243
|
+
name_embedding=record['name_embedding'],
|
|
242
244
|
labels=['Entity'],
|
|
243
245
|
created_at=record['created_at'].to_native(),
|
|
244
246
|
summary=record['summary'],
|
|
@@ -248,3 +250,6 @@ class EntityNode(Node):
|
|
|
248
250
|
logger.info(f'Found Node: {uuid}')
|
|
249
251
|
|
|
250
252
|
return nodes[0]
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
# Node helpers
|
|
@@ -129,7 +129,7 @@ def v3(context: dict[str, Any]) -> list[Message]:
|
|
|
129
129
|
Given the following context, determine whether the New Edge represents any of the edges in the list of Existing Edges.
|
|
130
130
|
|
|
131
131
|
Existing Edges:
|
|
132
|
-
{json.dumps(context['
|
|
132
|
+
{json.dumps(context['related_edges'], indent=2)}
|
|
133
133
|
|
|
134
134
|
New Edge:
|
|
135
135
|
{json.dumps(context['extracted_edges'], indent=2)}
|
|
@@ -21,10 +21,12 @@ from .models import Message, PromptFunction, PromptVersion
|
|
|
21
21
|
|
|
22
22
|
class Prompt(Protocol):
|
|
23
23
|
v1: PromptVersion
|
|
24
|
+
v2: PromptVersion
|
|
24
25
|
|
|
25
26
|
|
|
26
27
|
class Versions(TypedDict):
|
|
27
28
|
v1: PromptFunction
|
|
29
|
+
v2: PromptFunction
|
|
28
30
|
|
|
29
31
|
|
|
30
32
|
def v1(context: dict[str, Any]) -> list[Message]:
|
|
@@ -71,4 +73,38 @@ def v1(context: dict[str, Any]) -> list[Message]:
|
|
|
71
73
|
]
|
|
72
74
|
|
|
73
75
|
|
|
74
|
-
|
|
76
|
+
def v2(context: dict[str, Any]) -> list[Message]:
|
|
77
|
+
return [
|
|
78
|
+
Message(
|
|
79
|
+
role='system',
|
|
80
|
+
content='You are an AI assistant that helps determine which relationships in a knowledge graph should be invalidated based solely on explicit contradictions in newer information.',
|
|
81
|
+
),
|
|
82
|
+
Message(
|
|
83
|
+
role='user',
|
|
84
|
+
content=f"""
|
|
85
|
+
Based on the provided Existing Edges and a New Edge, determine which existing edges, if any, should be marked as invalidated due to contradictions with the New Edge.
|
|
86
|
+
|
|
87
|
+
Existing Edges:
|
|
88
|
+
{context['existing_edges']}
|
|
89
|
+
|
|
90
|
+
New Edge:
|
|
91
|
+
{context['new_edge']}
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
For each existing edge that should be invalidated, respond with a JSON object in the following format:
|
|
95
|
+
{{
|
|
96
|
+
"invalidated_edges": [
|
|
97
|
+
{{
|
|
98
|
+
"uuid": "The UUID of the edge to be invalidated",
|
|
99
|
+
"fact": "Updated fact of the edge"
|
|
100
|
+
}}
|
|
101
|
+
]
|
|
102
|
+
}}
|
|
103
|
+
|
|
104
|
+
If no relationships need to be invalidated based on these strict criteria, return an empty list for "invalidated_edges".
|
|
105
|
+
""",
|
|
106
|
+
),
|
|
107
|
+
]
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
versions: Versions = {'v1': v1, 'v2': v2}
|
|
@@ -23,7 +23,8 @@ async def get_mentioned_nodes(driver: AsyncDriver, episodes: list[EpisodicNode])
|
|
|
23
23
|
MATCH (episode:Episodic)-[:MENTIONS]->(n:Entity) WHERE episode.uuid IN $uuids
|
|
24
24
|
RETURN DISTINCT
|
|
25
25
|
n.uuid As uuid,
|
|
26
|
-
n.name AS name,
|
|
26
|
+
n.name AS name,
|
|
27
|
+
n.name_embedding AS name_embedding
|
|
27
28
|
n.created_at AS created_at,
|
|
28
29
|
n.summary AS summary
|
|
29
30
|
""",
|
|
@@ -37,6 +38,7 @@ async def get_mentioned_nodes(driver: AsyncDriver, episodes: list[EpisodicNode])
|
|
|
37
38
|
EntityNode(
|
|
38
39
|
uuid=record['uuid'],
|
|
39
40
|
name=record['name'],
|
|
41
|
+
name_embedding=record['name_embedding'],
|
|
40
42
|
labels=['Entity'],
|
|
41
43
|
created_at=record['created_at'].to_native(),
|
|
42
44
|
summary=record['summary'],
|
|
@@ -96,11 +98,11 @@ async def bfs(node_ids: list[str], driver: AsyncDriver):
|
|
|
96
98
|
|
|
97
99
|
|
|
98
100
|
async def edge_similarity_search(
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
101
|
+
driver: AsyncDriver,
|
|
102
|
+
search_vector: list[float],
|
|
103
|
+
source_node_uuid: str | None,
|
|
104
|
+
target_node_uuid: str | None,
|
|
105
|
+
limit: int = RELEVANT_SCHEMA_LIMIT,
|
|
104
106
|
) -> list[EntityEdge]:
|
|
105
107
|
# vector similarity search over embedded facts
|
|
106
108
|
query = Query("""
|
|
@@ -211,7 +213,7 @@ async def edge_similarity_search(
|
|
|
211
213
|
|
|
212
214
|
|
|
213
215
|
async def entity_similarity_search(
|
|
214
|
-
|
|
216
|
+
search_vector: list[float], driver: AsyncDriver, limit=RELEVANT_SCHEMA_LIMIT
|
|
215
217
|
) -> list[EntityNode]:
|
|
216
218
|
# vector similarity search over entity names
|
|
217
219
|
records, _, _ = await driver.execute_query(
|
|
@@ -221,7 +223,7 @@ async def entity_similarity_search(
|
|
|
221
223
|
RETURN
|
|
222
224
|
n.uuid As uuid,
|
|
223
225
|
n.name AS name,
|
|
224
|
-
n.
|
|
226
|
+
n.name_embedding AS name_embedding,
|
|
225
227
|
n.created_at AS created_at,
|
|
226
228
|
n.summary AS summary
|
|
227
229
|
ORDER BY score DESC
|
|
@@ -247,7 +249,7 @@ async def entity_similarity_search(
|
|
|
247
249
|
|
|
248
250
|
|
|
249
251
|
async def entity_fulltext_search(
|
|
250
|
-
|
|
252
|
+
query: str, driver: AsyncDriver, limit=RELEVANT_SCHEMA_LIMIT
|
|
251
253
|
) -> list[EntityNode]:
|
|
252
254
|
# BM25 search to get top nodes
|
|
253
255
|
fuzzy_query = re.sub(r'[^\w\s]', '', query) + '~'
|
|
@@ -257,7 +259,7 @@ async def entity_fulltext_search(
|
|
|
257
259
|
RETURN
|
|
258
260
|
node.uuid AS uuid,
|
|
259
261
|
node.name AS name,
|
|
260
|
-
node.
|
|
262
|
+
node.name_embedding AS name_embedding,
|
|
261
263
|
node.created_at AS created_at,
|
|
262
264
|
node.summary AS summary
|
|
263
265
|
ORDER BY score DESC
|
|
@@ -284,11 +286,11 @@ async def entity_fulltext_search(
|
|
|
284
286
|
|
|
285
287
|
|
|
286
288
|
async def edge_fulltext_search(
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
289
|
+
driver: AsyncDriver,
|
|
290
|
+
query: str,
|
|
291
|
+
source_node_uuid: str | None,
|
|
292
|
+
target_node_uuid: str | None,
|
|
293
|
+
limit=RELEVANT_SCHEMA_LIMIT,
|
|
292
294
|
) -> list[EntityEdge]:
|
|
293
295
|
# fulltext search over facts
|
|
294
296
|
cypher_query = Query("""
|
|
@@ -401,10 +403,10 @@ async def edge_fulltext_search(
|
|
|
401
403
|
|
|
402
404
|
|
|
403
405
|
async def hybrid_node_search(
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
406
|
+
queries: list[str],
|
|
407
|
+
embeddings: list[list[float]],
|
|
408
|
+
driver: AsyncDriver,
|
|
409
|
+
limit: int = RELEVANT_SCHEMA_LIMIT,
|
|
408
410
|
) -> list[EntityNode]:
|
|
409
411
|
"""
|
|
410
412
|
Perform a hybrid search for nodes using both text queries and embeddings.
|
|
@@ -466,8 +468,8 @@ async def hybrid_node_search(
|
|
|
466
468
|
|
|
467
469
|
|
|
468
470
|
async def get_relevant_nodes(
|
|
469
|
-
|
|
470
|
-
|
|
471
|
+
nodes: list[EntityNode],
|
|
472
|
+
driver: AsyncDriver,
|
|
471
473
|
) -> list[EntityNode]:
|
|
472
474
|
"""
|
|
473
475
|
Retrieve relevant nodes based on the provided list of EntityNodes.
|
|
@@ -503,11 +505,11 @@ async def get_relevant_nodes(
|
|
|
503
505
|
|
|
504
506
|
|
|
505
507
|
async def get_relevant_edges(
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
508
|
+
driver: AsyncDriver,
|
|
509
|
+
edges: list[EntityEdge],
|
|
510
|
+
source_node_uuid: str | None,
|
|
511
|
+
target_node_uuid: str | None,
|
|
512
|
+
limit: int = RELEVANT_SCHEMA_LIMIT,
|
|
511
513
|
) -> list[EntityEdge]:
|
|
512
514
|
start = time()
|
|
513
515
|
relevant_edges: list[EntityEdge] = []
|
|
@@ -557,7 +559,7 @@ def rrf(results: list[list[str]], rank_const=1) -> list[str]:
|
|
|
557
559
|
|
|
558
560
|
|
|
559
561
|
async def node_distance_reranker(
|
|
560
|
-
|
|
562
|
+
driver: AsyncDriver, results: list[list[str]], center_node_uuid: str
|
|
561
563
|
) -> list[str]:
|
|
562
564
|
# use rrf as a preliminary ranker
|
|
563
565
|
sorted_uuids = rrf(results)
|
|
@@ -579,8 +581,8 @@ async def node_distance_reranker(
|
|
|
579
581
|
|
|
580
582
|
for record in records:
|
|
581
583
|
if (
|
|
582
|
-
|
|
583
|
-
|
|
584
|
+
record['source_uuid'] == center_node_uuid
|
|
585
|
+
or record['target_uuid'] == center_node_uuid
|
|
584
586
|
):
|
|
585
587
|
continue
|
|
586
588
|
distance = record['score']
|
{graphiti_core-0.2.1 → graphiti_core-0.2.2}/graphiti_core/utils/maintenance/edge_operations.py
RENAMED
|
@@ -24,6 +24,10 @@ from graphiti_core.edges import EntityEdge, EpisodicEdge
|
|
|
24
24
|
from graphiti_core.llm_client import LLMClient
|
|
25
25
|
from graphiti_core.nodes import EntityNode, EpisodicNode
|
|
26
26
|
from graphiti_core.prompts import prompt_library
|
|
27
|
+
from graphiti_core.utils.maintenance.temporal_operations import (
|
|
28
|
+
extract_edge_dates,
|
|
29
|
+
get_edge_contradictions,
|
|
30
|
+
)
|
|
27
31
|
|
|
28
32
|
logger = logging.getLogger(__name__)
|
|
29
33
|
|
|
@@ -149,28 +153,110 @@ async def dedupe_extracted_edges(
|
|
|
149
153
|
async def resolve_extracted_edges(
|
|
150
154
|
llm_client: LLMClient,
|
|
151
155
|
extracted_edges: list[EntityEdge],
|
|
156
|
+
related_edges_lists: list[list[EntityEdge]],
|
|
152
157
|
existing_edges_lists: list[list[EntityEdge]],
|
|
153
|
-
|
|
154
|
-
|
|
158
|
+
current_episode: EpisodicNode,
|
|
159
|
+
previous_episodes: list[EpisodicNode],
|
|
160
|
+
) -> tuple[list[EntityEdge], list[EntityEdge]]:
|
|
161
|
+
# resolve edges with related edges in the graph, extract temporal information, and find invalidation candidates
|
|
162
|
+
results: list[tuple[EntityEdge, list[EntityEdge]]] = list(
|
|
155
163
|
await asyncio.gather(
|
|
156
164
|
*[
|
|
157
|
-
resolve_extracted_edge(
|
|
158
|
-
|
|
165
|
+
resolve_extracted_edge(
|
|
166
|
+
llm_client,
|
|
167
|
+
extracted_edge,
|
|
168
|
+
related_edges,
|
|
169
|
+
existing_edges,
|
|
170
|
+
current_episode,
|
|
171
|
+
previous_episodes,
|
|
172
|
+
)
|
|
173
|
+
for extracted_edge, related_edges, existing_edges in zip(
|
|
174
|
+
extracted_edges, related_edges_lists, existing_edges_lists
|
|
175
|
+
)
|
|
159
176
|
]
|
|
160
177
|
)
|
|
161
178
|
)
|
|
162
179
|
|
|
163
|
-
|
|
180
|
+
resolved_edges: list[EntityEdge] = []
|
|
181
|
+
invalidated_edges: list[EntityEdge] = []
|
|
182
|
+
for result in results:
|
|
183
|
+
resolved_edge = result[0]
|
|
184
|
+
invalidated_edge_chunk = result[1]
|
|
185
|
+
|
|
186
|
+
resolved_edges.append(resolved_edge)
|
|
187
|
+
invalidated_edges.extend(invalidated_edge_chunk)
|
|
188
|
+
|
|
189
|
+
return resolved_edges, invalidated_edges
|
|
164
190
|
|
|
165
191
|
|
|
166
192
|
async def resolve_extracted_edge(
|
|
167
|
-
llm_client: LLMClient,
|
|
193
|
+
llm_client: LLMClient,
|
|
194
|
+
extracted_edge: EntityEdge,
|
|
195
|
+
related_edges: list[EntityEdge],
|
|
196
|
+
existing_edges: list[EntityEdge],
|
|
197
|
+
current_episode: EpisodicNode,
|
|
198
|
+
previous_episodes: list[EpisodicNode],
|
|
199
|
+
) -> tuple[EntityEdge, list[EntityEdge]]:
|
|
200
|
+
resolved_edge, (valid_at, invalid_at), invalidation_candidates = await asyncio.gather(
|
|
201
|
+
dedupe_extracted_edge(llm_client, extracted_edge, related_edges),
|
|
202
|
+
extract_edge_dates(llm_client, extracted_edge, current_episode, previous_episodes),
|
|
203
|
+
get_edge_contradictions(llm_client, extracted_edge, existing_edges),
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
now = datetime.now()
|
|
207
|
+
|
|
208
|
+
resolved_edge.valid_at = valid_at if valid_at is not None else resolved_edge.valid_at
|
|
209
|
+
resolved_edge.invalid_at = invalid_at if invalid_at is not None else resolved_edge.invalid_at
|
|
210
|
+
if invalid_at is not None and resolved_edge.expired_at is None:
|
|
211
|
+
resolved_edge.expired_at = now
|
|
212
|
+
|
|
213
|
+
# Determine if the new_edge needs to be expired
|
|
214
|
+
if resolved_edge.expired_at is None:
|
|
215
|
+
invalidation_candidates.sort(key=lambda c: (c.valid_at is None, c.valid_at))
|
|
216
|
+
for candidate in invalidation_candidates:
|
|
217
|
+
if (
|
|
218
|
+
candidate.valid_at is not None and resolved_edge.valid_at is not None
|
|
219
|
+
) and candidate.valid_at > resolved_edge.valid_at:
|
|
220
|
+
# Expire new edge since we have information about more recent events
|
|
221
|
+
resolved_edge.invalid_at = candidate.valid_at
|
|
222
|
+
resolved_edge.expired_at = now
|
|
223
|
+
break
|
|
224
|
+
|
|
225
|
+
# Determine which contradictory edges need to be expired
|
|
226
|
+
invalidated_edges: list[EntityEdge] = []
|
|
227
|
+
for edge in invalidation_candidates:
|
|
228
|
+
# (Edge invalid before new edge becomes valid) or (new edge invalid before edge becomes valid)
|
|
229
|
+
if (
|
|
230
|
+
edge.invalid_at is not None
|
|
231
|
+
and resolved_edge.valid_at is not None
|
|
232
|
+
and edge.invalid_at < resolved_edge.valid_at
|
|
233
|
+
) or (
|
|
234
|
+
edge.valid_at is not None
|
|
235
|
+
and resolved_edge.invalid_at is not None
|
|
236
|
+
and resolved_edge.invalid_at < edge.valid_at
|
|
237
|
+
):
|
|
238
|
+
continue
|
|
239
|
+
# New edge invalidates edge
|
|
240
|
+
elif (
|
|
241
|
+
edge.valid_at is not None
|
|
242
|
+
and resolved_edge.valid_at is not None
|
|
243
|
+
and edge.valid_at < resolved_edge.valid_at
|
|
244
|
+
):
|
|
245
|
+
edge.invalid_at = resolved_edge.valid_at
|
|
246
|
+
edge.expired_at = edge.expired_at if edge.expired_at is not None else now
|
|
247
|
+
invalidated_edges.append(edge)
|
|
248
|
+
|
|
249
|
+
return resolved_edge, invalidated_edges
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
async def dedupe_extracted_edge(
|
|
253
|
+
llm_client: LLMClient, extracted_edge: EntityEdge, related_edges: list[EntityEdge]
|
|
168
254
|
) -> EntityEdge:
|
|
169
255
|
start = time()
|
|
170
256
|
|
|
171
257
|
# Prepare context for LLM
|
|
172
|
-
|
|
173
|
-
{'uuid': edge.uuid, 'name': edge.name, 'fact': edge.fact} for edge in
|
|
258
|
+
related_edges_context = [
|
|
259
|
+
{'uuid': edge.uuid, 'name': edge.name, 'fact': edge.fact} for edge in related_edges
|
|
174
260
|
]
|
|
175
261
|
|
|
176
262
|
extracted_edge_context = {
|
|
@@ -180,7 +266,7 @@ async def resolve_extracted_edge(
|
|
|
180
266
|
}
|
|
181
267
|
|
|
182
268
|
context = {
|
|
183
|
-
'
|
|
269
|
+
'related_edges': related_edges_context,
|
|
184
270
|
'extracted_edges': extracted_edge_context,
|
|
185
271
|
}
|
|
186
272
|
|
|
@@ -191,14 +277,14 @@ async def resolve_extracted_edge(
|
|
|
191
277
|
|
|
192
278
|
edge = extracted_edge
|
|
193
279
|
if is_duplicate:
|
|
194
|
-
for existing_edge in
|
|
280
|
+
for existing_edge in related_edges:
|
|
195
281
|
if existing_edge.uuid != uuid:
|
|
196
282
|
continue
|
|
197
283
|
edge = existing_edge
|
|
198
284
|
|
|
199
285
|
end = time()
|
|
200
286
|
logger.info(
|
|
201
|
-
f'Resolved
|
|
287
|
+
f'Resolved Edge: {extracted_edge.name} is {edge.name}, in {(end - start) * 1000} ms'
|
|
202
288
|
)
|
|
203
289
|
|
|
204
290
|
return edge
|
{graphiti_core-0.2.1 → graphiti_core-0.2.2}/graphiti_core/utils/maintenance/temporal_operations.py
RENAMED
|
@@ -16,6 +16,7 @@ limitations under the License.
|
|
|
16
16
|
|
|
17
17
|
import logging
|
|
18
18
|
from datetime import datetime
|
|
19
|
+
from time import time
|
|
19
20
|
from typing import List
|
|
20
21
|
|
|
21
22
|
from graphiti_core.edges import EntityEdge
|
|
@@ -181,3 +182,36 @@ async def extract_edge_dates(
|
|
|
181
182
|
logger.info(f'Edge date extraction explanation: {explanation}')
|
|
182
183
|
|
|
183
184
|
return valid_at_datetime, invalid_at_datetime
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
async def get_edge_contradictions(
|
|
188
|
+
llm_client: LLMClient, new_edge: EntityEdge, existing_edges: list[EntityEdge]
|
|
189
|
+
) -> list[EntityEdge]:
|
|
190
|
+
start = time()
|
|
191
|
+
existing_edge_map = {edge.uuid: edge for edge in existing_edges}
|
|
192
|
+
|
|
193
|
+
new_edge_context = {'uuid': new_edge.uuid, 'name': new_edge.name, 'fact': new_edge.fact}
|
|
194
|
+
existing_edge_context = [
|
|
195
|
+
{'uuid': existing_edge.uuid, 'name': existing_edge.name, 'fact': existing_edge.fact}
|
|
196
|
+
for existing_edge in existing_edges
|
|
197
|
+
]
|
|
198
|
+
|
|
199
|
+
context = {'new_edge': new_edge_context, 'existing_edges': existing_edge_context}
|
|
200
|
+
|
|
201
|
+
llm_response = await llm_client.generate_response(prompt_library.invalidate_edges.v2(context))
|
|
202
|
+
|
|
203
|
+
contradicted_edge_data = llm_response.get('invalidated_edges', [])
|
|
204
|
+
|
|
205
|
+
contradicted_edges: list[EntityEdge] = []
|
|
206
|
+
for edge_data in contradicted_edge_data:
|
|
207
|
+
if edge_data['uuid'] in existing_edge_map:
|
|
208
|
+
contradicted_edge = existing_edge_map[edge_data['uuid']]
|
|
209
|
+
contradicted_edge.fact = edge_data['fact']
|
|
210
|
+
contradicted_edges.append(contradicted_edge)
|
|
211
|
+
|
|
212
|
+
end = time()
|
|
213
|
+
logger.info(
|
|
214
|
+
f'Found invalidated edge candidates from {new_edge.fact}, in {(end - start) * 1000} ms'
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
return contradicted_edges
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{graphiti_core-0.2.1 → graphiti_core-0.2.2}/graphiti_core/utils/maintenance/graph_data_operations.py
RENAMED
|
File without changes
|
{graphiti_core-0.2.1 → graphiti_core-0.2.2}/graphiti_core/utils/maintenance/node_operations.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|