From afbf6cd265f159cc527056e04cd5c822ca732378 Mon Sep 17 00:00:00 2001 From: David Alexander Pfeiffer <20278364+davidmc971@users.noreply.github.com> Date: Tue, 25 Mar 2025 15:27:08 +0100 Subject: [PATCH 1/6] added score metadata from generated fern types --- langfuse/api/resources/commons/types/base_score.py | 1 + langfuse/api/resources/commons/types/score.py | 3 +++ langfuse/api/resources/ingestion/types/score_body.py | 1 + langfuse/api/resources/score/types/create_score_request.py | 1 + .../api/resources/score/types/get_scores_response_data.py | 3 +++ langfuse/extract_model.py | 5 ++++- 6 files changed, 13 insertions(+), 1 deletion(-) diff --git a/langfuse/api/resources/commons/types/base_score.py b/langfuse/api/resources/commons/types/base_score.py index 89394956b..07c4966da 100644 --- a/langfuse/api/resources/commons/types/base_score.py +++ b/langfuse/api/resources/commons/types/base_score.py @@ -23,6 +23,7 @@ class BaseScore(pydantic_v1.BaseModel): alias="authorUserId", default=None ) comment: typing.Optional[str] = None + metadata: typing.Optional[typing.Any] = None config_id: typing.Optional[str] = pydantic_v1.Field(alias="configId", default=None) """ Reference a score config on a score. When set, config and score name must be equal and value must comply to optionally defined numerical range diff --git a/langfuse/api/resources/commons/types/score.py b/langfuse/api/resources/commons/types/score.py index 8eed33b78..061853f45 100644 --- a/langfuse/api/resources/commons/types/score.py +++ b/langfuse/api/resources/commons/types/score.py @@ -26,6 +26,7 @@ class Score_Numeric(pydantic_v1.BaseModel): alias="authorUserId", default=None ) comment: typing.Optional[str] = None + metadata: typing.Optional[typing.Any] = None config_id: typing.Optional[str] = pydantic_v1.Field(alias="configId", default=None) queue_id: typing.Optional[str] = pydantic_v1.Field(alias="queueId", default=None) environment: typing.Optional[str] = None @@ -84,6 +85,7 @@ class Score_Categorical(pydantic_v1.BaseModel): alias="authorUserId", default=None ) comment: typing.Optional[str] = None + metadata: typing.Optional[typing.Any] = None config_id: typing.Optional[str] = pydantic_v1.Field(alias="configId", default=None) queue_id: typing.Optional[str] = pydantic_v1.Field(alias="queueId", default=None) environment: typing.Optional[str] = None @@ -142,6 +144,7 @@ class Score_Boolean(pydantic_v1.BaseModel): alias="authorUserId", default=None ) comment: typing.Optional[str] = None + metadata: typing.Optional[typing.Any] = None config_id: typing.Optional[str] = pydantic_v1.Field(alias="configId", default=None) queue_id: typing.Optional[str] = pydantic_v1.Field(alias="queueId", default=None) environment: typing.Optional[str] = None diff --git a/langfuse/api/resources/ingestion/types/score_body.py b/langfuse/api/resources/ingestion/types/score_body.py index dbe2fbbd9..df5a59b51 100644 --- a/langfuse/api/resources/ingestion/types/score_body.py +++ b/langfuse/api/resources/ingestion/types/score_body.py @@ -35,6 +35,7 @@ class ScoreBody(pydantic_v1.BaseModel): alias="observationId", default=None ) comment: typing.Optional[str] = None + metadata: typing.Optional[typing.Any] = None data_type: typing.Optional[ScoreDataType] = pydantic_v1.Field( alias="dataType", default=None ) diff --git a/langfuse/api/resources/score/types/create_score_request.py b/langfuse/api/resources/score/types/create_score_request.py index c11030f4f..74dccc597 100644 --- a/langfuse/api/resources/score/types/create_score_request.py +++ b/langfuse/api/resources/score/types/create_score_request.py @@ -34,6 +34,7 @@ class CreateScoreRequest(pydantic_v1.BaseModel): alias="observationId", default=None ) comment: typing.Optional[str] = None + metadata: typing.Optional[typing.Any] = None environment: typing.Optional[str] = pydantic_v1.Field(default=None) """ The environment of the score. Can be any lowercase alphanumeric string with hyphens and underscores that does not start with 'langfuse'. diff --git a/langfuse/api/resources/score/types/get_scores_response_data.py b/langfuse/api/resources/score/types/get_scores_response_data.py index 5642e0f80..11b20a7be 100644 --- a/langfuse/api/resources/score/types/get_scores_response_data.py +++ b/langfuse/api/resources/score/types/get_scores_response_data.py @@ -28,6 +28,7 @@ class GetScoresResponseData_Numeric(pydantic_v1.BaseModel): alias="authorUserId", default=None ) comment: typing.Optional[str] = None + metadata: typing.Optional[typing.Any] = None config_id: typing.Optional[str] = pydantic_v1.Field(alias="configId", default=None) queue_id: typing.Optional[str] = pydantic_v1.Field(alias="queueId", default=None) environment: typing.Optional[str] = None @@ -87,6 +88,7 @@ class GetScoresResponseData_Categorical(pydantic_v1.BaseModel): alias="authorUserId", default=None ) comment: typing.Optional[str] = None + metadata: typing.Optional[typing.Any] = None config_id: typing.Optional[str] = pydantic_v1.Field(alias="configId", default=None) queue_id: typing.Optional[str] = pydantic_v1.Field(alias="queueId", default=None) environment: typing.Optional[str] = None @@ -146,6 +148,7 @@ class GetScoresResponseData_Boolean(pydantic_v1.BaseModel): alias="authorUserId", default=None ) comment: typing.Optional[str] = None + metadata: typing.Optional[typing.Any] = None config_id: typing.Optional[str] = pydantic_v1.Field(alias="configId", default=None) queue_id: typing.Optional[str] = pydantic_v1.Field(alias="queueId", default=None) environment: typing.Optional[str] = None diff --git a/langfuse/extract_model.py b/langfuse/extract_model.py index 192522846..5880e9624 100644 --- a/langfuse/extract_model.py +++ b/langfuse/extract_model.py @@ -106,7 +106,10 @@ def _extract_model_name( def _extract_model_from_repr_by_pattern( - id: str, serialized: Optional[Dict[str, Any]], pattern: str, default: Optional[str] = None + id: str, + serialized: Optional[Dict[str, Any]], + pattern: str, + default: Optional[str] = None, ): if serialized is None: return None From 7bb581753a51700552d6a2270ce8aa116ce8c43a Mon Sep 17 00:00:00 2001 From: David Alexander Pfeiffer <20278364+davidmc971@users.noreply.github.com> Date: Tue, 25 Mar 2025 17:01:59 +0100 Subject: [PATCH 2/6] add metadata to score() functions --- langfuse/client.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/langfuse/client.py b/langfuse/client.py index dc7ed9409..72e269900 100644 --- a/langfuse/client.py +++ b/langfuse/client.py @@ -1573,6 +1573,7 @@ def score( trace_id: typing.Optional[str] = None, id: typing.Optional[str] = None, comment: typing.Optional[str] = None, + metadata: typing.Optional[typing.Any] = None, observation_id: typing.Optional[str] = None, config_id: typing.Optional[str] = None, **kwargs, @@ -1588,6 +1589,7 @@ def score( trace_id: typing.Optional[str] = None, id: typing.Optional[str] = None, comment: typing.Optional[str] = None, + metadata: typing.Optional[typing.Any] = None, observation_id: typing.Optional[str] = None, config_id: typing.Optional[str] = None, **kwargs, @@ -1602,6 +1604,7 @@ def score( trace_id: typing.Optional[str] = None, id: typing.Optional[str] = None, comment: typing.Optional[str] = None, + metadata: typing.Optional[typing.Any] = None, observation_id: typing.Optional[str] = None, config_id: typing.Optional[str] = None, **kwargs, @@ -1616,6 +1619,7 @@ def score( trace_id (str): The id of the trace to which the score should be attached. id (Optional[str]): The id of the score. If not provided, a new UUID is generated. comment (Optional[str]): Additional context/explanation of the score. + metadata (Optional[Any]): Additional metadata of the score. Can be any JSON object. Metadata is merged when being updated via the API. observation_id (Optional[str]): The id of the observation to which the score should be attached. config_id (Optional[str]): The id of the score config. When set, the score value is validated against the config. Defaults to None. **kwargs: Additional keyword arguments to include in the score. @@ -1655,6 +1659,7 @@ def score( "value": value, "data_type": data_type, "comment": comment, + "metadata": metadata, "config_id": config_id, "environment": self.environment, **kwargs, @@ -2415,6 +2420,7 @@ def score( value: float, data_type: typing.Optional[Literal["NUMERIC", "BOOLEAN"]] = None, comment: typing.Optional[str] = None, + metadata: typing.Optional[typing.Any] = None, config_id: typing.Optional[str] = None, **kwargs, ) -> "StatefulClient": ... @@ -2428,6 +2434,7 @@ def score( value: str, data_type: typing.Optional[Literal["CATEGORICAL"]] = "CATEGORICAL", comment: typing.Optional[str] = None, + metadata: typing.Optional[typing.Any] = None, config_id: typing.Optional[str] = None, **kwargs, ) -> "StatefulClient": ... @@ -2440,6 +2447,7 @@ def score( value: typing.Union[float, str], data_type: typing.Optional[ScoreDataType] = None, comment: typing.Optional[str] = None, + metadata: typing.Optional[typing.Any] = None, config_id: typing.Optional[str] = None, **kwargs, ) -> "StatefulClient": @@ -2451,6 +2459,7 @@ def score( data_type (Optional[ScoreDataType]): The data type of the score. When not set, the data type is inferred from the score config's data type, when present. When no config is set, the data type is inferred from the value's type, i.e. float values are categorized as numeric scores and string values as categorical scores. comment (Optional[str]): Additional context/explanation of the score. + metadata (Optional[Any]): Additional metadata of the score. Can be any JSON object. Metadata is merged when being updated via the API. id (Optional[str]): The id of the score. If not provided, a new UUID is generated. config_id (Optional[str]): The id of the score config. When set, the score value is validated against the config. Defaults to None. **kwargs: Additional keyword arguments to include in the score. @@ -2484,6 +2493,7 @@ def score( "value": value, "data_type": data_type, "comment": comment, + "metadata": metadata, "config_id": config_id, "environment": self.environment, **kwargs, From 58f1f8ce59af67c31d3da3e4a08a6c244931d949 Mon Sep 17 00:00:00 2001 From: David Alexander Pfeiffer <20278364+davidmc971@users.noreply.github.com> Date: Tue, 25 Mar 2025 17:02:16 +0100 Subject: [PATCH 3/6] WIP on tests --- tests/test_core_sdk.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/test_core_sdk.py b/tests/test_core_sdk.py index 38aea9a7a..bec9cbbf4 100644 --- a/tests/test_core_sdk.py +++ b/tests/test_core_sdk.py @@ -503,6 +503,7 @@ def test_score_trace(): name="valuation", value=0.5, comment="This is a comment", + metadata={"key": "value"}, ) langfuse.flush() @@ -522,6 +523,7 @@ def test_score_trace(): assert score["comment"] == "This is a comment" assert score["observationId"] is None assert score["dataType"] == "NUMERIC" + assert score["metadata"] == {"key": "value"} def test_score_trace_nested_trace(): @@ -535,6 +537,7 @@ def test_score_trace_nested_trace(): name="valuation", value=0.5, comment="This is a comment", + metadata={"key": "value"}, ) langfuse.flush() @@ -554,6 +557,7 @@ def test_score_trace_nested_trace(): assert score.comment == "This is a comment" assert score.observation_id is None assert score.data_type == "NUMERIC" + assert score.metadata == {"key": "value"} def test_score_trace_nested_observation(): @@ -568,6 +572,7 @@ def test_score_trace_nested_observation(): name="valuation", value=0.5, comment="This is a comment", + metadata={"key": "value"}, ) langfuse.flush() @@ -587,6 +592,7 @@ def test_score_trace_nested_observation(): assert score.comment == "This is a comment" assert score.observation_id == span.id assert score.data_type == "NUMERIC" + assert score.metadata == {"key": "value"} def test_score_span(): @@ -611,6 +617,7 @@ def test_score_span(): name="valuation", value=1, comment="This is a comment", + metadata={"key": "value"}, ) langfuse.flush() @@ -629,6 +636,7 @@ def test_score_span(): assert score["comment"] == "This is a comment" assert score["observationId"] == spanId assert score["dataType"] == "NUMERIC" + assert score["metadata"] == {"key": "value"} def test_create_trace_and_span(): From d318f70beb1d7880dcb27b57e47a3654a1ca510c Mon Sep 17 00:00:00 2001 From: David Alexander Pfeiffer <20278364+davidmc971@users.noreply.github.com> Date: Tue, 25 Mar 2025 19:53:43 +0100 Subject: [PATCH 4/6] score tests include metadata --- tests/test_core_sdk.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/tests/test_core_sdk.py b/tests/test_core_sdk.py index bec9cbbf4..8b78634d0 100644 --- a/tests/test_core_sdk.py +++ b/tests/test_core_sdk.py @@ -502,7 +502,7 @@ def test_score_trace(): trace_id=langfuse.get_trace_id(), name="valuation", value=0.5, - comment="This is a comment", + comment="tests/test_core_sdk.py::test_score_trace", metadata={"key": "value"}, ) @@ -520,7 +520,7 @@ def test_score_trace(): assert score["name"] == "valuation" assert score["value"] == 0.5 - assert score["comment"] == "This is a comment" + assert score["comment"] == "tests/test_core_sdk.py::test_score_trace" assert score["observationId"] is None assert score["dataType"] == "NUMERIC" assert score["metadata"] == {"key": "value"} @@ -536,7 +536,7 @@ def test_score_trace_nested_trace(): trace.score( name="valuation", value=0.5, - comment="This is a comment", + comment="tests/test_core_sdk.py::test_score_trace_nested_trace", metadata={"key": "value"}, ) @@ -554,7 +554,7 @@ def test_score_trace_nested_trace(): assert score.name == "valuation" assert score.value == 0.5 - assert score.comment == "This is a comment" + assert score.comment == "tests/test_core_sdk.py::test_score_trace_nested_trace" assert score.observation_id is None assert score.data_type == "NUMERIC" assert score.metadata == {"key": "value"} @@ -571,7 +571,7 @@ def test_score_trace_nested_observation(): span.score( name="valuation", value=0.5, - comment="This is a comment", + comment="tests/test_core_sdk.py::test_score_trace_nested_observation", metadata={"key": "value"}, ) @@ -589,7 +589,9 @@ def test_score_trace_nested_observation(): assert score.name == "valuation" assert score.value == 0.5 - assert score.comment == "This is a comment" + assert ( + score.comment == "tests/test_core_sdk.py::test_score_trace_nested_observation" + ) assert score.observation_id == span.id assert score.data_type == "NUMERIC" assert score.metadata == {"key": "value"} @@ -616,7 +618,7 @@ def test_score_span(): observation_id=spanId, name="valuation", value=1, - comment="This is a comment", + comment="tests/test_core_sdk.py::test_score_span", metadata={"key": "value"}, ) @@ -633,7 +635,7 @@ def test_score_span(): assert score["name"] == "valuation" assert score["value"] == 1 - assert score["comment"] == "This is a comment" + assert score["comment"] == "tests/test_core_sdk.py::test_score_span" assert score["observationId"] == spanId assert score["dataType"] == "NUMERIC" assert score["metadata"] == {"key": "value"} From f27dcccb99c74d872b32b9fcaad62f23602e2559 Mon Sep 17 00:00:00 2001 From: David Alexander Pfeiffer <20278364+davidmc971@users.noreply.github.com> Date: Wed, 9 Apr 2025 11:43:20 +0200 Subject: [PATCH 5/6] Added tests for different metadata types --- tests/test_core_sdk.py | 53 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/tests/test_core_sdk.py b/tests/test_core_sdk.py index 8b78634d0..c003bb1c8 100644 --- a/tests/test_core_sdk.py +++ b/tests/test_core_sdk.py @@ -1594,3 +1594,56 @@ def test_environment_from_env_var(monkeypatch): fetched_trace = api_wrapper.get_trace(trace.id) assert fetched_trace["environment"] == "testing" + + +def test_metadata(): + langfuse = Langfuse(debug=True) + api_wrapper = LangfuseAPI() + + def prepare_case(metadata): + trace = langfuse.trace(name="test_metadata", metadata=metadata) + observation = trace.generation(name="test_gen", metadata=metadata) + trace.score(name="test_score", value=1, metadata=metadata) + langfuse.flush() + sleep(1) + return trace, observation + + def fetch_values(trace, observation): + fetched_trace = api_wrapper.get_trace(trace.id) + fetched_observation = api_wrapper.get_observation(observation.id) + fetched_score = fetched_trace["scores"][0] + return fetched_trace, fetched_observation, fetched_score + + def submit_and_fetch(metadata): + trace, observation = prepare_case(metadata) + fetched_trace, fetched_observation, fetched_score = fetch_values( + trace, observation + ) + trace_metadata = fetched_trace["metadata"] + observation_metadata = fetched_observation["metadata"] + score_metadata = fetched_score["metadata"] + return [trace_metadata, observation_metadata, score_metadata] + + string_test = submit_and_fetch("Test Metadata") + string_test_expected = {"metadata": "Test Metadata"} + assert string_test == [ + string_test_expected, + string_test_expected, + string_test_expected, + ] + + int_test = submit_and_fetch(1) + int_test_expected = {"metadata": 1} + assert int_test == [int_test_expected, int_test_expected, int_test_expected] + + float_test = submit_and_fetch(1.0) + float_test_expected = {"metadata": 1.0} + assert float_test == [float_test_expected, float_test_expected, float_test_expected] + + dict_test = submit_and_fetch({"key": "value"}) + dict_test_expected = {"key": "value"} + assert dict_test == [dict_test_expected, dict_test_expected, dict_test_expected] + + list_test = submit_and_fetch(["value1", "value2"]) + list_test_expected = {"metadata": ["value1", "value2"]} + assert list_test == [list_test_expected, list_test_expected, list_test_expected] From 876ecec6e543db29d3aecc371d6d9fac26cd58bc Mon Sep 17 00:00:00 2001 From: David Alexander Pfeiffer <20278364+davidmc971@users.noreply.github.com> Date: Wed, 9 Apr 2025 11:53:31 +0200 Subject: [PATCH 6/6] parametrized new metadata test --- tests/test_core_sdk.py | 70 +++++++++++++++--------------------------- 1 file changed, 25 insertions(+), 45 deletions(-) diff --git a/tests/test_core_sdk.py b/tests/test_core_sdk.py index c003bb1c8..82d6b19fc 100644 --- a/tests/test_core_sdk.py +++ b/tests/test_core_sdk.py @@ -1596,54 +1596,34 @@ def test_environment_from_env_var(monkeypatch): assert fetched_trace["environment"] == "testing" -def test_metadata(): +@pytest.mark.parametrize( + "input_metadata, expected_metadata", + [ + ("Test Metadata", {"metadata": "Test Metadata"}), + (1, {"metadata": 1}), + (1.0, {"metadata": 1.0}), + ({"key": "value"}, {"key": "value"}), + (["value1", "value2"], {"metadata": ["value1", "value2"]}), + ], +) +def test_metadata(input_metadata, expected_metadata): langfuse = Langfuse(debug=True) api_wrapper = LangfuseAPI() - def prepare_case(metadata): - trace = langfuse.trace(name="test_metadata", metadata=metadata) - observation = trace.generation(name="test_gen", metadata=metadata) - trace.score(name="test_score", value=1, metadata=metadata) - langfuse.flush() - sleep(1) - return trace, observation - - def fetch_values(trace, observation): - fetched_trace = api_wrapper.get_trace(trace.id) - fetched_observation = api_wrapper.get_observation(observation.id) - fetched_score = fetched_trace["scores"][0] - return fetched_trace, fetched_observation, fetched_score - - def submit_and_fetch(metadata): - trace, observation = prepare_case(metadata) - fetched_trace, fetched_observation, fetched_score = fetch_values( - trace, observation - ) - trace_metadata = fetched_trace["metadata"] - observation_metadata = fetched_observation["metadata"] - score_metadata = fetched_score["metadata"] - return [trace_metadata, observation_metadata, score_metadata] - - string_test = submit_and_fetch("Test Metadata") - string_test_expected = {"metadata": "Test Metadata"} - assert string_test == [ - string_test_expected, - string_test_expected, - string_test_expected, - ] - - int_test = submit_and_fetch(1) - int_test_expected = {"metadata": 1} - assert int_test == [int_test_expected, int_test_expected, int_test_expected] + trace = langfuse.trace(name="test_metadata", metadata=input_metadata) + observation = trace.generation(name="test_gen", metadata=input_metadata) + trace.score(name="test_score", value=1, metadata=input_metadata) + langfuse.flush() + sleep(1) - float_test = submit_and_fetch(1.0) - float_test_expected = {"metadata": 1.0} - assert float_test == [float_test_expected, float_test_expected, float_test_expected] + fetched_trace = api_wrapper.get_trace(trace.id) + fetched_observation = api_wrapper.get_observation(observation.id) + fetched_score = fetched_trace["scores"][0] - dict_test = submit_and_fetch({"key": "value"}) - dict_test_expected = {"key": "value"} - assert dict_test == [dict_test_expected, dict_test_expected, dict_test_expected] + trace_metadata = fetched_trace["metadata"] + observation_metadata = fetched_observation["metadata"] + score_metadata = fetched_score["metadata"] - list_test = submit_and_fetch(["value1", "value2"]) - list_test_expected = {"metadata": ["value1", "value2"]} - assert list_test == [list_test_expected, list_test_expected, list_test_expected] + assert trace_metadata == expected_metadata + assert observation_metadata == expected_metadata + assert score_metadata == expected_metadata