Spaces:
Sleeping
Sleeping
| import asyncio | |
| import json | |
| import re | |
| import os | |
| from huggingface_hub import InferenceClient | |
| # ── Config ──────────────────────────────────────────────────────────────────── | |
| HF_TOKEN = os.getenv("HF_TOKEN", "") | |
| MODEL_NAME = os.getenv("HF_MODEL", "Qwen/Qwen2.5-72B-Instruct") | |
| conversation_store: dict[str, list] = {} | |
| _client: InferenceClient | None = None | |
| def _get_client() -> InferenceClient: | |
| global _client | |
| if _client is None: | |
| _client = InferenceClient(token=HF_TOKEN or None) | |
| return _client | |
| # ── Core call — utilise chat_completion (compatible tous providers HF) ───────── | |
| def _call_hf( | |
| system: str, | |
| user: str, | |
| max_tokens: int = 1024, | |
| temperature: float = 0.4, | |
| ) -> str: | |
| try: | |
| client = _get_client() | |
| response = client.chat_completion( | |
| model=MODEL_NAME, | |
| messages=[ | |
| {"role": "system", "content": system}, | |
| {"role": "user", "content": user}, | |
| ], | |
| max_tokens=max_tokens, | |
| temperature=temperature, | |
| ) | |
| return response.choices[0].message.content.strip() | |
| except Exception as e: | |
| raise Exception(f"HuggingFace InferenceClient error: {str(e)}") | |
| # ── JSON helpers ────────────────────────────────────────────────────────────── | |
| def _fix_json(s: str) -> str: | |
| s = re.sub(r',\s*([}\]])', r'\1', s) | |
| s = re.sub(r'[\x00-\x1f\x7f]', ' ', s) | |
| return s | |
| def _extract_json_array(raw: str) -> list: | |
| cleaned = re.sub(r'```(?:json)?\s*', '', raw) | |
| cleaned = re.sub(r'```', '', cleaned).strip() | |
| try: | |
| result = json.loads(cleaned) | |
| if isinstance(result, list): | |
| return result | |
| except Exception: | |
| pass | |
| start = cleaned.find('[') | |
| if start != -1: | |
| depth = 0 | |
| for i, ch in enumerate(cleaned[start:], start): | |
| if ch == '[': | |
| depth += 1 | |
| elif ch == ']': | |
| depth -= 1 | |
| if depth == 0: | |
| candidate = cleaned[start:i + 1] | |
| for attempt in (candidate, _fix_json(candidate)): | |
| try: | |
| result = json.loads(attempt) | |
| if isinstance(result, list): | |
| return result | |
| except Exception: | |
| pass | |
| break | |
| match = re.search(r'\[[\s\S]*\]', cleaned) | |
| if match: | |
| for attempt in (match.group(), _fix_json(match.group())): | |
| try: | |
| return json.loads(attempt) | |
| except Exception: | |
| pass | |
| return [] | |
| # ── Conversation history ────────────────────────────────────────────────────── | |
| def _get_history(user_id: str) -> list: | |
| return conversation_store.get(user_id, []) | |
| def _save_history(user_id: str, user_msg: str, ai_msg: str) -> None: | |
| if user_id not in conversation_store: | |
| conversation_store[user_id] = [] | |
| conversation_store[user_id].append({"user": user_msg, "assistant": ai_msg}) | |
| conversation_store[user_id] = conversation_store[user_id][-5:] | |
| # ── Async entry point ───────────────────────────────────────────────────────── | |
| async def run_agent(action: str, data: dict) -> dict: | |
| loop = asyncio.get_event_loop() | |
| return await loop.run_in_executor(None, _run_sync, action, data) | |
| def _run_sync(action: str, data: dict) -> dict: | |
| dispatch = { | |
| "chat": _chat, | |
| "quiz": _quiz, | |
| "flashcards": _flashcards, | |
| "explain": _explain, | |
| "resume": _resume, | |
| "rag-qa": _rag_qa, | |
| } | |
| handler = dispatch.get(action) | |
| if handler: | |
| return handler(data) | |
| return {"answer": f"Unknown action: {action}", "action": action} | |
| # ── Action handlers ─────────────────────────────────────────────────────────── | |
| def _chat(data: dict) -> dict: | |
| query = data.get("query", "") | |
| user_id = data.get("user_id", "anonymous") | |
| history = _get_history(user_id) | |
| history_text = "" | |
| if history: | |
| history_text = "Conversation récente :\n" + "\n".join( | |
| f"Utilisateur: {h['user']}\nAssistant: {h['assistant']}" | |
| for h in history | |
| ) + "\n\n" | |
| system = ( | |
| "Tu es PaperBrain AI, un assistant pédagogique pour les étudiants. " | |
| "Aide les étudiants à comprendre leurs cours, préparer leurs examens et apprendre efficacement. " | |
| "Réponds toujours dans la même langue que la question. " | |
| "Sois clair, structuré et pédagogique." | |
| ) | |
| user = f"{history_text}Utilisateur : {query}" | |
| answer = _call_hf(system, user, max_tokens=1024, temperature=0.5) | |
| _save_history(user_id, query, answer) | |
| return {"answer": answer, "user_id": user_id} | |
| def _quiz(data: dict) -> dict: | |
| topic = data.get("topic", "") | |
| num_questions = data.get("num_questions", 5) | |
| difficulty = data.get("difficulty", "medium") | |
| difficulty_map = { | |
| "easy": "simples et directes, pour débutants", | |
| "medium": "de difficulté intermédiaire", | |
| "hard": "difficiles et approfondies, pour experts", | |
| } | |
| level_desc = difficulty_map.get(difficulty, "de difficulté intermédiaire") | |
| system = ( | |
| "Tu es un générateur de quiz pédagogique. " | |
| "Tu réponds UNIQUEMENT avec un tableau JSON valide, sans texte avant ni après, sans balises markdown." | |
| ) | |
| user = ( | |
| f"Génère {num_questions} questions QCM ({level_desc}) sur : \"{topic}\".\n\n" | |
| "Chaque objet JSON doit contenir : question, options (tableau de 4 chaînes " | |
| "\"A) ...\", \"B) ...\", \"C) ...\", \"D) ...\"), correct_answer (A/B/C/D), explanation.\n\n" | |
| "Réponds UNIQUEMENT avec le tableau JSON." | |
| ) | |
| raw = _call_hf(system, user, max_tokens=1500, temperature=0.3) | |
| questions = _extract_json_array(raw) | |
| if questions: | |
| clean = [ | |
| { | |
| "question": str(q.get("question", "")), | |
| "options": list(q.get("options", [])), | |
| "correct_answer": str(q.get("correct_answer", "A")), | |
| "explanation": str(q.get("explanation", "")), | |
| } | |
| for q in questions | |
| if isinstance(q, dict) and q.get("question") and q.get("options") | |
| ] | |
| if clean: | |
| return {"questions": clean, "topic": topic, "difficulty": difficulty} | |
| return {"questions": [], "topic": topic, "error": "JSON invalide.", "raw_preview": raw[:300]} | |
| def _flashcards(data: dict) -> dict: | |
| topic = data.get("topic", "") | |
| num_cards = data.get("num_cards", 8) | |
| system = ( | |
| "Tu es un générateur de flashcards pédagogiques. " | |
| "Tu réponds UNIQUEMENT avec un tableau JSON valide, sans texte avant ni après, sans balises markdown." | |
| ) | |
| user = ( | |
| f"Génère {num_cards} flashcards sur : \"{topic}\".\n\n" | |
| "Chaque objet JSON doit contenir : front (question/terme) et back (réponse/définition).\n\n" | |
| "Réponds UNIQUEMENT avec le tableau JSON." | |
| ) | |
| raw = _call_hf(system, user, max_tokens=1024, temperature=0.3) | |
| cards = _extract_json_array(raw) | |
| if cards: | |
| clean = [ | |
| {"front": str(c.get("front", "")), "back": str(c.get("back", ""))} | |
| for c in cards | |
| if isinstance(c, dict) and c.get("front") and c.get("back") | |
| ] | |
| if clean: | |
| return {"flashcards": clean, "topic": topic} | |
| return {"flashcards": [], "topic": topic, "error": "Impossible de parser les flashcards."} | |
| def _explain(data: dict) -> dict: | |
| concept = data.get("concept", "") | |
| level = data.get("level", "intermediate") | |
| level_map = { | |
| "beginner": "de manière très simple, avec des analogies du quotidien, pour un lycéen", | |
| "intermediate": "clairement avec les concepts essentiels, pour un étudiant universitaire", | |
| "advanced": "de manière approfondie et technique, pour un expert du domaine", | |
| } | |
| level_desc = level_map.get(level, level_map["intermediate"]) | |
| system = ( | |
| "Tu es un professeur pédagogue expert. " | |
| "Réponds dans la même langue que le concept demandé." | |
| ) | |
| user = ( | |
| f"Explique le concept suivant {level_desc}.\n\n" | |
| "Structure ta réponse avec :\n" | |
| "1. Définition courte et claire\n" | |
| "2. Points clés à retenir\n" | |
| "3. Exemple concret\n" | |
| "4. Applications pratiques\n\n" | |
| f"Concept : {concept}" | |
| ) | |
| explanation = _call_hf(system, user, max_tokens=1024, temperature=0.5) | |
| return {"explanation": explanation, "concept": concept, "level": level} | |
| def _resume(data: dict) -> dict: | |
| text = data.get("text", "") | |
| if not text: | |
| return {"summary": "Aucun texte fourni."} | |
| system = ( | |
| "Tu es un assistant pédagogique expert en synthèse de documents. " | |
| "Réponds dans la même langue que le texte fourni." | |
| ) | |
| user = ( | |
| "Résume le texte suivant de façon claire et structurée.\n" | |
| "Utilise des titres et des points clés.\n\n" | |
| f"Texte :\n{text[:3000]}" | |
| ) | |
| summary = _call_hf(system, user, max_tokens=1024, temperature=0.4) | |
| return {"summary": summary} | |
| def _rag_qa(data: dict) -> dict: | |
| query = data.get("query", "") | |
| try: | |
| from app.rag import query_documents | |
| results = query_documents(query, n_results=4) | |
| documents = results.get("documents", [[]])[0] | |
| metadatas = results.get("metadatas", [[]])[0] | |
| distances = results.get("distances", [[]])[0] | |
| THRESHOLD = 0.8 | |
| relevant = [ | |
| (doc, meta) | |
| for doc, meta, dist in zip(documents, metadatas, distances) | |
| if dist < THRESHOLD | |
| ] | |
| if not relevant: | |
| return { | |
| "answer": "Aucune information pertinente trouvée dans vos documents.", | |
| "sources": [], | |
| } | |
| context = "\n\n---\n\n".join([doc for doc, _ in relevant]) | |
| sources = list(set([meta.get("source", "inconnu") for _, meta in relevant])) | |
| system = ( | |
| "Tu es un assistant pédagogique RAG. " | |
| "Réponds à la question en te basant UNIQUEMENT sur le contexte fourni. " | |
| "Si la réponse n'est pas dans le contexte, dis-le clairement. " | |
| "Réponds dans la même langue que la question." | |
| ) | |
| user = f"Contexte :\n{context[:3000]}\n\nQuestion : {query}" | |
| answer = _call_hf(system, user, max_tokens=1024, temperature=0.4) | |
| return {"answer": answer, "sources": sources} | |
| except Exception as e: | |
| return {"answer": f"Erreur RAG : {str(e)}", "sources": []} |