Spaces:
Paused
Paused
| """ | |
| Labeling Function Extraction and Application | |
| Inspired by ALCHEmist (NeurIPS 2024): extracts reusable labeling functions | |
| from high-confidence LLM predictions to label instances without API calls. | |
| A labeling function encodes a pattern like: | |
| "When text contains 'love it' -> positive (confidence: 0.95)" | |
| These functions are extracted from LLM reasoning on high-confidence predictions, | |
| then applied to unlabeled instances via majority voting. | |
| """ | |
| import logging | |
| import re | |
| import uuid | |
| from dataclasses import dataclass, field | |
| from datetime import datetime | |
| from typing import Any, Callable, Dict, List, Optional, Tuple | |
| logger = logging.getLogger(__name__) | |
| ABSTAIN = "__ABSTAIN__" | |
| class LabelingFunction: | |
| """A reusable labeling function extracted from LLM patterns. | |
| Each function encodes a condition-label mapping discovered from | |
| high-confidence LLM predictions. | |
| """ | |
| id: str | |
| pattern_text: str # Human-readable pattern description | |
| condition: str # The condition part (e.g., "text contains 'great'") | |
| label: str # The label to assign when condition matches | |
| confidence: float # Source LLM confidence when pattern was discovered | |
| source_instance_ids: List[str] = field(default_factory=list) | |
| coverage: int = 0 # Number of instances this function matched | |
| accuracy: Optional[float] = None # Accuracy against human labels if known | |
| enabled: bool = True | |
| created_at: str = "" | |
| extracted_from_reasoning: str = "" # Original LLM reasoning snippet | |
| def __post_init__(self): | |
| if not self.created_at: | |
| self.created_at = datetime.now().isoformat() | |
| def to_dict(self) -> Dict[str, Any]: | |
| """Serialize to dictionary.""" | |
| return { | |
| 'id': self.id, | |
| 'pattern_text': self.pattern_text, | |
| 'condition': self.condition, | |
| 'label': self.label, | |
| 'confidence': self.confidence, | |
| 'source_instance_ids': self.source_instance_ids, | |
| 'coverage': self.coverage, | |
| 'accuracy': self.accuracy, | |
| 'enabled': self.enabled, | |
| 'created_at': self.created_at, | |
| 'extracted_from_reasoning': self.extracted_from_reasoning, | |
| } | |
| def from_dict(cls, data: Dict[str, Any]) -> 'LabelingFunction': | |
| """Deserialize from dictionary.""" | |
| return cls( | |
| id=data['id'], | |
| pattern_text=data['pattern_text'], | |
| condition=data['condition'], | |
| label=data['label'], | |
| confidence=data.get('confidence', 0.0), | |
| source_instance_ids=data.get('source_instance_ids', []), | |
| coverage=data.get('coverage', 0), | |
| accuracy=data.get('accuracy'), | |
| enabled=data.get('enabled', True), | |
| created_at=data.get('created_at', ''), | |
| extracted_from_reasoning=data.get('extracted_from_reasoning', ''), | |
| ) | |
| class LabelingFunctionVote: | |
| """A vote from a labeling function for a specific instance.""" | |
| function_id: str | |
| label: str | |
| confidence: float | |
| class ApplyResult: | |
| """Result of applying labeling functions to an instance.""" | |
| instance_id: str | |
| label: Optional[str] = None | |
| votes: List[LabelingFunctionVote] = field(default_factory=list) | |
| abstained: bool = True | |
| vote_agreement: float = 0.0 # Fraction of votes that agree on the label | |
| def to_dict(self) -> Dict[str, Any]: | |
| return { | |
| 'instance_id': self.instance_id, | |
| 'label': self.label, | |
| 'abstained': self.abstained, | |
| 'vote_agreement': self.vote_agreement, | |
| 'num_votes': len(self.votes), | |
| } | |
| EXTRACTION_PROMPT = """Analyze the following high-confidence LLM predictions and extract reusable labeling patterns. | |
| For each prediction, the LLM was highly confident about its label. Extract patterns that could be applied to new instances without calling the LLM. | |
| Predictions: | |
| {predictions_text} | |
| Extract labeling functions as a JSON array. Each function should have: | |
| - "pattern_text": A human-readable description of the pattern | |
| - "condition": A simple text-matching condition (e.g., "text contains 'keyword'", "text starts with 'pattern'", "text mentions sentiment words like 'great', 'love'") | |
| - "label": The label to assign when the condition matches | |
| - "keywords": List of keywords/phrases that trigger this pattern | |
| Return ONLY a JSON array of objects. Example: | |
| [ | |
| {{ | |
| "pattern_text": "Positive sentiment keywords like 'love', 'great', 'amazing'", | |
| "condition": "text contains positive sentiment keywords", | |
| "label": "positive", | |
| "keywords": ["love", "great", "amazing", "excellent", "wonderful"] | |
| }} | |
| ]""" | |
| class LabelingFunctionExtractor: | |
| """Extracts labeling functions from high-confidence LLM predictions. | |
| Analyzes patterns in LLM reasoning to discover reusable rules | |
| that can label future instances without API calls. | |
| """ | |
| def __init__(self, app_config: Dict, solo_config): | |
| self._app_config = app_config | |
| self._solo_config = solo_config | |
| self._lf_config = solo_config.labeling_functions | |
| self._endpoint = None | |
| def extract_from_predictions( | |
| self, | |
| predictions: List[Dict[str, Any]], | |
| ) -> List[LabelingFunction]: | |
| """Extract labeling functions from high-confidence predictions. | |
| Args: | |
| predictions: List of dicts with 'instance_id', 'text', | |
| 'predicted_label', 'confidence', 'reasoning'. | |
| Returns: | |
| List of extracted LabelingFunction objects. | |
| """ | |
| if not predictions: | |
| return [] | |
| # Filter to high-confidence predictions | |
| min_conf = self._lf_config.min_confidence | |
| high_conf = [p for p in predictions if p.get('confidence', 0) >= min_conf] | |
| if not high_conf: | |
| return [] | |
| # Group by label | |
| by_label: Dict[str, List[Dict]] = {} | |
| for p in high_conf: | |
| label = str(p.get('predicted_label', '')) | |
| by_label.setdefault(label, []).append(p) | |
| # Try LLM-assisted extraction first | |
| functions = self._extract_with_llm(high_conf) | |
| # Fallback to keyword-based extraction if LLM fails | |
| if not functions: | |
| functions = self._extract_keyword_patterns(by_label) | |
| # Limit to max_functions | |
| max_fns = self._lf_config.max_functions | |
| if len(functions) > max_fns: | |
| # Keep highest-confidence functions | |
| functions.sort(key=lambda f: f.confidence, reverse=True) | |
| functions = functions[:max_fns] | |
| return functions | |
| def _extract_with_llm( | |
| self, predictions: List[Dict[str, Any]] | |
| ) -> List[LabelingFunction]: | |
| """Use LLM to extract labeling functions from prediction patterns.""" | |
| endpoint = self._get_endpoint() | |
| if endpoint is None: | |
| return [] | |
| # Build prompt with prediction examples (limit to 20 for context) | |
| sample = predictions[:20] | |
| pred_lines = [] | |
| for p in sample: | |
| text = str(p.get('text', ''))[:200] | |
| pred_lines.append( | |
| f"- Text: \"{text}\"\n" | |
| f" Label: {p.get('predicted_label')} " | |
| f"(confidence: {p.get('confidence', 0):.2f})\n" | |
| f" Reasoning: {p.get('reasoning', 'N/A')}" | |
| ) | |
| prompt = EXTRACTION_PROMPT.format( | |
| predictions_text="\n\n".join(pred_lines) | |
| ) | |
| try: | |
| response = endpoint.query(prompt) | |
| parsed = self._parse_json_array(response) | |
| if not parsed: | |
| return [] | |
| functions = [] | |
| for item in parsed: | |
| if not isinstance(item, dict): | |
| continue | |
| pattern_text = item.get('pattern_text', '') | |
| condition = item.get('condition', '') | |
| label = item.get('label', '') | |
| keywords = item.get('keywords', []) | |
| if not label or (not condition and not keywords): | |
| continue | |
| # Find source instances matching this pattern | |
| source_ids = [] | |
| for p in predictions: | |
| if str(p.get('predicted_label', '')) == label: | |
| source_ids.append(p['instance_id']) | |
| if len(source_ids) >= 5: | |
| break | |
| # Compute average confidence for matching predictions | |
| matching_confs = [ | |
| p['confidence'] for p in predictions | |
| if str(p.get('predicted_label', '')) == label | |
| ] | |
| avg_conf = ( | |
| sum(matching_confs) / len(matching_confs) | |
| if matching_confs else 0.0 | |
| ) | |
| fn = LabelingFunction( | |
| id=f"lf_{uuid.uuid4().hex[:8]}", | |
| pattern_text=pattern_text, | |
| condition=condition, | |
| label=label, | |
| confidence=avg_conf, | |
| source_instance_ids=source_ids, | |
| extracted_from_reasoning=', '.join(keywords) if keywords else condition, | |
| ) | |
| functions.append(fn) | |
| logger.info(f"LLM extracted {len(functions)} labeling functions") | |
| return functions | |
| except Exception as e: | |
| logger.warning(f"LLM extraction failed: {e}") | |
| return [] | |
| def _extract_keyword_patterns( | |
| self, by_label: Dict[str, List[Dict]] | |
| ) -> List[LabelingFunction]: | |
| """Fallback: extract keyword patterns from prediction texts. | |
| Groups predictions by label and finds common words/phrases | |
| that appear frequently in texts with the same label. | |
| """ | |
| functions = [] | |
| for label, preds in by_label.items(): | |
| if len(preds) < self._lf_config.min_coverage: | |
| continue | |
| # Collect all words from texts for this label | |
| word_counts: Dict[str, int] = {} | |
| word_instances: Dict[str, List[str]] = {} | |
| for p in preds: | |
| text = str(p.get('text', '')).lower() | |
| words = set(re.findall(r'\b\w{3,}\b', text)) | |
| for w in words: | |
| word_counts[w] = word_counts.get(w, 0) + 1 | |
| word_instances.setdefault(w, []).append(p['instance_id']) | |
| # Find words that appear in >= min_coverage predictions | |
| min_cov = self._lf_config.min_coverage | |
| common_words = { | |
| w: c for w, c in word_counts.items() | |
| if c >= min_cov | |
| } | |
| if not common_words: | |
| continue | |
| # Filter out very common words (> 80% of all predictions) | |
| total = len(preds) | |
| significant = { | |
| w: c for w, c in common_words.items() | |
| if c <= total * 0.8 | |
| } | |
| if not significant: | |
| continue | |
| # Take top keywords by frequency | |
| top_keywords = sorted( | |
| significant.items(), key=lambda x: x[1], reverse=True | |
| )[:5] | |
| keywords = [w for w, _ in top_keywords] | |
| avg_conf = ( | |
| sum(p.get('confidence', 0) for p in preds) / len(preds) | |
| ) | |
| source_ids = [p['instance_id'] for p in preds[:5]] | |
| fn = LabelingFunction( | |
| id=f"lf_{uuid.uuid4().hex[:8]}", | |
| pattern_text=( | |
| f"Text containing keywords like " | |
| f"'{', '.join(keywords)}' -> {label}" | |
| ), | |
| condition=f"text contains any of: {', '.join(keywords)}", | |
| label=label, | |
| confidence=avg_conf, | |
| source_instance_ids=source_ids, | |
| coverage=len(preds), | |
| extracted_from_reasoning=', '.join(keywords), | |
| ) | |
| functions.append(fn) | |
| return functions | |
| def _get_endpoint(self): | |
| """Get or create an AI endpoint for extraction.""" | |
| if self._endpoint is not None: | |
| return self._endpoint | |
| try: | |
| from potato.ai.ai_endpoint import AIEndpointFactory | |
| models = ( | |
| self._solo_config.revision_models | |
| or self._solo_config.labeling_models | |
| ) | |
| for model_config in models: | |
| try: | |
| endpoint_config = model_config.to_endpoint_config(temperature_override=0.3) | |
| endpoint = AIEndpointFactory.create_endpoint(endpoint_config) | |
| if endpoint: | |
| self._endpoint = endpoint | |
| return endpoint | |
| except Exception: | |
| continue | |
| except Exception as e: | |
| logger.warning(f"Could not create extraction endpoint: {e}") | |
| return None | |
| def _parse_json_array(self, response: str) -> Optional[list]: | |
| """Parse a JSON array from LLM response.""" | |
| import json | |
| if not response: | |
| return None | |
| # Try direct parse | |
| text = response.strip() | |
| try: | |
| result = json.loads(text) | |
| if isinstance(result, list): | |
| return result | |
| except (json.JSONDecodeError, TypeError): | |
| pass | |
| # Try extracting from markdown code fence | |
| match = re.search(r'```(?:json)?\s*\n?(.*?)\n?```', text, re.DOTALL) | |
| if match: | |
| try: | |
| result = json.loads(match.group(1).strip()) | |
| if isinstance(result, list): | |
| return result | |
| except (json.JSONDecodeError, TypeError): | |
| pass | |
| # Try finding array brackets | |
| start = text.find('[') | |
| end = text.rfind(']') | |
| if start >= 0 and end > start: | |
| try: | |
| result = json.loads(text[start:end + 1]) | |
| if isinstance(result, list): | |
| return result | |
| except (json.JSONDecodeError, TypeError): | |
| pass | |
| return None | |
| class LabelingFunctionApplier: | |
| """Applies labeling functions to instances for weak supervision. | |
| Uses majority voting among matching labeling functions to assign labels | |
| without calling the LLM. | |
| """ | |
| def __init__(self, vote_threshold: float = 0.5): | |
| self._vote_threshold = vote_threshold | |
| def apply( | |
| self, | |
| instance_id: str, | |
| text: str, | |
| functions: List[LabelingFunction], | |
| ) -> ApplyResult: | |
| """Apply all enabled labeling functions to an instance. | |
| Args: | |
| instance_id: The instance identifier. | |
| text: The instance text. | |
| functions: List of labeling functions to try. | |
| Returns: | |
| ApplyResult with the voted label or abstention. | |
| """ | |
| votes: List[LabelingFunctionVote] = [] | |
| text_lower = text.lower() | |
| for fn in functions: | |
| if not fn.enabled: | |
| continue | |
| if self._matches(fn, text_lower): | |
| votes.append(LabelingFunctionVote( | |
| function_id=fn.id, | |
| label=fn.label, | |
| confidence=fn.confidence, | |
| )) | |
| if not votes: | |
| return ApplyResult(instance_id=instance_id, abstained=True) | |
| # Majority vote weighted by confidence | |
| label_scores: Dict[str, float] = {} | |
| for v in votes: | |
| label_scores[v.label] = label_scores.get(v.label, 0) + v.confidence | |
| # Find winning label | |
| best_label = max(label_scores, key=label_scores.get) | |
| total_score = sum(label_scores.values()) | |
| agreement = label_scores[best_label] / total_score if total_score > 0 else 0 | |
| # Check if agreement meets threshold | |
| if agreement < self._vote_threshold: | |
| return ApplyResult( | |
| instance_id=instance_id, | |
| votes=votes, | |
| abstained=True, | |
| vote_agreement=agreement, | |
| ) | |
| return ApplyResult( | |
| instance_id=instance_id, | |
| label=best_label, | |
| votes=votes, | |
| abstained=False, | |
| vote_agreement=agreement, | |
| ) | |
| def apply_batch( | |
| self, | |
| instances: List[Dict[str, str]], | |
| functions: List[LabelingFunction], | |
| ) -> List[ApplyResult]: | |
| """Apply labeling functions to a batch of instances. | |
| Args: | |
| instances: List of dicts with 'instance_id' and 'text'. | |
| functions: List of labeling functions. | |
| Returns: | |
| List of ApplyResult, one per instance. | |
| """ | |
| enabled = [f for f in functions if f.enabled] | |
| if not enabled: | |
| return [ | |
| ApplyResult(instance_id=inst['instance_id'], abstained=True) | |
| for inst in instances | |
| ] | |
| return [ | |
| self.apply(inst['instance_id'], inst['text'], enabled) | |
| for inst in instances | |
| ] | |
| def _matches(self, fn: LabelingFunction, text_lower: str) -> bool: | |
| """Check if a labeling function matches the given text. | |
| Uses keyword matching from the function's extracted_from_reasoning | |
| and condition fields. | |
| """ | |
| # Extract keywords from the function | |
| keywords = self._get_keywords(fn) | |
| if not keywords: | |
| return False | |
| # Check if any keyword appears in the text | |
| return any(kw in text_lower for kw in keywords) | |
| def _get_keywords(self, fn: LabelingFunction) -> List[str]: | |
| """Extract lowercase keywords from a labeling function.""" | |
| keywords = [] | |
| # Parse keywords from extracted_from_reasoning (comma-separated) | |
| reasoning = fn.extracted_from_reasoning | |
| if reasoning: | |
| parts = [p.strip().lower() for p in reasoning.split(',')] | |
| keywords.extend(p for p in parts if p and len(p) >= 2) | |
| # Parse keywords from condition if it has "contains" pattern | |
| condition = fn.condition.lower() | |
| # Match patterns like "text contains 'word'" or "any of: word1, word2" | |
| contains_match = re.findall(r"'([^']+)'", condition) | |
| if contains_match: | |
| keywords.extend(w.lower() for w in contains_match) | |
| any_of_match = re.search(r'any of:\s*(.+)', condition) | |
| if any_of_match: | |
| parts = [p.strip().lower() for p in any_of_match.group(1).split(',')] | |
| keywords.extend(p for p in parts if p and len(p) >= 2) | |
| return keywords | |
| class LabelingFunctionManager: | |
| """Manages the lifecycle of labeling functions. | |
| Handles extraction, storage, application, and statistics tracking. | |
| """ | |
| def __init__(self, app_config: Dict, solo_config): | |
| self._app_config = app_config | |
| self._solo_config = solo_config | |
| self._lf_config = solo_config.labeling_functions | |
| self._functions: Dict[str, LabelingFunction] = {} | |
| self._extractor = LabelingFunctionExtractor(app_config, solo_config) | |
| self._applier = LabelingFunctionApplier( | |
| vote_threshold=self._lf_config.vote_threshold | |
| ) | |
| self._instances_labeled: int = 0 | |
| self._instances_abstained: int = 0 | |
| def enabled(self) -> bool: | |
| return self._lf_config.enabled | |
| def get_all_functions(self) -> List[LabelingFunction]: | |
| """Get all labeling functions.""" | |
| return list(self._functions.values()) | |
| def get_enabled_functions(self) -> List[LabelingFunction]: | |
| """Get only enabled labeling functions.""" | |
| return [f for f in self._functions.values() if f.enabled] | |
| def get_function(self, function_id: str) -> Optional[LabelingFunction]: | |
| """Get a specific labeling function by ID.""" | |
| return self._functions.get(function_id) | |
| def add_function(self, fn: LabelingFunction) -> None: | |
| """Add a labeling function.""" | |
| self._functions[fn.id] = fn | |
| def toggle_function(self, function_id: str) -> Optional[bool]: | |
| """Toggle a function's enabled state. Returns new state or None.""" | |
| fn = self._functions.get(function_id) | |
| if fn is None: | |
| return None | |
| fn.enabled = not fn.enabled | |
| return fn.enabled | |
| def remove_function(self, function_id: str) -> bool: | |
| """Remove a labeling function.""" | |
| return self._functions.pop(function_id, None) is not None | |
| def extract_functions( | |
| self, | |
| predictions: List[Dict[str, Any]], | |
| ) -> List[LabelingFunction]: | |
| """Extract new labeling functions from predictions. | |
| Args: | |
| predictions: List of dicts with instance_id, text, | |
| predicted_label, confidence, reasoning. | |
| Returns: | |
| List of newly extracted functions. | |
| """ | |
| new_fns = self._extractor.extract_from_predictions(predictions) | |
| for fn in new_fns: | |
| self._functions[fn.id] = fn | |
| if new_fns: | |
| logger.info( | |
| f"Extracted {len(new_fns)} labeling functions " | |
| f"(total: {len(self._functions)})" | |
| ) | |
| return new_fns | |
| def try_label( | |
| self, instance_id: str, text: str | |
| ) -> Optional[ApplyResult]: | |
| """Try to label an instance using labeling functions. | |
| Returns: | |
| ApplyResult if a label was assigned, None if abstained or disabled. | |
| """ | |
| if not self._lf_config.enabled: | |
| return None | |
| enabled = self.get_enabled_functions() | |
| if not enabled: | |
| return None | |
| result = self._applier.apply(instance_id, text, enabled) | |
| if result.abstained: | |
| self._instances_abstained += 1 | |
| return None | |
| self._instances_labeled += 1 | |
| # Update coverage counts | |
| for vote in result.votes: | |
| fn = self._functions.get(vote.function_id) | |
| if fn: | |
| fn.coverage += 1 | |
| return result | |
| def apply_batch( | |
| self, instances: List[Dict[str, str]] | |
| ) -> Tuple[List[ApplyResult], List[Dict[str, str]]]: | |
| """Apply labeling functions to a batch, returning labeled and remaining. | |
| Args: | |
| instances: List of dicts with instance_id and text. | |
| Returns: | |
| Tuple of (labeled_results, unlabeled_instances). | |
| """ | |
| if not self._lf_config.enabled: | |
| return [], instances | |
| enabled = self.get_enabled_functions() | |
| if not enabled: | |
| return [], instances | |
| labeled = [] | |
| remaining = [] | |
| for inst in instances: | |
| result = self._applier.apply( | |
| inst['instance_id'], inst['text'], enabled | |
| ) | |
| if result.abstained: | |
| remaining.append(inst) | |
| self._instances_abstained += 1 | |
| else: | |
| labeled.append(result) | |
| self._instances_labeled += 1 | |
| # Update coverage | |
| for vote in result.votes: | |
| fn = self._functions.get(vote.function_id) | |
| if fn: | |
| fn.coverage += 1 | |
| return labeled, remaining | |
| def get_stats(self) -> Dict[str, Any]: | |
| """Get labeling function statistics.""" | |
| functions = list(self._functions.values()) | |
| enabled = [f for f in functions if f.enabled] | |
| return { | |
| 'enabled': self._lf_config.enabled, | |
| 'total_functions': len(functions), | |
| 'enabled_functions': len(enabled), | |
| 'instances_labeled': self._instances_labeled, | |
| 'instances_abstained': self._instances_abstained, | |
| 'total_coverage': sum(f.coverage for f in functions), | |
| 'avg_confidence': ( | |
| sum(f.confidence for f in functions) / len(functions) | |
| if functions else 0.0 | |
| ), | |
| } | |
| def to_dict(self) -> Dict[str, Any]: | |
| """Serialize state for persistence.""" | |
| return { | |
| 'functions': [f.to_dict() for f in self._functions.values()], | |
| 'instances_labeled': self._instances_labeled, | |
| 'instances_abstained': self._instances_abstained, | |
| } | |
| def load_state(self, data: Dict[str, Any]) -> None: | |
| """Restore state from persisted data.""" | |
| self._functions = {} | |
| for fn_data in data.get('functions', []): | |
| fn = LabelingFunction.from_dict(fn_data) | |
| self._functions[fn.id] = fn | |
| self._instances_labeled = data.get('instances_labeled', 0) | |
| self._instances_abstained = data.get('instances_abstained', 0) | |