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::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("")
}

View File

@ -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);
}
}
}

View File

@ -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))
}

View File

@ -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)
}
}
}

View File

@ -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,

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#"
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#"

View File

@ -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?;
}

View File

@ -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()
}
}

View File

@ -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(())