mirror of
https://github.com/OpenHands/OpenHands.git
synced 2025-12-25 21:36:52 +08:00
227 lines
8.0 KiB
Python
Executable File
227 lines
8.0 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""
|
|
Update OpenHands OpenAPI documentation.
|
|
|
|
Generates the OpenAPI specification from the FastAPI application and writes it
|
|
to docs/openapi.json.
|
|
|
|
Usage:
|
|
python scripts/update_openapi.py
|
|
|
|
Behavior:
|
|
- Uses openhands.server.app.app.openapi() to build the spec.
|
|
- Preserves existing "servers" from docs/openapi.json if present; otherwise
|
|
writes sensible defaults.
|
|
- Sets info.version to openhands.__version__.
|
|
- Sanitizes endpoint descriptions to remove code blocks and internal-only sections.
|
|
- Excludes operational/UI-only convenience endpoints:
|
|
- /server_info
|
|
- /api/conversations/{conversation_id}/vscode-url
|
|
- /api/conversations/{conversation_id}/web-hosts
|
|
- Creates a backup docs/openapi.json.backup before overwriting.
|
|
|
|
Output:
|
|
- Prints OpenAPI and API versions, endpoint count, servers count, and sample endpoints.
|
|
"""
|
|
|
|
import json
|
|
import logging
|
|
import os
|
|
import sys
|
|
import warnings
|
|
from pathlib import Path
|
|
|
|
# Suppress warnings and logs during import
|
|
logging.getLogger().setLevel(logging.CRITICAL)
|
|
warnings.filterwarnings('ignore')
|
|
os.environ['OPENHANDS_LOG_LEVEL'] = 'CRITICAL'
|
|
|
|
# Add the project root to the Python path
|
|
project_root = Path(__file__).parent.parent
|
|
sys.path.insert(0, str(project_root))
|
|
|
|
try:
|
|
from openhands import __version__
|
|
from openhands.server.app import app
|
|
except ImportError as e:
|
|
print(f'Error importing OpenHands modules: {e}')
|
|
print(
|
|
"Make sure you're running this script from the project root and dependencies are installed."
|
|
)
|
|
sys.exit(1)
|
|
|
|
|
|
def _sanitize_description(text: str) -> str:
|
|
"""Remove internal, code-centric, or redundant sections from endpoint descriptions.
|
|
|
|
- Strip fenced code blocks
|
|
- Remove Args/Returns/Raises/Example/Examples/Notes sections
|
|
- Remove inline curl examples
|
|
- Avoid provider-implementation specifics like LiteLLM/Bedrock
|
|
"""
|
|
import re
|
|
|
|
if not text:
|
|
return text
|
|
|
|
# Remove fenced code blocks
|
|
text = re.sub(r'```[\s\S]*?```', '', text, flags=re.MULTILINE)
|
|
|
|
# Remove common docstring sections (until next blank line or end)
|
|
for header in [
|
|
r'Args?:',
|
|
r'Returns?:',
|
|
r'Raises?:',
|
|
r'Example[s]?:',
|
|
r'Notes?:',
|
|
]:
|
|
text = re.sub(rf'(?ms)^\s*{header}.*?(?:\n\s*\n|\Z)', '', text)
|
|
|
|
# Remove lines that contain curl examples
|
|
text = re.sub(r'(?im)^.*\bcurl\b.*$', '', text)
|
|
|
|
# Generalize provider-implementation specifics
|
|
text = re.sub(r'\bLiteLLM\b', 'configured model providers', text)
|
|
text = re.sub(r'\blitellm\b', 'configured providers', text)
|
|
text = re.sub(r'\bBedrock\b', '', text)
|
|
|
|
# Collapse excessive blank lines and trim
|
|
text = re.sub(r'\n{3,}', '\n\n', text).strip()
|
|
return text
|
|
|
|
|
|
def _sanitize_spec(spec: dict) -> dict:
|
|
"""Sanitize descriptions and summaries to be public-API friendly."""
|
|
path_summary_overrides = {
|
|
'/api/options/models': 'List Supported Models',
|
|
'/api/options/agents': 'List Agents',
|
|
'/api/options/security-analyzers': 'List Security Analyzers',
|
|
'/api/conversations/{conversation_id}/list-files': 'List Workspace Files',
|
|
'/api/conversations/{conversation_id}/select-file': 'Get File Content',
|
|
'/api/conversations/{conversation_id}/zip-directory': 'Download Workspace Archive',
|
|
}
|
|
path_description_overrides = {
|
|
'/api/options/models': 'List model identifiers available on this server based on configured providers.',
|
|
'/api/options/agents': 'List available agent types supported by this server.',
|
|
'/api/options/security-analyzers': 'List supported security analyzers.',
|
|
'/api/conversations/{conversation_id}/list-files': 'List workspace files visible to the conversation runtime. Applies .gitignore and internal ignore rules.',
|
|
'/api/conversations/{conversation_id}/select-file': 'Return the content of the given file from the conversation workspace.',
|
|
'/api/conversations/{conversation_id}/zip-directory': 'Return a ZIP archive of the current conversation workspace.',
|
|
}
|
|
|
|
for path, methods in list(spec.get('paths', {}).items()):
|
|
for method, meta in list(methods.items()):
|
|
if not isinstance(meta, dict):
|
|
continue
|
|
# Override overly specific summaries where helpful
|
|
if path in path_summary_overrides:
|
|
meta['summary'] = path_summary_overrides[path]
|
|
# Override description if provided; otherwise sanitize
|
|
if path in path_description_overrides:
|
|
meta['description'] = path_description_overrides[path]
|
|
elif 'description' in meta and isinstance(meta['description'], str):
|
|
meta['description'] = _sanitize_description(meta['description'])
|
|
|
|
return spec
|
|
|
|
|
|
def generate_openapi_spec():
|
|
"""Generate the OpenAPI specification from the FastAPI app."""
|
|
spec = app.openapi()
|
|
|
|
# Explicitly exclude certain endpoints that are operational, experimental, or UI-only convenience
|
|
excluded_endpoints = [
|
|
'/api/conversations/{conversation_id}/exp-config', # Internal experimentation endpoint
|
|
'/server_info', # Operational/system diagnostics
|
|
'/api/conversations/{conversation_id}/vscode-url', # UI/runtime convenience
|
|
'/api/conversations/{conversation_id}/web-hosts', # UI/runtime convenience
|
|
]
|
|
|
|
if 'paths' in spec:
|
|
for endpoint in excluded_endpoints:
|
|
if endpoint in spec['paths']:
|
|
del spec['paths'][endpoint]
|
|
print(f'Excluded endpoint: {endpoint}')
|
|
|
|
# Sanitize descriptions and summaries
|
|
spec = _sanitize_spec(spec)
|
|
|
|
return spec
|
|
|
|
|
|
def load_current_spec(spec_path):
|
|
"""Load the current OpenAPI specification if it exists."""
|
|
if spec_path.exists():
|
|
with open(spec_path, 'r') as f:
|
|
return json.load(f)
|
|
return {}
|
|
|
|
|
|
def update_openapi_spec(spec_path, backup=True):
|
|
"""Update the OpenAPI specification file."""
|
|
# Generate new spec
|
|
new_spec = generate_openapi_spec()
|
|
|
|
# Load current spec for server information
|
|
current_spec = load_current_spec(spec_path)
|
|
|
|
# Preserve server information from current spec if it exists
|
|
if 'servers' in current_spec:
|
|
new_spec['servers'] = current_spec['servers']
|
|
else:
|
|
# Default servers if none exist
|
|
new_spec['servers'] = [
|
|
{'url': 'https://app.all-hands.dev', 'description': 'Production server'},
|
|
{'url': 'http://localhost:3000', 'description': 'Local server'},
|
|
]
|
|
|
|
# Update version to match the package version
|
|
new_spec['info']['version'] = __version__
|
|
|
|
# Backup current file if requested
|
|
if backup and spec_path.exists():
|
|
backup_path = spec_path.with_suffix('.json.backup')
|
|
spec_path.rename(backup_path)
|
|
print(f'Backed up current spec to {backup_path}')
|
|
|
|
# Write new spec
|
|
with open(spec_path, 'w') as f:
|
|
json.dump(new_spec, f, indent=2)
|
|
|
|
return new_spec
|
|
|
|
|
|
def main():
|
|
"""Main function."""
|
|
spec_path = project_root / 'docs' / 'openapi.json'
|
|
|
|
print('Updating OpenAPI specification...')
|
|
print(f'Target file: {spec_path}')
|
|
|
|
try:
|
|
new_spec = update_openapi_spec(spec_path)
|
|
|
|
print('✅ Successfully updated OpenAPI specification!')
|
|
print(f' OpenAPI version: {new_spec.get("openapi", "N/A")}')
|
|
print(f' API version: {new_spec.get("info", {}).get("version", "N/A")}')
|
|
print(f' Total endpoints: {len(new_spec.get("paths", {}))}')
|
|
print(f' Servers: {len(new_spec.get("servers", []))}')
|
|
|
|
# List some key endpoints
|
|
paths = list(new_spec.get('paths', {}).keys())
|
|
if paths:
|
|
print(' Sample endpoints:')
|
|
for path in sorted(paths)[:5]:
|
|
methods = list(new_spec['paths'][path].keys())
|
|
print(f' {path}: {methods}')
|
|
if len(paths) > 5:
|
|
print(f' ... and {len(paths) - 5} more')
|
|
|
|
except Exception as e:
|
|
print(f'❌ Error updating OpenAPI specification: {e}')
|
|
sys.exit(1)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|