diff --git a/app/models/ai/tool.rb b/app/models/ai/tool.rb index 995393dc2..8c11d3ac6 100644 --- a/app/models/ai/tool.rb +++ b/app/models/ai/tool.rb @@ -3,6 +3,10 @@ class Ai::Tool < RubyLLM::Tool private def default_url_options - Rails.application.default_url_options + Rails.application.default_url_options.merge( + script_name: "/#{ApplicationRecord.current_tenant}", + host: "fizzy.localhost", + port: 3006 + ) end end diff --git a/app/models/ai/tool/list_cards.rb b/app/models/ai/tool/list_cards.rb index 9dc81984c..c5944b5ca 100644 --- a/app/models/ai/tool/list_cards.rb +++ b/app/models/ai/tool/list_cards.rb @@ -15,7 +15,7 @@ class Ai::Tool::ListCards < Ai::Tool } } ``` - Each collection object has the following fields: + Each card object has the following fields: - id [Integer, not null] - title [String, not null] - The title of the card - status [String, not null] - Enum of "creating", "draft" and "published" @@ -69,8 +69,14 @@ class Ai::Tool::ListCards < Ai::Tool desc: "If provided, will return only card that were last active on or before the given ISO timestamp", required: false + attr_reader :user + + def initialize(user:) + @user = user + end + def execute(**params) - cards = Card.all.includes(:stage, :creator, :assignees, :goldness) + cards = Card.where(collection: user.collections).includes(:stage, :creator, :assignees, :goldness) cards = cards.search(params[:query]) if params[:query].present? cards = cards.golden if params[:golden].present? @@ -99,6 +105,12 @@ class Ai::Tool::ListCards < Ai::Tool page = GearedPagination::Recordset.new(cards, ordered_by: { id: :desc }).page(params[:page]) + puts "="*80 + puts "Account: #{Account.sole.id}" + puts "Tenant: #{ApplicationRecord.current_tenant}" + puts "URL options: #{default_url_options.inspect}" + puts "="*80 + { cards: page.records.map do |card| { @@ -112,7 +124,7 @@ class Ai::Tool::ListCards < Ai::Tool creator: card.creator.as_json(only: [ :id, :name ]), assignees: card.assignees.as_json(only: [ :id, :name ]), description: card.description.to_plain_text.truncate(1000), - url: collection_card_path(card.collection, card) + url: collection_card_url(card.collection, card) } end, pagination: { diff --git a/app/models/ai/tool/list_collections.rb b/app/models/ai/tool/list_collections.rb index 14fc8e647..18cb68696 100644 --- a/app/models/ai/tool/list_collections.rb +++ b/app/models/ai/tool/list_collections.rb @@ -24,8 +24,14 @@ class Ai::Tool::ListCollections < Ai::Tool desc: "Which page to return. Leave balnk to get the first page", required: false + attr_reader :user + + def initialize(user:) + @user = user + end + def execute(**params) - scope = Collection.all + scope = user.collections page = GearedPagination::Recordset.new( scope, @@ -37,7 +43,7 @@ class Ai::Tool::ListCollections < Ai::Tool { id: collection.id, name: collection.name, - url: collection_path(collection) + url: collection_url(collection) } end, pagination: { diff --git a/app/models/ai/tool/list_comments.rb b/app/models/ai/tool/list_comments.rb index 28bcd89b0..47a7696c0 100644 --- a/app/models/ai/tool/list_comments.rb +++ b/app/models/ai/tool/list_comments.rb @@ -5,7 +5,7 @@ class Ai::Tool::ListComments < Ai::Tool Responses are JSON objects that look like this: ``` { - "collections": [ + "comments": [ { "id": 3, "card_id": 5, @@ -21,9 +21,18 @@ class Ai::Tool::ListComments < Ai::Tool } } ``` - Each collection object has the following fields: + Each comment object has the following fields: - id [Integer, not null] - - name [String, not null] + - card_id [Integer, not null] + - body [String, not null] + - created_at [String, not null] ISO8601 formatted timestamp + - creator [Object, not null] the User that created the comment + - system [Boolean, not null] indicates if the comment was created by the system + - reactions [Array] + - content [String, not null] + - reacter [Object] represents a User + - id [Integer, not null] + - name [String, not null] MD param :page, @@ -38,6 +47,10 @@ class Ai::Tool::ListComments < Ai::Tool type: :integer, desc: "If provided, will return only status changes for the specified card", required: false + param :type, + type: :string, + desc: "If provided, returns either 'user' or 'system' comments, if ommited it returns both", + required: false param :created_at_gte, type: :string, desc: "If provided, will return only comments created on or after after the given ISO timestamp", @@ -47,12 +60,25 @@ class Ai::Tool::ListComments < Ai::Tool desc: "If provided, will return only comments created on or before the given ISO timestamp", required: false + attr_reader :user + + def initialize(user:) + @user = user + end + def execute(**params) - scope = Comment.all.includes(:card, :creator, reactions: [ :reacter ]).where.not(creator: { role: "system" }) + cards = Card.where(collection: user.collections) + scope = Comment.where(card: cards).includes(:card, :creator, reactions: [ :reacter ]) scope = scope.search(params[:query]) if params[:query].present? scope = scope.where(card_id: params[:card_id].to_i) if params[:card_id].present? + if params[:type]&.casecmp?("system") + scope = scope.where(creator: { role: "system" }) + elsif params[:type]&.casecmp?("user") + scope = scope.where.not(creator: { role: "system" }) + end + if params[:created_at_gte].present? timestamp = Time.iso8601(params[:created_at_gte]) scope = scope.where(created_at: timestamp..) @@ -69,19 +95,21 @@ class Ai::Tool::ListComments < Ai::Tool ).page(params[:page]) { - collections: page.records.map do |comment| + comments: page.records.map do |comment| { id: comment.id, card_id: comment.card_id, body: comment.body.to_plain_text, created_at: comment.created_at.iso8601, creator: comment.creator.as_json(only: [ :id, :name ]), + system: comment.creator.system?, reactions: comment.reactions.map do |reaction| { content: reaction.content, reacter: reaction.reacter.as_json(only: [ :id, :name ]) } - end + end, + url: collection_card_url(comment.card.collection_id, comment.card, anchor: "comment_#{comment.id}") } end, pagination: { diff --git a/app/models/ai/tool/list_status_changes.rb b/app/models/ai/tool/list_status_changes.rb deleted file mode 100644 index c7d9b1bfb..000000000 --- a/app/models/ai/tool/list_status_changes.rb +++ /dev/null @@ -1,84 +0,0 @@ -class Ai::Tool::ListStatusChanges < Ai::Tool - description <<-MD - Lists all status changes accessible by the current user. - The response is paginated so you may need to iterate through multiple pages to get the full list. - Responses are JSON objects that look like this: - ``` - { - "collections": [ - { - "id": 3, - "card_id": 5, - "body": "Jane Doe moved this to Done", - "created_at": "2023-10-01T12:00:00Z", - "creator": { "id": 2, "name": "Jane Doe" } - } - ], - "pagination": { - "next_page": "e3c2gh75e4..." - } - } - ``` - Each collection object has the following fields: - - id [Integer, not null] - - name [String, not null] - MD - - param :page, - type: :string, - desc: "Which page to return. Leave balnk to get the first page", - required: false - param :query, - type: :string, - desc: "If provided, will perform a semantinc search by embeddings and return only matching status changes", - required: false - param :card_id, - type: :integer, - desc: "If provided, will return only status changes for the specified card", - required: false - param :created_at_gte, - type: :string, - desc: "If provided, will return only comments created on or after after the given ISO timestamp", - required: false - param :created_at_lte, - type: :string, - desc: "If provided, will return only comments created on or before the given ISO timestamp", - required: false - - def execute(**params) - scope = Comment.all.includes(:card, :creator).where(creator: { role: "system" }) - - scope = scope.search(params[:query]) if params[:query].present? - scope = scope.where(card_id: params[:card_id].to_i) if params[:card_id].present? - - if params[:created_at_gte].present? - timestamp = Time.iso8601(params[:created_at_gte]) - scope = scope.where(created_at: timestamp..) - end - - if params[:created_at_lte].present? - timestamp = Time.iso8601(params[:created_at_lte]) - scope = scope.where(created_at: ..timestamp) - end - - page = GearedPagination::Recordset.new( - scope, - ordered_by: { created_at: :asc, id: :desc } - ).page(params[:page]) - - { - collections: page.records.map do |comment| - { - id: comment.id, - card_id: comment.card_id, - body: comment.body.to_plain_text, - created_at: comment.created_at.iso8601, - creator: comment.creator.as_json(only: [ :id, :name ]) - } - end, - pagination: { - next_page: page.next_param - } - }.to_json - end -end diff --git a/app/models/ai/tool/list_users.rb b/app/models/ai/tool/list_users.rb index bd644929e..c785c786e 100644 --- a/app/models/ai/tool/list_users.rb +++ b/app/models/ai/tool/list_users.rb @@ -5,7 +5,7 @@ class Ai::Tool::ListUsers < Ai::Tool Responses are JSON objects that look like this: ``` { - "collections": [ + "users": [ { "id": 3, "name": "John Doe" }, { "id": 4, "name": "Johanna Doe" } ], @@ -14,9 +14,11 @@ class Ai::Tool::ListUsers < Ai::Tool } } ``` - Each collection object has the following fields: + Each user object has the following fields: - id [Integer, not null] - name [String, not null] + - role [String, not null] + - url [String, not null] MD param :page, @@ -28,6 +30,12 @@ class Ai::Tool::ListUsers < Ai::Tool desc: "If provided, will return only the users with the given IDs (comma-separated)", required: false + attr_reader :user + + def initialize(user:) + @user = user + end + def execute(**params) scope = User.all @@ -39,11 +47,12 @@ class Ai::Tool::ListUsers < Ai::Tool ).page(params[:page]) { - collections: page.records.map do |user| + users: page.records.map do |user| { id: user.id, name: user.name, - url: user_path(user) + role: user.role, + url: user_url(user) } end, pagination: { diff --git a/app/models/conversation/message/response_generator.rb b/app/models/conversation/message/response_generator.rb index 35df690cb..ac9b562f0 100644 --- a/app/models/conversation/message/response_generator.rb +++ b/app/models/conversation/message/response_generator.rb @@ -25,7 +25,7 @@ class Conversation::Message::ResponseGenerator - Respond in **Markdown** - Always include links to relevant cards, collections, comments, or users - Remember, you're here to help — not to anticipate. + You're here to help — not to anticipate. PROMPT attr_reader :message, :prompt, :llm_model @@ -62,11 +62,10 @@ class Conversation::Message::ResponseGenerator def llm RubyLLM.chat(model: llm_model).tap do |chat| - chat.with_tool(Ai::Tool::ListCards.new) - chat.with_tool(Ai::Tool::ListCollections.new) - chat.with_tool(Ai::Tool::ListComments.new) - chat.with_tool(Ai::Tool::ListStatusChanges.new) - chat.with_tool(Ai::Tool::ListUsers.new) + chat.with_tool(Ai::Tool::ListCards.new(user: message.owner)) + chat.with_tool(Ai::Tool::ListCollections.new(user: message.owner)) + chat.with_tool(Ai::Tool::ListComments.new(user: message.owner)) + chat.with_tool(Ai::Tool::ListUsers.new(user: message.owner)) chat.reset_messages!