Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FEATURE: AI artifacts #898

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
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
45 changes: 45 additions & 0 deletions app/controllers/discourse_ai/ai_bot/artifacts_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# frozen_string_literal: true

module DiscourseAi
module AiBot
class ArtifactsController < ApplicationController

requires_plugin DiscourseAi::PLUGIN_NAME

skip_before_action :preload_json, :check_xhr, only: %i[show]

def show
artifact = AiArtifact.find(params[:id])

post = Post.find_by(id: artifact.post_id)
raise Discourse::NotFound unless post && guardian.can_see?(post)

# Prepare the HTML document
html = <<~HTML
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>#{ERB::Util.html_escape(artifact.name)}</title>
<style>
#{artifact.css}
</style>
</head>
<body>
#{artifact.html}
<script>
#{artifact.js}
</script>
</body>
</html>
HTML

response.headers.delete("X-Frame-Options")
response.headers.delete("Content-Security-Policy")

# Render the content
render html: html.html_safe, layout: false, content_type: "text/html"
end
end
end
end
22 changes: 22 additions & 0 deletions app/models/ai_artifact.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# frozen_string_literal: true

class AiArtifact < ActiveRecord::Base
belongs_to :user
belongs_to :post
end

# == Schema Information
#
# Table name: ai_artifacts
#
# id :bigint not null, primary key
# user_id :integer not null
# post_id :integer not null
# name :string(255) not null
# html :string(65535)
# css :string(65535)
# js :string(65535)
# metadata :jsonb
# created_at :datetime not null
# updated_at :datetime not null
#
54 changes: 54 additions & 0 deletions assets/javascripts/initializers/ai-artifacts.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { withPluginApi } from "discourse/lib/plugin-api";

function initializeAiArtifactTabs(api) {
api.decorateCooked(
($element) => {
const element = $element[0];
const artifacts = element.querySelectorAll(".ai-artifact");
if (!artifacts.length) {
return;
}

artifacts.forEach((artifact) => {
const tabs = artifact.querySelectorAll(".ai-artifact-tab");
const panels = artifact.querySelectorAll(".ai-artifact-panel");

tabs.forEach((tab) => {
tab.addEventListener("click", (e) => {
e.preventDefault();

if (tab.hasAttribute("data-selected")) {
return;
}

const tabType = Object.keys(tab.dataset).find(
(key) => key !== "selected"
);

tabs.forEach((t) => t.removeAttribute("data-selected"));
panels.forEach((p) => p.removeAttribute("data-selected"));

tab.setAttribute("data-selected", "");
const targetPanel = artifact.querySelector(
`.ai-artifact-panel[data-${tabType}]`
);
if (targetPanel) {
targetPanel.setAttribute("data-selected", "");
}
});
});
});
},
{
id: "ai-artifact-tabs",
onlyStream: false,
}
);
}

export default {
name: "ai-artifact-tabs",
initialize() {
withPluginApi("0.8.7", initializeAiArtifactTabs);
},
};
5 changes: 5 additions & 0 deletions assets/javascripts/lib/discourse-markdown/ai-tags.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
export function setup(helper) {
helper.allowList(["details[class=ai-quote]"]);
helper.allowList(["div[class=ai-artifact]"]);
helper.allowList(["div[class=ai-artifact-tab]"]);
helper.allowList(["div[class=ai-artifact-tabs]"]);
helper.allowList(["div[class=ai-artifact-panels]"]);
helper.allowList(["div[class=ai-artifact-panel]"]);
}
56 changes: 56 additions & 0 deletions assets/stylesheets/modules/ai-bot/common/ai-artifact.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
.ai-artifact {
margin: 1em 0;

.ai-artifact-tabs {
display: flex;
gap: 0.20em;
border-bottom: 2px solid var(--primary-low);
padding: 0 0.2em;

.ai-artifact-tab {
margin-bottom: -2px;

&[data-selected] {
a {
color: var(--tertiary);
font-weight: 500;
border-bottom: 2px solid var(--tertiary);
}
}

&:hover:not([data-selected]) {
a {
color: var(--primary);
background: var(--primary-very-low);
}
}

a {
display: block;
padding: 0.5em 1em;
color: var(--primary-medium);
text-decoration: none;
cursor: pointer;
border-bottom: 2px solid transparent;
}
}
}

.ai-artifact-panels {
padding: 1em 0 0 0;
background: var(--blend-primary-secondary-5);

.ai-artifact-panel {
display: none;
min-height: 400px;

&[data-selected] {
display: block;
}

pre {
margin: 0;
}
}
}
}
3 changes: 3 additions & 0 deletions config/locales/server.en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,7 @@ en:
name: "Base Search Query"
description: "Base query to use when searching. Example: '#urgent' will prepend '#urgent' to the search query and only include topics with the urgent category or tag."
tool_summary:
create_artifact: "Create web artifact"
web_browser: "Browse Web"
github_search_files: "GitHub search files"
github_search_code: "GitHub code search"
Expand All @@ -241,6 +242,7 @@ en:
search_meta_discourse: "Search Meta Discourse"
javascript_evaluator: "Evaluate JavaScript"
tool_help:
create_artifact: "Create a web artifact using the AI Bot"
web_browser: "Browse web page using the AI Bot"
github_search_code: "Search for code in a GitHub repository"
github_search_files: "Search for files in a GitHub repository"
Expand All @@ -262,6 +264,7 @@ en:
search_meta_discourse: "Search Meta Discourse"
javascript_evaluator: "Evaluate JavaScript"
tool_description:
create_artifact: "Created a web artifact using the AI Bot"
web_browser: "Reading <a href='%{url}'>%{url}</a>"
github_search_files: "Searched for '%{keywords}' in %{repo}/%{branch}"
github_search_code: "Searched for '%{query}' in %{repo}"
Expand Down
4 changes: 4 additions & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@
get "/preview/:topic_id" => "shared_ai_conversations#preview"
end

scope module: :ai_bot, path: "/ai-bot/artifacts" do
get "/:id" => "artifacts#show"
end

scope module: :summarization, path: "/summarization", defaults: { format: :json } do
get "/t/:topic_id" => "summary#show", :constraints => { topic_id: /\d+/ }
get "/channels/:channel_id" => "chat_summary#show"
Expand Down
16 changes: 16 additions & 0 deletions db/migrate/20241104053017_add_ai_artifacts.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# frozen_string_literal: true
class AddAiArtifacts < ActiveRecord::Migration[7.1]
def change
create_table :ai_artifacts do |t|
t.integer :user_id, null: false
t.integer :post_id, null: false
t.string :name, null: false, limit: 255
t.string :html, limit: 65535 # ~64KB limit
t.string :css, limit: 65535 # ~64KB limit
t.string :js, limit: 65535 # ~64KB limit
t.jsonb :metadata # For any additional properties

t.timestamps
end
end
end
11 changes: 9 additions & 2 deletions lib/ai_bot/bot.rb
Original file line number Diff line number Diff line change
Expand Up @@ -105,8 +105,15 @@ def reply(context, &update_blk)
tool_found = false
force_tool_if_needed(prompt, context)

tool_progress = proc { |progress| p progress }

result =
llm.generate(prompt, feature_name: "bot", **llm_kwargs) do |partial, cancel|
llm.generate(
prompt,
feature_name: "bot",
tool_progress: tool_progress,
**llm_kwargs,
) do |partial, cancel|
tools = persona.find_tools(partial, bot_user: user, llm: llm, context: context)

if (tools.present?)
Expand Down Expand Up @@ -183,7 +190,7 @@ def invoke_tool(tool, llm, cancel, context, &update_blk)
update_blk.call("", cancel, build_placeholder(tool.summary, ""))

result =
tool.invoke do |progress|
tool.invoke(**tool.parameters) do |progress|
placeholder = build_placeholder(tool.summary, progress)
update_blk.call("", cancel, placeholder)
end
Expand Down
7 changes: 5 additions & 2 deletions lib/ai_bot/personas/persona.rb
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ def all_available_tools
Tools::GithubSearchFiles,
Tools::WebBrowser,
Tools::JavascriptEvaluator,
Tools::CreateArtifact,
]

tools << Tools::GithubSearchCode if SiteSetting.ai_bot_github_access_token.present?
Expand Down Expand Up @@ -242,14 +243,16 @@ def tool_instance(parsed_function, bot_user:, llm:, context:)
arguments[name.to_sym] = value if value
end

tool_klass.new(
arguments,
instance = tool_klass.new(
tool_call_id: function_id || function_name,
persona_options: options[tool_klass].to_h,
bot_user: bot_user,
llm: llm,
context: context,
)

instance.parameters = arguments
instance
end

def strip_quotes(value)
Expand Down
Loading
Loading