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
|
package assistant
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/yaoapp/yao/agent/context"
|
"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"
|
||||||
"github.com/yaoapp/yao/agent/search/nlp/keyword"
|
"github.com/yaoapp/yao/agent/search/nlp/keyword"
|
||||||
searchTypes "github.com/yaoapp/yao/agent/search/types"
|
searchTypes "github.com/yaoapp/yao/agent/search/types"
|
||||||
|
traceTypes "github.com/yaoapp/yao/trace/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
// shouldAutoSearch determines if auto search should be executed
|
// shouldAutoSearch determines if auto search should be executed
|
||||||
|
|
@ -136,6 +140,12 @@ func (ast *Assistant) executeAutoSearch(ctx *context.Context, messages []context
|
||||||
return nil
|
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
|
// Execute searches in parallel
|
||||||
ctx.Logger.Info("Executing %d search requests for query: %s", len(requests), truncateString(query, 50))
|
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 {
|
if err != nil {
|
||||||
// Log error but don't fail - search errors shouldn't block the main flow
|
// Log error but don't fail - search errors shouldn't block the main flow
|
||||||
ctx.Logger.Error("Auto search failed: %v", err)
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -153,15 +169,179 @@ func (ast *Assistant) executeAutoSearch(ctx *context.Context, messages []context
|
||||||
}
|
}
|
||||||
refCtx := search.BuildReferenceContext(results, citationConfig)
|
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")
|
ctx.Logger.Info("No search results found")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.Logger.Info("Auto search completed: %d references", len(refCtx.References))
|
ctx.Logger.Info("Auto search completed: %d references", resultCount)
|
||||||
return refCtx
|
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
|
// buildSearchRequests builds search requests based on assistant configuration
|
||||||
func (ast *Assistant) buildSearchRequests(query string, config *searchTypes.Config) []*searchTypes.Request {
|
func (ast *Assistant) buildSearchRequests(query string, config *searchTypes.Config) []*searchTypes.Request {
|
||||||
var requests []*searchTypes.Request
|
var requests []*searchTypes.Request
|
||||||
|
|
|
||||||
|
|
@ -98,6 +98,24 @@ func init() {
|
||||||
// KB: Chat collection
|
// KB: Chat collection
|
||||||
"kb.chat.name": "Chat Knowledge Base",
|
"kb.chat.name": "Chat Knowledge Base",
|
||||||
"kb.chat.description": "Auto-created knowledge base collection for chat sessions",
|
"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 collection
|
||||||
"kb.chat.name": "聊天知识库",
|
"kb.chat.name": "聊天知识库",
|
||||||
"kb.chat.description": "自动为聊天会话创建的知识库集合",
|
"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 collection
|
||||||
"kb.chat.name": "聊天知识库",
|
"kb.chat.name": "聊天知识库",
|
||||||
"kb.chat.description": "自动为聊天会话创建的知识库集合",
|
"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
|
│ │ ├── builtin.go # Builtin frequency-based extraction
|
||||||
│ │ ├── agent.go # Agent mode (LLM-powered)
|
│ │ ├── agent.go # Agent mode (LLM-powered)
|
||||||
│ │ └── mcp.go # MCP mode (external service)
|
│ │ └── 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)
|
│ ├── generator.go # Main generator (mode dispatch)
|
||||||
│ ├── builtin.go # Builtin template-based generation
|
│ ├── builtin.go # Builtin template-based generation
|
||||||
│ ├── agent.go # Agent mode (LLM-powered)
|
│ ├── agent.go # Agent mode (LLM-powered)
|
||||||
|
|
@ -171,22 +171,21 @@ agent/search/
|
||||||
│ │ ├── agent.go # Agent mode (AI Search)
|
│ │ ├── agent.go # Agent mode (AI Search)
|
||||||
│ │ └── mcp.go # MCP mode (external service)
|
│ │ └── mcp.go # MCP mode (external service)
|
||||||
│ │
|
│ │
|
||||||
│ ├── kb/ # Knowledge base search (骨架)
|
│ ├── kb/ # Knowledge base search (skeleton)
|
||||||
│ │ ├── handler.go # KB search handler
|
│ │ ├── handler.go # KB search handler
|
||||||
│ │ ├── vector.go # Vector similarity search (待实现)
|
│ │ ├── vector.go # Vector similarity search (TODO)
|
||||||
│ │ └── graph.go # Graph-based association (待实现)
|
│ │ └── graph.go # Graph-based association (TODO)
|
||||||
│ │
|
│ │
|
||||||
│ └── db/ # Database search (骨架)
|
│ └── db/ # Database search (skeleton)
|
||||||
│ ├── handler.go # DB search handler
|
│ ├── handler.go # DB search handler
|
||||||
│ ├── query.go # QueryDSL builder (待实现)
|
│ ├── query.go # QueryDSL builder (TODO)
|
||||||
│ └── schema.go # Model schema introspection (待实现)
|
│ └── schema.go # Model schema introspection (TODO)
|
||||||
│
|
│
|
||||||
└── defaults/ # Default configuration values
|
└── defaults/ # Default configuration values
|
||||||
└── defaults.go # System built-in defaults (used by agent/load.go)
|
└── defaults.go # System built-in defaults (used by agent/load.go)
|
||||||
|
|
||||||
# 待实现文件:
|
# Note: Output and Trace are integrated into assistant/search.go
|
||||||
# - trace.go # Trace node creation and management
|
# No separate trace.go or output.go files needed
|
||||||
# - output.go # Real-time output/streaming to client
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Dependency Graph
|
### Dependency Graph
|
||||||
|
|
@ -836,51 +835,177 @@ search:
|
||||||
|
|
||||||
## Trace Integration
|
## 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
|
### 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")
|
search (type: "search")
|
||||||
├── query // Original query
|
├── label // i18n: "Search" / "搜索"
|
||||||
├── search_type // "web", "kb", or "db"
|
├── status // "pending" | "running" | "completed" | "failed"
|
||||||
├── duration_ms
|
├── input
|
||||||
├── status // "success", "failed"
|
│ ├── query // Original query
|
||||||
├── result_count
|
│ └── types // ["web"], ["kb"], ["web", "kb", "db"]
|
||||||
└── children // Sub-operations
|
└── output // (set on complete)
|
||||||
├── embedding (kb only)
|
└── result_count // Total results found
|
||||||
├── vector_search (kb only)
|
```
|
||||||
├── graph_search (kb, if enabled)
|
|
||||||
├── querydsl_build (db only)
|
**Parallel Search:**
|
||||||
├── db_query (db only)
|
|
||||||
└── rerank (if enabled)
|
```
|
||||||
|
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
|
## 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
|
```go
|
||||||
const (
|
// Send loading message
|
||||||
TypeSearchStart = "search_start" // Search initiated
|
loadingID := ctx.Send(map[string]any{
|
||||||
TypeSearchResult = "search_result" // Result item (streamed)
|
"type": "loading",
|
||||||
TypeSearchComplete = "search_complete" // Search completed
|
"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
|
### Client Display Example
|
||||||
|
|
||||||
```
|
```
|
||||||
🔍 Searching "latest AI developments"...
|
Frame 1 - During search:
|
||||||
|
┌─────────────────────────────────┐
|
||||||
|
│ Searching... │ ← Loading (done: false)
|
||||||
|
└─────────────────────────────────┘
|
||||||
|
|
||||||
📄 Found 5 results:
|
Frame 2 - Result displayed:
|
||||||
1. #ref:a1b2 - OpenAI Announces GPT-5
|
┌─────────────────────────────────┐
|
||||||
2. #ref:c3d4 - Google's New AI Model
|
│ Found 5 references │ ← Result (done: false)
|
||||||
...
|
└─────────────────────────────────┘
|
||||||
|
|
||||||
✅ Search complete (1.2s)
|
Frame 3 - Removed:
|
||||||
|
(loading indicator removed when done: true)
|
||||||
```
|
```
|
||||||
|
|
||||||
## JSAPI Integration
|
## JSAPI Integration
|
||||||
|
|
@ -1978,7 +2103,7 @@ SerpAPI supports multiple search engines via the `engine` config:
|
||||||
| ------------ | ---------------------------- |
|
| ------------ | ---------------------------- |
|
||||||
| `google` | Google Search (default) |
|
| `google` | Google Search (default) |
|
||||||
| `bing` | Bing Search |
|
| `bing` | Bing Search |
|
||||||
| `baidu` | Baidu (百度) |
|
| `baidu` | Baidu Search (Chinese) |
|
||||||
| `yandex` | Yandex Search |
|
| `yandex` | Yandex Search |
|
||||||
| `yahoo` | Yahoo Search |
|
| `yahoo` | Yahoo Search |
|
||||||
| `duckduckgo` | DuckDuckGo Search |
|
| `duckduckgo` | DuckDuckGo Search |
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue