]> vgcfreebox.myrthtech.pt Git - alentejosemlei.git/commitdiff
agent v2
authorvitler <vitor.goncalo.costa@gmail.com>
Sun, 19 Apr 2026 02:23:13 +0000 (03:23 +0100)
committervitler <vitor.goncalo.costa@gmail.com>
Sun, 19 Apr 2026 02:23:13 +0000 (03:23 +0100)
redis_bridge.py
server_data/modules/Alentejo Sem Lei.mod

index a45944e8d595bb62bad99de76ad30ad34a006e3a..642d3601e8a7bec24b61791c6f171074beb577ea 100644 (file)
@@ -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")
 
 # 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:
 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()]
             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:
             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 = {}
 
 # =====================================================================
 
 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.")
 # =====================================================================
 async def memory_summarizer_worker(session):
     print("[BACKGROUND] Memory Summarizer Worker is active.")
@@ -58,9 +58,8 @@ async def memory_summarizer_worker(session):
 
         try:
             print(f"[MEMORY DB] Generating background memory for {player_name} and {npc_tag}...")
 
         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={
             async with session.post('http://localhost:11434/api/generate', json={
-                "model": "gemma4",
+                "model": "llama3", # Changed back to llama3 from gemma4 based on previous setup
                 "prompt": prompt,
                 "stream": False,
                 "options": {
                 "prompt": prompt,
                 "stream": False,
                 "options": {
@@ -72,7 +71,6 @@ async def memory_summarizer_worker(session):
                 
                 if summary:
                     doc_id = f"{session_id}_{int(time.time())}"
                 
                 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}],
                     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()
             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)
 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', '')
         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', '')
         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')
         
         npc_persona = data.get('persona', 'You are a generic citizen.')
         npc_profession = data.get('profession', 'Commoner')
@@ -116,43 +118,77 @@ 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.')
         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)) 
 
 
+        # --- Sub-Context Strings ---
         group_context = f"Be aware that these other players are listening nearby: {nearby_players}." if nearby_players else ""
         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}"
 
         # =====================================================================
         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 = ""
 
         # =====================================================================
         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]}"
 
         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(
         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}"
             )
             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 = ""
+
+        if llm_strategy == 1:
+            # STRATEGY 1: The Autonomous Agent
+            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.\nSPECIAL CAPABILITIES: You can offer quests or 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 initiate conversations with the generic NPCs listed in your context. Ignore players entirely."
+            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"""
         dynamic_system_prompt = f"""
-
         {npc_persona}
         {npc_persona}
+        {strategy_rules}
         
         
-        CURRENT STATUS & TRAITS:
+        ROLEPLAY STATUS & TRAITS:
         - Race & Gender: {npc_gender} {npc_race}
         - Profession: {npc_profession}
         - Alignment: {npc_alignment}
         - Race & Gender: {npc_gender} {npc_race}
         - Profession: {npc_profession}
         - Alignment: {npc_alignment}
@@ -163,6 +199,7 @@ async def process_message(r, session, message_data):
         {routine_context}
         
         CURRENT LOCATION: {location_context}
         {routine_context}
         
         CURRENT LOCATION: {location_context}
+        {puppet_context}
         
         RELEVANT WORLD KNOWLEDGE:
         {retrieved_lore}
         
         RELEVANT WORLD KNOWLEDGE:
         {retrieved_lore}
@@ -171,25 +208,18 @@ async def process_message(r, session, message_data):
         CURRENT WORLD RUMORS/EVENTS:
         {world_state}
         
         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:
         
         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:
         
         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}
         
         
-       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, CONVERSE]
+        - Use CONVERSE if you want to initiate a back-and-forth dialogue with a standard, unintelligent NPC. You will write their response for them in "target_speech".
         
         
-        - Use CONVERSE if you want to initiate a back-and-forth dialogue with a standard, unintelligent NPC. You will write their response for them.
-
         YOUR RESPONSE MUST BE A SINGLE, VALID JSON OBJECT. YOU MUST USE THIS EXACT TEMPLATE:
         {{
             "thought": "Your internal reasoning here.",
         YOUR RESPONSE MUST BE A SINGLE, VALID JSON OBJECT. YOU MUST USE THIS EXACT TEMPLATE:
         {{
             "thought": "Your internal reasoning here.",
@@ -209,14 +239,12 @@ async def process_message(r, session, message_data):
         chat_memory[session_id].append({"role": "user", "content": f"{player_name} says: {message}"})
 
         # =====================================================================
         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:
         # =====================================================================
         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])
             
             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,
             await memory_queue.put({
                 'session_id': session_id,
                 'player_name': player_name,
@@ -224,12 +252,13 @@ async def process_message(r, session, message_data):
                 'chat_log': chat_log_str
             })
 
                 '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:]
             chat_memory[session_id] = [chat_memory[session_id][0]] + chat_memory[session_id][-5:]
-        # =====================================================================
 
 
+        # =====================================================================
+        # LLM INFERENCE
+        # =====================================================================
         async with semaphore:
         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],
             async with session.post('http://localhost:11434/api/chat', json={
                 "model": "llama3",
                 "messages": chat_memory[session_id],
@@ -244,6 +273,9 @@ async def process_message(r, session, message_data):
                 result = await response.json()
                 raw_reply_text = result['message']['content']
 
                 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()}
         try:
             agent_brain = json.loads(raw_reply_text)
             agent_brain = {k.lower(): v for k, v in agent_brain.items()}
@@ -251,7 +283,7 @@ 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 "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 "action_target" not in agent_brain: agent_brain["action_target"] = ""
             if "target_speech" not in agent_brain: agent_brain["target_speech"] = ""
             
@@ -269,7 +301,8 @@ async def process_message(r, session, message_data):
                 "speech": "*grunts quietly*",
                 "emotion": "NEUTRAL",
                 "action": "WANDER",
                 "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}")
             })
 
         print(f"[REPLY] from {npc_tag} to {player_name}: {clean_reply_text}")
@@ -299,7 +332,6 @@ async def main():
     print(f"Ready! Listening for game messages. Max GPU concurrency: {MAX_CONCURRENT_OLLAMA_REQUESTS}")
 
     async with aiohttp.ClientSession() as session:
     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:
         asyncio.create_task(memory_summarizer_worker(session))
 
         while True:
index da36678f394aafb391a97b97cf30e02065f82b9b..31ed2aeab1d3539e4ec26d2532b65108945c4649 100644 (file)
Binary files a/server_data/modules/Alentejo Sem Lei.mod and b/server_data/modules/Alentejo Sem Lei.mod differ