Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion config
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@
import org.springframework.stereotype.Service;
import starlight.adapter.aireport.infrastructure.ocr.exception.OcrException;
import starlight.adapter.aireport.infrastructure.ocr.infra.ClovaOcrClient;
import starlight.adapter.aireport.infrastructure.ocr.infra.PdfDownloadClient;
import starlight.adapter.aireport.infrastructure.ocr.util.OcrResponseMerger;
import starlight.adapter.aireport.infrastructure.ocr.util.OcrTextExtractor;
import starlight.adapter.aireport.infrastructure.ocr.util.PdfUtils;
import starlight.application.aireport.required.OcrProviderPort;
import starlight.application.aireport.required.PdfDownloadPort;
import starlight.shared.dto.infrastructure.OcrResponse;

import java.util.ArrayList;
Expand All @@ -23,7 +23,7 @@ public class ClovaOcrProvider implements OcrProviderPort {
private static final int MAX_PAGES_PER_REQUEST = 10;

private final ClovaOcrClient clovaOcrClient;
private final PdfDownloadClient pdfDownloadClient;
private final PdfDownloadPort pdfDownloadPort;

/**
* 지정한 PDF URL을 전체 페이지 OCR 처리한 뒤, 단일 응답으로 병합해 반환한다.
Expand All @@ -40,7 +40,7 @@ public class ClovaOcrProvider implements OcrProviderPort {
*/
@Override
public OcrResponse ocrPdfByUrl(String pdfUrl) {
byte[] pdfBytes = pdfDownloadClient.downloadPdfFromUrl(pdfUrl);
byte[] pdfBytes = pdfDownloadPort.downloadFromUrl(pdfUrl);

List<byte[]> chunks = PdfUtils.splitByPageLimit(pdfBytes, MAX_PAGES_PER_REQUEST);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import starlight.application.aireport.util.AiReportResponseParser;
import starlight.application.aireport.provided.dto.AiReportResult;
import starlight.application.aireport.required.AiReportCommandPort;
import starlight.application.aireport.required.AiReportQueryPort;
import starlight.application.expert.required.AiReportSummaryLookupPort;
Expand All @@ -19,7 +19,6 @@
public class AiReportJpa implements AiReportCommandPort, AiReportQueryPort, AiReportSummaryLookupPort {

private final AiReportRepository aiReportRepository;
private final AiReportResponseParser responseParser;

@Override
public AiReport save(AiReport aiReport) {
Expand All @@ -41,7 +40,7 @@ public Map<Long, Integer> findTotalScoresByBusinessPlanIds(List<Long> businessPl
Map<Long, Integer> totalScoreMap = new HashMap<>();

for (AiReport report : reports) {
Integer totalScore = responseParser.toResponse(report).totalScore();
Integer totalScore = AiReportResult.from(report).totalScore();
totalScoreMap.put(report.getBusinessPlanId(), totalScore != null ? totalScore : 0);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
import starlight.adapter.aireport.report.supervisor.SpringAiReportSupervisor;
import starlight.application.aireport.provided.dto.AiReportResult;
import starlight.application.aireport.required.ReportGraderPort;
import starlight.application.businessplan.util.BusinessPlanContentExtractor;
import starlight.domain.aireport.exception.AiReportErrorType;
import starlight.domain.aireport.exception.AiReportException;
import starlight.shared.enumerate.SectionType;
Expand All @@ -31,14 +30,12 @@ public class SpringAiReportGrader implements ReportGraderPort {
private final Map<SectionType, SectionGradeAgent> sectionGradeAgentMap;
private final FullReportGradeAgent fullReportGradeAgent;
private final SpringAiReportSupervisor supervisor;
private final BusinessPlanContentExtractor contentExtractor;
private final Executor sectionGradingExecutor;

public SpringAiReportGrader(
List<SectionGradeAgent> sectionGradeAgentList,
FullReportGradeAgent fullReportGradeAgent,
SpringAiReportSupervisor supervisor,
BusinessPlanContentExtractor contentExtractor,
@Qualifier("sectionGradingExecutor") Executor sectionGradingExecutor) {
try {
this.sectionGradeAgentMap = sectionGradeAgentList.stream()
Expand All @@ -51,7 +48,6 @@ public SpringAiReportGrader(
}
this.fullReportGradeAgent = fullReportGradeAgent;
this.supervisor = supervisor;
this.contentExtractor = contentExtractor;
this.sectionGradingExecutor = sectionGradingExecutor;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
import starlight.adapter.aireport.report.agent.FullReportGradeAgent;
import starlight.adapter.aireport.report.provider.SpringAiAdvisorProvider;
import starlight.adapter.aireport.report.provider.ReportPromptProvider;
import starlight.application.aireport.util.AiReportResponseParser;
import starlight.adapter.aireport.report.parser.AiReportResponseParser;
import starlight.application.aireport.provided.dto.AiReportResult;
import starlight.domain.aireport.exception.AiReportErrorType;
import starlight.domain.aireport.exception.AiReportException;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,16 @@
import starlight.adapter.aireport.report.dto.SectionGradingResult;
import starlight.adapter.aireport.report.provider.SpringAiAdvisorProvider;
import starlight.adapter.aireport.report.provider.ReportPromptProvider;
import starlight.application.aireport.util.AiReportResponseParser;
import starlight.adapter.aireport.report.parser.AiReportResponseParser;
import starlight.application.aireport.provided.dto.AiReportResult;
import starlight.shared.enumerate.SectionType;

@Slf4j
@RequiredArgsConstructor
public class SpringAiSectionGradeAgent implements SectionGradeAgent {

private static final int MAX_RETRIES = 3;

private final SectionType sectionType;
private final ChatClient.Builder chatClientBuilder;
private final ReportPromptProvider reportPromptProvider;
Expand All @@ -40,47 +42,61 @@ public SectionGradingResult gradeSection(String sectionContent) {
return SectionGradingResult.failure(getSectionType(), "Circuit breaker is OPEN");
}

try {
Prompt prompt = reportPromptProvider.createSectionGradingPrompt(
getSectionType(),
sectionContent);

ChatClient chatClient = chatClientBuilder.build();

// SectionType의 tag만 사용
String filter = buildFilterExpression();
QuestionAnswerAdvisor qaAdvisor = advisorProvider
.getQuestionAnswerAdvisor(0.6, 3, filter);
SimpleLoggerAdvisor slAdvisor = advisorProvider.getSimpleLoggerAdvisor();

String llmResponse = chatClient
.prompt(prompt)
.options(ChatOptions.builder()
.temperature(0.0)
.topP(0.1)
.build())
.advisors(qaAdvisor, slAdvisor)
.call()
.content();

// 섹션별 응답 파싱
SectionGradingResult result = parseSectionResult(llmResponse);

if (result.success()) {
circuitBreaker.recordSuccess(getSectionType());
} else {
circuitBreaker.recordFailure(getSectionType());
Prompt prompt = reportPromptProvider.createSectionGradingPrompt(
getSectionType(),
sectionContent);

ChatClient chatClient = chatClientBuilder.build();
String filter = buildFilterExpression();
QuestionAnswerAdvisor qaAdvisor = advisorProvider
.getQuestionAnswerAdvisor(0.6, 3, filter);
SimpleLoggerAdvisor slAdvisor = advisorProvider.getSimpleLoggerAdvisor();

String lastFailureMessage = null;

for (int attempt = 1; attempt <= MAX_RETRIES; attempt++) {
if (attempt > 1) {
try {
long delay = (long) Math.pow(2, attempt - 1) * 1000L; // 2s, 4s
log.info("[{}] 재시도 대기: {}ms (시도 {}/{})", getSectionType(), delay, attempt, MAX_RETRIES);
Thread.sleep(delay);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
break;
}
}
try {
String llmResponse = chatClient
.prompt(prompt)
.options(ChatOptions.builder()
.temperature(0.0)
.topP(0.1)
.build())
.advisors(qaAdvisor, slAdvisor)
.call()
.content();

SectionGradingResult result = parseSectionResult(llmResponse);

if (result.success()) {
circuitBreaker.recordSuccess(getSectionType());
log.info("[{}] 채점 완료: score={}, filter={}", getSectionType(), result.score(), filter);
return result;
}

lastFailureMessage = result.errorMessage();
log.warn("[{}] 채점 실패 (시도 {}/{}): 파싱 결과 유효하지 않음", getSectionType(), attempt, MAX_RETRIES);

} catch (Exception e) {
lastFailureMessage = "파싱 실패: " + e.getMessage();
log.warn("[{}] 채점 실패 (시도 {}/{}): {}", getSectionType(), attempt, MAX_RETRIES, e.getMessage());
}

log.info("[{}] 채점 완료: score={}, filter={}",
getSectionType(), result.score(), filter);
return result;

} catch (Exception e) {
circuitBreaker.recordFailure(getSectionType());
log.error("[{}] 채점 실패", getSectionType(), e);
return SectionGradingResult.failure(getSectionType(), e.getMessage());
}

circuitBreaker.recordFailure(getSectionType());
String errorMessage = lastFailureMessage != null ? lastFailureMessage : "모든 재시도 실패";
log.error("[{}] 채점 최종 실패 ({}회 시도)", getSectionType(), MAX_RETRIES);
return SectionGradingResult.failure(getSectionType(), errorMessage);
}

private String buildFilterExpression() {
Expand Down
Loading