Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 72 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,70 @@ this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
For releases prior to `0.2.0`, see the auto-generated notes on the
[GitHub Releases](https://github.com/chatbotkit/go-sdk/releases) page.

## [Unreleased]

## [0.5.0] - 2026-09-18

### Added

- `Options.Token` is the new name for the API credential. `Options.Secret` is
deprecated and still works; `Token` wins when both are set.
- `client.Decision.Create` asks a decision model typed questions (boolean,
choice, score) about a state and returns an answer with probabilities for
each. The request is the hand-written `sdk.DecisionCreateRequest`, whose
state, instructions and criteria take any JSON value, because the generated
request type cannot hold the named options of a choice question.

### Fixed

- A path prefix on `Options.BaseURL` was dropped, so a platform served under a
sub-path (for example `http://localhost:3000/cbk`) was unreachable. The prefix
is now preserved for regular, raw and streaming requests. Plain `http` base
URLs work for local use.
- The SDK did not build: `types/aliases.go` still pointed at generated names
that the last types regeneration had renamed. The aliases are updated, and
the names consumers use are unchanged.

### Changed

- **BREAKING:** skillset ability link fields renamed. On create/update/fetch/
list/export, `secretId` / `fileId` / `botId` / `spaceId` are now
`linkedSecretId` / `linkedFileId` / `linkedBotId` / `linkedSpaceId`. Inline
conversation `extensions.skillsets[].abilities[]` entries use
`linkedSecretId` (and the new `linkedSpaceId`). GraphQL `Ability` relations
`secret` / `file` / `bot` / `space` are now `linkedSecret` / `linkedFile` /
`linkedBot` / `linkedSpace`. There are no compatibility aliases; upgrade
together with the platform deploy.
- Regenerated types also pick up unrelated API changes since the previous
regeneration (2026-08-19), grouped by resource:
- **BREAKING:** Dataset: `store` removed from `DatasetCreateRequest`,
`DatasetFetchResponse`, `DatasetListResponseItem` and
`DatasetListStreamItemData` (the platform now has a single vector store;
the REST API accepts and ignores `store`).
- Conversation: `expiresAt` (epoch ms, auto-delete) added to
`ConversationUpdateRequest`, `ConversationFetchResponse`,
`ConversationListResponseItem` and `ConversationListStreamItemData`.
- Memory: `expiresAt` added to `MemoryCreateRequest`, `MemoryUpdateRequest`,
`MemoryFetchResponse`, `MemoryListResponseItem` and
`MemoryListStreamItemData`.
- Task: `expiresAt` added to `TaskCreateRequest`, `TaskUpdateRequest`,
`TaskFetchResponse`, `TaskListResponseItem` and `TaskListStreamItemData`;
`resumeAt` (when a paused run resumes, null while running) added to
`TaskExecutionListResponseItem` and `TaskExecutionListStreamItemData`.
- Policy: `state` (`enabled` | `disabled`) added to `PolicyCreateRequest`,
`PolicyUpdateRequest`, `PolicyFetchResponse`, `PolicyListResponseItem` and
`PolicyListStreamItemData`.
- WhatsApp integration: `appSecret` (Meta app secret for webhook signature
validation, masked as `********` on read) added to
`IntegrationWhatsAppCreateRequest`, `IntegrationWhatsAppUpdateRequest`,
`IntegrationWhatsAppFetchResponse`, `IntegrationWhatsAppListResponseItem`
and `IntegrationWhatsAppListStreamItemData`; `idempotencyKey` added to
`WhatsappInitiateRequest`.
- GitHub integration: `allowFrom` (allowed senders) added to
`GithubIntegrationCreateRequest`.
- `agent.convertMessageExtensions` now forwards `LinkedSpaceID` alongside
`LinkedSecretID` for inline abilities.

## [0.4.0] - 2026-06-27

### Added
Expand All @@ -16,9 +80,14 @@ For releases prior to `0.2.0`, see the auto-generated notes on the
secrets only; owner-only) and return `{ Token, ExpiresAt }`.
`SecretClient.Proxy` / `ContactSecretClient.Proxy` proxy a request through a
secret — the credential is injected server-side (it never leaves the platform)
and the raw upstream `*http.Response` is returned verbatim. A non-2xx status
(including `409 authorization_required`) is returned, not an error; the caller
closes `resp.Body`. Backed by the new internal `httpclient.DoRaw`.
and the upstream `*http.Response` is returned as-is, success or error (the
caller closes `resp.Body`).
- `AuthorizationRequiredError` (aliased into the `sdk` package; detect with
`errors.As`) carrying the `URL` the user must visit to authorize. It is
returned when a secret or connection has not been authenticated yet
(`409 authorization_required`) — by `Mint`, by any normal route, and by
`Proxy` (which otherwise passes every genuine upstream response through
untouched). Generic API `Error`s now also carry the HTTP `Status`.

## [0.3.0] - 2026-06-26

Expand Down
13 changes: 6 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
[![ChatBotKit](https://img.shields.io/badge/credits-ChatBotKit-blue.svg)](https://chatbotkit.com)
[![CBK.AI](https://img.shields.io/badge/credits-CBK.AI-blue.svg)](https://cbk.ai)
[![Email](https://img.shields.io/badge/Email-Support-blue?logo=mail.ru)](mailto:support@chatbotkit.com)
[![Email](https://img.shields.io/badge/Email-Support-blue?logo=mail.ru)](mailto:support@cbk.ai)
[![Discord](https://img.shields.io/badge/Discord-Support-blue?logo=discord)](https://go.cbk.ai/discord)
[![Go Reference](https://pkg.go.dev/badge/github.com/chatbotkit/go-sdk.svg)](https://pkg.go.dev/github.com/chatbotkit/go-sdk)
[![Follow on Twitter](https://img.shields.io/twitter/follow/chatbotkit.svg?logo=twitter)](https://twitter.com/chatbotkit)
Expand Down Expand Up @@ -58,9 +57,9 @@ import (
)

func main() {
// Create a client with your API key
// Create a client with your API token
client := sdk.New(sdk.Options{
Secret: "your-api-key",
Token: "your-api-token",
})

// Run a simple conversation
Expand Down Expand Up @@ -95,7 +94,7 @@ The main `sdk` package provides access to all ChatBotKit API resources:

```go
client := sdk.New(sdk.Options{
Secret: "your-api-key",
Token: "your-api-token",
BaseURL: "https://api.chatbotkit.com", // optional
RunAsUserID: "user-id", // optional
Timezone: "America/New_York", // optional
Expand Down Expand Up @@ -132,7 +131,7 @@ client.Integration.McpServer // MCP server integrations
client.Integration.Microsoftteams // Microsoft Teams integrations
client.Integration.GoogleChat // Google Chat integrations
client.Memory // Memory management
client.Partner // Partner operations
client.User // User operations
client.Platform // Platform content and catalogue access
client.Policy // Policy management
client.Portal // Portal management
Expand Down Expand Up @@ -565,7 +564,7 @@ var resp types.BotCreateResponse
To regenerate the types from the latest API spec:

```bash
cd sites/main
cd platform/platform
pnpm script:generate-api-types --output ../../sdks/go/types/types.go --package types
```

Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.4.0
0.5.0
13 changes: 7 additions & 6 deletions agent/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
//
// Example usage:
//
// client := sdk.New(sdk.Options{Secret: "your-api-key"})
// client := sdk.New(sdk.Options{Token: "your-api-token"})
//
// result, err := agent.Complete(ctx, client, agent.CompleteOptions{
// Model: "gpt-4o",
Expand Down Expand Up @@ -1302,11 +1302,12 @@ func convertMessageExtensions(extensions *types.ConversationCompleteRequestExten
converted.Abilities = make([]types.MessageCompleteAbility, 0, len(skillset.Abilities))
for _, ability := range skillset.Abilities {
converted.Abilities = append(converted.Abilities, types.MessageCompleteAbility{
Description: ability.Description,
Instruction: ability.Instruction,
Meta: ability.Meta,
Name: ability.Name,
SecretID: ability.SecretID,
Description: ability.Description,
Instruction: ability.Instruction,
Meta: ability.Meta,
Name: ability.Name,
LinkedSecretID: ability.LinkedSecretID,
LinkedSpaceID: ability.LinkedSpaceID,
})
}
}
Expand Down
110 changes: 103 additions & 7 deletions agent/agent_internal_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package agent

import (
"reflect"
"strings"
"testing"

Expand Down Expand Up @@ -50,6 +51,7 @@ func TestConvertMessageExtensions(t *testing.T) {
datasetName := "kb"
skillsetName := "ops"
secretID := "secret_123"
spaceID := "space_123"

extensions := &types.ConversationCompleteRequestExtensions{
Backstory: &backstory,
Expand All @@ -67,11 +69,12 @@ func TestConvertMessageExtensions(t *testing.T) {
Skillsets: []types.CompleteSkillset{{
Name: &skillsetName,
Abilities: []types.CompleteAbility{{
Name: "lookup",
Description: "Look something up",
Instruction: "Search the knowledge base",
Meta: map[string]interface{}{"kind": "search"},
SecretID: &secretID,
Name: "lookup",
Description: "Look something up",
Instruction: "Search the knowledge base",
Meta: map[string]interface{}{"kind": "search"},
LinkedSecretID: &secretID,
LinkedSpaceID: &spaceID,
}},
}},
}
Expand All @@ -92,7 +95,100 @@ func TestConvertMessageExtensions(t *testing.T) {
if len(converted.Skillsets) != 1 || len(converted.Skillsets[0].Abilities) != 1 {
t.Fatalf("unexpected skillset conversion: %#v", converted.Skillsets)
}
if converted.Skillsets[0].Abilities[0].SecretID == nil || *converted.Skillsets[0].Abilities[0].SecretID != secretID {
t.Fatalf("unexpected ability conversion: %#v", converted.Skillsets[0].Abilities[0])
ability := converted.Skillsets[0].Abilities[0]
if ability.LinkedSecretID == nil || *ability.LinkedSecretID != secretID {
t.Fatalf("unexpected ability linkedSecretId: %#v", ability)
}
if ability.LinkedSpaceID == nil || *ability.LinkedSpaceID != spaceID {
t.Fatalf("unexpected ability linkedSpaceId: %#v", ability)
}
}

// fillStructNonZero sets every exported field of the struct pointed to by v to
// a non-zero value derived from the field name, so a round-trip can detect any
// field that is silently dropped.
func fillStructNonZero(t *testing.T, v reflect.Value) {
t.Helper()

for i := 0; i < v.NumField(); i++ {
field := v.Field(i)
name := v.Type().Field(i).Name
if !field.CanSet() {
continue
}
switch field.Kind() {
case reflect.String:
field.SetString(name)
case reflect.Ptr:
elem := reflect.New(field.Type().Elem())
if elem.Elem().Kind() == reflect.String {
elem.Elem().SetString(name)
} else if elem.Elem().Kind() == reflect.Struct {
fillStructNonZero(t, elem.Elem())
}
field.Set(elem)
case reflect.Map:
m := reflect.MakeMap(field.Type())
m.SetMapIndex(reflect.ValueOf(name), reflect.ValueOf(name).Convert(field.Type().Elem()))
field.Set(m)
case reflect.Slice:
s := reflect.MakeSlice(field.Type(), 1, 1)
if s.Index(0).Kind() == reflect.Struct {
fillStructNonZero(t, s.Index(0))
}
field.Set(s)
case reflect.Bool:
field.SetBool(true)
case reflect.Int, reflect.Int64, reflect.Float64:
field.SetInt(1)
default:
t.Fatalf("fillStructNonZero: unsupported kind %s for field %s", field.Kind(), name)
}
}
}

// TestConvertMessageExtensionsAbilityParity is a parity guard: every field on
// the inline ability request type must survive convertMessageExtensions. If the
// generated types gain a new ability field that the conversion does not copy,
// this test fails.
func TestConvertMessageExtensionsAbilityParity(t *testing.T) {
inType := reflect.TypeOf(types.CompleteAbility{})
outType := reflect.TypeOf(types.MessageCompleteAbility{})

for i := 0; i < inType.NumField(); i++ {
if _, ok := outType.FieldByName(inType.Field(i).Name); !ok {
t.Fatalf("output ability type is missing input field %s", inType.Field(i).Name)
}
}
for i := 0; i < outType.NumField(); i++ {
if _, ok := inType.FieldByName(outType.Field(i).Name); !ok {
t.Fatalf("input ability type is missing output field %s", outType.Field(i).Name)
}
}

var input types.CompleteAbility
fillStructNonZero(t, reflect.ValueOf(&input).Elem())

skillsetName := "ops"
converted := convertMessageExtensions(&types.ConversationCompleteRequestExtensions{
Skillsets: []types.CompleteSkillset{{
Name: &skillsetName,
Abilities: []types.CompleteAbility{input},
}},
})
if converted == nil || len(converted.Skillsets) != 1 || len(converted.Skillsets[0].Abilities) != 1 {
t.Fatalf("unexpected skillset conversion: %#v", converted)
}

out := reflect.ValueOf(converted.Skillsets[0].Abilities[0])
for i := 0; i < out.NumField(); i++ {
name := out.Type().Field(i).Name
if out.Field(i).IsZero() {
t.Fatalf("convertMessageExtensions dropped ability field %s", name)
}
in := reflect.ValueOf(input).FieldByName(name)
if !reflect.DeepEqual(in.Interface(), out.Field(i).Interface()) {
t.Fatalf("ability field %s changed during conversion: %#v != %#v", name, in.Interface(), out.Field(i).Interface())
}
}
}
28 changes: 14 additions & 14 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ This directory contains example applications demonstrating how to use the ChatBo
## Prerequisites

- Go 1.21 or later
- A ChatBotKit API key (get one at [chatbotkit.com](https://chatbotkit.com))
- A ChatBotKit API token (get one at [chatbotkit.com](https://chatbotkit.com))

## Examples

Expand All @@ -24,11 +24,11 @@ A simple interactive chatbot that demonstrates:
cd examples/chatbot

# Option 1: Use a .env file
echo 'CHATBOTKIT_API_SECRET=your-api-key' > .env
echo 'CHATBOTKIT_API_TOKEN=your-api-token' > .env
go run main.go

# Option 2: Use environment variable
export CHATBOTKIT_API_SECRET="your-api-key"
export CHATBOTKIT_API_TOKEN="your-api-token"
go run main.go
```

Expand Down Expand Up @@ -61,8 +61,8 @@ An autonomous agent that completes tasks end-to-end without interactive input:
```bash
cd examples/agent

# Set your API key
export CHATBOTKIT_API_SECRET="your-api-key"
# Set your API token
export CHATBOTKIT_API_TOKEN="your-api-token"

# Run with a custom task
go run main.go "Create a file called hello.txt with the content 'Hello, World!'"
Expand Down Expand Up @@ -115,11 +115,11 @@ An example demonstrating interactive chat with custom tool registration:
cd examples/agent-with-tools

# Option 1: Use a .env file
echo 'CHATBOTKIT_API_SECRET=your-api-key' > .env
echo 'CHATBOTKIT_API_TOKEN=your-api-token' > .env
go run main.go

# Option 2: Use environment variable
export CHATBOTKIT_API_SECRET="your-api-key"
export CHATBOTKIT_API_TOKEN="your-api-token"
go run main.go
```

Expand Down Expand Up @@ -185,7 +185,7 @@ An autonomous agent that ships with skill definitions baked into the binary usin
```bash
cd examples/agent-with-embedded-skills

export CHATBOTKIT_API_SECRET="your-api-key"
export CHATBOTKIT_API_TOKEN="your-api-token"

# Run with a custom task
go run . "Review this Go function: func add(a, b int) int { return a - b }"
Expand Down Expand Up @@ -233,8 +233,8 @@ Demonstrates how to manually drive `agent.CompleteWithTools`, giving you full co
```bash
cd examples/stateless-agent

# Set your API key
export CHATBOTKIT_API_SECRET="your-api-key"
# Set your API token
export CHATBOTKIT_API_TOKEN="your-api-token"

# Run the example
go run main.go
Expand Down Expand Up @@ -285,8 +285,8 @@ Demonstrates the same manually-driven `agent.CompleteWithTools` pattern against
```bash
cd examples/stateful-agent

# Set your API key
export CHATBOTKIT_API_SECRET="your-api-key"
# Set your API token
export CHATBOTKIT_API_TOKEN="your-api-token"

# Run the example
go run main.go
Expand Down Expand Up @@ -316,7 +316,7 @@ for iterationCount < maxIterations {
You can also run examples from the SDK root directory:

```bash
export CHATBOTKIT_API_SECRET="your-api-key"
export CHATBOTKIT_API_TOKEN="your-api-token"
go run ./examples/chatbot
go run ./examples/agent "Create a file called test.txt"
go run ./examples/agent-with-tools
Expand All @@ -329,7 +329,7 @@ go run ./examples/stateful-agent

| Variable | Description | Required |
| ----------------------- | ----------------------- | -------- |
| `CHATBOTKIT_API_SECRET` | Your ChatBotKit API key | Yes |
| `CHATBOTKIT_API_TOKEN` | Your ChatBotKit API token | Yes |

## Creating Your Own Chatbot

Expand Down
Loading
Loading