Skip to content

Commit

Permalink
feature: use llm to generate ideas #328
Browse files Browse the repository at this point in the history
  • Loading branch information
nwittstruck committed Aug 7, 2024
1 parent 8e5d252 commit 8043e15
Show file tree
Hide file tree
Showing 28 changed files with 373 additions and 141 deletions.
6 changes: 6 additions & 0 deletions .env.default
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ DOCKER_COMPOSE_APP_DATABASE_USER=mindwendel-user
DOCKER_COMPOSE_APP_MW_DEFAULT_LOCALE=en
DOCKER_COMPOSE_APP_MW_FEATURE_BRAINSTORMING_REMOVAL_AFTER_DAYS=30
DOCKER_COMPOSE_APP_MW_FEATURE_BRAINSTORMING_TEASER=true

# AI Integration is disabled by default:
DOCKER_COMPOSE_APP_MW_AI_ENABLED=false
DOCKER_COMPOSE_APP_MW_AI_API_KEY=
DOCKER_COMPOSE_APP_MW_AI_API_BASE_URL=

# This is an example secret key base that can be use in development
# NOTE: There are multiple commands you can use to generate a secret key base. Pick one command you like, e.g. `date +%s | sha256sum | base64 | head -c 64 ; echo`
# !!ATTENTION: DO NOT USE THIS FOR PRODUCTION!!
Expand Down
30 changes: 0 additions & 30 deletions .env.prod.default

This file was deleted.

2 changes: 1 addition & 1 deletion .github/workflows/on_push_main_publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ name: Create and publish a Docker image
# Configures this workflow to run every time a change is pushed to the branch called `master`.
on:
push:
branches: ["master"]
branches: ["master", "328-feature-generate-ideas-with-an-llm"]
release:
types: [published]

Expand Down
12 changes: 12 additions & 0 deletions config/runtime.exs
Original file line number Diff line number Diff line change
Expand Up @@ -179,3 +179,15 @@ if config_env() == :prod || config_env() == :dev do
],
queues: [default: 1]
end

# configure ai only in prod and dev, not test. default is disabled:
if config_env() == :prod || config_env() == :dev do
if System.get_env("MW_AI_ENABLED") == "true" do
config :mindwendel, :ai,
enabled: true,
api_key: System.get_env("MW_AI_API_KEY"),
api_base_url: System.get_env("MW_AI_API_BASE_URL")
end
else
config :mindwendel, :ai, enabled: false
end
68 changes: 0 additions & 68 deletions docker-compose-prod.yml

This file was deleted.

3 changes: 3 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ services:
MW_DEFAULT_LOCALE: ${DOCKER_COMPOSE_APP_MW_DEFAULT_LOCALE:-en}
MW_FEATURE_BRAINSTORMING_REMOVAL_AFTER_DAYS: ${DOCKER_COMPOSE_APP_MW_FEATURE_BRAINSTORMING_REMOVAL_AFTER_DAYS:-30}
MW_FEATURE_BRAINSTORMING_TEASER: ${DOCKER_COMPOSE_APP_MW_FEATURE_BRAINSTORMING_TEASER:-true}
MW_AI_ENABLED: ${DOCKER_COMPOSE_APP_MW_AI_ENABLED:-false}
MW_AI_API_KEY: ${DOCKER_COMPOSE_APP_MW_AI_API_KEY}
MW_AI_API_BASE_URL: ${DOCKER_COMPOSE_APP_MW_AI_API_BASE_URL}
# This is an example secret key base that can be use in development
# NOTE: There are multiple commands you can use to generate a secret key base. Pick one command you like, e.g.:
# - `date +%s | sha256sum | base64 | head -c 64 ; echo`
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
defmodule Mindwendel.Services.ChatCompletions.ChatCompletionsService do
@callback generate_ideas(String.t()) :: {:error, any()} | {:ok, any()}
@callback enabled?() :: boolean()

# See https://hexdocs.pm/elixir_mock/getting_started.html for why we are doing it this way:
def generate_ideas(title), do: impl().generate_ideas(title)
def enabled?(), do: impl().enabled?

defp impl,
do:
Application.get_env(
:mindwendel,
:chat_completions_service,
Mindwendel.Services.ChatCompletions.ChatCompletionsServiceImpl
)
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
defmodule Mindwendel.Services.ChatCompletions.ChatCompletionsServiceImpl do
require Logger

alias Mindwendel.Services.ChatCompletions.ChatCompletionsService
alias OpenaiEx.Chat
alias OpenaiEx.ChatMessage

@behaviour ChatCompletionsService

@impl ChatCompletionsService
def generate_ideas(title) do
case setup_chat_completions_service() do
{:ok, chat_completions_service} ->
chat_req =
Chat.Completions.new(
model: "meta-llama-3-8b-instruct",
messages: [
ChatMessage.user(
"Generate ONLY json for the following request, no other content. The format should be [{idea: generated_content}]. Replace generated_content."
),
ChatMessage.user("In a brainstorming, generate 5 ideas for the following title:"),
ChatMessage.user(title)
]
)

ideas =
case chat_completions_service |> Chat.Completions.create(chat_req) do
{:ok, chat_response} ->
choices = List.first(chat_response["choices"])
Jason.decode!(choices["message"]["content"])

{:error, error} ->
Logger.error("Error while fetching ideas from LLM:")
Logger.error(error)
[]
end

ideas

_ ->
{:error, "Error creating chat completion service"}
end
end

@impl ChatCompletionsService
@spec enabled?() :: boolean()
def enabled? do
ai_config = fetch_ai_config!()
enabled?(ai_config)
end

def enabled?(ai_config) do
ai_config[:enabled]
end

defp setup_chat_completions_service do
ai_config = fetch_ai_config!()

if enabled?(ai_config) do
{:ok, OpenaiEx.new(api_key(ai_config)) |> OpenaiEx.with_base_url(api_base_url(ai_config))}
else
{:error, :ai_not_enabled}
end
end

defp api_key(ai_config) do
ai_config[:api_key]
end

defp api_base_url(ai_config) do
ai_config[:api_base_url]
end

defp fetch_ai_config! do
Application.fetch_env!(:mindwendel, :ai)
end
end
26 changes: 26 additions & 0 deletions lib/mindwendel/services/idea_service.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
defmodule Mindwendel.Services.IdeaService do
alias Mindwendel.Services.ChatCompletions.ChatCompletionsService
alias Mindwendel.Ideas

require Logger

def idea_generation_enabled? do
ChatCompletionsService.enabled?()
end

def add_ideas_to_brainstorming(brainstorming) do
if !idea_generation_enabled?() do
[]
else
generated_ideas = ChatCompletionsService.generate_ideas(brainstorming.name)

Enum.map(generated_ideas, fn generated_idea ->
Ideas.create_idea(%{
username: "AI",
body: generated_idea["idea"],
brainstorming_id: brainstorming.id
})
end)
end
end
end
2 changes: 1 addition & 1 deletion lib/mindwendel_web/controllers/static_page_controller.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ defmodule MindwendelWeb.StaticPageController do
alias Mindwendel.Brainstormings
alias Mindwendel.Brainstormings.Brainstorming

plug :put_root_layout, {MindwendelWeb.LayoutView, :static_page}
plug :put_root_layout, html: {MindwendelWeb.LayoutView, :static_page}

def home(conn, _params) do
current_user =
Expand Down
19 changes: 19 additions & 0 deletions lib/mindwendel_web/live/brainstorming_live/show.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ defmodule MindwendelWeb.BrainstormingLive.Show do
alias Mindwendel.Brainstormings
alias Mindwendel.Ideas
alias Mindwendel.Brainstormings.Idea
alias Mindwendel.Services.IdeaService

@impl true
def mount(%{"id" => id}, session, socket) do
Expand Down Expand Up @@ -122,6 +123,24 @@ defmodule MindwendelWeb.BrainstormingLive.Show do
{:noreply, assign(socket, :ideas, Ideas.list_ideas_for_brainstorming(id))}
end

def handle_event("generate_ai_ideas", %{"id" => id}, socket) do
brainstorming =
Brainstormings.get_brainstorming!(id)

ideas = IdeaService.add_ideas_to_brainstorming(brainstorming)

case length(ideas) do
0 ->
{:noreply, put_flash(socket, :error, gettext("No ideas generated"))}

length ->
socket = assign(socket, :ideas, Ideas.list_ideas_for_brainstorming(id))

{:noreply,
put_flash(socket, :info, gettext("%{length} idea(s) generated", %{length: length}))}
end
end

def handle_event("sort_by_label", %{"id" => id}, socket) do
{:noreply, assign(socket, :ideas, Ideas.sort_ideas_by_labels(id))}
end
Expand Down
5 changes: 5 additions & 0 deletions lib/mindwendel_web/live/brainstorming_live/show.html.heex
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@
class: "btn btn-primary m-1 d-none d-md-block",
title: gettext("New idea page (Hotkey: i)")
) %>
<%= if idea_generation_enabled?() do %>
<%= link to: "#", class: "btn btn-primary m-1", phx_click: "generate_ai_ideas", phx_value_id: @brainstorming.id, title: gettext("AI") do %>
<i class="bi-magic"></i> <%= gettext("AI") %>
<% end %>
<% end %>
<%= link to: "#", class: "btn btn-primary m-1", phx_click: "sort_by_likes", phx_value_id: @brainstorming.id, title: gettext("Sort by likes") do %>
<i class="bi-sort-numeric-up-alt"></i> <%= gettext("Likes") %>
<% end %>
Expand Down
5 changes: 5 additions & 0 deletions lib/mindwendel_web/live/live_helpers.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ defmodule MindwendelWeb.LiveHelpers do
import MindwendelWeb.Gettext

alias Mindwendel.Brainstormings.Brainstorming
alias Mindwendel.Services.IdeaService

@doc """
Renders a component inside the `MindwendelWeb.ModalComponent` component.
Expand Down Expand Up @@ -37,4 +38,8 @@ defmodule MindwendelWeb.LiveHelpers do
def brainstorming_available_until(brainstorming) do
Brainstorming.brainstorming_available_until(brainstorming)
end

def idea_generation_enabled? do
IdeaService.idea_generation_enabled?()
end
end
2 changes: 1 addition & 1 deletion lib/mindwendel_web/router.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ defmodule MindwendelWeb.Router do
plug(:accepts, ["html", "csv"])
plug(:fetch_session)
plug(:fetch_live_flash)
plug(:put_root_layout, {MindwendelWeb.LayoutView, :root})
plug(:put_root_layout, html: {MindwendelWeb.LayoutView, :root})
plug(:protect_from_forgery)

# Ususally, you can directly include the csp header in this borwser pipeline like this
Expand Down
4 changes: 3 additions & 1 deletion mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,9 @@ defmodule Mindwendel.MixProject do
{:telemetry_poller, "1.1.0"},
{:timex, "3.7.11"},
{:logger_json, "6.0.3"},
{:libcluster, "3.3.3"}
{:libcluster, "3.3.3"},
{:openai_ex, "0.8.0"},
{:mox, "1.1.0", only: :test}
]
end

Expand Down
Loading

0 comments on commit 8043e15

Please sign in to comment.