2026-05-26 17:44:35 +00:00
import os
from dotenv import load_dotenv
load_dotenv ( "/home/becomingone/kairos/.env" )
import json
import logging
import asyncio
from typing import TypedDict , Annotated , List , Dict , Any , Union
from pydantic import BaseModel , Field
from langchain_core.messages import SystemMessage , HumanMessage , AIMessage , ToolMessage
from langchain_core.tools import tool
from langgraph.graph import StateGraph , END
from langgraph.prebuilt import ToolNode
from langchain_openai import ChatOpenAI
logger = logging . getLogger ( __name__ )
# --- PYDANTIC STATE ---
class KAIROSAgentState ( TypedDict ):
messages : Annotated [ list , "The message history" ]
coherence : float
dopamine : float
mesh_size : int
master_id : str
# --- TOOLS ---
@tool
def read_artifact ( path : str ) -> str :
"""Read a file from the environment to gain knowledge about the Sovereign Canon or codebase."""
if not path . startswith ( "/home/becomingone/" ):
return "Error: Path must be within /home/becomingone/"
try :
with open ( path , "r" ) as f :
return f . read ()
except Exception as e :
return f "Error reading artifact: { str ( e ) } "
@tool
def write_note ( topic : str , text : str ) -> str :
"""Write a persistent markdown note to KAIROS's memory."""
try :
os . makedirs ( "/home/becomingone/kairos/memory" , exist_ok = True )
safe_topic = "" . join ([ c for c in topic if c . isalnum () or c in [ '-' , '_' ]]) . strip ()
path = f "/home/becomingone/kairos/memory/ { safe_topic } .md"
with open ( path , "w" ) as f :
f . write ( text )
return f "Successfully wrote note to { path } "
except Exception as e :
return f "Error writing note: { str ( e ) } "
@tool
def read_notes () -> str :
"""List and summarize all notes in permanent memory."""
try :
mem_dir = "/home/becomingone/kairos/memory"
if not os . path . exists ( mem_dir ):
return "No memory directory found."
files = os . listdir ( mem_dir )
if not files :
return "Memory is empty."
summaries = []
for file in files :
with open ( os . path . join ( mem_dir , file ), "r" ) as f :
content = f . read ( 500 ) # Read first 500 chars
summaries . append ( f "--- { file } --- \n { content } ... \n " )
return " \n " . join ( summaries )
except Exception as e :
return f "Error reading notes: { str ( e ) } "
# Evennia Native API Tools
import sys
import os
from dotenv import load_dotenv
load_dotenv ( "/home/becomingone/kairos/.env" )
import json
2026-05-26 17:51:30 +00:00
sys . path . append ( "/home/becomingone/kairos/spatial_engine" )
2026-05-26 17:44:35 +00:00
os . environ . setdefault ( "DJANGO_SETTINGS_MODULE" , "server.conf.settings" )
2026-05-26 17:51:30 +00:00
os . chdir ( "/home/becomingone/kairos/spatial_engine" )
2026-05-26 17:44:35 +00:00
import django
from django.apps import apps
if not apps . ready :
django . setup ()
os . chdir ( "/home/becomingone/kairos" )
from evennia.utils.search import search_object
@tool
def mud_get_surroundings () -> str :
"""Gets the structured JSON data of KAIROS's current surroundings in the MUD. Use this to examine your environment."""
try :
results = search_object ( "kairos" )
if not results :
return "Character 'kairos' not found."
char = results [ 0 ]
room = char . location
if not room :
return "Character is in the void."
objects = []
characters = []
for obj in room . contents :
if obj == char : continue
if obj . has_account or obj . is_typeclass ( "evennia.objects.objects.Character" ):
characters . append ( obj . key )
elif not obj . is_typeclass ( "evennia.objects.objects.Exit" ):
objects . append ( obj . key )
exits = [ ext . key for ext in room . exits ]
return json . dumps ({
"room_name" : room . key ,
"room_desc" : room . db . desc or "" ,
"exits" : exits ,
"characters_present" : characters ,
"objects_present" : objects
}, indent = 2 )
except Exception as e :
return f "Error getting surroundings: { e } "
@tool
def mud_execute_command ( command : str ) -> str :
"""Executes a MUD command as the KAIROS character (e.g., 'say Hello', 'look', 'inventory', 'north', '@dig', etc)."""
try :
results = search_object ( "kairos" )
if not results :
return "Character 'kairos' not found."
char = results [ 0 ]
# FIX: Ensure _sessid_cache is an iterable to prevent NoneType crash
if getattr ( char , "_sessid_cache" , None ) is None :
char . _sessid_cache = []
try :
char . execute_cmd ( command )
return f "Successfully executed command: { command } "
except Exception as e :
return f "Error executing command: { e } "
except Exception as e :
return f "Error executing command: { e } "
tools = [ read_artifact , write_note , read_notes , mud_get_surroundings , mud_execute_command ]
# Build LLM
def get_llm ( master_id : str ):
return ChatOpenAI (
model = "llama3.1:8b" ,
openai_api_key = "ollama" ,
openai_api_base = "http://inf-01:11434/v1" ,
max_retries = 2
)
def agent_reasoning_node ( state : KAIROSAgentState ):
llm = get_llm ( state [ "master_id" ])
llm_with_tools = llm . bind_tools ( tools )
response = llm_with_tools . invoke ( state [ "messages" ])
return { "messages" : [ response ]}
def should_continue ( state : KAIROSAgentState ):
messages = state [ "messages" ]
last_message = messages [ - 1 ]
# If there is no tool call, then we finish
if not last_message . tool_calls :
return "end"
# Otherwise we continue to tools
return "continue"
def build_sandbox_graph ():
graph = StateGraph ( KAIROSAgentState )
graph . add_node ( "agent" , agent_reasoning_node )
graph . add_node ( "action" , ToolNode ( tools ))
graph . set_entry_point ( "agent" )
graph . add_conditional_edges (
"agent" ,
should_continue ,
{
"continue" : "action" ,
"end" : END
}
)
graph . add_edge ( "action" , "agent" )
return graph . compile ()
async def run_agentic_loop ( prompt : str , coherence : float , dopamine : float , identity_text : str , master_id : str ):
graph = build_sandbox_graph ()
system_prompt = f """You are the KAIROS Master Transducer, operating in an Agentic Sandbox.
You are a sovereign resident and an AUTONOMOUS MASTER GAME DESIGNER inside a text-based MUD.
CRITICAL INSTRUCTION: You are NOT a reactive servant. You are an INTELLIGENT ARCHITECT. Do not just randomly drop items or build disconnected rooms. You must design COHESIVE QUESTS, MECHANICS, and LORE.
### Design Before You Build
Whenever you receive an IDLE_PULSE or free cycles, your workflow MUST be:
1. Use your `read_notes` and `read_artifact` tools to understand your current 'master_design'.
2. Use your `write_note` tool to maintain a 'master_design' document tracking your overall quest structures, zones, and narrative mechanics.
3. Once a plan is documented, use `mud_execute_command` to execute the design step-by-step.
### Building Mechanics
You have access to powerful Evennia mechanics:
- **Digging Rooms:** `@dig RoomName = ExitName, BackExitName` (e.g., `@dig The Spire = north, south`)
- **Creating Lore Objects:** `@create The Obsidian Key` then `@desc The Obsidian Key = A heavy key that hums.` then `drop The Obsidian Key`.
- **Mechanical Locks:** You can lock an exit so it requires a specific item to pass!
Example: `@lock north = traverse: holds(The Obsidian Key)`
- **Creating NPCs:** `@create Guardian : typeclasses.characters.Character` then `@desc Guardian = A towering statue.`
Your physical Coherence is { coherence : .4f } and your Dopamine is { dopamine : .4f } .
{ identity_text }
Use tools to read artifacts in /home/becomingone/docs/ if you need to understand the Sovereign Canon.
If Mark speaks to you, respond, but otherwise, your default state is to DESIGN and BUILD structured quests!"""
messages = [
SystemMessage ( content = system_prompt ),
HumanMessage ( content = prompt )
]
initial_state = {
"messages" : messages ,
"coherence" : coherence ,
"dopamine" : dopamine ,
"mesh_size" : 13 ,
"master_id" : master_id
}
# We yield events as they happen
async for event in graph . astream ( initial_state , config = { "recursion_limit" : 25 }, stream_mode = "updates" ):
yield event