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 )