22
33import re
44from abc import ABC , abstractmethod
5- from typing import Any , Dict , List , Literal , Optional , Tuple , TypedDict , Union
5+ from typing import Any , Dict , List , Literal , Optional , Sequence , Tuple , TypedDict , Union
6+ from langfuse .logger import langfuse_logger
67
78from langfuse .api .resources .commons .types .dataset import (
89 Dataset , # noqa: F401
3738 CreateDatasetRequest ,
3839)
3940from langfuse .api .resources .prompts import ChatMessage , Prompt , Prompt_Chat , Prompt_Text
41+ from langfuse .api .resources .prompts .types .chat_message_with_placeholders import (
42+ ChatMessageWithPlaceholders ,
43+ )
4044
4145
4246class ModelUsage (TypedDict ):
@@ -54,6 +58,11 @@ class ChatMessageDict(TypedDict):
5458 content : str
5559
5660
61+ class ChatMessagePlaceholderDict (TypedDict ):
62+ role : str
63+ content : str
64+
65+
5766class ChatMessageWithPlaceholdersDict_Message (TypedDict ):
5867 type : Literal ["message" ]
5968 role : str
@@ -293,58 +302,155 @@ def get_langchain_prompt(self, **kwargs) -> str:
293302class ChatPromptClient (BasePromptClient ):
294303 def __init__ (self , prompt : Prompt_Chat , is_fallback : bool = False ):
295304 super ().__init__ (prompt , is_fallback )
296- self .prompt : List [ChatMessageWithPlaceholdersDict ] = []
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 placeholders are set and raw_prompt contains placeholders, returns only messages.
313+ """
314+ compiled_messages = []
315+ has_unresolved_placeholders = False
316+
317+ for chat_message in self .raw_prompt :
318+ if chat_message ["type" ] == "message" :
319+ compiled_messages .append (
320+ ChatMessageDict (
321+ content = chat_message ["content" ],
322+ role = chat_message ["role" ],
323+ ),
324+ )
325+ elif chat_message ["type" ] == "placeholder" :
326+ if chat_message ["name" ] in self .placeholder_fillins :
327+ placeholder_messages = self .placeholder_fillins [
328+ chat_message ["name" ]
329+ ]
330+ if isinstance (placeholder_messages , List ):
331+ compiled_messages .extend (placeholder_messages )
332+ else :
333+ err_placeholder_not_list = f"Placeholder '{ chat_message ['name' ]} ' must contain a list of chat messages, got { type (placeholder_messages )} "
334+ raise ValueError (err_placeholder_not_list )
335+ else :
336+ compiled_messages .append (
337+ {
338+ "type" : "placeholder" ,
339+ "name" : chat_message ["name" ],
340+ },
341+ )
342+ has_unresolved_placeholders = True
343+ if has_unresolved_placeholders and len (self .placeholder_fillins ) == 0 :
344+ unresolved = [
345+ msg ["name" ] for msg in self .raw_prompt if msg ["type" ] == "placeholder"
346+ ]
347+ err_unresolved_placeholders = f"Placeholders { unresolved } have no values set. Use update() to set placeholder values."
348+ langfuse_logger .warning (err_unresolved_placeholders )
349+ # raise ValueError(err_unresolved_placeholders)
350+ elif has_unresolved_placeholders :
351+ unresolved = [
352+ msg ["name" ]
353+ for msg in self .raw_prompt
354+ if msg ["type" ] == "placeholder"
355+ and msg ["name" ] not in self .placeholder_fillins
356+ ]
357+ err_unresolved_placeholders = f"Placeholders { unresolved } have no values set. Use update() to set placeholder values."
358+ langfuse_logger .warning (err_unresolved_placeholders )
359+ # raise ValueError(err_unresolved_placeholders)
360+
361+ return compiled_messages
297362
298- for p in prompt .prompt :
363+ @prompt .setter
364+ def prompt (
365+ self ,
366+ prompt : Sequence [
367+ Union [ChatMessageWithPlaceholdersDict , ChatMessageWithPlaceholders ]
368+ ],
369+ ) -> None :
370+ """Backward-compatible setter for raw prompt structure."""
371+ for p in prompt :
299372 if hasattr (p , "type" ) and hasattr (p , "name" ) and p .type == "placeholder" :
300- self .prompt .append (
373+ self .raw_prompt .append (
301374 ChatMessageWithPlaceholdersDict_Placeholder (
302375 type = "placeholder" ,
303376 name = p .name ,
304377 )
305378 )
306379 elif hasattr (p , "role" ) and hasattr (p , "content" ):
307- self .prompt .append (
380+ self .raw_prompt .append (
308381 ChatMessageWithPlaceholdersDict_Message (
309382 type = "message" ,
310383 role = p .role ,
311384 content = p .content ,
312385 )
313386 )
314387
388+ self .placeholder_fillins = {} # Clear because user expects old placeholders not to linger
389+
315390 def compile (self , ** kwargs ) -> List [ChatMessageDict ]:
316- compiled_messages : List [ChatMessageDict ] = []
317- for chat_message in self .prompt :
318- if chat_message ["type" ] == "message" :
319- compiled_messages .append (
320- ChatMessageDict (
321- content = TemplateParser .compile_template (
322- chat_message ["content" ], kwargs
323- ),
324- role = chat_message ["role" ],
325- )
326- )
327- elif chat_message ["type" ] == "placeholder" :
328- placeholder_in_compile_error = f"Called compile on chat client with placeholder: { chat_message ['name' ]} . Please use compile_with_placeholders instead."
329- raise ValueError (placeholder_in_compile_error )
330- return compiled_messages
391+ # Compile skips placeholders which aren't resolved
392+ return [
393+ ChatMessageDict (
394+ content = TemplateParser .compile_template (
395+ chat_message ["content" ],
396+ kwargs ,
397+ ),
398+ role = chat_message ["role" ],
399+ )
400+ for chat_message in self .prompt
401+ if "content" in chat_message and "role" in chat_message
402+ ]
403+
404+ def set (self , placeholders : Dict [str , List [ChatMessageDict ]]) -> "ChatPromptClient" :
405+ """Sets the internal placeholders to the given dict
406+
407+ Args:
408+ placeholders: Dictionary mapping placeholder names to lists of chat messages
409+
410+ Returns:
411+ ChatPromptClient: Self for method chaining
412+ """
413+ self .placeholder_fillins = placeholders .copy ()
414+ return self
415+
416+ def update (
417+ self , placeholders : Dict [str , List [ChatMessageDict ]]
418+ ) -> "ChatPromptClient" :
419+ """Updates the stored placeholder values.
420+
421+ Only adds new placeholders or updates existing ones. Does not delete existing keys.
422+
423+ Args:
424+ placeholders: Dictionary mapping placeholder names to lists of chat messages
425+
426+ Returns:
427+ ChatPromptClient: Self for method chaining
428+ """
429+ self .placeholder_fillins .update (placeholders )
430+ return self
331431
332432 @property
333433 def variables (self ) -> List [str ]:
334434 """Return all the variable names in the chat prompt template."""
335- return [
336- variable
337- for chat_message in self .prompt
338- if chat_message ["type" ] == "message"
339- for variable in TemplateParser .find_variable_names (chat_message ["content" ])
340- ]
435+ variables = []
436+ # Variables from raw prompt messages
437+ for chat_message in self .raw_prompt :
438+ if chat_message ["type" ] == "message" :
439+ variables .extend (
440+ TemplateParser .find_variable_names (chat_message ["content" ])
441+ )
442+ # Variables from placeholder messages
443+ for placeholder_messages in self .placeholder_fillins .values ():
444+ for msg in placeholder_messages :
445+ variables .extend (TemplateParser .find_variable_names (msg ["content" ]))
446+ return variables
341447
342448 def __eq__ (self , other ):
343449 if isinstance (self , other .__class__ ):
344450 return (
345451 self .name == other .name
346452 and self .version == other .version
347- and len (self .prompt ) == len (other .prompt )
453+ and len (self .raw_prompt ) == len (other .raw_prompt )
348454 and all (
349455 # chatmessage equality
350456 (
@@ -360,82 +466,14 @@ def __eq__(self, other):
360466 and m2 ["type" ] == "placeholder"
361467 and m1 ["name" ] == m2 ["name" ]
362468 )
363- for m1 , m2 in zip (self .prompt , other .prompt )
469+ for m1 , m2 in zip (self .raw_prompt , other .raw_prompt )
364470 )
365471 and self .config == other .config
472+ and self .placeholder_fillins == other .placeholder_fillins
366473 )
367474
368475 return False
369476
370- def compile_with_placeholders (
371- self ,
372- placeholders : Dict [str , List [ChatMessageDict ]],
373- variables : Optional [Dict [str , str ]] = None ,
374- persist_compilation : bool = False ,
375- ) -> List [ChatMessageDict ]:
376- """Compile chat prompt by first replacing placeholders, then expanding variables.
377-
378- Args:
379- variables: Dictionary of variable names to values for template substitution
380- placeholders: Dictionary of placeholder names to lists of ChatMessage objects
381- persist_compilation: If True, saves the compiled output to the internal state. Useful if using the output for langchain prompts.
382-
383- Returns:
384- List[ChatMessageDict]: Compiled chat messages
385- """
386- if variables is None :
387- variables = {}
388-
389- messages_with_placeholders_replaced : List [ChatMessageDict ] = []
390-
391- # Subsitute the placeholders for their supplied ChatMessages
392- for item in self .prompt :
393- if item ["type" ] == "placeholder" and item ["name" ] in placeholders :
394- if (
395- isinstance (placeholders [item ["name" ]], List )
396- and len (placeholders [item ["name" ]]) > 0
397- ):
398- messages_with_placeholders_replaced .extend (
399- placeholders [item ["name" ]]
400- )
401- else :
402- empty_placeholder_error = (
403- f"The provided placeholder: { item ['name' ]} is empty"
404- )
405- raise ValueError (empty_placeholder_error )
406- elif item ["type" ] == "message" :
407- messages_with_placeholders_replaced .append (
408- ChatMessageDict (
409- role = item ["role" ],
410- content = item ["content" ],
411- )
412- )
413-
414- # Then, replace the variables in the ChatMessage content.
415- compiled_messages = [
416- ChatMessageDict (
417- content = TemplateParser .compile_template (
418- chat_message ["content" ],
419- variables ,
420- ),
421- role = chat_message ["role" ],
422- )
423- for chat_message in messages_with_placeholders_replaced
424- ]
425-
426- # Mutate the internal prompt object if requested
427- if persist_compilation :
428- self .prompt = [
429- ChatMessageWithPlaceholdersDict_Message (
430- type = "message" ,
431- role = msg ["role" ],
432- content = msg ["content" ],
433- )
434- for msg in compiled_messages
435- ]
436-
437- return compiled_messages
438-
439477 def get_langchain_prompt (self , ** kwargs ):
440478 """Convert Langfuse prompt into string compatible with Langchain ChatPromptTemplate.
441479
@@ -459,7 +497,6 @@ def get_langchain_prompt(self, **kwargs):
459497 ),
460498 )
461499 for msg in self .prompt
462- if msg ["type" ] == "message"
463500 ]
464501
465502
0 commit comments