fix(pipeline): retry on 0-entity response, save raw debug, improve template
- SourcePipeline: retry split_entities stage once when 0 entity delimiters are found (free-tier models intermittently return short non-formatted responses); save raw LLM response to <stage>-raw.md alongside prompts - Return None (pause pipeline) rather than writing empty view file when no entities found after max retries - _http.py: wrap json.JSONDecodeError in LLMAPIError with body preview - extract-entities.md: add explicit H2-heading format example to Output Format section to prevent models from using inline "Section:" format Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -52,3 +52,35 @@ already contains it. Only extract entities that are genuinely new.
|
|||||||
|
|
||||||
Output each entity as a separate markdown document, delimited by
|
Output each entity as a separate markdown document, delimited by
|
||||||
`--- ENTITY: <entity-name> ---` markers.
|
`--- ENTITY: <entity-name> ---` markers.
|
||||||
|
|
||||||
|
Use **H2 headings** (`##`) for each section inside the entity document.
|
||||||
|
Do NOT use inline `Section:` format or H3 headings.
|
||||||
|
|
||||||
|
Example of a correctly formatted entity:
|
||||||
|
|
||||||
|
```
|
||||||
|
--- ENTITY: division of labour ---
|
||||||
|
|
||||||
|
# Division of Labour
|
||||||
|
|
||||||
|
## Definition
|
||||||
|
|
||||||
|
The separation of a work process into distinct tasks performed by specialised
|
||||||
|
workers, increasing productivity through greater dexterity, saved time, and
|
||||||
|
the invention of labour-saving machinery.
|
||||||
|
|
||||||
|
## Source Chapter
|
||||||
|
|
||||||
|
Book I, Chapter 1
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
The opening chapter's central argument, illustrated by Smith's pin factory
|
||||||
|
example showing how dividing 18 operations dramatically increases output.
|
||||||
|
|
||||||
|
## Economic Domain
|
||||||
|
|
||||||
|
Production
|
||||||
|
|
||||||
|
---
|
||||||
|
```
|
||||||
|
|||||||
@@ -197,14 +197,39 @@ class SourcePipeline:
|
|||||||
print(" No LLM adapter — skipping generation (manual mode).")
|
print(" No LLM adapter — skipping generation (manual mode).")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Call LLM
|
# Call LLM — with one retry for split_entities stages that return 0 entities
|
||||||
content = self._call_llm(prompt, stage_label)
|
max_attempts = 2 if stage.split_entities else 1
|
||||||
if content is None:
|
entity_files: List[Tuple[str, Path]] = []
|
||||||
return None
|
content = None
|
||||||
|
|
||||||
|
for attempt in range(max_attempts):
|
||||||
|
content = self._call_llm(prompt, stage_label)
|
||||||
|
if content is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Save raw response for debugging (overwritten on retry)
|
||||||
|
if output_file:
|
||||||
|
raw_file = output_file.parent / f"{source_id}-{stage.name or 'stage'}-raw.md"
|
||||||
|
raw_file.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
raw_file.write_text(content, encoding="utf-8")
|
||||||
|
|
||||||
|
if stage.split_entities:
|
||||||
|
entity_files = self._split_and_write_entities(stage, content)
|
||||||
|
if entity_files:
|
||||||
|
break # Got entities — proceed
|
||||||
|
if attempt < max_attempts - 1:
|
||||||
|
print(f" No entity delimiters found — retrying ({attempt + 2}/{max_attempts})...")
|
||||||
|
else:
|
||||||
|
print(
|
||||||
|
f" WARNING: No '--- ENTITY: ---' markers found after {max_attempts} attempt(s).\n"
|
||||||
|
f" Check {raw_file.name} to inspect the raw LLM response."
|
||||||
|
)
|
||||||
|
return None # Don't write empty view; allow re-run
|
||||||
|
else:
|
||||||
|
break # Non-split stages don't need retry
|
||||||
|
|
||||||
# Persist output
|
# Persist output
|
||||||
if stage.split_entities:
|
if stage.split_entities:
|
||||||
entity_files = self._split_and_write_entities(stage, content)
|
|
||||||
self._write_entity_view(source_id, entity_files, output_file)
|
self._write_entity_view(source_id, entity_files, output_file)
|
||||||
return content
|
return content
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -40,7 +40,14 @@ def post_json(
|
|||||||
try:
|
try:
|
||||||
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
||||||
body = resp.read().decode()
|
body = resp.read().decode()
|
||||||
return json.loads(body)
|
try:
|
||||||
|
return json.loads(body)
|
||||||
|
except json.JSONDecodeError as exc:
|
||||||
|
preview = body[:300].replace("\n", "\\n")
|
||||||
|
raise LLMAPIError(
|
||||||
|
f"Invalid JSON response from {url}: {exc} — body preview: {preview!r}",
|
||||||
|
cause=exc,
|
||||||
|
) from exc
|
||||||
except urllib.error.HTTPError as exc:
|
except urllib.error.HTTPError as exc:
|
||||||
body = ""
|
body = ""
|
||||||
try:
|
try:
|
||||||
|
|||||||
Reference in New Issue
Block a user