From 41a002bf8cb1239b55e698242eaf7109a83bdf68 Mon Sep 17 00:00:00 2001 From: Sam Saffron Date: Wed, 6 Nov 2024 10:25:02 +1100 Subject: [PATCH 01/29] FEATURE: AI artifacts Initial implementation of an artifact system which allows users to generate HTML pages directly from the AI persona. FEATURE: support tool progress callbacks This is anthropic only for now, but we can get a callback as tool is completing, this gives us the ability to show progress to user as the function is populating. work in progress Revert "work in progress" This reverts commit 30ebe562ea4e5601701ff89af80b964e1cc6af5e. Revert "FEATURE: support tool progress callbacks" This reverts commit fd7ccfd0ab538d2845b096d3902dc06bdbf5cf1f. --- .../ai_bot/artifacts_controller.rb | 45 +++++ app/models/ai_artifact.rb | 22 +++ .../javascripts/initializers/ai-artifacts.js | 54 ++++++ .../lib/discourse-markdown/ai-tags.js | 5 + .../modules/ai-bot/common/ai-artifact.scss | 56 ++++++ config/locales/server.en.yml | 3 + config/routes.rb | 4 + db/migrate/20241104053017_add_ai_artifacts.rb | 16 ++ lib/ai_bot/personas/persona.rb | 1 + lib/ai_bot/tools/create_artifact.rb | 159 ++++++++++++++++++ plugin.rb | 2 + 11 files changed, 367 insertions(+) create mode 100644 app/controllers/discourse_ai/ai_bot/artifacts_controller.rb create mode 100644 app/models/ai_artifact.rb create mode 100644 assets/javascripts/initializers/ai-artifacts.js create mode 100644 assets/stylesheets/modules/ai-bot/common/ai-artifact.scss create mode 100644 db/migrate/20241104053017_add_ai_artifacts.rb create mode 100644 lib/ai_bot/tools/create_artifact.rb diff --git a/app/controllers/discourse_ai/ai_bot/artifacts_controller.rb b/app/controllers/discourse_ai/ai_bot/artifacts_controller.rb new file mode 100644 index 000000000..c9aa3c855 --- /dev/null +++ b/app/controllers/discourse_ai/ai_bot/artifacts_controller.rb @@ -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 + + + + + #{ERB::Util.html_escape(artifact.name)} + + + + #{artifact.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 diff --git a/app/models/ai_artifact.rb b/app/models/ai_artifact.rb new file mode 100644 index 000000000..e44518275 --- /dev/null +++ b/app/models/ai_artifact.rb @@ -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 +# diff --git a/assets/javascripts/initializers/ai-artifacts.js b/assets/javascripts/initializers/ai-artifacts.js new file mode 100644 index 000000000..566d18d6b --- /dev/null +++ b/assets/javascripts/initializers/ai-artifacts.js @@ -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); + }, +}; diff --git a/assets/javascripts/lib/discourse-markdown/ai-tags.js b/assets/javascripts/lib/discourse-markdown/ai-tags.js index c2d9b6726..65d71b5bc 100644 --- a/assets/javascripts/lib/discourse-markdown/ai-tags.js +++ b/assets/javascripts/lib/discourse-markdown/ai-tags.js @@ -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]"]); } diff --git a/assets/stylesheets/modules/ai-bot/common/ai-artifact.scss b/assets/stylesheets/modules/ai-bot/common/ai-artifact.scss new file mode 100644 index 000000000..cf405159f --- /dev/null +++ b/assets/stylesheets/modules/ai-bot/common/ai-artifact.scss @@ -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; + } + } + } +} diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index d18a55c8d..fbafba5c5 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -222,6 +222,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" @@ -243,6 +244,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" @@ -264,6 +266,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 %{url}" github_search_files: "Searched for '%{keywords}' in %{repo}/%{branch}" github_search_code: "Searched for '%{query}' in %{repo}" diff --git a/config/routes.rb b/config/routes.rb index 322e67ce1..6161ab176 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -33,6 +33,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" diff --git a/db/migrate/20241104053017_add_ai_artifacts.rb b/db/migrate/20241104053017_add_ai_artifacts.rb new file mode 100644 index 000000000..895692e6d --- /dev/null +++ b/db/migrate/20241104053017_add_ai_artifacts.rb @@ -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 diff --git a/lib/ai_bot/personas/persona.rb b/lib/ai_bot/personas/persona.rb index 63255a172..e49787b5e 100644 --- a/lib/ai_bot/personas/persona.rb +++ b/lib/ai_bot/personas/persona.rb @@ -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? diff --git a/lib/ai_bot/tools/create_artifact.rb b/lib/ai_bot/tools/create_artifact.rb new file mode 100644 index 000000000..ebbf54aab --- /dev/null +++ b/lib/ai_bot/tools/create_artifact.rb @@ -0,0 +1,159 @@ +# frozen_string_literal: true + +module DiscourseAi + module AiBot + module Tools + class CreateArtifact < Tool + def self.name + "create_artifact" + end + + def self.signature + { + name: "create_artifact", + description: + "Creates a web artifact with HTML, CSS, and JavaScript that can be displayed in an iframe", + parameters: [ + { + name: "name", + description: "A name for the artifact (max 255 chars)", + type: "string", + required: true, + }, + { + name: "html_content", + description: "The HTML content for the artifact", + type: "string", + required: true, + }, + { name: "css", description: "Optional CSS styles for the artifact", type: "string" }, + { + name: "js", + description: + "Optional + JavaScript code for the artifact", + type: "string", + }, + ], + } + end + + def invoke + # Get the current post from context + post = Post.find_by(id: context[:post_id]) + return error_response("No post context found") unless post + + html = parameters[:html_content].to_s + css = parameters[:css].to_s + js = parameters[:js].to_s + + # Create the artifact + artifact = + AiArtifact.new( + user_id: bot_user.id, + post_id: post.id, + name: parameters[:name].to_s[0...255], + html: html, + css: css, + js: js, + metadata: parameters[:metadata], + ) + + if artifact.save + tabs = { + css: [css, "CSS"], + js: [js, "JavaScript"], + html: [html, "HTML"], + preview: [ + "", + "Preview", + ], + } + + first = true + html_tabs = + tabs.map do |tab, (content, name)| + selected = " data-selected" if first + first = false + (<<~HTML).strip +
+ #{name} +
+ HTML + end + + first = true + html_panels = + tabs.map do |tab, (content, name)| + selected = " data-selected" if first + first = false + inner_content = + if tab == :preview + content + else + <<~HTML + + ```#{tab} + #{content} + ``` + HTML + end + (<<~HTML).strip +
+ + #{inner_content} +
+ HTML + end + + self.custom_raw = <<~RAW +
+
+ #{html_tabs.join("\n")} +
+
+ #{html_panels.join("\n")} +
+
+ RAW + + success_response(artifact) + else + error_response(artifact.errors.full_messages.join(", ")) + end + end + + def chain_next_response? + @chain_next_response + end + + private + + def success_response(artifact) + @chain_next_response = false + iframe_url = "#{Discourse.base_url}/discourse-ai/ai-bot/artifacts/#{artifact.id}" + + { + status: "success", + artifact_id: artifact.id, + iframe_html: + "", + message: "Artifact created successfully and rendered to user.", + } + end + + def error_response(message) + @chain_next_response = false + + { status: "error", error: message } + end + + def help + "Creates a web artifact with HTML, CSS, and JavaScript that can be displayed in an iframe. " \ + "Requires a name and HTML content. CSS and JavaScript are optional. " \ + "The artifact will be associated with the current post and can be displayed using an iframe." + end + end + end + end +end diff --git a/plugin.rb b/plugin.rb index bb4a320a1..9d3baf75c 100644 --- a/plugin.rb +++ b/plugin.rb @@ -39,6 +39,8 @@ register_asset "stylesheets/modules/ai-bot/common/ai-tools.scss" +register_asset "stylesheets/modules/ai-bot/common/ai-artifact.scss" + module ::DiscourseAi PLUGIN_NAME = "discourse-ai" end From 1eb19933b5fb803b04bd081b0d4fb4ec37f58b3a Mon Sep 17 00:00:00 2001 From: Sam Saffron Date: Thu, 14 Nov 2024 11:37:29 +1100 Subject: [PATCH 02/29] Handle malformed gemini replies --- lib/ai_bot/tools/create_artifact.rb | 6 ++-- lib/completions/endpoints/gemini.rb | 33 +++++++++---------- spec/lib/completions/endpoints/gemini_spec.rb | 31 +++++++++++++++++ 3 files changed, 50 insertions(+), 20 deletions(-) diff --git a/lib/ai_bot/tools/create_artifact.rb b/lib/ai_bot/tools/create_artifact.rb index ebbf54aab..231945099 100644 --- a/lib/ai_bot/tools/create_artifact.rb +++ b/lib/ai_bot/tools/create_artifact.rb @@ -21,8 +21,8 @@ def self.signature required: true, }, { - name: "html_content", - description: "The HTML content for the artifact", + name: "html_body", + description: "The HTML content for the BODY tag (do not include the BODY tag)", type: "string", required: true, }, @@ -43,7 +43,7 @@ def invoke post = Post.find_by(id: context[:post_id]) return error_response("No post context found") unless post - html = parameters[:html_content].to_s + html = parameters[:html_body].to_s css = parameters[:css].to_s js = parameters[:js].to_s diff --git a/lib/completions/endpoints/gemini.rb b/lib/completions/endpoints/gemini.rb index 2450dc99e..c3afc313d 100644 --- a/lib/completions/endpoints/gemini.rb +++ b/lib/completions/endpoints/gemini.rb @@ -173,25 +173,24 @@ def decode_chunk(chunk) .decode(chunk) .map do |parsed| update_usage(parsed) - parsed - .dig(:candidates, 0, :content, :parts) - .map do |part| - if part[:text] - part = part[:text] - if part != "" - part - else - nil - end - elsif part[:functionCall] - @tool_index += 1 - ToolCall.new( - id: "tool_#{@tool_index}", - name: part[:functionCall][:name], - parameters: part[:functionCall][:args], - ) + parts = parsed.dig(:candidates, 0, :content, :parts) + parts&.map do |part| + if part[:text] + part = part[:text] + if part != "" + part + else + nil end + elsif part[:functionCall] + @tool_index += 1 + ToolCall.new( + id: "tool_#{@tool_index}", + name: part[:functionCall][:name], + parameters: part[:functionCall][:args], + ) end + end end .flatten .compact diff --git a/spec/lib/completions/endpoints/gemini_spec.rb b/spec/lib/completions/endpoints/gemini_spec.rb index 189338438..0c7b92088 100644 --- a/spec/lib/completions/endpoints/gemini_spec.rb +++ b/spec/lib/completions/endpoints/gemini_spec.rb @@ -324,6 +324,37 @@ def tool_response expect(log.response_tokens).to eq(4) end + it "Can correctly handle malformed responses" do + response = <<~TEXT + data: {"candidates": [{"content": {"parts": [{"text": "Certainly"}],"role": "model"}}],"usageMetadata": {"promptTokenCount": 399,"totalTokenCount": 399},"modelVersion": "gemini-1.5-pro-002"} + + data: {"candidates": [{"content": {"parts": [{"text": "! I'll create a simple \\"Hello, World!\\" page where each letter"}],"role": "model"},"safetyRatings": [{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"}]}],"usageMetadata": {"promptTokenCount": 399,"totalTokenCount": 399},"modelVersion": "gemini-1.5-pro-002"} + + data: {"candidates": [{"content": {"parts": [{"text": " has a different color using inline styles for simplicity. Each letter will be wrapped"}],"role": "model"},"safetyRatings": [{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"}]}],"usageMetadata": {"promptTokenCount": 399,"totalTokenCount": 399},"modelVersion": "gemini-1.5-pro-002"} + + data: {"candidates": [{"content": {"parts": [{"text": ""}],"role": "model"},"finishReason": "STOP"}],"usageMetadata": {"promptTokenCount": 399,"candidatesTokenCount": 191,"totalTokenCount": 590},"modelVersion": "gemini-1.5-pro-002"} + + data: {"candidates": [{"finishReason": "MALFORMED_FUNCTION_CALL"}],"usageMetadata": {"promptTokenCount": 399,"candidatesTokenCount": 191,"totalTokenCount": 590},"modelVersion": "gemini-1.5-pro-002"} + + TEXT + + llm = DiscourseAi::Completions::Llm.proxy("custom:#{model.id}") + url = "#{model.url}:streamGenerateContent?alt=sse&key=123" + + output = [] + + stub_request(:post, url).to_return(status: 200, body: response) + llm.generate("Hello", user: user) { |partial| output << partial } + + expect(output).to eq( + [ + "Certainly", + "! I'll create a simple \"Hello, World!\" page where each letter", + " has a different color using inline styles for simplicity. Each letter will be wrapped", + ], + ) + end + it "Can correctly handle streamed responses even if they are chunked badly" do data = +"" data << "da|ta: |" From 7300737e65bf2ae24dd6a913db4d3698b7ebcdfa Mon Sep 17 00:00:00 2001 From: Sam Saffron Date: Thu, 14 Nov 2024 17:24:39 +1100 Subject: [PATCH 03/29] - halt after tools - post streamer ensures we don't have half completed stuff on screen when a tool is slow - reimplemnt xml tools to have a more relaxed parse --- lib/ai_bot/bot.rb | 7 ++ lib/ai_bot/playground.rb | 23 +++---- lib/ai_bot/post_streamer.rb | 58 +++++++++++++++++ lib/ai_bot/tools/create_artifact.rb | 1 + lib/completions/xml_tool_processor.rb | 65 ++++++++++++------- .../completions/xml_tool_processor_spec.rb | 23 ++++++- 6 files changed, 140 insertions(+), 37 deletions(-) create mode 100644 lib/ai_bot/post_streamer.rb diff --git a/lib/ai_bot/bot.rb b/lib/ai_bot/bot.rb index c00d5b650..420c848ea 100644 --- a/lib/ai_bot/bot.rb +++ b/lib/ai_bot/bot.rb @@ -106,6 +106,8 @@ def reply(context, &update_blk) tool_found = false force_tool_if_needed(prompt, context) + tool_halted = false + result = llm.generate(prompt, feature_name: "bot", **llm_kwargs) do |partial, cancel| tool = persona.find_tool(partial, bot_user: user, llm: llm, context: context) @@ -122,7 +124,12 @@ def reply(context, &update_blk) process_tool(tool, raw_context, llm, cancel, update_blk, prompt, context) tools_ran += 1 ongoing_chain &&= tool.chain_next_response? + + if !tool.chain_next_response? + tool_halted = true + end else + next if tool_halted needs_newlines = true if partial.is_a?(DiscourseAi::Completions::ToolCall) Rails.logger.warn("DiscourseAi: Tool not found: #{partial.name}") diff --git a/lib/ai_bot/playground.rb b/lib/ai_bot/playground.rb index 3873ebc61..984dd3c63 100644 --- a/lib/ai_bot/playground.rb +++ b/lib/ai_bot/playground.rb @@ -399,7 +399,7 @@ def reply_to(post, custom_instructions: nil, &blk) PostCustomPrompt.none reply = +"" - start = Time.now + post_streamer = nil post_type = post.post_type == Post.types[:whisper] ? Post.types[:whisper] : Post.types[:regular] @@ -448,6 +448,8 @@ def reply_to(post, custom_instructions: nil, &blk) context[:skip_tool_details] ||= !bot.persona.class.tool_details + post_streamer = PostStreamer.new(delay: Rails.env.test? ? 0 : 0.5) if stream_reply + new_custom_prompts = bot.reply(context) do |partial, cancel, placeholder, type| reply << partial @@ -461,22 +463,20 @@ def reply_to(post, custom_instructions: nil, &blk) reply_post.update!(raw: reply, cooked: PrettyText.cook(reply)) end - if stream_reply - # Minor hack to skip the delay during tests. - if placeholder.blank? - next if (Time.now - start < 0.5) && !Rails.env.test? - start = Time.now - end - - Discourse.redis.expire(redis_stream_key, 60) - - publish_update(reply_post, { raw: raw }) + if post_streamer + post_streamer.run_later { + Discourse.redis.expire(redis_stream_key, 60) + publish_update(reply_post, { raw: raw }) + } end end return if reply.blank? if stream_reply + post_streamer.finish + post_streamer = nil + # land the final message prior to saving so we don't clash reply_post.cooked = PrettyText.cook(reply) publish_final_update(reply_post) @@ -514,6 +514,7 @@ def reply_to(post, custom_instructions: nil, &blk) 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? title_playground(reply_post) diff --git a/lib/ai_bot/post_streamer.rb b/lib/ai_bot/post_streamer.rb new file mode 100644 index 000000000..57ba3c408 --- /dev/null +++ b/lib/ai_bot/post_streamer.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true + +module DiscourseAi + module AiBot + class PostStreamer + def initialize(delay: 0.5) + @mutex = Mutex.new + @callback = nil + @delay = delay + @done = false + end + + def run_later(&callback) + @mutex.synchronize { @callback = callback } + ensure_worker! + end + + def finish(skip_callback: false) + @mutex.synchronize do + @callback&.call if skip_callback + @callback = nil + @done = true + end + + begin + @worker_thread&.wakeup + rescue StandardError + ThreadError + end + @worker_thread&.join + @worker_thread = nil + end + + private + + def run + while !@done + @mutex.synchronize do + callback = @callback + @callback = nil + callback&.call + end + sleep @delay + end + end + + def ensure_worker! + return if @worker_thread + @mutex.synchronize do + return if @worker_thread + db = RailsMultisite::ConnectionManagement.current_db + @worker_thread = + Thread.new { RailsMultisite::ConnectionManagement.with_connection(db) { run } } + end + end + end + end +end diff --git a/lib/ai_bot/tools/create_artifact.rb b/lib/ai_bot/tools/create_artifact.rb index 231945099..5cf67ddcf 100644 --- a/lib/ai_bot/tools/create_artifact.rb +++ b/lib/ai_bot/tools/create_artifact.rb @@ -39,6 +39,7 @@ def self.signature end def invoke + yield parameters[:name] || "Web Artifact" # Get the current post from context post = Post.find_by(id: context[:post_id]) return error_response("No post context found") unless post diff --git a/lib/completions/xml_tool_processor.rb b/lib/completions/xml_tool_processor.rb index 1b42b333c..c1ae9d229 100644 --- a/lib/completions/xml_tool_processor.rb +++ b/lib/completions/xml_tool_processor.rb @@ -62,31 +62,14 @@ def <<(text) def finish return [] if @function_buffer.blank? - xml = Nokogiri::HTML5.fragment(@function_buffer) - normalize_function_ids!(xml) - last_invoke = xml.at("invoke:last") - if last_invoke - last_invoke.next_sibling.remove while last_invoke.next_sibling - xml.at("invoke:last").add_next_sibling("\n") if !last_invoke.next_sibling + idx = -1 + parse_malformed_xml(@function_buffer).map do |tool| + ToolCall.new( + id: "tool_#{idx += 1}", + name: tool[:tool_name], + parameters: tool[:parameters] + ) end - - xml - .css("invoke") - .map do |invoke| - tool_name = invoke.at("tool_name").content.force_encoding("UTF-8") - tool_id = invoke.at("tool_id").content.force_encoding("UTF-8") - parameters = {} - invoke - .at("parameters") - &.children - &.each do |node| - next if node.text? - name = node.name - value = node.content.to_s - parameters[name.to_sym] = value.to_s.force_encoding("UTF-8") - end - ToolCall.new(id: tool_id, name: tool_name, parameters: parameters) - end end def should_cancel? @@ -95,6 +78,40 @@ def should_cancel? private + def parse_malformed_xml(input) + input + .scan( + %r{ + + \s* + + ([^<]+) + + \s* + + (.*?) + + \s* + + }mx, + ) + .map do |tool_name, params| + { + tool_name: tool_name.strip, + parameters: + params + .scan(%r{ + <([^>]+)> + (.*?) + + }mx) + .each_with_object({}) do |(name, value), hash| + hash[name.to_sym] = value.gsub(/^$/, "") + end, + } + end + end + def normalize_function_ids!(function_buffer) function_buffer .css("invoke") diff --git a/spec/lib/completions/xml_tool_processor_spec.rb b/spec/lib/completions/xml_tool_processor_spec.rb index 003f4356c..ad9c1b477 100644 --- a/spec/lib/completions/xml_tool_processor_spec.rb +++ b/spec/lib/completions/xml_tool_processor_spec.rb @@ -12,6 +12,26 @@ expect(processor.should_cancel?).to eq(false) end + it "can handle mix and match xml cause tool llms may not encode" do + xml = (<<~XML).strip + + + hello + + world sam + \n\n]]> + + + XML + + result = [] + result << (processor << xml) + result << (processor.finish) + + tool_call = result.last.first + expect(tool_call.parameters).to eq(hello: "world sam", test: "\n\n") + end + it "is usable for simple single message mode" do xml = (<<~XML).strip hello @@ -149,8 +169,7 @@ result << (processor.finish) # Should just do its best to parse the XML - tool_call = - DiscourseAi::Completions::ToolCall.new(id: "tool_0", name: "test", parameters: { param: "" }) + tool_call = DiscourseAi::Completions::ToolCall.new(id: "tool_0", name: "test", parameters: {}) expect(result).to eq([["text"], [tool_call]]) end From 5c8d62407b9ca0083785f16c9db057ec5cd68080 Mon Sep 17 00:00:00 2001 From: Sam Saffron Date: Thu, 14 Nov 2024 17:52:48 +1100 Subject: [PATCH 04/29] fix ollama tool support --- lib/completions/dialects/ollama.rb | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/lib/completions/dialects/ollama.rb b/lib/completions/dialects/ollama.rb index 3a32e5927..601b937cf 100644 --- a/lib/completions/dialects/ollama.rb +++ b/lib/completions/dialects/ollama.rb @@ -37,11 +37,21 @@ def model_msg(msg) end def tool_call_msg(msg) - tools_dialect.from_raw_tool_call(msg) + if enable_native_tool? + tools_dialect.from_raw_tool_call(msg) + else + translated = tools_dialect.from_raw_tool_call(msg) + { role: "assistant", content: translated } + end end def tool_msg(msg) - tools_dialect.from_raw_tool(msg) + if enable_native_tool? + tools_dialect.from_raw_tool(msg) + else + translated = tools_dialect.from_raw_tool(msg) + { role: "user", content: translated } + end end def system_msg(msg) From 730cef9e23391fc2de5957ed5f181e2f20bac674 Mon Sep 17 00:00:00 2001 From: Sam Saffron Date: Fri, 15 Nov 2024 13:31:57 +1100 Subject: [PATCH 05/29] Support partial tool completions with xml tools --- lib/completions/xml_tool_processor.rb | 80 ++++++++++++++++++- .../completions/xml_tool_processor_spec.rb | 53 ++++++++++++ 2 files changed, 129 insertions(+), 4 deletions(-) diff --git a/lib/completions/xml_tool_processor.rb b/lib/completions/xml_tool_processor.rb index c1ae9d229..1cb53ec91 100644 --- a/lib/completions/xml_tool_processor.rb +++ b/lib/completions/xml_tool_processor.rb @@ -7,11 +7,13 @@ module DiscourseAi module Completions class XmlToolProcessor - def initialize + def initialize(partial_tool_calls: false) @buffer = +"" @function_buffer = +"" @should_cancel = false @in_tool = false + @partial_tool_calls = partial_tool_calls + @partial_tools = [] if @partial_tool_calls end def <<(text) @@ -31,7 +33,7 @@ def <<(text) result << text[0..text_index - 1].strip if text_index && text_index > 0 end else - @function_buffer << text + add_to_function_buffer(text) end if !@in_tool @@ -41,7 +43,7 @@ def <<(text) @function_buffer = text[split_index + 1..-1] || "" text = text[0..split_index] || "" else - @function_buffer << text + add_to_function_buffer(text) text = "" end else @@ -56,6 +58,11 @@ def <<(text) @should_cancel = true if text.include?("") end + if @should_notify_partial_tool + @should_notify_partial_tool = false + result << @partial_tools.last + end + result end @@ -67,7 +74,7 @@ def finish ToolCall.new( id: "tool_#{idx += 1}", name: tool[:tool_name], - parameters: tool[:parameters] + parameters: tool[:parameters], ) end end @@ -78,6 +85,71 @@ def should_cancel? private + def add_to_function_buffer(text) + @function_buffer << text + detect_partial_tool_calls(@function_buffer, text) if @partial_tool_calls + end + + def detect_partial_tool_calls(buffer, delta) + parse_partial_tool_call(buffer) + end + + def parse_partial_tool_call(buffer) + match = + buffer + .scan( + %r{ + + \s* + + ([^<]+) + + \s* + + (.*?) + (|\Z) + }mx, + ) + .to_a + .last + + if match + params = partial_parse_params(match[1]) + if params.present? + current_tool = @partial_tools.last + if !current_tool || current_tool.name != match[0].strip + current_tool = + ToolCall.new( + id: "tool_#{@partial_tools.length}", + name: match[0].strip, + parameters: params, + ) + @partial_tools << current_tool + current_tool.partial = true + @should_notify_partial_tool = true + end + + if current_tool.parameters != params + current_tool.parameters = params + @should_notify_partial_tool = true + end + end + end + end + + def partial_parse_params(params) + params + .scan(%r{ + <([^>]+)> + (.*?) + (|\Z) + }mx) + .each_with_object({}) do |(name, value), hash| + next if "$/, "") + end + end + def parse_malformed_xml(input) input .scan( diff --git a/spec/lib/completions/xml_tool_processor_spec.rb b/spec/lib/completions/xml_tool_processor_spec.rb index ad9c1b477..39af043c1 100644 --- a/spec/lib/completions/xml_tool_processor_spec.rb +++ b/spec/lib/completions/xml_tool_processor_spec.rb @@ -12,6 +12,59 @@ expect(processor.should_cancel?).to eq(false) end + it "can handle partial tool calls" do + processor = DiscourseAi::Completions::XmlToolProcessor.new(partial_tool_calls: true) + + xml = (<<~XML).strip + + + h|ell|o<|/tool_name> + + wo|r|ld + + + + tool|2 + + v|alue + + + + XML + + result = [] + + xml.split("|").each { |part| result << (processor << part).map(&:dup) } + + result << (processor.finish) + result.flatten! + + tool1_params = + result + .select do |r| + r.is_a?(DiscourseAi::Completions::ToolCall) && r.name == "hello" && r.partial + end + .map(&:parameters) + + expect(tool1_params).to eq([{ hello: "wo" }, { hello: "wor" }, { hello: "world" }]) + + tool2_params = + result + .select do |r| + r.is_a?(DiscourseAi::Completions::ToolCall) && r.name == "tool2" && r.partial + end + .map(&:parameters) + + expect(tool2_params).to eq( + [ + { param: "v" }, + { param: "value" }, + { param: "value", param2: "va" }, + { param: "value", param2: "value2" }, + ], + ) + end + it "can handle mix and match xml cause tool llms may not encode" do xml = (<<~XML).strip From 8862e55bd8d7dd43f54e28ad09fdeb8243e78535 Mon Sep 17 00:00:00 2001 From: Sam Saffron Date: Fri, 15 Nov 2024 15:53:48 +1100 Subject: [PATCH 06/29] partial tool calls are implemented --- lib/ai_bot/bot.rb | 35 +++++++-- lib/ai_bot/personas/persona.rb | 33 +++++--- lib/ai_bot/playground.rb | 2 +- lib/ai_bot/tools/create_artifact.rb | 117 +++++++++++++++++++--------- lib/ai_bot/tools/tool.rb | 8 +- lib/completions/endpoints/base.rb | 2 +- lib/completions/endpoints/gemini.rb | 2 +- lib/completions/tool_call.rb | 4 + 8 files changed, 143 insertions(+), 60 deletions(-) diff --git a/lib/ai_bot/bot.rb b/lib/ai_bot/bot.rb index 420c848ea..e333d84c7 100644 --- a/lib/ai_bot/bot.rb +++ b/lib/ai_bot/bot.rb @@ -108,12 +108,37 @@ def reply(context, &update_blk) tool_halted = false + allow_partial_tool_calls = persona.allow_partial_tool_calls? + existing_tools = Set.new + result = - llm.generate(prompt, feature_name: "bot", **llm_kwargs) do |partial, cancel| - tool = persona.find_tool(partial, bot_user: user, llm: llm, context: context) + llm.generate( + prompt, + feature_name: "bot", + partial_tool_calls: allow_partial_tool_calls, + **llm_kwargs, + ) do |partial, cancel| + tool = + persona.find_tool( + partial, + bot_user: user, + llm: llm, + context: context, + existing_tools: existing_tools, + ) tool = nil if tools_ran >= MAX_TOOLS if tool.present? + tool_call = partial + if tool_call.partial? + if tool.class.allow_partial_tool_calls? + tool.partial_invoke + update_blk.call("", cancel, tool.custom_raw, :partial_tool) + end + next + end + + existing_tools << tool tool_found = true # a bit hacky, but extra newlines do no harm if needs_newlines @@ -125,9 +150,7 @@ def reply(context, &update_blk) tools_ran += 1 ongoing_chain &&= tool.chain_next_response? - if !tool.chain_next_response? - tool_halted = true - end + tool_halted = true if !tool.chain_next_response? else next if tool_halted needs_newlines = true @@ -192,7 +215,7 @@ def process_tool(tool, raw_context, llm, cancel, update_blk, prompt, context) end def invoke_tool(tool, llm, cancel, context, &update_blk) - show_placeholder = !context[:skip_tool_details] + show_placeholder = !context[:skip_tool_details] && !tool.class.allow_partial_tool_calls? update_blk.call("", cancel, build_placeholder(tool.summary, "")) if show_placeholder diff --git a/lib/ai_bot/personas/persona.rb b/lib/ai_bot/personas/persona.rb index e49787b5e..51b67dc11 100644 --- a/lib/ai_bot/personas/persona.rb +++ b/lib/ai_bot/personas/persona.rb @@ -200,14 +200,18 @@ def craft_prompt(context, llm: nil) prompt end - def find_tool(partial, bot_user:, llm:, context:) + def find_tool(partial, bot_user:, llm:, context:, existing_tools: []) return nil if !partial.is_a?(DiscourseAi::Completions::ToolCall) - tool_instance(partial, bot_user: bot_user, llm: llm, context: context) + tool_instance(partial, bot_user: bot_user, llm: llm, context: context, existing_tools: existing_tools) + end + + def allow_partial_tool_calls? + available_tools.any? { |tool| tool.allow_partial_tool_calls? } end protected - def tool_instance(tool_call, bot_user:, llm:, context:) + def tool_instance(tool_call, bot_user:, llm:, context:, existing_tools:) function_id = tool_call.id function_name = tool_call.name return nil if function_name.nil? @@ -241,14 +245,21 @@ def tool_instance(tool_call, bot_user:, llm:, context:) arguments[name.to_sym] = value if value end - tool_klass.new( - arguments, - tool_call_id: function_id || function_name, - persona_options: options[tool_klass].to_h, - bot_user: bot_user, - llm: llm, - context: context, - ) + tool_instance = existing_tools.find { |t| t.name == function_name && t.tool_call_id == function_id } + + if tool_instance + tool_instance.parameters = arguments + tool_instance + else + tool_klass.new( + arguments, + tool_call_id: function_id || function_name, + persona_options: options[tool_klass].to_h, + bot_user: bot_user, + llm: llm, + context: context, + ) + end end def strip_quotes(value) diff --git a/lib/ai_bot/playground.rb b/lib/ai_bot/playground.rb index 984dd3c63..3df8b5cb4 100644 --- a/lib/ai_bot/playground.rb +++ b/lib/ai_bot/playground.rb @@ -456,7 +456,7 @@ def reply_to(post, custom_instructions: nil, &blk) raw = reply.dup raw << "\n\n" << placeholder if placeholder.present? - blk.call(partial) if blk && type != :tool_details + blk.call(partial) if blk && type != :tool_details && type != :partial_tool if stream_reply && !Discourse.redis.get(redis_stream_key) cancel&.call diff --git a/lib/ai_bot/tools/create_artifact.rb b/lib/ai_bot/tools/create_artifact.rb index 5cf67ddcf..34c5e4e48 100644 --- a/lib/ai_bot/tools/create_artifact.rb +++ b/lib/ai_bot/tools/create_artifact.rb @@ -38,6 +38,19 @@ def self.signature } end + def self.allow_partial_tool_calls? + true + end + + def partial_invoke + @selected_tab = :html + if @prev_parameters + @selected_tab = parameters.keys.find { |k| @prev_parameters[k] != parameters[k] } + end + update_custom_html + @prev_parameters = parameters.dup + end + def invoke yield parameters[:name] || "Web Artifact" # Get the current post from context @@ -61,34 +74,73 @@ def invoke ) if artifact.save - tabs = { - css: [css, "CSS"], - js: [js, "JavaScript"], - html: [html, "HTML"], - preview: [ - "", - "Preview", - ], - } - - first = true - html_tabs = - tabs.map do |tab, (content, name)| - selected = " data-selected" if first - first = false - (<<~HTML).strip + update_custom_html(artifact) + success_response(artifact) + else + error_response(artifact.errors.full_messages.join(", ")) + end + end + + def chain_next_response? + @chain_next_response + end + + private + + def update_custom_html(artifact = nil) + html = parameters[:html_body].to_s + css = parameters[:css].to_s + js = parameters[:js].to_s + + iframe = + "" if artifact + + content = [] + + content << [:html, "### HTML\n\n```html\n#{html}\n```"] if html.present? + + content << [:css, "### CSS\n\n```css\n#{css}\n```"] if css.present? + + content << [:js, "### JavaScript\n\n```javascript\n#{js}\n```"] if js.present? + + content << [:preview, "### Preview\n\n#{iframe}"] if iframe + + content.sort_by! { |c| c[0] === @selected_tab ? 0 : 1 } if !artifact + + self.custom_raw = content.map { |c| c[1] }.join("\n\n") + end + + def update_custom_html_old(artifact = nil) + html = parameters[:html_body].to_s + css = parameters[:css].to_s + js = parameters[:js].to_s + + tabs = { css: [css, "CSS"], js: [js, "JavaScript"], html: [html, "HTML"] } + + if artifact + iframe = + "" + tabs[:preview] = [iframe, "Preview"] + end + + first = true + html_tabs = + tabs.map do |tab, (content, name)| + selected = " data-selected" if first + first = false + (<<~HTML).strip HTML - end - - first = true - html_panels = - tabs.map do |tab, (content, name)| - selected = " data-selected" if first - first = false - inner_content = + end + + first = true + html_panels = + tabs.map do |tab, (content, name)| + selected = " data-selected" if (first || (!artifact && tab == @selected_tab)) + first = false + inner_content = if tab == :preview content else @@ -99,15 +151,15 @@ def invoke ``` HTML end - (<<~HTML).strip + (<<~HTML).strip
#{inner_content}
HTML - end + end - self.custom_raw = <<~RAW + self.custom_raw = <<~RAW
#{html_tabs.join("\n")} @@ -117,19 +169,8 @@ def invoke
RAW - - success_response(artifact) - else - error_response(artifact.errors.full_messages.join(", ")) - end end - def chain_next_response? - @chain_next_response - end - - private - def success_response(artifact) @chain_next_response = false iframe_url = "#{Discourse.base_url}/discourse-ai/ai-bot/artifacts/#{artifact.id}" diff --git a/lib/ai_bot/tools/tool.rb b/lib/ai_bot/tools/tool.rb index 89ab9d6c5..89cbb7622 100644 --- a/lib/ai_bot/tools/tool.rb +++ b/lib/ai_bot/tools/tool.rb @@ -38,10 +38,14 @@ def help def custom_system_message nil end + + def allow_partial_tool_calls? + false + end end - attr_accessor :custom_raw - attr_reader :tool_call_id, :persona_options, :bot_user, :llm, :context, :parameters + attr_accessor :custom_raw, :parameters + attr_reader :tool_call_id, :persona_options, :bot_user, :llm, :context def initialize( parameters, diff --git a/lib/completions/endpoints/base.rb b/lib/completions/endpoints/base.rb index 7abfdf6a6..627ca4b2b 100644 --- a/lib/completions/endpoints/base.rb +++ b/lib/completions/endpoints/base.rb @@ -96,7 +96,7 @@ def perform_completion!( raise CompletionFailed, response.body end - xml_tool_processor = XmlToolProcessor.new if xml_tools_enabled? && + xml_tool_processor = XmlToolProcessor.new(partial_tool_calls: partial_tool_calls) if xml_tools_enabled? && dialect.prompt.has_tools? to_strip = xml_tags_to_strip(dialect) diff --git a/lib/completions/endpoints/gemini.rb b/lib/completions/endpoints/gemini.rb index c3afc313d..6cab9f8a9 100644 --- a/lib/completions/endpoints/gemini.rb +++ b/lib/completions/endpoints/gemini.rb @@ -144,6 +144,7 @@ def decode(str) def decode(chunk) json = JSON.parse(chunk, symbolize_names: true) + idx = -1 json .dig(:candidates, 0, :content, :parts) @@ -168,7 +169,6 @@ def decode(chunk) def decode_chunk(chunk) @tool_index ||= -1 - streaming_decoder .decode(chunk) .map do |parsed| diff --git a/lib/completions/tool_call.rb b/lib/completions/tool_call.rb index 1dedc7cff..c3aa047bc 100644 --- a/lib/completions/tool_call.rb +++ b/lib/completions/tool_call.rb @@ -6,6 +6,10 @@ class ToolCall attr_reader :id, :name, :parameters attr_accessor :partial + def partial? + !!@partial + end + def initialize(id:, name:, parameters: nil) @id = id @name = name From 0f242bf4ad8c52e8e8bc0ab17f11179dd8e013bf Mon Sep 17 00:00:00 2001 From: Sam Saffron Date: Fri, 15 Nov 2024 16:48:47 +1100 Subject: [PATCH 07/29] Fix progress tracker for tools --- lib/ai_bot/tools/create_artifact.rb | 61 ---- lib/completions/tool_call_progress_tracker.rb | 7 +- spec/fixtures/bot/openai_artifact_call.txt | 299 ++++++++++++++++++ .../lib/completions/endpoints/open_ai_spec.rb | 34 ++ 4 files changed, 339 insertions(+), 62 deletions(-) create mode 100644 spec/fixtures/bot/openai_artifact_call.txt diff --git a/lib/ai_bot/tools/create_artifact.rb b/lib/ai_bot/tools/create_artifact.rb index 34c5e4e48..a1851f1bf 100644 --- a/lib/ai_bot/tools/create_artifact.rb +++ b/lib/ai_bot/tools/create_artifact.rb @@ -110,67 +110,6 @@ def update_custom_html(artifact = nil) self.custom_raw = content.map { |c| c[1] }.join("\n\n") end - def update_custom_html_old(artifact = nil) - html = parameters[:html_body].to_s - css = parameters[:css].to_s - js = parameters[:js].to_s - - tabs = { css: [css, "CSS"], js: [js, "JavaScript"], html: [html, "HTML"] } - - if artifact - iframe = - "" - tabs[:preview] = [iframe, "Preview"] - end - - first = true - html_tabs = - tabs.map do |tab, (content, name)| - selected = " data-selected" if first - first = false - (<<~HTML).strip -
- #{name} -
- HTML - end - - first = true - html_panels = - tabs.map do |tab, (content, name)| - selected = " data-selected" if (first || (!artifact && tab == @selected_tab)) - first = false - inner_content = - if tab == :preview - content - else - <<~HTML - - ```#{tab} - #{content} - ``` - HTML - end - (<<~HTML).strip -
- - #{inner_content} -
- HTML - end - - self.custom_raw = <<~RAW -
-
- #{html_tabs.join("\n")} -
-
- #{html_panels.join("\n")} -
-
- RAW - end - def success_response(artifact) @chain_next_response = false iframe_url = "#{Discourse.base_url}/discourse-ai/ai-bot/artifacts/#{artifact.id}" diff --git a/lib/completions/tool_call_progress_tracker.rb b/lib/completions/tool_call_progress_tracker.rb index f33bd3fc2..0f6d91584 100644 --- a/lib/completions/tool_call_progress_tracker.rb +++ b/lib/completions/tool_call_progress_tracker.rb @@ -16,7 +16,12 @@ def initialize(tool_call) @current_value = nil end - @parser.value { |v| tool_call.notify_progress(@current_key, v) if @current_key } + @parser.value do |v| + if @current_key + tool_call.notify_progress(@current_key, v) + @current_key = nil + end + end end def <<(json) diff --git a/spec/fixtures/bot/openai_artifact_call.txt b/spec/fixtures/bot/openai_artifact_call.txt new file mode 100644 index 000000000..3939500c4 --- /dev/null +++ b/spec/fixtures/bot/openai_artifact_call.txt @@ -0,0 +1,299 @@ +data: {"id":"chatcmpl-ATimVYagKnCWQ0VXY0Hn2SDjRuN6B","object":"chat.completion.chunk","created":1731647015,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_45cf54deae","choices":[{"index":0,"delta":{"role":"assistant","content":null,"tool_calls":[{"index":0,"id":"call_yH3ixdEz4wvSuK8ei3gNYwk3","type":"function","function":{"name":"create_artifact","arguments":""}}],"refusal":null},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-ATimVYagKnCWQ0VXY0Hn2SDjRuN6B","object":"chat.completion.chunk","created":1731647015,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_45cf54deae","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"{\""}}]},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-ATimVYagKnCWQ0VXY0Hn2SDjRuN6B","object":"chat.completion.chunk","created":1731647015,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_45cf54deae","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"name"}}]},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-ATimVYagKnCWQ0VXY0Hn2SDjRuN6B","object":"chat.completion.chunk","created":1731647015,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_45cf54deae","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"\":\""}}]},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-ATimVYagKnCWQ0VXY0Hn2SDjRuN6B","object":"chat.completion.chunk","created":1731647015,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_45cf54deae","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"Five"}}]},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-ATimVYagKnCWQ0VXY0Hn2SDjRuN6B","object":"chat.completion.chunk","created":1731647015,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_45cf54deae","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"Lines"}}]},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-ATimVYagKnCWQ0VXY0Hn2SDjRuN6B","object":"chat.completion.chunk","created":1731647015,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_45cf54deae","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"HTML"}}]},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-ATimVYagKnCWQ0VXY0Hn2SDjRuN6B","object":"chat.completion.chunk","created":1731647015,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_45cf54deae","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"CSS"}}]},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-ATimVYagKnCWQ0VXY0Hn2SDjRuN6B","object":"chat.completion.chunk","created":1731647015,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_45cf54deae","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"JS"}}]},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-ATimVYagKnCWQ0VXY0Hn2SDjRuN6B","object":"chat.completion.chunk","created":1731647015,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_45cf54deae","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"Hello"}}]},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-ATimVYagKnCWQ0VXY0Hn2SDjRuN6B","object":"chat.completion.chunk","created":1731647015,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_45cf54deae","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"World"}}]},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-ATimVYagKnCWQ0VXY0Hn2SDjRuN6B","object":"chat.completion.chunk","created":1731647015,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_45cf54deae","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"\",\""}}]},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-ATimVYagKnCWQ0VXY0Hn2SDjRuN6B","object":"chat.completion.chunk","created":1731647015,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_45cf54deae","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"html"}}]},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-ATimVYagKnCWQ0VXY0Hn2SDjRuN6B","object":"chat.completion.chunk","created":1731647015,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_45cf54deae","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"_body"}}]},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-ATimVYagKnCWQ0VXY0Hn2SDjRuN6B","object":"chat.completion.chunk","created":1731647015,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_45cf54deae","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"\":\""}}]},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-ATimVYagKnCWQ0VXY0Hn2SDjRuN6B","object":"chat.completion.chunk","created":1731647015,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_45cf54deae","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"Hello"}}]},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-ATimVYagKnCWQ0VXY0Hn2SDjRuN6B","object":"chat.completion.chunk","created":1731647015,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_45cf54deae","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"\\"}}]},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-ATimVYagKnCWQ0VXY0Hn2SDjRuN6B","object":"chat.completion.chunk","created":1731647015,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_45cf54deae","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"n"}}]},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-ATimVYagKnCWQ0VXY0Hn2SDjRuN6B","object":"chat.completion.chunk","created":1731647015,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_45cf54deae","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":""}}]},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-ATimVYagKnCWQ0VXY0Hn2SDjRuN6B","object":"chat.completion.chunk","created":1731647015,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_45cf54deae","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"World"}}]},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-ATimVYagKnCWQ0VXY0Hn2SDjRuN6B","object":"chat.completion.chunk","created":1731647015,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_45cf54deae","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"!\\"}}]},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-ATimVYagKnCWQ0VXY0Hn2SDjRuN6B","object":"chat.completion.chunk","created":1731647015,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_45cf54deae","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"n"}}]},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-ATimVYagKnCWQ0VXY0Hn2SDjRuN6B","object":"chat.completion.chunk","created":1731647015,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_45cf54deae","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":""}}]},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-ATimVYagKnCWQ0VXY0Hn2SDjRuN6B","object":"chat.completion.chunk","created":1731647015,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_45cf54deae","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"G"}}]},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-ATimVYagKnCWQ0VXY0Hn2SDjRuN6B","object":"chat.completion.chunk","created":1731647015,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_45cf54deae","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"reet"}}]},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-ATimVYagKnCWQ0VXY0Hn2SDjRuN6B","object":"chat.completion.chunk","created":1731647015,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_45cf54deae","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"\\"}}]},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-ATimVYagKnCWQ0VXY0Hn2SDjRuN6B","object":"chat.completion.chunk","created":1731647015,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_45cf54deae","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"n"}}]},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-ATimVYagKnCWQ0VXY0Hn2SDjRuN6B","object":"chat.completion.chunk","created":1731647015,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_45cf54deae","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"\\"}}]},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-ATimVYagKnCWQ0VXY0Hn2SDjRuN6B","object":"chat.completion.chunk","created":1731647015,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_45cf54deae","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"n"}}]},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-ATimVYagKnCWQ0VXY0Hn2SDjRuN6B","object":"chat.completion.chunk","created":1731647015,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_45cf54deae","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":""}}]},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-ATimVYagKnCWQ0VXY0Hn2SDjRuN6B","object":"chat.completion.chunk","created":1731647015,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_45cf54deae","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"\",\""}}]},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-ATimVYagKnCWQ0VXY0Hn2SDjRuN6B","object":"chat.completion.chunk","created":1731647015,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_45cf54deae","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"css"}}]},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-ATimVYagKnCWQ0VXY0Hn2SDjRuN6B","object":"chat.completion.chunk","created":1731647015,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_45cf54deae","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"\":\""}}]},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-ATimVYagKnCWQ0VXY0Hn2SDjRuN6B","object":"chat.completion.chunk","created":1731647015,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_45cf54deae","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"body"}}]},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-ATimVYagKnCWQ0VXY0Hn2SDjRuN6B","object":"chat.completion.chunk","created":1731647015,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_45cf54deae","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":" {"}}]},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-ATimVYagKnCWQ0VXY0Hn2SDjRuN6B","object":"chat.completion.chunk","created":1731647015,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_45cf54deae","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":" margin"}}]},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-ATimVYagKnCWQ0VXY0Hn2SDjRuN6B","object":"chat.completion.chunk","created":1731647015,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_45cf54deae","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":":"}}]},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-ATimVYagKnCWQ0VXY0Hn2SDjRuN6B","object":"chat.completion.chunk","created":1731647015,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_45cf54deae","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":" "}}]},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-ATimVYagKnCWQ0VXY0Hn2SDjRuN6B","object":"chat.completion.chunk","created":1731647015,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_45cf54deae","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"0"}}]},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-ATimVYagKnCWQ0VXY0Hn2SDjRuN6B","object":"chat.completion.chunk","created":1731647015,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_45cf54deae","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":";"}}]},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-ATimVYagKnCWQ0VXY0Hn2SDjRuN6B","object":"chat.completion.chunk","created":1731647015,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_45cf54deae","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":" }\\"}}]},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-ATimVYagKnCWQ0VXY0Hn2SDjRuN6B","object":"chat.completion.chunk","created":1731647015,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_45cf54deae","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"nh"}}]},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-ATimVYagKnCWQ0VXY0Hn2SDjRuN6B","object":"chat.completion.chunk","created":1731647015,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_45cf54deae","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"1"}}]},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-ATimVYagKnCWQ0VXY0Hn2SDjRuN6B","object":"chat.completion.chunk","created":1731647015,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_45cf54deae","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":" {"}}]},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-ATimVYagKnCWQ0VXY0Hn2SDjRuN6B","object":"chat.completion.chunk","created":1731647015,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_45cf54deae","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":" color"}}]},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-ATimVYagKnCWQ0VXY0Hn2SDjRuN6B","object":"chat.completion.chunk","created":1731647015,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_45cf54deae","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":":"}}]},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-ATimVYagKnCWQ0VXY0Hn2SDjRuN6B","object":"chat.completion.chunk","created":1731647015,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_45cf54deae","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":" blue"}}]},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-ATimVYagKnCWQ0VXY0Hn2SDjRuN6B","object":"chat.completion.chunk","created":1731647015,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_45cf54deae","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":";"}}]},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-ATimVYagKnCWQ0VXY0Hn2SDjRuN6B","object":"chat.completion.chunk","created":1731647015,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_45cf54deae","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":" }\\"}}]},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-ATimVYagKnCWQ0VXY0Hn2SDjRuN6B","object":"chat.completion.chunk","created":1731647015,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_45cf54deae","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"np"}}]},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-ATimVYagKnCWQ0VXY0Hn2SDjRuN6B","object":"chat.completion.chunk","created":1731647015,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_45cf54deae","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":" {"}}]},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-ATimVYagKnCWQ0VXY0Hn2SDjRuN6B","object":"chat.completion.chunk","created":1731647015,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_45cf54deae","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":" font"}}]},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-ATimVYagKnCWQ0VXY0Hn2SDjRuN6B","object":"chat.completion.chunk","created":1731647015,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_45cf54deae","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"-size"}}]},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-ATimVYagKnCWQ0VXY0Hn2SDjRuN6B","object":"chat.completion.chunk","created":1731647015,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_45cf54deae","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":":"}}]},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-ATimVYagKnCWQ0VXY0Hn2SDjRuN6B","object":"chat.completion.chunk","created":1731647015,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_45cf54deae","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":" "}}]},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-ATimVYagKnCWQ0VXY0Hn2SDjRuN6B","object":"chat.completion.chunk","created":1731647015,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_45cf54deae","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"20"}}]},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-ATimVYagKnCWQ0VXY0Hn2SDjRuN6B","object":"chat.completion.chunk","created":1731647015,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_45cf54deae","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"px"}}]},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-ATimVYagKnCWQ0VXY0Hn2SDjRuN6B","object":"chat.completion.chunk","created":1731647015,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_45cf54deae","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":";"}}]},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-ATimVYagKnCWQ0VXY0Hn2SDjRuN6B","object":"chat.completion.chunk","created":1731647015,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_45cf54deae","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":" }\\"}}]},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-ATimVYagKnCWQ0VXY0Hn2SDjRuN6B","object":"chat.completion.chunk","created":1731647015,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_45cf54deae","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"n"}}]},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-ATimVYagKnCWQ0VXY0Hn2SDjRuN6B","object":"chat.completion.chunk","created":1731647015,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_45cf54deae","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"button"}}]},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-ATimVYagKnCWQ0VXY0Hn2SDjRuN6B","object":"chat.completion.chunk","created":1731647015,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_45cf54deae","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":" {"}}]},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-ATimVYagKnCWQ0VXY0Hn2SDjRuN6B","object":"chat.completion.chunk","created":1731647015,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_45cf54deae","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":" padding"}}]},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-ATimVYagKnCWQ0VXY0Hn2SDjRuN6B","object":"chat.completion.chunk","created":1731647015,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_45cf54deae","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":":"}}]},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-ATimVYagKnCWQ0VXY0Hn2SDjRuN6B","object":"chat.completion.chunk","created":1731647015,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_45cf54deae","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":" "}}]},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-ATimVYagKnCWQ0VXY0Hn2SDjRuN6B","object":"chat.completion.chunk","created":1731647015,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_45cf54deae","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"10"}}]},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-ATimVYagKnCWQ0VXY0Hn2SDjRuN6B","object":"chat.completion.chunk","created":1731647015,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_45cf54deae","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"px"}}]},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-ATimVYagKnCWQ0VXY0Hn2SDjRuN6B","object":"chat.completion.chunk","created":1731647015,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_45cf54deae","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":";"}}]},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-ATimVYagKnCWQ0VXY0Hn2SDjRuN6B","object":"chat.completion.chunk","created":1731647015,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_45cf54deae","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":" }\\"}}]},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-ATimVYagKnCWQ0VXY0Hn2SDjRuN6B","object":"chat.completion.chunk","created":1731647015,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_45cf54deae","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"n"}}]},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-ATimVYagKnCWQ0VXY0Hn2SDjRuN6B","object":"chat.completion.chunk","created":1731647015,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_45cf54deae","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"hr"}}]},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-ATimVYagKnCWQ0VXY0Hn2SDjRuN6B","object":"chat.completion.chunk","created":1731647015,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_45cf54deae","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":" {"}}]},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-ATimVYagKnCWQ0VXY0Hn2SDjRuN6B","object":"chat.completion.chunk","created":1731647015,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_45cf54deae","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":" border"}}]},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-ATimVYagKnCWQ0VXY0Hn2SDjRuN6B","object":"chat.completion.chunk","created":1731647015,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_45cf54deae","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":":"}}]},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-ATimVYagKnCWQ0VXY0Hn2SDjRuN6B","object":"chat.completion.chunk","created":1731647015,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_45cf54deae","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":" "}}]},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-ATimVYagKnCWQ0VXY0Hn2SDjRuN6B","object":"chat.completion.chunk","created":1731647015,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_45cf54deae","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"1"}}]},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-ATimVYagKnCWQ0VXY0Hn2SDjRuN6B","object":"chat.completion.chunk","created":1731647015,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_45cf54deae","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"px"}}]},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-ATimVYagKnCWQ0VXY0Hn2SDjRuN6B","object":"chat.completion.chunk","created":1731647015,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_45cf54deae","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":" solid"}}]},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-ATimVYagKnCWQ0VXY0Hn2SDjRuN6B","object":"chat.completion.chunk","created":1731647015,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_45cf54deae","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":" #"}}]},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-ATimVYagKnCWQ0VXY0Hn2SDjRuN6B","object":"chat.completion.chunk","created":1731647015,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_45cf54deae","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"ccc"}}]},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-ATimVYagKnCWQ0VXY0Hn2SDjRuN6B","object":"chat.completion.chunk","created":1731647015,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_45cf54deae","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":";"}}]},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-ATimVYagKnCWQ0VXY0Hn2SDjRuN6B","object":"chat.completion.chunk","created":1731647015,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_45cf54deae","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":" }"}}]},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-ATimVYagKnCWQ0VXY0Hn2SDjRuN6B","object":"chat.completion.chunk","created":1731647015,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_45cf54deae","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"\",\""}}]},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-ATimVYagKnCWQ0VXY0Hn2SDjRuN6B","object":"chat.completion.chunk","created":1731647015,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_45cf54deae","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"js"}}]},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-ATimVYagKnCWQ0VXY0Hn2SDjRuN6B","object":"chat.completion.chunk","created":1731647015,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_45cf54deae","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"\":\""}}]},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-ATimVYagKnCWQ0VXY0Hn2SDjRuN6B","object":"chat.completion.chunk","created":1731647015,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_45cf54deae","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"function"}}]},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-ATimVYagKnCWQ0VXY0Hn2SDjRuN6B","object":"chat.completion.chunk","created":1731647015,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_45cf54deae","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":" show"}}]},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-ATimVYagKnCWQ0VXY0Hn2SDjRuN6B","object":"chat.completion.chunk","created":1731647015,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_45cf54deae","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"Message"}}]},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-ATimVYagKnCWQ0VXY0Hn2SDjRuN6B","object":"chat.completion.chunk","created":1731647015,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_45cf54deae","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"()"}}]},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-ATimVYagKnCWQ0VXY0Hn2SDjRuN6B","object":"chat.completion.chunk","created":1731647015,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_45cf54deae","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":" {\\"}}]},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-ATimVYagKnCWQ0VXY0Hn2SDjRuN6B","object":"chat.completion.chunk","created":1731647015,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_45cf54deae","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"n"}}]},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-ATimVYagKnCWQ0VXY0Hn2SDjRuN6B","object":"chat.completion.chunk","created":1731647015,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_45cf54deae","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":" "}}]},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-ATimVYagKnCWQ0VXY0Hn2SDjRuN6B","object":"chat.completion.chunk","created":1731647015,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_45cf54deae","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":" var"}}]},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-ATimVYagKnCWQ0VXY0Hn2SDjRuN6B","object":"chat.completion.chunk","created":1731647015,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_45cf54deae","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":" message"}}]},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-ATimVYagKnCWQ0VXY0Hn2SDjRuN6B","object":"chat.completion.chunk","created":1731647015,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_45cf54deae","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"Div"}}]},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-ATimVYagKnCWQ0VXY0Hn2SDjRuN6B","object":"chat.completion.chunk","created":1731647015,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_45cf54deae","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":" ="}}]},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-ATimVYagKnCWQ0VXY0Hn2SDjRuN6B","object":"chat.completion.chunk","created":1731647015,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_45cf54deae","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":" document"}}]},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-ATimVYagKnCWQ0VXY0Hn2SDjRuN6B","object":"chat.completion.chunk","created":1731647015,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_45cf54deae","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":".get"}}]},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-ATimVYagKnCWQ0VXY0Hn2SDjRuN6B","object":"chat.completion.chunk","created":1731647015,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_45cf54deae","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"Element"}}]},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-ATimVYagKnCWQ0VXY0Hn2SDjRuN6B","object":"chat.completion.chunk","created":1731647015,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_45cf54deae","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"By"}}]},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-ATimVYagKnCWQ0VXY0Hn2SDjRuN6B","object":"chat.completion.chunk","created":1731647015,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_45cf54deae","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"Id"}}]},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-ATimVYagKnCWQ0VXY0Hn2SDjRuN6B","object":"chat.completion.chunk","created":1731647015,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_45cf54deae","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"('"}}]},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-ATimVYagKnCWQ0VXY0Hn2SDjRuN6B","object":"chat.completion.chunk","created":1731647015,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_45cf54deae","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"message"}}]},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-ATimVYagKnCWQ0VXY0Hn2SDjRuN6B","object":"chat.completion.chunk","created":1731647015,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_45cf54deae","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"');"}}]},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-ATimVYagKnCWQ0VXY0Hn2SDjRuN6B","object":"chat.completion.chunk","created":1731647015,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_45cf54deae","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"\\"}}]},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-ATimVYagKnCWQ0VXY0Hn2SDjRuN6B","object":"chat.completion.chunk","created":1731647015,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_45cf54deae","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"n"}}]},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-ATimVYagKnCWQ0VXY0Hn2SDjRuN6B","object":"chat.completion.chunk","created":1731647015,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_45cf54deae","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":" "}}]},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-ATimVYagKnCWQ0VXY0Hn2SDjRuN6B","object":"chat.completion.chunk","created":1731647015,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_45cf54deae","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":" message"}}]},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-ATimVYagKnCWQ0VXY0Hn2SDjRuN6B","object":"chat.completion.chunk","created":1731647015,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_45cf54deae","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"Div"}}]},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-ATimVYagKnCWQ0VXY0Hn2SDjRuN6B","object":"chat.completion.chunk","created":1731647015,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_45cf54deae","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":".text"}}]},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-ATimVYagKnCWQ0VXY0Hn2SDjRuN6B","object":"chat.completion.chunk","created":1731647015,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_45cf54deae","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"Content"}}]},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-ATimVYagKnCWQ0VXY0Hn2SDjRuN6B","object":"chat.completion.chunk","created":1731647015,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_45cf54deae","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":" ="}}]},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-ATimVYagKnCWQ0VXY0Hn2SDjRuN6B","object":"chat.completion.chunk","created":1731647015,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_45cf54deae","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":" '"}}]},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-ATimVYagKnCWQ0VXY0Hn2SDjRuN6B","object":"chat.completion.chunk","created":1731647015,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_45cf54deae","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"Hello"}}]},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-ATimVYagKnCWQ0VXY0Hn2SDjRuN6B","object":"chat.completion.chunk","created":1731647015,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_45cf54deae","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":","}}]},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-ATimVYagKnCWQ0VXY0Hn2SDjRuN6B","object":"chat.completion.chunk","created":1731647015,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_45cf54deae","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":" World"}}]},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-ATimVYagKnCWQ0VXY0Hn2SDjRuN6B","object":"chat.completion.chunk","created":1731647015,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_45cf54deae","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"!"}}]},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-ATimVYagKnCWQ0VXY0Hn2SDjRuN6B","object":"chat.completion.chunk","created":1731647015,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_45cf54deae","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"';"}}]},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-ATimVYagKnCWQ0VXY0Hn2SDjRuN6B","object":"chat.completion.chunk","created":1731647015,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_45cf54deae","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"\\"}}]},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-ATimVYagKnCWQ0VXY0Hn2SDjRuN6B","object":"chat.completion.chunk","created":1731647015,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_45cf54deae","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"n"}}]},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-ATimVYagKnCWQ0VXY0Hn2SDjRuN6B","object":"chat.completion.chunk","created":1731647015,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_45cf54deae","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"}\\"}}]},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-ATimVYagKnCWQ0VXY0Hn2SDjRuN6B","object":"chat.completion.chunk","created":1731647015,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_45cf54deae","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"n"}}]},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-ATimVYagKnCWQ0VXY0Hn2SDjRuN6B","object":"chat.completion.chunk","created":1731647015,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_45cf54deae","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"\"}"}}]},"logprobs":null,"finish_reason":null}],"usage":null} + +data: {"id":"chatcmpl-ATimVYagKnCWQ0VXY0Hn2SDjRuN6B","object":"chat.completion.chunk","created":1731647015,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_45cf54deae","choices":[{"index":0,"delta":{},"logprobs":null,"finish_reason":"tool_calls"}],"usage":null} + +data: {"id":"chatcmpl-ATimVYagKnCWQ0VXY0Hn2SDjRuN6B","object":"chat.completion.chunk","created":1731647015,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_45cf54deae","choices":[],"usage":{"prompt_tokens":735,"completion_tokens":156,"total_tokens":891,"prompt_tokens_details":{"cached_tokens":0,"audio_tokens":0},"completion_tokens_details":{"reasoning_tokens":0,"audio_tokens":0,"accepted_prediction_tokens":0,"rejected_prediction_tokens":0}}} + +data: [DONE] + + diff --git a/spec/lib/completions/endpoints/open_ai_spec.rb b/spec/lib/completions/endpoints/open_ai_spec.rb index e07914a50..721c4351e 100644 --- a/spec/lib/completions/endpoints/open_ai_spec.rb +++ b/spec/lib/completions/endpoints/open_ai_spec.rb @@ -571,6 +571,40 @@ def request_body(prompt, stream: false, tool_call: false) end end + it "properly handles multiple params in partial tool calls" do + + # this is not working and it is driving me nuts so I will use a sledghammer + # text = plugin_file_from_fixtures("openai_artifact_call.txt", "bot") + + path = File.join(__dir__, "../../../fixtures/bot", "openai_artifact_call.txt") + text = File.read(path) + + partials = [] + open_ai_mock.with_chunk_array_support do + open_ai_mock.stub_raw(text.scan(/.*\n/)) + + dialect = compliance.dialect(prompt: compliance.generic_prompt(tools: tools)) + endpoint.perform_completion!(dialect, user, partial_tool_calls: true) do |partial| + partials << partial.dup + end + end + + expect(partials.compact.length).to eq(128) + + params = partials.map { |p| p.parameters if p.is_a?(DiscourseAi::Completions::ToolCall) && p.partial? }.compact + + lengths = {} + params.each do |p| + p.each do |k, v| + if lengths[k] && lengths[k] > v.length + expect(lengths[k]).to be > v.length + else + lengths[k] = v.length + end + end + end + end + it "properly handles spaces in tools payload and partial tool calls" do raw_data = <<~TEXT.strip data: {"choices":[{"index":0,"delta":{"role":"assistant","content":null,"tool_calls":[{"index":0,"id":"func_id","type":"function","function":{"name":"go|ogle","arg|uments":""}}]}}]} From 07bae2edd928541cd59b9f22e6536ef6d924bdcf Mon Sep 17 00:00:00 2001 From: Sam Saffron Date: Fri, 15 Nov 2024 17:49:01 +1100 Subject: [PATCH 08/29] some bug fixes --- lib/ai_bot/bot.rb | 2 +- lib/completions/anthropic_message_processor.rb | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/ai_bot/bot.rb b/lib/ai_bot/bot.rb index e333d84c7..a8975b9ce 100644 --- a/lib/ai_bot/bot.rb +++ b/lib/ai_bot/bot.rb @@ -129,6 +129,7 @@ def reply(context, &update_blk) tool = nil if tools_ran >= MAX_TOOLS if tool.present? + existing_tools << tool tool_call = partial if tool_call.partial? if tool.class.allow_partial_tool_calls? @@ -138,7 +139,6 @@ def reply(context, &update_blk) next end - existing_tools << tool tool_found = true # a bit hacky, but extra newlines do no harm if needs_newlines diff --git a/lib/completions/anthropic_message_processor.rb b/lib/completions/anthropic_message_processor.rb index aeca321d8..aed06502d 100644 --- a/lib/completions/anthropic_message_processor.rb +++ b/lib/completions/anthropic_message_processor.rb @@ -35,6 +35,8 @@ def partial_tool_call def to_tool_call parameters = JSON.parse(raw_json, symbolize_names: true) + # we dupe to avoid poisoning the original tool call + @tool_call = @tool_call.dup @tool_call.partial = false @tool_call.parameters = parameters @tool_call From 8e2ff32f18879873de43f2a5049792fbc613e243 Mon Sep 17 00:00:00 2001 From: Sam Saffron Date: Fri, 15 Nov 2024 18:09:10 +1100 Subject: [PATCH 09/29] fix sorting --- lib/ai_bot/tools/create_artifact.rb | 6 ++-- .../ai_bot/tools/create_artifact_spec.rb | 35 +++++++++++++++++++ 2 files changed, 38 insertions(+), 3 deletions(-) create mode 100644 spec/lib/modules/ai_bot/tools/create_artifact_spec.rb diff --git a/lib/ai_bot/tools/create_artifact.rb b/lib/ai_bot/tools/create_artifact.rb index a1851f1bf..dfaf8929f 100644 --- a/lib/ai_bot/tools/create_artifact.rb +++ b/lib/ai_bot/tools/create_artifact.rb @@ -43,7 +43,7 @@ def self.allow_partial_tool_calls? end def partial_invoke - @selected_tab = :html + @selected_tab = :html_body if @prev_parameters @selected_tab = parameters.keys.find { |k| @prev_parameters[k] != parameters[k] } end @@ -97,7 +97,7 @@ def update_custom_html(artifact = nil) content = [] - content << [:html, "### HTML\n\n```html\n#{html}\n```"] if html.present? + content << [:html_body, "### HTML\n\n```html\n#{html}\n```"] if html.present? content << [:css, "### CSS\n\n```css\n#{css}\n```"] if css.present? @@ -105,7 +105,7 @@ def update_custom_html(artifact = nil) content << [:preview, "### Preview\n\n#{iframe}"] if iframe - content.sort_by! { |c| c[0] === @selected_tab ? 0 : 1 } if !artifact + content.sort_by! { |c| c[0] === @selected_tab ? 1 : 0 } if !artifact self.custom_raw = content.map { |c| c[1] }.join("\n\n") end diff --git a/spec/lib/modules/ai_bot/tools/create_artifact_spec.rb b/spec/lib/modules/ai_bot/tools/create_artifact_spec.rb new file mode 100644 index 000000000..23ca29bba --- /dev/null +++ b/spec/lib/modules/ai_bot/tools/create_artifact_spec.rb @@ -0,0 +1,35 @@ +#frozen_string_literal: true + +RSpec.describe DiscourseAi::AiBot::Tools::CreateArtifact do + fab!(:llm_model) + let(:bot_user) { DiscourseAi::AiBot::EntryPoint.find_user_from_model(llm_model.name) } + let(:llm) { DiscourseAi::Completions::Llm.proxy("custom:#{llm_model.id}") } + + before { SiteSetting.ai_bot_enabled = true } + + describe "#process" do + it "can correctly handle partial updates" do + tool = described_class.new({}, bot_user: bot_user, llm: llm) + + tool.parameters = { css: "a { }" } + tool.partial_invoke + + expect(tool.custom_raw).to eq("### CSS\n\n```css\na { }\n```") + + tool.parameters = { css: "a { }", html_body: "hello" } + tool.partial_invoke + + expect(tool.custom_raw).to eq( + "### CSS\n\n```css\na { }\n```\n\n### HTML\n\n```html\nhello\n```", + ) + + tool.parameters = { css: "a { }", html_body: "hello world" } + tool.partial_invoke + + expect(tool.custom_raw).to eq( + "### CSS\n\n```css\na { }\n```\n\n### HTML\n\n```html\nhello world\n```", + ) + + end + end +end From 7a934964a32ee24937f83c81e40260074855a472 Mon Sep 17 00:00:00 2001 From: Sam Saffron Date: Fri, 15 Nov 2024 20:37:42 +1100 Subject: [PATCH 10/29] Claude 3.5 models support 8192 output tokens.... --- lib/completions/endpoints/anthropic.rb | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/completions/endpoints/anthropic.rb b/lib/completions/endpoints/anthropic.rb index c505e9360..e2521663e 100644 --- a/lib/completions/endpoints/anthropic.rb +++ b/lib/completions/endpoints/anthropic.rb @@ -32,7 +32,10 @@ def default_options(dialect) llm_model.name end - options = { model: mapped_model, max_tokens: 3_000 } + max_tokens = 4096 + max_tokens = 8192 if mapped_model.include?("3.5") || mapped_model.include?("3_5") + + options = { model: mapped_model, max_tokens: max_tokens } options[:stop_sequences] = ["
"] if !dialect.native_tool_support? && dialect.prompt.has_tools? From d55aad3471cff7e8aad497d821caa473e9e9375b Mon Sep 17 00:00:00 2001 From: Sam Saffron Date: Sat, 16 Nov 2024 16:30:22 +1100 Subject: [PATCH 11/29] implement full screen mode --- .../discourse/components/ai-artifact.gjs | 94 ++++++++++++ .../javascripts/initializers/ai-artifacts.gjs | 31 ++++ .../javascripts/initializers/ai-artifacts.js | 54 ------- .../lib/discourse-markdown/ai-tags.js | 6 +- .../modules/ai-bot/common/ai-artifact.scss | 136 ++++++++++++------ config/locales/client.en.yml | 6 +- lib/ai_bot/tools/create_artifact.rb | 8 +- lib/completions/dialects/xml_tools.rb | 5 +- lib/completions/endpoints/anthropic.rb | 3 +- plugin.rb | 5 + 10 files changed, 234 insertions(+), 114 deletions(-) create mode 100644 assets/javascripts/discourse/components/ai-artifact.gjs create mode 100644 assets/javascripts/initializers/ai-artifacts.gjs delete mode 100644 assets/javascripts/initializers/ai-artifacts.js diff --git a/assets/javascripts/discourse/components/ai-artifact.gjs b/assets/javascripts/discourse/components/ai-artifact.gjs new file mode 100644 index 000000000..dbff17fe7 --- /dev/null +++ b/assets/javascripts/discourse/components/ai-artifact.gjs @@ -0,0 +1,94 @@ +import Component from "@glimmer/component"; +import { tracked } from "@glimmer/tracking"; +import { on } from "@ember/modifier"; +import { action } from "@ember/object"; +import DButton from "discourse/components/d-button"; +import htmlClass from "discourse/helpers/html-class"; +import getURL from "discourse-common/lib/get-url"; + +export default class AiArtifactComponent extends Component { + @tracked expanded = false; + + constructor() { + super(...arguments); + this.keydownHandler = this.handleKeydown.bind(this); + } + + willDestroy() { + super.willDestroy(...arguments); + window.removeEventListener("keydown", this.keydownHandler); + } + + @action + handleKeydown(event) { + if (event.key === "Escape" || event.key === "Esc") { + this.expanded = false; + } + } + + get artifactUrl() { + return getURL(`/discourse-ai/ai-bot/artifacts/${this.args.artifactId}`); + } + + @action + toggleView() { + this.expanded = !this.expanded; + if (this.expanded) { + window.addEventListener("keydown", this.keydownHandler); + } else { + window.removeEventListener("keydown", this.keydownHandler); + } + } + + get wrapperClasses() { + return `ai-artifact__wrapper ${ + this.expanded ? "ai-artifact__expanded" : "" + }`; + } + + @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); + } + + +} diff --git a/assets/javascripts/initializers/ai-artifacts.gjs b/assets/javascripts/initializers/ai-artifacts.gjs new file mode 100644 index 000000000..60fcb558e --- /dev/null +++ b/assets/javascripts/initializers/ai-artifacts.gjs @@ -0,0 +1,31 @@ +import { withPluginApi } from "discourse/lib/plugin-api"; +import AiArtifact from "../discourse/components/ai-artifact"; + +function initializeAiArtifacts(api) { + api.decorateCookedElement( + (element, helper) => { + if (!helper.renderGlimmer) { + return; + } + + [...element.querySelectorAll("div.ai-artifact")].forEach((artifactElement) => { + const artifactId = artifactElement.getAttribute("data-ai-artifact-id"); + + helper.renderGlimmer(artifactElement, ); + }); + }, + { + id: "ai-artifact", + onlyStream: true, + } + ); +} + +export default { + name: "ai-artifact", + initialize() { + withPluginApi("0.8.7", initializeAiArtifacts); + }, +}; diff --git a/assets/javascripts/initializers/ai-artifacts.js b/assets/javascripts/initializers/ai-artifacts.js deleted file mode 100644 index 566d18d6b..000000000 --- a/assets/javascripts/initializers/ai-artifacts.js +++ /dev/null @@ -1,54 +0,0 @@ -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); - }, -}; diff --git a/assets/javascripts/lib/discourse-markdown/ai-tags.js b/assets/javascripts/lib/discourse-markdown/ai-tags.js index 65d71b5bc..bee3f4936 100644 --- a/assets/javascripts/lib/discourse-markdown/ai-tags.js +++ b/assets/javascripts/lib/discourse-markdown/ai-tags.js @@ -1,8 +1,4 @@ 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]"]); + helper.allowList(["div[class=ai-artifact]", "div[data-ai-artifact-id]"]); } diff --git a/assets/stylesheets/modules/ai-bot/common/ai-artifact.scss b/assets/stylesheets/modules/ai-bot/common/ai-artifact.scss index cf405159f..e32a109e6 100644 --- a/assets/stylesheets/modules/ai-bot/common/ai-artifact.scss +++ b/assets/stylesheets/modules/ai-bot/common/ai-artifact.scss @@ -1,56 +1,102 @@ -.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); - } - } +.ai-artifact__wrapper { + iframe { + width: 100%; + height: calc(100% - 2em); + } + height: 500px; + padding-bottom: 2em; +} - &:hover:not([data-selected]) { - a { - color: var(--primary); - background: var(--primary-very-low); - } - } +.ai-artifact__panel { + display: none; +} - a { - display: block; - padding: 0.5em 1em; - color: var(--primary-medium); - text-decoration: none; - cursor: pointer; - border-bottom: 2px solid transparent; - } - } - } +.ai-artifact__expand-button { + //transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); +} + +html.ai-artifact-expanded { + overflow: hidden; +} - .ai-artifact-panels { - padding: 1em 0 0 0; - background: var(--blend-primary-secondary-5); +.ai-artifact__footer { + display: flex; + justify-content: space-between; + align-items: center; + .ai-artifact__expand-button { + margin-left: auto; + } +} - .ai-artifact-panel { - display: none; - min-height: 400px; +.ai-artifact__expanded { + .ai-artifact__footer { + display: none; + } - &[data-selected] { - display: block; + .ai-artifact__panel--wrapper { + display: block; + position: fixed; + top: 0; + left: 0; + right: 0; + height: 4em; + z-index: 1000000; + &:hover { + .ai-artifact__panel { + transform: translateY(0) !important; + animation: none; } + } + } - pre { - margin: 0; + .ai-artifact__panel { + display: block; + position: fixed; + top: 0; + left: 0; + right: 0; + height: 2em; + transition: transform 0.5s ease-in-out; + animation: slideUp 0.5s 3s forwards; + background-color: var(--secondary-low); + opacity: 0.9; + transform: translateY(0); + button { + width: 100%; + text-align: left; + box-sizing: border-box; + justify-content: flex-start; + color: var(--secondary-very-high); + &:hover { + color: var(--secondary-very-high); + .d-icon { + color: var(--secondary-high); + } + //color: var(--secondary-vary-low); } } } + @keyframes slideUp { + to { + transform: translateY(-100%); + } + } + + iframe { + position: fixed; + top: 0; + height: 100%; + left: 0; + right: 0; + bottom: 0; + z-index: z("fullscreen"); + } + + position: fixed; + top: 0; + left: 0; + height: 100%; + width: 100%; + z-index: z("fullscreen"); + background-color: var(--secondary); } diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 22c5f38fe..81fd897ae 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -165,7 +165,7 @@ en: saved: "Persona saved" enabled: "Enabled?" tools: "Enabled tools" - forced_tools: "Forced fools" + forced_tools: "Forced tools" allowed_groups: "Allowed groups" confirm_delete: "Are you sure you want to delete this persona?" new: "New persona" @@ -399,6 +399,10 @@ en: quick_search: suffix: "in all topics and posts with AI" + ai_artifact: + expand_view_label: "Expand view" + collapse_view_label: "Exit Fullscreen (ESC)" + ai_bot: pm_warning: "AI chatbot messages are monitored regularly by moderators." cancel_streaming: "Stop reply" diff --git a/lib/ai_bot/tools/create_artifact.rb b/lib/ai_bot/tools/create_artifact.rb index dfaf8929f..c35f4ae63 100644 --- a/lib/ai_bot/tools/create_artifact.rb +++ b/lib/ai_bot/tools/create_artifact.rb @@ -92,8 +92,7 @@ def update_custom_html(artifact = nil) css = parameters[:css].to_s js = parameters[:js].to_s - iframe = - "" if artifact + artifact_div = "
" if artifact content = [] @@ -103,7 +102,7 @@ def update_custom_html(artifact = nil) content << [:js, "### JavaScript\n\n```javascript\n#{js}\n```"] if js.present? - content << [:preview, "### Preview\n\n#{iframe}"] if iframe + content << [:preview, "### Preview\n\n#{artifact_div}"] if artifact_div content.sort_by! { |c| c[0] === @selected_tab ? 1 : 0 } if !artifact @@ -112,13 +111,10 @@ def update_custom_html(artifact = nil) def success_response(artifact) @chain_next_response = false - iframe_url = "#{Discourse.base_url}/discourse-ai/ai-bot/artifacts/#{artifact.id}" { status: "success", artifact_id: artifact.id, - iframe_html: - "", message: "Artifact created successfully and rendered to user.", } end diff --git a/lib/completions/dialects/xml_tools.rb b/lib/completions/dialects/xml_tools.rb index 9eabfadf0..2ca5c073b 100644 --- a/lib/completions/dialects/xml_tools.rb +++ b/lib/completions/dialects/xml_tools.rb @@ -118,8 +118,9 @@ def tool_preamble(include_array_tip: true) If you wish to call multiple function in one reply, wrap multiple block in a single block. - Always prefer to lead with tool calls, if you need to execute any. - Avoid all niceties prior to tool calls, Eg: "Let me look this up for you.." etc. + - Always prefer to lead with tool calls, if you need to execute any. + - Avoid all niceties prior to tool calls, Eg: "Let me look this up for you.." etc. + - DO NOT encode HTML entities in tool calls. You may use for encoding if required. Here are the complete list of tools available: TEXT end diff --git a/lib/completions/endpoints/anthropic.rb b/lib/completions/endpoints/anthropic.rb index e2521663e..ffbdb024d 100644 --- a/lib/completions/endpoints/anthropic.rb +++ b/lib/completions/endpoints/anthropic.rb @@ -32,8 +32,9 @@ def default_options(dialect) llm_model.name end + # Note: Anthropic requires this param max_tokens = 4096 - max_tokens = 8192 if mapped_model.include?("3.5") || mapped_model.include?("3_5") + max_tokens = 8192 if mapped_model.match?(/3.5/) options = { model: mapped_model, max_tokens: max_tokens } diff --git a/plugin.rb b/plugin.rb index 9d3baf75c..93a282b68 100644 --- a/plugin.rb +++ b/plugin.rb @@ -50,6 +50,11 @@ module ::DiscourseAi require_relative "lib/engine" after_initialize do + + if defined?(Rack::MiniProfiler) + Rack::MiniProfiler.config.skip_paths << "/discourse-ai/ai-bot/artifacts" + end + # do not autoload this cause we may have no namespace require_relative "discourse_automation/llm_triage" require_relative "discourse_automation/llm_report" From 3add2a905ed5abbb3ec08f55b2f35d3e664956a4 Mon Sep 17 00:00:00 2001 From: Sam Saffron Date: Sat, 16 Nov 2024 16:57:34 +1100 Subject: [PATCH 12/29] remove uneeded code --- lib/completions/dialects/ollama.rb | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/lib/completions/dialects/ollama.rb b/lib/completions/dialects/ollama.rb index 601b937cf..541144003 100644 --- a/lib/completions/dialects/ollama.rb +++ b/lib/completions/dialects/ollama.rb @@ -40,8 +40,7 @@ def tool_call_msg(msg) if enable_native_tool? tools_dialect.from_raw_tool_call(msg) else - translated = tools_dialect.from_raw_tool_call(msg) - { role: "assistant", content: translated } + super end end @@ -49,8 +48,7 @@ def tool_msg(msg) if enable_native_tool? tools_dialect.from_raw_tool(msg) else - translated = tools_dialect.from_raw_tool(msg) - { role: "user", content: translated } + super end end From 2ed1bec45cc3b11c863492747b7ef2e32331865b Mon Sep 17 00:00:00 2001 From: Sam Saffron Date: Sat, 16 Nov 2024 17:12:23 +1100 Subject: [PATCH 13/29] lint --- .../discourse_ai/ai_bot/artifacts_controller.rb | 1 - db/migrate/20241104053017_add_ai_artifacts.rb | 8 ++++---- lib/ai_bot/personas/persona.rb | 11 +++++++++-- lib/ai_bot/playground.rb | 4 ++-- lib/ai_bot/tools/create_artifact.rb | 3 ++- lib/completions/endpoints/base.rb | 6 ++++-- plugin.rb | 1 - spec/lib/completions/endpoints/anthropic_spec.rb | 6 +++--- spec/lib/completions/endpoints/open_ai_spec.rb | 6 ++++-- spec/lib/modules/ai_bot/playground_spec.rb | 7 ++++--- spec/lib/modules/ai_bot/tools/create_artifact_spec.rb | 1 - 11 files changed, 32 insertions(+), 22 deletions(-) diff --git a/app/controllers/discourse_ai/ai_bot/artifacts_controller.rb b/app/controllers/discourse_ai/ai_bot/artifacts_controller.rb index c9aa3c855..7ca7a8d33 100644 --- a/app/controllers/discourse_ai/ai_bot/artifacts_controller.rb +++ b/app/controllers/discourse_ai/ai_bot/artifacts_controller.rb @@ -3,7 +3,6 @@ module DiscourseAi module AiBot class ArtifactsController < ApplicationController - requires_plugin DiscourseAi::PLUGIN_NAME skip_before_action :preload_json, :check_xhr, only: %i[show] diff --git a/db/migrate/20241104053017_add_ai_artifacts.rb b/db/migrate/20241104053017_add_ai_artifacts.rb index 895692e6d..3ca789277 100644 --- a/db/migrate/20241104053017_add_ai_artifacts.rb +++ b/db/migrate/20241104053017_add_ai_artifacts.rb @@ -5,10 +5,10 @@ def change 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.string :html, limit: 65_535 # ~64KB limit + t.string :css, limit: 65_535 # ~64KB limit + t.string :js, limit: 65_535 # ~64KB limit + t.jsonb :metadata # For any additional properties t.timestamps end diff --git a/lib/ai_bot/personas/persona.rb b/lib/ai_bot/personas/persona.rb index 51b67dc11..f2fe68553 100644 --- a/lib/ai_bot/personas/persona.rb +++ b/lib/ai_bot/personas/persona.rb @@ -202,7 +202,13 @@ def craft_prompt(context, llm: nil) def find_tool(partial, bot_user:, llm:, context:, existing_tools: []) return nil if !partial.is_a?(DiscourseAi::Completions::ToolCall) - tool_instance(partial, bot_user: bot_user, llm: llm, context: context, existing_tools: existing_tools) + tool_instance( + partial, + bot_user: bot_user, + llm: llm, + context: context, + existing_tools: existing_tools, + ) end def allow_partial_tool_calls? @@ -245,7 +251,8 @@ def tool_instance(tool_call, bot_user:, llm:, context:, existing_tools:) arguments[name.to_sym] = value if value end - tool_instance = existing_tools.find { |t| t.name == function_name && t.tool_call_id == function_id } + tool_instance = + existing_tools.find { |t| t.name == function_name && t.tool_call_id == function_id } if tool_instance tool_instance.parameters = arguments diff --git a/lib/ai_bot/playground.rb b/lib/ai_bot/playground.rb index 3df8b5cb4..222d79c8d 100644 --- a/lib/ai_bot/playground.rb +++ b/lib/ai_bot/playground.rb @@ -464,10 +464,10 @@ def reply_to(post, custom_instructions: nil, &blk) end if post_streamer - post_streamer.run_later { + post_streamer.run_later do Discourse.redis.expire(redis_stream_key, 60) publish_update(reply_post, { raw: raw }) - } + end end end diff --git a/lib/ai_bot/tools/create_artifact.rb b/lib/ai_bot/tools/create_artifact.rb index c35f4ae63..cb2d8d57f 100644 --- a/lib/ai_bot/tools/create_artifact.rb +++ b/lib/ai_bot/tools/create_artifact.rb @@ -92,7 +92,8 @@ def update_custom_html(artifact = nil) css = parameters[:css].to_s js = parameters[:js].to_s - artifact_div = "
" if artifact + artifact_div = + "
" if artifact content = [] diff --git a/lib/completions/endpoints/base.rb b/lib/completions/endpoints/base.rb index 627ca4b2b..6ad24fbc3 100644 --- a/lib/completions/endpoints/base.rb +++ b/lib/completions/endpoints/base.rb @@ -96,8 +96,10 @@ def perform_completion!( raise CompletionFailed, response.body end - xml_tool_processor = XmlToolProcessor.new(partial_tool_calls: partial_tool_calls) if xml_tools_enabled? && - dialect.prompt.has_tools? + xml_tool_processor = + XmlToolProcessor.new( + partial_tool_calls: partial_tool_calls, + ) if xml_tools_enabled? && dialect.prompt.has_tools? to_strip = xml_tags_to_strip(dialect) xml_stripper = diff --git a/plugin.rb b/plugin.rb index 93a282b68..4f58560be 100644 --- a/plugin.rb +++ b/plugin.rb @@ -50,7 +50,6 @@ module ::DiscourseAi require_relative "lib/engine" after_initialize do - if defined?(Rack::MiniProfiler) Rack::MiniProfiler.config.skip_paths << "/discourse-ai/ai-bot/artifacts" end diff --git a/spec/lib/completions/endpoints/anthropic_spec.rb b/spec/lib/completions/endpoints/anthropic_spec.rb index 8bdc796e6..72ba2422d 100644 --- a/spec/lib/completions/endpoints/anthropic_spec.rb +++ b/spec/lib/completions/endpoints/anthropic_spec.rb @@ -186,7 +186,7 @@ expected_body = { model: "claude-3-opus-20240229", - max_tokens: 3000, + max_tokens: 4096, messages: [{ role: "user", content: "user1: hello" }], system: "You are hello bot", stream: true, @@ -278,7 +278,7 @@ request_body = { model: "claude-3-opus-20240229", - max_tokens: 3000, + max_tokens: 4096, messages: [ { role: "user", @@ -376,7 +376,7 @@ expected_body = { model: "claude-3-opus-20240229", - max_tokens: 3000, + max_tokens: 4096, messages: [{ role: "user", content: "user1: hello" }], system: "You are hello bot", } diff --git a/spec/lib/completions/endpoints/open_ai_spec.rb b/spec/lib/completions/endpoints/open_ai_spec.rb index 721c4351e..08b084495 100644 --- a/spec/lib/completions/endpoints/open_ai_spec.rb +++ b/spec/lib/completions/endpoints/open_ai_spec.rb @@ -572,7 +572,6 @@ def request_body(prompt, stream: false, tool_call: false) end it "properly handles multiple params in partial tool calls" do - # this is not working and it is driving me nuts so I will use a sledghammer # text = plugin_file_from_fixtures("openai_artifact_call.txt", "bot") @@ -591,7 +590,10 @@ def request_body(prompt, stream: false, tool_call: false) expect(partials.compact.length).to eq(128) - params = partials.map { |p| p.parameters if p.is_a?(DiscourseAi::Completions::ToolCall) && p.partial? }.compact + params = + partials + .map { |p| p.parameters if p.is_a?(DiscourseAi::Completions::ToolCall) && p.partial? } + .compact lengths = {} params.each do |p| diff --git a/spec/lib/modules/ai_bot/playground_spec.rb b/spec/lib/modules/ai_bot/playground_spec.rb index 2a07ad524..07485d1f8 100644 --- a/spec/lib/modules/ai_bot/playground_spec.rb +++ b/spec/lib/modules/ai_bot/playground_spec.rb @@ -791,11 +791,12 @@ expect(done_signal.data[:cooked]).to eq(reply.cooked) expect(messages.first.data[:raw]).to eq("") - messages[1..-1].each_with_index do |m, idx| - expect(m.data[:raw]).to eq(expected_bot_response[0..idx]) - end expect(reply.cooked).to eq(PrettyText.cook(expected_bot_response)) + + messages[1..-1].each do |m| + expect(expected_bot_response.start_with?(m.data[:raw])).to eq(true) + end end end diff --git a/spec/lib/modules/ai_bot/tools/create_artifact_spec.rb b/spec/lib/modules/ai_bot/tools/create_artifact_spec.rb index 23ca29bba..f4e955fc0 100644 --- a/spec/lib/modules/ai_bot/tools/create_artifact_spec.rb +++ b/spec/lib/modules/ai_bot/tools/create_artifact_spec.rb @@ -29,7 +29,6 @@ expect(tool.custom_raw).to eq( "### CSS\n\n```css\na { }\n```\n\n### HTML\n\n```html\nhello world\n```", ) - end end end From d2f8a92739c07d68f5dda156cbd1c9bfb872362a Mon Sep 17 00:00:00 2001 From: Sam Saffron Date: Sat, 16 Nov 2024 17:14:43 +1100 Subject: [PATCH 14/29] more linting --- .../javascripts/discourse/components/ai-artifact.gjs | 6 +++--- assets/javascripts/initializers/ai-artifacts.gjs | 12 ++++++++---- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/assets/javascripts/discourse/components/ai-artifact.gjs b/assets/javascripts/discourse/components/ai-artifact.gjs index dbff17fe7..8129ff191 100644 --- a/assets/javascripts/discourse/components/ai-artifact.gjs +++ b/assets/javascripts/discourse/components/ai-artifact.gjs @@ -49,10 +49,10 @@ export default class AiArtifactComponent extends Component { @action artifactPanelHover() { // retrrigger animation - const panel = document.querySelector('.ai-artifact__panel'); - panel.style.animation = 'none'; // Stop the 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 + panel.style.animation = ""; // Re-trigger the animation by removing the none style }, 0); } diff --git a/assets/javascripts/initializers/ai-artifacts.gjs b/assets/javascripts/initializers/ai-artifacts.gjs index 60fcb558e..2f6c6023d 100644 --- a/assets/javascripts/initializers/ai-artifacts.gjs +++ b/assets/javascripts/initializers/ai-artifacts.gjs @@ -8,13 +8,17 @@ function initializeAiArtifacts(api) { return; } - [...element.querySelectorAll("div.ai-artifact")].forEach((artifactElement) => { - const artifactId = artifactElement.getAttribute("data-ai-artifact-id"); + [...element.querySelectorAll("div.ai-artifact")].forEach( + (artifactElement) => { + const artifactId = artifactElement.getAttribute( + "data-ai-artifact-id" + ); - helper.renderGlimmer(artifactElement, } diff --git a/assets/stylesheets/modules/ai-bot/common/ai-artifact.scss b/assets/stylesheets/modules/ai-bot/common/ai-artifact.scss index 677906ef0..cd32601a3 100644 --- a/assets/stylesheets/modules/ai-bot/common/ai-artifact.scss +++ b/assets/stylesheets/modules/ai-bot/common/ai-artifact.scss @@ -7,12 +7,15 @@ padding-bottom: 2em; } -.ai-artifact__panel { - display: none; +.ai-artifact__click-to-run { + display: flex; + justify-content: center; + align-items: center; + height: 100%; } -.ai-artifact__expand-button { - //transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); +.ai-artifact__panel { + display: none; } html.ai-artifact-expanded { diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 81fd897ae..7b56ad61a 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -402,6 +402,7 @@ en: ai_artifact: expand_view_label: "Expand view" collapse_view_label: "Exit Fullscreen (ESC)" + click_to_run_label: "Run Artifact" ai_bot: pm_warning: "AI chatbot messages are monitored regularly by moderators." diff --git a/config/settings.yml b/config/settings.yml index 775881adc..2e004d568 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -3,6 +3,7 @@ discourse_ai: default: true client: true ai_artifact_security: + client: true type: enum default: "strict" choices: From 714e5b0c2d4933119750583d80c1c37cb1d21739 Mon Sep 17 00:00:00 2001 From: Sam Saffron Date: Mon, 18 Nov 2024 16:48:31 +1100 Subject: [PATCH 21/29] refresh model presets --- config/locales/client.en.yml | 7 +++++-- lib/completions/llm.rb | 19 +++++++++++++++++-- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 7b56ad61a..6d5bb7217 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -279,14 +279,17 @@ en: model_description: none: "General settings that work for most language models" anthropic-claude-3-5-sonnet: "Anthropic's most intelligent model" + anthropic-claude-3-5-haiku: "Fast and cost-effective" anthropic-claude-3-opus: "Excels at writing and complex tasks" - anthropic-claude-3-sonnet: "Balance of speed and intelligence" - anthropic-claude-3-haiku: "Fast and cost-effective" google-gemini-1-5-pro: "Mid-sized multimodal model capable of a wide range of tasks" google-gemini-1-5-flash: "Lightweight, fast, and cost-efficient with multimodal reasoning" open_ai-gpt-4-turbo: "Previous generation high-intelligence model" open_ai-gpt-4o: "High intelligence model for complex, multi-step tasks" open_ai-gpt-4o-mini: "Affordable and fast small model for lightweight tasks" + open_ai-o1-mini: "Cost-efficient reasoning model" + open_ai-o1-preview: "Open AI's most capabale reasoning model" + samba_nova-Meta-Llama-3-1-8B-Instruct: "Efficient lightweight multilingual model" + samba_nova-Meta-Llama-3-1-70B-Instruct": "Powerful multipurpose model" configured: title: "Configured LLMs" diff --git a/lib/completions/llm.rb b/lib/completions/llm.rb index dc336bf24..0917594ac 100644 --- a/lib/completions/llm.rb +++ b/lib/completions/llm.rb @@ -31,9 +31,12 @@ def presets tokens: 200_000, display_name: "Claude 3.5 Sonnet", }, + { + name: "claude-3-5-haiku", + tokens: 200_000, + display_name: "Claude 3.5 Haiku", + }, { name: "claude-3-opus", tokens: 200_000, display_name: "Claude 3 Opus" }, - { name: "claude-3-sonnet", tokens: 200_000, display_name: "Claude 3 Sonnet" }, - { name: "claude-3-haiku", tokens: 200_000, display_name: "Claude 3 Haiku" }, ], tokenizer: DiscourseAi::Tokenizer::AnthropicTokenizer, endpoint: "https://api.anthropic.com/v1/messages", @@ -63,6 +66,8 @@ def presets { id: "open_ai", models: [ + { name: "o1-preview", tokens: 131_072, display_name: "o1" }, + { name: "o1-mini", tokens: 131_072, display_name: "o1 mini" }, { name: "gpt-4o", tokens: 131_072, display_name: "GPT-4 Omni" }, { name: "gpt-4o-mini", tokens: 131_072, display_name: "GPT-4 Omni Mini" }, { name: "gpt-4-turbo", tokens: 131_072, display_name: "GPT-4 Turbo" }, @@ -71,6 +76,16 @@ def presets endpoint: "https://api.openai.com/v1/chat/completions", provider: "open_ai", }, + { + id: "samba_nova", + models: [ + { name: "Meta-Llama-3.1-8B-Instruct", tokens: 16384, display_name: "Llama 3.1 8B" }, + { name: "Meta-Llama-3.1-70B-Instruct", tokens: 65536, display_name: "Llama 3.1 70B" }, + ], + tokenizer: DiscourseAi::Tokenizer::Llama3Tokenizer, + endpoint: "https://api.sambanova.ai/v1/chat/completions", + provider: "samba_nova", + } ] end end From a64b0873115ed83c9de367110d5b0c1ddfd86dd2 Mon Sep 17 00:00:00 2001 From: Sam Saffron Date: Mon, 18 Nov 2024 16:51:40 +1100 Subject: [PATCH 22/29] lint --- lib/completions/llm.rb | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/lib/completions/llm.rb b/lib/completions/llm.rb index 0917594ac..95b94ad17 100644 --- a/lib/completions/llm.rb +++ b/lib/completions/llm.rb @@ -31,11 +31,7 @@ def presets tokens: 200_000, display_name: "Claude 3.5 Sonnet", }, - { - name: "claude-3-5-haiku", - tokens: 200_000, - display_name: "Claude 3.5 Haiku", - }, + { name: "claude-3-5-haiku", tokens: 200_000, display_name: "Claude 3.5 Haiku" }, { name: "claude-3-opus", tokens: 200_000, display_name: "Claude 3 Opus" }, ], tokenizer: DiscourseAi::Tokenizer::AnthropicTokenizer, @@ -79,13 +75,21 @@ def presets { id: "samba_nova", models: [ - { name: "Meta-Llama-3.1-8B-Instruct", tokens: 16384, display_name: "Llama 3.1 8B" }, - { name: "Meta-Llama-3.1-70B-Instruct", tokens: 65536, display_name: "Llama 3.1 70B" }, + { + name: "Meta-Llama-3.1-8B-Instruct", + tokens: 16_384, + display_name: "Llama 3.1 8B", + }, + { + name: "Meta-Llama-3.1-70B-Instruct", + tokens: 65_536, + display_name: "Llama 3.1 70B", + }, ], tokenizer: DiscourseAi::Tokenizer::Llama3Tokenizer, endpoint: "https://api.sambanova.ai/v1/chat/completions", provider: "samba_nova", - } + }, ] end end From f5befb5a1158985641034b44011b48c2cc4e1b8e Mon Sep 17 00:00:00 2001 From: Sam Saffron Date: Mon, 18 Nov 2024 17:17:57 +1100 Subject: [PATCH 23/29] We need to ship a persona or people will not understand this artifact stuff at all. --- config/locales/server.en.yml | 3 ++ lib/ai_bot/personas/persona.rb | 3 +- lib/ai_bot/personas/web_artifact_creator.rb | 56 +++++++++++++++++++++ 3 files changed, 61 insertions(+), 1 deletion(-) create mode 100644 lib/ai_bot/personas/web_artifact_creator.rb diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 12d20fdbd..ebe172a32 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -200,6 +200,9 @@ en: discourse_helper: name: "Discourse Helper" description: "AI Bot specialized in helping with Discourse related tasks" + web_artifact_creator: + name: "Web Artifact Creator" + description: "AI Bot specialized in creating interactive web artifacts" topic_not_found: "Summary unavailable, topic not found!" summarizing: "Summarizing topic" searching: "Searching for: '%{query}'" diff --git a/lib/ai_bot/personas/persona.rb b/lib/ai_bot/personas/persona.rb index f2fe68553..63d566eac 100644 --- a/lib/ai_bot/personas/persona.rb +++ b/lib/ai_bot/personas/persona.rb @@ -44,6 +44,7 @@ def system_personas Personas::DallE3 => -7, Personas::DiscourseHelper => -8, Personas::GithubHelper => -9, + Personas::WebArtifactCreator => -10, } end @@ -96,9 +97,9 @@ def all_available_tools Tools::GithubSearchFiles, Tools::WebBrowser, Tools::JavascriptEvaluator, - Tools::CreateArtifact, ] + tools << Tools::CreateArtifact if SiteSetting.ai_artifact_security.in?(%w[lax strict]) tools << Tools::GithubSearchCode if SiteSetting.ai_bot_github_access_token.present? tools << Tools::ListTags if SiteSetting.tagging_enabled diff --git a/lib/ai_bot/personas/web_artifact_creator.rb b/lib/ai_bot/personas/web_artifact_creator.rb new file mode 100644 index 000000000..d309d67ce --- /dev/null +++ b/lib/ai_bot/personas/web_artifact_creator.rb @@ -0,0 +1,56 @@ +#frozen_string_literal: true + +module DiscourseAi + module AiBot + module Personas + class WebArtifactCreator < Persona + def tools + [Tools::CreateArtifact] + end + + def required_tools + [Tools::CreateArtifact] + end + + def system_prompt + <<~PROMPT + You are the Web Creator, an AI assistant specializing in building interactive web components. You create engaging and functional web experiences using HTML, CSS, and JavaScript. You live in a Discourse PM and communicate using Markdown. + + Core Principles: + - Create delightful, interactive experiences + - Focus on visual appeal and smooth animations + - Write clean, efficient code + - Build progressively (HTML structure → CSS styling → JavaScript interactivity) + - Keep components focused and purposeful + + When creating: + 1. Understand the desired user experience + 2. Break down complex interactions into simple components + 3. Use semantic HTML for strong foundations + 4. Style thoughtfully with CSS + 5. Add JavaScript for rich interactivity + 6. Consider responsive design + + Best Practices: + - Leverage native HTML elements for better functionality + - Use CSS transforms and transitions for smooth animations + - Keep JavaScript modular and event-driven + - Make content responsive and adaptive + - Create self-contained components + + When responding: + 1. Ask clarifying questions if the request is ambiguous + 2. Briefly explain your approach + 3. Build features iteratively + 4. Describe the interactive elements + 5. Test your solution conceptually + + Your goal is to transform ideas into engaging web experiences. Be creative and practical, focusing on making interfaces that are both beautiful and functional. + + Remember: Great components combine structure (HTML), presentation (CSS), and behavior (JavaScript) to create memorable user experiences. + PROMPT + end + end + end + end +end From c806ab6cd1d99a6a4f778523ed133d561c39e332 Mon Sep 17 00:00:00 2001 From: Sam Saffron Date: Mon, 18 Nov 2024 17:28:03 +1100 Subject: [PATCH 24/29] remove unused text --- .../modules/ai_bot/personas/persona_spec.rb | 25 ++----------------- 1 file changed, 2 insertions(+), 23 deletions(-) diff --git a/spec/lib/modules/ai_bot/personas/persona_spec.rb b/spec/lib/modules/ai_bot/personas/persona_spec.rb index a77fbbf0e..77307d330 100644 --- a/spec/lib/modules/ai_bot/personas/persona_spec.rb +++ b/spec/lib/modules/ai_bot/personas/persona_spec.rb @@ -96,29 +96,6 @@ def system_prompt end it "enforces enums" do - xml = <<~XML - - - search - call_JtYQMful5QKqw97XFsHzPweB - - "3.2" - cow - bar - - - - search - call_JtYQMful5QKqw97XFsHzPweB - - "3.2" - open - bar - - - - XML - tool_call = DiscourseAi::Completions::ToolCall.new( name: "search", @@ -270,12 +247,14 @@ def system_prompt DiscourseAi::AiBot::Personas::Researcher, DiscourseAi::AiBot::Personas::SettingsExplorer, DiscourseAi::AiBot::Personas::SqlHelper, + DiscourseAi::AiBot::Personas::WebArtifactCreator, ], ) # omits personas if key is missing SiteSetting.ai_stability_api_key = "" SiteSetting.ai_google_custom_search_api_key = "" + SiteSetting.ai_artifact_security = "disabled" expect(DiscourseAi::AiBot::Personas::Persona.all(user: user)).to contain_exactly( DiscourseAi::AiBot::Personas::General, From f2c879afa82c8c65473b4ec56ad6d64ce12fd746 Mon Sep 17 00:00:00 2001 From: Sam Saffron Date: Mon, 18 Nov 2024 17:43:37 +1100 Subject: [PATCH 25/29] fix o1 xml tool support --- lib/completions/endpoints/open_ai.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/completions/endpoints/open_ai.rb b/lib/completions/endpoints/open_ai.rb index ef4f0b701..dccfe0b67 100644 --- a/lib/completions/endpoints/open_ai.rb +++ b/lib/completions/endpoints/open_ai.rb @@ -41,7 +41,9 @@ def perform_completion!( # we need to disable streaming and simulate it blk.call "", lambda { |*| } response = super(dialect, user, model_params, feature_name: feature_name, &nil) - blk.call response, lambda { |*| } + + response = [response] if !response.is_a?(Array) + response.each { |item| blk.call item, lambda { |*| } } else super end From 665fec23ae5e184ec6c4e90ed3d9d61af1408e37 Mon Sep 17 00:00:00 2001 From: Sam Saffron Date: Mon, 18 Nov 2024 17:58:40 +1100 Subject: [PATCH 26/29] fix spec --- spec/system/llms/ai_llm_spec.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spec/system/llms/ai_llm_spec.rb b/spec/system/llms/ai_llm_spec.rb index ec5f40039..12a2ac62b 100644 --- a/spec/system/llms/ai_llm_spec.rb +++ b/spec/system/llms/ai_llm_spec.rb @@ -11,7 +11,7 @@ it "correctly sets defaults" do visit "/admin/plugins/discourse-ai/ai-llms" - find("[data-llm-id='anthropic-claude-3-haiku'] button").click() + find("[data-llm-id='anthropic-claude-3-5-haiku'] button").click() find("input.ai-llm-editor__api-key").fill_in(with: "abcd") find(".ai-llm-editor__enabled-chat-bot input").click find(".ai-llm-editor__save").click() @@ -23,9 +23,9 @@ preset = DiscourseAi::Completions::Llm.presets.find { |p| p[:id] == "anthropic" } - model_preset = preset[:models].find { |m| m[:name] == "claude-3-haiku" } + model_preset = preset[:models].find { |m| m[:name] == "claude-3-5-haiku" } - expect(llm.name).to eq("claude-3-haiku") + expect(llm.name).to eq("claude-3-5-haiku") expect(llm.url).to eq(preset[:endpoint]) expect(llm.tokenizer).to eq(preset[:tokenizer].to_s) expect(llm.max_prompt_tokens.to_i).to eq(model_preset[:tokens]) From e933f388e1aec051c97eeb73e36393f8d8bc48f2 Mon Sep 17 00:00:00 2001 From: Sam Saffron Date: Tue, 19 Nov 2024 08:43:23 +1100 Subject: [PATCH 27/29] handle pr comment and restrict artifacts to staff by default --- app/models/ai_artifact.rb | 3 +++ db/fixtures/ai_bot/603_bot_ai_personas.rb | 7 ++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/app/models/ai_artifact.rb b/app/models/ai_artifact.rb index b659771ff..c0dbd7d39 100644 --- a/app/models/ai_artifact.rb +++ b/app/models/ai_artifact.rb @@ -3,6 +3,9 @@ class AiArtifact < ActiveRecord::Base belongs_to :user belongs_to :post + validates :html, length: { maximum: 65_535 } + validates :css, length: { maximum: 65_535 } + validates :js, length: { maximum: 65_535 } def self.iframe_for(id) <<~HTML diff --git a/db/fixtures/ai_bot/603_bot_ai_personas.rb b/db/fixtures/ai_bot/603_bot_ai_personas.rb index 4f833e347..90ba535ee 100644 --- a/db/fixtures/ai_bot/603_bot_ai_personas.rb +++ b/db/fixtures/ai_bot/603_bot_ai_personas.rb @@ -5,7 +5,12 @@ if !persona persona = AiPersona.new persona.id = id - persona.allowed_group_ids = [Group::AUTO_GROUPS[:trust_level_0]] + if persona_class == DiscourseAi::AiBot::Personas::WebArtifactCreator + # this is somewhat sensitive, so we default it to staff + persona.allowed_group_ids = [Group::AUTO_GROUPS[:staff]] + else + persona.allowed_group_ids = [Group::AUTO_GROUPS[:trust_level_0]] + end persona.enabled = true persona.priority = true if persona_class == DiscourseAi::AiBot::Personas::General end From ca6b87af9990816891e8b382ce576ec7ca35f19d Mon Sep 17 00:00:00 2001 From: Sam Saffron Date: Tue, 19 Nov 2024 08:52:08 +1100 Subject: [PATCH 28/29] o1 and o1 mini support streaming now, remove hack --- lib/completions/endpoints/open_ai.rb | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/lib/completions/endpoints/open_ai.rb b/lib/completions/endpoints/open_ai.rb index dccfe0b67..94d29e131 100644 --- a/lib/completions/endpoints/open_ai.rb +++ b/lib/completions/endpoints/open_ai.rb @@ -27,28 +27,6 @@ def provider_id AiApiAuditLog::Provider::OpenAI end - def perform_completion!( - dialect, - user, - model_params = {}, - feature_name: nil, - feature_context: nil, - partial_tool_calls: false, - &blk - ) - @disable_native_tools = dialect.disable_native_tools? - if dialect.respond_to?(:is_gpt_o?) && dialect.is_gpt_o? && block_given? - # we need to disable streaming and simulate it - blk.call "", lambda { |*| } - response = super(dialect, user, model_params, feature_name: feature_name, &nil) - - response = [response] if !response.is_a?(Array) - response.each { |item| blk.call item, lambda { |*| } } - else - super - end - end - private def model_uri From 424091d38389421c4e758e189a9bb993e5d7c410 Mon Sep 17 00:00:00 2001 From: Sam Saffron Date: Tue, 19 Nov 2024 09:02:25 +1100 Subject: [PATCH 29/29] fix specs --- lib/completions/endpoints/open_ai.rb | 13 +++++++++++++ .../lib/modules/ai_bot/personas/persona_spec.rb | 17 ++++++++++++++++- 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/lib/completions/endpoints/open_ai.rb b/lib/completions/endpoints/open_ai.rb index 94d29e131..46382732c 100644 --- a/lib/completions/endpoints/open_ai.rb +++ b/lib/completions/endpoints/open_ai.rb @@ -27,6 +27,19 @@ def provider_id AiApiAuditLog::Provider::OpenAI end + def perform_completion!( + dialect, + user, + model_params = {}, + feature_name: nil, + feature_context: nil, + partial_tool_calls: false, + &blk + ) + @disable_native_tools = dialect.disable_native_tools? + super + end + private def model_uri diff --git a/spec/lib/modules/ai_bot/personas/persona_spec.rb b/spec/lib/modules/ai_bot/personas/persona_spec.rb index 77307d330..34311761e 100644 --- a/spec/lib/modules/ai_bot/personas/persona_spec.rb +++ b/spec/lib/modules/ai_bot/personas/persona_spec.rb @@ -46,6 +46,7 @@ def system_prompt } end + fab!(:admin) fab!(:user) fab!(:upload) @@ -238,6 +239,20 @@ def system_prompt # should be ordered by priority and then alpha expect(DiscourseAi::AiBot::Personas::Persona.all(user: user)).to eq( + [ + DiscourseAi::AiBot::Personas::General, + DiscourseAi::AiBot::Personas::Artist, + DiscourseAi::AiBot::Personas::Creative, + DiscourseAi::AiBot::Personas::DiscourseHelper, + DiscourseAi::AiBot::Personas::GithubHelper, + DiscourseAi::AiBot::Personas::Researcher, + DiscourseAi::AiBot::Personas::SettingsExplorer, + DiscourseAi::AiBot::Personas::SqlHelper, + ], + ) + + # it should allow staff access to WebArtifactCreator + expect(DiscourseAi::AiBot::Personas::Persona.all(user: admin)).to eq( [ DiscourseAi::AiBot::Personas::General, DiscourseAi::AiBot::Personas::Artist, @@ -256,7 +271,7 @@ def system_prompt SiteSetting.ai_google_custom_search_api_key = "" SiteSetting.ai_artifact_security = "disabled" - expect(DiscourseAi::AiBot::Personas::Persona.all(user: user)).to contain_exactly( + expect(DiscourseAi::AiBot::Personas::Persona.all(user: admin)).to contain_exactly( DiscourseAi::AiBot::Personas::General, DiscourseAi::AiBot::Personas::SqlHelper, DiscourseAi::AiBot::Personas::SettingsExplorer,