Bunch of refactoring and renaming to support built-in commands.

This commit is contained in:
projectmoon 2024-01-17 10:34:24 +01:00
parent 001e15e594
commit 82001b99b7
11 changed files with 174 additions and 81 deletions

View File

@ -6,7 +6,7 @@ use super::prompts::{execution_prompts, parsing_prompts, world_prompts};
use crate::kobold_api::Client as KoboldClient; use crate::kobold_api::Client as KoboldClient;
use crate::models::coherence::{CoherenceFailure, SceneFix}; use crate::models::coherence::{CoherenceFailure, SceneFix};
use crate::models::commands::{Command, Commands, RawCommandExecution, VerbsResponse}; use crate::models::commands::{ParsedCommand, ParsedCommands, RawCommandExecution, VerbsResponse};
use crate::models::world::raw::{ use crate::models::world::raw::{
ExitSeed, ItemDetails, ItemSeed, PersonDetails, PersonSeed, SceneSeed, ExitSeed, ItemDetails, ItemSeed, PersonDetails, PersonSeed, SceneSeed,
}; };
@ -56,7 +56,7 @@ impl AiGenerator {
self.person_creation_convo.reset(); self.person_creation_convo.reset();
} }
pub async fn parse(&self, cmd: &str) -> Result<Commands> { pub async fn parse(&self, cmd: &str) -> Result<ParsedCommands> {
// If convo so far is empty, add the instruction header, // If convo so far is empty, add the instruction header,
// otherwise only append to existing convo. // otherwise only append to existing convo.
let prompt = match self.parsing_convo.is_empty() { let prompt = match self.parsing_convo.is_empty() {
@ -64,7 +64,7 @@ impl AiGenerator {
false => parsing_prompts::continuation_prompt(&cmd), false => parsing_prompts::continuation_prompt(&cmd),
}; };
let mut cmds: Commands = self.parsing_convo.execute(&prompt).await?; let mut cmds: ParsedCommands = self.parsing_convo.execute(&prompt).await?;
let verbs = self.find_verbs(cmd).await?; let verbs = self.find_verbs(cmd).await?;
self.check_coherence(&verbs, &mut cmds).await?; self.check_coherence(&verbs, &mut cmds).await?;
Ok(cmds) Ok(cmds)
@ -83,13 +83,13 @@ impl AiGenerator {
.collect()) .collect())
} }
async fn check_coherence(&self, verbs: &[String], commands: &mut Commands) -> Result<()> { async fn check_coherence(&self, verbs: &[String], commands: &mut ParsedCommands) -> Result<()> {
// let coherence_prompt = parsing_prompts::coherence_prompt(); // let coherence_prompt = parsing_prompts::coherence_prompt();
// let mut commands: Commands = self.parsing_convo.execute(&coherence_prompt).await?; // let mut commands: Commands = self.parsing_convo.execute(&coherence_prompt).await?;
// Non-LLM coherence checks: remove empty commands, remove // Non-LLM coherence checks: remove empty commands, remove
// non-verbs, etc. // non-verbs, etc.
let filtered_commands: Vec<Command> = commands let filtered_commands: Vec<ParsedCommand> = commands
.clone() .clone()
.commands .commands
.into_iter() .into_iter()
@ -102,7 +102,7 @@ impl AiGenerator {
Ok(()) Ok(())
} }
pub async fn execute_raw(&self, stage: &Stage, cmd: &Command) -> Result<RawCommandExecution> { pub async fn execute_raw(&self, stage: &Stage, cmd: &ParsedCommand) -> Result<RawCommandExecution> {
let prompt = execution_prompts::execution_prompt(stage, &cmd); let prompt = execution_prompts::execution_prompt(stage, &cmd);
let raw_exec: RawCommandExecution = self.execution_convo.execute(&prompt).await?; let raw_exec: RawCommandExecution = self.execution_convo.execute(&prompt).await?;
Ok(raw_exec) Ok(raw_exec)

View File

@ -1,7 +1,7 @@
use crate::db::Database; use crate::db::Database;
use crate::kobold_api::Client as KoboldClient; use crate::kobold_api::Client as KoboldClient;
use crate::models::commands::{ use crate::models::commands::{
CommandExecution, Commands, ExecutionConversionResult, RawCommandExecution, AiCommand, ParsedCommands, ExecutionConversionResult, RawCommandExecution,
}; };
use crate::models::world::items::{Category, Item, Rarity}; use crate::models::world::items::{Category, Item, Rarity};
use crate::models::world::people::{Gender, Person, Sex}; use crate::models::world::people::{Gender, Person, Sex};
@ -40,23 +40,23 @@ impl AiLogic {
} }
pub async fn execute( pub async fn execute(
&mut self, &self,
stage: &Stage, stage: &Stage,
cmd: &str, cmd: &str,
) -> Result<(Commands, CommandExecution)> { ) -> Result<(ParsedCommands, RawCommandExecution)> {
let parsed_cmd = self.generator.parse(cmd).await?; let parsed_cmd = self.generator.parse(cmd).await?;
let execution = self.execute_parsed(stage, &parsed_cmd).await?; let execution = self.execute_parsed(stage, &parsed_cmd).await?;
Ok((parsed_cmd, execution)) Ok((parsed_cmd, execution))
} }
pub async fn execute_parsed( pub async fn execute_parsed(
&mut self, &self,
stage: &Stage, stage: &Stage,
parsed_cmd: &Commands, parsed_cmd: &ParsedCommands,
) -> Result<CommandExecution> { ) -> Result<RawCommandExecution> {
//TODO handle multiple commands in list //TODO handle multiple commands in list
if parsed_cmd.commands.is_empty() { if parsed_cmd.commands.is_empty() {
return Ok(CommandExecution::empty()); return Ok(RawCommandExecution::empty());
} }
let cmd = &parsed_cmd.commands[0]; let cmd = &parsed_cmd.commands[0];
@ -67,21 +67,12 @@ impl AiLogic {
// Set aside anything with correct event, but wrong parameters. // Set aside anything with correct event, but wrong parameters.
// Ask LLM to fix them, if possible // Ask LLM to fix them, if possible
//TODO make a aiclient::fix_execution //TODO make a aiclient::fix_execution
let converted = command_converter::convert_raw_execution(raw_exec, &self.db).await;
self.generator.reset_commands(); self.generator.reset_commands();
Ok(raw_exec)
//TODO handle the errored events aside from yeeting them out
match converted {
ExecutionConversionResult::Success(execution) => Ok(execution),
ExecutionConversionResult::PartialSuccess(execution, _) => Ok(execution),
ExecutionConversionResult::Failure(failures) => {
bail!("unhandled command execution failure: {:?}", failures)
}
}
} }
pub async fn create_person(&mut self, scene: &SceneSeed, seed: &PersonSeed) -> Result<Person> { pub async fn create_person(&self, scene: &SceneSeed, seed: &PersonSeed) -> Result<Person> {
self.generator.reset_person_creation(); self.generator.reset_person_creation();
let details = self.generator.create_person_details(scene, seed).await?; let details = self.generator.create_person_details(scene, seed).await?;
@ -123,7 +114,7 @@ impl AiLogic {
}) })
} }
pub async fn create_item(&mut self, scene: &SceneSeed, seed: &ItemSeed) -> Result<Item> { pub async fn create_item(&self, scene: &SceneSeed, seed: &ItemSeed) -> Result<Item> {
let details = self.generator.create_item_details(scene, seed).await?; let details = self.generator.create_item_details(scene, seed).await?;
// TODO these have to be sent to the AI // TODO these have to be sent to the AI
@ -143,7 +134,7 @@ impl AiLogic {
} }
pub async fn create_scene_with_id( pub async fn create_scene_with_id(
&mut self, &self,
scene_type: &str, scene_type: &str,
fantasticalness: &str, fantasticalness: &str,
scene_id: &str, scene_id: &str,
@ -156,7 +147,7 @@ impl AiLogic {
} }
pub async fn create_scene_from_stub( pub async fn create_scene_from_stub(
&mut self, &self,
stub: SceneStub, stub: SceneStub,
connected_scene: &Scene, connected_scene: &Scene,
) -> Result<ContentContainer> { ) -> Result<ContentContainer> {
@ -181,7 +172,7 @@ impl AiLogic {
} }
pub async fn create_scene( pub async fn create_scene(
&mut self, &self,
scene_type: &str, scene_type: &str,
fantasticalness: &str, fantasticalness: &str,
) -> Result<ContentContainer> { ) -> Result<ContentContainer> {
@ -200,7 +191,7 @@ impl AiLogic {
} }
async fn fill_in_scene_from_stub( async fn fill_in_scene_from_stub(
&mut self, &self,
seed: SceneSeed, seed: SceneSeed,
stub: SceneStub, stub: SceneStub,
) -> Result<ContentContainer> { ) -> Result<ContentContainer> {
@ -212,7 +203,7 @@ impl AiLogic {
Ok(content) Ok(content)
} }
async fn fill_in_scene(&mut self, mut scene_seed: SceneSeed) -> Result<ContentContainer> { async fn fill_in_scene(&self, mut scene_seed: SceneSeed) -> Result<ContentContainer> {
let mut content_in_scene = vec![]; let mut content_in_scene = vec![];
// People in scene // People in scene

View File

@ -1,5 +1,5 @@
use crate::ai::convo::AiPrompt; use crate::ai::convo::AiPrompt;
use crate::models::commands::{Command, CommandEvent, EventConversionFailures}; use crate::models::commands::{ParsedCommand, CommandEvent, EventConversionFailures};
use crate::models::world::scenes::{Scene, Stage}; use crate::models::world::scenes::{Scene, Stage};
use strum::VariantNames; use strum::VariantNames;
@ -194,7 +194,7 @@ fn stage_info(stage: &Stage) -> String {
info info
} }
pub fn execution_prompt(stage: &Stage, cmd: &Command) -> AiPrompt { pub fn execution_prompt(stage: &Stage, cmd: &ParsedCommand) -> AiPrompt {
let scene_info = stage_info(&stage); let scene_info = stage_info(&stage);
let prompt = COMMAND_EXECUTION_PROMPT let prompt = COMMAND_EXECUTION_PROMPT

13
src/commands/builtins.rs Normal file
View File

@ -0,0 +1,13 @@
use crate::models::commands::{BuiltinCommand, AiCommand};
use crate::models::world::scenes::Stage;
pub fn check_builtin_command(stage: &Stage, cmd: &str) -> Option<BuiltinCommand> {
match cmd {
"look" => look_command(stage),
_ => None,
}
}
fn look_command(_stage: &Stage) -> Option<BuiltinCommand> {
Some(BuiltinCommand::Look)
}

View File

@ -1,7 +1,7 @@
use crate::{ use crate::{
db::Database, db::Database,
models::commands::{ models::commands::{
CommandEvent, CommandExecution, EventCoherenceFailure, EventConversionError, CommandEvent, AiCommand, EventCoherenceFailure, EventConversionError,
EventConversionFailures, ExecutionConversionResult, RawCommandEvent, RawCommandExecution, EventConversionFailures, ExecutionConversionResult, RawCommandEvent, RawCommandExecution,
}, },
}; };
@ -42,8 +42,8 @@ struct Narrative {
narration: String, narration: String,
} }
fn from_raw_success(raw: Narrative, events: Vec<CommandEvent>) -> CommandExecution { fn from_raw_success(raw: Narrative, events: Vec<CommandEvent>) -> AiCommand {
CommandExecution { AiCommand {
events, events,
valid: raw.valid, valid: raw.valid,
reason: match &raw.reason { reason: match &raw.reason {
@ -62,7 +62,7 @@ pub async fn convert_raw_execution(
db: &Database, db: &Database,
) -> ExecutionConversionResult { ) -> ExecutionConversionResult {
if !raw_exec.valid { if !raw_exec.valid {
return ExecutionConversionResult::Success(CommandExecution::from_raw_invalid(raw_exec)); return ExecutionConversionResult::Success(AiCommand::from_raw_invalid(raw_exec));
} }
let narrative = Narrative { let narrative = Narrative {

View File

@ -1,2 +1,62 @@
pub mod converter; use crate::{
ai::logic::AiLogic,
db::Database,
models::{
commands::{
BuiltinCommand, AiCommand, ExecutionConversionResult, RawCommandExecution, CommandExecution,
},
world::scenes::Stage,
},
};
use anyhow::{anyhow, Result};
use std::rc::Rc;
pub mod builtins; pub mod builtins;
pub mod converter;
pub struct CommandExecutor {
logic: Rc<AiLogic>,
db: Rc<Database>,
}
impl<'a> CommandExecutor {
pub fn new(logic: Rc<AiLogic>, db: Rc<Database>) -> CommandExecutor {
CommandExecutor { logic, db }
}
pub async fn execute(&self, stage: &Stage, cmd: &str) -> Result<CommandExecution> {
CommandExecution::AiCommand(AiCommand::empty());
if let Some(builtin) = builtins::check_builtin_command(stage, cmd) {
return Ok(CommandExecution::Builtin(builtin));
}
let cached_command = self.db.load_cached_command(cmd, &stage.scene).await?;
let raw_exec: RawCommandExecution = if let Some(cached) = cached_command {
self.logic.execute_parsed(stage, &cached.commands).await?
} else {
let (cmds_to_cache, execution) = self.logic.execute(stage, cmd).await?;
self.db
.cache_command(cmd, &stage.scene, &cmds_to_cache)
.await?;
execution
};
let converted = converter::convert_raw_execution(raw_exec, &self.db).await;
//TODO handle the errored events aside from getting rid of them
let execution: AiCommand = match converted {
ExecutionConversionResult::Success(execution) => Ok(execution),
ExecutionConversionResult::PartialSuccess(execution, _) => Ok(execution),
ExecutionConversionResult::Failure(failures) => Err(anyhow!(
"unhandled command execution failure: {:?}",
failures
)),
}?;
Ok(CommandExecution::AiCommand(execution))
}
}

View File

@ -1,4 +1,4 @@
use crate::models::commands::{CachedCommand, Command, Commands}; use crate::models::commands::{CachedParsedCommand, ParsedCommand, ParsedCommands};
use crate::models::world::scenes::{Scene, Stage, StageOrStub}; use crate::models::world::scenes::{Scene, Stage, StageOrStub};
use crate::models::{Content, ContentContainer, Insertable}; use crate::models::{Content, ContentContainer, Insertable};
use anyhow::Result; use anyhow::Result;
@ -388,10 +388,10 @@ impl Database {
&self, &self,
raw_cmd: &str, raw_cmd: &str,
scene: &Scene, scene: &Scene,
parsed_cmds: &Commands, parsed_cmds: &ParsedCommands,
) -> Result<()> { ) -> Result<()> {
let collection = self.collection(CMD_COLLECTION).await?; let collection = self.collection(CMD_COLLECTION).await?;
let doc = CachedCommand { let doc = CachedParsedCommand {
raw: raw_cmd.to_string(), raw: raw_cmd.to_string(),
scene_key: scene._key.as_ref().cloned().expect("scene is missing key"), scene_key: scene._key.as_ref().cloned().expect("scene is missing key"),
commands: parsed_cmds.clone(), commands: parsed_cmds.clone(),
@ -405,7 +405,7 @@ impl Database {
&self, &self,
raw_cmd: &str, raw_cmd: &str,
scene: &Scene, scene: &Scene,
) -> Result<Option<CachedCommand>> { ) -> Result<Option<CachedParsedCommand>> {
let scene_key = scene._key.as_deref(); let scene_key = scene._key.as_deref();
let aql = AqlQuery::builder() let aql = AqlQuery::builder()
.query(queries::LOAD_CACHED_COMMAND) .query(queries::LOAD_CACHED_COMMAND)

View File

@ -1,12 +1,13 @@
use crate::db::Database;
use crate::io::display; use crate::io::display;
use crate::models::commands::CommandExecution; use crate::models::commands::{BuiltinCommand, AiCommand, CommandExecution};
use crate::state::GameState; use crate::state::GameState;
use crate::{commands::CommandExecutor, db::Database};
use anyhow::Result; use anyhow::Result;
use reedline::{DefaultPrompt, Reedline, Signal}; use reedline::{DefaultPrompt, Reedline, Signal};
use std::rc::Rc; use std::rc::Rc;
pub struct GameLoop { pub struct GameLoop {
executor: CommandExecutor,
state: GameState, state: GameState,
db: Rc<Database>, db: Rc<Database>,
editor: Reedline, editor: Reedline,
@ -14,20 +15,25 @@ pub struct GameLoop {
} }
impl GameLoop { impl GameLoop {
pub fn new(state: GameState, db: Rc<Database>) -> GameLoop { pub fn new(state: GameState, db: &Rc<Database>) -> GameLoop {
let executor_db = db.clone();
let loop_db = db.clone();
let executor_logic = state.logic.clone();
GameLoop { GameLoop {
state, state,
db, db: loop_db,
executor: CommandExecutor::new(executor_logic, executor_db),
editor: Reedline::create(), editor: Reedline::create(),
prompt: DefaultPrompt::default(), prompt: DefaultPrompt::default(),
} }
} }
async fn handle_execution(&mut self, execution: CommandExecution) -> Result<()> { async fn handle_ai_command(&mut self, execution: AiCommand) -> Result<()> {
if !execution.valid { if !execution.valid {
display!( display!(
"You can't do that: {}", "You can't do that: {}",
execution.reason.unwrap_or("for some reason...".to_string()) execution.reason.unwrap_or("for some reason...".to_string())
); );
return Ok(()); return Ok(());
@ -42,31 +48,24 @@ impl GameLoop {
Ok(()) Ok(())
} }
async fn execute_command(&mut self, cmd: &str) -> Result<CommandExecution> { async fn handle_builtin(&mut self, builtin: BuiltinCommand) -> Result<()> {
let stage = &self.state.current_scene; Ok(())
let cached_command = self.db.load_cached_command(cmd, &stage.scene).await?; }
let execution = if let Some(cached) = cached_command { async fn handle_execution(&mut self, execution: CommandExecution) -> Result<()> {
self.state match execution {
.logic CommandExecution::Builtin(builtin) => self.handle_builtin(builtin).await?,
.execute_parsed(stage, &cached.commands) CommandExecution::AiCommand(exec) => self.handle_ai_command(exec).await?,
.await?
} else {
let (cmds_to_cache, execution) = self.state.logic.execute(stage, cmd).await?;
self.db
.cache_command(cmd, &stage.scene, &cmds_to_cache)
.await?;
execution
}; };
Ok(execution) Ok(())
} }
async fn handle_input(&mut self, cmd: &str) -> Result<()> { async fn handle_input(&mut self, cmd: &str) -> Result<()> {
if !cmd.is_empty() { if !cmd.is_empty() {
let execution = self.execute_command(cmd).await?; //let execution = self.execute_command(cmd).await?;
let mut stage = &self.state.current_scene;
let execution = self.executor.execute(&mut stage, cmd).await?;
self.handle_execution(execution).await?; self.handle_execution(execution).await?;
} }

View File

@ -102,7 +102,7 @@ async fn main() -> Result<()> {
base_client, base_client,
)); ));
let db = Rc::new(Database::new(conn, "test_world").await?); let db = Rc::new(Database::new(conn, "test_world").await?);
let logic = AiLogic::new(client, &db); let logic = Rc::new(AiLogic::new(client, &db));
let mut state = GameState { let mut state = GameState {
logic, logic,
@ -114,7 +114,7 @@ async fn main() -> Result<()> {
load_root_scene(&db, &mut state).await?; load_root_scene(&db, &mut state).await?;
let mut game_loop = GameLoop::new(state, db.clone()); let mut game_loop = GameLoop::new(state, &db);
game_loop.run_loop().await?; game_loop.run_loop().await?;
Ok(()) Ok(())

View File

@ -2,22 +2,24 @@ use serde::{Deserialize, Serialize};
use strum::{EnumString, EnumVariantNames}; use strum::{EnumString, EnumVariantNames};
use thiserror::Error; use thiserror::Error;
use super::world::scenes::Stage;
/// Stored in the database to bypass AI 'parsing' when possible. /// Stored in the database to bypass AI 'parsing' when possible.
#[derive(Debug, Serialize, Deserialize, Clone)] #[derive(Debug, Serialize, Deserialize, Clone)]
pub struct CachedCommand { pub struct CachedParsedCommand {
pub raw: String, pub raw: String,
pub scene_key: String, pub scene_key: String,
pub commands: Commands, pub commands: ParsedCommands,
} }
#[derive(Debug, Serialize, Deserialize, Clone)] #[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Commands { pub struct ParsedCommands {
pub commands: Vec<Command>, pub commands: Vec<ParsedCommand>,
pub count: usize, pub count: usize,
} }
#[derive(Debug, Serialize, Deserialize, Clone)] #[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Command { pub struct ParsedCommand {
pub verb: String, pub verb: String,
pub target: String, pub target: String,
pub location: String, pub location: String,
@ -49,6 +51,17 @@ pub struct RawCommandExecution {
pub events: Vec<RawCommandEvent>, pub events: Vec<RawCommandEvent>,
} }
impl RawCommandExecution {
pub fn empty() -> RawCommandExecution {
RawCommandExecution {
valid: true,
reason: None,
narration: "".to_string(),
events: vec![],
}
}
}
#[derive(Debug, Serialize, Deserialize, Clone)] #[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct RawCommandEvent { pub struct RawCommandEvent {
@ -87,17 +100,34 @@ pub enum CommandEvent {
}, },
} }
/// A builtin command has more immediate access to necessary
/// information, so we can be a bit more loose with what we give it. A
/// builtin command is only created directly via checking for builtin
/// commands. These commands may have little or no parameters, as they
/// are meant for simple, direct commands like looking, movement, etc.
pub enum BuiltinCommand {
Look,
}
pub enum CommandExecution {
Builtin(BuiltinCommand),
AiCommand(AiCommand),
}
/// An "AI Command" is a command execution generated by the LLM and
/// run through coherence validation/fixing, and (assuming it is
/// valid) contains a series of events to apply to the game state.
#[derive(Debug, Serialize, Deserialize, Clone)] #[derive(Debug, Serialize, Deserialize, Clone)]
pub struct CommandExecution { pub struct AiCommand {
pub valid: bool, pub valid: bool,
pub reason: Option<String>, pub reason: Option<String>,
pub narration: String, pub narration: String,
pub events: Vec<CommandEvent>, pub events: Vec<CommandEvent>,
} }
impl CommandExecution { impl AiCommand {
pub fn empty() -> CommandExecution { pub fn empty() -> AiCommand {
CommandExecution { AiCommand {
valid: true, valid: true,
reason: None, reason: None,
narration: "".to_string(), narration: "".to_string(),
@ -105,8 +135,8 @@ impl CommandExecution {
} }
} }
pub fn from_raw_invalid(raw: RawCommandExecution) -> CommandExecution { pub fn from_raw_invalid(raw: RawCommandExecution) -> AiCommand {
CommandExecution { AiCommand {
valid: raw.valid, valid: raw.valid,
reason: raw.reason, reason: raw.reason,
narration: "".to_string(), narration: "".to_string(),
@ -117,8 +147,8 @@ impl CommandExecution {
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub enum ExecutionConversionResult { pub enum ExecutionConversionResult {
Success(CommandExecution), Success(AiCommand),
PartialSuccess(CommandExecution, EventConversionFailures), PartialSuccess(AiCommand, EventConversionFailures),
Failure(EventConversionFailures), Failure(EventConversionFailures),
} }

View File

@ -13,7 +13,7 @@ use std::rc::Rc;
pub struct GameState { pub struct GameState {
pub start_prompt: String, pub start_prompt: String,
pub logic: AiLogic, pub logic: Rc<AiLogic>,
pub db: Rc<Database>, pub db: Rc<Database>,
pub current_scene: Stage, pub current_scene: Stage,
} }