guardia-cms/backend/src/main/java/com/zioinfo/cms/ai/OllamaClient.java
zio 90d33e32cc feat(uiws): UIWS 업무모듈(업무일지·일정·쪽지·통계)+2FA 이식
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 21:05:51 +09:00

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;
}
}
}