Skip to content

Commit 2fd12db

Browse files
ldionneclaude
andcommitted
[API] Add /llms.txt endpoint for AI agent orientation
Serve a plain-text orientation document at GET /llms.txt following the llms.txt convention (analogous to robots.txt). Helps AI agents quickly understand LNT's domain concepts, API structure, and common workflows. Content includes: what LNT is, key concepts (test suite, machine, order, run, test, sample, regression, field change), endpoint listing, pagination format, links to Swagger UI and OpenAPI spec, and common workflows. - Registered as a plain Flask blueprint (not flask-smorest) to stay out of the OpenAPI spec - Static content with Cache-Control (24h) and ETag headers - Points to OpenAPI spec for full write endpoint documentation Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
1 parent 59a0932 commit 2fd12db

5 files changed

Lines changed: 228 additions & 0 deletions

File tree

docs/design/v5-api.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,3 +180,14 @@ R9: Not in Scope (Deferred)
180180
- Run comparison / derived analytics endpoints
181181
- Report endpoints (daily, summary, latest runs)
182182
- Machine merge
183+
184+
R10: AI Agent Orientation
185+
186+
- Serve a plain-text orientation document at GET /llms.txt (following the
187+
llms.txt convention, analogous to robots.txt)
188+
- Content: what LNT is, key domain concepts, API structure, common workflows,
189+
and links to Swagger UI / OpenAPI spec
190+
- Static content, no authentication required
191+
- Served as text/plain with UTF-8 charset
192+
- Registered as a plain Flask blueprint (not flask-smorest) so it does not
193+
appear in the OpenAPI spec

docs/v5-api-implementation-plan.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -630,6 +630,26 @@ DELETE /api/v5/admin/api-keys/{prefix} — Revoke key by prefix (admin)
630630
- DELETE sets is_active=False (soft delete for audit trail)
631631
- Auth: admin scope required for all.
632632

633+
### 5.13 AI Agent Orientation (`GET /llms.txt`)
634+
635+
**File**: `lnt/server/api/v5/endpoints/agents.py`
636+
637+
Serves a plain-text orientation document at `GET /llms.txt` following the llms.txt
638+
convention (analogous to robots.txt). Helps AI agents understand what LNT is, its
639+
domain concepts, and how to navigate the API.
640+
641+
**Key design decisions:**
642+
- Plain Flask blueprint (not flask-smorest) — keeps it out of the OpenAPI spec
643+
- Registered on the Flask app directly in `create_v5_api()`
644+
- Static content defined as a Python string constant — no template or DB access
645+
- Content-Type: `text/plain; charset=utf-8`
646+
- No authentication required
647+
- Outside the `/api/v5/` prefix for conventional discoverability
648+
649+
Content includes: LNT description, key concepts (test suite, machine, order, run,
650+
test, sample, regression, field change), endpoint listing, pagination format,
651+
links to Swagger UI and OpenAPI spec, and common workflows.
652+
633653
---
634654

635655
## 6. Testing Strategy

lnt/server/api/v5/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,4 +32,7 @@ def create_v5_api(app):
3232
from .endpoints import register_all_endpoints
3333
register_all_endpoints(smorest_api)
3434

35+
from .endpoints.agents import llms_txt_bp
36+
app.register_blueprint(llms_txt_bp)
37+
3538
return smorest_api
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
"""llms.txt endpoint: GET /llms.txt
2+
3+
Serves a plain-text orientation document for AI agents, following the
4+
llms.txt convention (analogous to robots.txt). Describes what LNT is,
5+
its domain concepts, API structure, and common workflows.
6+
7+
Registered as a plain Flask blueprint (not flask-smorest) so it does not
8+
appear in the OpenAPI spec.
9+
"""
10+
11+
from flask import Blueprint, make_response
12+
import hashlib
13+
14+
llms_txt_bp = Blueprint('llms_txt', __name__)
15+
16+
LLMS_TEXT = """\
17+
# LNT — LLVM Nightly Test Infrastructure
18+
19+
LNT is a performance testing infrastructure designed for tracking software
20+
performance over time. It collects benchmark results from test runs, detects
21+
regressions, and provides tools for performance analysis. Originally built
22+
for the LLVM compiler project, it can be used for any software project.
23+
24+
## Key Concepts
25+
26+
- **Test Suite**: A schema defining what metrics to collect (e.g., "nts" for
27+
the LLVM nightly test suite). Each suite has its own set of machines, orders,
28+
runs, and tests. All data queries are scoped to a specific test suite.
29+
30+
- **Machine**: A build/test environment identified by name (e.g.,
31+
"clang-x86_64-linux"). Machines have key-value info fields describing
32+
their configuration.
33+
34+
- **Order**: A point in the revision history (e.g., a commit hash or revision
35+
number). Orders define the sequence for time-series analysis. They may have
36+
multiple fields (e.g., primary revision + dependent project revisions).
37+
38+
- **Run**: A single test execution on a machine at a specific order. Contains
39+
samples (individual test results) with metric values. Identified by UUID.
40+
41+
- **Test**: A named benchmark or test case (e.g., "SingleSource/Benchmarks/
42+
Dhrystone/dry"). Tests are created implicitly when runs are submitted.
43+
44+
- **Sample**: A single data point: one test's metric values from one run.
45+
Each sample records values for the metrics defined by the test suite schema
46+
(e.g., execution_time, compile_time, code_size).
47+
48+
- **Regression**: A detected performance change, grouping one or more field
49+
changes. Has a state (detected, active, fixed, ignored, etc.), optional
50+
title, and optional bug tracker link.
51+
52+
- **Field Change**: A statistically significant change in a metric value
53+
between two orders for a specific test on a specific machine.
54+
55+
## REST API (v5)
56+
57+
Base URL: /api/v5/
58+
Authentication: Bearer token in Authorization header. Reads are unauthenticated
59+
by default. Write operations require tokens with appropriate scopes
60+
(submit, triage, manage, admin).
61+
62+
### Discovery
63+
64+
GET /api/v5/ List test suites and API links
65+
66+
### Per-Suite Endpoints (replace {ts} with suite name, e.g., "nts")
67+
68+
GET /api/v5/{ts}/machines List machines
69+
GET /api/v5/{ts}/machines/{name} Machine detail
70+
GET /api/v5/{ts}/orders List orders
71+
GET /api/v5/{ts}/orders/{value} Order detail (with prev/next)
72+
GET /api/v5/{ts}/runs List runs
73+
POST /api/v5/{ts}/runs Submit a run
74+
GET /api/v5/{ts}/runs/{uuid} Run detail
75+
GET /api/v5/{ts}/tests List tests
76+
POST /api/v5/{ts}/query Query time-series data
77+
GET /api/v5/{ts}/regressions List regressions
78+
GET /api/v5/{ts}/field-changes List unassigned field changes
79+
80+
### Global Endpoints
81+
82+
GET /api/v5/test-suites List all test suites
83+
GET /api/v5/test-suites/{name} Suite detail (schema + metrics)
84+
GET /api/v5/admin/api-keys List API keys (admin)
85+
86+
The endpoints above cover the most common read operations. The API also
87+
supports write operations (creating/updating/deleting machines, orders,
88+
runs, regressions, field changes, test suites, and API keys) which require
89+
appropriate authentication scopes. See the OpenAPI spec or Swagger UI for
90+
the complete endpoint list including all write operations.
91+
92+
### Interactive Documentation
93+
94+
OpenAPI spec: /api/v5/openapi/openapi.json
95+
Swagger UI: /api/v5/openapi/swagger-ui
96+
97+
### Pagination
98+
99+
List endpoints return cursor-paginated responses:
100+
{ "items": [...], "cursor": { "next": "...", "previous": null } }
101+
Pass cursor=<next> to get the next page. Use limit= to control page size.
102+
103+
### Common Workflows
104+
105+
1. Discover available data: GET /api/v5/ to list test suites, then
106+
GET /api/v5/{ts}/machines to see what machines exist.
107+
108+
2. Query performance history: POST /api/v5/{ts}/query with
109+
{ "metric": "execution_time", "machine": "machine-name",
110+
"test": ["test/name"] } to get time-series data points.
111+
112+
3. Submit a run: POST /api/v5/{ts}/runs with the LNT JSON report format.
113+
Requires a token with "submit" scope.
114+
115+
4. Check for regressions: GET /api/v5/{ts}/regressions?state=detected
116+
to find new regressions. PATCH to update state, title, or bug link.
117+
118+
5. Inspect a specific order: GET /api/v5/{ts}/orders/{value} returns
119+
the order detail with previous/next navigation links.
120+
"""
121+
122+
_ETAG = hashlib.md5(LLMS_TEXT.encode()).hexdigest()
123+
124+
125+
@llms_txt_bp.route('/llms.txt')
126+
def llms_txt():
127+
"""Serve the LNT orientation document for AI agents."""
128+
resp = make_response(LLMS_TEXT)
129+
resp.mimetype = 'text/plain'
130+
resp.charset = 'utf-8'
131+
resp.headers['Cache-Control'] = 'public, max-age=86400'
132+
resp.headers['ETag'] = _ETAG
133+
return resp

tests/server/api/v5/test_agents.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# Tests for the /llms.txt endpoint (AI agent orientation).
2+
#
3+
# RUN: rm -rf %t.instance %t.pg.log
4+
# RUN: %{utils}/with_postgres.sh %t.pg.log \
5+
# RUN: %{utils}/with_temporary_instance.py %t.instance \
6+
# RUN: -- python %s %t.instance
7+
# END.
8+
9+
import sys
10+
import os
11+
import unittest
12+
13+
sys.path.insert(0, os.path.dirname(__file__))
14+
from v5_test_helpers import create_app, create_client
15+
16+
17+
class TestLlmsTxt(unittest.TestCase):
18+
@classmethod
19+
def setUpClass(cls):
20+
super().setUpClass()
21+
cls.app = create_app(sys.argv[1])
22+
cls.client = create_client(cls.app)
23+
24+
def test_returns_200(self):
25+
resp = self.client.get('/llms.txt')
26+
self.assertEqual(resp.status_code, 200)
27+
28+
def test_content_type_is_text_plain(self):
29+
resp = self.client.get('/llms.txt')
30+
self.assertTrue(resp.content_type.startswith('text/plain'))
31+
32+
def test_contains_lnt_description(self):
33+
resp = self.client.get('/llms.txt')
34+
text = resp.get_data(as_text=True)
35+
self.assertIn('LNT', text)
36+
self.assertIn('performance testing infrastructure', text)
37+
38+
def test_contains_key_concepts(self):
39+
resp = self.client.get('/llms.txt')
40+
text = resp.get_data(as_text=True)
41+
self.assertIn('Key Concepts', text)
42+
self.assertIn('Test Suite', text)
43+
self.assertIn('Machine', text)
44+
self.assertIn('Regression', text)
45+
46+
def test_contains_api_links(self):
47+
resp = self.client.get('/llms.txt')
48+
text = resp.get_data(as_text=True)
49+
self.assertIn('/api/v5/openapi/swagger-ui', text)
50+
self.assertIn('/api/v5/openapi/openapi.json', text)
51+
52+
def test_contains_endpoint_listing(self):
53+
resp = self.client.get('/llms.txt')
54+
text = resp.get_data(as_text=True)
55+
self.assertIn('/api/v5/{ts}/machines', text)
56+
self.assertIn('/api/v5/{ts}/runs', text)
57+
self.assertIn('/api/v5/{ts}/query', text)
58+
59+
60+
if __name__ == '__main__':
61+
unittest.main(argv=[sys.argv[0]], exit=True)

0 commit comments

Comments
 (0)