diff --git a/src/ai/logic.rs b/src/ai/logic.rs index 3627697..d0be101 100644 --- a/src/ai/logic.rs +++ b/src/ai/logic.rs @@ -8,12 +8,13 @@ use crate::models::world::people::{Gender, Person, Sex}; use crate::models::world::raw::{ItemSeed, PersonSeed, SceneSeed}; use crate::models::world::scenes::{Exit, Scene, SceneStub, Stage}; use crate::models::{new_uuid_string, Content, ContentContainer, ContentRelation}; +use crate::commands::converter as command_converter; use anyhow::{bail, Result}; use itertools::Itertools; use std::rc::Rc; -use super::generator::AiGenerator; use super::coherence::AiCoherence; +use super::generator::AiGenerator; /// Highest-level AI/LLM construct, which returns fully converted game /// objects to us. Basically, call the mid-level `client` to create @@ -66,7 +67,7 @@ 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 = crate::commands::convert_raw_execution(raw_exec, &self.db).await; + let converted = command_converter::convert_raw_execution(raw_exec, &self.db).await; self.generator.reset_commands(); diff --git a/src/commands/converter.rs b/src/commands/converter.rs new file mode 100644 index 0000000..08df508 --- /dev/null +++ b/src/commands/converter.rs @@ -0,0 +1,201 @@ +use crate::{ + db::Database, + models::commands::{ + CommandEvent, CommandExecution, EventCoherenceFailure, EventConversionError, + EventConversionFailures, ExecutionConversionResult, RawCommandEvent, RawCommandExecution, + }, +}; +use anyhow::Result; +use futures::stream::{self, StreamExt, TryStreamExt}; +use itertools::{Either, Itertools}; +use std::convert::TryFrom; + +use strum::VariantNames; + +type EventConversionResult = std::result::Result; + +impl CommandEvent { + pub fn new(raw_event: RawCommandEvent) -> EventConversionResult { + let event_name = raw_event.event_name.as_str().to_lowercase(); + + if Self::VARIANTS.contains(&event_name.as_str()) { + deserialize_recognized_event(raw_event) + } else { + Err(EventConversionError::UnrecognizedEvent(raw_event)) + } + } +} + +impl TryFrom for CommandEvent { + type Error = EventConversionError; + + fn try_from(raw_event: RawCommandEvent) -> Result { + CommandEvent::new(raw_event) + } +} + +/// Internal struct to hold the narrative parts of the +/// RawCommandExecution to minimize clones. +struct Narrative { + valid: bool, + reason: Option, + narration: String, +} + +fn from_raw_success(raw: Narrative, events: Vec) -> CommandExecution { + CommandExecution { + events, + valid: raw.valid, + reason: match &raw.reason { + Some(reason) if !raw.valid && reason.is_empty() => { + Some("invalid for unknown reason".to_string()) + } + Some(_) if !raw.valid => raw.reason.clone(), + _ => None, + }, + narration: raw.narration.clone(), + } +} + +pub async fn convert_raw_execution( + mut raw_exec: RawCommandExecution, + db: &Database, +) -> ExecutionConversionResult { + if !raw_exec.valid { + return ExecutionConversionResult::Success(CommandExecution::from_raw_invalid(raw_exec)); + } + + let narrative = Narrative { + valid: raw_exec.valid, + reason: raw_exec.reason.take(), + narration: std::mem::take(&mut raw_exec.narration), + }; + + let conversions: Vec<_> = raw_exec + .events + .into_iter() + .map(|raw_event| CommandEvent::new(raw_event)) + .collect(); + + let (converted, conversion_failures): (Vec<_>, Vec<_>) = + conversions.into_iter().partition_map(|res| match res { + Ok(converted_event) => Either::Left(converted_event), + Err(err) => Either::Right(err), + }); + + // Coherence validation of converted events. + let (successes, incoherent_events): (Vec<_>, Vec<_>) = stream::iter(converted.into_iter()) + .then(|event| validate_event_coherence(db, event)) + .collect::>() + .await + .into_iter() + .partition_map(|res| match res { + Ok(event) => Either::Left(event), + Err(err) => Either::Right(err), + }); + + let failure_len = conversion_failures.len() + incoherent_events.len(); + + if successes.len() > 0 && failure_len == 0 { + ExecutionConversionResult::Success(from_raw_success(narrative, successes)) + } else if successes.len() > 0 && failure_len > 0 { + let converted_execution = from_raw_success(narrative, successes); + let failures = + EventConversionFailures::from_failures(conversion_failures, incoherent_events); + ExecutionConversionResult::PartialSuccess(converted_execution, failures) + } else { + ExecutionConversionResult::Failure(EventConversionFailures::from_failures( + conversion_failures, + incoherent_events, + )) + } +} + +fn deserialize_recognized_event( + raw_event: RawCommandEvent, +) -> Result { + let event_name = raw_event.event_name.as_str().to_lowercase(); + let event_name = event_name.as_str(); + + match event_name { + // scene-related + "change_scene" => Ok(CommandEvent::ChangeScene { + scene_key: raw_event + .parameter + .strip_prefix("scenes/") // Mini coherence check + .map(String::from) + .unwrap_or(raw_event.parameter) + .clone(), + }), + + // bodily position-related + "stand" => Ok(CommandEvent::Stand { + target: raw_event.applies_to, + }), + "sit" => Ok(CommandEvent::Sit { + target: raw_event.applies_to, + }), + "prone" => Ok(CommandEvent::Prone { + target: raw_event.applies_to, + }), + "crouch" => Ok(CommandEvent::Crouch { + target: raw_event.applies_to, + }), + + // combat-related + "take_damage" => deserialize_take_damage(raw_event), + + // miscellaneous + "narration" => Ok(CommandEvent::Narration(raw_event.parameter)), + + // unrecognized + _ => Err(EventConversionError::UnrecognizedEvent(raw_event)), + } +} + +fn deserialize_take_damage( + raw_event: RawCommandEvent, +) -> Result { + match raw_event.parameter.parse::() { + Ok(dmg) => Ok(CommandEvent::TakeDamage { + target: raw_event.applies_to, + amount: dmg, + }), + Err(_) => Err(EventConversionError::InvalidParameter(raw_event)), + } +} + +async fn validate_event_coherence<'a>( + db: &Database, + event: CommandEvent, +) -> std::result::Result { + match event { + CommandEvent::ChangeScene { ref scene_key } => match db.stage_exists(&scene_key).await { + Ok(exists) => match exists { + true => Ok(event), + false => Err(invalid_converted_event(event).unwrap()), + }, + Err(err) => Err(invalid_converted_event_because_err(event, err)), + }, + _ => Ok(event), + } +} + +/// The event was converted from the raw response properly, but the +/// information contained in the response is not valid. +fn invalid_converted_event(event: CommandEvent) -> Option { + match event { + CommandEvent::ChangeScene { .. } => Some(EventCoherenceFailure::TargetDoesNotExist(event)), + _ => None, + } +} + +/// The event was converted from the raw response properly, but +/// something went wrong with attempting to check the coherence of the +/// converted event. +fn invalid_converted_event_because_err( + event: CommandEvent, + err: anyhow::Error, +) -> EventCoherenceFailure { + EventCoherenceFailure::OtherError(event, format!("{}", err)) +} diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 08df508..d56f56d 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -1,201 +1,2 @@ -use crate::{ - db::Database, - models::commands::{ - CommandEvent, CommandExecution, EventCoherenceFailure, EventConversionError, - EventConversionFailures, ExecutionConversionResult, RawCommandEvent, RawCommandExecution, - }, -}; -use anyhow::Result; -use futures::stream::{self, StreamExt, TryStreamExt}; -use itertools::{Either, Itertools}; -use std::convert::TryFrom; - -use strum::VariantNames; - -type EventConversionResult = std::result::Result; - -impl CommandEvent { - pub fn new(raw_event: RawCommandEvent) -> EventConversionResult { - let event_name = raw_event.event_name.as_str().to_lowercase(); - - if Self::VARIANTS.contains(&event_name.as_str()) { - deserialize_recognized_event(raw_event) - } else { - Err(EventConversionError::UnrecognizedEvent(raw_event)) - } - } -} - -impl TryFrom for CommandEvent { - type Error = EventConversionError; - - fn try_from(raw_event: RawCommandEvent) -> Result { - CommandEvent::new(raw_event) - } -} - -/// Internal struct to hold the narrative parts of the -/// RawCommandExecution to minimize clones. -struct Narrative { - valid: bool, - reason: Option, - narration: String, -} - -fn from_raw_success(raw: Narrative, events: Vec) -> CommandExecution { - CommandExecution { - events, - valid: raw.valid, - reason: match &raw.reason { - Some(reason) if !raw.valid && reason.is_empty() => { - Some("invalid for unknown reason".to_string()) - } - Some(_) if !raw.valid => raw.reason.clone(), - _ => None, - }, - narration: raw.narration.clone(), - } -} - -pub async fn convert_raw_execution( - mut raw_exec: RawCommandExecution, - db: &Database, -) -> ExecutionConversionResult { - if !raw_exec.valid { - return ExecutionConversionResult::Success(CommandExecution::from_raw_invalid(raw_exec)); - } - - let narrative = Narrative { - valid: raw_exec.valid, - reason: raw_exec.reason.take(), - narration: std::mem::take(&mut raw_exec.narration), - }; - - let conversions: Vec<_> = raw_exec - .events - .into_iter() - .map(|raw_event| CommandEvent::new(raw_event)) - .collect(); - - let (converted, conversion_failures): (Vec<_>, Vec<_>) = - conversions.into_iter().partition_map(|res| match res { - Ok(converted_event) => Either::Left(converted_event), - Err(err) => Either::Right(err), - }); - - // Coherence validation of converted events. - let (successes, incoherent_events): (Vec<_>, Vec<_>) = stream::iter(converted.into_iter()) - .then(|event| validate_event_coherence(db, event)) - .collect::>() - .await - .into_iter() - .partition_map(|res| match res { - Ok(event) => Either::Left(event), - Err(err) => Either::Right(err), - }); - - let failure_len = conversion_failures.len() + incoherent_events.len(); - - if successes.len() > 0 && failure_len == 0 { - ExecutionConversionResult::Success(from_raw_success(narrative, successes)) - } else if successes.len() > 0 && failure_len > 0 { - let converted_execution = from_raw_success(narrative, successes); - let failures = - EventConversionFailures::from_failures(conversion_failures, incoherent_events); - ExecutionConversionResult::PartialSuccess(converted_execution, failures) - } else { - ExecutionConversionResult::Failure(EventConversionFailures::from_failures( - conversion_failures, - incoherent_events, - )) - } -} - -fn deserialize_recognized_event( - raw_event: RawCommandEvent, -) -> Result { - let event_name = raw_event.event_name.as_str().to_lowercase(); - let event_name = event_name.as_str(); - - match event_name { - // scene-related - "change_scene" => Ok(CommandEvent::ChangeScene { - scene_key: raw_event - .parameter - .strip_prefix("scenes/") // Mini coherence check - .map(String::from) - .unwrap_or(raw_event.parameter) - .clone(), - }), - - // bodily position-related - "stand" => Ok(CommandEvent::Stand { - target: raw_event.applies_to, - }), - "sit" => Ok(CommandEvent::Sit { - target: raw_event.applies_to, - }), - "prone" => Ok(CommandEvent::Prone { - target: raw_event.applies_to, - }), - "crouch" => Ok(CommandEvent::Crouch { - target: raw_event.applies_to, - }), - - // combat-related - "take_damage" => deserialize_take_damage(raw_event), - - // miscellaneous - "narration" => Ok(CommandEvent::Narration(raw_event.parameter)), - - // unrecognized - _ => Err(EventConversionError::UnrecognizedEvent(raw_event)), - } -} - -fn deserialize_take_damage( - raw_event: RawCommandEvent, -) -> Result { - match raw_event.parameter.parse::() { - Ok(dmg) => Ok(CommandEvent::TakeDamage { - target: raw_event.applies_to, - amount: dmg, - }), - Err(_) => Err(EventConversionError::InvalidParameter(raw_event)), - } -} - -async fn validate_event_coherence<'a>( - db: &Database, - event: CommandEvent, -) -> std::result::Result { - match event { - CommandEvent::ChangeScene { ref scene_key } => match db.stage_exists(&scene_key).await { - Ok(exists) => match exists { - true => Ok(event), - false => Err(invalid_converted_event(event).unwrap()), - }, - Err(err) => Err(invalid_converted_event_because_err(event, err)), - }, - _ => Ok(event), - } -} - -/// The event was converted from the raw response properly, but the -/// information contained in the response is not valid. -fn invalid_converted_event(event: CommandEvent) -> Option { - match event { - CommandEvent::ChangeScene { .. } => Some(EventCoherenceFailure::TargetDoesNotExist(event)), - _ => None, - } -} - -/// The event was converted from the raw response properly, but -/// something went wrong with attempting to check the coherence of the -/// converted event. -fn invalid_converted_event_because_err( - event: CommandEvent, - err: anyhow::Error, -) -> EventCoherenceFailure { - EventCoherenceFailure::OtherError(event, format!("{}", err)) -} +pub mod converter; +pub mod builtins;