Add search and RAG workspace flows
This commit is contained in:
@@ -8,6 +8,7 @@ import type {
|
||||
FinancialStatementKind,
|
||||
FinancialSurfaceKind,
|
||||
ResearchJournalEntryType,
|
||||
SearchSource,
|
||||
TaskStatus
|
||||
} from '@/lib/types';
|
||||
import { auth } from '@/lib/auth';
|
||||
@@ -48,6 +49,7 @@ import {
|
||||
upsertWatchlistItemRecord
|
||||
} from '@/lib/server/repos/watchlist';
|
||||
import { getPriceHistory, getQuote } from '@/lib/server/prices';
|
||||
import { answerSearchQuery, searchKnowledgeBase } from '@/lib/server/search';
|
||||
import {
|
||||
enqueueTask,
|
||||
findInFlightTask,
|
||||
@@ -82,6 +84,7 @@ const FINANCIAL_SURFACES: FinancialSurfaceKind[] = [
|
||||
const COVERAGE_STATUSES: CoverageStatus[] = ['backlog', 'active', 'watch', 'archive'];
|
||||
const COVERAGE_PRIORITIES: CoveragePriority[] = ['low', 'medium', 'high'];
|
||||
const JOURNAL_ENTRY_TYPES: ResearchJournalEntryType[] = ['note', 'filing_note', 'status_change'];
|
||||
const SEARCH_SOURCES: SearchSource[] = ['documents', 'filings', 'research'];
|
||||
|
||||
function asRecord(value: unknown): Record<string, unknown> {
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
||||
@@ -205,6 +208,21 @@ function asJournalEntryType(value: unknown) {
|
||||
: undefined;
|
||||
}
|
||||
|
||||
function asSearchSources(value: unknown) {
|
||||
const raw = Array.isArray(value)
|
||||
? value
|
||||
: typeof value === 'string'
|
||||
? value.split(',')
|
||||
: [];
|
||||
|
||||
const normalized = raw
|
||||
.filter((entry): entry is string => typeof entry === 'string')
|
||||
.map((entry) => entry.trim().toLowerCase())
|
||||
.filter((entry): entry is SearchSource => SEARCH_SOURCES.includes(entry as SearchSource));
|
||||
|
||||
return normalized.length > 0 ? [...new Set(normalized)] : undefined;
|
||||
}
|
||||
|
||||
function formatLabel(value: string) {
|
||||
return value
|
||||
.split('_')
|
||||
@@ -763,6 +781,21 @@ export const app = new Elysia({ prefix: '/api' })
|
||||
});
|
||||
|
||||
await updateWatchlistReviewByTicker(session.user.id, ticker, entry.updated_at);
|
||||
try {
|
||||
await enqueueTask({
|
||||
userId: session.user.id,
|
||||
taskType: 'index_search',
|
||||
payload: {
|
||||
ticker: entry.ticker,
|
||||
journalEntryId: entry.id,
|
||||
sourceKinds: ['research_note']
|
||||
},
|
||||
priority: 52,
|
||||
resourceKey: `index_search:research_note:${session.user.id}:${entry.id}`
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('[search-index-journal-create] failed:', error);
|
||||
}
|
||||
|
||||
return Response.json({ entry });
|
||||
} catch (error) {
|
||||
@@ -800,6 +833,21 @@ export const app = new Elysia({ prefix: '/api' })
|
||||
}
|
||||
|
||||
await updateWatchlistReviewByTicker(session.user.id, entry.ticker, entry.updated_at);
|
||||
try {
|
||||
await enqueueTask({
|
||||
userId: session.user.id,
|
||||
taskType: 'index_search',
|
||||
payload: {
|
||||
ticker: entry.ticker,
|
||||
journalEntryId: entry.id,
|
||||
sourceKinds: ['research_note']
|
||||
},
|
||||
priority: 52,
|
||||
resourceKey: `index_search:research_note:${session.user.id}:${entry.id}`
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('[search-index-journal-update] failed:', error);
|
||||
}
|
||||
|
||||
return Response.json({ entry });
|
||||
} catch (error) {
|
||||
@@ -822,6 +870,25 @@ export const app = new Elysia({ prefix: '/api' })
|
||||
return jsonError('Journal entry not found', 404);
|
||||
}
|
||||
|
||||
try {
|
||||
await enqueueTask({
|
||||
userId: session.user.id,
|
||||
taskType: 'index_search',
|
||||
payload: {
|
||||
deleteSourceRefs: [{
|
||||
sourceKind: 'research_note',
|
||||
sourceRef: String(numericId),
|
||||
scope: 'user',
|
||||
userId: session.user.id
|
||||
}]
|
||||
},
|
||||
priority: 52,
|
||||
resourceKey: `index_search:research_note:${session.user.id}:${numericId}:delete`
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('[search-index-journal-delete] failed:', error);
|
||||
}
|
||||
|
||||
return Response.json({ success: true });
|
||||
}, {
|
||||
params: t.Object({
|
||||
@@ -1124,6 +1191,63 @@ export const app = new Elysia({ prefix: '/api' })
|
||||
limit: t.Optional(t.Numeric())
|
||||
})
|
||||
})
|
||||
.get('/search', async ({ query }) => {
|
||||
const { session, response } = await requireAuthenticatedSession();
|
||||
if (response) {
|
||||
return response;
|
||||
}
|
||||
|
||||
const q = typeof query.q === 'string' ? query.q.trim() : '';
|
||||
if (q.length < 2) {
|
||||
return jsonError('q is required', 400);
|
||||
}
|
||||
|
||||
const results = await searchKnowledgeBase({
|
||||
userId: session.user.id,
|
||||
query: q,
|
||||
ticker: asOptionalString(query.ticker),
|
||||
sources: asSearchSources(query.sources),
|
||||
limit: typeof query.limit === 'number' ? query.limit : Number(query.limit)
|
||||
});
|
||||
|
||||
return Response.json({ results });
|
||||
}, {
|
||||
query: t.Object({
|
||||
q: t.String({ minLength: 2 }),
|
||||
ticker: t.Optional(t.String()),
|
||||
sources: t.Optional(t.Union([t.String(), t.Array(t.String())])),
|
||||
limit: t.Optional(t.Numeric())
|
||||
})
|
||||
})
|
||||
.post('/search/answer', async ({ body }) => {
|
||||
const { session, response } = await requireAuthenticatedSession();
|
||||
if (response) {
|
||||
return response;
|
||||
}
|
||||
|
||||
const payload = asRecord(body);
|
||||
const query = typeof payload.query === 'string' ? payload.query.trim() : '';
|
||||
if (query.length < 2) {
|
||||
return jsonError('query is required', 400);
|
||||
}
|
||||
|
||||
const answer = await answerSearchQuery({
|
||||
userId: session.user.id,
|
||||
query,
|
||||
ticker: asOptionalString(payload.ticker),
|
||||
sources: asSearchSources(payload.sources),
|
||||
limit: asPositiveNumber(payload.limit) ?? undefined
|
||||
});
|
||||
|
||||
return Response.json(answer);
|
||||
}, {
|
||||
body: t.Object({
|
||||
query: t.String({ minLength: 2 }),
|
||||
ticker: t.Optional(t.String()),
|
||||
sources: t.Optional(t.Union([t.String(), t.Array(t.String())])),
|
||||
limit: t.Optional(t.Numeric())
|
||||
})
|
||||
})
|
||||
.post('/filings/sync', async ({ body }) => {
|
||||
const { session, response } = await requireAuthenticatedSession();
|
||||
if (response) {
|
||||
|
||||
Reference in New Issue
Block a user