guardia-mall/backend/src/main/java/com/zioinfo/mall/ai/OllamaClient.java

64 lines
2.5 KiB
Java

package com.zioinfo.mall.ai;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.client.WebClient;
import java.time.Duration;
import java.util.Map;
/**
* Ollama 온프레미스 LLM 클라이언트 (Mall AI 전용).
*
* <p>보안 불변 규칙: localhost Ollama만 호출. 외부 AI API 절대 금지.
* 장애/오프라인 시 예외 없이 빈 문자열 반환(서비스가 Java 폴백 수행).
*/
@Slf4j
@Component
public class OllamaClient {
private final WebClient.Builder builder;
private final String ollamaUrl;
private final String model;
public OllamaClient(WebClient.Builder builder,
@Value("${guardia.ollama-url:http://localhost:11434}") String ollamaUrl,
@Value("${guardia.ollama-text-model:llama3}") String model) {
this.builder = builder;
this.ollamaUrl = ollamaUrl;
this.model = model;
}
/** 기본 모델(guardia.ollama-text-model)로 생성. 실패 시 빈 문자열. */
public String generate(String prompt) {
return generateText(prompt, model);
}
/**
* 지정 모델로 평문 생성(AiTextRouter 의 Ollama 경로·Claude 폴백 공용 진입점).
*
* <p>모델 미지정 시 서버 기본(guardia.ollama-text-model). localhost Ollama 만 호출하며,
* 장애/오프라인/타임아웃 시 예외 없이 빈 문자열 반환(호출자가 폴백 수행). [GUARDiA-MALL]
*/
@SuppressWarnings("unchecked")
public String generateText(String prompt, String reqModel) {
if (prompt == null || prompt.isBlank()) return "";
String useModel = (reqModel == null || reqModel.isBlank()) ? model : reqModel.trim();
try {
Map<String, Object> body = Map.of("model", useModel, "prompt", prompt, "stream", false);
Map<String, Object> res = builder.baseUrl(ollamaUrl).build()
.post().uri("/api/generate").bodyValue(body)
.retrieve().bodyToMono(Map.class)
.timeout(Duration.ofSeconds(120))
.map(m -> (Map<String, Object>) m).block();
if (res == null) return "";
Object r = res.get("response");
return r == null ? "" : String.valueOf(r).trim();
} catch (Exception e) {
log.warn("Ollama 일시 불가 — Java 폴백 사용: {}", e.getClass().getSimpleName());
return "";
}
}
}