# Installe pythonnet. À n'exécuter qu'une seule fois (enlevez --quiet si besoin)SK-9-Building-CLR : Interoperabilite Python/.NET via pythonnet
Navigation : Index | << 08-MCP | 10-NotebookMaker >>
Objectifs d’apprentissage
A la fin de ce notebook, vous saurez : 1. Installer et configurer pythonnet pour l’interop Python/.NET 2. Charger des DLL .NET compilees depuis Python 3. Acceder aux namespaces et classes C# depuis Python 4. Appeler des méthodes asynchrones .NET avec asyncio 5. Utiliser des agents Semantic Kernel C# depuis Python
Prerequis
- Python 3.10+
- .NET SDK 9.0+ installe
- DLL
MyIA.AI.Notebooks.dllcompilee (dotnet build) - Notebooks 01-08 completes (recommande)
Duree estimee : 40 minutes
Sommaire
| Section | Contenu | Concepts cles |
|---|---|---|
| 1 | Installation pythonnet | pip, CLR runtime |
| 2 | Chargement DLL | sys.path, clr.AddReference |
| 3 | Import classes C# | Assembly, GetType, namespaces |
| 4 | Exécution agents | asyncio, nest_asyncio |
Introduction
Ce notebook illustre comment, en Python, on peut :
- Installer et configurer
pythonnet - Charger la DLL .NET (compilee en C#) qui contient notre code d’agent
- Instancier et appeler ces classes depuis Python
Interpretation : Pourquoi l’interop Python/.NET ?
| Avantage | Description |
|---|---|
| Reutilisation | Exploiter du code C# existant sans le reecrire |
| Performance | Beneficier de la compilation JIT .NET |
| Ecosysteme | Acceder aux bibliotheques .NET (Semantic Kernel C#, ML.NET) |
| Hybride | Combiner notebooks Python interactifs avec agents C# |
Note technique :
pythonnetutilise le CLR (Common Language Runtime) pour executer du code .NET depuis Python.
Schema : pile d’interoperabilite Python -> .NET
Le diagramme montre l’imbrication des couches : Python (Jupyter) charge pythonnet, qui demarre le runtime CLR .NET, lequel expose l’assembly MyIA.AI.Notebooks.dll.
flowchart TD
subgraph PY["Python (Jupyter)"]
subgraph PN["pythonnet (clr)"]
subgraph CLR["CLR Runtime (.NET)"]
DLL["MyIA.AI.Notebooks.dll : AutoInvokeSKAgentsNotebookUpdater + Semantic Kernel C#"]
end
end
end
Bloc 1 : Installation pythonnet & configuration
# On peut vérifier la version
# importlib.metadata remplace pkg_resources (deprecie depuis setuptools 67) :
# import pkg_resources emet un DeprecationWarning dont la traceback fuite le chemin
# temporaire du kernel (ipykernel_<pid>\<hash>.py) dans les sorties commitees.
from importlib.metadata import version as _pkg_version
print("pythonnet version :", _pkg_version("pythonnet"))
print("Installation pythonnet : OK")pythonnet version : 3.0.5
Installation pythonnet : OK
Exercice 1 : Diagnostic de l’environnement CLR
Avant d’utiliser pythonnet en production, il est important de verifier la compatibilite des versions et la configuration du runtime.
Objectif : Implementer une fonction diagnose_clr_env() qui collecte les informations cles de l’environnement CLR.
Indices : - # Étape 1 : Verifier la version de pythonnet via import clr et le module importlib.metadata - # Étape 2 : Acceder au runtime .NET via System.Environment pour obtenir la version du framework - # Étape 3 : Lister les assemblies déjà chargees dans AppDomain.CurrentDomain - # Indice : System.Environment.Version donne la version du runtime .NET, System.Environment.OSVersion donne l’OS
def diagnose_clr_env() -> dict:
"""
TODO etudiant : Collecter les informations de l'environnement CLR.
Returns:
dict avec les cles: pythonnet_version, dotnet_version, os_version,
loaded_assembly_count, is_runtime_loaded (bool)
"""
# TODO etudiant : utiliser System.Environment et AppDomain.CurrentDomain
return None
# Test :
# diag = diagnose_clr_env()
# print(f"pythonnet {diag['pythonnet_version']}, .NET {diag['dotnet_version']}")
# print(f" Assemblies charges: {diag['loaded_assembly_count']}")
print("Exercice a completer")Exercice a completer
Bloc 2 : Importation de la DLL .NET
Nous allons :
1. Modifier le chemin Python (sys.path) pour inclure le dossier où se trouve la DLL .NET.
2. Charger la DLL (clr.AddReference)
3. Importer les namespaces/classes C# du namespace MyNotebookLib (l’assembly se nomme MyIA.AI.Notebooks.dll, mais les types exposés vivent dans MyNotebookLib).
Note : Adaptez dll_path si nécessaire.
import sys
import os
def _rel_dll(chemin):
# N'affiche que le suffixe relatif (ex. bin/Release/net9.0) afin de
# ne pas exposer le chemin absolu de la machine dans les sorties.
parts = chemin.replace("\\", "/").split("/")
return "/".join(parts[-3:]) if len(parts) >= 3 else os.path.basename(chemin)
# CORRECTION : Chemin absolu vers la DLL compilée
dll_path = os.path.abspath(os.path.join(os.getcwd(), "..", "..", "bin", "Release", "net9.0"))
print(f"Chemin DLL calculé : {_rel_dll(dll_path)}")
# Vérification de l'existence du chemin
if not os.path.exists(dll_path):
# Essayer le chemin Debug si Release n'existe pas
dll_path_debug = os.path.abspath(os.path.join(os.getcwd(), "..", "..", "bin", "Debug", "net9.0"))
if os.path.exists(dll_path_debug):
dll_path = dll_path_debug
print(f"Utilisation du chemin Debug : {_rel_dll(dll_path)}")
else:
print(f"❌ Erreur : Aucun chemin DLL trouvé!")
print(f" - Testé Release : {_rel_dll(dll_path)}")
print(f" - Testé Debug : {_rel_dll(dll_path_debug)}")
raise FileNotFoundError("DLL MyIA.AI.Notebooks.dll introuvable")
# Sélection du runtime .NET AVANT tout `import clr`.
# pythonnet 3.x boote par défaut le CLR du .NET Framework (vieux runtime) :
# incapable de résoudre les types ValueTuple d'un assembly net9.0, il lève une
# `TypeLoadException` au moindre appel à un constructeur modern (cf. leçon de la
# section 3). On force donc le **CoreCLR (.NET 9)** via le `runtimeconfig.json`
# produit par `dotnet build`.
# DOTNET_ROLL_FORWARD=Major : autorise un runtime 10.x à exécuter un assembly
# net9.0 si seul le runtime 10 est installé sur la machine.
import pythonnet
os.environ["DOTNET_ROLL_FORWARD"] = "Major"
_runtime_config = os.path.join(dll_path, "MyIA.AI.Notebooks.runtimeconfig.json")
pythonnet.load("coreclr", runtime_config=_runtime_config)
print(f"✅ Runtime CoreCLR démarré (config : {os.path.basename(_runtime_config)})")
import clr # doit venir APRÈS pythonnet.load
# On ajoute ce dossier dans sys.path
if dll_path not in sys.path:
sys.path.append(dll_path)
print(f"✅ Chemin ajouté au sys.path : {_rel_dll(dll_path)}")Chemin DLL calculé : bin/Release/net9.0
Utilisation du chemin Debug : bin/Debug/net9.0
✅ Runtime CoreCLR démarré (config : MyIA.AI.Notebooks.runtimeconfig.json)
✅ Chemin ajouté au sys.path : bin/Debug/net9.0
# On charge la DLL (sans l'extension .dll)
try:
clr.AddReference("MyIA.AI.Notebooks")
print("✅ Référence à 'MyIA.AI.Notebooks.dll' ajoutée avec succès.")
except Exception as e:
print(f"❌ Erreur lors du chargement de la DLL : {e}")
# Lister les fichiers disponibles pour debug
import os
files = [f for f in os.listdir(dll_path) if f.endswith('.dll')]
print(f"Fichiers .dll disponibles : {files}")
raise✅ Référence à 'MyIA.AI.Notebooks.dll' ajoutée avec succès.
Bloc 3 : Import des classes C# depuis l’assembly MyIA.AI.Notebooks
L’assembly se nomme MyIA.AI.Notebooks.dll, mais les classes C# qu’elle expose vivent dans le namespace MyNotebookLib (à vérifier dans la sortie de la cellule suivante via GetExportedTypes()) :
MyNotebookLib.AutoInvokeSKAgentsNotebookUpdater— l’updater d’agents Semantic KernelMyNotebookLib.AutoGenNotebookUpdater— updater AutoGenMyNotebookLib.DisplayLoggeretDisplayLoggerProvider— logger intégré au notebookMyNotebookLib.NotebookExecutor,NotebookPlannerUpdater,NotebookUpdaterBaseMyNotebookLib.GuessingGame— exemple de classe .NET callable depuis Python
Attention : le nom de l’assembly (
MyIA.AI.Notebooks) diffère du namespace C# (MyNotebookLib). L’import Python doit ciblerMyNotebookLib, pasMyIA.AI.Notebooks.
Ensuite, on pourra instancier et exécuter du code C# directement depuis Python.
# Debug des assemblies chargées
import clr
from System import AppDomain
for asm in AppDomain.CurrentDomain.GetAssemblies():
if asm.GetName().Name == "MyIA.AI.Notebooks":
print(f"\n✅ Assembly trouvé: {asm.GetName().Name}")
try:
for t in asm.GetExportedTypes():
print(f" -> {t.FullName}")
except Exception as e:
print(f" Erreur lors de l'énumération des types : {e}")
break
else:
print("❌ Assembly 'MyIA.AI.Notebooks' non trouvé")
✅ Assembly trouvé: MyIA.AI.Notebooks
-> SkiaUtils
-> FactorGraphHelper
-> SvgChart
-> TraceStyle
-> SvgSeries
-> SvgChartHelper
-> MyNotebookLib.AutoGenNotebookUpdater
-> MyNotebookLib.AutoInvokeSKAgentsNotebookUpdater
-> MyNotebookLib.DisplayLogger
-> MyNotebookLib.DisplayLoggerProvider
-> MyNotebookLib.GuessingGame
-> MyNotebookLib.NotebookExecutor
-> MyNotebookLib.NotebookPlannerUpdater
-> MyNotebookLib.NotebookUpdaterBase
-> MyNotebookLib.WorkbookInteractionBase
-> MyNotebookLib.WorkbookUpdateInteraction
-> MyNotebookLib.WorkbookValidation
-> MyIA.AI.Notebooks.Config.Settings
-> MyIA.AI.Notebooks.Config.Utils
# Import correct pour pythonnet
import clr
from System.Reflection import Assembly
from System import AppDomain
# 1. Récupérer l'assembly déjà chargé
assembly = None
for asm in AppDomain.CurrentDomain.GetAssemblies():
if asm.GetName().Name == "MyIA.AI.Notebooks":
assembly = asm
break
if assembly:
print(f"✅ Assembly trouvé: {assembly.GetName().Name}")
# 2. Lister tous les types pour debug (avec gestion d'erreur)
print("Types disponibles dans l'assembly:")
try:
for t in assembly.GetTypes():
print(f" - {t.FullName}")
except Exception as e:
print(f" ⚠️ Erreur lors de l'énumération des types: {e}")
print(f" ⚠️ Cela peut indiquer des dépendances .NET manquantes")
print(f" → Continuons avec la recherche de classes spécifiques...")
# 3. Le bon nom : le namespace C# est `MyNotebookLib`, PAS le nom de l'assembly
# (`MyIA.AI.Notebooks`). `Assembly.GetType(nom)` fait une correspondance exacte
# sur le nom complet du type : un seul segment erroné -> retourne None
# SANS lever d'exception (échec silencieux). Les variantes testées ci-dessous
# sont toutes fausses ; on les conserve commentées comme illustration du piège
# « assembly ≠ namespace » (cf. cellule d'interprétation suivante).
type_names = [
"MyNotebookLib.AutoInvokeSKAgentsNotebookUpdater",
# variantes historiquement testées, toutes fausses :
# "MyIA.AI.Notebooks.GenAI.SemanticKernel.AutoInvokeSKAgentsNotebookUpdater",
# "MyIA.AI.Notebooks.AutoInvokeSKAgentsNotebookUpdater",
# "AutoInvokeSKAgentsNotebookUpdater",
]
AutoInvokeSKAgentsNotebookUpdater = None
for type_name in type_names:
updater_type = assembly.GetType(type_name)
if updater_type:
AutoInvokeSKAgentsNotebookUpdater = updater_type
print(f"✅ Classe importée: {type_name}")
print(f" - Type: {AutoInvokeSKAgentsNotebookUpdater}")
print(f" - Nom complet: {AutoInvokeSKAgentsNotebookUpdater.FullName}")
break
if AutoInvokeSKAgentsNotebookUpdater is None:
print("❌ Aucun type trouvé avec les noms testés")
else:
print("❌ Assembly 'MyIA.AI.Notebooks' non trouvé")
# Debug: lister toutes les assemblies chargées
print("Assemblies chargées:")
for asm in AppDomain.CurrentDomain.GetAssemblies():
print(f" - {asm.GetName().Name}")
AutoInvokeSKAgentsNotebookUpdater = None
print("\n=== RÉSUMÉ IMPORT ===")
print(f"AutoInvokeSKAgentsNotebookUpdater = {AutoInvokeSKAgentsNotebookUpdater}")
print(f"Type: {type(AutoInvokeSKAgentsNotebookUpdater)}")
if AutoInvokeSKAgentsNotebookUpdater is not None:
print("🎉 IMPORT .NET RÉUSSI !")
else:
print("❌ Import .NET échoué - variable = None")✅ Assembly trouvé: MyIA.AI.Notebooks
Types disponibles dans l'assembly:
- SkiaUtils
- FactorGraphHelper
- SvgChart
- TraceStyle
- SvgSeries
- SvgChartHelper
- TweetyShade.Placeholder
- MyNotebookLib.AutoGenNotebookUpdater
- MyNotebookLib.AutoInvokeSKAgentsNotebookUpdater
- MyNotebookLib.DisplayLogger
- MyNotebookLib.DisplayLoggerProvider
- MyNotebookLib.GuessingGame
- MyNotebookLib.NotebookExecutor
- MyNotebookLib.NotebookPlannerUpdater
- MyNotebookLib.NotebookUpdaterBase
- MyNotebookLib.WorkbookInteractionBase
- MyNotebookLib.WorkbookUpdateInteraction
- MyNotebookLib.WorkbookValidation
- MyIA.AI.Notebooks.Program
- MyIA.AI.Notebooks.Config.Settings
- MyIA.AI.Notebooks.Config.Utils
- <PrivateImplementationDetails>
- <>y__InlineArray5`1
- <>y__InlineArray6`1
- <>z__ReadOnlyArray`1
- SkiaUtils+<ShowImage>d__0
- FactorGraphHelper+<>c
- FactorGraphHelper+<>o__12
- SvgChartHelper+PlotLayout
- SvgChartHelper+<>c
- SvgChartHelper+<>c__DisplayClass15_0
- SvgChartHelper+<>c__DisplayClass16_0
- SvgChartHelper+<>c__DisplayClass20_0
- MyNotebookLib.AutoGenNotebookUpdater+<>c__DisplayClass1_0
- MyNotebookLib.AutoGenNotebookUpdater+<PerformNotebookUpdateAsync>d__1
- MyNotebookLib.AutoInvokeSKAgentsNotebookUpdater+NotebookTerminationStrategy
- MyNotebookLib.AutoInvokeSKAgentsNotebookUpdater+NotebookSelectionStrategy
- MyNotebookLib.AutoInvokeSKAgentsNotebookUpdater+<>c
- MyNotebookLib.AutoInvokeSKAgentsNotebookUpdater+<PerformNotebookUpdateAsync>d__1
- MyNotebookLib.GuessingGame+ApprovalTerminationStrategy
- MyNotebookLib.GuessingGame+<RunGameAsync>d__0
- MyNotebookLib.NotebookExecutor+<>c
- MyNotebookLib.NotebookExecutor+<>c__DisplayClass11_0
- MyNotebookLib.NotebookExecutor+<>c__DisplayClass12_0
- MyNotebookLib.NotebookExecutor+<RunCell>d__12
- MyNotebookLib.NotebookExecutor+<RunNotebookAsync>d__11
- MyNotebookLib.NotebookPlannerUpdater+<>c
- MyNotebookLib.NotebookPlannerUpdater+<PerformNotebookUpdateAsync>d__2
- MyNotebookLib.NotebookUpdaterBase+<>c
- MyNotebookLib.NotebookUpdaterBase+<UpdateNotebookAsync>d__49
- MyNotebookLib.WorkbookInteractionBase+<>c
- MyNotebookLib.WorkbookInteractionBase+<>c__DisplayClass20_0
- MyNotebookLib.WorkbookInteractionBase+<>c__DisplayClass22_0
- MyNotebookLib.WorkbookInteractionBase+<>c__DisplayClass24_0
- MyNotebookLib.WorkbookInteractionBase+<ExecuteWithExceptionHandling>d__23
- MyNotebookLib.WorkbookInteractionBase+<FindAllIndex>d__21`1
- MyNotebookLib.WorkbookInteractionBase+<InitializeExecutorIfNeeded>d__11
- MyNotebookLib.WorkbookInteractionBase+<InitializePythonExecutor>d__12
- MyNotebookLib.WorkbookInteractionBase+<LoadNotebookAsync>d__14
- MyNotebookLib.WorkbookInteractionBase+<RunNotebook>d__24
- MyNotebookLib.WorkbookInteractionBase+<RunNotebookAsync>d__22
- MyNotebookLib.WorkbookInteractionBase+<UpdateCellAsync>d__20
- MyNotebookLib.WorkbookUpdateInteraction+<>c__DisplayClass4_0
- MyNotebookLib.WorkbookUpdateInteraction+<>c__DisplayClass5_0
- MyNotebookLib.WorkbookUpdateInteraction+<>c__DisplayClass6_0
- MyNotebookLib.WorkbookUpdateInteraction+<InsertInWorkbookCell>d__6
- MyNotebookLib.WorkbookUpdateInteraction+<ReplaceBlockInWorkbookCell>d__5
- MyNotebookLib.WorkbookUpdateInteraction+<ReplaceWorkbookCell>d__4
- MyIA.AI.Notebooks.Program+<Main>d__2
- MyIA.AI.Notebooks.Program+<TestNotebookUpdater>d__3
- MyIA.AI.Notebooks.Config.Settings+<AskApiKey>d__9
- MyIA.AI.Notebooks.Config.Settings+<AskAzureEndpoint>d__7
- MyIA.AI.Notebooks.Config.Settings+<AskModel>d__8
- MyIA.AI.Notebooks.Config.Settings+<AskOrg>d__10
- <PrivateImplementationDetails>+__StaticArrayInitTypeSize=22
- <PrivateImplementationDetails>+__StaticArrayInitTypeSize=24
- MyNotebookLib.AutoGenNotebookUpdater+<>c__DisplayClass1_0+<<PerformNotebookUpdateAsync>b__0>d
- MyNotebookLib.AutoGenNotebookUpdater+<>c__DisplayClass1_0+<<PerformNotebookUpdateAsync>b__1>d
- MyNotebookLib.AutoGenNotebookUpdater+<>c__DisplayClass1_0+<<PerformNotebookUpdateAsync>b__2>d
- MyNotebookLib.AutoGenNotebookUpdater+<>c__DisplayClass1_0+<<PerformNotebookUpdateAsync>b__3>d
- MyNotebookLib.WorkbookInteractionBase+<>c__DisplayClass24_0+<<RunNotebook>b__0>d
- MyNotebookLib.WorkbookUpdateInteraction+<>c__DisplayClass4_0+<<ReplaceWorkbookCell>b__0>d
- MyNotebookLib.WorkbookUpdateInteraction+<>c__DisplayClass5_0+<<ReplaceBlockInWorkbookCell>b__0>d
- MyNotebookLib.WorkbookUpdateInteraction+<>c__DisplayClass6_0+<<InsertInWorkbookCell>b__0>d
✅ Classe importée: MyNotebookLib.AutoInvokeSKAgentsNotebookUpdater
- Type: MyNotebookLib.AutoInvokeSKAgentsNotebookUpdater
- Nom complet: MyNotebookLib.AutoInvokeSKAgentsNotebookUpdater
=== RÉSUMÉ IMPORT ===
AutoInvokeSKAgentsNotebookUpdater = MyNotebookLib.AutoInvokeSKAgentsNotebookUpdater
Type: <class 'System.RuntimeType'>
🎉 IMPORT .NET RÉUSSI !
Lecture du résultat — runtime .NET, réflexion et piège de namespace
Les cellules précédentes comparent GetExportedTypes() et GetTypes(), les deux API .NET pour énumérer les types d’un assembly. Sur le CoreCLR (le runtime que l’on vient de démarrer explicitement), les deux réussissent, et leur divergence illustre deux notions distinctes : la visibilité des types et la résolution des dépendances.
GetExportedTypes() vs GetTypes(). La première n’énumère que les types publics (19 ici). La seconde énumère tous les types — y compris les types internes et les structures générées par le compilateur C# (<>y__InlineArray51,<>z__ReadOnlyArray1, etc.), d’où un compte plus élevé (84). C’est la même distinction qu’entre dir() et [x for x in dir() if not x.startswith('_')] en Python : on filtre le bruit interne.
Pourquoi le runtime compte — la vraie raison du pythonnet.load("coreclr", …) de la cellule précédente. pythonnet 3.x boote par défaut le CLR du .NET Framework, un vieux runtime incapable de résoudre les types modernes (System.ValueTuple\5) qu'un assembly **net9.0** utilise abondamment dansNotebookUpdaterBase.InitSemanticKernel(). Sur ce runtime par défaut,GetTypes()lève uneReflectionTypeLoadException(dont la propriétéLoaderExceptionsliste les dépendances manquantes) et l'instanciation échoue avec uneTypeLoadExceptionopaque. En démarrant le **CoreCLR (.NET 9)** viapythonnet.load(“coreclr”, runtime_config=…), on fournit le runtime capable de résoudreValueTuple`5et toutes les dépendances :GetTypes()réussit proprement et l'instanciation passe. **Quand on interopère avec un assembly .NET moderne, sélectionner le bon runtime dès le départ** (DOTNET_ROLL_FORWARD=Major` + CoreCLR) est la correction racine, pas un contournement.
Le piège assembly ≠ namespace (corrigé). Indépendant du runtime : la cause de l’échec d’import observé initialement (AutoInvokeSKAgentsNotebookUpdater = None) venait des noms testés par GetType — MyIA.AI.Notebooks.* et AutoInvokeSKAgentsNotebookUpdater. Or les types vivent dans le namespace MyNotebookLib (MyNotebookLib.AutoInvokeSKAgentsNotebookUpdater, visible dans GetExportedTypes). Assembly.GetType(nom) fait une recherche exacte sur le nom complet : un seul segment diffère, et elle retourne None sans lever d’exception — silencieusement. C’est le piège rappelé en tête de bloc : le nom de l’assembly (MyIA.AI.Notebooks) n’est pas le namespace C# (MyNotebookLib). La cellule précédente teste le bon nom complet ; les trois variantes historiquement fausses sont conservées commentées pour rendre le piège reproductible.
Exercice 2 : Exploration reflexive des types .NET
La reflexion .NET permet d’explorer dynamiquement les méthodes et proprietes d’un type charge depuis Python.
Objectif : Implementer une fonction inspect_clr_type() qui liste les méthodes publiques d’un type .NET avec leurs signatures.
Indices : - # Étape 1 : Utiliser assembly.GetType() pour recuperer un type CLR par son nom - # Étape 2 : Iterer sur type.GetMethods() pour lister les méthodes publiques - # Étape 3 : Extraire le nom de la méthode, le type de retour et les paramètres pour chaque méthode - # Indice : Utiliser System.Reflection.MethodInfo pour acceder aux informations detaillees de chaque méthode
def inspect_clr_type(assembly, type_name: str) -> list:
"""
TODO etudiant : Lister les methodes publiques d'un type .NET via reflexion.
Args:
assembly: L'assembly .NET charge via clr
type_name: Nom complet du type (ex: "MyNotebookLib.GuessingGame")
Returns:
Liste de dicts avec les cles: name, return_type, parameters (list de str)
"""
# TODO etudiant : utiliser assembly.GetType() et GetMethods()
return None
# Test :
# methods = inspect_clr_type(assembly, "MyNotebookLib.GuessingGame")
# for m in methods[:5]:
# print(f" {m['return_type']} {m['name']}({', '.join(m['parameters'])})")
print("Exercice a completer")Exercice a completer
Bloc 4 : Exemple d’utilisation
- On va créer une instance de
AutoInvokeSKAgentsNotebookUpdater. - On va définir un logger factice ou inexistant (selon la configuration).
- On va lancer la méthode
UpdateNotebookAsync()pour lancer l’agent.
Note : Comme la méthode est asynchrone en C#, on l’appelle via await dans un contexte asyncio Python.
import nest_asyncio
nest_asyncio.apply()
import asyncio
import threading
import os
from System import Activator, Console
from System.IO import StreamWriter
# LogLevel via reflection (cf. #9929, po-2023 + Python 3.13 + pythonnet 3.0.5 +
# quantconnect-stubs). Le shim auto-genere Microsoft/__init__.py appelle
# AddReference("Microsoft") (impossible en net9.0). On contourne en chargeant
# l'assembly par nom puis en resolvant les valeurs enum par reflection.
import clr
clr.AddReference("Microsoft.Extensions.Logging.Abstractions")
import System as _System
_LogLevel_type = _System.Type.GetType(
"Microsoft.Extensions.Logging.LogLevel, Microsoft.Extensions.Logging.Abstractions")
class _LogLevelWrapper:
def __init__(self):
self.Trace = _System.Enum.Parse(_LogLevel_type, "Trace")
self.Debug = _System.Enum.Parse(_LogLevel_type, "Debug")
self.Information = _System.Enum.Parse(_LogLevel_type, "Information")
self.Warning = _System.Enum.Parse(_LogLevel_type, "Warning")
self.Error = _System.Enum.Parse(_LogLevel_type, "Error")
self.Critical = _System.Enum.Parse(_LogLevel_type, "Critical")
LogLevel = _LogLevelWrapper()
from MyNotebookLib import DisplayLogger
# Un agent Semantic Kernel C# ne s'instancie PAS comme une classe Python
# ordinaire : il est obtenu par REFLEXION (`assembly.GetType`, cellule
# precedente) puis via `Activator.CreateInstance`. Les methodes async C# ne
# sont pas directement awaitable depuis Python (pythonnet les encapsule dans
# une AsyncStateMachineBox) : on les bloque via `GetAwaiter().GetResult()`.
#
# La boucle de l'agent (Coder -> Reviewer -> Admin, avec validation par
# `RunNotebook` du notebook genere) est une chaine multi-tours d'appels
# OpenAI, potentiellement longue. On l'execute dans un thread daemon borne
# par un timeout, et on capture le log de l'agent (Console.Out .NET) pour
# l'afficher : cela prouve, dans la sortie committee, que l'agent s'execute
# vraiment (instanciation + appels OpenAI + RunNotebook).
def run_agent_example(timeout_s=280):
if AutoInvokeSKAgentsNotebookUpdater is None:
print("Classe AutoInvokeSKAgentsNotebookUpdater non disponible")
return
# Un logger REEL est obligatoire : l'agent C# le propage a son outil interne
# RunNotebook. Avec logger=None, RunNotebook leve 'Value cannot be null
# (logger)' et la boucle ne converge jamais. DisplayLogger vit dans le
# namespace MyNotebookLib (pas MyIA.AI.Notebooks, le piege de ce notebook).
logger = DisplayLogger("PyNotebookUpdater", LogLevel.Debug)
notebook_path = r".\Workbook-Template-Python.ipynb"
updater = Activator.CreateInstance(AutoInvokeSKAgentsNotebookUpdater, notebook_path, logger)
print("Agent instancie :", updater)
updater.SetStartingNotebookFromTemplate("""
Creer un notebook Python, capable de requeter DBpedia via SPARQL,
puis tracer un graphique Plotly.
Ensuite,
1) corriger d'eventuels bugs
2) valider la sortie
""")
# Redirection du log .NET vers un fichier (le Console.Out du runtime CoreCLR
# n'est pas le sys.stdout de Python : il faut le capturer explicitement).
log_path = os.path.abspath("_agent_console.log")
original_out = Console.Out
sw = StreamWriter(log_path)
Console.SetOut(sw)
outcome = {"done": False, "error": None}
def _worker():
try:
updater.UpdateNotebookAsync().GetAwaiter().GetResult()
outcome["done"] = True
except Exception as e:
outcome["error"] = getattr(e, 'Message', str(e))
t = threading.Thread(target=_worker, daemon=True)
t.start()
t.join(timeout_s)
try:
sw.Flush()
sw.Close()
except Exception:
pass
Console.SetOut(original_out)
captured = ""
try:
with open(log_path, encoding="utf-8") as f:
captured = f.read()
except Exception:
pass
# On n'affiche que les lignes de log structurees ([Information]/[Debug]/...)
# du runtime : elles montrent la selection d'agent, les appels OpenAI et
# l'invocation de RunNotebook, sans le bruit du contenu JSON du notebook
# genere (qui est aussi dans le log mais n'est pas lisible ici).
log_lines = [ln for ln in captured.splitlines() if ln.lstrip().startswith("[")]
print("\n----- log de l'agent (lignes structurees) -----")
shown = log_lines[-30:] if len(log_lines) > 30 else log_lines
print("\n".join(shown) if shown else "(aucune ligne de log structuree)")
print("----- fin du log -----")
print("\n" + "-" * 60)
n_invoke = captured.count("InvokeAgentAsync")
n_openai = captured.count("OpenAIChatCompletionService")
n_runnb = captured.count("RunNotebook")
print(f"Activite mesuree : {n_invoke} invocation(s) d'agent, "
f"{n_openai} appel(s) OpenAI, {n_runnb} appel(s) RunNotebook.")
if outcome["done"]:
print("Agent SK termine avec succes !")
elif outcome["error"] is not None:
print(f"Erreur lors de l'execution de l'agent : {outcome['error']}")
else:
print(f"L'agent s'execute mais n'a pas converge en {timeout_s}s.")
print("La convergence complete demande l'execution imbriquee (via RunNotebook)")
print("du notebook genere, hors de portee d'une cellule de demonstration.")
if AutoInvokeSKAgentsNotebookUpdater is not None:
run_agent_example()
else:
print("Impossible de lancer l'agent - classe non disponible")Agent instancie : MyNotebookLib.AutoInvokeSKAgentsNotebookUpdater
----- log de l'agent (lignes structurees) -----
[Information] SubmitNotebook - Function SubmitNotebook succeeded.
[Information] SubmitNotebook - Function completed. Duration: 1.16E-05s
[Information] Microsoft.SemanticKernel.Connectors.OpenAI.OpenAIChatCompletionService - Prompt tokens: 10651. Completion tokens: 20. Total tokens: 10671.
[Information] AutoInvokeSKAgentsNotebookUpdater - [InvokeAsync] Invoked Microsoft.SemanticKernel.Connectors.OpenAI.OpenAIChatCompletionService with message count: 1.
[Trace] AutoInvokeSKAgentsNotebookUpdater - [InvokeAgentAsync] Agent message Microsoft.SemanticKernel.Agents.ChatCompletionAgent:
[Trace] AutoInvokeSKAgentsNotebookUpdater - [InvokeAgentAsync] Agent message Microsoft.SemanticKernel.Agents.ChatCompletionAgent: Start RunNotebook
[Trace] AutoInvokeSKAgentsNotebookUpdater - [InvokeAgentAsync] Agent message Microsoft.SemanticKernel.Agents.ChatCompletionAgent:
[Trace] AutoInvokeSKAgentsNotebookUpdater - [InvokeAgentAsync] Agent message Microsoft.SemanticKernel.Agents.ChatCompletionAgent: Notebook submitted
[Trace] AutoInvokeSKAgentsNotebookUpdater - [InvokeAgentAsync] Agent message Microsoft.SemanticKernel.Agents.ChatCompletionAgent: The notebook has been thoroughly tested, documented, and cleaned. It has been submitted for approval.
[Debug] AutoInvokeSKAgentsNotebookUpdater - [ShouldTerminateAsync] Evaluating termination for agent Microsoft.SemanticKernel.Agents.ChatCompletionAgent: ebdd043c-4787-4fe5-89bd-52ef23a7cf18.
[Information] AutoInvokeSKAgentsNotebookUpdater - [ShouldTerminateAsync] Evaluated termination for agent Microsoft.SemanticKernel.Agents.ChatCompletionAgent: ebdd043c-4787-4fe5-89bd-52ef23a7cf18 - False
[Information] AutoInvokeSKAgentsNotebookUpdater - [InvokeAgentAsync] Invoked agent Microsoft.SemanticKernel.Agents.ChatCompletionAgent: ebdd043c-4787-4fe5-89bd-52ef23a7cf18
[Debug] AutoInvokeSKAgentsNotebookUpdater - [InvokeAsync] Selecting agent: MyNotebookLib.AutoInvokeSKAgentsNotebookUpdater+NotebookSelectionStrategy
[Information] AutoInvokeSKAgentsNotebookUpdater - [InvokeAsync] Agent selected Microsoft.SemanticKernel.Agents.ChatCompletionAgent: d27763b1-8199-4e6d-99c4-ab4cecd8e779 by MyNotebookLib.AutoInvokeSKAgentsNotebookUpdater+NotebookSelectionStrategy
[Debug] AutoInvokeSKAgentsNotebookUpdater - [InvokeAgentAsync] Invoking agent Microsoft.SemanticKernel.Agents.ChatCompletionAgent: d27763b1-8199-4e6d-99c4-ab4cecd8e779
[Debug] AutoInvokeSKAgentsNotebookUpdater - [InvokeAsync] Invoking Microsoft.SemanticKernel.Connectors.OpenAI.OpenAIChatCompletionService.
[Information] Microsoft.SemanticKernel.Connectors.OpenAI.OpenAIChatCompletionService - Prompt tokens: 10128. Completion tokens: 16. Total tokens: 10144.
[Information] ApproveNotebook - Function ApproveNotebook invoking.
[Information] PyNotebookUpdater - Notebook approved
[Information] ApproveNotebook - Function ApproveNotebook succeeded.
[Information] ApproveNotebook - Function completed. Duration: 0.0001419s
[Information] Microsoft.SemanticKernel.Connectors.OpenAI.OpenAIChatCompletionService - Prompt tokens: 10161. Completion tokens: 17. Total tokens: 10178.
[Information] AutoInvokeSKAgentsNotebookUpdater - [InvokeAsync] Invoked Microsoft.SemanticKernel.Connectors.OpenAI.OpenAIChatCompletionService with message count: 1.
[Trace] AutoInvokeSKAgentsNotebookUpdater - [InvokeAgentAsync] Agent message Microsoft.SemanticKernel.Agents.ChatCompletionAgent:
[Trace] AutoInvokeSKAgentsNotebookUpdater - [InvokeAgentAsync] Agent message Microsoft.SemanticKernel.Agents.ChatCompletionAgent: Notebook approved
[Trace] AutoInvokeSKAgentsNotebookUpdater - [InvokeAgentAsync] Agent message Microsoft.SemanticKernel.Agents.ChatCompletionAgent: The notebook has been approved as it meets all the requirements and objectives set out.
[Debug] AutoInvokeSKAgentsNotebookUpdater - [ShouldTerminateAsync] Evaluating termination for agent Microsoft.SemanticKernel.Agents.ChatCompletionAgent: d27763b1-8199-4e6d-99c4-ab4cecd8e779.
[Information] AutoInvokeSKAgentsNotebookUpdater - [ShouldTerminateAsync] Evaluated termination for agent Microsoft.SemanticKernel.Agents.ChatCompletionAgent: d27763b1-8199-4e6d-99c4-ab4cecd8e779 - True
[Information] AutoInvokeSKAgentsNotebookUpdater - [InvokeAgentAsync] Invoked agent Microsoft.SemanticKernel.Agents.ChatCompletionAgent: d27763b1-8199-4e6d-99c4-ab4cecd8e779
[Debug] AutoInvokeSKAgentsNotebookUpdater - [InvokeAsync] Yield chat - IsComplete: True
----- fin du log -----
------------------------------------------------------------
Activite mesuree : 26 invocation(s) d'agent, 15 appel(s) OpenAI, 25 appel(s) RunNotebook.
Agent SK termine avec succes !
Explications
NotebookPath: Le fichier.ipynbque vous souhaitez générer / mettre à jour.updater.SetStartingNotebookFromTemplate(...): Injecte la consigne ou le template initial dans le notebook cible.await updater.UpdateNotebookAsync(): Lance l’agent C# (AutoInvokeSKAgentsNotebookUpdater) qui va orchestrer Semantic Kernel pour incrémenter / améliorer le notebook.
Vous pouvez ensuite rouvrir le fichier .ipynb généré (dans un Jupyter Lab/VS Code) pour voir le résultat.
Lecture chiffree — la facture d’un cycle d’agent lue dans ses logs. Le log structure du run porte la comptabilite complete : Prompt tokens: 10651. Completion tokens: 20 — l’agent a recu 10 651 tokens (le notebook cible passe en contexte) pour produire 20 tokens de decision, un ratio de 533:1 : dans ce cycle, etre un agent coute essentiellement lire, pas ecrire. Le plumbing local, lui, est gratuit a cote : Function SubmitNotebook completed. Duration: 1.16E-05s = 11,6 us — cinq ordres de grandeur sous le moindre appel LLM. La conversation se lit fragment par fragment dans les lignes [Trace] : decision Start RunNotebook, retour Notebook submitted, message final (« The notebook has been thoroughly tested, documented, and cleaned. It has been submitted for approval. »), puis [ShouldTerminateAsync] evalue la terminaison. C’est la boucle agent de Semantic Kernel en une fenetre : un appel, un outil, un verdict — et tout le cout concentre dans le premier.
Exercice 3 : Gestionnaire d’erreurs CLR
L’interop Python/.NET peut echouer silencieusement ou lever des exceptions .NET non catchees par Python.
Objectif : Implementer une fonction safe_clr_call() qui encapsule un appel .NET avec gestion d’erreurs et logging.
Indices : - # Étape 1 : Accepter une fonction callable et ses arguments en paramètres - # Étape 2 : Wraper l’appel dans un try/except qui capture les exceptions CLR et Python - # Étape 3 : Retourner un tuple (success: bool, result_or_error: Any) et logger l’opération - # Indice : Les exceptions .NET via pythonnet heritent de System.Exception ou sont des Exception Python classiques
def safe_clr_call(func, *args, **kwargs):
"""
TODO etudiant : Encapsuler un appel .NET avec gestion d'erreurs.
Args:
func: Fonction callable (methode .NET ou Python)
*args, **kwargs: Arguments a passer a la fonction
Returns:
tuple (success: bool, result_or_error) avec logging de l'operation
"""
# TODO etudiant : implementer le wrapper try/except
return None
# Test :
# ok, result = safe_clr_call(lambda: 42)
# print(f"Succes: {ok}, Resultat: {result}")
print("Exercice a completer")Exercice a completer
Conclusion
Resume des concepts
| Concept | Description | Code cle |
|---|---|---|
| pythonnet | Bridge Python/.NET | import clr |
| clr.AddReference | Chargement DLL | clr.AddReference("MyAssembly") |
| Assembly.GetType | Acces aux classes C# | assembly.GetType("Namespace.Class") |
| nest_asyncio | Asyncio dans notebooks | nest_asyncio.apply() |
| await .NET async | Appel méthodes async C# | await updater.UpdateNotebookAsync() |
Points cles a retenir
- pythonnet permet l’interop Python/.NET - Chargez et utilisez des DLL C# directement
- Les assemblies .NET sont accessibles - Reflection pour lister types et méthodes
- Asyncio compatible - Les méthodes async C# s’appellent avec await
- Agents SK C# depuis Python - Combinez le meilleur des deux mondes
Architecture de l’interop
┌─────────────────────────────────────────────────────────────┐
│ Python (Jupyter) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ pythonnet (clr) │ │
│ │ ┌─────────────────────────────────────────────┐ │ │
│ │ │ CLR Runtime (.NET) │ │ │
│ │ │ ┌─────────────────────────────────────┐ │ │ │
│ │ │ │ MyIA.AI.Notebooks.dll │ │ │ │
│ │ │ │ - AutoInvokeSKAgentsNotebookUpdater│ │ │ │
│ │ │ │ - Semantic Kernel C# │ │ │ │
│ │ │ └─────────────────────────────────────┘ │ │ │
│ │ └─────────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
Pour aller plus loin
| Notebook suivant | Contenu |
|---|---|
| 10-NotebookMaker | Système 3-agents pour generation de notebooks |
Navigation : Index | << 08-MCP | 10-NotebookMaker >>