feat(backend): implement step 8 — EntityScheduler pipeline orchestration
This commit is contained in:
@@ -0,0 +1,75 @@
|
|||||||
|
package com.condado.newsletter.scheduler
|
||||||
|
|
||||||
|
import com.condado.newsletter.model.DispatchLog
|
||||||
|
import com.condado.newsletter.model.DispatchStatus
|
||||||
|
import com.condado.newsletter.model.VirtualEntity
|
||||||
|
import com.condado.newsletter.repository.DispatchLogRepository
|
||||||
|
import com.condado.newsletter.service.AiService
|
||||||
|
import com.condado.newsletter.service.EmailReaderService
|
||||||
|
import com.condado.newsletter.service.EmailSenderService
|
||||||
|
import com.condado.newsletter.service.PromptBuilderService
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import org.springframework.beans.factory.annotation.Value
|
||||||
|
import org.springframework.scheduling.annotation.Scheduled
|
||||||
|
import org.springframework.scheduling.config.ScheduledTaskRegistrar
|
||||||
|
import org.springframework.scheduling.support.CronTrigger
|
||||||
|
import org.springframework.stereotype.Component
|
||||||
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers and manages per-entity scheduled tasks using [SchedulingConfigurer].
|
||||||
|
* Refreshes task registrations every minute to pick up changes to active entities.
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
class EntityScheduler(
|
||||||
|
private val emailReaderService: EmailReaderService,
|
||||||
|
private val promptBuilderService: PromptBuilderService,
|
||||||
|
private val aiService: AiService,
|
||||||
|
private val emailSenderService: EmailSenderService,
|
||||||
|
private val dispatchLogRepository: DispatchLogRepository,
|
||||||
|
@Value("\${app.recipients:}") val recipients: String,
|
||||||
|
@Value("\${imap.inbox-folder:INBOX}") val inboxFolder: String
|
||||||
|
) {
|
||||||
|
private val log = LoggerFactory.getLogger(javaClass)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs the full email generation + send pipeline for the given [entity].
|
||||||
|
* If the entity is inactive, returns immediately.
|
||||||
|
* Always persists a [DispatchLog] with SENT or FAILED status.
|
||||||
|
*/
|
||||||
|
fun runPipeline(entity: VirtualEntity) {
|
||||||
|
if (!entity.active) return
|
||||||
|
|
||||||
|
val recipientList = recipients.split(",")
|
||||||
|
.map { it.trim() }
|
||||||
|
.filter { it.isNotEmpty() }
|
||||||
|
|
||||||
|
try {
|
||||||
|
val emails = emailReaderService.readEmails(inboxFolder, entity.contextWindowDays)
|
||||||
|
val prompt = promptBuilderService.buildPrompt(entity, emails)
|
||||||
|
val aiResponse = aiService.generate(prompt)
|
||||||
|
emailSenderService.send(entity.email, recipientList, aiResponse.subject, aiResponse.body)
|
||||||
|
|
||||||
|
dispatchLogRepository.save(
|
||||||
|
DispatchLog(
|
||||||
|
virtualEntity = entity,
|
||||||
|
promptSent = prompt,
|
||||||
|
aiResponse = "${aiResponse.subject}\n${aiResponse.body}",
|
||||||
|
emailSubject = aiResponse.subject,
|
||||||
|
emailBody = aiResponse.body,
|
||||||
|
status = DispatchStatus.SENT
|
||||||
|
)
|
||||||
|
)
|
||||||
|
log.info("Pipeline succeeded for entity '${entity.name}'")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
log.error("Pipeline failed for entity '${entity.name}': ${e.message}", e)
|
||||||
|
dispatchLogRepository.save(
|
||||||
|
DispatchLog(
|
||||||
|
virtualEntity = entity,
|
||||||
|
status = DispatchStatus.FAILED,
|
||||||
|
errorMessage = e.message
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,15 +11,7 @@ import com.condado.newsletter.service.AiServiceException
|
|||||||
import com.condado.newsletter.service.EmailReaderService
|
import com.condado.newsletter.service.EmailReaderService
|
||||||
import com.condado.newsletter.service.EmailSenderService
|
import com.condado.newsletter.service.EmailSenderService
|
||||||
import com.condado.newsletter.service.PromptBuilderService
|
import com.condado.newsletter.service.PromptBuilderService
|
||||||
import io.mockk.called
|
import io.mockk.*
|
||||||
import io.mockk.capture
|
|
||||||
import io.mockk.every
|
|
||||||
import io.mockk.just
|
|
||||||
import io.mockk.mockk
|
|
||||||
import io.mockk.mutableListOf
|
|
||||||
import io.mockk.runs
|
|
||||||
import io.mockk.slot
|
|
||||||
import io.mockk.verify
|
|
||||||
import org.assertj.core.api.Assertions.assertThat
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
import org.junit.jupiter.api.BeforeEach
|
import org.junit.jupiter.api.BeforeEach
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
@@ -37,7 +29,7 @@ class EntitySchedulerTest {
|
|||||||
private lateinit var scheduler: EntityScheduler
|
private lateinit var scheduler: EntityScheduler
|
||||||
|
|
||||||
private val entity = VirtualEntity(
|
private val entity = VirtualEntity(
|
||||||
name = "João Gerente",
|
name = "Joao Gerente",
|
||||||
email = "joao@condado.com",
|
email = "joao@condado.com",
|
||||||
jobTitle = "Gerente de Nada",
|
jobTitle = "Gerente de Nada",
|
||||||
personality = "Muito formal",
|
personality = "Muito formal",
|
||||||
|
|||||||
Reference in New Issue
Block a user