]> vgcfreebox.myrthtech.pt Git - alentejosemlei.git/blobdiff - redis_bridge.py
a lot of agent logic
[alentejosemlei.git] / redis_bridge.py
index bc40dfab6d461f4d7b05102d14a7a70a93b7348e..0c9cec719aa8f38147a888aa64e1b1d50f399b15 100644 (file)
-import redis
+import asyncio
 import json
 import json
-import requests
+import aiohttp
+import redis.asyncio as redis
+import re
+import os
 
 
-print("Initializing Redis Bridge...")
 
 
-# 1. Force the correct Windows IP address and test the connection immediately
-try:
-    r = redis.Redis(host='127.0.0.1', port=6380, decode_responses=True)
-    r.ping() # This will crash immediately if the connection is bad
-    print("SUCCESS: Connected to the Docker Redis database!")
-except Exception as e:
-    print(f"CRITICAL ERROR: Could not connect to Redis. {e}")
-    exit()
+# --- CONFIGURATION ---
+MAX_CONCURRENT_OLLAMA_REQUESTS = 3 
+ALLOW_TEXT_EMOTES = False
+
+# --- NEW: LOAD THE WORLD LORE ON STARTUP ---
+WORLD_LORE = ""
+if os.path.exists("world_lore.txt"):
+    with open("asl_lore.txt", "r", encoding="utf-8") as f:
+        WORLD_LORE = f.read().strip()
+else:
+    print("[WARNING] world_lore.txt not found. Running without global lore.")
 
 
-NPC_SYSTEM_PROMPT = """
-You are Elrendur Arna. You are currently residing in Alentejo Sem Lei.
-Keep your responses under 3 sentences.
-"""
-chat_history = [{"role": "system", "content": NPC_SYSTEM_PROMPT}]
 
 
-print("\nReady! Listening for game messages on 'nwn_to_llm'...")
+semaphore = asyncio.Semaphore(MAX_CONCURRENT_OLLAMA_REQUESTS)
+# Dictionary to keep individual conversations separate
+chat_memory = {}
 
 
-while True:
+async def process_message(r, session, message_data):
     try:
     try:
-        # This line freezes the script until a message arrives
-        result = r.blpop('nwn_to_llm')
-        
-        # --- IF IT HEARS THE GAME, IT WILL PRINT THIS ---
-        print(f"\n--- WAKE UP! NEW MESSAGE RECEIVED ---")
-        
-        queue_name, message_data = result
-        print(f"Raw data from Redis: {message_data}")
-        
-        # Try to parse the JSON
         data = json.loads(message_data)
         data = json.loads(message_data)
-        player_name = data.get('player', 'Unknown')
+        player_name = data.get('player', data.get('target_player', 'Unknown'))
         npc_tag = data.get('npc_tag', 'UnknownNPC')
         message = data.get('message', '')
         npc_tag = data.get('npc_tag', 'UnknownNPC')
         message = data.get('message', '')
+        
+        # --- NEW: THE ASTERISK SCRUBBER ---
+        if not ALLOW_TEXT_EMOTES:
+            # This deletes anything wrapped in asterisks (e.g. "*smiles* Hello" becomes " Hello")
+            message = re.sub(r'\*.*?\*', '', message).strip()
 
 
-        print(f"Parsed: {player_name} said to {npc_tag}: '{message}'")
+        # Extract the dynamic variables sent from the Aurora Toolset/Engine
+        player_race = data.get('player_race', 'Unknown')
+        player_alignment = data.get('player_alignment', 'Unknown')
+        nearby_players = data.get('nearby_players', '')
+        
+        # Extract the decoupled NPC attributes
+        npc_persona = data.get('persona', 'You are a generic citizen.')
+        npc_profession = data.get('profession', 'Commoner')
+        npc_mood = data.get('mood', 'Neutral')
+        npc_secret = data.get('secret', '')
+        
+        # Extract Character Traits & Native Engine Data
+        npc_alignment = data.get('npc_alignment', 'True Neutral')
+        npc_gender = data.get('npc_gender', 'Unknown')
+        npc_race = data.get('npc_race', 'Creature')
+        npc_routine = data.get('npc_routine', '')
+        
+        # Build the context strings
+        group_context = ""
+        if nearby_players:
+            group_context = f"Be aware that these other players are listening nearby: {nearby_players}."
+            
+        secret_context = ""
+        if npc_secret:
+            secret_context = f"YOUR SECRET (Reveal only if players are persuasive): {npc_secret}"
+            
+        routine_context = ""
+        if npc_routine:
+            routine_context = f"YOUR REQUIRED ROUTINE: {npc_routine}"
 
 
-        chat_history.append({"role": "user", "content": f"{player_name} says: {message}"})
+        # =====================================================================
+        # THE PROMPT COMPILER 
+        # =====================================================================
+        dynamic_system_prompt = f"""
+
+        {WORLD_LORE}
+
+        {npc_persona}
+        
+        CURRENT STATUS & TRAITS:
+        - Race & Gender: {npc_gender} {npc_race}
+        - Profession: {npc_profession}
+        - Alignment: {npc_alignment}
+        - Conversational Charisma: Low/Gruff unless otherwise specified.
+        - Current Mood: {npc_mood}
+        {secret_context}
+        
+        {routine_context}
+        
+        CURRENT TARGET: You are speaking to {player_name}, who is a {player_alignment} {player_race}. 
+        {group_context}
+        React appropriately based on your personality, alignment, and mood.
+        
+        CRITICAL ENGINE RULES:
+        Respond ONLY in valid JSON. You MUST use exactly these FIVE keys: "thought", "speech", "emotion", "action", and "action_target".
+        
+        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]
+        
+        - Use REST if you are severely injured, out of spells, or exhausted. This will heal you.
+        - Use STEALTH if you need to hide from enemies, sneak past someone, or if you are a rogue preparing an ambush.
+        - Use SEARCH if you suspect traps, are looking for clues, or are trying to find hidden enemies.
+        - Use UNSTEALTH to return to normal walking/visibility.
         
         
-        # Talk to Ollama
-        print("Thinking... (Sending to local Ollama)")
-        response = requests.post('http://localhost:11434/api/chat', json={
-            "model": "llama3",
-            "messages": chat_history,
-            "stream": False
-        }, timeout=45)
+        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 **.
         
         
-        response.raise_for_status() # Triggers an error if Ollama is broken
-        reply_text = response.json()['message']['content']
+        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.",
+            "emotion": "MACRO WORD",
+            "action": "MACRO WORD",
+            "action_target": "Target name"
+        }}
+        """
+
+        session_id = f"{player_name}_{npc_tag}"
         
         
-        print(f"Ollama Replied: {reply_text}")
-        chat_history.append({"role": "assistant", "content": reply_text})
+        if session_id not in chat_memory:
+            chat_memory[session_id] = [{"role": "system", "content": dynamic_system_prompt}]
+
+        chat_memory[session_id].append({"role": "user", "content": f"{player_name} says: {message}"})
+
+        # Sliding Window Fix
+        # Remember the System Prompt [0] + the last 4 messages [-4:]
+        if len(chat_memory[session_id]) > 10:
+            chat_memory[session_id] = [chat_memory[session_id][0]] + chat_memory[session_id][-5:]
+
+        async with semaphore:
+            print(f"[THINKING] Processing reply for {player_name}...")
+            async with session.post('http://localhost:11434/api/chat', json={
+                "model": "llama3",
+                #"model": "qwen2.5:3b",
+                "messages": chat_memory[session_id],
+                "format": "json",
+                "stream": False,
+                "options": {
+                    "temperature": 0.2
+                    #"num_predict": 120
+                }
+            }, timeout=45) as response:
+                
+                response.raise_for_status()
+                result = await response.json()
+                raw_reply_text = result['message']['content']
+
+        # =====================================================================
+        # THE PYTHON BOUNCER (Sanitization)
+        # =====================================================================
+        try:
+            agent_brain = json.loads(raw_reply_text)
+            agent_brain = {k.lower(): v for k, v in agent_brain.items()}
+            
+            # Ensure all 5 keys exist
+            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_target" not in agent_brain: agent_brain["action_target"] = ""
+            
+            # --- THE NEW ANTI-SILENCE & TARGET CLEANUP FIXES ---
+            if not agent_brain["speech"].strip():
+                agent_brain["speech"] = "*grunts quietly*"
+            
+            agent_brain["action_target"] = agent_brain["action_target"].replace("?", "").replace(".", "").strip()
+            # ---------------------------------------------------
+
+            clean_reply_text = json.dumps(agent_brain)
+
+        except json.JSONDecodeError:
+            print(f"[WARNING] AI Hallucinated! Overriding with safe defaults.")
+            clean_reply_text = json.dumps({
+                "thought": "I lost my train of thought.",
+                "speech": "*grunts quietly*",
+                "emotion": "NEUTRAL",
+                "action": "WANDER",
+                "action_target": ""
+            })
+        # =====================================================================
+
+        print(f"[REPLY] from {npc_tag} to {player_name}: {clean_reply_text}")
+        chat_memory[session_id].append({"role": "assistant", "content": clean_reply_text})
 
 
-        # Package and send back to the game
         reply_payload = {
         reply_payload = {
-            "player": player_name,
             "npc_tag": npc_tag,
             "npc_tag": npc_tag,
-            "reply": reply_text
+            "target_player": player_name, 
+            "reply": clean_reply_text
         }
         }
-        
-        r.rpush('llm_to_nwn', json.dumps(reply_payload))
-        print("SUCCESS: Sent reply to the 'llm_to_nwn' queue for the game to pick up!")
+        await r.rpush('llm_to_nwn', json.dumps(reply_payload))
 
 
-    except json.JSONDecodeError as e:
-        print(f"ERROR: The game sent bad JSON data: {e}")
-    except requests.exceptions.RequestException as e:
-        print(f"ERROR: Failed to talk to Ollama. Is it running? {e}")
     except Exception as e:
     except Exception as e:
-        print(f"UNEXPECTED ERROR: {e}")
\ No newline at end of file
+        print(f"[ERROR] Failed to process message: {e}")
+
+async def main():
+    print("Initializing Async Redis Bridge...")
+    r = redis.Redis(host='127.0.0.1', port=6380, decode_responses=True)
+
+    try:
+        await r.ping()
+        print("SUCCESS: Connected to the Docker Redis database!")
+    except Exception as e:
+        print(f"CRITICAL ERROR: Could not connect to Redis. {e}")
+        return
+
+    print(f"Ready! Listening for game messages. Max GPU concurrency: {MAX_CONCURRENT_OLLAMA_REQUESTS}")
+
+    async with aiohttp.ClientSession() as session:
+        while True:
+            try:
+                result = await r.blpop('nwn_to_llm')
+                if result:
+                    queue_name, message_data = result
+                    asyncio.create_task(process_message(r, session, message_data))
+            except Exception as e:
+                print(f"[LOOP ERROR] {e}")
+                await asyncio.sleep(1)
+
+if __name__ == "__main__":
+    asyncio.run(main())
\ No newline at end of file