|
@@ -280,6 +280,7 @@ async def detect_intent(text: str) -> dict:
|
|
|
'{"intent":"calendrier"} — agenda Google Calendar\n'
|
|
'{"intent":"calendrier"} — agenda Google Calendar\n'
|
|
|
'{"intent":"mails"} — mails Gmail non lus\n'
|
|
'{"intent":"mails"} — mails Gmail non lus\n'
|
|
|
'{"intent":"rappel","time":"30m","message":"texte"} — créer un rappel\n'
|
|
'{"intent":"rappel","time":"30m","message":"texte"} — créer un rappel\n'
|
|
|
|
|
+ '{"intent":"repondre_mail","expediteur":"nom ou email","consigne":"ce qu il faut dire"} — répondre à un email\n'
|
|
|
'{"intent":"conversation"} — tout le reste\n\n'
|
|
'{"intent":"conversation"} — tout le reste\n\n'
|
|
|
f'Message : "{text}"\n\nJSON :'
|
|
f'Message : "{text}"\n\nJSON :'
|
|
|
)
|
|
)
|
|
@@ -386,6 +387,49 @@ async def execute_intent(intent_data: dict, user_id: int, update, context) -> bo
|
|
|
await update.message.reply_text(f"⏰ Rappel créé : \"{message}\" à {target_time.strftime('%H:%M le %d/%m')}")
|
|
await update.message.reply_text(f"⏰ Rappel créé : \"{message}\" à {target_time.strftime('%H:%M le %d/%m')}")
|
|
|
return True
|
|
return True
|
|
|
|
|
|
|
|
|
|
+ elif intent == "repondre_mail":
|
|
|
|
|
+ expediteur = intent_data.get("expediteur", "")
|
|
|
|
|
+ consigne = intent_data.get("consigne", "")
|
|
|
|
|
+ emails = get_gmail_messages(max_results=10)
|
|
|
|
|
+ if emails is None:
|
|
|
|
|
+ await update.message.reply_text("Google non connecté. Lance /google_connect")
|
|
|
|
|
+ return True
|
|
|
|
|
+ # Trouver l'email correspondant
|
|
|
|
|
+ email_found = None
|
|
|
|
|
+ for e in emails:
|
|
|
|
|
+ if expediteur.lower() in e["from"].lower() or expediteur.lower() in e["subject"].lower():
|
|
|
|
|
+ email_found = e
|
|
|
|
|
+ break
|
|
|
|
|
+ if not email_found:
|
|
|
|
|
+ await update.message.reply_text(f"Je n'ai pas trouvé d'email de \"{expediteur}\" dans tes non-lus.")
|
|
|
|
|
+ return True
|
|
|
|
|
+ # Récupérer le corps
|
|
|
|
|
+ await update.message.reply_text(f"📧 Email trouvé de {email_found['from']}\nSujet : {email_found['subject']}\n\nRédaction de la réponse...")
|
|
|
|
|
+ body = get_email_body(email_found["id"])
|
|
|
|
|
+ prompt = (
|
|
|
|
|
+ f"Tu dois rédiger une réponse à cet email en français.\n\n"
|
|
|
|
|
+ f"De : {email_found['from']}\n"
|
|
|
|
|
+ f"Sujet : {email_found['subject']}\n"
|
|
|
|
|
+ f"Contenu : {body}\n\n"
|
|
|
|
|
+ f"Consigne : {consigne}\n\n"
|
|
|
|
|
+ "Rédige uniquement le corps de la réponse, sans salutation excessive, de façon professionnelle et concise."
|
|
|
|
|
+ )
|
|
|
|
|
+ reply, _ = await ask_agent(user_id, prompt, "konan")
|
|
|
|
|
+ # Stocker en attente de confirmation
|
|
|
|
|
+ pending_replies[user_id] = {
|
|
|
|
|
+ "to": email_found["from"],
|
|
|
|
|
+ "subject": email_found["subject"],
|
|
|
|
|
+ "thread_id": email_found["thread_id"],
|
|
|
|
|
+ "message_id": email_found["message_id"],
|
|
|
|
|
+ "draft": reply
|
|
|
|
|
+ }
|
|
|
|
|
+ await update.message.reply_text(
|
|
|
|
|
+ f"📝 *Voici ma réponse proposée :*\n\n{reply}\n\n"
|
|
|
|
|
+ "Réponds *oui* pour envoyer, *non* pour annuler.",
|
|
|
|
|
+ parse_mode="Markdown"
|
|
|
|
|
+ )
|
|
|
|
|
+ return True
|
|
|
|
|
+
|
|
|
return False
|
|
return False
|
|
|
|
|
|
|
|
# ─── Ollama ───────────────────────────────────────────────────────────────────
|
|
# ─── Ollama ───────────────────────────────────────────────────────────────────
|
|
@@ -525,12 +569,64 @@ def get_gmail_messages(max_results=5):
|
|
|
).execute()
|
|
).execute()
|
|
|
headers = {h["name"]: h["value"] for h in detail["payload"]["headers"]}
|
|
headers = {h["name"]: h["value"] for h in detail["payload"]["headers"]}
|
|
|
emails.append({
|
|
emails.append({
|
|
|
|
|
+ "id": msg["id"],
|
|
|
|
|
+ "thread_id": detail.get("threadId", ""),
|
|
|
|
|
+ "message_id": headers.get("Message-ID", ""),
|
|
|
"from": headers.get("From", ""),
|
|
"from": headers.get("From", ""),
|
|
|
"subject": headers.get("Subject", ""),
|
|
"subject": headers.get("Subject", ""),
|
|
|
"date": headers.get("Date", "")
|
|
"date": headers.get("Date", "")
|
|
|
})
|
|
})
|
|
|
return emails
|
|
return emails
|
|
|
|
|
|
|
|
|
|
+def get_email_body(msg_id: str) -> str:
|
|
|
|
|
+ creds = get_google_creds()
|
|
|
|
|
+ if not creds:
|
|
|
|
|
+ return ""
|
|
|
|
|
+ try:
|
|
|
|
|
+ service = build("gmail", "v1", credentials=creds)
|
|
|
|
|
+ detail = service.users().messages().get(userId="me", id=msg_id, format="full").execute()
|
|
|
|
|
+ parts = detail.get("payload", {}).get("parts", [])
|
|
|
|
|
+ for part in parts:
|
|
|
|
|
+ if part.get("mimeType") == "text/plain":
|
|
|
|
|
+ data = part["body"].get("data", "")
|
|
|
|
|
+ import base64 as b64
|
|
|
|
|
+ return b64.urlsafe_b64decode(data + "==").decode("utf-8", errors="ignore")[:1500]
|
|
|
|
|
+ # Pas de parts — corps direct
|
|
|
|
|
+ body_data = detail.get("payload", {}).get("body", {}).get("data", "")
|
|
|
|
|
+ if body_data:
|
|
|
|
|
+ import base64 as b64
|
|
|
|
|
+ return b64.urlsafe_b64decode(body_data + "==").decode("utf-8", errors="ignore")[:1500]
|
|
|
|
|
+ except Exception:
|
|
|
|
|
+ pass
|
|
|
|
|
+ return ""
|
|
|
|
|
+
|
|
|
|
|
+def send_email_reply(to: str, subject: str, body: str, thread_id: str, in_reply_to: str) -> bool:
|
|
|
|
|
+ import email.mime.text
|
|
|
|
|
+ import base64 as b64
|
|
|
|
|
+ creds = get_google_creds()
|
|
|
|
|
+ if not creds:
|
|
|
|
|
+ return False
|
|
|
|
|
+ try:
|
|
|
|
|
+ service = build("gmail", "v1", credentials=creds)
|
|
|
|
|
+ msg = email.mime.text.MIMEText(body)
|
|
|
|
|
+ msg["To"] = to
|
|
|
|
|
+ msg["Subject"] = subject if subject.startswith("Re:") else f"Re: {subject}"
|
|
|
|
|
+ if in_reply_to:
|
|
|
|
|
+ msg["In-Reply-To"] = in_reply_to
|
|
|
|
|
+ msg["References"] = in_reply_to
|
|
|
|
|
+ raw = b64.urlsafe_b64encode(msg.as_bytes()).decode()
|
|
|
|
|
+ service.users().messages().send(
|
|
|
|
|
+ userId="me",
|
|
|
|
|
+ body={"raw": raw, "threadId": thread_id}
|
|
|
|
|
+ ).execute()
|
|
|
|
|
+ return True
|
|
|
|
|
+ except Exception as e:
|
|
|
|
|
+ logger.error(f"Erreur envoi email : {e}")
|
|
|
|
|
+ return False
|
|
|
|
|
+
|
|
|
|
|
+# En attente de confirmation d'envoi d'email
|
|
|
|
|
+pending_replies: dict = {}
|
|
|
|
|
+
|
|
|
# ─── Notes ────────────────────────────────────────────────────────────────────
|
|
# ─── Notes ────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
def notes_path(user_id):
|
|
def notes_path(user_id):
|
|
@@ -1099,6 +1195,21 @@ async def handle_message(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
|
await update.message.reply_text(f"📝 Noté ! (#{note['id']}) : \"{content}\"")
|
|
await update.message.reply_text(f"📝 Noté ! (#{note['id']}) : \"{content}\"")
|
|
|
return
|
|
return
|
|
|
|
|
|
|
|
|
|
+ # Confirmation d'envoi d'email en attente
|
|
|
|
|
+ if user_id in pending_replies:
|
|
|
|
|
+ if text_lower.strip() in ["oui", "yes", "envoie", "ok", "confirme"]:
|
|
|
|
|
+ pr = pending_replies.pop(user_id)
|
|
|
|
|
+ ok = send_email_reply(pr["to"], pr["subject"], pr["draft"], pr["thread_id"], pr["message_id"])
|
|
|
|
|
+ if ok:
|
|
|
|
|
+ await update.message.reply_text("✅ Email envoyé !")
|
|
|
|
|
+ else:
|
|
|
|
|
+ await update.message.reply_text("Erreur lors de l'envoi. Vérifie la connexion Google.")
|
|
|
|
|
+ return
|
|
|
|
|
+ elif text_lower.strip() in ["non", "no", "annule", "cancel"]:
|
|
|
|
|
+ pending_replies.pop(user_id)
|
|
|
|
|
+ await update.message.reply_text("❌ Email annulé.")
|
|
|
|
|
+ return
|
|
|
|
|
+
|
|
|
await update.message.reply_text("⏳")
|
|
await update.message.reply_text("⏳")
|
|
|
intent_data = await detect_intent(text)
|
|
intent_data = await detect_intent(text)
|
|
|
if await execute_intent(intent_data, user_id, update, context):
|
|
if await execute_intent(intent_data, user_id, update, context):
|
|
@@ -1159,7 +1270,8 @@ GOOGLE_CREDS_FILE = os.path.expanduser("~/agent/google_credentials.json")
|
|
|
GOOGLE_TOKEN_FILE = os.path.expanduser("~/agent/google_token.json")
|
|
GOOGLE_TOKEN_FILE = os.path.expanduser("~/agent/google_token.json")
|
|
|
GOOGLE_SCOPES = [
|
|
GOOGLE_SCOPES = [
|
|
|
"https://www.googleapis.com/auth/calendar.readonly",
|
|
"https://www.googleapis.com/auth/calendar.readonly",
|
|
|
- "https://www.googleapis.com/auth/gmail.readonly"
|
|
|
|
|
|
|
+ "https://www.googleapis.com/auth/gmail.readonly",
|
|
|
|
|
+ "https://www.googleapis.com/auth/gmail.send"
|
|
|
]
|
|
]
|
|
|
|
|
|
|
|
# Stockage temporaire des flows OAuth en attente
|
|
# Stockage temporaire des flows OAuth en attente
|