def _reconstruct(keyvals, dialect, keep_order=False,
                 sort_attribute_values=False):
    if not dialect:
        raise AttributeStringError()
    if not keyvals:
        return ""
    parts = []
    if constants.ignore_url_escape_characters or dialect['fmt'] != 'gff3':
        attributes = keyvals
    else:
        attributes = {}
        for k, v in keyvals.items():
            attributes[k] = []
            for i in v:
                attributes[k].append(''.join([quoter[j] for j in i]))
    if dialect['repeated keys']:
        items = []
        for key, val in attributes.items():
            if len(val) > 1:
                for v in val:
                    items.append((key, [v]))
            else:
                items.append((key, val))
    else:
        items = list(attributes.items())
    def sort_key(x):
        try:
            return dialect['order'].index(x[0])
        except ValueError:
            return 1e6
    if keep_order:
        items.sort(key=sort_key)
    for key, val in items:
        if val:
            if sort_attribute_values:
                val = sorted(val)
            val_str = dialect['multival separator'].join(val)
            if val_str:
                if dialect['quoted GFF2 values']:
                    val_str = '"%s"' % val_str
                part = dialect['keyval separator'].join([key, val_str])
        else:
            if dialect['fmt'] == 'gtf':
                part = dialect['keyval separator'].join([key, '""'])
            else:
                part = key
        parts.append(part)
    parts_str = dialect['field separator'].join(parts)
    if dialect['trailing semicolon']:
        parts_str += ';'
    return parts_str