Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,63 @@ Now you can visit [`localhost:4000`](http://localhost:4000) from your browser.

Ready to run in production? Please [check our deployment guides](https://hexdocs.pm/phoenix/deployment.html).

## AI Provider Configuration

Bodhi supports multiple AI providers that can be switched via configuration.

### Available Providers

#### OpenRouter (Default)
- **Module:** `Bodhi.OpenRouter`
- **Default Model:** `deepseek/deepseek-r1-0528:free`
- **Environment Variable:** `OPENROUTER_API_KEY`
- **Website:** https://openrouter.ai/

#### Google Gemini
- **Module:** `Bodhi.Gemini`
- **Model:** `gemini-2.0-flash`
- **Environment Variable:** `GEMINI_API_KEY`

### Switching Providers

To switch AI providers, update `config/config.exs`:

```elixir
# Use OpenRouter (default)
config :bodhi, :ai_client, Bodhi.OpenRouter

# Use Google Gemini
config :bodhi, :ai_client, Bodhi.Gemini
```

### Setting Up API Keys

1. **OpenRouter:**
- Get API key from: https://openrouter.ai/keys
- Set in `.envrc`: `export OPENROUTER_API_KEY=sk-or-v1-your_api_key_here`

2. **Google Gemini:**
- Get API key from: https://aistudio.google.com/app/apikey
- Set in `.envrc`: `export GEMINI_API_KEY=your_api_key_here`

3. Reload environment: `direnv allow` (if using direnv)

### Changing OpenRouter Model

Edit `lib/bodhi/open_router.ex` and modify the `@default_model` attribute:

```elixir
@default_model "deepseek/deepseek-r1-0528:free" # Current default

# Other popular models:
# @default_model "anthropic/claude-3.5-sonnet"
# @default_model "openai/gpt-4-turbo"
# @default_model "meta-llama/llama-3.1-70b-instruct"
# @default_model "google/gemini-pro-1.5"
```

See all available models at: https://openrouter.ai/models

## Learn more

* Official website: https://www.phoenixframework.org/
Expand Down
2 changes: 1 addition & 1 deletion config/config.exs
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,6 @@ config :posthog,

# Configure behaviour implementations (can be overridden in env-specific configs)
config :bodhi, :telegram_client, Bodhi.Telegram.TelegexAdapter
config :bodhi, :ai_client, Bodhi.Gemini
config :bodhi, :ai_client, Bodhi.OpenRouter

import_config "#{config_env()}.exs"
3 changes: 2 additions & 1 deletion config/runtime.exs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ if System.get_env("PHX_SERVER") && System.get_env("RELEASE_NAME") do
end

config :bodhi,
gemini_token: "#{System.get_env("GEMINI_API_KEY")}"
gemini_token: "#{System.get_env("GEMINI_API_KEY")}",
openrouter_token: "#{System.get_env("OPENROUTER_API_KEY")}"

config :telegex,
token: "#{System.get_env("TG_TOKEN")}"
Expand Down
4 changes: 2 additions & 2 deletions lib/bodhi/ai.ex
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ defmodule Bodhi.AI do
Asks the AI to generate a response based on message history.
"""
@impl true
def ask_gemini(messages) do
impl().ask_gemini(messages)
def ask_llm(messages) do
impl().ask_llm(messages)
end

defp impl do
Expand Down
2 changes: 1 addition & 1 deletion lib/bodhi/application.ex
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ defmodule Bodhi.Application do
BodhiWeb.Endpoint,
Bodhi.TgWebhookHandler,
{Finch,
name: Gemini,
name: LLM,
pools: %{
:default => [size: 10]
}},
Expand Down
2 changes: 1 addition & 1 deletion lib/bodhi/behaviours/ai_client.ex
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,5 @@ defmodule Bodhi.Behaviours.AIClient do
@doc """
Asks the AI to generate a response based on message history.
"""
@callback ask_gemini([Message.t()]) :: {:ok, String.t()} | {:error, String.t()}
@callback ask_llm([Message.t()]) :: {:ok, String.t()} | {:error, String.t()}
end
6 changes: 3 additions & 3 deletions lib/bodhi/gemini.ex
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ defmodule Bodhi.Gemini do
Request Gemini for bot's response in dialogue.
"""
@impl true
@spec ask_gemini([Message.t()]) :: {:ok, String.t()} | {:error, String.t()}
def ask_gemini(messages) do
@spec ask_llm([Message.t()]) :: {:ok, String.t()} | {:error, String.t()}
def ask_llm(messages) do
%Prompt{text: prompt} = Prompts.get_latest_prompt!()

messages
Expand All @@ -43,7 +43,7 @@ defmodule Bodhi.Gemini do
[{"x-goog-api-key", Application.get_env(:bodhi, :gemini_token)}],
body
)
|> Finch.request!(Gemini)
|> Finch.request!(LLM)
|> handle_finch_response()
|> Jason.decode!()
end
Expand Down
88 changes: 88 additions & 0 deletions lib/bodhi/open_router.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
defmodule Bodhi.OpenRouter do
@moduledoc """
OpenRouter API wrapper
OpenRouter provides unified access to multiple AI models through OpenAI-compatible API.
"""
@behaviour Bodhi.Behaviours.AIClient

@openrouter_url "https://openrouter.ai/api/v1/chat/completions"
@default_model "deepseek/deepseek-r1-0528:free"

alias Bodhi.Chats.Message
alias Bodhi.Prompts
alias Bodhi.Prompts.Prompt

require Logger

@doc """
Request OpenRouter for bot's response in dialogue.
"""
@impl true
@spec ask_llm([Message.t()]) :: {:ok, String.t()} | {:error, String.t()}
def ask_llm(messages) do
%Prompt{text: prompt} = Prompts.get_latest_prompt!()

messages
|> prepare_messages()
|> request_openrouter(prompt)
|> parse_response()
end

defp prepare_messages(messages), do: Enum.map(messages, &build_message/1)

defp build_message(%Message{text: text, chat_id: user_id, user_id: user_id}),
do: %{role: "user", content: text}

defp build_message(%Message{text: text}), do: %{role: "assistant", content: text}

defp request_openrouter(messages, prompt) do
body = build_body(messages, prompt)

:post
|> Finch.build(
@openrouter_url,
[
{"Authorization", "Bearer #{Application.get_env(:bodhi, :openrouter_token)}"},
{"Content-Type", "application/json"},
{"HTTP-Referer", "https://lamabot.io"},
{"X-Title", "Lama Bot"}
],
body
)
|> Finch.request!(LLM)
|> handle_finch_response()
|> Jason.decode!()
end

defp build_body(messages, prompt) do
%{
model: @default_model,
messages: [
%{role: "system", content: prompt}
| messages
]
}
|> Jason.encode!()
end

defp handle_finch_response(%Finch.Response{status: 200, body: body}), do: body

defp handle_finch_response(%Finch.Response{status: code, body: body}) do
Logger.warning("OpenRouter request error code: #{code}, body: '#{body}'")
body
end

defp parse_response(%{"choices" => [%{"message" => %{"content" => content}} | _]}) do
{:ok, content}
end

defp parse_response(%{"error" => error}) do
Logger.error("OpenRouter API error: #{inspect(error)}")
{:error, "OpenRouter API error: #{inspect(error)}"}
end

defp parse_response(response) do
Logger.error("Unexpected OpenRouter response format: #{inspect(response)}")
{:error, "Unexpected response format"}
end
end
2 changes: 1 addition & 1 deletion lib/bodhi/tg_webhook_handler.ex
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ defmodule Bodhi.TgWebhookHandler do

defp get_answer(%_{chat_id: chat_id}, _) do
messages = Bodhi.Chats.get_chat_messages(chat_id)
{:ok, _answer} = Bodhi.AI.ask_gemini(messages)
{:ok, _answer} = Bodhi.AI.ask_llm(messages)
end

defp get_start_message(lang) do
Expand Down
2 changes: 1 addition & 1 deletion test/bodhi/tg_webhook_handler_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ defmodule Bodhi.TgWebhookHandlerTest do

# Set up expectations based on test parameters
if gemini? do
expect(Bodhi.GeminiMock, :ask_gemini, fn _messages ->
expect(Bodhi.GeminiMock, :ask_llm, fn _messages ->
{:ok, Faker.Lorem.paragraph()}
end)
end
Expand Down
2 changes: 1 addition & 1 deletion test/support/conn_case.ex
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ defmodule BodhiWeb.ConnCase do

# Set up default stub for Gemini mock
Bodhi.GeminiMock
|> stub(:ask_gemini, fn _ ->
|> stub(:ask_llm, fn _ ->
{:ok, Faker.Lorem.paragraph()}
end)

Expand Down
2 changes: 1 addition & 1 deletion test/support/oban_case.ex
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ defmodule Bodhi.ObanCase do

# Set up default stub for Gemini mock
Bodhi.GeminiMock
|> stub(:ask_gemini, fn _ ->
|> stub(:ask_llm, fn _ ->
{:ok, Faker.Lorem.paragraph()}
end)

Expand Down
1 change: 1 addition & 0 deletions test/test_helper.exs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
# Define Mox mocks
Mox.defmock(Bodhi.TelegramMock, for: Bodhi.Behaviours.TelegramClient)
Mox.defmock(Bodhi.GeminiMock, for: Bodhi.Behaviours.AIClient)
Mox.defmock(Bodhi.OpenRouterMock, for: Bodhi.Behaviours.AIClient)

Ecto.Adapters.SQL.Sandbox.mode(Bodhi.Repo, :manual)
ExUnit.start()
Loading