4.1 📤 RAG с вашими данными

🔧 Кастомный Text Splitter*. ✂️

(* сложная задача, можете спокойно её пропустить)

Вам предоставляется текст с четко обозначенными разделами и подразделами, как показано на картинке ниже (оглавление текущего курса с ключевыми словами в некоторых разделах и подразделах.)

Задача — написать функцию get_content_from_sections(text: str) -> List[str], которая принимает такой текст в качестве входных данных и возвращает список, содержащий ключевые слова (весь текст) из каждого раздела и подраздела, исключая сами заголовки.

Замечание — Собранный текст с каждого основого раздела (1, 2, 3 и т.д.) поместить в одну строку, разделив пробелами. То есть список на выходе должен иметь длину, равную количеству основных разделов.

Ответ для примера с картинки
[
    "Описание курса, начальные требования, для кого этот курс. Цели курса, канал и комьюнити, глоссарий, PET-проект, хард режим vs лайт режим? Рассказываем почему важно разбираться в LLM. Нужен ли ИИ обычному человеку? Получаем ключ в боте. Получаем официальный ключ от OpenAI. Ныряем в Open Source и получаем ключ от HuggingFace.", 
    "Поясняем за промпты. Техники и лайфхаки для промптинга. Из чего состоит промпт? Few-shot learning. Output Parser. Рассказываем про преимущества LangChain.",
    "Переводим LLM в чат-режим. Типы памяти. Chains & LCEL. Агенты и цепи. Инструменты (tools)."
]
Тексты из тестов

text1 = """
1. Вступление
Описание курса, начальные требования, для кого этот курс.
1.1 Как правильно входить в курс
Цели курса, канал и комьюнити, глоссарий, PET-проект, хард режим vs лайт режим?
1.2 Общий подход и точки улучшения приложений с LLM
Рассказываем почему важно разбираться в LLM. Нужен ли ИИ обычному человеку?
1.3 API ключ курса или от OpenAI?
1.3.1 Ключ от команды курса
Получаем ключ в боте.
1.3.2 Ключ от OpenAI
Получаем официальный ключ от OpenAI.
1.3.3 Ключ от HuggingFace
Ныряем в Open Source и получаем ключ от HuggingFace.
2. Промптинг - объясни LLM, что тебе от неё надо!
2.1 Введение в Prompt Engineering
Поясняем за промпты. Техники и лайфхаки для промптинга. Из чего состоит промпт?
2.2 Дизайн промптов в LangChain
Few-shot learning. Output Parser.
2.2.1 Введение в LangChain
Рассказываем про преимущества LangChain.
3. LangChain или причем тут попугаи?
3.1 Память в LangChain
Переводим LLM в чат-режим. Типы памяти.
3.2 Chains - собери свою цепь
Chains & LCEL.
3.3 Агенты intro
Агенты и цепи. Инструменты (tools).
"""

text2 = """


"""

text3 = """
1. Главный заголовок
Тут содержимое, которое нужно достать из главного заголовка.
1.1 Подзаголовок
Тут содержимое, которое нужно достать из подзаголовка.
"""

text4 = """
1. Первый заголовок

2. Второй заголовок
"""

text5 = """
1. Введение
Краткое описание целей и задач документа.

2. Основные разделы
Краткий обзор основных разделов.

2.1 Первый подраздел
Какие-то мелкие детали.

3. Заключение
Выводы и заключительные комментарии.
4. Приложения
Дополнительная информация и материалы.
"""

Failed. Runtime error

Error:
Traceback (most recent call last):
  File "jailed_code", line 109, in <module>
    assert False, "Ошибка на тесте 4."
AssertionError: Ошибка на тесте 4.

Подскажите по решению, локально проверяю - все тесты из наличия проходит, а тут jailed_code на 4ом. Что-то не учтено?

@Albert_Murtazin, тексты из тестов есть в подсказке

все тексты из подсказок проходят тесты локально, но на платформе на 4ом тесте падает с ошибкой "jailed_code"

@Albert_Murtazin,  ответом на 4-й тест должен быть пустой список [ ]. Посмотрите, что у вас

@Иван_Александров, тогда это противоречит условию "список на выходе должен иметь длину, равную количеству основных разделов". Даже если в разделе ничего нет, то из этого условия подразумевается, что элемент списка становится пустой строкой (или None\False\etc). Подправить бы.

@Albert_Murtazin, хорошо, проверим

['Цели курса, канал и комьюнити, глоссарий, PET-проект, хард режим vs лайт режим? Рассказываем почему важно разбираться в LLM. Нужен ли ИИ обычному человеку?  Получаем ключ в боте. Получаем официальный ключ от OpenAI. Ныряем в Open Source и получаем ключ от HuggingFace.',
 'Поясняем за промпты. Техники и лайфхаки для промптинга. Из чего состоит промпт? Few-shot learning. Output Parser. Рассказываем про преимущества LangChain.',
 'Переводим LLM в чат-режим. Типы памяти. Chains & LCEL. Агенты и цепи. Инструменты (tools).']

len(get_content_from_sections(text1) )

3

В чём это неправильный ответ на тест 1?

@Grigorii_Tarasov, Описание курса, начальные требования, для кого этот курс. не хватает

здравствуйте, сабмиты #1143641446 и #1143638817
один решает убирая все \n, второй только первый \n в строке (оба правильно работают на первом тексте, на остальных зависит от условия задачи, не до конца понятно)
при этом пишет ошибка на тесте 1
В чем ошибка может быть?

.

Python 3
import re


def get_content_from_sections(text: str) -> List[str]:
    # Удаляем строки подглав
    text_match = re.sub(r"\d+.\d.*?\n", r"", text)
    # Split по основным главам
    text_match = re.split(r"\d+.*?\n", text_match)

    # Очищаем пустые split'ы и фильтруем переносы строк 
    result = []
    for line in text_match:
        if len(line.strip()) == 0:
            continue

        result.append(line.replace("\n", " ").strip())

    return result

.

Python 3
import re
from typing import List

def get_content_from_sections(text: str) -> List[str]:
    text = re.sub(r'\n\d+\.\s', "#@_", text)
    sections = text.split('#')
    results = []
    for section in sections:
        a =''
        lst = section.split('\n')
        if len(lst) > 1:
            for i in section.split('\n'):
                if i == re.sub(r'\d+\.\d?(\.\d+)?\s+', '', i) and i == re.sub(r'@_', '', i):
                    a += i+' '
            if a.rstrip() != '':
                results.append(a.rstrip())

        if not section.strip():
            continue
    return results

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

Python 3
import re

def get_content_from_sections(text: str) -> list:
    # Регулярное выражение для извлечения всех разделов и подразделов
    pattern = r'(?:^|\n)(\d+(?:\.\d+)*)\.?\s*([^\n]*)\n((?:(?!\n\d+(?:\.\d+)*\.?\s).)*)'
    
    # Находим все разделы и подразделы
    sections = re.findall(pattern, text, flags=re.DOTALL | re.MULTILINE)
    
    # Словарь для хранения содержимого основных разделов
    main_sections = {}
    
    for section_num, title, content in sections:
        # Определяем номер основного раздела
        main_section = section_num.split('.')[0]
        
        # Очищаем содержимое от лишних пробелов, переносов строк и оставшихся заголовков подразделов
        cleaned_content = re.sub(r'\d+(?:\.\d+)*\.?\s*[^\n]*\n', '', content)
        cleaned_content = re.sub(r'\s+', ' ', cleaned_content).strip()
        
        # Добавляем содержимое к соответствующему основному разделу
        if main_section in main_sections:
            main_sections[main_section] += ' ' + cleaned_content
        else:
            main_sections[main_section] = cleaned_content
    
    # Возвращаем результаты, отсортированные по номеру раздела
    return [main_sections[key].strip() for key in sorted(main_sections.keys()) if main_sections[key].strip()]

Python 3
def get_content_from_sections(text: str) -> List[str]:
    res = []
    part = 0
    for row in text.split('\n'):
        if row:
            if row[0].isdigit():
                part = int(row.split('.')[0]) - 1
            else:
                if len(res) > part:
                    res[part] += ' ' + row
                else:
                    res.append(row)
    
    return res




Решение не без помощи Llm, но посмотрите, какой изящный костыль в предпоследней строке.

Python 3
import re
from typing import List

def get_content_from_sections(text: str) -> List[str]:
    # Разделяем текст на строки
    lines = text.splitlines()
    result = []
    current_section_content = []

    for line in lines:
        # Проверяем, является ли строка заголовком
        if re.match(r'^\d+\.\s', line):
            # Если есть накопленный текст, добавляем его в результат
            if current_section_content:
                result.append(' '.join(current_section_content))
                current_section_content = []
        elif re.match(r'^\d+\.\d+\s', line):
            # Если это подзаголовок, добавляем текст в текущий раздел
            continue
        else:
            # Убираем предложения, начинающиеся с номера
            if not re.match(r'^\d+', line.strip()):
                current_section_content.append(line.strip())

    # Добавляем последний накопленный текст, если он есть
    if current_section_content:
        result.append(' '.join(current_section_content))
    # Убираем пустые элементы из списка, включая содержащие пробелы  
    result = [element.rstrip() for element in result if element.strip()]
    return result

Скормил описание задания chatgpt, а потом скармливал ошибку при попытке сдать урок. На 5 итерацию код прошел проверку.

Python 3
import re
from typing import List

def get_content_from_sections(text: str) -> List[str]:
    # Разбиваем текст на основные разделы (например, 1., 2., 3. и т.д.)
    main_sections = re.split(r'\n(?=\d+\.\s)', text.strip())
    content_list = []

    for section in main_sections:
        # Пропускаем пустые разделы
        if not section.strip():
            continue
        
        # Разбиваем на строки и извлекаем заголовки и содержимое
        lines = section.split('\n')
        section_content = []

        for line in lines[1:]:  # Пропускаем первую строку (это заголовок раздела)
            # Если строка не содержит вложенного заголовка и не пустая, добавляем её
            if not re.match(r'\d+\.\d+', line.strip()) and line.strip():
                section_content.append(line.strip())
        
        # Объединяем текст внутри раздела и добавляем в список
        if section_content:
            content_list.append(" ".join(section_content))
    
    return content_list

.

Python 3
def get_content_from_sections(text: str) -> List[str]:
    curr_chapter = 0
    content = []
    for s in text.split('\n'):
        if s:
            chapter = s.split('.')[0]
            if chapter.isnumeric():
                if int(chapter) != curr_chapter:
                    content.append('')
                    curr_chapter += 1
            else:
                if content[-1]: 
                  content[-1] += ' '
                content[-1] += s
    return [c for c in content if c]




)

Python 3
def get_content_from_sections(text1: str) -> List[str]:
    output_list_of_str = []
    tmp_text = ''
    tem_lst = []
    text = text1.split('\n')
    for row in text:
        if len(row) == 0: pass
        elif row[0].isdigit():
            if row[0] in tem_lst:
                pass
            else:
                tem_lst.append(row[0])
                output_list_of_str.append(tmp_text)
                tmp_text = ''
        else: 
            #tmp_text += row
            if len(tmp_text) == 0: 
                tmp_text += row
            else:
                tmp_text += ' ' + row
            
    output_list_of_str.append(tmp_text)
    output_list_of_str.pop(0)
    
    output_list_of_str = list( filter(lambda x : len(x)!=0, output_list_of_str) )
    return output_list_of_str




.

Python 3
def get_content_from_sections(text: str) -> List[str]:
    res, tmp_list, current_char = [], [], None
    candidates = list(filter(lambda x: bool(x), text.strip().split('\n')))
    ln = len(candidates)
    if ln > 0:
        for i, c in enumerate(candidates):
            if bool(c):
                if c[0].isdigit():
                    if current_char is None:
                        current_char = c[0]
                    else:
                        if c[0] != current_char:
                            current_char = c[0]
                            if len(tmp_list) != 0:
                                res.append(' '.join(tmp_list))
                                tmp_list = []
                if not c.startswith(current_char):
                    tmp_list.append(c)
                if i == (ln - 1):
                    if len(tmp_list) != 0:
                        res.append(' '.join(tmp_list))
                        tmp_list = []
    return res

.

Python 3
def get_content_from_sections(text: str) -> List[str]:
    res = []
    cur = None
    for line in text.split('\n'):
        line = line.strip()
        if not line: continue
        if line[0].isdigit() and cur != line[0]:
            if res and not res[-1]: res.pop()
            res.append('')
            cur = line[0]
        elif line[0].isdigit(): continue
        else:
            pref = ' ' if res[-1] else ''
            res[-1] += pref + line
    if res and not res[-1]: res.pop()
    return res

Из-за 4 теста пришлось еще раз проверять, что нет пустой строки

Python 3
import re

def get_content_from_sections(text: str):
    result = text.strip()
    result = re.split('.*\d\. .*\n', result)
    result = [' '.join([s.strip() for s in line.strip().split('\n') if s and not s[0].isdigit()]) for line in result if line]
    return [line for line in result if line]

.

Python 3
import re


def get_content_from_sections(text: str) -> list:
    out = []
    for line in text.split("\n"):
        if not line:
            continue

        if re.search(r'^\d+\.\ ', line):  # Main section
            out.append("")

        if re.search( r'^[\d|\.]+\ ', line):  # Submain section
            continue

        out[-1] = f"{out[-1]} {line}" if out[-1] else line  # Text section

    out = [line for line in out if line]
    return out