Skip to content

Commit 2cddebc

Browse files
authored
feat: Add name for Phoenix traces (#99)
1 parent 9e16e3e commit 2cddebc

7 files changed

Lines changed: 203 additions & 119 deletions

File tree

app/src/common/phoenix_utils.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import opentelemetry.exporter.otlp.proto.http.trace_exporter as otel_trace_exporter
77
import phoenix.otel
88
from opentelemetry.sdk.trace.export import BatchSpanProcessor
9+
from opentelemetry.trace import NoOpTracerProvider, TracerProvider
910

1011
# https://docs.arize.com/phoenix/tracing/integrations-tracing/haystack
1112
# Arize's Phoenix observability platform
@@ -38,6 +39,9 @@ def service_alive() -> bool:
3839
return False
3940

4041

42+
tracer_provider: TracerProvider = NoOpTracerProvider()
43+
44+
4145
def configure_phoenix(only_if_alive: bool = True) -> None:
4246
"Set only_if_alive=True to fail fast if Phoenix is not reachable."
4347
if only_if_alive and not service_alive():
@@ -54,6 +58,7 @@ def configure_phoenix(only_if_alive: bool = True) -> None:
5458
logger.info("Using phoenix.otel.register with batch_otel=%s", config.batch_otel)
5559
# This uses PHOENIX_COLLECTOR_ENDPOINT and PHOENIX_PROJECT_NAME env variables
5660
# and PHOENIX_API_KEY to handle authentication to Phoenix.
61+
global tracer_provider
5762
tracer_provider = phoenix.otel.register(
5863
endpoint=trace_endpoint,
5964
batch=config.batch_otel,

app/src/pipelines/email_result/pipeline_wrapper.py

Lines changed: 44 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@
55
from hayhooks import BasePipelineWrapper
66
from haystack import Pipeline
77
from haystack.core.errors import PipelineRuntimeError
8-
from openinference.instrumentation import using_metadata
8+
from openinference.instrumentation import _tracers, using_metadata
9+
from opentelemetry.trace.status import Status, StatusCode
910

10-
from src.common import components
11+
from src.common import components, phoenix_utils
1112

1213
logger = logging.getLogger(__name__)
14+
tracer = phoenix_utils.tracer_provider.get_tracer(__name__)
1315

1416

1517
class PipelineWrapper(BasePipelineWrapper):
@@ -28,34 +30,46 @@ def setup(self) -> None:
2830

2931
def run_api(self, result_id: str, email: str) -> dict:
3032
with using_metadata({"email": email}):
31-
try:
32-
response = self.pipeline.run(
33-
{
34-
"logger": {
35-
"messages_list": [{"result_id": result_id, "email": email}],
36-
},
37-
"load_result": {
38-
"result_id": result_id,
39-
},
40-
"email_result": {
41-
"email": email,
42-
},
33+
# Must set using_metadata context before calling tracer.start_as_current_span()
34+
assert isinstance(tracer, _tracers.OITracer), f"Got unexpected {type(tracer)}"
35+
with tracer.start_as_current_span( # pylint: disable=not-context-manager,unexpected-keyword-arg
36+
self.name, openinference_span_kind="chain"
37+
) as span:
38+
result = self._run(result_id, email)
39+
span.set_input(result_id)
40+
span.set_output(result["email_result"]["status"])
41+
span.set_status(Status(StatusCode.OK))
42+
return result
43+
44+
def _run(self, result_id: str, email: str) -> dict:
45+
try:
46+
response = self.pipeline.run(
47+
{
48+
"logger": {
49+
"messages_list": [{"result_id": result_id, "email": email}],
4350
},
44-
include_outputs_from={"email_result"},
45-
)
46-
logger.debug("Results: %s", pformat(response, width=160))
47-
return response
48-
except PipelineRuntimeError as re:
49-
error_msg = str(re)
50-
if re.component_type == components.LoadResult:
51-
if "Invalid JSON format in result" in error_msg:
52-
status_code = 500 # Internal error
53-
else:
54-
status_code = 400 # User error
55-
else:
51+
"load_result": {
52+
"result_id": result_id,
53+
},
54+
"email_result": {
55+
"email": email,
56+
},
57+
},
58+
include_outputs_from={"email_result"},
59+
)
60+
logger.debug("Results: %s", pformat(response, width=160))
61+
return response
62+
except PipelineRuntimeError as re:
63+
error_msg = str(re)
64+
if re.component_type == components.LoadResult:
65+
if "Invalid JSON format in result" in error_msg:
5666
status_code = 500 # Internal error
67+
else:
68+
status_code = 400 # User error
69+
else:
70+
status_code = 500 # Internal error
5771

58-
raise HTTPException(
59-
status_code=status_code,
60-
detail=f"Error occurred: {error_msg}",
61-
) from re
72+
raise HTTPException(
73+
status_code=status_code,
74+
detail=f"Error occurred: {error_msg}",
75+
) from re

app/src/pipelines/generate_action_plan/pipeline_wrapper.py

Lines changed: 32 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,16 @@
44
from hayhooks import BasePipelineWrapper
55
from haystack import Pipeline
66
from haystack.components.builders import ChatPromptBuilder
7-
from openinference.instrumentation import using_attributes, using_metadata
7+
from openinference.instrumentation import _tracers, using_attributes, using_metadata
8+
from opentelemetry.trace.status import Status, StatusCode
89
from pydantic import BaseModel
910

10-
from src.common import haystack_utils
11+
from src.common import haystack_utils, phoenix_utils
1112
from src.common.components import OpenAIWebSearchGenerator, ReadableLogger
1213
from src.pipelines.generate_referrals.pipeline_wrapper import Resource
1314

1415
logger = logging.getLogger(__name__)
16+
tracer = phoenix_utils.tracer_provider.get_tracer(__name__)
1517

1618

1719
class ActionPlan(BaseModel):
@@ -59,23 +61,35 @@ def run_api(self, resources: list[Resource] | list[dict], user_email: str) -> di
5961
resource_objects = get_resources(resources)
6062

6163
with using_attributes(user_id=user_email), using_metadata({"user_id": user_email}):
62-
response = self.pipeline.run(
63-
{
64-
"logger": {
65-
"messages_list": [
66-
{"resource_count": len(resource_objects), "user_email": user_email}
67-
],
68-
},
69-
"prompt_builder": {
70-
"resources": format_resources(resource_objects),
71-
"action_plan_json": action_plan_as_json,
72-
},
73-
"llm": {"model": "gpt-5-mini", "reasoning_effort": "low"},
64+
# Must set using_metadata context before calling tracer.start_as_current_span()
65+
assert isinstance(tracer, _tracers.OITracer), f"Got unexpected {type(tracer)}"
66+
with tracer.start_as_current_span( # pylint: disable=not-context-manager,unexpected-keyword-arg
67+
self.name, openinference_span_kind="chain"
68+
) as span:
69+
result = self._run(resource_objects, user_email)
70+
span.set_input([r.name for r in resource_objects])
71+
span.set_output(result["response"])
72+
span.set_status(Status(StatusCode.OK))
73+
return result
74+
75+
def _run(self, resource_objects: list[Resource], user_email: str) -> dict:
76+
response = self.pipeline.run(
77+
{
78+
"logger": {
79+
"messages_list": [
80+
{"resource_count": len(resource_objects), "user_email": user_email}
81+
],
7482
},
75-
include_outputs_from={"llm"},
76-
)
77-
logger.debug("Results: %s", pformat(response, width=160))
78-
return {"response": response["llm"]["replies"][0]._content[0].text}
83+
"prompt_builder": {
84+
"resources": format_resources(resource_objects),
85+
"action_plan_json": action_plan_as_json,
86+
},
87+
"llm": {"model": "gpt-5-mini", "reasoning_effort": "low"},
88+
},
89+
include_outputs_from={"llm"},
90+
)
91+
logger.debug("Results: %s", pformat(response, width=160))
92+
return {"response": response["llm"]["replies"][0]._content[0].text}
7993

8094

8195
def get_resources(resources: list[Resource] | list[dict]) -> list[Resource]:

app/src/pipelines/generate_referrals/pipeline_wrapper.py

Lines changed: 54 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import json
12
import logging
23
from enum import Enum
34
from pprint import pformat
@@ -9,12 +10,14 @@
910
from haystack import Pipeline
1011
from haystack.components.builders import ChatPromptBuilder
1112
from haystack.core.errors import PipelineRuntimeError
12-
from openinference.instrumentation import using_attributes, using_metadata
13+
from openinference.instrumentation import _tracers, using_attributes, using_metadata
14+
from opentelemetry.trace.status import Status, StatusCode
1315
from pydantic import BaseModel
1416

15-
from src.common import components, haystack_utils
17+
from src.common import components, haystack_utils, phoenix_utils
1618

1719
logger = logging.getLogger(__name__)
20+
tracer = phoenix_utils.tracer_provider.get_tracer(__name__)
1821

1922

2023
class ReferralType(str, Enum):
@@ -97,37 +100,53 @@ def setup(self) -> None:
97100
# Called for the `generate-referrals/run` endpoint
98101
def run_api(self, query: str, user_email: str, prompt_version_id: str = "") -> dict:
99102
with using_attributes(user_id=user_email), using_metadata({"user_id": user_email}):
100-
# Retrieve the requested prompt_version_id and error if requested prompt version is not found
101-
try:
102-
prompt_template = haystack_utils.get_phoenix_prompt(
103-
"generate_referrals", prompt_version_id
104-
)
105-
except httpx.HTTPStatusError as he:
106-
raise HTTPException(
107-
status_code=422,
108-
detail=f"The requested prompt version '{prompt_version_id}' could not be retrieved due to HTTP status {he.response.status_code}",
109-
) from he
110-
111-
try:
112-
response = self.pipeline.run(
113-
{
114-
"logger": {
115-
"messages_list": [{"query": query, "user_email": user_email}],
116-
},
117-
"prompt_builder": {
118-
"template": prompt_template,
119-
"query": query,
120-
"response_json": response_schema,
121-
},
122-
"llm": {"model": "gpt-5-mini", "reasoning_effort": "low"},
103+
# Must set using_metadata context before calling tracer.start_as_current_span()
104+
assert isinstance(tracer, _tracers.OITracer), f"Got unexpected {type(tracer)}"
105+
with tracer.start_as_current_span( # pylint: disable=not-context-manager,unexpected-keyword-arg
106+
self.name, openinference_span_kind="chain"
107+
) as span:
108+
result = self._run(query, user_email, prompt_version_id)
109+
span.set_input(query)
110+
try:
111+
resp_obj = json.loads(result["llm"]["replies"][-1].text)
112+
span.set_output([r["name"] for r in resp_obj["resources"]])
113+
except (KeyError, IndexError):
114+
span.set_output(result["llm"]["replies"][-1].text)
115+
span.set_status(Status(StatusCode.OK))
116+
return result
117+
118+
def _run(self, query: str, user_email: str, prompt_version_id: str = "") -> dict:
119+
# Retrieve the requested prompt_version_id and error if requested prompt version is not found
120+
try:
121+
prompt_template = haystack_utils.get_phoenix_prompt(
122+
"generate_referrals", prompt_version_id
123+
)
124+
except httpx.HTTPStatusError as he:
125+
raise HTTPException(
126+
status_code=422,
127+
detail=f"The requested prompt version '{prompt_version_id}' could not be retrieved due to HTTP status {he.response.status_code}",
128+
) from he
129+
130+
try:
131+
response = self.pipeline.run(
132+
{
133+
"logger": {
134+
"messages_list": [{"query": query, "user_email": user_email}],
123135
},
124-
include_outputs_from={"llm", "save_result"},
125-
)
126-
logger.debug("Results: %s", pformat(response, width=160))
127-
return response
128-
except PipelineRuntimeError as re:
129-
logger.error("PipelineRuntimeError: %s", re, exc_info=True)
130-
raise HTTPException(status_code=500, detail=str(re)) from re
131-
except Exception as e:
132-
logger.error("Error %s: %s", type(e), e, exc_info=True)
133-
raise HTTPException(status_code=500, detail=f"Internal error: {str(e)}") from e
136+
"prompt_builder": {
137+
"template": prompt_template,
138+
"query": query,
139+
"response_json": response_schema,
140+
},
141+
"llm": {"model": "gpt-5-mini", "reasoning_effort": "low"},
142+
},
143+
include_outputs_from={"llm", "save_result"},
144+
)
145+
logger.debug("Results: %s", pformat(response, width=160))
146+
return response
147+
except PipelineRuntimeError as re:
148+
logger.error("PipelineRuntimeError: %s", re, exc_info=True)
149+
raise HTTPException(status_code=500, detail=str(re)) from re
150+
except Exception as e:
151+
logger.error("Error %s: %s", type(e), e, exc_info=True)
152+
raise HTTPException(status_code=500, detail=f"Internal error: {str(e)}") from e

app/src/pipelines/generate_referrals_from_doc/pipeline_wrapper.py

Lines changed: 36 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import json
12
import logging
23
from pprint import pformat
34
from typing import List, Optional
@@ -7,16 +8,18 @@
78
from haystack import Pipeline
89
from haystack.components.builders import ChatPromptBuilder
910
from haystack.components.converters import OutputAdapter, PyPDFToDocument
10-
from openinference.instrumentation import using_metadata
11+
from openinference.instrumentation import _tracers, using_metadata
12+
from opentelemetry.trace.status import Status, StatusCode
1113

12-
from src.common import components, haystack_utils
14+
from src.common import components, haystack_utils, phoenix_utils
1315
from src.pipelines.generate_referrals.pipeline_wrapper import response_schema
1416

1517
logger = logging.getLogger(__name__)
18+
tracer = phoenix_utils.tracer_provider.get_tracer(__name__)
1619

1720

1821
class PipelineWrapper(BasePipelineWrapper):
19-
name = "generate_referrals_from_document"
22+
name = "generate_referrals_from_doc"
2023

2124
def setup(self) -> None:
2225
pipeline = Pipeline()
@@ -67,18 +70,34 @@ def run_api(self, user_email: str, files: Optional[List[UploadFile]] = None) ->
6770
raise HTTPException(status_code=400, detail="No files provided for processing.")
6871

6972
with using_metadata({"user_id": user_email}):
70-
response = self.pipeline.run(
71-
{
72-
"logger": {
73-
"messages_list": [{"filenames": [file.filename for file in files]}],
74-
},
75-
"files_to_bytestreams": {"files": files},
76-
"prompt_builder": {
77-
"response_json": response_schema,
78-
},
79-
"llm": {"model": "gpt-5-mini", "reasoning_effort": "low"},
73+
# Must set using_metadata context before calling tracer.start_as_current_span()
74+
assert isinstance(tracer, _tracers.OITracer), f"Got unexpected {type(tracer)}"
75+
with tracer.start_as_current_span( # pylint: disable=not-context-manager,unexpected-keyword-arg
76+
self.name, openinference_span_kind="chain"
77+
) as span:
78+
result = self._run(files)
79+
span.set_input([file.filename for file in files])
80+
try:
81+
resp_obj = json.loads(result["llm"]["replies"][-1].text)
82+
span.set_output([r["name"] for r in resp_obj["resources"]])
83+
except (KeyError, IndexError):
84+
span.set_output(result["llm"]["replies"][-1].text)
85+
span.set_status(Status(StatusCode.OK))
86+
return result
87+
88+
def _run(self, files: List[UploadFile]) -> dict:
89+
response = self.pipeline.run(
90+
{
91+
"logger": {
92+
"messages_list": [{"filenames": [file.filename for file in files]}],
93+
},
94+
"files_to_bytestreams": {"files": files},
95+
"prompt_builder": {
96+
"response_json": response_schema,
8097
},
81-
include_outputs_from={"llm"},
82-
)
83-
logger.info("Pipeline result: %s", pformat(response, width=160))
84-
return response
98+
"llm": {"model": "gpt-5-mini", "reasoning_effort": "low"},
99+
},
100+
include_outputs_from={"llm"},
101+
)
102+
logger.info("Pipeline result: %s", pformat(response, width=160))
103+
return response

app/src/pipelines/hello_bedrock/pipeline_wrapper.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010

1111
logger = logging.getLogger(__name__)
1212

13-
1413
system_prompt = (
1514
"Your role is to say hello to the name provided by the user, if no name is found politely inform the user."
1615
"Assure them any PII is handled securely in AWS Bedrock. You should only greet the user, do not respond "

0 commit comments

Comments
 (0)