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.dll compilee (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 :

  1. Installer et configurer pythonnet
  2. Charger la DLL .NET (compilee en C#) qui contient notre code d’agent
  3. 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 : pythonnet utilise 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

# Installe pythonnet. À n'exécuter qu'une seule fois (enlevez --quiet si besoin)
# 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 Kernel
  • MyNotebookLib.AutoGenNotebookUpdater — updater AutoGen
  • MyNotebookLib.DisplayLogger et DisplayLoggerProvider — logger intégré au notebook
  • MyNotebookLib.NotebookExecutor, NotebookPlannerUpdater, NotebookUpdaterBase
  • MyNotebookLib.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 cibler MyNotebookLib, pas MyIA.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

  1. NotebookPath : Le fichier .ipynb que vous souhaitez générer / mettre à jour.
  2. updater.SetStartingNotebookFromTemplate(...) : Injecte la consigne ou le template initial dans le notebook cible.
  3. 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

  1. pythonnet permet l’interop Python/.NET - Chargez et utilisez des DLL C# directement
  2. Les assemblies .NET sont accessibles - Reflection pour lister types et méthodes
  3. Asyncio compatible - Les méthodes async C# s’appellent avec await
  4. 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 >>

Retour au sommet