95 lines
3.5 KiB
Java
95 lines
3.5 KiB
Java
package com.zioinfo.cms.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.List;
|
|
import java.util.Map;
|
|
|
|
/**
|
|
* Ollama 온프레미스 LLM 클라이언트.
|
|
*
|
|
* <p>보안 불변 규칙: localhost Ollama만 호출. 외부 AI API 절대 금지.
|
|
* 오프라인/장애 시 절대 예외를 던지지 않고 빈 문자열을 반환한다(서비스 계층이 Java 폴백 수행).
|
|
*/
|
|
@Slf4j
|
|
@Component
|
|
public class OllamaClient {
|
|
|
|
private final WebClient.Builder builder;
|
|
private final String ollamaUrl;
|
|
private final String textModel;
|
|
private final String visionModel;
|
|
|
|
public OllamaClient(WebClient.Builder builder,
|
|
@Value("${guardia.ollama-url:http://localhost:11434}") String ollamaUrl,
|
|
@Value("${guardia.ollama-text-model:llama3}") String textModel,
|
|
@Value("${guardia.ollama-vision-model:llava}") String visionModel) {
|
|
this.builder = builder;
|
|
this.ollamaUrl = ollamaUrl;
|
|
this.textModel = textModel;
|
|
this.visionModel = visionModel;
|
|
}
|
|
|
|
/** 프롬프트로 텍스트 생성. 실패 시 빈 문자열 반환(예외 없음). */
|
|
@SuppressWarnings("unchecked")
|
|
public String generate(String prompt) {
|
|
try {
|
|
Map<String, Object> body = Map.of("model", textModel, "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.getMessage());
|
|
return "";
|
|
}
|
|
}
|
|
|
|
/** llava 비전 모델로 이미지(base64) 분석. 실패 시 빈 문자열. */
|
|
@SuppressWarnings("unchecked")
|
|
public String vision(String prompt, String imageBase64) {
|
|
try {
|
|
Map<String, Object> body = Map.of(
|
|
"model", visionModel,
|
|
"prompt", prompt,
|
|
"images", List.of(imageBase64),
|
|
"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.getMessage());
|
|
return "";
|
|
}
|
|
}
|
|
|
|
public boolean available() {
|
|
try {
|
|
builder.baseUrl(ollamaUrl).build().get().uri("/api/tags")
|
|
.retrieve().bodyToMono(String.class).timeout(Duration.ofSeconds(3)).block();
|
|
return true;
|
|
} catch (Exception e) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|