Add hybrid research copilot workspace
This commit is contained in:
@@ -10,6 +10,7 @@ import type {
|
||||
import { runAiAnalysis } from '@/lib/server/ai';
|
||||
import { buildPortfolioSummary } from '@/lib/server/portfolio';
|
||||
import { getQuote } from '@/lib/server/prices';
|
||||
import { generateResearchBrief } from '@/lib/server/research-copilot';
|
||||
import { indexSearchDocuments } from '@/lib/server/search';
|
||||
import {
|
||||
getFilingByAccession,
|
||||
@@ -32,7 +33,9 @@ import {
|
||||
listUserHoldings
|
||||
} from '@/lib/server/repos/holdings';
|
||||
import { createPortfolioInsight } from '@/lib/server/repos/insights';
|
||||
import { createResearchArtifactRecord } from '@/lib/server/repos/research-library';
|
||||
import { updateTaskStage } from '@/lib/server/repos/tasks';
|
||||
import { updateWatchlistReviewByTicker } from '@/lib/server/repos/watchlist';
|
||||
import {
|
||||
fetchPrimaryFilingText,
|
||||
fetchRecentFilings
|
||||
@@ -1302,6 +1305,97 @@ async function processPortfolioInsights(task: Task) {
|
||||
);
|
||||
}
|
||||
|
||||
async function processResearchBrief(task: Task) {
|
||||
const ticker = typeof task.payload.ticker === 'string' ? task.payload.ticker.trim().toUpperCase() : '';
|
||||
const query = typeof task.payload.query === 'string' ? task.payload.query.trim() : '';
|
||||
const sources = Array.isArray(task.payload.sources)
|
||||
? task.payload.sources.filter((entry): entry is 'documents' | 'filings' | 'research' => entry === 'documents' || entry === 'filings' || entry === 'research')
|
||||
: undefined;
|
||||
|
||||
if (!ticker) {
|
||||
throw new Error('Research brief task requires a ticker');
|
||||
}
|
||||
|
||||
if (!query) {
|
||||
throw new Error('Research brief task requires a query');
|
||||
}
|
||||
|
||||
await setProjectionStage(task, 'research.retrieve', `Collecting evidence for ${ticker} research brief`, {
|
||||
subject: { ticker },
|
||||
progress: { current: 1, total: 3, unit: 'steps' }
|
||||
});
|
||||
|
||||
const brief = await generateResearchBrief({
|
||||
userId: task.user_id,
|
||||
ticker,
|
||||
query,
|
||||
selectedSources: sources
|
||||
});
|
||||
|
||||
await setProjectionStage(task, 'research.answer', `Generating research brief for ${ticker}`, {
|
||||
subject: { ticker },
|
||||
progress: { current: 2, total: 3, unit: 'steps' },
|
||||
counters: {
|
||||
evidence: brief.evidence.length
|
||||
}
|
||||
});
|
||||
|
||||
const summary = brief.bodyMarkdown
|
||||
.split('\n')
|
||||
.map((line) => line.trim())
|
||||
.find((line) => line.length > 0 && !line.startsWith('#'))
|
||||
?? `Generated research brief for ${ticker}.`;
|
||||
|
||||
await setProjectionStage(task, 'research.persist', `Saving research brief artifact for ${ticker}`, {
|
||||
subject: { ticker },
|
||||
progress: { current: 3, total: 3, unit: 'steps' },
|
||||
counters: {
|
||||
evidence: brief.evidence.length
|
||||
}
|
||||
});
|
||||
|
||||
const artifact = await createResearchArtifactRecord({
|
||||
userId: task.user_id,
|
||||
ticker,
|
||||
kind: 'ai_report',
|
||||
source: 'system',
|
||||
subtype: 'research_brief',
|
||||
title: `Research brief · ${query}`,
|
||||
summary,
|
||||
bodyMarkdown: brief.bodyMarkdown,
|
||||
tags: ['copilot', 'research-brief'],
|
||||
metadata: {
|
||||
query,
|
||||
sources: sources ?? ['documents', 'filings', 'research'],
|
||||
provider: brief.provider,
|
||||
model: brief.model,
|
||||
citations: brief.evidence.map((result, index) => ({
|
||||
index: index + 1,
|
||||
label: result.citationLabel,
|
||||
href: result.href
|
||||
}))
|
||||
}
|
||||
});
|
||||
|
||||
await updateWatchlistReviewByTicker(task.user_id, ticker, artifact.updated_at);
|
||||
|
||||
return buildTaskOutcome(
|
||||
{
|
||||
ticker,
|
||||
artifactId: artifact.id,
|
||||
provider: brief.provider,
|
||||
model: brief.model
|
||||
},
|
||||
`Generated research brief artifact for ${ticker}.`,
|
||||
{
|
||||
subject: { ticker },
|
||||
counters: {
|
||||
evidence: brief.evidence.length
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export const __taskProcessorInternals = {
|
||||
parseExtractionPayload,
|
||||
deterministicExtractionFallback,
|
||||
@@ -1320,6 +1414,8 @@ export async function runTaskProcessor(task: Task) {
|
||||
return await processPortfolioInsights(task);
|
||||
case 'index_search':
|
||||
return await processIndexSearch(task);
|
||||
case 'research_brief':
|
||||
return await processResearchBrief(task);
|
||||
default:
|
||||
throw new Error(`Unsupported task type: ${task.task_type}`);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user