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.

@@ -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,108 @@ 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
+ }
106
+
107
+ while entities_missed and reflexion_iterations <= MAX_REFLEXION_ITERATIONS:
130
108
  if episode.source == EpisodeType.message:
131
- extracted_node_names = await extract_message_nodes(
132
- llm_client, episode, previous_episodes, custom_prompt
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
- extracted_node_names = await extract_text_nodes(
136
- llm_client, episode, previous_episodes, custom_prompt
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
- extracted_node_names = await extract_json_nodes(llm_client, episode, custom_prompt)
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, episode, previous_episodes, extracted_node_names
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 = 'The following entities were missed in a previous extraction: '
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: {extracted_node_names} in {(end - start) * 1000} ms')
143
+ logger.debug(f'Extracted new nodes: {extracted_entities} in {(end - start) * 1000} ms')
186
144
  # 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]
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
- new_nodes.append(new_node)
160
+ extracted_nodes.append(new_node)
207
161
  logger.debug(f'Created new node: {new_node.name} (UUID: {new_node.uuid})')
208
162
 
209
- return new_nodes
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
- llm_client: LLMClient,
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
- 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
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
- for result in results:
291
- uuid_map.update(result[1])
292
- resolved_nodes.append(result[0])
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
- entity_types: dict[str, BaseModel] | None = None,
304
- ) -> tuple[EntityNode, dict[str, str]]:
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
- {**{'uuid': node.uuid, 'name': node.name, 'summary': node.summary}, **node.attributes}
310
- for node in existing_nodes
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
- 'summary': extracted_node.summary,
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
- 'extracted_nodes': extracted_node_context,
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
- 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
- }
306
+ llm_response = await llm_client.generate_response(
307
+ prompt_library.dedupe_nodes.node(context), response_model=NodeDuplicate
308
+ )
336
309
 
337
- attributes: list[dict[str, str]] = []
310
+ duplicate_id: int = llm_response.get('duplicate_node_id', -1)
338
311
 
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
- )
312
+ node = (
313
+ existing_nodes[duplicate_id] if 0 <= duplicate_id < len(existing_nodes) else extracted_node
314
+ )
347
315
 
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
- )
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
- summary_context['attributes'] = attributes
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
- 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
- ),
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
- 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
- }
349
+ await create_entity_node_embeddings(embedder, updated_nodes)
379
350
 
380
- with suppress(KeyError):
381
- del node_attributes['summary']
351
+ return updated_nodes
382
352
 
383
- extracted_node.attributes.update(node_attributes)
384
353
 
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', '')
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
- 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,
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
- 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))
388
+ entity_attributes_model = pydantic.create_model('EntityAttributes', **attributes_definitions)
412
389
 
413
- uuid_map[extracted_node.uuid] = existing_node.uuid
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
- end = time()
416
- logger.debug(
417
- f'Resolved node: {extracted_node.name} is {node.name}, in {(end - start) * 1000} ms'
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
- return node, uuid_map
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 = {'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.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
  [![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.