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
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,6 @@ public class PingController {

@GetMapping("/api/ping")
public ResponseEntity<String> ping() {
return new ResponseEntity<>("pong", HttpStatus.OK);
return new ResponseEntity<>("pong2", HttpStatus.OK);
}
}
1 change: 1 addition & 0 deletions gss-client/gss-mcp-client/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-webflux'

implementation 'org.springframework.ai:spring-ai-openai-spring-boot-starter:1.0.0-M6'
implementation 'org.springframework.ai:spring-ai-anthropic-spring-boot-starter:1.0.0-M6'
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.devoops;

public enum McpClientType {

OPEN_AI,
CLAUDE,
;

public boolean isOpenAi() {
return this == OPEN_AI;
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
package com.devoops.client;

import com.devoops.McpClientType;
import com.devoops.dto.request.AnalyzePrRequest;
import com.devoops.dto.response.AnalyzePrResponse;

public interface PrAnalysisClient {

AnalyzePrResponse analyze(AnalyzePrRequest request);

McpClientType getMcpClientType();
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ public class PromptBuilder {
@Value("${dev-oops.github-pr-analysis.system}")
private String systemPrompt;

@Value("${dev-oops.github-pr-analysis.format-message}")
private String formatMessage;

public String buildUserPrompt(String title, String description, String diff) {
return promptTemplate
.replace("{title}", title)
Expand All @@ -25,6 +28,10 @@ public String buildSystemPrompt() {
return systemPrompt;
}

public String buildSystemPromptWithResponseFormat(String jsonSchema) {
return systemPrompt + "\n\n" + formatMessage.formatted(jsonSchema);
}

private String encodeDiff(String diff) {
return Base64.getEncoder().encodeToString(diff.getBytes(StandardCharsets.UTF_8));
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package com.devoops.client.claude;

import com.devoops.McpClientType;
import com.devoops.client.PrAnalysisClient;
import com.devoops.client.PromptBuilder;
import com.devoops.dto.request.AnalyzePrRequest;
import com.devoops.dto.response.AnalyzePrResponse;
import com.devoops.dto.response.PrAnalysis;
import com.devoops.serdes.PrAnalysisMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.anthropic.AnthropicChatModel;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.metadata.Usage;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.ChatOptions;
import org.springframework.ai.converter.BeanOutputConverter;

@Slf4j
public class ClaudePrAnalysisClient implements PrAnalysisClient {

private static final McpClientType CLIENT_VENDOR = McpClientType.CLAUDE;

private final ChatClient chatClient;
private final PromptBuilder promptBuilder;
private final PrAnalysisMapper prAnalysisMapper;

public ClaudePrAnalysisClient(
AnthropicChatModel anthropicChatModel,
PromptBuilder promptBuilder,
PrAnalysisMapper prAnalysisMapper
) {
this.chatClient = ChatClient.create(anthropicChatModel);
this.promptBuilder = promptBuilder;
this.prAnalysisMapper = prAnalysisMapper;
}

@Override
public AnalyzePrResponse analyze(AnalyzePrRequest request) {
//option 설정
ChatOptions anthropicChatOptions = anthropicChatOptions(request.model());

ChatResponse chatresponse = callChatResponse(
request.title(),
request.description(),
request.codeDifference(),
anthropicChatOptions
);
Usage usage = chatresponse.getMetadata().getUsage();
String analysisResult = chatresponse.getResult().getOutput().getText();
PrAnalysis prAnalysis = prAnalysisMapper.mapToPrAnalysis(analysisResult);
return new AnalyzePrResponse(usage, prAnalysis);
}

@Override
public McpClientType getMcpClientType() {
return CLIENT_VENDOR;
}

private ChatOptions anthropicChatOptions(String model) {
return ChatOptions.builder()
.temperature(0.7)
.model(model)
.maxTokens(64_000)
.build();
}

private ChatResponse callChatResponse(
String title,
String description,
String codeDifference,
ChatOptions options
) {
String userPrompt = promptBuilder.buildUserPrompt(title, description, codeDifference);
String systemPrompt = promptBuilder.buildSystemPromptWithResponseFormat(outputJsonSchema());
return chatClient.prompt()
.options(options)
.system(systemPrompt)
.user(userPrompt)
.call()
.chatResponse();
}

private String outputJsonSchema() {
BeanOutputConverter<PrAnalysis> outputConverter = new BeanOutputConverter<>(PrAnalysis.class);
return outputConverter.getJsonSchema();
}
}
Original file line number Diff line number Diff line change
@@ -1,30 +1,46 @@
package com.devoops.client;
package com.devoops.client.openai;

import com.devoops.McpClientType;
import com.devoops.client.PrAnalysisClient;
import com.devoops.client.PromptBuilder;
import com.devoops.dto.request.AnalyzePrRequest;
import com.devoops.dto.response.AnalyzePrResponse;
import com.devoops.dto.response.PrAnalysis;
import com.devoops.serdes.PrAnalysisMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.metadata.Usage;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.ChatOptions;
import org.springframework.ai.converter.BeanOutputConverter;
import org.springframework.ai.openai.OpenAiChatModel;
import org.springframework.ai.openai.OpenAiChatOptions;
import org.springframework.ai.openai.api.ResponseFormat;
import org.springframework.ai.openai.api.ResponseFormat.Type;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Component;

@Primary
@Component
@RequiredArgsConstructor
@Slf4j
public class PrAnalysisClientImpl implements PrAnalysisClient {
public class OpenAiPrAnalysisClient implements PrAnalysisClient {

private static final McpClientType MCP_CLIENT_VENDOR = McpClientType.OPEN_AI;

private final ChatClient chatClient;
private final PromptBuilder promptBuilder;
private final PrAnalysisMapper prAnalysisMapper;

public OpenAiPrAnalysisClient(
OpenAiChatModel openAiChatModel,
PromptBuilder promptBuilder,
PrAnalysisMapper prAnalysisMapper
) {
this.chatClient = ChatClient.create(openAiChatModel);
this.promptBuilder = promptBuilder;
this.prAnalysisMapper = prAnalysisMapper;
}

@Override
public AnalyzePrResponse analyze(AnalyzePrRequest request) {
//option 설정
Expand All @@ -45,6 +61,11 @@ public AnalyzePrResponse analyze(AnalyzePrRequest request) {
return new AnalyzePrResponse(usage, prAnalysis);
}

@Override
public McpClientType getMcpClientType() {
return MCP_CLIENT_VENDOR;
}

private OpenAiChatOptions.Builder openAiChatBuilder() {
return OpenAiChatOptions.builder()
.responseFormat(new ResponseFormat(Type.JSON_SCHEMA, outputJsonSchema()))
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package com.devoops.config;

import com.devoops.client.PrAnalysisClient;
import com.devoops.client.PromptBuilder;
import com.devoops.client.claude.ClaudePrAnalysisClient;
import com.devoops.client.openai.OpenAiPrAnalysisClient;
import com.devoops.serdes.PrAnalysisMapper;
import org.springframework.ai.anthropic.AnthropicChatModel;
import org.springframework.ai.openai.OpenAiChatModel;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.core.annotation.Order;

@Profile("!test")
@Configuration
public class ClientConfig {

@Bean
@Order(1)
public PrAnalysisClient openAiClient(
OpenAiChatModel openAiChatModel,
PromptBuilder promptBuilder,
PrAnalysisMapper prAnalysisMapper
) {
return new OpenAiPrAnalysisClient(openAiChatModel, promptBuilder, prAnalysisMapper);
}

@Bean
@Order(2)
public PrAnalysisClient claudeClient(
AnthropicChatModel anthropicChatModel,
PromptBuilder promptBuilder,
PrAnalysisMapper prAnalysisMapper
) {
return new ClaudePrAnalysisClient(anthropicChatModel, promptBuilder, prAnalysisMapper);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
@Component
public class PrAnalysisMapper {

private static final int MAX_LOGGING_LENGTH = 255;
private static final int MAX_LOGGING_LENGTH = 500;

private static final ObjectMapper MAPPER = new ObjectMapper()
.findAndRegisterModules()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@ dev-oops:

diff를 Base64에서 디코딩한 후 분석하고 PR 요약과 질문을 만들어 주세요.

format-message: |
응답은 반드시 다음 JSON 스키마에 맞춰 정확한 JSON 형식으로 제공해주세요('''json도 제외)::

%s

응답에는 JSON만 포함하고, 다른 텍스트나 설명은 포함하지 마세요.
---

spring:
Expand All @@ -35,6 +41,8 @@ spring:
ai:
openai:
api-key: testKey
anthropic:
api-key: testKey2

---

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ public enum ErrorCode {
REDIS_SUBSCRIBE_ERROR(500, "레디스 이벤트 수신 과정에서 문제가 생겼습니다"),
GITHUB_CLIENT_ERROR(500, "깃허브 클라이언트 소통과정에 문제가 발생했습니다"),
AI_RESPONSE_PARSING_ERROR(500, "AI로부터 온 질문 생성을 파싱하는 과정에 오류가 발생했습니다"),
AI_CREATE_QUESTION_ERROR(500, "AI 질문 생성과정에 오류가 발생했습니다")
;

private final int statusCode;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.devoops.domain.entity.analysis;

public interface AiModel {

double getCharge(int promptToken, int completionTokens);

String getName();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.devoops.domain.entity.analysis;

import com.devoops.util.CurrencyUtil;
import lombok.Getter;
import lombok.RequiredArgsConstructor;

@Getter
@RequiredArgsConstructor
public enum ClaudeAiModel implements AiModel {

CLAUDE_SONNET_4("claude-sonnet-4-20250514", 0.000003, 0.000015),
;

private final String name;
private final double inputTokenCharge; //달러
private final double outputTokenCharge; //달러

public double getCharge(int promptToken, int completionTokens) {
double inputCharge = CurrencyUtil.usdToKrw(inputTokenCharge * promptToken);
double outputCharge = CurrencyUtil.usdToKrw(outputTokenCharge * completionTokens);
return inputCharge + outputCharge;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

@Getter
@RequiredArgsConstructor
public enum OpenAiModel {
public enum OpenAiModel implements AiModel {

GPT_5(0, 7500, "gpt-5", 0.00000125, 0.00001),
GPT_5_MINI(7501, 12500, "gpt-5-mini", 0.00000025, 0.000002),
Expand All @@ -23,12 +23,19 @@ public enum OpenAiModel {

public static OpenAiModel getModelByUsage(BigDecimal currentUsageWon) {
return Stream.of(values())
.filter(model -> model.moneyUnderCriteria <= currentUsageWon.doubleValue()
&& model.moneyUpperCriteria >= currentUsageWon.doubleValue())
.filter(model -> model.isBetween(
currentUsageWon.doubleValue(),
model.moneyUnderCriteria,
model.moneyUpperCriteria)
)
.findAny()
.orElse(GPT_5_NANO);
}

private boolean isBetween(double currentUsage, double min, double max) {
return currentUsage >= min && currentUsage <= max;
}

public double getCharge(int promptToken, int completionTokens) {
double inputCharge = CurrencyUtil.usdToKrw(inputTokenCharge * promptToken);
double outputCharge = CurrencyUtil.usdToKrw(outputTokenCharge * completionTokens);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,6 @@ public interface GithubRepoDomainRepository {

List<GithubRepository> findByUserId(long userId);

boolean existsByExternalIdAndUserId(long externalId, long userId);

Optional<GithubRepository> findByExternalIdAndUserId(long externalId, long userId);

GithubRepository getByExternalIdAndUserId(long externalId, long userId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,6 @@ public boolean existsByIdAndUserId(long id, long userId) {
return repoJpaRepository.existsByIdAndUserId(id, userId);
}

@Override
@Transactional(readOnly = true)
public boolean existsByExternalIdAndUserId(long externalId, long userId) {
return repoJpaRepository.existsByGithubRepositoryIdAndUserId(externalId, userId);
}

@Override
@Transactional(readOnly = true)
public GithubRepository findByIdAndUserId(long id, long userId) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,5 @@ public interface GithubRepoJpaRepository extends JpaRepository<GithubRepositoryE

boolean existsByGithubRepositoryIdAndUserId(long externalId, long userId);

Optional<GithubRepositoryEntity> findByGithubRepositoryId(long externalId);

Optional<GithubRepositoryEntity> findByGithubRepositoryIdAndUserId(long externalId, long userId);
}
Loading
Loading