Arif commited on
Commit
e020ac8
·
1 Parent(s): 3f44a73

Updated backend. Added ML system. Both if mlx is running in debug also docker model runner if not debug mode.

Browse files
backend/app/api/v1/router.py ADDED
@@ -0,0 +1,206 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ API v1 Router - Main entry point for all endpoints
3
+ Supports dual-mode LLM (MLX or Docker Model Runner)
4
+ """
5
+ from fastapi import APIRouter, File, UploadFile, HTTPException
6
+ from typing import Optional
7
+ from datetime import datetime
8
+ import logging
9
+
10
+ from ...models.schemas import (
11
+ ChatRequest, ChatResponse,
12
+ FileUploadResponse, AnalysisRequest, AnalysisResponse,
13
+ SuggestionRequest, SuggestionsResponse, HealthResponse
14
+ )
15
+ from ...services.data_processor import DataProcessor
16
+ from ...services.analyzer import Analyzer
17
+ from ...services.ml_suggester import MLSuggester
18
+ from ...config import settings
19
+
20
+ router = APIRouter(prefix="/api/v1", tags=["v1"])
21
+ logger = logging.getLogger(__name__)
22
+
23
+ # Global service instances
24
+ llm_service = None # Will be set from main.py
25
+ data_processor: Optional[DataProcessor] = None
26
+ analyzer: Optional[Analyzer] = None
27
+ ml_suggester: Optional[MLSuggester] = None
28
+
29
+
30
+ async def init_services():
31
+ """Initialize all services on startup (except LLM - done in main.py)"""
32
+ global data_processor, analyzer, ml_suggester
33
+
34
+ logger.info("🚀 Initializing data services...")
35
+
36
+ data_processor = DataProcessor()
37
+ analyzer = Analyzer()
38
+ ml_suggester = MLSuggester()
39
+
40
+ logger.info("✅ Data services initialized")
41
+
42
+
43
+ # ============ Chat Endpoint ============
44
+
45
+ @router.post("/chat", response_model=ChatResponse)
46
+ async def chat_endpoint(request: ChatRequest):
47
+ """Chat with LLM about data analysis"""
48
+ if not llm_service or not llm_service.is_loaded:
49
+ raise HTTPException(
50
+ status_code=503,
51
+ detail="LLM not ready - still loading or connection failed"
52
+ )
53
+
54
+ try:
55
+ logger.info(f"💬 Chat request with {len(request.messages)} messages")
56
+
57
+ # Convert Pydantic ChatMessage objects to dictionaries
58
+ messages_dict = [
59
+ {"role": msg.role, "content": msg.content}
60
+ for msg in request.messages
61
+ ]
62
+
63
+ response = await llm_service.chat(
64
+ messages_dict,
65
+ request.system_prompt
66
+ )
67
+
68
+ # Determine which model name to return based on DEBUG mode
69
+ model_name = (
70
+ settings.llm_model_name_mlx if settings.debug
71
+ else settings.llm_model_name_docker
72
+ )
73
+
74
+ return ChatResponse(
75
+ response=response,
76
+ model=model_name
77
+ )
78
+ except Exception as e:
79
+ logger.error(f"❌ Chat error: {e}")
80
+ raise HTTPException(status_code=500, detail=str(e))
81
+
82
+
83
+ # ============ File Upload Endpoint ============
84
+
85
+ @router.post("/upload", response_model=FileUploadResponse)
86
+ async def upload_file(file: UploadFile = File(...)):
87
+ """Upload and process data file (CSV or Excel)"""
88
+ try:
89
+ if not data_processor:
90
+ raise HTTPException(status_code=503, detail="Service not ready")
91
+
92
+ logger.info(f"📁 Processing file: {file.filename}")
93
+ data, file_type = await data_processor.process_file(file)
94
+
95
+ # Get preview (first 5 rows)
96
+ preview = data[:5] if data else []
97
+
98
+ # Get column names
99
+ column_names = list(data.keys()) if data else []
100
+
101
+ return FileUploadResponse(
102
+ filename=file.filename,
103
+ size=len(str(data)),
104
+ rows=len(data),
105
+ columns=len(column_names),
106
+ column_names=column_names,
107
+ preview=preview,
108
+ file_type=file_type
109
+ )
110
+ except ValueError as e:
111
+ logger.error(f"Validation error: {e}")
112
+ raise HTTPException(status_code=400, detail=str(e))
113
+ except Exception as e:
114
+ logger.error(f"❌ Upload error: {e}")
115
+ raise HTTPException(status_code=500, detail=str(e))
116
+
117
+
118
+ # ============ Analysis Endpoint ============
119
+
120
+ @router.post("/analyze", response_model=AnalysisResponse)
121
+ async def analyze_data(request: AnalysisRequest):
122
+ """Analyze data - supports multiple analysis types"""
123
+ try:
124
+ if not analyzer:
125
+ raise HTTPException(status_code=503, detail="Service not ready")
126
+
127
+ logger.info(f"📊 Analysis: {request.analysis_type} on {len(request.data)} rows")
128
+
129
+ results = analyzer.analyze(
130
+ request.data,
131
+ request.analysis_type,
132
+ request.columns
133
+ )
134
+
135
+ summary = analyzer.generate_summary(results)
136
+
137
+ return AnalysisResponse(
138
+ analysis_type=request.analysis_type,
139
+ results=results,
140
+ summary=summary,
141
+ timestamp=datetime.now()
142
+ )
143
+ except ValueError as e:
144
+ logger.error(f"Invalid analysis request: {e}")
145
+ raise HTTPException(status_code=400, detail=str(e))
146
+ except Exception as e:
147
+ logger.error(f"❌ Analysis error: {e}")
148
+ raise HTTPException(status_code=500, detail=str(e))
149
+
150
+
151
+ # ============ ML Suggestions Endpoint ============
152
+
153
+ @router.post("/suggestions", response_model=SuggestionsResponse)
154
+ async def get_suggestions(request: SuggestionRequest):
155
+ """Get ML-based suggestions for data improvement"""
156
+ try:
157
+ if not ml_suggester:
158
+ raise HTTPException(status_code=503, detail="Service not ready")
159
+
160
+ logger.info(f"🤖 Generating suggestions for {len(request.data)} rows")
161
+
162
+ suggestions_list = ml_suggester.generate(
163
+ request.data,
164
+ request.analysis_context
165
+ )
166
+
167
+ return SuggestionsResponse(
168
+ suggestions=suggestions_list,
169
+ total_suggestions=len(suggestions_list),
170
+ timestamp=datetime.now()
171
+ )
172
+ except Exception as e:
173
+ logger.error(f"❌ Suggestion error: {e}")
174
+ raise HTTPException(status_code=500, detail=str(e))
175
+
176
+
177
+ # ============ Health Check ============
178
+
179
+ @router.get("/health", response_model=HealthResponse)
180
+ async def health_check():
181
+ """Health check endpoint - shows current LLM mode"""
182
+
183
+ # Determine LLM status
184
+ llm_status = "unknown"
185
+ if llm_service:
186
+ if llm_service.is_mock:
187
+ llm_status = "mock-mode"
188
+ elif llm_service.is_loaded:
189
+ llm_status = "loaded"
190
+ else:
191
+ llm_status = "failed"
192
+
193
+ # Show which mode is active
194
+ mode = "MLX (local)" if settings.debug else "Docker Model Runner"
195
+
196
+ return HealthResponse(
197
+ status="healthy",
198
+ environment=settings.fastapi_env,
199
+ service="llm-data-analyzer-backend",
200
+ llm_loaded=llm_service.is_loaded if llm_service else False,
201
+ llm_model=(
202
+ settings.llm_model_name_mlx if settings.debug
203
+ else settings.llm_model_name_docker
204
+ ),
205
+ version="0.2.0"
206
+ )
backend/app/config.py CHANGED
@@ -1,74 +1,69 @@
1
  """
2
- Configuration management for FastAPI backend
3
- Loads ALL settings from .env.local at project ROOT
 
4
  """
5
- from pydantic_settings import BaseSettings
6
- from pathlib import Path
7
- from typing import List
8
  import logging
 
 
9
 
 
10
 
11
- def find_env_file() -> Path:
12
- """Find .env.local by checking multiple paths"""
13
- possible_paths = [
14
- Path("/Users/arif/Projects/Personal/LLM-Data-Analyzer/.env.local"),
15
- Path.cwd() / ".env.local",
16
- Path(__file__).parent.parent.parent / ".env.local",
17
- Path(__file__).parent.parent / ".env.local",
18
- ]
19
-
20
- for path in possible_paths:
21
- if path.exists():
22
- return path
23
-
24
- raise FileNotFoundError(
25
- f"❌ .env.local not found! Checked:\n" +
26
- "\n".join([f" - {p}" for p in possible_paths])
27
- )
28
-
29
 
30
  class Settings(BaseSettings):
31
- """Application settings - ALL loaded from .env.local"""
32
 
33
- # API Configuration
34
  fastapi_env: str
35
- api_host: str
36
- api_port: int
37
  log_level: str
38
 
39
- # LLM Configuration
40
- llm_model_name: str
 
 
 
 
 
41
  llm_max_tokens: int
42
  llm_temperature: float
43
  llm_device: str
44
 
45
- # File Upload
46
- max_file_size: int
47
- upload_timeout: int
 
 
 
 
48
 
49
- # CORS
50
- cors_origins: List[str]
51
 
52
  class Config:
53
- env_file = str(find_env_file())
54
  case_sensitive = False
55
- extra = "ignore"
56
 
 
 
 
 
57
 
58
- settings = Settings()
 
 
 
 
 
 
 
 
 
59
 
 
 
 
 
60
 
61
- def get_logger(name: str) -> logging.Logger:
62
- """Get configured logger instance"""
63
- logger = logging.getLogger(name)
64
-
65
- if not logger.handlers:
66
- handler = logging.StreamHandler()
67
- formatter = logging.Formatter(
68
- '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
69
- )
70
- handler.setFormatter(formatter)
71
- logger.addHandler(handler)
72
-
73
- logger.setLevel(settings.log_level)
74
- return logger
 
1
  """
2
+ Configuration for LLM Data Analyzer
3
+ Supports both MLX (local) and Docker Model Runner modes
4
+ All values from .env.local - NO hardcoded defaults
5
  """
 
 
 
6
  import logging
7
+ from functools import lru_cache
8
+ from pydantic_settings import BaseSettings
9
 
10
+ logger = logging.getLogger(__name__)
11
 
12
+ # Conditional MLX import
13
+ HAS_MLX = False
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
 
15
  class Settings(BaseSettings):
16
+ """Main settings - all from .env.local"""
17
 
18
+ # ===== CORE SETTINGS =====
19
  fastapi_env: str
20
+ fastapi_debug: bool
 
21
  log_level: str
22
 
23
+ # ===== LLM MODE SELECTION =====
24
+ # True = Use MLX locally (macOS Apple Silicon)
25
+ # False = Use Docker Model Runner
26
+ debug: bool
27
+
28
+ # ===== MLX MODE (DEBUG=true) =====
29
+ llm_model_name_mlx: str
30
  llm_max_tokens: int
31
  llm_temperature: float
32
  llm_device: str
33
 
34
+ # ===== DOCKER MODEL RUNNER MODE (DEBUG=false) =====
35
+ docker_model_runner_url: str
36
+ llm_model_name_docker: str
37
+ docker_timeout: int
38
+
39
+ # ===== DATA PROCESSING =====
40
+ max_file_size_mb: int
41
 
42
+ # Hardcoded (lists can't be parsed from env vars)
43
+ supported_file_types: list = ["csv", "xlsx", "xls"]
44
 
45
  class Config:
46
+ env_file = ".env.local"
47
  case_sensitive = False
 
48
 
49
+ @lru_cache
50
+ def get_settings():
51
+ """Get cached settings from .env.local"""
52
+ return Settings()
53
 
54
+ # Check if MLX is available (only needed for DEBUG=true)
55
+ try:
56
+ import mlx.core
57
+ from mlx_lm import load
58
+ from mlx_lm.generate import generate
59
+ HAS_MLX = True
60
+ logger.info("✅ MLX libraries available")
61
+ except ImportError:
62
+ HAS_MLX = False
63
+ logger.warning("⚠️ MLX not available (will use Docker Model Runner or mock)")
64
 
65
+ settings = get_settings()
66
+
67
+ # Export both settings and MLX availability
68
+ __all__ = ["settings", "get_settings", "HAS_MLX"]
69
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
backend/app/main.py CHANGED
@@ -1,54 +1,95 @@
1
  """
2
- Main FastAPI application entry point
3
- Uses relative imports to work correctly from any directory
4
  """
 
 
 
5
  from fastapi import FastAPI
6
  from fastapi.middleware.cors import CORSMiddleware
7
- from contextlib import asynccontextmanager
8
 
9
- from .config import settings, get_logger
 
 
10
 
11
- logger = get_logger(__name__)
 
12
 
13
 
14
  @asynccontextmanager
15
  async def lifespan(app: FastAPI):
16
- """Manage app lifecycle"""
17
- # Startup
18
  logger.info("🚀 FastAPI application starting...")
19
  logger.info(f"Environment: {settings.fastapi_env}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
  yield
21
- # Shutdown
22
- logger.info("🛑 FastAPI application shutting down...")
 
23
 
24
 
25
  # Create FastAPI app
26
  app = FastAPI(
27
- title="LLM Data Analyzer API",
28
- description="Backend API for LLM-based data analysis and ML suggestions",
29
- version="0.1.0",
30
  lifespan=lifespan
31
  )
32
 
33
- # Add CORS middleware
34
  app.add_middleware(
35
  CORSMiddleware,
36
- allow_origins=settings.cors_origins,
37
  allow_credentials=True,
38
  allow_methods=["*"],
39
  allow_headers=["*"],
40
  )
41
 
 
 
42
 
43
- # Health check endpoint
44
- @app.get("/health")
45
- async def health_check():
46
- """Health check endpoint"""
47
- return {
48
- "status": "healthy",
49
- "environment": settings.fastapi_env,
50
- "service": "llm-data-analyzer-backend"
51
- }
52
 
53
 
54
  # Root endpoint
@@ -57,9 +98,10 @@ async def root():
57
  """Root endpoint with API information"""
58
  return {
59
  "service": "LLM Data Analyzer API",
60
- "version": "0.1.0",
61
  "docs_url": "/docs",
62
- "health_url": "/health"
 
63
  }
64
 
65
 
@@ -67,7 +109,7 @@ if __name__ == "__main__":
67
  import uvicorn
68
  uvicorn.run(
69
  "backend.app.main:app",
70
- host=settings.api_host,
71
- port=settings.api_port,
72
  reload=settings.fastapi_env == "development"
73
  )
 
1
  """
2
+ FastAPI Application Entry Point
3
+ Supports MLX (local) and Docker Model Runner (containerized) modes
4
  """
5
+ import logging
6
+ from contextlib import asynccontextmanager
7
+
8
  from fastapi import FastAPI
9
  from fastapi.middleware.cors import CORSMiddleware
 
10
 
11
+ from .config import settings
12
+ from .api.v1.router import router, init_services
13
+ from .services.llm_service import get_llm_service
14
 
15
+ logger = logging.getLogger(__name__)
16
+ logging.basicConfig(level=settings.log_level)
17
 
18
 
19
  @asynccontextmanager
20
  async def lifespan(app: FastAPI):
21
+ """Manage application lifecycle"""
22
+ # ===== STARTUP =====
23
  logger.info("🚀 FastAPI application starting...")
24
  logger.info(f"Environment: {settings.fastapi_env}")
25
+ logger.info(f"Debug mode: {settings.debug}")
26
+
27
+ # Get appropriate LLM service based on DEBUG flag
28
+ logger.info("🚀 Initializing LLM service...")
29
+
30
+ mlx_config = {
31
+ "model_name": settings.llm_model_name_mlx,
32
+ "max_tokens": settings.llm_max_tokens,
33
+ "temperature": settings.llm_temperature,
34
+ "device": settings.llm_device
35
+ }
36
+
37
+ docker_config = {
38
+ "model_name": settings.llm_model_name_docker,
39
+ "max_tokens": settings.llm_max_tokens,
40
+ "temperature": settings.llm_temperature,
41
+ "docker_url": settings.docker_model_runner_url,
42
+ "timeout": settings.docker_timeout
43
+ }
44
+
45
+ llm_service = get_llm_service(
46
+ debug=settings.debug,
47
+ mlx_config=mlx_config,
48
+ docker_config=docker_config
49
+ )
50
+
51
+ # Load model/initialize connection
52
+ if await llm_service.load_model():
53
+ logger.info("✅ LLM service ready")
54
+ else:
55
+ logger.warning("⚠️ LLM service initialization failed - will use fallback")
56
+
57
+ # Pass llm_service to router module
58
+ from .api.v1 import router as router_module
59
+ router_module.llm_service = llm_service
60
+
61
+ # Initialize other services
62
+ logger.info("🚀 Initializing data services...")
63
+ await init_services()
64
+ logger.info("✅ All services initialized")
65
+
66
  yield
67
+
68
+ # ===== SHUTDOWN =====
69
+ logger.info("🛑 Application shutting down...")
70
 
71
 
72
  # Create FastAPI app
73
  app = FastAPI(
74
+ title="LLM Data Analyzer",
75
+ description="MLX LLM + Data Analysis Backend (Dual-mode: MLX or Docker Model Runner)",
76
+ version="0.2.0",
77
  lifespan=lifespan
78
  )
79
 
80
+ # CORS middleware
81
  app.add_middleware(
82
  CORSMiddleware,
83
+ allow_origins=["*"],
84
  allow_credentials=True,
85
  allow_methods=["*"],
86
  allow_headers=["*"],
87
  )
88
 
89
+ # Include router
90
+ app.include_router(router)
91
 
92
+ logger.info("✅ FastAPI application configured")
 
 
 
 
 
 
 
 
93
 
94
 
95
  # Root endpoint
 
98
  """Root endpoint with API information"""
99
  return {
100
  "service": "LLM Data Analyzer API",
101
+ "version": "0.2.0",
102
  "docs_url": "/docs",
103
+ "health_url": "/api/v1/health",
104
+ "mode": "MLX (local)" if settings.debug else "Docker Model Runner"
105
  }
106
 
107
 
 
109
  import uvicorn
110
  uvicorn.run(
111
  "backend.app.main:app",
112
+ host="0.0.0.0",
113
+ port=8000,
114
  reload=settings.fastapi_env == "development"
115
  )
backend/app/services/analyzer.py CHANGED
@@ -1,14 +1,250 @@
1
- """Analysis Service - statistical and exploratory analysis"""
2
- from app.config import get_logger
 
 
 
 
 
 
 
 
 
 
 
 
3
 
4
- logger = get_logger(__name__)
5
 
6
  class Analyzer:
7
- """Performs statistical analysis on data"""
8
 
9
  def __init__(self):
10
- logger.info("Analyzer initialized")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
 
12
- async def analyze(self, data):
13
- """Analyze data"""
14
- return {"status": "Analysis coming in Phase 4"}
 
1
+ """
2
+ Data analysis service - statistical, trend, correlation analysis
3
+ """
4
+ from typing import List, Dict, Any
5
+ import logging
6
+ import statistics
7
+
8
+ try:
9
+ import numpy as np
10
+ import pandas as pd
11
+ from scipy.stats import skew, kurtosis
12
+ HAS_SCIPY = True
13
+ except ImportError:
14
+ HAS_SCIPY = False
15
 
 
16
 
17
  class Analyzer:
18
+ """Perform statistical and trend analysis on data"""
19
 
20
  def __init__(self):
21
+ self.logger = logging.getLogger(__name__)
22
+
23
+ def analyze(self, data: List[Dict[str, Any]], analysis_type: str, columns: List[str] = None) -> Dict[str, Any]:
24
+ """Dispatch to appropriate analysis method"""
25
+ analysis_type = analysis_type.lower()
26
+
27
+ if analysis_type == "statistical":
28
+ return self.statistical_analysis(data, columns)
29
+ elif analysis_type == "correlation":
30
+ return self.correlation_analysis(data, columns)
31
+ elif analysis_type == "trend":
32
+ return self.trend_analysis(data, columns)
33
+ elif analysis_type == "outliers":
34
+ return self.outlier_analysis(data, columns)
35
+ elif analysis_type == "distribution":
36
+ return self.distribution_analysis(data, columns)
37
+ elif analysis_type == "summary":
38
+ return self.summary_analysis(data)
39
+ else:
40
+ raise ValueError(f"Unknown analysis type: {analysis_type}")
41
+
42
+ def statistical_analysis(self, data: List[Dict[str, Any]], columns: List[str] = None) -> Dict[str, Any]:
43
+ """Statistical analysis - mean, median, std, min, max"""
44
+ try:
45
+ numeric_cols = columns or self._get_numeric_columns(data)
46
+ results = {
47
+ "mean": {},
48
+ "median": {},
49
+ "std_dev": {},
50
+ "min": {},
51
+ "max": {},
52
+ "count": len(data)
53
+ }
54
+
55
+ for col in numeric_cols:
56
+ values = [row[col] for row in data if row[col] is not None]
57
+ if values:
58
+ results["mean"][col] = round(statistics.mean(values), 2)
59
+ results["median"][col] = round(statistics.median(values), 2)
60
+ if len(values) > 1:
61
+ results["std_dev"][col] = round(statistics.stdev(values), 2)
62
+ results["min"][col] = round(min(values), 2)
63
+ results["max"][col] = round(max(values), 2)
64
+
65
+ return results
66
+ except Exception as e:
67
+ self.logger.error(f"Statistical analysis failed: {e}")
68
+ raise
69
+
70
+ def correlation_analysis(self, data: List[Dict[str, Any]], columns: List[str] = None) -> Dict[str, Any]:
71
+ """Correlation analysis between numeric columns"""
72
+ try:
73
+ if not HAS_SCIPY:
74
+ raise RuntimeError("pandas and scipy required for correlation analysis")
75
+
76
+ df = pd.DataFrame(data)
77
+ numeric_cols = columns or df.select_dtypes(include=[np.number]).columns.tolist()
78
+
79
+ corr_matrix = df[numeric_cols].corr().round(2)
80
+
81
+ # Find significant correlations
82
+ significant_pairs = []
83
+ for i, col1 in enumerate(numeric_cols):
84
+ for col2 in numeric_cols[i+1:]:
85
+ corr_value = corr_matrix.loc[col1, col2]
86
+ if abs(corr_value) > 0.5: # Threshold
87
+ significant_pairs.append({
88
+ "col1": col1,
89
+ "col2": col2,
90
+ "correlation": float(corr_value)
91
+ })
92
+
93
+ return {
94
+ "matrix": corr_matrix.to_dict(),
95
+ "significant_pairs": significant_pairs
96
+ }
97
+ except Exception as e:
98
+ self.logger.error(f"Correlation analysis failed: {e}")
99
+ raise
100
+
101
+ def trend_analysis(self, data: List[Dict[str, Any]], columns: List[str] = None) -> Dict[str, Any]:
102
+ """Trend analysis - increasing, decreasing, stable"""
103
+ try:
104
+ numeric_cols = columns or self._get_numeric_columns(data)
105
+ trends = {}
106
+ trend_strength = {}
107
+
108
+ for col in numeric_cols:
109
+ values = [row[col] for row in data if row[col] is not None]
110
+ if len(values) > 2:
111
+ # Simple trend: compare first half vs second half
112
+ mid = len(values) // 2
113
+ first_half_avg = statistics.mean(values[:mid])
114
+ second_half_avg = statistics.mean(values[mid:])
115
+
116
+ if second_half_avg > first_half_avg * 1.05:
117
+ trends[col] = "increasing"
118
+ strength = (second_half_avg - first_half_avg) / first_half_avg
119
+ elif second_half_avg < first_half_avg * 0.95:
120
+ trends[col] = "decreasing"
121
+ strength = (first_half_avg - second_half_avg) / first_half_avg
122
+ else:
123
+ trends[col] = "stable"
124
+ strength = 0.0
125
+
126
+ trend_strength[col] = round(strength, 2)
127
+
128
+ return {
129
+ "trends": trends,
130
+ "trend_strength": trend_strength
131
+ }
132
+ except Exception as e:
133
+ self.logger.error(f"Trend analysis failed: {e}")
134
+ raise
135
+
136
+ def outlier_analysis(self, data: List[Dict[str, Any]], columns: List[str] = None) -> Dict[str, Any]:
137
+ """Outlier detection using IQR method"""
138
+ try:
139
+ numeric_cols = columns or self._get_numeric_columns(data)
140
+ outliers = {}
141
+ total_outliers = 0
142
+
143
+ for col in numeric_cols:
144
+ values = sorted([row[col] for row in data if row[col] is not None])
145
+ if len(values) > 4:
146
+ q1 = values[len(values) // 4]
147
+ q3 = values[3 * len(values) // 4]
148
+ iqr = q3 - q1
149
+ lower_bound = q1 - 1.5 * iqr
150
+ upper_bound = q3 + 1.5 * iqr
151
+
152
+ col_outliers = [v for v in values if v < lower_bound or v > upper_bound]
153
+ outliers[col] = col_outliers
154
+ total_outliers += len(col_outliers)
155
+
156
+ return {
157
+ "outliers": outliers,
158
+ "outlier_count": total_outliers,
159
+ "outlier_percentage": round((total_outliers / len(data)) * 100, 2) if data else 0
160
+ }
161
+ except Exception as e:
162
+ self.logger.error(f"Outlier analysis failed: {e}")
163
+ raise
164
+
165
+ def distribution_analysis(self, data: List[Dict[str, Any]], columns: List[str] = None) -> Dict[str, Any]:
166
+ """Distribution analysis - skewness, kurtosis"""
167
+ try:
168
+ if not HAS_SCIPY:
169
+ return {"error": "scipy required for distribution analysis"}
170
+
171
+ numeric_cols = columns or self._get_numeric_columns(data)
172
+ distributions = {}
173
+ skewness = {}
174
+ kurt = {}
175
+
176
+ for col in numeric_cols:
177
+ values = [row[col] for row in data if row[col] is not None]
178
+ if len(values) > 2:
179
+ distributions[col] = {
180
+ "min": round(min(values), 2),
181
+ "max": round(max(values), 2),
182
+ "range": round(max(values) - min(values), 2)
183
+ }
184
+ skewness[col] = round(float(skew(values)), 2)
185
+ kurt[col] = round(float(kurtosis(values)), 2)
186
+
187
+ return {
188
+ "distributions": distributions,
189
+ "skewness": skewness,
190
+ "kurtosis": kurt
191
+ }
192
+ except Exception as e:
193
+ self.logger.error(f"Distribution analysis failed: {e}")
194
+ raise
195
+
196
+ def summary_analysis(self, data: List[Dict[str, Any]]) -> Dict[str, Any]:
197
+ """Summary of data"""
198
+ try:
199
+ if not data:
200
+ return {"error": "No data"}
201
+
202
+ cols = list(data.keys())
203
+ return {
204
+ "total_rows": len(data),
205
+ "total_columns": len(cols),
206
+ "columns": cols,
207
+ "data_types": self._infer_types(data)
208
+ }
209
+ except Exception as e:
210
+ self.logger.error(f"Summary analysis failed: {e}")
211
+ raise
212
+
213
+ @staticmethod
214
+ def _get_numeric_columns(data: List[Dict[str, Any]]) -> List[str]:
215
+ """Get numeric columns"""
216
+ if not data:
217
+ return []
218
+
219
+ numeric = []
220
+ for key in data.keys():
221
+ try:
222
+ for row in data:
223
+ if row[key] is not None:
224
+ float(row[key])
225
+ numeric.append(key)
226
+ except (ValueError, TypeError):
227
+ pass
228
+ return numeric
229
+
230
+ @staticmethod
231
+ def _infer_types(data: List[Dict[str, Any]]) -> Dict[str, str]:
232
+ """Infer column data types"""
233
+ types = {}
234
+ if not data:
235
+ return types
236
+
237
+ for key in data.keys():
238
+ try:
239
+ for row in data:
240
+ if row[key] is not None:
241
+ float(row[key])
242
+ types[key] = "numeric"
243
+ except (ValueError, TypeError):
244
+ types[key] = "string"
245
+
246
+ return types
247
 
248
+ def generate_summary(self, results: Dict[str, Any]) -> str:
249
+ """Generate human-readable summary"""
250
+ return f"Analysis completed with {len(results)} metrics."
backend/app/services/data_processor.py CHANGED
@@ -1,14 +1,120 @@
1
- """Data Processing Service - handles file uploads and data parsing"""
2
- from app.config import get_logger
 
 
 
 
 
 
 
 
 
 
 
 
3
 
4
- logger = get_logger(__name__)
5
 
6
  class DataProcessor:
7
- """Handles data file processing (CSV, XLS, XLSX)"""
8
 
9
  def __init__(self):
10
- logger.info("DataProcessor initialized")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
 
12
- async def process_file(self, file_path: str):
13
- """Process uploaded file"""
14
- return {"status": "File processing coming in Phase 3"}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Data file processing - CSV and Excel support
3
+ """
4
+ import csv
5
+ import io
6
+ from typing import List, Dict, Any
7
+ from fastapi import UploadFile
8
+ import logging
9
+
10
+ try:
11
+ import openpyxl
12
+ HAS_OPENPYXL = True
13
+ except ImportError:
14
+ HAS_OPENPYXL = False
15
 
 
16
 
17
  class DataProcessor:
18
+ """Process uploaded data files"""
19
 
20
  def __init__(self):
21
+ self.logger = logging.getLogger(__name__)
22
+ self.max_file_size = 10 * 1024 * 1024 # 10 MB
23
+
24
+ async def process_file(self, file: UploadFile) -> tuple[List[Dict[str, Any]], str]:
25
+ """Process uploaded file - returns (data, file_type)"""
26
+ content = await file.read()
27
+
28
+ if len(content) > self.max_file_size:
29
+ raise ValueError(f"File too large. Max: {self.max_file_size / 1024 / 1024:.1f} MB")
30
+
31
+ if file.filename.endswith('.csv'):
32
+ return self._process_csv(content), 'csv'
33
+ elif file.filename.endswith('.xlsx') or file.filename.endswith('.xls'):
34
+ return self._process_excel(content), 'excel'
35
+ else:
36
+ raise ValueError(f"Unsupported file format: {file.filename}")
37
+
38
+ def _process_csv(self, content: bytes) -> List[Dict[str, Any]]:
39
+ """Process CSV file"""
40
+ try:
41
+ text_content = content.decode('utf-8')
42
+ reader = csv.DictReader(io.StringIO(text_content))
43
+ data = []
44
+ for row in reader:
45
+ # Convert numeric strings to numbers
46
+ processed_row = {}
47
+ for key, value in row.items():
48
+ processed_row[key] = self._try_convert_to_number(value)
49
+ data.append(processed_row)
50
+
51
+ if not data:
52
+ raise ValueError("CSV file is empty")
53
+
54
+ self.logger.info(f"✅ Processed CSV: {len(data)} rows")
55
+ return data
56
+ except Exception as e:
57
+ self.logger.error(f"❌ CSV processing failed: {e}")
58
+ raise
59
+
60
+ def _process_excel(self, content: bytes) -> List[Dict[str, Any]]:
61
+ """Process Excel file"""
62
+ try:
63
+ if not HAS_OPENPYXL:
64
+ raise RuntimeError("openpyxl not installed. Install with: uv add openpyxl")
65
+
66
+ workbook = openpyxl.load_workbook(io.BytesIO(content))
67
+ sheet = workbook.active
68
+
69
+ # Get headers
70
+ headers = [cell.value for cell in sheet]
71
+
72
+ # Get data
73
+ data = []
74
+ for row in sheet.iter_rows(min_row=2, values_only=True):
75
+ if any(cell is not None for cell in row):
76
+ row_dict = {}
77
+ for header, value in zip(headers, row):
78
+ row_dict[header] = value
79
+ data.append(row_dict)
80
+
81
+ if not data:
82
+ raise ValueError("Excel file is empty")
83
+
84
+ self.logger.info(f"✅ Processed Excel: {len(data)} rows")
85
+ return data
86
+ except Exception as e:
87
+ self.logger.error(f"❌ Excel processing failed: {e}")
88
+ raise
89
+
90
+ @staticmethod
91
+ def _try_convert_to_number(value: str) -> Any:
92
+ """Try converting string to int or float"""
93
+ if value is None or value == "":
94
+ return None
95
+
96
+ try:
97
+ if "." in str(value):
98
+ return float(value)
99
+ else:
100
+ return int(value)
101
+ except (ValueError, TypeError):
102
+ return value
103
 
104
+ @staticmethod
105
+ def get_numeric_columns(data: List[Dict[str, Any]]) -> List[str]:
106
+ """Get columns that contain numeric data"""
107
+ if not data:
108
+ return []
109
+
110
+ numeric_cols = []
111
+ for key in data.keys():
112
+ try:
113
+ for row in data:
114
+ if row[key] is not None:
115
+ float(row[key])
116
+ numeric_cols.append(key)
117
+ except (ValueError, TypeError):
118
+ pass
119
+
120
+ return numeric_cols
backend/app/services/llm_service.py CHANGED
@@ -1,17 +1,281 @@
1
- """LLM Service - handles MLX Llama 2 inference"""
2
- from app.config import get_logger
 
 
 
 
 
 
 
 
 
3
 
4
- logger = get_logger(__name__)
5
 
6
- class LLMService:
7
- """Wrapper around MLX LLM for convenient inference"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8
 
9
- def __init__(self):
10
- """Initialize LLM Service - actual LLM loading in Phase 2"""
11
- self.llm = None
12
- logger.info("LLMService initialized (Phase 2 will load actual model)")
13
 
14
- async def chat(self, message: str, history: list = None) -> str:
15
- """Process user message and return LLM response"""
16
- logger.info(f"Chat request: {message}")
17
- return "LLM response will be here in Phase 2"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Dual-mode LLM Service
3
+ - DEBUG=true: Uses MLX with Apple Silicon GPU
4
+ - DEBUG=false: Uses Docker Model Runner (OpenAI-compatible API)
5
+ - Fallback: Mock mode if neither available
6
+ """
7
+ import asyncio
8
+ import logging
9
+ from abc import ABC, abstractmethod
10
+ from typing import List, Optional
11
+ import httpx
12
 
13
+ logger = logging.getLogger(__name__)
14
 
15
+ # Import MLX conditionally
16
+ try:
17
+ from mlx_lm import load
18
+ from mlx_lm.generate import generate
19
+ HAS_MLX = True
20
+ except ImportError:
21
+ HAS_MLX = False
22
+
23
+
24
+ class BaseLLMService(ABC):
25
+ """Abstract base class for LLM services"""
26
+
27
+ def __init__(self, model_name: str, max_tokens: int, temperature: float):
28
+ self.model_name = model_name
29
+ self.max_tokens = max_tokens
30
+ self.temperature = temperature
31
+ self.is_loaded = False
32
+ self.is_mock = False
33
+ self.logger = logging.getLogger(__name__)
34
+
35
+ @abstractmethod
36
+ async def load_model(self) -> bool:
37
+ """Load/initialize the model"""
38
+ pass
39
+
40
+ @abstractmethod
41
+ async def generate(self, prompt: str) -> str:
42
+ """Generate text from prompt"""
43
+ pass
44
+
45
+ async def chat(self, messages: List[dict], system_prompt: str = None) -> str:
46
+ """Chat interface"""
47
+ prompt = self._build_prompt(messages, system_prompt)
48
+ return await self.generate(prompt)
49
+
50
+ def _build_prompt(self, messages: List[dict], system_prompt: str = None) -> str:
51
+ """Build prompt from chat messages"""
52
+ prompt_parts = []
53
+
54
+ if system_prompt:
55
+ prompt_parts.append(f"System: {system_prompt}\n\n")
56
+
57
+ for msg in messages:
58
+ role = msg.get("role", "user")
59
+ content = msg.get("content", "")
60
+ prompt_parts.append(f"{role.capitalize()}: {content}\n")
61
+
62
+ prompt_parts.append("Assistant: ")
63
+ return "".join(prompt_parts)
64
+
65
+
66
+ class LLMServiceMLX(BaseLLMService):
67
+ """MLX implementation for Apple Silicon (DEBUG=true)"""
68
+
69
+ def __init__(self, model_name: str, max_tokens: int, temperature: float, device: str):
70
+ super().__init__(model_name, max_tokens, temperature)
71
+ self.device = device
72
+ self.model = None
73
+ self.tokenizer = None
74
+
75
+ async def load_model(self) -> bool:
76
+ """Load MLX model"""
77
+ if self.is_loaded:
78
+ return True
79
+
80
+ if not HAS_MLX:
81
+ self.logger.error("❌ MLX not available")
82
+ return False
83
+
84
+ try:
85
+ self.logger.info(f"🔄 Loading MLX model: {self.model_name}")
86
+ loop = asyncio.get_event_loop()
87
+ self.model, self.tokenizer = await loop.run_in_executor(
88
+ None,
89
+ self._load_model_sync
90
+ )
91
+ self.is_loaded = True
92
+ self.logger.info(f"✅ MLX model loaded: {self.model_name}")
93
+ return True
94
+ except Exception as e:
95
+ self.logger.error(f"❌ MLX model loading failed: {e}")
96
+ return False
97
+
98
+ def _load_model_sync(self):
99
+ """Synchronous MLX model loading"""
100
+ if not HAS_MLX:
101
+ raise RuntimeError("MLX not installed")
102
+
103
+ self.logger.info("🔄 Starting model download/load...")
104
+ model, tokenizer = load(self.model_name)
105
+ self.logger.info("✅ Model download/load complete")
106
+ return model, tokenizer
107
+
108
+ async def generate(self, prompt: str) -> str:
109
+ """Generate with MLX"""
110
+ if not self.is_loaded:
111
+ raise RuntimeError("Model not loaded")
112
+
113
+ try:
114
+ loop = asyncio.get_event_loop()
115
+ response = await loop.run_in_executor(
116
+ None,
117
+ self._generate_sync,
118
+ prompt
119
+ )
120
+ return response
121
+ except Exception as e:
122
+ self.logger.error(f"❌ MLX generation failed: {e}")
123
+ raise
124
+
125
+ def _generate_sync(self, prompt: str) -> str:
126
+ """Synchronous text generation with MLX"""
127
+ response = generate(
128
+ model=self.model,
129
+ tokenizer=self.tokenizer,
130
+ prompt=prompt,
131
+ max_tokens=self.max_tokens,
132
+ temperature=self.temperature,
133
+ verbose=False
134
+ )
135
+ return response
136
+
137
+
138
+ class LLMServiceDockerModelRunner(BaseLLMService):
139
+ """Docker Model Runner implementation (DEBUG=false)"""
140
+
141
+ def __init__(self, model_name: str, max_tokens: int, temperature: float, docker_url: str, timeout: int = 300):
142
+ super().__init__(model_name, max_tokens, temperature)
143
+ self.docker_url = docker_url
144
+ self.timeout = timeout
145
+ self.client = None
146
+
147
+ async def load_model(self) -> bool:
148
+ """Initialize Docker Model Runner connection"""
149
+ if self.is_loaded:
150
+ return True
151
+
152
+ try:
153
+ self.logger.info(f"🔄 Connecting to Docker Model Runner: {self.docker_url}")
154
+ # Create async HTTP client
155
+ self.client = httpx.AsyncClient(timeout=self.timeout)
156
+
157
+ # Test connection with health check
158
+ response = await self.client.get(f"{self.docker_url}/models")
159
+
160
+ if response.status_code == 200:
161
+ self.is_loaded = True
162
+ self.logger.info(f"✅ Docker Model Runner connected")
163
+ return True
164
+ else:
165
+ self.logger.error(f"❌ Docker Model Runner returned {response.status_code}")
166
+ return False
167
+ except Exception as e:
168
+ self.logger.error(f"❌ Docker Model Runner connection failed: {e}")
169
+ return False
170
+
171
+ async def generate(self, prompt: str) -> str:
172
+ """Generate with Docker Model Runner (OpenAI-compatible API)"""
173
+ if not self.is_loaded:
174
+ raise RuntimeError("Docker Model Runner not connected")
175
+
176
+ try:
177
+ payload = {
178
+ "model": self.model_name,
179
+ "messages": [{"role": "user", "content": prompt}],
180
+ "temperature": self.temperature,
181
+ "max_tokens": self.max_tokens,
182
+ }
183
+
184
+ response = await self.client.post(
185
+ f"{self.docker_url}/chat/completions",
186
+ json=payload
187
+ )
188
+
189
+ if response.status_code == 200:
190
+ result = response.json()
191
+ return result["choices"]["message"]["content"]
192
+ else:
193
+ self.logger.error(f"❌ Docker Model Runner error: {response.text}")
194
+ raise RuntimeError(f"Model Runner error: {response.status_code}")
195
+ except Exception as e:
196
+ self.logger.error(f"❌ Docker Model Runner generation failed: {e}")
197
+ raise
198
+
199
+ async def __aenter__(self):
200
+ return self
201
+
202
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
203
+ if self.client:
204
+ await self.client.aclose()
205
+
206
+
207
+ class LLMServiceMock(BaseLLMService):
208
+ """Mock implementation as fallback"""
209
+
210
+ def __init__(self, model_name: str, max_tokens: int, temperature: float):
211
+ super().__init__(model_name, max_tokens, temperature)
212
+ self.is_mock = True
213
+
214
+ async def load_model(self) -> bool:
215
+ """Mock loading"""
216
+ self.logger.warning("⚠️ Using MOCK mode (no real LLM available)")
217
+ self.is_loaded = True
218
+ return True
219
+
220
+ async def generate(self, prompt: str) -> str:
221
+ """Generate mock response"""
222
+ return self._generate_mock_response(prompt)
223
+
224
+ def _generate_mock_response(self, prompt: str) -> str:
225
+ """Generate intelligent mock responses"""
226
+ prompt_lower = prompt.lower()
227
+
228
+ if "hello" in prompt_lower or "hi" in prompt_lower:
229
+ return "Hello! I'm running in mock mode (no LLM available). I can still help you analyze CSV and Excel files!"
230
+ elif "analyze" in prompt_lower or "data" in prompt_lower:
231
+ return "I can analyze your data with statistical analysis, trend detection, outlier detection, and correlation matrices."
232
+ elif "what can" in prompt_lower or "help" in prompt_lower:
233
+ return "I can help with: 1) Chatting, 2) Uploading files (CSV/Excel), 3) Statistical analysis, 4) Trend detection, 5) Anomaly detection."
234
+ elif "machine learning" in prompt_lower:
235
+ return "Machine learning is about creating algorithms that can learn from data and make predictions without being explicitly programmed."
236
+ else:
237
+ return f"Mock response: I processed your prompt about '{prompt[:40]}...' - please note I'm in mock mode with no real LLM."
238
+
239
+
240
+ def get_llm_service(debug: bool, mlx_config: dict = None, docker_config: dict = None) -> BaseLLMService:
241
+ """
242
+ Factory function to get appropriate LLM service
243
+
244
+ Args:
245
+ debug: If True, use MLX; if False, use Docker Model Runner
246
+ mlx_config: Config dict for MLX (model_name, max_tokens, temperature, device)
247
+ docker_config: Config dict for Docker Model Runner (model_name, max_tokens, temperature, url, timeout)
248
 
249
+ Returns:
250
+ Appropriate LLM service instance
251
+ """
 
252
 
253
+ if debug:
254
+ # Try MLX first
255
+ if HAS_MLX:
256
+ config = mlx_config or {
257
+ "model_name": "mlx-community/Llama-3.2-3B-Instruct-4bit",
258
+ "max_tokens": 512,
259
+ "temperature": 0.7,
260
+ "device": "auto"
261
+ }
262
+ logger.info("📌 Mode: MLX (DEBUG=true)")
263
+ return LLMServiceMLX(**config)
264
+ else:
265
+ logger.warning("⚠️ MLX not available, falling back to mock")
266
+ return LLMServiceMock(
267
+ model_name="mock-mlx",
268
+ max_tokens=512,
269
+ temperature=0.7
270
+ )
271
+ else:
272
+ # Use Docker Model Runner
273
+ config = docker_config or {
274
+ "model_name": "Llama-3.2-3B-Instruct",
275
+ "max_tokens": 512,
276
+ "temperature": 0.7,
277
+ "docker_url": "http://model-runner.docker.internal/engines/llama.cpp/v1",
278
+ "timeout": 300
279
+ }
280
+ logger.info("📌 Mode: Docker Model Runner (DEBUG=false)")
281
+ return LLMServiceDockerModelRunner(**config)
backend/app/services/ml_suggester.py CHANGED
@@ -1,14 +1,151 @@
1
- """ML Suggestion Service - suggests models and business problems"""
2
- from app.config import get_logger
 
 
 
 
3
 
4
- logger = get_logger(__name__)
5
 
6
  class MLSuggester:
7
- """Suggests appropriate ML models and identifies business problems"""
8
 
9
  def __init__(self):
10
- logger.info("MLSuggester initialized")
11
 
12
- async def suggest_models(self, data_summary: dict):
13
- """Suggest ML models based on data characteristics"""
14
- return {"suggestions": []}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ ML suggestions - anomaly detection and insights
3
+ """
4
+ from typing import List, Dict, Any
5
+ import logging
6
+ import statistics
7
 
 
8
 
9
  class MLSuggester:
10
+ """Generate ML-based suggestions from data"""
11
 
12
  def __init__(self):
13
+ self.logger = logging.getLogger(__name__)
14
 
15
+ def generate(self, data: List[Dict[str, Any]], context: str = None) -> List[Dict[str, Any]]:
16
+ """Generate ML suggestions"""
17
+ suggestions = []
18
+
19
+ if not data:
20
+ return suggestions
21
+
22
+ # Detect missing values
23
+ suggestions.extend(self._check_missing_values(data))
24
+
25
+ # Detect outliers
26
+ suggestions.extend(self._detect_outliers(data))
27
+
28
+ # Detect imbalances
29
+ suggestions.extend(self._detect_imbalances(data))
30
+
31
+ # Detect data quality issues
32
+ suggestions.extend(self._detect_quality_issues(data))
33
+
34
+ # Sort by confidence
35
+ suggestions.sort(key=lambda x: x["confidence"], reverse=True)
36
+
37
+ return suggestions[:10] # Top 10 suggestions
38
+
39
+ def _check_missing_values(self, data: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
40
+ """Check for missing values"""
41
+ suggestions = []
42
+
43
+ for col in data.keys():
44
+ missing_count = sum(1 for row in data if row[col] is None or row[col] == "")
45
+ if missing_count > 0:
46
+ percentage = (missing_count / len(data)) * 100
47
+
48
+ if percentage > 50:
49
+ suggestions.append({
50
+ "title": f"High Missing Values in {col}",
51
+ "description": f"{percentage:.1f}% of {col} is missing. Consider data imputation or removal.",
52
+ "confidence": min(0.95, percentage / 100),
53
+ "action": "impute_or_remove",
54
+ "category": "data_quality"
55
+ })
56
+ elif percentage > 10:
57
+ suggestions.append({
58
+ "title": f"Missing Values in {col}",
59
+ "description": f"{percentage:.1f}% of {col} is missing. Consider handling these values.",
60
+ "confidence": min(0.8, percentage / 100),
61
+ "action": "handle_missing",
62
+ "category": "data_quality"
63
+ })
64
+
65
+ return suggestions
66
+
67
+ def _detect_outliers(self, data: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
68
+ """Detect outliers"""
69
+ suggestions = []
70
+ numeric_cols = self._get_numeric_columns(data)
71
+
72
+ for col in numeric_cols:
73
+ values = [row[col] for row in data if row[col] is not None]
74
+ if len(values) > 4:
75
+ try:
76
+ q1 = sorted(values)[len(values) // 4]
77
+ q3 = sorted(values)[3 * len(values) // 4]
78
+ iqr = q3 - q1
79
+ outliers = [v for v in values if v < q1 - 1.5 * iqr or v > q3 + 1.5 * iqr]
80
+
81
+ if outliers:
82
+ outlier_percentage = (len(outliers) / len(values)) * 100
83
+ if outlier_percentage > 5:
84
+ suggestions.append({
85
+ "title": f"Outliers Detected in {col}",
86
+ "description": f"{len(outliers)} outlier(s) ({outlier_percentage:.1f}%) found. Review and handle appropriately.",
87
+ "confidence": min(0.85, outlier_percentage / 100),
88
+ "action": "review_outliers",
89
+ "category": "anomaly"
90
+ })
91
+ except:
92
+ pass
93
+
94
+ return suggestions
95
+
96
+ def _detect_imbalances(self, data: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
97
+ """Detect data imbalances"""
98
+ suggestions = []
99
+
100
+ for col in data.keys():
101
+ values = [row[col] for row in data if row[col] is not None]
102
+ if len(set(values)) < len(values) * 0.1: # Few unique values
103
+ from collections import Counter
104
+ counts = Counter(values)
105
+ most_common_count = counts.most_common(1)
106
+ imbalance_ratio = most_common_count / len(values)
107
+
108
+ if imbalance_ratio > 0.8:
109
+ suggestions.append({
110
+ "title": f"Class Imbalance in {col}",
111
+ "description": f"One value appears {imbalance_ratio*100:.1f}% of the time. Data is highly imbalanced.",
112
+ "confidence": min(0.9, imbalance_ratio),
113
+ "action": "rebalance_data",
114
+ "category": "pattern"
115
+ })
116
+
117
+ return suggestions
118
+
119
+ def _detect_quality_issues(self, data: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
120
+ """Detect data quality issues"""
121
+ suggestions = []
122
+
123
+ # Check for duplicates
124
+ if len(data) != len(set(str(row) for row in data)):
125
+ duplicate_count = len(data) - len(set(str(row) for row in data))
126
+ suggestions.append({
127
+ "title": "Duplicate Records Found",
128
+ "description": f"{duplicate_count} duplicate row(s) detected. Consider deduplication.",
129
+ "confidence": 0.9,
130
+ "action": "deduplicate",
131
+ "category": "data_quality"
132
+ })
133
+
134
+ return suggestions
135
+
136
+ @staticmethod
137
+ def _get_numeric_columns(data: List[Dict[str, Any]]) -> List[str]:
138
+ """Get numeric columns"""
139
+ if not data:
140
+ return []
141
+
142
+ numeric = []
143
+ for key in data.keys():
144
+ try:
145
+ for row in data:
146
+ if row[key] is not None:
147
+ float(row[key])
148
+ numeric.append(key)
149
+ except (ValueError, TypeError):
150
+ pass
151
+ return numeric
pyproject.toml CHANGED
@@ -10,28 +10,27 @@ dependencies = [
10
  # Backend - FastAPI & Server
11
  "fastapi==0.109.0",
12
  "uvicorn[standard]==0.27.0",
13
-
14
  # Data Processing
15
  "pandas==2.1.4",
16
  "numpy>=1.26.0,<2.0.0",
17
  "scikit-learn==1.3.2",
18
-
19
  # File Handling & Parsing
20
  "openpyxl==3.1.5",
21
  "python-multipart==0.0.6",
22
  "aiofiles==23.2.1",
23
-
24
  # Validation & Configuration
25
  "pydantic==2.5.0",
26
  "pydantic-settings==2.1.0",
27
  "python-dotenv==1.0.0",
28
-
29
  # Visualization
30
  "plotly==5.18.0",
31
-
32
  # Frontend - Streamlit
33
  "streamlit==1.28.1",
34
  "requests==2.31.0",
 
 
 
 
35
  ]
36
 
37
  [project.optional-dependencies]
 
10
  # Backend - FastAPI & Server
11
  "fastapi==0.109.0",
12
  "uvicorn[standard]==0.27.0",
 
13
  # Data Processing
14
  "pandas==2.1.4",
15
  "numpy>=1.26.0,<2.0.0",
16
  "scikit-learn==1.3.2",
 
17
  # File Handling & Parsing
18
  "openpyxl==3.1.5",
19
  "python-multipart==0.0.6",
20
  "aiofiles==23.2.1",
 
21
  # Validation & Configuration
22
  "pydantic==2.5.0",
23
  "pydantic-settings==2.1.0",
24
  "python-dotenv==1.0.0",
 
25
  # Visualization
26
  "plotly==5.18.0",
 
27
  # Frontend - Streamlit
28
  "streamlit==1.28.1",
29
  "requests==2.31.0",
30
+ "mlx-lm>=0.28.4",
31
+ "mlx>=0.30.0",
32
+ "scipy>=1.16.3",
33
+ "httpx>=0.28.1",
34
  ]
35
 
36
  [project.optional-dependencies]
uv.lock CHANGED
@@ -447,6 +447,15 @@ wheels = [
447
  { url = "https://files.pythonhosted.org/packages/e5/80/ddbf524c6169072ab5e8dd4e106d4eb482bf920da1996dde9f308f90aa8c/fastapi-0.109.0-py3-none-any.whl", hash = "sha256:8c77515984cd8e8cfeb58364f8cc7a28f0692088475e2614f7bf03275eba9093", size = 92049, upload-time = "2024-01-11T15:36:31.271Z" },
448
  ]
449
 
 
 
 
 
 
 
 
 
 
450
  [[package]]
451
  name = "frozenlist"
452
  version = "1.8.0"
@@ -552,6 +561,15 @@ wheels = [
552
  { url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409, upload-time = "2025-10-06T05:38:16.721Z" },
553
  ]
554
 
 
 
 
 
 
 
 
 
 
555
  [[package]]
556
  name = "gitdb"
557
  version = "4.0.12"
@@ -585,6 +603,48 @@ wheels = [
585
  { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
586
  ]
587
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
588
  [[package]]
589
  name = "httptools"
590
  version = "0.7.1"
@@ -621,6 +681,40 @@ wheels = [
621
  { url = "https://files.pythonhosted.org/packages/53/cf/878f3b91e4e6e011eff6d1fa9ca39f7eb17d19c9d7971b04873734112f30/httptools-0.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:cfabda2a5bb85aa2a904ce06d974a3f30fb36cc63d7feaddec05d2050acede96", size = 88205, upload-time = "2025-10-10T03:55:00.389Z" },
622
  ]
623
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
624
  [[package]]
625
  name = "idna"
626
  version = "3.11"
@@ -706,6 +800,9 @@ source = { virtual = "." }
706
  dependencies = [
707
  { name = "aiofiles" },
708
  { name = "fastapi" },
 
 
 
709
  { name = "numpy" },
710
  { name = "openpyxl" },
711
  { name = "pandas" },
@@ -716,6 +813,7 @@ dependencies = [
716
  { name = "python-multipart" },
717
  { name = "requests" },
718
  { name = "scikit-learn" },
 
719
  { name = "streamlit" },
720
  { name = "uvicorn", extra = ["standard"] },
721
  ]
@@ -734,6 +832,9 @@ requires-dist = [
734
  { name = "aiofiles", specifier = "==23.2.1" },
735
  { name = "black", marker = "extra == 'dev'", specifier = "==23.12.0" },
736
  { name = "fastapi", specifier = "==0.109.0" },
 
 
 
737
  { name = "numpy", specifier = ">=1.26.0,<2.0.0" },
738
  { name = "openpyxl", specifier = "==3.1.5" },
739
  { name = "pandas", specifier = "==2.1.4" },
@@ -748,6 +849,7 @@ requires-dist = [
748
  { name = "requests", specifier = "==2.31.0" },
749
  { name = "ruff", marker = "extra == 'dev'", specifier = "==0.1.11" },
750
  { name = "scikit-learn", specifier = "==1.3.2" },
 
751
  { name = "streamlit", specifier = "==1.28.1" },
752
  { name = "uvicorn", extras = ["standard"], specifier = "==0.27.0" },
753
  ]
@@ -848,6 +950,64 @@ wheels = [
848
  { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
849
  ]
850
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
851
  [[package]]
852
  name = "multidict"
853
  version = "6.7.0"
@@ -1532,6 +1692,98 @@ wheels = [
1532
  { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" },
1533
  ]
1534
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1535
  [[package]]
1536
  name = "requests"
1537
  version = "2.31.0"
@@ -1692,6 +1944,28 @@ wheels = [
1692
  { url = "https://files.pythonhosted.org/packages/a9/9b/770da4f22ea69fdd19ac6cb183f02abf8fc6f8d16a9dc00dfaf667649ee7/ruff-0.1.11-py3-none-win_arm64.whl", hash = "sha256:97ce4d752f964ba559c7023a86e5f8e97f026d511e48013987623915431c7ea9", size = 6827536, upload-time = "2024-01-02T22:59:08.394Z" },
1693
  ]
1694
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1695
  [[package]]
1696
  name = "scikit-learn"
1697
  version = "1.3.2"
@@ -1787,6 +2061,62 @@ wheels = [
1787
  { url = "https://files.pythonhosted.org/packages/64/47/a494741db7280eae6dc033510c319e34d42dd41b7ac0c7ead39354d1a2b5/scipy-1.16.3-cp314-cp314t-win_arm64.whl", hash = "sha256:21d9d6b197227a12dcbf9633320a4e34c6b0e51c57268df255a0942983bac562", size = 26464127, upload-time = "2025-10-28T17:38:11.34Z" },
1788
  ]
1789
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1790
  [[package]]
1791
  name = "six"
1792
  version = "1.17.0"
@@ -1869,6 +2199,31 @@ wheels = [
1869
  { url = "https://files.pythonhosted.org/packages/32/d5/f9a850d79b0851d1d4ef6456097579a9005b31fea68726a4ae5f2d82ddd9/threadpoolctl-3.6.0-py3-none-any.whl", hash = "sha256:43a0b8fd5a2928500110039e43a5eed8480b918967083ea48dc3ab9f13c4a7fb", size = 18638, upload-time = "2025-03-13T13:49:21.846Z" },
1870
  ]
1871
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1872
  [[package]]
1873
  name = "toml"
1874
  version = "0.10.2"
@@ -1946,6 +2301,39 @@ wheels = [
1946
  { url = "https://files.pythonhosted.org/packages/5e/4f/e1f65e8f8c76d73658b33d33b81eed4322fb5085350e4328d5c956f0c8f9/tornado-6.5.2-cp39-abi3-win_arm64.whl", hash = "sha256:d6c33dc3672e3a1f3618eb63b7ef4683a7688e7b9e6e8f0d9aa5726360a004af", size = 444456, upload-time = "2025-08-08T18:26:59.207Z" },
1947
  ]
1948
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1949
  [[package]]
1950
  name = "typing-extensions"
1951
  version = "4.15.0"
 
447
  { url = "https://files.pythonhosted.org/packages/e5/80/ddbf524c6169072ab5e8dd4e106d4eb482bf920da1996dde9f308f90aa8c/fastapi-0.109.0-py3-none-any.whl", hash = "sha256:8c77515984cd8e8cfeb58364f8cc7a28f0692088475e2614f7bf03275eba9093", size = 92049, upload-time = "2024-01-11T15:36:31.271Z" },
448
  ]
449
 
450
+ [[package]]
451
+ name = "filelock"
452
+ version = "3.20.0"
453
+ source = { registry = "https://pypi.org/simple" }
454
+ sdist = { url = "https://files.pythonhosted.org/packages/58/46/0028a82567109b5ef6e4d2a1f04a583fb513e6cf9527fcdd09afd817deeb/filelock-3.20.0.tar.gz", hash = "sha256:711e943b4ec6be42e1d4e6690b48dc175c822967466bb31c0c293f34334c13f4", size = 18922, upload-time = "2025-10-08T18:03:50.056Z" }
455
+ wheels = [
456
+ { url = "https://files.pythonhosted.org/packages/76/91/7216b27286936c16f5b4d0c530087e4a54eead683e6b0b73dd0c64844af6/filelock-3.20.0-py3-none-any.whl", hash = "sha256:339b4732ffda5cd79b13f4e2711a31b0365ce445d95d243bb996273d072546a2", size = 16054, upload-time = "2025-10-08T18:03:48.35Z" },
457
+ ]
458
+
459
  [[package]]
460
  name = "frozenlist"
461
  version = "1.8.0"
 
561
  { url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409, upload-time = "2025-10-06T05:38:16.721Z" },
562
  ]
563
 
564
+ [[package]]
565
+ name = "fsspec"
566
+ version = "2025.12.0"
567
+ source = { registry = "https://pypi.org/simple" }
568
+ sdist = { url = "https://files.pythonhosted.org/packages/b6/27/954057b0d1f53f086f681755207dda6de6c660ce133c829158e8e8fe7895/fsspec-2025.12.0.tar.gz", hash = "sha256:c505de011584597b1060ff778bb664c1bc022e87921b0e4f10cc9c44f9635973", size = 309748, upload-time = "2025-12-03T15:23:42.687Z" }
569
+ wheels = [
570
+ { url = "https://files.pythonhosted.org/packages/51/c7/b64cae5dba3a1b138d7123ec36bb5ccd39d39939f18454407e5468f4763f/fsspec-2025.12.0-py3-none-any.whl", hash = "sha256:8bf1fe301b7d8acfa6e8571e3b1c3d158f909666642431cc78a1b7b4dbc5ec5b", size = 201422, upload-time = "2025-12-03T15:23:41.434Z" },
571
+ ]
572
+
573
  [[package]]
574
  name = "gitdb"
575
  version = "4.0.12"
 
603
  { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
604
  ]
605
 
606
+ [[package]]
607
+ name = "hf-xet"
608
+ version = "1.2.0"
609
+ source = { registry = "https://pypi.org/simple" }
610
+ sdist = { url = "https://files.pythonhosted.org/packages/5e/6e/0f11bacf08a67f7fb5ee09740f2ca54163863b07b70d579356e9222ce5d8/hf_xet-1.2.0.tar.gz", hash = "sha256:a8c27070ca547293b6890c4bf389f713f80e8c478631432962bb7f4bc0bd7d7f", size = 506020, upload-time = "2025-10-24T19:04:32.129Z" }
611
+ wheels = [
612
+ { url = "https://files.pythonhosted.org/packages/9e/a5/85ef910a0aa034a2abcfadc360ab5ac6f6bc4e9112349bd40ca97551cff0/hf_xet-1.2.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:ceeefcd1b7aed4956ae8499e2199607765fbd1c60510752003b6cc0b8413b649", size = 2861870, upload-time = "2025-10-24T19:04:11.422Z" },
613
+ { url = "https://files.pythonhosted.org/packages/ea/40/e2e0a7eb9a51fe8828ba2d47fe22a7e74914ea8a0db68a18c3aa7449c767/hf_xet-1.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b70218dd548e9840224df5638fdc94bd033552963cfa97f9170829381179c813", size = 2717584, upload-time = "2025-10-24T19:04:09.586Z" },
614
+ { url = "https://files.pythonhosted.org/packages/a5/7d/daf7f8bc4594fdd59a8a596f9e3886133fdc68e675292218a5e4c1b7e834/hf_xet-1.2.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d40b18769bb9a8bc82a9ede575ce1a44c75eb80e7375a01d76259089529b5dc", size = 3315004, upload-time = "2025-10-24T19:04:00.314Z" },
615
+ { url = "https://files.pythonhosted.org/packages/b1/ba/45ea2f605fbf6d81c8b21e4d970b168b18a53515923010c312c06cd83164/hf_xet-1.2.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:cd3a6027d59cfb60177c12d6424e31f4b5ff13d8e3a1247b3a584bf8977e6df5", size = 3222636, upload-time = "2025-10-24T19:03:58.111Z" },
616
+ { url = "https://files.pythonhosted.org/packages/4a/1d/04513e3cab8f29ab8c109d309ddd21a2705afab9d52f2ba1151e0c14f086/hf_xet-1.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6de1fc44f58f6dd937956c8d304d8c2dea264c80680bcfa61ca4a15e7b76780f", size = 3408448, upload-time = "2025-10-24T19:04:20.951Z" },
617
+ { url = "https://files.pythonhosted.org/packages/f0/7c/60a2756d7feec7387db3a1176c632357632fbe7849fce576c5559d4520c7/hf_xet-1.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f182f264ed2acd566c514e45da9f2119110e48a87a327ca271027904c70c5832", size = 3503401, upload-time = "2025-10-24T19:04:22.549Z" },
618
+ { url = "https://files.pythonhosted.org/packages/4e/64/48fffbd67fb418ab07451e4ce641a70de1c40c10a13e25325e24858ebe5a/hf_xet-1.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:293a7a3787e5c95d7be1857358a9130694a9c6021de3f27fa233f37267174382", size = 2900866, upload-time = "2025-10-24T19:04:33.461Z" },
619
+ { url = "https://files.pythonhosted.org/packages/e2/51/f7e2caae42f80af886db414d4e9885fac959330509089f97cccb339c6b87/hf_xet-1.2.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:10bfab528b968c70e062607f663e21e34e2bba349e8038db546646875495179e", size = 2861861, upload-time = "2025-10-24T19:04:19.01Z" },
620
+ { url = "https://files.pythonhosted.org/packages/6e/1d/a641a88b69994f9371bd347f1dd35e5d1e2e2460a2e350c8d5165fc62005/hf_xet-1.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2a212e842647b02eb6a911187dc878e79c4aa0aa397e88dd3b26761676e8c1f8", size = 2717699, upload-time = "2025-10-24T19:04:17.306Z" },
621
+ { url = "https://files.pythonhosted.org/packages/df/e0/e5e9bba7d15f0318955f7ec3f4af13f92e773fbb368c0b8008a5acbcb12f/hf_xet-1.2.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:30e06daccb3a7d4c065f34fc26c14c74f4653069bb2b194e7f18f17cbe9939c0", size = 3314885, upload-time = "2025-10-24T19:04:07.642Z" },
622
+ { url = "https://files.pythonhosted.org/packages/21/90/b7fe5ff6f2b7b8cbdf1bd56145f863c90a5807d9758a549bf3d916aa4dec/hf_xet-1.2.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:29c8fc913a529ec0a91867ce3d119ac1aac966e098cf49501800c870328cc090", size = 3221550, upload-time = "2025-10-24T19:04:05.55Z" },
623
+ { url = "https://files.pythonhosted.org/packages/6f/cb/73f276f0a7ce46cc6a6ec7d6c7d61cbfe5f2e107123d9bbd0193c355f106/hf_xet-1.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e159cbfcfbb29f920db2c09ed8b660eb894640d284f102ada929b6e3dc410a", size = 3408010, upload-time = "2025-10-24T19:04:28.598Z" },
624
+ { url = "https://files.pythonhosted.org/packages/b8/1e/d642a12caa78171f4be64f7cd9c40e3ca5279d055d0873188a58c0f5fbb9/hf_xet-1.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9c91d5ae931510107f148874e9e2de8a16052b6f1b3ca3c1b12f15ccb491390f", size = 3503264, upload-time = "2025-10-24T19:04:30.397Z" },
625
+ { url = "https://files.pythonhosted.org/packages/17/b5/33764714923fa1ff922770f7ed18c2daae034d21ae6e10dbf4347c854154/hf_xet-1.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:210d577732b519ac6ede149d2f2f34049d44e8622bf14eb3d63bbcd2d4b332dc", size = 2901071, upload-time = "2025-10-24T19:04:37.463Z" },
626
+ { url = "https://files.pythonhosted.org/packages/96/2d/22338486473df5923a9ab7107d375dbef9173c338ebef5098ef593d2b560/hf_xet-1.2.0-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:46740d4ac024a7ca9b22bebf77460ff43332868b661186a8e46c227fdae01848", size = 2866099, upload-time = "2025-10-24T19:04:15.366Z" },
627
+ { url = "https://files.pythonhosted.org/packages/7f/8c/c5becfa53234299bc2210ba314eaaae36c2875e0045809b82e40a9544f0c/hf_xet-1.2.0-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:27df617a076420d8845bea087f59303da8be17ed7ec0cd7ee3b9b9f579dff0e4", size = 2722178, upload-time = "2025-10-24T19:04:13.695Z" },
628
+ { url = "https://files.pythonhosted.org/packages/9a/92/cf3ab0b652b082e66876d08da57fcc6fa2f0e6c70dfbbafbd470bb73eb47/hf_xet-1.2.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3651fd5bfe0281951b988c0facbe726aa5e347b103a675f49a3fa8144c7968fd", size = 3320214, upload-time = "2025-10-24T19:04:03.596Z" },
629
+ { url = "https://files.pythonhosted.org/packages/46/92/3f7ec4a1b6a65bf45b059b6d4a5d38988f63e193056de2f420137e3c3244/hf_xet-1.2.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d06fa97c8562fb3ee7a378dd9b51e343bc5bc8190254202c9771029152f5e08c", size = 3229054, upload-time = "2025-10-24T19:04:01.949Z" },
630
+ { url = "https://files.pythonhosted.org/packages/0b/dd/7ac658d54b9fb7999a0ccb07ad863b413cbaf5cf172f48ebcd9497ec7263/hf_xet-1.2.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:4c1428c9ae73ec0939410ec73023c4f842927f39db09b063b9482dac5a3bb737", size = 3413812, upload-time = "2025-10-24T19:04:24.585Z" },
631
+ { url = "https://files.pythonhosted.org/packages/92/68/89ac4e5b12a9ff6286a12174c8538a5930e2ed662091dd2572bbe0a18c8a/hf_xet-1.2.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a55558084c16b09b5ed32ab9ed38421e2d87cf3f1f89815764d1177081b99865", size = 3508920, upload-time = "2025-10-24T19:04:26.927Z" },
632
+ { url = "https://files.pythonhosted.org/packages/cb/44/870d44b30e1dcfb6a65932e3e1506c103a8a5aea9103c337e7a53180322c/hf_xet-1.2.0-cp37-abi3-win_amd64.whl", hash = "sha256:e6584a52253f72c9f52f9e549d5895ca7a471608495c4ecaa6cc73dba2b24d69", size = 2905735, upload-time = "2025-10-24T19:04:35.928Z" },
633
+ ]
634
+
635
+ [[package]]
636
+ name = "httpcore"
637
+ version = "1.0.9"
638
+ source = { registry = "https://pypi.org/simple" }
639
+ dependencies = [
640
+ { name = "certifi" },
641
+ { name = "h11" },
642
+ ]
643
+ sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" }
644
+ wheels = [
645
+ { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" },
646
+ ]
647
+
648
  [[package]]
649
  name = "httptools"
650
  version = "0.7.1"
 
681
  { url = "https://files.pythonhosted.org/packages/53/cf/878f3b91e4e6e011eff6d1fa9ca39f7eb17d19c9d7971b04873734112f30/httptools-0.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:cfabda2a5bb85aa2a904ce06d974a3f30fb36cc63d7feaddec05d2050acede96", size = 88205, upload-time = "2025-10-10T03:55:00.389Z" },
682
  ]
683
 
684
+ [[package]]
685
+ name = "httpx"
686
+ version = "0.28.1"
687
+ source = { registry = "https://pypi.org/simple" }
688
+ dependencies = [
689
+ { name = "anyio" },
690
+ { name = "certifi" },
691
+ { name = "httpcore" },
692
+ { name = "idna" },
693
+ ]
694
+ sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" }
695
+ wheels = [
696
+ { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
697
+ ]
698
+
699
+ [[package]]
700
+ name = "huggingface-hub"
701
+ version = "0.36.0"
702
+ source = { registry = "https://pypi.org/simple" }
703
+ dependencies = [
704
+ { name = "filelock" },
705
+ { name = "fsspec" },
706
+ { name = "hf-xet", marker = "platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'" },
707
+ { name = "packaging" },
708
+ { name = "pyyaml" },
709
+ { name = "requests" },
710
+ { name = "tqdm" },
711
+ { name = "typing-extensions" },
712
+ ]
713
+ sdist = { url = "https://files.pythonhosted.org/packages/98/63/4910c5fa9128fdadf6a9c5ac138e8b1b6cee4ca44bf7915bbfbce4e355ee/huggingface_hub-0.36.0.tar.gz", hash = "sha256:47b3f0e2539c39bf5cde015d63b72ec49baff67b6931c3d97f3f84532e2b8d25", size = 463358, upload-time = "2025-10-23T12:12:01.413Z" }
714
+ wheels = [
715
+ { url = "https://files.pythonhosted.org/packages/cb/bd/1a875e0d592d447cbc02805fd3fe0f497714d6a2583f59d14fa9ebad96eb/huggingface_hub-0.36.0-py3-none-any.whl", hash = "sha256:7bcc9ad17d5b3f07b57c78e79d527102d08313caa278a641993acddcb894548d", size = 566094, upload-time = "2025-10-23T12:11:59.557Z" },
716
+ ]
717
+
718
  [[package]]
719
  name = "idna"
720
  version = "3.11"
 
800
  dependencies = [
801
  { name = "aiofiles" },
802
  { name = "fastapi" },
803
+ { name = "httpx" },
804
+ { name = "mlx" },
805
+ { name = "mlx-lm" },
806
  { name = "numpy" },
807
  { name = "openpyxl" },
808
  { name = "pandas" },
 
813
  { name = "python-multipart" },
814
  { name = "requests" },
815
  { name = "scikit-learn" },
816
+ { name = "scipy" },
817
  { name = "streamlit" },
818
  { name = "uvicorn", extra = ["standard"] },
819
  ]
 
832
  { name = "aiofiles", specifier = "==23.2.1" },
833
  { name = "black", marker = "extra == 'dev'", specifier = "==23.12.0" },
834
  { name = "fastapi", specifier = "==0.109.0" },
835
+ { name = "httpx", specifier = ">=0.28.1" },
836
+ { name = "mlx", specifier = ">=0.30.0" },
837
+ { name = "mlx-lm", specifier = ">=0.28.4" },
838
  { name = "numpy", specifier = ">=1.26.0,<2.0.0" },
839
  { name = "openpyxl", specifier = "==3.1.5" },
840
  { name = "pandas", specifier = "==2.1.4" },
 
849
  { name = "requests", specifier = "==2.31.0" },
850
  { name = "ruff", marker = "extra == 'dev'", specifier = "==0.1.11" },
851
  { name = "scikit-learn", specifier = "==1.3.2" },
852
+ { name = "scipy", specifier = ">=1.16.3" },
853
  { name = "streamlit", specifier = "==1.28.1" },
854
  { name = "uvicorn", extras = ["standard"], specifier = "==0.27.0" },
855
  ]
 
950
  { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
951
  ]
952
 
953
+ [[package]]
954
+ name = "mlx"
955
+ version = "0.30.0"
956
+ source = { registry = "https://pypi.org/simple" }
957
+ dependencies = [
958
+ { name = "mlx-metal", marker = "sys_platform == 'darwin'" },
959
+ ]
960
+ wheels = [
961
+ { url = "https://files.pythonhosted.org/packages/4a/e8/69ebac29536c026489ded1ad58a6f5163b8fc10ab5eac21228f57ea9e83f/mlx-0.30.0-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:d5871a5f1ce5cba2f690d2c630a6672cc65a62326bcaa6db258185957a1f073f", size = 554825, upload-time = "2025-11-20T16:45:11.135Z" },
962
+ { url = "https://files.pythonhosted.org/packages/2b/c7/db80b1e9f613baf99745e9920a3a7fe7b6c61398420ed308f24f60877a15/mlx-0.30.0-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:302c3d52b4f68b80a8ee259489369524a3381fcc9aed12b17c1c537870c81fce", size = 554821, upload-time = "2025-11-20T01:16:38.53Z" },
963
+ { url = "https://files.pythonhosted.org/packages/8a/68/55cd5cab5a9b8b958ea995295aa3428ad65b3e893f04c27e2cdfcdc176af/mlx-0.30.0-cp311-cp311-macosx_26_0_arm64.whl", hash = "sha256:d72443a461f2e92329edf344ded8460df53fd7ec3c04ddd2353ee4ceb34c0ff2", size = 554791, upload-time = "2025-11-20T05:33:23.851Z" },
964
+ { url = "https://files.pythonhosted.org/packages/16/91/5b79b6febcbb7e1051ecf408a8c30226ebfdf19a6e304b4cfa32309059ab/mlx-0.30.0-cp311-cp311-manylinux_2_35_aarch64.whl", hash = "sha256:9de2ab05cc9721c99a0802721aa1f0d6c305e97acd2f8ec4c05af3449ee7700d", size = 625176, upload-time = "2025-12-01T15:23:40.991Z" },
965
+ { url = "https://files.pythonhosted.org/packages/80/17/16868ac1ea36ea3baf61f84721b4dfd98dd247a230b951ddd715981db79b/mlx-0.30.0-cp311-cp311-manylinux_2_35_x86_64.whl", hash = "sha256:0beb015a6afef2f86dfbefe65c9022dce91dbe1651e1b4330ef32434602323e0", size = 659735, upload-time = "2025-11-20T01:16:40.594Z" },
966
+ { url = "https://files.pythonhosted.org/packages/94/a3/32c4c05d8967591e2a1a1e7e3fc9cece8821f5aea8ac8f3bcfdb203f4722/mlx-0.30.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:11fbae58b1e992afdec6709d5e281932871a0138582a794cdcc82ff895a28670", size = 554567, upload-time = "2025-11-20T16:45:12.73Z" },
967
+ { url = "https://files.pythonhosted.org/packages/aa/b3/b6143f1c078fbc873e40e624dc428a3ada240721001414955f584afa866d/mlx-0.30.0-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:fc1994a4c9e60ccfe2ab9211c846be2ef7abc6fc3e53e1addb2cdf7468f61e7b", size = 554566, upload-time = "2025-11-20T01:16:41.877Z" },
968
+ { url = "https://files.pythonhosted.org/packages/96/cb/aec36297ef76b4b190cc6d4cea1ed995b458bbc21cb91c58b0862f8cae7d/mlx-0.30.0-cp312-cp312-macosx_26_0_arm64.whl", hash = "sha256:a9f30c5c94b30b65f026162e91256473187e1de57c13dcc2aedb2fc33c07f2c1", size = 554593, upload-time = "2025-11-20T05:33:25.312Z" },
969
+ { url = "https://files.pythonhosted.org/packages/40/d3/575eee4ef4b5f3dad9076a78f287affe046fd32b3bdba7a2e0af31f0d9d3/mlx-0.30.0-cp312-cp312-manylinux_2_35_aarch64.whl", hash = "sha256:f88d11e2719fdab08dbe68c9666003c8644ccd01dff973bc86e949cc16195eac", size = 612086, upload-time = "2025-12-01T15:23:42.317Z" },
970
+ { url = "https://files.pythonhosted.org/packages/23/37/b5dd68da0e79e258f5d6a0e9f5fd4f9e5452e92c20b13b64948a965ea429/mlx-0.30.0-cp312-cp312-manylinux_2_35_x86_64.whl", hash = "sha256:1c4a2f5285bdd585aa6485a4fb5759ebc2721ba9381404ff867c136e84764e9b", size = 653994, upload-time = "2025-11-20T01:16:43.492Z" },
971
+ { url = "https://files.pythonhosted.org/packages/33/a5/e171b2caa69b346bc1abc1bfd0b139f631f68a0ff602862dd255e7dd95ec/mlx-0.30.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:f46aaa6c562ba183e2a64a0e6ba15ed54f9027d9b7b1822e9eec7f59b13d610c", size = 554595, upload-time = "2025-11-20T16:45:13.919Z" },
972
+ { url = "https://files.pythonhosted.org/packages/3e/f4/aeb8980bbef08fc031ab1a2d043a1d76e60d49bf46728bef66cf25b26dfa/mlx-0.30.0-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:5edee9f1452703c0804e067f212c61d488b3ad7419b9fcacf337ffd35842f576", size = 554593, upload-time = "2025-11-20T01:16:45.033Z" },
973
+ { url = "https://files.pythonhosted.org/packages/0b/b3/904235e11610c7cf2ba39eb39f32587d34048f7293d386f19d42d1e3dabc/mlx-0.30.0-cp313-cp313-macosx_26_0_arm64.whl", hash = "sha256:b8ae92b61353d756bbd0668b065a4ee5ee35867b03d63c0c61134d656ed236fe", size = 554330, upload-time = "2025-11-20T05:33:26.653Z" },
974
+ { url = "https://files.pythonhosted.org/packages/de/0f/bb956ec9926596fe771ec67677c049e358c43f0506699b59dbed7ba9fedc/mlx-0.30.0-cp313-cp313-manylinux_2_35_aarch64.whl", hash = "sha256:f3ae4c99308ff4c006c3062ff73108b6f39414bcd90416930b917160070f759b", size = 612083, upload-time = "2025-12-01T15:23:43.369Z" },
975
+ { url = "https://files.pythonhosted.org/packages/5e/87/3f3505d3fbf0f977b2930b3596f590f0079c3a2a253d01349f936c40985a/mlx-0.30.0-cp313-cp313-manylinux_2_35_x86_64.whl", hash = "sha256:431009c531f8bdbf56f46b85d458d5526b40ea43178dbc85f37ed55e03e716be", size = 653971, upload-time = "2025-11-20T01:16:46.292Z" },
976
+ { url = "https://files.pythonhosted.org/packages/43/25/6fa174632f5beb583eda80902af80dc39a63e5b4b6e66c7831301751d82e/mlx-0.30.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:384819dccfd551aa1444acacedb9ed2619724b0e67fc361ab80d73b8a8a7618f", size = 558834, upload-time = "2025-11-20T16:45:15.303Z" },
977
+ { url = "https://files.pythonhosted.org/packages/b9/00/8c93f6ba5dc37a459b378bd22d2303824aa341595ed6c4958c6a48870677/mlx-0.30.0-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:8982a7d7fd078988c12c3fc53da1670aa910dd8a7df1fc0b9fee7303394ac8ff", size = 558833, upload-time = "2025-11-20T01:16:48.748Z" },
978
+ { url = "https://files.pythonhosted.org/packages/58/65/0ecb3a858142d7b250b46d1e5c2bb4ebff9f57b735643626b7d4653b0f11/mlx-0.30.0-cp314-cp314-macosx_26_0_arm64.whl", hash = "sha256:392af721cb0bcf600162d7258923c15d39e197a64e23b6abd171e1aea79771c5", size = 558510, upload-time = "2025-11-20T05:33:28.286Z" },
979
+ { url = "https://files.pythonhosted.org/packages/27/a2/df61dc58caf6238d90cfa2535018fa2c52745e76421dd3ff11c034aae26f/mlx-0.30.0-cp314-cp314-manylinux_2_35_aarch64.whl", hash = "sha256:a4dae4c0404560d3df9832ee709e502022a9e5f89c4d432c576af8405d9a45ef", size = 614193, upload-time = "2025-12-01T15:23:44.688Z" },
980
+ { url = "https://files.pythonhosted.org/packages/e1/dd/38f465477f996bb24ab133aaf79a3eb0fa13a9bd9d19504aad30c3be7ffc/mlx-0.30.0-cp314-cp314-manylinux_2_35_x86_64.whl", hash = "sha256:cf688c9dda18f2521d48f712515a47b145b37d5d0ab245eaf1a68f286af7ae66", size = 654400, upload-time = "2025-11-20T01:16:50.018Z" },
981
+ ]
982
+
983
+ [[package]]
984
+ name = "mlx-lm"
985
+ version = "0.28.4"
986
+ source = { registry = "https://pypi.org/simple" }
987
+ dependencies = [
988
+ { name = "jinja2" },
989
+ { name = "mlx", marker = "sys_platform == 'darwin'" },
990
+ { name = "numpy" },
991
+ { name = "protobuf" },
992
+ { name = "pyyaml" },
993
+ { name = "sentencepiece" },
994
+ { name = "transformers" },
995
+ ]
996
+ sdist = { url = "https://files.pythonhosted.org/packages/f2/7f/94b3f7e00c4681a4fe2d47b519458245bd8a8f0506b1ce018d1850bbcf79/mlx_lm-0.28.4.tar.gz", hash = "sha256:3661d8ef5f0e2695d52993e0df1ed2c1f93ca1d094258146c18d9cec0c50514e", size = 232455, upload-time = "2025-12-03T22:39:59.122Z" }
997
+ wheels = [
998
+ { url = "https://files.pythonhosted.org/packages/69/35/5767098993834582e8a34a0317d666ed53498f58940fad8bffa0a4662eb1/mlx_lm-0.28.4-py3-none-any.whl", hash = "sha256:64ff7bff02e902f1df7daafdbf27ffc836b9a7c9b332b9fc3ea40c19e31147ca", size = 323307, upload-time = "2025-12-03T22:39:57.711Z" },
999
+ ]
1000
+
1001
+ [[package]]
1002
+ name = "mlx-metal"
1003
+ version = "0.30.0"
1004
+ source = { registry = "https://pypi.org/simple" }
1005
+ wheels = [
1006
+ { url = "https://files.pythonhosted.org/packages/64/9f/47ebb6e9b2c33371c6ca3733e70324ed064f49e790ee4e194b713d6d7d84/mlx_metal-0.30.0-py3-none-macosx_14_0_arm64.whl", hash = "sha256:f48543b10d13bf0591b3f99c6eb585dd2c2e5db379edae5df0f19a728cb41742", size = 36819863, upload-time = "2025-11-20T06:42:23.927Z" },
1007
+ { url = "https://files.pythonhosted.org/packages/7f/91/c04e420326390c37d4c13f58960956d698cd34c7432ae3860bc5c6be71a0/mlx_metal-0.30.0-py3-none-macosx_15_0_arm64.whl", hash = "sha256:74bb5d10e0f24e21973d39430557bbd5d733c2a6599c3f1b87f9a0ff73fed2c8", size = 36817633, upload-time = "2025-11-20T01:16:27.77Z" },
1008
+ { url = "https://files.pythonhosted.org/packages/86/18/7af11eac0f488b68c436d249ffcf4f76003326e691994db3f720d21f21bb/mlx_metal-0.30.0-py3-none-macosx_26_0_arm64.whl", hash = "sha256:310fda8b2f7345865f3dd75a9b478e974f28d22e8ebf05f26af5adc0e8979cee", size = 44878409, upload-time = "2025-11-20T05:33:20.221Z" },
1009
+ ]
1010
+
1011
  [[package]]
1012
  name = "multidict"
1013
  version = "6.7.0"
 
1692
  { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" },
1693
  ]
1694
 
1695
+ [[package]]
1696
+ name = "regex"
1697
+ version = "2025.11.3"
1698
+ source = { registry = "https://pypi.org/simple" }
1699
+ sdist = { url = "https://files.pythonhosted.org/packages/cc/a9/546676f25e573a4cf00fe8e119b78a37b6a8fe2dc95cda877b30889c9c45/regex-2025.11.3.tar.gz", hash = "sha256:1fedc720f9bb2494ce31a58a1631f9c82df6a09b49c19517ea5cc280b4541e01", size = 414669, upload-time = "2025-11-03T21:34:22.089Z" }
1700
+ wheels = [
1701
+ { url = "https://files.pythonhosted.org/packages/f7/90/4fb5056e5f03a7048abd2b11f598d464f0c167de4f2a51aa868c376b8c70/regex-2025.11.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:eadade04221641516fa25139273505a1c19f9bf97589a05bc4cfcd8b4a618031", size = 488081, upload-time = "2025-11-03T21:31:11.946Z" },
1702
+ { url = "https://files.pythonhosted.org/packages/85/23/63e481293fac8b069d84fba0299b6666df720d875110efd0338406b5d360/regex-2025.11.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:feff9e54ec0dd3833d659257f5c3f5322a12eee58ffa360984b716f8b92983f4", size = 290554, upload-time = "2025-11-03T21:31:13.387Z" },
1703
+ { url = "https://files.pythonhosted.org/packages/2b/9d/b101d0262ea293a0066b4522dfb722eb6a8785a8c3e084396a5f2c431a46/regex-2025.11.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3b30bc921d50365775c09a7ed446359e5c0179e9e2512beec4a60cbcef6ddd50", size = 288407, upload-time = "2025-11-03T21:31:14.809Z" },
1704
+ { url = "https://files.pythonhosted.org/packages/0c/64/79241c8209d5b7e00577ec9dca35cd493cc6be35b7d147eda367d6179f6d/regex-2025.11.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f99be08cfead2020c7ca6e396c13543baea32343b7a9a5780c462e323bd8872f", size = 793418, upload-time = "2025-11-03T21:31:16.556Z" },
1705
+ { url = "https://files.pythonhosted.org/packages/3d/e2/23cd5d3573901ce8f9757c92ca4db4d09600b865919b6d3e7f69f03b1afd/regex-2025.11.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6dd329a1b61c0ee95ba95385fb0c07ea0d3fe1a21e1349fa2bec272636217118", size = 860448, upload-time = "2025-11-03T21:31:18.12Z" },
1706
+ { url = "https://files.pythonhosted.org/packages/2a/4c/aecf31beeaa416d0ae4ecb852148d38db35391aac19c687b5d56aedf3a8b/regex-2025.11.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4c5238d32f3c5269d9e87be0cf096437b7622b6920f5eac4fd202468aaeb34d2", size = 907139, upload-time = "2025-11-03T21:31:20.753Z" },
1707
+ { url = "https://files.pythonhosted.org/packages/61/22/b8cb00df7d2b5e0875f60628594d44dba283e951b1ae17c12f99e332cc0a/regex-2025.11.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:10483eefbfb0adb18ee9474498c9a32fcf4e594fbca0543bb94c48bac6183e2e", size = 800439, upload-time = "2025-11-03T21:31:22.069Z" },
1708
+ { url = "https://files.pythonhosted.org/packages/02/a8/c4b20330a5cdc7a8eb265f9ce593f389a6a88a0c5f280cf4d978f33966bc/regex-2025.11.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:78c2d02bb6e1da0720eedc0bad578049cad3f71050ef8cd065ecc87691bed2b0", size = 782965, upload-time = "2025-11-03T21:31:23.598Z" },
1709
+ { url = "https://files.pythonhosted.org/packages/b4/4c/ae3e52988ae74af4b04d2af32fee4e8077f26e51b62ec2d12d246876bea2/regex-2025.11.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e6b49cd2aad93a1790ce9cffb18964f6d3a4b0b3dbdbd5de094b65296fce6e58", size = 854398, upload-time = "2025-11-03T21:31:25.008Z" },
1710
+ { url = "https://files.pythonhosted.org/packages/06/d1/a8b9cf45874eda14b2e275157ce3b304c87e10fb38d9fc26a6e14eb18227/regex-2025.11.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:885b26aa3ee56433b630502dc3d36ba78d186a00cc535d3806e6bfd9ed3c70ab", size = 845897, upload-time = "2025-11-03T21:31:26.427Z" },
1711
+ { url = "https://files.pythonhosted.org/packages/ea/fe/1830eb0236be93d9b145e0bd8ab499f31602fe0999b1f19e99955aa8fe20/regex-2025.11.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ddd76a9f58e6a00f8772e72cff8ebcff78e022be95edf018766707c730593e1e", size = 788906, upload-time = "2025-11-03T21:31:28.078Z" },
1712
+ { url = "https://files.pythonhosted.org/packages/66/47/dc2577c1f95f188c1e13e2e69d8825a5ac582ac709942f8a03af42ed6e93/regex-2025.11.3-cp311-cp311-win32.whl", hash = "sha256:3e816cc9aac1cd3cc9a4ec4d860f06d40f994b5c7b4d03b93345f44e08cc68bf", size = 265812, upload-time = "2025-11-03T21:31:29.72Z" },
1713
+ { url = "https://files.pythonhosted.org/packages/50/1e/15f08b2f82a9bbb510621ec9042547b54d11e83cb620643ebb54e4eb7d71/regex-2025.11.3-cp311-cp311-win_amd64.whl", hash = "sha256:087511f5c8b7dfbe3a03f5d5ad0c2a33861b1fc387f21f6f60825a44865a385a", size = 277737, upload-time = "2025-11-03T21:31:31.422Z" },
1714
+ { url = "https://files.pythonhosted.org/packages/f4/fc/6500eb39f5f76c5e47a398df82e6b535a5e345f839581012a418b16f9cc3/regex-2025.11.3-cp311-cp311-win_arm64.whl", hash = "sha256:1ff0d190c7f68ae7769cd0313fe45820ba07ffebfddfaa89cc1eb70827ba0ddc", size = 270290, upload-time = "2025-11-03T21:31:33.041Z" },
1715
+ { url = "https://files.pythonhosted.org/packages/e8/74/18f04cb53e58e3fb107439699bd8375cf5a835eec81084e0bddbd122e4c2/regex-2025.11.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bc8ab71e2e31b16e40868a40a69007bc305e1109bd4658eb6cad007e0bf67c41", size = 489312, upload-time = "2025-11-03T21:31:34.343Z" },
1716
+ { url = "https://files.pythonhosted.org/packages/78/3f/37fcdd0d2b1e78909108a876580485ea37c91e1acf66d3bb8e736348f441/regex-2025.11.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:22b29dda7e1f7062a52359fca6e58e548e28c6686f205e780b02ad8ef710de36", size = 291256, upload-time = "2025-11-03T21:31:35.675Z" },
1717
+ { url = "https://files.pythonhosted.org/packages/bf/26/0a575f58eb23b7ebd67a45fccbc02ac030b737b896b7e7a909ffe43ffd6a/regex-2025.11.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3a91e4a29938bc1a082cc28fdea44be420bf2bebe2665343029723892eb073e1", size = 288921, upload-time = "2025-11-03T21:31:37.07Z" },
1718
+ { url = "https://files.pythonhosted.org/packages/ea/98/6a8dff667d1af907150432cf5abc05a17ccd32c72a3615410d5365ac167a/regex-2025.11.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:08b884f4226602ad40c5d55f52bf91a9df30f513864e0054bad40c0e9cf1afb7", size = 798568, upload-time = "2025-11-03T21:31:38.784Z" },
1719
+ { url = "https://files.pythonhosted.org/packages/64/15/92c1db4fa4e12733dd5a526c2dd2b6edcbfe13257e135fc0f6c57f34c173/regex-2025.11.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3e0b11b2b2433d1c39c7c7a30e3f3d0aeeea44c2a8d0bae28f6b95f639927a69", size = 864165, upload-time = "2025-11-03T21:31:40.559Z" },
1720
+ { url = "https://files.pythonhosted.org/packages/f9/e7/3ad7da8cdee1ce66c7cd37ab5ab05c463a86ffeb52b1a25fe7bd9293b36c/regex-2025.11.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:87eb52a81ef58c7ba4d45c3ca74e12aa4b4e77816f72ca25258a85b3ea96cb48", size = 912182, upload-time = "2025-11-03T21:31:42.002Z" },
1721
+ { url = "https://files.pythonhosted.org/packages/84/bd/9ce9f629fcb714ffc2c3faf62b6766ecb7a585e1e885eb699bcf130a5209/regex-2025.11.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a12ab1f5c29b4e93db518f5e3872116b7e9b1646c9f9f426f777b50d44a09e8c", size = 803501, upload-time = "2025-11-03T21:31:43.815Z" },
1722
+ { url = "https://files.pythonhosted.org/packages/7c/0f/8dc2e4349d8e877283e6edd6c12bdcebc20f03744e86f197ab6e4492bf08/regex-2025.11.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7521684c8c7c4f6e88e35ec89680ee1aa8358d3f09d27dfbdf62c446f5d4c695", size = 787842, upload-time = "2025-11-03T21:31:45.353Z" },
1723
+ { url = "https://files.pythonhosted.org/packages/f9/73/cff02702960bc185164d5619c0c62a2f598a6abff6695d391b096237d4ab/regex-2025.11.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7fe6e5440584e94cc4b3f5f4d98a25e29ca12dccf8873679a635638349831b98", size = 858519, upload-time = "2025-11-03T21:31:46.814Z" },
1724
+ { url = "https://files.pythonhosted.org/packages/61/83/0e8d1ae71e15bc1dc36231c90b46ee35f9d52fab2e226b0e039e7ea9c10a/regex-2025.11.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:8e026094aa12b43f4fd74576714e987803a315c76edb6b098b9809db5de58f74", size = 850611, upload-time = "2025-11-03T21:31:48.289Z" },
1725
+ { url = "https://files.pythonhosted.org/packages/c8/f5/70a5cdd781dcfaa12556f2955bf170cd603cb1c96a1827479f8faea2df97/regex-2025.11.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:435bbad13e57eb5606a68443af62bed3556de2f46deb9f7d4237bc2f1c9fb3a0", size = 789759, upload-time = "2025-11-03T21:31:49.759Z" },
1726
+ { url = "https://files.pythonhosted.org/packages/59/9b/7c29be7903c318488983e7d97abcf8ebd3830e4c956c4c540005fcfb0462/regex-2025.11.3-cp312-cp312-win32.whl", hash = "sha256:3839967cf4dc4b985e1570fd8d91078f0c519f30491c60f9ac42a8db039be204", size = 266194, upload-time = "2025-11-03T21:31:51.53Z" },
1727
+ { url = "https://files.pythonhosted.org/packages/1a/67/3b92df89f179d7c367be654ab5626ae311cb28f7d5c237b6bb976cd5fbbb/regex-2025.11.3-cp312-cp312-win_amd64.whl", hash = "sha256:e721d1b46e25c481dc5ded6f4b3f66c897c58d2e8cfdf77bbced84339108b0b9", size = 277069, upload-time = "2025-11-03T21:31:53.151Z" },
1728
+ { url = "https://files.pythonhosted.org/packages/d7/55/85ba4c066fe5094d35b249c3ce8df0ba623cfd35afb22d6764f23a52a1c5/regex-2025.11.3-cp312-cp312-win_arm64.whl", hash = "sha256:64350685ff08b1d3a6fff33f45a9ca183dc1d58bbfe4981604e70ec9801bbc26", size = 270330, upload-time = "2025-11-03T21:31:54.514Z" },
1729
+ { url = "https://files.pythonhosted.org/packages/e1/a7/dda24ebd49da46a197436ad96378f17df30ceb40e52e859fc42cac45b850/regex-2025.11.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c1e448051717a334891f2b9a620fe36776ebf3dd8ec46a0b877c8ae69575feb4", size = 489081, upload-time = "2025-11-03T21:31:55.9Z" },
1730
+ { url = "https://files.pythonhosted.org/packages/19/22/af2dc751aacf88089836aa088a1a11c4f21a04707eb1b0478e8e8fb32847/regex-2025.11.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9b5aca4d5dfd7fbfbfbdaf44850fcc7709a01146a797536a8f84952e940cca76", size = 291123, upload-time = "2025-11-03T21:31:57.758Z" },
1731
+ { url = "https://files.pythonhosted.org/packages/a3/88/1a3ea5672f4b0a84802ee9891b86743438e7c04eb0b8f8c4e16a42375327/regex-2025.11.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:04d2765516395cf7dda331a244a3282c0f5ae96075f728629287dfa6f76ba70a", size = 288814, upload-time = "2025-11-03T21:32:01.12Z" },
1732
+ { url = "https://files.pythonhosted.org/packages/fb/8c/f5987895bf42b8ddeea1b315c9fedcfe07cadee28b9c98cf50d00adcb14d/regex-2025.11.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d9903ca42bfeec4cebedba8022a7c97ad2aab22e09573ce9976ba01b65e4361", size = 798592, upload-time = "2025-11-03T21:32:03.006Z" },
1733
+ { url = "https://files.pythonhosted.org/packages/99/2a/6591ebeede78203fa77ee46a1c36649e02df9eaa77a033d1ccdf2fcd5d4e/regex-2025.11.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:639431bdc89d6429f6721625e8129413980ccd62e9d3f496be618a41d205f160", size = 864122, upload-time = "2025-11-03T21:32:04.553Z" },
1734
+ { url = "https://files.pythonhosted.org/packages/94/d6/be32a87cf28cf8ed064ff281cfbd49aefd90242a83e4b08b5a86b38e8eb4/regex-2025.11.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f117efad42068f9715677c8523ed2be1518116d1c49b1dd17987716695181efe", size = 912272, upload-time = "2025-11-03T21:32:06.148Z" },
1735
+ { url = "https://files.pythonhosted.org/packages/62/11/9bcef2d1445665b180ac7f230406ad80671f0fc2a6ffb93493b5dd8cd64c/regex-2025.11.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4aecb6f461316adf9f1f0f6a4a1a3d79e045f9b71ec76055a791affa3b285850", size = 803497, upload-time = "2025-11-03T21:32:08.162Z" },
1736
+ { url = "https://files.pythonhosted.org/packages/e5/a7/da0dc273d57f560399aa16d8a68ae7f9b57679476fc7ace46501d455fe84/regex-2025.11.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3b3a5f320136873cc5561098dfab677eea139521cb9a9e8db98b7e64aef44cbc", size = 787892, upload-time = "2025-11-03T21:32:09.769Z" },
1737
+ { url = "https://files.pythonhosted.org/packages/da/4b/732a0c5a9736a0b8d6d720d4945a2f1e6f38f87f48f3173559f53e8d5d82/regex-2025.11.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:75fa6f0056e7efb1f42a1c34e58be24072cb9e61a601340cc1196ae92326a4f9", size = 858462, upload-time = "2025-11-03T21:32:11.769Z" },
1738
+ { url = "https://files.pythonhosted.org/packages/0c/f5/a2a03df27dc4c2d0c769220f5110ba8c4084b0bfa9ab0f9b4fcfa3d2b0fc/regex-2025.11.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:dbe6095001465294f13f1adcd3311e50dd84e5a71525f20a10bd16689c61ce0b", size = 850528, upload-time = "2025-11-03T21:32:13.906Z" },
1739
+ { url = "https://files.pythonhosted.org/packages/d6/09/e1cd5bee3841c7f6eb37d95ca91cdee7100b8f88b81e41c2ef426910891a/regex-2025.11.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:454d9b4ae7881afbc25015b8627c16d88a597479b9dea82b8c6e7e2e07240dc7", size = 789866, upload-time = "2025-11-03T21:32:15.748Z" },
1740
+ { url = "https://files.pythonhosted.org/packages/eb/51/702f5ea74e2a9c13d855a6a85b7f80c30f9e72a95493260193c07f3f8d74/regex-2025.11.3-cp313-cp313-win32.whl", hash = "sha256:28ba4d69171fc6e9896337d4fc63a43660002b7da53fc15ac992abcf3410917c", size = 266189, upload-time = "2025-11-03T21:32:17.493Z" },
1741
+ { url = "https://files.pythonhosted.org/packages/8b/00/6e29bb314e271a743170e53649db0fdb8e8ff0b64b4f425f5602f4eb9014/regex-2025.11.3-cp313-cp313-win_amd64.whl", hash = "sha256:bac4200befe50c670c405dc33af26dad5a3b6b255dd6c000d92fe4629f9ed6a5", size = 277054, upload-time = "2025-11-03T21:32:19.042Z" },
1742
+ { url = "https://files.pythonhosted.org/packages/25/f1/b156ff9f2ec9ac441710764dda95e4edaf5f36aca48246d1eea3f1fd96ec/regex-2025.11.3-cp313-cp313-win_arm64.whl", hash = "sha256:2292cd5a90dab247f9abe892ac584cb24f0f54680c73fcb4a7493c66c2bf2467", size = 270325, upload-time = "2025-11-03T21:32:21.338Z" },
1743
+ { url = "https://files.pythonhosted.org/packages/20/28/fd0c63357caefe5680b8ea052131acbd7f456893b69cc2a90cc3e0dc90d4/regex-2025.11.3-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:1eb1ebf6822b756c723e09f5186473d93236c06c579d2cc0671a722d2ab14281", size = 491984, upload-time = "2025-11-03T21:32:23.466Z" },
1744
+ { url = "https://files.pythonhosted.org/packages/df/ec/7014c15626ab46b902b3bcc4b28a7bae46d8f281fc7ea9c95e22fcaaa917/regex-2025.11.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:1e00ec2970aab10dc5db34af535f21fcf32b4a31d99e34963419636e2f85ae39", size = 292673, upload-time = "2025-11-03T21:32:25.034Z" },
1745
+ { url = "https://files.pythonhosted.org/packages/23/ab/3b952ff7239f20d05f1f99e9e20188513905f218c81d52fb5e78d2bf7634/regex-2025.11.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a4cb042b615245d5ff9b3794f56be4138b5adc35a4166014d31d1814744148c7", size = 291029, upload-time = "2025-11-03T21:32:26.528Z" },
1746
+ { url = "https://files.pythonhosted.org/packages/21/7e/3dc2749fc684f455f162dcafb8a187b559e2614f3826877d3844a131f37b/regex-2025.11.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:44f264d4bf02f3176467d90b294d59bf1db9fe53c141ff772f27a8b456b2a9ed", size = 807437, upload-time = "2025-11-03T21:32:28.363Z" },
1747
+ { url = "https://files.pythonhosted.org/packages/1b/0b/d529a85ab349c6a25d1ca783235b6e3eedf187247eab536797021f7126c6/regex-2025.11.3-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7be0277469bf3bd7a34a9c57c1b6a724532a0d235cd0dc4e7f4316f982c28b19", size = 873368, upload-time = "2025-11-03T21:32:30.4Z" },
1748
+ { url = "https://files.pythonhosted.org/packages/7d/18/2d868155f8c9e3e9d8f9e10c64e9a9f496bb8f7e037a88a8bed26b435af6/regex-2025.11.3-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0d31e08426ff4b5b650f68839f5af51a92a5b51abd8554a60c2fbc7c71f25d0b", size = 914921, upload-time = "2025-11-03T21:32:32.123Z" },
1749
+ { url = "https://files.pythonhosted.org/packages/2d/71/9d72ff0f354fa783fe2ba913c8734c3b433b86406117a8db4ea2bf1c7a2f/regex-2025.11.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e43586ce5bd28f9f285a6e729466841368c4a0353f6fd08d4ce4630843d3648a", size = 812708, upload-time = "2025-11-03T21:32:34.305Z" },
1750
+ { url = "https://files.pythonhosted.org/packages/e7/19/ce4bf7f5575c97f82b6e804ffb5c4e940c62609ab2a0d9538d47a7fdf7d4/regex-2025.11.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:0f9397d561a4c16829d4e6ff75202c1c08b68a3bdbfe29dbfcdb31c9830907c6", size = 795472, upload-time = "2025-11-03T21:32:36.364Z" },
1751
+ { url = "https://files.pythonhosted.org/packages/03/86/fd1063a176ffb7b2315f9a1b08d17b18118b28d9df163132615b835a26ee/regex-2025.11.3-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:dd16e78eb18ffdb25ee33a0682d17912e8cc8a770e885aeee95020046128f1ce", size = 868341, upload-time = "2025-11-03T21:32:38.042Z" },
1752
+ { url = "https://files.pythonhosted.org/packages/12/43/103fb2e9811205e7386366501bc866a164a0430c79dd59eac886a2822950/regex-2025.11.3-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:ffcca5b9efe948ba0661e9df0fa50d2bc4b097c70b9810212d6b62f05d83b2dd", size = 854666, upload-time = "2025-11-03T21:32:40.079Z" },
1753
+ { url = "https://files.pythonhosted.org/packages/7d/22/e392e53f3869b75804762c7c848bd2dd2abf2b70fb0e526f58724638bd35/regex-2025.11.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c56b4d162ca2b43318ac671c65bd4d563e841a694ac70e1a976ac38fcf4ca1d2", size = 799473, upload-time = "2025-11-03T21:32:42.148Z" },
1754
+ { url = "https://files.pythonhosted.org/packages/4f/f9/8bd6b656592f925b6845fcbb4d57603a3ac2fb2373344ffa1ed70aa6820a/regex-2025.11.3-cp313-cp313t-win32.whl", hash = "sha256:9ddc42e68114e161e51e272f667d640f97e84a2b9ef14b7477c53aac20c2d59a", size = 268792, upload-time = "2025-11-03T21:32:44.13Z" },
1755
+ { url = "https://files.pythonhosted.org/packages/e5/87/0e7d603467775ff65cd2aeabf1b5b50cc1c3708556a8b849a2fa4dd1542b/regex-2025.11.3-cp313-cp313t-win_amd64.whl", hash = "sha256:7a7c7fdf755032ffdd72c77e3d8096bdcb0eb92e89e17571a196f03d88b11b3c", size = 280214, upload-time = "2025-11-03T21:32:45.853Z" },
1756
+ { url = "https://files.pythonhosted.org/packages/8d/d0/2afc6f8e94e2b64bfb738a7c2b6387ac1699f09f032d363ed9447fd2bb57/regex-2025.11.3-cp313-cp313t-win_arm64.whl", hash = "sha256:df9eb838c44f570283712e7cff14c16329a9f0fb19ca492d21d4b7528ee6821e", size = 271469, upload-time = "2025-11-03T21:32:48.026Z" },
1757
+ { url = "https://files.pythonhosted.org/packages/31/e9/f6e13de7e0983837f7b6d238ad9458800a874bf37c264f7923e63409944c/regex-2025.11.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:9697a52e57576c83139d7c6f213d64485d3df5bf84807c35fa409e6c970801c6", size = 489089, upload-time = "2025-11-03T21:32:50.027Z" },
1758
+ { url = "https://files.pythonhosted.org/packages/a3/5c/261f4a262f1fa65141c1b74b255988bd2fa020cc599e53b080667d591cfc/regex-2025.11.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e18bc3f73bd41243c9b38a6d9f2366cd0e0137a9aebe2d8ff76c5b67d4c0a3f4", size = 291059, upload-time = "2025-11-03T21:32:51.682Z" },
1759
+ { url = "https://files.pythonhosted.org/packages/8e/57/f14eeb7f072b0e9a5a090d1712741fd8f214ec193dba773cf5410108bb7d/regex-2025.11.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:61a08bcb0ec14ff4e0ed2044aad948d0659604f824cbd50b55e30b0ec6f09c73", size = 288900, upload-time = "2025-11-03T21:32:53.569Z" },
1760
+ { url = "https://files.pythonhosted.org/packages/3c/6b/1d650c45e99a9b327586739d926a1cd4e94666b1bd4af90428b36af66dc7/regex-2025.11.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c9c30003b9347c24bcc210958c5d167b9e4f9be786cb380a7d32f14f9b84674f", size = 799010, upload-time = "2025-11-03T21:32:55.222Z" },
1761
+ { url = "https://files.pythonhosted.org/packages/99/ee/d66dcbc6b628ce4e3f7f0cbbb84603aa2fc0ffc878babc857726b8aab2e9/regex-2025.11.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4e1e592789704459900728d88d41a46fe3969b82ab62945560a31732ffc19a6d", size = 864893, upload-time = "2025-11-03T21:32:57.239Z" },
1762
+ { url = "https://files.pythonhosted.org/packages/bf/2d/f238229f1caba7ac87a6c4153d79947fb0261415827ae0f77c304260c7d3/regex-2025.11.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6538241f45eb5a25aa575dbba1069ad786f68a4f2773a29a2bd3dd1f9de787be", size = 911522, upload-time = "2025-11-03T21:32:59.274Z" },
1763
+ { url = "https://files.pythonhosted.org/packages/bd/3d/22a4eaba214a917c80e04f6025d26143690f0419511e0116508e24b11c9b/regex-2025.11.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bce22519c989bb72a7e6b36a199384c53db7722fe669ba891da75907fe3587db", size = 803272, upload-time = "2025-11-03T21:33:01.393Z" },
1764
+ { url = "https://files.pythonhosted.org/packages/84/b1/03188f634a409353a84b5ef49754b97dbcc0c0f6fd6c8ede505a8960a0a4/regex-2025.11.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:66d559b21d3640203ab9075797a55165d79017520685fb407b9234d72ab63c62", size = 787958, upload-time = "2025-11-03T21:33:03.379Z" },
1765
+ { url = "https://files.pythonhosted.org/packages/99/6a/27d072f7fbf6fadd59c64d210305e1ff865cc3b78b526fd147db768c553b/regex-2025.11.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:669dcfb2e38f9e8c69507bace46f4889e3abbfd9b0c29719202883c0a603598f", size = 859289, upload-time = "2025-11-03T21:33:05.374Z" },
1766
+ { url = "https://files.pythonhosted.org/packages/9a/70/1b3878f648e0b6abe023172dacb02157e685564853cc363d9961bcccde4e/regex-2025.11.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:32f74f35ff0f25a5021373ac61442edcb150731fbaa28286bbc8bb1582c89d02", size = 850026, upload-time = "2025-11-03T21:33:07.131Z" },
1767
+ { url = "https://files.pythonhosted.org/packages/dd/d5/68e25559b526b8baab8e66839304ede68ff6727237a47727d240006bd0ff/regex-2025.11.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e6c7a21dffba883234baefe91bc3388e629779582038f75d2a5be918e250f0ed", size = 789499, upload-time = "2025-11-03T21:33:09.141Z" },
1768
+ { url = "https://files.pythonhosted.org/packages/fc/df/43971264857140a350910d4e33df725e8c94dd9dee8d2e4729fa0d63d49e/regex-2025.11.3-cp314-cp314-win32.whl", hash = "sha256:795ea137b1d809eb6836b43748b12634291c0ed55ad50a7d72d21edf1cd565c4", size = 271604, upload-time = "2025-11-03T21:33:10.9Z" },
1769
+ { url = "https://files.pythonhosted.org/packages/01/6f/9711b57dc6894a55faf80a4c1b5aa4f8649805cb9c7aef46f7d27e2b9206/regex-2025.11.3-cp314-cp314-win_amd64.whl", hash = "sha256:9f95fbaa0ee1610ec0fc6b26668e9917a582ba80c52cc6d9ada15e30aa9ab9ad", size = 280320, upload-time = "2025-11-03T21:33:12.572Z" },
1770
+ { url = "https://files.pythonhosted.org/packages/f1/7e/f6eaa207d4377481f5e1775cdeb5a443b5a59b392d0065f3417d31d80f87/regex-2025.11.3-cp314-cp314-win_arm64.whl", hash = "sha256:dfec44d532be4c07088c3de2876130ff0fbeeacaa89a137decbbb5f665855a0f", size = 273372, upload-time = "2025-11-03T21:33:14.219Z" },
1771
+ { url = "https://files.pythonhosted.org/packages/c3/06/49b198550ee0f5e4184271cee87ba4dfd9692c91ec55289e6282f0f86ccf/regex-2025.11.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ba0d8a5d7f04f73ee7d01d974d47c5834f8a1b0224390e4fe7c12a3a92a78ecc", size = 491985, upload-time = "2025-11-03T21:33:16.555Z" },
1772
+ { url = "https://files.pythonhosted.org/packages/ce/bf/abdafade008f0b1c9da10d934034cb670432d6cf6cbe38bbb53a1cfd6cf8/regex-2025.11.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:442d86cf1cfe4faabf97db7d901ef58347efd004934da045c745e7b5bd57ac49", size = 292669, upload-time = "2025-11-03T21:33:18.32Z" },
1773
+ { url = "https://files.pythonhosted.org/packages/f9/ef/0c357bb8edbd2ad8e273fcb9e1761bc37b8acbc6e1be050bebd6475f19c1/regex-2025.11.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:fd0a5e563c756de210bb964789b5abe4f114dacae9104a47e1a649b910361536", size = 291030, upload-time = "2025-11-03T21:33:20.048Z" },
1774
+ { url = "https://files.pythonhosted.org/packages/79/06/edbb67257596649b8fb088d6aeacbcb248ac195714b18a65e018bf4c0b50/regex-2025.11.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf3490bcbb985a1ae97b2ce9ad1c0f06a852d5b19dde9b07bdf25bf224248c95", size = 807674, upload-time = "2025-11-03T21:33:21.797Z" },
1775
+ { url = "https://files.pythonhosted.org/packages/f4/d9/ad4deccfce0ea336296bd087f1a191543bb99ee1c53093dcd4c64d951d00/regex-2025.11.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3809988f0a8b8c9dcc0f92478d6501fac7200b9ec56aecf0ec21f4a2ec4b6009", size = 873451, upload-time = "2025-11-03T21:33:23.741Z" },
1776
+ { url = "https://files.pythonhosted.org/packages/13/75/a55a4724c56ef13e3e04acaab29df26582f6978c000ac9cd6810ad1f341f/regex-2025.11.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f4ff94e58e84aedb9c9fce66d4ef9f27a190285b451420f297c9a09f2b9abee9", size = 914980, upload-time = "2025-11-03T21:33:25.999Z" },
1777
+ { url = "https://files.pythonhosted.org/packages/67/1e/a1657ee15bd9116f70d4a530c736983eed997b361e20ecd8f5ca3759d5c5/regex-2025.11.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7eb542fd347ce61e1321b0a6b945d5701528dca0cd9759c2e3bb8bd57e47964d", size = 812852, upload-time = "2025-11-03T21:33:27.852Z" },
1778
+ { url = "https://files.pythonhosted.org/packages/b8/6f/f7516dde5506a588a561d296b2d0044839de06035bb486b326065b4c101e/regex-2025.11.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d6c2d5919075a1f2e413c00b056ea0c2f065b3f5fe83c3d07d325ab92dce51d6", size = 795566, upload-time = "2025-11-03T21:33:32.364Z" },
1779
+ { url = "https://files.pythonhosted.org/packages/d9/dd/3d10b9e170cc16fb34cb2cef91513cf3df65f440b3366030631b2984a264/regex-2025.11.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:3f8bf11a4827cc7ce5a53d4ef6cddd5ad25595d3c1435ef08f76825851343154", size = 868463, upload-time = "2025-11-03T21:33:34.459Z" },
1780
+ { url = "https://files.pythonhosted.org/packages/f5/8e/935e6beff1695aa9085ff83195daccd72acc82c81793df480f34569330de/regex-2025.11.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:22c12d837298651e5550ac1d964e4ff57c3f56965fc1812c90c9fb2028eaf267", size = 854694, upload-time = "2025-11-03T21:33:36.793Z" },
1781
+ { url = "https://files.pythonhosted.org/packages/92/12/10650181a040978b2f5720a6a74d44f841371a3d984c2083fc1752e4acf6/regex-2025.11.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:62ba394a3dda9ad41c7c780f60f6e4a70988741415ae96f6d1bf6c239cf01379", size = 799691, upload-time = "2025-11-03T21:33:39.079Z" },
1782
+ { url = "https://files.pythonhosted.org/packages/67/90/8f37138181c9a7690e7e4cb388debbd389342db3c7381d636d2875940752/regex-2025.11.3-cp314-cp314t-win32.whl", hash = "sha256:4bf146dca15cdd53224a1bf46d628bd7590e4a07fbb69e720d561aea43a32b38", size = 274583, upload-time = "2025-11-03T21:33:41.302Z" },
1783
+ { url = "https://files.pythonhosted.org/packages/8f/cd/867f5ec442d56beb56f5f854f40abcfc75e11d10b11fdb1869dd39c63aaf/regex-2025.11.3-cp314-cp314t-win_amd64.whl", hash = "sha256:adad1a1bcf1c9e76346e091d22d23ac54ef28e1365117d99521631078dfec9de", size = 284286, upload-time = "2025-11-03T21:33:43.324Z" },
1784
+ { url = "https://files.pythonhosted.org/packages/20/31/32c0c4610cbc070362bf1d2e4ea86d1ea29014d400a6d6c2486fcfd57766/regex-2025.11.3-cp314-cp314t-win_arm64.whl", hash = "sha256:c54f768482cef41e219720013cd05933b6f971d9562544d691c68699bf2b6801", size = 274741, upload-time = "2025-11-03T21:33:45.557Z" },
1785
+ ]
1786
+
1787
  [[package]]
1788
  name = "requests"
1789
  version = "2.31.0"
 
1944
  { url = "https://files.pythonhosted.org/packages/a9/9b/770da4f22ea69fdd19ac6cb183f02abf8fc6f8d16a9dc00dfaf667649ee7/ruff-0.1.11-py3-none-win_arm64.whl", hash = "sha256:97ce4d752f964ba559c7023a86e5f8e97f026d511e48013987623915431c7ea9", size = 6827536, upload-time = "2024-01-02T22:59:08.394Z" },
1945
  ]
1946
 
1947
+ [[package]]
1948
+ name = "safetensors"
1949
+ version = "0.7.0"
1950
+ source = { registry = "https://pypi.org/simple" }
1951
+ sdist = { url = "https://files.pythonhosted.org/packages/29/9c/6e74567782559a63bd040a236edca26fd71bc7ba88de2ef35d75df3bca5e/safetensors-0.7.0.tar.gz", hash = "sha256:07663963b67e8bd9f0b8ad15bb9163606cd27cc5a1b96235a50d8369803b96b0", size = 200878, upload-time = "2025-11-19T15:18:43.199Z" }
1952
+ wheels = [
1953
+ { url = "https://files.pythonhosted.org/packages/fa/47/aef6c06649039accf914afef490268e1067ed82be62bcfa5b7e886ad15e8/safetensors-0.7.0-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:c82f4d474cf725255d9e6acf17252991c3c8aac038d6ef363a4bf8be2f6db517", size = 467781, upload-time = "2025-11-19T15:18:35.84Z" },
1954
+ { url = "https://files.pythonhosted.org/packages/e8/00/374c0c068e30cd31f1e1b46b4b5738168ec79e7689ca82ee93ddfea05109/safetensors-0.7.0-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:94fd4858284736bb67a897a41608b5b0c2496c9bdb3bf2af1fa3409127f20d57", size = 447058, upload-time = "2025-11-19T15:18:34.416Z" },
1955
+ { url = "https://files.pythonhosted.org/packages/f1/06/578ffed52c2296f93d7fd2d844cabfa92be51a587c38c8afbb8ae449ca89/safetensors-0.7.0-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e07d91d0c92a31200f25351f4acb2bc6aff7f48094e13ebb1d0fb995b54b6542", size = 491748, upload-time = "2025-11-19T15:18:09.79Z" },
1956
+ { url = "https://files.pythonhosted.org/packages/ae/33/1debbbb70e4791dde185edb9413d1fe01619255abb64b300157d7f15dddd/safetensors-0.7.0-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8469155f4cb518bafb4acf4865e8bb9d6804110d2d9bdcaa78564b9fd841e104", size = 503881, upload-time = "2025-11-19T15:18:16.145Z" },
1957
+ { url = "https://files.pythonhosted.org/packages/8e/1c/40c2ca924d60792c3be509833df711b553c60effbd91da6f5284a83f7122/safetensors-0.7.0-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:54bef08bf00a2bff599982f6b08e8770e09cc012d7bba00783fc7ea38f1fb37d", size = 623463, upload-time = "2025-11-19T15:18:21.11Z" },
1958
+ { url = "https://files.pythonhosted.org/packages/9b/3a/13784a9364bd43b0d61eef4bea2845039bc2030458b16594a1bd787ae26e/safetensors-0.7.0-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:42cb091236206bb2016d245c377ed383aa7f78691748f3bb6ee1bfa51ae2ce6a", size = 532855, upload-time = "2025-11-19T15:18:25.719Z" },
1959
+ { url = "https://files.pythonhosted.org/packages/a0/60/429e9b1cb3fc651937727befe258ea24122d9663e4d5709a48c9cbfceecb/safetensors-0.7.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dac7252938f0696ddea46f5e855dd3138444e82236e3be475f54929f0c510d48", size = 507152, upload-time = "2025-11-19T15:18:33.023Z" },
1960
+ { url = "https://files.pythonhosted.org/packages/3c/a8/4b45e4e059270d17af60359713ffd83f97900d45a6afa73aaa0d737d48b6/safetensors-0.7.0-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1d060c70284127fa805085d8f10fbd0962792aed71879d00864acda69dbab981", size = 541856, upload-time = "2025-11-19T15:18:31.075Z" },
1961
+ { url = "https://files.pythonhosted.org/packages/06/87/d26d8407c44175d8ae164a95b5a62707fcc445f3c0c56108e37d98070a3d/safetensors-0.7.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:cdab83a366799fa730f90a4ebb563e494f28e9e92c4819e556152ad55e43591b", size = 674060, upload-time = "2025-11-19T15:18:37.211Z" },
1962
+ { url = "https://files.pythonhosted.org/packages/11/f5/57644a2ff08dc6325816ba7217e5095f17269dada2554b658442c66aed51/safetensors-0.7.0-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:672132907fcad9f2aedcb705b2d7b3b93354a2aec1b2f706c4db852abe338f85", size = 771715, upload-time = "2025-11-19T15:18:38.689Z" },
1963
+ { url = "https://files.pythonhosted.org/packages/86/31/17883e13a814bd278ae6e266b13282a01049b0c81341da7fd0e3e71a80a3/safetensors-0.7.0-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:5d72abdb8a4d56d4020713724ba81dac065fedb7f3667151c4a637f1d3fb26c0", size = 714377, upload-time = "2025-11-19T15:18:40.162Z" },
1964
+ { url = "https://files.pythonhosted.org/packages/4a/d8/0c8a7dc9b41dcac53c4cbf9df2b9c83e0e0097203de8b37a712b345c0be5/safetensors-0.7.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b0f6d66c1c538d5a94a73aa9ddca8ccc4227e6c9ff555322ea40bdd142391dd4", size = 677368, upload-time = "2025-11-19T15:18:41.627Z" },
1965
+ { url = "https://files.pythonhosted.org/packages/05/e5/cb4b713c8a93469e3c5be7c3f8d77d307e65fe89673e731f5c2bfd0a9237/safetensors-0.7.0-cp38-abi3-win32.whl", hash = "sha256:c74af94bf3ac15ac4d0f2a7c7b4663a15f8c2ab15ed0fc7531ca61d0835eccba", size = 326423, upload-time = "2025-11-19T15:18:45.74Z" },
1966
+ { url = "https://files.pythonhosted.org/packages/5d/e6/ec8471c8072382cb91233ba7267fd931219753bb43814cbc71757bfd4dab/safetensors-0.7.0-cp38-abi3-win_amd64.whl", hash = "sha256:d1239932053f56f3456f32eb9625590cc7582e905021f94636202a864d470755", size = 341380, upload-time = "2025-11-19T15:18:44.427Z" },
1967
+ ]
1968
+
1969
  [[package]]
1970
  name = "scikit-learn"
1971
  version = "1.3.2"
 
2061
  { url = "https://files.pythonhosted.org/packages/64/47/a494741db7280eae6dc033510c319e34d42dd41b7ac0c7ead39354d1a2b5/scipy-1.16.3-cp314-cp314t-win_arm64.whl", hash = "sha256:21d9d6b197227a12dcbf9633320a4e34c6b0e51c57268df255a0942983bac562", size = 26464127, upload-time = "2025-10-28T17:38:11.34Z" },
2062
  ]
2063
 
2064
+ [[package]]
2065
+ name = "sentencepiece"
2066
+ version = "0.2.1"
2067
+ source = { registry = "https://pypi.org/simple" }
2068
+ sdist = { url = "https://files.pythonhosted.org/packages/15/15/2e7a025fc62d764b151ae6d0f2a92f8081755ebe8d4a64099accc6f77ba6/sentencepiece-0.2.1.tar.gz", hash = "sha256:8138cec27c2f2282f4a34d9a016e3374cd40e5c6e9cb335063db66a0a3b71fad", size = 3228515, upload-time = "2025-08-12T07:00:51.718Z" }
2069
+ wheels = [
2070
+ { url = "https://files.pythonhosted.org/packages/d8/15/46afbab00733d81788b64be430ca1b93011bb9388527958e26cc31832de5/sentencepiece-0.2.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6356d0986b8b8dc351b943150fcd81a1c6e6e4d439772e8584c64230e58ca987", size = 1942560, upload-time = "2025-08-12T06:59:25.82Z" },
2071
+ { url = "https://files.pythonhosted.org/packages/fa/79/7c01b8ef98a0567e9d84a4e7a910f8e7074fcbf398a5cd76f93f4b9316f9/sentencepiece-0.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8f8ba89a3acb3dc1ae90f65ec1894b0b9596fdb98ab003ff38e058f898b39bc7", size = 1325385, upload-time = "2025-08-12T06:59:27.722Z" },
2072
+ { url = "https://files.pythonhosted.org/packages/bb/88/2b41e07bd24f33dcf2f18ec3b74247aa4af3526bad8907b8727ea3caba03/sentencepiece-0.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:02593eca45440ef39247cee8c47322a34bdcc1d8ae83ad28ba5a899a2cf8d79a", size = 1253319, upload-time = "2025-08-12T06:59:29.306Z" },
2073
+ { url = "https://files.pythonhosted.org/packages/a0/54/38a1af0c6210a3c6f95aa46d23d6640636d020fba7135cd0d9a84ada05a7/sentencepiece-0.2.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a0d15781a171d188b661ae4bde1d998c303f6bd8621498c50c671bd45a4798e", size = 1316162, upload-time = "2025-08-12T06:59:30.914Z" },
2074
+ { url = "https://files.pythonhosted.org/packages/ef/66/fb191403ade791ad2c3c1e72fe8413e63781b08cfa3aa4c9dfc536d6e795/sentencepiece-0.2.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f5a3e0d9f445ed9d66c0fec47d4b23d12cfc858b407a03c194c1b26c2ac2a63", size = 1387785, upload-time = "2025-08-12T06:59:32.491Z" },
2075
+ { url = "https://files.pythonhosted.org/packages/a9/2d/3bd9b08e70067b2124518b308db6a84a4f8901cc8a4317e2e4288cdd9b4d/sentencepiece-0.2.1-cp311-cp311-win32.whl", hash = "sha256:6d297a1748d429ba8534eebe5535448d78b8acc32d00a29b49acf28102eeb094", size = 999555, upload-time = "2025-08-12T06:59:34.475Z" },
2076
+ { url = "https://files.pythonhosted.org/packages/32/b8/f709977f5fda195ae1ea24f24e7c581163b6f142b1005bc3d0bbfe4d7082/sentencepiece-0.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:82d9ead6591015f009cb1be1cb1c015d5e6f04046dbb8c9588b931e869a29728", size = 1054617, upload-time = "2025-08-12T06:59:36.461Z" },
2077
+ { url = "https://files.pythonhosted.org/packages/7a/40/a1fc23be23067da0f703709797b464e8a30a1c78cc8a687120cd58d4d509/sentencepiece-0.2.1-cp311-cp311-win_arm64.whl", hash = "sha256:39f8651bd10974eafb9834ce30d9bcf5b73e1fc798a7f7d2528f9820ca86e119", size = 1033877, upload-time = "2025-08-12T06:59:38.391Z" },
2078
+ { url = "https://files.pythonhosted.org/packages/4a/be/32ce495aa1d0e0c323dcb1ba87096037358edee539cac5baf8755a6bd396/sentencepiece-0.2.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:57cae326c8727de58c85977b175af132a7138d84c764635d7e71bbee7e774133", size = 1943152, upload-time = "2025-08-12T06:59:40.048Z" },
2079
+ { url = "https://files.pythonhosted.org/packages/88/7e/ff23008899a58678e98c6ff592bf4d368eee5a71af96d0df6b38a039dd4f/sentencepiece-0.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:56dd39a3c4d6493db3cdca7e8cc68c6b633f0d4195495cbadfcf5af8a22d05a6", size = 1325651, upload-time = "2025-08-12T06:59:41.536Z" },
2080
+ { url = "https://files.pythonhosted.org/packages/19/84/42eb3ce4796777a1b5d3699dfd4dca85113e68b637f194a6c8d786f16a04/sentencepiece-0.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d9381351182ff9888cc80e41c632e7e274b106f450de33d67a9e8f6043da6f76", size = 1253645, upload-time = "2025-08-12T06:59:42.903Z" },
2081
+ { url = "https://files.pythonhosted.org/packages/89/fa/d3d5ebcba3cb9e6d3775a096251860c41a6bc53a1b9461151df83fe93255/sentencepiece-0.2.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:99f955df238021bf11f0fc37cdb54fd5e5b5f7fd30ecc3d93fb48b6815437167", size = 1316273, upload-time = "2025-08-12T06:59:44.476Z" },
2082
+ { url = "https://files.pythonhosted.org/packages/04/88/14f2f4a2b922d8b39be45bf63d79e6cd3a9b2f248b2fcb98a69b12af12f5/sentencepiece-0.2.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0cdfecef430d985f1c2bcbfff3defd1d95dae876fbd0173376012d2d7d24044b", size = 1387881, upload-time = "2025-08-12T06:59:46.09Z" },
2083
+ { url = "https://files.pythonhosted.org/packages/fd/b8/903e5ccb77b4ef140605d5d71b4f9e0ad95d456d6184688073ed11712809/sentencepiece-0.2.1-cp312-cp312-win32.whl", hash = "sha256:a483fd29a34c3e34c39ac5556b0a90942bec253d260235729e50976f5dba1068", size = 999540, upload-time = "2025-08-12T06:59:48.023Z" },
2084
+ { url = "https://files.pythonhosted.org/packages/2d/81/92df5673c067148c2545b1bfe49adfd775bcc3a169a047f5a0e6575ddaca/sentencepiece-0.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:4cdc7c36234fda305e85c32949c5211faaf8dd886096c7cea289ddc12a2d02de", size = 1054671, upload-time = "2025-08-12T06:59:49.895Z" },
2085
+ { url = "https://files.pythonhosted.org/packages/fe/02/c5e3bc518655d714622bec87d83db9cdba1cd0619a4a04e2109751c4f47f/sentencepiece-0.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:daeb5e9e9fcad012324807856113708614d534f596d5008638eb9b40112cd9e4", size = 1033923, upload-time = "2025-08-12T06:59:51.952Z" },
2086
+ { url = "https://files.pythonhosted.org/packages/ba/4a/85fbe1706d4d04a7e826b53f327c4b80f849cf1c7b7c5e31a20a97d8f28b/sentencepiece-0.2.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:dcd8161eee7b41aae57ded06272905dbd680a0a04b91edd0f64790c796b2f706", size = 1943150, upload-time = "2025-08-12T06:59:53.588Z" },
2087
+ { url = "https://files.pythonhosted.org/packages/c2/83/4cfb393e287509fc2155480b9d184706ef8d9fa8cbf5505d02a5792bf220/sentencepiece-0.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c6c8f42949f419ff8c7e9960dbadcfbc982d7b5efc2f6748210d3dd53a7de062", size = 1325651, upload-time = "2025-08-12T06:59:55.073Z" },
2088
+ { url = "https://files.pythonhosted.org/packages/8d/de/5a007fb53b1ab0aafc69d11a5a3dd72a289d5a3e78dcf2c3a3d9b14ffe93/sentencepiece-0.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:097f3394e99456e9e4efba1737c3749d7e23563dd1588ce71a3d007f25475fff", size = 1253641, upload-time = "2025-08-12T06:59:56.562Z" },
2089
+ { url = "https://files.pythonhosted.org/packages/2c/d2/f552be5928105588f4f4d66ee37dd4c61460d8097e62d0e2e0eec41bc61d/sentencepiece-0.2.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d7b670879c370d350557edabadbad1f6561a9e6968126e6debca4029e5547820", size = 1316271, upload-time = "2025-08-12T06:59:58.109Z" },
2090
+ { url = "https://files.pythonhosted.org/packages/96/df/0cfe748ace5485be740fed9476dee7877f109da32ed0d280312c94ec259f/sentencepiece-0.2.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c7f0fd2f2693309e6628aeeb2e2faf6edd221134dfccac3308ca0de01f8dab47", size = 1387882, upload-time = "2025-08-12T07:00:00.701Z" },
2091
+ { url = "https://files.pythonhosted.org/packages/ac/dd/f7774d42a881ced8e1739f393ab1e82ece39fc9abd4779e28050c2e975b5/sentencepiece-0.2.1-cp313-cp313-win32.whl", hash = "sha256:92b3816aa2339355fda2c8c4e021a5de92180b00aaccaf5e2808972e77a4b22f", size = 999541, upload-time = "2025-08-12T07:00:02.709Z" },
2092
+ { url = "https://files.pythonhosted.org/packages/dd/e9/932b9eae6fd7019548321eee1ab8d5e3b3d1294df9d9a0c9ac517c7b636d/sentencepiece-0.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:10ed3dab2044c47f7a2e7b4969b0c430420cdd45735d78c8f853191fa0e3148b", size = 1054669, upload-time = "2025-08-12T07:00:04.915Z" },
2093
+ { url = "https://files.pythonhosted.org/packages/c9/3a/76488a00ea7d6931689cda28726a1447d66bf1a4837943489314593d5596/sentencepiece-0.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:ac650534e2251083c5f75dde4ff28896ce7c8904133dc8fef42780f4d5588fcd", size = 1033922, upload-time = "2025-08-12T07:00:06.496Z" },
2094
+ { url = "https://files.pythonhosted.org/packages/4a/b6/08fe2ce819e02ccb0296f4843e3f195764ce9829cbda61b7513f29b95718/sentencepiece-0.2.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:8dd4b477a7b069648d19363aad0cab9bad2f4e83b2d179be668efa672500dc94", size = 1946052, upload-time = "2025-08-12T07:00:08.136Z" },
2095
+ { url = "https://files.pythonhosted.org/packages/ab/d9/1ea0e740591ff4c6fc2b6eb1d7510d02f3fb885093f19b2f3abd1363b402/sentencepiece-0.2.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0c0f672da370cc490e4c59d89e12289778310a0e71d176c541e4834759e1ae07", size = 1327408, upload-time = "2025-08-12T07:00:09.572Z" },
2096
+ { url = "https://files.pythonhosted.org/packages/99/7e/1fb26e8a21613f6200e1ab88824d5d203714162cf2883248b517deb500b7/sentencepiece-0.2.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:ad8493bea8432dae8d6830365352350f3b4144415a1d09c4c8cb8d30cf3b6c3c", size = 1254857, upload-time = "2025-08-12T07:00:11.021Z" },
2097
+ { url = "https://files.pythonhosted.org/packages/bc/85/c72fd1f3c7a6010544d6ae07f8ddb38b5e2a7e33bd4318f87266c0bbafbf/sentencepiece-0.2.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b81a24733726e3678d2db63619acc5a8dccd074f7aa7a54ecd5ca33ca6d2d596", size = 1315722, upload-time = "2025-08-12T07:00:12.989Z" },
2098
+ { url = "https://files.pythonhosted.org/packages/4a/e8/661e5bd82a8aa641fd6c1020bd0e890ef73230a2b7215ddf9c8cd8e941c2/sentencepiece-0.2.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0a81799d0a68d618e89063fb423c3001a034c893069135ffe51fee439ae474d6", size = 1387452, upload-time = "2025-08-12T07:00:15.088Z" },
2099
+ { url = "https://files.pythonhosted.org/packages/99/5e/ae66c361023a470afcbc1fbb8da722c72ea678a2fcd9a18f1a12598c7501/sentencepiece-0.2.1-cp313-cp313t-win32.whl", hash = "sha256:89a3ea015517c42c0341d0d962f3e6aaf2cf10d71b1932d475c44ba48d00aa2b", size = 1002501, upload-time = "2025-08-12T07:00:16.966Z" },
2100
+ { url = "https://files.pythonhosted.org/packages/c1/03/d332828c4ff764e16c1b56c2c8f9a33488bbe796b53fb6b9c4205ddbf167/sentencepiece-0.2.1-cp313-cp313t-win_amd64.whl", hash = "sha256:33f068c9382dc2e7c228eedfd8163b52baa86bb92f50d0488bf2b7da7032e484", size = 1057555, upload-time = "2025-08-12T07:00:18.573Z" },
2101
+ { url = "https://files.pythonhosted.org/packages/88/14/5aee0bf0864df9bd82bd59e7711362908e4935e3f9cdc1f57246b5d5c9b9/sentencepiece-0.2.1-cp313-cp313t-win_arm64.whl", hash = "sha256:b3616ad246f360e52c85781e47682d31abfb6554c779e42b65333d4b5f44ecc0", size = 1036042, upload-time = "2025-08-12T07:00:20.209Z" },
2102
+ { url = "https://files.pythonhosted.org/packages/24/9c/89eb8b2052f720a612478baf11c8227dcf1dc28cd4ea4c0c19506b5af2a2/sentencepiece-0.2.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:5d0350b686c320068702116276cfb26c066dc7e65cfef173980b11bb4d606719", size = 1943147, upload-time = "2025-08-12T07:00:21.809Z" },
2103
+ { url = "https://files.pythonhosted.org/packages/82/0b/a1432bc87f97c2ace36386ca23e8bd3b91fb40581b5e6148d24b24186419/sentencepiece-0.2.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:c7f54a31cde6fa5cb030370566f68152a742f433f8d2be458463d06c208aef33", size = 1325624, upload-time = "2025-08-12T07:00:23.289Z" },
2104
+ { url = "https://files.pythonhosted.org/packages/ea/99/bbe054ebb5a5039457c590e0a4156ed073fb0fe9ce4f7523404dd5b37463/sentencepiece-0.2.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c83b85ab2d6576607f31df77ff86f28182be4a8de6d175d2c33ca609925f5da1", size = 1253670, upload-time = "2025-08-12T07:00:24.69Z" },
2105
+ { url = "https://files.pythonhosted.org/packages/19/ad/d5c7075f701bd97971d7c2ac2904f227566f51ef0838dfbdfdccb58cd212/sentencepiece-0.2.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1855f57db07b51fb51ed6c9c452f570624d2b169b36f0f79ef71a6e6c618cd8b", size = 1316247, upload-time = "2025-08-12T07:00:26.435Z" },
2106
+ { url = "https://files.pythonhosted.org/packages/fb/03/35fbe5f3d9a7435eebd0b473e09584bd3cc354ce118b960445b060d33781/sentencepiece-0.2.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01e6912125cb45d3792f530a4d38f8e21bf884d6b4d4ade1b2de5cf7a8d2a52b", size = 1387894, upload-time = "2025-08-12T07:00:28.339Z" },
2107
+ { url = "https://files.pythonhosted.org/packages/dc/aa/956ef729aafb6c8f9c443104c9636489093bb5c61d6b90fc27aa1a865574/sentencepiece-0.2.1-cp314-cp314-win32.whl", hash = "sha256:c415c9de1447e0a74ae3fdb2e52f967cb544113a3a5ce3a194df185cbc1f962f", size = 1096698, upload-time = "2025-08-12T07:00:29.764Z" },
2108
+ { url = "https://files.pythonhosted.org/packages/b8/cb/fe400d8836952cc535c81a0ce47dc6875160e5fedb71d2d9ff0e9894c2a6/sentencepiece-0.2.1-cp314-cp314-win_amd64.whl", hash = "sha256:881b2e44b14fc19feade3cbed314be37de639fc415375cefaa5bc81a4be137fd", size = 1155115, upload-time = "2025-08-12T07:00:32.865Z" },
2109
+ { url = "https://files.pythonhosted.org/packages/32/89/047921cf70f36c7b6b6390876b2399b3633ab73b8d0cb857e5a964238941/sentencepiece-0.2.1-cp314-cp314-win_arm64.whl", hash = "sha256:2005242a16d2dc3ac5fe18aa7667549134d37854823df4c4db244752453b78a8", size = 1133890, upload-time = "2025-08-12T07:00:34.763Z" },
2110
+ { url = "https://files.pythonhosted.org/packages/a1/11/5b414b9fae6255b5fb1e22e2ed3dc3a72d3a694e5703910e640ac78346bb/sentencepiece-0.2.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:a19adcec27c524cb7069a1c741060add95f942d1cbf7ad0d104dffa0a7d28a2b", size = 1946081, upload-time = "2025-08-12T07:00:36.97Z" },
2111
+ { url = "https://files.pythonhosted.org/packages/77/eb/7a5682bb25824db8545f8e5662e7f3e32d72a508fdce086029d89695106b/sentencepiece-0.2.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:e37e4b4c4a11662b5db521def4e44d4d30ae69a1743241412a93ae40fdcab4bb", size = 1327406, upload-time = "2025-08-12T07:00:38.669Z" },
2112
+ { url = "https://files.pythonhosted.org/packages/03/b0/811dae8fb9f2784e138785d481469788f2e0d0c109c5737372454415f55f/sentencepiece-0.2.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:477c81505db072b3ab627e7eab972ea1025331bd3a92bacbf798df2b75ea86ec", size = 1254846, upload-time = "2025-08-12T07:00:40.611Z" },
2113
+ { url = "https://files.pythonhosted.org/packages/ef/23/195b2e7ec85ebb6a547969f60b723c7aca5a75800ece6cc3f41da872d14e/sentencepiece-0.2.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:010f025a544ef770bb395091d57cb94deb9652d8972e0d09f71d85d5a0816c8c", size = 1315721, upload-time = "2025-08-12T07:00:42.914Z" },
2114
+ { url = "https://files.pythonhosted.org/packages/7e/aa/553dbe4178b5f23eb28e59393dddd64186178b56b81d9b8d5c3ff1c28395/sentencepiece-0.2.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:733e59ff1794d26db706cd41fc2d7ca5f6c64a820709cb801dc0ea31780d64ab", size = 1387458, upload-time = "2025-08-12T07:00:44.56Z" },
2115
+ { url = "https://files.pythonhosted.org/packages/66/7c/08ff0012507297a4dd74a5420fdc0eb9e3e80f4e88cab1538d7f28db303d/sentencepiece-0.2.1-cp314-cp314t-win32.whl", hash = "sha256:d3233770f78e637dc8b1fda2cd7c3b99ec77e7505041934188a4e7fe751de3b0", size = 1099765, upload-time = "2025-08-12T07:00:46.058Z" },
2116
+ { url = "https://files.pythonhosted.org/packages/91/d5/2a69e1ce15881beb9ddfc7e3f998322f5cedcd5e4d244cb74dade9441663/sentencepiece-0.2.1-cp314-cp314t-win_amd64.whl", hash = "sha256:5e4366c97b68218fd30ea72d70c525e6e78a6c0a88650f57ac4c43c63b234a9d", size = 1157807, upload-time = "2025-08-12T07:00:47.673Z" },
2117
+ { url = "https://files.pythonhosted.org/packages/f3/16/54f611fcfc2d1c46cbe3ec4169780b2cfa7cf63708ef2b71611136db7513/sentencepiece-0.2.1-cp314-cp314t-win_arm64.whl", hash = "sha256:105e36e75cbac1292642045458e8da677b2342dcd33df503e640f0b457cb6751", size = 1136264, upload-time = "2025-08-12T07:00:49.485Z" },
2118
+ ]
2119
+
2120
  [[package]]
2121
  name = "six"
2122
  version = "1.17.0"
 
2199
  { url = "https://files.pythonhosted.org/packages/32/d5/f9a850d79b0851d1d4ef6456097579a9005b31fea68726a4ae5f2d82ddd9/threadpoolctl-3.6.0-py3-none-any.whl", hash = "sha256:43a0b8fd5a2928500110039e43a5eed8480b918967083ea48dc3ab9f13c4a7fb", size = 18638, upload-time = "2025-03-13T13:49:21.846Z" },
2200
  ]
2201
 
2202
+ [[package]]
2203
+ name = "tokenizers"
2204
+ version = "0.22.1"
2205
+ source = { registry = "https://pypi.org/simple" }
2206
+ dependencies = [
2207
+ { name = "huggingface-hub" },
2208
+ ]
2209
+ sdist = { url = "https://files.pythonhosted.org/packages/1c/46/fb6854cec3278fbfa4a75b50232c77622bc517ac886156e6afbfa4d8fc6e/tokenizers-0.22.1.tar.gz", hash = "sha256:61de6522785310a309b3407bac22d99c4db5dba349935e99e4d15ea2226af2d9", size = 363123, upload-time = "2025-09-19T09:49:23.424Z" }
2210
+ wheels = [
2211
+ { url = "https://files.pythonhosted.org/packages/bf/33/f4b2d94ada7ab297328fc671fed209368ddb82f965ec2224eb1892674c3a/tokenizers-0.22.1-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:59fdb013df17455e5f950b4b834a7b3ee2e0271e6378ccb33aa74d178b513c73", size = 3069318, upload-time = "2025-09-19T09:49:11.848Z" },
2212
+ { url = "https://files.pythonhosted.org/packages/1c/58/2aa8c874d02b974990e89ff95826a4852a8b2a273c7d1b4411cdd45a4565/tokenizers-0.22.1-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:8d4e484f7b0827021ac5f9f71d4794aaef62b979ab7608593da22b1d2e3c4edc", size = 2926478, upload-time = "2025-09-19T09:49:09.759Z" },
2213
+ { url = "https://files.pythonhosted.org/packages/1e/3b/55e64befa1e7bfea963cf4b787b2cea1011362c4193f5477047532ce127e/tokenizers-0.22.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19d2962dd28bc67c1f205ab180578a78eef89ac60ca7ef7cbe9635a46a56422a", size = 3256994, upload-time = "2025-09-19T09:48:56.701Z" },
2214
+ { url = "https://files.pythonhosted.org/packages/71/0b/fbfecf42f67d9b7b80fde4aabb2b3110a97fac6585c9470b5bff103a80cb/tokenizers-0.22.1-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:38201f15cdb1f8a6843e6563e6e79f4abd053394992b9bbdf5213ea3469b4ae7", size = 3153141, upload-time = "2025-09-19T09:48:59.749Z" },
2215
+ { url = "https://files.pythonhosted.org/packages/17/a9/b38f4e74e0817af8f8ef925507c63c6ae8171e3c4cb2d5d4624bf58fca69/tokenizers-0.22.1-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d1cbe5454c9a15df1b3443c726063d930c16f047a3cc724b9e6e1a91140e5a21", size = 3508049, upload-time = "2025-09-19T09:49:05.868Z" },
2216
+ { url = "https://files.pythonhosted.org/packages/d2/48/dd2b3dac46bb9134a88e35d72e1aa4869579eacc1a27238f1577270773ff/tokenizers-0.22.1-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e7d094ae6312d69cc2a872b54b91b309f4f6fbce871ef28eb27b52a98e4d0214", size = 3710730, upload-time = "2025-09-19T09:49:01.832Z" },
2217
+ { url = "https://files.pythonhosted.org/packages/93/0e/ccabc8d16ae4ba84a55d41345207c1e2ea88784651a5a487547d80851398/tokenizers-0.22.1-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:afd7594a56656ace95cdd6df4cca2e4059d294c5cfb1679c57824b605556cb2f", size = 3412560, upload-time = "2025-09-19T09:49:03.867Z" },
2218
+ { url = "https://files.pythonhosted.org/packages/d0/c6/dc3a0db5a6766416c32c034286d7c2d406da1f498e4de04ab1b8959edd00/tokenizers-0.22.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2ef6063d7a84994129732b47e7915e8710f27f99f3a3260b8a38fc7ccd083f4", size = 3250221, upload-time = "2025-09-19T09:49:07.664Z" },
2219
+ { url = "https://files.pythonhosted.org/packages/d7/a6/2c8486eef79671601ff57b093889a345dd3d576713ef047776015dc66de7/tokenizers-0.22.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ba0a64f450b9ef412c98f6bcd2a50c6df6e2443b560024a09fa6a03189726879", size = 9345569, upload-time = "2025-09-19T09:49:14.214Z" },
2220
+ { url = "https://files.pythonhosted.org/packages/6b/16/32ce667f14c35537f5f605fe9bea3e415ea1b0a646389d2295ec348d5657/tokenizers-0.22.1-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:331d6d149fa9c7d632cde4490fb8bbb12337fa3a0232e77892be656464f4b446", size = 9271599, upload-time = "2025-09-19T09:49:16.639Z" },
2221
+ { url = "https://files.pythonhosted.org/packages/51/7c/a5f7898a3f6baa3fc2685c705e04c98c1094c523051c805cdd9306b8f87e/tokenizers-0.22.1-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:607989f2ea68a46cb1dfbaf3e3aabdf3f21d8748312dbeb6263d1b3b66c5010a", size = 9533862, upload-time = "2025-09-19T09:49:19.146Z" },
2222
+ { url = "https://files.pythonhosted.org/packages/36/65/7e75caea90bc73c1dd8d40438adf1a7bc26af3b8d0a6705ea190462506e1/tokenizers-0.22.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a0f307d490295717726598ef6fa4f24af9d484809223bbc253b201c740a06390", size = 9681250, upload-time = "2025-09-19T09:49:21.501Z" },
2223
+ { url = "https://files.pythonhosted.org/packages/30/2c/959dddef581b46e6209da82df3b78471e96260e2bc463f89d23b1bf0e52a/tokenizers-0.22.1-cp39-abi3-win32.whl", hash = "sha256:b5120eed1442765cd90b903bb6cfef781fd8fe64e34ccaecbae4c619b7b12a82", size = 2472003, upload-time = "2025-09-19T09:49:27.089Z" },
2224
+ { url = "https://files.pythonhosted.org/packages/b3/46/e33a8c93907b631a99377ef4c5f817ab453d0b34f93529421f42ff559671/tokenizers-0.22.1-cp39-abi3-win_amd64.whl", hash = "sha256:65fd6e3fb11ca1e78a6a93602490f134d1fdeb13bcef99389d5102ea318ed138", size = 2674684, upload-time = "2025-09-19T09:49:24.953Z" },
2225
+ ]
2226
+
2227
  [[package]]
2228
  name = "toml"
2229
  version = "0.10.2"
 
2301
  { url = "https://files.pythonhosted.org/packages/5e/4f/e1f65e8f8c76d73658b33d33b81eed4322fb5085350e4328d5c956f0c8f9/tornado-6.5.2-cp39-abi3-win_arm64.whl", hash = "sha256:d6c33dc3672e3a1f3618eb63b7ef4683a7688e7b9e6e8f0d9aa5726360a004af", size = 444456, upload-time = "2025-08-08T18:26:59.207Z" },
2302
  ]
2303
 
2304
+ [[package]]
2305
+ name = "tqdm"
2306
+ version = "4.67.1"
2307
+ source = { registry = "https://pypi.org/simple" }
2308
+ dependencies = [
2309
+ { name = "colorama", marker = "sys_platform == 'win32'" },
2310
+ ]
2311
+ sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737, upload-time = "2024-11-24T20:12:22.481Z" }
2312
+ wheels = [
2313
+ { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540, upload-time = "2024-11-24T20:12:19.698Z" },
2314
+ ]
2315
+
2316
+ [[package]]
2317
+ name = "transformers"
2318
+ version = "4.57.3"
2319
+ source = { registry = "https://pypi.org/simple" }
2320
+ dependencies = [
2321
+ { name = "filelock" },
2322
+ { name = "huggingface-hub" },
2323
+ { name = "numpy" },
2324
+ { name = "packaging" },
2325
+ { name = "pyyaml" },
2326
+ { name = "regex" },
2327
+ { name = "requests" },
2328
+ { name = "safetensors" },
2329
+ { name = "tokenizers" },
2330
+ { name = "tqdm" },
2331
+ ]
2332
+ sdist = { url = "https://files.pythonhosted.org/packages/dd/70/d42a739e8dfde3d92bb2fff5819cbf331fe9657323221e79415cd5eb65ee/transformers-4.57.3.tar.gz", hash = "sha256:df4945029aaddd7c09eec5cad851f30662f8bd1746721b34cc031d70c65afebc", size = 10139680, upload-time = "2025-11-25T15:51:30.139Z" }
2333
+ wheels = [
2334
+ { url = "https://files.pythonhosted.org/packages/6a/6b/2f416568b3c4c91c96e5a365d164f8a4a4a88030aa8ab4644181fdadce97/transformers-4.57.3-py3-none-any.whl", hash = "sha256:c77d353a4851b1880191603d36acb313411d3577f6e2897814f333841f7003f4", size = 11993463, upload-time = "2025-11-25T15:51:26.493Z" },
2335
+ ]
2336
+
2337
  [[package]]
2338
  name = "typing-extensions"
2339
  version = "4.15.0"