Merge pull request #1115 from trheyi/main

Refactor API routes for collections and segments management
This commit is contained in:
Max 2025-08-21 08:49:26 +08:00 committed by GitHub
commit 1714f47c48
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 1269 additions and 28 deletions

206
openapi/kb/graph.go Normal file
View file

@ -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)
}

362
openapi/kb/hit.go Normal file
View file

@ -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)
}

View file

@ -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)

View file

@ -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)
}

View file

@ -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,
}

356
openapi/kb/vote.go Normal file
View file

@ -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)
}