0147a79d18
Phase 9a (ADR-0014): Registry.RegisterResolver for dynamic tiers; DefineTool[Args] typed tools; Usage cache/reasoning detail fields wired through anthropic/openai/google; WithPromptCaching (Anthropic cache_control); agent supervision hooks (WithMaxStepsFunc, WithSteer, WithCompactor, WithToolErrorLimits + ErrToolLoop); health Bench/Unbench/Snapshot; ChainConfig.Observer failover events. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
78 lines
2.4 KiB
Go
78 lines
2.4 KiB
Go
package majordomo
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"reflect"
|
|
"strings"
|
|
|
|
"gitea.stevedudenhoeffer.com/steve/majordomo/llm"
|
|
)
|
|
|
|
// SchemaFor re-exports llm.SchemaFor: a JSON Schema derived from a Go type,
|
|
// suitable for WithSchema and tool parameters.
|
|
func SchemaFor[T any]() (json.RawMessage, error) { return llm.SchemaFor[T]() }
|
|
|
|
// DefineTool re-exports llm.DefineTool: a typed tool whose parameter schema
|
|
// is derived from Args and whose handler receives decoded arguments.
|
|
func DefineTool[Args any](name, description string, fn func(ctx context.Context, args Args) (any, error)) Tool {
|
|
return llm.DefineTool(name, description, fn)
|
|
}
|
|
|
|
// Generate performs a structured-output request and unmarshals the result
|
|
// into T: the schema is derived from T (llm.SchemaFor), injected via the
|
|
// provider's native structured-output mechanism, and the response text is
|
|
// decoded into a T value.
|
|
//
|
|
// type Verdict struct {
|
|
// Guilty bool `json:"guilty"`
|
|
// Why string `json:"why" description:"one-sentence rationale"`
|
|
// }
|
|
// v, err := majordomo.Generate[Verdict](ctx, m, req)
|
|
func Generate[T any](ctx context.Context, m Model, req Request, opts ...Option) (T, error) {
|
|
var zero T
|
|
schema, err := llm.SchemaFor[T]()
|
|
if err != nil {
|
|
return zero, err
|
|
}
|
|
|
|
name := "response"
|
|
if t := reflect.TypeFor[T](); t.Name() != "" {
|
|
name = strings.ToLower(t.Name())
|
|
}
|
|
|
|
resp, err := m.Generate(ctx, req, append(opts, llm.WithSchema(schema, name))...)
|
|
if err != nil {
|
|
return zero, err
|
|
}
|
|
|
|
text := strings.TrimSpace(resp.Text())
|
|
if text == "" {
|
|
return zero, fmt.Errorf("majordomo: structured response from %s is empty (finish: %s)", resp.Model, resp.FinishReason)
|
|
}
|
|
// Defensive: some models wrap JSON in a markdown fence despite the
|
|
// schema constraint.
|
|
text = stripFence(text)
|
|
|
|
var out T
|
|
if err := json.Unmarshal([]byte(text), &out); err != nil {
|
|
return zero, fmt.Errorf("majordomo: decode structured response from %s: %w (text: %.200s)", resp.Model, err, text)
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// stripFence removes a surrounding markdown code fence, if present.
|
|
func stripFence(s string) string {
|
|
if !strings.HasPrefix(s, "```") {
|
|
return s
|
|
}
|
|
s = strings.TrimPrefix(s, "```")
|
|
// Drop an optional language tag on the opening fence line.
|
|
if i := strings.IndexByte(s, '\n'); i >= 0 {
|
|
s = s[i+1:]
|
|
}
|
|
s = strings.TrimSuffix(strings.TrimSpace(s), "```")
|
|
return strings.TrimSpace(s)
|
|
}
|