Code
OPENAI_KEY = "<OPENAI_API_KY>"
SHEET_URL = "https://docs.google.com/spreadsheets/d/000000000000"Лекція з практичними прикладами у ChatGPT
Юрій Клебан
20 жовтня 2025 р.
Мета: ознайомити з поняттям AI-агента, його архітектурою та життєвим циклом; показати підходи до створення агентів для автоматизації аналітичних і рутинних бізнес-процесів за допомогою Python-екосистеми, фреймворку LangChain, а також «no-code/low-code» сервісів на кшталт OpenAI Agent Builder, Make і n8n. Після лекції ви розумітимете відмінності між реактивними та планувальними агентами, місце пам’яті, інструментів і середовища, умітимете формулювати завдання та межі відповідальності агента, а також проєктувати сценарії контролю якості, безпеки й етики в автоматизованих пайплайнах.
Після заняття ви зможете:
Що таке AI-агент. Агент — це система на основі ШІ, здатна самостійно сприймати інформацію, приймати рішення та виконувати дії для досягнення мети. Концептуально він поєднує LLM як «мозок» (розуміє запит, планує кроки), інструменти (виконують конкретні дії: пошук, робота з файлами, виклики API, бази даних), пам’ять (зберігає контекст діалогу, попередній досвід, проміжні артефакти) і середовище (джерела та системи, у яких агент діє). Така композиція створює «розумний оркестратор», що уміє ставити підзадачі, викликати потрібні засоби та перевіряти результати.
Робочий цикл агента. Типова петля — Observe → Think/Plan → Act → Check → Learn. Спершу агент сприймає запит і контекст, далі планує послідовність кроків (які інструменти, у якій черзі), виконує дії (інколи з проміжними підзапитами), перевіряє проміжні результати (еталонами, правилами, підрахунками) та оновлює пам’ять/стратегію. У складніших сценаріях додають розділення на «планувальника» і «виконавця», або використовують багатоагентні схеми (модератор, дослідник, виконавець).
Спеціалізовані GPTs: ігрова демонстрація. Прості ігри («Слова зі слів») демонструють здатність агента: чітко визначати правила, контролювати хід, повертати валідацію результату та адаптувати інструкції під користувача. Навчальна цінність — у вмінні формулювати інструкції й перевірки: агент не лише генерує варіанти, а й підтверджує коректність (існування слів, дотримання обмежень), позначає помилки й пропонує ескалацію складності. Це переноситься на «серйозні» задачі — екстракція даних, аудит звітів, контроль бізнес-правил.
Створення AI-агентів у Python. Етапи: визначити роль і межі відповідальності; описати інструментарій (які джерела/АПІ дозволені, яким чином перевіряємо відповіді); спроєктувати пам’ять (короткочасну для розмови, довготривалу для фактів/налаштувань, епізодичну для «історії кейсу»); визначити політики безпеки (чутливі дані, приватність, логування). Проєктний результат — паспорт агента: місія, вхід/вихід, індикатори якості (SLA/SLI), обмеження вартості/затримки, сценарії відмови та ескалації на людину.
У цьому прикладі показано, як за допомогою API OpenAI створити простий генератор хоку українською мовою. Код послідовно ініціалізує клієнт, надсилає запит до моделі й виводить результат.
Цей блок імпортує необхідні бібліотеки. Модуль openai надає інтерфейс для звернення до API, а config зберігає ключ доступу до сервісу (у змінній OPENAI_KEY).
Створюємо екземпляр клієнта OpenAI, передаючи йому ключ API.
Це дозволяє виконувати запити до моделей OpenAI із заданими параметрами.
У цьому фрагменті формується запит до моделі GPT-4o-mini.
Ми передаємо повідомлення з інструкцією: “напиши хоку про AI українською мовою”.
Отримана відповідь зберігається у змінній completion і одразу виводиться на екран.
Аргумент store=True дозволяє зберегти історію виклику в системі OpenAI для подальшого аналізу.
Цикл проходить рядки тексту відповіді моделі та виводить кожен окремо. > Це забезпечує охайний формат виведення, де кожен рядок хоку з’являється на новому рядку.
Цей приклад демонструє базовий сценарій взаємодії з API OpenAI для генерації поетичних текстів.
Його можна розширити для створення інтерфейсу або інтеграції в навчальний проєкт.
У цьому фрагменті імпортуються необхідні бібліотеки для роботи з API OpenAI. Модуль openai використовується для створення запитів до моделей, а інші бібліотеки можуть забезпечувати конфігурацію або обробку даних.
Цей код є допоміжним або демонстраційним фрагментом, який підтримує основну логіку чат-бота.
class SimpleChatAgent:
def __init__(self):
# Store conversation history
self.conversation_history = [
{"role": "system", "content": "Ти консультант, що відповідає на запитання про AI українською мовою та дуже піклуєшся про етичне використання AI у освіті та науці. Нагадуй про чесність у користуванні AI постійно."}
]
def get_response(self, user_input):
# Add user's message to conversation history
self.conversation_history.append({"role": "user", "content": user_input})
try:
# Make API call to OpenAI
response = openai.chat.completions.create(
model="gpt-4o-mini",
messages=self.conversation_history,
max_tokens=250, # Limit response length
temperature=0.7 # Controls creativity (0-1)
)
# Get AI's response
ai_response = response.choices[0].message.content
# Add AI's response to conversation history
self.conversation_history.append({"role": "assistant", "content": ai_response})
return ai_response
except Exception as e:
return f"Sorry, I encountered an error: {str(e)}"Цей код створює запит до моделі GPT, що імітує поведінку чат-бота. У параметрі messages визначається роль користувача та його запит. Результат зберігається у змінній completion, з якої потім можна отримати текст відповіді.
# Create instance of our AI agent
agent = SimpleChatAgent()
print("Вітаю у чаті!")
print("Введи 'quit', якщо бажаєш вийти")
while True:
# Get user input
user_input = input("\nYou: ")
# Check if user wants to quit
if user_input.lower() == 'quit':
print("Бувай!")
break
# Get and display AI response
response = agent.get_response(user_input)
print(f"AI: {response}")Цей блок забезпечує інтерактивність: користувач може вводити текстові запити до чат-бота. Код приймає введений текст і надсилає його моделі для отримання відповіді.
Імпорт необхідних бібліотек. Модуль openai використовується для доступу до моделей, а інші — для налаштування або зчитування конфігурації.
class FoodMeasureAgent:
def __init__(self):
# Initialize OpenAI client with API key
self.client = openai.OpenAI(api_key=C.OPENAI_KEY)
self.conversation_history = [
{"role": "system",
"content": "Конвертуй кожну одиницю харчових мір, яку надає користувач, у грами. Виводь лише значення у грамах. Наприклад, якщо запитують про склянку рису, відповідь має бути лише: '195 грамів'"
}
]
def get_response(self, user_input):
# Add user message to conversation history
self.conversation_history.append({"role": "user", "content": user_input})
# Get response from OpenAI
response = self.client.chat.completions.create(
model="gpt-4o-mini", # You can change to "gpt-4" if you have access
messages=self.conversation_history,
max_tokens=1000,
temperature=0.7
)
# Extract the assistant's response
assistant_response = response.choices[0].message.content.strip()
# Add assistant's response to conversation history
self.conversation_history.append({"role": "assistant", "content": assistant_response})
return assistant_responseСтворюється екземпляр клієнта OpenAI, використовуючи API-ключ із конфігураційного файлу. Цей клієнт дозволяє відправляти запити до моделей OpenAI.
agent = FoodMeasureAgent()
print("Ласкаво просимо! Введіть одиницю виміру продукту — і я конвертую її в грами. Щоб вийти, наберіть 'quit'.")
while True:
user_input = input("\nYou: ")
print(f"\nYou: {user_input}")
if user_input.lower() == 'quit':
print("Бувай!")
break
response = agent.get_response(user_input)
print(f"AI: {response}")Забезпечується інтерактивне введення користувача. Користувач вводить запит (наприклад, конвертацію), який надсилається моделі.
Цей код реалізує допоміжну логіку антискам-системи — обробку даних, підготовку запиту або форматування відповіді.
Ініціалізація клієнта OpenAI — ключовий крок для взаємодії з моделлю GPT. Ключ API має бути захищений і не зберігатися у відкритому коді.
# --- Agent 1: Scam Detector ---
class ScamDetector:
def __init__(self, client):
self.client = client
def analyze_email(self, email_text):
response = self.client.chat.completions.create(
# Формування запиту до моделі для аналізу повідомлення на шахрайство
model="gpt-4o-mini",
messages=[
{"role": "system", "content": 'Твоє завдання — класифікувати електронні листи як шахрайські або ні, на основі наданого тексту. Відповідай лише "YES" (якщо це шахрайство) або "NO" (якщо це не шахрайство).'},
{"role": "user", "content": email_text}
],
max_tokens=10,
temperature=0.1
)
return response.choices[0].message.content.strip()Цей блок коду формує запит до моделі OpenAI. У prompt описано задачу — розпізнати ознаки шахрайства та підготувати відповідь-шаблон, яка змусить шахрая витратити час. Модель аналізує контент, шукає ключові ризикові патерни (прохання про гроші, посилання, емоційний тиск). Рекомендація: зробити prompt структурованим — попросити модель повертати JSON з полями: label, confidence, reason, reply_template. Це спростить обробку результатів.
# --- Agent 2: Time Waster ---
class TimeWaster():
def __init__(self, client, signature_name="Юрій"):
self.client = client
self.signature_name = signature_name
def craft_response(self, email_text):
response = self.client.chat.completions.create(
# Формування запиту до моделі для аналізу повідомлення на шахрайство
model="gpt-4o-mini",
messages=[
{"role": "system", "content": f"Напиши довгу відповідь на цей шахрайський електронний лист (на підставі тексту, який надасть користувач), щоб затримати та марнувати час шахрая. Не повідомляй жодної особистої інформації або реальних контактних даних. Мета — утримати шахрая в листуванні якомога довше: став заплутані уточнювальні питання, вимагай зайвих деталей/документів, проси пояснити суперечності, показуй надмірну зацікавленість, погоджуйся на дивні процедури тощо — але ніякої реальної інформації про себе. Підписуйся як {self.signature_name} (замінити на потрібне ім’я при використанні). Формат відповіді: лише текст листа (без пояснень або інструкцій для користувача)."},
{"role": "user", "content": email_text}
],
max_tokens=500,
temperature=0.1 # increase temperature to make response more crazy
)
return response.choices[0].message.content.strip()Цей блок коду формує запит до моделі OpenAI. У prompt описано задачу — розпізнати ознаки шахрайства та підготувати відповідь-шаблон, яка змусить шахрая витратити час. Модель аналізує контент, шукає ключові ризикові патерни (прохання про гроші, посилання, емоційний тиск). Рекомендація: зробити prompt структурованим — попросити модель повертати JSON з полями: label, confidence, reason, reply_template. Це спростить обробку результатів.
# --- Multi-Agent System ---
class EmailHandler:
def __init__(self, client):
self.scam_detector = ScamDetector(client)
self.time_waster = TimeWaster(client)
def process_email(self, email_text):
# Step 1: Detect scam
status = self.scam_detector.analyze_email(email_text)
print(f"Email: {email_text}")
print(f"Scam Detector: {status}")
# Step 2: If scam, waste time
if status == "YES":
reply = self.time_waster.craft_response(email_text)
print(f"Відповідь шахраям: {reply}")
else:
print("Відповідь не створено.")
print("---")Цей код реалізує допоміжну логіку антискам-системи — обробку даних, підготовку запиту або форматування відповіді.
# Run the system
handler = EmailHandler(client)
# Simulated email examples
emails = [
"Шановний користувачу, ви були обрані для отримання спеціального грошового гранту у розмірі 250 000 грн. Для переказу коштів нам потрібен номер вашої картки та ПІН-код.",
"Привіт! Не забудь, сьогодні в нас зустріч з новими клієнтами о 10:30. Зустрічаємось в переговорній №2.",
"Вітаю! Я юрист покійного мільярдера, і ви зазначені в його заповіті як спадкоємець. Терміново зв’яжіться зі мною для оформлення спадщини.",
"Аллллоо! Ти скинеш фінальну версію презентації до 18:00? Ми плануємо все зібрати до завтра.",
"Ваш акаунт у ПриватБанку заблоковано. Щоб відновити доступ, натисніть на посилання нижче та введіть усі особисті дані для підтвердження."
]Цей код реалізує допоміжну логіку антискам-системи — обробку даних, підготовку запиту або форматування відповіді.
Цей код реалізує допоміжну логіку антискам-системи — обробку даних, підготовку запиту або форматування відповіді.
Що таке LangChain. Це фреймворк (Python/JavaScript) для побудови застосунків і агентів на базі LLM: він поєднує ланцюги мислення/дій (chains), зовнішні інструменти, пам’ять, ретрієвал (доступ до файлів/БД/пошуку) та агентні петлі (вибір наступної дії залежно від проміжного результату). Перевага — «конструктора» з готових блоків: промпт-шаблони, parsers, memory stores, tool adapters, retrievers; можливість додавати власні інструменти й правила валідації, будувати керовані сценарії із прозорим логуванням.
Практичний кейс: агент для запиту «Прошу підписати мені індивідуальний план». Задача — автоматизувати обробку вхідних листів і підготовку відповідей.
Вхід: поштові повідомлення; ознаки: відправник, тема, тіло, вкладення.
Ціль: знайти запити на підписання ІП, зібрати список студентів, підготувати шаблон-відповідь (інструкції, терміни, перелік кроків).
Інструменти: доступ до пошти (читання), шаблонізатор відповідей, таблиця/CRM для реєстру, календар/таск-менеджер для нагадувань.
Пам’ять: збереження вже оброблених листів, статусів (очікує документів/підписано/відхилено), історії комунікації.
Перевірки: чи справді лист про ІП (класифікація), чи всі обов’язкові поля заповнено, чи немає дубля.
Вихід: чернетки відповідей (для людини), оновлений список студентів, журнал рішень.
Теоретичні нюанси. Визначаємо порогові правила для класифікатора теми; політики ескалації, якщо невпевненість висока; вікна пам’яті для контексту; часові обмеження; облік вартості викликів; вимоги до приватності. Агент не відправляє листи самостійно без «human-in-the-loop» — це контроль ризиків і відповідальності.
Автоматизований GPT-помічник для обробки листів студентів щодо індивідуальних планів (Gmail → LangChain → Google Sheets)
Цей ноутбук: - читає листи з Gmail; - класифікує їх за допомогою GPT (LangChain 1.x); - витягує ПІБ та групу студента; - підбирає шаблон відповіді з аркуша “Шаблони” існуючого документа Google Sheets; - формує текст відповіді (GPT); - створює чернетку-відповідь у Gmail; - записує результат у аркуш “Запити” (існуючий документ Google Sheets).
⚙️ Пайплайн зібраний у єдиний Runnable Chain (LangChain 1.x).
За потреби розкоментуйте та виконайте:
import os, re, json, datetime, base64, string
# Імпорт необхідних бібліотек (робота з API, обробка пошти, AI-моделі)
import pandas as pd
# Імпорт необхідних бібліотек (робота з API, обробка пошти, AI-моделі)
from email.mime.text import MIMEText
from rapidfuzz import process
from dotenv import load_dotenv
from google.auth.transport.requests import Request
from google.oauth2.credentials import Credentials
from google.oauth2 import service_account # not used, but handy for alt flows
from google_auth_oauthlib.flow import InstalledAppFlow
from googleapiclient.discovery import build
# Підключення до Gmail API для читання та надсилання листів
import gspread
# Імпорт необхідних бібліотек (робота з API, обробка пошти, AI-моделі)
from gspread_formatting import *
# LangChain (modern, 1.x style)
from langchain_openai import ChatOpenAI
from langchain.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnableLambda, RunnableSequenceЦей блок реалізує допоміжну функціональність — наприклад, обробку тексту, логування або контроль потоку.
configМи використовуємо ваш локальний модуль
config.pyз полями: -OPENAI_KEY— OpenAI API ключ -SHEET_URL— URL на вже існуючий документ Google Sheets (“Індивідуальні плани”)
import config as C
# Імпорт необхідних бібліотек (робота з API, обробка пошти, AI-моделі)
OPENAI_API_KEY = C.OPENAI_KEY
MODEL_NAME = "gpt-4o-mini" # можна змінити на "gpt-4o", якщо доступно
os.environ["OPENAI_API_KEY"] = OPENAI_API_KEY
print("🔑 OpenAI API Key завантажено")
print("🧠 Модель:", MODEL_NAME)Цей блок реалізує допоміжну функціональність — наприклад, обробку тексту, логування або контроль потоку.
Використовується існуючий токен з
secret/token.jsonта OAuth-клієнтsecret/client_secret.json.
Якщоtoken.jsonвідсутній або прострочений — відбудеться інтерактивний OAuth-потік у браузері.
SCOPES = [
"https://www.googleapis.com/auth/gmail.readonly",
"https://www.googleapis.com/auth/gmail.compose",
"https://www.googleapis.com/auth/spreadsheets"
]
creds = None
if os.path.exists("secret/token.json"):
creds = Credentials.from_authorized_user_file("secret/token.json", SCOPES)
if not creds or not creds.valid:
if creds and creds.expired and creds.refresh_token:
creds.refresh(Request())
else:
flow = InstalledAppFlow.from_client_secrets_file("secret/client_secret.json", SCOPES)
creds = flow.run_local_server(port=0)
with open("secret/token.json", "w") as token:
token.write(creds.to_json())
gmail_service = build("gmail", "v1", credentials=creds)
gc = gspread.authorize(creds)
print("✅ Авторизація Gmail і Sheets виконана")
# Підключення до Gmail API для читання та надсилання листівЦей блок реалізує взаємодію з Gmail API. Він може отримувати вхідні листи, читати теми та тіла повідомлень, а також автоматично надсилати сформовані відповіді. Варто забезпечити фільтрацію (наприклад, лише листи від студентів або з темою ‘індивідуальний план’).
Ініціалізація клієнта OpenAI забезпечує інтеграцію з моделлю для автоматичного створення відповідей. Ключ API має бути збережений у середовищі безпеки, не в коді.
####Підключення до вже існуючого Google Sheets і читання шаблонів
Ми не створюємо нових документів. Використовується URL з
config.SHEET_URL. Обов’язкові аркуші: “Шаблони” (курс, група, шаблон) і “Запити”.
Цей блок реалізує допоміжну функціональність — наприклад, обробку тексту, логування або контроль потоку.
def get_latest_emails(query="subject:план OR індивідуальний план", max_results=30):
"""Читає останні листи за пошуковим запитом.
Повертає список словників з ключами: id, threadId, message_id, from, subject, body.
"""
res = gmail_service.users().messages().list(userId="me", q=query, maxResults=max_results).execute()
messages = res.get("messages", [])
emails = []
for msg in messages:
data = gmail_service.users().messages().get(userId="me", id=msg["id"], format="full").execute()
snippet = data.get("snippet", "")
headers = data["payload"].get("headers", [])
sender = next((h["value"] for h in headers if h["name"].lower()=="from"), "Unknown")
subject = next((h["value"] for h in headers if h["name"].lower()=="subject"), "No subject")
message_id = next((h["value"] for h in headers if h["name"].lower()=="message-id"), None)
emails.append({
"id": msg["id"],
"threadId": data.get("threadId"),
"message_id": message_id,
"from": sender,
"subject": subject,
"body": snippet
})
return emailsdef create_reply_draft(original_email: dict, to_email: str, reply_body: str):
"""Створює чернетку-відповідь у існуючому треді (якщо можливо)."""
# Формуємо MIME-повідомлення
message = MIMEText(reply_body, 'plain', 'utf-8')
message['To'] = to_email
message['Subject'] = f"Re: {original_email.get('subject','')}"
if original_email.get("message_id"):
message['In-Reply-To'] = original_email["message_id"]
message['References'] = original_email["message_id"]
raw_message = base64.urlsafe_b64encode(message.as_bytes()).decode('utf-8')
body = {'message': {'raw': raw_message}}
# Прив'язуємо до треду, якщо він відомий
if original_email.get("threadId"):
body['message']['threadId'] = original_email["threadId"]
gmail_service.users().drafts().create(userId='me', body=body).execute()Використовується підхід з твого ноутбука: спершу шукаємо за групою (низький поріг, як у тебе), потім — за назвою курсу/предмету.
def find_best_template_by_group(group: str):
if not group:
return None
candidates = [f"{c[1]}" for c in templates.keys()]
query = f"{group}"
best, score, _ = process.extractOne(query, candidates) if candidates else (None, 0, None)
if best and score >= 65:
for k, v in templates.items():
if f"{k[1]}" == best:
return v
return None
def find_best_template_by_subject(sbj: str):
if not sbj:
return None
candidates = [f"{c[0]}" for c in templates.keys()]
query = f"{sbj}"
best, score, _ = process.extractOne(query, candidates) if candidates else (None, 0, None)
if best and score >= 65:
for k, v in templates.items():
if f"{k[0]}" == best:
return v
return Noneprompt_analyze = ChatPromptTemplate.from_template("""
Ти — асистент викладача університету.
Завдання:
1) Визнач, чи цей емейл є проханням студента/студентки про підписання індивідуального навчального плану.
2) Якщо так — знайди у тексті:
- ПІБ студента/ки (дозволені формати "Прізвище Ім'я По батькові" або "Прізвище По батькові")
- групу (напр. КН-31, ІТ-22 тощо). Якщо не вдалося ідентифікувати групу - поверни пустий рядок, не намагайся вгадати. Серед доступних груп: {groups}.
- назву курсу / предмета (якщо є), назва курсу повинна бути схожою на {courses} (обери найближчу, якщо співпадіння 0.6 і більше) або не обирай зовсім
- e-mail відправника (якщо є в тексті, інакше використай поле From)
3) Якщо НЕ є таким проханням — поверни літерал `None` (без JSON).
Вхідні дані:
Від: {sender}
Тема: {subject}
Текст: {body}
Відповідь у JSON БЕЗ markdown:
{{
"student_name": "...",
"group": "...",
"subject": "...",
"sender_email": "..."
}}
АБО `None`.
""")
prompt_reply = ChatPromptTemplate.from_template("""
Сформулюй коротку професійну відповідь студенту/студентці, який/яка просить підписати індивідуальний план.
Використай шаблон (адаптуй під конкретні дані):
{template_text}
Повне ім'я студента/студентки: {name}
Група: {group}
Предмет/курс: {subject_name}
❗ Якщо в шаблоні є плейсхолдер `#FIRST_NAME`, заміни його на ім'я у кличному відмінку (якщо можливо).
""")
analyze_chain = prompt_analyze | llm | parser
reply_chain = prompt_reply | llm | parser
Архітектура:
email(dict)
→ analyze (GPT)
→ parse/update state
→ find template (group→subject)
→ reply (GPT)
→ write to Sheets
→ create Gmail draft
# Налаштування: ваша адреса, щоб не обробляти власні листи
MY_EMAIL = "yuriy.kleban@oa.edu.ua" # ← за потреби змініть
def _init_state(email: dict):
"""Ініціалізація стану для ланцюга."""
return {"email": email, "skip": False, "status": None}
def _analyze_step(state: dict):
"""Крок аналізу: GPT класифікація + вилучення полів або skip."""
email = state["email"]
sender = email.get("from", "")
subject = email.get("subject", "")
body = email.get("body", "")
# Не обробляємо власні листи
if MY_EMAIL.lower() in sender.lower():
state.update({"skip": True, "status": "own_email"})
return state
result = analyze_chain.invoke({"sender": sender, "subject": subject, "body": body, "courses": ", ".join(f"{k[0]}" for k in templates.keys()), "groups": ", ".join(f"{k[1]}" for k in templates.keys())}).strip()
if result == "None":
state.update({"skip": True, "status": "not_request"})
return state
# Очікуємо JSON
try:
# Обробка винятків — запобігає зупинці при помилках API або мережі
data = json.loads(result)
except Exception:
# Обробка винятків — запобігає зупинці при помилках API або мережі
state.update({"skip": True, "status": "bad_json"})
return state
# Уніфікація регістру/формату
name = data.get("student_name")
group = data.get("group")
subj = data.get("subject")
sender_email = data.get("sender_email") or sender
# Легка нормалізація
name = string.capwords(name) if isinstance(name, str) else None
group = group.upper() if isinstance(group, str) else None
state.update({
"student_name": name,
"group": group,
"subject_name": subj,
"sender_email": sender_email,
"status": "analyzed"
})
return state
def _choose_template_step(state: dict):
"""Крок вибору шаблону: спершу група, потім предмет."""
if state.get("skip"):
return state
tpl = None
if state.get("group"):
tpl = find_best_template_by_group(state["group"])
if not tpl and state.get("subject_name"):
tpl = find_best_template_by_subject(state["subject_name"])
if tpl:
state.update({"template_text": tpl, "status": "template_found"})
else:
state.update({"template_text": None, "status": "template_not_found"})
return state
def _generate_reply_step(state: dict):
"""Крок генерації відповіді (GPT), якщо є шаблон."""
if state.get("skip"):
return state
if state.get("status") != "template_found":
state.update({"draft_text": None})
return state
draft_text = reply_chain.invoke({
"template_text": state["template_text"],
"name": state.get("student_name",""),
"group": state.get("group",""),
"subject_name": state.get("subject_name","")
})
state.update({"draft_text": draft_text, "status": "draft_ready"})
return state
def _write_outputs_step(state: dict):
"""Запис у Sheets та створення Gmail чернетки (якщо є draft)."""
# Підключення до Gmail API для читання та надсилання листів
email = state["email"]
if state.get("skip"):
# нічого не пишемо
return state
# Статус за замовчуванням
status_to_write = "Не потребує відповіді"
if state.get("status") == "template_not_found":
status_to_write = "Шаблон не знайдено"
elif state.get("status") == "draft_ready":
status_to_write = "Чернетку створено"
# Створюємо чернетку
try:
# Обробка винятків — запобігає зупинці при помилках API або мережі
create_reply_draft(email, state.get("sender_email",""), state.get("draft_text",""))
except Exception as e:
# Обробка винятків — запобігає зупинці при помилках API або мережі
status_to_write = f"Draft error: {e}"
# Запис у аркуш "Запити"
try:
# Обробка винятків — запобігає зупинці при помилках API або мережі
sheet_requests.append_row([
state.get("student_name",""),
state.get("group",""),
state.get("subject_name",""),
state.get("sender_email",""),
datetime.datetime.now().strftime("%Y-%m-%d %H:%M"),
status_to_write
])
except Exception as e:
# Обробка винятків — запобігає зупинці при помилках API або мережі
# Якщо помилка — до стану
state["status"] = f"Sheets error: {e}"
# Підключення до Google Sheets для збереження результатів (журнал відповідей або статусів)
return stateЦей блок реалізує взаємодію з Gmail API. Він може отримувати вхідні листи, читати теми та тіла повідомлень, а також автоматично надсилати сформовані відповіді. Варто забезпечити фільтрацію (наприклад, лише листи від студентів або з темою ‘індивідуальний план’).
Налаштуйте
MY_EMAILвище (щоб пропускати власні листи).
emails = get_latest_emails(max_results=10)
print(f"📨 Отримано листів: {len(emails)}")
processed = 0
created = 0
skipped = 0
no_template = 0
for em in emails:
#print(em['from'])
state = full_chain.invoke(em)
processed += 1
st = state.get("status","")
if st in ("own_email","not_request","bad_json"):
skipped += 1
elif st == "template_not_found":
no_template += 1
elif st in ("draft_ready",):
created += 1
print("—"*40)
print("Звіт:")
print(" Оброблено:", processed)
print(" Пропущено (не запит / власний лист / bad json):", skipped)
print(" Без шаблону:", no_template)
print(" Чернеток створено:", created)
print("—"*40)
print("✅ Готово.")За потреби відкоригуйте: -
MY_EMAIL(ваша адреса, щоб листи від себе пропускати); - пороги схожості вfind_best_template_by_*; -get_latest_emails(query=...)— змініть пошуковий запит під ваші умови.
Навіщо «без коду». Частину процесів зручніше збирати як workflow: подія-тригер → фільтри → виклики інструментів → перевірки → повідомлення. Це пришвидшує прототипування, дозволяє бізнес-командам самостійно обслуговувати автоматизації та спиратися на корпоративні інтеграції. Проте архітектурні принципи лишаються: чітка роль агента, журнал рішень, політики приватності, ескалації на людину, контроль вартості/затримки.
Кейс: «Казочки на ніч». Ідея — за вхідними даними (тематика, вік дитини, довжина, стиль) агент генерує казку, надсилає її на email і публікує в Notion із підходящими емодзі.
Порівняння підходів. No-code хороші для швидкого MVP і стабільних сценаріїв; код (або LangChain) потрібен, коли складна логіка планування, нестандартні інструменти або тонкі вимоги до якості/трасування.
Ідея. Batch API — режим асинхронної пакетної обробки великої кількості запитів. Замість тисяч окремих викликів передаємо один файл (наприклад, JSONL) і отримуємо результати після завершення. Переваги: менша вартість на обсяг, стабільніший тротлінг і зручність для завдань на кшталт масової класифікації відгуків, розмітки документів, витягу сутностей. Обмеження: довший latency «від заявки до результату», вимоги до ідемпотентності та ретраїв, необхідність чіткої схеми валідної/невалідної відповіді. Для бізнес-пайплайнів важливо мати моніторинг прогресу, «мертві черги» та автоматичні сповіщення.
Приклад: класифікація відгуків (тональність і тема: доставка/підтримка/якість/пакування/інше). Теоретична постановка: готуємо інструкцію агента, обмежуємо формат відповіді (поле name, topic, sentiment), визначаємо політику для неоднозначних випадків («невпевнено» → на ручну валідацію), проєктуємо метрики якості (збіг анотацій, частка «невпевнено», продуктивність на 1000+ записів).
Демо 1–2. «Слова зі слів». Агент керує грою: задає правила, запускає таймер, перевіряє слова, підсвічує помилки, пропонує ускладнення (тематика). Освітній сенс — навчитися формулювати інструкції, перевірки та feedback-loop у легкому середовищі.
Демо 3–4. Творчість і консалтинг. «Хайку українською», «Нудний чат про AI з етичним нагадуванням»: показують рольову інструкцію й тон відповіді, необхідність стабільних політик (нагадування про чесність). Переноситься на корпоративні сценарії (відповіді саппорту з політиками).
Демо 5. Конвертер харчових мір. Агент-конвертер приймає запит у довільній формі, але повертає лише грамове значення — демонстрація суворого формату відповіді, що зручно для автоматизованих ланцюгів (жодного «води», тільки число й одиниці).
Демо 6. Антискам. Двоетапний сценарій: (1) класифікація листа на шахрайство/ні (тільки YES/NO), (2) генерація «довгої відповіді» для затягування часу шахрая без розголошення персональних даних — приклад етичних обмежень і чіткого формату виходу. У виробництві додаються чорні списки, ліміти частоти, правила ескалації.
Демо 7. Batch-класифікація відгуків. Форматований JSON-вихід (name, topic, sentiment), політика для «невпевнених» випадків, узгодженість з анотаціями людини — приклад, як агент інтегрується в аналітичний конвеєр.
Демо 8. LangChain-агент «Підпишіть інд. план». Теоретичний пайплайн: читання пошти → фільтрація листів про ІП → консолідація списку студентів → генерація чернеток відповідей за шаблонами → логування рішень → попередній перегляд людиною.
Демо 9. «Казкар для малюків». Інтеграція з поштою і Notion, політики контенту, публікація з емодзі. Важливо: модуль перевірки заборонених тем і ручне погодження перед розсилкою.
Якість та валідація. Для кожного агента визначаємо індикатори: частка коректних відповідей, дотримання формату, час відгуку, вартість на запит, частка ескалацій на людину. Використовуємо вибіркову перевірку «ground-truth», журналимо вхід/вихід і рішення (з урахуванням приватності), порівнюємо ітерації агента під різними налаштуваннями.
Безпека і приватність. Мінімізуємо дані у промптах (data minimization), деперсоналізуємо приклади, шифруємо журнали, обмежуємо інструменти агента (whitelist API). Для чутливих сценаріїв — незалежний аудит і відмова від повної автоматизації.
Етика й прозорість. Повідомляємо користувачам про участь ШІ, забезпечуємо можливість оскарження рішень, відстежуємо упередження (за групами, сегментами) і документуємо межі застосовності агентів.
---
title: "Автоматизація обробки даних з використанням AI-агентів"
subtitle: "Лекція з практичними прикладами у ChatGPT"
author: "Юрій Клебан"
date: "2025-10-20"
lang: uk
categories: ["Лекції", "Аналітика даних"]
format:
html:
toc: true
toc-location: right
math: mathjax
toc-title: "План лекції"
toc-depth: 3
number-sections: true
code-fold: show
code-tools: true
smooth-scroll: true
execute:
echo: true
warning: false
message: false
---
## Презентація
<iframe src="https://1drv.ms/p/c/0a1340ba71b3f0aa/IQTlknEsIwGkQKPIcow91CgAAWf0j0IWugv00eTIx9byKUg?em=2&wdAr=1.7777777777777777" width="100%" height="400px" frameborder="0">This is an embedded <a target="_blank" href="https://office.com">Microsoft Office</a> presentation, powered by <a target="_blank" href="https://office.com/webapps">Office</a>.</iframe>
## 🎯 Мета заняття та очікувані результати
**Мета:** ознайомити з поняттям AI-агента, його архітектурою та життєвим циклом; показати підходи до створення агентів для автоматизації аналітичних і рутинних бізнес-процесів за допомогою Python-екосистеми, фреймворку LangChain, а також «no-code/low-code» сервісів на кшталт OpenAI Agent Builder, Make і n8n. Після лекції ви розумітимете відмінності між реактивними та планувальними агентами, місце пам’яті, інструментів і середовища, умітимете формулювати завдання та межі відповідальності агента, а також проєктувати сценарії контролю якості, безпеки й етики в автоматизованих пайплайнах.
**Після заняття ви зможете:**
- пояснити складові AI-агента (LLM-мозок, інструменти, пам’ять, середовище) і цикл «сприйняття → план → дія → перевірка → навчання»;
- вибрати доречний тип агента під задачу (помічник, маршрутизатор, екстрактор даних, інтегратор, класифікатор, генератор контенту);
- спроєктувати теоретичну схему агента для аналітики (джерела, інструменти, політики та пороги рішень);
- окреслити ризики (галюцинації, витік даних, етичні обмеження) і запобіжники (людина-в-циклі, валідація, логування та версіонування).
---
## 🧭 План заняття
- **Блок 1. AI-агенти:** що це таке; спеціалізовані GPTs (ігрова демонстрація); створення агентів у Python (концепції, без коду).
- **Блок 2. LangChain:** ключові ідеї; практичний кейс — агент для підготовки відповіді на запит «Прошу підписати мені індивідуальний план».
- **Блок 3. No-code сервіси:** OpenAI Agent Builder, Make, n8n; кейс — «Казочки на ніч» без програмування.
- Додатково: **Batch API** для пакетної обробки та приклади класифікації відгуків.
- Підсумки та Q&A.
---
## 🧠 Блок 1. AI-агенти: визначення, архітектура та робочий цикл
**Що таке AI-агент.** Агент — це система на основі ШІ, здатна самостійно сприймати інформацію, приймати рішення та виконувати дії для досягнення мети. Концептуально він поєднує **LLM як «мозок»** (розуміє запит, планує кроки), **інструменти** (виконують конкретні дії: пошук, робота з файлами, виклики API, бази даних), **пам’ять** (зберігає контекст діалогу, попередній досвід, проміжні артефакти) і **середовище** (джерела та системи, у яких агент діє). Така композиція створює «розумний оркестратор», що уміє ставити підзадачі, викликати потрібні засоби та перевіряти результати.
**Робочий цикл агента.** Типова петля — *Observe → Think/Plan → Act → Check → Learn*. Спершу агент *сприймає* запит і контекст, далі *планує* послідовність кроків (які інструменти, у якій черзі), *виконує* дії (інколи з проміжними підзапитами), *перевіряє* проміжні результати (еталонами, правилами, підрахунками) та *оновлює пам’ять/стратегію*. У складніших сценаріях додають розділення на «планувальника» і «виконавця», або використовують багатоагентні схеми (модератор, дослідник, виконавець).
**Спеціалізовані GPTs: ігрова демонстрація.** Прості ігри («Слова зі слів») демонструють здатність агента: **чітко визначати правила**, *контролювати хід*, *повертати валідацію результату* та *адаптувати інструкції під користувача*. Навчальна цінність — у вмінні формулювати *інструкції й перевірки*: агент не лише генерує варіанти, а й підтверджує коректність (існування слів, дотримання обмежень), позначає помилки й пропонує ескалацію складності. Це переноситься на «серйозні» задачі — екстракція даних, аудит звітів, контроль бізнес-правил.
**Створення AI-агентів у Python.** Етапи: визначити роль і **межі відповідальності**; описати **інструментарій** (які джерела/АПІ дозволені, яким чином перевіряємо відповіді); спроєктувати **пам’ять** (короткочасну для розмови, довготривалу для фактів/налаштувань, епізодичну для «історії кейсу»); визначити **політики безпеки** (чутливі дані, приватність, логування). Проєктний результат — *паспорт агента*: місія, вхід/вихід, індикатори якості (SLA/SLI), обмеження вартості/затримки, сценарії відмови та ескалації на людину.
### Напиши хоку про AI
У цьому прикладі показано, як за допомогою API OpenAI створити простий генератор хоку українською мовою. Код послідовно ініціалізує клієнт, надсилає запит до моделі й виводить результат.
::: callout-tip
Попередньо я створив файл `config.py` та зберіг там змінні для використання у наступних завданнях:
```{python}
#| eval: false
OPENAI_KEY = "<OPENAI_API_KY>"
SHEET_URL = "https://docs.google.com/spreadsheets/d/000000000000"
```
:::
```{python}
#| eval: false
from openai import OpenAI
import config as C
```
---
Цей блок імпортує необхідні бібліотеки. Модуль `openai` надає інтерфейс для звернення до API, а `config` зберігає ключ доступу до сервісу (у змінній `OPENAI_KEY`).
```{python}
#| eval: false
client = OpenAI(
api_key=C.OPENAI_KEY
)
```
Створюємо екземпляр клієнта `OpenAI`, передаючи йому ключ API.
Це дозволяє виконувати запити до моделей OpenAI із заданими параметрами.
---
```{python}
#| eval: false
completion = client.chat.completions.create(
model="gpt-4o-mini",
store=True,
messages=[
{"role": "user", "content": "напиши хоку про AI українською мовою"}
]
)
print(completion.choices[0].message)
```
У цьому фрагменті формується запит до моделі **GPT-4o-mini**.
Ми передаємо повідомлення з інструкцією: *"напиши хоку про AI українською мовою"*.
Отримана відповідь зберігається у змінній `completion` і одразу виводиться на екран.
Аргумент `store=True` дозволяє зберегти історію виклику в системі OpenAI для подальшого аналізу.
---
```{python}
#| eval: false
for line in completion.choices[0].message.content.splitlines():
print(f"{line}")
```
Цикл проходить рядки тексту відповіді моделі та виводить кожен окремо. > Це забезпечує охайний формат виведення, де кожен рядок хоку з’являється на новому рядку.
---
::: callout-tip
#### Підсумок
Цей приклад демонструє базовий сценарій взаємодії з API OpenAI для генерації поетичних текстів.
Його можна розширити для створення інтерфейсу або інтеграції в навчальний проєкт.
:::
### Чат-бот із використанням OpenAI API
```{python}
#| eval: false
import openai
import config as C
```
У цьому фрагменті імпортуються необхідні бібліотеки для роботи з API OpenAI. Модуль `openai` використовується для створення запитів до моделей, а інші бібліотеки можуть забезпечувати конфігурацію або обробку даних.
```{python}
#| eval: false
openai.api_key = C.OPENAI_KEY
```
Цей код є допоміжним або демонстраційним фрагментом, який підтримує основну логіку чат-бота.
```{python}
#| eval: false
class SimpleChatAgent:
def __init__(self):
# Store conversation history
self.conversation_history = [
{"role": "system", "content": "Ти консультант, що відповідає на запитання про AI українською мовою та дуже піклуєшся про етичне використання AI у освіті та науці. Нагадуй про чесність у користуванні AI постійно."}
]
def get_response(self, user_input):
# Add user's message to conversation history
self.conversation_history.append({"role": "user", "content": user_input})
try:
# Make API call to OpenAI
response = openai.chat.completions.create(
model="gpt-4o-mini",
messages=self.conversation_history,
max_tokens=250, # Limit response length
temperature=0.7 # Controls creativity (0-1)
)
# Get AI's response
ai_response = response.choices[0].message.content
# Add AI's response to conversation history
self.conversation_history.append({"role": "assistant", "content": ai_response})
return ai_response
except Exception as e:
return f"Sorry, I encountered an error: {str(e)}"
```
Цей код створює запит до моделі GPT, що імітує поведінку чат-бота. У параметрі `messages` визначається роль користувача та його запит. Результат зберігається у змінній `completion`, з якої потім можна отримати текст відповіді.
```{python}
#| eval: false
# Create instance of our AI agent
agent = SimpleChatAgent()
print("Вітаю у чаті!")
print("Введи 'quit', якщо бажаєш вийти")
while True:
# Get user input
user_input = input("\nYou: ")
# Check if user wants to quit
if user_input.lower() == 'quit':
print("Бувай!")
break
# Get and display AI response
response = agent.get_response(user_input)
print(f"AI: {response}")
```
Цей блок забезпечує інтерактивність: користувач може вводити текстові запити до чат-бота. Код приймає введений текст і надсилає його моделі для отримання відповіді.
### Конвертація одиниць вимірювання за допомогою OpenAI API
```{python}
#| eval: false
import openai
import config as C
```
Імпорт необхідних бібліотек. Модуль `openai` використовується для доступу до моделей, а інші — для налаштування або зчитування конфігурації.
```{python}
#| eval: false
class FoodMeasureAgent:
def __init__(self):
# Initialize OpenAI client with API key
self.client = openai.OpenAI(api_key=C.OPENAI_KEY)
self.conversation_history = [
{"role": "system",
"content": "Конвертуй кожну одиницю харчових мір, яку надає користувач, у грами. Виводь лише значення у грамах. Наприклад, якщо запитують про склянку рису, відповідь має бути лише: '195 грамів'"
}
]
def get_response(self, user_input):
# Add user message to conversation history
self.conversation_history.append({"role": "user", "content": user_input})
# Get response from OpenAI
response = self.client.chat.completions.create(
model="gpt-4o-mini", # You can change to "gpt-4" if you have access
messages=self.conversation_history,
max_tokens=1000,
temperature=0.7
)
# Extract the assistant's response
assistant_response = response.choices[0].message.content.strip()
# Add assistant's response to conversation history
self.conversation_history.append({"role": "assistant", "content": assistant_response})
return assistant_response
```
Створюється екземпляр клієнта OpenAI, використовуючи API-ключ із конфігураційного файлу. Цей клієнт дозволяє відправляти запити до моделей OpenAI.
```{python}
#| eval: false
agent = FoodMeasureAgent()
print("Ласкаво просимо! Введіть одиницю виміру продукту — і я конвертую її в грами. Щоб вийти, наберіть 'quit'.")
while True:
user_input = input("\nYou: ")
print(f"\nYou: {user_input}")
if user_input.lower() == 'quit':
print("Бувай!")
break
response = agent.get_response(user_input)
print(f"AI: {response}")
```
Забезпечується інтерактивне введення користувача. Користувач вводить запит (наприклад, конвертацію), який надсилається моделі.
### Антискам: перевірка повідомлень на шахрайство та підготовка листа-відповіді
```{python}
#| eval: false
from openai import OpenAI
import config as C
# Імпорт необхідних бібліотек для роботи з API, обробки тексту та даних
```
Цей код реалізує допоміжну логіку антискам-системи — обробку даних, підготовку запиту або форматування відповіді.
```{python}
#| eval: false
client = OpenAI(
# Ініціалізація клієнта OpenAI з ключем API для взаємодії з моделлю
api_key=C.OPENAI_KEY
)
```
Ініціалізація клієнта OpenAI — ключовий крок для взаємодії з моделлю GPT. Ключ API має бути захищений і не зберігатися у відкритому коді.
```{python}
#| eval: false
# --- Agent 1: Scam Detector ---
class ScamDetector:
def __init__(self, client):
self.client = client
def analyze_email(self, email_text):
response = self.client.chat.completions.create(
# Формування запиту до моделі для аналізу повідомлення на шахрайство
model="gpt-4o-mini",
messages=[
{"role": "system", "content": 'Твоє завдання — класифікувати електронні листи як шахрайські або ні, на основі наданого тексту. Відповідай лише "YES" (якщо це шахрайство) або "NO" (якщо це не шахрайство).'},
{"role": "user", "content": email_text}
],
max_tokens=10,
temperature=0.1
)
return response.choices[0].message.content.strip()
```
Цей блок коду формує запит до моделі OpenAI. У prompt описано задачу — розпізнати ознаки шахрайства та підготувати відповідь-шаблон, яка змусить шахрая витратити час. Модель аналізує контент, шукає ключові ризикові патерни (прохання про гроші, посилання, емоційний тиск).
Рекомендація: зробити prompt структурованим — попросити модель повертати JSON з полями: `label`, `confidence`, `reason`, `reply_template`. Це спростить обробку результатів.
```{python}
#| eval: false
# --- Agent 2: Time Waster ---
class TimeWaster():
def __init__(self, client, signature_name="Юрій"):
self.client = client
self.signature_name = signature_name
def craft_response(self, email_text):
response = self.client.chat.completions.create(
# Формування запиту до моделі для аналізу повідомлення на шахрайство
model="gpt-4o-mini",
messages=[
{"role": "system", "content": f"Напиши довгу відповідь на цей шахрайський електронний лист (на підставі тексту, який надасть користувач), щоб затримати та марнувати час шахрая. Не повідомляй жодної особистої інформації або реальних контактних даних. Мета — утримати шахрая в листуванні якомога довше: став заплутані уточнювальні питання, вимагай зайвих деталей/документів, проси пояснити суперечності, показуй надмірну зацікавленість, погоджуйся на дивні процедури тощо — але ніякої реальної інформації про себе. Підписуйся як {self.signature_name} (замінити на потрібне ім’я при використанні). Формат відповіді: лише текст листа (без пояснень або інструкцій для користувача)."},
{"role": "user", "content": email_text}
],
max_tokens=500,
temperature=0.1 # increase temperature to make response more crazy
)
return response.choices[0].message.content.strip()
```
Цей блок коду формує запит до моделі OpenAI. У prompt описано задачу — розпізнати ознаки шахрайства та підготувати відповідь-шаблон, яка змусить шахрая витратити час. Модель аналізує контент, шукає ключові ризикові патерни (прохання про гроші, посилання, емоційний тиск).
Рекомендація: зробити prompt структурованим — попросити модель повертати JSON з полями: `label`, `confidence`, `reason`, `reply_template`. Це спростить обробку результатів.
```{python}
#| eval: false
# --- Multi-Agent System ---
class EmailHandler:
def __init__(self, client):
self.scam_detector = ScamDetector(client)
self.time_waster = TimeWaster(client)
def process_email(self, email_text):
# Step 1: Detect scam
status = self.scam_detector.analyze_email(email_text)
print(f"Email: {email_text}")
print(f"Scam Detector: {status}")
# Step 2: If scam, waste time
if status == "YES":
reply = self.time_waster.craft_response(email_text)
print(f"Відповідь шахраям: {reply}")
else:
print("Відповідь не створено.")
print("---")
```
Цей код реалізує допоміжну логіку антискам-системи — обробку даних, підготовку запиту або форматування відповіді.
```{python}
#| eval: false
# Run the system
handler = EmailHandler(client)
# Simulated email examples
emails = [
"Шановний користувачу, ви були обрані для отримання спеціального грошового гранту у розмірі 250 000 грн. Для переказу коштів нам потрібен номер вашої картки та ПІН-код.",
"Привіт! Не забудь, сьогодні в нас зустріч з новими клієнтами о 10:30. Зустрічаємось в переговорній №2.",
"Вітаю! Я юрист покійного мільярдера, і ви зазначені в його заповіті як спадкоємець. Терміново зв’яжіться зі мною для оформлення спадщини.",
"Аллллоо! Ти скинеш фінальну версію презентації до 18:00? Ми плануємо все зібрати до завтра.",
"Ваш акаунт у ПриватБанку заблоковано. Щоб відновити доступ, натисніть на посилання нижче та введіть усі особисті дані для підтвердження."
]
```
Цей код реалізує допоміжну логіку антискам-системи — обробку даних, підготовку запиту або форматування відповіді.
```{python}
#| eval: false
for email in emails:
handler.process_email(email)
input("Press enter to continue next email.")
# Отримання повідомлення від користувача для перевірки
```
Цей код реалізує допоміжну логіку антискам-системи — обробку даних, підготовку запиту або форматування відповіді.
---
## 🔗 Блок 2. LangChain: фреймворк для ланцюгів дій і агентів
**Що таке LangChain.** Це фреймворк (Python/JavaScript) для побудови застосунків і агентів на базі LLM: він поєднує **ланцюги мислення/дій** (chains), **зовнішні інструменти**, **пам’ять**, **ретрієвал** (доступ до файлів/БД/пошуку) та **агентні петлі** (вибір наступної дії залежно від проміжного результату). Перевага — «конструктора» з готових блоків: промпт-шаблони, parsers, memory stores, tool adapters, retrievers; можливість додавати власні інструменти й правила валідації, будувати керовані сценарії із прозорим логуванням.
**Практичний кейс: агент для запиту «Прошу підписати мені індивідуальний план».** Задача — автоматизувати обробку вхідних листів і підготовку відповідей.
- **Вхід:** поштові повідомлення; ознаки: відправник, тема, тіло, вкладення.
- **Ціль:** знайти запити на підписання ІП, зібрати список студентів, підготувати шаблон-відповідь (інструкції, терміни, перелік кроків).
- **Інструменти:** доступ до пошти (читання), шаблонізатор відповідей, таблиця/CRM для реєстру, календар/таск-менеджер для нагадувань.
- **Пам’ять:** збереження вже оброблених листів, статусів (очікує документів/підписано/відхилено), історії комунікації.
- **Перевірки:** чи справді лист про ІП (класифікація), чи всі обов’язкові поля заповнено, чи немає дубля.
- **Вихід:** чернетки відповідей (для людини), оновлений список студентів, журнал рішень.
**Теоретичні нюанси.** Визначаємо *порогові правила* для класифікатора теми; політики ескалації, якщо невпевненість висока; **вікна пам’яті** для контексту; часові обмеження; облік вартості викликів; вимоги до приватності. Агент не відправляє листи самостійно без «human-in-the-loop» — це контроль ризиків і відповідальності.
### Автоматизований GPT-помічник для обробки листів студентів
**Автоматизований GPT-помічник для обробки листів студентів щодо індивідуальних планів (Gmail → LangChain → Google Sheets)**
> Цей ноутбук:
> - читає листи з Gmail;
> - класифікує їх за допомогою GPT (LangChain 1.x);
> - витягує ПІБ та групу студента;
> - підбирає шаблон відповіді з аркуша **"Шаблони"** існуючого документа Google Sheets;
> - формує текст відповіді (GPT);
> - створює **чернетку-відповідь у Gmail**;
> - записує результат у аркуш **"Запити"** (існуючий документ Google Sheets).
>
> ⚙️ Пайплайн зібраний у **єдиний Runnable Chain** (LangChain 1.x).
#### Встановлення залежностей (одноразово)
> За потреби розкоментуйте та виконайте:
```bash
# !pip install -U langchain langchain-openai langchain-core
# !pip install google-auth google-auth-oauthlib google-auth-httplib2 google-api-python-client
# !pip install gspread gspread-formatting pandas rapidfuzz python-dotenv
```
#### Імпорт бібліотек
```{python}
#| eval: false
import os, re, json, datetime, base64, string
# Імпорт необхідних бібліотек (робота з API, обробка пошти, AI-моделі)
import pandas as pd
# Імпорт необхідних бібліотек (робота з API, обробка пошти, AI-моделі)
from email.mime.text import MIMEText
from rapidfuzz import process
from dotenv import load_dotenv
from google.auth.transport.requests import Request
from google.oauth2.credentials import Credentials
from google.oauth2 import service_account # not used, but handy for alt flows
from google_auth_oauthlib.flow import InstalledAppFlow
from googleapiclient.discovery import build
# Підключення до Gmail API для читання та надсилання листів
import gspread
# Імпорт необхідних бібліотек (робота з API, обробка пошти, AI-моделі)
from gspread_formatting import *
# LangChain (modern, 1.x style)
from langchain_openai import ChatOpenAI
from langchain.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnableLambda, RunnableSequence
```
Цей блок реалізує допоміжну функціональність — наприклад, обробку тексту, логування або контроль потоку.
#### Конфігурація: OpenAI ключ, модель; модуль `config`
> Ми використовуємо ваш локальний модуль `config.py` з полями:
> - `OPENAI_KEY` — OpenAI API ключ
> - `SHEET_URL` — URL на вже **існуючий** документ Google Sheets (*"Індивідуальні плани"*)
```{python}
#| eval: false
import config as C
# Імпорт необхідних бібліотек (робота з API, обробка пошти, AI-моделі)
OPENAI_API_KEY = C.OPENAI_KEY
MODEL_NAME = "gpt-4o-mini" # можна змінити на "gpt-4o", якщо доступно
os.environ["OPENAI_API_KEY"] = OPENAI_API_KEY
print("🔑 OpenAI API Key завантажено")
print("🧠 Модель:", MODEL_NAME)
```
Цей блок реалізує допоміжну функціональність — наприклад, обробку тексту, логування або контроль потоку.
#### Авторизація Google API (Gmail + Sheets)
> Використовується **існуючий токен** з `secret/token.json` та OAuth-клієнт `secret/client_secret.json`.
> Якщо `token.json` відсутній або прострочений — відбудеться інтерактивний OAuth-потік у браузері.
```{python}
#| eval: false
SCOPES = [
"https://www.googleapis.com/auth/gmail.readonly",
"https://www.googleapis.com/auth/gmail.compose",
"https://www.googleapis.com/auth/spreadsheets"
]
creds = None
if os.path.exists("secret/token.json"):
creds = Credentials.from_authorized_user_file("secret/token.json", SCOPES)
if not creds or not creds.valid:
if creds and creds.expired and creds.refresh_token:
creds.refresh(Request())
else:
flow = InstalledAppFlow.from_client_secrets_file("secret/client_secret.json", SCOPES)
creds = flow.run_local_server(port=0)
with open("secret/token.json", "w") as token:
token.write(creds.to_json())
gmail_service = build("gmail", "v1", credentials=creds)
gc = gspread.authorize(creds)
print("✅ Авторизація Gmail і Sheets виконана")
# Підключення до Gmail API для читання та надсилання листів
```
Цей блок реалізує взаємодію з Gmail API. Він може отримувати вхідні листи, читати теми та тіла повідомлень, а також автоматично надсилати сформовані відповіді. Варто забезпечити фільтрацію (наприклад, лише листи від студентів або з темою 'індивідуальний план').
#### Ініціалізація LLM та базових компонентів LangChain
```{python}
#| eval: false
llm = ChatOpenAI(model=MODEL_NAME, temperature=0)
# Ініціалізація клієнта OpenAI для створення AI-відповідей на листи
parser = StrOutputParser()
```
Ініціалізація клієнта OpenAI забезпечує інтеграцію з моделлю для автоматичного створення відповідей. Ключ API має бути збережений у середовищі безпеки, не в коді.
####Підключення до **вже існуючого** Google Sheets і читання шаблонів
> Ми **не створюємо** нових документів. Використовується URL з `config.SHEET_URL`.
> Обов'язкові аркуші: **"Шаблони"** (курс, група, шаблон) і **"Запити"**.
```{python}
#| eval: false
spreadsheet = gc.open_by_url(C.SHEET_URL)
print(f"Зчитано документ: {spreadsheet.title}")
print("Аркуші:")
for ws in spreadsheet.worksheets():
print(" -", ws.title)
```
Цей блок реалізує допоміжну функціональність — наприклад, обробку тексту, логування або контроль потоку.
```{python}
#| eval: false
# Основні аркуші
sheet_requests = spreadsheet.worksheet("Запити")
sheet_templates = spreadsheet.worksheet("Шаблони")
```
```{python}
#| eval: false
# Шаблони у форматі {(Курс, Група): "Шаблон"}
tpl_data = sheet_templates.get_all_records()
templates = {(t["Курс"], t["Група"]): t["Шаблон"] for t in tpl_data}
for k, v in templates.items():
print(f"{k[0]} / {k[1]}")
print(f"📚 Завантажено шаблонів: {len(templates)}")
```
#### Службові функції: читання листів і створення чернеток
```{python}
#| eval: false
def get_latest_emails(query="subject:план OR індивідуальний план", max_results=30):
"""Читає останні листи за пошуковим запитом.
Повертає список словників з ключами: id, threadId, message_id, from, subject, body.
"""
res = gmail_service.users().messages().list(userId="me", q=query, maxResults=max_results).execute()
messages = res.get("messages", [])
emails = []
for msg in messages:
data = gmail_service.users().messages().get(userId="me", id=msg["id"], format="full").execute()
snippet = data.get("snippet", "")
headers = data["payload"].get("headers", [])
sender = next((h["value"] for h in headers if h["name"].lower()=="from"), "Unknown")
subject = next((h["value"] for h in headers if h["name"].lower()=="subject"), "No subject")
message_id = next((h["value"] for h in headers if h["name"].lower()=="message-id"), None)
emails.append({
"id": msg["id"],
"threadId": data.get("threadId"),
"message_id": message_id,
"from": sender,
"subject": subject,
"body": snippet
})
return emails
```
```{python}
#| eval: false
def create_reply_draft(original_email: dict, to_email: str, reply_body: str):
"""Створює чернетку-відповідь у існуючому треді (якщо можливо)."""
# Формуємо MIME-повідомлення
message = MIMEText(reply_body, 'plain', 'utf-8')
message['To'] = to_email
message['Subject'] = f"Re: {original_email.get('subject','')}"
if original_email.get("message_id"):
message['In-Reply-To'] = original_email["message_id"]
message['References'] = original_email["message_id"]
raw_message = base64.urlsafe_b64encode(message.as_bytes()).decode('utf-8')
body = {'message': {'raw': raw_message}}
# Прив'язуємо до треду, якщо він відомий
if original_email.get("threadId"):
body['message']['threadId'] = original_email["threadId"]
gmail_service.users().drafts().create(userId='me', body=body).execute()
```
#### Вибір шаблону відповіді (пошук за групою і / або предметом)
> Використовується підхід з твого ноутбука: спершу шукаємо за **групою** (низький поріг, як у тебе), потім — за **назвою курсу/предмету**.
```{python}
#| eval: false
def find_best_template_by_group(group: str):
if not group:
return None
candidates = [f"{c[1]}" for c in templates.keys()]
query = f"{group}"
best, score, _ = process.extractOne(query, candidates) if candidates else (None, 0, None)
if best and score >= 65:
for k, v in templates.items():
if f"{k[1]}" == best:
return v
return None
def find_best_template_by_subject(sbj: str):
if not sbj:
return None
candidates = [f"{c[0]}" for c in templates.keys()]
query = f"{sbj}"
best, score, _ = process.extractOne(query, candidates) if candidates else (None, 0, None)
if best and score >= 65:
for k, v in templates.items():
if f"{k[0]}" == best:
return v
return None
```
#### Промпти (аналіз листа → генерація відповіді)
```{python}
#| eval: false
prompt_analyze = ChatPromptTemplate.from_template("""
Ти — асистент викладача університету.
Завдання:
1) Визнач, чи цей емейл є проханням студента/студентки про підписання індивідуального навчального плану.
2) Якщо так — знайди у тексті:
- ПІБ студента/ки (дозволені формати "Прізвище Ім'я По батькові" або "Прізвище По батькові")
- групу (напр. КН-31, ІТ-22 тощо). Якщо не вдалося ідентифікувати групу - поверни пустий рядок, не намагайся вгадати. Серед доступних груп: {groups}.
- назву курсу / предмета (якщо є), назва курсу повинна бути схожою на {courses} (обери найближчу, якщо співпадіння 0.6 і більше) або не обирай зовсім
- e-mail відправника (якщо є в тексті, інакше використай поле From)
3) Якщо НЕ є таким проханням — поверни літерал `None` (без JSON).
Вхідні дані:
Від: {sender}
Тема: {subject}
Текст: {body}
Відповідь у JSON БЕЗ markdown:
{{
"student_name": "...",
"group": "...",
"subject": "...",
"sender_email": "..."
}}
АБО `None`.
""")
prompt_reply = ChatPromptTemplate.from_template("""
Сформулюй коротку професійну відповідь студенту/студентці, який/яка просить підписати індивідуальний план.
Використай шаблон (адаптуй під конкретні дані):
{template_text}
Повне ім'я студента/студентки: {name}
Група: {group}
Предмет/курс: {subject_name}
❗ Якщо в шаблоні є плейсхолдер `#FIRST_NAME`, заміни його на ім'я у кличному відмінку (якщо можливо).
""")
```
#### Побудова саб-ланцюгів LangChain
> - `analyze_chain = prompt_analyze | llm | parser`
> - `reply_chain = prompt_reply | llm | parser`
```{python}
#| eval: false
analyze_chain = prompt_analyze | llm | parser
reply_chain = prompt_reply | llm | parser
```
#### Єдиний end-to-end Runnable Chain
Архітектура:
```
email(dict)
→ analyze (GPT)
→ parse/update state
→ find template (group→subject)
→ reply (GPT)
→ write to Sheets
→ create Gmail draft
```
```{python}
#| eval: false
# Налаштування: ваша адреса, щоб не обробляти власні листи
MY_EMAIL = "yuriy.kleban@oa.edu.ua" # ← за потреби змініть
def _init_state(email: dict):
"""Ініціалізація стану для ланцюга."""
return {"email": email, "skip": False, "status": None}
def _analyze_step(state: dict):
"""Крок аналізу: GPT класифікація + вилучення полів або skip."""
email = state["email"]
sender = email.get("from", "")
subject = email.get("subject", "")
body = email.get("body", "")
# Не обробляємо власні листи
if MY_EMAIL.lower() in sender.lower():
state.update({"skip": True, "status": "own_email"})
return state
result = analyze_chain.invoke({"sender": sender, "subject": subject, "body": body, "courses": ", ".join(f"{k[0]}" for k in templates.keys()), "groups": ", ".join(f"{k[1]}" for k in templates.keys())}).strip()
if result == "None":
state.update({"skip": True, "status": "not_request"})
return state
# Очікуємо JSON
try:
# Обробка винятків — запобігає зупинці при помилках API або мережі
data = json.loads(result)
except Exception:
# Обробка винятків — запобігає зупинці при помилках API або мережі
state.update({"skip": True, "status": "bad_json"})
return state
# Уніфікація регістру/формату
name = data.get("student_name")
group = data.get("group")
subj = data.get("subject")
sender_email = data.get("sender_email") or sender
# Легка нормалізація
name = string.capwords(name) if isinstance(name, str) else None
group = group.upper() if isinstance(group, str) else None
state.update({
"student_name": name,
"group": group,
"subject_name": subj,
"sender_email": sender_email,
"status": "analyzed"
})
return state
def _choose_template_step(state: dict):
"""Крок вибору шаблону: спершу група, потім предмет."""
if state.get("skip"):
return state
tpl = None
if state.get("group"):
tpl = find_best_template_by_group(state["group"])
if not tpl and state.get("subject_name"):
tpl = find_best_template_by_subject(state["subject_name"])
if tpl:
state.update({"template_text": tpl, "status": "template_found"})
else:
state.update({"template_text": None, "status": "template_not_found"})
return state
def _generate_reply_step(state: dict):
"""Крок генерації відповіді (GPT), якщо є шаблон."""
if state.get("skip"):
return state
if state.get("status") != "template_found":
state.update({"draft_text": None})
return state
draft_text = reply_chain.invoke({
"template_text": state["template_text"],
"name": state.get("student_name",""),
"group": state.get("group",""),
"subject_name": state.get("subject_name","")
})
state.update({"draft_text": draft_text, "status": "draft_ready"})
return state
def _write_outputs_step(state: dict):
"""Запис у Sheets та створення Gmail чернетки (якщо є draft)."""
# Підключення до Gmail API для читання та надсилання листів
email = state["email"]
if state.get("skip"):
# нічого не пишемо
return state
# Статус за замовчуванням
status_to_write = "Не потребує відповіді"
if state.get("status") == "template_not_found":
status_to_write = "Шаблон не знайдено"
elif state.get("status") == "draft_ready":
status_to_write = "Чернетку створено"
# Створюємо чернетку
try:
# Обробка винятків — запобігає зупинці при помилках API або мережі
create_reply_draft(email, state.get("sender_email",""), state.get("draft_text",""))
except Exception as e:
# Обробка винятків — запобігає зупинці при помилках API або мережі
status_to_write = f"Draft error: {e}"
# Запис у аркуш "Запити"
try:
# Обробка винятків — запобігає зупинці при помилках API або мережі
sheet_requests.append_row([
state.get("student_name",""),
state.get("group",""),
state.get("subject_name",""),
state.get("sender_email",""),
datetime.datetime.now().strftime("%Y-%m-%d %H:%M"),
status_to_write
])
except Exception as e:
# Обробка винятків — запобігає зупинці при помилках API або мережі
# Якщо помилка — до стану
state["status"] = f"Sheets error: {e}"
# Підключення до Google Sheets для збереження результатів (журнал відповідей або статусів)
return state
```
Цей блок реалізує взаємодію з Gmail API. Він може отримувати вхідні листи, читати теми та тіла повідомлень, а також автоматично надсилати сформовані відповіді. Варто забезпечити фільтрацію (наприклад, лише листи від студентів або з темою 'індивідуальний план').
```{python}
#| eval: false
# Єдиний Runnable Chain
full_chain = RunnableSequence(
RunnableLambda(_init_state),
RunnableLambda(_analyze_step),
RunnableLambda(_choose_template_step),
RunnableLambda(_generate_reply_step),
RunnableLambda(_write_outputs_step),
)
```
#### Запуск: читання листів з Gmail і обробка через **єдиний chain**
> Налаштуйте `MY_EMAIL` вище (щоб пропускати власні листи).
```{python}
#| eval: false
emails = get_latest_emails(max_results=10)
print(f"📨 Отримано листів: {len(emails)}")
processed = 0
created = 0
skipped = 0
no_template = 0
for em in emails:
#print(em['from'])
state = full_chain.invoke(em)
processed += 1
st = state.get("status","")
if st in ("own_email","not_request","bad_json"):
skipped += 1
elif st == "template_not_found":
no_template += 1
elif st in ("draft_ready",):
created += 1
print("—"*40)
print("Звіт:")
print(" Оброблено:", processed)
print(" Пропущено (не запит / власний лист / bad json):", skipped)
print(" Без шаблону:", no_template)
print(" Чернеток створено:", created)
print("—"*40)
print("✅ Готово.")
```
#### Підсумок
- Всі знайдені листи були проаналізовані GPT (LangChain 1.x).
- Для релевантних листів згенеровані відповіді на основі шаблонів із аркуша **"Шаблони"**.
- Результати записані у аркуш **"Запити"**.
- У Gmail створені відповідні **чернетки**.
> За потреби відкоригуйте:
> - `MY_EMAIL` (ваша адреса, щоб листи від себе пропускати);
> - пороги схожості в `find_best_template_by_*`;
> - `get_latest_emails(query=...)` — змініть пошуковий запит під ваші умови.
---
## 🧩 Блок 3. No-code/Low-code сервіси: OpenAI Agent Builder, Make, n8n
**Навіщо «без коду».** Частину процесів зручніше збирати як *workflow*: подія-тригер → фільтри → виклики інструментів → перевірки → повідомлення. Це пришвидшує прототипування, дозволяє бізнес-командам самостійно обслуговувати автоматизації та спиратися на корпоративні інтеграції. Проте **архітектурні принципи** лишаються: чітка роль агента, журнал рішень, політики приватності, ескалації на людину, контроль вартості/затримки.
**Кейс: «Казочки на ніч».** Ідея — за вхідними даними (тематика, вік дитини, довжина, стиль) агент генерує казку, надсилає її на email і публікує в Notion із підходящими емодзі.
- **Тригер:** надходить лист або веб-форма.
- **Кроки:** валідація параметрів → генерація чернетки → перевірка змісту (відсутність небажаних тем) → стилістична правка → створення нотатки в Notion → відправлення листа.
- **Політики:** чіткі гайдлайни щодо контенту; вимоги до приватності (жодних персональних даних у промптах); логування результату; права на повторну публікацію.
- **Ризики:** небажаний контент, помилкові адреси, збої інтеграцій; рішення — попередній перегляд людиною, ретраї, алерти.
**Порівняння підходів.** No-code хороші для швидкого MVP і стабільних сценаріїв; код (або LangChain) потрібен, коли складна логіка планування, нестандартні інструменти або тонкі вимоги до якості/трасування.
---
## 💸 Batch API: пакетна обробка як спосіб зменшити вартість
**Ідея.** Batch API — режим асинхронної пакетної обробки великої кількості запитів. Замість тисяч окремих викликів передаємо один файл (наприклад, JSONL) і отримуємо результати після завершення. Переваги: менша вартість на обсяг, стабільніший тротлінг і зручність для завдань на кшталт масової класифікації відгуків, розмітки документів, витягу сутностей. Обмеження: довший latency «від заявки до результату», вимоги до ідемпотентності та ретраїв, необхідність чіткої схеми валідної/невалідної відповіді. Для бізнес-пайплайнів важливо мати моніторинг прогресу, «мертві черги» та автоматичні сповіщення.
**Приклад: класифікація відгуків** (тональність і тема: доставка/підтримка/якість/пакування/інше). Теоретична постановка: готуємо інструкцію агента, обмежуємо формат відповіді (поле `name`, `topic`, `sentiment`), визначаємо політику для неоднозначних випадків («невпевнено» → на ручну валідацію), проєктуємо метрики якості (збіг анотацій, частка «невпевнено», продуктивність на 1000+ записів).
---
## 🧪 Демонстрації з презентації — теоретичні розбори
**Демо 1–2. «Слова зі слів».** Агент керує грою: задає правила, запускає таймер, перевіряє слова, підсвічує помилки, пропонує ускладнення (тематика). Освітній сенс — навчитися формулювати інструкції, перевірки та *feedback-loop* у легкому середовищі.
**Демо 3–4. Творчість і консалтинг.** «Хайку українською», «Нудний чат про AI з етичним нагадуванням»: показують *рольову інструкцію* й *тон відповіді*, необхідність стабільних політик (нагадування про чесність). Переноситься на корпоративні сценарії (відповіді саппорту з політиками).
**Демо 5. Конвертер харчових мір.** Агент-конвертер приймає запит у довільній формі, але повертає **лише грамове значення** — демонстрація суворого формату відповіді, що зручно для автоматизованих ланцюгів (жодного «води», тільки число й одиниці).
**Демо 6. Антискам.** Двоетапний сценарій: (1) класифікація листа на шахрайство/ні (тільки `YES/NO`), (2) генерація «довгої відповіді» для затягування часу шахрая без розголошення персональних даних — приклад етичних обмежень і чіткого формату виходу. У виробництві додаються чорні списки, ліміти частоти, правила ескалації.
**Демо 7. Batch-класифікація відгуків.** Форматований JSON-вихід (`name`, `topic`, `sentiment`), політика для «невпевнених» випадків, узгодженість з анотаціями людини — приклад, як агент інтегрується в аналітичний конвеєр.
**Демо 8. LangChain-агент «Підпишіть інд. план».** Теоретичний пайплайн: читання пошти → фільтрація листів про ІП → консолідація списку студентів → генерація чернеток відповідей за шаблонами → логування рішень → попередній перегляд людиною.
**Демо 9. «Казкар для малюків».** Інтеграція з поштою і Notion, політики контенту, публікація з емодзі. Важливо: модуль перевірки заборонених тем і ручне погодження перед розсилкою.
---
## 🛡️ Якість, безпека, етика й відповідальність
**Якість та валідація.** Для кожного агента визначаємо індикатори: частка коректних відповідей, дотримання формату, час відгуку, вартість на запит, частка ескалацій на людину. Використовуємо вибіркову перевірку «ground-truth», журналимо вхід/вихід і рішення (з урахуванням приватності), порівнюємо ітерації агента під різними налаштуваннями.
**Безпека і приватність.** Мінімізуємо дані у промптах (data minimization), деперсоналізуємо приклади, шифруємо журнали, обмежуємо інструменти агента (whitelist API). Для чутливих сценаріїв — незалежний аудит і відмова від повної автоматизації.
**Етика й прозорість.** Повідомляємо користувачам про участь ШІ, забезпечуємо можливість оскарження рішень, відстежуємо упередження (за групами, сегментами) і документуємо межі застосовності агентів.
---
## ✅ Підсумки
- **AI-агенти = LLM + інструменти + пам’ять + середовище** з чіткою роллю та межами.
- **LangChain** допомагає будувати керовані ланцюги дій, **no-code** — швидко збирати робочі процеси.
- **Batch-режим** знижує вартість для масових завдань; потрібні формати, валідація та моніторинг.
- **Людина-в-циклі** — ключ до якості, безпеки та етики в автоматизації аналітики.
---
## 📚 Матеріали
- Презентація «Автоматизація обробки даних з використанням AI-агентів» (структура блоків, демо-сценарії, сервіси та кейси).
---
:::: {.columns}
::: {.column width="20%"}
<img src="https://kleban.page/bc-2025/images/logo.png" alt="Лого" style="height: 70px;">
:::
::: {.column width="30%"}
<img src="https://kleban.page/bc-2025/images/eu-founded.png" alt="Лого" style="height: 100px;">
:::
::: {.column width="50%"}
Проєкт реалізується за підтримки **Європейського Союзу** в межах програми [Дім Європи](https://houseofeurope.org.ua/).
:::
::::