Add hybrid research copilot workspace
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
Reference in New Issue
Block a user