diff --git a/apps.go b/apps.go index 3a851d1..e2b8ea7 100644 --- a/apps.go +++ b/apps.go @@ -24,25 +24,22 @@ const ( Ongoing AppTrainingStatus = "ongoing" ) -// App - https://wit.ai/docs/http/20170307#get__apps_link +// App - https://wit.ai/docs/http/20200513/#get__apps_link type App struct { + ID string `json:"id,omitempty"` Name string `json:"name"` Lang string `json:"lang"` Private bool `json:"private"` - // Description presents when we get an app - Description string `json:"description,omitempty"` - // Use Desc when create an app - Desc string `json:"desc,omitempty"` - // ID presents when we get an app - ID string `json:"id,omitempty"` - // AppID presents when we create an app - AppID string `json:"app_id,omitempty"` - CreatedAt string `json:"created_at,omitempty"` - Timezone string `json:"timezone,omitempty"` - // Training information - LastTrainingDurationSecs int `json:"last_training_duration_secs,omitempty"` + + // Timezone is only used when creating/updating an app; it's + // not available when getting the details of an app. + Timezone string `json:"timezone,omitempty"` + + CreatedAt Time `json:"created_at,omitempty"` + WillTrainAt Time `json:"will_train_at,omitempty"` LastTrainedAt Time `json:"last_trained_at,omitempty"` + LastTrainingDurationSecs int `json:"last_training_duration_secs,omitempty"` TrainingStatus AppTrainingStatus `json:"training_status,omitempty"` } @@ -63,13 +60,15 @@ func (witTime *Time) UnmarshalJSON(input []byte) error { return nil } -// CreatedApp - https://wit.ai/docs/http/20170307#post__apps_link +// CreatedApp - https://wit.ai/docs/http/20200513/#post__apps_link type CreatedApp struct { AccessToken string `json:"access_token"` AppID string `json:"app_id"` } -// GetApps - Returns an array of all apps that you own. https://wit.ai/docs/http/20170307#get__apps_link +// GetApps - Returns an array of all apps that you own. +// +// https://wit.ai/docs/http/20200513/#get__apps_link func (c *Client) GetApps(limit int, offset int) ([]App, error) { if limit <= 0 { limit = 0 @@ -91,7 +90,9 @@ func (c *Client) GetApps(limit int, offset int) ([]App, error) { return apps, err } -// GetApp - returns map by ID. https://wit.ai/docs/http/20170307#get__apps__app_id_link +// GetApp - Returns an object representation of the specified app. +// +// https://wit.ai/docs/http/20200513/#get__apps__app_link func (c *Client) GetApp(id string) (*App, error) { resp, err := c.request(http.MethodGet, fmt.Sprintf("/apps/%s", url.PathEscape(id)), "application/json", nil) if err != nil { @@ -109,7 +110,49 @@ func (c *Client) GetApp(id string) (*App, error) { return app, nil } -// DeleteApp - deletes app by ID. https://wit.ai/docs/http/20170307#delete__apps__app_id_link +// CreateApp - creates new app. +// +// https://wit.ai/docs/http/20200513/#post__apps_link +func (c *Client) CreateApp(app App) (*CreatedApp, error) { + appJSON, err := json.Marshal(app) + if err != nil { + return nil, err + } + + resp, err := c.request(http.MethodPost, "/apps", "application/json", bytes.NewBuffer(appJSON)) + if err != nil { + return nil, err + } + + defer resp.Close() + + var createdApp *CreatedApp + decoder := json.NewDecoder(resp) + err = decoder.Decode(&createdApp) + + return createdApp, err +} + +// UpdateApp - Updates an app. +// +// https://wit.ai/docs/http/20200513/#put__apps__app_link +func (c *Client) UpdateApp(id string, app App) error { + appJSON, err := json.Marshal(app) + if err != nil { + return err + } + + resp, err := c.request(http.MethodPut, fmt.Sprintf("/apps/%s", url.PathEscape(id)), "application/json", bytes.NewBuffer(appJSON)) + if err == nil { + resp.Close() + } + + return err +} + +// DeleteApp - deletes app by ID. +// +// https://wit.ai/docs/http/20200513/#delete__apps__app_link func (c *Client) DeleteApp(id string) error { resp, err := c.request(http.MethodDelete, fmt.Sprintf("/apps/%s", url.PathEscape(id)), "application/json", nil) if err == nil { @@ -119,44 +162,170 @@ func (c *Client) DeleteApp(id string) error { return err } -// CreateApp - creates new app. https://wit.ai/docs/http/20170307#post__apps_link -func (c *Client) CreateApp(app App) (*CreatedApp, error) { - appJSON, err := json.Marshal(app) +// AppTag - https://wit.ai/docs/http/20200513/#get__apps__app_tags__tag_link +type AppTag struct { + Name string `json:"name,omitempty"` + Desc string `json:"desc,omitempty"` + + CreatedAt Time `json:"created_at,omitempty"` + UpdatedAt Time `json:"updated_at,omitempty"` +} + +// GetAppTags - Returns an array of all tag groups for an app. +// Within a group, all tags point to the same app state (as a result of moving tags). +// +// https://wit.ai/docs/http/20200513/#get__apps__app_tags_link +func (c *Client) GetAppTags(appID string) ([][]AppTag, error) { + resp, err := c.request(http.MethodGet, fmt.Sprintf("/apps/%s/tags", url.PathEscape(appID)), "application/json", nil) if err != nil { return nil, err } - resp, err := c.request(http.MethodPost, "/apps", "application/json", bytes.NewBuffer(appJSON)) + defer resp.Close() + + var tags [][]AppTag + decoder := json.NewDecoder(resp) + err = decoder.Decode(&tags) + return tags, err +} + +// GetAppTag - returns the detail of the specified tag. +// +// https://wit.ai/docs/http/20200513/#get__apps__app_tags__tag_link +func (c *Client) GetAppTag(appID, tagID string) (*AppTag, error) { + resp, err := c.request(http.MethodGet, fmt.Sprintf("/apps/%s/tags/%s", url.PathEscape(appID), url.PathEscape(tagID)), "application/json", nil) if err != nil { return nil, err } defer resp.Close() - var createdApp *CreatedApp + var tag *AppTag decoder := json.NewDecoder(resp) - err = decoder.Decode(&createdApp) + err = decoder.Decode(&tag) + return tag, err +} - return createdApp, err +// CreateAppTag - Take a snapshot of the current app state, save it as a tag (version) +// of the app. The name of the tag created will be returned in the response. +// +// https://wit.ai/docs/http/20200513/#post__apps__app_tags_link +func (c *Client) CreateAppTag(appID string, tag string) (*AppTag, error) { + type appTag struct { + Tag string `json:"tag"` + } + + tagJSON, err := json.Marshal(appTag{Tag: tag}) + if err != nil { + return nil, err + } + + resp, err := c.request(http.MethodPost, fmt.Sprintf("/apps/%s/tags", url.PathEscape(tag)), "application/json", bytes.NewBuffer(tagJSON)) + if err != nil { + return nil, err + } + + defer resp.Close() + + // theresponse format is different than the one in get API. + var tmp appTag + decoder := json.NewDecoder(resp) + if err := decoder.Decode(&tmp); err != nil { + return nil, err + } + + return &AppTag{Name: tmp.Tag}, nil } -// UpdateApp - Updates an app. https://wit.ai/docs/http/20170307#put__apps__app_id_link -func (c *Client) UpdateApp(id string, app App) (*App, error) { - appJSON, err := json.Marshal(app) +// UpdateAppTagRequest - https://wit.ai/docs/http/20200513/#put__apps__app_tags__tag_link +type UpdateAppTagRequest struct { + Tag string `json:"tag,omitempty"` + Desc string `json:"desc,omitempty"` + MoveTo string `json:"move_to,omitempty"` +} + +// UpdateAppTagResponse - https://wit.ai/docs/http/20200513/#put__apps__app_tags__tag_link +type UpdateAppTagResponse struct { + Tag string `json:"tag,omitempty"` + Desc string `json:"desc,omitempty"` + MovedTo string `json:"moved_to,omitempty"` +} + +// UpdateAppTag - Update the tag's name or description +// +// https://wit.ai/docs/http/20200513/#put__apps__app_tags__tag_link +func (c *Client) UpdateAppTag(appID, tagID string, updated AppTag) (*AppTag, error) { + type tag struct { + Tag string `json:"tag,omitempty"` + Desc string `json:"desc,omitempty"` + } + + updateJSON, err := json.Marshal(tag{Tag: updated.Name, Desc: updated.Desc}) if err != nil { return nil, err } - resp, err := c.request(http.MethodPut, fmt.Sprintf("/apps/%s", url.PathEscape(id)), "application/json", bytes.NewBuffer(appJSON)) + resp, err := c.request(http.MethodPut, fmt.Sprintf("/apps/%s/tags/%s", url.PathEscape(appID), url.PathEscape(tagID)), "application/json", bytes.NewBuffer(updateJSON)) + if err != nil { + return nil, err + } + + defer resp.Close() + + var tagResp tag + decoder := json.NewDecoder(resp) + err = decoder.Decode(&tagResp) + return &AppTag{Name: tagResp.Tag, Desc: tagResp.Desc}, err +} + +type MovedAppTag struct { + Tag string `json:"tag"` + Desc string `json:"desc"` + MovedTo string `json:"moved_to"` +} + +// MoveAppTag - move the tag to point to another tag. +// +// https://wit.ai/docs/http/20200513/#put__apps__app_tags__tag_link +func (c *Client) MoveAppTag(appID, tagID string, to string, updated *AppTag) (*MovedAppTag, error) { + type tag struct { + Tag string `json:"tag,omitempty"` + Desc string `json:"desc,omitempty"` + MoveTo string `json:"move_to,omitempty"` + } + + updateReq := tag{MoveTo: to} + if updated != nil { + updateReq.Tag = updated.Name + updateReq.Desc = updated.Desc + } + + updateJSON, err := json.Marshal(updateReq) + if err != nil { + return nil, err + } + + resp, err := c.request(http.MethodPut, fmt.Sprintf("/apps/%s/tags/%s", url.PathEscape(appID), url.PathEscape(tagID)), "application/json", bytes.NewBuffer(updateJSON)) if err != nil { return nil, err } defer resp.Close() - var updatedApp *App + var tagResp *MovedAppTag decoder := json.NewDecoder(resp) - err = decoder.Decode(&updatedApp) + err = decoder.Decode(&tagResp) + return tagResp, err +} + +// DeleteAppTag - Permanently delete the tag. +// +// https://wit.ai/docs/http/20200513/#delete__apps__app_tags__tag_link +func (c *Client) DeleteAppTag(appID, tagID string) error { + resp, err := c.request(http.MethodDelete, fmt.Sprintf("/apps/%s/tags/%s", url.PathEscape(appID), url.PathEscape(tagID)), "application/json", nil) + if err == nil { + resp.Close() + } - return updatedApp, err + return err } diff --git a/apps_test.go b/apps_test.go index 892efa9..c5b71fa 100644 --- a/apps_test.go +++ b/apps_test.go @@ -5,34 +5,80 @@ package witai import ( "net/http" "net/http/httptest" + "reflect" "testing" "time" ) func TestGetApps(t *testing.T) { testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { - res.Write([]byte(`[{"name": "app1"}, {"name":"app2"}]`)) + res.Write([]byte(`[ + { + "id": "9890809890", + "name": "My_Second_App", + "lang": "en", + "private": false, + "created_at": "2018-01-01T00:00:01Z" + }, + { + "id": "9890809891", + "name": "My_Third_App", + "lang": "en", + "private": false, + "created_at": "2018-01-02T00:00:01Z" + } + ]`)) })) defer func() { testServer.Close() }() c := NewClient(unitTestToken) c.APIBase = testServer.URL - apps, _ := c.GetApps(1, 0) + apps, err := c.GetApps(2, 0) - if len(apps) != 2 { - t.Fatalf("expected 2 apps, got: %v", apps) + wantApps := []App{ + { + ID: "9890809890", + Name: "My_Second_App", + Lang: "en", + Private: false, + CreatedAt: Time{time.Date(2018, 1, 1, 0, 0, 1, 0, time.UTC)}, + }, + { + ID: "9890809891", + Name: "My_Third_App", + Lang: "en", + Private: false, + CreatedAt: Time{time.Date(2018, 1, 2, 0, 0, 1, 0, time.UTC)}, + }, + } + + if err != nil { + t.Fatalf("nil error expected, got %v", err) + } + if !reflect.DeepEqual(wantApps, apps) { + t.Fatalf("expected\n\tapps: %v\n\tgot: %v", wantApps, apps) } } func TestGetApp(t *testing.T) { testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { - res.Write([]byte(`{"name": "alarm-clock","training_status":"done","will_train_at":"2018-07-29T18:17:34-0700","last_training_duration_secs":42,"last_trained_at":"2018-07-29T18:16:34-0700"}`)) + res.Write([]byte(`{ + "id": "2802177596527671", + "name": "alarm-clock", + "lang": "en", + "private": false, + "created_at": "2018-07-29T18:15:34-0700", + "last_training_duration_secs": 42, + "will_train_at": "2018-07-29T18:17:34-0700", + "last_trained_at": "2018-07-29T18:18:34-0700", + "training_status": "done" + }`)) })) defer func() { testServer.Close() }() c := NewClient(unitTestToken) c.APIBase = testServer.URL - app, err := c.GetApp("my-id") + app, err := c.GetApp("2802177596527671") if err != nil { t.Fatalf("not expected err, got: %s", err.Error()) } @@ -53,7 +99,7 @@ func TestGetApp(t *testing.T) { t.Fatalf("Expected 42 got %v", app.LastTrainingDurationSecs) } - expectedLastTrainTime, _ := time.Parse(WitTimeFormat, "2018-07-29T18:16:34-0700") + expectedLastTrainTime, _ := time.Parse(WitTimeFormat, "2018-07-29T18:18:34-0700") if !app.LastTrainedAt.Time.Equal(expectedLastTrainTime) { t.Fatalf("expected %v got: %v", app.WillTrainAt, expectedLastTrainTime) } @@ -80,7 +126,7 @@ func TestCreateApp(t *testing.T) { func TestDeleteApp(t *testing.T) { testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { - res.Write([]byte(`{}`)) + res.Write([]byte(`{"success": true}`)) })) defer func() { testServer.Close() }() @@ -93,20 +139,208 @@ func TestDeleteApp(t *testing.T) { func TestUpdateApp(t *testing.T) { testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { - res.Write([]byte(`{"description": "updated"}`)) + res.Write([]byte(`{"success": true}`)) })) defer func() { testServer.Close() }() c := NewClient(unitTestToken) c.APIBase = testServer.URL - app, err := c.UpdateApp("appid", App{ - Description: "new desc", + err := c.UpdateApp("appid", App{ + Lang: "fr", + Timezone: "Europe/Paris", }) if err != nil { t.Fatalf("err=nil expected, got: %v", err) } - if app.Description != "updated" { - t.Fatalf("description=updated expected, got: %s", app.Lang) +} + +func TestGetAppTags(t *testing.T) { + testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(`[ + [ + { + "name": "v3", + "created_at": "2019-09-14T20:29:53-0700", + "updated_at": "2019-09-14T20:29:53-0700", + "desc": "third version" + }, + { + "name": "v2", + "created_at": "2019-08-08T11:05:35-0700", + "updated_at": "2019-08-08T11:09:17-0700", + "desc": "second version, moved to v3" + } + ], + [ + { + "name": "v1", + "created_at": "2019-08-08T11:02:52-0700", + "updated_at": "2019-09-14T15:45:22-0700", + "desc": "legacy first version" + } + ] + ]`)) + })) + defer testServer.Close() + + c := NewClient(unitTestToken) + c.APIBase = testServer.URL + + wantTags := [][]AppTag{ + { + {Name: "v3", Desc: "third version"}, + {Name: "v2", Desc: "second version, moved to v3"}, + }, + { + {Name: "v1", Desc: "legacy first version"}, + }, + } + + tags, err := c.GetAppTags("appid") + if err != nil { + t.Fatalf("expected nil err, got: %v", err) + } + + if len(tags) != len(wantTags) { + t.Fatalf("expected\n\ttags: %v\n\tgot: %v", wantTags, tags) + } + + for i, group := range tags { + wantGroup := wantTags[i] + + if len(group) != len(wantGroup) { + t.Fatalf("expected\n\ttags[%v]: %v\n\tgot: %v", i, wantTags, tags) + } + + for j, tag := range group { + wantTag := wantGroup[j] + if tag.Name != wantTag.Name { + t.Fatalf("expected\n\ttags[%v][%v].Name: %v\n\tgot: %v", i, j, wantTag.Name, tag.Name) + } + if tag.Desc != wantTag.Desc { + t.Fatalf("expected\n\ttags[%v][%v].Desc: %v\n\tgot: %v", i, j, wantTag.Desc, tag.Desc) + } + } + } +} + +func TestGetAppTag(t *testing.T) { + testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(`{ + "name": "v1", + "created_at": "2019-08-08T11:02:52-0700", + "updated_at": "2019-09-14T15:45:22-0700", + "desc": "first version" + }`)) + })) + defer testServer.Close() + + c := NewClient(unitTestToken) + c.APIBase = testServer.URL + + tag, err := c.GetAppTag("appid", "tagid") + if err != nil { + t.Fatalf("expected nil err, got: %v", err) + } + + wantTag := &AppTag{ + Name: "v1", + Desc: "first version", + } + + if tag.Name != wantTag.Name { + t.Fatalf("expected\n\ttag.Name: %v, got: %v", wantTag.Name, tag.Name) + } + if tag.Desc != wantTag.Desc { + t.Fatalf("expected\n\ttag.Desc: %v, got: %v", wantTag.Desc, tag.Desc) + } +} + +func TestCreateAppTag(t *testing.T) { + testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(`{"tag": "v_1"}`)) + })) + defer testServer.Close() + + c := NewClient(unitTestToken) + c.APIBase = testServer.URL + + tag, err := c.CreateAppTag("appid", "v$1") + if err != nil { + t.Fatalf("expected nil err, got: %v", err) + } + + wantTag := &AppTag{Name: "v_1"} + if tag.Name != wantTag.Name { + t.Fatalf("expected\n\ttag.Name: %v, got: %v", wantTag.Name, tag.Name) + } +} + +func TestUpdateAppTag(t *testing.T) { + testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(`{ + "tag": "v1.0", + "desc": "new description" + }`)) + })) + defer testServer.Close() + + c := NewClient(unitTestToken) + c.APIBase = testServer.URL + + updated, err := c.UpdateAppTag("appid", "v1", AppTag{Name: "v1.0", Desc: "new description"}) + if err != nil { + t.Fatalf("expected nil err, got: %v", err) + } + + wantUpdated := &AppTag{Name: "v1.0", Desc: "new description"} + + if !reflect.DeepEqual(updated, wantUpdated) { + t.Fatalf("expected\n\tupdated: %v\n\tgot: %v", wantUpdated, updated) + } +} + +func TestMoveAppTag(t *testing.T) { + testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(`{ + "tag": "v1.0", + "desc": "1.0 version, moved to 1.1 version", + "moved_to": "v1.1" + }`)) + })) + defer testServer.Close() + + c := NewClient(unitTestToken) + c.APIBase = testServer.URL + + moved, err := c.MoveAppTag("appid", "v1", "v1.1", &AppTag{Name: "v1.0", Desc: "1.0 version, moved to 1.1 version"}) + if err != nil { + t.Fatalf("expected nil err, got: %v", err) + } + + wantMoved := &MovedAppTag{ + Tag: "v1.0", + Desc: "1.0 version, moved to 1.1 version", + MovedTo: "v1.1", + } + + if !reflect.DeepEqual(moved, wantMoved) { + t.Fatalf("expected\n\tmoved: %v\n\tgot: %v", wantMoved, moved) + } +} + +func TestDeleteAppTag(t *testing.T) { + testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(`{"success": true}`)) + })) + defer testServer.Close() + + c := NewClient(unitTestToken) + c.APIBase = testServer.URL + + err := c.DeleteAppTag("appid", "tagid") + if err != nil { + t.Fatalf("expected nil err, got: %v", err) } } diff --git a/entity.go b/entity.go index 26e415b..cb4cfcc 100644 --- a/entity.go +++ b/entity.go @@ -10,40 +10,45 @@ import ( "net/url" ) -// Entity - https://wit.ai/docs/http/20170307#post__entities_link +// Entity represents a wit-ai Entity. +// +// https://wit.ai/docs/http/20200513/#post__entities_link type Entity struct { - ID string `json:"id"` - Doc string `json:"doc"` - Name string `json:"name,omitempty"` - Lang string `json:"lang,omitempty"` - Builtin bool `json:"builtin,omitempty"` - Lookups []string `json:"lookups,omitempty"` - Values []EntityValue `json:"values,omitempty"` + ID string `json:"id"` + Name string `json:"name"` + Lookups []string `json:"lookups,omitempty"` + Roles []string `json:"roles,omitempty"` + Keywords []EntityKeyword `json:"keywords,omitempty"` } -// EntityValue - https://wit.ai/docs/http/20170307#get__entities__entity_id_link -type EntityValue struct { - Value string `json:"value"` - Expressions []string `json:"expressions"` - MetaData string `json:"metadata"` +// EntityKeyword is a keyword lookup for an Entity. +// +// https://wit.ai/docs/http/20200513/#post__entities__entity_keywords_link +type EntityKeyword struct { + Keyword string `json:"keyword"` + Synonyms []string `json:"synonyms"` } -// GetEntities - returns list of entities. https://wit.ai/docs/http/20170307#get__entities_link -func (c *Client) GetEntities() ([]string, error) { +// GetEntities - returns list of entities. +// +// https://wit.ai/docs/http/20200513/#get__entities_link +func (c *Client) GetEntities() ([]Entity, error) { resp, err := c.request(http.MethodGet, "/entities", "application/json", nil) if err != nil { - return []string{}, err + return []Entity{}, err } defer resp.Close() - var entities []string + var entities []Entity decoder := json.NewDecoder(resp) err = decoder.Decode(&entities) return entities, err } -// CreateEntity - Creates a new entity with the given attributes. https://wit.ai/docs/http/20170307#post__entities_link +// CreateEntity - Creates a new entity with the given attributes +// +// https://wit.ai/docs/http/20200513/#post__entities_link func (c *Client) CreateEntity(entity Entity) (*Entity, error) { entityJSON, err := json.Marshal(entity) if err != nil { @@ -63,9 +68,11 @@ func (c *Client) CreateEntity(entity Entity) (*Entity, error) { return entityResp, err } -// GetEntity - returns entity by ID. https://wit.ai/docs/http/20170307#get__entities__entity_id_link -func (c *Client) GetEntity(id string) (*Entity, error) { - resp, err := c.request(http.MethodGet, fmt.Sprintf("/entities/%s", url.PathEscape(id)), "application/json", nil) +// GetEntity - returns entity by ID or name. +// +// https://wit.ai/docs/http/20200513/#get__entities__entity_link +func (c *Client) GetEntity(entityID string) (*Entity, error) { + resp, err := c.request(http.MethodGet, fmt.Sprintf("/entities/%s", url.PathEscape(entityID)), "application/json", nil) if err != nil { return nil, err } @@ -78,9 +85,31 @@ func (c *Client) GetEntity(id string) (*Entity, error) { return entity, err } -// DeleteEntity - deletes entity by ID. https://wit.ai/docs/http/20170307#delete__entities__entity_id_link -func (c *Client) DeleteEntity(id string) error { - resp, err := c.request(http.MethodDelete, fmt.Sprintf("/entities/%s", url.PathEscape(id)), "application/json", nil) +// UpdateEntity - Updates an entity by ID or name. +// +// https://wit.ai/docs/http/20200513/#put__entities__entity_link +func (c *Client) UpdateEntity(entityID string, entity Entity) error { + entityJSON, err := json.Marshal(entity) + if err != nil { + return err + } + + resp, err := c.request(http.MethodPut, fmt.Sprintf("/entities/%s", url.PathEscape(entityID)), "application/json", bytes.NewBuffer(entityJSON)) + if err != nil { + return err + } + + defer resp.Close() + + decoder := json.NewDecoder(resp) + return decoder.Decode(&entity) +} + +// DeleteEntity - deletes entity by ID or name. +// +// https://wit.ai/docs/http/20200513/#delete__entities__entity_link +func (c *Client) DeleteEntity(entityID string) error { + resp, err := c.request(http.MethodDelete, fmt.Sprintf("/entities/%s", url.PathEscape(entityID)), "application/json", nil) if err == nil { resp.Close() } @@ -88,7 +117,9 @@ func (c *Client) DeleteEntity(id string) error { return err } -// DeleteEntityRole - deletes entity role. https://wit.ai/docs/http/20170307#delete__entities__entity_id_role_id_link +// DeleteEntityRole - deletes entity role. +// +// https://wit.ai/docs/http/20200513/#delete__entities__entity_role_link func (c *Client) DeleteEntityRole(entityID string, role string) error { resp, err := c.request(http.MethodDelete, fmt.Sprintf("/entities/%s:%s", url.PathEscape(entityID), url.PathEscape(role)), "application/json", nil) if err == nil { @@ -98,32 +129,16 @@ func (c *Client) DeleteEntityRole(entityID string, role string) error { return err } -// UpdateEntity - Updates an entity. https://wit.ai/docs/http/20170307#put__entities__entity_id_link -func (c *Client) UpdateEntity(id string, entity Entity) error { - entityJSON, err := json.Marshal(entity) - if err != nil { - return err - } - - resp, err := c.request(http.MethodPut, fmt.Sprintf("/entities/%s", url.PathEscape(id)), "application/json", bytes.NewBuffer(entityJSON)) - if err != nil { - return err - } - - defer resp.Close() - - decoder := json.NewDecoder(resp) - return decoder.Decode(&entity) -} - -// AddEntityValue - Add a possible value into the list of values for the keyword entity. https://wit.ai/docs/http/20170307#post__entities__entity_id_values_link -func (c *Client) AddEntityValue(entityID string, value EntityValue) (*Entity, error) { - valueJSON, err := json.Marshal(value) +// AddEntityKeyword - Add a possible value into the list of values for the keyword entity. +// +// https://wit.ai/docs/http/20200513/#post__entities__entity_keywords_link +func (c *Client) AddEntityKeyword(entityID string, keyword EntityKeyword) (*Entity, error) { + valueJSON, err := json.Marshal(keyword) if err != nil { return nil, err } - resp, err := c.request(http.MethodPost, fmt.Sprintf("/entities/%s/values", url.PathEscape(entityID)), "application/json", bytes.NewBuffer(valueJSON)) + resp, err := c.request(http.MethodPost, fmt.Sprintf("/entities/%s/keywords", url.PathEscape(entityID)), "application/json", bytes.NewBuffer(valueJSON)) if err != nil { return nil, err } @@ -139,9 +154,11 @@ func (c *Client) AddEntityValue(entityID string, value EntityValue) (*Entity, er return entityResp, nil } -// DeleteEntityValue - Delete a canonical value from the entity. https://wit.ai/docs/http/20170307#delete__entities__entity_id_values_link -func (c *Client) DeleteEntityValue(entityID string, value string) error { - resp, err := c.request(http.MethodDelete, fmt.Sprintf("/entities/%s/values/%s", url.PathEscape(entityID), url.PathEscape(value)), "application/json", nil) +// DeleteEntityKeyword - Delete a keyword from the keywords entity. +// +// https://wit.ai/docs/http/20200513/#delete__entities__entity_keywords__keyword_link +func (c *Client) DeleteEntityKeyword(entityID string, keyword string) error { + resp, err := c.request(http.MethodDelete, fmt.Sprintf("/entities/%s/keywords/%s", url.PathEscape(entityID), url.PathEscape(keyword)), "application/json", nil) if err == nil { resp.Close() } @@ -149,20 +166,22 @@ func (c *Client) DeleteEntityValue(entityID string, value string) error { return err } -// AddEntityValueExpression - Create a new expression of the canonical value of the keyword entity. https://wit.ai/docs/http/20170307#post__entities__entity_id_values__value_id_expressions_link -func (c *Client) AddEntityValueExpression(entityID string, value string, expression string) (*Entity, error) { - type expr struct { - Expression string `json:"expression"` +// AddEntityKeywordSynonym - Create a new synonym of the canonical value of the keywords entity. +// +// https://wit.ai/docs/http/20200513/#post__entities__entity_keywords__keyword_synonyms_link +func (c *Client) AddEntityKeywordSynonym(entityID string, keyword string, synonym string) (*Entity, error) { + type syn struct { + Synonym string `json:"synonym"` } - exprJSON, err := json.Marshal(expr{ - Expression: expression, + exprJSON, err := json.Marshal(syn{ + Synonym: synonym, }) if err != nil { return nil, err } - resp, err := c.request(http.MethodPost, fmt.Sprintf("/entities/%s/values/%s/expressions", url.PathEscape(entityID), url.PathEscape(value)), "application/json", bytes.NewBuffer(exprJSON)) + resp, err := c.request(http.MethodPost, fmt.Sprintf("/entities/%s/keywords/%s/synonyms", url.PathEscape(entityID), url.PathEscape(keyword)), "application/json", bytes.NewBuffer(exprJSON)) if err != nil { return nil, err } @@ -178,9 +197,11 @@ func (c *Client) AddEntityValueExpression(entityID string, value string, express return entityResp, nil } -// DeleteEntityValueExpression - Delete an expression of the canonical value of the entity. https://wit.ai/docs/http/20170307#delete__entities__entity_id_values__value_id_expressions_link -func (c *Client) DeleteEntityValueExpression(entityID string, value string, expression string) error { - resp, err := c.request(http.MethodDelete, fmt.Sprintf("/entities/%s/values/%s/expressions/%s", url.PathEscape(entityID), url.PathEscape(value), url.PathEscape(expression)), "application/json", nil) +// DeleteEntityKeywordSynonym - Delete a synonym of the keyword of the entity. +// +// https://wit.ai/docs/http/20200513/#delete__entities__entity_keywords__keyword_synonyms__synonym_link +func (c *Client) DeleteEntityKeywordSynonym(entityID string, keyword string, expression string) error { + resp, err := c.request(http.MethodDelete, fmt.Sprintf("/entities/%s/keywords/%s/synonyms/%s", url.PathEscape(entityID), url.PathEscape(keyword), url.PathEscape(expression)), "application/json", nil) if err == nil { resp.Close() } diff --git a/entity_test.go b/entity_test.go index 94bc009..e09d9f7 100644 --- a/entity_test.go +++ b/entity_test.go @@ -5,14 +5,23 @@ package witai import ( "net/http" "net/http/httptest" + "reflect" "testing" ) var unitTestToken = "unit_test_invalid_token" func TestGetEntities(t *testing.T) { - testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { - res.Write([]byte(`["e1", "e2"]`)) + testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + w.Write([]byte(`[ + { + "id": "2690212494559269", + "name": "car" + }, { + "id": "254954985556896", + "name": "color" + } + ]`)) })) defer func() { testServer.Close() }() @@ -20,148 +29,284 @@ func TestGetEntities(t *testing.T) { c.APIBase = testServer.URL entities, _ := c.GetEntities() - if len(entities) != 2 { - t.Fatalf("expected 2 entities, got: %d", len(entities)) + wantEntities := []Entity{ + {ID: "2690212494559269", Name: "car"}, + {ID: "254954985556896", Name: "color"}, + } + + if !reflect.DeepEqual(entities, wantEntities) { + t.Fatalf("expected\n\tentities: %v\n\tgot: %v", wantEntities, entities) } } func TestCreateEntity(t *testing.T) { - testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { - res.Write([]byte(`{"lang": "en"}`)) + testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + w.Write([]byte(`{ + "id": "5418abc7-cc68-4073-ae9e-3a5c3c81d965", + "name": "favorite_city", + "roles": ["favorite_city"], + "lookups": ["free-text", "keywords"], + "keywords": [] + }`)) })) defer func() { testServer.Close() }() c := NewClient(unitTestToken) c.APIBase = testServer.URL e, err := c.CreateEntity(Entity{ - ID: "favorite_city", - Doc: "A city that I like", + Name: "favorite_city", + Roles: []string{}, }) + + wantEntity := &Entity{ + ID: "5418abc7-cc68-4073-ae9e-3a5c3c81d965", + Name: "favorite_city", + Roles: []string{"favorite_city"}, + Lookups: []string{"free-text", "keywords"}, + Keywords: []EntityKeyword{}, + } + if err != nil { t.Fatalf("nil error expected, got %v", err) } - if e.Lang != "en" { - t.Fatalf("lang=en expected, got: %s", e.Lang) + if !reflect.DeepEqual(wantEntity, e) { + t.Fatalf("expected\n\tentity: %v\n\tgot: %v", wantEntity, e) } } func TestGetEntity(t *testing.T) { - testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { - res.Write([]byte(`{"doc": "My favorite city"}`)) + testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + w.Write([]byte(`{ + "id": "571979db-f6ac-4820-bc28-a1e0787b98fc", + "name": "first_name", + "lookups": ["free-text", "keywords"], + "roles": ["first_name"], + "keywords": [ { + "keyword": "Willy", + "synonyms": ["Willy"] + }, { + "keyword": "Laurent", + "synonyms": ["Laurent"] + }, { + "keyword": "Julien", + "synonyms": ["Julien"] + } ] + }`)) })) defer func() { testServer.Close() }() c := NewClient(unitTestToken) c.APIBase = testServer.URL - entity, err := c.GetEntity("favorite_city") + entity, err := c.GetEntity("first_name") + + wantEntity := &Entity{ + ID: "571979db-f6ac-4820-bc28-a1e0787b98fc", + Name: "first_name", + Roles: []string{"first_name"}, + Lookups: []string{"free-text", "keywords"}, + Keywords: []EntityKeyword{ + {Keyword: "Willy", Synonyms: []string{"Willy"}}, + {Keyword: "Laurent", Synonyms: []string{"Laurent"}}, + {Keyword: "Julien", Synonyms: []string{"Julien"}}, + }, + } + if err != nil { t.Fatalf("nil error expected, got %v", err) } - if entity.Doc != "My favorite city" { - t.Fatalf("expected valid entity, got: %v", entity) + if !reflect.DeepEqual(wantEntity, entity) { + t.Fatalf("expected\n\tentity: %v\n\tgot: %v", wantEntity, entity) } } -func TestDeleteEntity(t *testing.T) { +func TestUpdateEntity(t *testing.T) { testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { - res.Write([]byte(`{}`)) + res.Write([]byte(`{ + "id": "5418abc7-cc68-4073-ae9e-3a5c3c81d965", + "name": "favorite_city", + "roles": ["favorite_city"], + "lookups": ["free-text", "keywords"], + "keywords": [ + { + "keyword": "Paris", + "synonyms": ["Paris", "City of Light", "Capital of France"] + }, + { + "keyword": "Seoul", + "synonyms": ["Seoul", "서울", "Kimchi paradise"] + } + ] + }`)) })) defer func() { testServer.Close() }() c := NewClient(unitTestToken) c.APIBase = testServer.URL - if err := c.DeleteEntity("favorite_city"); err != nil { - t.Fatalf("expected nil error, got: %v", err) + + if err := c.UpdateEntity("favorite_city", Entity{ + Name: "favorite_city", + Roles: []string{"favorite_city"}, + Lookups: []string{"free-text", "keywords"}, + Keywords: []EntityKeyword{ + {Keyword: "Paris", Synonyms: []string{"Paris", "City of Light", "Capital of France"}}, + {Keyword: "Seoul", Synonyms: []string{"Seoul", "서울", "Kimchi paradise"}}, + }, + }); err != nil { + t.Fatalf("err=nil expected, got: %v", err) } } -func TestDeleteEntityRole(t *testing.T) { +func TestDeleteEntity(t *testing.T) { testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { - res.Write([]byte(`{}`)) + res.Write([]byte(`{"deleted": "favorite_city"}`)) })) defer func() { testServer.Close() }() c := NewClient(unitTestToken) c.APIBase = testServer.URL - if err := c.DeleteEntityRole("favorite_city", "role"); err != nil { + if err := c.DeleteEntity("favorite_city"); err != nil { t.Fatalf("expected nil error, got: %v", err) } } -func TestUpdateEntity(t *testing.T) { +func TestDeleteEntityRole(t *testing.T) { testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { - res.Write([]byte(`{"doc": "new doc"}`)) + res.Write([]byte(`{"deleted": "flight:destination"}`)) })) defer func() { testServer.Close() }() c := NewClient(unitTestToken) c.APIBase = testServer.URL - - if err := c.UpdateEntity("favorite_city", Entity{ - Doc: "new doc", - }); err != nil { - t.Fatalf("err=nil expected, got: %v", err) + if err := c.DeleteEntityRole("flight", "destination"); err != nil { + t.Fatalf("expected nil error, got: %v", err) } } -func TestAddEntityValue(t *testing.T) { +func TestAddEntityKeyword(t *testing.T) { testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { - res.Write([]byte(`{"lang": "de"}`)) + res.Write([]byte(`{ + "id": "5418abc7-cc68-4073-ae9e-3a5c3c81d965", + "name": "favorite_city", + "roles": ["favorite_city"], + "lookups": ["free-text", "keywords"], + "keywords": [ + { + "keyword": "Brussels", + "synonyms": ["Brussels", "Capital of Belgium"] + }, + { + "keyword": "Paris", + "synonyms": ["Paris", "City of Light", "Capital of France"] + }, + { + "keyword": "Seoul", + "synonyms": ["Seoul", "서울", "Kimchi paradise"] + } + ] + }`)) })) defer func() { testServer.Close() }() c := NewClient(unitTestToken) c.APIBase = testServer.URL - e, err := c.AddEntityValue("favorite_city", EntityValue{ - Value: "Minsk", + entity, err := c.AddEntityKeyword("favorite_city", EntityKeyword{ + Keyword: "Brussels", + Synonyms: []string{"Brussels", "Capital of Belgium"}, }) + + wantEntity := &Entity{ + ID: "5418abc7-cc68-4073-ae9e-3a5c3c81d965", + Name: "favorite_city", + Roles: []string{"favorite_city"}, + Lookups: []string{"free-text", "keywords"}, + Keywords: []EntityKeyword{ + {Keyword: "Brussels", Synonyms: []string{"Brussels", "Capital of Belgium"}}, + {Keyword: "Paris", Synonyms: []string{"Paris", "City of Light", "Capital of France"}}, + {Keyword: "Seoul", Synonyms: []string{"Seoul", "서울", "Kimchi paradise"}}, + }, + } + if err != nil { t.Fatalf("nil error expected, got %v", err) } - if e.Lang != "de" { - t.Fatalf("lang=de expected, got: %s", e.Lang) + + if !reflect.DeepEqual(wantEntity, entity) { + t.Fatalf("expected\n\tentity: %v\n\tgot: %v", wantEntity, entity) } } -func TestDeleteEntityValue(t *testing.T) { +func TestDeleteEntityKeyword(t *testing.T) { testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { - res.Write([]byte(`{}`)) + res.Write([]byte(`{"deleted": "Paris"}`)) })) defer func() { testServer.Close() }() c := NewClient(unitTestToken) c.APIBase = testServer.URL - if err := c.DeleteEntityValue("favorite_city", "London"); err != nil { + if err := c.DeleteEntityKeyword("favorite_city", "Paris"); err != nil { t.Fatalf("expected nil error, got: %v", err) } } -func TestAddEntityValueExpression(t *testing.T) { +func TestAddEntityKeywordSynonym(t *testing.T) { testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { - res.Write([]byte(`{"lang": "de"}`)) + res.Write([]byte(`{ + "id": "5418abc7-cc68-4073-ae9e-3a5c3c81d965", + "name": "favorite_city", + "roles": ["favorite_city"], + "lookups": ["free-text", "keywords"], + "keywords": [ + { + "keyword": "Brussels", + "synonyms": ["Brussels", "Capital of Belgium"] + }, + { + "keyword": "Paris", + "synonyms": ["Paris", "City of Light", "Capital of France", "Camembert city"] + }, + { + "keyword": "Seoul", + "synonyms": ["Seoul", "서울", "Kimchi paradise"] + } + ] + }`)) })) defer func() { testServer.Close() }() c := NewClient(unitTestToken) c.APIBase = testServer.URL - e, err := c.AddEntityValueExpression("favorite_city", "Minsk", "Minsk") + entity, err := c.AddEntityKeywordSynonym("favorite_city", "Paris", "Camembert city") + + wantEntity := &Entity{ + ID: "5418abc7-cc68-4073-ae9e-3a5c3c81d965", + Name: "favorite_city", + Roles: []string{"favorite_city"}, + Lookups: []string{"free-text", "keywords"}, + Keywords: []EntityKeyword{ + {Keyword: "Brussels", Synonyms: []string{"Brussels", "Capital of Belgium"}}, + {Keyword: "Paris", Synonyms: []string{"Paris", "City of Light", "Capital of France", "Camembert city"}}, + {Keyword: "Seoul", Synonyms: []string{"Seoul", "서울", "Kimchi paradise"}}, + }, + } + if err != nil { t.Fatalf("nil error expected, got %v", err) } - if e.Lang != "de" { - t.Fatalf("lang=de expected, got: %s", e.Lang) + + if !reflect.DeepEqual(wantEntity, entity) { + t.Fatalf("expected\n\tentity: %v\n\tgot: %v", wantEntity, entity) } } -func TestDeleteEntityValueExpression(t *testing.T) { +func TestDeleteEntityKeywordSynonym(t *testing.T) { testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { - res.Write([]byte(`{}`)) + res.Write([]byte(`{"deleted": "Camembert City"}`)) })) defer func() { testServer.Close() }() c := NewClient(unitTestToken) c.APIBase = testServer.URL - if err := c.DeleteEntityValueExpression("favorite_city", "Minsk", "Minsk"); err != nil { + if err := c.DeleteEntityKeywordSynonym("favorite_city", "Paris", "Camembert City"); err != nil { t.Fatalf("expected nil error, got: %v", err) } } diff --git a/intent.go b/intent.go new file mode 100644 index 0000000..f90a4bb --- /dev/null +++ b/intent.go @@ -0,0 +1,85 @@ +// Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved. + +package witai + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" + "net/url" +) + +// Intent - represents a wit-ai intent. +// +// https://wit.ai/docs/http/20200513/#get__intents_link +type Intent struct { + ID string `json:"id"` + Name string `json:"name"` + Entities []Entity `json:"entities,omitempty"` +} + +// GetIntents - returns the list of intents. +// +// https://wit.ai/docs/http/20200513/#get__intents_link +func (c *Client) GetIntents() ([]Intent, error) { + resp, err := c.request(http.MethodGet, "/intents", "application/json", nil) + if err != nil { + return []Intent{}, err + } + + defer resp.Close() + + var intents []Intent + err = json.NewDecoder(resp).Decode(&intents) + return intents, err +} + +// CreateIntent - creates a new intent with the given name. +// +// https://wit.ai/docs/http/20200513/#post__intents_link +func (c *Client) CreateIntent(name string) (*Intent, error) { + intentJSON, err := json.Marshal(Intent{Name: name}) + if err != nil { + return nil, err + } + + resp, err := c.request(http.MethodPost, "/intents", "application/json", bytes.NewBuffer(intentJSON)) + if err != nil { + return nil, err + } + + defer resp.Close() + + var intentResp *Intent + err = json.NewDecoder(resp).Decode(&intentResp) + return intentResp, err +} + +// GetIntent - returns intent by name. +// +// https://wit.ai/docs/http/20200513/#get__intents__intent_link +func (c *Client) GetIntent(name string) (*Intent, error) { + resp, err := c.request(http.MethodGet, fmt.Sprintf("/intents/%s", url.PathEscape(name)), "application/json", nil) + if err != nil { + return nil, err + } + + defer resp.Close() + + var intent *Intent + err = json.NewDecoder(resp).Decode(&intent) + return intent, err +} + +// DeleteIntent - permanently deletes an intent by name. +// +// https://wit.ai/docs/http/20200513/#delete__intents__intent_link +func (c *Client) DeleteIntent(name string) error { + resp, err := c.request(http.MethodDelete, fmt.Sprintf("/intents/%s", url.PathEscape(name)), "application/json", nil) + if err == nil { + resp.Close() + } + + return err +} diff --git a/intent_test.go b/intent_test.go new file mode 100644 index 0000000..3e1cdbc --- /dev/null +++ b/intent_test.go @@ -0,0 +1,120 @@ +// Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved. + +package witai + +import ( + "net/http" + "net/http/httptest" + "reflect" + "testing" +) + +func TestGetIntents(t *testing.T) { + testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(`[ + { + "id": "2690212494559269", + "name": "buy_car" + }, + { + "id": "254954985556896", + "name": "get_weather" + }, + { + "id": "233273197778131", + "name": "make_call" + } + ]`)) + })) + defer testServer.Close() + + c := NewClient(unitTestToken) + c.APIBase = testServer.URL + intents, _ := c.GetIntents() + + wantIntents := []Intent{ + {ID: "2690212494559269", Name: "buy_car"}, + {ID: "254954985556896", Name: "get_weather"}, + {ID: "233273197778131", Name: "make_call"}, + } + + if !reflect.DeepEqual(intents, wantIntents) { + t.Fatalf("expected\n\tintents: %v\n\tgot: %v", wantIntents, intents) + } +} + +func TestCreateIntent(t *testing.T) { + testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(`{ + "id": "13989798788", + "name": "buy_flowers" + }`)) + })) + defer testServer.Close() + + c := NewClient(unitTestToken) + c.APIBase = testServer.URL + intent, err := c.CreateIntent("buy_flowers") + + wantIntent := &Intent{ + ID: "13989798788", + Name: "buy_flowers", + } + + if err != nil { + t.Fatalf("nil error expected, got %v", err) + } + if !reflect.DeepEqual(wantIntent, intent) { + t.Fatalf("expected\n\tentity: %v\n\tgot: %v", wantIntent, intent) + } +} + +func TestGetIntent(t *testing.T) { + testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(`{ + "id": "13989798788", + "name": "buy_flowers", + "entities": [{ + "id": "9078938883", + "name": "flower:flower" + },{ + "id": "11223229984", + "name": "wit$contact:contact" + }] + }`)) + })) + defer testServer.Close() + + c := NewClient(unitTestToken) + c.APIBase = testServer.URL + intent, err := c.GetIntent("buy_flowers") + + wantIntent := &Intent{ + ID: "13989798788", + Name: "buy_flowers", + Entities: []Entity{ + {ID: "9078938883", Name: "flower:flower"}, + {ID: "11223229984", Name: "wit$contact:contact"}, + }, + } + + if err != nil { + t.Fatalf("nil error expected, got %v", err) + } + if !reflect.DeepEqual(wantIntent, intent) { + t.Fatalf("expected\n\tentity: %v\n\tgot: %v", wantIntent, intent) + } +} + +func TestDeleteIntent(t *testing.T) { + testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(`{"deleted": "buy_flowers"}`)) + })) + defer testServer.Close() + + c := NewClient(unitTestToken) + c.APIBase = testServer.URL + if err := c.DeleteIntent("buy_flowers"); err != nil { + t.Fatalf("nil error expected, got %v", err) + } +} diff --git a/language_detection.go b/language_detection.go index b922c74..33daef3 100644 --- a/language_detection.go +++ b/language_detection.go @@ -9,18 +9,18 @@ import ( "net/url" ) -// Locales - https://wit.ai/docs/http/20170307#get__language_link +// Locales - https://wit.ai/docs/http/20200513#get__language_link type Locales struct { DetectedLocales []Locale `json:"detected_locales"` } -// Locale - https://wit.ai/docs/http/20170307#get__language_link +// Locale - https://wit.ai/docs/http/20200513#get__language_link type Locale struct { Locale string `json:"locale"` Confidence float64 `json:"confidence"` } -// Detect - returns the detected languages from query - https://wit.ai/docs/http/20170307#get__language_link +// Detect - returns the detected languages from query - https://wit.ai/docs/http/20200513#get__language_link func (c *Client) Detect(text string) (*Locales, error) { resp, err := c.request(http.MethodGet, fmt.Sprintf("/language?q=%s", url.PathEscape(text)), "application/json", nil) if err != nil { diff --git a/message.go b/message.go index 05825b6..a480847 100644 --- a/message.go +++ b/message.go @@ -11,21 +11,51 @@ import ( "net/url" ) -// MessageResponse - https://wit.ai/docs/http/20170307#get__message_link +// MessageResponse - https://wit.ai/docs/http/20200513/#get__message_link type MessageResponse struct { - ID string `json:"msg_id"` - Text string `json:"_text"` - Entities map[string]interface{} `json:"entities"` + ID string `json:"msg_id"` + Text string `json:"text"` + Intents []MessageIntent `json:"intents"` + Entities map[string][]MessageEntity `json:"entities"` + Traits map[string][]MessageTrait `json:"traits"` } -// MessageRequest - https://wit.ai/docs/http/20170307#get__message_link +// MessageEntity - https://wit.ai/docs/http/20200513/#get__message_link +type MessageEntity struct { + ID string `json:"id"` + Name string `json:"name"` + Role string `json:"role"` + Start int `json:"start"` + End int `json:"end"` + Body string `json:"body"` + Value string `json:"value"` + Confidence float64 `json:"confidence"` + Entities []MessageEntity `json:"entities"` + Extra map[string]interface{} `json:"-"` +} + +// MessageTrait - https://wit.ai/docs/http/20200513/#get__message_link +type MessageTrait struct { + ID string `json:"id"` + Value string `json:"value"` + Confidence float64 `json:"confidence"` + Extra map[string]interface{} `json:"-"` +} + +// MessageIntent - https://wit.ai/docs/http/20200513/#get__message_link +type MessageIntent struct { + ID string `json:"id"` + Name string `json:"name"` + Confidence float64 `json:"confidence"` +} + +// MessageRequest - https://wit.ai/docs/http/20200513/#get__message_link type MessageRequest struct { - Query string `json:"q"` - MsgID string `json:"msg_id"` - N int `json:"n"` - ThreadID string `json:"thread_id"` - Context *MessageContext `json:"context"` - Speech *Speech `json:"-"` + Query string `json:"q"` + Tag string `json:"tag"` + N int `json:"n"` + Context *MessageContext `json:"context"` + Speech *Speech `json:"-"` } // Speech - https://wit.ai/docs/http/20170307#post__speech_link @@ -36,7 +66,7 @@ type Speech struct { // MessageContext - https://wit.ai/docs/http/20170307#context_link type MessageContext struct { - TeferenceTime string `json:"reference_time"` // "2014-10-30T12:18:45-07:00" + ReferenceTime string `json:"reference_time"` // "2014-10-30T12:18:45-07:00" Timezone string `json:"timezone"` Locale string `json:"locale"` Coords MessageCoords `json:"coords"` @@ -92,14 +122,17 @@ func (c *Client) Speech(req *MessageRequest) (*MessageResponse, error) { func buildParseQuery(req *MessageRequest) string { q := fmt.Sprintf("?q=%s", url.PathEscape(req.Query)) - if len(req.MsgID) != 0 { - q += fmt.Sprintf("&msg_id=%s", req.MsgID) - } if req.N != 0 { q += fmt.Sprintf("&n=%d", req.N) } - if len(req.ThreadID) != 0 { - q += fmt.Sprintf("&thread_id=%s", req.ThreadID) + if req.Tag != "" { + q += fmt.Sprintf("&tag=%s", req.Tag) + } + if req.Context != nil { + b, _ := json.Marshal(req.Context) + if b != nil { + q += fmt.Sprintf("&context=%s", url.PathEscape(string(b))) + } } return q diff --git a/message_test.go b/message_test.go index 818367c..779683d 100644 --- a/message_test.go +++ b/message_test.go @@ -6,12 +6,40 @@ import ( "bytes" "net/http" "net/http/httptest" + "net/url" + "reflect" "testing" ) func TestParse(t *testing.T) { testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { - res.Write([]byte(`{"msg_id": "msg1", "entities": {"e1": "d1"}}`)) + res.Write([]byte(`{ + "msg_id": "msg1", + "text": "text", + "intents": [ + {"id": "intent1", "name": "intent1_name", "confidence": 0.9}, + {"id": "intent2", "name": "intent2_name", "confidence": 0.7} + ], + "entities": { + "entity1": [{ + "id": "entity1-1", + "name": "entity1", + "role": "entity1", + "start": 1, + "end": 10, + "body": "value1", + "value": "value1", + "confidence": 0.8 + }] + }, + "traits": { + "trait1": [{ + "id": "trait1-1", + "value": "value1", + "confidence": 0.8 + }] + } + }`)) })) defer func() { testServer.Close() }() @@ -21,14 +49,64 @@ func TestParse(t *testing.T) { Query: "hello", }) - if msg == nil || msg.ID != "msg1" || len(msg.Entities) != 1 { - t.Fatalf("expected message id: msg1 and 1 entity, got %v", msg) + wantMessage := &MessageResponse{ + ID: "msg1", + Text: "text", + Intents: []MessageIntent{ + {ID: "intent1", Name: "intent1_name", Confidence: 0.9}, + {ID: "intent2", Name: "intent2_name", Confidence: 0.7}, + }, + Entities: map[string][]MessageEntity{ + "entity1": {{ + ID: "entity1-1", + Name: "entity1", + Role: "entity1", + Start: 1, + End: 10, + Body: "value1", + Value: "value1", + Confidence: 0.8, + }}, + }, + Traits: map[string][]MessageTrait{ + "trait1": {{ID: "trait1-1", Value: "value1", Confidence: 0.8}}, + }, + } + + if !reflect.DeepEqual(msg, wantMessage) { + t.Fatalf("expected \n\tmsg %v \n\tgot %v", wantMessage, msg) } } func TestSpeech(t *testing.T) { testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { - res.Write([]byte(`{"msg_id": "msg1", "entities": {"e1": "d1"}}`)) + res.Write([]byte(`{ + "msg_id": "msg1", + "text": "text", + "intents": [ + {"id": "intent1", "name": "intent1_name", "confidence": 0.9}, + {"id": "intent2", "name": "intent2_name", "confidence": 0.7} + ], + "entities": { + "entity1": [{ + "id": "entity1-1", + "name": "entity1", + "role": "entity1", + "start": 1, + "end": 10, + "body": "value1", + "value": "value1", + "confidence": 0.8 + }] + }, + "traits": { + "trait1": [{ + "id": "trait1-1", + "value": "value1", + "confidence": 0.8 + }] + } + }`)) })) defer func() { testServer.Close() }() @@ -41,7 +119,62 @@ func TestSpeech(t *testing.T) { }, }) - if msg == nil || msg.ID != "msg1" || len(msg.Entities) != 1 { - t.Fatalf("expected message id: msg1 and 1 entity, got %v", msg) + wantMessage := &MessageResponse{ + ID: "msg1", + Text: "text", + Intents: []MessageIntent{ + {ID: "intent1", Name: "intent1_name", Confidence: 0.9}, + {ID: "intent2", Name: "intent2_name", Confidence: 0.7}, + }, + Entities: map[string][]MessageEntity{ + "entity1": {{ + ID: "entity1-1", + Name: "entity1", + Role: "entity1", + Start: 1, + End: 10, + Body: "value1", + Value: "value1", + Confidence: 0.8, + }}, + }, + Traits: map[string][]MessageTrait{ + "trait1": {{ID: "trait1-1", Value: "value1", Confidence: 0.8}}, + }, + } + + if !reflect.DeepEqual(msg, wantMessage) { + t.Fatalf("expected \n\tmsg %v \n\tgot %v", wantMessage, msg) + } +} + +func Test_buildParseQuery(t *testing.T) { + want := "?q=" + url.PathEscape("hello world") + + "&n=1&tag=tag" + + "&context=" + + url.PathEscape("{"+ + "\"reference_time\":\"2014-10-30T12:18:45-07:00\","+ + "\"timezone\":\"America/Los_Angeles\","+ + "\"locale\":\"en_US\","+ + "\"coords\":{\"lat\":32.47104,\"long\":-122.14703}"+ + "}") + + got := buildParseQuery(&MessageRequest{ + Query: "hello world", + N: 1, + Tag: "tag", + Context: &MessageContext{ + Locale: "en_US", + Coords: MessageCoords{ + Lat: 32.47104, + Long: -122.14703, + }, + Timezone: "America/Los_Angeles", + ReferenceTime: "2014-10-30T12:18:45-07:00", + }, + }) + + if got != want { + t.Fatalf("expected \n\tquery = %v \n\tgot = %v", want, got) } } diff --git a/samples.go b/samples.go deleted file mode 100644 index 6678101..0000000 --- a/samples.go +++ /dev/null @@ -1,95 +0,0 @@ -// Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved. - -package witai - -import ( - "bytes" - "encoding/json" - "fmt" - "net/http" -) - -// Sample - https://wit.ai/docs/http/20170307#get__samples_link -type Sample struct { - Text string `json:"text"` - Entities []SampleEntity `json:"entities"` -} - -// SampleEntity - https://wit.ai/docs/http/20170307#get__samples_link -type SampleEntity struct { - Entity string `json:"entity"` - Value string `json:"value"` - Role string `json:"role"` - Start int `json:"start"` - End int `json:"end"` - Subentitites []SampleEntity `json:"subentities"` -} - -// ValidateSampleResponse - https://wit.ai/docs/http/20170307#post__samples_link -type ValidateSampleResponse struct { - Sent bool `json:"sent"` - N int `json:"n"` -} - -// GetSamples - Returns an array of samples. https://wit.ai/docs/http/20170307#get__samples_link -func (c *Client) GetSamples(limit int, offset int) ([]Sample, error) { - if limit <= 0 { - limit = 0 - } - if offset <= 0 { - offset = 0 - } - - resp, err := c.request(http.MethodGet, fmt.Sprintf("/samples?limit=%d&offset=%d", limit, offset), "application/json", nil) - if err != nil { - return []Sample{}, err - } - - defer resp.Close() - - var samples []Sample - decoder := json.NewDecoder(resp) - err = decoder.Decode(&samples) - return samples, err -} - -// ValidateSamples - Validate samples (sentence + entities annotations) to train your app programmatically. https://wit.ai/docs/http/20170307#post__samples_link -func (c *Client) ValidateSamples(samples []Sample) (*ValidateSampleResponse, error) { - samplesJSON, err := json.Marshal(samples) - if err != nil { - return nil, err - } - - resp, err := c.request(http.MethodPost, "/samples", "application/json", bytes.NewBuffer(samplesJSON)) - if err != nil { - return nil, err - } - - defer resp.Close() - - var r *ValidateSampleResponse - decoder := json.NewDecoder(resp) - err = decoder.Decode(&r) - return r, err -} - -// DeleteSamples - Delete validated samples from your app. https://wit.ai/docs/http/20170307#delete__samples_link -// Only text property is required -func (c *Client) DeleteSamples(samples []Sample) (*ValidateSampleResponse, error) { - samplesJSON, err := json.Marshal(samples) - if err != nil { - return nil, err - } - - resp, err := c.request(http.MethodDelete, "/samples", "application/json", bytes.NewBuffer(samplesJSON)) - if err != nil { - return nil, err - } - - defer resp.Close() - - var r *ValidateSampleResponse - decoder := json.NewDecoder(resp) - err = decoder.Decode(&r) - return r, err -} diff --git a/samples_test.go b/samples_test.go deleted file mode 100644 index a8e56bd..0000000 --- a/samples_test.go +++ /dev/null @@ -1,62 +0,0 @@ -// Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved. - -package witai - -import ( - "net/http" - "net/http/httptest" - "testing" -) - -func TestGetSamples(t *testing.T) { - testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { - res.Write([]byte(`[{"text": "s1"}, {"text":"s2"}]`)) - })) - defer func() { testServer.Close() }() - - c := NewClient(unitTestToken) - c.APIBase = testServer.URL - samples, _ := c.GetSamples(1, 0) - - if len(samples) != 2 { - t.Fatalf("expected 2 samples, got: %v", samples) - } -} - -func TestValidateSamples(t *testing.T) { - testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { - res.Write([]byte(`{"sent": true, "n": 2}`)) - })) - defer func() { testServer.Close() }() - - c := NewClient(unitTestToken) - c.APIBase = testServer.URL - r, _ := c.ValidateSamples([]Sample{ - { - Text: "hello", - }, - }) - - if r.N != 2 || !r.Sent { - t.Fatalf("expected N=2 and Sent=true, got: %v", r) - } -} - -func TestDeleteSamples(t *testing.T) { - testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { - res.Write([]byte(`{"sent": true, "n": 2}`)) - })) - defer func() { testServer.Close() }() - - c := NewClient(unitTestToken) - c.APIBase = testServer.URL - r, _ := c.DeleteSamples([]Sample{ - { - Text: "hello", - }, - }) - - if r.N != 2 || !r.Sent { - t.Fatalf("expected N=2 and Sent=true, got: %v", r) - } -} diff --git a/trait.go b/trait.go new file mode 100644 index 0000000..d8862fa --- /dev/null +++ b/trait.go @@ -0,0 +1,141 @@ +// Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved. + +package witai + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" + "net/url" +) + +// Trait - represents a wit-ai trait. +// +// https://wit.ai/docs/http/20200513/#post__traits_link +type Trait struct { + ID string `json:"id"` + Name string `json:"name"` + Values []TraitValue `json:"values"` +} + +// TraitValue - represents the value of a Trait. +// +// https://wit.ai/docs/http/20200513/#get__traits__trait_link +type TraitValue struct { + ID string `json:"id"` + Value string `json:"value"` +} + +// GetTraits - returns a list of traits. +// +// https://wit.ai/docs/http/20200513/#get__traits_link +func (c *Client) GetTraits() ([]Trait, error) { + resp, err := c.request(http.MethodGet, "/traits", "application/json", nil) + if err != nil { + return []Trait{}, err + } + + defer resp.Close() + + var traits []Trait + decoder := json.NewDecoder(resp) + err = decoder.Decode(&traits) + return traits, err +} + +// CreateTrait - creates a new trait with the given values. +// +// https://wit.ai/docs/http/20200513/#post__traits_link +func (c *Client) CreateTrait(name string, values []string) (*Trait, error) { + type trait struct { + Name string `json:"name"` + Values []string `json:"values"` + } + + traitJSON, err := json.Marshal(trait{Name: name, Values: values}) + if err != nil { + return nil, err + } + + resp, err := c.request(http.MethodPost, "/traits", "application/json", bytes.NewBuffer(traitJSON)) + if err != nil { + return nil, err + } + + defer resp.Close() + + var traitResp *Trait + decoder := json.NewDecoder(resp) + err = decoder.Decode(&traitResp) + return traitResp, err +} + +// GetTrait - returns all available information about a trait. +// +// https://wit.ai/docs/http/20200513/#get__traits__trait_link +func (c *Client) GetTrait(name string) (*Trait, error) { + resp, err := c.request(http.MethodGet, fmt.Sprintf("/traits/%s", url.PathEscape(name)), "application/json", nil) + if err != nil { + return nil, err + } + defer resp.Close() + + var traitResp *Trait + decoder := json.NewDecoder(resp) + err = decoder.Decode(&traitResp) + return traitResp, err +} + +// DeleteTrait - permanently deletes a trait. +// +// https://wit.ai/docs/http/20200513/#delete__traits__trait_link +func (c *Client) DeleteTrait(name string) error { + resp, err := c.request(http.MethodDelete, fmt.Sprintf("/traits/%s", url.PathEscape(name)), "application/json", nil) + if err == nil { + resp.Close() + } + + return err +} + +// AddTraitValue - create a new value for a trait. +// +// https://wit.ai/docs/http/20200513/#post__traits__trait_values_link +func (c *Client) AddTraitValue(traitName string, value string) (*Trait, error) { + type traitValue struct { + Value string `json:"value"` + } + + valueJSON, err := json.Marshal(traitValue{Value: value}) + if err != nil { + return nil, err + } + + resp, err := c.request(http.MethodPost, fmt.Sprintf("/traits/%s/values", url.PathEscape(traitName)), "application/json", bytes.NewBuffer(valueJSON)) + if err != nil { + return nil, err + } + + defer resp.Close() + + var traitResp *Trait + decoder := json.NewDecoder(resp) + if err = decoder.Decode(&traitResp); err != nil { + return nil, err + } + + return traitResp, nil +} + +// DeleteTraitValue - permanently deletes the trait value. +// +// https://wit.ai/docs/http/20200513/#delete__traits__trait_values__value_link +func (c *Client) DeleteTraitValue(traitName string, value string) error { + resp, err := c.request(http.MethodDelete, fmt.Sprintf("/traits/%s/values/%s", url.PathEscape(traitName), url.PathEscape(value)), "application/json", nil) + if err == nil { + resp.Close() + } + + return err +} diff --git a/trait_test.go b/trait_test.go new file mode 100644 index 0000000..e6c4a76 --- /dev/null +++ b/trait_test.go @@ -0,0 +1,191 @@ +// Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved. + +package witai + +import ( + "net/http" + "net/http/httptest" + "reflect" + "testing" +) + +func TestGetTraits(t *testing.T) { + testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + w.Write([]byte(`[ + { + "id": "2690212494559269", + "name": "wit$sentiment" + }, + { + "id": "254954985556896", + "name": "faq" + } + ]`)) + })) + defer testServer.Close() + + c := NewClient(unitTestToken) + c.APIBase = testServer.URL + traits, _ := c.GetTraits() + + wantTraits := []Trait{ + {ID: "2690212494559269", Name: "wit$sentiment"}, + {ID: "254954985556896", Name: "faq"}, + } + + if !reflect.DeepEqual(traits, wantTraits) { + t.Fatalf("expected\n\ttraits: %v\n\tgot: %v", wantTraits, traits) + } +} + +func TestCreateTrait(t *testing.T) { + testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + w.Write([]byte(`{ + "id": "13989798788", + "name": "politeness", + "values": [ + { + "id": "97873388", + "value": "polite" + }, + { + "id": "54493392772", + "value": "rude" + } + ] + }`)) + })) + defer testServer.Close() + + c := NewClient(unitTestToken) + c.APIBase = testServer.URL + trait, err := c.CreateTrait("politeness", []string{"polite", "rude"}) + + wantTrait := &Trait{ + ID: "13989798788", + Name: "politeness", + Values: []TraitValue{ + {ID: "97873388", Value: "polite"}, + {ID: "54493392772", Value: "rude"}, + }, + } + + if err != nil { + t.Fatalf("nil error expected, got %v", err) + } + if !reflect.DeepEqual(wantTrait, trait) { + t.Fatalf("expected\n\ttrait: %v\n\tgot: %v", wantTrait, trait) + } +} + +func TestGetTrait(t *testing.T) { + testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + w.Write([]byte(`{ + "id": "13989798788", + "name": "politeness", + "values": [ + { + "id": "97873388", + "value": "polite" + }, + { + "id": "54493392772", + "value": "rude" + } + ] + }`)) + })) + defer testServer.Close() + + c := NewClient(unitTestToken) + c.APIBase = testServer.URL + trait, err := c.GetTrait("politeness") + + wantTrait := &Trait{ + ID: "13989798788", + Name: "politeness", + Values: []TraitValue{ + {ID: "97873388", Value: "polite"}, + {ID: "54493392772", Value: "rude"}, + }, + } + + if err != nil { + t.Fatalf("nil error expected, got %v", err) + } + if !reflect.DeepEqual(wantTrait, trait) { + t.Fatalf("expected\n\ttrait: %v\n\tgot: %v", wantTrait, trait) + } +} + +func TestDeleteTrait(t *testing.T) { + testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { + res.Write([]byte(`{"deleted": "politeness"}`)) + })) + defer func() { testServer.Close() }() + + c := NewClient(unitTestToken) + c.APIBase = testServer.URL + if err := c.DeleteTrait("politeness"); err != nil { + t.Fatalf("expected nil error, got: %v", err) + } +} + +func TestAddTraitValue(t *testing.T) { + testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + w.Write([]byte(`{ + "id": "13989798788", + "name": "politeness", + "values": [ + { + "id": "97873388", + "value": "polite" + }, + { + "id": "54493392772", + "value": "rude" + }, + { + "id": "828283932", + "value": "neutral" + } + ] + }`)) + })) + defer func() { testServer.Close() }() + + c := NewClient(unitTestToken) + c.APIBase = testServer.URL + trait, err := c.AddTraitValue("politeness", "neutral") + + wantTrait := &Trait{ + ID: "13989798788", + Name: "politeness", + Values: []TraitValue{ + {ID: "97873388", Value: "polite"}, + {ID: "54493392772", Value: "rude"}, + {ID: "828283932", Value: "neutral"}, + }, + } + + if err != nil { + t.Fatalf("nil error expected, got %v", err) + } + + if !reflect.DeepEqual(wantTrait, trait) { + t.Fatalf("expected\n\ttrait: %v\n\tgot: %v", wantTrait, trait) + } +} + +func TestDeleteTraitValue(t *testing.T) { + testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { + res.Write([]byte(`{"deleted": "neutral"}`)) + })) + defer func() { testServer.Close() }() + + c := NewClient(unitTestToken) + c.APIBase = testServer.URL + if err := c.DeleteTraitValue("politeness", "neutral"); err != nil { + t.Fatalf("expected nil error, got: %v", err) + } +} diff --git a/utterance.go b/utterance.go new file mode 100644 index 0000000..1eb48e1 --- /dev/null +++ b/utterance.go @@ -0,0 +1,145 @@ +package witai + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" +) + +// Utterance - https://wit.ai/docs/http/20200513/#get__utterances_link +type Utterance struct { + Text string `json:"text"` + Intent UtteranceIntent `json:"intent"` + Entities []UtteranceEntity `json:"entities"` + Traits []UtteranceTrait `json:"traits"` +} + +// UtteranceIntent - https://wit.ai/docs/http/20200513/#get__utterances_link +type UtteranceIntent struct { + ID string `json:"id"` + Name string `json:"name"` +} + +// UtteranceEntity - https://wit.ai/docs/http/20200513/#get__utterances_link +type UtteranceEntity struct { + ID string `json:"id"` + Name string `json:"name"` + Role string `json:"role"` + Start int `json:"start"` + End int `json:"end"` + Body string `json:"body"` + Entities []UtteranceEntity `json:"entities"` +} + +// UtteranceTrait - https://wit.ai/docs/http/20200513/#get__utterances_link +type UtteranceTrait struct { + ID string `json:"id"` + Name string `json:"name"` + Value string `json:"value"` +} + +// GetUtterances - Returns an array of utterances. +// +// https://wit.ai/docs/http/20200513/#get__utterances_link +func (c *Client) GetUtterances(limit int, offset int) ([]Utterance, error) { + if limit <= 0 { + limit = 0 + } + if offset <= 0 { + offset = 0 + } + + resp, err := c.request(http.MethodGet, fmt.Sprintf("/utterances?limit=%d&offset=%d", limit, offset), "application/json", nil) + if err != nil { + return []Utterance{}, err + } + + defer resp.Close() + + var utterances []Utterance + decoder := json.NewDecoder(resp) + err = decoder.Decode(&utterances) + return utterances, err +} + +// DeleteUtterances - Delete validated utterances from your app. +// +// https://wit.ai/docs/http/20200513/#delete__utterances_link +func (c *Client) DeleteUtterances(texts []string) (*TrainingResponse, error) { + type text struct { + Text string `json:"text"` + } + reqTexts := make([]text, len(texts)) + for i, t := range texts { + reqTexts[i] = text{Text: t} + } + + utterancesJSON, err := json.Marshal(reqTexts) + if err != nil { + return nil, err + } + + resp, err := c.request(http.MethodDelete, "/utterances", "application/json", bytes.NewBuffer(utterancesJSON)) + if err != nil { + return nil, err + } + + defer resp.Close() + + var r *TrainingResponse + decoder := json.NewDecoder(resp) + err = decoder.Decode(&r) + return r, err +} + +// TrainingResponse - https://wit.ai/docs/http/20200513/#post__utterances_link +type Training struct { + Text string `json:"text"` + Intent string `json:"intent,omitempty"` + Entities []TrainingEntity `json:"entities"` + Traits []TrainingTrait `json:"traits"` +} + +// TrainingResponse - https://wit.ai/docs/http/20200513/#post__utterances_link +type TrainingEntity struct { + Entity string `json:"entity"` + Start int `json:"start"` + End int `json:"end"` + Body string `json:"body"` + Entities []TrainingEntity `json:"entities"` +} + +// TrainingResponse - https://wit.ai/docs/http/20200513/#post__utterances_link +type TrainingTrait struct { + Trait string `json:"trait"` + Value string `json:"value"` +} + +// TrainingResponse - https://wit.ai/docs/http/20200513/#post__utterances_link +type TrainingResponse struct { + Sent bool `json:"sent"` + N int `json:"n"` +} + +// TrainUtterances - Add utterances (sentence + entities annotations) to train your app programmatically. +// +// https://wit.ai/docs/http/20200513/#post__utterances_link +func (c *Client) TrainUtterances(trainings []Training) (*TrainingResponse, error) { + utterancesJSON, err := json.Marshal(trainings) + if err != nil { + return nil, err + } + + resp, err := c.request(http.MethodPost, "/utterances", "application/json", bytes.NewBuffer(utterancesJSON)) + if err != nil { + return nil, err + } + + defer resp.Close() + + var r *TrainingResponse + decoder := json.NewDecoder(resp) + err = decoder.Decode(&r) + return r, err +} diff --git a/utterance_test.go b/utterance_test.go new file mode 100644 index 0000000..f6d0993 --- /dev/null +++ b/utterance_test.go @@ -0,0 +1,279 @@ +package witai + +import ( + "net/http" + "net/http/httptest" + "reflect" + "testing" +) + +func TestGetUtterances(t *testing.T) { + testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(`[ + { + "text": "text 1", + "intent": { + "id": "i1", + "name": "intent1" + }, + "entities": [ + { + "id": "e1", + "name": "entity1", + "role": "role1", + "start": 0, + "end": 10, + "body": "entity1", + "entities": [ + { + "id": "sub-e1", + "name": "sub-entity1", + "role": "sub-role1", + "start": 5, + "end": 7, + "body": "sub-entity1", + "entities": null + } + ] + }, + { + "id": "e2", + "name": "entity2", + "role": "role2", + "start": 10, + "end": 20, + "body": "entity2", + "entities": [ + { + "id": "sub-e2", + "name": "sub-entity2", + "role": "sub-role2", + "start": 15, + "end": 17, + "body": "sub-entity2", + "entities": null + } + ] + } + ], + "traits": [ + { + "id": "t1", + "name": "trait1", + "value": "value1" + }, + { + "id": "t2", + "name": "trait2", + "value": "value2" + } + ] + }, + { + "text": "text 2", + "intent": { + "id": "i2", + "name": "intent2" + }, + "entities": [ + { + "id": "e1", + "name": "entity1", + "role": "role1", + "start": 0, + "end": 10, + "body": "entity1", + "entities": [ + { + "id": "sub-e1", + "name": "sub-entity1", + "role": "sub-role1", + "start": 5, + "end": 7, + "body": "sub-entity1", + "entities": null + } + ] + }, + { + "id": "e2", + "name": "entity2", + "role": "role2", + "start": 10, + "end": 20, + "body": "entity2", + "entities": [ + { + "id": "sub-e2", + "name": "sub-entity2", + "role": "sub-role2", + "start": 15, + "end": 17, + "body": "sub-entity2", + "entities": null + } + ] + } + ], + "traits": [ + { + "id": "t1", + "name": "trait1", + "value": "value1" + }, + { + "id": "t2", + "name": "trait2", + "value": "value2" + } + ] + } + ]`)) + })) + defer testServer.Close() + + c := NewClient(unitTestToken) + c.APIBase = testServer.URL + utterances, _ := c.GetUtterances(5, 0) + + wantUtterances := []Utterance{ + { + Text: "text 1", + Intent: UtteranceIntent{ID: "i1", Name: "intent1"}, + Entities: []UtteranceEntity{ + { + ID: "e1", + Name: "entity1", + Role: "role1", + Start: 0, + End: 10, + Body: "entity1", + Entities: []UtteranceEntity{ + { + ID: "sub-e1", + Name: "sub-entity1", + Role: "sub-role1", + Start: 5, + End: 7, + Body: "sub-entity1", + }, + }, + }, + { + ID: "e2", + Name: "entity2", + Role: "role2", + Start: 10, + End: 20, + Body: "entity2", + Entities: []UtteranceEntity{ + { + ID: "sub-e2", + Name: "sub-entity2", + Role: "sub-role2", + Start: 15, + End: 17, + Body: "sub-entity2", + }, + }, + }, + }, + Traits: []UtteranceTrait{ + {ID: "t1", Name: "trait1", Value: "value1"}, + {ID: "t2", Name: "trait2", Value: "value2"}, + }, + }, + { + Text: "text 2", + Intent: UtteranceIntent{ID: "i2", Name: "intent2"}, + Entities: []UtteranceEntity{ + { + ID: "e1", + Name: "entity1", + Role: "role1", + Start: 0, + End: 10, + Body: "entity1", + Entities: []UtteranceEntity{ + { + ID: "sub-e1", + Name: "sub-entity1", + Role: "sub-role1", + Start: 5, + End: 7, + Body: "sub-entity1", + }, + }, + }, + { + ID: "e2", + Name: "entity2", + Role: "role2", + Start: 10, + End: 20, + Body: "entity2", + Entities: []UtteranceEntity{ + { + ID: "sub-e2", + Name: "sub-entity2", + Role: "sub-role2", + Start: 15, + End: 17, + Body: "sub-entity2", + }, + }, + }, + }, + Traits: []UtteranceTrait{ + {ID: "t1", Name: "trait1", Value: "value1"}, + {ID: "t2", Name: "trait2", Value: "value2"}, + }, + }, + } + + if !reflect.DeepEqual(utterances, wantUtterances) { + t.Fatalf("expected \n\tmsg %+v \n\tgot %+v", wantUtterances, utterances) + } +} + +func TestDeleteUtterances(t *testing.T) { + testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(`{"sent": true, "n": 2}`)) + })) + defer testServer.Close() + + c := NewClient(unitTestToken) + c.APIBase = testServer.URL + resp, _ := c.DeleteUtterances([]string{"text1", "text2"}) + + wantResp := &TrainingResponse{ + Sent: true, + N: 2, + } + + if !reflect.DeepEqual(resp, wantResp) { + t.Fatalf("expected \n\tresp %+v \n\tgot %+v", wantResp, resp) + } +} + +func TestTrainUtterances(t *testing.T) { + testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(`{"sent": true, "n": 2}`)) + })) + defer testServer.Close() + + c := NewClient(unitTestToken) + c.APIBase = testServer.URL + resp, _ := c.TrainUtterances([]Training{ + {Text: "text1"}, + }) + + wantResp := &TrainingResponse{ + Sent: true, + N: 2, + } + + if !reflect.DeepEqual(resp, wantResp) { + t.Fatalf("expected \n\tresp %+v \n\tgot %+v", wantResp, resp) + } +} diff --git a/witai.go b/witai.go index 0508d05..4ad97f1 100644 --- a/witai.go +++ b/witai.go @@ -11,8 +11,8 @@ import ( ) const ( - // DefaultVersion - https://wit.ai/docs/http/20170307 - DefaultVersion = "20170307" + // DefaultVersion - https://wit.ai/docs/http/20200513/ + DefaultVersion = "20200513" // WitTimeFormat - the custom format of the timestamp sent by the api WitTimeFormat = "2006-01-02T15:04:05Z0700" ) @@ -34,11 +34,10 @@ type errorResp struct { // NewClient - returns Wit.ai client for default API version func NewClient(token string) *Client { - return NewClientWithVersion(token, DefaultVersion) + return newClientWithVersion(token, DefaultVersion) } -// NewClientWithVersion - returns Wit.ai client for specified API version -func NewClientWithVersion(token, version string) *Client { +func newClientWithVersion(token, version string) *Client { headerAuth := fmt.Sprintf("Bearer %s", token) headerAccept := fmt.Sprintf("application/vnd.wit.%s+json", version) diff --git a/witai_test.go b/witai_test.go index 311837a..73b9351 100644 --- a/witai_test.go +++ b/witai_test.go @@ -15,13 +15,3 @@ func TestNewClient(t *testing.T) { t.Fatalf("client default version is not set") } } - -func TestNewClientWithVersion(t *testing.T) { - c := NewClientWithVersion("token", "v2") - if c == nil { - t.Fatalf("client is nil") - } - if c.Version != "v2" { - t.Fatalf("client v2 version is not set") - } -}