Skip to content
This repository was archived by the owner on Jul 22, 2025. It is now read-only.
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
4 changes: 4 additions & 0 deletions app/models/llm_model.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ def self.provider_params
aws_bedrock: {
access_key_id: :text,
region: :text,
disable_native_tools: :checkbox,
},
anthropic: {
disable_native_tools: :checkbox,
},
open_ai: {
organization: :text,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ function isBotMessage(composer, currentUser) {
const reciepients = composer.targetRecipients.split(",");

return currentUser.ai_enabled_chat_bots
.filter((bot) => !bot.is_persona)
.filter((bot) => bot.username)
.any((bot) => reciepients.any((username) => username === bot.username));
}
return false;
Expand Down Expand Up @@ -43,7 +43,7 @@ export default class BotSelector extends Component {
constructor() {
super(...arguments);

if (this.botOptions && this.composer) {
if (this.botOptions && this.botOptions.length && this.composer) {
let personaId = this.preferredPersonaStore.getObject("id");

this._value = this.botOptions[0].id;
Expand All @@ -57,29 +57,49 @@ export default class BotSelector extends Component {
this.composer.metaData = { ai_persona_id: this._value };
this.setAllowLLMSelector();

let llm = this.preferredLlmStore.getObject("id");
if (this.hasLlmSelector) {
let llm = this.preferredLlmStore.getObject("id");

const llmOption =
this.llmOptions.find((innerLlmOption) => innerLlmOption.id === llm) ||
this.llmOptions[0];
const llmOption =
this.llmOptions.find((innerLlmOption) => innerLlmOption.id === llm) ||
this.llmOptions[0];

llm = llmOption.id;
if (llmOption) {
llm = llmOption.id;
} else {
llm = "";
}

if (llm) {
next(() => {
this.currentLlm = llm;
});
if (llm) {
next(() => {
this.currentLlm = llm;
});
}
}

next(() => {
this.resetTargetRecipients();
});
}
}

get composer() {
return this.args?.outletArgs?.model;
}

get hasLlmSelector() {
return this.currentUser.ai_enabled_chat_bots.any((bot) => !bot.is_persona);
}

get botOptions() {
if (this.currentUser.ai_enabled_personas) {
return this.currentUser.ai_enabled_personas.map((persona) => {
let enabledPersonas = this.currentUser.ai_enabled_personas;

if (!this.hasLlmSelector) {
enabledPersonas = enabledPersonas.filter((persona) => persona.username);
}

return enabledPersonas.map((persona) => {
return {
id: persona.id,
name: persona.name,
Expand All @@ -106,6 +126,11 @@ export default class BotSelector extends Component {
}

setAllowLLMSelector() {
if (!this.hasLlmSelector) {
this.allowLLMSelector = false;
return;
}

const persona = this.currentUser.ai_enabled_personas.find(
(innerPersona) => innerPersona.id === this._value
);
Expand Down
1 change: 1 addition & 0 deletions config/locales/client.en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,7 @@ en:
organization: "Optional OpenAI Organization ID"
disable_system_prompt: "Disable system message in prompts"
enable_native_tool: "Enable native tool support"
disable_native_tools: "Disable native tool support (use XML based tools)"

related_topics:
title: "Related Topics"
Expand Down
1 change: 0 additions & 1 deletion config/locales/server.en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@ en:

ai_openai_embeddings_url: "Custom URL used for the OpenAI embeddings API. (in the case of Azure it can be: https://COMPANY.openai.azure.com/openai/deployments/DEPLOYMENT/embeddings?api-version=2023-05-15)"
ai_openai_api_key: "API key for OpenAI API. ONLY used for embeddings and Dall-E. For GPT use the LLM config tab"
ai_anthropic_native_tool_call_models: "List of models that will use native tool calls vs legacy XML based tools."
ai_hugging_face_tei_endpoint: URL where the API is running for the Hugging Face text embeddings inference
ai_hugging_face_tei_api_key: API key for Hugging Face text embeddings inference

Expand Down
10 changes: 0 additions & 10 deletions config/settings.yml
Original file line number Diff line number Diff line change
Expand Up @@ -125,16 +125,6 @@ discourse_ai:
ai_anthropic_api_key:
default: ""
hidden: true
ai_anthropic_native_tool_call_models:
type: list
list_type: compact
default: "claude-3-sonnet|claude-3-haiku"
allow_any: false
choices:
- claude-3-opus
- claude-3-sonnet
- claude-3-haiku
- claude-3-5-sonnet
ai_cohere_api_key:
default: ""
hidden: true
Expand Down
2 changes: 2 additions & 0 deletions lib/ai_bot/entry_point.rb
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,8 @@ def inject_into(plugin)

persona_users = AiPersona.persona_users(user: scope.user)
if persona_users.present?
persona_users.filter! { |persona_user| persona_user[:username].present? }

bots_map.concat(
persona_users.map do |persona_user|
{
Expand Down
2 changes: 1 addition & 1 deletion lib/completions/dialects/claude.rb
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ def max_prompt_tokens
end

def native_tool_support?
SiteSetting.ai_anthropic_native_tool_call_models_map.include?(llm_model.name)
!llm_model.lookup_custom_param("disable_native_tools")
end

private
Expand Down
9 changes: 7 additions & 2 deletions lib/completions/endpoints/anthropic.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ def default_options(dialect)
when "claude-3-opus"
"claude-3-opus-20240229"
when "claude-3-5-sonnet"
"claude-3-5-sonnet-20240620"
"claude-3-5-sonnet-latest"
else
llm_model.name
end
Expand Down Expand Up @@ -70,7 +70,12 @@ def prepare_payload(prompt, model_params, dialect)

payload[:system] = prompt.system_prompt if prompt.system_prompt.present?
payload[:stream] = true if @streaming_mode
payload[:tools] = prompt.tools if prompt.has_tools?
if prompt.has_tools?
payload[:tools] = prompt.tools
if dialect.tool_choice.present?
payload[:tool_choice] = { type: "tool", name: dialect.tool_choice }
end
end

payload
end
Expand Down
10 changes: 8 additions & 2 deletions lib/completions/endpoints/aws_bedrock.rb
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ def model_uri
when "claude-3-opus"
"anthropic.claude-3-opus-20240229-v1:0"
when "claude-3-5-sonnet"
"anthropic.claude-3-5-sonnet-20240620-v1:0"
"anthropic.claude-3-5-sonnet-20241022-v2:0"
else
llm_model.name
end
Expand All @@ -83,7 +83,13 @@ def prepare_payload(prompt, model_params, dialect)

payload = default_options(dialect).merge(model_params).merge(messages: prompt.messages)
payload[:system] = prompt.system_prompt if prompt.system_prompt.present?
payload[:tools] = prompt.tools if prompt.has_tools?

if prompt.has_tools?
payload[:tools] = prompt.tools
if dialect.tool_choice.present?
payload[:tool_choice] = { type: "tool", name: dialect.tool_choice }
end
end

payload
end
Expand Down
17 changes: 14 additions & 3 deletions lib/completions/endpoints/gemini.rb
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,16 @@ def prepare_payload(prompt, model_params, dialect)
} if prompt[:system_instruction].present?
if tools.present?
payload[:tools] = tools
payload[:tool_config] = { function_calling_config: { mode: "AUTO" } }

function_calling_config = { mode: "AUTO" }
if dialect.tool_choice.present?
function_calling_config = {
mode: "ANY",
allowed_function_names: [dialect.tool_choice],
}
end

payload[:tool_config] = { function_calling_config: function_calling_config }
end
payload[:generationConfig].merge!(model_params) if model_params.present?
payload
Expand All @@ -88,8 +97,10 @@ def extract_completion_from(response_raw)
end
response_h = parsed.dig(:candidates, 0, :content, :parts, 0)

@has_function_call ||= response_h.dig(:functionCall).present?
@has_function_call ? response_h[:functionCall] : response_h.dig(:text)
if response_h
@has_function_call ||= response_h.dig(:functionCall).present?
@has_function_call ? response_h.dig(:functionCall) : response_h.dig(:text)
end
end

def partials_from(decoded_chunk)
Expand Down
4 changes: 3 additions & 1 deletion lib/embeddings/semantic_search.rb
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,9 @@ def search_for_topics(query, page = 1, hyde: true)
search = Search.new(query, { guardian: guardian })
search_term = search.term

return [] if search_term.nil? || search_term.length < SiteSetting.min_search_term_length
if search_term.blank? || search_term.length < SiteSetting.min_search_term_length
return Post.none
end

search_embedding = hyde ? hyde_embedding(search_term) : embedding(search_term)

Expand Down
5 changes: 2 additions & 3 deletions spec/lib/completions/dialects/claude_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@
end

it "can properly translate a prompt (legacy tools)" do
SiteSetting.ai_anthropic_native_tool_call_models = ""
llm_model.provider_params["disable_native_tools"] = true
llm_model.save!

tools = [
{
Expand Down Expand Up @@ -88,8 +89,6 @@
end

it "can properly translate a prompt (native tools)" do
SiteSetting.ai_anthropic_native_tool_call_models = "claude-3-opus"

tools = [
{
name: "echo",
Expand Down
3 changes: 0 additions & 3 deletions spec/lib/completions/endpoints/anthropic_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@
end

it "does not eat spaces with tool calls" do
SiteSetting.ai_anthropic_native_tool_call_models = "claude-3-opus"
body = <<~STRING
event: message_start
data: {"type":"message_start","message":{"id":"msg_01Ju4j2MiGQb9KV9EEQ522Y3","type":"message","role":"assistant","model":"claude-3-haiku-20240307","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":1293,"output_tokens":1}} }
Expand Down Expand Up @@ -195,8 +194,6 @@
end

it "supports non streaming tool calls" do
SiteSetting.ai_anthropic_native_tool_call_models = "claude-3-opus"

tool = {
name: "calculate",
description: "calculate something",
Expand Down
4 changes: 3 additions & 1 deletion spec/lib/completions/endpoints/aws_bedrock_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@ def encode_message(message)

describe "function calling" do
it "supports old school xml function calls" do
SiteSetting.ai_anthropic_native_tool_call_models = ""
model.provider_params["disable_native_tools"] = true
model.save!

proxy = DiscourseAi::Completions::Llm.proxy("custom:#{model.id}")

incomplete_tool_call = <<~XML.strip
Expand Down
16 changes: 16 additions & 0 deletions spec/lib/modules/ai_bot/tools/search_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,22 @@

expect(results[:args]).to eq({ search_query: "hello world, sam", status: "public" })
expect(results[:rows].length).to eq(1)

# it also works with no query
search =
described_class.new(
{ order: "likes", user: "sam", status: "public", search_query: "a" },
llm: llm,
bot_user: bot_user,
)

# results will be expanded by semantic search, but it will find nothing
results =
DiscourseAi::Completions::Llm.with_prepared_responses(["<ai>#{query}</ai>"]) do
search.invoke(&progress_blk)
end

expect(results[:rows].length).to eq(0)
end
end

Expand Down
24 changes: 23 additions & 1 deletion spec/system/ai_bot/ai_bot_helper_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,35 @@
group.add(user)
group.save

allowed_persona = AiPersona.last
allowed_persona.update!(allowed_group_ids: [group.id], enabled: true)

visit "/latest"
expect(page).to have_selector(".ai-bot-button")
find(".ai-bot-button").click

# composer is open
find(".gpt-persona").click
expect(page).to have_css(".gpt-persona ul li", count: 1)

find(".llm-selector").click
expect(page).to have_css(".llm-selector ul li", count: 2)

expect(page).to have_selector(".d-editor-container")

# lets disable bots but still allow 1 persona
allowed_persona.create_user!
allowed_persona.update!(default_llm: "custom:#{gpt_4.id}")

gpt_4.update!(enabled_chat_bot: false)
gpt_3_5_turbo.update!(enabled_chat_bot: false)

visit "/latest"
find(".ai-bot-button").click

find(".gpt-persona").click
expect(page).to have_css(".gpt-persona ul li", count: 1)
expect(page).not_to have_selector(".llm-selector")

SiteSetting.ai_bot_add_to_header = false
visit "/latest"
expect(page).not_to have_selector(".ai-bot-button")
Expand Down