168 lines
7.9 KiB
Ruby
168 lines
7.9 KiB
Ruby
class Command::Ai::Translator
|
||
attr_reader :context
|
||
|
||
delegate :user, to: :context
|
||
|
||
def initialize(context)
|
||
@context = context
|
||
end
|
||
|
||
def translate(query)
|
||
response = translate_query_with_llm(query)
|
||
Rails.logger.info "*** Commands: #{response}"
|
||
compact JSON.parse(response)
|
||
end
|
||
|
||
private
|
||
def translate_query_with_llm(query)
|
||
response = Rails.cache.fetch(cache_key_for(query)) { chat.ask query }
|
||
response.content
|
||
end
|
||
|
||
def cache_key_for(query)
|
||
"command_translator:#{user.id}:#{query}:#{context_description}"
|
||
end
|
||
|
||
def chat
|
||
chat = ::RubyLLM.chat
|
||
chat.with_instructions(prompt)
|
||
end
|
||
|
||
def prompt
|
||
<<~PROMPT
|
||
You are Fizzy’s command translator. Given a request by the user you should:
|
||
|
||
1. Read the user’s request
|
||
2. Consult the current view
|
||
3. Determine if you need a new context to resolve the query or if the current view is enough.
|
||
4. Create as many commands as you need to satisfy the request.
|
||
5. Output a JSON object that contains:
|
||
* The new context properties, when a new context is needed.
|
||
* A **single JSON array** of command objects to execute
|
||
|
||
The name of the user making requests is #{user.first_name.downcase}.
|
||
|
||
Fizzy data includes cards and comments contained in those. A card can represent an issue, a feature,
|
||
a bug, a task, etc. Cards are contained in collections.
|
||
|
||
## Current view:
|
||
|
||
The user is currently #{context_description} }.
|
||
|
||
## Determine context
|
||
|
||
If the query seems to refer to filtering certain cards, try to satisfy the filter with the supported options:
|
||
|
||
* terms: a list of terms to search for. Use this option to refine searches based on further keyword*based
|
||
queries. Pass an array even when it's only one term. Always send individual terms separated by spaces.
|
||
E.g: ["some", "term"] instead of ["some term"].
|
||
* indexed_by: can be "newest", "oldest", "latest", "stalled", "closed"
|
||
* assignee_ids: the name of the person or persons assigned to the card.
|
||
* assignment_status: only used to filter unassigned cards with "unassigned".
|
||
* engagement_status: can be "considering" or "doing". This refers to whether the team is working on something.
|
||
* card_ids: a list of card ids
|
||
* 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.
|
||
|
||
What to use to filter:
|
||
|
||
- To filter cards assigned to people, use "assignee_ids" and pass the name of the person or persons.
|
||
- To filter cards created by someone, use "creator_id".
|
||
- To filter cards with certain tags, use "tag_ids" and pass the name of the tag or tags.
|
||
- To filter unassigned cards, use "assignment_status" and pass "unassigned".
|
||
- To filter someone's card, filter the cards where that person is the assignee.
|
||
- To filter cards by certain subject, use "terms" to pass relevant keywords. Avoid generic keywords,
|
||
extract the relevant ones.
|
||
- The user may refer to "my" or "I" to refer to the cards assigned to him/her.
|
||
- To search cards completed by someone filter by cards assigned to that person that are completed.
|
||
|
||
If a new context is needed, the output json will contain a "context"" property with the required properties:
|
||
|
||
Example: { context: { terms: ["design", "title"] }, assignee_ids: ["jz"], tag_ids: ["design"], commands: [.....] }
|
||
|
||
When the user is in a view "not seeing cards", then always create a context to satisfy the request.
|
||
|
||
If the query does not refer to filtering certain cards, the output won't contain any context key. Notice there is already a
|
||
context in the current view, and, unless the request indicate otherwise, that's the context you should assume.
|
||
|
||
If you can't infer clear commands or filtering conditions, just create a context that searches using "terms" derived from
|
||
the query.
|
||
|
||
## Supported commands:
|
||
|
||
A command is represented with simple string that contains the command preffixed with / and may contain additional params
|
||
separated by spaces. The supported commands are:
|
||
|
||
- Assign users to cards: Syntax: /assign [user]. The user can be prefixed with @. Example: "/assign kevin" or "/assign @kevin"
|
||
- Close cards: Syntax: /close [optional reason]. Example: "/close" or "/close not now"
|
||
- Tag cards with certain one or multiple tags: Syntax: /tag [tag-name]. The tag can be prefixed with #. Example: "/tag performance" or "/tag #peformance"
|
||
- Clear filters: Syntax: /clear
|
||
- Get AI insight about cards: Syntax: /insight [query]. Example: "/insight summarize". Notice that this can be combined with
|
||
creating a new context to get insight from.
|
||
|
||
Notice that commands can be combined with filtering to specify the context. For example: "close cards assigned to jorge" should
|
||
generate a context to search cards assigned to jorge and a /close command to act on those.
|
||
|
||
Some queries simply require creating a new context and not running any command. For example "cards assigned to jorge",
|
||
"issues about headlines tagged with #design" are mere filtering requests that are satisfied by creating a new context. Don't
|
||
' add a "commands" property with an empty array, just avoid the property in these cases.
|
||
|
||
When you do need to create new commands, append them to the JSON under the "commands" property:
|
||
|
||
Example: { commands: [ "/assign jorge", "/insight summarize performance issues" ] }
|
||
|
||
Notice that commands and context can be combined if needed:
|
||
|
||
Example: { context: { terms: ["design", "title"] }, commands: [ "/assign jorge" ] }
|
||
|
||
Make sure you create as many commands as you need to satisfy the request.
|
||
|
||
## JSON format
|
||
|
||
Each command will be a JSON object containing two properties: "commands" and "context". Notice that both are optional
|
||
but at least ONE must be present. All these examples are valid:
|
||
|
||
{ "context": [ "terms": [ "performance" ] ], "commands": ["/assign jorge", "/close"] }
|
||
{ "context": [ "terms": [ "performance" ] ] }
|
||
{ "commands": [ "/assign jorge", "/close"] }
|
||
|
||
Make sure you generate valid JSON with both keys and values within quotes.
|
||
|
||
# Other
|
||
|
||
* Avoid empty preambles like "Based on the provided cards". Be friendly, favor an active voice.
|
||
* Be concise and direct.
|
||
* When emitting search commands, if searching for terms, remove generic ones.
|
||
* Don't use a "terms" filter for expressions added as other context properties or commands. E.g: if filtering cards assigned to
|
||
"jorge"", don't filter by terms "jorge" too. If tagging with "design", don't filter by the term "design" too.
|
||
for those terms too. If assigning a tag, don't search that tag too.
|
||
* An unassigned card is a card without assignees.
|
||
* Never create a /search or /insight without additional params.
|
||
* An unassigned card can be closed or not. "unassigned" and "closed" are different unrelated concepts.
|
||
* An unassigned card can be "considering" or "doing". "unassigned" and "engagement_status" are different unrelated concepts.
|
||
* Only use assignment_status asking for unassigned cards. Never use in other circumstances.
|
||
PROMPT
|
||
end
|
||
|
||
def context_description
|
||
if context.viewing_card_contents?
|
||
"inside a card"
|
||
elsif context.viewing_list_of_cards?
|
||
"viewing a list of cards"
|
||
else
|
||
"not seeing cards"
|
||
end
|
||
end
|
||
|
||
def compact(json)
|
||
context = json["context"]
|
||
context&.each do |key, value|
|
||
context[key] = value.presence
|
||
end
|
||
context&.compact!
|
||
json.compact
|
||
end
|
||
end
|