Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
9 changes: 9 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Qwen / DashScope credentials
# Fill ONE key variable only. Never commit the real value.
AWARELIQUID_LLM_API_KEY=填入你的千问_API_Key
# DASHSCOPE_API_KEY=填入你的千问_API_Key

# Qwen OpenAI-compatible endpoint
AWARELIQUID_LLM_BASE_URL=https://dashscope.aliyuncs.com/compatible-mode/v1
AWARELIQUID_LLM_MODEL=qwen-plus
AWARELIQUID_LLM_BACKEND=qwen
49 changes: 49 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# 证据门禁(evidence gate):单 job CI。
#
# M1 仓库的治理缺口是"靠自觉、没有机器强制"——文档里声称的证据没人校验。
# 本 job 把两件事变成合并/推送的硬性门槛:
# 1) python -m pytest -q 全量单元测试(离线、CPU-only);
# 2) python scripts/check_results_refs.py 校验 RESULTS.md(单一事实源)里
# 引用的每个路径真实存在、docs/runs/*/manifest.json 指向的仓库内产物存在、
# 每个 *.sha256 sidecar 与实际文件哈希一致。
# 任何一条失败,CI 红灯:杜绝"文档说了但仓库里没有"的断言进入 main。
name: CI

on:
push:
branches: [main]
pull_request:

jobs:
test:
name: test (evidence gate)
runs-on: ubuntu-latest
timeout-minutes: 30
permissions:
contents: read
env:
# 双保险:conftest.py 已在进程内固定 RNG/哈希行为,这里在 CI 环境层面
# 再钉一次 PYTHONHASHSEED,保证任何子进程/工具脚本继承同样的种子。
PYTHONHASHSEED: "0"
steps:
- name: Check out repository
uses: actions/checkout@v4

- name: Set up Python 3.11
uses: actions/setup-python@v5
with:
python-version: "3.11"

# 先装 CPU 版 torch,避免 PyPI 默认 wheel 拖进 CUDA 运行时大包;
# 之后的 `pip install -e '.[dev]'` 检测到 torch 已满足 torch>=2.0.0,不会重装。
- name: Install CPU-only PyTorch
run: pip install torch --index-url https://download.pytorch.org/whl/cpu

- name: Install package with dev dependencies
run: pip install -e '.[dev]'

- name: Run unit tests
run: python -m pytest -q

- name: Check RESULTS.md evidence references
run: python scripts/check_results_refs.py
63 changes: 43 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ at — reading a short, relevant context and answering — and nothing else.
|-------|--------|--------------|
| **Chunk** | `adapter/chunker.py` | Split documents on sentence/paragraph boundaries into overlapping passages (language-agnostic, handles Chinese). |
| **Embed** | `memory/encoder.py` | Lazy, L2-normalised multilingual sentence embedder (`intfloat/multilingual-e5-small` by default; `bge` selectable). |
| **Store** | `memory/knowledge_store.py` | SQLite-backed vector store (cosine) **plus an FTS5/BM25 lexical index**, content-addressable, with an anisotropy-robust centering option and an optional LRU cap. |
| **Retrieve** | `adapter/qa_agent.py` + `adapter/hybrid.py` | **Hybrid retrieval**: dense (e5 cosine) and lexical (BM25) channels fused with Reciprocal Rank Fusion, restricted to a caller-supplied document set. Dense captures meaning; BM25 captures exact tokens (rates, rating codes, `FY2023`). Falls back to dense-only when FTS5 is unavailable. |
| **Store** | `memory/lexical_store.py` | SQLite-backed FTS5/BM25 lexical index with deterministic source metadata and strict document filters. The legacy vector store remains research-only. |
| **Retrieve** | `adapter/qa_agent.py` + `adapter/evidence_index.py` | Competition-safe retrieval: structural source nodes, BM25, exact numeric/clause anchors, bounded neighbor expansion, and source-linked evidence deduplication. |
| **Compress** | `adapter/compressor.py` | Extractive, **LLM-free** sentence selection under a character budget — keeps sentences by question overlap plus a salience bonus for numbers, %, currency and dates. Compression itself costs **zero** generation tokens. |
| **Answer** | `adapter/qwen_client.py` | OpenAI-compatible Qwen chat call with exact per-call token accounting. |

Expand Down Expand Up @@ -97,10 +97,9 @@ python examples/run_qa.py
```

Retrieval and compression are tunable via `RetrievalConfig` (chunk size, overlap,
`top_k`, compression budget, answer-token cap, hybrid-retrieval settings
(`hybrid`, `rrf_k`, `rrf_pool`, dense/sparse weights), and optional multi-query
retrieval (`multi_query`) that issues a sub-query per temporal operand and option
and unions the results — for cross-document comparison / computation questions).
`top_k`, compression budget, answer-token cap, structural evidence settings, and
optional multi-query retrieval). The formal `submit.py` entry point is lexical/BM25
only; the legacy dense/hybrid path is not part of the competition workflow.

## Batch answering

Expand All @@ -112,6 +111,12 @@ python submit.py --questions examples/sample_questions.jsonl \
--docs examples/sample_docs.json --out submission.csv
```

Batch runs are atomic and ordered by default: after each completed question,
`<out>.checkpoint.json` is replaced atomically. Re-running the command resumes
from the first uncommitted question and validates the question/document
fingerprints before continuing. Use `--checkpoint PATH` to choose the file,
`--fresh` to intentionally restart, or `--no-checkpoint` for an ephemeral run.

Questions are JSONL/JSON (`qid`, `question`, `options` as a `{letter: text}` dict
or a list, `answer_format`, `doc_ids`); docs are a `{doc_id: text}` JSON or a
directory of `<doc_id>.txt` files. To score a labelled set end-to-end (accuracy +
Expand All @@ -127,11 +132,15 @@ run is always known.
## Tests

```bash
pytest
python3 -m venv .venv
.venv/bin/python -m pip install -e '.[dev]'
./scripts/test.sh
```

The suite runs offline (mock backend, in-memory store) and covers chunking,
compression, answer parsing and the end-to-end agent loop.
The suite runs offline with the deterministic local mock backend and in-memory
stores; it does not require a Qwen API key or a GPU. The `lexical` retrieval
backend uses SQLite/FTS5 and is the only backend accepted by formal submission.
The original `hybrid` backend remains available for isolated research tests only.

## Validation

Expand All @@ -144,18 +153,32 @@ python benchmarks/bench_adapter.py # real multilingual embedder
python benchmarks/bench_adapter.py --fake # lexical stand-in, no model download
```

On the bundled set (3 documents inflated to ≈2,800 tokens each, 6 questions):

| Metric | Real e5 | Lexical stand-in |
|--------|:-------:|:----------------:|
| Retrieval recall@4 (answer chunk retrieved) | **6/6** | 5/6 |
| Answer-sentence retention after compression | **6/6** | 5/6 |
| Prompt context tokens vs. full-document | **≈1.1k vs 17k (−94%)** | −94% |
| Mean compression ratio | 0.48 | 0.46 |

The semantic embedder recovers a paraphrased question ("归母净利润同比增长" vs. the
document's "归属于母公司股东的净利润…较上年同期增长") that pure lexical overlap misses —
On the bundled set (3 financial documents inflated with boilerplate to ≈22.7k
full-document tokens, 8 questions):

| Metric | Lexical stand-in (8-q set, deterministic) |
|--------|:----------------:|
| Retrieval recall@1, dense → hybrid RRF | 6/8 → 6/8 |
| Retrieval recall@4 (answer chunk retrieved) | 7/8 → 8/8 |
| Answer-sentence retention after compression | 7/8 → 8/8 |
| End-to-end valid rows (formal format) | 8/8 |
| Prompt context tokens vs. full-document | 1254 vs 22668 (−94%) |

These are **single-run screening counts, not conclusions**: they prove the
pipeline works and the gate conditions hold, not that retrieval beats any
baseline. Citing them externally requires multi-seed expansion through the
`publishable()` gate in `benchmarks/experiment_protocol.py`. Numbers are
reconciled in `docs/RESULTS.md` (single source of truth); pre-registration
rules live in `docs/PREREGISTRATION.md`. The `--fake` run is byte-identical
across processes (three-run proof: `benchmarks/results/bench_adapter_fake_20260906_r1.log`
and its `r2`/`r3` siblings; pinned by `tests/test_bench_determinism.py`).

A historical single-run e5 result (6-question set) is archived in
`docs/RESULTS.md` (SUPERSEDED section): it showed the multilingual embedder
recovering a paraphrased question ("归母净利润同比增长" vs. the document's
"归属于母公司股东的净利润…较上年同期增长") that pure lexical overlap misses —
which is why the multilingual model, not a keyword index, drives retrieval.
Re-run it through the `publishable()` gate before citing it anywhere.

**Scope:** this benchmark validates retrieval, compression and token efficiency,
which are the adapter's job. The final letter is chosen by the frozen model, so
Expand Down
2 changes: 2 additions & 0 deletions awareliquid/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
summarize_usage,
)
from .memory import (
LexicalKnowledgeMemory,
PersistentKnowledgeMemory,
SentenceEncoder,
)
Expand All @@ -47,6 +48,7 @@
"summarize_usage",
"build_chat_client",
"PersistentKnowledgeMemory",
"LexicalKnowledgeMemory",
"SentenceEncoder",
"__version__",
]
5 changes: 5 additions & 0 deletions awareliquid/adapter/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,12 @@
MockChatClient,
QwenChatClient,
TokenUsage,
UsageLedger,
build_chat_client,
)
from .chunker import Chunk, chunk_document
from .compressor import CompressedContext, ExtractiveCompressor
from .evidence_index import EvidenceNode, build_evidence_nodes
from .hybrid import rrf_fuse
from .schemas import AnswerResult, parse_answer, summarize_usage

Expand All @@ -27,9 +29,12 @@
"MockChatClient",
"ChatResult",
"TokenUsage",
"UsageLedger",
"build_chat_client",
"Chunk",
"chunk_document",
"EvidenceNode",
"build_evidence_nodes",
"ExtractiveCompressor",
"CompressedContext",
"AnswerResult",
Expand Down
Loading
Loading