Skip to content

Commit bbcf7bf

Browse files
authored
Merge pull request #1 from shahabkondri/1.0.2
Merge 1.0.2
2 parents 54195d7 + 406a777 commit bbcf7bf

File tree

12 files changed

+403
-69
lines changed

12 files changed

+403
-69
lines changed

.github/workflows/release.yml

Lines changed: 41 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,36 @@ on:
55
- 'v*'
66

77
jobs:
8+
jar_build:
9+
runs-on: ubuntu-latest
10+
steps:
11+
- name: Checkout repository
12+
uses: actions/checkout@v3
13+
14+
- name: Set up JDK 17
15+
uses: actions/setup-java@v2
16+
with:
17+
distribution: 'adopt'
18+
java-version: 17
19+
20+
- name: Cache Maven dependencies
21+
uses: actions/cache@v3
22+
with:
23+
path: ~/.m2
24+
key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }}
25+
restore-keys: ${{ runner.os }}-m2
26+
27+
- name: Build JAR
28+
run: ./mvnw clean package -DskipTests
29+
30+
- name: Upload JAR as artifact
31+
uses: actions/upload-artifact@v3
32+
with:
33+
name: chat-gpt-cli-jar
34+
path: target/chat-gpt-cli-*.jar
35+
836
native_build:
37+
needs: jar_build
938
runs-on: ${{ matrix.os }}
1039
strategy:
1140
matrix:
@@ -68,6 +97,12 @@ jobs:
6897
draft: false
6998
prerelease: false
7099

100+
- name: Download JAR artifact
101+
uses: actions/download-artifact@v3
102+
with:
103+
name: chat-gpt-cli-jar
104+
path: artifacts/chat-gpt-cli-jar
105+
71106
- name: Download artifacts
72107
uses: actions/download-artifact@v3
73108
with:
@@ -80,12 +115,11 @@ jobs:
80115
name: chat-gpt-cli-linux64
81116
path: artifacts/chat-gpt-cli-linux64
82117

83-
- name: Upload native images
118+
- name: Upload native images and JAR to the release
84119
run: |
85-
for artifact_name in chat-gpt-cli-macos-intel chat-gpt-cli-linux64; do
86-
echo "Uploading ${artifact_name}"
87-
cp artifacts/${artifact_name}/chat-gpt-cli artifacts/${artifact_name}/${artifact_name}
88-
gh release upload ${{ steps.extract_tag.outputs.tag }} artifacts/${artifact_name}/${artifact_name}
89-
done
120+
gh release upload ${{ steps.extract_tag.outputs.tag }} \
121+
artifacts/chat-gpt-cli-jar/chat-gpt-cli-*.jar \
122+
artifacts/chat-gpt-cli-macos-intel/chat-gpt-cli \
123+
artifacts/chat-gpt-cli-linux64/chat-gpt-cli
90124
env:
91-
GITHUB_TOKEN: ${{ secrets.GH_PAT }}
125+
GITHUB_TOKEN: ${{ secrets.GH_PAT }}

README.md

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
# ChatGPT CLI
2+
ChatGPT CLI is a command-line interface for interacting with the ChatGPT API, built with Java, Spring Boot, and Spring Shell. It is a simple and easy-to-use tool that allows users to send messages and receive AI-generated responses.
3+
4+
## Features
5+
- Customizable terminal prompt
6+
- Spring Shell-based application for an enhanced command-line experience
7+
8+
## Prerequisites
9+
10+
- Java 17
11+
- Maven (Optional)
12+
13+
## Configuration
14+
Before running the application, make sure to set the required configuration values:
15+
16+
- `openai.api-key`: Set this to your OpenAI API key.
17+
- `chat.gpt.model`: Set this to the desired GPT model (e.g., `gpt_3_5_turbo`).
18+
19+
You can set these values in the `application.properties` file or as environment variables.
20+
21+
## How to Build and Run
22+
Clone the repository:
23+
24+
```bash
25+
git clone https://github.com/shahabkondri/chat-gpt-cli.git
26+
cd chat-gpt-cli
27+
```
28+
29+
To build the project, run:
30+
31+
```bash
32+
./mvnw clean package
33+
```
34+
35+
To run the application:
36+
37+
```bash
38+
java -jar target/chat-gpt-cli-1.0.2.jar
39+
```
40+
41+
## Native Build
42+
Native builds offer faster startup times, lower memory footprint, and easier distribution. To create a native build, install GraalVM and use it as your JDK. Then, run the following command:
43+
44+
```bash
45+
./mvnw clean -Pnative native:compile
46+
```
47+
48+
This will produce a standalone executable optimized for your platform.
49+
Once compiled, you can run the native executable:
50+
51+
```bash
52+
./target/chat-gpt-cli
53+
```
54+
55+
## Usage
56+
After starting the application, you will see a terminal prompt:
57+
58+
```bash
59+
:>
60+
```
61+
62+
Enter your message and press Enter to send it to the ChatGPT API. The AI-generated response will be displayed in the terminal. To exit the application, type exit and press Enter.
63+
64+
```bash
65+
:> chat tell me a joke.
66+
```
67+
68+
## License
69+
This project is licensed under the MIT License. See the [LICENCE](LICENCE.md) file for details.
70+
71+
## Contributing
72+
Feel free to submit issues and enhancement requests on the GitHub issue tracker.

pom.xml

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
<groupId>com.shahabkondri</groupId>
1313
<artifactId>chat-gpt-cli</artifactId>
14-
<version>1.0.2-SNAPSHOT</version>
14+
<version>1.0.2</version>
1515
<packaging>jar</packaging>
1616

1717
<name>chat-gpt-cli</name>
@@ -48,6 +48,8 @@
4848
<java.version>17</java.version>
4949
<spring-shell.version>3.0.1</spring-shell.version>
5050

51+
<maven-source-plugin.version>3.2.1</maven-source-plugin.version>
52+
<maven-javadoc-plugin.version>3.5.0</maven-javadoc-plugin.version>
5153
<spring-javaformat-maven-plugin.version>0.0.35</spring-javaformat-maven-plugin.version>
5254
<maven-checkstyle-plugin.version>3.2.1</maven-checkstyle-plugin.version>
5355
<checkstyle.version>10.7.0</checkstyle.version>
@@ -69,7 +71,7 @@
6971
<dependency>
7072
<groupId>com.shahabkondri</groupId>
7173
<artifactId>chat-gpt-api</artifactId>
72-
<version>1.0.1</version>
74+
<version>1.0.2</version>
7375
</dependency>
7476
<dependency>
7577
<groupId>org.springframework.shell</groupId>
@@ -88,6 +90,32 @@
8890
<groupId>org.springframework.boot</groupId>
8991
<artifactId>spring-boot-maven-plugin</artifactId>
9092
</plugin>
93+
<plugin>
94+
<groupId>org.apache.maven.plugins</groupId>
95+
<artifactId>maven-javadoc-plugin</artifactId>
96+
<version>${maven-javadoc-plugin.version}</version>
97+
<executions>
98+
<execution>
99+
<id>attach-javadoc</id>
100+
<goals>
101+
<goal>jar</goal>
102+
</goals>
103+
</execution>
104+
</executions>
105+
</plugin>
106+
<plugin>
107+
<groupId>org.apache.maven.plugins</groupId>
108+
<artifactId>maven-source-plugin</artifactId>
109+
<version>${maven-source-plugin.version}</version>
110+
<executions>
111+
<execution>
112+
<id>attach-sources</id>
113+
<goals>
114+
<goal>jar</goal>
115+
</goals>
116+
</execution>
117+
</executions>
118+
</plugin>
91119
<plugin>
92120
<groupId>io.spring.javaformat</groupId>
93121
<artifactId>spring-javaformat-maven-plugin</artifactId>
Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,35 @@
11
package com.shahabkondri.chatgpt.cli;
22

3+
import org.springframework.boot.Banner;
34
import org.springframework.boot.SpringApplication;
45
import org.springframework.boot.autoconfigure.SpringBootApplication;
56
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
7+
import org.springframework.context.ConfigurableApplicationContext;
8+
9+
import java.util.Map;
610

711
/**
12+
* The main entry point of the ChatGPT CLI application. This class configures and starts
13+
* the Spring Boot application, disabling the banner, setting a random server port, and
14+
* scanning for configuration properties from the specified property source.
15+
*
816
* @author Shahab Kondri
917
*/
1018
@SpringBootApplication
1119
@ConfigurationPropertiesScan
1220
public class ChatGptCliApplication {
1321

22+
/**
23+
* The main method that starts the ChatGPT CLI application, configuring and launching
24+
* the Spring Boot application with the specified command line arguments.
25+
* @param args The command line arguments passed to the application.
26+
*/
1427
public static void main(String[] args) {
15-
SpringApplication.exit(SpringApplication.run(ChatGptCliApplication.class, args));
28+
SpringApplication app = new SpringApplication(ChatGptCliApplication.class);
29+
app.setBannerMode(Banner.Mode.OFF);
30+
app.setDefaultProperties(Map.of("server.port", "0"));
31+
ConfigurableApplicationContext context = app.run(args);
32+
SpringApplication.exit(context);
1633
}
1734

1835
}

src/main/java/com/shahabkondri/chatgpt/cli/command/ChatGptCommand.java

Lines changed: 91 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,28 @@
44
import com.shahabkondri.chatgpt.api.model.ChatGptRequest;
55
import com.shahabkondri.chatgpt.api.model.MessageRole;
66
import com.shahabkondri.chatgpt.cli.configuration.ChatGptProperties;
7-
import org.springframework.core.codec.DecodingException;
7+
import com.shahabkondri.chatgpt.cli.shell.Spinner;
8+
import com.shahabkondri.chatgpt.cli.shell.TerminalPrinter;
89
import org.springframework.shell.standard.ShellComponent;
910
import org.springframework.shell.standard.ShellMethod;
1011
import org.springframework.shell.standard.ShellOption;
12+
import reactor.core.publisher.SignalType;
13+
import reactor.core.scheduler.Schedulers;
1114

15+
import java.time.Duration;
1216
import java.util.List;
1317
import java.util.concurrent.CopyOnWriteArrayList;
18+
import java.util.concurrent.CountDownLatch;
19+
import java.util.concurrent.atomic.AtomicBoolean;
20+
import java.util.regex.Matcher;
21+
import java.util.regex.Pattern;
1422

1523
/**
24+
* A {@link ShellComponent} that facilitates user interaction with ChatGPT through the
25+
* terminal. Offers commands for sending messages to the API, processing AI-generated
26+
* responses as a stream, and managing chat history. Streamlines the process of obtaining
27+
* and displaying AI-generated responses in real-time.
28+
*
1629
* @author Shahab Kondri
1730
*/
1831
@ShellComponent
@@ -22,38 +35,100 @@ public class ChatGptCommand {
2235

2336
private final TerminalPrinter terminalPrinter;
2437

38+
private final ChatGptProperties chatGptProperties;
39+
40+
private final Spinner spinner;
41+
2542
private final List<ChatGptRequest.Message> messages = new CopyOnWriteArrayList<>();
2643

27-
private final ChatGptProperties chatGptProperties;
44+
private static final Pattern NEW_LINE_PATTERN = Pattern.compile("\n\n");
45+
46+
private static final Duration CHAT_TIMEOUT = Duration.ofSeconds(30);
2847

48+
/**
49+
* Constructs a new ChatGptCommand with the specified client, printer, properties, and
50+
* spinner.
51+
* @param chatGptClient The ChatGPT client for interacting with the API.
52+
* @param terminalPrinter The terminal printer for printing messages.
53+
* @param chatGptProperties The properties for the ChatGPT API.
54+
* @param spinner The spinner for showing loading state.
55+
*/
2956
public ChatGptCommand(ChatGptClient chatGptClient, TerminalPrinter terminalPrinter,
30-
ChatGptProperties chatGptProperties) {
57+
ChatGptProperties chatGptProperties, Spinner spinner) {
3158
this.chatGptClient = chatGptClient;
3259
this.terminalPrinter = terminalPrinter;
3360
this.chatGptProperties = chatGptProperties;
61+
this.spinner = spinner;
3462
}
3563

36-
@ShellMethod(key = "chat", value = "sends a message to a ChatGPT API client and returns the response.")
37-
public void chat(@ShellOption ChatGptRequest.Message message) {
38-
messages.add(message);
39-
40-
ChatGptRequest request = new ChatGptRequest(chatGptProperties.getModel(), messages);
64+
/**
65+
* Interacts with the ChatGPT API by sending a user message and processing the
66+
* AI-generated response as a stream. To use this command in the terminal, type 'chat'
67+
* or 'c', followed by your message. For example: <pre>
68+
* :> chat Hello ChatGPT, can you help me with my question?
69+
* </pre> or <pre>
70+
* :> c Hello ChatGPT, can you help me with my question?
71+
* </pre>
72+
* @param prompt The user input to send to the ChatGPT API.
73+
*/
74+
@ShellMethod(key = { "chat", "c" }, value = "Interacts with the ChatGPT API by sending a"
75+
+ " user message and processing the AI-generated response as a stream")
76+
public void chat(@ShellOption(arity = Integer.MAX_VALUE) String... prompt) {
77+
spinner.startSpinner();
78+
String message = String.join(" ", prompt);
79+
messages.add(new ChatGptRequest.Message(MessageRole.USER, message));
80+
ChatGptRequest request = new ChatGptRequest(chatGptProperties.model(), messages);
4181

82+
AtomicBoolean isFirstResultPrinted = new AtomicBoolean(false);
4283
StringBuilder builder = new StringBuilder();
84+
CountDownLatch latch = new CountDownLatch(1);
85+
4386
chatGptClient.completions(request).filter(response -> response.choices().get(0).delta().content() != null)
44-
.map(response -> response.choices().get(0).delta().content()).doOnNext(response -> {
45-
terminalPrinter.print(response);
46-
builder.append(response);
47-
}).onErrorContinue((error, o) -> {
48-
if (error instanceof DecodingException && o.toString().contains("[DONE]")) {
87+
.doOnNext(__ -> spinner.stopSpinner())
88+
.map(response -> normalizeOutput(response.choices().get(0).delta().content(), isFirstResultPrinted))
89+
.doOnNext(builder::append).publishOn(Schedulers.parallel()).timeout(CHAT_TIMEOUT).doFinally(signal -> {
90+
terminalPrinter.newLine();
91+
ChatGptRequest.Message assistantMessage = new ChatGptRequest.Message(MessageRole.ASSISTANT,
92+
builder.toString());
93+
messages.add(assistantMessage);
94+
latch.countDown();
95+
96+
// Stop the spinner if the timeout occurred
97+
if (signal == SignalType.ON_ERROR && latch.getCount() == 0) {
98+
spinner.stopSpinner();
99+
terminalPrinter.print("Oops, something went wrong. Please try again.");
49100
terminalPrinter.newLine();
50101
}
51-
}).collectList().block();
102+
}).subscribe(terminalPrinter::print);
103+
try {
104+
latch.await();
105+
}
106+
catch (InterruptedException e) {
107+
Thread.currentThread().interrupt();
108+
}
109+
}
52110

53-
ChatGptRequest.Message assistantMessage = new ChatGptRequest.Message(MessageRole.ASSISTANT, builder.toString());
54-
messages.add(assistantMessage);
111+
/**
112+
* Normalizes the output generated by the ChatGPT API, removing unnecessary new lines.
113+
* @param output The output generated by the ChatGPT API.
114+
* @param isFirstResultPrinted An atomic boolean flag to check if this is the first
115+
* result printed.
116+
* @return The normalized output string.
117+
*/
118+
private static String normalizeOutput(String output, AtomicBoolean isFirstResultPrinted) {
119+
Matcher matcher = NEW_LINE_PATTERN.matcher(output);
120+
if (matcher.matches()) {
121+
if (!isFirstResultPrinted.getAndSet(true)) {
122+
return matcher.replaceAll("");
123+
}
124+
return matcher.replaceAll("\n");
125+
}
126+
return output;
55127
}
56128

129+
/**
130+
* Clears the chat history by removing all messages from the messages list.
131+
*/
57132
@ShellMethod(key = "chat --clear", value = "Clear chat history.")
58133
public void clearChat() {
59134
messages.clear();

0 commit comments

Comments
 (0)