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