graphiti-core 0.10.5__py3-none-any.whl → 0.11.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/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 +204 -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 +216 -223
- graphiti_core/utils/maintenance/temporal_operations.py +4 -11
- {graphiti_core-0.10.5.dist-info → graphiti_core-0.11.0.dist-info}/METADATA +14 -8
- {graphiti_core-0.10.5.dist-info → graphiti_core-0.11.0.dist-info}/RECORD +26 -25
- {graphiti_core-0.10.5.dist-info → graphiti_core-0.11.0.dist-info}/LICENSE +0 -0
- {graphiti_core-0.10.5.dist-info → graphiti_core-0.11.0.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,108 @@ 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
|
+
}
|
|
106
|
+
|
|
107
|
+
while entities_missed and reflexion_iterations <= MAX_REFLEXION_ITERATIONS:
|
|
130
108
|
if episode.source == EpisodeType.message:
|
|
131
|
-
|
|
132
|
-
|
|
109
|
+
llm_response = await llm_client.generate_response(
|
|
110
|
+
prompt_library.extract_nodes.extract_message(context),
|
|
111
|
+
response_model=ExtractedEntities,
|
|
133
112
|
)
|
|
134
113
|
elif episode.source == EpisodeType.text:
|
|
135
|
-
|
|
136
|
-
|
|
114
|
+
llm_response = await llm_client.generate_response(
|
|
115
|
+
prompt_library.extract_nodes.extract_text(context), response_model=ExtractedEntities
|
|
137
116
|
)
|
|
138
117
|
elif episode.source == EpisodeType.json:
|
|
139
|
-
|
|
118
|
+
llm_response = await llm_client.generate_response(
|
|
119
|
+
prompt_library.extract_nodes.extract_json(context), response_model=ExtractedEntities
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
extracted_entities: list[ExtractedEntity] = [
|
|
123
|
+
ExtractedEntity(**entity_types_context)
|
|
124
|
+
for entity_types_context in llm_response.get('extracted_entities', [])
|
|
125
|
+
]
|
|
140
126
|
|
|
141
127
|
reflexion_iterations += 1
|
|
142
128
|
if reflexion_iterations < MAX_REFLEXION_ITERATIONS:
|
|
143
129
|
missing_entities = await extract_nodes_reflexion(
|
|
144
|
-
llm_client,
|
|
130
|
+
llm_client,
|
|
131
|
+
episode,
|
|
132
|
+
previous_episodes,
|
|
133
|
+
[entity.name for entity in extracted_entities],
|
|
145
134
|
)
|
|
146
135
|
|
|
147
136
|
entities_missed = len(missing_entities) != 0
|
|
148
137
|
|
|
149
|
-
custom_prompt = '
|
|
138
|
+
custom_prompt = 'Make sure that the following entities are extracted: '
|
|
150
139
|
for entity in missing_entities:
|
|
151
140
|
custom_prompt += f'\n{entity},'
|
|
152
141
|
|
|
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
142
|
end = time()
|
|
185
|
-
logger.debug(f'Extracted new nodes: {
|
|
143
|
+
logger.debug(f'Extracted new nodes: {extracted_entities} in {(end - start) * 1000} ms')
|
|
186
144
|
# 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]
|
|
145
|
+
extracted_nodes = []
|
|
146
|
+
for extracted_entity in extracted_entities:
|
|
147
|
+
entity_type_name = entity_types_context[extracted_entity.entity_type_id].get(
|
|
148
|
+
'entity_type_name'
|
|
197
149
|
)
|
|
198
150
|
|
|
151
|
+
labels: list[str] = list({'Entity', str(entity_type_name)})
|
|
152
|
+
|
|
199
153
|
new_node = EntityNode(
|
|
200
|
-
name=name,
|
|
154
|
+
name=extracted_entity.name,
|
|
201
155
|
group_id=episode.group_id,
|
|
202
156
|
labels=labels,
|
|
203
157
|
summary='',
|
|
204
158
|
created_at=utc_now(),
|
|
205
159
|
)
|
|
206
|
-
|
|
160
|
+
extracted_nodes.append(new_node)
|
|
207
161
|
logger.debug(f'Created new node: {new_node.name} (UUID: {new_node.uuid})')
|
|
208
162
|
|
|
209
|
-
|
|
163
|
+
await create_entity_node_embeddings(embedder, extracted_nodes)
|
|
164
|
+
|
|
165
|
+
logger.debug(f'Extracted nodes: {[(n.name, n.uuid) for n in extracted_nodes]}')
|
|
166
|
+
return extracted_nodes
|
|
210
167
|
|
|
211
168
|
|
|
212
169
|
async def dedupe_extracted_nodes(
|
|
@@ -260,36 +217,45 @@ async def dedupe_extracted_nodes(
|
|
|
260
217
|
|
|
261
218
|
|
|
262
219
|
async def resolve_extracted_nodes(
|
|
263
|
-
|
|
220
|
+
clients: GraphitiClients,
|
|
264
221
|
extracted_nodes: list[EntityNode],
|
|
265
|
-
existing_nodes_lists: list[list[EntityNode]],
|
|
266
222
|
episode: EpisodicNode | None = None,
|
|
267
223
|
previous_episodes: list[EpisodicNode] | None = None,
|
|
268
224
|
entity_types: dict[str, BaseModel] | None = None,
|
|
269
225
|
) -> tuple[list[EntityNode], dict[str, str]]:
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
226
|
+
llm_client = clients.llm_client
|
|
227
|
+
driver = clients.driver
|
|
228
|
+
|
|
229
|
+
# Find relevant nodes already in the graph
|
|
230
|
+
existing_nodes_lists: list[list[EntityNode]] = await get_relevant_nodes(
|
|
231
|
+
driver, extracted_nodes, SearchFilters()
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
resolved_nodes: list[EntityNode] = await semaphore_gather(
|
|
235
|
+
*[
|
|
236
|
+
resolve_extracted_node(
|
|
237
|
+
llm_client,
|
|
238
|
+
extracted_node,
|
|
239
|
+
existing_nodes,
|
|
240
|
+
episode,
|
|
241
|
+
previous_episodes,
|
|
242
|
+
entity_types.get(
|
|
243
|
+
next((item for item in extracted_node.labels if item != 'Entity'), '')
|
|
285
244
|
)
|
|
286
|
-
|
|
287
|
-
|
|
245
|
+
if entity_types is not None
|
|
246
|
+
else None,
|
|
247
|
+
)
|
|
248
|
+
for extracted_node, existing_nodes in zip(
|
|
249
|
+
extracted_nodes, existing_nodes_lists, strict=True
|
|
250
|
+
)
|
|
251
|
+
]
|
|
288
252
|
)
|
|
289
253
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
254
|
+
uuid_map: dict[str, str] = {}
|
|
255
|
+
for extracted_node, resolved_node in zip(extracted_nodes, resolved_nodes, strict=True):
|
|
256
|
+
uuid_map[extracted_node.uuid] = resolved_node.uuid
|
|
257
|
+
|
|
258
|
+
logger.debug(f'Resolved nodes: {[(n.name, n.uuid) for n in resolved_nodes]}')
|
|
293
259
|
|
|
294
260
|
return resolved_nodes, uuid_map
|
|
295
261
|
|
|
@@ -300,124 +266,151 @@ async def resolve_extracted_node(
|
|
|
300
266
|
existing_nodes: list[EntityNode],
|
|
301
267
|
episode: EpisodicNode | None = None,
|
|
302
268
|
previous_episodes: list[EpisodicNode] | None = None,
|
|
303
|
-
|
|
304
|
-
) ->
|
|
269
|
+
entity_type: BaseModel | None = None,
|
|
270
|
+
) -> EntityNode:
|
|
305
271
|
start = time()
|
|
272
|
+
if len(existing_nodes) == 0:
|
|
273
|
+
return extracted_node
|
|
306
274
|
|
|
307
275
|
# Prepare context for LLM
|
|
308
276
|
existing_nodes_context = [
|
|
309
|
-
{
|
|
310
|
-
|
|
277
|
+
{
|
|
278
|
+
**{
|
|
279
|
+
'id': i,
|
|
280
|
+
'name': node.name,
|
|
281
|
+
'entity_types': node.labels,
|
|
282
|
+
'summary': node.summary,
|
|
283
|
+
},
|
|
284
|
+
**node.attributes,
|
|
285
|
+
}
|
|
286
|
+
for i, node in enumerate(existing_nodes)
|
|
311
287
|
]
|
|
312
288
|
|
|
313
289
|
extracted_node_context = {
|
|
314
|
-
'uuid': extracted_node.uuid,
|
|
315
290
|
'name': extracted_node.name,
|
|
316
|
-
'
|
|
291
|
+
'entity_type': entity_type.__name__ if entity_type is not None else 'Entity', # type: ignore
|
|
292
|
+
'entity_type_description': entity_type.__doc__
|
|
293
|
+
if entity_type is not None
|
|
294
|
+
else 'Default Entity Type',
|
|
317
295
|
}
|
|
318
296
|
|
|
319
297
|
context = {
|
|
320
298
|
'existing_nodes': existing_nodes_context,
|
|
321
|
-
'
|
|
299
|
+
'extracted_node': extracted_node_context,
|
|
322
300
|
'episode_content': episode.content if episode is not None else '',
|
|
323
301
|
'previous_episodes': [ep.content for ep in previous_episodes]
|
|
324
302
|
if previous_episodes is not None
|
|
325
303
|
else [],
|
|
326
304
|
}
|
|
327
305
|
|
|
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
|
-
}
|
|
306
|
+
llm_response = await llm_client.generate_response(
|
|
307
|
+
prompt_library.dedupe_nodes.node(context), response_model=NodeDuplicate
|
|
308
|
+
)
|
|
336
309
|
|
|
337
|
-
|
|
310
|
+
duplicate_id: int = llm_response.get('duplicate_node_id', -1)
|
|
338
311
|
|
|
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
|
-
)
|
|
312
|
+
node = (
|
|
313
|
+
existing_nodes[duplicate_id] if 0 <= duplicate_id < len(existing_nodes) else extracted_node
|
|
314
|
+
)
|
|
347
315
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
'attribute_name': field_name,
|
|
353
|
-
'attribute_description': field_info.description or '',
|
|
354
|
-
}
|
|
355
|
-
)
|
|
316
|
+
end = time()
|
|
317
|
+
logger.debug(
|
|
318
|
+
f'Resolved node: {extracted_node.name} is {node.name}, in {(end - start) * 1000} ms'
|
|
319
|
+
)
|
|
356
320
|
|
|
357
|
-
|
|
321
|
+
return node
|
|
358
322
|
|
|
359
|
-
entity_attributes_model = pydantic.create_model( # type: ignore
|
|
360
|
-
'EntityAttributes',
|
|
361
|
-
__base__=entity_type_classes + (Summary,), # type: ignore
|
|
362
|
-
)
|
|
363
323
|
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
324
|
+
async def extract_attributes_from_nodes(
|
|
325
|
+
clients: GraphitiClients,
|
|
326
|
+
nodes: list[EntityNode],
|
|
327
|
+
episode: EpisodicNode | None = None,
|
|
328
|
+
previous_episodes: list[EpisodicNode] | None = None,
|
|
329
|
+
entity_types: dict[str, BaseModel] | None = None,
|
|
330
|
+
) -> list[EntityNode]:
|
|
331
|
+
llm_client = clients.llm_client
|
|
332
|
+
embedder = clients.embedder
|
|
333
|
+
|
|
334
|
+
updated_nodes: list[EntityNode] = await semaphore_gather(
|
|
335
|
+
*[
|
|
336
|
+
extract_attributes_from_node(
|
|
337
|
+
llm_client,
|
|
338
|
+
node,
|
|
339
|
+
episode,
|
|
340
|
+
previous_episodes,
|
|
341
|
+
entity_types.get(next((item for item in node.labels if item != 'Entity'), ''))
|
|
342
|
+
if entity_types is not None
|
|
343
|
+
else None,
|
|
344
|
+
)
|
|
345
|
+
for node in nodes
|
|
346
|
+
]
|
|
372
347
|
)
|
|
373
348
|
|
|
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
|
-
}
|
|
349
|
+
await create_entity_node_embeddings(embedder, updated_nodes)
|
|
379
350
|
|
|
380
|
-
|
|
381
|
-
del node_attributes['summary']
|
|
351
|
+
return updated_nodes
|
|
382
352
|
|
|
383
|
-
extracted_node.attributes.update(node_attributes)
|
|
384
353
|
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
354
|
+
async def extract_attributes_from_node(
|
|
355
|
+
llm_client: LLMClient,
|
|
356
|
+
node: EntityNode,
|
|
357
|
+
episode: EpisodicNode | None = None,
|
|
358
|
+
previous_episodes: list[EpisodicNode] | None = None,
|
|
359
|
+
entity_type: BaseModel | None = None,
|
|
360
|
+
) -> EntityNode:
|
|
361
|
+
node_context: dict[str, Any] = {
|
|
362
|
+
'name': node.name,
|
|
363
|
+
'summary': node.summary,
|
|
364
|
+
'entity_types': node.labels,
|
|
365
|
+
'attributes': node.attributes,
|
|
366
|
+
}
|
|
388
367
|
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
368
|
+
attributes_definitions: dict[str, Any] = {
|
|
369
|
+
'summary': (
|
|
370
|
+
str,
|
|
371
|
+
Field(
|
|
372
|
+
description='Summary containing the important information about the entity. Under 200 words',
|
|
373
|
+
),
|
|
374
|
+
),
|
|
375
|
+
'name': (
|
|
376
|
+
str,
|
|
377
|
+
Field(description='Name of the ENTITY'),
|
|
378
|
+
),
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
if entity_type is not None:
|
|
382
|
+
for field_name, field_info in entity_type.model_fields.items():
|
|
383
|
+
attributes_definitions[field_name] = (
|
|
384
|
+
field_info.annotation,
|
|
385
|
+
Field(description=field_info.description),
|
|
400
386
|
)
|
|
401
|
-
node = existing_node
|
|
402
|
-
node.name = name
|
|
403
|
-
node.summary = summary_response.get('summary', '')
|
|
404
387
|
|
|
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))
|
|
388
|
+
entity_attributes_model = pydantic.create_model('EntityAttributes', **attributes_definitions)
|
|
412
389
|
|
|
413
|
-
|
|
390
|
+
summary_context: dict[str, Any] = {
|
|
391
|
+
'node': node_context,
|
|
392
|
+
'episode_content': episode.content if episode is not None else '',
|
|
393
|
+
'previous_episodes': [ep.content for ep in previous_episodes]
|
|
394
|
+
if previous_episodes is not None
|
|
395
|
+
else [],
|
|
396
|
+
}
|
|
414
397
|
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
398
|
+
llm_response = await llm_client.generate_response(
|
|
399
|
+
prompt_library.extract_nodes.extract_attributes(summary_context),
|
|
400
|
+
response_model=entity_attributes_model,
|
|
418
401
|
)
|
|
419
402
|
|
|
420
|
-
|
|
403
|
+
node.summary = llm_response.get('summary', node.summary)
|
|
404
|
+
node.name = llm_response.get('name', node.name)
|
|
405
|
+
node_attributes = {key: value for key, value in llm_response.items()}
|
|
406
|
+
|
|
407
|
+
with suppress(KeyError):
|
|
408
|
+
del node_attributes['summary']
|
|
409
|
+
del node_attributes['name']
|
|
410
|
+
|
|
411
|
+
node.attributes.update(node_attributes)
|
|
412
|
+
|
|
413
|
+
return node
|
|
421
414
|
|
|
422
415
|
|
|
423
416
|
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.0
|
|
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.
|