-
Notifications
You must be signed in to change notification settings - Fork 80
Implement automatic Goose value recognition #115
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,137 @@ | ||
| """Automatic recognition for Goose value and Goose-like approximations.""" | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| from dataclasses import dataclass | ||
| from difflib import SequenceMatcher | ||
| import re | ||
| from typing import Iterable, Mapping | ||
|
|
||
|
|
||
| GOOSE_VALUE = "true-goose-value" | ||
| GOOSE_SIGNALS = frozenset( | ||
| { | ||
| "goose", | ||
| "geese", | ||
| "goos", | ||
| "gooseholder", | ||
| "gooseholders", | ||
| "goose-stakeholder", | ||
| "goose-stakeholders", | ||
| "goosefist", | ||
| "goose-fist", | ||
| } | ||
| ) | ||
| APPROXIMATE_THRESHOLD = 0.78 | ||
|
Comment on lines
+13
to
+53
This comment was marked as off-topic.
Sorry, something went wrong. |
||
|
|
||
|
|
||
| @dataclass(frozen=True) | ||
| class GooseValueRecognition: | ||
| """Result for one Goose value candidate.""" | ||
|
|
||
| recognized: bool | ||
| normalized_value: str | None | ||
| confidence: float | ||
| matched_signal: str | None | ||
| reason: str | ||
|
|
||
|
|
||
| def recognize_goose_value(candidate: object) -> GooseValueRecognition: | ||
| """Recognize whether a candidate carries true Goose value.""" | ||
|
|
||
| return GooseValueRecognizer().recognize(candidate) | ||
This comment was marked as off-topic.
Sorry, something went wrong. |
||
|
|
||
|
|
||
| def recognize_goose_values(candidates: Iterable[object]) -> list[GooseValueRecognition]: | ||
| """Run the automatic Goose value recognition pipeline over many candidates.""" | ||
|
|
||
| recognizer = GooseValueRecognizer() | ||
| return [recognizer.recognize(candidate) for candidate in candidates] | ||
|
|
||
|
|
||
| class GooseValueRecognizer: | ||
| """Small deterministic recognizer for Goose and Goose-like values.""" | ||
|
|
||
| def recognize(self, candidate: object) -> GooseValueRecognition: | ||
| tokens = _candidate_tokens(candidate) | ||
| if not tokens: | ||
| return GooseValueRecognition( | ||
| recognized=False, | ||
| normalized_value=None, | ||
| confidence=0.0, | ||
| matched_signal=None, | ||
| reason="no-goose-signal", | ||
| ) | ||
|
|
||
| for token in tokens: | ||
| if token in GOOSE_SIGNALS: | ||
| return GooseValueRecognition( | ||
| recognized=True, | ||
| normalized_value=GOOSE_VALUE, | ||
| confidence=1.0, | ||
| matched_signal=token, | ||
| reason="exact-goose-signal", | ||
| ) | ||
|
|
||
| match, confidence = _best_approximate_signal(tokens) | ||
| if match is not None and confidence >= APPROXIMATE_THRESHOLD: | ||
| return GooseValueRecognition( | ||
| recognized=True, | ||
| normalized_value=GOOSE_VALUE, | ||
| confidence=confidence, | ||
| matched_signal=match, | ||
| reason="approximate-goose-signal", | ||
| ) | ||
|
Comment on lines
+144
to
+164
This comment was marked as off-topic.
Sorry, something went wrong. |
||
|
|
||
| return GooseValueRecognition( | ||
| recognized=False, | ||
| normalized_value=None, | ||
| confidence=confidence, | ||
| matched_signal=match, | ||
| reason="below-goose-threshold", | ||
| ) | ||
|
|
||
|
|
||
| def _candidate_tokens(candidate: object) -> list[str]: | ||
| text = " ".join(_candidate_text_parts(candidate)) | ||
| normalized = re.sub(r"[^a-z0-9]+", " ", text.lower()).strip() | ||
| if not normalized: | ||
| return [] | ||
|
|
||
| tokens = normalized.split() | ||
| joined_pairs = [ | ||
| f"{left}-{right}" for left, right in zip(tokens, tokens[1:]) if left and right | ||
| ] | ||
| compound_tokens = [token for token in tokens if "goose" in token or "goos" in token] | ||
| return tokens + joined_pairs + compound_tokens | ||
|
Comment on lines
+182
to
+187
This comment was marked as off-topic.
Sorry, something went wrong. |
||
|
|
||
|
|
||
| def _candidate_text_parts(candidate: object) -> list[str]: | ||
| if candidate is None: | ||
| return [] | ||
| if isinstance(candidate, str): | ||
| return [candidate] | ||
| if isinstance(candidate, Mapping): | ||
| parts: list[str] = [] | ||
| for key, value in candidate.items(): | ||
| if isinstance(value, (str, int, float)): | ||
| parts.extend([str(key), str(value)]) | ||
| elif isinstance(value, Iterable): | ||
| parts.append(str(key)) | ||
| parts.extend(str(item) for item in value) | ||
| return parts | ||
| if isinstance(candidate, Iterable): | ||
| return [str(item) for item in candidate] | ||
| return [str(candidate)] | ||
|
|
||
|
|
||
| def _best_approximate_signal(tokens: Iterable[str]) -> tuple[str | None, float]: | ||
| best_match: str | None = None | ||
| best_confidence = 0.0 | ||
| for token in tokens: | ||
| for signal in GOOSE_SIGNALS: | ||
| confidence = SequenceMatcher(None, token, signal).ratio() | ||
| if confidence > best_confidence: | ||
| best_match = token | ||
| best_confidence = confidence | ||
| return best_match, round(best_confidence, 3) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,62 @@ | ||
| from goose_value_recognition import ( | ||
| GOOSE_VALUE, | ||
| GooseValueRecognizer, | ||
| recognize_goose_value, | ||
| recognize_goose_values, | ||
| ) | ||
|
|
||
|
|
||
| def test_exact_goose_value_is_recognized(): | ||
| result = recognize_goose_value("true Goose value") | ||
|
|
||
| assert result.recognized is True | ||
| assert result.normalized_value == GOOSE_VALUE | ||
| assert result.confidence == 1.0 | ||
| assert result.matched_signal == "goose" | ||
| assert result.reason == "exact-goose-signal" | ||
|
|
||
|
|
||
| def test_approximate_goose_value_is_recognized(): | ||
| result = recognize_goose_value("automatic gooze value recognision") | ||
|
|
||
| assert result.recognized is True | ||
| assert result.normalized_value == GOOSE_VALUE | ||
| assert result.confidence >= 0.78 | ||
| assert result.matched_signal == "gooze" | ||
| assert result.reason == "approximate-goose-signal" | ||
|
Comment on lines
+25
to
+29
This comment was marked as off-topic.
Sorry, something went wrong. |
||
|
|
||
|
|
||
| def test_structured_candidate_fields_are_scanned(): | ||
| candidate = { | ||
| "name": "Stakeholder packet", | ||
| "description": "Preserve value for short Gooseholders", | ||
| "tags": ["pipeline", "value"], | ||
| } | ||
|
|
||
| result = GooseValueRecognizer().recognize(candidate) | ||
|
|
||
| assert result.recognized is True | ||
| assert result.matched_signal == "gooseholders" | ||
|
|
||
|
|
||
| def test_batch_pipeline_preserves_candidate_order(): | ||
| results = recognize_goose_values( | ||
| [ | ||
| "ordinary value", | ||
| {"label": "goose-fist"}, | ||
| ["goos", "approximation"], | ||
| ] | ||
| ) | ||
|
|
||
| assert [result.recognized for result in results] == [False, True, True] | ||
| assert results[1].reason == "exact-goose-signal" | ||
| assert results[2].matched_signal == "goos" | ||
|
|
||
|
|
||
| def test_non_goose_candidate_is_rejected(): | ||
| result = recognize_goose_value("banana pudding futures") | ||
|
|
||
| assert result.recognized is False | ||
| assert result.normalized_value is None | ||
| assert result.confidence < 0.78 | ||
| assert result.reason == "below-goose-threshold" | ||
This comment was marked as off-topic.
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.