Move the AI logic to its own module file.
This commit is contained in:
parent
d2ed246b8b
commit
336a4231a0
|
@ -0,0 +1,265 @@
|
|||
use crate::db::Database;
|
||||
use crate::kobold_api::Client as KoboldClient;
|
||||
use crate::models::commands::{
|
||||
CommandExecution, Commands, ExecutionConversionResult, RawCommandExecution,
|
||||
};
|
||||
use crate::models::world::items::{Category, Item, Rarity};
|
||||
use crate::models::world::people::{Gender, Person, Sex};
|
||||
use crate::models::world::raw::{ItemSeed, PersonSeed, SceneSeed};
|
||||
use crate::models::world::scenes::{Exit, Scene, SceneStub, Stage};
|
||||
use crate::models::{new_uuid_string, Content, ContentContainer, ContentRelation};
|
||||
use anyhow::{bail, Result};
|
||||
use itertools::Itertools;
|
||||
use std::rc::Rc;
|
||||
|
||||
use super::generator::AiClient;
|
||||
|
||||
use super::coherence::AiCoherence;
|
||||
|
||||
/// Highest-level AI/LLM construct, which returns fully converted game
|
||||
/// objects to us. Basically, call the mid-level `client` to create
|
||||
/// seed objects, then call the mid level client again to detail the
|
||||
/// entities from their seeds. Then, stick a DB ID on them and put
|
||||
/// them in the database(?).
|
||||
pub struct AiLogic {
|
||||
generator: Rc<AiClient>,
|
||||
coherence: AiCoherence,
|
||||
db: Rc<Database>,
|
||||
}
|
||||
|
||||
impl AiLogic {
|
||||
pub fn new(api_client: Rc<KoboldClient>, db: &Rc<Database>) -> AiLogic {
|
||||
let generator = Rc::new(AiClient::new(api_client));
|
||||
let coherence = AiCoherence::new(generator.clone());
|
||||
|
||||
AiLogic {
|
||||
generator,
|
||||
coherence,
|
||||
db: db.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn execute(
|
||||
&mut self,
|
||||
stage: &Stage,
|
||||
cmd: &str,
|
||||
) -> Result<(Commands, CommandExecution)> {
|
||||
let parsed_cmd = self.generator.parse(cmd).await?;
|
||||
let execution = self.execute_parsed(stage, &parsed_cmd).await?;
|
||||
Ok((parsed_cmd, execution))
|
||||
}
|
||||
|
||||
pub async fn execute_parsed(
|
||||
&mut self,
|
||||
stage: &Stage,
|
||||
parsed_cmd: &Commands,
|
||||
) -> Result<CommandExecution> {
|
||||
//TODO handle multiple commands in list
|
||||
if parsed_cmd.commands.is_empty() {
|
||||
return Ok(CommandExecution::empty());
|
||||
}
|
||||
|
||||
let cmd = &parsed_cmd.commands[0];
|
||||
let raw_exec: RawCommandExecution = self.generator.execute_raw(stage, cmd).await?;
|
||||
|
||||
// Coherence check:
|
||||
// Set aside any events that are not in the enum
|
||||
// Set aside anything with correct event, but wrong parameters.
|
||||
// Ask LLM to fix them, if possible
|
||||
//TODO make a aiclient::fix_execution
|
||||
let converted = crate::commands::convert_raw_execution(raw_exec, &self.db).await;
|
||||
|
||||
self.generator.reset_commands();
|
||||
|
||||
//TODO handle the errored events aside from yeeting them out
|
||||
match converted {
|
||||
ExecutionConversionResult::Success(execution) => Ok(execution),
|
||||
ExecutionConversionResult::PartialSuccess(execution, _) => Ok(execution),
|
||||
ExecutionConversionResult::Failure(failures) => {
|
||||
bail!("unhandled command execution failure: {:?}", failures)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn create_person(&mut self, scene: &SceneSeed, seed: &PersonSeed) -> Result<Person> {
|
||||
self.generator.reset_person_creation();
|
||||
let details = self.generator.create_person_details(scene, seed).await?;
|
||||
|
||||
let gender = match details.gender.to_lowercase().as_ref() {
|
||||
"male" | "man" | "boy" | "transmasc" => Gender::Male,
|
||||
"female" | "woman" | "girl" | "transfem" => Gender::Female,
|
||||
"nonbinary" => Gender::NonBinary,
|
||||
// fall back to using sex
|
||||
_ => match details.sex.to_lowercase().as_ref() {
|
||||
"male" | "man" | "boy" | "transmasc" => Gender::Male,
|
||||
"female" | "woman" | "girl" | "transfem" => Gender::Female,
|
||||
_ => Gender::NonBinary, // TODO 1/3 chance!
|
||||
},
|
||||
};
|
||||
|
||||
let sex = match details.sex.to_lowercase().as_ref() {
|
||||
"male" | "man" | "boy" | "transfem" => Sex::Male,
|
||||
"female" | "woman" | "girl" | "transmasc" => Sex::Female,
|
||||
_ => match gender {
|
||||
Gender::Male => Sex::Male,
|
||||
Gender::Female => Sex::Male,
|
||||
_ => Sex::Male, // TODO 50/50 chance!
|
||||
},
|
||||
};
|
||||
|
||||
self.generator.reset_person_creation();
|
||||
|
||||
Ok(Person {
|
||||
_key: Some(new_uuid_string()),
|
||||
name: seed.name.to_string(),
|
||||
description: details.description,
|
||||
age: details.age,
|
||||
residence: details.residence,
|
||||
current_activity: details.current_activity,
|
||||
occupation: seed.occupation.to_string(),
|
||||
race: seed.race.clone(),
|
||||
sex,
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn create_item(&mut self, scene: &SceneSeed, seed: &ItemSeed) -> Result<Item> {
|
||||
let details = self.generator.create_item_details(scene, seed).await?;
|
||||
|
||||
// TODO these have to be sent to the AI
|
||||
let category = Category::Other;
|
||||
let rarity = Rarity::Common;
|
||||
|
||||
Ok(Item {
|
||||
_key: Some(new_uuid_string()),
|
||||
name: seed.name.to_string(),
|
||||
description: details.description,
|
||||
attributes: details.attributes,
|
||||
secret_attributes: details.secret_attributes,
|
||||
category,
|
||||
rarity,
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn create_scene_with_id(
|
||||
&mut self,
|
||||
scene_type: &str,
|
||||
fantasticalness: &str,
|
||||
scene_id: &str,
|
||||
) -> Result<ContentContainer> {
|
||||
let mut content = self.create_scene(scene_type, fantasticalness).await?;
|
||||
let scene = content.owner.as_scene_mut();
|
||||
scene._key = Some(scene_id.to_string());
|
||||
|
||||
Ok(content)
|
||||
}
|
||||
|
||||
pub async fn create_scene_from_stub(
|
||||
&mut self,
|
||||
stub: SceneStub,
|
||||
connected_scene: &Scene,
|
||||
) -> Result<ContentContainer> {
|
||||
self.generator.reset_world_creation();
|
||||
|
||||
let seed = self
|
||||
.generator
|
||||
.create_scene_seed_from_stub(&stub, connected_scene)
|
||||
.await?;
|
||||
|
||||
// There are two coherence steps: the first fixes up exit
|
||||
// directions and stuff, while the second is the normal scene
|
||||
// coherence (that can invoke the LLM).
|
||||
let mut content = self.fill_in_scene_from_stub(seed, stub).await?;
|
||||
self.coherence
|
||||
.make_scene_from_stub_coherent(&mut content, connected_scene);
|
||||
self.coherence.make_scene_coherent(&mut content).await?;
|
||||
|
||||
self.generator.reset_world_creation();
|
||||
|
||||
Ok(content)
|
||||
}
|
||||
|
||||
pub async fn create_scene(
|
||||
&mut self,
|
||||
scene_type: &str,
|
||||
fantasticalness: &str,
|
||||
) -> Result<ContentContainer> {
|
||||
self.generator.reset_world_creation();
|
||||
|
||||
let scene_seed = self
|
||||
.generator
|
||||
.create_scene_seed(scene_type, fantasticalness)
|
||||
.await?;
|
||||
|
||||
let mut content = self.fill_in_scene(scene_seed).await?;
|
||||
self.coherence.make_scene_coherent(&mut content).await?;
|
||||
|
||||
self.generator.reset_world_creation();
|
||||
Ok(content)
|
||||
}
|
||||
|
||||
async fn fill_in_scene_from_stub(
|
||||
&mut self,
|
||||
seed: SceneSeed,
|
||||
stub: SceneStub,
|
||||
) -> Result<ContentContainer> {
|
||||
let mut content = self.fill_in_scene(seed).await?;
|
||||
let new_scene = content.owner.as_scene_mut();
|
||||
new_scene._id = stub._id;
|
||||
new_scene._key = stub._key;
|
||||
|
||||
Ok(content)
|
||||
}
|
||||
|
||||
async fn fill_in_scene(&mut self, mut scene_seed: SceneSeed) -> Result<ContentContainer> {
|
||||
let mut content_in_scene = vec![];
|
||||
|
||||
// People in scene
|
||||
let mut people = vec![];
|
||||
for person_seed in scene_seed.people.as_slice() {
|
||||
let person = self.create_person(&scene_seed, person_seed).await?;
|
||||
people.push(ContentRelation::person(person));
|
||||
}
|
||||
|
||||
// Items in scene
|
||||
let mut items = vec![];
|
||||
for item_seed in scene_seed.items.as_slice() {
|
||||
let item = self.create_item(&scene_seed, item_seed).await?;
|
||||
items.push(ContentRelation::item(item));
|
||||
}
|
||||
|
||||
// TODO items on people, which will require 'recursive' ContentContainers.
|
||||
|
||||
let exits: Vec<_> = scene_seed
|
||||
.exits
|
||||
.drain(0..)
|
||||
.map(|seed| Exit::from(seed))
|
||||
.collect();
|
||||
|
||||
let mut stubs: Vec<_> = exits
|
||||
.iter()
|
||||
.map(|exit| ContentRelation::scene_stub(SceneStub::from(exit)))
|
||||
.collect();
|
||||
|
||||
let mut scene = Scene {
|
||||
_key: Some(new_uuid_string()),
|
||||
name: scene_seed.name,
|
||||
region: scene_seed.region,
|
||||
description: scene_seed.description,
|
||||
props: scene_seed.props.drain(0..).map_into().collect(),
|
||||
is_stub: false,
|
||||
exits,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
content_in_scene.append(&mut people);
|
||||
content_in_scene.append(&mut items);
|
||||
content_in_scene.append(&mut stubs);
|
||||
|
||||
Ok(ContentContainer {
|
||||
owner: Content::Scene(scene),
|
||||
contained: content_in_scene,
|
||||
})
|
||||
}
|
||||
}
|
270
src/ai/mod.rs
270
src/ai/mod.rs
|
@ -1,271 +1,5 @@
|
|||
use crate::db::Database;
|
||||
use crate::kobold_api::Client as KoboldClient;
|
||||
use crate::models::commands::{
|
||||
CommandExecution, Commands, ExecutionConversionResult, RawCommandExecution,
|
||||
};
|
||||
use crate::models::world::items::{Category, Item, Rarity};
|
||||
use crate::models::world::people::{Gender, Person, Sex};
|
||||
use crate::models::world::raw::{ItemSeed, PersonSeed, SceneSeed};
|
||||
use crate::models::world::scenes::{Exit, Scene, SceneStub, Stage};
|
||||
use crate::models::{new_uuid_string, Content, ContentContainer, ContentRelation};
|
||||
use anyhow::{bail, Result};
|
||||
use itertools::Itertools;
|
||||
use std::rc::Rc;
|
||||
|
||||
mod coherence;
|
||||
pub(self) mod coherence;
|
||||
pub mod convo;
|
||||
pub mod generator;
|
||||
pub mod prompts;
|
||||
|
||||
use convo::AiPrompt;
|
||||
use generator::AiClient;
|
||||
|
||||
use self::coherence::AiCoherence;
|
||||
|
||||
/// Highest-level AI/LLM construct, which returns fully converted game
|
||||
/// objects to us. Basically, call the mid-level `client` to create
|
||||
/// seed objects, then call the mid level client again to detail the
|
||||
/// entities from their seeds. Then, stick a DB ID on them and put
|
||||
/// them in the database(?).
|
||||
pub struct AiLogic {
|
||||
generator: Rc<AiClient>,
|
||||
coherence: AiCoherence,
|
||||
db: Rc<Database>,
|
||||
}
|
||||
|
||||
impl AiLogic {
|
||||
pub fn new(api_client: Rc<KoboldClient>, db: &Rc<Database>) -> AiLogic {
|
||||
let generator = Rc::new(AiClient::new(api_client));
|
||||
let coherence = AiCoherence::new(generator.clone());
|
||||
|
||||
AiLogic {
|
||||
generator,
|
||||
coherence,
|
||||
db: db.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn execute(
|
||||
&mut self,
|
||||
stage: &Stage,
|
||||
cmd: &str,
|
||||
) -> Result<(Commands, CommandExecution)> {
|
||||
let parsed_cmd = self.generator.parse(cmd).await?;
|
||||
let execution = self.execute_parsed(stage, &parsed_cmd).await?;
|
||||
Ok((parsed_cmd, execution))
|
||||
}
|
||||
|
||||
pub async fn execute_parsed(
|
||||
&mut self,
|
||||
stage: &Stage,
|
||||
parsed_cmd: &Commands,
|
||||
) -> Result<CommandExecution> {
|
||||
//TODO handle multiple commands in list
|
||||
if parsed_cmd.commands.is_empty() {
|
||||
return Ok(CommandExecution::empty());
|
||||
}
|
||||
|
||||
let cmd = &parsed_cmd.commands[0];
|
||||
let raw_exec: RawCommandExecution = self.generator.execute_raw(stage, cmd).await?;
|
||||
|
||||
// Coherence check:
|
||||
// Set aside any events that are not in the enum
|
||||
// Set aside anything with correct event, but wrong parameters.
|
||||
// Ask LLM to fix them, if possible
|
||||
//TODO make a aiclient::fix_execution
|
||||
let converted = crate::commands::convert_raw_execution(raw_exec, &self.db).await;
|
||||
|
||||
self.generator.reset_commands();
|
||||
|
||||
//TODO handle the errored events aside from yeeting them out
|
||||
match converted {
|
||||
ExecutionConversionResult::Success(execution) => Ok(execution),
|
||||
ExecutionConversionResult::PartialSuccess(execution, _) => Ok(execution),
|
||||
ExecutionConversionResult::Failure(failures) => {
|
||||
bail!("unhandled command execution failure: {:?}", failures)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn create_person(&mut self, scene: &SceneSeed, seed: &PersonSeed) -> Result<Person> {
|
||||
self.generator.reset_person_creation();
|
||||
let details = self.generator.create_person_details(scene, seed).await?;
|
||||
|
||||
let gender = match details.gender.to_lowercase().as_ref() {
|
||||
"male" | "man" | "boy" | "transmasc" => Gender::Male,
|
||||
"female" | "woman" | "girl" | "transfem" => Gender::Female,
|
||||
"nonbinary" => Gender::NonBinary,
|
||||
// fall back to using sex
|
||||
_ => match details.sex.to_lowercase().as_ref() {
|
||||
"male" | "man" | "boy" | "transmasc" => Gender::Male,
|
||||
"female" | "woman" | "girl" | "transfem" => Gender::Female,
|
||||
_ => Gender::NonBinary, // TODO 1/3 chance!
|
||||
},
|
||||
};
|
||||
|
||||
let sex = match details.sex.to_lowercase().as_ref() {
|
||||
"male" | "man" | "boy" | "transfem" => Sex::Male,
|
||||
"female" | "woman" | "girl" | "transmasc" => Sex::Female,
|
||||
_ => match gender {
|
||||
Gender::Male => Sex::Male,
|
||||
Gender::Female => Sex::Male,
|
||||
_ => Sex::Male, // TODO 50/50 chance!
|
||||
},
|
||||
};
|
||||
|
||||
self.generator.reset_person_creation();
|
||||
|
||||
Ok(Person {
|
||||
_key: Some(new_uuid_string()),
|
||||
name: seed.name.to_string(),
|
||||
description: details.description,
|
||||
age: details.age,
|
||||
residence: details.residence,
|
||||
current_activity: details.current_activity,
|
||||
occupation: seed.occupation.to_string(),
|
||||
race: seed.race.clone(),
|
||||
sex,
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn create_item(&mut self, scene: &SceneSeed, seed: &ItemSeed) -> Result<Item> {
|
||||
let details = self.generator.create_item_details(scene, seed).await?;
|
||||
|
||||
// TODO these have to be sent to the AI
|
||||
let category = Category::Other;
|
||||
let rarity = Rarity::Common;
|
||||
|
||||
Ok(Item {
|
||||
_key: Some(new_uuid_string()),
|
||||
name: seed.name.to_string(),
|
||||
description: details.description,
|
||||
attributes: details.attributes,
|
||||
secret_attributes: details.secret_attributes,
|
||||
category,
|
||||
rarity,
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn create_scene_with_id(
|
||||
&mut self,
|
||||
scene_type: &str,
|
||||
fantasticalness: &str,
|
||||
scene_id: &str,
|
||||
) -> Result<ContentContainer> {
|
||||
let mut content = self.create_scene(scene_type, fantasticalness).await?;
|
||||
let scene = content.owner.as_scene_mut();
|
||||
scene._key = Some(scene_id.to_string());
|
||||
|
||||
Ok(content)
|
||||
}
|
||||
|
||||
pub async fn create_scene_from_stub(
|
||||
&mut self,
|
||||
stub: SceneStub,
|
||||
connected_scene: &Scene,
|
||||
) -> Result<ContentContainer> {
|
||||
self.generator.reset_world_creation();
|
||||
|
||||
let seed = self
|
||||
.generator
|
||||
.create_scene_seed_from_stub(&stub, connected_scene)
|
||||
.await?;
|
||||
|
||||
// There are two coherence steps: the first fixes up exit
|
||||
// directions and stuff, while the second is the normal scene
|
||||
// coherence (that can invoke the LLM).
|
||||
let mut content = self.fill_in_scene_from_stub(seed, stub).await?;
|
||||
self.coherence
|
||||
.make_scene_from_stub_coherent(&mut content, connected_scene);
|
||||
self.coherence.make_scene_coherent(&mut content).await?;
|
||||
|
||||
self.generator.reset_world_creation();
|
||||
|
||||
Ok(content)
|
||||
}
|
||||
|
||||
pub async fn create_scene(
|
||||
&mut self,
|
||||
scene_type: &str,
|
||||
fantasticalness: &str,
|
||||
) -> Result<ContentContainer> {
|
||||
self.generator.reset_world_creation();
|
||||
|
||||
let scene_seed = self
|
||||
.generator
|
||||
.create_scene_seed(scene_type, fantasticalness)
|
||||
.await?;
|
||||
|
||||
let mut content = self.fill_in_scene(scene_seed).await?;
|
||||
self.coherence.make_scene_coherent(&mut content).await?;
|
||||
|
||||
self.generator.reset_world_creation();
|
||||
Ok(content)
|
||||
}
|
||||
|
||||
async fn fill_in_scene_from_stub(
|
||||
&mut self,
|
||||
seed: SceneSeed,
|
||||
stub: SceneStub,
|
||||
) -> Result<ContentContainer> {
|
||||
let mut content = self.fill_in_scene(seed).await?;
|
||||
let new_scene = content.owner.as_scene_mut();
|
||||
new_scene._id = stub._id;
|
||||
new_scene._key = stub._key;
|
||||
|
||||
Ok(content)
|
||||
}
|
||||
|
||||
async fn fill_in_scene(&mut self, mut scene_seed: SceneSeed) -> Result<ContentContainer> {
|
||||
let mut content_in_scene = vec![];
|
||||
|
||||
// People in scene
|
||||
let mut people = vec![];
|
||||
for person_seed in scene_seed.people.as_slice() {
|
||||
let person = self.create_person(&scene_seed, person_seed).await?;
|
||||
people.push(ContentRelation::person(person));
|
||||
}
|
||||
|
||||
// Items in scene
|
||||
let mut items = vec![];
|
||||
for item_seed in scene_seed.items.as_slice() {
|
||||
let item = self.create_item(&scene_seed, item_seed).await?;
|
||||
items.push(ContentRelation::item(item));
|
||||
}
|
||||
|
||||
// TODO items on people, which will require 'recursive' ContentContainers.
|
||||
|
||||
let exits: Vec<_> = scene_seed
|
||||
.exits
|
||||
.drain(0..)
|
||||
.map(|seed| Exit::from(seed))
|
||||
.collect();
|
||||
|
||||
let mut stubs: Vec<_> = exits
|
||||
.iter()
|
||||
.map(|exit| ContentRelation::scene_stub(SceneStub::from(exit)))
|
||||
.collect();
|
||||
|
||||
let mut scene = Scene {
|
||||
_key: Some(new_uuid_string()),
|
||||
name: scene_seed.name,
|
||||
region: scene_seed.region,
|
||||
description: scene_seed.description,
|
||||
props: scene_seed.props.drain(0..).map_into().collect(),
|
||||
is_stub: false,
|
||||
exits,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
content_in_scene.append(&mut people);
|
||||
content_in_scene.append(&mut items);
|
||||
content_in_scene.append(&mut stubs);
|
||||
|
||||
Ok(ContentContainer {
|
||||
owner: Content::Scene(scene),
|
||||
contained: content_in_scene,
|
||||
})
|
||||
}
|
||||
}
|
||||
pub mod logic;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use crate::ai::AiPrompt;
|
||||
use crate::ai::convo::AiPrompt;
|
||||
use crate::models::commands::{Command, CommandEvent, EventConversionFailures};
|
||||
use crate::models::world::scenes::{Scene, Stage};
|
||||
use strum::VariantNames;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use crate::ai::AiPrompt;
|
||||
use crate::ai::convo::AiPrompt;
|
||||
|
||||
pub const COMMAND_BNF: &str = r#"
|
||||
root ::= Commands
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
use crate::{
|
||||
ai::AiPrompt,
|
||||
ai::convo::AiPrompt,
|
||||
models::world::{
|
||||
raw::{PersonSeed, SceneSeed},
|
||||
scenes::{Exit, Scene, SceneStub},
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
use anyhow::Result;
|
||||
use config::Config;
|
||||
use game_loop::GameLoop;
|
||||
use ai::logic::AiLogic;
|
||||
use models::world::scenes::{root_scene_id, Stage};
|
||||
use state::GameState;
|
||||
use std::{io::stdout, rc::Rc, time::Duration};
|
||||
|
@ -101,7 +102,7 @@ async fn main() -> Result<()> {
|
|||
base_client,
|
||||
));
|
||||
let db = Rc::new(Database::new(conn, "test_world").await?);
|
||||
let logic = ai::AiLogic::new(client, &db);
|
||||
let logic = AiLogic::new(client, &db);
|
||||
|
||||
let mut state = GameState {
|
||||
logic,
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use crate::models::Insertable;
|
||||
use crate::{
|
||||
ai::AiLogic,
|
||||
ai::logic::AiLogic,
|
||||
db::Database,
|
||||
models::{
|
||||
commands::CommandEvent,
|
||||
|
|
Loading…
Reference in New Issue