Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 19 additions & 5 deletions langfuse/langchain/CallbackHandler.py
Original file line number Diff line number Diff line change
Expand Up @@ -1091,7 +1091,7 @@ def on_retriever_end(

def on_tool_end(
self,
output: str,
output: Any,
*,
run_id: UUID,
parent_run_id: Optional[UUID] = None,
Expand All @@ -1105,10 +1105,24 @@ def on_tool_end(
if observation is not None:
if parent_run_id is None:
self._clear_root_run_resume_key(run_id)
observation.update(
output=output,
input=kwargs.get("inputs"),
).end()

update_kwargs: Dict[str, Any] = {
"output": output,
"input": kwargs.get("inputs"),
}

if (
isinstance(output, ToolMessage)
and getattr(output, "status", None) == "error"
):
update_kwargs["level"] = "ERROR"
update_kwargs["status_message"] = (
output.content
if isinstance(output.content, str)
else str(output.content)
)
Comment thread
hassiebp marked this conversation as resolved.

observation.update(**update_kwargs).end()

except Exception as e:
langfuse_logger.exception(e)
Expand Down
35 changes: 34 additions & 1 deletion tests/unit/test_langchain.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

import pytest
from langchain.messages import HumanMessage
from langchain_core.messages import AIMessage
from langchain_core.messages import AIMessage, ToolMessage
from langchain_core.output_parsers import StrOutputParser
from langchain_core.outputs import ChatGeneration, ChatResult, Generation, LLMResult
from langchain_core.prompts import ChatPromptTemplate
Expand Down Expand Up @@ -791,6 +791,39 @@ class DummyControlFlowError(RuntimeError):
assert not _has_run_state(handler, retriever_run_id)


def test_handled_tool_error_marks_observation_error(
langfuse_memory_client, get_span, json_attr
):
handler = CallbackHandler()
run_id = uuid4()

handler.on_tool_start(
{"name": "failing_tool"},
'{"query": "x"}',
run_id=run_id,
)
handler.on_tool_end(
ToolMessage(
content="handled failure",
tool_call_id="call_1",
status="error",
),
run_id=run_id,
)

langfuse_memory_client.flush()
span = get_span("failing_tool")

assert span.attributes[LangfuseOtelSpanAttributes.OBSERVATION_LEVEL] == "ERROR"
assert (
span.attributes[LangfuseOtelSpanAttributes.OBSERVATION_STATUS_MESSAGE]
== "handled failure"
)
assert json_attr(span, LangfuseOtelSpanAttributes.OBSERVATION_OUTPUT)["status"] == (
"error"
)


def test_pending_resume_contexts_are_capped(langfuse_memory_client, monkeypatch):
class DummyControlFlowError(RuntimeError):
pass
Expand Down