Documentation
¶
Overview ¶
Package chatclient provides direct, optional conveniences around the minimal chat protocols and model capabilities defined by Core. Client.Output applies a provider-neutral OutputFormat and strictly decodes naturally completed text. Other completion reasons remain identifiable as OutputCompletionError. Typed output has only a terminal value; callers that need transport deltas use StreamClient.Stream with a required streaming dependency.
NewSingleBatchToolMiddleware covers the deliberately small direct-use path of one tool batch and one follow-up call. Further tool rounds, retries, concurrency, approval, and durable execution belong to Agent.
Example ¶
package main
import (
"context"
"fmt"
"github.com/Tangerg/scope/core/chat"
"github.com/Tangerg/scope/core/chatclient"
)
func main() {
model := chat.ModelFunc(func(context.Context, *chat.Request) (*chat.Response, error) {
return textResponse("Hello from the model"), nil
})
client, err := chatclient.New(model, chatclient.Config{})
if err != nil {
panic(err)
}
request, err := chat.NewRequest(chat.NewUserMessage(chat.NewTextPart("Hello")))
if err != nil {
panic(err)
}
request.Options.Model = "example"
response, err := client.Call(context.Background(), request)
if err != nil {
panic(err)
}
fmt.Println(response.Text())
}
func textResponse(text string) *chat.Response {
message := chat.NewAssistantMessage(chat.NewTextPart(text))
return &chat.Response{Output: &chat.Output{
Message: &message,
FinishReason: chat.FinishReasonStop,
}}
}
Output: Hello from the model
Index ¶
Examples ¶
Constants ¶
This section is empty.
Variables ¶
var ( ErrNilModel = errors.New("chatclient: nil model") ErrInvalidClient = errors.New("chatclient: uninitialized client") )
var ( ErrInvalidOutputFormat = errors.New("chatclient: invalid output format") ErrInvalidOutput = errors.New("chatclient: invalid output") )
var ErrInvalidTemplate = errors.New("chatclient: invalid template")
var (
ErrInvalidToolBatch = errors.New("chatclient: invalid tool batch")
)
var ErrNilStreamer = errors.New("chatclient: nil streamer")
Functions ¶
func NewSingleBatchToolMiddleware ¶ added in v0.20.0
func NewSingleBatchToolMiddleware(executables ...tool.Tool) (chat.CallMiddleware, error)
NewSingleBatchToolMiddleware advertises a frozen Tool set and executes at most one model-requested batch. Only FinishReasonToolCalls authorizes execution; other outcomes pass through. Every invocation is validated before any Tool runs, then the batch executes serially followed by one model call. Completed Tools are never rolled back: an execution failure returns ToolBatchError and a failed follow-up call returns ToolContinuationError, both preserving the cause for errors.Is and errors.As. The middleware owns Tools and ToolChoice exclusively, so it cannot be stacked. Placed inside history.Middleware.Call, history persists only fresh input and the final answer; placed outside, it also records tool proposals and results.
Types ¶
type Client ¶
type Client struct {
// contains filtered or unexported fields
}
Client is an immutable composition of a chat model and its middleware. It is as safe for concurrent use as the underlying model. Streaming has its own construction boundary in StreamClient.
func (Client) Output ¶
func (c Client) Output[T any](ctx context.Context, req *chat.Request, format OutputFormat[T]) (T, error)
Output asks the provider to enforce format, then strictly decodes naturally completed text. Refusal and other non-successful completion reasons return OutputCompletionError. Media and tool requests cannot become typed values. Output never repairs JSON or injects format instructions into the prompt.
Example ¶
package main
import (
"context"
"fmt"
"github.com/Tangerg/scope/core/chat"
"github.com/Tangerg/scope/core/chatclient"
)
func main() {
type answer struct {
Value int `json:"value"`
}
model := chat.ModelFunc(func(context.Context, *chat.Request) (*chat.Response, error) {
return textResponse(`{"value":42}`), nil
})
client, err := chatclient.New(model, chatclient.Config{})
if err != nil {
panic(err)
}
request, err := chat.NewRequest(chat.NewUserMessage(chat.NewTextPart("What is six times seven?")))
if err != nil {
panic(err)
}
result, err := client.Output(context.Background(), request, chatclient.JSON[answer]())
if err != nil {
panic(err)
}
fmt.Println(result.Value)
}
func textResponse(text string) *chat.Response {
message := chat.NewAssistantMessage(chat.NewTextPart(text))
return &chat.Response{Output: &chat.Output{
Message: &message,
FinishReason: chat.FinishReasonStop,
}}
}
Output: 42
type Config ¶
type Config struct {
CallMiddleware []chat.CallMiddleware
}
Config supplies synchronous middleware. The first entry is the outermost wrapper. New consumes the slice during construction and does not retain it.
type JSONSchemaConfig ¶ added in v0.13.0
type OutputCompletionError ¶ added in v0.15.0
type OutputCompletionError struct {
FinishReason chat.FinishReason
Refusal string
}
OutputCompletionError preserves a provider outcome that cannot become the requested typed value. It unwraps to ErrInvalidOutput; callers may inspect FinishReason and Refusal without parsing an error message.
func (*OutputCompletionError) Error ¶ added in v0.15.0
func (o *OutputCompletionError) Error() string
func (*OutputCompletionError) Unwrap ¶ added in v0.15.0
func (*OutputCompletionError) Unwrap() error
type OutputFormat ¶
type OutputFormat[T any] struct { // contains filtered or unexported fields }
OutputFormat couples a provider-neutral request contract with the decoder for its complete response. Pass one to Client.Output.
func JSON ¶
func JSON[T any]() OutputFormat[T]
JSON selects provider-native JSON output and rejects unknown object members when decoding T.
func JSONSchema ¶
func JSONSchema[T any](config JSONSchemaConfig) (OutputFormat[T], error)
func Text ¶
func Text() OutputFormat[string]
Text selects provider-native text output without post-processing.
type StreamClient ¶ added in v0.17.0
type StreamClient struct {
// contains filtered or unexported fields
}
StreamClient owns a required streaming capability and its middleware chain. It is immutable; concurrent use requires a concurrency-safe Streamer. It does not implement chat.Model or require an unused synchronous capability.
func NewStreamClient ¶ added in v0.17.0
func NewStreamClient(streamer chat.Streamer, config StreamConfig) (StreamClient, error)
func (StreamClient) Stream ¶ added in v0.17.0
func (s StreamClient) Stream(ctx context.Context, request *chat.Request) iter.Seq2[*chat.ResponseDelta, error]
Stream snapshots the request at invocation. Provider work remains lazy until iteration; stopping iteration synchronously releases the provider resources.
Example ¶
package main
import (
"context"
"fmt"
"iter"
"github.com/Tangerg/scope/core/chat"
"github.com/Tangerg/scope/core/chatclient"
)
func main() {
streamer := chat.StreamerFunc(func(context.Context, *chat.Request) iter.Seq2[*chat.ResponseDelta, error] {
return func(yield func(*chat.ResponseDelta, error) bool) {
if !yield(delta("Hello ", ""), nil) {
return
}
yield(delta("stream", chat.FinishReasonStop), nil)
}
})
client, err := chatclient.NewStreamClient(streamer, chatclient.StreamConfig{})
if err != nil {
panic(err)
}
request, err := chat.NewRequest(chat.NewUserMessage(chat.NewTextPart("Hello")))
if err != nil {
panic(err)
}
for response, streamErr := range client.Stream(context.Background(), request) {
if streamErr != nil {
panic(streamErr)
}
fmt.Print(response.Text())
}
fmt.Println()
}
func delta(text string, reason chat.FinishReason) *chat.ResponseDelta {
return &chat.ResponseDelta{Parts: []chat.PartDelta{chat.NewTextDelta(text)}, FinishReason: reason}
}
Output: Hello stream
type StreamConfig ¶ added in v0.17.0
type StreamConfig struct {
Middleware []chat.StreamMiddleware
}
StreamConfig supplies streaming middleware. The first entry is the outermost wrapper. NewStreamClient consumes the slice without retaining it.
type Template ¶
type Template struct {
// contains filtered or unexported fields
}
Template is an immutable, parsed prompt template safe for concurrent use. Rendering fails on missing map keys instead of emitting placeholder text. Require inspects the complete parse tree, including pipelines, branches, and root references such as $.Query.
Example ¶
package main
import (
"fmt"
"github.com/Tangerg/scope/core/chatclient"
)
func main() {
prompt, err := chatclient.ParseTemplate("Explain {{.Topic}} in one sentence.")
if err != nil {
panic(err)
}
message, err := prompt.UserMessage(struct{ Topic string }{Topic: "Go interfaces"})
if err != nil {
panic(err)
}
fmt.Println(message.Text())
}
Output: Explain Go interfaces in one sentence.
func ParseTemplate ¶
ParseTemplate rejects templates whose variable paths or functions could make rendering depend on ambient state.
type ToolBatchError ¶ added in v0.18.0
type ToolBatchError struct {
// contains filtered or unexported fields
}
ToolBatchError preserves the original request, complete model proposal, successful prefix, and failed call of a serial tool batch. Completed effects have not been rolled back and later calls were not executed. The cause may be a tool.Failure with acknowledged partial effects. It assigns no retry policy.
func (*ToolBatchError) Completed ¶ added in v0.18.0
func (t *ToolBatchError) Completed() []chat.ToolResult
Completed returns independently owned successful results in execution order.
func (*ToolBatchError) Error ¶ added in v0.18.0
func (t *ToolBatchError) Error() string
func (*ToolBatchError) FailedCall ¶ added in v0.18.0
func (t *ToolBatchError) FailedCall() chat.ToolCall
FailedCall returns the original model proposal that failed during execution or returned an invalid output. The proposal owns it, at len(Completed()) among its tool calls.
func (*ToolBatchError) Proposal ¶ added in v0.20.0
func (t *ToolBatchError) Proposal() *chat.Response
Proposal returns the complete model response, including every ordered call, its original arguments, assistant content, and metadata. The failed call is at len(Completed()) among tool-call parts; every later call was not executed. Failure does not prove absence of side effects from the failed call.
func (*ToolBatchError) Request ¶ added in v0.20.0
func (t *ToolBatchError) Request() *chat.Request
Request returns the frozen input to the model that proposed the batch.
func (*ToolBatchError) Unwrap ¶ added in v0.18.0
func (t *ToolBatchError) Unwrap() error
type ToolContinuationError ¶ added in v0.19.0
type ToolContinuationError struct {
// contains filtered or unexported fields
}
ToolContinuationError reports a failed model call after every tool in the batch completed; the effects have not been rolled back. Callers may retry Request through the downstream model without rerunning tools, but not through NewSingleBatchToolMiddleware, which owns the tool contract. It assigns no retry policy.
Example ¶
package main
import (
"context"
"errors"
"fmt"
"github.com/Tangerg/scope/core/chat"
"github.com/Tangerg/scope/core/chatclient"
"github.com/Tangerg/scope/core/tool"
)
func main() {
toolCalls := 0
executable, err := tool.NewFunc(tool.FuncConfig{Name: "save"}, func(context.Context, struct{}) (string, error) {
toolCalls++
return "saved", nil
})
if err != nil {
panic(err)
}
middleware, err := chatclient.NewSingleBatchToolMiddleware(executable)
if err != nil {
panic(err)
}
modelCalls := 0
model := chat.ModelFunc(func(context.Context, *chat.Request) (*chat.Response, error) {
modelCalls++
switch modelCalls {
case 1:
message := chat.NewAssistantMessage(chat.NewToolCallPart(chat.ToolCall{ID: "save-1", Name: "save", Arguments: `{}`}))
return &chat.Response{Output: &chat.Output{Message: &message, FinishReason: chat.FinishReasonToolCalls}}, nil
case 2:
return nil, errors.New("model connection interrupted")
default:
return textResponse("Saved."), nil
}
})
observedCalls := 0
observe := func(next chat.Model) chat.Model {
return chat.ModelFunc(func(ctx context.Context, request *chat.Request) (*chat.Response, error) {
observedCalls++
return next.Call(ctx, request)
})
}
// Keep the complete model chain used after tool execution. Recovery must
// retain these decorators as well as the completed tool results.
downstream := chat.Wrap(model, observe)
client, err := chatclient.New(downstream, chatclient.Config{CallMiddleware: []chat.CallMiddleware{middleware}})
if err != nil {
panic(err)
}
request, err := chat.NewRequest(chat.NewUserMessage(chat.NewTextPart("Save this.")))
if err != nil {
panic(err)
}
response, err := client.Call(context.Background(), request)
if continuation, ok := errors.AsType[*chatclient.ToolContinuationError](err); ok {
// The host chooses this single retry; it does not execute save again.
response, err = downstream.Call(context.Background(), continuation.Request())
}
if err != nil {
panic(err)
}
fmt.Println(response.Text())
fmt.Println("tool executions:", toolCalls)
fmt.Println("observed model calls:", observedCalls)
}
func textResponse(text string) *chat.Response {
message := chat.NewAssistantMessage(chat.NewTextPart(text))
return &chat.Response{Output: &chat.Output{
Message: &message,
FinishReason: chat.FinishReasonStop,
}}
}
Output: Saved. tool executions: 1 observed model calls: 3
func (*ToolContinuationError) Completed ¶ added in v0.19.0
func (t *ToolContinuationError) Completed() []chat.ToolResult
Completed returns independently owned results in execution order.
func (*ToolContinuationError) Error ¶ added in v0.19.0
func (t *ToolContinuationError) Error() string
func (*ToolContinuationError) Request ¶ added in v0.19.0
func (t *ToolContinuationError) Request() *chat.Request
Request returns an independently owned continuation, including the original messages, tool proposals, completed results, and frozen model settings.
func (*ToolContinuationError) Unwrap ¶ added in v0.19.0
func (t *ToolContinuationError) Unwrap() error