Add hybrid research copilot workspace

This commit is contained in:
2026-03-14 19:32:00 -04:00
parent 7a42d73a48
commit 2ee9a549a3
27 changed files with 2864 additions and 323 deletions

View File

@@ -77,7 +77,6 @@ function loadSqliteExtensions(client: Database) {
function isVectorExtensionLoaded(client: Database) {
return vectorExtensionStatus.get(client) ?? false;
}
function ensureSearchVirtualTables(client: Database) {
client.exec(`
CREATE VIRTUAL TABLE IF NOT EXISTS \`search_chunk_fts\` USING fts5(

View File

@@ -44,10 +44,12 @@ type ResearchMemoSection =
| 'risks'
| 'disconfirming_evidence'
| 'next_actions';
type SearchSource = 'documents' | 'filings' | 'research';
type FinancialCadence = 'annual' | 'quarterly' | 'ltm';
type SearchDocumentScope = 'global' | 'user';
type SearchDocumentSourceKind = 'filing_document' | 'filing_brief' | 'research_note';
type SearchIndexStatus = 'pending' | 'indexed' | 'failed';
type ResearchCopilotMessageRole = 'user' | 'assistant';
type FinancialSurfaceKind =
| 'income_statement'
| 'balance_sheet'
@@ -636,7 +638,7 @@ export const filingLink = sqliteTable('filing_link', {
export const taskRun = sqliteTable('task_run', {
id: text('id').primaryKey().notNull(),
user_id: text('user_id').notNull().references(() => user.id, { onDelete: 'cascade' }),
task_type: text('task_type').$type<'sync_filings' | 'refresh_prices' | 'analyze_filing' | 'portfolio_insights' | 'index_search'>().notNull(),
task_type: text('task_type').$type<'sync_filings' | 'refresh_prices' | 'analyze_filing' | 'portfolio_insights' | 'index_search' | 'research_brief'>().notNull(),
status: text('status').$type<'queued' | 'running' | 'completed' | 'failed'>().notNull(),
stage: text('stage').notNull(),
stage_detail: text('stage_detail'),
@@ -824,6 +826,38 @@ export const researchMemoEvidence = sqliteTable('research_memo_evidence', {
researchMemoEvidenceUnique: uniqueIndex('research_memo_evidence_unique_uidx').on(table.memo_id, table.artifact_id, table.section)
}));
export const researchCopilotSession = sqliteTable('research_copilot_session', {
id: integer('id').primaryKey({ autoIncrement: true }),
user_id: text('user_id').notNull().references(() => user.id, { onDelete: 'cascade' }),
ticker: text('ticker').notNull(),
title: text('title'),
selected_sources: text('selected_sources', { mode: 'json' }).$type<SearchSource[]>().notNull(),
pinned_artifact_ids: text('pinned_artifact_ids', { mode: 'json' }).$type<number[]>().notNull(),
created_at: text('created_at').notNull(),
updated_at: text('updated_at').notNull()
}, (table) => ({
researchCopilotSessionTickerUnique: uniqueIndex('research_copilot_session_ticker_uidx').on(table.user_id, table.ticker),
researchCopilotSessionUpdatedIndex: index('research_copilot_session_updated_idx').on(table.user_id, table.updated_at)
}));
export const researchCopilotMessage = sqliteTable('research_copilot_message', {
id: integer('id').primaryKey({ autoIncrement: true }),
session_id: integer('session_id').notNull().references(() => researchCopilotSession.id, { onDelete: 'cascade' }),
user_id: text('user_id').notNull().references(() => user.id, { onDelete: 'cascade' }),
role: text('role').$type<ResearchCopilotMessageRole>().notNull(),
content_markdown: text('content_markdown').notNull(),
citations: text('citations', { mode: 'json' }).$type<Record<string, unknown>[] | null>(),
follow_ups: text('follow_ups', { mode: 'json' }).$type<string[] | null>(),
suggested_actions: text('suggested_actions', { mode: 'json' }).$type<Record<string, unknown>[] | null>(),
selected_sources: text('selected_sources', { mode: 'json' }).$type<SearchSource[] | null>(),
pinned_artifact_ids: text('pinned_artifact_ids', { mode: 'json' }).$type<number[] | null>(),
memo_section: text('memo_section').$type<ResearchMemoSection | null>(),
created_at: text('created_at').notNull()
}, (table) => ({
researchCopilotMessageSessionIndex: index('research_copilot_message_session_idx').on(table.session_id, table.created_at),
researchCopilotMessageUserIndex: index('research_copilot_message_user_idx').on(table.user_id, table.created_at)
}));
export const authSchema = {
user,
session,
@@ -855,7 +889,9 @@ export const appSchema = {
searchChunk,
researchArtifact,
researchMemo,
researchMemoEvidence
researchMemoEvidence,
researchCopilotSession,
researchCopilotMessage
};
export const schema = {

View File

@@ -296,6 +296,50 @@ function ensureResearchWorkspaceSchema(client: Database) {
`);
}
function ensureResearchCopilotSchema(client: Database) {
if (!hasTable(client, 'research_copilot_session')) {
client.exec(`
CREATE TABLE IF NOT EXISTS \`research_copilot_session\` (
\`id\` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
\`user_id\` text NOT NULL,
\`ticker\` text NOT NULL,
\`title\` text,
\`selected_sources\` text NOT NULL DEFAULT '["documents","filings","research"]',
\`pinned_artifact_ids\` text NOT NULL DEFAULT '[]',
\`created_at\` text NOT NULL,
\`updated_at\` text NOT NULL,
FOREIGN KEY (\`user_id\`) REFERENCES \`user\`(\`id\`) ON UPDATE no action ON DELETE cascade
);
`);
}
if (!hasTable(client, 'research_copilot_message')) {
client.exec(`
CREATE TABLE IF NOT EXISTS \`research_copilot_message\` (
\`id\` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
\`session_id\` integer NOT NULL,
\`user_id\` text NOT NULL,
\`role\` text NOT NULL,
\`content_markdown\` text NOT NULL,
\`citations\` text,
\`follow_ups\` text,
\`suggested_actions\` text,
\`selected_sources\` text,
\`pinned_artifact_ids\` text,
\`memo_section\` text,
\`created_at\` text NOT NULL,
FOREIGN KEY (\`session_id\`) REFERENCES \`research_copilot_session\`(\`id\`) ON UPDATE no action ON DELETE cascade,
FOREIGN KEY (\`user_id\`) REFERENCES \`user\`(\`id\`) ON UPDATE no action ON DELETE cascade
);
`);
}
client.exec('CREATE UNIQUE INDEX IF NOT EXISTS `research_copilot_session_ticker_uidx` ON `research_copilot_session` (`user_id`, `ticker`);');
client.exec('CREATE INDEX IF NOT EXISTS `research_copilot_session_updated_idx` ON `research_copilot_session` (`user_id`, `updated_at`);');
client.exec('CREATE INDEX IF NOT EXISTS `research_copilot_message_session_idx` ON `research_copilot_message` (`session_id`, `created_at`);');
client.exec('CREATE INDEX IF NOT EXISTS `research_copilot_message_user_idx` ON `research_copilot_message` (`user_id`, `created_at`);');
}
const TAXONOMY_SNAPSHOT_REQUIRED_COLUMNS = [
'parser_engine',
'parser_version',
@@ -548,6 +592,7 @@ WHERE resource_key IS NOT NULL AND status IN ('queued', 'running');`);
}
ensureResearchWorkspaceSchema(client);
ensureResearchCopilotSchema(client);
}
export const __sqliteSchemaCompatInternals = {