From 0d6bc325d51a22b199e6033f43e6604a968b9318 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 2 Aug 2026 11:13:39 +0000 Subject: [PATCH] service: name services for domains, methods for what they do MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit search_search was not a naming rule that needed an exception, it was a service named for an action. Every other service is a noun — news, mail, places, web — so service_method reads as two different words. "search" was a verb, which left its one method nothing to be called but Search. Web search moves to the web service, next to web.Fetch, giving web_search and web_fetch. Those match the routes that already said web (/web/fetch, /web/read) and the tool name web_fetch has always carried. The /search page and the "Search" nav label stay exactly as they are: the label is what a person looks for, the service name is what a caller addresses. The search package keeps the Brave provider, the reader and the page; it just registers no service of its own. Methods returning the current set of something are now all List — news.List, blog.List, social.List, video.List, markets.List — joining stream.List, events.List and db.List. They were Headlines, Recent, Feed, Latest and Prices, five words for one idea. The derived names land on news_list, blog_list, social_list, video_list and markets_list, which are the names those tools already ship under. All 37 derived names are now service_method with no exceptions. Two tests hold it: no method may repeat its service, and no two endpoints may derive the same name. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01KdcPjN9ndJwMGKQSRrAGPE --- CLAUDE.md | 9 ++++- agent/guest.go | 7 ++-- agent/native.go | 16 ++++---- docs/SERVICE_REGISTRY.md | 25 +++++++++--- internal/service/naming_test.go | 68 +++++++++++++++++++++++++++++++++ main.go | 36 ++++++++--------- service/blog/service.go | 14 +++---- service/markets/service.go | 14 +++---- service/markets/service_test.go | 6 +-- service/news/service.go | 18 ++++----- service/news/service_test.go | 6 +-- service/search/search.go | 11 ++---- service/search/service.go | 32 ---------------- service/social/service.go | 14 +++---- service/video/service.go | 14 +++---- service/web/service.go | 43 ++++++++++++++++----- 16 files changed, 205 insertions(+), 128 deletions(-) create mode 100644 internal/service/naming_test.go delete mode 100644 service/search/service.go diff --git a/CLAUDE.md b/CLAUDE.md index 555f0d57..5cbddd31 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -33,9 +33,9 @@ A personal home server. News, mail, search, weather, markets, video — the ever | `client/telegram/` | Telegram bot with commands and groups | | `client/whatsapp/` | WhatsApp Business API integration | | `service/wallet/` | Credit system, Stripe, x402 | -| `service/search/` | Brave web search, readability reader | +| `service/search/` | Brave provider, readability reader, the /search page (no service of its own) | | `service/db/` | Per-user records for services and apps (headless) | -| `service/web/` | Fetch a URL and return readable content (headless) | +| `service/web/` | The open web: search it (`web.Search`), fetch a URL (`web.Fetch`) | | `service/index/` | Search across the caller's own content (headless) | | `service/stream/` | The console — this instance's own event timeline | | `service/chat/` | Live discussion rooms attached to an item | @@ -62,6 +62,11 @@ go vet ./... # vet is the runtime core that hosts them, not a service itself. See `docs/SERVICE_REGISTRY.md` for what is registered, which are headless, which are account-scoped, and which are deliberately not exposed to the agent +- A service is named for a **domain** (a noun), never an action. Tool names are + derived as `service_method`, so an action-named service leaves its main method + nothing to be called but the same word — that is how `search.Search` produced + the tool name `search_search`. Methods returning the current set of something + are all called `List`. Enforced by `TestNoMethodRepeatsItsService` ## The go-micro relationship diff --git a/agent/guest.go b/agent/guest.go index 4856efa6..63018a33 100644 --- a/agent/guest.go +++ b/agent/guest.go @@ -18,10 +18,9 @@ var guestAllowedTools = map[string]bool{ "weather_forecast": true, "video": true, "video_search": true, - "web_search": true, // legacy alias of search_web - "search_web": true, - "web_fetch": true, // legacy alias of search_fetch - "search_fetch": true, + "web_search": true, + "search_web": true, // legacy name for web_search + "web_fetch": true, "social": true, "social_search": true, "blog_list": true, diff --git a/agent/native.go b/agent/native.go index ec7d09f6..1377198a 100644 --- a/agent/native.go +++ b/agent/native.go @@ -95,7 +95,7 @@ var agentToolLabels = map[string]string{ "social": "Social", "video": "Video", "blog": "Blog", - "search": "Search", + "web": "Search", "places": "Places", "index": "Index", "apps": "Apps", @@ -103,7 +103,6 @@ var agentToolLabels = map[string]string{ "images": "Images", "islam": "Islam", "events": "Events", - "web": "Web", "chat": "Chat", "stream": "Stream", "db": "Storage", @@ -458,12 +457,15 @@ func nativeToolFormatterName(name string) string { switch method { case "search": return "news_search" - case "headlines": - return "news_headlines" + case "list": + return "news_list" default: return "news" } - case "search": + case "web": + if method == "fetch" { + return "web_fetch" + } return "web_search" } } @@ -492,7 +494,7 @@ func nativeToolTitle(name string) string { return "video" case "blog": return "blog" - case "search": + case "web": return "search" case "recall", "index": return "memory" @@ -531,7 +533,7 @@ func nativeToolLabel(name string) (label string, show bool) { return "📺 Finding videos", true case "blog": return "📝 Reading the blog", true - case "search": + case "web": return "🔎 Searching the web", true case "recall", "index": return "🧠 Recalling your data", true diff --git a/docs/SERVICE_REGISTRY.md b/docs/SERVICE_REGISTRY.md index 6961a3e4..136b2eea 100644 --- a/docs/SERVICE_REGISTRY.md +++ b/docs/SERVICE_REGISTRY.md @@ -22,12 +22,28 @@ Services live under `service//`. `internal/service` is the runtime core that hosts them — it is not itself a service. The exception is a **headless** service: a capability with no page, so no route -and no nav entry. `index`, `web` and `db` are headless — they exist for the +and no nav entry. `index` and `db` are headless — they exist for the agent, for apps and for other services to call. Two footnotes. `wallet` has a page that predates its service; both are the same -capability with two surfaces. And `/web` still 301s to `/search` for old links, -from when fetching lived inside search — the `web` service itself has no page. +capability with two surfaces. And the `web` service is reached at `/search`, +because "Search" is what a person looks for in the sidebar while `web` is what +the capability is about. The nav label is for humans; the service name is for +callers. `/web` still 301s to `/search` for old links. + +**A service is named for a domain, not for an action.** Every one is a noun — +`news`, `mail`, `places`, `web` — and its methods say what to do with that +domain. This is not style: tool names are derived as `service_method`, so a +service named for an action leaves its main method nothing to be called but the +same word. `search` used to be a service, its one method had to be `Search`, and +the derived tool name was `search_search`. Web search is now `web.Search` +alongside `web.Fetch`, matching the `/web/fetch` and `/web/read` routes. +`TestNoMethodRepeatsItsService` holds the line. + +Methods that return the current set of something are all called `List` — +`news.List`, `blog.List`, `social.List`, `video.List`, `markets.List`, +`stream.List`, `events.List`, `db.List` — so the derived names are uniform and +guessable. ## What is registered @@ -45,13 +61,12 @@ from when fetching lived inside search — the `web` service itself has no page. | `markets` | /markets | ✅ | | Crypto, futures, commodities, currencies | | `news` | /news | ✅ | | RSS aggregation, sentiment, search | | `places` | /places | ✅ | | Maps and points of interest | -| `search` | /search | ✅ | | Web search | | `social` | /social | ✅ | | Threads, replies, status | | `stream` | /stream | ✅ | | The console: this instance's own timeline | | `video` | /video | ✅ | | Search and playback | | `wallet` | /wallet | ✅ | ✅ | Credit check, charge, balance | | `weather` | /weather | ✅ | | Forecast and pollen | -| `web` | — | ✅ | | Fetch a URL, return readable content | +| `web` | /search | ✅ | | Search the web; fetch a URL and return readable content | ## Account-scoped diff --git a/internal/service/naming_test.go b/internal/service/naming_test.go new file mode 100644 index 00000000..19546580 --- /dev/null +++ b/internal/service/naming_test.go @@ -0,0 +1,68 @@ +package service + +import ( + "path/filepath" + "strings" + "testing" +) + +// A tool's name is derived, not written: service + "_" + method. That only +// reads well if the two halves say different things, which holds when a +// service is named for a domain (news, mail, places) and a method for what it +// does with that domain. +// +// A service named for an action has nowhere left to go: its main method has to +// repeat it. That is how "search" ended up with search.Search, deriving the +// tool name search_search. The capability moved to web.Search, and this test +// stops the next one arriving. +func TestNoMethodRepeatsItsService(t *testing.T) { + forEachService(t, func(svc string, methods []string) { + for _, m := range methods { + if strings.EqualFold(m, svc) { + t.Errorf("%s.%s derives the tool name %s_%s — name the service for a "+ + "domain and the method for what it does", + svc, m, svc, strings.ToLower(m)) + } + } + }) +} + +// Two endpoints deriving the same tool name would make one of them +// unreachable, silently. +func TestDerivedToolNamesAreUnique(t *testing.T) { + seen := map[string]string{} + forEachService(t, func(svc string, methods []string) { + for _, m := range methods { + name := svc + "_" + strings.ToLower(m) + if prev, dup := seen[name]; dup { + t.Errorf("%s.%s and %s both derive %q", svc, m, prev, name) + continue + } + seen[name] = svc + "." + m + } + }) +} + +// forEachService walks the service packages, handing each one its name and the +// RPC methods it declares. The directory name is the service name — that is +// the convention, and SERVICE_REGISTRY.md documents it. +func forEachService(t *testing.T, fn func(service string, methods []string)) { + t.Helper() + root := repoRoot(t) + dirs, err := filepath.Glob(filepath.Join(root, "service", "*")) + if err != nil { + t.Fatalf("glob: %v", err) + } + checked := 0 + for _, dir := range dirs { + methods, _, ok := scanService(t, dir) + if !ok { + continue // a package with no handler, e.g. search + } + checked++ + fn(filepath.Base(dir), methods) + } + if checked < 15 { + t.Fatalf("only scanned %d services; the scan is not finding them", checked) + } +} diff --git a/main.go b/main.go index ac57cbc9..788007f1 100644 --- a/main.go +++ b/main.go @@ -578,9 +578,9 @@ func main() { }, Handle: func(args map[string]any) (string, error) { q, _ := args["q"].(string) - var rsp search.SearchResponse - if err := service.Call(context.Background(), "search", "Server.Search", - &search.SearchRequest{Query: q}, &rsp); err != nil { + var rsp web.SearchResponse + if err := service.Call(context.Background(), "web", "Server.Search", + &web.SearchRequest{Query: q}, &rsp); err != nil { return "", err } return rsp.Text, nil @@ -618,9 +618,9 @@ func main() { case string: fmt.Sscanf(v, "%d", &limit) } - var rsp news.HeadlinesResponse - if err := service.Call(context.Background(), "news", "Server.Headlines", - &news.HeadlinesRequest{Topic: topic, Limit: limit}, &rsp); err != nil { + var rsp news.ListResponse + if err := service.Call(context.Background(), "news", "Server.List", + &news.ListRequest{Topic: topic, Limit: limit}, &rsp); err != nil { return "", err } return rsp.Text, nil @@ -787,9 +787,9 @@ func main() { }, Handle: func(args map[string]any) (string, error) { category, _ := args["category"].(string) - var rsp markets.PricesResponse - if err := service.Call(context.Background(), "markets", "Server.Prices", - &markets.PricesRequest{Category: category}, &rsp); err != nil { + var rsp markets.ListResponse + if err := service.Call(context.Background(), "markets", "Server.List", + &markets.ListRequest{Category: category}, &rsp); err != nil { return "", err } return rsp.Text, nil @@ -976,9 +976,9 @@ func main() { Aliases: []string{"social"}, Description: "Get the latest social posts from the network.", Handle: func(args map[string]any) (string, error) { - var rsp social.FeedResponse - if err := service.Call(context.Background(), "social", "Server.Feed", - &social.FeedRequest{}, &rsp); err != nil { + var rsp social.ListResponse + if err := service.Call(context.Background(), "social", "Server.List", + &social.ListRequest{}, &rsp); err != nil { return "", err } return rsp.Text, nil @@ -991,9 +991,9 @@ func main() { Aliases: []string{"video"}, Description: "Get the latest videos from curated channels.", Handle: func(args map[string]any) (string, error) { - var rsp video.LatestResponse - if err := service.Call(context.Background(), "video", "Server.Latest", - &video.LatestRequest{}, &rsp); err != nil { + var rsp video.ListResponse + if err := service.Call(context.Background(), "video", "Server.List", + &video.ListRequest{}, &rsp); err != nil { return "", err } return rsp.Text, nil @@ -1005,9 +1005,9 @@ func main() { Name: "blog_list", Description: "Get recent blog posts (titles, snippets and ids; use blog_read for one in full).", Handle: func(args map[string]any) (string, error) { - var rsp blog.RecentResponse - if err := service.Call(context.Background(), "blog", "Server.Recent", - &blog.RecentRequest{}, &rsp); err != nil { + var rsp blog.ListResponse + if err := service.Call(context.Background(), "blog", "Server.List", + &blog.ListRequest{}, &rsp); err != nil { return "", err } return rsp.Text, nil diff --git a/service/blog/service.go b/service/blog/service.go index 051579c4..75e824f3 100644 --- a/service/blog/service.go +++ b/service/blog/service.go @@ -9,23 +9,23 @@ import ( // Server is the go-micro service handler for blog. type Server struct{} -// RecentRequest controls how many posts to return. -type RecentRequest struct { +// ListRequest controls how many posts to return. +type ListRequest struct { Limit int `json:"limit" description:"Optional max number of posts (default all recent)"` } -// RecentResponse is a model-ready list of recent posts. -type RecentResponse struct { +// ListResponse is a model-ready list of recent posts. +type ListResponse struct { Text string `json:"text" description:"Recent blog posts: titles, snippets and ids"` } -// Recent returns recent blog posts (titles, snippets and ids). +// List returns recent blog posts (titles, snippets and ids). // @example {} -func (Server) Recent(_ context.Context, req *RecentRequest, rsp *RecentResponse) error { +func (Server) List(_ context.Context, req *ListRequest, rsp *ListResponse) error { rsp.Text = RecentText(req.Limit) return nil } var toolDocs = service.Docs{ - "Recent": "Read recent blog posts — titles, snippets and ids", + "List": "Read recent blog posts — titles, snippets and ids", } diff --git a/service/markets/service.go b/service/markets/service.go index 8a1a4b41..2e0642d7 100644 --- a/service/markets/service.go +++ b/service/markets/service.go @@ -10,24 +10,24 @@ import ( // as RPC endpoints and, through the agent and gateways, as AI tools. type Server struct{} -// PricesRequest selects a market category. -type PricesRequest struct { +// ListRequest selects a market category. +type ListRequest struct { Category string `json:"category" description:"crypto, futures, commodities or currencies (default crypto)"` } -// PricesResponse is a model-ready price summary. -type PricesResponse struct { +// ListResponse is a model-ready price summary. +type ListResponse struct { Text string `json:"text" description:"Live prices for the requested category"` } -// Prices returns live market prices for cryptocurrencies, futures, commodities +// List returns live market prices for cryptocurrencies, futures, commodities // and currencies. // @example {"category": "crypto"} -func (Server) Prices(_ context.Context, req *PricesRequest, rsp *PricesResponse) error { +func (Server) List(_ context.Context, req *ListRequest, rsp *ListResponse) error { rsp.Text = MarketsText(req.Category) return nil } var toolDocs = service.Docs{ - "Prices": "Get live prices for cryptocurrencies, futures, commodities and currencies", + "List": "Get live prices for cryptocurrencies, futures, commodities and currencies", } diff --git a/service/markets/service_test.go b/service/markets/service_test.go index acf0b233..6c9b9683 100644 --- a/service/markets/service_test.go +++ b/service/markets/service_test.go @@ -12,9 +12,9 @@ func TestMarketsViaMesh(t *testing.T) { if err := service.Register("markets", new(Server)); err != nil { t.Fatalf("register: %v", err) } - var rsp PricesResponse - if err := service.Call(context.Background(), "markets", "Server.Prices", - &PricesRequest{Category: "crypto"}, &rsp); err != nil { + var rsp ListResponse + if err := service.Call(context.Background(), "markets", "Server.List", + &ListRequest{Category: "crypto"}, &rsp); err != nil { t.Fatalf("call: %v", err) } } diff --git a/service/news/service.go b/service/news/service.go index 6bbf2522..0ca435c4 100644 --- a/service/news/service.go +++ b/service/news/service.go @@ -10,21 +10,21 @@ import ( // RPC endpoints and, through the agent and gateways, as AI tools. type Server struct{} -// HeadlinesRequest filters the headline list. -type HeadlinesRequest struct { +// ListRequest filters the headline list. +type ListRequest struct { Topic string `json:"topic" description:"Optional topic/category filter (e.g. tech, world, business)"` Limit int `json:"limit" description:"Optional max number of headlines (default 30)"` } -// HeadlinesResponse is a model-ready list of headlines. -type HeadlinesResponse struct { +// ListResponse is a model-ready list of headlines. +type ListResponse struct { Text string `json:"text" description:"Recent headlines with short summaries, balanced across topics"` } -// Headlines returns recent news headlines with short summaries, balanced across +// List returns recent news headlines with short summaries, balanced across // topics (not dominated by one topic like crypto). // @example {"topic": "tech"} -func (Server) Headlines(_ context.Context, req *HeadlinesRequest, rsp *HeadlinesResponse) error { +func (Server) List(_ context.Context, req *ListRequest, rsp *ListResponse) error { rsp.Text = HeadlinesText(req.Topic, req.Limit) return nil } @@ -68,7 +68,7 @@ func (Server) Search(_ context.Context, req *SearchRequest, rsp *SearchResponse) } var toolDocs = service.Docs{ - "Headlines": "Read recent news headlines with short summaries, balanced across topics", - "Read": "Read one news article in full by its id or URL", - "Search": "Search indexed and live news for a topic", + "List": "Read recent news headlines with short summaries, balanced across topics", + "Read": "Read one news article in full by its id or URL", + "Search": "Search indexed and live news for a topic", } diff --git a/service/news/service_test.go b/service/news/service_test.go index a6eb8a6a..b62f5a77 100644 --- a/service/news/service_test.go +++ b/service/news/service_test.go @@ -13,9 +13,9 @@ func TestNewsViaMesh(t *testing.T) { if err := service.Register("news", new(Server)); err != nil { t.Fatalf("register: %v", err) } - var rsp HeadlinesResponse - if err := service.Call(context.Background(), "news", "Server.Headlines", - &HeadlinesRequest{Limit: 5}, &rsp); err != nil { + var rsp ListResponse + if err := service.Call(context.Background(), "news", "Server.List", + &ListRequest{Limit: 5}, &rsp); err != nil { t.Fatalf("call: %v", err) } // Text may be empty without feeds loaded; the round-trip is what matters. diff --git a/service/search/search.go b/service/search/search.go index e834830d..ab0166bd 100644 --- a/service/search/search.go +++ b/service/search/search.go @@ -16,16 +16,13 @@ import ( "mu/internal/app" "mu/internal/auth" "mu/internal/data" - "mu/internal/service" "mu/service/wallet" ) -// Load initializes the search building block. -func Load() { - if err := service.Register("search", new(Server), toolDocs); err != nil { - app.Log("search", "service register failed: %v", err) - } -} +// Load initializes the search package. It registers no service of its own: +// the web search capability is web.Search, and this package provides the +// provider, the reader and the /search page that sit around it. +func Load() {} // BraveResult represents a single result from the Brave Search API type BraveResult struct { diff --git a/service/search/service.go b/service/search/service.go deleted file mode 100644 index c71018fc..00000000 --- a/service/search/service.go +++ /dev/null @@ -1,32 +0,0 @@ -package search - -import ( - "context" - - "mu/internal/service" -) - -// Server is the go-micro service handler for web search. -type Server struct{} - -// SearchRequest is a web search query. -type SearchRequest struct { - Query string `json:"query" description:"Search query"` - Limit int `json:"limit" description:"Optional max number of results"` -} - -// SearchResponse is a model-ready set of results. -type SearchResponse struct { - Text string `json:"text" description:"Search results for the query"` -} - -// Search searches the web for current information and news. -// @example {"query": "latest AI news"} -func (Server) Search(_ context.Context, req *SearchRequest, rsp *SearchResponse) error { - rsp.Text = WebSearchText(req.Query, req.Limit) - return nil -} - -var toolDocs = service.Docs{ - "Search": "Search the web for current information and news", -} diff --git a/service/social/service.go b/service/social/service.go index a8c045ca..cfc0d4fb 100644 --- a/service/social/service.go +++ b/service/social/service.go @@ -9,23 +9,23 @@ import ( // Server is the go-micro service handler for social. type Server struct{} -// FeedRequest controls how many posts to return. -type FeedRequest struct { +// ListRequest controls how many posts to return. +type ListRequest struct { Limit int `json:"limit" description:"Optional max number of posts (default all recent)"` } -// FeedResponse is a model-ready social feed. -type FeedResponse struct { +// ListResponse is a model-ready social feed. +type ListResponse struct { Text string `json:"text" description:"Latest social posts from the network"` } -// Feed returns the latest social posts from the network. +// List returns the latest social posts from the network. // @example {} -func (Server) Feed(_ context.Context, req *FeedRequest, rsp *FeedResponse) error { +func (Server) List(_ context.Context, req *ListRequest, rsp *ListResponse) error { rsp.Text = FeedText(req.Limit) return nil } var toolDocs = service.Docs{ - "Feed": "Read the latest social posts from the network", + "List": "Read the latest social posts from the network", } diff --git a/service/video/service.go b/service/video/service.go index a075f853..9d9a6286 100644 --- a/service/video/service.go +++ b/service/video/service.go @@ -9,23 +9,23 @@ import ( // Server is the go-micro service handler for video. type Server struct{} -// LatestRequest controls how many videos to return. -type LatestRequest struct { +// ListRequest controls how many videos to return. +type ListRequest struct { Limit int `json:"limit" description:"Optional max number of videos (default all recent)"` } -// LatestResponse is a model-ready video list. -type LatestResponse struct { +// ListResponse is a model-ready video list. +type ListResponse struct { Text string `json:"text" description:"Latest videos from curated channels"` } -// Latest returns the latest videos from curated channels. +// List returns the latest videos from curated channels. // @example {} -func (Server) Latest(_ context.Context, req *LatestRequest, rsp *LatestResponse) error { +func (Server) List(_ context.Context, req *ListRequest, rsp *ListResponse) error { rsp.Text = LatestText(req.Limit) return nil } var toolDocs = service.Docs{ - "Latest": "Read the latest videos from curated channels", + "List": "Read the latest videos from curated channels", } diff --git a/service/web/service.go b/service/web/service.go index 1c1e9124..c2e3ed0e 100644 --- a/service/web/service.go +++ b/service/web/service.go @@ -1,14 +1,18 @@ -// Package web is the read-a-URL capability: fetch a page and return its -// cleaned, readable content. +// Package web is the open web as a capability: search it, and read a page from +// it. Two jobs, one domain — the same way news is Headlines + Read + Search and +// places is Search + Nearby + Geocode. A service groups by what it is about, +// not by how many things it does. // -// It is a service in its own right rather than a method on search because the -// two are different jobs — search queries a paid index, web reads a page you -// already have the address of — and because everything else about this -// capability was already called "web": the routes are /web/fetch and -// /web/preview, and the tool has always carried the web_fetch name. Only the -// service disagreed. +// This is where web search lives too, rather than in a service of its own +// called "search". Every other service is named for a thing — news, mail, +// markets, places — and "search" was named for an action, which is why its one +// method had to be called Search: service.Method degenerated to search.Search, +// and the tool name to search_search. Naming the domain fixes it at the source +// and gives web_search alongside web_fetch, matching the routes (/web/fetch, +// /web/read) that already said "web". // -// Headless, like index: a capability with no page of its own. +// Headless, like index: the /search page is a surface over this service, not a +// service of its own. package web import ( @@ -23,6 +27,24 @@ import ( // through the agent and gateways, as AI tools. type Server struct{} +// SearchRequest is a web search query. +type SearchRequest struct { + Query string `json:"query" description:"Search query"` + Limit int `json:"limit" description:"Optional max number of results"` +} + +// SearchResponse is a model-ready set of results. +type SearchResponse struct { + Text string `json:"text" description:"Search results for the query"` +} + +// Search searches the web for current information and news. +// @example {"query": "latest AI news"} +func (Server) Search(_ context.Context, req *SearchRequest, rsp *SearchResponse) error { + rsp.Text = search.WebSearchText(req.Query, req.Limit) + return nil +} + // FetchRequest names the page to read. type FetchRequest struct { URL string `json:"url" description:"The URL to fetch"` @@ -60,5 +82,6 @@ func Load() { } var toolDocs = service.Docs{ - "Fetch": "Fetch a web page by URL and return its readable content", + "Search": "Search the web for current information and news", + "Fetch": "Fetch a web page by URL and return its readable content", }