Skip to content

Commit c35d1b3

Browse files
committed
use compile for all instead of update
1 parent cbf2265 commit c35d1b3

3 files changed

Lines changed: 153 additions & 157 deletions

File tree

langfuse/model.py

Lines changed: 94 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -302,142 +302,129 @@ 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-
self.raw_prompt: List[ChatMessageWithPlaceholdersDict] = []
306-
self.placeholder_fillins: Dict[str, List[ChatMessageDict]] = {}
307-
self.prompt = prompt.prompt
308-
309-
@property
310-
def prompt(self) -> List[Union[ChatMessageDict, ChatMessagePlaceholderDict]]:
311-
"""Returns the prompt with placeholders substituted for their values.
312-
If no placeholder fill-ins are set and raw_prompt contains placeholders, returns messages and placeholders
313-
with a warning.
314-
"""
315-
compiled_messages = []
316-
has_unresolved_placeholders = False
317-
318-
for chat_message in self.raw_prompt:
319-
if chat_message["type"] == "message":
320-
compiled_messages.append(
321-
ChatMessageDict(
322-
content=chat_message["content"],
323-
role=chat_message["role"],
324-
),
325-
)
326-
elif chat_message["type"] == "placeholder":
327-
if chat_message["name"] in self.placeholder_fillins:
328-
placeholder_messages = self.placeholder_fillins[
329-
chat_message["name"]
330-
]
331-
if isinstance(placeholder_messages, List):
332-
compiled_messages.extend(placeholder_messages)
333-
else:
334-
err_placeholder_not_list = f"Placeholder '{chat_message['name']}' must contain a list of chat messages, got {type(placeholder_messages)}"
335-
raise ValueError(err_placeholder_not_list)
336-
else:
337-
compiled_messages.append(
338-
{
339-
"type": "placeholder",
340-
"name": chat_message["name"],
341-
},
342-
)
343-
has_unresolved_placeholders = True
344-
if has_unresolved_placeholders and len(self.placeholder_fillins) == 0:
345-
unresolved = [
346-
msg["name"] for msg in self.raw_prompt if msg["type"] == "placeholder"
347-
]
348-
err_unresolved_placeholders = f"Placeholders {unresolved} have no values set. Use update() to set placeholder values."
349-
# Warning because users might want to further process placeholders as well
350-
langfuse_logger.warning(err_unresolved_placeholders)
351-
elif has_unresolved_placeholders:
352-
unresolved = [
353-
msg["name"]
354-
for msg in self.raw_prompt
355-
if msg["type"] == "placeholder"
356-
and msg["name"] not in self.placeholder_fillins
357-
]
358-
err_unresolved_placeholders = f"Placeholders {unresolved} have no values set. Use update() to set placeholder values."
359-
# Warning because users might want to further process placeholders as well
360-
langfuse_logger.warning(err_unresolved_placeholders)
361305

362-
return compiled_messages
363-
364-
@prompt.setter
365-
def prompt(
366-
self,
367-
prompt: Sequence[
368-
Union[ChatMessageWithPlaceholdersDict, ChatMessageWithPlaceholders]
369-
],
370-
) -> None:
371-
"""Backward-compatible setter for raw prompt structure."""
372-
for p in prompt:
306+
# Convert and store the prompt directly
307+
self.prompt = []
308+
for p in prompt.prompt:
309+
# Handle objects with attributes (normal case)
373310
if hasattr(p, "type") and hasattr(p, "name") and p.type == "placeholder":
374-
self.raw_prompt.append(
311+
self.prompt.append(
375312
ChatMessageWithPlaceholdersDict_Placeholder(
376313
type="placeholder",
377314
name=p.name,
378315
),
379316
)
380317
elif hasattr(p, "role") and hasattr(p, "content"):
381-
self.raw_prompt.append(
318+
self.prompt.append(
382319
ChatMessageWithPlaceholdersDict_Message(
383320
type="message",
384321
role=p.role,
385322
content=p.content,
386323
),
387324
)
388-
389-
self.placeholder_fillins = {} # Clear because user expects old placeholders not to linger
325+
# Handle plain dictionaries (fallback case)
326+
elif isinstance(p, dict):
327+
if p.get("type") == "placeholder" and "name" in p:
328+
self.prompt.append(
329+
ChatMessageWithPlaceholdersDict_Placeholder(
330+
type="placeholder",
331+
name=p["name"],
332+
),
333+
)
334+
elif "role" in p and "content" in p:
335+
self.prompt.append(
336+
ChatMessageWithPlaceholdersDict_Message(
337+
type="message",
338+
role=p["role"],
339+
content=p["content"],
340+
),
341+
)
390342

391343
def compile(self, **kwargs) -> List[ChatMessageDict]:
392-
# Compile skips placeholders which aren't resolved
393-
return [
394-
ChatMessageDict(
395-
content=TemplateParser.compile_template(
396-
chat_message["content"],
397-
kwargs,
398-
),
399-
role=chat_message["role"],
400-
)
401-
for chat_message in self.prompt
402-
if "content" in chat_message and "role" in chat_message
403-
]
404-
405-
def update(
406-
self, *, placeholders: Dict[str, List[ChatMessageDict]]
407-
) -> "ChatPromptClient":
408-
"""Updates the stored placeholder values with the provided ones.
344+
"""Compile the prompt with placeholders and variables.
409345
410346
Args:
411-
placeholders: Dictionary mapping placeholder names to lists of chat messages
347+
**kwargs: Can contain both placeholder values (list of chat messages) and variable values.
348+
Placeholders are resolved first, then variables are substituted.
412349
413350
Returns:
414-
ChatPromptClient: Self for method chaining
351+
List of compiled chat messages as plain dictionaries.
415352
"""
416-
self.placeholder_fillins.update(placeholders)
417-
return self
353+
compiled_messages = []
354+
unresolved_placeholders = []
355+
356+
for chat_message in self.prompt:
357+
if chat_message["type"] == "message":
358+
# For regular messages, compile variables and add to output
359+
compiled_messages.append(
360+
{
361+
"role": chat_message["role"],
362+
"content": TemplateParser.compile_template(
363+
chat_message["content"],
364+
kwargs,
365+
),
366+
}
367+
)
368+
elif chat_message["type"] == "placeholder":
369+
# Check if placeholder value is provided in kwargs
370+
placeholder_name = chat_message["name"]
371+
if placeholder_name in kwargs:
372+
placeholder_value = kwargs[placeholder_name]
373+
if isinstance(placeholder_value, list):
374+
# Add all messages from the placeholder
375+
for msg in placeholder_value:
376+
if (
377+
isinstance(msg, dict)
378+
and "role" in msg
379+
and "content" in msg
380+
):
381+
# Compile variables in placeholder messages too
382+
compiled_messages.append(
383+
{
384+
"role": msg["role"],
385+
"content": TemplateParser.compile_template(
386+
msg["content"],
387+
kwargs,
388+
),
389+
}
390+
)
391+
else:
392+
raise ValueError(
393+
f"Placeholder '{placeholder_name}' must contain a list of chat messages with 'role' and 'content' fields"
394+
)
395+
else:
396+
raise ValueError(
397+
f"Placeholder '{placeholder_name}' must contain a list of chat messages, got {type(placeholder_value)}"
398+
)
399+
else:
400+
# Placeholder not resolved - track it
401+
unresolved_placeholders.append(placeholder_name)
402+
403+
# Warn about unresolved placeholders
404+
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+
408+
return compiled_messages
418409

419410
@property
420411
def variables(self) -> List[str]:
421412
"""Return all the variable names in the chat prompt template."""
422413
variables = []
423-
# Variables from raw prompt messages
424-
for chat_message in self.raw_prompt:
414+
# Variables from prompt messages
415+
for chat_message in self.prompt:
425416
if chat_message["type"] == "message":
426417
variables.extend(
427418
TemplateParser.find_variable_names(chat_message["content"])
428419
)
429-
# Variables from placeholder messages
430-
for placeholder_messages in self.placeholder_fillins.values():
431-
for msg in placeholder_messages:
432-
variables.extend(TemplateParser.find_variable_names(msg["content"]))
433420
return variables
434421

435422
def __eq__(self, other):
436423
if isinstance(self, other.__class__):
437424
return (
438425
self.name == other.name
439426
and self.version == other.version
440-
and len(self.raw_prompt) == len(other.raw_prompt)
427+
and len(self.prompt) == len(other.prompt)
441428
and all(
442429
# chatmessage equality
443430
(
@@ -453,10 +440,9 @@ def __eq__(self, other):
453440
and m2["type"] == "placeholder"
454441
and m1["name"] == m2["name"]
455442
)
456-
for m1, m2 in zip(self.raw_prompt, other.raw_prompt)
443+
for m1, m2 in zip(self.prompt, other.prompt)
457444
)
458445
and self.config == other.config
459-
and self.placeholder_fillins == other.placeholder_fillins
460446
)
461447

462448
return False
@@ -470,20 +456,21 @@ def get_langchain_prompt(self, **kwargs):
470456
kwargs: Optional keyword arguments to precompile the template string. Variables that match
471457
the provided keyword arguments will be precompiled. Remaining variables must then be
472458
handled by Langchain's prompt template.
459+
Can also contain placeholders (list of chat messages) which will be expanded.
473460
474461
Returns:
475462
List of messages in the format expected by Langchain's ChatPromptTemplate: (role, content) tuple.
476463
"""
464+
# First compile with placeholders and variables to get full message list
465+
compiled_messages = self.compile(**kwargs)
466+
467+
# Then convert to Langchain format
477468
return [
478469
(
479470
msg["role"],
480-
self._get_langchain_prompt_string(
481-
TemplateParser.compile_template(msg["content"], kwargs)
482-
if kwargs
483-
else msg["content"],
484-
),
471+
self._get_langchain_prompt_string(msg["content"]),
485472
)
486-
for msg in self.prompt
473+
for msg in compiled_messages
487474
]
488475

489476

0 commit comments

Comments
 (0)