Skip to content

Commit 41c6252

Browse files
committed
messagesplaceholder object returned
1 parent c35d1b3 commit 41c6252

3 files changed

Lines changed: 146 additions & 44 deletions

File tree

langfuse/model.py

Lines changed: 45 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -302,9 +302,8 @@ def get_langchain_prompt(self, **kwargs) -> str:
302302
class ChatPromptClient(BasePromptClient):
303303
def __init__(self, prompt: Prompt_Chat, is_fallback: bool = False):
304304
super().__init__(prompt, is_fallback)
305-
306-
# Convert and store the prompt directly
307305
self.prompt = []
306+
308307
for p in prompt.prompt:
309308
# Handle objects with attributes (normal case)
310309
if hasattr(p, "type") and hasattr(p, "name") and p.type == "placeholder":
@@ -340,15 +339,17 @@ def __init__(self, prompt: Prompt_Chat, is_fallback: bool = False):
340339
),
341340
)
342341

343-
def compile(self, **kwargs) -> List[ChatMessageDict]:
342+
def compile(
343+
self, **kwargs
344+
) -> Sequence[Union[ChatMessageDict, ChatMessageWithPlaceholdersDict_Placeholder]]:
344345
"""Compile the prompt with placeholders and variables.
345346
346347
Args:
347348
**kwargs: Can contain both placeholder values (list of chat messages) and variable values.
348349
Placeholders are resolved first, then variables are substituted.
349350
350351
Returns:
351-
List of compiled chat messages as plain dictionaries.
352+
List of compiled chat messages as plain dictionaries, with unresolved placeholders kept as-is.
352353
"""
353354
compiled_messages = []
354355
unresolved_placeholders = []
@@ -363,47 +364,48 @@ def compile(self, **kwargs) -> List[ChatMessageDict]:
363364
chat_message["content"],
364365
kwargs,
365366
),
366-
}
367+
},
367368
)
368369
elif chat_message["type"] == "placeholder":
369-
# Check if placeholder value is provided in kwargs
370370
placeholder_name = chat_message["name"]
371371
if placeholder_name in kwargs:
372372
placeholder_value = kwargs[placeholder_name]
373373
if isinstance(placeholder_value, list):
374-
# Add all messages from the placeholder
375374
for msg in placeholder_value:
376375
if (
377376
isinstance(msg, dict)
378377
and "role" in msg
379378
and "content" in msg
380379
):
381-
# Compile variables in placeholder messages too
382380
compiled_messages.append(
383381
{
384382
"role": msg["role"],
385383
"content": TemplateParser.compile_template(
386384
msg["content"],
387385
kwargs,
388386
),
389-
}
387+
},
390388
)
391389
else:
392-
raise ValueError(
393-
f"Placeholder '{placeholder_name}' must contain a list of chat messages with 'role' and 'content' fields"
390+
compiled_messages.append(
391+
str(placeholder_value),
394392
)
393+
no_role_content_in_placeholder = f"Placeholder '{placeholder_name}' should contain a list of chat messages with 'role' and 'content' fields. Appended as string."
394+
langfuse_logger.warning(no_role_content_in_placeholder)
395395
else:
396-
raise ValueError(
397-
f"Placeholder '{placeholder_name}' must contain a list of chat messages, got {type(placeholder_value)}"
396+
compiled_messages.append(
397+
str(placeholder_value),
398398
)
399+
placeholder_not_a_list = f"Placeholder '{placeholder_name}' must contain a list of chat messages, got {type(placeholder_value)}"
400+
langfuse_logger.warning(placeholder_not_a_list)
399401
else:
400-
# Placeholder not resolved - track it
402+
# Keep unresolved placeholder in the compiled messages
403+
compiled_messages.append(chat_message)
401404
unresolved_placeholders.append(placeholder_name)
402405

403-
# Warn about unresolved placeholders
404406
if unresolved_placeholders:
405-
warning_msg = f"Placeholders {unresolved_placeholders} have not been resolved. Pass them as keyword arguments to compile()."
406-
langfuse_logger.warning(warning_msg)
407+
unresolved_placeholders = f"Placeholders {unresolved_placeholders} have not been resolved. Pass them as keyword arguments to compile()."
408+
langfuse_logger.warning(unresolved_placeholders)
407409

408410
return compiled_messages
409411

@@ -415,7 +417,7 @@ def variables(self) -> List[str]:
415417
for chat_message in self.prompt:
416418
if chat_message["type"] == "message":
417419
variables.extend(
418-
TemplateParser.find_variable_names(chat_message["content"])
420+
TemplateParser.find_variable_names(chat_message["content"]),
419421
)
420422
return variables
421423

@@ -452,26 +454,40 @@ def get_langchain_prompt(self, **kwargs):
452454
453455
It specifically adapts the mustache-style double curly braces {{variable}} used in Langfuse
454456
to the single curly brace {variable} format expected by Langchain.
457+
Placeholders are filled-in from kwargs and unresolved placeholders are returned as Langchain MessagesPlaceholder.
455458
456459
kwargs: Optional keyword arguments to precompile the template string. Variables that match
457460
the provided keyword arguments will be precompiled. Remaining variables must then be
458461
handled by Langchain's prompt template.
459-
Can also contain placeholders (list of chat messages) which will be expanded.
462+
Can also contain placeholders (list of chat messages) which will be resolved prior to variable
463+
compilation.
460464
461465
Returns:
462-
List of messages in the format expected by Langchain's ChatPromptTemplate: (role, content) tuple.
466+
List of messages in the format expected by Langchain's ChatPromptTemplate:
467+
(role, content) tuples for regular messages or MessagesPlaceholder objects for unresolved placeholders.
463468
"""
464-
# First compile with placeholders and variables to get full message list
465469
compiled_messages = self.compile(**kwargs)
470+
langchain_messages = []
466471

467-
# Then convert to Langchain format
468-
return [
469-
(
470-
msg["role"],
471-
self._get_langchain_prompt_string(msg["content"]),
472-
)
473-
for msg in compiled_messages
474-
]
472+
for msg in compiled_messages:
473+
if "type" in msg and msg["type"] == "placeholder":
474+
# unresolved placeholder -> add LC MessagesPlaceholder
475+
placeholder_name = msg["name"]
476+
try:
477+
from langchain_core.prompts.chat import MessagesPlaceholder # noqa: PLC0415, I001
478+
479+
langchain_messages.append(
480+
MessagesPlaceholder(variable_name=placeholder_name),
481+
)
482+
except ImportError as e:
483+
import_error = "langchain_core is required to use get_langchain_prompt() with unresolved placeholders."
484+
raise ImportError(import_error) from e
485+
else:
486+
langchain_messages.append(
487+
(msg["role"], self._get_langchain_prompt_string(msg["content"])),
488+
)
489+
490+
return langchain_messages
475491

476492

477493
PromptClient = Union[TextPromptClient, ChatPromptClient]

tests/test_prompt.py

Lines changed: 53 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -205,16 +205,24 @@ def test_get_prompt_with_placeholders():
205205
@pytest.mark.parametrize(
206206
("variables", "placeholders", "expected_len", "expected_contents"),
207207
[
208-
# 0. Variables only, no placeholders. Expect verbatim message only
209-
# Compile kills not filled placeholders
208+
# 0. Variables only, no placeholders. Unresolved placeholders kept in output
210209
(
211210
{"role": "helpful", "task": "coding"},
212211
{},
213-
2,
214-
["You are a helpful assistant", "Help me with coding"],
212+
3,
213+
[
214+
"You are a helpful assistant",
215+
None,
216+
"Help me with coding",
217+
], # None = placeholder
215218
),
216219
# 1. No variables, no placeholders. Expect verbatim message+placeholder output
217-
({}, {}, 2, ["You are a {{role}} assistant", "Help me with {{task}}"]),
220+
(
221+
{},
222+
{},
223+
3,
224+
["You are a {{role}} assistant", None, "Help me with {{task}}"],
225+
), # None = placeholder
218226
# 2. Placeholders only, empty variables. Expect output with placeholders filled in
219227
(
220228
{},
@@ -257,18 +265,45 @@ def test_get_prompt_with_placeholders():
257265
# 2,
258266
# ["You are a helpful assistant", "Help me with coding"],
259267
# ),
260-
# 4. Unused placeholder fill ins. Expect verbatim message+placeholder output
268+
# 4. Unused placeholder fill ins. Unresolved placeholders kept in output
261269
(
262270
{"role": "helpful", "task": "coding"},
263271
{"unused": [{"role": "user", "content": "Won't appear"}]},
264-
2,
265-
["You are a helpful assistant", "Help me with coding"],
272+
3,
273+
[
274+
"You are a helpful assistant",
275+
None,
276+
"Help me with coding",
277+
], # None = placeholder
278+
),
279+
# 5. Placeholder with non-list value (should log warning and create invalid dict)
280+
(
281+
{"role": "helpful", "task": "coding"},
282+
{"examples": "not a list"},
283+
3,
284+
[
285+
"You are a helpful assistant",
286+
"{'not a list'}", # Invalid dict becomes string when checked
287+
"Help me with coding",
288+
],
289+
),
290+
# 6. Placeholder with invalid message structure (should log warning and include both)
291+
(
292+
{"role": "helpful", "task": "coding"},
293+
{"examples": ["invalid message", {"role": "user", "content": "valid message"}]},
294+
4,
295+
[
296+
"You are a helpful assistant",
297+
"{\"['invalid message', {'role': 'user', 'content': 'valid message'}]\"}", # Invalid structure becomes string
298+
"valid message", # Valid message processed normally
299+
"Help me with coding",
300+
],
266301
),
267302
],
268303
)
269304
def test_compile_with_placeholders(
270305
variables, placeholders, expected_len, expected_contents
271-
):
306+
) -> None:
272307
"""Test compile_with_placeholders with different variable/placeholder combinations."""
273308
from langfuse.api.resources.prompts import Prompt_Chat
274309
from langfuse.model import ChatPromptClient
@@ -292,13 +327,16 @@ def test_compile_with_placeholders(
292327

293328
assert len(result) == expected_len
294329
for i, expected_content in enumerate(expected_contents):
295-
if "content" in result[i]:
296-
assert result[i]["content"] == expected_content
297-
elif "name" in result[i]:
298-
# this is a placeholder
299-
continue
330+
if expected_content is None:
331+
# This should be an unresolved placeholder
332+
assert "type" in result[i] and result[i]["type"] == "placeholder"
333+
elif expected_content.startswith("{") and expected_content.endswith("}"):
334+
# This is an invalid dictionary that becomes a string representation
335+
assert str(result[i]) == expected_content
300336
else:
301-
raise ValueError("Unexpected item in prompt compile output")
337+
# This should be a regular message
338+
assert "content" in result[i]
339+
assert result[i]["content"] == expected_content
302340

303341

304342
def test_warning_on_unresolved_placeholders():

tests/test_prompt_compilation.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -801,3 +801,51 @@ def test_chat_prompt_with_placeholders_langchain(self):
801801
assert formatted_messages[1].content == "Example: What is 2+2?"
802802
assert formatted_messages[2].content == "2+2 equals 4."
803803
assert formatted_messages[3].content == "Help me with addition."
804+
805+
def test_get_langchain_prompt_with_unresolved_placeholders(self):
806+
"""Test that unresolved placeholders become MessagesPlaceholder objects."""
807+
from langfuse.api.resources.prompts import Prompt_Chat
808+
from langfuse.model import ChatPromptClient
809+
810+
chat_messages = [
811+
{"role": "system", "content": "You are a {{role}} assistant"},
812+
{"type": "placeholder", "name": "examples"},
813+
{"role": "user", "content": "Help me with {{task}}"},
814+
]
815+
816+
prompt_client = ChatPromptClient(
817+
Prompt_Chat(
818+
type="chat",
819+
name="test_unresolved_placeholder",
820+
version=1,
821+
config={},
822+
tags=[],
823+
labels=[],
824+
prompt=chat_messages,
825+
),
826+
)
827+
828+
# Call get_langchain_prompt without resolving placeholder
829+
langchain_messages = prompt_client.get_langchain_prompt(
830+
role="helpful", task="coding",
831+
)
832+
833+
# Should have 3 items: system message, MessagesPlaceholder, user message
834+
assert len(langchain_messages) == 3
835+
836+
# First message should be the system message
837+
assert langchain_messages[0] == ("system", "You are a helpful assistant")
838+
839+
# Second should be a MessagesPlaceholder for the unresolved placeholder
840+
placeholder_msg = langchain_messages[1]
841+
try:
842+
from langchain_core.prompts.chat import MessagesPlaceholder
843+
844+
assert isinstance(placeholder_msg, MessagesPlaceholder)
845+
assert placeholder_msg.variable_name == "examples"
846+
except ImportError:
847+
# Fallback case when langchain_core is not available
848+
assert placeholder_msg == ("system", "{examples}")
849+
850+
# Third message should be the user message
851+
assert langchain_messages[2] == ("user", "Help me with coding")

0 commit comments

Comments
 (0)