Skip to content

Epic News Troubleshooting Guide

Last Updated: 2026-01-10 Target Audience: All developers working with epic_news

Quick Navigation


JSON Escaping Errors

Error: Invalid JSON escape at line X column Y

Real-World Example (Poem Crew):

pydantic_core._pydantic_core.ValidationError: 1 validation error for PoemJSONOutput
  Invalid JSON: invalid escape at line 3 column 753 [type=json_invalid, input_value='{\n  "title": "Chant du ...humilité dédiée."\n}', input_type=str]
    For further information visit https://errors.pydantic.dev/2.11/v/json_invalid

Root Cause

The LLM generated JSON containing unescaped special characters, particularly: - French apostrophes: "l'humilité" should be "l\'humilité" - Quotes inside strings: Not escaped with \" - Other special characters: Backslashes, newlines not properly escaped

This happens most commonly with: 1. Non-English text (French, Spanish, German with special characters) 2. Poetry or creative writing (contains quotes, apostrophes) 3. User-generated content (unpredictable characters) 4. Complex descriptions with nested quotes

Symptoms

  • Error message contains "invalid escape" or "invalid character"
  • Error occurs during Pydantic validation (model_validate_json)
  • Input value preview shows normal-looking JSON in error message
  • Happens inconsistently (depends on LLM output content)

Add explicit JSON formatting instructions to the agent that generates JSON output:

@agent
def poem_writer(self) -> Agent:
    return Agent(
        config=self.agents_config["poem_writer"],
        llm=LLMConfig.get_openrouter_llm(task_type="default"),
        respect_context_window=True,
        verbose=True,
        system_template="""You are a JSON formatting expert. Your output MUST be valid JSON.

        CRITICAL JSON FORMATTING RULES:
        - ALL string values must have special characters properly escaped
        - Use \\" for quotes inside strings
        - Use \\\\ for backslashes
        - French apostrophes MUST be escaped: "l'amour" → "l\\'amour"
        - Common patterns to escape:
          * "C'est" → "C\\'est"
          * "d'une" → "d\\'une"
          * "l'humilité" → "l\\'humilité"
          * Any ' inside strings → \\'

        Output ONLY valid JSON with properly escaped strings. No markdown, no explanations.""",
    )

Why this works: - Gives the LLM explicit JSON escaping rules - Focuses attention on special character handling - Works preventatively before JSON is generated

File changed: - /src/epic_news/crews/poem/poem_crew.py (lines 13-35)


Solution 2: Add Pydantic Field Validators

Add validators to automatically clean text fields:

# src/epic_news/models/crews/poem_report.py
from pydantic import BaseModel, Field, field_validator

class PoemJSONOutput(BaseModel):
    """Schema ensuring tasks return a single valid JSON object for a poem."""

    title: str = Field(..., description="The title of the poem.")
    poem: str = Field(..., description="The full text of the poem.")

    @field_validator('title', 'poem', mode='before')
    @classmethod
    def clean_text(cls, v):
        """Clean and normalize text with special characters."""
        if isinstance(v, str):
            # Fix common escape issues
            v = v.replace("'", "\\'")  # Escape single quotes
            v = v.replace('"', '\\"')  # Escape double quotes
            # Note: Only do this if JSON parsing fails
        return v

Caution: This approach can cause double-escaping if the JSON is already properly formatted. Use only if system_template doesn't work.


Solution 3: Pre-process JSON String

Add a JSON cleaning utility:

# src/epic_news/utils/json_validation.py
import json
import re
from typing import Any

def clean_llm_json(raw_json: str) -> str:
    """Clean and validate JSON from LLM output.

    Handles:
    - Markdown code blocks
    - Common escape issues
    - Malformed JSON strings
    """
    # Remove markdown code blocks
    raw_json = re.sub(r'```json\n?|```', '', raw_json.strip())

    try:
        # Try to parse - this will fail if there are escape issues
        data = json.loads(raw_json)
        # Re-serialize to ensure proper escaping
        return json.dumps(data, ensure_ascii=False)
    except json.JSONDecodeError as e:
        # Attempt to fix common escape issues
        # Replace unescaped apostrophes in likely positions
        fixed_json = re.sub(r"([^\\])'", r"\1\\'", raw_json)

        try:
            data = json.loads(fixed_json)
            return json.dumps(data, ensure_ascii=False)
        except json.JSONDecodeError:
            # Last resort: return original and let Pydantic fail with clear error
            return raw_json

Usage in crew execution:

result = PoemCrew().crew().kickoff(inputs=inputs)
cleaned_json = clean_llm_json(result.raw)
report = PoemJSONOutput.model_validate_json(cleaned_json)


Prevention Checklist

  • Agent generating JSON has system_template with escaping rules
  • Test with non-English text during development (French, Spanish, German)
  • Test with special characters (quotes, apostrophes, backslashes)
  • Add Pydantic validators for text-heavy fields (poems, descriptions)
  • Use JSON cleaning utility for user-generated content
  • Log raw JSON output for debugging (print(result.raw))

Testing the Fix

# Test with French input
crewai flow kickoff

# When prompted:
> "Get me a poem about l'amour and l'humilité"

# Check for successful execution (no ValidationError)

Expected result: Poem generated successfully without validation errors.


Pydantic Validation Errors

Error: AttributeError: 'UnionType' object has no attribute 'name'

Symptom

AttributeError: 'UnionType' object has no attribute '__name__'
  at PydanticSchemaParser.get_schema()

Root Cause

CrewAI < 1.8.0 could not parse Python 3.10+ union syntax (X | Y) in output_pydantic models. CrewAI 1.8.0+ supports it.

Solution

Upgrade CrewAI (uv add "crewai>=1.8.0"). Keep the project standard:

class Report(BaseModel):
    title: str | None = None
    count: int | float = 0
    items: list[str] = []

Reference

See CLAUDE.md section: "Pydantic Models"


Error: ValidationError - field required

Symptom

pydantic.ValidationError: 1 validation error
field_name
  field required (type=value_error.missing)

Root Cause

The LLM output doesn't include a required field, or the field name doesn't match the Pydantic model.

Debugging Steps

  1. Dump raw output:

    result = MyCrew().crew().kickoff(inputs=inputs)
    print("=== RAW OUTPUT ===")
    print(result.raw)
    print("=== END RAW OUTPUT ===")
    

  2. Check field name mapping:

    import json
    raw_data = json.loads(result.raw)
    print(f"LLM returned fields: {list(raw_data.keys())}")
    print(f"Model expects fields: {MyModel.model_fields.keys()}")
    

  3. Make fields optional temporarily:

    class Report(BaseModel):
        required_field: str
        optional_field: str | None = "default"  # Won't fail if missing
    

Solutions

Option 1: Fix field names in model

class Report(BaseModel):
    # Match what LLM actually returns
    summary: str = Field(..., alias="description")  # LLM returns "description"

Option 2: Normalize data before validation

raw_data = json.loads(result.raw)
if "description" in raw_data and "summary" not in raw_data:
    raw_data["summary"] = raw_data["description"]  # Normalize field name

report = Report(**raw_data)

Option 3: Update task description

# tasks.yaml - Be explicit about field names
research_task:
  expected_output: |
    JSON with EXACTLY these fields:
    - summary (string, required)
    - date (string, ISO format, required)
    - author (string, required)


Crew Execution Errors

Error: Report contains action traces, not final report

Symptom

The task output file (or a text field in the report) contains:

Action: Search the web
Action Input: {"query": "..."}
Observation: ...
Final Answer: <actual content>

Root Cause

The task writing output_file returns free text from an agent with tools. output_pydantic on the final task already yields a clean structured result; keeping the reporter tool-free makes it a pure formatting step.

Solution

Follow the two-agent pattern: 1. Research agent: Has tools, no output_file 2. Reporter agent: NO tools, has output_file or output_pydantic

# ❌ WRONG
@agent
def reporter(self) -> Agent:
    return Agent(
        tools=[SearchTool()],  # Tools cause action traces in output
        output_file="report.json",
    )

# ✅ CORRECT
@agent
def researcher(self) -> Agent:
    return Agent(
        tools=get_search_tools(),  # Has tools
        # No output_file
    )

@agent
def reporter(self) -> Agent:
    return Agent(
        tools=[],  # NO TOOLS = Clean output
    )

@task
def reporting_task(self) -> Task:
    return Task(
        agent=self.reporter(),
        context=[self.research_task()],  # Gets data from researcher
        output_pydantic=MyReport,
    )

Reference

See CLAUDE.md section: "Reports (DOCX)"


Error: KeyError when crew initializes

Symptom

KeyError: 'tool_name'

Root Cause

Tools listed in agents.yaml. CrewBase resolves YAML tool names only against @tool-decorated methods on the crew class; any other name raises KeyError. The project assigns tools in Python.

Solution

# ❌ WRONG - agents.yaml
researcher:
  role: "Researcher"
  tools:
    - SearchTool  # Don't define tools in YAML
# ✅ CORRECT - crew.py
@agent
def researcher(self) -> Agent:
    return Agent(
        config=self.agents_config["researcher"],
        tools=get_search_tools(),  # Assign tools in code
    )

Reference

See CLAUDE.md section: "CRITICAL: Tools must be assigned programmatically in the @agent method"


Error: Crew runs but produces empty/incomplete output

Symptoms

  • The DOCX or JSON file exists but has minimal/no content
  • Report is generated but missing sections
  • Execution completes without errors but output is incomplete

Debugging Checklist

1. Check LLM timeout:

@agent
def researcher(self) -> Agent:
    return Agent(
        llm=LLMConfig.get_openrouter_llm(task_type="long"),  # "default" (300s) -> "long" (600s)
    )

2. Enable verbose logging:

@agent
def researcher(self) -> Agent:
    return Agent(
        verbose=True,  # See agent reasoning process
    )

3. Check crew logs:

tail -f logs/epic_news.log
# Look for: timeouts, errors, incomplete responses

4. Validate task expected_output:

# tasks.yaml - Be specific about requirements
research_task:
  expected_output: |
    JSON with fields: title, summary, date, author
    Minimum 5 items required
    Each item must have all fields populated

5. Check max_iter limit:

@agent
def researcher(self) -> Agent:
    return Agent(
        max_iter=LLMConfig.get_max_iter(),  # CREW_MAX_ITER (15); Crew(max_iter=...) is ignored
    )


DOCX Report Issues

Reports are DOCX only (ADR-017). A report step that cannot build its file raises and stops the run; nothing is emailed.

Error: Refusing to write a report outside output/

build_docx raises ValueError when the output path does not resolve inside output/ (ADR-015). Build the path from a fixed directory and create_topic_slug(), never from raw request text.

Error: pandoc is missing or fails

Every report needs the system pandoc binary (not just pypandoc). Check pandoc --version; the Docker image and CI install it. See ADR-013.

Error: RuntimeError from the assembler about failed sections

An assembler narrates its sections with the LLM and aborts when more than half of them fail, rather than writing a document full of placeholders. Look earlier in logs/epic_news.log for the LLM errors (timeouts, rate limits, empty responses) and see "Crew runs but produces empty/incomplete output" above.

Error: OSINT global_report.json is missing or invalid

The OSINT assembler raises when the cross-reference step did not write output/osint/global_report.json. The file is always rewritten from the parsed model, so the cause is an earlier failure in the cross-reference step.


Tool/API Errors

Error: 401 Unauthorized or 403 Forbidden

Symptom

HTTPStatusError: 401 Unauthorized

Solution Checklist

1. Check .env file:

cat .env | grep API_KEY
# Verify key exists and is not empty

2. Restart if .env changed:

# .env changes require restart
crewai flow kickoff

3. Validate key in provider dashboard: - Log into provider (OpenRouter, Serper, Tavily, etc.) - Verify key is active and has credits/quota remaining - Check if key has correct permissions

4. Check API endpoint:

# Verify correct base URL
llm = LLM(
    model="openrouter/...",
    base_url="https://openrouter.ai/api/v1",  # Correct endpoint
)


Error: 429 Too Many Requests (Rate Limit)

Symptom

HTTPStatusError: 429 Too Many Requests

Solution

1. Reduce max_rpm:

@crew
def crew(self) -> Crew:
    return Crew(
        max_rpm=10,  # Reduce from default 20
    )

2. Enable tool caching:

from crewai_tools import TavilyTool

tool = TavilyTool(cache=True)  # Reuse results within session

3. Add retry logic:

@agent
def researcher(self) -> Agent:
    return Agent(
        max_retry_limit=3,  # Agent field; ignored on Crew
    )

kickoff_flow() also retries transient provider errors for the whole crew.

4. Batch operations: - Process multiple items in one crew run - Avoid separate crew executions per item


Import/Module Errors

Error: ModuleNotFoundError: No module named 'epic_news'

Symptom

ModuleNotFoundError: No module named 'epic_news'

Root Cause

Package not installed in editable mode. Python can't find the epic_news module.

Solution

uv pip install -e .

Why editable mode? - Creates symlink so Python finds epic_news package - Changes to code are immediately available - No need to reinstall after each edit

Verify installation:

uv pip list | grep epic-news
# Should show: epic-news (editable install)


Error: Circular import

Symptom

ImportError: cannot import name 'X' from partially initialized module 'Y'

Solution

Project rule: imports stay at the top of the file, so break the cycle instead of moving the import into a function.

Restructure dependencies - Extract shared code to separate module - Use dependency injection instead of direct imports - Avoid importing from __init__.py files


Performance Issues

Issue: Crew takes > 5 minutes to complete

Diagnosis Steps

1. Check LLM timeout:

# The timeout lives on the LLM; `llm_timeout=` is not a CrewAI field and is ignored
@agent
def researcher(self) -> Agent:
    return Agent(
        llm=LLMConfig.get_openrouter_llm(task_type="long"),  # 600s
    )

2. Profile tool calls:

# Count API calls in logs
grep "Action:" logs/epic_news.log | wc -l

3. Check for agent loops:

# Look for repeated actions
grep "Action:" logs/epic_news.log | tail -20

4. Monitor max_iter:

# max_iter is an Agent field (Crew has none; CrewAI default is 25)
@agent
def researcher(self) -> Agent:
    return Agent(
        max_iter=LLMConfig.get_max_iter(),  # lower CREW_MAX_ITER (default 15) if agents loop
    )

Optimization Strategies

1. Use faster models for simple tasks:

# Quick tasks (classification, simple extraction)
llm=LLMConfig.get_openrouter_llm(task_type="quick")  # Uses MODEL from .env

# Default (ADR-016): MODEL=gemini/gemini-3.8-flash
# OpenRouter alternative: MODEL=openrouter/mistralai/mistral-small-2603

2. Enable tool caching:

from crewai_tools import TavilyTool

tool = TavilyTool(cache=True)  # Reuse results

3. Parallelize independent tasks:

@task
def task1(self) -> Task:
    return Task(
        async_execution=True,  # Run in parallel
    )

@task
def task2(self) -> Task:
    return Task(
        async_execution=True,  # Run in parallel
    )

4. Reduce max_rpm for stability:

@crew
def crew(self) -> Crew:
    return Crew(
        max_rpm=10,  # Fewer concurrent requests
    )


Debugging Checklist

When you encounter any error:

  • Read the full stack trace (don't just look at last line)
  • Check logs: tail -f logs/epic_news.log
  • Verify .env file has required API keys
  • Confirm package installed: uv pip install -e .
  • Check CrewAI is >= 1.8.0 if X | None fields fail to parse
  • Validate crew structure: researcher (tools) + reporter (no tools)
  • Enable verbose mode: verbose=True on agents
  • Dump raw output: print(result.raw)
  • Test crew in isolation (not through ReceptionFlow)
  • Search this guide for error message keywords

Still Stuck?

1. Search existing documentation

  • CLAUDE.md for architectural patterns and critical rules
  • how-to/development_setup.md for setup and workflow issues
  • explanations/architecture.md for design solutions
  • reference/tools.md for tool usage patterns

2. Check example crews

Study working crews for patterns: - poem - Simple single-agent crew - cooking - Medium complexity - library - Two-agent pattern with a DOCX report - fin_daily - Complex with async execution

3. Enable debug logging

from loguru import logger

# In your code
logger.add("debug.log", level="DEBUG")
logger.debug(f"State: {self.state}")
logger.debug(f"Inputs: {crew_inputs}")
logger.debug(f"Raw output: {result.raw}")

4. Create minimal reproduction

Isolate the problem:

# e.g. tests/crews/test_poem_smoke.py: builds the crew, makes no LLM call
from epic_news.crews.poem.poem_crew import PoemCrew

def test_poem_crew_builds():
    crew = PoemCrew().crew()
    assert crew.agents and crew.tasks

End-to-end runs still go through crewai flow kickoff.

5. Ask for help with context

Include: - Error message with full stack trace - Relevant code (crew definition, model, renderer) - What you tried - What you expected vs. what happened


Summary

Most Common Errors:

  1. JSON escaping - Use system_template with explicit escaping rules
  2. Pydantic Union syntax - Upgrade CrewAI to 1.8.0+; use X | None
  3. Action traces in reports - Two-agent pattern (researcher + reporter)
  4. Tools in YAML - Assign tools in Python code, not YAML
  5. ModuleNotFoundError - Run uv pip install -e .

Quick Fixes:

Error Quick Fix
JSON escaping Add system_template to agent
Union syntax Upgrade CrewAI (>= 1.8.0)
Action traces output_pydantic on the final task; reporter tools=[]
Tools KeyError Move tool assignment from YAML to Python
Module not found Run uv pip install -e .
401 Unauthorized Check API keys in .env
429 Rate limit Reduce max_rpm in crew config

Next Steps:


Last Updated: 2026-01-10