Skip to content
Closed
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
Original file line number Diff line number Diff line change
Expand Up @@ -1362,10 +1362,11 @@ async def _extract_facts_from_chunk(
llm_max_retries = (
config.retain_llm_max_retries if config.retain_llm_max_retries is not None else config.llm_max_retries
)
extraction_attempts = max(1, llm_max_retries)
last_error: Exception | None = None

usage = TokenUsage() # Track cumulative usage across retries
for attempt in range(llm_max_retries):
for attempt in range(extraction_attempts):
try:
initial_backoff = (
config.retain_llm_initial_backoff
Expand Down Expand Up @@ -1672,7 +1673,7 @@ def get_value(field_name):
# If we exhausted all retries, raise the last error or a descriptive fallback
if last_error is not None:
raise last_error
raise RuntimeError(f"Fact extraction failed after {llm_max_retries} attempts: LLM did not return valid JSON")
raise RuntimeError(f"Fact extraction failed after {extraction_attempts} attempts: LLM did not return valid JSON")


async def _extract_facts_with_auto_split(
Expand Down
27 changes: 27 additions & 0 deletions hindsight-api-slim/tests/test_fact_extraction_retry.py
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,33 @@ async def test_retain_llm_max_retries_overrides_global():
assert llm_config.call.call_count == 5


@pytest.mark.asyncio
async def test_zero_retain_retry_budget_still_makes_initial_call():
"""A zero transport retry budget must not skip fact extraction."""
from hindsight_api.engine.retain.fact_extraction import _extract_facts_from_chunk

config = _make_config(llm_max_retries=10, retain_llm_max_retries=0)
llm_config = _make_llm_config(mock_response={"facts": [], "entities": []})

with patch(
"hindsight_api.engine.retain.fact_extraction._build_extraction_prompt_and_schema",
return_value=("system prompt", MagicMock()),
):
await _extract_facts_from_chunk(
chunk="Bob likes Python.",
chunk_index=0,
total_chunks=1,
event_date=datetime(2024, 1, 1, tzinfo=timezone.utc),
context="",
llm_config=llm_config,
config=config,
agent_name="agent",
)

llm_config.call.assert_awaited_once()
assert llm_config.call.await_args.kwargs["max_retries"] == 0


@pytest.mark.asyncio
async def test_none_event_date_with_empty_facts_no_crash():
"""
Expand Down