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

Implementation of communication with OpenAI API for Chat Completion, Text-to-Speech (TTS), and Audio Transcription #1

Merged
merged 13 commits into from
May 7, 2024
Merged
36 changes: 36 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -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
5 changes: 5 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
test:
go test -v ./...

run:
go run ./example/*.go
42 changes: 42 additions & 0 deletions client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package openai

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
}
83 changes: 83 additions & 0 deletions completion.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package openai

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(api OpenAIClient, httpClient HTTPClient, body *CompletionRequest) (*CompletionResponse, error) {
api.AddHeader(contentTypeJSON)
b, err := json.Marshal(body)
if err != nil {
return nil, err
}
options := &goxios.RequestOpts{
Headers: Headers(),
Body: bytes.NewBuffer(b),
}
res, err := httpClient.Post(api.BaseURL()+"/chat/completions", options)
if err != nil {
return nil, err
}
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
}
80 changes: 80 additions & 0 deletions completion_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package openai

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)
}
}
46 changes: 46 additions & 0 deletions example/audio.go
Original file line number Diff line number Diff line change
@@ -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)
}
22 changes: 22 additions & 0 deletions example/chat.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package main

import (
"log"

openai "github.com/Simplou/openai"
)

func chat() {
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 {
panic(err)
}
log.Println(res.Choices[0].Message.Content)
}
22 changes: 22 additions & 0 deletions example/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package 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()
tts()
whisper()
}
5 changes: 5 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module github.com/Simplou/openai

go 1.22.0

require github.com/Simplou/goxios v0.0.0-20240429182252-1d8cfb866de1
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -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=
22 changes: 22 additions & 0 deletions headers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package openai

import (
"github.com/Simplou/goxios"
)

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})
}

func (c *Client) AddHeader(h goxios.Header) {
headers = append(headers, h)
}

func Headers() []goxios.Header {
return headers
}
Binary file added temp/hello.mp3
Binary file not shown.
Loading