Guide pas par pas : Configuration de RAG pour les agents d'IA Gen en utilisant la base de données vectorielle IRIS en Python
Comment configurer le RAG pour les agents OpenAI à l'aide d'InterSystems IRIS Vector DB en Python
Dans cet article, je vous présenterai un exemple d'utilisation d'InterSystems IRIS Vector DB pour stocker des représentations vectorielles et les intégrer à un agent OpenAI.
Pour cette démonstration, nous créerons un agent OpenAI au moyen de connaissances sur la technologie InterSystems. Pour ce faire, nous stockerons les représentations de certains documents InterSystems dans IRIS, puis, à l'aide de la recherche vectorielle IRIS, nous récupérerons le contenu pertinent, ce qui permettra de mettre en place un processus de Retrieval-Augmented Generation (RAG)
Note: Remarque : vous trouverez dans la section 1 des détails sur la manière de transformer du texte en embeddings. Si vous ne vous intéressez qu'à la recherche vectorielle IRIS, vous pouvez passer directement à la section 2.
Section 1: embedding des données
La qualité de vos embeddings dépend de celle de vos données ! Pour obtenir les meilleurs résultats, vous devez préparer vos données avec soin. Voici les étapes à suivre :
- Nettoyage du texte (suppression des caractères spéciaux ou des espaces superflus)
- Segmentation des données en blocs plus petits
- Autres techniques de prétraitement
Dans cet exemple, la documentation est stockée dans de simples fichiers texte qui ne nécessitent qu'un nettoyage minimal. Cependant, nous allons diviser le texte en blocs afin de permettre un RAG plus efficace et plus précis.
Étape 1 : Segmentation des fichiers texte
La segmentation du texte en blocs gérables présente deux avantages pour les systèmes RAG :
- Une recherche plus précise : les embeddings représentent des sections de texte plus petites et plus spécifiques.
- Une recherche plus efficace : la réduction de la quantité de texte traitée par requête diminue les coûts et améliore les performances.
Dans cet exemple, les blocs du texte segmenté seront stockés dans des fichiers Parquet avant d’être téléchargés vers IRIS (mais vous pouvez utiliser n’importe quelle méthode, y compris le téléchargement direct).
Fonction de segmentation
Nous utiliserons RecursiveCharacterTextSplitter de langchain_text_splitters pour segmenter le texte de manière stratégique en fonction des limites des paragraphes, des phrases et des mots.
- Taille du bloc : 300 jetons (des segments plus grands offrent plus de contexte mais augmentent le coût de la recherche)
- Chevauchement de blocs : 50 jetons (permet de préserver le contexte entre les blocs)
from langchain_text_splitters import RecursiveCharacterTextSplitter
def chunk_text_by_tokens(text: str, chunk_size: int, chunk_overlap: int) -> list[str]:
"""
Chunk text prioritizing paragraph and sentence boundaries using
RecursiveCharacterTextSplitter. Returns a list of chunk strings.
"""
splitter = RecursiveCharacterTextSplitter(
# En priorité, les unités sémantiques les plus grandes, puis les plus petites
separators=["\n\n", "\n", ". ", " ", ""],
chunk_size=chunk_size,
chunk_overlap=chunk_overlap,
length_function=len,
is_separator_regex=False,
)
return splitter.split_text(text)
Nous allons ensuite utiliser la fonction de segmentation pour traiter un fichier texte à la fois et appliquons un encodeur tiktoken afin de calculer le nombre de jetons et de générer des métadonnées. Ces métadonnées nous seront utiles ultérieurement pour créer des représentations vectorielles et les stocker dans IRIS.
from pathlib import Path
import tiktoken
def chunk_file(path: Path, chunk_size: int, chunk_overlap: int, encoding_name: str = "cl100k_base") -> list[dict]:
"""
Read a file, split its contents into token-aware chunks, and return metadata for each chunk.
Returns a list of dicts with keys:
- filename
- relative_path
- absolute_path
- chunk_index
- chunk_text
- token_count
- modified_time
- size_bytes
"""
p = Path(path)
if not p.exists() or not p.is_file():
raise FileNotFoundError(f"File not found: {path}")
try:
text = p.read_text(encoding="utf-8", errors="replace")
except Exception as e:
raise RuntimeError(f"Failed to read file {p}: {e}")
# Préparation du tokenizer pour un comptage précis des jetons
try:
encoding = tiktoken.get_encoding(encoding_name)
except Exception as e:
raise ValueError(f"Invalid encoding name '{encoding_name}': {e}")
# Création des blocs à l'aide du « chunker » fourni
chunks = chunk_text_by_tokens(text, chunk_size, chunk_overlap)
# Métadonnées du fichier
stat = p.stat()
from datetime import datetime, timezone
modified_time = datetime.fromtimestamp(stat.st_mtime, tz=timezone.utc).isoformat()
absolute_path = str(p.resolve())
try:
relative_path = str(p.resolve().relative_to(Path.cwd()))
except Exception:
relative_path = p.name
# Création de lignes
rows: list[dict] = []
for idx, chunk in enumerate(chunks):
token_count = len(encoding.encode(chunk))
rows.append({
"filename": p.name,
"relative_path": relative_path,
"absolute_path": absolute_path,
"chunk_index": idx,
"chunk_text": chunk,
"token_count": token_count,
"modified_time": modified_time,
"size_bytes": stat.st_size,
})
return rows
Étape 2 : Création d'embeddings
Vous pouvez générer des embeddings à l'aide de fournisseurs de services cloud (par exemple, OpenAI) ou de modèles locaux via Ollama (par exemple, nomic-embed-text). Dans cet exemple, nous utiliserons le modèle « text-embedding-3-small » d'OpenAI pour incorporer chaque bloc et enregistrer les résultats au format Parquet en vue d'une intégration ultérieure dans la base de données IRIS Vector DB.
from openai import OpenAI
import pandas as pd
def embed_and_save_parquet(input_parquet_path: str, output_parquet_path: str):
"""
Loads a Parquet file, creates embeddings for the 'chunk_text' column using
OpenAI's small embedding model, and saves the result to a new Parquet file.
Args:
input_parquet_path (str): Path to the input Parquet file containing 'chunk_text'.
output_parquet_path (str): Path to save the new Parquet file with embeddings.
openai_api_key (str): Your OpenAI API key.
"""
key = os.getenv("OPENAI_API_KEY")
if not key:
print("ERROR: OPENAI_API_KEY environment variable is not set.", file=sys.stderr)
sys.exit(1)
try:
# Téléchargement du fichier Parquet
df = pd.read_parquet(input_parquet_path)
# Initiation du client OpenAI
client = OpenAI(api_key=key)
# Generation des embeddings pour chaque chunk_text
embeddings = []
for text in df['chunk_text']:
response = client.embeddings.create(
input=text,
model="text-embedding-3-small" # Using the small embedding model
)
embeddings.append(response.data[0].embedding)
# Ajout des embeddings au DataFrame
df['embedding'] = embeddings
# Enregistrement du nouveau DataFrame dans un fichier Parquet
df.to_parquet(output_parquet_path, index=False)
print(f"Embeddings generated and saved to {output_parquet_path}")
except FileNotFoundError:
print(f"Error: Input file not found at {input_parquet_path}")
except KeyError:
print("Error: 'chunk_text' column not found in the input Parquet file.")
except Exception as e:
print(f"An unexpected error occurred: {e}")
Étape 3 : Regroupement du traitement des données
Maintenant, il est temps d'exécuter le pipeline. Dans cet exemple, nous allons charger et segmenter la documentation de Business Service, générer des vecteurs d'encodage et enregistrer les résultats au format Parquet en vue de leur ingestion dans IRIS.
CHUNK_SIZE_TOKENS = 300
CHUNK_OVERLAP_TOKENS = 50
ENCODING_NAME="cl100k_base"
current_file_path = Path(file).resolve()
load_documentation_to_parquet(input_dir=current_file_path.parent / <span class="mention">"Documentation"</span> / <span class="mention">"BusinessService"</span>,
output_file=current_file_path.parent / <span class="mention">"BusinessService.parquet"</span>,
chunk_size=CHUNK_SIZE_TOKENS,
chunk_overlap=CHUNK_OVERLAP_TOKENS,
encoding_name=ENCODING_NAME)
embed_and_save_parquet(input_parquet_path=current_file_path.parent / <span class="mention">"BusinessService.parquet"</span>,
output_parquet_path=current_file_path.parent / <span class="mention">"BusinessService_embedded.parquet"</span>)
Une ligne de notre fichier Parquet final pour le service métier ressemblera à ceci :
{"filename":"FileInboundAdapters.txt","relative_path":"Documentation\BusinessService\Adapters\FileInboundAdapters.txt","absolute_path":"C:\Users\…\Documentation\BusinessService\Adapters\FileInboundAdapters.txt","chunk_index":0,"chunk_text":"Settings for the File Inbound Adapter\nProvides reference information for settings of the file inbound adapter, EnsLib.File.InboundAdapterOpens in a new tab. You can configure these settings after you have added a business service that uses this adapter to your production.\nSummary","token_count":52,"modified_time":"2025-11-25T18:34:16.120336+00:00","size_bytes":13316,"embedding":[-0.02851865254342556,0.01860344596207142,…,0.0135544464207155]}
Section 2 : Utilisation de la recherche vectorielle IRIS
Étape 4 : Téléchargement de vos embeddings vers IRIS
Sélectionnez l'espace de noms IRIS et le nom de la table que vous utiliserez pour stocker les embeddings. (Le script ci-dessous créera la table si elle n'existe pas encore.) Utilisez ensuite le pilote Python DB-API d'InterSystems IRIS pour insérer les blocs et leurs embeddings.
La fonction ci-dessous permet de lire un fichier Parquet contenant le texte segmenté et les embeddings, de normaliser la colonne d’embedding en une liste de nombres flottants sérialisable au format JSON, de se connecter à IRIS, de créer la table de destination si elle n’existe pas (au moyen d’une colonne VECTOR(FLOAT, 1536), où 1536 correspond au nombre de dimensions dans l’embedding) puis insère chaque ligne à l'aide de TO_VECTOR(?) dans une instruction SQL paramétrée. Elle confirme la transaction en cas de succès, consigne la progression et nettoie la connexion, tout en annulant les modifications en cas d'erreurs de base de données.
import iris # Le pilote Python DB-API d'InterSystems IRIS
import pandas as pd
import numpy as np
import json
from pathlib import Path
# --- Configuration ---
PARQUET_FILE_PATH = "your_embeddings.parquet"
IRIS_HOST = "localhost"
IRIS_PORT = 8881
IRIS_NAMESPACE = "VECTOR"
IRIS_USERNAME = "superuser"
IRIS_PASSWORD = "sys"
TABLE_NAME = "AIDemo.Embeddings" # D'être conforme à la table créée dans IRIS
EMBEDDING_DIMENSIONS = 1536 # D'être conformes aux dimensions des embeddings utilisés
def upload_embeddings_to_iris(parquet_path: str):
"""
Reads a Parquet file with 'chunk_text' and 'embedding' columns
and uploads them to an InterSystems IRIS vector database table.
"""
# 1. Téléchargement des données du fichier Parquet à l'aide de pandas
try:
df = pd.read_parquet(parquet_path)
if 'chunk_text' not in df.columns or 'embedding' not in df.columns:
print("Error: Parquet file must contain 'chunk_text' and 'embedding' columns.")
return
except FileNotFoundError:
print(f"Error: The file at {parquet_path} was not found.")
return
# Les embeddings doivent être dans un format compatible avec la fonction TO_VECTOR (liste de flottants)
# Parquet enregistre souvent les tables numpy sous forme de listes
if isinstance(df['embedding'].iloc[0], np.ndarray):
df['embedding'] = df['embedding'].apply(lambda x: x.tolist())
print(f"Loaded {len(df)} records from {parquet_path}.")
# 2. Établissement d'une connexion à InterSystems IRIS
connection = None
try:
conn_string = f"{IRIS_HOST}:{IRIS_PORT}/{IRIS_NAMESPACE}"
connection = iris.connect(conn_string, IRIS_USERNAME, IRIS_PASSWORD)
cursor = connection.cursor()
print("Successfully connected to InterSystems IRIS.")
# Création de la table d'embedding si elle n'existe pas
cursor.execute(f"""
CREATE TABLE IF NOT EXISTS {TABLE_NAME} (
ID INTEGER IDENTITY PRIMARY KEY,
chunk_text VARCHAR(2500), embedding VECTOR(FLOAT, {EMBEDDING_DIMENSIONS})
)"""
)
# 3. Préparation de l'instruction de SQL INSERT
# InterSystems IRIS utilise la fonction TO_VECTOR à l'aide de SQL pour insérer des données vectorielles
insert_sql = f"""
INSERT INTO {TABLE_NAME} (chunk_text, embedding)
VALUES (?, TO_VECTOR(?))
"""
# 4. Itération et insertion des données
count = 0
for index, row in df.iterrows():
text = row['chunk_text']
# Conversion de la liste de flottants en une chaîne JSON, ce qui est requis par la fonction TO_VECTOR lors de l'utilisation de la DB-API
vector_json_str = json.dumps(row['embedding'])
cursor.execute(insert_sql, (text, vector_json_str))
count += <span class="mention">1</span>
<span class="mention">if</span> count % <span class="mention">100</span> == <span class="mention">0</span>:
print(<span class="mention">f"Inserted </span><span class="mention">{count}</span> rows...")
<span class="mention"># Validation de la transaction</span>
connection.commit()
print(<span class="mention">f"Data upload complete. Total rows inserted: </span><span class="mention">{count}</span>.")
<span class="mention">except</span> iris.DBAPIError <span class="mention">as</span> e:
print(<span class="mention">f"A database error occurred: </span><span class="mention">{e}</span>")
<span class="mention">if</span> connection:
connection.rollback()
<span class="mention">except</span> Exception <span class="mention">as</span> e:
print(<span class="mention">f"An unexpected error occurred: </span><span class="mention">{e}</span>")
<span class="mention">finally</span>:
<span class="mention">if</span> connection:
connection.close()
print(<span class="mention">"Database connection closed."</span>)
Exemple d'utilisation :
current_file_path = Path(file).resolve()
upload_embeddings_to_iris(current_file_path.parent / "BusinessService_embedded.parquet")
Étape 5 : Création de votre fonctionnalité de recherche incorporée
Nous créerons ensuite une fonction de recherche qui intègre la requête de l’utilisateur, effectue une recherche par similarité vectorielle dans InterSystems IRIS via l’API Python DB‑et renvoie les meilleurs‑résultats (top-k) de notre table d’embeddings.
La fonction d’exemple ci-dessous lit un fichier Parquet contenant des segments de texte et leurs embeddings correspondants, puis charge ces données dans la table de stockage vectoriel d’InterSystems IRIS. Elle valide d'abord le fichier Parquet et normalise le format d'encodage en une chaîne de type tableau JSON compatible avec la fonction TO_VECTOR d'IRIS. Après avoir établi une connexion à IRIS, la fonction crée la table cible si elle n'existe pas, prépare une instruction SQL INSERT paramétrée et parcourt chaque ligne pour insérer le texte du segment et son encodage. Enfin, elle confirme la transaction, consigne la progression et assure une gestion correcte des erreurs ainsi que le nettoyage de la connexion à la base de données.
import iris
from typing import List
import os
from openai import OpenAI
# --- Configuration ---
PARQUET_FILE_PATH = "your_embeddings.parquet"
IRIS_HOST = "localhost"
IRIS_PORT = 8881
IRIS_NAMESPACE = "VECTOR"
IRIS_USERNAME = "superuser"
IRIS_PASSWORD = "sys"
TABLE_NAME = "AIDemo.Embeddings" # D'être conforme à la table créée dans IRIS
EMBEDDING_DIMENSIONS = 1536
MODEL = "text-embedding-3-small"
def get_embedding(text: str, model: str, client) -> List[float]:
# Normalisation des sauts de ligne et conversion en chaîne de caractères
payload = [("" if text is None else str(text)).replace("\n", " ") for _ in range(1)]
resp = client.embeddings.create(model=model, input=payload, encoding_format="float")
return resp.data[0].embedding
def search_embeddings(search: str, top_k: int):
print("-------RAG--------")
print(f"Searching IRIS vector store for: ", search)
key = os.getenv("OPENAI_API_KEY")
client = OpenAI(api_key=key)
# 2. Établissement d'une connexion à InterSystems IRIS
connection = None
try:
conn_string = f"{IRIS_HOST}:{IRIS_PORT}/{IRIS_NAMESPACE}"
connection = iris.connect(conn_string, IRIS_USERNAME, IRIS_PASSWORD)
cursor = connection.cursor()
print("Successfully connected to InterSystems IRIS.")
# Intégration d'une requête de recherche
#emb_raw = str(test_embedding) # FOR TESTING
emb_raw = get_embedding(search, model=MODEL, client=client)
emb_raw = str(emb_raw)
#print("EMB_RAW:", emb_raw)
emb_values = []
for x in emb_raw.replace('[', '').replace(']', '').split(','):
try:
emb_values.append(str(float(x.strip())))
except ValueError:
continue
emb_str = ", ".join(emb_values)
# Préparation de l'instruction SQL SELECT
search_sql = f"""
SELECT TOP {top_k} ID, chunk_text FROM {TABLE_NAME}
ORDER BY VECTOR_DOT_PRODUCT((embedding), TO_VECTOR(('{emb_str}'), FLOAT)) DESC
"""
cursor.execute(search_sql)
results = []
row = cursor.fetchone()
while row is not None:
results.append(row[:])
row = cursor.fetchone()
<span class="mention">except</span> iris.DBAPIError <span class="mention">as</span> e:
print(<span class="mention">f"A database error occurred: </span><span class="mention">{e}</span>")
<span class="mention">if</span> connection:
connection.rollback()
<span class="mention">except</span> Exception <span class="mention">as</span> e:
print(<span class="mention">f"An unexpected error occurred: </span><span class="mention">{e}</span>")
<span class="mention">finally</span>:
<span class="mention">if</span> connection:
connection.close()
print(<span class="mention">"Database connection closed."</span>)
print(<span class="mention">"------------RAG Finished-------------"</span>)
<span class="mention">return</span> results
Étape 6 : Ajout du contexte RAG à votre agent
Maintenant que vous avez:
- segmenté et intégré votre documentation,
- téléchargé les embeddings vers IRIS et créé un index vectoriel,
- développé une fonction de recherche pour les requêtes vectorielles IRIS,
il est temps de rassembler le tout dans un chat interactif de type « Retrieval-Augmented Generation » (RAG) à l'aide de l'API OpenAI Responses. Dans cet exemple, nous donnerons à l'agent un accès direct à la fonction de recherche (pour un contrôle plus précis de l'agent), mais cela peut également être réalisé en utilisant une bibliothèque telle que langchain.
Tout d'abord, vous devrez créer vos instructions pour l'agent, en veillant à lui donner accès à la fonction de recherche :
import os
---------------------------- Configuration ----------------------------
MODEL = os.getenv("OPENAI_RESPONSES_MODEL", "gpt-5-nano")
SYSTEM_INSTRUCTIONS = (
"You are a helpful assistant that answers questions about InterSystems "
"business services and related integration capabilities. You have access "
"to a vector database of documentation chunks about business services. "
"\n\n"
"Use the search_business_docs tool whenever the user asks about specific "
"settings, configuration options, or how to perform tasks with business "
"services. Ground your answers in the retrieved context, quoting or "
"summarizing relevant chunks. If nothing relevant is found, say so "
"clearly and answer from your general knowledge with a disclaimer."
)
---------------------------- Définition des outils ----------------------------
TOOLS = [
{
"type": "function",
"name": "search_business_docs",
"description": (
"Searches a vector database of documentation chunks related to "
"business services and returns the most relevant snippets."
),
"parameters": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": (
"Natural language search query describing what you want "
"to know about business services."
),
},
"top_k": {
"type": "integer",
"description": (
"Maximum number of results to retrieve from the vector DB."
),
"minimum": 1,
"maximum": 10,
},
},
"required": ["query", "top_k"],
"additionalProperties": False,
},
"strict": True,
}
]
Nous avons maintenant besoin d'une petite méthode de « routage » pour permettre au modèle d'utiliser notre outil RAG.
La fonction call_rag_tool(name, args) reçoit un appel de fonction émis par l’API OpenAI Responses et le redirige vers notre implémentation locale (l’outil search_business_docs qui encapsule Search.search_embeddings). Elle prend la requête du modèle et top_k, exécute la recherche vectorielle IRIS, puis renvoie une charge utile encodée en JSON‑contenant les meilleurs résultats (identifiants et snippets). Ce JSON converti en chaîne de caractères est important car l’API Responses attend que les sorties des outils soient sous forme de chaînes de caractères ; en formatant les résultats de manière prévisible, nous permettons au modèle d’ancrer facilement sa réponse finale dans la documentation récupérée. Si un nom d’outil inconnu est demandé, la fonction renvoie une charge utile d’erreur afin que le modèle puisse la gérer de manière appropriée.
def call_rag_tool(name: str, args: Dict[str, Any]) -> str:
"""Route function calls from the model to our local Python implementations.
Currently only supports the search_business_docs tool, which wraps
Search.search_embeddings.
The return value must be a string. We will JSON-encode a small structure
so the model can consume the results reliably.
"""
if name == "search_business_docs":
query = args.get("query", "")
top_k = args.get("top_k", "")
results = search_embeddings(query, top_k)
# Chaque ligne devrait se présenter sous la forme suivante (ID, chunk_text)
formatted: List[Dict[str, Any]] = []
for row in results:
if not row:
continue
# Il convient de prévoir une solution de secours si la longueur/structure des lignes est modifiée
doc_id = row[0] if len(row) > 0 else None
text = row[1] if len(row) > 1 else None
formatted.append({"id": doc_id, "text": text})
payload = {"query": query, "results": formatted}
return json.dumps(payload, ensure_ascii=False)
# Outil inconnu ; renvoie un message d'erreur structuré
return json.dumps({"error": f"Unknown tool name: {name}"})
Maintenant que nous avons notre outil RAG, nous pouvons commencer à travailler sur la logique de la boucle de chat. Tout d’abord, il nous faut un assistant pour extraire de manière fiable la réponse finale du modèle ainsi que les résultats des outils à partir de l’API OpenAI Responses. La fonction extract_answer_and_sources(response) parcourt les éléments response.output contenant les résultats du modèle et les concatène en une seule chaîne de réponse. Elle collecte également les charges utiles function_call_output (le JSON renvoyé par notre outil RAG), les analyse et les expose sous la forme de tool_context pour plus de transparence et de facilité de débogage. La fonction analyse la sortie du modèle en une structure compacte : {« answer »: ..., « tool_context »: [...]}.
def extract_answer_and_sources(response: Any) -> Dict[str, Any]:
"""Extract a structured answer and optional sources from a Responses API object.
We don't enforce a global JSON response schema here. Instead, we:
- Prefer the SDK's output_text convenience when present
- Fall back to concatenating any output_text content parts
- Also surface any tool-call-output payloads we got back this turn as
tool_context for debugging/inspection.
"""
answer_text = ""
# Solution privilégiée : facilité d'utilisation du SDK
if hasattr(response, "output_text") and response.output_text:
answer_text = response.output_text
else:
# Solution de secours : parcourir les éléments de sortie
parts: List[str] = []
for item in getattr(response, "output", []) or []:
if getattr(item, "type", None) == "message":
for c in getattr(item, "content", []) or []:
if getattr(c, "type", None) == "output_text":
parts.append(getattr(c, "text", ""))
answer_text = "".join(parts)
# Récupération de tous les éléments de type function_call_output pour les rendre visibles
tool_context: List[Dict[str, Any]] = []
for item in getattr(response, "output", []) or []:
if getattr(item, "type", None) == "function_call_output":
try:
tool_context.append({
"call_id": getattr(item, "call_id", None),
"output": json.loads(getattr(item, "output", "")),
})
except Exception:
tool_context.append({
"call_id": getattr(item, "call_id", None),
"output": getattr(item, "output", ""),
})
return {"answer": answer_text.strip(), "tool_context": tool_context}
Avec extract_answer_and_sources, nous pouvons construire la boucle de chat complète pour orchestrer une conversation en deux‑phases, impliquant l'appel d'outils‑, avec l'API OpenAI Responses. La fonction chat_loop() exécute une interface CLI interactive : elle recueille la question de l'utilisateur, envoie une première requête contenant des instructions système et l'outil search_business_docs, puis examine tous les éléments de type function_call émis par le modèle. Pour chaque appel de fonction, elle exécute notre outil RAG local (call_rag_tool, qui encapsule search_embeddings) et ajoute le résultat à la conversation sous la forme d’un élément function_call_output. Elle effectue ensuite une deuxième requête demandant au modèle d’utiliser ces résultats d’outils à l’aide de ceux-ci pour produire une réponse étayée, analyse cette réponse via extract_answer_and_sources, puis l’affiche. La boucle conserve le contexte d'exécution dans input_items afin que chaque tour puisse s'appuyer sur les messages précédents et les résultats des outils.
def chat_loop() -> None:
"""Run an interactive CLI chat loop using the OpenAI Responses API.
The loop supports multi-step tool-calling:
- First call may return one or more function_call items
- We execute those locally (e.g., call search_embeddings)
- We send the tool outputs back in a second responses.create call
- Then we print the model's final, grounded answer
"""
key = os.getenv("OPENAI_API_KEY")
if not key:
raise RuntimeError("OPENAI_API_KEY is not set in the environment.")
client = OpenAI(api_key=key)
print("\nBusiness Service RAG Chat")
print("Type 'exit' or 'quit' to stop.\n")
# Liste récapitulative des entrées (messages + appels d'outils + sorties d'outils) pour le contexte
input_items: List[Dict[str, Any]] = []
while True:
user_input = input("You: ").strip()
if not user_input:
continue
if user_input.lower() in {"exit", "quit"}:
print("Goodbye.")
break
# Ajout d'un message d'utilisateur
input_items.append({"role": "user", "content": user_input})
# Premier appel : le modèle décide s'il convient d'appeler des outils
response = client.responses.create(
model=MODEL,
instructions=SYSTEM_INSTRUCTIONS,
tools=TOOLS,
input=input_items,
)
# Les éléments de sortie du modèle sont enregistrés dans notre conversation en cours
input_items += response.output
# 2) Exécution des appels de fonction
# L'API Responses renvoie des éléments function_call dans response.output.
for item in response.output:
if getattr(item, "type", None) != "function_call":
continue
name = getattr(item, "name", None)
raw_args = getattr(item, "arguments", "{}")
try:
args = json.loads(raw_args) if isinstance(raw_args, str) else raw_args
except json.JSONDecodeError:
args = {"query": user_input}
result_str = call_rag_tool(name, args or {})
# Le résultat de l'outil est ajouté en tant que function_call_output
input_items.append(
{
"type": "function_call_output",
"call_id": getattr(item, "call_id", None),
"output": result_str,
}
)
# 3) Deuxième appel : le modèle est invité à répondre en utilisant les résultats des outils
followup = client.responses.create(
model=MODEL,
instructions=(
SYSTEM_INSTRUCTIONS
+ "\n\nYou have just received outputs from your tools. "
+ "Use them to give a concise, well-structured answer."
),
tools=TOOLS,
input=input_items,
)
structured = extract_answer_and_sources(followup)
print("Agent:\n" + structured["answer"] + "\n")
Ça y est ! Vous venez de créer un pipeline RAG complet propulsé par IRIS Vector Search. Bien que cet exemple se soit concentré sur un cas d'utilisation simple, IRIS Vector Search ouvre la voie à de nombreuses autres possibilités:
- Base de connaissances pour des agents de service client plus complexes
- Stockage du contexte conversationnel pour des agents hyper-personnalisés
- Détection d'anomalies dans les données textuelles
- Analyse par regroupement pour les données textuelles
J'espère que ce guide vous a fourni un point de départ solide pour explorer la recherche vectorielle et créer vos propres applications basées sur l'IA avec InterSystems IRIS !
Le code source complet est disponible ici :