mirror of
https://github.com/we-promise/sure.git
synced 2026-06-01 08:49:01 +00:00
The provider-agnostic vector store stack (VectorStore::Pgvector + the Embeddable concern) already shipped to main. This PR closes the Anthropic loop: - VectorStore::Registry.adapter_name now returns :pgvector when Setting.llm_provider == "anthropic" and no explicit VECTOR_STORE_PROVIDER override is set. Anthropic has no hosted vector store, so falling back to the local pgvector adapter is the only correct default. Explicit VECTOR_STORE_PROVIDER still wins. - SearchFamilyFiles surfaces a longer message when no adapter is wired up — calling out pgvector + EMBEDDING_URI_BASE as the supported Anthropic-only path so the user is not stuck with an "OpenAI required" hint that is no longer accurate. The Embeddable concern already pulls embeddings from EMBEDDING_URI_BASE / EMBEDDING_ACCESS_TOKEN (with OpenAI as fallback), so Anthropic installs point this at Voyage AI, a local Ollama instance, or OpenAI embeddings — independent of the chat provider. Tests cover the new default routing, the existing OpenAI default staying intact, and explicit VECTOR_STORE_PROVIDER overriding the Anthropic default. Stacked on #1985 (PR 3/5). 5/5 settings UI + retention disclosure next.
173 lines
5.3 KiB
Ruby
173 lines
5.3 KiB
Ruby
class Assistant::Function::SearchFamilyFiles < Assistant::Function
|
|
class << self
|
|
def name
|
|
"search_family_files"
|
|
end
|
|
|
|
def description
|
|
<<~DESC
|
|
Search through documents that the family has uploaded to their financial document store.
|
|
|
|
Use this when the user asks questions about their uploaded financial documents such as
|
|
tax returns, bank statements, contracts, insurance policies, investment reports, or any
|
|
other files they've imported.
|
|
|
|
Returns relevant excerpts from matching documents along with the source filename and
|
|
a relevance score.
|
|
|
|
Supported file types include: PDF, DOCX, XLSX, PPTX, TXT, CSV, JSON, XML, HTML, MD,
|
|
and common source code formats.
|
|
|
|
Example:
|
|
|
|
```
|
|
search_family_files({
|
|
query: "What was the total income on my 2024 tax return?"
|
|
})
|
|
```
|
|
DESC
|
|
end
|
|
end
|
|
|
|
def strict_mode?
|
|
false
|
|
end
|
|
|
|
def params_schema
|
|
build_schema(
|
|
required: [ "query" ],
|
|
properties: {
|
|
query: {
|
|
type: "string",
|
|
description: "The search query to find relevant information in the family's uploaded documents"
|
|
},
|
|
max_results: {
|
|
type: "integer",
|
|
description: "Maximum number of results to return (default: 10, max: 20)"
|
|
}
|
|
}
|
|
)
|
|
end
|
|
|
|
def call(params = {})
|
|
query = params["query"]
|
|
max_results = (params["max_results"] || 10).to_i.clamp(1, 20)
|
|
|
|
Rails.logger.debug("[SearchFamilyFiles] query=#{query.inspect} max_results=#{max_results} family_id=#{family.id}")
|
|
|
|
unless family.vector_store_id.present?
|
|
Rails.logger.debug("[SearchFamilyFiles] family #{family.id} has no vector_store_id")
|
|
return {
|
|
success: false,
|
|
error: "no_documents",
|
|
message: "No documents have been uploaded to the family document store yet."
|
|
}
|
|
end
|
|
|
|
adapter = VectorStore.adapter
|
|
|
|
unless adapter
|
|
Rails.logger.debug("[SearchFamilyFiles] no VectorStore adapter configured")
|
|
return {
|
|
success: false,
|
|
error: "provider_not_configured",
|
|
message: "No vector store is configured. Set VECTOR_STORE_PROVIDER " \
|
|
"(openai | pgvector | qdrant), configure OpenAI, or — for " \
|
|
"Anthropic-only installs — enable the pgvector adapter and " \
|
|
"point EMBEDDING_URI_BASE at an embeddings endpoint."
|
|
}
|
|
end
|
|
|
|
store_id = family.vector_store_id
|
|
Rails.logger.debug("[SearchFamilyFiles] searching store_id=#{store_id} via #{adapter.class.name}")
|
|
|
|
trace = create_langfuse_trace(
|
|
name: "search_family_files",
|
|
input: { query: query, max_results: max_results, store_id: store_id }
|
|
)
|
|
|
|
response = adapter.search(
|
|
store_id: store_id,
|
|
query: query,
|
|
max_results: max_results
|
|
)
|
|
|
|
unless response.success?
|
|
error_msg = response.error&.message
|
|
Rails.logger.debug("[SearchFamilyFiles] search failed: #{error_msg}")
|
|
begin
|
|
langfuse_client&.trace(id: trace.id, output: { error: error_msg }, level: "ERROR") if trace
|
|
rescue => e
|
|
Rails.logger.debug("[SearchFamilyFiles] Langfuse trace update failed: #{e.class}: #{e.message}\n#{e.backtrace&.first(5)&.join("\n")}")
|
|
end
|
|
return {
|
|
success: false,
|
|
error: "search_failed",
|
|
message: "Failed to search documents: #{error_msg}"
|
|
}
|
|
end
|
|
|
|
results = response.data
|
|
|
|
Rails.logger.debug("[SearchFamilyFiles] #{results.size} chunk(s) returned")
|
|
|
|
results.each_with_index do |r, i|
|
|
Rails.logger.debug(
|
|
"[SearchFamilyFiles] chunk[#{i}] score=#{r[:score]} file=#{r[:filename].inspect} " \
|
|
"content_length=#{r[:content]&.length} preview=#{r[:content]&.truncate(10).inspect}"
|
|
)
|
|
end
|
|
|
|
mapped = results.map do |result|
|
|
{ content: result[:content], filename: result[:filename], score: result[:score] }
|
|
end
|
|
|
|
output = if mapped.empty?
|
|
{ success: true, results: [], message: "No matching documents found for the query." }
|
|
else
|
|
{ success: true, query: query, result_count: mapped.size, results: mapped }
|
|
end
|
|
|
|
begin
|
|
if trace
|
|
langfuse_client&.trace(id: trace.id, output: {
|
|
result_count: mapped.size,
|
|
chunks: mapped.map { |r| { filename: r[:filename], score: r[:score], content_length: r[:content]&.length } }
|
|
})
|
|
end
|
|
rescue => e
|
|
Rails.logger.debug("[SearchFamilyFiles] Langfuse trace update failed: #{e.class}: #{e.message}\n#{e.backtrace&.first(5)&.join("\n")}")
|
|
end
|
|
|
|
output
|
|
rescue => e
|
|
Rails.logger.error("[SearchFamilyFiles] error: #{e.class.name} - #{e.message}")
|
|
{
|
|
success: false,
|
|
error: "search_failed",
|
|
message: "An error occurred while searching documents: #{e.message.truncate(200)}"
|
|
}
|
|
end
|
|
|
|
private
|
|
def langfuse_client
|
|
return unless ENV["LANGFUSE_PUBLIC_KEY"].present? && ENV["LANGFUSE_SECRET_KEY"].present?
|
|
|
|
@langfuse_client ||= Langfuse.new
|
|
end
|
|
|
|
def create_langfuse_trace(name:, input:)
|
|
return unless langfuse_client
|
|
|
|
langfuse_client.trace(
|
|
name: name,
|
|
input: input,
|
|
user_id: user.id&.to_s,
|
|
environment: Rails.env
|
|
)
|
|
rescue => e
|
|
Rails.logger.debug("[SearchFamilyFiles] Langfuse trace creation failed: #{e.class}: #{e.message}\n#{e.backtrace&.first(5)&.join("\n")}")
|
|
nil
|
|
end
|
|
end
|