The most effective way to test agent readiness is to simulate how AI agents interact with your systems. While you can't replicate every agent's implementation, you can create representative simulations based on common agent patterns.
Custom Agent Builder Framework
"""
Agent Readiness Testing Framework
Simulates how AI agents interact with web content and APIs
"""
import requests
import json
from bs4 import BeautifulSoup
from urllib.parse import urljoin, urlparse
import time
from typing import Dict, List, Optional
class AgentSimulator:
"""
Simulates AI agent behavior for testing agent readiness
"""
def __init__(self, config: Dict = None):
self.config = config or {}
self.session = requests.Session()
self.session.headers.update({
'User-Agent': 'AgentSimulator/1.0 (Agent-Readiness-Testing)',
'Accept': 'application/ld+json, application/json, text/html'
})
self.interaction_log = []
def test_content_extractability(self, url: str) -> Dict:
"""
Test if content can be extracted like an AI agent would
"""
result = {
'url': url,
'timestamp': time.time(),
'tests': {}
}
try:
response = self.session.get(url, timeout=10)
result['status_code'] = response.status_code
result['response_time_ms'] = round(
response.elapsed.total_seconds() * 1000, 2
)
result['tests']['schema_markup'] = self._detect_schema(response.text)
# Test 2: Semantic HTML Structure
result['tests']['semantic_structure'] = self._analyze_semantic_structure(response.text)
# Test 3: Meta Tags
result['tests']['meta_tags'] = self._check_meta_tags(response.text)
# Test 4: Internal Link Structure
result['tests']['link_structure'] = self._analyze_links(response.text, url)
# Test 5: Content Clarity
result['tests']['content_clarity'] = self._assess_content_clarity(response.text)
# Calculate overall score
result['score'] = self._calculate_score(result['tests'])
except Exception as e:
result['error'] = str(e)
self.interaction_log.append(result)
return result
def _detect_schema(self, html: str) -> Dict:
"""Detect and validate schema markup"""
soup = BeautifulSoup(html, 'html.parser')
schemas = []
for script in soup.find_all('script', type='application/ld+json'):
try:
schema = json.loads(script.string)
schemas.append({
'type': schema.get('@type', 'Unknown'),
'valid': self._validate_schema_structure(schema),
'size_kb': len(json.dumps(schema)) / 1024
})
except:
schemas.append({'error': 'Invalid JSON'})
return {
'found': len(schemas),
'schemas': schemas[:5], # First 5 schemas
'has_faq': any(s.get('type') == 'FAQPage' for s in schemas),
'has_product': any(s.get('type') == 'Product' for s in schemas),
'has_organization': any(s.get('type') == 'Organization' for s in schemas)
}
def _validate_schema_structure(self, schema: Dict) -> bool:
"""Basic schema structure validation"""
return bool(schema.get('@context') and schema.get('@type'))
def _analyze_semantic_structure(self, html: str) -> Dict:
"""Analyze HTML semantic structure"""
soup = BeautifulSoup(html, 'html.parser')
return {
'has_h1': bool(soup.find('h1')),
'h1_count': len(soup.find_all('h1')),
'h2_count': len(soup.find_all('h2')),
'h3_count': len(soup.find_all('h3')),
'heading_hierarchy': self._check_heading_hierarchy(soup),
'uses_semantic_tags': self._check_semantic_tags(soup),
'aria_labels': len(soup.find_all(attrs={'aria-label': True}))
}
def _check_heading_hierarchy(self, soup) -> bool:
"""Check if headings follow proper hierarchy"""
headings = soup.find_all(['h1', 'h2', 'h3', 'h4', 'h5', 'h6'])
levels = []
for h in headings:
level = int(h.name[1])
levels.append(level)
# Check no skipped levels
for i in range(1, len(levels)):
if levels[i] > levels[i-1] + 1:
return False
return True
def _check_semantic_tags(self, soup) -> List[str]:
"""Check for semantic HTML5 tags"""
semantic_tags = ['nav', 'main', 'article', 'section', 'aside', 'header', 'footer']
found = [tag for tag in semantic_tags if soup.find(tag)]
return found
def _check_meta_tags(self, html: str) -> Dict:
"""Check essential meta tags"""
soup = BeautifulSoup(html, 'html.parser')
return {
'title': bool(soup.find('title')),
'title_length': len(soup.find('title').text) if soup.find('title') else 0,
'meta_description': bool(soup.find('meta', attrs={'name': 'description'})),
'og_title': bool(soup.find('meta', attrs={'property': 'og:title'})),
'og_description': bool(soup.find('meta', attrs={'property': 'og:description'})),
'og_image': bool(soup.find('meta', attrs={'property': 'og:image'})),
'canonical': bool(soup.find('link', attrs={'rel': 'canonical'}))
}
def _analyze_links(self, html: str, base_url: str) -> Dict:
"""Analyze internal link structure"""
soup = BeautifulSoup(html, 'html.parser')
base_domain = urlparse(base_url).netloc
links = soup.find_all('a', href=True)
internal_links = []
external_links = []
for link in links:
href = link['href']
full_url = urljoin(base_url, href)
link_domain = urlparse(full_url).netloc
link_info = {
'url': full_url,
'text': link.get_text(strip=True)[:50],
'has_accessible_text': len(link.get_text(strip=True)) > 0
}
if link_domain == base_domain:
internal_links.append(link_info)
else:
external_links.append(link_info)
return {
'total_links': len(links),
'internal_links': len(internal_links),
'external_links': len(external_links),
'internal_sample': internal_links[:10],
'accessible_link_percentage': self._calc_accessible_links(links)
}
def _calc_accessible_links(self, links) -> float:
"""Calculate percentage of links with accessible text"""
accessible = sum(1 for link in links if len(link.get_text(strip=True)) > 0)
return round((accessible / len(links)) * 100, 1) if links else 0
def _assess_content_clarity(self, html: str) -> Dict:
"""Assess if content is clear and extractable"""
soup = BeautifulSoup(html, 'html.parser')
# Remove script and style elements
for element in soup(['script', 'style', 'nav', 'footer']):
element.decompose()
text = soup.get_text(separator=' ', strip=True)
words = text.split()
return {
'word_count': len(words),
'paragraph_count': len(soup.find_all('p')),
'has_clear_structure': len(soup.find_all('h1')) > 0 and len(soup.find_all('h2')) > 0,
'avg_paragraph_length': round(len(words) / max(len(soup.find_all('p')), 1), 1),
'uses_lists': len(soup.find_all(['ul', 'ol'])) > 0,
'uses_tables': len(soup.find_all('table')) > 0
}
def _calculate_score(self, tests: Dict) -> Dict:
"""Calculate overall agent readiness score"""
scores = {
'schema_markup': 0,
'semantic_structure': 0,
'meta_tags': 0,
'link_structure': 0,
'content_clarity': 0
}
# Schema scoring
if tests['schema_markup']['found'] > 0:
scores['schema_markup'] += 30
if tests['schema_markup'].get('has_faq'):
scores['schema_markup'] += 20
if tests['schema_markup'].get('has_organization'):
scores['schema_markup'] += 20
# Structure scoring
if tests['semantic_structure']['has_h1']:
scores['semantic_structure'] += 15
if tests['semantic_structure']['heading_hierarchy']:
scores['semantic_structure'] += 15
if tests['semantic_structure']['uses_semantic_tags']:
scores['semantic_structure'] += 20
# Meta tags scoring
meta = tests['meta_tags']
if meta['title'] and 50 <= meta['title_length'] <= 60:
scores['meta_tags'] += 15
if meta['meta_description']:
scores['meta_tags'] += 15
if all([meta['og_title'], meta['og_description'], meta['og_image']]):
scores['meta_tags'] += 20
# Link structure scoring
if tests['link_structure']['accessible_link_percentage'] > 80:
scores['link_structure'] += 20
# Content clarity scoring
if tests['content_clarity']['has_clear_structure']:
scores['content_clarity'] += 20
total = sum(scores.values())
percentage = round((total / 100) * 100, 1)
return {
'total': total,
'max': 100,
'percentage': percentage,
'by_category': scores,
'grade': self._get_grade(percentage)
}
def _get_grade(self, percentage: float) -> str:
"""Get letter grade for score"""
if percentage >= 90: return 'A'
if percentage >= 80: return 'B'
if percentage >= 70: return 'C'
if percentage >= 60: return 'D'
return 'F'
def test_api_agent_readiness(self, api_base_url: str) -> Dict:
"""
Test API endpoints for agent readiness
"""
results = {
'base_url': api_base_url,
'tests': {}
}
# Test OpenAPI/Swagger documentation
results['tests']['api_documentation'] = self._test_api_docs(api_base_url)
# Test rate limit headers
results['tests']['rate_limiting'] = self._test_rate_limits(api_base_url)
# Test response formats
results['tests']['response_format'] = self._test_response_formats(api_base_url)
# Test error handling
results['tests']['error_handling'] = self._test_error_handling(api_base_url)
return results
def _test_api_docs(self, base_url: str) -> Dict:
"""Test for API documentation"""
common_paths = ['/swagger.json', '/api-docs', '/openapi.json', '/docs']
for path in common_paths:
try:
url = urljoin(base_url, path)
response = self.session.get(url, timeout=5)
if response.status_code == 200:
return {
'found': True,
'url': url,
'format': self._detect_doc_format(response)
}
except:
continue
return {'found': False}
def _detect_doc_format(self, response) -> str:
"""Detect API documentation format"""
content_type = response.headers.get('Content-Type', '')
if 'json' in content_type:
try:
data = response.json()
if data.get('openapi'): return 'OpenAPI 3.x'
if data.get('swagger'): return 'Swagger 2.0'
except:
pass
return 'unknown'
def _test_rate_limits(self, base_url: str) -> Dict:
"""Test rate limiting headers"""
try:
response = self.session.get(base_url, timeout=5)
headers = response.headers
return {
'has_rate_limit_headers': any(
key in headers for key in
['X-RateLimit-Limit', 'RateLimit-Limit', 'X-RateLimit-Remaining']
),
'rate_limit_info': {
k: v for k, v in headers.items()
if 'rate' in k.lower() or 'limit' in k.lower()
}
}
except:
return {'error': 'Failed to test rate limits'}
def _test_response_formats(self, base_url: str) -> Dict:
"""Test response format consistency"""
# This would test actual endpoints
return {
'tested': False,
'note': 'Requires endpoint-specific testing'
}
def _test_error_handling(self, base_url: str) -> Dict:
"""Test error response quality"""
try:
# Test 404 error
response = self.session.get(urljoin(base_url, '/nonexistent'), timeout=5)
return {
'status_code': response.status_code,
'has_error_body': len(response.content) > 0,
'content_type': response.headers.get('Content-Type'),
'structured_error': 'application/json' in response.headers.get('Content-Type', '')
}
except:
return {'error': 'Failed to test error handling'}
def generate_report(self) -> Dict:
"""Generate comprehensive test report"""
return {
'summary': {
'total_tests': len(self.interaction_log),
'average_score': self._calculate_average_score(),
'passed': sum(1 for r in self.interaction_log if r.get('score', {}).get('percentage', 0) >= 70),
'failed': sum(1 for r in self.interaction_log if r.get('score', {}).get('percentage', 0) < 70)
},
'detailed_results': self.interaction_log,
'recommendations': self._generate_recommendations()
}
def _calculate_average_score(self) -> float:
"""Calculate average score across all tests"""
if not self.interaction_log:
return 0
scores = [r.get('score', {}).get('percentage', 0) for r in self.interaction_log]
return round(sum(scores) / len(scores), 1)
def _generate_recommendations(self) -> List[str]:
"""Generate improvement recommendations"""
recommendations = []
if not self.interaction_log:
return ['No tests run yet']
# Analyze common issues
all_tests = [r.get('tests', {}) for r in self.interaction_log]
schema_issues = sum(1 for t in all_tests if t.get('schema_markup', {}).get('found', 0) == 0)
if schema_issues > len(all_tests) / 2:
recommendations.append(
'Add schema markup to improve AI agent understanding'
)
meta_issues = sum(1 for t in all_tests if not t.get('meta_tags', {}).get('meta_description'))
if meta_issues > len(all_tests) / 2:
recommendations.append(
'Add meta descriptions to all pages'
)
return recommendations
Using Open Source Testing Frameworks
Several open-source tools can help with agent readiness testing:
Pa11y for accessibility (which correlates with agent readiness):
npm install -g pa11y
pa11y https://example.com --reporter json
Schema.org Validator for structured data:
# Validate schema markup
import requests
def validate_schema(url):
schema_url = f"https://validator.schema.org/?url={url}"
response = requests.get(schema_url)
return response.json()
Custom Lighthouse Plugin for agent metrics:
// agent-readiness-lighthouse.js
class AgentReadinessAudit {
static get meta() {
return {
id: 'agent-readiness',
title: 'Agent Readiness Audit',
description: 'Tests how well the page supports AI agent interaction'
}
}
static async audit(context) {
const { $, elements } = context;
// Check for schema markup
const hasSchema = $('script[type="application/ld+json"]').length > 0;
// Check semantic HTML
const hasSemantic = $('main, article, section, nav').length > 0;
// Check heading structure
const headings = $('h1, h2, h3, h4, h5, h6').toArray();
const hasProperHierarchy = this.checkHierarchy(headings);
return {
score: hasSchema * 30 + hasSemantic * 30 + hasProperHierarchy * 40,
details: {
hasSchema,
hasSemantic,
hasProperHierarchy
}
}
}
static checkHierarchy(headings) {
let previousLevel = 0;
for (const heading of headings) {
const level = parseInt(heading.tagName[1]);
if (level > previousLevel + 1 && previousLevel !== 0) {
return false;
}
previousLevel = level;
}
return true;
}
}
module.exports = AgentReadinessAudit;