AshAi tools

how do I get the schema of a tool?
29 Replies
Oliver
Oliver3w ago
you mean you want to see how it looks for the AI? the schema is generated from your linked action
Abu kumathra
Abu kumathraOP3w ago
yes I need to see the argument it exposes because the AI is making invalid calls that result in the db query failing
ZachDaniel
ZachDaniel3w ago
IIRC there is a debug or verbose flag in langchain to see exactly what it's doing And also a function like AshAi.parameter_schema or something that will give you the schema
Abu kumathra
Abu kumathraOP3w ago
@Zach The thing is I have an {:array, :string} field that I want the llm to provide a value and check whether it exists in the record.field or not, @barnabasj told me about :in filter but it does the reverse of what I want. and when I looked at the tool definition the only filter exposed by ash by default for {:array, :string} fields is is_nil. I think the naive approach would be to add an argument to the action and then do it myself
ZachDaniel
ZachDaniel3w ago
That is the best way to do it 👍 the pre-built filters can't do everything
Abu kumathra
Abu kumathraOP3w ago
the issue is that I have 12 fields like that :[
ZachDaniel
ZachDaniel3w ago
There will always be things the builtin filters can't do we can add a contains or member or has operator and then lift that up to the automatic filter stuff using arrays but its not going ot be something that can happen in the short term unfortunately
Abu kumathra
Abu kumathraOP3w ago
I think for postgres it's pretty straight forward "@@" and "= ANY" I will be will to PR it
ZachDaniel
ZachDaniel3w ago
Its more complicated to set up than you realize 😄
Abu kumathra
Abu kumathraOP3w ago
I don't know how it would mess with the other datalayers
ZachDaniel
ZachDaniel3w ago
First, a has function needs to be PRd to Ash core, similar to in basically copy that implementation and type it as [:same, {:any, :same}] and then a PR to ash_sql to implement the expression in expr.ex And then maybe some logic in ash_ai to pick that up, not sure 😄
Abu kumathra
Abu kumathraOP3w ago
I have some days off I will try to work on it maybe I don't know what I am getting myself into also while we're at it, an intersection function would be valuable too the "&&" operator shouldn't the type be [{:array, :same}, :same] because the lhs will have the array
ZachDaniel
ZachDaniel3w ago
Yeah if its has then yeah, as in has(list, thing) 👍
Abu kumathra
Abu kumathraOP3w ago
thank you I noticed something in the Opertor.In code, when we compare 2 predicates, if we add 2 subset checks we can tell if it will be :right_includes_left or vice versa
def compare(
%__MODULE__{left: left, right: %MapSet{} = mapset_left},
%__MODULE__{left: left, right: %MapSet{} = mapset_right}
) do
cond do
MapSet.equal?(mapset_left, mapset_right) -> :mutually_inclusive
MapSet.disjoint?(mapset_left, mapset_right) -> :mutually_exclusive
MapSet.subset?(mapset_right, mapset_left) -> :right_includes_left
MapSet.subset?(mapset_left, mapset_right) -> :left_includes_right
true -> :unknown
end
end
def compare(
%__MODULE__{left: left, right: %MapSet{} = mapset_left},
%__MODULE__{left: left, right: %MapSet{} = mapset_right}
) do
cond do
MapSet.equal?(mapset_left, mapset_right) -> :mutually_inclusive
MapSet.disjoint?(mapset_left, mapset_right) -> :mutually_exclusive
MapSet.subset?(mapset_right, mapset_left) -> :right_includes_left
MapSet.subset?(mapset_left, mapset_right) -> :left_includes_right
true -> :unknown
end
end
False alarm just don't mind this the issue is that if it's not in the small MapSet we will still need to check that it's in the big one. There is no equivalency
ZachDaniel
ZachDaniel3w ago
Don't worry about those callbacks TBH They aren't really used for anything currently
Abu kumathra
Abu kumathraOP3w ago
I think I understood the idea basically compare is used to reduce the predicates and for example left_includes_right means that if left is false right is false here the code for has
defmodule Ash.Query.Operator.Has do
@moduledoc """
left has 1

this predicate matches if the right value is in the list on the left
"""
use Ash.Query.Operator,
operator: :has,
predicate?: true,
types: [[{:array, :same}, :same]]

@dialyzer {:nowarn_function, compare: 2}

# def new(%Ash.Query.Ref{} = left, right) when is_list(right) do
# {:ok, %__MODULE__{left: left, right: MapSet.new(right)}}
# end

def new(left, right), do: {:ok, %__MODULE__{left: left, right: right}}

def can_return_nil?(%{left: left}), do: Ash.Expr.can_return_nil?(left)

def evaluate(%{left: nil}), do: {:known, nil}
def evaluate(%{right: nil}), do: {:known, nil}

def evaluate(%{left: left, right: right}) do
{:known, Enum.any?(left, &Comp.equal?(&1, right))}
end

@impl Ash.Filter.Predicate
def compare(
%__MODULE__{left: left, right: left_right},
%__MODULE__{left: left, right: right_right}
) do
cond do
left_right == right_right -> :mutually_inclusive
true -> :unknown
end
end

def compare(_, _) do
:unknown
end

def to_string(op, opts), do: super(op, opts)
end
defmodule Ash.Query.Operator.Has do
@moduledoc """
left has 1

this predicate matches if the right value is in the list on the left
"""
use Ash.Query.Operator,
operator: :has,
predicate?: true,
types: [[{:array, :same}, :same]]

@dialyzer {:nowarn_function, compare: 2}

# def new(%Ash.Query.Ref{} = left, right) when is_list(right) do
# {:ok, %__MODULE__{left: left, right: MapSet.new(right)}}
# end

def new(left, right), do: {:ok, %__MODULE__{left: left, right: right}}

def can_return_nil?(%{left: left}), do: Ash.Expr.can_return_nil?(left)

def evaluate(%{left: nil}), do: {:known, nil}
def evaluate(%{right: nil}), do: {:known, nil}

def evaluate(%{left: left, right: right}) do
{:known, Enum.any?(left, &Comp.equal?(&1, right))}
end

@impl Ash.Filter.Predicate
def compare(
%__MODULE__{left: left, right: left_right},
%__MODULE__{left: left, right: right_right}
) do
cond do
left_right == right_right -> :mutually_inclusive
true -> :unknown
end
end

def compare(_, _) do
:unknown
end

def to_string(op, opts), do: super(op, opts)
end
defmodule Ash.Query.Operator.Overlaps do
@moduledoc """
left overlaps [1, 2, 3]

this predicate matches if the list on the left overlaps the list on the left
"""
use Ash.Query.Operator,
operator: :overlaps,
predicate?: true,
types: [[{:array, :same}, {:array, :same}]]

@inspect_items_limit 10
@dialyzer {:nowarn_function, compare: 2}

def new(%Ash.Query.Ref{} = left, right) when is_list(right) do
{:ok, %__MODULE__{left: left, right: MapSet.new(right)}}
end

def new(left, right), do: {:ok, %__MODULE__{left: left, right: right}}

def can_return_nil?(%{left: left}), do: Ash.Expr.can_return_nil?(left)

def evaluate(%{left: nil}), do: {:known, nil}
def evaluate(%{right: nil}), do: {:known, nil}

def evaluate(%{left: left, right: right}) do
{:known, Enum.any?(left, fn l -> Enum.any?(right, &Comp.equal?(&1, l)) end)}
end

@impl Ash.Filter.Predicate
def compare(
%__MODULE__{left: left, right: %MapSet{} = mapset_left},
%__MODULE__{left: left, right: %MapSet{} = mapset_right}
) do
cond do
MapSet.equal?(mapset_left, mapset_right) -> :mutually_inclusive
MapSet.disjoint?(mapset_left, mapset_right) -> :mutually_exclusive
true -> :unknown
end

end

def compare(
%__MODULE__{left: left, right: %MapSet{} = mapset},
%Ash.Query.Operator.Has{left: left, right: value}
) do

if MapSet.member?(mapset, value) do
:left_includes_right
else
:mutually_exclusive
end
end

def compare(_, _) do
:unknown
end

def to_string(%{left: left, right: %MapSet{} = mapset}, opts) do
import Inspect.Algebra

list_doc =
case Enum.split(mapset, @inspect_items_limit) do
{left, []} -> to_doc(left, opts)
{left, _} -> concat(to_doc(left, opts), "...")
end

concat([
to_doc(left, opts),
" overlaps ",
list_doc
])
end

def to_string(op, opts), do: super(op, opts)
end
defmodule Ash.Query.Operator.Overlaps do
@moduledoc """
left overlaps [1, 2, 3]

this predicate matches if the list on the left overlaps the list on the left
"""
use Ash.Query.Operator,
operator: :overlaps,
predicate?: true,
types: [[{:array, :same}, {:array, :same}]]

@inspect_items_limit 10
@dialyzer {:nowarn_function, compare: 2}

def new(%Ash.Query.Ref{} = left, right) when is_list(right) do
{:ok, %__MODULE__{left: left, right: MapSet.new(right)}}
end

def new(left, right), do: {:ok, %__MODULE__{left: left, right: right}}

def can_return_nil?(%{left: left}), do: Ash.Expr.can_return_nil?(left)

def evaluate(%{left: nil}), do: {:known, nil}
def evaluate(%{right: nil}), do: {:known, nil}

def evaluate(%{left: left, right: right}) do
{:known, Enum.any?(left, fn l -> Enum.any?(right, &Comp.equal?(&1, l)) end)}
end

@impl Ash.Filter.Predicate
def compare(
%__MODULE__{left: left, right: %MapSet{} = mapset_left},
%__MODULE__{left: left, right: %MapSet{} = mapset_right}
) do
cond do
MapSet.equal?(mapset_left, mapset_right) -> :mutually_inclusive
MapSet.disjoint?(mapset_left, mapset_right) -> :mutually_exclusive
true -> :unknown
end

end

def compare(
%__MODULE__{left: left, right: %MapSet{} = mapset},
%Ash.Query.Operator.Has{left: left, right: value}
) do

if MapSet.member?(mapset, value) do
:left_includes_right
else
:mutually_exclusive
end
end

def compare(_, _) do
:unknown
end

def to_string(%{left: left, right: %MapSet{} = mapset}, opts) do
import Inspect.Algebra

list_doc =
case Enum.split(mapset, @inspect_items_limit) do
{left, []} -> to_doc(left, opts)
{left, _} -> concat(to_doc(left, opts), "...")
end

concat([
to_doc(left, opts),
" overlaps ",
list_doc
])
end

def to_string(op, opts), do: super(op, opts)
end
@Zach
ZachDaniel
ZachDaniel3w ago
Can't review at the moment but happy to review in PR 👌
Abu kumathra
Abu kumathraOP3w ago
is it the only thing that I should modify in core?
ZachDaniel
ZachDaniel3w ago
You'd need to also include those in the list of functions in Ash.Query.Filter And write tests for them
Abu kumathra
Abu kumathraOP3w ago
I created the PR I will add the tests and ping you
GitHub
feat: implemented 'has' and 'overlaps' operators by moutikabdessabo...
Initial implementation of has and overlaps operators to allow for intersection and inclusion checks against array fields Contributor checklist Leave anything that you believe does not apply uncheck...
Abu kumathra
Abu kumathraOP3w ago
for tests I will need to add them to test/filter/filter_test.exs right? I will need to probably add an array attribute to the resource for Ets datalayer tests but should I add tests here following the same patterns used with in?
ZachDaniel
ZachDaniel3w ago
Probably yes 🙂 I'm on an airplane Don't want to load up the core **code But if it's got tests for in then it's good for the others
Abu kumathra
Abu kumathraOP3w ago
sorry for bombarding you, and thanks for your patience
ZachDaniel
ZachDaniel3w ago
All good 😂
Abu kumathra
Abu kumathraOP3w ago
@Zach I added the tests
ZachDaniel
ZachDaniel3w ago
I will look this week
Abu kumathra
Abu kumathraOP3w ago
In github You mentioned that it should be implemented as functions
ZachDaniel
ZachDaniel3w ago
Yeah, there are operators and functions See something like string_concat But it's probably fine as is TBH Well...not sure will see what it looks like
Abu kumathra
Abu kumathraOP3w ago
One question, in ash_sql what are the providers that the changes should support, Sqlite/Postgres...

Did you find this page helpful?