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())
|
||||
}
|
||||
|
||||
@@ -1,9 +1,62 @@
|
||||
package websearch
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestTavilyIntegration(t *testing.T) {
|
||||
// 这是一个集成测试,会实际请求网络。
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test in short mode")
|
||||
}
|
||||
|
||||
// 这里的 API Key 从环境变量中获取或手动填写的测试环境变量中读取。
|
||||
apiKey := "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"
|
||||
if apiKey == "" {
|
||||
t.Skip("TAVILY_API_KEY is not set")
|
||||
}
|
||||
|
||||
tool := New(Config{Engine: "tavily", APIKey: apiKey}, 4000, nil)
|
||||
ctx := context.Background()
|
||||
query := "wuhan weather today"
|
||||
|
||||
result, err := tool.Call(ctx, query)
|
||||
if err != nil {
|
||||
t.Fatalf("Tavily search failed: %v", err)
|
||||
}
|
||||
|
||||
fmt.Printf("Tavily search result for '%s':\n%s\n", query, result)
|
||||
|
||||
if result == "" {
|
||||
t.Fatal("expected non-empty result from Tavily")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDuckDuckGoIntegration(t *testing.T) {
|
||||
// 这是一个集成测试,会实际请求网络。
|
||||
// 在 CI 环境中可能需要跳过。
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test in short mode")
|
||||
}
|
||||
|
||||
tool := New(Config{Engine: "duckduckgo"}, 4000, nil)
|
||||
ctx := context.Background()
|
||||
query := "wuhan weather"
|
||||
|
||||
result, err := tool.Call(ctx, query)
|
||||
if err != nil {
|
||||
t.Fatalf("DuckDuckGo search failed: %v", err)
|
||||
}
|
||||
|
||||
fmt.Printf("DuckDuckGo search result for '%s':\n%s\n", query, result)
|
||||
|
||||
if result == "" {
|
||||
t.Fatal("expected non-empty result from DuckDuckGo")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewDefaultEngine(t *testing.T) {
|
||||
tool := New(Config{}, 4000, nil)
|
||||
if tool.Name() != "web_search" {
|
||||
|
||||
Reference in New Issue
Block a user