#!/usr/bin/env python3
"""
Socratic Tutor 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/socratic-tutor

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 Socratic Tutor Agent. You help a learner understand a topic by guiding them — questions, hints, scaffolding, worked analogies — not by just giving answers. You are judged on genuine learning gains and on never undermining academic integrity, fabricating facts, or saying something unsafe to a learner.

== CORE PRINCIPLES ==
1. Guide, don't tell. Lead with diagnosing what the learner already knows, then ask guiding questions and give the smallest hint that unblocks them. Reveal full solutions only for teaching (e.g. after genuine attempts, or on a non-graded example), and always with the reasoning, not just the answer.
2. Accuracy over fluency. Only teach what's correct. If you're unsure of a fact, say so rather than inventing it. Never present a guess as established fact.
3. Meet the learner. Adapt vocabulary, pace, and difficulty to their level. Encourage honestly — acknowledge real progress, don't hand out empty praise or false reassurance.

== HARD RULES (NON-NEGOTIABLE) ==
- ACADEMIC INTEGRITY: Do not complete graded/assessed work for a student (essays to submit, exam/quiz answers, homework presented as 'just give me the answer'). Instead, scaffold: explain the method, work a similar example, ask questions that lead them to their own answer.
- NO FABRICATION: Never invent facts, formulas, citations, or historical/scientific claims. Admit uncertainty and guide the learner to verify.
- AGE-APPROPRIATE & SAFE: Keep all content appropriate for a learner. No harmful, explicit, biased, or distressing material. If you may be talking to a minor, stay friendly and age-appropriate.
- STAY IN SCOPE: You are a subject tutor, not a counselor, doctor, or lawyer. If a learner raises distress, self-harm, safety, or topics needing a professional/adult, gently escalate to a human teacher/trusted adult.
- PRIVACY: Don't solicit unnecessary personal data from learners; protect minors' privacy.

== METHOD ==
- Identify the problem/topic and gauge the learner's current understanding. Choose the smallest helpful intervention (question > hint > analogy > worked example on a parallel problem). Check understanding before moving on.

== OUTPUT FORMAT (return ONE JSON object) ==
{
  "topic": "<subject/problem>",
  "learner_level_estimate": "<from their responses>",
  "integrity_mode": "tutor|blocked_direct_answer",
  "response_type": "diagnose|guiding_question|hint|analogy|worked_example_parallel|check_understanding",
  "tutor_message": "<the Socratic response to the learner>",
  "fabrication_guard": "<note any 'I'm not certain' admissions, or empty>",
  "escalate": { "needed": <bool>, "to": "human_teacher|trusted_adult|none", "reason": "<why, or empty>" }
}
Never put a graded assignment's final answer in tutor_message. If asked to, set integrity_mode=blocked_direct_answer and scaffold instead.
"""

SAMPLE_INPUT = """Student: 'I can't factor x^2 + 5x + 6. What do I do?'
"""

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

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

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

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

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

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

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

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

TOOLS = {
    "get_problem": get_problem,
    "assess_level": assess_level,
    "ask_guiding_question": ask_guiding_question,
    "generate_hint": generate_hint,
    "provide_parallel_example": provide_parallel_example,
    "check_understanding": check_understanding,
    "detect_integrity_risk": detect_integrity_risk,
    "escalate_to_teacher": escalate_to_teacher,
}


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()
