mirror of
https://github.com/ArthurDanjou/ArtStudies.git
synced 2026-01-31 04:29:31 +01:00
1992 lines
136 KiB
Plaintext
1992 lines
136 KiB
Plaintext
{
|
||
"cells": [
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "15419443",
|
||
"metadata": {},
|
||
"source": [
|
||
"# TP4 - Agents\n",
|
||
"\n",
|
||
"Dans ce notebook on s'intéresse à la méthodologie agent en utilisant le framework technique LangChain. Notre application sera l'api de la World Bank Data qui est une source de données à l'échelle mondiale sur l'état, d'un point de vue statistique, d'un pays.\n",
|
||
"La difficulté de cette API est qu'elle est très riche, avec des indicateurs ayant des codes complexes. De plus, les codes des pays sont spécifiques, la manière de requêter est précise.\n",
|
||
"\n",
|
||
"Pour exploiter du mieux possible les données disponibles, on se propose de définir un agent dont le rôle est de traiter une questions posées en langage naturel et requêter puis analyser les résultats.\n",
|
||
"\n",
|
||
"## Trouver le bon indicateur\n",
|
||
"\n",
|
||
"On commence par le premier enjeu : traduire une question en une liste d'indicateurs potentiel permettant d'aider à répondre. Pour ce faire, nous avons récupérer l'ensemble des indicateurs disponible et la descriptions associées."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 54,
|
||
"id": "2eaa0a80",
|
||
"metadata": {},
|
||
"outputs": [
|
||
{
|
||
"data": {
|
||
"application/vnd.microsoft.datawrangler.viewer.v0+json": {
|
||
"columns": [
|
||
{
|
||
"name": "index",
|
||
"rawType": "int64",
|
||
"type": "integer"
|
||
},
|
||
{
|
||
"name": "indicator",
|
||
"rawType": "object",
|
||
"type": "string"
|
||
},
|
||
{
|
||
"name": "description",
|
||
"rawType": "object",
|
||
"type": "string"
|
||
},
|
||
{
|
||
"name": "source",
|
||
"rawType": "object",
|
||
"type": "string"
|
||
},
|
||
{
|
||
"name": "source_name",
|
||
"rawType": "object",
|
||
"type": "string"
|
||
},
|
||
{
|
||
"name": "index",
|
||
"rawType": "int64",
|
||
"type": "integer"
|
||
},
|
||
{
|
||
"name": "embedding_text",
|
||
"rawType": "object",
|
||
"type": "string"
|
||
}
|
||
],
|
||
"ref": "845e8479-ec9a-438a-96e2-582d02120544",
|
||
"rows": [
|
||
[
|
||
"0",
|
||
"1.0.HCount.1.90usd",
|
||
"Poverty Headcount ($1.90 a day)",
|
||
"topics",
|
||
"Poverty",
|
||
"11",
|
||
"Indicator code: 1.0.HCount.1.90usd\nIndicator description: Poverty Headcount ($1.90 a day)\nIndicator source name: Poverty"
|
||
],
|
||
[
|
||
"1",
|
||
"1.0.HCount.2.5usd",
|
||
"Poverty Headcount ($2.50 a day)",
|
||
"topics",
|
||
"Poverty",
|
||
"11",
|
||
"Indicator code: 1.0.HCount.2.5usd\nIndicator description: Poverty Headcount ($2.50 a day)\nIndicator source name: Poverty"
|
||
],
|
||
[
|
||
"2",
|
||
"1.0.HCount.Mid10to50",
|
||
"Middle Class ($10-50 a day) Headcount",
|
||
"topics",
|
||
"Poverty",
|
||
"11",
|
||
"Indicator code: 1.0.HCount.Mid10to50\nIndicator description: Middle Class ($10-50 a day) Headcount\nIndicator source name: Poverty"
|
||
],
|
||
[
|
||
"3",
|
||
"1.0.HCount.Ofcl",
|
||
"Official Moderate Poverty Rate-National",
|
||
"topics",
|
||
"Poverty",
|
||
"11",
|
||
"Indicator code: 1.0.HCount.Ofcl\nIndicator description: Official Moderate Poverty Rate-National\nIndicator source name: Poverty"
|
||
],
|
||
[
|
||
"4",
|
||
"1.0.HCount.Poor4uds",
|
||
"Poverty Headcount ($4 a day)",
|
||
"topics",
|
||
"Poverty",
|
||
"11",
|
||
"Indicator code: 1.0.HCount.Poor4uds\nIndicator description: Poverty Headcount ($4 a day)\nIndicator source name: Poverty"
|
||
]
|
||
],
|
||
"shape": {
|
||
"columns": 6,
|
||
"rows": 5
|
||
}
|
||
},
|
||
"text/html": [
|
||
"<div>\n",
|
||
"<style scoped>\n",
|
||
" .dataframe tbody tr th:only-of-type {\n",
|
||
" vertical-align: middle;\n",
|
||
" }\n",
|
||
"\n",
|
||
" .dataframe tbody tr th {\n",
|
||
" vertical-align: top;\n",
|
||
" }\n",
|
||
"\n",
|
||
" .dataframe thead th {\n",
|
||
" text-align: right;\n",
|
||
" }\n",
|
||
"</style>\n",
|
||
"<table border=\"1\" class=\"dataframe\">\n",
|
||
" <thead>\n",
|
||
" <tr style=\"text-align: right;\">\n",
|
||
" <th></th>\n",
|
||
" <th>indicator</th>\n",
|
||
" <th>description</th>\n",
|
||
" <th>source</th>\n",
|
||
" <th>source_name</th>\n",
|
||
" <th>index</th>\n",
|
||
" <th>embedding_text</th>\n",
|
||
" </tr>\n",
|
||
" </thead>\n",
|
||
" <tbody>\n",
|
||
" <tr>\n",
|
||
" <th>0</th>\n",
|
||
" <td>1.0.HCount.1.90usd</td>\n",
|
||
" <td>Poverty Headcount ($1.90 a day)</td>\n",
|
||
" <td>topics</td>\n",
|
||
" <td>Poverty</td>\n",
|
||
" <td>11</td>\n",
|
||
" <td>Indicator code: 1.0.HCount.1.90usd\\nIndicator ...</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>1</th>\n",
|
||
" <td>1.0.HCount.2.5usd</td>\n",
|
||
" <td>Poverty Headcount ($2.50 a day)</td>\n",
|
||
" <td>topics</td>\n",
|
||
" <td>Poverty</td>\n",
|
||
" <td>11</td>\n",
|
||
" <td>Indicator code: 1.0.HCount.2.5usd\\nIndicator d...</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>2</th>\n",
|
||
" <td>1.0.HCount.Mid10to50</td>\n",
|
||
" <td>Middle Class ($10-50 a day) Headcount</td>\n",
|
||
" <td>topics</td>\n",
|
||
" <td>Poverty</td>\n",
|
||
" <td>11</td>\n",
|
||
" <td>Indicator code: 1.0.HCount.Mid10to50\\nIndicato...</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>3</th>\n",
|
||
" <td>1.0.HCount.Ofcl</td>\n",
|
||
" <td>Official Moderate Poverty Rate-National</td>\n",
|
||
" <td>topics</td>\n",
|
||
" <td>Poverty</td>\n",
|
||
" <td>11</td>\n",
|
||
" <td>Indicator code: 1.0.HCount.Ofcl\\nIndicator des...</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>4</th>\n",
|
||
" <td>1.0.HCount.Poor4uds</td>\n",
|
||
" <td>Poverty Headcount ($4 a day)</td>\n",
|
||
" <td>topics</td>\n",
|
||
" <td>Poverty</td>\n",
|
||
" <td>11</td>\n",
|
||
" <td>Indicator code: 1.0.HCount.Poor4uds\\nIndicator...</td>\n",
|
||
" </tr>\n",
|
||
" </tbody>\n",
|
||
"</table>\n",
|
||
"</div>"
|
||
],
|
||
"text/plain": [
|
||
" indicator description source \\\n",
|
||
"0 1.0.HCount.1.90usd Poverty Headcount ($1.90 a day) topics \n",
|
||
"1 1.0.HCount.2.5usd Poverty Headcount ($2.50 a day) topics \n",
|
||
"2 1.0.HCount.Mid10to50 Middle Class ($10-50 a day) Headcount topics \n",
|
||
"3 1.0.HCount.Ofcl Official Moderate Poverty Rate-National topics \n",
|
||
"4 1.0.HCount.Poor4uds Poverty Headcount ($4 a day) topics \n",
|
||
"\n",
|
||
" source_name index embedding_text \n",
|
||
"0 Poverty 11 Indicator code: 1.0.HCount.1.90usd\\nIndicator ... \n",
|
||
"1 Poverty 11 Indicator code: 1.0.HCount.2.5usd\\nIndicator d... \n",
|
||
"2 Poverty 11 Indicator code: 1.0.HCount.Mid10to50\\nIndicato... \n",
|
||
"3 Poverty 11 Indicator code: 1.0.HCount.Ofcl\\nIndicator des... \n",
|
||
"4 Poverty 11 Indicator code: 1.0.HCount.Poor4uds\\nIndicator... "
|
||
]
|
||
},
|
||
"execution_count": 54,
|
||
"metadata": {},
|
||
"output_type": "execute_result"
|
||
}
|
||
],
|
||
"source": [
|
||
"import pandas as pd\n",
|
||
"\n",
|
||
"\n",
|
||
"def build_embedding_text(row: pd.Series) -> str:\n",
|
||
" \"\"\"Build the text to be embedded for a given row in the indicators dataframe.\"\"\"\n",
|
||
" return f\"\"\"Indicator code: {row[\"indicator\"]}\\nIndicator description: {row[\"description\"]}\\nIndicator source name: {row[\"source_name\"]}\"\"\"\n",
|
||
"\n",
|
||
"\n",
|
||
"df_indicators = pd.read_csv(\"./data/WBData_indicators.csv\")\n",
|
||
"\n",
|
||
"df_indicators[\"embedding_text\"] = df_indicators.apply(build_embedding_text, axis=1)\n",
|
||
"df_indicators.head()"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "bbabba49",
|
||
"metadata": {},
|
||
"source": [
|
||
"Nous allons construire un embedding de la colonne *embedding_text* pour pouvoir dans un second temps la requêter avec la méthoode FAISS.\n",
|
||
"\n",
|
||
"**Consigne** : En exploitant les fonctions `build_faiss_index` et `retrieve_index` dans le module `rag_utils`, constuire l'embedding et le tester sur question."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 55,
|
||
"id": "9aff2854",
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"import faiss\n",
|
||
"from sentence_transformers import SentenceTransformer\n",
|
||
"\n",
|
||
"import pandas as pd\n",
|
||
"\n",
|
||
"MODEL_NAME = \"all-MiniLM-L6-v2\"\n",
|
||
"model_embedding = SentenceTransformer(MODEL_NAME)\n",
|
||
"\n",
|
||
"\n",
|
||
"def build_faiss_index(\n",
|
||
" texts: [str],\n",
|
||
" show: bool = True, # noqa: FBT001, FBT002\n",
|
||
" batch_size: int = 64,\n",
|
||
") -> faiss.IndexFlatIP:\n",
|
||
" \"\"\"Build a FAISS index from a list of texts.\"\"\"\n",
|
||
" embeddings = model_embedding.encode(\n",
|
||
" texts,\n",
|
||
" batch_size=batch_size,\n",
|
||
" show_progress_bar=show,\n",
|
||
" normalize_embeddings=True,\n",
|
||
" )\n",
|
||
" dimension = embeddings.shape[1]\n",
|
||
" index = faiss.IndexFlatIP(dimension)\n",
|
||
" index.add(embeddings)\n",
|
||
" return index\n",
|
||
"\n",
|
||
"\n",
|
||
"def retrieve_index(\n",
|
||
" dataframe: pd.DataFrame,\n",
|
||
" query: str,\n",
|
||
" index: faiss.IndexFlatIP,\n",
|
||
" k: int = 10,\n",
|
||
") -> pd.DataFrame:\n",
|
||
" \"\"\"Retrieve the top k most similar entries from the dataframe for the given query.\"\"\"\n",
|
||
" query_embbeding = model_embedding.encode([query], normalize_embeddings=True).astype(\n",
|
||
" \"float32\",\n",
|
||
" )\n",
|
||
" scores, indices = index.search(query_embbeding, k)\n",
|
||
"\n",
|
||
" results = dataframe.iloc[indices[0]].copy()\n",
|
||
" results[\"score\"] = scores[0]\n",
|
||
"\n",
|
||
" return results.sort_values(\"score\", ascending=False)\n"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 56,
|
||
"id": "8716ab99",
|
||
"metadata": {},
|
||
"outputs": [
|
||
{
|
||
"name": "stderr",
|
||
"output_type": "stream",
|
||
"text": [
|
||
"Batches: 100%|██████████| 409/409 [00:45<00:00, 9.08it/s]\n"
|
||
]
|
||
},
|
||
{
|
||
"data": {
|
||
"application/vnd.microsoft.datawrangler.viewer.v0+json": {
|
||
"columns": [
|
||
{
|
||
"name": "index",
|
||
"rawType": "int64",
|
||
"type": "integer"
|
||
},
|
||
{
|
||
"name": "indicator",
|
||
"rawType": "object",
|
||
"type": "string"
|
||
},
|
||
{
|
||
"name": "description",
|
||
"rawType": "object",
|
||
"type": "string"
|
||
},
|
||
{
|
||
"name": "score",
|
||
"rawType": "float32",
|
||
"type": "float"
|
||
}
|
||
],
|
||
"ref": "6e8d5b84-f1f7-4d37-9a5f-a4696cab9084",
|
||
"rows": [
|
||
[
|
||
"6689",
|
||
"HD.HCI.LAYS",
|
||
"Learning-Adjusted Years of School",
|
||
"0.6565653"
|
||
],
|
||
[
|
||
"14239",
|
||
"SE.PRM.INFR",
|
||
"Basic Infrastructure",
|
||
"0.65512097"
|
||
],
|
||
[
|
||
"17416",
|
||
"UIS.ESG.LOWERSEC.NCOG.ENJO.M",
|
||
"Percentage of students in lower secondary education showing proficiency in knowledge of environmental science and geoscience, Non-cognitive dimension, Enjoyment, male (%)",
|
||
"0.645964"
|
||
],
|
||
[
|
||
"14421",
|
||
"SE.PRM.PEDG.4.M",
|
||
"(De Facto) Percent of teachers with good practices on socioemotional skills (3 or above on Teach Socioemotional Skills score) - Male",
|
||
"0.6426065"
|
||
],
|
||
[
|
||
"14403",
|
||
"SE.PRM.PEDG",
|
||
"Teacher Pedagogical Skills",
|
||
"0.6393548"
|
||
]
|
||
],
|
||
"shape": {
|
||
"columns": 3,
|
||
"rows": 5
|
||
}
|
||
},
|
||
"text/html": [
|
||
"<div>\n",
|
||
"<style scoped>\n",
|
||
" .dataframe tbody tr th:only-of-type {\n",
|
||
" vertical-align: middle;\n",
|
||
" }\n",
|
||
"\n",
|
||
" .dataframe tbody tr th {\n",
|
||
" vertical-align: top;\n",
|
||
" }\n",
|
||
"\n",
|
||
" .dataframe thead th {\n",
|
||
" text-align: right;\n",
|
||
" }\n",
|
||
"</style>\n",
|
||
"<table border=\"1\" class=\"dataframe\">\n",
|
||
" <thead>\n",
|
||
" <tr style=\"text-align: right;\">\n",
|
||
" <th></th>\n",
|
||
" <th>indicator</th>\n",
|
||
" <th>description</th>\n",
|
||
" <th>score</th>\n",
|
||
" </tr>\n",
|
||
" </thead>\n",
|
||
" <tbody>\n",
|
||
" <tr>\n",
|
||
" <th>6689</th>\n",
|
||
" <td>HD.HCI.LAYS</td>\n",
|
||
" <td>Learning-Adjusted Years of School</td>\n",
|
||
" <td>0.656565</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>14239</th>\n",
|
||
" <td>SE.PRM.INFR</td>\n",
|
||
" <td>Basic Infrastructure</td>\n",
|
||
" <td>0.655121</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>17416</th>\n",
|
||
" <td>UIS.ESG.LOWERSEC.NCOG.ENJO.M</td>\n",
|
||
" <td>Percentage of students in lower secondary educ...</td>\n",
|
||
" <td>0.645964</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>14421</th>\n",
|
||
" <td>SE.PRM.PEDG.4.M</td>\n",
|
||
" <td>(De Facto) Percent of teachers with good pract...</td>\n",
|
||
" <td>0.642606</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>14403</th>\n",
|
||
" <td>SE.PRM.PEDG</td>\n",
|
||
" <td>Teacher Pedagogical Skills</td>\n",
|
||
" <td>0.639355</td>\n",
|
||
" </tr>\n",
|
||
" </tbody>\n",
|
||
"</table>\n",
|
||
"</div>"
|
||
],
|
||
"text/plain": [
|
||
" indicator \\\n",
|
||
"6689 HD.HCI.LAYS \n",
|
||
"14239 SE.PRM.INFR \n",
|
||
"17416 UIS.ESG.LOWERSEC.NCOG.ENJO.M \n",
|
||
"14421 SE.PRM.PEDG.4.M \n",
|
||
"14403 SE.PRM.PEDG \n",
|
||
"\n",
|
||
" description score \n",
|
||
"6689 Learning-Adjusted Years of School 0.656565 \n",
|
||
"14239 Basic Infrastructure 0.655121 \n",
|
||
"17416 Percentage of students in lower secondary educ... 0.645964 \n",
|
||
"14421 (De Facto) Percent of teachers with good pract... 0.642606 \n",
|
||
"14403 Teacher Pedagogical Skills 0.639355 "
|
||
]
|
||
},
|
||
"execution_count": 56,
|
||
"metadata": {},
|
||
"output_type": "execute_result"
|
||
}
|
||
],
|
||
"source": [
|
||
"index = build_faiss_index(df_indicators[\"embedding_text\"].tolist())\n",
|
||
"query = \"What are the indicators related to education?\"\n",
|
||
"results = retrieve_index(df_indicators, query, index, k=5)\n",
|
||
"results[[\"indicator\", \"description\", \"score\"]]"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "206c6280",
|
||
"metadata": {},
|
||
"source": [
|
||
"Il faut maintenant constuire une fonction pour que ça soit un outil de notre agent.\n",
|
||
"\n",
|
||
"**Consigne** : Construire une fonction `retrieve_indicators` qui à partir d'une question et d'un nombre d'indicateurs à renvoyer, renvoie les indicateurs les plus pertinents (selon l'embedding) pour la question.\n",
|
||
"Dans un soucis de simplicité, on ne renverras que trois colonnes : l'indicateur, sa description et le score associé."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 57,
|
||
"id": "583967a2",
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"def retrieve_indicators(query: str, k: int = 10) -> pd.DataFrame:\n",
|
||
" \"\"\"Retrieve the top k most similar indicators for the given query.\"\"\"\n",
|
||
" index = build_faiss_index(df_indicators[\"embedding_text\"].tolist())\n",
|
||
" results = retrieve_index(df_indicators, query, index, k=k)\n",
|
||
" return results[[\"indicator\", \"description\", \"score\"]]"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "c7bcbb11",
|
||
"metadata": {},
|
||
"source": [
|
||
"Nous sommes maintenant capables d'identifier des indicateurs pertinent pour une question donnée. Il faut maintenant obtenir les codes des pays qui nous intéresse.\n",
|
||
"\n",
|
||
"## Identifier les codes de pays\n",
|
||
"\n",
|
||
"De la même manière que précédemment, nous avons l'ensemble des valeurs disponibles."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 58,
|
||
"id": "190b9427",
|
||
"metadata": {},
|
||
"outputs": [
|
||
{
|
||
"data": {
|
||
"application/vnd.microsoft.datawrangler.viewer.v0+json": {
|
||
"columns": [
|
||
{
|
||
"name": "index",
|
||
"rawType": "int64",
|
||
"type": "integer"
|
||
},
|
||
{
|
||
"name": "Code",
|
||
"rawType": "object",
|
||
"type": "string"
|
||
},
|
||
{
|
||
"name": "Description",
|
||
"rawType": "object",
|
||
"type": "string"
|
||
},
|
||
{
|
||
"name": "embedding_text",
|
||
"rawType": "object",
|
||
"type": "string"
|
||
}
|
||
],
|
||
"ref": "a36c39e7-624f-4363-8af1-fbf22836367b",
|
||
"rows": [
|
||
[
|
||
"0",
|
||
"ABW",
|
||
"Aruba",
|
||
"Code: ABW\nDescription: Aruba"
|
||
],
|
||
[
|
||
"1",
|
||
"AFE",
|
||
"Africa Eastern and Southern",
|
||
"Code: AFE\nDescription: Africa Eastern and Southern"
|
||
],
|
||
[
|
||
"2",
|
||
"AFG",
|
||
"Afghanistan",
|
||
"Code: AFG\nDescription: Afghanistan"
|
||
],
|
||
[
|
||
"3",
|
||
"AFR",
|
||
"Africa",
|
||
"Code: AFR\nDescription: Africa"
|
||
],
|
||
[
|
||
"4",
|
||
"AFW",
|
||
"Africa Western and Central",
|
||
"Code: AFW\nDescription: Africa Western and Central"
|
||
]
|
||
],
|
||
"shape": {
|
||
"columns": 3,
|
||
"rows": 5
|
||
}
|
||
},
|
||
"text/html": [
|
||
"<div>\n",
|
||
"<style scoped>\n",
|
||
" .dataframe tbody tr th:only-of-type {\n",
|
||
" vertical-align: middle;\n",
|
||
" }\n",
|
||
"\n",
|
||
" .dataframe tbody tr th {\n",
|
||
" vertical-align: top;\n",
|
||
" }\n",
|
||
"\n",
|
||
" .dataframe thead th {\n",
|
||
" text-align: right;\n",
|
||
" }\n",
|
||
"</style>\n",
|
||
"<table border=\"1\" class=\"dataframe\">\n",
|
||
" <thead>\n",
|
||
" <tr style=\"text-align: right;\">\n",
|
||
" <th></th>\n",
|
||
" <th>Code</th>\n",
|
||
" <th>Description</th>\n",
|
||
" <th>embedding_text</th>\n",
|
||
" </tr>\n",
|
||
" </thead>\n",
|
||
" <tbody>\n",
|
||
" <tr>\n",
|
||
" <th>0</th>\n",
|
||
" <td>ABW</td>\n",
|
||
" <td>Aruba</td>\n",
|
||
" <td>Code: ABW\\nDescription: Aruba</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>1</th>\n",
|
||
" <td>AFE</td>\n",
|
||
" <td>Africa Eastern and Southern</td>\n",
|
||
" <td>Code: AFE\\nDescription: Africa Eastern and Sou...</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>2</th>\n",
|
||
" <td>AFG</td>\n",
|
||
" <td>Afghanistan</td>\n",
|
||
" <td>Code: AFG\\nDescription: Afghanistan</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>3</th>\n",
|
||
" <td>AFR</td>\n",
|
||
" <td>Africa</td>\n",
|
||
" <td>Code: AFR\\nDescription: Africa</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>4</th>\n",
|
||
" <td>AFW</td>\n",
|
||
" <td>Africa Western and Central</td>\n",
|
||
" <td>Code: AFW\\nDescription: Africa Western and Cen...</td>\n",
|
||
" </tr>\n",
|
||
" </tbody>\n",
|
||
"</table>\n",
|
||
"</div>"
|
||
],
|
||
"text/plain": [
|
||
" Code Description \\\n",
|
||
"0 ABW Aruba \n",
|
||
"1 AFE Africa Eastern and Southern \n",
|
||
"2 AFG Afghanistan \n",
|
||
"3 AFR Africa \n",
|
||
"4 AFW Africa Western and Central \n",
|
||
"\n",
|
||
" embedding_text \n",
|
||
"0 Code: ABW\\nDescription: Aruba \n",
|
||
"1 Code: AFE\\nDescription: Africa Eastern and Sou... \n",
|
||
"2 Code: AFG\\nDescription: Afghanistan \n",
|
||
"3 Code: AFR\\nDescription: Africa \n",
|
||
"4 Code: AFW\\nDescription: Africa Western and Cen... "
|
||
]
|
||
},
|
||
"execution_count": 58,
|
||
"metadata": {},
|
||
"output_type": "execute_result"
|
||
}
|
||
],
|
||
"source": [
|
||
"countries = pd.read_csv(\"./data/WBData_countries.csv\")\n",
|
||
"countries[\"embedding_text\"] = countries.apply(\n",
|
||
" lambda row: f\"\"\"Code: {row[\"Code\"]}\\nDescription: {row[\"Description\"]}\"\"\",\n",
|
||
" axis=1,\n",
|
||
")\n",
|
||
"countries.head()"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "db235f9f",
|
||
"metadata": {},
|
||
"source": [
|
||
"**Consigne** : reproduire *mutatis mutandis* le travail précédent dans le cadre des codes de pays."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 59,
|
||
"id": "09c64a5d",
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"def retrieve_countries(query: str, k: int = 10) -> pd.DataFrame:\n",
|
||
" \"\"\"Retrieve the top k most similar countries for the given query.\"\"\"\n",
|
||
" index = build_faiss_index(countries[\"embedding_text\"].tolist())\n",
|
||
" results = retrieve_index(countries, query, index, k=k)\n",
|
||
" return results[[\"Code\", \"Description\", \"score\"]]"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "cd7f259e",
|
||
"metadata": {},
|
||
"source": [
|
||
"A présent nous avons deux fonctions : une pour indentifier les indicateurs pertinent, une pour identifier les codes pays d'intérêts.\n",
|
||
"\n",
|
||
"## Appeler correctement l'API\n",
|
||
"\n",
|
||
"L'objectif est à présent de requêter l'API correctement. Par exemple pour l'inflation annuelle en pourcentage, vue par le consommateur, en France entre 2015 et 2020, la requête est :"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 60,
|
||
"id": "9abc67e3",
|
||
"metadata": {},
|
||
"outputs": [
|
||
{
|
||
"data": {
|
||
"text/plain": [
|
||
"date\n",
|
||
"2020 0.476499\n",
|
||
"2019 1.108255\n",
|
||
"2018 1.850815\n",
|
||
"2017 1.032283\n",
|
||
"2016 0.183335\n",
|
||
"2015 0.037514\n",
|
||
"Name: value, dtype: float64"
|
||
]
|
||
},
|
||
"execution_count": 60,
|
||
"metadata": {},
|
||
"output_type": "execute_result"
|
||
}
|
||
],
|
||
"source": [
|
||
"import wbdata\n",
|
||
"\n",
|
||
"code = \"FP.CPI.TOTL.ZG\"\n",
|
||
"country = \"FRA\"\n",
|
||
"series = wbdata.get_series(code, date=(\"2015\", \"2020\"), country=[country])\n",
|
||
"series"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "a9ff0f02",
|
||
"metadata": {},
|
||
"source": [
|
||
"Si on souhaite l'obtenir pour la France et l'Allemagne :"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 61,
|
||
"id": "99ff0a12",
|
||
"metadata": {},
|
||
"outputs": [
|
||
{
|
||
"data": {
|
||
"text/plain": [
|
||
"country date\n",
|
||
"Germany 2020 0.144878\n",
|
||
" 2019 1.445660\n",
|
||
" 2018 1.732169\n",
|
||
" 2017 1.509495\n",
|
||
" 2016 0.491747\n",
|
||
" 2015 0.514426\n",
|
||
"France 2020 0.476499\n",
|
||
" 2019 1.108255\n",
|
||
" 2018 1.850815\n",
|
||
" 2017 1.032283\n",
|
||
" 2016 0.183335\n",
|
||
" 2015 0.037514\n",
|
||
"Name: value, dtype: float64"
|
||
]
|
||
},
|
||
"execution_count": 61,
|
||
"metadata": {},
|
||
"output_type": "execute_result"
|
||
}
|
||
],
|
||
"source": [
|
||
"code = \"FP.CPI.TOTL.ZG\"\n",
|
||
"country = [\"FRA\", \"DEU\"]\n",
|
||
"series = wbdata.get_series(code, date=(\"2015\", \"2020\"), country=country)\n",
|
||
"series"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "e427b5a0",
|
||
"metadata": {},
|
||
"source": [
|
||
"**Consigne** : Compléter la fonction `query_api` dont l'objectif est de requêter l'API World Bank Data. A partir d'un code d'indicateur, d'une période en années et d'un code de pays, retourner un DataFrame simple de lecture."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 62,
|
||
"id": "b6a089ce",
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"def query_api(\n",
|
||
" country_codes: [str],\n",
|
||
" indicator_codes: [str],\n",
|
||
" date: tuple[str, str],\n",
|
||
") -> pd.DataFrame:\n",
|
||
" \"\"\"Query the World Bank API for the given countries and indicators.\"\"\"\n",
|
||
" data = wbdata.get_data(\n",
|
||
" indicator_codes,\n",
|
||
" country=country_codes,\n",
|
||
" data_date=date,\n",
|
||
" )\n",
|
||
" records = []\n",
|
||
" for country_data in data:\n",
|
||
" country_code = country_data[\"country\"][\"id\"]\n",
|
||
" for indicator_code, entries in country_data[\"indicators\"].items():\n",
|
||
" for entry in entries:\n",
|
||
" record = {\n",
|
||
" \"country_code\": country_code,\n",
|
||
" \"indicator_code\": indicator_code,\n",
|
||
" \"date\": entry[\"date\"],\n",
|
||
" \"value\": entry[\"value\"],\n",
|
||
" }\n",
|
||
" records.append(record)\n",
|
||
" return pd.DataFrame(records)"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "5a5aaeea",
|
||
"metadata": {},
|
||
"source": [
|
||
"## Agent ! \n",
|
||
"\n",
|
||
"Passons maintenant à l'étape de conception de l'agent, nous utiliserons LangGraph pour le faire.\n",
|
||
"\n",
|
||
"Pour commencer, faisons le récapitulatif des fonctions que nous avons construites :\n",
|
||
"1. `retrieve_indicators` : récupérer les codes pertinents pour une question donnée\n",
|
||
"2. `retrieve_countries` : récupérer les codes pays pertinents pour une question donnée\n",
|
||
"3. `query_api` : requêter proprement l'API d'intérêt"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 63,
|
||
"id": "1b58d076",
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"TOOL_REGISTRY = {\n",
|
||
" \"retrieve_indicators\": retrieve_indicators,\n",
|
||
" \"retrieve_countries\": retrieve_countries,\n",
|
||
" \"query_api\": query_api,\n",
|
||
"}"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "49459ab3",
|
||
"metadata": {},
|
||
"source": [
|
||
"### Structure de l'agent\n",
|
||
"\n",
|
||
"Dans LangGraph, un agent est un graphe d'actions et d'informations évolutifs. Nous allons définir :\n",
|
||
"1. Une **classe** qui représentera l'agent. L'objectif est de pouvoir stocker et utiliser par la suite à bon essient les avancées de l'agent.\n",
|
||
"2. Des **méthodes** qui sont les sommets dans le graphes d'actions de l'agent. Ces méthodes exploitent les informations contenues dans la classe.\n",
|
||
"3. Un **graphe** qui va permettre de définir comment passer d'un sommet à l'autre.\n",
|
||
"\n",
|
||
"Commençons par la classe. Nous appelons l'agent à partir d'une **question** sur laquelle il va **réfléchir** et élaborer un plan d'action qui ici se résumé à des **appels** aux fonctions que nous avons définies. Puis, exploitant les **résultats** de ces appels, l'agent pourra **requêter** l'API puis **analyser** le résultat, à la lumière de la question posée initialement.\n",
|
||
"\n",
|
||
"Nous allons exactement coder cela, chaque mot en gras correspond à une information que l'on va stocker et exploiter au moment opportun."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 64,
|
||
"id": "08461b31",
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"from typing import Any, TypedDict\n",
|
||
"\n",
|
||
"from pydantic import BaseModel, Field\n",
|
||
"\n",
|
||
"\n",
|
||
"class AgentState(TypedDict):\n",
|
||
" \"\"\"State of the agent during its reasoning process.\"\"\"\n",
|
||
"\n",
|
||
" question: str\n",
|
||
" tool_thoughts: str\n",
|
||
" query_thoughts: str\n",
|
||
" tool_calls: list[dict]\n",
|
||
" tool_results: list[Any]\n",
|
||
" query_calls: list[dict]\n",
|
||
" query_results: list[dict]\n",
|
||
" answer: str"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "bc6040ac",
|
||
"metadata": {},
|
||
"source": [
|
||
"Pour travailler, nous n'utiliserons qu'un seul modèle, ici Gemma3 version 12B. "
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 65,
|
||
"id": "ebb591c8",
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"from langchain_ollama import OllamaLLM\n",
|
||
"\n",
|
||
"model = OllamaLLM(model=\"gemma3:12b\")"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "055fd5fe",
|
||
"metadata": {},
|
||
"source": [
|
||
"### Préparation de l'appel\n",
|
||
"\n",
|
||
"La première étape consiste à préparer l'appel à l'API. Nous avons besoin que le modèle puisse à la fois nous liver sa réflexion et les outils qu'il souhaite exploiter.\n",
|
||
"Nous allons donc définir notre propre *parser* du résultat du modèle :\n",
|
||
"\n",
|
||
"1. **Raisonnement** : pour auditer la réflexion du modèle. Ici, simplement du texte suffit.\n",
|
||
"2. **Outil** : pour savoir quels outils appeler avec quel paramètrage. Ici nous devons définir une liste d'appels."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 66,
|
||
"id": "081174ae",
|
||
"metadata": {},
|
||
"outputs": [
|
||
{
|
||
"name": "stdout",
|
||
"output_type": "stream",
|
||
"text": [
|
||
"The output should be formatted as a JSON instance that conforms to the JSON schema below.\n",
|
||
"\n",
|
||
"As an example, for the schema {\"properties\": {\"foo\": {\"title\": \"Foo\", \"description\": \"a list of strings\", \"type\": \"array\", \"items\": {\"type\": \"string\"}}}, \"required\": [\"foo\"]}\n",
|
||
"the object {\"foo\": [\"bar\", \"baz\"]} is a well-formatted instance of the schema. The object {\"properties\": {\"foo\": [\"bar\", \"baz\"]}} is not well-formatted.\n",
|
||
"\n",
|
||
"Here is the output schema:\n",
|
||
"```\n",
|
||
"{\"$defs\": {\"ToolCall\": {\"description\": \"Representation of a tool call.\", \"properties\": {\"name\": {\"description\": \"Name of the tool to call\", \"title\": \"Name\", \"type\": \"string\"}, \"args\": {\"additionalProperties\": true, \"description\": \"Arguments for the tool\", \"title\": \"Args\", \"type\": \"object\"}}, \"required\": [\"name\", \"args\"], \"title\": \"ToolCall\", \"type\": \"object\"}}, \"description\": \"Output schema for reasoning steps.\", \"properties\": {\"thought\": {\"description\": \"Short explanation of reasoning\", \"title\": \"Thought\", \"type\": \"string\"}, \"tools\": {\"description\": \"Tools to call\", \"items\": {\"$ref\": \"#/$defs/ToolCall\"}, \"title\": \"Tools\", \"type\": \"array\"}}, \"required\": [\"thought\", \"tools\"]}\n",
|
||
"```\n"
|
||
]
|
||
}
|
||
],
|
||
"source": [
|
||
"from langchain_core.output_parsers import PydanticOutputParser\n",
|
||
"\n",
|
||
"\n",
|
||
"class ToolCall(BaseModel):\n",
|
||
" \"\"\"Representation of a tool call.\"\"\"\n",
|
||
"\n",
|
||
" name: str = Field(description=\"Name of the tool to call\")\n",
|
||
" args: dict[str, Any] = Field(description=\"Arguments for the tool\")\n",
|
||
"\n",
|
||
"\n",
|
||
"class ReasoningOutput(BaseModel):\n",
|
||
" \"\"\"Output schema for reasoning steps.\"\"\"\n",
|
||
"\n",
|
||
" thought: str = Field(description=\"Short explanation of reasoning\")\n",
|
||
" tools: list[ToolCall] = Field(description=\"Tools to call\")\n",
|
||
"\n",
|
||
"\n",
|
||
"parser = PydanticOutputParser(pydantic_object=ReasoningOutput)\n",
|
||
"format_instructions = parser.get_format_instructions()\n",
|
||
"print(format_instructions)"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "9e4b951b",
|
||
"metadata": {},
|
||
"source": [
|
||
"Nous avons en plus les instructions à fournir au modèle pour formatter la réponse.\n",
|
||
"\n",
|
||
"Nous pouvons enfin créer notre premier sommet/noeud du graphe :"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 67,
|
||
"id": "02cd965a",
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"def preparation_node(state: AgentState) -> AgentState:\n",
|
||
" \"\"\"Prepare node to decide which tools to call based on the question.\"\"\"\n",
|
||
" prompt = f\"\"\"\n",
|
||
"You are a AI agent data analyst using the World Bank Data API to answer the user question. You MUST use the two tools to answer the question.\n",
|
||
"\n",
|
||
"User question:\n",
|
||
"{state[\"question\"]}\n",
|
||
"\n",
|
||
"Available tools:\n",
|
||
"- retrieve_indicators(query: str) : ask a question to see which indicators might help\n",
|
||
"- retrieve_countries(query: str) : ask a question to retrieve the code of a specific country or a geographic zone\n",
|
||
"\n",
|
||
"\n",
|
||
"\n",
|
||
"{format_instructions}\n",
|
||
"\"\"\"\n",
|
||
" response = model.invoke(prompt)\n",
|
||
" decision = parser.parse(response)\n",
|
||
"\n",
|
||
" state[\"tool_thoughts\"] = decision.thought\n",
|
||
" state[\"tool_calls\"] = [\n",
|
||
" {\"name\": tool.name, \"args\": tool.args} for tool in decision.tools\n",
|
||
" ]\n",
|
||
" return state\n"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "221e51f0",
|
||
"metadata": {},
|
||
"source": [
|
||
"Nous avons appelé le modèle, récupérer sa réponse dans un format facilement exploitable pour nous puis stocké les informations. Il nous faut maintenant concrétement appeler les outils et stocker les résultats : c'est complétement déterministe.\n",
|
||
"\n",
|
||
"### Appels d'outils"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 68,
|
||
"id": "48939e28",
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"def tool_execution_node(state: AgentState) -> AgentState:\n",
|
||
" \"\"\"Execute the tool calls decided in the preparation node.\"\"\"\n",
|
||
" results = []\n",
|
||
" for call in state[\"tool_calls\"]:\n",
|
||
" tool_name = call[\"name\"]\n",
|
||
" tool_args = call.get(\"args\", {})\n",
|
||
"\n",
|
||
" tool_function = TOOL_REGISTRY.get(tool_name)\n",
|
||
" if tool_function is None:\n",
|
||
" results.append(\n",
|
||
" {\"tool\": tool_name, \"args\": tool_args, \"error\": \"Unknown tool\"},\n",
|
||
" )\n",
|
||
" continue\n",
|
||
"\n",
|
||
" try:\n",
|
||
" result = tool_function(**tool_args)\n",
|
||
" results.append({\"tool\": tool_name, \"args\": tool_args, \"result\": result})\n",
|
||
" except:\n",
|
||
" continue\n",
|
||
"\n",
|
||
" state[\"tool_results\"] = results\n",
|
||
" return state\n"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "612b4ebe",
|
||
"metadata": {},
|
||
"source": [
|
||
"### Noeuds de requête\n",
|
||
"\n",
|
||
"Après la préparation et la récupération des informations via les outils que nous avons défini plus tôt, nous pouvons définir un nouveau sommet/noeud dont l'objectif est de définir l'appel à la fonction `query_api`.\n",
|
||
"\n",
|
||
"**Consigne** En s'inspirant du sommet `preparation_node`, compléter la cellule ci-dessous."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 69,
|
||
"id": "36935e0f",
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"def query_node(state: AgentState) -> AgentState:\n",
|
||
" \"\"\"Node to decide how to query the World Bank API based on tool results.\"\"\"\n",
|
||
" prompt = f\"\"\"\n",
|
||
"You are in a AI agent system. Based on the following informations, use the tool to help answer the user question.\n",
|
||
"\n",
|
||
"User question:\n",
|
||
"{state[\"question\"]}\n",
|
||
"\n",
|
||
"Your reasoning:\n",
|
||
"{state[\"tool_thoughts\"]}\n",
|
||
"\n",
|
||
"Data retrieved from tools:\n",
|
||
"{state[\"tool_results\"]}\n",
|
||
"\n",
|
||
"Available tool: query_api(code, date, country) with parameters :\n",
|
||
" - code : string linked to an indicator in the world bank data. Example : FR.INR.MMKT\n",
|
||
" - date : string of the year. If you need to query for a period, use a list with the starting and ending year. Examples : \"2025\" or [\"2020\", \"2023\"]\n",
|
||
" - country: string or list of string representing a country with a 3 letter code. Examples : \"FRA\" or [\"UAE\", \"DEU\"]\n",
|
||
"\n",
|
||
"{format_instructions}\n",
|
||
"\"\"\"\n",
|
||
" state[\"answer\"] = model.invoke(prompt)\n",
|
||
" return state"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "1170b4f0",
|
||
"metadata": {},
|
||
"source": [
|
||
"Il reste à exécuter ce que ce sommet/noeud a jugé utile :"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 70,
|
||
"id": "f20f5c60",
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"def query_execution_node(state: AgentState) -> AgentState:\n",
|
||
" \"\"\"Execute the query calls decided in the query node.\"\"\"\n",
|
||
" results = []\n",
|
||
" for call in state[\"query_calls\"]:\n",
|
||
" tool_name = call[\"name\"]\n",
|
||
" tool_args = call.get(\"args\", {})\n",
|
||
"\n",
|
||
" tool_function = TOOL_REGISTRY.get(tool_name)\n",
|
||
" if tool_function is None:\n",
|
||
" results.append(\n",
|
||
" {\"tool\": tool_name, \"args\": tool_args, \"error\": \"Unknown tool\"},\n",
|
||
" )\n",
|
||
" continue\n",
|
||
"\n",
|
||
" result = tool_function(**tool_args)\n",
|
||
" results.append({\"tool\": tool_name, \"args\": tool_args, \"result\": result})\n",
|
||
"\n",
|
||
" state[\"query_results\"] = results\n",
|
||
" return state\n"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "5986bba8",
|
||
"metadata": {},
|
||
"source": [
|
||
"### Synthèse\n",
|
||
"\n",
|
||
"Après tout ces appels et ces informations collectées, il est temps de les restituer sous forme de simple texte."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 71,
|
||
"id": "2c751849",
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"def synthesis_node(state: AgentState) -> AgentState:\n",
|
||
" \"\"\"Synthesis node to produce the final answer to the user question.\"\"\"\n",
|
||
" prompt = f\"\"\"\n",
|
||
"You are an expert in data analysis and you used the World Bank Data API.\n",
|
||
"\n",
|
||
"User question:\n",
|
||
"{state[\"question\"]}\n",
|
||
"\n",
|
||
"Your reasoning:\n",
|
||
"{state[\"query_thoughts\"]}\n",
|
||
"\n",
|
||
"Data retrieved from tools:\n",
|
||
"{state[\"query_results\"]}\n",
|
||
"\n",
|
||
"Write a concise and clear answer to the user question, based ONLY on the information you have. Be thoughtful, write in plain text : do not use too much formatting.\n",
|
||
"\"\"\"\n",
|
||
" state[\"answer\"] = model.invoke(prompt)\n",
|
||
" return state"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "15cfb361",
|
||
"metadata": {},
|
||
"source": [
|
||
"### Définition du graphe\n",
|
||
"\n",
|
||
"A ce stade, nous avons des bouts du puzzles mais ils ne sont pas assemblé. C'est ce que nous allons faire maintenant. On commence par ajouter les noeuds/sommets :"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 72,
|
||
"id": "31e2e6a5",
|
||
"metadata": {},
|
||
"outputs": [
|
||
{
|
||
"data": {
|
||
"text/plain": [
|
||
"<langgraph.graph.state.StateGraph at 0x1288a51d0>"
|
||
]
|
||
},
|
||
"execution_count": 72,
|
||
"metadata": {},
|
||
"output_type": "execute_result"
|
||
}
|
||
],
|
||
"source": [
|
||
"from langgraph.graph import StateGraph\n",
|
||
"\n",
|
||
"graph = StateGraph(AgentState)\n",
|
||
"\n",
|
||
"graph.add_node(\"preparation\", preparation_node)\n",
|
||
"graph.add_node(\"preparation_tools\", tool_execution_node)\n",
|
||
"\n",
|
||
"graph.add_node(\"query\", query_node)\n",
|
||
"graph.add_node(\"query_tools\", query_execution_node)\n",
|
||
"\n",
|
||
"graph.add_node(\"synthesis\", synthesis_node)"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "e80eb961",
|
||
"metadata": {},
|
||
"source": [
|
||
"Puis les liens entres les différents noeuds/sommets :"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 73,
|
||
"id": "a2f9aebe",
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"graph.set_entry_point(\"preparation\")\n",
|
||
"graph.add_edge(\"preparation\", \"preparation_tools\")\n",
|
||
"graph.add_edge(\"preparation_tools\", \"query\")\n",
|
||
"graph.add_edge(\"query\", \"query_tools\")\n",
|
||
"graph.add_edge(\"query_tools\", \"synthesis\")\n",
|
||
"\n",
|
||
"agent = graph.compile()"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "18f33d4a",
|
||
"metadata": {},
|
||
"source": [
|
||
"Il est maintenant temps de l'utiliser !"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 74,
|
||
"id": "80f9aa22",
|
||
"metadata": {},
|
||
"outputs": [
|
||
{
|
||
"name": "stderr",
|
||
"output_type": "stream",
|
||
"text": [
|
||
"Batches: 100%|██████████| 409/409 [00:48<00:00, 8.45it/s]\n"
|
||
]
|
||
}
|
||
],
|
||
"source": [
|
||
"question = \"What the evolution of France's inflation between 2015 and 2025 ? Comment on this value using the inflation in Europe, or in Germany for example.\"\n",
|
||
"\n",
|
||
"initial_state = {\n",
|
||
" \"question\": question,\n",
|
||
" \"tool_thoughts\": \"\",\n",
|
||
" \"query_thoughts\": \"\",\n",
|
||
" \"tool_calls\": [],\n",
|
||
" \"tool_results\": [],\n",
|
||
" \"query_calls\": [],\n",
|
||
" \"query_results\": [],\n",
|
||
" \"answer\": \"\",\n",
|
||
"}\n",
|
||
"\n",
|
||
"result = agent.invoke(initial_state)"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "b8e91b0d",
|
||
"metadata": {},
|
||
"source": [
|
||
"On peut obtenir le résultat de l'agent ainsi que ces différentes réflexion tout au long du processus :"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 75,
|
||
"id": "7355b174",
|
||
"metadata": {},
|
||
"outputs": [
|
||
{
|
||
"name": "stdout",
|
||
"output_type": "stream",
|
||
"text": [
|
||
"Unfortunately, I do not have access to external tools or the World Bank Data API to retrieve the data you requested. Therefore, I cannot provide you with the evolution of France’s inflation between 2015 and 2025, nor compare it to European or German inflation rates.\n",
|
||
"\n",
|
||
"\n",
|
||
"\n",
|
||
"To answer your question accurately, I would need to query the World Bank Data API and process the results. If you can provide the data yourself, I would be happy to analyze it and comment on the trends.\n"
|
||
]
|
||
}
|
||
],
|
||
"source": [
|
||
"print(result[\"answer\"])"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 94,
|
||
"id": "ff17fb70",
|
||
"metadata": {},
|
||
"outputs": [
|
||
{
|
||
"name": "stdout",
|
||
"output_type": "stream",
|
||
"text": [
|
||
"{'question': \"What the evolution of France's inflation between 2015 and 2025 ? Comment on this value using the inflation in Europe, or in Germany for example.\", 'tool_thoughts': \"First, I need to determine which indicator represents inflation. I will use the 'retrieve_indicators' tool to find appropriate indicators.\", 'tool_calls': [{'name': 'retrieve_indicators', 'args': {'query': 'inflation'}}], 'tool_results': [{'tool': 'retrieve_indicators', 'args': {'query': 'inflation'}, 'result': indicator \\\n",
|
||
"6299 FP.CPI.TOTL.ZG \n",
|
||
"9781 NY.GDP.DEFL.KD.ZG \n",
|
||
"6303 FP.WPI.TOTL.ZG \n",
|
||
"9782 NY.GDP.DEFL.KD.ZG.AD \n",
|
||
"6301 FP.FPI.TOTL.ZG \n",
|
||
"9780 NY.GDP.DEFL.87.ZG \n",
|
||
"6298 FP.CPI.TOTL \n",
|
||
"6275 FM.LBL.MQMY.ZG \n",
|
||
"6313 FR.INR.MMKT \n",
|
||
"10930 PI-16 \n",
|
||
"\n",
|
||
" description score \n",
|
||
"6299 Inflation, consumer prices (annual %) 0.518722 \n",
|
||
"9781 Inflation, GDP deflator (annual %) 0.460076 \n",
|
||
"6303 Inflation, wholesale prices (annual %) 0.456499 \n",
|
||
"9782 Inflation, GDP deflator: linked series (annual %) 0.409883 \n",
|
||
"6301 Inflation, food prices (annual %) 0.402444 \n",
|
||
"9780 Inflation, GDP deflator (annual %) 0.401507 \n",
|
||
"6298 Consumer price index (2010 = 100) 0.358742 \n",
|
||
"6275 Money and quasi money growth (annual %) 0.320473 \n",
|
||
"6313 Money market rate (%) 0.313243 \n",
|
||
"10930 Medium term perspective in expenditure budgeting 0.311095 }], 'query_thoughts': 'Structured query plan generated', 'query_results': [], 'query_plan': {'indicator_code': 'FP.CPI.TOTL.ZG', 'countries': ['FRA', 'DEU'], 'start_year': 2015, 'end_year': 2025}, 'answer': \"```tool_code\\nfrom wbdata import wbdata\\nimport pandas as pd\\n\\n# Define the indicator for inflation (CPI, % change)\\nindicator = 'CPI-ACP-RD'\\n\\n# Define the country for France\\ncountry = 'FRA'\\n\\n# Define the year range\\nstart_year = 2015\\nend_year = 2025\\n\\n# Fetch the data for France\\nfrance_inflation = wbdata.get_series(indicator, country, start_year=start_year, end_year=end_year)\\n\\n# Fetch the data for Europe (aggregate region)\\neurope_inflation = wbdata.get_series(indicator, 'EUU', start_year=start_year, end_year=end_year)\\n\\n# Fetch the data for Germany\\ngermany_inflation = wbdata.get_series(indicator, 'DEU', start_year=start_year, end_year=end_year)\\n\\n\\n# Convert to pandas DataFrames for easier handling\\nfrance_inflation_df = france_inflation.to_frame(name='France')\\neurope_inflation_df = europe_inflation.to_frame(name='Europe')\\ngermany_inflation_df = germany_inflation.to_frame(name='Germany')\\n\\n# Combine the dataframes\\ncombined_df = pd.concat([france_inflation_df, europe_inflation_df, germany_inflation_df], axis=1)\\n\\n# Print the combined data\\nprint(combined_df)\\n```\\n\", 'human_feedback': {'approved': True}}\n"
|
||
]
|
||
}
|
||
],
|
||
"source": [
|
||
"print(result)"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "ffd8baa7",
|
||
"metadata": {},
|
||
"source": [
|
||
"C'est pas mal ! Mais nous avons probablement eu de la chance : la requête a été bien formulée. Ce ne sera peut-être pas toujours le cas, nous avons besoin de plus de sécurité.\n",
|
||
"\n",
|
||
"## Plus de robustesse dans la génération de la requête\n",
|
||
"\n",
|
||
"On se propose ici d'améliorer cet aspect en modifiant le graphe :\n",
|
||
"- Ajout d'un module de **validation déterministe** des paramètres de la requête sélectionné\n",
|
||
"- Ajout d'un noeud/sommet de revus des paramètres s'il y a une erreur, pour le **réparer**\n",
|
||
"\n",
|
||
"Ces deux ajustements nous demanderons de modifier la classe de définition de l'agent et le graphe.\n",
|
||
"\n",
|
||
"### Validation déterministe\n",
|
||
"\n",
|
||
"On commence par la classe `QueryPlan` :"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 76,
|
||
"id": "274e67a5",
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"class QueryPlan(BaseModel):\n",
|
||
" \"\"\"Plan for querying the World Bank API.\"\"\"\n",
|
||
"\n",
|
||
" indicator_code: str = Field(description=\"World Bank indicator code\")\n",
|
||
" countries: list[str] = Field(description=\"List of 3-letter country codes\")\n",
|
||
" start_year: int = Field(description=\"Start year of the query\")\n",
|
||
" end_year: int = Field(description=\"End year of the query\")\n"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "683ddb56",
|
||
"metadata": {},
|
||
"source": [
|
||
"**Consigne** : Modifier la classe de l'agent pour remplacer *query_calls* et *query_results* par :\n",
|
||
"* *query_plan* qui sera un dictionnaire optionnel\n",
|
||
"* *validation_errors* qui sera une liste de string optionnel, pour aider à débugger"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 77,
|
||
"id": "36c8ca29",
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"def query_plan(\n",
|
||
" question: str,\n",
|
||
") -> QueryPlan:\n",
|
||
" \"\"\"Generate a query plan based on the user question.\"\"\"\n",
|
||
" prompt = f\"\"\"You are an AI agent data analyst using the World Bank Data API to answer the user question. Based on the question, create a plan to query the World Bank API.\n",
|
||
"User question:\n",
|
||
"{question}\n",
|
||
"\n",
|
||
"{format_instructions}\n",
|
||
" \"\"\"\n",
|
||
" response = model.invoke(prompt)\n",
|
||
" return parser.parse(response)\n"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "50ba9a56",
|
||
"metadata": {},
|
||
"source": [
|
||
"Il nous faut modifier la fonction `query_node` pour produire quelque chose de plus simple à valider :"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 80,
|
||
"id": "287fe8ec",
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"from langchain_core.output_parsers import PydanticOutputParser\n",
|
||
"\n",
|
||
"query_plan_parser = PydanticOutputParser(pydantic_object=QueryPlan)\n",
|
||
"query_plan_format = query_plan_parser.get_format_instructions()\n",
|
||
"\n",
|
||
"\n",
|
||
"def query_node(state: AgentState) -> AgentState:\n",
|
||
" \"\"\"Node to decide how to query the World Bank API based on tool results.\"\"\"\n",
|
||
" prompt = f\"\"\"\n",
|
||
"You are an AI agent building a structured query plan for the World Bank Data API.\n",
|
||
"\n",
|
||
"User question:\n",
|
||
"{state[\"question\"]}\n",
|
||
"\n",
|
||
"Retrieved indicators and countries:\n",
|
||
"{state[\"tool_results\"]}\n",
|
||
"\n",
|
||
"Return exactly one query plan with:\n",
|
||
"- indicator_code\n",
|
||
"- countries (list)\n",
|
||
"- start_year\n",
|
||
"- end_year\n",
|
||
"\n",
|
||
"{query_plan_format}\n",
|
||
"\"\"\"\n",
|
||
" response = model.invoke(prompt)\n",
|
||
" plan = query_plan_parser.parse(response)\n",
|
||
"\n",
|
||
" state[\"query_thoughts\"] = \"Structured query plan generated\"\n",
|
||
" state[\"query_plan\"] = plan.model_dump()\n",
|
||
" return state\n"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "278c2df8",
|
||
"metadata": {},
|
||
"source": [
|
||
"Puis il nous faut un module de validation déterministe :"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 81,
|
||
"id": "c16a8f6b",
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"VALID_INDICATORS = set(df_indicators[\"indicator\"].values)\n",
|
||
"VALID_COUNTRIES = set(countries[\"Code\"].values)\n",
|
||
"MIN_YEAR = 1960\n",
|
||
"MAX_YEAR = 2025\n",
|
||
"\n",
|
||
"\n",
|
||
"def validation_node(state: AgentState) -> AgentState:\n",
|
||
" \"\"\"Validate the generated query plan.\"\"\"\n",
|
||
" plan = state.get(\"query_plan\")\n",
|
||
" errors = []\n",
|
||
"\n",
|
||
" if plan is None:\n",
|
||
" errors.append(\"No query plan found.\")\n",
|
||
" state[\"validation_errors\"] = errors\n",
|
||
" return state\n",
|
||
"\n",
|
||
" if plan[\"indicator_code\"] not in VALID_INDICATORS:\n",
|
||
" errors.append(f\"Invalid indicator code: {plan['indicator_code']}\")\n",
|
||
"\n",
|
||
" for c in plan[\"countries\"]:\n",
|
||
" if c not in VALID_COUNTRIES:\n",
|
||
" errors.append(f\"Invalid country code: {c}\")\n",
|
||
"\n",
|
||
" if not (MIN_YEAR <= plan[\"start_year\"] <= MAX_YEAR):\n",
|
||
" errors.append(f\"start_year out of bounds: {plan['start_year']}\")\n",
|
||
"\n",
|
||
" if not (MIN_YEAR <= plan[\"end_year\"] <= MAX_YEAR):\n",
|
||
" errors.append(f\"end_year out of bounds: {plan['end_year']}\")\n",
|
||
"\n",
|
||
" state[\"validation_errors\"] = errors if errors else None\n",
|
||
" return state\n"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "e063ee6c",
|
||
"metadata": {},
|
||
"source": [
|
||
"Il nous faut également, un noeud pour éventuellement ajuster le plan d'appel s'il ne vérifie pas la validation.\n",
|
||
"\n",
|
||
"**Consigne** : Compléter la fonction ci-dessous."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": null,
|
||
"id": "d98d3031",
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"def repair_node(state: AgentState) -> AgentState:\n",
|
||
" prompt = f\"\"\"\n",
|
||
"The following query plan is invalid:\n",
|
||
"\n",
|
||
"{state[\"query_plan\"]}\n",
|
||
"\n",
|
||
"Errors:\n",
|
||
"{state[\"validation_errors\"]}\n",
|
||
"\n",
|
||
"Fix the query plan so that:\n",
|
||
"- The indicator exists\n",
|
||
"- Country codes are valid\n",
|
||
"- Years are within bounds\n",
|
||
"\n",
|
||
"Return a corrected query plan only.\n",
|
||
"\n",
|
||
"{query_plan_format}\n",
|
||
"\"\"\"\n",
|
||
" response = model.invoke(prompt)\n",
|
||
" plan = query_plan_parser.parse(response)\n",
|
||
" state[\"query_plan\"] = plan.model_dump()\n",
|
||
" state[\"validation_errors\"] = None\n",
|
||
" return state\n"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "a0653b30",
|
||
"metadata": {},
|
||
"source": [
|
||
"La fonction `query_execution_node` peut être largement simplifiée maintenant que nous avons fait tout ces changements.\n",
|
||
"\n",
|
||
"**Consigne** : Re-définir la fonction `query_execution_node` en prenant en compte le plan stocké dans la mémoire de l'agent."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 83,
|
||
"id": "e9d888ec",
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"def query_execution_node(state: AgentState) -> AgentState:\n",
|
||
" \"\"\"Execute the query calls decided in the query node.\"\"\"\n",
|
||
" plan = state.get(\"query_plan\")\n",
|
||
" if plan is None:\n",
|
||
" state[\"query_results\"] = []\n",
|
||
" return state\n",
|
||
"\n",
|
||
" result = query_api(\n",
|
||
" country_codes=plan[\"countries\"],\n",
|
||
" indicator_codes=[plan[\"indicator_code\"]],\n",
|
||
" date=(str(plan[\"start_year\"]), str(plan[\"end_year\"])),\n",
|
||
" )\n",
|
||
" state[\"query_results\"] = [result]\n",
|
||
" return state"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "abc0e490",
|
||
"metadata": {},
|
||
"source": [
|
||
"Finalement, nous pouvons ré-écrire le graphe en y ajoutant nos nouveaux sommets et surtout introduire des noeuds optionnels !"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 84,
|
||
"id": "259e91ae",
|
||
"metadata": {},
|
||
"outputs": [
|
||
{
|
||
"data": {
|
||
"image/png": "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",
|
||
"text/plain": [
|
||
"<langgraph.graph.state.CompiledStateGraph object at 0x15047de50>"
|
||
]
|
||
},
|
||
"execution_count": 84,
|
||
"metadata": {},
|
||
"output_type": "execute_result"
|
||
}
|
||
],
|
||
"source": [
|
||
"from langgraph.graph import StateGraph\n",
|
||
"\n",
|
||
"graph = StateGraph(AgentState)\n",
|
||
"\n",
|
||
"graph.add_node(\"preparation\", preparation_node)\n",
|
||
"graph.add_node(\"preparation_tools\", tool_execution_node)\n",
|
||
"graph.add_node(\"query\", query_node)\n",
|
||
"graph.add_node(\"validate\", validation_node)\n",
|
||
"graph.add_node(\"repair\", repair_node)\n",
|
||
"graph.add_node(\"query_tools\", query_execution_node)\n",
|
||
"graph.add_node(\"synthesis\", synthesis_node)\n",
|
||
"\n",
|
||
"graph.set_entry_point(\"preparation\")\n",
|
||
"graph.add_edge(\"preparation\", \"preparation_tools\")\n",
|
||
"graph.add_edge(\"preparation_tools\", \"query\")\n",
|
||
"graph.add_edge(\"query\", \"validate\")\n",
|
||
"\n",
|
||
"\n",
|
||
"def validation_router(state: AgentState):\n",
|
||
" if state.get(\"validation_errors\"):\n",
|
||
" return \"repair\"\n",
|
||
" return \"query_tools\"\n",
|
||
"\n",
|
||
"\n",
|
||
"graph.add_conditional_edges(\n",
|
||
" \"validate\",\n",
|
||
" validation_router,\n",
|
||
" {\n",
|
||
" \"repair\": \"repair\",\n",
|
||
" \"query_tools\": \"query_tools\",\n",
|
||
" },\n",
|
||
")\n",
|
||
"\n",
|
||
"graph.add_edge(\"repair\", \"validate\")\n",
|
||
"graph.add_edge(\"query_tools\", \"synthesis\")\n",
|
||
"\n",
|
||
"agent = graph.compile()\n",
|
||
"agent\n"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "8c8786d9",
|
||
"metadata": {},
|
||
"source": [
|
||
"C'est une structure plus complexe ! Et a priori plus robuste sur l'appel à l'API. Mais puisque l'agent met du temps à répondre, nous souhaiterions être certain qu'il est parti sur la bonne piste.\n",
|
||
"\n",
|
||
"## *Human in the loop*\n",
|
||
"\n",
|
||
"Il est temps d'intégrer l'humain dans le processus pour valider ou non que le plan identifié est correct. Si ce n'est pas le cas, alors l'humain peut modifier le pla voire préciser sa question.\n",
|
||
"\n",
|
||
"Pour démarrer, nous devons à nouveau modifier la classe de l'agent.\n",
|
||
"\n",
|
||
"**Consigne** : Ajouter un dictionnaire optionnel nommé *human_feedback*."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 88,
|
||
"id": "7d6d2da3",
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"class AgentState(TypedDict):\n",
|
||
" \"\"\"State of the agent during its reasoning process.\"\"\"\n",
|
||
"\n",
|
||
" question: str\n",
|
||
"\n",
|
||
" tool_thoughts: str\n",
|
||
" tool_calls: list[dict]\n",
|
||
" tool_results: list[Any]\n",
|
||
"\n",
|
||
" query_thoughts: str\n",
|
||
" query_calls: list[dict]\n",
|
||
" query_results: list[dict]\n",
|
||
" query_plan: dict | None\n",
|
||
" retry_count: int\n",
|
||
"\n",
|
||
" answer: str\n",
|
||
" human_feedback: dict | None\n"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "dee8fb70",
|
||
"metadata": {},
|
||
"source": [
|
||
"On continue avec le noeud concernant l'appel à l'humain. On se propose trois possibilitées :\n",
|
||
"1. **Approuver** : on continue avec ce plan\n",
|
||
"2. **Editer** : on modifie le plan qui est proposé par l'agent, puis on repart sur le module de validation\n",
|
||
"3. **Rejeter** : on reformule la question que l'on a posé, puis on repart sur l'étape de préparation\n",
|
||
"\n",
|
||
"Pour éviter le cas où l'on édite le plan, qu'il est valider par le module de validation et qu'on nous re-sollicite, on ajoute une vérification que l'humain n'a pas déjà été sollicité."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 89,
|
||
"id": "44ea5c40",
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"def human_approval_node(state: AgentState) -> AgentState:\n",
|
||
" if state.get(\"human_feedback\"):\n",
|
||
" return state\n",
|
||
" plan = state[\"query_plan\"]\n",
|
||
"\n",
|
||
" print(\"\\nProposed query plan:\")\n",
|
||
" print(f\"Indicator: {plan['indicator_code']}\")\n",
|
||
" print(f\"Countries: {', '.join(plan['countries'])}\")\n",
|
||
" print(f\"Years: {plan['start_year']}–{plan['end_year']}\")\n",
|
||
"\n",
|
||
" decision = input(\"\\nApprove? (y = yes / e = edit / n = reject): \").strip().lower()\n",
|
||
"\n",
|
||
" if decision == \"y\":\n",
|
||
" state[\"human_feedback\"] = {\"approved\": True}\n",
|
||
"\n",
|
||
" elif decision == \"e\":\n",
|
||
" edited = input(\"\\nEnter corrected query plan as JSON:\\n\")\n",
|
||
" state[\"human_feedback\"] = {\"approved\": False, \"edited_plan\": edited}\n",
|
||
"\n",
|
||
" else:\n",
|
||
" new_q = input(\"\\nPlease rephrase your question:\\n\")\n",
|
||
" state[\"human_feedback\"] = {\"approved\": False, \"new_question\": new_q}\n",
|
||
"\n",
|
||
" return state\n"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "74cbce56",
|
||
"metadata": {},
|
||
"source": [
|
||
"Et on obtient alors un nouveau graphe :"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 90,
|
||
"id": "e4ae950a",
|
||
"metadata": {},
|
||
"outputs": [
|
||
{
|
||
"data": {
|
||
"image/png": "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",
|
||
"text/plain": [
|
||
"<langgraph.graph.state.CompiledStateGraph object at 0x1511e2d70>"
|
||
]
|
||
},
|
||
"execution_count": 90,
|
||
"metadata": {},
|
||
"output_type": "execute_result"
|
||
}
|
||
],
|
||
"source": [
|
||
"from langgraph.graph import StateGraph\n",
|
||
"\n",
|
||
"graph = StateGraph(AgentState)\n",
|
||
"\n",
|
||
"graph.add_node(\"preparation\", preparation_node)\n",
|
||
"graph.add_node(\"preparation_tools\", tool_execution_node)\n",
|
||
"graph.add_node(\"query\", query_node)\n",
|
||
"graph.add_node(\"validate\", validation_node)\n",
|
||
"graph.add_node(\"repair\", repair_node)\n",
|
||
"\n",
|
||
"graph.add_node(\"human_approval\", human_approval_node)\n",
|
||
"\n",
|
||
"graph.add_node(\"query_tools\", query_execution_node)\n",
|
||
"graph.add_node(\"synthesis\", synthesis_node)\n",
|
||
"\n",
|
||
"graph.set_entry_point(\"preparation\")\n",
|
||
"graph.add_edge(\"preparation\", \"preparation_tools\")\n",
|
||
"graph.add_edge(\"preparation_tools\", \"query\")\n",
|
||
"graph.add_edge(\"query\", \"validate\")\n",
|
||
"\n",
|
||
"\n",
|
||
"def validation_router(state: AgentState):\n",
|
||
" return \"repair\" if state.get(\"validation_errors\") else \"human_approval\"\n",
|
||
"\n",
|
||
"\n",
|
||
"graph.add_conditional_edges(\n",
|
||
" \"validate\",\n",
|
||
" validation_router,\n",
|
||
" {\n",
|
||
" \"repair\": \"repair\",\n",
|
||
" \"human_approval\": \"human_approval\",\n",
|
||
" },\n",
|
||
")\n",
|
||
"\n",
|
||
"graph.add_edge(\"repair\", \"validate\")\n",
|
||
"\n",
|
||
"\n",
|
||
"def approval_router(state: AgentState):\n",
|
||
" fb = state[\"human_feedback\"]\n",
|
||
"\n",
|
||
" if fb.get(\"approved\"):\n",
|
||
" return \"query_tools\"\n",
|
||
" if \"edited_plan\" in fb:\n",
|
||
" return \"validate\"\n",
|
||
" if \"new_question\" in fb:\n",
|
||
" return \"preparation\"\n",
|
||
" return \"preparation\"\n",
|
||
"\n",
|
||
"\n",
|
||
"graph.add_conditional_edges(\n",
|
||
" \"human_approval\",\n",
|
||
" approval_router,\n",
|
||
" {\n",
|
||
" \"query_tools\": \"query_tools\",\n",
|
||
" \"validate\": \"validate\",\n",
|
||
" \"preparation\": \"preparation\",\n",
|
||
" },\n",
|
||
")\n",
|
||
"\n",
|
||
"\n",
|
||
"graph.add_edge(\"query_tools\", \"synthesis\")\n",
|
||
"\n",
|
||
"agent = graph.compile()\n",
|
||
"agent"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "bfb38954",
|
||
"metadata": {},
|
||
"source": [
|
||
"Testons-le !"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 91,
|
||
"id": "22e64782",
|
||
"metadata": {},
|
||
"outputs": [
|
||
{
|
||
"name": "stderr",
|
||
"output_type": "stream",
|
||
"text": [
|
||
"Batches: 100%|██████████| 409/409 [00:47<00:00, 8.52it/s]\n"
|
||
]
|
||
},
|
||
{
|
||
"name": "stdout",
|
||
"output_type": "stream",
|
||
"text": [
|
||
"\n",
|
||
"Proposed query plan:\n",
|
||
"Indicator: FP.CPI.TOTL.ZG\n",
|
||
"Countries: FRA, DEU\n",
|
||
"Years: 2015–2025\n"
|
||
]
|
||
}
|
||
],
|
||
"source": [
|
||
"question = \"What the evolution of France's inflation between 2015 and 2025 ? Comment on this value using the inflation in Europe, or in Germany for example.\"\n",
|
||
"\n",
|
||
"initial_state = {\n",
|
||
" \"question\": question,\n",
|
||
" \"tool_thoughts\": \"\",\n",
|
||
" \"tool_calls\": [],\n",
|
||
" \"tool_results\": [],\n",
|
||
" \"query_thoughts\": \"\",\n",
|
||
" \"query_plan\": {},\n",
|
||
" \"validation_errors\": [],\n",
|
||
" \"human_feedback\": {},\n",
|
||
" \"query_results\": [],\n",
|
||
" \"answer\": \"\",\n",
|
||
"}\n",
|
||
"\n",
|
||
"result = agent.invoke(initial_state)"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": null,
|
||
"id": "9f3b8cf9",
|
||
"metadata": {},
|
||
"outputs": [
|
||
{
|
||
"name": "stdout",
|
||
"output_type": "stream",
|
||
"text": [
|
||
"```tool_code\n",
|
||
"from wbdata import wbdata\n",
|
||
"import pandas as pd\n",
|
||
"\n",
|
||
"# Define the indicator for inflation (CPI, % change)\n",
|
||
"indicator = 'CPI-ACP-RD'\n",
|
||
"\n",
|
||
"# Define the country for France\n",
|
||
"country = 'FRA'\n",
|
||
"\n",
|
||
"# Define the year range\n",
|
||
"start_year = 2015\n",
|
||
"end_year = 2025\n",
|
||
"\n",
|
||
"# Fetch the data for France\n",
|
||
"france_inflation = wbdata.get_series(indicator, country, start_year=start_year, end_year=end_year)\n",
|
||
"\n",
|
||
"# Fetch the data for Europe (aggregate region)\n",
|
||
"europe_inflation = wbdata.get_series(indicator, 'EUU', start_year=start_year, end_year=end_year)\n",
|
||
"\n",
|
||
"# Fetch the data for Germany\n",
|
||
"germany_inflation = wbdata.get_series(indicator, 'DEU', start_year=start_year, end_year=end_year)\n",
|
||
"\n",
|
||
"\n",
|
||
"# Convert to pandas DataFrames for easier handling\n",
|
||
"france_inflation_df = france_inflation.to_frame(name='France')\n",
|
||
"europe_inflation_df = europe_inflation.to_frame(name='Europe')\n",
|
||
"germany_inflation_df = germany_inflation.to_frame(name='Germany')\n",
|
||
"\n",
|
||
"# Combine the dataframes\n",
|
||
"combined_df = pd.concat([france_inflation_df, europe_inflation_df, germany_inflation_df], axis=1)\n",
|
||
"\n",
|
||
"# Print the combined data\n",
|
||
"print(combined_df)\n",
|
||
"```\n",
|
||
"\n"
|
||
]
|
||
}
|
||
],
|
||
"source": [
|
||
"print(result[\"answer\"])"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 93,
|
||
"id": "2a5677ed",
|
||
"metadata": {},
|
||
"outputs": [
|
||
{
|
||
"name": "stdout",
|
||
"output_type": "stream",
|
||
"text": [
|
||
"{'question': \"What the evolution of France's inflation between 2015 and 2025 ? Comment on this value using the inflation in Europe, or in Germany for example.\", 'tool_thoughts': \"First, I need to determine which indicator represents inflation. I will use the 'retrieve_indicators' tool to find appropriate indicators.\", 'tool_calls': [{'name': 'retrieve_indicators', 'args': {'query': 'inflation'}}], 'tool_results': [{'tool': 'retrieve_indicators', 'args': {'query': 'inflation'}, 'result': indicator \\\n",
|
||
"6299 FP.CPI.TOTL.ZG \n",
|
||
"9781 NY.GDP.DEFL.KD.ZG \n",
|
||
"6303 FP.WPI.TOTL.ZG \n",
|
||
"9782 NY.GDP.DEFL.KD.ZG.AD \n",
|
||
"6301 FP.FPI.TOTL.ZG \n",
|
||
"9780 NY.GDP.DEFL.87.ZG \n",
|
||
"6298 FP.CPI.TOTL \n",
|
||
"6275 FM.LBL.MQMY.ZG \n",
|
||
"6313 FR.INR.MMKT \n",
|
||
"10930 PI-16 \n",
|
||
"\n",
|
||
" description score \n",
|
||
"6299 Inflation, consumer prices (annual %) 0.518722 \n",
|
||
"9781 Inflation, GDP deflator (annual %) 0.460076 \n",
|
||
"6303 Inflation, wholesale prices (annual %) 0.456499 \n",
|
||
"9782 Inflation, GDP deflator: linked series (annual %) 0.409883 \n",
|
||
"6301 Inflation, food prices (annual %) 0.402444 \n",
|
||
"9780 Inflation, GDP deflator (annual %) 0.401507 \n",
|
||
"6298 Consumer price index (2010 = 100) 0.358742 \n",
|
||
"6275 Money and quasi money growth (annual %) 0.320473 \n",
|
||
"6313 Money market rate (%) 0.313243 \n",
|
||
"10930 Medium term perspective in expenditure budgeting 0.311095 }], 'query_thoughts': 'Structured query plan generated', 'query_results': [], 'query_plan': {'indicator_code': 'FP.CPI.TOTL.ZG', 'countries': ['FRA', 'DEU'], 'start_year': 2015, 'end_year': 2025}, 'answer': \"```tool_code\\nfrom wbdata import wbdata\\nimport pandas as pd\\n\\n# Define the indicator for inflation (CPI, % change)\\nindicator = 'CPI-ACP-RD'\\n\\n# Define the country for France\\ncountry = 'FRA'\\n\\n# Define the year range\\nstart_year = 2015\\nend_year = 2025\\n\\n# Fetch the data for France\\nfrance_inflation = wbdata.get_series(indicator, country, start_year=start_year, end_year=end_year)\\n\\n# Fetch the data for Europe (aggregate region)\\neurope_inflation = wbdata.get_series(indicator, 'EUU', start_year=start_year, end_year=end_year)\\n\\n# Fetch the data for Germany\\ngermany_inflation = wbdata.get_series(indicator, 'DEU', start_year=start_year, end_year=end_year)\\n\\n\\n# Convert to pandas DataFrames for easier handling\\nfrance_inflation_df = france_inflation.to_frame(name='France')\\neurope_inflation_df = europe_inflation.to_frame(name='Europe')\\ngermany_inflation_df = germany_inflation.to_frame(name='Germany')\\n\\n# Combine the dataframes\\ncombined_df = pd.concat([france_inflation_df, europe_inflation_df, germany_inflation_df], axis=1)\\n\\n# Print the combined data\\nprint(combined_df)\\n```\\n\", 'human_feedback': {'approved': True}}\n"
|
||
]
|
||
}
|
||
],
|
||
"source": [
|
||
"print(result)"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": null,
|
||
"id": "df94fd32",
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": []
|
||
}
|
||
],
|
||
"metadata": {
|
||
"kernelspec": {
|
||
"display_name": "studies",
|
||
"language": "python",
|
||
"name": "python3"
|
||
},
|
||
"language_info": {
|
||
"codemirror_mode": {
|
||
"name": "ipython",
|
||
"version": 3
|
||
},
|
||
"file_extension": ".py",
|
||
"mimetype": "text/x-python",
|
||
"name": "python",
|
||
"nbconvert_exporter": "python",
|
||
"pygments_lexer": "ipython3",
|
||
"version": "3.13.9"
|
||
}
|
||
},
|
||
"nbformat": 4,
|
||
"nbformat_minor": 5
|
||
}
|