Skip to content

ParseAndValidate validates schema against Map instead of actualy StructuredOutput Type #101

@ozodrukh

Description

@ozodrukh

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:

// 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions