feat(email): suggest entity links with AI when opening messages
All checks were successful
Build and Push Docker Images / build-backend (push) Successful in 48s
Build and Push Docker Images / build-frontend (push) Successful in 1m15s
Build and Push Docker Images / build-website (push) Successful in 23s
Build and Push Docker Images / build-central-services-api (push) Successful in 21s
Build and Push Docker Images / build-central-services-admin (push) Successful in 20s
Build and Push Docker Images / build-docs (push) Successful in 1m17s
All checks were successful
Build and Push Docker Images / build-backend (push) Successful in 48s
Build and Push Docker Images / build-frontend (push) Successful in 1m15s
Build and Push Docker Images / build-website (push) Successful in 23s
Build and Push Docker Images / build-central-services-api (push) Successful in 21s
Build and Push Docker Images / build-central-services-admin (push) Successful in 20s
Build and Push Docker Images / build-docs (push) Successful in 1m17s
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import { suggestEmailEntities, type EntityCandidate, type EntitySuggestion } from "../modules/email/email.entity-suggestions"
|
||||
import nodemailer from "nodemailer"
|
||||
import { FastifyInstance } from "fastify"
|
||||
import { and, desc, eq, isNotNull } from "drizzle-orm"
|
||||
@@ -21,6 +22,7 @@ import { ImapFlow } from "imapflow"
|
||||
|
||||
export default async function emailAsUserRoutes(server: FastifyInstance) {
|
||||
const emailSync = emailSyncService(server)
|
||||
const pendingSuggestions = new Map<string, Promise<EntitySuggestion[]>>()
|
||||
|
||||
const encryptedValue = (value: unknown) => value ? decrypt(value as any) : null
|
||||
|
||||
@@ -555,6 +557,66 @@ export default async function emailAsUserRoutes(server: FastifyInstance) {
|
||||
}
|
||||
})
|
||||
|
||||
server.post("/email/messages/:id/entity-suggestions", async (req, reply) => {
|
||||
if (!req.user?.tenant_id) return reply.code(400).send({ error: "No tenant selected" })
|
||||
try {
|
||||
const { id } = req.params as { id: string }
|
||||
const tenantId = req.user.tenant_id
|
||||
const message = await emailSync.getMessage(tenantId, req.user.user_id, id)
|
||||
if (!message) return reply.code(404).send({ error: "E-Mail nicht gefunden" })
|
||||
const refresh = (req.body as { refresh?: boolean } | null)?.refresh === true
|
||||
const key = `${tenantId}:${req.user.user_id}:${id}`
|
||||
let suggestions = message.entitySuggestions
|
||||
if (!suggestions || refresh) {
|
||||
let pending = pendingSuggestions.get(key)
|
||||
if (!pending) {
|
||||
pending = (async () => {
|
||||
const candidates: EntityCandidate[] = []
|
||||
for (const [entityType, definition] of Object.entries(entityDefinitions)) {
|
||||
const table = definition.table
|
||||
const rows = await server.db.select({
|
||||
id: table.id, name: table.name,
|
||||
...("infoData" in table ? { infoData: table.infoData } : {}),
|
||||
...(entityType === "customers" ? { number: customers.customerNumber }
|
||||
: entityType === "vendors" ? { number: vendors.vendorNumber }
|
||||
: entityType === "projects" ? { number: projects.projectNumber } : {}),
|
||||
}).from(table).where(and(eq(definition.tenantColumn, tenantId), eq(table.archived, false)))
|
||||
for (const row of rows as any[]) {
|
||||
candidates.push({
|
||||
entityType: entityType as EmailEntityType, entityId: row.id,
|
||||
entityName: row.name, entityTypeLabel: definition.label, number: row.number,
|
||||
email: typeof row.infoData?.email === "string" ? row.infoData.email : null,
|
||||
invoiceEmail: typeof row.infoData?.invoiceEmail === "string" ? row.infoData.invoiceEmail : null,
|
||||
})
|
||||
}
|
||||
}
|
||||
const result = await suggestEmailEntities(message, candidates)
|
||||
await server.db.update(emailMessages).set({ entitySuggestions: result }).where(and(
|
||||
eq(emailMessages.id, id), eq(emailMessages.tenantId, tenantId),
|
||||
eq(emailMessages.userId, req.user.user_id),
|
||||
))
|
||||
return result
|
||||
})()
|
||||
pendingSuggestions.set(key, pending)
|
||||
void pending.finally(() => pendingSuggestions.delete(key)).catch(() => {})
|
||||
}
|
||||
suggestions = await pending
|
||||
}
|
||||
const links = await listMessageEntityLinks(tenantId, id)
|
||||
const resolved = await Promise.all(suggestions.map(async suggestion => {
|
||||
if (links.some(link => link.entityType === suggestion.entityType && link.entityId === suggestion.entityId)) return null
|
||||
const entity = await loadEntity(tenantId, suggestion.entityType, suggestion.entityId)
|
||||
return entity ? { ...suggestion, entityName: entity.name, entityTypeLabel: entity.typeLabel } : null
|
||||
}))
|
||||
return reply.send({ suggestions: resolved.filter(Boolean) })
|
||||
} catch (err: any) {
|
||||
req.log.error(err)
|
||||
return reply.code(err.statusCode === 503 ? 503 : 500).send({
|
||||
error: err.statusCode === 503 ? err.message : "KI-Vorschläge konnten nicht geladen werden. Bitte erneut versuchen.",
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
server.post("/email/messages/:id/entity-links", async (req, reply) => {
|
||||
try {
|
||||
if (!req.user?.tenant_id) {
|
||||
|
||||
Reference in New Issue
Block a user