langfuse-haystack 0.6.3__tar.gz → 0.7.0__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 (19) hide show
  1. {langfuse_haystack-0.6.3 → langfuse_haystack-0.7.0}/CHANGELOG.md +13 -0
  2. {langfuse_haystack-0.6.3 → langfuse_haystack-0.7.0}/PKG-INFO +1 -1
  3. {langfuse_haystack-0.6.3 → langfuse_haystack-0.7.0}/pyproject.toml +7 -1
  4. {langfuse_haystack-0.6.3 → langfuse_haystack-0.7.0}/src/haystack_integrations/components/connectors/langfuse/langfuse_connector.py +54 -3
  5. {langfuse_haystack-0.6.3 → langfuse_haystack-0.7.0}/src/haystack_integrations/tracing/langfuse/tracer.py +5 -1
  6. langfuse_haystack-0.7.0/tests/test_tracing.py +171 -0
  7. langfuse_haystack-0.6.3/tests/test_tracing.py +0 -79
  8. {langfuse_haystack-0.6.3 → langfuse_haystack-0.7.0}/.gitignore +0 -0
  9. {langfuse_haystack-0.6.3 → langfuse_haystack-0.7.0}/LICENSE.txt +0 -0
  10. {langfuse_haystack-0.6.3 → langfuse_haystack-0.7.0}/README.md +0 -0
  11. {langfuse_haystack-0.6.3 → langfuse_haystack-0.7.0}/example/basic_rag.py +0 -0
  12. {langfuse_haystack-0.6.3 → langfuse_haystack-0.7.0}/example/chat.py +0 -0
  13. {langfuse_haystack-0.6.3 → langfuse_haystack-0.7.0}/example/requirements.txt +0 -0
  14. {langfuse_haystack-0.6.3 → langfuse_haystack-0.7.0}/pydoc/config.yml +0 -0
  15. {langfuse_haystack-0.6.3 → langfuse_haystack-0.7.0}/src/haystack_integrations/components/connectors/langfuse/__init__.py +0 -0
  16. {langfuse_haystack-0.6.3 → langfuse_haystack-0.7.0}/src/haystack_integrations/tracing/langfuse/__init__.py +0 -0
  17. {langfuse_haystack-0.6.3 → langfuse_haystack-0.7.0}/tests/__init__.py +0 -0
  18. {langfuse_haystack-0.6.3 → langfuse_haystack-0.7.0}/tests/test_langfuse_span.py +0 -0
  19. {langfuse_haystack-0.6.3 → langfuse_haystack-0.7.0}/tests/test_tracer.py +0 -0
@@ -1,5 +1,18 @@
1
1
  # Changelog
2
2
 
3
+ ## [integrations/langfuse-v0.6.4] - 2025-01-17
4
+
5
+ ### 🚀 Features
6
+
7
+ - Add LangfuseConnector secure key management and serialization (#1287)
8
+
9
+
10
+ ## [integrations/langfuse-v0.6.3] - 2025-01-15
11
+
12
+ ### 🌀 Miscellaneous
13
+
14
+ - Chore: Langfuse - pin `haystack-ai>=2.9.0` and simplify message conversion (#1292)
15
+
3
16
  ## [integrations/langfuse-v0.6.2] - 2025-01-02
4
17
 
5
18
  ### 🚀 Features
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: langfuse-haystack
3
- Version: 0.6.3
3
+ Version: 0.7.0
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
@@ -67,7 +67,12 @@ python = ["3.8", "3.9", "3.10", "3.11", "3.12"]
67
67
  [tool.hatch.envs.lint]
68
68
  installer = "uv"
69
69
  detached = true
70
- dependencies = ["pip", "black>=23.1.0", "mypy>=1.0.0", "ruff>=0.0.243"]
70
+ dependencies = [
71
+ "pip",
72
+ "black>=23.1.0",
73
+ "mypy>=1.0.0",
74
+ "ruff>=0.0.243",
75
+ ]
71
76
 
72
77
  [tool.hatch.envs.lint.scripts]
73
78
  typing = "mypy --install-types --non-interactive --explicit-package-bases {args:src/ tests}"
@@ -167,6 +172,7 @@ module = [
167
172
  "haystack_integrations.*",
168
173
  "pytest.*",
169
174
  "numpy.*",
175
+ "httpx.*",
170
176
  ]
171
177
  ignore_missing_imports = true
172
178
 
@@ -1,6 +1,8 @@
1
1
  from typing import Any, Dict, Optional
2
2
 
3
- from haystack import component, logging, tracing
3
+ import httpx
4
+ from haystack import component, default_from_dict, default_to_dict, logging, tracing
5
+ from haystack.utils import Secret, deserialize_secrets_inplace
4
6
 
5
7
  from haystack_integrations.tracing.langfuse import LangfuseTracer
6
8
  from langfuse import Langfuse
@@ -94,7 +96,14 @@ class LangfuseConnector:
94
96
 
95
97
  """
96
98
 
97
- def __init__(self, name: str, public: bool = False):
99
+ def __init__(
100
+ self,
101
+ name: str,
102
+ public: bool = False,
103
+ public_key: Optional[Secret] = Secret.from_env_var("LANGFUSE_PUBLIC_KEY"), # noqa: B008
104
+ secret_key: Optional[Secret] = Secret.from_env_var("LANGFUSE_SECRET_KEY"), # noqa: B008
105
+ httpx_client: Optional[httpx.Client] = None,
106
+ ):
98
107
  """
99
108
  Initialize the LangfuseConnector component.
100
109
 
@@ -103,9 +112,25 @@ class LangfuseConnector:
103
112
  :param public: Whether the tracing data should be public or private. If set to `True`, the tracing data will be
104
113
  publicly accessible to anyone with the tracing URL. If set to `False`, the tracing data will be private and
105
114
  only accessible to the Langfuse account owner. The default is `False`.
115
+ :param public_key: The Langfuse public key. Defaults to reading from LANGFUSE_PUBLIC_KEY environment variable.
116
+ :param secret_key: The Langfuse secret key. Defaults to reading from LANGFUSE_SECRET_KEY environment variable.
117
+ :param httpx_client: Optional custom httpx.Client instance to use for Langfuse API calls. Note that when
118
+ deserializing a pipeline from YAML, any custom client is discarded and Langfuse will create its own default
119
+ client, since HTTPX clients cannot be serialized.
106
120
  """
107
121
  self.name = name
108
- self.tracer = LangfuseTracer(tracer=Langfuse(), name=name, public=public)
122
+ self.public = public
123
+ self.secret_key = secret_key
124
+ self.public_key = public_key
125
+ self.tracer = LangfuseTracer(
126
+ tracer=Langfuse(
127
+ secret_key=secret_key.resolve_value() if secret_key else None,
128
+ public_key=public_key.resolve_value() if public_key else None,
129
+ httpx_client=httpx_client,
130
+ ),
131
+ name=name,
132
+ public=public,
133
+ )
109
134
  tracing.enable_tracing(self.tracer)
110
135
 
111
136
  @component.output_types(name=str, trace_url=str)
@@ -126,3 +151,29 @@ class LangfuseConnector:
126
151
  invocation_context=invocation_context,
127
152
  )
128
153
  return {"name": self.name, "trace_url": self.tracer.get_trace_url()}
154
+
155
+ def to_dict(self) -> Dict[str, Any]:
156
+ """
157
+ Serialize this component to a dictionary.
158
+
159
+ :returns: The serialized component as a dictionary.
160
+ """
161
+ return default_to_dict(
162
+ self,
163
+ name=self.name,
164
+ public=self.public,
165
+ secret_key=self.secret_key.to_dict() if self.secret_key else None,
166
+ public_key=self.public_key.to_dict() if self.public_key else None,
167
+ # Note: httpx_client is not serialized as it's not serializable
168
+ )
169
+
170
+ @classmethod
171
+ def from_dict(cls, data: Dict[str, Any]) -> "LangfuseConnector":
172
+ """
173
+ Deserialize this component from a dictionary.
174
+
175
+ :param data: The dictionary representation of this component.
176
+ :returns: The deserialized component instance.
177
+ """
178
+ deserialize_secrets_inplace(data["init_parameters"], keys=["secret_key", "public_key"])
179
+ return default_from_dict(cls, data)
@@ -201,7 +201,11 @@ class LangfuseTracer(Tracer):
201
201
  )
202
202
 
203
203
  raw_span = span.raw_span()
204
- if isinstance(raw_span, langfuse.client.StatefulSpanClient):
204
+
205
+ # In this section, we finalize both regular spans and generation spans created using the LangfuseSpan class.
206
+ # It's important to end() these spans to ensure they are properly closed and all relevant data is recorded.
207
+ # Note that we do not call end() on the main trace span itself, as its lifecycle is managed differently.
208
+ if isinstance(raw_span, (langfuse.client.StatefulSpanClient, langfuse.client.StatefulGenerationClient)):
205
209
  raw_span.end()
206
210
  self._context.pop()
207
211
 
@@ -0,0 +1,171 @@
1
+ import os
2
+ import time
3
+ from urllib.parse import urlparse
4
+
5
+ import pytest
6
+ import requests
7
+ from haystack import Pipeline
8
+ from haystack.components.builders import ChatPromptBuilder
9
+ from haystack.components.generators.chat import OpenAIChatGenerator
10
+ from haystack.dataclasses import ChatMessage
11
+ from haystack.utils import Secret
12
+ from requests.auth import HTTPBasicAuth
13
+ import httpx
14
+
15
+ from haystack_integrations.components.connectors.langfuse import LangfuseConnector
16
+ from haystack_integrations.components.generators.anthropic import AnthropicChatGenerator
17
+ from haystack_integrations.components.generators.cohere import CohereChatGenerator
18
+
19
+ # don't remove (or move) this env var setting from here, it's needed to turn tracing on
20
+ os.environ["HAYSTACK_CONTENT_TRACING_ENABLED"] = "true"
21
+
22
+
23
+ @pytest.fixture
24
+ def pipeline_with_env_vars(llm_class, expected_trace):
25
+ """Pipeline factory using environment variables for Langfuse authentication"""
26
+ pipe = Pipeline()
27
+ pipe.add_component("tracer", LangfuseConnector(name=f"Chat example - {expected_trace}", public=True))
28
+ pipe.add_component("prompt_builder", ChatPromptBuilder())
29
+ pipe.add_component("llm", llm_class())
30
+ pipe.connect("prompt_builder.prompt", "llm.messages")
31
+ return pipe
32
+
33
+
34
+ @pytest.fixture
35
+ def pipeline_with_secrets(llm_class, expected_trace):
36
+ """Pipeline factory using Secret objects for Langfuse authentication"""
37
+ pipe = Pipeline()
38
+ pipe.add_component(
39
+ "tracer",
40
+ LangfuseConnector(
41
+ name=f"Chat example - {expected_trace}",
42
+ public=True,
43
+ secret_key=Secret.from_env_var("LANGFUSE_SECRET_KEY"),
44
+ public_key=Secret.from_env_var("LANGFUSE_PUBLIC_KEY"),
45
+ ),
46
+ )
47
+ pipe.add_component("prompt_builder", ChatPromptBuilder())
48
+ pipe.add_component("llm", llm_class())
49
+ pipe.connect("prompt_builder.prompt", "llm.messages")
50
+ return pipe
51
+
52
+
53
+ @pytest.fixture
54
+ def pipeline_with_custom_client(llm_class, expected_trace):
55
+ """Pipeline factory using custom httpx client for Langfuse"""
56
+ pipe = Pipeline()
57
+ custom_client = httpx.Client(timeout=30.0) # Custom timeout of 30 seconds
58
+ pipe.add_component(
59
+ "tracer",
60
+ LangfuseConnector(
61
+ name=f"Chat example - {expected_trace}",
62
+ public=True,
63
+ secret_key=Secret.from_env_var("LANGFUSE_SECRET_KEY"),
64
+ public_key=Secret.from_env_var("LANGFUSE_PUBLIC_KEY"),
65
+ httpx_client=custom_client,
66
+ ),
67
+ )
68
+ pipe.add_component("prompt_builder", ChatPromptBuilder())
69
+ pipe.add_component("llm", llm_class())
70
+ pipe.connect("prompt_builder.prompt", "llm.messages")
71
+ return pipe
72
+
73
+
74
+ @pytest.mark.integration
75
+ @pytest.mark.parametrize(
76
+ "llm_class, env_var, expected_trace",
77
+ [
78
+ (OpenAIChatGenerator, "OPENAI_API_KEY", "OpenAI"),
79
+ (AnthropicChatGenerator, "ANTHROPIC_API_KEY", "Anthropic"),
80
+ (CohereChatGenerator, "COHERE_API_KEY", "Cohere"),
81
+ ],
82
+ )
83
+ @pytest.mark.parametrize(
84
+ "pipeline_fixture", ["pipeline_with_env_vars", "pipeline_with_secrets", "pipeline_with_custom_client"]
85
+ )
86
+ def test_tracing_integration(llm_class, env_var, expected_trace, pipeline_fixture, request):
87
+ if not all([os.environ.get("LANGFUSE_SECRET_KEY"), os.environ.get("LANGFUSE_PUBLIC_KEY"), os.environ.get(env_var)]):
88
+ pytest.skip(f"Missing required environment variables: LANGFUSE_SECRET_KEY, LANGFUSE_PUBLIC_KEY, or {env_var}")
89
+
90
+ pipe = request.getfixturevalue(pipeline_fixture)
91
+ messages = [
92
+ ChatMessage.from_system("Always respond in German even if some input data is in other languages."),
93
+ ChatMessage.from_user("Tell me about {{location}}"),
94
+ ]
95
+
96
+ response = pipe.run(
97
+ data={
98
+ "prompt_builder": {"template_variables": {"location": "Berlin"}, "template": messages},
99
+ "tracer": {"invocation_context": {"user_id": "user_42"}},
100
+ }
101
+ )
102
+ assert "Berlin" in response["llm"]["replies"][0].text
103
+ assert response["tracer"]["trace_url"]
104
+
105
+ trace_url = response["tracer"]["trace_url"]
106
+ uuid = os.path.basename(urlparse(trace_url).path)
107
+ url = f"https://cloud.langfuse.com/api/public/traces/{uuid}"
108
+
109
+ # Poll the Langfuse API a bit as the trace might not be ready right away
110
+ attempts = 5
111
+ delay = 1
112
+ while attempts >= 0:
113
+ res = requests.get(
114
+ url, auth=HTTPBasicAuth(os.environ["LANGFUSE_PUBLIC_KEY"], os.environ["LANGFUSE_SECRET_KEY"])
115
+ )
116
+ if attempts > 0 and res.status_code != 200:
117
+ attempts -= 1
118
+ time.sleep(delay)
119
+ delay *= 2
120
+ continue
121
+ assert res.status_code == 200, f"Failed to retrieve data from Langfuse API: {res.status_code}"
122
+
123
+ # check if the trace contains the expected LLM name
124
+ assert expected_trace in str(res.content)
125
+ # check if the trace contains the expected generation span
126
+ assert "GENERATION" in str(res.content)
127
+ # check if the trace contains the expected user_id
128
+ assert "user_42" in str(res.content)
129
+ break
130
+
131
+
132
+ def test_pipeline_serialization(monkeypatch):
133
+ """Test that a pipeline with secrets can be properly serialized and deserialized"""
134
+
135
+ # Set test env vars
136
+ monkeypatch.setenv("LANGFUSE_SECRET_KEY", "secret")
137
+ monkeypatch.setenv("LANGFUSE_PUBLIC_KEY", "public")
138
+ monkeypatch.setenv("OPENAI_API_KEY", "openai_api_key")
139
+
140
+ # Create pipeline with OpenAI LLM
141
+ pipe = Pipeline()
142
+ pipe.add_component(
143
+ "tracer",
144
+ LangfuseConnector(
145
+ name="Chat example - OpenAI",
146
+ public=True,
147
+ secret_key=Secret.from_env_var("LANGFUSE_SECRET_KEY"),
148
+ public_key=Secret.from_env_var("LANGFUSE_PUBLIC_KEY"),
149
+ ),
150
+ )
151
+ pipe.add_component("prompt_builder", ChatPromptBuilder())
152
+ pipe.add_component("llm", OpenAIChatGenerator())
153
+ pipe.connect("prompt_builder.prompt", "llm.messages")
154
+
155
+ # Serialize
156
+ serialized = pipe.to_dict()
157
+
158
+ # Check serialized secrets
159
+ tracer_params = serialized["components"]["tracer"]["init_parameters"]
160
+ assert isinstance(tracer_params["secret_key"], dict)
161
+ assert tracer_params["secret_key"]["type"] == "env_var"
162
+ assert tracer_params["secret_key"]["env_vars"] == ["LANGFUSE_SECRET_KEY"]
163
+ assert isinstance(tracer_params["public_key"], dict)
164
+ assert tracer_params["public_key"]["type"] == "env_var"
165
+ assert tracer_params["public_key"]["env_vars"] == ["LANGFUSE_PUBLIC_KEY"]
166
+
167
+ # Deserialize
168
+ new_pipe = Pipeline.from_dict(serialized)
169
+
170
+ # Verify pipeline is the same
171
+ assert new_pipe == pipe
@@ -1,79 +0,0 @@
1
- import os
2
- import random
3
- import time
4
- from urllib.parse import urlparse
5
-
6
- import pytest
7
- import requests
8
- from haystack import Pipeline
9
- from haystack.components.builders import ChatPromptBuilder
10
- from haystack.components.generators.chat import OpenAIChatGenerator
11
- from haystack.dataclasses import ChatMessage
12
- from requests.auth import HTTPBasicAuth
13
-
14
- from haystack_integrations.components.connectors.langfuse import LangfuseConnector
15
- from haystack_integrations.components.generators.anthropic import AnthropicChatGenerator
16
- from haystack_integrations.components.generators.cohere import CohereChatGenerator
17
-
18
- # don't remove (or move) this env var setting from here, it's needed to turn tracing on
19
- os.environ["HAYSTACK_CONTENT_TRACING_ENABLED"] = "true"
20
-
21
-
22
- @pytest.mark.integration
23
- @pytest.mark.parametrize(
24
- "llm_class, env_var, expected_trace",
25
- [
26
- (OpenAIChatGenerator, "OPENAI_API_KEY", "OpenAI"),
27
- (AnthropicChatGenerator, "ANTHROPIC_API_KEY", "Anthropic"),
28
- (CohereChatGenerator, "COHERE_API_KEY", "Cohere"),
29
- ],
30
- )
31
- def test_tracing_integration(llm_class, env_var, expected_trace):
32
- if not all([os.environ.get("LANGFUSE_SECRET_KEY"), os.environ.get("LANGFUSE_PUBLIC_KEY"), os.environ.get(env_var)]):
33
- pytest.skip(f"Missing required environment variables: LANGFUSE_SECRET_KEY, LANGFUSE_PUBLIC_KEY, or {env_var}")
34
-
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
-
41
- messages = [
42
- ChatMessage.from_system("Always respond in German even if some input data is in other languages."),
43
- ChatMessage.from_user("Tell me about {{location}}"),
44
- ]
45
-
46
- response = pipe.run(
47
- data={
48
- "prompt_builder": {"template_variables": {"location": "Berlin"}, "template": messages},
49
- "tracer": {"invocation_context": {"user_id": "user_42"}},
50
- }
51
- )
52
- assert "Berlin" in response["llm"]["replies"][0].text
53
- assert response["tracer"]["trace_url"]
54
-
55
- trace_url = response["tracer"]["trace_url"]
56
- uuid = os.path.basename(urlparse(trace_url).path)
57
- url = f"https://cloud.langfuse.com/api/public/traces/{uuid}"
58
-
59
- # Poll the Langfuse API a bit as the trace might not be ready right away
60
- attempts = 5
61
- delay = 1
62
- while attempts >= 0:
63
- res = requests.get(
64
- url, auth=HTTPBasicAuth(os.environ["LANGFUSE_PUBLIC_KEY"], os.environ["LANGFUSE_SECRET_KEY"])
65
- )
66
- if attempts > 0 and res.status_code != 200:
67
- attempts -= 1
68
- time.sleep(delay)
69
- delay *= 2
70
- continue
71
- assert res.status_code == 200, f"Failed to retrieve data from Langfuse API: {res.status_code}"
72
-
73
- # check if the trace contains the expected LLM name
74
- assert expected_trace in str(res.content)
75
- # check if the trace contains the expected generation span
76
- assert "GENERATION" in str(res.content)
77
- # check if the trace contains the expected user_id
78
- assert "user_42" in str(res.content)
79
- break