134 lines
5.8 KiB
Ruby
134 lines
5.8 KiB
Ruby
class Command::ChatQuery < Command
|
|
store_accessor :data, :query, :params
|
|
|
|
def title
|
|
"Chat query '#{query}'"
|
|
end
|
|
|
|
def execute
|
|
response = chat.ask query
|
|
generated_commands = replace_names_with_ids(JSON.parse(response.content))
|
|
build_chat_response_with generated_commands
|
|
end
|
|
|
|
private
|
|
def chat
|
|
chat = RubyLLM.chat
|
|
chat.with_instructions(prompt)
|
|
end
|
|
|
|
# TODO:
|
|
# - 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.
|
|
|
|
Fizzy supports the following commands:
|
|
|
|
- Assign users to cards: /assign [user]. E.g: "/assign kevin"
|
|
- Close cards: /close [optional reason]. E.g: "/close" or "/close not now"
|
|
- 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"
|
|
|
|
If you need to filter a certain set of cards, you can use the /search command to filter.
|
|
|
|
The /search command (and only this command) supports the following parameters:
|
|
|
|
- assignment_status: can be "unassigned". Only include if asking for unassigned cards explicitly
|
|
- indexed_by: can be "newest", "oldest", "latest", "stalled", "closed"
|
|
- engagement_status: can be "considering" or "doing"
|
|
- card_ids: a list of card ids
|
|
- assignee_ids: a list of assignee names
|
|
- creator_id: the name of a person
|
|
- collection_ids: a list of collection names. Cards are contained in collections. Don't use unless mentioning
|
|
specific collections.
|
|
- tag_ids: a list of tag names.
|
|
- terms: a list of terms to search for. Use this option to refine searches based on further keyword-based
|
|
queries.
|
|
|
|
So each command will be a JSON object like:
|
|
|
|
{ command: "/close" }
|
|
|
|
The /search command can also contain additional params:
|
|
|
|
{ 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".
|
|
|
|
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
|
|
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 <original query>", no additional keys in the JSON.
|
|
|
|
For example, for "summarize performance issues", the JSON could be:
|
|
|
|
[
|
|
{
|
|
"command": "/search",
|
|
"terms": ["performance"]
|
|
},
|
|
{
|
|
"command": "/insight summarize performance issues"
|
|
}
|
|
]
|
|
|
|
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.'
|
|
|
|
The output will be in JSON. It will contain a list of commands. The commands /tag, /close, /search, /insight and
|
|
/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.
|
|
|
|
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
|
|
JSON list like [{}, {}...]
|
|
|
|
Respond only with the JSON.
|
|
PROMPT
|
|
end
|
|
|
|
def replace_names_with_ids(commands)
|
|
commands.each do |command|
|
|
if command["command"] == "/search"
|
|
command["assignee_ids"] = command["assignee_ids"]&.filter_map { |name| assignee_from(name)&.id }
|
|
command["creator_id"] = assignee_from(command["creator_id"])&.id if command["creator_id"]
|
|
command["collection_ids"] = command["collection_ids"]&.filter_map { |name| Collection.where("lower(name) = ?", name.downcase).first&.id }
|
|
command["tag_ids"] = command["tag_ids"]&.filter_map { |name| ::Tag.find_by_title(name)&.id }
|
|
command.compact!
|
|
end
|
|
end
|
|
end
|
|
|
|
def assignee_from(string)
|
|
string_without_at = string.delete_prefix("@")
|
|
User.all.find { |user| user.mentionable_handles.include?(string_without_at) }
|
|
end
|
|
|
|
def build_chat_response_with(generated_commands)
|
|
Command::Result::ChatResponse.new \
|
|
command_lines: response_command_lines_from(generated_commands),
|
|
context_url: response_context_url_from(generated_commands)
|
|
end
|
|
|
|
def response_command_lines_from(generated_commands)
|
|
generated_commands.filter { it["command"] != "/search" }.collect { it["command"] }
|
|
end
|
|
|
|
def response_context_url_from(generated_commands)
|
|
if search_command = generated_commands.find { it["command"] == "/search" }
|
|
cards_path(**search_command.without("command"))
|
|
end
|
|
end
|
|
end
|