graphiti-core 0.10.5__py3-none-any.whl → 0.11.1__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/edges.py +32 -57
- graphiti_core/embedder/client.py +3 -0
- graphiti_core/embedder/gemini.py +10 -0
- graphiti_core/embedder/openai.py +6 -0
- graphiti_core/embedder/voyage.py +7 -0
- graphiti_core/graphiti.py +42 -138
- graphiti_core/graphiti_types.py +31 -0
- graphiti_core/helpers.py +6 -1
- graphiti_core/models/edges/edge_db_queries.py +1 -1
- graphiti_core/nodes.py +8 -2
- graphiti_core/prompts/dedupe_edges.py +5 -7
- graphiti_core/prompts/dedupe_nodes.py +8 -21
- graphiti_core/prompts/extract_edges.py +61 -26
- graphiti_core/prompts/extract_nodes.py +89 -18
- graphiti_core/prompts/invalidate_edges.py +11 -11
- graphiti_core/search/search.py +13 -5
- graphiti_core/search/search_utils.py +208 -82
- graphiti_core/utils/bulk_utils.py +10 -7
- graphiti_core/utils/maintenance/edge_operations.py +88 -40
- graphiti_core/utils/maintenance/graph_data_operations.py +9 -3
- graphiti_core/utils/maintenance/node_operations.py +217 -223
- graphiti_core/utils/maintenance/temporal_operations.py +4 -11
- {graphiti_core-0.10.5.dist-info → graphiti_core-0.11.1.dist-info}/METADATA +14 -8
- {graphiti_core-0.10.5.dist-info → graphiti_core-0.11.1.dist-info}/RECORD +26 -25
- {graphiti_core-0.10.5.dist-info → graphiti_core-0.11.1.dist-info}/LICENSE +0 -0
- {graphiti_core-0.10.5.dist-info → graphiti_core-0.11.1.dist-info}/WHEEL +0 -0
|
@@ -20,80 +20,26 @@ from time import time
|
|
|
20
20
|
from typing import Any
|
|
21
21
|
|
|
22
22
|
import pydantic
|
|
23
|
-
from pydantic import BaseModel
|
|
23
|
+
from pydantic import BaseModel, Field
|
|
24
24
|
|
|
25
|
+
from graphiti_core.graphiti_types import GraphitiClients
|
|
25
26
|
from graphiti_core.helpers import MAX_REFLEXION_ITERATIONS, semaphore_gather
|
|
26
27
|
from graphiti_core.llm_client import LLMClient
|
|
27
|
-
from graphiti_core.nodes import EntityNode, EpisodeType, EpisodicNode
|
|
28
|
+
from graphiti_core.nodes import EntityNode, EpisodeType, EpisodicNode, create_entity_node_embeddings
|
|
28
29
|
from graphiti_core.prompts import prompt_library
|
|
29
30
|
from graphiti_core.prompts.dedupe_nodes import NodeDuplicate
|
|
30
|
-
from graphiti_core.prompts.extract_nodes import
|
|
31
|
-
|
|
31
|
+
from graphiti_core.prompts.extract_nodes import (
|
|
32
|
+
ExtractedEntities,
|
|
33
|
+
ExtractedEntity,
|
|
34
|
+
MissedEntities,
|
|
35
|
+
)
|
|
36
|
+
from graphiti_core.search.search_filters import SearchFilters
|
|
37
|
+
from graphiti_core.search.search_utils import get_relevant_nodes
|
|
32
38
|
from graphiti_core.utils.datetime_utils import utc_now
|
|
33
39
|
|
|
34
40
|
logger = logging.getLogger(__name__)
|
|
35
41
|
|
|
36
42
|
|
|
37
|
-
async def extract_message_nodes(
|
|
38
|
-
llm_client: LLMClient,
|
|
39
|
-
episode: EpisodicNode,
|
|
40
|
-
previous_episodes: list[EpisodicNode],
|
|
41
|
-
custom_prompt='',
|
|
42
|
-
) -> list[str]:
|
|
43
|
-
# Prepare context for LLM
|
|
44
|
-
context = {
|
|
45
|
-
'episode_content': episode.content,
|
|
46
|
-
'episode_timestamp': episode.valid_at.isoformat(),
|
|
47
|
-
'previous_episodes': [ep.content for ep in previous_episodes],
|
|
48
|
-
'custom_prompt': custom_prompt,
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
llm_response = await llm_client.generate_response(
|
|
52
|
-
prompt_library.extract_nodes.extract_message(context), response_model=ExtractedNodes
|
|
53
|
-
)
|
|
54
|
-
extracted_node_names = llm_response.get('extracted_node_names', [])
|
|
55
|
-
return extracted_node_names
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
async def extract_text_nodes(
|
|
59
|
-
llm_client: LLMClient,
|
|
60
|
-
episode: EpisodicNode,
|
|
61
|
-
previous_episodes: list[EpisodicNode],
|
|
62
|
-
custom_prompt='',
|
|
63
|
-
) -> list[str]:
|
|
64
|
-
# Prepare context for LLM
|
|
65
|
-
context = {
|
|
66
|
-
'episode_content': episode.content,
|
|
67
|
-
'episode_timestamp': episode.valid_at.isoformat(),
|
|
68
|
-
'previous_episodes': [ep.content for ep in previous_episodes],
|
|
69
|
-
'custom_prompt': custom_prompt,
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
llm_response = await llm_client.generate_response(
|
|
73
|
-
prompt_library.extract_nodes.extract_text(context), ExtractedNodes
|
|
74
|
-
)
|
|
75
|
-
extracted_node_names = llm_response.get('extracted_node_names', [])
|
|
76
|
-
return extracted_node_names
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
async def extract_json_nodes(
|
|
80
|
-
llm_client: LLMClient, episode: EpisodicNode, custom_prompt=''
|
|
81
|
-
) -> list[str]:
|
|
82
|
-
# Prepare context for LLM
|
|
83
|
-
context = {
|
|
84
|
-
'episode_content': episode.content,
|
|
85
|
-
'episode_timestamp': episode.valid_at.isoformat(),
|
|
86
|
-
'source_description': episode.source_description,
|
|
87
|
-
'custom_prompt': custom_prompt,
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
llm_response = await llm_client.generate_response(
|
|
91
|
-
prompt_library.extract_nodes.extract_json(context), ExtractedNodes
|
|
92
|
-
)
|
|
93
|
-
extracted_node_names = llm_response.get('extracted_node_names', [])
|
|
94
|
-
return extracted_node_names
|
|
95
|
-
|
|
96
|
-
|
|
97
43
|
async def extract_nodes_reflexion(
|
|
98
44
|
llm_client: LLMClient,
|
|
99
45
|
episode: EpisodicNode,
|
|
@@ -116,97 +62,109 @@ async def extract_nodes_reflexion(
|
|
|
116
62
|
|
|
117
63
|
|
|
118
64
|
async def extract_nodes(
|
|
119
|
-
|
|
65
|
+
clients: GraphitiClients,
|
|
120
66
|
episode: EpisodicNode,
|
|
121
67
|
previous_episodes: list[EpisodicNode],
|
|
122
68
|
entity_types: dict[str, BaseModel] | None = None,
|
|
123
69
|
) -> list[EntityNode]:
|
|
124
70
|
start = time()
|
|
125
|
-
|
|
71
|
+
llm_client = clients.llm_client
|
|
72
|
+
embedder = clients.embedder
|
|
73
|
+
llm_response = {}
|
|
126
74
|
custom_prompt = ''
|
|
127
75
|
entities_missed = True
|
|
128
76
|
reflexion_iterations = 0
|
|
129
|
-
|
|
77
|
+
|
|
78
|
+
entity_types_context = [
|
|
79
|
+
{
|
|
80
|
+
'entity_type_id': 0,
|
|
81
|
+
'entity_type_name': 'Entity',
|
|
82
|
+
'entity_type_description': 'Default entity classification. Use this entity type if the entity is not one of the other listed types.',
|
|
83
|
+
}
|
|
84
|
+
]
|
|
85
|
+
|
|
86
|
+
entity_types_context += (
|
|
87
|
+
[
|
|
88
|
+
{
|
|
89
|
+
'entity_type_id': i + 1,
|
|
90
|
+
'entity_type_name': type_name,
|
|
91
|
+
'entity_type_description': type_model.__doc__,
|
|
92
|
+
}
|
|
93
|
+
for i, (type_name, type_model) in enumerate(entity_types.items())
|
|
94
|
+
]
|
|
95
|
+
if entity_types is not None
|
|
96
|
+
else []
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
context = {
|
|
100
|
+
'episode_content': episode.content,
|
|
101
|
+
'episode_timestamp': episode.valid_at.isoformat(),
|
|
102
|
+
'previous_episodes': [ep.content for ep in previous_episodes],
|
|
103
|
+
'custom_prompt': custom_prompt,
|
|
104
|
+
'entity_types': entity_types_context,
|
|
105
|
+
'source_description': episode.source_description,
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
while entities_missed and reflexion_iterations <= MAX_REFLEXION_ITERATIONS:
|
|
130
109
|
if episode.source == EpisodeType.message:
|
|
131
|
-
|
|
132
|
-
|
|
110
|
+
llm_response = await llm_client.generate_response(
|
|
111
|
+
prompt_library.extract_nodes.extract_message(context),
|
|
112
|
+
response_model=ExtractedEntities,
|
|
133
113
|
)
|
|
134
114
|
elif episode.source == EpisodeType.text:
|
|
135
|
-
|
|
136
|
-
|
|
115
|
+
llm_response = await llm_client.generate_response(
|
|
116
|
+
prompt_library.extract_nodes.extract_text(context), response_model=ExtractedEntities
|
|
137
117
|
)
|
|
138
118
|
elif episode.source == EpisodeType.json:
|
|
139
|
-
|
|
119
|
+
llm_response = await llm_client.generate_response(
|
|
120
|
+
prompt_library.extract_nodes.extract_json(context), response_model=ExtractedEntities
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
extracted_entities: list[ExtractedEntity] = [
|
|
124
|
+
ExtractedEntity(**entity_types_context)
|
|
125
|
+
for entity_types_context in llm_response.get('extracted_entities', [])
|
|
126
|
+
]
|
|
140
127
|
|
|
141
128
|
reflexion_iterations += 1
|
|
142
129
|
if reflexion_iterations < MAX_REFLEXION_ITERATIONS:
|
|
143
130
|
missing_entities = await extract_nodes_reflexion(
|
|
144
|
-
llm_client,
|
|
131
|
+
llm_client,
|
|
132
|
+
episode,
|
|
133
|
+
previous_episodes,
|
|
134
|
+
[entity.name for entity in extracted_entities],
|
|
145
135
|
)
|
|
146
136
|
|
|
147
137
|
entities_missed = len(missing_entities) != 0
|
|
148
138
|
|
|
149
|
-
custom_prompt = '
|
|
139
|
+
custom_prompt = 'Make sure that the following entities are extracted: '
|
|
150
140
|
for entity in missing_entities:
|
|
151
141
|
custom_prompt += f'\n{entity},'
|
|
152
142
|
|
|
153
|
-
node_classification_context = {
|
|
154
|
-
'episode_content': episode.content,
|
|
155
|
-
'previous_episodes': [ep.content for ep in previous_episodes],
|
|
156
|
-
'extracted_entities': extracted_node_names,
|
|
157
|
-
'entity_types': {
|
|
158
|
-
type_name: values.model_json_schema().get('description')
|
|
159
|
-
for type_name, values in entity_types.items()
|
|
160
|
-
}
|
|
161
|
-
if entity_types is not None
|
|
162
|
-
else {},
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
node_classifications: dict[str, str | None] = {}
|
|
166
|
-
|
|
167
|
-
if entity_types is not None:
|
|
168
|
-
try:
|
|
169
|
-
llm_response = await llm_client.generate_response(
|
|
170
|
-
prompt_library.extract_nodes.classify_nodes(node_classification_context),
|
|
171
|
-
response_model=EntityClassification,
|
|
172
|
-
)
|
|
173
|
-
entity_classifications = llm_response.get('entity_classifications', [])
|
|
174
|
-
node_classifications.update(
|
|
175
|
-
{
|
|
176
|
-
entity_classification.get('name'): entity_classification.get('entity_type')
|
|
177
|
-
for entity_classification in entity_classifications
|
|
178
|
-
}
|
|
179
|
-
)
|
|
180
|
-
# catch classification errors and continue if we can't classify
|
|
181
|
-
except Exception as e:
|
|
182
|
-
logger.exception(e)
|
|
183
|
-
|
|
184
143
|
end = time()
|
|
185
|
-
logger.debug(f'Extracted new nodes: {
|
|
144
|
+
logger.debug(f'Extracted new nodes: {extracted_entities} in {(end - start) * 1000} ms')
|
|
186
145
|
# Convert the extracted data into EntityNode objects
|
|
187
|
-
|
|
188
|
-
for
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
entity_type = None
|
|
192
|
-
|
|
193
|
-
labels = (
|
|
194
|
-
['Entity']
|
|
195
|
-
if entity_type is None or entity_type == 'None' or entity_type == 'null'
|
|
196
|
-
else ['Entity', entity_type]
|
|
146
|
+
extracted_nodes = []
|
|
147
|
+
for extracted_entity in extracted_entities:
|
|
148
|
+
entity_type_name = entity_types_context[extracted_entity.entity_type_id].get(
|
|
149
|
+
'entity_type_name'
|
|
197
150
|
)
|
|
198
151
|
|
|
152
|
+
labels: list[str] = list({'Entity', str(entity_type_name)})
|
|
153
|
+
|
|
199
154
|
new_node = EntityNode(
|
|
200
|
-
name=name,
|
|
155
|
+
name=extracted_entity.name,
|
|
201
156
|
group_id=episode.group_id,
|
|
202
157
|
labels=labels,
|
|
203
158
|
summary='',
|
|
204
159
|
created_at=utc_now(),
|
|
205
160
|
)
|
|
206
|
-
|
|
161
|
+
extracted_nodes.append(new_node)
|
|
207
162
|
logger.debug(f'Created new node: {new_node.name} (UUID: {new_node.uuid})')
|
|
208
163
|
|
|
209
|
-
|
|
164
|
+
await create_entity_node_embeddings(embedder, extracted_nodes)
|
|
165
|
+
|
|
166
|
+
logger.debug(f'Extracted nodes: {[(n.name, n.uuid) for n in extracted_nodes]}')
|
|
167
|
+
return extracted_nodes
|
|
210
168
|
|
|
211
169
|
|
|
212
170
|
async def dedupe_extracted_nodes(
|
|
@@ -260,36 +218,45 @@ async def dedupe_extracted_nodes(
|
|
|
260
218
|
|
|
261
219
|
|
|
262
220
|
async def resolve_extracted_nodes(
|
|
263
|
-
|
|
221
|
+
clients: GraphitiClients,
|
|
264
222
|
extracted_nodes: list[EntityNode],
|
|
265
|
-
existing_nodes_lists: list[list[EntityNode]],
|
|
266
223
|
episode: EpisodicNode | None = None,
|
|
267
224
|
previous_episodes: list[EpisodicNode] | None = None,
|
|
268
225
|
entity_types: dict[str, BaseModel] | None = None,
|
|
269
226
|
) -> tuple[list[EntityNode], dict[str, str]]:
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
227
|
+
llm_client = clients.llm_client
|
|
228
|
+
driver = clients.driver
|
|
229
|
+
|
|
230
|
+
# Find relevant nodes already in the graph
|
|
231
|
+
existing_nodes_lists: list[list[EntityNode]] = await get_relevant_nodes(
|
|
232
|
+
driver, extracted_nodes, SearchFilters()
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
resolved_nodes: list[EntityNode] = await semaphore_gather(
|
|
236
|
+
*[
|
|
237
|
+
resolve_extracted_node(
|
|
238
|
+
llm_client,
|
|
239
|
+
extracted_node,
|
|
240
|
+
existing_nodes,
|
|
241
|
+
episode,
|
|
242
|
+
previous_episodes,
|
|
243
|
+
entity_types.get(
|
|
244
|
+
next((item for item in extracted_node.labels if item != 'Entity'), '')
|
|
285
245
|
)
|
|
286
|
-
|
|
287
|
-
|
|
246
|
+
if entity_types is not None
|
|
247
|
+
else None,
|
|
248
|
+
)
|
|
249
|
+
for extracted_node, existing_nodes in zip(
|
|
250
|
+
extracted_nodes, existing_nodes_lists, strict=True
|
|
251
|
+
)
|
|
252
|
+
]
|
|
288
253
|
)
|
|
289
254
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
255
|
+
uuid_map: dict[str, str] = {}
|
|
256
|
+
for extracted_node, resolved_node in zip(extracted_nodes, resolved_nodes, strict=True):
|
|
257
|
+
uuid_map[extracted_node.uuid] = resolved_node.uuid
|
|
258
|
+
|
|
259
|
+
logger.debug(f'Resolved nodes: {[(n.name, n.uuid) for n in resolved_nodes]}')
|
|
293
260
|
|
|
294
261
|
return resolved_nodes, uuid_map
|
|
295
262
|
|
|
@@ -300,124 +267,151 @@ async def resolve_extracted_node(
|
|
|
300
267
|
existing_nodes: list[EntityNode],
|
|
301
268
|
episode: EpisodicNode | None = None,
|
|
302
269
|
previous_episodes: list[EpisodicNode] | None = None,
|
|
303
|
-
|
|
304
|
-
) ->
|
|
270
|
+
entity_type: BaseModel | None = None,
|
|
271
|
+
) -> EntityNode:
|
|
305
272
|
start = time()
|
|
273
|
+
if len(existing_nodes) == 0:
|
|
274
|
+
return extracted_node
|
|
306
275
|
|
|
307
276
|
# Prepare context for LLM
|
|
308
277
|
existing_nodes_context = [
|
|
309
|
-
{
|
|
310
|
-
|
|
278
|
+
{
|
|
279
|
+
**{
|
|
280
|
+
'id': i,
|
|
281
|
+
'name': node.name,
|
|
282
|
+
'entity_types': node.labels,
|
|
283
|
+
'summary': node.summary,
|
|
284
|
+
},
|
|
285
|
+
**node.attributes,
|
|
286
|
+
}
|
|
287
|
+
for i, node in enumerate(existing_nodes)
|
|
311
288
|
]
|
|
312
289
|
|
|
313
290
|
extracted_node_context = {
|
|
314
|
-
'uuid': extracted_node.uuid,
|
|
315
291
|
'name': extracted_node.name,
|
|
316
|
-
'
|
|
292
|
+
'entity_type': entity_type.__name__ if entity_type is not None else 'Entity', # type: ignore
|
|
293
|
+
'entity_type_description': entity_type.__doc__
|
|
294
|
+
if entity_type is not None
|
|
295
|
+
else 'Default Entity Type',
|
|
317
296
|
}
|
|
318
297
|
|
|
319
298
|
context = {
|
|
320
299
|
'existing_nodes': existing_nodes_context,
|
|
321
|
-
'
|
|
300
|
+
'extracted_node': extracted_node_context,
|
|
322
301
|
'episode_content': episode.content if episode is not None else '',
|
|
323
302
|
'previous_episodes': [ep.content for ep in previous_episodes]
|
|
324
303
|
if previous_episodes is not None
|
|
325
304
|
else [],
|
|
326
305
|
}
|
|
327
306
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
'episode_content': episode.content if episode is not None else '',
|
|
332
|
-
'previous_episodes': [ep.content for ep in previous_episodes]
|
|
333
|
-
if previous_episodes is not None
|
|
334
|
-
else [],
|
|
335
|
-
}
|
|
307
|
+
llm_response = await llm_client.generate_response(
|
|
308
|
+
prompt_library.dedupe_nodes.node(context), response_model=NodeDuplicate
|
|
309
|
+
)
|
|
336
310
|
|
|
337
|
-
|
|
311
|
+
duplicate_id: int = llm_response.get('duplicate_node_id', -1)
|
|
338
312
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
filter(
|
|
343
|
-
lambda x: x is not None, # type: ignore
|
|
344
|
-
[entity_types.get(entity_type) for entity_type in extracted_node.labels], # type: ignore
|
|
345
|
-
)
|
|
346
|
-
)
|
|
313
|
+
node = (
|
|
314
|
+
existing_nodes[duplicate_id] if 0 <= duplicate_id < len(existing_nodes) else extracted_node
|
|
315
|
+
)
|
|
347
316
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
'attribute_name': field_name,
|
|
353
|
-
'attribute_description': field_info.description or '',
|
|
354
|
-
}
|
|
355
|
-
)
|
|
317
|
+
end = time()
|
|
318
|
+
logger.debug(
|
|
319
|
+
f'Resolved node: {extracted_node.name} is {node.name}, in {(end - start) * 1000} ms'
|
|
320
|
+
)
|
|
356
321
|
|
|
357
|
-
|
|
322
|
+
return node
|
|
358
323
|
|
|
359
|
-
entity_attributes_model = pydantic.create_model( # type: ignore
|
|
360
|
-
'EntityAttributes',
|
|
361
|
-
__base__=entity_type_classes + (Summary,), # type: ignore
|
|
362
|
-
)
|
|
363
324
|
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
325
|
+
async def extract_attributes_from_nodes(
|
|
326
|
+
clients: GraphitiClients,
|
|
327
|
+
nodes: list[EntityNode],
|
|
328
|
+
episode: EpisodicNode | None = None,
|
|
329
|
+
previous_episodes: list[EpisodicNode] | None = None,
|
|
330
|
+
entity_types: dict[str, BaseModel] | None = None,
|
|
331
|
+
) -> list[EntityNode]:
|
|
332
|
+
llm_client = clients.llm_client
|
|
333
|
+
embedder = clients.embedder
|
|
334
|
+
|
|
335
|
+
updated_nodes: list[EntityNode] = await semaphore_gather(
|
|
336
|
+
*[
|
|
337
|
+
extract_attributes_from_node(
|
|
338
|
+
llm_client,
|
|
339
|
+
node,
|
|
340
|
+
episode,
|
|
341
|
+
previous_episodes,
|
|
342
|
+
entity_types.get(next((item for item in node.labels if item != 'Entity'), ''))
|
|
343
|
+
if entity_types is not None
|
|
344
|
+
else None,
|
|
345
|
+
)
|
|
346
|
+
for node in nodes
|
|
347
|
+
]
|
|
372
348
|
)
|
|
373
349
|
|
|
374
|
-
|
|
375
|
-
node_attributes = {
|
|
376
|
-
key: value if (value != 'None' or key == 'summary') else None
|
|
377
|
-
for key, value in node_attributes_response.items()
|
|
378
|
-
}
|
|
350
|
+
await create_entity_node_embeddings(embedder, updated_nodes)
|
|
379
351
|
|
|
380
|
-
|
|
381
|
-
del node_attributes['summary']
|
|
352
|
+
return updated_nodes
|
|
382
353
|
|
|
383
|
-
extracted_node.attributes.update(node_attributes)
|
|
384
354
|
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
355
|
+
async def extract_attributes_from_node(
|
|
356
|
+
llm_client: LLMClient,
|
|
357
|
+
node: EntityNode,
|
|
358
|
+
episode: EpisodicNode | None = None,
|
|
359
|
+
previous_episodes: list[EpisodicNode] | None = None,
|
|
360
|
+
entity_type: BaseModel | None = None,
|
|
361
|
+
) -> EntityNode:
|
|
362
|
+
node_context: dict[str, Any] = {
|
|
363
|
+
'name': node.name,
|
|
364
|
+
'summary': node.summary,
|
|
365
|
+
'entity_types': node.labels,
|
|
366
|
+
'attributes': node.attributes,
|
|
367
|
+
}
|
|
388
368
|
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
369
|
+
attributes_definitions: dict[str, Any] = {
|
|
370
|
+
'summary': (
|
|
371
|
+
str,
|
|
372
|
+
Field(
|
|
373
|
+
description='Summary containing the important information about the entity. Under 200 words',
|
|
374
|
+
),
|
|
375
|
+
),
|
|
376
|
+
'name': (
|
|
377
|
+
str,
|
|
378
|
+
Field(description='Name of the ENTITY'),
|
|
379
|
+
),
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
if entity_type is not None:
|
|
383
|
+
for field_name, field_info in entity_type.model_fields.items():
|
|
384
|
+
attributes_definitions[field_name] = (
|
|
385
|
+
field_info.annotation,
|
|
386
|
+
Field(description=field_info.description),
|
|
400
387
|
)
|
|
401
|
-
node = existing_node
|
|
402
|
-
node.name = name
|
|
403
|
-
node.summary = summary_response.get('summary', '')
|
|
404
388
|
|
|
405
|
-
|
|
406
|
-
existing_attributes = existing_node.attributes
|
|
407
|
-
for attribute_name, attribute_value in existing_attributes.items():
|
|
408
|
-
if new_attributes.get(attribute_name) is None:
|
|
409
|
-
new_attributes[attribute_name] = attribute_value
|
|
410
|
-
node.attributes = new_attributes
|
|
411
|
-
node.labels = list(set(existing_node.labels + extracted_node.labels))
|
|
389
|
+
entity_attributes_model = pydantic.create_model('EntityAttributes', **attributes_definitions)
|
|
412
390
|
|
|
413
|
-
|
|
391
|
+
summary_context: dict[str, Any] = {
|
|
392
|
+
'node': node_context,
|
|
393
|
+
'episode_content': episode.content if episode is not None else '',
|
|
394
|
+
'previous_episodes': [ep.content for ep in previous_episodes]
|
|
395
|
+
if previous_episodes is not None
|
|
396
|
+
else [],
|
|
397
|
+
}
|
|
414
398
|
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
399
|
+
llm_response = await llm_client.generate_response(
|
|
400
|
+
prompt_library.extract_nodes.extract_attributes(summary_context),
|
|
401
|
+
response_model=entity_attributes_model,
|
|
418
402
|
)
|
|
419
403
|
|
|
420
|
-
|
|
404
|
+
node.summary = llm_response.get('summary', node.summary)
|
|
405
|
+
node.name = llm_response.get('name', node.name)
|
|
406
|
+
node_attributes = {key: value for key, value in llm_response.items()}
|
|
407
|
+
|
|
408
|
+
with suppress(KeyError):
|
|
409
|
+
del node_attributes['summary']
|
|
410
|
+
del node_attributes['name']
|
|
411
|
+
|
|
412
|
+
node.attributes.update(node_attributes)
|
|
413
|
+
|
|
414
|
+
return node
|
|
421
415
|
|
|
422
416
|
|
|
423
417
|
async def dedupe_node_list(
|
|
@@ -72,12 +72,10 @@ async def get_edge_contradictions(
|
|
|
72
72
|
llm_client: LLMClient, new_edge: EntityEdge, existing_edges: list[EntityEdge]
|
|
73
73
|
) -> list[EntityEdge]:
|
|
74
74
|
start = time()
|
|
75
|
-
existing_edge_map = {edge.uuid: edge for edge in existing_edges}
|
|
76
75
|
|
|
77
|
-
new_edge_context = {'
|
|
76
|
+
new_edge_context = {'fact': new_edge.fact}
|
|
78
77
|
existing_edge_context = [
|
|
79
|
-
{'
|
|
80
|
-
for existing_edge in existing_edges
|
|
78
|
+
{'id': i, 'fact': existing_edge.fact} for i, existing_edge in enumerate(existing_edges)
|
|
81
79
|
]
|
|
82
80
|
|
|
83
81
|
context = {'new_edge': new_edge_context, 'existing_edges': existing_edge_context}
|
|
@@ -86,14 +84,9 @@ async def get_edge_contradictions(
|
|
|
86
84
|
prompt_library.invalidate_edges.v2(context), response_model=InvalidatedEdges
|
|
87
85
|
)
|
|
88
86
|
|
|
89
|
-
|
|
87
|
+
contradicted_facts: list[int] = llm_response.get('contradicted_facts', [])
|
|
90
88
|
|
|
91
|
-
contradicted_edges: list[EntityEdge] = []
|
|
92
|
-
for edge_data in contradicted_edge_data:
|
|
93
|
-
if edge_data['uuid'] in existing_edge_map:
|
|
94
|
-
contradicted_edge = existing_edge_map[edge_data['uuid']]
|
|
95
|
-
contradicted_edge.fact = edge_data['fact']
|
|
96
|
-
contradicted_edges.append(contradicted_edge)
|
|
89
|
+
contradicted_edges: list[EntityEdge] = [existing_edges[i] for i in contradicted_facts]
|
|
97
90
|
|
|
98
91
|
end = time()
|
|
99
92
|
logger.debug(
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: graphiti-core
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.11.1
|
|
4
4
|
Summary: A temporal graph building library
|
|
5
5
|
License: Apache-2.0
|
|
6
6
|
Author: Paul Paliychuk
|
|
@@ -42,11 +42,11 @@ Graphiti
|
|
|
42
42
|
<h2 align="center"> Build Real-Time Knowledge Graphs for AI Agents</h2>
|
|
43
43
|
<div align="center">
|
|
44
44
|
|
|
45
|
-
|
|
46
45
|
[](https://github.com/getzep/Graphiti/actions/workflows/lint.yml)
|
|
47
46
|
[](https://github.com/getzep/Graphiti/actions/workflows/unit_tests.yml)
|
|
48
47
|
[](https://github.com/getzep/Graphiti/actions/workflows/typecheck.yml)
|
|
49
48
|
|
|
49
|
+

|
|
50
50
|
[](https://discord.com/invite/W8Kw6bsgXQ)
|
|
51
51
|
[](https://arxiv.org/abs/2501.13956)
|
|
52
52
|
[](https://github.com/getzep/graphiti/releases)
|
|
@@ -55,10 +55,16 @@ Graphiti
|
|
|
55
55
|
<div align="center">
|
|
56
56
|
|
|
57
57
|
<a href="https://trendshift.io/repositories/12986" target="_blank"><img src="https://trendshift.io/api/badge/repositories/12986" alt="getzep%2Fgraphiti | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
|
58
|
+
|
|
58
59
|
</div>
|
|
60
|
+
|
|
59
61
|
:star: _Help us reach more developers and grow the Graphiti community. Star this repo!_
|
|
62
|
+
|
|
60
63
|
<br />
|
|
61
64
|
|
|
65
|
+
> [!TIP]
|
|
66
|
+
> Check out the new [MCP server for Graphiti](mcp_server/README.md)! Give Claude, Cursor, and other MCP clients powerful Knowledge Graph-based memory.
|
|
67
|
+
|
|
62
68
|
Graphiti is a framework for building and querying temporally-aware knowledge graphs, specifically tailored for AI agents operating in dynamic environments. Unlike traditional retrieval-augmented generation (RAG) methods, Graphiti continuously integrates user interactions, structured and unstructured enterprise data, and external information into a coherent, queryable graph. The framework supports incremental data updates, efficient retrieval, and precise historical queries without requiring complete graph recomputation, making it suitable for developing interactive, context-aware AI applications.
|
|
63
69
|
|
|
64
70
|
Use Graphiti to:
|
|
@@ -191,12 +197,6 @@ For a complete working example, see the [Quickstart Example](./examples/quicksta
|
|
|
191
197
|
|
|
192
198
|
The example is fully documented with clear explanations of each functionality and includes a comprehensive README with setup instructions and next steps.
|
|
193
199
|
|
|
194
|
-
## Graph Service
|
|
195
|
-
|
|
196
|
-
The `server` directory contains an API service for interacting with the Graphiti API. It is built using FastAPI.
|
|
197
|
-
|
|
198
|
-
Please see the [server README](./server/README.md) for more information.
|
|
199
|
-
|
|
200
200
|
## MCP Server
|
|
201
201
|
|
|
202
202
|
The `mcp_server` directory contains a Model Context Protocol (MCP) server implementation for Graphiti. This server allows AI assistants to interact with Graphiti's knowledge graph capabilities through the MCP protocol.
|
|
@@ -213,6 +213,12 @@ The MCP server can be deployed using Docker with Neo4j, making it easy to integr
|
|
|
213
213
|
|
|
214
214
|
For detailed setup instructions and usage examples, see the [MCP server README](./mcp_server/README.md).
|
|
215
215
|
|
|
216
|
+
## REST Service
|
|
217
|
+
|
|
218
|
+
The `server` directory contains an API service for interacting with the Graphiti API. It is built using FastAPI.
|
|
219
|
+
|
|
220
|
+
Please see the [server README](./server/README.md) for more information.
|
|
221
|
+
|
|
216
222
|
## Optional Environment Variables
|
|
217
223
|
|
|
218
224
|
In addition to the Neo4j and OpenAi-compatible credentials, Graphiti also has a few optional environment variables.
|