acuity-framework 2.0.3__tar.gz → 3.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {acuity_framework-2.0.3/acuity_framework.egg-info → acuity_framework-3.0.0}/PKG-INFO +80 -4
- {acuity_framework-2.0.3 → acuity_framework-3.0.0}/README.md +79 -3
- {acuity_framework-2.0.3 → acuity_framework-3.0.0}/acuity/__init__.py +1 -1
- acuity_framework-3.0.0/acuity/extraction/interfaces.py +51 -0
- {acuity_framework-2.0.3 → acuity_framework-3.0.0}/acuity/extraction/pipeline.py +72 -5
- {acuity_framework-2.0.3 → acuity_framework-3.0.0}/acuity/recommendation/engine.py +23 -4
- acuity_framework-3.0.0/acuity/recommendation/interfaces.py +58 -0
- acuity_framework-3.0.0/acuity/scraper/interfaces.py +58 -0
- {acuity_framework-2.0.3 → acuity_framework-3.0.0/acuity_framework.egg-info}/PKG-INFO +80 -4
- {acuity_framework-2.0.3 → acuity_framework-3.0.0}/acuity_framework.egg-info/SOURCES.txt +3 -0
- {acuity_framework-2.0.3 → acuity_framework-3.0.0}/pyproject.toml +1 -1
- {acuity_framework-2.0.3 → acuity_framework-3.0.0}/LICENSE +0 -0
- {acuity_framework-2.0.3 → acuity_framework-3.0.0}/acuity/config.py +0 -0
- {acuity_framework-2.0.3 → acuity_framework-3.0.0}/acuity/extraction/__init__.py +0 -0
- {acuity_framework-2.0.3 → acuity_framework-3.0.0}/acuity/extraction/ner_crf.py +0 -0
- {acuity_framework-2.0.3 → acuity_framework-3.0.0}/acuity/extraction/ner_transformer.py +0 -0
- {acuity_framework-2.0.3 → acuity_framework-3.0.0}/acuity/extraction/postprocessing.py +0 -0
- {acuity_framework-2.0.3 → acuity_framework-3.0.0}/acuity/extraction/preprocessing.py +0 -0
- {acuity_framework-2.0.3 → acuity_framework-3.0.0}/acuity/extraction/rules.py +0 -0
- {acuity_framework-2.0.3 → acuity_framework-3.0.0}/acuity/recommendation/__init__.py +0 -0
- {acuity_framework-2.0.3 → acuity_framework-3.0.0}/acuity/recommendation/proximity.py +0 -0
- {acuity_framework-2.0.3 → acuity_framework-3.0.0}/acuity/recommendation/ranker.py +0 -0
- {acuity_framework-2.0.3 → acuity_framework-3.0.0}/acuity/recommendation/similarity.py +0 -0
- {acuity_framework-2.0.3 → acuity_framework-3.0.0}/acuity/recommendation/vectorizer.py +0 -0
- {acuity_framework-2.0.3 → acuity_framework-3.0.0}/acuity/scraper/__init__.py +0 -0
- {acuity_framework-2.0.3 → acuity_framework-3.0.0}/acuity/scraper/scraper.py +0 -0
- {acuity_framework-2.0.3 → acuity_framework-3.0.0}/acuity/scraper/utils.py +0 -0
- {acuity_framework-2.0.3 → acuity_framework-3.0.0}/acuity/utils.py +0 -0
- {acuity_framework-2.0.3 → acuity_framework-3.0.0}/acuity/verification/__init__.py +0 -0
- {acuity_framework-2.0.3 → acuity_framework-3.0.0}/acuity/verification/bplo.py +0 -0
- {acuity_framework-2.0.3 → acuity_framework-3.0.0}/acuity_framework.egg-info/dependency_links.txt +0 -0
- {acuity_framework-2.0.3 → acuity_framework-3.0.0}/acuity_framework.egg-info/requires.txt +0 -0
- {acuity_framework-2.0.3 → acuity_framework-3.0.0}/acuity_framework.egg-info/top_level.txt +0 -0
- {acuity_framework-2.0.3 → acuity_framework-3.0.0}/setup.cfg +0 -0
- {acuity_framework-2.0.3 → acuity_framework-3.0.0}/setup.py +0 -0
- {acuity_framework-2.0.3 → acuity_framework-3.0.0}/tests/test_extraction.py +0 -0
- {acuity_framework-2.0.3 → acuity_framework-3.0.0}/tests/test_recommendation.py +0 -0
- {acuity_framework-2.0.3 → acuity_framework-3.0.0}/tests/test_verification.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: acuity-framework
|
|
3
|
-
Version:
|
|
3
|
+
Version: 3.0.0
|
|
4
4
|
Summary: ACUITY — A machine learning framework for extracting, verifying, and recommending local micro-enterprise profiles from unstructured community posts.
|
|
5
5
|
Author: ACUITY Research Team
|
|
6
6
|
License: MIT
|
|
@@ -141,6 +141,75 @@ for r in results:
|
|
|
141
141
|
|
|
142
142
|
---
|
|
143
143
|
|
|
144
|
+
## 🔌 Extensibility (v3.0)
|
|
145
|
+
|
|
146
|
+
ACUITY v3.0 introduces **three pluggable extension points** via abstract base classes. You can inject custom implementations without modifying the framework's source code. All extension points are optional — existing code continues to work unchanged.
|
|
147
|
+
|
|
148
|
+
### Custom NER Backend
|
|
149
|
+
|
|
150
|
+
Replace the built-in CRF/Transformer NER with your own implementation:
|
|
151
|
+
|
|
152
|
+
```python
|
|
153
|
+
from acuity.extraction.interfaces import NERBackend
|
|
154
|
+
from acuity.extraction import ExtractionPipeline
|
|
155
|
+
|
|
156
|
+
class MyNERBackend(NERBackend):
|
|
157
|
+
def extract_entities(self, text: str) -> dict:
|
|
158
|
+
# Your custom entity extraction logic
|
|
159
|
+
return {
|
|
160
|
+
"business_name": ["Detected Name"],
|
|
161
|
+
"categories": ["food"],
|
|
162
|
+
"locations": ["Manila"],
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
# Inject it — existing config-based NER is used when ner_backend=None (default)
|
|
166
|
+
pipeline = ExtractionPipeline(ner_backend=MyNERBackend())
|
|
167
|
+
profiles = pipeline.extract_from_texts(["Sample post text"])
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### Custom Data Source
|
|
171
|
+
|
|
172
|
+
Replace the Facebook scraper with any data source (CSV, database, API, etc.):
|
|
173
|
+
|
|
174
|
+
```python
|
|
175
|
+
from acuity.scraper.interfaces import DataSource
|
|
176
|
+
from acuity.extraction import ExtractionPipeline
|
|
177
|
+
|
|
178
|
+
class MyDataSource(DataSource):
|
|
179
|
+
def fetch_posts(self, sources: list[str], max_posts: int = 500) -> list[dict]:
|
|
180
|
+
# Your custom data fetching logic
|
|
181
|
+
return [{"text": "Post content", "poster": "Author Name"}]
|
|
182
|
+
|
|
183
|
+
# Inject it and use extract_from_source() for fetch + extract in one call
|
|
184
|
+
pipeline = ExtractionPipeline(data_source=MyDataSource())
|
|
185
|
+
profiles = pipeline.extract_from_source(sources=["my_source_id"])
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### Custom Ranking Strategy
|
|
189
|
+
|
|
190
|
+
Replace TF-IDF + cosine similarity with your own text-relevance scoring:
|
|
191
|
+
|
|
192
|
+
```python
|
|
193
|
+
from acuity.recommendation.interfaces import RankingStrategy
|
|
194
|
+
from acuity.recommendation import RecommendationEngine
|
|
195
|
+
|
|
196
|
+
class MyRanking(RankingStrategy):
|
|
197
|
+
def compute_scores(self, profiles: list[dict], query: str) -> list[float]:
|
|
198
|
+
# Your custom relevance scoring logic
|
|
199
|
+
return [1.0 if query.lower() in str(p).lower() else 0.0 for p in profiles]
|
|
200
|
+
|
|
201
|
+
# Inject it — Haversine proximity is still used alongside (it's a fixed formula)
|
|
202
|
+
engine = RecommendationEngine(ranking_strategy=MyRanking())
|
|
203
|
+
engine.set_profiles(profiles)
|
|
204
|
+
results = engine.recommend("bakery")
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
> **Note:** Haversine distance, the pipeline stage order (preprocess → NER → rules → postprocess), and Levenshtein fuzzy matching are intentionally **not** abstracted — they are fixed, correct algorithms with no legitimate variation.
|
|
208
|
+
|
|
209
|
+
See [`examples/demo_extensibility.py`](examples/demo_extensibility.py) for a complete end-to-end demo using all three extension points.
|
|
210
|
+
|
|
211
|
+
---
|
|
212
|
+
|
|
144
213
|
## ⚙️ Configuration
|
|
145
214
|
|
|
146
215
|
All settings are controlled via the `AcuityConfig` dataclass:
|
|
@@ -210,6 +279,7 @@ acuity-framework/
|
|
|
210
279
|
│ ├── utils.py # Levenshtein similarity utilities
|
|
211
280
|
│ ├── extraction/ # NLP extraction pipeline
|
|
212
281
|
│ │ ├── pipeline.py # ExtractionPipeline class
|
|
282
|
+
│ │ ├── interfaces.py # NERBackend ABC (extensibility)
|
|
213
283
|
│ │ ├── preprocessing.py
|
|
214
284
|
│ │ ├── ner_crf.py
|
|
215
285
|
│ │ ├── ner_transformer.py
|
|
@@ -217,19 +287,25 @@ acuity-framework/
|
|
|
217
287
|
│ │ └── postprocessing.py
|
|
218
288
|
│ ├── recommendation/ # Recommendation engine
|
|
219
289
|
│ │ ├── engine.py # RecommendationEngine class
|
|
290
|
+
│ │ ├── interfaces.py # RankingStrategy ABC (extensibility)
|
|
220
291
|
│ │ ├── vectorizer.py # TF-IDF vectorizer
|
|
221
292
|
│ │ ├── similarity.py # Cosine similarity
|
|
222
|
-
│ │ ├── proximity.py # Haversine distance
|
|
293
|
+
│ │ ├── proximity.py # Haversine distance (fixed, not abstracted)
|
|
223
294
|
│ │ └── ranker.py # Combined ranking
|
|
224
295
|
│ ├── verification/ # Business verification
|
|
225
296
|
│ │ └── bplo.py # BPLOVerifier class
|
|
226
|
-
│ └── scraper/ #
|
|
297
|
+
│ └── scraper/ # Data collection (optional)
|
|
227
298
|
│ ├── scraper.py # FacebookScraper class
|
|
299
|
+
│ ├── interfaces.py # DataSource ABC (extensibility)
|
|
228
300
|
│ └── utils.py
|
|
229
301
|
├── examples/
|
|
230
302
|
│ ├── basic_extraction.py
|
|
231
303
|
│ ├── basic_recommendation.py
|
|
232
|
-
│
|
|
304
|
+
│ ├── flask_integration.py
|
|
305
|
+
│ ├── custom_ner_backend.py # Example: KeywordNERBackend
|
|
306
|
+
│ ├── custom_data_source.py # Example: CSVDataSource
|
|
307
|
+
│ ├── custom_ranking_strategy.py # Example: KeywordMatchRanking
|
|
308
|
+
│ └── demo_extensibility.py # Combined end-to-end demo
|
|
233
309
|
└── tests/
|
|
234
310
|
├── test_extraction.py
|
|
235
311
|
├── test_recommendation.py
|
|
@@ -97,6 +97,75 @@ for r in results:
|
|
|
97
97
|
|
|
98
98
|
---
|
|
99
99
|
|
|
100
|
+
## 🔌 Extensibility (v3.0)
|
|
101
|
+
|
|
102
|
+
ACUITY v3.0 introduces **three pluggable extension points** via abstract base classes. You can inject custom implementations without modifying the framework's source code. All extension points are optional — existing code continues to work unchanged.
|
|
103
|
+
|
|
104
|
+
### Custom NER Backend
|
|
105
|
+
|
|
106
|
+
Replace the built-in CRF/Transformer NER with your own implementation:
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
from acuity.extraction.interfaces import NERBackend
|
|
110
|
+
from acuity.extraction import ExtractionPipeline
|
|
111
|
+
|
|
112
|
+
class MyNERBackend(NERBackend):
|
|
113
|
+
def extract_entities(self, text: str) -> dict:
|
|
114
|
+
# Your custom entity extraction logic
|
|
115
|
+
return {
|
|
116
|
+
"business_name": ["Detected Name"],
|
|
117
|
+
"categories": ["food"],
|
|
118
|
+
"locations": ["Manila"],
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
# Inject it — existing config-based NER is used when ner_backend=None (default)
|
|
122
|
+
pipeline = ExtractionPipeline(ner_backend=MyNERBackend())
|
|
123
|
+
profiles = pipeline.extract_from_texts(["Sample post text"])
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Custom Data Source
|
|
127
|
+
|
|
128
|
+
Replace the Facebook scraper with any data source (CSV, database, API, etc.):
|
|
129
|
+
|
|
130
|
+
```python
|
|
131
|
+
from acuity.scraper.interfaces import DataSource
|
|
132
|
+
from acuity.extraction import ExtractionPipeline
|
|
133
|
+
|
|
134
|
+
class MyDataSource(DataSource):
|
|
135
|
+
def fetch_posts(self, sources: list[str], max_posts: int = 500) -> list[dict]:
|
|
136
|
+
# Your custom data fetching logic
|
|
137
|
+
return [{"text": "Post content", "poster": "Author Name"}]
|
|
138
|
+
|
|
139
|
+
# Inject it and use extract_from_source() for fetch + extract in one call
|
|
140
|
+
pipeline = ExtractionPipeline(data_source=MyDataSource())
|
|
141
|
+
profiles = pipeline.extract_from_source(sources=["my_source_id"])
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### Custom Ranking Strategy
|
|
145
|
+
|
|
146
|
+
Replace TF-IDF + cosine similarity with your own text-relevance scoring:
|
|
147
|
+
|
|
148
|
+
```python
|
|
149
|
+
from acuity.recommendation.interfaces import RankingStrategy
|
|
150
|
+
from acuity.recommendation import RecommendationEngine
|
|
151
|
+
|
|
152
|
+
class MyRanking(RankingStrategy):
|
|
153
|
+
def compute_scores(self, profiles: list[dict], query: str) -> list[float]:
|
|
154
|
+
# Your custom relevance scoring logic
|
|
155
|
+
return [1.0 if query.lower() in str(p).lower() else 0.0 for p in profiles]
|
|
156
|
+
|
|
157
|
+
# Inject it — Haversine proximity is still used alongside (it's a fixed formula)
|
|
158
|
+
engine = RecommendationEngine(ranking_strategy=MyRanking())
|
|
159
|
+
engine.set_profiles(profiles)
|
|
160
|
+
results = engine.recommend("bakery")
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
> **Note:** Haversine distance, the pipeline stage order (preprocess → NER → rules → postprocess), and Levenshtein fuzzy matching are intentionally **not** abstracted — they are fixed, correct algorithms with no legitimate variation.
|
|
164
|
+
|
|
165
|
+
See [`examples/demo_extensibility.py`](examples/demo_extensibility.py) for a complete end-to-end demo using all three extension points.
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
100
169
|
## ⚙️ Configuration
|
|
101
170
|
|
|
102
171
|
All settings are controlled via the `AcuityConfig` dataclass:
|
|
@@ -166,6 +235,7 @@ acuity-framework/
|
|
|
166
235
|
│ ├── utils.py # Levenshtein similarity utilities
|
|
167
236
|
│ ├── extraction/ # NLP extraction pipeline
|
|
168
237
|
│ │ ├── pipeline.py # ExtractionPipeline class
|
|
238
|
+
│ │ ├── interfaces.py # NERBackend ABC (extensibility)
|
|
169
239
|
│ │ ├── preprocessing.py
|
|
170
240
|
│ │ ├── ner_crf.py
|
|
171
241
|
│ │ ├── ner_transformer.py
|
|
@@ -173,19 +243,25 @@ acuity-framework/
|
|
|
173
243
|
│ │ └── postprocessing.py
|
|
174
244
|
│ ├── recommendation/ # Recommendation engine
|
|
175
245
|
│ │ ├── engine.py # RecommendationEngine class
|
|
246
|
+
│ │ ├── interfaces.py # RankingStrategy ABC (extensibility)
|
|
176
247
|
│ │ ├── vectorizer.py # TF-IDF vectorizer
|
|
177
248
|
│ │ ├── similarity.py # Cosine similarity
|
|
178
|
-
│ │ ├── proximity.py # Haversine distance
|
|
249
|
+
│ │ ├── proximity.py # Haversine distance (fixed, not abstracted)
|
|
179
250
|
│ │ └── ranker.py # Combined ranking
|
|
180
251
|
│ ├── verification/ # Business verification
|
|
181
252
|
│ │ └── bplo.py # BPLOVerifier class
|
|
182
|
-
│ └── scraper/ #
|
|
253
|
+
│ └── scraper/ # Data collection (optional)
|
|
183
254
|
│ ├── scraper.py # FacebookScraper class
|
|
255
|
+
│ ├── interfaces.py # DataSource ABC (extensibility)
|
|
184
256
|
│ └── utils.py
|
|
185
257
|
├── examples/
|
|
186
258
|
│ ├── basic_extraction.py
|
|
187
259
|
│ ├── basic_recommendation.py
|
|
188
|
-
│
|
|
260
|
+
│ ├── flask_integration.py
|
|
261
|
+
│ ├── custom_ner_backend.py # Example: KeywordNERBackend
|
|
262
|
+
│ ├── custom_data_source.py # Example: CSVDataSource
|
|
263
|
+
│ ├── custom_ranking_strategy.py # Example: KeywordMatchRanking
|
|
264
|
+
│ └── demo_extensibility.py # Combined end-to-end demo
|
|
189
265
|
└── tests/
|
|
190
266
|
├── test_extraction.py
|
|
191
267
|
├── test_recommendation.py
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ACUITY Framework — Extraction Interfaces
|
|
3
|
+
|
|
4
|
+
Abstract base classes defining pluggable extension points for the
|
|
5
|
+
extraction pipeline. Third-party code can implement these interfaces
|
|
6
|
+
to provide custom NER backends without modifying ACUITY's source.
|
|
7
|
+
|
|
8
|
+
Example:
|
|
9
|
+
>>> from acuity.extraction.interfaces import NERBackend
|
|
10
|
+
>>> class MyNER(NERBackend):
|
|
11
|
+
... def extract_entities(self, text):
|
|
12
|
+
... return {"business_name": [], "categories": [], "locations": []}
|
|
13
|
+
"""
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from abc import ABC, abstractmethod
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class NERBackend(ABC):
|
|
20
|
+
"""Abstract interface for Named Entity Recognition backends.
|
|
21
|
+
|
|
22
|
+
Implement this class to provide a custom NER backend that can be
|
|
23
|
+
injected into :class:`~acuity.extraction.pipeline.ExtractionPipeline`
|
|
24
|
+
via its ``ner_backend`` constructor parameter.
|
|
25
|
+
|
|
26
|
+
The backend receives preprocessed text and must return a dictionary
|
|
27
|
+
of extracted entity lists.
|
|
28
|
+
|
|
29
|
+
Example:
|
|
30
|
+
>>> class KeywordNER(NERBackend):
|
|
31
|
+
... def extract_entities(self, text: str) -> dict:
|
|
32
|
+
... names = ["My Shop"] if "shop" in text.lower() else []
|
|
33
|
+
... return {"business_name": names, "categories": [], "locations": []}
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
@abstractmethod
|
|
37
|
+
def extract_entities(self, text: str) -> dict:
|
|
38
|
+
"""Extract named entities from preprocessed text.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
text: Preprocessed post text (already cleaned and normalised).
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
A dictionary with the following keys, each mapping to a
|
|
45
|
+
list of extracted strings:
|
|
46
|
+
|
|
47
|
+
- ``"business_name"``: Extracted business name fragments.
|
|
48
|
+
- ``"categories"``: Extracted service/business categories.
|
|
49
|
+
- ``"locations"``: Extracted location/address fragments.
|
|
50
|
+
"""
|
|
51
|
+
...
|
|
@@ -22,6 +22,8 @@ from typing import Any
|
|
|
22
22
|
from .preprocessing import preprocess
|
|
23
23
|
from .rules import extract_structured_fields
|
|
24
24
|
from .postprocessing import build_business_profile
|
|
25
|
+
from .interfaces import NERBackend
|
|
26
|
+
from ..scraper.interfaces import DataSource
|
|
25
27
|
from ..config import AcuityConfig
|
|
26
28
|
|
|
27
29
|
|
|
@@ -35,13 +37,32 @@ class ExtractionPipeline:
|
|
|
35
37
|
config: An ``AcuityConfig`` instance. If ``None``, uses defaults.
|
|
36
38
|
ner_model: A pre-loaded NER model object. If provided, the pipeline
|
|
37
39
|
will use this model directly instead of loading from ``config.ner_model_path``.
|
|
40
|
+
ner_backend: An optional :class:`~acuity.extraction.interfaces.NERBackend`
|
|
41
|
+
instance. When provided, the pipeline uses this backend for NER
|
|
42
|
+
instead of the built-in CRF/transformer string-branch logic.
|
|
43
|
+
When ``None`` (the default), the existing config-driven backend
|
|
44
|
+
selection is used.
|
|
45
|
+
data_source: An optional :class:`~acuity.scraper.interfaces.DataSource`
|
|
46
|
+
instance. When provided, enables :meth:`extract_from_source` to
|
|
47
|
+
fetch posts from a custom data source before extraction.
|
|
48
|
+
When ``None`` (the default), only :meth:`extract_from_texts`
|
|
49
|
+
is available (existing behavior unchanged).
|
|
38
50
|
"""
|
|
39
51
|
|
|
40
|
-
def __init__(
|
|
52
|
+
def __init__(
|
|
53
|
+
self,
|
|
54
|
+
config: AcuityConfig | None = None,
|
|
55
|
+
ner_model: Any = None,
|
|
56
|
+
ner_backend: NERBackend | None = None,
|
|
57
|
+
data_source: DataSource | None = None,
|
|
58
|
+
):
|
|
41
59
|
self.config = config or AcuityConfig()
|
|
42
60
|
self._ner_model = ner_model
|
|
43
61
|
self._ner_extract_fn = None
|
|
44
|
-
self.
|
|
62
|
+
self._custom_ner_backend = ner_backend
|
|
63
|
+
self._data_source = data_source
|
|
64
|
+
if self._custom_ner_backend is None:
|
|
65
|
+
self._setup_ner()
|
|
45
66
|
|
|
46
67
|
def _setup_ner(self) -> None:
|
|
47
68
|
"""Initialise the NER backend based on configuration."""
|
|
@@ -86,9 +107,14 @@ class ExtractionPipeline:
|
|
|
86
107
|
cleaned = preprocess(text)
|
|
87
108
|
|
|
88
109
|
# Step 2: Named Entity Recognition
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
110
|
+
if self._custom_ner_backend is not None:
|
|
111
|
+
entities = self._custom_ner_backend.extract_entities(cleaned)
|
|
112
|
+
elif self._ner_extract_fn:
|
|
113
|
+
entities = self._ner_extract_fn(cleaned)
|
|
114
|
+
else:
|
|
115
|
+
entities = {
|
|
116
|
+
"business_name": [], "categories": [], "locations": []
|
|
117
|
+
}
|
|
92
118
|
|
|
93
119
|
# Step 3: Rule-based extraction (contacts, hours, address patterns)
|
|
94
120
|
structured = extract_structured_fields(cleaned)
|
|
@@ -169,3 +195,44 @@ class ExtractionPipeline:
|
|
|
169
195
|
profiles = unique_profiles
|
|
170
196
|
|
|
171
197
|
return profiles
|
|
198
|
+
|
|
199
|
+
def extract_from_source(
|
|
200
|
+
self,
|
|
201
|
+
sources: list[str],
|
|
202
|
+
max_posts: int = 500,
|
|
203
|
+
completeness_threshold: int | None = None,
|
|
204
|
+
deduplicate: bool = True,
|
|
205
|
+
) -> list[dict]:
|
|
206
|
+
"""Fetch posts from a :class:`~acuity.scraper.interfaces.DataSource` and extract profiles.
|
|
207
|
+
|
|
208
|
+
This is a convenience method that combines data fetching and extraction
|
|
209
|
+
into a single call. It requires a ``data_source`` to have been provided
|
|
210
|
+
to the pipeline constructor.
|
|
211
|
+
|
|
212
|
+
Args:
|
|
213
|
+
sources: List of source identifiers (meaning depends on the
|
|
214
|
+
:class:`~acuity.scraper.interfaces.DataSource` implementation).
|
|
215
|
+
max_posts: Maximum number of posts to fetch.
|
|
216
|
+
completeness_threshold: Minimum number of populated detail fields
|
|
217
|
+
to keep a profile. Defaults to ``config.completeness_threshold``.
|
|
218
|
+
deduplicate: If True, deduplicate profiles by business name.
|
|
219
|
+
|
|
220
|
+
Returns:
|
|
221
|
+
List of extracted business profile dicts.
|
|
222
|
+
|
|
223
|
+
Raises:
|
|
224
|
+
RuntimeError: If no ``data_source`` was provided to the constructor.
|
|
225
|
+
"""
|
|
226
|
+
if self._data_source is None:
|
|
227
|
+
raise RuntimeError(
|
|
228
|
+
"No data_source was provided to ExtractionPipeline. "
|
|
229
|
+
"Pass a DataSource instance via the data_source parameter, "
|
|
230
|
+
"or use extract_from_texts() directly."
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
posts = self._data_source.fetch_posts(sources, max_posts=max_posts)
|
|
234
|
+
return self.extract_from_texts(
|
|
235
|
+
texts=posts,
|
|
236
|
+
completeness_threshold=completeness_threshold,
|
|
237
|
+
deduplicate=deduplicate,
|
|
238
|
+
)
|
|
@@ -21,6 +21,7 @@ from .vectorizer import build_tfidf_matrix, transform_query
|
|
|
21
21
|
from .similarity import compute_cosine_scores
|
|
22
22
|
from .proximity import haversine_distance
|
|
23
23
|
from .ranker import rank_results
|
|
24
|
+
from .interfaces import RankingStrategy
|
|
24
25
|
from ..config import AcuityConfig
|
|
25
26
|
|
|
26
27
|
|
|
@@ -34,6 +35,13 @@ class RecommendationEngine:
|
|
|
34
35
|
config: An ``AcuityConfig`` instance. If ``None``, uses defaults.
|
|
35
36
|
relevance_weight: Override the config's relevance weight.
|
|
36
37
|
proximity_weight: Override the config's proximity weight.
|
|
38
|
+
ranking_strategy: An optional
|
|
39
|
+
:class:`~acuity.recommendation.interfaces.RankingStrategy` instance.
|
|
40
|
+
When provided, replaces the built-in TF-IDF + cosine similarity
|
|
41
|
+
computation for text relevance scoring. When ``None`` (the default),
|
|
42
|
+
the existing TF-IDF + cosine pipeline is used.
|
|
43
|
+
Note: Haversine proximity scoring is a fixed, correct geographic
|
|
44
|
+
formula and is not affected by this parameter.
|
|
37
45
|
"""
|
|
38
46
|
|
|
39
47
|
def __init__(
|
|
@@ -41,6 +49,7 @@ class RecommendationEngine:
|
|
|
41
49
|
config: AcuityConfig | None = None,
|
|
42
50
|
relevance_weight: float | None = None,
|
|
43
51
|
proximity_weight: float | None = None,
|
|
52
|
+
ranking_strategy: RankingStrategy | None = None,
|
|
44
53
|
):
|
|
45
54
|
self.config = config or AcuityConfig()
|
|
46
55
|
self.profiles: list[dict] = []
|
|
@@ -48,6 +57,7 @@ class RecommendationEngine:
|
|
|
48
57
|
self.proximity_weight = proximity_weight if proximity_weight is not None else self.config.proximity_weight
|
|
49
58
|
self._tfidf_matrix = None
|
|
50
59
|
self._vectorizer = None
|
|
60
|
+
self._ranking_strategy = ranking_strategy
|
|
51
61
|
|
|
52
62
|
def set_profiles(self, profiles: list[dict]) -> None:
|
|
53
63
|
"""Load business profiles from an in-memory list.
|
|
@@ -103,14 +113,23 @@ class RecommendationEngine:
|
|
|
103
113
|
if top_k is None:
|
|
104
114
|
top_k = self.config.default_top_k
|
|
105
115
|
|
|
106
|
-
if not self.profiles
|
|
116
|
+
if not self.profiles:
|
|
107
117
|
return []
|
|
108
118
|
|
|
109
119
|
# Textual relevance
|
|
110
|
-
|
|
111
|
-
|
|
120
|
+
if self._ranking_strategy is not None:
|
|
121
|
+
# Use injected ranking strategy
|
|
122
|
+
cosine_scores = self._ranking_strategy.compute_scores(self.profiles, query)
|
|
123
|
+
elif self._tfidf_matrix is not None and self._vectorizer is not None:
|
|
124
|
+
# Existing TF-IDF + cosine similarity path (unchanged)
|
|
125
|
+
query_vec = transform_query(self._vectorizer, query)
|
|
126
|
+
cosine_scores = compute_cosine_scores(self._tfidf_matrix, query_vec)
|
|
127
|
+
else:
|
|
128
|
+
return []
|
|
112
129
|
|
|
113
|
-
#
|
|
130
|
+
# Haversine proximity (if user location provided)
|
|
131
|
+
# NOTE: Haversine distance is a fixed, correct geographic formula
|
|
132
|
+
# with no legitimate variation — it is intentionally not abstracted.
|
|
114
133
|
distances: list[float | None] = []
|
|
115
134
|
if user_lat is not None and user_lon is not None:
|
|
116
135
|
for profile in self.profiles:
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ACUITY Framework — Recommendation Interfaces
|
|
3
|
+
|
|
4
|
+
Abstract base classes defining pluggable extension points for the
|
|
5
|
+
recommendation engine. Third-party code can implement these interfaces
|
|
6
|
+
to provide custom ranking strategies without modifying ACUITY's source.
|
|
7
|
+
|
|
8
|
+
Example:
|
|
9
|
+
>>> from acuity.recommendation.interfaces import RankingStrategy
|
|
10
|
+
>>> class MyRanking(RankingStrategy):
|
|
11
|
+
... def compute_scores(self, profiles, query):
|
|
12
|
+
... return [1.0] * len(profiles) # everyone is relevant!
|
|
13
|
+
"""
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from abc import ABC, abstractmethod
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class RankingStrategy(ABC):
|
|
20
|
+
"""Abstract interface for text-relevance ranking strategies.
|
|
21
|
+
|
|
22
|
+
Implement this class to provide a custom ranking algorithm that can
|
|
23
|
+
be injected into :class:`~acuity.recommendation.engine.RecommendationEngine`
|
|
24
|
+
via its ``ranking_strategy`` constructor parameter.
|
|
25
|
+
|
|
26
|
+
The strategy computes a relevance score for each business profile given
|
|
27
|
+
a user query. These scores are then combined with proximity scores
|
|
28
|
+
(Haversine distance — which is a fixed formula and not abstracted)
|
|
29
|
+
to produce the final ranking.
|
|
30
|
+
|
|
31
|
+
Example:
|
|
32
|
+
>>> class BM25Ranking(RankingStrategy):
|
|
33
|
+
... def compute_scores(self, profiles, query):
|
|
34
|
+
... # Implement BM25 scoring
|
|
35
|
+
... return scores
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
@abstractmethod
|
|
39
|
+
def compute_scores(
|
|
40
|
+
self,
|
|
41
|
+
profiles: list[dict],
|
|
42
|
+
query: str,
|
|
43
|
+
) -> list[float]:
|
|
44
|
+
"""Compute relevance scores for each profile against a query.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
profiles: List of business profile dictionaries. Each dict
|
|
48
|
+
may contain keys such as ``"name"``, ``"business_name"``,
|
|
49
|
+
``"description"``, ``"categories"``, and ``"services"``.
|
|
50
|
+
query: The user's search query text.
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
A list of float scores (higher = more relevant), one per
|
|
54
|
+
profile, in the same order as the input list. Scores should
|
|
55
|
+
typically be in the range [0, 1] but this is not strictly
|
|
56
|
+
required.
|
|
57
|
+
"""
|
|
58
|
+
...
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ACUITY Framework — Scraper Interfaces
|
|
3
|
+
|
|
4
|
+
Abstract base classes defining pluggable extension points for data
|
|
5
|
+
collection. Third-party code can implement these interfaces to provide
|
|
6
|
+
custom data sources without modifying ACUITY's source.
|
|
7
|
+
|
|
8
|
+
Example:
|
|
9
|
+
>>> from acuity.scraper.interfaces import DataSource
|
|
10
|
+
>>> class MySource(DataSource):
|
|
11
|
+
... def fetch_posts(self, sources, max_posts=500):
|
|
12
|
+
... return [{"text": "Sample post", "poster": "User"}]
|
|
13
|
+
"""
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from abc import ABC, abstractmethod
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class DataSource(ABC):
|
|
20
|
+
"""Abstract interface for data sources that provide post text.
|
|
21
|
+
|
|
22
|
+
Implement this class to provide a custom data source (e.g., CSV file,
|
|
23
|
+
database, API) that can be injected into
|
|
24
|
+
:class:`~acuity.extraction.pipeline.ExtractionPipeline` via its
|
|
25
|
+
``data_source`` constructor parameter.
|
|
26
|
+
|
|
27
|
+
The data source receives a list of source identifiers (URLs, file paths,
|
|
28
|
+
etc.) and returns a list of post dictionaries.
|
|
29
|
+
|
|
30
|
+
Example:
|
|
31
|
+
>>> class DatabaseSource(DataSource):
|
|
32
|
+
... def fetch_posts(self, sources, max_posts=500):
|
|
33
|
+
... # Query database for posts
|
|
34
|
+
... return [{"text": "post text", "poster": "author"}]
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
@abstractmethod
|
|
38
|
+
def fetch_posts(
|
|
39
|
+
self,
|
|
40
|
+
sources: list[str],
|
|
41
|
+
max_posts: int = 500,
|
|
42
|
+
) -> list[dict]:
|
|
43
|
+
"""Fetch posts from the data source.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
sources: List of source identifiers. The meaning depends on the
|
|
47
|
+
implementation (e.g., URLs, file paths, database table names).
|
|
48
|
+
max_posts: Maximum number of posts to return.
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
A list of dictionaries, each with at least a ``"text"`` key
|
|
52
|
+
containing the post content. Optional keys include:
|
|
53
|
+
|
|
54
|
+
- ``"poster"``: Name of the post author.
|
|
55
|
+
- ``"scraped_at"``: Timestamp or date string of when the post
|
|
56
|
+
was collected.
|
|
57
|
+
"""
|
|
58
|
+
...
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: acuity-framework
|
|
3
|
-
Version:
|
|
3
|
+
Version: 3.0.0
|
|
4
4
|
Summary: ACUITY — A machine learning framework for extracting, verifying, and recommending local micro-enterprise profiles from unstructured community posts.
|
|
5
5
|
Author: ACUITY Research Team
|
|
6
6
|
License: MIT
|
|
@@ -141,6 +141,75 @@ for r in results:
|
|
|
141
141
|
|
|
142
142
|
---
|
|
143
143
|
|
|
144
|
+
## 🔌 Extensibility (v3.0)
|
|
145
|
+
|
|
146
|
+
ACUITY v3.0 introduces **three pluggable extension points** via abstract base classes. You can inject custom implementations without modifying the framework's source code. All extension points are optional — existing code continues to work unchanged.
|
|
147
|
+
|
|
148
|
+
### Custom NER Backend
|
|
149
|
+
|
|
150
|
+
Replace the built-in CRF/Transformer NER with your own implementation:
|
|
151
|
+
|
|
152
|
+
```python
|
|
153
|
+
from acuity.extraction.interfaces import NERBackend
|
|
154
|
+
from acuity.extraction import ExtractionPipeline
|
|
155
|
+
|
|
156
|
+
class MyNERBackend(NERBackend):
|
|
157
|
+
def extract_entities(self, text: str) -> dict:
|
|
158
|
+
# Your custom entity extraction logic
|
|
159
|
+
return {
|
|
160
|
+
"business_name": ["Detected Name"],
|
|
161
|
+
"categories": ["food"],
|
|
162
|
+
"locations": ["Manila"],
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
# Inject it — existing config-based NER is used when ner_backend=None (default)
|
|
166
|
+
pipeline = ExtractionPipeline(ner_backend=MyNERBackend())
|
|
167
|
+
profiles = pipeline.extract_from_texts(["Sample post text"])
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### Custom Data Source
|
|
171
|
+
|
|
172
|
+
Replace the Facebook scraper with any data source (CSV, database, API, etc.):
|
|
173
|
+
|
|
174
|
+
```python
|
|
175
|
+
from acuity.scraper.interfaces import DataSource
|
|
176
|
+
from acuity.extraction import ExtractionPipeline
|
|
177
|
+
|
|
178
|
+
class MyDataSource(DataSource):
|
|
179
|
+
def fetch_posts(self, sources: list[str], max_posts: int = 500) -> list[dict]:
|
|
180
|
+
# Your custom data fetching logic
|
|
181
|
+
return [{"text": "Post content", "poster": "Author Name"}]
|
|
182
|
+
|
|
183
|
+
# Inject it and use extract_from_source() for fetch + extract in one call
|
|
184
|
+
pipeline = ExtractionPipeline(data_source=MyDataSource())
|
|
185
|
+
profiles = pipeline.extract_from_source(sources=["my_source_id"])
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### Custom Ranking Strategy
|
|
189
|
+
|
|
190
|
+
Replace TF-IDF + cosine similarity with your own text-relevance scoring:
|
|
191
|
+
|
|
192
|
+
```python
|
|
193
|
+
from acuity.recommendation.interfaces import RankingStrategy
|
|
194
|
+
from acuity.recommendation import RecommendationEngine
|
|
195
|
+
|
|
196
|
+
class MyRanking(RankingStrategy):
|
|
197
|
+
def compute_scores(self, profiles: list[dict], query: str) -> list[float]:
|
|
198
|
+
# Your custom relevance scoring logic
|
|
199
|
+
return [1.0 if query.lower() in str(p).lower() else 0.0 for p in profiles]
|
|
200
|
+
|
|
201
|
+
# Inject it — Haversine proximity is still used alongside (it's a fixed formula)
|
|
202
|
+
engine = RecommendationEngine(ranking_strategy=MyRanking())
|
|
203
|
+
engine.set_profiles(profiles)
|
|
204
|
+
results = engine.recommend("bakery")
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
> **Note:** Haversine distance, the pipeline stage order (preprocess → NER → rules → postprocess), and Levenshtein fuzzy matching are intentionally **not** abstracted — they are fixed, correct algorithms with no legitimate variation.
|
|
208
|
+
|
|
209
|
+
See [`examples/demo_extensibility.py`](examples/demo_extensibility.py) for a complete end-to-end demo using all three extension points.
|
|
210
|
+
|
|
211
|
+
---
|
|
212
|
+
|
|
144
213
|
## ⚙️ Configuration
|
|
145
214
|
|
|
146
215
|
All settings are controlled via the `AcuityConfig` dataclass:
|
|
@@ -210,6 +279,7 @@ acuity-framework/
|
|
|
210
279
|
│ ├── utils.py # Levenshtein similarity utilities
|
|
211
280
|
│ ├── extraction/ # NLP extraction pipeline
|
|
212
281
|
│ │ ├── pipeline.py # ExtractionPipeline class
|
|
282
|
+
│ │ ├── interfaces.py # NERBackend ABC (extensibility)
|
|
213
283
|
│ │ ├── preprocessing.py
|
|
214
284
|
│ │ ├── ner_crf.py
|
|
215
285
|
│ │ ├── ner_transformer.py
|
|
@@ -217,19 +287,25 @@ acuity-framework/
|
|
|
217
287
|
│ │ └── postprocessing.py
|
|
218
288
|
│ ├── recommendation/ # Recommendation engine
|
|
219
289
|
│ │ ├── engine.py # RecommendationEngine class
|
|
290
|
+
│ │ ├── interfaces.py # RankingStrategy ABC (extensibility)
|
|
220
291
|
│ │ ├── vectorizer.py # TF-IDF vectorizer
|
|
221
292
|
│ │ ├── similarity.py # Cosine similarity
|
|
222
|
-
│ │ ├── proximity.py # Haversine distance
|
|
293
|
+
│ │ ├── proximity.py # Haversine distance (fixed, not abstracted)
|
|
223
294
|
│ │ └── ranker.py # Combined ranking
|
|
224
295
|
│ ├── verification/ # Business verification
|
|
225
296
|
│ │ └── bplo.py # BPLOVerifier class
|
|
226
|
-
│ └── scraper/ #
|
|
297
|
+
│ └── scraper/ # Data collection (optional)
|
|
227
298
|
│ ├── scraper.py # FacebookScraper class
|
|
299
|
+
│ ├── interfaces.py # DataSource ABC (extensibility)
|
|
228
300
|
│ └── utils.py
|
|
229
301
|
├── examples/
|
|
230
302
|
│ ├── basic_extraction.py
|
|
231
303
|
│ ├── basic_recommendation.py
|
|
232
|
-
│
|
|
304
|
+
│ ├── flask_integration.py
|
|
305
|
+
│ ├── custom_ner_backend.py # Example: KeywordNERBackend
|
|
306
|
+
│ ├── custom_data_source.py # Example: CSVDataSource
|
|
307
|
+
│ ├── custom_ranking_strategy.py # Example: KeywordMatchRanking
|
|
308
|
+
│ └── demo_extensibility.py # Combined end-to-end demo
|
|
233
309
|
└── tests/
|
|
234
310
|
├── test_extraction.py
|
|
235
311
|
├── test_recommendation.py
|
|
@@ -6,6 +6,7 @@ acuity/__init__.py
|
|
|
6
6
|
acuity/config.py
|
|
7
7
|
acuity/utils.py
|
|
8
8
|
acuity/extraction/__init__.py
|
|
9
|
+
acuity/extraction/interfaces.py
|
|
9
10
|
acuity/extraction/ner_crf.py
|
|
10
11
|
acuity/extraction/ner_transformer.py
|
|
11
12
|
acuity/extraction/pipeline.py
|
|
@@ -14,11 +15,13 @@ acuity/extraction/preprocessing.py
|
|
|
14
15
|
acuity/extraction/rules.py
|
|
15
16
|
acuity/recommendation/__init__.py
|
|
16
17
|
acuity/recommendation/engine.py
|
|
18
|
+
acuity/recommendation/interfaces.py
|
|
17
19
|
acuity/recommendation/proximity.py
|
|
18
20
|
acuity/recommendation/ranker.py
|
|
19
21
|
acuity/recommendation/similarity.py
|
|
20
22
|
acuity/recommendation/vectorizer.py
|
|
21
23
|
acuity/scraper/__init__.py
|
|
24
|
+
acuity/scraper/interfaces.py
|
|
22
25
|
acuity/scraper/scraper.py
|
|
23
26
|
acuity/scraper/utils.py
|
|
24
27
|
acuity/verification/__init__.py
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "acuity-framework"
|
|
7
|
-
version = "
|
|
7
|
+
version = "3.0.0"
|
|
8
8
|
description = "ACUITY — A machine learning framework for extracting, verifying, and recommending local micro-enterprise profiles from unstructured community posts."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = {text = "MIT"}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{acuity_framework-2.0.3 → acuity_framework-3.0.0}/acuity_framework.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|