Skip to content

Instantly share code, notes, and snippets.

@nileshtrivedi
Last active June 16, 2025 03:21
Show Gist options
  • Save nileshtrivedi/9ed85017bbba3fd86c86ef70973fac68 to your computer and use it in GitHub Desktop.
Save nileshtrivedi/9ed85017bbba3fd86c86ef70973fac68 to your computer and use it in GitHub Desktop.
WIP: AI Chat using Tamnoon
# Run this with: OPENAI_API_KEY= elixir bot.exs
Mix.install([
{:tamnoon, "== 1.0.0-a.5"},
{:langchain, "== 0.4.0-rc.0"}
])
defmodule Bot.Components.Root do
@behaviour Tamnoon.Component
@impl true
def heex() do
~S"""
<!DOCTYPE html>
<html lang="en" data-theme="forest">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Bot</title>
<script src="ws_connect.js"></script>
<link href="https://cdn.jsdelivr.net/npm/daisyui@5" rel="stylesheet" type="text/css" />
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
<link href="https://cdn.jsdelivr.net/npm/daisyui@5/themes.css" rel="stylesheet" type="text/css" />
<script src="https://cdn.jsdelivr.net/npm/[email protected]/index.js"></script>
</head>
<body>
<div class="flex w-full h-screen font-sans">
<div class="flex-grow grid place-items-center bg-base-100 p-8">
<div class="hero">
<div class="hero-content text-center">
<div class="max-w-md">
<h1 class="text-5xl font-bold mb-4">@num</h1>
<button class="btn btn-primary px-8 mr-4" onclick=@pub-clients-nudge-num value="up">+</button>
<button class="btn btn-primary px-8" onclick=@pub-clients-nudge-num value="down">-</button>
</div>
</div>
</div>
</div>
<div class="divider divider-horizontal m-0"></div>
<div class="w-full max-w-md flex-shrink-0 bg-base-200 flex flex-col">
<div class="p-4 bg-base-300 border-b border-base-100">
<h2 class="text-xl font-bold text-base-content">Chat with AI</h2>
</div>
<div class="flex-grow p-4 overflow-y-auto space-y-4">@raw-all_msgs_html</div>
<div class="p-4 bg-base-300 border-t border-base-100">
<div class="flex items-center gap-2" hidden=@chatdisabled>
<textarea class="textarea textarea-bordered w-full flex-grow resize-none" oninput=@update-current_user_msg value=@current_user_msg placeholder="Ask the AI to nudge the counter..."></textarea>
<button class="btn btn-primary" onclick=@pub-clients-chat>Send</button>
</div>
</div>
</div>
</div>
</body>
</html>
"""
end
end
defmodule Bot.Components.MessageBox do
@behaviour Tamnoon.Component
@impl true
def heex do
~s"""
<div class="chat <%= if @role == "assistant", do: "chat-end", else: "chat-start" %>">
<div class="chat-image avatar">
<div class="w-10 rounded-full"><img alt="AI Avatar" src="https://placehold.co/192x192/a991f7/ffffff?text=<%= @role |> String.first |> String.upcase %>" /></div>
</div>
<div class="chat-bubble <%= if @role == "assistant", do: "chat-bubble-secondary", else: "chat-bubble-primary" %>"><%= h.(@content) %></div>
</div>
"""
end
end
defmodule Bot.Router do
use Plug.Router
plug(:match)
plug(:dispatch)
get "/" do
homepage_html =
Bot.Components.Root
|> Tamnoon.Compiler.render_component(%{}, true)
send_resp(conn, 200, homepage_html)
end
get "/ws_connect.js" do
send_file(conn, 200, Application.app_dir(:tamnoon, "priv/static/ws_connect.js"))
end
match _ do
send_resp(conn, 404, "404")
end
end
defmodule Bot.Methods do
import Tamnoon.MethodManager
defmethod :nudge do
key = Tamnoon.Methods.get_key(req, state)
num = Map.get(state, key)
new_num = if req["val"] == "up", do: num + 1, else: num - 1
diff(%{num: new_num}, state)
end
# The chat method that interacts with the LLM.
defmethod :chat do
user_message = %{role: "user", content: Map.get(state, :current_user_msg)}
custom_context = %{
"up" => 1,
"down" => -1,
}
# Define the 'nudge' function that the LLM can call.
nudge_tool = LangChain.Function.new!(%{
name: "nudge",
description: "Nudges the shared counter up or down.",
parameters_schema: %{
type: :object,
properties: %{
direction: %{
type: :string,
description: "The direction to nudge the counter",
enum: ["up", "down"]
}
},
required: ["direction"]
},
function: fn %{"direction" => direction} = _arguments, context ->
IO.inspect(context, label: "Context in nudge function")
trigger_method(:nudge, %{"val" => direction}, 0)
{:ok, "done"}
end
})
{:ok, updated_chain} =
LangChain.Chains.LLMChain.new!(%{
llm: LangChain.ChatModels.ChatOpenAI.new!(%{
model: "gpt-4o-mini-2024-07-18",
api_key: System.fetch_env!("OPENAI_API_KEY"),
}),
custom_context: custom_context,
verbose: true
})
|> LangChain.Chains.LLMChain.add_tools(nudge_tool)
|> LangChain.Chains.LLMChain.add_message(LangChain.Message.new_user!(user_message.content))
|> LangChain.Chains.LLMChain.run(mode: :while_needs_response)
# Get the final text response from the AI
response_text = LangChain.Utils.ChainResult.to_string!(updated_chain)
ai_response = %{role: "assistant", content: response_text}
diff(%{
current_user_msg: "",
all_msgs: state[:all_msgs] ++ [user_message, ai_response],
all_msgs_html: Enum.map(state[:all_msgs] ++ [user_message, ai_response], fn x ->
Tamnoon.Compiler.render_component(Bot.Components.MessageBox, x, true)
end) |> Enum.join("\n")
}, state)
end
end
defmodule Bot.Application do
@moduledoc false
use Application
@impl true
def start(_type, _args) do
children = [
{Tamnoon,
[
[
initial_state: %{
num: 0,
current_user_msg: "",
all_msgs: [],
all_msgs_html: "",
chatdisabled: false
},
router: Bot.Router,
methods_modules: [Bot.Methods]
]
]}
]
opts = [strategy: :one_for_one, name: Bot.Supervisor]
Supervisor.start_link(children, opts)
end
end
Application.ensure_all_started(:logger)
{:ok, _pid} = Bot.Application.start(:normal, [])
Process.sleep(:infinity)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment