Location Tagger default agent + geocoding in add_annotations_from_exact_strings (#1822) #254
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Frontend E2E Extract Pipeline (VCR) | |
| # Drives the full extract pipeline end-to-end via Playwright: | |
| # login → upload two PDFs → ingest+embed → create extract → run → CSV. | |
| # The LLM call inside `doc_extract_query_task` is wrapped in a VCR.py | |
| # cassette so no OpenAI traffic is generated. See: | |
| # docs/development/e2e_vcr.md | |
| # | |
| # Parsing runs against the in-stack Docling microservice | |
| # (`docling-parser` in `local.yml`), so PDF parsing is real but free. | |
| # The only mocked traffic is the LLM provider (api.openai.com / | |
| # api.anthropic.com), which is what the cassette covers. | |
| env: | |
| DOCKER_BUILDKIT: 1 | |
| COMPOSE_DOCKER_CLI_BUILD: 1 | |
| # Pin the compose project name so container names stay stable | |
| # (`opencontracts-django-1`) regardless of the checkout directory. | |
| # The repo was renamed `OpenContracts` → `cite`, which would otherwise | |
| # change docker compose's default project name (= directory name) to | |
| # `cite` and break the hardcoded container-name lookup in the wait | |
| # loop below. | |
| COMPOSE_PROJECT_NAME: opencontracts | |
| defaults: | |
| run: | |
| working-directory: ./ | |
| on: | |
| workflow_dispatch: | |
| pull_request: | |
| paths: | |
| - "opencontractserver/llms/**" | |
| - "opencontractserver/tasks/data_extract_tasks.py" | |
| - "opencontractserver/tasks/extract_orchestrator_tasks.py" | |
| - "opencontractserver/utils/vcr_replay.py" | |
| - "opencontractserver/tests/fixtures/cassettes/e2e_extract_pdf_workflow/**" | |
| - "frontend/tests/e2e/extract-pdf-workflow.spec.ts" | |
| - "frontend/tests/e2e/helpers.ts" | |
| - "frontend/tests/e2e/fixtures.ts" | |
| - ".github/workflows/frontend-e2e-extract.yml" | |
| concurrency: | |
| group: frontend-e2e-extract-${{ github.head_ref || github.run_id }} | |
| cancel-in-progress: true | |
| jobs: | |
| e2e-extract: | |
| name: Extract pipeline (PDF upload → run → CSV) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 30 | |
| permissions: | |
| contents: read | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: "20" | |
| - name: Install Yarn | |
| run: npm install -g yarn | |
| - name: Install frontend dependencies | |
| working-directory: ./frontend | |
| run: yarn install --frozen-lockfile | |
| - name: Install Playwright browsers | |
| working-directory: ./frontend | |
| run: yarn playwright install --with-deps chromium | |
| # ──────────────────────────────────────────────────────────────── | |
| # Materialize `.envs/.local/*` from the committed `.envs/.test/*` | |
| # templates. Real `.envs/.local/*` files are gitignored because | |
| # they hold developer-machine credentials; CI must NOT use those. | |
| # The `.test` templates ship fake API keys, STORAGE_BACKEND=LOCAL, | |
| # USE_AUTH0=false, and matching Postgres credentials, so they're | |
| # the safe baseline. We then overlay the bits `local.yml` needs | |
| # that the test templates intentionally omit: | |
| # | |
| # * DATABASE_URL — local.yml's django service reads this; the | |
| # test templates leave it unset because test.yml derives it | |
| # from POSTGRES_*. | |
| # * DJANGO_SETTINGS_MODULE — flipped from `config.settings.test` | |
| # to `config.settings.local` so `local.yml`'s `/start` | |
| # entrypoint (which boots the dev server, not the test runner) | |
| # finds its expected settings module. | |
| # * PDF_PARSER=docling — keeps the workflow off LlamaParse. | |
| # ──────────────────────────────────────────────────────────────── | |
| - name: Seed local env files (from .envs/.test) | |
| run: | | |
| mkdir -p .envs/.local | |
| cp .envs/.test/.django .envs/.local/.django | |
| cp .envs/.test/.postgres .envs/.local/.postgres | |
| # Source the postgres template into the shell so we can build | |
| # the matching DATABASE_URL local.yml expects to consume. | |
| set -a | |
| # shellcheck disable=SC1091 | |
| . .envs/.local/.postgres | |
| set +a | |
| echo "DATABASE_URL=postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}" >> .envs/.local/.django | |
| # `local.yml` expects local settings; the test templates pin | |
| # `config.settings.test`. Replace the module rather than | |
| # appending so there's no duplicate setting (env_file uses | |
| # last-wins, but explicit is better than implicit). | |
| sed -i 's|^DJANGO_SETTINGS_MODULE=.*|DJANGO_SETTINGS_MODULE=config.settings.local|' .envs/.local/.django | |
| # The .test template sets TESTING=true; the local stack must | |
| # not pick that up (it would route to the test runner code | |
| # paths). Strip it. | |
| sed -i '/^TESTING=/d' .envs/.local/.django | |
| # Pin the parser away from LlamaParse — see the file-level | |
| # note about Docling. | |
| echo "PDF_PARSER=docling" >> .envs/.local/.django | |
| # Sanity dump (without secrets — there are none in the .test | |
| # template, but be explicit). | |
| echo "--- .envs/.local/.django (head) ---" | |
| grep -E '^(DJANGO_SETTINGS_MODULE|DJANGO_SUPERUSER_USERNAME|STORAGE_BACKEND|USE_AUTH0|PDF_PARSER|DATABASE_URL=postgres://)' .envs/.local/.django || true | |
| - name: Build django image | |
| run: docker compose -f local.yml build django | |
| - name: Start backend stack with VCR replay (and coverage.py wrapping Django) | |
| env: | |
| OC_LLM_VCR_MODE: replay | |
| OC_LLM_VCR_CASSETTE: /app/opencontractserver/tests/fixtures/cassettes/e2e_extract_pdf_workflow/extract.yaml | |
| # CI provisions a fake OpenAI key — the cassette intercepts | |
| # every request so this is never sent. We pin it to a clearly | |
| # bogus value to make accidental real calls fail loudly. | |
| OPENAI_API_KEY: sk-FAKE-VCR-CI-NOT-REAL | |
| # Force the in-stack Docling microservice for PDF parsing so | |
| # the workflow does not depend on LlamaParse credentials and | |
| # produces no external traffic for parsing. | |
| PDF_PARSER: docling | |
| run: | | |
| # `local.e2e-coverage.yml` swaps the Django start command for | |
| # `/start-with-coverage` (coverage.py wrapping `manage.py runserver | |
| # --noreload`). All other services keep their default commands. | |
| docker compose -f local.yml -f local.e2e-coverage.yml up -d | |
| echo "Waiting for django to become healthy…" | |
| for i in {1..60}; do | |
| state=$(docker inspect -f '{{.State.Health.Status}}' \ | |
| opencontracts-django-1 2>/dev/null || echo "starting") | |
| if [ "$state" = "healthy" ]; then echo "django healthy"; break; fi | |
| if [ "$i" = "60" ]; then | |
| echo "django did not become healthy in time" | |
| docker compose -f local.yml logs django | tail -100 | |
| exit 1 | |
| fi | |
| sleep 2 | |
| done | |
| # PipelineSettings is a DB-backed singleton; the PDF_PARSER | |
| # env var only affects how the singleton is *seeded*. Force | |
| # the preferred parser to Docling explicitly so a pre-existing | |
| # singleton (test-DB volume reuse, etc.) does not silently | |
| # route this workflow through LlamaParse. | |
| docker compose -f local.yml exec -T django python manage.py shell -c " | |
| from opencontractserver.documents.models import PipelineSettings | |
| ps = PipelineSettings.get_instance(use_cache=False) | |
| docling = 'opencontractserver.pipeline.parsers.docling_parser_rest.DoclingParser' | |
| for mt in ('application/pdf', | |
| 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', | |
| 'application/vnd.openxmlformats-officedocument.presentationml.presentation'): | |
| ps.preferred_parsers[mt] = docling | |
| ps.save() | |
| print('preferred PDF parser:', ps.get_preferred_parser('application/pdf')) | |
| " | |
| # ──────────────────────────────────────────────────────────────── | |
| # Run the extract spec under coverage. | |
| # | |
| # COVERAGE=true causes vite-plugin-istanbul to instrument the | |
| # frontend source. The fixture in `tests/e2e/fixtures.ts` extracts | |
| # `window.__coverage__` after each test and dumps it under | |
| # `coverage/e2e/.nyc_output/`. We then run `nyc report` directly | |
| # (rather than `yarn test:e2e:coverage`, which is a multi-command | |
| # shell pipeline that doesn't accept --grep filters cleanly) to | |
| # merge those JSON files into an lcov report for Codecov. | |
| # ──────────────────────────────────────────────────────────────── | |
| - name: Run Playwright extract spec with coverage | |
| working-directory: ./frontend | |
| env: | |
| CI: "true" | |
| COVERAGE: "true" | |
| E2E_RUN_LLM_TESTS: "true" | |
| E2E_TEST_USERNAME: admin | |
| # Must match DJANGO_SUPERUSER_PASSWORD in | |
| # `.envs/.test/.django` (which the seed step above copies | |
| # into `.envs/.local/.django`). The .test template ships a | |
| # fixed, public test password — no GitHub Secret involved. | |
| E2E_TEST_PASSWORD: Openc0ntracts_def@ult | |
| run: | | |
| set +e | |
| yarn playwright test --grep "Extract PDF workflow" --reporter=list | |
| E2E_EXIT=$? | |
| mkdir -p coverage/e2e/.nyc_output | |
| yarn nyc report --all \ | |
| --reporter=lcov --reporter=text \ | |
| --report-dir=coverage/e2e \ | |
| --temp-dir=coverage/e2e/.nyc_output \ | |
| || echo 'No coverage data to report (nyc report failed)' | |
| exit $E2E_EXIT | |
| # ──────────────────────────────────────────────────────────────── | |
| # Backend coverage: gracefully stopping Django triggers | |
| # coverage.py's atexit handler which writes /app/.coverage (visible | |
| # on the host via the volume mount). A throwaway exec converts to | |
| # XML for codecov. | |
| # ──────────────────────────────────────────────────────────────── | |
| - name: Stop Django gracefully (triggers coverage write) | |
| if: success() || failure() | |
| run: docker compose -f local.yml stop -t 15 django | |
| - name: Export backend coverage to XML | |
| if: success() || failure() | |
| run: | | |
| docker compose -f local.yml run --rm --no-deps django \ | |
| coverage xml -o /app/coverage-backend-e2e-extract.xml || true | |
| ls -la coverage-backend-e2e-extract.xml || true | |
| - name: Capture backend logs on failure | |
| if: failure() | |
| run: | | |
| mkdir -p artifacts | |
| docker compose -f local.yml ps > artifacts/docker-ps.txt || true | |
| docker compose -f local.yml logs --no-color > artifacts/docker-compose-logs.txt || true | |
| - name: Upload backend logs on failure | |
| if: failure() | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: e2e-extract-backend-logs | |
| path: artifacts/ | |
| - name: Upload Playwright HTML report on failure | |
| if: failure() | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: e2e-extract-playwright-report | |
| path: frontend/playwright-report-e2e/ | |
| if-no-files-found: ignore | |
| # ──────────────────────────────────────────────────────────────── | |
| # Codecov uploads — same flag pattern frontend-e2e.yml uses so the | |
| # patch-coverage check can attribute the extract spec's coverage | |
| # to the right buckets. The extra `extract` flag drilling lets the | |
| # codecov UI break this job out from the broader e2e suite. | |
| # ──────────────────────────────────────────────────────────────── | |
| - name: Upload frontend E2E coverage to Codecov | |
| if: success() || failure() | |
| uses: codecov/codecov-action@v6 | |
| with: | |
| token: ${{ secrets.CODECOV_TOKEN }} | |
| files: frontend/coverage/e2e/lcov.info | |
| # Same flag pattern as `frontend-e2e.yml`. The `frontend` rollup | |
| # rides along so the README badge sees this upload's coverage. | |
| flags: frontend-e2e,frontend | |
| name: frontend-e2e-extract-coverage | |
| fail_ci_if_error: false | |
| disable_search: true | |
| - name: Upload backend E2E coverage to Codecov | |
| if: success() || failure() | |
| uses: codecov/codecov-action@v6 | |
| with: | |
| token: ${{ secrets.CODECOV_TOKEN }} | |
| files: ./coverage-backend-e2e-extract.xml | |
| flags: backend-e2e | |
| name: backend-e2e-extract-coverage | |
| fail_ci_if_error: false | |
| disable_search: true | |
| - name: Tear down backend stack | |
| if: always() | |
| run: docker compose -f local.yml down -v |