├── constants ├── api.go └── apiVersion.go ├── go.mod ├── types └── error.go ├── go.sum ├── LICENSE ├── README.md ├── fastgpt.go ├── fastgpt_test.go ├── universalSummarizer.go ├── universalSummarizer_test.go ├── enrichment.go ├── enrichment_test.go └── client.go /constants/api.go: -------------------------------------------------------------------------------- 1 | package constants 2 | 3 | const ( 4 | BASE_URL = "https://kagi.com/api" 5 | ) 6 | -------------------------------------------------------------------------------- /constants/apiVersion.go: -------------------------------------------------------------------------------- 1 | package constants 2 | 3 | type ApiVersion string 4 | 5 | const ( 6 | ApiVersionV0 ApiVersion = "v0" 7 | CurrentApiVersion ApiVersion = ApiVersionV0 8 | ) 9 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/httpjamesm/kagigo 2 | 3 | go 1.19 4 | 5 | require github.com/go-resty/resty/v2 v2.7.0 6 | 7 | require golang.org/x/net v0.0.0-20211029224645-99673261e6eb // indirect 8 | -------------------------------------------------------------------------------- /types/error.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | type Error struct { 4 | Code int `json:"code"` 5 | Msg string `json:"msg"` 6 | Ref interface{} `json:"ref"` 7 | } 8 | 9 | type ErrorResponse struct { 10 | Meta interface{} `json:"meta"` 11 | Data interface{} `json:"data"` 12 | Error []Error `json:"error"` 13 | } 14 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/go-resty/resty/v2 v2.7.0 h1:me+K9p3uhSmXtrBZ4k9jcEAfJmuC8IivWHwaLZwPrFY= 2 | github.com/go-resty/resty/v2 v2.7.0/go.mod h1:9PWDzw47qPphMRFfhsyk0NnSgvluHcljSMVIq3w7q0I= 3 | golang.org/x/net v0.0.0-20211029224645-99673261e6eb h1:pirldcYWx7rx7kE5r+9WsOXPXK0+WH5+uZ7uPmJ44uM= 4 | golang.org/x/net v0.0.0-20211029224645-99673261e6eb/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 5 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 6 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 7 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 8 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 9 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 httpjamesm 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # kagigo 2 | 3 | An unofficial [Kagi API](https://help.kagi.com/kagi/api/overview.html) client for Go. 4 | 5 | ## Installation 6 | 7 | ```bash 8 | go get -u github.com/httpjamesm/kagigo 9 | ``` 10 | 11 | ## Quick Start 12 | 13 | ### Client 14 | 15 | ```go 16 | client := kagi.NewClient(&kagi.ClientConfig{ 17 | APIKey: os.Getenv("KAGI_API_KEY"), 18 | APIVersion: "v0", 19 | }) 20 | ``` 21 | ### FastGPT 22 | 23 | ```go 24 | response, err := client.FastGPTCompletion(kagi.FastGPTCompletionParams{ 25 | Query: "query", 26 | WebSearch: true, 27 | Cache: true, 28 | }) 29 | if err != nil { 30 | fmt.Println(err) 31 | return 32 | } 33 | fmt.Println(response.Data.Output) 34 | ``` 35 | 36 | ### Universal Summarizer 37 | 38 | ```go 39 | response, err := client.UniversalSummarizerCompletion(kagi.UniversalSummarizerParams{ 40 | URL: "https://blog.kagi.com/security-audit", 41 | SummaryType: kagi.SummaryTypeSummary, 42 | Engine: kagi.SummaryEngineCecil, 43 | }) 44 | if err != nil { 45 | fmt.Println(err) 46 | return 47 | } 48 | fmt.Println(response.Data.Output) 49 | ``` 50 | 51 | ### Enrichment 52 | 53 | ```go 54 | response, err := client.EnrichmentCompletion(kagi.EndpointTypeWeb, kagi.EnrichmentParams{ 55 | Q: "kagi search", 56 | }) 57 | if err != nil { 58 | fmt.Println(err) 59 | return 60 | } 61 | fmt.Println(response.Data) 62 | ``` 63 | -------------------------------------------------------------------------------- /fastgpt.go: -------------------------------------------------------------------------------- 1 | package kagi 2 | 3 | import ( 4 | "fmt" 5 | "github.com/httpjamesm/kagigo/types" 6 | ) 7 | 8 | type FastGPTCompletionParams struct { 9 | Query string `json:"query"` 10 | WebSearch bool `json:"web_search"` 11 | Cache bool `json:"cache"` 12 | } 13 | 14 | type FastGPTCompletionResponse struct { 15 | Meta struct { 16 | ID string `json:"id"` 17 | Node string `json:"node"` 18 | Ms int `json:"ms"` 19 | APIBalance float64 `json:"api_balance"` 20 | } `json:"meta"` 21 | Data struct { 22 | Output string `json:"output"` 23 | Tokens int `json:"tokens"` 24 | References []struct { 25 | Title string `json:"title"` 26 | Snippet string `json:"snippet"` 27 | URL string `json:"url"` 28 | } `json:"references"` 29 | } `json:"data"` 30 | Errors []types.Error `json:"error"` 31 | } 32 | 33 | func (c *Client) FastGPTCompletion(params FastGPTCompletionParams) (res FastGPTCompletionResponse, err error) { 34 | if params.Query == "" { 35 | err = fmt.Errorf("query is required") 36 | return 37 | } 38 | 39 | err = c.SendRequest("POST", "/fastgpt", params, &res) 40 | if err != nil { 41 | return 42 | } 43 | 44 | if len(res.Errors) != 0 { 45 | errObj := res.Errors[0] 46 | err = fmt.Errorf("api returned error: %v", fmt.Sprintf("[code: %d, msg: %s, ref: %v]", errObj.Code, errObj.Msg, errObj.Ref)) 47 | return 48 | } 49 | 50 | return 51 | } 52 | -------------------------------------------------------------------------------- /fastgpt_test.go: -------------------------------------------------------------------------------- 1 | package kagi 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/httpjamesm/kagigo/constants" 8 | ) 9 | 10 | func TestFastGPTCompletion(t *testing.T) { 11 | apiToken := os.Getenv("KAGI_API_TOKEN") 12 | 13 | // Create a new test client 14 | client := NewClient(&ClientConfig{ 15 | APIKey: apiToken, 16 | APIVersion: constants.CurrentApiVersion, 17 | }) 18 | 19 | // Define test input parameters 20 | params := FastGPTCompletionParams{ 21 | Query: "test query", 22 | WebSearch: true, 23 | Cache: true, 24 | } 25 | 26 | // Call the function being tested 27 | res, err := client.FastGPTCompletion(params) 28 | 29 | // Check for errors 30 | if err != nil { 31 | t.Errorf("Unexpected error: %v", err) 32 | } 33 | 34 | // Check the response fields 35 | if res.Meta.ID == "" { 36 | t.Errorf("Expected non-empty ID, got '%s'", res.Meta.ID) 37 | } 38 | 39 | if res.Meta.Node == "" { 40 | t.Errorf("Expected non-empty Node, got '%s'", res.Meta.Node) 41 | } 42 | 43 | if res.Meta.Ms < 0 { 44 | t.Errorf("Expected positive Ms, got '%d'", res.Meta.Ms) 45 | } 46 | 47 | if res.Data.Output == "" { 48 | t.Errorf("Expected non-empty Output, got '%s'", res.Data.Output) 49 | } 50 | 51 | if res.Data.Tokens < 0 { 52 | t.Errorf("Expected positive Tokens, got '%d'", res.Data.Tokens) 53 | } 54 | 55 | params.Query = "" 56 | 57 | // Call the function being tested 58 | res, err = client.FastGPTCompletion(params) 59 | 60 | // Check for errors 61 | if err == nil { 62 | t.Errorf("Expected error, got nil") 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /universalSummarizer.go: -------------------------------------------------------------------------------- 1 | package kagi 2 | 3 | import ( 4 | "fmt" 5 | "github.com/httpjamesm/kagigo/types" 6 | ) 7 | 8 | type SummaryType string 9 | 10 | const ( 11 | SummaryTypeSummary SummaryType = "summary" 12 | SummaryTypeTakeaways SummaryType = "takeaway" 13 | ) 14 | 15 | type SummaryEngine string 16 | 17 | const ( 18 | SummaryEngineCecil SummaryEngine = "cecil" 19 | SummaryEngineAgnes SummaryEngine = "agnes" 20 | SummaryEngineDaphne SummaryEngine = "daphne" 21 | SummaryEngineMuriel SummaryEngine = "muriel" 22 | ) 23 | 24 | type UniversalSummarizerParams struct { 25 | URL string `json:"url"` 26 | SummaryType SummaryType `json:"summary_type"` 27 | Engine SummaryEngine `json:"engine"` 28 | } 29 | 30 | type UniversalSummarizerResponse struct { 31 | Meta struct { 32 | ID string `json:"id"` 33 | Node string `json:"node"` 34 | Ms int `json:"ms"` 35 | } `json:"meta"` 36 | Data struct { 37 | Output string `json:"output"` 38 | Tokens int `json:"tokens"` 39 | } `json:"data"` 40 | Errors []types.Error `json:"error"` 41 | } 42 | 43 | func (c *Client) UniversalSummarizerCompletion(params UniversalSummarizerParams) (res UniversalSummarizerResponse, err error) { 44 | if params.URL == "" { 45 | err = fmt.Errorf("url is required") 46 | return 47 | } 48 | 49 | err = c.SendRequest("POST", "/summarize", params, &res) 50 | if err != nil { 51 | return 52 | } 53 | 54 | if len(res.Errors) != 0 { 55 | errObj := res.Errors[0] 56 | err = fmt.Errorf("api returned error: %v", fmt.Sprintf("[code: %d, msg: %s, ref: %v]", errObj.Code, errObj.Msg, errObj.Ref)) 57 | return 58 | } 59 | 60 | return 61 | } 62 | -------------------------------------------------------------------------------- /universalSummarizer_test.go: -------------------------------------------------------------------------------- 1 | package kagi 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/httpjamesm/kagigo/constants" 8 | ) 9 | 10 | func TestUniversalSummarizerCompletion(t *testing.T) { 11 | apiToken := os.Getenv("KAGI_API_TOKEN") 12 | 13 | // Create a new test client 14 | client := NewClient(&ClientConfig{ 15 | APIKey: apiToken, 16 | APIVersion: constants.CurrentApiVersion, 17 | }) 18 | 19 | // Define test input parameters 20 | params := UniversalSummarizerParams{ 21 | URL: "https://blog.kagi.com/security-audit", 22 | SummaryType: SummaryTypeSummary, 23 | Engine: SummaryEngineCecil, 24 | } 25 | 26 | // Call the function being tested 27 | res, err := client.UniversalSummarizerCompletion(params) 28 | 29 | // Check for errors 30 | if err != nil { 31 | t.Errorf("Unexpected error: %v", err) 32 | } 33 | 34 | // Check the response fields 35 | if res.Meta.ID == "" { 36 | t.Errorf("Expected non-empty ID, got '%s'", res.Meta.ID) 37 | } 38 | 39 | if res.Meta.Node == "" { 40 | t.Errorf("Expected non-empty Node, got '%s'", res.Meta.Node) 41 | } 42 | 43 | if res.Meta.Ms < 0 { 44 | t.Errorf("Expected positive Ms, got '%d'", res.Meta.Ms) 45 | } 46 | 47 | if res.Data.Output == "" { 48 | t.Errorf("Expected non-empty Output, got '%s'", res.Data.Output) 49 | } 50 | 51 | if res.Data.Tokens < 0 { 52 | t.Errorf("Expected positive Tokens, got '%d'", res.Data.Tokens) 53 | } 54 | 55 | params.URL = "very-invalid-url" 56 | 57 | // Call the function being tested 58 | res, err = client.UniversalSummarizerCompletion(params) 59 | 60 | // Check for errors 61 | if err == nil { 62 | t.Errorf("Expected error, got nil") 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /enrichment.go: -------------------------------------------------------------------------------- 1 | package kagi 2 | 3 | import ( 4 | "fmt" 5 | "github.com/httpjamesm/kagigo/types" 6 | ) 7 | 8 | const ( 9 | EndpointTypeWeb string = "web" 10 | EndpointTypeNews string = "news" 11 | ) 12 | 13 | type EnrichmentParams struct { 14 | Q string `json:"q"` 15 | } 16 | 17 | type EnrichmentResponse struct { 18 | Meta struct { 19 | ID string `json:"id"` 20 | Node string `json:"node"` 21 | Ms int `json:"ms"` 22 | API float64 `json:"api_balance"` 23 | } `json:"meta"` 24 | Data []struct { 25 | T int `json:"t"` 26 | Rank int `json:"rank"` 27 | URL string `json:"url"` 28 | Title string `json:"title"` 29 | Snippet string `json:"snippet"` 30 | Published string `json:"published"` 31 | List []string `json:"list"` 32 | } `json:"data"` 33 | Errors []types.Error `json:"error"` 34 | } 35 | 36 | func (c *Client) EnrichmentCompletion(endpointType string, params EnrichmentParams) (res EnrichmentResponse, err error) { 37 | if params.Q == "" { 38 | err = fmt.Errorf("query is required") 39 | return 40 | } 41 | 42 | if endpointType == "" { 43 | err = fmt.Errorf("endpoint type is required") 44 | return 45 | } 46 | 47 | if endpointType != EndpointTypeWeb && endpointType != EndpointTypeNews { 48 | err = fmt.Errorf("endpoint type must be EndpointTypeWeb or EndpointTypeNews") 49 | return 50 | } 51 | 52 | // needed to set as a query parameter in client.go 53 | paramsMap := make(map[string]string) 54 | paramsMap["q"] = params.Q 55 | 56 | err = c.SendRequest("GET", "/enrich/"+endpointType, paramsMap, &res) 57 | if err != nil { 58 | return 59 | } 60 | 61 | if len(res.Errors) != 0 { 62 | errObj := res.Errors[0] 63 | err = fmt.Errorf("api returned error: %v", fmt.Sprintf("[code: %d, msg: %s, ref: %v]", errObj.Code, errObj.Msg, errObj.Ref)) 64 | return 65 | } 66 | 67 | return 68 | } 69 | -------------------------------------------------------------------------------- /enrichment_test.go: -------------------------------------------------------------------------------- 1 | package kagi 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/httpjamesm/kagigo/constants" 8 | ) 9 | 10 | func TestEnrichmentCompletion(t *testing.T) { 11 | apiToken := os.Getenv("KAGI_API_TOKEN") 12 | 13 | client := NewClient(&ClientConfig{ 14 | APIKey: apiToken, 15 | APIVersion: constants.CurrentApiVersion, 16 | }) 17 | 18 | params := EnrichmentParams{ 19 | Q: "kagi search", 20 | } 21 | 22 | // test EndpointType exists error 23 | res, err := client.EnrichmentCompletion("", params) 24 | if err == nil { 25 | t.Errorf("Expected endpoint type is required error, got nil") 26 | } 27 | 28 | // test EndpointType must be web or news error 29 | res, err = client.EnrichmentCompletion("unexpected", params) 30 | if err == nil { 31 | t.Errorf("Expected endpoint type must be web or news error, got nil") 32 | } 33 | 34 | // test news endpoint does not return error 35 | res, err = client.EnrichmentCompletion("news", EnrichmentParams{ 36 | Q: "los angeles", 37 | }) 38 | if err != nil { 39 | t.Errorf("Unexpected error: %v", err) 40 | } 41 | 42 | // test web endpoint does not return error 43 | res, err = client.EnrichmentCompletion("web", params) 44 | if err != nil { 45 | t.Errorf("Unexpected error: %v", err) 46 | } 47 | 48 | // Check the response fields 49 | if res.Meta.ID == "" { 50 | t.Errorf("Expected non-empty ID, got '%s'", res.Meta.ID) 51 | } 52 | 53 | if res.Meta.Node == "" { 54 | t.Errorf("Expected non-empty Node, got '%s'", res.Meta.Node) 55 | } 56 | 57 | if res.Meta.Ms < 0 { 58 | t.Errorf("Expected positive Ms, got '%d'", res.Meta.Ms) 59 | } 60 | 61 | if res.Data == nil { 62 | t.Errorf("Expected non-empty Data object, got '%v'", res.Data) 63 | } 64 | 65 | if res.Data[0].URL == "" || res.Data[0].Title == "" || res.Data[0].Snippet == "" { 66 | t.Errorf("Expected URL, Title, Snippet, got '%v'", res.Data[0]) 67 | } 68 | 69 | // test if query is empty 70 | params.Q = "" 71 | res, err = client.EnrichmentCompletion("web", params) 72 | if err == nil { 73 | t.Errorf("Expected query is required error, got nil") 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /client.go: -------------------------------------------------------------------------------- 1 | package kagi 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "io" 8 | 9 | "github.com/go-resty/resty/v2" 10 | "github.com/httpjamesm/kagigo/constants" 11 | "github.com/httpjamesm/kagigo/types" 12 | ) 13 | 14 | type ClientConfig struct { 15 | APIKey string 16 | APIVersion constants.ApiVersion 17 | } 18 | 19 | type Client struct { 20 | Config *ClientConfig 21 | } 22 | 23 | func NewClient(config *ClientConfig) *Client { 24 | return &Client{Config: config} 25 | } 26 | 27 | func (c *Client) GetAPIKey() string { 28 | return c.Config.APIKey 29 | } 30 | 31 | func (c *Client) SetAPIKey(apiKey string) { 32 | c.Config.APIKey = apiKey 33 | } 34 | 35 | func (c *Client) GetAPIVersion() constants.ApiVersion { 36 | return c.Config.APIVersion 37 | } 38 | 39 | func (c *Client) SetAPIVersion(apiVersion constants.ApiVersion) { 40 | c.Config.APIVersion = apiVersion 41 | } 42 | 43 | func (c *Client) getBaseURL() string { 44 | return constants.BASE_URL + "/" + string(c.Config.APIVersion) 45 | } 46 | 47 | func (c *Client) SendRequest(method, path string, data interface{}, v any) (err error) { 48 | 49 | baseURL := c.getBaseURL() 50 | 51 | client := resty.New() 52 | 53 | reqBuild := client.R(). 54 | SetHeader("Content-Type", "application/json"). 55 | SetHeader("Authorization", fmt.Sprintf("Bot %s", c.Config.APIKey)). 56 | SetBody(data) 57 | 58 | var resp *resty.Response 59 | 60 | switch method { 61 | case "GET": 62 | reqBuild.SetQueryParams(data.(map[string]string)) // enrichment api requires query params 63 | resp, err = reqBuild.Get(baseURL + path) 64 | case "POST": 65 | resp, err = reqBuild.Post(baseURL + path) 66 | case "PUT": 67 | resp, err = reqBuild.Put(baseURL + path) 68 | case "DELETE": 69 | resp, err = reqBuild.Delete(baseURL + path) 70 | default: 71 | err = fmt.Errorf("invalid method: %s", method) 72 | return 73 | } 74 | 75 | if resp.StatusCode() != 200 { 76 | var apiError types.ErrorResponse 77 | err := json.Unmarshal(resp.Body(), &apiError) 78 | if err != nil || len(apiError.Error) == 0 { 79 | return fmt.Errorf("received status code %d with unparseable error response", resp.StatusCode()) 80 | } 81 | return fmt.Errorf("received status code %d. error object: %v", resp.StatusCode(), 82 | fmt.Sprintf("[code: %d, msg: %s, ref: %v]", apiError.Error[0].Code, apiError.Error[0].Msg, apiError.Error[0].Ref)) 83 | } 84 | 85 | if err != nil { 86 | return 87 | } 88 | 89 | // get reader from body 90 | body := resp.Body() 91 | reader := bytes.NewReader(body) 92 | ioReader := io.Reader(reader) 93 | 94 | return decodeResponse(ioReader, v) 95 | } 96 | 97 | func decodeResponse(body io.Reader, v any) error { 98 | if v == nil { 99 | return nil 100 | } 101 | 102 | if result, ok := v.(*string); ok { 103 | return decodeString(body, result) 104 | } 105 | return json.NewDecoder(body).Decode(v) 106 | } 107 | 108 | func decodeString(body io.Reader, output *string) error { 109 | b, err := io.ReadAll(body) 110 | if err != nil { 111 | return err 112 | } 113 | *output = string(b) 114 | return nil 115 | } 116 | --------------------------------------------------------------------------------