From 82001b99b7f9a4e23bcf717233fcda8e698ba3b4 Mon Sep 17 00:00:00 2001 From: projectmoon Date: Wed, 17 Jan 2024 10:34:24 +0100 Subject: [PATCH] Bunch of refactoring and renaming to support built-in commands. --- src/ai/generator.rs | 12 +++--- src/ai/logic.rs | 39 +++++++----------- src/ai/prompts/execution_prompts.rs | 4 +- src/commands/builtins.rs | 13 ++++++ src/commands/converter.rs | 8 ++-- src/commands/mod.rs | 62 ++++++++++++++++++++++++++++- src/db/mod.rs | 8 ++-- src/game_loop.rs | 47 +++++++++++----------- src/main.rs | 4 +- src/models/commands.rs | 56 ++++++++++++++++++++------ src/state.rs | 2 +- 11 files changed, 174 insertions(+), 81 deletions(-) create mode 100644 src/commands/builtins.rs diff --git a/src/ai/generator.rs b/src/ai/generator.rs index 2c3a754..2ed9a0d 100644 --- a/src/ai/generator.rs +++ b/src/ai/generator.rs @@ -6,7 +6,7 @@ use super::prompts::{execution_prompts, parsing_prompts, world_prompts}; use crate::kobold_api::Client as KoboldClient; 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::{ ExitSeed, ItemDetails, ItemSeed, PersonDetails, PersonSeed, SceneSeed, }; @@ -56,7 +56,7 @@ impl AiGenerator { self.person_creation_convo.reset(); } - pub async fn parse(&self, cmd: &str) -> Result { + pub async fn parse(&self, cmd: &str) -> Result { // If convo so far is empty, add the instruction header, // otherwise only append to existing convo. let prompt = match self.parsing_convo.is_empty() { @@ -64,7 +64,7 @@ impl AiGenerator { 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?; self.check_coherence(&verbs, &mut cmds).await?; Ok(cmds) @@ -83,13 +83,13 @@ impl AiGenerator { .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 mut commands: Commands = self.parsing_convo.execute(&coherence_prompt).await?; // Non-LLM coherence checks: remove empty commands, remove // non-verbs, etc. - let filtered_commands: Vec = commands + let filtered_commands: Vec = commands .clone() .commands .into_iter() @@ -102,7 +102,7 @@ impl AiGenerator { Ok(()) } - pub async fn execute_raw(&self, stage: &Stage, cmd: &Command) -> Result { + pub async fn execute_raw(&self, stage: &Stage, cmd: &ParsedCommand) -> Result { let prompt = execution_prompts::execution_prompt(stage, &cmd); let raw_exec: RawCommandExecution = self.execution_convo.execute(&prompt).await?; Ok(raw_exec) diff --git a/src/ai/logic.rs b/src/ai/logic.rs index d0be101..38f45ec 100644 --- a/src/ai/logic.rs +++ b/src/ai/logic.rs @@ -1,7 +1,7 @@ use crate::db::Database; use crate::kobold_api::Client as KoboldClient; use crate::models::commands::{ - CommandExecution, Commands, ExecutionConversionResult, RawCommandExecution, + AiCommand, ParsedCommands, ExecutionConversionResult, RawCommandExecution, }; use crate::models::world::items::{Category, Item, Rarity}; use crate::models::world::people::{Gender, Person, Sex}; @@ -40,23 +40,23 @@ impl AiLogic { } pub async fn execute( - &mut self, + &self, stage: &Stage, cmd: &str, - ) -> Result<(Commands, CommandExecution)> { + ) -> Result<(ParsedCommands, RawCommandExecution)> { let parsed_cmd = self.generator.parse(cmd).await?; let execution = self.execute_parsed(stage, &parsed_cmd).await?; Ok((parsed_cmd, execution)) } pub async fn execute_parsed( - &mut self, + &self, stage: &Stage, - parsed_cmd: &Commands, - ) -> Result { + parsed_cmd: &ParsedCommands, + ) -> Result { //TODO handle multiple commands in list if parsed_cmd.commands.is_empty() { - return Ok(CommandExecution::empty()); + return Ok(RawCommandExecution::empty()); } let cmd = &parsed_cmd.commands[0]; @@ -67,21 +67,12 @@ impl AiLogic { // Set aside anything with correct event, but wrong parameters. // Ask LLM to fix them, if possible //TODO make a aiclient::fix_execution - let converted = command_converter::convert_raw_execution(raw_exec, &self.db).await; self.generator.reset_commands(); - - //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) - } - } + Ok(raw_exec) } - pub async fn create_person(&mut self, scene: &SceneSeed, seed: &PersonSeed) -> Result { + pub async fn create_person(&self, scene: &SceneSeed, seed: &PersonSeed) -> Result { self.generator.reset_person_creation(); 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 { + pub async fn create_item(&self, scene: &SceneSeed, seed: &ItemSeed) -> Result { let details = self.generator.create_item_details(scene, seed).await?; // TODO these have to be sent to the AI @@ -143,7 +134,7 @@ impl AiLogic { } pub async fn create_scene_with_id( - &mut self, + &self, scene_type: &str, fantasticalness: &str, scene_id: &str, @@ -156,7 +147,7 @@ impl AiLogic { } pub async fn create_scene_from_stub( - &mut self, + &self, stub: SceneStub, connected_scene: &Scene, ) -> Result { @@ -181,7 +172,7 @@ impl AiLogic { } pub async fn create_scene( - &mut self, + &self, scene_type: &str, fantasticalness: &str, ) -> Result { @@ -200,7 +191,7 @@ impl AiLogic { } async fn fill_in_scene_from_stub( - &mut self, + &self, seed: SceneSeed, stub: SceneStub, ) -> Result { @@ -212,7 +203,7 @@ impl AiLogic { Ok(content) } - async fn fill_in_scene(&mut self, mut scene_seed: SceneSeed) -> Result { + async fn fill_in_scene(&self, mut scene_seed: SceneSeed) -> Result { let mut content_in_scene = vec![]; // People in scene diff --git a/src/ai/prompts/execution_prompts.rs b/src/ai/prompts/execution_prompts.rs index f5b306f..5f7841c 100644 --- a/src/ai/prompts/execution_prompts.rs +++ b/src/ai/prompts/execution_prompts.rs @@ -1,5 +1,5 @@ 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 strum::VariantNames; @@ -194,7 +194,7 @@ fn stage_info(stage: &Stage) -> String { 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 prompt = COMMAND_EXECUTION_PROMPT diff --git a/src/commands/builtins.rs b/src/commands/builtins.rs new file mode 100644 index 0000000..3562ee0 --- /dev/null +++ b/src/commands/builtins.rs @@ -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 { + match cmd { + "look" => look_command(stage), + _ => None, + } +} + +fn look_command(_stage: &Stage) -> Option { + Some(BuiltinCommand::Look) +} diff --git a/src/commands/converter.rs b/src/commands/converter.rs index 08df508..815fb46 100644 --- a/src/commands/converter.rs +++ b/src/commands/converter.rs @@ -1,7 +1,7 @@ use crate::{ db::Database, models::commands::{ - CommandEvent, CommandExecution, EventCoherenceFailure, EventConversionError, + CommandEvent, AiCommand, EventCoherenceFailure, EventConversionError, EventConversionFailures, ExecutionConversionResult, RawCommandEvent, RawCommandExecution, }, }; @@ -42,8 +42,8 @@ struct Narrative { narration: String, } -fn from_raw_success(raw: Narrative, events: Vec) -> CommandExecution { - CommandExecution { +fn from_raw_success(raw: Narrative, events: Vec) -> AiCommand { + AiCommand { events, valid: raw.valid, reason: match &raw.reason { @@ -62,7 +62,7 @@ pub async fn convert_raw_execution( db: &Database, ) -> ExecutionConversionResult { 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 { diff --git a/src/commands/mod.rs b/src/commands/mod.rs index d56f56d..40c6227 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -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 converter; + +pub struct CommandExecutor { + logic: Rc, + db: Rc, +} + +impl<'a> CommandExecutor { + pub fn new(logic: Rc, db: Rc) -> CommandExecutor { + CommandExecutor { logic, db } + } + + pub async fn execute(&self, stage: &Stage, cmd: &str) -> Result { + 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)) + } +} diff --git a/src/db/mod.rs b/src/db/mod.rs index 308f0d0..5cc9bb7 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -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::{Content, ContentContainer, Insertable}; use anyhow::Result; @@ -388,10 +388,10 @@ impl Database { &self, raw_cmd: &str, scene: &Scene, - parsed_cmds: &Commands, + parsed_cmds: &ParsedCommands, ) -> Result<()> { let collection = self.collection(CMD_COLLECTION).await?; - let doc = CachedCommand { + let doc = CachedParsedCommand { raw: raw_cmd.to_string(), scene_key: scene._key.as_ref().cloned().expect("scene is missing key"), commands: parsed_cmds.clone(), @@ -405,7 +405,7 @@ impl Database { &self, raw_cmd: &str, scene: &Scene, - ) -> Result> { + ) -> Result> { let scene_key = scene._key.as_deref(); let aql = AqlQuery::builder() .query(queries::LOAD_CACHED_COMMAND) diff --git a/src/game_loop.rs b/src/game_loop.rs index 40fcfb2..e66fa2f 100644 --- a/src/game_loop.rs +++ b/src/game_loop.rs @@ -1,12 +1,13 @@ -use crate::db::Database; use crate::io::display; -use crate::models::commands::CommandExecution; +use crate::models::commands::{BuiltinCommand, AiCommand, CommandExecution}; use crate::state::GameState; +use crate::{commands::CommandExecutor, db::Database}; use anyhow::Result; use reedline::{DefaultPrompt, Reedline, Signal}; use std::rc::Rc; pub struct GameLoop { + executor: CommandExecutor, state: GameState, db: Rc, editor: Reedline, @@ -14,20 +15,25 @@ pub struct GameLoop { } impl GameLoop { - pub fn new(state: GameState, db: Rc) -> GameLoop { + pub fn new(state: GameState, db: &Rc) -> GameLoop { + let executor_db = db.clone(); + let loop_db = db.clone(); + let executor_logic = state.logic.clone(); + GameLoop { state, - db, + db: loop_db, + executor: CommandExecutor::new(executor_logic, executor_db), editor: Reedline::create(), 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 { display!( "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(()); @@ -42,31 +48,24 @@ impl GameLoop { Ok(()) } - async fn execute_command(&mut self, cmd: &str) -> Result { - let stage = &self.state.current_scene; - let cached_command = self.db.load_cached_command(cmd, &stage.scene).await?; + async fn handle_builtin(&mut self, builtin: BuiltinCommand) -> Result<()> { + Ok(()) + } - let execution = if let Some(cached) = cached_command { - self.state - .logic - .execute_parsed(stage, &cached.commands) - .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 + async fn handle_execution(&mut self, execution: CommandExecution) -> Result<()> { + match execution { + CommandExecution::Builtin(builtin) => self.handle_builtin(builtin).await?, + CommandExecution::AiCommand(exec) => self.handle_ai_command(exec).await?, }; - Ok(execution) + Ok(()) } async fn handle_input(&mut self, cmd: &str) -> Result<()> { 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?; } diff --git a/src/main.rs b/src/main.rs index b3d53ed..e8925ba 100644 --- a/src/main.rs +++ b/src/main.rs @@ -102,7 +102,7 @@ async fn main() -> Result<()> { base_client, )); 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 { logic, @@ -114,7 +114,7 @@ async fn main() -> Result<()> { 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?; Ok(()) diff --git a/src/models/commands.rs b/src/models/commands.rs index 7a98d32..824cec5 100644 --- a/src/models/commands.rs +++ b/src/models/commands.rs @@ -2,22 +2,24 @@ use serde::{Deserialize, Serialize}; use strum::{EnumString, EnumVariantNames}; use thiserror::Error; +use super::world::scenes::Stage; + /// Stored in the database to bypass AI 'parsing' when possible. #[derive(Debug, Serialize, Deserialize, Clone)] -pub struct CachedCommand { +pub struct CachedParsedCommand { pub raw: String, pub scene_key: String, - pub commands: Commands, + pub commands: ParsedCommands, } #[derive(Debug, Serialize, Deserialize, Clone)] -pub struct Commands { - pub commands: Vec, +pub struct ParsedCommands { + pub commands: Vec, pub count: usize, } #[derive(Debug, Serialize, Deserialize, Clone)] -pub struct Command { +pub struct ParsedCommand { pub verb: String, pub target: String, pub location: String, @@ -49,6 +51,17 @@ pub struct RawCommandExecution { pub events: Vec, } +impl RawCommandExecution { + pub fn empty() -> RawCommandExecution { + RawCommandExecution { + valid: true, + reason: None, + narration: "".to_string(), + events: vec![], + } + } +} + #[derive(Debug, Serialize, Deserialize, Clone)] #[serde(rename_all = "camelCase")] 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)] -pub struct CommandExecution { +pub struct AiCommand { pub valid: bool, pub reason: Option, pub narration: String, pub events: Vec, } -impl CommandExecution { - pub fn empty() -> CommandExecution { - CommandExecution { +impl AiCommand { + pub fn empty() -> AiCommand { + AiCommand { valid: true, reason: None, narration: "".to_string(), @@ -105,8 +135,8 @@ impl CommandExecution { } } - pub fn from_raw_invalid(raw: RawCommandExecution) -> CommandExecution { - CommandExecution { + pub fn from_raw_invalid(raw: RawCommandExecution) -> AiCommand { + AiCommand { valid: raw.valid, reason: raw.reason, narration: "".to_string(), @@ -117,8 +147,8 @@ impl CommandExecution { #[derive(Clone, Debug)] pub enum ExecutionConversionResult { - Success(CommandExecution), - PartialSuccess(CommandExecution, EventConversionFailures), + Success(AiCommand), + PartialSuccess(AiCommand, EventConversionFailures), Failure(EventConversionFailures), } diff --git a/src/state.rs b/src/state.rs index 8548152..3114d0b 100644 --- a/src/state.rs +++ b/src/state.rs @@ -13,7 +13,7 @@ use std::rc::Rc; pub struct GameState { pub start_prompt: String, - pub logic: AiLogic, + pub logic: Rc, pub db: Rc, pub current_scene: Stage, }