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 전용). * *

보안 불변 규칙: 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 폴백 공용 진입점). * *

모델 미지정 시 서버 기본(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 body = Map.of("model", useModel, "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.getClass().getSimpleName()); return ""; } } }