-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Feat: 사용자는 허브 태그 자동 생성 기능을 이용할 수 있다.
Feat: 사용자는 허브 태그 자동 생성 기능을 이용할 수 있다.
- Loading branch information
Showing
17 changed files
with
480 additions
and
1 deletion.
There are no files selected for viewing
13 changes: 13 additions & 0 deletions
13
src/main/java/com/seong/shoutlink/domain/common/ApiClient.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
package com.seong.shoutlink.domain.common; | ||
|
||
import java.util.List; | ||
import java.util.Map; | ||
|
||
public interface ApiClient { | ||
|
||
Map<String, Object> post( | ||
String url, | ||
Map<String, List<String>> uriVariables, | ||
Map<String, List<String>> headers, | ||
String requestBody); | ||
} |
18 changes: 18 additions & 0 deletions
18
src/main/java/com/seong/shoutlink/domain/link/LinkBundleAndLinks.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
package com.seong.shoutlink.domain.link; | ||
|
||
import com.seong.shoutlink.domain.link.Link; | ||
import com.seong.shoutlink.domain.linkbundle.LinkBundle; | ||
import java.util.List; | ||
import lombok.Getter; | ||
|
||
@Getter | ||
public class LinkBundleAndLinks { | ||
|
||
private final LinkBundle linkBundle; | ||
private final List<Link> links; | ||
|
||
public LinkBundleAndLinks(LinkBundle linkBundle, List<Link> links) { | ||
this.linkBundle = linkBundle; | ||
this.links = links; | ||
} | ||
} |
10 changes: 10 additions & 0 deletions
10
src/main/java/com/seong/shoutlink/domain/tag/service/AutoGenerativeClient.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
package com.seong.shoutlink.domain.tag.service; | ||
|
||
import com.seong.shoutlink.domain.tag.service.ai.GenerateAutoTagCommand; | ||
import com.seong.shoutlink.domain.tag.service.ai.GeneratedTag; | ||
import java.util.List; | ||
|
||
public interface AutoGenerativeClient { | ||
|
||
List<GeneratedTag> generateTags(GenerateAutoTagCommand command); | ||
} |
35 changes: 35 additions & 0 deletions
35
src/main/java/com/seong/shoutlink/domain/tag/service/ai/GenerateAutoTagCommand.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
package com.seong.shoutlink.domain.tag.service.ai; | ||
|
||
import com.seong.shoutlink.domain.link.Link; | ||
import com.seong.shoutlink.domain.link.LinkBundleAndLinks; | ||
import com.seong.shoutlink.domain.linkbundle.LinkBundle; | ||
import java.util.List; | ||
|
||
public record GenerateAutoTagCommand(List<AutoTagLinkBundle> linkBundles) { | ||
|
||
public record AutoTagLinkBundle(String description, List<AutoTagLink> links) { | ||
|
||
public static AutoTagLinkBundle from(LinkBundle linkBundle, List<AutoTagLink> autoTagLinks) { | ||
return new AutoTagLinkBundle(linkBundle.getDescription(), autoTagLinks); | ||
} | ||
} | ||
|
||
public record AutoTagLink(String url, String description) { | ||
|
||
public static AutoTagLink from(Link link) { | ||
return new AutoTagLink(link.getUrl(), link.getDescription()); | ||
} | ||
} | ||
|
||
public static GenerateAutoTagCommand create(List<LinkBundleAndLinks> linkBundlesAndLinks) { | ||
List<AutoTagLinkBundle> content = linkBundlesAndLinks.stream() | ||
.map(linkBundleAndLinks -> { | ||
List<AutoTagLink> autoTagLinks = linkBundleAndLinks.getLinks().stream() | ||
.map(AutoTagLink::from) | ||
.toList(); | ||
return AutoTagLinkBundle.from(linkBundleAndLinks.getLinkBundle(), autoTagLinks); | ||
}) | ||
.toList(); | ||
return new GenerateAutoTagCommand(content); | ||
} | ||
} |
5 changes: 5 additions & 0 deletions
5
src/main/java/com/seong/shoutlink/domain/tag/service/ai/GeneratedTag.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
package com.seong.shoutlink.domain.tag.service.ai; | ||
|
||
public record GeneratedTag(String name) { | ||
|
||
} |
28 changes: 28 additions & 0 deletions
28
src/main/java/com/seong/shoutlink/global/client/ai/AutoTagPrompt.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
package com.seong.shoutlink.global.client.ai; | ||
|
||
import com.seong.shoutlink.domain.tag.service.ai.GenerateAutoTagCommand; | ||
import com.seong.shoutlink.domain.tag.service.ai.GenerateAutoTagCommand.AutoTagLink; | ||
import com.seong.shoutlink.domain.tag.service.ai.GenerateAutoTagCommand.AutoTagLinkBundle; | ||
|
||
public record AutoTagPrompt(String request, GenerateAutoTagCommand command) { | ||
|
||
public String toPromptString() { | ||
StringBuilder sb = new StringBuilder(); | ||
sb.append("요청:").append(request).append("\n") | ||
.append("요약 대상:"); | ||
sb.append("{"); | ||
for (AutoTagLinkBundle linkBundle : command.linkBundles()) { | ||
sb.append("링크 묶음 설명:").append(linkBundle.description()); | ||
sb.append("["); | ||
for (AutoTagLink link : linkBundle.links()) { | ||
sb.append("{"); | ||
sb.append("링크 설명:").append(link.description()).append(",") | ||
.append("url:").append(link.url()); | ||
sb.append("}"); | ||
} | ||
sb.append("]"); | ||
} | ||
sb.append("}"); | ||
return sb.toString(); | ||
} | ||
} |
79 changes: 79 additions & 0 deletions
79
src/main/java/com/seong/shoutlink/global/client/ai/GeminiClient.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
package com.seong.shoutlink.global.client.ai; | ||
|
||
import com.fasterxml.jackson.core.JsonProcessingException; | ||
import com.fasterxml.jackson.core.type.TypeReference; | ||
import com.fasterxml.jackson.databind.ObjectMapper; | ||
import com.seong.shoutlink.domain.common.ApiClient; | ||
import com.seong.shoutlink.domain.exception.ErrorCode; | ||
import com.seong.shoutlink.domain.exception.ShoutLinkException; | ||
import com.seong.shoutlink.domain.tag.service.AutoGenerativeClient; | ||
import com.seong.shoutlink.domain.tag.service.ai.GenerateAutoTagCommand; | ||
import com.seong.shoutlink.domain.tag.service.ai.GeneratedTag; | ||
import java.text.MessageFormat; | ||
import java.util.Arrays; | ||
import java.util.HashMap; | ||
import java.util.List; | ||
import java.util.Map; | ||
import lombok.extern.slf4j.Slf4j; | ||
|
||
@Slf4j | ||
public class GeminiClient implements AutoGenerativeClient { | ||
|
||
private static final Map<String, List<String>> HEADERS = new HashMap<>(); | ||
private static final Map<String, List<String>> URI_VARIABLES = Map.of(); | ||
private static final String API_KEY_HEADER = "x-goog-api-key"; | ||
private static final String GENERATE_TAG_PROMPT = | ||
""" | ||
주어진 링크 묶음 설명, 링크 설명, url을 통해 사용자의 관심사를 추론하세요. | ||
추론한 관심사는 {0}개의 키워드로 요약해서 응답하세요. | ||
각 키워드는 10자 이내여야 하며 둘 이상인 경우 ,로 구분하세요. | ||
"""; | ||
|
||
static { | ||
HEADERS.put("Content-Type", List.of("application/json")); | ||
} | ||
|
||
private final String url; | ||
private final ObjectMapper objectMapper; | ||
private final ApiClient apiClient; | ||
|
||
public GeminiClient(String url, String apiKey, ObjectMapper objectMapper, ApiClient apiClient) { | ||
this.url = url; | ||
this.objectMapper = objectMapper; | ||
this.apiClient = apiClient; | ||
HEADERS.put(API_KEY_HEADER, List.of(apiKey)); | ||
} | ||
|
||
@Override | ||
public List<GeneratedTag> generateTags(GenerateAutoTagCommand command) { | ||
String requestPrompt = MessageFormat.format(GENERATE_TAG_PROMPT, 1); | ||
AutoTagPrompt autoTagPrompt = new AutoTagPrompt(requestPrompt, command); | ||
GeminiRequest geminiRequest = GeminiRequest.create(autoTagPrompt.toPromptString()); | ||
String requestBody = ""; | ||
try { | ||
requestBody = objectMapper.writeValueAsString(geminiRequest); | ||
} catch (JsonProcessingException e) { | ||
throw new ShoutLinkException("문자열 변환에 실패하였습니다.", ErrorCode.ILLEGAL_ARGUMENT); | ||
} | ||
|
||
log.info("[Gemini] 태그 자동 생성 요청"); | ||
Object rawCandidates = apiClient.post(url, URI_VARIABLES, HEADERS, requestBody) | ||
.get("candidates"); | ||
List<GeminiResponse.Candidate> candidates | ||
= objectMapper.convertValue(rawCandidates, new TypeReference<>() { | ||
}); | ||
|
||
log.info("[Gemini] 자동 생성된 태그 변환 시작"); | ||
String[] splitTags = candidates.stream() | ||
.findFirst() | ||
.map(GeminiResponse.Candidate::getResponse) | ||
.map(tags -> tags.trim().split(",")) | ||
.orElseThrow(() -> new ShoutLinkException("유효한 candidate가 포함되어 있지 않습니다.", | ||
ErrorCode.ILLEGAL_ARGUMENT)); | ||
|
||
log.info("[Gemini] 자동 생성된 태그 변환 성공"); | ||
return Arrays.stream(splitTags) | ||
.map(rawTag -> new GeneratedTag(rawTag.trim())) | ||
.toList(); | ||
} | ||
} |
20 changes: 20 additions & 0 deletions
20
src/main/java/com/seong/shoutlink/global/client/ai/GeminiRequest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
package com.seong.shoutlink.global.client.ai; | ||
|
||
import java.util.List; | ||
|
||
public record GeminiRequest(List<Content> contents) { | ||
|
||
record Content(List<Part> parts) { | ||
|
||
} | ||
|
||
record Part(String text) { | ||
|
||
} | ||
|
||
static GeminiRequest create(String text) { | ||
Part part = new Part(text); | ||
Content content = new Content(List.of(part)); | ||
return new GeminiRequest(List.of(content)); | ||
} | ||
} |
29 changes: 29 additions & 0 deletions
29
src/main/java/com/seong/shoutlink/global/client/ai/GeminiResponse.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
package com.seong.shoutlink.global.client.ai; | ||
|
||
import com.seong.shoutlink.domain.exception.ErrorCode; | ||
import com.seong.shoutlink.domain.exception.ShoutLinkException; | ||
import java.util.List; | ||
|
||
public record GeminiResponse(List<Candidate> candidates) { | ||
|
||
record Candidate(Content content) { | ||
|
||
public String getResponse() { | ||
return content.getResponse(); | ||
} | ||
} | ||
|
||
record Content(List<Part> parts, String role) { | ||
|
||
public String getResponse() { | ||
Part part = parts.stream() | ||
.findFirst() | ||
.orElseThrow(() -> new ShoutLinkException("", ErrorCode.ILLEGAL_ARGUMENT)); | ||
return part.text(); | ||
} | ||
} | ||
|
||
record Part(String text) { | ||
|
||
} | ||
} |
43 changes: 43 additions & 0 deletions
43
src/main/java/com/seong/shoutlink/global/client/api/RestApiClient.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
package com.seong.shoutlink.global.client.api; | ||
|
||
import com.fasterxml.jackson.core.JsonProcessingException; | ||
import com.fasterxml.jackson.core.type.TypeReference; | ||
import com.fasterxml.jackson.databind.ObjectMapper; | ||
import com.seong.shoutlink.domain.common.ApiClient; | ||
import com.seong.shoutlink.domain.exception.ErrorCode; | ||
import com.seong.shoutlink.domain.exception.ShoutLinkException; | ||
import java.util.List; | ||
import java.util.Map; | ||
import org.springframework.http.ResponseEntity; | ||
import org.springframework.web.client.RestClient; | ||
|
||
public class RestApiClient implements ApiClient { | ||
|
||
private final RestClient restClient; | ||
private final ObjectMapper objectMapper; | ||
|
||
public RestApiClient(ObjectMapper objectMapper) { | ||
restClient = RestClient.create(); | ||
this.objectMapper = objectMapper; | ||
} | ||
|
||
@Override | ||
public Map<String, Object> post( | ||
String url, | ||
Map<String, List<String>> uriVariables, | ||
Map<String, List<String>> headers, | ||
String requestBody) { | ||
ResponseEntity<String> entity = restClient.post() | ||
.uri(url, uriVariables) | ||
.headers(httpHeaders -> httpHeaders.putAll(headers)) | ||
.body(requestBody) | ||
.retrieve() | ||
.toEntity(String.class); | ||
|
||
try { | ||
return objectMapper.readValue(entity.getBody(), new TypeReference<>() {}); | ||
} catch (JsonProcessingException e) { | ||
throw new ShoutLinkException("API 응답을 읽는데 실패하였습니다.", ErrorCode.ILLEGAL_ARGUMENT); | ||
} | ||
} | ||
} |
22 changes: 22 additions & 0 deletions
22
src/main/java/com/seong/shoutlink/global/config/AiConfig.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
package com.seong.shoutlink.global.config; | ||
|
||
import com.fasterxml.jackson.databind.ObjectMapper; | ||
import com.seong.shoutlink.domain.tag.service.AutoGenerativeClient; | ||
import com.seong.shoutlink.domain.common.ApiClient; | ||
import com.seong.shoutlink.global.client.ai.GeminiClient; | ||
import org.springframework.beans.factory.annotation.Value; | ||
import org.springframework.context.annotation.Bean; | ||
import org.springframework.context.annotation.Configuration; | ||
|
||
@Configuration | ||
public class AiConfig { | ||
|
||
@Bean | ||
public AutoGenerativeClient autoGenerativeClient( | ||
@Value("${gemini.url}") String url, | ||
@Value("${gemini.api-key}") String apiKey, | ||
ObjectMapper objectMapper, | ||
ApiClient apiClient) { | ||
return new GeminiClient(url, apiKey, objectMapper, apiClient); | ||
} | ||
} |
16 changes: 16 additions & 0 deletions
16
src/main/java/com/seong/shoutlink/global/config/ApiConfig.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
package com.seong.shoutlink.global.config; | ||
|
||
import com.fasterxml.jackson.databind.ObjectMapper; | ||
import com.seong.shoutlink.domain.common.ApiClient; | ||
import com.seong.shoutlink.global.client.api.RestApiClient; | ||
import org.springframework.context.annotation.Bean; | ||
import org.springframework.context.annotation.Configuration; | ||
|
||
@Configuration | ||
public class ApiConfig { | ||
|
||
@Bean | ||
public ApiClient apiClient(ObjectMapper objectMapper) { | ||
return new RestApiClient(objectMapper); | ||
} | ||
} |
Submodule shout-link-config
updated
from 25007b to aadf1a
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
package com.seong.shoutlink.fixture; | ||
|
||
public final class ApiFixture { | ||
|
||
public static int DEFAULT_FIXED_TAG_COUNT = 3; | ||
|
||
public static String geminiResponse() { | ||
return """ | ||
{ | ||
"candidates": [ | ||
{ | ||
"content": { | ||
"parts": [ | ||
{ | ||
"text": "태그1,태그2,태그3" | ||
} | ||
], | ||
"role": "model" | ||
}, | ||
"finishReason": "STOP", | ||
"index": 0, | ||
"safetyRatings": [ | ||
{ | ||
"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", | ||
"probability": "NEGLIGIBLE" | ||
}, | ||
{ | ||
"category": "HARM_CATEGORY_HATE_SPEECH", | ||
"probability": "NEGLIGIBLE" | ||
}, | ||
{ | ||
"category": "HARM_CATEGORY_HARASSMENT", | ||
"probability": "NEGLIGIBLE" | ||
}, | ||
{ | ||
"category": "HARM_CATEGORY_DANGEROUS_CONTENT", | ||
"probability": "NEGLIGIBLE" | ||
} | ||
] | ||
} | ||
], | ||
"promptFeedback": { | ||
"safetyRatings": [ | ||
{ | ||
"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", | ||
"probability": "NEGLIGIBLE" | ||
}, | ||
{ | ||
"category": "HARM_CATEGORY_HATE_SPEECH", | ||
"probability": "NEGLIGIBLE" | ||
}, | ||
{ | ||
"category": "HARM_CATEGORY_HARASSMENT", | ||
"probability": "NEGLIGIBLE" | ||
}, | ||
{ | ||
"category": "HARM_CATEGORY_DANGEROUS_CONTENT", | ||
"probability": "NEGLIGIBLE" | ||
} | ||
] | ||
} | ||
} | ||
"""; | ||
} | ||
} |
Oops, something went wrong.