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.
Files changed (38) hide show
  1. {acuity_framework-2.0.3/acuity_framework.egg-info → acuity_framework-3.0.0}/PKG-INFO +80 -4
  2. {acuity_framework-2.0.3 → acuity_framework-3.0.0}/README.md +79 -3
  3. {acuity_framework-2.0.3 → acuity_framework-3.0.0}/acuity/__init__.py +1 -1
  4. acuity_framework-3.0.0/acuity/extraction/interfaces.py +51 -0
  5. {acuity_framework-2.0.3 → acuity_framework-3.0.0}/acuity/extraction/pipeline.py +72 -5
  6. {acuity_framework-2.0.3 → acuity_framework-3.0.0}/acuity/recommendation/engine.py +23 -4
  7. acuity_framework-3.0.0/acuity/recommendation/interfaces.py +58 -0
  8. acuity_framework-3.0.0/acuity/scraper/interfaces.py +58 -0
  9. {acuity_framework-2.0.3 → acuity_framework-3.0.0/acuity_framework.egg-info}/PKG-INFO +80 -4
  10. {acuity_framework-2.0.3 → acuity_framework-3.0.0}/acuity_framework.egg-info/SOURCES.txt +3 -0
  11. {acuity_framework-2.0.3 → acuity_framework-3.0.0}/pyproject.toml +1 -1
  12. {acuity_framework-2.0.3 → acuity_framework-3.0.0}/LICENSE +0 -0
  13. {acuity_framework-2.0.3 → acuity_framework-3.0.0}/acuity/config.py +0 -0
  14. {acuity_framework-2.0.3 → acuity_framework-3.0.0}/acuity/extraction/__init__.py +0 -0
  15. {acuity_framework-2.0.3 → acuity_framework-3.0.0}/acuity/extraction/ner_crf.py +0 -0
  16. {acuity_framework-2.0.3 → acuity_framework-3.0.0}/acuity/extraction/ner_transformer.py +0 -0
  17. {acuity_framework-2.0.3 → acuity_framework-3.0.0}/acuity/extraction/postprocessing.py +0 -0
  18. {acuity_framework-2.0.3 → acuity_framework-3.0.0}/acuity/extraction/preprocessing.py +0 -0
  19. {acuity_framework-2.0.3 → acuity_framework-3.0.0}/acuity/extraction/rules.py +0 -0
  20. {acuity_framework-2.0.3 → acuity_framework-3.0.0}/acuity/recommendation/__init__.py +0 -0
  21. {acuity_framework-2.0.3 → acuity_framework-3.0.0}/acuity/recommendation/proximity.py +0 -0
  22. {acuity_framework-2.0.3 → acuity_framework-3.0.0}/acuity/recommendation/ranker.py +0 -0
  23. {acuity_framework-2.0.3 → acuity_framework-3.0.0}/acuity/recommendation/similarity.py +0 -0
  24. {acuity_framework-2.0.3 → acuity_framework-3.0.0}/acuity/recommendation/vectorizer.py +0 -0
  25. {acuity_framework-2.0.3 → acuity_framework-3.0.0}/acuity/scraper/__init__.py +0 -0
  26. {acuity_framework-2.0.3 → acuity_framework-3.0.0}/acuity/scraper/scraper.py +0 -0
  27. {acuity_framework-2.0.3 → acuity_framework-3.0.0}/acuity/scraper/utils.py +0 -0
  28. {acuity_framework-2.0.3 → acuity_framework-3.0.0}/acuity/utils.py +0 -0
  29. {acuity_framework-2.0.3 → acuity_framework-3.0.0}/acuity/verification/__init__.py +0 -0
  30. {acuity_framework-2.0.3 → acuity_framework-3.0.0}/acuity/verification/bplo.py +0 -0
  31. {acuity_framework-2.0.3 → acuity_framework-3.0.0}/acuity_framework.egg-info/dependency_links.txt +0 -0
  32. {acuity_framework-2.0.3 → acuity_framework-3.0.0}/acuity_framework.egg-info/requires.txt +0 -0
  33. {acuity_framework-2.0.3 → acuity_framework-3.0.0}/acuity_framework.egg-info/top_level.txt +0 -0
  34. {acuity_framework-2.0.3 → acuity_framework-3.0.0}/setup.cfg +0 -0
  35. {acuity_framework-2.0.3 → acuity_framework-3.0.0}/setup.py +0 -0
  36. {acuity_framework-2.0.3 → acuity_framework-3.0.0}/tests/test_extraction.py +0 -0
  37. {acuity_framework-2.0.3 → acuity_framework-3.0.0}/tests/test_recommendation.py +0 -0
  38. {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: 2.0.3
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/ # Facebook scraper (optional)
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
- │ └── flask_integration.py
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/ # Facebook scraper (optional)
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
- │ └── flask_integration.py
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
@@ -19,7 +19,7 @@ Quick Start:
19
19
  >>> from acuity.verification import BPLOVerifier
20
20
  """
21
21
 
22
- __version__ = "2.0.0"
22
+ __version__ = "3.0.0"
23
23
  __author__ = "ACUITY Research Team"
24
24
 
25
25
  from .config import AcuityConfig
@@ -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__(self, config: AcuityConfig | None = None, ner_model: Any = None):
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._setup_ner()
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
- entities = self._ner_extract_fn(cleaned) if self._ner_extract_fn else {
90
- "business_name": [], "categories": [], "locations": []
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 or self._tfidf_matrix is None or self._vectorizer is None:
116
+ if not self.profiles:
107
117
  return []
108
118
 
109
119
  # Textual relevance
110
- query_vec = transform_query(self._vectorizer, query)
111
- cosine_scores = compute_cosine_scores(self._tfidf_matrix, query_vec)
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
- # Proximity (if user location provided)
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: 2.0.3
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/ # Facebook scraper (optional)
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
- │ └── flask_integration.py
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 = "2.0.3"
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"}