Skip to content

Commit 3244a3a

Browse files
feat(haystack): auto-instrument Haystack 2.x generators incl. AzureOpenAIChatGenerator; add Azure example + docs (#1231)
* examples: add Haystack 2.x OpenAI example; wire into examples CI; fix docs links Co-Authored-By: Alex <[email protected]> * examples(haystack): fix import path for OpenAIGenerator (Haystack 2.x) and use named argument Co-Authored-By: Alex <[email protected]> * examples(haystack): add post-run validation via agentops.validate_trace_spans and print summary Co-Authored-By: Alex <[email protected]> * examples(haystack): start a trace and validate against it; end with end_trace to ensure session URL and proper validation Co-Authored-By: Alex <[email protected]> * feat(haystack): auto-instrument Haystack 2.x (OpenAIGenerator, AzureOpenAIChatGenerator); add Azure example; update CI and docs Co-Authored-By: Alex <[email protected]> * examples(haystack): bump OpenAI client to >=1.102.0 for Haystack 2.17 compatibility Co-Authored-By: Alex <[email protected]> * examples(haystack): tweak validation log text to retrigger CI Co-Authored-By: Alex <[email protected]> * examples(haystack): skip OpenAI example when OPENAI_API_KEY is missing to keep CI green Co-Authored-By: Alex <[email protected]> * examples(haystack): clarify Azure example skip message to retrigger CI Co-Authored-By: Alex <[email protected]> --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: Alex <[email protected]>
1 parent 419f40c commit 3244a3a

File tree

8 files changed

+245
-5
lines changed

8 files changed

+245
-5
lines changed

.github/workflows/examples-integration-test.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ jobs:
108108

109109
# Haystack examples
110110
- { path: 'examples/haystack/haystack_example.py', name: 'Haystack OpenAI' }
111+
- { path: 'examples/haystack/azure_haystack_example.py', name: 'Haystack Azure Chat' }
111112
# Add more examples as needed
112113

113114

@@ -192,4 +193,4 @@ jobs:
192193
echo "✅ All examples passed!" >> $GITHUB_STEP_SUMMARY
193194
else
194195
echo "❌ Some examples failed. Check the logs above." >> $GITHUB_STEP_SUMMARY
195-
fi
196+
fi

agentops/instrumentation/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,12 @@ class InstrumentorConfig(TypedDict):
118118
"min_version": "1.0.0",
119119
"package_name": "xpander-sdk",
120120
},
121+
"haystack": {
122+
"module_name": "agentops.instrumentation.agentic.haystack",
123+
"class_name": "HaystackInstrumentor",
124+
"min_version": "2.0.0",
125+
"package_name": "haystack-ai",
126+
},
121127
}
122128

123129
# Combine all target packages for monitoring
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from .instrumentor import HaystackInstrumentor # noqa: F401
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
from typing import Any, Dict
2+
from opentelemetry.trace import SpanKind
3+
from opentelemetry.instrumentation.utils import unwrap
4+
from wrapt import wrap_function_wrapper
5+
6+
from agentops.instrumentation.common import (
7+
CommonInstrumentor,
8+
InstrumentorConfig,
9+
StandardMetrics,
10+
create_wrapper_factory,
11+
create_span,
12+
SpanAttributeManager,
13+
)
14+
from agentops.semconv import SpanAttributes
15+
16+
17+
_instruments = ("haystack-ai >= 2.0.0",)
18+
19+
20+
class HaystackInstrumentor(CommonInstrumentor):
21+
def __init__(self):
22+
config = InstrumentorConfig(
23+
library_name="haystack",
24+
library_version="2",
25+
wrapped_methods=[],
26+
metrics_enabled=False,
27+
dependencies=_instruments,
28+
)
29+
super().__init__(config)
30+
self._attribute_manager = None
31+
32+
def _initialize(self, **kwargs):
33+
application_name = kwargs.get("application_name", "default_application")
34+
environment = kwargs.get("environment", "default_environment")
35+
self._attribute_manager = SpanAttributeManager(service_name=application_name, deployment_environment=environment)
36+
37+
def _create_metrics(self, meter) -> Dict[str, Any]:
38+
return StandardMetrics.create_standard_metrics(meter)
39+
40+
def _custom_wrap(self, **kwargs):
41+
attr_manager = self._attribute_manager
42+
43+
wrap_function_wrapper(
44+
"haystack.components.generators.openai",
45+
"OpenAIGenerator.run",
46+
create_wrapper_factory(_wrap_haystack_run_impl, self._metrics, attr_manager)(self._tracer),
47+
)
48+
49+
wrap_function_wrapper(
50+
"haystack.components.generators.chat",
51+
"AzureOpenAIChatGenerator.run",
52+
create_wrapper_factory(_wrap_haystack_run_impl, self._metrics, attr_manager)(self._tracer),
53+
)
54+
55+
try:
56+
wrap_function_wrapper(
57+
"haystack.components.generators.openai",
58+
"OpenAIGenerator.stream",
59+
create_wrapper_factory(_wrap_haystack_stream_impl, self._metrics, attr_manager)(self._tracer),
60+
)
61+
except Exception:
62+
pass
63+
64+
try:
65+
wrap_function_wrapper(
66+
"haystack.components.generators.chat",
67+
"AzureOpenAIChatGenerator.stream",
68+
create_wrapper_factory(_wrap_haystack_stream_impl, self._metrics, attr_manager)(self._tracer),
69+
)
70+
except Exception:
71+
pass
72+
73+
def _custom_unwrap(self, **kwargs):
74+
unwrap("haystack.components.generators.openai", "OpenAIGenerator.run")
75+
unwrap("haystack.components.generators.chat", "AzureOpenAIChatGenerator.run")
76+
try:
77+
unwrap("haystack.components.generators.openai", "OpenAIGenerator.stream")
78+
except Exception:
79+
pass
80+
try:
81+
unwrap("haystack.components.generators.chat", "AzureOpenAIChatGenerator.stream")
82+
except Exception:
83+
pass
84+
85+
86+
def _first_non_empty_text(value):
87+
if isinstance(value, list) and value:
88+
return _first_non_empty_text(value[0])
89+
if isinstance(value, dict):
90+
if "content" in value:
91+
return str(value["content"])
92+
if "text" in value:
93+
return str(value["text"])
94+
if "replies" in value and value["replies"]:
95+
return str(value["replies"][0])
96+
if value is None:
97+
return None
98+
return str(value)
99+
100+
101+
def _extract_prompt(args, kwargs):
102+
if "prompt" in kwargs:
103+
return kwargs.get("prompt")
104+
if "messages" in kwargs:
105+
return kwargs.get("messages")
106+
if args:
107+
return args[0]
108+
return None
109+
110+
111+
def _get_model_name(instance):
112+
for attr in ("model", "model_name", "deployment_name", "deployment"):
113+
if hasattr(instance, attr):
114+
val = getattr(instance, attr)
115+
if val:
116+
return str(val)
117+
return None
118+
119+
120+
def _wrap_haystack_run_impl(tracer, metrics, attr_manager, wrapped, instance, args, kwargs):
121+
model = _get_model_name(instance)
122+
with create_span(
123+
tracer,
124+
"haystack.generator.run",
125+
kind=SpanKind.CLIENT,
126+
attributes={SpanAttributes.LLM_SYSTEM: "haystack", "gen_ai.model": model, SpanAttributes.LLM_REQUEST_STREAMING: False},
127+
attribute_manager=attr_manager,
128+
) as span:
129+
prompt = _extract_prompt(args, kwargs)
130+
prompt_text = _first_non_empty_text(prompt)
131+
if prompt_text:
132+
span.set_attribute("gen_ai.prompt.0.content", prompt_text[:500])
133+
134+
result = wrapped(*args, **kwargs)
135+
136+
reply_text = None
137+
if isinstance(result, dict):
138+
reply_text = _first_non_empty_text(result.get("replies"))
139+
if not reply_text:
140+
reply_text = _first_non_empty_text(result)
141+
else:
142+
reply_text = _first_non_empty_text(result)
143+
144+
if reply_text:
145+
span.set_attribute("gen_ai.response.0.content", str(reply_text)[:500])
146+
147+
return result
148+
149+
150+
def _wrap_haystack_stream_impl(tracer, metrics, attr_manager, wrapped, instance, args, kwargs):
151+
model = _get_model_name(instance)
152+
with create_span(
153+
tracer,
154+
"haystack.generator.stream",
155+
kind=SpanKind.CLIENT,
156+
attributes={SpanAttributes.LLM_SYSTEM: "haystack", "gen_ai.model": model, SpanAttributes.LLM_REQUEST_STREAMING: True},
157+
attribute_manager=attr_manager,
158+
) as span:
159+
prompt = _extract_prompt(args, kwargs)
160+
prompt_text = _first_non_empty_text(prompt)
161+
if prompt_text:
162+
span.set_attribute("gen_ai.prompt.0.content", prompt_text[:500])
163+
164+
out = wrapped(*args, **kwargs)
165+
166+
try:
167+
chunk_count = 0
168+
for chunk in out:
169+
chunk_count += 1
170+
last_text = _first_non_empty_text(chunk)
171+
if last_text:
172+
span.set_attribute("gen_ai.response.0.content", str(last_text)[:500])
173+
yield chunk
174+
span.set_attribute("gen_ai.response.chunk_count", chunk_count)
175+
except TypeError:
176+
return out

docs/v1/integrations/haystack.mdx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,12 +67,17 @@ AgentOps makes monitoring your Haystack agents seamless. Haystack, much like Aut
6767
</Step>
6868
</Steps>
6969

70+
## Supported generators
71+
72+
- OpenAI: `haystack.components.generators.openai.OpenAIGenerator`
73+
- Azure OpenAI Chat: `haystack.components.generators.chat.AzureOpenAIChatGenerator`
74+
7075
## Full Examples
7176

72-
You can refer to the following example -
77+
You can refer to the following examples -
7378

7479
- [Simple Haystack example (OpenAI)](https://github.com/AgentOps-AI/agentops/blob/main/examples/haystack/haystack_example.py)
75-
80+
- [Haystack Azure OpenAI Chat example](https://github.com/AgentOps-AI/agentops/blob/main/examples/haystack/azure_haystack_example.py)
7681

7782
<script type="module" src="/scripts/github_stars.js"></script>
7883
<script type="module" src="/scripts/scroll-img-fadein-animation.js"></script>
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import os
2+
3+
import agentops
4+
from haystack.components.generators.chat import AzureOpenAIChatGenerator
5+
6+
7+
def main():
8+
agentops.init(os.getenv("AGENTOPS_API_KEY"))
9+
10+
if not os.getenv("AZURE_OPENAI_API_KEY") or not os.getenv("AZURE_OPENAI_ENDPOINT"):
11+
print("Skipping Azure example: missing AZURE_OPENAI_API_KEY or AZURE_OPENAI_ENDPOINT (CI-safe skip)")
12+
return
13+
14+
tracer = agentops.start_trace(
15+
trace_name="Haystack Azure Chat Example",
16+
tags=["haystack", "azure", "chat", "agentops-example"],
17+
)
18+
19+
api_version = os.getenv("AZURE_OPENAI_API_VERSION", "2024-06-01")
20+
deployment = os.getenv("AZURE_OPENAI_DEPLOYMENT", "gpt-4o-mini")
21+
22+
generator = AzureOpenAIChatGenerator(
23+
api_key=os.getenv("AZURE_OPENAI_API_KEY"),
24+
api_version=api_version,
25+
azure_endpoint=os.getenv("AZURE_OPENAI_ENDPOINT"),
26+
deployment_name=deployment,
27+
)
28+
29+
messages = [{"role": "user", "content": "In one sentence, what is AgentOps?"}]
30+
result = generator.run(messages=messages)
31+
replies = result.get("replies") or []
32+
print("Haystack Azure reply:", replies[0] if replies else "<no reply>")
33+
34+
print("\n" + "=" * 50)
35+
print("Now let's verify that our LLM calls were tracked properly...")
36+
try:
37+
validation_result = agentops.validate_trace_spans(trace_context=tracer)
38+
agentops.print_validation_summary(validation_result)
39+
except agentops.ValidationError as e:
40+
print(f"\n❌ Error validating spans: {e}")
41+
raise
42+
43+
agentops.end_trace(tracer, end_state="Success")
44+
45+
46+
if __name__ == "__main__":
47+
main()

examples/haystack/haystack_example.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@
77
def main():
88
agentops.init(os.getenv("AGENTOPS_API_KEY"))
99

10+
if not os.getenv("OPENAI_API_KEY"):
11+
print("Skipping OpenAI example: missing OPENAI_API_KEY")
12+
return
13+
1014
tracer = agentops.start_trace(
1115
trace_name="Haystack OpenAI Example",
1216
tags=["haystack", "openai", "agentops-example"],
@@ -19,7 +23,7 @@ def main():
1923
print("Haystack reply:", replies[0] if replies else "<no reply>")
2024

2125
print("\n" + "=" * 50)
22-
print("Now let's verify that our LLM calls were tracked properly...")
26+
print("Now let's verify that our LLM calls were tracked properly with AgentOps...")
2327
try:
2428
validation_result = agentops.validate_trace_spans(trace_context=tracer)
2529
agentops.print_validation_summary(validation_result)

examples/haystack/requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
haystack-ai>=2.0.0
2-
openai>=1.0.0
2+
openai>=1.102.0

0 commit comments

Comments
 (0)