diff --git a/api/client.go b/api/client.go index 0d4c97ba9..bd74f7acd 100644 --- a/api/client.go +++ b/api/client.go @@ -370,6 +370,15 @@ func (c *Client) ListRunning(ctx context.Context) (*ProcessResponse, error) { return &lr, nil } +// WebSearch performs a web search using the ollama service. +func (c *Client) WebSearch(ctx context.Context, req *SearchRequest) (*SearchResponse, error) { + var sr SearchResponse + if err := c.do(ctx, http.MethodPost, "/api/web_search", req, &sr); err != nil { + return nil, err + } + return &sr, nil +} + // Copy copies a model - creating a model with another name from an existing // model. func (c *Client) Copy(ctx context.Context, req *CopyRequest) error { diff --git a/api/types.go b/api/types.go index 8cc7752ca..909277b94 100644 --- a/api/types.go +++ b/api/types.go @@ -1058,3 +1058,19 @@ func FormatParams(params map[string][]string) (map[string]any, error) { return out, nil } + +// Web search types +type SearchRequest struct { + Query string `json:"query"` + MaxResults int `json:"max_results,omitempty"` +} + +type SearchResult struct { + Title string `json:"title"` + URL string `json:"url"` + Content string `json:"content"` +} + +type SearchResponse struct { + Results []SearchResult `json:"results"` +} diff --git a/server/routes.go b/server/routes.go index 21a1b2b3d..85c0fa7fa 100644 --- a/server/routes.go +++ b/server/routes.go @@ -1227,6 +1227,59 @@ func (s *Server) CopyHandler(c *gin.Context) { } } +func (s *Server) WebSearchHandler(c *gin.Context) { + var req api.SearchRequest + if err := c.ShouldBindJSON(&req); errors.Is(err, io.EOF) { + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "missing request body"}) + return + } else if err != nil { + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + if req.Query == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "query is required"}) + return + } + + if req.MaxResults <= 0 { + req.MaxResults = 5 + } + + + results, err := s.callWebSearchAPI(req.Query, req.MaxResults) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + if len(results) > req.MaxResults { + results = results[:req.MaxResults] + } + + resp := api.SearchResponse{ + Results: results, + } + + c.JSON(http.StatusOK, resp) +} + +func (s *Server) callWebSearchAPI(query string, maxResults int) ([]api.SearchResult, error) { + searchReq := api.SearchRequest{ + Query: query, + MaxResults: maxResults, + } + + client := api.NewClient(&url.URL{Scheme: "https", Host: "ollama.com"}, http.DefaultClient) + + searchResp, err := client.WebSearch(context.Background(), &searchReq) + if err != nil { + return nil, err + } + + return searchResp.Results, nil +} + func (s *Server) HeadBlobHandler(c *gin.Context) { path, err := GetBlobsPath(c.Param("digest")) if err != nil { @@ -1447,6 +1500,7 @@ func (s *Server) GenerateRoutes(rc *ollama.Registry) (http.Handler, error) { r.POST("/api/chat", s.ChatHandler) r.POST("/api/embed", s.EmbedHandler) r.POST("/api/embeddings", s.EmbeddingsHandler) + r.POST("/api/web_search", s.WebSearchHandler) // Inference (OpenAI compatibility) r.POST("/v1/chat/completions", openai.ChatMiddleware(), s.ChatHandler)