How to conditionally do `run_oban_trigger`?

I am modifing code from the Ash.AI chat example. I could understand that whenever a message is created, it goes through one of the change change run_oban_trigger(:respond) which triggers the trigger :respond. So, whenever user type a message and enter, an LLM response is generated. I want to change it to only user explicitly type like "@M, xxxx", then the response from LLM should be triggered. How to do that? One solution is to modify the Response module, is it a good thing? because once reach here, I belive an Oban job is already enqueued. Can I do trigger the oban in with some condition? especially it would be better to do so in a seperate module, so I could implement complex logic. Thanks a lot ๐Ÿ™‚
Solution:
```elixir @impl true def change(changeset, _opts, _context) do case Ash.Changeset.get_attribute(changeset, :text) do nil ->...
Jump to solution
13 Replies
ZachDaniel
ZachDanielโ€ข4mo ago
You can, but you'll want to consider a few things I was going to say that it runs on a schedule, but actually it doesn't ๐Ÿ˜„ So yes you can do it pretty easily Instead of using change run_oban_trigger(:trigger), you can use a custom change module and say change YourCustomChange in that module, you can conditionally call AshOban.run_trigger
hyperion_zw
hyperion_zwOPโ€ข4mo ago
I tried following code :
defmodule Masque.Chat.Message.Changes.MaybeGenerateLLMResponse do
@moduledoc """
This module is responsible for conditionally generating a response from the LLM.
It triggers the Oban job only if the message contains "@M" and removes "@M" from the message.
"""

use Ash.Resource.Change
require Logger
alias AshOban

@impl true
def change(changeset, _opts, _context) do
case Ash.Changeset.get_attribute(changeset, :text) do
nil ->
changeset

text ->
Logger.info("->> text: #{text}")

if String.contains?(text, "@M") do
new_text = String.replace(text, "@M", "")
Logger.info("->> need to generate LLM response")

Ash.Changeset.set_argument(changeset, :text, new_text)
|> AshOban.run_trigger(:respond)
else
Logger.info("->> do not generate LLM response")
changeset
end
end
end
end
defmodule Masque.Chat.Message.Changes.MaybeGenerateLLMResponse do
@moduledoc """
This module is responsible for conditionally generating a response from the LLM.
It triggers the Oban job only if the message contains "@M" and removes "@M" from the message.
"""

use Ash.Resource.Change
require Logger
alias AshOban

@impl true
def change(changeset, _opts, _context) do
case Ash.Changeset.get_attribute(changeset, :text) do
nil ->
changeset

text ->
Logger.info("->> text: #{text}")

if String.contains?(text, "@M") do
new_text = String.replace(text, "@M", "")
Logger.info("->> need to generate LLM response")

Ash.Changeset.set_argument(changeset, :text, new_text)
|> AshOban.run_trigger(:respond)
else
Logger.info("->> do not generate LLM response")
changeset
end
end
end
end
and changed the according message change to use it
change Masque.Chat.Message.Changes.CreateConversationIfNotProvided
# change run_oban_trigger(:respond)
change Masque.Chat.Message.Changes.MaybeGenerateLLMResponse
change Masque.Chat.Message.Changes.CreateConversationIfNotProvided
# change run_oban_trigger(:respond)
change Masque.Chat.Message.Changes.MaybeGenerateLLMResponse
However, if I type "@Mxx", the log shows error:
[info] ->> text: @
[info] ->> do not generate LLM response
[debug] Replied in 592ยตs
[debug] HANDLE EVENT "validate_message" in MasqueWeb.ChatLive
Parameters: %{"_target" => ["form", "text"], "form" => %{"text" => "@M"}}
[info] ->> text: @M
[info] ->> need to generate LLM response
[error] GenServer #PID<0.1449.0> terminating
** (Ash.Error.Unknown)
Bread Crumbs:
> action change {Masque.Chat.Message.Changes.MaybeGenerateLLMResponse, []}
> building changeset for Masque.Chat.Message.create

Unknown Error
* ** (ArgumentError) `Ash.Changeset` is not a Spark DSL module.
(ash 3.5.23) Ash.Changeset.entities([:oban, :triggers])
(spark 2.2.66) lib/spark/dsl/extension.ex:202: Spark.Dsl.Extension.get_entities/2
[info] ->> text: @
[info] ->> do not generate LLM response
[debug] Replied in 592ยตs
[debug] HANDLE EVENT "validate_message" in MasqueWeb.ChatLive
Parameters: %{"_target" => ["form", "text"], "form" => %{"text" => "@M"}}
[info] ->> text: @M
[info] ->> need to generate LLM response
[error] GenServer #PID<0.1449.0> terminating
** (Ash.Error.Unknown)
Bread Crumbs:
> action change {Masque.Chat.Message.Changes.MaybeGenerateLLMResponse, []}
> building changeset for Masque.Chat.Message.create

Unknown Error
* ** (ArgumentError) `Ash.Changeset` is not a Spark DSL module.
(ash 3.5.23) Ash.Changeset.entities([:oban, :triggers])
(spark 2.2.66) lib/spark/dsl/extension.ex:202: Spark.Dsl.Extension.get_entities/2
ZachDaniel
ZachDanielโ€ข4mo ago
AshOban.run_trigger/2 takes a record as its first argument not a changeset And you'll want to do this in a before or after action hook anything with side effects or expensive logic should be in hooks So that its transactional with the main action
hyperion_zw
hyperion_zwOPโ€ข4mo ago
Yes, that is the part i am confusing, because I see the change is trigger when I am typing. Now I know my mistake. Thanks ๐Ÿ™‚
Solution
ZachDaniel
ZachDanielโ€ข4mo ago
@impl true
def change(changeset, _opts, _context) do
case Ash.Changeset.get_attribute(changeset, :text) do
nil ->
changeset

text ->
Logger.info("->> text: #{text}")

if String.contains?(text, "@M") do
new_text = String.replace(text, "@M", "")
Logger.info("->> need to generate LLM response")

changeset
|> Ash.Changeset.set_argument(:text, new_text)
|> Ash.Changeset.after_action(fn changeset, result ->
AshOban.run_trigger(result, :respond)
{:ok, result}
end)
else
Logger.info("->> do not generate LLM response")
changeset
end
end
end
@impl true
def change(changeset, _opts, _context) do
case Ash.Changeset.get_attribute(changeset, :text) do
nil ->
changeset

text ->
Logger.info("->> text: #{text}")

if String.contains?(text, "@M") do
new_text = String.replace(text, "@M", "")
Logger.info("->> need to generate LLM response")

changeset
|> Ash.Changeset.set_argument(:text, new_text)
|> Ash.Changeset.after_action(fn changeset, result ->
AshOban.run_trigger(result, :respond)
{:ok, result}
end)
else
Logger.info("->> do not generate LLM response")
changeset
end
end
end
ZachDaniel
ZachDanielโ€ข4mo ago
You want something like that
hyperion_zw
hyperion_zwOPโ€ข4mo ago
Good chance to ask a elixir question: sometimes I want to grab the variable and expriment it in "iex", currently I could only do this by print it out in iex, is there a technique to hold that variable in iex?
ZachDaniel
ZachDanielโ€ข4mo ago
There is, but keep in mind that you'll often hit timeouts when you do this that kill your process like if its in testing you'll hit a test timeout or in an HTTP request you'll hit a request timeout so you may want to set them higher for debugging
@impl true
def change(changeset, _opts, _context) do
case Ash.Changeset.get_attribute(changeset, :text) do
nil ->
changeset

text ->
Logger.info("->> text: #{text}")

if String.contains?(text, "@M") do
new_text = String.replace(text, "@M", "")
Logger.info("->> need to generate LLM response")

changeset
|> Ash.Changeset.set_argument(:text, new_text)
|> Ash.Changeset.after_action(fn changeset, result ->
require IEx
IEx.pry()
AshOban.run_trigger(result, :respond)
{:ok, result}
end)
else
Logger.info("->> do not generate LLM response")
changeset
end
end
end
@impl true
def change(changeset, _opts, _context) do
case Ash.Changeset.get_attribute(changeset, :text) do
nil ->
changeset

text ->
Logger.info("->> text: #{text}")

if String.contains?(text, "@M") do
new_text = String.replace(text, "@M", "")
Logger.info("->> need to generate LLM response")

changeset
|> Ash.Changeset.set_argument(:text, new_text)
|> Ash.Changeset.after_action(fn changeset, result ->
require IEx
IEx.pry()
AshOban.run_trigger(result, :respond)
{:ok, result}
end)
else
Logger.info("->> do not generate LLM response")
changeset
end
end
end
and make sure whatever command you ran with iex -S ...
hyperion_zw
hyperion_zwOPโ€ข4mo ago
wow, Thanks a lot ๐Ÿ™‚ I am wondering how in "Masque.Chat.Message.Changes.MaybeGenerateLLMResponse" call AshOban.run_trigger(result, :respond), it somehow could trigger the :respond in module Masque.Chat.Message.
oban do
triggers do
trigger :respond do
actor_persister Masque.AiAgentActorPersister
action :respond
queue :chat_responses
lock_for_update? false
scheduler_cron false
worker_module_name Masque.Chat.Message.Workers.Respond
scheduler_module_name Masque.Chat.Message.Schedulers.Respond
where expr(needs_response)
end
end
end
oban do
triggers do
trigger :respond do
actor_persister Masque.AiAgentActorPersister
action :respond
queue :chat_responses
lock_for_update? false
scheduler_cron false
worker_module_name Masque.Chat.Message.Workers.Respond
scheduler_module_name Masque.Chat.Message.Schedulers.Respond
where expr(needs_response)
end
end
end
is this because Masque.Chat.Message.Changes.MaybeGenerateLLMResponse is used as change in Masque.Chat.Message? I fould Ash is full of magic. how to get started to learn the idea behind Ash ? This lisp way of sovling problem. I didn't see it in other places.
ZachDaniel
ZachDanielโ€ข4mo ago
There are a couple of books out, mine and Rebeccas and another on https://devcarrots.com
Master Elixir, Phoenix & Ash Frameworks - Books, Training & Live Wo...
Get expert-led books, training materials, and live workshops on Elixir, Phoenix, and Ash Frameworks. Learn functional programming with practical, real-world applications.
ZachDaniel
ZachDanielโ€ข4mo ago
The underlying principle is simpler than it seems The DSL doesn't "do" anything really It just describes a data structure other things do things with that data structure
AshOban.run_trigger(result, :respond)
AshOban.run_trigger(result, :respond)
That looks up the trigger on the resource to figure out what it should do both simpler and more complex perhaps than you realize
ZachDaniel
ZachDanielโ€ข4mo ago
hyperion_zw
hyperion_zwOPโ€ข4mo ago
Thanks a lot for your patience . ๐Ÿ™‚

Did you find this page helpful?