Files
wehub-resource-sync 426e9eeabd
Voice Workbench / headless workbench (mocked backends) (push) Has been cancelled
Voice Workbench / real acoustic lane (nightly, provisioned only) (push) Has been cancelled
ci / test (push) Has been cancelled
ci / lint-and-format (push) Has been cancelled
ci / build (push) Has been cancelled
ci / dev-startup (push) Has been cancelled
gitleaks / gitleaks (push) Has been cancelled
Markdown Links / Relative Markdown Links (push) Has been cancelled
Quality (Extended) / Homepage Build (PR smoke) (push) Has been cancelled
Quality (Extended) / Comment-only diff guard (push) Has been cancelled
Quality (Extended) / Format + Type Safety Ratchet (push) Has been cancelled
Quality (Extended) / Develop Gate (secret scan + UI determinism) (push) Has been cancelled
Quality (Extended) / Develop Gate (lint) (push) Has been cancelled
Chat shell gestures / Chat shell gesture + parity e2e (push) Has been cancelled
Cloud Gateway Discord / Test (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx @biomejs/biome check packages/lifeops-bench/src, benchmark-lint) (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx vitest run --config packages/lifeops-bench/vitest.config.ts --root packages/lifeops-bench --passWithNoTests, benchmark-tests) (push) Has been cancelled
Build Agent Image / build-and-push (push) Has been cancelled
Dev Smoke / bun run dev onboarding chat (push) Has been cancelled
Dev Smoke / Vite HMR dependency-level smoke (push) Has been cancelled
Electrobun Submodule Guard / electrobun gitlink is fetchable (push) Has been cancelled
Publish @elizaos/example-code / check_npm (push) Has been cancelled
Publish @elizaos/example-code / publish_npm (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / verify_version (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / publish_npm (push) Has been cancelled
Sandbox Live Smoke / Sandbox live smoke (push) Has been cancelled
Snap Build & Test / Build Snap (amd64) (push) Has been cancelled
Snap Build & Test / Build Snap (arm64) (push) Has been cancelled
Test Packaging / elizaos CLI global-install smoke (node + bun) (push) Has been cancelled
Cloud Gateway Webhook / Test (push) Has been cancelled
Cloud Tests / lint-and-types (push) Has been cancelled
Cloud Tests / unit-tests (push) Has been cancelled
Cloud Tests / integration-tests (push) Has been cancelled
Cloud Tests / e2e-tests (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Apps Worker (Product 2) / Determine environment (push) Has been cancelled
Deploy Apps Worker (Product 2) / Deploy apps worker to apps-control host (${{ needs.determine-env.outputs.environment }}) (push) Has been cancelled
Deploy Eliza Provisioning Worker / Determine environment (push) Has been cancelled
Deploy Eliza Provisioning Worker / Deploy worker to Hetzner host (${{ needs.determine-env.outputs.environment }} @ ${{ needs.determine-env.outputs.deployment_sha }}) (push) Has been cancelled
Dev Smoke / Classify changed paths (push) Has been cancelled
supply-chain / sbom (push) Has been cancelled
supply-chain / vulnerability-scan (push) Has been cancelled
Build, Push & Deploy to Phala Cloud / build-and-push (push) Has been cancelled
Test Packaging / Validate Packaging Configs (push) Has been cancelled
Test Packaging / Build & Test PyPI Package (push) Has been cancelled
Test Packaging / PyPI on Python ${{ matrix.python }} (push) Has been cancelled
Test Packaging / Pack & Test JS Tarballs (push) Has been cancelled
UI Fixture E2E / ui-fixture-e2e (push) Has been cancelled
UI Fixture E2E / fixture-e2e (push) Has been cancelled
UI Story Gate / story-gate (push) Has been cancelled
vault-ci / test (macos-latest) (push) Has been cancelled
vault-ci / test (ubuntu-latest) (push) Has been cancelled
vault-ci / test (windows-latest) (push) Has been cancelled
vault-ci / app-core wiring tests (push) Has been cancelled
verify-patches / verify patches/CHECKSUMS.sha256 (push) Has been cancelled
Voice Benchmark Smoke / voice-emotion fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voiceagentbench fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench-quality unit smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench TypeScript unit (no audio) (push) Has been cancelled
Voice Benchmark Smoke / voice bench smoke summary (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/app-core test bun run --cwd packages/elizaos test bun run --cwd packages/cloud/shared test], app-and-cli) (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/scenario-runner test bun run --cwd packages/vault test bun run --cwd packages/security test bun run --cwd plugins/plugin-coding-tools test], framework-packages) (push) Has been cancelled
Windows CI / windows ([bun run --cwd plugins/plugin-elizacloud test bun run --cwd plugins/plugin-discord test bun run --cwd plugins/plugin-anthropic test bun run --cwd plugins/plugin-openai test bun run --cwd plugins/plugin-app-control test bun run --cwd plugins/pl… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run build --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/agent --concurrency=4 node packages/scripts/run-bash-linux-only.mjs scripts/verify-riscv64-buildpaths.sh node packages/scripts/run… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run typecheck --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/cloud-shared --concurrency=4 bun run --cwd packages/core test bun run --cwd packages/shared test], core-runtime, 75) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:43:05 +08:00

874 lines
24 KiB
TypeScript

/**
* Smart query playback action for research-backed music requests.
*
* It turns structured or media-routed requests into library and web-search
* lookups before queueing the selected track.
*/
import {
type ActionExample,
type ActionResult,
getActiveRoutingContextsForTurn,
type HandlerCallback,
type IAgentRuntime,
logger,
type Memory,
ModelType,
type State,
} from "@elizaos/core";
import type { MusicLibraryService } from "../services/musicLibraryService";
import { parseJsonObjectResponse } from "../utils/json";
import { mergedOptions, requireMusicConfirmation } from "./confirmation";
interface MusicQueryIntent {
needsResearch: boolean;
queryType:
| "first_single"
| "latest_song"
| "similar_artist"
| "debut_album"
| "popular_song"
| "era"
| "decade"
| "year"
| "genre"
| "mood"
| "vibe"
| "activity"
| "workout"
| "study"
| "party"
| "chill"
| "chart"
| "top_hits"
| "trending"
| "album"
| "album_track"
| "full_album"
| "movie_soundtrack"
| "game_soundtrack"
| "tv_theme"
| "lyrics_based"
| "topic"
| "cover"
| "remix"
| "acoustic"
| "live"
| "specific_track"
| "nth_album"
| "direct_search";
artist?: string;
album?: string;
song?: string;
genre?: string;
mood?: string;
decade?: string;
year?: string;
keywords?: string;
searchQuery?: string;
modifier?: "cover" | "remix" | "acoustic" | "live" | "instrumental";
}
interface SearchResultSnippet {
description?: string;
snippet?: string;
}
interface WebSearchService {
search(query: string): Promise<SearchResultSnippet[]>;
}
interface MusicQueueService {
addTrack(
guildId: string,
track: {
url: string;
title: string;
duration?: number;
requestedBy: Memory["entityId"];
},
): Promise<void>;
}
function getModelText(response: unknown): string | null {
return typeof response === "string" ? response : null;
}
function summarizeSearchResults(results: SearchResultSnippet[]): string {
return results
.slice(0, 3)
.map((result) => result.description || result.snippet || "")
.join("\n");
}
function formatPromptValue(value: unknown, depth = 0): string {
if (value == null) return "";
if (typeof value === "string") return value.replace(/\s+/g, " ").trim();
if (typeof value === "number" || typeof value === "boolean") {
return String(value);
}
if (Array.isArray(value)) {
return value
.filter((item) => item != null)
.slice(0, 20)
.map((item) => {
const rendered = formatPromptValue(item, depth + 1);
return rendered ? `${" ".repeat(depth)}- ${rendered}` : "";
})
.filter(Boolean)
.join("\n");
}
if (typeof value === "object") {
return Object.entries(value as Record<string, unknown>)
.filter(([, entry]) => entry != null && entry !== "")
.slice(0, 20)
.map(([key, entry]) => {
const rendered = formatPromptValue(entry, depth + 1);
if (!rendered) return "";
if (rendered.includes("\n")) {
return `${" ".repeat(depth)}${key}:\n${rendered}`;
}
return `${" ".repeat(depth)}${key}: ${rendered}`;
})
.filter(Boolean)
.join("\n");
}
return String(value);
}
function readMusicQueryText(
message: Memory,
options?: Record<string, unknown>,
): string {
const merged = mergedOptions(options);
const direct = merged.query ?? merged.searchQuery;
if (typeof direct === "string" && direct.trim().length >= 3) {
return direct.trim();
}
return message.content.text || "";
}
/**
* Use LLM to understand the user's music query intent
*/
const analyzeMusicQuery = async (
runtime: IAgentRuntime,
messageText: string,
): Promise<MusicQueryIntent | null> => {
try {
const prompt = `Analyze this music-related request and extract the intent. Be comprehensive - this could be any type of music query.
Message: "${messageText}"
Determine:
1. Does this need research (Wikipedia/music databases) or can it be directly searched on YouTube?
2. What type of query is this? Choose from:
ARTIST-SPECIFIC:
- "first_single": First/debut single of an artist
- "latest_song": Most recent song
- "similar_artist": Similar/related artists
- "debut_album": Songs from debut album
- "popular_song": Popular/hit song from artist
- "nth_album": Specific album by number (2nd album, third album, etc)
TEMPORAL:
- "era": Music from an era (80s, 90s, 2000s, etc)
- "decade": Music from a decade
- "year": Music from a specific year
GENRE/MOOD/VIBE:
- "genre": Specific genre (jazz, rock, hip hop, etc)
- "mood": Mood-based (sad, happy, angry, etc)
- "vibe": Vibe-based (chill, energetic, dark, uplifting, etc)
ACTIVITY:
- "activity": General activity music
- "workout": Workout/gym music
- "study": Study/focus music
- "party": Party music
- "chill": Chill/relaxing music
CHARTS/POPULARITY:
- "chart": Chart hits (Billboard, etc)
- "top_hits": Top hits
- "trending": Viral/trending songs
ALBUM:
- "album": Play from an album
- "album_track": Specific track from album
- "full_album": Play entire album
MEDIA:
- "movie_soundtrack": From a movie
- "game_soundtrack": From a video game
- "tv_theme": TV show theme
LYRICS/TOPIC:
- "lyrics_based": Based on lyrics or themes
- "topic": Songs about a topic
VERSIONS:
- "cover": Cover version
- "remix": Remix
- "acoustic": Acoustic version
- "live": Live performance
SPECIFIC:
- "specific_track": Track by number (track 3, etc)
- "direct_search": Can search directly
3. Extract relevant details:
- artist: Artist name if mentioned
- album: Album name if mentioned
- song: Song name if mentioned
- genre: Genre if mentioned
- mood: Mood if mentioned (happy, sad, energetic, chill, etc)
- decade: Decade if mentioned (80s, 90s, 2000s, etc)
- year: Specific year if mentioned
- keywords: Other important keywords
- modifier: If asking for specific version (cover, remix, acoustic, live, instrumental)
Respond with JSON only:
{
"needsResearch": true,
"queryType": "direct_search",
"artist": "artist name if mentioned",
"album": "album name if mentioned",
"song": "song name if mentioned",
"genre": "genre if mentioned",
"mood": "mood if mentioned",
"decade": "decade if mentioned",
"year": "year if mentioned",
"keywords": "other important keywords",
"modifier": "cover|remix|acoustic|live|instrumental if requested",
"searchQuery": "query to use for direct_search"
}`;
const rawResponse = await runtime.useModel(ModelType.TEXT_SMALL, {
prompt,
});
const response = getModelText(rawResponse);
if (!response) {
return null;
}
const parsedJson =
parseJsonObjectResponse<Record<string, unknown>>(response);
if (parsedJson?.queryType) {
return {
...parsedJson,
needsResearch:
parsedJson.needsResearch === true ||
String(parsedJson.needsResearch).toLowerCase() === "true",
} as MusicQueryIntent;
}
return null;
} catch (error) {
logger.error(
"Error analyzing music query:",
error instanceof Error ? error.message : String(error),
);
return null;
}
};
/**
* Research music information using Wikipedia and music services
*/
const researchMusicInfo = async (
runtime: IAgentRuntime,
intent: MusicQueryIntent,
): Promise<string | null> => {
try {
const musicLibrary = runtime.getService(
"musicLibrary",
) as MusicLibraryService | null;
const webSearchService = runtime.getService(
"webSearch",
) as WebSearchService | null;
logger.debug(
`Researching music info: ${intent.queryType} for ${intent.artist || intent.genre || intent.mood || "query"}`,
);
let searchQuery: string | null = null;
switch (intent.queryType) {
case "first_single":
case "debut_album": {
if (!intent.artist) break;
// Try to get artist info from Wikipedia
if (musicLibrary?.getWikipediaArtistInfo) {
const artistInfo = (await musicLibrary.getWikipediaArtistInfo(
intent.artist,
)) as { discography?: unknown; similarArtists?: string[] } | null;
if (artistInfo?.discography) {
// Use LLM to extract first single/album from discography
const prompt = `From this artist discography, what was their first ${intent.queryType === "first_single" ? "single" : "album"}?
Discography:
${formatPromptValue(artistInfo.discography).substring(0, 2000)}
Respond with ONLY the song/album name, nothing else.`;
const firstRelease = getModelText(
await runtime.useModel(ModelType.TEXT_SMALL, { prompt }),
);
if (firstRelease) {
searchQuery = `${intent.artist} ${firstRelease.trim()}`;
}
}
}
// Fallback: use web search
if (!searchQuery && webSearchService) {
const searchResults = await webSearchService.search(
`${intent.artist} ${intent.queryType === "first_single" ? "first single debut" : "debut album first album"}`,
);
if (searchResults && searchResults.length > 0) {
const prompt = `From these search results, what was ${intent.artist}'s ${intent.queryType === "first_single" ? "first single" : "debut album"}?
Results: ${summarizeSearchResults(searchResults)}
Respond with ONLY the song/album name, nothing else.`;
const answer = getModelText(
await runtime.useModel(ModelType.TEXT_SMALL, { prompt }),
);
if (answer) {
searchQuery = `${intent.artist} ${answer.trim()}`;
}
}
}
break;
}
case "nth_album": {
if (!intent.artist || !intent.keywords) break;
// Extract album number from keywords (second, third, 2nd, 3rd, etc)
const numberMatch = intent.keywords.match(
/(\d+)(?:st|nd|rd|th)|second|third|fourth|fifth/i,
);
if (!numberMatch) break;
if (webSearchService) {
const searchResults = await webSearchService.search(
`${intent.artist} ${intent.keywords} album discography`,
);
if (searchResults && searchResults.length > 0) {
const prompt = `From these search results, what was ${intent.artist}'s ${intent.keywords} album?
Results: ${summarizeSearchResults(searchResults)}
Respond with ONLY the album name, nothing else.`;
const answer = getModelText(
await runtime.useModel(ModelType.TEXT_SMALL, { prompt }),
);
if (answer) {
searchQuery = `${intent.artist} ${answer.trim()}`;
}
}
}
break;
}
case "similar_artist": {
if (!intent.artist) break;
// Try to get similar artists from Wikipedia
if (musicLibrary?.getWikipediaArtistInfo) {
const artistInfo = await musicLibrary.getWikipediaArtistInfo(
intent.artist,
);
if (
artistInfo?.similarArtists &&
artistInfo.similarArtists.length > 0
) {
const similar =
artistInfo.similarArtists[
Math.floor(Math.random() * artistInfo.similarArtists.length)
];
searchQuery = `${similar} popular song`;
logger.info(`Found similar artist: ${similar}`);
}
}
// Fallback: use canonical music metadata
if (!searchQuery && musicLibrary?.getArtistInfo) {
const artistInfo = await musicLibrary.getArtistInfo(intent.artist);
if (
artistInfo?.similarArtists &&
artistInfo.similarArtists.length > 0
) {
const similar = artistInfo.similarArtists[0];
searchQuery = `${similar} popular song`;
logger.info(`Found similar artist: ${similar}`);
}
}
break;
}
case "latest_song": {
if (!intent.artist) break;
searchQuery = `${intent.artist} latest song new ${new Date().getFullYear()}`;
break;
}
case "popular_song": {
if (!intent.artist) break;
searchQuery = `${intent.artist} most popular song hit`;
break;
}
case "movie_soundtrack":
case "game_soundtrack":
case "tv_theme": {
if (!intent.keywords) break;
const mediaType = intent.queryType
.replace("_soundtrack", "")
.replace("_theme", "");
searchQuery = `${intent.keywords} ${mediaType} ${intent.queryType.includes("theme") ? "theme" : "soundtrack"}`;
break;
}
case "era":
case "decade": {
const timeKeyword = intent.decade || intent.year || intent.keywords;
if (!timeKeyword) break;
const genrePrefix = intent.genre ? `${intent.genre} ` : "";
searchQuery = `${genrePrefix}${timeKeyword} hits popular songs`;
break;
}
case "genre": {
if (!intent.genre && !intent.keywords) break;
const genre = intent.genre || intent.keywords;
searchQuery = `${genre} music popular`;
break;
}
case "mood":
case "vibe": {
const mood = intent.mood || intent.keywords;
if (!mood) break;
searchQuery = `${mood} music songs`;
break;
}
case "workout":
case "study":
case "party":
case "chill":
case "activity": {
const activity =
intent.queryType === "activity" ? intent.keywords : intent.queryType;
searchQuery = `${activity} music playlist`;
break;
}
case "chart":
case "top_hits":
case "trending": {
const chartType = intent.keywords || intent.queryType;
searchQuery = `${chartType} ${new Date().getFullYear()} popular songs`;
break;
}
case "lyrics_based":
case "topic": {
if (!intent.keywords) break;
searchQuery = `songs about ${intent.keywords}`;
break;
}
case "album_track":
case "specific_track": {
if (!intent.album && !intent.artist) break;
const trackInfo = intent.keywords || "";
searchQuery =
`${intent.artist || ""} ${intent.album || ""} ${trackInfo}`.trim();
break;
}
case "full_album": {
if (!intent.album && !intent.artist) break;
searchQuery = `${intent.artist || ""} ${intent.album || ""} full album`;
break;
}
}
// Apply modifier if specified (cover, remix, acoustic, live)
if (searchQuery && intent.modifier) {
searchQuery = `${searchQuery} ${intent.modifier}`;
}
return searchQuery;
} catch (error) {
logger.error(
"Error researching music info:",
error instanceof Error ? error.message : String(error),
);
return null;
}
};
/**
* Smart music query action that can research and play complex queries
*/
export const playMusicQuerySimiles = [
"PLAY_MUSIC_QUERY",
"SMART_PLAY",
"RESEARCH_AND_PLAY",
"FIND_AND_PLAY",
"INTELLIGENT_MUSIC_SEARCH",
];
const PLAY_MUSIC_QUERY_CONTEXTS = ["media", "knowledge"] as const;
/**
* Read a structured play-query value from the action parameters. `play_query`
* is the "research and play this request" op, so any of the descriptive music
* fields the planner may emit counts as a query to resolve.
*/
function readPlayMusicQueryValue(
options?: Record<string, unknown>,
): string | null {
const merged = mergedOptions(options);
const direct =
merged.query ??
merged.searchQuery ??
merged.song ??
merged.artist ??
merged.album ??
merged.genre ??
merged.mood ??
merged.keywords;
if (typeof direct === "string" && direct.trim().length > 0) {
return direct.trim();
}
return null;
}
/**
* True when the turn was routed to a music/knowledge context. This mirrors the
* other music-library sub-handlers: op selection follows the planner's routing
* decision, never an English keyword match on raw message text.
*/
function hasPlayMusicQueryContext(message: Memory, state?: State): boolean {
const active = new Set(
getActiveRoutingContextsForTurn(state, message).map((context) =>
`${context}`.toLowerCase(),
),
);
const collect = (value: unknown) => {
if (!Array.isArray(value)) return;
for (const item of value) {
if (typeof item === "string") active.add(item.toLowerCase());
}
};
collect(
(state?.values as Record<string, unknown> | undefined)?.selectedContexts,
);
collect(
(state?.data as Record<string, unknown> | undefined)?.selectedContexts,
);
return PLAY_MUSIC_QUERY_CONTEXTS.some((context) => active.has(context));
}
/**
* A direct YouTube URL is a machine token, not English intent. The faster
* `playYouTubeAudio` path handles it, so `play_query` defers when one is
* present.
*/
function isYouTubeUrl(value: string): boolean {
return value.includes("youtube.com/") || value.includes("youtu.be/");
}
export async function validatePlayMusicQuery(
_runtime: IAgentRuntime,
message: Memory,
state?: State,
options?: Record<string, unknown>,
): Promise<boolean> {
if (message.content.source !== "discord") {
return false;
}
const structuredQuery = readPlayMusicQueryValue(options);
const urlCandidate = structuredQuery ?? message.content.text ?? "";
if (isYouTubeUrl(urlCandidate)) {
return false;
}
// `play_query` is selected when a structured query is present or when the turn
// was routed to a music/knowledge context. The handler's LLM query analysis
// (`analyzeMusicQuery`) decides research vs. direct search — intent is never
// re-derived here from English keywords in raw message text.
return Boolean(structuredQuery) || hasPlayMusicQueryContext(message, state);
}
export async function handlePlayMusicQuery(
runtime: IAgentRuntime,
message: Memory,
state: State | undefined,
options: Record<string, unknown> | undefined,
callback?: HandlerCallback,
): Promise<ActionResult | undefined> {
if (!callback) return { success: false, error: "Missing callback" };
const messageText = readMusicQueryText(message, options);
const preview = `Confirmation required before resolving and queueing music for: "${messageText}".`;
const confirmBlock = await requireMusicConfirmation({
runtime,
message,
actionName: "PLAY_MUSIC_QUERY",
pendingKey: `play_query:${messageText.slice(0, 160)}`,
preview,
callback,
});
if (confirmBlock) return confirmBlock;
try {
// Step 1: Analyze the query intent
await callback({
text: "🔍 Let me figure out what you want...",
source: message.content.source,
});
const intent = await analyzeMusicQuery(runtime, messageText);
if (!intent) {
await callback({
text: "I couldn't understand your music request. Try being more specific?",
source: message.content.source,
});
return;
}
logger.info(`Music query intent: ${JSON.stringify(intent)}`);
let finalSearchQuery: string | null = null;
// Step 2: Research if needed
if (intent.needsResearch && intent.queryType !== "direct_search") {
const researchResult = await researchMusicInfo(runtime, intent);
if (!researchResult) {
await callback({
text: "I couldn't resolve that music query from the available research services. Try a more direct song, artist, or album request.",
source: message.content.source,
});
return;
}
finalSearchQuery = researchResult;
} else {
// For direct searches, construct query from intent
if (intent.searchQuery) {
finalSearchQuery = intent.searchQuery;
} else {
const parts = [
intent.artist,
intent.song,
intent.album,
intent.genre,
intent.mood,
intent.keywords,
].filter(Boolean);
finalSearchQuery = parts.length > 0 ? parts.join(" ") : messageText;
if (intent.modifier) {
finalSearchQuery = `${finalSearchQuery} ${intent.modifier}`;
}
}
}
if (!finalSearchQuery) {
await callback({
text: "I couldn't figure out what to search for. Can you rephrase your request?",
source: message.content.source,
});
return;
}
logger.info(`Final search query: ${finalSearchQuery}`);
// Step 3: Search YouTube for the track
const musicLibrary = runtime.getService(
"musicLibrary",
) as MusicLibraryService | null;
if (!musicLibrary) {
await callback({
text: "YouTube search service is not available.",
source: message.content.source,
});
return;
}
const results = await musicLibrary.searchYouTube(finalSearchQuery, {
limit: 1,
});
if (!results || results.length === 0) {
await callback({
text: `I couldn't find anything matching "${finalSearchQuery}". Try being more specific?`,
source: message.content.source,
});
return;
}
const topResult = results[0];
logger.info(`Found: ${topResult.title} (${topResult.url})`);
// Step 4: Queue the track via music service
const musicService = runtime.getService(
"music",
) as MusicQueueService | null;
if (!musicService) {
await callback({
text: "Music service is not available.",
source: message.content.source,
});
return;
}
// Get Discord guild ID from room - same pattern as playAudio action
const room = state?.data?.room || (await runtime.getRoom(message.roomId));
const guildId = room?.serverId;
if (!guildId) {
await callback({
text: "Could not determine Discord server. Make sure you're messaging from a server channel.",
source: message.content.source,
});
return;
}
// Use entityId (UUID) not fromId (Discord snowflake) for requestedBy
// WHY: fromId in metadata is the raw Discord snowflake ID for security reference
// entityId is the proper UUID created by createUniqueUuid(runtime, discordId)
const requestEntityId = message.entityId;
await musicService.addTrack(guildId, {
url: topResult.url,
title: topResult.title,
duration: topResult.duration,
requestedBy: requestEntityId,
});
await callback({
text: `🎵 Queued: **${topResult.title}**`,
source: message.content.source,
});
return { success: true, text: `Queued: ${topResult.title}` };
} catch (error) {
logger.error(
"Error in playMusicQuery:",
error instanceof Error ? error.message : String(error),
);
await callback({
text: "I ran into an issue trying to find that music.",
source: message.content.source,
});
return {
success: false,
error: error instanceof Error ? error.message : String(error),
};
}
}
export const playMusicQueryExamples: ActionExample[][] = [
[
{
name: "{{name1}}",
content: {
text: "Play the strokes first single",
},
},
{
name: "{{name2}}",
content: {
text: "Let me look that up!",
actions: ["MUSIC_LIBRARY"],
},
},
],
[
{
name: "{{name1}}",
content: {
text: "Play something like radiohead",
},
},
{
name: "{{name2}}",
content: {
text: "I'll find a similar artist!",
actions: ["MUSIC_LIBRARY"],
},
},
],
[
{
name: "{{name1}}",
content: {
text: "Play some 80s synth pop",
},
},
{
name: "{{name2}}",
content: {
text: "Finding 80s synth pop for you!",
actions: ["MUSIC_LIBRARY"],
},
},
],
[
{
name: "{{name1}}",
content: {
text: "Play workout music",
},
},
{
name: "{{name2}}",
content: {
text: "Let's get you pumped up!",
actions: ["MUSIC_LIBRARY"],
},
},
],
[
{
name: "{{name1}}",
content: {
text: "Play a cover of wonderwall",
},
},
{
name: "{{name2}}",
content: {
text: "Looking for a cover version!",
actions: ["MUSIC_LIBRARY"],
},
},
],
[
{
name: "{{name1}}",
content: {
text: "Play the Inception soundtrack",
},
},
{
name: "{{name2}}",
content: {
text: "Finding that soundtrack!",
actions: ["MUSIC_LIBRARY"],
},
},
],
];