diff --git a/examples/langgraph-agent/agent.ipynb b/examples/langgraph-agent/agent.ipynb index 688c25ce..6d6dd3bb 100644 --- a/examples/langgraph-agent/agent.ipynb +++ b/examples/langgraph-agent/agent.ipynb @@ -124,7 +124,6 @@ "from graphiti_core import Graphiti\n", "from graphiti_core.edges import EntityEdge\n", "from graphiti_core.nodes import EpisodeType\n", - "from graphiti_core.utils.bulk_utils import RawEpisode\n", "from graphiti_core.utils.maintenance.graph_data_operations import clear_data\n", "\n", "neo4j_uri = os.environ.get('NEO4J_URI', 'bolt://localhost:7687')\n", @@ -185,18 +184,14 @@ " with open(json_file_path) as file:\n", " products = json.load(file)['products']\n", "\n", - " episodes: list[RawEpisode] = [\n", - " RawEpisode(\n", + " for i, product in enumerate(products):\n", + " await client.add_episode(\n", " name=product.get('title', f'Product {i}'),\n", - " content=str({k: v for k, v in product.items() if k != 'images'}),\n", + " episode_body=str({k: v for k, v in product.items() if k != 'images'}),\n", " source_description='ManyBirds products',\n", " source=EpisodeType.json,\n", " reference_time=datetime.now(timezone.utc),\n", " )\n", - " for i, product in enumerate(products)\n", - " ]\n", - "\n", - " await client.add_episode_bulk(episodes)\n", "\n", "\n", "await ingest_products_data(client)" @@ -213,11 +208,11 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 8, "metadata": {}, "outputs": [], "source": [ - "from graphiti_core.search.search_config_recipes import NODE_HYBRID_SEARCH_RRF\n", + "from graphiti_core.search.search_config_recipes import NODE_HYBRID_SEARCH_EPISODE_MENTIONS\n", "\n", "user_name = 'jess'\n", "\n", @@ -230,18 +225,18 @@ ")\n", "\n", "# let's get Jess's node uuid\n", - "nl = await client._search(user_name, NODE_HYBRID_SEARCH_RRF)\n", + "nl = await client._search(user_name, NODE_HYBRID_SEARCH_EPISODE_MENTIONS)\n", "\n", - "user_node_uuid = nl[0].uuid\n", + "user_node_uuid = nl.nodes[0].uuid\n", "\n", "# and the ManyBirds node uuid\n", - "nl = await client._search('ManyBirds', NODE_HYBRID_SEARCH_RRF)\n", - "manybirds_node_uuid = nl[0].uuid" + "nl = await client._search('ManyBirds', NODE_HYBRID_SEARCH_EPISODE_MENTIONS)\n", + "manybirds_node_uuid = nl.nodes[0].uuid" ] }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 9, "metadata": {}, "outputs": [], "source": [ @@ -251,7 +246,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 10, "metadata": {}, "outputs": [], "source": [ @@ -274,7 +269,7 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 17, "metadata": {}, "outputs": [], "source": [ @@ -295,7 +290,7 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 18, "metadata": {}, "outputs": [], "source": [ @@ -304,16 +299,16 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 19, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "{'messages': [ToolMessage(content=\"-The product 'Men's SuperLight Wool Runners - Dark Grey (Medium Grey Sole)' is made of Wool.\\n- Women's Tree Breezers Knit - Rugged Beige (Hazy Beige Sole) has sizing options related to women's move shoes half sizes.\\n- TinyBirds Wool Runners - Little Kids - Natural Black (Blizzard Sole) is a type of Shoes.\\n- The product 'Men's SuperLight Wool Runners - Dark Grey (Medium Grey Sole)' belongs to the category Shoes.\\n- The product 'Men's SuperLight Wool Runners - Dark Grey (Medium Grey Sole)' uses SuperLight Foam technology.\\n- TinyBirds Wool Runners - Little Kids - Natural Black (Blizzard Sole) is sold by Manybirds.\\n- Jess is interested in buying a pair of shoes.\\n- TinyBirds Wool Runners - Little Kids - Natural Black (Blizzard Sole) has the handle TinyBirds-wool-runners-little-kids.\\n- ManyBirds Men's Couriers are a type of Shoes.\\n- Women's Tree Breezers Knit - Rugged Beige (Hazy Beige Sole) belongs to the Shoes category.\", name='get_shoe_data', tool_call_id='call_EPpOpD75rdq9jKRBUsfRnfxx')]}" + "{'messages': [ToolMessage(content=\"-jess is interested in buying a pair of Men's SuperLight Wool Runners - Dark Grey (Medium Grey Sole)\\n- jess is interested in buying a pair of shoes.\\n- jess is interested in buying a pair of shoes.\\n- jess is interested in buying a pair of TinyBirds Wool Runners - Little Kids - Natural Black (Blizzard Sole)\\n- jess is interested in buying a pair of shoes.\\n- jess is interested in buying a pair of Anytime No Show Sock - Rugged Beige\\n- jess is interested in buying a pair of Women's Tree Breezers Knit - Rugged Beige (Hazy Beige Sole)\\n- Women's Tree Breezers Knit - Rugged Beige (Hazy Beige Sole) has a SKU of A10938W050.\\n- jess is interested in buying a pair of Men's Couriers - Natural Black/Basin Blue (Blizzard Sole)\\n- Women's Tree Breezers Knit - Rugged Beige (Hazy Beige Sole) has a variant ID of 40832464322640.\", name='get_shoe_data', tool_call_id='call_De26KzGhmXJUcljY70TVwYFR')]}" ] }, - "execution_count": 12, + "execution_count": 19, "metadata": {}, "output_type": "execute_result" } @@ -342,7 +337,7 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": 24, "metadata": {}, "outputs": [], "source": [ @@ -417,7 +412,7 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": 25, "metadata": {}, "outputs": [], "source": [ @@ -457,12 +452,12 @@ }, { "cell_type": "code", - "execution_count": 15, + "execution_count": 26, "metadata": {}, "outputs": [ { "data": { - "image/jpeg": "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", + "image/jpeg": "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", "text/plain": [ "" ] @@ -487,19 +482,21 @@ }, { "cell_type": "code", - "execution_count": 16, + "execution_count": 27, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "{'messages': [HumanMessage(content='What sizes do the TinyBirds Wool Runners in Natural Black come in?', id='6a940637-70a0-4c95-a4d7-4c4846909747'),\n", - " AIMessage(content='The TinyBirds Wool Runners in Natural Black are available in the following sizes for little kids: 5T, 6T, 8T, 9T, and 10T. \\n\\nDo you have a specific size in mind, or are you looking for something else? Let me know your needs, and I can help you find the perfect pair!', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 76, 'prompt_tokens': 314, 'total_tokens': 390}, 'model_name': 'gpt-4o-mini-2024-07-18', 'system_fingerprint': 'fp_f33667828e', 'finish_reason': 'stop', 'logprobs': None}, id='run-d2f79c7f-4d41-4896-88dc-476a8e38bea8-0', usage_metadata={'input_tokens': 314, 'output_tokens': 76, 'total_tokens': 390})],\n", + "{'messages': [HumanMessage(content='What sizes do the TinyBirds Wool Runners in Natural Black come in?', additional_kwargs={}, response_metadata={}, id='3285b0ce-b976-4e8b-bf28-66ca66c36a92'),\n", + " AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_AVFKkkU9A6q5n5aEKBH1mHXR', 'function': {'arguments': '{\"query\":\"TinyBirds Wool Runners Natural Black\"}', 'name': 'get_shoe_data'}, 'type': 'function'}], 'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 23, 'prompt_tokens': 290, 'total_tokens': 313, 'completion_tokens_details': {'accepted_prediction_tokens': 0, 'audio_tokens': 0, 'reasoning_tokens': 0, 'rejected_prediction_tokens': 0}, 'prompt_tokens_details': {'audio_tokens': 0, 'cached_tokens': 0}}, 'model_name': 'gpt-4o-mini-2024-07-18', 'system_fingerprint': 'fp_0705bf87c0', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-48bec7e1-170e-4e07-a587-4bb31125e236-0', tool_calls=[{'name': 'get_shoe_data', 'args': {'query': 'TinyBirds Wool Runners Natural Black'}, 'id': 'call_AVFKkkU9A6q5n5aEKBH1mHXR', 'type': 'tool_call'}], usage_metadata={'input_tokens': 290, 'output_tokens': 23, 'total_tokens': 313, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}}),\n", + " ToolMessage(content=\"-jess is interested in buying a pair of TinyBirds Wool Runners - Little Kids - Natural Black (Blizzard Sole)\\n- jess is interested in buying a pair of Men's Couriers - Natural Black/Basin Blue (Blizzard Sole)\\n- jess is interested in buying a pair of Men's SuperLight Wool Runners - Dark Grey (Medium Grey Sole)\\n- Women's Tree Breezers Knit - Rugged Beige (Hazy Beige Sole) has a variant ID of 40832464322640.\\n- Women's Tree Breezers Knit - Rugged Beige (Hazy Beige Sole) has a SKU of A10938W050.\\n- Women's Tree Breezers Knit - Rugged Beige (Hazy Beige Sole) has a compare at price of None.\\n- jess is interested in buying a pair of Women's Tree Breezers Knit - Rugged Beige (Hazy Beige Sole)\\n- jess is interested in buying a pair of Anytime No Show Sock - Rugged Beige\\n- jess is interested in buying a pair of shoes.\\n- jess is interested in buying a pair of shoes.\", name='get_shoe_data', id='74d4d3c2-8b68-4969-95b1-5e42b27dc2dd', tool_call_id='call_AVFKkkU9A6q5n5aEKBH1mHXR'),\n", + " AIMessage(content=\"The TinyBirds Wool Runners in Natural Black are available in little kids' sizes. Could you please let me know what size you need? Also, do you have any other preferences like color or style?\", additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 43, 'prompt_tokens': 565, 'total_tokens': 608, 'completion_tokens_details': {'accepted_prediction_tokens': 0, 'audio_tokens': 0, 'reasoning_tokens': 0, 'rejected_prediction_tokens': 0}, 'prompt_tokens_details': {'audio_tokens': 0, 'cached_tokens': 0}}, 'model_name': 'gpt-4o-mini-2024-07-18', 'system_fingerprint': 'fp_0705bf87c0', 'finish_reason': 'stop', 'logprobs': None}, id='run-70106127-cb1d-46b4-8c7a-80bd3ac9e454-0', usage_metadata={'input_tokens': 565, 'output_tokens': 43, 'total_tokens': 608, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}})],\n", " 'user_name': 'jess',\n", - " 'user_node_uuid': '186a845eee4849619d1e625b178d1845'}" + " 'user_node_uuid': '1dd451d6-8305-47eb-b5c1-8bab799592f1'}" ] }, - "execution_count": 16, + "execution_count": 27, "metadata": {}, "output_type": "execute_result" } @@ -636,7 +633,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.12.4" + "version": "3.12.7" } }, "nbformat": 4, diff --git a/graphiti_core/cross_encoder/client.py b/graphiti_core/cross_encoder/client.py index 1664d79d..a7645ed1 100644 --- a/graphiti_core/cross_encoder/client.py +++ b/graphiti_core/cross_encoder/client.py @@ -34,7 +34,7 @@ class CrossEncoderClient(ABC): passages (list[str]): A list of passages to rank. Returns: - List[tuple[str, float]]: A list of tuples containing the passage and its score, + list[tuple[str, float]]: A list of tuples containing the passage and its score, sorted in descending order of relevance. """ pass diff --git a/graphiti_core/llm_client/anthropic_client.py b/graphiti_core/llm_client/anthropic_client.py index ec6e88ff..df99fb91 100644 --- a/graphiti_core/llm_client/anthropic_client.py +++ b/graphiti_core/llm_client/anthropic_client.py @@ -20,6 +20,7 @@ import typing import anthropic from anthropic import AsyncAnthropic +from pydantic import BaseModel from ..prompts.models import Message from .client import LLMClient @@ -46,7 +47,9 @@ class AnthropicClient(LLMClient): max_retries=1, ) - async def _generate_response(self, messages: list[Message]) -> dict[str, typing.Any]: + async def _generate_response( + self, messages: list[Message], response_model: type[BaseModel] | None = None + ) -> dict[str, typing.Any]: system_message = messages[0] user_messages = [{'role': m.role, 'content': m.content} for m in messages[1:]] + [ {'role': 'assistant', 'content': '{'} diff --git a/graphiti_core/llm_client/client.py b/graphiti_core/llm_client/client.py index 7886c7f8..22cc3795 100644 --- a/graphiti_core/llm_client/client.py +++ b/graphiti_core/llm_client/client.py @@ -22,6 +22,7 @@ from abc import ABC, abstractmethod import httpx from diskcache import Cache +from pydantic import BaseModel from tenacity import retry, retry_if_exception, stop_after_attempt, wait_random_exponential from ..prompts.models import Message @@ -66,14 +67,18 @@ class LLMClient(ABC): else None, reraise=True, ) - async def _generate_response_with_retry(self, messages: list[Message]) -> dict[str, typing.Any]: + async def _generate_response_with_retry( + self, messages: list[Message], response_model: type[BaseModel] | None = None + ) -> dict[str, typing.Any]: try: - return await self._generate_response(messages) + return await self._generate_response(messages, response_model) except (httpx.HTTPStatusError, RateLimitError) as e: raise e @abstractmethod - async def _generate_response(self, messages: list[Message]) -> dict[str, typing.Any]: + async def _generate_response( + self, messages: list[Message], response_model: type[BaseModel] | None = None + ) -> dict[str, typing.Any]: pass def _get_cache_key(self, messages: list[Message]) -> str: @@ -82,7 +87,17 @@ class LLMClient(ABC): key_str = f'{self.model}:{message_str}' return hashlib.md5(key_str.encode()).hexdigest() - async def generate_response(self, messages: list[Message]) -> dict[str, typing.Any]: + async def generate_response( + self, messages: list[Message], response_model: type[BaseModel] | None = None + ) -> dict[str, typing.Any]: + if response_model is not None: + serialized_model = json.dumps(response_model.model_json_schema()) + messages[ + -1 + ].content += ( + f'\n\nRespond with a JSON object in the following format:\n\n{serialized_model}' + ) + if self.cache_enabled: cache_key = self._get_cache_key(messages) @@ -91,7 +106,7 @@ class LLMClient(ABC): logger.debug(f'Cache hit for {cache_key}') return cached_response - response = await self._generate_response_with_retry(messages) + response = await self._generate_response_with_retry(messages, response_model) if self.cache_enabled: self.cache_dir.set(cache_key, response) diff --git a/graphiti_core/llm_client/errors.py b/graphiti_core/llm_client/errors.py index 0c0f5dd1..cd8c22a1 100644 --- a/graphiti_core/llm_client/errors.py +++ b/graphiti_core/llm_client/errors.py @@ -21,3 +21,11 @@ class RateLimitError(Exception): def __init__(self, message='Rate limit exceeded. Please try again later.'): self.message = message super().__init__(self.message) + + +class RefusalError(Exception): + """Exception raised when the LLM refuses to generate a response.""" + + def __init__(self, message: str): + self.message = message + super().__init__(self.message) diff --git a/graphiti_core/llm_client/groq_client.py b/graphiti_core/llm_client/groq_client.py index 9f59e621..45ccc3cb 100644 --- a/graphiti_core/llm_client/groq_client.py +++ b/graphiti_core/llm_client/groq_client.py @@ -21,6 +21,7 @@ import typing import groq from groq import AsyncGroq from groq.types.chat import ChatCompletionMessageParam +from pydantic import BaseModel from ..prompts.models import Message from .client import LLMClient @@ -43,7 +44,9 @@ class GroqClient(LLMClient): self.client = AsyncGroq(api_key=config.api_key) - async def _generate_response(self, messages: list[Message]) -> dict[str, typing.Any]: + async def _generate_response( + self, messages: list[Message], response_model: type[BaseModel] | None = None + ) -> dict[str, typing.Any]: msgs: list[ChatCompletionMessageParam] = [] for m in messages: if m.role == 'user': diff --git a/graphiti_core/llm_client/openai_client.py b/graphiti_core/llm_client/openai_client.py index 957317cc..c92b4fb3 100644 --- a/graphiti_core/llm_client/openai_client.py +++ b/graphiti_core/llm_client/openai_client.py @@ -14,18 +14,18 @@ See the License for the specific language governing permissions and limitations under the License. """ -import json import logging import typing import openai from openai import AsyncOpenAI from openai.types.chat import ChatCompletionMessageParam +from pydantic import BaseModel from ..prompts.models import Message from .client import LLMClient from .config import LLMConfig -from .errors import RateLimitError +from .errors import RateLimitError, RefusalError logger = logging.getLogger(__name__) @@ -65,6 +65,10 @@ class OpenAIClient(LLMClient): client (Any | None): An optional async client instance to use. If not provided, a new AsyncOpenAI client is created. """ + # removed caching to simplify the `generate_response` override + if cache: + raise NotImplementedError('Caching is not implemented for OpenAI') + if config is None: config = LLMConfig() @@ -75,7 +79,9 @@ class OpenAIClient(LLMClient): else: self.client = client - async def _generate_response(self, messages: list[Message]) -> dict[str, typing.Any]: + async def _generate_response( + self, messages: list[Message], response_model: type[BaseModel] | None = None + ) -> dict[str, typing.Any]: openai_messages: list[ChatCompletionMessageParam] = [] for m in messages: if m.role == 'user': @@ -83,17 +89,33 @@ class OpenAIClient(LLMClient): elif m.role == 'system': openai_messages.append({'role': 'system', 'content': m.content}) try: - response = await self.client.chat.completions.create( + response = await self.client.beta.chat.completions.parse( model=self.model or DEFAULT_MODEL, messages=openai_messages, temperature=self.temperature, max_tokens=self.max_tokens, - response_format={'type': 'json_object'}, + response_format=response_model, # type: ignore ) - result = response.choices[0].message.content or '' - return json.loads(result) + + response_object = response.choices[0].message + + if response_object.parsed: + return response_object.parsed.model_dump() + elif response_object.refusal: + raise RefusalError(response_object.refusal) + else: + raise Exception('No response from LLM') + except openai.LengthFinishReasonError as e: + raise Exception(f'Output length exceeded max tokens {self.max_tokens}: {e}') from e except openai.RateLimitError as e: raise RateLimitError from e except Exception as e: logger.error(f'Error in generating LLM response: {e}') raise + + async def generate_response( + self, messages: list[Message], response_model: type[BaseModel] | None = None + ) -> dict[str, typing.Any]: + response = await self._generate_response(messages, response_model) + + return response diff --git a/graphiti_core/prompts/dedupe_edges.py b/graphiti_core/prompts/dedupe_edges.py index edc8c9de..ee16f782 100644 --- a/graphiti_core/prompts/dedupe_edges.py +++ b/graphiti_core/prompts/dedupe_edges.py @@ -15,11 +15,30 @@ limitations under the License. """ import json -from typing import Any, Protocol, TypedDict +from typing import Any, Optional, Protocol, TypedDict + +from pydantic import BaseModel, Field from .models import Message, PromptFunction, PromptVersion +class EdgeDuplicate(BaseModel): + is_duplicate: bool = Field(..., description='true or false') + uuid: Optional[str] = Field( + None, + description="uuid of the existing edge like '5d643020624c42fa9de13f97b1b3fa39' or null", + ) + + +class UniqueFact(BaseModel): + uuid: str = Field(..., description='unique identifier of the fact') + fact: str = Field(..., description='fact of a unique edge') + + +class UniqueFacts(BaseModel): + unique_facts: list[UniqueFact] + + class Prompt(Protocol): edge: PromptVersion edge_list: PromptVersion @@ -56,12 +75,6 @@ def edge(context: dict[str, Any]) -> list[Message]: Guidelines: 1. The facts do not need to be completely identical to be duplicates, they just need to express the same information. - - Respond with a JSON object in the following format: - {{ - "is_duplicate": true or false, - "uuid": uuid of the existing edge like "5d643020624c42fa9de13f97b1b3fa39" or null, - }} """, ), ] @@ -90,16 +103,6 @@ def edge_list(context: dict[str, Any]) -> list[Message]: 3. Facts will often discuss the same or similar relation between identical entities 4. The final list should have only unique facts. If 3 facts are all duplicates of each other, only one of their facts should be in the response - - Respond with a JSON object in the following format: - {{ - "unique_facts": [ - {{ - "uuid": "unique identifier of the fact", - "fact": "fact of a unique edge" - }} - ] - }} """, ), ] diff --git a/graphiti_core/prompts/dedupe_nodes.py b/graphiti_core/prompts/dedupe_nodes.py index c9ede300..696b64a2 100644 --- a/graphiti_core/prompts/dedupe_nodes.py +++ b/graphiti_core/prompts/dedupe_nodes.py @@ -15,11 +15,25 @@ limitations under the License. """ import json -from typing import Any, Protocol, TypedDict +from typing import Any, Optional, Protocol, TypedDict + +from pydantic import BaseModel, Field from .models import Message, PromptFunction, PromptVersion +class NodeDuplicate(BaseModel): + is_duplicate: bool = Field(..., description='true or false') + uuid: Optional[str] = Field( + None, + description="uuid of the existing node like '5d643020624c42fa9de13f97b1b3fa39' or null", + ) + name: str = Field( + ..., + description="Updated name of the new node (use the best name between the new node's name, an existing duplicate name, or a combination of both)", + ) + + class Prompt(Protocol): node: PromptVersion node_list: PromptVersion diff --git a/graphiti_core/prompts/eval.py b/graphiti_core/prompts/eval.py index 258b868b..2e58309c 100644 --- a/graphiti_core/prompts/eval.py +++ b/graphiti_core/prompts/eval.py @@ -17,9 +17,26 @@ limitations under the License. import json from typing import Any, Protocol, TypedDict +from pydantic import BaseModel, Field + from .models import Message, PromptFunction, PromptVersion +class QueryExpansion(BaseModel): + query: str = Field(..., description='query optimized for database search') + + +class QAResponse(BaseModel): + ANSWER: str = Field(..., description='how Alice would answer the question') + + +class EvalResponse(BaseModel): + is_correct: bool = Field(..., description='boolean if the answer is correct or incorrect') + reasoning: str = Field( + ..., description='why you determined the response was correct or incorrect' + ) + + class Prompt(Protocol): qa_prompt: PromptVersion eval_prompt: PromptVersion @@ -41,10 +58,6 @@ def query_expansion(context: dict[str, Any]) -> list[Message]: {json.dumps(context['query'])} - respond with a JSON object in the following format: - {{ - "query": "query optimized for database search" - }} """ return [ Message(role='system', content=sys_prompt), @@ -67,10 +80,6 @@ def qa_prompt(context: dict[str, Any]) -> list[Message]: {context['query']} - respond with a JSON object in the following format: - {{ - "ANSWER": "how Alice would answer the question" - }} """ return [ Message(role='system', content=sys_prompt), @@ -96,12 +105,6 @@ def eval_prompt(context: dict[str, Any]) -> list[Message]: {context['response']} - - respond with a JSON object in the following format: - {{ - "is_correct": "boolean if the answer is correct or incorrect" - "reasoning": "why you determined the response was correct or incorrect" - }} """ return [ Message(role='system', content=sys_prompt), diff --git a/graphiti_core/prompts/extract_edge_dates.py b/graphiti_core/prompts/extract_edge_dates.py index a84d877e..a2c30ca7 100644 --- a/graphiti_core/prompts/extract_edge_dates.py +++ b/graphiti_core/prompts/extract_edge_dates.py @@ -14,11 +14,24 @@ See the License for the specific language governing permissions and limitations under the License. """ -from typing import Any, Protocol, TypedDict +from typing import Any, Optional, Protocol, TypedDict + +from pydantic import BaseModel, Field from .models import Message, PromptFunction, PromptVersion +class EdgeDates(BaseModel): + valid_at: Optional[str] = Field( + None, + description='The date and time when the relationship described by the edge fact became true or was established. YYYY-MM-DDTHH:MM:SS.SSSSSSZ or null.', + ) + invalid_at: Optional[str] = Field( + None, + description='The date and time when the relationship described by the edge fact stopped being true or ended. YYYY-MM-DDTHH:MM:SS.SSSSSSZ or null.', + ) + + class Prompt(Protocol): v1: PromptVersion @@ -60,7 +73,7 @@ def v1(context: dict[str, Any]) -> list[Message]: Analyze the conversation and determine if there are dates that are part of the edge fact. Only set dates if they explicitly relate to the formation or alteration of the relationship itself. Guidelines: - 1. Use ISO 8601 format (YYYY-MM-DDTHH:MM:SSZ) for datetimes. + 1. Use ISO 8601 format (YYYY-MM-DDTHH:MM:SS.SSSSSSZ) for datetimes. 2. Use the reference timestamp as the current time when determining the valid_at and invalid_at dates. 3. If the fact is written in the present tense, use the Reference Timestamp for the valid_at date 4. If no temporal information is found that establishes or changes the relationship, leave the fields as null. @@ -69,11 +82,6 @@ def v1(context: dict[str, Any]) -> list[Message]: 7. If only a date is mentioned without a specific time, use 00:00:00 (midnight) for that date. 8. If only year is mentioned, use January 1st of that year at 00:00:00. 9. Always include the time zone offset (use Z for UTC if no specific time zone is mentioned). - Respond with a JSON object: - {{ - "valid_at": "YYYY-MM-DDTHH:MM:SS.SSSSSSZ or null", - "invalid_at": "YYYY-MM-DDTHH:MM:SS.SSSSSSZ or null", - }} """, ), ] diff --git a/graphiti_core/prompts/extract_edges.py b/graphiti_core/prompts/extract_edges.py index 58e984f9..ceebb47f 100644 --- a/graphiti_core/prompts/extract_edges.py +++ b/graphiti_core/prompts/extract_edges.py @@ -17,9 +17,26 @@ limitations under the License. import json from typing import Any, Protocol, TypedDict +from pydantic import BaseModel, Field + from .models import Message, PromptFunction, PromptVersion +class Edge(BaseModel): + relation_type: str = Field(..., description='RELATION_TYPE_IN_CAPS') + source_entity_name: str = Field(..., description='name of the source entity') + target_entity_name: str = Field(..., description='name of the target entity') + fact: str = Field(..., description='extracted factual information') + + +class ExtractedEdges(BaseModel): + edges: list[Edge] + + +class MissingFacts(BaseModel): + missing_facts: list[str] = Field(..., description="facts that weren't extracted") + + class Prompt(Protocol): edge: PromptVersion reflexion: PromptVersion @@ -54,25 +71,12 @@ def edge(context: dict[str, Any]) -> list[Message]: Given the above MESSAGES and ENTITIES, extract all facts pertaining to the listed ENTITIES from the CURRENT MESSAGE. - Guidelines: 1. Extract facts only between the provided entities. 2. Each fact should represent a clear relationship between two DISTINCT nodes. 3. The relation_type should be a concise, all-caps description of the fact (e.g., LOVES, IS_FRIENDS_WITH, WORKS_FOR). 4. Provide a more detailed fact containing all relevant information. 5. Consider temporal aspects of relationships when relevant. - - Respond with a JSON object in the following format: - {{ - "edges": [ - {{ - "relation_type": "RELATION_TYPE_IN_CAPS", - "source_entity_name": "name of the source entity", - "target_entity_name": "name of the target entity", - "fact": "extracted factual information", - }} - ] - }} """, ), ] @@ -98,12 +102,7 @@ def reflexion(context: dict[str, Any]) -> list[Message]: Given the above MESSAGES, list of EXTRACTED ENTITIES entities, and list of EXTRACTED FACTS; -determine if any facts haven't been extracted: - -Respond with a JSON object in the following format: -{{ - "missing_facts": [ "facts that weren't extracted", ...] -}} +determine if any facts haven't been extracted. """ return [ Message(role='system', content=sys_prompt), diff --git a/graphiti_core/prompts/extract_nodes.py b/graphiti_core/prompts/extract_nodes.py index 9374c816..49e2036b 100644 --- a/graphiti_core/prompts/extract_nodes.py +++ b/graphiti_core/prompts/extract_nodes.py @@ -17,9 +17,19 @@ limitations under the License. import json from typing import Any, Protocol, TypedDict +from pydantic import BaseModel, Field + from .models import Message, PromptFunction, PromptVersion +class ExtractedNodes(BaseModel): + extracted_node_names: list[str] = Field(..., description='Name of the extracted entity') + + +class MissedEntities(BaseModel): + missed_entities: list[str] = Field(..., description="Names of entities that weren't extracted") + + class Prompt(Protocol): extract_message: PromptVersion extract_json: PromptVersion @@ -56,11 +66,6 @@ Guidelines: 4. DO NOT create nodes for temporal information like dates, times or years (these will be added to edges later). 5. Be as explicit as possible in your node names, using full names. 6. DO NOT extract entities mentioned only in PREVIOUS MESSAGES, those messages are only to provide context. - -Respond with a JSON object in the following format: -{{ - "extracted_node_names": ["Name of the extracted entity", ...], -}} """ return [ Message(role='system', content=sys_prompt), @@ -87,11 +92,6 @@ Given the above source description and JSON, extract relevant entity nodes from Guidelines: 1. Always try to extract an entities that the JSON represents. This will often be something like a "name" or "user field 2. Do NOT extract any properties that contain dates - -Respond with a JSON object in the following format: -{{ - "extracted_node_names": ["Name of the extracted entity", ...], -}} """ return [ Message(role='system', content=sys_prompt), @@ -116,11 +116,6 @@ Guidelines: 2. Avoid creating nodes for relationships or actions. 3. Avoid creating nodes for temporal information like dates, times or years (these will be added to edges later). 4. Be as explicit as possible in your node names, using full names and avoiding abbreviations. - -Respond with a JSON object in the following format: -{{ - "extracted_node_names": ["Name of the extracted entity", ...], -}} """ return [ Message(role='system', content=sys_prompt), @@ -144,12 +139,7 @@ def reflexion(context: dict[str, Any]) -> list[Message]: Given the above previous messages, current message, and list of extracted entities; determine if any entities haven't been -extracted: - -Respond with a JSON object in the following format: -{{ - "missed_entities": [ "name of entity that wasn't extracted", ...] -}} +extracted. """ return [ Message(role='system', content=sys_prompt), diff --git a/graphiti_core/prompts/invalidate_edges.py b/graphiti_core/prompts/invalidate_edges.py index d2246f89..16473963 100644 --- a/graphiti_core/prompts/invalidate_edges.py +++ b/graphiti_core/prompts/invalidate_edges.py @@ -16,9 +16,22 @@ limitations under the License. from typing import Any, Protocol, TypedDict +from pydantic import BaseModel, Field + from .models import Message, PromptFunction, PromptVersion +class InvalidatedEdge(BaseModel): + uuid: str = Field(..., description='The UUID of the edge to be invalidated') + fact: str = Field(..., description='Updated fact of the edge') + + +class InvalidatedEdges(BaseModel): + invalidated_edges: list[InvalidatedEdge] = Field( + ..., description='List of edges that should be invalidated' + ) + + class Prompt(Protocol): v1: PromptVersion v2: PromptVersion @@ -56,18 +69,6 @@ def v1(context: dict[str, Any]) -> list[Message]: {context['new_edges']} Each edge is formatted as: "UUID | SOURCE_NODE - EDGE_NAME - TARGET_NODE (fact: EDGE_FACT), START_DATE (END_DATE, optional))" - - For each existing edge that should be invalidated, respond with a JSON object in the following format: - {{ - "invalidated_edges": [ - {{ - "edge_uuid": "The UUID of the edge to be invalidated (the part before the | character)", - "fact": "Updated fact of the edge" - }} - ] - }} - - If no relationships need to be invalidated based on these strict criteria, return an empty list for "invalidated_edges". """, ), ] @@ -89,19 +90,6 @@ def v2(context: dict[str, Any]) -> list[Message]: New Edge: {context['new_edge']} - - - For each existing edge that should be invalidated, respond with a JSON object in the following format: - {{ - "invalidated_edges": [ - {{ - "uuid": "The UUID of the edge to be invalidated", - "fact": "Updated fact of the edge" - }} - ] - }} - - If no relationships need to be invalidated based on these strict criteria, return an empty list for "invalidated_edges". """, ), ] diff --git a/graphiti_core/prompts/summarize_nodes.py b/graphiti_core/prompts/summarize_nodes.py index 88bc5556..c3ff3e0d 100644 --- a/graphiti_core/prompts/summarize_nodes.py +++ b/graphiti_core/prompts/summarize_nodes.py @@ -17,9 +17,21 @@ limitations under the License. import json from typing import Any, Protocol, TypedDict +from pydantic import BaseModel, Field + from .models import Message, PromptFunction, PromptVersion +class Summary(BaseModel): + summary: str = Field( + ..., description='Summary containing the important information from both summaries' + ) + + +class SummaryDescription(BaseModel): + description: str = Field(..., description='One sentence description of the provided summary') + + class Prompt(Protocol): summarize_pair: PromptVersion summarize_context: PromptVersion @@ -45,11 +57,6 @@ def summarize_pair(context: dict[str, Any]) -> list[Message]: Summaries: {json.dumps(context['node_summaries'], indent=2)} - - Respond with a JSON object in the following format: - {{ - "summary": "Summary containing the important information from both summaries" - }} """, ), ] @@ -77,12 +84,6 @@ def summarize_context(context: dict[str, Any]) -> list[Message]: {context['node_name']} - - - Respond with a JSON object in the following format: - {{ - "summary": "Entity summary" - }} """, ), ] @@ -101,11 +102,6 @@ def summary_description(context: dict[str, Any]) -> list[Message]: Summary: {json.dumps(context['summary'], indent=2)} - - Respond with a JSON object in the following format: - {{ - "description": "One sentence description of the provided summary" - }} """, ), ] diff --git a/graphiti_core/utils/maintenance/community_operations.py b/graphiti_core/utils/maintenance/community_operations.py index 805ebe12..fc71f707 100644 --- a/graphiti_core/utils/maintenance/community_operations.py +++ b/graphiti_core/utils/maintenance/community_operations.py @@ -16,6 +16,7 @@ from graphiti_core.nodes import ( get_community_node_from_record, ) from graphiti_core.prompts import prompt_library +from graphiti_core.prompts.summarize_nodes import Summary, SummaryDescription from graphiti_core.utils.maintenance.edge_operations import build_community_edges MAX_COMMUNITY_BUILD_CONCURRENCY = 10 @@ -131,7 +132,7 @@ async def summarize_pair(llm_client: LLMClient, summary_pair: tuple[str, str]) - context = {'node_summaries': [{'summary': summary} for summary in summary_pair]} llm_response = await llm_client.generate_response( - prompt_library.summarize_nodes.summarize_pair(context) + prompt_library.summarize_nodes.summarize_pair(context), response_model=Summary ) pair_summary = llm_response.get('summary', '') @@ -143,7 +144,8 @@ async def generate_summary_description(llm_client: LLMClient, summary: str) -> s context = {'summary': summary} llm_response = await llm_client.generate_response( - prompt_library.summarize_nodes.summary_description(context) + prompt_library.summarize_nodes.summary_description(context), + response_model=SummaryDescription, ) description = llm_response.get('description', '') diff --git a/graphiti_core/utils/maintenance/edge_operations.py b/graphiti_core/utils/maintenance/edge_operations.py index 90f38a65..1279cf14 100644 --- a/graphiti_core/utils/maintenance/edge_operations.py +++ b/graphiti_core/utils/maintenance/edge_operations.py @@ -24,6 +24,8 @@ from graphiti_core.helpers import MAX_REFLEXION_ITERATIONS from graphiti_core.llm_client import LLMClient from graphiti_core.nodes import CommunityNode, EntityNode, EpisodicNode from graphiti_core.prompts import prompt_library +from graphiti_core.prompts.dedupe_edges import EdgeDuplicate, UniqueFacts +from graphiti_core.prompts.extract_edges import ExtractedEdges, MissingFacts from graphiti_core.utils.maintenance.temporal_operations import ( extract_edge_dates, get_edge_contradictions, @@ -91,7 +93,7 @@ async def extract_edges( reflexion_iterations = 0 while facts_missed and reflexion_iterations < MAX_REFLEXION_ITERATIONS: llm_response = await llm_client.generate_response( - prompt_library.extract_edges.edge(context) + prompt_library.extract_edges.edge(context), response_model=ExtractedEdges ) edges_data = llm_response.get('edges', []) @@ -100,7 +102,7 @@ async def extract_edges( reflexion_iterations += 1 if reflexion_iterations < MAX_REFLEXION_ITERATIONS: reflexion_response = await llm_client.generate_response( - prompt_library.extract_edges.reflexion(context) + prompt_library.extract_edges.reflexion(context), response_model=MissingFacts ) missing_facts = reflexion_response.get('missing_facts', []) @@ -317,7 +319,9 @@ async def dedupe_extracted_edge( 'extracted_edges': extracted_edge_context, } - llm_response = await llm_client.generate_response(prompt_library.dedupe_edges.edge(context)) + llm_response = await llm_client.generate_response( + prompt_library.dedupe_edges.edge(context), response_model=EdgeDuplicate + ) is_duplicate: bool = llm_response.get('is_duplicate', False) uuid: str | None = llm_response.get('uuid', None) @@ -352,7 +356,7 @@ async def dedupe_edge_list( context = {'edges': [{'uuid': edge.uuid, 'fact': edge.fact} for edge in edges]} llm_response = await llm_client.generate_response( - prompt_library.dedupe_edges.edge_list(context) + prompt_library.dedupe_edges.edge_list(context), response_model=UniqueFacts ) unique_edges_data = llm_response.get('unique_facts', []) diff --git a/graphiti_core/utils/maintenance/node_operations.py b/graphiti_core/utils/maintenance/node_operations.py index 7fca5d49..08835023 100644 --- a/graphiti_core/utils/maintenance/node_operations.py +++ b/graphiti_core/utils/maintenance/node_operations.py @@ -23,6 +23,9 @@ from graphiti_core.helpers import MAX_REFLEXION_ITERATIONS from graphiti_core.llm_client import LLMClient from graphiti_core.nodes import EntityNode, EpisodeType, EpisodicNode from graphiti_core.prompts import prompt_library +from graphiti_core.prompts.dedupe_nodes import NodeDuplicate +from graphiti_core.prompts.extract_nodes import ExtractedNodes, MissedEntities +from graphiti_core.prompts.summarize_nodes import Summary logger = logging.getLogger(__name__) @@ -42,7 +45,7 @@ async def extract_message_nodes( } llm_response = await llm_client.generate_response( - prompt_library.extract_nodes.extract_message(context) + prompt_library.extract_nodes.extract_message(context), response_model=ExtractedNodes ) extracted_node_names = llm_response.get('extracted_node_names', []) return extracted_node_names @@ -63,7 +66,7 @@ async def extract_text_nodes( } llm_response = await llm_client.generate_response( - prompt_library.extract_nodes.extract_text(context) + prompt_library.extract_nodes.extract_text(context), ExtractedNodes ) extracted_node_names = llm_response.get('extracted_node_names', []) return extracted_node_names @@ -81,7 +84,7 @@ async def extract_json_nodes( } llm_response = await llm_client.generate_response( - prompt_library.extract_nodes.extract_json(context) + prompt_library.extract_nodes.extract_json(context), ExtractedNodes ) extracted_node_names = llm_response.get('extracted_node_names', []) return extracted_node_names @@ -101,7 +104,7 @@ async def extract_nodes_reflexion( } llm_response = await llm_client.generate_response( - prompt_library.extract_nodes.reflexion(context) + prompt_library.extract_nodes.reflexion(context), MissedEntities ) missed_entities = llm_response.get('missed_entities', []) @@ -273,9 +276,12 @@ async def resolve_extracted_node( } llm_response, node_summary_response = await asyncio.gather( - llm_client.generate_response(prompt_library.dedupe_nodes.node(context)), llm_client.generate_response( - prompt_library.summarize_nodes.summarize_context(summary_context) + prompt_library.dedupe_nodes.node(context), response_model=NodeDuplicate + ), + llm_client.generate_response( + prompt_library.summarize_nodes.summarize_context(summary_context), + response_model=Summary, ), ) @@ -294,7 +300,8 @@ async def resolve_extracted_node( summary_response = await llm_client.generate_response( prompt_library.summarize_nodes.summarize_pair( {'node_summaries': [extracted_node.summary, existing_node.summary]} - ) + ), + response_model=Summary, ) node = existing_node node.name = name diff --git a/graphiti_core/utils/maintenance/temporal_operations.py b/graphiti_core/utils/maintenance/temporal_operations.py index c95e4bb0..6028ecfb 100644 --- a/graphiti_core/utils/maintenance/temporal_operations.py +++ b/graphiti_core/utils/maintenance/temporal_operations.py @@ -22,6 +22,8 @@ from graphiti_core.edges import EntityEdge from graphiti_core.llm_client import LLMClient from graphiti_core.nodes import EpisodicNode from graphiti_core.prompts import prompt_library +from graphiti_core.prompts.extract_edge_dates import EdgeDates +from graphiti_core.prompts.invalidate_edges import InvalidatedEdges logger = logging.getLogger(__name__) @@ -38,7 +40,9 @@ async def extract_edge_dates( 'previous_episodes': [ep.content for ep in previous_episodes], 'reference_timestamp': current_episode.valid_at.isoformat(), } - llm_response = await llm_client.generate_response(prompt_library.extract_edge_dates.v1(context)) + llm_response = await llm_client.generate_response( + prompt_library.extract_edge_dates.v1(context), response_model=EdgeDates + ) valid_at = llm_response.get('valid_at') invalid_at = llm_response.get('invalid_at') @@ -75,7 +79,9 @@ async def get_edge_contradictions( context = {'new_edge': new_edge_context, 'existing_edges': existing_edge_context} - llm_response = await llm_client.generate_response(prompt_library.invalidate_edges.v2(context)) + llm_response = await llm_client.generate_response( + prompt_library.invalidate_edges.v2(context), response_model=InvalidatedEdges + ) contradicted_edge_data = llm_response.get('invalidated_edges', [])