mirror of
https://github.com/documenso/documenso
synced 2026-04-21 13:27:18 +00:00
Use Gemini to handle detection of recipients and fields within documents. Opt in using organisation or team settings. Replaces #2128 since the branch was cursed and would include dependencies that weren't even in the lock file. https://github.com/user-attachments/assets/e6cbb58f-62b9-4079-a9ae-7af5c4f2e4ec
150 lines
4.4 KiB
TypeScript
150 lines
4.4 KiB
TypeScript
import { sValidator } from '@hono/standard-validator';
|
|
import { Hono } from 'hono';
|
|
import { streamText } from 'hono/streaming';
|
|
|
|
import { getSession } from '@documenso/auth/server/lib/utils/get-session';
|
|
import { IS_AI_FEATURES_CONFIGURED } from '@documenso/lib/constants/app';
|
|
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
|
import { detectFieldsFromEnvelope } from '@documenso/lib/server-only/ai/envelope/detect-fields';
|
|
import { getTeamById } from '@documenso/lib/server-only/team/get-team';
|
|
|
|
import type { HonoEnv } from '../../router';
|
|
import { ZDetectFieldsRequestSchema } from './detect-fields.types';
|
|
|
|
const KEEPALIVE_INTERVAL_MS = 5000;
|
|
|
|
export const detectFieldsRoute = new Hono<HonoEnv>().post(
|
|
'/',
|
|
sValidator('json', ZDetectFieldsRequestSchema),
|
|
async (c) => {
|
|
const logger = c.get('logger');
|
|
|
|
try {
|
|
const { envelopeId, teamId, context } = c.req.valid('json');
|
|
|
|
const session = await getSession(c);
|
|
|
|
if (!session.user) {
|
|
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
|
message: 'You must be logged in to detect fields',
|
|
});
|
|
}
|
|
|
|
// Verify user has access to the team (abort early)
|
|
const team = await getTeamById({
|
|
userId: session.user.id,
|
|
teamId,
|
|
}).catch(() => null);
|
|
|
|
if (!team) {
|
|
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
|
message: 'You do not have access to this team',
|
|
});
|
|
}
|
|
|
|
// Check if AI features are enabled for the team
|
|
const { aiFeaturesEnabled } = team.derivedSettings;
|
|
|
|
if (!aiFeaturesEnabled) {
|
|
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
|
message: 'AI features are not enabled for this team',
|
|
});
|
|
}
|
|
|
|
if (!IS_AI_FEATURES_CONFIGURED()) {
|
|
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
|
message: 'AI features are not configured. Please contact support to enable AI features.',
|
|
});
|
|
}
|
|
|
|
logger.info({
|
|
event: 'ai.detect-fields.start',
|
|
envelopeId,
|
|
userId: session.user.id,
|
|
teamId: team.id,
|
|
hasContext: !!context,
|
|
});
|
|
|
|
// Return streaming response with NDJSON
|
|
return streamText(c, async (stream) => {
|
|
// Start keepalive to prevent connection timeout
|
|
let interval: NodeJS.Timeout | null = setInterval(() => {
|
|
void stream.writeln(JSON.stringify({ type: 'keepalive' }));
|
|
}, KEEPALIVE_INTERVAL_MS);
|
|
|
|
try {
|
|
const allFields = await detectFieldsFromEnvelope({
|
|
context,
|
|
envelopeId,
|
|
userId: session.user.id,
|
|
teamId: team.id,
|
|
onProgress: (progress) => {
|
|
void stream.writeln(
|
|
JSON.stringify({
|
|
type: 'progress',
|
|
pagesProcessed: progress.pagesProcessed,
|
|
totalPages: progress.totalPages,
|
|
fieldsDetected: progress.fieldsDetected,
|
|
}),
|
|
);
|
|
},
|
|
});
|
|
|
|
// Clear keepalive before sending final response
|
|
if (interval) {
|
|
clearInterval(interval);
|
|
interval = null;
|
|
}
|
|
|
|
logger.info({
|
|
event: 'ai.detect-fields.complete',
|
|
envelopeId,
|
|
userId: session.user.id,
|
|
teamId: team.id,
|
|
fieldCount: allFields.length,
|
|
});
|
|
|
|
await stream.writeln(
|
|
JSON.stringify({
|
|
type: 'complete',
|
|
fields: allFields,
|
|
}),
|
|
);
|
|
} catch (error) {
|
|
// Clear keepalive on error
|
|
if (interval) {
|
|
clearInterval(interval);
|
|
}
|
|
|
|
logger.error({
|
|
event: 'ai.detect-fields.error',
|
|
error,
|
|
});
|
|
|
|
const message = error instanceof AppError ? error.message : 'Failed to detect fields';
|
|
|
|
await stream.writeln(
|
|
JSON.stringify({
|
|
type: 'error',
|
|
message,
|
|
}),
|
|
);
|
|
}
|
|
});
|
|
} catch (error) {
|
|
// Handle errors that occur before streaming starts
|
|
logger.error({
|
|
event: 'ai.detect-fields.error',
|
|
error,
|
|
});
|
|
|
|
if (error instanceof AppError) {
|
|
const { status, body } = AppError.toRestAPIError(error);
|
|
|
|
return c.json(body, status);
|
|
}
|
|
|
|
return c.json({ error: 'Failed to detect fields' }, 500);
|
|
}
|
|
},
|
|
);
|