graphiti-core 0.14.0__py3-none-any.whl → 0.15.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.

@@ -15,8 +15,18 @@ limitations under the License.
15
15
  """
16
16
 
17
17
  import asyncio
18
-
19
- from sentence_transformers import CrossEncoder
18
+ from typing import TYPE_CHECKING
19
+
20
+ if TYPE_CHECKING:
21
+ from sentence_transformers import CrossEncoder
22
+ else:
23
+ try:
24
+ from sentence_transformers import CrossEncoder
25
+ except ImportError:
26
+ raise ImportError(
27
+ 'sentence-transformers is required for BGERerankerClient. '
28
+ 'Install it with: pip install graphiti-core[sentence-transformers]'
29
+ ) from None
20
30
 
21
31
  from graphiti_core.cross_encoder.client import CrossEncoderClient
22
32
 
@@ -0,0 +1,158 @@
1
+ """
2
+ Copyright 2024, Zep Software, Inc.
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ """
16
+
17
+ import logging
18
+ import re
19
+ from typing import TYPE_CHECKING
20
+
21
+ from ..helpers import semaphore_gather
22
+ from ..llm_client import LLMConfig, RateLimitError
23
+ from .client import CrossEncoderClient
24
+
25
+ if TYPE_CHECKING:
26
+ from google import genai
27
+ from google.genai import types
28
+ else:
29
+ try:
30
+ from google import genai
31
+ from google.genai import types
32
+ except ImportError:
33
+ raise ImportError(
34
+ 'google-genai is required for GeminiRerankerClient. '
35
+ 'Install it with: pip install graphiti-core[google-genai]'
36
+ ) from None
37
+
38
+ logger = logging.getLogger(__name__)
39
+
40
+ DEFAULT_MODEL = 'gemini-2.5-flash-lite-preview-06-17'
41
+
42
+
43
+ class GeminiRerankerClient(CrossEncoderClient):
44
+ def __init__(
45
+ self,
46
+ config: LLMConfig | None = None,
47
+ client: 'genai.Client | None' = None,
48
+ ):
49
+ """
50
+ Initialize the GeminiRerankerClient with the provided configuration and client.
51
+
52
+ The Gemini Developer API does not yet support logprobs. Unlike the OpenAI reranker,
53
+ this reranker uses the Gemini API to perform direct relevance scoring of passages.
54
+ Each passage is scored individually on a 0-100 scale.
55
+
56
+ Args:
57
+ config (LLMConfig | None): The configuration for the LLM client, including API key, model, base URL, temperature, and max tokens.
58
+ client (genai.Client | None): An optional async client instance to use. If not provided, a new genai.Client is created.
59
+ """
60
+
61
+ if config is None:
62
+ config = LLMConfig()
63
+
64
+ self.config = config
65
+ if client is None:
66
+ self.client = genai.Client(api_key=config.api_key)
67
+ else:
68
+ self.client = client
69
+
70
+ async def rank(self, query: str, passages: list[str]) -> list[tuple[str, float]]:
71
+ """
72
+ Rank passages based on their relevance to the query using direct scoring.
73
+
74
+ Each passage is scored individually on a 0-100 scale, then normalized to [0,1].
75
+ """
76
+ if len(passages) <= 1:
77
+ return [(passage, 1.0) for passage in passages]
78
+
79
+ # Generate scoring prompts for each passage
80
+ scoring_prompts = []
81
+ for passage in passages:
82
+ prompt = f"""Rate how well this passage answers or relates to the query. Use a scale from 0 to 100.
83
+
84
+ Query: {query}
85
+
86
+ Passage: {passage}
87
+
88
+ Provide only a number between 0 and 100 (no explanation, just the number):"""
89
+
90
+ scoring_prompts.append(
91
+ [
92
+ types.Content(
93
+ role='user',
94
+ parts=[types.Part.from_text(text=prompt)],
95
+ ),
96
+ ]
97
+ )
98
+
99
+ try:
100
+ # Execute all scoring requests concurrently - O(n) API calls
101
+ responses = await semaphore_gather(
102
+ *[
103
+ self.client.aio.models.generate_content(
104
+ model=self.config.model or DEFAULT_MODEL,
105
+ contents=prompt_messages, # type: ignore
106
+ config=types.GenerateContentConfig(
107
+ system_instruction='You are an expert at rating passage relevance. Respond with only a number from 0-100.',
108
+ temperature=0.0,
109
+ max_output_tokens=3,
110
+ ),
111
+ )
112
+ for prompt_messages in scoring_prompts
113
+ ]
114
+ )
115
+
116
+ # Extract scores and create results
117
+ results = []
118
+ for passage, response in zip(passages, responses, strict=True):
119
+ try:
120
+ if hasattr(response, 'text') and response.text:
121
+ # Extract numeric score from response
122
+ score_text = response.text.strip()
123
+ # Handle cases where model might return non-numeric text
124
+ score_match = re.search(r'\b(\d{1,3})\b', score_text)
125
+ if score_match:
126
+ score = float(score_match.group(1))
127
+ # Normalize to [0, 1] range and clamp to valid range
128
+ normalized_score = max(0.0, min(1.0, score / 100.0))
129
+ results.append((passage, normalized_score))
130
+ else:
131
+ logger.warning(
132
+ f'Could not extract numeric score from response: {score_text}'
133
+ )
134
+ results.append((passage, 0.0))
135
+ else:
136
+ logger.warning('Empty response from Gemini for passage scoring')
137
+ results.append((passage, 0.0))
138
+ except (ValueError, AttributeError) as e:
139
+ logger.warning(f'Error parsing score from Gemini response: {e}')
140
+ results.append((passage, 0.0))
141
+
142
+ # Sort by score in descending order (highest relevance first)
143
+ results.sort(reverse=True, key=lambda x: x[1])
144
+ return results
145
+
146
+ except Exception as e:
147
+ # Check if it's a rate limit error based on Gemini API error codes
148
+ error_message = str(e).lower()
149
+ if (
150
+ 'rate limit' in error_message
151
+ or 'quota' in error_message
152
+ or 'resource_exhausted' in error_message
153
+ or '429' in str(e)
154
+ ):
155
+ raise RateLimitError from e
156
+
157
+ logger.error(f'Error in generating LLM response: {e}')
158
+ raise
@@ -22,7 +22,7 @@ import openai
22
22
  from openai import AsyncAzureOpenAI, AsyncOpenAI
23
23
 
24
24
  from ..helpers import semaphore_gather
25
- from ..llm_client import LLMConfig, RateLimitError
25
+ from ..llm_client import LLMConfig, OpenAIClient, RateLimitError
26
26
  from ..prompts import Message
27
27
  from .client import CrossEncoderClient
28
28
 
@@ -35,7 +35,7 @@ class OpenAIRerankerClient(CrossEncoderClient):
35
35
  def __init__(
36
36
  self,
37
37
  config: LLMConfig | None = None,
38
- client: AsyncOpenAI | AsyncAzureOpenAI | None = None,
38
+ client: AsyncOpenAI | AsyncAzureOpenAI | OpenAIClient | None = None,
39
39
  ):
40
40
  """
41
41
  Initialize the OpenAIRerankerClient with the provided configuration and client.
@@ -45,7 +45,7 @@ class OpenAIRerankerClient(CrossEncoderClient):
45
45
 
46
46
  Args:
47
47
  config (LLMConfig | None): The configuration for the LLM client, including API key, model, base URL, temperature, and max tokens.
48
- client (AsyncOpenAI | AsyncAzureOpenAI | None): An optional async client instance to use. If not provided, a new AsyncOpenAI client is created.
48
+ client (AsyncOpenAI | AsyncAzureOpenAI | OpenAIClient | None): An optional async client instance to use. If not provided, a new AsyncOpenAI client is created.
49
49
  """
50
50
  if config is None:
51
51
  config = LLMConfig()
@@ -53,6 +53,8 @@ class OpenAIRerankerClient(CrossEncoderClient):
53
53
  self.config = config
54
54
  if client is None:
55
55
  self.client = AsyncOpenAI(api_key=config.api_key, base_url=config.base_url)
56
+ elif isinstance(client, OpenAIClient):
57
+ self.client = client.client
56
58
  else:
57
59
  self.client = client
58
60
 
@@ -14,4 +14,6 @@ See the License for the specific language governing permissions and
14
14
  limitations under the License.
15
15
  """
16
16
 
17
- __all__ = ['GraphDriver', 'Neo4jDriver', 'FalkorDriver']
17
+ from neo4j import Neo4jDriver
18
+
19
+ __all__ = ['Neo4jDriver']
@@ -15,12 +15,22 @@ limitations under the License.
15
15
  """
16
16
 
17
17
  import logging
18
- from collections.abc import Coroutine
19
18
  from datetime import datetime
20
- from typing import Any
21
-
22
- from falkordb import Graph as FalkorGraph # type: ignore
23
- from falkordb.asyncio import FalkorDB # type: ignore
19
+ from typing import TYPE_CHECKING, Any
20
+
21
+ if TYPE_CHECKING:
22
+ from falkordb import Graph as FalkorGraph
23
+ from falkordb.asyncio import FalkorDB
24
+ else:
25
+ try:
26
+ from falkordb import Graph as FalkorGraph
27
+ from falkordb.asyncio import FalkorDB
28
+ except ImportError:
29
+ # If falkordb is not installed, raise an ImportError
30
+ raise ImportError(
31
+ 'falkordb is required for FalkorDriver. '
32
+ 'Install it with: pip install graphiti-core[falkordb]'
33
+ ) from None
24
34
 
25
35
  from graphiti_core.driver.driver import GraphDriver, GraphDriverSession
26
36
  from graphiti_core.helpers import DEFAULT_DATABASE
@@ -52,11 +62,11 @@ class FalkorDriverSession(GraphDriverSession):
52
62
  if isinstance(query, list):
53
63
  for cypher, params in query:
54
64
  params = convert_datetimes_to_strings(params)
55
- await self.graph.query(str(cypher), params)
65
+ await self.graph.query(str(cypher), params) # type: ignore[reportUnknownArgumentType]
56
66
  else:
57
67
  params = dict(kwargs)
58
68
  params = convert_datetimes_to_strings(params)
59
- await self.graph.query(str(query), params)
69
+ await self.graph.query(str(query), params) # type: ignore[reportUnknownArgumentType]
60
70
  # Assuming `graph.query` is async (ideal); otherwise, wrap in executor
61
71
  return None
62
72
 
@@ -66,22 +76,30 @@ class FalkorDriver(GraphDriver):
66
76
 
67
77
  def __init__(
68
78
  self,
69
- uri: str,
70
- user: str,
71
- password: str,
79
+ host: str = 'localhost',
80
+ port: int = 6379,
81
+ username: str | None = None,
82
+ password: str | None = None,
83
+ falkor_db: FalkorDB | None = None,
72
84
  ):
73
- super().__init__()
74
- uri_parts = uri.split('://', 1)
75
- uri = f'{uri_parts[0]}://{user}:{password}@{uri_parts[1]}'
85
+ """
86
+ Initialize the FalkorDB driver.
76
87
 
77
- self.client = FalkorDB(
78
- host='your-db.falkor.cloud', port=6380, password='your_password', ssl=True
79
- )
88
+ FalkorDB is a multi-tenant graph database.
89
+ To connect, provide the host and port.
90
+ The default parameters assume a local (on-premises) FalkorDB instance.
91
+ """
92
+ super().__init__()
93
+ if falkor_db is not None:
94
+ # If a FalkorDB instance is provided, use it directly
95
+ self.client = falkor_db
96
+ else:
97
+ self.client = FalkorDB(host=host, port=port, username=username, password=password)
80
98
 
81
99
  def _get_graph(self, graph_name: str | None) -> FalkorGraph:
82
- # FalkorDB requires a non-None database name for multi-tenant graphs; the default is "DEFAULT_DATABASE"
100
+ # FalkorDB requires a non-None database name for multi-tenant graphs; the default is DEFAULT_DATABASE
83
101
  if graph_name is None:
84
- graph_name = 'DEFAULT_DATABASE'
102
+ graph_name = DEFAULT_DATABASE
85
103
  return self.client.select_graph(graph_name)
86
104
 
87
105
  async def execute_query(self, cypher_query_, **kwargs: Any):
@@ -92,7 +110,7 @@ class FalkorDriver(GraphDriver):
92
110
  params = convert_datetimes_to_strings(dict(kwargs))
93
111
 
94
112
  try:
95
- result = await graph.query(cypher_query_, params)
113
+ result = await graph.query(cypher_query_, params) # type: ignore[reportUnknownArgumentType]
96
114
  except Exception as e:
97
115
  if 'already indexed' in str(e):
98
116
  # check if index already exists
@@ -102,17 +120,36 @@ class FalkorDriver(GraphDriver):
102
120
  raise
103
121
 
104
122
  # Convert the result header to a list of strings
105
- header = [h[1].decode('utf-8') for h in result.header]
106
- return result.result_set, header, None
123
+ header = [h[1] for h in result.header]
124
+
125
+ # Convert FalkorDB's result format (list of lists) to the format expected by Graphiti (list of dicts)
126
+ records = []
127
+ for row in result.result_set:
128
+ record = {}
129
+ for i, field_name in enumerate(header):
130
+ if i < len(row):
131
+ record[field_name] = row[i]
132
+ else:
133
+ # If there are more fields in header than values in row, set to None
134
+ record[field_name] = None
135
+ records.append(record)
136
+
137
+ return records, header, None
107
138
 
108
139
  def session(self, database: str | None) -> GraphDriverSession:
109
140
  return FalkorDriverSession(self._get_graph(database))
110
141
 
111
142
  async def close(self) -> None:
112
- await self.client.connection.close()
113
-
114
- async def delete_all_indexes(self, database_: str = DEFAULT_DATABASE) -> Coroutine:
115
- return self.execute_query(
143
+ """Close the driver connection."""
144
+ if hasattr(self.client, 'aclose'):
145
+ await self.client.aclose() # type: ignore[reportUnknownMemberType]
146
+ elif hasattr(self.client.connection, 'aclose'):
147
+ await self.client.connection.aclose()
148
+ elif hasattr(self.client.connection, 'close'):
149
+ await self.client.connection.close()
150
+
151
+ async def delete_all_indexes(self, database_: str = DEFAULT_DATABASE) -> None:
152
+ await self.execute_query(
116
153
  'CALL db.indexes() YIELD name DROP INDEX name',
117
154
  database_=database_,
118
155
  )
@@ -18,7 +18,7 @@ import logging
18
18
  from collections.abc import Coroutine
19
19
  from typing import Any
20
20
 
21
- from neo4j import AsyncGraphDatabase
21
+ from neo4j import AsyncGraphDatabase, EagerResult
22
22
  from typing_extensions import LiteralString
23
23
 
24
24
  from graphiti_core.driver.driver import GraphDriver, GraphDriverSession
@@ -42,7 +42,7 @@ class Neo4jDriver(GraphDriver):
42
42
  auth=(user or '', password or ''),
43
43
  )
44
44
 
45
- async def execute_query(self, cypher_query_: LiteralString, **kwargs: Any) -> Coroutine:
45
+ async def execute_query(self, cypher_query_: LiteralString, **kwargs: Any) -> EagerResult:
46
46
  params = kwargs.pop('params', None)
47
47
  result = await self.client.execute_query(cypher_query_, parameters_=params, **kwargs)
48
48
 
@@ -54,7 +54,9 @@ class Neo4jDriver(GraphDriver):
54
54
  async def close(self) -> None:
55
55
  return await self.client.close()
56
56
 
57
- def delete_all_indexes(self, database_: str = DEFAULT_DATABASE) -> Coroutine:
57
+ def delete_all_indexes(
58
+ self, database_: str = DEFAULT_DATABASE
59
+ ) -> Coroutine[Any, Any, EagerResult]:
58
60
  return self.client.execute_query(
59
61
  'CALL db.indexes() YIELD name DROP INDEX name',
60
62
  database_=database_,
@@ -15,9 +15,21 @@ limitations under the License.
15
15
  """
16
16
 
17
17
  from collections.abc import Iterable
18
+ from typing import TYPE_CHECKING
19
+
20
+ if TYPE_CHECKING:
21
+ from google import genai
22
+ from google.genai import types
23
+ else:
24
+ try:
25
+ from google import genai
26
+ from google.genai import types
27
+ except ImportError:
28
+ raise ImportError(
29
+ 'google-genai is required for GeminiEmbedder. '
30
+ 'Install it with: pip install graphiti-core[google-genai]'
31
+ ) from None
18
32
 
19
- from google import genai # type: ignore
20
- from google.genai import types # type: ignore
21
33
  from pydantic import Field
22
34
 
23
35
  from .client import EmbedderClient, EmbedderConfig
@@ -15,8 +15,19 @@ limitations under the License.
15
15
  """
16
16
 
17
17
  from collections.abc import Iterable
18
+ from typing import TYPE_CHECKING
19
+
20
+ if TYPE_CHECKING:
21
+ import voyageai
22
+ else:
23
+ try:
24
+ import voyageai
25
+ except ImportError:
26
+ raise ImportError(
27
+ 'voyageai is required for VoyageAIEmbedderClient. '
28
+ 'Install it with: pip install graphiti-core[voyageai]'
29
+ ) from None
18
30
 
19
- import voyageai # type: ignore
20
31
  from pydantic import Field
21
32
 
22
33
  from .client import EmbedderClient, EmbedderConfig
@@ -38,7 +49,7 @@ class VoyageAIEmbedder(EmbedderClient):
38
49
  if config is None:
39
50
  config = VoyageAIEmbedderConfig()
40
51
  self.config = config
41
- self.client = voyageai.AsyncClient(api_key=config.api_key)
52
+ self.client = voyageai.AsyncClient(api_key=config.api_key) # type: ignore[reportUnknownMemberType]
42
53
 
43
54
  async def create(
44
55
  self, input_data: str | list[str] | Iterable[int] | Iterable[Iterable[int]]
graphiti_core/graphiti.py CHANGED
@@ -101,7 +101,7 @@ class AddEpisodeResults(BaseModel):
101
101
  class Graphiti:
102
102
  def __init__(
103
103
  self,
104
- uri: str,
104
+ uri: str | None = None,
105
105
  user: str | None = None,
106
106
  password: str | None = None,
107
107
  llm_client: LLMClient | None = None,
@@ -162,7 +162,12 @@ class Graphiti:
162
162
  Graphiti if you're using the default OpenAIClient.
163
163
  """
164
164
 
165
- self.driver = graph_driver if graph_driver else Neo4jDriver(uri, user, password)
165
+ if graph_driver:
166
+ self.driver = graph_driver
167
+ else:
168
+ if uri is None:
169
+ raise ValueError('uri must be provided when graph_driver is None')
170
+ self.driver = Neo4jDriver(uri, user, password)
166
171
 
167
172
  self.database = DEFAULT_DATABASE
168
173
  self.store_raw_episode_content = store_raw_episode_content
graphiti_core/helpers.py CHANGED
@@ -19,6 +19,7 @@ import os
19
19
  import re
20
20
  from collections.abc import Coroutine
21
21
  from datetime import datetime
22
+ from typing import Any
22
23
 
23
24
  import numpy as np
24
25
  from dotenv import load_dotenv
@@ -31,7 +32,7 @@ from graphiti_core.errors import GroupIdValidationError
31
32
 
32
33
  load_dotenv()
33
34
 
34
- DEFAULT_DATABASE = os.getenv('DEFAULT_DATABASE', 'neo4j')
35
+ DEFAULT_DATABASE = os.getenv('DEFAULT_DATABASE', 'default_db')
35
36
  USE_PARALLEL_RUNTIME = bool(os.getenv('USE_PARALLEL_RUNTIME', False))
36
37
  SEMAPHORE_LIMIT = int(os.getenv('SEMAPHORE_LIMIT', 20))
37
38
  MAX_REFLEXION_ITERATIONS = int(os.getenv('MAX_REFLEXION_ITERATIONS', 0))
@@ -99,7 +100,7 @@ def normalize_l2(embedding: list[float]) -> NDArray:
99
100
  async def semaphore_gather(
100
101
  *coroutines: Coroutine,
101
102
  max_coroutines: int | None = None,
102
- ):
103
+ ) -> list[Any]:
103
104
  semaphore = asyncio.Semaphore(max_coroutines or SEMAPHORE_LIMIT)
104
105
 
105
106
  async def _wrap_coroutine(coroutine):
@@ -19,11 +19,8 @@ import logging
19
19
  import os
20
20
  import typing
21
21
  from json import JSONDecodeError
22
- from typing import Literal
22
+ from typing import TYPE_CHECKING, Literal
23
23
 
24
- import anthropic
25
- from anthropic import AsyncAnthropic
26
- from anthropic.types import MessageParam, ToolChoiceParam, ToolUnionParam
27
24
  from pydantic import BaseModel, ValidationError
28
25
 
29
26
  from ..prompts.models import Message
@@ -31,6 +28,22 @@ from .client import LLMClient
31
28
  from .config import DEFAULT_MAX_TOKENS, LLMConfig, ModelSize
32
29
  from .errors import RateLimitError, RefusalError
33
30
 
31
+ if TYPE_CHECKING:
32
+ import anthropic
33
+ from anthropic import AsyncAnthropic
34
+ from anthropic.types import MessageParam, ToolChoiceParam, ToolUnionParam
35
+ else:
36
+ try:
37
+ import anthropic
38
+ from anthropic import AsyncAnthropic
39
+ from anthropic.types import MessageParam, ToolChoiceParam, ToolUnionParam
40
+ except ImportError:
41
+ raise ImportError(
42
+ 'anthropic is required for AnthropicClient. '
43
+ 'Install it with: pip install graphiti-core[anthropic]'
44
+ ) from None
45
+
46
+
34
47
  logger = logging.getLogger(__name__)
35
48
 
36
49
  AnthropicModel = Literal[
@@ -17,19 +17,34 @@ limitations under the License.
17
17
  import json
18
18
  import logging
19
19
  import typing
20
+ from typing import TYPE_CHECKING, ClassVar
20
21
 
21
- from google import genai # type: ignore
22
- from google.genai import types # type: ignore
23
22
  from pydantic import BaseModel
24
23
 
25
24
  from ..prompts.models import Message
26
- from .client import LLMClient
25
+ from .client import MULTILINGUAL_EXTRACTION_RESPONSES, LLMClient
27
26
  from .config import DEFAULT_MAX_TOKENS, LLMConfig, ModelSize
28
27
  from .errors import RateLimitError
29
28
 
29
+ if TYPE_CHECKING:
30
+ from google import genai
31
+ from google.genai import types
32
+ else:
33
+ try:
34
+ from google import genai
35
+ from google.genai import types
36
+ except ImportError:
37
+ # If gemini client is not installed, raise an ImportError
38
+ raise ImportError(
39
+ 'google-genai is required for GeminiClient. '
40
+ 'Install it with: pip install graphiti-core[google-genai]'
41
+ ) from None
42
+
43
+
30
44
  logger = logging.getLogger(__name__)
31
45
 
32
- DEFAULT_MODEL = 'gemini-2.0-flash'
46
+ DEFAULT_MODEL = 'gemini-2.5-flash'
47
+ DEFAULT_SMALL_MODEL = 'models/gemini-2.5-flash-lite-preview-06-17'
33
48
 
34
49
 
35
50
  class GeminiClient(LLMClient):
@@ -43,27 +58,34 @@ class GeminiClient(LLMClient):
43
58
  model (str): The model name to use for generating responses.
44
59
  temperature (float): The temperature to use for generating responses.
45
60
  max_tokens (int): The maximum number of tokens to generate in a response.
46
-
61
+ thinking_config (types.ThinkingConfig | None): Optional thinking configuration for models that support it.
47
62
  Methods:
48
- __init__(config: LLMConfig | None = None, cache: bool = False):
49
- Initializes the GeminiClient with the provided configuration and cache setting.
63
+ __init__(config: LLMConfig | None = None, cache: bool = False, thinking_config: types.ThinkingConfig | None = None):
64
+ Initializes the GeminiClient with the provided configuration, cache setting, and optional thinking config.
50
65
 
51
66
  _generate_response(messages: list[Message]) -> dict[str, typing.Any]:
52
67
  Generates a response from the language model based on the provided messages.
53
68
  """
54
69
 
70
+ # Class-level constants
71
+ MAX_RETRIES: ClassVar[int] = 2
72
+
55
73
  def __init__(
56
74
  self,
57
75
  config: LLMConfig | None = None,
58
76
  cache: bool = False,
59
77
  max_tokens: int = DEFAULT_MAX_TOKENS,
78
+ thinking_config: types.ThinkingConfig | None = None,
60
79
  ):
61
80
  """
62
- Initialize the GeminiClient with the provided configuration and cache setting.
81
+ Initialize the GeminiClient with the provided configuration, cache setting, and optional thinking config.
63
82
 
64
83
  Args:
65
84
  config (LLMConfig | None): The configuration for the LLM client, including API key, model, temperature, and max tokens.
66
85
  cache (bool): Whether to use caching for responses. Defaults to False.
86
+ thinking_config (types.ThinkingConfig | None): Optional thinking configuration for models that support it.
87
+ Only use with models that support thinking (gemini-2.5+). Defaults to None.
88
+
67
89
  """
68
90
  if config is None:
69
91
  config = LLMConfig()
@@ -76,6 +98,50 @@ class GeminiClient(LLMClient):
76
98
  api_key=config.api_key,
77
99
  )
78
100
  self.max_tokens = max_tokens
101
+ self.thinking_config = thinking_config
102
+
103
+ def _check_safety_blocks(self, response) -> None:
104
+ """Check if response was blocked for safety reasons and raise appropriate exceptions."""
105
+ # Check if the response was blocked for safety reasons
106
+ if not (hasattr(response, 'candidates') and response.candidates):
107
+ return
108
+
109
+ candidate = response.candidates[0]
110
+ if not (hasattr(candidate, 'finish_reason') and candidate.finish_reason == 'SAFETY'):
111
+ return
112
+
113
+ # Content was blocked for safety reasons - collect safety details
114
+ safety_info = []
115
+ safety_ratings = getattr(candidate, 'safety_ratings', None)
116
+
117
+ if safety_ratings:
118
+ for rating in safety_ratings:
119
+ if getattr(rating, 'blocked', False):
120
+ category = getattr(rating, 'category', 'Unknown')
121
+ probability = getattr(rating, 'probability', 'Unknown')
122
+ safety_info.append(f'{category}: {probability}')
123
+
124
+ safety_details = (
125
+ ', '.join(safety_info) if safety_info else 'Content blocked for safety reasons'
126
+ )
127
+ raise Exception(f'Response blocked by Gemini safety filters: {safety_details}')
128
+
129
+ def _check_prompt_blocks(self, response) -> None:
130
+ """Check if prompt was blocked and raise appropriate exceptions."""
131
+ prompt_feedback = getattr(response, 'prompt_feedback', None)
132
+ if not prompt_feedback:
133
+ return
134
+
135
+ block_reason = getattr(prompt_feedback, 'block_reason', None)
136
+ if block_reason:
137
+ raise Exception(f'Prompt blocked by Gemini: {block_reason}')
138
+
139
+ def _get_model_for_size(self, model_size: ModelSize) -> str:
140
+ """Get the appropriate model name based on the requested size."""
141
+ if model_size == ModelSize.small:
142
+ return self.small_model or DEFAULT_SMALL_MODEL
143
+ else:
144
+ return self.model or DEFAULT_MODEL
79
145
 
80
146
  async def _generate_response(
81
147
  self,
@@ -91,17 +157,17 @@ class GeminiClient(LLMClient):
91
157
  messages (list[Message]): A list of messages to send to the language model.
92
158
  response_model (type[BaseModel] | None): An optional Pydantic model to parse the response into.
93
159
  max_tokens (int): The maximum number of tokens to generate in the response.
160
+ model_size (ModelSize): The size of the model to use (small or medium).
94
161
 
95
162
  Returns:
96
163
  dict[str, typing.Any]: The response from the language model.
97
164
 
98
165
  Raises:
99
166
  RateLimitError: If the API rate limit is exceeded.
100
- RefusalError: If the content is blocked by the model.
101
- Exception: If there is an error generating the response.
167
+ Exception: If there is an error generating the response or content is blocked.
102
168
  """
103
169
  try:
104
- gemini_messages: list[types.Content] = []
170
+ gemini_messages: typing.Any = []
105
171
  # If a response model is provided, add schema for structured output
106
172
  system_prompt = ''
107
173
  if response_model is not None:
@@ -127,6 +193,9 @@ class GeminiClient(LLMClient):
127
193
  types.Content(role=m.role, parts=[types.Part.from_text(text=m.content)])
128
194
  )
129
195
 
196
+ # Get the appropriate model for the requested size
197
+ model = self._get_model_for_size(model_size)
198
+
130
199
  # Create generation config
131
200
  generation_config = types.GenerateContentConfig(
132
201
  temperature=self.temperature,
@@ -134,15 +203,20 @@ class GeminiClient(LLMClient):
134
203
  response_mime_type='application/json' if response_model else None,
135
204
  response_schema=response_model if response_model else None,
136
205
  system_instruction=system_prompt,
206
+ thinking_config=self.thinking_config,
137
207
  )
138
208
 
139
209
  # Generate content using the simple string approach
140
210
  response = await self.client.aio.models.generate_content(
141
- model=self.model or DEFAULT_MODEL,
142
- contents=gemini_messages, # type: ignore[arg-type] # mypy fails on broad union type
211
+ model=model,
212
+ contents=gemini_messages,
143
213
  config=generation_config,
144
214
  )
145
215
 
216
+ # Check for safety and prompt blocks
217
+ self._check_safety_blocks(response)
218
+ self._check_prompt_blocks(response)
219
+
146
220
  # If this was a structured output request, parse the response into the Pydantic model
147
221
  if response_model is not None:
148
222
  try:
@@ -160,9 +234,16 @@ class GeminiClient(LLMClient):
160
234
  return {'content': response.text}
161
235
 
162
236
  except Exception as e:
163
- # Check if it's a rate limit error
164
- if 'rate limit' in str(e).lower() or 'quota' in str(e).lower():
237
+ # Check if it's a rate limit error based on Gemini API error codes
238
+ error_message = str(e).lower()
239
+ if (
240
+ 'rate limit' in error_message
241
+ or 'quota' in error_message
242
+ or 'resource_exhausted' in error_message
243
+ or '429' in str(e)
244
+ ):
165
245
  raise RateLimitError from e
246
+
166
247
  logger.error(f'Error in generating LLM response: {e}')
167
248
  raise
168
249
 
@@ -174,13 +255,14 @@ class GeminiClient(LLMClient):
174
255
  model_size: ModelSize = ModelSize.medium,
175
256
  ) -> dict[str, typing.Any]:
176
257
  """
177
- Generate a response from the Gemini language model.
178
- This method overrides the parent class method to provide a direct implementation.
258
+ Generate a response from the Gemini language model with retry logic and error handling.
259
+ This method overrides the parent class method to provide a direct implementation with advanced retry logic.
179
260
 
180
261
  Args:
181
262
  messages (list[Message]): A list of messages to send to the language model.
182
263
  response_model (type[BaseModel] | None): An optional Pydantic model to parse the response into.
183
- max_tokens (int): The maximum number of tokens to generate in the response.
264
+ max_tokens (int | None): The maximum number of tokens to generate in the response.
265
+ model_size (ModelSize): The size of the model to use (small or medium).
184
266
 
185
267
  Returns:
186
268
  dict[str, typing.Any]: The response from the language model.
@@ -188,10 +270,53 @@ class GeminiClient(LLMClient):
188
270
  if max_tokens is None:
189
271
  max_tokens = self.max_tokens
190
272
 
191
- # Call the internal _generate_response method
192
- return await self._generate_response(
193
- messages=messages,
194
- response_model=response_model,
195
- max_tokens=max_tokens,
196
- model_size=model_size,
197
- )
273
+ retry_count = 0
274
+ last_error = None
275
+
276
+ # Add multilingual extraction instructions
277
+ messages[0].content += MULTILINGUAL_EXTRACTION_RESPONSES
278
+
279
+ while retry_count <= self.MAX_RETRIES:
280
+ try:
281
+ response = await self._generate_response(
282
+ messages=messages,
283
+ response_model=response_model,
284
+ max_tokens=max_tokens,
285
+ model_size=model_size,
286
+ )
287
+ return response
288
+ except RateLimitError:
289
+ # Rate limit errors should not trigger retries (fail fast)
290
+ raise
291
+ except Exception as e:
292
+ last_error = e
293
+
294
+ # Check if this is a safety block - these typically shouldn't be retried
295
+ if 'safety' in str(e).lower() or 'blocked' in str(e).lower():
296
+ logger.warning(f'Content blocked by safety filters: {e}')
297
+ raise
298
+
299
+ # Don't retry if we've hit the max retries
300
+ if retry_count >= self.MAX_RETRIES:
301
+ logger.error(f'Max retries ({self.MAX_RETRIES}) exceeded. Last error: {e}')
302
+ raise
303
+
304
+ retry_count += 1
305
+
306
+ # Construct a detailed error message for the LLM
307
+ error_context = (
308
+ f'The previous response attempt was invalid. '
309
+ f'Error type: {e.__class__.__name__}. '
310
+ f'Error details: {str(e)}. '
311
+ f'Please try again with a valid response, ensuring the output matches '
312
+ f'the expected format and constraints.'
313
+ )
314
+
315
+ error_message = Message(role='user', content=error_context)
316
+ messages.append(error_message)
317
+ logger.warning(
318
+ f'Retrying after application error (attempt {retry_count}/{self.MAX_RETRIES}): {e}'
319
+ )
320
+
321
+ # If we somehow get here, raise the last error
322
+ raise last_error or Exception('Max retries exceeded with no specific error')
@@ -17,10 +17,21 @@ limitations under the License.
17
17
  import json
18
18
  import logging
19
19
  import typing
20
+ from typing import TYPE_CHECKING
20
21
 
21
- import groq
22
- from groq import AsyncGroq
23
- from groq.types.chat import ChatCompletionMessageParam
22
+ if TYPE_CHECKING:
23
+ import groq
24
+ from groq import AsyncGroq
25
+ from groq.types.chat import ChatCompletionMessageParam
26
+ else:
27
+ try:
28
+ import groq
29
+ from groq import AsyncGroq
30
+ from groq.types.chat import ChatCompletionMessageParam
31
+ except ImportError:
32
+ raise ImportError(
33
+ 'groq is required for GroqClient. Install it with: pip install graphiti-core[groq]'
34
+ ) from None
24
35
  from pydantic import BaseModel
25
36
 
26
37
  from ..prompts.models import Message
graphiti_core/nodes.py CHANGED
@@ -540,10 +540,18 @@ class CommunityNode(Node):
540
540
 
541
541
  # Node helpers
542
542
  def get_episodic_node_from_record(record: Any) -> EpisodicNode:
543
+ created_at = parse_db_date(record['created_at'])
544
+ valid_at = parse_db_date(record['valid_at'])
545
+
546
+ if created_at is None:
547
+ raise ValueError(f'created_at cannot be None for episode {record.get("uuid", "unknown")}')
548
+ if valid_at is None:
549
+ raise ValueError(f'valid_at cannot be None for episode {record.get("uuid", "unknown")}')
550
+
543
551
  return EpisodicNode(
544
552
  content=record['content'],
545
- created_at=parse_db_date(record['created_at']), # type: ignore
546
- valid_at=parse_db_date(record['valid_at']), # type: ignore
553
+ created_at=created_at,
554
+ valid_at=valid_at,
547
555
  uuid=record['uuid'],
548
556
  group_id=record['group_id'],
549
557
  source=EpisodeType.from_str(record['source']),
@@ -19,7 +19,6 @@ from enum import Enum
19
19
  from typing import Any
20
20
 
21
21
  from pydantic import BaseModel, Field
22
- from typing_extensions import LiteralString
23
22
 
24
23
 
25
24
  class ComparisonOperator(Enum):
@@ -53,8 +52,8 @@ class SearchFilters(BaseModel):
53
52
 
54
53
  def node_search_filter_query_constructor(
55
54
  filters: SearchFilters,
56
- ) -> tuple[LiteralString, dict[str, Any]]:
57
- filter_query: LiteralString = ''
55
+ ) -> tuple[str, dict[str, Any]]:
56
+ filter_query: str = ''
58
57
  filter_params: dict[str, Any] = {}
59
58
 
60
59
  if filters.node_labels is not None:
@@ -67,8 +66,8 @@ def node_search_filter_query_constructor(
67
66
 
68
67
  def edge_search_filter_query_constructor(
69
68
  filters: SearchFilters,
70
- ) -> tuple[LiteralString, dict[str, Any]]:
71
- filter_query: LiteralString = ''
69
+ ) -> tuple[str, dict[str, Any]]:
70
+ filter_query: str = ''
72
71
  filter_params: dict[str, Any] = {}
73
72
 
74
73
  if filters.edge_types is not None:
@@ -278,9 +278,6 @@ async def edge_similarity_search(
278
278
  routing_='r',
279
279
  )
280
280
 
281
- if driver.provider == 'falkordb':
282
- records = [dict(zip(header, row, strict=True)) for row in records]
283
-
284
281
  edges = [get_entity_edge_from_record(record) for record in records]
285
282
 
286
283
  return edges
@@ -377,8 +374,6 @@ async def node_fulltext_search(
377
374
  database_=DEFAULT_DATABASE,
378
375
  routing_='r',
379
376
  )
380
- if driver.provider == 'falkordb':
381
- records = [dict(zip(header, row, strict=True)) for row in records]
382
377
 
383
378
  nodes = [get_entity_node_from_record(record) for record in records]
384
379
 
@@ -433,8 +428,7 @@ async def node_similarity_search(
433
428
  database_=DEFAULT_DATABASE,
434
429
  routing_='r',
435
430
  )
436
- if driver.provider == 'falkordb':
437
- records = [dict(zip(header, row, strict=True)) for row in records]
431
+
438
432
  nodes = [get_entity_node_from_record(record) for record in records]
439
433
 
440
434
  return nodes
@@ -545,7 +539,8 @@ async def community_fulltext_search(
545
539
  comm.group_id AS group_id,
546
540
  comm.name AS name,
547
541
  comm.created_at AS created_at,
548
- comm.summary AS summary
542
+ comm.summary AS summary,
543
+ comm.name_embedding AS name_embedding
549
544
  ORDER BY score DESC
550
545
  LIMIT $limit
551
546
  """
@@ -595,7 +590,8 @@ async def community_similarity_search(
595
590
  comm.group_id AS group_id,
596
591
  comm.name AS name,
597
592
  comm.created_at AS created_at,
598
- comm.summary AS summary
593
+ comm.summary AS summary,
594
+ comm.name_embedding AS name_embedding
599
595
  ORDER BY score DESC
600
596
  LIMIT $limit
601
597
  """
@@ -40,7 +40,7 @@ async def get_community_clusters(
40
40
  database_=DEFAULT_DATABASE,
41
41
  )
42
42
 
43
- group_ids = group_id_values[0]['group_ids']
43
+ group_ids = group_id_values[0]['group_ids'] if group_id_values else []
44
44
 
45
45
  for group_id in group_ids:
46
46
  projection: dict[str, list[Neighbor]] = {}
@@ -297,7 +297,7 @@ async def resolve_extracted_edges(
297
297
  embedder = clients.embedder
298
298
  await create_entity_edge_embeddings(embedder, extracted_edges)
299
299
 
300
- search_results: tuple[list[list[EntityEdge]], list[list[EntityEdge]]] = await semaphore_gather(
300
+ search_results = await semaphore_gather(
301
301
  get_relevant_edges(driver, extracted_edges, SearchFilters()),
302
302
  get_edge_invalidation_candidates(driver, extracted_edges, SearchFilters(), 0.2),
303
303
  )
@@ -21,7 +21,7 @@ from typing_extensions import LiteralString
21
21
 
22
22
  from graphiti_core.driver.driver import GraphDriver
23
23
  from graphiti_core.graph_queries import get_fulltext_indices, get_range_indices
24
- from graphiti_core.helpers import DEFAULT_DATABASE, semaphore_gather
24
+ from graphiti_core.helpers import DEFAULT_DATABASE, parse_db_date, semaphore_gather
25
25
  from graphiti_core.nodes import EpisodeType, EpisodicNode
26
26
 
27
27
  EPISODE_WINDOW_LEN = 3
@@ -140,10 +140,9 @@ async def retrieve_episodes(
140
140
  episodes = [
141
141
  EpisodicNode(
142
142
  content=record['content'],
143
- created_at=datetime.fromtimestamp(
144
- record['created_at'].to_native().timestamp(), timezone.utc
145
- ),
146
- valid_at=(record['valid_at'].to_native()),
143
+ created_at=parse_db_date(record['created_at'])
144
+ or datetime.min.replace(tzinfo=timezone.utc),
145
+ valid_at=parse_db_date(record['valid_at']) or datetime.min.replace(tzinfo=timezone.utc),
147
146
  uuid=record['uuid'],
148
147
  group_id=record['group_id'],
149
148
  source=EpisodeType.from_str(record['source']),
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: graphiti-core
3
- Version: 0.14.0
3
+ Version: 0.15.1
4
4
  Summary: A temporal graph building library
5
5
  Project-URL: Homepage, https://help.getzep.com/graphiti/graphiti/overview
6
6
  Project-URL: Repository, https://github.com/getzep/graphiti
@@ -21,6 +21,7 @@ Requires-Dist: anthropic>=0.49.0; extra == 'anthropic'
21
21
  Provides-Extra: dev
22
22
  Requires-Dist: anthropic>=0.49.0; extra == 'dev'
23
23
  Requires-Dist: diskcache-stubs>=5.6.3.6.20240818; extra == 'dev'
24
+ Requires-Dist: falkordb<2.0.0,>=1.1.2; extra == 'dev'
24
25
  Requires-Dist: google-genai>=1.8.0; extra == 'dev'
25
26
  Requires-Dist: groq>=0.2.0; extra == 'dev'
26
27
  Requires-Dist: ipykernel>=6.29.5; extra == 'dev'
@@ -29,7 +30,7 @@ Requires-Dist: langchain-anthropic>=0.2.4; extra == 'dev'
29
30
  Requires-Dist: langchain-openai>=0.2.6; extra == 'dev'
30
31
  Requires-Dist: langgraph>=0.2.15; extra == 'dev'
31
32
  Requires-Dist: langsmith>=0.1.108; extra == 'dev'
32
- Requires-Dist: mypy>=1.11.1; extra == 'dev'
33
+ Requires-Dist: pyright>=1.1.380; extra == 'dev'
33
34
  Requires-Dist: pytest-asyncio>=0.24.0; extra == 'dev'
34
35
  Requires-Dist: pytest-xdist>=3.6.1; extra == 'dev'
35
36
  Requires-Dist: pytest>=8.3.3; extra == 'dev'
@@ -37,12 +38,16 @@ Requires-Dist: ruff>=0.7.1; extra == 'dev'
37
38
  Requires-Dist: sentence-transformers>=3.2.1; extra == 'dev'
38
39
  Requires-Dist: transformers>=4.45.2; extra == 'dev'
39
40
  Requires-Dist: voyageai>=0.2.3; extra == 'dev'
40
- Provides-Extra: falkord-db
41
- Requires-Dist: falkordb<2.0.0,>=1.1.2; extra == 'falkord-db'
41
+ Provides-Extra: falkordb
42
+ Requires-Dist: falkordb<2.0.0,>=1.1.2; extra == 'falkordb'
42
43
  Provides-Extra: google-genai
43
44
  Requires-Dist: google-genai>=1.8.0; extra == 'google-genai'
44
45
  Provides-Extra: groq
45
46
  Requires-Dist: groq>=0.2.0; extra == 'groq'
47
+ Provides-Extra: sentence-transformers
48
+ Requires-Dist: sentence-transformers>=3.2.1; extra == 'sentence-transformers'
49
+ Provides-Extra: voyageai
50
+ Requires-Dist: voyageai>=0.2.3; extra == 'voyageai'
46
51
  Description-Content-Type: text/markdown
47
52
 
48
53
  <p align="center">
@@ -153,7 +158,7 @@ Requirements:
153
158
 
154
159
  - Python 3.10 or higher
155
160
  - Neo4j 5.26 / FalkorDB 1.1.2 or higher (serves as the embeddings storage backend)
156
- - OpenAI API key (for LLM inference and embedding)
161
+ - OpenAI API key (Graphiti defaults to OpenAI for LLM inference and embedding)
157
162
 
158
163
  > [!IMPORTANT]
159
164
  > Graphiti works best with LLM services that support Structured Output (such as OpenAI and Gemini).
@@ -167,6 +172,12 @@ Optional:
167
172
  > [!TIP]
168
173
  > The simplest way to install Neo4j is via [Neo4j Desktop](https://neo4j.com/download/). It provides a user-friendly
169
174
  > interface to manage Neo4j instances and databases.
175
+ > Alternatively, you can use FalkorDB on-premises via Docker and instantly start with the quickstart example:
176
+
177
+ ```bash
178
+ docker run -p 6379:6379 -p 3000:3000 -it --rm falkordb/falkordb:latest
179
+
180
+ ```
170
181
 
171
182
  ```bash
172
183
  pip install graphiti-core
@@ -178,7 +189,18 @@ or
178
189
  uv add graphiti-core
179
190
  ```
180
191
 
181
- You can also install optional LLM providers as extras:
192
+ ### Installing with FalkorDB Support
193
+
194
+ If you plan to use FalkorDB as your graph database backend, install with the FalkorDB extra:
195
+
196
+ ```bash
197
+ pip install graphiti-core[falkordb]
198
+
199
+ # or with uv
200
+ uv add graphiti-core[falkordb]
201
+ ```
202
+
203
+ ### You can also install optional LLM providers as extras:
182
204
 
183
205
  ```bash
184
206
  # Install with Anthropic support
@@ -192,18 +214,21 @@ pip install graphiti-core[google-genai]
192
214
 
193
215
  # Install with multiple providers
194
216
  pip install graphiti-core[anthropic,groq,google-genai]
217
+
218
+ # Install with FalkorDB and LLM providers
219
+ pip install graphiti-core[falkordb,anthropic,google-genai]
195
220
  ```
196
221
 
197
222
  ## Quick Start
198
223
 
199
224
  > [!IMPORTANT]
200
- > Graphiti uses OpenAI for LLM inference and embedding. Ensure that an `OPENAI_API_KEY` is set in your environment.
225
+ > Graphiti defaults to using OpenAI for LLM inference and embedding. Ensure that an `OPENAI_API_KEY` is set in your environment.
201
226
  > Support for Anthropic and Groq LLM inferences is available, too. Other LLM providers may be supported via OpenAI
202
227
  > compatible APIs.
203
228
 
204
229
  For a complete working example, see the [Quickstart Example](./examples/quickstart/README.md) in the examples directory. The quickstart demonstrates:
205
230
 
206
- 1. Connecting to a Neo4j database
231
+ 1. Connecting to a Neo4j or FalkorDB database
207
232
  2. Initializing Graphiti indices and constraints
208
233
  3. Adding episodes to the graph (both text and structured JSON)
209
234
  4. Searching for relationships (edges) using hybrid search
@@ -247,7 +272,7 @@ as such this feature is off by default.
247
272
 
248
273
  ## Using Graphiti with Azure OpenAI
249
274
 
250
- Graphiti supports Azure OpenAI for both LLM inference and embeddings. To use Azure OpenAI, you'll need to configure both the LLM client and embedder with your Azure OpenAI credentials.
275
+ Graphiti supports Azure OpenAI for both LLM inference and embeddings. Azure deployments often require different endpoints for LLM and embedding services, and separate deployments for default and small models.
251
276
 
252
277
  ```python
253
278
  from openai import AsyncAzureOpenAI
@@ -256,19 +281,26 @@ from graphiti_core.llm_client import LLMConfig, OpenAIClient
256
281
  from graphiti_core.embedder.openai import OpenAIEmbedder, OpenAIEmbedderConfig
257
282
  from graphiti_core.cross_encoder.openai_reranker_client import OpenAIRerankerClient
258
283
 
259
- # Azure OpenAI configuration
284
+ # Azure OpenAI configuration - use separate endpoints for different services
260
285
  api_key = "<your-api-key>"
261
286
  api_version = "<your-api-version>"
262
- azure_endpoint = "<your-azure-endpoint>"
287
+ llm_endpoint = "<your-llm-endpoint>" # e.g., "https://your-llm-resource.openai.azure.com/"
288
+ embedding_endpoint = "<your-embedding-endpoint>" # e.g., "https://your-embedding-resource.openai.azure.com/"
263
289
 
264
- # Create Azure OpenAI client for LLM
265
- azure_openai_client = AsyncAzureOpenAI(
290
+ # Create separate Azure OpenAI clients for different services
291
+ llm_client_azure = AsyncAzureOpenAI(
266
292
  api_key=api_key,
267
293
  api_version=api_version,
268
- azure_endpoint=azure_endpoint
294
+ azure_endpoint=llm_endpoint
269
295
  )
270
296
 
271
- # Create LLM Config with your Azure deployed model names
297
+ embedding_client_azure = AsyncAzureOpenAI(
298
+ api_key=api_key,
299
+ api_version=api_version,
300
+ azure_endpoint=embedding_endpoint
301
+ )
302
+
303
+ # Create LLM Config with your Azure deployment names
272
304
  azure_llm_config = LLMConfig(
273
305
  small_model="gpt-4.1-nano",
274
306
  model="gpt-4.1-mini",
@@ -281,29 +313,30 @@ graphiti = Graphiti(
281
313
  "password",
282
314
  llm_client=OpenAIClient(
283
315
  llm_config=azure_llm_config,
284
- client=azure_openai_client
316
+ client=llm_client_azure
285
317
  ),
286
318
  embedder=OpenAIEmbedder(
287
319
  config=OpenAIEmbedderConfig(
288
- embedding_model="text-embedding-3-small" # Use your Azure deployed embedding model name
320
+ embedding_model="text-embedding-3-small-deployment" # Your Azure embedding deployment name
289
321
  ),
290
- client=azure_openai_client
322
+ client=embedding_client_azure
291
323
  ),
292
- # Optional: Configure the OpenAI cross encoder with Azure OpenAI
293
324
  cross_encoder=OpenAIRerankerClient(
294
- llm_config=azure_llm_config,
295
- client=azure_openai_client
325
+ llm_config=LLMConfig(
326
+ model=azure_llm_config.small_model # Use small model for reranking
327
+ ),
328
+ client=llm_client_azure
296
329
  )
297
330
  )
298
331
 
299
332
  # Now you can use Graphiti with Azure OpenAI
300
333
  ```
301
334
 
302
- Make sure to replace the placeholder values with your actual Azure OpenAI credentials and specify the correct embedding model name that's deployed in your Azure OpenAI service.
335
+ Make sure to replace the placeholder values with your actual Azure OpenAI credentials and deployment names that match your Azure OpenAI service configuration.
303
336
 
304
337
  ## Using Graphiti with Google Gemini
305
338
 
306
- Graphiti supports Google's Gemini models for both LLM inference and embeddings. To use Gemini, you'll need to configure both the LLM client and embedder with your Google API key.
339
+ Graphiti supports Google's Gemini models for LLM inference, embeddings, and cross-encoding/reranking. To use Gemini, you'll need to configure the LLM client, embedder, and the cross-encoder with your Google API key.
307
340
 
308
341
  Install Graphiti:
309
342
 
@@ -319,6 +352,7 @@ pip install "graphiti-core[google-genai]"
319
352
  from graphiti_core import Graphiti
320
353
  from graphiti_core.llm_client.gemini_client import GeminiClient, LLMConfig
321
354
  from graphiti_core.embedder.gemini import GeminiEmbedder, GeminiEmbedderConfig
355
+ from graphiti_core.cross_encoder.gemini_reranker_client import GeminiRerankerClient
322
356
 
323
357
  # Google API key configuration
324
358
  api_key = "<your-google-api-key>"
@@ -339,12 +373,20 @@ graphiti = Graphiti(
339
373
  api_key=api_key,
340
374
  embedding_model="embedding-001"
341
375
  )
376
+ ),
377
+ cross_encoder=GeminiRerankerClient(
378
+ config=LLMConfig(
379
+ api_key=api_key,
380
+ model="gemini-2.5-flash-lite-preview-06-17"
381
+ )
342
382
  )
343
383
  )
344
384
 
345
- # Now you can use Graphiti with Google Gemini
385
+ # Now you can use Graphiti with Google Gemini for all components
346
386
  ```
347
387
 
388
+ The Gemini reranker uses the `gemini-2.5-flash-lite-preview-06-17` model by default, which is optimized for cost-effective and low-latency classification tasks. It uses the same boolean classification approach as the OpenAI reranker, leveraging Gemini's log probabilities feature to rank passage relevance.
389
+
348
390
  ## Using Graphiti with Ollama (Local LLM)
349
391
 
350
392
  Graphiti supports Ollama for running local LLMs and embedding models via Ollama's OpenAI-compatible API. This is ideal for privacy-focused applications or when you want to avoid API costs.
@@ -2,33 +2,34 @@ graphiti_core/__init__.py,sha256=e5SWFkRiaUwfprYIeIgVIh7JDedNiloZvd3roU-0aDY,55
2
2
  graphiti_core/edges.py,sha256=h67vyXYhZYqlwaOmaqjHiGns6nEjuBVSIAFBMveNVo8,16257
3
3
  graphiti_core/errors.py,sha256=cH_v9TPgEPeQE6GFOHIg5TvejpUCBddGarMY2Whxbwc,2707
4
4
  graphiti_core/graph_queries.py,sha256=KfWDp8xDnPa9bcHskw8NeMpeeHBtZWBCosVdu1Iwv34,7076
5
- graphiti_core/graphiti.py,sha256=FzSTwU5zK6aFOETXLzdEvGn8yuf5cEjrBfnwabYY-xw,32990
5
+ graphiti_core/graphiti.py,sha256=TcZTJJ_4E3qt_bFNZNDy3ujFyjZLMmSYKmixN2kdqqg,33163
6
6
  graphiti_core/graphiti_types.py,sha256=rL-9bvnLobunJfXU4hkD6mAj14pofKp_wq8QsFDZwDU,1035
7
- graphiti_core/helpers.py,sha256=xHSlDlu5cCLOw40EeJSzshUqdsbqsNqv9AGGIiI-7qI,4907
8
- graphiti_core/nodes.py,sha256=WG7czM-neIeUDjLc5JCS1k0xRDANMY1lT9rDBc7Ms8U,18724
7
+ graphiti_core/helpers.py,sha256=ixUOfWN_GJVRvdiK-RzgAYJD18nM1CLmLBDNmVrIboQ,4948
8
+ graphiti_core/nodes.py,sha256=mRK5QaTzYzznAx4OSHKFFOoiyAUKJmXbjWv3ovDJ6z8,18994
9
9
  graphiti_core/py.typed,sha256=vlmmzQOt7bmeQl9L3XJP4W6Ry0iiELepnOrinKz5KQg,79
10
10
  graphiti_core/cross_encoder/__init__.py,sha256=hry59vz21x-AtGZ0MJ7ugw0HTwJkXiddpp_Yqnwsen0,723
11
- graphiti_core/cross_encoder/bge_reranker_client.py,sha256=sY7RKsCp90vTjYxv6vmIHT4p3oCsFCRYWH-H0Ia0vN0,1449
11
+ graphiti_core/cross_encoder/bge_reranker_client.py,sha256=y3TfFxZh0Yvj6HUShmfUm6MC7OPXwWUlv1Qe5HF3S3I,1797
12
12
  graphiti_core/cross_encoder/client.py,sha256=KLsbfWKOEaAV3adFe3XZlAeb-gje9_sVKCVZTaJP3ac,1441
13
- graphiti_core/cross_encoder/openai_reranker_client.py,sha256=_Hftiz250HbEkY_26z6A1oxg4pzM8Sbr8CwnbJEsggc,4522
14
- graphiti_core/driver/__init__.py,sha256=DumfxIEY3z_nkz5YGaYH1GM50HgeAdEowNK189jcdAg,626
13
+ graphiti_core/cross_encoder/gemini_reranker_client.py,sha256=FoOwTqGFQ833X7eAZU11FX7qW4_Fs9iSNyHIc7cslFU,6158
14
+ graphiti_core/cross_encoder/openai_reranker_client.py,sha256=hoaGyu9nCNMJyP8si0Bha5Q9CFszfiHQmLgE9IsX7sY,4653
15
+ graphiti_core/driver/__init__.py,sha256=kCWimqQU19airu5gKwCmZtZuXkDfaQfKSUhMDoL-rTA,626
15
16
  graphiti_core/driver/driver.py,sha256=-FHAA2gM8FA0re-q6udmjQ6pNFdFGRQrMRuAiqX_1A4,1829
16
- graphiti_core/driver/falkordb_driver.py,sha256=Iz3wnfoJIO7EslqZvG6mduyZ5C-DWxFDPM5Q4QJRCuo,4686
17
- graphiti_core/driver/neo4j_driver.py,sha256=D8CV5GbhKoHIQ78BA9ozlwdvXPLUbBmFSfT2lww8PJk,1910
17
+ graphiti_core/driver/falkordb_driver.py,sha256=9VnZwAgv0YzpFK5g-nS7YiqOOp3zxaiCW1y2etSYt3w,6331
18
+ graphiti_core/driver/neo4j_driver.py,sha256=f8cSkcaCDyQLyI85JBprw0rdrarpd5Tq1mlX-Mz3kls,1962
18
19
  graphiti_core/embedder/__init__.py,sha256=EL564ZuE-DZjcuKNUK_exMn_XHXm2LdO9fzdXePVKL4,179
19
20
  graphiti_core/embedder/azure_openai.py,sha256=OyomPwC1fIsddI-3n6g00kQFdQznZorBhHwkQKCLUok,2384
20
21
  graphiti_core/embedder/client.py,sha256=qEpSHceL_Gc4QQPJWIOnuNLemNuR_TYA4r28t2Vldbg,1115
21
- graphiti_core/embedder/gemini.py,sha256=7En-W46YxqC5qL3vYB5Ed-Xm0hqLxi7-LgZ95c4M7ME,3263
22
+ graphiti_core/embedder/gemini.py,sha256=RbJnG-GqzzGamxsGrbjLQUu6ayQ-p-sl1y9wb0SsAik,3580
22
23
  graphiti_core/embedder/openai.py,sha256=bIThUoLMeGlHG2-3VikzK6JZfOHKn4PKvUMx5sHxJy8,2192
23
- graphiti_core/embedder/voyage.py,sha256=gQhdcz2IYPSyOcDn3w8aHToVS3KQhyZrUBm4vqr3WcE,2224
24
+ graphiti_core/embedder/voyage.py,sha256=oJHAZiNqjdEJOKgoKfGWcxK2-Ewqn5UB3vrBwIwP2u4,2546
24
25
  graphiti_core/llm_client/__init__.py,sha256=QgBWUiCeBp6YiA_xqyrDvJ9jIyy1hngH8g7FWahN3nw,776
25
- graphiti_core/llm_client/anthropic_client.py,sha256=392rtkH_I7yOJUlQvjoOnS8Lz14WBP8egQ3OfRH0nFs,12481
26
+ graphiti_core/llm_client/anthropic_client.py,sha256=xTFcrgMDK77BwnChBhYj51Jaa2mRNI850oJv2pKZI0A,12892
26
27
  graphiti_core/llm_client/azure_openai_client.py,sha256=ekERggAekbb7enes1RJqdRChf_mjaZTFXsnMbxO7azQ,2497
27
28
  graphiti_core/llm_client/client.py,sha256=v_w5TBbDJYYADCXSs2r287g5Ami2Urma-GGEbHSI_Jg,5826
28
29
  graphiti_core/llm_client/config.py,sha256=90IgSBxZE_3nWdaEONVLUznI8lytPA7ZyexQz-_c55U,2560
29
30
  graphiti_core/llm_client/errors.py,sha256=pn6brRiLW60DAUIXJYKBT6MInrS4ueuH1hNLbn_JbQo,1243
30
- graphiti_core/llm_client/gemini_client.py,sha256=OdRAB2bWlXAi3gRmE1xVljYJ0T7JTZC82VK71wHyZi8,7722
31
- graphiti_core/llm_client/groq_client.py,sha256=k7zbXHfOpb4jhvvKFsccVYTq4yGGpxmY7xzNA02N2zk,2559
31
+ graphiti_core/llm_client/gemini_client.py,sha256=LLyos2irtidZL3qZ8gGLk23l9JWuHtxRdrcmFHtn0Uw,13235
32
+ graphiti_core/llm_client/groq_client.py,sha256=bYLE_cg1QEhugsJOXh4b1vPbxagKeMWqk48240GCzMs,2922
32
33
  graphiti_core/llm_client/openai_base_client.py,sha256=gfMcKPyLrylz_ouRdoenDWXyitmgfFZ17Zthbkq3Qs4,8126
33
34
  graphiti_core/llm_client/openai_client.py,sha256=ykBK94gxzE7iXux5rvOzVNA8q0Sqzq-8njPB75XcRe8,3240
34
35
  graphiti_core/llm_client/openai_generic_client.py,sha256=WElMnPqdb1CxzYH4p2-m_9rVMr5M93-eXnc3yVxBgFg,7001
@@ -54,23 +55,23 @@ graphiti_core/search/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hS
54
55
  graphiti_core/search/search.py,sha256=bJCFaNApu5396pXTa-xciu8ORDdRFJqfE3j2ieRVd7Y,15162
55
56
  graphiti_core/search/search_config.py,sha256=VvKg6AB_RPhoe56DBBXHRBXHThAVJ_OLFCyq_yKof-A,3765
56
57
  graphiti_core/search/search_config_recipes.py,sha256=4GquRphHhJlpXQhAZOySYnCzBWYoTwxlJj44eTOavZQ,7443
57
- graphiti_core/search/search_filters.py,sha256=jG30nMWX03xoT9ohgyHNu_Xes8GwjIF2eTv6QaiWMqw,6466
58
+ graphiti_core/search/search_filters.py,sha256=H7Vgob2SvwsG56qiTDXDhI4R4MMY40TVpphY5KHPwYU,6382
58
59
  graphiti_core/search/search_helpers.py,sha256=G5Ceaq5Pfgx0Weelqgeylp_pUHwiBnINaUYsDbURJbE,2636
59
- graphiti_core/search/search_utils.py,sha256=74d3RDbx9MWkDei1U5g0K5l1EenzB1NPNYdSP9l8aEg,34958
60
+ graphiti_core/search/search_utils.py,sha256=2g5xL11IUhG2tce2hVwN1bPYWFFxuB3jJh8G-aOdYzg,34724
60
61
  graphiti_core/telemetry/__init__.py,sha256=5kALLDlU9bb2v19CdN7qVANsJWyfnL9E60J6FFgzm3o,226
61
62
  graphiti_core/telemetry/telemetry.py,sha256=47LrzOVBCcZxsYPsnSxWFiztHoxYKKxPwyRX0hnbDGc,3230
62
63
  graphiti_core/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
63
64
  graphiti_core/utils/bulk_utils.py,sha256=YnyXzmOFgqbLdIAIu9Y6aJjUZHhXj8nBnlegkXBTKi8,16344
64
65
  graphiti_core/utils/datetime_utils.py,sha256=Ti-2tnrDFRzBsbfblzsHybsM3jaDLP4-VT2t0VhpIzU,1357
65
66
  graphiti_core/utils/maintenance/__init__.py,sha256=vW4H1KyapTl-OOz578uZABYcpND4wPx3Vt6aAPaXh78,301
66
- graphiti_core/utils/maintenance/community_operations.py,sha256=2rhRqtL9gDbjXKO4-S0nGpaWvS4ck5rFiazZiogIJao,10088
67
- graphiti_core/utils/maintenance/edge_operations.py,sha256=Fwu2TLmQF_9EVcA-uUlt1ZiGC6RILIfKDr9W7R4gAno,21633
68
- graphiti_core/utils/maintenance/graph_data_operations.py,sha256=OHuiAyP1Z7dfR90dWVQ87TJQO83P0sQihJyr4WIhOhk,5362
67
+ graphiti_core/utils/maintenance/community_operations.py,sha256=AimQzT7wr4M3ofsUetHa1cPEmhsngqJoNWm3Q-3Hwww,10115
68
+ graphiti_core/utils/maintenance/edge_operations.py,sha256=sj4AJ9zPm8ACiC1wSj99bFUUmg4OgFVFnPOSXKfb3T8,21578
69
+ graphiti_core/utils/maintenance/graph_data_operations.py,sha256=NH1FLwVKqnDdt2JKa38g_y2lG08ID5cAR-GPTQccxC4,5403
69
70
  graphiti_core/utils/maintenance/node_operations.py,sha256=0WdH_VrkVXLV9YX3xPErXOFygOo2N9g3es9yIB2Yl8Q,15876
70
71
  graphiti_core/utils/maintenance/temporal_operations.py,sha256=mJkw9xLB4W2BsLfC5POr0r-PHWL9SIfNj_l_xu0B5ug,3410
71
72
  graphiti_core/utils/maintenance/utils.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
72
73
  graphiti_core/utils/ontology_utils/entity_types_utils.py,sha256=QJX5cG0GSSNF_Mm_yrldr69wjVAbN_MxLhOSznz85Hk,1279
73
- graphiti_core-0.14.0.dist-info/METADATA,sha256=ePJs8ax8EBgFysrMfz-D_uJ9RKo6O5T5DjvETb7ijqU,20591
74
- graphiti_core-0.14.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
75
- graphiti_core-0.14.0.dist-info/licenses/LICENSE,sha256=KCUwCyDXuVEgmDWkozHyniRyWjnWUWjkuDHfU6o3JlA,11325
76
- graphiti_core-0.14.0.dist-info/RECORD,,
74
+ graphiti_core-0.15.1.dist-info/METADATA,sha256=KGLbx0MsKX6M0lw_26eCbKdy_yYHm8XyaFMKB3QDf7Q,22414
75
+ graphiti_core-0.15.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
76
+ graphiti_core-0.15.1.dist-info/licenses/LICENSE,sha256=KCUwCyDXuVEgmDWkozHyniRyWjnWUWjkuDHfU6o3JlA,11325
77
+ graphiti_core-0.15.1.dist-info/RECORD,,