Add hybrid research copilot workspace
This commit is contained in:
@@ -21,6 +21,7 @@ import { requireAuthenticatedSession } from '@/lib/server/auth-session';
|
||||
import { getLatestFinancialIngestionSchemaStatus } from '@/lib/server/db/financial-ingestion-schema';
|
||||
import { asErrorMessage, jsonError } from '@/lib/server/http';
|
||||
import { buildPortfolioSummary } from '@/lib/server/portfolio';
|
||||
import { runResearchCopilotTurn } from '@/lib/server/research-copilot';
|
||||
import {
|
||||
defaultFinancialSyncLimit,
|
||||
getCompanyFinancials
|
||||
@@ -61,6 +62,7 @@ import {
|
||||
listResearchJournalEntries,
|
||||
updateResearchJournalEntryRecord
|
||||
} from '@/lib/server/repos/research-journal';
|
||||
import { getResearchCopilotSessionByTicker } from '@/lib/server/repos/research-copilot';
|
||||
import {
|
||||
deleteWatchlistItemRecord,
|
||||
getWatchlistItemById,
|
||||
@@ -839,6 +841,116 @@ export const app = new Elysia({ prefix: '/api' })
|
||||
ticker: t.String({ minLength: 1 })
|
||||
})
|
||||
})
|
||||
.get('/research/copilot/session', async ({ query }) => {
|
||||
const { session, response } = await requireAuthenticatedSession();
|
||||
if (response) {
|
||||
return response;
|
||||
}
|
||||
|
||||
const ticker = typeof query.ticker === 'string' ? query.ticker.trim().toUpperCase() : '';
|
||||
if (!ticker) {
|
||||
return jsonError('ticker is required');
|
||||
}
|
||||
|
||||
const copilotSession = await getResearchCopilotSessionByTicker(session.user.id, ticker);
|
||||
return Response.json({ session: copilotSession });
|
||||
}, {
|
||||
query: t.Object({
|
||||
ticker: t.String({ minLength: 1 })
|
||||
})
|
||||
})
|
||||
.post('/research/copilot/turn', async ({ body }) => {
|
||||
const { session, response } = await requireAuthenticatedSession();
|
||||
if (response) {
|
||||
return response;
|
||||
}
|
||||
|
||||
const payload = asRecord(body);
|
||||
const ticker = typeof payload.ticker === 'string' ? payload.ticker.trim().toUpperCase() : '';
|
||||
const query = typeof payload.query === 'string' ? payload.query.trim() : '';
|
||||
const memoSection = asResearchMemoSection(payload.memoSection);
|
||||
|
||||
if (!ticker) {
|
||||
return jsonError('ticker is required');
|
||||
}
|
||||
|
||||
if (!query) {
|
||||
return jsonError('query is required');
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await runResearchCopilotTurn({
|
||||
userId: session.user.id,
|
||||
ticker,
|
||||
query,
|
||||
selectedSources: asSearchSources(payload.sources),
|
||||
pinnedArtifactIds: Array.isArray(payload.pinnedArtifactIds)
|
||||
? payload.pinnedArtifactIds.map((entry) => Number(entry)).filter((entry) => Number.isInteger(entry) && entry > 0)
|
||||
: undefined,
|
||||
memoSection
|
||||
});
|
||||
|
||||
return Response.json(result);
|
||||
} catch (error) {
|
||||
return jsonError(asErrorMessage(error, 'Unable to run research copilot turn'));
|
||||
}
|
||||
}, {
|
||||
body: t.Object({
|
||||
ticker: t.String({ minLength: 1 }),
|
||||
query: t.String({ minLength: 1 }),
|
||||
sources: t.Optional(t.Union([t.String(), t.Array(t.String())])),
|
||||
pinnedArtifactIds: t.Optional(t.Array(t.Numeric())),
|
||||
memoSection: t.Optional(t.String())
|
||||
})
|
||||
})
|
||||
.post('/research/copilot/job', async ({ body }) => {
|
||||
const { session, response } = await requireAuthenticatedSession();
|
||||
if (response) {
|
||||
return response;
|
||||
}
|
||||
|
||||
const payload = asRecord(body);
|
||||
const ticker = typeof payload.ticker === 'string' ? payload.ticker.trim().toUpperCase() : '';
|
||||
const query = typeof payload.query === 'string' ? payload.query.trim() : '';
|
||||
|
||||
if (!ticker) {
|
||||
return jsonError('ticker is required');
|
||||
}
|
||||
|
||||
if (!query) {
|
||||
return jsonError('query is required');
|
||||
}
|
||||
|
||||
try {
|
||||
const resourceKey = `research_brief:${ticker}:${query.toLowerCase()}`;
|
||||
const existing = await findInFlightTask(session.user.id, 'research_brief', resourceKey);
|
||||
if (existing) {
|
||||
return Response.json({ task: existing });
|
||||
}
|
||||
|
||||
const task = await enqueueTask({
|
||||
userId: session.user.id,
|
||||
taskType: 'research_brief',
|
||||
payload: {
|
||||
ticker,
|
||||
query,
|
||||
sources: asSearchSources(payload.sources) ?? SEARCH_SOURCES
|
||||
},
|
||||
priority: 55,
|
||||
resourceKey
|
||||
});
|
||||
|
||||
return Response.json({ task });
|
||||
} catch (error) {
|
||||
return jsonError(asErrorMessage(error, 'Unable to queue research brief'));
|
||||
}
|
||||
}, {
|
||||
body: t.Object({
|
||||
ticker: t.String({ minLength: 1 }),
|
||||
query: t.String({ minLength: 1 }),
|
||||
sources: t.Optional(t.Union([t.String(), t.Array(t.String())]))
|
||||
})
|
||||
})
|
||||
.get('/research/library', async ({ query }) => {
|
||||
const { session, response } = await requireAuthenticatedSession();
|
||||
if (response) {
|
||||
|
||||
Reference in New Issue
Block a user