textilindo-ai-assistant / api_server.py
Stefanus Simandjuntak
feat(prompt): add/update system prompt
c696f9e
#!/usr/bin/env python3
"""
Textilindo AI API Server
Clean API-only implementation
"""
from flask import Flask, request, jsonify
from flask_cors import CORS
import os
import json
import requests
from difflib import SequenceMatcher
import logging
# Setup logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
app = Flask(__name__)
CORS(app) # Enable CORS for all routes
def load_system_prompt(default_text):
try:
base_dir = os.path.dirname(__file__)
md_path = os.path.join(base_dir, 'configs', 'system_prompt.md')
if not os.path.exists(md_path):
return default_text
with open(md_path, 'r', encoding='utf-8') as f:
content = f.read()
start = content.find('"""')
end = content.rfind('"""')
if start != -1 and end != -1 and end > start:
return content[start+3:end].strip()
lines = []
for line in content.splitlines():
if line.strip().startswith('#'):
continue
lines.append(line)
cleaned = '\n'.join(lines).strip()
return cleaned or default_text
except Exception:
return default_text
class TextilindoAI:
def __init__(self, api_key):
self.api_key = api_key
self.base_url = "https://api.novita.ai/openai"
self.headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json"
}
self.model = "qwen/qwen3-235b-a22b-instruct-2507"
self.system_prompt = os.getenv(
'SYSTEM_PROMPT',
load_system_prompt("You are Textilindo AI Assistant. Be concise, helpful, and use Indonesian.")
)
self.dataset = self.load_dataset()
def load_dataset(self):
"""Load the training dataset"""
dataset = []
dataset_path = "data/textilindo_training_data.jsonl"
if os.path.exists(dataset_path):
try:
with open(dataset_path, 'r', encoding='utf-8') as f:
for line in f:
line = line.strip()
if line:
data = json.loads(line)
dataset.append(data)
logger.info(f"Loaded {len(dataset)} examples from dataset")
except Exception as e:
logger.error(f"Error loading dataset: {e}")
return dataset
def find_relevant_context(self, user_query, top_k=3):
"""Find most relevant examples from dataset"""
if not self.dataset:
return []
scores = []
for i, example in enumerate(self.dataset):
instruction = example.get('instruction', '').lower()
output = example.get('output', '').lower()
query = user_query.lower()
instruction_score = SequenceMatcher(None, query, instruction).ratio()
output_score = SequenceMatcher(None, query, output).ratio()
combined_score = (instruction_score * 0.7) + (output_score * 0.3)
scores.append((combined_score, i))
scores.sort(reverse=True)
relevant_examples = []
for score, idx in scores[:top_k]:
if score > 0.1:
relevant_examples.append(self.dataset[idx])
return relevant_examples
def create_context_prompt(self, user_query, relevant_examples):
"""Create a prompt with relevant context"""
if not relevant_examples:
return user_query
context_parts = []
context_parts.append("Berikut adalah beberapa contoh pertanyaan dan jawaban tentang Textilindo:")
context_parts.append("")
for i, example in enumerate(relevant_examples, 1):
instruction = example.get('instruction', '')
output = example.get('output', '')
context_parts.append(f"Contoh {i}:")
context_parts.append(f"Pertanyaan: {instruction}")
context_parts.append(f"Jawaban: {output}")
context_parts.append("")
context_parts.append("Berdasarkan contoh di atas, jawab pertanyaan berikut:")
context_parts.append(f"Pertanyaan: {user_query}")
context_parts.append("Jawaban:")
return "\n".join(context_parts)
def chat(self, message, max_tokens=300, temperature=0.7, system_prompt_override=None):
"""Send message to Novita AI with RAG context"""
relevant_examples = self.find_relevant_context(message, 3)
if relevant_examples:
enhanced_prompt = self.create_context_prompt(message, relevant_examples)
context_used = True
else:
enhanced_prompt = message
context_used = False
system_message = {
"role": "system",
"content": (system_prompt_override or self.system_prompt)
}
payload = {
"model": self.model,
"messages": [system_message, {"role": "user", "content": enhanced_prompt}],
"max_tokens": max_tokens,
"temperature": temperature,
"top_p": 0.9
}
try:
response = requests.post(
f"{self.base_url}/chat/completions",
headers=self.headers,
json=payload,
timeout=30
)
if response.status_code == 200:
result = response.json()
ai_response = result.get('choices', [{}])[0].get('message', {}).get('content', '')
return {
"success": True,
"response": ai_response,
"context_used": context_used,
"relevant_examples_count": len(relevant_examples),
"model": self.model,
"tokens_used": result.get('usage', {}).get('total_tokens', 0)
}
else:
return {
"success": False,
"error": f"API Error: {response.status_code}",
"details": response.text
}
except Exception as e:
return {
"success": False,
"error": f"Request Error: {str(e)}"
}
# Initialize AI
api_key = os.getenv('NOVITA_API_KEY')
if not api_key:
logger.error("NOVITA_API_KEY not found in environment variables")
exit(1)
ai = TextilindoAI(api_key)
@app.route('/health', methods=['GET'])
def health_check():
"""Health check endpoint"""
return jsonify({
"status": "healthy",
"service": "Textilindo AI API",
"model": ai.model,
"dataset_loaded": len(ai.dataset) > 0,
"dataset_size": len(ai.dataset)
})
@app.route('/chat', methods=['POST'])
def chat():
"""Main chat endpoint"""
try:
data = request.get_json()
if not data:
return jsonify({
"success": False,
"error": "No JSON data provided"
}), 400
message = data.get('message', '').strip()
if not message:
return jsonify({
"success": False,
"error": "Message is required"
}), 400
# Optional parameters
max_tokens = data.get('max_tokens', 300)
temperature = data.get('temperature', 0.7)
system_prompt = data.get('system_prompt')
# Validate parameters
if not isinstance(max_tokens, int) or max_tokens < 1 or max_tokens > 1000:
return jsonify({
"success": False,
"error": "max_tokens must be between 1 and 1000"
}), 400
if not isinstance(temperature, (int, float)) or temperature < 0 or temperature > 2:
return jsonify({
"success": False,
"error": "temperature must be between 0 and 2"
}), 400
# Process chat
result = ai.chat(message, max_tokens, temperature, system_prompt_override=system_prompt)
if result["success"]:
return jsonify(result)
else:
return jsonify(result), 500
except Exception as e:
logger.error(f"Error in chat endpoint: {e}")
return jsonify({
"success": False,
"error": f"Internal server error: {str(e)}"
}), 500
@app.route('/stats', methods=['GET'])
def get_stats():
"""Get dataset and system statistics"""
try:
topics = {}
for example in ai.dataset:
metadata = example.get('metadata', {})
topic = metadata.get('topic', 'unknown')
topics[topic] = topics.get(topic, 0) + 1
return jsonify({
"success": True,
"dataset": {
"total_examples": len(ai.dataset),
"topics": topics,
"topics_count": len(topics)
},
"model": {
"name": ai.model,
"provider": "Novita AI"
},
"system": {
"api_version": "1.0.0",
"status": "operational"
}
})
except Exception as e:
logger.error(f"Error in stats endpoint: {e}")
return jsonify({
"success": False,
"error": f"Internal server error: {str(e)}"
}), 500
@app.route('/examples', methods=['GET'])
def get_examples():
"""Get sample questions from dataset"""
try:
limit = request.args.get('limit', 10, type=int)
limit = min(limit, 50) # Max 50 examples
examples = []
for example in ai.dataset[:limit]:
examples.append({
"instruction": example.get('instruction', ''),
"output": example.get('output', ''),
"topic": example.get('metadata', {}).get('topic', 'unknown')
})
return jsonify({
"success": True,
"examples": examples,
"total_returned": len(examples),
"total_available": len(ai.dataset)
})
except Exception as e:
logger.error(f"Error in examples endpoint: {e}")
return jsonify({
"success": False,
"error": f"Internal server error: {str(e)}"
}), 500
@app.route('/', methods=['GET'])
def root():
"""API root endpoint with documentation"""
return jsonify({
"service": "Textilindo AI API",
"version": "1.0.0",
"description": "AI-powered customer service for Textilindo",
"endpoints": {
"GET /": "API documentation (this endpoint)",
"GET /health": "Health check",
"POST /chat": "Chat with AI",
"GET /stats": "Dataset and system statistics",
"GET /examples": "Sample questions from dataset"
},
"usage": {
"chat": {
"method": "POST",
"url": "/chat",
"body": {
"message": "string (required)",
"max_tokens": "integer (optional, default: 300)",
"temperature": "float (optional, default: 0.7)"
}
}
},
"model": ai.model,
"dataset_size": len(ai.dataset)
})
if __name__ == '__main__':
logger.info("Starting Textilindo AI API Server...")
logger.info(f"Model: {ai.model}")
logger.info(f"Dataset loaded: {len(ai.dataset)} examples")
app.run(
debug=False, # Set to False for production
host='0.0.0.0',
port=8082
)