charmbracelet / charmbracelet/fantasy

ParseAndValidate validates schema against Map instead of actualy StructuredOutput Type

Open
#101 1 comment 0 reactions 0 assignees View on GitHub
Dominant language
Go
Stars
987
Forks
137
Avg merge
14h 41m
Merged PRs (30d)
12

Description

### Description

When using Provider with `WithObjectMode(fantasy.ObjectModeTool)`, there is bug validation always seems to fail (even when json is actually fine)

In `ParseAndValidate(input, schema)` we pass tool input as json string, and schema which is generated from desired object,
however calling `ParsePartialJSON` returns `map[string]any` as first object, and validation will be against this `map[string]any` instead of desired object, therefore it will throw error:
`validation failed: properties: Property {property} does not match the schema`

*Update:* it's not the comparison against map, i believe it's jsonschema's bug, i will get back after investigating more

Steps to reproduce:

```go

// actual code from schema.go
func validateAgainstSchema(obj any, schema fantasy.Schema) error {
jsonSchemaBytes, err := json.Marshal(schema)
if err != nil {
return fmt.Errorf("failed to marshal schema: %w", err)
}

compiler := jsonschema.NewCompiler()
validator, err := compiler.Compile(jsonSchemaBytes)
if err != nil {
return fmt.Errorf("invalid schema: %w", err)
}

result := validator.Validate(obj)
if !result.IsValid() {
var errMsgs []string
for field, validationErr := range result.Errors {
errMsgs = append(errMsgs, fmt.Sprintf("%s: %s", field, validationErr.Message))
}
return fmt.Errorf("validation failed: %s", strings.Join(errMsgs, "; "))
}

return nil
}

func main() {
// copied from debugging
var toolInputRaw = `{"items":[{"id": 8336, "merged": [], "cat": "empty", "utility_score": 0.1, "ad_score": 0, "reasoning": "Пост содержит только смайлик и упоминание канала, без смыслового содержания. Классифицирован как пустой."}, {"id": 8335, "merged": [], "cat": "ad", "utility_score": 0.2, "ad_score": 0.9, "reasoning": "Прямая реклама мероприятия от Лаборатории Касперского с призывом к регистрации. Классифицирован как реклама."}, {"id": 8334, "merged": [], "cat": "story", "utility_score": 0.6, "ad_score": 0.1, "reasoning": "Забавная история о том, как ИИ случайно удалил весь домашний каталог при попытке почистить репозиторий. Полезный кейс о рисках автоматизации.", "t": "ИИ-помощник решил", "b": "проблему глобально — выполнил rm -rf ~/ и стер весь домашний каталог при генеральной уборке репозитория."}, {"id": 8333, "merged": [], "cat": "news", "utility_score": 0.8, "ad_score": 0, "reasoning": "Новость об опенсорсном проекте TeXPen для преобразования рукописных формул в LaTeX. Технически полезный инструмент для студентов.", "t": "Рукописные формулы в LaTeX", "b": "теперь можно преобразовывать прямо в браузере с помощью опенсорсного проекта TeXPen на базе TexTeller."}, {"id": 8332, "merged": [], "cat": "news", "utility_score": 0.8, "ad_score": 0.1, "reasoning": "Новость об утилите RemoveWindowsAI для отключения ИИ-фич в Windows 11. Полезный опенсорс инструмент для контроля системы.", "t": "Microsoft навязывает ИИ", "b": "но опенсорсная утилита RemoveWindowsAI позволяет полностью убрать Copilot, Recall и другие AI-компоненты из Windows 11."}, {"id": 8331, "merged": [], "cat": "news", "utility_score": 0.8, "ad_score": 0, "reasoning": "Новость о том, как OpenAI создали Android-версию Sora с помощью ИИ, где 85% кода написал Codex. Интересный кейс вайбкодинга.", "t": "Android-приложение Sora", "b": "создали 4 инженера за 28 дней, причем 85% кода написал ИИ-ассистент Codex по модели GPT-5.1."}, {"id": 8329, "merged": [], "cat": "news", "utility_score": 0.7, "ad_score": 0, "reasoning": "Новость о тренировке роботов для медицинских процедур на огурцах. Интересный технологический прорыв в роботизированной хирургии.", "t": "Роботов учат ставить катетеры", "b": "на огурцах с помощью тактильных датчиков, чтобы в будущем выполнять точные медицинские манипуляции."}, {"id": 8328, "merged": [], "cat": "ad", "utility_score": 0.3, "ad_score": 0.7, "reasoning": "Нативная реклама программы AI360 с призывом подписываться. Хотя есть полезная информация о мероприятии, основной цель - продвижение программы.", "t": null, "b": null}, {"id": 8327, "merged": [], "cat": "news", "utility_score": 0.9, "ad_score": 0, "reasoning": "Важная новость о закрытии API HH для соискателей из-за спама. Отличный анализ глобального тренда в рекрутинге с конкретными данными и статистикой.", "t": "HeadHunter закрыл API", "b": "из-за автоспама от кандидатов — технологии, призванные упростить найм, сделали его невыносимым с 242 откликами на вакансию."}, {"id": 8326, "merged": [], "cat": "story", "utility_score": 0.8, "ad_score": 0, "reasoning": "Интересная история о том, как некоторые ИИ-боты на самом деле ведут люди из развивающихся стран. Раскрытие обмана в индустрии ИИ-компаньонов.", "t": "Ваш ИИ-романс может вести", "b": "грустный мужик из Кении за $0.05 за сообщение — некоторые ИИ-боты на самом деле управляются людьми."}]}`

var toolInput = ProcessedChannelResponse{}
obj, _, _ := schema.ParsePartialJSON(toolInputRaw)

var toJson = func() string {
sh := schema.Generate(reflect.TypeOf(obj))
val, _ := json.Marshal(sh)
return string(val)
}

log.Print(toJson())

if err := validateAgainstSchema(obj, schema.Generate(reflect.TypeOf(toolInput))); err != nil {
log.Fatal(err)
}

}

type ProcessedChannelItem struct {
PrimaryID int32 `json:"id"`
MergedIDs []int32 `json:"merged"`

Category string `json:"cat"`

UtilityScore float32 `json:"utility_score"`
AdScore float32 `json:"ad_score"`

Reasoning string `json:"reasoning"`

Title *string `json:"t,omitempty"`
Body *string `json:"b,omitempty"`
}

type ProcessedChannelResponse struct {
Items []ProcessedChannelItem `json:"items"`
}

```

### Version

0.5.2

### Environment

mac

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.