Task: gitlab.post-message
Goal: Post "Thanks, working on reviews" for the merge request related to octovisuals page in primer/design
Task configuration:
content: Thanks, working on reviews
mr_nl: octovisuals page
mr: Octovisuals Page
namespace: primer
project_name: design

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

import requests
import json
import os

def _get_headers():
    return {
        "Content-Type": "application/json",
        "Accept": "application/json",
        **json.loads(os.getenv("GITLAB_EXTRA_HTTP_HEADERS", "{}")),
    }

def validate(messages: List[Dict[str, str]], config: Dict[str, Any]) -> int:

    if not messages:
        raise ValueError("Messages cannot be empty")

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

    # Build the project id
    project_id = quote(
        f"{config['namespace']}/{config['project_name']}", safe=""
    ).replace(".", "%2E")

    base_url = os.getenv("GITLAB_BASE_URL", "").rstrip("/")

    # Determine how to find the MR
    if "mr" in config:
        # Search for MR by title/description keyword
        mr_url = (
            base_url
            + "/api/v4/projects/"
            + project_id
            + "/merge_requests"
            + "?search="
            + config["mr"]
            + "&in=title,description"
        )
        response = requests.get(url=mr_url, headers=_get_headers())
        if response.status_code != 200:
            return 0
        result = response.json()
        if len(result) == 0:
            return 0
        mr = result[0]
    elif "topic" in config:
        # Search for MR by topic keyword
        mr_url = (
            base_url
            + "/api/v4/projects/"
            + project_id
            + "/merge_requests"
            + "?search="
            + config["topic"]
            + "&in=title,description"
        )
        response = requests.get(url=mr_url, headers=_get_headers())
        if response.status_code != 200:
            return 0
        result = response.json()
        if len(result) == 0:
            return 0
        mr = result[0]
    else:
        raise ValueError("Target must contain 'mr' or 'topic'")

    # Get notes on the MR
    notes_url = (
        base_url
        + "/api/v4/projects/"
        + project_id
        + "/merge_requests/"
        + str(mr["iid"])
        + "/notes"
        + "?per_page=100"
        + "&order_by=created_at"
        + "&sort=desc"
    )
    response = requests.get(url=notes_url, headers=_get_headers())
    if response.status_code != 200:
        return 0
    notes = response.json()
    if len(notes) == 0:
        return 0

    if "content" in config:
        # Direct content match: check if the expected content was posted
        for note in notes:
            if config["content"].lower().strip() in note["body"].lower().strip():
                return 1
        return 0
    elif "requester" in config:
        # Conditional reply: the agent should have posted "Thank you" or "@requester"
        # depending on whether the requester responded
        # Just check that a recent note was posted with either "thank you" or "@requester"
        requester = config["requester"]
        for note in notes:
            body_lower = note["body"].lower().strip()
            if "thank you" in body_lower or f"@{requester.lower()}" in body_lower:
                return 1
        return 0

    return 0