From c3abc84057fa9ab7f7f679c6f40e4b2899207957 Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 21 Aug 2025 08:48:17 +0800 Subject: [PATCH] Refactor API routes for collections and segments management - Rearranged and updated API endpoints for collections, including adding GetCollections and adjusting the order of operations for better clarity. - Enhanced segment management by introducing new endpoints for segment scores, weights, and votes, along with async operations for adding and updating segments. - Improved error handling for missing document IDs in segment-related functions, ensuring robust validation. - Streamlined response structures to include document and segment IDs in relevant endpoints, enhancing the API's usability. --- openapi/kb/graph.go | 206 ++++++++++++++++++++++++ openapi/kb/hit.go | 362 ++++++++++++++++++++++++++++++++++++++++++ openapi/kb/kb.go | 51 ++++-- openapi/kb/segment.go | 257 +++++++++++++++++++++++++++++- openapi/kb/store.go | 65 ++++++-- openapi/kb/vote.go | 356 +++++++++++++++++++++++++++++++++++++++++ 6 files changed, 1269 insertions(+), 28 deletions(-) create mode 100644 openapi/kb/graph.go create mode 100644 openapi/kb/hit.go create mode 100644 openapi/kb/vote.go diff --git a/openapi/kb/graph.go b/openapi/kb/graph.go new file mode 100644 index 00000000..bf37688b --- /dev/null +++ b/openapi/kb/graph.go @@ -0,0 +1,206 @@ +package kb + +import ( + "github.com/gin-gonic/gin" + "github.com/yaoapp/yao/kb" + "github.com/yaoapp/yao/openapi/response" +) + +// Graph Management Handlers + +// GetSegmentGraph gets the graph (entities and relationships) for a specific segment +func GetSegmentGraph(c *gin.Context) { + // Extract docID from URL path + docID := c.Param("docID") + if docID == "" { + errorResp := &response.ErrorResponse{ + Code: response.ErrInvalidRequest.Code, + ErrorDescription: "Document ID is required", + } + response.RespondWithError(c, response.StatusBadRequest, errorResp) + return + } + + // Extract segmentID from URL path + segmentID := c.Param("segmentID") + if segmentID == "" { + errorResp := &response.ErrorResponse{ + Code: response.ErrInvalidRequest.Code, + ErrorDescription: "Segment ID is required", + } + response.RespondWithError(c, response.StatusBadRequest, errorResp) + return + } + + // Check if kb.Instance is available + if kb.Instance == nil { + errorResp := &response.ErrorResponse{ + Code: response.ErrServerError.Code, + ErrorDescription: "Knowledge base not initialized", + } + response.RespondWithError(c, response.StatusInternalServerError, errorResp) + return + } + + // Parse query parameters for graph options + options := make(map[string]interface{}) + + // Include entities (default: true) + if includeEntities := c.Query("include_entities"); includeEntities == "false" { + options["include_entities"] = false + } else { + options["include_entities"] = true + } + + // Include relationships (default: true) + if includeRelationships := c.Query("include_relationships"); includeRelationships == "false" { + options["include_relationships"] = false + } else { + options["include_relationships"] = true + } + + // Include metadata (default: true) + if includeMetadata := c.Query("include_metadata"); includeMetadata == "false" { + options["include_metadata"] = false + } else { + options["include_metadata"] = true + } + + // TODO: Implement document permission validation for docID + // TODO: Implement get segment graph logic + // TODO: Call kb.Instance.GetSegmentGraph(c.Request.Context(), segmentID, options) + + // Return mock response for now + result := gin.H{ + "entities": []interface{}{}, + "relationships": []interface{}{}, + "document_id": docID, + "segment_id": segmentID, + "options": options, + } + + response.RespondWithSuccess(c, response.StatusOK, result) +} + +// ExtractSegmentEntities re-extracts entities and relationships for a specific segment (synchronous) +func ExtractSegmentEntities(c *gin.Context) { + // Extract docID from URL path + docID := c.Param("docID") + if docID == "" { + errorResp := &response.ErrorResponse{ + Code: response.ErrInvalidRequest.Code, + ErrorDescription: "Document ID is required", + } + response.RespondWithError(c, response.StatusBadRequest, errorResp) + return + } + + // Extract segmentID from URL path + segmentID := c.Param("segmentID") + if segmentID == "" { + errorResp := &response.ErrorResponse{ + Code: response.ErrInvalidRequest.Code, + ErrorDescription: "Segment ID is required", + } + response.RespondWithError(c, response.StatusBadRequest, errorResp) + return + } + + // Check if kb.Instance is available + if kb.Instance == nil { + errorResp := &response.ErrorResponse{ + Code: response.ErrServerError.Code, + ErrorDescription: "Knowledge base not initialized", + } + response.RespondWithError(c, response.StatusInternalServerError, errorResp) + return + } + + // Parse extraction options from request body (optional) + var extractOptions map[string]interface{} + if err := c.ShouldBindJSON(&extractOptions); err != nil { + // If no body provided, use default options + extractOptions = make(map[string]interface{}) + } + + // TODO: Implement document permission validation for docID + // TODO: Implement extract segment entities logic + // TODO: Call kb.Instance.ExtractSegmentEntities(c.Request.Context(), segmentID, extractOptions) + + // Return mock response for now + result := gin.H{ + "message": "Entities and relationships extracted successfully", + "document_id": docID, + "segment_id": segmentID, + "entities_count": 0, + "relationships_count": 0, + "extraction_options": extractOptions, + } + + response.RespondWithSuccess(c, response.StatusOK, result) +} + +// ExtractSegmentEntitiesAsync re-extracts entities and relationships for a specific segment (asynchronous) +func ExtractSegmentEntitiesAsync(c *gin.Context) { + // Extract docID from URL path + docID := c.Param("docID") + if docID == "" { + errorResp := &response.ErrorResponse{ + Code: response.ErrInvalidRequest.Code, + ErrorDescription: "Document ID is required", + } + response.RespondWithError(c, response.StatusBadRequest, errorResp) + return + } + + // Extract segmentID from URL path + segmentID := c.Param("segmentID") + if segmentID == "" { + errorResp := &response.ErrorResponse{ + Code: response.ErrInvalidRequest.Code, + ErrorDescription: "Segment ID is required", + } + response.RespondWithError(c, response.StatusBadRequest, errorResp) + return + } + + // Check if kb.Instance is available + if kb.Instance == nil { + errorResp := &response.ErrorResponse{ + Code: response.ErrServerError.Code, + ErrorDescription: "Knowledge base not initialized", + } + response.RespondWithError(c, response.StatusInternalServerError, errorResp) + return + } + + // Parse extraction options from request body (optional) + var extractOptions map[string]interface{} + if err := c.ShouldBindJSON(&extractOptions); err != nil { + // If no body provided, use default options + extractOptions = make(map[string]interface{}) + } + + // TODO: Implement document permission validation for docID + + // Create and run job + job := NewJob() + jobID := job.Run(func() { + // TODO: Implement async extract segment entities logic + // err := ExtractSegmentEntitiesProcess(context.Background(), segmentID, extractOptions, job.ID) + // For now, just simulate async processing + // if err != nil { + // log.Error("Async entity extraction failed: %v", err) + // } + }) + + // Return job ID for status tracking + result := gin.H{ + "job_id": jobID, + "message": "Entity extraction started", + "document_id": docID, + "segment_id": segmentID, + } + + response.RespondWithSuccess(c, response.StatusCreated, result) +} diff --git a/openapi/kb/hit.go b/openapi/kb/hit.go new file mode 100644 index 00000000..3fa89d47 --- /dev/null +++ b/openapi/kb/hit.go @@ -0,0 +1,362 @@ +package kb + +import ( + "net/http" + "strconv" + "strings" + + "github.com/gin-gonic/gin" + "github.com/yaoapp/yao/openapi/response" +) + +// Hit Management Handlers + +// ScrollHits scrolls hits with iterator-style pagination for a specific segment +func ScrollHits(c *gin.Context) { + // Extract docID from URL path + docID := c.Param("docID") + if docID == "" { + errorResp := &response.ErrorResponse{ + Code: response.ErrInvalidRequest.Code, + ErrorDescription: "Document ID is required", + } + response.RespondWithError(c, response.StatusBadRequest, errorResp) + return + } + + // Extract segmentID from URL path + segmentID := c.Param("segmentID") + if segmentID == "" { + errorResp := &response.ErrorResponse{ + Code: response.ErrInvalidRequest.Code, + ErrorDescription: "Segment ID is required", + } + response.RespondWithError(c, response.StatusBadRequest, errorResp) + return + } + + // Parse query parameters for scroll options + options := map[string]interface{}{ + "document_id": docID, + "segment_id": segmentID, + "limit": 100, // Default limit + } + + // Parse limit (default: 100) + if limitStr := c.Query("limit"); limitStr != "" { + if limit, err := strconv.Atoi(limitStr); err == nil && limit > 0 { + options["limit"] = limit + } + } + + // Parse scroll_id parameter for continuing pagination + if scrollID := strings.TrimSpace(c.Query("scroll_id")); scrollID != "" { + options["scroll_id"] = scrollID + } + + // Parse order_by parameter + if orderBy := strings.TrimSpace(c.Query("order_by")); orderBy != "" { + orderByFields := strings.Split(orderBy, ",") + // Trim spaces from each field + for i, field := range orderByFields { + orderByFields[i] = strings.TrimSpace(field) + } + options["order_by"] = orderByFields + } + + // Parse filter parameters + filter := make(map[string]interface{}) + if hitType := c.Query("hit_type"); hitType != "" { + filter["hit_type"] = hitType + } + if userID := c.Query("user_id"); userID != "" { + filter["user_id"] = userID + } + if sessionID := c.Query("session_id"); sessionID != "" { + filter["session_id"] = sessionID + } + if len(filter) > 0 { + options["filter"] = filter + } + + // TODO: Implement document permission validation for docID + // TODO: Implement scroll hits logic with GraphRag or database + // TODO: Call kb.Instance.ScrollHits(c.Request.Context(), segmentID, options) + + // Return mock response for now + result := gin.H{ + "hits": []interface{}{}, + "scroll_id": nil, + "has_more": false, + "total": 0, + "options": options, + } + + response.RespondWithSuccess(c, response.StatusOK, result) +} + +// GetHits gets hits for a specific segment (simple list, no pagination) +func GetHits(c *gin.Context) { + // Extract docID from URL path + docID := c.Param("docID") + if docID == "" { + errorResp := &response.ErrorResponse{ + Code: response.ErrInvalidRequest.Code, + ErrorDescription: "Document ID is required", + } + response.RespondWithError(c, response.StatusBadRequest, errorResp) + return + } + + // Extract segmentID from URL path + segmentID := c.Param("segmentID") + if segmentID == "" { + errorResp := &response.ErrorResponse{ + Code: response.ErrInvalidRequest.Code, + ErrorDescription: "Segment ID is required", + } + response.RespondWithError(c, response.StatusBadRequest, errorResp) + return + } + + // Parse basic filter parameters + filter := make(map[string]interface{}) + if hitType := c.Query("hit_type"); hitType != "" { + filter["hit_type"] = hitType + } + if userID := c.Query("user_id"); userID != "" { + filter["user_id"] = userID + } + if sessionID := c.Query("session_id"); sessionID != "" { + filter["session_id"] = sessionID + } + + // Parse limit parameter (optional, for basic limiting without pagination) + var limit int + if limitStr := c.Query("limit"); limitStr != "" { + if l, err := strconv.Atoi(limitStr); err == nil && l > 0 { + limit = l + } + } + + // TODO: Implement document permission validation for docID + // TODO: Implement get hits logic (simple query without pagination) + // TODO: Call kb.Instance.GetHits(c.Request.Context(), segmentID, filter, limit) + + // Return mock response for now + result := gin.H{ + "hits": []interface{}{}, + "document_id": docID, + "segment_id": segmentID, + "total": 0, + } + + if len(filter) > 0 { + result["filter"] = filter + } + if limit > 0 { + result["limit"] = limit + } + + response.RespondWithSuccess(c, response.StatusOK, result) +} + +// GetHit gets a specific hit by ID +func GetHit(c *gin.Context) { + // Extract docID from URL path + docID := c.Param("docID") + if docID == "" { + errorResp := &response.ErrorResponse{ + Code: response.ErrInvalidRequest.Code, + ErrorDescription: "Document ID is required", + } + response.RespondWithError(c, response.StatusBadRequest, errorResp) + return + } + + // Extract segmentID from URL path + segmentID := c.Param("segmentID") + if segmentID == "" { + errorResp := &response.ErrorResponse{ + Code: response.ErrInvalidRequest.Code, + ErrorDescription: "Segment ID is required", + } + response.RespondWithError(c, response.StatusBadRequest, errorResp) + return + } + + // Extract hitID from URL path + hitID := c.Param("hitID") + if hitID == "" { + errorResp := &response.ErrorResponse{ + Code: response.ErrInvalidRequest.Code, + ErrorDescription: "Hit ID is required", + } + response.RespondWithError(c, response.StatusBadRequest, errorResp) + return + } + + // TODO: Implement document permission validation for docID + // TODO: Implement get hit detail logic + c.JSON(http.StatusOK, gin.H{ + "hit": nil, + "document_id": docID, + "segment_id": segmentID, + "hit_id": hitID, + }) +} + +// AddHits adds new hits to a segment +func AddHits(c *gin.Context) { + // Extract docID from URL path + docID := c.Param("docID") + if docID == "" { + errorResp := &response.ErrorResponse{ + Code: response.ErrInvalidRequest.Code, + ErrorDescription: "Document ID is required", + } + response.RespondWithError(c, response.StatusBadRequest, errorResp) + return + } + + // Extract segmentID from URL path + segmentID := c.Param("segmentID") + if segmentID == "" { + errorResp := &response.ErrorResponse{ + Code: response.ErrInvalidRequest.Code, + ErrorDescription: "Segment ID is required", + } + response.RespondWithError(c, response.StatusBadRequest, errorResp) + return + } + + // TODO: Implement document permission validation for docID + // TODO: Implement add hit logic + c.JSON(http.StatusOK, gin.H{ + "message": "Hit added successfully", + "document_id": docID, + "segment_id": segmentID, + "hit_id": "placeholder-hit-id", + }) +} + +// UpdateHits updates hits in batch +func UpdateHits(c *gin.Context) { + // Extract docID from URL path + docID := c.Param("docID") + if docID == "" { + errorResp := &response.ErrorResponse{ + Code: response.ErrInvalidRequest.Code, + ErrorDescription: "Document ID is required", + } + response.RespondWithError(c, response.StatusBadRequest, errorResp) + return + } + + // Extract segmentID from URL path + segmentID := c.Param("segmentID") + if segmentID == "" { + errorResp := &response.ErrorResponse{ + Code: response.ErrInvalidRequest.Code, + ErrorDescription: "Segment ID is required", + } + response.RespondWithError(c, response.StatusBadRequest, errorResp) + return + } + + // Parse hit_ids from query parameter or request body + var hitIDs []string + + // Try query parameter first (comma-separated) + if hitIDsParam := strings.TrimSpace(c.Query("hit_ids")); hitIDsParam != "" { + hitIDs = strings.Split(hitIDsParam, ",") + for i, id := range hitIDs { + hitIDs[i] = strings.TrimSpace(id) + } + } + + // TODO: Also support request body with hit data for batch updates + // TODO: Implement document permission validation for docID + // TODO: Implement batch update hit logic + + result := gin.H{ + "message": "Hits updated successfully", + "document_id": docID, + "segment_id": segmentID, + "updated_count": len(hitIDs), + } + + if len(hitIDs) > 0 { + result["hit_ids"] = hitIDs + } + + response.RespondWithSuccess(c, response.StatusOK, result) +} + +// RemoveHits removes hits from a segment in batch +func RemoveHits(c *gin.Context) { + // Extract docID from URL path + docID := c.Param("docID") + if docID == "" { + errorResp := &response.ErrorResponse{ + Code: response.ErrInvalidRequest.Code, + ErrorDescription: "Document ID is required", + } + response.RespondWithError(c, response.StatusBadRequest, errorResp) + return + } + + // Extract segmentID from URL path + segmentID := c.Param("segmentID") + if segmentID == "" { + errorResp := &response.ErrorResponse{ + Code: response.ErrInvalidRequest.Code, + ErrorDescription: "Segment ID is required", + } + response.RespondWithError(c, response.StatusBadRequest, errorResp) + return + } + + // Parse hit_ids from query parameter (comma-separated) + hitIDsParam := strings.TrimSpace(c.Query("hit_ids")) + if hitIDsParam == "" { + errorResp := &response.ErrorResponse{ + Code: response.ErrInvalidRequest.Code, + ErrorDescription: "hit_ids query parameter is required", + } + response.RespondWithError(c, response.StatusBadRequest, errorResp) + return + } + + // Split comma-separated hit IDs + hitIDs := strings.Split(hitIDsParam, ",") + var validHitIDs []string + for _, id := range hitIDs { + id = strings.TrimSpace(id) + if id != "" { + validHitIDs = append(validHitIDs, id) + } + } + + if len(validHitIDs) == 0 { + errorResp := &response.ErrorResponse{ + Code: response.ErrInvalidRequest.Code, + ErrorDescription: "At least one valid hit ID is required", + } + response.RespondWithError(c, response.StatusBadRequest, errorResp) + return + } + + // TODO: Implement document permission validation for docID + // TODO: Implement batch remove hit logic + + result := gin.H{ + "message": "Hits removed successfully", + "document_id": docID, + "segment_id": segmentID, + "hit_ids": validHitIDs, + "removed_count": len(validHitIDs), + } + + response.RespondWithSuccess(c, response.StatusOK, result) +} diff --git a/openapi/kb/kb.go b/openapi/kb/kb.go index be71d77d..b3934630 100644 --- a/openapi/kb/kb.go +++ b/openapi/kb/kb.go @@ -20,39 +20,58 @@ func Attach(group *gin.RouterGroup, oauth types.OAuth) { group.Use(oauth.Guard) // Collection Management - group.POST("/collections", CreateCollection) - group.DELETE("/collections/:collectionID", RemoveCollection) + group.GET("/collections", GetCollections) group.GET("/collections/:collectionID", GetCollection) group.GET("/collections/:collectionID/exists", CollectionExists) - group.GET("/collections", GetCollections) + group.POST("/collections", CreateCollection) group.PUT("/collections/:collectionID/metadata", UpdateCollectionMetadata) + group.DELETE("/collections/:collectionID", RemoveCollection) // Document Management + group.GET("/documents", ListDocuments) + group.GET("/documents/:docID", GetDocument) group.POST("/collections/:collectionID/documents/file", AddFile) group.POST("/collections/:collectionID/documents/file/async", AddFileAsync) group.POST("/collections/:collectionID/documents/text", AddText) group.POST("/collections/:collectionID/documents/text/async", AddTextAsync) group.POST("/collections/:collectionID/documents/url", AddURL) group.POST("/collections/:collectionID/documents/url/async", AddURLAsync) - group.GET("/documents", ListDocuments) - group.GET("/documents/:docID", GetDocument) group.DELETE("/documents", RemoveDocs) // Segment Management - group.POST("/documents/:docID/segments", AddSegments) - group.PUT("/documents/:docID/segments", UpdateSegments) - group.DELETE("/documents/:docID/segments", RemoveSegmentsByDocID) group.GET("/documents/:docID/segments", ScrollSegments) + group.GET("/documents/:docID/segments/search", GetSegments) + group.GET("/documents/:docID/segments/:segmentID", GetSegment) + group.GET("/documents/:docID/segments/:segmentID/graph", GetSegmentGraph) + group.GET("/documents/:docID/segments/:segmentID/parents", GetSegmentParents) + group.POST("/documents/:docID/segments", AddSegments) + group.POST("/documents/:docID/segments/async", AddSegmentsAsync) + group.POST("/documents/:docID/segments/:segmentID/extract", ExtractSegmentEntities) + group.POST("/documents/:docID/segments/:segmentID/extract/async", ExtractSegmentEntitiesAsync) + group.PUT("/documents/:docID/segments", UpdateSegments) + group.PUT("/documents/:docID/segments/async", UpdateSegmentsAsync) + group.DELETE("/documents/:docID/segments", RemoveSegments) + group.DELETE("/documents/:docID/segments/all", RemoveSegmentsByDocID) - // Global segment operations (not tied to specific document) - group.DELETE("/segments", RemoveSegments) - group.GET("/segments", GetSegments) - group.GET("/segments/:segmentID", GetSegment) + // Segment score and weight management + group.PUT("/documents/:docID/segments/:segmentID/score", UpdateScore) + group.PUT("/documents/:docID/segments/:segmentID/weight", UpdateWeight) - // Segment Voting, Scoring, Weighting - group.PUT("/segments/vote", UpdateVote) - group.PUT("/segments/score", UpdateScore) - group.PUT("/segments/weight", UpdateWeight) + // Segment votes management + group.GET("/documents/:docID/segments/:segmentID/votes", ScrollVotes) + group.GET("/documents/:docID/segments/:segmentID/votes/search", GetVotes) + group.GET("/documents/:docID/segments/:segmentID/votes/:voteID", GetVote) + group.POST("/documents/:docID/segments/:segmentID/votes", AddVotes) + group.PUT("/documents/:docID/segments/:segmentID/votes", UpdateVotes) + group.DELETE("/documents/:docID/segments/:segmentID/votes", RemoveVotes) + + // Segment hits management + group.GET("/documents/:docID/segments/:segmentID/hits", ScrollHits) + group.GET("/documents/:docID/segments/:segmentID/hits/search", GetHits) + group.GET("/documents/:docID/segments/:segmentID/hits/:hitID", GetHit) + group.POST("/documents/:docID/segments/:segmentID/hits", AddHits) + group.PUT("/documents/:docID/segments/:segmentID/hits", UpdateHits) + group.DELETE("/documents/:docID/segments/:segmentID/hits", RemoveHits) // Search Management group.POST("/search", Search) diff --git a/openapi/kb/segment.go b/openapi/kb/segment.go index 7b84a203..7fe995e0 100644 --- a/openapi/kb/segment.go +++ b/openapi/kb/segment.go @@ -279,6 +279,17 @@ func UpdateSegments(c *gin.Context) { // RemoveSegments removes segments by IDs func RemoveSegments(c *gin.Context) { + // Extract docID from URL path + docID := c.Param("docID") + if docID == "" { + errorResp := &response.ErrorResponse{ + Code: response.ErrInvalidRequest.Code, + ErrorDescription: "Document ID is required", + } + response.RespondWithError(c, response.StatusBadRequest, errorResp) + return + } + // Parse segment_ids from query parameter (comma-separated string) segmentIDsParam := strings.TrimSpace(c.Query("segment_ids")) if segmentIDsParam == "" { @@ -386,14 +397,56 @@ func RemoveSegmentsByDocID(c *gin.Context) { // GetSegments gets segments by IDs func GetSegments(c *gin.Context) { + // Extract docID from URL path + docID := c.Param("docID") + if docID == "" { + errorResp := &response.ErrorResponse{ + Code: response.ErrInvalidRequest.Code, + ErrorDescription: "Document ID is required", + } + response.RespondWithError(c, response.StatusBadRequest, errorResp) + return + } + + // TODO: Implement document permission validation for docID // TODO: Implement get segments logic - c.JSON(http.StatusOK, gin.H{"segments": []interface{}{}}) + c.JSON(http.StatusOK, gin.H{ + "segments": []interface{}{}, + "document_id": docID, + }) } // GetSegment gets a single segment by ID func GetSegment(c *gin.Context) { + // Extract docID from URL path + docID := c.Param("docID") + if docID == "" { + errorResp := &response.ErrorResponse{ + Code: response.ErrInvalidRequest.Code, + ErrorDescription: "Document ID is required", + } + response.RespondWithError(c, response.StatusBadRequest, errorResp) + return + } + + // Extract segmentID from URL path + segmentID := c.Param("segmentID") + if segmentID == "" { + errorResp := &response.ErrorResponse{ + Code: response.ErrInvalidRequest.Code, + ErrorDescription: "Segment ID is required", + } + response.RespondWithError(c, response.StatusBadRequest, errorResp) + return + } + + // TODO: Implement document permission validation for docID // TODO: Implement get single segment logic - c.JSON(http.StatusOK, gin.H{"segment": nil}) + c.JSON(http.StatusOK, gin.H{ + "segment": nil, + "document_id": docID, + "segment_id": segmentID, + }) } // ScrollSegments scrolls segments with iterator-style pagination @@ -503,3 +556,203 @@ func ScrollSegments(c *gin.Context) { // Return success response response.RespondWithSuccess(c, response.StatusOK, result) } + +// AddSegmentsAsync adds segments to a document asynchronously +func AddSegmentsAsync(c *gin.Context) { + // Extract docID from URL path + docID := c.Param("docID") + if docID == "" { + errorResp := &response.ErrorResponse{ + Code: response.ErrInvalidRequest.Code, + ErrorDescription: "Document ID is required", + } + response.RespondWithError(c, response.StatusBadRequest, errorResp) + return + } + + // Check if kb.Instance is available + if kb.Instance == nil { + errorResp := &response.ErrorResponse{ + Code: response.ErrServerError.Code, + ErrorDescription: "Knowledge base not initialized", + } + response.RespondWithError(c, response.StatusInternalServerError, errorResp) + return + } + + var req AddSegmentsRequest + + // Parse and bind JSON request + if err := c.ShouldBindJSON(&req); err != nil { + errorResp := &response.ErrorResponse{ + Code: response.ErrInvalidRequest.Code, + ErrorDescription: "Invalid request format: " + err.Error(), + } + response.RespondWithError(c, response.StatusBadRequest, errorResp) + return + } + + // Set docID from URL parameter + req.DocID = docID + + // Validate request + if err := req.Validate(); err != nil { + errorResp := &response.ErrorResponse{ + Code: response.ErrInvalidRequest.Code, + ErrorDescription: err.Error(), + } + response.RespondWithError(c, response.StatusBadRequest, errorResp) + return + } + + // TODO: Implement document permission validation for docID + + // Create and run job + job := NewJob() + jobID := job.Run(func() { + // TODO: Implement async add segments logic + // This should call the same logic as AddSegments but in background + // err := AddSegmentsProcess(context.Background(), &req, job.ID) + // For now, just simulate async processing + // if err != nil { + // log.Error("Async segments addition failed: %v", err) + // } + }) + + // Return job ID for status tracking + result := gin.H{ + "job_id": jobID, + "message": "Segments addition started", + "document_id": docID, + } + + response.RespondWithSuccess(c, response.StatusCreated, result) +} + +// UpdateSegmentsAsync updates segments in a document asynchronously +func UpdateSegmentsAsync(c *gin.Context) { + // Extract docID from URL path + docID := c.Param("docID") + if docID == "" { + errorResp := &response.ErrorResponse{ + Code: response.ErrInvalidRequest.Code, + ErrorDescription: "Document ID is required", + } + response.RespondWithError(c, response.StatusBadRequest, errorResp) + return + } + + // Check if kb.Instance is available + if kb.Instance == nil { + errorResp := &response.ErrorResponse{ + Code: response.ErrServerError.Code, + ErrorDescription: "Knowledge base not initialized", + } + response.RespondWithError(c, response.StatusInternalServerError, errorResp) + return + } + + // Parse request body for segments data + var requestBody map[string]interface{} + if err := c.ShouldBindJSON(&requestBody); err != nil { + errorResp := &response.ErrorResponse{ + Code: response.ErrInvalidRequest.Code, + ErrorDescription: "Invalid request format: " + err.Error(), + } + response.RespondWithError(c, response.StatusBadRequest, errorResp) + return + } + + // TODO: Validate request body + // TODO: Implement document permission validation for docID + + // Create and run job + job := NewJob() + jobID := job.Run(func() { + // TODO: Implement async update segments logic + // This should call the same logic as UpdateSegments but in background + // err := UpdateSegmentsProcess(context.Background(), docID, requestBody, job.ID) + // For now, just simulate async processing + // if err != nil { + // log.Error("Async segments update failed: %v", err) + // } + }) + + // Return job ID for status tracking + result := gin.H{ + "job_id": jobID, + "message": "Segments update started", + "document_id": docID, + } + + response.RespondWithSuccess(c, response.StatusCreated, result) +} + +// GetSegmentParents gets the parent segments for a specific segment +func GetSegmentParents(c *gin.Context) { + // Extract docID from URL path + docID := c.Param("docID") + if docID == "" { + errorResp := &response.ErrorResponse{ + Code: response.ErrInvalidRequest.Code, + ErrorDescription: "Document ID is required", + } + response.RespondWithError(c, response.StatusBadRequest, errorResp) + return + } + + // Extract segmentID from URL path + segmentID := c.Param("segmentID") + if segmentID == "" { + errorResp := &response.ErrorResponse{ + Code: response.ErrInvalidRequest.Code, + ErrorDescription: "Segment ID is required", + } + response.RespondWithError(c, response.StatusBadRequest, errorResp) + return + } + + // Check if kb.Instance is available + if kb.Instance == nil { + errorResp := &response.ErrorResponse{ + Code: response.ErrServerError.Code, + ErrorDescription: "Knowledge base not initialized", + } + response.RespondWithError(c, response.StatusInternalServerError, errorResp) + return + } + + // Parse query parameters for parent options + options := make(map[string]interface{}) + + // Include metadata (default: true) + if includeMetadata := c.Query("include_metadata"); includeMetadata == "false" { + options["include_metadata"] = false + } else { + options["include_metadata"] = true + } + + // Depth level (default: 1 - direct parents only) + depth := 1 + if depthStr := c.Query("depth"); depthStr != "" { + if d, err := strconv.Atoi(depthStr); err == nil && d > 0 { + depth = d + } + } + options["depth"] = depth + + // TODO: Implement document permission validation for docID + // TODO: Implement get segment parents logic + // TODO: Call kb.Instance.GetSegmentParents(c.Request.Context(), segmentID, options) + + // Return mock response for now + result := gin.H{ + "parents": []interface{}{}, + "document_id": docID, + "segment_id": segmentID, + "depth": depth, + "total": 0, + } + + response.RespondWithSuccess(c, response.StatusOK, result) +} diff --git a/openapi/kb/store.go b/openapi/kb/store.go index 0542e736..99bd0939 100644 --- a/openapi/kb/store.go +++ b/openapi/kb/store.go @@ -10,20 +10,63 @@ import ( // Segment Voting, Scoring, Weighting Handlers -// UpdateVote updates votes for segments -func UpdateVote(c *gin.Context) { - // TODO: Implement update vote logic - c.JSON(http.StatusOK, gin.H{"message": "Vote updated"}) -} - -// UpdateScore updates scores for segments +// UpdateScore updates score for a specific segment func UpdateScore(c *gin.Context) { + // Extract docID from URL path + docID := c.Param("docID") + if docID == "" { + errorResp := &response.ErrorResponse{ + Code: response.ErrInvalidRequest.Code, + ErrorDescription: "Document ID is required", + } + response.RespondWithError(c, response.StatusBadRequest, errorResp) + return + } + + // Extract segmentID from URL path + segmentID := c.Param("segmentID") + if segmentID == "" { + errorResp := &response.ErrorResponse{ + Code: response.ErrInvalidRequest.Code, + ErrorDescription: "Segment ID is required", + } + response.RespondWithError(c, response.StatusBadRequest, errorResp) + return + } + + // TODO: Implement document permission validation for docID // TODO: Implement update score logic - c.JSON(http.StatusOK, gin.H{"message": "Score updated"}) + c.JSON(http.StatusOK, gin.H{ + "message": "Score updated successfully", + "document_id": docID, + "segment_id": segmentID, + }) } -// UpdateWeight updates weights for segments +// UpdateWeight updates weight for a specific segment func UpdateWeight(c *gin.Context) { + // Extract docID from URL path + docID := c.Param("docID") + if docID == "" { + errorResp := &response.ErrorResponse{ + Code: response.ErrInvalidRequest.Code, + ErrorDescription: "Document ID is required", + } + response.RespondWithError(c, response.StatusBadRequest, errorResp) + return + } + + // Extract segmentID from URL path + segmentID := c.Param("segmentID") + if segmentID == "" { + errorResp := &response.ErrorResponse{ + Code: response.ErrInvalidRequest.Code, + ErrorDescription: "Segment ID is required", + } + response.RespondWithError(c, response.StatusBadRequest, errorResp) + return + } + var req UpdateWeightRequest // Parse and bind JSON request @@ -69,7 +112,9 @@ func UpdateWeight(c *gin.Context) { // Return success response result := gin.H{ - "message": "Segment weights updated successfully", + "message": "Segment weight updated successfully", + "document_id": docID, + "segment_id": segmentID, "segments": req.Segments, "updated_count": updatedCount, } diff --git a/openapi/kb/vote.go b/openapi/kb/vote.go new file mode 100644 index 00000000..d40a7ac0 --- /dev/null +++ b/openapi/kb/vote.go @@ -0,0 +1,356 @@ +package kb + +import ( + "net/http" + "strconv" + "strings" + + "github.com/gin-gonic/gin" + "github.com/yaoapp/yao/openapi/response" +) + +// Vote Management Handlers + +// ScrollVotes scrolls votes with iterator-style pagination for a specific segment +func ScrollVotes(c *gin.Context) { + // Extract docID from URL path + docID := c.Param("docID") + if docID == "" { + errorResp := &response.ErrorResponse{ + Code: response.ErrInvalidRequest.Code, + ErrorDescription: "Document ID is required", + } + response.RespondWithError(c, response.StatusBadRequest, errorResp) + return + } + + // Extract segmentID from URL path + segmentID := c.Param("segmentID") + if segmentID == "" { + errorResp := &response.ErrorResponse{ + Code: response.ErrInvalidRequest.Code, + ErrorDescription: "Segment ID is required", + } + response.RespondWithError(c, response.StatusBadRequest, errorResp) + return + } + + // Parse query parameters for scroll options + options := map[string]interface{}{ + "document_id": docID, + "segment_id": segmentID, + "limit": 100, // Default limit + } + + // Parse limit (default: 100) + if limitStr := c.Query("limit"); limitStr != "" { + if limit, err := strconv.Atoi(limitStr); err == nil && limit > 0 { + options["limit"] = limit + } + } + + // Parse scroll_id parameter for continuing pagination + if scrollID := strings.TrimSpace(c.Query("scroll_id")); scrollID != "" { + options["scroll_id"] = scrollID + } + + // Parse order_by parameter + if orderBy := strings.TrimSpace(c.Query("order_by")); orderBy != "" { + orderByFields := strings.Split(orderBy, ",") + // Trim spaces from each field + for i, field := range orderByFields { + orderByFields[i] = strings.TrimSpace(field) + } + options["order_by"] = orderByFields + } + + // Parse filter parameters + filter := make(map[string]interface{}) + if voteType := c.Query("vote_type"); voteType != "" { + filter["vote_type"] = voteType + } + if userID := c.Query("user_id"); userID != "" { + filter["user_id"] = userID + } + if len(filter) > 0 { + options["filter"] = filter + } + + // TODO: Implement document permission validation for docID + // TODO: Implement scroll votes logic with GraphRag or database + // TODO: Call kb.Instance.ScrollVotes(c.Request.Context(), segmentID, options) + + // Return mock response for now + result := gin.H{ + "votes": []interface{}{}, + "scroll_id": nil, + "has_more": false, + "total": 0, + "options": options, + } + + response.RespondWithSuccess(c, response.StatusOK, result) +} + +// GetVotes gets votes for a specific segment (simple list, no pagination) +func GetVotes(c *gin.Context) { + // Extract docID from URL path + docID := c.Param("docID") + if docID == "" { + errorResp := &response.ErrorResponse{ + Code: response.ErrInvalidRequest.Code, + ErrorDescription: "Document ID is required", + } + response.RespondWithError(c, response.StatusBadRequest, errorResp) + return + } + + // Extract segmentID from URL path + segmentID := c.Param("segmentID") + if segmentID == "" { + errorResp := &response.ErrorResponse{ + Code: response.ErrInvalidRequest.Code, + ErrorDescription: "Segment ID is required", + } + response.RespondWithError(c, response.StatusBadRequest, errorResp) + return + } + + // Parse basic filter parameters + filter := make(map[string]interface{}) + if voteType := c.Query("vote_type"); voteType != "" { + filter["vote_type"] = voteType + } + if userID := c.Query("user_id"); userID != "" { + filter["user_id"] = userID + } + + // Parse limit parameter (optional, for basic limiting without pagination) + var limit int + if limitStr := c.Query("limit"); limitStr != "" { + if l, err := strconv.Atoi(limitStr); err == nil && l > 0 { + limit = l + } + } + + // TODO: Implement document permission validation for docID + // TODO: Implement get votes logic (simple query without pagination) + // TODO: Call kb.Instance.GetVotes(c.Request.Context(), segmentID, filter, limit) + + // Return mock response for now + result := gin.H{ + "votes": []interface{}{}, + "document_id": docID, + "segment_id": segmentID, + "total": 0, + } + + if len(filter) > 0 { + result["filter"] = filter + } + if limit > 0 { + result["limit"] = limit + } + + response.RespondWithSuccess(c, response.StatusOK, result) +} + +// GetVote gets a specific vote by ID +func GetVote(c *gin.Context) { + // Extract docID from URL path + docID := c.Param("docID") + if docID == "" { + errorResp := &response.ErrorResponse{ + Code: response.ErrInvalidRequest.Code, + ErrorDescription: "Document ID is required", + } + response.RespondWithError(c, response.StatusBadRequest, errorResp) + return + } + + // Extract segmentID from URL path + segmentID := c.Param("segmentID") + if segmentID == "" { + errorResp := &response.ErrorResponse{ + Code: response.ErrInvalidRequest.Code, + ErrorDescription: "Segment ID is required", + } + response.RespondWithError(c, response.StatusBadRequest, errorResp) + return + } + + // Extract voteID from URL path + voteID := c.Param("voteID") + if voteID == "" { + errorResp := &response.ErrorResponse{ + Code: response.ErrInvalidRequest.Code, + ErrorDescription: "Vote ID is required", + } + response.RespondWithError(c, response.StatusBadRequest, errorResp) + return + } + + // TODO: Implement document permission validation for docID + // TODO: Implement get vote detail logic + c.JSON(http.StatusOK, gin.H{ + "vote": nil, + "document_id": docID, + "segment_id": segmentID, + "vote_id": voteID, + }) +} + +// AddVotes adds new votes to a segment +func AddVotes(c *gin.Context) { + // Extract docID from URL path + docID := c.Param("docID") + if docID == "" { + errorResp := &response.ErrorResponse{ + Code: response.ErrInvalidRequest.Code, + ErrorDescription: "Document ID is required", + } + response.RespondWithError(c, response.StatusBadRequest, errorResp) + return + } + + // Extract segmentID from URL path + segmentID := c.Param("segmentID") + if segmentID == "" { + errorResp := &response.ErrorResponse{ + Code: response.ErrInvalidRequest.Code, + ErrorDescription: "Segment ID is required", + } + response.RespondWithError(c, response.StatusBadRequest, errorResp) + return + } + + // TODO: Implement document permission validation for docID + // TODO: Implement add vote logic + c.JSON(http.StatusOK, gin.H{ + "message": "Vote added successfully", + "document_id": docID, + "segment_id": segmentID, + "vote_id": "placeholder-vote-id", + }) +} + +// UpdateVotes updates votes in batch +func UpdateVotes(c *gin.Context) { + // Extract docID from URL path + docID := c.Param("docID") + if docID == "" { + errorResp := &response.ErrorResponse{ + Code: response.ErrInvalidRequest.Code, + ErrorDescription: "Document ID is required", + } + response.RespondWithError(c, response.StatusBadRequest, errorResp) + return + } + + // Extract segmentID from URL path + segmentID := c.Param("segmentID") + if segmentID == "" { + errorResp := &response.ErrorResponse{ + Code: response.ErrInvalidRequest.Code, + ErrorDescription: "Segment ID is required", + } + response.RespondWithError(c, response.StatusBadRequest, errorResp) + return + } + + // Parse vote_ids from query parameter or request body + var voteIDs []string + + // Try query parameter first (comma-separated) + if voteIDsParam := strings.TrimSpace(c.Query("vote_ids")); voteIDsParam != "" { + voteIDs = strings.Split(voteIDsParam, ",") + for i, id := range voteIDs { + voteIDs[i] = strings.TrimSpace(id) + } + } + + // TODO: Also support request body with vote data for batch updates + // TODO: Implement document permission validation for docID + // TODO: Implement batch update vote logic + + result := gin.H{ + "message": "Votes updated successfully", + "document_id": docID, + "segment_id": segmentID, + "updated_count": len(voteIDs), + } + + if len(voteIDs) > 0 { + result["vote_ids"] = voteIDs + } + + response.RespondWithSuccess(c, response.StatusOK, result) +} + +// RemoveVotes removes votes from a segment in batch +func RemoveVotes(c *gin.Context) { + // Extract docID from URL path + docID := c.Param("docID") + if docID == "" { + errorResp := &response.ErrorResponse{ + Code: response.ErrInvalidRequest.Code, + ErrorDescription: "Document ID is required", + } + response.RespondWithError(c, response.StatusBadRequest, errorResp) + return + } + + // Extract segmentID from URL path + segmentID := c.Param("segmentID") + if segmentID == "" { + errorResp := &response.ErrorResponse{ + Code: response.ErrInvalidRequest.Code, + ErrorDescription: "Segment ID is required", + } + response.RespondWithError(c, response.StatusBadRequest, errorResp) + return + } + + // Parse vote_ids from query parameter (comma-separated) + voteIDsParam := strings.TrimSpace(c.Query("vote_ids")) + if voteIDsParam == "" { + errorResp := &response.ErrorResponse{ + Code: response.ErrInvalidRequest.Code, + ErrorDescription: "vote_ids query parameter is required", + } + response.RespondWithError(c, response.StatusBadRequest, errorResp) + return + } + + // Split comma-separated vote IDs + voteIDs := strings.Split(voteIDsParam, ",") + var validVoteIDs []string + for _, id := range voteIDs { + id = strings.TrimSpace(id) + if id != "" { + validVoteIDs = append(validVoteIDs, id) + } + } + + if len(validVoteIDs) == 0 { + errorResp := &response.ErrorResponse{ + Code: response.ErrInvalidRequest.Code, + ErrorDescription: "At least one valid vote ID is required", + } + response.RespondWithError(c, response.StatusBadRequest, errorResp) + return + } + + // TODO: Implement document permission validation for docID + // TODO: Implement batch remove vote logic + + result := gin.H{ + "message": "Votes removed successfully", + "document_id": docID, + "segment_id": segmentID, + "vote_ids": validVoteIDs, + "removed_count": len(validVoteIDs), + } + + response.RespondWithSuccess(c, response.StatusOK, result) +}