Files
steve 0147a79d18
CI / Tidy (push) Successful in 9m31s
CI / Build & Test (push) Successful in 10m13s
feat: conversion-driven extensions — resolvers, DefineTool, hooks, ops controls
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>
2026-06-10 13:30:06 +02:00

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)
}