| |
| """ |
| API-Based Clue Generator for Crossword Puzzles |
| Uses Hugging Face Inference API to test multiple models without local downloads. |
| """ |
|
|
| import os |
| import time |
| import json |
| import logging |
| import requests |
| from typing import List, Dict, Optional, Tuple |
| from pathlib import Path |
|
|
| |
| logging.basicConfig( |
| level=logging.INFO, |
| format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' |
| ) |
| logger = logging.getLogger(__name__) |
|
|
|
|
| class APIClueGenerator: |
| """ |
| API-based clue generator using Hugging Face Inference API. |
| Tests multiple models without local downloads. |
| """ |
| |
| def __init__(self, hf_token: Optional[str] = None): |
| """Initialize API clue generator. |
| |
| Args: |
| hf_token: Hugging Face API token (optional but recommended for rate limits) |
| """ |
| self.hf_token = hf_token or os.getenv('HF_TOKEN') or os.getenv('HUGGINGFACE_TOKEN') |
| self.base_url = "https://router.huggingface.co/v1" |
| |
| |
| self.models = { |
| "deepseek-v3": "deepseek-ai/DeepSeek-V3-0324:fireworks-ai", |
| "llama-3.3-70b": "meta-llama/Llama-3.3-70B-Instruct:fireworks-ai", |
| } |
| |
| |
| self.headers = {} |
| if self.hf_token: |
| self.headers["Authorization"] = f"Bearer {self.hf_token}" |
| |
| |
| self.prompts = { |
| "instruction": """Generate a crossword clue for '{word}' (category: {topic}). |
| |
| Rules: |
| - 2-6 words only |
| - Don't use the word '{word}' in the clue |
| - Be descriptive and accurate |
| |
| Examples: |
| - CAT (animals) → "Feline household pet" |
| - GUITAR (music) → "Six-stringed instrument" |
| - AIRPORT (transportation) → "Flight departure hub" |
| |
| Clue for '{word}' ({topic}):""", |
|
|
| "simple": """Complete this crossword clue: |
| |
| {word} ({topic}) = [ANSWER] |
| |
| Examples: |
| VIOLIN (music) = Bowed string instrument |
| SCIENTIST (science) = Research professional |
| DATABASE (technology) = Information storage system |
| |
| {word} ({topic}) =""", |
|
|
| "question": """What is '{word}' in the context of {topic}? Give a brief crossword clue (2-5 words) without using the word '{word}'. |
| |
| Answer:""" |
| } |
| |
| def query_model(self, model_name: str, word: str, context: str, max_retries: int = 3) -> Optional[str]: |
| """Query a model via Hugging Face Router API using chat completions format. |
| |
| Args: |
| model_name: Name of the model to query |
| word: Target word for clue generation |
| context: Topic/context for the word |
| max_retries: Maximum number of retries |
| |
| Returns: |
| Generated clue text or None if failed |
| """ |
| url = f"{self.base_url}/chat/completions" |
| |
| |
| messages = [ |
| { |
| "role": "system", |
| "content": f"You are a crossword puzzle clue generator. Generate a single, concise, creative crossword clue for the word '{word}'. The clue should be 2-8 words, accurate, and not contain the word '{word}' itself." |
| }, |
| { |
| "role": "user", |
| "content": f"Generate a crossword clue for the word '{word}' in the context of '{context}'." |
| } |
| ] |
| |
| payload = { |
| "model": model_name, |
| "messages": messages, |
| "temperature": 0.7, |
| "max_tokens": 50 |
| } |
| |
| for attempt in range(max_retries): |
| try: |
| response = requests.post(url, headers=self.headers, json=payload, timeout=30) |
| response.raise_for_status() |
| |
| result = response.json() |
| |
| |
| if "choices" in result and len(result["choices"]) > 0: |
| generated_text = result["choices"][0]["message"]["content"].strip() |
| return self._clean_response(generated_text) |
| else: |
| logger.warning(f"No choices in response for {model_name}") |
| return None |
| |
| except requests.exceptions.RequestException as e: |
| logger.warning(f"Request failed for {model_name} (attempt {attempt+1}): {e}") |
| if hasattr(e, 'response') and e.response is not None: |
| logger.warning(f"Response content: {e.response.text}") |
| if attempt < max_retries - 1: |
| time.sleep(2) |
| except Exception as e: |
| logger.warning(f"Unexpected error for {model_name} (attempt {attempt+1}): {e}") |
| if attempt < max_retries - 1: |
| time.sleep(2) |
| |
| return None |
| |
| def _clean_response(self, text: str) -> str: |
| """Clean and validate API response.""" |
| if not text: |
| return "" |
| |
| |
| text = text.strip() |
| text = text.replace('\n', ' ').replace('\t', ' ') |
| |
| |
| text = text.strip('"\'[](){}') |
| |
| |
| if '.' in text and len(text.split('.')) > 1: |
| text = text.split('.')[0].strip() |
| if '\n' in text: |
| text = text.split('\n')[0].strip() |
| |
| |
| if len(text) < 3 or len(text) > 100: |
| return "" |
| |
| return text |
| |
| def generate_clue(self, word: str, topic: str) -> Dict[str, Optional[str]]: |
| """Generate clues using all available models. |
| |
| Args: |
| word: Target word |
| topic: Topic/category context |
| |
| Returns: |
| Dictionary mapping model names to generated clues |
| """ |
| results = {} |
| |
| logger.info(f"🎯 Generating clues for '{word}' + '{topic}' using {len(self.models)} models") |
| |
| for model_key, model_name in self.models.items(): |
| logger.info(f" Querying {model_key}...") |
| clue = self.query_model(model_name, word, topic) |
| results[model_key] = clue |
| |
| |
| time.sleep(1) |
| |
| return results |
| |
| def evaluate_clue_quality(self, word: str, clue: str) -> Tuple[str, float]: |
| """Evaluate the quality of a generated clue. |
| |
| Args: |
| word: Target word |
| clue: Generated clue |
| |
| Returns: |
| Tuple of (quality_label, quality_score) |
| """ |
| if not clue or len(clue.strip()) < 3: |
| return "FAILED", 0.0 |
| |
| word_lower = word.lower() |
| clue_lower = clue.lower() |
| |
| |
| if word_lower in clue_lower: |
| return "POOR", 0.2 |
| |
| |
| score = 0.5 |
| quality_words = ["player", "instrument", "device", "system", "location", "animal", |
| "food", "building", "activity", "professional", "tool", "creature", |
| "terminal", "hub", "language", "storage", "sport", "game"] |
| |
| if any(qw in clue_lower for qw in quality_words): |
| score += 0.3 |
| |
| if 2 <= len(clue.split()) <= 6: |
| score += 0.1 |
| |
| if len(clue) >= 8: |
| score += 0.1 |
| |
| |
| if score >= 0.8: |
| return "EXCELLENT", score |
| elif score >= 0.6: |
| return "GOOD", score |
| elif score >= 0.4: |
| return "ACCEPTABLE", score |
| else: |
| return "POOR", score |
|
|
|
|
| def main(): |
| """Demo the API clue generator using your working approach.""" |
| generator = APIClueGenerator() |
| |
| if not generator.hf_token: |
| print("❌ Error: HF_TOKEN environment variable not set") |
| print("Please set your Hugging Face token: export HF_TOKEN='hf_your_token_here'") |
| return |
| |
| |
| test_cases = [ |
| ("CRICKET", "sports"), |
| ("SHUTTLE", "space"), |
| ("CAT", "animals"), |
| ("DATABASE", "technology"), |
| ] |
| |
| for word, topic in test_cases: |
| print(f"\n🧪 Testing: {word} + {topic}") |
| print("=" * 50) |
| |
| results = generator.generate_clue(word, topic) |
| |
| for model, clue in results.items(): |
| if clue: |
| quality, score = generator.evaluate_clue_quality(word, clue) |
| print(f"{model:15} | {quality:10} | {clue}") |
| else: |
| print(f"{model:15} | FAILED | No response") |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|