package llm import ( "errors" "strings" ) // ErrNoJSON — в тексте не нашлось JSON-объекта. var ErrNoJSON = errors.New("llm: no JSON object in response") // ExtractJSONObject вытаскивает первый верхнеуровневый JSON-объект из ответа // модели. Модели часто оборачивают JSON в ```-ограждения или добавляют // пояснения до/после — здесь это срезается, а скобки считаются с учётом // строковых литералов и экранирования, чтобы `{` внутри строки не сбивал // баланс. Возвращает срез исходного текста (валидацию делает вызывающий). func ExtractJSONObject(s string) (string, error) { s = stripCodeFences(s) start := strings.IndexByte(s, '{') if start < 0 { return "", ErrNoJSON } depth := 0 inStr := false esc := false for i := start; i < len(s); i++ { c := s[i] if inStr { switch { case esc: esc = false case c == '\\': esc = true case c == '"': inStr = false } continue } switch c { case '"': inStr = true case '{': depth++ case '}': depth-- if depth == 0 { return s[start : i+1], nil } } } return "", ErrNoJSON } // stripCodeFences убирает markdown-ограждение ```...``` (с опциональным // языковым тегом вроде ```json), если ответ обёрнут в него целиком. func stripCodeFences(s string) string { t := strings.TrimSpace(s) if !strings.HasPrefix(t, "```") { return s } // Отрезаем первую строку с открывающим ``` и языковым тегом. if nl := strings.IndexByte(t, '\n'); nl >= 0 { t = t[nl+1:] } else { return s } if end := strings.LastIndex(t, "```"); end >= 0 { t = t[:end] } return t }