Skip to content

Go SDK

The Go SDK gives your code the ten functions as methods on a client; tt.Answer turns each result into a typed answer and an error, such as true or false from decide.

main.go
package main
import (
"context"
"errors"
"fmt"
"log"
tt "github.com/botassembly/thinkthen/sdk/go/thinkthen"
)
func main() {
client, err := tt.New(tt.Config{})
if err != nil {
log.Fatal(err)
}
defer client.Close()
refundQuestion := tt.NewDecide("Does the customer request a refund?")
isRefund, err := tt.Answer(client.Decide(context.Background(),
refundQuestion, tt.Text("Please refund my order.")))
switch {
case errors.Is(err, tt.ErrUnsure):
fmt.Println("not sure: send to a person")
case err != nil:
log.Fatal(err)
default:
fmt.Println(isRefund)
}
}

Output

true

Install it (Go 1.23 or later, with cgo and a C compiler):

Terminal window
go get github.com/botassembly/thinkthen/sdk/go/thinkthen@v0.2.0

The client uses the key you set in the Quickstart. Call client.Close() when you're done with it.

Give decide a low and a high bar, and a text that falls between them is not sure. tt.Answer then returns tt.ErrUnsure, and the answer beside it is the zero value, false. Check the error before the answer, or not sure reads as a no. This message lands between the bars:

main.go
package main
import (
"context"
"errors"
"fmt"
"log"
tt "github.com/botassembly/thinkthen/sdk/go/thinkthen"
)
func main() {
client, err := tt.New(tt.Config{})
if err != nil {
log.Fatal(err)
}
defer client.Close()
ctx := context.Background()
refundQuestion := tt.NewDecide(
"Does the customer request a refund?",
).WithThreshold(tt.Band(0.2, 0.8))
message := tt.Text("I was charged twice for one invoice.")
isRefund, err := tt.Answer(
client.Decide(ctx, refundQuestion, message))
switch {
case errors.Is(err, tt.ErrUnsure):
fmt.Println("Not sure. Ask a person.")
case err != nil:
log.Fatal(err)
case isRefund:
fmt.Println("Send to refunds.")
default:
fmt.Println("Leave in the inbox.")
}
}

Output

Not sure. Ask a person.

Send not sure to a person.

Blank text fails at once, and nothing is sent. The error is a *tt.Error, and its Code() names the problem:

main.go
package main
import (
"context"
"errors"
"fmt"
"log"
tt "github.com/botassembly/thinkthen/sdk/go/thinkthen"
)
func main() {
client, err := tt.New(tt.Config{})
if err != nil {
log.Fatal(err)
}
defer client.Close()
ctx := context.Background()
refundQuestion := tt.NewDecide(
"Does the customer request a refund?")
_, err = tt.Answer(
client.Decide(ctx, refundQuestion, tt.Text("")))
var failure *tt.Error
if errors.As(err, &failure) {
fmt.Println(failure.Code())
}
}

Output

record.empty

Any other failure, such as the service being down, comes back as a result whose Status() is tt.Failed, and tt.Answer returns its error. Match a family with errors.Is(err, tt.ErrRecord), or one code with errors.Is(err, tt.CodeRecordEmpty). A problem with the client itself, such as a wrong argument, comes back from tt.New or the call. Troubleshooting lists each code and its fix.

Each example asks about the same support-inbox messages as the function pages. Each function has a one-message call, such as client.Decide, and an All call, such as client.DecideAll, that takes many messages and yields a result for each in a range loop. In an All loop, a failed message arrives as a result, and the loop's own error means the whole call failed.

decide: yes, no or not sure
main.go
package main
import (
"context"
"errors"
"fmt"
"log"
tt "github.com/botassembly/thinkthen/sdk/go/thinkthen"
)
func main() {
client, err := tt.New(tt.Config{})
if err != nil {
log.Fatal(err)
}
defer client.Close()
refundQuestion := tt.NewDecide("Does the customer request a refund?")
isRefund, err := tt.Answer(client.Decide(context.Background(),
refundQuestion, tt.Text("Please refund my order.")))
switch {
case errors.Is(err, tt.ErrUnsure):
fmt.Println("not sure: send to a person")
case err != nil:
log.Fatal(err)
default:
fmt.Println(isRefund)
}
}

Output

true

The message asks for a refund, so decide says true. .WithThreshold(tt.Cutoff(0.8)) moves the bar, and tt.Band(0.2, 0.8) sets a low and a high bar, as above. .WithField("/text") picks the field to judge when a message is JSON. decide shows every answer.

choose: one answer from your list
main.go
package main
import (
"context"
"fmt"
"log"
tt "github.com/botassembly/thinkthen/sdk/go/thinkthen"
)
func main() {
client, err := tt.New(tt.Config{})
if err != nil {
log.Fatal(err)
}
defer client.Close()
ctx := context.Background()
topicQuestion := tt.NewChoose(
"What is the message about?",
[]string{"billing", "shipping", "other"})
message := tt.Text("Please refund my order.")
topic, err := tt.Answer(
client.Choose(ctx, topicQuestion, message))
if err != nil {
log.Fatal(err)
}
fmt.Println(topic)
}

Output

billing

A refund request is about billing. With .WithThreshold(tt.Cutoff(0.8)), choose is not sure when no choice reaches the bar, and tt.Answer returns tt.ErrUnsure with an empty string. client.ChooseAll reads many messages:

main.go
package main
import (
"context"
"errors"
"fmt"
"log"
tt "github.com/botassembly/thinkthen/sdk/go/thinkthen"
)
func main() {
client, err := tt.New(tt.Config{})
if err != nil {
log.Fatal(err)
}
defer client.Close()
topicQuestion := tt.NewChoose("What is the message about?",
[]string{"billing", "shipping", "other"})
inbox := tt.Texts(
"Please refund my order.",
"Where is my parcel?",
"I was charged twice for one invoice.",
)
// ChooseAll sends every message in one call and yields rows in input order.
for message, err := range client.ChooseAll(
context.Background(), topicQuestion, inbox,
) {
if err != nil {
log.Fatal(err) // the call failed, not one message
}
topic, err := message.Answer()
switch {
case errors.Is(err, tt.ErrUnsure):
fmt.Println(message.Position(), "unsure: send to a person")
case err != nil:
// prints the status, code and reason
fmt.Println(message.Position(), message)
default:
fmt.Println(message.Position(), topic)
}
}
}

Output

1 billing
2 shipping
3 billing

choose shows every answer.

filter: keep the messages that answer yes
main.go
package main
import (
"context"
"fmt"
"log"
tt "github.com/botassembly/thinkthen/sdk/go/thinkthen"
)
func main() {
client, err := tt.New(tt.Config{})
if err != nil {
log.Fatal(err)
}
defer client.Close()
ctx := context.Background()
refundQuestion := tt.NewFilter(
"Does the customer request a refund?")
messages := tt.Texts(
"Please refund my order.",
"Where is my parcel?",
"I was charged twice for one invoice.",
)
for kept, err := range client.FilterAll(
ctx, refundQuestion, messages) {
if err != nil {
log.Fatal(err)
}
fmt.Println(kept.Item())
}
}

Output

Please refund my order.
I was charged twice for one invoice.

FilterAll yields the messages that answer yes: the refund request and the double charge. Item() is your own message. Not-sure messages are left out. A failed message arrives with its error in its place, and the loop may go on; this example stops on it. client.FilterRowsAll yields a result for every message instead, with Kept(). filter shows every answer.

tag: every label that fits
main.go
package main
import (
"context"
"fmt"
"log"
tt "github.com/botassembly/thinkthen/sdk/go/thinkthen"
)
func main() {
client, err := tt.New(tt.Config{})
if err != nil {
log.Fatal(err)
}
defer client.Close()
ctx := context.Background()
topicsQuestion := tt.NewTag("Which topics appear?",
[]string{"billing", "shipping"})
message := tt.Text(
"I was charged twice and my parcel is late again.")
topics, err := tt.Answer(
client.Tag(ctx, topicsQuestion, message))
if err != nil {
log.Fatal(err)
}
fmt.Println(topics)
}

Output

[billing shipping]

The message names a double charge and a late parcel, so both labels fit. The answer is a []string, empty when none fits. tag shows every answer.

score: a number from 0 to 1
main.go
package main
import (
"context"
"fmt"
"log"
tt "github.com/botassembly/thinkthen/sdk/go/thinkthen"
)
func main() {
client, err := tt.New(tt.Config{})
if err != nil {
log.Fatal(err)
}
defer client.Close()
ctx := context.Background()
urgencyQuestion := tt.NewScore(
"How urgent is the request?",
[]string{"routine", "urgent"})
message := tt.Text("Please refund my order.")
urgency, err := tt.Answer(
client.Score(ctx, urgencyQuestion, message))
if err != nil {
log.Fatal(err)
}
fmt.Println(urgency)
}

Output

0.05

A refund request is routine, so it scores near 0. The answer is a float64, and levels run lowest first. score shows every answer.

rank: put messages in order
main.go
package main
import (
"context"
"fmt"
"log"
tt "github.com/botassembly/thinkthen/sdk/go/thinkthen"
)
func main() {
client, err := tt.New(tt.Config{})
if err != nil {
log.Fatal(err)
}
defer client.Close()
ctx := context.Background()
urgencyQuestion := tt.NewRank(
"How urgent is the request?",
[]string{"routine", "urgent"})
messages := tt.Texts(
"Please refund my order.",
"Where is my parcel?",
"I was charged twice for one invoice.",
)
for ranked, err := range client.RankAll(
ctx, urgencyQuestion, messages) {
if err != nil {
log.Fatal(err)
}
rank, err := ranked.Answer()
if err != nil {
log.Fatal(err)
}
fmt.Println(rank, ranked.Item())
}
}

Output

1 I was charged twice for one invoice.
2 Where is my parcel?
3 Please refund my order.

The double charge is the most urgent, so it ranks 1. The answer is an int, and Item() is the message it ranks. .WithTop(2) ranks only the first two. rank shows every answer.

find: the line that answers
main.go
package main
import (
"context"
"fmt"
"log"
tt "github.com/botassembly/thinkthen/sdk/go/thinkthen"
)
func main() {
client, err := tt.New(tt.Config{})
if err != nil {
log.Fatal(err)
}
defer client.Close()
ctx := context.Background()
actionQuestion := tt.NewFind(
"Which line states the requested action?")
message := tt.Text("Morgan works for Example Group.\n" +
"Please refund my order.")
action, err := tt.Answer(
client.Find(ctx, actionQuestion, message))
if err != nil {
log.Fatal(err)
}
if action == nil {
fmt.Println("No line answers.")
} else {
fmt.Println(action.Text)
}
}

Output

Please refund my order.

The second line holds the request. The answer is a *tt.Span with Text, Start and End, counted in bytes, and it is nil when no line answers. find shows every answer.

recognize: names and their kinds
main.go
package main
import (
"context"
"fmt"
"log"
tt "github.com/botassembly/thinkthen/sdk/go/thinkthen"
)
func main() {
client, err := tt.New(tt.Config{})
if err != nil {
log.Fatal(err)
}
defer client.Close()
ctx := context.Background()
kinds := tt.NewRecognize(
[]string{"person", "organization"})
message := tt.Text("Morgan works for Example Group.")
names, err := tt.Answer(
client.Recognize(ctx, kinds, message))
if err != nil {
log.Fatal(err)
}
for _, name := range names {
fmt.Println(name.Kind, name.Text)
}
}

Output

person Morgan
organization Example Group

Morgan is a person and Example Group an organization. Each tt.Name also has Start, End and Probability. recognize shows every answer.

relate: how names connect
main.go
package main
import (
"context"
"fmt"
"log"
tt "github.com/botassembly/thinkthen/sdk/go/thinkthen"
)
func main() {
client, err := tt.New(tt.Config{})
if err != nil {
log.Fatal(err)
}
defer client.Close()
ctx := context.Background()
worksFor := tt.NewRelate([]tt.Rule{{
Name: "works_for",
Source: "person",
Target: "organization",
}})
message := tt.Text("Morgan works for Example Group.\n" +
"Please refund my order.")
relations, err := tt.Answer(
client.Relate(ctx, worksFor, message))
if err != nil {
log.Fatal(err)
}
for _, r := range relations {
fmt.Println(r.Source.Text, r.Label, r.Target.Text)
}
}

Output

Morgan works_for Example Group

Morgan works for Example Group, so relate links the two by the rule. relate finds the names itself, so it needs no recognize call first. Each tt.Relation also has Probability. relate shows every answer.

annotate: several questions at once
main.go
package main
import (
"context"
"fmt"
"log"
tt "github.com/botassembly/thinkthen/sdk/go/thinkthen"
)
func main() {
client, err := tt.New(tt.Config{})
if err != nil {
log.Fatal(err)
}
defer client.Close()
ctx := context.Background()
form := tt.NewAnnotate(map[string]tt.AnnotationQuestion{
"refund": tt.NewDecide(
"Does the customer request a refund?"),
"topic": tt.NewChoose("What is the message about?",
[]string{"billing", "shipping", "other"}),
"urgency": tt.NewScore("How urgent is the request?",
[]string{"routine", "urgent"}),
})
messages := tt.Texts(
"Please refund my order.",
"Where is my parcel?",
"I was charged twice for one invoice.",
)
for row, err := range client.AnnotateAll(
ctx, form, messages) {
if err != nil {
log.Fatal(err)
}
answers, err := row.Answer()
if err != nil {
log.Fatal(err)
}
isRefund, err := tt.Answer(answers.Decide("refund"))
if err != nil {
log.Fatal(err)
}
topic, err := tt.Answer(answers.Choose("topic"))
if err != nil {
log.Fatal(err)
}
urgency, err := tt.Answer(answers.Score("urgency"))
if err != nil {
log.Fatal(err)
}
fmt.Println(isRefund, topic, urgency)
}
}

Output

true billing 0.05
false shipping 0.14
true billing 0.45

Each message gets all three answers: refund or not, the topic, and its urgency. answers.Decide("refund") and the other selectors pick one answer, and tt.Answer reads it as a one-question call does, so a not-sure answer inside returns tt.ErrUnsure too. client.Annotate answers one message. annotate shows every answer.

tt.Config sets the batch size and retries, and tt.JSON and tt.Records build messages with ids. The Go source lists every name.