fix: read_audio fails when LLM passes literal <media:audio> as media_id (#156)

The read_audio tool errors with "audio not found" because:
1. BuildMediaTags() creates bare <media:audio> tags without IDs
2. persistMedia() generates UUIDs stored in context, not in message content
3. LLM sees <media:audio> and passes it literally as media_id parameter
4. resolveAudioFile() tries to match "<media:audio>" against UUIDs — fails

Two-part fix:
- enrichAudioIDs(): embed persisted UUIDs into <media:audio> tags (like
  enrichDocumentPaths does for documents), so LLM sees actual IDs
- resolveAudioFile(): sanitize tag-like media_id values and fallback to
  most recent audio instead of hard error on unmatched IDs

Co-authored-by: Luvu182 <208665161+Luvu182@users.noreply.github.com>
This commit is contained in:
Luan VuandLuvu182 authored and GitHub committed 2026-03-11 19:57:15 +07:00
1 parent b5b40e7d94
commit 7386fc8ad7
3 files changed
+54 -1

No files matched your search

+2
View File
@@ -259,6 +259,8 @@ func (l *Loop) runLoop(ctx context.Context, req RunRequest) (*RunResult, error)
}
if len(audioRefs) > 0 {
ctx = tools.WithMediaAudioRefs(ctx, audioRefs)
// Embed media IDs into <media:audio> tags so LLM can reference them.
l.enrichAudioIDs(messages, mediaRefs)
}
// 2d. Collect video MediaRefs (historical + current) for read_video tool.
+41
View File
@@ -163,6 +163,47 @@ func (l *Loop) enrichDocumentPaths(messages []providers.Message, refs []provider
messages[lastIdx].Content = content
}
// enrichAudioIDs updates the last user message to embed persisted media IDs
// in <media:audio> and <media:voice> tags so the LLM can reference them.
// Without this, the LLM sees plain <media:audio> and cannot pass a valid media_id.
func (l *Loop) enrichAudioIDs(messages []providers.Message, refs []providers.MediaRef) {
if len(messages) == 0 {
return
}
lastIdx := -1
for i := len(messages) - 1; i >= 0; i-- {
if messages[i].Role == "user" {
lastIdx = i
break
}
}
if lastIdx < 0 {
return
}
content := messages[lastIdx].Content
for _, ref := range refs {
if ref.Kind != "audio" {
continue
}
idAttr := fmt.Sprintf(" id=%q", ref.ID)
// Replace bare <media:audio> with <media:audio id="uuid">
bare := "<media:audio>"
if strings.Contains(content, bare) {
content = strings.Replace(content, bare, "<media:audio"+idAttr+">", 1)
continue
}
// Replace bare <media:voice> with <media:voice id="uuid">
bareVoice := "<media:voice>"
if strings.Contains(content, bareVoice) {
content = strings.Replace(content, bareVoice, "<media:voice"+idAttr+">", 1)
continue
}
}
messages[lastIdx].Content = content
}
// mediaKindFromMime returns the media kind ("image", "video", "audio", "document")
// based on MIME type prefix.
func mediaKindFromMime(mime string) string {
+11 -1
View File
@@ -23,6 +23,13 @@ func (t *ReadAudioTool) resolveAudioFile(ctx context.Context, mediaID string) (p
return "", "", fmt.Errorf("no audio files available in this conversation. The user may not have sent an audio file.")
}
// Sanitize media_id: LLM may pass the literal tag string (e.g. "<media:audio>")
// instead of a UUID. Treat tag-like values as empty to fall back to most recent.
if strings.Contains(mediaID, "<") || strings.Contains(mediaID, "media:") {
slog.Debug("read_audio: sanitizing tag-like media_id", "raw", mediaID)
mediaID = ""
}
var ref *providers.MediaRef
if mediaID != "" {
for i := range refs {
@@ -32,7 +39,10 @@ func (t *ReadAudioTool) resolveAudioFile(ctx context.Context, mediaID string) (p
}
}
if ref == nil {
return "", "", fmt.Errorf("audio with media_id %q not found in conversation", mediaID)
// Fallback to most recent audio instead of hard error,
// since LLM may generate invalid IDs.
slog.Warn("read_audio: media_id not found, falling back to most recent", "media_id", mediaID)
ref = &refs[len(refs)-1]
}
} else {
ref = &refs[len(refs)-1]