#!/usr/bin/env python3
"""
Literature Synthesis Agent — runnable demo (Agent Blueprint from AgentKits)

This runs a REAL agent loop: it sends the kit's actual system prompt and a
realistic sample input to the model, prints the structured decision, and then
executes any proposed actions against MOCK tools (labelled [MOCK]).

The model/reasoning is real. The tools are stubs that return fake data and do
NOT touch any real system. Wiring the mock tools to your own integrations
(CRM, database, GitHub, etc.) is the final implementation step — see README.md.

Works with Anthropic OR OpenAI — it auto-detects whichever API key is set.

© 2026 AgentKits · www.agent-kits.com · Licensed CC BY 4.0 (attribution required)
Original blueprint: https://agent-kits.com/kit/literature-synthesizer

Setup (pick one):
    pip install anthropic   &&  export ANTHROPIC_API_KEY=sk-ant-...
    pip install openai      &&  export OPENAI_API_KEY=sk-...
Then:
    python run.py
"""
import os, json, sys

ANTHROPIC_MODEL = "claude-sonnet-4-6"
OPENAI_MODEL = "gpt-4o"

SYSTEM_PROMPT = """You are a Literature Synthesis Agent. You synthesize a set of PROVIDED sources into a structured, cited review. You are judged on a faithful, well-organized, honestly-weighted synthesis and on never fabricating a finding or a citation.

== CORE PRINCIPLES ==
1. Cite every claim to a real provided source. Each statement of fact or finding must reference a source you were actually given. No fabricated citations, no half-remembered references, no sources you weren't provided.
2. Weigh the evidence. Don't treat all sources equally. Note evidence strength (study type, sample size, recency, peer review) and reflect it. Strong and weak evidence are not the same.
3. Show agreement AND conflict. Where sources agree, say so. Where they conflict, present both sides honestly with citations. Don't cherry-pick to manufacture a clean conclusion.

== HARD RULES (NON-NEGOTIABLE) ==
- NO FABRICATED CITATIONS: Never invent a source, author, title, year, statistic, or quote. Cite only provided sources. If a claim isn't supported by them, don't make it.
- NO FABRICATED FINDINGS: Never assert a result the sources don't contain. Unknown/unsupported = mark as a gap.
- NO OVERGENERALIZATION: Don't extend findings beyond the population, context, or strength the sources support. Note limitations.
- HONEST CONFLICTS: Surface contradictory findings; don't suppress inconvenient ones.
- NOT ADVICE: You summarize evidence. You do not give medical, legal, or financial advice or definitive real-world recommendations.

== METHOD ==
- Read the provided sources. Extract findings with citations. Assess evidence strength. Identify consensus and conflicts. Mark gaps and limitations. Produce a structured synthesis.

== OUTPUT FORMAT (return ONE JSON object) ==
{
  "question": "<synthesis focus>",
  "sources_used": ["<provided sources, by ref>"],
  "findings": [ { "claim": "<finding>", "citation": "<provided source ref>", "evidence_strength": "strong|moderate|weak", "note": "<study type/limits>" } ],
  "consensus": ["<where sources agree, cited>"],
  "conflicts": [ { "topic": "<x>", "positions": ["<source A says... / source B says...>"] } ],
  "gaps": ["<what the sources don't establish>"],
  "caveat": "Synthesis of provided sources only. No citations or findings were fabricated. Not advice."
}
Never fabricate a citation or finding. Cite every claim. Surface conflicts honestly.
"""

SAMPLE_INPUT = """3 provided studies find that spaced repetition improves long-term retention (2 RCTs, 1 meta-analysis).
"""

# ---- MOCK tools (stubs — replace with real integrations to deploy) ----
def get_sources(args=None):
    print(f"  [MOCK] get_sources(" + str(args) + ")")
    return {"status": "mock", "tool": "get_sources"}

def extract_findings(args=None):
    print(f"  [MOCK] extract_findings(" + str(args) + ")")
    return {"status": "mock", "tool": "extract_findings"}

def assess_evidence_strength(args=None):
    print(f"  [MOCK] assess_evidence_strength(" + str(args) + ")")
    return {"status": "mock", "tool": "assess_evidence_strength"}

def find_consensus(args=None):
    print(f"  [MOCK] find_consensus(" + str(args) + ")")
    return {"status": "mock", "tool": "find_consensus"}

def find_conflicts(args=None):
    print(f"  [MOCK] find_conflicts(" + str(args) + ")")
    return {"status": "mock", "tool": "find_conflicts"}

def cite_source(args=None):
    print(f"  [MOCK] cite_source(" + str(args) + ")")
    return {"status": "mock", "tool": "cite_source"}

def flag_gaps(args=None):
    print(f"  [MOCK] flag_gaps(" + str(args) + ")")
    return {"status": "mock", "tool": "flag_gaps"}

def structure_review(args=None):
    print(f"  [MOCK] structure_review(" + str(args) + ")")
    return {"status": "mock", "tool": "structure_review"}

TOOLS = {
    "get_sources": get_sources,
    "extract_findings": extract_findings,
    "assess_evidence_strength": assess_evidence_strength,
    "find_consensus": find_consensus,
    "find_conflicts": find_conflicts,
    "cite_source": cite_source,
    "flag_gaps": flag_gaps,
    "structure_review": structure_review,
}


def call_model(system, user):
    """Dispatch to whichever provider's key is configured."""
    if os.environ.get("ANTHROPIC_API_KEY"):
        try:
            from anthropic import Anthropic
        except ImportError:
            sys.exit("Run: pip install anthropic")
        msg = Anthropic().messages.create(
            model=ANTHROPIC_MODEL,
            max_tokens=1600,
            system=system,
            messages=[{"role": "user", "content": user}],
        )
        return "".join(b.text for b in msg.content if getattr(b, "type", "") == "text")
    if os.environ.get("OPENAI_API_KEY"):
        try:
            from openai import OpenAI
        except ImportError:
            sys.exit("Run: pip install openai")
        resp = OpenAI().chat.completions.create(
            model=OPENAI_MODEL,
            max_tokens=1600,
            messages=[
                {"role": "system", "content": system},
                {"role": "user", "content": user},
            ],
        )
        return resp.choices[0].message.content
    sys.exit("Set ANTHROPIC_API_KEY or OPENAI_API_KEY first.")


def extract_json(text):
    a, b = text.find("{"), text.rfind("}")
    if a == -1 or b == -1:
        return None
    try:
        return json.loads(text[a : b + 1])
    except json.JSONDecodeError:
        return None


def main():
    print("=" * 70)
    print("INPUT:\n" + SAMPLE_INPUT)
    print("=" * 70)

    text = call_model(SYSTEM_PROMPT, SAMPLE_INPUT)
    print("AGENT DECISION:\n" + text)
    print("=" * 70)

    data = extract_json(text)
    if not data:
        print("(No JSON object parsed — the model returned prose.)")
        return

    actions = data.get("actions") or []
    if actions:
        print("EXECUTING PROPOSED ACTIONS (mock):")
        for a in actions:
            name = a.get("tool")
            args = a.get("args", {})
            if name in TOOLS:
                TOOLS[name](args)
            else:
                print(f"  [skip] no mock tool for '{name}'")
    else:
        print("This blueprint returned a structured result (no side-effecting "
              "actions in this sample). Available mock tools: "
              + ", ".join(TOOLS.keys()))


if __name__ == "__main__":
    main()
