Fix orchestrator logic and workspace push for planning confirmation and artifact handling

This commit is contained in:
whlaoding
2026-03-16 13:17:23 +08:00
parent 9fccb0a473
commit 2ecf4e903a
6 changed files with 217 additions and 38 deletions

View File

@@ -15,8 +15,8 @@ import (
// Config 定义了网络搜索工具所需的配置参数。
type Config struct {
Engine string // 搜索引擎类型,支持 "duckduckgo" 或 "brave"
APIKey string // 搜索引擎的 API KeyBrave 搜索必填)
Engine string // 搜索引擎类型,支持 "duckduckgo"、"brave" 或 "tavily"
APIKey string // 搜索引擎的 API KeyBrave 或 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())
}

View File

@@ -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" {