Skip to content
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

feat: Add custom Langfuse span handling support #1313

Merged
merged 10 commits into from
Jan 28, 2025
Merged

feat: Add custom Langfuse span handling support #1313

merged 10 commits into from
Jan 28, 2025

Conversation

vblagoje
Copy link
Member

@vblagoje vblagoje commented Jan 21, 2025

Why:

The main motivation behind this change is to enable developers to easily customize Langfuse traces without having to rewrite the entire LangfuseConnector. By introducing the SpanHandler interface and its default implementation, DefaultSpanHandler, developers can now extend SpanHandler or, even better, override the handle method in DefaultSpanHandler to tailor trace processing to their needs. This simplifies the process of customizing what data is logged to Langfuse, providing a more flexible and developer-friendly integration.

Enables developers to add further customization to spans as noted in these issues:

What:

  • Added a new SpanHandler class and its default implementation DefaultSpanHandler for customizing span creation and processing.
  • Grouped all span parameters into a SpanContext dataclass.
  • Updated the LangfuseConnector and LangfuseTracer to support custom span handlers.
  • Modified tests to verify the new functionality and its impact on span handling.

How can it be used:

Developers can utilize custom span handlers by providing their implementations that extend the SpanHandler class:

class CustomSpanHandler(DefaultSpanHandler):
    def handle(self, span: LangfuseSpan, component_type: Optional[str]) -> None:
        # Custom span handling logic, customize Langfuse spans however it fits you
        pass

connector = LangfuseConnector(span_handler=CustomSpanHandler())

How did you test it:

Tests were conducted using the QualityCheckSpanHandler to ensure span WARNING levels are set correctly. Different response scenarios were simulated to check the handler's response, and the Langfuse API was polled to verify the output.

Notes for the reviewer:

Pay attention to the SpanHandler's implementation to ensure it meets necessary specifications and seamlessly integrates with the existing LangfuseTracer. Additionally, confirm that the new test scenarios cover diverse usage cases adequately.

@github-actions github-actions bot added integration:langfuse type:documentation Improvements or additions to documentation labels Jan 21, 2025
@vblagoje vblagoje changed the title Add SpanHandler feat: Add custom Langfuse span handling support Jan 21, 2025
@vblagoje vblagoje marked this pull request as ready for review January 21, 2025 16:21
@vblagoje vblagoje requested a review from a team as a code owner January 21, 2025 16:21
@vblagoje vblagoje requested review from anakin87 and removed request for a team January 21, 2025 16:21
@vblagoje
Copy link
Member Author

vblagoje commented Jan 21, 2025

@alex-stoica @ggdupont @lc-bo @LastRemote would this approach fit all of your Langfuse customization needs?

@vblagoje vblagoje marked this pull request as draft January 21, 2025 16:23
@alex-stoica
Copy link
Contributor

@vblagoje I have to think deeper, but at the first glance, my answer is "partially", i.e.

  1. I can create a CustomSpanHandler that would trace some prompt information about a generation ✅
  2. I still need to build a langfuse prompt builder, quite similar with something proposed in Connect the generators to prompts in langfuse  #1154, that pushes the prompt name and a custom generator outputs this prompt name so that I can log it further with my current span ❌(This part means this extra component is still needed)

So, I'd say it's a solid and elegant step in the right direction, but not the full solution.

@vblagoje
Copy link
Member Author

@vblagoje I have to think deeper, but at the first glance, my answer is "partially", i.e.

  1. I can create a CustomSpanHandler that would trace some prompt information about a generation ✅
  2. I still need to build a langfuse prompt builder, quite similar with something proposed in Connect the generators to prompts in langfuse  #1154, that pushes the prompt name and a custom generator outputs this prompt name so that I can log it further with my current span ❌(This part means this extra component is still needed)

So, I'd say it's a solid and elegant step in the right direction, but not the full solution.

@alex-stoica thanks for the feedback! What you describe seems a bit orthogonal to this issue, no? Here we enable devs to customize these spans as they see fit - that's all, good first step. This prompt injection seems something different - and that's ok.

@alex-stoica
Copy link
Contributor

@vblagoje
short answer: Yes, it's independent but aligned with the same goal, just wanted to highlight this to ensure we continue making progress in that direction too
longer explanation: to address the issue I was describing, I previously had to:

  1. Modify legacy code from the Haystack repository to make sure I can log extra fields from a generator - an undesirable and messy solution.
  2. Develop a custom LangfusePromptBuilder to forward the prompt name, along with a CustomGenerator to output the prompt name as well.

With this new modification, step 1 is resolved (not required anymore). In terms of this specific issue, the codebase moves closer to being open/closed (SOLID principles), I now only need to extend functionality (e.g., create a custom span, prompt builder, or generator) without modifying existing code. As mentioned earlier, this is a good step forward.

@vblagoje
Copy link
Member Author

@alex-stoica thanks a lot for the feedback. I added a small change to encapsulate all the parameters for span creation into SpanContext. SpanHandler.create_span had just too many parameters and looked fragile in terms of future maintenance. I'll keep this PR in draft state for this week for you and others to try it out and kick the tires - next week we can move to PR integration if there are no major changes required. 🙏

@LastRemote
Copy link
Contributor

LastRemote commented Jan 22, 2025

Thanks for the ping. Unfortunately it does not help with my current problems with Langfuse (which are unrelated to this PR), but I do think this is a more elegant approach and a good step forward.

To be clear, I already implemented a slightly modified version of LangfuseTracer that is able to register new generators on the fly (this part can be done in a better way by using CustomSpanHandler) and automatically translate Anthropic / Bedrock-Anthropic usage to OpenAI format.

Things that trouble me right now, for a more transparent feedback:

  • LangfuseSpan.set_content_tag does some fancy translations to chat messages which allows pretty-printing in Langfuse. However this is causing all other inputs/output information to be dropped.
  • Context variables have a rather complicated interaction with Langfuse when deploying the pipeline as an API service.
  • Both Langfuse and Haystack have poor support for o1-like reasoning models regarding how to track and handle reasoning contents. This is a big problem and it is worth opening a new thread for this.
  • When there is a runtime error, Langfuse is not capturing any information. I think this is better handled by doing some try and except clauses in haystack pipeline executions. This PR partially fixes this problem.
  • [just a thought, not bugging me right now] Some components might need to store additional information in a trace, such as the intermediate steps in a complicated component. Currently there is no way to do this because the component does not have direct access to the tracing span, and we cannot handle the data if they are not captured in the first place.

@alex-stoica
Copy link
Contributor

Both Langfuse and Haystack has poor support for o1-like reasoning models regarding how to track and handle reasoning contents. This is a big problem and it is worth opening a new thread for this.

Totally agree with that. Also, the trend is towards more "LRM"s, whatever they are

@LastRemote
Copy link
Contributor

For reference I just created a new issue for LRMs: deepset-ai/haystack#8760

@vblagoje vblagoje marked this pull request as ready for review January 24, 2025 09:03
@vblagoje
Copy link
Member Author

vblagoje commented Jan 24, 2025

Thanks for the feedback @alex-stoica and @LastRemote. The PR is now opened and will go into review next week, with a new langfuse integration released shortly after - if all goes as planned. If you see anything else that can be adjusted now - let us know.

Copy link
Member

@anakin87 anakin87 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've taken a look and in principle I can't find anything wrong with the implementation....

I think we should mention this development in our docs. Can you take care of that?

Since I am not an expert on this part, another pair of eyes might be helpful (I would propose involving @mpangrazzi).

@@ -50,7 +52,7 @@ class LangfuseSpan(Span):
Internal class representing a bridge between the Haystack span tracing API and Langfuse.
"""

def __init__(self, span: "Union[langfuse.client.StatefulSpanClient, langfuse.client.StatefulTraceClient]") -> None:
def __init__(self, span: "langfuse.client.StatefulClient") -> None:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could you please explain this change?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for asking @anakin87 . It is a mistake! StatefulClient is a superclass of all these spans and one can create other spans from StatefulClient superclass API using generation/span methods but we need to keep the previous version because we also call update on these spans which is only declared in subclasses: StatefulSpanClient, StatefulTraceClient, and StatefulGenerationClient. I guess IDE/linters didn't pick these up because of forward references. I'll make corrections. 🙏

@mpangrazzi
Copy link
Contributor

I am also not a Langfuse expert too, but after looking at the why and the changes, I would say the look good to me.

I agree with @anakin87, we should definitely mention this additional feature in the docs.

@vblagoje
Copy link
Member Author

@dfokina I updated https://docs.haystack.deepset.ai/docs/langfuseconnector but can't somehow get the formatting right for the code snippet. It seems like Python option is gone from readme. Please investigate.

@anakin87 please review a743f15 pydoc additions. Daria and I will fix the doc to be just right.

@vblagoje vblagoje requested a review from anakin87 January 28, 2025 10:27
Copy link
Member

@anakin87 anakin87 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Feel free to merge once tests are green

@vblagoje
Copy link
Member Author

@anakin87 this change deserve a jump to 0.8.0 in https://pypi.org/project/langfuse-haystack/#history , thumbs up/down?

@vblagoje
Copy link
Member Author

@dfokina I also noticed that we should perhaps, in our LangfuseConnector doc, describe an additional init param:

httpx_client: Optional[httpx.Client] = None,

As this parameter has also been added recently. It is a small change to docs:

:param httpx_client: Optional custom httpx.Client instance to use for Langfuse API calls. Note that when
            deserializing a pipeline from YAML, any custom client is discarded and Langfuse will create its own default
            client, since HTTPX clients cannot be serialized.

Perhaps you can mention both httpx_client and span_handler params somehow. I'll let you do that in a way you see fit.

@vblagoje vblagoje merged commit 6883012 into main Jan 28, 2025
10 checks passed
@vblagoje vblagoje deleted the span_handler branch January 28, 2025 10:45
@vblagoje
Copy link
Member Author

vblagoje commented Jan 28, 2025

Fixed and included in https://pypi.org/project/langfuse-haystack/0.8.0/ thank you all @alex-stoica @LastRemote @lc-bo @marcklingen @davidsbatista @anakin87 @mpangrazzi @dfokina

@dfokina
Copy link
Contributor

dfokina commented Jan 28, 2025

Sorry for the delay @vblagoje. I fixed the code snippet and edited the text a bit. Will add that new param in a moment, too.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
integration:langfuse type:documentation Improvements or additions to documentation
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Add SpanHandler abstraction to enable custom LLM tracing in Langfuse
6 participants