Spaces:
Sleeping
Sleeping
| import os, re, json, time, uuid | |
| from dataclasses import dataclass, asdict | |
| from typing import List, Dict | |
| # Don't use a hardcoded fallback - if no API key is set, use demo mode | |
| GOOGLE_API_KEY = os.getenv("GOOGLE_API_KEY") or os.getenv("GENAI_API_KEY") | |
| def ask_real(prompt: str) -> str: | |
| """Use Google AI REST API instead of google-genai SDK to avoid websockets dependency""" | |
| if not GOOGLE_API_KEY: | |
| raise RuntimeError("Real LLM unavailable - missing GOOGLE_API_KEY.") | |
| import requests | |
| # Use v1beta API with gemini-2.5-flash model | |
| url = f"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key={GOOGLE_API_KEY}" | |
| payload = { | |
| "contents": [{ | |
| "parts": [{"text": prompt}] | |
| }] | |
| } | |
| try: | |
| response = requests.post(url, json=payload, timeout=30) | |
| response.raise_for_status() | |
| data = response.json() | |
| # Extract text from response | |
| if "candidates" in data and len(data["candidates"]) > 0: | |
| candidate = data["candidates"][0] | |
| if "content" in candidate and "parts" in candidate["content"]: | |
| parts = candidate["content"]["parts"] | |
| if len(parts) > 0 and "text" in parts[0]: | |
| return parts[0]["text"].strip() | |
| return "(no response)" | |
| except Exception as e: | |
| raise RuntimeError(f"API call failed: {e}") | |
| def _stub_summary(text: str) -> str: | |
| return """- Summary: A public incident prompts officials, executives, and advocates to respond.\n" | |
| "- Impact: Affected users demand remedies, oversight considers penalties.\n" | |
| "- Company: Leadership apologizes and outlines next steps.\n" | |
| "- Regulator: Signals investigation and potential sanctions.\n" | |
| "- Outlook: Stakeholders coordinate on mitigation and communications.""" | |
| def _stub_extract(tldr: str, text: str) -> list: | |
| return [ | |
| {"name": "Dana Kim", "role": "CEO of Acme Health", "relevance": "Wants to contain damage and reassure customers.", "quote": "We apologize and are notifying all affected users."}, | |
| {"name": "Luis Ortega", "role": "State Attorney General", "relevance": "Wants accountability and compliance.", "quote": "Fines are possible if negligence is found."}, | |
| {"name": "Pat Rivera", "role": "Patient Advocate", "relevance": "Wants immediate consumer protections.", "quote": "Offer credit monitoring now."}, | |
| {"name": "Casey Tran", "role": "Cybersecurity Consultant", "relevance": "Wants root cause identified and fixed.", "quote": "Likely a misconfigured cloud bucket."} | |
| ] | |
| def _stub_turn(name: str, role: str, world_facts: List[str], memory: List[str]) -> str: | |
| if name == "Dana Kim": | |
| return "We will notify all impacted users and rotate credentials. <action>Announce 24-month free credit monitoring</action>" | |
| if name == "Luis Ortega": | |
| return "My office will review compliance controls and require a corrective plan. <action>Open a formal investigation</action>" | |
| if name == "Pat Rivera": | |
| return "Consumers deserve rapid help and transparency. <action>Publish hotline and guidance for affected patients</action>" | |
| if name == "Casey Tran": | |
| return "Patch the misconfiguration, enable logging, and run tabletop drills. <action>Deliver a 48-hour remediation report</action>" | |
| return "Noted." | |
| def _stub_reflect(last: str, others_text: str) -> dict: | |
| return { | |
| "knowledge_additions": ["Stakeholders coordinated first actions."], | |
| "stance_updates": {}, | |
| "emotions": ["focused"], | |
| "objectives_update": [], | |
| "summary_memory": "Committed initial steps and oversight review." | |
| } | |
| def ask(prompt: str) -> str: | |
| if GOOGLE_API_KEY: | |
| try: | |
| return ask_real(prompt) | |
| except Exception: | |
| pass | |
| if "Summarize the following news event" in prompt or "总结以下新闻事件" in prompt: | |
| return _stub_summary(prompt) | |
| if "You are an extraction system" in prompt or "你是一个提取系统" in prompt: | |
| return json.dumps({"actors": _stub_extract("", "")}) | |
| if prompt.strip().startswith("System:") or prompt.strip().startswith("系统:"): | |
| import re | |
| m = re.search(r"You are ([^,]+), ([^\n]+).", prompt) | |
| if not m: | |
| m = re.search(r"你是 ([^,]+), ([^\n]+).", prompt) | |
| name = m.group(1) if m else "Actor" | |
| role = m.group(2) if m else "Role" | |
| return _stub_turn(name, role, [], []) | |
| if "You are reflecting as" in prompt or "你正在作为" in prompt: | |
| return json.dumps(_stub_reflect("", "")) | |
| return "Acknowledged." | |
| def llm_json(prompt: str) -> dict: | |
| resp = ask(prompt) | |
| import re, json | |
| m = re.search(r'{.*}', resp, flags=re.S) | |
| text = m.group(0) if m else resp | |
| try: | |
| return json.loads(text) | |
| except Exception: | |
| return {"raw": resp} | |
| # --- PROMPTS --- | |
| PROMPTS = { | |
| "en": { | |
| "summarize": "Summarize the following news event in 5 bullet points, factual and neutral:\n\n{text}\n", | |
| "extract": """You are an extraction system. | |
| Article TLDR: | |
| {article_tldr} | |
| Full text: | |
| {article_text} | |
| Return JSON with a list 'actors' of up to 5 items. Each item: | |
| - name | |
| - role | |
| - relevance | |
| - any quoted line from them if present | |
| """, | |
| "persona": """Build a compact personality card for this actor as JSON. | |
| Actor: | |
| {actor_json} | |
| Event TLDR: | |
| {article_tldr} | |
| Return JSON with: | |
| - traits: list of 5 short adjectives | |
| - objectives: list of 3 concise goals | |
| - red_lines: list of 3 concise constraints | |
| - knowledge: list of 4 short facts they believe | |
| - stance: object with up to 3 key topics -> single sentence stance each | |
| """, | |
| "turn": """System: | |
| You are {name}, {role}. | |
| Traits: {traits} | |
| Objectives: {objectives} | |
| Red lines: {red_lines} | |
| Shared facts so far: | |
| - {world_facts} | |
| Recent conversation: | |
| {recent_conversation} | |
| Your private memory: | |
| - {private_memory} | |
| Speak concisely to advance your objectives. | |
| If you commit to a concrete step, announce exactly one action as: | |
| <action>your concise action line</action> | |
| Do not invent facts not in the article or shared facts. | |
| """, | |
| "reflect": """You are reflecting as {name}. | |
| Given your last message: | |
| {last_message} | |
| And others' replies: | |
| {others_replies} | |
| Update in JSON: | |
| - knowledge_additions: list of new factual nuggets you accept | |
| - stance_updates: topic -> one sentence if changed | |
| - emotions: up to 2 words | |
| - objectives_update: if any | |
| - summary_memory: one line summary of what you learned | |
| """ | |
| }, | |
| "zh": { | |
| "summarize": "请用5个要点总结以下新闻事件,保持客观中立:\n\n{text}\n", | |
| "extract": """你是一个提取系统。 | |
| 文章摘要: | |
| {article_tldr} | |
| 全文: | |
| {article_text} | |
| 请返回包含最多5个项目的列表 'actors' 的JSON。每个项目包含: | |
| - name (姓名) | |
| - role (角色) | |
| - relevance (相关性) | |
| - any quoted line from them if present (引用的原话) | |
| """, | |
| "persona": """请为该角色构建一个简洁的个性卡片(JSON格式)。 | |
| 角色: | |
| {actor_json} | |
| 事件摘要: | |
| {article_tldr} | |
| 返回 JSON 包含: | |
| - traits: 5个简短的形容词列表 | |
| - objectives: 3个简洁的目标列表 | |
| - red_lines: 3个简洁的限制列表 | |
| - knowledge: 4个他们相信的简短事实列表 | |
| - stance: 对象,包含最多3个关键话题 -> 每个话题一句话的立场 | |
| """, | |
| "turn": """系统: | |
| 你是 {name}, {role}。 | |
| 特征: {traits} | |
| 目标: {objectives} | |
| 底线: {red_lines} | |
| 目前共享的事实: | |
| - {world_facts} | |
| 最近的对话: | |
| {recent_conversation} | |
| 你的私人记忆: | |
| - {private_memory} | |
| 请简洁发言以推进你的目标。 | |
| 如果你承诺采取具体步骤,请确切宣布一个行动,格式为: | |
| <action>你的简洁行动行</action> | |
| 不要捏造文章或共享事实中不存在的事实。 | |
| """, | |
| "reflect": """你正在作为 {name} 进行反思。 | |
| 鉴于你的上一条消息: | |
| {last_message} | |
| 以及其他人的回复: | |
| {others_replies} | |
| 请用 JSON 更新: | |
| - knowledge_additions: 你接受的新事实信息列表 | |
| - stance_updates: 话题 -> 如果改变,用一句话描述 | |
| - emotions: 最多2个词 | |
| - objectives_update: 如果有更新 | |
| - summary_memory: 一句话总结你学到的内容 | |
| """ | |
| } | |
| } | |
| def summarize_article(text: str, lang: str = "en") -> str: | |
| prompt = PROMPTS[lang]["summarize"].format(text=text) | |
| return ask(prompt).strip() | |
| def extract_actors(article_tldr: str, article_text: str, lang: str = "en") -> list: | |
| prompt = PROMPTS[lang]["extract"].format(article_tldr=article_tldr, article_text=article_text) | |
| data = llm_json(prompt) | |
| return data.get("actors", []) | |
| def bootstrap_persona(actor: dict, article_tldr: str, lang: str = "en") -> dict: | |
| prompt = PROMPTS[lang]["persona"].format(actor_json=json.dumps(actor, ensure_ascii=False), article_tldr=article_tldr) | |
| data = llm_json(prompt) | |
| if "traits" not in data: | |
| data = { | |
| "traits": ["measured","curious","pragmatic","analytical","candid"], | |
| "objectives": ["seek clarity","protect interests","act credibly"], | |
| "red_lines": ["avoid speculation","no illegal acts","respect privacy"], | |
| "knowledge": [], | |
| "stance": {} | |
| } | |
| return data | |
| from dataclasses import dataclass | |
| class Personality: | |
| name: str | |
| role: str | |
| traits: list | |
| objectives: list | |
| red_lines: list | |
| knowledge: list | |
| stance: dict | |
| class CharacterState: | |
| def __init__(self, persona: Personality): | |
| self.id = str(uuid.uuid4())[:8] | |
| self.card = persona | |
| self.memory_log: List[str] = [] | |
| self.has_acted: bool = False | |
| self.actions: List[str] = [] | |
| class Conversation: | |
| def __init__(self, article, lang: str = "en"): | |
| self.article = article | |
| self.lang = lang | |
| self.characters: Dict[str, CharacterState] = {} | |
| self.transcript: List[Dict] = [] | |
| self.world_facts: List[str] = [ | |
| f"Title: {article['title']}", | |
| f"Source: {article['source']}", | |
| article['tldr'] | |
| ] | |
| self.debug: List[str] = [] | |
| def add_character(self, persona: Personality): | |
| self.characters[persona.name] = CharacterState(persona) | |
| def everyone_acted(self) -> bool: | |
| return all(ch.has_acted for ch in self.characters.values()) and len(self.characters) > 0 | |
| def next_speaker(self) -> str: | |
| order = sorted(self.characters.keys(), key=lambda n: self.characters[n].has_acted) | |
| if not self.transcript: | |
| return order[0] | |
| last = self.transcript[-1]['speaker'] | |
| idx = (order.index(last) + 1) % len(order) | |
| return order[idx] | |
| def step(self): | |
| import re | |
| if not self.characters: | |
| self.debug.append("No characters present, skipping step.") | |
| return | |
| speaker_name = self.next_speaker() | |
| ch = self.characters[speaker_name] | |
| self.debug.append(f"Step: next speaker -> {speaker_name}") | |
| tail_text = "\n".join([f"{t['speaker']}: {t['text']}" for t in self.transcript[-6:]]) | |
| p = ch.card | |
| prompt = PROMPTS[self.lang]["turn"].format( | |
| name=p.name, | |
| role=p.role, | |
| traits=', '.join(p.traits), | |
| objectives=', '.join(p.objectives), | |
| red_lines=', '.join(p.red_lines), | |
| world_facts="\n- ".join(self.world_facts[-8:]), | |
| recent_conversation=tail_text, | |
| private_memory="\n- ".join(ch.memory_log[-5:]) | |
| ) | |
| reply = ask(prompt).strip() | |
| self.transcript.append({"speaker": speaker_name, "text": reply}) | |
| self.debug.append(f"Received reply from {speaker_name} ({len(reply)} chars).") | |
| acts = re.findall(r"<action>(.*?)</action>", reply, flags=re.S) | |
| if acts: | |
| ch.has_acted = True | |
| ch.actions.extend([a.strip() for a in acts]) | |
| last_action = acts[-1].strip() | |
| self.world_facts.append(f"{speaker_name} committed: {last_action}") | |
| self.debug.append(f"Action parsed for {speaker_name}: {last_action}") | |
| others = [t for t in self.transcript[-3:] if t['speaker'] != speaker_name] | |
| others_text = "\n".join([f"{o['speaker']}: {o['text']}" for o in others]) | |
| reflect_prompt = PROMPTS[self.lang]["reflect"].format( | |
| name=ch.card.name, | |
| last_message=reply, | |
| others_replies=others_text | |
| ) | |
| reflection = llm_json(reflect_prompt) | |
| if isinstance(reflection, dict): | |
| mem = reflection.get("summary_memory") | |
| if mem: | |
| ch.memory_log.append(mem) | |
| self.debug.append(f"Memory appended for {speaker_name}: {mem}") | |
| # Ensure knowledge_additions is a list | |
| knowledge_additions = reflection.get("knowledge_additions") | |
| if isinstance(knowledge_additions, list): | |
| for nugget in knowledge_additions: | |
| self.world_facts.append(nugget) | |
| self.debug.append(f"World fact added: {nugget}") | |
| # Ensure stance_updates is a dict | |
| stance_updates = reflection.get("stance_updates") | |
| if isinstance(stance_updates, dict): | |
| for k, v in stance_updates.items(): | |
| ch.card.stance[k] = v | |
| self.debug.append(f"Stance update for {speaker_name} [{k}]: {v}") | |
| def run(self, max_rounds: int = 24): | |
| self.debug.append(f"Run start: max_rounds={max_rounds}, characters={list(self.characters.keys())}") | |
| for r in range(max_rounds): | |
| if self.everyone_acted(): | |
| self.debug.append(f"All actors have taken an action by round {r}. Stopping.") | |
| break | |
| self.debug.append(f"Round {r + 1} begin.") | |
| self.step() | |
| self.debug.append(f"Round {r + 1} end. Transcript len={len(self.transcript)}.") | |
| else: | |
| self.debug.append(f"Hit max_rounds={max_rounds} without all actions taken. Stopping.") | |
| def report(self) -> Dict: | |
| return { | |
| "world_facts": self.world_facts, | |
| "characters": { | |
| name: { | |
| "role": ch.card.role, | |
| "traits": ch.card.traits, | |
| "objectives": ch.card.objectives, | |
| "stance": ch.card.stance, | |
| "actions": ch.actions | |
| } for name, ch in self.characters.items() | |
| }, | |
| "transcript": self.transcript, | |
| "debug": self.debug, | |
| } | |
| def run_simulation_from_text(title: str, source: str, text: str, url: str = None, date: str = "", language: str = "en", additional_context: str = "") -> Dict: | |
| tldr = summarize_article(text, lang=language) | |
| article = {"title": title, "date": date, "source": source, "url": url, "text": text, "tldr": tldr} | |
| actors = extract_actors(tldr, text, lang=language) | |
| convo = Conversation(article, lang=language) | |
| for a in actors: | |
| card = bootstrap_persona(a, tldr, lang=language) | |
| persona = Personality( | |
| name=a.get("name", "Unknown"), | |
| role=a.get("role", "Actor"), | |
| traits=card.get("traits", ["measured","curious","pragmatic","analytical","candid"]), | |
| objectives=card.get("objectives", ["seek clarity","protect interests","act credibly"]), | |
| red_lines=card.get("red_lines", ["avoid speculation","no illegal acts","respect privacy"]), | |
| knowledge=card.get("knowledge", []), | |
| stance=card.get("stance", {}) | |
| ) | |
| convo.add_character(persona) | |
| for a in actors[:3]: | |
| if a.get("relevance"): | |
| convo.world_facts.append(f"{a['name']}: {a['relevance']}") | |
| # Add additional context if provided | |
| if additional_context and additional_context.strip(): | |
| convo.world_facts.append(f"Additional context: {additional_context.strip()}") | |
| convo.run(max_rounds=24) | |
| return convo.report() | |