lib/vnext_genai/vnext.graph.ex

defmodule GenAI.VNext.Graph do
  @vsn 1.0
  @moduledoc """
  A graph data structure for representing AI graphs, threads, conversations, uml, etc. Utility Class
  """

  use GenAI.Graph.NodeBehaviour

  alias GenAI.Graph.Link
  alias GenAI.Graph.NodeProtocol
  alias GenAI.Records, as: R
  alias GenAI.Types, as: T
  # alias VNextGenAI.Session.NodeProtocol.Records, as: Node
  import GenAI.Records.Link
  import GenAI.Records.Node

  require GenAI.Records.Link
  require GenAI.Records.Node
  require GenAI.Types.Graph
  # require VNextGenAI.Session.NodeProtocol.Records

  @derive GenAI.Graph.NodeProtocol
  # @derive VNextGenAI.Session.NodeProtocol
  defnodetype(
    nodes: %{T.Graph.graph_node_id() => T.Graph.graph_node()},
    node_handles: %{T.handle() => T.Graph.graph_node_id()},
    links: %{T.Graph.graph_link_id() => T.Graph.graph_link()},
    link_handles: %{T.handle() => T.Graph.graph_link_id()},
    head: T.Graph.graph_node_id() | nil,
    last_node: T.Graph.graph_node_id() | nil,
    last_link: T.Graph.graph_link_id() | nil
  )

  defnodestruct(
    nodes: %{},
    node_handles: %{},
    links: %{},
    link_handles: %{},
    head: nil,
    last_node: nil,
    last_link: nil,
    settings: nil
  )
  
  
  
  
  def inspect_custom_details(subject, opts) do
    [
      "nodes:", Inspect.Algebra.to_doc(subject.nodes, opts), ", ",
      "links:", Inspect.Algebra.to_doc(subject.links, opts), ", ",
      "head:", Inspect.Algebra.to_doc(subject.head, opts), ", ",
      "last_node:", Inspect.Algebra.to_doc(subject.last_node, opts), ", ",
      "last_link:", Inspect.Algebra.to_doc(subject.last_link, opts), ", ",
      "settings:", Inspect.Algebra.to_doc(subject.settings, opts), ", ",
    ]
  end
  
  
  def process_node(%__MODULE__{} = subject, link, _, session, context, options) do
    with {:ok, head} <- GenAI.VNext.Graph.head(subject) do
      do_process_node(head, link, subject, session, context, options)
    end
  end

  def do_process_node(subject, link, container, session, context, options) do
    case GenAI.Graph.NodeProtocol.process_node(
           subject,
           link,
           container,
           session,
           context,
           options
         ) do
      GenAI.Records.Node.process_next(
        element: element_context(element: target, link: link, container: container),
        session: session
      ) ->
        do_process_node(target, link, container, session, context, options)

      x = process_end() ->
        x

      x = process_error() ->
        x

      x = process_yield() ->
        x
    end
  end

  @spec do_new() :: __MODULE__.t()
  @spec do_new(keyword) :: __MODULE__.t()
  def do_new(options \\ nil)

  def do_new(options) do
    settings =
      Keyword.merge(
        [
          auto_head: false,
          update_last: true,
          update_last_link: false,
          auto_link: false
        ],
        options[:settings] || []
      )

    %__MODULE__{
      id: options[:id] || UUID.uuid4(),
      handle: options[:handle] || nil,
      name: options[:name] || nil,
      description: options[:description] || nil,
      nodes: %{},
      node_handles: %{},
      links: %{},
      link_handles: %{},
      head: nil,
      last_node: nil,
      last_link: nil,
      settings: settings
    }
  end

  @spec setting(__MODULE__.t(), atom, keyword, any) :: any
  def setting(%__MODULE__{settings: settings}, setting, options, default \\ nil) do
    x = options[setting]

    cond do
      is_nil(x) or x == :default ->
        x = settings[setting]

        cond do
          x == nil -> default
          :else -> x
        end

      :else ->
        x
    end
  end

  # =============================================================================
  # Graph Protocol
  # =============================================================================

  # -------------------------
  # node/2
  # -------------------------

  @doc """
  Obtain node by id.

  ## Examples

  ### When Found
      iex> graph = GenAI.VNext.Graph.new()
      ...> node = GenAI.Graph.Node.new(id: UUID.uuid4())
      ...> graph = GenAI.VNext.Graph.add_node(graph, node)
      ...> GenAI.VNext.Graph.node(graph, node.id)
      {:ok, node}

  ### When Not Found
      iex> graph = GenAI.VNext.Graph.new()
      ...> GenAI.VNext.Graph.node(graph, UUID.uuid4())
      {:error, {:node, :not_found}}
  """
  @spec node(graph :: T.Graph.graph(), id :: T.Graph.graph_node_id()) ::
          T.result(T.Graph.graph_node(), T.details())
  def node(graph, graph_node)

  def node(graph, R.Link.connector(node: id)) do
    node(graph, id)
  end

  def node(graph, graph_node) when T.Graph.is_node_id(graph_node) do
    if x = graph.nodes[graph_node] do
      {:ok, x}
    else
      {:error, {:node, :not_found}}
    end
  end

  def node(graph, graph_node) when is_struct(graph_node) do
    with {:ok, id} <- NodeProtocol.id(graph_node) do
      node(graph, id)
    end
  end

  def do_node(graph, graph_node), do: node(graph, graph_node)

  @spec nodes(T.Graph.graph()) :: {:ok, list(T.Graph.graph_node())}
  @spec nodes(T.Graph.graph(), keyword) :: {:ok, list(T.Graph.graph_node())}
  def nodes(graph, options \\ nil)

  def nodes(graph, _) do
    nodes = Map.values(graph.nodes)
    {:ok, nodes}
  end

  # -------------------------
  # node!/2
  # -------------------------
  @spec nodes!(T.Graph.graph()) :: list(T.Graph.graph_node())
  @spec nodes!(T.Graph.graph(), keyword) :: list(T.Graph.graph_node())
  def nodes!(graph, options \\ nil)

  def nodes!(graph, _) do
    Map.values(graph.nodes)
  end

  # -------------------------
  # link/2
  # -------------------------

  @doc """
  Obtain link by id.

  ## Examples

  ### When Found
      iex> graph = GenAI.VNext.Graph.new()
      ...> node1 = GenAI.Graph.Node.new(id: UUID.uuid4())
      ...> node2 = GenAI.Graph.Node.new(id: UUID.uuid4())
      ...> graph = GenAI.VNext.Graph.add_node(graph, node1)
      ...> graph = GenAI.VNext.Graph.add_node(graph, node2)
      ...> link = GenAI.Graph.Link.new(node1.id, node2.id)
      ...> graph = GenAI.VNext.Graph.add_link(graph, link)
      ...> GenAI.VNext.Graph.link(graph, link.id)
      {:ok, link}

  ### When Not Found
      iex> graph = GenAI.VNext.Graph.new()
      ...> GenAI.VNext.Graph.link(graph, UUID.uuid4())
      {:error, {:link, :not_found}}
  """
  @spec link(graph :: T.Graph.graph(), id :: T.Graph.graph_link_id()) ::
          T.result(T.Graph.graph_link(), T.details())
  def link(graph, graph_link)

  def link(graph, graph_link) when T.Graph.is_link_id(graph_link) do
    if x = graph.links[graph_link] do
      {:ok, x}
    else
      {:error, {:link, :not_found}}
    end
  end

  def link(graph, graph_link) when is_struct(graph_link) do
    with {:ok, id} <- Link.id(graph_link) do
      node(graph, id)
    end
  end

  # -------------------------
  # member?/2
  # -------------------------

  @doc """
  Check if a node is a member of the graph.

  ## Examples

      iex> graph = GenAI.VNext.Graph.new()
      ...> node = GenAI.Graph.Node.new(id: UUID.uuid4())
      ...> graph = GenAI.VNext.Graph.add_node(graph, node)
      ...> GenAI.VNext.Graph.member?(graph, node.id)
      true

      iex> graph = GenAI.VNext.Graph.new()
      ...> GenAI.VNext.Graph.member?(graph, UUID.uuid4())
      false
  """
  @spec member?(graph :: T.Graph.graph(), id :: T.Graph.graph_node_id()) :: boolean
  def member?(graph, graph_node)

  def member?(graph, graph_node) when T.Graph.is_node_id(graph_node) do
    (graph.nodes[graph_node] && true) || false
  end

  def member?(graph, graph_node) when is_struct(graph_node) do
    with {:ok, id} <- NodeProtocol.id(graph_node) do
      member?(graph, id)
    end
  end

  # -------------------------
  # by_handle/2
  # -------------------------

  @doc """
  Obtain node by handle.

  ## Examples

  ### When Found
      iex> graph = GenAI.VNext.Graph.new()
      ...> node = GenAI.Graph.Node.new(id: UUID.uuid4(), handle: :foo)
      ...> graph = GenAI.VNext.Graph.add_node(graph, node)
      ...> GenAI.VNext.Graph.by_handle(graph, :foo)
      {:ok, node}

  ### When Not Found
      iex> graph = GenAI.VNext.Graph.new()
      ...> GenAI.VNext.Graph.by_handle(graph, :foo)
      {:error, {:handle, :not_found}}
  """
  @spec by_handle(graph :: T.Graph.graph(), handle :: T.handle()) ::
          T.result(T.Graph.graph_node(), T.details())
  def by_handle(graph, handle)

  def by_handle(graph, handle) do
    if x = graph.node_handles[handle] do
      node(graph, x)
    else
      {:error, {:handle, :not_found}}
    end
  end

  # -------------------------
  # link_by_handle/2
  # -------------------------

  @doc """
  Obtain link by handle.

  ## Examples

  ### When Found
      iex> graph = GenAI.VNext.Graph.new()
      ...> node1 = GenAI.Graph.Node.new(id: UUID.uuid4())
      ...> node2 = GenAI.Graph.Node.new(id: UUID.uuid4())
      ...> graph = GenAI.VNext.Graph.add_node(graph, node1)
      ...> graph = GenAI.VNext.Graph.add_node(graph, node2)
      ...> link = GenAI.Graph.Link.new(node1.id, node2.id, handle: :bar)
      ...> graph = GenAI.VNext.Graph.add_link(graph, link)
      ...> GenAI.VNext.Graph.link_by_handle(graph, :bar)
      {:ok, link}

  ### When Not Found
      iex> graph = GenAI.VNext.Graph.new()
      ...> GenAI.VNext.Graph.link_by_handle(graph, :bar)
      {:error, {:handle, :not_found}}
  """
  @spec link_by_handle(graph :: T.Graph.graph(), handle :: T.handle()) ::
          T.result(T.Graph.graph_link(), T.details())
  def link_by_handle(graph, handle)

  def link_by_handle(graph, handle) do
    if x = graph.link_handles[handle] do
      link(graph, x)
    else
      {:error, {:handle, :not_found}}
    end
  end

  # -------------------------
  # head/1
  # -------------------------
  @spec head(T.Graph.graph()) :: T.result(T.Graph.graph_node(), T.details())
  def head(graph)
  def head(%__MODULE__{head: nil}), do: {:error, {:head, :is_nil}}
  def head(graph = %__MODULE__{head: x}), do: node(graph, x)

  # -------------------------
  # last_node/1
  # -------------------------
  @spec last_node(T.Graph.graph()) :: T.result(T.Graph.graph_node(), T.details())
  def last_node(graph)
  def last_node(%__MODULE__{last_node: nil}), do: {:error, {:last_node, :is_nil}}
  def last_node(graph = %__MODULE__{last_node: x}), do: node(graph, x)

  # -------------------------
  # last_link/1
  # -------------------------
  @spec last_link(T.Graph.graph()) :: T.result(T.Graph.graph_link(), T.details())
  def last_link(graph)
  def last_link(%__MODULE__{last_link: nil}), do: {:error, {:last_link, :is_nil}}
  def last_link(graph = %__MODULE__{last_link: x}), do: link(graph, x)

  @spec attempt_set_handle(T.Graph.graph(), T.Graph.graph_node_id(), T.Graph.graph_node()) ::
          T.Graph.graph()
  defp attempt_set_handle(graph, id, node) do
    with {:ok, handle} <- NodeProtocol.handle(node) do
      if graph.node_handles[handle] do
        raise GenAI.Graph.Exception,
          message: "Node with handle #{handle} already defined in graph",
          details: {:handle_exists, handle}
      end

      put_in(graph, [Access.key(:node_handles), handle], id)
    else
      _ -> graph
    end
  end

  @spec attempt_set_head(T.Graph.graph(), T.Graph.graph_node_id(), T.Graph.graph_node(), keyword) ::
          T.Graph.graph()
  defp attempt_set_head(graph, id, node, options)

  defp attempt_set_head(graph, id, _, options) do
    if setting(graph, :auto_head, options, false) || options[:head] == true do
      update_in(graph, [Access.key(:head)], &(&1 || id))
    else
      graph
    end
  end

  @spec attempt_set_last_node(
          T.Graph.graph(),
          T.Graph.graph_node_id(),
          T.Graph.graph_node(),
          keyword
        ) :: T.Graph.graph()
  defp attempt_set_last_node(graph, id, node, options)

  defp attempt_set_last_node(graph, id, _, options) do
    if setting(graph, :update_last, options, false) do
      put_in(graph, [Access.key(:last_node)], id)
    else
      graph
    end
  end

  @spec auto_link_setting(T.Graph.graph(), keyword) :: any
  defp auto_link_setting(graph, options) do
    case options[:link] do
      true ->
        graph.settings[:auto_link] || true

      false ->
        false

      default when default in [nil, :default] ->
        graph.settings[:auto_link] || false

      {:template, template} ->
        if x = graph.settings[:auto_link_templates][template] do
          x
        else
          raise GenAI.Graph.Exception,
            message: "Auto Link Template #{inspect(template)} Not Found",
            details: {:template_not_found, template}
        end

      x ->
        x
    end
  end

  @spec attempt_auto_link(
          T.Graph.graph(),
          T.Graph.graph_node_id(),
          T.Graph.graph_node_id(),
          T.Graph.graph_node(),
          keyword
        ) :: T.Graph.graph()
  defp attempt_auto_link(graph, from_node, node_id, node, options)

  defp attempt_auto_link(graph, from_node, node_id, _, options) do
    auto_link = auto_link_setting(graph, options)

    cond do
      auto_link == false ->
        graph

      auto_link == true ->
        link = Link.new(from_node, node_id)
        GenAI.VNext.Graph.add_link(graph, link, options)

      is_struct(auto_link, Link) ->
        link = auto_link

        with {:ok, link} <- Link.putnew_source(link, from_node),
             {:ok, link} <- Link.putnew_target(link, node_id),
             {:ok, link} <- Link.with_id(link) do
          GenAI.VNext.Graph.add_link(graph, link, options)
        else
          {:error, details} ->
            raise GenAI.Graph.Exception,
              message: "Auto Link Failed",
              details: details
        end

      not is_struct(auto_link) and (is_map(auto_link) or is_list(auto_link)) ->
        auto_link_options = auto_link
        link = Link.new(from_node, node_id, auto_link_options)
        GenAI.VNext.Graph.add_link(graph, link, options)
    end
  end

  @spec attempt_set_node(T.Graph.graph(), T.Graph.graph_node_id(), T.Graph.graph_node(), keyword) ::
          T.Graph.graph()
  def attempt_set_node(graph, node_id, graph_node, options)

  def attempt_set_node(graph, node_id, graph_node, _) do
    if member?(graph, node_id) do
      raise GenAI.Graph.Exception,
        message: "Node with #{node_id} already defined in graph",
        details: {:node_exists, node_id}
    end

    put_in(graph, [Access.key(:nodes), node_id], graph_node)
  end

  # -------------------------
  # attach_node/3
  # -------------------------

  @doc """
  Attach a node to the graph linked to last inserted item.

  ## Examples

      iex> graph = GenAI.VNext.Graph.new()
      ...> node = GenAI.Graph.Node.new(id: UUID.uuid4())
      ...> graph = GenAI.VNext.Graph.attach_node(graph, node)
      ...> GenAI.VNext.Graph.member?(graph, node.id)
      true
  """
  @spec attach_node(graph :: T.Graph.graph(), node :: T.Graph.graph_node(), options :: map) ::
          T.result(T.Graph.graph(), T.details())
  def attach_node(graph, graph_node, options \\ nil)

  def attach_node(graph, graph_node, options) do
    options =
      Keyword.merge(
        [auto_head: true, update_last: true, update_last_link: true, link: true],
        options || []
      )

    add_node(graph, graph_node, options)
  end

  # -------------------------
  # add_node/3
  # -------------------------

  @doc """
  Add a node to the graph.

  ## Examples

      iex> graph = GenAI.VNext.Graph.new()
      ...> node = GenAI.Graph.Node.new(id: UUID.uuid4())
      ...> graph = GenAI.VNext.Graph.add_node(graph, node)
      ...> GenAI.VNext.Graph.member?(graph, node.id)
      true
  """
  @spec add_node(graph :: T.Graph.graph(), node :: T.Graph.graph_node(), options :: map) ::
          T.result(T.Graph.graph(), T.details())
  def add_node(graph, graph_node, options \\ nil)

  def add_node(graph, graph_node, options) do
    with {:ok, node_id} <- NodeProtocol.id(graph_node) do
      graph
      |> attempt_set_node(node_id, graph_node, options)
      |> attempt_set_handle(node_id, graph_node)
      |> attempt_set_head(node_id, graph_node, options)
      |> attempt_set_last_node(node_id, graph_node, options)
      |> attempt_auto_link(graph.last_node, node_id, graph_node, options)
    else
      x -> x
    end
  end

  @spec local_reference?(T.Graph.graph_link(), T.Graph.graph_link()) :: boolean
  defp local_reference?(source, target) do
    if R.Link.connector(source, :external) && R.Link.connector(target, :external) do
      false
    else
      true
    end
  end

  # -------------------------
  # add_node/3
  # -------------------------

  @doc """
  Add a link to the graph.

  ## Examples

      iex> graph = GenAI.VNext.Graph.new()
      ...> node1 = GenAI.Graph.Node.new(id: UUID.uuid4())
      ...> node2 = GenAI.Graph.Node.new(id: UUID.uuid4())
      ...> graph = GenAI.VNext.Graph.add_node(graph, node1)
      ...> graph = GenAI.VNext.Graph.add_node(graph, node2)
      ...> link = GenAI.Graph.Link.new(node1.id, node2.id)
      ...> graph = GenAI.VNext.Graph.add_link(graph, link)
      ...> GenAI.VNext.Graph.link(graph, link.id)
      {:ok, link}
  """
  @spec add_link(graph :: T.Graph.graph(), link :: T.Graph.graph_link(), options :: map) ::
          T.result(T.Graph.graph(), T.details())
  def add_link(graph, graph_link, options \\ nil)

  def add_link(graph, graph_link, options) do
    with {:ok, link_id} <- Link.id(graph_link),
         {:ok, source} <- Link.source_connector(graph_link),
         {:ok, target} <- Link.target_connector(graph_link),
         true <- local_reference?(source, target) || {:error, {:link, :local_reference_required}} do
      graph
      |> attempt_set_link(link_id, graph_link, options)
      |> attempt_set_last_link(link_id, graph_link, options)
      |> attempt_register_link(source, graph_link, options)
      |> attempt_register_link(target, graph_link, options)
    else
      {:error, details} ->
        raise GenAI.Graph.Exception,
          message: "Link Failure - #{inspect(details)}",
          details: details
    end
  end

  @spec attempt_set_link(T.Graph.graph(), T.Graph.graph_link_id(), T.Graph.graph_link(), keyword) ::
          T.Graph.graph()
  defp attempt_set_link(graph, link_id, graph_link, options)

  defp attempt_set_link(graph, link_id, graph_link, _) do
    if Map.has_key?(graph.links, link_id) do
      raise GenAI.Graph.Exception,
        message: "Link with #{link_id} already defined in graph",
        details: {:link_exists, link_id}
    end

    with {:ok, handle} <- Link.handle(graph_link) do
      graph
      |> put_in([Access.key(:link_handles), handle], link_id)
      |> put_in([Access.key(:links), link_id], graph_link)
    else
      _ ->
        put_in(graph, [Access.key(:links), link_id], graph_link)
    end
  end

  @spec attempt_set_last_link(
          T.Graph.graph(),
          T.Graph.graph_link_id(),
          T.Graph.graph_link(),
          keyword
        ) :: T.Graph.graph()
  defp attempt_set_last_link(graph, link_id, graph_link, options)

  defp attempt_set_last_link(graph, link_id, _, options) do
    if setting(graph, :update_last_link, options, false) do
      put_in(graph, [Access.key(:last_link)], link_id)
    else
      graph
    end
  end

  @spec attempt_register_link(
          T.Graph.graph(),
          T.Graph.graph_link(),
          T.Graph.graph_link(),
          keyword
        ) :: T.Graph.graph()
  defp attempt_register_link(graph, connector, link, options) do
    connector_node_id = R.Link.connector(connector, :node)

    cond do
      R.Link.connector(connector, :external) ->
        graph

      member?(graph, connector_node_id) ->
        n = graph.nodes[connector_node_id]
        {:ok, n} = NodeProtocol.register_link(n, graph, link, options)
        put_in(graph, [Access.key(:nodes), connector_node_id], n)

      :else ->
        raise GenAI.Graph.Exception,
          message: "Node Not Found",
          details: {:source_not_found, connector}
    end
  end
end

defimpl GenAI.Graph.MermaidProtocol, for: GenAI.VNext.Graph do
  @spec mermaid_id(any) :: any
  def mermaid_id(subject) do
    GenAI.Graph.MermaidProtocol.Helpers.mermaid_id(subject.id)
  end

  @spec encode(any) :: {:ok, String.t()} | {:error, any}
  def encode(graph_element), do: encode(graph_element, [])

  @spec encode(any, any) :: {:ok, String.t()} | {:error, any}
  def encode(graph_element, options), do: encode(graph_element, options, %{})

  @spec encode(any, any, any) :: {:ok, String.t()} | {:error, any}
  def encode(graph_element, options, state) do
    case GenAI.Graph.MermaidProtocol.Helpers.diagram_type(options) do
      :state_diagram_v2 -> state_diagram_v2(graph_element, options, state)
      x -> {:error, {:unsupported_diagram, x}}
    end
  end

  @spec state_diagram_v2(any, keyword, map) :: {:ok, String.t()} | {:error, any}
  def state_diagram_v2(graph_element, options, state) do
    identifier = mermaid_id(graph_element)

    headline = """
    stateDiagram-v2
    """

    if graph_element.nodes == %{} do
      body =
        GenAI.Graph.MermaidProtocol.Helpers.indent("""
        [*] --> #{identifier}
        state "Empty Graph" as #{identifier}
        """)

      graph = headline <> body
      {:ok, graph}
    else
      entry_point =
        if head = graph_element.head do
          """
          [*] --> #{GenAI.Graph.MermaidProtocol.Helpers.mermaid_id(head)}
          """
        else
          ""
        end

      # We need expanded nodes with link details
      state = update_in(state, [:container], &[graph_element | &1 || []])

      contents =
        graph_element.nodes
        |> Enum.map(fn {_, n} ->
          GenAI.Graph.MermaidProtocol.encode(n, options, state)
        end)
        |> Enum.map_join("\n", fn {:ok, x} -> x end)

      body = GenAI.Graph.MermaidProtocol.Helpers.indent(entry_point <> contents)

      graph = headline <> body
      {:ok, graph}
    end
  end
end