feat: 支持输入框内选择知识库和文件,优化选择交互体验

This commit is contained in:
wizardchen
2025-12-22 13:11:31 +08:00
committed by lyingbug
parent 485d8d7252
commit de1ffa7f7c
49 changed files with 2246 additions and 521 deletions
+1 -1
View File
@@ -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") {
+34
View File
@@ -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
+18 -26
View File
@@ -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
+50 -2
View File
@@ -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
}
+1
View File
@@ -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)