Enhance Search Functionality with Loading and Result Messaging

- Implemented loading and result messaging in the executeAutoSearch method to improve user experience during search operations.
- Added methods to send loading, result, and completion messages, providing real-time feedback to users.
- Integrated trace node creation and completion for search operations, enhancing transparency and debugging capabilities.
- Updated localization files to include new messages for search status updates in both English and Chinese.
- Revised DESIGN.md to document the new output flow and trace integration for search operations.
This commit is contained in:
Max 2025-12-15 14:52:20 +08:00
parent 2a191950d7
commit 42bb27a8dd
3 changed files with 398 additions and 39 deletions

View file

@ -1,12 +1,16 @@
package assistant
import (
"fmt"
"strings"
"github.com/yaoapp/yao/agent/context"
"github.com/yaoapp/yao/agent/i18n"
"github.com/yaoapp/yao/agent/output/message"
"github.com/yaoapp/yao/agent/search"
"github.com/yaoapp/yao/agent/search/nlp/keyword"
searchTypes "github.com/yaoapp/yao/agent/search/types"
traceTypes "github.com/yaoapp/yao/trace/types"
)
// shouldAutoSearch determines if auto search should be executed
@ -136,6 +140,12 @@ func (ast *Assistant) executeAutoSearch(ctx *context.Context, messages []context
return nil
}
// === Output: Send loading message ===
loadingID := ast.sendSearchLoading(ctx)
// === Trace: Create search trace node ===
searchNode := ast.createSearchTrace(ctx, query, requests)
// Execute searches in parallel
ctx.Logger.Info("Executing %d search requests for query: %s", len(requests), truncateString(query, 50))
@ -143,6 +153,12 @@ func (ast *Assistant) executeAutoSearch(ctx *context.Context, messages []context
if err != nil {
// Log error but don't fail - search errors shouldn't block the main flow
ctx.Logger.Error("Auto search failed: %v", err)
// === Output: Send failed message ===
ast.sendSearchDone(ctx, loadingID, 0, true)
// === Trace: Mark as failed ===
ast.completeSearchTrace(searchNode, 0, err)
return nil
}
@ -153,15 +169,179 @@ func (ast *Assistant) executeAutoSearch(ctx *context.Context, messages []context
}
refCtx := search.BuildReferenceContext(results, citationConfig)
if len(refCtx.References) == 0 {
resultCount := len(refCtx.References)
// === Output: Send result message, then done ===
ast.sendSearchResult(ctx, loadingID, resultCount)
ast.sendSearchDone(ctx, loadingID, resultCount, false)
// === Trace: Mark as completed ===
ast.completeSearchTrace(searchNode, resultCount, nil)
if resultCount == 0 {
ctx.Logger.Info("No search results found")
return nil
}
ctx.Logger.Info("Auto search completed: %d references", len(refCtx.References))
ctx.Logger.Info("Auto search completed: %d references", resultCount)
return refCtx
}
// ============================================================================
// Output: Loading Replace Pattern
// ============================================================================
// sendSearchLoading sends the initial loading message
// Returns the message ID for later replacement
func (ast *Assistant) sendSearchLoading(ctx *context.Context) string {
loadingMsg := i18n.T(ctx.Locale, "search.loading")
msg := &message.Message{
Type: "loading",
Props: map[string]any{
"message": loadingMsg,
},
}
// Send and get message ID
msgID, err := ctx.SendStream(msg)
if err != nil {
ctx.Logger.Warn("Failed to send search loading message: %v", err)
return ""
}
return msgID
}
// sendSearchResult replaces loading with result message (without done flag)
func (ast *Assistant) sendSearchResult(ctx *context.Context, loadingID string, count int) {
if loadingID == "" {
return
}
var resultMsg string
if count == 0 {
resultMsg = i18n.T(ctx.Locale, "search.no_results")
} else if count == 1 {
resultMsg = i18n.T(ctx.Locale, "search.success.one")
} else {
resultMsg = fmt.Sprintf(i18n.T(ctx.Locale, "search.success"), count)
}
msg := &message.Message{
MessageID: loadingID,
Delta: true,
DeltaAction: message.DeltaReplace,
Type: "loading",
Props: map[string]any{
"message": resultMsg,
},
}
if err := ctx.Send(msg); err != nil {
ctx.Logger.Warn("Failed to send search result message: %v", err)
}
}
// sendSearchDone sends the final done message (removes loading indicator)
func (ast *Assistant) sendSearchDone(ctx *context.Context, loadingID string, count int, failed bool) {
if loadingID == "" {
return
}
var resultMsg string
if failed {
resultMsg = i18n.T(ctx.Locale, "search.failed")
} else if count == 0 {
resultMsg = i18n.T(ctx.Locale, "search.no_results")
} else if count == 1 {
resultMsg = i18n.T(ctx.Locale, "search.success.one")
} else {
resultMsg = fmt.Sprintf(i18n.T(ctx.Locale, "search.success"), count)
}
msg := &message.Message{
MessageID: loadingID,
Delta: true,
DeltaAction: message.DeltaReplace,
Type: "loading",
Props: map[string]any{
"message": resultMsg,
"done": true, // Frontend will remove loading indicator
},
}
if err := ctx.Send(msg); err != nil {
ctx.Logger.Warn("Failed to send search done message: %v", err)
}
}
// ============================================================================
// Trace: Search Node
// ============================================================================
// createSearchTrace creates a trace node for search operation
func (ast *Assistant) createSearchTrace(ctx *context.Context, query string, requests []*searchTypes.Request) traceTypes.Node {
trace, _ := ctx.Trace()
if trace == nil {
return nil
}
// Build search types list
var searchTypes []string
for _, req := range requests {
searchTypes = append(searchTypes, string(req.Type))
}
input := map[string]any{
"query": query,
"types": searchTypes,
}
node, err := trace.Add(input, traceTypes.TraceNodeOption{
Label: i18n.T(ctx.Locale, "search.trace.label"),
Type: "search",
Icon: "search",
Description: i18n.T(ctx.Locale, "search.trace.description"),
})
if err != nil {
ctx.Logger.Warn("Failed to create search trace node: %v", err)
return nil
}
// Log search start
node.Info("Starting search", map[string]any{
"query": query,
"types": searchTypes,
})
return node
}
// completeSearchTrace marks the search trace node as completed or failed
func (ast *Assistant) completeSearchTrace(node traceTypes.Node, resultCount int, err error) {
if node == nil {
return
}
if err != nil {
node.Warn("Search failed", map[string]any{"error": err.Error()})
node.Fail(err)
return
}
// Log completion
node.Info("Search completed", map[string]any{
"result_count": resultCount,
})
// Complete with output
node.Complete(map[string]any{
"result_count": resultCount,
})
}
// buildSearchRequests builds search requests based on assistant configuration
func (ast *Assistant) buildSearchRequests(query string, config *searchTypes.Config) []*searchTypes.Request {
var requests []*searchTypes.Request

View file

@ -98,6 +98,24 @@ func init() {
// KB: Chat collection
"kb.chat.name": "Chat Knowledge Base",
"kb.chat.description": "Auto-created knowledge base collection for chat sessions",
// Search: assistant/search.go - Output messages
"search.loading": "Searching...",
"search.success": "Found %d references",
"search.success.one": "Found 1 reference",
"search.partial": "Found %d references (some sources failed)",
"search.failed": "Search failed",
"search.no_results": "No references found",
// Search: assistant/search.go - Trace labels
"search.trace.label": "Search",
"search.trace.description": "Search the web and knowledge base for relevant information",
"search.trace.web.label": "Web Search",
"search.trace.web.description": "Searching the web",
"search.trace.kb.label": "KB Search",
"search.trace.kb.description": "Searching knowledge base",
"search.trace.db.label": "DB Search",
"search.trace.db.description": "Searching database",
},
}
@ -164,6 +182,24 @@ func init() {
// KB: Chat collection
"kb.chat.name": "聊天知识库",
"kb.chat.description": "自动为聊天会话创建的知识库集合",
// Search: assistant/search.go - Output messages
"search.loading": "正在搜索...",
"search.success": "找到 %d 条参考资料",
"search.success.one": "找到 1 条参考资料",
"search.partial": "找到 %d 条参考资料(部分来源失败)",
"search.failed": "搜索失败",
"search.no_results": "未找到相关资料",
// Search: assistant/search.go - Trace labels
"search.trace.label": "搜索",
"search.trace.description": "搜索网络和知识库获取相关信息",
"search.trace.web.label": "网页搜索",
"search.trace.web.description": "搜索网页获取相关信息",
"search.trace.kb.label": "知识库搜索",
"search.trace.kb.description": "搜索知识库获取相关信息",
"search.trace.db.label": "数据库搜索",
"search.trace.db.description": "搜索数据库获取相关信息",
},
}
@ -258,6 +294,24 @@ func init() {
// KB: Chat collection
"kb.chat.name": "聊天知识库",
"kb.chat.description": "自动为聊天会话创建的知识库集合",
// Search: assistant/search.go - Output messages
"search.loading": "正在搜索...",
"search.success": "找到 %d 条参考资料",
"search.success.one": "找到 1 条参考资料",
"search.partial": "找到 %d 条参考资料(部分来源失败)",
"search.failed": "搜索失败",
"search.no_results": "未找到相关资料",
// Search: assistant/search.go - Trace labels
"search.trace.label": "搜索",
"search.trace.description": "搜索网络和知识库获取相关信息",
"search.trace.web.label": "网页搜索",
"search.trace.web.description": "搜索网页获取相关信息",
"search.trace.kb.label": "知识库搜索",
"search.trace.kb.description": "搜索知识库获取相关信息",
"search.trace.db.label": "数据库搜索",
"search.trace.db.description": "搜索数据库获取相关信息",
},
}
}

View file

@ -155,7 +155,7 @@ agent/search/
│ │ ├── builtin.go # Builtin frequency-based extraction
│ │ ├── agent.go # Agent mode (LLM-powered)
│ │ └── mcp.go # MCP mode (external service)
│ └── querydsl/ # QueryDSL generation for DB search (待实现)
│ └── querydsl/ # QueryDSL generation for DB search (TODO)
│ ├── generator.go # Main generator (mode dispatch)
│ ├── builtin.go # Builtin template-based generation
│ ├── agent.go # Agent mode (LLM-powered)
@ -171,22 +171,21 @@ agent/search/
│ │ ├── agent.go # Agent mode (AI Search)
│ │ └── mcp.go # MCP mode (external service)
│ │
│ ├── kb/ # Knowledge base search (骨架)
│ ├── kb/ # Knowledge base search (skeleton)
│ │ ├── handler.go # KB search handler
│ │ ├── vector.go # Vector similarity search (待实现)
│ │ └── graph.go # Graph-based association (待实现)
│ │ ├── vector.go # Vector similarity search (TODO)
│ │ └── graph.go # Graph-based association (TODO)
│ │
│ └── db/ # Database search (骨架)
│ └── db/ # Database search (skeleton)
│ ├── handler.go # DB search handler
│ ├── query.go # QueryDSL builder (待实现)
│ └── schema.go # Model schema introspection (待实现)
│ ├── query.go # QueryDSL builder (TODO)
│ └── schema.go # Model schema introspection (TODO)
└── defaults/ # Default configuration values
└── defaults.go # System built-in defaults (used by agent/load.go)
# 待实现文件:
# - trace.go # Trace node creation and management
# - output.go # Real-time output/streaming to client
# Note: Output and Trace are integrated into assistant/search.go
# No separate trace.go or output.go files needed
```
### Dependency Graph
@ -836,51 +835,177 @@ search:
## Trace Integration
Search operations create trace nodes to report execution details to users, providing transparency about what the agent is doing.
Search operations create minimal trace nodes to report execution status to users, providing transparency about what the agent is doing. Detailed information is recorded via LOG for debugging.
### Trace Node Structure
Uses `trace/types.NodeStatus` constants:
- `pending` - Node created but not started
- `running` - Node is currently executing
- `completed` - Node finished successfully
- `failed` - Node failed with error
**Single Search:**
```
search (type: "search")
├── query // Original query
├── search_type // "web", "kb", or "db"
├── duration_ms
├── status // "success", "failed"
├── result_count
└── children // Sub-operations
├── embedding (kb only)
├── vector_search (kb only)
├── graph_search (kb, if enabled)
├── querydsl_build (db only)
├── db_query (db only)
└── rerank (if enabled)
├── label // i18n: "Search" / "搜索"
├── status // "pending" | "running" | "completed" | "failed"
├── input
│ ├── query // Original query
│ └── types // ["web"], ["kb"], ["web", "kb", "db"]
└── output // (set on complete)
└── result_count // Total results found
```
**Parallel Search:**
```
search (type: "search")
├── label // i18n: "Search" / "搜索"
├── status // "pending" | "running" | "completed" | "failed"
├── input
│ ├── query // Original query
│ └── types // ["web", "kb", "db"]
└── children
├── web (type: "search_item")
│ ├── label // i18n: "Web Search" / "网页搜索"
│ ├── status // "pending" | "running" | "completed" | "failed"
│ └── output
│ └── result_count
├── kb (type: "search_item")
│ └── ...
└── db (type: "search_item")
└── ...
```
### Trace Logging
Detailed search information is recorded via Trace node logging methods (broadcasts to client):
```go
// Node logging methods (from trace/node.go):
// - node.Info(message, args...) - Info level log
// - node.Debug(message, args...) - Debug level log
// - node.Warn(message, args...) - Warning level log
// - node.Error(message, args...) - Error level log
// Search start
searchNode.Info("Starting search", map[string]any{"query": query, "types": types})
// Per-type results (on parallel search children)
webNode.Debug("Web search completed", map[string]any{"count": count, "duration_ms": duration})
kbNode.Debug("KB search completed", map[string]any{"count": count, "duration_ms": duration})
dbNode.Debug("DB search completed", map[string]any{"count": count, "duration_ms": duration})
// Errors (non-blocking, search continues)
webNode.Warn("Web search failed", map[string]any{"error": err.Error()})
// Final summary (on parent node)
searchNode.Info("Search completed", map[string]any{"total": total, "duration_ms": duration})
```
**Log Event Structure** (broadcasted via SSE):
```go
// types.TraceLog
type TraceLog struct {
Timestamp int64 `json:"timestamp"` // milliseconds since epoch
Level string `json:"level"` // "info", "debug", "warn", "error"
Message string `json:"message"` // Log message
Data any `json:"data"` // Additional data
NodeID string `json:"node_id"` // Parent node ID
}
```
## Real-time Output
Search progress is streamed to the client via the output system.
Search progress is displayed to the client using **Loading component with Replace** pattern. Uses `ctx.Send()` and `ctx.Replace()` methods.
### Output Message Types
### Output Flow
```
1. Send Loading Message
loading_id = ctx.Send({ type: "loading", props: { message: "Searching..." } })
→ Client displays loading indicator
2. Execute Search (parallel web/kb/db)
3. Replace with Result Message (shows result to user)
ctx.Replace(loading_id, { type: "loading", props: { message: "Found 5 references" } })
→ Client displays result message
4. Mark as Done (removes the loading after brief display)
ctx.Replace(loading_id, { type: "loading", props: { message: "Found 5 references", done: true } })
→ Client removes loading indicator
```
### Implementation
```go
const (
TypeSearchStart = "search_start" // Search initiated
TypeSearchResult = "search_result" // Result item (streamed)
TypeSearchComplete = "search_complete" // Search completed
)
// Send loading message
loadingID := ctx.Send(map[string]any{
"type": "loading",
"props": map[string]any{
"message": i18n.Tr("search.loading", locale), // "Searching..." / "正在搜索..."
},
})
// Execute search...
// Replace with result message (displayed to user)
resultMessage := i18n.Tr("search.success", locale, count) // "Found 5 references"
ctx.Replace(loadingID, map[string]any{
"type": "loading",
"props": map[string]any{
"message": resultMessage,
},
})
// Mark as done (removes loading indicator after user sees the result)
ctx.Replace(loadingID, map[string]any{
"type": "loading",
"props": map[string]any{
"message": resultMessage,
"done": true, // Frontend will remove loading indicator
},
})
```
### Loading Props
| Prop | Type | Description |
| --------- | ------ | --------------------------------------------------- |
| `message` | string | Localized message to display |
| `done` | bool | When `true`, frontend removes the loading indicator |
### Localized Messages
| Scenario | English | Chinese |
| ------------- | ---------------------------------------- | --------------------------------- |
| Loading | Searching... | 正在搜索... |
| Success (1) | Found 1 reference | 找到 1 条参考资料 |
| Success (N) | Found N references | 找到 N 条参考资料 |
| Partial Error | Found N references (some sources failed) | 找到 N 条参考资料(部分来源失败) |
| All Failed | Search failed | 搜索失败 |
| No Results | No references found | 未找到相关资料 |
### Client Display Example
```
🔍 Searching "latest AI developments"...
Frame 1 - During search:
┌─────────────────────────────────┐
│ Searching... │ ← Loading (done: false)
└─────────────────────────────────┘
📄 Found 5 results:
1. #ref:a1b2 - OpenAI Announces GPT-5
2. #ref:c3d4 - Google's New AI Model
...
Frame 2 - Result displayed:
┌─────────────────────────────────┐
│ Found 5 references │ ← Result (done: false)
└─────────────────────────────────┘
✅ Search complete (1.2s)
Frame 3 - Removed:
(loading indicator removed when done: true)
```
## JSAPI Integration
@ -1978,7 +2103,7 @@ SerpAPI supports multiple search engines via the `engine` config:
| ------------ | ---------------------------- |
| `google` | Google Search (default) |
| `bing` | Bing Search |
| `baidu` | Baidu (百度) |
| `baidu` | Baidu Search (Chinese) |
| `yandex` | Yandex Search |
| `yahoo` | Yahoo Search |
| `duckduckgo` | DuckDuckGo Search |