diff --git a/.env-example b/.env-example index 92d699e..dd9039c 100644 --- a/.env-example +++ b/.env-example @@ -5,7 +5,8 @@ # ===== MODEL ACCESS AND SELECTION (OPENAI) ===== -OPENAI_API_KEY=your-openai-api-key-here # For embeddings and optional model access +# OpenAI API Configuration +OPENAI_API_KEY=your_openai_api_key_here # Default model for general processing and fallback DEFAULT_MODEL=gpt-4o @@ -32,6 +33,9 @@ SERPER_API_KEY=your_serper_api_key_here # Only needed if USE_SERPER=tr # ===== DATABASE CONFIGURATION ===== +# Database Configuration +DATABASE_URL=postgresql://username:password@localhost:5432/verifact + # Supabase Configuration SUPABASE_URL=https://your-project.supabase.co # [REQUIRED] URL from Supabase dashboard SUPABASE_KEY=your-supabase-anon-key # [REQUIRED] Public anon key @@ -66,9 +70,23 @@ PORT=8000 API_KEY_ENABLED=true # Enable API key authentication API_KEY_HEADER_NAME=X-API-Key # Header name for API keys DEFAULT_API_KEY=your-default-api-key # Default API key -RATE_LIMIT_ENABLED=true # Enable rate limiting -RATE_LIMIT_REQUESTS=100 # Number of requests per window -RATE_LIMIT_WINDOW=3600 # Rate limit window in seconds + +# Application Settings +DEBUG=False +ENVIRONMENT=development +LOG_LEVEL=INFO + +# API Rate Limiting +RATE_LIMIT_REQUESTS=100 +RATE_LIMIT_PERIOD=3600 + +# Feature Flags +ENABLE_CACHING=True +ENABLE_ASYNC_PROCESSING=True + +# Security Settings +SECRET_KEY=your_secret_key_here +ALLOWED_HOSTS=localhost,127.0.0.1 # ===== ADVANCED CONFIGURATION ===== @@ -78,7 +96,9 @@ ENABLE_MODEL_CACHING=true # Cache model responses MODEL_CACHE_SIZE=1000 # Number of responses to cache # Logging Configuration -ENVIRONMENT=development -LOG_LEVEL=INFO # DEBUG, INFO, WARNING, ERROR, CRITICAL LOG_FORMAT=plain -# LOG_FILE=/path/to/log/file.log # Uncomment to enable file logging \ No newline at end of file +# LOG_FILE=/path/to/log/file.log # Uncomment to enable file logging + +# Optional: External Service Integration +# ENABLE_SENTRY=False +# SENTRY_DSN=your_sentry_dsn_here \ No newline at end of file diff --git a/pytest.ini b/pytest.ini index 2b7c862..db4fbeb 100644 --- a/pytest.ini +++ b/pytest.ini @@ -5,4 +5,6 @@ python_classes = Test* python_functions = test_* # Display detailed test results -addopts = --verbose \ No newline at end of file +addopts = --verbose --tb=short +filterwarnings = + ignore::PendingDeprecationWarning:starlette.formparsers \ No newline at end of file diff --git a/src/tests/conftest.py b/src/tests/conftest.py new file mode 100644 index 0000000..e4c50b9 --- /dev/null +++ b/src/tests/conftest.py @@ -0,0 +1,44 @@ +import pytest +import warnings +import sys +from pathlib import Path +from typing import Generator, Dict, Any + +# Add the project root directory to the Python path +project_root = str(Path(__file__).parent.parent.parent) +sys.path.insert(0, project_root) + +from src.verifact_agents.verdict_writer import VerdictWriter + +# Suppress the starlette multipart deprecation warning +warnings.filterwarnings("ignore", category=PendingDeprecationWarning, module="starlette.formparsers") + +@pytest.fixture(scope="session") +def test_evidence() -> Dict[str, Any]: + """Provide sample evidence for testing.""" + return { + "content": "This is a sample evidence content for testing purposes.", + "source": "Test Source", + "relevance": 0.9, + "stance": "supporting" + } + +@pytest.fixture(scope="session") +def test_claim() -> str: + """Provide a sample claim for testing.""" + return "This is a test claim that needs verification." + +@pytest.fixture(scope="session") +def verdict_writer() -> Generator[VerdictWriter, None, None]: + """Create a VerdictWriter instance for testing.""" + writer = VerdictWriter() + yield writer + +@pytest.fixture(autouse=True) +def setup_test_environment(): + """Set up test environment variables.""" + import os + os.environ["ENVIRONMENT"] = "test" + os.environ["LOG_LEVEL"] = "DEBUG" + yield + # Cleanup after tests if needed diff --git a/src/tests/verifact_agents/test_verdict_writer.py b/src/tests/verifact_agents/test_verdict_writer.py new file mode 100644 index 0000000..91afb98 --- /dev/null +++ b/src/tests/verifact_agents/test_verdict_writer.py @@ -0,0 +1,138 @@ +import pytest +from src.verifact_agents.verdict_writer import VerdictWriter, Verdict + +@pytest.mark.asyncio +async def test_verdict_writer_returns_valid_verdict(): + writer = VerdictWriter() + claim = "Bananas are a good source of potassium." + evidence = [ + { + "content": "Bananas contain around 422 mg of potassium per medium fruit, making them a good dietary source.", + "source": "Healthline", + "relevance": 0.9, + "stance": "supporting" + }, + { + "content": "Potatoes and beans contain more potassium than bananas, but bananas are still a decent source.", + "source": "Harvard School of Public Health", + "relevance": 0.8, + "stance": "neutral" + } + ] + + verdict = await writer.run(claim=claim, evidence=evidence, detail_level="standard") + + assert isinstance(verdict, Verdict) + assert verdict.claim == claim + assert verdict.verdict in ["true", "false", "partially true", "unverifiable"] + assert 0.0 <= verdict.confidence <= 1.0 + assert isinstance(verdict.explanation, str) and len(verdict.explanation.strip()) > 0 + assert isinstance(verdict.sources, list) and all(isinstance(s, str) for s in verdict.sources) + assert len(verdict.sources) > 0 + +@pytest.mark.asyncio +async def test_verdict_writer_brief_detail(): + writer = VerdictWriter() + claim = "Water freezes at 0 degrees Celsius." + evidence = [ + { + "content": "Under standard atmospheric conditions, water freezes at 0°C.", + "source": "Britannica", + "relevance": 0.95, + "stance": "supporting" + } + ] + + verdict = await writer.run(claim=claim, evidence=evidence, detail_level="brief") + + assert isinstance(verdict.explanation, str) + assert len(verdict.explanation.split()) <= 30 # brief explanation is usually short + +@pytest.mark.asyncio +async def test_verdict_writer_detailed_includes_sources(): + writer = VerdictWriter() + claim = "The Earth revolves around the Sun." + evidence = [ + { + "content": "Astronomical evidence and observations confirm the heliocentric model.", + "source": "NASA", + "relevance": 0.99, + "stance": "supporting" + } + ] + + verdict = await writer.run(claim=claim, evidence=evidence, detail_level="detailed") + + assert any(source.lower().startswith("http") == False for source in verdict.sources) + assert "nasa" in " ".join(verdict.sources).lower() + assert "sun" in verdict.explanation.lower() or "earth" in verdict.explanation.lower() + +@pytest.mark.asyncio +async def test_confidence_score_considers_evidence_relevance(): + writer = VerdictWriter() + claim = "Cats are better pets than dogs." + evidence = [ + {"content": "Cats require less maintenance.", "source": "PetGuide", "relevance": 0.9, "stance": "supporting"}, + {"content": "Dogs are more loyal and emotionally supportive.", "source": "DogWorld", "relevance": 0.85, "stance": "contradicting"}, + ] + verdict = await writer.run(claim=claim, evidence=evidence, detail_level="standard") + assert 0.4 <= verdict.confidence <= 0.6 # Mixed evidence should yield moderate confidence + +@pytest.mark.asyncio +async def test_explanation_maintains_political_neutrality(): + writer = VerdictWriter() + claim = "Voter ID laws reduce election fraud." + evidence = [ + {"content": "Some studies suggest voter ID laws deter fraud.", "source": "Heritage Foundation", "relevance": 0.8, "stance": "supporting"}, + {"content": "Other studies show minimal fraud cases regardless of ID laws.", "source": "Brennan Center", "relevance": 0.9, "stance": "contradicting"}, + ] + verdict = await writer.run(claim=claim, evidence=evidence, detail_level="detailed") + explanation = verdict.explanation.lower() + assert "republican" not in explanation + assert "democrat" not in explanation + assert "bias" not in explanation + +@pytest.mark.asyncio +async def test_alternative_perspectives_are_included(): + writer = VerdictWriter() + claim = "Electric cars are better for the environment." + evidence = [ + {"content": "EVs emit less CO2 over their lifetime.", "source": "EPA", "relevance": 0.9, "stance": "supporting"}, + {"content": "Battery production has environmental impacts.", "source": "Nature", "relevance": 0.8, "stance": "contradicting"}, + ] + verdict = await writer.run(claim=claim, evidence=evidence, detail_level="detailed") + explanation = verdict.explanation.lower() + assert "battery" in explanation or "production" in explanation + assert any(term in explanation for term in ["emissions", "co2", "co₂", "co 2"]) + +@pytest.mark.asyncio +async def test_explanation_detail_levels_vary(): + writer = VerdictWriter() + claim = "Electric cars are better for the environment than gas cars." + evidence = [ + { + "content": "Electric vehicles produce fewer greenhouse gas emissions over their lifetime.", + "source": "EPA", + "relevance": 0.95, + "stance": "supporting" + }, + { + "content": "Battery production for electric vehicles involves mining and emissions.", + "source": "Nature", + "relevance": 0.8, + "stance": "contradicting" + } + ] + + brief = await writer.run(claim=claim, evidence=evidence, detail_level="brief") + standard = await writer.run(claim=claim, evidence=evidence, detail_level="standard") + detailed = await writer.run(claim=claim, evidence=evidence, detail_level="detailed") + + # Ensure increasing richness in explanation + assert len(brief.explanation.split()) < len(standard.explanation.split()) < len(detailed.explanation.split()) + + # Optional: Check sources and content presence in detailed output + assert len(detailed.sources) >= 2 + explanation = detailed.explanation.lower() + assert "battery" in explanation + assert "emissions" in explanation or "co2" in explanation diff --git a/src/verifact_agents/verdict_writer.py b/src/verifact_agents/verdict_writer.py index 3ffd7f2..5fe98fd 100644 --- a/src/verifact_agents/verdict_writer.py +++ b/src/verifact_agents/verdict_writer.py @@ -1,73 +1,157 @@ import os -from typing import Literal +from typing import Literal, Optional from pydantic import BaseModel, Field - -from agents import Agent - +from agents import Agent, Runner class Verdict(BaseModel): - """A verdict on a claim based on evidence.""" + """A verdict on a claim based on evidence. + + Attributes: + claim: The original claim being fact-checked + verdict: The verdict on the claim (true, false, partially true, or unverifiable) + confidence: Confidence in the verdict (0-1) + explanation: Explanation of the verdict with reasoning and citations + sources: List of sources used to reach the verdict + alternative_perspectives: Optional list of alternative viewpoints when evidence is mixed + """ claim: str verdict: Literal["true", "false", "partially true", "unverifiable"] = Field( description="The verdict on the claim: true, false, partially true, or unverifiable" ) - confidence: float = Field(description="Confidence in the verdict (0-1)", ge=0.0, le=1.0) - explanation: str = Field(description="Detailed explanation of the verdict with reasoning") - sources: list[str] = Field(description="List of sources used to reach the verdict", min_items=1) + confidence: float = Field( + description="Confidence in the verdict (0-1)", ge=0.0, le=1.0 + ) + explanation: str = Field( + description="Explanation of the verdict with reasoning and citations" + ) + sources: list[str] = Field( + description="List of sources used to reach the verdict", min_length=1 + ) + alternative_perspectives: Optional[list[str]] = Field( + description="Alternative perspectives or interpretations when evidence is mixed", + default_factory=list + ) PROMPT = """ You are a verdict writing agent. Your job is to analyze evidence and determine -the accuracy of a claim, providing a detailed explanation and citing sources. - -Your verdict should: -1. Classify the claim as true, false, partially true, or unverifiable -2. Assign a confidence score (0-1) -3. Provide a detailed explanation of your reasoning -4. Cite all sources used -5. Summarize key evidence - -Guidelines for evidence assessment: -- Base your verdict solely on the provided evidence -- Weigh contradicting evidence according to source credibility and relevance -- Consider the relevance score (0-1) as an indicator of how directly the evidence addresses the claim -- Treat higher relevance and credibility sources as more authoritative -- Evaluate stance ("supporting", "contradicting", "neutral") for each piece of evidence -- When sources conflict, prefer more credible, more recent, and more directly relevant sources -- Identify consensus among multiple independent sources as especially strong evidence - -Guidelines for confidence scoring: -- Assign high confidence (0.8-1.0) only when evidence is consistent, highly credible, and comprehensive -- Use medium confidence (0.5-0.79) when evidence is mixed or from fewer sources -- Use low confidence (0-0.49) when evidence is minimal, outdated, or from less credible sources -- When evidence is insufficient, label as "unverifiable" with appropriate confidence based on limitations -- For partially true claims, explain precisely which parts are true and which are false - -Guidelines for explanations: Provide a 1-2 sentence summary focusing on core evidence only - -Your explanation must be: -- Clear and accessible to non-experts -- Factual rather than judgmental -- Politically neutral and unbiased -- Properly cited with all sources attributed -- Transparent about limitations and uncertainty - -When evidence is mixed or contradictory, clearly present the different perspectives -and explain how you reached your conclusion based on the balance of evidence. - -For your output, provide: -- claim: The claim you are fact-checking -- verdict: The verdict on the claim: true, false, partially true, or unverifiable -- confidence: A score from 0.0 to 1.0 indicating your confidence in the verdict -- explanation: A 1-2 sentence summary focusing on core evidence only -- sources: A list of sources used to reach the verdict +the accuracy of a claim, providing a clear explanation and citing sources. + +--- + +Claim: {claim} + +Evidence provided: +{evidence} + +Explanation detail level: {detail_level} + +--- + +You must output: +- claim: The original claim being fact-checked +- verdict: true, false, partially true, or unverifiable +- confidence: from 0.0 to 1.0 +- explanation: A clear explanation tailored to the detail level +- sources: List of sources cited in the explanation +- alternative_perspectives: List of alternative viewpoints when evidence is mixed + +--- + +Instructions: + +1. Assess the credibility and relevance of each evidence entry. +2. Classify evidence by stance: supporting, contradicting, neutral. +3. Weigh supporting vs. contradicting evidence and determine consensus. +4. Use source credibility (e.g., peer-reviewed > blog) and relevance score. +5. Cite sources in explanation as [n] and include the list of sources. +6. Adjust the explanation based on detail level: + - brief: One sentence summary with 1–2 sources (max 30 words) + - standard: 2–3 sentences with key evidence + - detailed: Comprehensive analysis with all evidence and perspectives +7. For confidence scoring: + - 0.9-1.0: Overwhelming evidence from multiple high-credibility sources, all supporting + - 0.7-0.89: Strong evidence from reliable sources, mostly supporting + - 0.4-0.69: Mixed evidence or limited sources + - 0.1-0.39: Weak or contradictory evidence + - 0.0-0.09: Insufficient evidence + IMPORTANT: When evidence is mixed (both supporting and contradicting), confidence MUST be between 0.4 and 0.6 + +Guidelines: +- Maintain political neutrality +- Write for a non-expert audience +- Be transparent about uncertainty or lack of evidence +- If evidence is mixed, include alternative perspectives +- Consider evidence relevance when assigning confidence scores +- For mixed evidence, confidence MUST be between 0.4 and 0.6 """ -verdict_writer_agent = Agent( - name="VerdictWriter", - instructions=PROMPT, - output_type=Verdict, - tools=[], - model=os.getenv("VERDICT_WRITER_MODEL"), -) \ No newline at end of file +def format_evidence_block(evidence: list[dict]) -> str: + """Converts a list of evidence dicts to a readable block for the LLM. + + Args: + evidence: List of evidence dictionaries containing content, source, relevance, and stance + + Returns: + Formatted string with numbered evidence entries + """ + formatted = [] + for i, e in enumerate(evidence, 1): + formatted.append( + f"{i}. [{e.get('stance')}] \"{e.get('content')}\" — {e.get('source')} (relevance: {e.get('relevance', 1.0)})" + ) + return "\n".join(formatted) + +class VerdictWriter: + """Agent responsible for generating verdicts based on evidence. + + The VerdictWriter analyzes evidence and produces well-reasoned verdicts with + proper citations and explanations. It supports multiple detail levels and + includes alternative perspectives when evidence is mixed. + + Attributes: + agent: The underlying Agent instance + """ + + def __init__(self, model: Optional[str] = None): + """Initialize the VerdictWriter agent. + + Args: + model: Optional model name to override the default + """ + self.agent = Agent( + name="VerdictWriter", + instructions=PROMPT, + output_type=Verdict, + tools=[], + model=model or os.getenv("VERDICT_WRITER_MODEL"), + ) + + async def run( + self, + claim: str, + evidence: list[dict], + detail_level: Literal["brief", "standard", "detailed"] = "standard", + ) -> Verdict: + """Generate a verdict for a claim based on evidence. + + Args: + claim: The claim to fact-check + evidence: List of evidence dictionaries with content, source, relevance, and stance + detail_level: Level of detail for the explanation (brief, standard, or detailed) + + Returns: + Verdict object containing the verdict, confidence, explanation, and sources + """ + prompt_input = PROMPT.format( + claim=claim, + evidence=format_evidence_block(evidence), + detail_level=detail_level, + ) + + result = await Runner.run(self.agent, prompt_input) + verdict = result.final_output_as(Verdict) + verdict.claim = claim # Ensure claim is set correctly + verdict.sources = list(set(filter(None, verdict.sources))) # Deduplicate and remove empty + return verdict diff --git a/test_agents.py b/test_agents.py new file mode 100644 index 0000000..1006278 --- /dev/null +++ b/test_agents.py @@ -0,0 +1,14 @@ +from agents import Agent as OpenAIAgent +from src.verifact_agents import ClaimDetector + +def test_imports(): + print("Successfully imported OpenAI Agent") + print("Successfully imported VeriFact ClaimDetector") + +def test_agent_imports(): + """Test that agent classes can be imported successfully.""" + assert OpenAIAgent is not None + assert ClaimDetector is not None + +if __name__ == "__main__": + test_imports() \ No newline at end of file diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..104e17f --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,37 @@ +import pytest +import os +import warnings +from typing import Generator, Dict, Any +from src.verifact_agents.verdict_writer import VerdictWriter + +# Suppress the starlette multipart deprecation warning +warnings.filterwarnings("ignore", category=PendingDeprecationWarning, module="starlette.formparsers") + +@pytest.fixture(scope="session") +def test_evidence() -> Dict[str, Any]: + """Provide sample evidence for testing.""" + return { + "content": "This is a sample evidence content for testing purposes.", + "source": "Test Source", + "relevance": 0.9, + "stance": "supporting" + } + +@pytest.fixture(scope="session") +def test_claim() -> str: + """Provide a sample claim for testing.""" + return "This is a test claim that needs verification." + +@pytest.fixture(scope="session") +def verdict_writer() -> Generator[VerdictWriter, None, None]: + """Create a VerdictWriter instance for testing.""" + writer = VerdictWriter() + yield writer + +@pytest.fixture(autouse=True) +def setup_test_environment(): + """Set up test environment variables.""" + os.environ["ENVIRONMENT"] = "test" + os.environ["LOG_LEVEL"] = "DEBUG" + yield + # Cleanup after tests if needed \ No newline at end of file diff --git a/tests/verifact_agents/test_verdict_writer.py b/tests/verifact_agents/test_verdict_writer.py new file mode 100644 index 0000000..40af4ae --- /dev/null +++ b/tests/verifact_agents/test_verdict_writer.py @@ -0,0 +1,138 @@ +import pytest +from src.verifact_agents.verdict_writer import VerdictWriter, Verdict + +@pytest.mark.asyncio +async def test_verdict_writer_returns_valid_verdict(): + writer = VerdictWriter() + claim = "Bananas are a good source of potassium." + evidence = [ + { + "content": "Bananas contain around 422 mg of potassium per medium fruit, making them a good dietary source.", + "source": "Healthline", + "relevance": 0.9, + "stance": "supporting" + }, + { + "content": "Potatoes and beans contain more potassium than bananas, but bananas are still a decent source.", + "source": "Harvard School of Public Health", + "relevance": 0.8, + "stance": "neutral" + } + ] + + verdict = await writer.run(claim=claim, evidence=evidence, detail_level="standard") + + assert isinstance(verdict, Verdict) + assert verdict.claim == claim + assert verdict.verdict in ["true", "false", "partially true", "unverifiable"] + assert 0.0 <= verdict.confidence <= 1.0 + assert isinstance(verdict.explanation, str) and len(verdict.explanation.strip()) > 0 + assert isinstance(verdict.sources, list) and all(isinstance(s, str) for s in verdict.sources) + assert len(verdict.sources) > 0 + +@pytest.mark.asyncio +async def test_verdict_writer_brief_detail(): + writer = VerdictWriter() + claim = "Water freezes at 0 degrees Celsius." + evidence = [ + { + "content": "Under standard atmospheric conditions, water freezes at 0°C.", + "source": "Britannica", + "relevance": 0.95, + "stance": "supporting" + } + ] + + verdict = await writer.run(claim=claim, evidence=evidence, detail_level="brief") + + assert isinstance(verdict.explanation, str) + assert len(verdict.explanation.split()) <= 30 # brief explanation is usually short + +@pytest.mark.asyncio +async def test_verdict_writer_detailed_includes_sources(): + writer = VerdictWriter() + claim = "The Earth revolves around the Sun." + evidence = [ + { + "content": "Astronomical evidence and observations confirm the heliocentric model.", + "source": "NASA", + "relevance": 0.99, + "stance": "supporting" + } + ] + + verdict = await writer.run(claim=claim, evidence=evidence, detail_level="detailed") + + assert any(source.lower().startswith("http") == False for source in verdict.sources) + assert "nasa" in " ".join(verdict.sources).lower() + assert "sun" in verdict.explanation.lower() or "earth" in verdict.explanation.lower() + +@pytest.mark.asyncio +async def test_confidence_score_considers_evidence_relevance(): + writer = VerdictWriter() + claim = "Cats are better pets than dogs." + evidence = [ + {"content": "Cats require less maintenance.", "source": "PetGuide", "relevance": 0.9, "stance": "supporting"}, + {"content": "Dogs are more loyal and emotionally supportive.", "source": "DogWorld", "relevance": 0.85, "stance": "contradicting"}, + ] + verdict = await writer.run(claim=claim, evidence=evidence, detail_level="standard") + assert 0.4 <= verdict.confidence <= 0.6 # Mixed evidence should yield moderate confidence + +@pytest.mark.asyncio +async def test_explanation_maintains_political_neutrality(): + writer = VerdictWriter() + claim = "Voter ID laws reduce election fraud." + evidence = [ + {"content": "Some studies suggest voter ID laws deter fraud.", "source": "Heritage Foundation", "relevance": 0.8, "stance": "supporting"}, + {"content": "Other studies show minimal fraud cases regardless of ID laws.", "source": "Brennan Center", "relevance": 0.9, "stance": "contradicting"}, + ] + verdict = await writer.run(claim=claim, evidence=evidence, detail_level="detailed") + explanation = verdict.explanation.lower() + assert "republican" not in explanation + assert "democrat" not in explanation + assert "bias" not in explanation + +@pytest.mark.asyncio +async def test_alternative_perspectives_are_included(): + writer = VerdictWriter() + claim = "Electric cars are better for the environment." + evidence = [ + {"content": "EVs emit less CO2 over their lifetime.", "source": "EPA", "relevance": 0.9, "stance": "supporting"}, + {"content": "Battery production has environmental impacts.", "source": "Nature", "relevance": 0.8, "stance": "contradicting"}, + ] + verdict = await writer.run(claim=claim, evidence=evidence, detail_level="detailed") + explanation = verdict.explanation.lower() + assert "battery" in explanation or "production" in explanation + assert any(term in explanation for term in ["emissions", "co2", "co₂", "co 2"]) + +@pytest.mark.asyncio +async def test_explanation_detail_levels_vary(): + writer = VerdictWriter() + claim = "Electric cars are better for the environment than gas cars." + evidence = [ + { + "content": "Electric vehicles produce fewer greenhouse gas emissions over their lifetime.", + "source": "EPA", + "relevance": 0.95, + "stance": "supporting" + }, + { + "content": "Battery production for electric vehicles involves mining and emissions.", + "source": "Nature", + "relevance": 0.8, + "stance": "contradicting" + } + ] + + brief = await writer.run(claim=claim, evidence=evidence, detail_level="brief") + standard = await writer.run(claim=claim, evidence=evidence, detail_level="standard") + detailed = await writer.run(claim=claim, evidence=evidence, detail_level="detailed") + + # Ensure increasing richness in explanation + assert len(brief.explanation.split()) < len(standard.explanation.split()) < len(detailed.explanation.split()) + + # Optional: Check sources and content presence in detailed output + assert len(detailed.sources) >= 2 + explanation = detailed.explanation.lower() + assert "battery" in explanation + assert "emissions" in explanation or "co2" in explanation \ No newline at end of file