From d6f3bdcdac9172ab5248d6be8c3e1761446a434c Mon Sep 17 00:00:00 2001 From: Simone Vellei Date: Thu, 9 Nov 2023 20:17:30 +0100 Subject: [PATCH] Feat implement Run APIs (#560) * chore: first commit * add apis * chore: add tests * feat add apis * chore: add api and tests * chore: add tests * fix * trigger build * fix * chore: formatting code * chore: add pagination type --- client_test.go | 27 ++++ run.go | 399 +++++++++++++++++++++++++++++++++++++++++++++++++ run_test.go | 237 +++++++++++++++++++++++++++++ 3 files changed, 663 insertions(+) create mode 100644 run.go create mode 100644 run_test.go diff --git a/client_test.go b/client_test.go index b2f28f9..d5d3e26 100644 --- a/client_test.go +++ b/client_test.go @@ -313,6 +313,33 @@ func TestClientReturnsRequestBuilderErrors(t *testing.T) { {"DeleteThread", func() (any, error) { return client.DeleteThread(ctx, "") }}, + {"CreateRun", func() (any, error) { + return client.CreateRun(ctx, "", RunRequest{}) + }}, + {"RetrieveRun", func() (any, error) { + return client.RetrieveRun(ctx, "", "") + }}, + {"ModifyRun", func() (any, error) { + return client.ModifyRun(ctx, "", "", RunModifyRequest{}) + }}, + {"ListRuns", func() (any, error) { + return client.ListRuns(ctx, "", Pagination{}) + }}, + {"SubmitToolOutputs", func() (any, error) { + return client.SubmitToolOutputs(ctx, "", "", SubmitToolOutputsRequest{}) + }}, + {"CancelRun", func() (any, error) { + return client.CancelRun(ctx, "", "") + }}, + {"CreateThreadAndRun", func() (any, error) { + return client.CreateThreadAndRun(ctx, CreateThreadAndRunRequest{}) + }}, + {"RetrieveRunStep", func() (any, error) { + return client.RetrieveRunStep(ctx, "", "", "") + }}, + {"ListRunSteps", func() (any, error) { + return client.ListRunSteps(ctx, "", "", Pagination{}) + }}, } for _, testCase := range testCases { diff --git a/run.go b/run.go new file mode 100644 index 0000000..5d6ea58 --- /dev/null +++ b/run.go @@ -0,0 +1,399 @@ +package openai + +import ( + "context" + "fmt" + "net/http" + "net/url" +) + +type Run struct { + ID string `json:"id"` + Object string `json:"object"` + CreatedAt int64 `json:"created_at"` + ThreadID string `json:"thread_id"` + AssistantID string `json:"assistant_id"` + Status RunStatus `json:"status"` + RequiredAction *RunRequiredAction `json:"required_action,omitempty"` + LastError *RunLastError `json:"last_error,omitempty"` + ExpiresAt int64 `json:"expires_at"` + StartedAt *int64 `json:"started_at,omitempty"` + CancelledAt *int64 `json:"cancelled_at,omitempty"` + FailedAt *int64 `json:"failed_at,omitempty"` + CompletedAt *int64 `json:"completed_at,omitempty"` + Model string `json:"model"` + Instructions string `json:"instructions,omitempty"` + Tools []Tool `json:"tools"` + FileIDS []string `json:"file_ids"` + Metadata map[string]any `json:"metadata"` + + httpHeader +} + +type RunStatus string + +const ( + RunStatusQueued RunStatus = "queued" + RunStatusInProgress RunStatus = "in_progress" + RunStatusRequiresAction RunStatus = "requires_action" + RunStatusCancelling RunStatus = "cancelling" + RunStatusFailed RunStatus = "failed" + RunStatusCompleted RunStatus = "completed" + RunStatusExpired RunStatus = "expired" +) + +type RunRequiredAction struct { + Type RequiredActionType `json:"type"` + SubmitToolOutputs *SubmitToolOutputs `json:"submit_tool_outputs,omitempty"` +} + +type RequiredActionType string + +const ( + RequiredActionTypeSubmitToolOutputs RequiredActionType = "submit_tool_outputs" +) + +type SubmitToolOutputs struct { + ToolCalls []ToolCall `json:"tool_calls"` +} + +type RunLastError struct { + Code RunError `json:"code"` + Message string `json:"message"` +} + +type RunError string + +const ( + RunErrorServerError RunError = "server_error" + RunErrorRateLimitExceeded RunError = "rate_limit_exceeded" +) + +type RunRequest struct { + AssistantID string `json:"assistant_id"` + Model *string `json:"model,omitempty"` + Instructions *string `json:"instructions,omitempty"` + Tools []Tool `json:"tools,omitempty"` + Metadata map[string]any +} + +type RunModifyRequest struct { + Metadata map[string]any `json:"metadata,omitempty"` +} + +// RunList is a list of runs. +type RunList struct { + Runs []Run `json:"data"` + + httpHeader +} + +type SubmitToolOutputsRequest struct { + ToolOutputs []ToolOutput `json:"tool_outputs"` +} + +type ToolOutput struct { + ToolCallID string `json:"tool_call_id"` + Output any `json:"output"` +} + +type CreateThreadAndRunRequest struct { + RunRequest + Thread ThreadRequest `json:"thread"` +} + +type RunStep struct { + ID string `json:"id"` + Object string `json:"object"` + CreatedAt int64 `json:"created_at"` + AssistantID string `json:"assistant_id"` + ThreadID string `json:"thread_id"` + RunID string `json:"run_id"` + Type RunStepType `json:"type"` + Status RunStepStatus `json:"status"` + StepDetails StepDetails `json:"step_details"` + LastError *RunLastError `json:"last_error,omitempty"` + ExpiredAt *int64 `json:"expired_at,omitempty"` + CancelledAt *int64 `json:"cancelled_at,omitempty"` + FailedAt *int64 `json:"failed_at,omitempty"` + CompletedAt *int64 `json:"completed_at,omitempty"` + Metadata map[string]any `json:"metadata"` + + httpHeader +} + +type RunStepStatus string + +const ( + RunStepStatusInProgress RunStepStatus = "in_progress" + RunStepStatusCancelling RunStepStatus = "cancelled" + RunStepStatusFailed RunStepStatus = "failed" + RunStepStatusCompleted RunStepStatus = "completed" + RunStepStatusExpired RunStepStatus = "expired" +) + +type RunStepType string + +const ( + RunStepTypeMessageCreation RunStepType = "message_creation" + RunStepTypeToolCalls RunStepType = "tool_calls" +) + +type StepDetails struct { + Type RunStepType `json:"type"` + MessageCreation *StepDetailsMessageCreation `json:"message_creation,omitempty"` + ToolCalls *StepDetailsToolCalls `json:"tool_calls,omitempty"` +} + +type StepDetailsMessageCreation struct { + MessageID string `json:"message_id"` +} + +type StepDetailsToolCalls struct { + ToolCalls []ToolCall `json:"tool_calls"` +} + +// RunStepList is a list of steps. +type RunStepList struct { + RunSteps []RunStep `json:"data"` + + httpHeader +} + +type Pagination struct { + Limit *int + Order *string + After *string + Before *string +} + +// CreateRun creates a new run. +func (c *Client) CreateRun( + ctx context.Context, + threadID string, + request RunRequest, +) (response Run, err error) { + urlSuffix := fmt.Sprintf("/threads/%s/runs", threadID) + req, err := c.newRequest( + ctx, + http.MethodPost, + c.fullURL(urlSuffix), + withBody(request), + withBetaAssistantV1(), + ) + if err != nil { + return + } + + err = c.sendRequest(req, &response) + return +} + +// RetrieveRun retrieves a run. +func (c *Client) RetrieveRun( + ctx context.Context, + threadID string, + runID string, +) (response Run, err error) { + urlSuffix := fmt.Sprintf("/threads/%s/runs/%s", threadID, runID) + req, err := c.newRequest( + ctx, + http.MethodGet, + c.fullURL(urlSuffix), + withBetaAssistantV1(), + ) + if err != nil { + return + } + + err = c.sendRequest(req, &response) + return +} + +// ModifyRun modifies a run. +func (c *Client) ModifyRun( + ctx context.Context, + threadID string, + runID string, + request RunModifyRequest, +) (response Run, err error) { + urlSuffix := fmt.Sprintf("/threads/%s/runs/%s", threadID, runID) + req, err := c.newRequest( + ctx, + http.MethodPost, + c.fullURL(urlSuffix), + withBody(request), + withBetaAssistantV1(), + ) + if err != nil { + return + } + + err = c.sendRequest(req, &response) + return +} + +// ListRuns lists runs. +func (c *Client) ListRuns( + ctx context.Context, + threadID string, + pagination Pagination, +) (response RunList, err error) { + urlValues := url.Values{} + if pagination.Limit != nil { + urlValues.Add("limit", fmt.Sprintf("%d", *pagination.Limit)) + } + if pagination.Order != nil { + urlValues.Add("order", *pagination.Order) + } + if pagination.After != nil { + urlValues.Add("after", *pagination.After) + } + if pagination.Before != nil { + urlValues.Add("before", *pagination.Before) + } + + encodedValues := "" + if len(urlValues) > 0 { + encodedValues = "?" + urlValues.Encode() + } + + urlSuffix := fmt.Sprintf("/threads/%s/runs%s", threadID, encodedValues) + req, err := c.newRequest( + ctx, + http.MethodGet, + c.fullURL(urlSuffix), + withBetaAssistantV1(), + ) + if err != nil { + return + } + + err = c.sendRequest(req, &response) + return +} + +// SubmitToolOutputs submits tool outputs. +func (c *Client) SubmitToolOutputs( + ctx context.Context, + threadID string, + runID string, + request SubmitToolOutputsRequest) (response Run, err error) { + urlSuffix := fmt.Sprintf("/threads/%s/runs/%s/submit_tool_outputs", threadID, runID) + req, err := c.newRequest( + ctx, + http.MethodPost, + c.fullURL(urlSuffix), + withBody(request), + withBetaAssistantV1(), + ) + if err != nil { + return + } + + err = c.sendRequest(req, &response) + return +} + +// CancelRun cancels a run. +func (c *Client) CancelRun( + ctx context.Context, + threadID string, + runID string) (response Run, err error) { + urlSuffix := fmt.Sprintf("/threads/%s/runs/%s/cancel", threadID, runID) + req, err := c.newRequest( + ctx, + http.MethodPost, + c.fullURL(urlSuffix), + withBetaAssistantV1(), + ) + if err != nil { + return + } + + err = c.sendRequest(req, &response) + return +} + +// CreateThreadAndRun submits tool outputs. +func (c *Client) CreateThreadAndRun( + ctx context.Context, + request CreateThreadAndRunRequest) (response Run, err error) { + urlSuffix := "/threads/runs" + req, err := c.newRequest( + ctx, + http.MethodPost, + c.fullURL(urlSuffix), + withBody(request), + withBetaAssistantV1(), + ) + if err != nil { + return + } + + err = c.sendRequest(req, &response) + return +} + +// RetrieveRunStep retrieves a run step. +func (c *Client) RetrieveRunStep( + ctx context.Context, + threadID string, + runID string, + stepID string, +) (response RunStep, err error) { + urlSuffix := fmt.Sprintf("/threads/%s/runs/%s/steps/%s", threadID, runID, stepID) + req, err := c.newRequest( + ctx, + http.MethodGet, + c.fullURL(urlSuffix), + withBetaAssistantV1(), + ) + if err != nil { + return + } + + err = c.sendRequest(req, &response) + return +} + +// ListRunSteps lists run steps. +func (c *Client) ListRunSteps( + ctx context.Context, + threadID string, + runID string, + pagination Pagination, +) (response RunStepList, err error) { + urlValues := url.Values{} + if pagination.Limit != nil { + urlValues.Add("limit", fmt.Sprintf("%d", *pagination.Limit)) + } + if pagination.Order != nil { + urlValues.Add("order", *pagination.Order) + } + if pagination.After != nil { + urlValues.Add("after", *pagination.After) + } + if pagination.Before != nil { + urlValues.Add("before", *pagination.Before) + } + + encodedValues := "" + if len(urlValues) > 0 { + encodedValues = "?" + urlValues.Encode() + } + + urlSuffix := fmt.Sprintf("/threads/%s/runs/%s/steps%s", threadID, runID, encodedValues) + req, err := c.newRequest( + ctx, + http.MethodGet, + c.fullURL(urlSuffix), + withBetaAssistantV1(), + ) + if err != nil { + return + } + + err = c.sendRequest(req, &response) + return +} diff --git a/run_test.go b/run_test.go new file mode 100644 index 0000000..cdf99db --- /dev/null +++ b/run_test.go @@ -0,0 +1,237 @@ +package openai_test + +import ( + "context" + + openai "github.com/sashabaranov/go-openai" + "github.com/sashabaranov/go-openai/internal/test/checks" + + "encoding/json" + "fmt" + "net/http" + "testing" +) + +// TestAssistant Tests the assistant endpoint of the API using the mocked server. +func TestRun(t *testing.T) { + assistantID := "asst_abc123" + threadID := "thread_abc123" + runID := "run_abc123" + stepID := "step_abc123" + limit := 20 + order := "desc" + after := "asst_abc122" + before := "asst_abc124" + + client, server, teardown := setupOpenAITestServer() + defer teardown() + + server.RegisterHandler( + "/v1/threads/"+threadID+"/runs/"+runID+"/steps/"+stepID, + func(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodGet { + resBytes, _ := json.Marshal(openai.RunStep{ + ID: runID, + Object: "run", + CreatedAt: 1234567890, + Status: openai.RunStepStatusCompleted, + }) + fmt.Fprintln(w, string(resBytes)) + } + }, + ) + + server.RegisterHandler( + "/v1/threads/"+threadID+"/runs/"+runID+"/steps", + func(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodGet { + resBytes, _ := json.Marshal(openai.RunStepList{ + RunSteps: []openai.RunStep{ + { + ID: runID, + Object: "run", + CreatedAt: 1234567890, + Status: openai.RunStepStatusCompleted, + }, + }, + }) + fmt.Fprintln(w, string(resBytes)) + } + }, + ) + + server.RegisterHandler( + "/v1/threads/"+threadID+"/runs/"+runID+"/cancel", + func(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodPost { + resBytes, _ := json.Marshal(openai.Run{ + ID: runID, + Object: "run", + CreatedAt: 1234567890, + Status: openai.RunStatusCancelling, + }) + fmt.Fprintln(w, string(resBytes)) + } + }, + ) + + server.RegisterHandler( + "/v1/threads/"+threadID+"/runs/"+runID+"/submit_tool_outputs", + func(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodPost { + resBytes, _ := json.Marshal(openai.Run{ + ID: runID, + Object: "run", + CreatedAt: 1234567890, + Status: openai.RunStatusCancelling, + }) + fmt.Fprintln(w, string(resBytes)) + } + }, + ) + + server.RegisterHandler( + "/v1/threads/"+threadID+"/runs/"+runID, + func(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodGet { + resBytes, _ := json.Marshal(openai.Run{ + ID: runID, + Object: "run", + CreatedAt: 1234567890, + Status: openai.RunStatusQueued, + }) + fmt.Fprintln(w, string(resBytes)) + } else if r.Method == http.MethodPost { + var request openai.RunModifyRequest + err := json.NewDecoder(r.Body).Decode(&request) + checks.NoError(t, err, "Decode error") + + resBytes, _ := json.Marshal(openai.Run{ + ID: runID, + Object: "run", + CreatedAt: 1234567890, + Status: openai.RunStatusQueued, + Metadata: request.Metadata, + }) + fmt.Fprintln(w, string(resBytes)) + } + }, + ) + + server.RegisterHandler( + "/v1/threads/"+threadID+"/runs", + func(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodPost { + var request openai.RunRequest + err := json.NewDecoder(r.Body).Decode(&request) + checks.NoError(t, err, "Decode error") + + resBytes, _ := json.Marshal(openai.Run{ + ID: runID, + Object: "run", + CreatedAt: 1234567890, + Status: openai.RunStatusQueued, + }) + fmt.Fprintln(w, string(resBytes)) + } else if r.Method == http.MethodGet { + resBytes, _ := json.Marshal(openai.RunList{ + Runs: []openai.Run{ + { + ID: runID, + Object: "run", + CreatedAt: 1234567890, + Status: openai.RunStatusQueued, + }, + }, + }) + fmt.Fprintln(w, string(resBytes)) + } + }, + ) + + server.RegisterHandler( + "/v1/threads/runs", + func(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodPost { + var request openai.CreateThreadAndRunRequest + err := json.NewDecoder(r.Body).Decode(&request) + checks.NoError(t, err, "Decode error") + + resBytes, _ := json.Marshal(openai.Run{ + ID: runID, + Object: "run", + CreatedAt: 1234567890, + Status: openai.RunStatusQueued, + }) + fmt.Fprintln(w, string(resBytes)) + } + }, + ) + + ctx := context.Background() + + _, err := client.CreateRun(ctx, threadID, openai.RunRequest{ + AssistantID: assistantID, + }) + checks.NoError(t, err, "CreateRun error") + + _, err = client.RetrieveRun(ctx, threadID, runID) + checks.NoError(t, err, "RetrieveRun error") + + _, err = client.ModifyRun(ctx, threadID, runID, openai.RunModifyRequest{ + Metadata: map[string]any{ + "key": "value", + }, + }) + checks.NoError(t, err, "ModifyRun error") + + _, err = client.ListRuns( + ctx, + threadID, + openai.Pagination{ + Limit: &limit, + Order: &order, + After: &after, + Before: &before, + }, + ) + checks.NoError(t, err, "ListRuns error") + + _, err = client.SubmitToolOutputs(ctx, threadID, runID, + openai.SubmitToolOutputsRequest{}) + checks.NoError(t, err, "SubmitToolOutputs error") + + _, err = client.CancelRun(ctx, threadID, runID) + checks.NoError(t, err, "CancelRun error") + + _, err = client.CreateThreadAndRun(ctx, openai.CreateThreadAndRunRequest{ + RunRequest: openai.RunRequest{ + AssistantID: assistantID, + }, + Thread: openai.ThreadRequest{ + Messages: []openai.ThreadMessage{ + { + Role: openai.ThreadMessageRoleUser, + Content: "Hello, World!", + }, + }, + }, + }) + checks.NoError(t, err, "CreateThreadAndRun error") + + _, err = client.RetrieveRunStep(ctx, threadID, runID, stepID) + checks.NoError(t, err, "RetrieveRunStep error") + + _, err = client.ListRunSteps( + ctx, + threadID, + runID, + openai.Pagination{ + Limit: &limit, + Order: &order, + After: &after, + Before: &before, + }, + ) + checks.NoError(t, err, "ListRunSteps error") +}