openvibers/openvibe.studio Commits

Add llms-full.txt and the agent descriptor (#5)

OP OpenVibers committed 4 days ago · 7607baa1b9 · 3 files, +67 −2

llms.txt lists the public pages, but there was no /llms-full.txt with
their text and no /.well-known/agent.json. Add both from one summary and
page list, and name them in llms.txt.

The full text carries the home, Photo, Light and Attribution pages; the
descriptor points at the real API, OpenAPI, llms and sitemap URLs. A test
asks for each and for the page headings and URLs it carries.

Changes

ddiff --git a/server/agent-descriptor.js b/server/agent-descriptor.js
nnew file mode 100644
iindex 0000000..dcec49b
---- /dev/null
++++ b/server/agent-descriptor.js
@@@ -0,0 +1,22 @@
++'use strict';
++// What openvibe.studio tells an agent about itself (SEO-2). Every field is something the site really serves; the
++// OpenAPI document is the workshop API this site publishes.
++module.exports = {
++ network: 'OpenVibe',
++ site: 'openvibe.studio',
++ id: 'studio',
++ name: 'OpenVibe.Studio',
++ purpose: 'Photo and Light: two open-source creative apps (PhotoCraft, LightCraft) built from pinned release tags and served inside an OpenVibe account.',
++ home: 'https://openvibe.studio',
++ api: 'https://openvibe.studio/api/v1/studio',
++ openapi: 'https://openvibe.studio/openapi.json',
++ docs: 'https://openvibe.studio/llms-full.txt',
++ llms: 'https://openvibe.studio/llms.txt',
++ llmsFull: 'https://openvibe.studio/llms-full.txt',
++ sitemap: 'https://openvibe.studio/sitemap.xml',
++ policy: {
++ robots: 'https://openvibe.network/bot.json',
++ auth: ['none', 'Network OIDC'],
++ rateLimit: 'The API is rate limited per address; the app bundle files are static and cached.',
++ },
++};
ddiff --git a/server/app.js b/server/app.js
iindex 55da4c9..df4a479 100644
---- a/server/app.js
++++ b/server/app.js
@@@ -8,8 +8,25 @@ const { createPages } = require('./pages');
const { createBundleFiles } = require('./bundle-files');
const { createMediaClient } = require('./openvibe/media');
const { createPicsClient } = require('./openvibe/pics');
++const { APPS } = require('./product');
++const seo = require('openvibe-shared/seo');
++const agentDescriptor = require('./agent-descriptor');
const configLib = require('./config');
++const SITE_NAME = 'OpenVibe.Studio';
++const SUMMARY = 'Photo and Light: two open-source creative apps (PhotoCraft, LightCraft) built from pinned release tags and served inside an OpenVibe account.';
++const appText = (id) => {
++ const app = APPS.find((one) => one.id === id);
++ return app ? `${app.tagline} ${app.what}` : '';
++};
++// The public pages, for /llms-full.txt: the same pages llms.txt and the sitemap list.
++const PUBLIC_PAGES = [
++ { title: SITE_NAME, url: '/', text: SUMMARY },
++ { title: 'Photo', url: '/photo', text: appText('photo') },
++ { title: 'Light', url: '/light', text: appText('light') },
++ { title: 'Attribution', url: '/attribution', text: 'The open-source projects Studio hosts, their licences, upstream repositories and the licence files kept with each build.' },
++];
++
function createApp(opts = {}) {
const config = opts.config || configLib.load();
const sso = opts.sso || createSsoClient({
@@@ -48,7 +65,14 @@ function createApp(opts = {}) {
'<url><loc>https://openvibe.studio/light</loc></url><url><loc>https://openvibe.studio/attribution</loc></url></urlset>'));
{ const legal = require('openvibe-shared/legal'); app.get(legal.PATHS, legal.handler({ id: 'studio', service: 'studio', host: 'openvibe.studio', name: 'OpenVibe.Studio', profile: 'hosting' })); }
app.get('/llms.txt', (_req, res) => res.type('text/plain').send(
-- '# OpenVibe.Studio\n\nPhoto and Light: two open-source creative apps (PhotoCraft, LightCraft) built from pinned release tags and served inside an OpenVibe account.\n\n- [Home](/)\n- [Photo](/photo)\n- [Light](/light)\n- [Attribution](/attribution)\n- [API docs](/openapi.json)\n'));
++ '# OpenVibe.Studio\n\n' + SUMMARY + '\n\n- [Home](/)\n- [Photo](/photo)\n- [Light](/light)\n- [Attribution](/attribution)\n- [API docs](/openapi.json)\n- [Full text for language models](/llms-full.txt)\n- [Agent descriptor](/.well-known/agent.json)\n'));
++ // The same pages with their text, for a model that wants the site in one read (llms-full.txt, llmstxt.org).
++ app.get('/llms-full.txt', (_req, res) => res.type('text/plain').send(seo.llmsFull({
++ site: SITE_NAME, summary: SUMMARY, base: 'https://openvibe.studio',
++ sections: [{ title: 'Pages', pages: PUBLIC_PAGES }],
++ })));
++ // What openvibe.studio tells an agent about itself (SEO-2, openvibe-shared/agent-layer's shape).
++ app.get('/.well-known/agent.json', (_req, res) => res.set('Cache-Control', 'public, max-age=3600').json(agentDescriptor));
app.get('/openapi.json', (_req, res) => res.sendFile(path.join(__dirname, '..', 'public', 'openapi.json')));
app.use('/api/v1/studio', createApi({ sso, media, pics }));
// GET /release.json (ADR-016): the navbar's release watch polls it so open tabs learn of a new release (the 11-09
ddiff --git a/test/studio.test.js b/test/studio.test.js
iindex 2e37520..c481595 100644
---- a/test/studio.test.js
++++ b/test/studio.test.js
@@@ -182,7 +182,26 @@ test('the app catalogue, the attribution page and the small endpoints answer', a
assert.match(attributionHtml, /storytold\/lightcraft/);
assert.match(attributionHtml, /v0\.19\.0/);
assert.equal((await fetch(s.base + '/api/health')).status, 200);
-- assert.equal((await fetch(s.base + '/llms.txt')).status, 200);
++ const llms = await fetch(s.base + '/llms.txt');
++ assert.equal(llms.status, 200);
++ const llmsText = await llms.text();
++ assert.ok(llmsText.includes('(/llms-full.txt)') && llmsText.includes('(/.well-known/agent.json)'), 'llms.txt names the full text and the agent descriptor');
++ const full = await fetch(s.base + '/llms-full.txt');
++ assert.equal(full.status, 200);
++ assert.match(full.headers.get('content-type'), /^text\/plain/);
++ const fullText = await full.text();
++ assert.ok(fullText.startsWith('# OpenVibe.Studio\n'), 'llms-full.txt opens with the site heading');
++ assert.ok(fullText.length > 500, `llms-full.txt carries the pages (${fullText.length} characters)`);
++ for (const page of ['/photo', '/light', '/attribution']) assert.ok(fullText.includes(`URL: https://openvibe.studio${page}`), `llms-full.txt lists ${page}`);
++ assert.ok(fullText.includes('PhotoCraft') && fullText.includes('LightCraft'), 'and the apps it serves');
++ const agent = await fetch(s.base + '/.well-known/agent.json');
++ assert.equal(agent.status, 200);
++ assert.match(agent.headers.get('content-type'), /^application\/json/);
++ const manifest = await agent.json();
++ assert.equal(manifest.id, 'studio');
++ assert.equal(manifest.home, 'https://openvibe.studio');
++ assert.equal(manifest.llmsFull, 'https://openvibe.studio/llms-full.txt');
++ assert.equal(manifest.openapi, 'https://openvibe.studio/openapi.json');
assert.equal((await fetch(s.base + '/robots.txt')).status, 200);
{ const r = await fetch(s.base + '/release.json'); assert.equal(r.status, 200, 'the navbar release watch polls /release.json'); assert.equal((await r.json()).service, 'studio'); }
assert.equal((await fetch(s.base + '/nope')).status, 404);