langfuse-haystack 0.6.2__tar.gz → 0.6.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (18) hide show
  1. {langfuse_haystack-0.6.2 → langfuse_haystack-0.6.4}/CHANGELOG.md +8 -1
  2. {langfuse_haystack-0.6.2 → langfuse_haystack-0.6.4}/PKG-INFO +2 -2
  3. {langfuse_haystack-0.6.2 → langfuse_haystack-0.6.4}/pyproject.toml +1 -1
  4. {langfuse_haystack-0.6.2 → langfuse_haystack-0.6.4}/src/haystack_integrations/components/connectors/langfuse/langfuse_connector.py +47 -3
  5. {langfuse_haystack-0.6.2 → langfuse_haystack-0.6.4}/src/haystack_integrations/tracing/langfuse/tracer.py +2 -16
  6. {langfuse_haystack-0.6.2 → langfuse_haystack-0.6.4}/tests/test_tracing.py +76 -8
  7. {langfuse_haystack-0.6.2 → langfuse_haystack-0.6.4}/.gitignore +0 -0
  8. {langfuse_haystack-0.6.2 → langfuse_haystack-0.6.4}/LICENSE.txt +0 -0
  9. {langfuse_haystack-0.6.2 → langfuse_haystack-0.6.4}/README.md +0 -0
  10. {langfuse_haystack-0.6.2 → langfuse_haystack-0.6.4}/example/basic_rag.py +0 -0
  11. {langfuse_haystack-0.6.2 → langfuse_haystack-0.6.4}/example/chat.py +0 -0
  12. {langfuse_haystack-0.6.2 → langfuse_haystack-0.6.4}/example/requirements.txt +0 -0
  13. {langfuse_haystack-0.6.2 → langfuse_haystack-0.6.4}/pydoc/config.yml +0 -0
  14. {langfuse_haystack-0.6.2 → langfuse_haystack-0.6.4}/src/haystack_integrations/components/connectors/langfuse/__init__.py +0 -0
  15. {langfuse_haystack-0.6.2 → langfuse_haystack-0.6.4}/src/haystack_integrations/tracing/langfuse/__init__.py +0 -0
  16. {langfuse_haystack-0.6.2 → langfuse_haystack-0.6.4}/tests/__init__.py +0 -0
  17. {langfuse_haystack-0.6.2 → langfuse_haystack-0.6.4}/tests/test_langfuse_span.py +0 -0
  18. {langfuse_haystack-0.6.2 → langfuse_haystack-0.6.4}/tests/test_tracer.py +0 -0
@@ -1,6 +1,12 @@
1
1
  # Changelog
2
2
 
3
- ## [unreleased]
3
+ ## [integrations/langfuse-v0.6.3] - 2025-01-15
4
+
5
+ ### 🌀 Miscellaneous
6
+
7
+ - Chore: Langfuse - pin `haystack-ai>=2.9.0` and simplify message conversion (#1292)
8
+
9
+ ## [integrations/langfuse-v0.6.2] - 2025-01-02
4
10
 
5
11
  ### 🚀 Features
6
12
 
@@ -13,6 +19,7 @@
13
19
  ### 🌀 Miscellaneous
14
20
 
15
21
  - Chore: Fix tracing_context_var lint errors (#1220)
22
+ - Fix messages conversion to OpenAI format (#1272)
16
23
 
17
24
  ## [integrations/langfuse-v0.6.0] - 2024-11-18
18
25
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: langfuse-haystack
3
- Version: 0.6.2
3
+ Version: 0.6.4
4
4
  Project-URL: Documentation, https://github.com/deepset-ai/haystack-core-integrations/tree/main/integrations/langfuse#readme
5
5
  Project-URL: Issues, https://github.com/deepset-ai/haystack-core-integrations/issues
6
6
  Project-URL: Source, https://github.com/deepset-ai/haystack-core-integrations/tree/main/integrations/langfuse
@@ -17,7 +17,7 @@ Classifier: Programming Language :: Python :: 3.12
17
17
  Classifier: Programming Language :: Python :: Implementation :: CPython
18
18
  Classifier: Programming Language :: Python :: Implementation :: PyPy
19
19
  Requires-Python: >=3.8
20
- Requires-Dist: haystack-ai>=2.1.0
20
+ Requires-Dist: haystack-ai>=2.9.0
21
21
  Requires-Dist: langfuse
22
22
  Description-Content-Type: text/markdown
23
23
 
@@ -22,7 +22,7 @@ classifiers = [
22
22
  "Programming Language :: Python :: Implementation :: CPython",
23
23
  "Programming Language :: Python :: Implementation :: PyPy",
24
24
  ]
25
- dependencies = ["haystack-ai>=2.1.0", "langfuse"]
25
+ dependencies = ["haystack-ai>=2.9.0", "langfuse"]
26
26
 
27
27
  [project.urls]
28
28
  Documentation = "https://github.com/deepset-ai/haystack-core-integrations/tree/main/integrations/langfuse#readme"
@@ -1,6 +1,7 @@
1
1
  from typing import Any, Dict, Optional
2
2
 
3
- from haystack import component, logging, tracing
3
+ from haystack import component, default_from_dict, default_to_dict, logging, tracing
4
+ from haystack.utils import Secret, deserialize_secrets_inplace
4
5
 
5
6
  from haystack_integrations.tracing.langfuse import LangfuseTracer
6
7
  from langfuse import Langfuse
@@ -94,7 +95,13 @@ class LangfuseConnector:
94
95
 
95
96
  """
96
97
 
97
- def __init__(self, name: str, public: bool = False):
98
+ def __init__(
99
+ self,
100
+ name: str,
101
+ public: bool = False,
102
+ public_key: Optional[Secret] = Secret.from_env_var("LANGFUSE_PUBLIC_KEY"), # noqa: B008
103
+ secret_key: Optional[Secret] = Secret.from_env_var("LANGFUSE_SECRET_KEY"), # noqa: B008
104
+ ):
98
105
  """
99
106
  Initialize the LangfuseConnector component.
100
107
 
@@ -103,9 +110,21 @@ class LangfuseConnector:
103
110
  :param public: Whether the tracing data should be public or private. If set to `True`, the tracing data will be
104
111
  publicly accessible to anyone with the tracing URL. If set to `False`, the tracing data will be private and
105
112
  only accessible to the Langfuse account owner. The default is `False`.
113
+ :param public_key: The Langfuse public key. Defaults to reading from LANGFUSE_PUBLIC_KEY environment variable.
114
+ :param secret_key: The Langfuse secret key. Defaults to reading from LANGFUSE_SECRET_KEY environment variable.
106
115
  """
107
116
  self.name = name
108
- self.tracer = LangfuseTracer(tracer=Langfuse(), name=name, public=public)
117
+ self.public = public
118
+ self.secret_key = secret_key
119
+ self.public_key = public_key
120
+ self.tracer = LangfuseTracer(
121
+ tracer=Langfuse(
122
+ secret_key=secret_key.resolve_value() if secret_key else None,
123
+ public_key=public_key.resolve_value() if public_key else None,
124
+ ),
125
+ name=name,
126
+ public=public,
127
+ )
109
128
  tracing.enable_tracing(self.tracer)
110
129
 
111
130
  @component.output_types(name=str, trace_url=str)
@@ -126,3 +145,28 @@ class LangfuseConnector:
126
145
  invocation_context=invocation_context,
127
146
  )
128
147
  return {"name": self.name, "trace_url": self.tracer.get_trace_url()}
148
+
149
+ def to_dict(self) -> Dict[str, Any]:
150
+ """
151
+ Serialize this component to a dictionary.
152
+
153
+ :returns: The serialized component as a dictionary.
154
+ """
155
+ return default_to_dict(
156
+ self,
157
+ name=self.name,
158
+ public=self.public,
159
+ secret_key=self.secret_key.to_dict() if self.secret_key else None,
160
+ public_key=self.public_key.to_dict() if self.public_key else None,
161
+ )
162
+
163
+ @classmethod
164
+ def from_dict(cls, data: Dict[str, Any]) -> "LangfuseConnector":
165
+ """
166
+ Deserialize this component from a dictionary.
167
+
168
+ :param data: The dictionary representation of this component.
169
+ :returns: The deserialized component instance.
170
+ """
171
+ deserialize_secrets_inplace(data["init_parameters"], keys=["secret_key", "public_key"])
172
+ return default_from_dict(cls, data)
@@ -6,16 +6,12 @@ from typing import Any, Dict, Iterator, List, Optional, Union
6
6
 
7
7
  from haystack import logging
8
8
  from haystack.dataclasses import ChatMessage
9
- from haystack.lazy_imports import LazyImport
10
9
  from haystack.tracing import Span, Tracer
11
10
  from haystack.tracing import tracer as proxy_tracer
12
11
  from haystack.tracing import utils as tracing_utils
13
12
 
14
13
  import langfuse
15
14
 
16
- with LazyImport("") as openai_utils_import:
17
- from haystack.components.generators.openai_utils import _convert_message_to_openai_format
18
-
19
15
  logger = logging.getLogger(__name__)
20
16
 
21
17
  HAYSTACK_LANGFUSE_ENFORCE_FLUSH_ENV_VAR = "HAYSTACK_LANGFUSE_ENFORCE_FLUSH"
@@ -86,24 +82,14 @@ class LangfuseSpan(Span):
86
82
  return
87
83
  if key.endswith(".input"):
88
84
  if "messages" in value:
89
- if openai_utils_import.is_successful():
90
- # Haystack < 2.9.0
91
- messages = [_convert_message_to_openai_format(m) for m in value["messages"]]
92
- else:
93
- # Haystack >= 2.9.0
94
- messages = [m.to_openai_dict_format() for m in value["messages"]]
85
+ messages = [m.to_openai_dict_format() for m in value["messages"]]
95
86
  self._span.update(input=messages)
96
87
  else:
97
88
  self._span.update(input=value)
98
89
  elif key.endswith(".output"):
99
90
  if "replies" in value:
100
91
  if all(isinstance(r, ChatMessage) for r in value["replies"]):
101
- if openai_utils_import.is_successful():
102
- # Haystack < 2.9.0
103
- replies = [_convert_message_to_openai_format(m) for m in value["replies"]]
104
- else:
105
- # Haystack >= 2.9.0
106
- replies = [m.to_openai_dict_format() for m in value["replies"]]
92
+ replies = [m.to_openai_dict_format() for m in value["replies"]]
107
93
  else:
108
94
  replies = value["replies"]
109
95
  self._span.update(output=replies)
@@ -1,5 +1,4 @@
1
1
  import os
2
- import random
3
2
  import time
4
3
  from urllib.parse import urlparse
5
4
 
@@ -9,6 +8,7 @@ from haystack import Pipeline
9
8
  from haystack.components.builders import ChatPromptBuilder
10
9
  from haystack.components.generators.chat import OpenAIChatGenerator
11
10
  from haystack.dataclasses import ChatMessage
11
+ from haystack.utils import Secret
12
12
  from requests.auth import HTTPBasicAuth
13
13
 
14
14
  from haystack_integrations.components.connectors.langfuse import LangfuseConnector
@@ -19,6 +19,36 @@ from haystack_integrations.components.generators.cohere import CohereChatGenerat
19
19
  os.environ["HAYSTACK_CONTENT_TRACING_ENABLED"] = "true"
20
20
 
21
21
 
22
+ @pytest.fixture
23
+ def pipeline_with_env_vars(llm_class, expected_trace):
24
+ """Pipeline factory using environment variables for Langfuse authentication"""
25
+ pipe = Pipeline()
26
+ pipe.add_component("tracer", LangfuseConnector(name=f"Chat example - {expected_trace}", public=True))
27
+ pipe.add_component("prompt_builder", ChatPromptBuilder())
28
+ pipe.add_component("llm", llm_class())
29
+ pipe.connect("prompt_builder.prompt", "llm.messages")
30
+ return pipe
31
+
32
+
33
+ @pytest.fixture
34
+ def pipeline_with_secrets(llm_class, expected_trace):
35
+ """Pipeline factory using Secret objects for Langfuse authentication"""
36
+ pipe = Pipeline()
37
+ pipe.add_component(
38
+ "tracer",
39
+ LangfuseConnector(
40
+ name=f"Chat example - {expected_trace}",
41
+ public=True,
42
+ secret_key=Secret.from_env_var("LANGFUSE_SECRET_KEY"),
43
+ public_key=Secret.from_env_var("LANGFUSE_PUBLIC_KEY"),
44
+ ),
45
+ )
46
+ pipe.add_component("prompt_builder", ChatPromptBuilder())
47
+ pipe.add_component("llm", llm_class())
48
+ pipe.connect("prompt_builder.prompt", "llm.messages")
49
+ return pipe
50
+
51
+
22
52
  @pytest.mark.integration
23
53
  @pytest.mark.parametrize(
24
54
  "llm_class, env_var, expected_trace",
@@ -28,16 +58,12 @@ os.environ["HAYSTACK_CONTENT_TRACING_ENABLED"] = "true"
28
58
  (CohereChatGenerator, "COHERE_API_KEY", "Cohere"),
29
59
  ],
30
60
  )
31
- def test_tracing_integration(llm_class, env_var, expected_trace):
61
+ @pytest.mark.parametrize("pipeline_fixture", ["pipeline_with_env_vars", "pipeline_with_secrets"])
62
+ def test_tracing_integration(llm_class, env_var, expected_trace, pipeline_fixture, request):
32
63
  if not all([os.environ.get("LANGFUSE_SECRET_KEY"), os.environ.get("LANGFUSE_PUBLIC_KEY"), os.environ.get(env_var)]):
33
64
  pytest.skip(f"Missing required environment variables: LANGFUSE_SECRET_KEY, LANGFUSE_PUBLIC_KEY, or {env_var}")
34
65
 
35
- pipe = Pipeline()
36
- pipe.add_component("tracer", LangfuseConnector(name=f"Chat example - {expected_trace}", public=True))
37
- pipe.add_component("prompt_builder", ChatPromptBuilder())
38
- pipe.add_component("llm", llm_class())
39
- pipe.connect("prompt_builder.prompt", "llm.messages")
40
-
66
+ pipe = request.getfixturevalue(pipeline_fixture)
41
67
  messages = [
42
68
  ChatMessage.from_system("Always respond in German even if some input data is in other languages."),
43
69
  ChatMessage.from_user("Tell me about {{location}}"),
@@ -77,3 +103,45 @@ def test_tracing_integration(llm_class, env_var, expected_trace):
77
103
  # check if the trace contains the expected user_id
78
104
  assert "user_42" in str(res.content)
79
105
  break
106
+
107
+
108
+ def test_pipeline_serialization(monkeypatch):
109
+ """Test that a pipeline with secrets can be properly serialized and deserialized"""
110
+
111
+ # Set test env vars
112
+ monkeypatch.setenv("LANGFUSE_SECRET_KEY", "secret")
113
+ monkeypatch.setenv("LANGFUSE_PUBLIC_KEY", "public")
114
+ monkeypatch.setenv("OPENAI_API_KEY", "openai_api_key")
115
+
116
+ # Create pipeline with OpenAI LLM
117
+ pipe = Pipeline()
118
+ pipe.add_component(
119
+ "tracer",
120
+ LangfuseConnector(
121
+ name="Chat example - OpenAI",
122
+ public=True,
123
+ secret_key=Secret.from_env_var("LANGFUSE_SECRET_KEY"),
124
+ public_key=Secret.from_env_var("LANGFUSE_PUBLIC_KEY"),
125
+ ),
126
+ )
127
+ pipe.add_component("prompt_builder", ChatPromptBuilder())
128
+ pipe.add_component("llm", OpenAIChatGenerator())
129
+ pipe.connect("prompt_builder.prompt", "llm.messages")
130
+
131
+ # Serialize
132
+ serialized = pipe.to_dict()
133
+
134
+ # Check serialized secrets
135
+ tracer_params = serialized["components"]["tracer"]["init_parameters"]
136
+ assert isinstance(tracer_params["secret_key"], dict)
137
+ assert tracer_params["secret_key"]["type"] == "env_var"
138
+ assert tracer_params["secret_key"]["env_vars"] == ["LANGFUSE_SECRET_KEY"]
139
+ assert isinstance(tracer_params["public_key"], dict)
140
+ assert tracer_params["public_key"]["type"] == "env_var"
141
+ assert tracer_params["public_key"]["env_vars"] == ["LANGFUSE_PUBLIC_KEY"]
142
+
143
+ # Deserialize
144
+ new_pipe = Pipeline.from_dict(serialized)
145
+
146
+ # Verify pipeline is the same
147
+ assert new_pipe == pipe