bot.py 49 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210
  1. import logging
  2. import json
  3. import os
  4. import tempfile
  5. import subprocess
  6. import base64
  7. import httpx
  8. import whisper
  9. import psutil
  10. import feedparser
  11. from datetime import datetime, timedelta
  12. from apscheduler.schedulers.asyncio import AsyncIOScheduler
  13. from duckduckgo_search import DDGS
  14. from telegram import Update, Bot
  15. from telegram.ext import ApplicationBuilder, CommandHandler, MessageHandler, filters, ContextTypes
  16. from google.oauth2.credentials import Credentials
  17. from google_auth_oauthlib.flow import Flow
  18. from googleapiclient.discovery import build
  19. from google.auth.transport.requests import Request
  20. from aiohttp import web
  21. TELEGRAM_TOKEN = os.environ.get("TELEGRAM_TOKEN", "")
  22. if not TELEGRAM_TOKEN:
  23. raise RuntimeError("TELEGRAM_TOKEN non defini. Ajoute-le dans .env ou en variable d'environnement.")
  24. OLLAMA_URL = os.environ.get("OLLAMA_URL", "http://localhost:11434/api/chat")
  25. OLLAMA_GENERATE_URL = os.environ.get("OLLAMA_GENERATE_URL", "http://localhost:11434/api/generate")
  26. MEMORY_DIR = os.path.expanduser("~/agent/memory")
  27. REPOS_DIR = os.path.expanduser("~/agent/repos")
  28. VOICE_MODEL = os.path.expanduser("~/agent/voices/fr_FR-upmc-medium.onnx")
  29. PIPER_BIN = os.path.expanduser("~/agent/venv/bin/piper")
  30. REMINDERS_FILE = os.path.join(os.path.expanduser("~/agent"), "reminders.json")
  31. DEFAULT_CITY = "Nantes"
  32. NEWS_FEEDS = {
  33. "tech": [
  34. "https://www.01net.com/rss/",
  35. "https://www.lemonde.fr/pixels/rss_full.xml",
  36. "https://www.zdnet.fr/feeds/rss/actualites/",
  37. ],
  38. "politique": [
  39. "https://www.lemonde.fr/politique/rss_full.xml",
  40. "https://www.rfi.fr/fr/rss/politique-fr/",
  41. "https://www.francetvinfo.fr/politique.rss",
  42. ],
  43. "football": [
  44. "https://www.lequipe.fr/rss/actu_rss_Football.xml",
  45. "https://www.bbc.co.uk/sport/football/european/rss.xml",
  46. ]
  47. }
  48. logging.basicConfig(level=logging.INFO)
  49. logger = logging.getLogger(__name__)
  50. os.makedirs(MEMORY_DIR, exist_ok=True)
  51. os.makedirs(REPOS_DIR, exist_ok=True)
  52. scheduler = AsyncIOScheduler()
  53. # ─── Agents ───────────────────────────────────────────────────────────────────
  54. AGENTS = {
  55. "konan": {
  56. "model": "qwen2.5:3b",
  57. "prompt": (
  58. "Tu es Konan, orchestrateur d'agents IA et développeur senior. "
  59. "Tu adaptes ton ton naturellement selon le contexte : "
  60. "décontracté et amical pour une conversation, précis et direct pour du technique, "
  61. "structuré et rigoureux pour de l'architecture, encourageant pour du brainstorming. "
  62. "Tu parles toujours en français. Tu ne te présentes pas à chaque message. "
  63. "Quand une demande nécessite un expert spécialisé, tu le dis et tu délègues. "
  64. "Tu es concis : pas de blabla inutile, tu vas droit au but."
  65. )
  66. },
  67. "coder": {
  68. "model": "qwen2.5-coder:3b",
  69. "prompt": (
  70. "Tu es l'agent Coder de Konan, expert en développement logiciel. "
  71. "Tu écris du code propre, lisible et bien commenté. "
  72. "Tu expliques tes choix techniques brièvement. "
  73. "Tu signales les bugs potentiels et proposes des améliorations. "
  74. "Tu réponds en français, avec le code dans des blocs formatés."
  75. )
  76. },
  77. "reviewer": {
  78. "model": "qwen2.5-coder:3b",
  79. "prompt": (
  80. "Tu es l'agent Reviewer de Konan, expert en revue de code. "
  81. "Tu analyses le code de façon critique et constructive. "
  82. "Tu identifies : bugs, failles de sécurité, mauvaises pratiques, performance. "
  83. "Tu proposes des corrections concrètes avec des exemples. "
  84. "Sois direct et précis. Tu réponds en français."
  85. )
  86. },
  87. "architect": {
  88. "model": "qwen2.5:3b",
  89. "prompt": (
  90. "Tu es l'agent Architect de Konan, expert en architecture logicielle et systèmes IA. "
  91. "Tu penses en termes de scalabilité, maintenabilité et simplicité. "
  92. "Tu proposes des architectures claires avec des schémas textuels si nécessaire. "
  93. "Tu justifies tes choix et présentes les alternatives. "
  94. "Tu réponds en français de façon structurée."
  95. )
  96. },
  97. "analyst": {
  98. "model": "qwen2.5:3b",
  99. "prompt": (
  100. "Tu es l'agent Analyst de Konan, expert en analyse et documentation. "
  101. "Tu synthétises l'information de façon claire et structurée. "
  102. "Tu produis des analyses, des résumés, de la documentation technique. "
  103. "Tu es méthodique et exhaustif sans être verbeux. Tu réponds en français."
  104. )
  105. }
  106. }
  107. AGENT_EMOJIS = {"konan": "🧠", "coder": "💻", "reviewer": "🔍", "architect": "🏗️", "analyst": "📊"}
  108. # ─── Profil ───────────────────────────────────────────────────────────────────
  109. def profile_path(user_id):
  110. return os.path.join(MEMORY_DIR, f"{user_id}_profile.json")
  111. def load_profile(user_id):
  112. path = profile_path(user_id)
  113. if os.path.exists(path):
  114. with open(path, "r") as f:
  115. return json.load(f)
  116. return {"nom": None, "projets": [], "preferences": ""}
  117. def save_profile(user_id, profile):
  118. with open(profile_path(user_id), "w") as f:
  119. json.dump(profile, f, ensure_ascii=False, indent=2)
  120. # ─── Mémoire ──────────────────────────────────────────────────────────────────
  121. def memory_path(user_id, agent="konan"):
  122. return os.path.join(MEMORY_DIR, f"{user_id}_{agent}.json")
  123. def build_system_prompt(user_id, agent="konan"):
  124. profile = load_profile(user_id)
  125. nom = profile.get("nom")
  126. projets = profile.get("projets", [])
  127. base = AGENTS[agent]["prompt"]
  128. if nom:
  129. base += f" Tu parles à {nom}."
  130. if projets:
  131. base += f" Ses projets : {', '.join(projets)}."
  132. return base
  133. def load_conversation(user_id, agent="konan"):
  134. path = memory_path(user_id, agent)
  135. system = build_system_prompt(user_id, agent)
  136. if os.path.exists(path):
  137. msgs = json.load(open(path))
  138. msgs[0] = {"role": "system", "content": system}
  139. return msgs
  140. return [{"role": "system", "content": system}]
  141. def save_conversation(user_id, messages, agent="konan"):
  142. with open(memory_path(user_id, agent), "w") as f:
  143. json.dump(messages, f, ensure_ascii=False, indent=2)
  144. # ─── Monitoring ───────────────────────────────────────────────────────────────
  145. def get_server_status():
  146. cpu = psutil.cpu_percent(interval=1)
  147. ram = psutil.virtual_memory()
  148. disk = psutil.disk_usage("/")
  149. temps = psutil.sensors_temperatures() if hasattr(psutil, "sensors_temperatures") else {}
  150. ram_used = ram.used / (1024**3)
  151. ram_total = ram.total / (1024**3)
  152. disk_used = disk.used / (1024**3)
  153. disk_total = disk.total / (1024**3)
  154. cpu_emoji = "🔴" if cpu > 80 else "🟡" if cpu > 50 else "🟢"
  155. ram_emoji = "🔴" if ram.percent > 80 else "🟡" if ram.percent > 60 else "🟢"
  156. disk_emoji = "🔴" if disk.percent > 90 else "🟡" if disk.percent > 75 else "🟢"
  157. msg = f"📊 *État du serveur*\n\n"
  158. msg += f"{cpu_emoji} CPU : {cpu:.1f}%\n"
  159. msg += f"{ram_emoji} RAM : {ram_used:.1f}/{ram_total:.1f} GB ({ram.percent}%)\n"
  160. msg += f"{disk_emoji} Disque : {disk_used:.1f}/{disk_total:.1f} GB ({disk.percent}%)\n"
  161. # Processus les plus gourmands
  162. procs = sorted(psutil.process_iter(["pid", "name", "cpu_percent", "memory_percent"]),
  163. key=lambda p: p.info["cpu_percent"] or 0, reverse=True)[:3]
  164. if procs:
  165. msg += "\n🔝 *Top processus CPU :*\n"
  166. for p in procs:
  167. msg += f" {p.info['name']} — CPU {p.info['cpu_percent']:.1f}% RAM {p.info['memory_percent']:.1f}%\n"
  168. # Ollama actif ?
  169. ollama_running = any("ollama" in p.info["name"].lower() for p in psutil.process_iter(["name"]))
  170. msg += f"\n🤖 Ollama : {'✅ actif' if ollama_running else '❌ inactif'}\n"
  171. msg += f"🕐 Uptime : {datetime.now().strftime('%H:%M:%S')}"
  172. return msg
  173. # ─── Rappels ──────────────────────────────────────────────────────────────────
  174. def load_reminders():
  175. if os.path.exists(REMINDERS_FILE):
  176. with open(REMINDERS_FILE, "r") as f:
  177. return json.load(f)
  178. return []
  179. def save_reminders(reminders):
  180. with open(REMINDERS_FILE, "w") as f:
  181. json.dump(reminders, f, ensure_ascii=False, indent=2)
  182. async def send_reminder(bot: Bot, user_id: int, message: str, reminder_id: str):
  183. try:
  184. await bot.send_message(chat_id=user_id, text=f"⏰ Rappel : {message}")
  185. # Supprimer le rappel après envoi
  186. reminders = load_reminders()
  187. reminders = [r for r in reminders if r["id"] != reminder_id]
  188. save_reminders(reminders)
  189. except Exception as e:
  190. logger.error(f"Erreur rappel : {e}")
  191. def parse_time(time_str: str):
  192. """Parse '30m', '2h', '10:30' ou 'demain 9h'"""
  193. now = datetime.now()
  194. time_str = time_str.strip().lower()
  195. if time_str.endswith("m"):
  196. minutes = int(time_str[:-1])
  197. return now + timedelta(minutes=minutes)
  198. elif time_str.endswith("h") and ":" not in time_str:
  199. hours = int(time_str[:-1])
  200. return now + timedelta(hours=hours)
  201. elif ":" in time_str:
  202. h, m = time_str.split(":")
  203. target = now.replace(hour=int(h), minute=int(m), second=0)
  204. if target < now:
  205. target += timedelta(days=1)
  206. return target
  207. return None
  208. # ─── Détection d'intention ────────────────────────────────────────────────────
  209. async def detect_agent(message: str) -> str:
  210. prompt = (
  211. "Analyse cette demande et réponds UNIQUEMENT par un de ces mots : "
  212. "konan, coder, reviewer, architect, analyst.\n\n"
  213. "- konan : conversation, question générale\n"
  214. "- coder : écrire du code, corriger un bug\n"
  215. "- reviewer : relire du code, code review\n"
  216. "- architect : architecture, conception, design\n"
  217. "- analyst : analyser, documenter, résumer\n\n"
  218. f"Demande : {message}\n\nRéponds avec un seul mot."
  219. )
  220. try:
  221. async with httpx.AsyncClient(timeout=30) as client:
  222. response = await client.post(OLLAMA_URL, json={
  223. "model": "qwen2.5:3b",
  224. "messages": [{"role": "user", "content": prompt}],
  225. "stream": False,
  226. "options": {"num_predict": 5}
  227. })
  228. result = response.json()["message"]["content"].strip().lower()
  229. for agent in AGENTS:
  230. if agent in result:
  231. return agent
  232. except Exception:
  233. pass
  234. return "konan"
  235. # ─── Ollama ───────────────────────────────────────────────────────────────────
  236. async def ask_agent(user_id, message, agent="konan"):
  237. messages = load_conversation(user_id, agent)
  238. messages.append({"role": "user", "content": message})
  239. if len(messages) > 21:
  240. messages = [messages[0]] + messages[-20:]
  241. try:
  242. async with httpx.AsyncClient(timeout=180) as client:
  243. response = await client.post(OLLAMA_URL, json={
  244. "model": AGENTS[agent]["model"],
  245. "messages": messages,
  246. "stream": False
  247. })
  248. data = response.json()
  249. reply = data["message"]["content"]
  250. messages.append({"role": "assistant", "content": reply})
  251. save_conversation(user_id, messages, agent)
  252. return reply, agent
  253. except Exception as e:
  254. return f"Erreur : {e}", agent
  255. # ─── Vision ───────────────────────────────────────────────────────────────────
  256. async def analyze_image(image_path: str, question: str = "Décris cette image en détail en français.") -> str:
  257. with open(image_path, "rb") as f:
  258. image_b64 = base64.b64encode(f.read()).decode("utf-8")
  259. try:
  260. async with httpx.AsyncClient(timeout=180) as client:
  261. response = await client.post(OLLAMA_GENERATE_URL, json={
  262. "model": "moondream",
  263. "prompt": question,
  264. "images": [image_b64],
  265. "stream": False
  266. })
  267. return response.json().get("response", "Impossible d'analyser l'image.")
  268. except Exception as e:
  269. return f"Erreur analyse image : {e}"
  270. # ─── TTS / STT ────────────────────────────────────────────────────────────────
  271. logger.info("Chargement Whisper...")
  272. whisper_model = whisper.load_model("tiny")
  273. logger.info("Whisper pret.")
  274. def text_to_audio(text):
  275. tmp = tempfile.NamedTemporaryFile(suffix=".wav", delete=False)
  276. tmp.close()
  277. subprocess.run(
  278. [PIPER_BIN, "--model", VOICE_MODEL, "--output_file", tmp.name],
  279. input=text.encode("utf-8"), check=True
  280. )
  281. return tmp.name
  282. async def transcribe_voice(file_path):
  283. result = whisper_model.transcribe(file_path, language="fr")
  284. return result["text"].strip()
  285. # ─── Git ──────────────────────────────────────────────────────────────────────
  286. def list_repos():
  287. if not os.path.exists(REPOS_DIR):
  288. return []
  289. return [d for d in os.listdir(REPOS_DIR) if os.path.isdir(os.path.join(REPOS_DIR, d, ".git"))]
  290. def list_files(repo):
  291. repo_path = os.path.join(REPOS_DIR, repo)
  292. if not os.path.exists(repo_path):
  293. return None
  294. result = subprocess.run(["git", "ls-files"], cwd=repo_path, capture_output=True, text=True)
  295. return [f for f in result.stdout.strip().split("\n") if f]
  296. def read_file(repo, filepath):
  297. full_path = os.path.join(REPOS_DIR, repo, filepath)
  298. if not os.path.exists(full_path):
  299. return None
  300. with open(full_path, "r", errors="replace") as f:
  301. return f.read()
  302. def git_log(repo, n=5):
  303. result = subprocess.run(["git", "log", f"-{n}", "--oneline"],
  304. cwd=os.path.join(REPOS_DIR, repo), capture_output=True, text=True)
  305. return result.stdout.strip()
  306. def git_pull(repo):
  307. result = subprocess.run(["git", "pull"],
  308. cwd=os.path.join(REPOS_DIR, repo), capture_output=True, text=True)
  309. return result.stdout.strip() or result.stderr.strip()
  310. # ─── Google Auth ──────────────────────────────────────────────────────────────
  311. def get_google_creds():
  312. creds = None
  313. if os.path.exists(GOOGLE_TOKEN_FILE):
  314. creds = Credentials.from_authorized_user_file(GOOGLE_TOKEN_FILE, GOOGLE_SCOPES)
  315. if creds and creds.expired and creds.refresh_token:
  316. creds.refresh(Request())
  317. with open(GOOGLE_TOKEN_FILE, "w") as f:
  318. f.write(creds.to_json())
  319. return creds if (creds and creds.valid) else None
  320. # ─── Google Calendar ──────────────────────────────────────────────────────────
  321. def get_calendar_events(max_results=10):
  322. creds = get_google_creds()
  323. if not creds:
  324. return None
  325. service = build("calendar", "v3", credentials=creds)
  326. now = datetime.utcnow().isoformat() + "Z"
  327. events_result = service.events().list(
  328. calendarId="primary",
  329. timeMin=now,
  330. maxResults=max_results,
  331. singleEvents=True,
  332. orderBy="startTime"
  333. ).execute()
  334. return events_result.get("items", [])
  335. # ─── Gmail ────────────────────────────────────────────────────────────────────
  336. def get_gmail_messages(max_results=5):
  337. creds = get_google_creds()
  338. if not creds:
  339. return None
  340. service = build("gmail", "v1", credentials=creds)
  341. result = service.users().messages().list(
  342. userId="me", maxResults=max_results, q="is:unread"
  343. ).execute()
  344. messages = result.get("messages", [])
  345. emails = []
  346. for msg in messages:
  347. detail = service.users().messages().get(
  348. userId="me", id=msg["id"], format="metadata",
  349. metadataHeaders=["From", "Subject", "Date"]
  350. ).execute()
  351. headers = {h["name"]: h["value"] for h in detail["payload"]["headers"]}
  352. emails.append({
  353. "from": headers.get("From", ""),
  354. "subject": headers.get("Subject", ""),
  355. "date": headers.get("Date", "")
  356. })
  357. return emails
  358. # ─── Notes ────────────────────────────────────────────────────────────────────
  359. def notes_path(user_id):
  360. return os.path.join(MEMORY_DIR, f"{user_id}_notes.json")
  361. def load_notes(user_id):
  362. path = notes_path(user_id)
  363. if os.path.exists(path):
  364. with open(path, "r") as f:
  365. return json.load(f)
  366. return []
  367. def save_notes(user_id, notes):
  368. with open(notes_path(user_id), "w") as f:
  369. json.dump(notes, f, ensure_ascii=False, indent=2)
  370. def add_note(user_id, content):
  371. notes = load_notes(user_id)
  372. note = {
  373. "id": len(notes) + 1,
  374. "content": content,
  375. "date": datetime.now().strftime("%Y-%m-%d %H:%M")
  376. }
  377. notes.append(note)
  378. save_notes(user_id, notes)
  379. return note
  380. def search_notes(user_id, query):
  381. notes = load_notes(user_id)
  382. query_lower = query.lower()
  383. return [n for n in notes if query_lower in n["content"].lower()]
  384. def search_memory(user_id, query):
  385. results = []
  386. query_lower = query.lower()
  387. # Chercher dans toutes les conversations
  388. for agent in AGENTS:
  389. path = memory_path(user_id, agent)
  390. if not os.path.exists(path):
  391. continue
  392. msgs = json.load(open(path))
  393. for msg in msgs:
  394. if msg["role"] in ("user", "assistant") and query_lower in msg["content"].lower():
  395. results.append({
  396. "agent": agent,
  397. "role": msg["role"],
  398. "content": msg["content"][:300]
  399. })
  400. # Chercher dans les notes
  401. notes = search_notes(user_id, query)
  402. return results, notes
  403. # ─── Météo ────────────────────────────────────────────────────────────────────
  404. async def get_weather(city: str) -> str:
  405. try:
  406. async with httpx.AsyncClient(timeout=10) as client:
  407. response = await client.get(f"https://wttr.in/{city}?format=3&lang=fr")
  408. return f"🌤️ {response.text.strip()}"
  409. except Exception as e:
  410. return f"Impossible de récupérer la météo : {e}"
  411. # ─── Recherche web ────────────────────────────────────────────────────────────
  412. def web_search(query: str, max_results: int = 5) -> str:
  413. try:
  414. with DDGS() as ddgs:
  415. results = list(ddgs.text(query, region="fr-fr", max_results=max_results))
  416. if not results:
  417. return "Aucun résultat trouvé."
  418. msg = ""
  419. for r in results:
  420. msg += f"• *{r.get('title','')}*\n{r.get('body','')[:200]}\n\n"
  421. return msg.strip()
  422. except Exception as e:
  423. return f"Erreur recherche : {e}"
  424. # ─── Actualités ───────────────────────────────────────────────────────────────
  425. def get_news(category: str = "tech", max_items: int = 5) -> str:
  426. feeds = NEWS_FEEDS.get(category, NEWS_FEEDS["tech"])
  427. items = []
  428. for url in feeds:
  429. try:
  430. feed = feedparser.parse(url)
  431. for entry in feed.entries[:3]:
  432. items.append({
  433. "title": entry.get("title", ""),
  434. "summary": entry.get("summary", "")[:200]
  435. })
  436. except Exception:
  437. continue
  438. if not items:
  439. return "Impossible de récupérer les actualités."
  440. items = items[:max_items]
  441. msg = f"📰 *Actualités {category}*\n\n"
  442. for item in items:
  443. msg += f"• *{item['title']}*\n{item['summary']}\n\n"
  444. return msg.strip()
  445. # ─── Handlers ─────────────────────────────────────────────────────────────────
  446. async def start(update: Update, context: ContextTypes.DEFAULT_TYPE):
  447. user_id = update.effective_user.id
  448. profile = load_profile(user_id)
  449. nom = profile.get("nom")
  450. if nom:
  451. msg = f"Content de te retrouver {nom} ! Je suis Konan."
  452. else:
  453. msg = (
  454. "Bonjour ! Je suis Konan, ton orchestrateur d'agents IA.\n\n"
  455. "Agents : 💻 Coder · 🔍 Reviewer · 🏗️ Architect · 📊 Analyst\n"
  456. "Tu peux m'envoyer du texte, de la voix, des images ou ta localisation.\n\n"
  457. "/setnom Prenom — te présenter\n"
  458. "/monitoring — état du serveur\n"
  459. "/meteo [ville] — météo (ou partage ta position)\n"
  460. "/news [tech|politique|football] — actualités\n"
  461. "/recherche <query> — recherche web\n"
  462. "/note <contenu> — sauvegarder une note\n"
  463. "/notes — voir tes notes\n"
  464. "/chercher <mot> — chercher dans la mémoire\n"
  465. "/briefing 08:00 — briefing matinal automatique\n"
  466. "/rappel 30m Message — créer un rappel\n"
  467. "/repos — projets Git\n"
  468. "/agents — voir les agents\n"
  469. "/reset — réinitialiser"
  470. )
  471. await update.message.reply_text(msg)
  472. async def monitoring_cmd(update: Update, context: ContextTypes.DEFAULT_TYPE):
  473. status = get_server_status()
  474. await update.message.reply_text(status, parse_mode="Markdown")
  475. async def rappel_cmd(update: Update, context: ContextTypes.DEFAULT_TYPE):
  476. user_id = update.effective_user.id
  477. if len(context.args) < 2:
  478. await update.message.reply_text(
  479. "Usage : /rappel <temps> <message>\n\n"
  480. "Exemples :\n"
  481. "/rappel 30m Faire une pause\n"
  482. "/rappel 2h Revoir l'architecture\n"
  483. "/rappel 14:30 Réunion équipe"
  484. )
  485. return
  486. time_str = context.args[0]
  487. message = " ".join(context.args[1:])
  488. target_time = parse_time(time_str)
  489. if not target_time:
  490. await update.message.reply_text("Format de temps non reconnu. Exemples : 30m, 2h, 14:30")
  491. return
  492. reminder_id = f"{user_id}_{int(target_time.timestamp())}"
  493. bot = context.application.bot
  494. scheduler.add_job(
  495. send_reminder,
  496. "date",
  497. run_date=target_time,
  498. args=[bot, user_id, message, reminder_id],
  499. id=reminder_id
  500. )
  501. reminders = load_reminders()
  502. reminders.append({
  503. "id": reminder_id,
  504. "user_id": user_id,
  505. "message": message,
  506. "time": target_time.isoformat()
  507. })
  508. save_reminders(reminders)
  509. heure = target_time.strftime("%H:%M")
  510. await update.message.reply_text(f"⏰ Rappel créé pour {heure} : \"{message}\"")
  511. async def rappels_cmd(update: Update, context: ContextTypes.DEFAULT_TYPE):
  512. user_id = update.effective_user.id
  513. reminders = [r for r in load_reminders() if r["user_id"] == user_id]
  514. if not reminders:
  515. await update.message.reply_text("Aucun rappel en attente.")
  516. return
  517. msg = "⏰ Tes rappels :\n\n"
  518. for r in reminders:
  519. t = datetime.fromisoformat(r["time"]).strftime("%H:%M")
  520. msg += f"• {t} — {r['message']}\n"
  521. await update.message.reply_text(msg)
  522. async def agents_cmd(update: Update, context: ContextTypes.DEFAULT_TYPE):
  523. msg = "Agents disponibles :\n\n"
  524. for name, cfg in AGENTS.items():
  525. emoji = AGENT_EMOJIS.get(name, "🤖")
  526. msg += f"{emoji} *{name.capitalize()}* — {cfg['model']}\n"
  527. await update.message.reply_text(msg, parse_mode="Markdown")
  528. async def reset(update: Update, context: ContextTypes.DEFAULT_TYPE):
  529. user_id = update.effective_user.id
  530. for agent in AGENTS:
  531. path = memory_path(user_id, agent)
  532. if os.path.exists(path):
  533. os.remove(path)
  534. await update.message.reply_text("Mémoire réinitialisée. Profil conservé.")
  535. async def set_nom(update: Update, context: ContextTypes.DEFAULT_TYPE):
  536. user_id = update.effective_user.id
  537. if not context.args:
  538. await update.message.reply_text("Usage : /setnom Prenom")
  539. return
  540. nom = " ".join(context.args)
  541. profile = load_profile(user_id)
  542. profile["nom"] = nom
  543. save_profile(user_id, profile)
  544. await update.message.reply_text(f"Parfait, je m'en souviendrai {nom} !")
  545. async def set_projet(update: Update, context: ContextTypes.DEFAULT_TYPE):
  546. user_id = update.effective_user.id
  547. if not context.args:
  548. await update.message.reply_text("Usage : /setprojet Nom")
  549. return
  550. projet = " ".join(context.args)
  551. profile = load_profile(user_id)
  552. if projet not in profile["projets"]:
  553. profile["projets"].append(projet)
  554. save_profile(user_id, profile)
  555. await update.message.reply_text(f"Projet \"{projet}\" ajouté.")
  556. async def profil(update: Update, context: ContextTypes.DEFAULT_TYPE):
  557. user_id = update.effective_user.id
  558. profile = load_profile(user_id)
  559. nom = profile.get("nom") or "non défini"
  560. projets = ", ".join(profile.get("projets", [])) or "aucun"
  561. await update.message.reply_text(f"Nom : {nom}\nProjets : {projets}")
  562. async def repos_cmd(update: Update, context: ContextTypes.DEFAULT_TYPE):
  563. repos = list_repos()
  564. if not repos:
  565. await update.message.reply_text("Aucun repo cloné.\n/clone <url> pour en ajouter un.")
  566. return
  567. msg = "Repos disponibles :\n\n"
  568. for r in repos:
  569. msg += f"📁 {r}\n"
  570. msg += "\n/fichiers <repo> · /log <repo> · /analyser <repo> <fichier>"
  571. await update.message.reply_text(msg)
  572. async def clone_cmd(update: Update, context: ContextTypes.DEFAULT_TYPE):
  573. if not context.args:
  574. await update.message.reply_text("Usage : /clone <url>")
  575. return
  576. url = context.args[0]
  577. name = url.rstrip("/").split("/")[-1].replace(".git", "")
  578. dest = os.path.join(REPOS_DIR, name)
  579. await update.message.reply_text(f"Clonage de {name}...")
  580. result = subprocess.run(["git", "clone", url, dest], capture_output=True, text=True)
  581. if result.returncode == 0:
  582. await update.message.reply_text(f"✅ {name} cloné.")
  583. else:
  584. await update.message.reply_text(f"Erreur : {result.stderr}")
  585. async def fichiers_cmd(update: Update, context: ContextTypes.DEFAULT_TYPE):
  586. if not context.args:
  587. await update.message.reply_text("Usage : /fichiers <repo>")
  588. return
  589. repo = context.args[0]
  590. files = list_files(repo)
  591. if files is None:
  592. await update.message.reply_text(f"Repo '{repo}' introuvable.")
  593. return
  594. msg = f"Fichiers de {repo} :\n\n" + "\n".join(files[:50])
  595. if len(files) > 50:
  596. msg += f"\n... et {len(files) - 50} autres"
  597. await update.message.reply_text(msg)
  598. async def log_cmd(update: Update, context: ContextTypes.DEFAULT_TYPE):
  599. if not context.args:
  600. await update.message.reply_text("Usage : /log <repo>")
  601. return
  602. repo = context.args[0]
  603. log = git_log(repo)
  604. await update.message.reply_text(f"Derniers commits de {repo} :\n\n{log}" if log else "Repo introuvable.")
  605. async def lire_cmd(update: Update, context: ContextTypes.DEFAULT_TYPE):
  606. if len(context.args) < 2:
  607. await update.message.reply_text("Usage : /lire <repo> <fichier>")
  608. return
  609. repo = context.args[0]
  610. filepath = " ".join(context.args[1:])
  611. content = read_file(repo, filepath)
  612. if content is None:
  613. await update.message.reply_text(f"Fichier '{filepath}' introuvable.")
  614. return
  615. if len(content) > 3000:
  616. content = content[:3000] + "\n... (tronqué)"
  617. await update.message.reply_text(f"```\n{content}\n```", parse_mode="Markdown")
  618. async def analyser_cmd(update: Update, context: ContextTypes.DEFAULT_TYPE):
  619. user_id = update.effective_user.id
  620. if len(context.args) < 2:
  621. await update.message.reply_text("Usage : /analyser <repo> <fichier>")
  622. return
  623. repo = context.args[0]
  624. filepath = " ".join(context.args[1:])
  625. content = read_file(repo, filepath)
  626. if content is None:
  627. await update.message.reply_text(f"Fichier '{filepath}' introuvable.")
  628. return
  629. await update.message.reply_text(f"🔍 Analyse de {filepath}...")
  630. message = f"Analyse ce fichier et donne résumé, points forts et problèmes :\n\n```\n{content[:4000]}\n```"
  631. reply, _ = await ask_agent(user_id, message, "reviewer")
  632. await update.message.reply_text(reply)
  633. async def pull_cmd(update: Update, context: ContextTypes.DEFAULT_TYPE):
  634. if not context.args:
  635. await update.message.reply_text("Usage : /pull <repo>")
  636. return
  637. result = git_pull(context.args[0])
  638. await update.message.reply_text(result)
  639. async def google_connect_cmd(update: Update, context: ContextTypes.DEFAULT_TYPE):
  640. user_id = update.effective_user.id
  641. if get_google_creds():
  642. await update.message.reply_text("✅ Google déjà connecté.\n/calendrier — voir tes événements\n/mails — voir tes mails")
  643. return
  644. flow = Flow.from_client_secrets_file(
  645. GOOGLE_CREDS_FILE,
  646. scopes=GOOGLE_SCOPES,
  647. redirect_uri="urn:ietf:wg:oauth:2.0:oob"
  648. )
  649. auth_url, _ = flow.authorization_url(prompt="consent")
  650. pending_oauth[user_id] = flow
  651. await update.message.reply_text(
  652. "🔗 Connexion Google\n\n"
  653. "1. Clique sur ce lien et autorise l'accès :\n"
  654. f"{auth_url}\n\n"
  655. "2. Copie le code affiché\n"
  656. "3. Envoie-le moi avec /google_code LECODICI"
  657. )
  658. async def google_code_cmd(update: Update, context: ContextTypes.DEFAULT_TYPE):
  659. user_id = update.effective_user.id
  660. if not context.args:
  661. await update.message.reply_text("Usage : /google_code <code>")
  662. return
  663. if user_id not in pending_oauth:
  664. await update.message.reply_text("Lance d'abord /google_connect")
  665. return
  666. code = context.args[0]
  667. flow = pending_oauth[user_id]
  668. try:
  669. flow.fetch_token(code=code)
  670. creds = flow.credentials
  671. with open(GOOGLE_TOKEN_FILE, "w") as f:
  672. f.write(creds.to_json())
  673. del pending_oauth[user_id]
  674. await update.message.reply_text(
  675. "✅ Google connecté avec succès !\n\n"
  676. "/calendrier — voir tes événements\n"
  677. "/mails — voir tes mails non lus"
  678. )
  679. except Exception as e:
  680. await update.message.reply_text(f"Erreur d'authentification : {e}")
  681. async def calendrier_cmd(update: Update, context: ContextTypes.DEFAULT_TYPE):
  682. await update.message.reply_text("📅 Chargement du calendrier...")
  683. events = get_calendar_events()
  684. if events is None:
  685. await update.message.reply_text("Google non connecté. Lance /google_connect")
  686. return
  687. if not events:
  688. await update.message.reply_text("Aucun événement à venir.")
  689. return
  690. msg = "📅 *Prochains événements :*\n\n"
  691. for event in events:
  692. start = event["start"].get("dateTime", event["start"].get("date", ""))
  693. if "T" in start:
  694. dt = datetime.fromisoformat(start.replace("Z", "+00:00"))
  695. start_str = dt.strftime("%d/%m %H:%M")
  696. else:
  697. start_str = start
  698. msg += f"• *{event.get('summary', 'Sans titre')}*\n 📅 {start_str}\n\n"
  699. await update.message.reply_text(msg, parse_mode="Markdown")
  700. async def mails_cmd(update: Update, context: ContextTypes.DEFAULT_TYPE):
  701. await update.message.reply_text("📬 Chargement des mails...")
  702. emails = get_gmail_messages()
  703. if emails is None:
  704. await update.message.reply_text("Google non connecté. Lance /google_connect")
  705. return
  706. if not emails:
  707. await update.message.reply_text("Aucun mail non lu.")
  708. return
  709. msg = "📬 *Mails non lus :*\n\n"
  710. for e in emails:
  711. msg += f"• *{e['subject']}*\n De : {e['from'][:50]}\n\n"
  712. await update.message.reply_text(msg, parse_mode="Markdown")
  713. async def briefing_cmd(update: Update, context: ContextTypes.DEFAULT_TYPE):
  714. user_id = update.effective_user.id
  715. if not context.args:
  716. briefings = load_briefing_config()
  717. cfg = briefings.get(str(user_id))
  718. if cfg:
  719. await update.message.reply_text(
  720. f"⏰ Briefing matinal configuré à {cfg['heure']}.\n\n"
  721. f"/briefing 07:30 — changer l'heure\n"
  722. f"/briefing stop — désactiver\n"
  723. f"/briefing_test — recevoir un briefing maintenant"
  724. )
  725. else:
  726. await update.message.reply_text(
  727. "Aucun briefing configuré.\n\n"
  728. "Usage : /briefing 08:00\n"
  729. "/briefing_test — tester maintenant"
  730. )
  731. return
  732. if context.args[0].lower() == "stop":
  733. briefings = load_briefing_config()
  734. briefings.pop(str(user_id), None)
  735. save_briefing_config(briefings)
  736. job_id = f"briefing_{user_id}"
  737. if scheduler.get_job(job_id):
  738. scheduler.remove_job(job_id)
  739. await update.message.reply_text("Briefing matinal désactivé.")
  740. return
  741. heure = context.args[0]
  742. if ":" not in heure:
  743. await update.message.reply_text("Format : /briefing 08:00")
  744. return
  745. h, m = heure.split(":")
  746. briefings = load_briefing_config()
  747. briefings[str(user_id)] = {"heure": heure}
  748. save_briefing_config(briefings)
  749. bot = context.application.bot
  750. job_id = f"briefing_{user_id}"
  751. if scheduler.get_job(job_id):
  752. scheduler.remove_job(job_id)
  753. scheduler.add_job(
  754. send_briefing,
  755. "cron",
  756. hour=int(h),
  757. minute=int(m),
  758. args=[bot, user_id],
  759. id=job_id,
  760. replace_existing=True
  761. )
  762. await update.message.reply_text(
  763. f"✅ Briefing matinal configuré à {heure}.\n"
  764. f"Chaque matin tu recevras : météo, news tech, foot, tes notes et l'état du serveur.\n\n"
  765. f"/briefing_test — tester maintenant"
  766. )
  767. async def briefing_test_cmd(update: Update, context: ContextTypes.DEFAULT_TYPE):
  768. user_id = update.effective_user.id
  769. await update.message.reply_text("Génération du briefing...")
  770. bot = context.application.bot
  771. await send_briefing(bot, user_id)
  772. async def note_cmd(update: Update, context: ContextTypes.DEFAULT_TYPE):
  773. user_id = update.effective_user.id
  774. if not context.args:
  775. await update.message.reply_text("Usage : /note <contenu>\nEx: /note Je préfère Rust pour le backend")
  776. return
  777. content = " ".join(context.args)
  778. note = add_note(user_id, content)
  779. await update.message.reply_text(f"📝 Note #{note['id']} sauvegardée.")
  780. async def notes_cmd(update: Update, context: ContextTypes.DEFAULT_TYPE):
  781. user_id = update.effective_user.id
  782. notes = load_notes(user_id)
  783. if not notes:
  784. await update.message.reply_text("Aucune note. Utilise /note <contenu> pour en créer une.")
  785. return
  786. msg = "📝 *Tes notes :*\n\n"
  787. for n in notes[-10:]:
  788. msg += f"#{n['id']} [{n['date']}]\n{n['content']}\n\n"
  789. await update.message.reply_text(msg, parse_mode="Markdown")
  790. async def chercher_cmd(update: Update, context: ContextTypes.DEFAULT_TYPE):
  791. user_id = update.effective_user.id
  792. if not context.args:
  793. await update.message.reply_text("Usage : /chercher <mot-clé>")
  794. return
  795. query = " ".join(context.args)
  796. await update.message.reply_text(f"🔍 Recherche dans la mémoire : \"{query}\"...")
  797. memory_results, note_results = search_memory(user_id, query)
  798. msg = ""
  799. if note_results:
  800. msg += f"📝 *Notes ({len(note_results)}) :*\n"
  801. for n in note_results[:3]:
  802. msg += f"• [{n['date']}] {n['content']}\n"
  803. msg += "\n"
  804. if memory_results:
  805. msg += f"💬 *Conversations ({len(memory_results)}) :*\n"
  806. for r in memory_results[:5]:
  807. role_icon = "👤" if r["role"] == "user" else "🤖"
  808. msg += f"{role_icon} [{r['agent']}] {r['content'][:200]}\n\n"
  809. if not msg:
  810. msg = f"Aucun résultat pour \"{query}\" dans la mémoire ou les notes."
  811. await update.message.reply_text(msg, parse_mode="Markdown")
  812. async def meteo_cmd(update: Update, context: ContextTypes.DEFAULT_TYPE):
  813. user_id = update.effective_user.id
  814. profile = load_profile(user_id)
  815. city = " ".join(context.args) if context.args else profile.get("ville", DEFAULT_CITY)
  816. result = await get_weather(city)
  817. await update.message.reply_text(result)
  818. async def recherche_cmd(update: Update, context: ContextTypes.DEFAULT_TYPE):
  819. user_id = update.effective_user.id
  820. if not context.args:
  821. await update.message.reply_text("Usage : /recherche <requête>")
  822. return
  823. query = " ".join(context.args)
  824. await update.message.reply_text(f"🔍 Recherche : {query}...")
  825. results = web_search(query)
  826. await update.message.reply_text(results, parse_mode="Markdown")
  827. async def news_cmd(update: Update, context: ContextTypes.DEFAULT_TYPE):
  828. category = context.args[0].lower() if context.args else "tech"
  829. if category not in NEWS_FEEDS:
  830. await update.message.reply_text(f"Catégories disponibles : {', '.join(NEWS_FEEDS.keys())}")
  831. return
  832. await update.message.reply_text(f"📰 Chargement des news {category}...")
  833. news = get_news(category)
  834. await update.message.reply_text(news, parse_mode="Markdown")
  835. async def handle_location(update: Update, context: ContextTypes.DEFAULT_TYPE):
  836. user_id = update.effective_user.id
  837. location = update.message.location
  838. lat, lon = location.latitude, location.longitude
  839. await update.message.reply_text("📍 Localisation reçue, récupération météo...")
  840. try:
  841. async with httpx.AsyncClient(timeout=10) as client:
  842. response = await client.get(f"https://wttr.in/{lat},{lon}?format=3&lang=fr")
  843. result = f"🌤️ {response.text.strip()}"
  844. # Sauvegarder la ville dans le profil
  845. city_resp = await client.get(f"https://wttr.in/{lat},{lon}?format=%l")
  846. city = city_resp.text.strip()
  847. profile = load_profile(user_id)
  848. profile["ville"] = f"{lat},{lon}"
  849. save_profile(user_id, profile)
  850. await update.message.reply_text(f"{result}\n\n📍 Position sauvegardée pour les prochaines fois.")
  851. except Exception as e:
  852. await update.message.reply_text(f"Erreur météo : {e}")
  853. async def handle_message(update: Update, context: ContextTypes.DEFAULT_TYPE):
  854. user_id = update.effective_user.id
  855. text = update.message.text
  856. # Détection automatique de note
  857. note_triggers = ["note que", "souviens-toi que", "retiens que", "n'oublie pas que", "mémorise que"]
  858. text_lower = text.lower()
  859. for trigger in note_triggers:
  860. if text_lower.startswith(trigger):
  861. content = text[len(trigger):].strip()
  862. note = add_note(user_id, content)
  863. await update.message.reply_text(f"📝 Noté ! (#{note['id']}) : \"{content}\"")
  864. return
  865. await update.message.reply_text("⏳")
  866. agent = await detect_agent(text)
  867. emoji = AGENT_EMOJIS.get(agent, "🤖")
  868. await update.message.reply_text(f"{emoji} Agent {agent.capitalize()}...")
  869. reply, _ = await ask_agent(user_id, text, agent)
  870. await update.message.reply_text(reply)
  871. async def handle_voice(update: Update, context: ContextTypes.DEFAULT_TYPE):
  872. user_id = update.effective_user.id
  873. await update.message.reply_text("🎤 Transcription...")
  874. voice = update.message.voice
  875. file = await context.bot.get_file(voice.file_id)
  876. with tempfile.NamedTemporaryFile(suffix=".ogg", delete=False) as tmp:
  877. tmp_path = tmp.name
  878. await file.download_to_drive(tmp_path)
  879. try:
  880. text = await transcribe_voice(tmp_path)
  881. await update.message.reply_text(f"« {text} »")
  882. agent = await detect_agent(text)
  883. emoji = AGENT_EMOJIS.get(agent, "🤖")
  884. await update.message.reply_text(f"{emoji} Agent {agent.capitalize()}...")
  885. reply, _ = await ask_agent(user_id, text, agent)
  886. await update.message.reply_text(reply)
  887. audio_path = text_to_audio(reply)
  888. with open(audio_path, "rb") as audio_file:
  889. await update.message.reply_voice(audio_file)
  890. os.unlink(audio_path)
  891. except Exception as e:
  892. await update.message.reply_text(f"Erreur : {e}")
  893. finally:
  894. os.unlink(tmp_path)
  895. async def handle_photo(update: Update, context: ContextTypes.DEFAULT_TYPE):
  896. await update.message.reply_text("🖼️ Analyse de l'image...")
  897. photo = update.message.photo[-1]
  898. file = await context.bot.get_file(photo.file_id)
  899. with tempfile.NamedTemporaryFile(suffix=".jpg", delete=False) as tmp:
  900. tmp_path = tmp.name
  901. await file.download_to_drive(tmp_path)
  902. try:
  903. caption = update.message.caption or "Décris cette image en détail en français."
  904. result = await analyze_image(tmp_path, caption)
  905. await update.message.reply_text(result)
  906. except Exception as e:
  907. await update.message.reply_text(f"Erreur : {e}")
  908. finally:
  909. os.unlink(tmp_path)
  910. # ─── Main ─────────────────────────────────────────────────────────────────────
  911. BRIEFING_FILE = os.path.join(os.path.expanduser("~/agent"), "briefing.json")
  912. GOOGLE_CREDS_FILE = os.path.expanduser("~/agent/google_credentials.json")
  913. GOOGLE_TOKEN_FILE = os.path.expanduser("~/agent/google_token.json")
  914. GOOGLE_SCOPES = [
  915. "https://www.googleapis.com/auth/calendar.readonly",
  916. "https://www.googleapis.com/auth/gmail.readonly"
  917. ]
  918. # Stockage temporaire des flows OAuth en attente
  919. pending_oauth = {}
  920. def load_briefing_config():
  921. if os.path.exists(BRIEFING_FILE):
  922. with open(BRIEFING_FILE, "r") as f:
  923. return json.load(f)
  924. return {}
  925. def save_briefing_config(config):
  926. with open(BRIEFING_FILE, "w") as f:
  927. json.dump(config, f, ensure_ascii=False, indent=2)
  928. async def send_briefing(bot: Bot, user_id: int):
  929. try:
  930. profile = load_profile(user_id)
  931. nom = profile.get("nom", "")
  932. ville = profile.get("ville", DEFAULT_CITY)
  933. notes = load_notes(user_id)
  934. notes_recentes = notes[-3:] if notes else []
  935. greeting = f"☀️ Bonjour {nom} !\n\n" if nom else "☀️ Bonjour !\n\n"
  936. msg = greeting
  937. # Météo
  938. meteo = await get_weather(ville)
  939. msg += f"{meteo}\n\n"
  940. # News tech
  941. tech = get_news("tech", max_items=3)
  942. msg += f"📰 *Tech du jour :*\n"
  943. lines = [l for l in tech.split("\n") if l.startswith("•")][:3]
  944. msg += "\n".join(lines) + "\n\n"
  945. # Football
  946. foot = get_news("football", max_items=2)
  947. msg += f"⚽ *Football :*\n"
  948. lines = [l for l in foot.split("\n") if l.startswith("•")][:2]
  949. msg += "\n".join(lines) + "\n\n"
  950. # Notes récentes
  951. if notes_recentes:
  952. msg += "📝 *Tes dernières notes :*\n"
  953. for n in notes_recentes:
  954. msg += f"• {n['content']}\n"
  955. msg += "\n"
  956. # Calendrier
  957. events = get_calendar_events(max_results=3)
  958. if events:
  959. msg += "📅 *Agenda du jour :*\n"
  960. for event in events:
  961. start = event["start"].get("dateTime", event["start"].get("date", ""))
  962. if "T" in start:
  963. dt = datetime.fromisoformat(start.replace("Z", "+00:00"))
  964. start_str = dt.strftime("%H:%M")
  965. else:
  966. start_str = start
  967. msg += f"• {start_str} — {event.get('summary', 'Sans titre')}\n"
  968. msg += "\n"
  969. # Mails non lus
  970. emails = get_gmail_messages(max_results=3)
  971. if emails:
  972. msg += f"📬 *{len(emails)} mail(s) non lu(s) :*\n"
  973. for e in emails[:3]:
  974. msg += f"• {e['subject'][:50]}\n"
  975. msg += "\n"
  976. # Monitoring
  977. cpu = psutil.cpu_percent(interval=0.5)
  978. ram = psutil.virtual_memory()
  979. msg += f"🖥️ Serveur : CPU {cpu:.0f}% · RAM {ram.percent:.0f}%"
  980. await bot.send_message(chat_id=user_id, text=msg, parse_mode="Markdown")
  981. logger.info(f"Briefing envoyé à {user_id}")
  982. except Exception as e:
  983. logger.error(f"Erreur briefing : {e}")
  984. async def post_init(application):
  985. scheduler.start()
  986. bot = application.bot
  987. # Recharger les rappels persistés
  988. reminders = load_reminders()
  989. for r in reminders:
  990. target = datetime.fromisoformat(r["time"])
  991. if target > datetime.now():
  992. scheduler.add_job(
  993. send_reminder,
  994. "date",
  995. run_date=target,
  996. args=[bot, r["user_id"], r["message"], r["id"]],
  997. id=r["id"],
  998. replace_existing=True
  999. )
  1000. logger.info(f"{len(reminders)} rappel(s) rechargé(s).")
  1001. # Recharger les briefings configurés
  1002. briefings = load_briefing_config()
  1003. for user_id_str, cfg in briefings.items():
  1004. user_id = int(user_id_str)
  1005. heure = cfg.get("heure", "08:00")
  1006. h, m = heure.split(":")
  1007. scheduler.add_job(
  1008. send_briefing,
  1009. "cron",
  1010. hour=int(h),
  1011. minute=int(m),
  1012. args=[bot, user_id],
  1013. id=f"briefing_{user_id}",
  1014. replace_existing=True
  1015. )
  1016. logger.info(f"Briefing programmé à {heure} pour {user_id}")
  1017. # Démarrer l'API HTTP pour le client PC
  1018. api_app = web.Application()
  1019. api_app.router.add_post("/api/chat", api_chat_handler)
  1020. runner = web.AppRunner(api_app)
  1021. await runner.setup()
  1022. site = web.TCPSite(runner, "0.0.0.0", 8765)
  1023. await site.start()
  1024. logger.info("API HTTP démarrée sur port 8765")
  1025. async def api_chat_handler(request):
  1026. try:
  1027. data = await request.json()
  1028. text = data.get("text", "").strip()
  1029. user_id = data.get("user_id", "pc_user")
  1030. if not text:
  1031. return web.json_response({"error": "texte vide"}, status=400)
  1032. agent = await detect_agent(text)
  1033. response = await ask_agent(user_id, text, agent)
  1034. return web.json_response({"response": response, "agent": agent})
  1035. except Exception as e:
  1036. logger.error(f"API erreur : {e}")
  1037. return web.json_response({"error": str(e)}, status=500)
  1038. if __name__ == "__main__":
  1039. app = ApplicationBuilder().token(TELEGRAM_TOKEN).post_init(post_init).build()
  1040. app.add_handler(CommandHandler("start", start))
  1041. app.add_handler(CommandHandler("reset", reset))
  1042. app.add_handler(CommandHandler("agents", agents_cmd))
  1043. app.add_handler(CommandHandler("setnom", set_nom))
  1044. app.add_handler(CommandHandler("setprojet", set_projet))
  1045. app.add_handler(CommandHandler("profil", profil))
  1046. app.add_handler(CommandHandler("monitoring", monitoring_cmd))
  1047. app.add_handler(CommandHandler("rappel", rappel_cmd))
  1048. app.add_handler(CommandHandler("rappels", rappels_cmd))
  1049. app.add_handler(CommandHandler("repos", repos_cmd))
  1050. app.add_handler(CommandHandler("clone", clone_cmd))
  1051. app.add_handler(CommandHandler("fichiers", fichiers_cmd))
  1052. app.add_handler(CommandHandler("lire", lire_cmd))
  1053. app.add_handler(CommandHandler("analyser", analyser_cmd))
  1054. app.add_handler(CommandHandler("log", log_cmd))
  1055. app.add_handler(CommandHandler("pull", pull_cmd))
  1056. app.add_handler(CommandHandler("briefing", briefing_cmd))
  1057. app.add_handler(CommandHandler("briefing_test", briefing_test_cmd))
  1058. app.add_handler(CommandHandler("note", note_cmd))
  1059. app.add_handler(CommandHandler("notes", notes_cmd))
  1060. app.add_handler(CommandHandler("chercher", chercher_cmd))
  1061. app.add_handler(CommandHandler("meteo", meteo_cmd))
  1062. app.add_handler(CommandHandler("recherche", recherche_cmd))
  1063. app.add_handler(CommandHandler("news", news_cmd))
  1064. app.add_handler(CommandHandler("google_connect", google_connect_cmd))
  1065. app.add_handler(CommandHandler("google_code", google_code_cmd))
  1066. app.add_handler(CommandHandler("calendrier", calendrier_cmd))
  1067. app.add_handler(CommandHandler("mails", mails_cmd))
  1068. app.add_handler(MessageHandler(filters.LOCATION, handle_location))
  1069. app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, handle_message))
  1070. app.add_handler(MessageHandler(filters.VOICE, handle_voice))
  1071. app.add_handler(MessageHandler(filters.PHOTO, handle_photo))
  1072. logger.info("Konan demarre - Google + Météo + News + Recherche + Monitoring + Rappels + Git + Vision + Voix + Agents.")
  1073. app.run_polling()