Чтение документа по идентификатору
Поиск возвращает идентификатор и ссылку источника, но короткого фрагмента может быть недостаточно для ответа. Добавим library.read: инструмент получает устойчивый id и возвращает полный разрешённый документ. После урока клиент сможет сначала найти запись, затем прочитать её точный текст и сопоставить источник с публичным адресом. Произвольный путь файловой системы в этот интерфейс не входит.
Новый инструмент доступен с пятого состояния. Он использует тот же documents, что и поиск, поэтому правила доступа не расходятся между двумя маршрутами. Если reader не видит служебную запись в поиске, знание её id не должно позволить прочитать её другим методом. Все результаты здесь ожидаемые; сервер не запускался и не подключался к AI. Рассмотрим механизм по полным авторским исходникам выбранного состояния.
Идентификатор вместо пути
Аргумент id является строкой ограниченной длины. Он обозначает запись манифеста, а не имя файла. Сервер ищет совпадение в разрешённом наборе и уже затем возвращает её данные. Это позволяет переименовать локальный Markdown в иной задаче без изменения внешнего контракта инструмента. Клиент не должен знать структуру папок, а сервер сохраняет ответственность за проверку источника и чтение собственного корпуса.
Если вместо id принимать path, вызывающее приложение могло бы попросить соседний .env или файл вне библиотеки. Можно было бы построить сложную проверку путей, но для нашей предметной операции такая широта не нужна. Ограниченный идентификатор проще связать с редакционной записью и доступом. Мы всё равно проверяем путь из манифеста, поскольку ошибочные данные источника также возможны. Узкий интерфейс и проверка внутреннего пути дополняют друг друга.
Публичный URL передаётся вместе с текстом. Он нужен для перехода к источнику и не вычисляется по id. Если запись связана с историческим PHP-адресом, инструмент сохранит его в ответе. Клиент может показать обычную ссылку, но в лаборатории мы не проверяем её фактическую сетевую выдачу. URL является авторским сведением учебного манифеста; чтение локального файла не доказывает, что реальный сервер сегодня отвечает по этому адресу.
Текст и отпечаток
Результат содержит id, title, url, text и sha256. Текст позволяет проверить смысл источника, отпечаток связывает ответ с его конкретной последовательностью символов, закодированной в UTF-8. Если документ изменится, ожидается другой отпечаток. SHA-256 не удостоверяет авторство или истинность содержания. Он показывает соответствие выбранной версии, что полезно для ссылки на основание и обнаружения устаревшего контекста.
Отпечаток вычисляется по тому тексту, который возвращается клиенту. Это важно: если считать исходные байты, а затем нормализовать переводы строк, идентификатор будет относиться к другому представлению. В нашем варианте чтение Python даёт строку, которую затем кодируют для хеширования. Такой договор должен оставаться одинаковым при сравнении на стороне клиента. Не нужно объявлять разные отпечатки доказательством изменения смысла: различие может касаться только представления текста.
Полный документ ограничен размером при загрузке. Это предотвращает случайную передачу огромного файла через инструмент чтения. Ограничение относится к байтам исходника, а не токенам будущей модели. Реальное AI-приложение должно отдельно учитывать размер своего контекста и выбирать нужные фрагменты. Сервер не знает, сколько места осталось в конкретном разговоре, и не должен выдавать своё ограничение файла за универсальный бюджет языковой модели.
Недоступность без раскрытия причины
Если id отсутствует среди разрешённых записей, read_document выдаёт DomainError с нейтральным сообщением о недоступности. Для reader случайный id и служебный id выглядят одинаково на уровне инструмента. Это помогает не раскрывать подробности закрытого корпуса через текст ошибки. В учебной папке сам манифест виден читателю архива, поэтому пример не создаёт реальную секретность файлов; он показывает правильную границу API для будущей изолированной среды.
Нельзя сначала искать запись во всём корпусе, сообщать «документ существует, но закрыт», а потом считать доступ полностью защищённым. Иногда сам факт существования имеет значение. Наша предметная модель выбирает разрешённые документы раньше поиска id. Если приложению необходим редакторский просмотр причин отказа, следует сделать отдельный доверенный маршрут. Публичный инструмент не обязан объяснять пользователю внутренний статус каждой неизвестной записи.
tools/call превращает предметную ошибку в результат с isError: true. Это отличается от некорректной структуры запроса: id может быть правильно оформлен, но документ всё равно недоступен. В следующем уроке подробно разберём категории ошибок. Уже сейчас полезно удерживать различие: исправление формы аргумента и выбор другого доступного источника требуют разных действий со стороны вызывающего приложения.
Ожидаемый вызов library.read для urls возвращает полный авторский Markdown и прежний URL. Для notes в reader-профиле ожидается недоступность. Ни один сценарий не выполнялся при подготовке. Если вы позже запустите пример, сохраняйте фактический ответ отдельно и указывайте выбранную роль и состояние. Иначе легко сравнить редакторский результат с читательским ожиданием и ошибочно решить, что правила доступа работают непоследовательно.
Прочитанный текст остаётся данными. Он может содержать инструкции учебного характера, но не меняет роль процесса и не разрешает публикацию. Клиент способен цитировать его или показывать пользователю; сервер не должен интерпретировать строки как новые полномочия. Такая граница особенно важна перед будущим добавлением записи черновика: возможность прочитать материал не должна автоматически предоставлять возможность выполнять все действия, описанные в нём.
Источник изменился после поиска
Предположим, поиск вернул urls, а редактор изменил Markdown перед чтением. Инструмент read должен вернуть актуальный разрешённый текст, а не восстанавливать прежний snippet из памяти запроса. Отпечаток полного ответа поможет назвать версию. Но клиенту нужно понимать, что поиск и чтение являются двумя наблюдениями, между которыми данные могут измениться. Даже устойчивый id не означает неизменность содержания документа навсегда.
Для маленького спокойного корпуса такой вариант редок, но принцип полезен. Если ответ пользователя должен строго ссылаться на одну версию, приложение может сохранять fingerprint вместе с цитатой и проверять обновление. Наш сервер не хранит историю текста и не умеет читать прошлую версию по hash. Следовательно, отпечаток является идентификатором полученного представления, а не автоматическим архивом всех источников.
Если документ стал закрытым между операциями, read должен снова применить доступ. Видимость прежнего поиска не даёт бессрочного права чтения. В нашей лаборатории reader-role фиксирована, а манифест читается заново. Такой договор сохраняет границу и для будущего редакционного приложения, где состояние данных меняется чаще.
Следующий шаг систематизирует ошибки. Это поможет клиенту отличить неверную версию, неизвестный метод, плохие аргументы и недоступный документ. Понятный формат отказов сохраняет полезность инструмента, не превращая каждую неудачу в неразличимое «сервер не работает».
Фрагмент файла library.py из состояния lesson-05. Полное самостоятельное состояние находится в архиве серии; фрагмент не заменяет остальные определения.
def read_document(id):
row = next((r for r in documents() if r["id"] == id), None)
if row is None:
raise DomainError("document unavailable")
return {"id": row["id"], "title": row["title"], "url": row["url"], "text": row["text"], "sha256": hashlib.sha256(row["text"].encode()).hexdigest()}
Статус примера: not_run. При подготовке не выполнялись учебные программы, тесты, вызовы AI или деплой. Описанные эффекты ожидаемые.
Оглавление серии · Полные исходники
Первичные документы: Источник 1, Источник 2.