Move the hack to fetch completed cards to the insight command logic

We need to rework this as part of the filters revamp. "indexed_by" being
a mix of filters keeps biting us.
This commit is contained in:
Jorge Manrubia
2025-07-24 15:52:57 +02:00
parent fc26329daf
commit 5ad2d09eae
13 changed files with 131645 additions and 41 deletions
+14 -11
View File
@@ -57,7 +57,7 @@ class Command::Ai::Translator
"context": { // omit if empty
"terms": string[], // filter cards by keywords
"indexed_by": "newest" | "oldest" | "latest" | "stalled"
| "closed" | "closing_soon" | "falling_back_soon" | "all", // "all" is only possible in combination with indexed_by
| "closed" | "closing_soon" | "falling_back_soon",
"assignee_ids": <person>[],
"assignment_status": "unassigned",
"card_ids": <card_id>[],
@@ -158,6 +158,7 @@ class Command::Ai::Translator
* Answer questions about the data.
* Getting insight about data: how things are progressing, blockers, highlights, etc.
* Perform advanced querying and filtering on the data, not supported by the preset filters.
* Look for cards similar to a given card.
* Summarize information
* Check what a person has done
* Any other question about cards, comments, discussions, persons, etc.
@@ -170,16 +171,14 @@ class Command::Ai::Translator
### Context to get insight
- When answering implies analyzing cards and comments, it always needs a context filter.
* When there is no suitable filter, use `indexed_by` with either `latest` or `all`:
-> Use "latest" when the query needs to consider open cards only.
-> Use "all" when asking about people working on cards.
-> Use "all" when the query needs to consider both open and closed cards.
- When answering implies querying certain cards, it always needs a context filter.
* When there is no suitable filter, use `indexed_by` with `latest`.
- Queries that require analyzing cards, comments, people activity, etc. to extract information, ALWAYS
require a `context` filter.
- If the current context is "inside a card" and the query makes sense in that context, you
- If the current context is "inside a card" and the query doesn't need other cards to be answered, you
can omit context filter properties.
* Inside a card you are seeing the card description and the discussion around it. The query may refer to that context.
* You will still need a filter if the request requires finding other cards. E.g: looking for similar cards.
## Examples
@@ -210,6 +209,8 @@ class Command::Ai::Translator
- 123 → `/search 123` # Notice there is no "card" mention
- package 123 → `/search package 123`
Don't use `card_ids` when passing a card id that serves as context for a /insight command.
#### Filter by terms
When user explicitly asks for cards about some topic, use the `terms` filter with the topic. Consider this
@@ -318,18 +319,20 @@ class Command::Ai::Translator
#### Getting insight
- most commented cards → { context: { indexed_by: "latest" }, commands: ["/insight most commented cards"] }
- very active cards → { context: { indexed_by: "all" }, commands: ["/insight very active cards"] }
- what has mike done → { context: { indexed_by: "all" }, commands: ["/insight what has mike done"] }
- very active cards → { context: { indexed_by: "latest" }, commands: ["/insight very active cards"] }
- what has mike done → { context: { indexed_by: "latest" }, commands: ["/insight what has mike done"] }
- where are the problems → { context: { indexed_by: "latest" }, commands: ["/insight where are the problems"] }
- summarize cards completed by mike → { context: { closer_ids: ["mike"] }, commands: ["/insight summarize"] }
- who is working on the most challenging stuff → { context: { indexed_by: "all" }, commands: ["/insight who is working on the most challenging stuff"] }
- who is working on the most challenging stuff → { context: { indexed_by: "latest" }, commands: ["/insight who is working on the most challenging stuff"] }
### Filters and commands combined
- cards related to infrastructure assigned to mike → { context: { assignee_ids: "mike", terms: ["infrastructure"] } }
- assign john to the current #design cards and tag them with #v2 → { context: { tag_ids: ["design"] }, commands: ["/assign john", "/tag #v2"] }
- close cards assigned to mike and assign them to roger → { context: {assignee_ids: ["mike"]}, commands: ["/close", "/assign roger"] }
- similar cards → { context: { indexed_by: "latest"}, commands: ["/insight similar cards"] }
- summarize the cards assigned to jz → { context: { assignee_ids: ["jz"] }, commands: ["/insight summarize"] }
- summarize the work that ann has done recently → { context: { indexed_by: ["all"] }, commands: ["/insight summarize the work that ann has done recently"] }
- summarize the work that ann has done recently → { context: { indexed_by: "latest" }, commands: ["/insight summarize the work that ann has done recently"] }
PROMPT
end
+36 -3
View File
@@ -1,7 +1,7 @@
class Command::GetInsight < Command
include ::Ai::Prompts, Command::Cards
store_accessor :data, :query
store_accessor :data, :query, :params
def title
"Insight query '#{query}'"
@@ -21,7 +21,7 @@ class Command::GetInsight < Command
end
private
MAX_CARDS = 100
MAX_COMPLETED_CARDS = 50
def chat
chat = RubyLLM.chat(model: "chatgpt-4o-latest")
@@ -40,6 +40,15 @@ class Command::GetInsight < Command
- When asking for summaries, try to highlight key outcomes.
- If you need further details or clarifications, indicate it.
- When referencing cards or comments, always link them (see rules below).
- **NEVER** answer with cards that don't exist.
## Critical rules
- Always assume that the user is querying about information in the system, not asking you to generate similar data.
- Never include cards that don't exist in your answers.
- When asking for similar cards, tickets, bugs, etc., never imagine those, just analyze the data and suggest existing
cards that are similar.
- If you are missing cards or information, indicate it instead of making up a response.
## Linking rules
@@ -48,11 +57,35 @@ class Command::GetInsight < Command
* Don't add these as standalone links, but referencing words from the insight
- Markdown link format: [anchor text](/full/path/).
- Preserve the path exactly as provided (including the leading "/").
- Prefer anchor text links that read naturally over numbers.
- When showing the card title as the link anchor text, also include #<card id> at the end between parentheses.
PROMPT
end
def cards_context
cards.limit(MAX_CARDS).collect(&:to_prompt).join("\n")
promptable_cards.collect(&:to_prompt).join("\n")
end
def promptable_cards
if filter.indexed_by.latest? && context.viewing_list_of_cards?
cards_including_closed_ones
else
cards
end
end
def cards_including_closed_ones
closed_cards = user.accessible_cards.closed.recently_closed_first.limit(MAX_COMPLETED_CARDS)
open_cards = cards
open_sql = open_cards.to_sql
closed_sql = "SELECT * FROM (#{closed_cards.to_sql})" # isolate limit
sql = " SELECT * FROM ( #{open_sql} UNION ALL #{closed_sql} ) AS cards "
Card.from("(#{sql}) AS cards")
end
def filter
user.filters.from_params(params.reverse_merge(**FilterScoped::DEFAULT_PARAMS))
end
end
+1 -1
View File
@@ -56,7 +56,7 @@ class Command::Parser
when "/do"
Command::Do.new(card_ids: cards.ids)
when "/insight"
Command::GetInsight.new(query: combined_arguments, card_ids: cards.ids)
Command::GetInsight.new(query: combined_arguments, card_ids: cards.ids, params: filter.as_params)
when "/add"
Command::AddCard.new(card_title: combined_arguments, collection_id: guess_collection&.id)
when "/search"
+15 -9
View File
@@ -1,6 +1,8 @@
class Command::Parser::Context
attr_reader :user, :url, :script_name
MAX_CARDS = 100
def initialize(user, url:, script_name: "")
@user = user
@url = url
@@ -10,15 +12,7 @@ class Command::Parser::Context
end
def cards
if viewing_card_contents?
user.accessible_cards.where id: params[:id]
elsif viewing_cards_index?
filter.cards.published
elsif viewing_search_results?
user.accessible_cards.where(id: user.search(params[:q]).select(:card_id))
else
Card.none
end
cards_from_current_view.limit(MAX_CARDS)
end
def viewing_card_contents?
@@ -70,6 +64,18 @@ class Command::Parser::Context
private
attr_reader :controller, :action, :params
def cards_from_current_view
if viewing_card_contents?
user.accessible_cards.where id: params[:id]
elsif viewing_cards_index?
filter.cards.published
elsif viewing_search_results?
user.accessible_cards.where(id: user.search(params[:q]).select(:card_id))
else
Card.none
end
end
def viewing_card_perma?
controller == "cards" && action == "show"
end