├── .gitignore ├── main.go ├── cmd ├── root.go ├── config.go └── search.go ├── validation └── main.go ├── .github └── workflows │ └── release.yml ├── .goreleaser.yaml ├── LICENSE ├── constants └── main.go ├── go.mod ├── config └── main.go ├── README.md ├── perplexity └── main.go └── go.sum /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | .DS_Store 3 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/japelsin/pplx/cmd" 4 | 5 | func main() { 6 | cmd.Execute() 7 | } 8 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/japelsin/pplx/config" 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | var rootCmd = &cobra.Command{ 11 | Use: "pplx", 12 | Short: "Simple CLI for interfacing with Perplexity's API", 13 | } 14 | 15 | func Execute() { 16 | err := rootCmd.Execute() 17 | if err != nil { 18 | os.Exit(1) 19 | } 20 | } 21 | 22 | func init() { 23 | err := config.Init() 24 | cobra.CheckErr(err) 25 | } 26 | -------------------------------------------------------------------------------- /validation/main.go: -------------------------------------------------------------------------------- 1 | package validation 2 | 3 | import ( 4 | "errors" 5 | "strconv" 6 | "strings" 7 | 8 | "github.com/japelsin/pplx/constants" 9 | ) 10 | 11 | func ValidateInt(value string) error { 12 | _, err := strconv.Atoi(value) 13 | if err != nil { 14 | return errors.New("Must be an int") 15 | } 16 | 17 | return nil 18 | } 19 | 20 | func ValidateRequired(value string) error { 21 | if value == "" { 22 | return errors.New("May not be empty") 23 | } 24 | 25 | return nil 26 | } 27 | 28 | func ValidateRecencyFilter(value string) error { 29 | for _, recencyFilter := range constants.SEARCH_RECENCY_FILTERS { 30 | if value == recencyFilter { 31 | return nil 32 | } 33 | } 34 | 35 | return errors.New("Invalid recency filter, must be one of: " + strings.Join(constants.SEARCH_RECENCY_FILTERS[:], ", ")) 36 | } 37 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # https://goreleaser.com/ci/actions/?h=github+ac#workflow 2 | name: goreleaser 3 | 4 | on: 5 | pull_request: 6 | push: 7 | tags: 8 | - "*" 9 | 10 | permissions: 11 | contents: write 12 | packages: write 13 | # issues: write 14 | 15 | jobs: 16 | goreleaser: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@v4 21 | with: 22 | fetch-depth: 0 23 | - name: Set up Go 24 | uses: actions/setup-go@v5 25 | with: 26 | go-version: stable 27 | - name: Run GoReleaser 28 | uses: goreleaser/goreleaser-action@v5 29 | with: 30 | distribution: goreleaser 31 | version: latest 32 | args: release --clean 33 | env: 34 | GITHUB_TOKEN: ${{ secrets.GORELEASER_TOKEN }} 35 | -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | version: 1 2 | 3 | before: 4 | hooks: 5 | - go mod tidy 6 | 7 | builds: 8 | - env: 9 | - CGO_ENABLED=0 10 | goos: 11 | - linux 12 | - windows 13 | - darwin 14 | 15 | archives: 16 | - format: tar.gz 17 | # this name template makes the OS and Arch compatible with the results of `uname`. 18 | name_template: >- 19 | {{ .ProjectName }}_ 20 | {{- title .Os }}_ 21 | {{- if eq .Arch "amd64" }}x86_64 22 | {{- else if eq .Arch "386" }}i386 23 | {{- else }}{{ .Arch }}{{ end }} 24 | {{- if .Arm }}v{{ .Arm }}{{ end }} 25 | # use zip for windows archives 26 | format_overrides: 27 | - goos: windows 28 | format: zip 29 | 30 | brews: 31 | - 32 | name: pplx 33 | homepage: "https://github.com/japelsin/pplx" 34 | repository: 35 | owner: japelsin 36 | name: homebrew-tap 37 | commit_author: 38 | name: japelsin 39 | 40 | changelog: 41 | sort: asc 42 | filters: 43 | exclude: 44 | - "^docs:" 45 | - "^test:" 46 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 japelsin 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 | -------------------------------------------------------------------------------- /constants/main.go: -------------------------------------------------------------------------------- 1 | package constants 2 | 3 | const ( 4 | API_KEY_KEY = "api_key" 5 | DEFAULT_MAX_TOKENS = 1000 6 | DEFAULT_MODEL = "sonar" 7 | FREQUENCY_PENALTY_KEY = "frequency_penalty" 8 | MAX_TOKENS_KEY = "max_tokens" 9 | MODEL_KEY = "model" 10 | PRESENCE_PENALTY_KEY = "presence_penalty" 11 | SEARCH_DOMAIN_FILTER_KEY = "search_domain_filter" 12 | SEARCH_RECENCY_FILTER_KEY = "search_recency_filter" 13 | STREAM_KEY = "stream" 14 | SYSTEM_PROMPT_KEY = "system_prompt" 15 | TEMPERATURE_KEY = "temperature" 16 | TOP_K_KEY = "top_k" 17 | TOP_P_KEY = "top_p" 18 | 19 | // Closed beta 20 | // RETURN_CITATIONS_KEY = "return_citations" 21 | // RETURN_IMAGES_KEY = "return_images" 22 | // RETURN_RELATED_QUESTIONS_KEY = "return_related_questions" 23 | ) 24 | 25 | var ( 26 | AVAILABLE_MODELS = []string{DEFAULT_MODEL, "sonar-pro", "sonar-pro", "sonar-pro", "sonar-reasoning-pro"} 27 | CONFIG_KEYS = []string{API_KEY_KEY, MAX_TOKENS_KEY, MODEL_KEY, STREAM_KEY, SYSTEM_PROMPT_KEY} 28 | SEARCH_RECENCY_FILTERS = []string{"month", "week", "day", "hour"} 29 | ) 30 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/japelsin/pplx 2 | 3 | go 1.22.0 4 | 5 | require ( 6 | github.com/kirsle/configdir v0.0.0-20170128060238-e45d2f54772f 7 | github.com/manifoldco/promptui v0.9.0 8 | github.com/spf13/cobra v1.8.0 9 | github.com/spf13/pflag v1.0.5 10 | github.com/spf13/viper v1.18.2 11 | ) 12 | 13 | require ( 14 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // indirect 15 | github.com/fsnotify/fsnotify v1.7.0 // indirect 16 | github.com/hashicorp/hcl v1.0.0 // indirect 17 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 18 | github.com/magiconair/properties v1.8.7 // indirect 19 | github.com/mitchellh/mapstructure v1.5.0 // indirect 20 | github.com/pelletier/go-toml/v2 v2.1.0 // indirect 21 | github.com/sagikazarmark/locafero v0.4.0 // indirect 22 | github.com/sagikazarmark/slog-shim v0.1.0 // indirect 23 | github.com/sourcegraph/conc v0.3.0 // indirect 24 | github.com/spf13/afero v1.11.0 // indirect 25 | github.com/spf13/cast v1.6.0 // indirect 26 | github.com/subosito/gotenv v1.6.0 // indirect 27 | go.uber.org/atomic v1.9.0 // indirect 28 | go.uber.org/multierr v1.9.0 // indirect 29 | golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect 30 | golang.org/x/sys v0.15.0 // indirect 31 | golang.org/x/text v0.14.0 // indirect 32 | gopkg.in/ini.v1 v1.67.0 // indirect 33 | gopkg.in/yaml.v3 v3.0.1 // indirect 34 | ) 35 | 36 | retract ( 37 | v0.1.1 // Old repo 38 | v0.1.0 // Old repo 39 | ) 40 | -------------------------------------------------------------------------------- /config/main.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "github.com/japelsin/pplx/constants" 5 | "github.com/kirsle/configdir" 6 | "github.com/spf13/viper" 7 | ) 8 | 9 | type config struct{} 10 | 11 | func Init() error { 12 | configPath := configdir.LocalConfig() 13 | 14 | viper.AddConfigPath(configPath) 15 | viper.SetConfigName("pplx") 16 | viper.SetConfigType("json") 17 | 18 | err := viper.ReadInConfig() 19 | if err != nil { 20 | if err == err.(viper.ConfigFileNotFoundError) { 21 | return Reset() 22 | } 23 | 24 | return err 25 | } 26 | 27 | return nil 28 | } 29 | 30 | func GetApiKey() string { 31 | return viper.GetString(constants.API_KEY_KEY) 32 | } 33 | 34 | func SetApiKey(value string) { 35 | viper.Set(constants.API_KEY_KEY, value) 36 | } 37 | 38 | func GetMaxTokens() int { 39 | return viper.GetInt(constants.MAX_TOKENS_KEY) 40 | } 41 | 42 | func SetMaxTokens(value int) { 43 | viper.Set(constants.MAX_TOKENS_KEY, value) 44 | } 45 | 46 | func GetModel() string { 47 | return viper.GetString(constants.MODEL_KEY) 48 | } 49 | 50 | func SetModel(value string) { 51 | viper.Set(constants.MODEL_KEY, value) 52 | } 53 | 54 | func GetSystemPrompt() string { 55 | return viper.GetString(constants.SYSTEM_PROMPT_KEY) 56 | } 57 | 58 | func SetSystemPrompt(value string) { 59 | viper.Set(constants.SYSTEM_PROMPT_KEY, value) 60 | } 61 | 62 | func GetStream() bool { 63 | return viper.GetBool(constants.STREAM_KEY) 64 | } 65 | 66 | func SetStream(value bool) { 67 | viper.Set(constants.STREAM_KEY, value) 68 | } 69 | 70 | func Save() error { 71 | return viper.WriteConfig() 72 | } 73 | 74 | func Reset() error { 75 | SetApiKey("") 76 | SetMaxTokens(constants.DEFAULT_MAX_TOKENS) 77 | SetModel(constants.DEFAULT_MODEL) 78 | SetSystemPrompt("") 79 | SetStream(true) 80 | 81 | return viper.SafeWriteConfig() 82 | } 83 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Perplexity CLI 2 | 3 | [![Go](https://github.com/japelsin/pplx/actions/workflows/release.yml/badge.svg)](https://github.com/japelsin/pplx/actions/workflows/release.yml) 4 | [![License](https://img.shields.io/badge/license-MIT-blue)](https://github.com/japelsin/pplx/blob/main/LICENSE) 5 | 6 | CLI for searching with [Perplexity](https://www.perplexity.ai/)'s API. Can also be used as a chatbot. 7 | 8 | ## Prerequisites 9 | 10 | - Perplexity account and API key. You'll be prompted for the API key the first time you run `pplx`. 11 | 12 | ## Installation 13 | 14 | ### With [Homebrew](https://brew.sh) 15 | 16 | ```bash 17 | brew install japelsin/tap/pplx 18 | ``` 19 | 20 | ### From source 21 | 22 | If you have [go](https://go.dev/) installed: 23 | 24 | ```bash 25 | go install github.com/japelsin/pplx@latest 26 | ``` 27 | 28 | You could also grab the appropriate executable from [releases](https://github.com/japelsin/pplx/releases). 29 | 30 | ## Usage 31 | 32 | ### Search 33 | 34 | Search command. Most parameters allowed by the [Perplexity API](https://docs.perplexity.ai/api-reference/chat-completions) are available as options. The defaults for some flags can also be set through the config (see below). 35 | 36 | ``` 37 | Usage: 38 | pplx search [flags] 39 | 40 | Flags: 41 | -a, --api_key string API Key 42 | -f, --frequency_penalty float Token frequency penalty [0, 1.0] 43 | -l, --max_tokens int Token limit per request 44 | -m, --model string Model to use 45 | -p, --presence_penalty float Token presence penalty [-2.0, 2.0] 46 | -q, --query string Your query 47 | -d, --search_domain_filter stringArray Domain filter (e.g. '-d https://x.com -d https://y.com') 48 | -r, --search_recency_filter string Recency filter (month, week, day or hour) 49 | -s, --system_prompt string System prompt 50 | -t, --temperature float Response randomness [0, 2.0] 51 | -K, --top_k int Number of tokens to sample from [0, 2048] 52 | -P, --top_p float Probability cutoff for token selection [0, 1.0] 53 | ``` 54 | 55 | ### Config 56 | 57 | The following config options are available: 58 | 59 | ``` 60 | Usage: 61 | pplx config set [command] 62 | 63 | Available Commands: 64 | api_key Set API key 65 | max_tokens Set default max tokens 66 | model Set default model 67 | stream Set whether to stream response 68 | system_prompt Set default system prompt 69 | ``` 70 | -------------------------------------------------------------------------------- /perplexity/main.go: -------------------------------------------------------------------------------- 1 | package perplexity 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "encoding/json" 7 | "errors" 8 | "net/http" 9 | ) 10 | 11 | type Client struct { 12 | ApiKey string 13 | Messages []Message 14 | Payload map[string]interface{} 15 | } 16 | 17 | type Message struct { 18 | Role string `json:"role"` 19 | Content string `json:"content"` 20 | } 21 | 22 | type Result struct { 23 | ID string `json:"id"` 24 | Model string `json:"model"` 25 | Created int `json:"created"` 26 | Usage struct { 27 | PromptTokens int `json:"prompt_tokens"` 28 | CompletionTokens int `json:"completion_tokens"` 29 | TotalTokens int `json:"total_tokens"` 30 | } `json:"usage"` 31 | Object string `json:"object"` 32 | Choices []struct { 33 | Index int `json:"index"` 34 | FinishReason string `json:"finish_reason"` 35 | Message struct { 36 | Role string `json:"role"` 37 | Content string `json:"content"` 38 | } `json:"message"` 39 | Delta struct { 40 | Role string `json:"role"` 41 | Content string `json:"content"` 42 | } `json:"delta"` 43 | } `json:"choices"` 44 | } 45 | 46 | func NewClient(apiKey string) *Client { 47 | return &Client{ 48 | ApiKey: apiKey, 49 | Messages: []Message{}, 50 | Payload: make(map[string]interface{}), 51 | } 52 | } 53 | 54 | func (c *Client) SetPayload(key string, value any) { 55 | c.Payload[key] = value 56 | } 57 | 58 | func (c *Client) AppendMessage(role string, content string) { 59 | c.Messages = append(c.Messages, Message{role, content}) 60 | } 61 | 62 | func (c *Client) getResponse() (*http.Response, error) { 63 | url := "https://api.perplexity.ai/chat/completions" 64 | 65 | payload := c.Payload 66 | payload["messages"] = c.Messages 67 | 68 | data, err := json.Marshal(payload) 69 | if err != nil { 70 | return nil, err 71 | } 72 | 73 | req, err := http.NewRequest("POST", url, bytes.NewReader(data)) 74 | if err != nil { 75 | return nil, err 76 | } 77 | 78 | req.Header.Add("Authorization", "Bearer "+c.ApiKey) 79 | req.Header.Add("Content-Type", "application/json") 80 | 81 | client := http.Client{} 82 | response, err := client.Do(req) 83 | if err != nil { 84 | return response, err 85 | } 86 | if response.StatusCode != 200 { 87 | return nil, errors.New("Request error: " + response.Status) 88 | } 89 | 90 | return response, err 91 | } 92 | 93 | func (c *Client) MakeRequest() (*Result, error) { 94 | response, err := c.getResponse() 95 | if err != nil { 96 | return nil, err 97 | } 98 | 99 | result := Result{} 100 | 101 | err = json.NewDecoder(response.Body).Decode(&result) 102 | defer response.Body.Close() 103 | 104 | if err != nil { 105 | return nil, err 106 | } 107 | 108 | return &result, nil 109 | } 110 | 111 | func (c *Client) MakeStreamedRequest(callback func(string)) (*Result, error) { 112 | response, err := c.getResponse() 113 | if err != nil { 114 | return nil, err 115 | } 116 | 117 | result := Result{} 118 | scanner := bufio.NewScanner(response.Body) 119 | prevLen := 0 120 | 121 | for scanner.Scan() { 122 | bytes := scanner.Bytes() 123 | bytesLen := len(bytes) 124 | 125 | // There's probably a better way to do this 126 | if bytesLen > 6 { 127 | err := json.Unmarshal(bytes[6:], &result) 128 | if err != nil { 129 | return nil, err 130 | } 131 | 132 | message := result.Choices[0].Message.Content 133 | callback(message[prevLen:]) 134 | 135 | prevLen = len(message) 136 | } 137 | } 138 | 139 | defer response.Body.Close() 140 | return &result, nil 141 | } 142 | -------------------------------------------------------------------------------- /cmd/config.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | 7 | "github.com/japelsin/pplx/config" 8 | "github.com/japelsin/pplx/constants" 9 | "github.com/japelsin/pplx/validation" 10 | "github.com/kirsle/configdir" 11 | "github.com/manifoldco/promptui" 12 | "github.com/spf13/cobra" 13 | ) 14 | 15 | func ensureValue(label string, args []string) string { 16 | if len(args) > 0 { 17 | return args[0] 18 | } 19 | 20 | prompt := promptui.Prompt{ 21 | Label: label, 22 | Validate: validation.ValidateRequired, 23 | } 24 | 25 | result, err := prompt.Run() 26 | cobra.CheckErr(err) 27 | 28 | return result 29 | } 30 | 31 | func ensureSelectValue(label string, args []string, items []string) string { 32 | if len(args) > 0 { 33 | return args[0] 34 | } 35 | 36 | prompt := promptui.Select{ 37 | Label: label, 38 | Items: items, 39 | } 40 | 41 | _, result, err := prompt.Run() 42 | cobra.CheckErr(err) 43 | 44 | return result 45 | } 46 | 47 | var configCmd = &cobra.Command{ 48 | Use: "config", 49 | Short: "Configure pplx", 50 | } 51 | 52 | var configPathCmd = &cobra.Command{ 53 | Use: "path", 54 | Short: "Get configuration file path", 55 | Run: func(cmd *cobra.Command, args []string) { 56 | path := configdir.LocalConfig() 57 | fmt.Println(path) 58 | }, 59 | } 60 | 61 | var configResetCmd = &cobra.Command{ 62 | Use: "reset", 63 | Short: "Reset config", 64 | Args: cobra.NoArgs, 65 | Run: func(cmd *cobra.Command, args []string) { 66 | config.Reset() 67 | }, 68 | } 69 | 70 | var configSetCmd = &cobra.Command{ 71 | Use: "set", 72 | Short: "Set config values", 73 | ValidArgs: constants.CONFIG_KEYS, 74 | Args: cobra.OnlyValidArgs, 75 | } 76 | 77 | var configSetApiKeyCmd = &cobra.Command{ 78 | Use: constants.API_KEY_KEY, 79 | Short: "Set API key", 80 | Args: cobra.RangeArgs(0, 1), 81 | Run: func(cmd *cobra.Command, args []string) { 82 | value := ensureValue("API key", args) 83 | config.SetApiKey(value) 84 | 85 | err := config.Save() 86 | cobra.CheckErr(err) 87 | }, 88 | } 89 | 90 | var configSetMaxTokensCmd = &cobra.Command{ 91 | Use: constants.MAX_TOKENS_KEY, 92 | Short: "Set default max tokens", 93 | Args: cobra.RangeArgs(0, 1), 94 | Run: func(cmd *cobra.Command, args []string) { 95 | value := ensureValue("Max tokens", args) 96 | 97 | v, _ := strconv.Atoi(value) // Already validated 98 | config.SetMaxTokens(v) 99 | 100 | err := config.Save() 101 | cobra.CheckErr(err) 102 | }, 103 | } 104 | 105 | var configSetModelCmd = &cobra.Command{ 106 | Use: constants.MODEL_KEY, 107 | Short: "Set default model", 108 | Args: cobra.RangeArgs(0, 1), 109 | ValidArgs: constants.AVAILABLE_MODELS, 110 | Run: func(cmd *cobra.Command, args []string) { 111 | value := ensureSelectValue("Model", args, constants.AVAILABLE_MODELS) 112 | config.SetModel(value) 113 | 114 | err := config.Save() 115 | cobra.CheckErr(err) 116 | }, 117 | } 118 | 119 | var STREAM_OPTIONS = []string{"Yes", "No"} 120 | 121 | var configSetStreamCmd = &cobra.Command{ 122 | Use: constants.STREAM_KEY, 123 | Short: "Set whether to stream response", 124 | Args: cobra.RangeArgs(0, 1), 125 | ValidArgs: STREAM_OPTIONS, 126 | Run: func(cmd *cobra.Command, args []string) { 127 | value := ensureSelectValue("Stream response", args, STREAM_OPTIONS) 128 | config.SetStream(value == "Yes") 129 | 130 | err := config.Save() 131 | cobra.CheckErr(err) 132 | }, 133 | } 134 | 135 | var configSetSystemPromptCmd = &cobra.Command{ 136 | Use: constants.SYSTEM_PROMPT_KEY, 137 | Short: "Set default system prompt", 138 | Args: cobra.RangeArgs(0, 1), 139 | ValidArgs: constants.AVAILABLE_MODELS, 140 | Run: func(cmd *cobra.Command, args []string) { 141 | value := ensureSelectValue("Model", args, constants.AVAILABLE_MODELS) 142 | config.SetModel(value) 143 | 144 | err := config.Save() 145 | cobra.CheckErr(err) 146 | }, 147 | } 148 | 149 | func init() { 150 | rootCmd.AddCommand(configCmd) 151 | 152 | // Config subcommands 153 | configCmd.AddCommand(configPathCmd) 154 | configCmd.AddCommand(configResetCmd) 155 | configCmd.AddCommand(configSetCmd) 156 | 157 | // Set subcommands 158 | configSetCmd.AddCommand(configSetApiKeyCmd) 159 | configSetCmd.AddCommand(configSetMaxTokensCmd) 160 | configSetCmd.AddCommand(configSetModelCmd) 161 | configSetCmd.AddCommand(configSetStreamCmd) 162 | configSetCmd.AddCommand(configSetSystemPromptCmd) 163 | } 164 | -------------------------------------------------------------------------------- /cmd/search.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/japelsin/pplx/config" 7 | "github.com/japelsin/pplx/constants" 8 | "github.com/japelsin/pplx/perplexity" 9 | "github.com/japelsin/pplx/validation" 10 | "github.com/manifoldco/promptui" 11 | "github.com/spf13/cobra" 12 | "github.com/spf13/pflag" 13 | "github.com/spf13/viper" 14 | ) 15 | 16 | var query = "" 17 | 18 | var searchCmd = &cobra.Command{ 19 | Use: "search", 20 | Short: "Search using Perplexity", 21 | Args: cobra.NoArgs, 22 | PersistentPreRunE: func(cmd *cobra.Command, args []string) error { 23 | recencyFilter := cmd.Flags().Lookup(constants.SEARCH_RECENCY_FILTER_KEY) 24 | 25 | if recencyFilter.Changed { 26 | err := validation.ValidateRecencyFilter(recencyFilter.Value.String()) 27 | if err != nil { 28 | return err 29 | } 30 | } 31 | 32 | return nil 33 | }, 34 | PreRun: func(cmd *cobra.Command, args []string) { 35 | // Ensure API key is set 36 | 37 | if config.GetApiKey() == "" { 38 | fmt.Println("Enter your Perplexity API key to get started") 39 | 40 | prompt := promptui.Prompt{ 41 | Label: "API key", 42 | Validate: validation.ValidateRequired, 43 | } 44 | 45 | apiKey, err := prompt.Run() 46 | cobra.CheckErr(err) 47 | 48 | config.SetApiKey(apiKey) 49 | config.Save() 50 | } 51 | 52 | // Ensure query is set 53 | 54 | if query == "" { 55 | prompt := promptui.Prompt{ 56 | Label: "Query", 57 | Validate: validation.ValidateRequired, 58 | } 59 | 60 | promptQuery, err := prompt.Run() 61 | cobra.CheckErr(err) 62 | 63 | query = promptQuery 64 | } 65 | }, 66 | Run: func(cmd *cobra.Command, args []string) { 67 | apiKey := viper.GetString(constants.API_KEY_KEY) 68 | pplxClient := perplexity.NewClient(apiKey) 69 | 70 | systemPrompt := viper.GetString(constants.SYSTEM_PROMPT_KEY) 71 | if systemPrompt != "" { 72 | pplxClient.AppendMessage("system", systemPrompt) 73 | } 74 | 75 | pplxClient.AppendMessage("user", query) 76 | pplxClient.SetPayload(constants.STREAM_KEY, true) 77 | pplxClient.SetPayload(constants.MODEL_KEY, viper.GetString(constants.MODEL_KEY)) 78 | pplxClient.SetPayload(constants.MAX_TOKENS_KEY, viper.GetString(constants.MAX_TOKENS_KEY)) 79 | 80 | // Skip validation for now 81 | cmd.Flags().Visit(func(flag *pflag.Flag) { 82 | pplxClient.SetPayload(flag.Name, flag.Value) 83 | }) 84 | 85 | result, err := pplxClient.MakeStreamedRequest(func(res string) { 86 | fmt.Print(res) 87 | }) 88 | cobra.CheckErr(err) 89 | 90 | fmt.Printf("\n\nTotal tokens: %d | Prompt: %d | Completion: %d\n", result.Usage.TotalTokens, result.Usage.PromptTokens, result.Usage.CompletionTokens) 91 | }, 92 | } 93 | 94 | func init() { 95 | rootCmd.AddCommand(searchCmd) 96 | 97 | searchCmd.Flags().StringVarP(&query, "query", "q", "", "Your query") 98 | 99 | // Config flags 100 | searchCmd.Flags().IntP(constants.MAX_TOKENS_KEY, "l", 0, "Token limit per request") 101 | searchCmd.Flags().StringP(constants.MODEL_KEY, "m", "", "Model to use") 102 | searchCmd.Flags().StringP(constants.SYSTEM_PROMPT_KEY, "s", "", "System prompt") 103 | searchCmd.Flags().StringP(constants.API_KEY_KEY, "a", "", "API Key") 104 | 105 | viper.BindPFlag(constants.API_KEY_KEY, searchCmd.Flags().Lookup(constants.API_KEY_KEY)) 106 | viper.BindPFlag(constants.MAX_TOKENS_KEY, searchCmd.Flags().Lookup(constants.MAX_TOKENS_KEY)) 107 | viper.BindPFlag(constants.MODEL_KEY, searchCmd.Flags().Lookup(constants.MODEL_KEY)) 108 | viper.BindPFlag(constants.SYSTEM_PROMPT_KEY, searchCmd.Flags().Lookup(constants.SYSTEM_PROMPT_KEY)) 109 | viper.BindPFlag(constants.STREAM_KEY, searchCmd.Flags().Lookup(constants.STREAM_KEY)) 110 | 111 | // Optional flags 112 | searchCmd.Flags().Float64P(constants.FREQUENCY_PENALTY_KEY, "f", 0, "Token frequency penalty [0, 1.0]") 113 | searchCmd.Flags().Float64P(constants.PRESENCE_PENALTY_KEY, "p", 0, "Token presence penalty [-2.0, 2.0]") 114 | searchCmd.Flags().Float64P(constants.TEMPERATURE_KEY, "t", 0, "Response randomness [0, 2.0]") 115 | searchCmd.Flags().Float64P(constants.TOP_P_KEY, "P", 0, "Probability cutoff for token selection [0, 1.0]") 116 | searchCmd.Flags().IntP(constants.TOP_K_KEY, "K", 0, "Number of tokens to sample from [0, 2048]") 117 | searchCmd.Flags().StringArrayP(constants.SEARCH_DOMAIN_FILTER_KEY, "d", []string{}, "Domain filter (e.g. '-d https://x.com -d https://y.com')") 118 | searchCmd.Flags().StringP(constants.SEARCH_RECENCY_FILTER_KEY, "r", "", "Recency filter (month, week, day or hour)") 119 | } 120 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE= 2 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= 3 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8= 4 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= 5 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8= 6 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 7 | github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 8 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 9 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 10 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= 11 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 12 | github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= 13 | github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= 14 | github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= 15 | github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= 16 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= 17 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 18 | github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= 19 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 20 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 21 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 22 | github.com/kirsle/configdir v0.0.0-20170128060238-e45d2f54772f h1:dKccXx7xA56UNqOcFIbuqFjAWPVtP688j5QMgmo6OHU= 23 | github.com/kirsle/configdir v0.0.0-20170128060238-e45d2f54772f/go.mod h1:4rEELDSfUAlBSyUjPG0JnaNGjf13JySHFeRdD/3dLP0= 24 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 25 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 26 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 27 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 28 | github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= 29 | github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= 30 | github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA= 31 | github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg= 32 | github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= 33 | github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 34 | github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4= 35 | github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= 36 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 37 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= 38 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 39 | github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= 40 | github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= 41 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 42 | github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= 43 | github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= 44 | github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= 45 | github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= 46 | github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= 47 | github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= 48 | github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= 49 | github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= 50 | github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= 51 | github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= 52 | github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= 53 | github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= 54 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 55 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 56 | github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ= 57 | github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk= 58 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 59 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 60 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 61 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 62 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 63 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 64 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 65 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 66 | github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= 67 | github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= 68 | go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= 69 | go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= 70 | go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= 71 | go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= 72 | golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= 73 | golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= 74 | golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 75 | golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= 76 | golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 77 | golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= 78 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 79 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 80 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= 81 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 82 | gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= 83 | gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 84 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 85 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 86 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 87 | --------------------------------------------------------------------------------