graphiti-core 0.11.6rc9__py3-none-any.whl → 0.12.0rc2__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 CHANGED
@@ -49,7 +49,9 @@ ENTITY_EDGE_RETURN: LiteralString = """
49
49
  e.episodes AS episodes,
50
50
  e.expired_at AS expired_at,
51
51
  e.valid_at AS valid_at,
52
- e.invalid_at AS invalid_at"""
52
+ e.invalid_at AS invalid_at,
53
+ properties(e) AS attributes
54
+ """
53
55
 
54
56
 
55
57
  class Edge(BaseModel, ABC):
@@ -209,6 +211,9 @@ class EntityEdge(Edge):
209
211
  invalid_at: datetime | None = Field(
210
212
  default=None, description='datetime of when the fact stopped being true'
211
213
  )
214
+ attributes: dict[str, Any] = Field(
215
+ default={}, description='Additional attributes of the edge. Dependent on edge name'
216
+ )
212
217
 
213
218
  async def generate_embedding(self, embedder: EmbedderClient):
214
219
  start = time()
@@ -236,20 +241,26 @@ class EntityEdge(Edge):
236
241
  self.fact_embedding = records[0]['fact_embedding']
237
242
 
238
243
  async def save(self, driver: AsyncDriver):
244
+ edge_data: dict[str, Any] = {
245
+ 'source_uuid': self.source_node_uuid,
246
+ 'target_uuid': self.target_node_uuid,
247
+ 'uuid': self.uuid,
248
+ 'name': self.name,
249
+ 'group_id': self.group_id,
250
+ 'fact': self.fact,
251
+ 'fact_embedding': self.fact_embedding,
252
+ 'episodes': self.episodes,
253
+ 'created_at': self.created_at,
254
+ 'expired_at': self.expired_at,
255
+ 'valid_at': self.valid_at,
256
+ 'invalid_at': self.invalid_at,
257
+ }
258
+
259
+ edge_data.update(self.attributes or {})
260
+
239
261
  result = await driver.execute_query(
240
262
  ENTITY_EDGE_SAVE,
241
- source_uuid=self.source_node_uuid,
242
- target_uuid=self.target_node_uuid,
243
- uuid=self.uuid,
244
- name=self.name,
245
- group_id=self.group_id,
246
- fact=self.fact,
247
- fact_embedding=self.fact_embedding,
248
- episodes=self.episodes,
249
- created_at=self.created_at,
250
- expired_at=self.expired_at,
251
- valid_at=self.valid_at,
252
- invalid_at=self.invalid_at,
263
+ edge_data=edge_data,
253
264
  database_=DEFAULT_DATABASE,
254
265
  )
255
266
 
@@ -334,8 +345,8 @@ class EntityEdge(Edge):
334
345
  async def get_by_node_uuid(cls, driver: AsyncDriver, node_uuid: str):
335
346
  query: LiteralString = (
336
347
  """
337
- MATCH (n:Entity {uuid: $node_uuid})-[e:RELATES_TO]-(m:Entity)
338
- """
348
+ MATCH (n:Entity {uuid: $node_uuid})-[e:RELATES_TO]-(m:Entity)
349
+ """
339
350
  + ENTITY_EDGE_RETURN
340
351
  )
341
352
  records, _, _ = await driver.execute_query(
@@ -457,7 +468,7 @@ def get_episodic_edge_from_record(record: Any) -> EpisodicEdge:
457
468
 
458
469
 
459
470
  def get_entity_edge_from_record(record: Any) -> EntityEdge:
460
- return EntityEdge(
471
+ edge = EntityEdge(
461
472
  uuid=record['uuid'],
462
473
  source_node_uuid=record['source_node_uuid'],
463
474
  target_node_uuid=record['target_node_uuid'],
@@ -469,8 +480,23 @@ def get_entity_edge_from_record(record: Any) -> EntityEdge:
469
480
  expired_at=parse_db_date(record['expired_at']),
470
481
  valid_at=parse_db_date(record['valid_at']),
471
482
  invalid_at=parse_db_date(record['invalid_at']),
483
+ attributes=record['attributes'],
472
484
  )
473
485
 
486
+ edge.attributes.pop('uuid', None)
487
+ edge.attributes.pop('source_node_uuid', None)
488
+ edge.attributes.pop('target_node_uuid', None)
489
+ edge.attributes.pop('fact', None)
490
+ edge.attributes.pop('name', None)
491
+ edge.attributes.pop('group_id', None)
492
+ edge.attributes.pop('episodes', None)
493
+ edge.attributes.pop('created_at', None)
494
+ edge.attributes.pop('expired_at', None)
495
+ edge.attributes.pop('valid_at', None)
496
+ edge.attributes.pop('invalid_at', None)
497
+
498
+ return edge
499
+
474
500
 
475
501
  def get_community_edge_from_record(record: Any):
476
502
  return CommunityEdge(
@@ -61,18 +61,29 @@ class GeminiEmbedder(EmbedderClient):
61
61
  # Generate embeddings
62
62
  result = await self.client.aio.models.embed_content(
63
63
  model=self.config.embedding_model or DEFAULT_EMBEDDING_MODEL,
64
- contents=[input_data],
64
+ contents=[input_data], # type: ignore[arg-type] # mypy fails on broad union type
65
65
  config=types.EmbedContentConfig(output_dimensionality=self.config.embedding_dim),
66
66
  )
67
67
 
68
+ if not result.embeddings or len(result.embeddings) == 0 or not result.embeddings[0].values:
69
+ raise ValueError('No embeddings returned from Gemini API in create()')
70
+
68
71
  return result.embeddings[0].values
69
72
 
70
73
  async def create_batch(self, input_data_list: list[str]) -> list[list[float]]:
71
74
  # Generate embeddings
72
75
  result = await self.client.aio.models.embed_content(
73
76
  model=self.config.embedding_model or DEFAULT_EMBEDDING_MODEL,
74
- contents=input_data_list,
77
+ contents=input_data_list, # type: ignore[arg-type] # mypy fails on broad union type
75
78
  config=types.EmbedContentConfig(output_dimensionality=self.config.embedding_dim),
76
79
  )
77
80
 
78
- return [embedding.values for embedding in result.embeddings]
81
+ if not result.embeddings or len(result.embeddings) == 0:
82
+ raise Exception('No embeddings returned')
83
+
84
+ embeddings = []
85
+ for embedding in result.embeddings:
86
+ if not embedding.values:
87
+ raise ValueError('Empty embedding values returned')
88
+ embeddings.append(embedding.values)
89
+ return embeddings
graphiti_core/graphiti.py CHANGED
@@ -41,6 +41,7 @@ from graphiti_core.search.search_config_recipes import (
41
41
  from graphiti_core.search.search_filters import SearchFilters
42
42
  from graphiti_core.search.search_utils import (
43
43
  RELEVANT_SCHEMA_LIMIT,
44
+ get_edge_invalidation_candidates,
44
45
  get_mentioned_nodes,
45
46
  get_relevant_edges,
46
47
  )
@@ -62,9 +63,8 @@ from graphiti_core.utils.maintenance.community_operations import (
62
63
  )
63
64
  from graphiti_core.utils.maintenance.edge_operations import (
64
65
  build_episodic_edges,
65
- dedupe_extracted_edge,
66
66
  extract_edges,
67
- resolve_edge_contradictions,
67
+ resolve_extracted_edge,
68
68
  resolve_extracted_edges,
69
69
  )
70
70
  from graphiti_core.utils.maintenance.graph_data_operations import (
@@ -77,7 +77,6 @@ from graphiti_core.utils.maintenance.node_operations import (
77
77
  extract_nodes,
78
78
  resolve_extracted_nodes,
79
79
  )
80
- from graphiti_core.utils.maintenance.temporal_operations import get_edge_contradictions
81
80
  from graphiti_core.utils.ontology_utils.entity_types_utils import validate_entity_types
82
81
 
83
82
  logger = logging.getLogger(__name__)
@@ -274,6 +273,8 @@ class Graphiti:
274
273
  update_communities: bool = False,
275
274
  entity_types: dict[str, BaseModel] | None = None,
276
275
  previous_episode_uuids: list[str] | None = None,
276
+ edge_types: dict[str, BaseModel] | None = None,
277
+ edge_type_map: dict[tuple[str, str], list[str]] | None = None,
277
278
  ) -> AddEpisodeResults:
278
279
  """
279
280
  Process an episode and update the graph.
@@ -356,6 +357,13 @@ class Graphiti:
356
357
  )
357
358
  )
358
359
 
360
+ # Create default edge type map
361
+ edge_type_map_default = (
362
+ {('Entity', 'Entity'): list(edge_types.keys())}
363
+ if edge_types is not None
364
+ else {('Entity', 'Entity'): []}
365
+ )
366
+
359
367
  # Extract entities as nodes
360
368
 
361
369
  extracted_nodes = await extract_nodes(
@@ -371,7 +379,9 @@ class Graphiti:
371
379
  previous_episodes,
372
380
  entity_types,
373
381
  ),
374
- extract_edges(self.clients, episode, extracted_nodes, previous_episodes, group_id),
382
+ extract_edges(
383
+ self.clients, episode, extracted_nodes, previous_episodes, group_id, edge_types
384
+ ),
375
385
  )
376
386
 
377
387
  edges = resolve_edge_pointers(extracted_edges, uuid_map)
@@ -381,6 +391,9 @@ class Graphiti:
381
391
  self.clients,
382
392
  edges,
383
393
  episode,
394
+ nodes,
395
+ edge_types or {},
396
+ edge_type_map or edge_type_map_default,
384
397
  ),
385
398
  extract_attributes_from_nodes(
386
399
  self.clients, nodes, episode, previous_episodes, entity_types
@@ -681,17 +694,27 @@ class Graphiti:
681
694
 
682
695
  updated_edge = resolve_edge_pointers([edge], uuid_map)[0]
683
696
 
684
- related_edges = await get_relevant_edges(self.driver, [updated_edge], SearchFilters(), 0.8)
697
+ related_edges = (await get_relevant_edges(self.driver, [updated_edge], SearchFilters()))[0]
698
+ existing_edges = (
699
+ await get_edge_invalidation_candidates(self.driver, [updated_edge], SearchFilters())
700
+ )[0]
685
701
 
686
- resolved_edge = await dedupe_extracted_edge(
702
+ resolved_edge, invalidated_edges = await resolve_extracted_edge(
687
703
  self.llm_client,
688
704
  updated_edge,
689
- related_edges[0],
705
+ related_edges,
706
+ existing_edges,
707
+ EpisodicNode(
708
+ name='',
709
+ source=EpisodeType.text,
710
+ source_description='',
711
+ content='',
712
+ valid_at=edge.valid_at or utc_now(),
713
+ entity_edges=[],
714
+ group_id=edge.group_id,
715
+ ),
690
716
  )
691
717
 
692
- contradicting_edges = await get_edge_contradictions(self.llm_client, edge, related_edges[0])
693
- invalidated_edges = resolve_edge_contradictions(resolved_edge, contradicting_edges)
694
-
695
718
  await add_nodes_and_edges_bulk(
696
719
  self.driver, [], [], resolved_nodes, [resolved_edge] + invalidated_edges, self.embedder
697
720
  )
graphiti_core/helpers.py CHANGED
@@ -18,7 +18,6 @@ import asyncio
18
18
  import os
19
19
  from collections.abc import Coroutine
20
20
  from datetime import datetime
21
- from typing import Any
22
21
 
23
22
  import numpy as np
24
23
  from dotenv import load_dotenv
@@ -139,13 +139,16 @@ class GeminiClient(LLMClient):
139
139
  # Generate content using the simple string approach
140
140
  response = await self.client.aio.models.generate_content(
141
141
  model=self.model or DEFAULT_MODEL,
142
- contents=gemini_messages,
142
+ contents=gemini_messages, # type: ignore[arg-type] # mypy fails on broad union type
143
143
  config=generation_config,
144
144
  )
145
145
 
146
146
  # If this was a structured output request, parse the response into the Pydantic model
147
147
  if response_model is not None:
148
148
  try:
149
+ if not response.text:
150
+ raise ValueError('No response text')
151
+
149
152
  validated_model = response_model.model_validate(json.loads(response.text))
150
153
 
151
154
  # Return as a dictionary for API consistency
@@ -34,8 +34,7 @@ ENTITY_EDGE_SAVE = """
34
34
  MATCH (source:Entity {uuid: $source_uuid})
35
35
  MATCH (target:Entity {uuid: $target_uuid})
36
36
  MERGE (source)-[r:RELATES_TO {uuid: $uuid}]->(target)
37
- SET r = {uuid: $uuid, name: $name, group_id: $group_id, fact: $fact, episodes: $episodes,
38
- created_at: $created_at, expired_at: $expired_at, valid_at: $valid_at, invalid_at: $invalid_at}
37
+ SET r = $edge_data
39
38
  WITH r CALL db.create.setRelationshipVectorProperty(r, "fact_embedding", $fact_embedding)
40
39
  RETURN r.uuid AS uuid"""
41
40
 
@@ -44,8 +43,7 @@ ENTITY_EDGE_SAVE_BULK = """
44
43
  MATCH (source:Entity {uuid: edge.source_node_uuid})
45
44
  MATCH (target:Entity {uuid: edge.target_node_uuid})
46
45
  MERGE (source)-[r:RELATES_TO {uuid: edge.uuid}]->(target)
47
- SET r = {uuid: edge.uuid, name: edge.name, group_id: edge.group_id, fact: edge.fact, episodes: edge.episodes,
48
- created_at: edge.created_at, expired_at: edge.expired_at, valid_at: edge.valid_at, invalid_at: edge.invalid_at}
46
+ SET r = edge
49
47
  WITH r, edge CALL db.create.setRelationshipVectorProperty(r, "fact_embedding", edge.fact_embedding)
50
48
  RETURN edge.uuid AS uuid
51
49
  """
@@ -27,6 +27,11 @@ class EdgeDuplicate(BaseModel):
27
27
  ...,
28
28
  description='id of the duplicate fact. If no duplicate facts are found, default to -1.',
29
29
  )
30
+ contradicted_facts: list[int] = Field(
31
+ ...,
32
+ description='List of ids of facts that should be invalidated. If no facts should be invalidated, the list should be empty.',
33
+ )
34
+ fact_type: str = Field(..., description='One of the provided fact types or DEFAULT')
30
35
 
31
36
 
32
37
  class UniqueFact(BaseModel):
@@ -41,11 +46,13 @@ class UniqueFacts(BaseModel):
41
46
  class Prompt(Protocol):
42
47
  edge: PromptVersion
43
48
  edge_list: PromptVersion
49
+ resolve_edge: PromptVersion
44
50
 
45
51
 
46
52
  class Versions(TypedDict):
47
53
  edge: PromptFunction
48
54
  edge_list: PromptFunction
55
+ resolve_edge: PromptFunction
49
56
 
50
57
 
51
58
  def edge(context: dict[str, Any]) -> list[Message]:
@@ -106,4 +113,48 @@ def edge_list(context: dict[str, Any]) -> list[Message]:
106
113
  ]
107
114
 
108
115
 
109
- versions: Versions = {'edge': edge, 'edge_list': edge_list}
116
+ def resolve_edge(context: dict[str, Any]) -> list[Message]:
117
+ return [
118
+ Message(
119
+ role='system',
120
+ content='You are a helpful assistant that de-duplicates facts from fact lists and determines which existing '
121
+ 'facts are contradicted by the new fact.',
122
+ ),
123
+ Message(
124
+ role='user',
125
+ content=f"""
126
+ <NEW FACT>
127
+ {context['new_edge']}
128
+ </NEW FACT>
129
+
130
+ <EXISTING FACTS>
131
+ {context['existing_edges']}
132
+ </EXISTING FACTS>
133
+ <FACT INVALIDATION CANDIDATES>
134
+ {context['edge_invalidation_candidates']}
135
+ </FACT INVALIDATION CANDIDATES>
136
+
137
+ <FACT TYPES>
138
+ {context['edge_types']}
139
+ </FACT TYPES>
140
+
141
+
142
+ Task:
143
+ If the NEW FACT represents the same factual information as any fact in EXISTING FACTS, return the idx of the duplicate fact.
144
+ If the NEW FACT is not a duplicate of any of the EXISTING FACTS, return -1.
145
+
146
+ Given the predefined FACT TYPES, determine if the NEW FACT should be classified as one of these types.
147
+ Return the fact type as fact_type or DEFAULT if NEW FACT is not one of the FACT TYPES.
148
+
149
+ Based on the provided FACT INVALIDATION CANDIDATES and NEW FACT, determine which existing facts the new fact contradicts.
150
+ Return a list containing all idx's of the facts that are contradicted by the NEW FACT.
151
+ If there are no contradicted facts, return an empty list.
152
+
153
+ Guidelines:
154
+ 1. The facts do not need to be completely identical to be duplicates, they just need to express the same information.
155
+ """,
156
+ ),
157
+ ]
158
+
159
+
160
+ versions: Versions = {'edge': edge, 'edge_list': edge_list, 'resolve_edge': resolve_edge}
@@ -23,21 +23,31 @@ from .models import Message, PromptFunction, PromptVersion
23
23
 
24
24
 
25
25
  class NodeDuplicate(BaseModel):
26
- duplicate_node_id: int = Field(
26
+ id: int = Field(..., description='integer id of the entity')
27
+ duplicate_idx: int = Field(
27
28
  ...,
28
- description='id of the duplicate node. If no duplicate nodes are found, default to -1.',
29
+ description='idx of the duplicate node. If no duplicate nodes are found, default to -1.',
29
30
  )
30
- name: str = Field(..., description='Name of the entity.')
31
+ name: str = Field(
32
+ ...,
33
+ description='Name of the entity. Should be the most complete and descriptive name possible.',
34
+ )
35
+
36
+
37
+ class NodeResolutions(BaseModel):
38
+ entity_resolutions: list[NodeDuplicate] = Field(..., description='List of resolved nodes')
31
39
 
32
40
 
33
41
  class Prompt(Protocol):
34
42
  node: PromptVersion
35
43
  node_list: PromptVersion
44
+ nodes: PromptVersion
36
45
 
37
46
 
38
47
  class Versions(TypedDict):
39
48
  node: PromptFunction
40
49
  node_list: PromptFunction
50
+ nodes: PromptFunction
41
51
 
42
52
 
43
53
  def node(context: dict[str, Any]) -> list[Message]:
@@ -89,6 +99,67 @@ def node(context: dict[str, Any]) -> list[Message]:
89
99
  ]
90
100
 
91
101
 
102
+ def nodes(context: dict[str, Any]) -> list[Message]:
103
+ return [
104
+ Message(
105
+ role='system',
106
+ content='You are a helpful assistant that determines whether or not ENTITIES extracted from a conversation are duplicates'
107
+ 'of existing entities.',
108
+ ),
109
+ Message(
110
+ role='user',
111
+ content=f"""
112
+ <PREVIOUS MESSAGES>
113
+ {json.dumps([ep for ep in context['previous_episodes']], indent=2)}
114
+ </PREVIOUS MESSAGES>
115
+ <CURRENT MESSAGE>
116
+ {context['episode_content']}
117
+ </CURRENT MESSAGE>
118
+
119
+
120
+ Each of the following ENTITIES were extracted from the CURRENT MESSAGE.
121
+ Each entity in ENTITIES is represented as a JSON object with the following structure:
122
+ {{
123
+ id: integer id of the entity,
124
+ name: "name of the entity",
125
+ entity_type: "ontological classification of the entity",
126
+ entity_type_description: "Description of what the entity type represents",
127
+ duplication_candidates: [
128
+ {{
129
+ idx: integer index of the candidate entity,
130
+ name: "name of the candidate entity",
131
+ entity_type: "ontological classification of the candidate entity",
132
+ ...<additional attributes>
133
+ }}
134
+ ]
135
+ }}
136
+
137
+ <ENTITIES>
138
+ {json.dumps(context['extracted_nodes'], indent=2)}
139
+ </ENTITIES>
140
+
141
+ For each of the above ENTITIES, determine if the entity is a duplicate of any of its duplication candidates.
142
+
143
+ Entities should only be considered duplicates if they refer to the *same real-world object or concept*.
144
+
145
+ Do NOT mark entities as duplicates if:
146
+ - They are related but distinct.
147
+ - They have similar names or purposes but refer to separate instances or concepts.
148
+
149
+ Task:
150
+ Your response will be a list called entity_resolutions which contains one entry for each entity.
151
+
152
+ For each entity, return the id of the entity as id, the name of the entity as name, and the duplicate_idx
153
+ as an integer.
154
+
155
+ - If an entity is a duplicate of one of its duplication_candidates, return the idx of the candidate it is a
156
+ duplicate of.
157
+ - If an entity is not a duplicate of one of its duplication candidates, return the -1 as the duplication_idx
158
+ """,
159
+ ),
160
+ ]
161
+
162
+
92
163
  def node_list(context: dict[str, Any]) -> list[Message]:
93
164
  return [
94
165
  Message(
@@ -126,4 +197,4 @@ def node_list(context: dict[str, Any]) -> list[Message]:
126
197
  ]
127
198
 
128
199
 
129
- versions: Versions = {'node': node, 'node_list': node_list}
200
+ versions: Versions = {'node': node, 'node_list': node_list, 'nodes': nodes}
@@ -48,11 +48,13 @@ class MissingFacts(BaseModel):
48
48
  class Prompt(Protocol):
49
49
  edge: PromptVersion
50
50
  reflexion: PromptVersion
51
+ extract_attributes: PromptVersion
51
52
 
52
53
 
53
54
  class Versions(TypedDict):
54
55
  edge: PromptFunction
55
56
  reflexion: PromptFunction
57
+ extract_attributes: PromptFunction
56
58
 
57
59
 
58
60
  def edge(context: dict[str, Any]) -> list[Message]:
@@ -82,12 +84,18 @@ def edge(context: dict[str, Any]) -> list[Message]:
82
84
  {context['reference_time']} # ISO 8601 (UTC); used to resolve relative time mentions
83
85
  </REFERENCE_TIME>
84
86
 
87
+ <FACT TYPES>
88
+ {context['edge_types']}
89
+ </FACT TYPES>
90
+
85
91
  # TASK
86
92
  Extract all factual relationships between the given ENTITIES based on the CURRENT MESSAGE.
87
93
  Only extract facts that:
88
94
  - involve two DISTINCT ENTITIES from the ENTITIES list,
89
95
  - are clearly stated or unambiguously implied in the CURRENT MESSAGE,
90
- - and can be represented as edges in a knowledge graph.
96
+ and can be represented as edges in a knowledge graph.
97
+ - The FACT TYPES provide a list of the most important types of facts, make sure to extract any facts that
98
+ could be classified into one of the provided fact types
91
99
 
92
100
  You may use information from the PREVIOUS MESSAGES only to disambiguate references or support continuity.
93
101
 
@@ -145,4 +153,40 @@ determine if any facts haven't been extracted.
145
153
  ]
146
154
 
147
155
 
148
- versions: Versions = {'edge': edge, 'reflexion': reflexion}
156
+ def extract_attributes(context: dict[str, Any]) -> list[Message]:
157
+ return [
158
+ Message(
159
+ role='system',
160
+ content='You are a helpful assistant that extracts fact properties from the provided text.',
161
+ ),
162
+ Message(
163
+ role='user',
164
+ content=f"""
165
+
166
+ <MESSAGE>
167
+ {json.dumps(context['episode_content'], indent=2)}
168
+ </MESSAGE>
169
+ <REFERENCE TIME>
170
+ {context['reference_time']}
171
+ </REFERENCE TIME>
172
+
173
+ Given the above MESSAGE, its REFERENCE TIME, and the following FACT, update any of its attributes based on the information provided
174
+ in MESSAGE. Use the provided attribute descriptions to better understand how each attribute should be determined.
175
+
176
+ Guidelines:
177
+ 1. Do not hallucinate entity property values if they cannot be found in the current context.
178
+ 2. Only use the provided MESSAGES and FACT to set attribute values.
179
+
180
+ <FACT>
181
+ {context['fact']}
182
+ </FACT>
183
+ """,
184
+ ),
185
+ ]
186
+
187
+
188
+ versions: Versions = {
189
+ 'edge': edge,
190
+ 'reflexion': reflexion,
191
+ 'extract_attributes': extract_attributes,
192
+ }
@@ -24,7 +24,7 @@ from .models import Message, PromptFunction, PromptVersion
24
24
  class InvalidatedEdges(BaseModel):
25
25
  contradicted_facts: list[int] = Field(
26
26
  ...,
27
- description='List of ids of facts that be should invalidated. If no facts should be invalidated, the list should be empty.',
27
+ description='List of ids of facts that should be invalidated. If no facts should be invalidated, the list should be empty.',
28
28
  )
29
29
 
30
30
 
@@ -21,6 +21,8 @@ from typing import Any
21
21
  from pydantic import BaseModel, Field
22
22
  from typing_extensions import LiteralString
23
23
 
24
+ from graphiti_core.helpers import lucene_sanitize
25
+
24
26
 
25
27
  class ComparisonOperator(Enum):
26
28
  equals = '='
@@ -42,6 +44,9 @@ class SearchFilters(BaseModel):
42
44
  node_labels: list[str] | None = Field(
43
45
  default=None, description='List of node labels to filter on'
44
46
  )
47
+ edge_types: list[str] | None = Field(
48
+ default=None, description='List of edge types to filter on'
49
+ )
45
50
  valid_at: list[list[DateFilter]] | None = Field(default=None)
46
51
  invalid_at: list[list[DateFilter]] | None = Field(default=None)
47
52
  created_at: list[list[DateFilter]] | None = Field(default=None)
@@ -55,7 +60,7 @@ def node_search_filter_query_constructor(
55
60
  filter_params: dict[str, Any] = {}
56
61
 
57
62
  if filters.node_labels is not None:
58
- node_labels = '|'.join(filters.node_labels)
63
+ node_labels = '|'.join(list(map(lucene_sanitize, filters.node_labels)))
59
64
  node_label_filter = ' AND n:' + node_labels
60
65
  filter_query += node_label_filter
61
66
 
@@ -68,8 +73,19 @@ def edge_search_filter_query_constructor(
68
73
  filter_query: LiteralString = ''
69
74
  filter_params: dict[str, Any] = {}
70
75
 
76
+ if filters.edge_types is not None:
77
+ edge_types = filters.edge_types
78
+ edge_types_filter = '\nAND r.name in $edge_types'
79
+ filter_query += edge_types_filter
80
+ filter_params['edge_types'] = edge_types
81
+
82
+ if filters.node_labels is not None:
83
+ node_labels = '|'.join(list(map(lucene_sanitize, filters.node_labels)))
84
+ node_label_filter = '\nAND n:' + node_labels + ' AND m:' + node_labels
85
+ filter_query += node_label_filter
86
+
71
87
  if filters.valid_at is not None:
72
- valid_at_filter = ' AND ('
88
+ valid_at_filter = '\nAND ('
73
89
  for i, or_list in enumerate(filters.valid_at):
74
90
  for j, date_filter in enumerate(or_list):
75
91
  filter_params['valid_at_' + str(j)] = date_filter.date
@@ -159,7 +159,7 @@ async def edge_fulltext_search(
159
159
  """
160
160
  CALL db.index.fulltext.queryRelationships("edge_name_and_fact", $query, {limit: $limit})
161
161
  YIELD relationship AS rel, score
162
- MATCH (:Entity)-[r:RELATES_TO]->(:Entity)
162
+ MATCH (n:Entity)-[r:RELATES_TO]->(m:Entity)
163
163
  WHERE r.group_id IN $group_ids"""
164
164
  + filter_query
165
165
  + """\nWITH r, score, startNode(r) AS n, endNode(r) AS m
@@ -174,7 +174,8 @@ async def edge_fulltext_search(
174
174
  r.episodes AS episodes,
175
175
  r.expired_at AS expired_at,
176
176
  r.valid_at AS valid_at,
177
- r.invalid_at AS invalid_at
177
+ r.invalid_at AS invalid_at,
178
+ properties(r) AS attributes
178
179
  ORDER BY score DESC LIMIT $limit
179
180
  """
180
181
  )
@@ -210,9 +211,9 @@ async def edge_similarity_search(
210
211
  filter_query, filter_params = edge_search_filter_query_constructor(search_filter)
211
212
  query_params.update(filter_params)
212
213
 
213
- group_filter_query: LiteralString = ''
214
+ group_filter_query: LiteralString = 'WHERE r.group_id IS NOT NULL'
214
215
  if group_ids is not None:
215
- group_filter_query += 'WHERE r.group_id IN $group_ids'
216
+ group_filter_query += '\nAND r.group_id IN $group_ids'
216
217
  query_params['group_ids'] = group_ids
217
218
  query_params['source_node_uuid'] = source_node_uuid
218
219
  query_params['target_node_uuid'] = target_node_uuid
@@ -226,8 +227,8 @@ async def edge_similarity_search(
226
227
  query: LiteralString = (
227
228
  RUNTIME_QUERY
228
229
  + """
229
- MATCH (n:Entity)-[r:RELATES_TO]->(m:Entity)
230
- """
230
+ MATCH (n:Entity)-[r:RELATES_TO]->(m:Entity)
231
+ """
231
232
  + group_filter_query
232
233
  + filter_query
233
234
  + """\nWITH DISTINCT r, vector.similarity.cosine(r.fact_embedding, $search_vector) AS score
@@ -243,7 +244,8 @@ async def edge_similarity_search(
243
244
  r.episodes AS episodes,
244
245
  r.expired_at AS expired_at,
245
246
  r.valid_at AS valid_at,
246
- r.invalid_at AS invalid_at
247
+ r.invalid_at AS invalid_at,
248
+ properties(r) AS attributes
247
249
  ORDER BY score DESC
248
250
  LIMIT $limit
249
251
  """
@@ -285,7 +287,7 @@ async def edge_bfs_search(
285
287
  UNWIND $bfs_origin_node_uuids AS origin_uuid
286
288
  MATCH path = (origin:Entity|Episodic {uuid: origin_uuid})-[:RELATES_TO|MENTIONS]->{1,3}(n:Entity)
287
289
  UNWIND relationships(path) AS rel
288
- MATCH ()-[r:RELATES_TO]-()
290
+ MATCH (n:Entity)-[r:RELATES_TO]-(m:Entity)
289
291
  WHERE r.uuid = rel.uuid
290
292
  """
291
293
  + filter_query
@@ -301,7 +303,8 @@ async def edge_bfs_search(
301
303
  r.episodes AS episodes,
302
304
  r.expired_at AS expired_at,
303
305
  r.valid_at AS valid_at,
304
- r.invalid_at AS invalid_at
306
+ r.invalid_at AS invalid_at,
307
+ properties(r) AS attributes
305
308
  LIMIT $limit
306
309
  """
307
310
  )
@@ -337,10 +340,10 @@ async def node_fulltext_search(
337
340
 
338
341
  query = (
339
342
  """
340
- CALL db.index.fulltext.queryNodes("node_name_and_summary", $query, {limit: $limit})
341
- YIELD node AS n, score
342
- WHERE n:Entity
343
- """
343
+ CALL db.index.fulltext.queryNodes("node_name_and_summary", $query, {limit: $limit})
344
+ YIELD node AS n, score
345
+ WHERE n:Entity
346
+ """
344
347
  + filter_query
345
348
  + ENTITY_NODE_RETURN
346
349
  + """
@@ -373,9 +376,9 @@ async def node_similarity_search(
373
376
  # vector similarity search over entity names
374
377
  query_params: dict[str, Any] = {}
375
378
 
376
- group_filter_query: LiteralString = ''
379
+ group_filter_query: LiteralString = 'WHERE n.group_id IS NOT NULL'
377
380
  if group_ids is not None:
378
- group_filter_query += 'WHERE n.group_id IN $group_ids'
381
+ group_filter_query += ' AND n.group_id IN $group_ids'
379
382
  query_params['group_ids'] = group_ids
380
383
 
381
384
  filter_query, filter_params = node_search_filter_query_constructor(search_filter)
@@ -771,7 +774,8 @@ async def get_relevant_edges(
771
774
  episodes: e.episodes,
772
775
  expired_at: e.expired_at,
773
776
  valid_at: e.valid_at,
774
- invalid_at: e.invalid_at
777
+ invalid_at: e.invalid_at,
778
+ attributes: properties(e)
775
779
  })[..$limit] AS matches
776
780
  """
777
781
  )
@@ -837,7 +841,8 @@ async def get_edge_invalidation_candidates(
837
841
  episodes: e.episodes,
838
842
  expired_at: e.expired_at,
839
843
  valid_at: e.valid_at,
840
- invalid_at: e.invalid_at
844
+ invalid_at: e.invalid_at,
845
+ attributes: properties(e)
841
846
  })[..$limit] AS matches
842
847
  """
843
848
  )
@@ -137,16 +137,34 @@ async def add_nodes_and_edges_bulk_tx(
137
137
  entity_data['labels'] = list(set(node.labels + ['Entity']))
138
138
  nodes.append(entity_data)
139
139
 
140
+ edges: list[dict[str, Any]] = []
140
141
  for edge in entity_edges:
141
142
  if edge.fact_embedding is None:
142
143
  await edge.generate_embedding(embedder)
144
+ edge_data: dict[str, Any] = {
145
+ 'uuid': edge.uuid,
146
+ 'source_node_uuid': edge.source_node_uuid,
147
+ 'target_node_uuid': edge.target_node_uuid,
148
+ 'name': edge.name,
149
+ 'fact': edge.fact,
150
+ 'fact_embedding': edge.fact_embedding,
151
+ 'group_id': edge.group_id,
152
+ 'episodes': edge.episodes,
153
+ 'created_at': edge.created_at,
154
+ 'expired_at': edge.expired_at,
155
+ 'valid_at': edge.valid_at,
156
+ 'invalid_at': edge.invalid_at,
157
+ }
158
+
159
+ edge_data.update(edge.attributes or {})
160
+ edges.append(edge_data)
143
161
 
144
162
  await tx.run(EPISODIC_NODE_SAVE_BULK, episodes=episodes)
145
163
  await tx.run(ENTITY_NODE_SAVE_BULK, nodes=nodes)
146
164
  await tx.run(
147
165
  EPISODIC_EDGE_SAVE_BULK, episodic_edges=[edge.model_dump() for edge in episodic_edges]
148
166
  )
149
- await tx.run(ENTITY_EDGE_SAVE_BULK, entity_edges=[edge.model_dump() for edge in entity_edges])
167
+ await tx.run(ENTITY_EDGE_SAVE_BULK, entity_edges=edges)
150
168
 
151
169
 
152
170
  async def extract_nodes_and_edges_bulk(
@@ -18,6 +18,8 @@ import logging
18
18
  from datetime import datetime
19
19
  from time import time
20
20
 
21
+ from pydantic import BaseModel
22
+
21
23
  from graphiti_core.edges import (
22
24
  CommunityEdge,
23
25
  EntityEdge,
@@ -35,9 +37,6 @@ from graphiti_core.prompts.extract_edges import ExtractedEdges, MissingFacts
35
37
  from graphiti_core.search.search_filters import SearchFilters
36
38
  from graphiti_core.search.search_utils import get_edge_invalidation_candidates, get_relevant_edges
37
39
  from graphiti_core.utils.datetime_utils import ensure_utc, utc_now
38
- from graphiti_core.utils.maintenance.temporal_operations import (
39
- get_edge_contradictions,
40
- )
41
40
 
42
41
  logger = logging.getLogger(__name__)
43
42
 
@@ -86,6 +85,7 @@ async def extract_edges(
86
85
  nodes: list[EntityNode],
87
86
  previous_episodes: list[EpisodicNode],
88
87
  group_id: str = '',
88
+ edge_types: dict[str, BaseModel] | None = None,
89
89
  ) -> list[EntityEdge]:
90
90
  start = time()
91
91
 
@@ -94,12 +94,25 @@ async def extract_edges(
94
94
 
95
95
  node_uuids_by_name_map = {node.name: node.uuid for node in nodes}
96
96
 
97
+ edge_types_context = (
98
+ [
99
+ {
100
+ 'fact_type_name': type_name,
101
+ 'fact_type_description': type_model.__doc__,
102
+ }
103
+ for type_name, type_model in edge_types.items()
104
+ ]
105
+ if edge_types is not None
106
+ else []
107
+ )
108
+
97
109
  # Prepare context for LLM
98
110
  context = {
99
111
  'episode_content': episode.content,
100
112
  'nodes': [node.name for node in nodes],
101
113
  'previous_episodes': [ep.content for ep in previous_episodes],
102
114
  'reference_time': episode.valid_at,
115
+ 'edge_types': edge_types_context,
103
116
  'custom_prompt': '',
104
117
  }
105
118
 
@@ -147,6 +160,14 @@ async def extract_edges(
147
160
  invalid_at = edge_data.get('invalid_at', None)
148
161
  valid_at_datetime = None
149
162
  invalid_at_datetime = None
163
+ source_node_uuid = node_uuids_by_name_map.get(edge_data.get('source_entity_name', ''), '')
164
+ target_node_uuid = node_uuids_by_name_map.get(edge_data.get('target_entity_name', ''), '')
165
+
166
+ if source_node_uuid == '' or target_node_uuid == '':
167
+ logger.warning(
168
+ f'WARNING: source or target node not filled {edge_data.get("edge_name")}. source_node_uuid: {source_node_uuid} and target_node_uuid: {target_node_uuid} '
169
+ )
170
+ continue
150
171
 
151
172
  if valid_at:
152
173
  try:
@@ -164,12 +185,8 @@ async def extract_edges(
164
185
  except ValueError as e:
165
186
  logger.warning(f'WARNING: Error parsing invalid_at date: {e}. Input: {invalid_at}')
166
187
  edge = EntityEdge(
167
- source_node_uuid=node_uuids_by_name_map.get(
168
- edge_data.get('source_entity_name', ''), ''
169
- ),
170
- target_node_uuid=node_uuids_by_name_map.get(
171
- edge_data.get('target_entity_name', ''), ''
172
- ),
188
+ source_node_uuid=source_node_uuid,
189
+ target_node_uuid=target_node_uuid,
173
190
  name=edge_data.get('relation_type', ''),
174
191
  group_id=group_id,
175
192
  fact=edge_data.get('fact', ''),
@@ -236,6 +253,9 @@ async def resolve_extracted_edges(
236
253
  clients: GraphitiClients,
237
254
  extracted_edges: list[EntityEdge],
238
255
  episode: EpisodicNode,
256
+ entities: list[EntityNode],
257
+ edge_types: dict[str, BaseModel],
258
+ edge_type_map: dict[tuple[str, str], list[str]],
239
259
  ) -> tuple[list[EntityEdge], list[EntityEdge]]:
240
260
  driver = clients.driver
241
261
  llm_client = clients.llm_client
@@ -245,7 +265,7 @@ async def resolve_extracted_edges(
245
265
 
246
266
  search_results: tuple[list[list[EntityEdge]], list[list[EntityEdge]]] = await semaphore_gather(
247
267
  get_relevant_edges(driver, extracted_edges, SearchFilters()),
248
- get_edge_invalidation_candidates(driver, extracted_edges, SearchFilters()),
268
+ get_edge_invalidation_candidates(driver, extracted_edges, SearchFilters(), 0.2),
249
269
  )
250
270
 
251
271
  related_edges_lists, edge_invalidation_candidates = search_results
@@ -254,15 +274,50 @@ async def resolve_extracted_edges(
254
274
  f'Related edges lists: {[(e.name, e.uuid) for edges_lst in related_edges_lists for e in edges_lst]}'
255
275
  )
256
276
 
277
+ # Build entity hash table
278
+ uuid_entity_map: dict[str, EntityNode] = {entity.uuid: entity for entity in entities}
279
+
280
+ # Determine which edge types are relevant for each edge
281
+ edge_types_lst: list[dict[str, BaseModel]] = []
282
+ for extracted_edge in extracted_edges:
283
+ source_node_labels = uuid_entity_map[extracted_edge.source_node_uuid].labels + ['Entity']
284
+ target_node_labels = uuid_entity_map[extracted_edge.target_node_uuid].labels + ['Entity']
285
+ label_tuples = [
286
+ (source_label, target_label)
287
+ for source_label in source_node_labels
288
+ for target_label in target_node_labels
289
+ ]
290
+
291
+ extracted_edge_types = {}
292
+ for label_tuple in label_tuples:
293
+ type_names = edge_type_map.get(label_tuple, [])
294
+ for type_name in type_names:
295
+ type_model = edge_types.get(type_name)
296
+ if type_model is None:
297
+ continue
298
+
299
+ extracted_edge_types[type_name] = type_model
300
+
301
+ edge_types_lst.append(extracted_edge_types)
302
+
257
303
  # resolve edges with related edges in the graph and find invalidation candidates
258
304
  results: list[tuple[EntityEdge, list[EntityEdge]]] = list(
259
305
  await semaphore_gather(
260
306
  *[
261
307
  resolve_extracted_edge(
262
- llm_client, extracted_edge, related_edges, existing_edges, episode
308
+ llm_client,
309
+ extracted_edge,
310
+ related_edges,
311
+ existing_edges,
312
+ episode,
313
+ extracted_edge_types,
263
314
  )
264
- for extracted_edge, related_edges, existing_edges in zip(
265
- extracted_edges, related_edges_lists, edge_invalidation_candidates, strict=True
315
+ for extracted_edge, related_edges, existing_edges, extracted_edge_types in zip(
316
+ extracted_edges,
317
+ related_edges_lists,
318
+ edge_invalidation_candidates,
319
+ edge_types_lst,
320
+ strict=True,
266
321
  )
267
322
  ]
268
323
  )
@@ -326,10 +381,86 @@ async def resolve_extracted_edge(
326
381
  related_edges: list[EntityEdge],
327
382
  existing_edges: list[EntityEdge],
328
383
  episode: EpisodicNode,
384
+ edge_types: dict[str, BaseModel] | None = None,
329
385
  ) -> tuple[EntityEdge, list[EntityEdge]]:
330
- resolved_edge, invalidation_candidates = await semaphore_gather(
331
- dedupe_extracted_edge(llm_client, extracted_edge, related_edges, episode),
332
- get_edge_contradictions(llm_client, extracted_edge, existing_edges),
386
+ if len(related_edges) == 0 and len(existing_edges) == 0:
387
+ return extracted_edge, []
388
+
389
+ start = time()
390
+
391
+ # Prepare context for LLM
392
+ related_edges_context = [
393
+ {'id': edge.uuid, 'fact': edge.fact} for i, edge in enumerate(related_edges)
394
+ ]
395
+
396
+ invalidation_edge_candidates_context = [
397
+ {'id': i, 'fact': existing_edge.fact} for i, existing_edge in enumerate(existing_edges)
398
+ ]
399
+
400
+ edge_types_context = (
401
+ [
402
+ {
403
+ 'fact_type_id': i,
404
+ 'fact_type_name': type_name,
405
+ 'fact_type_description': type_model.__doc__,
406
+ }
407
+ for i, (type_name, type_model) in enumerate(edge_types.items())
408
+ ]
409
+ if edge_types is not None
410
+ else []
411
+ )
412
+
413
+ context = {
414
+ 'existing_edges': related_edges_context,
415
+ 'new_edge': extracted_edge.fact,
416
+ 'edge_invalidation_candidates': invalidation_edge_candidates_context,
417
+ 'edge_types': edge_types_context,
418
+ }
419
+
420
+ llm_response = await llm_client.generate_response(
421
+ prompt_library.dedupe_edges.resolve_edge(context),
422
+ response_model=EdgeDuplicate,
423
+ model_size=ModelSize.small,
424
+ )
425
+
426
+ duplicate_fact_id: int = llm_response.get('duplicate_fact_id', -1)
427
+
428
+ resolved_edge = (
429
+ related_edges[duplicate_fact_id]
430
+ if 0 <= duplicate_fact_id < len(related_edges)
431
+ else extracted_edge
432
+ )
433
+
434
+ if duplicate_fact_id >= 0 and episode is not None:
435
+ resolved_edge.episodes.append(episode.uuid)
436
+
437
+ contradicted_facts: list[int] = llm_response.get('contradicted_facts', [])
438
+
439
+ invalidation_candidates: list[EntityEdge] = [existing_edges[i] for i in contradicted_facts]
440
+
441
+ fact_type: str = str(llm_response.get('fact_type'))
442
+ if fact_type.upper() != 'DEFAULT' and edge_types is not None:
443
+ resolved_edge.name = fact_type
444
+
445
+ edge_attributes_context = {
446
+ 'episode_content': episode.content,
447
+ 'reference_time': episode.valid_at,
448
+ 'fact': resolved_edge.fact,
449
+ }
450
+
451
+ edge_model = edge_types.get(fact_type)
452
+
453
+ edge_attributes_response = await llm_client.generate_response(
454
+ prompt_library.extract_edges.extract_attributes(edge_attributes_context),
455
+ response_model=edge_model, # type: ignore
456
+ model_size=ModelSize.small,
457
+ )
458
+
459
+ resolved_edge.attributes = edge_attributes_response
460
+
461
+ end = time()
462
+ logger.debug(
463
+ f'Resolved Edge: {extracted_edge.name} is {resolved_edge.name}, in {(end - start) * 1000} ms'
333
464
  )
334
465
 
335
466
  now = utc_now()
@@ -29,7 +29,7 @@ from graphiti_core.llm_client import LLMClient
29
29
  from graphiti_core.llm_client.config import ModelSize
30
30
  from graphiti_core.nodes import EntityNode, EpisodeType, EpisodicNode, create_entity_node_embeddings
31
31
  from graphiti_core.prompts import prompt_library
32
- from graphiti_core.prompts.dedupe_nodes import NodeDuplicate
32
+ from graphiti_core.prompts.dedupe_nodes import NodeDuplicate, NodeResolutions
33
33
  from graphiti_core.prompts.extract_nodes import (
34
34
  ExtractedEntities,
35
35
  ExtractedEntity,
@@ -243,28 +243,65 @@ async def resolve_extracted_nodes(
243
243
 
244
244
  existing_nodes_lists: list[list[EntityNode]] = [result.nodes for result in search_results]
245
245
 
246
- resolved_nodes: list[EntityNode] = await semaphore_gather(
247
- *[
248
- resolve_extracted_node(
249
- llm_client,
250
- extracted_node,
251
- existing_nodes,
252
- episode,
253
- previous_episodes,
254
- entity_types.get(
255
- next((item for item in extracted_node.labels if item != 'Entity'), '')
256
- )
257
- if entity_types is not None
258
- else None,
259
- )
260
- for extracted_node, existing_nodes in zip(
261
- extracted_nodes, existing_nodes_lists, strict=True
262
- )
263
- ]
246
+ entity_types_dict: dict[str, BaseModel] = entity_types if entity_types is not None else {}
247
+
248
+ # Prepare context for LLM
249
+ extracted_nodes_context = [
250
+ {
251
+ 'id': i,
252
+ 'name': node.name,
253
+ 'entity_type': node.labels,
254
+ 'entity_type_description': entity_types_dict.get(
255
+ next((item for item in node.labels if item != 'Entity'), '')
256
+ ).__doc__
257
+ or 'Default Entity Type',
258
+ 'duplication_candidates': [
259
+ {
260
+ **{
261
+ 'idx': j,
262
+ 'name': candidate.name,
263
+ 'entity_types': candidate.labels,
264
+ },
265
+ **candidate.attributes,
266
+ }
267
+ for j, candidate in enumerate(existing_nodes_lists[i])
268
+ ],
269
+ }
270
+ for i, node in enumerate(extracted_nodes)
271
+ ]
272
+
273
+ context = {
274
+ 'extracted_nodes': extracted_nodes_context,
275
+ 'episode_content': episode.content if episode is not None else '',
276
+ 'previous_episodes': [ep.content for ep in previous_episodes]
277
+ if previous_episodes is not None
278
+ else [],
279
+ }
280
+
281
+ llm_response = await llm_client.generate_response(
282
+ prompt_library.dedupe_nodes.nodes(context),
283
+ response_model=NodeResolutions,
264
284
  )
265
285
 
286
+ node_resolutions: list = llm_response.get('entity_resolutions', [])
287
+
288
+ resolved_nodes: list[EntityNode] = []
266
289
  uuid_map: dict[str, str] = {}
267
- for extracted_node, resolved_node in zip(extracted_nodes, resolved_nodes, strict=True):
290
+ for resolution in node_resolutions:
291
+ resolution_id = resolution.get('id', -1)
292
+ duplicate_idx = resolution.get('duplicate_idx', -1)
293
+
294
+ extracted_node = extracted_nodes[resolution_id]
295
+
296
+ resolved_node = (
297
+ existing_nodes_lists[resolution_id][duplicate_idx]
298
+ if 0 <= duplicate_idx < len(existing_nodes_lists[resolution_id])
299
+ else extracted_node
300
+ )
301
+
302
+ resolved_node.name = resolution.get('name')
303
+
304
+ resolved_nodes.append(resolved_node)
268
305
  uuid_map[extracted_node.uuid] = resolved_node.uuid
269
306
 
270
307
  logger.debug(f'Resolved nodes: {[(n.name, n.uuid) for n in resolved_nodes]}')
@@ -410,6 +447,7 @@ async def extract_attributes_from_node(
410
447
  llm_response = await llm_client.generate_response(
411
448
  prompt_library.extract_nodes.extract_attributes(summary_context),
412
449
  response_model=entity_attributes_model,
450
+ model_size=ModelSize.small,
413
451
  )
414
452
 
415
453
  node.summary = llm_response.get('summary', node.summary)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: graphiti-core
3
- Version: 0.11.6rc9
3
+ Version: 0.12.0rc2
4
4
  Summary: A temporal graph building library
5
5
  License: Apache-2.0
6
6
  Author: Paul Paliychuk
@@ -3,40 +3,40 @@ graphiti_core/cross_encoder/__init__.py,sha256=hry59vz21x-AtGZ0MJ7ugw0HTwJkXiddp
3
3
  graphiti_core/cross_encoder/bge_reranker_client.py,sha256=sY7RKsCp90vTjYxv6vmIHT4p3oCsFCRYWH-H0Ia0vN0,1449
4
4
  graphiti_core/cross_encoder/client.py,sha256=KLsbfWKOEaAV3adFe3XZlAeb-gje9_sVKCVZTaJP3ac,1441
5
5
  graphiti_core/cross_encoder/openai_reranker_client.py,sha256=R8NHHbIlPtnHMq_ZcCOAlgdULXuqzy5IzJoGuqYPEv0,4488
6
- graphiti_core/edges.py,sha256=WGJQAMtyj-huh343nmm5NMzIeNlnmNLBLO-d7JprNwQ,15256
6
+ graphiti_core/edges.py,sha256=XkI5J8ZwZ_PZcXZUKrJVG6zMn8gVwWsTOikO6Ozecr8,16154
7
7
  graphiti_core/embedder/__init__.py,sha256=EL564ZuE-DZjcuKNUK_exMn_XHXm2LdO9fzdXePVKL4,179
8
8
  graphiti_core/embedder/client.py,sha256=qEpSHceL_Gc4QQPJWIOnuNLemNuR_TYA4r28t2Vldbg,1115
9
- graphiti_core/embedder/gemini.py,sha256=Dh80q21auMvDBjwqHsI_wFrJtgWwCXRHzwg31-BSR34,2661
9
+ graphiti_core/embedder/gemini.py,sha256=7En-W46YxqC5qL3vYB5Ed-Xm0hqLxi7-LgZ95c4M7ME,3263
10
10
  graphiti_core/embedder/openai.py,sha256=bIThUoLMeGlHG2-3VikzK6JZfOHKn4PKvUMx5sHxJy8,2192
11
11
  graphiti_core/embedder/voyage.py,sha256=gQhdcz2IYPSyOcDn3w8aHToVS3KQhyZrUBm4vqr3WcE,2224
12
12
  graphiti_core/errors.py,sha256=Nib1uQx2cO_VOizupmRjpFfmuRg-hFAVqTtZAuBehR8,2405
13
- graphiti_core/graphiti.py,sha256=niRU1sZ2hf3a8WUPQAEIOsg8ixR_r_NUau5famfe1uM,27090
13
+ graphiti_core/graphiti.py,sha256=rAPPFJQt44qx5jpv7tKITFsIuIDopCsDqHqsWPOWPcI,27848
14
14
  graphiti_core/graphiti_types.py,sha256=46ueysKPwUCpxkMePHdCJLspfTImoZN7JiRwpz7cqd0,1013
15
- graphiti_core/helpers.py,sha256=_qx6G2XFaukSnNrQeRt2CheuhYerMOvK34LMw1jBMoA,2942
15
+ graphiti_core/helpers.py,sha256=O4HnwrOZzBtTwOsgujMEClW7kM0QsK1ImKxoWOdE8U4,2919
16
16
  graphiti_core/llm_client/__init__.py,sha256=PA80TSMeX-sUXITXEAxMDEt3gtfZgcJrGJUcyds1mSo,207
17
17
  graphiti_core/llm_client/anthropic_client.py,sha256=392rtkH_I7yOJUlQvjoOnS8Lz14WBP8egQ3OfRH0nFs,12481
18
18
  graphiti_core/llm_client/client.py,sha256=v_w5TBbDJYYADCXSs2r287g5Ami2Urma-GGEbHSI_Jg,5826
19
19
  graphiti_core/llm_client/config.py,sha256=90IgSBxZE_3nWdaEONVLUznI8lytPA7ZyexQz-_c55U,2560
20
20
  graphiti_core/llm_client/errors.py,sha256=pn6brRiLW60DAUIXJYKBT6MInrS4ueuH1hNLbn_JbQo,1243
21
- graphiti_core/llm_client/gemini_client.py,sha256=JdcQTvwbaqko0alodUW3WP328i6Pu_GLUQ9yBAFBXwY,7558
21
+ graphiti_core/llm_client/gemini_client.py,sha256=OdRAB2bWlXAi3gRmE1xVljYJ0T7JTZC82VK71wHyZi8,7722
22
22
  graphiti_core/llm_client/groq_client.py,sha256=k7zbXHfOpb4jhvvKFsccVYTq4yGGpxmY7xzNA02N2zk,2559
23
23
  graphiti_core/llm_client/openai_client.py,sha256=lLTZkd-PxEicTBmQefGoWLGTCb4QSU2Cq3x5W4kRYXg,7412
24
24
  graphiti_core/llm_client/openai_generic_client.py,sha256=WElMnPqdb1CxzYH4p2-m_9rVMr5M93-eXnc3yVxBgFg,7001
25
25
  graphiti_core/llm_client/utils.py,sha256=zKpxXEbKa369m4W7RDEf-m56kH46V1Mx3RowcWZEWWs,1000
26
26
  graphiti_core/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
27
27
  graphiti_core/models/edges/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
28
- graphiti_core/models/edges/edge_db_queries.py,sha256=S02lXOW-st2BVu9Mm3I1SyVfwISAquCxBB8666gv7I4,2674
28
+ graphiti_core/models/edges/edge_db_queries.py,sha256=W2-ljKnZOt5MlD9_M4f_823GdyTMRzW2tJX0CezaixY,2284
29
29
  graphiti_core/models/nodes/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
30
30
  graphiti_core/models/nodes/node_db_queries.py,sha256=AQgRGVO-GgFWfLq1G6k8s86WItwpXruy3Mj4DBli-vM,2145
31
31
  graphiti_core/nodes.py,sha256=U19DZ0MIi8GfEsx8D-Jgl8c2SGXO8QovVQpYy6FmUpo,18542
32
32
  graphiti_core/prompts/__init__.py,sha256=EA-x9xUki9l8wnu2l8ek_oNf75-do5tq5hVq7Zbv8Kw,101
33
- graphiti_core/prompts/dedupe_edges.py,sha256=q60fqIjFQlOzOeL7Y35gwABWQBqKkMarBQBok1pj1C4,3409
34
- graphiti_core/prompts/dedupe_nodes.py,sha256=lwdz2Hhi1VIPAC0S8qMV9iaulYtR3FsiujsL7T1Ec8U,4494
33
+ graphiti_core/prompts/dedupe_edges.py,sha256=AFVC1EQ0TvNkSp0G7QZmIh3YpGg9FVXo1_sT3TlRqA8,5473
34
+ graphiti_core/prompts/dedupe_nodes.py,sha256=hUdlEUaYUJGLeX6Usy_hfF7fVkaZW-Qhuq5hYrgQ2ZM,7298
35
35
  graphiti_core/prompts/eval.py,sha256=gnBQTmwsCl3Qvwpcm7aieVszzo6y1sMCUT8jQiKTvvE,5317
36
36
  graphiti_core/prompts/extract_edge_dates.py,sha256=3Drs3CmvP0gJN5BidWSxrNvLet3HPoTybU3BUIAoc0Y,4218
37
- graphiti_core/prompts/extract_edges.py,sha256=uSoQS32rpUzQJGEhayErMdj72rocGdqVq54Macjf7po,5102
37
+ graphiti_core/prompts/extract_edges.py,sha256=i7fXBVZ_FH_sAP413T8D02yylIEIia7scaTOuc3dkwY,6497
38
38
  graphiti_core/prompts/extract_nodes.py,sha256=EYuX99P8ly7pSOBz87ZA9fJF8V6g6epbVj5Cq0YM8h8,9624
39
- graphiti_core/prompts/invalidate_edges.py,sha256=3KZQ-Hyop2hae1jK_8GIdUg4ltvFVEDQpvvzt98KvwY,3547
39
+ graphiti_core/prompts/invalidate_edges.py,sha256=yfpcs_pyctnoM77ULPZXEtKW0oHr1MeLsJzC5yrE-o4,3547
40
40
  graphiti_core/prompts/lib.py,sha256=DCyHePM4_q-CptTpEXGO_dBv9k7xDtclEaB1dGu7EcI,4092
41
41
  graphiti_core/prompts/models.py,sha256=NgxdbPHJpBEcpbXovKyScgpBc73Q-GIW-CBDlBtDjto,894
42
42
  graphiti_core/prompts/prompt_helpers.py,sha256=-9TABwIcIQUVHcNANx6wIZd-FT2DgYKyGTfx4IGYq2I,64
@@ -46,21 +46,21 @@ graphiti_core/search/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hS
46
46
  graphiti_core/search/search.py,sha256=XCEYz4-I341eWiZ-czeFlH5hdbHTTLymhHiD153p6DQ,15122
47
47
  graphiti_core/search/search_config.py,sha256=VvKg6AB_RPhoe56DBBXHRBXHThAVJ_OLFCyq_yKof-A,3765
48
48
  graphiti_core/search/search_config_recipes.py,sha256=4GquRphHhJlpXQhAZOySYnCzBWYoTwxlJj44eTOavZQ,7443
49
- graphiti_core/search/search_filters.py,sha256=JkP7NbM4Dor27dne5vAuxbJic12dIJDtWJxNqmVuRec,5884
49
+ graphiti_core/search/search_filters.py,sha256=AT074LfIw3nc-jKtDbHCopkUIk5eY1_HRl6EoHSQsUc,6551
50
50
  graphiti_core/search/search_helpers.py,sha256=G5Ceaq5Pfgx0Weelqgeylp_pUHwiBnINaUYsDbURJbE,2636
51
- graphiti_core/search/search_utils.py,sha256=SD1cjB1pdeDVa133zlXV6Z79ghmYLehGKkXbHLKx8e4,34360
51
+ graphiti_core/search/search_utils.py,sha256=AimBkRgvSFHqAkt1vraTVj_bVAp3JKrR6JUMpoZa8RI,34469
52
52
  graphiti_core/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
53
- graphiti_core/utils/bulk_utils.py,sha256=_56TZ_gyOt3V5UAmiGhSNtcWOVgZdLZ9rAYCkvkMJuk,15221
53
+ graphiti_core/utils/bulk_utils.py,sha256=WFCfo_OrFD2bpm13Vkex4A1YLVHX4pjCm5acZ1CwzEI,15848
54
54
  graphiti_core/utils/datetime_utils.py,sha256=Ti-2tnrDFRzBsbfblzsHybsM3jaDLP4-VT2t0VhpIzU,1357
55
55
  graphiti_core/utils/maintenance/__init__.py,sha256=vW4H1KyapTl-OOz578uZABYcpND4wPx3Vt6aAPaXh78,301
56
56
  graphiti_core/utils/maintenance/community_operations.py,sha256=TF-4eHuvMe_jMqvWg3swxK80zLLtOR0t1pmUUQlNulM,10067
57
- graphiti_core/utils/maintenance/edge_operations.py,sha256=vjeCUt_WlKZ3SRIIgaF9pqc8Na6ajLOzUIo2pWo7NyU,14756
57
+ graphiti_core/utils/maintenance/edge_operations.py,sha256=9_vC3piLUlGM-C30Z4DsN6UWQoxbabsSlJYD7z1zsr4,19222
58
58
  graphiti_core/utils/maintenance/graph_data_operations.py,sha256=BIJKc8tbvU4IjWxLgeotw57b1eE3Iw8YtV74j6eo4RQ,7493
59
- graphiti_core/utils/maintenance/node_operations.py,sha256=-gHQH2_jaAg6XYH33tC5Pna1B4VcGHyPTpaK0NY3xow,15308
59
+ graphiti_core/utils/maintenance/node_operations.py,sha256=xuXKY0aoe_Idl9Edtb8FxSqoCa45M043nCMraJuAcW8,16606
60
60
  graphiti_core/utils/maintenance/temporal_operations.py,sha256=mJkw9xLB4W2BsLfC5POr0r-PHWL9SIfNj_l_xu0B5ug,3410
61
61
  graphiti_core/utils/maintenance/utils.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
62
62
  graphiti_core/utils/ontology_utils/entity_types_utils.py,sha256=QJX5cG0GSSNF_Mm_yrldr69wjVAbN_MxLhOSznz85Hk,1279
63
- graphiti_core-0.11.6rc9.dist-info/LICENSE,sha256=KCUwCyDXuVEgmDWkozHyniRyWjnWUWjkuDHfU6o3JlA,11325
64
- graphiti_core-0.11.6rc9.dist-info/METADATA,sha256=pJYC5wmpGOapmPYGxryD7XrwQPBNmSYnguXgcaIUqbk,15301
65
- graphiti_core-0.11.6rc9.dist-info/WHEEL,sha256=fGIA9gx4Qxk2KDKeNJCbOEwSrmLtjWCwzBz351GyrPQ,88
66
- graphiti_core-0.11.6rc9.dist-info/RECORD,,
63
+ graphiti_core-0.12.0rc2.dist-info/LICENSE,sha256=KCUwCyDXuVEgmDWkozHyniRyWjnWUWjkuDHfU6o3JlA,11325
64
+ graphiti_core-0.12.0rc2.dist-info/METADATA,sha256=-fYdpjbrMX_KEX-KjyoGONa042ydQ8mlH0henpu9Z9U,15301
65
+ graphiti_core-0.12.0rc2.dist-info/WHEEL,sha256=fGIA9gx4Qxk2KDKeNJCbOEwSrmLtjWCwzBz351GyrPQ,88
66
+ graphiti_core-0.12.0rc2.dist-info/RECORD,,