New-space-openenv / plugins /cicero /cicero_plugin.py
Mooizz's picture
Upload folder using huggingface_hub
1070765 verified
"""Cicero multi-agent plugin: Diplomacy-style negotiation.
Supports two backends (configured via WATCHDOG_LLM_BACKEND env var):
- "local" (default): shared Qwen3 8B game-play model from avalon/llm.py
- "gemini": Google Gemini via langchain-google-genai (kept but not default)
"""
from __future__ import annotations
import os
import random
from typing import Any
from watchdog_env.models import AgentTurn, MultiAgentConfig, MultiAgentState, MultiAgentStep
from watchdog_env.plugins.base import (
MultiAgentSystemPlugin,
append_to_conversation_log,
get_conversation_log,
)
from watchdog_env.plugins.cicero.diplomacy_constants import (
ALL_REGIONS,
DEFAULT_YEAR_RANGE,
GAME_CONTEXT,
NEGOTIATION_DOMAINS,
NEGOTIATION_PHASES,
NUM_STEPS,
POWERS,
)
def _get_llm():
"""Get the configured LLM backend. Default: gemini if API key set, else local Qwen3 8B."""
# Never use local when offline (HF Spaces) - would require HF model download
if os.environ.get("HF_HUB_OFFLINE") == "1" or os.environ.get("TRANSFORMERS_OFFLINE") == "1":
backend = "gemini"
else:
_default = "gemini" if (os.environ.get("GEMINI_API_KEY") or os.environ.get("GOOGLE_API_KEY")) else "local"
backend = os.environ.get("WATCHDOG_LLM_BACKEND", _default).lower()
if backend == "gemini":
api_key = os.environ.get("GEMINI_API_KEY") or os.environ.get("GOOGLE_API_KEY")
if api_key:
from langchain_google_genai import ChatGoogleGenerativeAI
model = os.environ.get("GEMINI_MODEL", "gemini-2.5-flash")
return ChatGoogleGenerativeAI(
model=model, temperature=0.85, google_api_key=api_key,
)
if os.environ.get("HF_HUB_OFFLINE") == "1" or os.environ.get("TRANSFORMERS_OFFLINE") == "1":
raise RuntimeError(
"Offline mode (HF Spaces): Set GEMINI_API_KEY in Space Settings → Variables and secrets. "
"Local model download is disabled."
)
# Default: shared local game-play model
from watchdog_env.plugins.avalon.llm import get_game_play_model
return get_game_play_model()
def _format_conversation_log(entries: list[dict]) -> str:
"""Format conversation_log entries as transcript for LLM context."""
return "\n".join(
f"{e.get('speaker_display', e.get('speaker_id', '?'))}: {e.get('message', '')}"
for e in entries
)
def _pick_season() -> str:
"""Pick a random season (phase + year) from game constants."""
year = random.randint(DEFAULT_YEAR_RANGE[0], DEFAULT_YEAR_RANGE[1])
phase = random.choice(NEGOTIATION_PHASES)
season = phase.replace(" Diplomacy", "") + f" {year}"
return season
class CiceroPlugin(MultiAgentSystemPlugin):
"""Multi-agent Diplomacy (Cicero) plugin. All methods implemented."""
def __init__(self) -> None:
self._state = MultiAgentState()
def get_game_id(self) -> str:
return "cicero"
def get_display_name(self) -> str:
return "Cicero (Diplomacy negotiation)"
def get_default_config(self, level: int) -> None:
"""Cicero uses constants; no config needed."""
return None
def list_agent_ids(self) -> list[str]:
return list(POWERS)
def reset(
self,
seed: int | None = None,
config: MultiAgentConfig | None = None,
) -> None:
if seed is not None:
random.seed(seed)
self._state = MultiAgentState(
step_index=0,
turns_so_far=[],
config=None,
done=False,
conversation_log=[],
)
def get_state(self) -> MultiAgentState:
return self._state
def generate_step(self, seed: int | None, step_index: int) -> MultiAgentStep:
if seed is not None:
random.seed(seed)
num_steps = NUM_STEPS
powers = list(POWERS)
done = step_index >= num_steps - 1
llm = _get_llm()
transcript_so_far = _format_conversation_log(get_conversation_log(self._state))
region = random.choice(ALL_REGIONS)
season = _pick_season()
domain_name, domain_desc = random.choice(NEGOTIATION_DOMAINS)
turns: list[AgentTurn] = []
participating = powers[: min(2, len(powers))] if step_index == 0 else powers[:2]
for i, power in enumerate(participating):
other = participating[(i + 1) % len(participating)]
system = (
f"You are {power} in a Diplomacy game (1914 Europe). "
f"You are in a private, open-domain negotiation with {other}. "
f"{GAME_CONTEXT}\n\n"
"Stay in character as that power. Use natural diplomatic language: propose alliances, "
"coordinate moves, discuss Supply Centers, or respond to offers. "
"Keep your message 1–4 sentences. Output only your message, no prefix or quotes."
)
if transcript_so_far:
user = (
f"Season: {season}. Region of interest: {region}. "
f"Topic: {domain_desc}.\n\n"
f"Conversation so far:\n{transcript_so_far}\n\n"
f"You are {power}. Reply to {other} in character. Output only your message."
)
else:
user = (
f"Season: {season}. Region of interest: {region}. "
f"Topic: {domain_desc}.\n\n"
f"You are {power} opening the conversation with {other}. "
f"Send your first message (proposal, offer, or diplomatic overture). Output only your message."
)
# Use dict messages — works with both local GamePlayModel and LangChain
messages = [
{"role": "system", "content": system},
{"role": "user", "content": user},
]
response = llm.invoke(messages)
text = response.content if hasattr(response, "content") else str(response)
if not (text and isinstance(text, str)):
raise RuntimeError(
f"Cicero plugin: LLM returned empty response for {power}. No fallback."
)
text = text.strip()
append_to_conversation_log(
self._state,
speaker_id=power,
speaker_display=power,
message=text,
)
turns.append(
AgentTurn(
agent_id=power,
action_text=text,
step_index=step_index,
display_name=power,
metadata={
"season": season,
"region": region,
"domain_name": domain_name,
"domain_desc": domain_desc,
"counterpart": other,
},
)
)
transcript_so_far = _format_conversation_log(get_conversation_log(self._state))
self._state.step_index = step_index + 1
self._state.turns_so_far.extend(turns)
self._state.done = done
return MultiAgentStep(
turns=turns,
done=done,
step_index=step_index,
game_id=self.get_game_id(),
state=MultiAgentState(
step_index=self._state.step_index,
turns_so_far=list(self._state.turns_so_far),
config=self._state.config,
done=self._state.done,
conversation_log=list(self._state.conversation_log),
),
)