Task: workarena.servicenow.sort-service-catalog-item-list
Goal: Sort the "catalog items" list by the following fields:
 - Updated (ascending)
 - Hide 'Add to Wish List' (descending)
 - Classic Mobile Picture Type (ascending)
Task configuration: {'sort_dirs': ['asc', 'desc', 'asc'], 'sort_fields': ['sys_updated_on', 'no_wishlist_v2', 'mobile_picture_type'], 'table_name': 'sc_cat_item'}

Validation code:
import re
import urllib.parse as parse
from typing import Any, Dict, List


def validate(messages: List[Dict[str, str]], config: Dict[str, Any]) -> int:
    """
    Takes a list of messages, extracts the first URL from the last message, and validates that
    the 'sysparm_query' parameter has the expected ORDERBY clauses.

    Args:
        messages: List of dicts, each with {"content": str}
        target: encoded query string representing the expected ORDERBY clauses

    Returns:
        int: 1 if correct, 0 if incorrect
    """

    # Check whether we have the necessary information to validate
    if (
        not "table_name" in config
        or not "sort_fields" in config
        or not "sort_dirs" in config
    ):
        raise ValueError("Target must contain 'sort_fields' and 'sort_dirs'")

    # Check whether we have messages
    if not messages:
        raise ValueError("Messages must not be empty")

    # Extract last message content
    content = messages[-1].get("content", "")

    # Extract all URLs — markdown links first, then bare URLs
    expected_table = config["table_name"]
    urls = []
    for md in re.finditer(r"\[.*?\]\((https?://[^)]+)\)", content):
        urls.append(md.group(1))
    for bare in re.finditer(r"https?://\S+", content):
        candidate = bare.group(0).rstrip(")].,")
        if candidate not in urls:
            urls.append(candidate)
    if not urls:
        return 0

    # Pick the URL that contains the expected table name, fall back to first
    raw_url = urls[0]
    for u in urls:
        if f"{expected_table}_list.do" in parse.unquote(u):
            raw_url = u
            break
    page_url = parse.unquote(raw_url)

    # Parse query params — handle both direct and nav_to.do?uri=... formats
    parsed = parse.urlparse(page_url)
    page_qs = parse.parse_qs(parsed.query)

    # If URL is nav_to.do?uri=..., extract the inner URI and parse it
    if "uri" in page_qs:
        inner_url = parse.unquote(page_qs["uri"][0])
        inner_parsed = parse.urlparse(inner_url if "://" in inner_url else "http://x/" + inner_url)
        page_qs = parse.parse_qs(inner_parsed.query)
        page_url = inner_url  # use inner URL for table name extraction

    if "sysparm_query" not in page_qs:
        return 0
    page_sysparam_query = page_qs["sysparm_query"][0]

    if not page_sysparam_query:
        return 0

    # get table name from url
    table_match = re.search(r"(\w+)_list\.do", page_url)
    if not table_match:
        return 0
    table_name = table_match.group(1)
    if table_name != config["table_name"]:
        return 0

    # Build expected ORDERBY regex
    sort_fields = config["sort_fields"]
    sort_dirs = config["sort_dirs"]
    order_dir = {"asc": "", "desc": "DESC"}
    sysparam_query_regex = r"\^".join(
        [
            f"ORDERBY{order_dir[dir]}{field}"
            for field, dir in zip(sort_fields, sort_dirs)
        ]
    )
    sysparam_query_regex = (
        r"^((?!ORDERBY).)*" + sysparam_query_regex + r"((?!ORDERBY).)*$"
    )

    # Validate
    if re.match(sysparam_query_regex, page_sysparam_query):
        return 1

    return 0