From 718f6cb1bd2628c239c09f72df4a0485a8f39303 Mon Sep 17 00:00:00 2001 From: Matt Lindsey Date: Fri, 2 Oct 2026 17:51:20 -0400 Subject: [PATCH] Autotitle conversations whose reply is wrapped in a markdown json fence Claude via RubyLLM answers the title prompt with ```json ... ``` despite the JSON-only instruction, so JSON.parse failed and the title stayed blank. Co-Authored-By: Claude Opus 5.5 --- app/jobs/autotitle_conversation_job.rb | 8 +++++++- test/jobs/autotitle_conversation_job_test.rb | 11 +++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/app/jobs/autotitle_conversation_job.rb b/app/jobs/autotitle_conversation_job.rb index 5d35dd80..62003eb4 100644 --- a/app/jobs/autotitle_conversation_job.rb +++ b/app/jobs/autotitle_conversation_job.rb @@ -50,7 +50,7 @@ def usable_topic(response) return nil end - document = JSON.parse(response) + document = JSON.parse(unfenced(response)) topic = document.is_a?(Hash) ? document["topic"] : nil if topic.is_a?(String) && topic.present? @@ -64,6 +64,12 @@ def usable_topic(response) nil end + def unfenced(text) + return text unless text.is_a?(String) + + text[/```(?:json)?\s*(.*?)\s*```/m, 1] || text + end + def system_message <<~END You extract a 2-4 word topic from text. I will give the text of a chat. You reply with the topic of this chat, diff --git a/test/jobs/autotitle_conversation_job_test.rb b/test/jobs/autotitle_conversation_job_test.rb index 16acefe5..c5ed16f7 100644 --- a/test/jobs/autotitle_conversation_job_test.rb +++ b/test/jobs/autotitle_conversation_job_test.rb @@ -35,6 +35,17 @@ class AutotitleConversationJobTest < ActiveJob::TestCase assert_equal "Claude Summary", conversation.reload.title end + test "a topic wrapped in a markdown json fence is still used" do + conversation = conversations(:hello_claude) + conversation.update!(title: nil) + + TestClient::Anthropic.stub :text, "```json\n{ \"topic\": \"Vermont's capital\" }\n```" do + AutotitleConversationJob.perform_now(conversation.id) + end + + assert_equal "Vermont's capital", conversation.reload.title, "The fence should be stripped before parsing the JSON" + end + test "gemini conversations are titled through the same intent path" do conversation = conversations(:gemini_conversation) conversation.update!(title: nil)