artifacts: implement musicbrainz tooling

This commit is contained in:
2026-06-16 11:32:15 +02:00
parent ac6cb425ac
commit d69ba43a6b
7 changed files with 177 additions and 56 deletions
+47 -36
View File
@@ -1,4 +1,4 @@
use std::sync::Arc;
use std::{collections::HashSet, sync::Arc};
use async_openai::{Client, config::OpenAIConfig, types::chat::{ChatCompletionMessageToolCalls, ChatCompletionRequestAssistantMessageArgs, ChatCompletionRequestMessage, ChatCompletionRequestSystemMessageArgs, ChatCompletionRequestToolMessageArgs, ChatCompletionTool, ChatCompletionTools, CreateChatCompletionRequestArgs, FinishReason, FunctionObjectArgs, ResponseFormat, ResponseFormatJsonSchema}};
use bandcamp::SearchResultItem;
@@ -8,7 +8,7 @@ use serde::{Deserialize, Serialize};
use serde_json::{Serializer, ser::CompactFormatter};
use tokio::sync::{RwLock, mpsc, watch};
use crate::{SaveData, artifacts::{Album, Artifact, Artist, Track, beets::BeatsQueryArgs, bandcamp::BandcampQueryArgs, mixxx::MixxxDB}, scene::{Scene, Scenery, StageDirection, conversation::ConversationEntry}};
use crate::{SaveData, artifacts::{self, Album, Artifact, Artist, Merge, SourceID, Track, bandcamp::BandcampQueryArgs, beets::BeatsQueryArgs, mixxx::MixxxDB, musicbrainz::{MusicbrainzQueryArgs, search_artifacts}}, scene::{Scene, Scenery, StageDirection, conversation::ConversationEntry}};
const SYSTEM_PROMPT: &str = include_str!("system-prompt.txt");
@@ -94,33 +94,35 @@ impl Session {
StageEvent::StageDirection(text) => ConversationEntry::StageDirection(text)
};
ToolResults {
result: Some(msg.to_string()),
messages: vec![msg],
..Default::default()
}
}
async fn tool_bandcamp_scan(&mut self, args: BandcampQueryArgs) -> ToolResults {
let mut messages = vec![];
log::info!("Fetching artifacts from Bandcamp with {:?}", args);
log::debug!("Fetching artifacts from Bandcamp with {:?}", args);
let mut json_results = vec![];
if let Ok(results) = bandcamp::search(args.query.as_str()).await {
for result in results {
log::debug!("Result: {:?}", result);
match result {
SearchResultItem::Artist(data) => {
let result = Artifact::Artist(Artist {
/*let result = Artifact::Artist(Artist {
name: data.name,
location: data.location,
..Default::default()
});
});*/
let result = bandcamp::fetch_artist(data.artist_id).await.unwrap().into();
json_results.push(result);
},
SearchResultItem::Album(data) => {
let result = Artifact::Album(Album {
let result = bandcamp::fetch_album(data.band_id, data.album_id).await.unwrap().into();
/*let result = Artifact::Album(Album {
title: data.name,
artist: data.band_name,
..Default::default()
});
});*/
json_results.push(result);
},
SearchResultItem::Track(data) => {
@@ -128,6 +130,7 @@ impl Session {
title: data.name,
artist: Some(data.band_name),
album: data.album_name,
sources: HashSet::from([SourceID::Bandcamp(data.track_id)]),
..Default::default()
});
json_results.push(result);
@@ -137,15 +140,9 @@ impl Session {
}
}
let artifact_count = json_results.len();
messages.push(ConversationEntry::ShipComputer(format!("Relay scan for '{}' complete. {} artifacts added to the archive.", args.query, artifact_count).into()));
messages.push(ConversationEntry::ShipComputer(format!("Bandcamp relay scan for '{}' complete. {} artifacts added to the archive.", args.query, artifact_count).into()));
for track in &json_results {
if let Some(merge_target) = self.scenery.artifacts.iter_mut().find(|a| { *a == track }) {
merge_target.merge(track.clone());
} else {
self.scenery.artifacts.push(track.clone());
}
}
self.scenery.artifacts.merge(json_results);
ToolResults {
result: Some(format!("{} artifacts were added to the archive.", artifact_count)),
@@ -155,16 +152,10 @@ impl Session {
async fn tool_artifact_query(&mut self, args: BeatsQueryArgs) -> ToolResults {
let mut messages = vec![];
messages.push(ConversationEntry::ShipComputer(format!("Executing archive query {:?}", args)));
log::info!("Executing beets query {:?}", args);
if let Ok(output) = args.execute() {
for track in &output {
if let Some(merge_target) = self.scenery.artifacts.iter_mut().find(|a| { *a == track }) {
merge_target.merge(track.clone());
} else {
self.scenery.artifacts.push(track.clone());
}
}
log::debug!("Executing beets query {:?}", args);
if let Ok(output) = args.clone().execute() {
messages.push(ConversationEntry::ShipComputer(format!("Found {} artifacts with archive query {:?}", output.len(), args)));
self.scenery.artifacts.merge(output);
} else {
messages.push(ConversationEntry::ShipComputer("Unable to execute query!".into()));
};
@@ -175,24 +166,34 @@ impl Session {
}
}
async fn tool_musicbrainz_fetch_tracks(&mut self, args: MusicbrainzQueryArgs) -> ToolResults {
log::debug!("Executing musicbrainz fetch for {:?}", args);
let results = search_artifacts(args).await.unwrap();
let msg = format!("Found {} results via Musicbrainz relay search.", results.len());
self.scenery.artifacts.merge(results);
ToolResults {
result: Some(msg.clone()),
messages: vec![ConversationEntry::ShipComputer(msg)]
}
}
fn generate_conversation(&self, direction: &StageDirection) -> Vec<ChatCompletionRequestMessage> {
let mut json_buf = vec![];
let mut ser = Serializer::with_formatter(&mut json_buf, CompactFormatter);
direction.serialize(&mut ser).unwrap();
serde_json::json!({
"direction": direction,
"scenery": self.scenery
}).serialize(&mut ser).unwrap();
let direction_message: ChatCompletionRequestMessage = ChatCompletionRequestSystemMessageArgs::default()
.content(String::from_utf8(json_buf).unwrap())
.build().unwrap().into();
let mut json_buf = vec![];
let mut ser = Serializer::with_formatter(&mut json_buf, CompactFormatter);
self.scenery.serialize(&mut ser).unwrap();
let scenery_message: ChatCompletionRequestMessage = ChatCompletionRequestSystemMessageArgs::default()
.content(String::from_utf8(json_buf).unwrap())
.build().unwrap().into();
let mut full_conversation = vec![
self.header_message.clone(),
direction_message,
scenery_message,
];
full_conversation.append(&mut self.messages.clone());
@@ -231,9 +232,18 @@ impl Session {
.description("Scans Bandcamp to find artifacts to use in the scene that match the given search parameters. To find an artist, provide only the artist name. To find an album, provide the artist and the album.")
.parameters(schema_for!(BandcampQueryArgs))
.build().unwrap()
}),
ChatCompletionTools::Function(ChatCompletionTool {
function: FunctionObjectArgs::default()
.name("musicbrainz_track_search")
.description("Fetches metadata from bandcamp for the given musicbrainz recording IDs (mbid)")
.parameters(schema_for!(MusicbrainzQueryArgs))
.build().unwrap()
})
// TODO: We should be able to have eva update lore memories with a function call, and this lore is somehow fed into the show? but only the relevant bits? or maybe eva even queries it directly
// TODO: The memory should also be able to remember facts about artists, albums, tracks we've had in the past, and those could be pulled up when there are hits in the playlist.
];
log::info!("Sending request..");
log::debug!("Sending request..");
let request = CreateChatCompletionRequestArgs::default()
.messages(full_conversation)
.model("gpt-5.4-mini")
@@ -255,7 +265,7 @@ impl Session {
if let Some(usage) = response.usage {
self.tokens_consumed += usage.total_tokens as usize;
log::info!("{} tokens cast into the void", usage.total_tokens);
log::debug!("{} tokens cast into the void", usage.total_tokens);
}
if let Some(message) = response.choices.first() {
@@ -287,6 +297,7 @@ impl Session {
"log_stage_event" => self.tool_stage_event(serde_json::from_str(args).unwrap()).await,
"bandcamp_artifact_scan" => self.tool_bandcamp_scan(serde_json::from_str(args).unwrap()).await,
"archive_query" => self.tool_artifact_query(serde_json::from_str(args).unwrap()).await,
"musicbrainz_track_search" => self.tool_musicbrainz_fetch_tracks(serde_json::from_str(args).unwrap()).await,
_ => unreachable!()
};
results.push((&call.id, tool_result));