yao/openapi/kb/addtext.go
Max ae965d0b32 Refactor document and collection update processes to include GraphRag synchronization
- Updated AddFileProcess, AddTextProcess, and AddURLProcess functions to use new UpdateDocumentCountWithSync method for document count updates, ensuring synchronization with GraphRag.
- Enhanced CreateCollection function to utilize UpdateCollectionWithSync for collection status updates, improving consistency in metadata management.
- Modified RemoveDocs function to sync document count updates to GraphRag, ensuring accurate tracking of affected collections.
- Introduced new utility functions for updating collections and document counts with GraphRag synchronization, enhancing overall API functionality.
2025-08-30 09:39:16 +08:00

231 lines
6.6 KiB
Go

package kb
import (
"context"
"fmt"
"github.com/gin-gonic/gin"
"github.com/yaoapp/gou/graphrag/utils"
"github.com/yaoapp/kun/log"
"github.com/yaoapp/kun/maps"
"github.com/yaoapp/yao/kb"
"github.com/yaoapp/yao/openapi/response"
)
// AddTextProcess processes a text addition request with business logic only
// This function is Gin-agnostic and can be used for both sync and async operations
func AddTextProcess(ctx context.Context, req *AddTextRequest, jobID ...string) error {
// Check if kb.Instance is available
if kb.Instance == nil {
return fmt.Errorf("knowledge base not initialized")
}
// Validate request
if err := req.Validate(); err != nil {
return err
}
// DocID should be generated by the caller before calling this function
if req.DocID == "" {
return fmt.Errorf("document ID is required")
}
// Get KB config
config, err := kb.GetConfig()
if err != nil {
return fmt.Errorf("failed to get KB config: %w", err)
}
// Prepare document data for database
documentData := map[string]interface{}{
"document_id": req.DocID,
"collection_id": req.CollectionID,
"name": "Text Document",
"type": "text",
"status": "pending",
"text_content": req.Text,
"size": int64(len(req.Text)),
}
// Add job_id if provided (for async operations)
if len(jobID) > 0 && jobID[0] != "" {
documentData["job_id"] = jobID[0]
}
// Use title from metadata if available
if req.Metadata != nil {
if title, ok := req.Metadata["title"].(string); ok && title != "" {
documentData["name"] = title
}
}
// Add base request fields
req.BaseUpsertRequest.AddBaseFields(documentData)
// First create database record
_, err = config.CreateDocument(maps.MapStrAny(documentData))
if err != nil {
return fmt.Errorf("failed to save document metadata: %w", err)
}
// Convert request to UpsertOptions
upsertOptions, err := req.BaseUpsertRequest.ToUpsertOptions()
if err != nil {
// Rollback: remove the database record
if rollbackErr := config.RemoveDocument(req.DocID); rollbackErr != nil {
log.Error("Failed to rollback document database record: %v", rollbackErr)
}
return fmt.Errorf("failed to convert request to upsert options: %w", err)
}
// Perform upsert operation with text
_, err = kb.Instance.AddText(ctx, req.Text, upsertOptions)
if err != nil {
// Update status to error
config.UpdateDocument(req.DocID, maps.MapStrAny{"status": "error", "error_message": err.Error()})
return fmt.Errorf("failed to add text: %w", err)
}
// Update status to completed after successful processing
if err := config.UpdateDocument(req.DocID, maps.MapStrAny{"status": "completed"}); err != nil {
log.Error("Failed to update document status to completed: %v", err)
}
// Update segment count for the document
if segmentCount, err := kb.Instance.SegmentCount(ctx, req.DocID); err != nil {
log.Error("Failed to get segment count for document %s: %v", req.DocID, err)
} else {
log.Info("Got segment count %d for document %s", segmentCount, req.DocID)
if err := config.UpdateSegmentCount(req.DocID, segmentCount); err != nil {
log.Error("Failed to update segment count for document %s: %v", req.DocID, err)
} else {
log.Info("Successfully updated segment count to %d for document %s", segmentCount, req.DocID)
}
}
// Update document count for the collection and sync to GraphRag
if err := UpdateDocumentCountWithSync(req.CollectionID, config); err != nil {
log.Error("Failed to update document count for collection %s: %v", req.CollectionID, err)
} else {
log.Info("Successfully updated document count for collection %s", req.CollectionID)
}
return nil
}
// addTextWithRequest processes a text addition with pre-parsed request using Gin context
func addTextWithRequest(c *gin.Context, req *AddTextRequest) {
// Use the business logic function
err := AddTextProcess(c.Request.Context(), req)
if err != nil {
errorResp := &response.ErrorResponse{
Code: response.ErrServerError.Code,
ErrorDescription: err.Error(),
}
response.RespondWithError(c, response.StatusInternalServerError, errorResp)
return
}
// Return success response
result := gin.H{
"message": "Text added successfully",
"collection_id": req.CollectionID,
"doc_id": req.DocID,
}
response.RespondWithSuccess(c, response.StatusCreated, result)
}
// AddText adds text to a collection
func AddText(c *gin.Context) {
var req AddTextRequest
// Check if kb.Instance is available
if !checkKBInstance(c) {
return
}
// 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
}
// 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
}
// Generate document ID if not provided
if req.DocID == "" {
req.DocID = utils.GenDocIDWithCollectionID(req.CollectionID)
}
// Process the request
addTextWithRequest(c, &req)
}
// AddTextAsync adds text to a collection asynchronously
func AddTextAsync(c *gin.Context) {
var req AddTextRequest
// 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
}
// 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
}
// Check if kb.Instance is available
if !checkKBInstance(c) {
return
}
// Convert request to UpsertOptions (just for validation)
_, err := getUpsertOptions(c, &req.BaseUpsertRequest)
if err != nil {
return
}
// Generate document ID if not provided
if req.DocID == "" {
req.DocID = utils.GenDocIDWithCollectionID(req.CollectionID)
}
// Create and run job
job := NewJob()
jobID := job.Run(func() {
err := AddTextProcess(context.Background(), &req, job.ID)
if err != nil {
log.Error("Async text processing failed: %v", err)
}
})
// Return job_id and doc_id
response.RespondWithSuccess(c, response.StatusCreated, gin.H{
"job_id": jobID,
"doc_id": req.DocID,
})
}