From 196eb2f077a34e2643a2720c9f3c23aa9cb5404c Mon Sep 17 00:00:00 2001 From: Daniel Chalef <131175+danielchalef@users.noreply.github.com> Date: Mon, 6 Oct 2025 16:08:43 -0700 Subject: [PATCH] Remove JSON indentation from prompts to reduce token usage (#985) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Changes to `to_prompt_json()` helper to default to minified JSON (no indentation) instead of 2-space indentation. This reduces token consumption in LLM prompts while maintaining all necessary information. - Changed default `indent` parameter from `2` to `None` in `prompt_helpers.py` - Updated all prompt modules to remove explicit `indent=2` arguments - Minor code formatting fixes in LLM clients 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude --- graphiti_core/llm_client/anthropic_client.py | 4 ++- .../llm_client/openai_base_client.py | 6 ++++- .../llm_client/openai_generic_client.py | 6 ++++- graphiti_core/prompts/dedupe_edges.py | 8 +++--- graphiti_core/prompts/dedupe_nodes.py | 20 +++++++------- graphiti_core/prompts/extract_edges.py | 8 +++--- graphiti_core/prompts/extract_nodes.py | 26 +++++++++---------- graphiti_core/prompts/prompt_helpers.py | 4 +-- graphiti_core/prompts/summarize_nodes.py | 24 ++++++++--------- graphiti_core/search/search_helpers.py | 8 +++--- graphiti_core/utils/maintenance/utils.py | 0 uv.lock | 2 +- 12 files changed, 63 insertions(+), 53 deletions(-) delete mode 100644 graphiti_core/utils/maintenance/utils.py diff --git a/graphiti_core/llm_client/anthropic_client.py b/graphiti_core/llm_client/anthropic_client.py index 17956cc2..1f2916b3 100644 --- a/graphiti_core/llm_client/anthropic_client.py +++ b/graphiti_core/llm_client/anthropic_client.py @@ -349,7 +349,9 @@ class AnthropicClient(LLMClient): # Common retry logic retry_count += 1 messages.append(Message(role='user', content=error_context)) - logger.warning(f'Retrying after error (attempt {retry_count}/{max_retries}): {e}') + logger.warning( + f'Retrying after error (attempt {retry_count}/{max_retries}): {e}' + ) # If we somehow get here, raise the last error span.set_status('error', str(last_error)) diff --git a/graphiti_core/llm_client/openai_base_client.py b/graphiti_core/llm_client/openai_base_client.py index 3a5552e9..66419819 100644 --- a/graphiti_core/llm_client/openai_base_client.py +++ b/graphiti_core/llm_client/openai_base_client.py @@ -209,7 +209,11 @@ class BaseOpenAIClient(LLMClient): # These errors should not trigger retries span.set_status('error', str(last_error)) raise - except (openai.APITimeoutError, openai.APIConnectionError, openai.InternalServerError): + except ( + openai.APITimeoutError, + openai.APIConnectionError, + openai.InternalServerError, + ): # Let OpenAI's client handle these retries span.set_status('error', str(last_error)) raise diff --git a/graphiti_core/llm_client/openai_generic_client.py b/graphiti_core/llm_client/openai_generic_client.py index 41bd7d5a..c2ee9691 100644 --- a/graphiti_core/llm_client/openai_generic_client.py +++ b/graphiti_core/llm_client/openai_generic_client.py @@ -161,7 +161,11 @@ class OpenAIGenericClient(LLMClient): # These errors should not trigger retries span.set_status('error', str(last_error)) raise - except (openai.APITimeoutError, openai.APIConnectionError, openai.InternalServerError): + except ( + openai.APITimeoutError, + openai.APIConnectionError, + openai.InternalServerError, + ): # Let OpenAI's client handle these retries span.set_status('error', str(last_error)) raise diff --git a/graphiti_core/prompts/dedupe_edges.py b/graphiti_core/prompts/dedupe_edges.py index c5b55427..2d9bc042 100644 --- a/graphiti_core/prompts/dedupe_edges.py +++ b/graphiti_core/prompts/dedupe_edges.py @@ -67,13 +67,13 @@ def edge(context: dict[str, Any]) -> list[Message]: Given the following context, determine whether the New Edge represents any of the edges in the list of Existing Edges. - {to_prompt_json(context['related_edges'], indent=2)} + {to_prompt_json(context['related_edges'])} - {to_prompt_json(context['extracted_edges'], indent=2)} + {to_prompt_json(context['extracted_edges'])} - + Task: If the New Edges represents the same factual information as any edge in Existing Edges, return the id of the duplicate fact as part of the list of duplicate_facts. @@ -98,7 +98,7 @@ def edge_list(context: dict[str, Any]) -> list[Message]: Given the following context, find all of the duplicates in a list of facts: Facts: - {to_prompt_json(context['edges'], indent=2)} + {to_prompt_json(context['edges'])} Task: If any facts in Facts is a duplicate of another fact, return a new fact with one of their uuid's. diff --git a/graphiti_core/prompts/dedupe_nodes.py b/graphiti_core/prompts/dedupe_nodes.py index 9ecc926a..ec1b0fe0 100644 --- a/graphiti_core/prompts/dedupe_nodes.py +++ b/graphiti_core/prompts/dedupe_nodes.py @@ -64,20 +64,20 @@ def node(context: dict[str, Any]) -> list[Message]: role='user', content=f""" - {to_prompt_json([ep for ep in context['previous_episodes']], indent=2)} + {to_prompt_json([ep for ep in context['previous_episodes']])} {context['episode_content']} - {to_prompt_json(context['extracted_node'], indent=2)} + {to_prompt_json(context['extracted_node'])} - {to_prompt_json(context['entity_type_description'], indent=2)} + {to_prompt_json(context['entity_type_description'])} - {to_prompt_json(context['existing_nodes'], indent=2)} + {to_prompt_json(context['existing_nodes'])} 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]: role='user', content=f""" - {to_prompt_json([ep for ep in context['previous_episodes']], indent=2)} + {to_prompt_json([ep for ep in context['previous_episodes']])} {context['episode_content']} - - + + Each of the following ENTITIES were extracted from the CURRENT MESSAGE. Each entity in ENTITIES is represented as a JSON object with the following structure: {{ @@ -142,11 +142,11 @@ def nodes(context: dict[str, Any]) -> list[Message]: }} - {to_prompt_json(context['extracted_nodes'], indent=2)} + {to_prompt_json(context['extracted_nodes'])} - {to_prompt_json(context['existing_nodes'], indent=2)} + {to_prompt_json(context['existing_nodes'])} 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]: Given the following context, deduplicate a list of nodes: Nodes: - {to_prompt_json(context['nodes'], indent=2)} + {to_prompt_json(context['nodes'])} Task: 1. Group nodes together such that all duplicate nodes are in the same list of uuids diff --git a/graphiti_core/prompts/extract_edges.py b/graphiti_core/prompts/extract_edges.py index 28d9bddc..7e9d6d6c 100644 --- a/graphiti_core/prompts/extract_edges.py +++ b/graphiti_core/prompts/extract_edges.py @@ -80,7 +80,7 @@ def edge(context: dict[str, Any]) -> list[Message]: -{to_prompt_json([ep for ep in context['previous_episodes']], indent=2)} +{to_prompt_json([ep for ep in context['previous_episodes']])} @@ -88,7 +88,7 @@ def edge(context: dict[str, Any]) -> list[Message]: -{to_prompt_json(context['nodes'], indent=2)} +{to_prompt_json(context['nodes'])} @@ -141,7 +141,7 @@ def reflexion(context: dict[str, Any]) -> list[Message]: user_prompt = f""" -{to_prompt_json([ep for ep in context['previous_episodes']], indent=2)} +{to_prompt_json([ep for ep in context['previous_episodes']])} {context['episode_content']} @@ -175,7 +175,7 @@ def extract_attributes(context: dict[str, Any]) -> list[Message]: content=f""" - {to_prompt_json(context['episode_content'], indent=2)} + {to_prompt_json(context['episode_content'])} {context['reference_time']} diff --git a/graphiti_core/prompts/extract_nodes.py b/graphiti_core/prompts/extract_nodes.py index 8e85c7a6..1187c039 100644 --- a/graphiti_core/prompts/extract_nodes.py +++ b/graphiti_core/prompts/extract_nodes.py @@ -93,7 +93,7 @@ def extract_message(context: dict[str, Any]) -> list[Message]: -{to_prompt_json([ep for ep in context['previous_episodes']], indent=2)} +{to_prompt_json([ep for ep in context['previous_episodes']])} @@ -201,7 +201,7 @@ def reflexion(context: dict[str, Any]) -> list[Message]: user_prompt = f""" -{to_prompt_json([ep for ep in context['previous_episodes']], indent=2)} +{to_prompt_json([ep for ep in context['previous_episodes']])} {context['episode_content']} @@ -225,22 +225,22 @@ def classify_nodes(context: dict[str, Any]) -> list[Message]: user_prompt = f""" - {to_prompt_json([ep for ep in context['previous_episodes']], indent=2)} + {to_prompt_json([ep for ep in context['previous_episodes']])} {context['episode_content']} - + {context['extracted_entities']} - + {context['entity_types']} - + Given the above conversation, extracted entities, and provided entity types and their descriptions, classify the extracted entities. - + Guidelines: 1. Each entity must have exactly one type 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]: 2. Only use the provided MESSAGES and ENTITY to set attribute values. - {to_prompt_json(context['previous_episodes'], indent=2)} - {to_prompt_json(context['episode_content'], indent=2)} + {to_prompt_json(context['previous_episodes'])} + {to_prompt_json(context['episode_content'])} - + {context['node']} @@ -292,12 +292,12 @@ def extract_summary(context: dict[str, Any]) -> list[Message]: content=f""" Given the MESSAGES and the ENTITY, update the summary that combines relevant information about the entity from the messages and relevant information from the existing summary. - + {summary_instructions} - {to_prompt_json(context['previous_episodes'], indent=2)} - {to_prompt_json(context['episode_content'], indent=2)} + {to_prompt_json(context['previous_episodes'])} + {to_prompt_json(context['episode_content'])} diff --git a/graphiti_core/prompts/prompt_helpers.py b/graphiti_core/prompts/prompt_helpers.py index 8c4b5123..10e69312 100644 --- a/graphiti_core/prompts/prompt_helpers.py +++ b/graphiti_core/prompts/prompt_helpers.py @@ -20,14 +20,14 @@ from typing import Any DO_NOT_ESCAPE_UNICODE = '\nDo not escape unicode characters.\n' -def to_prompt_json(data: Any, ensure_ascii: bool = False, indent: int = 2) -> str: +def to_prompt_json(data: Any, ensure_ascii: bool = False, indent: int | None = None) -> str: """ Serialize data to JSON for use in prompts. Args: data: The data to serialize ensure_ascii: If True, escape non-ASCII characters. If False (default), preserve them. - indent: Number of spaces for indentation + indent: Number of spaces for indentation. Defaults to None (minified). Returns: JSON string representation of the data diff --git a/graphiti_core/prompts/summarize_nodes.py b/graphiti_core/prompts/summarize_nodes.py index 154dc5ed..d771b5c9 100644 --- a/graphiti_core/prompts/summarize_nodes.py +++ b/graphiti_core/prompts/summarize_nodes.py @@ -56,11 +56,11 @@ def summarize_pair(context: dict[str, Any]) -> list[Message]: role='user', content=f""" Synthesize the information from the following two summaries into a single succinct summary. - + IMPORTANT: Keep the summary concise and to the point. SUMMARIES MUST BE LESS THAN 250 CHARACTERS. Summaries: - {to_prompt_json(context['node_summaries'], indent=2)} + {to_prompt_json(context['node_summaries'])} """, ), ] @@ -77,28 +77,28 @@ def summarize_context(context: dict[str, Any]) -> list[Message]: content=f""" Given the MESSAGES and the ENTITY name, create a summary for the ENTITY. Your summary must only use information from the provided MESSAGES. Your summary should also only contain information relevant to the - provided ENTITY. - + provided ENTITY. + In addition, extract any values for the provided entity properties based on their descriptions. 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. - + {summary_instructions} - {to_prompt_json(context['previous_episodes'], indent=2)} - {to_prompt_json(context['episode_content'], indent=2)} + {to_prompt_json(context['previous_episodes'])} + {to_prompt_json(context['episode_content'])} - + {context['node_name']} - + {context['node_summary']} - + - {to_prompt_json(context['attributes'], indent=2)} + {to_prompt_json(context['attributes'])} """, ), @@ -118,7 +118,7 @@ def summary_description(context: dict[str, Any]) -> list[Message]: Summaries must be under 250 characters. Summary: - {to_prompt_json(context['summary'], indent=2)} + {to_prompt_json(context['summary'])} """, ), ] diff --git a/graphiti_core/search/search_helpers.py b/graphiti_core/search/search_helpers.py index 620f8ceb..5ee530c0 100644 --- a/graphiti_core/search/search_helpers.py +++ b/graphiti_core/search/search_helpers.py @@ -56,16 +56,16 @@ def search_results_to_context_string(search_results: SearchResults) -> str: These are the most relevant facts and their valid and invalid dates. Facts are considered valid between their valid_at and invalid_at dates. Facts with an invalid_at date of "Present" are considered valid. - {to_prompt_json(fact_json, indent=12)} + {to_prompt_json(fact_json)} - {to_prompt_json(entity_json, indent=12)} + {to_prompt_json(entity_json)} - {to_prompt_json(episode_json, indent=12)} + {to_prompt_json(episode_json)} - {to_prompt_json(community_json, indent=12)} + {to_prompt_json(community_json)} """ diff --git a/graphiti_core/utils/maintenance/utils.py b/graphiti_core/utils/maintenance/utils.py deleted file mode 100644 index e69de29b..00000000 diff --git a/uv.lock b/uv.lock index 12905404..5c150a85 100644 --- a/uv.lock +++ b/uv.lock @@ -783,7 +783,7 @@ wheels = [ [[package]] name = "graphiti-core" -version = "0.22.0rc3" +version = "0.22.0rc4" source = { editable = "." } dependencies = [ { name = "diskcache" },