vkryukov
Semantics of calculation return value
Hello,
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
{:ok, run} = LLMRun |> Ash.get("27face7e-6443-43bd-bb69-e7529eedd20c", load: [:llm_response])
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).
Thank you!
Marked As Solved
ahey
Calculations operate on a list of records for efficiency reasons. The calculate function is passed in a list of records and must return a list of calculated terms.
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.
Also Liked
zachdaniel
Also worth pointing out that that only works correctly because you are using Ash.get and so only have a single record.
If you did a_list_of_records |> Ash.load(:llm_response) your [single_thing] = ... match would fail.
vkryukov
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
and now the above Ash.get as well as
LLMRun |> Ash.Query.for_read(:read) |> Ash.read!(load: :llm_response)
work as expected.
zachdaniel
You can optimize this further:
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
Popular in Questions
Other popular topics
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #deployment
- #library
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #channels
- #elixirconf
- #exunit
- #discussion
- #javascript
- #code-sync
- #podcasts
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #elixir-ls
- #phoenix_html
- #iex
- #blog-post
- #graphql
- #genstage
- #ai
- #websockets
- #supervisor
- #advent-of-code
- #elixirconf-us
- #distillery
- #processes
- #forms
- #api
- #metaprogramming
- #security
- #performance








