Fix orchestrator logic and workspace push for planning confirmation and artifact handling
This commit is contained in:
@@ -15,8 +15,8 @@ import (
|
||||
|
||||
// Config 定义了网络搜索工具所需的配置参数。
|
||||
type Config struct {
|
||||
Engine string // 搜索引擎类型,支持 "duckduckgo" 或 "brave"
|
||||
APIKey string // 搜索引擎的 API Key(Brave 搜索必填)
|
||||
Engine string // 搜索引擎类型,支持 "duckduckgo"、"brave" 或 "tavily"
|
||||
APIKey string // 搜索引擎的 API Key(Brave 或 Tavily 搜索必填)
|
||||
}
|
||||
|
||||
// Tool represents a web search tool.
|
||||
@@ -85,6 +85,8 @@ func (t *Tool) Call(ctx context.Context, input string) (string, error) {
|
||||
switch t.engine {
|
||||
case "brave":
|
||||
result, err = t.searchBrave(ctx, query)
|
||||
case "tavily":
|
||||
result, err = t.searchTavily(ctx, query)
|
||||
default:
|
||||
result, err = t.searchDuckDuckGo(ctx, query)
|
||||
}
|
||||
@@ -126,12 +128,20 @@ func (t *Tool) searchDuckDuckGo(ctx context.Context, query string) (string, erro
|
||||
return "", fmt.Errorf("read response body failed: %w", err)
|
||||
}
|
||||
|
||||
if t.log != nil {
|
||||
t.log.Debugf("duckduckgo raw response: %s", string(body))
|
||||
}
|
||||
|
||||
var ddg duckDuckGoResponse
|
||||
if err := json.Unmarshal(body, &ddg); err != nil {
|
||||
return "", fmt.Errorf("parse duckduckgo response failed: %w", err)
|
||||
}
|
||||
|
||||
return t.formatDuckDuckGoResult(query, ddg), nil
|
||||
result := t.formatDuckDuckGoResult(query, ddg)
|
||||
if t.log != nil {
|
||||
t.log.Infof("duckduckgo search finished, content_found=%v", (ddg.Answer != "" || ddg.AbstractText != "" || len(ddg.RelatedTopics) > 0))
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// duckDuckGoResponse 从 DuckDuckGo 获取的即时结果 JSON 映射结构。
|
||||
@@ -239,11 +249,19 @@ func (t *Tool) searchBrave(ctx context.Context, query string) (string, error) {
|
||||
return "", fmt.Errorf("read response body failed: %w", err)
|
||||
}
|
||||
|
||||
if t.log != nil {
|
||||
t.log.Debugf("brave search raw response: %s", string(body))
|
||||
}
|
||||
|
||||
var braveResp braveSearchResponse
|
||||
if err := json.Unmarshal(body, &braveResp); err != nil {
|
||||
return "", fmt.Errorf("parse brave response failed: %w", err)
|
||||
}
|
||||
|
||||
if t.log != nil {
|
||||
t.log.Infof("brave search finished, results_count=%d", len(braveResp.Web.Results))
|
||||
}
|
||||
|
||||
return t.formatBraveResult(query, braveResp), nil
|
||||
}
|
||||
|
||||
@@ -286,3 +304,92 @@ func (t *Tool) formatBraveResult(query string, resp braveSearchResponse) string
|
||||
|
||||
return strings.TrimSpace(b.String())
|
||||
}
|
||||
|
||||
// searchTavily uses the Tavily Search API (requires API key).
|
||||
func (t *Tool) searchTavily(ctx context.Context, query string) (string, error) {
|
||||
if t.apiKey == "" {
|
||||
return "", fmt.Errorf("WEB_SEARCH_API_KEY is required for Tavily engine")
|
||||
}
|
||||
|
||||
apiURL := "https://api.tavily.com/search"
|
||||
payload := map[string]interface{}{
|
||||
"api_key": t.apiKey,
|
||||
"query": query,
|
||||
"search_depth": "basic",
|
||||
"include_answer": true,
|
||||
"include_images": false,
|
||||
"include_raw_content": false,
|
||||
"max_results": 5,
|
||||
}
|
||||
|
||||
jsonData, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("marshal tavily payload failed: %w", err)
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, apiURL, strings.NewReader(string(jsonData)))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("create request failed: %w", err)
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := t.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("http request failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
bodySnippet, _ := io.ReadAll(io.LimitReader(resp.Body, 1024))
|
||||
return "", fmt.Errorf("tavily search returned status %d: %s", resp.StatusCode, string(bodySnippet))
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(io.LimitReader(resp.Body, 512*1024))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("read response body failed: %w", err)
|
||||
}
|
||||
|
||||
if t.log != nil {
|
||||
t.log.Debugf("tavily search raw response: %s", string(body))
|
||||
}
|
||||
|
||||
var tavilyResp tavilyResponse
|
||||
if err := json.Unmarshal(body, &tavilyResp); err != nil {
|
||||
return "", fmt.Errorf("parse tavily response failed: %w", err)
|
||||
}
|
||||
|
||||
return t.formatTavilyResult(query, tavilyResp), nil
|
||||
}
|
||||
|
||||
type tavilyResponse struct {
|
||||
Answer string `json:"answer"`
|
||||
Results []tavilyResult `json:"results"`
|
||||
}
|
||||
|
||||
type tavilyResult struct {
|
||||
Title string `json:"title"`
|
||||
URL string `json:"url"`
|
||||
Content string `json:"content"`
|
||||
Score float64 `json:"score"`
|
||||
}
|
||||
|
||||
func (t *Tool) formatTavilyResult(query string, resp tavilyResponse) string {
|
||||
b := strings.Builder{}
|
||||
b.WriteString("Search: " + query + "\n")
|
||||
b.WriteString("Engine: Tavily\n\n")
|
||||
|
||||
if resp.Answer != "" {
|
||||
b.WriteString("Answer: " + resp.Answer + "\n\n")
|
||||
}
|
||||
|
||||
if len(resp.Results) == 0 && resp.Answer == "" {
|
||||
b.WriteString("No results found.\n")
|
||||
return strings.TrimSpace(b.String())
|
||||
}
|
||||
|
||||
for i, r := range resp.Results {
|
||||
b.WriteString(fmt.Sprintf("%d. %s\n %s\n %s\n\n", i+1, r.Title, r.URL, r.Content))
|
||||
}
|
||||
|
||||
return strings.TrimSpace(b.String())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user