graphiti-core 0.3.20__py3-none-any.whl → 0.4.0__py3-none-any.whl
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/cross_encoder/openai_reranker_client.py +2 -2
- graphiti_core/graphiti.py +50 -71
- graphiti_core/helpers.py +1 -0
- graphiti_core/models/edges/edge_db_queries.py +16 -0
- graphiti_core/models/nodes/node_db_queries.py +16 -0
- graphiti_core/nodes.py +2 -2
- graphiti_core/prompts/dedupe_edges.py +9 -93
- graphiti_core/prompts/dedupe_nodes.py +19 -101
- graphiti_core/prompts/extract_edge_dates.py +14 -7
- graphiti_core/prompts/extract_edges.py +55 -81
- graphiti_core/prompts/extract_nodes.py +72 -96
- graphiti_core/prompts/summarize_nodes.py +40 -1
- graphiti_core/search/search.py +20 -0
- graphiti_core/search/search_config_recipes.py +35 -0
- graphiti_core/search/search_utils.py +5 -4
- graphiti_core/utils/bulk_utils.py +3 -3
- graphiti_core/utils/maintenance/community_operations.py +3 -3
- graphiti_core/utils/maintenance/edge_operations.py +87 -55
- graphiti_core/utils/maintenance/node_operations.py +122 -52
- {graphiti_core-0.3.20.dist-info → graphiti_core-0.4.0.dist-info}/METADATA +6 -5
- {graphiti_core-0.3.20.dist-info → graphiti_core-0.4.0.dist-info}/RECORD +23 -23
- {graphiti_core-0.3.20.dist-info → graphiti_core-0.4.0.dist-info}/LICENSE +0 -0
- {graphiti_core-0.3.20.dist-info → graphiti_core-0.4.0.dist-info}/WHEEL +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import logging
|
|
3
3
|
from collections import defaultdict
|
|
4
|
-
from datetime import datetime
|
|
4
|
+
from datetime import datetime, timezone
|
|
5
5
|
|
|
6
6
|
from neo4j import AsyncDriver
|
|
7
7
|
from pydantic import BaseModel
|
|
@@ -178,7 +178,7 @@ async def build_community(
|
|
|
178
178
|
|
|
179
179
|
summary = summaries[0]
|
|
180
180
|
name = await generate_summary_description(llm_client, summary)
|
|
181
|
-
now = datetime.now()
|
|
181
|
+
now = datetime.now(timezone.utc)
|
|
182
182
|
community_node = CommunityNode(
|
|
183
183
|
name=name,
|
|
184
184
|
group_id=community_cluster[0].group_id,
|
|
@@ -305,7 +305,7 @@ async def update_community(
|
|
|
305
305
|
community.name = new_name
|
|
306
306
|
|
|
307
307
|
if is_new:
|
|
308
|
-
community_edge = (build_community_edges([entity], community, datetime.now()))[0]
|
|
308
|
+
community_edge = (build_community_edges([entity], community, datetime.now(timezone.utc)))[0]
|
|
309
309
|
await community_edge.save(driver)
|
|
310
310
|
|
|
311
311
|
await community.generate_name_embedding(embedder)
|
|
@@ -16,11 +16,12 @@ limitations under the License.
|
|
|
16
16
|
|
|
17
17
|
import asyncio
|
|
18
18
|
import logging
|
|
19
|
-
from datetime import datetime
|
|
19
|
+
from datetime import datetime, timezone
|
|
20
20
|
from time import time
|
|
21
21
|
from typing import List
|
|
22
22
|
|
|
23
23
|
from graphiti_core.edges import CommunityEdge, EntityEdge, EpisodicEdge
|
|
24
|
+
from graphiti_core.helpers import MAX_REFLEXION_ITERATIONS
|
|
24
25
|
from graphiti_core.llm_client import LLMClient
|
|
25
26
|
from graphiti_core.nodes import CommunityNode, EntityNode, EpisodicNode
|
|
26
27
|
from graphiti_core.prompts import prompt_library
|
|
@@ -77,24 +78,41 @@ async def extract_edges(
|
|
|
77
78
|
) -> list[EntityEdge]:
|
|
78
79
|
start = time()
|
|
79
80
|
|
|
81
|
+
node_uuids_by_name_map = {node.name: node.uuid for node in nodes}
|
|
82
|
+
|
|
80
83
|
# Prepare context for LLM
|
|
81
84
|
context = {
|
|
82
85
|
'episode_content': episode.content,
|
|
83
|
-
'
|
|
84
|
-
'
|
|
85
|
-
|
|
86
|
-
],
|
|
87
|
-
'previous_episodes': [
|
|
88
|
-
{
|
|
89
|
-
'content': ep.content,
|
|
90
|
-
'timestamp': ep.valid_at.isoformat() if ep.valid_at else None,
|
|
91
|
-
}
|
|
92
|
-
for ep in previous_episodes
|
|
93
|
-
],
|
|
86
|
+
'nodes': [node.name for node in nodes],
|
|
87
|
+
'previous_episodes': [ep.content for ep in previous_episodes],
|
|
88
|
+
'custom_prompt': '',
|
|
94
89
|
}
|
|
95
90
|
|
|
96
|
-
|
|
97
|
-
|
|
91
|
+
facts_missed = True
|
|
92
|
+
reflexion_iterations = 0
|
|
93
|
+
while facts_missed and reflexion_iterations < MAX_REFLEXION_ITERATIONS:
|
|
94
|
+
llm_response = await llm_client.generate_response(
|
|
95
|
+
prompt_library.extract_edges.edge(context)
|
|
96
|
+
)
|
|
97
|
+
edges_data = llm_response.get('edges', [])
|
|
98
|
+
|
|
99
|
+
context['extracted_facts'] = [edge_data.get('fact', '') for edge_data in edges_data]
|
|
100
|
+
|
|
101
|
+
reflexion_iterations += 1
|
|
102
|
+
if reflexion_iterations < MAX_REFLEXION_ITERATIONS:
|
|
103
|
+
reflexion_response = await llm_client.generate_response(
|
|
104
|
+
prompt_library.extract_edges.reflexion(context)
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
missing_facts = reflexion_response.get('missing_facts', [])
|
|
108
|
+
|
|
109
|
+
custom_prompt = 'The following facts were missed in a previous extraction: '
|
|
110
|
+
for fact in missing_facts:
|
|
111
|
+
custom_prompt += f'\n{fact},'
|
|
112
|
+
|
|
113
|
+
context['custom_prompt'] = custom_prompt
|
|
114
|
+
|
|
115
|
+
facts_missed = len(missing_facts) != 0
|
|
98
116
|
|
|
99
117
|
end = time()
|
|
100
118
|
logger.debug(f'Extracted new edges: {edges_data} in {(end - start) * 1000} ms')
|
|
@@ -102,22 +120,25 @@ async def extract_edges(
|
|
|
102
120
|
# Convert the extracted data into EntityEdge objects
|
|
103
121
|
edges = []
|
|
104
122
|
for edge_data in edges_data:
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
)
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
123
|
+
edge = EntityEdge(
|
|
124
|
+
source_node_uuid=node_uuids_by_name_map.get(
|
|
125
|
+
edge_data.get('source_entity_name', ''), ''
|
|
126
|
+
),
|
|
127
|
+
target_node_uuid=node_uuids_by_name_map.get(
|
|
128
|
+
edge_data.get('target_entity_name', ''), ''
|
|
129
|
+
),
|
|
130
|
+
name=edge_data.get('relation_type', ''),
|
|
131
|
+
group_id=group_id,
|
|
132
|
+
fact=edge_data.get('fact', ''),
|
|
133
|
+
episodes=[episode.uuid],
|
|
134
|
+
created_at=datetime.now(timezone.utc),
|
|
135
|
+
valid_at=None,
|
|
136
|
+
invalid_at=None,
|
|
137
|
+
)
|
|
138
|
+
edges.append(edge)
|
|
139
|
+
logger.debug(
|
|
140
|
+
f'Created new edge: {edge.name} from (UUID: {edge.source_node_uuid}) to (UUID: {edge.target_node_uuid})'
|
|
141
|
+
)
|
|
121
142
|
|
|
122
143
|
return edges
|
|
123
144
|
|
|
@@ -142,7 +163,7 @@ async def dedupe_extracted_edges(
|
|
|
142
163
|
],
|
|
143
164
|
}
|
|
144
165
|
|
|
145
|
-
llm_response = await llm_client.generate_response(prompt_library.dedupe_edges.
|
|
166
|
+
llm_response = await llm_client.generate_response(prompt_library.dedupe_edges.edge(context))
|
|
146
167
|
duplicate_data = llm_response.get('duplicates', [])
|
|
147
168
|
logger.debug(f'Extracted unique edges: {duplicate_data}')
|
|
148
169
|
|
|
@@ -205,6 +226,38 @@ async def resolve_extracted_edges(
|
|
|
205
226
|
return resolved_edges, invalidated_edges
|
|
206
227
|
|
|
207
228
|
|
|
229
|
+
def resolve_edge_contradictions(
|
|
230
|
+
resolved_edge: EntityEdge, invalidation_candidates: list[EntityEdge]
|
|
231
|
+
) -> list[EntityEdge]:
|
|
232
|
+
# Determine which contradictory edges need to be expired
|
|
233
|
+
invalidated_edges: list[EntityEdge] = []
|
|
234
|
+
for edge in invalidation_candidates:
|
|
235
|
+
# (Edge invalid before new edge becomes valid) or (new edge invalid before edge becomes valid)
|
|
236
|
+
if (
|
|
237
|
+
edge.invalid_at is not None
|
|
238
|
+
and resolved_edge.valid_at is not None
|
|
239
|
+
and edge.invalid_at <= resolved_edge.valid_at
|
|
240
|
+
) or (
|
|
241
|
+
edge.valid_at is not None
|
|
242
|
+
and resolved_edge.invalid_at is not None
|
|
243
|
+
and resolved_edge.invalid_at <= edge.valid_at
|
|
244
|
+
):
|
|
245
|
+
continue
|
|
246
|
+
# New edge invalidates edge
|
|
247
|
+
elif (
|
|
248
|
+
edge.valid_at is not None
|
|
249
|
+
and resolved_edge.valid_at is not None
|
|
250
|
+
and edge.valid_at < resolved_edge.valid_at
|
|
251
|
+
):
|
|
252
|
+
edge.invalid_at = resolved_edge.valid_at
|
|
253
|
+
edge.expired_at = (
|
|
254
|
+
edge.expired_at if edge.expired_at is not None else datetime.now(timezone.utc)
|
|
255
|
+
)
|
|
256
|
+
invalidated_edges.append(edge)
|
|
257
|
+
|
|
258
|
+
return invalidated_edges
|
|
259
|
+
|
|
260
|
+
|
|
208
261
|
async def resolve_extracted_edge(
|
|
209
262
|
llm_client: LLMClient,
|
|
210
263
|
extracted_edge: EntityEdge,
|
|
@@ -219,7 +272,7 @@ async def resolve_extracted_edge(
|
|
|
219
272
|
get_edge_contradictions(llm_client, extracted_edge, existing_edges),
|
|
220
273
|
)
|
|
221
274
|
|
|
222
|
-
now = datetime.now()
|
|
275
|
+
now = datetime.now(timezone.utc)
|
|
223
276
|
|
|
224
277
|
resolved_edge.valid_at = valid_at if valid_at is not None else resolved_edge.valid_at
|
|
225
278
|
resolved_edge.invalid_at = invalid_at if invalid_at is not None else resolved_edge.invalid_at
|
|
@@ -239,28 +292,7 @@ async def resolve_extracted_edge(
|
|
|
239
292
|
break
|
|
240
293
|
|
|
241
294
|
# Determine which contradictory edges need to be expired
|
|
242
|
-
invalidated_edges
|
|
243
|
-
for edge in invalidation_candidates:
|
|
244
|
-
# (Edge invalid before new edge becomes valid) or (new edge invalid before edge becomes valid)
|
|
245
|
-
if (
|
|
246
|
-
edge.invalid_at is not None
|
|
247
|
-
and resolved_edge.valid_at is not None
|
|
248
|
-
and edge.invalid_at <= resolved_edge.valid_at
|
|
249
|
-
) or (
|
|
250
|
-
edge.valid_at is not None
|
|
251
|
-
and resolved_edge.invalid_at is not None
|
|
252
|
-
and resolved_edge.invalid_at <= edge.valid_at
|
|
253
|
-
):
|
|
254
|
-
continue
|
|
255
|
-
# New edge invalidates edge
|
|
256
|
-
elif (
|
|
257
|
-
edge.valid_at is not None
|
|
258
|
-
and resolved_edge.valid_at is not None
|
|
259
|
-
and edge.valid_at < resolved_edge.valid_at
|
|
260
|
-
):
|
|
261
|
-
edge.invalid_at = resolved_edge.valid_at
|
|
262
|
-
edge.expired_at = edge.expired_at if edge.expired_at is not None else now
|
|
263
|
-
invalidated_edges.append(edge)
|
|
295
|
+
invalidated_edges = resolve_edge_contradictions(resolved_edge, invalidation_candidates)
|
|
264
296
|
|
|
265
297
|
return resolved_edge, invalidated_edges
|
|
266
298
|
|
|
@@ -286,7 +318,7 @@ async def dedupe_extracted_edge(
|
|
|
286
318
|
'extracted_edges': extracted_edge_context,
|
|
287
319
|
}
|
|
288
320
|
|
|
289
|
-
llm_response = await llm_client.generate_response(prompt_library.dedupe_edges.
|
|
321
|
+
llm_response = await llm_client.generate_response(prompt_library.dedupe_edges.edge(context))
|
|
290
322
|
|
|
291
323
|
is_duplicate: bool = llm_response.get('is_duplicate', False)
|
|
292
324
|
uuid: str | None = llm_response.get('uuid', None)
|
|
@@ -16,10 +16,10 @@ limitations under the License.
|
|
|
16
16
|
|
|
17
17
|
import asyncio
|
|
18
18
|
import logging
|
|
19
|
-
from datetime import datetime
|
|
19
|
+
from datetime import datetime, timezone
|
|
20
20
|
from time import time
|
|
21
|
-
from typing import Any
|
|
22
21
|
|
|
22
|
+
from graphiti_core.helpers import MAX_REFLEXION_ITERATIONS
|
|
23
23
|
from graphiti_core.llm_client import LLMClient
|
|
24
24
|
from graphiti_core.nodes import EntityNode, EpisodeType, EpisodicNode
|
|
25
25
|
from graphiti_core.prompts import prompt_library
|
|
@@ -28,65 +28,84 @@ logger = logging.getLogger(__name__)
|
|
|
28
28
|
|
|
29
29
|
|
|
30
30
|
async def extract_message_nodes(
|
|
31
|
-
llm_client: LLMClient,
|
|
32
|
-
|
|
31
|
+
llm_client: LLMClient,
|
|
32
|
+
episode: EpisodicNode,
|
|
33
|
+
previous_episodes: list[EpisodicNode],
|
|
34
|
+
custom_prompt='',
|
|
35
|
+
) -> list[str]:
|
|
33
36
|
# Prepare context for LLM
|
|
34
37
|
context = {
|
|
35
38
|
'episode_content': episode.content,
|
|
36
39
|
'episode_timestamp': episode.valid_at.isoformat(),
|
|
37
|
-
'previous_episodes': [
|
|
38
|
-
|
|
39
|
-
'content': ep.content,
|
|
40
|
-
'timestamp': ep.valid_at.isoformat(),
|
|
41
|
-
}
|
|
42
|
-
for ep in previous_episodes
|
|
43
|
-
],
|
|
40
|
+
'previous_episodes': [ep.content for ep in previous_episodes],
|
|
41
|
+
'custom_prompt': custom_prompt,
|
|
44
42
|
}
|
|
45
43
|
|
|
46
|
-
llm_response = await llm_client.generate_response(
|
|
47
|
-
|
|
48
|
-
|
|
44
|
+
llm_response = await llm_client.generate_response(
|
|
45
|
+
prompt_library.extract_nodes.extract_message(context)
|
|
46
|
+
)
|
|
47
|
+
extracted_node_names = llm_response.get('extracted_node_names', [])
|
|
48
|
+
return extracted_node_names
|
|
49
49
|
|
|
50
50
|
|
|
51
51
|
async def extract_text_nodes(
|
|
52
|
-
llm_client: LLMClient,
|
|
53
|
-
|
|
52
|
+
llm_client: LLMClient,
|
|
53
|
+
episode: EpisodicNode,
|
|
54
|
+
previous_episodes: list[EpisodicNode],
|
|
55
|
+
custom_prompt='',
|
|
56
|
+
) -> list[str]:
|
|
54
57
|
# Prepare context for LLM
|
|
55
58
|
context = {
|
|
56
59
|
'episode_content': episode.content,
|
|
57
60
|
'episode_timestamp': episode.valid_at.isoformat(),
|
|
58
|
-
'previous_episodes': [
|
|
59
|
-
|
|
60
|
-
'content': ep.content,
|
|
61
|
-
'timestamp': ep.valid_at.isoformat(),
|
|
62
|
-
}
|
|
63
|
-
for ep in previous_episodes
|
|
64
|
-
],
|
|
61
|
+
'previous_episodes': [ep.content for ep in previous_episodes],
|
|
62
|
+
'custom_prompt': custom_prompt,
|
|
65
63
|
}
|
|
66
64
|
|
|
67
65
|
llm_response = await llm_client.generate_response(
|
|
68
66
|
prompt_library.extract_nodes.extract_text(context)
|
|
69
67
|
)
|
|
70
|
-
|
|
71
|
-
return
|
|
68
|
+
extracted_node_names = llm_response.get('extracted_node_names', [])
|
|
69
|
+
return extracted_node_names
|
|
72
70
|
|
|
73
71
|
|
|
74
72
|
async def extract_json_nodes(
|
|
75
|
-
llm_client: LLMClient,
|
|
76
|
-
|
|
77
|
-
) -> list[dict[str, Any]]:
|
|
73
|
+
llm_client: LLMClient, episode: EpisodicNode, custom_prompt=''
|
|
74
|
+
) -> list[str]:
|
|
78
75
|
# Prepare context for LLM
|
|
79
76
|
context = {
|
|
80
77
|
'episode_content': episode.content,
|
|
81
78
|
'episode_timestamp': episode.valid_at.isoformat(),
|
|
82
79
|
'source_description': episode.source_description,
|
|
80
|
+
'custom_prompt': custom_prompt,
|
|
83
81
|
}
|
|
84
82
|
|
|
85
83
|
llm_response = await llm_client.generate_response(
|
|
86
84
|
prompt_library.extract_nodes.extract_json(context)
|
|
87
85
|
)
|
|
88
|
-
|
|
89
|
-
return
|
|
86
|
+
extracted_node_names = llm_response.get('extracted_node_names', [])
|
|
87
|
+
return extracted_node_names
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
async def extract_nodes_reflexion(
|
|
91
|
+
llm_client: LLMClient,
|
|
92
|
+
episode: EpisodicNode,
|
|
93
|
+
previous_episodes: list[EpisodicNode],
|
|
94
|
+
node_names: list[str],
|
|
95
|
+
) -> list[str]:
|
|
96
|
+
# Prepare context for LLM
|
|
97
|
+
context = {
|
|
98
|
+
'episode_content': episode.content,
|
|
99
|
+
'previous_episodes': [ep.content for ep in previous_episodes],
|
|
100
|
+
'extracted_entities': node_names,
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
llm_response = await llm_client.generate_response(
|
|
104
|
+
prompt_library.extract_nodes.reflexion(context)
|
|
105
|
+
)
|
|
106
|
+
missed_entities = llm_response.get('missed_entities', [])
|
|
107
|
+
|
|
108
|
+
return missed_entities
|
|
90
109
|
|
|
91
110
|
|
|
92
111
|
async def extract_nodes(
|
|
@@ -95,25 +114,45 @@ async def extract_nodes(
|
|
|
95
114
|
previous_episodes: list[EpisodicNode],
|
|
96
115
|
) -> list[EntityNode]:
|
|
97
116
|
start = time()
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
117
|
+
extracted_node_names: list[str] = []
|
|
118
|
+
custom_prompt = ''
|
|
119
|
+
entities_missed = True
|
|
120
|
+
reflexion_iterations = 0
|
|
121
|
+
while entities_missed and reflexion_iterations < MAX_REFLEXION_ITERATIONS:
|
|
122
|
+
if episode.source == EpisodeType.message:
|
|
123
|
+
extracted_node_names = await extract_message_nodes(
|
|
124
|
+
llm_client, episode, previous_episodes, custom_prompt
|
|
125
|
+
)
|
|
126
|
+
elif episode.source == EpisodeType.text:
|
|
127
|
+
extracted_node_names = await extract_text_nodes(
|
|
128
|
+
llm_client, episode, previous_episodes, custom_prompt
|
|
129
|
+
)
|
|
130
|
+
elif episode.source == EpisodeType.json:
|
|
131
|
+
extracted_node_names = await extract_json_nodes(llm_client, episode, custom_prompt)
|
|
132
|
+
|
|
133
|
+
reflexion_iterations += 1
|
|
134
|
+
if reflexion_iterations < MAX_REFLEXION_ITERATIONS:
|
|
135
|
+
missing_entities = await extract_nodes_reflexion(
|
|
136
|
+
llm_client, episode, previous_episodes, extracted_node_names
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
entities_missed = len(missing_entities) != 0
|
|
140
|
+
|
|
141
|
+
custom_prompt = 'The following entities were missed in a previous extraction: '
|
|
142
|
+
for entity in missing_entities:
|
|
143
|
+
custom_prompt += f'\n{entity},'
|
|
105
144
|
|
|
106
145
|
end = time()
|
|
107
|
-
logger.debug(f'Extracted new nodes: {
|
|
146
|
+
logger.debug(f'Extracted new nodes: {extracted_node_names} in {(end - start) * 1000} ms')
|
|
108
147
|
# Convert the extracted data into EntityNode objects
|
|
109
148
|
new_nodes = []
|
|
110
|
-
for
|
|
149
|
+
for name in extracted_node_names:
|
|
111
150
|
new_node = EntityNode(
|
|
112
|
-
name=
|
|
151
|
+
name=name,
|
|
113
152
|
group_id=episode.group_id,
|
|
114
|
-
labels=
|
|
115
|
-
summary=
|
|
116
|
-
created_at=datetime.now(),
|
|
153
|
+
labels=['Entity'],
|
|
154
|
+
summary='',
|
|
155
|
+
created_at=datetime.now(timezone.utc),
|
|
117
156
|
)
|
|
118
157
|
new_nodes.append(new_node)
|
|
119
158
|
logger.debug(f'Created new node: {new_node.name} (UUID: {new_node.uuid})')
|
|
@@ -147,7 +186,7 @@ async def dedupe_extracted_nodes(
|
|
|
147
186
|
'extracted_nodes': extracted_nodes_context,
|
|
148
187
|
}
|
|
149
188
|
|
|
150
|
-
llm_response = await llm_client.generate_response(prompt_library.dedupe_nodes.
|
|
189
|
+
llm_response = await llm_client.generate_response(prompt_library.dedupe_nodes.node(context))
|
|
151
190
|
|
|
152
191
|
duplicate_data = llm_response.get('duplicates', [])
|
|
153
192
|
|
|
@@ -175,13 +214,17 @@ async def resolve_extracted_nodes(
|
|
|
175
214
|
llm_client: LLMClient,
|
|
176
215
|
extracted_nodes: list[EntityNode],
|
|
177
216
|
existing_nodes_lists: list[list[EntityNode]],
|
|
217
|
+
episode: EpisodicNode | None = None,
|
|
218
|
+
previous_episodes: list[EpisodicNode] | None = None,
|
|
178
219
|
) -> tuple[list[EntityNode], dict[str, str]]:
|
|
179
220
|
uuid_map: dict[str, str] = {}
|
|
180
221
|
resolved_nodes: list[EntityNode] = []
|
|
181
222
|
results: list[tuple[EntityNode, dict[str, str]]] = list(
|
|
182
223
|
await asyncio.gather(
|
|
183
224
|
*[
|
|
184
|
-
resolve_extracted_node(
|
|
225
|
+
resolve_extracted_node(
|
|
226
|
+
llm_client, extracted_node, existing_nodes, episode, previous_episodes
|
|
227
|
+
)
|
|
185
228
|
for extracted_node, existing_nodes in zip(extracted_nodes, existing_nodes_lists)
|
|
186
229
|
]
|
|
187
230
|
)
|
|
@@ -195,14 +238,16 @@ async def resolve_extracted_nodes(
|
|
|
195
238
|
|
|
196
239
|
|
|
197
240
|
async def resolve_extracted_node(
|
|
198
|
-
llm_client: LLMClient,
|
|
241
|
+
llm_client: LLMClient,
|
|
242
|
+
extracted_node: EntityNode,
|
|
243
|
+
existing_nodes: list[EntityNode],
|
|
244
|
+
episode: EpisodicNode | None = None,
|
|
245
|
+
previous_episodes: list[EpisodicNode] | None = None,
|
|
199
246
|
) -> tuple[EntityNode, dict[str, str]]:
|
|
200
247
|
start = time()
|
|
201
248
|
|
|
202
249
|
# Prepare context for LLM
|
|
203
|
-
existing_nodes_context = [
|
|
204
|
-
{'uuid': node.uuid, 'name': node.name, 'summary': node.summary} for node in existing_nodes
|
|
205
|
-
]
|
|
250
|
+
existing_nodes_context = [{'uuid': node.uuid, 'name': node.name} for node in existing_nodes]
|
|
206
251
|
|
|
207
252
|
extracted_node_context = {
|
|
208
253
|
'uuid': extracted_node.uuid,
|
|
@@ -213,13 +258,32 @@ async def resolve_extracted_node(
|
|
|
213
258
|
context = {
|
|
214
259
|
'existing_nodes': existing_nodes_context,
|
|
215
260
|
'extracted_nodes': extracted_node_context,
|
|
261
|
+
'episode_content': episode.content if episode is not None else '',
|
|
262
|
+
'previous_episodes': [ep.content for ep in previous_episodes]
|
|
263
|
+
if previous_episodes is not None
|
|
264
|
+
else [],
|
|
216
265
|
}
|
|
217
266
|
|
|
218
|
-
|
|
267
|
+
summary_context = {
|
|
268
|
+
'node_name': extracted_node.name,
|
|
269
|
+
'episode_content': episode.content if episode is not None else '',
|
|
270
|
+
'previous_episodes': [ep.content for ep in previous_episodes]
|
|
271
|
+
if previous_episodes is not None
|
|
272
|
+
else [],
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
llm_response, node_summary_response = await asyncio.gather(
|
|
276
|
+
llm_client.generate_response(prompt_library.dedupe_nodes.node(context)),
|
|
277
|
+
llm_client.generate_response(
|
|
278
|
+
prompt_library.summarize_nodes.summarize_context(summary_context)
|
|
279
|
+
),
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
extracted_node.summary = node_summary_response.get('summary', '')
|
|
219
283
|
|
|
220
284
|
is_duplicate: bool = llm_response.get('is_duplicate', False)
|
|
221
285
|
uuid: str | None = llm_response.get('uuid', None)
|
|
222
|
-
|
|
286
|
+
name = llm_response.get('name', '')
|
|
223
287
|
|
|
224
288
|
node = extracted_node
|
|
225
289
|
uuid_map: dict[str, str] = {}
|
|
@@ -227,8 +291,14 @@ async def resolve_extracted_node(
|
|
|
227
291
|
for existing_node in existing_nodes:
|
|
228
292
|
if existing_node.uuid != uuid:
|
|
229
293
|
continue
|
|
294
|
+
summary_response = await llm_client.generate_response(
|
|
295
|
+
prompt_library.summarize_nodes.summarize_pair(
|
|
296
|
+
{'node_summaries': [extracted_node.summary, existing_node.summary]}
|
|
297
|
+
)
|
|
298
|
+
)
|
|
230
299
|
node = existing_node
|
|
231
|
-
node.
|
|
300
|
+
node.name = name
|
|
301
|
+
node.summary = summary_response.get('summary', '')
|
|
232
302
|
uuid_map[extracted_node.uuid] = existing_node.uuid
|
|
233
303
|
|
|
234
304
|
end = time()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: graphiti-core
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: A temporal graph building library
|
|
5
5
|
License: Apache-2.0
|
|
6
6
|
Author: Paul Paliychuk
|
|
@@ -14,9 +14,10 @@ Classifier: Programming Language :: Python :: 3.12
|
|
|
14
14
|
Requires-Dist: diskcache (>=5.6.3,<6.0.0)
|
|
15
15
|
Requires-Dist: neo4j (>=5.23.0,<6.0.0)
|
|
16
16
|
Requires-Dist: numpy (>=1.0.0)
|
|
17
|
-
Requires-Dist: openai (>=1.
|
|
17
|
+
Requires-Dist: openai (>=1.53.0,<2.0.0)
|
|
18
18
|
Requires-Dist: pydantic (>=2.8.2,<3.0.0)
|
|
19
|
-
Requires-Dist:
|
|
19
|
+
Requires-Dist: python-dotenv (>=1.0.1,<2.0.0)
|
|
20
|
+
Requires-Dist: tenacity (==9.0.0)
|
|
20
21
|
Description-Content-Type: text/markdown
|
|
21
22
|
|
|
22
23
|
<div align="center">
|
|
@@ -129,7 +130,7 @@ poetry add graphiti-core
|
|
|
129
130
|
```python
|
|
130
131
|
from graphiti_core import Graphiti
|
|
131
132
|
from graphiti_core.nodes import EpisodeType
|
|
132
|
-
from datetime import datetime
|
|
133
|
+
from datetime import datetime, timezone
|
|
133
134
|
|
|
134
135
|
# Initialize Graphiti
|
|
135
136
|
graphiti = Graphiti("bolt://localhost:7687", "neo4j", "password")
|
|
@@ -149,7 +150,7 @@ for i, episode in enumerate(episodes):
|
|
|
149
150
|
episode_body=episode,
|
|
150
151
|
source=EpisodeType.text,
|
|
151
152
|
source_description="podcast",
|
|
152
|
-
reference_time=datetime.now()
|
|
153
|
+
reference_time=datetime.now(timezone.utc)
|
|
153
154
|
)
|
|
154
155
|
|
|
155
156
|
# Search the graph
|
|
@@ -2,15 +2,15 @@ graphiti_core/__init__.py,sha256=e5SWFkRiaUwfprYIeIgVIh7JDedNiloZvd3roU-0aDY,55
|
|
|
2
2
|
graphiti_core/cross_encoder/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
3
|
graphiti_core/cross_encoder/bge_reranker_client.py,sha256=jsXBUHfFpGsNASHaRnfz1_miQ3x070DdU8QS4J3DciI,1466
|
|
4
4
|
graphiti_core/cross_encoder/client.py,sha256=PyFYYsALQAD9wu0gL5uquPsulmaBZ0AZkJmLq2DFA-c,1472
|
|
5
|
-
graphiti_core/cross_encoder/openai_reranker_client.py,sha256=
|
|
5
|
+
graphiti_core/cross_encoder/openai_reranker_client.py,sha256=F0S9ksusyxdlcRp4yRJuCyAEg-YqgdCwXjZbH8L-Xxo,4063
|
|
6
6
|
graphiti_core/edges.py,sha256=wKXmNXtu1deT5MfcqUNGHQlBLgwt3MJ_3j35ibNbpi8,13087
|
|
7
7
|
graphiti_core/embedder/__init__.py,sha256=eWd-0sPxflnYXLoWNT9sxwCIFun5JNO9Fk4E-ZXXf8Y,164
|
|
8
8
|
graphiti_core/embedder/client.py,sha256=gVr_xdN-d0UQfeE4Nyoa4pL3M8UxllNH4eFqmarxwys,1011
|
|
9
9
|
graphiti_core/embedder/openai.py,sha256=yYUYPymx_lBlxDTGrlc03yNhPFyGG-etM2sszRK2G2U,1618
|
|
10
10
|
graphiti_core/embedder/voyage.py,sha256=_eGFI5_NjNG8z7qG3jTWCdE7sAs1Yb8fiSZSJlQLD9o,1879
|
|
11
11
|
graphiti_core/errors.py,sha256=ddHrHGQxhwkVAtSph4AV84UoOlgwZufMczXPwB7uqPo,1795
|
|
12
|
-
graphiti_core/graphiti.py,sha256
|
|
13
|
-
graphiti_core/helpers.py,sha256=
|
|
12
|
+
graphiti_core/graphiti.py,sha256=-dJUa_spL0yRbGCUqWaAtZ1hlA2FaIzoHcIMT57KQYY,26919
|
|
13
|
+
graphiti_core/helpers.py,sha256=0kkfmswLxRek27WpeUolTBNyyu3_XQwl8xOLRSkAwqk,2207
|
|
14
14
|
graphiti_core/llm_client/__init__.py,sha256=PA80TSMeX-sUXITXEAxMDEt3gtfZgcJrGJUcyds1mSo,207
|
|
15
15
|
graphiti_core/llm_client/anthropic_client.py,sha256=4l2PbCjIoeRr7UJ2DUh2grYLTtE2vNaWlo72IIRQDeI,2405
|
|
16
16
|
graphiti_core/llm_client/client.py,sha256=WAnX0e4EuCFHXdFHeq_O1HZsW1STSByvDCFUHMAHEFU,3394
|
|
@@ -21,37 +21,37 @@ graphiti_core/llm_client/openai_client.py,sha256=xLkbpusRVFRK0zPr3kOqY31HK_XCXrp
|
|
|
21
21
|
graphiti_core/llm_client/utils.py,sha256=zKpxXEbKa369m4W7RDEf-m56kH46V1Mx3RowcWZEWWs,1000
|
|
22
22
|
graphiti_core/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
23
23
|
graphiti_core/models/edges/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
24
|
-
graphiti_core/models/edges/edge_db_queries.py,sha256=
|
|
24
|
+
graphiti_core/models/edges/edge_db_queries.py,sha256=2UoLkmazO-FJYqjc3g0LuL-pyjekzQxxed_XHVv_HZE,2671
|
|
25
25
|
graphiti_core/models/nodes/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
26
|
-
graphiti_core/models/nodes/node_db_queries.py,sha256=
|
|
27
|
-
graphiti_core/nodes.py,sha256=
|
|
26
|
+
graphiti_core/models/nodes/node_db_queries.py,sha256=I0top_N23FN0U5ZbypaS5IXvtfx2zgJmKUCT_7mpUdo,2257
|
|
27
|
+
graphiti_core/nodes.py,sha256=sIEa8QzF2w-qlxUakISoVb8oRsntc4xDU90q0kO9_pk,13866
|
|
28
28
|
graphiti_core/prompts/__init__.py,sha256=EA-x9xUki9l8wnu2l8ek_oNf75-do5tq5hVq7Zbv8Kw,101
|
|
29
|
-
graphiti_core/prompts/dedupe_edges.py,sha256=
|
|
30
|
-
graphiti_core/prompts/dedupe_nodes.py,sha256=
|
|
29
|
+
graphiti_core/prompts/dedupe_edges.py,sha256=7jff4x60p6x8K1Oy7EN8twdOvsTt67_SwoF-lvf2c-8,3539
|
|
30
|
+
graphiti_core/prompts/dedupe_nodes.py,sha256=_2zD0VcygH1Sut3FwRrKSBALFaxhnnO3JOFJkwHhEVo,4130
|
|
31
31
|
graphiti_core/prompts/eval.py,sha256=9gavc4SKAPdsrhpN8NEUTc632erkaifyOf0hevmdeKY,3657
|
|
32
|
-
graphiti_core/prompts/extract_edge_dates.py,sha256=
|
|
33
|
-
graphiti_core/prompts/extract_edges.py,sha256=
|
|
34
|
-
graphiti_core/prompts/extract_nodes.py,sha256=
|
|
32
|
+
graphiti_core/prompts/extract_edge_dates.py,sha256=fg5CPofLIuPR6x15ED4vwZQeRMcdfNrr1GDy_GTnfxg,3785
|
|
33
|
+
graphiti_core/prompts/extract_edges.py,sha256=6dVIKDxht5eQ3jlCRojUj0fZKtF_w-1ONmSlrpZ25uM,3461
|
|
34
|
+
graphiti_core/prompts/extract_nodes.py,sha256=OJ9zzU3VuuQXloNxDnn_BDB9z2lzpy8dL5O02ODNpzI,5494
|
|
35
35
|
graphiti_core/prompts/invalidate_edges.py,sha256=2vhi9TsL9poAHqApfk_Us0VveG0-T8cZymfBwOgA8tc,4341
|
|
36
36
|
graphiti_core/prompts/lib.py,sha256=ZOE6nNoI_wQ12Sufx7rQkQtkIm_eTAL7pCiYGU2hcMI,4054
|
|
37
37
|
graphiti_core/prompts/models.py,sha256=cvx_Bv5RMFUD_5IUawYrbpOKLPHogai7_bm7YXrSz84,867
|
|
38
|
-
graphiti_core/prompts/summarize_nodes.py,sha256=
|
|
38
|
+
graphiti_core/prompts/summarize_nodes.py,sha256=lNpagCq6Pz71vipBonQZUxBZG6OY4bQbyGZ5UEgdL4s,3488
|
|
39
39
|
graphiti_core/py.typed,sha256=vlmmzQOt7bmeQl9L3XJP4W6Ry0iiELepnOrinKz5KQg,79
|
|
40
40
|
graphiti_core/search/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
41
|
-
graphiti_core/search/search.py,sha256=
|
|
41
|
+
graphiti_core/search/search.py,sha256=qkiAZCYhKhTkzyGuSH9j_W2Kz60TGyhLM1hZNCaCeUs,11996
|
|
42
42
|
graphiti_core/search/search_config.py,sha256=UZN8jFA4pBlw2O5N1cuhVRBdTwMLR9N3Oyo6sQ4MDVw,3117
|
|
43
|
-
graphiti_core/search/search_config_recipes.py,sha256=
|
|
44
|
-
graphiti_core/search/search_utils.py,sha256=
|
|
43
|
+
graphiti_core/search/search_config_recipes.py,sha256=yUqiLnn9vFg39M8eVwjVKfBCL_ptGrfDMQ47m_Blb0g,6885
|
|
44
|
+
graphiti_core/search/search_utils.py,sha256=8Ro90L7fqtw5tsdusIhS_pdYcLX2JVIDiH5dD04G7ug,22968
|
|
45
45
|
graphiti_core/utils/__init__.py,sha256=cJAcMnBZdHBQmWrZdU1PQ1YmaL75bhVUkyVpIPuOyns,260
|
|
46
|
-
graphiti_core/utils/bulk_utils.py,sha256=
|
|
46
|
+
graphiti_core/utils/bulk_utils.py,sha256=AMi42deCXjIIgdpLEiD5SYCMZfX8XKoOsVYEwXVTX5A,14016
|
|
47
47
|
graphiti_core/utils/maintenance/__init__.py,sha256=TRY3wWWu5kn3Oahk_KKhltrWnh0NACw0FskjqF6OtlA,314
|
|
48
|
-
graphiti_core/utils/maintenance/community_operations.py,sha256=
|
|
49
|
-
graphiti_core/utils/maintenance/edge_operations.py,sha256=
|
|
48
|
+
graphiti_core/utils/maintenance/community_operations.py,sha256=a2ICCNwEognGx2oBL43Y6R4nNZYo-6Siia2kDVlwu_U,9838
|
|
49
|
+
graphiti_core/utils/maintenance/edge_operations.py,sha256=3VkyKfMPwH0ziCQRRsnmnCZXMSfOh2vFhBJSL3JEljU,12387
|
|
50
50
|
graphiti_core/utils/maintenance/graph_data_operations.py,sha256=w66_SLlvPapuG91YGGfR3bG2sM6cJ2XPHIaxM0slAdE,6526
|
|
51
|
-
graphiti_core/utils/maintenance/node_operations.py,sha256=
|
|
51
|
+
graphiti_core/utils/maintenance/node_operations.py,sha256=m0q-PPCI5LM0dZMkrcyUENw6WfDBMO61DQ7S6MuNQ8E,11624
|
|
52
52
|
graphiti_core/utils/maintenance/temporal_operations.py,sha256=MvaRLWrBlDeYw8CQrKish1xbYcY5ovpfdqA2hSX7v5k,3367
|
|
53
53
|
graphiti_core/utils/maintenance/utils.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
54
|
-
graphiti_core-0.
|
|
55
|
-
graphiti_core-0.
|
|
56
|
-
graphiti_core-0.
|
|
57
|
-
graphiti_core-0.
|
|
54
|
+
graphiti_core-0.4.0.dist-info/LICENSE,sha256=KCUwCyDXuVEgmDWkozHyniRyWjnWUWjkuDHfU6o3JlA,11325
|
|
55
|
+
graphiti_core-0.4.0.dist-info/METADATA,sha256=URZ0gW5i_Q9aOe5M60tsxFsYbAwqoW7HJc8oVyrzuFk,10058
|
|
56
|
+
graphiti_core-0.4.0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
|
|
57
|
+
graphiti_core-0.4.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|