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]() } // 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) }