|
@@ -265,6 +265,129 @@ async def detect_agent(message: str) -> str:
|
|
|
pass
|
|
pass
|
|
|
return "konan"
|
|
return "konan"
|
|
|
|
|
|
|
|
|
|
+# ─── Détection d'intention (tool calling) ─────────────────────────────────────
|
|
|
|
|
+
|
|
|
|
|
+import re as _re
|
|
|
|
|
+
|
|
|
|
|
+async def detect_intent(text: str) -> dict:
|
|
|
|
|
+ prompt = (
|
|
|
|
|
+ "Analyse ce message en français et réponds UNIQUEMENT avec un objet JSON sur une seule ligne.\n\n"
|
|
|
|
|
+ "Intents possibles :\n"
|
|
|
|
|
+ '{"intent":"meteo","city":"NomVille"} — météo (city=null si non précisé)\n'
|
|
|
|
|
+ '{"intent":"news","category":"tech"} — actualités (tech/sport/politique/general)\n'
|
|
|
|
|
+ '{"intent":"recherche","query":"..."} — recherche web\n'
|
|
|
|
|
+ '{"intent":"monitoring"} — état serveur CPU/RAM/disque\n'
|
|
|
|
|
+ '{"intent":"calendrier"} — agenda Google Calendar\n'
|
|
|
|
|
+ '{"intent":"mails"} — mails Gmail non lus\n'
|
|
|
|
|
+ '{"intent":"rappel","time":"30m","message":"texte"} — créer un rappel\n'
|
|
|
|
|
+ '{"intent":"conversation"} — tout le reste\n\n'
|
|
|
|
|
+ f'Message : "{text}"\n\nJSON :'
|
|
|
|
|
+ )
|
|
|
|
|
+ try:
|
|
|
|
|
+ async with httpx.AsyncClient(timeout=15) as client:
|
|
|
|
|
+ resp = await client.post(OLLAMA_URL, json={
|
|
|
|
|
+ "model": "qwen2.5:3b",
|
|
|
|
|
+ "messages": [{"role": "user", "content": prompt}],
|
|
|
|
|
+ "stream": False,
|
|
|
|
|
+ "options": {"num_predict": 60, "temperature": 0}
|
|
|
|
|
+ })
|
|
|
|
|
+ raw = resp.json()["message"]["content"].strip()
|
|
|
|
|
+ match = _re.search(r'\{[^}]+\}', raw)
|
|
|
|
|
+ if match:
|
|
|
|
|
+ return json.loads(match.group())
|
|
|
|
|
+ except Exception:
|
|
|
|
|
+ pass
|
|
|
|
|
+ return {"intent": "conversation"}
|
|
|
|
|
+
|
|
|
|
|
+async def execute_intent(intent_data: dict, user_id: int, update, context) -> bool:
|
|
|
|
|
+ """Exécute l'intention détectée. Retourne True si traitée, False pour conversation."""
|
|
|
|
|
+ intent = intent_data.get("intent", "conversation")
|
|
|
|
|
+
|
|
|
|
|
+ if intent == "meteo":
|
|
|
|
|
+ city = intent_data.get("city") or DEFAULT_CITY
|
|
|
|
|
+ result = await get_weather(city)
|
|
|
|
|
+ await update.message.reply_text(result)
|
|
|
|
|
+ return True
|
|
|
|
|
+
|
|
|
|
|
+ elif intent == "news":
|
|
|
|
|
+ category = intent_data.get("category", "tech")
|
|
|
|
|
+ if category not in NEWS_FEEDS:
|
|
|
|
|
+ category = "tech"
|
|
|
|
|
+ result = get_news(category)
|
|
|
|
|
+ await update.message.reply_text(result, parse_mode="Markdown")
|
|
|
|
|
+ return True
|
|
|
|
|
+
|
|
|
|
|
+ elif intent == "recherche":
|
|
|
|
|
+ query = intent_data.get("query", "")
|
|
|
|
|
+ if not query:
|
|
|
|
|
+ return False
|
|
|
|
|
+ result = web_search(query)
|
|
|
|
|
+ await update.message.reply_text(result, parse_mode="Markdown")
|
|
|
|
|
+ return True
|
|
|
|
|
+
|
|
|
|
|
+ elif intent == "monitoring":
|
|
|
|
|
+ status = get_server_status()
|
|
|
|
|
+ await update.message.reply_text(status, parse_mode="Markdown")
|
|
|
|
|
+ return True
|
|
|
|
|
+
|
|
|
|
|
+ elif intent == "calendrier":
|
|
|
|
|
+ events = get_calendar_events()
|
|
|
|
|
+ if events is None:
|
|
|
|
|
+ await update.message.reply_text("Google non connecté. Lance /google_connect")
|
|
|
|
|
+ elif not events:
|
|
|
|
|
+ await update.message.reply_text("Aucun événement à venir.")
|
|
|
|
|
+ else:
|
|
|
|
|
+ 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")
|
|
|
|
|
+ return True
|
|
|
|
|
+
|
|
|
|
|
+ elif intent == "mails":
|
|
|
|
|
+ emails = get_gmail_messages()
|
|
|
|
|
+ if emails is None:
|
|
|
|
|
+ await update.message.reply_text("Google non connecté. Lance /google_connect")
|
|
|
|
|
+ elif not emails:
|
|
|
|
|
+ await update.message.reply_text("Aucun mail non lu.")
|
|
|
|
|
+ else:
|
|
|
|
|
+ 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")
|
|
|
|
|
+ return True
|
|
|
|
|
+
|
|
|
|
|
+ elif intent == "rappel":
|
|
|
|
|
+ time_str = intent_data.get("time", "")
|
|
|
|
|
+ message = intent_data.get("message", "")
|
|
|
|
|
+ if not time_str or not message:
|
|
|
|
|
+ await update.message.reply_text("Précise l'heure et le message du rappel.")
|
|
|
|
|
+ return True
|
|
|
|
|
+ target_time = parse_time(time_str)
|
|
|
|
|
+ if not target_time:
|
|
|
|
|
+ await update.message.reply_text(f"Format de temps non reconnu : {time_str}")
|
|
|
|
|
+ return True
|
|
|
|
|
+ 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, replace_existing=True
|
|
|
|
|
+ )
|
|
|
|
|
+ reminders = load_reminders()
|
|
|
|
|
+ reminders.append({"id": reminder_id, "user_id": user_id, "message": message, "time": target_time.isoformat()})
|
|
|
|
|
+ save_reminders(reminders)
|
|
|
|
|
+ await update.message.reply_text(f"⏰ Rappel créé : \"{message}\" à {target_time.strftime('%H:%M le %d/%m')}")
|
|
|
|
|
+ return True
|
|
|
|
|
+
|
|
|
|
|
+ return False
|
|
|
|
|
+
|
|
|
# ─── Ollama ───────────────────────────────────────────────────────────────────
|
|
# ─── Ollama ───────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
async def ask_agent(user_id, message, agent="konan"):
|
|
async def ask_agent(user_id, message, agent="konan"):
|
|
@@ -977,6 +1100,9 @@ async def handle_message(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
|
return
|
|
return
|
|
|
|
|
|
|
|
await update.message.reply_text("⏳")
|
|
await update.message.reply_text("⏳")
|
|
|
|
|
+ intent_data = await detect_intent(text)
|
|
|
|
|
+ if await execute_intent(intent_data, user_id, update, context):
|
|
|
|
|
+ return
|
|
|
agent = await detect_agent(text)
|
|
agent = await detect_agent(text)
|
|
|
emoji = AGENT_EMOJIS.get(agent, "🤖")
|
|
emoji = AGENT_EMOJIS.get(agent, "🤖")
|
|
|
await update.message.reply_text(f"{emoji} Agent {agent.capitalize()}...")
|
|
await update.message.reply_text(f"{emoji} Agent {agent.capitalize()}...")
|
|
@@ -994,11 +1120,13 @@ async def handle_voice(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
|
try:
|
|
try:
|
|
|
text = await transcribe_voice(tmp_path)
|
|
text = await transcribe_voice(tmp_path)
|
|
|
await update.message.reply_text(f"« {text} »")
|
|
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)
|
|
|
|
|
|
|
+ intent_data = await detect_intent(text)
|
|
|
|
|
+ if not await execute_intent(intent_data, user_id, update, context):
|
|
|
|
|
+ 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)
|
|
audio_path = text_to_audio(reply)
|
|
|
with open(audio_path, "rb") as audio_file:
|
|
with open(audio_path, "rb") as audio_file:
|
|
|
await update.message.reply_voice(audio_file)
|
|
await update.message.reply_voice(audio_file)
|