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 클라이언트. * *

보안 불변 규칙: 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 body = Map.of("model", textModel, "prompt", prompt, "stream", false); Map res = builder.baseUrl(ollamaUrl).build() .post().uri("/api/generate") .bodyValue(body) .retrieve() .bodyToMono(Map.class) .timeout(Duration.ofSeconds(120)) .map(m -> (Map) 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 body = Map.of( "model", visionModel, "prompt", prompt, "images", List.of(imageBase64), "stream", false); Map res = builder.baseUrl(ollamaUrl).build() .post().uri("/api/generate") .bodyValue(body) .retrieve() .bodyToMono(Map.class) .timeout(Duration.ofSeconds(120)) .map(m -> (Map) 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; } } }