# 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
