diff --git a/API명세서.md b/API명세서.md index bc94468..10f3c8c 100644 --- a/API명세서.md +++ b/API명세서.md @@ -559,10 +559,10 @@ MCP 서버를 초기화하고 서버 정보 및 지원 기능을 반환합니다 "result": { "contents": [ { - "uri": "memory://recent", - "mimeType": "application/json", + "uri": "memory://recent", + "mimeType": "application/json", "text": "{\"message\": \"Resource content for memory://recent\"}" - } + } ] } } diff --git a/src/main/java/com/pandol365/dewey/api/controller/McpController.java b/src/main/java/com/pandol365/dewey/api/controller/McpController.java index f3743d2..e9e9132 100644 --- a/src/main/java/com/pandol365/dewey/api/controller/McpController.java +++ b/src/main/java/com/pandol365/dewey/api/controller/McpController.java @@ -180,7 +180,7 @@ public class McpController { toolArguments = params.getArguments(); } - yield mcpService.callTool(toolName, toolArguments); + yield mcpService.callTool(toolName, toolArguments); } case "resources/list" -> mcpService.listResources(); case "resources/read" -> { diff --git a/src/main/java/com/pandol365/dewey/config/DatabaseInitializer.java b/src/main/java/com/pandol365/dewey/config/DatabaseInitializer.java new file mode 100644 index 0000000..c85728b --- /dev/null +++ b/src/main/java/com/pandol365/dewey/config/DatabaseInitializer.java @@ -0,0 +1,104 @@ +package com.pandol365.dewey.config; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.CommandLineRunner; +import org.springframework.core.annotation.Order; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Component; + +/** + * 데이터베이스 초기화 + * 프로젝트 시작 시 pgvector 확장 및 테이블 자동 생성 + */ +@Slf4j +@Component +@Order(1) // 다른 초기화보다 먼저 실행 +@RequiredArgsConstructor +public class DatabaseInitializer implements CommandLineRunner { + + private final JdbcTemplate jdbcTemplate; + + @Override + public void run(String... args) { + try { + log.info("=== 데이터베이스 초기화 시작 ==="); + + // 1. pgvector 확장 확인 및 생성 + initializePgVectorExtension(); + + // 2. memory_embeddings 테이블 생성 + createMemoryEmbeddingsTable(); + + // 3. 인덱스 생성 + createVectorIndex(); + + log.info("=== 데이터베이스 초기화 완료 ==="); + + } catch (Exception e) { + log.error("데이터베이스 초기화 실패: {}", e.getMessage(), e); + // 초기화 실패해도 애플리케이션은 계속 실행 (이미 존재하는 경우 등) + } + } + + /** + * pgvector 확장 생성 + */ + private void initializePgVectorExtension() { + try { + log.info("pgvector 확장 확인 중..."); + jdbcTemplate.execute("CREATE EXTENSION IF NOT EXISTS vector"); + log.info("pgvector 확장 확인 완료"); + } catch (Exception e) { + log.warn("pgvector 확장 생성 실패 (이미 존재할 수 있음): {}", e.getMessage()); + } + } + + /** + * memory_embeddings 테이블 생성 + */ + private void createMemoryEmbeddingsTable() { + try { + log.info("memory_embeddings 테이블 생성 중..."); + String sql = """ + CREATE TABLE IF NOT EXISTS memory_embeddings ( + id BIGSERIAL PRIMARY KEY, + memory_id BIGINT NULL, + user_id VARCHAR(255), + memory_text TEXT NOT NULL, + importance INT DEFAULT 1, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + embedding VECTOR(768) NOT NULL + ) + """; + jdbcTemplate.execute(sql); + log.info("memory_embeddings 테이블 생성 완료"); + } catch (Exception e) { + log.warn("memory_embeddings 테이블 생성 실패 (이미 존재할 수 있음): {}", e.getMessage()); + } + } + + /** + * 벡터 인덱스 생성 (cosine similarity) + */ + private void createVectorIndex() { + try { + log.info("벡터 인덱스 생성 중..."); + // 기존 인덱스가 있으면 삭제 후 재생성 (IF NOT EXISTS는 ivfflat에서 지원 안 함) + String dropIndexSql = "DROP INDEX IF EXISTS memory_embeddings_embedding_idx"; + jdbcTemplate.execute(dropIndexSql); + + // ivfflat 인덱스 생성 (lists=100은 데이터량에 따라 조정 필요) + String createIndexSql = """ + CREATE INDEX memory_embeddings_embedding_idx + ON memory_embeddings USING ivfflat (embedding vector_cosine_ops) + WITH (lists = 100) + """; + jdbcTemplate.execute(createIndexSql); + log.info("벡터 인덱스 생성 완료"); + } catch (Exception e) { + log.warn("벡터 인덱스 생성 실패 (이미 존재할 수 있음): {}", e.getMessage()); + } + } +} + diff --git a/src/main/java/com/pandol365/dewey/domain/memory/service/EmbeddingClient.java b/src/main/java/com/pandol365/dewey/domain/memory/service/EmbeddingClient.java index 1a44230..2598266 100644 --- a/src/main/java/com/pandol365/dewey/domain/memory/service/EmbeddingClient.java +++ b/src/main/java/com/pandol365/dewey/domain/memory/service/EmbeddingClient.java @@ -24,12 +24,15 @@ public class EmbeddingClient { @Value("${embedding.api.base-url:http://localhost:8000}") private String embeddingBaseUrl; - @Value("${embedding.api.path:/embed}") + @Value("${embedding.api.path:/embeddings}") private String embeddingPath; + @Value("${embedding.api.model:sentence-transformers/all-mpnet-base-v2}") + private String embeddingModel; + /** * 텍스트 임베딩 생성 - * 기대 응답 형식: { "embedding": [float, float, ...] } + * Infinity API 형식: 요청 { "model": "...", "input": "..." }, 응답 { "data": [{"embedding": [...]}] } */ public float[] embed(String text) { try { @@ -37,21 +40,38 @@ public class EmbeddingClient { .scheme(extractScheme(embeddingBaseUrl)) .host(extractHost(embeddingBaseUrl)) .port(extractPort(embeddingBaseUrl)) - .path(embeddingPath == null ? "/embed" : embeddingPath) + .path(embeddingPath == null ? "/embeddings" : embeddingPath) .toUriString(); + // Infinity API 요청 형식: {"model": "...", "input": "..."} + Map request = Map.of( + "model", embeddingModel != null ? embeddingModel : "sentence-transformers/all-mpnet-base-v2", + "input", text + ); + @SuppressWarnings("unchecked") Map resp = restTemplate.postForObject( url, - Map.of("text", text), + request, Map.class ); - if (resp == null || !resp.containsKey("embedding")) { - throw new IllegalStateException("embedding field missing in response"); + if (resp == null || !resp.containsKey("data")) { + throw new IllegalStateException("data field missing in response"); } - Object embObj = resp.get("embedding"); + Object dataObj = resp.get("data"); + if (!(dataObj instanceof List dataList) || dataList.isEmpty()) { + throw new IllegalStateException("data is not a non-empty list"); + } + + // data[0].embedding 추출 + Object firstItem = dataList.get(0); + if (!(firstItem instanceof Map firstMap)) { + throw new IllegalStateException("data[0] is not a map"); + } + + Object embObj = firstMap.get("embedding"); if (!(embObj instanceof List list)) { throw new IllegalStateException("embedding is not a list"); } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index c90fb00..122a960 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -17,6 +17,7 @@ spring.data.redis.host=localhost spring.data.redis.port=6379 # Embedding service -# all-mpnet-base-v2 768d embedding endpoint -embedding.api.base-url=http://localhost:8000 -embedding.api.path=/embed +# all-mpnet-base-v2 768d embedding endpoint (infinity) +embedding.api.base-url=http://192.168.100.220:8000 +embedding.api.path=/embeddings +embedding.api.model=sentence-transformers/all-mpnet-base-v2