Extraire des données structurées à l'aide d'appels de fonction

Afficher sur l'IA de Google Essayer un notebook Colab Afficher le notebook sur GitHub

Dans ce tutoriel, vous allez suivre un exemple d'extraction de données structurées à l'aide de l'API Gemini pour extraire les listes de personnages, de relations, d'éléments et de lieux d'une histoire.

Configuration

pip install -U -q google-generativeai
import json
import textwrap

from google import genai
from google.genai import types

from IPython.display import JSON
from IPython.display import display
from IPython.display import Markdown


def to_markdown(text):
  text = text.replace('•', '  *')
  return Markdown(textwrap.indent(text, '> ', predicate=lambda _: True))

Une fois que vous disposez de la clé API, transmettez-la au SDK. Pour cela, vous avez le choix entre deux méthodes :

  • Placez la clé dans la variable d'environnement GOOGLE_API_KEY (le SDK la récupérera automatiquement à partir de là).
  • Transmettre la clé à genai.Client(api_key=...)
client = genai.Client(api_key=GOOGLE_API_KEY)

Exemple de tâche

Dans ce tutoriel, vous allez extraire des entités à partir d'histoires en langage naturel. Voici un exemple d'article écrit par Gemini.

MODEL_ID="gemini-2.0-flash"
prompt = """
Write a long story about a girl with magic backpack, her family, and at
least one other character. Make sure everyone has names. Don't forget to
describe the contents of the backpack, and where everyone and everything
starts and ends up.
"""

response = client.models.generate_content(
  model=MODEL_ID,
  contents=prompt,    
)
story = response.text
to_markdown(story)

Elara était une petite fille, tout en coudes et en genoux, avec des yeux de la couleur d'une mer agitée. Elle vivait dans la paisible ville de Havenwood, nichée à côté d'une forêt murmurante que tout le monde ignorait. Elara, cependant, a entendu ses secrets. C'est elle, par exemple, qui savait que les roses primées de Mme Gable étaient l'objet de commérages chuchotés par les chênes, ou que le ruisseau murmurait d'anciennes chansons pour enfants.

Cependant, son bien le plus précieux n'était pas une oreille attentive. Il s'agissait d'un simple sac à dos en cuir usé qu'elle avait trouvé dans le grenier de la vieille maison grinçante de sa famille. Il avait l'air assez ordinaire, mais il ne l'était pas du tout. Il ne s'agissait pas d'un sac à dos ordinaire, mais du sac à dos magique d'Elara.

À l'intérieur, le sac à dos renfermait des merveilles. Il ne s'agit pas de bijoux ni d'or, mais d'objets bien plus utiles. Il y avait un pot de miel sans fond qui ne se vidait jamais, parfait pour soulager les maux de gorge ou faire du thé. Il s'agit d'un petit oiseau en bois sculpté de manière complexe qui, lorsqu'il est libéré, peut transporter des messages plus rapidement que le vent. Une poignée de poussière irisée et chatoyante qui pouvait réparer tout ce qui était cassé, de la poterie fêlée aux sentiments blessés. Une boussole qui ne pointait pas vers le nord, mais vers ce dont elle avait le plus besoin. Et son préféré, un petit livre relié en cuir rempli de pages blanches qui se remplissaient de l'histoire, du poème ou du sort parfaits chaque fois qu'elle en avait besoin.

Ses parents, Arthur, un charpentier discret dont les sourcils étaient toujours recouverts de sciure, et Clara, une boulangère dont les brioches à la cannelle étaient légendaires, savaient que le sac à dos existait. Ils ne comprenaient pas vraiment sa magie, la considérant comme l'imagination débordante d'Elara, mais ils l'aimaient profondément et toléraient les événements étranges qui la suivaient parfois. C'étaient des gens bons et stables, satisfaits de leur vie à Havenwood.

Un jour, un nouveau visage est apparu à Havenwood. Un homme bourru et trapu nommé Silas, dont les yeux brillaient d'acier et dont le visage était marqué d'un froncement de sourcils permanent. Il a acheté l'ancienne usine délabrée à la périphérie de la ville, un endroit que tout le monde évitait, en prétendant vouloir la restaurer. Mais Elara ressentait un léger malaise chaque fois qu'il était à proximité. La forêt, qui était habituellement une source de réconfort, semblait murmurer des avertissements lorsque Silas passait devant.

Le premier signe de problème est apparu lorsque les brioches à la cannelle de Clara ont commencé à avoir un goût… étrange. Fade, presque sans saveur. Puis, les projets de menuiserie d'Arthur ont commencé à se fissurer et à se briser, même s'il faisait très attention. La ville, autrefois animée et pleine de vie, semblait perdre ses couleurs et sa joie.

Elara savait, avec une certitude qui résonnait au plus profond de son être, que Silas en était la cause. Un soir, elle l'avait vu s'approcher des bois de Whispering en rampant, en murmurant d'étranges incantations sous la lueur maladive de la lune. Il vidait la magie de Havenwood, petit à petit.

Déterminée à l'arrêter, Elara se tourna vers son sac à dos. Elle a sorti la boussole. Elle tourne d'abord de manière désordonnée, puis se stabilise, pointant non pas directement vers Silas, mais vers les bois de Whispering. C'est là qu'elle devait aller.

Consciente qu'elle ne pouvait pas affronter Silas seule, Elara se confia à Thomas, le bibliothécaire de la ville. C'était un homme gentil et érudit, souvent plongé dans les pages d'anciens ouvrages. Il a d'abord considéré ses inquiétudes comme de la fantaisie, mais le désespoir dans sa voix et la peur réelle dans ses yeux l'ont convaincu de l'écouter.

Ensemble, Elara et Thomas s'aventurent dans les bois de Whispering. La forêt était différente maintenant, plus sombre, plus silencieuse, les murmures presque réduits au silence. L'air était lourd, suffocant.

Guidés par la boussole, ils suivirent un chemin sinueux jusqu'à une clairière où Silas se tenait devant un chêne tordu et noueux. Il chantait, sa voix était rauque et grinçante, et autour de lui, l'air scintillait de magie volée.

"Tu ne peux pas faire ça, Silas !" s'écria Elara, sa voix tremblante mais ferme.

Silas se retourna, les yeux écarquillés de surprise. "Qu'est-ce que c'est ? Des enfants qui se mêlent de tout ? Vous n'avez aucune idée de ce que je fais. Je vais libérer cette ville de sa magie inutile ! Cela vous rend faible et dépendant !"

"C'est ce qui fait de nous ce que nous sommes !" rétorqua Elara en s'avançant. Elle plonge dans son sac à dos et en sort le livre. Alors qu'elle le tenait, les pages blanches se sont remplies de mots, tissant une histoire de courage, de résilience et de pouvoir de la communauté. Il s'agissait d'un contre-sort, conçu pour restaurer la magie que Silas volait.

Silas se précipita vers le livre, mais Thomas, étonnamment agile pour un bibliothécaire, se plaça sur son chemin. Il n'avait peut-être pas de magie, mais il possédait un savoir acquis au fil de sa vie et une protection féroce envers Havenwood.

Elara commença à lire. Sa voix, claire et forte, emplit la clairière. Pendant qu'elle lisait, la magie volée commença à revenir dans la forêt et dans la ville. Les arbres se redressèrent, l'air s'éclaircit, et les murmures revinrent, plus forts et plus vibrants qu'auparavant.

Silas hurla, son visage tordu de rage. Le chêne noueux devant lequel il se tenait se mit à craquer et à s'effriter, sa magie volée revenant à sa source.

Le livre a terminé son histoire, et ses pages sont redevenues blanches. Silas s'effondra, ses pouvoirs disparus. Il a rétréci, tant physiquement que spirituellement. Il n'est plus la figure imposante qui a terrorisé Havenwood. Il n'était qu'un petit homme aigri, animé par le ressentiment.

Il a finalement été chassé de la ville, promettant une vengeance qui n'a jamais eu lieu.

Havenwood est lentement revenu à la normale. Les brioches à la cannelle de Clara étaient plus savoureuses que jamais, les meubles en bois d'Arthur étaient plus solides et plus beaux, et la ville était pleine d'énergie.

Elara, en revanche, a été modifiée. Elle n'était plus une simple silhouette. Elle était une protectrice, une gardienne de Havenwood. Elle continua à porter son sac à dos magique, à explorer la forêt, à écouter ses secrets et à être toujours prête à défendre sa maison.

Inspiré par son aventure, Thomas a créé un jardin communautaire, utilisant ses nouvelles connaissances sur les plantes et les herbes pour apporter beauté et guérison à la ville. Il est devenu un héros silencieux, qui ne cherche pas la reconnaissance, mais trouve sa satisfaction à aider les autres.

Les parents d'Elara, encore un peu désorientés par toute cette épreuve, ont compris l'importance de sa magie et le rôle qu'elle jouait dans leur vie. Arthur a même construit une étagère spéciale pour son sac à dos, juste à côté de la cheminée, symbole de son importance pour la famille.

Ainsi, Elara et son sac à dos magique sont restés à Havenwood, en tant que gardienne silencieuse, toujours à l'écoute, toujours protectrice, sa vie entrelacée avec les murmures de la forêt et l'amour de sa famille et de ses amis. Elle avait sauvé sa maison, non pas par la force brute, mais par la gentillesse, le courage et le pouvoir durable d'un sac à dos magique.

Utiliser un langage naturel

Les grands modèles de langage sont de puissants outils multitâches. Souvent, il vous suffit de demander à Gemini ce que vous voulez, et il s'en chargera.

L'API Gemini ne dispose pas de mode JSON. Vous devez donc prendre en compte quelques points lorsque vous générez des structures de données de cette manière:

  • Il arrive que l'analyse échoue.
  • Le schéma ne peut pas être appliqué de manière stricte.

Vous allez résoudre ces problèmes dans la section suivante. Commencez par essayer une requête simple en langage naturel avec le schéma écrit sous forme de texte. Cette fonctionnalité n'a pas été optimisée:

MODEL_ID="gemini-2.0-flash"
prompt = """
Please return JSON describing the people, places, things and relationships from this story using the following schema:

{"people": list[PERSON], "places":list[PLACE], "things":list[THING], "relationships": list[RELATIONSHIP]}

PERSON = {"name": str, "description": str, "start_place_name": str, "end_place_name": str}
PLACE = {"name": str, "description": str}
THING = {"name": str, "description": str, "start_place_name": str, "end_place_name": str}
RELATIONSHIP = {"person_1_name": str, "person_2_name": str, "relationship": str}

All fields are required.

Important: Only return a single piece of valid JSON text.

Here is the story:

""" + story

response = client.models.generate_content(
  model=MODEL_ID,
  contents=prompt,
  config=types.GenerateContentConfig(
    response_mime_type="application/json"
  ),
)

Cela a renvoyé une chaîne JSON. Essayez de l'analyser:

import json

print(json.dumps(json.loads(response.text), indent=4))
{
    "people": [
        {
            "name": "Elara",
            "description": "A wisp of a girl with eyes the color of a stormy sea, who can hear the forest's secrets.",
            "start_place_name": "Havenwood",
            "end_place_name": "Havenwood"
        },
        {
            "name": "Arthur",
            "description": "Elara's father, a quiet carpenter with sawdust permanently clinging to his eyebrows.",
            "start_place_name": "Havenwood",
            "end_place_name": "Havenwood"
        },
        {
            "name": "Clara",
            "description": "Elara's mother, a baker whose cinnamon rolls were legendary.",
            "start_place_name": "Havenwood",
            "end_place_name": "Havenwood"
        },
        {
            "name": "Silas",
            "description": "A gruff, barrel-chested man with a glint of steel in his eyes who wanted to drain the magic from Havenwood.",
            "start_place_name": "Havenwood",
            "end_place_name": "Havenwood"
        },
        {
            "name": "Thomas",
            "description": "The town\u2019s librarian, a kind, bookish man.",
            "start_place_name": "Havenwood",
            "end_place_name": "Havenwood"
        }
    ],
    "places": [
        {
            "name": "Havenwood",
            "description": "A quiet town nestled beside a whispering forest."
        },
        {
            "name": "Whispering Woods",
            "description": "A forest beside Havenwood that murmurs secrets."
        },
        {
            "name": "Old Mill",
            "description": "A dilapidated old mill on the edge of Havenwood."
        }
    ],
    "things": [
        {
            "name": "Magic Backpack",
            "description": "A simple, worn, leather backpack that holds magical items.",
            "start_place_name": "Elara's House",
            "end_place_name": "Havenwood"
        },
        {
            "name": "Honey Jar",
            "description": "A bottomless jar of honey.",
            "start_place_name": "Magic Backpack",
            "end_place_name": "Magic Backpack"
        },
        {
            "name": "Wooden Bird",
            "description": "A small, intricately carved wooden bird that can fly messages.",
            "start_place_name": "Magic Backpack",
            "end_place_name": "Magic Backpack"
        },
        {
            "name": "Iridescent Dust",
            "description": "Shimmering dust that can mend anything broken.",
            "start_place_name": "Magic Backpack",
            "end_place_name": "Magic Backpack"
        },
        {
            "name": "Compass",
            "description": "A compass that points towards what is needed most.",
            "start_place_name": "Magic Backpack",
            "end_place_name": "Magic Backpack"
        },
        {
            "name": "Leather-bound Book",
            "description": "A small, leather-bound book filled with blank pages that fill with the perfect story, poem, or spell whenever needed.",
            "start_place_name": "Magic Backpack",
            "end_place_name": "Magic Backpack"
        },
        {
            "name": "Cinnamon Rolls",
            "description": "Legendary cinnamon rolls made by Clara.",
            "start_place_name": "Havenwood",
            "end_place_name": "Havenwood"
        }
    ],
    "relationships": [
        {
            "person_1_name": "Elara",
            "person_2_name": "Arthur",
            "relationship": "Daughter-Father"
        },
        {
            "person_1_name": "Elara",
            "person_2_name": "Clara",
            "relationship": "Daughter-Mother"
        },
        {
            "person_1_name": "Elara",
            "person_2_name": "Thomas",
            "relationship": "Friends"
        },
        {
            "person_1_name": "Elara",
            "person_2_name": "Silas",
            "relationship": "Adversaries"
        },
        {
            "person_1_name": "Arthur",
            "person_2_name": "Clara",
            "relationship": "Spouses"
        }
    ]
}

Cette méthode est relativement simple et fonctionne souvent, mais vous pouvez la rendre plus stricte/robuste en définissant le schéma à l'aide de la fonctionnalité d'appel de fonction de l'API.

Utiliser l'appel de fonction

Si vous n'avez pas encore suivi le tutoriel sur les principes de base des appels de fonction, assurez-vous de le faire en premier.

Avec l'appel de fonction, votre fonction et ses paramètres sont décrits à l'API en tant que genai.protos.FunctionDeclaration. Dans les cas de base, le SDK peut créer le FunctionDeclaration à partir de la fonction et de ses annotations. Vous devrez donc les définir explicitement pour le moment.

Définir le schéma

Commencez par définir person comme un objet avec les champs de chaîne name, description, start_place_name et end_place_name.

Person = {
    
    "type": "OBJECT",
    "properties": {
        "character_name": {
            "type": "STRING",
            "description": "Character name",
        },
        "character_description": {
            "type": "STRING",
            "description": "Character description",
        }
    },
    "required": ["character_name", "character_description"]
}

Faites de même pour chacune des entités que vous essayez d'extraire:

Relationships = {
    "type": "OBJECT",
    "properties": {
        "first_character": {
            "type": "STRING",
            "description": "First character name",
        },
        "second_character": {
            "type": "STRING",
            "description": "Second character name",
        },
        "relationship": {
            "type": "STRING",
            "description": "Familiar elationship between first and second character",
        }
    },
    "required": ["first_character", "second_character", "relationship"]
}
Places = {
    "type": "OBJECT",
    "properties": {
        "place_name": {
            "type": "STRING",
            "description": "Place name",
        },
        "place_description": {
            "type": "STRING",
            "description": "Place description",
        }
    },
    "required": ["place_name", "place_description"]
}
Things = {
    "type": "OBJECT",
    "properties": {
        "thing_name": {
            "type": "STRING",
            "description": "Thing name",
        },
        "thing_description": {
            "type": "STRING",
            "description": "Thing description",
        }
    },
    "required": ["thing_name", "thing_description"]
}

Créez maintenant le FunctionDeclaration:

get_people = types.FunctionDeclaration(
    name="get_people",
    description="Get information about characters",
    parameters=Person,
)

get_relationships = types.FunctionDeclaration(
    name="get_relationships",
    description="Get information about relationships between people",
    parameters=Relationships
)

get_places = types.FunctionDeclaration(
    name="get_places",
    description="Get information about places",
    parameters=Places
)

get_things = types.FunctionDeclaration(
    name="get_things",
    description="Get information about things",
    parameters=Things
)

story_tools = types.Tool(
    function_declarations=[get_people, get_relationships, get_places, get_things],
)

Appeler l'API

Comme vous l'avez vu dans la section Principes de base des appels de fonction, vous pouvez maintenant transmettre ce FunctionDeclaration à l'argument tools du constructeur genai.GenerativeModel (le constructeur accepte également une représentation JSON équivalente de la déclaration de fonction):

MODEL_ID="gemini-2.0-flash"
prompt = f"""
{story}

Please add the people, places, things, and relationships from this story to the database
"""

result = client.models.generate_content(
    model=MODEL_ID,
    contents=prompt,
    config=types.GenerateContentConfig(
        tools=[story_tools],
        temperature=0
        )
)

Il n'y a plus de texte à analyser. Le résultat est une structure de données.

print(result.candidates[0].content.parts[0].text)
None
result.candidates[0].content.parts[0].function_call
FunctionCall(id=None, args={'character_name': 'Elara', 'character_description': 'a wisp of a girl, all elbows and knees, with eyes the color of a stormy sea'}, name='get_people')
fc = result.candidates[0].content.parts[0].function_call
print(type(fc))
<class 'google.genai.types.FunctionCall'>

La classe genai.protos.FunctionCall est basée sur les tampons de protocole Google. Convertissez-la en objet compatible JSON plus familier:

for part in result.candidates[0].content.parts:
  print(json.dumps(part.function_call.args, indent=4))
{
    "character_name": "Elara",
    "character_description": "a wisp of a girl, all elbows and knees, with eyes the color of a stormy sea"
}
{
    "character_name": "Arthur",
    "character_description": "a quiet carpenter with sawdust permanently clinging to his eyebrows, Elara's father"
}
{
    "character_description": "a baker whose cinnamon rolls were legendary, Elara's mother",
    "character_name": "Clara"
}
{
    "character_description": "a gruff, barrel-chested man with eyes that held a glint of steel and a perpetual scowl etched onto his face",
    "character_name": "Silas"
}
{
    "character_description": "the town\u2019s librarian, a kind, bookish man, often lost in the pages of ancient tomes",
    "character_name": "Thomas"
}
{
    "place_name": "Havenwood",
    "place_description": "a quiet town, nestled beside a whispering forest"
}
{
    "place_description": "a forest near Havenwood",
    "place_name": "the Whispering Woods"
}
{
    "place_description": "a dilapidated old mill on the edge of town",
    "place_name": "the old mill"
}
{
    "thing_name": "Elara's magic backpack",
    "thing_description": "a simple, worn, leather backpack that held wonders"
}
{
    "thing_description": "never emptied, perfect for soothing sore throats or making tea",
    "thing_name": "a bottomless jar of honey"
}
{
    "thing_description": "when released, could fly messages faster than the wind",
    "thing_name": "a small, intricately carved wooden bird"
}
{
    "thing_name": "a handful of shimmering, iridescent dust",
    "thing_description": "could mend anything broken, from cracked pottery to hurt feelings"
}
{
    "thing_name": "a compass",
    "thing_description": "pointed not north, but towards what she needed most"
}
{
    "thing_description": "filled with blank pages that would fill with the perfect story, poem, or spell whenever she needed one",
    "thing_name": "a small, leather-bound book"
}
{
    "first_character": "Elara",
    "relationship": "father",
    "second_character": "Arthur"
}
{
    "relationship": "mother",
    "second_character": "Clara",
    "first_character": "Elara"
}

Conclusion

Bien que l'API puisse gérer les problèmes d'extraction de données structurées avec une entrée et une sortie textuelles pures, l'utilisation d'un appel de fonction est probablement plus fiable, car elle vous permet de définir un schéma strict et élimine une étape d'analyse potentiellement sujette à des erreurs.