rui3000 commited on
Commit
6db1ab1
Β·
verified Β·
1 Parent(s): 2bbf3aa

Update RockPaperScissor/services/LLM_service.py

Browse files
RockPaperScissor/services/LLM_service.py CHANGED
@@ -4,59 +4,58 @@ from typing import List, Dict, Optional
4
  import re
5
  import os
6
 
 
 
 
 
 
 
 
 
 
7
  class LLMService:
8
  def __init__(self):
9
  # Use Hugging Face Hub model name, not a local path
10
- self.model_name = "Qwen/Qwen3-0.6B"
11
- print("[LLMService] Initializing LLM Service...")
12
- self.tokenizer = None
 
13
  self.model = None
14
  self.device = "cuda" if torch.cuda.is_available() else "cpu"
15
  print(f"[LLMService] Device set to: {self.device}")
16
-
17
- # Initialize tokenizer only, delay model loading
18
- self._init_tokenizer()
19
-
20
- def _init_tokenizer(self):
21
- """Initialize tokenizer separately"""
22
- try:
23
- print("[LLMService] Loading Qwen tokenizer from Hugging Face Hub...")
24
- self.tokenizer = AutoTokenizer.from_pretrained(self.model_name)
25
- if self.tokenizer.pad_token is None:
26
- self.tokenizer.pad_token = self.tokenizer.eos_token
27
- print("[LLMService] Qwen tokenizer loaded.")
28
- except Exception as e:
29
- print(f"[LLMService] Error loading tokenizer: {e}")
30
- raise
31
 
32
  def load_model(self):
33
- """Load model on-demand"""
34
  if self.model is None:
35
  print(f"[LLMService] Loading Qwen model from Hugging Face Hub on device: {self.device}...")
36
  try:
37
- # Load with appropriate settings
38
- load_kwargs = {
39
- "torch_dtype": torch.float16 if self.device == "cuda" else torch.float32,
40
- "trust_remote_code": True,
41
- "low_cpu_mem_usage": True
42
- }
43
-
44
- if self.device == "cuda":
45
- load_kwargs["device_map"] = "auto"
46
-
47
  self.model = AutoModelForCausalLM.from_pretrained(
48
  self.model_name,
49
- **load_kwargs
 
 
50
  )
51
-
52
  if self.device == "cpu":
53
  self.model = self.model.to(self.device)
54
-
55
  print(f"[LLMService] Qwen model loaded successfully on {self.device}.")
56
  except Exception as e:
57
  print(f"[LLMService] Error loading model: {e}")
58
  raise
59
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
60
  def _format_frequency_stats(self, stats: Dict) -> str:
61
  """Format frequency statistics into a readable string."""
62
  total_moves = sum(stats.get('move_distribution', {}).values())
@@ -68,6 +67,14 @@ class LLMService:
68
  for move, count in stats.get('move_distribution', {}).items():
69
  frequencies[move] = (count / total_moves) * 100
70
 
 
 
 
 
 
 
 
 
71
  formatted = "AI Move Analysis:\n"
72
  formatted += f"Rock: {frequencies.get('rock', 0):.0f}%\n"
73
  formatted += f"Paper: {frequencies.get('paper', 0):.0f}%\n"
@@ -75,108 +82,89 @@ class LLMService:
75
  return formatted
76
 
77
  def _create_frequency_prompt(self, stats: Dict) -> str:
78
- """Create a concise prompt for frequency-based analysis."""
79
  base_prompt = (
80
- "You are a helpful assistant that analyzes Rock-Paper-Scissors AI opponent statistics. "
81
- "Based on the AI move frequency stats, recommend the best counter move. "
82
- "Start your answer with '>>>' followed by explanation and 'Recommendation: [Move]'.\n\n"
83
  "Example:\n"
84
- ">>> The AI plays Scissors most (84%), so play Rock to counter it.\nRecommendation: Rock\n\n"
85
- "AI Move Stats:\n"
86
  )
87
  base_prompt += self._format_frequency_stats(stats)
88
  return base_prompt
89
 
90
  def _clean_llm_output(self, output: str) -> str:
91
- """Clean and extract the relevant response."""
92
- # Remove code blocks and markdown
93
  output = re.sub(r'`{3,}', '', output)
94
- output = re.sub(r'\*\*.*?\*\*', '', output)
95
-
96
  lines = [line.strip() for line in output.splitlines() if line.strip()]
97
-
98
- # Look for our beacon pattern
99
- result_lines = []
100
  for i, line in enumerate(lines):
101
  if line.startswith('>>>'):
102
- result_lines.append(line)
103
- # Get the next line if it's a recommendation
104
- if i + 1 < len(lines) and 'recommendation:' in lines[i + 1].lower():
105
- result_lines.append(lines[i + 1])
106
- break
107
-
108
- if result_lines:
109
- return '\n'.join(result_lines)
110
-
111
- # Fallback: look for any recommendation
112
  for line in lines:
113
- if 'recommendation:' in line.lower():
 
 
 
 
114
  return line
115
-
116
- # Last resort fallback
117
- return ">>> Try to counter the AI's most common move!\nRecommendation: Rock"
118
 
119
  async def generate_response(self, prompt: str, stats: Optional[Dict] = None) -> str:
120
  print("[LLMService] generate_response called.")
121
-
 
 
 
 
122
  try:
123
- if stats:
124
- print(f"[LLMService] Using stats for prompt: {stats}")
125
- prompt = self._create_frequency_prompt(stats)
126
- else:
127
- print(f"[LLMService] Using raw prompt: {prompt}")
128
-
129
  # Ensure model is loaded
130
  self.load_model()
131
  print("[LLMService] Model generation started...")
132
 
133
- # Tokenize input
134
- inputs = self.tokenizer([prompt], return_tensors="pt", padding=True, truncation=True)
135
-
136
- # Move to device if using GPU
137
- if self.device == "cuda":
138
  inputs = {k: v.to(self.device) for k, v in inputs.items()}
139
 
140
- # Generate response
141
- with torch.no_grad():
142
- outputs = self.model.generate(
143
- **inputs,
144
- max_new_tokens=200,
145
- temperature=0.7,
146
- top_p=0.9,
147
- do_sample=True,
148
- pad_token_id=self.tokenizer.eos_token_id,
149
- eos_token_id=self.tokenizer.eos_token_id
150
- )
 
 
 
 
151
 
152
- # Decode response
153
  response = self.tokenizer.decode(outputs[0], skip_special_tokens=True)
154
-
155
- # Extract only the new generated part (remove input prompt)
156
- if prompt in response:
157
- response = response.replace(prompt, "").strip()
158
-
159
  print("[LLMService] Model generation finished.")
 
160
  return self._clean_llm_output(response)
161
-
162
  except Exception as e:
163
  print(f"[LLMService] Error during model generation: {e}")
164
- # Return a reasonable fallback
165
- if stats and 'move_distribution' in stats:
166
- moves = stats['move_distribution']
167
- most_common_move = max(moves.items(), key=lambda x: x[1])[0]
168
- counter_moves = {"rock": "Paper", "paper": "Scissors", "scissors": "Rock"}
169
- counter = counter_moves.get(most_common_move, "Rock")
170
- return f">>> The AI often plays {most_common_move.title()}, so try {counter} to counter it!\nRecommendation: {counter}"
171
- else:
172
- return ">>> Try to observe the AI's patterns and counter accordingly!\nRecommendation: Rock"
173
 
174
  async def close(self):
175
  """Clean up resources"""
176
  if self.model is not None:
177
  del self.model
178
- if hasattr(self, 'tokenizer') and self.tokenizer is not None:
179
- del self.tokenizer
180
- if torch.cuda.is_available():
181
- torch.cuda.empty_cache()
182
  print("[LLMService] Resources cleaned up.")
 
4
  import re
5
  import os
6
 
7
+ # Better handling of spaces GPU decorator
8
+ try:
9
+ from spaces import GPU
10
+ print("[LLMService] spaces.GPU imported successfully")
11
+ except ImportError:
12
+ print("[LLMService] spaces.GPU not available, using fallback")
13
+ def GPU(f):
14
+ return f # fallback if not running on Spaces
15
+
16
  class LLMService:
17
  def __init__(self):
18
  # Use Hugging Face Hub model name, not a local path
19
+ self.model_name = "Qwen/Qwen3-0.6B" # Use the official repo name from the Hugging Face Hub
20
+ print("[LLMService] Loading Qwen tokenizer from Hugging Face Hub...")
21
+ self.tokenizer = AutoTokenizer.from_pretrained(self.model_name)
22
+ print("[LLMService] Qwen tokenizer loaded.")
23
  self.model = None
24
  self.device = "cuda" if torch.cuda.is_available() else "cpu"
25
  print(f"[LLMService] Device set to: {self.device}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
 
27
  def load_model(self):
28
+ """Load model without GPU decorator to avoid startup issues"""
29
  if self.model is None:
30
  print(f"[LLMService] Loading Qwen model from Hugging Face Hub on device: {self.device}...")
31
  try:
 
 
 
 
 
 
 
 
 
 
32
  self.model = AutoModelForCausalLM.from_pretrained(
33
  self.model_name,
34
+ torch_dtype=torch.float16 if self.device == "cuda" else torch.float32,
35
+ device_map={"": self.device} if self.device == "cuda" else None,
36
+ trust_remote_code=True # Add this for some models
37
  )
 
38
  if self.device == "cpu":
39
  self.model = self.model.to(self.device)
 
40
  print(f"[LLMService] Qwen model loaded successfully on {self.device}.")
41
  except Exception as e:
42
  print(f"[LLMService] Error loading model: {e}")
43
  raise
44
 
45
+ @GPU # Apply decorator to generation method instead
46
+ def _generate_with_gpu(self, inputs):
47
+ """GPU-accelerated generation method"""
48
+ with torch.no_grad():
49
+ outputs = self.model.generate(
50
+ **inputs,
51
+ max_new_tokens=300,
52
+ temperature=0.4,
53
+ top_p=0.8,
54
+ do_sample=True,
55
+ pad_token_id=self.tokenizer.eos_token_id # Add this to avoid warnings
56
+ )
57
+ return outputs
58
+
59
  def _format_frequency_stats(self, stats: Dict) -> str:
60
  """Format frequency statistics into a readable string."""
61
  total_moves = sum(stats.get('move_distribution', {}).values())
 
67
  for move, count in stats.get('move_distribution', {}).items():
68
  frequencies[move] = (count / total_moves) * 100
69
 
70
+ # Find the most common move
71
+ most_common = max(frequencies.items(), key=lambda x: x[1])
72
+ counter_move = {
73
+ "rock": "paper",
74
+ "paper": "scissors",
75
+ "scissors": "rock"
76
+ }[most_common[0]]
77
+
78
  formatted = "AI Move Analysis:\n"
79
  formatted += f"Rock: {frequencies.get('rock', 0):.0f}%\n"
80
  formatted += f"Paper: {frequencies.get('paper', 0):.0f}%\n"
 
82
  return formatted
83
 
84
  def _create_frequency_prompt(self, stats: Dict) -> str:
85
+ """Create a concise prompt for frequency-based analysis with a beacon example."""
86
  base_prompt = (
87
+ "You are a helpful assistant that analyzes Rock-Paper-Scissors (RPS) AI opponent statistics to help players win. "
88
+ "Based on the provided AI move frequency stats, briefly recommend the best move for the player to counter the AI's most frequent move. "
89
+ "Your answer must start with '>>>', followed by a one-sentence explanation and then a new line with 'Recommendation: [Move]'.\n\n"
90
  "Example:\n"
91
+ ">>> The AI most often plays Scissors (84% of the time), so you should play Rock to counter it, since Rock beats Scissors.\nRecommendation: Rock\n\n"
92
+ "AI Move Frequency Stats:\n"
93
  )
94
  base_prompt += self._format_frequency_stats(stats)
95
  return base_prompt
96
 
97
  def _clean_llm_output(self, output: str) -> str:
98
+ # Remove code block markers and markdown
 
99
  output = re.sub(r'`{3,}', '', output)
100
+ output = re.sub(r'\*\*Final Answer\*\*', '', output, flags=re.IGNORECASE)
101
+ # Find the beacon and extract the answer
102
  lines = [line.strip() for line in output.splitlines() if line.strip()]
 
 
 
103
  for i, line in enumerate(lines):
104
  if line.startswith('>>>'):
105
+ result = [line]
106
+ # Optionally include the next line if it's a recommendation
107
+ if i + 1 < len(lines) and lines[i + 1].lower().startswith('recommendation:'):
108
+ result.append(lines[i + 1])
109
+ return '\n'.join(result)
110
+ # Fallback: previous cleaning logic
111
+ seen = set()
112
+ cleaned_lines = []
 
 
113
  for line in lines:
114
+ if line not in seen:
115
+ cleaned_lines.append(line)
116
+ seen.add(line)
117
+ for line in cleaned_lines:
118
+ if line.lower().startswith('recommendation:'):
119
  return line
120
+ return '\n'.join(cleaned_lines)
 
 
121
 
122
  async def generate_response(self, prompt: str, stats: Optional[Dict] = None) -> str:
123
  print("[LLMService] generate_response called.")
124
+ if stats:
125
+ print(f"[LLMService] Using stats for prompt: {stats}")
126
+ prompt = self._create_frequency_prompt(stats)
127
+ else:
128
+ print(f"[LLMService] Using raw prompt: {prompt}")
129
  try:
 
 
 
 
 
 
130
  # Ensure model is loaded
131
  self.load_model()
132
  print("[LLMService] Model generation started...")
133
 
134
+ # Move inputs to the same device as model
135
+ inputs = self.tokenizer([prompt], return_tensors="pt")
136
+ if self.device == "cuda" and self.model is not None:
 
 
137
  inputs = {k: v.to(self.device) for k, v in inputs.items()}
138
 
139
+ # Use GPU-decorated method if available, otherwise regular generation
140
+ try:
141
+ outputs = self._generate_with_gpu(inputs)
142
+ except Exception as gpu_error:
143
+ print(f"[LLMService] GPU generation failed, falling back to CPU: {gpu_error}")
144
+ # Fallback to CPU generation
145
+ with torch.no_grad():
146
+ outputs = self.model.generate(
147
+ **inputs,
148
+ max_new_tokens=300,
149
+ temperature=0.4,
150
+ top_p=0.8,
151
+ do_sample=True,
152
+ pad_token_id=self.tokenizer.eos_token_id
153
+ )
154
 
 
155
  response = self.tokenizer.decode(outputs[0], skip_special_tokens=True)
 
 
 
 
 
156
  print("[LLMService] Model generation finished.")
157
+ # Clean up the output before returning
158
  return self._clean_llm_output(response)
 
159
  except Exception as e:
160
  print(f"[LLMService] Error during model generation: {e}")
161
+ # Return a fallback response instead of raising
162
+ return ">>> I'm having trouble analyzing the game right now, but generally try to counter the AI's most common move!\nRecommendation: Rock"
 
 
 
 
 
 
 
163
 
164
  async def close(self):
165
  """Clean up resources"""
166
  if self.model is not None:
167
  del self.model
168
+ if torch.cuda.is_available():
169
+ torch.cuda.empty_cache()
 
 
170
  print("[LLMService] Resources cleaned up.")