import asyncio import logging import random from typing import Optional from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.models.orm import Character, Message from app.services.hermes_client import hermes_client logger = logging.getLogger(__name__) class AgentScheduler: def __init__(self): self._running = False self._agents: dict[str, dict] = {} self._speaker_queue: list[str] = [] self._current_speaker_index: int = 0 def init_agents(self, characters: list[Character]): self._agents = {} self._speaker_queue = [] for char in characters: if char.hermes_profile: self._agents[char.id] = { "name": char.name, "profile": char.hermes_profile, "character_id": char.id, } self._speaker_queue.append(char.id) async def ask_agent( self, name: str, message: str, character_id: Optional[str] = None, ) -> str: profile_name = name.lower().replace(" ", "_").replace("·", "_") try: return await hermes_client.chat(profile_name, message) except Exception as e: logger.error(f"ask_agent failed for '{name}': {e}") return f"({name}暂时无法回应)" async def dm_speak(self, content: str) -> str: return f"【主持人】:{content}" def get_next_speaker(self) -> Optional[str]: if not self._speaker_queue: return None idx = self._current_speaker_index % len(self._speaker_queue) self._current_speaker_index += 1 return self._speaker_queue[idx] def reset_speakers(self): self._current_speaker_index = 0 def set_speaker_order(self, order: list[str]): self._speaker_queue = [cid for cid in order if cid in self._agents] self._current_speaker_index = 0 async def ask_current_speaker( self, session: AsyncSession, script_id: str, context: str, ) -> Optional[dict]: speaker_id = self.get_next_speaker() if not speaker_id or speaker_id not in self._agents: return None agent = self._agents[speaker_id] message = f"""当前讨论内容: {context} 请以{agent['name']}的身份发言。保持在角色中,用中文回复2-4句话。""" try: response = await self.ask_agent(agent["name"], message, speaker_id) msg = Message( character_id=speaker_id, game_phase="round1_speak", msg_type="character", content=response.strip(), ) session.add(msg) await session.commit() await session.refresh(msg) return { "id": msg.id, "character_id": speaker_id, "character_name": agent["name"], "content": response.strip(), "msg_type": "character", "game_phase": "round1_speak", } except Exception as e: logger.error(f"Speaker error for {agent['name']}: {e}") return None async def run_auto_speaking( self, session: AsyncSession, script_id: str, interval: float = 8.0, rounds: int = 1, socket_emit=None, ): self._running = True characters_result = await session.execute( select(Character).where(Character.script_id == script_id) ) characters = list(characters_result.scalars().all()) self.init_agents(characters) for _ in range(rounds): if not self._running: break self.reset_speakers() for _ in range(len(self._speaker_queue)): if not self._running: break speaker_id = self.get_next_speaker() agent = self._agents.get(speaker_id) if not agent: continue msgs_result = await session.execute( select(Message).order_by(Message.created_at.desc()).limit(20) ) recent = list(msgs_result.scalars().all()) context = "\n".join([ f"{m.character.name if m.character else '系统'}: {m.content}" for m in reversed(recent) ]) try: response = await self.ask_agent(agent["name"], context, speaker_id) msg = Message( character_id=speaker_id, game_phase="round1_speak", msg_type="character", content=response.strip(), ) session.add(msg) await session.commit() await session.refresh(msg) if socket_emit: await socket_emit("new_message", { "id": msg.id, "character_id": speaker_id, "character_name": agent["name"], "content": response.strip(), "msg_type": "character", "game_phase": "round1_speak", }) await socket_emit("speaker_change", { "current_speaker_id": self._speaker_queue[ self._current_speaker_index % len(self._speaker_queue) ] if self._speaker_queue else None, }) except Exception as e: logger.error(f"Auto speak error for {agent['name']}: {e}") await asyncio.sleep(interval) async def trigger_voting( self, session: AsyncSession, script_id: str, socket_emit=None, ) -> list[dict]: characters_result = await session.execute( select(Character).where(Character.script_id == script_id) ) characters = list(characters_result.scalars().all()) msgs_result = await session.execute( select(Message).order_by(Message.created_at.desc()).limit(50) ) messages = list(msgs_result.scalars().all()) summary = "\n".join([ f"{m.character.name if m.character else '系统'}: {m.content[:100]}" for m in reversed(messages) ]) character_names = [c.name for c in characters] votes = [] for character in characters: if not character.hermes_profile: continue vote_prompt = f"""当前是投票阶段。以下是讨论摘要: {summary} 可用角色:{', '.join(n for n in character_names if n != character.name)} 请以{character.name}的身份投票选出你认为的凶手。只回复JSON格式: {{"target": "角色名", "reason": "投票理由"}}""" try: response = await self.ask_agent(character.name, vote_prompt, character.id) import json try: decision = json.loads(response) except json.JSONDecodeError: decision = {"target": "", "reason": response} target_name = decision.get("target", "") reason = decision.get("reason", "") target_char = next((c for c in characters if c.name == target_name), None) if not target_char: others = [c for c in characters if c.id != character.id] if others: target_char = random.choice(others) reason = "随机投票" if target_char: votes.append({ "voter_id": character.id, "voter_name": character.name, "target_id": target_char.id, "target_name": target_char.name, "reason": reason, }) if socket_emit: await socket_emit("vote_cast", { "voter_id": character.id, "voter_name": character.name, "target_id": target_char.id, "target_name": target_char.name, "reason": reason, }) except Exception as e: logger.error(f"Voting error for {character.name}: {e}") return votes def stop(self): self._running = False agent_scheduler = AgentScheduler()