120 lines
5.2 KiB
Java
120 lines
5.2 KiB
Java
package com.zioinfo.signage.config;
|
|
|
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
import lombok.extern.slf4j.Slf4j;
|
|
import org.springframework.beans.factory.annotation.Value;
|
|
import org.springframework.stereotype.Component;
|
|
|
|
import java.net.URI;
|
|
import java.net.http.HttpClient;
|
|
import java.net.http.HttpRequest;
|
|
import java.net.http.HttpResponse;
|
|
import java.time.Duration;
|
|
import java.util.Map;
|
|
|
|
/**
|
|
* Ollama 온프레미스 AI 클라이언트.
|
|
* 보안 불변 규칙: 외부 AI API 절대 금지 — Ollama localhost:11434만 허용.
|
|
* 연결 실패 시 폴백 응답 반환(예외 미전파).
|
|
*/
|
|
@Slf4j
|
|
@Component
|
|
public class OllamaClient {
|
|
|
|
@Value("${guardia.ollama-url:http://localhost:11434}")
|
|
private String baseUrl;
|
|
|
|
@Value("${guardia.ollama-text-model:llama3}")
|
|
private String model;
|
|
|
|
private final HttpClient http = HttpClient.newBuilder()
|
|
.connectTimeout(Duration.ofSeconds(5))
|
|
.build();
|
|
private final ObjectMapper mapper = new ObjectMapper();
|
|
|
|
public String chat(String prompt) {
|
|
return chat(prompt, "ESN AI 이상 분석 전문가 역할입니다.");
|
|
}
|
|
|
|
public String chat(String prompt, String systemPrompt) {
|
|
try {
|
|
var body = Map.of(
|
|
"model", model,
|
|
"messages", new Object[]{
|
|
Map.of("role", "system", "content", systemPrompt),
|
|
Map.of("role", "user", "content", prompt)
|
|
},
|
|
"stream", false
|
|
);
|
|
String json = mapper.writeValueAsString(body);
|
|
HttpRequest req = HttpRequest.newBuilder()
|
|
.uri(URI.create(baseUrl + "/api/chat"))
|
|
.header("Content-Type", "application/json")
|
|
.POST(HttpRequest.BodyPublishers.ofString(json))
|
|
.timeout(Duration.ofSeconds(120))
|
|
.build();
|
|
HttpResponse<String> resp = http.send(req, HttpResponse.BodyHandlers.ofString());
|
|
if (resp.statusCode() == 200) {
|
|
var res = mapper.readTree(resp.body());
|
|
return res.path("message").path("content").asText("분석 결과를 생성했습니다.");
|
|
}
|
|
return fallback(prompt);
|
|
} catch (Exception e) {
|
|
log.warn("Ollama 연결 실패 — 폴백 응답 반환: {}", e.getMessage());
|
|
return fallback(prompt);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 평문 텍스트 생성(모델 지정). AiTextRouter/AiConfigService 연결 테스트 진입점.
|
|
*
|
|
* <p>{@link #chat(String, String)} 과 달리 <b>실패 시 규칙기반 폴백 문자열을 만들지 않고 {@code null}</b>
|
|
* 을 반환한다(라우터가 degraded 로 감지해 다음 단계 폴백을 수행하게 하기 위함).
|
|
*
|
|
* @param prompt 사용자 프롬프트(시스템 지시 포함 가능)
|
|
* @param modelOverride 사용할 Ollama 모델(null/blank 이면 서버 기본 {@code guardia.ollama-text-model})
|
|
* @return 생성 텍스트(성공) 또는 {@code null}(비200/타임아웃/예외/빈응답)
|
|
*/
|
|
public String generateText(String prompt, String modelOverride) {
|
|
if (prompt == null || prompt.isBlank()) {
|
|
return null;
|
|
}
|
|
String useModel = (modelOverride == null || modelOverride.isBlank()) ? model : modelOverride;
|
|
try {
|
|
var body = Map.of(
|
|
"model", useModel,
|
|
"messages", new Object[]{ Map.of("role", "user", "content", prompt) },
|
|
"stream", false
|
|
);
|
|
String json = mapper.writeValueAsString(body);
|
|
HttpRequest req = HttpRequest.newBuilder()
|
|
.uri(URI.create(baseUrl + "/api/chat"))
|
|
.header("Content-Type", "application/json")
|
|
.POST(HttpRequest.BodyPublishers.ofString(json))
|
|
.timeout(Duration.ofSeconds(120))
|
|
.build();
|
|
HttpResponse<String> resp = http.send(req, HttpResponse.BodyHandlers.ofString());
|
|
if (resp.statusCode() == 200) {
|
|
var res = mapper.readTree(resp.body());
|
|
String text = res.path("message").path("content").asText("");
|
|
return (text == null || text.isBlank()) ? null : text.trim();
|
|
}
|
|
log.warn("Ollama generateText status {} — null 반환(라우터 폴백)", resp.statusCode());
|
|
return null;
|
|
} catch (Exception e) {
|
|
log.warn("Ollama generateText 실패 — null 반환(라우터 폴백): {}", e.getClass().getSimpleName());
|
|
return null;
|
|
}
|
|
}
|
|
|
|
private String fallback(String prompt) {
|
|
if (prompt.toLowerCase().contains("alarm") || prompt.contains("알람")) {
|
|
return "알람 분석: 장치 연결 상태를 확인하고 네트워크 환경을 점검하세요. 지속 발생 시 현장 엔지니어 파견이 필요합니다.";
|
|
}
|
|
if (prompt.toLowerCase().contains("pos") || prompt.contains("가격")) {
|
|
return "POS 데이터 분류: 정상 가격 변환 데이터입니다. 이상 감지된 항목은 수동 검토가 필요합니다.";
|
|
}
|
|
return "AI 분석 결과: 현재 시스템 상태를 검토하고 운영 매뉴얼을 참조하세요.";
|
|
}
|
|
}
|