Spaces:
Running on Zero
Running on Zero
Refresh RANGE with maintained CPU runtime, benchmarks and roadmap
Browse filesStandalone RANGE presentation; Python package coming soon. Rebuilt Linux CPU wheel passed10 contract tests plus local single/batch API checks. CUDA benchmark record is historical and distinct from live CPU demo.
- BENCHMARKS.md +60 -0
- CHANGELOG.md +36 -32
- Cargo.toml +7 -7
- DEPLOYMENT.md +204 -202
- PACKAGE_README.md +16 -1
- README.md +41 -14
- SPACE_BUNDLE_MANIFEST.json +16 -16
- app.py +617 -581
- docs/attention2alpha-compatibility-review.md +0 -46
- docs/performance.md +2 -2
- docs/readme-research.md +2 -2
- docs/release.md +10 -10
- docs/taxonomy.md +6 -6
- docs/technical-signoff.md +11 -11
- rust/core.rs +0 -0
- rust/lib.rs +11 -11
- vendor/range_news/_native.cpython-310-x86_64-linux-gnu.so +2 -2
BENCHMARKS.md
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# RANGE benchmark record
|
| 2 |
+
|
| 3 |
+
Updated 2026-09-12. These results describe compact screening of a staged corpus,
|
| 4 |
+
not the live Space, rich explanations, disk ingestion, or a service guarantee.
|
| 5 |
+
|
| 6 |
+
| Evidence | Articles/s | Workload and status |
|
| 7 |
+
| --- | ---: | --- |
|
| 8 |
+
| Maintained CPU, Phase24 | 489,759 | 10 million articles; qualified warm screening |
|
| 9 |
+
| CUDA historical record, Phase60 | 1,833,059 | 10 million articles; exact outputs, stable timing |
|
| 10 |
+
| Earlier installed CUDA, Phase61 | 1,809,133 | Separate qualified local installation |
|
| 11 |
+
| Newer retained CUDA installed, Phase65 | 958,155 | Separate qualified local installation of Phase64 |
|
| 12 |
+
|
| 13 |
+
Hardware: Intel i7-10750H, 10 CPU workers, Windows, RTX 2070 Max-Q (sm75) for CUDA.
|
| 14 |
+
The CUDA figures include input staging/transfers, exact CPU fallback and output
|
| 15 |
+
assembly within classification calls. Dataset loading, imports, configuration
|
| 16 |
+
construction and result verification are outside timing. CPU figures cover warm
|
| 17 |
+
compact screening, excluding loading and rich explanation construction.
|
| 18 |
+
|
| 19 |
+
These are separate experiments. They do not establish a paired CPU/GPU speedup.
|
| 20 |
+
The later Phase64 algorithm won its own controlled comparison, but that does not
|
| 21 |
+
allow multiplying its improvement by the historical Phase60 peak. Session timing
|
| 22 |
+
varied substantially; the cause of cross-session changes is not established.
|
| 23 |
+
|
| 24 |
+
## Evidence identities
|
| 25 |
+
|
| 26 |
+
The 10-million-row Arrow stage SHA-256 is
|
| 27 |
+
769d7de4a8295f4ce3f3d7508774e57ebdeaf2de9b4f4d7e31d1bb5a84d2865f.
|
| 28 |
+
|
| 29 |
+
Phase60's complete-call median was5.45536245 seconds versus6.17977660 for its
|
| 30 |
+
Phase59 control. Its18 calls matched all nine output fields. Both arms passed
|
| 31 |
+
the5% cycle-drift threshold; the same-run improvement was13.2789%.
|
| 32 |
+
Its actual CUDA binary SHA-256 was
|
| 33 |
+
dd943d9adac702e41c2bb0a2b58f78698498edd8a3dfab16cf9d224c8c6a83fa.
|
| 34 |
+
Source checkpoint:8f3fa47, with the original failed finite-gate attempt retained
|
| 35 |
+
in the internal append-only experiment record.
|
| 36 |
+
|
| 37 |
+
Phase65 installed the Phase64 algorithm in a fresh target using existing
|
| 38 |
+
dependencies. It measured958,155 articles/s versus956,701/s for its research
|
| 39 |
+
control. The companion wheel SHA-256 was
|
| 40 |
+
2269c6d41c8ca59bfd48468e5d0c9a00a7abab99297f0194e0227730a2e92643.
|
| 41 |
+
|
| 42 |
+
## What is available
|
| 43 |
+
|
| 44 |
+
The Space runs the maintained CPU/Rust rich-explanation demo. It does not execute
|
| 45 |
+
the CUDA research engine. The Python package is coming soon; no public package
|
| 46 |
+
release or general CUDA portability is claimed here. Current CUDA installation
|
| 47 |
+
qualification is limited to the tested Windows CPython3.10/sm75 environment.
|
| 48 |
+
|
| 49 |
+
## Research still open
|
| 50 |
+
|
| 51 |
+
Release packaging and platform validation come first. Other opportunities include
|
| 52 |
+
sharing work across more taxonomies, workload-based CPU/CUDA selection, direct
|
| 53 |
+
StringZilla integration, other CUSMART methods, and state/compiler optimization
|
| 54 |
+
trained on separate representative development data. These are proposals.
|
| 55 |
+
|
| 56 |
+
Selected alternatives already tested include PFAC, Set Horspool, Wu-Manber,
|
| 57 |
+
SOG-style filtering, SBOM and two CUSMART-listed methods (BNDM/Quick Search).
|
| 58 |
+
Their tested adaptations did not replace the retained matcher. Phase81's two
|
| 59 |
+
methods produced exact outputs but failed timing stability and were much slower.
|
| 60 |
+
This does not reject all variants or all64 CUSMART algorithms.
|
CHANGELOG.md
CHANGED
|
@@ -7,8 +7,8 @@ versioning and uses Python's pre-release version syntax.
|
|
| 7 |
|
| 8 |
### Added
|
| 9 |
|
| 10 |
-
- The public `range_news` Python namespace and **RANGE** product name: Rapid Analysis
|
| 11 |
-
of News for the General Economy.
|
| 12 |
- An exact CPU/Rust path for rich predictions, compact list screening, and Arrow screening.
|
| 13 |
- A conservative, offline general-economy taxonomy covering monetary policy, inflation,
|
| 14 |
labor markets, growth, rates, credit, fiscal policy, trade, supply disruptions,
|
|
@@ -17,40 +17,44 @@ versioning and uses Python's pre-release version syntax.
|
|
| 17 |
ranking, and editable taxonomy copies.
|
| 18 |
- Wheel and source-distribution verification on Windows and Linux CI.
|
| 19 |
- Runnable README examples with visible output.
|
| 20 |
-
- A Space-ready Gradio demo under `app/` with highlighted evidence, evidence JSON,
|
| 21 |
-
batch screening, downloadable outputs, named API endpoints, a multi-stage Docker
|
| 22 |
-
build, and a deterministic offline export manifest.
|
| 23 |
-
- Separate deployment exports for local Docker and free Gradio/ZeroGPU hosting. The
|
| 24 |
-
hosted export vendors the runtime payload from an inspected CPython 3.10
|
| 25 |
-
manylinux2014 wheel while keeping prediction on the compiled CPU/Rust path.
|
| 26 |
- CI validation and a Linux container smoke test for the future Hugging Face Space.
|
| 27 |
|
| 28 |
-
### Changed
|
| 29 |
-
|
| 30 |
-
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
pass leaves the established default taxonomy semantics unchanged.
|
| 41 |
-
- The package version is `0.1.0a1`, explicitly identifying a research alpha.
|
| 42 |
-
- The hosted demo now rejects oversized or malformed requests before inference,
|
| 43 |
-
shares one bounded execution queue across its public endpoints, and hides internal
|
| 44 |
-
error details in production.
|
| 45 |
-
- Evidence downloads now omit screened article text, retain a SHA-256 identifier and text
|
| 46 |
-
lengths, live in Gradio's managed cache, and become eligible for best-effort cleanup
|
| 47 |
-
after five minutes.
|
| 48 |
-
- Hosted dependencies, Docker base images, and the Dockerfile frontend are pinned for
|
| 49 |
-
reproducible deployment builds. Managed networks can supply a trusted build root as
|
| 50 |
-
a temporary secret without disabling TLS verification.
|
| 51 |
|
| 52 |
### Not included
|
| 53 |
|
| 54 |
- No GPU backend.
|
| 55 |
-
- No public or production hosted service, model download, live-data integration,
|
| 56 |
-
trading recommendation, release tag, or package-index publication.
|
|
|
|
| 7 |
|
| 8 |
### Added
|
| 9 |
|
| 10 |
+
- The public `range_news` Python namespace and **RANGE** product name: Rapid Analysis
|
| 11 |
+
of News for the General Economy.
|
| 12 |
- An exact CPU/Rust path for rich predictions, compact list screening, and Arrow screening.
|
| 13 |
- A conservative, offline general-economy taxonomy covering monetary policy, inflation,
|
| 14 |
labor markets, growth, rates, credit, fiscal policy, trade, supply disruptions,
|
|
|
|
| 17 |
ranking, and editable taxonomy copies.
|
| 18 |
- Wheel and source-distribution verification on Windows and Linux CI.
|
| 19 |
- Runnable README examples with visible output.
|
| 20 |
+
- A Space-ready Gradio demo under `app/` with highlighted evidence, evidence JSON,
|
| 21 |
+
batch screening, downloadable outputs, named API endpoints, a multi-stage Docker
|
| 22 |
+
build, and a deterministic offline export manifest.
|
| 23 |
+
- Separate deployment exports for local Docker and free Gradio/ZeroGPU hosting. The
|
| 24 |
+
hosted export vendors the runtime payload from an inspected CPython 3.10
|
| 25 |
+
manylinux2014 wheel while keeping prediction on the compiled CPU/Rust path.
|
| 26 |
- CI validation and a Linux container smoke test for the future Hugging Face Space.
|
| 27 |
|
| 28 |
+
### Changed
|
| 29 |
+
|
| 30 |
+
- Refreshed the standalone RANGE Space with qualified CPU/CUDA benchmark context,
|
| 31 |
+
a package-coming-soon notice, a research roadmap and a rebuilt maintained CPU
|
| 32 |
+
runtime for Linux. Exported documentation omits monorepo-specific branding.
|
| 33 |
+
|
| 34 |
+
- The project is now named **RANGE** throughout its distribution, Python namespace,
|
| 35 |
+
native module, documentation, deployment identifiers, and demo.
|
| 36 |
+
- The README now leads with the job-to-be-done, a working result, intended users,
|
| 37 |
+
an architecture overview, a mechanism-level speed explanation, API selection
|
| 38 |
+
guidance, and carefully qualified benchmark context.
|
| 39 |
+
- The Space demo now separates article relevance from supported exposure, distinguishes
|
| 40 |
+
unresolved evidence paths visually, and includes a verified private-deployment
|
| 41 |
+
runbook.
|
| 42 |
+
- The public package identity, examples, and documentation are no longer tied to the
|
| 43 |
+
parent research project project or a particular leveraged-ETF portfolio. This presentation
|
| 44 |
pass leaves the established default taxonomy semantics unchanged.
|
| 45 |
+
- The package version is `0.1.0a1`, explicitly identifying a research alpha.
|
| 46 |
+
- The hosted demo now rejects oversized or malformed requests before inference,
|
| 47 |
+
shares one bounded execution queue across its public endpoints, and hides internal
|
| 48 |
+
error details in production.
|
| 49 |
+
- Evidence downloads now omit screened article text, retain a SHA-256 identifier and text
|
| 50 |
+
lengths, live in Gradio's managed cache, and become eligible for best-effort cleanup
|
| 51 |
+
after five minutes.
|
| 52 |
+
- Hosted dependencies, Docker base images, and the Dockerfile frontend are pinned for
|
| 53 |
+
reproducible deployment builds. Managed networks can supply a trusted build root as
|
| 54 |
+
a temporary secret without disabling TLS verification.
|
| 55 |
|
| 56 |
### Not included
|
| 57 |
|
| 58 |
- No GPU backend.
|
| 59 |
+
- No public or production hosted service, model download, live-data integration,
|
| 60 |
+
trading recommendation, release tag, or package-index publication.
|
Cargo.toml
CHANGED
|
@@ -7,13 +7,13 @@ publish = false
|
|
| 7 |
license = "MIT"
|
| 8 |
description = "Native engine for RANGE financial-news relevance screening"
|
| 9 |
|
| 10 |
-
[lib]
|
| 11 |
-
name = "_native"
|
| 12 |
-
path = "rust/lib.rs"
|
| 13 |
-
crate-type = ["cdylib"]
|
| 14 |
-
|
| 15 |
-
[dependencies]
|
| 16 |
-
aho-corasick = "1.1.5"
|
| 17 |
arrow-array = "59"
|
| 18 |
arrow-schema = "59"
|
| 19 |
pyo3 = { version = "0.29.2", features = ["extension-module"] }
|
|
|
|
| 7 |
license = "MIT"
|
| 8 |
description = "Native engine for RANGE financial-news relevance screening"
|
| 9 |
|
| 10 |
+
[lib]
|
| 11 |
+
name = "_native"
|
| 12 |
+
path = "rust/lib.rs"
|
| 13 |
+
crate-type = ["cdylib"]
|
| 14 |
+
|
| 15 |
+
[dependencies]
|
| 16 |
+
aho-corasick = "1.1.5"
|
| 17 |
arrow-array = "59"
|
| 18 |
arrow-schema = "59"
|
| 19 |
pyo3 = { version = "0.29.2", features = ["extension-module"] }
|
DEPLOYMENT.md
CHANGED
|
@@ -1,202 +1,204 @@
|
|
| 1 |
-
# Run RANGE locally or deploy it free on Hugging Face
|
| 2 |
-
|
| 3 |
-
RANGE supports two deployments from the same Gradio app:
|
| 4 |
-
|
| 5 |
-
| Route | Best for | Runtime |
|
| 6 |
-
| --- | --- | --- |
|
| 7 |
-
| Local Docker | Private local use and a portable container | Docker with the compiled CPU/Rust engine |
|
| 8 |
-
| Private Hugging Face Space | A shareable demo without a paid plan | Gradio on the free ZeroGPU runtime, with the same CPU/Rust engine |
|
| 9 |
-
|
| 10 |
-
The Hugging Face route does **not** turn RANGE into a GPU application. Its real
|
| 11 |
-
`/analyze` and `/batch` endpoints remain on CPU and never request a GPU allocation.
|
| 12 |
-
The app contains one unused `@spaces.GPU` function solely because the free ZeroGPU
|
| 13 |
-
runtime requires a decorated function.
|
| 14 |
-
|
| 15 |
-
Free ZeroGPU hosting is currently available to eligible personal accounts in good
|
| 16 |
-
standing, with a verified email, an account older than 30 days, and no more than two
|
| 17 |
-
hosted ZeroGPU Spaces. Hugging Face can change these limits. See the
|
| 18 |
-
[ZeroGPU documentation](https://huggingface.co/docs/hub/spaces-zerogpu) before a new
|
| 19 |
-
deployment.
|
| 20 |
-
|
| 21 |
-
## Prepare the repository
|
| 22 |
-
|
| 23 |
-
Run these commands from `dataset/news_relevance/range_news` on the `codex` branch.
|
| 24 |
-
The existing project environment is sufficient for validation:
|
| 25 |
-
|
| 26 |
-
```powershell
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
python
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
--
|
| 33 |
-
`
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
`
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
--
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
--
|
| 87 |
-
--
|
| 88 |
-
`
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
"
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
`
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
--
|
| 133 |
-
--
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
`
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
hf spaces
|
| 149 |
-
hf spaces
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
``
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
-
|
| 192 |
-
|
| 193 |
-
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
|
|
|
|
|
|
|
|
| 1 |
+
# Run RANGE locally or deploy it free on Hugging Face
|
| 2 |
+
|
| 3 |
+
RANGE supports two deployments from the same Gradio app:
|
| 4 |
+
|
| 5 |
+
| Route | Best for | Runtime |
|
| 6 |
+
| --- | --- | --- |
|
| 7 |
+
| Local Docker | Private local use and a portable container | Docker with the compiled CPU/Rust engine |
|
| 8 |
+
| Private Hugging Face Space | A shareable demo without a paid plan | Gradio on the free ZeroGPU runtime, with the same CPU/Rust engine |
|
| 9 |
+
|
| 10 |
+
The Hugging Face route does **not** turn RANGE into a GPU application. Its real
|
| 11 |
+
`/analyze` and `/batch` endpoints remain on CPU and never request a GPU allocation.
|
| 12 |
+
The app contains one unused `@spaces.GPU` function solely because the free ZeroGPU
|
| 13 |
+
runtime requires a decorated function.
|
| 14 |
+
|
| 15 |
+
Free ZeroGPU hosting is currently available to eligible personal accounts in good
|
| 16 |
+
standing, with a verified email, an account older than 30 days, and no more than two
|
| 17 |
+
hosted ZeroGPU Spaces. Hugging Face can change these limits. See the
|
| 18 |
+
[ZeroGPU documentation](https://huggingface.co/docs/hub/spaces-zerogpu) before a new
|
| 19 |
+
deployment.
|
| 20 |
+
|
| 21 |
+
## Prepare the repository
|
| 22 |
+
|
| 23 |
+
Run these commands from `dataset/news_relevance/range_news` on the `codex` branch.
|
| 24 |
+
The existing project environment is sufficient for validation:
|
| 25 |
+
|
| 26 |
+
```powershell
|
| 27 |
+
python -m venv .venv
|
| 28 |
+
.\.venv\Scripts\Activate.ps1
|
| 29 |
+
python -m pip install '.[demo]'
|
| 30 |
+
python app/validate_space.py
|
| 31 |
+
python app/export_space.py `
|
| 32 |
+
--target docker `
|
| 33 |
+
--output build/huggingface-space `
|
| 34 |
+
--force
|
| 35 |
+
```
|
| 36 |
+
|
| 37 |
+
The exporter writes only below `build/` and replaces a directory only when its
|
| 38 |
+
manifest proves that it is an earlier RANGE export.
|
| 39 |
+
|
| 40 |
+
## Route A: run the Docker app locally
|
| 41 |
+
|
| 42 |
+
```powershell
|
| 43 |
+
docker build `
|
| 44 |
+
--tag range-news-space:0.1.0a1 `
|
| 45 |
+
build/huggingface-space
|
| 46 |
+
|
| 47 |
+
docker run --rm --name range-news-space `
|
| 48 |
+
--publish 127.0.0.1:7860:7860 `
|
| 49 |
+
range-news-space:0.1.0a1
|
| 50 |
+
```
|
| 51 |
+
|
| 52 |
+
Open `http://127.0.0.1:7860`. Stop the container with `Ctrl+C`.
|
| 53 |
+
|
| 54 |
+
The Docker bundle is self-contained source: it builds the Rust extension, installs
|
| 55 |
+
the package and Gradio, and runs as an unprivileged user. It does not need a Hugging
|
| 56 |
+
Face account or network access for inference.
|
| 57 |
+
|
| 58 |
+
If downloads fail with an `UnknownIssuer` certificate error on a managed network,
|
| 59 |
+
export that network's trusted root certificate as a PEM file and add
|
| 60 |
+
`--secret "id=build_ca,src=C:\path\to\trusted-root.pem"` to the Docker build command.
|
| 61 |
+
The certificate is available only while dependencies download and is not copied into
|
| 62 |
+
the image. Do not disable TLS verification or add a trusted-host exception.
|
| 63 |
+
|
| 64 |
+
## Route B: build the free Hugging Face bundle
|
| 65 |
+
|
| 66 |
+
Hugging Face does not compile the Rust extension for this route. Export the portable
|
| 67 |
+
CPython 3.10 manylinux2014 wheel from the Docker build first:
|
| 68 |
+
|
| 69 |
+
```powershell
|
| 70 |
+
docker build `
|
| 71 |
+
--platform linux/amd64 `
|
| 72 |
+
--target wheel-export `
|
| 73 |
+
--output "type=local,dest=build/manylinux2014-wheel" `
|
| 74 |
+
build/huggingface-space
|
| 75 |
+
|
| 76 |
+
$Wheels = @(Get-ChildItem `
|
| 77 |
+
-LiteralPath build/manylinux2014-wheel `
|
| 78 |
+
-Filter "range_news-0.1.0a1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl")
|
| 79 |
+
|
| 80 |
+
if ($Wheels.Count -ne 1) {
|
| 81 |
+
throw "Expected exactly one portable RANGE x86_64 wheel."
|
| 82 |
+
}
|
| 83 |
+
$Wheel = $Wheels[0]
|
| 84 |
+
|
| 85 |
+
python app/export_space.py `
|
| 86 |
+
--target zerogpu `
|
| 87 |
+
--wheel $Wheel.FullName `
|
| 88 |
+
--output build/huggingface-zerogpu-space `
|
| 89 |
+
--force
|
| 90 |
+
```
|
| 91 |
+
|
| 92 |
+
The second export contains Gradio Space metadata, the app, package documentation and
|
| 93 |
+
source, and a runtime copy extracted from the validated wheel. Gradio Spaces install
|
| 94 |
+
`requirements.txt` before copying repository files, so a relative wheel reference
|
| 95 |
+
cannot work during their build. Vendoring the inspected wheel payload keeps the build
|
| 96 |
+
self-contained without publishing the package and makes the native extension available
|
| 97 |
+
after the source copy. The requirements file installs RANGE's small Python
|
| 98 |
+
dependencies; the platform supplies Gradio, `spaces`, and `huggingface_hub`.
|
| 99 |
+
|
| 100 |
+
Before uploading, inspect `build/huggingface-zerogpu-space`. Do not add credentials,
|
| 101 |
+
private dataset rows, licensed articles, caches, or machine-specific paths. A public
|
| 102 |
+
Space exposes this bundle as a public repository, including the app source, bundled
|
| 103 |
+
RANGE source and native runtime, package documentation, and default taxonomy. Review
|
| 104 |
+
the uploaded revision history as well as the latest files before changing visibility.
|
| 105 |
+
|
| 106 |
+
## Create and upload the private Space
|
| 107 |
+
|
| 108 |
+
Use a small deployment environment to keep Hub tooling separate from the research
|
| 109 |
+
environment. Create it once; skip the first command when it already exists:
|
| 110 |
+
|
| 111 |
+
```powershell
|
| 112 |
+
conda create --name range-news-hf-deploy --yes python=3.10
|
| 113 |
+
conda activate range-news-hf-deploy
|
| 114 |
+
python -m pip install `
|
| 115 |
+
"huggingface_hub>=1.16,<2" `
|
| 116 |
+
"gradio-client==2.6.1" `
|
| 117 |
+
"pip-system-certs>=5,<6"
|
| 118 |
+
hf auth login
|
| 119 |
+
hf auth whoami --format json
|
| 120 |
+
```
|
| 121 |
+
|
| 122 |
+
`pip-system-certs` lets the Python CLI use the Windows certificate store, including
|
| 123 |
+
institution-managed certificate authorities.
|
| 124 |
+
|
| 125 |
+
Create the private Gradio Space on the free ZeroGPU hardware and upload the prepared
|
| 126 |
+
directory:
|
| 127 |
+
|
| 128 |
+
```powershell
|
| 129 |
+
$SpaceId = "Brianferrell787/range-news"
|
| 130 |
+
|
| 131 |
+
hf repos create $SpaceId `
|
| 132 |
+
--type space `
|
| 133 |
+
--sdk gradio `
|
| 134 |
+
--flavor zero-a10g `
|
| 135 |
+
--private
|
| 136 |
+
|
| 137 |
+
hf upload $SpaceId build/huggingface-zerogpu-space . `
|
| 138 |
+
--repo-type space `
|
| 139 |
+
--commit-message "Deploy private RANGE 0.1.0a1 demo"
|
| 140 |
+
```
|
| 141 |
+
|
| 142 |
+
Do not run the upload if creation fails. For an existing Space, first verify that its
|
| 143 |
+
repository is private, its SDK is Gradio, and its hardware is ZeroGPU.
|
| 144 |
+
|
| 145 |
+
## Verify the live deployment
|
| 146 |
+
|
| 147 |
+
```powershell
|
| 148 |
+
hf spaces wait $SpaceId --timeout 30m
|
| 149 |
+
hf spaces logs $SpaceId --build -n 200
|
| 150 |
+
hf spaces logs $SpaceId -n 200
|
| 151 |
+
hf spaces info $SpaceId --expand sdk,runtime --format json
|
| 152 |
+
```
|
| 153 |
+
|
| 154 |
+
Open `https://huggingface.co/spaces/Brianferrell787/range-news` and test all three
|
| 155 |
+
single-article examples plus the batch example. The Space can sleep when idle, so the
|
| 156 |
+
first request may include a cold start.
|
| 157 |
+
|
| 158 |
+
Private API calls use the token already stored by `hf auth login`:
|
| 159 |
+
|
| 160 |
+
```python
|
| 161 |
+
from gradio_client import Client
|
| 162 |
+
from huggingface_hub import get_token
|
| 163 |
+
|
| 164 |
+
client = Client("Brianferrell787/range-news", token=get_token())
|
| 165 |
+
client.view_api()
|
| 166 |
+
```
|
| 167 |
+
|
| 168 |
+
The generated `agents.md` points coding agents to the API schema, which lists the
|
| 169 |
+
named `/analyze` and `/batch` endpoints.
|
| 170 |
+
|
| 171 |
+
## Privacy and performance boundaries
|
| 172 |
+
|
| 173 |
+
- Do not add an HF token as a Space secret; RANGE needs no runtime credential.
|
| 174 |
+
- Text submitted to the hosted demo is processed on Hugging Face infrastructure. Do
|
| 175 |
+
not submit confidential or unlicensed material.
|
| 176 |
+
- The interface and named API endpoints reject more than 100 rows, more than 20,000
|
| 177 |
+
characters or 80,000 UTF-8 bytes in one article, and more than 200,000 characters
|
| 178 |
+
or 800,000 UTF-8 bytes across a batch. Use the package's compact or Arrow API for
|
| 179 |
+
large datasets. Batch rows must have exactly four columns. Publication timestamps
|
| 180 |
+
are capped at 128 characters, while article IDs and sources are capped at 256
|
| 181 |
+
characters each. Metadata across the batch is capped at 64,000 characters and
|
| 182 |
+
256,000 UTF-8 bytes, and combined article and metadata cell content is capped at
|
| 183 |
+
264,000 characters and 1,056,000 UTF-8 bytes. These caps do not count JSON framing
|
| 184 |
+
or Gradio's transport envelope. A separate 4 MiB server limit covers the complete
|
| 185 |
+
HTTP request body before JSON parsing.
|
| 186 |
+
- Downloaded evidence identifies the screened article by text length and SHA-256 hash
|
| 187 |
+
instead of copying the screened text. RANGE removes leading and trailing whitespace
|
| 188 |
+
before screening. Download files become eligible for best-effort
|
| 189 |
+
deletion after five minutes, cleanup runs once a minute, and a Space restart may
|
| 190 |
+
remove them sooner. They are not durable or private storage.
|
| 191 |
+
- Production responses hide internal error details. Use the Space logs to diagnose a
|
| 192 |
+
failure; do not expose a traceback or exception text in the browser.
|
| 193 |
+
- Package benchmark rates describe the native batch engine on the recorded hardware,
|
| 194 |
+
not the latency or throughput of this shared two-core hosted demo.
|
| 195 |
+
- A `relevant` result means the configured financial and economic policy accepted the
|
| 196 |
+
article. It is not sentiment, materiality, a return forecast, or investment advice.
|
| 197 |
+
|
| 198 |
+
## Planned Python 3.12 migration
|
| 199 |
+
|
| 200 |
+
The current alpha bundle uses CPython 3.10. Python 3.12 support is a planned follow-up,
|
| 201 |
+
not part of this deployment. Because RANGE includes a compiled Rust extension, that
|
| 202 |
+
migration must update and test the package metadata, build image, native wheel, Space
|
| 203 |
+
runtime, exported bundle, and clean-install checks together. Do not change only the
|
| 204 |
+
Space metadata or try to load the existing `cp310` wheel in Python 3.12.
|
PACKAGE_README.md
CHANGED
|
@@ -20,6 +20,12 @@ RANGE answers three practical questions for each article:
|
|
| 20 |
|
| 21 |
It runs on CPU, makes no network request during inference, and does not depend on an embedding model, an external API, or an LLM. The `0.1.0a1` source and CI artifacts are prepared for review but are **not published to PyPI**.
|
| 22 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
## See it in 30 seconds
|
| 24 |
|
| 25 |
Install from this checkout:
|
|
@@ -116,7 +122,16 @@ The result is low-overhead, predictable CPU screening with a rich Python path av
|
|
| 116 |
|
| 117 |
### Research benchmark context
|
| 118 |
|
| 119 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 120 |
|
| 121 |
## Choose the right API
|
| 122 |
|
|
|
|
| 20 |
|
| 21 |
It runs on CPU, makes no network request during inference, and does not depend on an embedding model, an external API, or an LLM. The `0.1.0a1` source and CI artifacts are prepared for review but are **not published to PyPI**.
|
| 22 |
|
| 23 |
+
A separate optional GPU research companion now has local Windows CPython3.10/sm75
|
| 24 |
+
installation qualification for bulk Arrow screening. See the
|
| 25 |
+
[GPU research installation and results](https://huggingface.co/spaces/Brianferrell787/range-news/blob/main/BENCHMARKS.md)
|
| 26 |
+
for the local artifacts, measured performance and supported scope. The CPU
|
| 27 |
+
installation below remains the maintained package default.
|
| 28 |
+
|
| 29 |
## See it in 30 seconds
|
| 30 |
|
| 31 |
Install from this checkout:
|
|
|
|
| 122 |
|
| 123 |
### Research benchmark context
|
| 124 |
|
| 125 |
+
**Python package coming soon.** The maintained CPU implementation recorded
|
| 126 |
+
**489,759 articles/s** in a qualified 10-million-article warm screening run.
|
| 127 |
+
CUDA research recorded a historical qualified **1,833,059 articles/s** on that
|
| 128 |
+
workload size; the newer retained CUDA installation separately measured
|
| 129 |
+
**958,155 articles/s**. These are different sessions, not a paired speedup.
|
| 130 |
+
|
| 131 |
+
Measurements used an Intel i7-10750H Windows laptop with 10 CPU workers and an
|
| 132 |
+
RTX 2070 Max-Q for CUDA. Timings cover compact screening, including CUDA
|
| 133 |
+
transfers/fallback/assembly, but exclude loading and initialization. They are
|
| 134 |
+
not hosted-demo performance. See the Space's BENCHMARKS.md for provenance.
|
| 135 |
|
| 136 |
## Choose the right API
|
| 137 |
|
README.md
CHANGED
|
@@ -23,6 +23,8 @@ tags:
|
|
| 23 |
|
| 24 |
**Rapid Analysis of News for the General Economy**
|
| 25 |
|
|
|
|
|
|
|
| 26 |
RANGE checks timestamped financial news, identifies whether it matches the current
|
| 27 |
financial and economic rules, shows any supported broad-market connection, and
|
| 28 |
explains exactly which words and rules produced the result.
|
|
@@ -76,23 +78,48 @@ eligible for deletion after five minutes, and the app makes a best-effort cleanu
|
|
| 76 |
once a minute. The host may remove it sooner when the Space restarts. Save a file
|
| 77 |
promptly if you need it, and do not rely on the download link as an archive.
|
| 78 |
|
| 79 |
-
##
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 80 |
|
| 81 |
-
|
| 82 |
|
| 83 |
-
-
|
| 84 |
-
|
| 85 |
-
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 89 |
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
We measured this with a specially optimized RANGE build on one Intel i7-10750H Windows
|
| 93 |
-
laptop using 10 CPU workers. These are the fastest recorded results for the screening
|
| 94 |
-
step alone. Loading data, running a complete pipeline, and using the hosted demo will
|
| 95 |
-
be slower. Speed varies with hardware, article length, and screening rules.
|
| 96 |
|
| 97 |
## What the result means
|
| 98 |
|
|
|
|
| 23 |
|
| 24 |
**Rapid Analysis of News for the General Economy**
|
| 25 |
|
| 26 |
+
**Python package coming soon. CPU today, optional CUDA for bulk screening.**
|
| 27 |
+
|
| 28 |
RANGE checks timestamped financial news, identifies whether it matches the current
|
| 29 |
financial and economic rules, shows any supported broad-market connection, and
|
| 30 |
explains exactly which words and rules produced the result.
|
|
|
|
| 78 |
once a minute. The host may remove it sooner when the Space restarts. Save a file
|
| 79 |
promptly if you need it, and do not rely on the download link as an archive.
|
| 80 |
|
| 81 |
+
## Measured speed. Clear boundaries.
|
| 82 |
+
|
| 83 |
+
| Implementation | Articles / second | Measurement |
|
| 84 |
+
| --- | ---: | --- |
|
| 85 |
+
| CUDA research record | **1,833,059** | Qualified warm screening of 10 million articles |
|
| 86 |
+
| Maintained CPU implementation | **489,759** | Qualified warm screening of 10 million articles |
|
| 87 |
+
| Newer retained CUDA installation | **958,155** | Separate local installed-package qualification |
|
| 88 |
+
|
| 89 |
+
Measured on an Intel i7-10750H Windows laptop with 10 CPU workers; CUDA used an
|
| 90 |
+
RTX 2070 Max-Q. These are separate experiments, not a same-run CPU/GPU speedup.
|
| 91 |
+
The CUDA peak belongs to an earlier qualified implementation; later sessions
|
| 92 |
+
varied substantially. Do not combine its peak with later percentage gains.
|
| 93 |
+
|
| 94 |
+
Screening timings exclude file loading, imports, initialization and verification.
|
| 95 |
+
CUDA timings include transfers, exact CPU fallback and result assembly.
|
| 96 |
+
The live demo uses a different CPU environment and rich explanations, so these
|
| 97 |
+
figures are not its throughput or a service guarantee.
|
| 98 |
+
|
| 99 |
+
[Read the benchmark record](https://huggingface.co/spaces/Brianferrell787/range-news/blob/main/BENCHMARKS.md)
|
| 100 |
+
|
| 101 |
+
## Python package coming soon
|
| 102 |
+
|
| 103 |
+
We are preparing RANGE for straightforward Python installation, with CPU support
|
| 104 |
+
and an optional CUDA backend. Public package release, clean installation across
|
| 105 |
+
supported platforms and broader GPU compatibility are still ahead.
|
| 106 |
+
The current Space lets you explore the engine today.
|
| 107 |
|
| 108 |
+
## What we want to try next
|
| 109 |
|
| 110 |
+
- **Finish the release:** clean installs, supported platforms, documentation and
|
| 111 |
+
validation of the exact wheels people will use.
|
| 112 |
+
- **Share work across taxonomies:** build on successful two-taxonomy experiments,
|
| 113 |
+
then extend packaging and memory scheduling for more rule sets.
|
| 114 |
+
- **Choose CPU or CUDA by workload:** measure small batches, large batches and
|
| 115 |
+
routing overhead before introducing automatic selection.
|
| 116 |
+
- **Target remaining matching ideas:** direct StringZilla integration and other
|
| 117 |
+
CUSMART methods remain candidates, not demonstrated improvements.
|
| 118 |
+
- **Train optimizations on separate data:** investigate corpus-trained automata
|
| 119 |
+
and fresh compiler profiles using representative development articles.
|
| 120 |
|
| 121 |
+
Recent alternatives did not beat the retained matcher. We keep negative results
|
| 122 |
+
and require exact outputs before considering a speed improvement.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 123 |
|
| 124 |
## What the result means
|
| 125 |
|
SPACE_BUNDLE_MANIFEST.json
CHANGED
|
@@ -1,21 +1,21 @@
|
|
| 1 |
{
|
| 2 |
"files": {
|
| 3 |
".gitattributes": "30af955bc8b9589ff13954efae1020fb4fcbd3e2114ce2530c1e19a198331490",
|
| 4 |
-
"
|
|
|
|
| 5 |
"CITATION.cff": "acf3bf2c922c2afd0f8cc336024f525d9334c4b94b83efaa96dc36a1df5c6198",
|
| 6 |
"Cargo.lock": "64eaf82038b21074bf9d6b30e00bf05536958b08463dbe4a4c9a88a86e28a2d8",
|
| 7 |
-
"Cargo.toml": "
|
| 8 |
-
"DEPLOYMENT.md": "
|
| 9 |
"LICENSE": "98564786fde1cf443f2b761bc9c3b329dcac3528b3c383a66b25101ee9670585",
|
| 10 |
-
"PACKAGE_README.md": "
|
| 11 |
-
"README.md": "
|
| 12 |
-
"app.py": "
|
| 13 |
-
"docs/
|
| 14 |
-
"docs/
|
| 15 |
-
"docs/
|
| 16 |
-
"docs/
|
| 17 |
-
"docs/
|
| 18 |
-
"docs/technical-signoff.md": "4c1efa828b2460ff5092f07df30319ca7775ec3118ccdfc573aaa5b2980928c5",
|
| 19 |
"examples.py": "94da2be9d8e523a54b6aae6fbd365c5e3456e836b120e4a4a0bd78acb5969d10",
|
| 20 |
"examples/app.py": "53c76cc3834e73435483db6fbf89d8125a0f7aa590269d880cffdaa8e40097f5",
|
| 21 |
"examples/benchmark_hf.py": "5df4e9d756a6447eb5f462ac7b72aaf2a8c1d92558a58620e5abb51e97e62bca",
|
|
@@ -23,12 +23,12 @@
|
|
| 23 |
"examples/quickstart.py": "19bdd779ec202957c72c2b49e7bf778a13ea1b94d0a52f1bba5521810eb71626",
|
| 24 |
"pyproject.toml": "d40182524a367d9ad2d08892450b4bcecfb2133c92bf45867c4e8f4a05910300",
|
| 25 |
"requirements.txt": "94f5ee00edc14a89e5bf6f5fcb90261cde4a57dc3ebc20e0639e68ec504cc220",
|
| 26 |
-
"rust/core.rs": "
|
| 27 |
"rust/generated/python_whitespace.bin": "6c314375b6c075589b515139e50068d72c0cbd7daf4246e555a9dcfc9748ecc9",
|
| 28 |
"rust/generated/unicode_index.bin": "fe152ac184aa77c40d0f040ba725140b17d845bae9fae255b2b3d3c43bb79c8f",
|
| 29 |
"rust/generated/unicode_meta.txt": "d7b7f5d807b712597a04e8cf1394c1734962c4e042057cc7fca85f5173919d38",
|
| 30 |
"rust/generated/unicode_pool.bin": "109a8da1563408ad1da82919a5dfa1ab9fa18870f0f4d22e2818857b5a4fc30f",
|
| 31 |
-
"rust/lib.rs": "
|
| 32 |
"src/range_news/__init__.py": "816ba0ee72545cc03d6e5183d46530acd026e67f4b38d6d972a1abd520eb899d",
|
| 33 |
"src/range_news/_core/__init__.py": "7e6c8615e347bca422f9b1e6e10160ac277c346956adf9e86a8a08419f879e96",
|
| 34 |
"src/range_news/_core/accelerated/__init__.py": "ed04df1ca0da8254d9558201c6172969626aced74864842c5a17068ce87a6999",
|
|
@@ -59,7 +59,7 @@
|
|
| 59 |
"vendor/range_news/_core/configs/default.yml": "6e443b14b9e7c4524e8dbdd3035726627345d01d02cc21de7a53a133dea5802c",
|
| 60 |
"vendor/range_news/_core/indicator.py": "488178693d90e7a7f329b40309eb4583908d5a990cd0b293b272c5fbb192f3a4",
|
| 61 |
"vendor/range_news/_core/models.py": "e152077ecd18c31eed76b150e234934180a5ffad061d2bf26e35229160cf2733",
|
| 62 |
-
"vendor/range_news/_native.cpython-310-x86_64-linux-gnu.so": "
|
| 63 |
"vendor/range_news/_taxonomy_impl.py": "db6802ef00114e33bbb47b2d71886f037e151f397ae82258d1a6760f43bdfed1",
|
| 64 |
"vendor/range_news/api.py": "1d165a3211fad9a9de1c274320dc21e787303fcd1a37599e399ba82881d4886d",
|
| 65 |
"vendor/range_news/backends.py": "7a25879d21ed9992aeecc9dc5f4093de00ae68c117646b132140d663987829ca",
|
|
@@ -73,7 +73,7 @@
|
|
| 73 |
"runtime_package": "vendor/range_news",
|
| 74 |
"sdk": "gradio",
|
| 75 |
"source_wheel": "range_news-0.1.0a1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",
|
| 76 |
-
"source_wheel_sha256": "
|
| 77 |
"target": "zerogpu",
|
| 78 |
"version": "0.1.0a1"
|
| 79 |
}
|
|
|
|
| 1 |
{
|
| 2 |
"files": {
|
| 3 |
".gitattributes": "30af955bc8b9589ff13954efae1020fb4fcbd3e2114ce2530c1e19a198331490",
|
| 4 |
+
"BENCHMARKS.md": "7035a05f986a6c28a91262d34e499ae3d2612931673a82d7ad778f1bad9c15da",
|
| 5 |
+
"CHANGELOG.md": "63ef44e35ad6fbc3184d93141e0494eeb18a0289c5adda22ab1b0f8abebaa35b",
|
| 6 |
"CITATION.cff": "acf3bf2c922c2afd0f8cc336024f525d9334c4b94b83efaa96dc36a1df5c6198",
|
| 7 |
"Cargo.lock": "64eaf82038b21074bf9d6b30e00bf05536958b08463dbe4a4c9a88a86e28a2d8",
|
| 8 |
+
"Cargo.toml": "962f3ed0cdcf18aafd4b83f3073fb087934472a8f718f87a37885318804ba665",
|
| 9 |
+
"DEPLOYMENT.md": "b370e9b8c3f2b3118b2298a21dda028b1aad9b5b588cf00235ffbaedd4bd6dde",
|
| 10 |
"LICENSE": "98564786fde1cf443f2b761bc9c3b329dcac3528b3c383a66b25101ee9670585",
|
| 11 |
+
"PACKAGE_README.md": "0652c0f42333f96de7a7f35c5fa15d2a20a496df7c0f5cfa78ab63e2ebffbdb0",
|
| 12 |
+
"README.md": "0b2a476a20c8502e0274c01c643f6d5152aeafe92a2f3f70a4b16a80b280bd4a",
|
| 13 |
+
"app.py": "d666b475abadbb723baa1cc5f1ef5ca981e682f4839e448547febfac1a614845",
|
| 14 |
+
"docs/performance.md": "b25202cb154bb2bcdef48c14af8cedc821a2dd298fe32d53608d4dcc0f1f7fbb",
|
| 15 |
+
"docs/readme-research.md": "2f8e2f3e7ed2dc386995ae6c9abca89b7e910209716ad31102f633a9a2bb7fe7",
|
| 16 |
+
"docs/release.md": "e449a9001d97b20ea1f9b505d9982eec5b97ec068cae3e0574a498949ac0363e",
|
| 17 |
+
"docs/taxonomy.md": "c1a33b8efd93a87d3ccfc413a879f11a088290ec9859a5c4254cf88d86e21998",
|
| 18 |
+
"docs/technical-signoff.md": "ba912d29e0ba23d1c8d42ad5616663b811b610a98af9941ed770de1a89ea88a8",
|
|
|
|
| 19 |
"examples.py": "94da2be9d8e523a54b6aae6fbd365c5e3456e836b120e4a4a0bd78acb5969d10",
|
| 20 |
"examples/app.py": "53c76cc3834e73435483db6fbf89d8125a0f7aa590269d880cffdaa8e40097f5",
|
| 21 |
"examples/benchmark_hf.py": "5df4e9d756a6447eb5f462ac7b72aaf2a8c1d92558a58620e5abb51e97e62bca",
|
|
|
|
| 23 |
"examples/quickstart.py": "19bdd779ec202957c72c2b49e7bf778a13ea1b94d0a52f1bba5521810eb71626",
|
| 24 |
"pyproject.toml": "d40182524a367d9ad2d08892450b4bcecfb2133c92bf45867c4e8f4a05910300",
|
| 25 |
"requirements.txt": "94f5ee00edc14a89e5bf6f5fcb90261cde4a57dc3ebc20e0639e68ec504cc220",
|
| 26 |
+
"rust/core.rs": "db6d48765f3a551bf81bc0fd6b471306a587f75e265c35f2f277b3115ffba172",
|
| 27 |
"rust/generated/python_whitespace.bin": "6c314375b6c075589b515139e50068d72c0cbd7daf4246e555a9dcfc9748ecc9",
|
| 28 |
"rust/generated/unicode_index.bin": "fe152ac184aa77c40d0f040ba725140b17d845bae9fae255b2b3d3c43bb79c8f",
|
| 29 |
"rust/generated/unicode_meta.txt": "d7b7f5d807b712597a04e8cf1394c1734962c4e042057cc7fca85f5173919d38",
|
| 30 |
"rust/generated/unicode_pool.bin": "109a8da1563408ad1da82919a5dfa1ab9fa18870f0f4d22e2818857b5a4fc30f",
|
| 31 |
+
"rust/lib.rs": "6d099c6cbc33b85bd7633a790f2b8d3a0296d74d604fbabf14806634e538fb0a",
|
| 32 |
"src/range_news/__init__.py": "816ba0ee72545cc03d6e5183d46530acd026e67f4b38d6d972a1abd520eb899d",
|
| 33 |
"src/range_news/_core/__init__.py": "7e6c8615e347bca422f9b1e6e10160ac277c346956adf9e86a8a08419f879e96",
|
| 34 |
"src/range_news/_core/accelerated/__init__.py": "ed04df1ca0da8254d9558201c6172969626aced74864842c5a17068ce87a6999",
|
|
|
|
| 59 |
"vendor/range_news/_core/configs/default.yml": "6e443b14b9e7c4524e8dbdd3035726627345d01d02cc21de7a53a133dea5802c",
|
| 60 |
"vendor/range_news/_core/indicator.py": "488178693d90e7a7f329b40309eb4583908d5a990cd0b293b272c5fbb192f3a4",
|
| 61 |
"vendor/range_news/_core/models.py": "e152077ecd18c31eed76b150e234934180a5ffad061d2bf26e35229160cf2733",
|
| 62 |
+
"vendor/range_news/_native.cpython-310-x86_64-linux-gnu.so": "e05b832a8b33169dd988b4bc88a36bfb1f5b40f70676a09ecda727961d123c9c",
|
| 63 |
"vendor/range_news/_taxonomy_impl.py": "db6802ef00114e33bbb47b2d71886f037e151f397ae82258d1a6760f43bdfed1",
|
| 64 |
"vendor/range_news/api.py": "1d165a3211fad9a9de1c274320dc21e787303fcd1a37599e399ba82881d4886d",
|
| 65 |
"vendor/range_news/backends.py": "7a25879d21ed9992aeecc9dc5f4093de00ae68c117646b132140d663987829ca",
|
|
|
|
| 73 |
"runtime_package": "vendor/range_news",
|
| 74 |
"sdk": "gradio",
|
| 75 |
"source_wheel": "range_news-0.1.0a1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",
|
| 76 |
+
"source_wheel_sha256": "48581327a16ef46a6da9dbf7f00a296b7037dd7ba1b3af3e9108a945729174b7",
|
| 77 |
"target": "zerogpu",
|
| 78 |
"version": "0.1.0a1"
|
| 79 |
}
|
app.py
CHANGED
|
@@ -1,14 +1,14 @@
|
|
| 1 |
-
"""Space-ready Gradio demo for explainable financial-news relevance screening."""
|
| 2 |
|
| 3 |
from __future__ import annotations
|
| 4 |
|
| 5 |
-
import dataclasses
|
| 6 |
-
import hashlib
|
| 7 |
-
import html
|
| 8 |
-
import json
|
| 9 |
-
import os
|
| 10 |
-
import sys
|
| 11 |
-
import uuid
|
| 12 |
from collections.abc import Mapping, Sequence
|
| 13 |
from datetime import datetime
|
| 14 |
from enum import Enum
|
|
@@ -16,21 +16,21 @@ from functools import lru_cache
|
|
| 16 |
from pathlib import Path
|
| 17 |
from typing import Any
|
| 18 |
|
| 19 |
-
import gradio as gr
|
| 20 |
-
from starlette.middleware import Middleware
|
| 21 |
-
from starlette.middleware.body_limit import RequestBodyLimitMiddleware
|
| 22 |
-
from gradio.utils import get_upload_folder
|
| 23 |
-
|
| 24 |
-
VENDORED_PACKAGE_ROOT = Path(__file__).resolve().parent / "vendor"
|
| 25 |
-
if VENDORED_PACKAGE_ROOT.is_dir():
|
| 26 |
-
sys.path.insert(0, str(VENDORED_PACKAGE_ROOT))
|
| 27 |
-
|
| 28 |
-
try:
|
| 29 |
-
import spaces
|
| 30 |
except ModuleNotFoundError: # Local installs and the Docker image do not need it.
|
| 31 |
spaces = None
|
| 32 |
|
| 33 |
-
from range_news import RelevanceEngine, __version__
|
| 34 |
|
| 35 |
from examples import ARTICLE_EXAMPLES, BATCH_ROWS, SINGLE_EXAMPLES
|
| 36 |
|
|
@@ -46,23 +46,23 @@ if spaces is not None:
|
|
| 46 |
|
| 47 |
GRADIO_MAJOR = int(gr.__version__.split(".", 1)[0])
|
| 48 |
THEME = gr.themes.Soft(primary_hue="teal", neutral_hue="slate")
|
| 49 |
-
MAX_BATCH_ROWS = 100
|
| 50 |
-
EXPECTED_BATCH_COLUMNS = 4
|
| 51 |
-
MAX_ARTICLE_CHARS = 20_000
|
| 52 |
-
MAX_ARTICLE_BYTES = 80_000
|
| 53 |
-
MAX_TIMESTAMP_CHARS = 128
|
| 54 |
-
MAX_TIMESTAMP_BYTES = 512
|
| 55 |
-
MAX_ARTICLE_ID_CHARS = 256
|
| 56 |
-
MAX_ARTICLE_ID_BYTES = 1_024
|
| 57 |
-
MAX_SOURCE_CHARS = 256
|
| 58 |
-
MAX_SOURCE_BYTES = 1_024
|
| 59 |
-
MAX_BATCH_CHARS = 200_000
|
| 60 |
-
MAX_BATCH_BYTES = 800_000
|
| 61 |
-
MAX_BATCH_METADATA_CHARS = 64_000
|
| 62 |
-
MAX_BATCH_METADATA_BYTES = 256_000
|
| 63 |
-
MAX_BATCH_CONTENT_CHARS = MAX_BATCH_CHARS + MAX_BATCH_METADATA_CHARS
|
| 64 |
-
MAX_BATCH_CONTENT_BYTES = MAX_BATCH_BYTES + MAX_BATCH_METADATA_BYTES
|
| 65 |
-
MAX_HTTP_REQUEST_BYTES = 4 * 1024 * 1024
|
| 66 |
EVIDENCE_HEADERS = [
|
| 67 |
"evidence path",
|
| 68 |
"matched text",
|
|
@@ -81,98 +81,103 @@ BATCH_HEADERS = [
|
|
| 81 |
"why",
|
| 82 |
]
|
| 83 |
|
| 84 |
-
CSS = """
|
| 85 |
-
html, body {width: 100%; margin: 0 !important; overflow-x: clip;}
|
| 86 |
-
.gradio-container {
|
| 87 |
-
width: 100% !important;
|
| 88 |
-
max-width: none !important;
|
| 89 |
-
margin: 0 !important;
|
| 90 |
-
padding: 0 0 2rem !important;
|
| 91 |
-
}
|
| 92 |
-
.hero-host {
|
| 93 |
-
width: 100vw !important;
|
| 94 |
-
max-width: none !important;
|
| 95 |
-
margin-inline: calc(50% - 50vw) !important;
|
| 96 |
-
padding: 0 !important;
|
| 97 |
-
border: 0 !important;
|
| 98 |
-
box-sizing: border-box !important;
|
| 99 |
-
}
|
| 100 |
-
.hero {
|
| 101 |
-
position: relative;
|
| 102 |
-
width: 100%;
|
| 103 |
-
margin: 0;
|
| 104 |
-
overflow: hidden;
|
| 105 |
-
box-sizing: border-box;
|
| 106 |
-
|
| 107 |
-
border-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
radial-gradient(circle at
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
.hero
|
| 123 |
-
.hero
|
| 124 |
-
.hero-
|
| 125 |
-
.
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
|
|
|
| 132 |
font-size: 0.78rem;
|
| 133 |
font-weight: 750;
|
| 134 |
letter-spacing: 0.12em;
|
| 135 |
text-transform: uppercase;
|
| 136 |
}
|
| 137 |
-
.hero h1 {
|
| 138 |
-
max-width: 960px;
|
| 139 |
-
margin: 0 auto;
|
| 140 |
-
font-size: clamp(3.4rem,
|
| 141 |
-
line-height: 0.92;
|
| 142 |
-
letter-spacing: -0.065em;
|
| 143 |
-
}
|
| 144 |
-
.hero-expansion {
|
| 145 |
-
max-width: 960px;
|
| 146 |
-
margin: 1rem auto 0;
|
| 147 |
-
font-size: clamp(1.08rem, 2.5vw, 1.55rem);
|
| 148 |
-
font-weight: 720;
|
| 149 |
-
line-height: 1.35;
|
| 150 |
-
}
|
| 151 |
-
.hero-copy {
|
| 152 |
-
max-width: 900px;
|
| 153 |
-
margin: 1rem auto 0;
|
| 154 |
-
font-size: clamp(1rem, 1.5vw, 1.16rem);
|
| 155 |
-
line-height: 1.65;
|
| 156 |
-
opacity: 0.88;
|
| 157 |
-
}
|
| 158 |
-
.speed-grid {
|
| 159 |
-
display: grid;
|
| 160 |
-
grid-template-columns: repeat(3, minmax(0, 1fr));
|
| 161 |
-
gap: 0.75rem;
|
| 162 |
-
max-width: 900px;
|
| 163 |
-
margin: 1.6rem auto 0;
|
| 164 |
-
}
|
| 165 |
-
.speed-stat {
|
| 166 |
-
padding: 0.9rem 1rem;
|
| 167 |
-
border: 1px solid var(--border-color-primary);
|
| 168 |
-
border-radius: 14px;
|
| 169 |
-
background: var(--background-fill-primary);
|
| 170 |
-
text-align: center;
|
| 171 |
-
}
|
| 172 |
-
.
|
| 173 |
-
.speed-stat
|
| 174 |
-
.
|
| 175 |
-
.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 176 |
.trust-pill {
|
| 177 |
padding: 0.36rem 0.68rem;
|
| 178 |
border: 1px solid var(--border-color-primary);
|
|
@@ -181,26 +186,26 @@ html, body {width: 100%; margin: 0 !important; overflow-x: clip;}
|
|
| 181 |
font-size: 0.82rem;
|
| 182 |
font-weight: 650;
|
| 183 |
}
|
| 184 |
-
.value-grid {
|
| 185 |
-
display: grid;
|
| 186 |
-
grid-template-columns: repeat(3, minmax(0, 1fr));
|
| 187 |
-
gap: 0.8rem;
|
| 188 |
-
width: min(calc(100% - 2rem), 1120px);
|
| 189 |
-
max-width: 1120px;
|
| 190 |
-
margin: 1.15rem auto 1.35rem;
|
| 191 |
-
}
|
| 192 |
-
.content-shell {
|
| 193 |
-
width: min(calc(100% - 2rem), 1120px) !important;
|
| 194 |
-
max-width: 1120px !important;
|
| 195 |
-
margin-inline: auto !important;
|
| 196 |
-
align-self: center !important;
|
| 197 |
-
}
|
| 198 |
-
.value-card {
|
| 199 |
padding: 1rem 1.05rem;
|
| 200 |
border: 1px solid var(--border-color-primary);
|
| 201 |
-
border-radius: 14px;
|
| 202 |
-
background: var(--background-fill-primary);
|
| 203 |
-
text-align: center;
|
| 204 |
}
|
| 205 |
.value-card strong {display: block; margin-bottom: 0.25rem; font-size: 0.95rem;}
|
| 206 |
.value-card span {font-size: 0.88rem; line-height: 1.45; opacity: 0.78;}
|
|
@@ -266,107 +271,135 @@ mark.evidence-observed {background: #bfdbfe;}
|
|
| 266 |
font-size: 0.88rem;
|
| 267 |
line-height: 1.5;
|
| 268 |
}
|
| 269 |
-
@media (max-width: 760px) {
|
| 270 |
-
.speed-grid, .value-grid, .result-grid {grid-template-columns: 1fr;}
|
| 271 |
-
.hero-inner {padding: 1.8rem 1.25rem 2rem;}
|
| 272 |
-
.speed-stat {text-align: center;}
|
| 273 |
-
.status-card {min-height: auto;}
|
| 274 |
-
}
|
| 275 |
"""
|
| 276 |
|
| 277 |
ABOUT = """
|
| 278 |
-
##
|
| 279 |
-
|
| 280 |
-
RANGE
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
|
| 311 |
-
500,000-article run** and **395,017 articles/second in a 10,000,000-article run**.
|
| 312 |
-
We measured this with a specially optimized RANGE build on one Intel i7-10750H Windows
|
| 313 |
-
laptop using 10 CPU workers. These are the fastest recorded results for the screening
|
| 314 |
-
step alone. Loading data, running a complete pipeline, and using the hosted demo will
|
| 315 |
-
be slower. Speed varies with hardware, article length, and screening rules.
|
| 316 |
-
|
| 317 |
-
### What it is not
|
| 318 |
-
|
| 319 |
-
This demo does not infer sentiment, forecast returns, recommend a position, or use an
|
| 320 |
-
LLM. A `relevant` result means the article passed the bundled financial and economic
|
| 321 |
-
relevance policy; evidence-path state and supported exposure are reported separately.
|
| 322 |
-
Neither result means the news is bullish, bearish, material, complete, or predictive
|
| 323 |
-
of returns.
|
| 324 |
-
|
| 325 |
-
The hosted demo is intended for short evaluation inputs. The downloadable package APIs
|
| 326 |
-
are the appropriate interface for large datasets.
|
| 327 |
"""
|
| 328 |
|
| 329 |
-
|
| 330 |
-
|
| 331 |
-
|
| 332 |
-
|
| 333 |
-
|
| 334 |
-
|
| 335 |
-
|
| 336 |
-
|
| 337 |
-
|
| 338 |
-
|
| 339 |
-
|
| 340 |
-
|
| 341 |
-
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
|
| 347 |
-
|
| 348 |
-
|
| 349 |
-
|
| 350 |
-
|
| 351 |
-
|
| 352 |
-
|
| 353 |
-
|
| 354 |
-
|
| 355 |
-
|
| 356 |
-
|
| 357 |
-
|
| 358 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 359 |
"""
|
| 360 |
|
| 361 |
INITIAL_STATUS_HTML = """
|
| 362 |
<div class="status-card status-empty" role="status" aria-live="polite">
|
| 363 |
<span class="status-label">Screening result</span>
|
| 364 |
<h3>Your result will appear here</h3>
|
| 365 |
-
<p>RANGE reports article relevance and supported broad-market exposure as two
|
| 366 |
separate outputs.</p>
|
| 367 |
<div class="result-grid">
|
| 368 |
<div class="result-cell"><strong>Article relevance</strong><span>Relevant · No configured match</span></div>
|
| 369 |
-
<div class="result-cell"><strong>Related market</strong><span>Shown only when the article supports a connection</span></div>
|
| 370 |
</div>
|
| 371 |
<p class="boundary-note">This is a relevance screen—not sentiment, materiality, a
|
| 372 |
return forecast, or investment advice.</p>
|
|
@@ -374,90 +407,90 @@ INITIAL_STATUS_HTML = """
|
|
| 374 |
"""
|
| 375 |
|
| 376 |
|
| 377 |
-
def _text_size(value: str, *, label: str) -> tuple[int, int]:
|
| 378 |
-
"""Return character and UTF-8 sizes with a public-safe Unicode error."""
|
| 379 |
-
|
| 380 |
-
characters = len(value)
|
| 381 |
-
try:
|
| 382 |
-
utf8_bytes = len(value.encode("utf-8"))
|
| 383 |
-
except UnicodeEncodeError as exc:
|
| 384 |
-
raise gr.Error(f"{label} contains invalid Unicode text.") from exc
|
| 385 |
-
return characters, utf8_bytes
|
| 386 |
-
|
| 387 |
-
|
| 388 |
-
def _validate_article_size(text: str, *, label: str = "Article") -> tuple[int, int]:
|
| 389 |
-
"""Validate public-request bounds before the engine sees article text."""
|
| 390 |
-
|
| 391 |
-
characters, utf8_bytes = _text_size(text, label=label)
|
| 392 |
-
if characters > MAX_ARTICLE_CHARS:
|
| 393 |
-
raise gr.Error(f"{label} exceeds the {MAX_ARTICLE_CHARS:,}-character limit.")
|
| 394 |
-
if utf8_bytes > MAX_ARTICLE_BYTES:
|
| 395 |
-
raise gr.Error(f"{label} exceeds the {MAX_ARTICLE_BYTES:,}-byte UTF-8 limit.")
|
| 396 |
-
return characters, utf8_bytes
|
| 397 |
-
|
| 398 |
-
|
| 399 |
-
def _bounded_metadata(
|
| 400 |
-
value: Any,
|
| 401 |
-
*,
|
| 402 |
-
label: str,
|
| 403 |
-
max_characters: int,
|
| 404 |
-
max_bytes: int,
|
| 405 |
-
required: bool = False,
|
| 406 |
-
) -> tuple[str, int, int]:
|
| 407 |
-
"""Normalize one metadata value after enforcing its raw request bounds."""
|
| 408 |
-
|
| 409 |
-
if value is None:
|
| 410 |
-
raw_value = ""
|
| 411 |
-
elif isinstance(value, str):
|
| 412 |
-
raw_value = value
|
| 413 |
-
else:
|
| 414 |
-
raise gr.Error(f"{label} must be text.")
|
| 415 |
-
|
| 416 |
-
characters, utf8_bytes = _text_size(raw_value, label=label)
|
| 417 |
-
if characters > max_characters:
|
| 418 |
-
raise gr.Error(f"{label} exceeds the {max_characters:,}-character limit.")
|
| 419 |
-
if utf8_bytes > max_bytes:
|
| 420 |
-
raise gr.Error(f"{label} exceeds the {max_bytes:,}-byte UTF-8 limit.")
|
| 421 |
-
|
| 422 |
-
cleaned = raw_value.strip()
|
| 423 |
-
if required and not cleaned:
|
| 424 |
-
raise gr.Error(f"Enter {label.lower()}.")
|
| 425 |
-
return cleaned, characters, utf8_bytes
|
| 426 |
-
|
| 427 |
-
|
| 428 |
-
def _batch_cell_text(value: Any, *, label: str) -> str:
|
| 429 |
-
"""Accept ordinary table scalars without stringifying nested objects."""
|
| 430 |
-
|
| 431 |
-
if value is None:
|
| 432 |
-
return ""
|
| 433 |
-
if isinstance(value, str):
|
| 434 |
-
return value
|
| 435 |
-
if isinstance(value, (bool, int, float)):
|
| 436 |
-
return str(value)
|
| 437 |
-
raise gr.Error(f"{label} must be plain text or a number.")
|
| 438 |
-
|
| 439 |
-
|
| 440 |
-
def _validate_batch_dimensions(row_count: int, column_count: int) -> None:
|
| 441 |
-
"""Reject oversized or malformed tables before their cells are copied."""
|
| 442 |
-
|
| 443 |
-
if row_count > MAX_BATCH_ROWS:
|
| 444 |
-
raise gr.Error(
|
| 445 |
-
f"The interactive demo is capped at {MAX_BATCH_ROWS} rows per request."
|
| 446 |
-
)
|
| 447 |
-
if column_count != EXPECTED_BATCH_COLUMNS:
|
| 448 |
-
raise gr.Error(
|
| 449 |
-
"Batch input must have exactly four columns: "
|
| 450 |
-
"article_id, timestamp, source, and text."
|
| 451 |
-
)
|
| 452 |
-
|
| 453 |
-
|
| 454 |
-
def _local_error_details_enabled() -> bool:
|
| 455 |
-
"""Allow detailed tracebacks only through an explicit non-Space opt-in."""
|
| 456 |
-
|
| 457 |
-
requested = os.environ.get("RANGE_LOCAL_SHOW_ERROR", "").strip().casefold()
|
| 458 |
-
if requested not in {"1", "true", "yes", "on"}:
|
| 459 |
-
return False
|
| 460 |
-
return not any(os.environ.get(name) for name in ("SPACE_ID", "SPACE_HOST"))
|
| 461 |
|
| 462 |
|
| 463 |
@lru_cache(maxsize=1)
|
|
@@ -495,10 +528,10 @@ def _jsonable(value: Any) -> Any:
|
|
| 495 |
return value
|
| 496 |
|
| 497 |
|
| 498 |
-
def _download_file(payload: Any, *, suffix: str) -> str:
|
| 499 |
-
cache_root = Path(get_upload_folder())
|
| 500 |
-
cache_root.mkdir(parents=True, exist_ok=True)
|
| 501 |
-
path = cache_root / f"relevance-{uuid.uuid4().hex}{suffix}"
|
| 502 |
if suffix == ".jsonl":
|
| 503 |
lines = [json.dumps(row, ensure_ascii=False, sort_keys=True) for row in payload]
|
| 504 |
path.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
|
@@ -695,44 +728,44 @@ def _status_card(prediction) -> str:
|
|
| 695 |
'<div class="result-grid">'
|
| 696 |
'<div class="result-cell"><strong>Article relevance</strong>'
|
| 697 |
f"<span>{html.escape(badge)}</span></div>"
|
| 698 |
-
'<div class="result-cell"><strong>Related market</strong>'
|
| 699 |
f"<span>{html.escape(exposure_value)}</span></div>"
|
| 700 |
"</div>"
|
| 701 |
-
f"<p><strong>Why RANGE returned this:</strong> {html.escape(prediction.why)}</p>"
|
| 702 |
f"{exposure_note}"
|
| 703 |
'<p class="boundary-note">Relevant means the article passed the configured '
|
| 704 |
"screen. It does not mean bullish, bearish, material, or predictive.</p>"
|
| 705 |
-
f'<p class="small-note">RANGE {html.escape(__version__)} · '
|
| 706 |
f"taxonomy {html.escape(_engine().taxonomy.version)} · "
|
| 707 |
f"CPU workers {_engine().workers}</p>"
|
| 708 |
"</div>"
|
| 709 |
)
|
| 710 |
|
| 711 |
|
| 712 |
-
def _audit_payload(prediction) -> dict[str, Any]:
|
| 713 |
-
decision = _jsonable(prediction)
|
| 714 |
-
article = decision.get("article") if isinstance(decision, dict) else None
|
| 715 |
-
if not isinstance(article, dict):
|
| 716 |
-
raise RuntimeError("RANGE produced an invalid evidence record.")
|
| 717 |
-
|
| 718 |
-
article_text = prediction.article.text
|
| 719 |
-
encoded_text = article_text.encode("utf-8")
|
| 720 |
-
article.pop("text", None)
|
| 721 |
-
article.update(
|
| 722 |
-
{
|
| 723 |
-
"text_sha256": hashlib.sha256(encoded_text).hexdigest(),
|
| 724 |
-
"text_characters": len(article_text),
|
| 725 |
-
"text_utf8_bytes": len(encoded_text),
|
| 726 |
-
"text_normalization": "leading_and_trailing_whitespace_removed",
|
| 727 |
-
}
|
| 728 |
-
)
|
| 729 |
-
return {
|
| 730 |
-
"summary": prediction.summary(),
|
| 731 |
-
"package_version": __version__,
|
| 732 |
-
"backend": _jsonable(_engine().backend_info),
|
| 733 |
-
"taxonomy": _engine().taxonomy.describe(),
|
| 734 |
-
"decision": decision,
|
| 735 |
-
}
|
| 736 |
|
| 737 |
|
| 738 |
def analyze_article(
|
|
@@ -744,47 +777,47 @@ def analyze_article(
|
|
| 744 |
"""Screen one timestamped article for configured financial-news relevance.
|
| 745 |
|
| 746 |
Returns a plain-language result, highlighted evidence, an application-ready
|
| 747 |
-
summary, rule path, detailed evidence data, Mermaid source, and a download.
|
| 748 |
"""
|
| 749 |
|
| 750 |
-
if not isinstance(text, str):
|
| 751 |
-
raise gr.Error("Article text must be text.")
|
| 752 |
-
_validate_article_size(text)
|
| 753 |
-
cleaned_text = text.strip()
|
| 754 |
-
if not cleaned_text:
|
| 755 |
-
raise gr.Error("Enter non-empty article text.")
|
| 756 |
-
resolved_timestamp, _, _ = _bounded_metadata(
|
| 757 |
-
timestamp,
|
| 758 |
-
label="Publication timestamp",
|
| 759 |
-
max_characters=MAX_TIMESTAMP_CHARS,
|
| 760 |
-
max_bytes=MAX_TIMESTAMP_BYTES,
|
| 761 |
-
required=True,
|
| 762 |
-
)
|
| 763 |
-
resolved_id, _, _ = _bounded_metadata(
|
| 764 |
-
article_id,
|
| 765 |
-
label="Article ID",
|
| 766 |
-
max_characters=MAX_ARTICLE_ID_CHARS,
|
| 767 |
-
max_bytes=MAX_ARTICLE_ID_BYTES,
|
| 768 |
-
)
|
| 769 |
-
resolved_source, _, _ = _bounded_metadata(
|
| 770 |
-
source,
|
| 771 |
-
label="Source",
|
| 772 |
-
max_characters=MAX_SOURCE_CHARS,
|
| 773 |
-
max_bytes=MAX_SOURCE_BYTES,
|
| 774 |
-
)
|
| 775 |
-
|
| 776 |
-
try:
|
| 777 |
-
prediction = _engine().predict(
|
| 778 |
-
cleaned_text,
|
| 779 |
-
timestamp=resolved_timestamp,
|
| 780 |
-
article_id=resolved_id or None,
|
| 781 |
-
source=resolved_source or "demo",
|
| 782 |
-
)
|
| 783 |
-
except (TypeError, ValueError) as exc:
|
| 784 |
-
raise gr.Error(
|
| 785 |
-
"The article or publication timestamp is invalid. "
|
| 786 |
-
"Use an ISO 8601 timestamp such as 2025-01-15T09:30:00-05:00."
|
| 787 |
-
) from exc
|
| 788 |
|
| 789 |
audit = _audit_payload(prediction)
|
| 790 |
explanation = _engine().explain(prediction, format="text", detail="audit")
|
|
@@ -801,43 +834,43 @@ def analyze_article(
|
|
| 801 |
)
|
| 802 |
|
| 803 |
|
| 804 |
-
def _rows(value: Any) -> list[list[Any]]:
|
| 805 |
-
"""Return a small four-column table after validating its shape up front."""
|
| 806 |
-
|
| 807 |
-
if value is None:
|
| 808 |
-
return []
|
| 809 |
-
|
| 810 |
-
shape = getattr(value, "shape", None)
|
| 811 |
-
if shape is not None:
|
| 812 |
-
try:
|
| 813 |
-
dimensions = tuple(int(part) for part in shape)
|
| 814 |
-
except (TypeError, ValueError) as exc:
|
| 815 |
-
raise gr.Error("Batch input must be a two-dimensional table.") from exc
|
| 816 |
-
if len(dimensions) != 2:
|
| 817 |
-
raise gr.Error("Batch input must be a two-dimensional table.")
|
| 818 |
-
_validate_batch_dimensions(dimensions[0], dimensions[1])
|
| 819 |
-
if hasattr(value, "values") and hasattr(value.values, "tolist"):
|
| 820 |
-
converted = value.values.tolist()
|
| 821 |
-
elif hasattr(value, "tolist"):
|
| 822 |
-
converted = value.tolist()
|
| 823 |
-
else:
|
| 824 |
-
raise gr.Error("Batch input must be a two-dimensional table.")
|
| 825 |
-
if not isinstance(converted, list):
|
| 826 |
-
raise gr.Error("Batch input must be a two-dimensional table.")
|
| 827 |
-
rows: Sequence[Any] = converted
|
| 828 |
-
else:
|
| 829 |
-
if not isinstance(value, Sequence) or isinstance(
|
| 830 |
-
value, (str, bytes, bytearray)
|
| 831 |
-
):
|
| 832 |
-
raise gr.Error("Batch input must be a two-dimensional table.")
|
| 833 |
-
_validate_batch_dimensions(len(value), EXPECTED_BATCH_COLUMNS)
|
| 834 |
-
rows = value
|
| 835 |
-
|
| 836 |
-
for index, row in enumerate(rows):
|
| 837 |
-
if not isinstance(row, Sequence) or isinstance(row, (str, bytes, bytearray)):
|
| 838 |
-
raise gr.Error(f"Batch row {index + 1} must contain four cells.")
|
| 839 |
-
_validate_batch_dimensions(len(rows), len(row))
|
| 840 |
-
return [list(row) for row in rows]
|
| 841 |
|
| 842 |
|
| 843 |
def analyze_batch(data: Any):
|
|
@@ -847,103 +880,103 @@ def analyze_batch(data: Any):
|
|
| 847 |
or Arrow APIs for high-throughput datasets.
|
| 848 |
"""
|
| 849 |
|
| 850 |
-
normalized: list[tuple[str, str, str, str]] = []
|
| 851 |
-
total_text_characters = 0
|
| 852 |
-
total_text_bytes = 0
|
| 853 |
-
total_metadata_characters = 0
|
| 854 |
-
total_metadata_bytes = 0
|
| 855 |
-
for index, row in enumerate(_rows(data)):
|
| 856 |
-
raw_article_id, raw_timestamp, raw_source, raw_text = (
|
| 857 |
-
_batch_cell_text(cell, label=f"Batch row {index + 1}, column {offset + 1}")
|
| 858 |
-
for offset, cell in enumerate(row)
|
| 859 |
-
)
|
| 860 |
-
article_id, id_characters, id_bytes = _bounded_metadata(
|
| 861 |
-
raw_article_id,
|
| 862 |
-
label=f"Batch row {index + 1} article ID",
|
| 863 |
-
max_characters=MAX_ARTICLE_ID_CHARS,
|
| 864 |
-
max_bytes=MAX_ARTICLE_ID_BYTES,
|
| 865 |
-
)
|
| 866 |
-
timestamp, timestamp_characters, timestamp_bytes = _bounded_metadata(
|
| 867 |
-
raw_timestamp,
|
| 868 |
-
label=f"Batch row {index + 1} publication timestamp",
|
| 869 |
-
max_characters=MAX_TIMESTAMP_CHARS,
|
| 870 |
-
max_bytes=MAX_TIMESTAMP_BYTES,
|
| 871 |
-
)
|
| 872 |
-
source, source_characters, source_bytes = _bounded_metadata(
|
| 873 |
-
raw_source,
|
| 874 |
-
label=f"Batch row {index + 1} source",
|
| 875 |
-
max_characters=MAX_SOURCE_CHARS,
|
| 876 |
-
max_bytes=MAX_SOURCE_BYTES,
|
| 877 |
-
)
|
| 878 |
-
text_characters, text_bytes = _validate_article_size(
|
| 879 |
-
raw_text, label=f"Batch row {index + 1} article"
|
| 880 |
-
)
|
| 881 |
-
text = raw_text.strip()
|
| 882 |
-
|
| 883 |
-
metadata_characters = id_characters + timestamp_characters + source_characters
|
| 884 |
-
metadata_bytes = id_bytes + timestamp_bytes + source_bytes
|
| 885 |
-
total_metadata_characters += metadata_characters
|
| 886 |
-
total_metadata_bytes += metadata_bytes
|
| 887 |
-
total_text_characters += text_characters
|
| 888 |
-
total_text_bytes += text_bytes
|
| 889 |
-
total_content_characters = total_metadata_characters + total_text_characters
|
| 890 |
-
total_content_bytes = total_metadata_bytes + total_text_bytes
|
| 891 |
-
|
| 892 |
-
if total_metadata_characters > MAX_BATCH_METADATA_CHARS:
|
| 893 |
-
raise gr.Error(
|
| 894 |
-
"Batch metadata exceeds the "
|
| 895 |
-
f"{MAX_BATCH_METADATA_CHARS:,}-character aggregate limit."
|
| 896 |
-
)
|
| 897 |
-
if total_metadata_bytes > MAX_BATCH_METADATA_BYTES:
|
| 898 |
-
raise gr.Error(
|
| 899 |
-
"Batch metadata exceeds the "
|
| 900 |
-
f"{MAX_BATCH_METADATA_BYTES:,}-byte aggregate UTF-8 limit."
|
| 901 |
-
)
|
| 902 |
-
if total_text_characters > MAX_BATCH_CHARS:
|
| 903 |
-
raise gr.Error(
|
| 904 |
-
f"Batch article text exceeds the {MAX_BATCH_CHARS:,}-character "
|
| 905 |
-
"aggregate limit."
|
| 906 |
-
)
|
| 907 |
-
if total_text_bytes > MAX_BATCH_BYTES:
|
| 908 |
-
raise gr.Error(
|
| 909 |
-
f"Batch article text exceeds the {MAX_BATCH_BYTES:,}-byte "
|
| 910 |
-
"aggregate UTF-8 limit."
|
| 911 |
-
)
|
| 912 |
-
if total_content_characters > MAX_BATCH_CONTENT_CHARS:
|
| 913 |
-
raise gr.Error(
|
| 914 |
-
"Combined batch article and metadata content exceeds the "
|
| 915 |
-
f"{MAX_BATCH_CONTENT_CHARS:,}-character limit."
|
| 916 |
-
)
|
| 917 |
-
if total_content_bytes > MAX_BATCH_CONTENT_BYTES:
|
| 918 |
-
raise gr.Error(
|
| 919 |
-
"Combined batch article and metadata content exceeds the "
|
| 920 |
-
f"{MAX_BATCH_CONTENT_BYTES:,}-byte UTF-8 limit."
|
| 921 |
-
)
|
| 922 |
-
|
| 923 |
-
if not any((article_id, timestamp, source, text)):
|
| 924 |
-
continue
|
| 925 |
-
if not text:
|
| 926 |
-
raise gr.Error(f"Batch row {index + 1} has no article text.")
|
| 927 |
-
if not timestamp:
|
| 928 |
-
raise gr.Error(f"Batch row {index + 1} has no timestamp.")
|
| 929 |
-
normalized.append(
|
| 930 |
-
(article_id or f"row-{index}", timestamp, source or "demo", text)
|
| 931 |
-
)
|
| 932 |
|
| 933 |
if not normalized:
|
| 934 |
raise gr.Error("Add at least one complete batch row.")
|
| 935 |
-
try:
|
| 936 |
predictions = _engine().predict_batch(
|
| 937 |
[row[3] for row in normalized],
|
| 938 |
timestamps=[row[1] for row in normalized],
|
| 939 |
article_ids=[row[0] for row in normalized],
|
| 940 |
sources=[row[2] for row in normalized],
|
| 941 |
)
|
| 942 |
-
except (TypeError, ValueError) as exc:
|
| 943 |
-
raise gr.Error(
|
| 944 |
-
"A batch row is invalid. Check that every publication timestamp is "
|
| 945 |
-
"valid ISO 8601 text."
|
| 946 |
-
) from exc
|
| 947 |
|
| 948 |
summaries = [prediction.summary() for prediction in predictions]
|
| 949 |
table = [
|
|
@@ -972,62 +1005,62 @@ def analyze_batch(data: Any):
|
|
| 972 |
def build_demo() -> gr.Blocks:
|
| 973 |
"""Build the human-facing UI and named Gradio API endpoints."""
|
| 974 |
|
| 975 |
-
blocks_options: dict[str, Any] = {
|
| 976 |
-
"title": "RANGE",
|
| 977 |
-
"fill_width": True,
|
| 978 |
-
"delete_cache": (60, 300),
|
| 979 |
-
}
|
| 980 |
-
if GRADIO_MAJOR < 6:
|
| 981 |
-
blocks_options.update(css=CSS, theme=THEME)
|
| 982 |
-
|
| 983 |
-
def build_content() -> None:
|
| 984 |
-
with gr.Tab("Screen one article"):
|
| 985 |
gr.Markdown(
|
| 986 |
"### Check an article\n"
|
| 987 |
"Paste the text and add its publication time. Time is required so "
|
| 988 |
-
"RANGE can apply time-valid names and relationships; it is not "
|
| 989 |
"used to forecast prices."
|
| 990 |
)
|
| 991 |
with gr.Row():
|
| 992 |
with gr.Column(scale=5):
|
| 993 |
-
article = gr.Textbox(
|
| 994 |
-
label="Article text",
|
| 995 |
-
lines=13,
|
| 996 |
-
value=SINGLE_EXAMPLES[0][0],
|
| 997 |
-
placeholder="Paste a headline, excerpt, or full article...",
|
| 998 |
-
max_length=MAX_ARTICLE_CHARS,
|
| 999 |
-
)
|
| 1000 |
with gr.Row():
|
| 1001 |
timestamp = gr.Textbox(
|
| 1002 |
label="Publication timestamp",
|
| 1003 |
-
value=SINGLE_EXAMPLES[0][1],
|
| 1004 |
-
info="Required · ISO 8601; include an offset when known.",
|
| 1005 |
-
max_length=MAX_TIMESTAMP_CHARS,
|
| 1006 |
)
|
| 1007 |
with gr.Accordion("Optional article metadata", open=False):
|
| 1008 |
gr.Markdown(
|
| 1009 |
-
"Article ID and source make exported records easier to join "
|
| 1010 |
"back to your data. They do not change text matching."
|
| 1011 |
)
|
| 1012 |
with gr.Row():
|
| 1013 |
-
article_id = gr.Textbox(
|
| 1014 |
-
label="Article ID",
|
| 1015 |
-
value=SINGLE_EXAMPLES[0][2],
|
| 1016 |
-
max_length=MAX_ARTICLE_ID_CHARS,
|
| 1017 |
-
)
|
| 1018 |
-
source = gr.Textbox(
|
| 1019 |
-
label="Source",
|
| 1020 |
-
value=SINGLE_EXAMPLES[0][3],
|
| 1021 |
-
max_length=MAX_SOURCE_CHARS,
|
| 1022 |
-
)
|
| 1023 |
analyze = gr.Button("Check relevance", variant="primary")
|
| 1024 |
with gr.Column(scale=4):
|
| 1025 |
status = gr.HTML(INITIAL_STATUS_HTML)
|
| 1026 |
with gr.Accordion("Developer output and download", open=False):
|
| 1027 |
-
summary = gr.JSON(label="Application-ready summary")
|
| 1028 |
-
result_file = gr.File(
|
| 1029 |
-
label="Download evidence record (article text omitted)"
|
| 1030 |
-
)
|
| 1031 |
|
| 1032 |
example_options: dict[str, Any] = {
|
| 1033 |
"examples": SINGLE_EXAMPLES,
|
|
@@ -1054,9 +1087,9 @@ def build_demo() -> gr.Blocks:
|
|
| 1054 |
interactive=False,
|
| 1055 |
label="Evidence and rule path",
|
| 1056 |
)
|
| 1057 |
-
with gr.Accordion("Why RANGE returned this result", open=True):
|
| 1058 |
explanation = gr.Textbox(lines=16, label=None)
|
| 1059 |
-
with gr.Accordion("Evidence record (article text omitted)", open=False):
|
| 1060 |
audit_json = gr.Code(language="json", lines=24, label=None)
|
| 1061 |
with gr.Accordion("Mermaid evidence-flow source (developer)", open=False):
|
| 1062 |
diagram = gr.Code(language="markdown", lines=20, label=None)
|
|
@@ -1074,10 +1107,10 @@ def build_demo() -> gr.Blocks:
|
|
| 1074 |
diagram,
|
| 1075 |
result_file,
|
| 1076 |
],
|
| 1077 |
-
api_name="analyze",
|
| 1078 |
-
scroll_to_output=True,
|
| 1079 |
-
concurrency_limit=1,
|
| 1080 |
-
concurrency_id="range-cpu",
|
| 1081 |
)
|
| 1082 |
|
| 1083 |
with gr.Tab("Batch demo (up to 100)"):
|
|
@@ -1120,11 +1153,11 @@ def build_demo() -> gr.Blocks:
|
|
| 1120 |
screen.click(
|
| 1121 |
fn=analyze_batch,
|
| 1122 |
inputs=batch_input,
|
| 1123 |
-
outputs=[batch_results, batch_metrics, batch_file],
|
| 1124 |
-
api_name="batch",
|
| 1125 |
-
concurrency_limit=1,
|
| 1126 |
-
concurrency_id="range-cpu",
|
| 1127 |
-
)
|
| 1128 |
|
| 1129 |
with gr.Tab("How it works"):
|
| 1130 |
gr.Markdown(ABOUT)
|
|
@@ -1134,39 +1167,42 @@ def build_demo() -> gr.Blocks:
|
|
| 1134 |
"API documentation and `agents.md` instructions for coding agents."
|
| 1135 |
)
|
| 1136 |
|
| 1137 |
-
gr.
|
| 1138 |
-
|
| 1139 |
-
|
| 1140 |
-
|
|
|
|
|
|
|
|
|
|
| 1141 |
"advice or a forecast. In a hosted Space, submitted text and generated "
|
| 1142 |
"downloads are processed temporarily on the Space host. Do not submit "
|
| 1143 |
-
"private, licensed, or confidential text unless you have permission.</div>"
|
| 1144 |
-
)
|
| 1145 |
-
|
| 1146 |
-
with gr.Blocks(**blocks_options) as demo:
|
| 1147 |
-
gr.HTML(HERO_HTML, elem_classes=["hero-host"])
|
| 1148 |
-
with gr.Column(elem_classes=["content-shell"]):
|
| 1149 |
-
build_content()
|
| 1150 |
-
return demo
|
| 1151 |
|
| 1152 |
|
| 1153 |
demo = build_demo()
|
| 1154 |
|
| 1155 |
|
| 1156 |
if __name__ == "__main__":
|
| 1157 |
-
launch_options: dict[str, Any] = {
|
| 1158 |
-
"server_name": os.environ.get("GRADIO_SERVER_NAME", "0.0.0.0"),
|
| 1159 |
-
"server_port": int(os.environ.get("GRADIO_SERVER_PORT", "7860")),
|
| 1160 |
-
"show_error": _local_error_details_enabled(),
|
| 1161 |
-
"app_kwargs": {
|
| 1162 |
-
"middleware": [
|
| 1163 |
-
Middleware(
|
| 1164 |
-
RequestBodyLimitMiddleware,
|
| 1165 |
-
max_body_size=MAX_HTTP_REQUEST_BYTES,
|
| 1166 |
-
)
|
| 1167 |
-
]
|
| 1168 |
-
},
|
| 1169 |
-
}
|
| 1170 |
if GRADIO_MAJOR >= 6:
|
| 1171 |
launch_options.update(css=CSS, theme=THEME)
|
| 1172 |
-
demo.queue(api_open=False, max_size=32).launch(**launch_options)
|
|
|
|
| 1 |
+
"""Space-ready Gradio demo for explainable financial-news relevance screening."""
|
| 2 |
|
| 3 |
from __future__ import annotations
|
| 4 |
|
| 5 |
+
import dataclasses
|
| 6 |
+
import hashlib
|
| 7 |
+
import html
|
| 8 |
+
import json
|
| 9 |
+
import os
|
| 10 |
+
import sys
|
| 11 |
+
import uuid
|
| 12 |
from collections.abc import Mapping, Sequence
|
| 13 |
from datetime import datetime
|
| 14 |
from enum import Enum
|
|
|
|
| 16 |
from pathlib import Path
|
| 17 |
from typing import Any
|
| 18 |
|
| 19 |
+
import gradio as gr
|
| 20 |
+
from starlette.middleware import Middleware
|
| 21 |
+
from starlette.middleware.body_limit import RequestBodyLimitMiddleware
|
| 22 |
+
from gradio.utils import get_upload_folder
|
| 23 |
+
|
| 24 |
+
VENDORED_PACKAGE_ROOT = Path(__file__).resolve().parent / "vendor"
|
| 25 |
+
if VENDORED_PACKAGE_ROOT.is_dir():
|
| 26 |
+
sys.path.insert(0, str(VENDORED_PACKAGE_ROOT))
|
| 27 |
+
|
| 28 |
+
try:
|
| 29 |
+
import spaces
|
| 30 |
except ModuleNotFoundError: # Local installs and the Docker image do not need it.
|
| 31 |
spaces = None
|
| 32 |
|
| 33 |
+
from range_news import RelevanceEngine, __version__
|
| 34 |
|
| 35 |
from examples import ARTICLE_EXAMPLES, BATCH_ROWS, SINGLE_EXAMPLES
|
| 36 |
|
|
|
|
| 46 |
|
| 47 |
GRADIO_MAJOR = int(gr.__version__.split(".", 1)[0])
|
| 48 |
THEME = gr.themes.Soft(primary_hue="teal", neutral_hue="slate")
|
| 49 |
+
MAX_BATCH_ROWS = 100
|
| 50 |
+
EXPECTED_BATCH_COLUMNS = 4
|
| 51 |
+
MAX_ARTICLE_CHARS = 20_000
|
| 52 |
+
MAX_ARTICLE_BYTES = 80_000
|
| 53 |
+
MAX_TIMESTAMP_CHARS = 128
|
| 54 |
+
MAX_TIMESTAMP_BYTES = 512
|
| 55 |
+
MAX_ARTICLE_ID_CHARS = 256
|
| 56 |
+
MAX_ARTICLE_ID_BYTES = 1_024
|
| 57 |
+
MAX_SOURCE_CHARS = 256
|
| 58 |
+
MAX_SOURCE_BYTES = 1_024
|
| 59 |
+
MAX_BATCH_CHARS = 200_000
|
| 60 |
+
MAX_BATCH_BYTES = 800_000
|
| 61 |
+
MAX_BATCH_METADATA_CHARS = 64_000
|
| 62 |
+
MAX_BATCH_METADATA_BYTES = 256_000
|
| 63 |
+
MAX_BATCH_CONTENT_CHARS = MAX_BATCH_CHARS + MAX_BATCH_METADATA_CHARS
|
| 64 |
+
MAX_BATCH_CONTENT_BYTES = MAX_BATCH_BYTES + MAX_BATCH_METADATA_BYTES
|
| 65 |
+
MAX_HTTP_REQUEST_BYTES = 4 * 1024 * 1024
|
| 66 |
EVIDENCE_HEADERS = [
|
| 67 |
"evidence path",
|
| 68 |
"matched text",
|
|
|
|
| 81 |
"why",
|
| 82 |
]
|
| 83 |
|
| 84 |
+
CSS = """
|
| 85 |
+
html, body {width: 100%; margin: 0 !important; overflow-x: clip;}
|
| 86 |
+
.gradio-container {
|
| 87 |
+
width: 100% !important;
|
| 88 |
+
max-width: none !important;
|
| 89 |
+
margin: 0 !important;
|
| 90 |
+
padding: 0 0 2rem !important;
|
| 91 |
+
}
|
| 92 |
+
.hero-host {
|
| 93 |
+
width: 100vw !important;
|
| 94 |
+
max-width: none !important;
|
| 95 |
+
margin-inline: calc(50% - 50vw) !important;
|
| 96 |
+
padding: 0 !important;
|
| 97 |
+
border: 0 !important;
|
| 98 |
+
box-sizing: border-box !important;
|
| 99 |
+
}
|
| 100 |
+
.hero {
|
| 101 |
+
position: relative;
|
| 102 |
+
width: 100%;
|
| 103 |
+
margin: 0;
|
| 104 |
+
overflow: hidden;
|
| 105 |
+
box-sizing: border-box;
|
| 106 |
+
color: #f1f5f9;
|
| 107 |
+
border-block: 1px solid #1e3448;
|
| 108 |
+
border-inline: 0;
|
| 109 |
+
background:
|
| 110 |
+
radial-gradient(circle at 92% 15%, rgba(13, 148, 136, 0.18), transparent 34%),
|
| 111 |
+
radial-gradient(circle at 8% 88%, rgba(37, 99, 235, 0.12), transparent 30%),
|
| 112 |
+
linear-gradient(145deg, #101f30, #123b43);
|
| 113 |
+
box-shadow: 0 18px 48px rgba(15, 23, 42, 0.08);
|
| 114 |
+
}
|
| 115 |
+
.hero-inner {
|
| 116 |
+
width: min(calc(100% - 2rem), 1180px);
|
| 117 |
+
margin-inline: auto;
|
| 118 |
+
padding: clamp(2rem, 4vw, 3rem) clamp(1.25rem, 4vw, 3.2rem);
|
| 119 |
+
box-sizing: border-box;
|
| 120 |
+
text-align: center;
|
| 121 |
+
}
|
| 122 |
+
.hero-eyebrow,
|
| 123 |
+
.hero h1,
|
| 124 |
+
.hero-expansion,
|
| 125 |
+
.hero-copy,
|
| 126 |
+
.benchmark-note {
|
| 127 |
+
width: 100%;
|
| 128 |
+
text-align: center !important;
|
| 129 |
+
}
|
| 130 |
+
.hero-eyebrow {
|
| 131 |
+
margin: 0 0 0.8rem;
|
| 132 |
+
color: #7de8d5;
|
| 133 |
font-size: 0.78rem;
|
| 134 |
font-weight: 750;
|
| 135 |
letter-spacing: 0.12em;
|
| 136 |
text-transform: uppercase;
|
| 137 |
}
|
| 138 |
+
.hero h1 {
|
| 139 |
+
max-width: 960px;
|
| 140 |
+
margin: 0 auto;
|
| 141 |
+
font-size: clamp(3.4rem, 9vw, 6.2rem);
|
| 142 |
+
line-height: 0.92;
|
| 143 |
+
letter-spacing: -0.065em;
|
| 144 |
+
}
|
| 145 |
+
.hero-expansion {
|
| 146 |
+
max-width: 960px;
|
| 147 |
+
margin: 1rem auto 0;
|
| 148 |
+
font-size: clamp(1.08rem, 2.5vw, 1.55rem);
|
| 149 |
+
font-weight: 720;
|
| 150 |
+
line-height: 1.35;
|
| 151 |
+
}
|
| 152 |
+
.hero-copy {
|
| 153 |
+
max-width: 900px;
|
| 154 |
+
margin: 1rem auto 0;
|
| 155 |
+
font-size: clamp(1rem, 1.5vw, 1.16rem);
|
| 156 |
+
line-height: 1.65;
|
| 157 |
+
opacity: 0.88;
|
| 158 |
+
}
|
| 159 |
+
.speed-grid {
|
| 160 |
+
display: grid;
|
| 161 |
+
grid-template-columns: repeat(3, minmax(0, 1fr));
|
| 162 |
+
gap: 0.75rem;
|
| 163 |
+
max-width: 900px;
|
| 164 |
+
margin: 1.6rem auto 0;
|
| 165 |
+
}
|
| 166 |
+
.speed-stat {
|
| 167 |
+
padding: 0.9rem 1rem;
|
| 168 |
+
border: 1px solid var(--border-color-primary);
|
| 169 |
+
border-radius: 14px;
|
| 170 |
+
background: var(--background-fill-primary);
|
| 171 |
+
text-align: center;
|
| 172 |
+
}
|
| 173 |
+
.hero h1, .hero-expansion, .hero-copy, .hero .benchmark-note, .hero .speed-stat span {color: #edf8f7 !important;}
|
| 174 |
+
.hero .speed-stat, .hero .trust-pill {background: rgba(255,255,255,0.06); border-color: rgba(255,255,255,0.2); color: #f1f5f9;}
|
| 175 |
+
.hero .package-pill {background: #a4f4da; color: #12352f; border-color: #a4f4da;}
|
| 176 |
+
.hero .speed-stat strong {color: #a4f4da;}
|
| 177 |
+
.speed-stat strong {display: block; font-size: clamp(1.2rem, 2.6vw, 1.75rem); line-height: 1.15;}
|
| 178 |
+
.speed-stat span {display: block; margin-top: 0.25rem; font-size: 0.8rem; line-height: 1.35; opacity: 0.72;}
|
| 179 |
+
.benchmark-note {max-width: 900px; margin: 0.75rem auto 0; font-size: 0.86rem; line-height: 1.5; opacity: 0.72;}
|
| 180 |
+
.trust-row {display: flex; justify-content: center; flex-wrap: wrap; gap: 0.55rem; margin-top: 1.25rem;}
|
| 181 |
.trust-pill {
|
| 182 |
padding: 0.36rem 0.68rem;
|
| 183 |
border: 1px solid var(--border-color-primary);
|
|
|
|
| 186 |
font-size: 0.82rem;
|
| 187 |
font-weight: 650;
|
| 188 |
}
|
| 189 |
+
.value-grid {
|
| 190 |
+
display: grid;
|
| 191 |
+
grid-template-columns: repeat(3, minmax(0, 1fr));
|
| 192 |
+
gap: 0.8rem;
|
| 193 |
+
width: min(calc(100% - 2rem), 1120px);
|
| 194 |
+
max-width: 1120px;
|
| 195 |
+
margin: 1.15rem auto 1.35rem;
|
| 196 |
+
}
|
| 197 |
+
.content-shell {
|
| 198 |
+
width: min(calc(100% - 2rem), 1120px) !important;
|
| 199 |
+
max-width: 1120px !important;
|
| 200 |
+
margin-inline: auto !important;
|
| 201 |
+
align-self: center !important;
|
| 202 |
+
}
|
| 203 |
+
.value-card {
|
| 204 |
padding: 1rem 1.05rem;
|
| 205 |
border: 1px solid var(--border-color-primary);
|
| 206 |
+
border-radius: 14px;
|
| 207 |
+
background: var(--background-fill-primary);
|
| 208 |
+
text-align: center;
|
| 209 |
}
|
| 210 |
.value-card strong {display: block; margin-bottom: 0.25rem; font-size: 0.95rem;}
|
| 211 |
.value-card span {font-size: 0.88rem; line-height: 1.45; opacity: 0.78;}
|
|
|
|
| 271 |
font-size: 0.88rem;
|
| 272 |
line-height: 1.5;
|
| 273 |
}
|
| 274 |
+
@media (max-width: 760px) {
|
| 275 |
+
.speed-grid, .value-grid, .result-grid {grid-template-columns: 1fr;}
|
| 276 |
+
.hero-inner {padding: 1.8rem 1.25rem 2rem;}
|
| 277 |
+
.speed-stat {text-align: center;}
|
| 278 |
+
.status-card {min-height: auto;}
|
| 279 |
+
}
|
| 280 |
"""
|
| 281 |
|
| 282 |
ABOUT = """
|
| 283 |
+
## Financial news, with the evidence attached
|
| 284 |
+
|
| 285 |
+
RANGE screens timestamped articles for economic and financial relevance, maps
|
| 286 |
+
supported evidence to broad-market exposures, and shows exactly why it made each
|
| 287 |
+
decision. It is a deterministic engine: the same text, timestamp and taxonomy
|
| 288 |
+
produce the same result.
|
| 289 |
+
|
| 290 |
+
### Follow every decision
|
| 291 |
+
1. Normalize the text while preserving its original locations.
|
| 292 |
+
2. Match configured phrases and identifiers.
|
| 293 |
+
3. Apply context checks and names valid on the publication date.
|
| 294 |
+
4. Return relevance, supported market connections and the complete evidence trail.
|
| 295 |
+
|
| 296 |
+
### Built for volume
|
| 297 |
+
The CPU engine uses compiled Rust multi-pattern matching and parallel workers.
|
| 298 |
+
The optional CUDA research engine distributes normalization and matching across
|
| 299 |
+
the GPU, overlaps transfers with work, and preserves exact CPU handling where needed.
|
| 300 |
+
Compact Arrow results avoid creating a large Python object for every article.
|
| 301 |
+
|
| 302 |
+
### Your taxonomy, your rules
|
| 303 |
+
RANGE's bundled taxonomy covers economic and financial news. Taxonomies are
|
| 304 |
+
versioned configuration: add topics, aliases, context rules and relationships
|
| 305 |
+
without retraining a language model. New taxonomies still need correctness and
|
| 306 |
+
performance checks; their speed is not guaranteed by the default benchmark.
|
| 307 |
+
|
| 308 |
+
### What you are trying here
|
| 309 |
+
This live demo runs on CPU and returns rich explanations. Its small batches are
|
| 310 |
+
for exploring decisions. The high-throughput benchmarks measure compact bulk
|
| 311 |
+
screening, including GPU transfers and CPU fallback where applicable.
|
| 312 |
+
|
| 313 |
+
A relevant result means the configured screen accepted the article. It does not
|
| 314 |
+
infer sentiment, predict returns, or recommend a trade. No LLM, API key or model
|
| 315 |
+
download is required for inference.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 316 |
"""
|
| 317 |
|
| 318 |
+
PERFORMANCE = """
|
| 319 |
+
## Measured speed. Clear boundaries.
|
| 320 |
+
|
| 321 |
+
| Implementation | Articles / second | Measurement |
|
| 322 |
+
| --- | ---: | --- |
|
| 323 |
+
| CUDA research record | **1,833,059** | Qualified warm screening of 10 million articles |
|
| 324 |
+
| Maintained CPU implementation | **489,759** | Qualified warm screening of 10 million articles |
|
| 325 |
+
| Newer retained CUDA installation | **958,155** | Separate local installed-package qualification |
|
| 326 |
+
|
| 327 |
+
Measured on an Intel i7-10750H Windows laptop with 10 CPU workers; CUDA used an
|
| 328 |
+
RTX 2070 Max-Q. These are separate experiments, not a same-run CPU/GPU speedup.
|
| 329 |
+
The CUDA peak belongs to an earlier qualified implementation; later sessions
|
| 330 |
+
varied substantially. Do not combine its peak with later percentage gains.
|
| 331 |
+
|
| 332 |
+
Screening timings exclude file loading, imports, initialization and verification.
|
| 333 |
+
CUDA timings include transfers, exact CPU fallback and result assembly.
|
| 334 |
+
The live demo uses a different CPU environment and rich explanations, so these
|
| 335 |
+
figures are not its throughput or a service guarantee.
|
| 336 |
+
|
| 337 |
+
[Read the benchmark record](https://huggingface.co/spaces/Brianferrell787/range-news/blob/main/BENCHMARKS.md)
|
| 338 |
+
|
| 339 |
+
## Python package coming soon
|
| 340 |
+
|
| 341 |
+
We are preparing RANGE for straightforward Python installation, with CPU support
|
| 342 |
+
and an optional CUDA backend. Public package release, clean installation across
|
| 343 |
+
supported platforms and broader GPU compatibility are still ahead.
|
| 344 |
+
The current Space lets you explore the engine today.
|
| 345 |
+
|
| 346 |
+
## What we want to try next
|
| 347 |
+
|
| 348 |
+
- **Finish the release:** clean installs, supported platforms, documentation and
|
| 349 |
+
validation of the exact wheels people will use.
|
| 350 |
+
- **Share work across taxonomies:** build on successful two-taxonomy experiments,
|
| 351 |
+
then extend packaging and memory scheduling for more rule sets.
|
| 352 |
+
- **Choose CPU or CUDA by workload:** measure small batches, large batches and
|
| 353 |
+
routing overhead before introducing automatic selection.
|
| 354 |
+
- **Target remaining matching ideas:** direct StringZilla integration and other
|
| 355 |
+
CUSMART methods remain candidates, not demonstrated improvements.
|
| 356 |
+
- **Train optimizations on separate data:** investigate corpus-trained automata
|
| 357 |
+
and fresh compiler profiles using representative development articles.
|
| 358 |
+
|
| 359 |
+
Recent alternatives did not beat the retained matcher. We keep negative results
|
| 360 |
+
and require exact outputs before considering a speed improvement.
|
| 361 |
+
"""
|
| 362 |
+
|
| 363 |
+
HERO_HTML = """
|
| 364 |
+
<section class="hero">
|
| 365 |
+
<div class="hero-inner">
|
| 366 |
+
<p class="hero-eyebrow">Fast · consistent · shows its work</p>
|
| 367 |
+
<h1>RANGE</h1>
|
| 368 |
+
<p class="hero-expansion">Rapid Analysis of News for the General Economy</p>
|
| 369 |
+
<p class="hero-copy">Find the financial news that matters to your rules.<br>
|
| 370 |
+
Screen articles, trace market connections, and see the exact evidence behind
|
| 371 |
+
every decision.</p>
|
| 372 |
+
<div class="trust-row" aria-label="Product characteristics">
|
| 373 |
+
<span class="trust-pill package-pill">Python package coming soon</span>
|
| 374 |
+
<span class="trust-pill">No LLM required</span>
|
| 375 |
+
<span class="trust-pill">Customizable taxonomies</span>
|
| 376 |
+
</div>
|
| 377 |
+
<div class="speed-grid" aria-label="Recorded bulk screening benchmarks">
|
| 378 |
+
<div class="speed-stat"><strong>1.83M</strong><span>articles/s · CUDA research record</span></div>
|
| 379 |
+
<div class="speed-stat"><strong>489,759</strong><span>articles/s · maintained CPU result</span></div>
|
| 380 |
+
<div class="speed-stat"><strong>Exact evidence</strong><span>Matched text, rules and market connections</span></div>
|
| 381 |
+
</div>
|
| 382 |
+
<p class="benchmark-note">Separate qualified 10-million-article screening runs on one Windows laptop.
|
| 383 |
+
CUDA peak is historical; later installed CUDA measured 958,155 articles/s.
|
| 384 |
+
This live demo runs on CPU. See Performance & roadmap for conditions.</p>
|
| 385 |
+
</div>
|
| 386 |
+
</section>
|
| 387 |
+
<section class="value-grid" aria-label="What RANGE returns">
|
| 388 |
+
<div class="value-card"><strong>Screen the article</strong><span>Find economic and financial topics covered by your configured rules.</span></div>
|
| 389 |
+
<div class="value-card"><strong>Connect the evidence</strong><span>See supported broad-market and index relationships, with time-valid identities.</span></div>
|
| 390 |
+
<div class="value-card"><strong>Inspect the reason</strong><span>Follow each result back to the original words and the rule that accepted them.</span></div>
|
| 391 |
+
</section>
|
| 392 |
"""
|
| 393 |
|
| 394 |
INITIAL_STATUS_HTML = """
|
| 395 |
<div class="status-card status-empty" role="status" aria-live="polite">
|
| 396 |
<span class="status-label">Screening result</span>
|
| 397 |
<h3>Your result will appear here</h3>
|
| 398 |
+
<p>RANGE reports article relevance and supported broad-market exposure as two
|
| 399 |
separate outputs.</p>
|
| 400 |
<div class="result-grid">
|
| 401 |
<div class="result-cell"><strong>Article relevance</strong><span>Relevant · No configured match</span></div>
|
| 402 |
+
<div class="result-cell"><strong>Related market</strong><span>Shown only when the article supports a connection</span></div>
|
| 403 |
</div>
|
| 404 |
<p class="boundary-note">This is a relevance screen—not sentiment, materiality, a
|
| 405 |
return forecast, or investment advice.</p>
|
|
|
|
| 407 |
"""
|
| 408 |
|
| 409 |
|
| 410 |
+
def _text_size(value: str, *, label: str) -> tuple[int, int]:
|
| 411 |
+
"""Return character and UTF-8 sizes with a public-safe Unicode error."""
|
| 412 |
+
|
| 413 |
+
characters = len(value)
|
| 414 |
+
try:
|
| 415 |
+
utf8_bytes = len(value.encode("utf-8"))
|
| 416 |
+
except UnicodeEncodeError as exc:
|
| 417 |
+
raise gr.Error(f"{label} contains invalid Unicode text.") from exc
|
| 418 |
+
return characters, utf8_bytes
|
| 419 |
+
|
| 420 |
+
|
| 421 |
+
def _validate_article_size(text: str, *, label: str = "Article") -> tuple[int, int]:
|
| 422 |
+
"""Validate public-request bounds before the engine sees article text."""
|
| 423 |
+
|
| 424 |
+
characters, utf8_bytes = _text_size(text, label=label)
|
| 425 |
+
if characters > MAX_ARTICLE_CHARS:
|
| 426 |
+
raise gr.Error(f"{label} exceeds the {MAX_ARTICLE_CHARS:,}-character limit.")
|
| 427 |
+
if utf8_bytes > MAX_ARTICLE_BYTES:
|
| 428 |
+
raise gr.Error(f"{label} exceeds the {MAX_ARTICLE_BYTES:,}-byte UTF-8 limit.")
|
| 429 |
+
return characters, utf8_bytes
|
| 430 |
+
|
| 431 |
+
|
| 432 |
+
def _bounded_metadata(
|
| 433 |
+
value: Any,
|
| 434 |
+
*,
|
| 435 |
+
label: str,
|
| 436 |
+
max_characters: int,
|
| 437 |
+
max_bytes: int,
|
| 438 |
+
required: bool = False,
|
| 439 |
+
) -> tuple[str, int, int]:
|
| 440 |
+
"""Normalize one metadata value after enforcing its raw request bounds."""
|
| 441 |
+
|
| 442 |
+
if value is None:
|
| 443 |
+
raw_value = ""
|
| 444 |
+
elif isinstance(value, str):
|
| 445 |
+
raw_value = value
|
| 446 |
+
else:
|
| 447 |
+
raise gr.Error(f"{label} must be text.")
|
| 448 |
+
|
| 449 |
+
characters, utf8_bytes = _text_size(raw_value, label=label)
|
| 450 |
+
if characters > max_characters:
|
| 451 |
+
raise gr.Error(f"{label} exceeds the {max_characters:,}-character limit.")
|
| 452 |
+
if utf8_bytes > max_bytes:
|
| 453 |
+
raise gr.Error(f"{label} exceeds the {max_bytes:,}-byte UTF-8 limit.")
|
| 454 |
+
|
| 455 |
+
cleaned = raw_value.strip()
|
| 456 |
+
if required and not cleaned:
|
| 457 |
+
raise gr.Error(f"Enter {label.lower()}.")
|
| 458 |
+
return cleaned, characters, utf8_bytes
|
| 459 |
+
|
| 460 |
+
|
| 461 |
+
def _batch_cell_text(value: Any, *, label: str) -> str:
|
| 462 |
+
"""Accept ordinary table scalars without stringifying nested objects."""
|
| 463 |
+
|
| 464 |
+
if value is None:
|
| 465 |
+
return ""
|
| 466 |
+
if isinstance(value, str):
|
| 467 |
+
return value
|
| 468 |
+
if isinstance(value, (bool, int, float)):
|
| 469 |
+
return str(value)
|
| 470 |
+
raise gr.Error(f"{label} must be plain text or a number.")
|
| 471 |
+
|
| 472 |
+
|
| 473 |
+
def _validate_batch_dimensions(row_count: int, column_count: int) -> None:
|
| 474 |
+
"""Reject oversized or malformed tables before their cells are copied."""
|
| 475 |
+
|
| 476 |
+
if row_count > MAX_BATCH_ROWS:
|
| 477 |
+
raise gr.Error(
|
| 478 |
+
f"The interactive demo is capped at {MAX_BATCH_ROWS} rows per request."
|
| 479 |
+
)
|
| 480 |
+
if column_count != EXPECTED_BATCH_COLUMNS:
|
| 481 |
+
raise gr.Error(
|
| 482 |
+
"Batch input must have exactly four columns: "
|
| 483 |
+
"article_id, timestamp, source, and text."
|
| 484 |
+
)
|
| 485 |
+
|
| 486 |
+
|
| 487 |
+
def _local_error_details_enabled() -> bool:
|
| 488 |
+
"""Allow detailed tracebacks only through an explicit non-Space opt-in."""
|
| 489 |
+
|
| 490 |
+
requested = os.environ.get("RANGE_LOCAL_SHOW_ERROR", "").strip().casefold()
|
| 491 |
+
if requested not in {"1", "true", "yes", "on"}:
|
| 492 |
+
return False
|
| 493 |
+
return not any(os.environ.get(name) for name in ("SPACE_ID", "SPACE_HOST"))
|
| 494 |
|
| 495 |
|
| 496 |
@lru_cache(maxsize=1)
|
|
|
|
| 528 |
return value
|
| 529 |
|
| 530 |
|
| 531 |
+
def _download_file(payload: Any, *, suffix: str) -> str:
|
| 532 |
+
cache_root = Path(get_upload_folder())
|
| 533 |
+
cache_root.mkdir(parents=True, exist_ok=True)
|
| 534 |
+
path = cache_root / f"relevance-{uuid.uuid4().hex}{suffix}"
|
| 535 |
if suffix == ".jsonl":
|
| 536 |
lines = [json.dumps(row, ensure_ascii=False, sort_keys=True) for row in payload]
|
| 537 |
path.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
|
|
|
| 728 |
'<div class="result-grid">'
|
| 729 |
'<div class="result-cell"><strong>Article relevance</strong>'
|
| 730 |
f"<span>{html.escape(badge)}</span></div>"
|
| 731 |
+
'<div class="result-cell"><strong>Related market</strong>'
|
| 732 |
f"<span>{html.escape(exposure_value)}</span></div>"
|
| 733 |
"</div>"
|
| 734 |
+
f"<p><strong>Why RANGE returned this:</strong> {html.escape(prediction.why)}</p>"
|
| 735 |
f"{exposure_note}"
|
| 736 |
'<p class="boundary-note">Relevant means the article passed the configured '
|
| 737 |
"screen. It does not mean bullish, bearish, material, or predictive.</p>"
|
| 738 |
+
f'<p class="small-note">RANGE {html.escape(__version__)} · '
|
| 739 |
f"taxonomy {html.escape(_engine().taxonomy.version)} · "
|
| 740 |
f"CPU workers {_engine().workers}</p>"
|
| 741 |
"</div>"
|
| 742 |
)
|
| 743 |
|
| 744 |
|
| 745 |
+
def _audit_payload(prediction) -> dict[str, Any]:
|
| 746 |
+
decision = _jsonable(prediction)
|
| 747 |
+
article = decision.get("article") if isinstance(decision, dict) else None
|
| 748 |
+
if not isinstance(article, dict):
|
| 749 |
+
raise RuntimeError("RANGE produced an invalid evidence record.")
|
| 750 |
+
|
| 751 |
+
article_text = prediction.article.text
|
| 752 |
+
encoded_text = article_text.encode("utf-8")
|
| 753 |
+
article.pop("text", None)
|
| 754 |
+
article.update(
|
| 755 |
+
{
|
| 756 |
+
"text_sha256": hashlib.sha256(encoded_text).hexdigest(),
|
| 757 |
+
"text_characters": len(article_text),
|
| 758 |
+
"text_utf8_bytes": len(encoded_text),
|
| 759 |
+
"text_normalization": "leading_and_trailing_whitespace_removed",
|
| 760 |
+
}
|
| 761 |
+
)
|
| 762 |
+
return {
|
| 763 |
+
"summary": prediction.summary(),
|
| 764 |
+
"package_version": __version__,
|
| 765 |
+
"backend": _jsonable(_engine().backend_info),
|
| 766 |
+
"taxonomy": _engine().taxonomy.describe(),
|
| 767 |
+
"decision": decision,
|
| 768 |
+
}
|
| 769 |
|
| 770 |
|
| 771 |
def analyze_article(
|
|
|
|
| 777 |
"""Screen one timestamped article for configured financial-news relevance.
|
| 778 |
|
| 779 |
Returns a plain-language result, highlighted evidence, an application-ready
|
| 780 |
+
summary, rule path, detailed evidence data, Mermaid source, and a download.
|
| 781 |
"""
|
| 782 |
|
| 783 |
+
if not isinstance(text, str):
|
| 784 |
+
raise gr.Error("Article text must be text.")
|
| 785 |
+
_validate_article_size(text)
|
| 786 |
+
cleaned_text = text.strip()
|
| 787 |
+
if not cleaned_text:
|
| 788 |
+
raise gr.Error("Enter non-empty article text.")
|
| 789 |
+
resolved_timestamp, _, _ = _bounded_metadata(
|
| 790 |
+
timestamp,
|
| 791 |
+
label="Publication timestamp",
|
| 792 |
+
max_characters=MAX_TIMESTAMP_CHARS,
|
| 793 |
+
max_bytes=MAX_TIMESTAMP_BYTES,
|
| 794 |
+
required=True,
|
| 795 |
+
)
|
| 796 |
+
resolved_id, _, _ = _bounded_metadata(
|
| 797 |
+
article_id,
|
| 798 |
+
label="Article ID",
|
| 799 |
+
max_characters=MAX_ARTICLE_ID_CHARS,
|
| 800 |
+
max_bytes=MAX_ARTICLE_ID_BYTES,
|
| 801 |
+
)
|
| 802 |
+
resolved_source, _, _ = _bounded_metadata(
|
| 803 |
+
source,
|
| 804 |
+
label="Source",
|
| 805 |
+
max_characters=MAX_SOURCE_CHARS,
|
| 806 |
+
max_bytes=MAX_SOURCE_BYTES,
|
| 807 |
+
)
|
| 808 |
+
|
| 809 |
+
try:
|
| 810 |
+
prediction = _engine().predict(
|
| 811 |
+
cleaned_text,
|
| 812 |
+
timestamp=resolved_timestamp,
|
| 813 |
+
article_id=resolved_id or None,
|
| 814 |
+
source=resolved_source or "demo",
|
| 815 |
+
)
|
| 816 |
+
except (TypeError, ValueError) as exc:
|
| 817 |
+
raise gr.Error(
|
| 818 |
+
"The article or publication timestamp is invalid. "
|
| 819 |
+
"Use an ISO 8601 timestamp such as 2025-01-15T09:30:00-05:00."
|
| 820 |
+
) from exc
|
| 821 |
|
| 822 |
audit = _audit_payload(prediction)
|
| 823 |
explanation = _engine().explain(prediction, format="text", detail="audit")
|
|
|
|
| 834 |
)
|
| 835 |
|
| 836 |
|
| 837 |
+
def _rows(value: Any) -> list[list[Any]]:
|
| 838 |
+
"""Return a small four-column table after validating its shape up front."""
|
| 839 |
+
|
| 840 |
+
if value is None:
|
| 841 |
+
return []
|
| 842 |
+
|
| 843 |
+
shape = getattr(value, "shape", None)
|
| 844 |
+
if shape is not None:
|
| 845 |
+
try:
|
| 846 |
+
dimensions = tuple(int(part) for part in shape)
|
| 847 |
+
except (TypeError, ValueError) as exc:
|
| 848 |
+
raise gr.Error("Batch input must be a two-dimensional table.") from exc
|
| 849 |
+
if len(dimensions) != 2:
|
| 850 |
+
raise gr.Error("Batch input must be a two-dimensional table.")
|
| 851 |
+
_validate_batch_dimensions(dimensions[0], dimensions[1])
|
| 852 |
+
if hasattr(value, "values") and hasattr(value.values, "tolist"):
|
| 853 |
+
converted = value.values.tolist()
|
| 854 |
+
elif hasattr(value, "tolist"):
|
| 855 |
+
converted = value.tolist()
|
| 856 |
+
else:
|
| 857 |
+
raise gr.Error("Batch input must be a two-dimensional table.")
|
| 858 |
+
if not isinstance(converted, list):
|
| 859 |
+
raise gr.Error("Batch input must be a two-dimensional table.")
|
| 860 |
+
rows: Sequence[Any] = converted
|
| 861 |
+
else:
|
| 862 |
+
if not isinstance(value, Sequence) or isinstance(
|
| 863 |
+
value, (str, bytes, bytearray)
|
| 864 |
+
):
|
| 865 |
+
raise gr.Error("Batch input must be a two-dimensional table.")
|
| 866 |
+
_validate_batch_dimensions(len(value), EXPECTED_BATCH_COLUMNS)
|
| 867 |
+
rows = value
|
| 868 |
+
|
| 869 |
+
for index, row in enumerate(rows):
|
| 870 |
+
if not isinstance(row, Sequence) or isinstance(row, (str, bytes, bytearray)):
|
| 871 |
+
raise gr.Error(f"Batch row {index + 1} must contain four cells.")
|
| 872 |
+
_validate_batch_dimensions(len(rows), len(row))
|
| 873 |
+
return [list(row) for row in rows]
|
| 874 |
|
| 875 |
|
| 876 |
def analyze_batch(data: Any):
|
|
|
|
| 880 |
or Arrow APIs for high-throughput datasets.
|
| 881 |
"""
|
| 882 |
|
| 883 |
+
normalized: list[tuple[str, str, str, str]] = []
|
| 884 |
+
total_text_characters = 0
|
| 885 |
+
total_text_bytes = 0
|
| 886 |
+
total_metadata_characters = 0
|
| 887 |
+
total_metadata_bytes = 0
|
| 888 |
+
for index, row in enumerate(_rows(data)):
|
| 889 |
+
raw_article_id, raw_timestamp, raw_source, raw_text = (
|
| 890 |
+
_batch_cell_text(cell, label=f"Batch row {index + 1}, column {offset + 1}")
|
| 891 |
+
for offset, cell in enumerate(row)
|
| 892 |
+
)
|
| 893 |
+
article_id, id_characters, id_bytes = _bounded_metadata(
|
| 894 |
+
raw_article_id,
|
| 895 |
+
label=f"Batch row {index + 1} article ID",
|
| 896 |
+
max_characters=MAX_ARTICLE_ID_CHARS,
|
| 897 |
+
max_bytes=MAX_ARTICLE_ID_BYTES,
|
| 898 |
+
)
|
| 899 |
+
timestamp, timestamp_characters, timestamp_bytes = _bounded_metadata(
|
| 900 |
+
raw_timestamp,
|
| 901 |
+
label=f"Batch row {index + 1} publication timestamp",
|
| 902 |
+
max_characters=MAX_TIMESTAMP_CHARS,
|
| 903 |
+
max_bytes=MAX_TIMESTAMP_BYTES,
|
| 904 |
+
)
|
| 905 |
+
source, source_characters, source_bytes = _bounded_metadata(
|
| 906 |
+
raw_source,
|
| 907 |
+
label=f"Batch row {index + 1} source",
|
| 908 |
+
max_characters=MAX_SOURCE_CHARS,
|
| 909 |
+
max_bytes=MAX_SOURCE_BYTES,
|
| 910 |
+
)
|
| 911 |
+
text_characters, text_bytes = _validate_article_size(
|
| 912 |
+
raw_text, label=f"Batch row {index + 1} article"
|
| 913 |
+
)
|
| 914 |
+
text = raw_text.strip()
|
| 915 |
+
|
| 916 |
+
metadata_characters = id_characters + timestamp_characters + source_characters
|
| 917 |
+
metadata_bytes = id_bytes + timestamp_bytes + source_bytes
|
| 918 |
+
total_metadata_characters += metadata_characters
|
| 919 |
+
total_metadata_bytes += metadata_bytes
|
| 920 |
+
total_text_characters += text_characters
|
| 921 |
+
total_text_bytes += text_bytes
|
| 922 |
+
total_content_characters = total_metadata_characters + total_text_characters
|
| 923 |
+
total_content_bytes = total_metadata_bytes + total_text_bytes
|
| 924 |
+
|
| 925 |
+
if total_metadata_characters > MAX_BATCH_METADATA_CHARS:
|
| 926 |
+
raise gr.Error(
|
| 927 |
+
"Batch metadata exceeds the "
|
| 928 |
+
f"{MAX_BATCH_METADATA_CHARS:,}-character aggregate limit."
|
| 929 |
+
)
|
| 930 |
+
if total_metadata_bytes > MAX_BATCH_METADATA_BYTES:
|
| 931 |
+
raise gr.Error(
|
| 932 |
+
"Batch metadata exceeds the "
|
| 933 |
+
f"{MAX_BATCH_METADATA_BYTES:,}-byte aggregate UTF-8 limit."
|
| 934 |
+
)
|
| 935 |
+
if total_text_characters > MAX_BATCH_CHARS:
|
| 936 |
+
raise gr.Error(
|
| 937 |
+
f"Batch article text exceeds the {MAX_BATCH_CHARS:,}-character "
|
| 938 |
+
"aggregate limit."
|
| 939 |
+
)
|
| 940 |
+
if total_text_bytes > MAX_BATCH_BYTES:
|
| 941 |
+
raise gr.Error(
|
| 942 |
+
f"Batch article text exceeds the {MAX_BATCH_BYTES:,}-byte "
|
| 943 |
+
"aggregate UTF-8 limit."
|
| 944 |
+
)
|
| 945 |
+
if total_content_characters > MAX_BATCH_CONTENT_CHARS:
|
| 946 |
+
raise gr.Error(
|
| 947 |
+
"Combined batch article and metadata content exceeds the "
|
| 948 |
+
f"{MAX_BATCH_CONTENT_CHARS:,}-character limit."
|
| 949 |
+
)
|
| 950 |
+
if total_content_bytes > MAX_BATCH_CONTENT_BYTES:
|
| 951 |
+
raise gr.Error(
|
| 952 |
+
"Combined batch article and metadata content exceeds the "
|
| 953 |
+
f"{MAX_BATCH_CONTENT_BYTES:,}-byte UTF-8 limit."
|
| 954 |
+
)
|
| 955 |
+
|
| 956 |
+
if not any((article_id, timestamp, source, text)):
|
| 957 |
+
continue
|
| 958 |
+
if not text:
|
| 959 |
+
raise gr.Error(f"Batch row {index + 1} has no article text.")
|
| 960 |
+
if not timestamp:
|
| 961 |
+
raise gr.Error(f"Batch row {index + 1} has no timestamp.")
|
| 962 |
+
normalized.append(
|
| 963 |
+
(article_id or f"row-{index}", timestamp, source or "demo", text)
|
| 964 |
+
)
|
| 965 |
|
| 966 |
if not normalized:
|
| 967 |
raise gr.Error("Add at least one complete batch row.")
|
| 968 |
+
try:
|
| 969 |
predictions = _engine().predict_batch(
|
| 970 |
[row[3] for row in normalized],
|
| 971 |
timestamps=[row[1] for row in normalized],
|
| 972 |
article_ids=[row[0] for row in normalized],
|
| 973 |
sources=[row[2] for row in normalized],
|
| 974 |
)
|
| 975 |
+
except (TypeError, ValueError) as exc:
|
| 976 |
+
raise gr.Error(
|
| 977 |
+
"A batch row is invalid. Check that every publication timestamp is "
|
| 978 |
+
"valid ISO 8601 text."
|
| 979 |
+
) from exc
|
| 980 |
|
| 981 |
summaries = [prediction.summary() for prediction in predictions]
|
| 982 |
table = [
|
|
|
|
| 1005 |
def build_demo() -> gr.Blocks:
|
| 1006 |
"""Build the human-facing UI and named Gradio API endpoints."""
|
| 1007 |
|
| 1008 |
+
blocks_options: dict[str, Any] = {
|
| 1009 |
+
"title": "RANGE",
|
| 1010 |
+
"fill_width": True,
|
| 1011 |
+
"delete_cache": (60, 300),
|
| 1012 |
+
}
|
| 1013 |
+
if GRADIO_MAJOR < 6:
|
| 1014 |
+
blocks_options.update(css=CSS, theme=THEME)
|
| 1015 |
+
|
| 1016 |
+
def build_content() -> None:
|
| 1017 |
+
with gr.Tab("Screen one article"):
|
| 1018 |
gr.Markdown(
|
| 1019 |
"### Check an article\n"
|
| 1020 |
"Paste the text and add its publication time. Time is required so "
|
| 1021 |
+
"RANGE can apply time-valid names and relationships; it is not "
|
| 1022 |
"used to forecast prices."
|
| 1023 |
)
|
| 1024 |
with gr.Row():
|
| 1025 |
with gr.Column(scale=5):
|
| 1026 |
+
article = gr.Textbox(
|
| 1027 |
+
label="Article text",
|
| 1028 |
+
lines=13,
|
| 1029 |
+
value=SINGLE_EXAMPLES[0][0],
|
| 1030 |
+
placeholder="Paste a headline, excerpt, or full article...",
|
| 1031 |
+
max_length=MAX_ARTICLE_CHARS,
|
| 1032 |
+
)
|
| 1033 |
with gr.Row():
|
| 1034 |
timestamp = gr.Textbox(
|
| 1035 |
label="Publication timestamp",
|
| 1036 |
+
value=SINGLE_EXAMPLES[0][1],
|
| 1037 |
+
info="Required · ISO 8601; include an offset when known.",
|
| 1038 |
+
max_length=MAX_TIMESTAMP_CHARS,
|
| 1039 |
)
|
| 1040 |
with gr.Accordion("Optional article metadata", open=False):
|
| 1041 |
gr.Markdown(
|
| 1042 |
+
"Article ID and source make exported records easier to join "
|
| 1043 |
"back to your data. They do not change text matching."
|
| 1044 |
)
|
| 1045 |
with gr.Row():
|
| 1046 |
+
article_id = gr.Textbox(
|
| 1047 |
+
label="Article ID",
|
| 1048 |
+
value=SINGLE_EXAMPLES[0][2],
|
| 1049 |
+
max_length=MAX_ARTICLE_ID_CHARS,
|
| 1050 |
+
)
|
| 1051 |
+
source = gr.Textbox(
|
| 1052 |
+
label="Source",
|
| 1053 |
+
value=SINGLE_EXAMPLES[0][3],
|
| 1054 |
+
max_length=MAX_SOURCE_CHARS,
|
| 1055 |
+
)
|
| 1056 |
analyze = gr.Button("Check relevance", variant="primary")
|
| 1057 |
with gr.Column(scale=4):
|
| 1058 |
status = gr.HTML(INITIAL_STATUS_HTML)
|
| 1059 |
with gr.Accordion("Developer output and download", open=False):
|
| 1060 |
+
summary = gr.JSON(label="Application-ready summary")
|
| 1061 |
+
result_file = gr.File(
|
| 1062 |
+
label="Download evidence record (article text omitted)"
|
| 1063 |
+
)
|
| 1064 |
|
| 1065 |
example_options: dict[str, Any] = {
|
| 1066 |
"examples": SINGLE_EXAMPLES,
|
|
|
|
| 1087 |
interactive=False,
|
| 1088 |
label="Evidence and rule path",
|
| 1089 |
)
|
| 1090 |
+
with gr.Accordion("Why RANGE returned this result", open=True):
|
| 1091 |
explanation = gr.Textbox(lines=16, label=None)
|
| 1092 |
+
with gr.Accordion("Evidence record (article text omitted)", open=False):
|
| 1093 |
audit_json = gr.Code(language="json", lines=24, label=None)
|
| 1094 |
with gr.Accordion("Mermaid evidence-flow source (developer)", open=False):
|
| 1095 |
diagram = gr.Code(language="markdown", lines=20, label=None)
|
|
|
|
| 1107 |
diagram,
|
| 1108 |
result_file,
|
| 1109 |
],
|
| 1110 |
+
api_name="analyze",
|
| 1111 |
+
scroll_to_output=True,
|
| 1112 |
+
concurrency_limit=1,
|
| 1113 |
+
concurrency_id="range-cpu",
|
| 1114 |
)
|
| 1115 |
|
| 1116 |
with gr.Tab("Batch demo (up to 100)"):
|
|
|
|
| 1153 |
screen.click(
|
| 1154 |
fn=analyze_batch,
|
| 1155 |
inputs=batch_input,
|
| 1156 |
+
outputs=[batch_results, batch_metrics, batch_file],
|
| 1157 |
+
api_name="batch",
|
| 1158 |
+
concurrency_limit=1,
|
| 1159 |
+
concurrency_id="range-cpu",
|
| 1160 |
+
)
|
| 1161 |
|
| 1162 |
with gr.Tab("How it works"):
|
| 1163 |
gr.Markdown(ABOUT)
|
|
|
|
| 1167 |
"API documentation and `agents.md` instructions for coding agents."
|
| 1168 |
)
|
| 1169 |
|
| 1170 |
+
with gr.Tab("Performance & roadmap"):
|
| 1171 |
+
gr.Markdown(PERFORMANCE)
|
| 1172 |
+
|
| 1173 |
+
gr.Markdown(
|
| 1174 |
+
"---\n"
|
| 1175 |
+
'<div class="privacy-note"><strong>Research alpha · privacy notice.</strong> '
|
| 1176 |
+
"RANGE records configured evidence; it does not provide investment "
|
| 1177 |
"advice or a forecast. In a hosted Space, submitted text and generated "
|
| 1178 |
"downloads are processed temporarily on the Space host. Do not submit "
|
| 1179 |
+
"private, licensed, or confidential text unless you have permission.</div>"
|
| 1180 |
+
)
|
| 1181 |
+
|
| 1182 |
+
with gr.Blocks(**blocks_options) as demo:
|
| 1183 |
+
gr.HTML(HERO_HTML, elem_classes=["hero-host"])
|
| 1184 |
+
with gr.Column(elem_classes=["content-shell"]):
|
| 1185 |
+
build_content()
|
| 1186 |
+
return demo
|
| 1187 |
|
| 1188 |
|
| 1189 |
demo = build_demo()
|
| 1190 |
|
| 1191 |
|
| 1192 |
if __name__ == "__main__":
|
| 1193 |
+
launch_options: dict[str, Any] = {
|
| 1194 |
+
"server_name": os.environ.get("GRADIO_SERVER_NAME", "0.0.0.0"),
|
| 1195 |
+
"server_port": int(os.environ.get("GRADIO_SERVER_PORT", "7860")),
|
| 1196 |
+
"show_error": _local_error_details_enabled(),
|
| 1197 |
+
"app_kwargs": {
|
| 1198 |
+
"middleware": [
|
| 1199 |
+
Middleware(
|
| 1200 |
+
RequestBodyLimitMiddleware,
|
| 1201 |
+
max_body_size=MAX_HTTP_REQUEST_BYTES,
|
| 1202 |
+
)
|
| 1203 |
+
]
|
| 1204 |
+
},
|
| 1205 |
+
}
|
| 1206 |
if GRADIO_MAJOR >= 6:
|
| 1207 |
launch_options.update(css=CSS, theme=THEME)
|
| 1208 |
+
demo.queue(api_open=False, max_size=32).launch(**launch_options)
|
docs/attention2alpha-compatibility-review.md
DELETED
|
@@ -1,46 +0,0 @@
|
|
| 1 |
-
# Attention2Alpha compatibility-change safety review
|
| 2 |
-
|
| 3 |
-
Status: **safe to retain; no revert recommended**
|
| 4 |
-
|
| 5 |
-
Scope: non-package files changed in PR #157 while preparing the standalone RANGE
|
| 6 |
-
package and its validation gates.
|
| 7 |
-
|
| 8 |
-
This review distinguishes narrow compatibility and correctness repairs from changes to
|
| 9 |
-
the trading or learning objective. The reviewed edits do not alter the RANGE
|
| 10 |
-
taxonomy and do not redefine Attention2Alpha reward, order execution, portfolio
|
| 11 |
-
accounting, replay semantics, or the policy-learning objective.
|
| 12 |
-
|
| 13 |
-
## File review
|
| 14 |
-
|
| 15 |
-
| file | change | safety assessment |
|
| 16 |
-
| --- | --- | --- |
|
| 17 |
-
| `attention2alpha/data_split.py` | preserves opaque string split keys instead of coercing them to timestamps | correct for fraction-mode and synthetic indexes; timestamp inputs retain existing handling |
|
| 18 |
-
| `attention2alpha/embeddings/embedding_base.py` | serializes optional configuration objects without importing full optional stacks; makes checkpoint-probe publication failure-safe; clarifies validation errors | reduces import coupling and prevents a stale probe from describing an incomplete artifact |
|
| 19 |
-
| `attention2alpha/embeddings/embedding_flair.py` | supports legacy and lightweight adapters and validates persisted round-trip probes on legacy loads | compatibility hardening; production `nn.Module` behavior remains supported |
|
| 20 |
-
| `attention2alpha/environments/leveraged_etf_portfolio_env.py` | permits validation with W&B disabled while re-raising failures when an active run exists | reporting-path resilience only; no trade or reward calculation changes |
|
| 21 |
-
| `attention2alpha/state_features.py` | uses `min(2, window)` for rolling standard deviation | fixes the invalid `window=1, min_periods=2` case while preserving behavior for windows of two or more |
|
| 22 |
-
| `attention2alpha/train_eval.py` | filters loader keyword arguments against legacy signatures, restores fallback metadata, and reuses the production order-matrix builder for simple adapters | compatibility and best-effort reporting improvements; no training-objective change |
|
| 23 |
-
|
| 24 |
-
## Risk notes
|
| 25 |
-
|
| 26 |
-
The W&B guard catches a broad exception because W&B versions expose different
|
| 27 |
-
uninitialized-run failures. It re-raises when `wandb.run` exists, so failures during an
|
| 28 |
-
active tracked run are not silently hidden. A future cleanup may narrow the exception
|
| 29 |
-
after the supported W&B range is frozen, but the current behavior is safe for this PR.
|
| 30 |
-
|
| 31 |
-
The one-period rolling-volatility change is mathematically well-defined: sample
|
| 32 |
-
standard deviation remains `NaN` until enough observations exist, but Pandas no longer
|
| 33 |
-
rejects an impossible `min_periods` setting.
|
| 34 |
-
|
| 35 |
-
## Evidence
|
| 36 |
-
|
| 37 |
-
The repository deterministic suite exercises checkpoint serialization, round-trip
|
| 38 |
-
validation, train/eval compatibility, state features, and environment guards. The
|
| 39 |
-
package workflow is separate and verifies that the RANGE wheel does not import the
|
| 40 |
-
Attention2Alpha package.
|
| 41 |
-
|
| 42 |
-
## Decision
|
| 43 |
-
|
| 44 |
-
Keep the changes. When RANGE moves to a dedicated repository, copy only the package
|
| 45 |
-
and `app/` sources; these monorepo compatibility repairs remain part of Attention2Alpha
|
| 46 |
-
history rather than becoming dependencies of the standalone package.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
docs/performance.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
# Performance and architecture
|
| 2 |
|
| 3 |
-
RANGE is optimized as an exact information-retrieval system. It is not a language
|
| 4 |
model and it does not use an external inference service.
|
| 5 |
|
| 6 |
## Shipping path
|
|
@@ -80,4 +80,4 @@ The strongest portable claim for `0.1.0a1` is architectural:
|
|
| 80 |
|
| 81 |
> Relevance replaces per-pattern Python work and neural inference with compiled
|
| 82 |
> multi-pattern CPU retrieval, parallel article processing, and sparse or columnar
|
| 83 |
-
> result paths while preserving a traceable evidence contract.
|
|
|
|
| 1 |
# Performance and architecture
|
| 2 |
|
| 3 |
+
RANGE is optimized as an exact information-retrieval system. It is not a language
|
| 4 |
model and it does not use an external inference service.
|
| 5 |
|
| 6 |
## Shipping path
|
|
|
|
| 80 |
|
| 81 |
> Relevance replaces per-pattern Python work and neural inference with compiled
|
| 82 |
> multi-pattern CPU retrieval, parallel article processing, and sparse or columnar
|
| 83 |
+
> result paths while preserving a traceable evidence contract.
|
docs/readme-research.md
CHANGED
|
@@ -30,7 +30,7 @@ was to extract durable communication patterns rather than copy rankings or wordi
|
|
| 30 |
|
| 31 |
| observed pattern | application in Relevance |
|
| 32 |
| --- | --- |
|
| 33 |
-
| one-sentence value proposition | “Fast financial-news screening that shows its work.” |
|
| 34 |
| show a useful result early | the README opens with real quickstart stdout |
|
| 35 |
| put runnable code before exhaustive reference | a complete one-article example appears near the top |
|
| 36 |
| explain performance mechanistically | Rust, automata, Rayon, sparse output, and Arrow are mapped to their practical effects |
|
|
@@ -44,7 +44,7 @@ was to extract durable communication patterns rather than copy rankings or wordi
|
|
| 44 |
## Marketing standard
|
| 45 |
|
| 46 |
The README is intentionally specific rather than superlative. It explains why the
|
| 47 |
-
system can be fast and traceable, but it does not claim to be the fastest system in all
|
| 48 |
settings. It shows the output contract before implementation details and keeps
|
| 49 |
limitations close to the value proposition.
|
| 50 |
|
|
|
|
| 30 |
|
| 31 |
| observed pattern | application in Relevance |
|
| 32 |
| --- | --- |
|
| 33 |
+
| one-sentence value proposition | “Fast financial-news screening that shows its work.” |
|
| 34 |
| show a useful result early | the README opens with real quickstart stdout |
|
| 35 |
| put runnable code before exhaustive reference | a complete one-article example appears near the top |
|
| 36 |
| explain performance mechanistically | Rust, automata, Rayon, sparse output, and Arrow are mapped to their practical effects |
|
|
|
|
| 44 |
## Marketing standard
|
| 45 |
|
| 46 |
The README is intentionally specific rather than superlative. It explains why the
|
| 47 |
+
system can be fast and traceable, but it does not claim to be the fastest system in all
|
| 48 |
settings. It shows the output contract before implementation details and keeps
|
| 49 |
limitations close to the value proposition.
|
| 50 |
|
docs/release.md
CHANGED
|
@@ -1,19 +1,19 @@
|
|
| 1 |
-
# RANGE 0.1.0a1 release-candidate contract
|
| 2 |
|
| 3 |
-
This directory is the authoritative source for the **RANGE** research alpha.
|
| 4 |
|
| 5 |
## Identity
|
| 6 |
|
| 7 |
-
- Product: `RANGE`
|
| 8 |
-
- Distribution: `range-news`
|
| 9 |
-
- Import namespace: `range_news`
|
| 10 |
- Version: `0.1.0a1`
|
| 11 |
- Supported runtime: CPython 3.10
|
| 12 |
- Supported backend: CPU
|
| 13 |
- License: MIT
|
| 14 |
- Publication state: not published
|
| 15 |
|
| 16 |
-
The distribution name is descriptive while the product name remains RANGE.
|
| 17 |
|
| 18 |
## Required artifacts
|
| 19 |
|
|
@@ -30,7 +30,7 @@ A candidate build must produce:
|
|
| 30 |
|
| 31 |
## Required validation
|
| 32 |
|
| 33 |
-
CI is deliberately small and manually invoked. The `range-news-release-candidate`
|
| 34 |
workflow has only two jobs:
|
| 35 |
|
| 36 |
1. **Linux package + Space smoke** validates source syntax, the Space scaffold, the
|
|
@@ -41,13 +41,13 @@ workflow has only two jobs:
|
|
| 41 |
metadata and contents, clean installations, public contracts, PyArrow support, and
|
| 42 |
README output.
|
| 43 |
|
| 44 |
-
The broader
|
| 45 |
gates. They remain available through the separate manual/main-only `repository-tests`
|
| 46 |
workflow.
|
| 47 |
|
| 48 |
## Why the workflow is manual
|
| 49 |
|
| 50 |
-
While RANGE remains inside a larger private monorepo, every feature-branch push can
|
| 51 |
otherwise produce duplicate `push` and `pull_request` runs. Manual invocation keeps the
|
| 52 |
PR readable and conserves hosted-runner minutes while preserving the checks that matter
|
| 53 |
before publication or final technical sign-off.
|
|
@@ -73,7 +73,7 @@ Publishing requires a separate, explicit human decision after:
|
|
| 73 |
|
| 74 |
`0.1.0a1` makes a narrow claim:
|
| 75 |
|
| 76 |
-
> An exact, CPU-first, research-alpha package for traceable financial-news relevance
|
| 77 |
> screening against a versioned general-economy taxonomy.
|
| 78 |
|
| 79 |
It does not claim complete semantic recall, investment usefulness, universal
|
|
|
|
| 1 |
+
# RANGE 0.1.0a1 release-candidate contract
|
| 2 |
|
| 3 |
+
This directory is the authoritative source for the **RANGE** research alpha.
|
| 4 |
|
| 5 |
## Identity
|
| 6 |
|
| 7 |
+
- Product: `RANGE`
|
| 8 |
+
- Distribution: `range-news`
|
| 9 |
+
- Import namespace: `range_news`
|
| 10 |
- Version: `0.1.0a1`
|
| 11 |
- Supported runtime: CPython 3.10
|
| 12 |
- Supported backend: CPU
|
| 13 |
- License: MIT
|
| 14 |
- Publication state: not published
|
| 15 |
|
| 16 |
+
The distribution name is descriptive while the product name remains RANGE.
|
| 17 |
|
| 18 |
## Required artifacts
|
| 19 |
|
|
|
|
| 30 |
|
| 31 |
## Required validation
|
| 32 |
|
| 33 |
+
CI is deliberately small and manually invoked. The `range-news-release-candidate`
|
| 34 |
workflow has only two jobs:
|
| 35 |
|
| 36 |
1. **Linux package + Space smoke** validates source syntax, the Space scaffold, the
|
|
|
|
| 41 |
metadata and contents, clean installations, public contracts, PyArrow support, and
|
| 42 |
README output.
|
| 43 |
|
| 44 |
+
The broader parent research project and historical acceleration suites are not package-release
|
| 45 |
gates. They remain available through the separate manual/main-only `repository-tests`
|
| 46 |
workflow.
|
| 47 |
|
| 48 |
## Why the workflow is manual
|
| 49 |
|
| 50 |
+
While RANGE remains inside a larger private monorepo, every feature-branch push can
|
| 51 |
otherwise produce duplicate `push` and `pull_request` runs. Manual invocation keeps the
|
| 52 |
PR readable and conserves hosted-runner minutes while preserving the checks that matter
|
| 53 |
before publication or final technical sign-off.
|
|
|
|
| 73 |
|
| 74 |
`0.1.0a1` makes a narrow claim:
|
| 75 |
|
| 76 |
+
> An exact, CPU-first, research-alpha package for traceable financial-news relevance
|
| 77 |
> screening against a versioned general-economy taxonomy.
|
| 78 |
|
| 79 |
It does not claim complete semantic recall, investment usefulness, universal
|
docs/taxonomy.md
CHANGED
|
@@ -1,12 +1,12 @@
|
|
| 1 |
# The bundled general-economy taxonomy
|
| 2 |
|
| 3 |
-
The default policy in RANGE is a conservative, offline taxonomy for economic and
|
| 4 |
financial-news screening. It is not a portfolio definition and it is not a sentiment
|
| 5 |
or trading model.
|
| 6 |
|
| 7 |
## Decision layers
|
| 8 |
|
| 9 |
-
Each article is processed through four traceable layers:
|
| 10 |
|
| 11 |
1. **Observation** — literal text spans are matched by effective-dated aliases,
|
| 12 |
normalized phrases, context-qualified acronyms, or conjunction rules.
|
|
@@ -64,7 +64,7 @@ guess a security or broad benchmark when no explicit relationship exists.
|
|
| 64 |
|
| 65 |
## Direction neutrality
|
| 66 |
|
| 67 |
-
RANGE answers whether evidence is connected to a maintained economic or market
|
| 68 |
concept. It does not infer whether the evidence is:
|
| 69 |
|
| 70 |
- bullish or bearish;
|
|
@@ -103,7 +103,7 @@ documented America/New_York convention.
|
|
| 103 |
## Inspect the taxonomy
|
| 104 |
|
| 105 |
```python
|
| 106 |
-
from range_news import GeneralEconomyTaxonomy
|
| 107 |
|
| 108 |
taxonomy = GeneralEconomyTaxonomy()
|
| 109 |
|
|
@@ -117,7 +117,7 @@ print(taxonomy.lexical_rule_ids)
|
|
| 117 |
## Copy and edit the complete bundle
|
| 118 |
|
| 119 |
```python
|
| 120 |
-
from range_news import RelevanceEngine, copy_default_taxonomy
|
| 121 |
|
| 122 |
path = copy_default_taxonomy("my_taxonomy")
|
| 123 |
# Edit my_taxonomy/taxonomy.yml and my_taxonomy/catalogs/*.yml.
|
|
@@ -143,7 +143,7 @@ Unknown fields fail closed. Existing files are not overwritten unless
|
|
| 143 |
new immutable taxonomy objects and never mutate the bundled default.
|
| 144 |
|
| 145 |
```python
|
| 146 |
-
from range_news import GeneralEconomyTaxonomy
|
| 147 |
|
| 148 |
|
| 149 |
def add_indicator(builder):
|
|
|
|
| 1 |
# The bundled general-economy taxonomy
|
| 2 |
|
| 3 |
+
The default policy in RANGE is a conservative, offline taxonomy for economic and
|
| 4 |
financial-news screening. It is not a portfolio definition and it is not a sentiment
|
| 5 |
or trading model.
|
| 6 |
|
| 7 |
## Decision layers
|
| 8 |
|
| 9 |
+
Each article is processed through four traceable layers:
|
| 10 |
|
| 11 |
1. **Observation** — literal text spans are matched by effective-dated aliases,
|
| 12 |
normalized phrases, context-qualified acronyms, or conjunction rules.
|
|
|
|
| 64 |
|
| 65 |
## Direction neutrality
|
| 66 |
|
| 67 |
+
RANGE answers whether evidence is connected to a maintained economic or market
|
| 68 |
concept. It does not infer whether the evidence is:
|
| 69 |
|
| 70 |
- bullish or bearish;
|
|
|
|
| 103 |
## Inspect the taxonomy
|
| 104 |
|
| 105 |
```python
|
| 106 |
+
from range_news import GeneralEconomyTaxonomy
|
| 107 |
|
| 108 |
taxonomy = GeneralEconomyTaxonomy()
|
| 109 |
|
|
|
|
| 117 |
## Copy and edit the complete bundle
|
| 118 |
|
| 119 |
```python
|
| 120 |
+
from range_news import RelevanceEngine, copy_default_taxonomy
|
| 121 |
|
| 122 |
path = copy_default_taxonomy("my_taxonomy")
|
| 123 |
# Edit my_taxonomy/taxonomy.yml and my_taxonomy/catalogs/*.yml.
|
|
|
|
| 143 |
new immutable taxonomy objects and never mutate the bundled default.
|
| 144 |
|
| 145 |
```python
|
| 146 |
+
from range_news import GeneralEconomyTaxonomy
|
| 147 |
|
| 148 |
|
| 149 |
def add_indicator(builder):
|
docs/technical-signoff.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
# RANGE 0.1.0a1 technical sign-off
|
| 2 |
|
| 3 |
This document defines the bounded technical sign-off for the unpublished research
|
| 4 |
alpha. It does not authorize publication, tagging, release creation, Space deployment,
|
|
@@ -6,22 +6,22 @@ or merge.
|
|
| 6 |
|
| 7 |
## Identity
|
| 8 |
|
| 9 |
-
- Product: **RANGE**
|
| 10 |
-
- Distribution: `range-news`
|
| 11 |
-
- Python namespace: `range_news`
|
| 12 |
-
- Native Rust crate: `range_news_native`
|
| 13 |
- Version: `0.1.0a1`
|
| 14 |
- Runtime: CPython 3.10
|
| 15 |
- Shipping backend: exact CPU/Rust
|
| 16 |
|
| 17 |
-
Shipping source, wheels, and source distributions must not contain retired package
|
| 18 |
-
identifiers. The repository URL may retain the parent repository's historical name;
|
| 19 |
-
that is not a package identity.
|
| 20 |
|
| 21 |
## Required current-head evidence
|
| 22 |
|
| 23 |
Technical sign-off intentionally has only two required jobs. Manually run the
|
| 24 |
-
`range-news-release-candidate` workflow on the exact commit being reviewed and require:
|
| 25 |
|
| 26 |
1. **Linux package + Space smoke** — source validation, Rust lock/format checks, wheel
|
| 27 |
and source-distribution construction, metadata and archive inspection, clean
|
|
@@ -32,8 +32,8 @@ Technical sign-off intentionally has only two required jobs. Manually run the
|
|
| 32 |
metadata/archive inspection, clean installs, package contracts, PyArrow coverage,
|
| 33 |
and README output verification.
|
| 34 |
|
| 35 |
-
The full
|
| 36 |
-
RANGE package sign-off gates. They remain available in `repository-tests` for a
|
| 37 |
manual run and execute after integration to `main`.
|
| 38 |
|
| 39 |
A success recorded for an older commit is not sufficient.
|
|
|
|
| 1 |
+
# RANGE 0.1.0a1 technical sign-off
|
| 2 |
|
| 3 |
This document defines the bounded technical sign-off for the unpublished research
|
| 4 |
alpha. It does not authorize publication, tagging, release creation, Space deployment,
|
|
|
|
| 6 |
|
| 7 |
## Identity
|
| 8 |
|
| 9 |
+
- Product: **RANGE**
|
| 10 |
+
- Distribution: `range-news`
|
| 11 |
+
- Python namespace: `range_news`
|
| 12 |
+
- Native Rust crate: `range_news_native`
|
| 13 |
- Version: `0.1.0a1`
|
| 14 |
- Runtime: CPython 3.10
|
| 15 |
- Shipping backend: exact CPU/Rust
|
| 16 |
|
| 17 |
+
Shipping source, wheels, and source distributions must not contain retired package
|
| 18 |
+
identifiers. The repository URL may retain the parent repository's historical name;
|
| 19 |
+
that is not a package identity.
|
| 20 |
|
| 21 |
## Required current-head evidence
|
| 22 |
|
| 23 |
Technical sign-off intentionally has only two required jobs. Manually run the
|
| 24 |
+
`range-news-release-candidate` workflow on the exact commit being reviewed and require:
|
| 25 |
|
| 26 |
1. **Linux package + Space smoke** — source validation, Rust lock/format checks, wheel
|
| 27 |
and source-distribution construction, metadata and archive inspection, clean
|
|
|
|
| 32 |
metadata/archive inspection, clean installs, package contracts, PyArrow coverage,
|
| 33 |
and README output verification.
|
| 34 |
|
| 35 |
+
The full parent research project deterministic suite and historical acceleration suite are not
|
| 36 |
+
RANGE package sign-off gates. They remain available in `repository-tests` for a
|
| 37 |
manual run and execute after integration to `main`.
|
| 38 |
|
| 39 |
A success recorded for an older commit is not sufficient.
|
rust/core.rs
CHANGED
|
The diff for this file is too large to render.
See raw diff
|
|
|
rust/lib.rs
CHANGED
|
@@ -200,7 +200,7 @@ fn facts_to_record_batch(rows: Vec<FactRow>) -> Result<RecordBatch, String> {
|
|
| 200 |
RecordBatch::try_new(Arc::new(Schema::new(fields)), columns).map_err(|error| error.to_string())
|
| 201 |
}
|
| 202 |
|
| 203 |
-
#[pyclass(frozen, module = "range_news._native")]
|
| 204 |
struct NativeArrowCompactFactScanner {
|
| 205 |
inner: NativeCompactFactScanner,
|
| 206 |
}
|
|
@@ -276,13 +276,13 @@ impl NativeArrowCompactFactScanner {
|
|
| 276 |
let batch_rows: Result<Vec<FactRow>, String> = pool.install(|| {
|
| 277 |
(0..batch.num_rows())
|
| 278 |
.into_par_iter()
|
| 279 |
-
.map_init(ScanScratch::default, |scratch, index| {
|
| 280 |
-
let epoch = timestamps.value_microseconds(index)?;
|
| 281 |
-
Ok(scan_one(
|
| 282 |
-
&inner.automaton,
|
| 283 |
-
&inner.pattern_consumers,
|
| 284 |
-
&inner.consumers,
|
| 285 |
-
&inner.exact_specs,
|
| 286 |
&inner.aliases,
|
| 287 |
&inner.lexical,
|
| 288 |
&inner.contexts,
|
|
@@ -309,9 +309,9 @@ impl NativeArrowCompactFactScanner {
|
|
| 309 |
|
| 310 |
let record_batch = facts_to_record_batch(result.map_err(PyValueError::new_err)?)
|
| 311 |
.map_err(PyValueError::new_err)?;
|
| 312 |
-
Ok(PyRecordBatch::new(record_batch))
|
| 313 |
-
}
|
| 314 |
-
}
|
| 315 |
|
| 316 |
#[pymodule]
|
| 317 |
fn _native(module: &Bound<'_, PyModule>) -> PyResult<()> {
|
|
|
|
| 200 |
RecordBatch::try_new(Arc::new(Schema::new(fields)), columns).map_err(|error| error.to_string())
|
| 201 |
}
|
| 202 |
|
| 203 |
+
#[pyclass(frozen, module = "range_news._native")]
|
| 204 |
struct NativeArrowCompactFactScanner {
|
| 205 |
inner: NativeCompactFactScanner,
|
| 206 |
}
|
|
|
|
| 276 |
let batch_rows: Result<Vec<FactRow>, String> = pool.install(|| {
|
| 277 |
(0..batch.num_rows())
|
| 278 |
.into_par_iter()
|
| 279 |
+
.map_init(ScanScratch::default, |scratch, index| {
|
| 280 |
+
let epoch = timestamps.value_microseconds(index)?;
|
| 281 |
+
Ok(scan_one(
|
| 282 |
+
&inner.automaton,
|
| 283 |
+
&inner.pattern_consumers,
|
| 284 |
+
&inner.consumers,
|
| 285 |
+
&inner.exact_specs,
|
| 286 |
&inner.aliases,
|
| 287 |
&inner.lexical,
|
| 288 |
&inner.contexts,
|
|
|
|
| 309 |
|
| 310 |
let record_batch = facts_to_record_batch(result.map_err(PyValueError::new_err)?)
|
| 311 |
.map_err(PyValueError::new_err)?;
|
| 312 |
+
Ok(PyRecordBatch::new(record_batch))
|
| 313 |
+
}
|
| 314 |
+
}
|
| 315 |
|
| 316 |
#[pymodule]
|
| 317 |
fn _native(module: &Bound<'_, PyModule>) -> PyResult<()> {
|
vendor/range_news/_native.cpython-310-x86_64-linux-gnu.so
CHANGED
|
@@ -1,3 +1,3 @@
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:
|
| 3 |
-
size
|
|
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:e05b832a8b33169dd988b4bc88a36bfb1f5b40f70676a09ecda727961d123c9c
|
| 3 |
+
size 15254376
|