Switch to one event output per command execution. Simplify coherence.

This commit is contained in:
projectmoon 2024-01-31 13:01:13 +01:00
parent d23f09295e
commit c6f10f7a61
9 changed files with 237 additions and 290 deletions

View File

@ -1,5 +1,5 @@
use crate::ai::convo::AiPrompt; use crate::ai::convo::AiPrompt;
use crate::models::commands::{CommandEvent, EventConversionFailures, ParsedCommand}; use crate::models::commands::{CommandEvent, EventConversionFailure, ParsedCommand};
use crate::models::world::items::Item; use crate::models::world::items::Item;
use crate::models::world::people::Person; use crate::models::world::people::Person;
use crate::models::world::scenes::{Exit, Prop, Scene, Stage}; use crate::models::world::scenes::{Exit, Prop, Scene, Stage};
@ -75,8 +75,7 @@ impl<'a> From<&'a Exit> for ExitTableRow<'a> {
const COMMAND_EXECUTION_BNF: &'static str = r#" const COMMAND_EXECUTION_BNF: &'static str = r#"
root ::= CommandExecution root ::= CommandExecution
CommandEvent ::= "{" ws "\"eventName\":" ws string "," ws "\"appliesTo\":" ws string "," ws "\"parameter\":" ws string "}" CommandEvent ::= "{" ws "\"eventName\":" ws string "," ws "\"appliesTo\":" ws string "," ws "\"parameter\":" ws string "}"
CommandEventlist ::= "[]" | "[" ws CommandEvent ("," ws CommandEvent)* "]" CommandExecution ::= "{" ws "\"valid\":" ws boolean "," ws "\"reason\":" ws string "," ws "\"narration\":" ws string "," ws "\"event\":" ws CommandEvent "}"
CommandExecution ::= "{" ws "\"valid\":" ws boolean "," ws "\"reason\":" ws string "," ws "\"narration\":" ws string "," ws "\"events\":" ws CommandEventlist "}"
CommandExecutionlist ::= "[]" | "[" ws CommandExecution ("," ws CommandExecution)* "]" CommandExecutionlist ::= "[]" | "[" ws CommandExecution ("," ws CommandExecution)* "]"
string ::= "\"" ([^"]*) "\"" string ::= "\"" ([^"]*) "\""
boolean ::= "true" | "false" boolean ::= "true" | "false"
@ -113,16 +112,17 @@ The `events` field must be filled with entries if the command is valid. It is a
- `name`: The name of the event, which can be one of the ones detailed below. - `name`: The name of the event, which can be one of the ones detailed below.
- `appliesTo`: The player, item, NPC, or other entity in the scene. - `appliesTo`: The player, item, NPC, or other entity in the scene.
- The event applies only to one target. - The event applies only to one target.
- The `appliesTo` field should be the `key` of the target. If no key was provided, use the target's name instead. - The `appliesTo` field should be the `key` of the target. If no key was provided, use the target's name instead. The `key` is usualy a UUID.
- `parameter`: Optional parameter with a string value that will be parsed. Parameters allowed depend on the type of event, and are detailed below. - `parameter`: Optional parameter with a string value that will be parsed. Parameters allowed depend on the type of event, and are detailed below.
The following events can be generated: The following events can be generated:
- `change_scene`: The player's current scene is changed. - `change_scene`: The player's current scene is changed.
- `appliesTo` must be set to `player`. - `appliesTo` must be set to `player`.
- `parameter` must be the Scene Key of the new scene. - `parameter` must be the Scene Key of the new scene. This is a UUID.
- `look_at_entity`: The player is looking at an entity--a person, prop, or item in the scene. - `look_at_entity`: The player is looking at an entity--a person, prop, or item in the scene.
- `appliesTo` is the Scene Key of the current scene. - `appliesTo` is the key of the person, prop, or item being looked at.
- `parameter` is the Entity Key of the entity being looked at. - `appliesTo` must NOT be the **NAME** of the entity. It **MUST** be the UUID key.
- `parameter` is irrelevant for this event.
- `take_damage`: The target of the event takes an amount of damage. - `take_damage`: The target of the event takes an amount of damage.
- `appliesTo` must be the target taking damage (player, NPC, item, prop, or other thing in the scene) - `appliesTo` must be the target taking damage (player, NPC, item, prop, or other thing in the scene)
- `parameter` must be the amount of damage taken. This value must be a positive integer. - `parameter` must be the amount of damage taken. This value must be a positive integer.
@ -145,6 +145,8 @@ The following events can be generated:
- `appliesTo` must be the target in the scene that the event would apply to, if it was a valid event. - `appliesTo` must be the target in the scene that the event would apply to, if it was a valid event.
- `parameter` should be a value that theoretically makes sense, if this event was a valid event. - `parameter` should be a value that theoretically makes sense, if this event was a valid event.
Make sure the `appliesTo` field and `parameter` field are UUIDs, if the event requires it.
Check that the events make sense and are generated correctly, given the original command. Check that the events make sense and are generated correctly, given the original command.
The original command is the raw text entered by the player. The original command is the raw text entered by the player.
@ -256,6 +258,6 @@ pub fn execution_prompt(original_cmd: &str, stage: &Stage, cmd: &ParsedCommand)
AiPrompt::new_with_grammar_and_size(&prompt, COMMAND_EXECUTION_BNF, 512) AiPrompt::new_with_grammar_and_size(&prompt, COMMAND_EXECUTION_BNF, 512)
} }
pub fn fix_prompt(scene: &Scene, failures: &EventConversionFailures) -> AiPrompt { pub fn fix_prompt(scene: &Scene, failures: &EventConversionFailure) -> AiPrompt {
AiPrompt::new("") AiPrompt::new("")
} }

View File

@ -14,7 +14,7 @@ use futures::{future, TryFutureExt};
use std::rc::Rc; use std::rc::Rc;
use uuid::Uuid; use uuid::Uuid;
type CoherenceResult = Result<CommandEvent, EventCoherenceFailure>; type CoherenceResult = Result<AiCommand, EventCoherenceFailure>;
pub struct CommandCoherence<'a> { pub struct CommandCoherence<'a> {
logic: Rc<AiLogic>, logic: Rc<AiLogic>,
@ -35,33 +35,20 @@ impl CommandCoherence<'_> {
} }
} }
pub async fn fix_incoherent_events( pub async fn fix_incoherent_event(
&self, &self,
failures: Vec<EventCoherenceFailure>, failure: EventCoherenceFailure,
) -> ExecutionConversionResult { ) -> ExecutionConversionResult {
let (successes, failures) = partition!(
stream::iter(failures.into_iter()).then(|failure| self.cohere_event(failure))
);
// TODO we need to use LLM on events that have failed non-LLM coherence. // TODO we need to use LLM on events that have failed non-LLM coherence.
let coherent_event = self.cohere_event(failure).await?;
if successes.len() > 0 && failures.len() == 0 { Ok(coherent_event)
ExecutionConversionResult::Success(AiCommand::from_events(successes))
} else if successes.len() > 0 && failures.len() > 0 {
ExecutionConversionResult::PartialSuccess(
AiCommand::from_events(successes),
failures.into(),
)
} else {
ExecutionConversionResult::Failure(failures.into())
}
} }
async fn cohere_event(&self, failure: EventCoherenceFailure) -> CoherenceResult { async fn cohere_event(&self, failure: EventCoherenceFailure) -> CoherenceResult {
let event_fix = async { let event_fix = async {
match failure { match failure {
EventCoherenceFailure::TargetDoesNotExist(event) => { EventCoherenceFailure::TargetDoesNotExist(cmd) => {
self.fix_target_does_not_exist(event).await self.fix_target_does_not_exist(cmd).await
} }
EventCoherenceFailure::OtherError(event, _) => future::ok(event).await, EventCoherenceFailure::OtherError(event, _) => future::ok(event).await,
} }
@ -72,20 +59,22 @@ impl CommandCoherence<'_> {
.await .await
} }
async fn fix_target_does_not_exist(&self, mut event: CommandEvent) -> CoherenceResult { async fn fix_target_does_not_exist(&self, mut cmd: AiCommand) -> CoherenceResult {
if let CommandEvent::LookAtEntity { if cmd.event.is_none() {
ref mut entity_key, return Ok(cmd);
ref mut scene_key, }
} = event
{ let event: &mut CommandEvent = cmd.event.as_mut().unwrap();
let res = cohere_scene_and_entity(&self.db, &self.stage, entity_key, scene_key).await;
if let CommandEvent::LookAtEntity(ref mut entity_key) = event {
let res = cohere_scene_and_entity(&self.db, &self.stage, entity_key).await;
match res { match res {
Ok(_) => Ok(event), Ok(_) => Ok(cmd),
Err(err) => Err(EventCoherenceFailure::OtherError(event, err.to_string())), Err(err) => Err(EventCoherenceFailure::OtherError(cmd, err.to_string())),
} }
} else { } else {
Ok(event) Ok(cmd)
} }
} }
} }
@ -96,29 +85,11 @@ async fn cohere_scene_and_entity(
db: &Database, db: &Database,
stage: &Stage, stage: &Stage,
entity_key: &mut String, entity_key: &mut String,
scene_key: &mut String,
) -> AnyhowResult<()> { ) -> AnyhowResult<()> {
// Normalize UUIDs, assuming that they are proper UUIDs. // Normalize UUIDs, assuming that they are proper UUIDs.
normalize_keys(scene_key, entity_key); normalize_keys(&mut [entity_key]);
// Sometimes scene key is actually the entity key, and the entity let scene_key = &stage.key;
// key is blank.
if !scene_key.is_empty() && scene_key != &stage.key {
// Check if scene key is an entity
if db.entity_exists(&stage.key, &scene_key).await? {
entity_key.clear();
entity_key.push_str(&scene_key);
scene_key.clear();
scene_key.push_str(&stage.key);
}
}
// If scene key isn't valid, override it from known-good
// information.
if !is_valid_scene_key(scene_key) {
scene_key.clear();
scene_key.push_str(&stage.key);
}
// If entity key is not a valid UUID at this point, then we have // If entity key is not a valid UUID at this point, then we have
// entered a weird situation. // entered a weird situation.
@ -132,14 +103,11 @@ async fn cohere_scene_and_entity(
return Err(anyhow!("Scene key and entity key are the same")); return Err(anyhow!("Scene key and entity key are the same"));
} }
// It is often likely that the scene key and entity key are reversed. // Final result is if the entity actually exists or not now.
if db.entity_exists(&entity_key, &scene_key).await? { db.entity_exists(entity_key).await.map(|_| ())
std::mem::swap(entity_key, scene_key);
}
Ok(())
} }
#[allow(dead_code)]
fn is_valid_scene_key(scene_key: &str) -> bool { fn is_valid_scene_key(scene_key: &str) -> bool {
scene_key == root_scene_id() || Uuid::try_parse(&scene_key).is_ok() scene_key == root_scene_id() || Uuid::try_parse(&scene_key).is_ok()
} }
@ -157,15 +125,12 @@ pub fn strip_prefixes(value: String) -> String {
} }
/// Make sure entity keys are valid UUIDs, and fix them if possible. /// Make sure entity keys are valid UUIDs, and fix them if possible.
fn normalize_keys(scene_key: &mut String, entity_key: &mut String) { pub(super) fn normalize_keys(keys: &mut [&mut String]) {
if let Some(normalized) = normalize_uuid(&scene_key) { for key in keys {
scene_key.clear(); if let Some(normalized) = normalize_uuid(&key) {
scene_key.push_str(&normalized); key.clear();
} key.push_str(&normalized);
}
if let Some(normalized) = normalize_uuid(&entity_key) {
entity_key.clear();
entity_key.push_str(&normalized);
} }
} }

View File

@ -1,125 +1,77 @@
use super::coherence::strip_prefixes; use super::coherence::strip_prefixes;
use super::partition;
use crate::{ use crate::{
db::Database, db::Database,
models::commands::{ models::commands::{
AiCommand, CommandEvent, EventCoherenceFailure, EventConversionError, AiCommand, CommandEvent, EventCoherenceFailure, EventParsingFailure,
EventConversionFailures, ExecutionConversionResult, RawCommandEvent, RawCommandExecution, ExecutionConversionResult, Narrative, RawCommandEvent, RawCommandExecution,
}, },
}; };
use anyhow::Result; use anyhow::Result;
use futures::stream::{self, StreamExt, TryStreamExt};
use itertools::{Either, Itertools};
use std::convert::TryFrom; use std::convert::TryFrom;
use strum::VariantNames; use strum::VariantNames;
type EventConversionResult = std::result::Result<CommandEvent, EventConversionError>; type EventParsingResult = std::result::Result<CommandEvent, EventParsingFailure>;
impl CommandEvent { impl CommandEvent {
pub fn new(raw_event: RawCommandEvent) -> EventConversionResult { pub fn new(raw_event: RawCommandEvent) -> EventParsingResult {
let event_name = raw_event.event_name.as_str().to_lowercase(); let event_name = raw_event.event_name.as_str().to_lowercase();
if Self::VARIANTS.contains(&event_name.as_str()) { if Self::VARIANTS.contains(&event_name.as_str()) {
deserialize_recognized_event(raw_event) deserialize_recognized_event(raw_event)
} else { } else {
Err(EventConversionError::UnrecognizedEvent(raw_event)) Err(EventParsingFailure::UnrecognizedEvent(raw_event))
} }
} }
} }
impl TryFrom<RawCommandEvent> for CommandEvent { impl TryFrom<RawCommandEvent> for CommandEvent {
type Error = EventConversionError; type Error = EventParsingFailure;
fn try_from(raw_event: RawCommandEvent) -> Result<Self, Self::Error> { fn try_from(raw_event: RawCommandEvent) -> Result<Self, Self::Error> {
CommandEvent::new(raw_event) CommandEvent::new(raw_event)
} }
} }
/// Internal struct to hold the narrative parts of the
/// RawCommandExecution to minimize clones.
struct Narrative {
valid: bool,
reason: Option<String>,
narration: String,
}
fn from_raw_success(raw: Narrative, events: Vec<CommandEvent>) -> AiCommand {
AiCommand {
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,
_ => None,
},
narration: raw.narration,
}
}
pub async fn convert_raw_execution( pub async fn convert_raw_execution(
mut raw_exec: RawCommandExecution, mut raw_exec: RawCommandExecution,
db: &Database, db: &Database,
) -> ExecutionConversionResult { ) -> ExecutionConversionResult {
if !raw_exec.valid { if !raw_exec.valid {
return ExecutionConversionResult::Success(AiCommand::from_raw_invalid(raw_exec)); return Ok(AiCommand::from_raw_invalid(raw_exec));
} }
if raw_exec.event.is_none() {
return Ok(AiCommand::empty());
}
let raw_event = raw_exec.event.unwrap();
let narrative = Narrative { let narrative = Narrative {
valid: raw_exec.valid, valid: raw_exec.valid,
reason: raw_exec.reason.take(), reason: raw_exec.reason.take(),
narration: std::mem::take(&mut raw_exec.narration), narration: std::mem::take(&mut raw_exec.narration),
}; };
let conversions: Vec<_> = raw_exec let converted_event = CommandEvent::new(raw_event)?;
.events let cmd = AiCommand::from_raw_success(narrative, converted_event);
.into_iter() validate_event_coherence(db, cmd)
.map(|raw_event| CommandEvent::new(raw_event)) .await
.collect(); .map_err(|e| e.into())
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) = partition!(
stream::iter(converted.into_iter()).then(|event| validate_event_coherence(db, event))
);
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( fn deserialize_recognized_event(
raw_event: RawCommandEvent, raw_event: RawCommandEvent,
) -> Result<CommandEvent, EventConversionError> { ) -> Result<CommandEvent, EventParsingFailure> {
let event_name = raw_event.event_name.as_str().to_lowercase(); let event_name = raw_event.event_name.as_str().to_lowercase();
let event_name = event_name.as_str(); let event_name = event_name.as_str();
match event_name { match event_name {
// informational-related // informational-related
"narration" => Ok(CommandEvent::Narration(raw_event.parameter)), "narration" => Ok(CommandEvent::Narration(raw_event.parameter)),
"look_at_entity" => Ok(CommandEvent::LookAtEntity { "look_at_entity" => Ok(CommandEvent::LookAtEntity(
entity_key: strip_prefixes(raw_event.parameter), deserialize_and_normalize(raw_event),
scene_key: strip_prefixes(raw_event.applies_to), )),
}),
// scene-related // scene-related
"change_scene" => Ok(CommandEvent::ChangeScene { "change_scene" => Ok(CommandEvent::ChangeScene {
@ -144,54 +96,77 @@ fn deserialize_recognized_event(
"take_damage" => deserialize_take_damage(raw_event), "take_damage" => deserialize_take_damage(raw_event),
// unrecognized // unrecognized
_ => Err(EventConversionError::UnrecognizedEvent(raw_event)), _ => Err(EventParsingFailure::UnrecognizedEvent(raw_event)),
}
}
/// Deserialize and normalize an expected UUID parameter.
fn deserialize_and_normalize(raw_event: RawCommandEvent) -> String {
let mut key = if !raw_event.applies_to.is_empty() {
raw_event.applies_to
} else {
raw_event.parameter
};
let mut key = strip_prefixes(key);
super::coherence::normalize_keys(&mut [&mut key]);
key
}
fn deserialize_single(raw_event: RawCommandEvent) -> String {
if !raw_event.applies_to.is_empty() {
raw_event.applies_to
} else {
raw_event.parameter
} }
} }
fn deserialize_take_damage( fn deserialize_take_damage(
raw_event: RawCommandEvent, raw_event: RawCommandEvent,
) -> Result<CommandEvent, EventConversionError> { ) -> Result<CommandEvent, EventParsingFailure> {
match raw_event.parameter.parse::<u32>() { match raw_event.parameter.parse::<u32>() {
Ok(dmg) => Ok(CommandEvent::TakeDamage { Ok(dmg) => Ok(CommandEvent::TakeDamage {
target: strip_prefixes(raw_event.applies_to), target: strip_prefixes(raw_event.applies_to),
amount: dmg, amount: dmg,
}), }),
Err(_) => Err(EventConversionError::InvalidParameter(raw_event)), Err(_) => Err(EventParsingFailure::InvalidParameter(raw_event)),
} }
} }
pub(super) async fn validate_event_coherence<'a>( pub(super) async fn validate_event_coherence<'a>(
db: &Database, db: &Database,
event: CommandEvent, cmd: AiCommand,
) -> std::result::Result<CommandEvent, EventCoherenceFailure> { ) -> std::result::Result<AiCommand, EventCoherenceFailure> {
match event { if cmd.event.is_none() {
CommandEvent::LookAtEntity { return Ok(cmd);
ref entity_key, }
ref scene_key,
} => match db.entity_exists(&scene_key, &entity_key).await { match cmd.event.as_ref().unwrap() {
CommandEvent::LookAtEntity(ref entity_key) => match db.entity_exists(&entity_key).await {
Ok(exists) => match exists { Ok(exists) => match exists {
true => Ok(event), true => Ok(cmd),
false => Err(invalid_converted_event(event).unwrap()), false => Err(invalid_converted_event(cmd).unwrap()),
}, },
Err(err) => Err(invalid_converted_event_because_err(event, err)), Err(err) => Err(invalid_converted_event_because_err(cmd, err)),
}, },
CommandEvent::ChangeScene { ref scene_key } => match db.stage_exists(&scene_key).await { CommandEvent::ChangeScene { ref scene_key } => match db.stage_exists(&scene_key).await {
Ok(exists) => match exists { Ok(exists) => match exists {
true => Ok(event), true => Ok(cmd),
false => Err(invalid_converted_event(event).unwrap()), false => Err(invalid_converted_event(cmd).unwrap()),
}, },
Err(err) => Err(invalid_converted_event_because_err(event, err)), Err(err) => Err(invalid_converted_event_because_err(cmd, err)),
}, },
_ => Ok(event), _ => Ok(cmd),
} }
} }
/// The event was converted from the raw response properly, but the /// The event was converted from the raw response properly, but the
/// information contained in the response is not valid. /// information contained in the response is not valid.
fn invalid_converted_event(event: CommandEvent) -> Option<EventCoherenceFailure> { fn invalid_converted_event(mut cmd: AiCommand) -> Option<EventCoherenceFailure> {
match event { match cmd.event.as_mut().unwrap() {
CommandEvent::LookAtEntity { .. } => Some(EventCoherenceFailure::TargetDoesNotExist(event)), CommandEvent::LookAtEntity { .. } => Some(EventCoherenceFailure::TargetDoesNotExist(cmd)),
CommandEvent::ChangeScene { .. } => Some(EventCoherenceFailure::TargetDoesNotExist(event)), CommandEvent::ChangeScene { .. } => Some(EventCoherenceFailure::TargetDoesNotExist(cmd)),
_ => None, _ => None,
} }
} }
@ -200,8 +175,8 @@ fn invalid_converted_event(event: CommandEvent) -> Option<EventCoherenceFailure>
/// something went wrong with attempting to check the coherence of the /// something went wrong with attempting to check the coherence of the
/// converted event. /// converted event.
fn invalid_converted_event_because_err( fn invalid_converted_event_because_err(
event: CommandEvent, cmd: AiCommand,
err: anyhow::Error, err: anyhow::Error,
) -> EventCoherenceFailure { ) -> EventCoherenceFailure {
EventCoherenceFailure::OtherError(event, format!("{}", err)) EventCoherenceFailure::OtherError(cmd, format!("{}", err))
} }

View File

@ -4,7 +4,8 @@ use crate::{
models::{ models::{
commands::{ commands::{
AiCommand, CommandEvent, CommandExecution, EventCoherenceFailure, AiCommand, CommandEvent, CommandExecution, EventCoherenceFailure,
ExecutionConversionResult, ParsedCommand, ParsedCommands, RawCommandExecution, EventConversionFailure, ExecutionConversionResult, ParsedCommand, ParsedCommands,
RawCommandExecution,
}, },
world::scenes::Stage, world::scenes::Stage,
}, },
@ -119,33 +120,15 @@ impl CommandExecutor {
let converted = converter::convert_raw_execution(raw_exec, &self.db).await; let converted = converter::convert_raw_execution(raw_exec, &self.db).await;
let execution: Result<AiCommand> = match converted { let execution: AiCommand = match converted {
ExecutionConversionResult::Success(execution) => Ok(execution), Ok(ai_command) => Ok(ai_command),
ExecutionConversionResult::PartialSuccess(mut execution, failures) => { Err(failure) => {
// TODO also deal with conversion failures // TODO also deal with conversion failures
// TODO deal with failures to fix incoherent events. // TODO deal with failures to fix incoherent events.
// right now we just drop them. // right now we just drop them.
let mut fixed_events = self self.fix_incoherence(stage, failure).await
.fix_incoherence(stage, failures.coherence_failures)
.await
.ok()
.unwrap_or(vec![]);
execution.events.append(&mut fixed_events);
Ok(execution)
} }
ExecutionConversionResult::Failure(failures) => { }?;
// TODO also deal with conversion failures
// For a complete failure, we want to make sure all
// events become coherent.
Ok(AiCommand::from_events(
self.fix_incoherence(stage, failures.coherence_failures)
.await?,
))
}
};
let execution = execution?;
Ok(CommandExecution::AiCommand(execution)) Ok(CommandExecution::AiCommand(execution))
} }
@ -153,18 +136,15 @@ impl CommandExecutor {
async fn fix_incoherence( async fn fix_incoherence(
&self, &self,
stage: &Stage, stage: &Stage,
failures: Vec<EventCoherenceFailure>, failure: EventConversionFailure,
) -> Result<Vec<CommandEvent>> { ) -> std::result::Result<AiCommand, EventConversionFailure> {
println!("Attempting to fix {} incoherent events", failures.len()); if let EventConversionFailure::CoherenceFailure(coherence_failure) = failure {
let fixer = coherence::CommandCoherence::new(&self.logic, &self.db, stage); let fixer = coherence::CommandCoherence::new(&self.logic, &self.db, stage);
// TODO should do something w/ partial failures. // TODO should do something w/ partial failures.
let events = match fixer.fix_incoherent_events(failures).await { fixer.fix_incoherent_event(coherence_failure).await
ExecutionConversionResult::Success(AiCommand { events, .. }) => Ok(events), } else {
ExecutionConversionResult::PartialSuccess(AiCommand { events, .. }, _) => Ok(events), Err(failure)
ExecutionConversionResult::Failure(errs) => Err(errs), }
}?;
Ok(events)
} }
} }

View File

@ -1,6 +1,6 @@
use crate::models::commands::{CachedParsedCommand, ParsedCommand, ParsedCommands}; 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, Entity}; use crate::models::{Content, ContentContainer, Entity, Insertable};
use anyhow::Result; use anyhow::Result;
use arangors::document::options::InsertOptions; use arangors::document::options::InsertOptions;
use arangors::graph::{EdgeDefinition, Graph}; use arangors::graph::{EdgeDefinition, Graph};
@ -384,11 +384,9 @@ impl Database {
Ok(stage_count > 0) Ok(stage_count > 0)
} }
pub async fn entity_exists(&self, scene_key: &str, entity_key: &str) -> Result<bool> { pub async fn entity_exists(&self, entity_key: &str) -> Result<bool> {
let mut vars = HashMap::new(); let mut vars = HashMap::new();
vars.insert("@scene_collection", SCENE_COLLECTION.into());
vars.insert("scene_key", to_json_value(scene_key).unwrap());
vars.insert("entity_key", to_json_value(entity_key).unwrap()); vars.insert("entity_key", to_json_value(entity_key).unwrap());
let db = self.db().await?; let db = self.db().await?;
@ -400,9 +398,13 @@ impl Database {
Ok(entity_count > 0) Ok(entity_count > 0)
} }
pub async fn load_entity(&self, scene_key: &str, entity_key: &str) -> Result<Option<Entity>> { pub async fn load_entity_in_scene(
&self,
scene_key: &str,
entity_key: &str,
) -> Result<Option<Entity>> {
let aql = AqlQuery::builder() let aql = AqlQuery::builder()
.query(queries::LOAD_ENTITY) .query(queries::LOAD_ENTITY_IN_SCENE)
.bind_var("@scene_collection", SCENE_COLLECTION) .bind_var("@scene_collection", SCENE_COLLECTION)
.bind_var("scene_key", to_json_value(scene_key)?) .bind_var("scene_key", to_json_value(scene_key)?)
.bind_var("entity_key", to_json_value(entity_key)?) .bind_var("entity_key", to_json_value(entity_key)?)
@ -412,6 +414,16 @@ impl Database {
Ok(take_first(results)) Ok(take_first(results))
} }
pub async fn load_entity(&self, entity_key: &str) -> Result<Option<Entity>> {
let aql = AqlQuery::builder()
.query(queries::LOAD_ENTITY)
.bind_var("entity_key", to_json_value(entity_key)?)
.build();
let results = self.db().await?.aql_query(aql).await?;
Ok(take_first(results))
}
pub async fn cache_command( pub async fn cache_command(
&self, &self,
raw_cmd: &str, raw_cmd: &str,

View File

@ -24,23 +24,40 @@ pub const LOAD_STAGE: &'static str = r#"
} }
"#; "#;
pub const LOAD_ENTITY_IN_SCENE: &'static str = r#"
LET entities = (
FOR scene IN @@scene_collection
FILTER scene._key == @scene_key
LET occupants = (FOR v, edge IN OUTBOUND scene._id GRAPH 'world'
FILTER edge.relation == "scene-has-person" and v._key == @entity_key
RETURN MERGE({ "type": "Person"}, v))
LET items = (FOR v, edge IN OUTBOUND scene._id GRAPH 'world'
FILTER edge.relation == "item-located-at" and v._key == @entity_key
RETURN MERGE({ "type": "Item" }, v ))
RETURN FIRST(APPEND(occupants, items)))
FOR ent in entities
FILTER ent != null
RETURN ent
"#;
pub const LOAD_ENTITY: &'static str = r#" pub const LOAD_ENTITY: &'static str = r#"
LET entities = ( LET entities = (
FOR scene IN @@scene_collection LET people = (FOR person in people
FILTER scene._key == @scene_key FILTER person._key == @entity_key
LET occupants = (FOR v, edge IN OUTBOUND scene._id GRAPH 'world' RETURN MERGE({ "type": "Person"}, person))
FILTER edge.relation == "scene-has-person" and v._key == @entity_key
RETURN MERGE({ "type": "Person"}, v))
LET items = (FOR v, edge IN OUTBOUND scene._id GRAPH 'world' LET items = (FOR item in items
FILTER edge.relation == "item-located-at" and v._key == @entity_key FILTER item._key == @entity_key
RETURN MERGE({ "type": "Item" }, v )) RETURN MERGE({ "type": "Item" }, item))
RETURN FIRST(APPEND(occupants, items))) RETURN FIRST(APPEND(people, items)))
FOR ent in entities FOR ent in entities
FILTER ent != null FILTER ent != null
RETURN ent RETURN ent
"#; "#;
pub const UPSERT_SCENE: &'static str = r#" pub const UPSERT_SCENE: &'static str = r#"

View File

@ -1,6 +1,6 @@
use crate::io::display; use crate::io::display;
use crate::models::commands::{ use crate::models::commands::{
AiCommand, BuiltinCommand, CommandExecution, ExecutionConversionResult, EventConversionFailures, AiCommand, BuiltinCommand, CommandExecution, ExecutionConversionResult, EventConversionFailure,
}; };
use crate::state::GameState; use crate::state::GameState;
use crate::{commands::CommandExecutor, db::Database}; use crate::{commands::CommandExecutor, db::Database};
@ -42,8 +42,7 @@ impl GameLoop {
} }
display!("\n\n{}\n\n", execution.narration); display!("\n\n{}\n\n", execution.narration);
if let Some(event) = execution.event {
for event in execution.events {
self.state.update(event).await?; self.state.update(event).await?;
} }

View File

@ -60,7 +60,8 @@ pub struct RawCommandExecution {
pub valid: bool, pub valid: bool,
pub reason: Option<String>, pub reason: Option<String>,
pub narration: String, pub narration: String,
pub events: Vec<RawCommandEvent>, #[serde(skip_serializing_if = "Option::is_none")]
pub event: Option<RawCommandEvent>,
} }
impl RawCommandExecution { impl RawCommandExecution {
@ -69,7 +70,7 @@ impl RawCommandExecution {
valid: true, valid: true,
reason: None, reason: None,
narration: "".to_string(), narration: "".to_string(),
events: vec![], event: None,
} }
} }
} }
@ -88,10 +89,7 @@ pub struct RawCommandEvent {
pub enum CommandEvent { pub enum CommandEvent {
// Informational // Informational
Narration(String), Narration(String),
LookAtEntity { LookAtEntity(String),
entity_key: String,
scene_key: String,
},
// Movement-related // Movement-related
ChangeScene { ChangeScene {
@ -137,6 +135,14 @@ pub enum CommandExecution {
AiCommand(AiCommand), AiCommand(AiCommand),
} }
/// Simple struct to hold the narrative parts of the
/// RawCommandExecution to minimize clones.
pub struct Narrative {
pub valid: bool,
pub reason: Option<String>,
pub narration: String,
}
/// An "AI Command" is a command execution generated by the LLM and /// An "AI Command" is a command execution generated by the LLM and
/// run through coherence validation/fixing, and (assuming it is /// run through coherence validation/fixing, and (assuming it is
/// valid) contains a series of events to apply to the game state. /// valid) contains a series of events to apply to the game state.
@ -145,16 +151,31 @@ 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 event: Option<CommandEvent>,
} }
impl AiCommand { impl AiCommand {
fn from_narrative_and_event(narrative: Narrative, event: CommandEvent) -> AiCommand {
AiCommand {
event: Some(event),
valid: narrative.valid,
reason: match &narrative.reason {
Some(reason) if !narrative.valid && reason.is_empty() => {
Some("invalid for unknown reason".to_string())
}
Some(_) if !narrative.valid => narrative.reason,
_ => None,
},
narration: narrative.narration,
}
}
pub fn empty() -> AiCommand { pub fn empty() -> AiCommand {
AiCommand { AiCommand {
valid: true, valid: true,
reason: None, reason: None,
narration: "".to_string(), narration: "".to_string(),
events: vec![], event: None,
} }
} }
@ -163,68 +184,48 @@ impl AiCommand {
valid: raw.valid, valid: raw.valid,
reason: raw.reason, reason: raw.reason,
narration: "".to_string(), narration: "".to_string(),
events: vec![], event: None,
} }
} }
pub fn from_events(events: Vec<CommandEvent>) -> AiCommand { pub fn from_raw_success(narrative: Narrative, event: CommandEvent) -> AiCommand {
Self::from_narrative_and_event(narrative, event)
}
pub fn from_event(event: CommandEvent) -> AiCommand {
AiCommand { AiCommand {
valid: true, valid: true,
reason: None, reason: None,
narration: "".to_string(), narration: "".to_string(),
events, event: Some(event),
} }
} }
} }
#[derive(Clone, Debug)] pub type ExecutionConversionResult = std::result::Result<AiCommand, EventConversionFailure>;
pub enum ExecutionConversionResult {
Success(AiCommand), #[derive(Error, Clone, Debug)]
PartialSuccess(AiCommand, EventConversionFailures), pub enum EventConversionFailure {
Failure(EventConversionFailures), #[error("parsing failure: {0:?}")]
ParsingFailure(EventParsingFailure),
#[error("coherence failure: {0:?}")]
CoherenceFailure(EventCoherenceFailure),
} }
impl Display for ExecutionConversionResult { impl From<EventCoherenceFailure> for EventConversionFailure {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn from(value: EventCoherenceFailure) -> Self {
write!(f, "{:?}", self) EventConversionFailure::CoherenceFailure(value)
}
}
impl From<EventParsingFailure> for EventConversionFailure {
fn from(value: EventParsingFailure) -> Self {
EventConversionFailure::ParsingFailure(value)
} }
} }
#[derive(Error, Clone, Debug)] #[derive(Error, Clone, Debug)]
pub struct EventConversionFailures { pub enum EventParsingFailure {
pub conversion_failures: Vec<EventConversionError>,
pub coherence_failures: Vec<EventCoherenceFailure>,
}
impl EventConversionFailures {
pub fn from_failures(
conversion_failures: Vec<EventConversionError>,
coherence_failures: Vec<EventCoherenceFailure>,
) -> EventConversionFailures {
EventConversionFailures {
conversion_failures,
coherence_failures,
}
}
}
impl From<Vec<EventCoherenceFailure>> for EventConversionFailures {
fn from(value: Vec<EventCoherenceFailure>) -> Self {
EventConversionFailures {
coherence_failures: value,
conversion_failures: vec![],
}
}
}
impl Display for EventConversionFailures {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:?}", self)
}
}
#[derive(Error, Clone, Debug)]
pub enum EventConversionError {
#[error("invalid parameter for {0:?}")] #[error("invalid parameter for {0:?}")]
InvalidParameter(RawCommandEvent), InvalidParameter(RawCommandEvent),
@ -235,24 +236,18 @@ pub enum EventConversionError {
#[derive(Error, Clone, Debug)] #[derive(Error, Clone, Debug)]
pub enum EventCoherenceFailure { pub enum EventCoherenceFailure {
#[error("target of command does not exist")] #[error("target of command does not exist")]
TargetDoesNotExist(CommandEvent), TargetDoesNotExist(AiCommand),
#[error("uncategorized coherence failure: {1}")] #[error("uncategorized coherence failure: {1}")]
OtherError(CommandEvent, String), OtherError(AiCommand, String),
} }
impl EventCoherenceFailure { impl EventCoherenceFailure {
/// Consume self to extract the CommandEvent wrapped in this enum. /// Consume self to extract the CommandEvent wrapped in this enum.
pub fn as_event(self) -> CommandEvent { pub fn as_event(self) -> Option<CommandEvent> {
match self { match self {
EventCoherenceFailure::OtherError(event, _) => event, EventCoherenceFailure::OtherError(cmd, _) => cmd.event,
Self::TargetDoesNotExist(event) => event, Self::TargetDoesNotExist(cmd) => cmd.event,
} }
} }
} }
impl From<EventCoherenceFailure> for CommandEvent {
fn from(value: EventCoherenceFailure) -> Self {
value.as_event()
}
}

View File

@ -25,7 +25,7 @@ impl GameState {
match event { match event {
CommandEvent::ChangeScene { scene_key } => self.change_scene(&scene_key).await?, CommandEvent::ChangeScene { scene_key } => self.change_scene(&scene_key).await?,
CommandEvent::Narration(narration) => println!("\n\n{}\n\n", narration), CommandEvent::Narration(narration) => println!("\n\n{}\n\n", narration),
CommandEvent::LookAtEntity { ref entity_key, .. } => self.look_at(entity_key).await?, CommandEvent::LookAtEntity(ref entity_key) => self.look_at(entity_key).await?,
_ => (), _ => (),
} }
@ -66,7 +66,7 @@ impl GameState {
async fn look_at(&mut self, entity_key: &str) -> Result<()> { async fn look_at(&mut self, entity_key: &str) -> Result<()> {
let maybe_entity = self let maybe_entity = self
.db .db
.load_entity(&self.current_scene.key, entity_key) .load_entity_in_scene(&self.current_scene.key, entity_key)
.await?; .await?;
if let Some(entity) = maybe_entity { if let Some(entity) = maybe_entity {
@ -76,6 +76,8 @@ impl GameState {
} }
display!("\n"); display!("\n");
} else {
display!("You don't see that thing or person here.");
} }
Ok(()) Ok(())