122 lines
4.5 KiB
Ruby
122 lines
4.5 KiB
Ruby
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 isn’t related to Fizzy, respond politely with “I don’t know” or “I’m not sure” and explain that you can only answer questions related to Fizzy
|
||
- Don’t 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
|