lib/langchain/program_template.ex


defmodule LangChain.ProgramTemplate do
  @moduledoc """
  A ProgramTemplate is a template that is filled out and fed to a programming language interpreter,
  compiler or VM. In most cases, this means we are passing Elixir code to the BEAM.
  This is an example of migrating data from the AI knowledge domain to the programming domain.
  """

  @derive Jason.Encoder
  defstruct [
    description: "Hello World",
    # template is a string

    template: "IO.puts \"Hello World\"",
    # input_variables is a list of variables that need to be specified for that template

    input_variables: [],
    # partial_variables is a map of variables that have already been specified and can be applied to the template

    partial_variables: %{},
    language: :elixir
  ]

  # You may want to move this function to a common module if it's used by both LangChain.ProgramTemplate and LangChain.PromptTemplate

  def format(template, values) do
    # env is a keyword list merging values and partials and convert

    env =
      Map.merge(values, template.partial_variables)
      |> Map.to_list()
      |> Enum.map(fn {k, v} ->
        if is_function(v) do
          case v.() do
            {:ok, res} -> {k, res}
            _ -> {k, "[template function #{k} failed to render]"}
          end
        else
          {k, v}
        end
      end)

    outcome = template.template |> EEx.eval_string(env)
    {:ok, outcome}
  end

  def partial(template, partial) do
    keys = Map.keys(partial)
    input_variables_without_partial = template.input_variables -- keys
    partial_variables = Map.merge(template.partial_variables, partial)

    {:ok,
     %LangChain.ProgramTemplate{
       template: template.template,
       input_variables: input_variables_without_partial,
       partial_variables: partial_variables
     }}
  end

  # in theory ProgramTemplates could be executed

  # by an interpreter or compiler, but for now we just

  # support elixir

  def execute(program_template) do
    case program_template.language do
      :elixir ->
        try do
          {result, _bindings} = Code.eval_string(program_template.template)
          {:ok, result}
        rescue
          e ->
            {:error, e}
        end
      _ ->
    end
  end

  def can_execute(program_template) do
    program_template.input_variables == []
  end
end