conflens 0.1.14__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. conflens-0.1.14/.dockerignore +12 -0
  2. conflens-0.1.14/.env.example +41 -0
  3. conflens-0.1.14/.github/workflows/lint.yml +31 -0
  4. conflens-0.1.14/.github/workflows/release.yml +100 -0
  5. conflens-0.1.14/.github/workflows/test.yml +33 -0
  6. conflens-0.1.14/.gitignore +10 -0
  7. conflens-0.1.14/.python-version +1 -0
  8. conflens-0.1.14/.readthedocs.yaml +26 -0
  9. conflens-0.1.14/AGENTS.md +116 -0
  10. conflens-0.1.14/CLAUDE.md +33 -0
  11. conflens-0.1.14/Dockerfile +43 -0
  12. conflens-0.1.14/LICENSE +373 -0
  13. conflens-0.1.14/PKG-INFO +349 -0
  14. conflens-0.1.14/README.md +325 -0
  15. conflens-0.1.14/conflens/__init__.py +8 -0
  16. conflens-0.1.14/conflens/app.py +788 -0
  17. conflens-0.1.14/conflens/bibtex.py +56 -0
  18. conflens-0.1.14/conflens/cache.py +36 -0
  19. conflens-0.1.14/conflens/classifier.py +174 -0
  20. conflens-0.1.14/conflens/cli.py +75 -0
  21. conflens-0.1.14/conflens/dedup.py +100 -0
  22. conflens-0.1.14/conflens/llm.py +336 -0
  23. conflens-0.1.14/conflens/models.py +116 -0
  24. conflens-0.1.14/conflens/pipeline.py +207 -0
  25. conflens-0.1.14/conflens/pptx_export.py +225 -0
  26. conflens-0.1.14/conflens/scraper.py +277 -0
  27. conflens-0.1.14/conflens/sources.py +839 -0
  28. conflens-0.1.14/conflens/topics.py +365 -0
  29. conflens-0.1.14/conflens/view.py +188 -0
  30. conflens-0.1.14/docker-compose.yml +42 -0
  31. conflens-0.1.14/docs/ARCHITECTURE.md +222 -0
  32. conflens-0.1.14/pyproject.toml +64 -0
  33. conflens-0.1.14/run.py +10 -0
  34. conflens-0.1.14/start_docker.sh +17 -0
  35. conflens-0.1.14/tests/__init__.py +0 -0
  36. conflens-0.1.14/tests/test_bibtex_cache.py +43 -0
  37. conflens-0.1.14/tests/test_classifier_topics.py +115 -0
  38. conflens-0.1.14/tests/test_cli.py +34 -0
  39. conflens-0.1.14/tests/test_dblp_source.py +81 -0
  40. conflens-0.1.14/tests/test_dedup.py +41 -0
  41. conflens-0.1.14/tests/test_emnlp_source.py +42 -0
  42. conflens-0.1.14/tests/test_ijcai_source.py +40 -0
  43. conflens-0.1.14/tests/test_llm.py +88 -0
  44. conflens-0.1.14/tests/test_matching.py +63 -0
  45. conflens-0.1.14/tests/test_naacl_source.py +42 -0
  46. conflens-0.1.14/tests/test_openreview_source.py +125 -0
  47. conflens-0.1.14/tests/test_pscc_source.py +66 -0
  48. conflens-0.1.14/tests/test_result_roundtrip.py +72 -0
  49. conflens-0.1.14/tests/test_scraper.py +42 -0
  50. conflens-0.1.14/tests/test_view.py +60 -0
  51. conflens-0.1.14/uv.lock +2838 -0
@@ -0,0 +1,12 @@
1
+ .git
2
+ .gitignore
3
+ .venv
4
+ venv
5
+ __pycache__
6
+ *.pyc
7
+ *.egg-info
8
+ .cache
9
+ build
10
+ dist
11
+ *.whl
12
+ docs
@@ -0,0 +1,41 @@
1
+ # Conference Paper Analyzer — environment variables
2
+ #
3
+ # Copy this file to `.env` and fill in the key(s) for the provider you use:
4
+ #
5
+ # cp .env.example .env
6
+ #
7
+ # The app loads `.env` automatically on startup. Every value is optional — set
8
+ # only what your chosen LLM provider needs. The in-app "API key" / "LLM endpoint"
9
+ # fields override these at runtime.
10
+
11
+ # ── Anthropic / Claude (default provider) ───────────────────────────────────
12
+ ANTHROPIC_API_KEY=
13
+ # Optional: custom Anthropic-compatible base URL
14
+ # ANTHROPIC_BASE_URL=
15
+
16
+ # ── OpenAI (install: uv sync --extra openai) ───────────────────────────────
17
+ OPENAI_API_KEY=
18
+ # Optional: OpenAI-compatible base URL (the OpenAI SDK reads this automatically;
19
+ # you can also set it per-run in the UI's "LLM endpoint" field)
20
+ # OPENAI_BASE_URL=
21
+
22
+ # ── LiteLLM (install: uv sync --extra litellm) ─────────────────────────────
23
+ # Key for your LiteLLM proxy/endpoint. If unset, LITELLM falls back to OPENAI_API_KEY.
24
+ # Set the endpoint itself in the UI's "LLM endpoint" field.
25
+ # LITELLM_API_KEY=
26
+ #
27
+ # LiteLLM also reads provider-native keys from the environment when it routes to
28
+ # a given backend, e.g.:
29
+ # AZURE_API_KEY=
30
+ # AZURE_API_BASE=
31
+ # GEMINI_API_KEY=
32
+ # COHERE_API_KEY=
33
+
34
+ # ── OpenReview source (ICLR / NeurIPS) ──────────────────────────────────────
35
+ # Usually required. OpenReview now gates anonymous note queries behind an
36
+ # anti-bot challenge (HTTP 403), so ICLR / NeurIPS venues need a logged-in
37
+ # session. Provide credentials to authenticate (this also enables private
38
+ # access). Use EITHER a pre-obtained token OR username + password.
39
+ # OPENREVIEW_TOKEN=
40
+ # OPENREVIEW_USERNAME=
41
+ # OPENREVIEW_PASSWORD=
@@ -0,0 +1,31 @@
1
+ name: Lint
2
+
3
+ # Ruff on every pushed commit (any branch) and every pull request. The
4
+ # concurrency group keys on the branch name — shared by a branch's push and
5
+ # pull_request events — so cancel-in-progress collapses the two into a single
6
+ # run per commit and supersedes stale runs.
7
+ on:
8
+ push:
9
+ pull_request:
10
+
11
+ concurrency:
12
+ group: ${{ github.workflow }}-${{ github.head_ref || github.ref_name }}
13
+ cancel-in-progress: true
14
+
15
+ jobs:
16
+ ruff:
17
+ name: ruff
18
+ runs-on: ubuntu-latest
19
+ steps:
20
+ - uses: actions/checkout@v4
21
+
22
+ - name: Install uv
23
+ uses: astral-sh/setup-uv@v6
24
+ with:
25
+ enable-cache: true
26
+
27
+ - name: Sync dependencies (base + dev)
28
+ run: uv sync
29
+
30
+ - name: Lint (ruff)
31
+ run: uv run ruff check .
@@ -0,0 +1,100 @@
1
+ name: Release
2
+
3
+ # When a pull request is merged into main, bump the version in pyproject.toml
4
+ # (and keep uv.lock in sync), commit it back to main, create a matching tag,
5
+ # then build and publish the package to PyPI.
6
+ #
7
+ # Bump level is patch by default; add a "minor" or "major" label to the PR to
8
+ # bump those instead. The version-bump commit and tag are pushed with the
9
+ # built-in GITHUB_TOKEN, which by design does NOT re-trigger workflows — so this
10
+ # never loops (the `[skip ci]` marker is belt-and-suspenders). Publishing runs
11
+ # in this same job (a separate tag-triggered workflow would never fire, since
12
+ # GITHUB_TOKEN-pushed tags don't trigger workflows).
13
+ #
14
+ # PyPI publishing requires a repo secret PYPI_API_TOKEN (a PyPI API token; set
15
+ # TWINE_USERNAME=__token__). Without it the publish step fails; the bump/tag
16
+ # still succeed.
17
+ on:
18
+ pull_request:
19
+ types: [closed]
20
+ branches: [main]
21
+
22
+ permissions:
23
+ contents: write
24
+
25
+ concurrency:
26
+ group: release
27
+ cancel-in-progress: false
28
+
29
+ jobs:
30
+ tag:
31
+ if: github.event.pull_request.merged == true
32
+ runs-on: ubuntu-latest
33
+ env:
34
+ BUMP: >-
35
+ ${{ contains(github.event.pull_request.labels.*.name, 'major') && 'major'
36
+ || contains(github.event.pull_request.labels.*.name, 'minor') && 'minor'
37
+ || 'patch' }}
38
+ PR_NUMBER: ${{ github.event.pull_request.number }}
39
+ PR_TITLE: ${{ github.event.pull_request.title }}
40
+ steps:
41
+ - name: Checkout main
42
+ uses: actions/checkout@v4
43
+ with:
44
+ ref: main
45
+ fetch-depth: 0
46
+ fetch-tags: true
47
+
48
+ - name: Install uv
49
+ uses: astral-sh/setup-uv@v6
50
+ with:
51
+ enable-cache: true
52
+
53
+ - name: Configure git identity
54
+ run: |
55
+ git config user.name "github-actions[bot]"
56
+ git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
57
+
58
+ - name: Bump version and create tag
59
+ id: bump
60
+ run: |
61
+ set -euo pipefail
62
+ CUR=$(grep -m1 -E '^version *= *"' pyproject.toml | sed -E 's/.*"([^"]+)".*/\1/')
63
+ IFS='.' read -r MA MI PA <<< "$CUR"
64
+ case "$BUMP" in
65
+ major) MA=$((MA + 1)); MI=0; PA=0 ;;
66
+ minor) MI=$((MI + 1)); PA=0 ;;
67
+ *) PA=$((PA + 1)) ;;
68
+ esac
69
+ NEW="${MA}.${MI}.${PA}"
70
+ TAG="v${NEW}"
71
+ echo "PR #${PR_NUMBER} merged — bumping ${CUR} -> ${NEW} (${BUMP})"
72
+
73
+ if git rev-parse -q --verify "refs/tags/${TAG}" >/dev/null; then
74
+ echo "Tag ${TAG} already exists — nothing to do."
75
+ echo "released=false" >> "$GITHUB_OUTPUT"
76
+ exit 0
77
+ fi
78
+
79
+ sed -i -E "s/^version *= *\"[0-9]+\.[0-9]+\.[0-9]+\"/version = \"${NEW}\"/" pyproject.toml
80
+ # Keep the lockfile's project version in step with pyproject.toml.
81
+ uv lock
82
+ git add pyproject.toml uv.lock
83
+ git commit -m "chore: release ${TAG} [skip ci]"
84
+ git tag -a "${TAG}" -m "Release ${TAG} (PR #${PR_NUMBER}: ${PR_TITLE})"
85
+ git push origin HEAD:main
86
+ git push origin "${TAG}"
87
+ echo "released=true" >> "$GITHUB_OUTPUT"
88
+ echo "Released ${TAG}"
89
+
90
+ - name: Build distributions
91
+ if: steps.bump.outputs.released == 'true'
92
+ run: uv build # builds from the just-bumped pyproject.toml -> dist/*
93
+
94
+ - name: Publish to PyPI
95
+ if: steps.bump.outputs.released == 'true'
96
+ env:
97
+ TWINE_USERNAME: __token__
98
+ TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}
99
+ # --skip-existing keeps re-runs idempotent if the version is already up.
100
+ run: uvx twine upload --non-interactive --skip-existing dist/*
@@ -0,0 +1,33 @@
1
+ name: Tests
2
+
3
+ # pytest + package build on every pushed commit (any branch) and every pull
4
+ # request. Concurrency keys on the branch name so a branch's push and
5
+ # pull_request events collapse into a single run and stale runs are superseded.
6
+ on:
7
+ push:
8
+ pull_request:
9
+
10
+ concurrency:
11
+ group: ${{ github.workflow }}-${{ github.head_ref || github.ref_name }}
12
+ cancel-in-progress: true
13
+
14
+ jobs:
15
+ pytest:
16
+ name: pytest + build
17
+ runs-on: ubuntu-latest
18
+ steps:
19
+ - uses: actions/checkout@v4
20
+
21
+ - name: Install uv
22
+ uses: astral-sh/setup-uv@v6
23
+ with:
24
+ enable-cache: true
25
+
26
+ - name: Sync dependencies (base + dev)
27
+ run: uv sync
28
+
29
+ - name: Tests (pytest)
30
+ run: uv run pytest -q
31
+
32
+ - name: Build package
33
+ run: uv build
@@ -0,0 +1,10 @@
1
+ __pycache__/
2
+ *.pyc
3
+ .venv/
4
+ venv/
5
+ .env
6
+ *.egg-info/
7
+ .cache/
8
+ build/
9
+ dist/
10
+ *.whl
@@ -0,0 +1 @@
1
+ 3.13
@@ -0,0 +1,26 @@
1
+
2
+
3
+ # Read the Docs configuration file
4
+ # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
5
+
6
+ # Required
7
+ version: 2
8
+
9
+ # Set the OS, Python version, and other tools you might need
10
+ build:
11
+ os: ubuntu-24.04
12
+ tools:
13
+ python: "3.13"
14
+
15
+ # Build documentation in the "docs/" directory with Sphinx
16
+ sphinx:
17
+ configuration: docs/conf.py
18
+
19
+ # Optionally, but recommended,
20
+ # declare the Python requirements required to build your documentation
21
+ # See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html
22
+ # python:
23
+ # install:
24
+ # - requirements: docs/requirements.txt
25
+
26
+
@@ -0,0 +1,116 @@
1
+ # AGENTS.md
2
+
3
+ Guidance for coding agents (and humans) working in this repository. See
4
+ [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) for diagrams.
5
+
6
+ ## What this is
7
+
8
+ **ConfLens** — a NiceGUI web app that browses conference papers (ACL Anthology,
9
+ EMNLP, NAACL, IJCAI, OpenReview / ICLR · NeurIPS, PSCC, and DBLP-indexed venues
10
+ such as ISGT Europe), classifies them against a theme with an LLM, discovers
11
+ topics, and synthesises per-topic findings. It also
12
+ flags near-duplicate titles and offers a fully client-side results view
13
+ (live confidence re-threshold, keyword search + highlight, sort/author facets,
14
+ save/load a run). Python 3.13, managed with **uv**.
15
+
16
+ ## Setup & run
17
+
18
+ ```bash
19
+ uv sync # base install (Anthropic, OpenAI, LiteLLM + NiceGUI)
20
+ uv sync --extra bertopic # + BERTopic topic backend (heavy)
21
+
22
+ cp .env.example .env # add provider key(s); loaded automatically
23
+ uv run conflens # NiceGUI GUI on http://localhost:6868
24
+ uv run conflens --clear-cache # wipe the on-disk cache
25
+ ```
26
+
27
+ The GUI (`app.py`) is a thin **NiceGUI** presentation layer; all non-UI logic
28
+ lives in `view.py` + `pipeline.py` — put behaviour changes there.
29
+
30
+ Docker: `docker compose up --build` (see the README).
31
+
32
+ ## Checks before you commit
33
+
34
+ Run the full gate — CI (GitHub Actions) runs the same three on every push/PR:
35
+
36
+ ```bash
37
+ uv run ruff check . # lint (also `ruff check --fix .` to auto-fix imports)
38
+ uv run pytest -q # test suite — network- and API-free, fast
39
+ uv build # wheel + sdist build
40
+ ```
41
+
42
+ The tests live in `tests/` and are deterministic: parsers run on HTML/JSON
43
+ fixtures and the LLM stages use a fake `LLMClient` (a class with a
44
+ `structured()` method), so **no network or API keys are needed**. When you add
45
+ behaviour, add a test next to the matching `tests/test_*.py`.
46
+
47
+ Beyond the automated gate, for changes with real runtime surface:
48
+
49
+ - **Boot the app:** `uv run conflens`, confirm `GET /` returns 200
50
+ and the logs are clean.
51
+ - **Scraper/source changes:** also validate against the live source (counts, a
52
+ sample record's title/authors/abstract). ACL listing pages are large and may
53
+ truncate mid-download — `_robust_get` retries for a complete read; don't cache
54
+ a partial. OpenReview's v2 API may challenge anonymous requests from some IPs —
55
+ set `OPENREVIEW_USERNAME`/`OPENREVIEW_PASSWORD` or `OPENREVIEW_TOKEN` to
56
+ authenticate (see `.env.example`).
57
+
58
+ Keep changes ASCII-clean and match the surrounding style (dataclasses, targeted
59
+ regexes over heavyweight parsers, small focused modules).
60
+
61
+ ## Where things live
62
+
63
+ | Area | Module |
64
+ |------|--------|
65
+ | UI (NiceGUI) | `app.py` |
66
+ | View logic | `view.py` (filter/sort/highlight/compute_view/exports) |
67
+ | Exports | `pptx_export.py`, `bibtex.py` |
68
+ | Orchestration | `pipeline.py` (`AnalysisConfig`, `run_analysis`) |
69
+ | Sources | `sources.py` (registry + `make_source`; `IJCAISource`, `OpenReviewSource`, `PSCCSource`, `DBLPSource`), `scraper.py` (`AnthologyScraper` — also serves EMNLP/NAACL) |
70
+ | LLM providers | `llm.py` (`LLMClient`, `make_client`) |
71
+ | Classify / topics | `classifier.py`, `topics.py` |
72
+ | Near-duplicate detection | `dedup.py` (`annotate_duplicates`) |
73
+ | Cache / models | `cache.py`, `models.py` |
74
+
75
+ ## Conventions
76
+
77
+ - **All LLM calls go through `llm.py`'s `structured()`** — never call a provider
78
+ SDK directly from `classifier.py` / `topics.py`.
79
+ - **Add a conference**: implement `resolve_url` / `list_papers` /
80
+ `enrich_abstracts` in `sources.py`, then register it in `SOURCES`.
81
+ - **Add an LLM provider**: add an `LLMClient` subclass + a branch in
82
+ `make_client()`. Everything downstream uses the same interface.
83
+ - **Anything expensive should be cached** under `cache.default_cache_dir()`,
84
+ keyed so a re-run with the same inputs is free (see the caching table in
85
+ ARCHITECTURE.md). Respect the `force_refresh` flag.
86
+ - **Never hardcode API keys.** Read from env / `.env` / the in-app field.
87
+
88
+ ## LLM / model notes
89
+
90
+ - Default model is `gpt-5.4` (OpenAI); structured output uses
91
+ `output_config.format` (Anthropic) or JSON-object mode + schema-in-prompt
92
+ (OpenAI/LiteLLM).
93
+ - The `effort` parameter is only sent to models that support it (e.g. **not**
94
+ Haiku 4.5 — it 400s). See `_EFFORT_MODELS` in `llm.py`.
95
+ - OpenAI/LiteLLM calls fall back gracefully (drop `response_format`, then
96
+ `temperature`) for models/endpoints that reject them.
97
+
98
+ ## Git workflow
99
+
100
+ - **Do not commit to `main` directly.** Create a feature branch per change and
101
+ open a PR into `main`. Name the branch with a `feat/…` or `fix/…` prefix, and
102
+ **the branch pushed to GitHub (the PR's source) must not contain "claude"** in
103
+ its name (see [CLAUDE.md](CLAUDE.md)).
104
+ - **No AI-assistant / Claude Code attribution** anywhere — no `Co-Authored-By`,
105
+ `Claude-Session`, `🤖 Generated with …`, or `claude.ai/code` in commit
106
+ messages or PR descriptions, and don't attribute commit authorship to an
107
+ assistant. See [CLAUDE.md](CLAUDE.md). (The Anthropic *LLM provider* stays —
108
+ it's a feature.)
109
+ - `.env`, `.venv`, `__pycache__`, `~/.cache/...` and build artefacts are
110
+ gitignored — never commit them.
111
+ - **CI** is one workflow per concern under `.github/workflows/`: `lint.yml`
112
+ (ruff on every commit), `test.yml` (pytest + build), `release.yml`
113
+ (version bump + tag on PR merge). Keep lint and tests green.
114
+ - **Releases are automated** (`.github/workflows/release.yml`): merging a PR into
115
+ `main` bumps the version and tags it. Don't bump `version` in `pyproject.toml`
116
+ by hand — control the bump with a `minor`/`major` PR label (default patch).
@@ -0,0 +1,33 @@
1
+ # ConfLens — project rules for agents
2
+
3
+ See [AGENTS.md](AGENTS.md) for contributor/agent guidance and
4
+ [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) for the design.
5
+
6
+ ## Branch naming (persistent — applies to every session and agent)
7
+
8
+ Develop each change on a feature branch named `feat/…` or `fix/…` and open a PR
9
+ into `main`; never commit directly to `main`. **The branch pushed to GitHub (the
10
+ PR's source branch) must not contain "claude" in its name** — this holds even if
11
+ a session's setup designates a `claude/…` branch.
12
+
13
+ ## Attribution policy (persistent — applies to every session and agent)
14
+
15
+ Do **not** add AI-assistant / "Claude Code" attribution anywhere in this
16
+ repository or its GitHub artifacts:
17
+
18
+ - **No** `Co-Authored-By:` trailers, `Claude-Session:` lines, `🤖 Generated
19
+ with …` footers, or `claude.ai/code` links in **commit messages** or
20
+ **pull-request descriptions**.
21
+ - Keep source, docs, and config free of Claude Code / assistant references.
22
+ - Do not attribute commit authorship to an AI assistant.
23
+
24
+ **Exception:** Anthropic / Claude *as an LLM provider* is a genuine product
25
+ feature (the `anthropic` provider and `claude-*` model ids in `llm.py`) and
26
+ stays — this policy is about tooling attribution, not the app's providers.
27
+
28
+ ## Front-end (NiceGUI)
29
+
30
+ The app has a single **NiceGUI** GUI in `conflens/app.py` (console script
31
+ `conflens`). All non-UI logic (filtering, sorting, highlighting, the
32
+ computed view, exports) lives in **`conflens/view.py`** and the pipeline — put
33
+ behaviour changes there so the UI stays a thin presentation layer.
@@ -0,0 +1,43 @@
1
+ # syntax=docker/dockerfile:1
2
+
3
+ # uv-managed image pinned to the project's Python (3.13).
4
+ FROM ghcr.io/astral-sh/uv:python3.13-bookworm-slim
5
+
6
+ # Which optional extras to install (e.g. bertopic).
7
+ # Override at build time, e.g. --build-arg EXTRAS="--extra bertopic".
8
+ ARG EXTRAS=""
9
+
10
+ ENV PYTHONUNBUFFERED=1 \
11
+ UV_COMPILE_BYTECODE=1 \
12
+ UV_LINK_MODE=copy \
13
+ UV_PROJECT_ENVIRONMENT=/app/.venv
14
+
15
+ WORKDIR /app
16
+
17
+ # 1) Install dependencies in a cached layer (no project source yet).
18
+ RUN --mount=type=cache,target=/root/.cache/uv \
19
+ --mount=type=bind,source=uv.lock,target=uv.lock \
20
+ --mount=type=bind,source=pyproject.toml,target=pyproject.toml \
21
+ uv sync --frozen --no-install-project ${EXTRAS}
22
+
23
+ # 2) Copy the project and install it (the conflens entry point).
24
+ COPY . /app
25
+ COPY --link .env* /app/
26
+ RUN --mount=type=cache,target=/root/.cache/uv \
27
+ uv sync --frozen ${EXTRAS}
28
+
29
+ ENV PATH="/app/.venv/bin:${PATH}"
30
+
31
+ # Run as a non-root user; keep the on-disk cache on a persistable path.
32
+ RUN useradd --create-home --uid 1000 app \
33
+ && mkdir -p /home/app/.cache/conflens \
34
+ && chown -R app:app /app /home/app
35
+ USER app
36
+ ENV HOME=/home/app
37
+
38
+ EXPOSE 6868
39
+
40
+ # ENTRYPOINT + CMD: default launches the server; `docker run <image> --clear-cache`
41
+ # (or any other flags) is appended to `conflens`.
42
+ ENTRYPOINT ["conflens"]
43
+ CMD ["--host", "0.0.0.0", "--port", "6868"]