| 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 |
|
|
| |
| MODEL_NAME = "Hercule66/qwen3-freelance-chatbot-tpu" |
| BASE_MODEL_NAME = "Qwen/Qwen3-0.6B" |
|
|
| |
| model = None |
| tokenizer = None |
| model_loading = False |
| load_lock = Lock() |
|
|
| |
| 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() |
| |
| |
| hf_token = os.getenv("HF_TOKEN") |
| if hf_token: |
| login(token=hf_token) |
| |
| |
| tokenizer = AutoTokenizer.from_pretrained( |
| MODEL_NAME, |
| cache_dir="./model_cache", |
| use_fast=True |
| ) |
| |
| |
| 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" |
| |
| |
| base_model = AutoModelForCausalLM.from_pretrained( |
| BASE_MODEL_NAME, |
| **load_config |
| ) |
| |
| |
| model = PeftModel.from_pretrained( |
| base_model, |
| MODEL_NAME, |
| cache_dir="./model_cache" |
| ) |
| |
| |
| 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: |
| |
| 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} |
| """) |
| }] |
|
|
| |
| inputs = tokenizer.apply_chat_template( |
| messages, |
| add_generation_prompt=True, |
| tokenize=True, |
| return_dict=True, |
| return_tensors="pt", |
| max_length=1024, |
| truncation=True |
| ) |
| |
| 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, 400), |
| temperature=temperature, |
| top_p=top_p, |
| repetition_penalty=1.05, |
| do_sample=True, |
| pad_token_id=tokenizer.eos_token_id, |
| use_cache=True, |
| num_beams=1 |
| ) |
|
|
| 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 |
| |
| |
| conversation_id = f"conv_{int(time.time())}_{hash(message) % 10000}" |
| |
| |
| if model is None or tokenizer is None: |
| load_model() |
| |
| try: |
| start_time = time.time() |
| |
| |
| 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_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 = system_message + "\n" |
| |
| |
| if history: |
| for user_msg, bot_msg in history[-3:]: |
| 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_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 |
|
|
| |
| 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)}" |
|
|
| |
| ensure_log_files() |
|
|
| |
| 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): |
| |
| 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", |
| ) |
| |
| |
| 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" |
| ) |
| |
| |
| 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 |
| """) |
|
|
| |
| def respond_and_save(message, history, system_msg, max_tok, temp, top_p): |
| |
| response = respond(message, history, system_msg, max_tok, temp, top_p) |
| |
| |
| new_history = history + [[message, response]] |
| |
| |
| save_last_exchange(new_history) |
| |
| return new_history, "" |
| |
| |
| 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 |
| ) |