From f46716995a330b928d0af390df4ff626e5019479 Mon Sep 17 00:00:00 2001 From: i-anubhav-anand Date: Mon, 15 Jun 2026 02:14:53 +0530 Subject: [PATCH 1/2] fix(langchain): convert prompt variables with non-word characters _get_langchain_prompt_string only matched {{ \w+ }} when converting Langfuse mustache variables to langchain single-brace placeholders, so variables whose names contain hyphens, spaces, or unicode (all valid Langfuse variable names) were left as {{name}} and silently lost: PromptTemplate reads {{ as a literal brace, so the variable became un-fillable. Broaden the capture to [^{}"]+? so any variable name converts, while still excluding already-escaped JSON (which appears as {{"key": ...}} and must stay doubled). Adds a unit test (fails before: variable left as {{user-name}}). --- langfuse/model.py | 6 +++++- tests/unit/test_prompt_compilation.py | 23 +++++++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/langfuse/model.py b/langfuse/model.py index 69d721597..5e600e79d 100644 --- a/langfuse/model.py +++ b/langfuse/model.py @@ -163,7 +163,11 @@ def get_langchain_prompt(self) -> Any: def _get_langchain_prompt_string(content: str) -> str: json_escaped_content = BasePromptClient._escape_json_for_langchain(content) - return re.sub(r"{{\s*(\w+)\s*}}", r"{\g<1>}", json_escaped_content) + # Match any Langfuse variable name between {{ }} (Langfuse allows names + # with hyphens, spaces, unicode, etc.), not just \w+. The character class + # excludes braces and quotes so already-escaped JSON (which appears as + # {{"key": ...}} after _escape_json_for_langchain) is left untouched. + return re.sub(r'{{\s*([^{}"]+?)\s*}}', r"{\g<1>}", json_escaped_content) @staticmethod def _escape_json_for_langchain(text: str) -> str: diff --git a/tests/unit/test_prompt_compilation.py b/tests/unit/test_prompt_compilation.py index 1b96a14dd..c2cd56453 100644 --- a/tests/unit/test_prompt_compilation.py +++ b/tests/unit/test_prompt_compilation.py @@ -255,6 +255,29 @@ def test_normal_variables_with_nested_json(self): assert formatted_prompt == expected + def test_get_langchain_prompt_preserves_special_variable_names(self): + """Variable names with hyphens/spaces/unicode must convert to langchain + single-brace placeholders instead of being left as unusable {{...}}.""" + prompt = TextPromptClient( + Prompt_Text( + type="text", + name="special_var_names", + version=1, + config={}, + tags=[], + labels=[], + prompt="Hello {{user-name}}!", + ) + ) + + langchain_prompt_string = prompt.get_langchain_prompt() + + assert langchain_prompt_string == "Hello {user-name}!" + + langchain_prompt = PromptTemplate.from_template(langchain_prompt_string) + assert langchain_prompt.input_variables == ["user-name"] + assert langchain_prompt.format(**{"user-name": "Ada"}) == "Hello Ada!" + def test_mixed_variables_with_nested_json(self): """Test normal variables (double braces) and Langchain variables (single braces) with nested JSON.""" prompt_string = """Normal variable: {{user_name}} From d321095e28f8b7fc9b7e2871f72f5c79c7ef6df3 Mon Sep 17 00:00:00 2001 From: i-anubhav-anand Date: Mon, 15 Jun 2026 02:29:56 +0530 Subject: [PATCH 2/2] fix(langchain): also exclude single quotes from variable regex Address review feedback: _escape_json_for_langchain doubles braces for both "-prefixed and '-prefixed content, so the variable regex must exclude both quote styles or it un-escapes single-quote JSON like {{'key': 'value'}}. Broaden exclusion class to [^{}"'] and add a regression test. --- langfuse/model.py | 7 ++++--- tests/unit/test_prompt_compilation.py | 19 +++++++++++++++++++ 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/langfuse/model.py b/langfuse/model.py index 5e600e79d..348969d1e 100644 --- a/langfuse/model.py +++ b/langfuse/model.py @@ -165,9 +165,10 @@ def _get_langchain_prompt_string(content: str) -> str: # Match any Langfuse variable name between {{ }} (Langfuse allows names # with hyphens, spaces, unicode, etc.), not just \w+. The character class - # excludes braces and quotes so already-escaped JSON (which appears as - # {{"key": ...}} after _escape_json_for_langchain) is left untouched. - return re.sub(r'{{\s*([^{}"]+?)\s*}}', r"{\g<1>}", json_escaped_content) + # excludes braces and both quote styles so already-escaped JSON (which + # _escape_json_for_langchain doubles to {{"...}} or {{'...}}) is left + # untouched. + return re.sub(r"{{\s*([^{}\"']+?)\s*}}", r"{\g<1>}", json_escaped_content) @staticmethod def _escape_json_for_langchain(text: str) -> str: diff --git a/tests/unit/test_prompt_compilation.py b/tests/unit/test_prompt_compilation.py index c2cd56453..a010eae24 100644 --- a/tests/unit/test_prompt_compilation.py +++ b/tests/unit/test_prompt_compilation.py @@ -278,6 +278,25 @@ def test_get_langchain_prompt_preserves_special_variable_names(self): assert langchain_prompt.input_variables == ["user-name"] assert langchain_prompt.format(**{"user-name": "Ada"}) == "Hello Ada!" + def test_get_langchain_prompt_leaves_quoted_json_escaped(self): + """Brace-pairs wrapping JSON (single- or double-quoted) must stay doubled + for langchain, not be converted as if they were variables.""" + prompt = TextPromptClient( + Prompt_Text( + type="text", + name="json_braces", + version=1, + config={}, + tags=[], + labels=[], + prompt="Reply with {'key': 'value'} and {{name}}", + ) + ) + + result = prompt.get_langchain_prompt() + + assert result == "Reply with {{'key': 'value'}} and {name}" + def test_mixed_variables_with_nested_json(self): """Test normal variables (double braces) and Langchain variables (single braces) with nested JSON.""" prompt_string = """Normal variable: {{user_name}}