harismlnaslm commited on
Commit
60664bc
·
1 Parent(s): cb197a6

Fix Hugging Face Spaces compatibility: Use standard app.py and proper routing

Browse files
Files changed (2) hide show
  1. Dockerfile +1 -1
  2. app.py +376 -0
Dockerfile CHANGED
@@ -29,4 +29,4 @@ USER user
29
  EXPOSE 7860
30
 
31
  # Run the application
32
- CMD ["python", "api_server_simple.py"]
 
29
  EXPOSE 7860
30
 
31
  # Run the application
32
+ CMD ["python", "app.py"]
app.py ADDED
@@ -0,0 +1,376 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Textilindo AI API Server - Hugging Face Spaces Compatible
4
+ Uses dataset-based similarity matching without heavy ML dependencies
5
+ """
6
+
7
+ from flask import Flask, request, jsonify
8
+ from flask_cors import CORS
9
+ import os
10
+ import json
11
+ from difflib import SequenceMatcher
12
+ import logging
13
+
14
+ # Setup logging
15
+ logging.basicConfig(level=logging.INFO)
16
+ logger = logging.getLogger(__name__)
17
+
18
+ app = Flask(__name__)
19
+ CORS(app) # Enable CORS for all routes
20
+
21
+ def load_system_prompt(default_text):
22
+ try:
23
+ base_dir = os.path.dirname(__file__)
24
+ md_path = os.path.join(base_dir, 'configs', 'system_prompt.md')
25
+ if not os.path.exists(md_path):
26
+ return default_text
27
+ with open(md_path, 'r', encoding='utf-8') as f:
28
+ content = f.read()
29
+ start = content.find('"""')
30
+ end = content.rfind('"""')
31
+ if start != -1 and end != -1 and end > start:
32
+ return content[start+3:end].strip()
33
+ lines = []
34
+ for line in content.splitlines():
35
+ if line.strip().startswith('#'):
36
+ continue
37
+ lines.append(line)
38
+ cleaned = '\n'.join(lines).strip()
39
+ return cleaned or default_text
40
+ except Exception:
41
+ return default_text
42
+
43
+ class TextilindoAI:
44
+ def __init__(self):
45
+ self.system_prompt = os.getenv(
46
+ 'SYSTEM_PROMPT',
47
+ load_system_prompt("You are Textilindo AI Assistant. Be concise, helpful, and use Indonesian.")
48
+ )
49
+ self.dataset = self.load_all_datasets()
50
+
51
+ def load_all_datasets(self):
52
+ """Load all available datasets"""
53
+ dataset = []
54
+
55
+ # Try multiple possible data directory paths
56
+ possible_data_dirs = [
57
+ "data",
58
+ "./data",
59
+ "/app/data",
60
+ os.path.join(os.path.dirname(__file__), "data")
61
+ ]
62
+
63
+ data_dir = None
64
+ for dir_path in possible_data_dirs:
65
+ if os.path.exists(dir_path):
66
+ data_dir = dir_path
67
+ logger.info(f"Found data directory: {data_dir}")
68
+ break
69
+
70
+ if not data_dir:
71
+ logger.warning("No data directory found in any of the expected locations")
72
+ return dataset
73
+
74
+ # Load all JSONL files
75
+ try:
76
+ for filename in os.listdir(data_dir):
77
+ if filename.endswith('.jsonl'):
78
+ filepath = os.path.join(data_dir, filename)
79
+ try:
80
+ with open(filepath, 'r', encoding='utf-8') as f:
81
+ for line_num, line in enumerate(f, 1):
82
+ line = line.strip()
83
+ if line:
84
+ try:
85
+ data = json.loads(line)
86
+ dataset.append(data)
87
+ except json.JSONDecodeError as e:
88
+ logger.warning(f"Invalid JSON in {filename} line {line_num}: {e}")
89
+ continue
90
+ logger.info(f"Loaded {filename}: {len([d for d in dataset if d.get('instruction')])} examples")
91
+ except Exception as e:
92
+ logger.error(f"Error loading {filename}: {e}")
93
+ except Exception as e:
94
+ logger.error(f"Error reading data directory {data_dir}: {e}")
95
+
96
+ logger.info(f"Total examples loaded: {len(dataset)}")
97
+ return dataset
98
+
99
+ def find_relevant_context(self, user_query, top_k=3):
100
+ """Find most relevant examples from dataset"""
101
+ if not self.dataset:
102
+ return []
103
+
104
+ scores = []
105
+ for i, example in enumerate(self.dataset):
106
+ instruction = example.get('instruction', '').lower()
107
+ output = example.get('output', '').lower()
108
+ query = user_query.lower()
109
+
110
+ instruction_score = SequenceMatcher(None, query, instruction).ratio()
111
+ output_score = SequenceMatcher(None, query, output).ratio()
112
+ combined_score = (instruction_score * 0.7) + (output_score * 0.3)
113
+ scores.append((combined_score, i))
114
+
115
+ scores.sort(reverse=True)
116
+ relevant_examples = []
117
+
118
+ for score, idx in scores[:top_k]:
119
+ if score > 0.1:
120
+ relevant_examples.append(self.dataset[idx])
121
+
122
+ return relevant_examples
123
+
124
+ def generate_response(self, user_query, relevant_examples):
125
+ """Generate response based on relevant examples"""
126
+ if not relevant_examples:
127
+ return "Maaf, saya tidak memiliki informasi yang cukup untuk menjawab pertanyaan Anda. Silakan hubungi Textilindo langsung untuk informasi lebih lanjut."
128
+
129
+ # Find the most relevant example
130
+ best_example = relevant_examples[0]
131
+ best_answer = best_example.get('output', '')
132
+
133
+ if best_answer:
134
+ return f"Berdasarkan informasi yang tersedia: {best_answer}"
135
+ else:
136
+ return "Saya menemukan beberapa informasi terkait, tetapi tidak dapat memberikan jawaban yang tepat. Silakan coba rephrasing pertanyaan Anda."
137
+
138
+ def chat(self, message, max_tokens=300, temperature=0.7, system_prompt_override=None):
139
+ """Generate response using RAG context"""
140
+ try:
141
+ # Find relevant context
142
+ relevant_examples = self.find_relevant_context(message, 3)
143
+
144
+ # Generate response
145
+ response = self.generate_response(message, relevant_examples)
146
+
147
+ return {
148
+ "success": True,
149
+ "response": response,
150
+ "context_used": len(relevant_examples) > 0,
151
+ "relevant_examples_count": len(relevant_examples),
152
+ "model": "textilindo-rag",
153
+ "tokens_used": len(response.split()) # Approximate token count
154
+ }
155
+
156
+ except Exception as e:
157
+ logger.error(f"Error in chat: {e}")
158
+ return {
159
+ "success": False,
160
+ "error": f"Chat error: {str(e)}"
161
+ }
162
+
163
+ # Initialize AI (lazy loading)
164
+ ai = None
165
+
166
+ def get_ai_assistant():
167
+ """Get or create the AI assistant instance"""
168
+ global ai
169
+ if ai is None:
170
+ try:
171
+ logger.info("Initializing Textilindo AI Assistant...")
172
+ ai = TextilindoAI()
173
+ logger.info("AI Assistant initialized successfully")
174
+ except Exception as e:
175
+ logger.error(f"Failed to initialize AI Assistant: {e}")
176
+ # Create a minimal fallback
177
+ ai = type('FallbackAI', (), {
178
+ 'dataset': [],
179
+ 'chat': lambda self, message, **kwargs: {
180
+ "success": False,
181
+ "error": f"AI Assistant is not available. Error: {str(e)}"
182
+ }
183
+ })()
184
+ return ai
185
+
186
+ @app.route('/health', methods=['GET'])
187
+ def health_check():
188
+ """Health check endpoint"""
189
+ try:
190
+ ai_assistant = get_ai_assistant()
191
+ return jsonify({
192
+ "status": "healthy",
193
+ "service": "Textilindo AI API (RAG-based)",
194
+ "model": "textilindo-rag",
195
+ "dataset_loaded": len(ai_assistant.dataset) > 0,
196
+ "dataset_size": len(ai_assistant.dataset)
197
+ })
198
+ except Exception as e:
199
+ return jsonify({
200
+ "status": "error",
201
+ "error": str(e)
202
+ }), 500
203
+
204
+ @app.route('/chat', methods=['POST'])
205
+ def chat():
206
+ """Main chat endpoint"""
207
+ try:
208
+ data = request.get_json()
209
+
210
+ if not data:
211
+ return jsonify({
212
+ "success": False,
213
+ "error": "No JSON data provided"
214
+ }), 400
215
+
216
+ message = data.get('message', '').strip()
217
+ if not message:
218
+ return jsonify({
219
+ "success": False,
220
+ "error": "Message is required"
221
+ }), 400
222
+
223
+ # Optional parameters
224
+ max_tokens = data.get('max_tokens', 300)
225
+ temperature = data.get('temperature', 0.7)
226
+ system_prompt = data.get('system_prompt')
227
+
228
+ # Validate parameters
229
+ if not isinstance(max_tokens, int) or max_tokens < 1 or max_tokens > 1000:
230
+ return jsonify({
231
+ "success": False,
232
+ "error": "max_tokens must be between 1 and 1000"
233
+ }), 400
234
+
235
+ if not isinstance(temperature, (int, float)) or temperature < 0 or temperature > 2:
236
+ return jsonify({
237
+ "success": False,
238
+ "error": "temperature must be between 0 and 2"
239
+ }), 400
240
+
241
+ # Get AI assistant and process chat
242
+ ai_assistant = get_ai_assistant()
243
+ result = ai_assistant.chat(message, max_tokens, temperature, system_prompt_override=system_prompt)
244
+
245
+ if result["success"]:
246
+ return jsonify(result)
247
+ else:
248
+ return jsonify(result), 500
249
+
250
+ except Exception as e:
251
+ logger.error(f"Error in chat endpoint: {e}")
252
+ return jsonify({
253
+ "success": False,
254
+ "error": f"Internal server error: {str(e)}"
255
+ }), 500
256
+
257
+ @app.route('/stats', methods=['GET'])
258
+ def get_stats():
259
+ """Get dataset and system statistics"""
260
+ try:
261
+ ai_assistant = get_ai_assistant()
262
+ topics = {}
263
+ for example in ai_assistant.dataset:
264
+ metadata = example.get('metadata', {})
265
+ topic = metadata.get('topic', 'unknown')
266
+ topics[topic] = topics.get(topic, 0) + 1
267
+
268
+ return jsonify({
269
+ "success": True,
270
+ "dataset": {
271
+ "total_examples": len(ai_assistant.dataset),
272
+ "topics": topics,
273
+ "topics_count": len(topics)
274
+ },
275
+ "model": {
276
+ "name": "textilindo-rag",
277
+ "type": "RAG-based similarity matching"
278
+ },
279
+ "system": {
280
+ "api_version": "1.0.0",
281
+ "status": "operational"
282
+ }
283
+ })
284
+
285
+ except Exception as e:
286
+ logger.error(f"Error in stats endpoint: {e}")
287
+ return jsonify({
288
+ "success": False,
289
+ "error": f"Internal server error: {str(e)}"
290
+ }), 500
291
+
292
+ @app.route('/examples', methods=['GET'])
293
+ def get_examples():
294
+ """Get sample questions from dataset"""
295
+ try:
296
+ ai_assistant = get_ai_assistant()
297
+ limit = request.args.get('limit', 10, type=int)
298
+ limit = min(limit, 50) # Max 50 examples
299
+
300
+ examples = []
301
+ for example in ai_assistant.dataset[:limit]:
302
+ examples.append({
303
+ "instruction": example.get('instruction', ''),
304
+ "output": example.get('output', ''),
305
+ "topic": example.get('metadata', {}).get('topic', 'unknown')
306
+ })
307
+
308
+ return jsonify({
309
+ "success": True,
310
+ "examples": examples,
311
+ "total_returned": len(examples),
312
+ "total_available": len(ai_assistant.dataset)
313
+ })
314
+
315
+ except Exception as e:
316
+ logger.error(f"Error in examples endpoint: {e}")
317
+ return jsonify({
318
+ "success": False,
319
+ "error": f"Internal server error: {str(e)}"
320
+ }), 500
321
+
322
+ @app.route('/', methods=['GET'])
323
+ def root():
324
+ """API root endpoint with documentation"""
325
+ try:
326
+ ai_assistant = get_ai_assistant()
327
+ return jsonify({
328
+ "service": "Textilindo AI API (RAG-based)",
329
+ "version": "1.0.0",
330
+ "description": "AI-powered customer service for Textilindo using RAG similarity matching",
331
+ "endpoints": {
332
+ "GET /": "API documentation (this endpoint)",
333
+ "GET /health": "Health check",
334
+ "POST /chat": "Chat with AI",
335
+ "GET /stats": "Dataset and system statistics",
336
+ "GET /examples": "Sample questions from dataset"
337
+ },
338
+ "usage": {
339
+ "chat": {
340
+ "method": "POST",
341
+ "url": "/chat",
342
+ "body": {
343
+ "message": "string (required)",
344
+ "max_tokens": "integer (optional, default: 300)",
345
+ "temperature": "float (optional, default: 0.7)"
346
+ }
347
+ }
348
+ },
349
+ "model": "textilindo-rag",
350
+ "dataset_size": len(ai_assistant.dataset)
351
+ })
352
+ except Exception as e:
353
+ return jsonify({
354
+ "success": False,
355
+ "error": f"Internal server error: {str(e)}"
356
+ }), 500
357
+
358
+ if __name__ == '__main__':
359
+ logger.info("Starting Textilindo AI API Server (RAG-based)...")
360
+
361
+ # Try to initialize AI assistant early to catch any issues
362
+ try:
363
+ ai_assistant = get_ai_assistant()
364
+ logger.info(f"Dataset loaded: {len(ai_assistant.dataset)} examples")
365
+ except Exception as e:
366
+ logger.warning(f"AI Assistant initialization failed: {e}")
367
+ logger.info("Continuing with fallback mode...")
368
+
369
+ # Get port from environment variable (for Hugging Face Spaces)
370
+ port = int(os.environ.get('PORT', 7860))
371
+
372
+ app.run(
373
+ debug=False,
374
+ host='0.0.0.0',
375
+ port=port
376
+ )