CoolFace
Apppublic

uxoxo/eb2ab

sourceHugging Faceapache-2.0updated 11mo agoView on Hugging Face
0likes
llm_processor.py341 linesDownload Raw Back to lib
1#!/usr/bin/env python32"""3LLM Processor - Process text chunks with LLM providers4 5Supports:6- OpenRouter (unified API for multiple models)7- OpenAI (direct)8- Anthropic (direct)9- Ollama (local)10 11Author: Claude Code12Date: 2025-10-1713"""14 15import os16import time17import traceback18from dataclasses import dataclass19from typing import List, Dict, Any, Callable, Optional20 21try:22    import openai23    HAS_OPENAI = True24except ImportError:25    HAS_OPENAI = False26 27try:28    import anthropic29    HAS_ANTHROPIC = True30except ImportError:31    HAS_ANTHROPIC = False32 33 34@dataclass35class TextChunk:36    """Text chunk for processing"""37    content: str38    index: int39    tokens: int40    metadata: Dict[str, Any]41 42 43@dataclass44class ProcessingProgress:45    """Progress update during processing"""46    current_chunk: int47    total_chunks: int48    progress_percent: float49    elapsed_time: float50    estimated_remaining: float51    current_cost: float52 53 54@dataclass55class ProcessingResult:56    """Result from LLM processing"""57    success: bool58    processed_chunks: List[str]59    total_cost: float60    total_time_seconds: float61    total_input_tokens: int62    total_output_tokens: int63    error: Optional[str] = None64 65 66# OpenRouter model pricing (per 1M tokens)67# Updated to match OpenRouter's current model IDs (without version dates)68OPENROUTER_PRICING = {69    "anthropic/claude-3.5-sonnet": {"input": 3.00, "output": 15.00},70    "anthropic/claude-3-haiku": {"input": 0.25, "output": 1.25},71    "openai/gpt-4o": {"input": 2.50, "output": 10.00},72    "openai/gpt-4o-mini": {"input": 0.15, "output": 0.60},73    "openai/gpt-3.5-turbo": {"input": 0.50, "output": 1.50},74    "meta-llama/llama-3.1-8b-instruct": {"input": 0.05, "output": 0.05},75    "meta-llama/llama-3.1-70b-instruct": {"input": 0.35, "output": 0.40},76}77 78 79def process_with_llm(80    chunks: List[TextChunk],81    instruction: str,82    provider: str,83    progress_callback: Optional[Callable] = None,84    max_retries: int = 385) -> ProcessingResult:86    """87    Process text chunks with LLM88 89    Args:90        chunks: List of text chunks to process91        instruction: Processing instruction (e.g., "Convert to modern English")92        provider: Provider name (e.g., "Claude 3 Haiku (Fastest, Cheapest)")93        progress_callback: Optional callback for progress updates94        max_retries: Max retry attempts per chunk95 96    Returns:97        ProcessingResult with success status and processed text98    """99 100    # Map UI provider names to actual implementations101    provider_map = {102        "Claude 3 Haiku (Fastest, Cheapest)": "openrouter/claude-haiku",103        "Claude 3.5 Sonnet (Balanced)": "openrouter/claude-sonnet",104        "GPT-4o (Best Quality)": "openrouter/gpt-4o",105        "GPT-3.5 Turbo (Fast & Cheap)": "openrouter/gpt-3.5-turbo",106        "Ollama (Free, Local - Slower)": "ollama/local",107    }108 109    provider_key = provider_map.get(provider, "openrouter/claude-haiku")110 111    # Check for API keys112    openrouter_key = os.environ.get("OPENROUTER_API_KEY")113 114    if not openrouter_key and provider_key.startswith("openrouter"):115        return ProcessingResult(116            success=False,117            processed_chunks=[],118            total_cost=0,119            total_time_seconds=0,120            total_input_tokens=0,121            total_output_tokens=0,122            error="OPENROUTER_API_KEY not found in environment variables"123        )124 125    # Process chunks126    processed_chunks = []127    total_input_tokens = 0128    total_output_tokens = 0129    total_cost = 0.0130    start_time = time.time()131 132    for i, chunk in enumerate(chunks):133        chunk_start = time.time()134 135        # Progress update136        if progress_callback:137            elapsed = time.time() - start_time138            avg_time_per_chunk = elapsed / max(i, 1)139            remaining = avg_time_per_chunk * (len(chunks) - i)140 141            progress = ProcessingProgress(142                current_chunk=i + 1,143                total_chunks=len(chunks),144                progress_percent=(i / len(chunks)) * 100,145                elapsed_time=elapsed,146                estimated_remaining=remaining,147                current_cost=total_cost148            )149            progress_callback(progress)150 151        # Process chunk with retries152        for attempt in range(max_retries):153            try:154                if provider_key.startswith("openrouter"):155                    result = _process_with_openrouter(156                        chunk.content,157                        instruction,158                        provider_key,159                        openrouter_key160                    )161                elif provider_key.startswith("ollama"):162                    result = _process_with_ollama(chunk.content, instruction)163                else:164                    raise ValueError(f"Unsupported provider: {provider_key}")165 166                processed_chunks.append(result["processed_text"])167                total_input_tokens += result.get("input_tokens", chunk.tokens)168                total_output_tokens += result.get("output_tokens", chunk.tokens)169                total_cost += result.get("cost", 0)170                break171 172            except Exception as e:173                if attempt == max_retries - 1:174                    # Final attempt failed175                    return ProcessingResult(176                        success=False,177                        processed_chunks=processed_chunks,178                        total_cost=total_cost,179                        total_time_seconds=time.time() - start_time,180                        total_input_tokens=total_input_tokens,181                        total_output_tokens=total_output_tokens,182                        error=f"Failed on chunk {i+1}/{len(chunks)}: {str(e)}"183                    )184 185                # Exponential backoff186                time.sleep(2 ** attempt)187 188    # Final progress update189    if progress_callback:190        progress = ProcessingProgress(191            current_chunk=len(chunks),192            total_chunks=len(chunks),193            progress_percent=100,194            elapsed_time=time.time() - start_time,195            estimated_remaining=0,196            current_cost=total_cost197        )198        progress_callback(progress)199 200    return ProcessingResult(201        success=True,202        processed_chunks=processed_chunks,203        total_cost=total_cost,204        total_time_seconds=time.time() - start_time,205        total_input_tokens=total_input_tokens,206        total_output_tokens=total_output_tokens207    )208 209 210def _process_with_openrouter(211    text: str,212    instruction: str,213    provider_key: str,214    api_key: str215) -> Dict[str, Any]:216    """217    Process text with OpenRouter API218 219    Args:220        text: Text to process221        instruction: Processing instruction222        provider_key: Provider key (e.g., "openrouter/claude-haiku")223        api_key: OpenRouter API key224 225    Returns:226        Dict with processed_text, input_tokens, output_tokens, cost227    """228 229    # Map provider_key to OpenRouter model name230    # OpenRouter uses format: provider/model (without version dates)231    model_map = {232        "openrouter/claude-haiku": "anthropic/claude-3-haiku",233        "openrouter/claude-sonnet": "anthropic/claude-3.5-sonnet",234        "openrouter/gpt-4o": "openai/gpt-4o",235        "openrouter/gpt-4o-mini": "openai/gpt-4o-mini",236        "openrouter/gpt-3.5-turbo": "openai/gpt-3.5-turbo",237    }238 239    model = model_map.get(provider_key, "anthropic/claude-3-haiku")240 241    # Use OpenAI-compatible client for OpenRouter242    if not HAS_OPENAI:243        raise ImportError("openai package required for OpenRouter. Install with: pip install openai")244 245    client = openai.OpenAI(246        base_url="https://openrouter.ai/api/v1",247        api_key=api_key,248    )249 250    # Create messages251    messages = [252        {253            "role": "user",254            "content": f"{instruction}\n\n{text}"255        }256    ]257 258    # Call API259    response = client.chat.completions.create(260        model=model,261        messages=messages,262        max_tokens=4000,263        extra_headers={264            "HTTP-Referer": "https://github.com/your-repo",  # Required by OpenRouter265            "X-Title": "LLM Ebook Processor"266        }267    )268 269    # Extract results270    processed_text = response.choices[0].message.content271    input_tokens = response.usage.prompt_tokens272    output_tokens = response.usage.completion_tokens273 274    # Calculate cost275    pricing = OPENROUTER_PRICING.get(model, {"input": 0, "output": 0})276    cost = (277        (input_tokens / 1_000_000) * pricing["input"] +278        (output_tokens / 1_000_000) * pricing["output"]279    )280 281    return {282        "processed_text": processed_text,283        "input_tokens": input_tokens,284        "output_tokens": output_tokens,285        "cost": cost286    }287 288 289def _process_with_ollama(text: str, instruction: str) -> Dict[str, Any]:290    """291    Process text with local Ollama292 293    Args:294        text: Text to process295        instruction: Processing instruction296 297    Returns:298        Dict with processed_text, input_tokens, output_tokens, cost299    """300 301    try:302        import requests303    except ImportError:304        raise ImportError("requests package required for Ollama")305 306    # Check if Ollama is running307    try:308        response = requests.get("http://localhost:11434/api/tags", timeout=5)309        response.raise_for_status()310    except Exception as e:311        raise ConnectionError(f"Ollama not running on localhost:11434. Please start Ollama first. Error: {e}")312 313    # Call Ollama API314    response = requests.post(315        "http://localhost:11434/api/generate",316        json={317            "model": "llama3.1",318            "prompt": f"{instruction}\n\n{text}",319            "stream": False320        }321    )322 323    response.raise_for_status()324    result = response.json()325 326    return {327        "processed_text": result["response"],328        "input_tokens": result.get("prompt_eval_count", 0),329        "output_tokens": result.get("eval_count", 0),330        "cost": 0  # Ollama is free331    }332 333 334# Export main functions335__all__ = [336    'process_with_llm',337    'ProcessingResult',338    'ProcessingProgress',339    'TextChunk',340]341