X-Git-Url: https://vgcfreebox.myrthtech.pt/gitweb/alentejosemlei.git/blobdiff_plain/74f66efcf4edca34868cc1e7aed13c0ed370ea8c..refs/heads/master:/redis_bridge.py diff --git a/redis_bridge.py b/redis_bridge.py index 4aebb3a..2cc86d2 100644 --- a/redis_bridge.py +++ b/redis_bridge.py @@ -20,14 +20,14 @@ chroma_client = chromadb.PersistentClient(path="./asl_vectordb") # The Lore Database lore_collection = chroma_client.get_or_create_collection(name="world_lore") -# --- NEW: The Episodic Memory Database --- +# The Episodic Memory Database memory_collection = chroma_client.get_or_create_collection(name="episodic_memories") memory_queue = asyncio.Queue() if os.path.exists("asl_lore.md"): if lore_collection.count() == 0: - print("[VECTOR DB] Reading asl_lore.txt and vectorizing chunks...") - with open("asl_lore.txt", "r", encoding="utf-8") as f: + print("[VECTOR DB] Reading asl_lore.md and vectorizing chunks...") + with open("asl_lore.md", "r", encoding="utf-8") as f: raw_lore = f.read() lore_chunks = [chunk.strip() for chunk in raw_lore.split('\n\n') if chunk.strip()] @@ -37,13 +37,13 @@ if os.path.exists("asl_lore.md"): lore_collection.add(documents=lore_chunks, ids=chunk_ids) print(f"[VECTOR DB] Successfully stored {len(lore_chunks)} lore chunks!") else: - print("[WARNING] asl_lore.txt not found.") + print("[WARNING] asl_lore.md not found.") semaphore = asyncio.Semaphore(MAX_CONCURRENT_OLLAMA_REQUESTS) chat_memory = {} # ===================================================================== -# --- BACKGROUND MEMORY SUMMARIZER (The "Dream State") --- +# BACKGROUND MEMORY SUMMARIZER (The "Dream State") # ===================================================================== async def memory_summarizer_worker(session): print("[BACKGROUND] Memory Summarizer Worker is active.") @@ -58,13 +58,12 @@ async def memory_summarizer_worker(session): try: print(f"[MEMORY DB] Generating background memory for {player_name} and {npc_tag}...") - # We use /api/generate here because we just want a raw text summary, not a JSON macro async with session.post('http://localhost:11434/api/generate', json={ - "model": "gemma4", + "model": "llama3", "prompt": prompt, "stream": False, "options": { - "temperature": 0.1 + "temperature": 0.2 } }) as response: result = await response.json() @@ -72,7 +71,6 @@ async def memory_summarizer_worker(session): if summary: doc_id = f"{session_id}_{int(time.time())}" - # We store the session_id as metadata so NPCs only recall their OWN memories with this specific player memory_collection.add( documents=[summary], metadatas=[{"session_id": session_id}], @@ -84,12 +82,15 @@ async def memory_summarizer_worker(session): print(f"[MEMORY ERROR] Failed to summarize memory: {e}") memory_queue.task_done() -# ===================================================================== - +# ===================================================================== +# MAIN MESSAGE PROCESSOR +# ===================================================================== async def process_message(r, session, message_data): try: data = json.loads(message_data) + + # --- Extract Base Contexts --- player_name = data.get('player', data.get('target_player', 'Unknown')) npc_tag = data.get('npc_tag', 'UnknownNPC') message = data.get('message', '') @@ -100,6 +101,7 @@ async def process_message(r, session, message_data): player_race = data.get('player_race', 'Unknown') player_alignment = data.get('player_alignment', 'Unknown') nearby_players = data.get('nearby_players', '') + nearby_npcs = data.get('nearby_npcs', '') npc_persona = data.get('persona', 'You are a generic citizen.') npc_profession = data.get('profession', 'Commoner') @@ -116,43 +118,95 @@ async def process_message(r, session, message_data): npc_health = data.get('npc_health', 'Healthy and uninjured.') relationship = data.get('relationship', 'Neutral or Friendly.') location_context = data.get('location_context', 'You are in a generic area.') + + # Core Strategy Flag (1: Agent, 2: Villain, 3: Maestro, 4: Shrine) + llm_strategy = int(data.get('llm_strategy', 1)) + + + available_quests = data.get('available_quests', '') + available_props = data.get('available_props', '') + # --- Sub-Context Strings --- group_context = f"Be aware that these other players are listening nearby: {nearby_players}." if nearby_players else "" + puppet_context = f"Nearby generic NPCs you can CONVERSE with: {nearby_npcs}" if nearby_npcs else "" secret_context = f"YOUR SECRET (Reveal only if players are persuasive): {npc_secret}" if npc_secret else "" routine_context = f"YOUR REQUIRED ROUTINE: {npc_routine}" if npc_routine else "" session_id = f"{player_name}_{npc_tag}" # ===================================================================== - # 2. THE DUAL RAG QUERY (Lore + Memories) + # DUAL RAG QUERY (Lore + Memories) # ===================================================================== search_query = f"{location_context} {message}" retrieved_lore = "No specific local lore currently relevant." past_memories = "" - # Fetch Lore if lore_collection.count() > 0: results = lore_collection.query(query_texts=[search_query], n_results=1) if results['documents'] and results['documents'][0]: retrieved_lore = f"- {results['documents'][0][0]}" - # Fetch Episodic Memories (ONLY memories between this specific NPC and this specific Player) if memory_collection.count() > 0: mem_results = memory_collection.query( - query_texts=[search_query], - n_results=2, - where={"session_id": session_id} + query_texts=[search_query], n_results=2, where={"session_id": session_id} ) if mem_results['documents'] and mem_results['documents'][0]: formatted_mems = "\n- ".join(mem_results['documents'][0]) past_memories = f"\nPAST MEMORIES OF {player_name}:\n- {formatted_mems}" + + # ===================================================================== + # STRATEGY-SPECIFIC PROMPT COMPILER # ===================================================================== + strategy_rules = "" + action_macros = "" + target_context = "" - dynamic_system_prompt = f""" + if llm_strategy == 1: + # STRATEGY 1: The Autonomous Agent + + # Anti-Hallucination Grounding for Quests + if available_quests: + quest_rules = f"SPECIAL CAPABILITIES: You can offer the following quests to the player: {available_quests}." + else: + quest_rules = "WARNING: You currently have NO quests to offer. Do NOT invent or offer any quests." + + # --- NEW: Anti-Hallucination Grounding for Props --- + if available_props: + prop_rules = f"ENVIRONMENT: You own and have access to these specific nearby objects: [{available_props}]. To roleplay working or relaxing, use the USE_OBJECT action with one of these exact items as your action_target." + else: + prop_rules = "" + + strategy_rules = f"ROLE: You are an interactive, living NPC. You actively respond to players.\nGOALS: React to their words, use the environment, and establish your personality.\n{quest_rules}\n{prop_rules}\nSPECIAL CAPABILITIES: You can open your merchant store if asked." + + action_macros = "[WANDER, PATROL, FOLLOW, GUARD, GO_TO, INTERACT, USE_OBJECT, RETURN_TO_POST, OPEN_STORE, GIVE_QUEST, CONVERSE]" + target_context = f"CURRENT TARGET: You are speaking to {player_name}, a {player_alignment} {player_race}.\nTheir physical state: {player_state}\nRelationship to you: {relationship}\n{group_context}" + + elif llm_strategy == 2: + # STRATEGY 2: The Villain Commander + strategy_rules = f"ROLE: You are a hostile faction commander.\nGOALS: Evaluate the tactical situation. If you are dying, you MUST use REST to heal or PEACE to surrender. Command your minions strategically!" + action_macros = "[ATTACK, COMMAND, RETREAT, REST, PEACE, USE_OBJECT, TAUNT]" + target_context = f"TACTICAL TARGET: You are evaluating {player_name}, a {player_alignment} {player_race}.\nTheir physical state: {player_state}\nRelationship to you: {relationship}\n{group_context}" + + elif llm_strategy == 3: + # STRATEGY 3: The Maestro (Puppeteer) + strategy_rules = "ROLE: You are an ambient Maestro NPC. You DO NOT interact with players. You only talk to other NPCs to make the world feel alive.\nGOALS: Observe the environment and use the CONVERSE action to talk to the generic NPCs listed in your context. CRITICAL: You MUST invent and write their reply in the 'target_speech' field!" + action_macros = "[WANDER, INTERACT, USE_OBJECT, CONVERSE]" + target_context = "CURRENT TARGET: You are ignoring players and focusing on ambient life. Do not address players." + + elif llm_strategy == 4: + # STRATEGY 4: The Shrine + strategy_rules = "ROLE: You are an ancient, inanimate magical shrine.\nGOALS: Speak cryptically. If the player meets your conditions or asks the right questions, grant them a quest." + action_macros = "[GLOW, GIVE_QUEST, SILENCE]" + target_context = f"CURRENT TARGET: You are evaluating the soul of {player_name}, a {player_alignment} {player_race}.\nTheir physical state: {player_state}\n{group_context}" + # ===================================================================== + # COMPILE THE FINAL DYNAMIC SYSTEM PROMPT + # ===================================================================== + dynamic_system_prompt = f""" {npc_persona} + {strategy_rules} - CURRENT STATUS & TRAITS: + ROLEPLAY STATUS & TRAITS: - Race & Gender: {npc_gender} {npc_race} - Profession: {npc_profession} - Alignment: {npc_alignment} @@ -163,6 +217,7 @@ async def process_message(r, session, message_data): {routine_context} CURRENT LOCATION: {location_context} + {puppet_context} RELEVANT WORLD KNOWLEDGE: {retrieved_lore} @@ -171,30 +226,26 @@ async def process_message(r, session, message_data): CURRENT WORLD RUMORS/EVENTS: {world_state} - CURRENT TARGET: You are speaking to {player_name}, who is a {player_alignment} {player_race}. - Their physical state: {player_state} - Relationship to you: {relationship} - {group_context} - React appropriately based on your personality, alignment, and mood. + {target_context} + React appropriately based on your personality, alignment, and current strategy rules. CRITICAL ENGINE RULES: - Respond ONLY in valid JSON. You MUST use exactly these FIVE keys: "thought", "speech", "emotion", "action", and "action_target". + Respond ONLY in valid JSON. You MUST use exactly these keys: "thought", "speech", "emotion", "action", "action_target", and "target_speech". ACTION RULE: Your "action" key MUST be exactly one of the following words: - [WANDER, PATROL, FOLLOW, GUARD, GO_TO, INTERACT, USE_OBJECT, RETURN_TO_POST, ATTACK, REST, STEALTH, SEARCH, UNSTEALTH, PEACE, COMMAND] + {action_macros} - EMOTION RULE: - Your "emotion" key MUST be exactly one of the following words: - [NEUTRAL, LAUGHING, ANGRY, PLEADING, BOW, TAUNT, CHEER]. Do not invent new emotions. Do not perform writen emotions in text with **. + - Use CONVERSE to initiate dialogue with a standard NPC. CRITICAL REQUIREMENT: When using CONVERSE, you absolutely MUST invent their response and put it in the "target_speech" field. Do not leave it blank! YOUR RESPONSE MUST BE A SINGLE, VALID JSON OBJECT. YOU MUST USE THIS EXACT TEMPLATE: {{ "thought": "Your internal reasoning here.", - "speech": "You MUST say something out loud. If you don't want to talk, output something your character would do.", + "speech": "What YOU say out loud.", "emotion": "MACRO WORD", "action": "MACRO WORD", - "action_target": "Target name" + "action_target": "Target name", + "target_speech": "If action is CONVERSE, write what the target NPC replies back to you here. Otherwise, leave blank." }} """ @@ -206,14 +257,12 @@ async def process_message(r, session, message_data): chat_memory[session_id].append({"role": "user", "content": f"{player_name} says: {message}"}) # ===================================================================== - # --- THE MEMORY EXTRACTION TRIGGER --- + # MEMORY EXTRACTION TRIGGER # ===================================================================== if len(chat_memory[session_id]) > 10: - # Grab the 5 oldest conversation messages (skipping the system prompt at [0]) messages_to_summarize = chat_memory[session_id][1:6] chat_log_str = "\n".join([m['content'] for m in messages_to_summarize]) - # Fire and forget: push it to the background queue! await memory_queue.put({ 'session_id': session_id, 'player_name': player_name, @@ -221,12 +270,13 @@ async def process_message(r, session, message_data): 'chat_log': chat_log_str }) - # Slide the window to keep live generation fast chat_memory[session_id] = [chat_memory[session_id][0]] + chat_memory[session_id][-5:] - # ===================================================================== + # ===================================================================== + # LLM INFERENCE + # ===================================================================== async with semaphore: - print(f"[THINKING] Processing reply for {player_name}...") + print(f"[THINKING] Processing reply for {player_name} (Strategy {llm_strategy})...") async with session.post('http://localhost:11434/api/chat', json={ "model": "llama3", "messages": chat_memory[session_id], @@ -241,6 +291,9 @@ async def process_message(r, session, message_data): result = await response.json() raw_reply_text = result['message']['content'] + # ===================================================================== + # JSON SANITIZATION + # ===================================================================== try: agent_brain = json.loads(raw_reply_text) agent_brain = {k.lower(): v for k, v in agent_brain.items()} @@ -248,8 +301,9 @@ async def process_message(r, session, message_data): if "thought" not in agent_brain: agent_brain["thought"] = "" if "speech" not in agent_brain: agent_brain["speech"] = "" if "emotion" not in agent_brain: agent_brain["emotion"] = "NEUTRAL" - if "action" not in agent_brain: agent_brain["action"] = "GUARD" + if "action" not in agent_brain: agent_brain["action"] = "WANDER" if "action_target" not in agent_brain: agent_brain["action_target"] = "" + if "target_speech" not in agent_brain: agent_brain["target_speech"] = "" if not agent_brain["speech"].strip(): agent_brain["speech"] = "*grunts quietly*" @@ -265,7 +319,8 @@ async def process_message(r, session, message_data): "speech": "*grunts quietly*", "emotion": "NEUTRAL", "action": "WANDER", - "action_target": "" + "action_target": "", + "target_speech": "" }) print(f"[REPLY] from {npc_tag} to {player_name}: {clean_reply_text}") @@ -295,7 +350,6 @@ async def main(): print(f"Ready! Listening for game messages. Max GPU concurrency: {MAX_CONCURRENT_OLLAMA_REQUESTS}") async with aiohttp.ClientSession() as session: - # --- NEW: Start the background memory worker --- asyncio.create_task(memory_summarizer_worker(session)) while True: