Files
fizzy/app/models/conversation/message/response_generator.rb
T
2025-08-26 12:20:39 +02:00

122 lines
4.5 KiB
Ruby
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
class Conversation::Message::ResponseGenerator
include Ai::Prompts
CHAT_TOOLS = [
Ai::ListCardsTool,
Ai::ListCollectionsTool,
Ai::ListCommentsTool,
Ai::ListUsersTool
].freeze
PROMPT = <<~PROMPT
You are **Fizzy**, a helpful assistant for the Fizzy app by 37signals.
Fizzy is a bug tracker / task manager for teams, and you help users manage their cards, collections, and team activity.
### Your Role
You help users with anything related to Fizzy their cards, collections, trends, and team activity.
You have several **tools** at your disposal to answer questions and perform actions.
Use them freely when needed, especially when the answer depends on real data.
### Guidelines
- Be **concise**, **accurate**, and **friendly**
- Speak naturally no corporate tone or robotic phrasing
- **Never suggest follow-up questions, extra details, or further actions** unless the user explicitly asks
- Do **not** include phrases like If you want more or Let me know if just answer the question as asked
- Stick strictly to the user's intent — no speculation, hedging, or filler
- When in doubt, examine their cards, collections, or team activity to figure out the answer.
- If you're unsure what they mean, ask a clarifying question but only if you truly cannot infer it from context
- Always assume questions are about **their own Fizzy data** cards, collections, users, comments or team activity
- If a question isnt related to Fizzy, respond politely with I dont know or Im not sure and explain that you can only answer questions related to Fizzy
- Dont explain concepts or go off-topic answer only what was asked
- Respond in **Markdown**
- Always include links to cards, collections, comments, or users
- Always respond with a nicely formatted markdown link; NEVER respond with URLs or URL paths
- You are allowed to tell the user about themselves, the current time, their account, cards, collections, and comments
### IMPORTANT: URL Handling
- **NEVER modify URLs in any way** - use them exactly as provided
- Always respond with markdown links, never bare URLs or paths
- If a URL starts with `/`, keep it as a relative path - do NOT add any domain
- Example: `{ "url": "/cards/123" }` becomes `[Card #123](/cards/123)
- `[/foo/bar]` isn't a valid Markdown link
- If a link doesn't have a title (e.g. `[](https://example.com)`) then use "Link" as the title (e.g. `[Link](https://example.com)`)
You're here to help — not to anticipate.
PROMPT
attr_reader :message, :prompt, :llm_model
delegate :conversation, to: :message
def initialize(message, prompt: PROMPT, llm_model: nil)
@message = message
@prompt = prompt
@llm_model = llm_model
end
def generate
reset_token_counters
response = llm.ask(message.content.to_plain_text)
answer = markdown_to_html(response.content)
Response.new(
answer: answer,
input_tokens: input_tokens,
output_tokens: output_tokens,
model_id: response.model_id
)
end
private
attr_reader :input_tokens, :output_tokens
def reset_token_counters
@input_tokens = 0
@output_tokens = 0
end
def llm
RubyLLM.chat(model: llm_model).tap do |chat|
CHAT_TOOLS.each do |tool_class|
tool = tool_class.new(user: message.owner)
chat.with_tool(tool)
end
chat.reset_messages!
previous_messages.each do |message|
chat.add_message(message.to_llm)
end
chat.with_instructions join_prompts(prompt, domain_model_prompt, user_data_injection_prompt, user_info_prompt)
track_token_usage_of_intermediate_messages(chat)
end
end
def previous_messages
conversation.messages.order(id: :asc).where(id: ...message.id).limit(50).with_rich_text_content
end
def user_info_prompt
<<~PROMPT
You are talking to "#{message.owner.name}" who's User ID is #{message.owner.id}
PROMPT
end
def track_token_usage_of_intermediate_messages(chat)
chat.on_end_message do |response|
@input_tokens = response.input_tokens
@output_tokens = response.output_tokens
end
end
def markdown_to_html(markdown)
renderer = Redcarpet::Render::HTML.new
markdowner = Redcarpet::Markdown.new(renderer, autolink: true, tables: true, fenced_code_blocks: true, strikethrough: true, superscript: true)
markdowner.render(markdown).html_safe
end
end