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

DEV: artifact system update #1096

Draft
wants to merge 18 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
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ def show
artifact = AiArtifact.find(params[:id])

post = Post.find_by(id: artifact.post_id)
if artifact.metadata&.dig("public")
if artifact.public?
# no guardian needed
else
raise Discourse::NotFound if !post&.topic&.private_message?
Expand Down
4 changes: 4 additions & 0 deletions app/models/ai_artifact.rb
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,10 @@ def create_new_version(html: nil, css: nil, js: nil, change_description: nil)

version
end

def public?
!!metadata&.dig("public")
end
end

# == Schema Information
Expand Down
34 changes: 22 additions & 12 deletions app/models/ai_persona.rb
Original file line number Diff line number Diff line change
Expand Up @@ -130,8 +130,6 @@ def class_instance
tool_details
]

persona_class = DiscourseAi::AiBot::Personas::Persona.system_personas_by_id[self.id]

instance_attributes = {}
attributes.each do |attr|
value = self.read_attribute(attr)
Expand All @@ -140,14 +138,6 @@ def class_instance

instance_attributes[:username] = user&.username_lower

if persona_class
instance_attributes.each do |key, value|
# description/name are localized
persona_class.define_singleton_method(key) { value } if key != :description && key != :name
end
return persona_class
end

options = {}
force_tool_use = []

Expand Down Expand Up @@ -180,6 +170,16 @@ def class_instance
klass
end

persona_class = DiscourseAi::AiBot::Personas::Persona.system_personas_by_id[self.id]
if persona_class
instance_attributes.each do |key, value|
# description/name are localized
persona_class.define_singleton_method(key) { value } if key != :description && key != :name
end
persona_class.define_method(:options) { options }
return persona_class
end

ai_persona_id = self.id

Class.new(DiscourseAi::AiBot::Personas::Persona) do
Expand Down Expand Up @@ -264,9 +264,19 @@ def chat_preconditions
end

def system_persona_unchangeable
if top_p_changed? || temperature_changed? || system_prompt_changed? || tools_changed? ||
name_changed? || description_changed?
if top_p_changed? || temperature_changed? || system_prompt_changed? || name_changed? ||
description_changed?
errors.add(:base, I18n.t("discourse_ai.ai_bot.personas.cannot_edit_system_persona"))
elsif tools_changed?
old_tools = tools_change[0]
new_tools = tools_change[1]

old_tool_names = old_tools.map { |t| t.is_a?(Array) ? t[0] : t }.to_set
new_tool_names = new_tools.map { |t| t.is_a?(Array) ? t[0] : t }.to_set

if old_tool_names != new_tool_names
errors.add(:base, I18n.t("discourse_ai.ai_bot.personas.cannot_edit_system_persona"))
end
end
end

Expand Down
2 changes: 2 additions & 0 deletions app/serializers/ai_tool_serializer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ def options
name: option.localized_name,
description: option.localized_description,
type: option.type,
values: option.values,
default: option.default,
}
end
options
Expand Down
1 change: 1 addition & 0 deletions assets/javascripts/discourse/admin/models/ai-persona.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ const SYSTEM_ATTRIBUTES = [
"enabled",
"system",
"priority",
"tools",
"user_id",
"default_llm",
"force_default_llm",
Expand Down
4 changes: 3 additions & 1 deletion assets/javascripts/discourse/components/ai-llm-selector.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,12 @@ export default class AiLlmSelector extends ComboBox {

@computed
get content() {
const blankName =
this.attrs.blankName || i18n("discourse_ai.ai_persona.no_llm_selected");
return [
{
id: "blank",
name: i18n("discourse_ai.ai_persona.no_llm_selected"),
name: blankName,
},
].concat(this.llms);
}
Expand Down
13 changes: 6 additions & 7 deletions assets/javascripts/discourse/components/ai-persona-editor.gjs
Original file line number Diff line number Diff line change
Expand Up @@ -420,13 +420,12 @@ export default class PersonaEditor extends Component {
</div>
{{/if}}
{{/if}}
{{#unless this.editingModel.system}}
<AiPersonaToolOptions
@persona={{this.editingModel}}
@tools={{this.selectedToolNames}}
@allTools={{@personas.resultSetMeta.tools}}
/>
{{/unless}}
<AiPersonaToolOptions
@persona={{this.editingModel}}
@tools={{this.selectedToolNames}}
@llms={{@personas.resultSetMeta.llms}}
@allTools={{@personas.resultSetMeta.tools}}
/>
<div class="control-group">
<label>{{i18n "discourse_ai.ai_persona.allowed_groups"}}</label>
<GroupChooser
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,78 @@
import { Input } from "@ember/component";
import { on } from "@ember/modifier";
import { action } from "@ember/object";
import { eq } from "truth-helpers";
import { i18n } from "discourse-i18n";
import AiLlmSelector from "./ai-llm-selector";

export default class AiPersonaToolOptionEditor extends Component {
get isBoolean() {
return this.args.option.type === "boolean";
}

get isEnum() {
return this.args.option.type === "enum";
}

get isLlm() {
return this.args.option.type === "llm";
}

get selectedValue() {
return this.args.option.value.value === "true";
}

get selectedLlm() {
if (this.args.option.value.value) {
return `custom:${this.args.option.value.value}`;
} else {
return "blank";
}
}

set selectedLlm(value) {
if (value === "blank") {
this.args.option.value.value = null;
} else {
this.args.option.value.value = value.replace("custom:", "");
}
}

@action
onCheckboxChange(event) {
this.args.option.value.value = event.target.checked ? "true" : "false";
}

@action
onSelectOption(event) {
this.args.option.value.value = event.target.value;
}

<template>
<div class="control-group ai-persona-tool-option-editor">
<label>
{{@option.name}}
</label>
<div class="">
{{#if this.isBoolean}}
{{#if this.isEnum}}
<select name="input" {{on "change" this.onSelectOption}}>
{{#each this.args.option.values as |value|}}

Check failure on line 60 in assets/javascripts/discourse/components/ai-persona-tool-option-editor.gjs

View workflow job for this annotation

GitHub Actions / ci / linting

Component templates should avoid "this.args.option.values" usage, try "@option.values" instead.
<option
value={{value}}
selected={{eq value this.args.option.value.value}}

Check failure on line 63 in assets/javascripts/discourse/components/ai-persona-tool-option-editor.gjs

View workflow job for this annotation

GitHub Actions / ci / linting

Component templates should avoid "this.args.option.value.value" usage, try "@option.value.value" instead.
>
{{value}}
</option>
{{/each}}
</select>
{{else if this.isLlm}}
<AiLlmSelector
class="ai-persona-tool-option-editor__llms"
@value={{this.selectedLlm}}
@llms={{@llms}}
@blankName={{i18n "discourse_ai.ai_persona.use_parent_llm"}}
/>
{{else if this.isBoolean}}
<input
type="checkbox"
checked={{this.selectedValue}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,10 @@ export default class AiPersonaToolOptions extends Component {
</div>
<div class="ai-persona-editor__tool-option-options">
{{#each toolOption.options as |option|}}
<AiPersonaToolOptionEditor @option={{option}} />
<AiPersonaToolOptionEditor
@option={{option}}
@llms={{@llms}}
/>
{{/each}}
</div>
</div>
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 @@ -213,6 +213,7 @@ en:
edit: "Edit"
description: "Description"
no_llm_selected: "No language model selected"
use_parent_llm: "Use personas language model"
max_context_posts: "Max context posts"
max_context_posts_help: "The maximum number of posts to use as context for the AI when responding to a user. (empty for default)"
vision_enabled: Vision enabled
Expand Down
20 changes: 19 additions & 1 deletion config/locales/server.en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,7 @@ en:
link: "Show Artifact in new tab"
view_source: "View Source"
view_changes: "View Changes"
change_description: "Change Description"
unknown_model: "Unknown AI model"

tools:
Expand Down Expand Up @@ -293,6 +294,20 @@ en:
summarizing: "Summarizing topic"
searching: "Searching for: '%{query}'"
tool_options:
create_artifact:
creator_llm:
name: "LLM"
description: "Language model to use for artifact creation"
echo_artifact:
name: "Echo Artifact"
description: "Include full artifact source in subsequent replies"
update_artifact:
editor_llm:
name: "LLM"
description: "Language model to use for artifact edits"
update_algorithm:
name: "Update Algorithm"
description: "Ask LLM to fully replace, or use diff to update"
google:
base_query:
name: "Base Search Query"
Expand All @@ -312,6 +327,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:
read_artifact: "Read a web artifact"
update_artifact: "Update a web artifact"
create_artifact: "Create web artifact"
web_browser: "Browse Web"
Expand All @@ -335,6 +351,7 @@ en:
search_meta_discourse: "Search Meta Discourse"
javascript_evaluator: "Evaluate JavaScript"
tool_help:
read_artifact: "Read a web artifact using the AI Bot"
update_artifact: "Update a web artifact using the AI Bot"
create_artifact: "Create a web artifact using the AI Bot"
web_browser: "Browse web page using the AI Bot"
Expand All @@ -358,8 +375,9 @@ en:
search_meta_discourse: "Search Meta Discourse"
javascript_evaluator: "Evaluate JavaScript"
tool_description:
read_artifact: "Read a web artifact using the AI Bot"
update_artifact: "Updated a web artifact using the AI Bot"
create_artifact: "Created a web artifact using the AI Bot"
create_artifact: "Created a web artifact: %{name} - %{specification}"
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
14 changes: 13 additions & 1 deletion db/fixtures/ai_bot/603_bot_ai_personas.rb
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,19 @@

persona.system = true
instance = persona_class.new
persona.tools = instance.tools.map { |tool| tool.to_s.split("::").last }
tools = {}
instance.tools.map { |tool| tool.to_s.split("::").last }.each { |name| tools[name] = nil }
existing_tools = persona.tools || []

existing_tools.each do |tool|
if tool.is_a?(Array)
name, value = tool
tools[name] = value if tools.key?(name)
end
end

persona.tools = tools.map { |name, value| [name, value] }

persona.system_prompt = instance.system_prompt
persona.top_p = instance.top_p
persona.temperature = instance.temperature
Expand Down
53 changes: 53 additions & 0 deletions lib/ai_bot/artifact_update_strategies/base.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# frozen_string_literal: true
module DiscourseAi
module AiBot
module ArtifactUpdateStrategies
class InvalidFormatError < StandardError
end
class Base
attr_reader :post, :user, :artifact, :artifact_version, :instructions, :llm

def initialize(llm:, post:, user:, artifact:, artifact_version:, instructions:)
@llm = llm
@post = post
@user = user
@artifact = artifact
@artifact_version = artifact_version
@instructions = instructions
end

def apply(&progress)
changes = generate_changes(&progress)
parsed_changes = parse_changes(changes)
apply_changes(parsed_changes)
end

private

def generate_changes(&progress)
response = +""
llm.generate(build_prompt, user: user) do |partial|
progress.call(partial) if progress
response << partial
end
response
end

def build_prompt
# To be implemented by subclasses
raise NotImplementedError
end

def parse_changes(response)
# To be implemented by subclasses
raise NotImplementedError
end

def apply_changes(changes)
# To be implemented by subclasses
raise NotImplementedError
end
end
end
end
end
Loading
Loading