Repository: sashabaranov/go-openai
Pull request #536 (2023-11-07 07:54:09)

Feat Implement threads API
Describe the change
This branch implements [threads API](https://platform.openai.com/docs/api-reference/threads)

Describe your solution
Create a new implementation file dedicated to this API

Tests
tests attached in `thread_test.go`

Issue: https://github.com/sashabaranov/go-openai/issues/533

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/sashabaranov/go-openai/pull/536?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) Report
> Merging [#536](https://app.codecov.io/gh/sashabaranov/go-openai/pull/536?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) (c8603fe) into [master](https://app.codecov.io/gh/sashabaranov/go-openai/commit/d07833e19bfbb2f26011c8881f7fb61366c07e75?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) (d07833e) will **decrease** coverage by `3.36%`.
> The diff coverage is `0.00%`.

```diff
@@            Coverage Diff             @@
##           master     #536      +/-   ##
==========================================
- Coverage   97.59%   94.24%   -3.36%     
==========================================
  Files          19       20       +1     
  Lines         872      903      +31     
==========================================
  Hits          851      851              
- Misses         15       46      +31     
  Partials        6        6              
```


| [Files](https://app.codecov.io/gh/sashabaranov/go-openai/pull/536?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) | Coverage Δ | |
|---|---|---|
| [thread.go](https://app.codecov.io/gh/sashabaranov/go-openai/pull/536?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-dGhyZWFkLmdv) | `0.00% <0.00%> (ø)` | |


henomis: @sashabaranov In this PR I need `withBetaAssistantV1()` from[ this PR](https://github.com/sashabaranov/go-openai/pull/545). So as soon as you merge it I'll be able to complete this one.
vvatanabe: @henomis #545 was merged! 三GO
henomis: @sashabaranov done! 🔥 

Patch:
diff --git a/thread.go b/thread.go
new file mode 100644
index 0000000..291f3dc
--- /dev/null
+++ b/thread.go
@@ -0,0 +1,107 @@
+package openai
+
+import (
+	"context"
+	"net/http"
+)
+
+const (
+	threadsSuffix = "/threads"
+)
+
+type Thread struct {
+	ID        string         `json:"id"`
+	Object    string         `json:"object"`
+	CreatedAt int64          `json:"created_at"`
+	Metadata  map[string]any `json:"metadata"`
+
+	httpHeader
+}
+
+type ThreadRequest struct {
+	Messages []ThreadMessage `json:"messages,omitempty"`
+	Metadata map[string]any  `json:"metadata,omitempty"`
+}
+
+type ModifyThreadRequest struct {
+	Metadata map[string]any `json:"metadata"`
+}
+
+type ThreadMessageRole string
+
+const (
+	ThreadMessageRoleUser ThreadMessageRole = "user"
+)
+
+type ThreadMessage struct {
+	Role     ThreadMessageRole `json:"role"`
+	Content  string            `json:"content"`
+	FileIDs  []string          `json:"file_ids,omitempty"`
+	Metadata map[string]any    `json:"metadata,omitempty"`
+}
+
+type ThreadDeleteResponse struct {
+	ID      string `json:"id"`
+	Object  string `json:"object"`
+	Deleted bool   `json:"deleted"`
+
+	httpHeader
+}
+
+// CreateThread creates a new thread.
+func (c *Client) CreateThread(ctx context.Context, request ThreadRequest) (response Thread, err error) {
+	req, err := c.newRequest(ctx, http.MethodPost, c.fullURL(threadsSuffix), withBody(request),
+		withBetaAssistantV1())
+	if err != nil {
+		return
+	}
+
+	err = c.sendRequest(req, &response)
+	return
+}
+
+// RetrieveThread retrieves a thread.
+func (c *Client) RetrieveThread(ctx context.Context, threadID string) (response Thread, err error) {
+	urlSuffix := threadsSuffix + "/" + threadID
+	req, err := c.newRequest(ctx, http.MethodGet, c.fullURL(urlSuffix),
+		withBetaAssistantV1())
+	if err != nil {
+		return
+	}
+
+	err = c.sendRequest(req, &response)
+	return
+}
+
+// ModifyThread modifies a thread.
+func (c *Client) ModifyThread(
+	ctx context.Context,
+	threadID string,
+	request ModifyThreadRequest,
+) (response Thread, err error) {
+	urlSuffix := threadsSuffix + "/" + threadID
+	req, err := c.newRequest(ctx, http.MethodPost, c.fullURL(urlSuffix), withBody(request),
+		withBetaAssistantV1())
+	if err != nil {
+		return
+	}
+
+	err = c.sendRequest(req, &response)
+	return
+}
+
+// DeleteThread deletes a thread.
+func (c *Client) DeleteThread(
+	ctx context.Context,
+	threadID string,
+) (response ThreadDeleteResponse, err error) {
+	urlSuffix := threadsSuffix + "/" + threadID
+	req, err := c.newRequest(ctx, http.MethodDelete, c.fullURL(urlSuffix),
+		withBetaAssistantV1())
+	if err != nil {
+		return
+	}
+
+	err = c.sendRequest(req, &response)
+	return
+}


Test patch:
diff --git a/client_test.go b/client_test.go
index bff2597..b2f28f9 100644
--- a/client_test.go
+++ b/client_test.go
@@ -301,6 +301,18 @@ func TestClientReturnsRequestBuilderErrors(t *testing.T) {
 		{"DeleteAssistantFile", func() (any, error) {
 			return nil, client.DeleteAssistantFile(ctx, "", "")
 		}},
+		{"CreateThread", func() (any, error) {
+			return client.CreateThread(ctx, ThreadRequest{})
+		}},
+		{"RetrieveThread", func() (any, error) {
+			return client.RetrieveThread(ctx, "")
+		}},
+		{"ModifyThread", func() (any, error) {
+			return client.ModifyThread(ctx, "", ModifyThreadRequest{})
+		}},
+		{"DeleteThread", func() (any, error) {
+			return client.DeleteThread(ctx, "")
+		}},
 	}
 
 	for _, testCase := range testCases {
diff --git a/thread_test.go b/thread_test.go
new file mode 100644
index 0000000..227ab63
--- /dev/null
+++ b/thread_test.go
@@ -0,0 +1,95 @@
+package openai_test
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+	"net/http"
+	"testing"
+
+	openai "github.com/sashabaranov/go-openai"
+	"github.com/sashabaranov/go-openai/internal/test/checks"
+)
+
+// TestThread Tests the thread endpoint of the API using the mocked server.
+func TestThread(t *testing.T) {
+	threadID := "thread_abc123"
+	client, server, teardown := setupOpenAITestServer()
+	defer teardown()
+
+	server.RegisterHandler(
+		"/v1/threads/"+threadID,
+		func(w http.ResponseWriter, r *http.Request) {
+			switch r.Method {
+			case http.MethodGet:
+				resBytes, _ := json.Marshal(openai.Thread{
+					ID:        threadID,
+					Object:    "thread",
+					CreatedAt: 1234567890,
+				})
+				fmt.Fprintln(w, string(resBytes))
+			case http.MethodPost:
+				var request openai.ThreadRequest
+				err := json.NewDecoder(r.Body).Decode(&request)
+				checks.NoError(t, err, "Decode error")
+
+				resBytes, _ := json.Marshal(openai.Thread{
+					ID:        threadID,
+					Object:    "thread",
+					CreatedAt: 1234567890,
+				})
+				fmt.Fprintln(w, string(resBytes))
+			case http.MethodDelete:
+				fmt.Fprintln(w, `{
+					"id": "thread_abc123",
+					"object": "thread.deleted",
+					"deleted": true
+					}`)
+			}
+		},
+	)
+
+	server.RegisterHandler(
+		"/v1/threads",
+		func(w http.ResponseWriter, r *http.Request) {
+			if r.Method == http.MethodPost {
+				var request openai.ModifyThreadRequest
+				err := json.NewDecoder(r.Body).Decode(&request)
+				checks.NoError(t, err, "Decode error")
+
+				resBytes, _ := json.Marshal(openai.Thread{
+					ID:        threadID,
+					Object:    "thread",
+					CreatedAt: 1234567890,
+					Metadata:  request.Metadata,
+				})
+				fmt.Fprintln(w, string(resBytes))
+			}
+		},
+	)
+
+	ctx := context.Background()
+
+	_, err := client.CreateThread(ctx, openai.ThreadRequest{
+		Messages: []openai.ThreadMessage{
+			{
+				Role:    openai.ThreadMessageRoleUser,
+				Content: "Hello, World!",
+			},
+		},
+	})
+	checks.NoError(t, err, "CreateThread error")
+
+	_, err = client.RetrieveThread(ctx, threadID)
+	checks.NoError(t, err, "RetrieveThread error")
+
+	_, err = client.ModifyThread(ctx, threadID, openai.ModifyThreadRequest{
+		Metadata: map[string]interface{}{
+			"key": "value",
+		},
+	})
+	checks.NoError(t, err, "ModifyThread error")
+
+	_, err = client.DeleteThread(ctx, threadID)
+	checks.NoError(t, err, "DeleteThread error")
+}