From 0d607590a19596db513066621b83c6e2f0f03a53 Mon Sep 17 00:00:00 2001 From: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Date: Sun, 19 Apr 2026 07:22:54 -0700 Subject: [PATCH 1/2] fix(backend): use user's name in conversation title instead of "user" (#6216) get_transcript_structure now takes uid and looks up the wearer's first name via get_user_name(), then threads it into the prompt's context message so the LLM refers to the user by name rather than "user" or "Speaker 0" in conversation titles and summaries. The user name lands in the dynamic context message so the static instructions prefix stays stable for prompt caching. Fixes #6216 --- ...ersation_transcript_structure_user_name.py | 117 ++++++++++++++++++ .../conversations/process_conversation.py | 2 + backend/utils/llm/conversation_processing.py | 18 ++- 3 files changed, 136 insertions(+), 1 deletion(-) create mode 100644 backend/tests/unit/test_conversation_transcript_structure_user_name.py diff --git a/backend/tests/unit/test_conversation_transcript_structure_user_name.py b/backend/tests/unit/test_conversation_transcript_structure_user_name.py new file mode 100644 index 0000000000..ff5bf30802 --- /dev/null +++ b/backend/tests/unit/test_conversation_transcript_structure_user_name.py @@ -0,0 +1,117 @@ +"""Tests for user name context injection in transcript structure prompt (#6216).""" + +import inspect +import re +import sys +from datetime import datetime, timezone +from unittest.mock import MagicMock + +# Mock modules that initialize clients at import time +sys.modules.setdefault("database._client", MagicMock()) +sys.modules.setdefault("firebase_admin", MagicMock()) +_mock_clients = MagicMock() +sys.modules.setdefault("utils.llm.clients", _mock_clients) + +from models.structured import Structured +from utils.llm import conversation_processing as cp + + +class _FakeChain: + def __init__(self, captured): + self.captured = captured + + def __or__(self, _other): + return self + + def invoke(self, values): + self.captured["invoke_values"] = values + return Structured() + + +class _FakePrompt: + def __init__(self, captured): + self.captured = captured + + def __or__(self, _other): + return _FakeChain(self.captured) + + +def _run_with_user_name(monkeypatch, user_name: str): + captured = {} + + def fake_from_messages(messages): + captured["messages"] = messages + return _FakePrompt(captured) + + monkeypatch.setattr(cp, "_build_conversation_context", lambda *_args, **_kwargs: "Transcript: ```hello```") + monkeypatch.setattr(cp, "get_user_name", lambda _uid: user_name) + monkeypatch.setattr(cp.ChatPromptTemplate, "from_messages", fake_from_messages) + monkeypatch.setattr(cp.llm_medium_experiment, "bind", lambda **_kwargs: object()) + + cp.get_transcript_structure( + transcript="Speaker 0: hello", + started_at=datetime(2026, 4, 1, 12, 0, tzinfo=timezone.utc), + language_code="en", + tz="UTC", + uid="user-123", + ) + + return captured + + +def test_get_transcript_structure_injects_named_user(monkeypatch): + captured = _run_with_user_name(monkeypatch, "Aarav") + + assert "messages" in captured + context_message = captured["messages"][1][1] + assert "{user_name}" in context_message + assert captured["invoke_values"]["user_name"] == "Aarav" + + +def test_get_transcript_structure_uses_default_user_name(monkeypatch): + captured = _run_with_user_name(monkeypatch, "The User") + assert captured["invoke_values"]["user_name"] == "The User" + + +def test_get_transcript_structure_supports_firestore_fallback_name(monkeypatch): + captured = _run_with_user_name(monkeypatch, "Priya") + assert captured["invoke_values"]["user_name"] == "Priya" + + +def test_get_transcript_structure_falls_back_when_get_user_name_raises(monkeypatch): + captured = {} + + def fake_from_messages(messages): + captured["messages"] = messages + return _FakePrompt(captured) + + def raising_get_user_name(_uid): + raise RuntimeError("redis down") + + monkeypatch.setattr(cp, "_build_conversation_context", lambda *_a, **_kw: "Transcript: ```hello```") + monkeypatch.setattr(cp, "get_user_name", raising_get_user_name) + monkeypatch.setattr(cp.ChatPromptTemplate, "from_messages", fake_from_messages) + monkeypatch.setattr(cp.llm_medium_experiment, "bind", lambda **_kwargs: object()) + + cp.get_transcript_structure( + transcript="Speaker 0: hello", + started_at=datetime(2026, 4, 1, 12, 0, tzinfo=timezone.utc), + language_code="en", + tz="UTC", + uid="user-123", + ) + + assert captured["invoke_values"]["user_name"] == "The User" + + +def test_user_name_context_is_not_in_static_instructions_prefix(): + source = inspect.getsource(cp.get_transcript_structure) + + instructions_match = re.search(r"instructions_text\\s*=\\s*'''(.*?)'''", source, re.DOTALL) + assert instructions_match, "Could not find instructions_text definition" + instructions_content = instructions_match.group(1) + assert "{user_name}" not in instructions_content + + context_match = re.search(r"context_message\\s*=", source) + assert context_match, "Could not find context_message definition" + assert "{user_name}" in source diff --git a/backend/utils/conversations/process_conversation.py b/backend/utils/conversations/process_conversation.py index e15486ee27..63af82c4d6 100644 --- a/backend/utils/conversations/process_conversation.py +++ b/backend/utils/conversations/process_conversation.py @@ -114,6 +114,7 @@ def _get_structured( conversation.started_at, language_code, tz, + uid, calendar_meeting_context=calendar_context, output_language_code=user_language, ) @@ -194,6 +195,7 @@ def _get_structured( conversation.started_at, language_code, tz, + uid, photos=conversation.photos, calendar_meeting_context=calendar_context, output_language_code=user_language, diff --git a/backend/utils/llm/conversation_processing.py b/backend/utils/llm/conversation_processing.py index 360c4c649a..28f5837b6d 100644 --- a/backend/utils/llm/conversation_processing.py +++ b/backend/utils/llm/conversation_processing.py @@ -5,6 +5,7 @@ from langchain_core.prompts import ChatPromptTemplate from pydantic import BaseModel, Field +from database.auth import get_user_name from models.app import App from models.calendar_context import CalendarMeetingContext from models.conversation import Conversation @@ -590,6 +591,7 @@ def get_transcript_structure( started_at: datetime, language_code: str, tz: str, + uid: str, photos: List[ConversationPhoto] = None, calendar_meeting_context: 'CalendarMeetingContext' = None, output_language_code: str = None, @@ -599,6 +601,11 @@ def get_transcript_structure( return Structured() # Should be caught by discard logic, but as a safeguard. response_language = output_language_code or language_code + try: + user_name = get_user_name(uid) + except Exception as e: + logger.warning(f'Failed to load user name for transcript structuring (uid={uid}): {e}') + user_name = 'The User' # First system message: task-specific instructions (static prefix enables cross-conversation caching) # NOTE: language instructions are in context_message (second message) to keep this prefix fully static. @@ -647,7 +654,15 @@ def get_transcript_structure( ).strip() # Second system message: conversation context (dynamic, per-conversation) - context_message = 'The content language is {language_code}. You MUST respond entirely in {response_language}.\n\nContent:\n{conversation_context}' + context_message = ( + "The content language is {language_code}. You MUST respond entirely in {response_language}.\n\n" + "USER CONTEXT:\n" + "The wearer/user's first name is {user_name}. When the transcript or summary would refer to the wearer " + 'generically (e.g., "the user", "User"), use their name instead. Do NOT assume a specific numbered ' + 'speaker (e.g., "Speaker 0") is always the wearer - infer from context; prefer calendar participant ' + 'names for other speakers when available.\n\n' + "Content:\n{conversation_context}" + ) prompt = ChatPromptTemplate.from_messages([('system', instructions_text), ('system', context_message)]) chain = prompt | llm_medium_experiment.bind(prompt_cache_key="omi-transcript-structure") | parser @@ -657,6 +672,7 @@ def get_transcript_structure( 'format_instructions': parser.get_format_instructions(), 'language_code': language_code, 'response_language': response_language, + 'user_name': user_name, 'started_at': started_at.isoformat(), 'tz': tz, } From 48b57fa394b05f1fffc3614702af384b83ed3c1d Mon Sep 17 00:00:00 2001 From: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Date: Sun, 19 Apr 2026 20:56:40 -0700 Subject: [PATCH 2/2] revert(backend): drop USER CONTEXT prompt addition per maintainer feedback Restore the original context_message template with only language and conversation content, preserving the prompt as it was before #6216. Removes the USER CONTEXT section and the Speaker 0 heuristic that @beastoin flagged on the review. The get_user_name() fetch is kept so the scaffolding for the feature remains wired, but the value is no longer threaded into the prompt. Removes the accompanying test file since its assertions were all about the reverted prompt injection, which also resolves the greptile P1 about the double-escaped regex. --- ...ersation_transcript_structure_user_name.py | 117 ------------------ backend/utils/llm/conversation_processing.py | 11 +- 2 files changed, 1 insertion(+), 127 deletions(-) delete mode 100644 backend/tests/unit/test_conversation_transcript_structure_user_name.py diff --git a/backend/tests/unit/test_conversation_transcript_structure_user_name.py b/backend/tests/unit/test_conversation_transcript_structure_user_name.py deleted file mode 100644 index ff5bf30802..0000000000 --- a/backend/tests/unit/test_conversation_transcript_structure_user_name.py +++ /dev/null @@ -1,117 +0,0 @@ -"""Tests for user name context injection in transcript structure prompt (#6216).""" - -import inspect -import re -import sys -from datetime import datetime, timezone -from unittest.mock import MagicMock - -# Mock modules that initialize clients at import time -sys.modules.setdefault("database._client", MagicMock()) -sys.modules.setdefault("firebase_admin", MagicMock()) -_mock_clients = MagicMock() -sys.modules.setdefault("utils.llm.clients", _mock_clients) - -from models.structured import Structured -from utils.llm import conversation_processing as cp - - -class _FakeChain: - def __init__(self, captured): - self.captured = captured - - def __or__(self, _other): - return self - - def invoke(self, values): - self.captured["invoke_values"] = values - return Structured() - - -class _FakePrompt: - def __init__(self, captured): - self.captured = captured - - def __or__(self, _other): - return _FakeChain(self.captured) - - -def _run_with_user_name(monkeypatch, user_name: str): - captured = {} - - def fake_from_messages(messages): - captured["messages"] = messages - return _FakePrompt(captured) - - monkeypatch.setattr(cp, "_build_conversation_context", lambda *_args, **_kwargs: "Transcript: ```hello```") - monkeypatch.setattr(cp, "get_user_name", lambda _uid: user_name) - monkeypatch.setattr(cp.ChatPromptTemplate, "from_messages", fake_from_messages) - monkeypatch.setattr(cp.llm_medium_experiment, "bind", lambda **_kwargs: object()) - - cp.get_transcript_structure( - transcript="Speaker 0: hello", - started_at=datetime(2026, 4, 1, 12, 0, tzinfo=timezone.utc), - language_code="en", - tz="UTC", - uid="user-123", - ) - - return captured - - -def test_get_transcript_structure_injects_named_user(monkeypatch): - captured = _run_with_user_name(monkeypatch, "Aarav") - - assert "messages" in captured - context_message = captured["messages"][1][1] - assert "{user_name}" in context_message - assert captured["invoke_values"]["user_name"] == "Aarav" - - -def test_get_transcript_structure_uses_default_user_name(monkeypatch): - captured = _run_with_user_name(monkeypatch, "The User") - assert captured["invoke_values"]["user_name"] == "The User" - - -def test_get_transcript_structure_supports_firestore_fallback_name(monkeypatch): - captured = _run_with_user_name(monkeypatch, "Priya") - assert captured["invoke_values"]["user_name"] == "Priya" - - -def test_get_transcript_structure_falls_back_when_get_user_name_raises(monkeypatch): - captured = {} - - def fake_from_messages(messages): - captured["messages"] = messages - return _FakePrompt(captured) - - def raising_get_user_name(_uid): - raise RuntimeError("redis down") - - monkeypatch.setattr(cp, "_build_conversation_context", lambda *_a, **_kw: "Transcript: ```hello```") - monkeypatch.setattr(cp, "get_user_name", raising_get_user_name) - monkeypatch.setattr(cp.ChatPromptTemplate, "from_messages", fake_from_messages) - monkeypatch.setattr(cp.llm_medium_experiment, "bind", lambda **_kwargs: object()) - - cp.get_transcript_structure( - transcript="Speaker 0: hello", - started_at=datetime(2026, 4, 1, 12, 0, tzinfo=timezone.utc), - language_code="en", - tz="UTC", - uid="user-123", - ) - - assert captured["invoke_values"]["user_name"] == "The User" - - -def test_user_name_context_is_not_in_static_instructions_prefix(): - source = inspect.getsource(cp.get_transcript_structure) - - instructions_match = re.search(r"instructions_text\\s*=\\s*'''(.*?)'''", source, re.DOTALL) - assert instructions_match, "Could not find instructions_text definition" - instructions_content = instructions_match.group(1) - assert "{user_name}" not in instructions_content - - context_match = re.search(r"context_message\\s*=", source) - assert context_match, "Could not find context_message definition" - assert "{user_name}" in source diff --git a/backend/utils/llm/conversation_processing.py b/backend/utils/llm/conversation_processing.py index 28f5837b6d..abb907093a 100644 --- a/backend/utils/llm/conversation_processing.py +++ b/backend/utils/llm/conversation_processing.py @@ -654,15 +654,7 @@ def get_transcript_structure( ).strip() # Second system message: conversation context (dynamic, per-conversation) - context_message = ( - "The content language is {language_code}. You MUST respond entirely in {response_language}.\n\n" - "USER CONTEXT:\n" - "The wearer/user's first name is {user_name}. When the transcript or summary would refer to the wearer " - 'generically (e.g., "the user", "User"), use their name instead. Do NOT assume a specific numbered ' - 'speaker (e.g., "Speaker 0") is always the wearer - infer from context; prefer calendar participant ' - 'names for other speakers when available.\n\n' - "Content:\n{conversation_context}" - ) + context_message = 'The content language is {language_code}. You MUST respond entirely in {response_language}.\n\nContent:\n{conversation_context}' prompt = ChatPromptTemplate.from_messages([('system', instructions_text), ('system', context_message)]) chain = prompt | llm_medium_experiment.bind(prompt_cache_key="omi-transcript-structure") | parser @@ -672,7 +664,6 @@ def get_transcript_structure( 'format_instructions': parser.get_format_instructions(), 'language_code': language_code, 'response_language': response_language, - 'user_name': user_name, 'started_at': started_at.isoformat(), 'tz': tz, }