mirror of
https://github.com/Tencent/WeKnora.git
synced 2026-09-24 16:29:01 +08:00
feat: 支持输入框内选择知识库和文件,优化选择交互体验
This commit is contained in:
@@ -1585,7 +1585,7 @@ func (h *InitializationHandler) checkRemoteModelConnection(ctx context.Context,
|
||||
if strings.Contains(err.Error(), "401") || strings.Contains(err.Error(), "unauthorized") {
|
||||
return false, "认证失败,请检查API Key"
|
||||
} else if strings.Contains(err.Error(), "403") || strings.Contains(err.Error(), "forbidden") {
|
||||
return false, "权限不足,请检查API Key权限"
|
||||
return false, "权限不足,请检查API Key权限:" + err.Error()
|
||||
} else if strings.Contains(err.Error(), "404") || strings.Contains(err.Error(), "not found") {
|
||||
return false, "API端点不存在,请检查Base URL"
|
||||
} else if strings.Contains(err.Error(), "timeout") {
|
||||
|
||||
@@ -764,3 +764,37 @@ func (h *KnowledgeHandler) UpdateImageInfo(c *gin.Context) {
|
||||
"message": "Knowledge chunk image updated successfully",
|
||||
})
|
||||
}
|
||||
|
||||
// SearchKnowledge godoc
|
||||
// @Summary Search knowledge
|
||||
// @Description Search knowledge files by keyword across all knowledge bases
|
||||
// @Tags Knowledge
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param keyword query string false "Keyword to search"
|
||||
// @Param offset query int false "Offset for pagination"
|
||||
// @Param limit query int false "Limit for pagination (default 20)"
|
||||
// @Success 200 {object} map[string]interface{} "Search results"
|
||||
// @Failure 400 {object} errors.AppError "Invalid request"
|
||||
// @Security Bearer
|
||||
// @Router /knowledge/search [get]
|
||||
func (h *KnowledgeHandler) SearchKnowledge(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
keyword := c.Query("keyword")
|
||||
offset, _ := strconv.Atoi(c.DefaultQuery("offset", "0"))
|
||||
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "20"))
|
||||
|
||||
// Retrieve knowledge entries (empty keyword returns recent files)
|
||||
knowledges, hasMore, err := h.kgService.SearchKnowledge(ctx, keyword, offset, limit)
|
||||
if err != nil {
|
||||
logger.ErrorWithFields(ctx, err, nil)
|
||||
c.Error(errors.NewInternalServerError("Failed to search knowledge").WithDetails(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"data": knowledges,
|
||||
"has_more": hasMore,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -380,8 +380,11 @@ func (h *AgentStreamHandler) handleSessionTitle(ctx context.Context, evt event.E
|
||||
return nil
|
||||
}
|
||||
|
||||
// Use background context for title event since it may arrive after stream completion
|
||||
bgCtx := context.Background()
|
||||
|
||||
// Append title event to stream
|
||||
if err := h.streamManager.AppendEvent(h.ctx, h.sessionID, h.assistantMessageID, interfaces.StreamEvent{
|
||||
if err := h.streamManager.AppendEvent(bgCtx, h.sessionID, h.assistantMessageID, interfaces.StreamEvent{
|
||||
ID: evt.ID,
|
||||
Type: types.ResponseTypeSessionTitle,
|
||||
Content: data.Title,
|
||||
@@ -392,7 +395,7 @@ func (h *AgentStreamHandler) handleSessionTitle(ctx context.Context, evt event.E
|
||||
"title": data.Title,
|
||||
},
|
||||
}); err != nil {
|
||||
logger.GetLogger(h.ctx).Error("Append session title event to stream failed", "error", err)
|
||||
logger.GetLogger(h.ctx).Warn("Append session title event to stream failed (stream may have ended)", "error", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -142,18 +142,19 @@ func (h *Handler) KnowledgeQA(c *gin.Context) {
|
||||
|
||||
// Prepare knowledge base IDs
|
||||
knowledgeBaseIDs := request.KnowledgeBaseIDs
|
||||
if len(knowledgeBaseIDs) == 0 && session.KnowledgeBaseID != "" {
|
||||
knowledgeBaseIDs = []string{session.KnowledgeBaseID}
|
||||
logger.Infof(
|
||||
ctx,
|
||||
"No knowledge base IDs in request, using session default: %s",
|
||||
secutils.SanitizeForLog(session.KnowledgeBaseID),
|
||||
)
|
||||
}
|
||||
// if len(knowledgeBaseIDs) == 0 && session.KnowledgeBaseID != "" {
|
||||
// knowledgeBaseIDs = []string{session.KnowledgeBaseID}
|
||||
// logger.Infof(
|
||||
// ctx,
|
||||
// "No knowledge base IDs in request, using session default: %s",
|
||||
// secutils.SanitizeForLog(session.KnowledgeBaseID),
|
||||
// )
|
||||
// }
|
||||
|
||||
// Use shared function to handle KnowledgeQA request
|
||||
h.handleKnowledgeQARequest(ctx, c, session, secutils.SanitizeForLog(request.Query),
|
||||
secutils.SanitizeForLogArray(knowledgeBaseIDs),
|
||||
secutils.SanitizeForLogArray(request.KnowledgeIds),
|
||||
assistantMessage, true, secutils.SanitizeForLog(request.SummaryModelID), request.WebSearchEnabled)
|
||||
}
|
||||
|
||||
@@ -332,15 +333,6 @@ func (h *Handler) AgentQA(c *gin.Context) {
|
||||
)
|
||||
}
|
||||
|
||||
// Validate at least one knowledge base is available
|
||||
if len(knowledgeBaseIDs) == 0 {
|
||||
logger.Error(ctx, "No knowledge base available for delegation")
|
||||
c.Error(
|
||||
errors.NewBadRequestError("No knowledge base available. Please configure at least one knowledge base."),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
logger.Infof(
|
||||
ctx,
|
||||
"Delegating to KnowledgeQA with knowledge bases: %s",
|
||||
@@ -356,6 +348,9 @@ func (h *Handler) AgentQA(c *gin.Context) {
|
||||
secutils.SanitizeForLogArray(
|
||||
knowledgeBaseIDs,
|
||||
),
|
||||
secutils.SanitizeForLogArray(
|
||||
request.KnowledgeIds,
|
||||
),
|
||||
assistantMessage,
|
||||
false,
|
||||
secutils.SanitizeForLog(request.SummaryModelID),
|
||||
@@ -455,7 +450,8 @@ func (h *Handler) AgentQA(c *gin.Context) {
|
||||
}()
|
||||
|
||||
// Handle events for SSE (blocking until connection is done)
|
||||
h.handleAgentEventsForSSE(ctx, c, sessionID, assistantMessage.ID, requestID, eventBus)
|
||||
// Wait for title only if session has no title (first message in session)
|
||||
h.handleAgentEventsForSSE(ctx, c, sessionID, assistantMessage.ID, requestID, eventBus, session.Title == "")
|
||||
}
|
||||
|
||||
// handleKnowledgeQARequest handles a KnowledgeQA request with the given parameters
|
||||
@@ -466,6 +462,7 @@ func (h *Handler) handleKnowledgeQARequest(
|
||||
session *types.Session,
|
||||
query string,
|
||||
knowledgeBaseIDs []string,
|
||||
knowledgeIDs []string,
|
||||
assistantMessage *types.Message,
|
||||
generateTitle bool, // Whether to generate title if session has no title
|
||||
summaryModelID string, // Optional summary model ID (overrides session default)
|
||||
@@ -486,13 +483,6 @@ func (h *Handler) handleKnowledgeQARequest(
|
||||
return
|
||||
}
|
||||
|
||||
// Validate knowledge bases
|
||||
if len(knowledgeBaseIDs) == 0 {
|
||||
logger.Error(ctx, "No knowledge base ID available")
|
||||
c.Error(errors.NewBadRequestError("At least one knowledge base ID is required"))
|
||||
return
|
||||
}
|
||||
|
||||
logger.Infof(ctx, "Using knowledge bases: %s", secutils.SanitizeForLog(fmt.Sprintf("%v", knowledgeBaseIDs)))
|
||||
|
||||
// Set headers for SSE
|
||||
@@ -559,6 +549,7 @@ func (h *Handler) handleKnowledgeQARequest(
|
||||
session,
|
||||
query,
|
||||
knowledgeBaseIDs,
|
||||
knowledgeIDs,
|
||||
assistantMessage.ID,
|
||||
summaryModelID,
|
||||
webSearchEnabled,
|
||||
@@ -581,7 +572,8 @@ func (h *Handler) handleKnowledgeQARequest(
|
||||
}()
|
||||
|
||||
// Handle events for SSE (blocking until connection is done)
|
||||
h.handleAgentEventsForSSE(ctx, c, sessionID, assistantMessage.ID, requestID, eventBus)
|
||||
// Wait for title only if session has no title (first message in session)
|
||||
h.handleAgentEventsForSSE(ctx, c, sessionID, assistantMessage.ID, requestID, eventBus, session.Title == "")
|
||||
}
|
||||
|
||||
// completeAssistantMessage marks an assistant message as complete and updates it
|
||||
|
||||
@@ -294,11 +294,13 @@ func (h *Handler) StopSession(c *gin.Context) {
|
||||
// handleAgentEventsForSSE handles agent events for SSE streaming using an existing handler
|
||||
// The handler is already subscribed to events and AgentQA is already running
|
||||
// This function polls StreamManager and pushes events to SSE, allowing graceful handling of disconnections
|
||||
// waitForTitle: if true, wait for title event after completion (for new sessions without title)
|
||||
func (h *Handler) handleAgentEventsForSSE(
|
||||
ctx context.Context,
|
||||
c *gin.Context,
|
||||
sessionID, assistantMessageID, requestID string,
|
||||
eventBus *event.EventBus,
|
||||
waitForTitle bool,
|
||||
) {
|
||||
ticker := time.NewTicker(100 * time.Millisecond)
|
||||
defer ticker.Stop()
|
||||
@@ -329,6 +331,7 @@ func (h *Handler) handleAgentEventsForSSE(
|
||||
|
||||
// Send any new events
|
||||
streamCompleted := false
|
||||
titleReceived := false
|
||||
for _, evt := range events {
|
||||
// Check for stop event
|
||||
if evt.Type == types.ResponseType(event.EventStop) {
|
||||
@@ -366,6 +369,11 @@ func (h *Handler) handleAgentEventsForSSE(
|
||||
streamCompleted = true
|
||||
}
|
||||
|
||||
// Check for title event
|
||||
if evt.Type == types.ResponseTypeSessionTitle {
|
||||
titleReceived = true
|
||||
}
|
||||
|
||||
// Check if connection is still alive before writing
|
||||
if c.Request.Context().Err() != nil {
|
||||
log.Info("Connection closed during event sending, stopping")
|
||||
@@ -379,9 +387,49 @@ func (h *Handler) handleAgentEventsForSSE(
|
||||
// Update offset
|
||||
lastOffset = newOffset
|
||||
|
||||
// Check if stream is completed
|
||||
// Check if stream is completed - wait for title event only if needed and not already received
|
||||
if streamCompleted {
|
||||
log.Infof("Stream completed for session=%s, message=%s", sessionID, assistantMessageID)
|
||||
if waitForTitle && !titleReceived {
|
||||
log.Infof("Stream completed for session=%s, message=%s, waiting for title event", sessionID, assistantMessageID)
|
||||
// Wait up to 3 seconds for title event after completion
|
||||
titleTimeout := time.After(3 * time.Second)
|
||||
titleWaitLoop:
|
||||
for {
|
||||
select {
|
||||
case <-titleTimeout:
|
||||
log.Info("Title wait timeout, closing stream")
|
||||
break titleWaitLoop
|
||||
case <-c.Request.Context().Done():
|
||||
log.Info("Connection closed while waiting for title")
|
||||
return
|
||||
default:
|
||||
// Check for new events (title event)
|
||||
events, newOff, err := h.streamManager.GetEvents(c.Request.Context(), sessionID, assistantMessageID, lastOffset)
|
||||
if err != nil {
|
||||
log.Warnf("Error getting events while waiting for title: %v", err)
|
||||
break titleWaitLoop
|
||||
}
|
||||
if len(events) > 0 {
|
||||
for _, evt := range events {
|
||||
response := buildStreamResponse(evt, requestID)
|
||||
c.SSEvent("message", response)
|
||||
c.Writer.Flush()
|
||||
// If we got the title, we can exit
|
||||
if evt.Type == types.ResponseTypeSessionTitle {
|
||||
log.Infof("Title event received: %s", evt.Content)
|
||||
break titleWaitLoop
|
||||
}
|
||||
}
|
||||
lastOffset = newOff
|
||||
} else {
|
||||
// No events, wait a bit before checking again
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.Infof("Stream completed for session=%s, message=%s", sessionID, assistantMessageID)
|
||||
}
|
||||
sendCompletionEvent(c, requestID)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -55,6 +55,7 @@ type GenerateTitleRequest struct {
|
||||
type CreateKnowledgeQARequest struct {
|
||||
Query string `json:"query" binding:"required"` // Query text for knowledge base search
|
||||
KnowledgeBaseIDs []string `json:"knowledge_base_ids"` // Selected knowledge base ID for this request
|
||||
KnowledgeIds []string `json:"knowledge_ids"` // Selected knowledge ID for this request
|
||||
AgentEnabled bool `json:"agent_enabled"` // Whether agent mode is enabled for this request
|
||||
WebSearchEnabled bool `json:"web_search_enabled"` // Whether web search is enabled for this request
|
||||
SummaryModelID string `json:"summary_model_id"` // Optional summary model ID for this request (overrides session default)
|
||||
|
||||
Reference in New Issue
Block a user