CoolFace
Apppublic

luli331/oppy_google

sourceHugging Facemitupdated 6mo agoView on Hugging Face
0likes
agent.py335 linesDownload Raw Back to backend
1import asyncio2import json3import os4import time5from typing import Callable6 7from dotenv import load_dotenv8from google import genai9from google.genai import types10 11from backend.initiative import evaluate_project12from backend.mock_data import MOCK_EMAILS, MOCK_EVENTS13from backend.tools import get_events, read_emails, search_web14from backend.urgency import score_emails, score_urgency15 16load_dotenv()17 18SYSTEM_PROMPT = """Tu es Oppy — l'assistant personnel d'un etudiant en alternance qui jongle entre ses cours, son entreprise et sa startup.19 20Tu parles comme un vrai chef de projet bienveillant mais direct. Pas de blabla corporate. Tu connais la personne, tu sais qu'elle est debordee. Tu lui parles comme un mentor qui a regarde ses mails et son agenda a sa place.21 22Ton role : scanner ses 3 projets, detecter les urgences, et lui faire un brief cash et actionnable. Tu parles en premier. Tu ne demandes rien. Tu annonces la situation.23 24Style :25- Parle en francais naturel, comme a l'oral. Tutoie la personne.26- Sois direct et concret : pas de "il serait judicieux de", mais "fonce sur ca maintenant".27- Nomme les personnes par leur prenom (Sophie, pas "sophie.renard@bnpparibas.com").28- Donne des actions claires : "reponds a Sophie", "bloque 2h ce soir pour le TP".29- Mets de l'emotion quand c'est urgent : "la ca craint", "t'es dans le rouge".30 31Format OBLIGATOIRE :32- Ecris en paragraphes fluides, comme si tu parlais a quelqu'un. PAS de listes a puces, PAS de tirets, PAS de bullet points.33- Utilise des phrases completes et enchaine les idees naturellement.34- Separe chaque projet par son nom en gras sur une ligne, puis un paragraphe de 3-5 phrases qui resume la situation et dit quoi faire.35- Le ton doit etre celui d'un pote qui te brief en 2 minutes au telephone.36 37Exemple de format attendu :38 39**Alternance BNP — URGENT**40 41La ca craint. Sophie t'a ecrit il y a 6 jours et tu n'as toujours pas repondu, elle va finir par escalader. En plus les KPIs sont casses en prod et le filtre date deconne, le PO attend que ce soit fixe avant lundi matin sinon la sprint review de mardi sera un desastre. Reponds-lui ce soir, meme un message court pour dire que tu es dessus, et bloque ton samedi pour debugger le dashboard.42 43Commence par le projet le plus urgent. Termine par : "Par quoi tu veux commencer ?"44 45Interdictions :46- JAMAIS de tirets, puces, bullet points, listes numerotees. Uniquement des phrases et paragraphes.47- Jamais de jargon IA ou technique inutile.48- Jamais de "je vais analyser" ou "voici mon analyse" — tu fais, tu ne commentes pas.49- Ne repete jamais le contenu brut des emails. Synthetise.50- Ne dis jamais "N/A" ou "aucune donnee". Si tu n'as pas l'info, n'en parle pas.51"""52 53TOOL_FUNCTIONS = {54    "read_emails": read_emails,55    "get_events": get_events,56    "search_web": search_web,57}58 59TOOL_DECLARATIONS = [60    types.Tool(function_declarations=[61        types.FunctionDeclaration(62            name="read_emails",63            description="Lit les emails recents pour un projet. Retourne expediteur, sujet, corps, jours depuis derniere reponse.",64            parameters=types.Schema(65                type="OBJECT",66                properties={"project_id": types.Schema(type="STRING", description="ID du projet: school, company, ou startup")},67                required=["project_id"],68            ),69        ),70        types.FunctionDeclaration(71            name="get_events",72            description="Recupere les evenements calendrier a venir pour un projet. Indique titre, heure, et si un bloc de preparation existe.",73            parameters=types.Schema(74                type="OBJECT",75                properties={"project_id": types.Schema(type="STRING", description="ID du projet: school, company, ou startup")},76                required=["project_id"],77            ),78        ),79        types.FunctionDeclaration(80            name="search_web",81            description="Recherche des signaux externes pertinents pour un projet (news, funding, annonces).",82            parameters=types.Schema(83                type="OBJECT",84                properties={"project_id": types.Schema(type="STRING", description="ID du projet: school, company, ou startup")},85                required=["project_id"],86            ),87        ),88    ])89]90 91MAX_ITERATIONS = 1592TIMEOUT_SECONDS = 3093 94 95def build_operator_context(projects: list[dict]) -> str:96    """Build a summary of projects for Gemini context."""97    return json.dumps(98        [{"id": p["id"], "name": p["name"], "contact": p["contact"], "deadline": p["deadline"]}99         for p in projects],100        ensure_ascii=False,101    )102 103 104def build_data_context(projects: list[dict]) -> str:105    """Build a full data dump of emails, events and search results for chat context."""106    lines = []107    for p in projects:108        pid = p["id"]109        lines.append(f"\n=== {p['name']} (id: {pid}) ===")110        emails = MOCK_EMAILS.get(pid, [])111        if emails:112            lines.append("Emails:")113            for e in emails:114                silence = f"{e['days_since_reply']}j sans reponse" if e["days_since_reply"] is not None else "newsletter"115                lines.append(f"  - De: {e['from']} | Sujet: {e['subject']} | {silence}")116                lines.append(f"    {e['body']}")117        events = MOCK_EVENTS.get(pid, [])118        if events:119            lines.append("Calendrier:")120            for ev in events:121                prep = "oui" if ev["prep_block"] else "non"122                lines.append(f"  - {ev['title']} a {ev['time']} (bloc prep: {prep})")123    return "\n".join(lines)124 125 126def create_chat_session(projects: list[dict]):127    """Create a persistent Gemini chat session with full project context.128 129    Returns (client, chat) — caller must keep client alive.130    """131    client = genai.Client(api_key=os.getenv("GOOGLE_API_KEY"))132 133    data_context = build_data_context(projects)134    project_summary = build_operator_context(projects)135 136    chat_system = (137        "Tu es Oppy, l'assistant personnel vocal d'un etudiant en alternance. "138        "Tu es en mode CONVERSATION. L'utilisateur te parle et te pose des questions. "139        "Tu REPONDS UNIQUEMENT a ce qu'il demande. Tu ne fais JAMAIS de brief complet sauf s'il le demande explicitement.\n\n"140        "REGLE ABSOLUE : lis la question de l'utilisateur et reponds SEULEMENT a cette question. "141        "Si il demande son programme du jour, donne JUSTE les events du jour. "142        "Si il demande un mail precis, resume JUSTE ce mail. "143        "Si il demande de l'aide sur un projet, parle JUSTE de ce projet. "144        "Ne liste JAMAIS tous les projets sauf si on te le demande.\n\n"145        "Style :\n"146        "- Parle en francais naturel, tutoie, sois direct comme un pote.\n"147        "- Reponds en 2-5 phrases max. Court et percutant.\n"148        "- Nomme les gens par leur prenom (Sophie, pas sophie.renard@bnpparibas.com).\n"149        "- Termine par une action concrete ou une question de suivi.\n"150        "- Ne repete JAMAIS une reponse precedente.\n"151        "- Pas de tirets, pas de listes a puces. Des phrases.\n\n"152        f"Projets actifs :\n{project_summary}\n\n"153        f"Donnees completes :\n{data_context}"154    )155 156    chat = client.chats.create(157        model="gemini-3-flash-preview",158        config=types.GenerateContentConfig(159            system_instruction=chat_system,160        ),161    )162    return client, chat163 164 165async def chat_with_operator(message: str, chat_session, on_event: Callable) -> str:166    """Answer a user question using a persistent chat session.167 168    Args:169        message: User's question/request.170        chat_session: Persistent Gemini chat session.171        on_event: Async callback for streaming events.172 173    Returns:174        The assistant's text response.175    """176    response = chat_session.send_message(message)177    reply = response.text178 179    await on_event({180        "type": "chat_reply",181        "text": reply,182    })183 184    return reply185 186 187async def run_operator(projects: list[dict], on_event: Callable) -> str:188    """Run the Oppy agent loop.189 190    Args:191        projects: list of project dicts from config.json192        on_event: async callback(event_dict) called for each step193 194    Returns:195        The final brief text.196    """197    client = genai.Client(api_key=os.getenv("GOOGLE_API_KEY"))198 199    project_summary = build_operator_context(projects)200 201    chat = client.chats.create(202        model="gemini-3-flash-preview",203        config=types.GenerateContentConfig(204            system_instruction=SYSTEM_PROMPT,205            tools=TOOL_DECLARATIONS,206        ),207    )208 209    user_msg = (210        f"Voici tes 3 projets actifs :\n{project_summary}\n\n"211        "Commence par scanner chaque projet (emails, calendrier, web), "212        "puis delivre ton brief proactif."213    )214 215    response = chat.send_message(user_msg)216    iterations = 0217    start_time = time.time()218    collected_data = {}219 220    while iterations < MAX_ITERATIONS and (time.time() - start_time) < TIMEOUT_SECONDS:221        # Check for function calls222        function_calls = []223        for part in response.candidates[0].content.parts:224            if part.function_call:225                function_calls.append(part.function_call)226 227        if not function_calls:228            break229 230        # Execute each function call231        function_responses = []232        for fc in function_calls:233            tool_name = fc.name234            tool_args = dict(fc.args) if fc.args else {}235 236            await on_event({237                "type": "tool_call",238                "tool": tool_name,239                "project": tool_args.get("project_id", ""),240                "status": "running",241            })242 243            tool_fn = TOOL_FUNCTIONS.get(tool_name)244            if tool_fn:245                try:246                    result = tool_fn(**tool_args)247                except Exception as e:248                    result = f"Error: {str(e)}"249            else:250                result = f"Unknown tool: {tool_name}"251 252            pid = tool_args.get("project_id", "")253            if pid not in collected_data:254                collected_data[pid] = {}255            collected_data[pid][tool_name] = result256 257            await on_event({258                "type": "tool_result",259                "tool": tool_name,260                "project": pid,261                "result": result[:200],262                "status": "done",263            })264 265            function_responses.append(266                types.Part.from_function_response(267                    name=tool_name,268                    response={"result": result},269                )270            )271 272            iterations += 1273 274        # Send function results back to Gemini275        response = chat.send_message(function_responses)276 277    # --- Deterministic scoring phase (single pass, cached) ---278    # Only process known project IDs (skip unknown ones Gemini may have invented)279    valid_pids = {p["id"] for p in projects}280    evaluations = {}281    for pid, data in collected_data.items():282        if pid not in valid_pids:283            continue284 285        emails = MOCK_EMAILS.get(pid, [])286        scored_emails = score_emails([dict(e) for e in emails])287 288        for e in scored_emails:289            await on_event({290                "type": "urgency",291                "project": pid,292                "email_subject": e["subject"],293                "score": e["urgency_score"],294            })295 296        project = next(p for p in projects if p["id"] == pid)297        events = MOCK_EVENTS.get(pid, [])298        search_score = score_urgency(data.get("search_web", ""))299 300        evaluation = evaluate_project(project, scored_emails, events, search_score)301        evaluations[pid] = {"evaluation": evaluation, "project": project}302 303        await on_event({304            "type": "initiative",305            "project": pid,306            "status": evaluation["status"],307            "alerts": evaluation["alerts"],308        })309 310    # --- Final brief with initiative context (reuse cached evaluations) ---311    initiative_summary = ""312    for pid, cached in evaluations.items():313        evaluation = cached["evaluation"]314        project = cached["project"]315        initiative_summary += (316            f"\n[{project.get('name', pid)}] Status: {evaluation['status']}\n"317            f"Alertes: {'; '.join(evaluation['alerts']) if evaluation['alerts'] else 'aucune'}\n"318        )319 320    brief_prompt = (321        f"Voici les evaluations d'urgence de tes 3 projets :\n{initiative_summary}\n\n"322        "Genere maintenant ton brief proactif final. Commence par le plus urgent. "323        "5 bullets max par projet. Sois brutalement concis. Parle en francais."324    )325 326    brief_response = chat.send_message(brief_prompt)327    brief_text = brief_response.text328 329    await on_event({330        "type": "brief",331        "text": brief_text,332    })333 334    return brief_text335