I hope someone can clarify the semantics of calculation return value. The following calculation of a last response from an LLM that is stored in a relationship works:
calculations do
calculate :llm_response, :string do
calculation fn llm_response, _context ->
[llm_response] = Ash.load!(llm_response, run: [:messages])
last_message = llm_response.run.messages |> Enum.at(-1)
[last_message.content]
end
end
end
It loads a nested relationship and extract the content of the last message. After the definitions above, calling
will return a run resource where run.llm_response is a string, as expected.
However, I don’t fully understand why I should wrap the content (which is a string) in a list, given that I defined the return type as a :string in calculate :llm_response, :string? (If I don’t wrap it in a list, I get (Protocol.UndefinedError) protocol Enumerable not implemented for type BitString.; I decided to experiment with wrapping the return value in a list and it worked, to my surprise).
I’m using AshSqlite.DataLayer (not sure if that matters or not).
For example, if you were selecting 100 records, and needed to load some associated data from the database for each record, this could be done in a single load instead of 100.
Indeed, I changed my code (after refactoring some attribute names) to
calculations do
calculate :llm_response, :string do
calculation fn runs, _context ->
runs
|> Ash.load!(chain: [:messages])
|> Enum.map(fn run ->
last_message = run.chain.messages |> List.last()
last_message.content
end)
end
end
end
calculations do
calculate :llm_response, :string do
calculation fn runs, _context ->
message_query =
Message
|> Ash.Query.sort(created_at: :desc)
|> Ash.Query.limit(1)
runs
|> Ash.load!(chain: [messages: message_query])
|> Enum.map(fn run ->
last_message = run.chain.messages |> List.last()
last_message.content
end)
end
end
end
Furthermore, if you break it out into a module-based calculation, you can take advantage of the more optimized dependency loading for calculations.
defmodule MyApp.MyDomain.MyResource.Calculations.LastRun do
use Ash.Resource.Calculation
def load(_, _, _) do
message_query =
Message
|> Ash.Query.sort(created_at: :desc)
|> Ash.Query.limit(1)
[chain: [messages: message_query]]
end
def calculate(_, _, _) do
Enum.map(runs, fn run ->
last_message = run.chain.messages |> List.last()
last_message.content
end)
end
end
Then you can do calculate :llm_response, :string, MyApp.MyDomain.MyResource.Calculations.LastRun
Great suggestion, thank you! Reading the documentation more closely, I see that anonymous lambdas are not recommended, and I should be really using module-based approach everywhere.