You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

224 lines
7.0 KiB
Go

/*
* Copyright 2025 CloudWeGo Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package main
import (
"context"
"fmt"
"log"
"strings"
"time"
"github.com/cloudwego/eino/adk"
"github.com/cloudwego/eino/components/prompt"
"github.com/cloudwego/eino/components/tool"
"github.com/cloudwego/eino/compose"
"github.com/cloudwego/eino/schema"
"github.com/cloudwego/eino-examples/adk/common/model"
"github.com/cloudwego/eino-examples/adk/common/prints"
"github.com/cloudwego/eino-examples/adk/common/tool/graphtool"
)
type ResearchInput struct {
Query string `json:"query" jsonschema_description:"The research topic or question to investigate"`
}
func mockWebSearch(ctx context.Context, query string) (string, error) {
time.Sleep(100 * time.Millisecond)
return fmt.Sprintf(`Web Search Results for "%s":
1. Wikipedia: %s is a widely discussed topic with multiple perspectives...
2. News Article: Recent developments in %s show promising trends...
3. Research Paper: A comprehensive study on %s reveals key insights...`, query, query, query, query), nil
}
func mockKnowledgeBaseSearch(ctx context.Context, query string) (string, error) {
time.Sleep(80 * time.Millisecond)
return fmt.Sprintf(`Knowledge Base Results for "%s":
- Internal Doc #1: Company guidelines related to %s...
- Internal Doc #2: Best practices for handling %s...
- FAQ Entry: Common questions about %s answered...`, query, query, query, query), nil
}
func mockLocalFileSearch(ctx context.Context, query string) (string, error) {
time.Sleep(50 * time.Millisecond)
return fmt.Sprintf(`Local File Results for "%s":
- notes/research_%s.md: Personal notes on %s...
- docs/guide_%s.txt: Step-by-step guide for %s...`, query, strings.ReplaceAll(query, " ", "_"), query, strings.ReplaceAll(query, " ", "_"), query), nil
}
type searchResults struct {
Query string
WebResults string
KBResults string
LocalResults string
}
func NewResearchTool(ctx context.Context) (tool.StreamableTool, error) {
cm := model.NewChatModel()
synthesizePrompt := prompt.FromMessages(schema.FString,
schema.SystemMessage(`You are a research analyst. Synthesize the following search results from multiple sources into a coherent summary.
Focus on the most relevant and reliable information. Identify any conflicting information across sources.
Be concise but comprehensive. Output the summary directly without any JSON formatting.`),
schema.UserMessage(`Research Query: {query}
Web Search Results:
{web_results}
Knowledge Base Results:
{kb_results}
Local File Results:
{local_results}
Please synthesize these results into a comprehensive summary:`))
graph := compose.NewGraph[*ResearchInput, *schema.Message]()
_ = graph.AddLambdaNode("parallel_search", compose.InvokableLambda(func(ctx context.Context, input *ResearchInput) (*searchResults, error) {
fmt.Println(" [Graph] Starting parallel searches...")
type result struct {
source string
data string
err error
}
resultCh := make(chan result, 3)
go func() {
data, err := mockWebSearch(ctx, input.Query)
resultCh <- result{source: "web", data: data, err: err}
}()
go func() {
data, err := mockKnowledgeBaseSearch(ctx, input.Query)
resultCh <- result{source: "kb", data: data, err: err}
}()
go func() {
data, err := mockLocalFileSearch(ctx, input.Query)
resultCh <- result{source: "local", data: data, err: err}
}()
results := &searchResults{Query: input.Query}
for i := 0; i < 3; i++ {
r := <-resultCh
if r.err != nil {
return nil, r.err
}
switch r.source {
case "web":
results.WebResults = r.data
fmt.Println(" [Graph] Web search completed")
case "kb":
results.KBResults = r.data
fmt.Println(" [Graph] Knowledge base search completed")
case "local":
results.LocalResults = r.data
fmt.Println(" [Graph] Local file search completed")
}
}
fmt.Println(" [Graph] All searches completed, preparing synthesis...")
return results, nil
}))
_ = graph.AddLambdaNode("prepare_prompt_input", compose.InvokableLambda(func(ctx context.Context, results *searchResults) (map[string]any, error) {
return map[string]any{
"query": results.Query,
"web_results": results.WebResults,
"kb_results": results.KBResults,
"local_results": results.LocalResults,
}, nil
}))
_ = graph.AddChatTemplateNode("prepare_prompt", synthesizePrompt)
_ = graph.AddChatModelNode("synthesize", cm)
_ = graph.AddEdge(compose.START, "parallel_search")
_ = graph.AddEdge("parallel_search", "prepare_prompt_input")
_ = graph.AddEdge("prepare_prompt_input", "prepare_prompt")
_ = graph.AddEdge("prepare_prompt", "synthesize")
_ = graph.AddEdge("synthesize", compose.END)
return graphtool.NewStreamableGraphTool[*ResearchInput, *schema.Message](
graph,
"research_topic",
"Research a topic by querying multiple sources (web, knowledge base, local files) in parallel and synthesizing the results. Returns a streaming summary directly.",
)
}
func main() {
ctx := context.Background()
researchTool, err := NewResearchTool(ctx)
if err != nil {
log.Fatalf("failed to create research tool: %v", err)
}
agent, err := adk.NewChatModelAgent(ctx, &adk.ChatModelAgentConfig{
Name: "ResearchAssistant",
Description: "An assistant that can research topics using multiple sources",
Instruction: `You are a helpful research assistant.
When the user asks about a topic or wants to learn something, use the research_topic tool to gather information from multiple sources.
The tool will stream the research results directly to the user.`,
Model: model.NewChatModel(),
ToolsConfig: adk.ToolsConfig{
ToolsNodeConfig: compose.ToolsNodeConfig{
Tools: []tool.BaseTool{researchTool},
},
ReturnDirectly: map[string]bool{
"research_topic": true,
},
},
})
if err != nil {
log.Fatalf("failed to create agent: %v", err)
}
runner := adk.NewRunner(ctx, adk.RunnerConfig{
EnableStreaming: true,
Agent: agent,
})
query := "What are the best practices for building microservices?"
iter := runner.Query(ctx, query)
fmt.Println("=== Multi-Source Research Example (using compose.Graph + StreamableGraphTool) ===")
fmt.Println()
fmt.Println("This example demonstrates:")
fmt.Println("1. StreamableGraphTool with compose.Graph")
fmt.Println("2. Parallel search execution within a graph node")
fmt.Println("3. Streaming output from ChatModel via ReturnDirectly")
fmt.Println()
for {
event, ok := iter.Next()
if !ok {
break
}
if event.Err != nil {
log.Fatalf("error: %v", event.Err)
}
prints.Event(event)
}
}