diff --git a/i18n/ar/code.json b/i18n/ar/code.json index 466b209..40d3a2e 100644 --- a/i18n/ar/code.json +++ b/i18n/ar/code.json @@ -917,5 +917,13 @@ "homepage.projects.rateLimitedApi.summary": { "message": "ابنِ خدمة FastAPI حقيقية تغلّف مجموعة بياناتك الخاصة، مع مصادقة حقيقية بمفتاح API ومحدِّد معدل بنافذة منزلقة تبنيّه من الصفر.", "description": "Homepage project card summary" + }, + "homepage.projects.recipePlannerAgent.title": { + "message": "بناء وكيل مخطط للوصفات", + "description": "Homepage project card title" + }, + "homepage.projects.recipePlannerAgent.summary": { + "message": "ابنِ وكيل ذكاء اصطناعي يستخدم الأدوات يقترح وجبات من المكونات المتوفرة لديك، مرتكزًا على قاعدة بيانات وصفات محلية حقيقية بدلًا من التخمين.", + "description": "Homepage project card summary" } } diff --git a/i18n/ar/docusaurus-plugin-content-docs/current/projects/index.mdx b/i18n/ar/docusaurus-plugin-content-docs/current/projects/index.mdx index b23cb2e..e6fb270 100644 --- a/i18n/ar/docusaurus-plugin-content-docs/current/projects/index.mdx +++ b/i18n/ar/docusaurus-plugin-content-docs/current/projects/index.mdx @@ -155,5 +155,11 @@ import {mergeProjectMeta} from '@site/src/data/projects'; summary: 'ابنِ خدمة FastAPI حقيقية تغلّف مجموعة بياناتك الخاصة، مع مصادقة حقيقية بمفتاح API ومحدِّد معدل بنافذة منزلقة تبنيّه من الصفر.', }, + { + id: 'recipe-planner-agent', + title: 'بناء وكيل مخطط للوصفات', + summary: + 'ابنِ وكيل ذكاء اصطناعي يستخدم الأدوات مع deepagents من LangChain، يقترح وجبات من المكونات المتوفرة لديك، مرتكزًا على قاعدة بيانات وصفات محلية حقيقية.', + }, ])} /> diff --git a/i18n/ar/docusaurus-plugin-content-docs/current/projects/recipe-planner-agent/_category_.json b/i18n/ar/docusaurus-plugin-content-docs/current/projects/recipe-planner-agent/_category_.json new file mode 100644 index 0000000..9d2b4a8 --- /dev/null +++ b/i18n/ar/docusaurus-plugin-content-docs/current/projects/recipe-planner-agent/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "وكيل مخطط للوصفات", + "position": 14 +} diff --git a/i18n/ar/docusaurus-plugin-content-docs/current/projects/recipe-planner-agent/index.md b/i18n/ar/docusaurus-plugin-content-docs/current/projects/recipe-planner-agent/index.md new file mode 100644 index 0000000..8eb7e18 --- /dev/null +++ b/i18n/ar/docusaurus-plugin-content-docs/current/projects/recipe-planner-agent/index.md @@ -0,0 +1,412 @@ +--- +id: recipe-planner-agent +title: "بناء وكيل مخطط للوصفات" +sidebar_label: "وكيل مخطط للوصفات" +slug: /projects/recipe-planner-agent +description: "انتقل من الملعب داخل المتصفح إلى Python فعلي: ابنِ وكيل ذكاء اصطناعي يستخدم الأدوات مع deepagents من LangChain، يقترح وجبات من المكونات المتوفرة لديك، مرتكزًا على قاعدة بيانات وصفات محلية حقيقية." +--- + +import ProjectProgressCheckbox from '@site/src/components/ProjectProgressCheckbox'; +import ProjectPublishedDate from '@site/src/components/ProjectPublishedDate'; +import ProjectGreeting from '@site/src/components/ProjectGreeting'; +import {StepChecklist, StepChecklistItem} from '@site/src/components/StepChecklist'; + +# 🌍 بناء وكيل مخطط للوصفات + + + + + +تكتب قائمة بمكونات لديك فعليًا — لنقل، بيضًا وطماطمًا وثومًا وخبزًا — فيقترح وكيل 2-3 وجبات حقيقية يمكنك إعدادها بها، ثم يبني قائمة تسوق لما ينقص لأفضل خيار. ما يجعل هذا وكيلًا مفيدًا فعليًا لا مجرد روبوت محادثة: إنه لا يخترع وصفة أبدًا. يستدعي أداة تبحث في قاعدة بيانات وصفات محلية حقيقية ولا يمكنه اقتراح سوى ما تعيده تلك الأداة فعلًا — نفس فكرة الارتكاز وراء أنظمة أكثر جدية بكثير من "لا تدع النموذج يختلق الأمور"، مصغَّرة إلى شيء يمكنك بناؤه في ظهيرة واحدة. + +يفترض هذا Python بمستوى 101. إن إنجاز [مشروع وكيل الذكاء الاصطناعي](/docs/projects/ai-agent) أولًا مساعدة حقيقية، لا شرطًا صارمًا — يعيد هذا المشروع استخدام نفس إطار عمل `deepagents` ونفس نمط استدعاء الأدوات، فقط بأداة أكثر تنظيماً وأقرب لشكل العالم الحقيقي. هذا اختياري وغير مُقيَّم؛ راجع [مشاريع من العالم الحقيقي](/docs/projects) للاطلاع على القائمة الكاملة والنامية. + +## 🎯 ما ستفعله + +1. تثبيت `uv`، والحصول على مفتاح API للذكاء الاصطناعي من الطبقة المجانية، وإعداد مشروع صغير بـ`deepagents` — كل ذلك مقدمًا، في قسم الإعداد أدناه. +2. تعريف "قاعدة بيانات وصفات" محلية صغيرة — قائمة Python بسيطة من القواميس، 10-15 وصفة، لكل واحدة قائمة مكوناتها الخاصة. +3. كتابة دالة أداة يستطيع الوكيل استدعاءها للبحث في تلك القاعدة بالمكونات المتوفرة لديك. +4. ربط تلك الأداة بوكيل `deepagents` مع برومبت نظام يبقيه مرتكزًا على وصفات حقيقية فقط. +5. طلب اقتراحات وجبات من الوكيل انطلاقًا من قائمة مكونات حقيقية، ثم جعله يبني قائمة تسوق للوصفة التي تختارها. + +## أين تُشغّل هذا + +**محليًا باستخدام `uv`** هو المسار الأساسي والموصى به — Python فعلي مثبّت على جهازك الخاص، نفس خطوة "التخرّج إلى Python فعلي" ككل مشروع آخر في هذا القسم. تفترض الخطوات من 1 فصاعدًا هذا المسار. + +**GitHub Codespaces** يعمل بنفس الجودة: افتح [مستودع الدورة كاملًا في Codespace مجاني](https://codespaces.new/abderrahim-lectures/python-data-analysis-course) (Node وPython وuv مثبّتة بالفعل، وفق `.devcontainer/devcontainer.json` الخاص بالمستودع) ونفّذ نفس أوامر `uv` تمامًا من طرفية في تبويب متصفحك. + +**Google Colab وKaggle Notebooks أو Binder** جيدة أيضًا — هذا سكربت خفيف يستدعي API فقط، بلا GPU أو تثبيت ثقيل. نسخة دفتر ملاحظات جاهزة للتشغيل من هذا المشروع ([`examples/recipe-planner-agent/notebook.ipynb`](https://github.com/abderrahim-lectures/python-data-analysis-course/blob/main/examples/recipe-planner-agent/notebook.ipynb)) على بُعد نقرة واحدة: + +[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/abderrahim-lectures/python-data-analysis-course/blob/main/examples/recipe-planner-agent/notebook.ipynb) +[![Open In Kaggle](https://kaggle.com/static/images/open-in-kaggle.svg)](https://kaggle.com/kernels/welcome?src=https://github.com/abderrahim-lectures/python-data-analysis-course/blob/main/examples/recipe-planner-agent/notebook.ipynb) +[![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/abderrahim-lectures/python-data-analysis-course/main?filepath=examples%2Frecipe-planner-agent%2Fnotebook.ipynb) + +إنها طريقة أقل دقة لتجربة المشروع من مشروع `uv` محلي فعلي — بلا ملفات منفصلة، بلا بنية مشروع حقيقية — لكنها قابلة للعمل تمامًا لتجربة الفكرة. اضبط مفتاح API خاصتك بـ`os.environ["GITHUB_TOKEN"] = "..."` في خلية getpass (أو استخدم لوحة Secrets في Colab). + +## الإعداد + +كل ما تحتاجه قبل أن تكتب سطرًا واحدًا من الوكيل نفسه موجود هنا — تثبيت `uv`، والحصول على مفتاح API، وإنشاء المشروع، وإعداد ملف `.env` الخاص بك. تفترض الخطوات من 1 فصاعدًا أن كل هذا قد أُنجز بالفعل. + +### ثبّت `uv` + +`uv` أداة واحدة تحل محل السلسلة المعتادة "ثبّت Python، ثم ثبّت pip، ثم ثبّت أداة بيئة افتراضية، ثم ثبّت الحزم" — يمكنها تثبيت وإدارة إصدارات Python بنفسها، إلى جانب تبعيات مشروعك. + +**macOS / Linux** (الطرفية): + +```bash +curl -LsSf https://astral.sh/uv/install.sh | sh +``` + +**Windows** (PowerShell): + +```powershell +powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex" +``` + +أغلق طرفيتك وأعد فتحها، ثم تأكد من أنها ثُبِّتت: + +```bash +uv --version +``` + +إن لم يكن لديك بعد مُفسِّر Python فعلي مثبّت ومُدار بواسطة `uv` (من مشروع سابق في هذه السلسلة)، فاحصل على واحد الآن: + +```bash +uv python install 3.12 +``` + +### احصل على مفتاح API للذكاء الاصطناعي مجانًا + +**اختر أي مزوّد تفضّله** — لا يتطلب أي منها بطاقة ائتمان وقت كتابة هذا، ولا تفضّل هذه الدورة مزوّدًا على آخر. + +| المزوّد | أين تحصل على مفتاح | لماذا قد تختاره | +|---|---|---| +| **GitHub Models** *(مقترح افتراضيًا)* | [github.com/settings/tokens](https://github.com/settings/tokens) — رمز وصول شخصي بنطاق `models: read` | بلا تسجيل منفصل — لديك بالفعل حساب GitHub. حدود الطبقة المجانية أسخى من حدود Gemini. | +| Gemini | [Google AI Studio](https://aistudio.google.com/) | الخيار الأكثر شيوعًا في الإشارة إليه. | +| Groq | [console.groq.com/keys](https://console.groq.com/keys) | استدلال سريع، طبقة مجانية سخية، بلا بطاقة. | +| Mistral | [console.mistral.ai/api-keys](https://console.mistral.ai/api-keys) | أحد الحصص المجانية الدائمة الأسخى. | +| Cerebras | [cloud.cerebras.ai](https://cloud.cerebras.ai/) | حجم رموز يومي مرتفع، بلا بطاقة. | +| OpenRouter | [openrouter.ai/keys](https://openrouter.ai/keys) | API واحد، نماذج مجانية كثيرة — جيد لمقارنة المزوّدين. | + +أيًا كان ما تختاره، فالعملية نفسها: سجّل الدخول، وولّد مفتاحًا على موقع ذلك المزوّد، و**لا تلصقه أبدًا مباشرة في الكود ولا تثبّته في مستودع**. يبقي هذا المشروع المفتاح في ملف `.env` (أدناه) بدلًا من ذلك. + +### أعدّ المشروع باستخدام `uv` + +```bash +uv init recipe-planner-agent +cd recipe-planner-agent +uv add deepagents langchain-openai python-dotenv +``` + +ينشئ `uv init` مشروعًا صغيرًا (ملف `pyproject.toml` يتتبع اعتمادياتك)، ويثبّت `uv add` الحزم في بيئة معزولة لذلك المشروع تلقائيًا، دون إعداد بيئة افتراضية يدويًا. `deepagents` هو إطار عمل LangChain لبناء وكلاء مزوّدين باستخدام أدوات مدمج — نفس الإطار المستخدم في [مشروع وكيل الذكاء الاصطناعي](/docs/projects/ai-agent)؛ `langchain-openai` هي حزمة التكامل التي يستخدمها هذا المثال للتحدث مع GitHub Models (واجهته البرمجية متوافقة مع OpenAI، لذا تعمل حزمة تكامل OpenAI معه أيضًا — انظر التلميح أدناه إن اخترت مزوّدًا مختلفًا)؛ `python-dotenv` تتيح لك إبقاء مفتاح API في ملف `.env` محلي. + +إن اخترت مزوّدًا مختلفًا أعلاه، فاستبدل `langchain-openai` بحزمة ذلك المزوّد — `langchain-google-genai` (Gemini)، أو `langchain-groq` (Groq)، أو `langchain-mistralai` (Mistral). كل من Cerebras وOpenRouter متوافقان أيضًا مع OpenAI، لذا يستخدمان `langchain-openai` كذلك، فقط مع `base_url` مختلف. + +:::tip[تحقق من الوثائق الحالية — ومن اسم النموذج] +تتقدم أطر عمل الوكلاء بسرعة، وكذلك أسماء النماذج: تُعاد تسميتها وتُسحب على مقياس أشهر لا سنوات. استخدم معرّف نموذج صريحًا ومُحدَّد الإصدار بدلًا من اسم مستعار `-latest` — عدة مزوّدين، بما في ذلك Google، ألغوا هذه الأسماء المستعارة لأنها تبدّل بصمت إلى إصدار نموذج جديد، مما قد يكسر كودًا يعمل دون أي تحذير. قبل تشغيل هذا، تحقق من صفحة الأسعار/النموذج الحالية لمزوّدك، واطّلع على README الخاص بـ`deepagents` نفسه لواجهته البرمجية الحالية. +::: + +### أنشئ ملف `.env` الخاص بك + +في مجلد مشروعك، أنشئ ملفًا باسم `.env` (لا تثبّته أبدًا) يحوي مفتاح أي مزوّد اخترته: + +```bash +# .env +GITHUB_TOKEN=your-key-here +``` + +يقرأ `python-dotenv` (المثبّت أعلاه) هذا الملف في `os.environ` في أعلى سكربتك، لذا لا يحوي كودك المفتاح مطبوعًا فيه مباشرة أبدًا. + +**✅ قائمة التحقق** + + +يطبع `uv --version` رقم إصدار. +لديك مفتاح API حقيقي من مزوّد واحد، وهو محفوظ في ملف `.env` — لا ملصوق في أي ملف `.py`. +اكتمل `uv add deepagents langchain-openai python-dotenv` (أو حزمة مزوّدك) دون أخطاء. + + +## الخطوة 1: ابنِ قاعدة بيانات الوصفات المحلية الخاصة بك + +كل ما سيقترحه الوكيل يومًا ما يأتي من هذه البنية البياناتية الواحدة — قائمة Python بسيطة من القواميس، بلا خادم قاعدة بيانات، بلا API خارجي. أنشئ `recipes.py`: + +```python +# recipes.py +RECIPES = [ + { + "name": "Tomato Egg Stir-Fry", + "ingredients": ["eggs", "tomatoes", "garlic", "salt", "oil"], + "instructions": "Scramble the eggs, set aside. Saute garlic and chopped tomatoes " + "until soft, stir the eggs back in, season with salt.", + }, + { + "name": "Garlic Butter Pasta", + "ingredients": ["pasta", "butter", "garlic", "parmesan", "salt"], + "instructions": "Boil the pasta. Melt butter with minced garlic, toss the pasta " + "in it, top with grated parmesan and salt.", + }, + { + "name": "Classic Grilled Cheese", + "ingredients": ["bread", "cheese", "butter"], + "instructions": "Butter one side of each bread slice, add cheese between the " + "unbuttered sides, grill in a pan until golden on both sides.", + }, + { + "name": "Simple Fried Rice", + "ingredients": ["rice", "eggs", "soy sauce", "onion", "oil"], + "instructions": "Scramble the eggs and set aside. Fry chopped onion in oil, add " + "cooked rice, stir in soy sauce and the eggs.", + }, + { + "name": "Chickpea Salad", + "ingredients": ["chickpeas", "cucumber", "tomatoes", "olive oil", "lemon", "salt"], + "instructions": "Drain the chickpeas, dice the cucumber and tomatoes, toss " + "everything with olive oil, lemon juice, and salt.", + }, + # ... a real database keeps going. See examples/recipe-planner-agent/recipes.py + # in the course repo for the full 13-recipe version this lesson uses. +] +``` + +كل وصفة مجرد قاموس يحوي `name`، وقائمة `ingredients` (بحروف صغيرة، بلا كميات — فقط ما يُحتاج إليه)، و`instructions` قصيرة. هذا نفس الشكل تمامًا لقائمة `topics` التجريبية من `search_course_topics` في مشروع وكيل الذكاء الاصطناعي، فقط أثرى: قائمة سجلات مُهيكَلة يمكن لدالة أداتك البحث فوقها. + +:::tip[الأكبر أفضل فعلًا هنا] +قاعدة بيانات وصفات تحوي 3-4 مدخلات ستجعل وكيلك يبدو معطّلًا حتى عندما يكون الكود سليمًا — معظم قوائم المكونات التي يكتبها طالب لن تتقاطع ببساطة مع أي شيء. استهدف الوصفات العشر إلى الخمس عشرة كاملة (نسخة المستودع تحوي 13)، تغطي مزيجًا حقيقيًا من البروتينات والكربوهيدرات والخضروات، لكي تتاح لقائمة نموذجية من "ماذا يوجد في ثلاجتي" فرصة لائقة للتوافق مع شيء. +::: + +**✅ قائمة التحقق** + + +يعرّف `recipes.py` قائمة `RECIPES` كقائمة من 10 قواميس على الأقل. +لكل وصفة `name`، و`ingredients` (قائمة)، و`instructions`. +أسماء المكونات بحروف صغيرة ومتسقة عبر الوصفات (مثلًا دائمًا `"tomatoes"`، لا خليط من `"tomatoes"` و`"Tomato"` أبدًا). + + +**🤔 سؤال (أسئلة) سقراطي(ة)** + +- لماذا قائمة قواميس بدلًا من، لنقل، قاموس مفتاحه اسم الوصفة؟ ماذا ستكسب أو تخسر في كلتا الحالتين؟ +- إن تشاركت وصفاتان كل مكوناتهما تقريبًا، كيف قد يؤثر ذلك على أيّهما يميل الوكيل لاقتراحها أولًا؟ + +## الخطوة 2: اكتب أداة يستطيع الوكيل البحث بها عن الوصفات + +لا يحق للوكيل قراءة `recipes.py` مباشرة — لا يمكنه رؤية سوى ما تُرجعه دالة أداة، تمامًا مثل `search_course_topics` في مشروع وكيل الذكاء الاصطناعي. أضف هذا إلى `recipes.py`، أو إلى ملف جديد يستورد `RECIPES`: + +```python +def search_recipes_by_ingredients(ingredients: list[str]) -> str: + """Search the local recipe database for recipes that best match the given ingredients. + + `ingredients` should be a list of ingredient names the caller already + has on hand (e.g. ["eggs", "tomatoes", "garlic"]). Returns the top + matching recipes, ranked by how many of their ingredients are already + covered, each with its full ingredient list and the ingredients still + missing -- so a shopping list can be built from the result without + guessing. Returns a plain "no matches" message if nothing overlaps at + all, so the caller never has to invent a recipe out of thin air. + """ + have = {i.strip().lower() for i in ingredients} + scored = [] + for recipe in RECIPES: + needed = {i.lower() for i in recipe["ingredients"]} + overlap = have & needed + if not overlap: + continue + missing = sorted(needed - have) + scored.append((len(overlap), recipe, missing)) + + if not scored: + return "No matching recipes found in the database for those ingredients." + + scored.sort(key=lambda row: row[0], reverse=True) + top = scored[:5] + + lines = [] + for _, recipe, missing in top: + missing_text = ", ".join(missing) if missing else "nothing -- you have it all!" + lines.append( + f"- {recipe['name']} | full ingredient list: {', '.join(recipe['ingredients'])} " + f"| missing: {missing_text}" + ) + return "Matching recipes (best match first):\n" + "\n".join(lines) +``` + +الفكرة الجوهرية: `have & needed` (تقاطع المجموعات) يعدّ كم من مكونات وصفة لديك بالفعل، و`needed - have` (فرق المجموعات) هو بالضبط ما ينقص بعد. ترتيب بحجم التقاطع، الأكبر أولًا، يعني أن الوصفات الأقرب إلى "جاهزة للطهي الآن" تأتي أولًا — ولأن الأداة تُرجع المكونات الناقصة *لكل* مرشَّح، لا الأفضل فقط، فللوكيل كل ما يحتاجه لبناء قائمة تسوق لاحقًا دون بحث ثانٍ. + +لاحظ أن نوع الإرجاع سلسلة بسيطة، مثل `search_course_topics` و`count_words` في المشاريع السابقة — يقرأ النموذج نصًا لا كائنات Python، لذا فإن سلسلة مُنسَّقة بوضوح هي ما يجب أن تُرجعه الأداة. + +**✅ قائمة التحقق** + + +`search_recipes_by_ingredients(["eggs", "tomatoes", "garlic"])` المُستدعاة مباشرة في Python (بلا وكيل بعد) تُرجع سلسلة حقيقية غير فارغة. +استدعاؤها بمكونات لا تتطابق مع أي شيء في `RECIPES` يُرجع رسالة "no matching recipes"، لا خطأ. +يشرح الـdocstring ما تفعله الدالة وما تُرجعه — لا حشوًا مؤقتًا. + + +**🤔 سؤال (أسئلة) سقراطي(ة)** + +- لماذا تُرجع الأداة المكونات الناقصة لأفضل 5 تطابقات، لا لأفضل واحد فقط؟ ماذا سيخسر الوكيل لو حصل على أفضل تطابق فقط؟ +- ماذا يحدث الآن إذا مرّر شخص `["Tomatoes"]` (بحرف كبير) — هل ما يزال يتطابق مع `"tomatoes"` في قاعدة البيانات؟ ولماذا؟ + +## الخطوة 3: اربط الأداة بوكيل `deepagents` + +أنشئ `planner.py`: + +```python +import os + +from deepagents import create_deep_agent +from dotenv import load_dotenv +from langchain_openai import ChatOpenAI + +from recipes import RECIPES, search_recipes_by_ingredients + +load_dotenv() # reads .env into the environment, if present + +SYSTEM_PROMPT = """You are a helpful recipe-planning assistant. + +You have exactly one source of truth for what recipes exist: the +search_recipes_by_ingredients tool. Never invent, guess, or recall a recipe +from your own training data -- only suggest recipes that tool actually +returned in its results for this conversation. + +When a student lists what they have on hand: +1. Call search_recipes_by_ingredients with that ingredient list. +2. Suggest 2-3 recipes from the tool's results, explaining briefly why each + is a good fit (how much they already have). +3. If the tool returns no matches, say so plainly and suggest the student + try listing a few more ingredients -- do not make up a recipe to fill + the gap. +4. If asked to build a shopping list for a specific recipe, use the + "missing" ingredients the tool already reported for that recipe -- don't + recompute or guess at what's missing. +""" + +model = ChatOpenAI( + model="gpt-4o-mini", # confirm this still has a free tier before running -- see the tip above + api_key=os.environ["GITHUB_TOKEN"], + base_url="https://models.github.ai/inference", +) + +agent = create_deep_agent( + model=model, + tools=[search_recipes_by_ingredients], + system_prompt=SYSTEM_PROMPT, +) +``` + +هذا نفس شكل `create_deep_agent(model=..., tools=[...], system_prompt=...)` من مشروع وكيل الذكاء الاصطناعي، بأداة واحدة بدلًا من اثنتين. ما يختلف، ويستحق التأمل، هو **برومبت النظام**: لا يصف الأداة فحسب، بل يحظر صراحةً نمط الفشل الذي صُمم هذا المشروع بأكمله لإظهاره — اقتراح وصفة لم تُرجعها الأداة أبدًا. كون الأداة *متاحة* لا يضمن أن النموذج يستخدمها دائمًا؛ برومبت النظام هو المكان الذي تخبره فيه أن استخدام الأداة، والأداة فقط، ليس اختياريًا هنا. + +**✅ قائمة التحقق** + + +يستورد `planner.py` كلا `RECIPES` و`search_recipes_by_ingredients` من `recipes.py` دون أخطاء. +يعمل `agent = create_deep_agent(...)` دون رفع استثناء — هذا وحده لا يستدعي النموذج بعد، فقط يبني الوكيل. +يقول برومبت النظام صراحةً ألا يقترح وصفة لم تُرجعها الأداة. + + +**🤔 سؤال (أسئلة) سقراطي(ة)** + +- يخبر برومبت النظام النموذج ماذا يفعل إن لم تُرجع الأداة أي تطابق. ماذا تعتقد أن يحدث لو حذفت تلك التعليمات تمامًا — من أين قد تأتي إجابة النموذج بدلًا من ذلك؟ +- لماذا تمرير `tools=[search_recipes_by_ingredients]` (الدالة نفسها) بدلًا من، لنقل، `tools=[RECIPES]` (البيانات الخام)؟ ماذا يمكن للنموذج فعلًا أن يفعل بقائمة خام من القواميس كـ"أداة"؟ + +## الخطوة 4: اطلب اقتراحات الوجبات + +أضف كتلة تشغيل في أسفل `planner.py`: + +```python +if __name__ == "__main__": + on_hand = "I have eggs, tomatoes, garlic, bread, and cheese. What can I make?" + print("🧑 You:", on_hand) + result = agent.invoke({"messages": [{"role": "user", "content": on_hand}]}) + print("🤖 Agent:", result["messages"][-1].content) +``` + +شغّلها: + +```bash +uv run python planner.py +``` + +يجب أن ترى الإجابة النهائية للوكيل: 2-3 أسماء وصفات حقيقية مسحوبة مباشرة من `RECIPES`، كل واحد مع سبب قصير لملاءمته لمكوناتك. إذا كنت فضوليًا حول *كيف* وصل إلى هناك — أي استدعاء أداة حدث، وبأي وسائط، وماذا أعادت الأداة فعلًا قبل أن يكتب النموذج إجابته — اطبع قائمة `result["messages"]` كاملة بدلًا من الأخيرة فقط، وهي نفس التقنية المشروحة في قسم "فهم التتبع الداخلي الكامل" في مشروع وكيل الذكاء الاصطناعي: `HumanMessage` (سؤالك)، و`AIMessage` يطلب استدعاء الأداة، و`ToolMessage` يحوي السلسلة الحقيقية التي أعادتها `search_recipes_by_ingredients`، ثم `AIMessage` أخير بالإجابة. + +**✅ قائمة التحقق** + + +طباعة تشغيل `uv run python planner.py` تعرض إجابة حقيقية، لا traceback. +كل اسم وصفة في الإجابة يظهر فعلًا في `RECIPES` — تحقق بالعين، أو بالبحث في `recipes.py`. +جرّبت قائمة مكونات واحدة على الأقل تتطابق بشكل ضعيف، وعالجها الوكيل بشكل معقول (قال ذلك، أو اقترح خيارات قريبة) بدلًا من اختلاق شيء. + + +**🤔 سؤال (أسئلة) سقراطي(ة)** + +- إذا غيّرت `on_hand` إلى مكونات لا تتقاطع مع أي شيء في قاعدتك، ماذا يقول الوكيل؟ هل يتبع تعليمات برومبت النظام، أم يتراجع إلى التخمين؟ +- تُرجع الأداة أفضل 5 تطابقات لديها، لكن برومبت النظام يطلب 2-3 اقتراحات. أين يحدث هذا التضييق — في كود Python الخاص بك، أم داخل استدلال النموذج؟ + +## الخطوة 5: ابنِ قائمة تسوق وشغّله من البداية إلى النهاية + +لأن `search_recipes_by_ingredients` حسَبَت بالفعل المكونات الناقصة لكل وصفة مرشَّحة، فإن الحصول على قائمة تسوق هو مجرد سؤال متابعة في نفس المحادثة — بلا أداة جديدة مطلوبة. وسّع كتلة التشغيل لتواصل المحادثة بدلًا من بدء واحدة جديدة كل مرة: + +```python +if __name__ == "__main__": + conversation = [] + + on_hand = "I have eggs, tomatoes, garlic, bread, and cheese. What can I make?" + print("🧑 You:", on_hand) + conversation.append({"role": "user", "content": on_hand}) + result = agent.invoke({"messages": conversation}) + conversation = result["messages"] # carry the full history forward + print("🤖 Agent:", conversation[-1].content) + + print() + follow_up = "Great, let's go with the first one -- what's my shopping list?" + print("🧑 You:", follow_up) + conversation.append({"role": "user", "content": follow_up}) + result = agent.invoke({"messages": conversation}) + conversation = result["messages"] + print("🤖 Agent:", conversation[-1].content) +``` + +`conversation = result["messages"]` هي السطر المهم: كل استدعاء `agent.invoke(...)` عديم الحالة بذاته، لذا فإن *الطريقة الوحيدة* لمعرفة السؤال الثاني بماذا يشير "الأول" هي أن تُعيد كامل سجل الرسائل — بما فيه إجابة النموذج السابقة نفسها وأي استدعاءات أدوات أجراها — كجزء من مدخلات الاستدعاء التالي. احذف هذا السطر وأعد التشغيل: سيعجز السؤال الثاني عن تحويل "الأول" إلى أي شيء، لأنه بالنسبة لذلك الاستدعاء، لم توجد رسالة أولى قط. + +شغّله مجددًا بـ`uv run python planner.py` ويجب أن ترى تبادلًا كاملًا وحقيقيًا: اقتراحًا، ثم قائمة تسوق مبنية من مكونات "missing" الدقيقة التي أبلغت عنها الأداة لأي وصفة اخترتها — لا تخمينًا جديدًا. + +:::tip[جرّب قائمة مكونات متناثرة عن عمد] +شغّله مجددًا بمكون أو مكونين فقط، شيئًا مثل `"I have onions and salt. What can I make?"` هذه أفضل طريقة لرؤية الحاجز الوقائي لبرومبت نظامك يعمل فعلًا: مع عدم وجود شيء تقريبًا لتطابقه، ستحصل إما على اقتراحات صادقة من "لا تطابق كبير، لكن إليك أقرب خيار"، أو (إن كان التقاطع رفيعًا جدًا) رسالة "no matches" من الأداة تمر مباشرة — في كلتا الحالتين، راقب هل ما يزال الوكيل يقاوم اختلاق ما ليس في `RECIPES`. +::: + +**✅ قائمة التحقق** + + +يشير السؤال الثاني في المحادثة بشكل صحيح إلى "الأول" من الإجابة السابقة. +قائمة التسوق التي ينتجها تطابق مكونات "missing" التي أبلغت عنها الأداة لتلك الوصفة — لا قائمة مختلفة أو مختلقة. +نفّذت اختبار المكونات المتناثرة أعلاه ولم يختلق الوكيل وصفة غير موجودة في `RECIPES`. + + +**🤔 سؤال (أسئلة) سقراطي(ة)** + +- ماذا سينكسر في سؤال المتابعة لو بدأت `conversation = []` جديدة كليًا له بدلًا من إعادة استخدام واحدة السؤال الأول؟ +- لا تستدعي خطوة قائمة التسوق أي أداة جديدة — تعيد استخدام بيانات أعادها استدعاء الأداة الأول بالفعل. ماذا يقترح ذلك بشأن تصميم القيمة المُرجَعة لأداة مع التفكير بأكثر من السؤال المباشر فقط؟ + +## ⚠️ مآزق شائعة + +- **قاعدة بيانات وصفات أصغر من اللازم.** مع حفنة وصفات فقط، لن تتقاطع معظم قوائم المكونات التي يكتبها طالب مع أي شيء، وسيبدو الوكيل معطلًا حتى عندما يكون الكود صحيحًا. استهدف الوصفات العشر إلى الخمس عشرة كاملة التي تغطي تنوعًا حقيقيًا. +- **أسماء مكونات لا تتطابق.** لن يتطابق `"tomato"` في قائمتك المكتوبة مع `"tomatoes"` في قاعدة البيانات بهذه الأداة البسيطة القائمة على المجموعات — لا توجد مطابقة ضبابية هنا. حافظ على اتساق أسماء المكونات (دائمًا بصيغة الجمع، ودائمًا بحروف صغيرة) في كل من قاعدة البيانات وما تطلبه من الوكيل، أو وسّع الأداة بتطبيع أساسي (مثل إزالة `"s"` أخيرة) إن أردت المضي أبعد. +- **اختلاق الوكيل وصفة عندما لا تُرجع الأداة شيئًا.** هذا بالضبط نمط الفشل الذي وُجد برومبت النظام في الخطوة 3 لمنعه. إذا تخطيت تلك التعليمات، أو صغتها بشكل غامض جدًا، فغالبًا ما "يساعد" نموذج قادر باقتراح شيء يبدو معقولًا بدلًا من الاعتراف بأنه لا يملك شيئًا — اختبر تحديدًا حالة المكونات المتناثرة من التلميح أعلاه للقبض على هذا. +- **فقدان سجل المحادثة بين الأسئلة.** إذا حصل سؤال متابعة مثل "ما قائمة التسوق للأول" على إجابة مشوشة أو عامة، تحقق من أنك تمرر قائمة `conversation` المتراكمة (الخطوة 5) إلى `agent.invoke(...)`، لا الرسالة الأحدث فقط وحدها. + +## ما بنيته للتو + +وكيل يجيب سؤالًا منفتحًا فعليًا — "ماذا يمكنني أن أصنع؟" — عبر إرساء كل جزء من إجابته في بيانات محلية حقيقية ومُهيكَلة بدلًا من معرفة تدريبه الخاصة، ويرفض سدّ الفجوات بتفاصيل مختلقة عندما لا تدعمها البيانات. نمط الإرساء هذا (أداة مدعومة ببيانات حقيقية، وبرومبت نظام يحظر الإجابة خارجها) هو الشكل نفسه وراء أنظمة أكثر جدية بكثير تحتاج أن يظل الذكاء الاصطناعي فيها واقعيًا: روبوت دعم مقصورًا على وثائق حقيقية، ومساعد برمجة مقصورًا على قاعدة كود حقيقية، وأداة بحث مقصورة على مصادر مسترجعة حقيقية. لقد بنيت للتو أصغر نسخة من تلك الفكرة، بالوصفات. + +## إلى أين تذهب من هنا + +- أنمِ `recipes.py` لما يتجاوز 13 مدخلًا بكثير، أو حمّله من ملف JSON أو CSV حقيقي بدلًا من قائمة Python مثبتة في الكود — بالكاد تحتاج دالة الأداة إلى تغيير. +- أضف أداة ثانية، مثل `get_recipe_instructions(name: str) -> str`، لكي يستطيع الوكيل إرشاد طالب خلال طهي الوصفة التي اقترحها للتو، لا تسميتها فقط. +- حسّن المطابقة في `search_recipes_by_ingredients` — تعامل مع صيغ الجمع البسيطة، وتجاهل أساسيات المخزن الشائعة مثل الملح والزيت عند تسجيل التقاطع (معظم المطابخ تملكها بالفعل)، أو دع الطالب يقول ما *لا* يريده صراحةً. +- أعد النظر في قسم **الوكلاء الفرعيين** من مشروع وكيل الذكاء الاصطناعي — يمكنك تقسيم هذا إلى وكيل فرعي "باحث عن الوصفات" ووكيل فرعي "قائمة تسوق"، لكل منهما مهمة أضيق. + +## شارك مشروعك مع الصف + +بنيت شيئًا فخورًا به؟ [`examples/student-projects/`](https://github.com/abderrahim-lectures/python-data-analysis-course/tree/main/examples/student-projects) معرض لمشاريع طلاب آخرين قدَّموها — وملف README الخاص به يحتوي شرحًا كاملًا وودودًا للمبتدئين لإضافة مشروعك عبر **pull request**، حتى لو لم تستخدم git من قبل قط: عمل fork للمستودع، وإنشاء فرع، وتثبيت ملفاتك، وفتح الـ PR، خطوة بخطوة. لا يُفترَض أي خبرة سابقة بـ git. + +مرحبًا بك في كتابة Python خارج المتصفح. 🎓 + + diff --git a/i18n/es/code.json b/i18n/es/code.json index a3b159c..207f767 100644 --- a/i18n/es/code.json +++ b/i18n/es/code.json @@ -917,5 +917,13 @@ "homepage.projects.rateLimitedApi.summary": { "message": "Construye un servicio FastAPI real que envuelve tu propio conjunto de datos, con autenticación genuina por clave de API y un limitador de tasa de ventana deslizante que construyes desde cero.", "description": "Homepage project card summary" + }, + "homepage.projects.recipePlannerAgent.title": { + "message": "Construye un Agente Planificador de Recetas", + "description": "Homepage project card title" + }, + "homepage.projects.recipePlannerAgent.summary": { + "message": "Construye un agente de IA que usa herramientas que sugiere comidas a partir de los ingredientes que tienes a la mano, anclado a una base de datos de recetas local real en lugar de adivinar.", + "description": "Homepage project card summary" } } diff --git a/i18n/es/docusaurus-plugin-content-docs/current/projects/index.mdx b/i18n/es/docusaurus-plugin-content-docs/current/projects/index.mdx index 86633e3..d99acee 100644 --- a/i18n/es/docusaurus-plugin-content-docs/current/projects/index.mdx +++ b/i18n/es/docusaurus-plugin-content-docs/current/projects/index.mdx @@ -155,5 +155,11 @@ Son opcionales y no calificados. Explóralos en cualquier momento — la introdu summary: 'Construye un servicio FastAPI real que envuelve tu propio conjunto de datos, con autenticación genuina por clave de API y un limitador de tasa de ventana deslizante que construyes desde cero.', }, + { + id: 'recipe-planner-agent', + title: 'Construye un Agente Planificador de Recetas', + summary: + 'Construye un agente de IA que usa herramientas con los deepagents de LangChain, que sugiere comidas a partir de los ingredientes que tienes a la mano, anclado a una base de datos de recetas local real.', + }, ])} /> diff --git a/i18n/es/docusaurus-plugin-content-docs/current/projects/recipe-planner-agent/_category_.json b/i18n/es/docusaurus-plugin-content-docs/current/projects/recipe-planner-agent/_category_.json new file mode 100644 index 0000000..f6e055f --- /dev/null +++ b/i18n/es/docusaurus-plugin-content-docs/current/projects/recipe-planner-agent/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Agente Planificador de Recetas", + "position": 14 +} diff --git a/i18n/es/docusaurus-plugin-content-docs/current/projects/recipe-planner-agent/index.md b/i18n/es/docusaurus-plugin-content-docs/current/projects/recipe-planner-agent/index.md new file mode 100644 index 0000000..31a89f7 --- /dev/null +++ b/i18n/es/docusaurus-plugin-content-docs/current/projects/recipe-planner-agent/index.md @@ -0,0 +1,412 @@ +--- +id: recipe-planner-agent +title: "Construye un Agente Planificador de Recetas" +sidebar_label: "Agente Planificador de Recetas" +slug: /projects/recipe-planner-agent +description: "Graduéate del playground en el navegador al Python real: construye un agente de IA que usa herramientas con los deepagents de LangChain, que sugiere comidas a partir de los ingredientes que tienes a la mano, anclado a una base de datos de recetas local real." +--- + +import ProjectProgressCheckbox from '@site/src/components/ProjectProgressCheckbox'; +import ProjectPublishedDate from '@site/src/components/ProjectPublishedDate'; +import ProjectGreeting from '@site/src/components/ProjectGreeting'; +import {StepChecklist, StepChecklistItem} from '@site/src/components/StepChecklist'; + +# 🌍 Construye un Agente Planificador de Recetas + + + + + +Escribes una lista de ingredientes que de hecho tienes a la mano — digamos, huevos, tomates, ajo y pan — y un agente sugiere 2-3 comidas reales que podrías hacer con ellos, luego arma una lista de compras de lo que falta para la mejor opción. El giro que hace de esto un agente genuinamente útil, no solo un chatbot: nunca inventa una receta. Llama a una herramienta que busca en una base de datos de recetas local real y solo puede sugerir lo que esa herramienta realmente devuelve — la misma idea de anclaje detrás de sistemas mucho más serios de "no dejes que el modelo invente cosas", reducida a algo que puedes construir en una tarde. + +Esto asume Python a nivel 101. Haber hecho el [proyecto de Agente de IA](/docs/projects/ai-agent) primero es una ayuda real, no un requisito duro — este proyecto reutiliza el mismo framework `deepagents` y el mismo patrón de llamada a herramientas, solo con una herramienta más estructurada y con forma del mundo real. Es opcional y no calificado; consulta [Proyectos del mundo real](/docs/projects) para la lista completa y creciente. + +## 🎯 Lo que harás + +1. Instalar `uv`, obtener una clave de API de IA de nivel gratuito, y configurar un pequeño proyecto con `deepagents` — todo por adelantado, en Configuración abajo. +2. Definir una pequeña "base de datos de recetas" local — una lista de Python simple de dicts, 10-15 recetas, cada una con su propia lista de ingredientes. +3. Escribir una función de herramienta que el agente pueda llamar para buscar en esa base de datos por los ingredientes que tienes a la mano. +4. Conectar esa herramienta a un agente `deepagents` con un prompt de sistema que lo mantenga anclado solo a recetas reales. +5. Pedirle al agente sugerencias de comidas a partir de una lista de ingredientes real, luego hacer que arme una lista de compras para la que elijas. + +## Dónde ejecutar esto + +**Localmente con `uv`** es el camino principal y recomendado — Python real instalado en tu propia máquina, el mismo movimiento de "gradúate a Python real" que todos los demás proyectos de esta sección. Los pasos 1 en adelante asumen este camino. + +**GitHub Codespaces** funciona igual de bien: abre [todo el repositorio del curso en un Codespace gratuito](https://codespaces.new/abderrahim-lectures/python-data-analysis-course) (Node, Python y `uv` ya están instalados, según el `.devcontainer/devcontainer.json` del repositorio) y ejecuta exactamente los mismos comandos `uv` desde una terminal en tu pestaña del navegador. + +**Google Colab, Kaggle Notebooks, o Binder** también funcionan bien — esto es un script ligero que solo llama a una API, sin GPU ni instalación pesada. Una versión lista para ejecutar de este proyecto ([`examples/recipe-planner-agent/notebook.ipynb`](https://github.com/abderrahim-lectures/python-data-analysis-course/blob/main/examples/recipe-planner-agent/notebook.ipynb)) está a un clic de distancia: + +[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/abderrahim-lectures/python-data-analysis-course/blob/main/examples/recipe-planner-agent/notebook.ipynb) +[![Open In Kaggle](https://kaggle.com/static/images/open-in-kaggle.svg)](https://kaggle.com/kernels/welcome?src=https://github.com/abderrahim-lectures/python-data-analysis-course/blob/main/examples/recipe-planner-agent/notebook.ipynb) +[![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/abderrahim-lectures/python-data-analysis-course/main?filepath=examples%2Frecipe-planner-agent%2Fnotebook.ipynb) + +Es una forma de menor fidelidad de experimentar el proyecto que un proyecto `uv` local real — sin archivos separados, sin estructura de proyecto real — pero perfectamente factible para probar la idea. Configura tu clave de API con `os.environ["GITHUB_TOKEN"] = "..."` en la celda de getpass (o usa el panel de Secretos de Colab). + +## Configuración + +Todo lo necesario antes de que escribas una sola línea del agente mismo vive aquí — instalar `uv`, obtener una clave de API, crear el proyecto, y configurar tu archivo `.env`. Los pasos 1 en adelante asumen que todo esto ya está hecho. + +### Instala `uv` + +`uv` es una sola herramienta que reemplaza la cadena habitual de "instala Python, luego instala pip, luego instala una herramienta de entorno virtual, luego instala paquetes" — puede instalar y gestionar versiones de Python por sí misma, junto con las dependencias de tu proyecto. + +**macOS / Linux** (terminal): + +```bash +curl -LsSf https://astral.sh/uv/install.sh | sh +``` + +**Windows** (PowerShell): + +```powershell +powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex" +``` + +Cierra y vuelve a abrir tu terminal, luego confirma que se instaló: + +```bash +uv --version +``` + +Si aún no tienes un intérprete de Python real instalado y gestionado por `uv` (de un proyecto anterior de esta serie), obtén uno ahora: + +```bash +uv python install 3.12 +``` + +### Obtén una clave de API de IA gratuita + +**Elige el proveedor que prefieras** — ninguno requiere tarjeta de crédito al momento de escribir esto, y este curso no favorece a uno sobre otro. + +| Proveedor | Dónde obtener una clave | Por qué podrías elegirlo | +|---|---|---| +| **GitHub Models** *(sugerido por defecto)* | [github.com/settings/tokens](https://github.com/settings/tokens) — un token de acceso personal con el alcance `models: read` | Sin registro por separado — ya tienes una cuenta de GitHub. Límites de nivel gratuito más generosos que los de Gemini. | +| Gemini | [Google AI Studio](https://aistudio.google.com/) | La opción más referenciada comúnmente. | +| Groq | [console.groq.com/keys](https://console.groq.com/keys) | Inferencia rápida, nivel gratuito generoso, sin tarjeta. | +| Mistral | [console.mistral.ai/api-keys](https://console.mistral.ai/api-keys) | Una de las cuotas gratuitas permanentes más generosas. | +| Cerebras | [cloud.cerebras.ai](https://cloud.cerebras.ai/) | Alto volumen de tokens diarios, sin tarjeta. | +| OpenRouter | [openrouter.ai/keys](https://openrouter.ai/keys) | Una API, muchos modelos gratuitos — bueno para comparar proveedores. | + +Sea cual elijas, el proceso es el mismo: inicia sesión, genera una clave en el sitio de ese proveedor, y **nunca la pegues directamente en el código ni la confirmes en un repositorio**. Este proyecto la mantiene en un archivo `.env` (abajo) en su lugar. + +### Configura el proyecto con `uv` + +```bash +uv init recipe-planner-agent +cd recipe-planner-agent +uv add deepagents langchain-openai python-dotenv +``` + +`uv init` crea un pequeño proyecto (un `pyproject.toml` que rastrea tus dependencias) y `uv add` instala paquetes en un entorno aislado para ese proyecto automáticamente, sin configuración manual de entorno virtual. `deepagents` es el framework de LangChain para construir agentes con uso de herramientas incorporado — el mismo usado en el [proyecto de Agente de IA](/docs/projects/ai-agent); `langchain-openai` es el paquete de integración que usa este ejemplo para hablar con GitHub Models (su API es compatible con OpenAI, así que el paquete de integración de OpenAI funciona para él también — mira el consejo abajo si elegiste un proveedor distinto); `python-dotenv` te permite mantener tu clave de API en un archivo `.env` local. + +Si elegiste un proveedor distinto arriba, cambia `langchain-openai` por el paquete de ese proveedor — `langchain-google-genai` (Gemini), `langchain-groq` (Groq), o `langchain-mistralai` (Mistral). Cerebras y OpenRouter también son compatibles con OpenAI, así que usan `langchain-openai` también, solo con un `base_url` diferente. + +:::tip[Revisa la documentación actual — y el nombre del modelo] +Los frameworks de agentes avanzan rápido, y los nombres de los modelos también: se renombran y retiran en una escala de meses, no de años. Usa un ID de modelo explícito y versionado en lugar de un alias `-latest` — varios proveedores, incluyendo Google, han dejado de lado esos alias porque cambian silenciosamente a una nueva versión del modelo, lo que puede romper código que funciona sin aviso. Antes de ejecutar esto, revisa la página de precios/modelo actual de tu proveedor, y echa un vistazo al propio README de `deepagents` para su API actual. +::: + +### Crea tu archivo `.env` + +En tu carpeta de proyecto, crea un archivo llamado `.env` (nunca lo confirmes) con la clave del proveedor que elegiste: + +```bash +# .env +GITHUB_TOKEN=your-key-here +``` + +`python-dotenv` (instalado arriba) lee este archivo en `os.environ` al inicio de tu script, así tu código nunca tiene la clave escrita directamente en él. + +**✅ Lista de verificación** + + +`uv --version` imprime un número de versión. +Tienes una clave de API real de un proveedor, guardada en un archivo `.env` — no pegada en ningún archivo `.py`. +`uv add deepagents langchain-openai python-dotenv` (o el paquete de tu proveedor) se completó sin errores. + + +## Paso 1: Construye tu base de datos de recetas local + +Todo lo que el agente sugerirá alguna vez viene de esta única estructura de datos — una lista de Python simple de dicts, sin servidor de base de datos, sin API externa. Crea `recipes.py`: + +```python +# recipes.py +RECIPES = [ + { + "name": "Tomato Egg Stir-Fry", + "ingredients": ["eggs", "tomatoes", "garlic", "salt", "oil"], + "instructions": "Scramble the eggs, set aside. Saute garlic and chopped tomatoes " + "until soft, stir the eggs back in, season with salt.", + }, + { + "name": "Garlic Butter Pasta", + "ingredients": ["pasta", "butter", "garlic", "parmesan", "salt"], + "instructions": "Boil the pasta. Melt butter with minced garlic, toss the pasta " + "in it, top with grated parmesan and salt.", + }, + { + "name": "Classic Grilled Cheese", + "ingredients": ["bread", "cheese", "butter"], + "instructions": "Butter one side of each bread slice, add cheese between the " + "unbuttered sides, grill in a pan until golden on both sides.", + }, + { + "name": "Simple Fried Rice", + "ingredients": ["rice", "eggs", "soy sauce", "onion", "oil"], + "instructions": "Scramble the eggs and set aside. Fry chopped onion in oil, add " + "cooked rice, stir in soy sauce and the eggs.", + }, + { + "name": "Chickpea Salad", + "ingredients": ["chickpeas", "cucumber", "tomatoes", "olive oil", "lemon", "salt"], + "instructions": "Drain the chickpeas, dice the cucumber and tomatoes, toss " + "everything with olive oil, lemon juice, and salt.", + }, + # ... a real database keeps going. See examples/recipe-planner-agent/recipes.py + # in the course repo for the full 13-recipe version this lesson uses. +] +``` + +Cada receta es solo un dict con un `name`, una lista de `ingredients` (en minúsculas, sin cantidades — solo lo que se necesita), e `instructions` cortas. Esta es exactamente la misma forma que la lista de juguete `topics` del `search_course_topics` del proyecto de Agente de IA, solo más rica: una lista de registros estructurados sobre la que tu función de herramienta puede buscar. + +:::tip[Más grande es genuinamente mejor aquí] +Una base de datos de recetas con 3-4 entradas hará que tu agente parezca roto incluso cuando el código está bien — la mayoría de las listas de ingredientes que un estudiante escribe simplemente no se cruzarán con nada. Apunta a las 10-15 recetas completas (la copia del repositorio tiene 13), cubriendo una mezcla real de proteínas, carbohidratos y verduras, para que una lista típica de "qué hay en mi refrigerador" tenga una oportunidad decente de coincidir con algo. +::: + +**✅ Lista de verificación** + + +`recipes.py` define `RECIPES` como una lista de al menos 10 dicts. +Cada receta tiene `name`, `ingredients` (una lista), e `instructions`. +Los nombres de ingredientes están en minúsculas y son consistentes entre recetas (p. ej. siempre `"tomatoes"`, nunca una mezcla de `"tomatoes"` y `"Tomato"`). + + +**🤔 Pregunta(s) socrática(s)** + +- ¿Por qué una lista de dicts en lugar de, digamos, un dict claveado por nombre de receta? ¿Qué ganarías o perderías con cualquiera de los dos? +- Si dos recetas comparten casi todos sus ingredientes, ¿cómo podría afectar eso a cuál tiende a sugerir primero el agente? + +## Paso 2: Escribe una herramienta con la que el agente pueda buscar recetas + +El agente no puede leer `recipes.py` directamente — solo puede ver lo que devuelve una función de herramienta, exactamente como `search_course_topics` en el proyecto de Agente de IA. Agrega esto a `recipes.py`, o a un archivo nuevo que importe `RECIPES`: + +```python +def search_recipes_by_ingredients(ingredients: list[str]) -> str: + """Search the local recipe database for recipes that best match the given ingredients. + + `ingredients` should be a list of ingredient names the caller already + has on hand (e.g. ["eggs", "tomatoes", "garlic"]). Returns the top + matching recipes, ranked by how many of their ingredients are already + covered, each with its full ingredient list and the ingredients still + missing -- so a shopping list can be built from the result without + guessing. Returns a plain "no matches" message if nothing overlaps at + all, so the caller never has to invent a recipe out of thin air. + """ + have = {i.strip().lower() for i in ingredients} + scored = [] + for recipe in RECIPES: + needed = {i.lower() for i in recipe["ingredients"]} + overlap = have & needed + if not overlap: + continue + missing = sorted(needed - have) + scored.append((len(overlap), recipe, missing)) + + if not scored: + return "No matching recipes found in the database for those ingredients." + + scored.sort(key=lambda row: row[0], reverse=True) + top = scored[:5] + + lines = [] + for _, recipe, missing in top: + missing_text = ", ".join(missing) if missing else "nothing -- you have it all!" + lines.append( + f"- {recipe['name']} | full ingredient list: {', '.join(recipe['ingredients'])} " + f"| missing: {missing_text}" + ) + return "Matching recipes (best match first):\n" + "\n".join(lines) +``` + +La idea central: `have & needed` (intersección de conjuntos) cuenta cuántos de los ingredientes de una receta ya tienes, `needed - have` (diferencia de conjuntos) es exactamente lo que aún falta. Ordenar por tamaño de superposición, del mayor al menor, significa que las recetas más cercanas a "listas para cocinar ahora mismo" vienen primero — y como la herramienta devuelve los ingredientes faltantes para *cada* candidato, no solo el mejor, el agente tiene todo lo que necesita para armar una lista de compras más tarde sin una segunda búsqueda. + +Nota que el tipo de retorno es una cadena simple, igual que `search_course_topics` y `count_words` en los proyectos anteriores — el modelo lee texto, no objetos de Python, así que una cadena claramente formateada es lo que una herramienta debería devolver. + +**✅ Lista de verificación** + + +`search_recipes_by_ingredients(["eggs", "tomatoes", "garlic"])` llamado directamente en Python (sin agente todavía) devuelve una cadena real y no vacía. +Llamarlo con ingredientes que no coinciden con nada en `RECIPES` devuelve el mensaje de "no matching recipes", no un error. +El docstring explica qué hace la función y qué devuelve — no un marcador de posición. + + +**🤔 Pregunta(s) socrática(s)** + +- ¿Por qué la herramienta devuelve los ingredientes faltantes para las 5 mejores coincidencias, no solo la mejor única? ¿Qué perdería el agente si solo obtuviera la mejor coincidencia? +- ¿Qué pasa ahora mismo si alguien pasa `["Tomatoes"]` (con mayúscula) — ¿todavía coincide con `"tomatoes"` en la base de datos? ¿Por qué? + +## Paso 3: Conecta la herramienta a un agente `deepagents` + +Crea `planner.py`: + +```python +import os + +from deepagents import create_deep_agent +from dotenv import load_dotenv +from langchain_openai import ChatOpenAI + +from recipes import RECIPES, search_recipes_by_ingredients + +load_dotenv() # reads .env into the environment, if present + +SYSTEM_PROMPT = """You are a helpful recipe-planning assistant. + +You have exactly one source of truth for what recipes exist: the +search_recipes_by_ingredients tool. Never invent, guess, or recall a recipe +from your own training data -- only suggest recipes that tool actually +returned in its results for this conversation. + +When a student lists what they have on hand: +1. Call search_recipes_by_ingredients with that ingredient list. +2. Suggest 2-3 recipes from the tool's results, explaining briefly why each + is a good fit (how much they already have). +3. If the tool returns no matches, say so plainly and suggest the student + try listing a few more ingredients -- do not make up a recipe to fill + the gap. +4. If asked to build a shopping list for a specific recipe, use the + "missing" ingredients the tool already reported for that recipe -- don't + recompute or guess at what's missing. +""" + +model = ChatOpenAI( + model="gpt-4o-mini", # confirm this still has a free tier before running -- see the tip above + api_key=os.environ["GITHUB_TOKEN"], + base_url="https://models.github.ai/inference", +) + +agent = create_deep_agent( + model=model, + tools=[search_recipes_by_ingredients], + system_prompt=SYSTEM_PROMPT, +) +``` + +Esta es la misma forma `create_deep_agent(model=..., tools=[...], system_prompt=...)` del proyecto de Agente de IA, con una herramienta en lugar de dos. Lo que es diferente, y vale la pena asimilar, es el **prompt de sistema**: no solo describe la herramienta, prohíbe explícitamente el modo de fallo que este proyecto entero está diseñado para demostrar — sugerir una receta que la herramienta nunca devolvió. Que una herramienta esté *disponible* no garantiza que el modelo siempre la use; el prompt de sistema es donde le dices que usar la herramienta, y solo la herramienta, no es opcional aquí. + +**✅ Lista de verificación** + + +`planner.py` importa `RECIPES` y `search_recipes_by_ingredients` de `recipes.py` sin errores. +`agent = create_deep_agent(...)` se ejecuta sin lanzar — esto solo construye el agente, aún no llama al modelo. +El prompt de sistema dice explícitamente no sugerir una receta que la herramienta no devolvió. + + +**🤔 Pregunta(s) socrática(s)** + +- El prompt de sistema le dice al modelo qué hacer si la herramienta no devuelve coincidencias. ¿Qué crees que pasa si dejas esa instrucción por completo — de dónde podría venir la respuesta del modelo en su lugar? +- ¿Por qué pasar `tools=[search_recipes_by_ingredients]` (la función misma) en lugar de, digamos, `tools=[RECIPES]` (los datos crudos)? ¿Qué podría hacer el modelo realmente con una lista cruda de dicts como "herramienta"? + +## Paso 4: Pide sugerencias de comidas + +Agrega un bloque de ejecución al final de `planner.py`: + +```python +if __name__ == "__main__": + on_hand = "I have eggs, tomatoes, garlic, bread, and cheese. What can I make?" + print("🧑 You:", on_hand) + result = agent.invoke({"messages": [{"role": "user", "content": on_hand}]}) + print("🤖 Agent:", result["messages"][-1].content) +``` + +Ejecútalo: + +```bash +uv run python planner.py +``` + +Deberías ver la respuesta final del agente: 2-3 nombres de recetas reales sacados directamente de `RECIPES`, cada uno con una razón corta de por qué encaja con tus ingredientes. Si tienes curiosidad sobre *cómo* llegó ahí — qué llamada de herramienta ocurrió, con qué argumentos, y qué devolvió la herramienta antes de que el modelo escribiera su respuesta — imprime la lista completa `result["messages"]` en lugar de solo la última, la misma técnica cubierta en la sección "Entender la traza interna completa" del proyecto de Agente de IA: un `HumanMessage` (tu pregunta), un `AIMessage` solicitando la llamada a la herramienta, un `ToolMessage` con la cadena real que devolvió `search_recipes_by_ingredients`, y luego un `AIMessage` final con la respuesta. + +**✅ Lista de verificación** + + +Ejecutar `uv run python planner.py` imprime una respuesta real, no un traceback. +Cada nombre de receta en la respuesta aparece de hecho en `RECIPES` — verifica a simple vista, o buscando en `recipes.py`. +Probaste al menos una lista de ingredientes que coincide mal, y el agente la manejó razonablemente (lo dijo, o sugirió opciones vagamente relacionadas) en lugar de inventar algo. + + +**🤔 Pregunta(s) socrática(s)** + +- Si cambias `on_hand` a ingredientes que no se cruzan con nada en tu base de datos, ¿qué dice el agente? ¿Sigue la instrucción del prompt de sistema, o se desliza de vuelta a adivinar? +- La herramienta devuelve sus 5 mejores coincidencias, pero el prompt de sistema pide 2-3 sugerencias. ¿Dónde ocurre ese estrechamiento — en tu código de Python, o dentro del razonamiento del modelo? + +## Paso 5: Arma una lista de compras y ejecútalo de extremo a extremo + +Como `search_recipes_by_ingredients` ya calculó los ingredientes faltantes para cada receta candidata, obtener una lista de compras es solo una pregunta de seguimiento en la misma conversación — no se necesita una herramienta nueva. Extiende el bloque de ejecución para continuar la conversación en lugar de comenzar una nueva cada vez: + +```python +if __name__ == "__main__": + conversation = [] + + on_hand = "I have eggs, tomatoes, garlic, bread, and cheese. What can I make?" + print("🧑 You:", on_hand) + conversation.append({"role": "user", "content": on_hand}) + result = agent.invoke({"messages": conversation}) + conversation = result["messages"] # carry the full history forward + print("🤖 Agent:", conversation[-1].content) + + print() + follow_up = "Great, let's go with the first one -- what's my shopping list?" + print("🧑 You:", follow_up) + conversation.append({"role": "user", "content": follow_up}) + result = agent.invoke({"messages": conversation}) + conversation = result["messages"] + print("🤖 Agent:", conversation[-1].content) +``` + +`conversation = result["messages"]` es la línea importante: cada llamada a `agent.invoke(...)` no tiene estado por sí sola, así que la *única* manera de que la segunda pregunta sepa a qué se refiere "el primero" es si devuelves todo el historial de mensajes — incluyendo la respuesta anterior del propio modelo y cualquier llamada a herramienta que haya hecho — como parte de la entrada de la siguiente llamada. Elimina esa línea y vuelve a ejecutar: la segunda pregunta no podrá resolver "el primero" a nada, porque para esa llamada, nunca existió un primer mensaje. + +Ejecútalo de nuevo con `uv run python planner.py` y deberías ver un intercambio completo y real: una sugerencia, luego una lista de compras armada a partir de los ingredientes "missing" exactos que la herramienta reportó para la receta que elegiste — no una suposición nueva. + +:::tip[Prueba una lista de ingredientes deliberadamente escasa] +Ejecútalo de nuevo con solo uno o dos ingredientes, algo como `"I have onions and salt. What can I make?"` Esta es la mejor manera de ver realmente el mecanismo de protección de tu prompt de sistema en acción: con casi nada que coincidir, obtendrás sugerencias honestas de "no es gran coincidencia, pero aquí está la opción más cercana", o (si el cruce es demasiado fino) el mensaje de "no matches" de la herramienta pasando directamente — de cualquier manera, observa si el agente aún se resiste a inventar algo que no esté en `RECIPES`. +::: + +**✅ Lista de verificación** + + +La segunda pregunta de la conversación se refiere correctamente de vuelta a "el primero" de la respuesta anterior. +La lista de compras que produce coincide con los ingredientes "missing" que la herramienta reportó para esa receta — no una lista diferente o inventada. +Ejecutaste la prueba de ingredientes escasos de arriba y el agente no inventó una receta que no esté presente en `RECIPES`. + + +**🤔 Pregunta(s) socrática(s)** + +- ¿Qué se rompería de la pregunta de seguimiento si comenzaras una `conversation = []` completamente nueva para ella en lugar de reutilizar la de la primera pregunta? +- El paso de la lista de compras no llama ninguna herramienta nueva — reutiliza datos que la primera llamada a la herramienta ya devolvió. ¿Qué sugiere eso sobre diseñar el valor de retorno de una herramienta pensando en más que solo la pregunta inmediata? + +## ⚠️ Errores comunes + +- **Una base de datos de recetas demasiado pequeña.** Con solo un puñado de recetas, la mayoría de las listas de ingredientes que un estudiante escribe no se cruzarán con nada, y el agente parecerá roto incluso cuando el código sea correcto. Apunta a las 10-15 recetas completas cubriendo una variedad real. +- **Nombres de ingredientes que no coinciden.** `"tomato"` en tu lista escrita no coincidirá con `"tomatoes"` en la base de datos con esta herramienta simple basada en conjuntos — no hay coincidencia difusa aquí. Mantén los nombres de ingredientes consistentes (siempre en plural, siempre en minúsculas) tanto en la base de datos como en lo que le pides al agente, o extiende la herramienta con normalización básica (p. ej. eliminar una `"s"` final) si quieres ir más allá. +- **El agente inventando una receta cuando la herramienta no devuelve nada.** Este es exactamente el modo de fallo que el prompt de sistema del Paso 3 existe para prevenir. Si omites esa instrucción, o la redactas demasiado vagamente, un modelo capaz a menudo "ayudará" sugiriendo algo que suena plausible en lugar de admitir que no tiene nada — prueba específicamente el caso de ingredientes escasos del consejo de arriba para detectar esto. +- **Perder el historial de conversación entre preguntas.** Si una pregunta de seguimiento como "cuál es la lista de compras del primero" obtiene una respuesta confusa o genérica, verifica que estás pasando la lista `conversation` acumulada (Paso 5) a `agent.invoke(...)`, no solo el mensaje más nuevo por sí solo. + +## Lo que acabas de construir + +Un agente que responde una pregunta genuinamente abierta — "¿qué puedo hacer?" — anclando cada parte de su respuesta en datos locales reales y estructurados en lugar de su propio conocimiento de entrenamiento, y que se niega a llenar vacíos con detalles inventados cuando los datos no respaldan uno. Ese patrón de anclaje (una herramienta respaldada por datos reales, un prompt de sistema que prohíbe responder fuera de ella) es la misma forma detrás de sistemas mucho más serios que necesitan que una IA se mantenga factual: un bot de soporte restringido a documentación real, un asistente de codificación restringido a una base de código real, una herramienta de investigación restringida a fuentes recuperadas reales. Acabas de construir la versión más pequeña de esa idea, con recetas. + +## A dónde ir desde aquí + +- Haz crecer `recipes.py` muy por encima de las 13 entradas, o cárgalo de un archivo JSON o CSV real en lugar de una lista de Python codificada — la función de herramienta apenas tiene que cambiar. +- Agrega una segunda herramienta, p. ej. `get_recipe_instructions(name: str) -> str`, para que el agente pueda guiar a un estudiante a cocinar la receta que acaba de sugerir, no solo nombrarla. +- Mejora la coincidencia en `search_recipes_by_ingredients` — maneja plurales simples, ignora básicos comunes de despensa como sal y aceite al puntuar el cruce (la mayoría de las cocinas ya los tienen), o deja que el estudiante diga qué *no* quiere explícitamente. +- Revisita la sección sobre **sub-agentes** del proyecto de Agente de IA — podrías dividir esto en un sub-agente "buscador de recetas" y un sub-agente "lista de compras", cada uno con un trabajo más acotado. + +## Comparte tu proyecto con la clase + +¿Construiste algo de lo que estás orgulloso? [`examples/student-projects/`](https://github.com/abderrahim-lectures/python-data-analysis-course/tree/main/examples/student-projects) es una galería de proyectos que otros estudiantes han enviado — y su README tiene un recorrido completo y amigable para principiantes sobre cómo agregar el tuyo vía una **pull request**, incluso si nunca has usado git antes: hacer fork del repositorio, crear una rama, confirmar tus archivos, y abrir el PR, un paso a la vez. No se asume experiencia previa con git. + +Bienvenido a escribir Python fuera del navegador. 🎓 + + diff --git a/i18n/fr/code.json b/i18n/fr/code.json index c1941ce..f3954c6 100644 --- a/i18n/fr/code.json +++ b/i18n/fr/code.json @@ -917,5 +917,13 @@ "homepage.projects.rateLimitedApi.summary": { "message": "Construis un vrai service FastAPI qui enveloppe ton propre jeu de données, avec une authentification par clé API authentique et un limiteur de débit à fenêtre glissante que tu construis de zéro.", "description": "Homepage project card summary" + }, + "homepage.projects.recipePlannerAgent.title": { + "message": "Construire un Agent Planificateur de Recettes", + "description": "Homepage project card title" + }, + "homepage.projects.recipePlannerAgent.summary": { + "message": "Construis un agent IA qui utilise des outils et suggère des repas à partir des ingrédients que tu as sous la main, ancré dans une vraie base de données de recettes locale plutôt que de deviner.", + "description": "Homepage project card summary" } } diff --git a/i18n/fr/docusaurus-plugin-content-docs/current/projects/index.mdx b/i18n/fr/docusaurus-plugin-content-docs/current/projects/index.mdx index 1f45a14..ec3d390 100644 --- a/i18n/fr/docusaurus-plugin-content-docs/current/projects/index.mdx +++ b/i18n/fr/docusaurus-plugin-content-docs/current/projects/index.mdx @@ -155,5 +155,11 @@ Ils sont optionnels et non notés. Parcourez-les à tout moment — l'introducti summary: "Construis un vrai service FastAPI qui enveloppe ton propre jeu de données, avec une authentification par clé API authentique et un limiteur de débit à fenêtre glissante que tu construis de zéro.", }, + { + id: 'recipe-planner-agent', + title: 'Construire un Agent Planificateur de Recettes', + summary: + "Construis un agent IA qui utilise des outils avec les deepagents de LangChain, qui suggère des repas à partir des ingrédients que tu as sous la main, ancré dans une vraie base de données de recettes locale.", + }, ])} /> diff --git a/i18n/fr/docusaurus-plugin-content-docs/current/projects/recipe-planner-agent/_category_.json b/i18n/fr/docusaurus-plugin-content-docs/current/projects/recipe-planner-agent/_category_.json new file mode 100644 index 0000000..9797dc9 --- /dev/null +++ b/i18n/fr/docusaurus-plugin-content-docs/current/projects/recipe-planner-agent/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Agent Planificateur de Recettes", + "position": 14 +} diff --git a/i18n/fr/docusaurus-plugin-content-docs/current/projects/recipe-planner-agent/index.md b/i18n/fr/docusaurus-plugin-content-docs/current/projects/recipe-planner-agent/index.md new file mode 100644 index 0000000..e8a4432 --- /dev/null +++ b/i18n/fr/docusaurus-plugin-content-docs/current/projects/recipe-planner-agent/index.md @@ -0,0 +1,412 @@ +--- +id: recipe-planner-agent +title: "Construire un Agent Planificateur de Recettes" +sidebar_label: "Agent Planificateur de Recettes" +slug: /projects/recipe-planner-agent +description: "Passe du playground dans le navigateur au vrai Python : construis un agent IA qui utilise des outils avec les deepagents de LangChain, qui suggère des repas à partir des ingrédients que tu as sous la main, ancré dans une vraie base de données de recettes locale." +--- + +import ProjectProgressCheckbox from '@site/src/components/ProjectProgressCheckbox'; +import ProjectPublishedDate from '@site/src/components/ProjectPublishedDate'; +import ProjectGreeting from '@site/src/components/ProjectGreeting'; +import {StepChecklist, StepChecklistItem} from '@site/src/components/StepChecklist'; + +# 🌍 Construire un Agent Planificateur de Recettes + + + + + +Tu tapes une liste d'ingrédients que tu as réellement sous la main — disons, des œufs, des tomates, de l'ail et du pain — et un agent suggère 2-3 vrais repas que tu pourrais préparer avec, puis construit une liste de courses pour tout ce qui manque pour le meilleur. Le twist qui en fait un agent authentiquement utile, pas juste un chatbot : il n'invente jamais de recette. Il appelle un outil qui cherche dans une vraie base de données de recettes locale et ne peut suggérer que ce que cet outil retourne réellement — la même idée d'ancrage derrière des systèmes bien plus sérieux de « ne laisse pas le modèle inventer des choses », réduite à quelque chose que tu peux construire en un après-midi. + +Cela suppose du Python de niveau Python 101. Avoir fait le [projet Agent IA](/docs/projects/ai-agent) d'abord est une vraie aide, pas une exigence stricte — ce projet réutilise le même framework `deepagents` et le même motif d'appel d'outils, juste avec un outil plus structuré et façonné pour le monde réel. C'est optionnel et non noté ; voir [Projets du monde réel](/docs/projects) pour la liste complète et croissante. + +## 🎯 Ce que tu vas faire + +1. Installer `uv`, obtenir une clé API IA de niveau gratuit, et configurer un petit projet avec `deepagents` — tout d'abord, dans Configuration ci-dessous. +2. Définir une petite « base de données de recettes » locale — une simple liste Python de dicts, 10-15 recettes, chacune avec sa propre liste d'ingrédients. +3. Écrire une fonction d'outil que l'agent peut appeler pour chercher dans cette base de données par les ingrédients que tu as sous la main. +4. Connecter cet outil à un agent `deepagents` avec un prompt système qui le garde ancré uniquement dans de vraies recettes. +5. Demander à l'agent des suggestions de repas à partir d'une vraie liste d'ingrédients, puis lui faire construire une liste de courses pour celui que tu choisis. + +## Où exécuter ceci + +**En local avec `uv`** est le chemin principal et recommandé — du vrai Python installé sur ta propre machine, le même mouvement « gradue vers du vrai Python » que tous les autres projets de cette section. Les étapes 1 et suivantes supposent ce chemin. + +**GitHub Codespaces** fonctionne tout aussi bien : ouvre [tout le dépôt du cours dans un Codespace gratuit](https://codespaces.new/abderrahim-lectures/python-data-analysis-course) (Node, Python et `uv` sont déjà installés, selon le `.devcontainer/devcontainer.json` du dépôt) et exécute exactement les mêmes commandes `uv` depuis un terminal dans ton onglet de navigateur. + +**Google Colab, Kaggle Notebooks, ou Binder** conviennent aussi — c'est un script léger qui appelle juste une API, pas de GPU ni d'installation lourde. Une version notebook prête à exécuter de ce projet ([`examples/recipe-planner-agent/notebook.ipynb`](https://github.com/abderrahim-lectures/python-data-analysis-course/blob/main/examples/recipe-planner-agent/notebook.ipynb)) est à un clic : + +[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/abderrahim-lectures/python-data-analysis-course/blob/main/examples/recipe-planner-agent/notebook.ipynb) +[![Open In Kaggle](https://kaggle.com/static/images/open-in-kaggle.svg)](https://kaggle.com/kernels/welcome?src=https://github.com/abderrahim-lectures/python-data-analysis-course/blob/main/examples/recipe-planner-agent/notebook.ipynb) +[![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/abderrahim-lectures/python-data-analysis-course/main?filepath=examples%2Frecipe-planner-agent%2Fnotebook.ipynb) + +C'est une façon de moindre fidélité de vivre le projet qu'un vrai projet `uv` local — pas de fichiers séparés, pas de vraie structure de projet — mais parfaitement faisable pour tester l'idée. Définis ta clé API avec `os.environ["GITHUB_TOKEN"] = "..."` dans la cellule getpass (ou utilise le panneau Secrets de Colab). + +## Configuration + +Tout ce qui est nécessaire avant que tu écrives une seule ligne de l'agent lui-même vit ici — installer `uv`, obtenir une clé API, créer le projet, et configurer ton fichier `.env`. Les étapes 1 et suivantes supposent que tout cela est déjà fait. + +### Installer `uv` + +`uv` est un seul outil qui remplace la chaîne habituelle « installe Python, puis installe pip, puis installe un outil d'environnement virtuel, puis installe les paquets » — il peut installer et gérer les versions de Python lui-même, en plus des dépendances de ton projet. + +**macOS / Linux** (terminal) : + +```bash +curl -LsSf https://astral.sh/uv/install.sh | sh +``` + +**Windows** (PowerShell) : + +```powershell +powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex" +``` + +Ferme et rouvre ton terminal, puis confirme que c'est installé : + +```bash +uv --version +``` + +Si tu n'as pas encore un vrai interpréteur Python installé et géré par `uv` (d'un projet précédent de cette série), procure-toi-en un maintenant : + +```bash +uv python install 3.12 +``` + +### Obtenir une clé API IA gratuite + +**Choisis le fournisseur que tu préfères** — aucun n'exige de carte de crédit au moment où ceci est écrit, et ce cours n'en favorise aucun. + +| Fournisseur | Où obtenir une clé | Pourquoi tu pourrais le choisir | +|---|---|---| +| **GitHub Models** *(suggéré par défaut)* | [github.com/settings/tokens](https://github.com/settings/tokens) — un jeton d'accès personnel avec le champ d'application `models: read` | Pas d'inscription séparée — tu as déjà un compte GitHub. Limites de niveau gratuit plus généreuses que celles de Gemini. | +| Gemini | [Google AI Studio](https://aistudio.google.com/) | L'option la plus couramment référencée. | +| Groq | [console.groq.com/keys](https://console.groq.com/keys) | Inférence rapide, niveau gratuit généreux, pas de carte. | +| Mistral | [console.mistral.ai/api-keys](https://console.mistral.ai/api-keys) | Un des quotas gratuits permanents les plus généreux. | +| Cerebras | [cloud.cerebras.ai](https://cloud.cerebras.ai/) | Volume quotidien de jetons élevé, pas de carte. | +| OpenRouter | [openrouter.ai/keys](https://openrouter.ai/keys) | Une API, de nombreux modèles gratuits — bon pour comparer les fournisseurs. | + +Quel que soit celui que tu choisis, le processus est le même : connecte-toi, génère une clé sur le site de ce fournisseur, et **ne la colle jamais directement dans le code ni ne la commit dans un dépôt**. Ce projet la garde dans un fichier `.env` (ci-dessous) à la place. + +### Configurer le projet avec `uv` + +```bash +uv init recipe-planner-agent +cd recipe-planner-agent +uv add deepagents langchain-openai python-dotenv +``` + +`uv init` crée un petit projet (un `pyproject.toml` qui suit tes dépendances) et `uv add` installe les paquets dans un environnement isolé pour ce projet automatiquement, sans configuration manuelle d'environnement virtuel. `deepagents` est le framework de LangChain pour construire des agents avec utilisation d'outils intégrée — le même que celui utilisé dans le [projet Agent IA](/docs/projects/ai-agent) ; `langchain-openai` est le paquet d'intégration que cet exemple utilise pour parler à GitHub Models (son API est compatible OpenAI, donc le paquet d'intégration OpenAI fonctionne aussi pour lui — voir l'astuce ci-dessous si tu as choisi un fournisseur différent) ; `python-dotenv` te permet de garder ta clé API dans un fichier `.env` local. + +Si tu as choisi un fournisseur différent ci-dessus, remplace `langchain-openai` par le paquet de ce fournisseur — `langchain-google-genai` (Gemini), `langchain-groq` (Groq), ou `langchain-mistralai` (Mistral). Cerebras et OpenRouter sont aussi compatibles OpenAI, donc ils utilisent `langchain-openai` également, juste avec un `base_url` différent. + +:::tip[Consulte la documentation actuelle — et le nom du modèle] +Les frameworks d'agents évoluent vite, et les noms de modèles aussi : ils sont renommés et retirés sur une échelle de mois, pas d'années. Utilise un ID de modèle explicite et versionné plutôt qu'un alias `-latest` — plusieurs fournisseurs, dont Google, ont déprécié ces alias parce qu'ils permutent silencieusement vers une nouvelle version du modèle, ce qui peut casser du code qui fonctionne sans avertissement. Avant d'exécuter ceci, vérifie la page de prix/modèle actuelle de ton fournisseur, et parcours le README de `deepagents` lui-même pour son API actuelle. +::: + +### Créer ton fichier `.env` + +Dans ton dossier de projet, crée un fichier nommé `.env` (ne le commit jamais) avec la clé du fournisseur que tu as choisi : + +```bash +# .env +GITHUB_TOKEN=your-key-here +``` + +`python-dotenv` (installé ci-dessus) lit ce fichier dans `os.environ` en haut de ton script, donc ton code n'a jamais la clé tapée directement dedans. + +**✅ Liste de vérification** + + +`uv --version` affiche un numéro de version. +Tu as une vraie clé API d'un fournisseur, et elle est enregistrée dans un fichier `.env` — pas collée dans un fichier `.py`. +`uv add deepagents langchain-openai python-dotenv` (ou le paquet de ton fournisseur) s'est terminé sans erreur. + + +## Étape 1 : Construis ta base de données de recettes locale + +Tout ce que l'agent suggérera jamais vient de cette seule structure de données — une simple liste Python de dicts, pas de serveur de base de données, pas d'API externe. Crée `recipes.py` : + +```python +# recipes.py +RECIPES = [ + { + "name": "Tomato Egg Stir-Fry", + "ingredients": ["eggs", "tomatoes", "garlic", "salt", "oil"], + "instructions": "Scramble the eggs, set aside. Saute garlic and chopped tomatoes " + "until soft, stir the eggs back in, season with salt.", + }, + { + "name": "Garlic Butter Pasta", + "ingredients": ["pasta", "butter", "garlic", "parmesan", "salt"], + "instructions": "Boil the pasta. Melt butter with minced garlic, toss the pasta " + "in it, top with grated parmesan and salt.", + }, + { + "name": "Classic Grilled Cheese", + "ingredients": ["bread", "cheese", "butter"], + "instructions": "Butter one side of each bread slice, add cheese between the " + "unbuttered sides, grill in a pan until golden on both sides.", + }, + { + "name": "Simple Fried Rice", + "ingredients": ["rice", "eggs", "soy sauce", "onion", "oil"], + "instructions": "Scramble the eggs and set aside. Fry chopped onion in oil, add " + "cooked rice, stir in soy sauce and the eggs.", + }, + { + "name": "Chickpea Salad", + "ingredients": ["chickpeas", "cucumber", "tomatoes", "olive oil", "lemon", "salt"], + "instructions": "Drain the chickpeas, dice the cucumber and tomatoes, toss " + "everything with olive oil, lemon juice, and salt.", + }, + # ... a real database keeps going. See examples/recipe-planner-agent/recipes.py + # in the course repo for the full 13-recipe version this lesson uses. +] +``` + +Chaque recette est juste un dict avec un `name`, une liste d'`ingredients` (en minuscules, sans quantités — juste ce qui est nécessaire), et de courtes `instructions`. C'est exactement la même forme que la liste jouet `topics` du `search_course_topics` du projet Agent IA, juste plus riche : une liste d'enregistrements structurés sur laquelle ta fonction d'outil peut chercher. + +:::tip[Plus c'est grand, mieux c'est ici] +Une base de données de recettes avec 3-4 entrées donnera l'impression que ton agent est cassé même quand le code est bon — la plupart des listes d'ingrédients qu'un élève tape ne chevaucheront simplement rien. Vise les 10-15 recettes complètes (la copie du dépôt en a 13), couvrant un vrai mélange de protéines, de glucides et de légumes, pour qu'une liste typique de « qu'est-ce qu'il y a dans mon frigo » ait une chance décente de correspondre à quelque chose. +::: + +**✅ Liste de vérification** + + +`recipes.py` définit `RECIPES` comme une liste d'au moins 10 dicts. +Chaque recette a un `name`, des `ingredients` (une liste), et des `instructions`. +Les noms d'ingrédients sont en minuscules et cohérents entre les recettes (ex. toujours `"tomatoes"`, jamais un mélange de `"tomatoes"` et `"Tomato"`). + + +**🤔 Question(s) socratique(s)** + +- Pourquoi une liste de dicts plutôt que, disons, un dict indexé par nom de recette ? Que gagnerais-tu ou perdrais-tu dans les deux cas ? +- Si deux recettes partagent presque tous leurs ingrédients, comment cela pourrait-il affecter celle que l'agent tend à suggérer en premier ? + +## Étape 2 : Écris un outil avec lequel l'agent peut chercher des recettes + +L'agent n'a pas le droit de lire `recipes.py` directement — il ne peut voir que ce qu'une fonction d'outil retourne, exactement comme `search_course_topics` dans le projet Agent IA. Ajoute ceci à `recipes.py`, ou à un nouveau fichier qui importe `RECIPES` : + +```python +def search_recipes_by_ingredients(ingredients: list[str]) -> str: + """Search the local recipe database for recipes that best match the given ingredients. + + `ingredients` should be a list of ingredient names the caller already + has on hand (e.g. ["eggs", "tomatoes", "garlic"]). Returns the top + matching recipes, ranked by how many of their ingredients are already + covered, each with its full ingredient list and the ingredients still + missing -- so a shopping list can be built from the result without + guessing. Returns a plain "no matches" message if nothing overlaps at + all, so the caller never has to invent a recipe out of thin air. + """ + have = {i.strip().lower() for i in ingredients} + scored = [] + for recipe in RECIPES: + needed = {i.lower() for i in recipe["ingredients"]} + overlap = have & needed + if not overlap: + continue + missing = sorted(needed - have) + scored.append((len(overlap), recipe, missing)) + + if not scored: + return "No matching recipes found in the database for those ingredients." + + scored.sort(key=lambda row: row[0], reverse=True) + top = scored[:5] + + lines = [] + for _, recipe, missing in top: + missing_text = ", ".join(missing) if missing else "nothing -- you have it all!" + lines.append( + f"- {recipe['name']} | full ingredient list: {', '.join(recipe['ingredients'])} " + f"| missing: {missing_text}" + ) + return "Matching recipes (best match first):\n" + "\n".join(lines) +``` + +L'idée centrale : `have & needed` (intersection d'ensembles) compte combien d'ingrédients d'une recette tu as déjà, `needed - have` (différence d'ensembles) est exactement ce qui manque encore. Trier par taille de chevauchement, du plus grand au plus petit, signifie que les recettes les plus proches de « prêtes à cuisiner maintenant » viennent en premier — et parce que l'outil retourne les ingrédients manquants pour *chaque* candidat, pas seulement le meilleur, l'agent a tout ce qu'il faut pour construire une liste de courses plus tard sans une deuxième recherche. + +Note que le type de retour est une simple chaîne, comme `search_course_topics` et `count_words` dans les projets précédents — le modèle lit du texte, pas des objets Python, donc une chaîne clairement formatée est ce qu'un outil devrait renvoyer. + +**✅ Liste de vérification** + + +`search_recipes_by_ingredients(["eggs", "tomatoes", "garlic"])` appelé directement en Python (pas encore d'agent) retourne une vraie chaîne non vide. +L'appeler avec des ingrédients qui ne correspondent à rien dans `RECIPES` retourne le message « no matching recipes », pas une erreur. +Le docstring explique ce que la fonction fait et ce qu'elle retourne — pas un espace réservé. + + +**🤔 Question(s) socratique(s)** + +- Pourquoi l'outil retourne-t-il les ingrédients manquants pour les 5 meilleures correspondances, pas seulement la meilleure unique ? Que perdrait l'agent s'il n'obtenait que la meilleure correspondance ? +- Que se passe-t-il en ce moment si quelqu'un passe `["Tomatoes"]` (avec une majuscule) — est-ce que ça correspond toujours à `"tomatoes"` dans la base de données ? Pourquoi ? + +## Étape 3 : Connecte l'outil à un agent `deepagents` + +Crée `planner.py` : + +```python +import os + +from deepagents import create_deep_agent +from dotenv import load_dotenv +from langchain_openai import ChatOpenAI + +from recipes import RECIPES, search_recipes_by_ingredients + +load_dotenv() # reads .env into the environment, if present + +SYSTEM_PROMPT = """You are a helpful recipe-planning assistant. + +You have exactly one source of truth for what recipes exist: the +search_recipes_by_ingredients tool. Never invent, guess, or recall a recipe +from your own training data -- only suggest recipes that tool actually +returned in its results for this conversation. + +When a student lists what they have on hand: +1. Call search_recipes_by_ingredients with that ingredient list. +2. Suggest 2-3 recipes from the tool's results, explaining briefly why each + is a good fit (how much they already have). +3. If the tool returns no matches, say so plainly and suggest the student + try listing a few more ingredients -- do not make up a recipe to fill + the gap. +4. If asked to build a shopping list for a specific recipe, use the + "missing" ingredients the tool already reported for that recipe -- don't + recompute or guess at what's missing. +""" + +model = ChatOpenAI( + model="gpt-4o-mini", # confirm this still has a free tier before running -- see the tip above + api_key=os.environ["GITHUB_TOKEN"], + base_url="https://models.github.ai/inference", +) + +agent = create_deep_agent( + model=model, + tools=[search_recipes_by_ingredients], + system_prompt=SYSTEM_PROMPT, +) +``` + +C'est la même forme `create_deep_agent(model=..., tools=[...], system_prompt=...)` du projet Agent IA, avec un outil au lieu de deux. Ce qui est différent, et mérite qu'on s'y attarde, c'est le **prompt système** : il ne décrit pas juste l'outil, il interdit explicitement le mode d'échec que tout ce projet est conçu pour démontrer — suggérer une recette que l'outil n'a jamais retournée. Qu'un outil soit *disponible* ne garantit pas que le modèle l'utilise toujours ; c'est dans le prompt système que tu lui dis qu'utiliser l'outil, et seulement l'outil, n'est pas optionnel ici. + +**✅ Liste de vérification** + + +`planner.py` importe `RECIPES` et `search_recipes_by_ingredients` depuis `recipes.py` sans erreur. +`agent = create_deep_agent(...)` s'exécute sans lever d'exception — cela ne fait que construire l'agent, ça n'appelle pas encore le modèle. +Le prompt système dit explicitement de ne pas suggérer une recette que l'outil n'a pas retournée. + + +**🤔 Question(s) socratique(s)** + +- Le prompt système dit au modèle quoi faire si l'outil ne retourne aucune correspondance. Que penses-tu qu'il se passe si tu omets cette instruction entièrement — d'où pourrait venir la réponse du modèle à la place ? +- Pourquoi passer `tools=[search_recipes_by_ingredients]` (la fonction elle-même) plutôt que, disons, `tools=[RECIPES]` (les données brutes) ? Que pourrait réellement faire le modèle avec une liste brute de dicts comme « outil » ? + +## Étape 4 : Demande des suggestions de repas + +Ajoute un bloc d'exécution en bas de `planner.py` : + +```python +if __name__ == "__main__": + on_hand = "I have eggs, tomatoes, garlic, bread, and cheese. What can I make?" + print("🧑 You:", on_hand) + result = agent.invoke({"messages": [{"role": "user", "content": on_hand}]}) + print("🤖 Agent:", result["messages"][-1].content) +``` + +Exécute-le : + +```bash +uv run python planner.py +``` + +Tu devrais voir la réponse finale de l'agent : 2-3 vrais noms de recettes tirés directement de `RECIPES`, chacun avec une courte raison de pourquoi il correspond à tes ingrédients. Si tu es curieux·se de *comment* il y est arrivé — quel appel d'outil s'est produit, avec quels arguments, et ce que l'outil a réellement retourné avant que le modèle écrive sa réponse — affiche la liste complète `result["messages"]` au lieu de juste la dernière, la même technique couverte dans la section « Comprendre la trace interne complète » du projet Agent IA : un `HumanMessage` (ta question), un `AIMessage` demandant l'appel d'outil, un `ToolMessage` avec la vraie chaîne retournée par `search_recipes_by_ingredients`, puis un `AIMessage` final avec la réponse. + +**✅ Liste de vérification** + + +Exécuter `uv run python planner.py` affiche une vraie réponse, pas un traceback. +Chaque nom de recette dans la réponse apparaît réellement dans `RECIPES` — vérifie à l'œil, ou en cherchant dans `recipes.py`. +Tu as essayé au moins une liste d'ingrédients qui correspond mal, et l'agent l'a gérée raisonnablement (il l'a dit, ou a suggéré des options vaguement liées) au lieu d'inventer quelque chose. + + +**🤔 Question(s) socratique(s)** + +- Si tu changes `on_hand` en ingrédients qui ne chevauchent rien dans ta base de données, que dit l'agent ? Suit-il l'instruction du prompt système, ou retombe-t-il dans la supposition ? +- L'outil retourne ses 5 meilleures correspondances, mais le prompt système demande 2-3 suggestions. Où ce resserrement se produit-il — dans ton code Python, ou dans le raisonnement du modèle ? + +## Étape 5 : Construis une liste de courses et exécute-le de bout en bout + +Parce que `search_recipes_by_ingredients` a déjà calculé les ingrédients manquants pour chaque recette candidate, obtenir une liste de courses n'est qu'une question de suivi dans la même conversation — aucun nouvel outil nécessaire. Étends le bloc d'exécution pour continuer la conversation au lieu d'en commencer une nouvelle à chaque fois : + +```python +if __name__ == "__main__": + conversation = [] + + on_hand = "I have eggs, tomatoes, garlic, bread, and cheese. What can I make?" + print("🧑 You:", on_hand) + conversation.append({"role": "user", "content": on_hand}) + result = agent.invoke({"messages": conversation}) + conversation = result["messages"] # carry the full history forward + print("🤖 Agent:", conversation[-1].content) + + print() + follow_up = "Great, let's go with the first one -- what's my shopping list?" + print("🧑 You:", follow_up) + conversation.append({"role": "user", "content": follow_up}) + result = agent.invoke({"messages": conversation}) + conversation = result["messages"] + print("🤖 Agent:", conversation[-1].content) +``` + +`conversation = result["messages"]` est la ligne importante : chaque appel `agent.invoke(...)` est sans état en soi, donc la *seule* façon pour la deuxième question de savoir à quoi « le premier » se réfère est si tu lui rends tout l'historique de messages — y compris la réponse précédente du modèle lui-même et tous les appels d'outil qu'il a faits — comme partie de l'entrée de l'appel suivant. Supprime cette ligne et relance : la deuxième question ne pourra résoudre « le premier » à rien, parce qu'en ce qui concerne cet appel, aucun premier message n'a jamais existé. + +Relance-le avec `uv run python planner.py` et tu devrais voir un échange complet et réel : une suggestion, puis une liste de courses construite à partir des ingrédients « missing » exacts que l'outil a rapportés pour la recette que tu as choisie — pas une nouvelle supposition. + +:::tip[Essaie une liste d'ingrédients délibérément parcimonieuse] +Relance-le avec seulement un ou deux ingrédients, quelque chose comme `"I have onions and salt. What can I make?"` C'est la meilleure façon de voir réellement le garde-fou de ton prompt système agir : avec presque rien à faire correspondre, tu obtiendras soit des suggestions honnêtes de « pas vraiment une correspondance, mais voici l'option la plus proche », soit (si le chevauchement est trop mince) le message « no matches » de l'outil passé directement — dans les deux cas, observe si l'agent résiste encore à inventer quelque chose qui n'est pas dans `RECIPES`. +::: + +**✅ Liste de vérification** + + +La deuxième question de la conversation se réfère correctement de retour à « le premier » de la réponse précédente. +La liste de courses qu'elle produit correspond aux ingrédients « missing » que l'outil a rapportés pour cette recette — pas une liste différente ou inventée. +Tu as exécuté le test d'ingrédients parcimonieux ci-dessus et l'agent n'a pas inventé une recette absente de `RECIPES`. + + +**🤔 Question(s) socratique(s)** + +- Qu'est-ce qui casserait dans la question de suivi si tu commençais une `conversation = []` toute neuve pour elle au lieu de réutiliser celle de la première question ? +- L'étape de la liste de courses n'appelle aucun nouvel outil — elle réutilise des données que le premier appel d'outil a déjà retournées. Qu'est-ce que cela suggère sur la conception de la valeur de retour d'un outil en pensant à plus que la seule question immédiate ? + +## ⚠️ Pièges courants + +- **Une base de données de recettes trop petite.** Avec seulement une poignée de recettes, la plupart des listes d'ingrédients qu'un élève tape ne chevaucheront rien, et l'agent aura l'air cassé même quand le code est correct. Vise les 10-15 recettes complètes couvrant une vraie variété. +- **Des noms d'ingrédients qui ne correspondent pas.** `"tomato"` dans ta liste tapée ne correspondra pas à `"tomatoes"` dans la base de données avec cet outil simple basé sur des ensembles — il n'y a pas de correspondance floue ici. Garde les noms d'ingrédients cohérents (toujours au pluriel, toujours en minuscules) à la fois dans la base de données et dans ce que tu demandes à l'agent, ou étends l'outil avec une normalisation de base (ex. retirer un `"s"` final) si tu veux aller plus loin. +- **L'agent inventant une recette quand l'outil ne retourne rien.** C'est exactement le mode d'échec que le prompt système de l'étape 3 existe pour empêcher. Si tu sautes cette instruction, ou si tu la formules trop vaguement, un modèle capable « aidera » souvent en suggérant quelque chose qui semble plausible plutôt qu'en admettant qu'il n'a rien — teste spécifiquement le cas des ingrédients parcimonieux de l'astuce ci-dessus pour l'attraper. +- **Perdre l'historique de conversation entre les questions.** Si une question de suivi comme « c'est quoi la liste de courses pour le premier » obtient une réponse confuse ou générique, vérifie que tu passes la liste `conversation` accumulée (étape 5) à `agent.invoke(...)`, pas juste le message le plus récent tout seul. + +## Ce que tu viens de construire + +Un agent qui répond à une question authentiquement ouverte — « qu'est-ce que je peux faire ? » — en ancrant chaque partie de sa réponse dans de vraies données locales structurées plutôt que dans son propre savoir d'entraînement, et qui refuse de combler les vides avec des détails inventés quand les données n'en soutiennent pas un. Ce motif d'ancrage (un outil soutenu par de vraies données, un prompt système qui interdit de répondre en dehors de lui) est la même forme derrière des systèmes bien plus sérieux qui exigent qu'une IA reste factuelle : un bot d'assistance restreint à la vraie documentation, un assistant de codage restreint à une vraie base de code, un outil de recherche restreint à de vraies sources récupérées. Tu viens de construire la plus petite version de cette idée, avec des recettes. + +## Où aller à partir d'ici + +- Fais grandir `recipes.py` bien au-delà de 13 entrées, ou charge-le depuis un vrai fichier JSON ou CSV au lieu d'une liste Python codée en dur — la fonction d'outil n'a presque pas à changer. +- Ajoute un deuxième outil, ex. `get_recipe_instructions(name: str) -> str`, pour que l'agent puisse guider un élève dans la cuisine de la recette qu'il vient de suggérer, pas juste la nommer. +- Améliore la correspondance dans `search_recipes_by_ingredients` — gère les pluriels simples, ignore les basiques de garde-manger courants comme le sel et l'huile lors du score du chevauchement (la plupart des cuisines en ont déjà), ou laisse l'élève dire ce qu'il ne veut *pas* explicitement. +- Revisite la section sur les **sous-agents** du projet Agent IA — tu pourrais scinder ceci en un sous-agent « trouveur de recettes » et un sous-agent « liste de courses », chacun avec une tâche plus restreinte. + +## Partage ton projet avec la classe + +Tu as construit quelque chose dont tu es fier ? [`examples/student-projects/`](https://github.com/abderrahim-lectures/python-data-analysis-course/tree/main/examples/student-projects) est une galerie de projets soumis par d'autres élèves — et son README a un tutoriel complet et adapté aux débutants pour ajouter le tien via une **pull request**, même si tu n'as jamais utilisé git avant : forker le dépôt, créer une branche, commiter tes fichiers, et ouvrir la PR, une étape à la fois. Aucune expérience préalable avec git n'est supposée. + +Bienvenue dans l'écriture de Python hors du navigateur. 🎓 + +