Skip to content

Commit e7189a0

Browse files
authored
fix(media): add timeout to reference resolution (#1037)
1 parent 7e1ce87 commit e7189a0

File tree

3 files changed

+80
-9
lines changed

3 files changed

+80
-9
lines changed

langfuse/client.py

Lines changed: 72 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
from langfuse.api.resources.utils.resources.pagination.types.meta_response import (
5050
MetaResponse,
5151
)
52+
from langfuse.api.resources.media import GetMediaResponse
5253
from langfuse.model import (
5354
ChatMessageDict,
5455
ChatPromptClient,
@@ -74,6 +75,7 @@
7475
from langfuse.environment import get_common_release_envs
7576
from langfuse.logging import clean_logger
7677
from langfuse.model import Dataset, MapValue, Observation, TraceWithFullDetails
78+
from langfuse.media import LangfuseMedia
7779
from langfuse.request import LangfuseClient
7880
from langfuse.types import MaskFunction, ScoreDataType, SpanLevel
7981
from langfuse.utils import _convert_usage_input, _create_prompt_context, _get_timestamp
@@ -111,6 +113,13 @@ class FetchObservationResponse:
111113
data: Observation
112114

113115

116+
@dataclass
117+
class FetchMediaResponse:
118+
"""Response object for fetch_media method."""
119+
120+
data: GetMediaResponse
121+
122+
114123
@dataclass
115124
class FetchSessionsResponse:
116125
"""Response object for fetch_sessions method."""
@@ -885,19 +894,78 @@ def fetch_observation(
885894
handle_fern_exception(e)
886895
raise e
887896

888-
def fetch_media(self, id: str):
897+
def fetch_media(self, id: str) -> FetchMediaResponse:
889898
"""Get media content by ID.
890899
891900
Args:
892901
id: The identifier of the media content to fetch.
893902
894903
Returns:
895-
Media object
904+
FetchMediaResponse: The media data of the given id on `data`.
896905
897906
Raises:
898-
Exception: If the media content could not be found or if an error occurred during the request.
907+
Exception: If the media content with the given id could not be found within the authenticated project or if an error occurred during the request.
908+
"""
909+
try:
910+
return FetchMediaResponse(data=self.client.media.get(id))
911+
except Exception as e:
912+
handle_fern_exception(e)
913+
raise e
914+
915+
def resolve_media_references(
916+
self,
917+
*,
918+
obj: Any,
919+
resolve_with: Literal["base64_data_uri"],
920+
max_depth: int = 10,
921+
content_fetch_timeout_seconds: int = 10,
922+
):
923+
"""Replace media reference strings in an object with base64 data URIs.
924+
925+
This method recursively traverses an object (up to max_depth) looking for media reference strings
926+
in the format "@@@langfuseMedia:...@@@". When found, it (synchronously) fetches the actual media content using
927+
the provided Langfuse client and replaces the reference string with a base64 data URI.
928+
929+
If fetching media content fails for a reference string, a warning is logged and the reference
930+
string is left unchanged.
931+
932+
Args:
933+
obj: The object to process. Can be a primitive value, array, or nested object.
934+
If the object has a __dict__ attribute, a dict will be returned instead of the original object type.
935+
resolve_with: The representation of the media content to replace the media reference string with.
936+
Currently only "base64_data_uri" is supported.
937+
max_depth: int: The maximum depth to traverse the object. Default is 10.
938+
content_fetch_timeout_seconds: int: The timeout in seconds for fetching media content. Default is 10.
939+
940+
Returns:
941+
A deep copy of the input object with all media references replaced with base64 data URIs where possible.
942+
If the input object has a __dict__ attribute, a dict will be returned instead of the original object type.
943+
944+
Example:
945+
obj = {
946+
"image": "@@@langfuseMedia:type=image/jpeg|id=123|source=bytes@@@",
947+
"nested": {
948+
"pdf": "@@@langfuseMedia:type=application/pdf|id=456|source=bytes@@@"
949+
}
950+
}
951+
952+
result = await LangfuseMedia.resolve_media_references(obj, langfuse_client)
953+
954+
# Result:
955+
# {
956+
# "image": "data:image/jpeg;base64,/9j/4AAQSkZJRg...",
957+
# "nested": {
958+
# "pdf": "data:application/pdf;base64,JVBERi0xLjcK..."
959+
# }
960+
# }
899961
"""
900-
return self.client.media.get(id)
962+
return LangfuseMedia.resolve_media_references(
963+
langfuse_client=self,
964+
obj=obj,
965+
resolve_with=resolve_with,
966+
max_depth=max_depth,
967+
content_fetch_timeout_seconds=content_fetch_timeout_seconds,
968+
)
901969

902970
def get_observation(
903971
self,

langfuse/media.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,7 @@ def resolve_media_references(
213213
langfuse_client: Any,
214214
resolve_with: Literal["base64_data_uri"],
215215
max_depth: int = 10,
216+
content_fetch_timeout_seconds: int = 10,
216217
) -> T:
217218
"""Replace media reference strings in an object with base64 data URIs.
218219
@@ -258,7 +259,7 @@ def traverse(obj: Any, depth: int) -> Any:
258259
if depth > max_depth:
259260
return obj
260261

261-
# Handle string with potential media references
262+
# Handle string
262263
if isinstance(obj, str):
263264
regex = r"@@@langfuseMedia:.+?@@@"
264265
reference_string_matches = re.findall(regex, obj)
@@ -275,8 +276,10 @@ def traverse(obj: Any, depth: int) -> Any:
275276
)
276277
media_data = langfuse_client.fetch_media(
277278
parsed_media_reference["media_id"]
279+
).data
280+
media_content = requests.get(
281+
media_data.url, timeout=content_fetch_timeout_seconds
278282
)
279-
media_content = requests.get(media_data.url)
280283
if not media_content.ok:
281284
raise Exception("Failed to fetch media content")
282285

@@ -289,7 +292,7 @@ def traverse(obj: Any, depth: int) -> Any:
289292
base64_data_uri
290293
)
291294
except Exception as e:
292-
logging.warning(
295+
LangfuseMedia._log.warning(
293296
f"Error fetching media content for reference string {reference_string}: {e}"
294297
)
295298
# Do not replace the reference string if there's an error

tests/test_media.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -145,8 +145,8 @@ def test_replace_media_reference_string_in_object(tmp_path):
145145
)
146146

147147
# Resolve media references back to base64
148-
resolved_trace = LangfuseMedia.resolve_media_references(
149-
obj=fetched_trace, langfuse_client=langfuse, resolve_with="base64_data_uri"
148+
resolved_trace = langfuse.resolve_media_references(
149+
obj=fetched_trace, resolve_with="base64_data_uri"
150150
)
151151

152152
# Verify resolved base64 matches original

0 commit comments

Comments
 (0)