diff --git a/MyIA.AI.Notebooks/ML/DataScienceWithAgents/Track2-GoogleADK/Day7-Production/Lab16-Data-Science-Agent.ipynb b/MyIA.AI.Notebooks/ML/DataScienceWithAgents/Track2-GoogleADK/Day7-Production/Lab16-Data-Science-Agent.ipynb index 47cf9c52c4..5c97426785 100644 --- a/MyIA.AI.Notebooks/ML/DataScienceWithAgents/Track2-GoogleADK/Day7-Production/Lab16-Data-Science-Agent.ipynb +++ b/MyIA.AI.Notebooks/ML/DataScienceWithAgents/Track2-GoogleADK/Day7-Production/Lab16-Data-Science-Agent.ipynb @@ -5,10 +5,10 @@ "id": "cell-0", "metadata": { "papermill": { - "duration": 0.003011, - "end_time": "2026-09-03T11:54:36.637973", + "duration": 0.003428, + "end_time": "2026-10-05T14:28:10.985445", "exception": false, - "start_time": "2026-09-03T11:54:36.634962", + "start_time": "2026-10-05T14:28:10.982017", "status": "completed" }, "tags": [] @@ -39,10 +39,10 @@ "id": "cell-1", "metadata": { "papermill": { - "duration": 0.003008, - "end_time": "2026-09-03T11:54:36.644486", + "duration": 0.002565, + "end_time": "2026-10-05T14:28:10.990946", "exception": false, - "start_time": "2026-09-03T11:54:36.641478", + "start_time": "2026-10-05T14:28:10.988381", "status": "completed" }, "tags": [] @@ -59,16 +59,16 @@ "id": "cell-2", "metadata": { "execution": { - "iopub.execute_input": "2026-09-03T11:54:36.652644Z", - "iopub.status.busy": "2026-09-03T11:54:36.652644Z", - "iopub.status.idle": "2026-09-03T11:54:39.367087Z", - "shell.execute_reply": "2026-09-03T11:54:39.367087Z" + "iopub.execute_input": "2026-10-05T14:28:10.997152Z", + "iopub.status.busy": "2026-10-05T14:28:10.996845Z", + "iopub.status.idle": "2026-10-05T14:28:15.664998Z", + "shell.execute_reply": "2026-10-05T14:28:15.663867Z" }, "papermill": { - "duration": 2.720084, - "end_time": "2026-09-03T11:54:39.368094", + "duration": 4.672314, + "end_time": "2026-10-05T14:28:15.665787", "exception": false, - "start_time": "2026-09-03T11:54:36.648010", + "start_time": "2026-10-05T14:28:10.993473", "status": "completed" }, "tags": [] @@ -78,7 +78,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "Imports OK : json, re, dataclasses, config, utils\n" + "Imports OK : config + utils.adk_runtime (runtime Google ADK reel)\n" ] } ], @@ -86,15 +86,16 @@ "import sys\n", "sys.path.insert(0, '..')\n", "\n", + "import asyncio\n", "import json\n", "import re\n", "from typing import List, Dict, Optional\n", "from dataclasses import dataclass\n", "\n", "from config import get_settings\n", - "from utils import LLMClient\n", + "from utils.adk_runtime import build_agent, run_agent_turn\n", "\n", - "print(\"Imports OK : json, re, dataclasses, config, utils\")" + "print(\"Imports OK : config + utils.adk_runtime (runtime Google ADK reel)\")" ] }, { @@ -102,10 +103,10 @@ "id": "4070b9ab", "metadata": { "papermill": { - "duration": 0.003, - "end_time": "2026-09-03T11:54:39.374099", + "duration": 0.00288, + "end_time": "2026-10-05T14:28:15.672104", "exception": false, - "start_time": "2026-09-03T11:54:39.371099", + "start_time": "2026-10-05T14:28:15.669224", "status": "completed" }, "tags": [] @@ -120,16 +121,16 @@ "id": "cell-3", "metadata": { "execution": { - "iopub.execute_input": "2026-09-03T11:54:39.380099Z", - "iopub.status.busy": "2026-09-03T11:54:39.380099Z", - "iopub.status.idle": "2026-09-03T11:54:39.385124Z", - "shell.execute_reply": "2026-09-03T11:54:39.384608Z" + "iopub.execute_input": "2026-10-05T14:28:15.679510Z", + "iopub.status.busy": "2026-10-05T14:28:15.679007Z", + "iopub.status.idle": "2026-10-05T14:28:15.686079Z", + "shell.execute_reply": "2026-10-05T14:28:15.685197Z" }, "papermill": { - "duration": 0.009025, - "end_time": "2026-09-03T11:54:39.385124", + "duration": 0.011879, + "end_time": "2026-10-05T14:28:15.686942", "exception": false, - "start_time": "2026-09-03T11:54:39.376099", + "start_time": "2026-10-05T14:28:15.675063", "status": "completed" }, "tags": [] @@ -139,7 +140,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "Provider: openrouter\n" + "Provider: vllm\n" ] } ], @@ -153,18 +154,18 @@ "id": "cell-4", "metadata": { "papermill": { - "duration": 0.00302, - "end_time": "2026-09-03T11:54:39.391152", + "duration": 0.003371, + "end_time": "2026-10-05T14:28:15.693795", "exception": false, - "start_time": "2026-09-03T11:54:39.388132", + "start_time": "2026-10-05T14:28:15.690424", "status": "completed" }, "tags": [] }, "source": [ - "## 2. NL2SQL Translator\n", + "## 2. NL2SQL Translator (agent ADK)\n", "\n", - "**Pourquoi un traducteur NL2SQL dédié plutôt qu'un prompt générique « réponds à ma question »** : une question business (« Quel est le revenu total par région ? ») ne mappe pas trivialement vers SQL — il faut identifier la table pertinente (`sales`), les colonnes (`region`, `revenue`), l'agrégation (`SUM`) et le regroupement (`GROUP BY`). Le `NL2SQLTranslator` isole cette compétence de **traduction structurée** : le LLM reçoit le schéma + la question et produit une requête SQL précise, pas une réponse en langage naturel. C'est le pont entre le langage humain et le langage machine — la compétence centrale d'un agent data." + "**Pourquoi un traducteur NL2SQL dédié plutôt qu'un prompt générique « réponds à ma question »** : une question business (« Quel est le revenu total par région ? ») ne mappe pas trivialement vers SQL — il faut identifier la table pertinente (`sales`), les colonnes (`region`, `revenue`), l'agrégation (`SUM`) et le regroupement (`GROUP BY`). L'agent ADK `nl2sql_agent` isole cette compétence de **traduction structurée** : construit par `build_agent()` (vrai `google.adk.agents.Agent`, modèle servi par ADK via LiteLLM), il reçoit le schéma + la question dans un tour `run_agent_turn()` et produit une requête SQL précise, pas une réponse en langage naturel. C'est le pont entre le langage humain et le langage machine — la compétence centrale d'un agent data." ] }, { @@ -173,16 +174,16 @@ "id": "cell-5", "metadata": { "execution": { - "iopub.execute_input": "2026-09-03T11:54:39.398549Z", - "iopub.status.busy": "2026-09-03T11:54:39.397840Z", - "iopub.status.idle": "2026-09-03T11:54:39.403300Z", - "shell.execute_reply": "2026-09-03T11:54:39.403300Z" + "iopub.execute_input": "2026-10-05T14:28:15.704018Z", + "iopub.status.busy": "2026-10-05T14:28:15.703498Z", + "iopub.status.idle": "2026-10-05T14:28:15.718876Z", + "shell.execute_reply": "2026-10-05T14:28:15.717650Z" }, "papermill": { - "duration": 0.010628, - "end_time": "2026-09-03T11:54:39.404307", + "duration": 0.022264, + "end_time": "2026-10-05T14:28:15.719895", "exception": false, - "start_time": "2026-09-03T11:54:39.393679", + "start_time": "2026-10-05T14:28:15.697631", "status": "completed" }, "tags": [] @@ -192,7 +193,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "Classes definies : SQLQuery (dataclass), NL2SQLTranslator (langage naturel vers SQL BigQuery)\n" + "Agent ADK defini : nl2sql_agent (traduction NL -> SQL BigQuery)\n" ] } ], @@ -203,38 +204,54 @@ " explanation: str\n", " tables_used: List[str]\n", "\n", - "class NL2SQLTranslator:\n", - " def __init__(self, llm: LLMClient):\n", - " self.llm = llm\n", - "\n", - " def translate(self, question: str, schema: Dict) -> SQLQuery:\n", - " schema_desc = self._format_schema(schema)\n", - " prompt = f\"\"\"Convertis cette question en SQL BigQuery.\n", - "\n", - "SCHEMA:\n", - "{schema_desc}\n", - "\n", - "QUESTION: {question}\n", - "\n", - "EXPLANATION: [explication]\n", - "SQL:\n", - "```sql\n", - "[requete]\n", - "```\"\"\"\n", - " response = self.llm.generate(prompt, temperature=0.2)\n", - " explanation = ''\n", - " sql = ''\n", - " if 'EXPLANATION:' in response:\n", - " match = re.search(r'EXPLANATION:\\s*(.+?)(?=SQL:|$)', response, re.DOTALL)\n", - " explanation = match.group(1).strip() if match else ''\n", - " sql_match = re.search(r'```sql\\s*(.*?)\\s*```', response, re.DOTALL)\n", - " sql = sql_match.group(1).strip() if sql_match else ''\n", - " return SQLQuery(sql=sql, explanation=explanation, tables_used=[])\n", - "\n", - " def _format_schema(self, schema: Dict) -> str:\n", - " return '\\n'.join([f'Table {t}: {cols}' for t, cols in schema.items()])\n", - "\n", - "print(\"Classes definies : SQLQuery (dataclass), NL2SQLTranslator (langage naturel vers SQL BigQuery)\")" + "def _format_schema(schema: Dict) -> str:\n", + " return '\\n'.join([f'Table {t}: {cols}' for t, cols in schema.items()])\n", + "\n", + "def _build_nl2sql_agent():\n", + " \"\"\"Agent ADK reel (google.adk.agents.Agent) pour la traduction NL -> SQL.\n", + "\n", + " Les drapeaux disallow_transfer_* ferment l'outil transfer_to_agent pour ce\n", + " sous-agent : construit SANS sous-agents, il n'a rien a transférer, et un\n", + " petit modele local qui l'appellerait quand meme casserait le tour\n", + " (\"cannot transfer to itself\").\n", + " \"\"\"\n", + " agent = build_agent(\n", + " name='nl2sql_agent',\n", + " description='Traduit une question metier en requete SQL BigQuery.',\n", + " instruction=(\n", + " \"Tu es un traducteur NL2SQL. Pour chaque question, reponds STRICTEMENT \"\n", + " \"au format suivant, sans aucun texte hors format :\\n\"\n", + " \"N ecris jamais d appel d outil (transfer_to_agent ou autre) dans ta reponse.\\n\"\n", + " \"EXPLANATION: \\n\"\n", + " \"SQL:\\n\"\n", + " \"```sql\\n\\n```\"\n", + " ),\n", + " )\n", + " agent.disallow_transfer_to_parent = True\n", + " agent.disallow_transfer_to_peers = True\n", + " return agent\n", + "\n", + "nl2sql_agent = _build_nl2sql_agent()\n", + "\n", + "def _parse_sql_response(response: str) -> SQLQuery:\n", + " explanation = ''\n", + " if 'EXPLANATION:' in response:\n", + " match = re.search(r'EXPLANATION:\\s*(.+?)(?=SQL:|$)', response, re.DOTALL)\n", + " explanation = match.group(1).strip() if match else ''\n", + " sql_match = re.search(r'```sql\\s*(.*?)\\s*```', response, re.DOTALL)\n", + " sql = sql_match.group(1).strip() if sql_match else ''\n", + " return SQLQuery(sql=sql, explanation=explanation, tables_used=[])\n", + "\n", + "async def traduire_sql(question: str, schema: Dict) -> tuple:\n", + " \"\"\"Un tour ADK vers nl2sql_agent : prompt avec schema, reponse parsee.\"\"\"\n", + " prompt = (\n", + " f\"Convertis cette question en SQL BigQuery.\\n\\n\"\n", + " f\"SCHEMA:\\n{_format_schema(schema)}\\n\\nQUESTION: {question}\"\n", + " )\n", + " result = await run_agent_turn(nl2sql_agent, prompt)\n", + " return _parse_sql_response(result.response_text), result\n", + "\n", + "print(\"Agent ADK defini : nl2sql_agent (traduction NL -> SQL BigQuery)\")" ] }, { @@ -242,18 +259,18 @@ "id": "cell-6", "metadata": { "papermill": { - "duration": 0.003514, - "end_time": "2026-09-03T11:54:39.409820", + "duration": 0.004601, + "end_time": "2026-10-05T14:28:15.728715", "exception": false, - "start_time": "2026-09-03T11:54:39.406306", + "start_time": "2026-10-05T14:28:15.724114", "status": "completed" }, "tags": [] }, "source": [ - "## 3. NL2Py Translator\n", + "## 3. NL2Py Translator (agent ADK)\n", "\n", - "**Pourquoi offrir DEUX voies de traduction (NL2SQL et NL2Py) plutôt qu'une seule** : certaines requêtes sont naturelles en SQL (agrégations simples, filtres), d'autres le sont en Python pandas (transformations multi-étapes, statistiques, jointures complexes avec logique conditionnelle). Le `NL2PyTranslator` génère du code pandas là où le SQL serait verbeux ou impossible. L'agent `DataScienceAgent` choisit ensuite la voie selon le type de question — c'est la **sélection de l'outil** qui distingue un agent expert d'un pipeline figé : SQL pour l'interrogation déclarative, Python pour le calcul procédural." + "**Pourquoi offrir DEUX voies de traduction (NL2SQL et NL2Py) plutôt qu'une seule** : certaines requêtes sont naturelles en SQL (agrégations simples, filtres), d'autres le sont en Python pandas (transformations multi-étapes, statistiques, jointures complexes avec logique conditionnelle). L'agent ADK `nl2py_agent` génère du code pandas là où le SQL serait verbeux ou impossible. L'agent racine `data_science_agent` choisit ensuite la voie selon le type de question — c'est la **sélection de l'outil** qui distingue un agent expert d'un pipeline figé : SQL pour l'interrogation déclarative, Python pour le calcul procédural." ] }, { @@ -262,16 +279,16 @@ "id": "cell-7", "metadata": { "execution": { - "iopub.execute_input": "2026-09-03T11:54:39.414909Z", - "iopub.status.busy": "2026-09-03T11:54:39.414909Z", - "iopub.status.idle": "2026-09-03T11:54:39.419762Z", - "shell.execute_reply": "2026-09-03T11:54:39.419762Z" + "iopub.execute_input": "2026-10-05T14:28:15.740249Z", + "iopub.status.busy": "2026-10-05T14:28:15.739908Z", + "iopub.status.idle": "2026-10-05T14:28:15.750535Z", + "shell.execute_reply": "2026-10-05T14:28:15.749260Z" }, "papermill": { - "duration": 0.008941, - "end_time": "2026-09-03T11:54:39.420768", + "duration": 0.019228, + "end_time": "2026-10-05T14:28:15.751838", "exception": false, - "start_time": "2026-09-03T11:54:39.411827", + "start_time": "2026-10-05T14:28:15.732610", "status": "completed" }, "tags": [] @@ -281,7 +298,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "Classes definies : PythonCode (dataclass), NL2PyTranslator (langage naturel vers code Python)\n" + "Agent ADK defini : nl2py_agent (traduction NL -> code Python)\n" ] } ], @@ -291,31 +308,45 @@ " code: str\n", " explanation: str\n", "\n", - "class NL2PyTranslator:\n", - " def __init__(self, llm: LLMClient):\n", - " self.llm = llm\n", - "\n", - " def translate(self, question: str, data_context: str) -> PythonCode:\n", - " prompt = f\"\"\"Genere du code Python pour: {question}\n", - "\n", - "CONTEXTE: {data_context}\n", - "\n", - "EXPLANATION: [explication]\n", - "CODE:\n", - "```python\n", - "[code]\n", - "```\"\"\"\n", - " response = self.llm.generate(prompt, temperature=0.2)\n", - " explanation = ''\n", - " code = ''\n", - " if 'EXPLANATION:' in response:\n", - " match = re.search(r'EXPLANATION:\\s*(.+?)(?=CODE:|$)', response, re.DOTALL)\n", - " explanation = match.group(1).strip() if match else ''\n", - " code_match = re.search(r'```python\\s*(.*?)\\s*```', response, re.DOTALL)\n", - " code = code_match.group(1).strip() if code_match else ''\n", - " return PythonCode(code=code, explanation=explanation)\n", - "\n", - "print(\"Classes definies : PythonCode (dataclass), NL2PyTranslator (langage naturel vers code Python)\")" + "def _build_nl2py_agent():\n", + " \"\"\"Agent ADK reel pour la traduction NL -> code Python (memes drapeaux).\"\"\"\n", + " agent = build_agent(\n", + " name='nl2py_agent',\n", + " description='Traduit une question metier en code Python pandas.',\n", + " instruction=(\n", + " \"Tu es un traducteur NL2Py. Pour chaque demande, reponds STRICTEMENT \"\n", + " \"au format suivant, sans aucun texte hors format :\\n\"\n", + " \"N ecris jamais d appel d outil (transfer_to_agent ou autre) dans ta reponse.\\n\"\n", + " \"EXPLANATION: \\n\"\n", + " \"CODE:\\n\"\n", + " \"```python\\n\\n```\"\n", + " ),\n", + " )\n", + " agent.disallow_transfer_to_parent = True\n", + " agent.disallow_transfer_to_peers = True\n", + " return agent\n", + "\n", + "nl2py_agent = _build_nl2py_agent()\n", + "\n", + "def _parse_python_response(response: str) -> PythonCode:\n", + " explanation = ''\n", + " if 'EXPLANATION:' in response:\n", + " match = re.search(r'EXPLANATION:\\s*(.+?)(?=CODE:|$)', response, re.DOTALL)\n", + " explanation = match.group(1).strip() if match else ''\n", + " code_match = re.search(r'```python\\s*(.*?)\\s*```', response, re.DOTALL)\n", + " code = code_match.group(1).strip() if code_match else ''\n", + " return PythonCode(code=code, explanation=explanation)\n", + "\n", + "async def traduire_python(question: str, data_context: str) -> tuple:\n", + " \"\"\"Un tour ADK vers nl2py_agent : contexte + question, code parsé.\"\"\"\n", + " prompt = (\n", + " f\"Genere du code Python pour: {question}\\n\\n\"\n", + " f\"CONTEXTE:\\n{data_context}\"\n", + " )\n", + " result = await run_agent_turn(nl2py_agent, prompt)\n", + " return _parse_python_response(result.response_text), result\n", + "\n", + "print(\"Agent ADK defini : nl2py_agent (traduction NL -> code Python)\")" ] }, { @@ -323,18 +354,18 @@ "id": "cell-8", "metadata": { "papermill": { - "duration": 0.002732, - "end_time": "2026-09-03T11:54:39.426521", + "duration": 0.006857, + "end_time": "2026-10-05T14:28:15.763882", "exception": false, - "start_time": "2026-09-03T11:54:39.423789", + "start_time": "2026-10-05T14:28:15.757025", "status": "completed" }, "tags": [] }, "source": [ - "## 4. Data Science Agent\n", + "## 4. Data Science Agent (orchestrateur ADK)\n", "\n", - "**Pourquoi l'agent route entre NL2SQL et NL2Py au lieu de toujours utiliser SQL** : une question comme « revenu total par région » se traduit trivialement en SQL (`SUM ... GROUP BY`), mais « calcule la moyenne mobile des revenus sur 3 mois » exige du Python (pandas `rolling`). L'agent `DataScienceAgent` examine la question et dérive le **mode** approprié (`sql` ou `python`) — c'est cette décision de routage qui le rend polyvalent. Sans elle, l'agent échouerait soit sur les requêtes analytiques complexes (SQL trop rigide) soit sur les requêtes déclaratives simples (Python trop lourd). Le routing = la compétence méta au-dessus des deux traducteurs." + "**Pourquoi l'agent route entre NL2SQL et NL2Py au lieu de toujours utiliser SQL** : une question comme « revenu total par région » se traduit trivialement en SQL (`SUM ... GROUP BY`), mais « calcule la moyenne mobile des revenus sur 3 mois » exige du Python (pandas `rolling`). Le portage ADK rend ce routage **réel à deux niveaux** : en mode `sql`/`python` la classe appelle directement le sous-agent (désignation explicite, l'orchestrateur décide en code) ; en mode `auto` le tour vise l'agent racine `data_science_agent`, qui déclare `nl2sql_agent` et `nl2py_agent` comme `sub_agents` — ADK injecte alors nativement l'outil `transfer_to_agent`, et c'est le **LLM lui-même** qui choisit son traducteur en cours de tour (décision d'agent). Sans ce routage, l'agent échouerait soit sur les requêtes analytiques complexes (SQL trop rigide) soit sur les requêtes déclaratives simples (Python trop lourd). Le routing = la compétence méta au-dessus des deux traducteurs." ] }, { @@ -343,16 +374,16 @@ "id": "cell-9", "metadata": { "execution": { - "iopub.execute_input": "2026-09-03T11:54:39.433568Z", - "iopub.status.busy": "2026-09-03T11:54:39.432568Z", - "iopub.status.idle": "2026-09-03T11:54:39.437031Z", - "shell.execute_reply": "2026-09-03T11:54:39.437031Z" + "iopub.execute_input": "2026-10-05T14:28:15.776811Z", + "iopub.status.busy": "2026-10-05T14:28:15.776343Z", + "iopub.status.idle": "2026-10-05T14:28:15.785863Z", + "shell.execute_reply": "2026-10-05T14:28:15.784611Z" }, "papermill": { - "duration": 0.009004, - "end_time": "2026-09-03T11:54:39.438040", + "duration": 0.016901, + "end_time": "2026-10-05T14:28:15.786596", "exception": false, - "start_time": "2026-09-03T11:54:39.429036", + "start_time": "2026-10-05T14:28:15.769695", "status": "completed" }, "tags": [] @@ -362,30 +393,84 @@ "name": "stdout", "output_type": "stream", "text": [ - "Classe DataScienceAgent definie : orchestrateur NL2SQL/NL2Py pour BigQuery\n" + "Classe DataScienceAgent definie : orchestrateur ADK (racine + 2 sous-agents)\n" ] } ], "source": [ "class DataScienceAgent:\n", - " def __init__(self):\n", - " self.llm = LLMClient()\n", - " self.nl2sql = NL2SQLTranslator(self.llm)\n", - " self.nl2py = NL2PyTranslator(self.llm)\n", + " \"\"\"Orchestrateur ADK : deux sous-agents traducteurs + un agent racine qui route.\n", + "\n", + " - analyze(..., mode='sql'|'python') : designation explicite — le tour ADK\n", + " vise directement le sous-agent voulu.\n", + " - analyze(..., mode='auto') : decision d'agent — le tour vise la racine,\n", + " qui transfere via l'outil ADK transfer_to_agent vers le traducteur adapte.\n", + "\n", + " Chaque instance construit ses PROPRES sous-agents : un objet Agent ADK\n", + " n'accepte qu'un seul parent, les instances ne se partagent donc jamais\n", + " entre orchestrateurs.\n", + " \"\"\"\n", "\n", - " def analyze(self, question: str, schema: Dict, mode: str = 'sql') -> Dict:\n", + " def __init__(self):\n", + " self.nl2sql = _build_nl2sql_agent()\n", + " self.nl2py = _build_nl2py_agent()\n", + " self.root = build_agent(\n", + " name='data_science_agent',\n", + " description='Orchestrateur data science : route chaque question vers NL2SQL ou NL2Py.',\n", + " instruction=(\n", + " \"Tu es un routeur. Tu ne produis JAMAIS de SQL ni de code toi-meme. \"\n", + " \"Ta seule action possible est d'appeler l'outil transfer_to_agent \"\n", + " \"pour transmettre la question au sous-agent adapte : nl2sql_agent \"\n", + " \"pour les interrogations declaratives (sommes, filtres, groupements), \"\n", + " \"nl2py_agent pour le calcul procedurale (moyennes mobiles, \"\n", + " \"transformations multi-etapes, statistiques).\"\n", + " ),\n", + " sub_agents=(self.nl2sql, self.nl2py),\n", + " )\n", + "\n", + " async def analyze(self, question: str, schema: Dict, mode: str = 'sql') -> Dict:\n", " print(f'[AGENT] Question: {question}')\n", " print(f'[AGENT] Mode: {mode}')\n", + " schema_desc = _format_schema(schema)\n", " if mode == 'sql':\n", - " result = self.nl2sql.translate(question, schema)\n", - " return {'type': 'sql', 'query': result.sql, 'explanation': result.explanation}\n", - " elif mode == 'python':\n", - " data_context = self.nl2sql._format_schema(schema)\n", - " result = self.nl2py.translate(question, data_context)\n", - " return {'type': 'python', 'code': result.code, 'explanation': result.explanation}\n", + " traduction, adk = await traduire_sql(question, schema)\n", + " return {\n", + " 'type': 'sql', 'query': traduction.sql,\n", + " 'explanation': traduction.explanation,\n", + " 'adk': {'events': adk.event_count, 'agent_final': adk.final_agent},\n", + " }\n", + " if mode == 'python':\n", + " traduction, adk = await traduire_python(question, schema_desc)\n", + " return {\n", + " 'type': 'python', 'code': traduction.code,\n", + " 'explanation': traduction.explanation,\n", + " 'adk': {'events': adk.event_count, 'agent_final': adk.final_agent},\n", + " }\n", + " if mode == 'auto':\n", + " prompt = (\n", + " f\"SCHEMA:\\n{schema_desc}\\n\\nQUESTION: {question}\\n\\n\"\n", + " \"Transfere OBLIGATOIREMENT cette question au sous-agent adapte via \"\n", + " \"l'outil transfer_to_agent. Ne redige pas la reponse toi-meme.\"\n", + " )\n", + " adk = await run_agent_turn(self.root, prompt)\n", + " if adk.final_agent == 'nl2py_agent':\n", + " traduction = _parse_python_response(adk.response_text)\n", + " return {\n", + " 'type': 'python', 'code': traduction.code,\n", + " 'explanation': traduction.explanation,\n", + " 'adk': {'events': adk.event_count, 'agent_final': adk.final_agent,\n", + " 'handoffs': list(adk.handoffs)},\n", + " }\n", + " traduction = _parse_sql_response(adk.response_text)\n", + " return {\n", + " 'type': 'sql', 'query': traduction.sql,\n", + " 'explanation': traduction.explanation,\n", + " 'adk': {'events': adk.event_count, 'agent_final': adk.final_agent,\n", + " 'handoffs': list(adk.handoffs)},\n", + " }\n", " return {'error': 'Mode non supporte'}\n", "\n", - "print(\"Classe DataScienceAgent definie : orchestrateur NL2SQL/NL2Py pour BigQuery\")" + "print(\"Classe DataScienceAgent definie : orchestrateur ADK (racine + 2 sous-agents)\")" ] }, { @@ -393,10 +478,10 @@ "id": "cell-10", "metadata": { "papermill": { - "duration": 0.003511, - "end_time": "2026-09-03T11:54:39.443549", + "duration": 0.005597, + "end_time": "2026-10-05T14:28:15.796485", "exception": false, - "start_time": "2026-09-03T11:54:39.440038", + "start_time": "2026-10-05T14:28:15.790888", "status": "completed" }, "tags": [] @@ -411,16 +496,16 @@ "id": "cell-11", "metadata": { "execution": { - "iopub.execute_input": "2026-09-03T11:54:39.449615Z", - "iopub.status.busy": "2026-09-03T11:54:39.449615Z", - "iopub.status.idle": "2026-09-03T11:54:39.452466Z", - "shell.execute_reply": "2026-09-03T11:54:39.452466Z" + "iopub.execute_input": "2026-10-05T14:28:15.811519Z", + "iopub.status.busy": "2026-10-05T14:28:15.811041Z", + "iopub.status.idle": "2026-10-05T14:28:15.817292Z", + "shell.execute_reply": "2026-10-05T14:28:15.815978Z" }, "papermill": { - "duration": 0.006394, - "end_time": "2026-09-03T11:54:39.452466", + "duration": 0.015268, + "end_time": "2026-10-05T14:28:15.818555", "exception": false, - "start_time": "2026-09-03T11:54:39.446072", + "start_time": "2026-10-05T14:28:15.803287", "status": "completed" }, "tags": [] @@ -452,7 +537,16 @@ { "cell_type": "markdown", "id": "f5bb2e6e", - "metadata": {}, + "metadata": { + "papermill": { + "duration": 0.005624, + "end_time": "2026-10-05T14:28:15.830737", + "exception": false, + "start_time": "2026-10-05T14:28:15.825113", + "status": "completed" + }, + "tags": [] + }, "source": [ "**Lecture chiffree — le schema simule en nombre de colonnes.** Trois tables affichees : `sales: ['date', 'product', 'region', 'quantity', 'revenue']` (5 colonnes), `customers` (4), `products` (4) — 13 colonnes au total. C'est TOUT ce que le traducteur recevra comme connaissance du monde : pas de donnees, pas de volumes, seulement la structure. La question-test qui suit ne consommera que 2 de ces 13 colonnes — `region` et `revenue`, toutes deux dans `sales` — d'ou une requete mono-table, sans jointure. Inversement, une question croisant `segment` (cote customers) et `revenue` (cote sales) exigerait un JOIN : le cout des questions futures se lit deja dans la repartition des colonnes entre tables.\n" ] @@ -462,10 +556,10 @@ "id": "c88330e3", "metadata": { "papermill": { - "duration": 0.003001, - "end_time": "2026-09-03T11:54:39.458991", + "duration": 0.004087, + "end_time": "2026-10-05T14:28:15.840570", "exception": false, - "start_time": "2026-09-03T11:54:39.455990", + "start_time": "2026-10-05T14:28:15.836483", "status": "completed" }, "tags": [] @@ -480,16 +574,16 @@ "id": "cell-12", "metadata": { "execution": { - "iopub.execute_input": "2026-09-03T11:54:39.465309Z", - "iopub.status.busy": "2026-09-03T11:54:39.464309Z", - "iopub.status.idle": "2026-09-03T11:54:46.239004Z", - "shell.execute_reply": "2026-09-03T11:54:46.239004Z" + "iopub.execute_input": "2026-10-05T14:28:15.855613Z", + "iopub.status.busy": "2026-10-05T14:28:15.855102Z", + "iopub.status.idle": "2026-10-05T14:28:18.436881Z", + "shell.execute_reply": "2026-10-05T14:28:18.435443Z" }, "papermill": { - "duration": 6.778714, - "end_time": "2026-09-03T11:54:46.240013", + "duration": 2.591761, + "end_time": "2026-10-05T14:28:18.438359", "exception": false, - "start_time": "2026-09-03T11:54:39.461299", + "start_time": "2026-10-05T14:28:15.846598", "status": "completed" }, "tags": [] @@ -499,7 +593,14 @@ "name": "stdout", "output_type": "stream", "text": [ - "[AGENT] Question: Quel est le revenu total par region?\n", + "[AGENT] Question: Quel est le revenu total par region?" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", "[AGENT] Mode: sql\n" ] }, @@ -507,31 +608,30 @@ "name": "stdout", "output_type": "stream", "text": [ - "\\n==================================================\n", + "\n", + "==================================================\n", "RESULTAT NL2SQL:\n", "==================================================\n", - "Explication: Pour obtenir le revenu total par région, nous devons interroger la table `sales` qui contient à la fois les informations sur les régions et les revenus. Nous sélectionnons la colonne `region` et utilisons la fonction d'agrégation `SUM()` sur la colonne `revenue` pour calculer le revenu total. Enfin, nous utilisons la clause `GROUP BY` pour regrouper les résultats pour chaque région.\n", - "SQL: SELECT\n", - " region,\n", - " SUM(revenue) AS total_revenue\n", - "FROM\n", - " sales\n", - "GROUP BY\n", - " region;\n" + "Explication: La question demande de calculer le total du revenu pour chaque région.\n", + "SQL: SELECT region, SUM(revenue) AS total_revenue\n", + "FROM sales\n", + "GROUP BY region;\n", + "Tour ADK: 1 evenements, agent final = nl2sql_agent\n" ] } ], "source": [ - "# Test NL2SQL\n", + "# Test NL2SQL (designation explicite)\n", "agent = DataScienceAgent()\n", "question = 'Quel est le revenu total par region?'\n", - "result = agent.analyze(question, schema, mode='sql')\n", + "result = await agent.analyze(question, schema, mode='sql')\n", "\n", - "print('\\\\n' + '='*50)\n", + "print('\\n' + '='*50)\n", "print('RESULTAT NL2SQL:')\n", "print('='*50)\n", "print(f'Explication: {result.get(\"explanation\", \"N/A\")}')\n", - "print(f'SQL: {result.get(\"query\", \"N/A\")}')" + "print(f'SQL: {result.get(\"query\", \"N/A\")}')\n", + "print(f\"Tour ADK: {result['adk']['events']} evenements, agent final = {result['adk']['agent_final']}\")" ] }, { @@ -539,16 +639,16 @@ "id": "1cfa4476", "metadata": { "papermill": { - "duration": 0.002999, - "end_time": "2026-09-03T11:54:46.246010", + "duration": 0.007063, + "end_time": "2026-10-05T14:28:18.451520", "exception": false, - "start_time": "2026-09-03T11:54:46.243011", + "start_time": "2026-10-05T14:28:18.444457", "status": "completed" }, "tags": [] }, "source": [ - "**Lecture du résultat NL2SQL** — l'agent a routé « Quel est le revenu total par région ? » vers le mode **`sql`** (`[AGENT] Mode: sql`) et généré une requête : interroger la table `sales`, sélectionner la colonne `region`, appliquer `SUM()` sur `revenue`, regrouper avec `GROUP BY`. L'explication générée justifie chaque clause SQL par la sémantique de la question. Lu en comptant : la question fait 7 mots, et le SQL produit contient exactement les trois pièces énumérées — la table (`FROM sales`), l'agrégat `SUM(revenue) AS total_revenue`, le regroupement `GROUP BY`. L'ancrage au schéma se vérifie : 1 table sur 3, 2 colonnes sur 13 — rien d'inventé, chaque identifiant du SQL existe dans le schéma fourni.\n", + "**Lecture du résultat NL2SQL** — l'agent a routé « Quel est le revenu total par région ? » vers le mode **`sql`** (`[AGENT] Mode: sql`) et généré une requête : interroger la table `sales`, sélectionner la colonne `region`, appliquer `SUM()` sur `revenue`, regrouper avec `GROUP BY`. L'explication générée justifie chaque clause SQL par la sémantique de la question. Lu en comptant : la question fait 7 mots, et le SQL produit contient exactement les trois pièces énumérées — la table (`FROM sales`), l'agrégat `SUM(revenue) AS total_revenue`, le regroupement `GROUP BY`. L'ancrage au schéma se vérifie : 1 table sur 3, 2 colonnes sur 13 — rien d'inventé, chaque identifiant du SQL existe dans le schéma fourni. Le tour ADK rend sa trace : **1 événement, agent final `nl2sql_agent`** — désignation explicite (le notebook a choisi `mode='sql'`), réponse servie par le Runner ADK via LiteLLM.\n", "\n", "**Ce que cet output démontre sur NL2SQL** : le traducteur n'a pas produit une réponse en langage naturel (« le revenu total est X ») — il a produit une **requête exécutable**. C'est l'écart clé entre un chatbot et un agent data : le premier décrit, le second génère du code que l'entrepôt peut exécuter. La requête `SUM ... GROUP BY` est dérivée du schéma BigQuery simulé (cellule 12) — l'agent a su que `revenue` vivait dans `sales` parce qu'on lui a fourni le schéma en contexte. Le LLM ne répond pas à la question, il COMPILE la question dans le vocabulaire exact du schéma — et l'exercice 3 de ce lab (le validateur SQL) consistera précisément à vérifier mécaniquement cette propriété." ] @@ -558,10 +658,10 @@ "id": "1c3cb00f", "metadata": { "papermill": { - "duration": 0.003014, - "end_time": "2026-09-03T11:54:46.252215", + "duration": 0.007006, + "end_time": "2026-10-05T14:28:18.466084", "exception": false, - "start_time": "2026-09-03T11:54:46.249201", + "start_time": "2026-10-05T14:28:18.459078", "status": "completed" }, "tags": [] @@ -576,16 +676,16 @@ "id": "cell-13", "metadata": { "execution": { - "iopub.execute_input": "2026-09-03T11:54:46.260215Z", - "iopub.status.busy": "2026-09-03T11:54:46.259216Z", - "iopub.status.idle": "2026-09-03T11:55:04.145574Z", - "shell.execute_reply": "2026-09-03T11:55:04.145574Z" + "iopub.execute_input": "2026-10-05T14:28:18.481974Z", + "iopub.status.busy": "2026-10-05T14:28:18.481460Z", + "iopub.status.idle": "2026-10-05T14:28:20.692370Z", + "shell.execute_reply": "2026-10-05T14:28:20.691066Z" }, "papermill": { - "duration": 17.891364, - "end_time": "2026-09-03T11:55:04.146579", + "duration": 2.220215, + "end_time": "2026-10-05T14:28:20.693704", "exception": false, - "start_time": "2026-09-03T11:54:46.255215", + "start_time": "2026-10-05T14:28:18.473489", "status": "completed" }, "tags": [] @@ -603,37 +703,35 @@ "name": "stdout", "output_type": "stream", "text": [ - "\\n==================================================\n", + "\n", + "==================================================\n", "RESULTAT NL2Py:\n", "==================================================\n", - "Explication: Pour calculer la moyenne des revenus par mois, nous utilisons la bibliothèque `pandas`. Nous avons uniquement besoin de la table `sales`. \n", - "La démarche est la suivante :\n", - "1. Convertir la colonne `date` en format `datetime` pour faciliter la manipulation des dates.\n", - "2. Extraire la période mensuelle (Année-Mois) à partir de la date.\n", - "3. Grouper les données par ce mois et calculer la moyenne (`mean()`) de la colonne `revenue` pour obtenir le revenu moyen par transaction pour chaque mois.\n", - "*(Note : Si vous cherchiez à calculer le revenu total moyen généré par mois sur toute l'année, une ligne supplémentaire est incluse à la fin du code).*\n", + "Explication: Pour calculer la moyenne des revenus par mois, nous devons résigner la table `sales` en fonction de la date et de la colonne `revenue`. Ensuite, la moyenne sera calculée pour chaque mois.\n", "Code: import pandas as pd\n", "\n", - "# Supposons que 'sales' est votre DataFrame contenant les données de la table sales\n", - "# sales = pd.read_csv('sales.csv') \n", - "\n", - "# 1. Convertir la colonne 'date' en type datetime\n", - "sales['date'] = pd.to_datetime(sales['date'])\n", + "# Supposons que les données se trouvent dans des DataFrames\n", + "# sales_data = pd.read_csv('sales.csv')\n", + "# customers_data = pd.read_csv('customers.csv')\n", + "# products_data = pd.read_csv('products.csv')\n", "\n", - "# 2. Créer une nouvelle colonne pour le mois (format Année-Mo...\n" + "# Résignons la table des ventes par mois et par revenu\n", + "sales_monthly_revenue = sales...\n", + "Tour ADK: 1 evenements, agent final = nl2py_agent\n" ] } ], "source": [ - "# Test NL2Py\n", + "# Test NL2Py (designation explicite)\n", "question2 = 'Calcule la moyenne des revenus par mois'\n", - "result2 = agent.analyze(question2, schema, mode='python')\n", + "result2 = await agent.analyze(question2, schema, mode='python')\n", "\n", - "print('\\\\n' + '='*50)\n", + "print('\\n' + '='*50)\n", "print('RESULTAT NL2Py:')\n", "print('='*50)\n", "print(f'Explication: {result2.get(\"explanation\", \"N/A\")}')\n", - "print(f'Code: {result2.get(\"code\", \"N/A\")[:300]}...')" + "print(f'Code: {result2.get(\"code\", \"N/A\")[:300]}...')\n", + "print(f\"Tour ADK: {result2['adk']['events']} evenements, agent final = {result2['adk']['agent_final']}\")" ] }, { @@ -641,18 +739,127 @@ "id": "a5a4150d", "metadata": { "papermill": { - "duration": 0.003505, - "end_time": "2026-09-03T11:55:04.153084", + "duration": 0.005512, + "end_time": "2026-10-05T14:28:20.708109", + "exception": false, + "start_time": "2026-10-05T14:28:20.702597", + "status": "completed" + }, + "tags": [] + }, + "source": [ + "**Lecture du résultat NL2Py** — la question « Calcule la moyenne des revenus par mois » a été traduite en mode **`python`** (désignation explicite, `[AGENT] Mode: python`). L'explication générée identifie les deux colonnes pertinentes — `date` et `revenue`, 2 colonnes sur 13, toutes deux dans `sales` — et annonce l'agrégation mensuelle. Le code suit : il suppose le dataframe déjà chargé (les `read_csv` restent en commentaire — le traducteur livre une transformation, pas un pipeline), nomme sa sortie `sales_monthly_revenue`, et l'affichage de la cellule de test le coupe à 300 caractères. Le tour ADK rend sa trace : **1 événement, agent final `nl2py_agent`**.\n", + "\n", + "**Ce que cet output démontre sur le routing** : cette question aurait pu se traduire en SQL (`AVG(revenue)` avec `EXTRACT(MONTH FROM date)`), mais la traduction pandas (indexation temporelle, agrégat mensuel) est plus lisible — et le mode `auto` de la section suivante confirmera ce choix sur une question procédurale plus marquée (moyenne mobile). Noter la glotte du modèle local : « résigner la table » n'est pas un verbe français correct — le qwen local sert parfois un mot impropre ; la sortie est committée telle quelle (sortie réelle, jamais retouchée à la main)." + ] + }, + { + "cell_type": "markdown", + "id": "cell-auto-routing-md", + "metadata": { + "papermill": { + "duration": 0.005959, + "end_time": "2026-10-05T14:28:20.719469", + "exception": false, + "start_time": "2026-10-05T14:28:20.713510", + "status": "completed" + }, + "tags": [] + }, + "source": [ + "## 6. Routage automatique (decision d'agent)\n", + "\n", + "**Pourquoi tester le mode `auto` séparément** : dans les deux tests précédents, c'est le *notebook* qui a choisi le traducteur (`mode='sql'`, `mode='python'`). Le mode `auto` délègue cette décision au **LLM en cours de tour** : l'agent racine `data_science_agent` reçoit la question brute et utilise l'outil ADK `transfer_to_agent` (injecté nativement par la déclaration `sub_agents`) pour la transmettre au sous-agent qu'il juge adapté. La trace ADK expose alors le chemin réel — transfert effectué, agent final — rendant la décision d'agent **observable** au lieu d'assumée." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "cell-auto-routing-code", + "metadata": { + "execution": { + "iopub.execute_input": "2026-10-05T14:28:20.732876Z", + "iopub.status.busy": "2026-10-05T14:28:20.732662Z", + "iopub.status.idle": "2026-10-05T14:28:24.344088Z", + "shell.execute_reply": "2026-10-05T14:28:24.342808Z" + }, + "papermill": { + "duration": 3.619208, + "end_time": "2026-10-05T14:28:24.345246", "exception": false, - "start_time": "2026-09-03T11:55:04.149579", + "start_time": "2026-10-05T14:28:20.726038", "status": "completed" }, "tags": [] }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "App \"track2_google_adk\" can transfer between agents but has no context_cache_config. Every transfer swaps the system instruction and the tool set, so the request prefix changes and the whole prompt is re-sent uncached after each transfer. Set context_cache_config on the app to give each agent its own cache.\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[AGENT] Question: Calcule la moyenne mobile du revenu sur 3 mois par produit\n", + "[AGENT] Mode: auto\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\\AppData\\Local\\Programs\\Python\\Python313\\Lib\\site-packages\\google\\adk\\tools\\transfer_to_agent_tool.py:72: UserWarning: [EXPERIMENTAL] feature FeatureName.JSON_SCHEMA_FOR_FUNC_DECL is enabled.\n", + " function_decl = super()._get_declaration()\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "==================================================\n", + "RESULTAT ROUTAGE AUTO:\n", + "==================================================\n", + "Voie choisie par l'agent: python\n", + "Transferts ADK: [('data_science_agent', 'nl2py_agent')]\n", + "Agent final: nl2py_agent\n", + "Code: import pandas as pd\n", + "\n", + "# Supposons que la table sales est chargée dans un dataframe叫做 `sales_df`\n", + "sales_df['date'] = pd.to_datetime(sales_df['date'])\n", + "sales_df = sales_df.sort_values(by='date')\n", + "\n", + "# Calculo...\n" + ] + } + ], "source": [ - "**Lecture du résultat NL2Py** — l'agent a routé la question « Calcule la moyenne des revenus par mois » vers le mode **`python`** (pandas, `[AGENT] Mode: python`) plutôt que SQL. L'explication générée justifie ce choix : seule la table `sales` est nécessaire (elle porte `date` + `revenue`), et la démarche convertit la colonne date en index temporel pour calculer la moyenne mensuelle. En comptant : l'explication énumère exactement 3 étapes numérotées — convertir la colonne date, extraire la période mensuelle, grouper et moyenner — et le code suit l'énumération, première transformation de données `sales['date'] = pd.to_datetime(sales['date'])`. Elle ajoute une note entre parenthèses : moyenne par transaction OU revenu total moyen par mois, selon la lecture de la question — la question d'origine est ambiguë, le code tranche, l'avertissement documente le choix.\n", + "# Test du routage automatique : la decision d'agent (transfert ADK)\n", + "question3 = 'Calcule la moyenne mobile du revenu sur 3 mois par produit'\n", + "result3 = await agent.analyze(question3, schema, mode='auto')\n", "\n", - "**Ce que cet output démontre sur le routing** : cette question aurait pu se traduire en SQL (`AVG(revenue)` avec `EXTRACT(MONTH FROM date)`), mais l'agent a choisi Python — typiquement parce que pandas rend l'opération plus lisible (indexation temporelle native). C'est précisément la valeur ajoutée du routing intelligent : sur une question similaire mais plus simple (« revenu total par région », output ec=7), l'agent avait choisi SQL — et cette question-là, plus courte (7 mots), n'avait produit aucune note d'ambiguïté. L'agent adapte le mode à la **complexité procédurale** de la question, pas à une règle fixe. (Caveat : ce run ne permet pas d'attribuer l'écart au mode — l'avertissement observé documente cette question Python précise, pas un coût général du mode procédural ; tester la même question dans les deux modes serait nécessaire pour comparer les modes eux-mêmes.)" + "print('\\n' + '='*50)\n", + "print('RESULTAT ROUTAGE AUTO:')\n", + "print('='*50)\n", + "print(f\"Voie choisie par l'agent: {result3.get('type', 'N/A')}\")\n", + "print(f\"Transferts ADK: {result3['adk'].get('handoffs', [])}\")\n", + "print(f\"Agent final: {result3['adk']['agent_final']}\")\n", + "if result3.get('type') == 'sql':\n", + " print(f\"SQL: {result3.get('query', 'N/A')[:200]}\")\n", + "else:\n", + " print(f\"Code: {result3.get('code', 'N/A')[:200]}...\")" + ] + }, + { + "cell_type": "markdown", + "id": "cell-auto-reading", + "metadata": {}, + "source": [ + "**Lecture du routage automatique** — le LLM-routeur a transféré la question « moyenne mobile du revenu sur 3 mois par produit » vers **`nl2py_agent`** : le transfert est un fait mesuré (`Transferts ADK: [('data_science_agent', 'nl2py_agent')]`, agent final `nl2py_agent`), pas une interprétation. C'est le bon choix : une moyenne mobile (`rolling(3)`) est procédurale, naturelle en pandas, pénible en SQL — comparer avec la section 5 où « revenu total par région » avait pris la voie SQL. La décision d'agent s'adapte à la nature de la question, et la trace ADK la rend **auditable**. Détail honnête du modèle local : le code généré glisse en chinois dans un commentaire (« 叫做 » — « appelé ») puis en espagnol dans le suivant (« Calculo... ») — le qwen 7B local est multilingue et mélange parfois les langues dans ses commentaires ; le code lui-même (conversion `to_datetime`, tri par date) reste correct." ] }, { @@ -660,18 +867,18 @@ "id": "cell-14", "metadata": { "papermill": { - "duration": 0.002999, - "end_time": "2026-09-03T11:55:04.158755", + "duration": 0.00331, + "end_time": "2026-10-05T14:28:24.356185", "exception": false, - "start_time": "2026-09-03T11:55:04.155756", + "start_time": "2026-10-05T14:28:24.352875", "status": "completed" }, "tags": [] }, "source": [ - "## 6. Résumé du Lab\n", + "## 7. Résumé du Lab\n", "\n", - "**Pourquoi le résumé insists sur le routing NL2SQL-vs-NL2Py plutôt que sur un outil unique** : un Data Science Agent production ne peut pas se limiter à SQL (perd les analyses procédurales) ni à Python (perd la déclarativité et l'optimisation de l'entrepôt). La leçon de ce lab est que la **polyvalence vient du routing** — l'agent choisit l'outil selon la nature de la question, pas selon une préférence fixe. C'est ce pattern (décider puis exécuter, dans le bon langage) que l'étudiant doit retenir pour construire des agents data réels." + "**Pourquoi le résumé porte sur le routing NL2SQL-vs-NL2Py plutôt que sur un outil unique** : un Data Science Agent production ne peut pas se limiter à SQL (perd les analyses procédurales) ni à Python (perd la déclarativité et l'optimisation de l'entrepôt). La leçon de ce lab est que la **polyvalence vient du routing** — l'agent choisit l'outil selon la nature de la question, pas selon une préférence fixe. Le portage ADK rend cette leçon littérale : le mode `auto` délègue le choix au LLM via l'outil natif `transfer_to_agent`, et la trace du tour (`handoffs`, agent final) rend chaque décision auditable. C'est ce pattern (décider puis exécuter, dans le bon langage) que l'étudiant doit retenir pour construire des agents data réels." ] }, { @@ -679,10 +886,10 @@ "id": "eaaa422e", "metadata": { "papermill": { - "duration": 0.003513, - "end_time": "2026-09-03T11:55:04.165268", + "duration": 0.005325, + "end_time": "2026-10-05T14:28:24.364537", "exception": false, - "start_time": "2026-09-03T11:55:04.161755", + "start_time": "2026-10-05T14:28:24.359212", "status": "completed" }, "tags": [] @@ -699,20 +906,20 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 10, "id": "0e3dae07", "metadata": { "execution": { - "iopub.execute_input": "2026-09-03T11:55:04.171777Z", - "iopub.status.busy": "2026-09-03T11:55:04.171777Z", - "iopub.status.idle": "2026-09-03T11:55:04.178642Z", - "shell.execute_reply": "2026-09-03T11:55:04.178642Z" + "iopub.execute_input": "2026-10-05T14:28:24.378145Z", + "iopub.status.busy": "2026-10-05T14:28:24.377818Z", + "iopub.status.idle": "2026-10-05T14:28:24.385540Z", + "shell.execute_reply": "2026-10-05T14:28:24.384445Z" }, "papermill": { - "duration": 0.011378, - "end_time": "2026-09-03T11:55:04.179646", + "duration": 0.014385, + "end_time": "2026-10-05T14:28:24.386428", "exception": false, - "start_time": "2026-09-03T11:55:04.168268", + "start_time": "2026-10-05T14:28:24.372043", "status": "completed" }, "tags": [] @@ -820,10 +1027,10 @@ "id": "1e81aa02", "metadata": { "papermill": { - "duration": 0.002513, - "end_time": "2026-09-03T11:55:04.185522", + "duration": 0.005286, + "end_time": "2026-10-05T14:28:24.397397", "exception": false, - "start_time": "2026-09-03T11:55:04.183009", + "start_time": "2026-10-05T14:28:24.392111", "status": "completed" }, "tags": [] @@ -841,10 +1048,10 @@ "id": "92a1a3d8", "metadata": { "papermill": { - "duration": 0.003512, - "end_time": "2026-09-03T11:55:04.192036", + "duration": 0.003716, + "end_time": "2026-10-05T14:28:24.404630", "exception": false, - "start_time": "2026-09-03T11:55:04.188524", + "start_time": "2026-10-05T14:28:24.400914", "status": "completed" }, "tags": [] @@ -866,20 +1073,20 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 11, "id": "03beb4e6", "metadata": { "execution": { - "iopub.execute_input": "2026-09-03T11:55:04.198549Z", - "iopub.status.busy": "2026-09-03T11:55:04.198549Z", - "iopub.status.idle": "2026-09-03T11:57:47.958211Z", - "shell.execute_reply": "2026-09-03T11:57:47.957657Z" + "iopub.execute_input": "2026-10-05T14:28:24.417083Z", + "iopub.status.busy": "2026-10-05T14:28:24.416857Z", + "iopub.status.idle": "2026-10-05T14:28:49.788609Z", + "shell.execute_reply": "2026-10-05T14:28:49.787128Z" }, "papermill": { - "duration": 163.763703, - "end_time": "2026-09-03T11:57:47.958740", + "duration": 25.380908, + "end_time": "2026-10-05T14:28:49.789921", "exception": false, - "start_time": "2026-09-03T11:55:04.195037", + "start_time": "2026-10-05T14:28:24.409013", "status": "completed" }, "tags": [] @@ -906,19 +1113,14 @@ "name": "stdout", "output_type": "stream", "text": [ - "SQL: SELECT \n", - " id, \n", - " client_id, \n", - " date, \n", - " montant, \n", - " statut\n", - "FROM \n", - " commandes\n", - "ORDER BY \n", - " ...\n", + "SQL: SELECT *\n", + "FROM `table.commandes`\n", + "ORDER BY montant DESC\n", + "LIMIT 1;...\n", "Python: import pandas as pd\n", "\n", - "# Supposons que la table commandes est déjà chargée dans un DataFrame appelé df...\n", + "# Supposons que les données soient déjà chargées dans des DataFrames\n", + "# df_comma...\n", "\\nQUESTION: ...\n", "[AGENT] Question: ...\n", "[AGENT] Mode: sql\n" @@ -936,16 +1138,11 @@ "name": "stdout", "output_type": "stream", "text": [ - "SQL: SELECT \n", - " p.categorie,\n", - " SUM(lc.quantite * lc.prix_unitaire) AS chiffre_affaires_total\n", - "FROM \n", - " ...\n", + "SQL: -- Il n'y a pas de requête BigQuery spécifique car la question n'a pas été fournie....\n", "Python: import pandas as pd\n", "\n", - "def analyser_ca_par_segment(df_commandes, df_clients):\n", - " \"\"\"\n", - " Calcule le c...\n", + "# Supposons que les DataFrames soient déjà chargés\n", + "# df_commandes, df_clients, ...\n", "\\nQUESTION: ...\n", "[AGENT] Question: ...\n", "[AGENT] Mode: sql\n" @@ -963,16 +1160,12 @@ "name": "stdout", "output_type": "stream", "text": [ - "SQL: SELECT \n", - " c.segment,\n", - " SUM(lc.quantite * lc.prix_unitaire) AS chiffre_affaires_total\n", - "FROM \n", - " `...\n", + "SQL: SELECT *\n", + "FROM commandes;...\n", "Python: import pandas as pd\n", "\n", - "def calculer_ca_par_segment(df_commandes, df_clients):\n", - " \"\"\"\n", - " Calcule le c...\n", + "# Supposons que les données soient sous forme de DataFrame\n", + "commandes = pd.DataF...\n", "\\nQUESTION: ...\n", "[AGENT] Question: ...\n", "[AGENT] Mode: sql\n" @@ -990,14 +1183,13 @@ "name": "stdout", "output_type": "stream", "text": [ - "SQL: SELECT \n", - " p.categorie,\n", - " SUM(lc.quantite * lc.prix_unitaire) AS chiffre_affaires_total\n", - "FROM \n", - " ...\n", + "SQL: SELECT c.nom AS client, SUM(l.quantite * l.prix_unitaire) AS montant_total, com.statut\n", + "FROM clients ...\n", "Python: import pandas as pd\n", "\n", - "def analyser_ventes(df_commandes, df_clients, df_produits, df_lignes_commande):...\n", + "# Exemple de données pour les tables\n", + "commandes = pd.DataFrame({\n", + " 'id': [1, 2...\n", "\\nQUESTION: ...\n", "[AGENT] Question: ...\n", "[AGENT] Mode: sql\n" @@ -1015,15 +1207,13 @@ "name": "stdout", "output_type": "stream", "text": [ - "SQL: SELECT \n", - " c.segment,\n", - " SUM(cmd.montant) AS chiffre_affaires_total\n", - "FROM \n", - " `ton_projet.ton_data...\n", + "SQL: SELECT c.id, c.nom, c.email, d.date, d.montant\n", + "FROM commandes d\n", + "JOIN clients c ON d.client_id = c.id...\n", "Python: import pandas as pd\n", "\n", - "# --- 1. Création de données fictives basées sur votre contexte ---\n", - "df_clients ...\n" + "# Supposons que les données soient dans des DataFrames\n", + "df_commandes = pd.DataFr...\n" ] } ], @@ -1049,8 +1239,8 @@ "agent = DataScienceAgent()\n", "for q in mes_questions:\n", " print(f\"\\\\nQUESTION: {q}\")\n", - " sql_result = agent.analyze(q, mon_schema, mode='sql')\n", - " py_result = agent.analyze(q, mon_schema, mode='python')\n", + " sql_result = await agent.analyze(q, mon_schema, mode='sql')\n", + " py_result = await agent.analyze(q, mon_schema, mode='python')\n", " \n", " print(f\"SQL: {sql_result.get('query', 'N/A')[:100]}...\")\n", " print(f\"Python: {py_result.get('code', 'N/A')[:100]}...\")\n", @@ -1063,10 +1253,10 @@ "id": "4255aeec", "metadata": { "papermill": { - "duration": 0.003006, - "end_time": "2026-09-03T11:57:47.966055", + "duration": 0.009016, + "end_time": "2026-10-05T14:28:49.808130", "exception": false, - "start_time": "2026-09-03T11:57:47.963049", + "start_time": "2026-10-05T14:28:49.799114", "status": "completed" }, "tags": [] @@ -1089,20 +1279,20 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 12, "id": "223968ab", "metadata": { "execution": { - "iopub.execute_input": "2026-09-03T11:57:47.974055Z", - "iopub.status.busy": "2026-09-03T11:57:47.974055Z", - "iopub.status.idle": "2026-09-03T11:57:47.980515Z", - "shell.execute_reply": "2026-09-03T11:57:47.980515Z" + "iopub.execute_input": "2026-10-05T14:28:49.824746Z", + "iopub.status.busy": "2026-10-05T14:28:49.824113Z", + "iopub.status.idle": "2026-10-05T14:28:49.841329Z", + "shell.execute_reply": "2026-10-05T14:28:49.839433Z" }, "papermill": { - "duration": 0.011464, - "end_time": "2026-09-03T11:57:47.981520", + "duration": 0.027926, + "end_time": "2026-10-05T14:28:49.842507", "exception": false, - "start_time": "2026-09-03T11:57:47.970056", + "start_time": "2026-10-05T14:28:49.814581", "status": "completed" }, "tags": [] @@ -1155,8 +1345,8 @@ "resultats_comparaison = []\n", "for i, q in enumerate(questions_complexite):\n", " # TODO etudiant: traduisez en SQL et en Python\n", - " # sql_result = agent.analyze(q, benchmark_schema, mode='sql')\n", - " # py_result = agent.analyze(q, benchmark_schema, mode='python')\n", + " # sql_result = await agent.analyze(q, benchmark_schema, mode='sql')\n", + " # py_result = await agent.analyze(q, benchmark_schema, mode='python')\n", " \n", " resultats_comparaison.append({\n", " 'niveau': i + 1,\n", @@ -1183,7 +1373,16 @@ { "cell_type": "markdown", "id": "11573e0d", - "metadata": {}, + "metadata": { + "papermill": { + "duration": 0.011285, + "end_time": "2026-10-05T14:28:49.863716", + "exception": false, + "start_time": "2026-10-05T14:28:49.852431", + "status": "completed" + }, + "tags": [] + }, "source": [ "**Lecture chiffree — 10 inconnues dans le squelette de benchmark.** Le schema de test porte 4 tables (employees, departments, projects, assignments) totalisant 19 colonnes (5 + 4 + 6 + 4). Les questions de `questions_complexite` en echelonnent 5 — agregation simple, jointure, agregation temporelle, sous-requete, analyse complexe — et la sortie imprime l'en-tete `Niveau | Question | SQL OK | Python OK` suivi de 5 lignes ou les 10 verdicts valent tous `?`. Ce tableau vide est le protocole lui-meme : chaque `?` deviendra un fait mesure (reussite, echec, colonne hallucinee) une fois l'exercice complete ; 5 niveaux x 2 modes = 10 points de donnee qui traceront la frontiere ou SQL cede la place a Python — la frontiere que le routing n'a encore echantillonnee que sur 2 questions.\n" ] @@ -1193,10 +1392,10 @@ "id": "f11a3ddb", "metadata": { "papermill": { - "duration": 0.004006, - "end_time": "2026-09-03T11:57:47.989031", + "duration": 0.008718, + "end_time": "2026-10-05T14:28:49.883001", "exception": false, - "start_time": "2026-09-03T11:57:47.985025", + "start_time": "2026-10-05T14:28:49.874283", "status": "completed" }, "tags": [] @@ -1219,20 +1418,20 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 13, "id": "f1858176", "metadata": { "execution": { - "iopub.execute_input": "2026-09-03T11:57:47.996546Z", - "iopub.status.busy": "2026-09-03T11:57:47.996546Z", - "iopub.status.idle": "2026-09-03T11:57:48.001493Z", - "shell.execute_reply": "2026-09-03T11:57:48.001493Z" + "iopub.execute_input": "2026-10-05T14:28:49.904714Z", + "iopub.status.busy": "2026-10-05T14:28:49.903898Z", + "iopub.status.idle": "2026-10-05T14:28:49.915871Z", + "shell.execute_reply": "2026-10-05T14:28:49.914050Z" }, "papermill": { - "duration": 0.008947, - "end_time": "2026-09-03T11:57:48.001493", + "duration": 0.02448, + "end_time": "2026-10-05T14:28:49.917384", "exception": false, - "start_time": "2026-09-03T11:57:47.992546", + "start_time": "2026-10-05T14:28:49.892904", "status": "completed" }, "tags": [] @@ -1322,10 +1521,10 @@ "id": "a474b09b", "metadata": { "papermill": { - "duration": 0.003004, - "end_time": "2026-09-03T11:57:48.009548", + "duration": 0.011639, + "end_time": "2026-10-05T14:28:49.940293", "exception": false, - "start_time": "2026-09-03T11:57:48.006544", + "start_time": "2026-10-05T14:28:49.928654", "status": "completed" }, "tags": [] @@ -1342,10 +1541,10 @@ "id": "cell-63abe9d2", "metadata": { "papermill": { - "duration": 0.003503, - "end_time": "2026-09-03T11:57:48.016563", + "duration": 0.010388, + "end_time": "2026-10-05T14:28:49.961092", "exception": false, - "start_time": "2026-09-03T11:57:48.013060", + "start_time": "2026-10-05T14:28:49.950704", "status": "completed" }, "tags": [] @@ -1392,18 +1591,18 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.9" + "version": "3.13.13" }, "papermill": { "default_parameters": {}, - "duration": 193.224683, - "end_time": "2026-09-03T11:57:48.489528", + "duration": 41.866478, + "end_time": "2026-10-05T14:28:51.191606", "environment_variables": {}, "exception": null, "input_path": "Lab16-Data-Science-Agent.ipynb", - "output_path": "Lab16-Data-Science-Agent.ipynb", + "output_path": "Lab16_exec4.ipynb", "parameters": {}, - "start_time": "2026-09-03T11:54:35.264845", + "start_time": "2026-10-05T14:28:09.325128", "version": "2.6.0" } }, diff --git a/MyIA.AI.Notebooks/ML/DataScienceWithAgents/Track2-GoogleADK/Day7-Production/Lab17-Final-Project.ipynb b/MyIA.AI.Notebooks/ML/DataScienceWithAgents/Track2-GoogleADK/Day7-Production/Lab17-Final-Project.ipynb index 6d2956b1f9..095ec03a24 100644 --- a/MyIA.AI.Notebooks/ML/DataScienceWithAgents/Track2-GoogleADK/Day7-Production/Lab17-Final-Project.ipynb +++ b/MyIA.AI.Notebooks/ML/DataScienceWithAgents/Track2-GoogleADK/Day7-Production/Lab17-Final-Project.ipynb @@ -5,10 +5,10 @@ "id": "cell-0", "metadata": { "papermill": { - "duration": 0.006766, - "end_time": "2026-05-13T22:34:53.838481+00:00", + "duration": 0.00347, + "end_time": "2026-10-05T14:29:30.118345", "exception": false, - "start_time": "2026-05-13T22:34:53.831715+00:00", + "start_time": "2026-10-05T14:29:30.114875", "status": "completed" }, "tags": [] @@ -47,10 +47,10 @@ "id": "cell-1", "metadata": { "papermill": { - "duration": 0.01083, - "end_time": "2026-05-13T22:34:53.856448+00:00", + "duration": 0.002766, + "end_time": "2026-10-05T14:29:30.124239", "exception": false, - "start_time": "2026-05-13T22:34:53.845618+00:00", + "start_time": "2026-10-05T14:29:30.121473", "status": "completed" }, "tags": [] @@ -58,7 +58,7 @@ "source": [ "## 1. Configuration\n", "\n", - "Cette section importe les briques techniques du pipeline DS-STAR : types standards (`pandas`, `numpy`) pour la manipulation de données, structures (`dataclass`, `Enum`) pour faire circuler l'information entre les agents, et surtout le client LLM (`LLMClient` de `utils`) qui sera partagé par les six composants. L'objectif est de n'avoir **qu'une seule instance de LLM** pour tout le pipeline — cela garantit une configuration cohérente (provider, modèle, température) et évite les fuites de connexion.\n", + "Cette section importe les briques techniques du pipeline DS-STAR : types standards (`pandas`, `numpy`) pour la manipulation de données, structures (`dataclass`, `Enum`) pour faire circuler l'information entre les agents, et surtout le **runtime Google ADK** (`build_agent`, `run_agent_turn` de `utils.adk_runtime`) qui servira chaque rôle LLM du pipeline. Chaque composant reçoit un vrai `google.adk.agents.Agent` — même modèle, même configuration provider — construit et exécuté par l'organ partagé du track : une configuration cohérente pour tout le pipeline, sans fuite de connexion.\n", "\n", "Notez le `sys.path.insert(0, '..')` : les modules `config` et `utils` vivent un niveau au-dessus, partagés avec les autres labs de la série Track2-GoogleADK." ] @@ -69,16 +69,16 @@ "id": "cell-2", "metadata": { "execution": { - "iopub.execute_input": "2026-05-13T22:34:53.872127Z", - "iopub.status.busy": "2026-05-13T22:34:53.871520Z", - "iopub.status.idle": "2026-05-13T22:35:02.866478Z", - "shell.execute_reply": "2026-05-13T22:35:02.864974Z" + "iopub.execute_input": "2026-10-05T14:29:30.131048Z", + "iopub.status.busy": "2026-10-05T14:29:30.130838Z", + "iopub.status.idle": "2026-10-05T14:29:37.114257Z", + "shell.execute_reply": "2026-10-05T14:29:37.113490Z" }, "papermill": { - "duration": 9.004473, - "end_time": "2026-05-13T22:35:02.867708+00:00", + "duration": 6.987838, + "end_time": "2026-10-05T14:29:37.115002", "exception": false, - "start_time": "2026-05-13T22:34:53.863235+00:00", + "start_time": "2026-10-05T14:29:30.127164", "status": "completed" }, "tags": [] @@ -88,7 +88,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "Imports et configuration charges.\n" + "Imports et configuration charges (runtime Google ADK reel).\n" ] } ], @@ -107,9 +107,9 @@ "from pathlib import Path\n", "\n", "from config import get_settings\n", - "from utils import LLMClient\n", + "from utils.adk_runtime import build_agent, run_agent_turn\n", "\n", - "print(\"Imports et configuration charges.\")" + "print(\"Imports et configuration charges (runtime Google ADK reel).\")" ] }, { @@ -117,16 +117,16 @@ "id": "31e55edc", "metadata": { "papermill": { - "duration": 0.004622, - "end_time": "2026-05-13T22:35:02.877669+00:00", + "duration": 0.003217, + "end_time": "2026-10-05T14:29:37.121916", "exception": false, - "start_time": "2026-05-13T22:35:02.873047+00:00", + "start_time": "2026-10-05T14:29:37.118699", "status": "completed" }, "tags": [] }, "source": [ - "Chargement effectif des paramètres via `get_settings()` : cette fonction lit la configuration du provider LLM (clé API, endpoint, modèle actif) depuis l'environnement. L'affichage du provider actif (`openrouter` ici) permet de vérifier instantanément que le bon backend est branché avant de lancer le pipeline — un diagnostic utile quand une exécution échoue silencieusement à cause d'une clé manquante." + "Chargement effectif des paramètres via `get_settings()` : cette fonction lit la configuration du provider LLM (clé API, endpoint, modèle actif) depuis l'environnement. L'affichage du provider actif permet de vérifier instantanément que le bon backend est branché avant de lancer le pipeline — un diagnostic utile quand une exécution échoue silencieusement à cause d'une clé manquante." ] }, { @@ -135,16 +135,16 @@ "id": "cell-3", "metadata": { "execution": { - "iopub.execute_input": "2026-05-13T22:35:02.888642Z", - "iopub.status.busy": "2026-05-13T22:35:02.888009Z", - "iopub.status.idle": "2026-05-13T22:35:02.902328Z", - "shell.execute_reply": "2026-05-13T22:35:02.901078Z" + "iopub.execute_input": "2026-10-05T14:29:37.129461Z", + "iopub.status.busy": "2026-10-05T14:29:37.129005Z", + "iopub.status.idle": "2026-10-05T14:29:37.134435Z", + "shell.execute_reply": "2026-10-05T14:29:37.133593Z" }, "papermill": { - "duration": 0.021396, - "end_time": "2026-05-13T22:35:02.903483+00:00", + "duration": 0.010165, + "end_time": "2026-10-05T14:29:37.135117", "exception": false, - "start_time": "2026-05-13T22:35:02.882087+00:00", + "start_time": "2026-10-05T14:29:37.124952", "status": "completed" }, "tags": [] @@ -154,7 +154,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "Provider: openrouter\n" + "Provider: vllm\n" ] } ], @@ -168,10 +168,10 @@ "id": "cell-4", "metadata": { "papermill": { - "duration": 0.005048, - "end_time": "2026-05-13T22:35:02.913355+00:00", + "duration": 0.003609, + "end_time": "2026-10-05T14:29:37.142115", "exception": false, - "start_time": "2026-05-13T22:35:02.908307+00:00", + "start_time": "2026-10-05T14:29:37.138506", "status": "completed" }, "tags": [] @@ -195,16 +195,16 @@ "id": "cell-5", "metadata": { "execution": { - "iopub.execute_input": "2026-05-13T22:35:02.924116Z", - "iopub.status.busy": "2026-05-13T22:35:02.923618Z", - "iopub.status.idle": "2026-05-13T22:35:02.932666Z", - "shell.execute_reply": "2026-05-13T22:35:02.931362Z" + "iopub.execute_input": "2026-10-05T14:29:37.149362Z", + "iopub.status.busy": "2026-10-05T14:29:37.149102Z", + "iopub.status.idle": "2026-10-05T14:29:37.154544Z", + "shell.execute_reply": "2026-10-05T14:29:37.153819Z" }, "papermill": { - "duration": 0.016088, - "end_time": "2026-05-13T22:35:02.933998+00:00", + "duration": 0.010029, + "end_time": "2026-10-05T14:29:37.155205", "exception": false, - "start_time": "2026-05-13T22:35:02.917910+00:00", + "start_time": "2026-10-05T14:29:37.145176", "status": "completed" }, "tags": [] @@ -253,16 +253,16 @@ "id": "cell-6", "metadata": { "papermill": { - "duration": 0.005114, - "end_time": "2026-05-13T22:35:02.944214+00:00", + "duration": 0.003103, + "end_time": "2026-10-05T14:29:37.161670", "exception": false, - "start_time": "2026-05-13T22:35:02.939100+00:00", + "start_time": "2026-10-05T14:29:37.158567", "status": "completed" }, "tags": [] }, "source": [ - "## 3. Modules DS-STAR\n", + "## 3. Modules DS-STAR (agents ADK)\n", "\n", "Ce projet final integre le pipeline **DS-STAR** complet (FileAnalyzer -> Planner -> Coder ->\n", "Executor -> Verifier -> Reporter) tel que défini par Guo et al. (2025) : un système multi-agent\n", @@ -270,6 +270,11 @@ "résultats avant de produire un rapport. Ce lab orchestre l'**integration end-to-end** de tous les\n", "composants, par opposition a l'étude d'un composant isole (cf. Lab 10 sur le FileAnalyzer).\n", "\n", + "Depuis le portage ADK, les quatre rôles LLM (Planner, Coder, Verifier, Reporter) sont de **vrais\n", + "agents Google ADK** — chacun un `google.adk.agents.Agent` construit par `build_agent()` — et chaque\n", + "étape du pipeline est un tour `run_agent_turn()` servi par le Runner ADK. Les deux composants\n", + "déterministes (FileAnalyzer, Executor) restent du Python pur : ils ne consultent jamais le LLM.\n", + "\n", "> Reference : Nam, J., et al. (2025). *DS-STAR: Data Science Agent for Solving Diverse Tasks across Heterogeneous Formats and Open-Ended Queries\n", "> arXiv:2509.21825. https://arxiv.org/abs/2509.21825" ] @@ -280,16 +285,16 @@ "id": "cell-7", "metadata": { "execution": { - "iopub.execute_input": "2026-05-13T22:35:02.955005Z", - "iopub.status.busy": "2026-05-13T22:35:02.954550Z", - "iopub.status.idle": "2026-05-13T22:35:02.963051Z", - "shell.execute_reply": "2026-05-13T22:35:02.961643Z" + "iopub.execute_input": "2026-10-05T14:29:37.170032Z", + "iopub.status.busy": "2026-10-05T14:29:37.169729Z", + "iopub.status.idle": "2026-10-05T14:29:37.174847Z", + "shell.execute_reply": "2026-10-05T14:29:37.174087Z" }, "papermill": { - "duration": 0.015424, - "end_time": "2026-05-13T22:35:02.964138+00:00", + "duration": 0.01015, + "end_time": "2026-10-05T14:29:37.175415", "exception": false, - "start_time": "2026-05-13T22:35:02.948714+00:00", + "start_time": "2026-10-05T14:29:37.165265", "status": "completed" }, "tags": [] @@ -299,17 +304,15 @@ "name": "stdout", "output_type": "stream", "text": [ - "FileAnalyzer pret.\n" + "FileAnalyzer pret (deterministe, sans LLM).\n" ] } ], "source": [ "class FileAnalyzer:\n", + " \"\"\"Composant deterministe : aucun appel LLM, aucune dependance runtime.\"\"\"\n", " SUPPORTED = {'.csv': 'csv', '.json': 'json'}\n", "\n", - " def __init__(self, llm=None):\n", - " self.llm = llm or LLMClient()\n", - "\n", " def analyze_csv(self, path: str) -> FileMetadata:\n", " df = pd.read_csv(path)\n", " cols = [{'name': c, 'dtype': str(df[c].dtype)} for c in df.columns]\n", @@ -322,9 +325,9 @@ " lines = [f'Fichier: {meta.filename}', f'Lignes: {meta.num_rows}', f'Colonnes: {meta.num_columns}']\n", " if meta.columns:\n", " lines.append('Colonnes: ' + ', '.join([c['name'] for c in meta.columns[:5]]))\n", - " return '\\\\n'.join(lines)\n", + " return '\\n'.join(lines)\n", "\n", - "print(\"FileAnalyzer pret.\")" + "print(\"FileAnalyzer pret (deterministe, sans LLM).\")" ] }, { @@ -332,10 +335,10 @@ "id": "eabe6f50", "metadata": { "papermill": { - "duration": 0.004966, - "end_time": "2026-05-13T22:35:02.974102+00:00", + "duration": 0.003154, + "end_time": "2026-10-05T14:29:37.182174", "exception": false, - "start_time": "2026-05-13T22:35:02.969136+00:00", + "start_time": "2026-10-05T14:29:37.179020", "status": "completed" }, "tags": [] @@ -352,16 +355,16 @@ "id": "cell-8", "metadata": { "execution": { - "iopub.execute_input": "2026-05-13T22:35:02.985918Z", - "iopub.status.busy": "2026-05-13T22:35:02.985461Z", - "iopub.status.idle": "2026-05-13T22:35:02.993001Z", - "shell.execute_reply": "2026-05-13T22:35:02.991663Z" + "iopub.execute_input": "2026-10-05T14:29:37.189680Z", + "iopub.status.busy": "2026-10-05T14:29:37.189360Z", + "iopub.status.idle": "2026-10-05T14:29:37.199968Z", + "shell.execute_reply": "2026-10-05T14:29:37.199121Z" }, "papermill": { - "duration": 0.014938, - "end_time": "2026-05-13T22:35:02.994116+00:00", + "duration": 0.015437, + "end_time": "2026-10-05T14:29:37.200703", "exception": false, - "start_time": "2026-05-13T22:35:02.979178+00:00", + "start_time": "2026-10-05T14:29:37.185266", "status": "completed" }, "tags": [] @@ -371,21 +374,31 @@ "name": "stdout", "output_type": "stream", "text": [ - "Planner pret.\n" + "Planner pret (agent ADK planner_agent).\n" ] } ], "source": [ + "planner_agent = build_agent(\n", + " name='planner_agent',\n", + " description='Planifie les etapes d analyse d une question data science.',\n", + " instruction=(\n", + " \"Tu es un planificateur data science. Reponds STRICTEMENT au format :\\n\"\n", + " \"REASONING: \\n\"\n", + " \"STEPS: 1. 2. 3. \"\n", + " ),\n", + ")\n", + "\n", "class Planner:\n", - " def __init__(self, llm: LLMClient):\n", - " self.llm = llm\n", + " def __init__(self, agent):\n", + " self.agent = agent\n", "\n", - " def create_plan(self, question: str, context: str) -> Plan:\n", + " async def create_plan(self, question: str, context: str) -> Plan:\n", " prompt = f'Planifie: {question}\\nCONTEXTE: {context[:600]}\\nDonne 2-3 etapes. Format: REASONING: [...] STEPS: 1. [...] 2. [...]'\n", - " response = self.llm.generate(prompt, temperature=0.3)\n", + " result = await run_agent_turn(self.agent, prompt)\n", " steps, reasoning = [], ''\n", " in_steps = False\n", - " for line in response.split('\\n'):\n", + " for line in result.response_text.split('\\n'):\n", " if line.startswith('REASONING:'):\n", " reasoning = line.replace('REASONING:', '').strip()\n", " elif line.startswith('STEPS:'):\n", @@ -394,7 +407,7 @@ " steps.append(re.sub(r'^\\d+\\.\\s*', '', line.strip()))\n", " return Plan(steps=steps, reasoning=reasoning)\n", "\n", - "print(\"Planner pret.\")" + "print(\"Planner pret (agent ADK planner_agent).\")" ] }, { @@ -402,16 +415,16 @@ "id": "7ac0f778", "metadata": { "papermill": { - "duration": 0.004481, - "end_time": "2026-05-13T22:35:03.003270+00:00", + "duration": 0.003195, + "end_time": "2026-10-05T14:29:37.207165", "exception": false, - "start_time": "2026-05-13T22:35:02.998789+00:00", + "start_time": "2026-10-05T14:29:37.203970", "status": "completed" }, "tags": [] }, "source": [ - "**Planner** est le cerveau stratégique du pipeline : il prend la question de l'utilisateur + le contexte du fichier et produit un `Plan` (étapes + raisonnement). Notez le prompt structuré (`REASONING:` / `STEPS:`) et le parsing par expression régulière — c'est une approche fragile (visible dans l'interprétation de la section 6 : si le LLM ne respecte pas le format, l'extraction échoue), mais simple à implémenter. La température basse (0.3) favorise des plans cohérents et reproductibles.\n", + "**Planner** est le cerveau stratégique du pipeline : il prend la question de l'utilisateur + le contexte du fichier et produit un `Plan` (étapes + raisonnement). C'est un agent ADK dont l'instruction exige un format structuré (`REASONING:` / `STEPS:`), suivi d'un parsing par expression régulière — une approche fragile (visible dans l'interprétation de la section 6 : si le LLM ne respecte pas le format, l'extraction échoue), mais simple à implémenter. L'instruction stricte de l'agent (le format est répété dans l'instruction système ET dans le prompt du tour) favorise des plans cohérents et reproductibles.\n", "\n", "Le **Coder** reçoit ce plan et génère le code Python correspondant." ] @@ -422,16 +435,16 @@ "id": "cell-9", "metadata": { "execution": { - "iopub.execute_input": "2026-05-13T22:35:03.014825Z", - "iopub.status.busy": "2026-05-13T22:35:03.014332Z", - "iopub.status.idle": "2026-05-13T22:35:03.021972Z", - "shell.execute_reply": "2026-05-13T22:35:03.020388Z" + "iopub.execute_input": "2026-10-05T14:29:37.215009Z", + "iopub.status.busy": "2026-10-05T14:29:37.214516Z", + "iopub.status.idle": "2026-10-05T14:29:37.220885Z", + "shell.execute_reply": "2026-10-05T14:29:37.220090Z" }, "papermill": { - "duration": 0.015005, - "end_time": "2026-05-13T22:35:03.023174+00:00", + "duration": 0.011185, + "end_time": "2026-10-05T14:29:37.221564", "exception": false, - "start_time": "2026-05-13T22:35:03.008169+00:00", + "start_time": "2026-10-05T14:29:37.210379", "status": "completed" }, "tags": [] @@ -441,23 +454,32 @@ "name": "stdout", "output_type": "stream", "text": [ - "Coder pret.\n" + "Coder pret (agent ADK coder_agent).\n" ] } ], "source": [ + "coder_agent = build_agent(\n", + " name='coder_agent',\n", + " description='Genere le code Python d une etape d analyse.',\n", + " instruction=(\n", + " \"Tu es un programmeur Python pour data science. Reponds UNIQUEMENT par un \"\n", + " \"bloc de code delimite par ```python et ```, sans aucun texte hors bloc.\"\n", + " ),\n", + ")\n", + "\n", "class Coder:\n", - " def __init__(self, llm: LLMClient):\n", - " self.llm = llm\n", + " def __init__(self, agent):\n", + " self.agent = agent\n", "\n", - " def generate_code(self, plan: Plan, context: str) -> str:\n", + " async def generate_code(self, plan: Plan, context: str) -> str:\n", " steps_text = '\\n'.join(f'{i+1}. {s}' for i, s in enumerate(plan.steps))\n", " prompt = f'Code Python pour: {steps_text}\\nDataFrame: df. Utilise print(). ```python [code] ```'\n", - " response = self.llm.generate(prompt, temperature=0.2)\n", - " match = re.search(r'```python\\s*(.*?)\\s*```', response, re.DOTALL)\n", + " result = await run_agent_turn(self.agent, prompt)\n", + " match = re.search(r'```python\\s*(.*?)\\s*```', result.response_text, re.DOTALL)\n", " return match.group(1).strip() if match else ''\n", "\n", - "print(\"Coder pret.\")" + "print(\"Coder pret (agent ADK coder_agent).\")" ] }, { @@ -465,16 +487,16 @@ "id": "d3642aa1", "metadata": { "papermill": { - "duration": 0.004982, - "end_time": "2026-05-13T22:35:03.033332+00:00", + "duration": 0.003265, + "end_time": "2026-10-05T14:29:37.228225", "exception": false, - "start_time": "2026-05-13T22:35:03.028350+00:00", + "start_time": "2026-10-05T14:29:37.224960", "status": "completed" }, "tags": [] }, "source": [ - "**Coder** traduit chaque étape du plan en code Python exécutable. Le prompt demande explicitement un bloc ` ```python ` et une extraction par regex isole le code. Le `DataFrame df` est fourni comme contexte pour que le code généré suppose qu'un `df` existe déjà. La température 0.2 rend la génération presque déterministe — on veut du code fiable, pas de la créativité.\n", + "**Coder** traduit chaque étape du plan en code Python exécutable. L'instruction de l'agent exige un bloc ` ```python ` et une extraction par regex isole le code. Le `DataFrame df` est fourni comme contexte pour que le code généré suppose qu'un `df` existe déjà. L'instruction « uniquement le bloc de code, rien d'autre » rend la génération presque déterministe — on veut du code fiable, pas de la créativité.\n", "\n", "Ce code est ensuite exécuté en sandbox par l'**Executor**." ] @@ -485,16 +507,16 @@ "id": "cell-10", "metadata": { "execution": { - "iopub.execute_input": "2026-05-13T22:35:03.045035Z", - "iopub.status.busy": "2026-05-13T22:35:03.044538Z", - "iopub.status.idle": "2026-05-13T22:35:03.052116Z", - "shell.execute_reply": "2026-05-13T22:35:03.050748Z" + "iopub.execute_input": "2026-10-05T14:29:37.235943Z", + "iopub.status.busy": "2026-10-05T14:29:37.235649Z", + "iopub.status.idle": "2026-10-05T14:29:37.240126Z", + "shell.execute_reply": "2026-10-05T14:29:37.239451Z" }, "papermill": { - "duration": 0.015052, - "end_time": "2026-05-13T22:35:03.053139+00:00", + "duration": 0.009647, + "end_time": "2026-10-05T14:29:37.241025", "exception": false, - "start_time": "2026-05-13T22:35:03.038087+00:00", + "start_time": "2026-10-05T14:29:37.231378", "status": "completed" }, "tags": [] @@ -535,10 +557,10 @@ "id": "2c57ab3f", "metadata": { "papermill": { - "duration": 0.004675, - "end_time": "2026-05-13T22:35:03.062886+00:00", + "duration": 0.003201, + "end_time": "2026-10-05T14:29:37.247529", "exception": false, - "start_time": "2026-05-13T22:35:03.058211+00:00", + "start_time": "2026-10-05T14:29:37.244328", "status": "completed" }, "tags": [] @@ -555,16 +577,16 @@ "id": "cell-11", "metadata": { "execution": { - "iopub.execute_input": "2026-05-13T22:35:03.073211Z", - "iopub.status.busy": "2026-05-13T22:35:03.072746Z", - "iopub.status.idle": "2026-05-13T22:35:03.079649Z", - "shell.execute_reply": "2026-05-13T22:35:03.078357Z" + "iopub.execute_input": "2026-10-05T14:29:37.255311Z", + "iopub.status.busy": "2026-10-05T14:29:37.255018Z", + "iopub.status.idle": "2026-10-05T14:29:37.261240Z", + "shell.execute_reply": "2026-10-05T14:29:37.260496Z" }, "papermill": { - "duration": 0.013678, - "end_time": "2026-05-13T22:35:03.080777+00:00", + "duration": 0.011039, + "end_time": "2026-10-05T14:29:37.261885", "exception": false, - "start_time": "2026-05-13T22:35:03.067099+00:00", + "start_time": "2026-10-05T14:29:37.250846", "status": "completed" }, "tags": [] @@ -574,25 +596,34 @@ "name": "stdout", "output_type": "stream", "text": [ - "Verifier pret.\n" + "Verifier pret (agent ADK verifier_agent).\n" ] } ], "source": [ + "verifier_agent = build_agent(\n", + " name='verifier_agent',\n", + " description='Verifie qu un resultat repond a la question posee.',\n", + " instruction=(\n", + " \"Tu es un verificateur. Reponds EXACTEMENT par SUCCESS si le resultat \"\n", + " \"repond a la question, ou NEEDS_REFINEMENT sinon. Aucun autre texte.\"\n", + " ),\n", + ")\n", + "\n", "class Verifier:\n", - " def __init__(self, llm: LLMClient):\n", - " self.llm = llm\n", + " def __init__(self, agent):\n", + " self.agent = agent\n", "\n", - " def verify(self, question: str, result: ExecutionResult) -> Tuple[VerificationStatus, str]:\n", + " async def verify(self, question: str, result: ExecutionResult) -> Tuple[VerificationStatus, str]:\n", " if not result.success:\n", " return VerificationStatus.FAILED, f'Erreur: {result.error}'\n", " prompt = f'Verifie: {question}\\nResultat: {result.output[:400]}\\nReponds SUCCESS ou NEEDS_REFINEMENT.'\n", - " response = self.llm.generate(prompt, temperature=0.1).upper()\n", - " if 'SUCCESS' in response:\n", + " adk_result = await run_agent_turn(self.agent, prompt)\n", + " if 'SUCCESS' in adk_result.response_text.upper():\n", " return VerificationStatus.SUCCESS, 'OK'\n", " return VerificationStatus.NEEDS_REFINEMENT, 'A ameliorer'\n", "\n", - "print(\"Verifier pret.\")" + "print(\"Verifier pret (agent ADK verifier_agent).\")" ] }, { @@ -600,16 +631,16 @@ "id": "9416003d", "metadata": { "papermill": { - "duration": 0.004358, - "end_time": "2026-05-13T22:35:03.090319+00:00", + "duration": 0.003271, + "end_time": "2026-10-05T14:29:37.268592", "exception": false, - "start_time": "2026-05-13T22:35:03.085961+00:00", + "start_time": "2026-10-05T14:29:37.265321", "status": "completed" }, "tags": [] }, "source": [ - "**Verifier** est le garde-fou qualitatif : il demande au LLM si le résultat répond à la question (`SUCCESS` ou `NEEDS_REFINEMENT`). Son verdict pilote la boucle d'itération du pipeline — si le résultat est incomplet, le contexte est enrichi de la sortie précédente et une nouvelle itération est tentée (jusqu'à `max_iterations`). Notez que si l'exécution a échoué (`result.success == False`), le Verifier court-circuite et renvoie `FAILED` sans appeler le LLM — une économie d'appels bienvenue.\n", + "**Verifier** est le garde-fou qualitatif : son agent ADK reçoit le résultat et répond `SUCCESS` ou `NEEDS_REFINEMENT`. Son verdict pilote la boucle d'itération du pipeline — si le résultat est incomplet, le contexte est enrichi de la sortie précédente et une nouvelle itération est tentée (jusqu'à `max_iterations`). Notez que si l'exécution a échoué (`result.success == False`), le Verifier court-circuite et renvoie `FAILED` **sans consommer de tour ADK** — une économie d'appels bienvenue.\n", "\n", "Enfin, le **ReportGenerator** synthétise les résultats en un rapport Markdown." ] @@ -620,16 +651,16 @@ "id": "cell-12", "metadata": { "execution": { - "iopub.execute_input": "2026-05-13T22:35:03.100972Z", - "iopub.status.busy": "2026-05-13T22:35:03.100558Z", - "iopub.status.idle": "2026-05-13T22:35:03.106599Z", - "shell.execute_reply": "2026-05-13T22:35:03.105322Z" + "iopub.execute_input": "2026-10-05T14:29:37.276402Z", + "iopub.status.busy": "2026-10-05T14:29:37.276157Z", + "iopub.status.idle": "2026-10-05T14:29:37.282034Z", + "shell.execute_reply": "2026-10-05T14:29:37.281249Z" }, "papermill": { - "duration": 0.01277, - "end_time": "2026-05-13T22:35:03.107655+00:00", + "duration": 0.010841, + "end_time": "2026-10-05T14:29:37.282724", "exception": false, - "start_time": "2026-05-13T22:35:03.094885+00:00", + "start_time": "2026-10-05T14:29:37.271883", "status": "completed" }, "tags": [] @@ -639,20 +670,30 @@ "name": "stdout", "output_type": "stream", "text": [ - "ReportGenerator pret.\n" + "ReportGenerator pret (agent ADK reporter_agent).\n" ] } ], "source": [ + "reporter_agent = build_agent(\n", + " name='reporter_agent',\n", + " description='Redige le rapport Markdown de synthese d une analyse.',\n", + " instruction=(\n", + " \"Tu rediges des rapports d analyse Markdown en francais, structures en \"\n", + " \"sections : Contexte, Methodologie, Resultats, Observations.\"\n", + " ),\n", + ")\n", + "\n", "class ReportGenerator:\n", - " def __init__(self, llm: LLMClient):\n", - " self.llm = llm\n", + " def __init__(self, agent):\n", + " self.agent = agent\n", "\n", - " def generate_report(self, question: str, results: List[Dict], metadata: FileMetadata) -> str:\n", + " async def generate_report(self, question: str, results: List[Dict], metadata: FileMetadata) -> str:\n", " prompt = f'Genere un rapport Markdown pour: {question}\\nFichier: {metadata.filename} ({metadata.num_rows} lignes)\\nResultats: {len(results)} analyses'\n", - " return self.llm.generate(prompt, temperature=0.3)\n", + " result = await run_agent_turn(self.agent, prompt)\n", + " return result.response_text\n", "\n", - "print(\"ReportGenerator pret.\")" + "print(\"ReportGenerator pret (agent ADK reporter_agent).\")" ] }, { @@ -660,10 +701,10 @@ "id": "cell-13", "metadata": { "papermill": { - "duration": 0.004242, - "end_time": "2026-05-13T22:35:03.116527+00:00", + "duration": 0.003507, + "end_time": "2026-10-05T14:29:37.289937", "exception": false, - "start_time": "2026-05-13T22:35:03.112285+00:00", + "start_time": "2026-10-05T14:29:37.286430", "status": "completed" }, "tags": [] @@ -671,7 +712,7 @@ "source": [ "## 4. Pipeline Complet\n", "\n", - "La classe `DSStarPipeline` est l'**orchestrateur** qui assemble les six composants en une boucle cohérente. C'est le cœur du lab : là où les sections précédentes isolaient chaque agent, cette section montre comment les **composer** — l'apport spécifique d'un projet final par rapport aux labs monocomposant (Lab 10 FileAnalyzer, Lab 11 Planner-Coder).\n", + "La classe `DSStarPipeline` est l'**orchestrateur** qui assemble les six composants en une boucle cohérente. C'est le cœur du lab : là où les sections précédentes isolaient chaque agent, cette section montre comment les **composer** — l'apport spécifique d'un projet final par rapport aux labs monocomposant (Lab 10 FileAnalyzer, Lab 11 Planner-Coder). Depuis le portage ADK, chaque étape LLM de la boucle est un tour `run_agent_turn()` vers l'agent du rôle, exécuté par le Runner ADK.\n", "\n", "La boucle d'itération (`for i in range(self.max_iterations)`) est le mécanisme clé : à chaque tour, Planner → Coder → Executor → Verifier s'enchaînent, et si le Verifier demande un raffinement (`NEEDS_REFINEMENT`), le contexte est enrichi du résultat précédent avant de retenter. C'est l'implémentation concrète du pattern « plan-execute-verify-refine » de la littérature sur les agents (cf. référence DS-STAR)." ] @@ -682,16 +723,16 @@ "id": "cell-14", "metadata": { "execution": { - "iopub.execute_input": "2026-05-13T22:35:03.127093Z", - "iopub.status.busy": "2026-05-13T22:35:03.126665Z", - "iopub.status.idle": "2026-05-13T22:35:03.136545Z", - "shell.execute_reply": "2026-05-13T22:35:03.135374Z" + "iopub.execute_input": "2026-10-05T14:29:37.298007Z", + "iopub.status.busy": "2026-10-05T14:29:37.297802Z", + "iopub.status.idle": "2026-10-05T14:29:37.304315Z", + "shell.execute_reply": "2026-10-05T14:29:37.303506Z" }, "papermill": { - "duration": 0.016997, - "end_time": "2026-05-13T22:35:03.137659+00:00", + "duration": 0.011728, + "end_time": "2026-10-05T14:29:37.305053", "exception": false, - "start_time": "2026-05-13T22:35:03.120662+00:00", + "start_time": "2026-10-05T14:29:37.293325", "status": "completed" }, "tags": [] @@ -701,22 +742,27 @@ "name": "stdout", "output_type": "stream", "text": [ - "DS-STAR Pipeline complet initialise.\n" + "DS-STAR Pipeline complet initialise (4 agents ADK + 2 composants deterministes).\n" ] } ], "source": [ "class DSStarPipeline:\n", + " \"\"\"Orchestrateur DS-STAR : chaque role LLM est un agent ADK reel.\n", + "\n", + " La boucle plan -> code -> execute -> verify reste explicite (c'est le\n", + " concept DS-STAR) ; chaque etape LLM est un tour ADK vers l'agent du role.\n", + " \"\"\"\n", + "\n", " def __init__(self, max_iterations: int = 2):\n", - " self.llm = LLMClient()\n", - " self.file_analyzer = FileAnalyzer(self.llm)\n", - " self.planner = Planner(self.llm)\n", - " self.coder = Coder(self.llm)\n", - " self.verifier = Verifier(self.llm)\n", - " self.reporter = ReportGenerator(self.llm)\n", + " self.file_analyzer = FileAnalyzer()\n", + " self.planner = Planner(planner_agent)\n", + " self.coder = Coder(coder_agent)\n", + " self.verifier = Verifier(verifier_agent)\n", + " self.reporter = ReportGenerator(reporter_agent)\n", " self.max_iterations = max_iterations\n", "\n", - " def analyze(self, file_path: str, question: str) -> Dict:\n", + " async def analyze(self, file_path: str, question: str) -> Dict:\n", " print('='*50 + '\\nDS-STAR PIPELINE\\n' + '='*50)\n", "\n", " meta = self.file_analyzer.analyze_csv(file_path)\n", @@ -729,23 +775,23 @@ "\n", " for i in range(self.max_iterations):\n", " print(f'\\n=== ITERATION {i+1} ===')\n", - " plan = self.planner.create_plan(question, context)\n", + " plan = await self.planner.create_plan(question, context)\n", " print(f'[PLANNER] {len(plan.steps)} etapes')\n", - " code = self.coder.generate_code(plan, context)\n", + " code = await self.coder.generate_code(plan, context)\n", " result = executor.execute(code)\n", " print(f'[EXECUTOR] {\"OK\" if result.success else \"ERROR\"}')\n", " if result.success:\n", - " status, msg = self.verifier.verify(question, result)\n", + " status, msg = await self.verifier.verify(question, result)\n", " print(f'[VERIFIER] {status.value}')\n", " results.append({'output': result.output, 'status': status.value})\n", " if status == VerificationStatus.SUCCESS:\n", " break\n", " context += f'\\nResultat: {result.output[:200]}'\n", "\n", - " report = self.reporter.generate_report(question, results, meta)\n", + " report = await self.reporter.generate_report(question, results, meta)\n", " return {'success': len(results) > 0, 'results': results, 'report': report}\n", "\n", - "print(\"DS-STAR Pipeline complet initialise.\")" + "print(\"DS-STAR Pipeline complet initialise (4 agents ADK + 2 composants deterministes).\")" ] }, { @@ -753,10 +799,10 @@ "id": "cell-15", "metadata": { "papermill": { - "duration": 0.004331, - "end_time": "2026-05-13T22:35:03.146451+00:00", + "duration": 0.003349, + "end_time": "2026-10-05T14:29:37.312990", "exception": false, - "start_time": "2026-05-13T22:35:03.142120+00:00", + "start_time": "2026-10-05T14:29:37.309641", "status": "completed" }, "tags": [] @@ -775,16 +821,16 @@ "id": "cell-16", "metadata": { "execution": { - "iopub.execute_input": "2026-05-13T22:35:03.157452Z", - "iopub.status.busy": "2026-05-13T22:35:03.157051Z", - "iopub.status.idle": "2026-05-13T22:35:03.178121Z", - "shell.execute_reply": "2026-05-13T22:35:03.176871Z" + "iopub.execute_input": "2026-10-05T14:29:37.320890Z", + "iopub.status.busy": "2026-10-05T14:29:37.320678Z", + "iopub.status.idle": "2026-10-05T14:29:37.338151Z", + "shell.execute_reply": "2026-10-05T14:29:37.337417Z" }, "papermill": { - "duration": 0.028726, - "end_time": "2026-05-13T22:35:03.179211+00:00", + "duration": 0.022553, + "end_time": "2026-10-05T14:29:37.338863", "exception": false, - "start_time": "2026-05-13T22:35:03.150485+00:00", + "start_time": "2026-10-05T14:29:37.316310", "status": "completed" }, "tags": [] @@ -794,7 +840,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "Dataset: ~\\AppData\\Local\\Temp\\tmp1bu24mbj\\sales.csv (200 lignes)\n" + "Dataset: \\AppData\\Local\\Temp\\tmpjectve84\\sales.csv (200 lignes)\n" ] } ], @@ -821,10 +867,10 @@ "id": "cell-17", "metadata": { "papermill": { - "duration": 0.004418, - "end_time": "2026-05-13T22:35:03.188803+00:00", + "duration": 0.003321, + "end_time": "2026-10-05T14:29:37.346000", "exception": false, - "start_time": "2026-05-13T22:35:03.184385+00:00", + "start_time": "2026-10-05T14:29:37.342679", "status": "completed" }, "tags": [] @@ -843,16 +889,16 @@ "id": "cell-18", "metadata": { "execution": { - "iopub.execute_input": "2026-05-13T22:35:03.200499Z", - "iopub.status.busy": "2026-05-13T22:35:03.200167Z", - "iopub.status.idle": "2026-05-13T22:35:19.533021Z", - "shell.execute_reply": "2026-05-13T22:35:19.531741Z" + "iopub.execute_input": "2026-10-05T14:29:37.354232Z", + "iopub.status.busy": "2026-10-05T14:29:37.353878Z", + "iopub.status.idle": "2026-10-05T14:29:49.281159Z", + "shell.execute_reply": "2026-10-05T14:29:49.280563Z" }, "papermill": { - "duration": 16.339825, - "end_time": "2026-05-13T22:35:19.533896+00:00", + "duration": 11.932899, + "end_time": "2026-10-05T14:29:49.282254", "exception": false, - "start_time": "2026-05-13T22:35:03.194071+00:00", + "start_time": "2026-10-05T14:29:37.349355", "status": "completed" }, "tags": [] @@ -864,8 +910,21 @@ "text": [ "==================================================\n", "DS-STAR PIPELINE\n", - "==================================================\n", - "[FILE] sales.csv: 200 lignes\n", + "==================================================\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[FILE] sales.csv: 200 lignes" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", "\n", "=== ITERATION 1 ===\n" ] @@ -874,55 +933,65 @@ "name": "stdout", "output_type": "stream", "text": [ - "[PLANNER] 3 etapes\n" + "[PLANNER] 0 etapes\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ - "[EXECUTOR] ERROR\n" + "[EXECUTOR] OK\n", + "[VERIFIER] needs_refinement\n" ] } ], "source": [ "pipeline = DSStarPipeline(max_iterations=1)\n", - "result = pipeline.analyze(csv_path, 'Analyse les ventes par region')" + "result = await pipeline.analyze(csv_path, 'Analyse les ventes par region')" ] }, { "cell_type": "markdown", "id": "58ed476a", + "metadata": { + "papermill": { + "duration": 0.003544, + "end_time": "2026-10-05T14:29:49.289824", + "exception": false, + "start_time": "2026-10-05T14:29:49.286280", + "status": "completed" + }, + "tags": [] + }, "source": [ "### Interpretation : Exécution du pipeline complet\n", "\n", - "**Résultat obtenu** : Le pipeline DS-STAR s'execute sur le dataset de ventes mais produit un résultat partiel — le Planner genere 0 étape et le Verifier classe le résultat comme `needs_refinement`.\n", + "**Résultat obtenu** : Le pipeline DS-STAR (4 agents ADK + 2 composants déterministes) s'execute sur le dataset de ventes mais produit un résultat partiel — le Planner genere 0 étape et le Verifier classe le résultat comme `needs_refinement`.\n", "\n", "**Analyse des étapes** :\n", "\n", "| Composant | Résultat | Observation |\n", "|-----------|----------|-------------|\n", - "| FileAnalyzer | OK | Fichier detecte : 200 lignes |\n", + "| FileAnalyzer | OK | Fichier detecte : dataset de ventes profile |\n", "| Planner | 0 étapes | Le LLM n'a pas decompose la question correctement |\n", "| Coder | Code genere | Malgre 0 étapes formelles, du code est produit |\n", "| Executor | OK | Le code s'execute sans erreur |\n", "| Verifier | needs_refinement | Le résultat ne repond pas completement a la question |\n", "\n", - "**Cause probable** : Le prompt du Planner demande un format strict (REASONING + STEPS numerotes). Si le LLM ne respecte pas exactement ce format, le parseur par expression reguliere echoue a extraire les étapes. C'est une limitation courante des approches de parsing structure basees sur des regex.\n", + "**Cause probable** : Le tour ADK vers `planner_agent` porte le même prompt strict (format `REASONING`/`STEPS` répété dans l'instruction ET dans le prompt du tour). Le modèle local servi par le Runner n'a pas respecté ce format, et le parseur par expression reguliere echoue a extraire les étapes. C'est une limitation courante des approches de parsing structure basees sur des regex — le portage ADK ne la supprime pas : il rend le tour observable, pas le format infaillible.\n", "\n", "**Point pedagogique** : Avec `max_iterations=1`, le pipeline ne peut pas retenter. En augmentant a `max_iterations=2` ou 3, le contexte enrichi après la première itération permettrait au Planner de mieux repondre." - ], - "metadata": {} + ] }, { "cell_type": "markdown", "id": "cell-19", "metadata": { "papermill": { - "duration": 0.005019, - "end_time": "2026-05-13T22:35:19.544013+00:00", + "duration": 0.003773, + "end_time": "2026-10-05T14:29:49.296976", "exception": false, - "start_time": "2026-05-13T22:35:19.538994+00:00", + "start_time": "2026-10-05T14:29:49.293203", "status": "completed" }, "tags": [] @@ -930,7 +999,7 @@ "source": [ "## 7. Résultats\n", "\n", - "Cette section affiche le rapport final généré par le `ReportGenerator`. C'est ici qu'apparaît le phénomène central d'**hallucination** commenté dans l'interprétation suivante : faute d'accès aux chiffres réels produits par l'Executor, le LLM invente des montants plausibles mais faux. Ce défaut — et sa correction par injection des données réelles — est précisément le sujet du 3ᵉ exercice (`SafeReportGenerator` anti-hallucination)." + "Cette section affiche le rapport final généré par le `ReportGenerator` (tour ADK vers `reporter_agent`). C'est ici qu'apparaît le phénomène central commenté dans l'interprétation suivante : faute d'accès aux chiffres réels produits par l'Executor, le rapport reste **générique** — aucune valeur du dataset n'y figure, et selon le tour, ce vide d'ancrage peut aussi se traduire par des montants inventés. Ce défaut — et sa correction par injection des données réelles — est précisément le sujet du 3ᵉ exercice (`SafeReportGenerator` anti-hallucination)." ] }, { @@ -939,16 +1008,16 @@ "id": "cell-20", "metadata": { "execution": { - "iopub.execute_input": "2026-05-13T22:35:19.554626Z", - "iopub.status.busy": "2026-05-13T22:35:19.554091Z", - "iopub.status.idle": "2026-05-13T22:35:19.559815Z", - "shell.execute_reply": "2026-05-13T22:35:19.558651Z" + "iopub.execute_input": "2026-10-05T14:29:49.305098Z", + "iopub.status.busy": "2026-10-05T14:29:49.304784Z", + "iopub.status.idle": "2026-10-05T14:29:49.308476Z", + "shell.execute_reply": "2026-10-05T14:29:49.307796Z" }, "papermill": { - "duration": 0.01228, - "end_time": "2026-05-13T22:35:19.560653+00:00", + "duration": 0.00845, + "end_time": "2026-10-05T14:29:49.309107", "exception": false, - "start_time": "2026-05-13T22:35:19.548373+00:00", + "start_time": "2026-10-05T14:29:49.300657", "status": "completed" }, "tags": [] @@ -963,29 +1032,17 @@ "RAPPORT\n", "==================================================\n", "```markdown\n", - "# Rapport d'Analyse des Ventes par Région\n", + "# Analyse des ventes par région\n", "\n", "## Contexte\n", - "Ce rapport présente une analyse des ventes par région à partir du fichier `sales.csv` contenant 200 lignes de données.\n", - "\n", - "## Données\n", - "- Fichier source : `sales.csv`\n", - "- Nombre de lignes : 200\n", "\n", - "## Résultats de l'analyse\n", - "Aucune analyse n'a pu être réalisée à partir des données fournies. \n", - "Cela peut être dû à plusieurs raisons possibles :\n", - "- Données manquantes ou corrompues dans le fichier.\n", - "- Absence de colonne indiquant la région.\n", - "- Données non exploitables pour une analyse par région.\n", + "Dans ce rapport, nous effectuons une analyse des ventes par région en nous appuyant sur les données fournies dans le fichier \"sales.csv\". La finalité de cette analyse est de comprendre les tendances de vente régionales, pour ensuite pouvoir prendre des décisions basées sur ces informations.\n", "\n", - "## Recommandations\n", - "- Vérifier la structure et la qualité des données dans le fichier `sales.csv`.\n", - "- S'assurer que la colonne région est bien présente et correctement renseignée.\n", - "- Nettoyer les données si nécessaire avant de relancer l'analyse.\n", + "## Méthodologie\n", "\n", - "---\n", - "\n" + "1. **Importation des données** : La base de données fournie est un fichier CSV qui a été chargé dans un environnement d'analyse de données pour une exploitation plus facile.\n", + "2. **Traitement des données** : Les colonnes ont été évaluées pour vérifier leurs noms de colonnes, leur type de données, et les valeurs manquantes ont été traitées.\n", + "3. **Exploration des données** : Les ventes totales par région ont été calculées et visualis\n" ] } ], @@ -997,35 +1054,37 @@ { "cell_type": "markdown", "id": "68099cdf", + "metadata": { + "papermill": { + "duration": 0.003407, + "end_time": "2026-10-05T14:29:49.316177", + "exception": false, + "start_time": "2026-10-05T14:29:49.312770", + "status": "completed" + }, + "tags": [] + }, "source": [ - "### Interpretation : Rapport genere par le LLM\n", + "### Interpretation : Rapport genere par l agent reporter\n", "\n", - "**Résultat obtenu** : Le pipeline produit un rapport Markdown structure avec context, methodologie, analyse par region et observations — malgre l'echec partiel de l'étape de verification.\n", + "**Résultat obtenu** : Le pipeline produit un rapport Markdown structuré en français — titre, `## Contexte`, `## Méthodologie` numérotée (importation, traitement, exploration) — généré par le tour ADK vers `reporter_agent`.\n", "\n", - "**Analyse du rapport** :\n", + "**Analyse du rapport** : la méthodologie raconte les étapes attendues d'une analyse de ventes par région, mais **aucune valeur réelle du dataset n'y figure** : sur les 800 premiers caractères affichés, pas un seul chiffre du dataset analysé, pas un total par région. Ce n'est pas un accident : le prompt du ReportGenerator ne transmet ni les outputs de l'Executor ni le moindre chiffre (uniquement la question, le nom du fichier et le nombre d'analyses). L'agent rédige ce qu'il peut avec ça : une structure plausible.\n", "\n", - "| Section | Contenu | Qualite |\n", - "|---------|---------|---------|\n", - "| Contexte | Description du dataset | Correct (200 lignes, fichier identifie) |\n", - "| Methodologie | Étapes d'analyse | Standard mais generique |\n", - "| Tableau de résultats | Ventes par region | **Données fictives** — le LLM invente les chiffres |\n", - "| Observations | Interpretations | Coherentes avec les données fictives |\n", + "**Point critique** : le défaut observable est l'**absence d'ancrage** — le rapport ne peut pas citer les résultats puisqu'il ne les reçoit pas. Sur un autre tour, le même vide peut se traduire en chiffres inventés (hallucination classique) quand la structure demandée invite à en produire. Dans les deux cas la cause est identique : les données réelles ne circulent pas jusqu'au rapport.\n", "\n", - "**Point critique** : Les montants du tableau (123 456 euros, etc.) sont generes par le LLM et ne correspondent PAS aux données reelles du dataset. Le rapport inclut d'ailleurs une note \"Les montants sont des exemples, a remplacer par les résultats reels\". C'est un phenomene classique d'hallucination du LLM quand il n'a pas acces aux résultats d'exécution.\n", - "\n", - "**Amelioration** : Injecter les résultats reels de l'Executor dans le prompt du ReportGenerator pour eviter l'hallucination des chiffres." - ], - "metadata": {} + "**Amelioration** : Injecter les résultats reels de l'Executor dans le prompt du ReportGenerator — c'est exactement l'objet du 3ᵉ exercice (`SafeReportGenerator`)." + ] }, { "cell_type": "markdown", "id": "ecfcc968", "metadata": { "papermill": { - "duration": 0.004626, - "end_time": "2026-05-13T22:35:19.570150+00:00", + "duration": 0.003631, + "end_time": "2026-10-05T14:29:49.323192", "exception": false, - "start_time": "2026-05-13T22:35:19.565524+00:00", + "start_time": "2026-10-05T14:29:49.319561", "status": "completed" }, "tags": [] @@ -1040,16 +1099,16 @@ "id": "cell-21", "metadata": { "execution": { - "iopub.execute_input": "2026-05-13T22:35:19.581560Z", - "iopub.status.busy": "2026-05-13T22:35:19.581061Z", - "iopub.status.idle": "2026-05-13T22:35:19.588274Z", - "shell.execute_reply": "2026-05-13T22:35:19.586937Z" + "iopub.execute_input": "2026-10-05T14:29:49.331222Z", + "iopub.status.busy": "2026-10-05T14:29:49.331011Z", + "iopub.status.idle": "2026-10-05T14:29:49.334664Z", + "shell.execute_reply": "2026-10-05T14:29:49.333949Z" }, "papermill": { - "duration": 0.014349, - "end_time": "2026-05-13T22:35:19.589282+00:00", + "duration": 0.008668, + "end_time": "2026-10-05T14:29:49.335322", "exception": false, - "start_time": "2026-05-13T22:35:19.574933+00:00", + "start_time": "2026-10-05T14:29:49.326654", "status": "completed" }, "tags": [] @@ -1074,10 +1133,10 @@ "id": "cell-22", "metadata": { "papermill": { - "duration": 0.004445, - "end_time": "2026-05-13T22:35:19.599233+00:00", + "duration": 0.003762, + "end_time": "2026-10-05T14:29:49.342690", "exception": false, - "start_time": "2026-05-13T22:35:19.594788+00:00", + "start_time": "2026-10-05T14:29:49.338928", "status": "completed" }, "tags": [] @@ -1102,10 +1161,10 @@ "id": "467d9b04", "metadata": { "papermill": { - "duration": 0.005697, - "end_time": "2026-05-13T22:35:19.609699+00:00", + "duration": 0.003354, + "end_time": "2026-10-05T14:29:49.349442", "exception": false, - "start_time": "2026-05-13T22:35:19.604002+00:00", + "start_time": "2026-10-05T14:29:49.346088", "status": "completed" }, "tags": [] @@ -1131,16 +1190,16 @@ "id": "2acbc79a", "metadata": { "execution": { - "iopub.execute_input": "2026-05-13T22:35:19.621918Z", - "iopub.status.busy": "2026-05-13T22:35:19.621446Z", - "iopub.status.idle": "2026-05-13T22:35:19.627262Z", - "shell.execute_reply": "2026-05-13T22:35:19.625940Z" + "iopub.execute_input": "2026-10-05T14:29:49.357417Z", + "iopub.status.busy": "2026-10-05T14:29:49.357126Z", + "iopub.status.idle": "2026-10-05T14:29:49.361036Z", + "shell.execute_reply": "2026-10-05T14:29:49.360290Z" }, "papermill": { - "duration": 0.013601, - "end_time": "2026-05-13T22:35:19.628406+00:00", + "duration": 0.008912, + "end_time": "2026-10-05T14:29:49.361713", "exception": false, - "start_time": "2026-05-13T22:35:19.614805+00:00", + "start_time": "2026-10-05T14:29:49.352801", "status": "completed" }, "tags": [] @@ -1178,7 +1237,7 @@ "# Exercice: Executez le pipeline pour chaque question\n", "# pipeline = DSStarPipeline(max_iterations=2)\n", "# for q in questions:\n", - "# result = pipeline.analyze('data_temp.csv', q)\n", + "# result = await pipeline.analyze('data_temp.csv', q)\n", "# print(f\"\\n{q}: {result['success']}\")\n", "\n", "# Exercice: Generez un rapport de synthese\n", @@ -1203,27 +1262,67 @@ { "cell_type": "markdown", "id": "8ba2e35c", + "metadata": { + "papermill": { + "duration": 0.003444, + "end_time": "2026-10-05T14:29:49.368816", + "exception": false, + "start_time": "2026-10-05T14:29:49.365372", + "status": "completed" + }, + "tags": [] + }, "source": [ "## Exercice : Benchmark du Pipeline avec Différentes Configurations\n", "\n", - "Testez le pipeline DS-STAR avec différentes configurations de paramètres (nombre d'itérations, temperature du LLM, longueur du contexte) et mesurez l'impact sur la qualite des résultats.\n", + "Testez le pipeline DS-STAR avec différentes configurations de paramètres (nombre d'itérations, richesse du contexte passé au Planner) et mesurez l'impact sur la qualite des résultats.\n", "\n", "### Objectifs\n", "1. Définir 3 configurations avec des paramètres différents\n", "2. Executer le pipeline sur la même question pour chaque configuration\n", - "3. Comparer les résultats (succes, nombre d'itérations, qualite de l'output)\n", + "3. Comparer les résultats (succes, nombre d'itérations, qualité de l'output)\n", "\n", "**Indice :**\n", - "- Configuration 1: `max_iterations=1`, temperature=0.1 (conservateur)\n", - "- Configuration 2: `max_iterations=3`, temperature=0.3 (equilibre)\n", - "- Configuration 3: `max_iterations=2`, temperature=0.7 (creatif)\n", + "- Configuration 1: `max_iterations=1` (une seule tentative, pas de raffinement)\n", + "- Configuration 2: `max_iterations=3` (le Verifier peut exiger plusieurs itérations)\n", + "- Configuration 3: `max_iterations=2` + contexte enrichi (collez un extrait du `df.head()` dans la question)\n", "- Mesurez le taux de succes et le nombre moyen d'itérations pour chaque config" - ], - "metadata": {} + ] }, { "cell_type": "code", + "execution_count": 16, "id": "3671b527", + "metadata": { + "execution": { + "iopub.execute_input": "2026-10-05T14:29:49.376872Z", + "iopub.status.busy": "2026-10-05T14:29:49.376574Z", + "iopub.status.idle": "2026-10-05T14:29:49.386352Z", + "shell.execute_reply": "2026-10-05T14:29:49.385588Z" + }, + "papermill": { + "duration": 0.014806, + "end_time": "2026-10-05T14:29:49.387007", + "exception": false, + "start_time": "2026-10-05T14:29:49.372201", + "status": "completed" + }, + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Configuration | Succes | Iterations\n", + "--------------|--------|-----------\n", + "conservateur | ? | ?\n", + "equilibre | ? | ?\n", + "creatif | ? | ?\n", + "Exercice a completer : benchmark du pipeline avec differentes configurations\n" + ] + } + ], "source": [ "# Exercice : Benchmark du pipeline DS-STAR avec differentes configurations\n", "# Objectif : Mesurer l'impact des parametres sur la qualite des resultats\n", @@ -1258,9 +1357,9 @@ "resultats_benchmark = []\n", "for config in configurations:\n", " # Indice: creez un pipeline avec max_iterations=config['max_iterations']\n", - " # Note: la temperature se configure dans le LLMClient, pas directement dans le pipeline\n", + " # Note: la temperature est celle du provider configure (.env du track), pas un parametre du pipeline\n", " # pipeline = DSStarPipeline(max_iterations=config['max_iterations'])\n", - " # result = pipeline.analyze(bench_path, test_question)\n", + " # result = await pipeline.analyze(bench_path, test_question)\n", " \n", " resultats_benchmark.append({\n", " 'config': config['nom'],\n", @@ -1283,22 +1382,21 @@ "shutil.rmtree(bench_dir)\n", "\n", "print(\"Exercice a completer : benchmark du pipeline avec differentes configurations\")" - ], - "metadata": {}, - "execution_count": 16, - "outputs": [ - { - "output_type": "stream", - "name": "stdout", - "text": [ - "Exercice a completer\n" - ] - } ] }, { "cell_type": "markdown", "id": "fcbedf7f", + "metadata": { + "papermill": { + "duration": 0.003483, + "end_time": "2026-10-05T14:29:49.394999", + "exception": false, + "start_time": "2026-10-05T14:29:49.391516", + "status": "completed" + }, + "tags": [] + }, "source": [ "## Exercice : Amelioration du ReportGenerator Anti-Hallucination\n", "\n", @@ -1322,12 +1420,37 @@ "- Le prompt actuel demande \"genere un rapport\" sans fournir les résultats concrets\n", "- Injectez les outputs reels dans le prompt avec une instruction stricte \"n'invente aucun chiffre\"\n", "- Ajoutez un avertissement \"Si tu n'as pas de données pour une section, indique 'Données non disponibles'\"" - ], - "metadata": {} + ] }, { "cell_type": "code", + "execution_count": 17, "id": "bb6cef6c", + "metadata": { + "execution": { + "iopub.execute_input": "2026-10-05T14:29:49.403225Z", + "iopub.status.busy": "2026-10-05T14:29:49.403008Z", + "iopub.status.idle": "2026-10-05T14:29:49.409200Z", + "shell.execute_reply": "2026-10-05T14:29:49.408258Z" + }, + "papermill": { + "duration": 0.01165, + "end_time": "2026-10-05T14:29:49.410062", + "exception": false, + "start_time": "2026-10-05T14:29:49.398412", + "status": "completed" + }, + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Exercice a completer : SafeReportGenerator anti-hallucination\n" + ] + } + ], "source": [ "# Exercice : SafeReportGenerator - Anti-Hallucination\n", "# Objectif : Creer un generateur de rapport qui n'invente pas de donnees\n", @@ -1335,10 +1458,17 @@ "class SafeReportGenerator:\n", " \"\"\"Generateur de rapport qui n'utilise que les donnees reelles injectees.\"\"\"\n", " \n", - " def __init__(self, llm: LLMClient):\n", - " self.llm = llm\n", + " def __init__(self):\n", + " self.agent = build_agent(\n", + " name='safe_reporter_agent',\n", + " description='Redige un rapport Markdown strictement ancre sur les donnees fournies.',\n", + " instruction=(\n", + " \"Tu rediges des rapports d analyse Markdown. Tu n inventes AUCUN chiffre : \"\n", + " \"tu ne cites que les donnees fournies dans la demande.\"\n", + " ),\n", + " )\n", " \n", - " def generate_safe_report(self, question: str, outputs: list, metadata) -> str:\n", + " async def generate_safe_report(self, question: str, outputs: list, metadata) -> str:\n", " \"\"\"\n", " Genere un rapport en n'utilisant QUE les donnees fournies.\n", " \n", @@ -1375,7 +1505,8 @@ "\n", "Rapport:\"\"\"\n", " \n", - " return self.llm.generate(prompt, temperature=0.1)\n", + " adk_result = await run_agent_turn(self.agent, prompt)\n", + " return adk_result.response_text\n", " \n", " def generate_from_template(self, question: str, outputs: list, metadata) -> str:\n", " \"\"\"\n", @@ -1397,12 +1528,12 @@ " return report\n", "\n", "# TODO: Testez les deux methodes et comparez\n", - "# safe_reporter = SafeReportGenerator(LLMClient())\n", + "# safe_reporter = SafeReportGenerator()\n", "# \n", "# # Utilisez des outputs reels du pipeline\n", "# test_outputs = [\"Nord: 12500.50, Sud: 18300.75, Est: 9800.20\"]\n", "# \n", - "# rapport_safe = safe_reporter.generate_safe_report(\n", + "# rapport_safe = await safe_reporter.generate_safe_report(\n", "# \"Revenu par region\", test_outputs, meta\n", "# )\n", "# rapport_template = safe_reporter.generate_from_template(\n", @@ -1415,17 +1546,6 @@ "# print(rapport_template[:300])\n", "\n", "print(\"Exercice a completer : SafeReportGenerator anti-hallucination\")" - ], - "metadata": {}, - "execution_count": 17, - "outputs": [ - { - "output_type": "stream", - "name": "stdout", - "text": [ - "Exercice a completer\n" - ] - } ] }, { @@ -1433,10 +1553,10 @@ "id": "429c50f5", "metadata": { "papermill": { - "duration": 0.005288, - "end_time": "2026-05-13T22:35:19.639108+00:00", + "duration": 0.003951, + "end_time": "2026-10-05T14:29:49.417952", "exception": false, - "start_time": "2026-05-13T22:35:19.633820+00:00", + "start_time": "2026-10-05T14:29:49.414001", "status": "completed" }, "tags": [] @@ -1457,18 +1577,44 @@ }, { "cell_type": "markdown", - "metadata": {}, + "id": "cell-848aa1a6", + "metadata": { + "papermill": { + "duration": 0.003793, + "end_time": "2026-10-05T14:29:49.425807", + "exception": false, + "start_time": "2026-10-05T14:29:49.422014", + "status": "completed" + }, + "tags": [] + }, "source": [ "## References\n", "\n", "- Nam, J., et al. (2025). *DS-STAR: Data Science Agent for Solving Diverse Tasks across Heterogeneous Formats and Open-Ended Queries*. arXiv:2509.21825. https://arxiv.org/abs/2509.21825\n", "- Ji, Z., Lee, N., Frieske, R., et al. (2023). *Survey of Hallucination in Natural Language Generation*. ACM Computing Surveys 55. arXiv:2202.03629. https://arxiv.org/abs/2202.03629\n", "- Xi, Z., et al. (2023). *The Rise and Potential of Large Language Model Based Agents: A Survey*. arXiv:2309.07864." - ], - "id": "cell-848aa1a6" + ] } ], "metadata": { + "cost": { + "api_provider": "google", + "api_usd_est": 1.0, + "cpu_min": 4, + "external_account": "google", + "free_alternative": "ollama", + "gpu_min": 0, + "gpu_required": false, + "metadata_written": "2026-07-23T13:00Z", + "network": true, + "notes": "Projet final intégrant tous les composants : Planner-Coder-\nVerifier (DS-STAR) + Web Search SOTA + Ablation + Raffinement.\nPipeline agentic de data science de bout\nen bout sur un projet libre (étudiant choisit le dataset).\nCoût ~$1.00/run sur Gemini-2.5-Flash free tier (limite 15 RPM\npeut saturer sur pipeline long). Ollama local recommandé pour\nitérations sans coût (GPU 24 GB pour modèles 70B).\n---", + "reduced_pedagogical": "gemini-2.5-flash-free-tier", + "reproducibility": "LOW", + "validator": "papermill", + "vram_gb": 4, + "vram_tier": "LITE" + }, "kernelspec": { "display_name": "Python 3", "language": "python", @@ -1484,36 +1630,19 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.13.12" + "version": "3.13.13" }, "papermill": { "default_parameters": {}, - "duration": 29.864654, - "end_time": "2026-05-13T22:35:20.617417+00:00", + "duration": 22.28761, + "end_time": "2026-10-05T14:29:50.653262", "environment_variables": {}, "exception": null, "input_path": "Lab17-Final-Project.ipynb", - "output_path": "Lab17-Final-Project.ipynb", + "output_path": "Lab17_exec.ipynb", "parameters": {}, - "start_time": "2026-05-13T22:34:50.752763+00:00", + "start_time": "2026-10-05T14:29:28.365652", "version": "2.6.0" - }, - "cost": { - "api_provider": "google", - "network": true, - "cpu_min": 4, - "api_usd_est": 1.0, - "metadata_written": "2026-07-23T13:00Z", - "vram_tier": "LITE", - "validator": "papermill", - "reproducibility": "LOW", - "gpu_required": false, - "gpu_min": 0, - "vram_gb": 4, - "reduced_pedagogical": "gemini-2.5-flash-free-tier", - "external_account": "google", - "free_alternative": "ollama", - "notes": "Projet final intégrant tous les composants : Planner-Coder-\nVerifier (DS-STAR) + Web Search SOTA + Ablation + Raffinement.\nPipeline agentic de data science de bout\nen bout sur un projet libre (étudiant choisit le dataset).\nCoût ~$1.00/run sur Gemini-2.5-Flash free tier (limite 15 RPM\npeut saturer sur pipeline long). Ollama local recommandé pour\nitérations sans coût (GPU 24 GB pour modèles 70B).\n---" } }, "nbformat": 4,