mirror of
https://github.com/maybe-finance/maybe.git
synced 2025-07-26 00:29:40 +02:00
* AI sidebar * Add chat and message models with associations * Implement AI chat functionality with sidebar and messaging system - Add chat and messages controllers - Create chat and message views - Implement chat-related routes - Add message broadcasting and user interactions - Update application layout to support chat sidebar - Enhance user model with initials method * Refactor AI sidebar with enhanced chat menu and interactions - Update sidebar layout with dynamic width and improved responsiveness - Add new chat menu Stimulus controller for toggling between chat and chat list views - Improve chat list display with recent chats and empty state - Extract AI avatar to a partial for reusability - Enhance message display and interaction styling - Add more contextual buttons and interaction hints * Improve chat scroll behavior and message styling - Refactor chat scroll functionality with Stimulus controller - Optimize message scrolling in chat views - Update message styling for better visual hierarchy - Enhance chat container layout with flex and auto-scroll - Simplify message rendering across different chat views * Extract AI avatar to a shared partial for consistent styling - Refactor AI avatar rendering across chat views - Replace hardcoded avatar markup with a reusable partial - Simplify avatar display in chats and messages views * Update sidebar controller to handle right panel width dynamically - Add conditional width class for right sidebar panel - Ensure consistent sidebar toggle behavior for both left and right panels - Use specific width class for right panel (w-[375px]) * Refactor chat form and AI greeting with flexible partials - Extract message form to a reusable partial with dynamic context support - Create flexible AI greeting partial for consistent welcome messages - Simplify chat and sidebar views by leveraging new partials - Add support for different form scenarios (chat, new chat, sidebar) - Improve code modularity and reduce duplication * Add chat clearing functionality with dynamic menu options - Implement clear chat action in ChatsController - Add clear chat route to support clearing messages - Update AI sidebar with dropdown menu for chat actions - Preserve system message when clearing chat - Enhance chat interaction with new menu options * Add frontmatter to project structure documentation - Create initial frontmatter for structure.mdc file - Include description and configuration options - Prepare for potential dynamic documentation rendering * Update general project rules with additional guidelines - Add rule for using `Current.family` instead of `current_family` - Include new guidelines for testing, API routes, and solution approach - Expand project-specific rules for more consistent development practices * Add OpenAI gem and AI-friendly data representations - Add `ruby-openai` gem for AI integration - Implement `to_ai_readable_hash` methods in BalanceSheet and IncomeStatement - Include Promptable module in both models - Add savings rate calculation method in IncomeStatement - Prepare financial models for AI-powered insights and interactions * Enhance AI Financial Assistant with Advanced Querying and Debugging Capabilities - Implement comprehensive AI financial query system with function-based interactions - Add detailed debug logging for AI responses and function calls - Extend BalanceSheet and IncomeStatement models with AI-friendly methods - Create robust error handling and fallback mechanisms for AI queries - Update chat and message views to support debug mode and enhanced rendering - Add AI query routes and initial test coverage for financial assistant * Refactor AI sidebar and chat layout with improved structure and comments - Remove inline AI chat from application layout - Enhance AI sidebar with more semantic HTML structure - Add descriptive comments to clarify different sections of chat view - Improve flex layout and scrolling behavior in chat messages container - Optimize message rendering with more explicit class names and structure * Add Markdown rendering support for AI chat messages - Implement `markdown` helper method in ApplicationHelper using Redcarpet - Update message view to render AI messages with Markdown formatting - Add comprehensive Markdown rendering options (tables, code blocks, links) - Enhance AI Financial Assistant prompt to encourage Markdown usage - Remove commented Markdown CSS in Tailwind application stylesheet * Missing comma * Enhance AI response processing with chat history context * Improve AI debug logging with payload size limits and internal message flag * Enhance AI chat interaction with improved thinking indicator and scrolling behavior * Add AI consent and enable/disable functionality for AI chat * Upgrade Biome and refactor JavaScript template literals - Update @biomejs/biome to latest version with caret (^) notation - Refactor AI query and chat controllers to use template literals - Standardize npm scripts formatting in package.json * Add beta testing usage note to AI consent modal * Update test fixtures and configurations for AI chat functionality - Add family association to chat fixtures and tests - Set consistent password digest for test users - Enable AI for test users - Add OpenAI access token for test environment - Update chat and user model tests to include family context * Simplify data model and get tests passing * Remove structure.mdc from version control * Integrate AI chat styles into existing prose pattern * Match Figma design spec, implement Turbo frames and actions for chats controller * AI rules refresh * Consolidate Stimulus controllers, thinking state, controllers, and views * Naming, domain alignment * Reset migrations * Improve data model to support tool calls and message types * Tool calling tests and fixtures * Tool call implementation and test * Get assistant test working again * Test updates * Process tool calls within provider * Chat UI back to working state again * Remove stale code * Tests passing * Update openai class naming to avoid conflicts * Reconfigure test env * Rebuild gemfile * Fix naming conflicts for ChatResponse * Message styles * Use OpenAI conversation state management * Assistant function base implementation * Add back thinking messages, clean up error handling for chat * Fix sync error when security price has bad data from provider * Add balance sheet function to assistant * Add better function calling error visibility * Add income statement function * Simplify and clean up "thinking" interactions with Turbo frames * Remove stale data definitions from functions * Ensure VCR fixtures working with latest code * basic stream implementation * Get streaming working * Make AI sidebar wider when left sidebar is collapsed * Get tests working with streaming responses * Centralize provider error handling * Provider data boundaries --------- Co-authored-by: Josh Pigford <josh@joshpigford.com>
251 lines
6.6 KiB
Ruby
251 lines
6.6 KiB
Ruby
class Provider::Synth < Provider
|
|
include ExchangeRateProvider, SecurityProvider
|
|
|
|
def initialize(api_key)
|
|
@api_key = api_key
|
|
end
|
|
|
|
def healthy?
|
|
with_provider_response do
|
|
response = client.get("#{base_url}/user")
|
|
JSON.parse(response.body).dig("id").present?
|
|
end
|
|
end
|
|
|
|
def usage
|
|
with_provider_response do
|
|
response = client.get("#{base_url}/user")
|
|
|
|
parsed = JSON.parse(response.body)
|
|
|
|
remaining = parsed.dig("api_calls_remaining")
|
|
limit = parsed.dig("api_limit")
|
|
used = limit - remaining
|
|
|
|
UsageData.new(
|
|
used: used,
|
|
limit: limit,
|
|
utilization: used.to_f / limit * 100,
|
|
plan: parsed.dig("plan"),
|
|
)
|
|
end
|
|
end
|
|
|
|
# ================================
|
|
# Exchange Rates
|
|
# ================================
|
|
|
|
def fetch_exchange_rate(from:, to:, date:)
|
|
with_provider_response retries: 2 do
|
|
response = client.get("#{base_url}/rates/historical") do |req|
|
|
req.params["date"] = date.to_s
|
|
req.params["from"] = from
|
|
req.params["to"] = to
|
|
end
|
|
|
|
rates = JSON.parse(response.body).dig("data", "rates")
|
|
|
|
Rate.new(date:, from:, to:, rate: rates.dig(to))
|
|
end
|
|
end
|
|
|
|
def fetch_exchange_rates(from:, to:, start_date:, end_date:)
|
|
with_provider_response retries: 1 do
|
|
data = paginate(
|
|
"#{base_url}/rates/historical-range",
|
|
from: from,
|
|
to: to,
|
|
date_start: start_date.to_s,
|
|
date_end: end_date.to_s
|
|
) do |body|
|
|
body.dig("data")
|
|
end
|
|
|
|
data.paginated.map do |rate|
|
|
Rate.new(date: rate.dig("date"), from:, to:, rate: rate.dig("rates", to))
|
|
end
|
|
end
|
|
end
|
|
|
|
# ================================
|
|
# Securities
|
|
# ================================
|
|
|
|
def search_securities(symbol, country_code: nil, exchange_operating_mic: nil)
|
|
with_provider_response do
|
|
response = client.get("#{base_url}/tickers/search") do |req|
|
|
req.params["name"] = symbol
|
|
req.params["dataset"] = "limited"
|
|
req.params["country_code"] = country_code if country_code.present?
|
|
req.params["exchange_operating_mic"] = exchange_operating_mic if exchange_operating_mic.present?
|
|
req.params["limit"] = 25
|
|
end
|
|
|
|
parsed = JSON.parse(response.body)
|
|
|
|
parsed.dig("data").map do |security|
|
|
Security.new(
|
|
symbol: security.dig("symbol"),
|
|
name: security.dig("name"),
|
|
logo_url: security.dig("logo_url"),
|
|
exchange_operating_mic: security.dig("exchange", "operating_mic_code"),
|
|
)
|
|
end
|
|
end
|
|
end
|
|
|
|
def fetch_security_info(security)
|
|
with_provider_response do
|
|
response = client.get("#{base_url}/tickers/#{security.ticker}") do |req|
|
|
req.params["mic_code"] = security.exchange_mic if security.exchange_mic.present?
|
|
req.params["operating_mic"] = security.exchange_operating_mic if security.exchange_operating_mic.present?
|
|
end
|
|
|
|
data = JSON.parse(response.body).dig("data")
|
|
|
|
SecurityInfo.new(
|
|
symbol: data.dig("ticker"),
|
|
name: data.dig("name"),
|
|
links: data.dig("links"),
|
|
logo_url: data.dig("logo_url"),
|
|
description: data.dig("description"),
|
|
kind: data.dig("kind")
|
|
)
|
|
end
|
|
end
|
|
|
|
def fetch_security_price(security, date:)
|
|
with_provider_response do
|
|
historical_data = fetch_security_prices(security, start_date: date, end_date: date)
|
|
|
|
raise ProviderError, "No prices found for security #{security.ticker} on date #{date}" if historical_data.data.empty?
|
|
|
|
historical_data.data.first
|
|
end
|
|
end
|
|
|
|
def fetch_security_prices(security, start_date:, end_date:)
|
|
with_provider_response retries: 1 do
|
|
params = {
|
|
start_date: start_date,
|
|
end_date: end_date
|
|
}
|
|
|
|
params[:operating_mic_code] = security.exchange_operating_mic if security.exchange_operating_mic.present?
|
|
|
|
data = paginate(
|
|
"#{base_url}/tickers/#{security.ticker}/open-close",
|
|
params
|
|
) do |body|
|
|
body.dig("prices")
|
|
end
|
|
|
|
currency = data.first_page.dig("currency")
|
|
country_code = data.first_page.dig("exchange", "country_code")
|
|
exchange_mic = data.first_page.dig("exchange", "mic_code")
|
|
exchange_operating_mic = data.first_page.dig("exchange", "operating_mic_code")
|
|
|
|
data.paginated.map do |price|
|
|
Price.new(
|
|
security: security,
|
|
date: price.dig("date"),
|
|
price: price.dig("close") || price.dig("open"),
|
|
currency: currency
|
|
)
|
|
end
|
|
end
|
|
end
|
|
|
|
# ================================
|
|
# Transactions
|
|
# ================================
|
|
|
|
def enrich_transaction(description, amount: nil, date: nil, city: nil, state: nil, country: nil)
|
|
with_provider_response do
|
|
params = {
|
|
description: description,
|
|
amount: amount,
|
|
date: date,
|
|
city: city,
|
|
state: state,
|
|
country: country
|
|
}.compact
|
|
|
|
response = client.get("#{base_url}/enrich", params)
|
|
|
|
parsed = JSON.parse(response.body)
|
|
|
|
TransactionEnrichmentData.new(
|
|
name: parsed.dig("merchant"),
|
|
icon_url: parsed.dig("icon"),
|
|
category: parsed.dig("category")
|
|
)
|
|
end
|
|
end
|
|
|
|
private
|
|
attr_reader :api_key
|
|
|
|
TransactionEnrichmentData = Data.define(:name, :icon_url, :category)
|
|
|
|
def retryable_errors
|
|
[
|
|
Faraday::TimeoutError,
|
|
Faraday::ConnectionFailed,
|
|
Faraday::SSLError
|
|
]
|
|
end
|
|
|
|
def base_url
|
|
ENV["SYNTH_URL"] || "https://api.synthfinance.com"
|
|
end
|
|
|
|
def app_name
|
|
"maybe_app"
|
|
end
|
|
|
|
def app_type
|
|
Rails.application.config.app_mode
|
|
end
|
|
|
|
def client
|
|
@client ||= Faraday.new(url: base_url) do |faraday|
|
|
faraday.response :raise_error
|
|
faraday.headers["Authorization"] = "Bearer #{api_key}"
|
|
faraday.headers["X-Source"] = app_name
|
|
faraday.headers["X-Source-Type"] = app_type
|
|
end
|
|
end
|
|
|
|
def fetch_page(url, page, params = {})
|
|
client.get(url, params.merge(page: page))
|
|
end
|
|
|
|
def paginate(url, params = {})
|
|
results = []
|
|
page = 1
|
|
current_page = 0
|
|
total_pages = 1
|
|
first_page = nil
|
|
|
|
while current_page < total_pages
|
|
response = fetch_page(url, page, params)
|
|
|
|
body = JSON.parse(response.body)
|
|
first_page = body unless first_page
|
|
page_results = yield(body)
|
|
results.concat(page_results)
|
|
|
|
current_page = body.dig("paging", "current_page")
|
|
total_pages = body.dig("paging", "total_pages")
|
|
|
|
page += 1
|
|
end
|
|
|
|
PaginatedData.new(
|
|
paginated: results,
|
|
first_page: first_page,
|
|
total_pages: total_pages
|
|
)
|
|
end
|
|
end
|