|
@@ -0,0 +1,1210 @@
|
|
|
|
|
+import logging
|
|
|
|
|
+import json
|
|
|
|
|
+import os
|
|
|
|
|
+import tempfile
|
|
|
|
|
+import subprocess
|
|
|
|
|
+import base64
|
|
|
|
|
+import httpx
|
|
|
|
|
+import whisper
|
|
|
|
|
+import psutil
|
|
|
|
|
+import feedparser
|
|
|
|
|
+from datetime import datetime, timedelta
|
|
|
|
|
+from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
|
|
|
|
+from duckduckgo_search import DDGS
|
|
|
|
|
+from telegram import Update, Bot
|
|
|
|
|
+from telegram.ext import ApplicationBuilder, CommandHandler, MessageHandler, filters, ContextTypes
|
|
|
|
|
+from google.oauth2.credentials import Credentials
|
|
|
|
|
+from google_auth_oauthlib.flow import Flow
|
|
|
|
|
+from googleapiclient.discovery import build
|
|
|
|
|
+from google.auth.transport.requests import Request
|
|
|
|
|
+from aiohttp import web
|
|
|
|
|
+
|
|
|
|
|
+TELEGRAM_TOKEN = os.environ.get("TELEGRAM_TOKEN", "")
|
|
|
|
|
+if not TELEGRAM_TOKEN:
|
|
|
|
|
+ raise RuntimeError("TELEGRAM_TOKEN non defini. Ajoute-le dans .env ou en variable d'environnement.")
|
|
|
|
|
+OLLAMA_URL = os.environ.get("OLLAMA_URL", "http://localhost:11434/api/chat")
|
|
|
|
|
+OLLAMA_GENERATE_URL = os.environ.get("OLLAMA_GENERATE_URL", "http://localhost:11434/api/generate")
|
|
|
|
|
+MEMORY_DIR = os.path.expanduser("~/agent/memory")
|
|
|
|
|
+REPOS_DIR = os.path.expanduser("~/agent/repos")
|
|
|
|
|
+VOICE_MODEL = os.path.expanduser("~/agent/voices/fr_FR-upmc-medium.onnx")
|
|
|
|
|
+PIPER_BIN = os.path.expanduser("~/agent/venv/bin/piper")
|
|
|
|
|
+REMINDERS_FILE = os.path.join(os.path.expanduser("~/agent"), "reminders.json")
|
|
|
|
|
+DEFAULT_CITY = "Nantes"
|
|
|
|
|
+
|
|
|
|
|
+NEWS_FEEDS = {
|
|
|
|
|
+ "tech": [
|
|
|
|
|
+ "https://www.01net.com/rss/",
|
|
|
|
|
+ "https://www.lemonde.fr/pixels/rss_full.xml",
|
|
|
|
|
+ "https://www.zdnet.fr/feeds/rss/actualites/",
|
|
|
|
|
+ ],
|
|
|
|
|
+ "politique": [
|
|
|
|
|
+ "https://www.lemonde.fr/politique/rss_full.xml",
|
|
|
|
|
+ "https://www.rfi.fr/fr/rss/politique-fr/",
|
|
|
|
|
+ "https://www.francetvinfo.fr/politique.rss",
|
|
|
|
|
+ ],
|
|
|
|
|
+ "football": [
|
|
|
|
|
+ "https://www.lequipe.fr/rss/actu_rss_Football.xml",
|
|
|
|
|
+ "https://www.bbc.co.uk/sport/football/european/rss.xml",
|
|
|
|
|
+ ]
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+logging.basicConfig(level=logging.INFO)
|
|
|
|
|
+logger = logging.getLogger(__name__)
|
|
|
|
|
+os.makedirs(MEMORY_DIR, exist_ok=True)
|
|
|
|
|
+os.makedirs(REPOS_DIR, exist_ok=True)
|
|
|
|
|
+
|
|
|
|
|
+scheduler = AsyncIOScheduler()
|
|
|
|
|
+
|
|
|
|
|
+# ─── Agents ───────────────────────────────────────────────────────────────────
|
|
|
|
|
+
|
|
|
|
|
+AGENTS = {
|
|
|
|
|
+ "konan": {
|
|
|
|
|
+ "model": "qwen2.5:3b",
|
|
|
|
|
+ "prompt": (
|
|
|
|
|
+ "Tu es Konan, orchestrateur d'agents IA et développeur senior. "
|
|
|
|
|
+ "Tu adaptes ton ton naturellement selon le contexte : "
|
|
|
|
|
+ "décontracté et amical pour une conversation, précis et direct pour du technique, "
|
|
|
|
|
+ "structuré et rigoureux pour de l'architecture, encourageant pour du brainstorming. "
|
|
|
|
|
+ "Tu parles toujours en français. Tu ne te présentes pas à chaque message. "
|
|
|
|
|
+ "Quand une demande nécessite un expert spécialisé, tu le dis et tu délègues. "
|
|
|
|
|
+ "Tu es concis : pas de blabla inutile, tu vas droit au but."
|
|
|
|
|
+ )
|
|
|
|
|
+ },
|
|
|
|
|
+ "coder": {
|
|
|
|
|
+ "model": "qwen2.5-coder:3b",
|
|
|
|
|
+ "prompt": (
|
|
|
|
|
+ "Tu es l'agent Coder de Konan, expert en développement logiciel. "
|
|
|
|
|
+ "Tu écris du code propre, lisible et bien commenté. "
|
|
|
|
|
+ "Tu expliques tes choix techniques brièvement. "
|
|
|
|
|
+ "Tu signales les bugs potentiels et proposes des améliorations. "
|
|
|
|
|
+ "Tu réponds en français, avec le code dans des blocs formatés."
|
|
|
|
|
+ )
|
|
|
|
|
+ },
|
|
|
|
|
+ "reviewer": {
|
|
|
|
|
+ "model": "qwen2.5-coder:3b",
|
|
|
|
|
+ "prompt": (
|
|
|
|
|
+ "Tu es l'agent Reviewer de Konan, expert en revue de code. "
|
|
|
|
|
+ "Tu analyses le code de façon critique et constructive. "
|
|
|
|
|
+ "Tu identifies : bugs, failles de sécurité, mauvaises pratiques, performance. "
|
|
|
|
|
+ "Tu proposes des corrections concrètes avec des exemples. "
|
|
|
|
|
+ "Sois direct et précis. Tu réponds en français."
|
|
|
|
|
+ )
|
|
|
|
|
+ },
|
|
|
|
|
+ "architect": {
|
|
|
|
|
+ "model": "qwen2.5:3b",
|
|
|
|
|
+ "prompt": (
|
|
|
|
|
+ "Tu es l'agent Architect de Konan, expert en architecture logicielle et systèmes IA. "
|
|
|
|
|
+ "Tu penses en termes de scalabilité, maintenabilité et simplicité. "
|
|
|
|
|
+ "Tu proposes des architectures claires avec des schémas textuels si nécessaire. "
|
|
|
|
|
+ "Tu justifies tes choix et présentes les alternatives. "
|
|
|
|
|
+ "Tu réponds en français de façon structurée."
|
|
|
|
|
+ )
|
|
|
|
|
+ },
|
|
|
|
|
+ "analyst": {
|
|
|
|
|
+ "model": "qwen2.5:3b",
|
|
|
|
|
+ "prompt": (
|
|
|
|
|
+ "Tu es l'agent Analyst de Konan, expert en analyse et documentation. "
|
|
|
|
|
+ "Tu synthétises l'information de façon claire et structurée. "
|
|
|
|
|
+ "Tu produis des analyses, des résumés, de la documentation technique. "
|
|
|
|
|
+ "Tu es méthodique et exhaustif sans être verbeux. Tu réponds en français."
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+AGENT_EMOJIS = {"konan": "🧠", "coder": "💻", "reviewer": "🔍", "architect": "🏗️", "analyst": "📊"}
|
|
|
|
|
+
|
|
|
|
|
+# ─── Profil ───────────────────────────────────────────────────────────────────
|
|
|
|
|
+
|
|
|
|
|
+def profile_path(user_id):
|
|
|
|
|
+ return os.path.join(MEMORY_DIR, f"{user_id}_profile.json")
|
|
|
|
|
+
|
|
|
|
|
+def load_profile(user_id):
|
|
|
|
|
+ path = profile_path(user_id)
|
|
|
|
|
+ if os.path.exists(path):
|
|
|
|
|
+ with open(path, "r") as f:
|
|
|
|
|
+ return json.load(f)
|
|
|
|
|
+ return {"nom": None, "projets": [], "preferences": ""}
|
|
|
|
|
+
|
|
|
|
|
+def save_profile(user_id, profile):
|
|
|
|
|
+ with open(profile_path(user_id), "w") as f:
|
|
|
|
|
+ json.dump(profile, f, ensure_ascii=False, indent=2)
|
|
|
|
|
+
|
|
|
|
|
+# ─── Mémoire ──────────────────────────────────────────────────────────────────
|
|
|
|
|
+
|
|
|
|
|
+def memory_path(user_id, agent="konan"):
|
|
|
|
|
+ return os.path.join(MEMORY_DIR, f"{user_id}_{agent}.json")
|
|
|
|
|
+
|
|
|
|
|
+def build_system_prompt(user_id, agent="konan"):
|
|
|
|
|
+ profile = load_profile(user_id)
|
|
|
|
|
+ nom = profile.get("nom")
|
|
|
|
|
+ projets = profile.get("projets", [])
|
|
|
|
|
+ base = AGENTS[agent]["prompt"]
|
|
|
|
|
+ if nom:
|
|
|
|
|
+ base += f" Tu parles à {nom}."
|
|
|
|
|
+ if projets:
|
|
|
|
|
+ base += f" Ses projets : {', '.join(projets)}."
|
|
|
|
|
+ return base
|
|
|
|
|
+
|
|
|
|
|
+def load_conversation(user_id, agent="konan"):
|
|
|
|
|
+ path = memory_path(user_id, agent)
|
|
|
|
|
+ system = build_system_prompt(user_id, agent)
|
|
|
|
|
+ if os.path.exists(path):
|
|
|
|
|
+ msgs = json.load(open(path))
|
|
|
|
|
+ msgs[0] = {"role": "system", "content": system}
|
|
|
|
|
+ return msgs
|
|
|
|
|
+ return [{"role": "system", "content": system}]
|
|
|
|
|
+
|
|
|
|
|
+def save_conversation(user_id, messages, agent="konan"):
|
|
|
|
|
+ with open(memory_path(user_id, agent), "w") as f:
|
|
|
|
|
+ json.dump(messages, f, ensure_ascii=False, indent=2)
|
|
|
|
|
+
|
|
|
|
|
+# ─── Monitoring ───────────────────────────────────────────────────────────────
|
|
|
|
|
+
|
|
|
|
|
+def get_server_status():
|
|
|
|
|
+ cpu = psutil.cpu_percent(interval=1)
|
|
|
|
|
+ ram = psutil.virtual_memory()
|
|
|
|
|
+ disk = psutil.disk_usage("/")
|
|
|
|
|
+ temps = psutil.sensors_temperatures() if hasattr(psutil, "sensors_temperatures") else {}
|
|
|
|
|
+
|
|
|
|
|
+ ram_used = ram.used / (1024**3)
|
|
|
|
|
+ ram_total = ram.total / (1024**3)
|
|
|
|
|
+ disk_used = disk.used / (1024**3)
|
|
|
|
|
+ disk_total = disk.total / (1024**3)
|
|
|
|
|
+
|
|
|
|
|
+ cpu_emoji = "🔴" if cpu > 80 else "🟡" if cpu > 50 else "🟢"
|
|
|
|
|
+ ram_emoji = "🔴" if ram.percent > 80 else "🟡" if ram.percent > 60 else "🟢"
|
|
|
|
|
+ disk_emoji = "🔴" if disk.percent > 90 else "🟡" if disk.percent > 75 else "🟢"
|
|
|
|
|
+
|
|
|
|
|
+ msg = f"📊 *État du serveur*\n\n"
|
|
|
|
|
+ msg += f"{cpu_emoji} CPU : {cpu:.1f}%\n"
|
|
|
|
|
+ msg += f"{ram_emoji} RAM : {ram_used:.1f}/{ram_total:.1f} GB ({ram.percent}%)\n"
|
|
|
|
|
+ msg += f"{disk_emoji} Disque : {disk_used:.1f}/{disk_total:.1f} GB ({disk.percent}%)\n"
|
|
|
|
|
+
|
|
|
|
|
+ # Processus les plus gourmands
|
|
|
|
|
+ procs = sorted(psutil.process_iter(["pid", "name", "cpu_percent", "memory_percent"]),
|
|
|
|
|
+ key=lambda p: p.info["cpu_percent"] or 0, reverse=True)[:3]
|
|
|
|
|
+ if procs:
|
|
|
|
|
+ msg += "\n🔝 *Top processus CPU :*\n"
|
|
|
|
|
+ for p in procs:
|
|
|
|
|
+ msg += f" {p.info['name']} — CPU {p.info['cpu_percent']:.1f}% RAM {p.info['memory_percent']:.1f}%\n"
|
|
|
|
|
+
|
|
|
|
|
+ # Ollama actif ?
|
|
|
|
|
+ ollama_running = any("ollama" in p.info["name"].lower() for p in psutil.process_iter(["name"]))
|
|
|
|
|
+ msg += f"\n🤖 Ollama : {'✅ actif' if ollama_running else '❌ inactif'}\n"
|
|
|
|
|
+ msg += f"🕐 Uptime : {datetime.now().strftime('%H:%M:%S')}"
|
|
|
|
|
+
|
|
|
|
|
+ return msg
|
|
|
|
|
+
|
|
|
|
|
+# ─── Rappels ──────────────────────────────────────────────────────────────────
|
|
|
|
|
+
|
|
|
|
|
+def load_reminders():
|
|
|
|
|
+ if os.path.exists(REMINDERS_FILE):
|
|
|
|
|
+ with open(REMINDERS_FILE, "r") as f:
|
|
|
|
|
+ return json.load(f)
|
|
|
|
|
+ return []
|
|
|
|
|
+
|
|
|
|
|
+def save_reminders(reminders):
|
|
|
|
|
+ with open(REMINDERS_FILE, "w") as f:
|
|
|
|
|
+ json.dump(reminders, f, ensure_ascii=False, indent=2)
|
|
|
|
|
+
|
|
|
|
|
+async def send_reminder(bot: Bot, user_id: int, message: str, reminder_id: str):
|
|
|
|
|
+ try:
|
|
|
|
|
+ await bot.send_message(chat_id=user_id, text=f"⏰ Rappel : {message}")
|
|
|
|
|
+ # Supprimer le rappel après envoi
|
|
|
|
|
+ reminders = load_reminders()
|
|
|
|
|
+ reminders = [r for r in reminders if r["id"] != reminder_id]
|
|
|
|
|
+ save_reminders(reminders)
|
|
|
|
|
+ except Exception as e:
|
|
|
|
|
+ logger.error(f"Erreur rappel : {e}")
|
|
|
|
|
+
|
|
|
|
|
+def parse_time(time_str: str):
|
|
|
|
|
+ """Parse '30m', '2h', '10:30' ou 'demain 9h'"""
|
|
|
|
|
+ now = datetime.now()
|
|
|
|
|
+ time_str = time_str.strip().lower()
|
|
|
|
|
+
|
|
|
|
|
+ if time_str.endswith("m"):
|
|
|
|
|
+ minutes = int(time_str[:-1])
|
|
|
|
|
+ return now + timedelta(minutes=minutes)
|
|
|
|
|
+ elif time_str.endswith("h") and ":" not in time_str:
|
|
|
|
|
+ hours = int(time_str[:-1])
|
|
|
|
|
+ return now + timedelta(hours=hours)
|
|
|
|
|
+ elif ":" in time_str:
|
|
|
|
|
+ h, m = time_str.split(":")
|
|
|
|
|
+ target = now.replace(hour=int(h), minute=int(m), second=0)
|
|
|
|
|
+ if target < now:
|
|
|
|
|
+ target += timedelta(days=1)
|
|
|
|
|
+ return target
|
|
|
|
|
+ return None
|
|
|
|
|
+
|
|
|
|
|
+# ─── Détection d'intention ────────────────────────────────────────────────────
|
|
|
|
|
+
|
|
|
|
|
+async def detect_agent(message: str) -> str:
|
|
|
|
|
+ prompt = (
|
|
|
|
|
+ "Analyse cette demande et réponds UNIQUEMENT par un de ces mots : "
|
|
|
|
|
+ "konan, coder, reviewer, architect, analyst.\n\n"
|
|
|
|
|
+ "- konan : conversation, question générale\n"
|
|
|
|
|
+ "- coder : écrire du code, corriger un bug\n"
|
|
|
|
|
+ "- reviewer : relire du code, code review\n"
|
|
|
|
|
+ "- architect : architecture, conception, design\n"
|
|
|
|
|
+ "- analyst : analyser, documenter, résumer\n\n"
|
|
|
|
|
+ f"Demande : {message}\n\nRéponds avec un seul mot."
|
|
|
|
|
+ )
|
|
|
|
|
+ try:
|
|
|
|
|
+ async with httpx.AsyncClient(timeout=30) as client:
|
|
|
|
|
+ response = await client.post(OLLAMA_URL, json={
|
|
|
|
|
+ "model": "qwen2.5:3b",
|
|
|
|
|
+ "messages": [{"role": "user", "content": prompt}],
|
|
|
|
|
+ "stream": False,
|
|
|
|
|
+ "options": {"num_predict": 5}
|
|
|
|
|
+ })
|
|
|
|
|
+ result = response.json()["message"]["content"].strip().lower()
|
|
|
|
|
+ for agent in AGENTS:
|
|
|
|
|
+ if agent in result:
|
|
|
|
|
+ return agent
|
|
|
|
|
+ except Exception:
|
|
|
|
|
+ pass
|
|
|
|
|
+ return "konan"
|
|
|
|
|
+
|
|
|
|
|
+# ─── Ollama ───────────────────────────────────────────────────────────────────
|
|
|
|
|
+
|
|
|
|
|
+async def ask_agent(user_id, message, agent="konan"):
|
|
|
|
|
+ messages = load_conversation(user_id, agent)
|
|
|
|
|
+ messages.append({"role": "user", "content": message})
|
|
|
|
|
+ if len(messages) > 21:
|
|
|
|
|
+ messages = [messages[0]] + messages[-20:]
|
|
|
|
|
+ try:
|
|
|
|
|
+ async with httpx.AsyncClient(timeout=180) as client:
|
|
|
|
|
+ response = await client.post(OLLAMA_URL, json={
|
|
|
|
|
+ "model": AGENTS[agent]["model"],
|
|
|
|
|
+ "messages": messages,
|
|
|
|
|
+ "stream": False
|
|
|
|
|
+ })
|
|
|
|
|
+ data = response.json()
|
|
|
|
|
+ reply = data["message"]["content"]
|
|
|
|
|
+ messages.append({"role": "assistant", "content": reply})
|
|
|
|
|
+ save_conversation(user_id, messages, agent)
|
|
|
|
|
+ return reply, agent
|
|
|
|
|
+ except Exception as e:
|
|
|
|
|
+ return f"Erreur : {e}", agent
|
|
|
|
|
+
|
|
|
|
|
+# ─── Vision ───────────────────────────────────────────────────────────────────
|
|
|
|
|
+
|
|
|
|
|
+async def analyze_image(image_path: str, question: str = "Décris cette image en détail en français.") -> str:
|
|
|
|
|
+ with open(image_path, "rb") as f:
|
|
|
|
|
+ image_b64 = base64.b64encode(f.read()).decode("utf-8")
|
|
|
|
|
+ try:
|
|
|
|
|
+ async with httpx.AsyncClient(timeout=180) as client:
|
|
|
|
|
+ response = await client.post(OLLAMA_GENERATE_URL, json={
|
|
|
|
|
+ "model": "moondream",
|
|
|
|
|
+ "prompt": question,
|
|
|
|
|
+ "images": [image_b64],
|
|
|
|
|
+ "stream": False
|
|
|
|
|
+ })
|
|
|
|
|
+ return response.json().get("response", "Impossible d'analyser l'image.")
|
|
|
|
|
+ except Exception as e:
|
|
|
|
|
+ return f"Erreur analyse image : {e}"
|
|
|
|
|
+
|
|
|
|
|
+# ─── TTS / STT ────────────────────────────────────────────────────────────────
|
|
|
|
|
+
|
|
|
|
|
+logger.info("Chargement Whisper...")
|
|
|
|
|
+whisper_model = whisper.load_model("tiny")
|
|
|
|
|
+logger.info("Whisper pret.")
|
|
|
|
|
+
|
|
|
|
|
+def text_to_audio(text):
|
|
|
|
|
+ tmp = tempfile.NamedTemporaryFile(suffix=".wav", delete=False)
|
|
|
|
|
+ tmp.close()
|
|
|
|
|
+ subprocess.run(
|
|
|
|
|
+ [PIPER_BIN, "--model", VOICE_MODEL, "--output_file", tmp.name],
|
|
|
|
|
+ input=text.encode("utf-8"), check=True
|
|
|
|
|
+ )
|
|
|
|
|
+ return tmp.name
|
|
|
|
|
+
|
|
|
|
|
+async def transcribe_voice(file_path):
|
|
|
|
|
+ result = whisper_model.transcribe(file_path, language="fr")
|
|
|
|
|
+ return result["text"].strip()
|
|
|
|
|
+
|
|
|
|
|
+# ─── Git ──────────────────────────────────────────────────────────────────────
|
|
|
|
|
+
|
|
|
|
|
+def list_repos():
|
|
|
|
|
+ if not os.path.exists(REPOS_DIR):
|
|
|
|
|
+ return []
|
|
|
|
|
+ return [d for d in os.listdir(REPOS_DIR) if os.path.isdir(os.path.join(REPOS_DIR, d, ".git"))]
|
|
|
|
|
+
|
|
|
|
|
+def list_files(repo):
|
|
|
|
|
+ repo_path = os.path.join(REPOS_DIR, repo)
|
|
|
|
|
+ if not os.path.exists(repo_path):
|
|
|
|
|
+ return None
|
|
|
|
|
+ result = subprocess.run(["git", "ls-files"], cwd=repo_path, capture_output=True, text=True)
|
|
|
|
|
+ return [f for f in result.stdout.strip().split("\n") if f]
|
|
|
|
|
+
|
|
|
|
|
+def read_file(repo, filepath):
|
|
|
|
|
+ full_path = os.path.join(REPOS_DIR, repo, filepath)
|
|
|
|
|
+ if not os.path.exists(full_path):
|
|
|
|
|
+ return None
|
|
|
|
|
+ with open(full_path, "r", errors="replace") as f:
|
|
|
|
|
+ return f.read()
|
|
|
|
|
+
|
|
|
|
|
+def git_log(repo, n=5):
|
|
|
|
|
+ result = subprocess.run(["git", "log", f"-{n}", "--oneline"],
|
|
|
|
|
+ cwd=os.path.join(REPOS_DIR, repo), capture_output=True, text=True)
|
|
|
|
|
+ return result.stdout.strip()
|
|
|
|
|
+
|
|
|
|
|
+def git_pull(repo):
|
|
|
|
|
+ result = subprocess.run(["git", "pull"],
|
|
|
|
|
+ cwd=os.path.join(REPOS_DIR, repo), capture_output=True, text=True)
|
|
|
|
|
+ return result.stdout.strip() or result.stderr.strip()
|
|
|
|
|
+
|
|
|
|
|
+# ─── Google Auth ──────────────────────────────────────────────────────────────
|
|
|
|
|
+
|
|
|
|
|
+def get_google_creds():
|
|
|
|
|
+ creds = None
|
|
|
|
|
+ if os.path.exists(GOOGLE_TOKEN_FILE):
|
|
|
|
|
+ creds = Credentials.from_authorized_user_file(GOOGLE_TOKEN_FILE, GOOGLE_SCOPES)
|
|
|
|
|
+ if creds and creds.expired and creds.refresh_token:
|
|
|
|
|
+ creds.refresh(Request())
|
|
|
|
|
+ with open(GOOGLE_TOKEN_FILE, "w") as f:
|
|
|
|
|
+ f.write(creds.to_json())
|
|
|
|
|
+ return creds if (creds and creds.valid) else None
|
|
|
|
|
+
|
|
|
|
|
+# ─── Google Calendar ──────────────────────────────────────────────────────────
|
|
|
|
|
+
|
|
|
|
|
+def get_calendar_events(max_results=10):
|
|
|
|
|
+ creds = get_google_creds()
|
|
|
|
|
+ if not creds:
|
|
|
|
|
+ return None
|
|
|
|
|
+ service = build("calendar", "v3", credentials=creds)
|
|
|
|
|
+ now = datetime.utcnow().isoformat() + "Z"
|
|
|
|
|
+ events_result = service.events().list(
|
|
|
|
|
+ calendarId="primary",
|
|
|
|
|
+ timeMin=now,
|
|
|
|
|
+ maxResults=max_results,
|
|
|
|
|
+ singleEvents=True,
|
|
|
|
|
+ orderBy="startTime"
|
|
|
|
|
+ ).execute()
|
|
|
|
|
+ return events_result.get("items", [])
|
|
|
|
|
+
|
|
|
|
|
+# ─── Gmail ────────────────────────────────────────────────────────────────────
|
|
|
|
|
+
|
|
|
|
|
+def get_gmail_messages(max_results=5):
|
|
|
|
|
+ creds = get_google_creds()
|
|
|
|
|
+ if not creds:
|
|
|
|
|
+ return None
|
|
|
|
|
+ service = build("gmail", "v1", credentials=creds)
|
|
|
|
|
+ result = service.users().messages().list(
|
|
|
|
|
+ userId="me", maxResults=max_results, q="is:unread"
|
|
|
|
|
+ ).execute()
|
|
|
|
|
+ messages = result.get("messages", [])
|
|
|
|
|
+ emails = []
|
|
|
|
|
+ for msg in messages:
|
|
|
|
|
+ detail = service.users().messages().get(
|
|
|
|
|
+ userId="me", id=msg["id"], format="metadata",
|
|
|
|
|
+ metadataHeaders=["From", "Subject", "Date"]
|
|
|
|
|
+ ).execute()
|
|
|
|
|
+ headers = {h["name"]: h["value"] for h in detail["payload"]["headers"]}
|
|
|
|
|
+ emails.append({
|
|
|
|
|
+ "from": headers.get("From", ""),
|
|
|
|
|
+ "subject": headers.get("Subject", ""),
|
|
|
|
|
+ "date": headers.get("Date", "")
|
|
|
|
|
+ })
|
|
|
|
|
+ return emails
|
|
|
|
|
+
|
|
|
|
|
+# ─── Notes ────────────────────────────────────────────────────────────────────
|
|
|
|
|
+
|
|
|
|
|
+def notes_path(user_id):
|
|
|
|
|
+ return os.path.join(MEMORY_DIR, f"{user_id}_notes.json")
|
|
|
|
|
+
|
|
|
|
|
+def load_notes(user_id):
|
|
|
|
|
+ path = notes_path(user_id)
|
|
|
|
|
+ if os.path.exists(path):
|
|
|
|
|
+ with open(path, "r") as f:
|
|
|
|
|
+ return json.load(f)
|
|
|
|
|
+ return []
|
|
|
|
|
+
|
|
|
|
|
+def save_notes(user_id, notes):
|
|
|
|
|
+ with open(notes_path(user_id), "w") as f:
|
|
|
|
|
+ json.dump(notes, f, ensure_ascii=False, indent=2)
|
|
|
|
|
+
|
|
|
|
|
+def add_note(user_id, content):
|
|
|
|
|
+ notes = load_notes(user_id)
|
|
|
|
|
+ note = {
|
|
|
|
|
+ "id": len(notes) + 1,
|
|
|
|
|
+ "content": content,
|
|
|
|
|
+ "date": datetime.now().strftime("%Y-%m-%d %H:%M")
|
|
|
|
|
+ }
|
|
|
|
|
+ notes.append(note)
|
|
|
|
|
+ save_notes(user_id, notes)
|
|
|
|
|
+ return note
|
|
|
|
|
+
|
|
|
|
|
+def search_notes(user_id, query):
|
|
|
|
|
+ notes = load_notes(user_id)
|
|
|
|
|
+ query_lower = query.lower()
|
|
|
|
|
+ return [n for n in notes if query_lower in n["content"].lower()]
|
|
|
|
|
+
|
|
|
|
|
+def search_memory(user_id, query):
|
|
|
|
|
+ results = []
|
|
|
|
|
+ query_lower = query.lower()
|
|
|
|
|
+ # Chercher dans toutes les conversations
|
|
|
|
|
+ for agent in AGENTS:
|
|
|
|
|
+ path = memory_path(user_id, agent)
|
|
|
|
|
+ if not os.path.exists(path):
|
|
|
|
|
+ continue
|
|
|
|
|
+ msgs = json.load(open(path))
|
|
|
|
|
+ for msg in msgs:
|
|
|
|
|
+ if msg["role"] in ("user", "assistant") and query_lower in msg["content"].lower():
|
|
|
|
|
+ results.append({
|
|
|
|
|
+ "agent": agent,
|
|
|
|
|
+ "role": msg["role"],
|
|
|
|
|
+ "content": msg["content"][:300]
|
|
|
|
|
+ })
|
|
|
|
|
+ # Chercher dans les notes
|
|
|
|
|
+ notes = search_notes(user_id, query)
|
|
|
|
|
+ return results, notes
|
|
|
|
|
+
|
|
|
|
|
+# ─── Météo ────────────────────────────────────────────────────────────────────
|
|
|
|
|
+
|
|
|
|
|
+async def get_weather(city: str) -> str:
|
|
|
|
|
+ try:
|
|
|
|
|
+ async with httpx.AsyncClient(timeout=10) as client:
|
|
|
|
|
+ response = await client.get(f"https://wttr.in/{city}?format=3&lang=fr")
|
|
|
|
|
+ return f"🌤️ {response.text.strip()}"
|
|
|
|
|
+ except Exception as e:
|
|
|
|
|
+ return f"Impossible de récupérer la météo : {e}"
|
|
|
|
|
+
|
|
|
|
|
+# ─── Recherche web ────────────────────────────────────────────────────────────
|
|
|
|
|
+
|
|
|
|
|
+def web_search(query: str, max_results: int = 5) -> str:
|
|
|
|
|
+ try:
|
|
|
|
|
+ with DDGS() as ddgs:
|
|
|
|
|
+ results = list(ddgs.text(query, region="fr-fr", max_results=max_results))
|
|
|
|
|
+ if not results:
|
|
|
|
|
+ return "Aucun résultat trouvé."
|
|
|
|
|
+ msg = ""
|
|
|
|
|
+ for r in results:
|
|
|
|
|
+ msg += f"• *{r.get('title','')}*\n{r.get('body','')[:200]}\n\n"
|
|
|
|
|
+ return msg.strip()
|
|
|
|
|
+ except Exception as e:
|
|
|
|
|
+ return f"Erreur recherche : {e}"
|
|
|
|
|
+
|
|
|
|
|
+# ─── Actualités ───────────────────────────────────────────────────────────────
|
|
|
|
|
+
|
|
|
|
|
+def get_news(category: str = "tech", max_items: int = 5) -> str:
|
|
|
|
|
+ feeds = NEWS_FEEDS.get(category, NEWS_FEEDS["tech"])
|
|
|
|
|
+ items = []
|
|
|
|
|
+ for url in feeds:
|
|
|
|
|
+ try:
|
|
|
|
|
+ feed = feedparser.parse(url)
|
|
|
|
|
+ for entry in feed.entries[:3]:
|
|
|
|
|
+ items.append({
|
|
|
|
|
+ "title": entry.get("title", ""),
|
|
|
|
|
+ "summary": entry.get("summary", "")[:200]
|
|
|
|
|
+ })
|
|
|
|
|
+ except Exception:
|
|
|
|
|
+ continue
|
|
|
|
|
+ if not items:
|
|
|
|
|
+ return "Impossible de récupérer les actualités."
|
|
|
|
|
+ items = items[:max_items]
|
|
|
|
|
+ msg = f"📰 *Actualités {category}*\n\n"
|
|
|
|
|
+ for item in items:
|
|
|
|
|
+ msg += f"• *{item['title']}*\n{item['summary']}\n\n"
|
|
|
|
|
+ return msg.strip()
|
|
|
|
|
+
|
|
|
|
|
+# ─── Handlers ─────────────────────────────────────────────────────────────────
|
|
|
|
|
+
|
|
|
|
|
+async def start(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
|
|
|
+ user_id = update.effective_user.id
|
|
|
|
|
+ profile = load_profile(user_id)
|
|
|
|
|
+ nom = profile.get("nom")
|
|
|
|
|
+ if nom:
|
|
|
|
|
+ msg = f"Content de te retrouver {nom} ! Je suis Konan."
|
|
|
|
|
+ else:
|
|
|
|
|
+ msg = (
|
|
|
|
|
+ "Bonjour ! Je suis Konan, ton orchestrateur d'agents IA.\n\n"
|
|
|
|
|
+ "Agents : 💻 Coder · 🔍 Reviewer · 🏗️ Architect · 📊 Analyst\n"
|
|
|
|
|
+ "Tu peux m'envoyer du texte, de la voix, des images ou ta localisation.\n\n"
|
|
|
|
|
+ "/setnom Prenom — te présenter\n"
|
|
|
|
|
+ "/monitoring — état du serveur\n"
|
|
|
|
|
+ "/meteo [ville] — météo (ou partage ta position)\n"
|
|
|
|
|
+ "/news [tech|politique|football] — actualités\n"
|
|
|
|
|
+ "/recherche <query> — recherche web\n"
|
|
|
|
|
+ "/note <contenu> — sauvegarder une note\n"
|
|
|
|
|
+ "/notes — voir tes notes\n"
|
|
|
|
|
+ "/chercher <mot> — chercher dans la mémoire\n"
|
|
|
|
|
+ "/briefing 08:00 — briefing matinal automatique\n"
|
|
|
|
|
+ "/rappel 30m Message — créer un rappel\n"
|
|
|
|
|
+ "/repos — projets Git\n"
|
|
|
|
|
+ "/agents — voir les agents\n"
|
|
|
|
|
+ "/reset — réinitialiser"
|
|
|
|
|
+ )
|
|
|
|
|
+ await update.message.reply_text(msg)
|
|
|
|
|
+
|
|
|
|
|
+async def monitoring_cmd(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
|
|
|
+ status = get_server_status()
|
|
|
|
|
+ await update.message.reply_text(status, parse_mode="Markdown")
|
|
|
|
|
+
|
|
|
|
|
+async def rappel_cmd(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
|
|
|
+ user_id = update.effective_user.id
|
|
|
|
|
+ if len(context.args) < 2:
|
|
|
|
|
+ await update.message.reply_text(
|
|
|
|
|
+ "Usage : /rappel <temps> <message>\n\n"
|
|
|
|
|
+ "Exemples :\n"
|
|
|
|
|
+ "/rappel 30m Faire une pause\n"
|
|
|
|
|
+ "/rappel 2h Revoir l'architecture\n"
|
|
|
|
|
+ "/rappel 14:30 Réunion équipe"
|
|
|
|
|
+ )
|
|
|
|
|
+ return
|
|
|
|
|
+
|
|
|
|
|
+ time_str = context.args[0]
|
|
|
|
|
+ message = " ".join(context.args[1:])
|
|
|
|
|
+ target_time = parse_time(time_str)
|
|
|
|
|
+
|
|
|
|
|
+ if not target_time:
|
|
|
|
|
+ await update.message.reply_text("Format de temps non reconnu. Exemples : 30m, 2h, 14:30")
|
|
|
|
|
+ return
|
|
|
|
|
+
|
|
|
|
|
+ reminder_id = f"{user_id}_{int(target_time.timestamp())}"
|
|
|
|
|
+ bot = context.application.bot
|
|
|
|
|
+
|
|
|
|
|
+ scheduler.add_job(
|
|
|
|
|
+ send_reminder,
|
|
|
|
|
+ "date",
|
|
|
|
|
+ run_date=target_time,
|
|
|
|
|
+ args=[bot, user_id, message, reminder_id],
|
|
|
|
|
+ id=reminder_id
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ reminders = load_reminders()
|
|
|
|
|
+ reminders.append({
|
|
|
|
|
+ "id": reminder_id,
|
|
|
|
|
+ "user_id": user_id,
|
|
|
|
|
+ "message": message,
|
|
|
|
|
+ "time": target_time.isoformat()
|
|
|
|
|
+ })
|
|
|
|
|
+ save_reminders(reminders)
|
|
|
|
|
+
|
|
|
|
|
+ heure = target_time.strftime("%H:%M")
|
|
|
|
|
+ await update.message.reply_text(f"⏰ Rappel créé pour {heure} : \"{message}\"")
|
|
|
|
|
+
|
|
|
|
|
+async def rappels_cmd(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
|
|
|
+ user_id = update.effective_user.id
|
|
|
|
|
+ reminders = [r for r in load_reminders() if r["user_id"] == user_id]
|
|
|
|
|
+ if not reminders:
|
|
|
|
|
+ await update.message.reply_text("Aucun rappel en attente.")
|
|
|
|
|
+ return
|
|
|
|
|
+ msg = "⏰ Tes rappels :\n\n"
|
|
|
|
|
+ for r in reminders:
|
|
|
|
|
+ t = datetime.fromisoformat(r["time"]).strftime("%H:%M")
|
|
|
|
|
+ msg += f"• {t} — {r['message']}\n"
|
|
|
|
|
+ await update.message.reply_text(msg)
|
|
|
|
|
+
|
|
|
|
|
+async def agents_cmd(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
|
|
|
+ msg = "Agents disponibles :\n\n"
|
|
|
|
|
+ for name, cfg in AGENTS.items():
|
|
|
|
|
+ emoji = AGENT_EMOJIS.get(name, "🤖")
|
|
|
|
|
+ msg += f"{emoji} *{name.capitalize()}* — {cfg['model']}\n"
|
|
|
|
|
+ await update.message.reply_text(msg, parse_mode="Markdown")
|
|
|
|
|
+
|
|
|
|
|
+async def reset(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
|
|
|
+ user_id = update.effective_user.id
|
|
|
|
|
+ for agent in AGENTS:
|
|
|
|
|
+ path = memory_path(user_id, agent)
|
|
|
|
|
+ if os.path.exists(path):
|
|
|
|
|
+ os.remove(path)
|
|
|
|
|
+ await update.message.reply_text("Mémoire réinitialisée. Profil conservé.")
|
|
|
|
|
+
|
|
|
|
|
+async def set_nom(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
|
|
|
+ user_id = update.effective_user.id
|
|
|
|
|
+ if not context.args:
|
|
|
|
|
+ await update.message.reply_text("Usage : /setnom Prenom")
|
|
|
|
|
+ return
|
|
|
|
|
+ nom = " ".join(context.args)
|
|
|
|
|
+ profile = load_profile(user_id)
|
|
|
|
|
+ profile["nom"] = nom
|
|
|
|
|
+ save_profile(user_id, profile)
|
|
|
|
|
+ await update.message.reply_text(f"Parfait, je m'en souviendrai {nom} !")
|
|
|
|
|
+
|
|
|
|
|
+async def set_projet(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
|
|
|
+ user_id = update.effective_user.id
|
|
|
|
|
+ if not context.args:
|
|
|
|
|
+ await update.message.reply_text("Usage : /setprojet Nom")
|
|
|
|
|
+ return
|
|
|
|
|
+ projet = " ".join(context.args)
|
|
|
|
|
+ profile = load_profile(user_id)
|
|
|
|
|
+ if projet not in profile["projets"]:
|
|
|
|
|
+ profile["projets"].append(projet)
|
|
|
|
|
+ save_profile(user_id, profile)
|
|
|
|
|
+ await update.message.reply_text(f"Projet \"{projet}\" ajouté.")
|
|
|
|
|
+
|
|
|
|
|
+async def profil(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
|
|
|
+ user_id = update.effective_user.id
|
|
|
|
|
+ profile = load_profile(user_id)
|
|
|
|
|
+ nom = profile.get("nom") or "non défini"
|
|
|
|
|
+ projets = ", ".join(profile.get("projets", [])) or "aucun"
|
|
|
|
|
+ await update.message.reply_text(f"Nom : {nom}\nProjets : {projets}")
|
|
|
|
|
+
|
|
|
|
|
+async def repos_cmd(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
|
|
|
+ repos = list_repos()
|
|
|
|
|
+ if not repos:
|
|
|
|
|
+ await update.message.reply_text("Aucun repo cloné.\n/clone <url> pour en ajouter un.")
|
|
|
|
|
+ return
|
|
|
|
|
+ msg = "Repos disponibles :\n\n"
|
|
|
|
|
+ for r in repos:
|
|
|
|
|
+ msg += f"📁 {r}\n"
|
|
|
|
|
+ msg += "\n/fichiers <repo> · /log <repo> · /analyser <repo> <fichier>"
|
|
|
|
|
+ await update.message.reply_text(msg)
|
|
|
|
|
+
|
|
|
|
|
+async def clone_cmd(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
|
|
|
+ if not context.args:
|
|
|
|
|
+ await update.message.reply_text("Usage : /clone <url>")
|
|
|
|
|
+ return
|
|
|
|
|
+ url = context.args[0]
|
|
|
|
|
+ name = url.rstrip("/").split("/")[-1].replace(".git", "")
|
|
|
|
|
+ dest = os.path.join(REPOS_DIR, name)
|
|
|
|
|
+ await update.message.reply_text(f"Clonage de {name}...")
|
|
|
|
|
+ result = subprocess.run(["git", "clone", url, dest], capture_output=True, text=True)
|
|
|
|
|
+ if result.returncode == 0:
|
|
|
|
|
+ await update.message.reply_text(f"✅ {name} cloné.")
|
|
|
|
|
+ else:
|
|
|
|
|
+ await update.message.reply_text(f"Erreur : {result.stderr}")
|
|
|
|
|
+
|
|
|
|
|
+async def fichiers_cmd(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
|
|
|
+ if not context.args:
|
|
|
|
|
+ await update.message.reply_text("Usage : /fichiers <repo>")
|
|
|
|
|
+ return
|
|
|
|
|
+ repo = context.args[0]
|
|
|
|
|
+ files = list_files(repo)
|
|
|
|
|
+ if files is None:
|
|
|
|
|
+ await update.message.reply_text(f"Repo '{repo}' introuvable.")
|
|
|
|
|
+ return
|
|
|
|
|
+ msg = f"Fichiers de {repo} :\n\n" + "\n".join(files[:50])
|
|
|
|
|
+ if len(files) > 50:
|
|
|
|
|
+ msg += f"\n... et {len(files) - 50} autres"
|
|
|
|
|
+ await update.message.reply_text(msg)
|
|
|
|
|
+
|
|
|
|
|
+async def log_cmd(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
|
|
|
+ if not context.args:
|
|
|
|
|
+ await update.message.reply_text("Usage : /log <repo>")
|
|
|
|
|
+ return
|
|
|
|
|
+ repo = context.args[0]
|
|
|
|
|
+ log = git_log(repo)
|
|
|
|
|
+ await update.message.reply_text(f"Derniers commits de {repo} :\n\n{log}" if log else "Repo introuvable.")
|
|
|
|
|
+
|
|
|
|
|
+async def lire_cmd(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
|
|
|
+ if len(context.args) < 2:
|
|
|
|
|
+ await update.message.reply_text("Usage : /lire <repo> <fichier>")
|
|
|
|
|
+ return
|
|
|
|
|
+ repo = context.args[0]
|
|
|
|
|
+ filepath = " ".join(context.args[1:])
|
|
|
|
|
+ content = read_file(repo, filepath)
|
|
|
|
|
+ if content is None:
|
|
|
|
|
+ await update.message.reply_text(f"Fichier '{filepath}' introuvable.")
|
|
|
|
|
+ return
|
|
|
|
|
+ if len(content) > 3000:
|
|
|
|
|
+ content = content[:3000] + "\n... (tronqué)"
|
|
|
|
|
+ await update.message.reply_text(f"```\n{content}\n```", parse_mode="Markdown")
|
|
|
|
|
+
|
|
|
|
|
+async def analyser_cmd(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
|
|
|
+ user_id = update.effective_user.id
|
|
|
|
|
+ if len(context.args) < 2:
|
|
|
|
|
+ await update.message.reply_text("Usage : /analyser <repo> <fichier>")
|
|
|
|
|
+ return
|
|
|
|
|
+ repo = context.args[0]
|
|
|
|
|
+ filepath = " ".join(context.args[1:])
|
|
|
|
|
+ content = read_file(repo, filepath)
|
|
|
|
|
+ if content is None:
|
|
|
|
|
+ await update.message.reply_text(f"Fichier '{filepath}' introuvable.")
|
|
|
|
|
+ return
|
|
|
|
|
+ await update.message.reply_text(f"🔍 Analyse de {filepath}...")
|
|
|
|
|
+ message = f"Analyse ce fichier et donne résumé, points forts et problèmes :\n\n```\n{content[:4000]}\n```"
|
|
|
|
|
+ reply, _ = await ask_agent(user_id, message, "reviewer")
|
|
|
|
|
+ await update.message.reply_text(reply)
|
|
|
|
|
+
|
|
|
|
|
+async def pull_cmd(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
|
|
|
+ if not context.args:
|
|
|
|
|
+ await update.message.reply_text("Usage : /pull <repo>")
|
|
|
|
|
+ return
|
|
|
|
|
+ result = git_pull(context.args[0])
|
|
|
|
|
+ await update.message.reply_text(result)
|
|
|
|
|
+
|
|
|
|
|
+async def google_connect_cmd(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
|
|
|
+ user_id = update.effective_user.id
|
|
|
|
|
+ if get_google_creds():
|
|
|
|
|
+ await update.message.reply_text("✅ Google déjà connecté.\n/calendrier — voir tes événements\n/mails — voir tes mails")
|
|
|
|
|
+ return
|
|
|
|
|
+ flow = Flow.from_client_secrets_file(
|
|
|
|
|
+ GOOGLE_CREDS_FILE,
|
|
|
|
|
+ scopes=GOOGLE_SCOPES,
|
|
|
|
|
+ redirect_uri="urn:ietf:wg:oauth:2.0:oob"
|
|
|
|
|
+ )
|
|
|
|
|
+ auth_url, _ = flow.authorization_url(prompt="consent")
|
|
|
|
|
+ pending_oauth[user_id] = flow
|
|
|
|
|
+ await update.message.reply_text(
|
|
|
|
|
+ "🔗 Connexion Google\n\n"
|
|
|
|
|
+ "1. Clique sur ce lien et autorise l'accès :\n"
|
|
|
|
|
+ f"{auth_url}\n\n"
|
|
|
|
|
+ "2. Copie le code affiché\n"
|
|
|
|
|
+ "3. Envoie-le moi avec /google_code LECODICI"
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+async def google_code_cmd(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
|
|
|
+ user_id = update.effective_user.id
|
|
|
|
|
+ if not context.args:
|
|
|
|
|
+ await update.message.reply_text("Usage : /google_code <code>")
|
|
|
|
|
+ return
|
|
|
|
|
+ if user_id not in pending_oauth:
|
|
|
|
|
+ await update.message.reply_text("Lance d'abord /google_connect")
|
|
|
|
|
+ return
|
|
|
|
|
+ code = context.args[0]
|
|
|
|
|
+ flow = pending_oauth[user_id]
|
|
|
|
|
+ try:
|
|
|
|
|
+ flow.fetch_token(code=code)
|
|
|
|
|
+ creds = flow.credentials
|
|
|
|
|
+ with open(GOOGLE_TOKEN_FILE, "w") as f:
|
|
|
|
|
+ f.write(creds.to_json())
|
|
|
|
|
+ del pending_oauth[user_id]
|
|
|
|
|
+ await update.message.reply_text(
|
|
|
|
|
+ "✅ Google connecté avec succès !\n\n"
|
|
|
|
|
+ "/calendrier — voir tes événements\n"
|
|
|
|
|
+ "/mails — voir tes mails non lus"
|
|
|
|
|
+ )
|
|
|
|
|
+ except Exception as e:
|
|
|
|
|
+ await update.message.reply_text(f"Erreur d'authentification : {e}")
|
|
|
|
|
+
|
|
|
|
|
+async def calendrier_cmd(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
|
|
|
+ await update.message.reply_text("📅 Chargement du calendrier...")
|
|
|
|
|
+ events = get_calendar_events()
|
|
|
|
|
+ if events is None:
|
|
|
|
|
+ await update.message.reply_text("Google non connecté. Lance /google_connect")
|
|
|
|
|
+ return
|
|
|
|
|
+ if not events:
|
|
|
|
|
+ await update.message.reply_text("Aucun événement à venir.")
|
|
|
|
|
+ return
|
|
|
|
|
+ msg = "📅 *Prochains événements :*\n\n"
|
|
|
|
|
+ for event in events:
|
|
|
|
|
+ start = event["start"].get("dateTime", event["start"].get("date", ""))
|
|
|
|
|
+ if "T" in start:
|
|
|
|
|
+ dt = datetime.fromisoformat(start.replace("Z", "+00:00"))
|
|
|
|
|
+ start_str = dt.strftime("%d/%m %H:%M")
|
|
|
|
|
+ else:
|
|
|
|
|
+ start_str = start
|
|
|
|
|
+ msg += f"• *{event.get('summary', 'Sans titre')}*\n 📅 {start_str}\n\n"
|
|
|
|
|
+ await update.message.reply_text(msg, parse_mode="Markdown")
|
|
|
|
|
+
|
|
|
|
|
+async def mails_cmd(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
|
|
|
+ await update.message.reply_text("📬 Chargement des mails...")
|
|
|
|
|
+ emails = get_gmail_messages()
|
|
|
|
|
+ if emails is None:
|
|
|
|
|
+ await update.message.reply_text("Google non connecté. Lance /google_connect")
|
|
|
|
|
+ return
|
|
|
|
|
+ if not emails:
|
|
|
|
|
+ await update.message.reply_text("Aucun mail non lu.")
|
|
|
|
|
+ return
|
|
|
|
|
+ msg = "📬 *Mails non lus :*\n\n"
|
|
|
|
|
+ for e in emails:
|
|
|
|
|
+ msg += f"• *{e['subject']}*\n De : {e['from'][:50]}\n\n"
|
|
|
|
|
+ await update.message.reply_text(msg, parse_mode="Markdown")
|
|
|
|
|
+
|
|
|
|
|
+async def briefing_cmd(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
|
|
|
+ user_id = update.effective_user.id
|
|
|
|
|
+ if not context.args:
|
|
|
|
|
+ briefings = load_briefing_config()
|
|
|
|
|
+ cfg = briefings.get(str(user_id))
|
|
|
|
|
+ if cfg:
|
|
|
|
|
+ await update.message.reply_text(
|
|
|
|
|
+ f"⏰ Briefing matinal configuré à {cfg['heure']}.\n\n"
|
|
|
|
|
+ f"/briefing 07:30 — changer l'heure\n"
|
|
|
|
|
+ f"/briefing stop — désactiver\n"
|
|
|
|
|
+ f"/briefing_test — recevoir un briefing maintenant"
|
|
|
|
|
+ )
|
|
|
|
|
+ else:
|
|
|
|
|
+ await update.message.reply_text(
|
|
|
|
|
+ "Aucun briefing configuré.\n\n"
|
|
|
|
|
+ "Usage : /briefing 08:00\n"
|
|
|
|
|
+ "/briefing_test — tester maintenant"
|
|
|
|
|
+ )
|
|
|
|
|
+ return
|
|
|
|
|
+
|
|
|
|
|
+ if context.args[0].lower() == "stop":
|
|
|
|
|
+ briefings = load_briefing_config()
|
|
|
|
|
+ briefings.pop(str(user_id), None)
|
|
|
|
|
+ save_briefing_config(briefings)
|
|
|
|
|
+ job_id = f"briefing_{user_id}"
|
|
|
|
|
+ if scheduler.get_job(job_id):
|
|
|
|
|
+ scheduler.remove_job(job_id)
|
|
|
|
|
+ await update.message.reply_text("Briefing matinal désactivé.")
|
|
|
|
|
+ return
|
|
|
|
|
+
|
|
|
|
|
+ heure = context.args[0]
|
|
|
|
|
+ if ":" not in heure:
|
|
|
|
|
+ await update.message.reply_text("Format : /briefing 08:00")
|
|
|
|
|
+ return
|
|
|
|
|
+
|
|
|
|
|
+ h, m = heure.split(":")
|
|
|
|
|
+ briefings = load_briefing_config()
|
|
|
|
|
+ briefings[str(user_id)] = {"heure": heure}
|
|
|
|
|
+ save_briefing_config(briefings)
|
|
|
|
|
+
|
|
|
|
|
+ bot = context.application.bot
|
|
|
|
|
+ job_id = f"briefing_{user_id}"
|
|
|
|
|
+ if scheduler.get_job(job_id):
|
|
|
|
|
+ scheduler.remove_job(job_id)
|
|
|
|
|
+ scheduler.add_job(
|
|
|
|
|
+ send_briefing,
|
|
|
|
|
+ "cron",
|
|
|
|
|
+ hour=int(h),
|
|
|
|
|
+ minute=int(m),
|
|
|
|
|
+ args=[bot, user_id],
|
|
|
|
|
+ id=job_id,
|
|
|
|
|
+ replace_existing=True
|
|
|
|
|
+ )
|
|
|
|
|
+ await update.message.reply_text(
|
|
|
|
|
+ f"✅ Briefing matinal configuré à {heure}.\n"
|
|
|
|
|
+ f"Chaque matin tu recevras : météo, news tech, foot, tes notes et l'état du serveur.\n\n"
|
|
|
|
|
+ f"/briefing_test — tester maintenant"
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+async def briefing_test_cmd(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
|
|
|
+ user_id = update.effective_user.id
|
|
|
|
|
+ await update.message.reply_text("Génération du briefing...")
|
|
|
|
|
+ bot = context.application.bot
|
|
|
|
|
+ await send_briefing(bot, user_id)
|
|
|
|
|
+
|
|
|
|
|
+async def note_cmd(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
|
|
|
+ user_id = update.effective_user.id
|
|
|
|
|
+ if not context.args:
|
|
|
|
|
+ await update.message.reply_text("Usage : /note <contenu>\nEx: /note Je préfère Rust pour le backend")
|
|
|
|
|
+ return
|
|
|
|
|
+ content = " ".join(context.args)
|
|
|
|
|
+ note = add_note(user_id, content)
|
|
|
|
|
+ await update.message.reply_text(f"📝 Note #{note['id']} sauvegardée.")
|
|
|
|
|
+
|
|
|
|
|
+async def notes_cmd(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
|
|
|
+ user_id = update.effective_user.id
|
|
|
|
|
+ notes = load_notes(user_id)
|
|
|
|
|
+ if not notes:
|
|
|
|
|
+ await update.message.reply_text("Aucune note. Utilise /note <contenu> pour en créer une.")
|
|
|
|
|
+ return
|
|
|
|
|
+ msg = "📝 *Tes notes :*\n\n"
|
|
|
|
|
+ for n in notes[-10:]:
|
|
|
|
|
+ msg += f"#{n['id']} [{n['date']}]\n{n['content']}\n\n"
|
|
|
|
|
+ await update.message.reply_text(msg, parse_mode="Markdown")
|
|
|
|
|
+
|
|
|
|
|
+async def chercher_cmd(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
|
|
|
+ user_id = update.effective_user.id
|
|
|
|
|
+ if not context.args:
|
|
|
|
|
+ await update.message.reply_text("Usage : /chercher <mot-clé>")
|
|
|
|
|
+ return
|
|
|
|
|
+ query = " ".join(context.args)
|
|
|
|
|
+ await update.message.reply_text(f"🔍 Recherche dans la mémoire : \"{query}\"...")
|
|
|
|
|
+
|
|
|
|
|
+ memory_results, note_results = search_memory(user_id, query)
|
|
|
|
|
+
|
|
|
|
|
+ msg = ""
|
|
|
|
|
+
|
|
|
|
|
+ if note_results:
|
|
|
|
|
+ msg += f"📝 *Notes ({len(note_results)}) :*\n"
|
|
|
|
|
+ for n in note_results[:3]:
|
|
|
|
|
+ msg += f"• [{n['date']}] {n['content']}\n"
|
|
|
|
|
+ msg += "\n"
|
|
|
|
|
+
|
|
|
|
|
+ if memory_results:
|
|
|
|
|
+ msg += f"💬 *Conversations ({len(memory_results)}) :*\n"
|
|
|
|
|
+ for r in memory_results[:5]:
|
|
|
|
|
+ role_icon = "👤" if r["role"] == "user" else "🤖"
|
|
|
|
|
+ msg += f"{role_icon} [{r['agent']}] {r['content'][:200]}\n\n"
|
|
|
|
|
+
|
|
|
|
|
+ if not msg:
|
|
|
|
|
+ msg = f"Aucun résultat pour \"{query}\" dans la mémoire ou les notes."
|
|
|
|
|
+
|
|
|
|
|
+ await update.message.reply_text(msg, parse_mode="Markdown")
|
|
|
|
|
+
|
|
|
|
|
+async def meteo_cmd(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
|
|
|
+ user_id = update.effective_user.id
|
|
|
|
|
+ profile = load_profile(user_id)
|
|
|
|
|
+ city = " ".join(context.args) if context.args else profile.get("ville", DEFAULT_CITY)
|
|
|
|
|
+ result = await get_weather(city)
|
|
|
|
|
+ await update.message.reply_text(result)
|
|
|
|
|
+
|
|
|
|
|
+async def recherche_cmd(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
|
|
|
+ user_id = update.effective_user.id
|
|
|
|
|
+ if not context.args:
|
|
|
|
|
+ await update.message.reply_text("Usage : /recherche <requête>")
|
|
|
|
|
+ return
|
|
|
|
|
+ query = " ".join(context.args)
|
|
|
|
|
+ await update.message.reply_text(f"🔍 Recherche : {query}...")
|
|
|
|
|
+ results = web_search(query)
|
|
|
|
|
+ await update.message.reply_text(results, parse_mode="Markdown")
|
|
|
|
|
+
|
|
|
|
|
+async def news_cmd(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
|
|
|
+ category = context.args[0].lower() if context.args else "tech"
|
|
|
|
|
+ if category not in NEWS_FEEDS:
|
|
|
|
|
+ await update.message.reply_text(f"Catégories disponibles : {', '.join(NEWS_FEEDS.keys())}")
|
|
|
|
|
+ return
|
|
|
|
|
+ await update.message.reply_text(f"📰 Chargement des news {category}...")
|
|
|
|
|
+ news = get_news(category)
|
|
|
|
|
+ await update.message.reply_text(news, parse_mode="Markdown")
|
|
|
|
|
+
|
|
|
|
|
+async def handle_location(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
|
|
|
+ user_id = update.effective_user.id
|
|
|
|
|
+ location = update.message.location
|
|
|
|
|
+ lat, lon = location.latitude, location.longitude
|
|
|
|
|
+ await update.message.reply_text("📍 Localisation reçue, récupération météo...")
|
|
|
|
|
+ try:
|
|
|
|
|
+ async with httpx.AsyncClient(timeout=10) as client:
|
|
|
|
|
+ response = await client.get(f"https://wttr.in/{lat},{lon}?format=3&lang=fr")
|
|
|
|
|
+ result = f"🌤️ {response.text.strip()}"
|
|
|
|
|
+ # Sauvegarder la ville dans le profil
|
|
|
|
|
+ city_resp = await client.get(f"https://wttr.in/{lat},{lon}?format=%l")
|
|
|
|
|
+ city = city_resp.text.strip()
|
|
|
|
|
+ profile = load_profile(user_id)
|
|
|
|
|
+ profile["ville"] = f"{lat},{lon}"
|
|
|
|
|
+ save_profile(user_id, profile)
|
|
|
|
|
+ await update.message.reply_text(f"{result}\n\n📍 Position sauvegardée pour les prochaines fois.")
|
|
|
|
|
+ except Exception as e:
|
|
|
|
|
+ await update.message.reply_text(f"Erreur météo : {e}")
|
|
|
|
|
+
|
|
|
|
|
+async def handle_message(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
|
|
|
+ user_id = update.effective_user.id
|
|
|
|
|
+ text = update.message.text
|
|
|
|
|
+
|
|
|
|
|
+ # Détection automatique de note
|
|
|
|
|
+ note_triggers = ["note que", "souviens-toi que", "retiens que", "n'oublie pas que", "mémorise que"]
|
|
|
|
|
+ text_lower = text.lower()
|
|
|
|
|
+ for trigger in note_triggers:
|
|
|
|
|
+ if text_lower.startswith(trigger):
|
|
|
|
|
+ content = text[len(trigger):].strip()
|
|
|
|
|
+ note = add_note(user_id, content)
|
|
|
|
|
+ await update.message.reply_text(f"📝 Noté ! (#{note['id']}) : \"{content}\"")
|
|
|
|
|
+ return
|
|
|
|
|
+
|
|
|
|
|
+ await update.message.reply_text("⏳")
|
|
|
|
|
+ agent = await detect_agent(text)
|
|
|
|
|
+ emoji = AGENT_EMOJIS.get(agent, "🤖")
|
|
|
|
|
+ await update.message.reply_text(f"{emoji} Agent {agent.capitalize()}...")
|
|
|
|
|
+ reply, _ = await ask_agent(user_id, text, agent)
|
|
|
|
|
+ await update.message.reply_text(reply)
|
|
|
|
|
+
|
|
|
|
|
+async def handle_voice(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
|
|
|
+ user_id = update.effective_user.id
|
|
|
|
|
+ await update.message.reply_text("🎤 Transcription...")
|
|
|
|
|
+ voice = update.message.voice
|
|
|
|
|
+ file = await context.bot.get_file(voice.file_id)
|
|
|
|
|
+ with tempfile.NamedTemporaryFile(suffix=".ogg", delete=False) as tmp:
|
|
|
|
|
+ tmp_path = tmp.name
|
|
|
|
|
+ await file.download_to_drive(tmp_path)
|
|
|
|
|
+ try:
|
|
|
|
|
+ text = await transcribe_voice(tmp_path)
|
|
|
|
|
+ await update.message.reply_text(f"« {text} »")
|
|
|
|
|
+ agent = await detect_agent(text)
|
|
|
|
|
+ emoji = AGENT_EMOJIS.get(agent, "🤖")
|
|
|
|
|
+ await update.message.reply_text(f"{emoji} Agent {agent.capitalize()}...")
|
|
|
|
|
+ reply, _ = await ask_agent(user_id, text, agent)
|
|
|
|
|
+ await update.message.reply_text(reply)
|
|
|
|
|
+ audio_path = text_to_audio(reply)
|
|
|
|
|
+ with open(audio_path, "rb") as audio_file:
|
|
|
|
|
+ await update.message.reply_voice(audio_file)
|
|
|
|
|
+ os.unlink(audio_path)
|
|
|
|
|
+ except Exception as e:
|
|
|
|
|
+ await update.message.reply_text(f"Erreur : {e}")
|
|
|
|
|
+ finally:
|
|
|
|
|
+ os.unlink(tmp_path)
|
|
|
|
|
+
|
|
|
|
|
+async def handle_photo(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
|
|
|
+ await update.message.reply_text("🖼️ Analyse de l'image...")
|
|
|
|
|
+ photo = update.message.photo[-1]
|
|
|
|
|
+ file = await context.bot.get_file(photo.file_id)
|
|
|
|
|
+ with tempfile.NamedTemporaryFile(suffix=".jpg", delete=False) as tmp:
|
|
|
|
|
+ tmp_path = tmp.name
|
|
|
|
|
+ await file.download_to_drive(tmp_path)
|
|
|
|
|
+ try:
|
|
|
|
|
+ caption = update.message.caption or "Décris cette image en détail en français."
|
|
|
|
|
+ result = await analyze_image(tmp_path, caption)
|
|
|
|
|
+ await update.message.reply_text(result)
|
|
|
|
|
+ except Exception as e:
|
|
|
|
|
+ await update.message.reply_text(f"Erreur : {e}")
|
|
|
|
|
+ finally:
|
|
|
|
|
+ os.unlink(tmp_path)
|
|
|
|
|
+
|
|
|
|
|
+# ─── Main ─────────────────────────────────────────────────────────────────────
|
|
|
|
|
+
|
|
|
|
|
+BRIEFING_FILE = os.path.join(os.path.expanduser("~/agent"), "briefing.json")
|
|
|
|
|
+GOOGLE_CREDS_FILE = os.path.expanduser("~/agent/google_credentials.json")
|
|
|
|
|
+GOOGLE_TOKEN_FILE = os.path.expanduser("~/agent/google_token.json")
|
|
|
|
|
+GOOGLE_SCOPES = [
|
|
|
|
|
+ "https://www.googleapis.com/auth/calendar.readonly",
|
|
|
|
|
+ "https://www.googleapis.com/auth/gmail.readonly"
|
|
|
|
|
+]
|
|
|
|
|
+
|
|
|
|
|
+# Stockage temporaire des flows OAuth en attente
|
|
|
|
|
+pending_oauth = {}
|
|
|
|
|
+
|
|
|
|
|
+def load_briefing_config():
|
|
|
|
|
+ if os.path.exists(BRIEFING_FILE):
|
|
|
|
|
+ with open(BRIEFING_FILE, "r") as f:
|
|
|
|
|
+ return json.load(f)
|
|
|
|
|
+ return {}
|
|
|
|
|
+
|
|
|
|
|
+def save_briefing_config(config):
|
|
|
|
|
+ with open(BRIEFING_FILE, "w") as f:
|
|
|
|
|
+ json.dump(config, f, ensure_ascii=False, indent=2)
|
|
|
|
|
+
|
|
|
|
|
+async def send_briefing(bot: Bot, user_id: int):
|
|
|
|
|
+ try:
|
|
|
|
|
+ profile = load_profile(user_id)
|
|
|
|
|
+ nom = profile.get("nom", "")
|
|
|
|
|
+ ville = profile.get("ville", DEFAULT_CITY)
|
|
|
|
|
+ notes = load_notes(user_id)
|
|
|
|
|
+ notes_recentes = notes[-3:] if notes else []
|
|
|
|
|
+
|
|
|
|
|
+ greeting = f"☀️ Bonjour {nom} !\n\n" if nom else "☀️ Bonjour !\n\n"
|
|
|
|
|
+ msg = greeting
|
|
|
|
|
+
|
|
|
|
|
+ # Météo
|
|
|
|
|
+ meteo = await get_weather(ville)
|
|
|
|
|
+ msg += f"{meteo}\n\n"
|
|
|
|
|
+
|
|
|
|
|
+ # News tech
|
|
|
|
|
+ tech = get_news("tech", max_items=3)
|
|
|
|
|
+ msg += f"📰 *Tech du jour :*\n"
|
|
|
|
|
+ lines = [l for l in tech.split("\n") if l.startswith("•")][:3]
|
|
|
|
|
+ msg += "\n".join(lines) + "\n\n"
|
|
|
|
|
+
|
|
|
|
|
+ # Football
|
|
|
|
|
+ foot = get_news("football", max_items=2)
|
|
|
|
|
+ msg += f"⚽ *Football :*\n"
|
|
|
|
|
+ lines = [l for l in foot.split("\n") if l.startswith("•")][:2]
|
|
|
|
|
+ msg += "\n".join(lines) + "\n\n"
|
|
|
|
|
+
|
|
|
|
|
+ # Notes récentes
|
|
|
|
|
+ if notes_recentes:
|
|
|
|
|
+ msg += "📝 *Tes dernières notes :*\n"
|
|
|
|
|
+ for n in notes_recentes:
|
|
|
|
|
+ msg += f"• {n['content']}\n"
|
|
|
|
|
+ msg += "\n"
|
|
|
|
|
+
|
|
|
|
|
+ # Calendrier
|
|
|
|
|
+ events = get_calendar_events(max_results=3)
|
|
|
|
|
+ if events:
|
|
|
|
|
+ msg += "📅 *Agenda du jour :*\n"
|
|
|
|
|
+ for event in events:
|
|
|
|
|
+ start = event["start"].get("dateTime", event["start"].get("date", ""))
|
|
|
|
|
+ if "T" in start:
|
|
|
|
|
+ dt = datetime.fromisoformat(start.replace("Z", "+00:00"))
|
|
|
|
|
+ start_str = dt.strftime("%H:%M")
|
|
|
|
|
+ else:
|
|
|
|
|
+ start_str = start
|
|
|
|
|
+ msg += f"• {start_str} — {event.get('summary', 'Sans titre')}\n"
|
|
|
|
|
+ msg += "\n"
|
|
|
|
|
+
|
|
|
|
|
+ # Mails non lus
|
|
|
|
|
+ emails = get_gmail_messages(max_results=3)
|
|
|
|
|
+ if emails:
|
|
|
|
|
+ msg += f"📬 *{len(emails)} mail(s) non lu(s) :*\n"
|
|
|
|
|
+ for e in emails[:3]:
|
|
|
|
|
+ msg += f"• {e['subject'][:50]}\n"
|
|
|
|
|
+ msg += "\n"
|
|
|
|
|
+
|
|
|
|
|
+ # Monitoring
|
|
|
|
|
+ cpu = psutil.cpu_percent(interval=0.5)
|
|
|
|
|
+ ram = psutil.virtual_memory()
|
|
|
|
|
+ msg += f"🖥️ Serveur : CPU {cpu:.0f}% · RAM {ram.percent:.0f}%"
|
|
|
|
|
+
|
|
|
|
|
+ await bot.send_message(chat_id=user_id, text=msg, parse_mode="Markdown")
|
|
|
|
|
+ logger.info(f"Briefing envoyé à {user_id}")
|
|
|
|
|
+ except Exception as e:
|
|
|
|
|
+ logger.error(f"Erreur briefing : {e}")
|
|
|
|
|
+
|
|
|
|
|
+async def post_init(application):
|
|
|
|
|
+ scheduler.start()
|
|
|
|
|
+ bot = application.bot
|
|
|
|
|
+
|
|
|
|
|
+ # Recharger les rappels persistés
|
|
|
|
|
+ reminders = load_reminders()
|
|
|
|
|
+ for r in reminders:
|
|
|
|
|
+ target = datetime.fromisoformat(r["time"])
|
|
|
|
|
+ if target > datetime.now():
|
|
|
|
|
+ scheduler.add_job(
|
|
|
|
|
+ send_reminder,
|
|
|
|
|
+ "date",
|
|
|
|
|
+ run_date=target,
|
|
|
|
|
+ args=[bot, r["user_id"], r["message"], r["id"]],
|
|
|
|
|
+ id=r["id"],
|
|
|
|
|
+ replace_existing=True
|
|
|
|
|
+ )
|
|
|
|
|
+ logger.info(f"{len(reminders)} rappel(s) rechargé(s).")
|
|
|
|
|
+
|
|
|
|
|
+ # Recharger les briefings configurés
|
|
|
|
|
+ briefings = load_briefing_config()
|
|
|
|
|
+ for user_id_str, cfg in briefings.items():
|
|
|
|
|
+ user_id = int(user_id_str)
|
|
|
|
|
+ heure = cfg.get("heure", "08:00")
|
|
|
|
|
+ h, m = heure.split(":")
|
|
|
|
|
+ scheduler.add_job(
|
|
|
|
|
+ send_briefing,
|
|
|
|
|
+ "cron",
|
|
|
|
|
+ hour=int(h),
|
|
|
|
|
+ minute=int(m),
|
|
|
|
|
+ args=[bot, user_id],
|
|
|
|
|
+ id=f"briefing_{user_id}",
|
|
|
|
|
+ replace_existing=True
|
|
|
|
|
+ )
|
|
|
|
|
+ logger.info(f"Briefing programmé à {heure} pour {user_id}")
|
|
|
|
|
+
|
|
|
|
|
+ # Démarrer l'API HTTP pour le client PC
|
|
|
|
|
+ api_app = web.Application()
|
|
|
|
|
+ api_app.router.add_post("/api/chat", api_chat_handler)
|
|
|
|
|
+ runner = web.AppRunner(api_app)
|
|
|
|
|
+ await runner.setup()
|
|
|
|
|
+ site = web.TCPSite(runner, "0.0.0.0", 8765)
|
|
|
|
|
+ await site.start()
|
|
|
|
|
+ logger.info("API HTTP démarrée sur port 8765")
|
|
|
|
|
+
|
|
|
|
|
+async def api_chat_handler(request):
|
|
|
|
|
+ try:
|
|
|
|
|
+ data = await request.json()
|
|
|
|
|
+ text = data.get("text", "").strip()
|
|
|
|
|
+ user_id = data.get("user_id", "pc_user")
|
|
|
|
|
+ if not text:
|
|
|
|
|
+ return web.json_response({"error": "texte vide"}, status=400)
|
|
|
|
|
+ agent = await detect_agent(text)
|
|
|
|
|
+ response = await ask_agent(user_id, text, agent)
|
|
|
|
|
+ return web.json_response({"response": response, "agent": agent})
|
|
|
|
|
+ except Exception as e:
|
|
|
|
|
+ logger.error(f"API erreur : {e}")
|
|
|
|
|
+ return web.json_response({"error": str(e)}, status=500)
|
|
|
|
|
+
|
|
|
|
|
+if __name__ == "__main__":
|
|
|
|
|
+ app = ApplicationBuilder().token(TELEGRAM_TOKEN).post_init(post_init).build()
|
|
|
|
|
+ app.add_handler(CommandHandler("start", start))
|
|
|
|
|
+ app.add_handler(CommandHandler("reset", reset))
|
|
|
|
|
+ app.add_handler(CommandHandler("agents", agents_cmd))
|
|
|
|
|
+ app.add_handler(CommandHandler("setnom", set_nom))
|
|
|
|
|
+ app.add_handler(CommandHandler("setprojet", set_projet))
|
|
|
|
|
+ app.add_handler(CommandHandler("profil", profil))
|
|
|
|
|
+ app.add_handler(CommandHandler("monitoring", monitoring_cmd))
|
|
|
|
|
+ app.add_handler(CommandHandler("rappel", rappel_cmd))
|
|
|
|
|
+ app.add_handler(CommandHandler("rappels", rappels_cmd))
|
|
|
|
|
+ app.add_handler(CommandHandler("repos", repos_cmd))
|
|
|
|
|
+ app.add_handler(CommandHandler("clone", clone_cmd))
|
|
|
|
|
+ app.add_handler(CommandHandler("fichiers", fichiers_cmd))
|
|
|
|
|
+ app.add_handler(CommandHandler("lire", lire_cmd))
|
|
|
|
|
+ app.add_handler(CommandHandler("analyser", analyser_cmd))
|
|
|
|
|
+ app.add_handler(CommandHandler("log", log_cmd))
|
|
|
|
|
+ app.add_handler(CommandHandler("pull", pull_cmd))
|
|
|
|
|
+ app.add_handler(CommandHandler("briefing", briefing_cmd))
|
|
|
|
|
+ app.add_handler(CommandHandler("briefing_test", briefing_test_cmd))
|
|
|
|
|
+ app.add_handler(CommandHandler("note", note_cmd))
|
|
|
|
|
+ app.add_handler(CommandHandler("notes", notes_cmd))
|
|
|
|
|
+ app.add_handler(CommandHandler("chercher", chercher_cmd))
|
|
|
|
|
+ app.add_handler(CommandHandler("meteo", meteo_cmd))
|
|
|
|
|
+ app.add_handler(CommandHandler("recherche", recherche_cmd))
|
|
|
|
|
+ app.add_handler(CommandHandler("news", news_cmd))
|
|
|
|
|
+ app.add_handler(CommandHandler("google_connect", google_connect_cmd))
|
|
|
|
|
+ app.add_handler(CommandHandler("google_code", google_code_cmd))
|
|
|
|
|
+ app.add_handler(CommandHandler("calendrier", calendrier_cmd))
|
|
|
|
|
+ app.add_handler(CommandHandler("mails", mails_cmd))
|
|
|
|
|
+ app.add_handler(MessageHandler(filters.LOCATION, handle_location))
|
|
|
|
|
+ app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, handle_message))
|
|
|
|
|
+ app.add_handler(MessageHandler(filters.VOICE, handle_voice))
|
|
|
|
|
+ app.add_handler(MessageHandler(filters.PHOTO, handle_photo))
|
|
|
|
|
+ logger.info("Konan demarre - Google + Météo + News + Recherche + Monitoring + Rappels + Git + Vision + Voix + Agents.")
|
|
|
|
|
+ app.run_polling()
|