liborax / app.py
Hercule66's picture
Update app.py
b95be75 verified
Raw
History Blame Contribute Delete
17.9 kB
import gradio as gr
import torch
from transformers import AutoTokenizer, AutoModelForCausalLM
from peft import PeftModel
from huggingface_hub import login
import os
import json
import datetime
import time
from threading import Lock
import csv
# Configuration du modèle
MODEL_NAME = "Hercule66/qwen3-freelance-chatbot-tpu"
BASE_MODEL_NAME = "Qwen/Qwen3-0.6B"
# Variables globales pour le modèle et tokenizer
model = None
tokenizer = None
model_loading = False
load_lock = Lock()
# Fichier pour stocker les logs
LOGS_FILE = "conversation_logs.jsonl"
FEEDBACK_FILE = "feedback_logs.csv"
def ensure_log_files():
"""S'assurer que les fichiers de log existent"""
if not os.path.exists(LOGS_FILE):
with open(LOGS_FILE, 'w') as f:
pass
if not os.path.exists(FEEDBACK_FILE):
with open(FEEDBACK_FILE, 'w', newline='') as f:
writer = csv.writer(f)
writer.writerow(['timestamp', 'conversation_id', 'user_input', 'model_output', 'rating', 'feedback_type'])
def log_conversation(user_input, model_output, conversation_id=None):
"""Enregistre une conversation dans le fichier de log"""
try:
log_entry = {
"timestamp": datetime.datetime.now().isoformat(),
"conversation_id": conversation_id or f"conv_{int(time.time())}",
"user_input": user_input,
"model_output": model_output,
"model_name": MODEL_NAME
}
with open(LOGS_FILE, 'a', encoding='utf-8') as f:
f.write(json.dumps(log_entry, ensure_ascii=False) + '\n')
except Exception as e:
print(f"Erreur lors de l'enregistrement du log: {e}")
def log_feedback(conversation_id, user_input, model_output, rating, feedback_type):
"""Enregistre le feedback utilisateur"""
try:
with open(FEEDBACK_FILE, 'a', newline='', encoding='utf-8') as f:
writer = csv.writer(f)
writer.writerow([
datetime.datetime.now().isoformat(),
conversation_id,
user_input[:200] + "..." if len(user_input) > 200 else user_input,
model_output[:200] + "..." if len(model_output) > 200 else model_output,
rating,
feedback_type
])
except Exception as e:
print(f"Erreur lors de l'enregistrement du feedback: {e}")
def load_model():
"""Charge le modèle et le tokenizer une seule fois avec optimisations"""
global model, tokenizer, model_loading
with load_lock:
if model is not None and tokenizer is not None:
return
if model_loading:
return
model_loading = True
try:
print("🔄 Chargement du modèle en cours...")
start_time = time.time()
# Authentification avec le token HF
hf_token = os.getenv("HF_TOKEN")
if hf_token:
login(token=hf_token)
# Chargement du tokenizer avec mise en cache
tokenizer = AutoTokenizer.from_pretrained(
MODEL_NAME,
cache_dir="./model_cache",
use_fast=True
)
# Configuration optimisée pour le chargement du modèle
load_config = {
"torch_dtype": torch.float16 if torch.cuda.is_available() else torch.float32,
"low_cpu_mem_usage": True,
"cache_dir": "./model_cache"
}
if torch.cuda.is_available():
load_config["device_map"] = "auto"
# Chargement du modèle de base
base_model = AutoModelForCausalLM.from_pretrained(
BASE_MODEL_NAME,
**load_config
)
# Chargement du modèle fine-tuné avec PEFT
model = PeftModel.from_pretrained(
base_model,
MODEL_NAME,
cache_dir="./model_cache"
)
# Optimisation pour l'inférence
model.eval()
if hasattr(model, 'merge_and_unload'):
print("🔧 Optimisation du modèle...")
model = model.merge_and_unload()
load_time = time.time() - start_time
print(f"✅ Modèle chargé avec succès en {load_time:.2f}s!")
except Exception as e:
print(f"❌ Erreur lors du chargement du modèle: {e}")
raise e
finally:
model_loading = False
def generate_proposal_fast(job_posting, max_tokens=500, temperature=0.7, top_p=0.9):
"""Version optimisée de génération de proposition"""
try:
# Format du prompt optimisé
messages = [{
"role": "user",
"content": (
f"""
You are a world-class strategic freelance consultant. Your goal is to win jobs by writing hyper-personalized, direct, and insightful proposals that show you've deeply understood the client's true need.
First, analyze the following job posting step-by-step based on this framework:
1. **CORE TASK:** What is the single most important thing the client wants to accomplish? (e.g., "Publish an app," not "develop an app").
2. **CRITICAL REQUIREMENT:** What is the one specific asset or piece of information the client absolutely needs from the freelancer to even consider them? (e.g., "A valid Play Console account without restrictions").
3. **MISLEADING KEYWORDS:** What keywords are in the post that might trick a generic AI into giving a wrong or irrelevant answer? (e.g., "Android App Development" might mislead an AI to talk about coding skills).
4. **IMMEDIATE ACTION:** Is there a specific instruction the freelancer must follow for their proposal to be read? (e.g., "Provide a screenshot," "Answer 3+3=?").
After your analysis, write a concise, professional, and ready-to-send proposal that directly addresses the CORE TASK and CRITICAL REQUIREMENT.
**RULES FOR THE PROPOSAL:**
- Be direct and confident.
- Immediately address the CRITICAL REQUIREMENT.
- Confirm you can perform the CORE TASK.
- If there's an IMMEDIATE ACTION, do it first.
- Avoid filler phrases like “I am passionate about…” or “I believe I can…”.
- Do NOT list generic skills that are not directly relevant to the CORE TASK.
- Show 1–2 concrete past examples or results, but keep them short.
- Include exactly one clarifying question to open dialogue.
- Suggest a clear rate or fixed price aligned with the client’s budget.
- End with a strong call to action that invites the client to reply quickly.
Now, here is the job posting:
{job_posting}
""")
}]
# Préparation des inputs avec optimisations
inputs = tokenizer.apply_chat_template(
messages,
add_generation_prompt=True,
tokenize=True,
return_dict=True,
return_tensors="pt",
max_length=1024, # Limite pour accélérer
truncation=True
)
if hasattr(model, 'device'):
inputs = {k: v.to(model.device) for k, v in inputs.items()}
# Génération optimisée
with torch.no_grad():
outputs = model.generate(
**inputs,
max_new_tokens=min(max_tokens, 400), # Limite raisonnabe
temperature=temperature,
top_p=top_p,
repetition_penalty=1.05, # Réduit pour plus de fluidité
do_sample=True,
pad_token_id=tokenizer.eos_token_id,
use_cache=True, # Active le cache KV
num_beams=1 # Pas de beam search pour plus de rapidité
)
response = tokenizer.decode(
outputs[0][inputs["input_ids"].shape[-1]:],
skip_special_tokens=True
)
return response.strip()
except Exception as e:
return f"Erreur lors de la génération: {str(e)}"
def respond(message, history, system_message, max_tokens, temperature, top_p):
"""
Fonction de réponse simple qui utilise le format Gradio standard
"""
global model, tokenizer
# ID unique pour cette conversation
conversation_id = f"conv_{int(time.time())}_{hash(message) % 10000}"
# Chargement du modèle si nécessaire
if model is None or tokenizer is None:
load_model()
try:
start_time = time.time()
# Détection des job postings avec plus de mots-clés
job_keywords = ['job', 'project', 'freelance', 'budget', 'requirements',
'looking for', 'need', 'hiring', 'developer', 'designer',
'writer', 'urgent', 'deadline', 'experience', 'skills']
is_job_posting = any(keyword in message.lower() for keyword in job_keywords)
if is_job_posting:
response = generate_proposal_fast(message, max_tokens, temperature, top_p)
# Log de la conversation
log_conversation(message, response, conversation_id)
generation_time = time.time() - start_time
final_response = f"{response}\n\n*⚡ Généré en {generation_time:.1f}s*"
return final_response
else:
# Conversation normale optimisée
# Construction du contexte depuis l'historique Gradio
conversation = system_message + "\n"
# Ajouter l'historique (format Gradio: [[user1, bot1], [user2, bot2], ...])
if history:
for user_msg, bot_msg in history[-3:]: # Garde les 3 derniers échanges
conversation += f"User: {user_msg}\n"
if bot_msg:
conversation += f"Assistant: {bot_msg}\n"
conversation += f"User: {message}\nAssistant:"
inputs = tokenizer(
conversation,
return_tensors="pt",
truncation=True,
max_length=1024
)
if hasattr(model, 'device'):
inputs = {k: v.to(model.device) for k, v in inputs.items()}
with torch.no_grad():
outputs = model.generate(
**inputs,
max_new_tokens=min(max_tokens, 300),
temperature=temperature,
top_p=top_p,
repetition_penalty=1.05,
do_sample=True,
pad_token_id=tokenizer.eos_token_id,
use_cache=True
)
response = tokenizer.decode(
outputs[0][inputs["input_ids"].shape[-1]:],
skip_special_tokens=True
).strip()
# Log de la conversation
log_conversation(message, response, conversation_id)
return response
except Exception as e:
error_msg = f"❌ Erreur: {str(e)}"
log_conversation(message, error_msg, conversation_id)
return error_msg
# Variables pour stocker le dernier échange (pour le feedback)
last_user_input = ""
last_model_output = ""
def save_last_exchange(history):
"""Sauvegarde le dernier échange pour le feedback"""
global last_user_input, last_model_output
if history and len(history) > 0:
last_exchange = history[-1]
if len(last_exchange) >= 2:
last_user_input = last_exchange[0] or ""
last_model_output = last_exchange[1] or ""
return history
def handle_feedback(rating_type):
"""Gère le feedback utilisateur"""
global last_user_input, last_model_output
try:
if not last_user_input or not last_model_output:
return "❌ Aucun échange récent trouvé pour le feedback"
conversation_id = f"feedback_{int(time.time())}"
log_feedback(conversation_id, last_user_input, last_model_output,
1 if rating_type == "like" else 0, rating_type)
emoji = "👍" if rating_type == "like" else "👎"
return f"{emoji} Merci pour votre feedback!"
except Exception as e:
return f"❌ Erreur lors de l'enregistrement: {str(e)}"
# Initialisation des fichiers de log
ensure_log_files()
# Interface Gradio simplifiée
with gr.Blocks(theme=gr.themes.Soft(), title="🎯 Freelance Proposal Assistant") as demo:
gr.Markdown("# 🎯 Freelance Proposal Assistant")
gr.Markdown("*Powered by Qwen3 fine-tuned model - Optimized for speed*")
with gr.Row():
with gr.Column(scale=4):
# Interface de chat simple
chatbot = gr.Chatbot(
label="💬 Assistant Freelance",
height=500,
show_copy_button=True
)
with gr.Row():
msg = gr.Textbox(
placeholder="Collez votre job posting ici ou posez une question...",
container=False,
scale=4,
lines=2
)
send_btn = gr.Button("📤 Envoyer", variant="primary", scale=1)
with gr.Row():
clear_btn = gr.Button("🗑️ Effacer", variant="secondary")
like_btn = gr.Button("👍 Utile", variant="secondary")
dislike_btn = gr.Button("👎 Pas utile", variant="secondary")
feedback_msg = gr.Textbox(
label="Feedback",
visible=False,
interactive=False
)
with gr.Column(scale=1):
gr.Markdown("### ⚙️ Paramètres")
system_message = gr.Textbox(
value="You are a professional freelance consultant specialized in creating winning proposals. Be concise, professional, and actionable.",
label="Message système",
lines=3
)
max_tokens = gr.Slider(
minimum=100,
maximum=800,
value=400,
step=50,
label="Tokens max"
)
temperature = gr.Slider(
minimum=0.1,
maximum=1.0,
value=0.7,
step=0.1,
label="Créativité"
)
top_p = gr.Slider(
minimum=0.1,
maximum=1.0,
value=0.9,
step=0.05,
label="Focus",
)
# Exemples
with gr.Row():
examples = gr.Examples(
examples=[
["Web development project: Build a responsive e-commerce site with payment integration. Budget: $1000-2000"],
["Data analysis: Analyze sales data and create visualizations. Urgent - 3 days deadline"],
["Content writing: Need blog articles about digital marketing, 5 articles, $50 each"],
["Mobile app: iOS/Android app for food delivery, budget $5000, 2 months timeline"]
],
inputs=msg,
label="🔥 Exemples de job postings"
)
# Stats et informations
gr.Markdown("""
### 💡 Conseils pour de meilleurs résultats:
- **Collez le job posting complet** pour une analyse précise
- **Ajustez la créativité** (0.1 = conservateur, 1.0 = créatif)
- **Utilisez le feedback** 👍👎 pour améliorer le modèle
- **Copiez facilement** vos propositions avec le bouton de copie
""")
# Configuration des événements
def respond_and_save(message, history, system_msg, max_tok, temp, top_p):
# Génération de la réponse
response = respond(message, history, system_msg, max_tok, temp, top_p)
# Mise à jour de l'historique
new_history = history + [[message, response]]
# Sauvegarde pour le feedback
save_last_exchange(new_history)
return new_history, ""
# Événements
msg.submit(
respond_and_save,
[msg, chatbot, system_message, max_tokens, temperature, top_p],
[chatbot, msg]
)
send_btn.click(
respond_and_save,
[msg, chatbot, system_message, max_tokens, temperature, top_p],
[chatbot, msg]
)
clear_btn.click(lambda: [], None, [chatbot])
like_btn.click(
lambda: handle_feedback("like"),
None,
feedback_msg
).then(
lambda x: gr.update(value=x, visible=True),
feedback_msg,
feedback_msg
)
dislike_btn.click(
lambda: handle_feedback("dislike"),
None,
feedback_msg
).then(
lambda x: gr.update(value=x, visible=True),
feedback_msg,
feedback_msg
)
if __name__ == "__main__":
print("🚀 Démarrage de l'interface optimisée...")
print("📊 Logs sauvegardés dans:", LOGS_FILE)
print("👍 Feedback sauvegardé dans:", FEEDBACK_FILE)
demo.launch(
server_name="0.0.0.0",
server_port=7860,
show_error=True
)