Last active
June 16, 2025 03:21
-
-
Save nileshtrivedi/9ed85017bbba3fd86c86ef70973fac68 to your computer and use it in GitHub Desktop.
WIP: AI Chat using Tamnoon
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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