Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions graphiti_core/prompts/dedupe_edges.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,11 @@

class EdgeDuplicate(BaseModel):
duplicate_facts: list[int] = Field(
...,
default_factory=list,
description='List of idx values of duplicate facts (only from EXISTING FACTS range). Empty list if none.',
)
contradicted_facts: list[int] = Field(
...,
default_factory=list,
description='List of idx values of contradicted facts (from full idx range). Empty list if none.',
)

Expand Down
4 changes: 3 additions & 1 deletion graphiti_core/prompts/dedupe_nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,9 @@ class NodeDuplicate(BaseModel):


class NodeResolutions(BaseModel):
entity_resolutions: list[NodeDuplicate] = Field(..., description='List of resolved nodes')
entity_resolutions: list[NodeDuplicate] = Field(
default_factory=list, description='List of resolved nodes'
)


class Prompt(Protocol):
Expand Down
2 changes: 1 addition & 1 deletion graphiti_core/prompts/extract_edges.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ class Edge(BaseModel):


class ExtractedEdges(BaseModel):
edges: list[Edge]
edges: list[Edge] = Field(default_factory=list)


class Prompt(Protocol):
Expand Down
6 changes: 4 additions & 2 deletions graphiti_core/prompts/extract_nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,9 @@ class ExtractedEntity(BaseModel):


class ExtractedEntities(BaseModel):
extracted_entities: list[ExtractedEntity] = Field(..., description='List of extracted entities')
extracted_entities: list[ExtractedEntity] = Field(
default_factory=list, description='List of extracted entities'
)


class EntitySummary(BaseModel):
Expand All @@ -48,7 +50,7 @@ class SummarizedEntity(BaseModel):

class SummarizedEntities(BaseModel):
summaries: list[SummarizedEntity] = Field(
...,
default_factory=list,
description='List of entity summaries. Only include entities that need summary updates.',
)

Expand Down
Empty file added tests/prompts/__init__.py
Empty file.
100 changes: 100 additions & 0 deletions tests/prompts/test_response_models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
"""Regression tests for response model defaults.

When LLM providers (Anthropic, Gemini) return empty tool input ``{}``,
response models with required list fields crash with ``ValidationError``.
These tests verify that all list-based response models accept empty input
and produce valid instances with empty lists.
"""

from graphiti_core.prompts.dedupe_edges import EdgeDuplicate
from graphiti_core.prompts.dedupe_nodes import NodeDuplicate, NodeResolutions
from graphiti_core.prompts.extract_edges import Edge, ExtractedEdges
from graphiti_core.prompts.extract_nodes import (
ExtractedEntities,
ExtractedEntity,
SummarizedEntities,
SummarizedEntity,
)


class TestResponseModelsAcceptEmptyInput:
"""Verify that response models handle empty LLM output without crashing.

This simulates the exact code path in anthropic_client.py (line 403):
model_instance = response_model(**response)
where response is {} (empty dict from tool_use input).
"""

def test_extracted_entities_empty_input(self):
result = ExtractedEntities(**{})
assert result.extracted_entities == []

def test_extracted_edges_empty_input(self):
result = ExtractedEdges(**{})
assert result.edges == []

def test_edge_duplicate_empty_input(self):
result = EdgeDuplicate(**{})
assert result.duplicate_facts == []
assert result.contradicted_facts == []

def test_node_resolutions_empty_input(self):
result = NodeResolutions(**{})
assert result.entity_resolutions == []

def test_summarized_entities_empty_input(self):
result = SummarizedEntities(**{})
assert result.summaries == []


class TestResponseModelsPopulatedInput:
"""Verify that response models still work correctly with populated input."""

def test_extracted_entities_with_data(self):
result = ExtractedEntities(
extracted_entities=[
ExtractedEntity(name='Alice', entity_type_id=1),
ExtractedEntity(name='Bob', entity_type_id=2),
]
)
assert len(result.extracted_entities) == 2
assert result.extracted_entities[0].name == 'Alice'

def test_extracted_edges_with_data(self):
result = ExtractedEdges(
edges=[
Edge(
source_entity_name='Alice',
target_entity_name='Bob',
relation_type='KNOWS',
fact='Alice knows Bob',
valid_at=None,
invalid_at=None,
)
]
)
assert len(result.edges) == 1
assert result.edges[0].relation_type == 'KNOWS'

def test_edge_duplicate_with_data(self):
result = EdgeDuplicate(duplicate_facts=[1, 2], contradicted_facts=[3])
assert result.duplicate_facts == [1, 2]
assert result.contradicted_facts == [3]

def test_node_resolutions_with_data(self):
result = NodeResolutions(
entity_resolutions=[
NodeDuplicate(id=1, name='Alice', duplicate_candidate_id=-1),
]
)
assert len(result.entity_resolutions) == 1
assert result.entity_resolutions[0].duplicate_candidate_id == -1

def test_summarized_entities_with_data(self):
result = SummarizedEntities(
summaries=[
SummarizedEntity(name='Alice', summary='A person named Alice'),
]
)
assert len(result.summaries) == 1
assert result.summaries[0].name == 'Alice'
Loading