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
1 change: 1 addition & 0 deletions app/models/ai_api_audit_log.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ module Provider
Ollama = 7
SambaNova = 8
Mistral = 9
OpenRouter = 10
end

def next_log_id
Expand Down
5 changes: 5 additions & 0 deletions app/models/llm_model.rb
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,11 @@ def self.provider_params
disable_system_prompt: :checkbox,
enable_native_tool: :checkbox,
},
open_router: {
disable_native_tools: :checkbox,
provider_order: :text,
provider_quantizations: :text,
},
}
end

Expand Down
46 changes: 27 additions & 19 deletions assets/javascripts/discourse/components/ai-artifact.gjs
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
import Component from "@glimmer/component";
import { tracked } from "@glimmer/tracking";
import { on } from "@ember/modifier";
import { action } from "@ember/object";
import { service } from "@ember/service";
import DButton from "discourse/components/d-button";
import htmlClass from "discourse/helpers/html-class";
import getURL from "discourse-common/lib/get-url";

// note the panel for artifact full screen can not be at position 0,0
// otherwise this hack will not activate.
// https://github.com/discourse/discourse/blob/b8325f2190a8c0a9022405c219faeac6f0f98ca5/app/assets/javascripts/discourse/app/components/scrolling-post-stream.js#L77-L77
// this will cause post stream to navigate to a different post

export default class AiArtifactComponent extends Component {
@service siteSettings;
@tracked expanded = false;
Expand All @@ -15,17 +19,29 @@ export default class AiArtifactComponent extends Component {
constructor() {
super(...arguments);
this.keydownHandler = this.handleKeydown.bind(this);
this.popStateHandler = this.handlePopState.bind(this);
window.addEventListener("popstate", this.popStateHandler);
}

willDestroy() {
super.willDestroy(...arguments);
window.removeEventListener("keydown", this.keydownHandler);
window.removeEventListener("popstate", this.popStateHandler);
}

@action
handleKeydown(event) {
if (event.key === "Escape" || event.key === "Esc") {
this.expanded = false;
history.back();
}
}

@action
handlePopState(event) {
const state = event.state;
this.expanded = state?.artifactId === this.args.artifactId;
if (!this.expanded) {
window.removeEventListener("keydown", this.keydownHandler);
}
}

Expand All @@ -52,12 +68,17 @@ export default class AiArtifactComponent extends Component {

@action
toggleView() {
this.expanded = !this.expanded;
if (this.expanded) {
if (!this.expanded) {
window.history.pushState(
{ artifactId: this.args.artifactId },
"",
window.location.href + "#artifact-fullscreen"
);
window.addEventListener("keydown", this.keydownHandler);
} else {
window.removeEventListener("keydown", this.keydownHandler);
history.back();
}
this.expanded = !this.expanded;
}

get wrapperClasses() {
Expand All @@ -66,25 +87,12 @@ export default class AiArtifactComponent extends Component {
}`;
}

@action
artifactPanelHover() {
// retrrigger animation
const panel = document.querySelector(".ai-artifact__panel");
panel.style.animation = "none"; // Stop the animation
setTimeout(() => {
panel.style.animation = ""; // Re-trigger the animation by removing the none style
}, 0);
}

<template>
{{#if this.expanded}}
{{htmlClass "ai-artifact-expanded"}}
{{/if}}
<div class={{this.wrapperClasses}}>
<div
class="ai-artifact__panel--wrapper"
{{on "mouseleave" this.artifactPanelHover}}
>
<div class="ai-artifact__panel--wrapper">
<div class="ai-artifact__panel">
<DButton
class="btn-flat btn-icon-text"
Expand Down
25 changes: 12 additions & 13 deletions assets/stylesheets/modules/ai-bot/common/ai-artifact.scss
Original file line number Diff line number Diff line change
Expand Up @@ -37,27 +37,21 @@ html.ai-artifact-expanded {
}

.ai-artifact__panel--wrapper {
display: block;
position: fixed;
top: 0;
left: 0;
right: 0;
height: 4em;
left: 2em;
right: 2em;
height: 2em;
z-index: 1000000;
&:hover {
.ai-artifact__panel {
transform: translateY(0) !important;
animation: none;
}
}
animation: vanishing 0.5s 3s forwards;
}

.ai-artifact__panel {
display: block;
position: fixed;
top: 0;
left: 0;
right: 0;
left: 2em;
right: 2em;
height: 2em;
transition: transform 0.5s ease-in-out;
animation: slideUp 0.5s 3s forwards;
Expand All @@ -75,7 +69,6 @@ html.ai-artifact-expanded {
.d-icon {
color: var(--secondary-high);
}
//color: var(--secondary-vary-low);
}
}
}
Expand All @@ -85,6 +78,12 @@ html.ai-artifact-expanded {
}
}

@keyframes vanishing {
to {
display: none;
}
}

iframe {
position: fixed;
top: 0;
Expand Down
5 changes: 4 additions & 1 deletion config/locales/client.en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,7 @@ en:
CDCK: "CDCK"
samba_nova: "SambaNova"
mistral: "Mistral"
open_router: "OpenRouter"
fake: "Custom"

provider_fields:
Expand All @@ -360,6 +361,8 @@ en:
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)"
provider_order: "Provider order (comma delimited list)"
provider_quantizations: "Order of provider quantizations (comma delimited list eg: fp16,fp8)"

related_topics:
title: "Related topics"
Expand Down Expand Up @@ -436,7 +439,7 @@ en:

ai_artifact:
expand_view_label: "Expand view"
collapse_view_label: "Exit Fullscreen (ESC)"
collapse_view_label: "Exit Fullscreen (ESC or Back button)"
click_to_run_label: "Run Artifact"

ai_bot:
Expand Down
1 change: 1 addition & 0 deletions config/locales/server.en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,7 @@ en:
failed_to_share: "Failed to share the conversation"
conversation_deleted: "Conversation share deleted successfully"
ai_bot:
reply_error: "Sorry, it looks like our system encountered an unexpected issue while trying to reply.\n\n[details='Error details']\n%{details}\n[/details]"
default_pm_prefix: "[Untitled AI bot PM]"
personas:
default_llm_required: "Default LLM model is required prior to enabling Chat"
Expand Down
20 changes: 16 additions & 4 deletions lib/ai_bot/playground.rb
Original file line number Diff line number Diff line change
Expand Up @@ -533,14 +533,26 @@ def reply_to(post, custom_instructions: nil, &blk)
reply_post.post_custom_prompt.update!(custom_prompt: prompt)
end

reply_post
rescue => e
if reply_post
details = e.message.to_s
reply = "#{reply}\n\n#{I18n.t("discourse_ai.ai_bot.reply_error", details: details)}"
reply_post.revise(
bot.bot_user,
{ raw: reply },
skip_validations: true,
skip_revision: true,
)
end
raise e
ensure
# since we are skipping validations and jobs we
# may need to fix participant count
if reply_post.topic.private_message? && reply_post.topic.participant_count < 2
if reply_post && reply_post.topic && reply_post.topic.private_message? &&
reply_post.topic.participant_count < 2
reply_post.topic.update!(participant_count: 2)
end

reply_post
ensure
post_streamer&.finish(skip_callback: true)
publish_final_update(reply_post) if stream_reply
if reply_post && post.post_number == 1 && post.topic.private_message?
Expand Down
3 changes: 2 additions & 1 deletion lib/completions/dialects/chat_gpt.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ module Dialects
class ChatGpt < Dialect
class << self
def can_translate?(llm_model)
llm_model.provider == "open_ai" || llm_model.provider == "azure"
llm_model.provider == "open_router" || llm_model.provider == "open_ai" ||
llm_model.provider == "azure"
end
end

Expand Down
1 change: 1 addition & 0 deletions lib/completions/endpoints/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ def endpoint_for(provider_name)
DiscourseAi::Completions::Endpoints::Cohere,
DiscourseAi::Completions::Endpoints::SambaNova,
DiscourseAi::Completions::Endpoints::Mistral,
DiscourseAi::Completions::Endpoints::OpenRouter,
]

endpoints << DiscourseAi::Completions::Endpoints::Ollama if Rails.env.development?
Expand Down
42 changes: 42 additions & 0 deletions lib/completions/endpoints/open_router.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# frozen_string_literal: true

module DiscourseAi
module Completions
module Endpoints
class OpenRouter < OpenAi
def self.can_contact?(model_provider)
%w[open_router].include?(model_provider)
end

def prepare_request(payload)
headers = { "Content-Type" => "application/json" }
api_key = llm_model.api_key

headers["Authorization"] = "Bearer #{api_key}"
headers["X-Title"] = "Discourse AI"
headers["HTTP-Referer"] = "https://www.discourse.org/ai"

Net::HTTP::Post.new(model_uri, headers).tap { |r| r.body = payload }
end

def prepare_payload(prompt, model_params, dialect)
payload = super

if quantizations = llm_model.provider_params["provider_quantizations"].presence
options = quantizations.split(",").map(&:strip)

payload[:provider] = { quantizations: options }
end

if order = llm_model.provider_params["provider_order"].presence
options = order.split(",").map(&:strip)
payload[:provider] ||= {}
payload[:provider][:order] = options
end

payload
end
end
end
end
end
19 changes: 19 additions & 0 deletions lib/completions/llm.rb
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,24 @@ def presets
endpoint: "https://api.mistral.ai/v1/chat/completions",
provider: "mistral",
},
{
id: "open_router",
models: [
{
name: "meta-llama/llama-3.3-70b-instruct",
tokens: 128_000,
display_name: "Llama 3.3 70B",
},
{
name: "google/gemini-flash-1.5-exp",
tokens: 1_000_000,
display_name: "Gemini Flash 1.5 Exp",
},
],
tokenizer: DiscourseAi::Tokenizer::OpenAiTokenizer,
endpoint: "https://openrouter.ai/api/v1/chat/completions",
provider: "open_router",
},
]
end
end
Expand All @@ -124,6 +142,7 @@ def provider_names
azure
samba_nova
mistral
open_router
]
if !Rails.env.production?
providers << "fake"
Expand Down
8 changes: 4 additions & 4 deletions lib/summarization/strategies/hot_topic_gists.rb
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ def summary_extension_prompt(summary, contents)
.map { |item| "(#{item[:id]} #{item[:poster]} said: #{item[:text]} " }
.join("\n")

prompt = DiscourseAi::Completions::Prompt.new(<<~TEXT.strip)
prompt = DiscourseAi::Completions::Prompt.new(<<~TEXT.strip, topic_id: target.id)
You are an advanced summarization bot. Your task is to update an existing single-sentence summary by integrating new developments from a conversation.
Analyze the most recent messages to identify key updates or shifts in the main topic and reflect these in the updated summary.
Emphasize new significant information or developments within the context of the initial conversation theme.
Expand Down Expand Up @@ -103,7 +103,7 @@ def first_summary_prompt(contents)
statements =
contents.to_a.map { |item| "(#{item[:id]} #{item[:poster]} said: #{item[:text]} " }

prompt = DiscourseAi::Completions::Prompt.new(<<~TEXT.strip)
prompt = DiscourseAi::Completions::Prompt.new(<<~TEXT.strip, topic_id: target.id)
You are an advanced summarization bot. Analyze a given conversation and produce a concise,
single-sentence summary that conveys the main topic and current developments to someone with no prior context.

Expand All @@ -124,9 +124,9 @@ def first_summary_prompt(contents)
### Context:

#{content_title.present? ? "The discussion title is: " + content_title + ". (DO NOT REPEAT THIS IN THE SUMMARY)\n" : ""}

The conversation began with the following statement:

#{statements.shift}\n
TEXT

Expand Down
4 changes: 2 additions & 2 deletions lib/summarization/strategies/topic_summary.rb
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ def summary_extension_prompt(summary, contents)
input =
contents.map { |item| "(#{item[:id]} #{item[:poster]} said: #{item[:text]})" }.join

prompt = DiscourseAi::Completions::Prompt.new(<<~TEXT)
prompt = DiscourseAi::Completions::Prompt.new(<<~TEXT, topic_id: target.id)
You are an advanced summarization bot tasked with enhancing an existing summary by incorporating additional posts.

### Guidelines:
Expand Down Expand Up @@ -76,7 +76,7 @@ def first_summary_prompt(contents)
input =
contents.map { |item| "(#{item[:id]} #{item[:poster]} said: #{item[:text]} " }.join

prompt = DiscourseAi::Completions::Prompt.new(<<~TEXT.strip)
prompt = DiscourseAi::Completions::Prompt.new(<<~TEXT.strip, topic_id: target.id)
You are an advanced summarization bot that generates concise, coherent summaries of provided text.

- Only include the summary, without any additional commentary.
Expand Down
9 changes: 9 additions & 0 deletions spec/fabricators/llm_model_fabricator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,15 @@
provider "hugging_face"
end

Fabricator(:open_router_model, from: :llm_model) do
display_name "OpenRouter"
name "openrouter-1.0"
provider "open_router"
tokenizer "DiscourseAi::Tokenizer::OpenAiTokenizer"
max_prompt_tokens 64_000
url "https://openrouter.ai/api/v1/chat/completions"
end

Fabricator(:vllm_model, from: :llm_model) do
display_name "Llama 3.1 vLLM"
name "meta-llama/Meta-Llama-3.1-70B-Instruct"
Expand Down
Loading
Loading