chatclient

package
v0.12.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 30, 2026 License: Apache-2.0 Imports: 16 Imported by: 0

Documentation

Overview

Package chatclient provides direct, optional conveniences around the minimal chat protocols and model capabilities defined by Core. Client.Output binds a provider-neutral OutputFormat to the decoder shared by synchronous and streaming generations. Client also discovers provider-owned input-token counting only when the post-middleware model can measure the same prepared request that Call will execute.

NewToolMiddleware covers the deliberately small direct-use path: it advertises a frozen executable Tool set, validates complete calls, executes them serially, and continues the model conversation. Agent control flow, retries, concurrency, approval, and durable execution remain outside Core.

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{Defaults: chat.Options{Model: "example"}})
	if err != nil {
		panic(err)
	}
	request, err := chat.NewRequest(chat.NewUserMessage(chat.NewTextPart("Hello")))
	if err != nil {
		panic(err)
	}
	response, err := client.Call(context.Background(), request)
	if err != nil {
		panic(err)
	}
	fmt.Println(response.Text())
}

func textResponse(text string) *chat.Response {
	return response(text, chat.FinishReasonStop)
}

func response(text string, reason chat.FinishReason) *chat.Response {
	message := chat.NewAssistantMessage(chat.NewTextPart(text))
	return &chat.Response{Output: &chat.Output{
		Message:      &message,
		FinishReason: reason,
	}}
}
Output:
Hello from the model

Index

Examples

Constants

This section is empty.

Variables

View Source
var (
	ErrNilModel                      = errors.New("chatclient: nil model")
	ErrNilClient                     = errors.New("chatclient: nil client")
	ErrStreamingUnsupported          = errors.New("chatclient: streaming unsupported")
	ErrInputTokenCountingUnsupported = errors.New("chatclient: input token counting unsupported")
)
View Source
var (
	ErrInvalidOutputFormat = errors.New("chatclient: invalid output format")

	ErrInvalidOutput = errors.New("chatclient: invalid output")
)
View Source
var ErrInvalidTemplate = errors.New("chatclient: invalid template")
View Source
var ErrInvalidToolMiddleware = errors.New("chatclient: invalid tool middleware")

Functions

func NewToolMiddleware

func NewToolMiddleware(executables ...tool.Tool) (chat.CallMiddleware, error)

NewToolMiddleware keeps direct Client usage useful for the small case where model-requested Tools only need schema validation and serial execution. Agent control flow and execution policy deliberately remain outside this boundary.

Types

type Client

type Client struct {
	// contains filtered or unexported fields
}

Client is an immutable, concurrency-safe composition of chat capabilities, defaults, and middleware. It does not make an underlying model concurrency safe; callers must still follow the model's concurrency contract.

Call and Stream accept ordinary chat.Request values directly. Client snapshots each request before invoking middleware or a provider, so those layers cannot mutate caller-owned protocol values. A configured Streamer takes precedence; otherwise New discovers the capability on the model. Stream remains lazy and reports unsupported streaming as its single terminal item.

func New

func New(model chat.Model, config Config) (Client, error)

func (Client) Call

func (c Client) Call(ctx context.Context, req *chat.Request) (*chat.Response, error)

func (Client) CountInputTokens

func (c Client) CountInputTokens(ctx context.Context, req *chat.Request) (int64, error)

CountInputTokens snapshots and resolves defaults exactly like Call, then asks the model's optional provider-owned counter to measure the complete input.

func (Client) Output

func (c Client) Output[T any](format OutputFormat[T]) Generation[T]
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(chatclient.JSON[answer]()).
		Call(context.Background(), request)
	if err != nil {
		panic(err)
	}
	fmt.Println(result.Value)
}

func textResponse(text string) *chat.Response {
	return response(text, chat.FinishReasonStop)
}

func response(text string, reason chat.FinishReason) *chat.Response {
	message := chat.NewAssistantMessage(chat.NewTextPart(text))
	return &chat.Response{Output: &chat.Output{
		Message:      &message,
		FinishReason: reason,
	}}
}
Output:
42

func (Client) Stream

func (c Client) Stream(ctx context.Context, req *chat.Request) iter.Seq2[*chat.Response, error]
Example
package main

import (
	"context"
	"fmt"
	"iter"

	"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("fallback"), nil
	})
	streamer := chat.StreamerFunc(func(context.Context, *chat.Request) iter.Seq2[*chat.Response, error] {
		return func(yield func(*chat.Response, error) bool) {
			if !yield(response("Hello ", ""), nil) {
				return
			}
			yield(response("stream", chat.FinishReasonStop), nil)
		}
	})
	client, err := chatclient.New(model, chatclient.Config{Streamer: streamer})
	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 textResponse(text string) *chat.Response {
	return response(text, chat.FinishReasonStop)
}

func response(text string, reason chat.FinishReason) *chat.Response {
	message := chat.NewAssistantMessage(chat.NewTextPart(text))
	return &chat.Response{Output: &chat.Output{
		Message:      &message,
		FinishReason: reason,
	}}
}
Output:
Hello stream

func (Client) SupportsInputTokenCounting

func (c Client) SupportsInputTokenCounting() bool

SupportsInputTokenCounting reports whether CountInputTokens observes the same prepared provider request as Call. Middleware must explicitly preserve that capability after any request transformation.

type Config

type Config struct {
	Defaults         chat.Options
	Streamer         chat.Streamer
	CallMiddleware   []chat.CallMiddleware
	StreamMiddleware []chat.StreamMiddleware
}

Config describes construction-time Client behavior. Request-specific values belong in chat.Request.

The slices and Defaults are snapshotted by New, so callers may safely reuse or mutate their input after construction. The first middleware in each slice is the outermost wrapper, matching chat.Wrap and chat.WrapStream.

type Generation

type Generation[T any] struct {
	// contains filtered or unexported fields
}

Generation is an immutable Client with one typed output contract bound to it. Call and Stream both return the complete decoded result; use Client.Stream directly when individual response chunks are required.

func (Generation[T]) Call

func (g Generation[T]) Call(ctx context.Context, request *chat.Request) (T, error)

func (Generation[T]) Stream

func (g Generation[T]) Stream(ctx context.Context, request *chat.Request) (T, error)

type OutputFormat

type OutputFormat[T any] struct {
	// contains filtered or unexported fields
}

OutputFormat couples a provider-neutral request contract with the decoder that consumes its response stream. Pass one to Client.Output.

func JSON

func JSON[T any]() OutputFormat[T]

func JSONSchema

func JSONSchema[T any](name string) (OutputFormat[T], error)

JSONSchema returns a result format coupled to the named contract derived from T.

func Text

func Text() OutputFormat[string]

type Template

type Template struct {
	// contains filtered or unexported fields
}

Template is an immutable, parsed prompt template safe for concurrent use. Parsing fails on missing map keys instead of emitting placeholder text; Require inspects the complete parse tree, including pipelines and branches. Message projections validate the rendered protocol value and preserve media order without retaining per-render variables in Template.

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

func ParseTemplate(source string) (*Template, error)

func (*Template) Render

func (t *Template) Render(data any) (string, error)

func (*Template) Require

func (t *Template) Require(names ...string) error

func (*Template) Source

func (t *Template) Source() string

func (*Template) SystemMessage

func (t *Template) SystemMessage(data any) (chat.Message, error)

func (*Template) UserMessage

func (t *Template) UserMessage(data any, attachments ...*media.Media) (chat.Message, error)

Directories

Path Synopsis
Package safeguard provides fail-closed input and output screening as Core chat middleware.
Package safeguard provides fail-closed input and output screening as Core chat middleware.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL