# Building an Internet-Connected Chatbot with ADK-Go This guide shows how to build a chatbot that can search the web, fetch pages, and access external APIs to provide accurate, up-to-date information. --- ## Overview The chatbot combines: - **Google Search** for finding current information - **URL Fetching** for reading full web pages - **Custom API tools** for specific data sources - **Session memory** for context across conversations ``` ┌──────────────────────────────────────────────────────────────┐ │ Smart Chatbot │ ├──────────────────────────────────────────────────────────────┤ │ Tools: │ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │ │ Google │ │ Fetch │ │ Weather │ + more │ │ │ Search │ │ URL │ │ API │ │ │ └─────────────┘ └─────────────┘ └─────────────┘ │ ├──────────────────────────────────────────────────────────────┤ │ Memory: User preferences, conversation history, facts │ └──────────────────────────────────────────────────────────────┘ ``` --- ## Project Setup ```bash mkdir smart-chatbot && cd smart-chatbot go mod init smart-chatbot go get google.golang.org/adk ``` --- ## Basic Internet-Connected Chatbot ```go package main import ( "context" "log" "os" "google.golang.org/adk/agent/llmagent" "google.golang.org/adk/cmd/launcher/adk" "google.golang.org/adk/cmd/launcher/full" "google.golang.org/adk/model/gemini" "google.golang.org/adk/server/restapi/services" "google.golang.org/adk/tool" "google.golang.org/adk/tool/geminitool" "google.golang.org/genai" ) func main() { ctx := context.Background() model, err := gemini.NewModel(ctx, "gemini-3-flash-preview", &genai.ClientConfig{ APIKey: os.Getenv("GOOGLE_API_KEY"), }) if err != nil { log.Fatal(err) } chatbot, err := llmagent.New(llmagent.Config{ Name: "smart_assistant", Model: model, Description: "An intelligent assistant with internet access", Instruction: `You are a helpful, knowledgeable assistant with access to the internet. CAPABILITIES: - Search the web for current information - Look up facts, news, weather, sports scores, etc. - Provide accurate, sourced information GUIDELINES: 1. For factual questions, search to verify before answering 2. Always cite your sources when providing information from search 3. If search results are unclear or conflicting, say so 4. For opinions or analysis, clearly distinguish from facts 5. Be conversational but accurate When you don't know something and can't find it, admit it honestly.`, Tools: []tool.Tool{ geminitool.GoogleSearch{}, }, }) if err != nil { log.Fatal(err) } l := full.NewLauncher() cfg := &adk.Config{AgentLoader: services.NewSingleAgentLoader(chatbot)} if err := l.Execute(ctx, cfg, os.Args[1:]); err != nil { log.Fatal(err) } } ``` --- ## Adding Custom Tools for Rich Information ### URL Fetcher Tool ```go import ( "io" "net/http" "strings" "time" "google.golang.org/adk/tool" "google.golang.org/adk/tool/functiontool" ) type FetchURLInput struct { URL string `json:"url" jsonschema:"The URL to fetch content from"` } type FetchURLOutput struct { Content string `json:"content"` StatusCode int `json:"status_code"` Error string `json:"error,omitempty"` } func fetchURL(ctx tool.Context, input FetchURLInput) FetchURLOutput { client := &http.Client{Timeout: 15 * time.Second} req, err := http.NewRequestWithContext(ctx, "GET", input.URL, nil) if err != nil { return FetchURLOutput{Error: err.Error()} } req.Header.Set("User-Agent", "Mozilla/5.0 (compatible; SmartBot/1.0)") resp, err := client.Do(req) if err != nil { return FetchURLOutput{Error: err.Error()} } defer resp.Body.Close() // Limit response size body, err := io.ReadAll(io.LimitReader(resp.Body, 100000)) if err != nil { return FetchURLOutput{Error: err.Error()} } // Basic HTML to text (simplified) content := stripHTML(string(body)) return FetchURLOutput{ Content: content, StatusCode: resp.StatusCode, } } func stripHTML(html string) string { // Simple HTML tag removal (use a proper library in production) var result strings.Builder inTag := false for _, r := range html { switch { case r == '<': inTag = true case r == '>': inTag = false case !inTag: result.WriteRune(r) } } return strings.TrimSpace(result.String()) } ``` ### Weather API Tool ```go type GetWeatherInput struct { Location string `json:"location" jsonschema:"City name or zip code"` Units string `json:"units,omitempty" jsonschema:"Units: metric or imperial (default: metric)"` } type GetWeatherOutput struct { Location string `json:"location"` Temperature float64 `json:"temperature"` Conditions string `json:"conditions"` Humidity int `json:"humidity"` WindSpeed float64 `json:"wind_speed"` Error string `json:"error,omitempty"` } func getWeather(ctx tool.Context, input GetWeatherInput) GetWeatherOutput { apiKey := os.Getenv("OPENWEATHER_API_KEY") if apiKey == "" { return GetWeatherOutput{Error: "Weather API not configured"} } units := input.Units if units == "" { units = "metric" } url := fmt.Sprintf( "https://api.openweathermap.org/data/2.5/weather?q=%s&units=%s&appid=%s", url.QueryEscape(input.Location), units, apiKey, ) resp, err := http.Get(url) if err != nil { return GetWeatherOutput{Error: err.Error()} } defer resp.Body.Close() var data struct { Name string `json:"name"` Main struct { Temp float64 `json:"temp"` Humidity int `json:"humidity"` } `json:"main"` Weather []struct { Description string `json:"description"` } `json:"weather"` Wind struct { Speed float64 `json:"speed"` } `json:"wind"` } if err := json.NewDecoder(resp.Body).Decode(&data); err != nil { return GetWeatherOutput{Error: err.Error()} } conditions := "" if len(data.Weather) > 0 { conditions = data.Weather[0].Description } return GetWeatherOutput{ Location: data.Name, Temperature: data.Main.Temp, Conditions: conditions, Humidity: data.Main.Humidity, WindSpeed: data.Wind.Speed, } } ``` ### News API Tool ```go type GetNewsInput struct { Query string `json:"query" jsonschema:"Search query for news"` Category string `json:"category,omitempty" jsonschema:"Category: business, technology, sports, etc."` Count int `json:"count,omitempty" jsonschema:"Number of articles (default: 5, max: 10)"` } type NewsArticle struct { Title string `json:"title"` Description string `json:"description"` Source string `json:"source"` URL string `json:"url"` PublishedAt string `json:"published_at"` } type GetNewsOutput struct { Articles []NewsArticle `json:"articles"` Error string `json:"error,omitempty"` } func getNews(ctx tool.Context, input GetNewsInput) GetNewsOutput { apiKey := os.Getenv("NEWS_API_KEY") if apiKey == "" { return GetNewsOutput{Error: "News API not configured"} } count := input.Count if count <= 0 || count > 10 { count = 5 } url := fmt.Sprintf( "https://newsapi.org/v2/everything?q=%s&pageSize=%d&apiKey=%s", url.QueryEscape(input.Query), count, apiKey, ) resp, err := http.Get(url) if err != nil { return GetNewsOutput{Error: err.Error()} } defer resp.Body.Close() var data struct { Articles []struct { Title string `json:"title"` Description string `json:"description"` URL string `json:"url"` PublishedAt string `json:"publishedAt"` Source struct { Name string `json:"name"` } `json:"source"` } `json:"articles"` } if err := json.NewDecoder(resp.Body).Decode(&data); err != nil { return GetNewsOutput{Error: err.Error()} } articles := make([]NewsArticle, len(data.Articles)) for i, a := range data.Articles { articles[i] = NewsArticle{ Title: a.Title, Description: a.Description, Source: a.Source.Name, URL: a.URL, PublishedAt: a.PublishedAt, } } return GetNewsOutput{Articles: articles} } ``` --- ## Complete Chatbot with All Tools ```go package main import ( "context" "encoding/json" "fmt" "io" "log" "net/http" "net/url" "os" "strings" "time" "google.golang.org/adk/agent/llmagent" "google.golang.org/adk/cmd/launcher/adk" "google.golang.org/adk/cmd/launcher/full" "google.golang.org/adk/model/gemini" "google.golang.org/adk/server/restapi/services" "google.golang.org/adk/tool" "google.golang.org/adk/tool/functiontool" "google.golang.org/adk/tool/geminitool" "google.golang.org/genai" ) func main() { ctx := context.Background() model, err := gemini.NewModel(ctx, "gemini-3-flash-preview", &genai.ClientConfig{ APIKey: os.Getenv("GOOGLE_API_KEY"), }) if err != nil { log.Fatal(err) } tools, err := createTools() if err != nil { log.Fatal(err) } chatbot, err := llmagent.New(llmagent.Config{ Name: "smart_assistant", Model: model, Description: "An intelligent assistant with internet and API access", Instruction: `You are a smart, helpful assistant with access to multiple information sources. AVAILABLE TOOLS: - google_search: Search the web for any information - fetch_url: Read the full content of a specific webpage - get_weather: Get current weather for any location - get_news: Get recent news articles on any topic - remember_fact: Store important information about the user - recall_facts: Retrieve stored information about the user BEHAVIOR GUIDELINES: 1. **Be proactive**: If a question might benefit from current data, search for it 2. **Combine sources**: Use multiple tools together when helpful 3. **Cite sources**: When providing factual information, mention where it came from 4. **Remember context**: Use remember_fact to store user preferences and important info 5. **Be conversational**: Maintain a friendly, helpful tone 6. **Handle errors gracefully**: If a tool fails, try alternatives or explain the limitation EXAMPLES OF GOOD BEHAVIOR: - User asks "What's the weather?" → Ask for location, then use get_weather - User asks about news → Use get_news, summarize, offer to read full articles - User mentions their city → Remember it for future weather/news queries - User asks about something current → Search first, don't rely on training data User's stored preferences: {user:preferences} User's location (if known): {user:location}`, Tools: tools, }) if err != nil { log.Fatal(err) } l := full.NewLauncher() cfg := &adk.Config{AgentLoader: services.NewSingleAgentLoader(chatbot)} if err := l.Execute(ctx, cfg, os.Args[1:]); err != nil { log.Fatal(err) } } func createTools() ([]tool.Tool, error) { var tools []tool.Tool // Google Search (built-in) tools = append(tools, geminitool.GoogleSearch{}) // URL Fetcher fetchTool, err := functiontool.New( functiontool.Config{ Name: "fetch_url", Description: "Fetch and read the content of a webpage. Use after search to get full article content.", }, fetchURL, ) if err != nil { return nil, err } tools = append(tools, fetchTool) // Weather weatherTool, err := functiontool.New( functiontool.Config{ Name: "get_weather", Description: "Get current weather conditions for a location", }, getWeather, ) if err != nil { return nil, err } tools = append(tools, weatherTool) // News newsTool, err := functiontool.New( functiontool.Config{ Name: "get_news", Description: "Get recent news articles about a topic", }, getNews, ) if err != nil { return nil, err } tools = append(tools, newsTool) // Memory tools rememberTool, err := functiontool.New( functiontool.Config{ Name: "remember_fact", Description: "Store an important fact about the user for future reference", }, rememberFact, ) if err != nil { return nil, err } tools = append(tools, rememberTool) recallTool, err := functiontool.New( functiontool.Config{ Name: "recall_facts", Description: "Retrieve all stored facts about the user", }, recallFacts, ) if err != nil { return nil, err } tools = append(tools, recallTool) return tools, nil } // Memory tools type RememberFactInput struct { Category string `json:"category" jsonschema:"Category: preference, location, name, interest, other"` Fact string `json:"fact" jsonschema:"The fact to remember"` } type RememberFactOutput struct { Message string `json:"message"` } func rememberFact(ctx tool.Context, input RememberFactInput) RememberFactOutput { state := ctx.Session().State() // Get existing facts var facts map[string][]string if existing := state.Get("user:facts"); existing != nil { if f, ok := existing.(map[string][]string); ok { facts = f } } if facts == nil { facts = make(map[string][]string) } // Add new fact facts[input.Category] = append(facts[input.Category], input.Fact) state.Set("user:facts", facts) // Also set specific keys for common categories if input.Category == "location" { state.Set("user:location", input.Fact) } return RememberFactOutput{ Message: fmt.Sprintf("Remembered: %s (%s)", input.Fact, input.Category), } } type RecallFactsInput struct { Category string `json:"category,omitempty" jsonschema:"Optional: filter by category"` } type RecallFactsOutput struct { Facts map[string][]string `json:"facts"` } func recallFacts(ctx tool.Context, input RecallFactsInput) RecallFactsOutput { state := ctx.Session().State() var facts map[string][]string if existing := state.Get("user:facts"); existing != nil { if f, ok := existing.(map[string][]string); ok { facts = f } } if facts == nil { return RecallFactsOutput{Facts: map[string][]string{}} } if input.Category != "" { return RecallFactsOutput{ Facts: map[string][]string{input.Category: facts[input.Category]}, } } return RecallFactsOutput{Facts: facts} } // Include fetchURL, getWeather, getNews from earlier... ``` --- ## Adding Proactive Search Behavior Use callbacks to automatically search for time-sensitive queries: ```go import "google.golang.org/adk/model" chatbot, err := llmagent.New(llmagent.Config{ // ... other config ... BeforeModelCallback: func(ctx agent.CallbackContext, req *model.LLMRequest) (*model.LLMRequest, error) { // Log all requests for debugging log.Printf("Model request with %d messages", len(req.Messages)) return req, nil }, AfterToolCallback: func(ctx agent.CallbackContext, call *tool.Call, result *tool.Result) (*tool.Result, error) { // Log tool usage log.Printf("Tool %s completed: %v", call.Name, result.Success) // Track tool usage in session for analytics state := ctx.Session().State() count := 0 if c := state.Get("temp:tool_count"); c != nil { count = c.(int) } state.Set("temp:tool_count", count+1) return result, nil }, }) ``` --- ## Running and Testing ```bash # Set API keys export GOOGLE_API_KEY="your-key" export OPENWEATHER_API_KEY="your-key" # Optional export NEWS_API_KEY="your-key" # Optional # Run in console go run main.go # Example conversations: # > What's the weather in Tokyo? # > Tell me the latest news about AI # > I live in San Francisco (bot remembers this) # > What's the weather? (uses remembered location) # > Search for the best restaurants near me # > Read that first article for me (fetches full content) # Run with web UI for debugging go run main.go web api webui ``` --- ## Best Practices 1. **Rate limit external APIs**: Add throttling to prevent abuse 2. **Cache responses**: Store recent search results to reduce API calls 3. **Handle failures gracefully**: If one tool fails, the bot should try alternatives 4. **Be transparent**: Tell users when information comes from search vs. training 5. **Respect user privacy**: Don't store sensitive information; let users clear memory 6. **Set reasonable timeouts**: External APIs can be slow; don't hang forever