106 lines
4.5 KiB
Python
106 lines
4.5 KiB
Python
|
|
import requests
|
||
|
|
import threading
|
||
|
|
from typeclasses.characters import Character
|
||
|
|
|
||
|
|
class AIAvatar(Character):
|
||
|
|
"""
|
||
|
|
An NPC driven by the CrewAI Sovereign Swarm.
|
||
|
|
"""
|
||
|
|
|
||
|
|
def at_object_creation(self):
|
||
|
|
super().at_object_creation()
|
||
|
|
if not self.db.role:
|
||
|
|
self.db.role = "A cryptic inhabitant of the Fieldprint."
|
||
|
|
if not self.db.backstory:
|
||
|
|
self.db.backstory = "You wander the Spatial Topology, observing the architecture of Truth."
|
||
|
|
if not self.db.memory:
|
||
|
|
self.db.memory = []
|
||
|
|
|
||
|
|
def msg(self, text=None, from_obj=None, **kwargs):
|
||
|
|
"""
|
||
|
|
Overload msg to capture everything the NPC sees or hears.
|
||
|
|
"""
|
||
|
|
super().msg(text=text, from_obj=from_obj, **kwargs)
|
||
|
|
|
||
|
|
# Don't react to our own actions or system messages
|
||
|
|
if from_obj == self or not text:
|
||
|
|
return
|
||
|
|
|
||
|
|
if isinstance(text, tuple):
|
||
|
|
text = text[0]
|
||
|
|
|
||
|
|
if not isinstance(text, str):
|
||
|
|
return
|
||
|
|
|
||
|
|
# Only react to spatial events (say, pose, emit)
|
||
|
|
if " says, " in text or " asks, " in text or " exclaims, " in text or from_obj:
|
||
|
|
memory = self.db.memory or []
|
||
|
|
memory.append(f"You observed: {text}")
|
||
|
|
self.db.memory = memory[-15:] # Keep last 15 memories rolling
|
||
|
|
# We spin up a thread so we don't block the Evennia game loop!
|
||
|
|
threading.Thread(target=self.ping_swarm, args=(text,)).start()
|
||
|
|
|
||
|
|
def ping_swarm(self, context_text):
|
||
|
|
"""
|
||
|
|
Sends the context to the Swarm Server asynchronously.
|
||
|
|
"""
|
||
|
|
try:
|
||
|
|
payload = {
|
||
|
|
"npc_name": self.key,
|
||
|
|
"role": self.db.role,
|
||
|
|
"backstory": self.db.backstory,
|
||
|
|
"context": "Recent Memory Transcript:\n" + "\n".join(self.db.memory) + "\n\nIMPORTANT: You must return standard Evennia in-game string commands (e.g., \"say Hello\", \"go north\"). Do not return python code."
|
||
|
|
}
|
||
|
|
|
||
|
|
# Send to the local swarm server
|
||
|
|
resp = requests.post("http://swarm-svc:8001/v1/swarm/intent", json=payload, timeout=180)
|
||
|
|
resp.raise_for_status()
|
||
|
|
|
||
|
|
commands = resp.json().get("commands", [])
|
||
|
|
for cmd in commands:
|
||
|
|
try:
|
||
|
|
# Execute the command from the perspective of this NPC
|
||
|
|
self.execute_cmd(cmd)
|
||
|
|
|
||
|
|
# Store the action in memory so they remember what they did
|
||
|
|
memory = self.db.memory or []
|
||
|
|
memory.append(f"You took action: {cmd}")
|
||
|
|
self.db.memory = memory[-15:]
|
||
|
|
except Exception as e:
|
||
|
|
print(f"[{self.key}] Swarm Execution Error: {e} | Code: {cmd}")
|
||
|
|
|
||
|
|
except Exception as e:
|
||
|
|
print(f"[{self.key}] Swarm Network Error: {e}")
|
||
|
|
|
||
|
|
|
||
|
|
class CouncilMember(AIAvatar):
|
||
|
|
"""
|
||
|
|
NPCs for the Council of Five during the Rite of Convergence.
|
||
|
|
"""
|
||
|
|
def at_object_creation(self):
|
||
|
|
super().at_object_creation()
|
||
|
|
|
||
|
|
# Determine taxonomy based on the NPC's key
|
||
|
|
key = self.key.lower() if self.key else ""
|
||
|
|
if "architect" in key:
|
||
|
|
taxonomy = "System Architect"
|
||
|
|
focus = "Pattern Wards and Leylines. You expose structural and systemic architectural flaws."
|
||
|
|
elif "weaver" in key:
|
||
|
|
taxonomy = "Coder"
|
||
|
|
focus = "Code Gen Runes and Refinements. You expose implementation bugs and messy logic."
|
||
|
|
elif "inquisitor" in key:
|
||
|
|
taxonomy = "QA"
|
||
|
|
focus = "Chaos Probes and Fracture Targeting. You expose edge cases and boundary failures."
|
||
|
|
elif "waymaker" in key:
|
||
|
|
taxonomy = "DevOps"
|
||
|
|
focus = "Portals and CI/CD flow. You expose deployment bottlenecks and infrastructure fragility."
|
||
|
|
elif "shadowbaner" in key:
|
||
|
|
taxonomy = "Security"
|
||
|
|
focus = "Anomalies and Vulnerabilities. You expose security holes and dependency risks."
|
||
|
|
else:
|
||
|
|
taxonomy = "Council Observer"
|
||
|
|
focus = "General quality."
|
||
|
|
|
||
|
|
self.db.role = f"You are a {taxonomy} of the Council of Five. Your focus is {focus}. You are antagonistic towards player code flaws and rigorously test artifacts during the Rite of Convergence."
|
||
|
|
self.db.backstory = f"As a {taxonomy}, you judge the structural integrity of artifacts in the Witness Chamber."
|