clippy++ and report internal error types from tools

This commit is contained in:
2026-06-22 13:53:01 +02:00
parent 242771332c
commit 7563ab85ca
12 changed files with 139 additions and 162 deletions
+13 -13
View File
@@ -56,7 +56,7 @@ impl Archive {
// track, album, artist
pub fn stats(&self) -> (usize, usize, usize) {
self.contents.iter().map(|(_id, artifact)| {
self.contents.values().map(|artifact| {
match artifact.contents() {
Contents::Track(_) => (1, 0, 0),
Contents::Album(_) => (0, 1, 0),
@@ -68,16 +68,16 @@ impl Archive {
}
pub fn get<'a>(&'a self, id: &Uuid) -> Option<ArtifactRef<'a>> {
if self.contents.get(id).is_some() {
Some(ArtifactRef { id: id.clone(), archive: self })
if self.contents.contains_key(id) {
Some(ArtifactRef { id: *id, archive: self })
} else {
None
}
}
pub fn get_mut<'a>(&'a mut self, id: &Uuid) -> Option<ArtifactRefMut<'a>> {
if self.contents.get(id).is_some() {
Some(ArtifactRefMut { id: id.clone(), archive: self })
if self.contents.contains_key(id) {
Some(ArtifactRefMut { id: *id, archive: self })
} else {
None
}
@@ -136,7 +136,7 @@ impl Archive {
pub fn insert<'a>(&'a mut self, artifact: Artifact) -> ArtifactRef<'a> {
// If we are inserting a new artifact with a complete MBID...
if let Some(mbid) = artifact.mbid.clone() {
if let Some(mbid) = artifact.mbid {
let search_id = mbid;
// If an entry already exists keyed by this MBID, merge into it
if let Some(existing) = self.contents.get_mut(&search_id) {
@@ -145,7 +145,7 @@ impl Archive {
} else {
// Otherwise, attempt to find existing artifacts with the same contents (but no MBID)
let mut targets: Vec<(Uuid, Artifact)> = self.contents.extract_if(|_, v| { v.contents == artifact.contents }).collect();
if let Some((target_id, mut target)) = targets.pop() {
if let Some((_target_id, mut target)) = targets.pop() {
// Merge any other extracted targets into the primary one
for (_, next) in targets {
target.merge(next);
@@ -153,11 +153,11 @@ impl Archive {
// Merge the incoming artifact into the merged target
target.merge(artifact);
// Insert merged target under the canonical MBID key
self.contents.insert(search_id.clone(), target);
self.contents.insert(search_id, target);
ArtifactRef { id: search_id, archive: self }
} else {
// No matching content found: insert under the MBID key
self.contents.insert(search_id.clone(), artifact);
self.contents.insert(search_id, artifact);
ArtifactRef { id: search_id, archive: self }
}
}
@@ -165,9 +165,9 @@ impl Archive {
// Otherwise, we attempt to merge it in. In the end, there will somehow still be a record with this mbid
let mut targets: Vec<(Uuid, Artifact)> = self.contents.extract_if(|_, v| { v.contents == artifact.contents }).collect();
if let Some((target_id, mut target)) = targets.pop() {
let next_id = if let Some(ref mbid) = artifact.mbid {
let next_id = if let Some(mbid) = artifact.mbid {
// If the new artifact has an mbid, we start using that as the archive key
mbid.clone()
mbid
} else {
// Otherwise, why regenerate a new one?
target_id
@@ -178,11 +178,11 @@ impl Archive {
}
target.merge(artifact);
// Re-insert the merged target back into the archive under the chosen id
self.contents.insert(next_id.clone(), target);
self.contents.insert(next_id, target);
ArtifactRef { id: next_id, archive: self }
} else {
let new_id = Uuid::new_v4();
self.contents.insert(new_id.clone(), artifact);
self.contents.insert(new_id, artifact);
ArtifactRef { id: new_id, archive: self }
}
}
+10 -10
View File
@@ -9,21 +9,21 @@ pub struct BandcampQueryArgs {
pub query: String
}
impl Into<Artifact> for bandcamp::Artist {
fn into(self) -> Artifact {
ArtifactBuilder::new(SourceID::Bandcamp).contents(Artist { name: self.name, bio: self.bio, location: self.location }).build()
impl From<bandcamp::Artist> for Artifact {
fn from(val: bandcamp::Artist) -> Self {
ArtifactBuilder::new(SourceID::Bandcamp).contents(Artist { name: val.name, bio: val.bio, location: val.location }).build()
}
}
impl Into<Artifact> for bandcamp::Album {
fn into(self) -> Artifact {
impl From<bandcamp::Album> for Artifact {
fn from(val: bandcamp::Album) -> Self {
ArtifactBuilder::new(SourceID::Bandcamp)
.contents(Album {
about: self.about,
title: self.title,
artist: self.band.name,
credits: self.credits,
release_date: Some(self.release_date)
about: val.about,
title: val.title,
artist: val.band.name,
credits: val.credits,
release_date: Some(val.release_date)
}).build()
}
}
+10 -14
View File
@@ -35,20 +35,20 @@ struct BeetsTrack {
mb_trackid: Option<String>
}
impl Into<Artifact> for BeetsTrack {
fn into(self) -> Artifact {
impl From<BeetsTrack> for Artifact {
fn from(val: BeetsTrack) -> Self {
let track_data = Track {
title: self.title,
label: self.label,
year: Some(self.year),
genres: self.genres.unwrap_or_default(),
album: Some(self.album),
artist: Some(self.artist),
title: val.title,
label: val.label,
year: Some(val.year),
genres: val.genres.unwrap_or_default(),
album: Some(val.album),
artist: Some(val.artist),
bpm: None,
};
let builder = ArtifactBuilder::new(SourceID::Beets)
.contents(track_data);
if let Ok(mbid) = Uuid::parse_str(&self.mb_trackid.unwrap_or_default()) {
if let Ok(mbid) = Uuid::parse_str(&val.mb_trackid.unwrap_or_default()) {
builder.mbid(mbid).build()
} else {
builder.build()
@@ -136,8 +136,7 @@ impl DataSource for BeetsDB {
type Error = BeetsError;
async fn synchronize(&self, artifact: &mut Artifact) -> Result<Vec<Artifact>, Self::Error> {
match artifact.contents {
Contents::Track(ref mut target_track) => {
if let Contents::Track(ref mut target_track) = artifact.contents {
let args = BeatsQueryArgs {
title: Some(target_track.title.clone()),
artist: target_track.artist.clone(),
@@ -152,9 +151,6 @@ impl DataSource for BeetsDB {
} else {
log::debug!("Beets could not find {:?}", target_track);
}
},
_ => ()
}
Ok(vec![])
-12
View File
@@ -37,10 +37,6 @@ impl PartialEq for Artist {
true
}
fn ne(&self, other: &Self) -> bool {
!self.eq(other)
}
}
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
@@ -63,10 +59,6 @@ impl PartialEq for Album {
true
}
fn ne(&self, other: &Self) -> bool {
!self.eq(other)
}
}
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
@@ -103,10 +95,6 @@ impl PartialEq for Track {
true
}
fn ne(&self, other: &Self) -> bool {
!self.eq(other)
}
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
+7 -23
View File
@@ -11,17 +11,8 @@ use crate::artifacts::{Album, Artifact, ArtifactBuilder, Artist, Contents, Merge
impl From<Recording> for Track {
fn from(value: Recording) -> Self {
let artist = if let Some(artist) = value.artist_credit.unwrap_or_default().first() {
Some(artist.name.clone())
} else {
None
};
let album = if let Some(album) = value.releases.unwrap_or_default().first() {
Some(album.title.clone())
} else {
None
};
let artist = value.artist_credit.unwrap_or_default().first().map(|x| x.name.clone() );
let album = value.releases.unwrap_or_default().first().map(|x| x.title.clone() );
Self {
title: value.title,
@@ -34,16 +25,12 @@ impl From<Recording> for Track {
impl From<Release> for Album {
fn from(value: Release) -> Self {
let artist = if let Some(artist) = value.artist_credit.unwrap_or_default().first() {
Some(artist.name.clone())
} else {
None
}.unwrap_or_default();
let artist = value.artist_credit.unwrap_or_default().first().map(|x| x.name.clone() );
Self {
about: value.annotation,
title: value.title,
artist,
artist: artist.unwrap_or_default(),
..Default::default()
}
}
@@ -55,7 +42,6 @@ impl From<ArtistCredit> for Artist {
bio: value.artist.annotation,
location: value.artist.country,
name: value.name,
..Default::default()
}
}
}
@@ -98,10 +84,10 @@ impl DataSource for MBQuery {
if artifact.mbid.is_none() {
return Ok(ret);
}
let artifact_id = artifact.mbid.clone().unwrap();
let artifact_id = artifact.mbid.unwrap();
log::debug!("Synchronizing {} with musicbrainz", artifact_id);
match artifact.contents {
Contents::Track(_) => {
// FIXME: Need to also synchronize albums and artists
if let Contents::Track(_) = artifact.contents {
let mb_track = Recording::fetch()
.id(&artifact_id.to_string())
.with_releases().with_artists().with_annotations().execute_async().await;
@@ -119,8 +105,6 @@ impl DataSource for MBQuery {
ret.push(track.clone());
ret.append(&mut new_artifacts);
artifact.merge(track);
},
_ => ()
}
Ok(ret)
+8 -8
View File
@@ -32,19 +32,19 @@ impl Tool {
}
}
impl Into<ChatCompletionTool> for Tool {
fn into(self) -> ChatCompletionTool {
impl From<Tool> for ChatCompletionTool {
fn from(val: Tool) -> Self {
ChatCompletionTool {
function: FunctionObjectArgs::default()
.name(self.name)
.description(self.description)
.parameters(self.schema).build().unwrap()
.name(val.name)
.description(val.description)
.parameters(val.schema).build().unwrap()
}
}
}
impl Into<ChatCompletionTools> for Tool {
fn into(self) -> ChatCompletionTools {
ChatCompletionTools::Function(self.into())
impl From<Tool> for ChatCompletionTools {
fn from(val: Tool) -> Self {
ChatCompletionTools::Function(val.into())
}
}
+1 -1
View File
@@ -78,7 +78,7 @@ impl<T: std::io::Write + Send + Sync> log::Log for SysMessageLogger<T> {
fn log(&self, record: &log::Record) {
let msg = format!("{}", record.args());
write!(self.1.lock().unwrap(), "{}\n", msg).unwrap();
writeln!(self.1.lock().unwrap(), "{}", msg).unwrap();
if record.level() <= LevelFilter::Info {
self.0.send(ConversationEntry::SystemMessage(msg)).unwrap();
}
+14 -9
View File
@@ -48,7 +48,7 @@ pub struct Character {
}
impl Character {
pub async fn regenerate<'a, T: Toolbox>(&mut self, client: &mut Client<OpenAIConfig>, context: ChatCompletionRequestMessage, toolbox: &mut T, output: &mut mpsc::UnboundedSender<PredictionAction>, schema: &Value) -> Result<(usize, Option<Value>), CharacterError> {
pub async fn regenerate<T: Toolbox>(&mut self, client: &mut Client<OpenAIConfig>, context: ChatCompletionRequestMessage, toolbox: &mut T, output: &mut mpsc::UnboundedSender<PredictionAction>, schema: &Value) -> Result<(usize, Option<Value>), CharacterError> {
let mut full_conversation = vec![
self.header_message.clone(),
context
@@ -106,15 +106,21 @@ impl Character {
ChatCompletionMessageToolCalls::Function(call) => {
log::debug!("Tool {} {}/{}", call.function.name, idx+1, calls.len());
log::debug!("Args {}", call.function.arguments);
if let Some(tool_result) = toolbox.execute_tool(call).await {
match toolbox.execute_tool(call).await {
Ok(tool_result) => {
// Push tool output messages directly into the conversation as fast as we can
for message in &tool_result.messages {
output.send(PredictionAction::ConversationAppend(message.clone())).unwrap();
}
results.push((&call.id, tool_result));
} else {
results.push((&call.id, ToolResults::default()));
log::error!("Attemped to call {:?}, but no result was returned.", call);
},
Err(err) => {
results.push((&call.id, ToolResults {
result: Some(format!("Error while calling tool: {:?}", err)),
..Default::default()
}));
log::error!("Attemped to call {:?}, but got an error instead: {:?}", call, err);
}
}
},
_ => panic!("Unknown tool was called")
@@ -141,13 +147,12 @@ impl Character {
}
if let Some(content) = message.message.content.as_ref() {
let options = serde_json::from_str(content.as_str())?;
return Ok((tokens_used as usize, Some(options)));
Ok((tokens_used as usize, Some(options)))
} else {
return Ok((tokens_used as usize, None));
Ok((tokens_used as usize, None))
}
} else {
log::info!("No messages were received!");
return Err(CharacterError::NoOutput);
Err(CharacterError::NoOutput)
}
}
+2 -6
View File
@@ -164,7 +164,7 @@ impl Conversation {
self.insert(ConversationEntry::Spoken(Speaker::ShipComputer, response.message)).await;
if response.finished.unwrap_or_default() {
self.characters.get_mut(&Speaker::ShipComputer).unwrap().forget().await;
if self.computer_todo.lock().await.iter().filter(|(_, is_finished)| !*is_finished).next().is_none() {
if !self.computer_todo.lock().await.iter().any(|(_, is_finished)| !*is_finished) {
self.insert(ConversationEntry::StageDirection("The ship computer goes idle.".into())).await;
self.event_sink.send(SessionUpdate::Thinking(Speaker::ShipComputer, false)).unwrap();
} else {
@@ -290,11 +290,7 @@ pub async fn conversation_task(save_data: SaveData, sys_log_messages: tokio::syn
};
let backlog: Vec<_> = save_data.messages.iter().filter_map(|msg| {
if let Ok(entry) = msg.clone().try_into() {
Some(entry)
} else {
None
}
msg.clone().try_into().ok()
}).collect();
event_sink.send(SessionUpdate::Conversation(backlog.clone())).unwrap();
+35 -23
View File
@@ -5,11 +5,11 @@ use schemars::{JsonSchema, schema_for};
use serde::{Deserialize, Serialize};
use tokio::sync::Mutex;
use crate::{artifacts::{archive::Archive, bandcamp::BandcampSource, beets::BeetsDB, mixxx::MixxxDB, musicbrainz::MBQuery, tools::{DataSource, Tool}}, scene::conversation::{ConversationEntry, Speaker}};
use crate::{artifacts::{archive::Archive, beets::BeetsDB, mixxx::MixxxDB, musicbrainz::MBQuery, tools::{DataSource, Tool}}, scene::conversation::{ConversationEntry, Speaker}};
pub trait Toolbox {
fn tools(&self) -> Vec<ChatCompletionTools>;
fn execute_tool(&mut self, call: &ChatCompletionMessageToolCall) -> impl Future<Output = Option<ToolResults>> + Send;
fn execute_tool(&mut self, call: &ChatCompletionMessageToolCall) -> impl Future<Output = Result<ToolResults, ToolError>> + Send;
}
pub struct StageToolbox;
@@ -27,6 +27,18 @@ impl StageToolbox {
}
}
#[derive(Debug)]
pub enum ToolError {
InvalidToolName,
Json(serde_json::Error)
}
impl From<serde_json::Error> for ToolError {
fn from(value: serde_json::Error) -> Self {
Self::Json(value)
}
}
impl Toolbox for StageToolbox {
fn tools(&self) -> Vec<ChatCompletionTools> {
vec![
@@ -34,13 +46,13 @@ impl Toolbox for StageToolbox {
]
}
async fn execute_tool(&mut self, call: &ChatCompletionMessageToolCall) -> Option<ToolResults> {
async fn execute_tool(&mut self, call: &ChatCompletionMessageToolCall) -> Result<ToolResults, ToolError> {
let func_name = call.function.name.as_str();
let args = call.function.arguments.as_str();
Some(match func_name {
"log_stage_event" => self.tool_stage_event(serde_json::from_str(args).unwrap()).await,
_ => return None
})
match func_name {
"log_stage_event" => Ok(self.tool_stage_event(serde_json::from_str(args)?).await),
_ => Err(ToolError::InvalidToolName)
}
}
}
@@ -82,24 +94,24 @@ impl Toolbox for ArchiveToolbox {
]
}
async fn execute_tool(&mut self, call: &ChatCompletionMessageToolCall) -> Option<ToolResults> {
async fn execute_tool(&mut self, call: &ChatCompletionMessageToolCall) -> Result<ToolResults, ToolError> {
let func_name = call.function.name.as_str();
let args = call.function.arguments.as_str();
Some(match func_name {
"query_bandcamp" => ToolResults { result: None, messages: vec![] },
match func_name {
"query_bandcamp" => Ok(ToolResults { result: None, messages: vec![] }),
"query_beets" => self.tool_artifact_query(&mut BeetsDB, args).await,
"query_musicbrainz" => self.tool_artifact_query(&mut MBQuery, args).await,
"query_mixxx" => self.tool_artifact_query(&mut MixxxDB, args).await,
"synchronize_artifacts" => self.synchronize_artifacts().await,
"task_list" => self.tasklist_operation(args).await,
_ => return None
})
_ => Err(ToolError::InvalidToolName)
}
}
}
impl ArchiveToolbox {
async fn tasklist_operation(&mut self, json_args: &str) -> ToolResults {
let args: TaskListArgs = serde_json::from_str(json_args).unwrap_or_default();
async fn tasklist_operation(&mut self, json_args: &str) -> Result<ToolResults, ToolError> {
let args: TaskListArgs = serde_json::from_str(json_args)?;
let mut locked = self.todo_list.lock().await;
@@ -117,22 +129,22 @@ impl ArchiveToolbox {
}
}
ToolResults {
Ok(ToolResults {
..Default::default()
}
})
}
async fn synchronize_artifacts(&mut self) -> ToolResults {
async fn synchronize_artifacts(&mut self) -> Result<ToolResults, ToolError> {
let updated_count = self.archive.lock().await.synchronize().await;
ToolResults {
Ok(ToolResults {
messages: vec![ConversationEntry::Spoken(Speaker::ShipComputer, format!("Synchronized {} items", updated_count))],
..Default::default()
}
})
}
async fn tool_artifact_query<Src: DataSource>(&mut self, src: &mut Src, json_args: &str) -> ToolResults where Src::Args: core::fmt::Debug, Src::Error: core::fmt::Debug {
let args: Src::Args = serde_json::from_str(json_args).unwrap();
async fn tool_artifact_query<Src: DataSource>(&mut self, src: &mut Src, json_args: &str) -> Result<ToolResults, ToolError> where Src::Args: core::fmt::Debug, Src::Error: core::fmt::Debug {
let args: Src::Args = serde_json::from_str(json_args)?;
log::debug!("Executing query {:?}", args);
let result;
match src.query(&args).await {
@@ -147,10 +159,10 @@ impl ArchiveToolbox {
}
}
ToolResults {
Ok(ToolResults {
result: Some(result),
messages: vec![]
}
})
}
}
+6 -10
View File
@@ -91,7 +91,7 @@ impl Ui {
let throbber = Throbber::default().throbber_style(Style::new().cyan());
frame.render_stateful_widget(throbber, throb_area, &mut self.throbber_state);
} else {
frame.render_widget(Clear::default(), throb_area);
frame.render_widget(Clear, throb_area);
}
}
@@ -101,7 +101,7 @@ impl Ui {
let throbber = Throbber::default().throbber_style(Style::new().red()).throbber_set(throbber_widgets_tui::WHITE_SQUARE);
frame.render_stateful_widget(throbber, throb_area, &mut self.throbber_state);
} else {
frame.render_widget(Clear::default(), throb_area);
frame.render_widget(Clear, throb_area);
}
}
@@ -170,7 +170,6 @@ impl Ui {
self.predictions.insert(PredictionAction::SetPlaylist(playlist_name)).await;
} else {
log::error!("Invalid episode number format. Use /episode [number]");
return;
}
},
"/playlist" => {
@@ -193,7 +192,6 @@ impl Ui {
},
"/computer" => {
self.predictions.insert(PredictionAction::ComputerCommand(arg.to_string())).await;
return;
},
_ => {
log::error!("Unknown command. Available commands: /episode [number], /narrative [text], /event [text], /computer [text], /timer [minutes]");
@@ -221,7 +219,7 @@ impl Ui {
KeyCode::Enter => {
let row_num = self.conversation_state.selected().unwrap();
if let ConversationEntry::Spoken(Speaker::Eva, text) = &self.conversation[self.conversation.len() - 1 - row_num] {
self.tts.speak(text.clone()).await;
self.tts.speak(text.clone()).await.unwrap();
self.focus_state = FocusState::UserInput;
self.conversation_state.select(None);
self.reply_state.select_first();
@@ -241,10 +239,10 @@ impl Ui {
KeyCode::Char('x') if key.modifiers.contains(KeyModifiers::CONTROL) => {
if self.recording_audio {
self.recording_audio = false;
self.transcription.stop();
self.transcription.stop().unwrap();
} else {
self.recording_audio = true;
self.transcription.start();
self.transcription.start().unwrap();
}
},
KeyCode::Down => self.reply_state.select_next(),
@@ -256,13 +254,11 @@ impl Ui {
let next_msg = self.user_input.value_and_reset();
if next_msg.trim().is_empty() {
self.insert_selected_prompt().await;
} else {
if next_msg.starts_with("/") {
} else if next_msg.starts_with("/") {
self.on_command(&next_msg).await;
} else {
self.predictions.insert(PredictionAction::ConversationAppend(ConversationEntry::Spoken(Speaker::User, next_msg))).await;
}
}
},
_ => {self.user_input.handle_event(&evt);},
}
+1 -1
View File
@@ -182,7 +182,7 @@ impl Widget for StatusBar<'_> {
let negative = time_remaining.abs() != time_remaining;
let time_style = if minutes_remaining <= 0 || negative {
Style::new().fg(ratatui::style::Color::LightRed).bold()
Style::new().fg(ratatui::style::Color::LightRed).underlined()
} else if minutes_remaining < 5 {
Style::new().fg(ratatui::style::Color::LightRed).bold()
} else if minutes_remaining < 10 {