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.

@@ -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 EntityClassification, ExtractedNodes, MissedEntities
31
- from graphiti_core.prompts.summarize_nodes import Summary
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
- llm_client: LLMClient,
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
- extracted_node_names: list[str] = []
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
- while entities_missed and reflexion_iterations < MAX_REFLEXION_ITERATIONS:
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
- extracted_node_names = await extract_message_nodes(
132
- llm_client, episode, previous_episodes, custom_prompt
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
- extracted_node_names = await extract_text_nodes(
136
- llm_client, episode, previous_episodes, custom_prompt
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
- extracted_node_names = await extract_json_nodes(llm_client, episode, custom_prompt)
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, episode, previous_episodes, extracted_node_names
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 = 'The following entities were missed in a previous extraction: '
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: {extracted_node_names} in {(end - start) * 1000} ms')
144
+ logger.debug(f'Extracted new nodes: {extracted_entities} in {(end - start) * 1000} ms')
186
145
  # Convert the extracted data into EntityNode objects
187
- new_nodes = []
188
- for name in extracted_node_names:
189
- entity_type = node_classifications.get(name)
190
- if entity_types is not None and entity_type not in entity_types:
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
- new_nodes.append(new_node)
161
+ extracted_nodes.append(new_node)
207
162
  logger.debug(f'Created new node: {new_node.name} (UUID: {new_node.uuid})')
208
163
 
209
- return new_nodes
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
- llm_client: LLMClient,
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
- uuid_map: dict[str, str] = {}
271
- resolved_nodes: list[EntityNode] = []
272
- results: list[tuple[EntityNode, dict[str, str]]] = list(
273
- await semaphore_gather(
274
- *[
275
- resolve_extracted_node(
276
- llm_client,
277
- extracted_node,
278
- existing_nodes,
279
- episode,
280
- previous_episodes,
281
- entity_types,
282
- )
283
- for extracted_node, existing_nodes in zip(
284
- extracted_nodes, existing_nodes_lists, strict=False
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
- for result in results:
291
- uuid_map.update(result[1])
292
- resolved_nodes.append(result[0])
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
- entity_types: dict[str, BaseModel] | None = None,
304
- ) -> tuple[EntityNode, dict[str, str]]:
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
- {**{'uuid': node.uuid, 'name': node.name, 'summary': node.summary}, **node.attributes}
310
- for node in existing_nodes
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
- 'summary': extracted_node.summary,
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
- 'extracted_nodes': extracted_node_context,
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
- summary_context: dict[str, Any] = {
329
- 'node_name': extracted_node.name,
330
- 'node_summary': extracted_node.summary,
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
- attributes: list[dict[str, str]] = []
311
+ duplicate_id: int = llm_response.get('duplicate_node_id', -1)
338
312
 
339
- entity_type_classes: tuple[BaseModel, ...] = tuple()
340
- if entity_types is not None: # type: ignore
341
- entity_type_classes = entity_type_classes + tuple(
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
- for entity_type in entity_type_classes:
349
- for field_name, field_info in entity_type.model_fields.items():
350
- attributes.append(
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
- summary_context['attributes'] = attributes
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
- llm_response, node_attributes_response = await semaphore_gather(
365
- llm_client.generate_response(
366
- prompt_library.dedupe_nodes.node(context), response_model=NodeDuplicate
367
- ),
368
- llm_client.generate_response(
369
- prompt_library.summarize_nodes.summarize_context(summary_context),
370
- response_model=entity_attributes_model,
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
- extracted_node.summary = node_attributes_response.get('summary', '')
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
- with suppress(KeyError):
381
- del node_attributes['summary']
352
+ return updated_nodes
382
353
 
383
- extracted_node.attributes.update(node_attributes)
384
354
 
385
- is_duplicate: bool = llm_response.get('is_duplicate', False)
386
- uuid: str | None = llm_response.get('uuid', None)
387
- name = llm_response.get('name', '')
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
- node = extracted_node
390
- uuid_map: dict[str, str] = {}
391
- if is_duplicate:
392
- for existing_node in existing_nodes:
393
- if existing_node.uuid != uuid:
394
- continue
395
- summary_response = await llm_client.generate_response(
396
- prompt_library.summarize_nodes.summarize_pair(
397
- {'node_summaries': [extracted_node.summary, existing_node.summary]}
398
- ),
399
- response_model=Summary,
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
- new_attributes = extracted_node.attributes
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
- uuid_map[extracted_node.uuid] = existing_node.uuid
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
- end = time()
416
- logger.debug(
417
- f'Resolved node: {extracted_node.name} is {node.name}, in {(end - start) * 1000} ms'
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
- return node, uuid_map
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 = {'uuid': new_edge.uuid, 'name': new_edge.name, 'fact': new_edge.fact}
76
+ new_edge_context = {'fact': new_edge.fact}
78
77
  existing_edge_context = [
79
- {'uuid': existing_edge.uuid, 'name': existing_edge.name, 'fact': existing_edge.fact}
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
- contradicted_edge_data = llm_response.get('invalidated_edges', [])
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.10.5
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
  [![Lint](https://github.com/getzep/Graphiti/actions/workflows/lint.yml/badge.svg?style=flat)](https://github.com/getzep/Graphiti/actions/workflows/lint.yml)
47
46
  [![Unit Tests](https://github.com/getzep/Graphiti/actions/workflows/unit_tests.yml/badge.svg)](https://github.com/getzep/Graphiti/actions/workflows/unit_tests.yml)
48
47
  [![MyPy Check](https://github.com/getzep/Graphiti/actions/workflows/typecheck.yml/badge.svg)](https://github.com/getzep/Graphiti/actions/workflows/typecheck.yml)
49
48
 
49
+ ![GitHub Repo stars](https://img.shields.io/github/stars/getzep/graphiti)
50
50
  [![Discord](https://dcbadge.vercel.app/api/server/W8Kw6bsgXQ?style=flat)](https://discord.com/invite/W8Kw6bsgXQ)
51
51
  [![arXiv](https://img.shields.io/badge/arXiv-2501.13956-b31b1b.svg?style=flat)](https://arxiv.org/abs/2501.13956)
52
52
  [![Release](https://img.shields.io/github/v/release/getzep/graphiti?style=flat&label=Release&color=limegreen)](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.