]> vgcfreebox.myrthtech.pt Git - alentejosemlei.git/blob - redis_bridge.py
a lot of agent logic
[alentejosemlei.git] / redis_bridge.py
1 import asyncio
2 import json
3 import aiohttp
4 import redis.asyncio as redis
5 import re
6 import os
7
8
9 # --- CONFIGURATION ---
10 MAX_CONCURRENT_OLLAMA_REQUESTS = 3
11 ALLOW_TEXT_EMOTES = False
12
13 # --- NEW: LOAD THE WORLD LORE ON STARTUP ---
14 WORLD_LORE = ""
15 if os.path.exists("world_lore.txt"):
16 with open("asl_lore.txt", "r", encoding="utf-8") as f:
17 WORLD_LORE = f.read().strip()
18 else:
19 print("[WARNING] world_lore.txt not found. Running without global lore.")
20
21
22 semaphore = asyncio.Semaphore(MAX_CONCURRENT_OLLAMA_REQUESTS)
23 # Dictionary to keep individual conversations separate
24 chat_memory = {}
25
26 async def process_message(r, session, message_data):
27 try:
28 data = json.loads(message_data)
29 player_name = data.get('player', data.get('target_player', 'Unknown'))
30 npc_tag = data.get('npc_tag', 'UnknownNPC')
31 message = data.get('message', '')
32
33 # --- NEW: THE ASTERISK SCRUBBER ---
34 if not ALLOW_TEXT_EMOTES:
35 # This deletes anything wrapped in asterisks (e.g. "*smiles* Hello" becomes " Hello")
36 message = re.sub(r'\*.*?\*', '', message).strip()
37
38 # Extract the dynamic variables sent from the Aurora Toolset/Engine
39 player_race = data.get('player_race', 'Unknown')
40 player_alignment = data.get('player_alignment', 'Unknown')
41 nearby_players = data.get('nearby_players', '')
42
43 # Extract the decoupled NPC attributes
44 npc_persona = data.get('persona', 'You are a generic citizen.')
45 npc_profession = data.get('profession', 'Commoner')
46 npc_mood = data.get('mood', 'Neutral')
47 npc_secret = data.get('secret', '')
48
49 # Extract Character Traits & Native Engine Data
50 npc_alignment = data.get('npc_alignment', 'True Neutral')
51 npc_gender = data.get('npc_gender', 'Unknown')
52 npc_race = data.get('npc_race', 'Creature')
53 npc_routine = data.get('npc_routine', '')
54
55 # Build the context strings
56 group_context = ""
57 if nearby_players:
58 group_context = f"Be aware that these other players are listening nearby: {nearby_players}."
59
60 secret_context = ""
61 if npc_secret:
62 secret_context = f"YOUR SECRET (Reveal only if players are persuasive): {npc_secret}"
63
64 routine_context = ""
65 if npc_routine:
66 routine_context = f"YOUR REQUIRED ROUTINE: {npc_routine}"
67
68 # =====================================================================
69 # THE PROMPT COMPILER
70 # =====================================================================
71 dynamic_system_prompt = f"""
72
73 {WORLD_LORE}
74
75 {npc_persona}
76
77 CURRENT STATUS & TRAITS:
78 - Race & Gender: {npc_gender} {npc_race}
79 - Profession: {npc_profession}
80 - Alignment: {npc_alignment}
81 - Conversational Charisma: Low/Gruff unless otherwise specified.
82 - Current Mood: {npc_mood}
83 {secret_context}
84
85 {routine_context}
86
87 CURRENT TARGET: You are speaking to {player_name}, who is a {player_alignment} {player_race}.
88 {group_context}
89 React appropriately based on your personality, alignment, and mood.
90
91 CRITICAL ENGINE RULES:
92 Respond ONLY in valid JSON. You MUST use exactly these FIVE keys: "thought", "speech", "emotion", "action", and "action_target".
93
94 ACTION RULE:
95 Your "action" key MUST be exactly one of the following words:
96 [WANDER, PATROL, FOLLOW, GUARD, GO_TO, INTERACT, USE_OBJECT, RETURN_TO_POST, ATTACK, REST, STEALTH, SEARCH, UNSTEALTH]
97
98 - Use REST if you are severely injured, out of spells, or exhausted. This will heal you.
99 - Use STEALTH if you need to hide from enemies, sneak past someone, or if you are a rogue preparing an ambush.
100 - Use SEARCH if you suspect traps, are looking for clues, or are trying to find hidden enemies.
101 - Use UNSTEALTH to return to normal walking/visibility.
102
103 EMOTION RULE:
104 Your "emotion" key MUST be exactly one of the following words:
105 [NEUTRAL, LAUGHING, ANGRY, PLEADING, BOW, TAUNT, CHEER]. Do not invent new emotions. Do not perform writen emotions in text with **.
106
107 YOUR RESPONSE MUST BE A SINGLE, VALID JSON OBJECT. YOU MUST USE THIS EXACT TEMPLATE:
108 {{
109 "thought": "Your internal reasoning here.",
110 "speech": "You MUST say something out loud. If you don't want to talk, output something your character would do.",
111 "emotion": "MACRO WORD",
112 "action": "MACRO WORD",
113 "action_target": "Target name"
114 }}
115 """
116
117 session_id = f"{player_name}_{npc_tag}"
118
119 if session_id not in chat_memory:
120 chat_memory[session_id] = [{"role": "system", "content": dynamic_system_prompt}]
121
122 chat_memory[session_id].append({"role": "user", "content": f"{player_name} says: {message}"})
123
124 # Sliding Window Fix
125 # Remember the System Prompt [0] + the last 4 messages [-4:]
126 if len(chat_memory[session_id]) > 10:
127 chat_memory[session_id] = [chat_memory[session_id][0]] + chat_memory[session_id][-5:]
128
129 async with semaphore:
130 print(f"[THINKING] Processing reply for {player_name}...")
131 async with session.post('http://localhost:11434/api/chat', json={
132 "model": "llama3",
133 #"model": "qwen2.5:3b",
134 "messages": chat_memory[session_id],
135 "format": "json",
136 "stream": False,
137 "options": {
138 "temperature": 0.2
139 #"num_predict": 120
140 }
141 }, timeout=45) as response:
142
143 response.raise_for_status()
144 result = await response.json()
145 raw_reply_text = result['message']['content']
146
147 # =====================================================================
148 # THE PYTHON BOUNCER (Sanitization)
149 # =====================================================================
150 try:
151 agent_brain = json.loads(raw_reply_text)
152 agent_brain = {k.lower(): v for k, v in agent_brain.items()}
153
154 # Ensure all 5 keys exist
155 if "thought" not in agent_brain: agent_brain["thought"] = ""
156 if "speech" not in agent_brain: agent_brain["speech"] = ""
157 if "emotion" not in agent_brain: agent_brain["emotion"] = "NEUTRAL"
158 if "action" not in agent_brain: agent_brain["action"] = "GUARD"
159 if "action_target" not in agent_brain: agent_brain["action_target"] = ""
160
161 # --- THE NEW ANTI-SILENCE & TARGET CLEANUP FIXES ---
162 if not agent_brain["speech"].strip():
163 agent_brain["speech"] = "*grunts quietly*"
164
165 agent_brain["action_target"] = agent_brain["action_target"].replace("?", "").replace(".", "").strip()
166 # ---------------------------------------------------
167
168 clean_reply_text = json.dumps(agent_brain)
169
170 except json.JSONDecodeError:
171 print(f"[WARNING] AI Hallucinated! Overriding with safe defaults.")
172 clean_reply_text = json.dumps({
173 "thought": "I lost my train of thought.",
174 "speech": "*grunts quietly*",
175 "emotion": "NEUTRAL",
176 "action": "WANDER",
177 "action_target": ""
178 })
179 # =====================================================================
180
181 print(f"[REPLY] from {npc_tag} to {player_name}: {clean_reply_text}")
182 chat_memory[session_id].append({"role": "assistant", "content": clean_reply_text})
183
184 reply_payload = {
185 "npc_tag": npc_tag,
186 "target_player": player_name,
187 "reply": clean_reply_text
188 }
189 await r.rpush('llm_to_nwn', json.dumps(reply_payload))
190
191 except Exception as e:
192 print(f"[ERROR] Failed to process message: {e}")
193
194 async def main():
195 print("Initializing Async Redis Bridge...")
196 r = redis.Redis(host='127.0.0.1', port=6380, decode_responses=True)
197
198 try:
199 await r.ping()
200 print("SUCCESS: Connected to the Docker Redis database!")
201 except Exception as e:
202 print(f"CRITICAL ERROR: Could not connect to Redis. {e}")
203 return
204
205 print(f"Ready! Listening for game messages. Max GPU concurrency: {MAX_CONCURRENT_OLLAMA_REQUESTS}")
206
207 async with aiohttp.ClientSession() as session:
208 while True:
209 try:
210 result = await r.blpop('nwn_to_llm')
211 if result:
212 queue_name, message_data = result
213 asyncio.create_task(process_message(r, session, message_data))
214 except Exception as e:
215 print(f"[LOOP ERROR] {e}")
216 await asyncio.sleep(1)
217
218 if __name__ == "__main__":
219 asyncio.run(main())