대충 넘어가지 않는 습관을 위한 기록

Gemini api pro 연동 기록 - RestTemplate

uhyvn 2024. 11. 12. 00:17

이번 프로젝트에서 음식점 업주가 우리 서비스에 매장 상품을 등록할 때,

상품 설명을 기입하는 과정에서 ai를 사용할 수 있게 연동을 해보려고 한다!

 

 

 


 

 

 

우선 아래 링크에서 구글 api 키를 등록한다.

 

https://ai.google.dev/?utm_source=google&utm_medium=cpc&utm_campaign=brand_core_brand&gad_source=1

 

Gemini Developer API | Gemma open models  |  Google AI for Developers

Build with Gemini 1.5 Flash and 1.5 Pro using the Gemini API and Google AI Studio, or access our Gemma open models.

ai.google.dev

 

 

아래와 같이 등록되면 된다!

*조직 계정 말고 @gmail.com 으로 끝나는 계정이어야 한다고 하네요..!

 

 

 

 

 

코드를 구현하기 전에 먼저 테스트할 수 있는 방법이 있다.

https://generativelanguage.googleapis.com/v1/models/gemini-pro:generateContent?key=(YOUR_API_KEY)

위 url로 요청값을 맞춰서 Post 요청을 넣으면 응답받을 수 있다!

 

 

postman으로 요청 테스트한 결과

{
    "contents": [
        {
            "parts": 
                {
                    "text": "안녕! 너는 누구야?"
                }
            
        }]
}

 

 

응답 구조

{
    "candidates": [
        {
            "content": {
                "parts": [
                    {
                        "text": "안녕하세요! 저는 구글에서 개발한 대규모 언어 모델입니다. \n\n더 궁금한 점이 있으신가요? \n"
                    }
                ],
                "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"
                }
            ]
        }
    ],
    "usageMetadata": {
        "promptTokenCount": **, // **처리
        "candidatesTokenCount": **, // **처리
        "totalTokenCount": ** // **처리
    },
    "modelVersion": "gemini-1.5-flash-001"
}

 

 

 

 


 

 

 

 

 

그럼 이제 application.yml부터 설정해준다.

 

gemini:
  api:
    url: ${GEMINI_URL} -> https://generativelanguage.googleapis.com/v1/models/gemini-pro:generateContent
    key: ${GEMINI_KEY} -> {YOUR_API_KEY}

위와 같이 api url과 key를 등록해준다.

 

 

 

 

이후 api 요청을 위해 RestTemplateConfig를 작성해준다.

@Configuration
@RequiredArgsConstructor
public class RestTemplateConfig {

	@Bean
	public RestTemplate geminiRestTemplate() {
		RestTemplate restTemplate = new RestTemplate();
		restTemplate.getInterceptors().add((request, body, execution) -> execution.execute(request, body));

		return restTemplate;
	}

}

 

 

 

 

 

 

 

위 테스트의 요청과 응답 구조에 따라 그에 따른 dto를 생성해준다.

public record ChatRequest(
	List<Content> contents,
	GenerationConfig generationConfig
) {

	public record Content(Parts parts) {
	}

	public record Parts(String text) {
	}

	public record GenerationConfig(
		int candidate_count,
		int max_output_tokens,
		double temperature
	) {
	}

	public ChatRequest(String prompt) {
		this(
			new ArrayList<>(),
			new GenerationConfig(1, 1000, 0.7)
		);

		Content content = new Content(new Parts(prompt));
		this.contents.add(content);
	}
}

 

 

public record ChatResponse(
	List<Candidate> candidates,
	PromptFeedback promptFeedback
) {

	public record Candidate(
		Content content,
		String finishReason,
		int index,
		List<SafetyRating> safetyRatings
	) {
	}

	public record Content(
		List<Parts> parts,
		String role
	) {
	}

	public record Parts(
		String text
	) {
	}

	public record SafetyRating(
		String category,
		String probability
	) {
	}

	public record PromptFeedback(
		List<SafetyRating> safetyRatings
	) {
	}
}

 

 

 

 

 

 

 


 

 

이제 Controller와 Service 코드를 작성할 차례다.

@RestController
@RequestMapping(value = {"/v1/api"})
@RequiredArgsConstructor
public class AIController {

	private final AIService aiService;

	@PostMapping("/stores/{storeId}/products/description/ai")
	public ResponseEntity<CommonResponse<AIResponse>> createProductDescription(
		@AuthenticationPrincipal UserDetailsImpl userDetails,
		@PathVariable("storeId") String storeId,
		@RequestBody AIRequest request
	) {
		return CommonResponse.success(SuccessCode.SUCCESS_INSERT,
			aiService.createProductDescription(storeId, request, userDetails.getUser()));
	}

}

 

AIRequest 는 String text 필드 하나가 전부인 레코드다.

 

 

 

@Service
@RequiredArgsConstructor
public class AIService {

	private final AIJpaRepository aiJpaRepository;
	private final StoreJpaRepository storeJpaRepository;
	private final RestTemplate restTemplate;

	@Value("${gemini.api.url}")
	private String apiUrl;

	@Value("${gemini.api.key}")
	private String apiKey;

	private static final String STORE_NAME_PROMPT = "우리 매장의 이름은 ";
	private static final String PRODUCT_DESCRIPTION_PROMPT = " 입니다. 배달 서비스에 상품 등록을 하려고 하는데, 내가 다음 문장에 주는 정보들을 이용해서 상품 설명 작성을 도와줘.";
	private static final String MAX_LENGTH_HINT = "답변은 최대한 간결하게 50자 이하로 작성해줘.";

	public AIResponse createProductDescription(String storeId, AIRequest request, User user) {
		validateOwner(user.getUserRole());
		Store store = storeJpaRepository.findById(UUID.fromString(storeId))
			.orElseThrow(() -> new IllegalArgumentException(ErrorCode.NOT_FOUND_STORE.getMessage()));

		String requestText =
			STORE_NAME_PROMPT + store.getName() + PRODUCT_DESCRIPTION_PROMPT + request.text() + MAX_LENGTH_HINT;

		String requestUrl = apiUrl + "?key=" + apiKey;
		ChatRequest chatRequest = new ChatRequest(requestText);
		ChatResponse chatResponse = restTemplate.postForObject(requestUrl, chatRequest, ChatResponse.class);

		if (chatResponse == null) {
			throw new IllegalStateException(ErrorCode.INTERNAL_SERVER_ERROR.getMessage());
		}
		String message = chatResponse.candidates().get(0).content().parts().get(0).text();

		aiJpaRepository.save(AIRequestLog.create(store, request.text(), message, user.getId()));

		return new AIResponse(message);
	}

	private void validateOwner(UserRole role) {
		if (!role.equals(UserRole.MANAGER) && !role.equals(UserRole.MASTER) && !role.equals(UserRole.OWNER)) {
			throw new IllegalArgumentException(ErrorCode.INSUFFICIENT_PERMISSIONS.getMessage());
		}
	}
}

 

 

 

  • valdateOwner() -> 일반 유저는 접근하지 못하도록 첫 메소드에 검증한다.
  • 해당 storeId로 검증과 함께 매장 객체를 갖고 온다.
  • requestText -> 프롬프팅의 일부인데, 이건 조금 더 다듬어야 할 필요성이 있다.
  • chatRequest -> 생성한 요청문으로 새 ChatRequest 객체를 생성한다.
  • chatResponse -> 요청할 url, 생성한 요청값을 바탕으로 응답을 받아서 담아온다.
  • 여기서 chatResponse가 null일 수 있으므로, NPE 방지 에러 핸들링을 추가해준다.
  • message -> 위 테스트 응답구조를 보면 알겠지만, 첫 번째 candidate의 content의 첫 번째 part의 text값을 받아온다.
  • 이후에 로그를 기록하기 위해 저장한다.
  • dto로 반환

 

위의 순서대로 진행하였다..!

 

 

 

 

테스트 결과

매장을 DB에 삽입하기 전 테스트라, store.getName()을 문자열 처리한 점을 감안해주시길 바랍니다..

 

 

 


 

 

 

 

 

이후에 할 일은 아래와 같다!

  • RestTemplate 방식을 FeignClient로 변환
  • 비용이 나올 수 있는 api라서, 불필요한 요청 방지를 위해 rate limit을 걸어야 함
  • 더 나은 프롬프팅