Switch to one event output per command execution. Simplify coherence.
This commit is contained in:
parent
d23f09295e
commit
c6f10f7a61
|
@ -1,5 +1,5 @@
|
|||
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::people::Person;
|
||||
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#"
|
||||
root ::= CommandExecution
|
||||
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 "\"events\":" ws CommandEventlist "}"
|
||||
CommandExecution ::= "{" ws "\"valid\":" ws boolean "," ws "\"reason\":" ws string "," ws "\"narration\":" ws string "," ws "\"event\":" ws CommandEvent "}"
|
||||
CommandExecutionlist ::= "[]" | "[" ws CommandExecution ("," ws CommandExecution)* "]"
|
||||
string ::= "\"" ([^"]*) "\""
|
||||
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.
|
||||
- `appliesTo`: The player, item, NPC, or other entity in the scene.
|
||||
- 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.
|
||||
|
||||
The following events can be generated:
|
||||
- `change_scene`: The player's current scene is changed.
|
||||
- `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.
|
||||
- `appliesTo` is the Scene Key of the current scene.
|
||||
- `parameter` is the Entity Key of the entity being looked at.
|
||||
- `appliesTo` is the key of the person, prop, or item 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.
|
||||
- `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.
|
||||
|
@ -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.
|
||||
- `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.
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
pub fn fix_prompt(scene: &Scene, failures: &EventConversionFailures) -> AiPrompt {
|
||||
pub fn fix_prompt(scene: &Scene, failures: &EventConversionFailure) -> AiPrompt {
|
||||
AiPrompt::new("")
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@ use futures::{future, TryFutureExt};
|
|||
use std::rc::Rc;
|
||||
use uuid::Uuid;
|
||||
|
||||
type CoherenceResult = Result<CommandEvent, EventCoherenceFailure>;
|
||||
type CoherenceResult = Result<AiCommand, EventCoherenceFailure>;
|
||||
|
||||
pub struct CommandCoherence<'a> {
|
||||
logic: Rc<AiLogic>,
|
||||
|
@ -35,33 +35,20 @@ impl CommandCoherence<'_> {
|
|||
}
|
||||
}
|
||||
|
||||
pub async fn fix_incoherent_events(
|
||||
pub async fn fix_incoherent_event(
|
||||
&self,
|
||||
failures: Vec<EventCoherenceFailure>,
|
||||
failure: EventCoherenceFailure,
|
||||
) -> 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.
|
||||
|
||||
if successes.len() > 0 && failures.len() == 0 {
|
||||
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())
|
||||
}
|
||||
let coherent_event = self.cohere_event(failure).await?;
|
||||
Ok(coherent_event)
|
||||
}
|
||||
|
||||
async fn cohere_event(&self, failure: EventCoherenceFailure) -> CoherenceResult {
|
||||
let event_fix = async {
|
||||
match failure {
|
||||
EventCoherenceFailure::TargetDoesNotExist(event) => {
|
||||
self.fix_target_does_not_exist(event).await
|
||||
EventCoherenceFailure::TargetDoesNotExist(cmd) => {
|
||||
self.fix_target_does_not_exist(cmd).await
|
||||
}
|
||||
EventCoherenceFailure::OtherError(event, _) => future::ok(event).await,
|
||||
}
|
||||
|
@ -72,20 +59,22 @@ impl CommandCoherence<'_> {
|
|||
.await
|
||||
}
|
||||
|
||||
async fn fix_target_does_not_exist(&self, mut event: CommandEvent) -> CoherenceResult {
|
||||
if let CommandEvent::LookAtEntity {
|
||||
ref mut entity_key,
|
||||
ref mut scene_key,
|
||||
} = event
|
||||
{
|
||||
let res = cohere_scene_and_entity(&self.db, &self.stage, entity_key, scene_key).await;
|
||||
async fn fix_target_does_not_exist(&self, mut cmd: AiCommand) -> CoherenceResult {
|
||||
if cmd.event.is_none() {
|
||||
return Ok(cmd);
|
||||
}
|
||||
|
||||
let event: &mut CommandEvent = cmd.event.as_mut().unwrap();
|
||||
|
||||
if let CommandEvent::LookAtEntity(ref mut entity_key) = event {
|
||||
let res = cohere_scene_and_entity(&self.db, &self.stage, entity_key).await;
|
||||
|
||||
match res {
|
||||
Ok(_) => Ok(event),
|
||||
Err(err) => Err(EventCoherenceFailure::OtherError(event, err.to_string())),
|
||||
Ok(_) => Ok(cmd),
|
||||
Err(err) => Err(EventCoherenceFailure::OtherError(cmd, err.to_string())),
|
||||
}
|
||||
} else {
|
||||
Ok(event)
|
||||
Ok(cmd)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -96,29 +85,11 @@ async fn cohere_scene_and_entity(
|
|||
db: &Database,
|
||||
stage: &Stage,
|
||||
entity_key: &mut String,
|
||||
scene_key: &mut String,
|
||||
) -> AnyhowResult<()> {
|
||||
// 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
|
||||
// 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);
|
||||
}
|
||||
let scene_key = &stage.key;
|
||||
|
||||
// If entity key is not a valid UUID at this point, then we have
|
||||
// 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"));
|
||||
}
|
||||
|
||||
// It is often likely that the scene key and entity key are reversed.
|
||||
if db.entity_exists(&entity_key, &scene_key).await? {
|
||||
std::mem::swap(entity_key, scene_key);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
// Final result is if the entity actually exists or not now.
|
||||
db.entity_exists(entity_key).await.map(|_| ())
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn is_valid_scene_key(scene_key: &str) -> bool {
|
||||
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.
|
||||
fn normalize_keys(scene_key: &mut String, entity_key: &mut String) {
|
||||
if let Some(normalized) = normalize_uuid(&scene_key) {
|
||||
scene_key.clear();
|
||||
scene_key.push_str(&normalized);
|
||||
}
|
||||
|
||||
if let Some(normalized) = normalize_uuid(&entity_key) {
|
||||
entity_key.clear();
|
||||
entity_key.push_str(&normalized);
|
||||
pub(super) fn normalize_keys(keys: &mut [&mut String]) {
|
||||
for key in keys {
|
||||
if let Some(normalized) = normalize_uuid(&key) {
|
||||
key.clear();
|
||||
key.push_str(&normalized);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,125 +1,77 @@
|
|||
use super::coherence::strip_prefixes;
|
||||
use super::partition;
|
||||
use crate::{
|
||||
db::Database,
|
||||
models::commands::{
|
||||
AiCommand, CommandEvent, EventCoherenceFailure, EventConversionError,
|
||||
EventConversionFailures, ExecutionConversionResult, RawCommandEvent, RawCommandExecution,
|
||||
AiCommand, CommandEvent, EventCoherenceFailure, EventParsingFailure,
|
||||
ExecutionConversionResult, Narrative, 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<CommandEvent, EventConversionError>;
|
||||
type EventParsingResult = std::result::Result<CommandEvent, EventParsingFailure>;
|
||||
|
||||
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();
|
||||
|
||||
if Self::VARIANTS.contains(&event_name.as_str()) {
|
||||
deserialize_recognized_event(raw_event)
|
||||
} else {
|
||||
Err(EventConversionError::UnrecognizedEvent(raw_event))
|
||||
Err(EventParsingFailure::UnrecognizedEvent(raw_event))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<RawCommandEvent> for CommandEvent {
|
||||
type Error = EventConversionError;
|
||||
type Error = EventParsingFailure;
|
||||
|
||||
fn try_from(raw_event: RawCommandEvent) -> Result<Self, Self::Error> {
|
||||
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(
|
||||
mut raw_exec: RawCommandExecution,
|
||||
db: &Database,
|
||||
) -> ExecutionConversionResult {
|
||||
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 {
|
||||
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) = 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,
|
||||
))
|
||||
}
|
||||
let converted_event = CommandEvent::new(raw_event)?;
|
||||
let cmd = AiCommand::from_raw_success(narrative, converted_event);
|
||||
validate_event_coherence(db, cmd)
|
||||
.await
|
||||
.map_err(|e| e.into())
|
||||
}
|
||||
|
||||
fn deserialize_recognized_event(
|
||||
raw_event: RawCommandEvent,
|
||||
) -> Result<CommandEvent, EventConversionError> {
|
||||
) -> Result<CommandEvent, EventParsingFailure> {
|
||||
let event_name = raw_event.event_name.as_str().to_lowercase();
|
||||
let event_name = event_name.as_str();
|
||||
|
||||
match event_name {
|
||||
// informational-related
|
||||
"narration" => Ok(CommandEvent::Narration(raw_event.parameter)),
|
||||
"look_at_entity" => Ok(CommandEvent::LookAtEntity {
|
||||
entity_key: strip_prefixes(raw_event.parameter),
|
||||
scene_key: strip_prefixes(raw_event.applies_to),
|
||||
}),
|
||||
"look_at_entity" => Ok(CommandEvent::LookAtEntity(
|
||||
deserialize_and_normalize(raw_event),
|
||||
)),
|
||||
|
||||
// scene-related
|
||||
"change_scene" => Ok(CommandEvent::ChangeScene {
|
||||
|
@ -144,54 +96,77 @@ fn deserialize_recognized_event(
|
|||
"take_damage" => deserialize_take_damage(raw_event),
|
||||
|
||||
// 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(
|
||||
raw_event: RawCommandEvent,
|
||||
) -> Result<CommandEvent, EventConversionError> {
|
||||
) -> Result<CommandEvent, EventParsingFailure> {
|
||||
match raw_event.parameter.parse::<u32>() {
|
||||
Ok(dmg) => Ok(CommandEvent::TakeDamage {
|
||||
target: strip_prefixes(raw_event.applies_to),
|
||||
amount: dmg,
|
||||
}),
|
||||
Err(_) => Err(EventConversionError::InvalidParameter(raw_event)),
|
||||
Err(_) => Err(EventParsingFailure::InvalidParameter(raw_event)),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) async fn validate_event_coherence<'a>(
|
||||
db: &Database,
|
||||
event: CommandEvent,
|
||||
) -> std::result::Result<CommandEvent, EventCoherenceFailure> {
|
||||
match event {
|
||||
CommandEvent::LookAtEntity {
|
||||
ref entity_key,
|
||||
ref scene_key,
|
||||
} => match db.entity_exists(&scene_key, &entity_key).await {
|
||||
cmd: AiCommand,
|
||||
) -> std::result::Result<AiCommand, EventCoherenceFailure> {
|
||||
if cmd.event.is_none() {
|
||||
return Ok(cmd);
|
||||
}
|
||||
|
||||
match cmd.event.as_ref().unwrap() {
|
||||
CommandEvent::LookAtEntity(ref entity_key) => match db.entity_exists(&entity_key).await {
|
||||
Ok(exists) => match exists {
|
||||
true => Ok(event),
|
||||
false => Err(invalid_converted_event(event).unwrap()),
|
||||
true => Ok(cmd),
|
||||
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 {
|
||||
Ok(exists) => match exists {
|
||||
true => Ok(event),
|
||||
false => Err(invalid_converted_event(event).unwrap()),
|
||||
true => Ok(cmd),
|
||||
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
|
||||
/// information contained in the response is not valid.
|
||||
fn invalid_converted_event(event: CommandEvent) -> Option<EventCoherenceFailure> {
|
||||
match event {
|
||||
CommandEvent::LookAtEntity { .. } => Some(EventCoherenceFailure::TargetDoesNotExist(event)),
|
||||
CommandEvent::ChangeScene { .. } => Some(EventCoherenceFailure::TargetDoesNotExist(event)),
|
||||
fn invalid_converted_event(mut cmd: AiCommand) -> Option<EventCoherenceFailure> {
|
||||
match cmd.event.as_mut().unwrap() {
|
||||
CommandEvent::LookAtEntity { .. } => Some(EventCoherenceFailure::TargetDoesNotExist(cmd)),
|
||||
CommandEvent::ChangeScene { .. } => Some(EventCoherenceFailure::TargetDoesNotExist(cmd)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
@ -200,8 +175,8 @@ fn invalid_converted_event(event: CommandEvent) -> Option<EventCoherenceFailure>
|
|||
/// something went wrong with attempting to check the coherence of the
|
||||
/// converted event.
|
||||
fn invalid_converted_event_because_err(
|
||||
event: CommandEvent,
|
||||
cmd: AiCommand,
|
||||
err: anyhow::Error,
|
||||
) -> EventCoherenceFailure {
|
||||
EventCoherenceFailure::OtherError(event, format!("{}", err))
|
||||
EventCoherenceFailure::OtherError(cmd, format!("{}", err))
|
||||
}
|
||||
|
|
|
@ -4,7 +4,8 @@ use crate::{
|
|||
models::{
|
||||
commands::{
|
||||
AiCommand, CommandEvent, CommandExecution, EventCoherenceFailure,
|
||||
ExecutionConversionResult, ParsedCommand, ParsedCommands, RawCommandExecution,
|
||||
EventConversionFailure, ExecutionConversionResult, ParsedCommand, ParsedCommands,
|
||||
RawCommandExecution,
|
||||
},
|
||||
world::scenes::Stage,
|
||||
},
|
||||
|
@ -119,33 +120,15 @@ impl CommandExecutor {
|
|||
|
||||
let converted = converter::convert_raw_execution(raw_exec, &self.db).await;
|
||||
|
||||
let execution: Result<AiCommand> = match converted {
|
||||
ExecutionConversionResult::Success(execution) => Ok(execution),
|
||||
ExecutionConversionResult::PartialSuccess(mut execution, failures) => {
|
||||
let execution: AiCommand = match converted {
|
||||
Ok(ai_command) => Ok(ai_command),
|
||||
Err(failure) => {
|
||||
// TODO also deal with conversion failures
|
||||
// TODO deal with failures to fix incoherent events.
|
||||
// right now we just drop them.
|
||||
let mut fixed_events = self
|
||||
.fix_incoherence(stage, failures.coherence_failures)
|
||||
.await
|
||||
.ok()
|
||||
.unwrap_or(vec![]);
|
||||
|
||||
execution.events.append(&mut fixed_events);
|
||||
Ok(execution)
|
||||
self.fix_incoherence(stage, failure).await
|
||||
}
|
||||
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))
|
||||
}
|
||||
|
@ -153,18 +136,15 @@ impl CommandExecutor {
|
|||
async fn fix_incoherence(
|
||||
&self,
|
||||
stage: &Stage,
|
||||
failures: Vec<EventCoherenceFailure>,
|
||||
) -> Result<Vec<CommandEvent>> {
|
||||
println!("Attempting to fix {} incoherent events", failures.len());
|
||||
let fixer = coherence::CommandCoherence::new(&self.logic, &self.db, stage);
|
||||
failure: EventConversionFailure,
|
||||
) -> std::result::Result<AiCommand, EventConversionFailure> {
|
||||
if let EventConversionFailure::CoherenceFailure(coherence_failure) = failure {
|
||||
let fixer = coherence::CommandCoherence::new(&self.logic, &self.db, stage);
|
||||
|
||||
// TODO should do something w/ partial failures.
|
||||
let events = match fixer.fix_incoherent_events(failures).await {
|
||||
ExecutionConversionResult::Success(AiCommand { events, .. }) => Ok(events),
|
||||
ExecutionConversionResult::PartialSuccess(AiCommand { events, .. }, _) => Ok(events),
|
||||
ExecutionConversionResult::Failure(errs) => Err(errs),
|
||||
}?;
|
||||
|
||||
Ok(events)
|
||||
// TODO should do something w/ partial failures.
|
||||
fixer.fix_incoherent_event(coherence_failure).await
|
||||
} else {
|
||||
Err(failure)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use crate::models::commands::{CachedParsedCommand, ParsedCommand, ParsedCommands};
|
||||
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 arangors::document::options::InsertOptions;
|
||||
use arangors::graph::{EdgeDefinition, Graph};
|
||||
|
@ -384,11 +384,9 @@ impl Database {
|
|||
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();
|
||||
|
||||
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());
|
||||
|
||||
let db = self.db().await?;
|
||||
|
@ -400,9 +398,13 @@ impl Database {
|
|||
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()
|
||||
.query(queries::LOAD_ENTITY)
|
||||
.query(queries::LOAD_ENTITY_IN_SCENE)
|
||||
.bind_var("@scene_collection", SCENE_COLLECTION)
|
||||
.bind_var("scene_key", to_json_value(scene_key)?)
|
||||
.bind_var("entity_key", to_json_value(entity_key)?)
|
||||
|
@ -412,6 +414,16 @@ impl Database {
|
|||
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(
|
||||
&self,
|
||||
raw_cmd: &str,
|
||||
|
|
|
@ -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#"
|
||||
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 entities = (
|
||||
LET people = (FOR person in people
|
||||
FILTER person._key == @entity_key
|
||||
RETURN MERGE({ "type": "Person"}, person))
|
||||
|
||||
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 ))
|
||||
LET items = (FOR item in items
|
||||
FILTER item._key == @entity_key
|
||||
RETURN MERGE({ "type": "Item" }, item))
|
||||
|
||||
RETURN FIRST(APPEND(occupants, items)))
|
||||
RETURN FIRST(APPEND(people, items)))
|
||||
|
||||
FOR ent in entities
|
||||
FILTER ent != null
|
||||
RETURN ent
|
||||
FOR ent in entities
|
||||
FILTER ent != null
|
||||
RETURN ent
|
||||
"#;
|
||||
|
||||
pub const UPSERT_SCENE: &'static str = r#"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use crate::io::display;
|
||||
use crate::models::commands::{
|
||||
AiCommand, BuiltinCommand, CommandExecution, ExecutionConversionResult, EventConversionFailures,
|
||||
AiCommand, BuiltinCommand, CommandExecution, ExecutionConversionResult, EventConversionFailure,
|
||||
};
|
||||
use crate::state::GameState;
|
||||
use crate::{commands::CommandExecutor, db::Database};
|
||||
|
@ -42,8 +42,7 @@ impl GameLoop {
|
|||
}
|
||||
|
||||
display!("\n\n{}\n\n", execution.narration);
|
||||
|
||||
for event in execution.events {
|
||||
if let Some(event) = execution.event {
|
||||
self.state.update(event).await?;
|
||||
}
|
||||
|
||||
|
|
|
@ -60,7 +60,8 @@ pub struct RawCommandExecution {
|
|||
pub valid: bool,
|
||||
pub reason: Option<String>,
|
||||
pub narration: String,
|
||||
pub events: Vec<RawCommandEvent>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub event: Option<RawCommandEvent>,
|
||||
}
|
||||
|
||||
impl RawCommandExecution {
|
||||
|
@ -69,7 +70,7 @@ impl RawCommandExecution {
|
|||
valid: true,
|
||||
reason: None,
|
||||
narration: "".to_string(),
|
||||
events: vec![],
|
||||
event: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -88,10 +89,7 @@ pub struct RawCommandEvent {
|
|||
pub enum CommandEvent {
|
||||
// Informational
|
||||
Narration(String),
|
||||
LookAtEntity {
|
||||
entity_key: String,
|
||||
scene_key: String,
|
||||
},
|
||||
LookAtEntity(String),
|
||||
|
||||
// Movement-related
|
||||
ChangeScene {
|
||||
|
@ -137,6 +135,14 @@ pub enum CommandExecution {
|
|||
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
|
||||
/// run through coherence validation/fixing, and (assuming it is
|
||||
/// valid) contains a series of events to apply to the game state.
|
||||
|
@ -145,16 +151,31 @@ pub struct AiCommand {
|
|||
pub valid: bool,
|
||||
pub reason: Option<String>,
|
||||
pub narration: String,
|
||||
pub events: Vec<CommandEvent>,
|
||||
pub event: Option<CommandEvent>,
|
||||
}
|
||||
|
||||
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 {
|
||||
AiCommand {
|
||||
valid: true,
|
||||
reason: None,
|
||||
narration: "".to_string(),
|
||||
events: vec![],
|
||||
event: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -163,68 +184,48 @@ impl AiCommand {
|
|||
valid: raw.valid,
|
||||
reason: raw.reason,
|
||||
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 {
|
||||
valid: true,
|
||||
reason: None,
|
||||
narration: "".to_string(),
|
||||
events,
|
||||
event: Some(event),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum ExecutionConversionResult {
|
||||
Success(AiCommand),
|
||||
PartialSuccess(AiCommand, EventConversionFailures),
|
||||
Failure(EventConversionFailures),
|
||||
pub type ExecutionConversionResult = std::result::Result<AiCommand, EventConversionFailure>;
|
||||
|
||||
#[derive(Error, Clone, Debug)]
|
||||
pub enum EventConversionFailure {
|
||||
#[error("parsing failure: {0:?}")]
|
||||
ParsingFailure(EventParsingFailure),
|
||||
#[error("coherence failure: {0:?}")]
|
||||
CoherenceFailure(EventCoherenceFailure),
|
||||
}
|
||||
|
||||
impl Display for ExecutionConversionResult {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{:?}", self)
|
||||
impl From<EventCoherenceFailure> for EventConversionFailure {
|
||||
fn from(value: EventCoherenceFailure) -> Self {
|
||||
EventConversionFailure::CoherenceFailure(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<EventParsingFailure> for EventConversionFailure {
|
||||
fn from(value: EventParsingFailure) -> Self {
|
||||
EventConversionFailure::ParsingFailure(value)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Error, Clone, Debug)]
|
||||
pub struct EventConversionFailures {
|
||||
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 {
|
||||
pub enum EventParsingFailure {
|
||||
#[error("invalid parameter for {0:?}")]
|
||||
InvalidParameter(RawCommandEvent),
|
||||
|
||||
|
@ -235,24 +236,18 @@ pub enum EventConversionError {
|
|||
#[derive(Error, Clone, Debug)]
|
||||
pub enum EventCoherenceFailure {
|
||||
#[error("target of command does not exist")]
|
||||
TargetDoesNotExist(CommandEvent),
|
||||
TargetDoesNotExist(AiCommand),
|
||||
|
||||
#[error("uncategorized coherence failure: {1}")]
|
||||
OtherError(CommandEvent, String),
|
||||
OtherError(AiCommand, String),
|
||||
}
|
||||
|
||||
impl EventCoherenceFailure {
|
||||
/// 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 {
|
||||
EventCoherenceFailure::OtherError(event, _) => event,
|
||||
Self::TargetDoesNotExist(event) => event,
|
||||
EventCoherenceFailure::OtherError(cmd, _) => cmd.event,
|
||||
Self::TargetDoesNotExist(cmd) => cmd.event,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<EventCoherenceFailure> for CommandEvent {
|
||||
fn from(value: EventCoherenceFailure) -> Self {
|
||||
value.as_event()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,7 +25,7 @@ impl GameState {
|
|||
match event {
|
||||
CommandEvent::ChangeScene { scene_key } => self.change_scene(&scene_key).await?,
|
||||
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<()> {
|
||||
let maybe_entity = self
|
||||
.db
|
||||
.load_entity(&self.current_scene.key, entity_key)
|
||||
.load_entity_in_scene(&self.current_scene.key, entity_key)
|
||||
.await?;
|
||||
|
||||
if let Some(entity) = maybe_entity {
|
||||
|
@ -76,6 +76,8 @@ impl GameState {
|
|||
}
|
||||
|
||||
display!("\n");
|
||||
} else {
|
||||
display!("You don't see that thing or person here.");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
|
Loading…
Reference in New Issue