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:
parent
2a191950d7
commit
42bb27a8dd
3 changed files with 398 additions and 39 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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": "搜索数据库获取相关信息",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 |
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue