split out scene management to its own crate
This commit is contained in:
Generated
+1
@@ -345,6 +345,7 @@ dependencies = [
|
|||||||
"iana-time-zone",
|
"iana-time-zone",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"num-traits",
|
"num-traits",
|
||||||
|
"serde",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
"windows-link",
|
"windows-link",
|
||||||
]
|
]
|
||||||
|
|||||||
+1
-1
@@ -5,7 +5,7 @@ edition = "2024"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
async-openai = { version = "0.40.2", features = ["completions", "full"] }
|
async-openai = { version = "0.40.2", features = ["completions", "full"] }
|
||||||
chrono = "0.4.44"
|
chrono = { version = "0.4.44", features = ["serde"] }
|
||||||
color-eyre = "0.6.5"
|
color-eyre = "0.6.5"
|
||||||
crossterm = { version = "0.29.0", features = ["event-stream"] }
|
crossterm = { version = "0.29.0", features = ["event-stream"] }
|
||||||
futures = "0.3.32"
|
futures = "0.3.32"
|
||||||
|
|||||||
+62
-113
@@ -1,20 +1,17 @@
|
|||||||
use std::time::Duration;
|
use async_openai::{Client, config::OpenAIConfig, types::chat::{ChatCompletionMessageToolCalls, ChatCompletionTool, ChatCompletionTools, CreateChatCompletionRequest, CreateChatCompletionResponse, FunctionObject}};
|
||||||
|
use chrono::{DateTime, Duration, Utc};
|
||||||
use async_openai::{Client, config::OpenAIConfig, types::chat::{ChatCompletionMessageToolCalls, ChatCompletionRequestAssistantMessage, ChatCompletionRequestMessage, ChatCompletionRequestSystemMessage, ChatCompletionRequestUserMessage, ChatCompletionTool, ChatCompletionTools, CreateChatCompletionRequest, CreateChatCompletionResponse, FunctionObject, ResponseFormat, ResponseFormatJsonSchema}};
|
|
||||||
use futures_timer::Delay;
|
use futures_timer::Delay;
|
||||||
use schemars::{JsonSchema, schema_for};
|
use schemars::{JsonSchema, schema_for};
|
||||||
use scraper::{Html, Selector};
|
use scraper::{Html, Selector};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::Value;
|
|
||||||
|
|
||||||
use ratatui::{Frame, layout::{Constraint, Direction, Layout}, style::Modifier, widgets::{Block, BorderType, Clear, List, ListDirection, ListItem, ListState, Paragraph, Wrap}};
|
use ratatui::{Frame, layout::{Constraint, Direction, Layout}, widgets::{Block, BorderType, Clear, List, ListDirection, ListItem, ListState, Paragraph, Wrap}};
|
||||||
use throbber_widgets_tui::{Throbber, ThrobberState};
|
use throbber_widgets_tui::{Throbber, ThrobberState};
|
||||||
use crossterm::{event::{self, EventStream, KeyCode, KeyModifiers}};
|
use crossterm::{event::{self, EventStream, KeyCode, KeyModifiers}};
|
||||||
use tokio::sync::watch;
|
use tokio::sync::watch;
|
||||||
use tui_input::{Input, backend::crossterm::EventHandler};
|
use tui_input::{Input, backend::crossterm::EventHandler};
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
use futures::{StreamExt, future::FutureExt};
|
use futures::{StreamExt, future::FutureExt};
|
||||||
use futures::select;
|
|
||||||
|
|
||||||
/* Usage loop:
|
/* Usage loop:
|
||||||
- Prompt user to select one of:
|
- Prompt user to select one of:
|
||||||
@@ -34,10 +31,12 @@ use futures::select;
|
|||||||
- Right panel: Shortcuts for triggering scenarios, soundboard events, etc
|
- Right panel: Shortcuts for triggering scenarios, soundboard events, etc
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const SYSTEM_PROMPT: &str = include_str!("system-prompt.txt");
|
|
||||||
|
|
||||||
use ratatui::prelude::*;
|
use ratatui::prelude::*;
|
||||||
|
|
||||||
|
use crate::scene::{ConversationEntry, Scene};
|
||||||
|
|
||||||
|
mod scene;
|
||||||
|
|
||||||
#[derive(JsonSchema, Deserialize, Serialize, Debug, Clone)]
|
#[derive(JsonSchema, Deserialize, Serialize, Debug, Clone)]
|
||||||
struct PossibleResponse {
|
struct PossibleResponse {
|
||||||
text: String,
|
text: String,
|
||||||
@@ -63,23 +62,6 @@ struct GeneratedResponses {
|
|||||||
responses: Vec<PossibleResponse>,
|
responses: Vec<PossibleResponse>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
||||||
enum ConversationEntry {
|
|
||||||
User(String),
|
|
||||||
Eva(String),
|
|
||||||
ShipComputer(String),
|
|
||||||
StageDirection(String),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
|
|
||||||
struct StageDirection {
|
|
||||||
episode_number: u32,
|
|
||||||
time_remaining: Duration,
|
|
||||||
narrative: String,
|
|
||||||
artifacts: Vec<String>
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#[derive(Debug, Default, Serialize, Deserialize, Clone, JsonSchema)]
|
#[derive(Debug, Default, Serialize, Deserialize, Clone, JsonSchema)]
|
||||||
struct StageDirectionUpdate {
|
struct StageDirectionUpdate {
|
||||||
episode_number: Option<u32>,
|
episode_number: Option<u32>,
|
||||||
@@ -91,66 +73,13 @@ struct StageEventArgs {
|
|||||||
text: String,
|
text: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
|
|
||||||
struct Scene {
|
|
||||||
conversation: Vec<ConversationEntry>,
|
|
||||||
direction: StageDirection
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Into<CreateChatCompletionRequest> for Scene {
|
|
||||||
fn into(self) -> CreateChatCompletionRequest {
|
|
||||||
let mut messages = vec![
|
|
||||||
ChatCompletionRequestMessage::System(ChatCompletionRequestSystemMessage { content: SYSTEM_PROMPT.into(), ..Default::default()}),
|
|
||||||
ChatCompletionRequestMessage::System(ChatCompletionRequestSystemMessage { content: serde_json::to_string(&self.direction).unwrap().into(), ..Default::default()}),
|
|
||||||
];
|
|
||||||
messages.extend(self.conversation.into_iter().map(|entry| {
|
|
||||||
match entry {
|
|
||||||
ConversationEntry::User(text) => ChatCompletionRequestMessage::User(ChatCompletionRequestUserMessage { content: text.into(), ..Default::default()}),
|
|
||||||
ConversationEntry::Eva(text) => ChatCompletionRequestMessage::Assistant(ChatCompletionRequestAssistantMessage { content: Some(text.into()), ..Default::default()}),
|
|
||||||
ConversationEntry::ShipComputer(text) => ChatCompletionRequestMessage::System(ChatCompletionRequestSystemMessage { content: text.into(), name: Some("ship-computer".into()), ..Default::default() }),
|
|
||||||
ConversationEntry::StageDirection(text) => ChatCompletionRequestMessage::System(ChatCompletionRequestSystemMessage { content: text.into(), name: Some("stage-direction".into()), ..Default::default() })
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
let response_schema: Value = schema_for!(GeneratedResponses).into();
|
|
||||||
let tools = vec![
|
|
||||||
ChatCompletionTools::Function(
|
|
||||||
ChatCompletionTool {
|
|
||||||
function: FunctionObject {
|
|
||||||
name: "set_stage_direction".into(),
|
|
||||||
description: Some("Set or update the current stage direction instructions.".into()),
|
|
||||||
parameters: Some(schema_for!(StageDirectionUpdate).into()),
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
),
|
|
||||||
ChatCompletionTools::Function(
|
|
||||||
ChatCompletionTool {
|
|
||||||
function: FunctionObject {
|
|
||||||
name: "log_stage_event".into(),
|
|
||||||
description: Some("Log an event in the stage direction.".into()),
|
|
||||||
parameters: Some(schema_for!(StageEventArgs).into()),
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
];
|
|
||||||
CreateChatCompletionRequest {
|
|
||||||
model: "gpt-5.4-mini".into(),
|
|
||||||
messages: messages,
|
|
||||||
max_completion_tokens: Some(350),
|
|
||||||
response_format: Some(ResponseFormat::JsonSchema { json_schema: ResponseFormatJsonSchema { description: None, name: "responses".into(), schema: response_schema, strict: None } }),
|
|
||||||
tools: Some(tools),
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
struct App {
|
struct App {
|
||||||
scene: Scene,
|
scene: Scene,
|
||||||
next_reply_options: Vec<PossibleResponse>,
|
next_reply_options: Vec<PossibleResponse>,
|
||||||
reply_state: ListState,
|
reply_state: ListState,
|
||||||
user_input: Input,
|
user_input: Input,
|
||||||
|
end_time: DateTime<Utc>,
|
||||||
throbber_state: ThrobberState,
|
throbber_state: ThrobberState,
|
||||||
prediction_request_sink: watch::Sender<Scene>,
|
prediction_request_sink: watch::Sender<Scene>,
|
||||||
is_requesting: bool
|
is_requesting: bool
|
||||||
@@ -163,6 +92,7 @@ impl App {
|
|||||||
next_reply_options: Vec::new(),
|
next_reply_options: Vec::new(),
|
||||||
reply_state: ListState::default(),
|
reply_state: ListState::default(),
|
||||||
user_input: Input::default(),
|
user_input: Input::default(),
|
||||||
|
end_time: Utc::now() + Duration::hours(2),
|
||||||
throbber_state: ThrobberState::default(),
|
throbber_state: ThrobberState::default(),
|
||||||
prediction_request_sink,
|
prediction_request_sink,
|
||||||
is_requesting: false
|
is_requesting: false
|
||||||
@@ -176,6 +106,7 @@ impl App {
|
|||||||
ConversationEntry::Eva(text) => Line::from_iter([Span::from("Eva: ").style(ratatui::style::Color::Cyan), Span::from(text)]),
|
ConversationEntry::Eva(text) => Line::from_iter([Span::from("Eva: ").style(ratatui::style::Color::Cyan), Span::from(text)]),
|
||||||
ConversationEntry::ShipComputer(text) => Line::from_iter([Span::from("Ship Computer: ").style(ratatui::style::Color::Green), Span::from(text)]),
|
ConversationEntry::ShipComputer(text) => Line::from_iter([Span::from("Ship Computer: ").style(ratatui::style::Color::Green), Span::from(text)]),
|
||||||
ConversationEntry::StageDirection(text) => Line::from_iter([text]).style(ratatui::style::Color::Yellow),
|
ConversationEntry::StageDirection(text) => Line::from_iter([text]).style(ratatui::style::Color::Yellow),
|
||||||
|
ConversationEntry::SystemMessage(text) => Line::from_iter([text]).style(ratatui::style::Color::DarkGray)
|
||||||
}
|
}
|
||||||
}).collect();
|
}).collect();
|
||||||
frame.render_widget(List::new(items).block(Block::bordered().border_style(style::Color::LightYellow).title("Conversation")).direction(ListDirection::BottomToTop), area);
|
frame.render_widget(List::new(items).block(Block::bordered().border_style(style::Color::LightYellow).title("Conversation")).direction(ListDirection::BottomToTop), area);
|
||||||
@@ -187,7 +118,7 @@ impl App {
|
|||||||
.block(Block::bordered().border_style(style::Color::LightGreen).title("Reply Options (Press 'Ctrl+R' to regenerate, Tab to use)"))
|
.block(Block::bordered().border_style(style::Color::LightGreen).title("Reply Options (Press 'Ctrl+R' to regenerate, Tab to use)"))
|
||||||
.style(ratatui::style::Color::White)
|
.style(ratatui::style::Color::White)
|
||||||
.highlight_symbol("> ")
|
.highlight_symbol("> ")
|
||||||
.highlight_style(Modifier::REVERSED),
|
.highlight_style(style::Style::new().bold().fg(style::Color::Cyan)),
|
||||||
area,
|
area,
|
||||||
&mut self.reply_state
|
&mut self.reply_state
|
||||||
);
|
);
|
||||||
@@ -213,7 +144,7 @@ impl App {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn draw_status(&self, frame: &mut Frame, area: Rect) {
|
fn draw_status(&self, frame: &mut Frame, area: Rect) {
|
||||||
let minutes_remaining = self.scene.direction.time_remaining.as_secs() / 60;
|
let minutes_remaining = self.scene.direction.time_remaining.num_seconds() / 60;
|
||||||
let time_style = if minutes_remaining == 0 {
|
let time_style = if minutes_remaining == 0 {
|
||||||
Style::new().fg(ratatui::style::Color::Red).bold().rapid_blink()
|
Style::new().fg(ratatui::style::Color::Red).bold().rapid_blink()
|
||||||
} else if minutes_remaining <= 5 {
|
} else if minutes_remaining <= 5 {
|
||||||
@@ -230,7 +161,7 @@ impl App {
|
|||||||
let status_line = Line::from_iter([
|
let status_line = Line::from_iter([
|
||||||
Span::from(format!("Episode {}", self.scene.direction.episode_number)).style(ratatui::style::Color::LightBlue),
|
Span::from(format!("Episode {}", self.scene.direction.episode_number)).style(ratatui::style::Color::LightBlue),
|
||||||
Span::from(" | ").style(ratatui::style::Color::DarkGray),
|
Span::from(" | ").style(ratatui::style::Color::DarkGray),
|
||||||
Span::from(format!("Time Remaining: {}m", minutes_remaining)).style(time_style)
|
Span::from(format!("Time Remaining: {:0>2}:{:0>2}:{:0>2}", self.scene.direction.time_remaining.num_hours(), self.scene.direction.time_remaining.num_minutes() % 60, self.scene.direction.time_remaining.num_seconds() % 60)).style(time_style)
|
||||||
]);
|
]);
|
||||||
frame.render_widget(status_line, area);
|
frame.render_widget(status_line, area);
|
||||||
}
|
}
|
||||||
@@ -294,9 +225,9 @@ impl App {
|
|||||||
fn insert_selected_prompt(&mut self) {
|
fn insert_selected_prompt(&mut self) {
|
||||||
let selected = self.next_reply_options[self.reply_state.selected().unwrap()].clone();
|
let selected = self.next_reply_options[self.reply_state.selected().unwrap()].clone();
|
||||||
if let Some(direction) = &selected.stage_direction {
|
if let Some(direction) = &selected.stage_direction {
|
||||||
self.insert_stage_direction(direction);
|
self.scene.insert_conversation(ConversationEntry::StageDirection(direction.clone()));
|
||||||
}
|
}
|
||||||
self.insert_reply(&selected.text);
|
self.scene.insert_conversation(ConversationEntry::Eva(selected.text.clone()));
|
||||||
self.save();
|
self.save();
|
||||||
self.speak(&selected.text.as_str());
|
self.speak(&selected.text.as_str());
|
||||||
self.regenerate_responses();
|
self.regenerate_responses();
|
||||||
@@ -323,14 +254,14 @@ impl App {
|
|||||||
match command {
|
match command {
|
||||||
"/bandcamp" => {
|
"/bandcamp" => {
|
||||||
self.add_bandcamp_artifact(arg).await;
|
self.add_bandcamp_artifact(arg).await;
|
||||||
self.insert_computer_reply(format!("Added Bandcamp artifact from {}", arg).as_str());
|
self.scene.insert_conversation(ConversationEntry::SystemMessage(format!("Added Bandcamp artifact from {}", arg)));
|
||||||
},
|
},
|
||||||
"/episode" => {
|
"/episode" => {
|
||||||
if let Ok(episode_number) = arg.trim().parse::<u32>() {
|
if let Ok(episode_number) = arg.trim().parse::<u32>() {
|
||||||
self.scene.direction.episode_number = episode_number;
|
self.scene.direction.episode_number = episode_number;
|
||||||
self.insert_computer_reply(&format!("Updated episode number: {}", self.scene.direction.episode_number));
|
self.scene.insert_conversation(ConversationEntry::SystemMessage(format!("Updated episode number: {}", self.scene.direction.episode_number)));
|
||||||
} else {
|
} else {
|
||||||
self.insert_computer_reply("Invalid episode number format. Use /episode [number]");
|
self.scene.insert_conversation(ConversationEntry::SystemMessage("Invalid episode number format. Use /episode [number]".into()));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/reset" => {
|
"/reset" => {
|
||||||
@@ -339,12 +270,15 @@ impl App {
|
|||||||
},
|
},
|
||||||
"/narrative" => {
|
"/narrative" => {
|
||||||
self.scene.direction.narrative = arg.to_string();
|
self.scene.direction.narrative = arg.to_string();
|
||||||
self.insert_computer_reply(&format!("Updated stage direction: {}", self.scene.direction.narrative));
|
self.scene.insert_conversation(ConversationEntry::SystemMessage(format!("Updated stage direction: {}", self.scene.direction.narrative)));
|
||||||
},
|
},
|
||||||
_ => self.insert_computer_reply("Unknown command. Available commands: /bandcamp [url], /episode [number], /narrative [text], /reset")
|
_ => {
|
||||||
|
self.scene.insert_conversation(ConversationEntry::SystemMessage("Unknown command. Available commands: /bandcamp [url], /episode [number], /narrative [text], /reset".into()));
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
self.insert_chat(&next_msg);
|
self.scene.insert_conversation(ConversationEntry::User(next_msg));
|
||||||
}
|
}
|
||||||
self.save();
|
self.save();
|
||||||
self.regenerate_responses();
|
self.regenerate_responses();
|
||||||
@@ -370,26 +304,15 @@ impl App {
|
|||||||
|
|
||||||
fn load(&mut self) {
|
fn load(&mut self) {
|
||||||
if let Ok(save_data) = std::fs::read_to_string("save.json") {
|
if let Ok(save_data) = std::fs::read_to_string("save.json") {
|
||||||
self.scene = serde_json::from_str(&save_data).unwrap();
|
if let Ok(scene) = serde_json::from_str(&save_data) {
|
||||||
|
self.scene = scene;
|
||||||
|
self.scene.insert_conversation(ConversationEntry::SystemMessage("Loaded stored session.".into()));
|
||||||
|
} else {
|
||||||
|
self.scene.insert_conversation(ConversationEntry::SystemMessage("Failed to load saved session!".into()));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn insert_chat(&mut self, text: &str) {
|
|
||||||
self.scene.conversation.push(ConversationEntry::User(text.to_string()));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn insert_reply(&mut self, text: &str) {
|
|
||||||
self.scene.conversation.push(ConversationEntry::Eva(text.to_string()));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn insert_computer_reply(&mut self, text: &str) {
|
|
||||||
self.scene.conversation.push(ConversationEntry::ShipComputer(text.to_string()));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn insert_stage_direction(&mut self, text: &str) {
|
|
||||||
self.scene.conversation.push(ConversationEntry::StageDirection(text.to_string()));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn speak(&mut self, text: &str) {
|
fn speak(&mut self, text: &str) {
|
||||||
Command::new("spd-say").arg("-y").arg("English (America)+Linda").arg(text).spawn().unwrap().wait().unwrap();
|
Command::new("spd-say").arg("-y").arg("English (America)+Linda").arg(text).spawn().unwrap().wait().unwrap();
|
||||||
}
|
}
|
||||||
@@ -410,15 +333,15 @@ impl App {
|
|||||||
let args: StageDirectionUpdate = serde_json::from_str(call.function.arguments.as_str()).unwrap();
|
let args: StageDirectionUpdate = serde_json::from_str(call.function.arguments.as_str()).unwrap();
|
||||||
if let Some(episode_number) = args.episode_number {
|
if let Some(episode_number) = args.episode_number {
|
||||||
self.scene.direction.episode_number = episode_number;
|
self.scene.direction.episode_number = episode_number;
|
||||||
self.insert_computer_reply(&format!("Updated episode number: {}", self.scene.direction.episode_number));
|
self.scene.insert_conversation(ConversationEntry::ShipComputer(format!("Updated episode number: {}", self.scene.direction.episode_number)));
|
||||||
}
|
}
|
||||||
if let Some(narrative) = args.narrative {
|
if let Some(narrative) = args.narrative {
|
||||||
self.scene.direction.narrative = narrative;
|
self.scene.direction.narrative = narrative;
|
||||||
self.insert_computer_reply(&format!("Updated stage direction: {}", self.scene.direction.narrative));
|
self.scene.insert_conversation(ConversationEntry::ShipComputer(format!("Updated stage direction: {}", self.scene.direction.narrative)));
|
||||||
}
|
}
|
||||||
} else if call.function.name == "log_stage_event" {
|
} else if call.function.name == "log_stage_event" {
|
||||||
let args: StageEventArgs = serde_json::from_str(call.function.arguments.as_str()).unwrap();
|
let args: StageEventArgs = serde_json::from_str(call.function.arguments.as_str()).unwrap();
|
||||||
self.insert_stage_direction(&args.text);
|
self.scene.insert_conversation(ConversationEntry::StageDirection(args.text));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
_ => panic!("Unkown tool call type"),
|
_ => panic!("Unkown tool call type"),
|
||||||
@@ -438,7 +361,7 @@ impl App {
|
|||||||
async fn main() {
|
async fn main() {
|
||||||
color_eyre::install().unwrap();
|
color_eyre::install().unwrap();
|
||||||
|
|
||||||
let mut terminal = ratatui::init();
|
let mut terminal: Terminal<CrosstermBackend<std::io::Stdout>> = ratatui::init();
|
||||||
|
|
||||||
let (prediction_in, mut prediction_out) = tokio::sync::watch::channel(None);
|
let (prediction_in, mut prediction_out) = tokio::sync::watch::channel(None);
|
||||||
let (prediction_request_in, mut prediction_request_out) = tokio::sync::watch::channel(Scene::default());
|
let (prediction_request_in, mut prediction_request_out) = tokio::sync::watch::channel(Scene::default());
|
||||||
@@ -448,7 +371,32 @@ async fn main() {
|
|||||||
loop {
|
loop {
|
||||||
prediction_request_out.changed().await.unwrap();
|
prediction_request_out.changed().await.unwrap();
|
||||||
let request = prediction_request_out.borrow().clone();
|
let request = prediction_request_out.borrow().clone();
|
||||||
let response = client.chat().create(request.into()).await.unwrap();
|
let chat_request = CreateChatCompletionRequest {
|
||||||
|
tools: Some(vec![
|
||||||
|
ChatCompletionTools::Function(
|
||||||
|
ChatCompletionTool {
|
||||||
|
function: FunctionObject {
|
||||||
|
name: "set_stage_direction".into(),
|
||||||
|
description: Some("Set or update the current stage direction instructions.".into()),
|
||||||
|
parameters: Some(schema_for!(StageDirectionUpdate).into()),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
),
|
||||||
|
ChatCompletionTools::Function(
|
||||||
|
ChatCompletionTool {
|
||||||
|
function: FunctionObject {
|
||||||
|
name: "log_stage_event".into(),
|
||||||
|
description: Some("Log an event in the stage direction.".into()),
|
||||||
|
parameters: Some(schema_for!(StageEventArgs).into()),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
]),
|
||||||
|
..request.into()
|
||||||
|
};
|
||||||
|
let response = client.chat().create(chat_request).await.unwrap();
|
||||||
prediction_in.send(Some(response)).unwrap();
|
prediction_in.send(Some(response)).unwrap();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -461,8 +409,9 @@ async fn main() {
|
|||||||
loop {
|
loop {
|
||||||
terminal.draw(|frame| { app.draw(frame)}).unwrap();
|
terminal.draw(|frame| { app.draw(frame)}).unwrap();
|
||||||
app.throbber_state.calc_next();
|
app.throbber_state.calc_next();
|
||||||
|
app.scene.direction.time_remaining = app.end_time.signed_duration_since(Utc::now());
|
||||||
|
|
||||||
let delay = Delay::new(Duration::from_millis(60)).fuse();
|
let delay = Delay::new(std::time::Duration::from_millis(60)).fuse();
|
||||||
let event = events.next().fuse();
|
let event = events.next().fuse();
|
||||||
|
|
||||||
tokio::select! {
|
tokio::select! {
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
use async_openai::types::chat::*;
|
||||||
|
use chrono::Duration;
|
||||||
|
use schemars::schema_for;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use serde_json::Value;
|
||||||
|
|
||||||
|
use crate::GeneratedResponses;
|
||||||
|
|
||||||
|
const SYSTEM_PROMPT: &str = include_str!("system-prompt.txt");
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub enum ConversationEntry {
|
||||||
|
User(String),
|
||||||
|
Eva(String),
|
||||||
|
ShipComputer(String),
|
||||||
|
StageDirection(String),
|
||||||
|
SystemMessage(String)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
|
||||||
|
pub struct StageDirection {
|
||||||
|
pub episode_number: u32,
|
||||||
|
pub time_remaining: Duration,
|
||||||
|
pub narrative: String,
|
||||||
|
pub artifacts: Vec<String>
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct Scene {
|
||||||
|
pub conversation: Vec<ConversationEntry>,
|
||||||
|
pub direction: StageDirection
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Scene {
|
||||||
|
pub fn insert_conversation(&mut self, entry: ConversationEntry) {
|
||||||
|
self.conversation.push(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Into<CreateChatCompletionRequest> for Scene {
|
||||||
|
fn into(self) -> CreateChatCompletionRequest {
|
||||||
|
let mut messages = vec![
|
||||||
|
ChatCompletionRequestMessage::System(ChatCompletionRequestSystemMessage { content: SYSTEM_PROMPT.into(), ..Default::default()}),
|
||||||
|
ChatCompletionRequestMessage::System(ChatCompletionRequestSystemMessage { content: serde_json::to_string(&self.direction).unwrap().into(), ..Default::default()}),
|
||||||
|
];
|
||||||
|
messages.extend(self.conversation.into_iter().filter(|x| if let ConversationEntry::SystemMessage(_) = x { false } else { true }).map(|entry| {
|
||||||
|
match entry {
|
||||||
|
ConversationEntry::User(text) => ChatCompletionRequestMessage::User(ChatCompletionRequestUserMessage { content: text.into(), ..Default::default()}),
|
||||||
|
ConversationEntry::Eva(text) => ChatCompletionRequestMessage::Assistant(ChatCompletionRequestAssistantMessage { content: Some(text.into()), ..Default::default()}),
|
||||||
|
ConversationEntry::ShipComputer(text) => ChatCompletionRequestMessage::System(ChatCompletionRequestSystemMessage { content: text.into(), name: Some("ship-computer".into()), ..Default::default() }),
|
||||||
|
ConversationEntry::StageDirection(text) => ChatCompletionRequestMessage::System(ChatCompletionRequestSystemMessage { content: text.into(), name: Some("stage-direction".into()), ..Default::default() }),
|
||||||
|
ConversationEntry::SystemMessage(_) => unreachable!()
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
let response_schema: Value = schema_for!(GeneratedResponses).into();
|
||||||
|
CreateChatCompletionRequest {
|
||||||
|
model: "gpt-5.4-mini".into(),
|
||||||
|
messages: messages,
|
||||||
|
max_completion_tokens: Some(350),
|
||||||
|
response_format: Some(ResponseFormat::JsonSchema { json_schema: ResponseFormatJsonSchema { description: None, name: "responses".into(), schema: response_schema, strict: None } }),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user