Skip to content

Commit 6ba9483

Browse files
committed
Merge branch 'master' into exception-wrappers
2 parents b695c84 + 3e060b8 commit 6ba9483

File tree

5 files changed

+414
-4
lines changed

5 files changed

+414
-4
lines changed

.github/workflows/readme-version.yml

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
name: Update README Version
2+
3+
on:
4+
release:
5+
types:
6+
- published
7+
workflow_dispatch:
8+
9+
jobs:
10+
update-version:
11+
runs-on: ubuntu-latest
12+
13+
steps:
14+
- name: Checkout repository
15+
uses: actions/checkout@v2
16+
17+
- name: Set up Python
18+
uses: actions/setup-python@v2
19+
with:
20+
python-version: '3.x'
21+
22+
- name: Update version in README
23+
run: |
24+
# Extract the version from the build.gradle.kts file
25+
VERSION=$(grep -oP 'version\s*=\s*"\K\d+\.\d+\.\d+' build.gradle.kts)
26+
27+
# Update the README.md file
28+
export VERSION
29+
python -c "import re, os; version=os.environ['VERSION']; content=open('README.md').read(); content=re.sub(r'\d+\.\d+\.\d+', f'{version}', content); open('README.md', 'w').write(content)"
30+
31+
- name: Commit and push changes
32+
run: |
33+
git config --global user.name "GitHub Actions Bot"
34+
git config --global user.email "[email protected]"
35+
git remote set-url origin https://${{ secrets.PAT }}@github.com/CJCrafter/ChatGPT-Java-API.git
36+
git add README.md
37+
git diff --quiet && git diff --staged --quiet || (git commit -m "Update version in README.md" && git push)

build.gradle.kts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import com.github.breadmoirai.githubreleaseplugin.GithubReleaseTask
22

33
group = "com.cjcrafter"
4-
version = "1.2.0"
4+
version = "1.2.1"
55

66
plugins {
77
`java-library`
@@ -60,6 +60,7 @@ val sourcesJar by tasks.registering(Jar::class) {
6060
}
6161

6262
nexusStaging {
63+
serverUrl = "https://s01.oss.sonatype.org/service/local/"
6364
packageGroup = "com.cjcrafter"
6465
stagingProfileId = findProperty("OSSRH_ID").toString()
6566
username = findProperty("OSSRH_USERNAME").toString()
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
package com.cjcrafter.openai
2+
3+
import com.cjcrafter.openai.chat.ChatRequest
4+
import com.cjcrafter.openai.chat.ChatResponse
5+
import com.cjcrafter.openai.chat.ChatResponseChunk
6+
import com.cjcrafter.openai.chat.ChatUser
7+
import com.google.gson.Gson
8+
import com.google.gson.GsonBuilder
9+
import com.google.gson.JsonObject
10+
import com.google.gson.JsonParser
11+
import com.google.gson.JsonSerializer
12+
import okhttp3.*
13+
import okhttp3.MediaType.Companion.toMediaType
14+
import okhttp3.RequestBody.Companion.toRequestBody
15+
import java.io.IOException
16+
import java.lang.IllegalArgumentException
17+
import java.util.function.Consumer
18+
19+
/**
20+
* To get your API key:
21+
* 1. Log in to your account: Go to [https://www.openai.com/](openai.com) and
22+
* log in.
23+
* 2. Access the API dashboard: After logging in, click on the "API" tab.
24+
* 3. Choose a subscription plan: Select a suitable plan based on your needs
25+
* and complete the payment process.
26+
* 4. Obtain your API key: After subscribing to a plan, you will be redirected
27+
* to the API dashboard, where you can find your unique API key. Copy and store it securely.
28+
*
29+
* @property apiKey Your OpenAI API key. It starts with `"sk-"` (without the quotes).
30+
* @property organization If you belong to multiple organizations, specify which one to use (else `null`).
31+
* @property client Controls proxies, timeouts, etc.
32+
* @constructor Create a ChatBot for responding to requests.
33+
*/
34+
class OpenAI @JvmOverloads constructor(
35+
private val apiKey: String,
36+
private val organization: String? = null,
37+
private val client: OkHttpClient = OkHttpClient()
38+
) {
39+
40+
private val mediaType: MediaType = "application/json; charset=utf-8".toMediaType()
41+
private val gson: Gson = GsonBuilder()
42+
.registerTypeAdapter(ChatUser::class.java, JsonSerializer<ChatUser> { src, _, context -> context!!.serialize(src!!.name.lowercase())!! })
43+
.create()
44+
45+
private fun buildRequest(request: Any): Request {
46+
val json = gson.toJson(request)
47+
val body: RequestBody = json.toRequestBody(mediaType)
48+
return Request.Builder()
49+
.url("https://api.openai.com/v1/chat/completions")
50+
.addHeader("Content-Type", "application/json")
51+
.addHeader("Authorization", "Bearer $apiKey")
52+
.apply { if (organization != null) addHeader("OpenAI-Organization", organization) }
53+
.post(body).build()
54+
}
55+
56+
/**
57+
* Blocks the current thread until OpenAI responds to https request. The
58+
* returned value includes information including tokens, generated text,
59+
* and stop reason. You can access the generated message through
60+
* [ChatResponse.choices].
61+
*
62+
* @param request The input information for ChatGPT.
63+
* @return The returned response.
64+
* @throws IOException If an IO Exception occurs.
65+
* @throws IllegalArgumentException If the input arguments are invalid.
66+
*/
67+
@Throws(IOException::class)
68+
fun generateResponse(request: ChatRequest): ChatResponse {
69+
request.stream = false // use streamResponse for stream=true
70+
val httpRequest = buildRequest(request)
71+
72+
// Save the JsonObject to check for errors
73+
var rootObject: JsonObject? = null
74+
try {
75+
client.newCall(httpRequest).execute().use { response ->
76+
77+
// Servers respond to API calls with json blocks. Since raw JSON isn't
78+
// very developer friendly, we wrap for easy data access.
79+
rootObject = JsonParser.parseString(response.body!!.string()).asJsonObject
80+
require(!rootObject!!.has("error")) { rootObject!!.get("error").asJsonObject["message"].asString }
81+
return ChatResponse(rootObject!!)
82+
}
83+
} catch (ex: Throwable) {
84+
throw ex
85+
}
86+
}
87+
88+
/**
89+
* This is a helper method that calls [streamResponse], which lets you use
90+
* the generated tokens in real time (As ChatGPT generates them).
91+
*
92+
* This method does not block the thread. Method calls to [onResponse] are
93+
* not handled by the main thread. It is crucial to consider thread safety
94+
* within the context of your program.
95+
*
96+
* Usage:
97+
* ```
98+
* val messages = mutableListOf("Write a poem".toUserMessage())
99+
* val request = ChatRequest("gpt-3.5-turbo", messages)
100+
* val bot = ChatBot(/* your key */)
101+
102+
* bot.streamResponseKotlin(request) {
103+
* print(choices[0].delta)
104+
*
105+
* // when finishReason != null, this is the last message (done generating new tokens)
106+
* if (choices[0].finishReason != null)
107+
* messages.add(choices[0].message)
108+
* }
109+
* ```
110+
*
111+
* @param request The input information for ChatGPT.
112+
* @param onResponse The method to call for each chunk.
113+
* @since 1.2.0
114+
*/
115+
fun streamResponseKotlin(request: ChatRequest, onResponse: ChatResponseChunk.() -> Unit) {
116+
streamResponse(request, { it.onResponse() })
117+
}
118+
119+
/**
120+
* Uses ChatGPT to generate tokens in real time. As ChatGPT generates
121+
* content, those tokens are sent in a stream in real time. This allows you
122+
* to update the user without long delays between their input and OpenAI's
123+
* response.
124+
*
125+
* For *"simpler"* calls, you can use [generateResponse] which will block
126+
* the thread until the entire response is generated.
127+
*
128+
* Instead of using the [ChatResponse], this method uses [ChatResponseChunk].
129+
* This means that it is not possible to retrieve the number of tokens from
130+
* this method,
131+
*
132+
* This method does not block the thread. Method calls to [onResponse] are
133+
* not handled by the main thread. It is crucial to consider thread safety
134+
* within the context of your program.
135+
*
136+
* @param request The input information for ChatGPT.
137+
* @param onResponse The method to call for each chunk.
138+
* @param onFailure The method to call if the HTTP fails. This method will
139+
* not be called if OpenAI returns an error.
140+
* @see generateResponse
141+
* @see streamResponseKotlin
142+
* @since 1.2.0
143+
*/
144+
@JvmOverloads
145+
fun streamResponse(
146+
request: ChatRequest,
147+
onResponse: Consumer<ChatResponseChunk>, // use Consumer instead of Kotlin for better Java syntax
148+
onFailure: Consumer<IOException> = Consumer { it.printStackTrace() }
149+
) {
150+
request.stream = true // use requestResponse for stream=false
151+
val httpRequest = buildRequest(request)
152+
153+
client.newCall(httpRequest).enqueue(object : Callback {
154+
var cache: ChatResponseChunk? = null
155+
156+
override fun onFailure(call: Call, e: IOException) {
157+
onFailure.accept(e)
158+
}
159+
160+
override fun onResponse(call: Call, response: Response) {
161+
response.body?.source()?.use { source ->
162+
while (!source.exhausted()) {
163+
164+
// Parse the JSON string as a map. Every string starts
165+
// with "data: ", so we need to remove that.
166+
var jsonResponse = source.readUtf8Line() ?: continue
167+
if (jsonResponse.isEmpty())
168+
continue
169+
jsonResponse = jsonResponse.substring("data: ".length)
170+
if (jsonResponse == "[DONE]")
171+
continue
172+
173+
val rootObject = JsonParser.parseString(jsonResponse).asJsonObject
174+
if (cache == null)
175+
cache = ChatResponseChunk(rootObject)
176+
else
177+
cache!!.update(rootObject)
178+
179+
onResponse.accept(cache!!)
180+
}
181+
}
182+
}
183+
})
184+
}
185+
}

src/main/kotlin/com/cjcrafter/openai/chat/ChatBot.kt

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,14 @@ import java.util.function.Consumer
2929
* to the API dashboard, where you can find your unique API key. Copy and store it securely.
3030
*
3131
* @property apiKey Your OpenAI API key. It starts with `"sk-"` (without the quotes).
32+
* @property organization If you belong to multiple organizations, specify which one to use (else `null`).
3233
* @property client Controls proxies, timeouts, etc.
3334
* @constructor Create a ChatBot for responding to requests.
3435
*/
36+
@Deprecated(level = DeprecationLevel.ERROR, message = "Use com.cjcrafter.openai.OpenAI")
3537
class ChatBot @JvmOverloads constructor(
3638
private val apiKey: String,
39+
private val organization: String? = null,
3740
private val client: OkHttpClient = OkHttpClient()
3841
) {
3942

@@ -49,6 +52,7 @@ class ChatBot @JvmOverloads constructor(
4952
.url("https://api.openai.com/v1/chat/completions")
5053
.addHeader("Content-Type", "application/json")
5154
.addHeader("Authorization", "Bearer $apiKey")
55+
.apply { if (organization != null) addHeader("OpenAI-Organization", organization) }
5256
.post(body).build()
5357
}
5458

@@ -91,6 +95,21 @@ class ChatBot @JvmOverloads constructor(
9195
* not handled by the main thread. It is crucial to consider thread safety
9296
* within the context of your program.
9397
*
98+
* Usage:
99+
* ```
100+
* val messages = mutableListOf("Write a poem".toUserMessage())
101+
* val request = ChatRequest("gpt-3.5-turbo", messages)
102+
* val bot = ChatBot(/* your key */)
103+
104+
* bot.streamResponseKotlin(request) {
105+
* print(choices[0].delta)
106+
*
107+
* // when finishReason != null, this is the last message (done generating new tokens)
108+
* if (choices[0].finishReason != null)
109+
* messages.add(choices[0].message)
110+
* }
111+
* ```
112+
*
94113
* @param request The input information for ChatGPT.
95114
* @param onResponse The method to call for each chunk.
96115
* @since 1.2.0

0 commit comments

Comments
 (0)