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 3 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.nil? || 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