Skip to content

Commit e295821

Browse files
committed
migrate to getter setter
1 parent 9199be5 commit e295821

3 files changed

Lines changed: 209 additions & 155 deletions

File tree

langfuse/model.py

Lines changed: 135 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22

33
import re
44
from 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

78
from langfuse.api.resources.commons.types.dataset import (
89
Dataset, # noqa: F401
@@ -37,6 +38,9 @@
3738
CreateDatasetRequest,
3839
)
3940
from 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

4246
class 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+
5766
class ChatMessageWithPlaceholdersDict_Message(TypedDict):
5867
type: Literal["message"]
5968
role: str
@@ -293,58 +302,155 @@ def get_langchain_prompt(self, **kwargs) -> str:
293302
class 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

Comments
 (0)