graphiti-core 0.22.0rc3__py3-none-any.whl → 0.22.0rc5__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/graphiti.py +459 -326
- graphiti_core/graphiti_types.py +2 -0
- graphiti_core/llm_client/anthropic_client.py +64 -45
- graphiti_core/llm_client/client.py +60 -17
- graphiti_core/llm_client/gemini_client.py +69 -52
- graphiti_core/llm_client/openai_base_client.py +62 -41
- graphiti_core/llm_client/openai_generic_client.py +62 -41
- graphiti_core/prompts/dedupe_edges.py +4 -4
- graphiti_core/prompts/dedupe_nodes.py +10 -10
- graphiti_core/prompts/extract_edges.py +4 -4
- graphiti_core/prompts/extract_nodes.py +13 -13
- graphiti_core/prompts/prompt_helpers.py +2 -2
- graphiti_core/prompts/summarize_nodes.py +12 -12
- graphiti_core/search/search_helpers.py +4 -4
- graphiti_core/tracer.py +193 -0
- graphiti_core/utils/maintenance/community_operations.py +4 -1
- graphiti_core/utils/maintenance/edge_operations.py +4 -0
- graphiti_core/utils/maintenance/node_operations.py +7 -0
- graphiti_core/utils/maintenance/temporal_operations.py +4 -1
- {graphiti_core-0.22.0rc3.dist-info → graphiti_core-0.22.0rc5.dist-info}/METADATA +5 -1
- {graphiti_core-0.22.0rc3.dist-info → graphiti_core-0.22.0rc5.dist-info}/RECORD +23 -23
- graphiti_core/utils/maintenance/utils.py +0 -0
- {graphiti_core-0.22.0rc3.dist-info → graphiti_core-0.22.0rc5.dist-info}/WHEEL +0 -0
- {graphiti_core-0.22.0rc3.dist-info → graphiti_core-0.22.0rc5.dist-info}/licenses/LICENSE +0 -0
|
@@ -67,13 +67,13 @@ def edge(context: dict[str, Any]) -> list[Message]:
|
|
|
67
67
|
Given the following context, determine whether the New Edge represents any of the edges in the list of Existing Edges.
|
|
68
68
|
|
|
69
69
|
<EXISTING EDGES>
|
|
70
|
-
{to_prompt_json(context['related_edges']
|
|
70
|
+
{to_prompt_json(context['related_edges'])}
|
|
71
71
|
</EXISTING EDGES>
|
|
72
72
|
|
|
73
73
|
<NEW EDGE>
|
|
74
|
-
{to_prompt_json(context['extracted_edges']
|
|
74
|
+
{to_prompt_json(context['extracted_edges'])}
|
|
75
75
|
</NEW EDGE>
|
|
76
|
-
|
|
76
|
+
|
|
77
77
|
Task:
|
|
78
78
|
If the New Edges represents the same factual information as any edge in Existing Edges, return the id of the duplicate fact
|
|
79
79
|
as part of the list of duplicate_facts.
|
|
@@ -98,7 +98,7 @@ def edge_list(context: dict[str, Any]) -> list[Message]:
|
|
|
98
98
|
Given the following context, find all of the duplicates in a list of facts:
|
|
99
99
|
|
|
100
100
|
Facts:
|
|
101
|
-
{to_prompt_json(context['edges']
|
|
101
|
+
{to_prompt_json(context['edges'])}
|
|
102
102
|
|
|
103
103
|
Task:
|
|
104
104
|
If any facts in Facts is a duplicate of another fact, return a new fact with one of their uuid's.
|
|
@@ -64,20 +64,20 @@ def node(context: dict[str, Any]) -> list[Message]:
|
|
|
64
64
|
role='user',
|
|
65
65
|
content=f"""
|
|
66
66
|
<PREVIOUS MESSAGES>
|
|
67
|
-
{to_prompt_json([ep for ep in context['previous_episodes']]
|
|
67
|
+
{to_prompt_json([ep for ep in context['previous_episodes']])}
|
|
68
68
|
</PREVIOUS MESSAGES>
|
|
69
69
|
<CURRENT MESSAGE>
|
|
70
70
|
{context['episode_content']}
|
|
71
71
|
</CURRENT MESSAGE>
|
|
72
72
|
<NEW ENTITY>
|
|
73
|
-
{to_prompt_json(context['extracted_node']
|
|
73
|
+
{to_prompt_json(context['extracted_node'])}
|
|
74
74
|
</NEW ENTITY>
|
|
75
75
|
<ENTITY TYPE DESCRIPTION>
|
|
76
|
-
{to_prompt_json(context['entity_type_description']
|
|
76
|
+
{to_prompt_json(context['entity_type_description'])}
|
|
77
77
|
</ENTITY TYPE DESCRIPTION>
|
|
78
78
|
|
|
79
79
|
<EXISTING ENTITIES>
|
|
80
|
-
{to_prompt_json(context['existing_nodes']
|
|
80
|
+
{to_prompt_json(context['existing_nodes'])}
|
|
81
81
|
</EXISTING ENTITIES>
|
|
82
82
|
|
|
83
83
|
Given the above EXISTING ENTITIES and their attributes, MESSAGE, and PREVIOUS MESSAGES; Determine if the NEW ENTITY extracted from the conversation
|
|
@@ -125,13 +125,13 @@ def nodes(context: dict[str, Any]) -> list[Message]:
|
|
|
125
125
|
role='user',
|
|
126
126
|
content=f"""
|
|
127
127
|
<PREVIOUS MESSAGES>
|
|
128
|
-
{to_prompt_json([ep for ep in context['previous_episodes']]
|
|
128
|
+
{to_prompt_json([ep for ep in context['previous_episodes']])}
|
|
129
129
|
</PREVIOUS MESSAGES>
|
|
130
130
|
<CURRENT MESSAGE>
|
|
131
131
|
{context['episode_content']}
|
|
132
132
|
</CURRENT MESSAGE>
|
|
133
|
-
|
|
134
|
-
|
|
133
|
+
|
|
134
|
+
|
|
135
135
|
Each of the following ENTITIES were extracted from the CURRENT MESSAGE.
|
|
136
136
|
Each entity in ENTITIES is represented as a JSON object with the following structure:
|
|
137
137
|
{{
|
|
@@ -142,11 +142,11 @@ def nodes(context: dict[str, Any]) -> list[Message]:
|
|
|
142
142
|
}}
|
|
143
143
|
|
|
144
144
|
<ENTITIES>
|
|
145
|
-
{to_prompt_json(context['extracted_nodes']
|
|
145
|
+
{to_prompt_json(context['extracted_nodes'])}
|
|
146
146
|
</ENTITIES>
|
|
147
147
|
|
|
148
148
|
<EXISTING ENTITIES>
|
|
149
|
-
{to_prompt_json(context['existing_nodes']
|
|
149
|
+
{to_prompt_json(context['existing_nodes'])}
|
|
150
150
|
</EXISTING ENTITIES>
|
|
151
151
|
|
|
152
152
|
Each entry in EXISTING ENTITIES is an object with the following structure:
|
|
@@ -197,7 +197,7 @@ def node_list(context: dict[str, Any]) -> list[Message]:
|
|
|
197
197
|
Given the following context, deduplicate a list of nodes:
|
|
198
198
|
|
|
199
199
|
Nodes:
|
|
200
|
-
{to_prompt_json(context['nodes']
|
|
200
|
+
{to_prompt_json(context['nodes'])}
|
|
201
201
|
|
|
202
202
|
Task:
|
|
203
203
|
1. Group nodes together such that all duplicate nodes are in the same list of uuids
|
|
@@ -80,7 +80,7 @@ def edge(context: dict[str, Any]) -> list[Message]:
|
|
|
80
80
|
</FACT TYPES>
|
|
81
81
|
|
|
82
82
|
<PREVIOUS_MESSAGES>
|
|
83
|
-
{to_prompt_json([ep for ep in context['previous_episodes']]
|
|
83
|
+
{to_prompt_json([ep for ep in context['previous_episodes']])}
|
|
84
84
|
</PREVIOUS_MESSAGES>
|
|
85
85
|
|
|
86
86
|
<CURRENT_MESSAGE>
|
|
@@ -88,7 +88,7 @@ def edge(context: dict[str, Any]) -> list[Message]:
|
|
|
88
88
|
</CURRENT_MESSAGE>
|
|
89
89
|
|
|
90
90
|
<ENTITIES>
|
|
91
|
-
{to_prompt_json(context['nodes']
|
|
91
|
+
{to_prompt_json(context['nodes'])}
|
|
92
92
|
</ENTITIES>
|
|
93
93
|
|
|
94
94
|
<REFERENCE_TIME>
|
|
@@ -141,7 +141,7 @@ def reflexion(context: dict[str, Any]) -> list[Message]:
|
|
|
141
141
|
|
|
142
142
|
user_prompt = f"""
|
|
143
143
|
<PREVIOUS MESSAGES>
|
|
144
|
-
{to_prompt_json([ep for ep in context['previous_episodes']]
|
|
144
|
+
{to_prompt_json([ep for ep in context['previous_episodes']])}
|
|
145
145
|
</PREVIOUS MESSAGES>
|
|
146
146
|
<CURRENT MESSAGE>
|
|
147
147
|
{context['episode_content']}
|
|
@@ -175,7 +175,7 @@ def extract_attributes(context: dict[str, Any]) -> list[Message]:
|
|
|
175
175
|
content=f"""
|
|
176
176
|
|
|
177
177
|
<MESSAGE>
|
|
178
|
-
{to_prompt_json(context['episode_content']
|
|
178
|
+
{to_prompt_json(context['episode_content'])}
|
|
179
179
|
</MESSAGE>
|
|
180
180
|
<REFERENCE TIME>
|
|
181
181
|
{context['reference_time']}
|
|
@@ -93,7 +93,7 @@ def extract_message(context: dict[str, Any]) -> list[Message]:
|
|
|
93
93
|
</ENTITY TYPES>
|
|
94
94
|
|
|
95
95
|
<PREVIOUS MESSAGES>
|
|
96
|
-
{to_prompt_json([ep for ep in context['previous_episodes']]
|
|
96
|
+
{to_prompt_json([ep for ep in context['previous_episodes']])}
|
|
97
97
|
</PREVIOUS MESSAGES>
|
|
98
98
|
|
|
99
99
|
<CURRENT MESSAGE>
|
|
@@ -201,7 +201,7 @@ def reflexion(context: dict[str, Any]) -> list[Message]:
|
|
|
201
201
|
|
|
202
202
|
user_prompt = f"""
|
|
203
203
|
<PREVIOUS MESSAGES>
|
|
204
|
-
{to_prompt_json([ep for ep in context['previous_episodes']]
|
|
204
|
+
{to_prompt_json([ep for ep in context['previous_episodes']])}
|
|
205
205
|
</PREVIOUS MESSAGES>
|
|
206
206
|
<CURRENT MESSAGE>
|
|
207
207
|
{context['episode_content']}
|
|
@@ -225,22 +225,22 @@ def classify_nodes(context: dict[str, Any]) -> list[Message]:
|
|
|
225
225
|
|
|
226
226
|
user_prompt = f"""
|
|
227
227
|
<PREVIOUS MESSAGES>
|
|
228
|
-
{to_prompt_json([ep for ep in context['previous_episodes']]
|
|
228
|
+
{to_prompt_json([ep for ep in context['previous_episodes']])}
|
|
229
229
|
</PREVIOUS MESSAGES>
|
|
230
230
|
<CURRENT MESSAGE>
|
|
231
231
|
{context['episode_content']}
|
|
232
232
|
</CURRENT MESSAGE>
|
|
233
|
-
|
|
233
|
+
|
|
234
234
|
<EXTRACTED ENTITIES>
|
|
235
235
|
{context['extracted_entities']}
|
|
236
236
|
</EXTRACTED ENTITIES>
|
|
237
|
-
|
|
237
|
+
|
|
238
238
|
<ENTITY TYPES>
|
|
239
239
|
{context['entity_types']}
|
|
240
240
|
</ENTITY TYPES>
|
|
241
|
-
|
|
241
|
+
|
|
242
242
|
Given the above conversation, extracted entities, and provided entity types and their descriptions, classify the extracted entities.
|
|
243
|
-
|
|
243
|
+
|
|
244
244
|
Guidelines:
|
|
245
245
|
1. Each entity must have exactly one type
|
|
246
246
|
2. Only use the provided ENTITY TYPES as types, do not use additional types to classify entities.
|
|
@@ -269,10 +269,10 @@ def extract_attributes(context: dict[str, Any]) -> list[Message]:
|
|
|
269
269
|
2. Only use the provided MESSAGES and ENTITY to set attribute values.
|
|
270
270
|
|
|
271
271
|
<MESSAGES>
|
|
272
|
-
{to_prompt_json(context['previous_episodes']
|
|
273
|
-
{to_prompt_json(context['episode_content']
|
|
272
|
+
{to_prompt_json(context['previous_episodes'])}
|
|
273
|
+
{to_prompt_json(context['episode_content'])}
|
|
274
274
|
</MESSAGES>
|
|
275
|
-
|
|
275
|
+
|
|
276
276
|
<ENTITY>
|
|
277
277
|
{context['node']}
|
|
278
278
|
</ENTITY>
|
|
@@ -292,12 +292,12 @@ def extract_summary(context: dict[str, Any]) -> list[Message]:
|
|
|
292
292
|
content=f"""
|
|
293
293
|
Given the MESSAGES and the ENTITY, update the summary that combines relevant information about the entity
|
|
294
294
|
from the messages and relevant information from the existing summary.
|
|
295
|
-
|
|
295
|
+
|
|
296
296
|
{summary_instructions}
|
|
297
297
|
|
|
298
298
|
<MESSAGES>
|
|
299
|
-
{to_prompt_json(context['previous_episodes']
|
|
300
|
-
{to_prompt_json(context['episode_content']
|
|
299
|
+
{to_prompt_json(context['previous_episodes'])}
|
|
300
|
+
{to_prompt_json(context['episode_content'])}
|
|
301
301
|
</MESSAGES>
|
|
302
302
|
|
|
303
303
|
<ENTITY>
|
|
@@ -20,14 +20,14 @@ from typing import Any
|
|
|
20
20
|
DO_NOT_ESCAPE_UNICODE = '\nDo not escape unicode characters.\n'
|
|
21
21
|
|
|
22
22
|
|
|
23
|
-
def to_prompt_json(data: Any, ensure_ascii: bool = False, indent: int =
|
|
23
|
+
def to_prompt_json(data: Any, ensure_ascii: bool = False, indent: int | None = None) -> str:
|
|
24
24
|
"""
|
|
25
25
|
Serialize data to JSON for use in prompts.
|
|
26
26
|
|
|
27
27
|
Args:
|
|
28
28
|
data: The data to serialize
|
|
29
29
|
ensure_ascii: If True, escape non-ASCII characters. If False (default), preserve them.
|
|
30
|
-
indent: Number of spaces for indentation
|
|
30
|
+
indent: Number of spaces for indentation. Defaults to None (minified).
|
|
31
31
|
|
|
32
32
|
Returns:
|
|
33
33
|
JSON string representation of the data
|
|
@@ -56,11 +56,11 @@ def summarize_pair(context: dict[str, Any]) -> list[Message]:
|
|
|
56
56
|
role='user',
|
|
57
57
|
content=f"""
|
|
58
58
|
Synthesize the information from the following two summaries into a single succinct summary.
|
|
59
|
-
|
|
59
|
+
|
|
60
60
|
IMPORTANT: Keep the summary concise and to the point. SUMMARIES MUST BE LESS THAN 250 CHARACTERS.
|
|
61
61
|
|
|
62
62
|
Summaries:
|
|
63
|
-
{to_prompt_json(context['node_summaries']
|
|
63
|
+
{to_prompt_json(context['node_summaries'])}
|
|
64
64
|
""",
|
|
65
65
|
),
|
|
66
66
|
]
|
|
@@ -77,28 +77,28 @@ def summarize_context(context: dict[str, Any]) -> list[Message]:
|
|
|
77
77
|
content=f"""
|
|
78
78
|
Given the MESSAGES and the ENTITY name, create a summary for the ENTITY. Your summary must only use
|
|
79
79
|
information from the provided MESSAGES. Your summary should also only contain information relevant to the
|
|
80
|
-
provided ENTITY.
|
|
81
|
-
|
|
80
|
+
provided ENTITY.
|
|
81
|
+
|
|
82
82
|
In addition, extract any values for the provided entity properties based on their descriptions.
|
|
83
83
|
If the value of the entity property cannot be found in the current context, set the value of the property to the Python value None.
|
|
84
|
-
|
|
84
|
+
|
|
85
85
|
{summary_instructions}
|
|
86
86
|
|
|
87
87
|
<MESSAGES>
|
|
88
|
-
{to_prompt_json(context['previous_episodes']
|
|
89
|
-
{to_prompt_json(context['episode_content']
|
|
88
|
+
{to_prompt_json(context['previous_episodes'])}
|
|
89
|
+
{to_prompt_json(context['episode_content'])}
|
|
90
90
|
</MESSAGES>
|
|
91
|
-
|
|
91
|
+
|
|
92
92
|
<ENTITY>
|
|
93
93
|
{context['node_name']}
|
|
94
94
|
</ENTITY>
|
|
95
|
-
|
|
95
|
+
|
|
96
96
|
<ENTITY CONTEXT>
|
|
97
97
|
{context['node_summary']}
|
|
98
98
|
</ENTITY CONTEXT>
|
|
99
|
-
|
|
99
|
+
|
|
100
100
|
<ATTRIBUTES>
|
|
101
|
-
{to_prompt_json(context['attributes']
|
|
101
|
+
{to_prompt_json(context['attributes'])}
|
|
102
102
|
</ATTRIBUTES>
|
|
103
103
|
""",
|
|
104
104
|
),
|
|
@@ -118,7 +118,7 @@ def summary_description(context: dict[str, Any]) -> list[Message]:
|
|
|
118
118
|
Summaries must be under 250 characters.
|
|
119
119
|
|
|
120
120
|
Summary:
|
|
121
|
-
{to_prompt_json(context['summary']
|
|
121
|
+
{to_prompt_json(context['summary'])}
|
|
122
122
|
""",
|
|
123
123
|
),
|
|
124
124
|
]
|
|
@@ -56,16 +56,16 @@ def search_results_to_context_string(search_results: SearchResults) -> str:
|
|
|
56
56
|
These are the most relevant facts and their valid and invalid dates. Facts are considered valid
|
|
57
57
|
between their valid_at and invalid_at dates. Facts with an invalid_at date of "Present" are considered valid.
|
|
58
58
|
<FACTS>
|
|
59
|
-
{to_prompt_json(fact_json
|
|
59
|
+
{to_prompt_json(fact_json)}
|
|
60
60
|
</FACTS>
|
|
61
61
|
<ENTITIES>
|
|
62
|
-
{to_prompt_json(entity_json
|
|
62
|
+
{to_prompt_json(entity_json)}
|
|
63
63
|
</ENTITIES>
|
|
64
64
|
<EPISODES>
|
|
65
|
-
{to_prompt_json(episode_json
|
|
65
|
+
{to_prompt_json(episode_json)}
|
|
66
66
|
</EPISODES>
|
|
67
67
|
<COMMUNITIES>
|
|
68
|
-
{to_prompt_json(community_json
|
|
68
|
+
{to_prompt_json(community_json)}
|
|
69
69
|
</COMMUNITIES>
|
|
70
70
|
"""
|
|
71
71
|
|
graphiti_core/tracer.py
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
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
|
+
from abc import ABC, abstractmethod
|
|
18
|
+
from collections.abc import Generator
|
|
19
|
+
from contextlib import AbstractContextManager, contextmanager, suppress
|
|
20
|
+
from typing import TYPE_CHECKING, Any
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
from opentelemetry.trace import Span, StatusCode
|
|
24
|
+
|
|
25
|
+
try:
|
|
26
|
+
from opentelemetry.trace import Span, StatusCode
|
|
27
|
+
|
|
28
|
+
OTEL_AVAILABLE = True
|
|
29
|
+
except ImportError:
|
|
30
|
+
OTEL_AVAILABLE = False
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class TracerSpan(ABC):
|
|
34
|
+
"""Abstract base class for tracer spans."""
|
|
35
|
+
|
|
36
|
+
@abstractmethod
|
|
37
|
+
def add_attributes(self, attributes: dict[str, Any]) -> None:
|
|
38
|
+
"""Add attributes to the span."""
|
|
39
|
+
pass
|
|
40
|
+
|
|
41
|
+
@abstractmethod
|
|
42
|
+
def set_status(self, status: str, description: str | None = None) -> None:
|
|
43
|
+
"""Set the status of the span."""
|
|
44
|
+
pass
|
|
45
|
+
|
|
46
|
+
@abstractmethod
|
|
47
|
+
def record_exception(self, exception: Exception) -> None:
|
|
48
|
+
"""Record an exception in the span."""
|
|
49
|
+
pass
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class Tracer(ABC):
|
|
53
|
+
"""Abstract base class for tracers."""
|
|
54
|
+
|
|
55
|
+
@abstractmethod
|
|
56
|
+
def start_span(self, name: str) -> AbstractContextManager[TracerSpan]:
|
|
57
|
+
"""Start a new span with the given name."""
|
|
58
|
+
pass
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class NoOpSpan(TracerSpan):
|
|
62
|
+
"""No-op span implementation that does nothing."""
|
|
63
|
+
|
|
64
|
+
def add_attributes(self, attributes: dict[str, Any]) -> None:
|
|
65
|
+
pass
|
|
66
|
+
|
|
67
|
+
def set_status(self, status: str, description: str | None = None) -> None:
|
|
68
|
+
pass
|
|
69
|
+
|
|
70
|
+
def record_exception(self, exception: Exception) -> None:
|
|
71
|
+
pass
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class NoOpTracer(Tracer):
|
|
75
|
+
"""No-op tracer implementation that does nothing."""
|
|
76
|
+
|
|
77
|
+
@contextmanager
|
|
78
|
+
def start_span(self, name: str) -> Generator[NoOpSpan, None, None]:
|
|
79
|
+
"""Return a no-op span."""
|
|
80
|
+
yield NoOpSpan()
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class OpenTelemetrySpan(TracerSpan):
|
|
84
|
+
"""Wrapper for OpenTelemetry span."""
|
|
85
|
+
|
|
86
|
+
def __init__(self, span: 'Span'):
|
|
87
|
+
self._span = span
|
|
88
|
+
|
|
89
|
+
def add_attributes(self, attributes: dict[str, Any]) -> None:
|
|
90
|
+
"""Add attributes to the OpenTelemetry span."""
|
|
91
|
+
try:
|
|
92
|
+
# Filter out None values and convert all values to appropriate types
|
|
93
|
+
filtered_attrs = {}
|
|
94
|
+
for key, value in attributes.items():
|
|
95
|
+
if value is not None:
|
|
96
|
+
# Convert to string if not a primitive type
|
|
97
|
+
if isinstance(value, str | int | float | bool):
|
|
98
|
+
filtered_attrs[key] = value
|
|
99
|
+
else:
|
|
100
|
+
filtered_attrs[key] = str(value)
|
|
101
|
+
|
|
102
|
+
if filtered_attrs:
|
|
103
|
+
self._span.set_attributes(filtered_attrs)
|
|
104
|
+
except Exception:
|
|
105
|
+
# Silently ignore tracing errors
|
|
106
|
+
pass
|
|
107
|
+
|
|
108
|
+
def set_status(self, status: str, description: str | None = None) -> None:
|
|
109
|
+
"""Set the status of the OpenTelemetry span."""
|
|
110
|
+
try:
|
|
111
|
+
if OTEL_AVAILABLE:
|
|
112
|
+
if status == 'error':
|
|
113
|
+
self._span.set_status(StatusCode.ERROR, description)
|
|
114
|
+
elif status == 'ok':
|
|
115
|
+
self._span.set_status(StatusCode.OK, description)
|
|
116
|
+
except Exception:
|
|
117
|
+
# Silently ignore tracing errors
|
|
118
|
+
pass
|
|
119
|
+
|
|
120
|
+
def record_exception(self, exception: Exception) -> None:
|
|
121
|
+
"""Record an exception in the OpenTelemetry span."""
|
|
122
|
+
with suppress(Exception):
|
|
123
|
+
self._span.record_exception(exception)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class OpenTelemetryTracer(Tracer):
|
|
127
|
+
"""Wrapper for OpenTelemetry tracer with configurable span name prefix."""
|
|
128
|
+
|
|
129
|
+
def __init__(self, tracer: Any, span_prefix: str = 'graphiti'):
|
|
130
|
+
"""
|
|
131
|
+
Initialize the OpenTelemetry tracer wrapper.
|
|
132
|
+
|
|
133
|
+
Parameters
|
|
134
|
+
----------
|
|
135
|
+
tracer : opentelemetry.trace.Tracer
|
|
136
|
+
The OpenTelemetry tracer instance.
|
|
137
|
+
span_prefix : str, optional
|
|
138
|
+
Prefix to prepend to all span names. Defaults to 'graphiti'.
|
|
139
|
+
"""
|
|
140
|
+
if not OTEL_AVAILABLE:
|
|
141
|
+
raise ImportError(
|
|
142
|
+
'OpenTelemetry is not installed. Install it with: pip install opentelemetry-api'
|
|
143
|
+
)
|
|
144
|
+
self._tracer = tracer
|
|
145
|
+
self._span_prefix = span_prefix.rstrip('.')
|
|
146
|
+
|
|
147
|
+
@contextmanager
|
|
148
|
+
def start_span(self, name: str) -> Generator[OpenTelemetrySpan | NoOpSpan, None, None]:
|
|
149
|
+
"""Start a new OpenTelemetry span with the configured prefix."""
|
|
150
|
+
try:
|
|
151
|
+
full_name = f'{self._span_prefix}.{name}'
|
|
152
|
+
with self._tracer.start_as_current_span(full_name) as span:
|
|
153
|
+
yield OpenTelemetrySpan(span)
|
|
154
|
+
except Exception:
|
|
155
|
+
# If tracing fails, yield a no-op span to prevent breaking the operation
|
|
156
|
+
yield NoOpSpan()
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def create_tracer(otel_tracer: Any | None = None, span_prefix: str = 'graphiti') -> Tracer:
|
|
160
|
+
"""
|
|
161
|
+
Create a tracer instance.
|
|
162
|
+
|
|
163
|
+
Parameters
|
|
164
|
+
----------
|
|
165
|
+
otel_tracer : opentelemetry.trace.Tracer | None, optional
|
|
166
|
+
An OpenTelemetry tracer instance. If None, a no-op tracer is returned.
|
|
167
|
+
span_prefix : str, optional
|
|
168
|
+
Prefix to prepend to all span names. Defaults to 'graphiti'.
|
|
169
|
+
|
|
170
|
+
Returns
|
|
171
|
+
-------
|
|
172
|
+
Tracer
|
|
173
|
+
A tracer instance (either OpenTelemetryTracer or NoOpTracer).
|
|
174
|
+
|
|
175
|
+
Examples
|
|
176
|
+
--------
|
|
177
|
+
Using with OpenTelemetry:
|
|
178
|
+
|
|
179
|
+
>>> from opentelemetry import trace
|
|
180
|
+
>>> otel_tracer = trace.get_tracer(__name__)
|
|
181
|
+
>>> tracer = create_tracer(otel_tracer, span_prefix='myapp.graphiti')
|
|
182
|
+
|
|
183
|
+
Using no-op tracer:
|
|
184
|
+
|
|
185
|
+
>>> tracer = create_tracer() # Returns NoOpTracer
|
|
186
|
+
"""
|
|
187
|
+
if otel_tracer is None:
|
|
188
|
+
return NoOpTracer()
|
|
189
|
+
|
|
190
|
+
if not OTEL_AVAILABLE:
|
|
191
|
+
return NoOpTracer()
|
|
192
|
+
|
|
193
|
+
return OpenTelemetryTracer(otel_tracer, span_prefix)
|
|
@@ -138,7 +138,9 @@ async def summarize_pair(llm_client: LLMClient, summary_pair: tuple[str, str]) -
|
|
|
138
138
|
}
|
|
139
139
|
|
|
140
140
|
llm_response = await llm_client.generate_response(
|
|
141
|
-
prompt_library.summarize_nodes.summarize_pair(context),
|
|
141
|
+
prompt_library.summarize_nodes.summarize_pair(context),
|
|
142
|
+
response_model=Summary,
|
|
143
|
+
prompt_name='summarize_nodes.summarize_pair',
|
|
142
144
|
)
|
|
143
145
|
|
|
144
146
|
pair_summary = llm_response.get('summary', '')
|
|
@@ -154,6 +156,7 @@ async def generate_summary_description(llm_client: LLMClient, summary: str) -> s
|
|
|
154
156
|
llm_response = await llm_client.generate_response(
|
|
155
157
|
prompt_library.summarize_nodes.summary_description(context),
|
|
156
158
|
response_model=SummaryDescription,
|
|
159
|
+
prompt_name='summarize_nodes.summary_description',
|
|
157
160
|
)
|
|
158
161
|
|
|
159
162
|
description = llm_response.get('description', '')
|
|
@@ -140,6 +140,7 @@ async def extract_edges(
|
|
|
140
140
|
response_model=ExtractedEdges,
|
|
141
141
|
max_tokens=extract_edges_max_tokens,
|
|
142
142
|
group_id=group_id,
|
|
143
|
+
prompt_name='extract_edges.edge',
|
|
143
144
|
)
|
|
144
145
|
edges_data = ExtractedEdges(**llm_response).edges
|
|
145
146
|
|
|
@@ -152,6 +153,7 @@ async def extract_edges(
|
|
|
152
153
|
response_model=MissingFacts,
|
|
153
154
|
max_tokens=extract_edges_max_tokens,
|
|
154
155
|
group_id=group_id,
|
|
156
|
+
prompt_name='extract_edges.reflexion',
|
|
155
157
|
)
|
|
156
158
|
|
|
157
159
|
missing_facts = reflexion_response.get('missing_facts', [])
|
|
@@ -526,6 +528,7 @@ async def resolve_extracted_edge(
|
|
|
526
528
|
prompt_library.dedupe_edges.resolve_edge(context),
|
|
527
529
|
response_model=EdgeDuplicate,
|
|
528
530
|
model_size=ModelSize.small,
|
|
531
|
+
prompt_name='dedupe_edges.resolve_edge',
|
|
529
532
|
)
|
|
530
533
|
response_object = EdgeDuplicate(**llm_response)
|
|
531
534
|
duplicate_facts = response_object.duplicate_facts
|
|
@@ -589,6 +592,7 @@ async def resolve_extracted_edge(
|
|
|
589
592
|
prompt_library.extract_edges.extract_attributes(edge_attributes_context),
|
|
590
593
|
response_model=edge_model, # type: ignore
|
|
591
594
|
model_size=ModelSize.small,
|
|
595
|
+
prompt_name='extract_edges.extract_attributes',
|
|
592
596
|
)
|
|
593
597
|
|
|
594
598
|
resolved_edge.attributes = edge_attributes_response
|
|
@@ -78,6 +78,7 @@ async def extract_nodes_reflexion(
|
|
|
78
78
|
prompt_library.extract_nodes.reflexion(context),
|
|
79
79
|
MissedEntities,
|
|
80
80
|
group_id=group_id,
|
|
81
|
+
prompt_name='extract_nodes.reflexion',
|
|
81
82
|
)
|
|
82
83
|
missed_entities = llm_response.get('missed_entities', [])
|
|
83
84
|
|
|
@@ -134,18 +135,21 @@ async def extract_nodes(
|
|
|
134
135
|
prompt_library.extract_nodes.extract_message(context),
|
|
135
136
|
response_model=ExtractedEntities,
|
|
136
137
|
group_id=episode.group_id,
|
|
138
|
+
prompt_name='extract_nodes.extract_message',
|
|
137
139
|
)
|
|
138
140
|
elif episode.source == EpisodeType.text:
|
|
139
141
|
llm_response = await llm_client.generate_response(
|
|
140
142
|
prompt_library.extract_nodes.extract_text(context),
|
|
141
143
|
response_model=ExtractedEntities,
|
|
142
144
|
group_id=episode.group_id,
|
|
145
|
+
prompt_name='extract_nodes.extract_text',
|
|
143
146
|
)
|
|
144
147
|
elif episode.source == EpisodeType.json:
|
|
145
148
|
llm_response = await llm_client.generate_response(
|
|
146
149
|
prompt_library.extract_nodes.extract_json(context),
|
|
147
150
|
response_model=ExtractedEntities,
|
|
148
151
|
group_id=episode.group_id,
|
|
152
|
+
prompt_name='extract_nodes.extract_json',
|
|
149
153
|
)
|
|
150
154
|
|
|
151
155
|
response_object = ExtractedEntities(**llm_response)
|
|
@@ -318,6 +322,7 @@ async def _resolve_with_llm(
|
|
|
318
322
|
llm_response = await llm_client.generate_response(
|
|
319
323
|
prompt_library.dedupe_nodes.nodes(context),
|
|
320
324
|
response_model=NodeResolutions,
|
|
325
|
+
prompt_name='dedupe_nodes.nodes',
|
|
321
326
|
)
|
|
322
327
|
|
|
323
328
|
node_resolutions: list[NodeDuplicate] = NodeResolutions(**llm_response).entity_resolutions
|
|
@@ -527,6 +532,7 @@ async def _extract_entity_attributes(
|
|
|
527
532
|
response_model=entity_type,
|
|
528
533
|
model_size=ModelSize.small,
|
|
529
534
|
group_id=node.group_id,
|
|
535
|
+
prompt_name='extract_nodes.extract_attributes',
|
|
530
536
|
)
|
|
531
537
|
|
|
532
538
|
# validate response
|
|
@@ -561,6 +567,7 @@ async def _extract_entity_summary(
|
|
|
561
567
|
response_model=EntitySummary,
|
|
562
568
|
model_size=ModelSize.small,
|
|
563
569
|
group_id=node.group_id,
|
|
570
|
+
prompt_name='extract_nodes.extract_summary',
|
|
564
571
|
)
|
|
565
572
|
|
|
566
573
|
node.summary = truncate_at_sentence(summary_response.get('summary', ''), MAX_SUMMARY_CHARS)
|
|
@@ -43,7 +43,9 @@ async def extract_edge_dates(
|
|
|
43
43
|
'reference_timestamp': current_episode.valid_at.isoformat(),
|
|
44
44
|
}
|
|
45
45
|
llm_response = await llm_client.generate_response(
|
|
46
|
-
prompt_library.extract_edge_dates.v1(context),
|
|
46
|
+
prompt_library.extract_edge_dates.v1(context),
|
|
47
|
+
response_model=EdgeDates,
|
|
48
|
+
prompt_name='extract_edge_dates.v1',
|
|
47
49
|
)
|
|
48
50
|
|
|
49
51
|
valid_at = llm_response.get('valid_at')
|
|
@@ -90,6 +92,7 @@ async def get_edge_contradictions(
|
|
|
90
92
|
prompt_library.invalidate_edges.v2(context),
|
|
91
93
|
response_model=InvalidatedEdges,
|
|
92
94
|
model_size=ModelSize.small,
|
|
95
|
+
prompt_name='invalidate_edges.v2',
|
|
93
96
|
)
|
|
94
97
|
|
|
95
98
|
contradicted_facts: list[int] = llm_response.get('contradicted_facts', [])
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: graphiti-core
|
|
3
|
-
Version: 0.22.
|
|
3
|
+
Version: 0.22.0rc5
|
|
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
|
|
@@ -34,6 +34,7 @@ Requires-Dist: langchain-openai>=0.2.6; extra == 'dev'
|
|
|
34
34
|
Requires-Dist: langgraph>=0.2.15; extra == 'dev'
|
|
35
35
|
Requires-Dist: langsmith>=0.1.108; extra == 'dev'
|
|
36
36
|
Requires-Dist: opensearch-py>=3.0.0; extra == 'dev'
|
|
37
|
+
Requires-Dist: opentelemetry-sdk>=1.20.0; extra == 'dev'
|
|
37
38
|
Requires-Dist: pyright>=1.1.404; extra == 'dev'
|
|
38
39
|
Requires-Dist: pytest-asyncio>=0.24.0; extra == 'dev'
|
|
39
40
|
Requires-Dist: pytest-xdist>=3.6.1; extra == 'dev'
|
|
@@ -59,6 +60,9 @@ Requires-Dist: langchain-aws>=0.2.29; extra == 'neptune'
|
|
|
59
60
|
Requires-Dist: opensearch-py>=3.0.0; extra == 'neptune'
|
|
60
61
|
Provides-Extra: sentence-transformers
|
|
61
62
|
Requires-Dist: sentence-transformers>=3.2.1; extra == 'sentence-transformers'
|
|
63
|
+
Provides-Extra: tracing
|
|
64
|
+
Requires-Dist: opentelemetry-api>=1.20.0; extra == 'tracing'
|
|
65
|
+
Requires-Dist: opentelemetry-sdk>=1.20.0; extra == 'tracing'
|
|
62
66
|
Provides-Extra: voyageai
|
|
63
67
|
Requires-Dist: voyageai>=0.2.3; extra == 'voyageai'
|
|
64
68
|
Description-Content-Type: text/markdown
|