Галоўная / Артыкулы / Практычныя прытамулкі: Праект з AI-агентамі: стварыце чатбота для адказоў на запиты кліянтаў для

Практычныя прытамулкі: Праект з AI-агентамі: стварыце чатбота для адказоў на запиты кліянтаў для

Практычныя прыказкі: Праект з AI-агентамі: стварэнне чатбота для адказоў на запыткі клёячыхся, які можа выкарыстоўвацься для кантрактаў, пераказаў грошэй і для команд, якія викорыстоўваюць гэты патэрн, у виглядзе шаблонаў коду.

5381 слоў

Наступныя прыміткі паказваюць практычны шлях адбыць праект «Agentic AI Project: Стварэнне чат-бота для адказоў на запыткі кліянтацтва ў клініцы». Акцэнс ставяецца на контракты, пераканання і мескі для коду, а не на мотывацыйныя аспекты. Калі працуеце над стадзіяй агледжэння, спачатку запісайце контракт: неабяжныя даны, сігнал успеху і тое, што выканаецца у разе частковага нявыполнення. Такі список пераканаецца дапамагае залишыць пазнейшыя змены ў кодзе чыстымі. Документавайце як шлях успеху, так і шлях вярнення да нормы. Перапрыбуткі, людзкі контроль і обработка некоректных паведамленняў є частью продукту, а не пазнейшым дапрацоўкам.

Введэнне

Этап введэння працюе найкраща, калі яго розглядаць як вимерную паверхню. Запісаўце адна «золатая» транскрыпцыя, адин прыклад неудачы і запіс пра вярнэнне да пачатковага стану перш чым расширваць масштаб. Валіце малыя, тэставаныя елементы замест большых скрыптаў. Калі якісь крок не выйшоў, прычына неудачы павінна вказваць на адную адпаведальнасць, а не на заплутаны ланцужок задач. Рэзерваваце стан графа ў простам і типізаваным формате. Вярнутыя структуры маскуюць інфармацыю пра тое, який вузел запісаў кожны поле, і спакшуюць продажчэнне роботы пасля перарываў.

Формулюванне проблемы

Этап анаізу проблемы работае найкраща, калі яго спрыяваць як мерыемую структуру. Запісаўце адна ідеальная версія, адзін прыклад неудачы і запіс пра вярнэнне да пачатковага стану перш чым расширваць масштабы. Спрыяйце цэму этапу як кантракту межа вхіднымі даннымі і перакананымі выходнымі рэзультатамі. Даўце назвы артыфактам, задаце критэрыя успеху і адмовіцеся ад тыхняй частковай роботы без паведамлення. Храніце стан графа ў простым і типаванам формате. Вкладзеныя структуры маскуюць інфармацыю пра тое, калькі вузел запісаў кожны поле, і спакшваюць продажчэнне роботы пасля перарываў.

Рашэння

Этап рашэння працуе наякша, калі яго спрыяваць як мерыемую паверхню. Зберагачыце адны ідеальны прыклад работы, адзін прыклад неудачы і запіс пра вярнэнне да пачатковага стану перад расшырэнням масштаба. Запісвайце часы выканання і косты токэнаў або запытак праза функцыйнае рэзультат. Відкрытыя данні пра косты з самага пачатку запобегаюць неспакоўным рахункам, калі процес пераходзіць з дэмаверсіі ў спяльныя среды. Храніце стан графаў у простам і типаваным формате. Вярнутыя структуры данных маскуюць інфармацыю пра тое, який вузел запісаў канкрэтны поле, і спакшваюць продовжэнне роботы пасля перарываў. Этап рашэння працуе наякша, калі яго спрыяваць як мерыемую паверхню. Зберагачыце адны ідеальны прыклад работы, адзін прыклад неудачы і запіс пра вярнэнне да пачатковага стану перад расшырэнням масштаба. Дакументавайце як шлях успеху, так і шлях вяснавання проблем. Перапрыбуткі, людзкія контрольныя пункты і обработка некоректных паведамленняў є часткай продукту, а не чымсь, што дадаецца пазней.

Настройка

У стадії налагоджэння неабяцо практычна апішацыя вхідных дадзеных, адпаведнага адпаведальніка за крок і крэтарыяў завершэння працы перад зменым коду. Аперацыйныя працавнікі павінны магчымае запускаць крок з вядомай точкі перапытку без неабяцовага адгадвання схованых станоў. Лепш выбіраць маленькія, тэставаныя елементы замест большых скрыптов. Калі крок не выйшаў, прычына неабяцо вказваць на адзін конкрэтны аспект, а не на заплутаны ланцоўкі задач. Пры выкарыстоўванні грошэй чы зменыніх у працуючых дадзеных неабяцо ўключыць паштоверэнне чалавека. Компіляцыйныя налашчэння не ўзначаюць павнае адпрацоўванне бізнес-процэсаў.

Mac / Linux / Windows

Для этапа Mac, Linux, Windows неабяжна прадзецелаваць параметры вводу, адпаведнага власніка крока і крэтарыя выходу пры змены коду. Аператары должны магчымаць перзапуск крока з вядомай точкі контролю, не падозрываючы схованы стан. Спрыяйце гэтам этапу як кантракту межа прадзецелаваннямі і перакананымі выходамі. Даўце назвы артыфактам, прадзецелаваць перагляд успеху і адмовіцеся ад тыхнаго частковага завершэння без паведамлення. Забяжце людзкія апраўды для тых крокаў, якія витрачаюць грошы або зменяюць данні ў працэсе. Компіляцыйныя налашчэння не ўзроўнаўцуюцься з павнай завершанасцю бізнес-процэса.

python -m venv .venv
source .venv/bin/activate
.venv\Scripts\Activate.ps1
.venv\Scripts\activate
(.venv) your-folder-name %
streamlit>=1.50.0
python-dotenv==1.0.0
pydantic==2.12.5
pandas==2.3.3
python-dateutil==2.8.2
langgraph>=1.0.7
openai>=2.16.0
pygraphviz==1.14
cd clinic-agent
pip install -r requirements.txt
OPENAI_API_KEY=your_openai_api_key_here

Частка 1: Наладжэнне базы даных (data/db.py)

У стадії налагоджэння базы даных часткі 1 неабяцкова практычна вказаць інпуты, адміністратара крока і критэрыя завершэння пры перадзеіснаванні коду. Аператары должны магчымаць перзапуск крока з вядомай точкі контролю, не спрабоўваючы здагадвацца пра схованы стан. Запісвайце час выконання і вартасць токена або запыту праза функцыйнальнымі рэзултатамі. Відразы вартасці з самага пачатку запобегае неспакоўным рахункам, калі процес пераходзіць з дэмовай среды ў спяльнаныя сераўеры. Заставіце людзкую апрацоўку для крокаў, якія витрачаюць грошы або зменяюць даны ў працэсе. Працэс складання коду не ўзроўнаважваецца з абсягамі бізнес-функцыйнасці. У стадії налагоджэння базы даных часткі 1 неабяцкова практычна вказаць інпуты, адміністратара крока і критэрыя завершэння пры перадзеіснаванні коду. Аператары должны магчымаць перзапуск крока з вядомай точкі контролю, не спрабоўваючы здагадвацца пра схованы стан. Дакументавайце як шлях успеху, так і шлях вярнення да нормальнага стану. Перапрыбуткі, людзкія контрольныя пункты і обработка некоректных паведамленняў є часткай продукту, а не елементамі пазнейшага доўнерэшання.

1. Таблыца лекароў

Калі працуеце над стадзіяй «1. Таблыца лекароў», спачатку запісайце угоду: неабходныя даны, сигнал успеху і тое, што выканаецца у разы частковага невяснення. Такі список пераканальвае ў тым, што пазнейшыя змены коду будуць чыстымі. Валіце маленькія, тэставаныя елементы замест большых скрыптаў. Калі якісьць крок не выйшла, невясненне павінна адносіцца да адной відпаведальнасці, а не да заплутанага ланца задач. Зробіце перапактаванне пасля дорогіх крокаў. Програма не павинна зноў выканаць той самы вызов LLM, калі аператар пракушае пазнейшы вузел.

2. Таблыца кліянтаў

Калі працюеце над стадзіяй «Таблыца 2 Кліентав», спачатку запісайце угоду: неабяжлівыя даны, сігнал успеху і тое, што выходзіць у разе частковага нявыпання. Такі чэк-ліст дапамагае заставаць правдзівымі пазнейшыя змены коду. Спрацоўвайце з гэтай стадзіяй як з угодай межа данымі і перакананымі выходамі. Дайце назву рэзультатам, задаце критэрыя успеху і не прымайце часткова завершэнне без паведамлення. Зробіце перапактаванне пасля дорогіх крокаў. Система вярнення не павінна зноў выклікаць той самы калл LLM, калі аператар прабуе зноў выконаць пазнейшы вузел.

3. Таблыца Броніравання

Калі працуеце над 3 стадзямі Booking Table, спачатку запісайце угоду: неабяжлівыя даны, сигнал успеху і тое, што выходзіць у разе частковага нявыпання. Такі список перакладзець пазнейшыя змены коду ў правільным напрамку. Запісвайце часы выканання і кост токена або запиту праза функцыональныя рэзультаты. Відразлівасць коста з самага пачатку запобегае неспакоўным рахункам, калі працэс пераходзіць з дэмовай среды ў спяльныя сераўры. Зробіце перапытаку пасля дорогіх крокаў. Система не должна зноў нараховваць кост той самай вызову LLM, калі аператар прабуюць зноў выканаць пазнейшы элемент. Калі працуеце над 3 стадзямі Booking Table, спачатку запісайце угоду: неабяжлівыя даны, сигнал успеху і тое, што выходзіць у разе частковага нявыпання. Такі список перакладзець пазнейшыя змены коду ў правільным напрамку. Дакументавайце як успешны, так і патэнцыйны шляхы выкаанання. Прабуры, людзкія контралі і обработка некоректных паведамленняў є часткай продукту, а не чымсь, што дадаецца пазней.

4. Стварэнне базы дадзеных

Этап 4 «Стварэнне базы дадзеных» працюе найкраща, калі яго розглядаць як меркаваную плошчу. Зафіксавайце адны ідеальны прыклад, адзін прыклад неудачы і запіс пра вярнэнне да пачатковага стану, перш чым расширваць масштабы. Валіце маленькія, тэставаныя елементы замест большых скрыптов. Калі якісь крок не выйшоў, прычына неудачы павінна вказываць на адную адповядальнасць, а не на заплутаны ланцужок задач. Рэгулюйце стан графа так, каб ён быў простым і з адзіным типам дадзеных. Вярнутыя структуры дадзеных маскуюць інфармацыю пра тое, який вузел запісаў канкрэтны поле, і спакойваюць працу пасля перерываў.

# data/db.py - Database initialization and operations

import sqlite3
import os
from datetime import datetime, timedelta

DB_PATH = os.path.join(os.path.dirname(__file__), "clinic.db")

def get_connection():
    """Get a database connection."""
    return sqlite3.connect(DB_PATH)

def init_db():
    """Initialize the database with tables and sample data."""
    conn = get_connection()
    cursor = conn.cursor()

    # Doctors table
    cursor.execute("""
        CREATE TABLE IF NOT EXISTS doctors (
            doctor_id TEXT PRIMARY KEY,
            doctor_name TEXT NOT NULL,
            speciality TEXT NOT NULL,
            office_timing TEXT NOT NULL
        )
    """)

    # Customers table
    cursor.execute("""
        CREATE TABLE IF NOT EXISTS customers (
            customer_id TEXT PRIMARY KEY,
            name TEXT NOT NULL,
            phone TEXT NOT NULL
        )
    """)

    # Bookings table
    cursor.execute("""
        CREATE TABLE IF NOT EXISTS bookings (
            booking_id TEXT PRIMARY KEY,
            doctor_id TEXT NOT NULL,
            customer_id TEXT NOT NULL,
            appointment_date TEXT NOT NULL,
            appointment_time TEXT NOT NULL,
            status TEXT NOT NULL,
            FOREIGN KEY (doctor_id) REFERENCES doctors (doctor_id),
            FOREIGN KEY (customer_id) REFERENCES customers (customer_id)
        )
    """)

    # Insert sample doctors
    doctors = [
        ("D1", "Dr. Anil Sharma", "General Physician", "10:00-14:00"),
        ("D2", "Dr. Neha Verma", "Dermatologist", "11:00-16:00"),
        ("D3", "Dr. Rohit Mehta", "Orthopedic", "09:00-13:00"),
        ("D4", "Dr. Kavita Rao", "Pediatrician", "10:00-15:00"),
        ("D5", "Dr. Sanjay Iyer", "ENT Specialist", "12:00-17:00"),
    ]

    for doctor in doctors:
        cursor.execute(
            "INSERT OR IGNORE INTO doctors (doctor_id, doctor_name, speciality, office_timing) VALUES (?, ?, ?, ?)",
            doctor
        )

    conn.commit()
    conn.close()

if __name__ == "__main__":
    print("Initializing database...")
    init_db()
    print("Database initialized successfully.")
cd clinic-agent
python data/db.py

Частка 2: Слой службы (інструменты для агента)

Этап слоя абстракцыі Сэрвіса 2 працюе наяўней, калі яго спрыяваць як мерыемую паверхню. Зафіксавайце адна ідеальная транскрыпцыя, адзін прыклад неудачы і запіс пра вярнэнне да пачатковага стану перш чым расширваць сферу дзеяння. Спрыявайце гэты этап як кантракт межа вхіднымі дадзеннямі і перакананымі выходнымі рэзультатамі. Дайце назвы артыфактам, задаць критэрыя успеху і не падзеўляйцеся частым, непূরным выкананнем задач. Адкройце інструменты з вузкімі схемамі та чысткімі пазначэннямі побачных эфектаў. Адпаведальныя за хоставанне должны знать, якія вызовы мутуюць стан, перш чым автаматычна схваліць іх.

Сэрвіс доктара (services/doctor_service.py)

Сэрвіс Doctor Service у стадыі doctorservice працюе наяўнейша, калі яго спрыяваць як мерыемую паверхню. Зберагуце адна ідеальная транскрыпцыя, адзін прыклад неудачы і запіс пра вярнэнне да пачатковага стану перад расшырэнням масштаба. Запісвайце часы выконання і кост токена або запиту па боку функцыональных рэзультатаў. Відразлівае прадставленне костаў з’являецца рана, чым утвараюцца неспакойныя рахункі, калі процес пераходзіць з дэмовай среды ў спяльнаныя сераўеры. Храніце стан графа як плоскі і з адначытаемымі даннымі. Вярнутыя блокі маскуюць інфармацыю пра тое, який вузел запісаў канкрэтны поле, і спакшваюць продовжэнне роботы пасля перарываў. Сэрвіс Doctor Service у стадыі doctorservice працюе наяўнейша, калі яго спрыяваць як мерыемую паверхню. Зберагуце адна ідеальная транскрыпцыя, адзін прыклад неудачы і запіс пра вярнэнне да пачатковага стану перад расшырэнням масштаба. Дакументавайце як успішны, так і вярнучыся шляхы роботы. Перапрыбуткі, людзкія контрольныя пункты і обработка некоректных паведамленняў є часткай продукту, а не чымсь, што дадаецца пазней.

# services/doctor_service.py - Doctor operations

from data.db import get_all_doctors, get_doctor_by_speciality, get_doctor_by_id

def get_specialities_list():
    """Get list of all specialities."""
    doctors = get_all_doctors()
    # Return unique specialities
    return list(dict.fromkeys([doc[2] for doc in doctors]))

def get_doctor_info(speciality):
    """Get doctor information by speciality."""
    doctor = get_doctor_by_speciality(speciality)
    if doctor:
        return {
            "doctor_id": doctor[0],
            "doctor_name": doctor[1],
            "speciality": doctor[2],
            "office_timing": doctor[3]
        }
    return None

def generate_time_slots(office_timing):
    """Generate hourly time slots from office timing string.

    Args:
        office_timing: String like "11:00-16:00"

    Returns:
        List of time slots like ["11:00 AM", "12:00 PM", ...]
    """
    start_time, end_time = office_timing.split("-")
    start_hour = int(start_time.split(":")[0])
    end_hour = int(end_time.split(":")[0])

    slots = []
    for hour in range(start_hour, end_hour):
        if hour < 12:
            suffix = "AM"
            display_hour = hour if hour > 0 else 12
        elif hour == 12:
            suffix = "PM"
            display_hour = 12
        else:
            suffix = "PM"
            display_hour = hour - 12
        slots.append(f"{display_hour}:00 {suffix}")

    return slots

def parse_time_slot(slot_str):
    """Parse time slot string to 24-hour format.

    Args:
        slot_str: String like "1:00 PM"

    Returns:
        String like "13:00"
    """
    time_part, suffix = slot_str.split(" ")
    hour, minute = time_part.split(":")
    hour = int(hour)

    if suffix == "PM" and hour != 12:
        hour += 12
    elif suffix == "AM" and hour == 12:
        hour = 0

    return f"{hour:02d}:{minute}"

Сэрвіс броніравання (services/booking_service.py)

Для стадіі Booking Service services bookingservice неабяжна прадзеўкаванне вхідных дадзеных, адпаведнага адпаведальнага за шаг і крэтарыяў выходу пры перадзеўкаванні коду. Аператары должны магчымае запускать шаг з вядомай точкі контролю, не падозрываючы схованы статус. Лепш выбіраць маленькія, тэставаныя елементы замест большых скрыптов. Калі шаг не выконваецца, прычына нехарактэрыстыкі должна вказываць на адну конкрэтную адпаведальнасць, а не на заплутаны процес. Неабяжна людская апраўда ў тых моментах, калі выконваюцца расходы або зменяюцыся даныя у працэсе. Компіляцыйныя налашчэнні не ўзначаюць павнае выкананне бізнес-функцый.

# services/booking_service.py - Booking operations

import uuid
from datetime import datetime
from data.db import (
    create_customer,
    create_booking,
    get_customer_by_phone,
    get_bookings_by_doctor_and_date,
    get_booking_by_id
)
from services.doctor_service import parse_time_slot

def get_or_create_customer(name, phone):
    """Get existing customer or create new one."""
    customer = get_customer_by_phone(phone)
    if customer:
        return customer[0]  # Return customer_id

    customer_id = f"CUST-{uuid.uuid4().hex[:6].upper()}"
    create_customer(customer_id, name, phone)
    return customer_id

def get_available_slots(doctor_id, office_timing):
    """Get available time slots for a doctor for today.

    Args:
        doctor_id: Doctor ID
        office_timing: Office timing string like "11:00-16:00"

    Returns:
        List of available time slots
    """
    from services.doctor_service import generate_time_slots

    today = datetime.now().strftime("%Y-%m-%d")
    all_slots = generate_time_slots(office_timing)

    # Get booked slots
    booked_times = get_bookings_by_doctor_and_date(doctor_id, today)

    # Filter out booked slots
    available = []
    for slot in all_slots:
        slot_24h = parse_time_slot(slot)
        if slot_24h not in booked_times:
            available.append(slot)

    return available

def confirm_booking(doctor_id, customer_name, customer_phone, time_slot, appointment_date=None):
    """Confirm a booking.

    Args:
        doctor_id: Doctor ID
        customer_name: Customer name
        customer_phone: Customer phone
        time_slot: Time slot like "1:00 PM"
        appointment_date: Optional date in YYYY-MM-DD format. Defaults to today.

    Returns:
        Booking ID
    """
    # Get or create customer
    customer_id = get_or_create_customer(customer_name, customer_phone)

    # Generate booking ID
    booking_id = f"BKG-{uuid.uuid4().hex[:6].upper()}"

    # Format appointment time
    if not appointment_date:
        appointment_date = datetime.now().strftime("%Y-%m-%d")
    appointment_time = parse_time_slot(time_slot)

    # Create booking
    create_booking(booking_id, doctor_id, customer_id, appointment_date, appointment_time)

    return booking_id

Тэставанне службы

Для стадіі тэставання службы неабяжна прадзефінаваць вхідныя даны, адпаведальнага за кожны крок і крэтыяры завершэння пры зміне коду. Аперацыйныя працавнікі должны магчымаць перзапуск кроку з вядомай точкі контролю, не падозрываючы прыхованы стан. Спрыяйце гэтай стадіі як кантракту межа вхіднымі данымі і перакананымі выходнымі рэзультатамі. Даўце назвы артыфактам, прадзефінаваць крэтыяры успеху і адмовіцеся ад бяспечнага частковага завершэння. Заставіце людзкую апраўду ў тых випадках, калі выконваюцца витраты чы зміняюцыся даны для працы. Компіляцыйныя налашчэння не ўзроўнаўцяюцься з пачатковым станом бізнесу.

# test/test_service.py - to test the services created

from pathlib import Path
import sys

# Allow running this file directly: `python test/test_service.py`.
PROJECT_ROOT = Path(__file__).resolve().parents[1]
if str(PROJECT_ROOT) not in sys.path:
    sys.path.insert(0, str(PROJECT_ROOT))

from services.doctor_service import get_specialities_list, generate_time_slots
from services.booking_service import confirm_booking

# Get all specialities
specialities = get_specialities_list()
print("Available specialities:", specialities)

# Generate time slots for a doctor (11:00 AM - 4:00 PM)
slots = generate_time_slots("11:00-16:00")
print("Available slots:", slots)

# Confirm a booking
booking_id = confirm_booking(
    doctor_id="D1",
    customer_name="John Doe",
    customer_phone="9876543210",
    time_slot="2:00 PM"
)
print(f"Booking confirmed: {booking_id}")
cd clinic-agent
python test_service.py
(.venv) (base) my-mac clinic-agent % python test_service.py
Available specialities: ['General Physician', 'Dermatologist', 'Orthopedic', 'Pediatrician', 'ENT Specialist']
Available slots: ['11:00 AM', '12:00 PM', '1:00 PM', '2:00 PM', '3:00 PM']
Booking confirmed: BKG-C4F60A

Частка 3: Агентны шар

Для стадіі Агентнага шара часткі 3 неабяцо практычна апрацаваць вхідных дадзеных, выявлення адпаведнага адпаведальніка за крок і вакабаленыя крэтыры завершэння пры перадзеіснаванні коду. Аператары должны магчымаць перапрацаваць крок з вядомага пункта контролю без неабяцовага адгадвання захаваных станоў. Запісваюць час выконання і кост токенаў або запытаў праза функцыйнае рэзультат. Відкрытыя данні пра косцы запобегаюць неспакоўным рашчыткам, калі процес пераходзіць з дэмавай версіі ў спяльныя среды. Апрацаваць людскія празгляды для тых крокаў, якія витрачаюць грошы або зменяюць данні ў працэсе. Компіляцыйныя налашчэння не ўсё равнася цэлысці бізнес-процэса. Для стадіі Агентнага шара часткі 3 неабяцо практычна апрацаваць вхідных дадзеных, выявлення адпаведнага адпаведальніка за крок і вакабаленыя крэтыры завершэння пры перадзеіснаванні коду. Аператары должны магчымаць перапрацаваць крок з вядомага пункта контролю без неабяцовага адгадвання захаваных станоў. Неабяцо задокументаваць як шлях успеху, так і шлях вярнення да нормальнага стану. Перапрыбуткі, людскія празгляды і апрацавка некоректных запытаў є часткай самага продукту, а не чымсь, што дадаецца пазней.

Статус бронівкі:

Калі працуеце з стадзіяй «Статус бронівкі», спачатку запісаце кантракт: неабяжлівыя даны, сігнал успеху і тое, што выходзіць у разе частковага нявыпання. Такі список пераконтроўкаў дапамагае заліцьвачыць пазнейшыя змены ў кодзе. Лепш выбіраць маленькія, тэставаныя елементы замест вялікіх скрыптав. Калі якісь крок не выйшае, прычына нявыпання павінна вказваць на адну конкрэтную адпаведальнасць, а не на заплутаны ланцужок задач. Зробіце пераконтроўку пасля дорогіх крокаў. Програма не павінна зноў выкарыстоўваць той самы вызов LLM, калі аператар прабуе зноў выконаць пазнейшы элемент.

# agents/booking_agent.py - LangGraph agent implementation

from typing import TypedDict, Annotated, List, Optional
from langgraph.graph import StateGraph, END
from openai import OpenAI
import os
from dotenv import load_dotenv

load_dotenv()
client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))

class BookingState(TypedDict):
    """State for the booking conversation."""
    messages: List[dict]                    # Chat history
    stage: str                               # greeting, select_speciality, select_doctor, etc.
    selected_speciality: Optional[str]      # Chosen medical specialty
    selected_doctor: Optional[dict]         # Selected doctor details
    selected_date: Optional[str]            # Appointment date
    selected_slot: Optional[str]            # Time slot
    customer_name: Optional[str]            # Customer name
    customer_phone: Optional[str]           # Customer phone
    booking_id: Optional[str]               # Confirmation ID
    available_options: List[str]            # UI options

def create_initial_state():
    """Create initial state for the conversation."""
    return {
        "messages": [],
        "stage": "greeting",
        "selected_speciality": None,
        "selected_doctor": None,
        "selected_date": None,
        "selected_slot": None,
        "customer_name": None,
        "customer_phone": None,
        "booking_id": None,
        "available_options": []
    }

Функцыя-падрабнік LLM

Калі працюеце над стадзіяй LLM Helper Function, спачатку запісайце контракт: неабяжлівыя вхідныя даны, сігнал успеху і тое, што выходзіць у разе частковага невыпання. Такі чэк-ліст дапамагае заставіць пазнейшыя змены коду быць чыстымі. Спрэтавайцеся да гэтай стадзіі як да контракту межа вхіднымі данымі і перакананымі выходнымі рэзультатамі. Дайце назвы артыфактам, задаце перакананні на успех і адмовіцеся ад тыхоўага частковага завершэння. Зберагачвайце стабільныя інструкцыі системы і схемы інструментаў. Перадача тых самых прамэры ёсць частым выклікам затрат ресурсаў.

# agents/booking_agent.py - LangGraph agent implementation
def call_llm(
    system_prompt: str,
    user_prompt: str,
    *,
    model: str = "gpt-4o-mini",
    temperature: float = 0,
    max_tokens: int = 50,
) -> str:
    """
    Centralized helper for all LLM calls.
    Returns the assistant's response
    """
    try:
        response = client.chat.completions.create(
            model=model,
            messages=[
                {"role": "system", "content": system_prompt},
                {"role": "user", "content": user_prompt},
            ],
            temperature=temperature,
            max_tokens=max_tokens,
        )
        return response
    except Exception as e:
        print(f"LLM call error: {e}")
        return ""

Agent Nodes

Калі працуеце з этапам Агентных вузлаў, спачатку запісайце умовы викорыстоўвання: неабходныя даны, сігнал успеху і тое, што выходзіць па частковай нявыполненасці. Такі список дапамагае залічыць усі змяны ў кодзе чыста.

Стварэнне графа

Этап стварэння графа працюе найкраща, калі яго розглядаць як вимерную паверхню. Запісаўце адна «золатая» транскрыпцыя, адин прыклад неудачы і прыметку па поверненню да пярвінскага стану пры расшырэнні масштаба. Валіце маленькія, тэставаныя елементы замест большых скрыптов. Калі якісь крок не выйшае, прычына неудачы павінна вказываць на адную адпаведальнасць, а не на заплутаны ланцюг задач. Храніце стан графа у простаму, типаванаму формате. Вкладныя структуры маскуюць інфармацыю пра тое, який вузел запісаў які поле, і спакоююць працэз восстанавлення пасля перерываў.

# agents/booking_agent.py - LangGraph agent implementation
from langgraph.checkpoint.memory import MemorySaver

def build_booking_graph():
    """Build the LangGraph workflow."""
    workflow = StateGraph(BookingState)

    # Add all nodes
    workflow.add_node("greeting", greeting_node)
    workflow.add_node("select_speciality", select_speciality_node)
    workflow.add_node("select_doctor", select_doctor_node)
    workflow.add_node("select_date", select_date_node)
    workflow.add_node("select_slot", select_slot_node)
    workflow.add_node("confirm", confirm_node)
    workflow.add_node("collect_details", collect_details_node)
    workflow.add_node("completed", completed_node)
    workflow.add_node("cancelled", cancelled_node)

    # Set entry point
    workflow.set_entry_point("greeting")

    # Add conditional edges based on routing
    workflow.add_conditional_edges(
        "greeting",
        llm_router,
        {
            "greeting": "greeting",
            "select_speciality": "select_speciality",
            "cancelled": "cancelled"
        }
    )

    # Similar conditional edges for other nodes...

    # Final edges to END
    workflow.add_edge("completed", END)
    workflow.add_edge("cancelled", END)

    # Compile with checkpointer for session management
    return workflow.compile(checkpointer=MemorySaver())

# Create the compiled graph
booking_graph = build_booking_graph()
#agents/save_langgraph_flow.py
"""Save the clinic booking LangGraph flow to png format in this folder."""


from pathlib import Path
import sys


# Ensure imports work whether the script is run from project root or this folder.
AGENTS_DIR = Path(__file__).resolve().parent
PROJECT_ROOT = AGENTS_DIR.parent
if str(PROJECT_ROOT) not in sys.path:
    sys.path.insert(0, str(PROJECT_ROOT))

from agents.booking_agent import booking_graph  # noqa: E402


def save_graph_files() -> None:
    """Export graph as PNG."""
    graph = booking_graph.get_graph()

    png_path = AGENTS_DIR / "langgraph_flow.png"
    png_data = graph.draw_mermaid_png()
    png_path.write_bytes(png_data)
    print(f"Saved PNG flow to: {png_path}")


if __name__ == "__main__":
    save_graph_files()
cd clinic-agent
python save_langgraph_flow.py

Процэс выканання звесці:

Этап «Паведамленне працэсу» работае наяўней, калі яго спрыяваць як мерыемую паверхню. Зафіксавайце адна ідеальная версія даных, адзін прыклад неудачы і запіс пра вярнэнне да пачатковага стану, перш чым расширваць сферу дзеяння. Спрыявайце гэты этап як кантракт межа вхіднымі даннымі і перакананымі выходнымі рэзультатамі. Даўце назвы артыфактам, задаць критэрыя успеху і не падзеўляйцеся частым, непূরным выкананнем задачы. Храніце стан графа ў простам і типаваным формате. Вкладзеныя блокі данных маскуюць інфармацыю пра тое, який вузел запісаў кожны поле, і спакоююць продажчыку роботы пасля перарываў.

# agents/booking_agent.py - LangGraph agent implementation
def process_message(state: BookingState, user_message: str, thread_id: str = "default_session") -> BookingState:
    """Process a user message through the booking graph."""
    config = {"configurable": {"thread_id": thread_id}}

    # Check if the graph is currently interrupted
    current_state = booking_graph.get_state(config)

    if current_state.tasks and current_state.tasks[0].interrupts:
        # Resume the graph with the user's message
        result = booking_graph.invoke(Command(resume=user_message), config=config)
    else:
        # No interrupt, so start/continue normally
        # Add user message to state (unless it's an initial trigger)
        if user_message.lower() != "hi" or state["messages"]:
            # Avoid duplicate user messages if already added
            if not state["messages"] or state["messages"][-1].get("content") != user_message:
                state["messages"].append({
                    "role": "user",
                    "content": user_message
                })
        # Run the graph
        result = booking_graph.invoke(state, config=config)

    # Update available_options and ensure message is in history
    snapshot = booking_graph.get_state(config)
    if snapshot.tasks and snapshot.tasks[0].interrupts:
        interrupt_value = snapshot.tasks[0].interrupts[0].value

        # Handle both dict and string interrupt values
        msg_content = ""
        options = []
        if isinstance(interrupt_value, dict):
            msg_content = interrupt_value.get("content", "")
            options = interrupt_value.get("available_options", [])
        else:
            msg_content = str(interrupt_value)

        # Ensure the interrupt message is in the chat history
        if msg_content:
            # Check if it was already added by the node
            last_msg_content = result["messages"][-1].get("content", "") if result["messages"] else ""
            if last_msg_content != msg_content:
                result["messages"].append({
                    "role": "assistant",
                    "content": msg_content,
                    "options": options
                })
            else:
                # If already added, just update it with options if missing
                result["messages"][-1]["options"] = options

        result["available_options"] = options
    else:
        # If not interrupted, use whatever set in state, or default to empty
        if "available_options" not in result:
            result["available_options"] = []

    return result

Тэставанне агента

Этап «Тэсты агента» працюе найкраща, калі яго спрыяваць як меркавыя показнікі. Зберагчыце адна ідеальная версія рэзультата, адзин случай неудачы і прыметку па вярнэнню да пачатковага стану, перш чым расширваць сферу працы. Запісвайце час выканання задач і кост токеноў або запытаў разам з функцыйнальнымі рэзультатамі. Відразлівае прадставленне костаў з’являецца рана, таму не будзе неспакою, калі праця перейдзе з дэмовай среды ў спільныя сераўеры. Храніце стан графа ў простам і типаваным формате. Вярнутыя структуры данных маскуюць інфармацыю пра тое, який вузел запісаў кожны поле, і спакшваюць продовжэнне роботы пасля перерываў. Этап «Тэсты агента» працюе найкраща, калі яго спрыяваць як меркавыя показнікі. Зберагчыце адна ідеальная версія рэзультата, адзин случай неудачы і прыметку па вярнэнню да пачатковага стану, перш чым расширваць сферу працы. Дакументавайце як успешны, так і вярнучыся паходы роботы адночасна. Перапрыбуткі, людзкія контрольныя пункты і обработка некоректных паведамленняў є часткай продукту, а не чымсь, што дадаецца пазней.

# test/test_agent.py - test agent
# Initialize state

from pathlib import Path
import sys

# Allow running this file directly: `python test/test_agent.py`.
PROJECT_ROOT = Path(__file__).resolve().parents[1]
if str(PROJECT_ROOT) not in sys.path:
 sys.path.insert(0, str(PROJECT_ROOT))

from agents.booking_agent import create_initial_state, process_message


state = create_initial_state()

# Process messages
state = process_message(state, "Hi", thread_id="session_1")
print(state["messages"][-1]["content"])

state = process_message(state, "I want to book", thread_id="session_1")
print(state["available_options"])
cd clinic-agent
python test/test_agent.py

Частка 4: UI Streamlit

Для стадіі UI Streamlit у частцы 4 неабяжна практычна адзначэнне вхідных дадзеных, адпаведальнага за крок і крэтарыяў выходу пры перадзеіснаванні коду. Аператары должны магчымаць паўтарнае адзеіснаванне кроку з вядомага пункта контролю, не прыпускаючы сустояння, якое залишаецца незрозумелым. Лепей выбіраць маленькія, тэставаныя елементы замест большых скрыптов. Калі крок не выйшаў, прычына неудачы павінна вказываць на адзін конкрэтны элемент, а не на заплутаны ланцоўкі задач. Неабяжна людскае затверджэнне для тых крокаў, якія ведуць да выдаткаў грошаў або зміняюць даны, якія викорыстоўваюцца у працэсе. Працэс кампілявання не є гарантыяй полнай адпаведнасці програмы выкананню бізнес-трэбаванняў.

# ui/chat_ui.py - Streamlit chatbot interface

"""Streamlit UI for the clinic booking chatbot."""

import streamlit as st
from agents.booking_agent import create_initial_state, process_message
from data.db import init_db


def initialize_session():
    """Initialize session state."""
    if "state" not in st.session_state:
        st.session_state.state = create_initial_state()
    if "initialized" not in st.session_state:
        st.session_state.initialized = False
    if "session_id" not in st.session_state:
        import uuid
        st.session_state.session_id = str(uuid.uuid4())


def display_chat_history():
    """Display the chat history with persistent options and styling."""
    messages = st.session_state.state.get("messages", [])
    for i, message in enumerate(messages):
        if message["role"] == "assistant":
            with st.chat_message("assistant"):
                st.markdown(message["content"])

                # Show options if they exist
                options = message.get("options", [])
                if options:
                    # If this is the last message in history, show as clickable buttons
                    if i == len(messages) - 1 and st.session_state.state["stage"] not in ["completed", "cancelled"]:
                        st.markdown("---")
                        # Create columns for buttons
                        cols = st.columns(min(len(options), 3))
                        for idx, option in enumerate(options):
                            col_idx = idx % 3
                            with cols[col_idx]:
                                if st.button(option, key=f"btn_{i}_{idx}", use_container_width=True):
                                    handle_user_input(option)
                    else:
                        # For older messages, show options as pills/text to keep history
                        options_str = "  ".join([f"`{opt}`" for opt in options])
                        st.markdown(f"**Available options:** {options_str}")
        else:
            with st.chat_message("user"):
                st.markdown(message["content"])


def handle_user_input(user_input: str):
    """Handle user input and process through agent."""
    # Process the message
    st.session_state.state = process_message(
        st.session_state.state,
        user_input,
        thread_id=st.session_state.session_id
    )

    # Rerun to update UI
    st.rerun()


def run_chat_ui():
    """Run the chat UI."""
    # Page config
    st.set_page_config(
        page_title="CarePlus Clinic - Book Appointment",
        page_icon="🏥",
        layout="centered"
    )

    # Custom CSS for distinction between messages
    st.markdown("""
        <style>
        [data-testid="stChatMessageUser"] {
            flex-direction: row-reverse;
            text-align: right;
            background-color: #e0f2f1;
            border-radius: 15px 15px 0px 15px;
        }
        [data-testid="stChatMessageAssistant"] {
            background-color: #f5f5f5;
            border-radius: 15px 15px 15px 0px;
        }
        </style>
    """, unsafe_allow_html=True)

    # Initialize database
    init_db()

    # Initialize session
    initialize_session()

    # Header
    st.title("🏥 CarePlus Clinic")
    st.markdown("*Book your doctor appointment easily*")
    st.markdown("---")

    # Send initial greeting if not initialized
    if not st.session_state.initialized:
        st.session_state.state = process_message(
            st.session_state.state,
            "Hi",
            thread_id=st.session_state.session_id
        )
        st.session_state.initialized = True
        st.rerun()

    # Display chat history
    display_chat_history()

    # Chat input (only show if not completed)
    if st.session_state.state["stage"] not in ["completed", "cancelled"]:
        if prompt := st.chat_input("Type your message here..."):
            handle_user_input(prompt)
    else:
        # Show restart button after completion
        st.markdown("---")
        if st.button("🔄 Start New Booking", use_container_width=True):
            st.session_state.state = create_initial_state()
            st.session_state.initialized = False
            st.rerun()

Частка 5: Тэчкі входу ў прыемніку

Для стадіі введзення дакларацыі ў частцы 5 неабходна пазначыць вхідныя данні, адпаведальную особу за выкананне крока і критэрыя завершэння прычым перад зменайом коду. Аператары должны магчымаць перзапуск крока з вядомай точкі контролю, не прабуючы спадарожваць схованы стан. Спрыяйце цій стадіі як даговору межаў вхідных і перакананых выходных дадзенняў. Даць назвы артыфактам, пазначыць критэрыя успеху і не прымаць тыхню частковую завершэннасць. Заставіць людзкую апраўдку для тых крокаў, якія выкарыстоўваюць грошы або зменяюць данні ў працэсе вырабоцтва. Компіляцыйныя налашчэнні не ўзроўнаўцяюцься з пачатковай цэласнасцю бізнес-процэсу.

# app.py - Main entry point for the application

from ui.chat_ui import run_chat_ui

if __name__ == "__main__":
    run_chat_ui()

Адкліканне чат-бота: два падходы

Для дваэтапнага адканання чатбота неабяжна пазначыць вхідныя даны, адпаведнага адпаведальнага за крок і крэтыяры выходу пры перадзеіснавленні коду. Аператары должны магчымаць парадзеіснавленне кроку з вядомага пункта контролю без неабяжнай адгадвання скрытага стану. Запісваць час выконання і кост токенаў або запытаў разам з функцыйнальнымі рэзултатамі. Відразы костаў з самага пачатку запобегае неспакойным рахункам, калі працэс пераходзіць з дэмавайнтару ў спяльныя среды. Заставіць людзкую апраўдку для тых крокаў, якія витрачаюць грошы або зменяюць даны ў працэсе. Працэс кампілявання не ўзначае повнайшага адпрацоўвання бізнес-функцый.

Падход 1: Веб-дзеянне Streamlit

Для стадіі Streamlit Web парадыгмы №1 неабяжна з’явіць вводныя данныя, адпаведальнага за крок і крэтарыя выходу пры перадзеіснаванні коду. Аператары должны магчымаць перзапуск крока з вядомай точкі контролю, не падозрываючы схованы стан. Конфігурацыю трэба залічыць пазначкай ад коду прыемліка. Файлы сераўнавання, сховішчы секрэтных данных і флагі функцый належыць у аднам месца, якое аператары можаць пераглядзець, не чытаяўшы весь лянцуг задач. Прызначыць людскія апраўдкі для рэлей, якія витрачаюць грошы або зменяюць данныя у працэйным режыме. Падключэння ў час компілявання не адпавядае пачатковай цэлесообразнасі бізнес-процэсаў.

streamlit run app.py

Парадыгма №2: Jupyter Notebook

Для стадіі Jupyter Notebook падчынства 2 неабяжна ўзначыць вхідныя даны, адпаведальнага за крок і критэрыя завершэння пры перадзеіснаванні коду. Аператары должны магчымаць перзапуск кроку з вядомай точкі контролю, не падозрываючы схованы стан. Неабяжна задокументаваць як шлях успеху, так і шлях вярнення. Перапрыбуткі, людзкія перакрыцці та обробка некоректных паведамленняў є часткай продукту, а не наступным этапам дапрацоўкі. Неабяжна застаўляць людзкую затверджэнняе для тых крокаў, якія выкарыстоўваюць грошы або зменяюць даны у працэсе. Працэс складання коду не ўзначае повнайшага адпрацоўвання продукту з точкі зору бізнеса.

# clinic-agent.ipynb
from services.doctor_service import get_specialities_list, get_doctor_info, generate_time_slots
from services.booking_service import confirm_booking
from agents.booking_agent import (
    BookingState,
    create_initial_state,
    build_booking_graph,
    process_message
)
# clinic-agent.ipynb
# Initialize the booking graph
booking_graph = build_booking_graph()

## Visualize the booking graph structure
from IPython.display import Image, display

png_bytes = booking_graph.get_graph().draw_mermaid_png()
display(Image(png_bytes))

Запуск сесіі чатбота:

Для стадіі «Выкананне сесіі чатбота» неабяжна прадзефінаваць вхідныя даны, адпаведальнага за крок і крэтыяры завершэння пры перадзеўранні коду. Аператары должны магчымаць перывыканне кроку з вядомай точкі контролю без неабяжнага вычыслення схованых станоў. Лепш выбіраць маленькія, тэставаныя елементы замест большых скрыптов. Калі крок не выканаецца, прычына неудачы должна вказываць на адну конкрэтную адпаведальнасць, а не на заплутаны процес. Неабяжна ўключыць людзкую апраўдку для тых крокаў, якія выкарыстоўваюць грошы або зміняюць даны ў працэсе виробніцтва. Праця ў часе компілявання не абавесць цэлыснасцю бізнес-процеса.

# clinic-agent.ipynb
from langgraph.types import Command

def run_booking_session(graph, thread_id="notebook_session", reset=False):
    config = {"configurable": {"thread_id": thread_id}}

    # 1. Start or Reset logic
    current_state = graph.get_state(config)
    if reset or not current_state.values:
        print(f"--- {'🔄 Resetting' if reset else '🆕 Initializing'} Session ---")
        # Using invoke() here kicks off the 'greeting' node immediately
        graph.invoke(create_initial_state(), config=config)

    print("---⚕⚕ Starting CarePlus Booking Session ---")

    last_displayed_message_idx = -1  # Track which messages have been displayed

    while True:
        state = graph.get_state(config)

        # Display any new assistant messages that haven't been shown yet
        # (This handles guardrail/off-topic responses)
        if state.values and state.values.get('messages'):
            messages = state.values['messages']
            for idx in range(last_displayed_message_idx + 1, len(messages)):
                msg = messages[idx]
                if msg.get("role") == "assistant":
                    print(f"\n[AI]: {msg['content']}")
            last_displayed_message_idx = len(messages) - 1

        # 2. Check for Interrupts
        if state.tasks and state.tasks[0].interrupts:
            interrupt_info = state.tasks[0].interrupts[0].value

            # --- FIX: Safely handle both String and Dict interrupts ---
            if isinstance(interrupt_info, dict):
                ai_message = interrupt_info.get('content', 'No message content')
                options = interrupt_info.get('available_options', [])
            else:
                ai_message = interrupt_info
                options = []

            print(f"\n[AI]: {ai_message}")
            if options:
                print(f"Options: {', '.join(options)}")
            # -------------------------------------------------------

            user_input = input("\n[YOU]: ")
            print(f"[YOU]: {user_input}")

            # Resume the graph with the user's input
            graph.invoke(Command(resume=user_input), config=config)

        # 3. Check if the graph has finished
        elif not state.next:
            # Before ending, check if there's a final assistant message to print
            if state.values and state.values.get('messages') and state.values['messages'][-1]["role"] == "assistant":
                if last_displayed_message_idx < len(state.values['messages']) - 1:
                    print(f"\n[AI]: {state.values['messages'][-1]['content']}")
            print("\n--- ⚑⚑ Session Ended ---")
            break

        # 4. If nodes are pending but no interrupt, let them run (the gas pedal)
        else:
            graph.invoke(None, config=config)

# IMPORTANT: Set reset=True only when you want to wipe the history.
# Set it to False to actually continue the conversation!
run_booking_session(booking_graph, reset=True)

Вывад:

У стадії Заключэння неабяжна практычна вызначыць інпуты, адпавядающага за крок адпаведальнага, а таксама крэтырыя выходу пры перадзеіснаванні коду. Аператары должны магчымаць перзапуск кроку з вядомай точкі контролю, не падозрываючы схованы стан. Спрыяйце гэтай стадіі як даговору межа інпутамі та падтверджанымым выходам. Дайце назвы артыфактам, вызначыце пераканальнія для успеху та адмовіцеся ад беззвучнага частковага завершэння. Заставіце людзкую апраўду для тых крокаў, якія витрачаюць грошы чы выконваюць змяны ў продакшн-данных. Компіляцыйныя налашчэння не ўзроўнаўцяюцься з павнайсткамасным завершэнням задачы.

Справакі:

У стадії Рэферэнсаў неабходна прадзефінаваць вхідныя даны, адпаведальную за крок і крэтынія выходу пры перамены коду. Аператары должны магчымае запускваць крок з вядомай точкі контролю, не падозрываючы прыхованы стан. Запісваць час выконання і кост токенаў або запытаў разам з функцыйнальнымі рэзултатамі. Відразы костаў з самага пачатку запобегае неспакойным рахункам, калі процес пераходзіць з дэмаверсіі ў спяльныя среды. Апраўдка чалавека патрэбна для тых крокаў, якія выкарыстоўваюць грошы або зміняюць даны ў працэсе. Кампайляванне коду не абавесць цэліснасцю бізнес-процесаў. У стадії Рэферэнсаў неабходна прадзефінаваць вхідныя даны, адпаведальную за крок і крэтынія выходу пры перамены коду. Аператары должны магчымае запускваць крок з вядомай точкі контролю, не падозрываючы прыхованы стан. Неабходна задокументаваць як «шчаслівы» шлях, так і шлях вяснавання ситуацыі. Перапрыбуткі, апраўдка чалавека і обработка нераспакаваных паведамленняў є часткай продукту, а не етапамі далейшага доўнелення.

Чэкліст аператыўнай роботы

На стадзіі чэкліста аператыўнай роботы неабходна праказаць вхідныя даны, адпаведальнага за кожны крок і крэтыяры завершэння пры змены коду. Аператары должны магчыма было перзапускаць крок з вядомай точкі контролю, не падозрываючы прыхованы стан.

Конфігурацыю трэба зберагаць паза кодам прыкладнення. Файлы сяродавішча, хранальнікі секрэтных дадзеных і флагі функцыйяў должны знаходзіцца ў аднам месцы, якое аператары можаць пераглядаць, не чытаючы весь код.

Неабходна адзначаць людскія празгляды для тых крокоў, якія ведуць да выдатка грошаў або змены дадзеных у прыемніку. Працэс кампайлявання не є падтверджэннем полныя адпаведнасці продукту выкананню задач.

Напісце кароткі посібнік: як роцыяваць клучы, як спрачыслаць чергу, як анулюваць пярэдніе дадзеныя.

Дакументаваць трэба як шляхы успеху, так і шляхі вяснавання проблем. Перапрыбуткі, людскія перагляды і обробка некоректных паведамленняў є часткай продукту, а не чымсь, што дадаецца пазней.

Неабяжна людская празначэнне для тых элементаў, якія выдвайуць грошы або зменяюць данні праработкі. Кампайл-тайм вяроўнаванне не ўзначае пачытнасці бізнес-процэсаў.

Перш чым пераводзіць стэк, заморажваюце версіі, фіксуюце «золаты» транскрыпты для критычных шляхоў і парабяжваеце крокі анулювання. У спадзяльных средах неабходны ліміты частоты, перакананні ў прыналежнасці тэнантам і чысткі власнік для ротацыі секрэтных даных. Лепш выбіраць простую надзеяннасць, чым хітрыя експерыментальныя дэманстрацыі.

Запіс для пакета 9744ef4a5b25: не кладзіце ключы прадаўцаў у репозітарый, задаце верхнюю межу токенаў на сесію і храніце транскрыпты празаўседы ў фіксаты eval, каб пазнейшыя замены модэляў заставаліся парабяжнымі.