diff --git a/apollo.code-workspace b/apollo.code-workspace
new file mode 100644
index 0000000..6f4ce07
--- /dev/null
+++ b/apollo.code-workspace
@@ -0,0 +1,10 @@
+{
+ "folders": [
+ {
+ "path": "."
+ },
+ {
+ "path": "../olympus-common"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/chroma-data/chroma.sqlite3 b/chroma-data/chroma.sqlite3
new file mode 100644
index 0000000..4022737
Binary files /dev/null and b/chroma-data/chroma.sqlite3 differ
diff --git a/chroma-data/chroma.sqlite3.backup.0.5.5 b/chroma-data/chroma.sqlite3.backup.0.5.5
new file mode 100644
index 0000000..392543c
Binary files /dev/null and b/chroma-data/chroma.sqlite3.backup.0.5.5 differ
diff --git a/mvnw b/mvnw
old mode 100644
new mode 100755
diff --git a/pom.xml b/pom.xml
index 30d0872..c722408 100644
--- a/pom.xml
+++ b/pom.xml
@@ -88,11 +88,6 @@
spring-ai-starter-model-azure-openai
-
- org.springframework.ai
- spring-ai-starter-vector-store-chroma
-
-
org.springframework.ai
spring-ai-tika-document-reader
diff --git a/src/main/java/com/olympus/apollo/client/NewDocumentServiceClient.java b/src/main/java/com/olympus/apollo/client/NewDocumentServiceClient.java
new file mode 100644
index 0000000..65f2789
--- /dev/null
+++ b/src/main/java/com/olympus/apollo/client/NewDocumentServiceClient.java
@@ -0,0 +1,146 @@
+package com.olympus.apollo.client;
+
+import com.olympus.apollo.dto.AgentSearchRequest;
+import com.olympus.apollo.dto.AgentSearchResponse;
+import com.olympus.apollo.dto.DeleteDocumentResponse;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.http.client.SimpleClientHttpRequestFactory;
+import org.springframework.stereotype.Service;
+import org.springframework.web.client.RestTemplate;
+import org.springframework.web.util.UriComponentsBuilder;
+
+@Service
+public class NewDocumentServiceClient {
+
+ private static final Logger logger = LoggerFactory.getLogger(NewDocumentServiceClient.class);
+
+ @Value("${new-document-service.url}")
+ private String serviceUrl;
+
+ private final RestTemplate restTemplate;
+
+ public NewDocumentServiceClient() {
+ SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
+ factory.setConnectTimeout(5000); // 5 seconds connection timeout
+ factory.setReadTimeout(60000); // 60 seconds read timeout
+ factory.setBufferRequestBody(false); // Don't buffer request body
+
+ this.restTemplate = new RestTemplate(factory);
+ logger.info("RestTemplate initialized with timeouts - connect: 5s, read: 60s");
+ }
+
+ /**
+ * Delete all documents with the given KsDocumentId.
+ *
+ * This endpoint will:
+ * 1. Search for all documents with the specified KsDocumentId
+ * 2. Delete all matching documents from the index
+ * 3. Return the count of deleted documents
+ *
+ * @param ksDocumentId The KsDocumentId to delete
+ * @param project Project name for index isolation (default: "default")
+ * @param esUrl Elasticsearch URL (default: "http://localhost:9200")
+ * @return DeleteDocumentResponse with deletion details
+ */
+ public ResponseEntity deleteDocumentByKsDocumentId(
+ String ksDocumentId, String project, String esUrl) {
+
+ try {
+ // Build the URL with query parameters
+ UriComponentsBuilder uriBuilder = UriComponentsBuilder
+ .fromHttpUrl(serviceUrl)
+ .path("/api/documents/by-ks-document-id/{ks_document_id}")
+ .queryParam("project", project != null ? project : "default");
+
+ if (esUrl != null && !esUrl.isEmpty()) {
+ uriBuilder.queryParam("es_url", esUrl);
+ }
+
+ String url = uriBuilder.buildAndExpand(ksDocumentId).toUriString();
+
+ logger.info("Deleting document with KsDocumentId: {} from search index at URL: {}", ksDocumentId, url);
+
+ // Execute DELETE request
+ ResponseEntity response = restTemplate.exchange(
+ url,
+ HttpMethod.DELETE,
+ null,
+ DeleteDocumentResponse.class
+ );
+
+ logger.info("Delete request completed with status: {}", response.getStatusCode());
+ return response;
+
+ } catch (Exception e) {
+ logger.error("Error deleting document with KsDocumentId {}: {}", ksDocumentId, e.getMessage(), e);
+ throw e;
+ }
+ }
+
+ /**
+ * Unified search endpoint for AI agents.
+ *
+ * Supports multiple search types:
+ * - semantic: Neural/vector similarity search
+ * - keyword: Exact term matching (AND operator)
+ * - fulltext: Analyzed text matching with fuzziness
+ * - hybrid: Combines semantic similarity with entity matching
+ *
+ * @param searchRequest The search request containing query, project, and search parameters
+ * @param esUrl Elasticsearch URL (default: "http://localhost:9200")
+ * @return AgentSearchResponse with search results and formatted context
+ */
+ public ResponseEntity agentSearch(
+ AgentSearchRequest searchRequest, String esUrl) {
+
+ try {
+ // Build the URL with query parameters
+ UriComponentsBuilder uriBuilder = UriComponentsBuilder
+ .fromHttpUrl(serviceUrl)
+ .path("/api/agent/search");
+
+
+ String url = uriBuilder.toUriString();
+
+ logger.info("Performing agent search for project: {} with query: {} using search type: {}",
+ searchRequest.getProject(), searchRequest.getQuery(), searchRequest.getSearchType());
+
+ // Set up headers
+ HttpHeaders headers = new HttpHeaders();
+ headers.setContentType(MediaType.APPLICATION_JSON);
+
+ // Create request entity
+ HttpEntity requestEntity = new HttpEntity<>(searchRequest, headers);
+
+ // Execute POST request
+ logger.info("Executing POST request to: {}", url);
+ ResponseEntity response = restTemplate.exchange(
+ url,
+ HttpMethod.POST,
+ requestEntity,
+ AgentSearchResponse.class
+ );
+
+ logger.info("Agent search HTTP call completed with status: {}", response.getStatusCode());
+
+ if (response.getBody() != null) {
+ logger.info("Response body received with {} results", response.getBody().getTotalResults());
+ }
+
+ logger.info("Returning response from client");
+ return response;
+
+ } catch (Exception e) {
+ logger.error("Error performing agent search for project {}: {}",
+ searchRequest.getProject(), e.getMessage(), e);
+ throw e;
+ }
+ }
+}
diff --git a/src/main/java/com/olympus/apollo/config/OpenApiConfig.java b/src/main/java/com/olympus/apollo/config/OpenApiConfig.java
new file mode 100644
index 0000000..0c63ab2
--- /dev/null
+++ b/src/main/java/com/olympus/apollo/config/OpenApiConfig.java
@@ -0,0 +1,32 @@
+package com.olympus.apollo.config;
+
+import io.swagger.v3.oas.models.Components;
+import io.swagger.v3.oas.models.OpenAPI;
+import io.swagger.v3.oas.models.info.Info;
+import io.swagger.v3.oas.models.security.SecurityRequirement;
+import io.swagger.v3.oas.models.security.SecurityScheme;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+public class OpenApiConfig {
+
+ @Bean
+ public OpenAPI customOpenAPI() {
+ final String securitySchemeName = "bearerAuth";
+
+ return new OpenAPI()
+ .info(new Info()
+ .title("Apollo API")
+ .version("1.0")
+ .description("Apollo Service API Documentation"))
+ .addSecurityItem(new SecurityRequirement().addList(securitySchemeName))
+ .components(new Components()
+ .addSecuritySchemes(securitySchemeName, new SecurityScheme()
+ .name(securitySchemeName)
+ .type(SecurityScheme.Type.HTTP)
+ .scheme("bearer")
+ .bearerFormat("JWT")
+ .description("Enter JWT token obtained from /api/auth/login endpoint")));
+ }
+}
diff --git a/src/main/java/com/olympus/apollo/controllers/FeApi/KsDocumentController.java b/src/main/java/com/olympus/apollo/controllers/FeApi/KsDocumentController.java
index cb83b40..c0f7677 100644
--- a/src/main/java/com/olympus/apollo/controllers/FeApi/KsDocumentController.java
+++ b/src/main/java/com/olympus/apollo/controllers/FeApi/KsDocumentController.java
@@ -3,10 +3,13 @@ package com.olympus.apollo.controllers.FeApi;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.*;
+import com.olympus.apollo.dto.KnowledgeSystemNode;
import com.olympus.model.apollo.KSDocument;
import com.olympus.apollo.repository.KSDocumentRepository;
+import com.olympus.apollo.security.entity.User;
import com.olympus.apollo.services.KSDocumentService;
import org.springframework.core.io.Resource;
import org.springframework.http.ResponseEntity;
@@ -45,6 +48,23 @@ public class KsDocumentController {
return ksDocumentService.downloadKSDocument(doc);
-}
+ }
+
+ /**
+ * Deletes a document by its ID
+ * The service method will be extended to handle search index cancellation
+ * @param id The ID of the document to delete
+ * @return ResponseEntity indicating success or failure
+ */
+ @DeleteMapping("/{id}")
+ public ResponseEntity deleteDocument(@PathVariable String id) {
+ boolean deleted = ksDocumentService.deleteDocument(id);
+
+ if (deleted) {
+ return ResponseEntity.ok().build();
+ } else {
+ return ResponseEntity.notFound().build();
+ }
+ }
}
diff --git a/src/main/java/com/olympus/apollo/controllers/KSFileController.java b/src/main/java/com/olympus/apollo/controllers/KSFileController.java
index 39b78b3..9890412 100644
--- a/src/main/java/com/olympus/apollo/controllers/KSFileController.java
+++ b/src/main/java/com/olympus/apollo/controllers/KSFileController.java
@@ -15,6 +15,7 @@ import java.util.Optional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.ExceptionHandler;
@@ -52,13 +53,15 @@ public class KSFileController {
private static final Logger logger = LoggerFactory.getLogger(KSFileController.class);
- private String document_path="/mnt/apollo_storage/documents";
+ @Value("${document.storage.path}")
+ private String document_path;
@PostMapping("/upload")
public ResponseEntity handleFileUpload(
@RequestParam("file") MultipartFile file,
- @ModelAttribute FileUploadDTO fileUploadDTO
+ @ModelAttribute FileUploadDTO fileUploadDTO,
+ @RequestParam(value = "version", required = false) String version
) {
try (InputStream inputStream = file.getInputStream()) {
@@ -88,7 +91,13 @@ public class KSFileController {
ksDocument.setFileName(file.getOriginalFilename());
ksDocument.setName(file.getOriginalFilename());
ksDocument.setDescription(fileUploadDTO.getDescription());
- ksDocument.setIngestionStatus("INGESTION_QUEUE");
+
+ if (version != null && version.contains("v2")) {
+ ksDocument.setIngestionStatus("IGNORE_INGESTION");
+ ksDocument.setIngestionStatusV2("INGESTION_QUEUE");
+ } else {
+ ksDocument.setIngestionStatus("INGESTION_QUEUE");
+ }
ksDocument.setIngestionDateFormat(new SimpleDateFormat("MM/dd/yy").format(new Date()));
Date now = new Date();
@@ -103,6 +112,14 @@ public class KSFileController {
metadata.put("KsDocSource", fileUploadDTO.getKsDocSource());
metadata.put("KsFileSource", file.getOriginalFilename());
metadata.put("KsProjectName", fileUploadDTO.getKsProjectName());
+
+ logger.info("[UPLOAD] ksKnowledgePath received: {}", fileUploadDTO.getKsKnowledgePath());
+ if (fileUploadDTO.getKsKnowledgePath() != null && !fileUploadDTO.getKsKnowledgePath().isEmpty()) {
+ metadata.put("ksKnowledgePath", fileUploadDTO.getKsKnowledgePath());
+ logger.info("[UPLOAD] Added ksKnowledgePath to metadata: {}", fileUploadDTO.getKsKnowledgePath());
+ } else {
+ logger.warn("[UPLOAD] ksKnowledgePath is null or empty");
+ }
ksIngestionInfo.setMetadata(metadata);
ksIngestionInfo.setDefaultChunkSize(fileUploadDTO.getDefaultChunkSize());
diff --git a/src/main/java/com/olympus/apollo/controllers/KnowledgeTreeController.java b/src/main/java/com/olympus/apollo/controllers/KnowledgeTreeController.java
new file mode 100644
index 0000000..0d34e61
--- /dev/null
+++ b/src/main/java/com/olympus/apollo/controllers/KnowledgeTreeController.java
@@ -0,0 +1,393 @@
+package com.olympus.apollo.controllers;
+
+import java.io.FileOutputStream;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.http.ResponseEntity;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.ModelAttribute;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.multipart.MultipartFile;
+
+import com.olympus.apollo.client.NewDocumentServiceClient;
+import com.olympus.apollo.dto.AgentSearchRequest;
+import com.olympus.apollo.dto.AgentSearchResponse;
+import com.olympus.apollo.dto.KnowledgeSystemNode;
+import com.olympus.apollo.exception.StorageException;
+import com.olympus.apollo.repository.KSDocumentRepository;
+import com.olympus.apollo.repository.KSVideoRepository;
+import com.olympus.apollo.security.entity.User;
+import com.olympus.apollo.services.KSDocumentService;
+import com.olympus.dto.FileUploadDTO;
+import com.olympus.dto.VideoUploadDTO;
+import com.olympus.model.apollo.KSDocument;
+import com.olympus.model.apollo.KSIngestionInfo;
+import com.olympus.model.apollo.KSVideo;
+import com.olympus.model.apollo.KSVideoIngestionInfo;
+
+/**
+ * KnowledgeTree Controller - V2 API
+ * This controller manages the new version of Knowledge Tree operations including
+ * file uploads and video uploads for the new application flow.
+ * Legacy endpoints remain in KSFileController and VideoController for backward compatibility.
+ */
+@RestController
+@RequestMapping("/api/v2/knowledgetree")
+public class KnowledgeTreeController {
+
+ @Autowired
+ private KSDocumentRepository ksDocumentRepository;
+
+ @Autowired
+ private KSVideoRepository ksVideoRepository;
+
+ @Autowired
+ private KSDocumentService ksDocumentService;
+
+ @Autowired
+ private NewDocumentServiceClient newDocumentServiceClient;
+
+ @Value("${document.storage.path}")
+ private String documentStoragePath;
+
+ @Value("${video.storage.path}")
+ private String videoStoragePath;
+
+ private static final Logger logger = LoggerFactory.getLogger(KnowledgeTreeController.class);
+
+ /**
+ * Upload a file to the knowledge tree (V2)
+ * Enhanced version with improved metadata handling and knowledge path support
+ *
+ * @param file The file to upload
+ * @param fileUploadDTO Upload metadata
+ * @return ResponseEntity containing the created KSDocument
+ */
+ @PostMapping("/files/upload")
+ public ResponseEntity handleFileUpload(
+ @RequestParam("file") MultipartFile file,
+ @ModelAttribute FileUploadDTO fileUploadDTO) {
+
+ logger.info("[V2-UPLOAD] Starting file upload for: {}", file.getOriginalFilename());
+ logger.info("[V2-UPLOAD] Project: {}, Knowledge Path: {}",
+ fileUploadDTO.getKsProjectName(),
+ fileUploadDTO.getKsKnowledgePath());
+
+ try (InputStream inputStream = file.getInputStream()) {
+ // Validate file
+ if (file.isEmpty()) {
+ logger.error("[V2-UPLOAD] File is empty: {}", file.getOriginalFilename());
+ throw new StorageException("Failed to store empty file.");
+ }
+
+ // Create storage directory
+ Path folderPath = Paths.get(documentStoragePath)
+ .resolve(fileUploadDTO.getKsProjectName())
+ .normalize()
+ .toAbsolutePath();
+
+ Files.createDirectories(folderPath);
+ logger.info("[V2-UPLOAD] Created folder: {}", folderPath);
+
+ // Store file
+ String filePath = folderPath.resolve(file.getOriginalFilename()).toString();
+ try (OutputStream outputStream = new FileOutputStream(filePath)) {
+ byte[] buffer = new byte[8 * 1024];
+ int bytesRead;
+ while ((bytesRead = inputStream.read(buffer)) != -1) {
+ outputStream.write(buffer, 0, bytesRead);
+ }
+ logger.info("[V2-UPLOAD] File stored successfully at: {}", filePath);
+ }
+
+ // Create KSDocument entity with V2 ingestion status
+ KSDocument ksDocument = new KSDocument();
+ ksDocument.setFilePath(filePath);
+ ksDocument.setFileName(file.getOriginalFilename());
+ ksDocument.setName(file.getOriginalFilename());
+ ksDocument.setDescription(fileUploadDTO.getDescription());
+
+ // V2 uses new ingestion flow
+ ksDocument.setIngestionStatus("IGNORE_INGESTION");
+ ksDocument.setIngestionStatusV2("INGESTION_QUEUE");
+
+ Date now = new Date();
+ ksDocument.setIngestionDate(now);
+ ksDocument.setIngestionDateFormat(new SimpleDateFormat("MM/dd/yy").format(now));
+
+ // Build ingestion info with enhanced metadata
+ KSIngestionInfo ksIngestionInfo = new KSIngestionInfo();
+ ksIngestionInfo.setType(fileUploadDTO.getType());
+
+ HashMap metadata = new HashMap<>();
+ metadata.put("KsApplicationName", fileUploadDTO.getKsApplicationName());
+ metadata.put("KsDoctype", fileUploadDTO.getKsDocType());
+ metadata.put("KsDocSource", fileUploadDTO.getKsDocSource());
+ metadata.put("KsFileSource", file.getOriginalFilename());
+ metadata.put("KsProjectName", fileUploadDTO.getKsProjectName());
+
+ // Knowledge path support for V2
+ if (fileUploadDTO.getKsKnowledgePath() != null && !fileUploadDTO.getKsKnowledgePath().isEmpty()) {
+ metadata.put("ksKnowledgePath", fileUploadDTO.getKsKnowledgePath());
+ logger.info("[V2-UPLOAD] Knowledge path added: {}", fileUploadDTO.getKsKnowledgePath());
+ } else {
+ logger.warn("[V2-UPLOAD] No knowledge path provided");
+ }
+
+ ksIngestionInfo.setMetadata(metadata);
+ ksIngestionInfo.setDefaultChunkSize(fileUploadDTO.getDefaultChunkSize());
+ ksIngestionInfo.setMinChunkSize(fileUploadDTO.getMinChunkSize());
+ ksIngestionInfo.setMaxNumberOfChunks(fileUploadDTO.getMaxNumberOfChunks());
+ ksIngestionInfo.setMinChunkSizeToEmbed(fileUploadDTO.getMinChunkSizeToEmbed());
+
+ ksDocument.setIngestionInfo(ksIngestionInfo);
+ ksDocumentRepository.save(ksDocument);
+
+ logger.info("[V2-UPLOAD] Document saved successfully with ID: {}", ksDocument.getId());
+ return ResponseEntity.ok(ksDocument);
+
+ } catch (StorageException e) {
+ logger.error("[V2-UPLOAD] Storage exception: {}", e.getMessage(), e);
+ return ResponseEntity.status(400).body(null);
+ } catch (Exception e) {
+ logger.error("[V2-UPLOAD] Unexpected error during file upload: {}", e.getMessage(), e);
+ return ResponseEntity.status(500).body(null);
+ }
+ }
+
+ /**
+ * Upload a video to the knowledge tree (V2)
+ * Enhanced version with improved metadata handling
+ *
+ * @param file The video file to upload
+ * @param videoUploadDTO Upload metadata
+ * @return ResponseEntity containing the created KSVideo
+ */
+ @PostMapping("/videos/upload")
+ public ResponseEntity handleVideoUpload(
+ @RequestParam("file") MultipartFile file,
+ @ModelAttribute VideoUploadDTO videoUploadDTO) {
+
+ logger.info("[V2-VIDEO-UPLOAD] Starting video upload for: {}", file.getOriginalFilename());
+ logger.info("[V2-VIDEO-UPLOAD] Project: {}, Video Group: {}",
+ videoUploadDTO.getKsProjectName(),
+ videoUploadDTO.getKsVideoGroupId());
+
+ try (InputStream inputStream = file.getInputStream()) {
+ // Validate file
+ if (file.isEmpty()) {
+ logger.error("[V2-VIDEO-UPLOAD] File is empty: {}", file.getOriginalFilename());
+ throw new StorageException("Failed to store empty file.");
+ }
+
+ // Create storage directory
+ Path folderPath = Paths.get(videoStoragePath)
+ .resolve(videoUploadDTO.getKsProjectName())
+ .normalize()
+ .toAbsolutePath();
+
+ Files.createDirectories(folderPath);
+ logger.info("[V2-VIDEO-UPLOAD] Created folder: {}", folderPath);
+
+ // Store video file
+ String filePath = folderPath.resolve(file.getOriginalFilename()).toString();
+ try (OutputStream outputStream = new FileOutputStream(filePath)) {
+ byte[] buffer = new byte[8 * 1024];
+ int bytesRead;
+ while ((bytesRead = inputStream.read(buffer)) != -1) {
+ outputStream.write(buffer, 0, bytesRead);
+ }
+ logger.info("[V2-VIDEO-UPLOAD] Video stored successfully at: {}", filePath);
+ }
+
+ // Create KSVideo entity
+ KSVideo ksVideo = new KSVideo();
+ ksVideo.setFilePath(filePath);
+ ksVideo.setFileName(file.getOriginalFilename());
+ ksVideo.setName(file.getOriginalFilename());
+ ksVideo.setDescription(videoUploadDTO.getDescription());
+ ksVideo.setIngestionStatus("IGNORE_INGESTION");
+ ksVideo.setIngestionStatusV2("INGESTION_QUEUE");
+ Date now = new Date();
+ ksVideo.setIngestionDate(now);
+ ksVideo.setIngestionDateFormat(new SimpleDateFormat("MM/dd/yy").format(now));
+
+ // Build video ingestion info
+ KSVideoIngestionInfo ksVideoIngestionInfo = new KSVideoIngestionInfo();
+ ksVideoIngestionInfo.setType(videoUploadDTO.getType());
+
+ HashMap metadata = new HashMap<>();
+ metadata.put("KsApplicationName", videoUploadDTO.getKsApplicationName());
+ metadata.put("KsDoctype", videoUploadDTO.getKsDocType());
+ metadata.put("KsDocSource", videoUploadDTO.getKsDocSource());
+ metadata.put("KsFileSource", file.getOriginalFilename());
+ metadata.put("KsVideoGroupId", videoUploadDTO.getKsVideoGroupId());
+ metadata.put("KsProjectName", videoUploadDTO.getKsProjectName());
+
+ // Knowledge path support for V2 videos
+ if (videoUploadDTO.getKsKnowledgePath() != null && !videoUploadDTO.getKsKnowledgePath().isEmpty()) {
+ metadata.put("ksKnowledgePath", videoUploadDTO.getKsKnowledgePath());
+ logger.info("[V2-VIDEO-UPLOAD] Knowledge path added: {}", videoUploadDTO.getKsKnowledgePath());
+ } else {
+ logger.warn("[V2-VIDEO-UPLOAD] No knowledge path provided");
+ }
+
+ ksVideoIngestionInfo.setMetadata(metadata);
+ ksVideoIngestionInfo.setNumberOfChunkToEmbed(videoUploadDTO.getNumberOfChunkToEmbed());
+ ksVideoIngestionInfo.setChunkDurationInSeconds(videoUploadDTO.getChunkDurationInSeconds());
+ ksVideoIngestionInfo.setLanguage(videoUploadDTO.getLanguage());
+
+ ksVideo.setIngestionInfo(ksVideoIngestionInfo);
+ ksVideoRepository.save(ksVideo);
+
+ logger.info("[V2-VIDEO-UPLOAD] Video saved successfully with ID: {}", ksVideo.getId());
+ return ResponseEntity.ok(ksVideo);
+
+ } catch (StorageException e) {
+ logger.error("[V2-VIDEO-UPLOAD] Storage exception: {}", e.getMessage(), e);
+ return ResponseEntity.status(400).body(null);
+ } catch (Exception e) {
+ logger.error("[V2-VIDEO-UPLOAD] Unexpected error during video upload: {}", e.getMessage(), e);
+ return ResponseEntity.status(500).body(null);
+ }
+ }
+
+ /**
+ * Returns a filesystem-like tree structure based on ksKnowledgePath metadata
+ * for the current user's selected project
+ * @return KnowledgeSystemNode representing the root of the tree
+ */
+ @GetMapping("/knowledge-tree")
+ public ResponseEntity getKnowledgeTree() {
+ User principal = (User) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
+ String projectName = principal.getSelectedProject().getInternal_name();
+
+ KnowledgeSystemNode tree = ksDocumentService.buildKnowledgePathTree(projectName);
+
+ if (tree == null) {
+ return ResponseEntity.noContent().build();
+ }
+
+ return ResponseEntity.ok(tree);
+ }
+
+ /**
+ * Returns a filesystem-like tree structure based on ksKnowledgePath metadata
+ * for a specific project
+ * @param projectName The name of the project
+ * @return KnowledgeSystemNode representing the root of the tree
+ */
+ @GetMapping("/knowledge-tree/{projectName}")
+ public ResponseEntity getKnowledgeTreeByProject(@PathVariable String projectName) {
+ KnowledgeSystemNode tree = ksDocumentService.buildKnowledgePathTree(projectName);
+
+ if (tree == null) {
+ return ResponseEntity.noContent().build();
+ }
+
+ return ResponseEntity.ok(tree);
+ }
+
+
+
+ /**
+ * Get all distinct metadata keys and their possible values from both KsDocument and KsVideo collections
+ * for the current user's selected project
+ *
+ * @return Map where keys are metadata field names and values are sets of all possible values for each field
+ */
+ @GetMapping("/metadata/all-values")
+ public ResponseEntity