llms.txt — запропонований формат: текстовий файл у Markdown, який лежить в корені сайту
поруч з robots.txt і дає короткий опис ресурсу плюс список посилань. Специфікацію
опублікував Jeremy Howard 3 вересня 2024 року на llmstxt.org. Ідея — дати AI-агенту стислий
орієнтир замість обходу всього сайту; специфікація описує читання файлу як «on demand, when
an agent needs information» — за запитом, коли агенту потрібна інформація. Автор спеки пише,
що очікував переважно inference-використання, а не навчання моделей, але прямо визнає:
«training runs could take advantage of the information too» — тобто повністю тренувальний
сценарій специфікація не виключає.
Важливо! Специфікація нічого не каже про те, що ChatGPT, Claude, Perplexity чи Google використовують цей файл для відповідей чи ранжування в пошуку — першоджерела від самих цих компаній я не знайшов. Вважайте llms.txt рекомендацією для тих агентів, які самі вирішили його читати, не гарантованим сигналом для видачі.
Ну ок. Модуль kw_llms_txt вирішує вужчу задачу — дає точку, куди покласти такий файл на
Odoo-сайті, і маршрут, який його віддасть.
Що зберігає вміст
Усе поле — одне текстове поле на моделі website:
# kw_llms_txt/models/website.py:4-12
class Website(models.Model):
_inherit = 'website'
llms_txt = fields.Text(
string='llms.txt',
translate=False,
groups='website.group_website_designer',
help='Markdown content served at /llms.txt (llmstxt.org format).',
)
translate=False означає, що мовних варіантів немає — один текст на весь сайт, незалежно
від того, скільки мов у ньому увімкнено.
Маршрут
# kw_llms_txt/controllers/main.py:1-17
from odoo.http import request
from odoo import http
class KwLlmsTxt(http.Controller):
@http.route(
'/llms.txt', type='http', auth='public', website=True,
multilang=False, sitemap=False)
def llms_txt(self, **kwargs):
content = request.website.sudo().llms_txt
if not content:
raise request.not_found()
return request.make_response(
content,
headers=[('Content-Type', 'text/plain; charset=utf-8')])
Контролер бере request.website — сайт, який Odoo вже визначив за доменом запиту — і
читає поле через sudo(), бо воно закрите групою website.group_website_designer: без
sudo() анонімний відвідувач отримав би AccessError замість файлу. Порожній рядок або
False — і маршрут повертає 404, а не порожню сторінку.
Неправильно очікувати, що модуль сам збере вміст зі сторінок, меню, блогу чи товарів.
Правильно розуміти: це поле й маршрут, вміст пишете ви самі — руками або скриптом через
write(). Генерації, синхронізації з sitemap чи кешування тут немає: кожен запит на
/llms.txt — звичайне читання поточного значення поля.
Ось що поверне контролер для рядка з тестового пакета модуля:
# Site One
> Summary — café
## Docs
- [Home](https://one.example.com/): main page
Структура (H1, blockquote-резюме, H2 зі списком посилань) відповідає прикладу зі специфікації llmstxt.org — той самий приклад продубльований у формі редагування як підказка.
Хто може редагувати
# kw_llms_txt/security/ir.model.access.csv:2
access_website_llms,access.website.llms,model_website_llms,website.group_website_designer,1,1,1,0
Це право на транзієнтну модель wizard'а website.llms. Саме поле llms_txt на website
захищене окремо — атрибутом groups у визначенні поля, а не через цей csv. Тест це показує:
користувач без групи Website Designer вільно читає name сайту, але на llms_txt отримує
AccessError (tests/test_llms_txt_access.py:41-44); і навіть користувач з правом write()
на всю модель website, але без групи дизайнера, на записі llms_txt падає так само
(tests/test_llms_txt_access.py:46-53) — право на модель не замінює право на поле.
Мультисайт
Кожен сайт має власне значення поля — контролер обирає його через request.website, той
самий домен-based механізм, яким Odoo визначає сайт для будь-якого запиту. У тестах два
сайти з різними доменами повертають кожен свій файл, без перетину вмісту
(tests/test_llms_txt_route.py:47-56).
Редагування прив'язане до сайту, вибраного в Settings: кнопка "Edit llms.txt" викликає
# kw_llms_txt/models/res_config_settings.py:7-16
def action_open_llms_txt(self):
self.website_id._force()
return {
'name': _("llms.txt"),
'view_mode': 'form',
'res_model': 'website.llms',
'type': 'ir.actions.act_window',
'views': [[False, 'form']],
'target': 'new',
}
_force() переключає активний сайт на обраний у Settings ДО відкриття wizard'а — інакше
форма показала б контент не того сайту, з яким ви щойно працювали.
Увімкнути й заповнити
- Заходимо в Settings → Website і вибираємо сайт (якщо їх декілька).
- У секції SEO, поруч із robots.txt, натискаємо «Edit llms.txt».
- Вводимо Markdown-текст, зберігаємо.
- Відкриваємо
https://<домен>/llms.txtі перевіряємо.
Кнопка вставлена xpath'ом одразу після robots.txt:
<!-- kw_llms_txt/views/res_config_settings_views.xml:9-17 -->
<xpath expr="//setting[@id='robots_setting']" position="after">
<setting id="llms_txt_setting" string="llms.txt"
help="llms.txt: a Markdown file that gives AI assistants and LLM-based tools a concise overview of your website.">
<div class="mt4">
<button type="object" name="action_open_llms_txt" string="Edit llms.txt"
class="btn-link" icon="fa-file-text-o" noSaveDialog="true"/>
</div>
</setting>
</xpath>
Де межі
Модуль не читає жодної іншої моделі, крім website — немає коду, що звертався б до
website.page, website.menu, товарів чи постів блогу. Автоматично протягнути в /llms.txt
неопубліковану сторінку чи чернетку блогу він фізично не може, бо туди й не заглядає. Але це
ж означає, що дисципліна повністю на вас: вставите в поле текст чи посилання з внутрішньої
сторінки — модуль віддасть це в plain text будь-якому анонімному відвідувачу, валідації
немає. І зміни на сайті в /llms.txt не відображаються самі — оновили меню, правте файл
окремо.
Порожнє поле — стандартний стан щойно встановленого модуля, і тоді /llms.txt віддає
404 (це перевірено тестом, а не особливий випадок). На kitworks.systems модуль встановлений;
що саме зараз у полі — дивіться на живому /llms.txt, бо вміст ручний, а не автогенерація
(розділ «Де межі»).