Skip to content

Commit

Permalink
Initial commit of the functional CLI tool
Browse files Browse the repository at this point in the history
  • Loading branch information
bcspragu committed Jun 7, 2023
1 parent a8eb7b5 commit 4e4d717
Show file tree
Hide file tree
Showing 4 changed files with 211 additions and 0 deletions.
52 changes: 52 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Kagi FastGPT CLI

`kagi` is a quick-and-dirty CLI for querying the [Kagi search engine](https://kagi.com/) with their [FastGPT API](https://help.kagi.com/kagi/api/fastgpt.html)

## Installation

First, build + install the `kagi` CLI with:

```
go install github.com/bcspragu/kagi/cmd/kagi@latest
```

Then, create an API key + add API credits, following [the official Kagi instructions](https://help.kagi.com/kagi/api/fastgpt.html#quick-start). Add the API key to your `~/.bashrc` or equivalent with something like:

```
export KAGI_API_KEY=...
```

And you should be good to go! Try running `kagi <query>` to test it out.

As an aside, I'm **really** not a fan of storing sensitive credentials in accessible-to-everything-all-the-time environment variables, and if anyone has a good + ergonomic alternative (e.g. involving `pass` or credential helpers), I'm all ears.

## Example Output

```
$ kagi net/http golang
===== OUTPUT =====
The net/http package is part of the Go standard library and provides HTTP client and server functionality. [1][2][3]
===== REFERENCES =====
1. http package - net/http - Go Packages - https://pkg.go.dev/net/http
- Package http provides HTTP client and server implementations. ... Manually configuring HTTP/2 via the golang.org/x/net/http2 package takes precedence over...
2. Writing Web Applications - The Go Programming Language - https://go.dev/doc/articles/wiki/
- Covered in this tutorial: Creating a data structure with load and save methods; Using the net/http package to build web applications; Using the html/...
3. How To Make an HTTP Server in Go | DigitalOcean - https://www.digitalocean.com/community/tutorials/how-to-make-an-http-server-in-go
- Apr 21, 2022 ... The net/http package not only includes the ability to make HTTP requests, but also provides an HTTP server you can use to handle those requests.
```

## Troubleshooting

If the `kagi` tool isn't working for you, make sure:

- You've reloaded your `~/.bashrc` or equivalent after adding your API key, e.g. with `source ~/.bashrc` or opening a new shell.
- Run `echo $KAGI_API_KEY` to confirm it's set in the current shell
- You've 'topped up' your API credits
- Confirm there's a non-zero 'Credit remaining' balance in [the Kagi billing UI](https://kagi.com/settings?p=billing_api)

If you're still having issues after that, feel free to file an issue, including any error output.
101 changes: 101 additions & 0 deletions api/api.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
// Package api provides a Kagi API client.
package api

import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
)

type Client struct {
http *http.Client
}

func NewClient(tkn string) *Client {
return &Client{
http: &http.Client{
Transport: &roundTripper{tkn: tkn},
},
}
}

type FastGPTResponse struct {
Meta FastGPTResponseMeta `json:"meta"`
Data FastGPTResponseData `json:"data"`
Errors []FastGPTResponseError `json:"error"`
}

type FastGPTResponseMeta struct {
ID string `json:"id"`
Node string `json:"node"`
Milliseconds int `json:"ms"`
}

type FastGPTResponseData struct {
Output string `json:"output"`
Tokens int `json:"tokens"`
References []FastGPTResponseReference `json:"references"`
}

type FastGPTResponseReference struct {
Title string `json:"title"`
Snippet string `json:"snippet"`
Link string `json:"url"`
}

type FastGPTResponseError struct {
Code int `json:"code"`
Message string `json:"msg"`
// There's also "ref", but it was null so I don't know its type>
}

type FastGPTRequest struct {
Query string `json:"query"`
WebSearch bool `json:"web_search"`
Cache bool `json:"cache"`
}

func (c *Client) QueryFastGPT(query string) (*FastGPTResponse, error) {
var buf bytes.Buffer
req := FastGPTRequest{
Query: query,
WebSearch: true,
Cache: true,
}
if err := json.NewEncoder(&buf).Encode(req); err != nil {
return nil, fmt.Errorf("failed to encode request: %w", err)
}
resp, err := c.http.Post("https://kagi.com/api/v0/fastgpt", "application/json", &buf)
if err != nil {
return nil, fmt.Errorf("failed to query FastGPT API: %w", err)
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
body, _ := ioutil.ReadAll(resp.Body)
return nil, fmt.Errorf("unexpected status code %d, body %q", resp.StatusCode, string(body))
}

var gptResp FastGPTResponse
if err := json.NewDecoder(resp.Body).Decode(&gptResp); err != nil {
return nil, fmt.Errorf("failed to decode FastGPT API response body: %w", err)
}

if len(gptResp.Errors) > 0 {
e := gptResp.Errors[0]
return nil, fmt.Errorf("received %d error(s) from the API: [%d] %q", len(gptResp.Errors), e.Code, e.Message)
}

return &gptResp, nil
}

type roundTripper struct {
tkn string
}

func (rt *roundTripper) RoundTrip(r *http.Request) (*http.Response, error) {
r.Header.Add("Authorization", "Bot "+rt.tkn)
return http.DefaultTransport.RoundTrip(r)
}
55 changes: 55 additions & 0 deletions cmd/kagi/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package main

import (
"errors"
"fmt"
"log"
"os"
"strings"

"github.com/bcspragu/kagi/api"
)

func main() {
if err := run(os.Args); err != nil {
log.Fatal(err)
}
}

func run(args []string) error {
var query string
switch len(args) {
case 0, 1:
return errors.New("usage: kagi <query>")
case 2:
query = args[1]
default:
query = strings.Join(args[1:], " ")
}
if len(args) < 2 {
}
apiKey := os.Getenv("KAGI_API_KEY")
if apiKey == "" {
return errors.New("no KAGI_API_KEY was set")
}

client := api.NewClient(apiKey)

resp, err := client.QueryFastGPT(query)
if err != nil {
return fmt.Errorf("error performing query: %w", err)
}

fmt.Println("===== OUTPUT =====")
fmt.Println()
fmt.Println(resp.Data.Output)
fmt.Println()
fmt.Println("===== REFERENCES =====")
fmt.Println()

for i, ref := range resp.Data.References {
fmt.Printf("%d. %s - %s\n - %s\n\n", i+1, ref.Title, ref.Link, ref.Snippet)
}

return nil
}
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module github.com/bcspragu/kagi

go 1.20

0 comments on commit 4e4d717

Please sign in to comment.