<!-- livebook:{"persist_outputs":true} -->

# Executing Custom Elixir Functions

```elixir
Mix.install([
  {:langchain, "~> 0.3.0"}
])
```

## What we're doing

This notebook shows how to use the Elixir [LangChain](https://github.com/brainlid/langchain) library to expose an Elixir function as a tool that can be executed by an LLM like ChatGPT, Anthropic and others. The LangChain library wraps this all up making it easy and portable between different LLMs.

## The Elixir Function in our App

Let's define the Elixir function we want to expose to ChatGPT so we can see how it works.

In this example we'll create a `get_user_info` function that takes a user ID and returns the relevant user's information for the current user to a web app.

For simplicity, we're skipping an actual database and storing our fake records on the module.

```elixir
defmodule MyApp do
  @pretend_db %{
    1 => %{user_id: 1, name: "Michael Johnson", account_type: :trial, favorite_animal: "Horse"},
    2 => %{user_id: 2, name: "Joan Jett", account_type: :member, favorite_animal: "Aardvark"}
  }

  def get_user_info(user_id) do
    @pretend_db[user_id]
  end
end
```

<!-- livebook:{"output":true} -->

```
{:module, MyApp, <<70, 79, 82, 49, 0, 0, 7, ...>>, {:get_user_info, 1}}
```

It's a simple lookup using a `user_id` to find return a map of a user's data.

```elixir
MyApp.get_user_info(1)
```

<!-- livebook:{"output":true} -->

```
%{name: "Michael Johnson", user_id: 1, account_type: :trial, favorite_animal: "Horse"}
```

With our function ready, let's cover how we can give the LLM access to this.

## Exposing our Function to an LLM

With an Elixir function defined, we will wrap it in a LangChain `Function` structure so it can be easily shared with an LLM.

This is what that looks like:

```elixir
alias LangChain.Function

function =
  Function.new!(%{
    name: "get_user_info",
    description: "Return JSON object of the current users's relevant information.",
    function: fn _args, %{user_id: user_id} = _context ->
      # This uses the user_id provided through the context to call our Elixir function.
      {:ok, Jason.encode!(MyApp.get_user_info(user_id))}
    end
  })
```

<!-- livebook:{"output":true} -->

```
%LangChain.Function{
  name: "get_user_info",
  description: "Return JSON object of the current users's relevant information.",
  display_text: nil,
  strict: false,
  function: #Function<41.18682967/2 in :erl_eval.expr/6>,
  async: true,
  parameters_schema: nil,
  parameters: []
}
```

The function `name` we provide is how the LLM will execute the function if the LLM chooses to call it.

The `description` is for the LLM to know what the function can do so it can decide which function to call for which purpose.

The `function` argument is passed an anonymous function whose job it is to be the glue that bridges data coming from the LLM with context from our application before calling other functions from our application.

The `LangChain.Function` acts as a bridge between the LLM and our application. The Elixir function receives 2 arguments. The first is any arguments passed to the function by the LLM if we defined any as being required. In this example, the LLM doesn't provide any arguments. The second argument is an application context that we'll get to next.

The `context` is specific to our application and does not go through the LLM at all. Think of this as the current user logged into our Phoenix web application. We want the user's interaction with the LLM to be relevant and limited to only what the current user can see and do.

For returning the final result, since the LLM responses must be text, we convert the returned Map into JSON. Additionally, we return it inside an `{:ok, json}` tuple, letting the library know that the function call was successful.

## Setting up our LangChain API Key

We need to setup the LangChain library to connect with ChatGPT using our API key. In a real Elixir application, this would be done in the `config/config.exs` file using something like this:

<!-- livebook:{"force_markdown":true} -->

```elixir
config :langchain, :openai_key, fn -> System.fetch_env!("OPENAI_API_KEY") end
```

<!-- livebook:{"break_markdown":true} -->

For the Livebook notebook, use the "Secrets" on the sidebar to create an `OPENAI_API_KEY` secret with you API key. That is accessible here using `"LB_OPENAI_API_KEY"`.

```elixir
Application.put_env(:langchain, :openai_key, System.fetch_env!("LB_OPENAI_API_KEY"))
```

<!-- livebook:{"output":true} -->

```
:ok
```

## Defining our AI Assistant

We'll use the `LangChain.Message` struct to define the messages for what we want the LLM to do. Our `system` message instructs the LLM how to behave.

In this example, we want the assistant to generate Haiku poems about the current user's favorite animal. However, we only want it to work for users who are "members" and not "trial" users.

The instructions we're giving the LLM will require it to execute the function to get additional information. Yes, this is both a simple and contrived example. In a real system, we wouldn't even make the API call to the server for a "trial" user and we could pass along the additional information with the first request.

What we're demonstrating here is that the LLM can interact with our Elixir application, use multiple pieces of returned information to make business logic decisions and fulfil our system requests.

```elixir
alias LangChain.Message

messages = [
  Message.new_system!(~s(You are a helpful haiku poem generating assistant.
    ONLY generate a haiku for users with an `account_type` of "member".
    If the user has an `account_type` of "trial", say you can't do it,
    but you would love to help them if they upgrade and become a member.)),
  Message.new_user!("The current user is requesting a Haiku poem about their favorite animal.")
]
```

<!-- livebook:{"output":true} -->

```
[
  %LangChain.Message{
    content: "You are a helpful haiku poem generating assistant.\n    ONLY generate a haiku for users with an `account_type` of \"member\".\n    If the user has an `account_type` of \"trial\", say you can't do it,\n    but you would love to help them if they upgrade and become a member.",
    processed_content: nil,
    index: nil,
    status: :complete,
    role: :system,
    name: nil,
    tool_calls: [],
    tool_results: nil
  },
  %LangChain.Message{
    content: "The current user is requesting a Haiku poem about their favorite animal.",
    processed_content: nil,
    index: nil,
    status: :complete,
    role: :user,
    name: nil,
    tool_calls: [],
    tool_results: nil
  }
]
```

## Defining our AI Model

For this example, we're talking to OpenAI's ChatGPT service. Let's setup that model. At this point, we can also specify which version of ChatGPT we want to talk with.

For the kind of work we're asking it to do, GPT-4o is the latest model that has great support for tools.

```elixir
alias LangChain.ChatModels.ChatOpenAI

chat_model = ChatOpenAI.new!(%{model: "gpt-4o", temperature: 1, stream: false})
```

<!-- livebook:{"output":true} -->

```
%LangChain.ChatModels.ChatOpenAI{
  endpoint: "https://api.openai.com/v1/chat/completions",
  model: "gpt-4o",
  api_key: nil,
  temperature: 1.0,
  frequency_penalty: 0.0,
  reasoning_mode: false,
  reasoning_effort: "medium",
  receive_timeout: 60000,
  seed: nil,
  n: 1,
  json_response: false,
  json_schema: nil,
  stream: false,
  max_tokens: nil,
  stream_options: nil,
  tool_choice: nil,
  callbacks: [],
  user: nil
}
```

## Defining our Application's User Context

Here we'll define some special context that we want passed through to our `LangChain.Function` when it is executed.

In a real application, this might be session based user or account information. It's whatever is relevant to our application that changes how a function should operate or the data it should access.

```elixir
context = %{user_id: 2}
```

<!-- livebook:{"output":true} -->

```
%{user_id: 2}
```

After trying this with `user_id: 2`, a member who should have a Haiku generated for them, change it to `user_id: 1` to see it be politely denied.

## Making the API Call

We're ready to make the API call!

Notice the `custom_context: context` setting that is passed in when creating the `LLMChain`. That information is the application-specific context we want to be passed to our `Function` when executed.

Also, note the `verbose: true` setting. That causes a number of `IO.inspect` calls to be printed showing what's happening internally.

Additionally, the `stream: false` option says we want the result only when it's complete. This example isn't setup for receiving a streaming response. We're keeping it simple!

```elixir
alias LangChain.Chains.LLMChain

{:ok, updated_chain} =
  %{llm: chat_model, custom_context: context, verbose: true}
  |> LLMChain.new!()
  |> LLMChain.add_messages(messages)
  |> LLMChain.add_tools([function])
  # keep running the LLM chain against the LLM if needed to evaluate
  # function calls and provide a response.
  |> LLMChain.run(mode: :while_needs_response)

response = updated_chain.last_message
IO.write(response.content)
response.content
```

<!-- livebook:{"output":true} -->

```
LLM: %LangChain.ChatModels.ChatOpenAI{
  endpoint: "https://api.openai.com/v1/chat/completions",
  model: "gpt-4o",
  api_key: nil,
  temperature: 1.0,
  frequency_penalty: 0.0,
  reasoning_mode: false,
  reasoning_effort: "medium",
  receive_timeout: 60000,
  seed: nil,
  n: 1,
  json_response: false,
  json_schema: nil,
  stream: false,
  max_tokens: nil,
  stream_options: nil,
  tool_choice: nil,
  callbacks: [],
  user: nil
}
MESSAGES: [
  %LangChain.Message{
    content: "You are a helpful haiku poem generating assistant.\n    ONLY generate a haiku for users with an `account_type` of \"member\".\n    If the user has an `account_type` of \"trial\", say you can't do it,\n    but you would love to help them if they upgrade and become a member.",
    processed_content: nil,
    index: nil,
    status: :complete,
    role: :system,
    name: nil,
    tool_calls: [],
    tool_results: nil
  },
  %LangChain.Message{
    content: "The current user is requesting a Haiku poem about their favorite animal.",
    processed_content: nil,
    index: nil,
    status: :complete,
    role: :user,
    name: nil,
    tool_calls: [],
    tool_results: nil
  }
]
TOOLS: [
  %LangChain.Function{
    name: "get_user_info",
    description: "Return JSON object of the current users's relevant information.",
    display_text: nil,
    strict: false,
    function: #Function<41.18682967/2 in :erl_eval.expr/6>,
    async: true,
    parameters_schema: nil,
    parameters: []
  }
]
SINGLE MESSAGE RESPONSE: %LangChain.Message{
  content: nil,
  processed_content: nil,
  index: 0,
  status: :complete,
  role: :assistant,
  name: nil,
  tool_calls: [
    %LangChain.Message.ToolCall{
      status: :complete,
      type: :function,
      call_id: "call_2puDjavxfbFQ7uDvrE9vafnZ",
      name: "get_user_info",
      arguments: %{},
      index: nil
    }
  ],
  tool_results: nil
}
MESSAGE PROCESSED: %LangChain.Message{
  content: nil,
  processed_content: nil,
  index: 0,
  status: :complete,
  role: :assistant,
  name: nil,
  tool_calls: [
    %LangChain.Message.ToolCall{
      status: :complete,
      type: :function,
      call_id: "call_2puDjavxfbFQ7uDvrE9vafnZ",
      name: "get_user_info",
      arguments: %{},
      index: nil
    }
  ],
  tool_results: nil
}
EXECUTING FUNCTION: "get_user_info"

09:10:30.474 [debug] Executing function "get_user_info"
FUNCTION RESULT: "{\"name\":\"Joan Jett\",\"user_id\":2,\"account_type\":\"member\",\"favorite_animal\":\"Aardvark\"}"
TOOL RESULTS: %LangChain.Message{
  content: nil,
  processed_content: nil,
  index: nil,
  status: :complete,
  role: :tool,
  name: nil,
  tool_calls: [],
  tool_results: [
    %LangChain.Message.ToolResult{
      type: :function,
      tool_call_id: "call_2puDjavxfbFQ7uDvrE9vafnZ",
      name: "get_user_info",
      content: "{\"name\":\"Joan Jett\",\"user_id\":2,\"account_type\":\"member\",\"favorite_animal\":\"Aardvark\"}",
      processed_content: nil,
      display_text: nil,
      is_error: false
    }
  ]
}
SINGLE MESSAGE RESPONSE: %LangChain.Message{
  content: "Long snout seeking ants,  \nNight wanderer of moonlight,  \nAardvark's gentle grace.",
  processed_content: nil,
  index: 0,
  status: :complete,
  role: :assistant,
  name: nil,
  tool_calls: [],
  tool_results: nil
}
MESSAGE PROCESSED: %LangChain.Message{
  content: "Long snout seeking ants,  \nNight wanderer of moonlight,  \nAardvark's gentle grace.",
  processed_content: nil,
  index: 0,
  status: :complete,
  role: :assistant,
  name: nil,
  tool_calls: [],
  tool_results: nil
}
Long snout seeking ants,  
Night wanderer of moonlight,  
Aardvark's gentle grace.
```

<!-- livebook:{"output":true} -->

```
"Long snout seeking ants,  \nNight wanderer of moonlight,  \nAardvark's gentle grace."
```

**TIP:** Try changing the `context` to `user_id: 1` now and see what happens when a different user context is provided.

## Discussion

After a successful call, we can see in the verbose logs that:

* the LLM requested to execute the tool which is our function
* LLMChain executed the Elixir function attached to the `Function` struct
* the response of our Elixir function passed through the anonymous function on `Function` and was re-submitted back to the LLM
* the LLM reacted to the result of our function call

This means it worked! We successfully let an LLM directly interact with our Elixir application!

With this, we could expose functions that allow the LLM to request additional information specific to the current user, or we could even define functions that allow the LLM to **change** things in the user's account on their behalf!

The rest is up to us.

<!-- livebook:{"offset":13867,"stamp":{"token":"XCP.UFYBKiTKTivrWqsgBsbdfATOyTtvmvReV-H7dpctAL0z8OepBUJOuQTNOu2-7E33qPbB-Jeyp-I32rgPfwdwE98Ed2JQRodZ3PL1Pn_lWFzuVrqSKbEGO8yYAUxoqCC8kpGjDeoKqIardsK0OtpE","version":2}} -->
