defmodule GenAI.Graph.Root do @vsn 1.0 @moduledoc """ The root data structure that contains nested graphs, nodes and other structures. """ import GenAI.Records.Link require GenAI.Records.Link require GenAI.Types.Graph defstruct [ # Entry Point graph: nil, # uuid => element_lookup, handle => {:resolver, %{paths => element_entry}} | element_entry lookup_table: %{by_element: %{}, by_handle: %{}}, # id of last inserted node last_node: nil, # id of last inserted link last_link: nil ] @doc """ Retrieve a element_lookup entry by element id. """ def element_entry(this, element) def element_entry(_, error = {:error, _}), do: error def element_entry(this, element_lookup(element: element)), do: element_entry(this, element) def element_entry(%__MODULE__{} = this, id) do if entry = this.lookup_table.by_element[id] do {:ok, entry} else {:error, {:element, :not_found}} end end @doc """ Retrieve an element_lookup entry by handle and base node """ def handle_entry(this, handle, base \\ nil) def handle_entry(%__MODULE__{} = this, graph_handle(name: handle), base), do: handle_entry(this, handle, base) def handle_entry(%__MODULE__{} = this, handle, base) do if global_entry = this.lookup_table.by_handle[graph_handle(scope: :global, name: handle)] do element_entry(this, global_entry) else standard_entry = this.lookup_table.by_handle[graph_handle(scope: :standard, name: handle)] local_entries = this.lookup_table.by_handle[graph_handle(scope: :local, name: handle)] || [] with {:ok, element_lookup(path: path)} <- element_entry(this, base) do {_, entry} = local_entries |> Enum.reduce( {0, standard_entry}, fn {entry_path, entry_element}, b = {b_depth, _} -> e_depth = length(entry_path) cond do e_depth > b_depth && List.starts_with?(path, entry_path) -> {e_depth, entry_element} :else -> b end end ) if entry, do: element_entry(this, entry), else: {:error, {:element, :not_found}} else _ -> if standard_entry, do: element_entry(this, standard_entry), else: {:error, {:element, :not_found}} end end end @max_depth 1_000_000 defp if_element_under_path(this, element, under) defp if_element_under_path(_, nil, _), do: nil defp if_element_under_path(this, element, element_lookup(path: under_path)) do case element_entry(this, element) do entry = {:ok, element_lookup(path: entry_path)} -> if List.starts_with?(entry_path, under_path), do: entry _ -> nil end end @doc """ Retrieve closest nested handle nested under base. If a global entry is found and under base it will be used. If a standard entry is found and under base it will be used. Only if no standard or global entry under base exists will locals be checked. """ def nearest_handle_entry(this, handle, base) def nearest_handle_entry(%__MODULE__{} = this, graph_handle(name: handle), base), do: nearest_handle_entry(this, handle, base) def nearest_handle_entry(%__MODULE__{} = this, handle, base = element_lookup(path: under_path)) do global_entry = this.lookup_table.by_handle[graph_handle(scope: :global, name: handle)] standard_entry = this.lookup_table.by_handle[graph_handle(scope: :standard, name: handle)] local_entries = this.lookup_table.by_handle[graph_handle(scope: :local, name: handle)] || [] cond do x = if_element_under_path(this, global_entry, base) -> x x = if_element_under_path(this, standard_entry, base) -> x :else -> {_, entry} = local_entries |> Enum.reduce( {@max_depth, nil}, fn {entry_path, entry_element}, b = {b_depth, _} -> e_depth = length(entry_path) cond do e_depth < b_depth && List.starts_with?(entry_path, under_path) -> {e_depth, entry_element} :else -> b end end ) if entry, do: element_entry(this, entry), else: {:error, {:element, :not_found}} end end @doc """ Retrieve a nested element by id from graph """ def element(%__MODULE__{} = this, id) do with {:ok, element_lookup(path: [_ | path])} <- element_entry(this, id) do get_nested_element(this.graph, path) end end @doc """ Retrieve a nested element by handle from graph """ def element_by_handle(%__MODULE__{} = this, handle, base \\ nil) do with {:ok, element_lookup(path: [_ | path])} <- handle_entry(this, handle, base) do get_nested_element(this.graph, path) end end @doc """ extract nested entry by path from source element. """ def get_nested_element(source, path) def get_nested_element(nil, _) do {:error, {:element, :not_found}} end def get_nested_element(element, []) do {:ok, element} end def get_nested_element(element, [h | t]) do with {:ok, nested} <- element.__struct__.node(element, h) do get_nested_element(nested, t) end end @doc """ Merge element lookup entries """ def merge_lookup_table_entries(%__MODULE__{} = this, entries) do update_in( this, [Access.key(:lookup_table), Access.key(:by_element)], &Enum.into(entries, &1 || %{}) ) end @doc """ Merge list of {handle, element or path to element} entries into lookup table. """ def merge_handles(%__MODULE__{} = this, handles) when is_list(handles) do Enum.reduce(handles, this, &merge_handle(&2, &1)) end @doc """ Merge a single graph handle entry into lookup table. """ def merge_handle(this, {handle = graph_handle(scope: :global), element}) do put_in(this, [Access.key(:lookup_table), Access.key(:by_handle), handle], element) end def merge_handle(this, {handle = graph_handle(scope: :standard), element}) do put_in(this, [Access.key(:lookup_table), Access.key(:by_handle), handle], element) end def merge_handle(this, {handle = graph_handle(scope: :local), elements}) do elements = (is_list(elements) && elements) || [elements] update_in( this, [Access.key(:lookup_table), Access.key(:by_handle), handle], &(elements ++ (&1 || [])) ) end @doc """ Retrieve graph context by link """ def graph_context_by_link(this, link, from_element) def graph_context_by_link( this, %GenAI.Graph.Link{target: connector(node: target)} = link, from_element ) do {:ok, base} = GenAI.Graph.NodeProtocol.id(from_element || this.graph) base = element_entry(this, base) case target do anchor_link(anchor_handle: anchor, local_handle: path) -> anchor_base = handle_entry(this, anchor, base) graph_context_by_link__to_anchor_path(this, link, path, anchor_base) handle = graph_handle() -> graph_context_by_link__to_handle(this, link, handle, base) element -> graph_context_by_link__to_element(this, link, element) end end # -------------- # graph_context_by_link__to_anchor_path/5 # -------------- defp graph_context_by_link__to_anchor_path(this, link, path, base) defp graph_context_by_link__to_anchor_path(this, link, path, base) when is_list(path) do entry = reduce_anchor_path(this, path, base) graph_context_by_link__entry(this, link, entry) end defp graph_context_by_link__to_anchor_path(this, link, nil, base), do: graph_context_by_link__to_anchor_path(this, link, [], base) defp graph_context_by_link__to_anchor_path(this, link, path, base) when not is_list(path), do: graph_context_by_link__to_anchor_path(this, link, [path], base) # -------------- # graph_context_by_link__to_handle/4 # -------------- defp graph_context_by_link__to_handle(this, link, handle, base) do # return element context for a given element handle under base graph_context_by_link__entry(this, link, handle_entry(this, handle, base)) end # -------------- # graph_context_by_link__to_element/3 # -------------- defp graph_context_by_link__to_element(this, link, element) do # return element context for a given element identifier graph_context_by_link__entry(this, link, element_entry(this, element)) end # -------------- # graph_context_by_link__entry/3 # -------------- defp graph_context_by_link__entry(this, link, entry) defp graph_context_by_link__entry(this, link, {:ok, entry}), do: graph_context_by_link__entry(this, link, entry) defp graph_context_by_link__entry(this, link, element_lookup(path: [_ | path])) do # return element context for a given element lookup entry with {container_path, target_path} <- Enum.split(path, -1), {:ok, target_container} <- get_nested_element(this.graph, container_path), {:ok, target_element} <- get_nested_element(target_container, target_path) do {:ok, element_context(element: target_element, link: link, container: target_container)} end end defp graph_context_by_link__entry(_, _, entry = {:error, _}), do: entry # -------------- # nearest_handle_entry/3 # -------------- defp reduce_anchor_path(this, path, base) defp reduce_anchor_path(this, path, {:ok, base}), do: reduce_anchor_path(this, path, base) defp reduce_anchor_path(_, _, error = {:error, _}), do: error defp reduce_anchor_path(this, path, base) do Enum.reduce(path, {:ok, base}, fn handle, {:ok, base} -> nearest_handle_entry(this, handle, base) _, error -> error end) end def process_node(subject, _, _, session, context, options) do subject = subject |> GenAI.Graph.Root.merge_lookup_table_entries( GenAI.Graph.NodeProtocol.build_node_lookup(subject.graph, []) ) |> GenAI.Graph.Root.merge_handles( GenAI.Graph.NodeProtocol.build_handle_lookup(subject.graph, []) ) session = %{session | root: subject} GenAI.Graph.NodeProtocol.process_node(subject.graph, nil, subject, session, context, options) end end # # defimpl GenAI.Thread.SessionProtocol, for: GenAI.Graph.Root do # require GenAI.Records.Session # alias GenAI.Records.Session, as: Node # # def process_node(subject, scope, context, options) do # subject = subject # |> GenAI.Graph.Root.merge_lookup_table_entries(GenAI.Graph.NodeProtocol.build_node_lookup(subject.graph, [])) # |> GenAI.Graph.Root.merge_handles(GenAI.Graph.NodeProtocol.build_handle_lookup(subject.graph, [])) # # updated_scope = Node.scope( # scope, # graph_node: subject.graph, # graph_link: nil, # graph_container: subject, # session_root: subject # ) # GenAI.Thread.SessionProtocol.process_node(subject.graph, updated_scope, context, options) # end # end