Skip to content

Commit

Permalink
FEATURE: Configurable LLMs. (#606)
Browse files Browse the repository at this point in the history
This PR introduces the concept of "LlmModel" as a new way to quickly add new LLM models without making any code changes. We are releasing this first version and will add incremental improvements, so expect changes.

The AI Bot can't fully take advantage of this feature as users are hard-coded. We'll fix this in a separate PR.s
  • Loading branch information
romanrizzi committed May 13, 2024
1 parent 5c02b88 commit 62fc7d6
Show file tree
Hide file tree
Showing 34 changed files with 656 additions and 70 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import DiscourseRoute from "discourse/routes/discourse";

export default DiscourseRoute.extend({
async model() {
const record = this.store.createRecord("ai-llm");
return record;
},

setupController(controller, model) {
this._super(controller, model);
controller.set(
"allLlms",
this.modelFor("adminPlugins.show.discourse-ai-llms")
);
},
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import DiscourseRoute from "discourse/routes/discourse";

export default DiscourseRoute.extend({
async model(params) {
const allLlms = this.modelFor("adminPlugins.show.discourse-ai-llms");
const id = parseInt(params.id, 10);
return allLlms.findBy("id", id);
},

setupController(controller, model) {
this._super(controller, model);
controller.set(
"allLlms",
this.modelFor("adminPlugins.show.discourse-ai-llms")
);
},
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import DiscourseRoute from "discourse/routes/discourse";

export default class DiscourseAiAiLlmsRoute extends DiscourseRoute {
model() {
return this.store.findAll("ai-llm");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<AiLlmsListEditor @llms={{this.model}} />
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<AiLlmsListEditor @llms={{this.allLlms}} @currentLlm={{this.model}} />
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<AiLlmsListEditor @llms={{this.allLlms}} @currentLlm={{this.model}} />
64 changes: 64 additions & 0 deletions app/controllers/discourse_ai/admin/ai_llms_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# frozen_string_literal: true

module DiscourseAi
module Admin
class AiLlmsController < ::Admin::AdminController
requires_plugin ::DiscourseAi::PLUGIN_NAME

def index
llms = LlmModel.all

render json: {
ai_llms:
ActiveModel::ArraySerializer.new(
llms,
each_serializer: LlmModelSerializer,
root: false,
).as_json,
meta: {
providers: DiscourseAi::Completions::Llm.provider_names,
tokenizers:
DiscourseAi::Completions::Llm.tokenizer_names.map { |tn|
{ id: tn, name: tn.split("::").last }
},
},
}
end

def show
llm_model = LlmModel.find(params[:id])
render json: LlmModelSerializer.new(llm_model)
end

def create
if llm_model = LlmModel.new(ai_llm_params).save
render json: { ai_persona: llm_model }, status: :created
else
render_json_error llm_model
end
end

def update
llm_model = LlmModel.find(params[:id])

if llm_model.update(ai_llm_params)
render json: llm_model
else
render_json_error llm_model
end
end

private

def ai_llm_params
params.require(:ai_llm).permit(
:display_name,
:name,
:provider,
:tokenizer,
:max_prompt_tokens,
)
end
end
end
end
7 changes: 7 additions & 0 deletions app/models/llm_model.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# frozen_string_literal: true

class LlmModel < ActiveRecord::Base
def tokenizer_class
tokenizer.constantize
end
end
7 changes: 7 additions & 0 deletions app/serializers/llm_model_serializer.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# frozen_string_literal: true

class LlmModelSerializer < ApplicationSerializer
root "llm"

attributes :id, :display_name, :name, :provider, :max_prompt_tokens, :tokenizer
end
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,10 @@ export default {
this.route("new");
this.route("show", { path: "/:id" });
});

this.route("discourse-ai-llms", { path: "ai-llms" }, function () {
this.route("new");
this.route("show", { path: "/:id" });
});
},
};
21 changes: 21 additions & 0 deletions assets/javascripts/discourse/admin/adapters/ai-llm.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import RestAdapter from "discourse/adapters/rest";

export default class Adapter extends RestAdapter {
jsonMode = true;

basePath() {
return "/admin/plugins/discourse-ai/";
}

pathFor(store, type, findArgs) {
// removes underscores which are implemented in base
let path =
this.basePath(store, type, findArgs) +
store.pluralize(this.apiNameFor(type));
return this.appendQueryParams(path, findArgs);
}

apiNameFor() {
return "ai-llm";
}
}
20 changes: 20 additions & 0 deletions assets/javascripts/discourse/admin/models/ai-llm.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import RestModel from "discourse/models/rest";

export default class AiLlm extends RestModel {
createProperties() {
return this.getProperties(
"display_name",
"name",
"provider",
"tokenizer",
"max_prompt_tokens"
);
}

updateProperties() {
const attrs = this.createProperties();
attrs.id = this.id;

return attrs;
}
}
122 changes: 122 additions & 0 deletions assets/javascripts/discourse/components/ai-llm-editor.gjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import Component from "@glimmer/component";
import { tracked } from "@glimmer/tracking";
import { Input } from "@ember/component";
import { action } from "@ember/object";
import { later } from "@ember/runloop";
import { inject as service } from "@ember/service";
import DButton from "discourse/components/d-button";
import { popupAjaxError } from "discourse/lib/ajax-error";
import i18n from "discourse-common/helpers/i18n";
import I18n from "discourse-i18n";
import ComboBox from "select-kit/components/combo-box";
import DTooltip from "float-kit/components/d-tooltip";

export default class AiLlmEditor extends Component {
@service toasts;
@service router;

@tracked isSaving = false;

get selectedProviders() {
const t = (provName) => {
return I18n.t(`discourse_ai.llms.providers.${provName}`);
};

return this.args.llms.resultSetMeta.providers.map((prov) => {
return { id: prov, name: t(prov) };
});
}

@action
async save() {
this.isSaving = true;
const isNew = this.args.model.isNew;

try {
await this.args.model.save();

if (isNew) {
this.args.llms.addObject(this.args.model);
this.router.transitionTo(
"adminPlugins.show.discourse-ai-llms.show",
this.args.model
);
} else {
this.toasts.success({
data: { message: I18n.t("discourse_ai.llms.saved") },
duration: 2000,
});
}
} catch (e) {
popupAjaxError(e);
} finally {
later(() => {
this.isSaving = false;
}, 1000);
}
}

<template>
<form class="form-horizontal ai-llm-editor">
<div class="control-group">
<label>{{i18n "discourse_ai.llms.display_name"}}</label>
<Input
class="ai-llm-editor__display-name"
@type="text"
@value={{@model.display_name}}
/>
</div>
<div class="control-group">
<label>{{i18n "discourse_ai.llms.name"}}</label>
<Input
class="ai-llm-editor__name"
@type="text"
@value={{@model.name}}
/>
<DTooltip
@icon="question-circle"
@content={{I18n.t "discourse_ai.llms.hints.name"}}
/>
</div>
<div class="control-group">
<label>{{I18n.t "discourse_ai.llms.provider"}}</label>
<ComboBox
@value={{@model.provider}}
@content={{this.selectedProviders}}
/>
</div>
<div class="control-group">
<label>{{I18n.t "discourse_ai.llms.tokenizer"}}</label>
<ComboBox
@value={{@model.tokenizer}}
@content={{@llms.resultSetMeta.tokenizers}}
/>
</div>
<div class="control-group">
<label>{{i18n "discourse_ai.llms.max_prompt_tokens"}}</label>
<Input
@type="number"
class="ai-llm-editor__max-prompt-tokens"
step="any"
min="0"
lang="en"
@value={{@model.max_prompt_tokens}}
/>
<DTooltip
@icon="question-circle"
@content={{I18n.t "discourse_ai.llms.hints.max_prompt_tokens"}}
/>
</div>

<div class="control-group ai-llm-editor__action_panel">
<DButton
class="btn-primary ai-llm-editor__save"
@action={{this.save}}
@disabled={{this.isSaving}}
>
{{I18n.t "discourse_ai.llms.save"}}
</DButton>
</div>
</form>
</template>
}
61 changes: 61 additions & 0 deletions assets/javascripts/discourse/components/ai-llms-list-editor.gjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import Component from "@glimmer/component";
import { LinkTo } from "@ember/routing";
import icon from "discourse-common/helpers/d-icon";
import i18n from "discourse-common/helpers/i18n";
import I18n from "discourse-i18n";
import AiLlmEditor from "./ai-llm-editor";

export default class AiLlmsListEditor extends Component {
get hasNoLLMElements() {
this.args.llms.length !== 0;
}

<template>
<section class="ai-llms-list-editor admin-detail pull-left">

<div class="ai-llms-list-editor__header">
<h3>{{i18n "discourse_ai.llms.short_title"}}</h3>
{{#unless @currentLlm.isNew}}
<LinkTo
@route="adminPlugins.show.discourse-ai-llms.new"
class="btn btn-small btn-primary"
>
{{icon "plus"}}
<span>{{I18n.t "discourse_ai.llms.new"}}</span>
</LinkTo>
{{/unless}}
</div>

<div class="ai-llms-list-editor__container">
{{#if this.hasNoLLMElements}}
<div class="ai-llms-list-editor__empty_list">
{{icon "robot"}}
{{i18n "discourse_ai.llms.no_llms"}}
</div>
{{else}}
<div class="content-list ai-llms-list-editor__content_list">
<ul>
{{#each @llms as |llm|}}
<li>
<LinkTo
@route="adminPlugins.show.discourse-ai-llms.show"
current-when="true"
@model={{llm}}
>
{{llm.display_name}}
</LinkTo>
</li>
{{/each}}
</ul>
</div>
{{/if}}

<div class="ai-llms-list-editor__current">
{{#if @currentLlm}}
<AiLlmEditor @model={{@currentLlm}} @llms={{@llms}} />
{{/if}}
</div>
</div>
</section>
</template>
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ export default {
label: "discourse_ai.ai_persona.short_title",
route: "adminPlugins.show.discourse-ai-personas",
},
{
label: "discourse_ai.llms.short_title",
route: "adminPlugins.show.discourse-ai-llms",
},
]);
});
},
Expand Down

0 comments on commit 62fc7d6

Please sign in to comment.