Reaperxxxx commited on
Commit
338842a
·
verified ·
1 Parent(s): 8d5bf84

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +500 -0
app.py ADDED
@@ -0,0 +1,500 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Flask, request, jsonify
2
+ from mistral_inference.transformer import Transformer
3
+ from mistral_inference.generate import generate
4
+ from mistral_common.tokens.tokenizers.mistral import MistralTokenizer
5
+ from mistral_common.protocol.instruct.messages import UserMessage, AssistantMessage, ToolMessage
6
+ from mistral_common.protocol.instruct.request import ChatCompletionRequest
7
+ from mistral_common.protocol.instruct.tool_calls import Function, Tool
8
+ import requests
9
+ import json
10
+ import os
11
+ import threading
12
+ import queue
13
+ import time
14
+ from datetime import datetime
15
+ from pathlib import Path
16
+ import logging
17
+
18
+ # Configure logging
19
+ logging.basicConfig(
20
+ level=logging.INFO,
21
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
22
+ )
23
+ logger = logging.getLogger(__name__)
24
+
25
+ app = Flask(__name__)
26
+
27
+ # Global variables
28
+ model = None
29
+ tokenizer = None
30
+ MODEL_PATH = "/app/models"
31
+ MEMORY_FILE = "/app/memory.json"
32
+ MAX_MEMORY_ENTRIES_TOTAL = 500 # Total shared memory entries for the group
33
+ MAX_MEMORY_CONTEXT = 20 # Number of recent messages to include in context
34
+
35
+ # Request queue for handling concurrent requests
36
+ request_queue = queue.Queue(maxsize=100)
37
+ processing_lock = threading.Lock()
38
+
39
+ class MemoryManager:
40
+ """Manages shared group memories with size limits"""
41
+
42
+ def __init__(self, memory_file):
43
+ self.memory_file = memory_file
44
+ self.memory_lock = threading.Lock()
45
+ self.load_memory()
46
+
47
+ def load_memory(self):
48
+ """Load memory from file"""
49
+ try:
50
+ if os.path.exists(self.memory_file):
51
+ with open(self.memory_file, 'r') as f:
52
+ data = json.load(f)
53
+ # Support both old format and new shared format
54
+ if isinstance(data, dict) and "entries" in data:
55
+ self.memory = data
56
+ else:
57
+ # Convert old format to new
58
+ self.memory = {"entries": [], "users": {}}
59
+ else:
60
+ self.memory = {"entries": [], "users": {}}
61
+ except Exception as e:
62
+ logger.error(f"Error loading memory: {e}")
63
+ self.memory = {"entries": [], "users": {}}
64
+
65
+ def save_memory(self):
66
+ """Save memory to file"""
67
+ try:
68
+ with open(self.memory_file, 'w') as f:
69
+ json.dump(self.memory, f, indent=2)
70
+ except Exception as e:
71
+ logger.error(f"Error saving memory: {e}")
72
+
73
+ def add_memory(self, chat_id, username, name, message):
74
+ """Add a memory entry to shared group memory"""
75
+ with self.memory_lock:
76
+ chat_key = str(chat_id)
77
+
78
+ # Track user info
79
+ if chat_key not in self.memory["users"]:
80
+ self.memory["users"][chat_key] = {
81
+ "username": username,
82
+ "name": name,
83
+ "first_seen": datetime.now().isoformat(),
84
+ "message_count": 0
85
+ }
86
+
87
+ # Update user info
88
+ self.memory["users"][chat_key]["username"] = username
89
+ self.memory["users"][chat_key]["name"] = name
90
+ self.memory["users"][chat_key]["message_count"] += 1
91
+ self.memory["users"][chat_key]["last_seen"] = datetime.now().isoformat()
92
+
93
+ # Add new entry to shared memory
94
+ entry = {
95
+ "chat_id": chat_key,
96
+ "message": message,
97
+ "timestamp": datetime.now().isoformat(),
98
+ "username": username,
99
+ "name": name
100
+ }
101
+
102
+ self.memory["entries"].append(entry)
103
+
104
+ # Trim if too large - keep most recent entries
105
+ if len(self.memory["entries"]) > MAX_MEMORY_ENTRIES_TOTAL:
106
+ # Keep first 50 (important early context) and last 450 (recent context)
107
+ important = self.memory["entries"][:50]
108
+ recent = self.memory["entries"][-(MAX_MEMORY_ENTRIES_TOTAL-50):]
109
+ self.memory["entries"] = important + recent
110
+
111
+ self.save_memory()
112
+
113
+ return {
114
+ "status": "success",
115
+ "total_entries": len(self.memory["entries"]),
116
+ "user_message_count": self.memory["users"][chat_key]["message_count"]
117
+ }
118
+
119
+ def get_memory(self, chat_id):
120
+ """Get memory for a specific chat (for compatibility)"""
121
+ with self.memory_lock:
122
+ chat_key = str(chat_id)
123
+ if chat_key in self.memory["users"]:
124
+ # Return user-specific view
125
+ user_entries = [e for e in self.memory["entries"] if e.get("chat_id") == chat_key]
126
+ return {
127
+ "username": self.memory["users"][chat_key]["username"],
128
+ "name": self.memory["users"][chat_key]["name"],
129
+ "entries": user_entries
130
+ }
131
+ return None
132
+
133
+ def get_all_memory(self):
134
+ """Get all shared group memory"""
135
+ with self.memory_lock:
136
+ return self.memory
137
+
138
+ def get_memory_summary(self, chat_id=None):
139
+ """Get a condensed summary of shared group memories for context"""
140
+ with self.memory_lock:
141
+ if not self.memory.get("entries"):
142
+ return ""
143
+
144
+ # Get recent entries for context
145
+ recent_entries = self.memory["entries"][-MAX_MEMORY_CONTEXT:]
146
+
147
+ summary = f"\n--- Shared Group Memory Context ---\n"
148
+ summary += f"Group has {len(self.memory['users'])} members\n"
149
+ summary += f"Recent conversation history:\n"
150
+
151
+ for entry in recent_entries:
152
+ timestamp = entry.get('timestamp', '')[:16] # Date + time
153
+ name = entry.get('name', 'Unknown')
154
+ username = entry.get('username', 'unknown')
155
+ msg = entry.get('message', '')[:150] # Truncate long messages
156
+ summary += f"- [{timestamp}] {name} (@{username}): {msg}\n"
157
+
158
+ summary += "--- End Memory Context ---\n"
159
+ return summary
160
+
161
+ def search_memory(self, keyword, chat_id=None):
162
+ """Search for specific information in memory"""
163
+ with self.memory_lock:
164
+ results = []
165
+ for entry in self.memory["entries"]:
166
+ # If chat_id provided, filter to that user
167
+ if chat_id and entry.get("chat_id") != str(chat_id):
168
+ continue
169
+
170
+ if keyword.lower() in entry["message"].lower():
171
+ results.append(entry)
172
+
173
+ return results
174
+
175
+ def get_user_info(self, chat_id):
176
+ """Get information about a specific user"""
177
+ with self.memory_lock:
178
+ chat_key = str(chat_id)
179
+ return self.memory["users"].get(chat_key)
180
+
181
+ # Initialize memory manager
182
+ memory_manager = MemoryManager(MEMORY_FILE)
183
+
184
+ def initialize_model():
185
+ """Initialize the Mistral model and tokenizer"""
186
+ global model, tokenizer
187
+
188
+ if model is None or tokenizer is None:
189
+ logger.info("Loading Mistral model...")
190
+ try:
191
+ tokenizer = MistralTokenizer.from_file(f"{MODEL_PATH}/tokenizer.model.v3")
192
+ model = Transformer.from_folder(MODEL_PATH)
193
+ logger.info("Model loaded successfully")
194
+ except Exception as e:
195
+ logger.error(f"Error loading model: {e}")
196
+ raise
197
+
198
+ def check_health_api():
199
+ """Call the health API"""
200
+ try:
201
+ response = requests.get("https://bridge.zone.id/api/health", timeout=5)
202
+ return response.json()
203
+ except Exception as e:
204
+ logger.error(f"Health API error: {e}")
205
+ return {"error": str(e), "status": "error"}
206
+
207
+ def extract_tool_calls(text):
208
+ """Extract tool calls from model output more robustly"""
209
+ try:
210
+ # Look for function call patterns
211
+ if "[TOOL_CALLS]" in text or "check_health" in text.lower():
212
+ return True
213
+ return False
214
+ except Exception as e:
215
+ logger.error(f"Error extracting tool calls: {e}")
216
+ return False
217
+
218
+ def process_with_mistral(query, chat_id=None, use_tools=False, max_retries=2):
219
+ """Process query with Mistral model with error handling"""
220
+
221
+ for attempt in range(max_retries):
222
+ try:
223
+ initialize_model()
224
+
225
+ # Prepare tools if needed
226
+ tools = None
227
+ if use_tools:
228
+ tools = [
229
+ Tool(
230
+ function=Function(
231
+ name="check_health",
232
+ description="Check the health status of the bridge.zone.id API service",
233
+ parameters={
234
+ "type": "object",
235
+ "properties": {},
236
+ "required": [],
237
+ },
238
+ )
239
+ )
240
+ ]
241
+
242
+ # Build context with shared group memory
243
+ context = ""
244
+ memory_context = memory_manager.get_memory_summary(chat_id)
245
+ if memory_context:
246
+ context = memory_context + "\n"
247
+
248
+ # Create user message with context
249
+ user_content = context + query if context else query
250
+ messages = [UserMessage(content=user_content)]
251
+
252
+ completion_request = ChatCompletionRequest(
253
+ messages=messages,
254
+ tools=tools if use_tools else None
255
+ )
256
+
257
+ tokens = tokenizer.encode_chat_completion(completion_request).tokens
258
+
259
+ # Limit context to prevent overflow
260
+ max_context_tokens = 4096 # Adjust based on your model
261
+ if len(tokens) > max_context_tokens:
262
+ tokens = tokens[-max_context_tokens:]
263
+
264
+ with processing_lock: # Ensure single-threaded model access
265
+ out_tokens, _ = generate(
266
+ [tokens],
267
+ model,
268
+ max_tokens=512,
269
+ temperature=0.7,
270
+ eos_id=tokenizer.instruct_tokenizer.tokenizer.eos_id
271
+ )
272
+
273
+ result = tokenizer.instruct_tokenizer.tokenizer.decode(out_tokens[0])
274
+
275
+ # Handle tool calls
276
+ if use_tools and extract_tool_calls(result):
277
+ logger.info("Tool call detected, executing health check")
278
+ health_data = check_health_api()
279
+
280
+ # Create follow-up request with tool result
281
+ messages.append(AssistantMessage(content=result))
282
+ messages.append(ToolMessage(
283
+ content=json.dumps(health_data),
284
+ tool_call_id="check_health_1"
285
+ ))
286
+
287
+ completion_request = ChatCompletionRequest(messages=messages)
288
+ tokens = tokenizer.encode_chat_completion(completion_request).tokens
289
+
290
+ if len(tokens) > max_context_tokens:
291
+ tokens = tokens[-max_context_tokens:]
292
+
293
+ with processing_lock:
294
+ out_tokens, _ = generate(
295
+ [tokens],
296
+ model,
297
+ max_tokens=512,
298
+ temperature=0.7,
299
+ eos_id=tokenizer.instruct_tokenizer.tokenizer.eos_id
300
+ )
301
+
302
+ result = tokenizer.instruct_tokenizer.tokenizer.decode(out_tokens[0])
303
+
304
+ return {"success": True, "response": result}
305
+
306
+ except Exception as e:
307
+ logger.error(f"Attempt {attempt + 1} failed: {e}")
308
+ if attempt == max_retries - 1:
309
+ return {"success": False, "error": str(e)}
310
+ time.sleep(1) # Brief pause before retry
311
+
312
+ return {"success": False, "error": "Max retries exceeded"}
313
+
314
+ @app.route('/api/chat', methods=['POST'])
315
+ def chat():
316
+ """Main chat endpoint with queue management"""
317
+ try:
318
+ data = request.get_json()
319
+
320
+ if not data or 'query' not in data:
321
+ return jsonify({
322
+ "error": "Missing 'query' parameter"
323
+ }), 400
324
+
325
+ query = data['query'].strip()
326
+ chat_id = data.get('chat_id')
327
+
328
+ if not query:
329
+ return jsonify({
330
+ "error": "Empty query"
331
+ }), 400
332
+
333
+ # Check queue size
334
+ if request_queue.qsize() >= 95:
335
+ return jsonify({
336
+ "error": "Server is busy, please try again later"
337
+ }), 503
338
+
339
+ # Determine if we should use function calling
340
+ health_keywords = ['health', 'status', 'api status', 'bridge', 'check service', 'api check']
341
+ use_tools = any(keyword in query.lower() for keyword in health_keywords)
342
+
343
+ # Process with Mistral
344
+ start_time = time.time()
345
+ result = process_with_mistral(query, chat_id=chat_id, use_tools=use_tools)
346
+ processing_time = time.time() - start_time
347
+
348
+ if not result["success"]:
349
+ return jsonify({
350
+ "error": result.get("error", "Processing failed")
351
+ }), 500
352
+
353
+ logger.info(f"Processed query in {processing_time:.2f}s")
354
+
355
+ return jsonify({
356
+ "query": query,
357
+ "response": result["response"],
358
+ "used_tools": use_tools,
359
+ "processing_time": processing_time,
360
+ "has_memory": len(memory_manager.memory.get("entries", [])) > 0,
361
+ "total_group_memories": len(memory_manager.memory.get("entries", []))
362
+ })
363
+
364
+ except Exception as e:
365
+ logger.error(f"Chat endpoint error: {e}")
366
+ return jsonify({
367
+ "error": str(e)
368
+ }), 500
369
+
370
+ @app.route('/api/update-memory', methods=['POST'])
371
+ def update_memory():
372
+ """Update memory for a user"""
373
+ try:
374
+ data = request.get_json()
375
+
376
+ required_fields = ['chat_id', 'username', 'name', 'message']
377
+ if not all(field in data for field in required_fields):
378
+ return jsonify({
379
+ "error": f"Missing required fields: {required_fields}"
380
+ }), 400
381
+
382
+ result = memory_manager.add_memory(
383
+ chat_id=data['chat_id'],
384
+ username=data['username'],
385
+ name=data['name'],
386
+ message=data['message']
387
+ )
388
+
389
+ return jsonify(result)
390
+
391
+ except Exception as e:
392
+ logger.error(f"Update memory error: {e}")
393
+ return jsonify({
394
+ "error": str(e)
395
+ }), 500
396
+
397
+ @app.route('/api/get-memory/<chat_id>', methods=['GET'])
398
+ def get_memory(chat_id):
399
+ """Get memory for a specific user"""
400
+ try:
401
+ memory_data = memory_manager.get_memory(chat_id)
402
+
403
+ if memory_data is None:
404
+ return jsonify({
405
+ "error": "No memory found for this user"
406
+ }), 404
407
+
408
+ return jsonify(memory_data)
409
+
410
+ except Exception as e:
411
+ logger.error(f"Get memory error: {e}")
412
+ return jsonify({
413
+ "error": str(e)
414
+ }), 500
415
+
416
+ @app.route('/api/search-memory/<chat_id>', methods=['POST'])
417
+ def search_memory(chat_id):
418
+ """Search memory for specific information"""
419
+ try:
420
+ data = request.get_json()
421
+
422
+ if not data or 'keyword' not in data:
423
+ return jsonify({
424
+ "error": "Missing 'keyword' parameter"
425
+ }), 400
426
+
427
+ # If chat_id is "all", search entire group memory
428
+ search_chat_id = None if chat_id == "all" else chat_id
429
+ results = memory_manager.search_memory(data['keyword'], chat_id=search_chat_id)
430
+
431
+ return jsonify({
432
+ "results": results,
433
+ "count": len(results)
434
+ })
435
+
436
+ except Exception as e:
437
+ logger.error(f"Search memory error: {e}")
438
+ return jsonify({
439
+ "error": str(e)
440
+ }), 500
441
+
442
+ @app.route('/api/get-all-memory', methods=['GET'])
443
+ def get_all_memory():
444
+ """Get all shared group memory"""
445
+ try:
446
+ memory_data = memory_manager.get_all_memory()
447
+ return jsonify(memory_data)
448
+
449
+ except Exception as e:
450
+ logger.error(f"Get all memory error: {e}")
451
+ return jsonify({
452
+ "error": str(e)
453
+ }), 500
454
+
455
+ @app.route('/api/get-user-info/<chat_id>', methods=['GET'])
456
+ def get_user_info(chat_id):
457
+ """Get information about a specific user"""
458
+ try:
459
+ user_info = memory_manager.get_user_info(chat_id)
460
+
461
+ if user_info is None:
462
+ return jsonify({
463
+ "error": "User not found"
464
+ }), 404
465
+
466
+ return jsonify(user_info)
467
+
468
+ except Exception as e:
469
+ logger.error(f"Get user info error: {e}")
470
+ return jsonify({
471
+ "error": str(e)
472
+ }), 500
473
+
474
+ @app.route('/health', methods=['GET'])
475
+ def health():
476
+ """Health check endpoint"""
477
+ return jsonify({
478
+ "status": "ok",
479
+ "model_loaded": model is not None and tokenizer is not None,
480
+ "queue_size": request_queue.qsize(),
481
+ "total_group_members": len(memory_manager.memory.get("users", {})),
482
+ "total_group_messages": len(memory_manager.memory.get("entries", []))
483
+ })
484
+
485
+ @app.route('/api/check-bridge-health', methods=['GET'])
486
+ def check_bridge_health():
487
+ """Direct endpoint to check bridge API health"""
488
+ health_data = check_health_api()
489
+ return jsonify(health_data)
490
+
491
+ if __name__ == '__main__':
492
+ # Initialize model on startup
493
+ try:
494
+ initialize_model()
495
+ except Exception as e:
496
+ logger.error(f"Warning: Could not initialize model on startup: {e}")
497
+ logger.info("Model will be loaded on first request")
498
+
499
+ # Run with threading enabled
500
+ app.run(host='0.0.0.0', port=7860, debug=False, threaded=True)