Download scripts/generate_corpus.py from Prabu17/askmydocs: direct link, hf CLI and curl.
- Browser
- Download file 35.7 kB
-
https://huggingface.co/spaces/Prabu17/askmydocs/resolve/main/scripts/generate_corpus.py
- Command line
-
hf download hf://spaces/Prabu17/askmydocs/scripts/generate_corpus.py
-
curl -L -o generate_corpus.py https://huggingface.co/spaces/Prabu17/askmydocs/resolve/main/scripts/generate_corpus.py
35.7 kB
| """Generate the AskMyDocs enterprise documentation corpus. | |
| Produces a mixed-format tree under ``data/corpus/``: | |
| data/corpus/ | |
| ├── markdown/ ~30 docs | |
| ├── pdf/ 5 docs (authentication-guide, api-reference, security-guide, | |
| deployment-guide, troubleshooting-guide) | |
| ├── docx/ ~5 docs | |
| └── html/ ~6 docs | |
| Run: | |
| python scripts/generate_corpus.py [--out data/corpus] [--seed 42] | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import sys | |
| from collections.abc import Callable | |
| from pathlib import Path | |
| sys.path.insert(0, str(Path(__file__).resolve().parent)) | |
| from corpus_content import FACTS, create_auth_context, doc # noqa: E402 | |
| from corpus_shims import ( # noqa: E402 | |
| code_block, | |
| h2, | |
| note_box, | |
| para, | |
| table, | |
| write_doc, | |
| ) | |
| from pdf_docx_render import render_docx, render_html, render_pdf # noqa: E402 | |
| DATA_DIR = Path(__file__).resolve().parent.parent / "data" / "corpus" | |
| # -------------------------------------------------------------------------- | |
| # Section builders (return markdown fragments) | |
| # -------------------------------------------------------------------------- | |
| def sec_auth_intro() -> str: | |
| return ( | |
| para("This guide describes how authentication works for the AskMyDocs platform.") | |
| + para( | |
| "Every API request must present credentials. AskMyDocs supports " | |
| "OAuth 2.0 bearer tokens as the primary mechanism for service " | |
| "accounts and API keys for long-running integrations." | |
| ) | |
| + para( | |
| "The platform issues tokens through a dedicated Identity Service. " | |
| "The Identity Service is the only component allowed to mint or " | |
| "refresh tokens. Other services validate tokens against the " | |
| "Identity Service's public key endpoint." | |
| ) | |
| ) | |
| def sec_auth_tokens() -> str: | |
| ctx = create_auth_context() | |
| return ( | |
| h2("Token Lifecycle") | |
| + para( | |
| f"Access tokens are short-lived. By default, an access token expires after " | |
| f"{ctx['token_expiry']}. Refresh tokens are long-lived and expire after " | |
| f"{ctx['refresh_token_expiry']}." | |
| ) | |
| + table( | |
| ["Token type", "Lifetime", "Transport"], | |
| [ | |
| ["Access token (JWT)", ctx["token_expiry"], "Authorization header"], | |
| ["Refresh token (opaque)", ctx["refresh_token_expiry"], "Secure cookie or POST body"], | |
| ], | |
| ) | |
| + para( | |
| "When an access token expires, the client must use the refresh token " | |
| "to obtain a new one. The refresh flow is described in the OAuth " | |
| "guide." | |
| ) | |
| + note_box( | |
| "warning", | |
| "Never store refresh tokens in browser local storage. Use a secure, " | |
| "HttpOnly cookie.", | |
| ) | |
| ) | |
| def sec_auth_api_keys() -> str: | |
| return ( | |
| h2("API Keys") | |
| + para( | |
| "API keys are the recommended credential for server-to-server " | |
| "integrations. Keys are prefixed with " | |
| f"``{FACTS['api_key_prefix']}`` and are shown only once at creation time." | |
| ) | |
| + para( | |
| "An API key can be scoped to a set of permissions using the " | |
| "permissions claim. Scoping keys is strongly recommended." | |
| ) | |
| + para( | |
| f"Keys expire after {FACTS['api_key_lifetime']} and can be revoked at any time from the Admin Console." | |
| ) | |
| + code_block( | |
| "bash", | |
| "# Example: authenticate with an API key\n" | |
| 'curl -H "Authorization: Bearer amsk_live_abc123" \\\n' | |
| " https://api.askmydocs.example/v1/projects", | |
| ) | |
| ) | |
| def sec_auth_basic() -> str: | |
| return ( | |
| h2("Basic Authentication for Legacy Integrations") | |
| + para( | |
| "Basic authentication is supported only for legacy integrations and " | |
| "is disabled by default. Use OAuth 2.0 or API keys for new " | |
| "integrations." | |
| ) | |
| + note_box( | |
| "note", | |
| "Basic auth is not available for accounts enrolled in SSO.", | |
| ) | |
| ) | |
| def sec_auth_errors() -> str: | |
| return ( | |
| h2("Authentication Errors") | |
| + para("The Identity Service returns the following error codes:") | |
| + table( | |
| ["Error code", "HTTP status", "Meaning"], | |
| [ | |
| ["invalid_client", "401", "Client id or secret is invalid"], | |
| ["invalid_grant", "400", "Refresh token is invalid or expired"], | |
| ["invalid_token", "401", "Access token is invalid or expired"], | |
| ["insufficient_scope", "403", "Token lacks required scope"], | |
| ["temporarily_unavailable", "503", "Identity Service is unavailable"], | |
| ], | |
| ) | |
| + note_box( | |
| "tip", | |
| "Clients should treat 401 responses as a signal to re-authenticate, " | |
| "not to retry the same request.", | |
| ) | |
| ) | |
| def sec_mfa() -> str: | |
| return ( | |
| h2("Multi-Factor Authentication (MFA)") | |
| + para( | |
| "MFA adds a second verification factor on top of a password. " | |
| "Supported factors: TOTP authenticator apps, SMS, and WebAuthn " | |
| "security keys." | |
| ) | |
| + para( | |
| "MFA is required for administrators by default. Users can enroll " | |
| "multiple devices, but only the most recently enrolled device is " | |
| "active at any time." | |
| ) | |
| ) | |
| def sec_security_encryption() -> str: | |
| return ( | |
| h2("Encryption") | |
| + para( | |
| f"All data at rest is encrypted with {FACTS['pii_encryption']}. " | |
| "Data in transit uses TLS 1.2 or later." | |
| ) | |
| + para( | |
| "Customer encryption keys are supported for enterprise plans. " | |
| "If you provision a customer key, AskMyDocs cannot recover data if " | |
| "the key is lost." | |
| ) | |
| ) | |
| def sec_security_audit() -> str: | |
| return ( | |
| h2("Audit Logging") | |
| + para( | |
| "Security-relevant events are recorded in the audit log: sign-in, " | |
| "sign-out, permission changes, API key creation/revocation, and " | |
| "webhook secret rotation." | |
| ) | |
| + para( | |
| "Audit logs are retained for 180 days by default and can be " | |
| "exported to an external SIEM through the Admin Console." | |
| ) | |
| ) | |
| def sec_oauth_intro() -> str: | |
| return ( | |
| h2("Overview") | |
| + para( | |
| f"The AskMyDocs platform implements {FACTS['oauth_provider']} to let " | |
| "third-party applications access customer resources on the user's " | |
| "behalf." | |
| ) | |
| + para( | |
| "The authorization server is the Identity Service. It supports the " | |
| "Authorization Code grant flow with Proof Key for Code Exchange " | |
| "(PKCE), the Client Credentials grant, and the Refresh Token grant." | |
| ) | |
| ) | |
| def sec_oauth_flows() -> str: | |
| return ( | |
| h2("Supported Flows") | |
| + table( | |
| ["Flow", "Use case", "Actor"], | |
| [ | |
| ["Authorization Code + PKCE", "Browser, mobile, SPAs", "End user"], | |
| ["Client Credentials", "Server-to-server", "Service account"], | |
| ["Refresh Token", "Rotate access token", "End user or service"], | |
| ], | |
| ) | |
| + para( | |
| "For browser-based applications, the Authorization Code flow with " | |
| "PKCE is recommended. The implicit flow is not supported." | |
| ) | |
| ) | |
| def sec_oauth_scopes() -> str: | |
| return ( | |
| h2("Scopes") | |
| + para( | |
| "Scopes are space-delimited strings. The platform defines the " | |
| "following standard scopes:" | |
| ) | |
| + table( | |
| ["Scope", "Grants"], | |
| [ | |
| ["openid", "Identity information"], | |
| ["profile", "Read user profile"], | |
| ["documents:read", "Read documents"], | |
| ["documents:write", "Create, edit, delete documents"], | |
| ["admin", "Admin Console access (superuser)"], | |
| ], | |
| ) | |
| + para( | |
| "Scopes are enforced by the API gateway. A token without the " | |
| "``documents:write`` scope receives 403 for write operations." | |
| ) | |
| ) | |
| def sec_oauth_claims() -> str: | |
| return ( | |
| h2("Token Claims") | |
| + para( | |
| "Access tokens are JWTs signed with RS256. Standard claims: " | |
| "``iss``, ``sub``, ``aud``, ``exp``, ``iat``, ``scope``, and " | |
| "``permissions``." | |
| ) | |
| + para( | |
| "The ``permissions`` claim is an array of permission strings such " | |
| "as ``documents:read``. Services must validate the signature using " | |
| "the Identity Service JWKS endpoint before trusting any claim." | |
| ) | |
| ) | |
| def sec_oauth_discovery() -> str: | |
| return ( | |
| h2("Discovery") | |
| + para("OAuth configuration is published at the well-known endpoint:") | |
| + code_block( | |
| "text", | |
| "https://identity.askmydocs.example/.well-known/openid-configuration", | |
| ) | |
| + para( | |
| "The discovery document lists authorization, token, revocation, and " | |
| "JWKS endpoints. Clients must fetch this document at startup and " | |
| "honor the ``expiration`` hints." | |
| ) | |
| ) | |
| def sec_oauth_refresh() -> str: | |
| return ( | |
| h2("Refresh Token Rotation") | |
| + para( | |
| f"Refresh tokens expire after {FACTS['refresh_token_expiry']}. " | |
| "AskMyDocs rotates refresh tokens on every use: the previous " | |
| "refresh token is invalidated immediately." | |
| ) | |
| + para( | |
| "If a rotated refresh token is used again (token replay), the " | |
| "entire session is revoked and the user is forced to " | |
| "re-authenticate. Clients must therefore persist each new refresh " | |
| "token before using the access token it returns." | |
| ) | |
| ) | |
| def sec_oauth_sso() -> str: | |
| return ( | |
| h2("SSO and Federation") | |
| + para( | |
| "Enterprise SSO is available on the Enterprise plan. AskMyDocs " | |
| f"supports {FACTS['sso_saml']} and {FACTS['sso_oidc']} identity " | |
| "providers." | |
| ) | |
| + para( | |
| "When SSO is enabled, password authentication is disabled for all " | |
| "users in that organization. SCIM is supported for automatic " | |
| "user provisioning and deprovisioning." | |
| ) | |
| ) | |
| def sec_api_terminal() -> str: | |
| return ( | |
| h2("API Terminal") | |
| + para("All API requests must use HTTPS.") | |
| + code_block( | |
| "text", | |
| "https://api.askmydocs.example/v1", | |
| ) | |
| + para( | |
| "The API is versioned in the URL path. The current version is v1. " | |
| "Breaking changes are announced at least 6 months in advance." | |
| ) | |
| ) | |
| def sec_api_auth_header() -> str: | |
| return ( | |
| h2("Authentication") | |
| + para( | |
| "Every request must include an Authorization header with a bearer " | |
| "token:" | |
| ) | |
| + code_block( | |
| "http", | |
| 'GET /v1/projects HTTP/1.1\nHost: api.askmydocs.example\nAuthorization: Bearer eyJhbGciOiJSUzI1NiIs...', | |
| ) | |
| + para( | |
| f"Tokens expire after {FACTS['token_expiry']}. Use the refresh " | |
| "flow before expiry to avoid 401 responses." | |
| ) | |
| ) | |
| def sec_api_pagination() -> str: | |
| return ( | |
| h2("Pagination") | |
| + para( | |
| "List endpoints paginate using cursor-based pagination. The response " | |
| "contains a ``next_cursor`` field; pass it as the ``cursor`` " | |
| "parameter to fetch the next page." | |
| ) | |
| + table( | |
| ["Parameter", "Type", "Description"], | |
| [ | |
| ["limit", "int", "Max items per page (1-100, default 20)"], | |
| ["cursor", "string", "Opaque pagination cursor"], | |
| ], | |
| ) | |
| ) | |
| def sec_api_idempotency() -> str: | |
| return ( | |
| h2("Idempotency") | |
| + para( | |
| "Write endpoints accept an ``Idempotency-Key`` header. If a " | |
| "request with the same key is retried, the server returns the " | |
| "original response without applying the change twice." | |
| ) | |
| + para( | |
| "Idempotency keys are honored for 24 hours. Use UUID v4 values." | |
| ) | |
| ) | |
| def sec_api_rate_limits() -> str: | |
| return ( | |
| h2("Rate Limits") | |
| + para( | |
| f"The default rate limit is {FACTS['rate_limit_default']}." | |
| f"Short bursts up to {FACTS['rate_limit_burst']} are allowed." | |
| ) | |
| + para( | |
| "Rate limits apply per API key, per endpoint. Responses include " | |
| "``X-RateLimit-Limit``, ``X-RateLimit-Remaining``, and " | |
| "``X-RateLimit-Reset`` headers." | |
| ) | |
| + note_box( | |
| "warning", | |
| "Hitting the rate limit returns HTTP 429 with a ``Retry-After`` " | |
| "header. Respect the header; do not retry immediately.", | |
| ) | |
| ) | |
| def sec_api_webhooks() -> str: | |
| return ( | |
| h2("Webhooks") | |
| + para( | |
| "Webhooks notify your system about events such as document " | |
| "processed, document failed, and ingestion complete." | |
| ) | |
| + para( | |
| f"Each delivery is signed with {FACTS['webhook_delivery']} using " | |
| "the webhook secret. The signature is sent in the " | |
| "``X-AskMyDocs-Signature`` header as ``t=<timestamp>,v1=<hex>``." | |
| ) | |
| + para( | |
| f"Delivery is retried up to {FACTS['webhook_retry']}." | |
| ) | |
| + code_block( | |
| "python", | |
| "# Verify a webhook signature\n" | |
| "import hashlib, hmac\n\n" | |
| "def verify(secret, body, header):\n" | |
| " timestamp, signature = header.split(',')\n" | |
| " digest = hmac.new(secret.encode(), f'{timestamp}.{body}'.encode(), hashlib.sha256).hexdigest()\n" | |
| " return hmac.compare_digest(digest, signature.split('=')[1])", | |
| ) | |
| ) | |
| def sec_api_headers() -> str: | |
| return ( | |
| h2("Common Headers") | |
| + table( | |
| ["Header", "Required", "Description"], | |
| [ | |
| ["Authorization", "Yes", "Bearer token"], | |
| ["Idempotency-Key", "Write ops", "Prevent duplicate writes"], | |
| ["X-Request-ID", "No", "Correlate logs; echoed in response"], | |
| ["Accept-Version", "No", "Pin API version (default v1)"], | |
| ], | |
| ) | |
| ) | |
| def sec_errors_summary() -> str: | |
| return ( | |
| h2("Error Response Format") | |
| + para("All errors use a consistent JSON envelope:") | |
| + code_block( | |
| "json", | |
| '{\n "error": {\n "code": "rate_limit_exceeded",\n' | |
| ' "message": "Rate limit exceeded. Retry after 5 seconds.",\n' | |
| ' "request_id": "req_9f1c",\n "details": {}\n }\n}', | |
| ) | |
| + para( | |
| "The ``code`` field is a stable machine-readable string. The " | |
| "``message`` is human-readable and may change." | |
| ) | |
| ) | |
| def sec_errors_codes() -> str: | |
| return ( | |
| h2("Error Codes") | |
| + table( | |
| ["HTTP", "Code", "Common cause"], | |
| [ | |
| ["400", "invalid_request", "Malformed request body"], | |
| ["401", "unauthenticated", "Missing or invalid token"], | |
| ["403", "permission_denied", "Token lacks required scope"], | |
| ["404", "not_found", "Resource does not exist"], | |
| ["409", "conflict", "Resource already exists or state conflict"], | |
| ["422", "validation_failed", "Request body failed validation"], | |
| ["429", "rate_limit_exceeded", "Too many requests"], | |
| ["5xx", "internal_error", "Server-side failure"], | |
| ], | |
| ) | |
| + para( | |
| "Retryable errors are those in the 5xx range, 429, and 408. " | |
| "Client code should only retry those statuses, with exponential " | |
| "backoff and jitter." | |
| ) | |
| ) | |
| def sec_errors_troubleshoot() -> str: | |
| return ( | |
| h2("Troubleshooting Common Errors") | |
| + para("**401 unauthenticated**: verify the token is not expired and is sent in the Authorization header.") | |
| + para("**403 permission_denied**: the token lacks the required scope. Request the scope in the OAuth consent screen.") | |
| + para("**429 rate_limit_exceeded**: inspect the Retry-After header and back off.") | |
| + para("**5xx internal_error**: check the status page; retry with exponential backoff.") | |
| ) | |
| def sec_config_env() -> str: | |
| return ( | |
| h2("Environment Variables") | |
| + para("The platform is configured through environment variables. Key variables:") | |
| + table( | |
| ["Variable", "Default", "Description"], | |
| [ | |
| ["APP_ENV", "development", "Runtime environment"], | |
| ["DATABASE_URL", "", "PostgreSQL connection string"], | |
| ["QDRANT_URL", "http://localhost:6333", "Qdrant vector database"], | |
| ["OPENSEARCH_URL", "http://localhost:9200", "OpenSearch BM25 index"], | |
| ["EMBEDDING_MODEL", "BAAI/bge-m3", "Embedding model"], | |
| ["RERANKER_MODEL", "cross-encoder/mmarco-mMiniLMv2-L12-H384-v1", "Cross-encoder reranker"], | |
| ["OPENROUTER_API_KEY", "", "LLM provider key"], | |
| ["CHUNK_SIZE", "512", "Chunk size (tokens)"], | |
| ["CHUNK_OVERLAP", "64", "Chunk overlap"], | |
| ], | |
| ) | |
| + note_box( | |
| "tip", | |
| "Never commit secrets to version control. Use a secret manager in " | |
| "production.", | |
| ) | |
| ) | |
| def sec_config_flags() -> str: | |
| return ( | |
| h2("Feature Flags") | |
| + para( | |
| "Flags are evaluated server-side. Flags are boolean by default; " | |
| "some flags accept a percentage rollout or a targeting rule." | |
| ) | |
| + table( | |
| ["Flag", "Default", "Effect"], | |
| [ | |
| ["rag.hybrid_search", "true", "Enable hybrid BM25 + vector retrieval"], | |
| ["rag.reranker", "true", "Enable cross-encoder reranking"], | |
| ["citations.require", "true", "Require citations on answers"], | |
| ["experimental.new_parser", "false", "Use new document parser"], | |
| ], | |
| ) | |
| ) | |
| def sec_deploy_overview() -> str: | |
| return ( | |
| h2("Deployment Overview") | |
| + para( | |
| f"The AskMyDocs platform has a guaranteed uptime SLA of {FACTS['sla_uptime']} " | |
| "for the Enterprise plan." | |
| ) | |
| + para( | |
| "The standard topology deploys the API service, the ingestion " | |
| "worker, and the Identity Service. The ingestion worker processes " | |
| "documents asynchronously from a queue." | |
| ) | |
| + table( | |
| ["Component", "Purpose"], | |
| [ | |
| ["api", "Serves the REST API"], | |
| ["ingestion-worker", "Parses and indexes documents"], | |
| ["identity", "OAuth tokens and user management"], | |
| ["postgres", "Application metadata"], | |
| ["qdrant", "Vector search"], | |
| ["opensearch", "BM25 full-text search"], | |
| ], | |
| ) | |
| ) | |
| def sec_deploy_envs() -> str: | |
| return ( | |
| h2("Environments") | |
| + para("The platform supports three deployment environments.") | |
| + table( | |
| ["Environment", "Purpose", "Data isolation"], | |
| [ | |
| ["development", "Local iteration", "Full isolation"], | |
| ["staging", "Pre-production validation", "Shared test data"], | |
| ["production", "Live traffic", "Full isolation"], | |
| ], | |
| ) | |
| + note_box( | |
| "warning", | |
| "Do not use production credentials in development or staging.", | |
| ) | |
| ) | |
| def sec_deploy_migrations() -> str: | |
| return ( | |
| h2("Database Migrations") | |
| + para( | |
| "Schema changes are applied through versioned migrations. " | |
| "Migrations run automatically at deploy time before new code is " | |
| "released." | |
| ) | |
| + para( | |
| "Downgrades are not supported automatically. Always back up before " | |
| "deploying a migration." | |
| ) | |
| ) | |
| def sec_monitoring_metrics() -> str: | |
| return ( | |
| h2("Metrics") | |
| + para("The platform exposes Prometheus metrics at ``/metrics``.") | |
| + table( | |
| ["Metric", "Type", "Description"], | |
| [ | |
| ["api_requests_total", "counter", "Total API requests by route and status"], | |
| ["api_request_duration_seconds", "histogram", "Request latency"], | |
| ["ingestion_documents_processed_total", "counter", "Documents ingested"], | |
| ["retrieval_latency_seconds", "histogram", "Retrieval pipeline latency"], | |
| ["verification_success_rate", "histogram", "Citation validation pass rate"], | |
| ], | |
| ) | |
| + note_box( | |
| "note", | |
| "Metrics are retained for 30 days. Alerts should aggregate over 5-minute windows.", | |
| ) | |
| ) | |
| def sec_monitoring_alerts() -> str: | |
| return ( | |
| h2("Alerts") | |
| + para("Recommended alerting thresholds:") | |
| + table( | |
| ["Alert", "Threshold", "Severity"], | |
| [ | |
| ["Error rate", "> 1% over 5 minutes", "critical"], | |
| ["p95 latency", "> 2s over 5 minutes", "warning"], | |
| ["Queue depth", "> 5000 messages", "critical"], | |
| ["Verification success rate", "< 95% over 1 hour", "warning"], | |
| ], | |
| ) | |
| ) | |
| def sec_users_roles() -> str: | |
| return ( | |
| h2("Roles") | |
| + para( | |
| "Permissions are assigned through roles. The platform ships with " | |
| "four default roles." | |
| ) | |
| + table( | |
| ["Role", "Permissions"], | |
| [ | |
| ["owner", "Full access including billing"], | |
| ["admin", "Manage users, projects, and settings"], | |
| ["editor", "Create and edit documents"], | |
| [FACTS["rbac_default_role"], "Read-only access"], | |
| ], | |
| ) | |
| + para( | |
| "Roles can be scoped per project. Custom roles can be defined with " | |
| "an arbitrary combination of permissions." | |
| ) | |
| ) | |
| def sec_users_scim() -> str: | |
| return ( | |
| h2("SCIM Provisioning") | |
| + para( | |
| "SCIM 2.0 is supported on the Enterprise plan. When users are " | |
| "deprovisioned in the identity provider, their access is revoked " | |
| "within 5 minutes." | |
| ) | |
| ) | |
| def sec_db_behavior() -> str: | |
| return ( | |
| h2("Database Behavior") | |
| + para( | |
| f"The platform stores metadata in {FACTS['database_engine']}. " | |
| "The connection pool has a maximum of " | |
| f"{FACTS['db_connection_max']} connections per instance." | |
| ) | |
| + para( | |
| "The database stores documents, chunks, ingestion jobs, queries, " | |
| "and evaluation runs. Vector embeddings are stored in Qdrant, not " | |
| "in PostgreSQL." | |
| ) | |
| + para( | |
| "Backups are taken every 12 hours and retained for " | |
| f"{FACTS['backup_retention']}." | |
| ) | |
| ) | |
| def sec_troubleshooting_common() -> str: | |
| return ( | |
| h2("Common Issues") | |
| + para( | |
| "This section covers frequently reported issues and their " | |
| "resolutions." | |
| ) | |
| + table( | |
| ["Symptom", "Likely cause", "Resolution"], | |
| [ | |
| [ | |
| "401 from all endpoints", | |
| "Expired access token", | |
| "Refresh the token before expiry", | |
| ], | |
| [ | |
| "Documents not searchable after upload", | |
| "Ingestion job failed or still running", | |
| "Check ingestion status; retry the job", | |
| ], | |
| [ | |
| "Slow retrieval", | |
| "Reranker enabled on large candidate sets", | |
| "Reduce BM25/VECTOR_TOP_K", | |
| ], | |
| [ | |
| "429 responses", | |
| "Rate limit exceeded", | |
| "Back off using Retry-After", | |
| ], | |
| [ | |
| "Answers marked ungrounded", | |
| "Evidence insufficient or citation mismatch", | |
| "Re-ask with more specific wording", | |
| ], | |
| ], | |
| ) | |
| ) | |
| def sec_troubleshooting_secrets() -> str: | |
| return ( | |
| h2("Rotating Secrets") | |
| + para( | |
| "Webhook secrets can be rotated from the Admin Console. After " | |
| "rotation, signatures generated with the old secret fail " | |
| "verification immediately. Rotate during low-traffic windows." | |
| ) | |
| + para( | |
| "API keys are single-secret; rotating an API key invalidates the " | |
| "previous key immediately." | |
| ) | |
| ) | |
| def sec_limits_quotas() -> str: | |
| return ( | |
| h2("Quotas by Plan") | |
| + para("Rate limits and quotas vary by plan.") | |
| + table( | |
| ["Plan", "Default rate", "Burst", "Max documents"], | |
| [ | |
| ["Free", "30 req/min", "60 req/min", "100"], | |
| ["Pro", FACTS["rate_limit_default"], FACTS["rate_limit_burst"], "10,000"], | |
| ["Enterprise", "1,000 req/min", "2,000 req/min", "Unlimited"], | |
| ], | |
| ) | |
| ) | |
| def sec_webhooks_events() -> str: | |
| return ( | |
| h2("Webhook Events") | |
| + para("The platform emits the following events.") | |
| + table( | |
| ["Event", "Trigger"], | |
| [ | |
| ["document.uploaded", "A document was uploaded"], | |
| ["document.ingested", "Ingestion completed successfully"], | |
| ["document.failed", "Ingestion failed"], | |
| ["query.completed", "A query completed with citations"], | |
| ], | |
| ) | |
| ) | |
| def sec_webhooks_endpoints() -> str: | |
| return ( | |
| h2("Registering Endpoints") | |
| + para( | |
| "Register up to 10 webhook endpoints per project from the Admin " | |
| "Console. Each endpoint can filter events by type." | |
| ) | |
| + para( | |
| "Endpoints that fail 8 consecutive deliveries are automatically " | |
| "disabled and an alert is raised." | |
| ) | |
| ) | |
| # -------------------------------------------------------------------------- | |
| # Document assembly | |
| # -------------------------------------------------------------------------- | |
| def build_documents() -> dict[str, dict[str, object]]: | |
| """Return ``{doc_id: {"title", "module", "content"}}`` for all documents.""" | |
| docs: dict[str, dict[str, object]] = {} | |
| def add(doc_id: str, title: str, module: str, *sections: Callable[[], str]) -> None: | |
| docs[doc_id] = { | |
| "title": title, | |
| "module": module, | |
| "content": doc(doc_id, title, "1.0", module, *sections), | |
| } | |
| # --- Authentication & security --- | |
| add( | |
| "authentication-guide", | |
| "Authentication Guide", | |
| "authentication", | |
| lambda: (sec_auth_intro() + sec_auth_tokens() + sec_auth_api_keys() + sec_auth_basic() + sec_auth_errors()), | |
| ) | |
| add("mfa-guide", "Multi-Factor Authentication", "authentication", lambda: (sec_mfa() + sec_auth_errors())) | |
| add("security-guide", "Security Guide", "security", lambda: (sec_security_encryption() + sec_security_audit() + sec_auth_errors())) | |
| add("api-keys", "API Keys Management", "authentication", lambda: (sec_auth_api_keys() + sec_auth_basic())) | |
| add("token-refresh", "Token Refresh and Rotation", "authentication", lambda: (sec_oauth_refresh() + sec_auth_tokens())) | |
| # --- OAuth / SSO --- | |
| add( | |
| "oauth-guide", | |
| "OAuth 2.0 Integration Guide", | |
| "oauth", | |
| lambda: (sec_oauth_intro() + sec_oauth_flows() + sec_oauth_scopes() + sec_oauth_claims() + sec_oauth_discovery() + sec_oauth_refresh()), | |
| ) | |
| add("sso-saml", "SAML 2.0 Enterprise SSO", "oauth", lambda: (sec_oauth_sso() + sec_oauth_discovery())) | |
| add("oauth-sso", "SSO and Federation", "oauth", lambda: (sec_oauth_sso() + sec_oauth_discovery())) | |
| add("scopes", "OAuth Scopes and Permissions", "oauth", lambda: (sec_oauth_scopes() + sec_oauth_claims())) | |
| # --- API --- | |
| add( | |
| "api-reference", | |
| "API Reference", | |
| "api", | |
| lambda: (sec_api_terminal() + sec_api_auth_header() + sec_api_pagination() + sec_api_idempotency() + sec_api_rate_limits() + sec_api_webhooks() + sec_api_headers()), | |
| ) | |
| add( | |
| "api-errors", | |
| "API Error Reference", | |
| "api", | |
| lambda: (sec_errors_summary() + sec_errors_codes() + sec_errors_troubleshoot()), | |
| ) | |
| add("api-pagination", "Pagination Guide", "api", lambda: (sec_api_pagination() + sec_api_headers())) | |
| add("api-idempotency", "Idempotency Guide", "api", lambda: (sec_api_idempotency() + sec_api_headers())) | |
| # --- Configuration --- | |
| add( | |
| "configuration-guide", | |
| "Configuration Guide", | |
| "configuration", | |
| lambda: (sec_config_env() + sec_config_flags()), | |
| ) | |
| add("env-vars", "Environment Variables Reference", "configuration", lambda: (sec_config_env())) | |
| add("feature-flags", "Feature Flags", "configuration", lambda: (sec_config_flags())) | |
| # --- Deployment --- | |
| add( | |
| "deployment-guide", | |
| "Deployment Guide", | |
| "deployment", | |
| lambda: (sec_deploy_overview() + sec_deploy_envs() + sec_deploy_migrations()), | |
| ) | |
| add("environments", "Environments", "deployment", lambda: (sec_deploy_envs() + sec_deploy_migrations())) | |
| # --- Monitoring --- | |
| add( | |
| "monitoring-guide", | |
| "Monitoring Guide", | |
| "monitoring", | |
| lambda: (sec_monitoring_metrics() + sec_monitoring_alerts()), | |
| ) | |
| add("alerts-guide", "Alerting Guide", "monitoring", lambda: (sec_monitoring_alerts())) | |
| # --- User management --- | |
| add( | |
| "user-management", | |
| "User Management", | |
| "users", | |
| lambda: (sec_users_roles() + sec_users_scim()), | |
| ) | |
| add("roles-permissions", "Roles and Permissions", "users", lambda: (sec_users_roles())) | |
| # --- Database --- | |
| add("database-behavior", "Database Behavior", "database", lambda: (sec_db_behavior())) | |
| add("backup-recovery", "Backup and Recovery", "database", lambda: (sec_db_behavior() + sec_deploy_migrations())) | |
| # --- Troubleshooting --- | |
| add( | |
| "troubleshooting-guide", | |
| "Troubleshooting Guide", | |
| "troubleshooting", | |
| lambda: (sec_troubleshooting_common() + sec_troubleshooting_secrets()), | |
| ) | |
| add("rotating-secrets", "Rotating Secrets", "troubleshooting", lambda: (sec_troubleshooting_secrets())) | |
| # --- Rate limits --- | |
| add("rate-limits", "Rate Limits and Quotas", "rate-limits", lambda: (sec_api_rate_limits() + sec_limits_quotas())) | |
| # --- Webhooks --- | |
| add( | |
| "webhooks-guide", | |
| "Webhooks Guide", | |
| "webhooks", | |
| lambda: (sec_api_webhooks() + sec_webhooks_events() + sec_webhooks_endpoints()), | |
| ) | |
| add("webhook-signing", "Webhook Signature Verification", "webhooks", lambda: (sec_api_webhooks())) | |
| return docs | |
| # -------------------------------------------------------------------------- | |
| # Multi-format output | |
| # -------------------------------------------------------------------------- | |
| def emit_files(docs: dict[str, dict[str, object]], out_root: Path) -> dict[str, Path]: | |
| """Write all documents to the four format directories. | |
| Returns a mapping of ``doc_id -> chosen primary path`` for the manifest. | |
| """ | |
| markdown_dir = out_root / "markdown" | |
| pdf_dir = out_root / "pdf" | |
| docx_dir = out_root / "docx" | |
| html_dir = out_root / "html" | |
| for d in (markdown_dir, pdf_dir, docx_dir, html_dir): | |
| d.mkdir(parents=True, exist_ok=True) | |
| # Which documents become PDF/DOCX/HTML? | |
| pdf_set = { | |
| "authentication-guide", | |
| "api-reference", | |
| "security-guide", | |
| "deployment-guide", | |
| "troubleshooting-guide", | |
| } | |
| docx_set = {"oauth-guide", "configuration-guide", "monitoring-guide", "user-management", "webhooks-guide"} | |
| html_set = {"api-errors", "rate-limits", "environments", "roles-permissions", "sso-saml", "database-behavior"} | |
| manifest: dict[str, Path] = {} | |
| emitted: list[Path] = [] | |
| for doc_id, meta in docs.items(): | |
| content = str(meta["content"]) | |
| md_path = markdown_dir / f"{doc_id}.md" | |
| write_doc(md_path, content) | |
| emitted.append(md_path) | |
| manifest[doc_id] = md_path | |
| if doc_id in pdf_set: | |
| emitted.append(render_pdf(content, md_path, output_dir=pdf_dir)) | |
| if doc_id in docx_set: | |
| emitted.append(render_docx(content, md_path, output_dir=docx_dir)) | |
| if doc_id in html_set: | |
| emitted.append(render_html(content, md_path, output_dir=html_dir)) | |
| # Manifest for ingestion tests / reproducibility | |
| manifest_path = out_root / "manifest.json" | |
| manifest_json = {k: str(v.relative_to(out_root)) for k, v in sorted(manifest.items())} | |
| manifest_path.write_text( | |
| __import__("json").dumps(manifest_json, indent=2) + "\n", encoding="utf-8" | |
| ) | |
| # Remove stale files from a previous run that are no longer produced. | |
| expected_names = {p.name for p in emitted} | |
| for d in (markdown_dir, pdf_dir, docx_dir, html_dir): | |
| for f in list(d.glob("*")): | |
| if f.is_file() and f.name not in expected_names: | |
| f.unlink() | |
| return manifest | |
| def main() -> int: | |
| parser = argparse.ArgumentParser(description="Generate the AskMyDocs enterprise documentation corpus.") | |
| parser.add_argument("--out", default=str(DATA_DIR), help="Output directory (default: data/corpus)") | |
| args = parser.parse_args() | |
| out_root = Path(args.out).resolve() | |
| docs = build_documents() | |
| emit_files(docs, out_root) | |
| print(f"Generated {len(docs)} documents under {out_root}") | |
| for fmt in ("markdown", "pdf", "docx", "html"): | |
| d = out_root / fmt | |
| print(f" {fmt}: {len(list(d.glob('*')))} file(s)") | |
| print(f"Manifest written to {out_root / 'manifest.json'}") | |
| return 0 | |
| if __name__ == "__main__": | |
| raise SystemExit(main()) |