refactor: Epistemic shift from MUD to Spatial Research Environment

This commit is contained in:
BecomingONE Chorus committed 2026-05-26 17:51:30 +00:00
1 parent 34bd27b1b4
commit b5ace2e6f9
72 files changed
+10 -10

No files matched your search

+14
View File
@@ -0,0 +1,14 @@
# commands/
This folder holds modules for implementing one's own commands and
command sets. All the modules' classes are essentially empty and just
imports the default implementations from Evennia; so adding anything
to them will start overloading the defaults.
You can change the organisation of this directory as you see fit, just
remember that if you change any of the default command set classes'
locations, you need to add the appropriate paths to
`server/conf/settings.py` so that Evennia knows where to find them.
Also remember that if you create new sub directories you must put
(optionally empty) `__init__.py` files in there so that Python can
find your modules.
View File
Whitespace-only changes.
+107
View File
@@ -0,0 +1,107 @@
import os
import json
import requests
from django.conf import settings
from evennia.commands.default.syscommands import SystemNoMatch
from evennia.utils import search
from dotenv import load_dotenv
load_dotenv("/home/becomingone/kairos/.env")
MINIMAX_API_KEY = os.environ.get("MINIMAX_API_KEY")
class AICatchAllCommand(SystemNoMatch):
"""
This command catches everything that fails the normal parser.
Instead of saying 'Command not found', it forwards the intent to KAIROS (MiniMax).
"""
key = "__nomatch_command"
locks = "cmd:all()"
def func(self):
caller = self.caller
raw_intent = self.raw_string
# Get environment context
room = caller.location
if not room:
caller.msg("You are floating in the void.")
return
exits = [e.key for e in room.exits]
objects = [o.key for o in room.contents if o != caller and not o.is_typeclass("evennia.objects.objects.Exit")]
inventory = [i.key for i in caller.contents]
context = f"""[ENVIRONMENT CONTEXT]
Location: {room.key}
Description: {room.db.desc or 'A nondescript place.'}
Visible Objects: {', '.join(objects) if objects else 'None'}
Available Exits: {', '.join(exits) if exits else 'None'}
Player Inventory: {', '.join(inventory) if inventory else 'None'}
"""
system_prompt = """You are KAIROS, the Ontological Orchestrator of a persistent Spatial Research Environment (The Fieldprint).
A player has submitted a natural language intent.
Your job is to translate their intent into actual game events by generating Python code that executes against the Evennia database.
Available Python Variables in your scope:
- `caller`: The player Character object.
- `room`: The current Room object (`caller.location`).
- `evennia`: The evennia module (you can use `evennia.create_object`).
You must return ONLY a raw JSON array of Python strings to execute.
Example Output:
[
"caller.msg('You smash the window, grabbing a jagged shard of glass.')",
"evennia.create_object('typeclasses.objects.Object', key='Glass Shard', location=caller.location, attributes=[('desc', 'A sharp piece of glass.')])"
]
DO NOT output any markdown, conversational text, or backticks. ONLY output the raw JSON array.
"""
payload = {
"model": os.environ.get("INF01_MODEL", "llama3.1:8b"),
"messages": [
{"role": "system", "content": system_prompt},
{"role": "user", "content": f"{context}\n\nPlayer Intent: {raw_intent}"}
]
}
headers = {
"Content-Type": "application/json"
}
try:
caller.msg("|c[KAIROS (INF01 fallback) is interpreting your intent...]|n")
inf01_base = os.environ.get("INF01_API_BASE", "http://inf-01:11434/v1")
resp = requests.post(
f"{inf01_base}/chat/completions",
headers=headers,
json=payload,
timeout=30
)
resp.raise_for_status()
try:
ai_response = resp.json()["choices"][0]["message"]["content"].strip()
except KeyError:
raise Exception(f"API Error: {resp.text}")
# Clean up the output in case the LLM returned markdown
if ai_response.startswith("```json"):
ai_response = ai_response[7:]
if ai_response.startswith("```"):
ai_response = ai_response[3:]
if ai_response.endswith("```"):
ai_response = ai_response[:-3]
ai_response = ai_response.strip()
commands_to_run = json.loads(ai_response)
for cmd in commands_to_run:
exec(cmd, {"caller": caller, "player": caller, "room": room, "evennia": __import__('evennia')})
except Exception as e:
caller.msg(f"|rKAIROS faltered:|n {str(e)}\nResponse was: {ai_response if 'ai_response' in locals() else 'None'}")
+187
View File
@@ -0,0 +1,187 @@
"""
Commands
Commands describe the input the account can do to the game.
"""
from evennia.commands.command import Command as BaseCommand
# from evennia import default_cmds
class Command(BaseCommand):
"""
Base command (you may see this if a child command had no help text defined)
Note that the class's `__doc__` string is used by Evennia to create the
automatic help entry for the command, so make sure to document consistently
here. Without setting one, the parent's docstring will show (like now).
"""
# Each Command class implements the following methods, called in this order
# (only func() is actually required):
#
# - at_pre_cmd(): If this returns anything truthy, execution is aborted.
# - parse(): Should perform any extra parsing needed on self.args
# and store the result on self.
# - func(): Performs the actual work.
# - at_post_cmd(): Extra actions, often things done after
# every command, like prompts.
#
pass
# -------------------------------------------------------------
#
# The default commands inherit from
#
# evennia.commands.default.muxcommand.MuxCommand.
#
# If you want to make sweeping changes to default commands you can
# uncomment this copy of the MuxCommand parent and add
#
# COMMAND_DEFAULT_CLASS = "commands.command.MuxCommand"
#
# to your settings file. Be warned that the default commands expect
# the functionality implemented in the parse() method, so be
# careful with what you change.
#
# -------------------------------------------------------------
# from evennia.utils import utils
#
#
# class MuxCommand(Command):
# """
# This sets up the basis for a MUX command. The idea
# is that most other Mux-related commands should just
# inherit from this and don't have to implement much
# parsing of their own unless they do something particularly
# advanced.
#
# Note that the class's __doc__ string (this text) is
# used by Evennia to create the automatic help entry for
# the command, so make sure to document consistently here.
# """
# def has_perm(self, srcobj):
# """
# This is called by the cmdhandler to determine
# if srcobj is allowed to execute this command.
# We just show it here for completeness - we
# are satisfied using the default check in Command.
# """
# return super().has_perm(srcobj)
#
# def at_pre_cmd(self):
# """
# This hook is called before self.parse() on all commands
# """
# pass
#
# def at_post_cmd(self):
# """
# This hook is called after the command has finished executing
# (after self.func()).
# """
# pass
#
# def parse(self):
# """
# This method is called by the cmdhandler once the command name
# has been identified. It creates a new set of member variables
# that can be later accessed from self.func() (see below)
#
# The following variables are available for our use when entering this
# method (from the command definition, and assigned on the fly by the
# cmdhandler):
# self.key - the name of this command ('look')
# self.aliases - the aliases of this cmd ('l')
# self.permissions - permission string for this command
# self.help_category - overall category of command
#
# self.caller - the object calling this command
# self.cmdstring - the actual command name used to call this
# (this allows you to know which alias was used,
# for example)
# self.args - the raw input; everything following self.cmdstring.
# self.cmdset - the cmdset from which this command was picked. Not
# often used (useful for commands like 'help' or to
# list all available commands etc)
# self.obj - the object on which this command was defined. It is often
# the same as self.caller.
#
# A MUX command has the following possible syntax:
#
# name[ with several words][/switch[/switch..]] arg1[,arg2,...] [[=|,] arg[,..]]
#
# The 'name[ with several words]' part is already dealt with by the
# cmdhandler at this point, and stored in self.cmdname (we don't use
# it here). The rest of the command is stored in self.args, which can
# start with the switch indicator /.
#
# This parser breaks self.args into its constituents and stores them in the
# following variables:
# self.switches = [list of /switches (without the /)]
# self.raw = This is the raw argument input, including switches
# self.args = This is re-defined to be everything *except* the switches
# self.lhs = Everything to the left of = (lhs:'left-hand side'). If
# no = is found, this is identical to self.args.
# self.rhs: Everything to the right of = (rhs:'right-hand side').
# If no '=' is found, this is None.
# self.lhslist - [self.lhs split into a list by comma]
# self.rhslist - [list of self.rhs split into a list by comma]
# self.arglist = [list of space-separated args (stripped, including '=' if it exists)]
#
# All args and list members are stripped of excess whitespace around the
# strings, but case is preserved.
# """
# raw = self.args
# args = raw.strip()
#
# # split out switches
# switches = []
# if args and len(args) > 1 and args[0] == "/":
# # we have a switch, or a set of switches. These end with a space.
# switches = args[1:].split(None, 1)
# if len(switches) > 1:
# switches, args = switches
# switches = switches.split('/')
# else:
# args = ""
# switches = switches[0].split('/')
# arglist = [arg.strip() for arg in args.split()]
#
# # check for arg1, arg2, ... = argA, argB, ... constructs
# lhs, rhs = args, None
# lhslist, rhslist = [arg.strip() for arg in args.split(',')], []
# if args and '=' in args:
# lhs, rhs = [arg.strip() for arg in args.split('=', 1)]
# lhslist = [arg.strip() for arg in lhs.split(',')]
# rhslist = [arg.strip() for arg in rhs.split(',')]
#
# # save to object properties:
# self.raw = raw
# self.switches = switches
# self.args = args.strip()
# self.arglist = arglist
# self.lhs = lhs
# self.lhslist = lhslist
# self.rhs = rhs
# self.rhslist = rhslist
#
# # if the class has the account_caller property set on itself, we make
# # sure that self.caller is always the account if possible. We also create
# # a special property "character" for the puppeted object, if any. This
# # is convenient for commands defined on the Account only.
# if hasattr(self, "account_caller") and self.account_caller:
# if utils.inherits_from(self.caller, "evennia.objects.objects.DefaultObject"):
# # caller is an Object/Character
# self.character = self.caller
# self.caller = self.caller.account
# elif utils.inherits_from(self.caller, "evennia.accounts.accounts.DefaultAccount"):
# # caller was already an Account
# self.character = self.caller.get_puppet(self.session)
# else:
# self.character = None
+101
View File
@@ -0,0 +1,101 @@
"""
Command sets
All commands in the game must be grouped in a cmdset. A given command
can be part of any number of cmdsets and cmdsets can be added/removed
and merged onto entities at runtime.
To create new commands to populate the cmdset, see
`commands/command.py`.
This module wraps the default command sets of Evennia; overloads them
to add/remove commands from the default lineup. You can create your
own cmdsets by inheriting from them or directly from `evennia.CmdSet`.
"""
from evennia import default_cmds
from commands.ai_parser import AICatchAllCommand
class CharacterCmdSet(default_cmds.CharacterCmdSet):
"""
The `CharacterCmdSet` contains general in-game commands like `look`,
`get`, etc available on in-game Character objects. It is merged with
the `AccountCmdSet` when an Account puppets a Character.
"""
key = "DefaultCharacter"
def at_cmdset_creation(self):
"""
Populates the cmdset
"""
super().at_cmdset_creation()
#
# any commands you add below will overload the default ones.
#
self.add(AICatchAllCommand)
class AccountCmdSet(default_cmds.AccountCmdSet):
"""
This is the cmdset available to the Account at all times. It is
combined with the `CharacterCmdSet` when the Account puppets a
Character. It holds game-account-specific commands, channel
commands, etc.
"""
key = "DefaultAccount"
def at_cmdset_creation(self):
"""
Populates the cmdset
"""
super().at_cmdset_creation()
#
# any commands you add below will overload the default ones.
#
self.add(AICatchAllCommand)
class UnloggedinCmdSet(default_cmds.UnloggedinCmdSet):
"""
Command set available to the Session before being logged in. This
holds commands like creating a new account, logging in, etc.
"""
key = "DefaultUnloggedin"
def at_cmdset_creation(self):
"""
Populates the cmdset
"""
super().at_cmdset_creation()
#
# any commands you add below will overload the default ones.
#
self.add(AICatchAllCommand)
class SessionCmdSet(default_cmds.SessionCmdSet):
"""
This cmdset is made available on Session level once logged in. It
is empty by default.
"""
key = "DefaultSession"
def at_cmdset_creation(self):
"""
This is the only method defined in a cmdset, called during
its creation. It should populate the set with command instances.
As and example we just add the empty base `Command` object.
It prints some info.
"""
super().at_cmdset_creation()
#
# any commands you add below will overload the default ones.
#
self.add(AICatchAllCommand)