From bc32664985aa146d517eda42d4f9e5559771f8cd Mon Sep 17 00:00:00 2001 From: gabrielluizsf Date: Mon, 6 May 2024 16:41:27 -0300 Subject: [PATCH 01/13] go mod init/tidy --- go.mod | 5 +++++ go.sum | 2 ++ 2 files changed, 7 insertions(+) create mode 100644 go.mod create mode 100644 go.sum diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..26e0031 --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module github.com/Simplou/openai-go + +go 1.22.0 + +require github.com/Simplou/goxios v0.0.0-20240429182252-1d8cfb866de1 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..7c7d897 --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +github.com/Simplou/goxios v0.0.0-20240429182252-1d8cfb866de1 h1:HoSNW9B/kcP3H1Eqz9i+aesSEPUjM9gIDcPxDIPXf4Y= +github.com/Simplou/goxios v0.0.0-20240429182252-1d8cfb866de1/go.mod h1:Zi5vYOgi0Rm3eO67OO6rkg0MlrFYv6Z2P5cO3N5sJa0= From 591b904213d6e4d4b42fe2cee1a3f595f6fa4fdf Mon Sep 17 00:00:00 2001 From: gabrielluizsf Date: Mon, 6 May 2024 16:43:02 -0300 Subject: [PATCH 02/13] openai client implemented --- client.go | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 client.go diff --git a/client.go b/client.go new file mode 100644 index 0000000..7e4cd4e --- /dev/null +++ b/client.go @@ -0,0 +1,42 @@ +package openaigo + +import ( + "context" + "net/http" + + "github.com/Simplou/goxios" +) + +type Client struct { + ctx context.Context + apiKey string +} + +type OpenAIClient interface { + Context() context.Context + ApiKey() string + BaseURL() string + AddHeader(goxios.Header) +} + +type HTTPClient interface { + Post(string, *goxios.RequestOpts) (*http.Response, error) +} + +func (c *Client) BaseURL() string { + return "https://api.openai.com/v1" +} + +func (c *Client) Context() context.Context { + return c.ctx +} + +func (c *Client) ApiKey() string { + return c.apiKey +} + +func New(ctx context.Context, apiKey string) *Client { + openaiClient := &Client{ctx, apiKey} + openaiClient.setAuthorizationHeader() + return openaiClient +} From 2fb73c759f98c99dad739af22ced2fd147c9ba17 Mon Sep 17 00:00:00 2001 From: gabrielluizsf Date: Mon, 6 May 2024 16:43:20 -0300 Subject: [PATCH 03/13] openai request headers implemented --- headers.go | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 headers.go diff --git a/headers.go b/headers.go new file mode 100644 index 0000000..da8dc0f --- /dev/null +++ b/headers.go @@ -0,0 +1,19 @@ +package openaigo + +import ( + "github.com/Simplou/goxios" +) + +var headers = []goxios.Header{} + +func (c *Client) setAuthorizationHeader(){ + headers = append(headers, goxios.Header{Key: "Authorization", Value: "Bearer "+c.apiKey}) +} + +func (c *Client) AddHeader(h goxios.Header) { + headers = append(headers, h) +} + +func Headers() []goxios.Header { + return headers +} From 89b8f469caaf71a9c3bcb9eb8315295b2900c835 Mon Sep 17 00:00:00 2001 From: gabrielluizsf Date: Mon, 6 May 2024 16:43:41 -0300 Subject: [PATCH 04/13] openai chat completion implemented --- completion.go | 84 ++++++++++++++++++++++++++++++++++++++++++++++ completion_test.go | 80 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 164 insertions(+) create mode 100644 completion.go create mode 100644 completion_test.go diff --git a/completion.go b/completion.go new file mode 100644 index 0000000..e9f9196 --- /dev/null +++ b/completion.go @@ -0,0 +1,84 @@ +package openaigo + +import ( + "bytes" + "encoding/json" + + "github.com/Simplou/goxios" +) + +// CompletionRequest represents the structure of the request sent to the OpenAI API. +type CompletionRequest struct { + Model string `json:"model"` + Messages []Message `json:"messages"` + ToolChoice string `json:"tool_choice,omitempty"` + Tools []Tool `json:"tools,omitempty"` +} + +// Message represents a message in the conversation. +type Message struct { + Role string `json:"role"` + Content string `json:"content"` +} + +// Tool represents a tool that can be used during the conversation. +type Tool struct { + Type string `json:"type"` + Function Function `json:"function,omitempty"` +} + +// Function represents a function call that can be used as a tool. +type Function struct { + Name string `json:"name"` +} + +// CompletionResponse represents the structure of the response received from the OpenAI API. +type CompletionResponse struct { + ID string `json:"id"` + Object string `json:"object"` + Created int64 `json:"created"` + Model string `json:"model"` + Choices []Choice `json:"choices"` + Usage Usage `json:"usage"` +} + +// Choice represents a response choice in the conversation. +type Choice struct { + Index int `json:"index"` + Message Message `json:"message"` + Logprobs interface{} `json:"logprobs,omitempty"` + FinishReason string `json:"finish_reason"` +} + +// Usage represents the token usage in the request and response. +type Usage struct { + PromptTokens int `json:"prompt_tokens"` + CompletionTokens int `json:"completion_tokens"` + TotalTokens int `json:"total_tokens"` +} + +func ChatCompletion(client OpenAIClient, httpClient HTTPClient, body *CompletionRequest) (*CompletionResponse, error) { + client.AddHeader(goxios.Header{Key: "Content-Type", Value: "application/json"}) + b, err := json.Marshal(body) + if err != nil { + return nil, err + } + options := &goxios.RequestOpts{ + Headers: Headers(), + Body: bytes.NewBuffer(b), + } + res, err := httpClient.Post(client.BaseURL()+"/chat/completions", options) + if err != nil { + return nil, err + } + defer res.Request.Body.Close() + response := new(CompletionResponse) + if err := goxios.DecodeJSON(res.Body, response); err != nil { + return nil, err + } + + if err := res.Body.Close(); err != nil { + return nil, err + } + return response, nil +} diff --git a/completion_test.go b/completion_test.go new file mode 100644 index 0000000..a9fb955 --- /dev/null +++ b/completion_test.go @@ -0,0 +1,80 @@ +package openaigo + +import ( + "bytes" + "context" + "io" + "net/http" + "testing" + + "github.com/Simplou/goxios" +) + +type MockClient struct { + baseUrl string +} + +func (c MockClient) Context() context.Context { + return context.TODO() +} +func (c MockClient) ApiKey() string { + return "mock_api_key" +} +func (c MockClient) BaseURL() string { + return c.baseUrl +} + +func (c MockClient) AddHeader(h goxios.Header) {} + +type testHTTPClient struct { + req *http.Request +} + +func (c *testHTTPClient) Post(url string, opts *goxios.RequestOpts) (*http.Response, error) { + statusCode := http.StatusOK + body := goxios.JSON{ + "id": "123", + "model": "gpt-3.5-turbo", + "choices": []Choice{ + { + Index: 0, + Message: Message{Role: "assistant", Content: "Hi"}, + FinishReason: "", + }, + }, + } + resBytes, err := body.Marshal() + if err != nil { + return nil, err + } + resReader := bytes.NewBuffer(resBytes) + res := &http.Response{ + Request: c.req, + Status: http.StatusText(statusCode), + StatusCode: statusCode, + Header: http.Header{}, + Body: io.NopCloser(resReader), + ContentLength: int64(len(resBytes)), + } + + return res, nil +} + +func TestChatCompletionRequest(t *testing.T) { + mockClient := MockClient{"http://localhost:399317"} + httpClient := testHTTPClient{} + completionRequest := &CompletionRequest{ + Model: "gpt-3.5-turbo", + Messages: []Message{{Role: "user", Content: "Hello!"}}, + } + + response, err := ChatCompletion(mockClient, &httpClient, completionRequest) + if err != nil { + t.Errorf("Erro ao chamar ChatCompletion: %v", err) + } + + expectedID := "123" + if response.ID != expectedID { + t.Errorf("ID da resposta esperado: %s, ID recebido: %s", expectedID, response.ID) + } +} From ee6e1a5920d7f65a299ccc2061aeea976697b9be Mon Sep 17 00:00:00 2001 From: gabrielluizsf Date: Mon, 6 May 2024 16:43:55 -0300 Subject: [PATCH 05/13] chat completion example --- example/chat.go | 31 +++++++++++++++++++++++++++++++ example/main.go | 5 +++++ 2 files changed, 36 insertions(+) create mode 100644 example/chat.go create mode 100644 example/main.go diff --git a/example/chat.go b/example/chat.go new file mode 100644 index 0000000..8766677 --- /dev/null +++ b/example/chat.go @@ -0,0 +1,31 @@ +package main + +import ( + "context" + "log" + "os" + + "github.com/Simplou/goxios" + openaigo "github.com/Simplou/openai-go" +) + +func chat() { + var ( + ctx = context.Background() + apiKey = os.Getenv("OPENAI_KEY") + client = openaigo.New(ctx, apiKey) + httpClient = goxios.New(ctx) + ) + body := &openaigo.CompletionRequest{ + Model: "gpt-3.5-turbo", + Messages: []openaigo.Message{ + {Role: "user", Content: "Hello"}, + }, + } + + res, err := openaigo.ChatCompletion(client, httpClient, body) + if err != nil{ + panic(err) + } + log.Println(res.Choices[0].Message.Content) +} diff --git a/example/main.go b/example/main.go new file mode 100644 index 0000000..1a87fa1 --- /dev/null +++ b/example/main.go @@ -0,0 +1,5 @@ +package main + +func main(){ + chat() +} \ No newline at end of file From 5c88a0c2ce8ae417e5283ed5e6a472633ea67552 Mon Sep 17 00:00:00 2001 From: gabrielluizsf Date: Mon, 6 May 2024 16:44:54 -0300 Subject: [PATCH 06/13] Add Makefile for project setup --- Makefile | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 Makefile diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..581ed9e --- /dev/null +++ b/Makefile @@ -0,0 +1,5 @@ +test: + go test -v ./... + +run: + go run ./example/*.go \ No newline at end of file From 9beb7a23d465d4cad0773e9aeeda190eb92fce30 Mon Sep 17 00:00:00 2001 From: gabrielluizsf Date: Mon, 6 May 2024 16:49:22 -0300 Subject: [PATCH 07/13] Adding GitHub Actions workflow file --- .github/workflows/tests.yml | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 .github/workflows/tests.yml diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..22cd2c2 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,36 @@ +name: Run Tests + +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - name: Configuring environment + uses: actions/setup-go@v2 + with: + go-version: ^1.22.0 + + - name: Cloning repository + uses: actions/checkout@v2 + + - name: Run all openai-go tests + run: go test ./... + + - name: Create Release + id: create_release + uses: actions/create-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: v1.${{ github.run_number }} + release_name: Release v1.${{ github.run_number }} + draft: false + prerelease: false From 852f4927fd80066474677394d0bf1b47cfd05d2b Mon Sep 17 00:00:00 2001 From: gabrielluizsf Date: Mon, 6 May 2024 16:51:38 -0300 Subject: [PATCH 08/13] fix: invalid memory address or nil pointer dereference [recovered] --- completion.go | 1 - 1 file changed, 1 deletion(-) diff --git a/completion.go b/completion.go index e9f9196..bb142d5 100644 --- a/completion.go +++ b/completion.go @@ -71,7 +71,6 @@ func ChatCompletion(client OpenAIClient, httpClient HTTPClient, body *Completion if err != nil { return nil, err } - defer res.Request.Body.Close() response := new(CompletionResponse) if err := goxios.DecodeJSON(res.Body, response); err != nil { return nil, err From 40e3eb830e8d1af6e2dc0d99d277d562cf055ae0 Mon Sep 17 00:00:00 2001 From: gabrielluizsf Date: Tue, 7 May 2024 08:17:35 -0300 Subject: [PATCH 09/13] change package name --- client.go | 2 +- completion.go | 2 +- completion_test.go | 2 +- example/chat.go | 10 +++++----- go.mod | 2 +- headers.go | 6 +++--- 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/client.go b/client.go index 7e4cd4e..1abf164 100644 --- a/client.go +++ b/client.go @@ -1,4 +1,4 @@ -package openaigo +package openai import ( "context" diff --git a/completion.go b/completion.go index bb142d5..b15339a 100644 --- a/completion.go +++ b/completion.go @@ -1,4 +1,4 @@ -package openaigo +package openai import ( "bytes" diff --git a/completion_test.go b/completion_test.go index a9fb955..384ab49 100644 --- a/completion_test.go +++ b/completion_test.go @@ -1,4 +1,4 @@ -package openaigo +package openai import ( "bytes" diff --git a/example/chat.go b/example/chat.go index 8766677..c95b7b7 100644 --- a/example/chat.go +++ b/example/chat.go @@ -6,24 +6,24 @@ import ( "os" "github.com/Simplou/goxios" - openaigo "github.com/Simplou/openai-go" + openai "github.com/Simplou/openai" ) func chat() { var ( ctx = context.Background() apiKey = os.Getenv("OPENAI_KEY") - client = openaigo.New(ctx, apiKey) + client = openai.New(ctx, apiKey) httpClient = goxios.New(ctx) ) - body := &openaigo.CompletionRequest{ + body := &openai.CompletionRequest{ Model: "gpt-3.5-turbo", - Messages: []openaigo.Message{ + Messages: []openai.Message{ {Role: "user", Content: "Hello"}, }, } - res, err := openaigo.ChatCompletion(client, httpClient, body) + res, err := openai.ChatCompletion(client, httpClient, body) if err != nil{ panic(err) } diff --git a/go.mod b/go.mod index 26e0031..07af9a2 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/Simplou/openai-go +module github.com/Simplou/openai go 1.22.0 diff --git a/headers.go b/headers.go index da8dc0f..6e94ba7 100644 --- a/headers.go +++ b/headers.go @@ -1,4 +1,4 @@ -package openaigo +package openai import ( "github.com/Simplou/goxios" @@ -6,8 +6,8 @@ import ( var headers = []goxios.Header{} -func (c *Client) setAuthorizationHeader(){ - headers = append(headers, goxios.Header{Key: "Authorization", Value: "Bearer "+c.apiKey}) +func (c *Client) setAuthorizationHeader() { + headers = append(headers, goxios.Header{Key: "Authorization", Value: "Bearer " + c.apiKey}) } func (c *Client) AddHeader(h goxios.Header) { From f1b8997979603a7a5a1941a1c1a7c5eab7adf98c Mon Sep 17 00:00:00 2001 From: gabrielluizsf Date: Tue, 7 May 2024 08:59:24 -0300 Subject: [PATCH 10/13] defining variable to use in different functions --- completion.go | 2 +- headers.go | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/completion.go b/completion.go index b15339a..de577dd 100644 --- a/completion.go +++ b/completion.go @@ -58,7 +58,7 @@ type Usage struct { } func ChatCompletion(client OpenAIClient, httpClient HTTPClient, body *CompletionRequest) (*CompletionResponse, error) { - client.AddHeader(goxios.Header{Key: "Content-Type", Value: "application/json"}) + client.AddHeader(contentTypeJSON) b, err := json.Marshal(body) if err != nil { return nil, err diff --git a/headers.go b/headers.go index 6e94ba7..9ee6d20 100644 --- a/headers.go +++ b/headers.go @@ -4,7 +4,10 @@ import ( "github.com/Simplou/goxios" ) -var headers = []goxios.Header{} +var ( + headers = []goxios.Header{} + contentTypeJSON = goxios.Header{Key: "Content-Type", Value: "application/json"} +) func (c *Client) setAuthorizationHeader() { headers = append(headers, goxios.Header{Key: "Authorization", Value: "Bearer " + c.apiKey}) From 35c91ada2e6c2f465b506babe1545a4eff25854c Mon Sep 17 00:00:00 2001 From: gabrielluizsf Date: Tue, 7 May 2024 09:01:24 -0300 Subject: [PATCH 11/13] gofmt --- headers.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/headers.go b/headers.go index 9ee6d20..089e931 100644 --- a/headers.go +++ b/headers.go @@ -5,7 +5,7 @@ import ( ) var ( - headers = []goxios.Header{} + headers = []goxios.Header{} contentTypeJSON = goxios.Header{Key: "Content-Type", Value: "application/json"} ) From 3be64256cf4cf6bc95f2243a91a457d457cfbe34 Mon Sep 17 00:00:00 2001 From: gabrielluizsf Date: Tue, 7 May 2024 10:07:33 -0300 Subject: [PATCH 12/13] Refactoring the ChatCompletion function to use the 'api' parameter instead of client --- completion.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/completion.go b/completion.go index de577dd..3eda512 100644 --- a/completion.go +++ b/completion.go @@ -57,8 +57,8 @@ type Usage struct { TotalTokens int `json:"total_tokens"` } -func ChatCompletion(client OpenAIClient, httpClient HTTPClient, body *CompletionRequest) (*CompletionResponse, error) { - client.AddHeader(contentTypeJSON) +func ChatCompletion(api OpenAIClient, httpClient HTTPClient, body *CompletionRequest) (*CompletionResponse, error) { + api.AddHeader(contentTypeJSON) b, err := json.Marshal(body) if err != nil { return nil, err @@ -67,7 +67,7 @@ func ChatCompletion(client OpenAIClient, httpClient HTTPClient, body *Completion Headers: Headers(), Body: bytes.NewBuffer(b), } - res, err := httpClient.Post(client.BaseURL()+"/chat/completions", options) + res, err := httpClient.Post(api.BaseURL()+"/chat/completions", options) if err != nil { return nil, err } From 8c77466d04359627c4ee3544208e0e5490c753c0 Mon Sep 17 00:00:00 2001 From: gabrielluizsf Date: Tue, 7 May 2024 10:34:55 -0300 Subject: [PATCH 13/13] audio apis implemented --- example/audio.go | 46 +++++++++++++++++++++++++++ example/chat.go | 15 ++------- example/main.go | 21 +++++++++++-- temp/hello.mp3 | Bin 0 -> 10560 bytes tts.go | 79 +++++++++++++++++++++++++++++++++++++++++++++++ whisper.go | 73 +++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 220 insertions(+), 14 deletions(-) create mode 100644 example/audio.go create mode 100755 temp/hello.mp3 create mode 100644 tts.go create mode 100644 whisper.go diff --git a/example/audio.go b/example/audio.go new file mode 100644 index 0000000..9fa25ec --- /dev/null +++ b/example/audio.go @@ -0,0 +1,46 @@ +package main + +import ( + "fmt" + "io" + "os" + + "github.com/Simplou/openai" +) + +const fileName = "hello.mp3" + +var ( + audioFilePath = fmt.Sprintf("./temp/%s", fileName) +) + +func tts() { + audio, err := openai.TextToSpeech(client, httpClient, &openai.SpeechRequestBody{ + Model: "tts-1", + Input: "Hello", + Voice: openai.SpeechVoices.Onyx, + }) + if err != nil { + panic(err) + } + defer audio.Close() + b, err := io.ReadAll(audio) + if err != nil { + panic(err) + } + if err := os.WriteFile(audioFilePath, b, os.ModePerm); err != nil { + panic(err) + } +} + +func whisper() { + transcription, err := openai.Transcription(client, httpClient, &openai.TranscriptionsRequestBody{ + Model: openai.DefaultTranscriptionModel, + Filename: fileName, + AudioFilePath: audioFilePath, + }) + if err != nil{ + panic(err) + } + println(transcription.Text) +} diff --git a/example/chat.go b/example/chat.go index c95b7b7..b2ab988 100644 --- a/example/chat.go +++ b/example/chat.go @@ -1,30 +1,21 @@ package main import ( - "context" "log" - "os" - "github.com/Simplou/goxios" openai "github.com/Simplou/openai" ) func chat() { - var ( - ctx = context.Background() - apiKey = os.Getenv("OPENAI_KEY") - client = openai.New(ctx, apiKey) - httpClient = goxios.New(ctx) - ) - body := &openai.CompletionRequest{ + body := &openai.CompletionRequest{ Model: "gpt-3.5-turbo", Messages: []openai.Message{ {Role: "user", Content: "Hello"}, }, } - + res, err := openai.ChatCompletion(client, httpClient, body) - if err != nil{ + if err != nil { panic(err) } log.Println(res.Choices[0].Message.Content) diff --git a/example/main.go b/example/main.go index 1a87fa1..212080f 100644 --- a/example/main.go +++ b/example/main.go @@ -1,5 +1,22 @@ package main -func main(){ +import ( + "context" + "os" + + "github.com/Simplou/goxios" + openai "github.com/Simplou/openai" +) + +var ( + ctx = context.Background() + apiKey = os.Getenv("OPENAI_KEY") + client = openai.New(ctx, apiKey) + httpClient = goxios.New(ctx) +) + +func main() { chat() -} \ No newline at end of file + tts() + whisper() +} diff --git a/temp/hello.mp3 b/temp/hello.mp3 new file mode 100755 index 0000000000000000000000000000000000000000..c60b2fda930b7b74c35adebbb496c9b3bb8aca00 GIT binary patch literal 10560 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