Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat!: add support for model configuration and persistent history storage #30

Merged
merged 1 commit into from
Dec 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 67 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,41 +8,90 @@ A command-line interface (CLI) for [Google Gemini](https://deepmind.google/techn
Google Gemini is a family of multimodal artificial intelligence (AI) large language models that have
capabilities in language, audio, code and video understanding.

The current version only supports multi-turn conversations (chat), using the `gemini-pro` model.
This application offers a command-line interface for interacting with various generative models through
multi-turn chat. Model selection is controlled via [system command](#system-commands) inputs.

## Installation
Choose a binary from the [releases](https://github.com/reugn/gemini-cli/releases).
Choose a binary from [releases](https://github.com/reugn/gemini-cli/releases).

### Build from Source
Download and [install Go](https://golang.org/doc/install).

Install the application:

```sh
go install github.com/reugn/gemini-cli/cmd/gemini@latest
```

See the [go install](https://go.dev/ref/mod#go-install) instructions for more information about the command.

## Usage
> [!NOTE]
> For information on the available regions for the Gemini API and Google AI Studio,
> see [here](https://ai.google.dev/available_regions#available_regions).

### API key
To use `gemini-cli`, you'll need an API key set in the `GEMINI_API_KEY` environment variable.
If you don't already have one, create a key in [Google AI Studio](https://makersuite.google.com/app/apikey).

> [!NOTE]
> For information on the available regions for the Gemini API and Google AI Studio, see [here](https://ai.google.dev/available_regions#available_regions).
To set the environment variable in the terminal:
```console
export GEMINI_API_KEY=<your_api_key>
```

### System commands
The system chat message must begin with an exclamation mark and is used for internal operations.
A short list of supported system commands:

| Command | Description |
|---------|------------------------------------------------------|
| !q | Quit the application |
| !p | Delete the history used as chat context by the model |
| !i | Toggle input mode (single-line <-> multi-line) |
| !m | Select generative model |
| Command | Description |
|---------|----------------------------------------------------|
| !q | Quit the application |
| !p | Select the system prompt for the chat <sup>1</sup> |
| !i | Toggle input mode (single-line <-> multi-line) |
| !m | Select a model operation <sup>2</sup> |
| !h | Select a history operation <sup>3</sup> |

<sup>1</sup> System instruction (also known as "system prompt") is a more forceful prompt to the model.
The model will adhere the instructions more strongly than if they appeared in a normal prompt.
The system instructions must be specified by the user in the [configuration file](#configuration-file).

<sup>2</sup> Model operations:
* Select a generative model from the list of available models
* Show the selected model information

<sup>3</sup> History operations:
* Clear the chat history
* Store the chat history to the configuration file
* Load a chat history record from the configuration file
* Delete all history records from the configuration file

### Configuration file
The application uses a configuration file to store generative model settings and chat history. This file is optional.
If it doesn't exist, the application will attempt to create it using default values. You can use the
[config flag](#cli-help) to specify the location of the configuration file.

An example of basic configuration:
```json
{
"SystemPrompts": {
"Software Engineer": "You are an experienced software engineer.",
"Technical Writer": "Act as a tech writer. I will provide you with the basic steps of an app functionality, and you will come up with an engaging article on how to do those steps."
},
"SafetySettings": [
{
"Category": 7,
"Threshold": 1
},
{
"Category": 10,
"Threshold": 1
}
],
"History": {
}
}
```
Upon user request, the `History` map will be populated with records. Note that the chat history is stored in plain
text format. See [history operations](#system-commands) for details.

### CLI help
```console
Expand All @@ -53,13 +102,13 @@ Usage:
[flags]

Flags:
-f, --format render markdown-formatted response (default true)
-h, --help help for this command
-m, --model string generative model name (default "gemini-pro")
--multiline read input as a multi-line string
-s, --style string markdown format style (ascii, dark, light, pink, notty, dracula) (default "auto")
-t, --term string multi-line input terminator (default "$")
-v, --version version for this command
-c, --config string path to configuration file in JSON format (default "gemini_cli_config.json")
-h, --help help for this command
-m, --model string generative model name (default "gemini-pro")
--multiline read input as a multi-line string
-s, --style string markdown format style (ascii, dark, light, pink, notty, dracula) (default "auto")
-t, --term string multi-line input terminator (default "$")
-v, --version version for this command
```

## License
Expand Down
1 change: 1 addition & 0 deletions cmd/gemini/.gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
gemini
*.json
37 changes: 26 additions & 11 deletions cmd/gemini/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@ import (
"os/user"

"github.com/reugn/gemini-cli/gemini"
"github.com/reugn/gemini-cli/internal/cli"
"github.com/reugn/gemini-cli/internal/chat"
"github.com/reugn/gemini-cli/internal/config"
"github.com/spf13/cobra"
)

const (
version = "0.3.1"
apiKeyEnv = "GEMINI_API_KEY" //nolint:gosec
version = "0.3.1"
apiKeyEnv = "GEMINI_API_KEY" //nolint:gosec
defaultConfigPath = "gemini_cli_config.json"
)

func run() int {
Expand All @@ -21,26 +23,39 @@ func run() int {
Version: version,
}

var opts cli.ChatOpts
rootCmd.Flags().StringVarP(&opts.Model, "model", "m", gemini.DefaultModel, "generative model name")
rootCmd.Flags().BoolVarP(&opts.Format, "format", "f", true, "render markdown-formatted response")
var opts chat.Opts
var configPath string
rootCmd.Flags().StringVarP(&opts.GenerativeModel, "model", "m", gemini.DefaultModel,
"generative model name")
rootCmd.Flags().StringVarP(&opts.Style, "style", "s", "auto",
"markdown format style (ascii, dark, light, pink, notty, dracula)")
rootCmd.Flags().BoolVar(&opts.Multiline, "multiline", false, "read input as a multi-line string")
rootCmd.Flags().StringVarP(&opts.Terminator, "term", "t", "$", "multi-line input terminator")
rootCmd.Flags().BoolVar(&opts.Multiline, "multiline", false,
"read input as a multi-line string")
rootCmd.Flags().StringVarP(&opts.LineTerminator, "term", "t", "$",
"multi-line input terminator")
rootCmd.Flags().StringVarP(&configPath, "config", "c", defaultConfigPath,
"path to configuration file in JSON format")

rootCmd.RunE = func(_ *cobra.Command, _ []string) error {
configuration, err := config.NewConfiguration(configPath)
if err != nil {
return err
}

modelBuilder := gemini.NewGenerativeModelBuilder().
WithName(opts.GenerativeModel).
WithSafetySettings(configuration.Data.SafetySettings)
apiKey := os.Getenv(apiKeyEnv)
chatSession, err := gemini.NewChatSession(context.Background(), opts.Model, apiKey)
chatSession, err := gemini.NewChatSession(context.Background(), modelBuilder, apiKey)
if err != nil {
return err
}

chat, err := cli.NewChat(getCurrentUser(), chatSession, &opts)
chatHandler, err := chat.New(getCurrentUser(), chatSession, configuration, os.Stdout, &opts)
if err != nil {
return err
}
chat.StartChat()
chatHandler.Start()

return chatSession.Close()
}
Expand Down
50 changes: 43 additions & 7 deletions gemini/chat_session.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package gemini

import (
"context"
"encoding/json"
"fmt"
"sync"

"github.com/google/generative-ai-go/genai"
Expand All @@ -15,23 +17,28 @@ type ChatSession struct {
ctx context.Context

client *genai.Client
model *genai.GenerativeModel
session *genai.ChatSession

loadModels sync.Once
models []string
}

// NewChatSession returns a new [ChatSession].
func NewChatSession(ctx context.Context, model, apiKey string) (*ChatSession, error) {
func NewChatSession(
ctx context.Context, modelBuilder *GenerativeModelBuilder, apiKey string,
) (*ChatSession, error) {
client, err := genai.NewClient(ctx, option.WithAPIKey(apiKey))
if err != nil {
return nil, err
}

generativeModel := modelBuilder.build(client)
return &ChatSession{
ctx: ctx,
client: client,
session: client.GenerativeModel(model).StartChat(),
model: generativeModel,
session: generativeModel.StartChat(),
}, nil
}

Expand All @@ -45,14 +52,33 @@ func (c *ChatSession) SendMessageStream(input string) *genai.GenerateContentResp
return c.session.SendMessageStream(c.ctx, genai.Text(input))
}

// SetGenerativeModel sets the name of the generative model for the chat.
// It preserves the history from the previous chat session.
func (c *ChatSession) SetGenerativeModel(model string) {
// SetModel sets a new generative model configured with the builder and starts
// a new chat session. It preserves the history of the previous chat session.
func (c *ChatSession) SetModel(modelBuilder *GenerativeModelBuilder) {
history := c.session.History
c.session = c.client.GenerativeModel(model).StartChat()
c.model = modelBuilder.build(c.client)
c.session = c.model.StartChat()
c.session.History = history
}

// CopyModelBuilder returns a copy builder for the chat generative model.
func (c *ChatSession) CopyModelBuilder() *GenerativeModelBuilder {
return newCopyGenerativeModelBuilder(c.model)
}

// ModelInfo returns information about the chat generative model in JSON format.
func (c *ChatSession) ModelInfo() (string, error) {
modelInfo, err := c.model.Info(c.ctx)
if err != nil {
return "", err
}
encoded, err := json.MarshalIndent(modelInfo, "", " ")
if err != nil {
return "", fmt.Errorf("error encoding model info: %w", err)
}
return string(encoded), nil
}

// ListModels returns a list of the supported generative model names.
func (c *ChatSession) ListModels() []string {
c.loadModels.Do(func() {
Expand All @@ -69,7 +95,17 @@ func (c *ChatSession) ListModels() []string {
return c.models
}

// ClearHistory clears chat history.
// GetHistory returns the chat session history.
func (c *ChatSession) GetHistory() []*genai.Content {
return c.session.History
}

// SetHistory sets the chat session history.
func (c *ChatSession) SetHistory(content []*genai.Content) {
c.session.History = content
}

// ClearHistory clears the chat session history.
func (c *ChatSession) ClearHistory() {
c.session.History = make([]*genai.Content, 0)
}
Expand Down
Loading
Loading