diff --git a/app/controllers/commands_controller.rb b/app/controllers/commands_controller.rb index 679e1219b..90ae51fcd 100644 --- a/app/controllers/commands_controller.rb +++ b/app/controllers/commands_controller.rb @@ -42,6 +42,8 @@ class CommandsController < ApplicationController redirect_to result.url when Command::Result::ChatResponse respond_with_chat_response(result) + when Command::Result::InsightResponse + respond_with_insight_response(result) else redirect_back_or_to root_path end @@ -51,8 +53,8 @@ class CommandsController < ApplicationController command = chat_response_to_command(result) if confirmed?(command) - command.execute - redirect_back_or_to root_path + result = command.execute + respond_with_execution_result result else respond_with_needs_confirmation(command.commands, redirect_to: result.context_url) end @@ -67,4 +69,8 @@ class CommandsController < ApplicationController parser = Command::Parser.new(context) Command::Composite.new(chat_response.command_lines.collect { parser.parse it }) end + + def respond_with_insight_response(chat_response) + render json: { message: chat_response.content }, status: :accepted + end end diff --git a/app/javascript/controllers/terminal_controller.js b/app/javascript/controllers/terminal_controller.js index ce40fdde1..4b2ce0f56 100644 --- a/app/javascript/controllers/terminal_controller.js +++ b/app/javascript/controllers/terminal_controller.js @@ -44,10 +44,16 @@ export default class extends Controller { } handleCommandResponse(event) { + const response = event.detail.fetchResponse?.response + if (event.detail.success) { + if (response.headers.get("Content-Type")?.includes("application/json")) { + response.json().then((responseJson) => { + this.element.querySelector("#chat-insight").innerHTML = marked.parse(responseJson.message) + }) + } this.#reset() - } else { - const response = event.detail.fetchResponse.response + } else if (response) { this.#handleErrorResponse(response) } } @@ -88,7 +94,6 @@ export default class extends Controller { } #reset(inputValue = "") { - console.debug("RESET!") this.inputTarget.value = inputValue this.confirmationTarget.value = "" this.waitingForConfirmationValue = false @@ -104,7 +109,6 @@ export default class extends Controller { async #requestConfirmation(jsonResponse) { const message = jsonResponse.commands[0] - console.debug("request confirmation", this.inputTarget.value); this.originalInputValue = this.inputTarget.value diff --git a/app/models/command/chat_query.rb b/app/models/command/chat_query.rb index 0b47cea67..84f604b6a 100644 --- a/app/models/command/chat_query.rb +++ b/app/models/command/chat_query.rb @@ -7,7 +7,9 @@ class Command::ChatQuery < Command def execute response = chat.ask query - generated_commands = replace_names_with_ids(JSON.parse(response.content)) + generated_commands = replace_names_with_ids(JSON.parse(response.content)).tap do |commands| + Rails.logger.info "*** #{commands}" + end build_chat_response_with generated_commands end @@ -21,7 +23,7 @@ class Command::ChatQuery < Command # - Don't generate initial /search if not requested. "Assign to JZ" should def prompt <<~PROMPT - You are a helpful assistant that translates natural language into commands that Fizzy understand. + You are a helpful assistant that translates natural language into one or more commands that Fizzy understand. Fizzy supports the following commands: @@ -30,7 +32,8 @@ class Command::ChatQuery < Command - Tag cards: /tag [tag-name]. E.g: "/tag performance" - Get insight about cards: /insight [query]. Use this as the default command to satisfy questions and requests about cards. This relies on /search. Example: "/insight summarize performance issues". - - Search cards based on certain keywords: /search. See how this works below. E.g: "/search meetup montreal" + - Search cards based on certain keywords: /search. See how this works below. E.g: "/search meetup montreal". IMPORTANT: There can + only be one /search command in the response. If you need to filter a certain set of cards, you can use the /search command to filter. @@ -56,14 +59,26 @@ class Command::ChatQuery < Command { command: "/search", indexed_by: "closed", collection_ids: [ "Writebook", "Design" ] } - Notice that there are overlapping commands (filter by assignee or assign cards). Favor filtering/queries for - commands like "cards assigned to someone". + Notice that there are similar commands to filter and act on cards (e.g: filter by assignee or assign cards). Favor + filtering/queries for commands like "cards assigned to someone". For example, to assign a card, you invoke `assign kevin`. For insight about "something", you invoke "/insight something". - Important: When using the /insight command, ALWAYS add first a /search command that filters out the relevant cards to answer + Important: + + - When using the /insight command, consider adding first a /search command that filters out the relevant cards to answer the question. If there are relevant keywords to filter, pass those to /search but avoid passing generic ones. Then, reformulate pass the query itself VERBATIM to /insight as in "/insight ", no additional keys in the JSON. + - Only add an /insight command is there is a specific question about the data. Some requests are just about searching some + cards. Those are fine. + - Sometimes, the current context is a given card or the set of cards in the screen. In that case, there is no need to add a + /search command. Assume that's the case if it's missing any description of the the set of cards required. E.g: just "summarise" + or "this card". + - A response can only contain ONE /search command AT MOST. + - A response can only contain ONE /insight command AT MOST. + - Unless asking for explicit filtering, always prefer /insight over /search. When asking about "cards" with properties that can + be satisfied with /search, then use /search. + For example, for "summarize performance issues", the JSON could be: @@ -77,10 +92,6 @@ class Command::ChatQuery < Command } ] - Unless asking for explicit filtering, always prefer /insight over /search. When asking about "cards" with properties that can - be satisfied with /search, then use /search. - - A response can contain at most one /search command. Please combine commands to satisfy what the user needs. E.g: search with keywords and filters and then apply as many commands as needed. Make sure you don't leave actions mentioned in the query needs unattended.' @@ -89,6 +100,8 @@ class Command::ChatQuery < Command /assign don't support additional JSON keys, they will only contain the "command:" key". For /search, it can contain additional JSON keys matching the /search params described above. + Don't generate any /search without params. Just don't add it. + Avoid empty preambles like "Based on the provided cards". Also, prefer a natural a friendly language favoring active voice. Make sure to place into double quotes the strings in JSON values and that you generate valid JSON. I want a @@ -122,11 +135,17 @@ class Command::ChatQuery < Command end def response_command_lines_from(generated_commands) - generated_commands.filter { it["command"] != "/search" }.collect { it["command"] } + # We translate standalone /search commands as redirections to execute. Otherwise, they + # will be excluded out from the commands to run, as they represent the context url. + if generated_commands.size == 1 + [ "/visit #{cards_path(**generated_commands.first.without("command"))}" ] + else + generated_commands.filter { it["command"] != "/search" }.collect { it["command"] } + end end def response_context_url_from(generated_commands) - if search_command = generated_commands.find { it["command"] == "/search" } + if generated_commands.size > 1 && search_command = generated_commands.find { it["command"] == "/search" } cards_path(**search_command.without("command")) end end diff --git a/app/models/command/composite.rb b/app/models/command/composite.rb index 237e5b636..da79e2280 100644 --- a/app/models/command/composite.rb +++ b/app/models/command/composite.rb @@ -11,9 +11,11 @@ class Command::Composite end def execute + result = nil ApplicationRecord.transaction do - commands.each(&:execute) + commands.each { result = it.execute } end + result end def undo diff --git a/app/models/command/get_insight.rb b/app/models/command/get_insight.rb index c43380b9c..3cf3dd6d8 100644 --- a/app/models/command/get_insight.rb +++ b/app/models/command/get_insight.rb @@ -9,7 +9,7 @@ class Command::GetInsight < Command def execute response = chat.ask query - Command::Result::InsightResponse.new({ reply: response.content }) + Command::Result::InsightResponse.new(response.content) end def undoable? @@ -59,6 +59,7 @@ class Command::GetInsight < Command def card_context_for(card) <<~CONTEXT + ==CARD== Title: #{card.title} Card created by: #{card.creator.name}} Id: #{card.id} @@ -73,7 +74,7 @@ class Command::GetInsight < Command def comment_context_for(comment) <<~CONTEXT - Card: #{comment.card.id} + ==COMMENT== Id: #{comment.id} Content: #{comment.body.to_plain_text}} Comment created by: #{comment.creator.name}} diff --git a/app/models/command/parser.rb b/app/models/command/parser.rb index 3a69418f3..eefa7a507 100644 --- a/app/models/command/parser.rb +++ b/app/models/command/parser.rb @@ -31,6 +31,8 @@ class Command::Parser Command::ClearFilters.new(params: filter.as_params) when "/close" Command::Close.new(card_ids: cards.ids, reason: command_arguments.join(" ")) + when "/visit" + Command::VisitUrl.new(url: command_arguments.first) when "/tag" Command::Tag.new(tag_title: tag_title_from(command_arguments.join(" ")), card_ids: cards.ids) else diff --git a/app/models/command/visit_url.rb b/app/models/command/visit_url.rb new file mode 100644 index 000000000..6066ea0ed --- /dev/null +++ b/app/models/command/visit_url.rb @@ -0,0 +1,11 @@ +class Command::VisitUrl < Command + store_accessor :data, :url + + def title + "Visit #{url}" + end + + def execute + redirect_to url + end +end