-
Notifications
You must be signed in to change notification settings - Fork 0
⚡ virtual AI + Gemini 서비스 합병 #205
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,180 @@ | ||
| package com.umc.linkyou.service.curation.gemini; | ||
|
|
||
| import com.fasterxml.jackson.core.type.TypeReference; | ||
| import com.fasterxml.jackson.databind.ObjectMapper; | ||
| import com.google.cloud.vertexai.VertexAI; | ||
| import com.google.cloud.vertexai.api.GenerateContentResponse; | ||
| import com.google.cloud.vertexai.api.GenerationConfig; | ||
| import com.google.cloud.vertexai.api.Tool; | ||
| import com.google.cloud.vertexai.api.GoogleSearchRetrieval; | ||
| import com.google.cloud.vertexai.generativeai.ContentMaker; | ||
| import com.google.cloud.vertexai.generativeai.GenerativeModel; | ||
| import com.google.cloud.vertexai.generativeai.ResponseHandler; | ||
| import com.umc.linkyou.web.dto.curation.RecommendedLinkResponse; | ||
| import jakarta.annotation.PostConstruct; | ||
| import jakarta.annotation.PreDestroy; | ||
| import lombok.RequiredArgsConstructor; | ||
| import lombok.extern.slf4j.Slf4j; | ||
| import org.springframework.beans.factory.annotation.Value; | ||
| import org.springframework.stereotype.Service; | ||
|
|
||
| import java.io.IOException; | ||
| import java.net.URI; | ||
| import java.util.*; | ||
| import java.util.stream.Collectors; | ||
|
|
||
| @Slf4j | ||
| @Service | ||
| @RequiredArgsConstructor | ||
| public class GeminiExternalSearchService { | ||
|
|
||
| private final ObjectMapper objectMapper; | ||
|
|
||
| // application.properties에서 값 가져오기 | ||
| @Value("${spring.cloud.gcp.project-id}") | ||
| private String projectId; | ||
|
|
||
| @Value("${spring.cloud.gcp.location}") | ||
| private String location; | ||
|
|
||
| @Value("${gemini.model.name}") | ||
| private String modelName; | ||
|
|
||
| private VertexAI vertexAI; | ||
| private GenerativeModel model; | ||
|
|
||
| /** | ||
| * 서버 시작 시 Gemini 모델과 연결 설정 (Client 역할 대체) | ||
| */ | ||
| @PostConstruct | ||
| public void init() { | ||
| try { | ||
| // 1. Vertex AI 클라이언트 초기화 | ||
| // (참고: 로컬 개발 시 환경변수 GOOGLE_APPLICATION_CREDENTIALS 설정 필수) | ||
| this.vertexAI = new VertexAI(projectId, location); | ||
|
|
||
| // 2. 구글 검색 도구(Grounding) 설정 | ||
| Tool googleSearchTool = Tool.newBuilder() | ||
| .setGoogleSearchRetrieval( | ||
| GoogleSearchRetrieval.newBuilder().build() | ||
| ) | ||
| .build(); | ||
|
|
||
| // 3. 생성 설정 (JSON 포맷 강제 등) | ||
| GenerationConfig generationConfig = GenerationConfig.newBuilder() | ||
| .setMaxOutputTokens(2048) | ||
| .setTemperature(0.9f) // 창의성(다양한 검색 결과)을 위해 높임 | ||
| .setResponseMimeType("application/json") // JSON 응답 강제 | ||
| .build(); | ||
|
|
||
| // 4. 모델 생성 | ||
| this.model = new GenerativeModel.Builder() | ||
| .setModelName(modelName) | ||
| .setVertexAi(vertexAI) | ||
| .setTools(Collections.singletonList(googleSearchTool)) | ||
| .setGenerationConfig(generationConfig) | ||
| .build(); | ||
|
|
||
| log.info("✅ Gemini Search Service 초기화 완료 (Project: {}, Location: {})", projectId, location); | ||
|
|
||
| } catch (Exception e) { | ||
| log.error("❌ Gemini 초기화 실패: GCP 인증 파일이나 설정을 확인하세요.", e); | ||
| } | ||
| } | ||
|
Comment on lines
+49
to
+83
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 초기화 실패 시 서비스가 비정상 상태로 유지됩니다.
초기화 실패 시 애플리케이션 시작을 중단하거나, @PostConstruct
public void init() {
try {
// ... 초기화 코드 ...
log.info("✅ Gemini Search Service 초기화 완료 (Project: {}, Location: {})", projectId, location);
} catch (Exception e) {
- log.error("❌ Gemini 초기화 실패: GCP 인증 파일이나 설정을 확인하세요.", e);
+ log.error("❌ Gemini 초기화 실패: GCP 인증 파일이나 설정을 확인하세요.", e);
+ throw new IllegalStateException("Gemini 서비스 초기화 실패", e);
}
}또는 if (this.model == null) {
log.warn("Gemini 모델이 초기화되지 않음 - 빈 결과 반환");
return Collections.emptyList();
}🤖 Prompt for AI Agents |
||
|
|
||
| /** | ||
| * 서버 종료 시 리소스 정리 | ||
| */ | ||
| @PreDestroy | ||
| public void close() { | ||
| if (this.vertexAI != null) { | ||
| this.vertexAI.close(); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * 외부 링크 추천 기능 메인 로직 | ||
| */ | ||
| public List<RecommendedLinkResponse> searchExternalLinks( | ||
| List<String> recentUrls, | ||
| List<String> tagNames, | ||
| int limit, | ||
| String jobName, | ||
| String gender | ||
| ) { | ||
| // 중복 방지를 위해 이미 본 URL에서 도메인만 추출 (예: naver.com, tistory.com) | ||
| String excludedDomains = recentUrls.stream() | ||
| .map(this::extractDomain) | ||
| .filter(Objects::nonNull) | ||
| .distinct() | ||
| .collect(Collectors.joining(", ")); | ||
|
|
||
| // 1. 시스템 프롬프트 (규칙 정의) | ||
| String systemInstruction = """ | ||
| You are a professional content curator for '%s'. | ||
| Target Audience Job: %s | ||
| [CRITICAL RULES] | ||
| 1. Use Google Search to find REAL, LIVE web pages. | ||
| 2. EXCLUDE content from these domains: [%s] (User already saw them). | ||
| 3. Find NEW content (Published within the last 1 year preferred). | ||
| 4. Output must be a pure JSON Array. | ||
| 5. Fields: "title", "url", "summary". | ||
| """.formatted(safe(jobName), safe(jobName), excludedDomains); | ||
|
|
||
| // 2. 유저 프롬프트 (실제 요청) | ||
| String userPrompt = """ | ||
| Find %d high-quality, practical links about: %s. | ||
| Focus on tutorials, trends, or engineering blogs. | ||
| Exclude generic wikis. | ||
| """.formatted(limit, String.join(", ", tagNames)); | ||
|
|
||
| try { | ||
| // 3. Gemini에게 질문 (여기가 Client.chat() 역할) | ||
| GenerateContentResponse response = model.generateContent( | ||
| ContentMaker.fromMultiModalData(systemInstruction + "\n\n" + userPrompt) | ||
| ); | ||
|
|
||
| // 4. 응답 텍스트 추출 | ||
| String jsonResponse = ResponseHandler.getText(response); | ||
| log.info("Gemini 응답: {}", jsonResponse); | ||
|
|
||
| // 5. JSON 파싱 | ||
| List<Map<String, String>> parsed = objectMapper.readValue(jsonResponse, new TypeReference<>() {}); | ||
|
|
||
| // 6. DTO 변환 | ||
| return parsed.stream() | ||
| .filter(m -> m.get("url") != null && !m.get("url").isBlank()) | ||
| .limit(limit) | ||
| .map(m -> RecommendedLinkResponse.builder() | ||
| .title(m.getOrDefault("title", "No Title")) | ||
| .url(m.get("url")) | ||
| .domain(extractDomain(m.get("url"))) | ||
| .build()) | ||
| .collect(Collectors.toList()); | ||
|
|
||
| } catch (Exception e) { | ||
| log.error("Gemini 검색 중 오류 발생", e); | ||
| return Collections.emptyList(); | ||
| } | ||
| } | ||
|
|
||
| // null 방지용 | ||
| private String safe(String s) { | ||
| return (s == null || s.isBlank()) ? "Technology" : s; | ||
| } | ||
|
|
||
| // URL에서 도메인 추출 (예: https://www.naver.com/news -> naver.com) | ||
| private String extractDomain(String url) { | ||
| try { | ||
| URI uri = new URI(url); | ||
| String domain = uri.getHost(); | ||
| if (domain != null && domain.startsWith("www.")) { | ||
| return domain.substring(4); | ||
| } | ||
| return domain; | ||
| } catch (Exception e) { | ||
| return null; | ||
| } | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🌐 Web query:
google-cloud-vertexai latest version maven central 2025💡 Result:
Latest Maven Central version (Nov 30, 2025): com.google.cloud:google-cloud-vertexai — 1.40.0 (released Nov 15, 2025). [1][2]
Maven coordinate:
com.google.cloud
google-cloud-vertexai
1.40.0
Sources:
[1] Google Cloud Java docs (google-cloud-vertexai overview, 1.40.0).
[2] Maven Central (repo1.maven.org) listing for com/google/cloud/google-cloud-vertexai showing 1.40.0 (2025-11-15).
Update google-cloud-vertexai to the latest version.
The dependency is outdated. Version 1.14.0 is significantly behind the current latest release (1.40.0, released November 15, 2025). Update the dependency to:
This 26-version gap includes important updates, security patches, and feature improvements that should be incorporated.
🤖 Prompt for AI Agents