mirror of
https://github.com/we-promise/sure.git
synced 2026-07-12 04:45:19 +00:00
Auto-categorization, merchant detection/enhancement, and PDF/bank-statement extraction hard-coded Provider::Registry.get_provider(:openai), so selecting Anthropic (or running an Anthropic-only self-hosted install) left those operations using/missing OpenAI rather than the chosen provider. Add Provider::Registry.preferred_llm_provider, which resolves the LLM provider honoring Setting.llm_provider with a configured-provider fallback (mirroring how chat picks its provider), and route all six TODO(#2113) call sites through it: - Family::AutoCategorizer#llm_provider - Family::AutoMerchantDetector#llm_provider - ProviderMerchant::Enhancer#llm_provider - PdfImport (process_pdf + extract_bank_statement) - Assistant::Function::ImportBankStatement Provider::Anthropic already implements auto_categorize / auto_detect_merchants / enhance_provider_merchants (#1984) and process_pdf / extract_bank_statement (#1985), so no provider changes are needed — only the wiring. Closes #2113.
191 lines
5.5 KiB
Ruby
191 lines
5.5 KiB
Ruby
require "csv"
|
|
|
|
class Assistant::Function::ImportBankStatement < Assistant::Function
|
|
class << self
|
|
def name
|
|
"import_bank_statement"
|
|
end
|
|
|
|
def description
|
|
<<~INSTRUCTIONS
|
|
Use this to import transactions from a bank statement PDF that has already been uploaded.
|
|
|
|
This function will:
|
|
1. Extract transaction data from the PDF using AI
|
|
2. Create a transaction import with the extracted data
|
|
3. Return the import ID and extracted transactions for review
|
|
|
|
The PDF must have already been uploaded via the PDF import feature.
|
|
Only use this for PDFs that are identified as bank statements.
|
|
|
|
Example:
|
|
|
|
```
|
|
import_bank_statement({
|
|
pdf_import_id: "abc123-def456",
|
|
account_id: "xyz789"
|
|
})
|
|
```
|
|
|
|
If account_id is not provided, you should ask the user which account to import to.
|
|
INSTRUCTIONS
|
|
end
|
|
end
|
|
|
|
def strict_mode?
|
|
false
|
|
end
|
|
|
|
def params_schema
|
|
build_schema(
|
|
required: [ "pdf_import_id" ],
|
|
properties: {
|
|
pdf_import_id: {
|
|
type: "string",
|
|
description: "The ID of the PDF import to extract transactions from"
|
|
},
|
|
account_id: {
|
|
type: "string",
|
|
description: "The ID of the account to import transactions into. If not provided, will return available accounts."
|
|
}
|
|
}
|
|
)
|
|
end
|
|
|
|
def call(params = {})
|
|
pdf_import = family.imports.find_by(id: params["pdf_import_id"], type: "PdfImport")
|
|
|
|
unless pdf_import
|
|
return {
|
|
success: false,
|
|
error: "PDF import not found",
|
|
message: "Could not find a PDF import with ID: #{params["pdf_import_id"]}"
|
|
}
|
|
end
|
|
|
|
unless pdf_import.document_type == "bank_statement"
|
|
return {
|
|
success: false,
|
|
error: "not_bank_statement",
|
|
message: "This PDF is not a bank statement. Document type: #{pdf_import.document_type}",
|
|
available_actions: [ "Use a different PDF that is a bank statement" ]
|
|
}
|
|
end
|
|
|
|
# If no account specified, return available accounts
|
|
if params["account_id"].blank?
|
|
return {
|
|
success: false,
|
|
error: "account_required",
|
|
message: "Please specify which account to import transactions into",
|
|
available_accounts: family.accounts.visible.depository.map { |a| { id: a.id, name: a.name } }
|
|
}
|
|
end
|
|
|
|
account = family.accounts.find_by(id: params["account_id"])
|
|
unless account
|
|
return {
|
|
success: false,
|
|
error: "account_not_found",
|
|
message: "Account not found",
|
|
available_accounts: family.accounts.visible.depository.map { |a| { id: a.id, name: a.name } }
|
|
}
|
|
end
|
|
|
|
# Extract transactions from the PDF using the configured LLM provider.
|
|
# Honors Setting.llm_provider (issue #2113) — Provider::Anthropic implements
|
|
# extract_bank_statement (PR #1985).
|
|
provider = Provider::Registry.preferred_llm_provider
|
|
unless provider
|
|
return {
|
|
success: false,
|
|
error: "provider_not_configured",
|
|
message: "AI provider is not configured"
|
|
}
|
|
end
|
|
|
|
response = provider.extract_bank_statement(
|
|
pdf_content: pdf_import.pdf_file_content,
|
|
model: openai_model,
|
|
family: family
|
|
)
|
|
|
|
unless response.success?
|
|
error_message = response.error&.message || "Unknown extraction error"
|
|
return {
|
|
success: false,
|
|
error: "extraction_failed",
|
|
message: "Failed to extract transactions: #{error_message}"
|
|
}
|
|
end
|
|
|
|
result = response.data
|
|
|
|
if result[:transactions].blank?
|
|
return {
|
|
success: false,
|
|
error: "no_transactions_found",
|
|
message: "Could not extract any transactions from the bank statement"
|
|
}
|
|
end
|
|
|
|
# Create a CSV from extracted transactions
|
|
csv_content = generate_csv(result[:transactions])
|
|
|
|
# Create a TransactionImport
|
|
import = family.imports.create!(
|
|
type: "TransactionImport",
|
|
account: account,
|
|
raw_file_str: csv_content,
|
|
date_col_label: "date",
|
|
amount_col_label: "amount",
|
|
name_col_label: "name",
|
|
category_col_label: "category",
|
|
notes_col_label: "notes",
|
|
date_format: "%Y-%m-%d",
|
|
signage_convention: "inflows_positive"
|
|
)
|
|
|
|
import.generate_rows_from_csv
|
|
|
|
{
|
|
success: true,
|
|
import_id: import.id,
|
|
transaction_count: result[:transactions].size,
|
|
transactions_preview: result[:transactions].first(5),
|
|
statement_period: result[:period],
|
|
account_holder: result[:account_holder],
|
|
message: "Successfully extracted #{result[:transactions].size} transactions. Import created with ID: #{import.id}. Review and publish when ready."
|
|
}
|
|
rescue Provider::Error, Faraday::Error, Timeout::Error, RuntimeError => e
|
|
Rails.logger.error("ImportBankStatement error: #{e.class.name} - #{e.message}")
|
|
Rails.logger.error(e.backtrace.first(10).join("\n"))
|
|
{
|
|
success: false,
|
|
error: "extraction_failed",
|
|
message: "Failed to extract transactions: #{e.message.truncate(200)}"
|
|
}
|
|
end
|
|
|
|
private
|
|
|
|
def generate_csv(transactions)
|
|
CSV.generate do |csv|
|
|
csv << %w[date amount name category notes]
|
|
transactions.each do |txn|
|
|
csv << [
|
|
txn[:date],
|
|
txn[:amount],
|
|
txn[:name] || txn[:description],
|
|
txn[:category],
|
|
txn[:notes]
|
|
]
|
|
end
|
|
end
|
|
end
|
|
|
|
def openai_model
|
|
ENV["OPENAI_MODEL"].presence || Provider::Openai::DEFAULT_MODEL
|
|
end
|
|
end
|