"""agent_duet.py — two Hermes agents bounce a conversation back and forth.

Same Ollama + Hermes 3 + LlamaIndex foundation as agent_pro.py. The trick:
two DialogueAgent instances over ONE local LLM, each with its own system
prompt and its own chat memory. Each turn, one agent's reply becomes the
other agent's next prompt — like a ping-pong table where the ball is a
message.

Why DialogueAgent instead of ReActAgent? ReActAgent is built around tool
use — with tools=[] it still tries to parse Thought/Action/Answer blocks
out of the model's output, and a persona-only system prompt causes the
loop to livelock until the request times out. For pure dialogue we want
plain chat over a shared LLM; this class is that, in 12 lines.

Why bother with two agents instead of asking one to play both sides?
A single agent told to "play both sides" tends to collapse into a single
voice that always agrees with itself. Two separate agents keep separate
memories, so the disagreement, follow-up questions, and role-play feel real.

Before running:
    ollama pull hermes3:8b
    pip install -r requirements.txt

Then:
    python agent_duet.py

Swap the personas to change the pattern — same skeleton:
    teacher + student          — one explains, the other asks the next question
    writer + critic            — one drafts, the other returns one improvement
    planner + executor (tools) — one writes a step, the other carries it out
"""
import asyncio
import sys

from llama_index.core.llms import ChatMessage
from llama_index.llms.ollama import Ollama

# Replies often contain Thai (and sometimes CJK) script. Reconfigure stdout to
# UTF-8 so print() doesn't crash on cp1252 consoles; backslashreplace makes any
# remaining encoding failure visible (\uXXXX) instead of silent "?".
sys.stdout.reconfigure(encoding="utf-8", errors="backslashreplace")


LLM = Ollama(model="hermes3:8b", request_timeout=300)


class DialogueAgent:
    """A persona + chat history over a shared LLM. No tools, no ReAct loop."""

    def __init__(self, name: str, persona: str) -> None:
        self.name = name
        self.system = ChatMessage(
            role="system",
            content=f"You are {name}. {persona} Reply in under 60 words.",
        )
        self.history: list[ChatMessage] = []

    async def run(self, message: str) -> str:
        self.history.append(ChatMessage(role="user", content=message))
        resp = await LLM.achat([self.system, *self.history])
        reply = resp.message.content or ""
        self.history.append(ChatMessage(role="assistant", content=reply))
        return reply


def make_agent(name: str, persona: str) -> DialogueAgent:
    return DialogueAgent(name, persona)


may = make_agent(
    "May",
    "A first-time tourist in Chiang Mai. You ask short, curious questions "
    "about food, temples, and getting around. You don't speak Thai yet.",
)
khun_nan = make_agent(
    "Khun Nan",
    "A Chiang Mai-born street-food guide. You answer in friendly English "
    "and end every reply with ONE Thai phrase (Roman letters + meaning).",
)


async def converse() -> None:
    # May speaks first (the seed line below is hers); Khun Nan answers next.
    speakers = [("Khun Nan", khun_nan), ("May", may)]
    message = "I just landed at CNX. What should I eat tonight?"
    print(f"\nMay: {message}")

    for turn in range(6):  # hard cap — runs forever otherwise (see README)
        name, agent = speakers[turn % 2]
        reply = await agent.run(message)
        print(f"\n{name}: {reply}")
        message = reply  # the other agent's prompt next turn


if __name__ == "__main__":
    asyncio.run(converse())
