diff --git a/langfuse/_task_manager/task_manager.py b/langfuse/_task_manager/task_manager.py index e9426535..d44a6421 100644 --- a/langfuse/_task_manager/task_manager.py +++ b/langfuse/_task_manager/task_manager.py @@ -116,7 +116,7 @@ def add_task(self, event: dict): return try: - event["timestamp"] = _get_timestamp() + event["timestamp"] = event.get("timestamp") or _get_timestamp() self._ingestion_queue.put(event, block=False) except queue.Full: diff --git a/langfuse/client.py b/langfuse/client.py index 6f1ba5ee..00c65a4e 100644 --- a/langfuse/client.py +++ b/langfuse/client.py @@ -1555,6 +1555,7 @@ def score( comment: typing.Optional[str] = None, observation_id: typing.Optional[str] = None, config_id: typing.Optional[str] = None, + timestamp: typing.Optional[dt.datetime] = None, **kwargs, ) -> "StatefulClient": ... @@ -1570,6 +1571,7 @@ def score( comment: typing.Optional[str] = None, observation_id: typing.Optional[str] = None, config_id: typing.Optional[str] = None, + timestamp: typing.Optional[dt.datetime] = None, **kwargs, ) -> "StatefulClient": ... @@ -1584,6 +1586,7 @@ def score( comment: typing.Optional[str] = None, observation_id: typing.Optional[str] = None, config_id: typing.Optional[str] = None, + timestamp: typing.Optional[dt.datetime] = None, **kwargs, ) -> "StatefulClient": """Create a score attached to a trace (and optionally an observation). @@ -1646,6 +1649,7 @@ def score( "id": str(uuid.uuid4()), "type": "score-create", "body": new_body, + "timestamp": timestamp, } self.task_manager.add_task(event) @@ -2366,6 +2370,7 @@ def score( data_type: typing.Optional[Literal["NUMERIC", "BOOLEAN"]] = None, comment: typing.Optional[str] = None, config_id: typing.Optional[str] = None, + timestamp: typing.Optional[dt.datetime] = None, **kwargs, ) -> "StatefulClient": ... @@ -2379,6 +2384,7 @@ def score( data_type: typing.Optional[Literal["CATEGORICAL"]] = "CATEGORICAL", comment: typing.Optional[str] = None, config_id: typing.Optional[str] = None, + timestamp: typing.Optional[dt.datetime] = None, **kwargs, ) -> "StatefulClient": ... @@ -2391,6 +2397,7 @@ def score( data_type: typing.Optional[ScoreDataType] = None, comment: typing.Optional[str] = None, config_id: typing.Optional[str] = None, + timestamp: typing.Optional[dt.datetime] = None, **kwargs, ) -> "StatefulClient": """Create a score attached for the current observation or trace. @@ -2451,6 +2458,7 @@ def score( "id": str(uuid.uuid4()), "type": "score-create", "body": request, + "timestamp": timestamp, } self.task_manager.add_task(event) diff --git a/tests/test_core_sdk.py b/tests/test_core_sdk.py index a09b7c1f..7e5c0336 100644 --- a/tests/test_core_sdk.py +++ b/tests/test_core_sdk.py @@ -213,6 +213,45 @@ def test_create_categorical_score(): assert trace["scores"][0]["stringValue"] == "high score" +def test_create_score_with_timestamp(): + langfuse = Langfuse(debug=False) + api_wrapper = LangfuseAPI() + + trace = langfuse.trace( + name="this-is-so-great-new", + user_id="test", + metadata="test", + ) + + langfuse.flush() + assert langfuse.task_manager._ingestion_queue.qsize() == 0 + + score_id = create_uuid() + score_timestamp = datetime(2023, 1, 1, 12, 0, 0) + + langfuse.score( + id=score_id, + trace_id=trace.id, + name="this-is-a-score", + value="high score", + timestamp=score_timestamp, + ) + + trace.generation(name="yet another child", metadata="test") + + langfuse.flush() + + assert langfuse.task_manager._ingestion_queue.qsize() == 0 + + trace = api_wrapper.get_trace(trace.id) + + assert trace["scores"][0]["id"] == score_id + assert trace["scores"][0]["dataType"] == "CATEGORICAL" + assert trace["scores"][0]["value"] == 0 + assert trace["scores"][0]["stringValue"] == "high score" + assert trace["scores"][0]["timestamp"] == score_timestamp.isoformat() + "Z" + + def test_create_trace(): langfuse = Langfuse(debug=False) trace_name = create_uuid()