name: Gallery Agent
on:

  schedule:
    - cron: '0 */12 * * *'  # Run every 4 hours
  workflow_dispatch:
    inputs:
      search_term:
        description: 'Search term for models'
        required: false
        default: 'GGUF'
        type: string
      limit:
        description: 'Maximum number of models to process'
        required: false
        default: '15'
        type: string
      quantization:
        description: 'Preferred quantization format'
        required: false
        default: 'Q4_K_M'
        type: string
      max_models:
        description: 'Maximum number of models to add to the gallery'
        required: false
        default: '1'
        type: string
jobs:
  gallery-agent:
    if: github.repository == 'mudler/LocalAI'
    runs-on: ubuntu-latest
    steps:
      - name: Checkout repository
        uses: actions/checkout@v7
        with:
          token: ${{ secrets.GITHUB_TOKEN }}

      - name: Set up Go
        uses: actions/setup-go@v5
        with:
          go-version: '1.21'
      - name: Proto Dependencies
        run: |
          # Install protoc
          curl -L -s https://github.com/protocolbuffers/protobuf/releases/download/v26.1/protoc-26.1-linux-x86_64.zip -o protoc.zip && \
          unzip -j -d /usr/local/bin protoc.zip bin/protoc && \
          rm protoc.zip
          go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.34.2
          go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@1958fcbe2ca8bd93af633f11e97d44e567e945af
          PATH="$PATH:$HOME/go/bin" make protogen-go
      - name: Process gallery-agent PR commands
        env:
          GH_TOKEN: ${{ secrets.UPDATE_BOT_TOKEN }}
          REPO: ${{ github.repository }}
          SEARCH: 'gallery agent in:title'
        run: |
          # Walk gallery-agent PRs and act on maintainer comments:
          #   /gallery-agent blacklist → label `gallery-agent/blacklisted` + close (never repropose)
          #   /gallery-agent recreate  → close without label (next run may repropose)
          # Only comments from OWNER / MEMBER / COLLABORATOR are honored so
          # random users can't drive the bot.
          #
          # We scan both open PRs AND recently-closed PRs that don't already
          # carry the blacklist label. This covers the common flow where a
          # maintainer writes /gallery-agent blacklist and immediately clicks
          # Close — without this, the next scheduled run wouldn't see the
          # command (PR is already closed) and would repropose the model.
          gh label create gallery-agent/blacklisted \
            --repo "$REPO" --color ededed \
            --description "gallery-agent must not repropose this model" 2>/dev/null || true

          prs_open=$(gh pr list --repo "$REPO" --state open --search "$SEARCH" \
            --json number --jq '.[].number')
          # Closed PRs from the last 14 days that don't yet have the blacklist label.
          # Bounded window keeps the scan cheap while covering late-applied commands.
          since=$(date -u -d '14 days ago' +%Y-%m-%d)
          prs_closed=$(gh pr list --repo "$REPO" --state closed \
            --search "$SEARCH closed:>=$since -label:gallery-agent/blacklisted" \
            --json number --jq '.[].number')
          prs=$(printf '%s\n%s\n' "$prs_open" "$prs_closed" | sort -u | sed '/^$/d')
          for pr in $prs; do
            state=$(gh pr view "$pr" --repo "$REPO" --json state --jq '.state')
            cmds=$(gh pr view "$pr" --repo "$REPO" --json comments \
              --jq '.comments[] | select(.authorAssociation=="OWNER" or .authorAssociation=="MEMBER" or .authorAssociation=="COLLABORATOR") | .body')
            if echo "$cmds" | grep -qE '(^|[[:space:]])/gallery-agent[[:space:]]+blacklist([[:space:]]|$)'; then
              echo "PR #$pr: blacklist command found (state=$state)"
              gh pr edit "$pr" --repo "$REPO" --add-label gallery-agent/blacklisted || true
              if [ "$state" = "OPEN" ]; then
                gh pr close "$pr" --repo "$REPO" --comment "Blacklisted via \`/gallery-agent blacklist\`. This model will not be reproposed." || true
              fi
            elif [ "$state" = "OPEN" ] && echo "$cmds" | grep -qE '(^|[[:space:]])/gallery-agent[[:space:]]+recreate([[:space:]]|$)'; then
              echo "PR #$pr: recreate command found"
              gh pr close "$pr" --repo "$REPO" --comment "Closed via \`/gallery-agent recreate\`. The next scheduled run will propose this model again." || true
            fi
          done

      - name: Collect skip URLs for the gallery agent
        id: open_prs
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          REPO: ${{ github.repository }}
          SEARCH: 'gallery agent in:title'
        run: |
          # Skip set =
          #   URLs from any open gallery-agent PR (avoid duplicate PRs for the same model while one is pending)
          # + URLs from closed PRs carrying the `gallery-agent/blacklisted` label (hard blacklist)
          # Plain-closed PRs without the label are ignored — closing a PR is
          # not by itself a "never propose again" signal; maintainers must
          # opt in via the /gallery-agent blacklist comment command.
          urls_open=$(gh pr list --repo "$REPO" --state open --search "$SEARCH" \
            --json body --jq '[.[].body] | join("\n")' \
            | grep -oE 'https://huggingface\.co/[^ )]+' || true)
          urls_blacklist=$(gh pr list --repo "$REPO" --state closed --search "$SEARCH" \
            --label gallery-agent/blacklisted \
            --json body --jq '[.[].body] | join("\n")' \
            | grep -oE 'https://huggingface\.co/[^ )]+' || true)
          urls=$(printf '%s\n%s\n' "$urls_open" "$urls_blacklist" | sort -u | sed '/^$/d')
          echo "Skip URLs:"
          echo "$urls"
          {
            echo "urls<<EOF"
            echo "$urls"
            echo "EOF"
          } >> "$GITHUB_OUTPUT"

      - name: Run gallery agent
        env:
          SEARCH_TERM: ${{ github.event.inputs.search_term || 'GGUF' }}
          LIMIT: ${{ github.event.inputs.limit || '15' }}
          QUANTIZATION: ${{ github.event.inputs.quantization || 'Q4_K_M' }}
          MAX_MODELS: ${{ github.event.inputs.max_models || '1' }}
          EXTRA_SKIP_URLS: ${{ steps.open_prs.outputs.urls }}
        run: |
          export GALLERY_INDEX_PATH=$PWD/gallery/index.yaml
          go run ./.github/gallery-agent

      - name: Check for changes
        id: check_changes
        run: |
          if git diff --quiet gallery/index.yaml; then
            echo "changes=false" >> $GITHUB_OUTPUT
            echo "No changes detected in gallery/index.yaml"
          else
            echo "changes=true" >> $GITHUB_OUTPUT
            echo "Changes detected in gallery/index.yaml"
            git diff gallery/index.yaml
          fi

      - name: Read gallery agent summary
        id: read_summary
        if: steps.check_changes.outputs.changes == 'true'
        run: |
          if [ -f "./gallery-agent-summary.json" ]; then
            echo "summary_exists=true" >> $GITHUB_OUTPUT
            # Extract summary data using jq
            echo "search_term=$(jq -r '.search_term' ./gallery-agent-summary.json)" >> $GITHUB_OUTPUT
            echo "total_found=$(jq -r '.total_found' ./gallery-agent-summary.json)" >> $GITHUB_OUTPUT
            echo "models_added=$(jq -r '.models_added' ./gallery-agent-summary.json)" >> $GITHUB_OUTPUT
            echo "quantization=$(jq -r '.quantization' ./gallery-agent-summary.json)" >> $GITHUB_OUTPUT
            echo "processing_time=$(jq -r '.processing_time' ./gallery-agent-summary.json)" >> $GITHUB_OUTPUT
            
            # Create a formatted list of added models with URLs
            added_models=$(jq -r 'range(0; .added_model_ids | length) as $i | "- [\(.added_model_ids[$i])](\(.added_model_urls[$i]))"' ./gallery-agent-summary.json | tr '\n' '\n')
            echo "added_models<<EOF" >> $GITHUB_OUTPUT
            echo "$added_models" >> $GITHUB_OUTPUT
            echo "EOF" >> $GITHUB_OUTPUT
            rm -f ./gallery-agent-summary.json
          else
            echo "summary_exists=false" >> $GITHUB_OUTPUT
          fi

      - name: Create Pull Request
        if: steps.check_changes.outputs.changes == 'true'
        uses: peter-evans/create-pull-request@v8
        with:
          token: ${{ secrets.UPDATE_BOT_TOKEN }}
          push-to-fork: ci-forks/LocalAI
          commit-message: 'chore(model gallery): :robot: add new models via gallery agent'
          title: 'chore(model gallery): :robot: add ${{ steps.read_summary.outputs.models_added || 0 }} new models via gallery agent'
          # Branch has to be unique so PRs are not overriding each other
          branch-suffix: timestamp
          body: |
            This PR was automatically created by the gallery agent workflow.
            
            **Summary:**
            - **Search Term:** ${{ steps.read_summary.outputs.search_term || github.event.inputs.search_term || 'GGUF' }}
            - **Models Found:** ${{ steps.read_summary.outputs.total_found || 'N/A' }}
            - **Models Added:** ${{ steps.read_summary.outputs.models_added || '0' }}
            - **Quantization:** ${{ steps.read_summary.outputs.quantization || github.event.inputs.quantization || 'Q4_K_M' }}
            - **Processing Time:** ${{ steps.read_summary.outputs.processing_time || 'N/A' }}
            
            **Added Models:**
            ${{ steps.read_summary.outputs.added_models || '- No models added' }}

            ### Bot commands

            Maintainers (owner / member / collaborator) can control this PR
            by leaving a comment with one of:

            - `/gallery-agent recreate` — close this PR; the next scheduled
              run will propose this model again (useful if the entry needs
              to be regenerated with fresh metadata).
            - `/gallery-agent blacklist` — close this PR and permanently
              prevent the gallery agent from ever reproposing this model.

            Plain "Close" (without a command) is treated as a no-op: the
            model may be reproposed by a future run.

            **Workflow Details:**
            - Triggered by: `${{ github.event_name }}`
            - Run ID: `${{ github.run_id }}`
            - Commit: `${{ github.sha }}`
          signoff: true
          delete-branch: true
