Skip to content

Commit

Permalink
Include context from the stacktrace for caught exceptions for LLM fun…
Browse files Browse the repository at this point in the history
…ctions and function callbacks. (#241)

- caught exceptions were logging their type and message, however, without a stack trace or location for the exception debugging may be required to locate some issues. 
 - added format_exception/3 to LangChain.LangChainError to format exceptions with context from the stacktrace. The third argument is the format - :short only includes the location and the default gives a full stack trace at the end of the message.
 - I updated the most obvious places where a lack of context could slow down fixing issues, however there are other rescue blocks where this may be useful.
  • Loading branch information
montebrown authored Jan 30, 2025
1 parent 3c1260c commit b349f18
Show file tree
Hide file tree
Showing 7 changed files with 33 additions and 7 deletions.
2 changes: 1 addition & 1 deletion lib/callbacks.ex
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ defmodule LangChain.Callbacks do
rescue
err ->
msg =
"Callback handler for #{inspect(callback_name)} raised an exception: #{inspect(err)}"
"Callback handler for #{inspect(callback_name)} raised an exception: #{LangChainError.format_exception(err, __STACKTRACE__, :short)}"

Logger.error(msg)
raise LangChainError, msg
Expand Down
4 changes: 3 additions & 1 deletion lib/chains/llm_chain.ex
Original file line number Diff line number Diff line change
Expand Up @@ -978,7 +978,9 @@ defmodule LangChain.Chains.LLMChain do
end
rescue
err ->
Logger.error("Function #{function.name} failed in execution. Exception: #{inspect(err)}")
Logger.error(
"Function #{function.name} failed in execution. Exception: #{LangChainError.format_exception(err, __STACKTRACE__)}"
)

ToolResult.new!(%{
tool_call_id: call.call_id,
Expand Down
7 changes: 5 additions & 2 deletions lib/function.ex
Original file line number Diff line number Diff line change
Expand Up @@ -281,8 +281,11 @@ defmodule LangChain.Function do
end
rescue
err ->
Logger.error("Function #{function.name} failed in execution. Exception: #{inspect(err)}")
{:error, "ERROR: #{inspect(err)}"}
Logger.error(
"Function! #{function.name} failed in execution. Exception: #{LangChainError.format_exception(err, __STACKTRACE__)}"
)

{:error, "ERROR: #{LangChainError.format_exception(err, __STACKTRACE__, :short)}"}
end
end

Expand Down
15 changes: 15 additions & 0 deletions lib/langchain_error.ex
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,19 @@ defmodule LangChain.LangChainError do
original: Keyword.get(opts, :original)
}
end

@doc """
Formats the exception as a string using a stacktrace to provide context.
"""
@spec format_exception(exception :: struct(), trace :: Exception.stacktrace(), format :: atom()) ::
String.t()
def format_exception(exception, trace, format \\ :full_stacktrace) do
case format do
:short ->
"(#{inspect(exception.__struct__)}) #{Exception.message(exception)} at #{Enum.at(trace, 0) |> Exception.format_stacktrace_entry()}"

_ ->
Exception.format(:error, exception, trace)
end
end
end
2 changes: 1 addition & 1 deletion test/callbacks_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ defmodule LangChain.CallbacksTest do
handlers = %{custom: fn _value -> raise ArgumentError, "BOOM!" end}

assert_raise LangChainError,
"Callback handler for :custom raised an exception: %ArgumentError{message: \"BOOM!\"}",
"Callback handler for :custom raised an exception: (ArgumentError) BOOM! at test/callbacks_test.exs:#{__ENV__.line - 3}: anonymous fn/1 in LangChain.CallbacksTest.\"test fire/3 handles when a handler errors\"/1",
fn ->
Callbacks.fire([handlers], :custom, ["123"])
end
Expand Down
5 changes: 4 additions & 1 deletion test/chains/llm_chain_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -1756,7 +1756,10 @@ defmodule LangChain.Chains.LLMChainTest do

assert updated_chain.last_message.role == :tool
[%ToolResult{} = result] = updated_chain.last_message.tool_results
assert result.content == "ERROR: %RuntimeError{message: \"Stuff went boom!\"}"

assert result.content ==
"ERROR: (RuntimeError) Stuff went boom! at test/chains/llm_chain_test.exs:#{__ENV__.line - 19}: anonymous fn/2 in LangChain.Chains.LLMChainTest.\"test execute_tool_calls/2 catches exceptions from executed function and returns Tool result with error message\"/1"

assert result.is_error == true
end

Expand Down
5 changes: 4 additions & 1 deletion test/function_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,10 @@ defmodule LangChain.FunctionTest do

# rescues an exception and returns as string text
result = Function.execute(function, %{}, %{result: :exception})
assert result == {:error, "ERROR: %RuntimeError{message: \"fake exception\"}"}

assert result ==
{:error,
"ERROR: (RuntimeError) fake exception at test/function_test.exs:13: LangChain.FunctionTest.returns_context/2"}

# returns an error when anything else is returned
result = Function.execute(function, %{}, %{result: 123})
Expand Down

0 comments on commit b349f18

Please sign in to comment.