Miroir commited on
Commit
8b5d6c8
·
1 Parent(s): bb68168

modularize api

Browse files
app.py CHANGED
@@ -1,11 +1,14 @@
1
- # semantix-api/app.py
2
- from fastapi import FastAPI, HTTPException
 
3
  from fastapi.middleware.cors import CORSMiddleware
4
- from pydantic import BaseModel
5
  from loguru import logger
6
  import sys
7
- import os
 
8
 
 
 
9
  from services.word_service import WordEmbeddingService
10
  from services.game_service import GameService
11
  from services.visualization_service import VisualizationService
@@ -14,126 +17,39 @@ from services.visualization_service import VisualizationService
14
  logger.remove()
15
  logger.add(sys.stdout, level="INFO")
16
 
17
- app = FastAPI()
18
-
19
- # Configure CORS
20
- app.add_middleware(
21
- CORSMiddleware,
22
- allow_origins=["*"], # Adjust this in production
23
- allow_credentials=True,
24
- allow_methods=["*"],
25
- allow_headers=["*"],
26
- )
27
-
28
- # At the top of app.py, add:
29
- import socket
30
- import platform
31
-
32
- @app.on_event("startup")
33
- async def startup_event():
34
- """Log information about the environment when the application starts"""
35
- logger.info("=" * 40)
36
- logger.info("Starting Semantix API")
37
- logger.info(f"Python version: {platform.python_version()}")
38
- logger.info(f"Host: {socket.gethostname()}")
39
- logger.info(f"Model URL: {os.getenv('MODEL_URL')}")
40
- logger.info("=" * 40)
41
-
42
-
43
- # Initialize services
44
- try:
45
  word_service = WordEmbeddingService()
46
  game_service = GameService(word_service)
47
  visualization_service = VisualizationService(word_service)
48
- logger.info("Services initialized successfully")
49
- except Exception as e:
50
- logger.error(f"Failed to initialize services: {str(e)}")
51
- raise e
52
-
53
- # Pydantic models for request validation
54
- class WordCheck(BaseModel):
55
- word: str
56
-
57
- class JokerUse(BaseModel):
58
- joker_type: str
59
-
60
-
61
- @app.get("/api/game-state")
62
- async def get_game_state():
63
- try:
64
- return game_service.get_state()
65
- except Exception as e:
66
- logger.error(f"Error getting game state: {str(e)}")
67
- raise HTTPException(status_code=500, detail="Internal server error")
68
-
69
- @app.post("/api/check-word")
70
- async def check_word(word_check: WordCheck):
71
- try:
72
- response = game_service.check_word(word_check.word)
73
- print(f"Response sent: {response}")
74
- return response
75
- except Exception as e:
76
- logger.error(f"Error checking word: {str(e)}")
77
- raise HTTPException(status_code=500, detail="Internal server error")
78
-
79
- @app.post("/api/use-joker")
80
- async def use_joker(joker: JokerUse):
81
- try:
82
- return game_service.use_joker(joker.joker_type)
83
- except Exception as e:
84
- logger.error(f"Error using joker: {str(e)}")
85
- raise HTTPException(status_code=500, detail="Internal server error")
86
-
87
- @app.post("/api/reset-game")
88
- async def reset_game():
89
- try:
90
- return game_service.reset_game()
91
- except Exception as e:
92
- logger.error(f"Error resetting game: {str(e)}")
93
- raise HTTPException(status_code=500, detail="Internal server error")
94
-
95
- @app.get("/api/visualization")
96
- async def get_visualization():
97
- try:
98
- state = game_service.get_state()
99
- guessed_words = [attempt['word'] for attempt in state["attempts"]]
100
- return visualization_service.prepare_3d_visualization(
101
- state["target_word"],
102
- guessed_words
103
- )
104
- except Exception as e:
105
- logger.error(f"Error getting visualization: {str(e)}")
106
- raise HTTPException(status_code=500, detail="Internal server error")
107
-
108
-
109
- # Add new health models
110
- class HealthResponse(BaseModel):
111
- status: str
112
- version: str = "1.0.0"
113
- model_loaded: bool
114
 
115
- @app.get("/api/health")
116
- async def health_check():
117
- """Health check endpoint"""
118
- try:
119
- # Check if model is loaded by accessing word service
120
- model_loaded = word_service._model is not None
121
- return {
122
- "status": "healthy" if model_loaded else "degraded",
123
- "version": "1.0.0",
124
- "model_loaded": model_loaded
125
- }
126
- except Exception as e:
127
- logger.exception("Health check failed")
128
- raise HTTPException(status_code=500, detail=str(e))
129
 
 
130
 
131
- @app.get("/")
132
- async def root():
133
- """Health check endpoint"""
134
- return {"status": "ok", "message": "Semantix API is running"}
135
 
136
  if __name__ == "__main__":
137
  import uvicorn
138
- logger.info("Starting FastAPI server in development mode...")
139
- uvicorn.run(app, host="0.0.0.0", port=8000, log_level="info")
 
1
+
2
+
3
+ from fastapi import FastAPI
4
  from fastapi.middleware.cors import CORSMiddleware
 
5
  from loguru import logger
6
  import sys
7
+ import socket
8
+ import platform
9
 
10
+ from config import settings
11
+ from routes import game, health
12
  from services.word_service import WordEmbeddingService
13
  from services.game_service import GameService
14
  from services.visualization_service import VisualizationService
 
17
  logger.remove()
18
  logger.add(sys.stdout, level="INFO")
19
 
20
+ def create_app():
21
+ app = FastAPI(title=settings.app_name, version=settings.version)
22
+
23
+ # Configure CORS
24
+ app.add_middleware(
25
+ CORSMiddleware,
26
+ allow_origins=["*"],
27
+ allow_credentials=True,
28
+ allow_methods=["*"],
29
+ allow_headers=["*"],
30
+ )
31
+
32
+ @app.on_event("startup")
33
+ async def startup_event():
34
+ logger.info("=" * 40)
35
+ logger.info(f"Starting {settings.app_name}")
36
+ logger.info(f"Python version: {platform.python_version()}")
37
+ logger.info(f"Host: {socket.gethostname()}")
38
+ logger.info("=" * 40)
39
+
40
+ # Initialize services
 
 
 
 
 
 
 
41
  word_service = WordEmbeddingService()
42
  game_service = GameService(word_service)
43
  visualization_service = VisualizationService(word_service)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44
 
45
+ # Include routers
46
+ app.include_router(game.init_router(game_service, visualization_service))
47
+ app.include_router(health.init_router(word_service))
 
 
 
 
 
 
 
 
 
 
 
48
 
49
+ return app
50
 
51
+ app = create_app()
 
 
 
52
 
53
  if __name__ == "__main__":
54
  import uvicorn
55
+ uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=settings.debug)
 
config.py ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic_settings import BaseSettings
2
+
3
+ class Settings(BaseSettings):
4
+ app_name: str = "Semantix API"
5
+ version: str = "1.0.0"
6
+ debug: bool = False
7
+ model_url: str = "https://huggingface.co/Miroir/cc.fr.300.reduced/resolve/main/cc.fr.300.reduced.vec"
8
+
9
+ settings = Settings()
models/game.py ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic import BaseModel
2
+
3
+ class WordCheck(BaseModel):
4
+ word: str
5
+
6
+ class JokerUse(BaseModel):
7
+ joker_type: str
8
+
9
+ class SetTargetWord(BaseModel):
10
+ word: str
models/health.py ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ from pydantic import BaseModel
2
+
3
+ class HealthResponse(BaseModel):
4
+ status: str
5
+ version: str = "1.0.0"
6
+ model_loaded: bool
requirements.txt CHANGED
@@ -9,4 +9,5 @@ requests==2.31.0
9
  scikit-learn==1.3.2
10
  umap-learn==0.5.5
11
  requests==2.31.0
12
- pydantic==2.5.2
 
 
9
  scikit-learn==1.3.2
10
  umap-learn==0.5.5
11
  requests==2.31.0
12
+ pydantic==2.5.2
13
+ pydantic-settings
routes/game.py ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, HTTPException
2
+ from loguru import logger
3
+ from models.game import WordCheck, JokerUse, SetTargetWord
4
+ from services.game_service import GameService
5
+ from services.visualization_service import VisualizationService
6
+
7
+ router = APIRouter(prefix="/api")
8
+
9
+ def init_router(game_service: GameService, visualization_service: VisualizationService):
10
+ @router.get("/game-state")
11
+ async def get_game_state():
12
+ try:
13
+ return game_service.get_state()
14
+ except Exception as e:
15
+ logger.error(f"Error getting game state: {str(e)}")
16
+ raise HTTPException(status_code=500, detail="Internal server error")
17
+
18
+ @router.post("/check-word")
19
+ async def check_word(word_check: WordCheck):
20
+ try:
21
+ response = game_service.check_word(word_check.word)
22
+ return response
23
+ except Exception as e:
24
+ logger.error(f"Error checking word: {str(e)}")
25
+ raise HTTPException(status_code=500, detail="Internal server error")
26
+
27
+ @router.post("/use-joker")
28
+ async def use_joker(joker: JokerUse):
29
+ try:
30
+ return game_service.use_joker(joker.joker_type)
31
+ except Exception as e:
32
+ logger.error(f"Error using joker: {str(e)}")
33
+ raise HTTPException(status_code=500, detail="Internal server error")
34
+
35
+ @router.post("/reset-game")
36
+ async def reset_game():
37
+ try:
38
+ return game_service.reset_game()
39
+ except Exception as e:
40
+ logger.error(f"Error resetting game: {str(e)}")
41
+ raise HTTPException(status_code=500, detail="Internal server error")
42
+
43
+ @router.get("/visualization")
44
+ async def get_visualization():
45
+ try:
46
+ state = game_service.get_state()
47
+ guessed_words = [attempt['word'] for attempt in state["attempts"]]
48
+ return visualization_service.prepare_3d_visualization(
49
+ state["target_word"],
50
+ guessed_words
51
+ )
52
+ except Exception as e:
53
+ logger.error(f"Error getting visualization: {str(e)}")
54
+ raise HTTPException(status_code=500, detail="Internal server error")
55
+
56
+ @router.post("/set-target")
57
+ async def set_target_word(target: SetTargetWord):
58
+ try:
59
+ return game_service.set_target_word(target.word)
60
+ except Exception as e:
61
+ logger.error(f"Error setting target word: {str(e)}")
62
+ raise HTTPException(status_code=500, detail="Internal server error")
63
+
64
+ return router
routes/health.py ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, HTTPException
2
+ from loguru import logger
3
+ from app.models.health import HealthResponse
4
+ from services.word_service import WordEmbeddingService
5
+
6
+ router = APIRouter(prefix="/api")
7
+
8
+ def init_router(word_service: WordEmbeddingService):
9
+ @router.get("/health")
10
+ async def health_check():
11
+ try:
12
+ model_loaded = word_service._model is not None
13
+ return {
14
+ "status": "healthy" if model_loaded else "degraded",
15
+ "version": "1.0.0",
16
+ "model_loaded": model_loaded
17
+ }
18
+ except Exception as e:
19
+ logger.exception("Health check failed")
20
+ raise HTTPException(status_code=500, detail=str(e))
21
+
22
+ return router
services/game_service.py CHANGED
@@ -194,6 +194,19 @@ class GameService:
194
  logger.exception("Error getting history")
195
  return []
196
 
 
 
 
 
 
 
 
 
 
 
 
 
 
197
  def check_word(self, word: str) -> Dict:
198
  """
199
  Check a word against the target word and save the attempt.
 
194
  logger.exception("Error getting history")
195
  return []
196
 
197
+
198
+ # In game_service.py
199
+ def set_target_word(self, word: str):
200
+ """Set a specific target word for testing"""
201
+ if not self.word_service.get_vector(word):
202
+ raise ValueError(f"Word '{word}' not found in vocabulary")
203
+ self.target_word = word
204
+ self.attempts = []
205
+ self.jokers_used = 0
206
+ return self.get_state()
207
+
208
+
209
+
210
  def check_word(self, word: str) -> Dict:
211
  """
212
  Check a word against the target word and save the attempt.
services/model_downloader.py DELETED
@@ -1,29 +0,0 @@
1
- import os
2
- import requests
3
- from loguru import logger
4
- from pathlib import Path
5
-
6
- def download_model(url: str, model_path: str):
7
- """Download the model file if it doesn't exist."""
8
- if os.path.exists(model_path):
9
- logger.info(f"Model file already exists at {model_path}")
10
- return
11
-
12
- logger.info(f"Downloading model from {url}")
13
- os.makedirs(os.path.dirname(model_path), exist_ok=True)
14
-
15
- try:
16
- response = requests.get(url, stream=True)
17
- response.raise_for_status()
18
-
19
- total_size = int(response.headers.get('content-length', 0))
20
- block_size = 1024 # 1 KB
21
-
22
- with open(model_path, 'wb') as f:
23
- for data in response.iter_content(block_size):
24
- f.write(data)
25
-
26
- logger.info(f"Model downloaded successfully to {model_path}")
27
- except Exception as e:
28
- logger.error(f"Error downloading model: {str(e)}")
29
- raise
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
services/study_service.py ADDED
@@ -0,0 +1,105 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from loguru import logger
2
+ from typing import List, Dict, Optional
3
+ import numpy as np
4
+ from .word_service import WordEmbeddingService
5
+
6
+ class StudyService:
7
+ def __init__(self, word_service: WordEmbeddingService):
8
+ self.word_service = word_service
9
+
10
+ def analyze_word_neighborhood(self, word: str, n_neighbors: int = 20) -> Dict:
11
+ """
12
+ Get detailed analysis of a word's semantic neighborhood
13
+ """
14
+ try:
15
+ similar_words = self.word_service.get_most_similar_words(word, n=n_neighbors)
16
+ vector = self.word_service.get_vector(word)
17
+
18
+ return {
19
+ "word": word,
20
+ "in_vocabulary": vector is not None,
21
+ "similar_words": similar_words,
22
+ "vector_norm": float(np.linalg.norm(vector)) if vector is not None else None
23
+ }
24
+ except Exception as e:
25
+ logger.exception(f"Error analyzing word neighborhood: {e}")
26
+ return {
27
+ "word": word,
28
+ "in_vocabulary": False,
29
+ "similar_words": [],
30
+ "vector_norm": None
31
+ }
32
+
33
+ def compare_words(self, words: List[str]) -> Dict:
34
+ """
35
+ Compare multiple words to understand their relationships
36
+ """
37
+ results = []
38
+ similarity_matrix = []
39
+
40
+ for i, word1 in enumerate(words):
41
+ row = []
42
+ for j, word2 in enumerate(words):
43
+ sim = self.word_service.calculate_similarity(word1, word2)
44
+ row.append(sim)
45
+ similarity_matrix.append(row)
46
+
47
+ # Get vector if available
48
+ vector = self.word_service.get_vector(word1)
49
+ results.append({
50
+ "word": word1,
51
+ "in_vocabulary": vector is not None,
52
+ "vector_norm": float(np.linalg.norm(vector)) if vector is not None else None
53
+ })
54
+
55
+ return {
56
+ "words": results,
57
+ "similarity_matrix": similarity_matrix
58
+ }
59
+
60
+ def analyze_similarity_distribution(self, target_word: str, test_words: List[str]) -> Dict:
61
+ """
62
+ Analyze similarity distribution across test words
63
+ """
64
+ similarities = []
65
+ for word in test_words:
66
+ sim = self.word_service.calculate_similarity(target_word, word)
67
+ similarities.append({
68
+ "word": word,
69
+ "similarity": sim
70
+ })
71
+
72
+ # Sort by similarity
73
+ similarities.sort(key=lambda x: x["similarity"], reverse=True)
74
+
75
+ return {
76
+ "target_word": target_word,
77
+ "similarities": similarities,
78
+ "statistics": {
79
+ "max_similarity": max(s["similarity"] for s in similarities),
80
+ "min_similarity": min(s["similarity"] for s in similarities),
81
+ "mean_similarity": np.mean([s["similarity"] for s in similarities]),
82
+ "median_similarity": np.median([s["similarity"] for s in similarities])
83
+ }
84
+ }
85
+
86
+ def get_similarity_ranges(self, word: str) -> Dict:
87
+ """
88
+ Get words in different similarity ranges to understand semantic distances
89
+ """
90
+ ranges = [
91
+ (0.9, 1.0, "very_high"),
92
+ (0.7, 0.9, "high"),
93
+ (0.5, 0.7, "medium"),
94
+ (0.3, 0.5, "low"),
95
+ (0.1, 0.3, "very_low")
96
+ ]
97
+
98
+ results = {}
99
+ for min_sim, max_sim, range_name in ranges:
100
+ words = self.word_service.get_words_in_range(
101
+ word, min_sim, max_sim, n=5
102
+ )
103
+ results[range_name] = words
104
+
105
+ return results
services/word_service.py CHANGED
@@ -105,10 +105,10 @@ class WordEmbeddingService:
105
  self._ensure_model_loaded()
106
  try:
107
  w = target_word.lower()
108
- if w not in WordEmbeddingService._model: # Changed from self.model
109
  logger.warning(f"Target word not found in vocab: {target_word}")
110
  return []
111
- similar = WordEmbeddingService._model.most_similar(w, topn=n) # Changed from self.model
112
  return [{'word': word, 'similarity': float(sim)} for word, sim in similar]
113
  except Exception:
114
  logger.exception(f"Error finding similar words for: {target_word}")
 
105
  self._ensure_model_loaded()
106
  try:
107
  w = target_word.lower()
108
+ if w not in WordEmbeddingService._model:
109
  logger.warning(f"Target word not found in vocab: {target_word}")
110
  return []
111
+ similar = WordEmbeddingService._model.most_similar(w, topn=n)
112
  return [{'word': word, 'similarity': float(sim)} for word, sim in similar]
113
  except Exception:
114
  logger.exception(f"Error finding similar words for: {target_word}")