Fixing bug
This commit is contained in:
@@ -35,6 +35,16 @@ export default async function DashboardLayout({
|
||||
.eq('user_id', user.id)
|
||||
.eq('enabled', true)
|
||||
|
||||
// Check if ai-chat is explicitly disabled (legacy default: enabled when no row exists)
|
||||
const { data: aiChatToggle } = await supabase
|
||||
.from('extension_toggles')
|
||||
.select('enabled')
|
||||
.eq('user_id', user.id)
|
||||
.eq('sector_slug', 'general')
|
||||
.eq('extension_slug', 'ai-chat')
|
||||
.single()
|
||||
const showAiChat = aiChatToggle ? aiChatToggle.enabled : true
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
{/* Skip to content link for keyboard/screen reader users */}
|
||||
@@ -54,7 +64,7 @@ export default async function DashboardLayout({
|
||||
{children}
|
||||
</div>
|
||||
</main>
|
||||
<ChatWidget />
|
||||
{showAiChat && <ChatWidget />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -12,6 +12,10 @@ vi.mock('@/lib/supabase/server', () => ({
|
||||
createClient: () => Promise.resolve(mockSupabase),
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/init', () => ({
|
||||
ensureInitialized: vi.fn(),
|
||||
}))
|
||||
|
||||
const mockCreateJournalEntry = vi.fn()
|
||||
vi.mock('@/lib/bookkeeping/engine', () => ({
|
||||
createJournalEntry: (...args: unknown[]) => mockCreateJournalEntry(...args),
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { createClient } from '@/lib/supabase/server'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { createJournalEntry } from '@/lib/bookkeeping/engine'
|
||||
import { ensureInitialized } from '@/lib/init'
|
||||
import type { CreateJournalEntryInput } from '@/types'
|
||||
|
||||
ensureInitialized()
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const supabase = await createClient()
|
||||
const { data: { user } } = await supabase.auth.getUser()
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { createClient } from '@/lib/supabase/server'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { getSettings, saveSettings } from '@/extensions/general/ai-categorization'
|
||||
import { ensureInitialized } from '@/lib/init'
|
||||
|
||||
ensureInitialized()
|
||||
|
||||
/**
|
||||
* GET /api/extensions/ai-categorization/settings
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { createClient } from '@/lib/supabase/server'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { categorizeTransactions } from '@/extensions/general/ai-categorization'
|
||||
import { ensureInitialized } from '@/lib/init'
|
||||
import type { CategorizationSuggestion } from '@/extensions/general/ai-categorization/categorizer'
|
||||
|
||||
ensureInitialized()
|
||||
|
||||
/**
|
||||
* GET /api/extensions/ai-categorization/suggestions?transaction_ids=id1,id2,...
|
||||
* Fetch pre-computed AI suggestions for given transaction IDs
|
||||
|
||||
@@ -2,8 +2,11 @@ import { createClient } from '@supabase/supabase-js'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { syncAccountTransactions } from '@/extensions/general/enable-banking/lib/sync'
|
||||
import { isConsentExpiringSoon, getDaysUntilExpiry } from '@/extensions/general/enable-banking/lib/api-client'
|
||||
import { ensureInitialized } from '@/lib/init'
|
||||
import type { StoredAccount } from '@/extensions/general/enable-banking/types'
|
||||
|
||||
ensureInitialized()
|
||||
|
||||
/**
|
||||
* GET /api/extensions/enable-banking/sync/cron
|
||||
* Automatic daily bank transaction sync
|
||||
|
||||
@@ -143,6 +143,8 @@ export async function POST(request: Request) {
|
||||
vat_amount: item.vatRate && item.lineTotal ? (item.lineTotal * item.vatRate) / (100 + item.vatRate) : null,
|
||||
extraction_confidence: item.confidence,
|
||||
suggested_category: item.suggestedCategory,
|
||||
category: item.category,
|
||||
bas_account: item.basAccount,
|
||||
sort_order: index,
|
||||
}))
|
||||
|
||||
|
||||
@@ -128,7 +128,7 @@ describe('POST /api/transactions/[id]/categorize', () => {
|
||||
// Fetch company settings
|
||||
enqueue({ data: { entity_type: 'enskild_firma', fiscal_year_start_month: 1 }, error: null })
|
||||
// ensureFiscalPeriod: check existing
|
||||
enqueue({ data: { id: 'period-1' }, error: null })
|
||||
enqueue({ data: [{ id: 'period-1' }], error: null })
|
||||
|
||||
mockCreateTransactionJournalEntry.mockResolvedValue({ id: 'je-1' })
|
||||
mockSaveUserMappingRule.mockResolvedValue(undefined)
|
||||
@@ -177,7 +177,7 @@ describe('POST /api/transactions/[id]/categorize', () => {
|
||||
|
||||
enqueue({ data: tx, error: null })
|
||||
enqueue({ data: { entity_type: 'enskild_firma', fiscal_year_start_month: 1 }, error: null })
|
||||
enqueue({ data: { id: 'period-1' }, error: null })
|
||||
enqueue({ data: [{ id: 'period-1' }], error: null })
|
||||
|
||||
mockCreateTransactionJournalEntry.mockRejectedValue(new Error('Period locked'))
|
||||
|
||||
@@ -210,7 +210,7 @@ describe('POST /api/transactions/[id]/categorize', () => {
|
||||
|
||||
enqueue({ data: tx, error: null })
|
||||
enqueue({ data: { entity_type: 'enskild_firma', fiscal_year_start_month: 1 }, error: null })
|
||||
enqueue({ data: { id: 'period-1' }, error: null })
|
||||
enqueue({ data: [{ id: 'period-1' }], error: null })
|
||||
|
||||
mockCreateTransactionJournalEntry.mockResolvedValue({ id: 'je-1' })
|
||||
|
||||
@@ -237,7 +237,7 @@ describe('POST /api/transactions/[id]/categorize', () => {
|
||||
|
||||
enqueue({ data: tx, error: null })
|
||||
enqueue({ data: { entity_type: 'enskild_firma', fiscal_year_start_month: 1 }, error: null })
|
||||
enqueue({ data: { id: 'period-1' }, error: null })
|
||||
enqueue({ data: [{ id: 'period-1' }], error: null })
|
||||
|
||||
mockCreateTransactionJournalEntry.mockResolvedValue({ id: 'je-1' })
|
||||
|
||||
|
||||
@@ -14,14 +14,11 @@ vi.mock('@/lib/supabase/server', () => ({
|
||||
createClient: () => Promise.resolve(mockSupabase),
|
||||
}))
|
||||
|
||||
const mockCreateJournalEntry = vi.fn()
|
||||
const mockFindFiscalPeriod = vi.fn()
|
||||
vi.mock('@/lib/bookkeeping/engine', () => ({
|
||||
createJournalEntry: (...args: unknown[]) => mockCreateJournalEntry(...args),
|
||||
findFiscalPeriod: (...args: unknown[]) => mockFindFiscalPeriod(...args),
|
||||
}))
|
||||
|
||||
const mockCreateInvoicePaymentJournalEntry = vi.fn()
|
||||
const mockCreateInvoiceCashEntry = vi.fn()
|
||||
vi.mock('@/lib/bookkeeping/invoice-entries', () => ({
|
||||
createInvoicePaymentJournalEntry: (...args: unknown[]) => mockCreateInvoicePaymentJournalEntry(...args),
|
||||
createInvoiceCashEntry: (...args: unknown[]) => mockCreateInvoiceCashEntry(...args),
|
||||
getRevenueAccount: vi.fn().mockReturnValue('3001'),
|
||||
getOutputVatAccount: vi.fn().mockReturnValue('2611'),
|
||||
}))
|
||||
@@ -35,7 +32,6 @@ describe('POST /api/transactions/[id]/match-invoice', () => {
|
||||
vi.clearAllMocks()
|
||||
reset()
|
||||
mockSupabase.auth.getUser.mockResolvedValue({ data: { user: mockUser } })
|
||||
mockFindFiscalPeriod.mockResolvedValue('period-1')
|
||||
})
|
||||
|
||||
it('returns 401 when not authenticated', async () => {
|
||||
@@ -161,7 +157,7 @@ describe('POST /api/transactions/[id]/match-invoice', () => {
|
||||
// Fetch company settings
|
||||
enqueue({ data: { accounting_method: 'accrual', entity_type: 'enskild_firma' }, error: null })
|
||||
|
||||
mockCreateJournalEntry.mockResolvedValue({ id: 'je-1' })
|
||||
mockCreateInvoicePaymentJournalEntry.mockResolvedValue({ id: 'je-1' })
|
||||
|
||||
// Update invoice to paid
|
||||
enqueue({ data: null, error: null })
|
||||
@@ -186,16 +182,11 @@ describe('POST /api/transactions/[id]/match-invoice', () => {
|
||||
expect(body.paid_amount).toBe(12500)
|
||||
expect(body.journal_entry_id).toBe('je-1')
|
||||
|
||||
// Verify accrual journal entry: debit 1930, credit 1510
|
||||
expect(mockCreateJournalEntry).toHaveBeenCalledWith(
|
||||
// Verify accrual payment entry was called
|
||||
expect(mockCreateInvoicePaymentJournalEntry).toHaveBeenCalledWith(
|
||||
'user-1',
|
||||
expect.objectContaining({
|
||||
source_type: 'invoice_paid',
|
||||
lines: expect.arrayContaining([
|
||||
expect.objectContaining({ account_number: '1930', debit_amount: 12500 }),
|
||||
expect.objectContaining({ account_number: '1510', credit_amount: 12500 }),
|
||||
]),
|
||||
})
|
||||
expect.objectContaining({ id: 'inv-1' }),
|
||||
'2024-06-15'
|
||||
)
|
||||
})
|
||||
|
||||
@@ -207,7 +198,7 @@ describe('POST /api/transactions/[id]/match-invoice', () => {
|
||||
enqueue({ data: invoice, error: null })
|
||||
enqueue({ data: { accounting_method: 'accrual', entity_type: 'enskild_firma' }, error: null })
|
||||
|
||||
mockCreateJournalEntry.mockRejectedValue(new Error('Period locked'))
|
||||
mockCreateInvoicePaymentJournalEntry.mockRejectedValue(new Error('Period locked'))
|
||||
|
||||
// Update invoice
|
||||
enqueue({ data: null, error: null })
|
||||
|
||||
@@ -45,16 +45,22 @@ export async function getSettings(userId: string): Promise<PushNotificationSetti
|
||||
const supabase = await createClient()
|
||||
|
||||
const { data } = await supabase
|
||||
.from('extension_data')
|
||||
.select('value')
|
||||
.from('notification_settings')
|
||||
.select(
|
||||
'period_locked_enabled, period_year_closed_enabled, invoice_sent_enabled, receipt_extracted_enabled, receipt_matched_enabled'
|
||||
)
|
||||
.eq('user_id', userId)
|
||||
.eq('extension_id', 'push-notifications')
|
||||
.eq('key', 'settings')
|
||||
.single()
|
||||
|
||||
if (!data?.value) return { ...DEFAULT_SETTINGS }
|
||||
if (!data) return { ...DEFAULT_SETTINGS }
|
||||
|
||||
return { ...DEFAULT_SETTINGS, ...(data.value as Partial<PushNotificationSettings>) }
|
||||
return {
|
||||
periodLockedEnabled: data.period_locked_enabled ?? DEFAULT_SETTINGS.periodLockedEnabled,
|
||||
periodYearClosedEnabled: data.period_year_closed_enabled ?? DEFAULT_SETTINGS.periodYearClosedEnabled,
|
||||
invoiceSentEnabled: data.invoice_sent_enabled ?? DEFAULT_SETTINGS.invoiceSentEnabled,
|
||||
receiptExtractedEnabled: data.receipt_extracted_enabled ?? DEFAULT_SETTINGS.receiptExtractedEnabled,
|
||||
receiptMatchedEnabled: data.receipt_matched_enabled ?? DEFAULT_SETTINGS.receiptMatchedEnabled,
|
||||
}
|
||||
}
|
||||
|
||||
export async function saveSettings(
|
||||
@@ -67,15 +73,17 @@ export async function saveSettings(
|
||||
const supabase = await createClient()
|
||||
|
||||
await supabase
|
||||
.from('extension_data')
|
||||
.from('notification_settings')
|
||||
.upsert(
|
||||
{
|
||||
user_id: userId,
|
||||
extension_id: 'push-notifications',
|
||||
key: 'settings',
|
||||
value: merged,
|
||||
period_locked_enabled: merged.periodLockedEnabled,
|
||||
period_year_closed_enabled: merged.periodYearClosedEnabled,
|
||||
invoice_sent_enabled: merged.invoiceSentEnabled,
|
||||
receipt_extracted_enabled: merged.receiptExtractedEnabled,
|
||||
receipt_matched_enabled: merged.receiptMatchedEnabled,
|
||||
},
|
||||
{ onConflict: 'user_id,extension_id,key' }
|
||||
{ onConflict: 'user_id' }
|
||||
)
|
||||
|
||||
return merged
|
||||
@@ -231,6 +239,21 @@ export const pushNotificationsExtension: Extension = {
|
||||
path: '/settings/extensions/push-notifications',
|
||||
},
|
||||
async onInstall(ctx) {
|
||||
await saveSettings(ctx.userId, DEFAULT_SETTINGS)
|
||||
const supabase = await createClient()
|
||||
|
||||
// Ensure a notification_settings row exists for this user
|
||||
await supabase
|
||||
.from('notification_settings')
|
||||
.upsert(
|
||||
{
|
||||
user_id: ctx.userId,
|
||||
period_locked_enabled: DEFAULT_SETTINGS.periodLockedEnabled,
|
||||
period_year_closed_enabled: DEFAULT_SETTINGS.periodYearClosedEnabled,
|
||||
invoice_sent_enabled: DEFAULT_SETTINGS.invoiceSentEnabled,
|
||||
receipt_extracted_enabled: DEFAULT_SETTINGS.receiptExtractedEnabled,
|
||||
receipt_matched_enabled: DEFAULT_SETTINGS.receiptMatchedEnabled,
|
||||
},
|
||||
{ onConflict: 'user_id' }
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -21,6 +21,11 @@ export interface NotificationSettings {
|
||||
quiet_end: string // time format "HH:MM"
|
||||
email_enabled: boolean
|
||||
push_enabled: boolean
|
||||
period_locked_enabled: boolean
|
||||
period_year_closed_enabled: boolean
|
||||
invoice_sent_enabled: boolean
|
||||
receipt_extracted_enabled: boolean
|
||||
receipt_matched_enabled: boolean
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
@@ -174,6 +174,8 @@ async function handleDocumentUploaded(
|
||||
: null,
|
||||
extraction_confidence: item.confidence,
|
||||
suggested_category: item.suggestedCategory,
|
||||
category: item.category,
|
||||
bas_account: item.basAccount,
|
||||
sort_order: index,
|
||||
}))
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { createJournalEntry, findFiscalPeriod } from './engine'
|
||||
import { generateSalesVatLines, generateReverseChargeLines } from './vat-entries'
|
||||
import { getVatTreatmentForRate } from '@/lib/invoice/vat-rules'
|
||||
import { getVatTreatmentForRate } from '@/lib/invoices/vat-rules'
|
||||
import type {
|
||||
CreateJournalEntryInput,
|
||||
CreateJournalEntryLineInput,
|
||||
|
||||
@@ -159,9 +159,14 @@ describe('generateBalanceSheet', () => {
|
||||
const report = await generateBalanceSheet('user-1', 'period-1')
|
||||
|
||||
expect(report.asset_sections).toHaveLength(1) // Only 1930
|
||||
expect(report.equity_liability_sections).toEqual([])
|
||||
// Class 3-8 accounts are not included as balance sheet rows, but their
|
||||
// net result (credit - debit = 40000 + 500 - 8000 = 32500) appears as
|
||||
// "Årets resultat" in equity so the balance sheet can balance.
|
||||
expect(report.equity_liability_sections).toHaveLength(1)
|
||||
expect(report.equity_liability_sections[0].title).toBe('Årets resultat')
|
||||
expect(report.equity_liability_sections[0].subtotal).toBe(32500)
|
||||
expect(report.total_assets).toBe(50000)
|
||||
expect(report.total_equity_liabilities).toBe(0)
|
||||
expect(report.total_equity_liabilities).toBe(32500)
|
||||
})
|
||||
|
||||
it('uses Math.round for monetary precision on subtotals', async () => {
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
-- Migration 033: AI Chat Schema
|
||||
-- Creates tables for the AI chat assistant extension:
|
||||
-- chat_sessions, chat_messages, knowledge_documents, and match_documents RPC
|
||||
|
||||
-- Enable pgvector for embedding storage
|
||||
create extension if not exists vector with schema extensions;
|
||||
|
||||
-- ============================================================
|
||||
-- chat_sessions
|
||||
-- ============================================================
|
||||
|
||||
create table public.chat_sessions (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
user_id uuid references auth.users on delete cascade not null,
|
||||
title text,
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now()
|
||||
);
|
||||
|
||||
alter table public.chat_sessions enable row level security;
|
||||
|
||||
create policy "chat_sessions_select" on public.chat_sessions
|
||||
for select using (auth.uid() = user_id);
|
||||
create policy "chat_sessions_insert" on public.chat_sessions
|
||||
for insert with check (auth.uid() = user_id);
|
||||
create policy "chat_sessions_update" on public.chat_sessions
|
||||
for update using (auth.uid() = user_id);
|
||||
create policy "chat_sessions_delete" on public.chat_sessions
|
||||
for delete using (auth.uid() = user_id);
|
||||
|
||||
create index idx_chat_sessions_user_created on public.chat_sessions (user_id, created_at desc);
|
||||
|
||||
create trigger chat_sessions_updated_at
|
||||
before update on public.chat_sessions
|
||||
for each row execute function public.update_updated_at_column();
|
||||
|
||||
-- ============================================================
|
||||
-- chat_messages
|
||||
-- ============================================================
|
||||
|
||||
create table public.chat_messages (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
session_id uuid references public.chat_sessions on delete cascade not null,
|
||||
user_id uuid references auth.users on delete cascade not null,
|
||||
role text not null check (role in ('user', 'assistant')),
|
||||
content text not null,
|
||||
sources jsonb,
|
||||
created_at timestamptz not null default now()
|
||||
);
|
||||
|
||||
alter table public.chat_messages enable row level security;
|
||||
|
||||
create policy "chat_messages_select" on public.chat_messages
|
||||
for select using (auth.uid() = user_id);
|
||||
create policy "chat_messages_insert" on public.chat_messages
|
||||
for insert with check (auth.uid() = user_id);
|
||||
create policy "chat_messages_update" on public.chat_messages
|
||||
for update using (auth.uid() = user_id);
|
||||
create policy "chat_messages_delete" on public.chat_messages
|
||||
for delete using (auth.uid() = user_id);
|
||||
|
||||
create index idx_chat_messages_session on public.chat_messages (session_id, created_at);
|
||||
|
||||
-- ============================================================
|
||||
-- knowledge_documents
|
||||
-- ============================================================
|
||||
|
||||
create table public.knowledge_documents (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
source_file text not null,
|
||||
title text not null,
|
||||
section_title text,
|
||||
content text not null,
|
||||
content_hash text unique not null,
|
||||
embedding extensions.vector(1536),
|
||||
metadata jsonb default '{}',
|
||||
created_at timestamptz not null default now()
|
||||
);
|
||||
|
||||
alter table public.knowledge_documents enable row level security;
|
||||
|
||||
-- Knowledge documents are shared — any authenticated user can read
|
||||
create policy "knowledge_documents_select" on public.knowledge_documents
|
||||
for select using (true);
|
||||
|
||||
create index idx_knowledge_documents_hash on public.knowledge_documents (content_hash);
|
||||
|
||||
-- ============================================================
|
||||
-- match_documents RPC (vector similarity search)
|
||||
-- ============================================================
|
||||
|
||||
create or replace function public.match_documents(
|
||||
query_embedding extensions.vector,
|
||||
match_count int default 5,
|
||||
match_threshold float default 0.7
|
||||
)
|
||||
returns table (
|
||||
id uuid,
|
||||
source_file text,
|
||||
title text,
|
||||
section_title text,
|
||||
content text,
|
||||
metadata jsonb,
|
||||
similarity float
|
||||
)
|
||||
language plpgsql
|
||||
security definer
|
||||
set search_path = public, extensions
|
||||
as $$
|
||||
begin
|
||||
return query
|
||||
select
|
||||
kd.id,
|
||||
kd.source_file,
|
||||
kd.title,
|
||||
kd.section_title,
|
||||
kd.content,
|
||||
kd.metadata,
|
||||
1 - (kd.embedding <=> query_embedding)::float as similarity
|
||||
from public.knowledge_documents kd
|
||||
where 1 - (kd.embedding <=> query_embedding) >= match_threshold
|
||||
order by kd.embedding <=> query_embedding
|
||||
limit match_count;
|
||||
end;
|
||||
$$;
|
||||
|
||||
grant execute on function public.match_documents(extensions.vector, int, float) to authenticated;
|
||||
@@ -0,0 +1,19 @@
|
||||
-- Migration 034: Fix extension_data updated_at trigger
|
||||
-- The original trigger references update_updated_at() which does not exist.
|
||||
-- The correct function is public.update_updated_at_column().
|
||||
-- Wrapped in DO block in case extension_data table does not yet exist.
|
||||
|
||||
do $$
|
||||
begin
|
||||
if exists (
|
||||
select 1 from information_schema.tables
|
||||
where table_schema = 'public' and table_name = 'extension_data'
|
||||
) then
|
||||
drop trigger if exists extension_data_updated_at on public.extension_data;
|
||||
|
||||
create trigger extension_data_updated_at
|
||||
before update on public.extension_data
|
||||
for each row execute function public.update_updated_at_column();
|
||||
end if;
|
||||
end;
|
||||
$$;
|
||||
@@ -0,0 +1,28 @@
|
||||
-- Migration 035: Fix push notifications
|
||||
-- 1. Expand notification_log notification_type CHECK constraint to include new event types
|
||||
-- 2. Add per-event enabled columns to notification_settings
|
||||
|
||||
-- Drop and recreate the CHECK constraint with new types
|
||||
alter table public.notification_log
|
||||
drop constraint if exists notification_log_notification_type_check;
|
||||
|
||||
alter table public.notification_log
|
||||
add constraint notification_log_notification_type_check
|
||||
check (notification_type in (
|
||||
'tax_deadline',
|
||||
'invoice_due',
|
||||
'invoice_overdue',
|
||||
'period_locked',
|
||||
'period_year_closed',
|
||||
'invoice_sent',
|
||||
'receipt_extracted',
|
||||
'receipt_matched'
|
||||
));
|
||||
|
||||
-- Add new per-event enabled columns to notification_settings
|
||||
alter table public.notification_settings
|
||||
add column if not exists period_locked_enabled boolean default true,
|
||||
add column if not exists period_year_closed_enabled boolean default true,
|
||||
add column if not exists invoice_sent_enabled boolean default false,
|
||||
add column if not exists receipt_extracted_enabled boolean default true,
|
||||
add column if not exists receipt_matched_enabled boolean default true;
|
||||
@@ -0,0 +1,5 @@
|
||||
-- Migration 036: Fix Enable Banking
|
||||
-- Add authorization_id column to bank_connections for PSD2 authorization tracking
|
||||
|
||||
alter table public.bank_connections
|
||||
add column if not exists authorization_id text;
|
||||
@@ -0,0 +1,37 @@
|
||||
-- Migration 038: Fix match_documents search_path
|
||||
-- The function needs the extensions schema in search_path to use pgvector operators.
|
||||
|
||||
create or replace function public.match_documents(
|
||||
query_embedding extensions.vector,
|
||||
match_count int default 5,
|
||||
match_threshold float default 0.7
|
||||
)
|
||||
returns table (
|
||||
id uuid,
|
||||
source_file text,
|
||||
title text,
|
||||
section_title text,
|
||||
content text,
|
||||
metadata jsonb,
|
||||
similarity float
|
||||
)
|
||||
language plpgsql
|
||||
security definer
|
||||
set search_path = public, extensions
|
||||
as $$
|
||||
begin
|
||||
return query
|
||||
select
|
||||
kd.id,
|
||||
kd.source_file,
|
||||
kd.title,
|
||||
kd.section_title,
|
||||
kd.content,
|
||||
kd.metadata,
|
||||
1 - (kd.embedding <=> query_embedding)::float as similarity
|
||||
from public.knowledge_documents kd
|
||||
where 1 - (kd.embedding <=> query_embedding) >= match_threshold
|
||||
order by kd.embedding <=> query_embedding
|
||||
limit match_count;
|
||||
end;
|
||||
$$;
|
||||
Reference in New Issue
Block a user