File size: 35,731 Bytes
f8b48da | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 | """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()) |