-
Notifications
You must be signed in to change notification settings - Fork 127
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Connect the generators to prompts in langfuse #1154
Comments
Hey @alex-stoica let me see if I understand everything correctly here. It seems the assumption that |
Hello @vblagoje,
However, it lacks prompt linkage to generations. In Langfuse, prompts (with model params) typically link directly to generations, aiding in tracking I/O per prompt and evaluating prompt effectiveness. To address your question: yes, it’s specific to Langfuse to require prompt names / objecgts for generations, and Haystack's modular design doesn’t natively support this. What I did on my side is to
if tags.get("haystack.component.type") in _SUPPORTED_GENERATORS:
meta = span._data.get("haystack.component.output", {}).get("meta")
if meta:
# Haystack returns one meta dict for each message, but the 'usage' value
# is always the same, let's just pick the first item
m = meta[0]
print(m)
try:
prompt = self._tracer.get_prompt(m.get("prompt_name"))
if prompt:
span._span.update(prompt=prompt)
except Exception as e:
print(f"Prompt not found or error occurred: {e}") This is a patch I wouldn't put that into production, but I see the need to link multiple components together when tracing in the future, and that's not that easy to fix Haystack's "blocky" design |
I wouldn't necessarily agree with blocky design statement :-) but let's not go there, let's build. In Langfuse integration we can indeed introduce LangfusePromptBuilder that loads prompts directly from Langfuse platform, renders them and also injects this |
I’d be happy to contribute. However, my local implementation, which worked, took the following approach:
This approach functions more as a patch than a robust design. Langfuse expects the prompt object attached, not the prompt name. I needed a quick fix, so in my case, the prompt is loaded, compiled, passed both as a name and as a compiled template in the generator, returned as a name, and then loaded again for Langfuse. This cycle feels inelegant, so I’d appreciate any guidance you could offer on a cleaner, more efficient implementation |
We could do this in a more simple approach that can work with all chat generators (we are deprecating generators btw). |
I looked a bit more into this one and we could do the following in
In Having said this I notice that having these explicit hardcoded component handlers in |
@alex-stoica your approach seems a bit more custom to your use case. Perhaps we should hold off the PR in that case. A few notes:
|
Thank you for the clarification on prompt caching. This likely removes the necessity of retrieving Regarding @component
class LangfusePromptBuilder():
"""
Extends the default PromptBuilder to handle Langfuse prompts
This component allows you to specify a prompt by its name in Langfuse,
or by providing a prompt template string.
If a Langfuse prompt name is provided, it loads the prompt from Langfuse,
renders it using the provided variables, and returns the rendered prompt.
"""
def __init__(
self,
template: Optional[str] = None,
langfuse_prompt_name: Optional[str] = None,
required_variables: Optional[List[str]] = None,
variables: Optional[List[str]] = None,
):
if template is not None and langfuse_prompt_name is not None:
raise ValueError("Either one of 'template' or 'langfuse_prompt_name' should be provided.")
self.required_variables = required_variables or []
self.variables = variables or []
self.langfuse_tracer = self.get_langfuse_tracer()
if langfuse_prompt_name:
self.langfuse_prompt_name = langfuse_prompt_name
self.prompt_obj = self.langfuse_tracer._tracer.get_prompt(langfuse_prompt_name)
self._template_string = self.prompt_obj.prompt
if not self._template_string:
raise RuntimeError(f"Prompt '{langfuse_prompt_name}' not found in Langfuse.")
elif template is not None:
self._template_string = template
self.langfuse_prompt_name = None
else:
raise ValueError("One of 'template' or 'langfuse_prompt_name' must be provided.")
self.prompt_builder = PromptBuilder(
template=self._template_string,
required_variables=required_variables,
variables=variables
)
self.set_input_types()
def get_langfuse_tracer(self) -> LangfuseTracer:
langfuse_client = tracing.tracer.actual_tracer
if isinstance(langfuse_client, LangfuseTracer):
return langfuse_client
else:
raise RuntimeError("Tracer is not of type LangfuseTracer. Cannot proceed.")
def set_input_types(self):
variables = self.extract_new_input_variables()
for var, var_type in variables.items():
component.set_input_type(self, var, var_type)
def extract_new_input_variables(self):
predefined_sockets = {"template", "template_variables"}
new_input_variables = {}
if hasattr(self.prompt_builder, "__haystack_input__"):
if hasattr(self.prompt_builder.__haystack_input__, "_sockets_dict"):
input_sockets = self.prompt_builder.__haystack_input__._sockets_dict
for variable_name, socket in input_sockets.items():
if variable_name not in predefined_sockets:
new_input_variables[variable_name] = socket.type
return new_input_variables
@component.output_types(prompt=str, prompt_name=str)
def run(
self,
template: Optional[str] = None,
template_variables: Optional[Dict[str, Any]] = None,
**kwargs,
):
template_to_use = template or self._template_string
if not template_to_use:
raise ValueError("No template provided to render the prompt.")
response = self.prompt_builder.run(
template=template_to_use,
template_variables=template_variables,
**kwargs
)
prompt_obj = self.prompt_obj or ""
# pb_context = {self.langfuse_prompt_name: prompt_obj} no longer required because of the caching
# self.langfuse_tracer.update_pipeline_run_context(**pb_context) no longer required because of the caching
response['prompt_name'] = self.langfuse_prompt_name
return response
def _validate_variables(self, provided_variables: Set[str]):
missing_variables = [var for var in self.required_variables if var not in provided_variables]
if missing_variables:
missing_vars_str = ", ".join(missing_variables)
raise ValueError(
f"Missing required input variables in LangfusePromptBuilder: {missing_vars_str}. "
f"Required variables: {self.required_variables}. Provided variables: {provided_variables}."
)
def to_dict(self) -> Dict[str, Any]:
class_path = f"{self.__class__.__module__}.{self.__class__.__name__}"
init_params = {
'required_variables': self.required_variables,
'variables': self.variables
}
if self.langfuse_prompt_name is not None:
init_params['langfuse_prompt_name'] = self.langfuse_prompt_name
else:
init_params['template'] = self._template_string
data = {
'type': class_path,
'init_parameters': init_params
}
return data
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> "LangfusePromptBuilder":
init_params = data.get("init_parameters", {})
return cls(
template=init_params.get("template"),
langfuse_prompt_name=init_params.get("langfuse_prompt_name"),
required_variables=init_params.get("required_variables"),
variables=init_params.get("variables"),
) |
@alex-stoica composition is a good approach in my opinion. 👍 |
@silvanocerza thanks! @vblagoje I am more than willing to contribute, but there are some concerns
My proposed solutions for solving langfuse-prompt-obj <-> generation linkage are Refactoring generators should leave an open way to implement B if this is the route you want to pursue |
Yes @alex-stoica let's address the parent span issue and the PR first. So in B solution we'd attach prompt name in ChatMessage meta but generator will return a new message that won't have that prompt name attached. Wouldn't that be a problem as we are tracing LLM response and tying it back to prompt metadata here? Perhaps I didn't understand you completely here... |
Not quite, I might have explained unclearly. I see B as a way to pass the response = ...run(**regular_inputs)
if extra_inputs:
response['meta']['extra_outputs'] = extra_inputs In tracer if m.get('extra_outputs'):
# add prompt etc ... In A
if self.get_extra_outputs_from_context(component_name):
# add prompt etc ... If neither A nor B is implemented, another component like LangfuseGenerator should be created that does exactly what a regular generator does + A (receives more input and outputs that extra input as output in |
Following up the ContextVars discussions in #1184 : I would need to think more about this but I'd assume having a higher-level component (which includes prompt builder & generator) might be the cleanest way to handle this, instead of retrieving inter-component relations on the generator/tracer end. |
Is your feature request related to a problem? Please describe.
Hello! Currently, pipeline runs (equivalent to traces in Langfuse) do not have their corresponding prompts tied with the "generation" blocks. This can be seen in the example from the Haystack Langfuse Integration blog post.
Describe the solution you'd like
When prompts exist, they should be added to the tracer file, such as in the following example:
This would ensure that prompts are tied to the corresponding generation blocks, giving better traceability.
Describe alternatives you've considered
N/A
Additional context
Desired output:
One challenge is that the prompt name is not available in the generator by default. This could complicate the integration, as it may require accessing the prompt metadata manually. In some cases, creating a custom generator that explicitly handles the prompt name and metadata might be necessary for an integration with Langfuse + allowing that custom generator to be traced (see #1153)
The text was updated successfully, but these errors were encountered: