Skip to content

Commit

Permalink
feat: ok for coze workflow
Browse files Browse the repository at this point in the history
  • Loading branch information
yubing744 committed Nov 17, 2024
1 parent 00596a7 commit 88dbcd6
Show file tree
Hide file tree
Showing 9 changed files with 325 additions and 43 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
.PHONY: clean build unit-test run docker-* tag release

NAME=trading-gpt
VERSION=0.28.4
VERSION=0.29.0

clean:
rm -rf build/*
Expand Down
7 changes: 4 additions & 3 deletions bbgo.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -64,13 +64,14 @@ exchangeStrategies:
enabled: true
base_url: "https://api.coze.com"
timeout: 60s
indicators:
workflow_indicators:
- name: "news_changed"
description: "Sui news score and reasons:"
interval: "5m"
before: "20s"
bot_id: "7372584552936079367"
message: "Please obtain the SUI message score and give a brief reason"
workflow_id: "7372569763169533960"
params:
symbol: "SUI"
include_events:
- news_changed
- kline_changed
Expand Down
82 changes: 78 additions & 4 deletions pkg/apis/coze/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"time"
)
Expand All @@ -16,15 +17,22 @@ type Client struct {
HTTPClient *http.Client
}

// NewClient creates a new Coze API client with a specified timeout
func NewClient(baseURL, apiKey string, timeout time.Duration) ICozeClient {
return &Client{
// NewClient creates a new Coze API client with options
func NewClient(baseURL, apiKey string, opts ...ClientOption) ICozeClient {
c := &Client{
BaseURL: baseURL,
APIKey: apiKey,
HTTPClient: &http.Client{
Timeout: timeout,
Timeout: 30 * time.Second, // default timeout
},
}

// Apply options
for _, opt := range opts {
opt(c)
}

return c
}

// Chat sends a chat request to the Coze API and handles the response
Expand Down Expand Up @@ -55,3 +63,69 @@ func (c *Client) Chat(ctx context.Context, req *ChatRequest) (*ChatResponse, err

return &response, nil
}

// RunWorkflow executes a workflow and returns the result
func (c *Client) RunWorkflow(ctx context.Context, req *WorkflowRequest) (*WorkflowResponse, error) {
url := fmt.Sprintf("%s/v1/workflow/run", c.BaseURL)
body, err := json.Marshal(req)
if err != nil {
return nil, fmt.Errorf("failed to marshal request: %w", err)
}

httpReq, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(body))
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
httpReq.Header.Set("Content-Type", "application/json")
httpReq.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.APIKey))

resp, err := c.HTTPClient.Do(httpReq)
if err != nil {
return nil, fmt.Errorf("failed to execute request: %w", err)
}
defer resp.Body.Close()

// Read response body
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response body: %w", err)
}

// Handle non-200 status codes
if resp.StatusCode != http.StatusOK {
message := "unknown error"
switch resp.StatusCode {
case http.StatusUnauthorized:
message = "unauthorized: invalid API key"
case http.StatusForbidden:
message = "forbidden: insufficient permissions"
case http.StatusTooManyRequests:
message = "rate limit exceeded"
case http.StatusGatewayTimeout:
message = "workflow execution timed out"
case http.StatusInternalServerError:
message = "internal server error"
}

return nil, &HTTPError{
StatusCode: resp.StatusCode,
Message: message,
Body: string(respBody),
}
}

var response WorkflowResponse
if err := json.Unmarshal(respBody, &response); err != nil {
return nil, fmt.Errorf("failed to decode response: %w", err)
}

// Handle API error responses
if response.Code != 0 {
return nil, &CozeError{
Code: response.Code,
Message: response.Msg,
}
}

return &response, nil
}
78 changes: 74 additions & 4 deletions pkg/apis/coze/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ func TestNewClient(t *testing.T) {
apiKey := "test-api-key"
timeout := 10 * time.Second

client := NewClient(baseURL, apiKey, timeout).(*Client)
client := NewClient(baseURL, apiKey, WithTimeout(timeout)).(*Client)

assert.Equal(t, baseURL, client.BaseURL)
assert.Equal(t, apiKey, client.APIKey)
Expand Down Expand Up @@ -46,7 +46,7 @@ func TestChatSuccess(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(mockChatHandler))
defer server.Close()

client := NewClient(server.URL, "test-api-key", 10*time.Second).(*Client)
client := NewClient(server.URL, "test-api-key", WithTimeout(time.Second*10)).(*Client)

req := &ChatRequest{
BotID: "bot-id",
Expand Down Expand Up @@ -80,7 +80,7 @@ func TestChatFailure(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(mockChatHandlerFail))
defer server.Close()

client := NewClient(server.URL, "test-api-key", 10*time.Second).(*Client)
client := NewClient(server.URL, "test-api-key", WithTimeout(time.Second*10)).(*Client)

req := &ChatRequest{
// Simulated request data
Expand All @@ -102,7 +102,7 @@ func TestChatTimeout(t *testing.T) {

defer server.Close()

client := NewClient(server.URL, "test-api-key", 1*time.Second).(*Client)
client := NewClient(server.URL, "test-api-key", WithTimeout(time.Second*1)).(*Client)

req := &ChatRequest{
// Simulated request data
Expand All @@ -114,3 +114,73 @@ func TestChatTimeout(t *testing.T) {
assert.Error(t, err)
assert.Nil(t, resp)
}

// Mock server response for a successful workflow request
func mockWorkflowHandler(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
response := WorkflowResponse{
Data: `{"output":"Test workflow output"}`,
DebugURL: "https://www.coze.com/work_flow?execute_id=123&space_id=456&workflow_id=789",
}
json.NewEncoder(w).Encode(response)
}

func TestRunWorkflowSuccess(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(mockWorkflowHandler))
defer server.Close()

client := NewClient(server.URL, "test-api-key", WithTimeout(time.Second*10)).(*Client)

req := &WorkflowRequest{
WorkflowID: "test-workflow-id",
BotID: "test-bot-id",
Parameters: map[string]string{
"input": "test input",
},
}

ctx := context.Background()
resp, err := client.RunWorkflow(ctx, req)

assert.NoError(t, err)
assert.NotNil(t, resp)
assert.Contains(t, resp.Data, "Test workflow output")
assert.Contains(t, resp.DebugURL, "work_flow")
}

func TestRunWorkflowTimeout(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusGatewayTimeout)
}))
defer server.Close()

client := NewClient(server.URL, "test-api-key", WithTimeout(time.Second*1)).(*Client)

req := &WorkflowRequest{
WorkflowID: "test-workflow-id",
}

ctx := context.Background()
resp, err := client.RunWorkflow(ctx, req)

assert.Error(t, err)
assert.Nil(t, resp)
assert.Contains(t, err.Error(), "workflow execution timed out")
}

func TestRunWorkflowFailure(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(mockChatHandlerFail))
defer server.Close()

client := NewClient(server.URL, "test-api-key", WithTimeout(time.Second*10)).(*Client)

req := &WorkflowRequest{
WorkflowID: "test-workflow-id",
}

ctx := context.Background()
resp, err := client.RunWorkflow(ctx, req)

assert.Error(t, err)
assert.Nil(t, resp)
}
53 changes: 52 additions & 1 deletion pkg/apis/coze/interface.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,29 @@
package coze

import "context"
import (
"context"
"fmt"
)

// Common error codes
const (
ErrCodeUnauthorized = 401
ErrCodeForbidden = 403
ErrCodeRateLimit = 429
ErrCodeInternalError = 500
ErrCodeGatewayTimeout = 504
)

// HTTPError represents an HTTP error response
type HTTPError struct {
StatusCode int
Message string
Body string
}

func (e *HTTPError) Error() string {
return fmt.Sprintf("http error: status=%d, message=%s", e.StatusCode, e.Message)
}

// Message represents an item in the chat
type Message struct {
Expand Down Expand Up @@ -29,7 +52,35 @@ type ChatResponse struct {
Messages []Message `json:"messages,omitempty"`
}

// WorkflowRequest represents the payload for running a workflow
type WorkflowRequest struct {
WorkflowID string `json:"workflow_id"`
BotID string `json:"bot_id,omitempty"`
Parameters map[string]string `json:"parameters,omitempty"`
}

// WorkflowResponse represents the response from running a workflow
type WorkflowResponse struct {
Cost string `json:"cost"`
Code int `json:"code"`
Msg string `json:"msg"`
Data string `json:"data"`
DebugURL string `json:"debug_url"`
Token int `json:"token"`
}

// CozeError represents an error response from the Coze API
type CozeError struct {
Code int `json:"code"`
Message string `json:"msg"`
}

func (e *CozeError) Error() string {
return fmt.Sprintf("coze api error: code=%d, message=%s", e.Code, e.Message)
}

// CozeClient is an interface for Coze API client
type ICozeClient interface {
Chat(ctx context.Context, req *ChatRequest) (*ChatResponse, error)
RunWorkflow(ctx context.Context, req *WorkflowRequest) (*WorkflowResponse, error)
}
23 changes: 23 additions & 0 deletions pkg/apis/coze/option.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package coze

import (
"net/http"
"time"
)

// ClientOption defines the type for client options
type ClientOption func(*Client)

// WithTimeout sets custom timeout for HTTP client
func WithTimeout(timeout time.Duration) ClientOption {
return func(c *Client) {
c.HTTPClient.Timeout = timeout
}
}

// WithTransport sets custom transport for HTTP client
func WithTransport(transport http.RoundTripper) ClientOption {
return func(c *Client) {
c.HTTPClient.Transport = transport
}
}
20 changes: 15 additions & 5 deletions pkg/config/env_coze_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,21 @@ type IndicatorItem struct {
Message string `json:"message"` // The message content to send to the bot
}

type WorkflowIndicatorItem struct {
Name string `json:"name"` // A unique name for the scheduled task
Description string `json:"description"` // A description of what the task does
Interval types.Interval `json:"interval"` // How often to run the task
Before types.Interval `json:"before"` // How often to run the task
WorkflowID string `json:"workflow_id"` // The ID of the bot to interact with
Params map[string]string `json:"params"` // The message content to send to the bot
}

// CozeEntityConfig holds the configuration for a CozeEntity.
type CozeEntityConfig struct {
Enabled bool `json:"enabled"`
BaseURL string `json:"base_url"`
APIKey string `json:"api_key"`
Timeout types.Interval `json:"timeout"`
IndicatorItems []*IndicatorItem `json:"indicators"` // A list of scheduled tasks
Enabled bool `json:"enabled"`
BaseURL string `json:"base_url"`
APIKey string `json:"api_key"`
Timeout types.Interval `json:"timeout"`
IndicatorItems []*IndicatorItem `json:"indicators"` // A list of scheduled tasks
WorkflowIndicatorItems []*WorkflowIndicatorItem `json:"workflow_indicators"` // A list of scheduled tasks
}
Loading

0 comments on commit 88dbcd6

Please sign in to comment.