c56bef871b
Sync docs with Docusaurus / sync (push) Waiting to run
Tests / Check if changed (push) Waiting to run
Tests / format (push) Blocked by required conditions
Tests / check-imports (push) Blocked by required conditions
Tests / Unit / macos-latest (push) Blocked by required conditions
Tests / Unit / ubuntu-latest (push) Blocked by required conditions
Tests / Unit / windows-latest (push) Blocked by required conditions
Tests / mypy (push) Blocked by required conditions
Tests / Integration / ubuntu-latest (push) Blocked by required conditions
Tests / Integration / macos-latest (push) Blocked by required conditions
Tests / Integration / windows-latest (push) Blocked by required conditions
Tests / notify-slack-on-failure (push) Blocked by required conditions
Tests / Mark tests as completed (push) Blocked by required conditions
Update Platform Components Table / update (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
Docker image release / Build base image (push) Waiting to run
404 lines
15 KiB
Python
404 lines
15 KiB
Python
# SPDX-FileCopyrightText: 2022-present deepset GmbH <info@deepset.ai>
|
|
#
|
|
# SPDX-License-Identifier: Apache-2.0
|
|
|
|
"""
|
|
Generate a Haystack Enterprise Platform Components MDX table.
|
|
|
|
Scans deepset-ai/haystack and deepset-ai/haystack-core-integrations for classes
|
|
decorated with @component, cross-references them against the platform component
|
|
schema (schema-full-component-list.json from deepset-ai/haystack-runtime), and
|
|
writes a formatted MDX page.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import ast
|
|
import json
|
|
import logging
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# Maps schema `family` value / module path segment → display type name
|
|
TYPE_MAP: dict[str, str] = {
|
|
"generators": "Generator",
|
|
"retrievers": "Retriever",
|
|
"embedders": "Embedder",
|
|
"converters": "Converter",
|
|
"rankers": "Ranker",
|
|
"document_stores": "Document Store",
|
|
"preprocessors": "Preprocessor",
|
|
"readers": "Reader",
|
|
"routers": "Router",
|
|
"joiners": "Joiner",
|
|
"builders": "Builder",
|
|
"classifiers": "Classifier",
|
|
"extractors": "Extractor",
|
|
"samplers": "Sampler",
|
|
"writers": "Writer",
|
|
"connectors": "Connector",
|
|
"fetchers": "Fetcher",
|
|
"validators": "Validator",
|
|
"audio": "Audio",
|
|
}
|
|
|
|
# Human-readable partner labels; snake_case partner keys → display name.
|
|
# Keys that map to the same value are intentionally merged into one section.
|
|
PARTNER_LABELS: dict[str, str] = {
|
|
"aimlapi": "AIML API",
|
|
"alloydb": "AlloyDB",
|
|
"amazon_bedrock": "Amazon Bedrock",
|
|
"amazon_sagemaker": "Amazon SageMaker",
|
|
"amazon_textract": "Amazon Textract",
|
|
"anthropic": "Anthropic",
|
|
"arcadedb": "ArcadeDB",
|
|
"astra": "Astra",
|
|
"azure_ai_search": "Azure AI Search",
|
|
"azure_doc_intelligence": "Azure Document Intelligence",
|
|
"brave": "Brave",
|
|
"chroma": "Chroma",
|
|
"cohere": "Cohere",
|
|
"docling": "Docling",
|
|
"docling_serve": "Docling", # merged with docling
|
|
"elasticsearch": "Elasticsearch",
|
|
"falkordb": "FalkorDB",
|
|
"fastembed": "FastEmbed",
|
|
"firecrawl": "Firecrawl",
|
|
"github": "GitHub",
|
|
"google_genai": "Google Generative AI",
|
|
"huggingface_api": "Hugging Face API",
|
|
"jina": "Jina AI",
|
|
"langfuse": "Langfuse",
|
|
"lara": "Lara",
|
|
"litellm": "LiteLLM",
|
|
"llama_cpp": "Llama.cpp",
|
|
"llama_stack": "Llama Stack",
|
|
"markitdown": "MarkItDown",
|
|
"mem0": "Mem0",
|
|
"meta_llama": "Meta Llama",
|
|
"mistral": "Mistral",
|
|
"mongodb_atlas": "MongoDB Atlas",
|
|
"nvidia": "NVIDIA",
|
|
"ollama": "Ollama",
|
|
"opensearch": "OpenSearch",
|
|
"openrouter": "OpenRouter",
|
|
"oracle": "Oracle",
|
|
"perplexity": "Perplexity",
|
|
"pgvector": "pgvector",
|
|
"pinecone": "Pinecone",
|
|
"presidio": "Presidio",
|
|
"pyversity": "Pyversity",
|
|
"qdrant": "Qdrant",
|
|
"s3": "Amazon S3",
|
|
"snowflake": "Snowflake",
|
|
"sqlalchemy": "SQLAlchemy",
|
|
"stackit": "STACKIT",
|
|
"supabase": "Supabase",
|
|
"tavily": "Tavily",
|
|
"togetherai": "TogetherAI",
|
|
"unstructured": "Unstructured",
|
|
"valkey": "Valkey",
|
|
"vespa": "Vespa",
|
|
"vllm": "vLLM",
|
|
"voyage": "Voyage AI",
|
|
"voyage_embedders": "Voyage AI", # merged with voyage
|
|
"weave": "Weights & Biases (Weave)",
|
|
"weaviate": "Weaviate",
|
|
}
|
|
|
|
_HAYSTACK_DOCS_BASE = "https://docs.haystack.deepset.ai"
|
|
_HAYSTACK_DOCS_PREFIX = "/docs" # Docusaurus routeBasePath
|
|
|
|
# Namespaces that must never appear in public documentation
|
|
_EXCLUDED_NAMESPACES = ("deepset_cloud_custom_nodes.", "studio_internal.", "haystack_experimental.", "deepl_haystack.")
|
|
|
|
# Individual class names to exclude regardless of namespace
|
|
_EXCLUDED_CLASS_NAMES = {
|
|
"SuperComponent", # internal base class, not a user-facing component
|
|
"LLM", # alias not present in Haystack OS
|
|
}
|
|
|
|
# Sets allow O(1) membership tests
|
|
_PACKAGE_ROOTS: set[str] = {"haystack_integrations", "haystack"}
|
|
_COMPONENT_DECORATORS: set[str] = {"component", "super_component"}
|
|
|
|
|
|
# ── Component type helpers ────────────────────────────────────────────────────
|
|
|
|
|
|
def infer_type(family: str | None, fqn: str) -> str:
|
|
"""Return a human-readable type string from the schema family or FQN path segments."""
|
|
if family and family in TYPE_MAP:
|
|
return TYPE_MAP[family]
|
|
return next((TYPE_MAP[seg] for seg in fqn.split(".") if seg in TYPE_MAP), "Component")
|
|
|
|
|
|
def partner_label_for(fqn: str) -> str | None:
|
|
"""Return the display-name partner label for a ``haystack_integrations.*`` FQN."""
|
|
parts = fqn.split(".")
|
|
# haystack_integrations . components . <family> . <partner> . …
|
|
if len(parts) < 4:
|
|
return None
|
|
key = parts[3]
|
|
return PARTNER_LABELS.get(key, key.replace("_", " ").title())
|
|
|
|
|
|
# ── Docs-site link map ────────────────────────────────────────────────────────
|
|
|
|
|
|
def _parse_frontmatter(text: str) -> dict[str, str]:
|
|
"""Extract key-value pairs from YAML-style frontmatter delimited by ``---``."""
|
|
if not text.startswith("---"):
|
|
return {}
|
|
try:
|
|
end = text.index("---", 3)
|
|
except ValueError:
|
|
return {}
|
|
result: dict[str, str] = {}
|
|
for line in text[3:end].splitlines():
|
|
if ":" in line:
|
|
key, _, value = line.partition(":")
|
|
result[key.strip()] = value.strip().strip('"')
|
|
return result
|
|
|
|
|
|
def scan_docs_links(docs_src: Path) -> dict[str, str]:
|
|
"""
|
|
Scan docs-website MDX files and return ``{component_title: absolute_url}``.
|
|
|
|
Reads only the first 1 KB of each file — enough to cover the frontmatter —
|
|
avoiding the cost of loading large MDX files in full.
|
|
"""
|
|
link_map: dict[str, str] = {}
|
|
for mdx_file in docs_src.rglob("*.mdx"):
|
|
try:
|
|
with mdx_file.open(encoding="utf-8") as fh:
|
|
head = fh.read(1024)
|
|
except OSError:
|
|
continue
|
|
fm = _parse_frontmatter(head)
|
|
title = fm.get("title", "")
|
|
slug = fm.get("slug", "")
|
|
if title and slug:
|
|
link_map[title] = f"{_HAYSTACK_DOCS_BASE}{_HAYSTACK_DOCS_PREFIX}{slug}"
|
|
return link_map
|
|
|
|
|
|
# ── Source-scanning helpers ───────────────────────────────────────────────────
|
|
|
|
|
|
def has_component_decorator(node: ast.ClassDef) -> bool:
|
|
"""Return True if *node* has a ``@component`` or ``@super_component`` decorator."""
|
|
for dec in node.decorator_list:
|
|
if isinstance(dec, ast.Name) and dec.id in _COMPONENT_DECORATORS:
|
|
return True
|
|
if isinstance(dec, ast.Call) and isinstance(dec.func, ast.Name) and dec.func.id in _COMPONENT_DECORATORS:
|
|
return True
|
|
return False
|
|
|
|
|
|
def compute_module_path(file_path: Path, repo_root: Path) -> str | None:
|
|
"""Convert a ``.py`` file path to a dotted Python module path."""
|
|
try:
|
|
parts = list(file_path.relative_to(repo_root).with_suffix("").parts)
|
|
except ValueError:
|
|
return None
|
|
if parts and parts[-1] == "__init__":
|
|
parts.pop()
|
|
start = next((i for i, p in enumerate(parts) if p in _PACKAGE_ROOTS), None)
|
|
return ".".join(parts[start:]) if start is not None else None
|
|
|
|
|
|
def scan_for_components(scan_dir: Path, repo_root: Path) -> set[str]:
|
|
"""Scan *scan_dir* for Python files with ``@component``-decorated classes."""
|
|
found: set[str] = set()
|
|
py_files = list(scan_dir.rglob("*.py"))
|
|
logger.info("Scanned %d Python files in %s", len(py_files), scan_dir)
|
|
|
|
for py_file in py_files:
|
|
module_path = compute_module_path(py_file, repo_root)
|
|
if module_path is None:
|
|
continue
|
|
try:
|
|
source = py_file.read_text(encoding="utf-8")
|
|
except UnicodeDecodeError:
|
|
continue
|
|
# Fast pre-filter: skip files with no decorator mention at all
|
|
if "@component" not in source and "@super_component" not in source:
|
|
continue
|
|
try:
|
|
tree = ast.parse(source, filename=str(py_file))
|
|
except SyntaxError as exc:
|
|
logger.warning("Could not parse %s: %s", py_file, exc)
|
|
continue
|
|
for node in ast.walk(tree):
|
|
if isinstance(node, ast.ClassDef) and has_component_decorator(node):
|
|
found.add(f"{module_path}.{node.name}")
|
|
|
|
return found
|
|
|
|
|
|
# ── Schema loading ────────────────────────────────────────────────────────────
|
|
|
|
|
|
def load_platform_components(schema_path: Path) -> dict[str, dict]:
|
|
"""Parse ``schema-full-component-list.json`` and return public components."""
|
|
all_defs: dict = json.loads(schema_path.read_text(encoding="utf-8")).get("definitions", {}).get("Components", {})
|
|
result: dict[str, dict] = {}
|
|
for fqn, defn in all_defs.items():
|
|
if any(fqn.startswith(ns) for ns in _EXCLUDED_NAMESPACES):
|
|
continue
|
|
if not fqn.startswith(("haystack.", "haystack_integrations.")):
|
|
continue
|
|
if fqn.split(".")[-1] in _EXCLUDED_CLASS_NAMES:
|
|
continue
|
|
type_props = defn.get("properties", {}).get("type", {})
|
|
result[fqn] = {"title": defn.get("title", fqn.split(".")[-1]), "family": type_props.get("family", "")}
|
|
return result
|
|
|
|
|
|
# ── MDX generation ────────────────────────────────────────────────────────────
|
|
|
|
|
|
def build_mdx(
|
|
platform_components: dict[str, dict], source_components: set[str], docs_link_map: dict[str, str] | None = None
|
|
) -> str:
|
|
"""Render the MDX page content."""
|
|
_link_map = docs_link_map or {}
|
|
core_fqns: list[str] = []
|
|
partner_components: dict[str, list[str]] = {}
|
|
visible_count = 0
|
|
|
|
for fqn in platform_components:
|
|
if fqn not in source_components:
|
|
logger.warning("Schema component not found in source scan: %s", fqn)
|
|
continue
|
|
cls = fqn.split(".")[-1]
|
|
if cls.endswith("Generator") and not cls.endswith("ChatGenerator"):
|
|
continue
|
|
visible_count += 1
|
|
if fqn.startswith("haystack_integrations."):
|
|
partner_components.setdefault(partner_label_for(fqn) or "Other", []).append(fqn)
|
|
else:
|
|
core_fqns.append(fqn)
|
|
|
|
def render_table(fqns: list[str]) -> list[str]:
|
|
rows = [
|
|
"| Component | Type | Haystack Enterprise Platform |",
|
|
"|-----------|------|------------------------------|",
|
|
]
|
|
for fqn in sorted(fqns, key=lambda x: x.split(".")[-1].lower()):
|
|
meta = platform_components[fqn]
|
|
name = meta["title"]
|
|
link = _link_map.get(name, "")
|
|
name_cell = f"[{name}]({link})" if link else name
|
|
rows.append(f"| {name_cell} | {infer_type(meta.get('family'), fqn)} | ✅ Available |")
|
|
return rows
|
|
|
|
sections: list[str] = [
|
|
"---",
|
|
'title: "Haystack Enterprise Components"',
|
|
"id: platform-components",
|
|
'slug: "/platform-components"',
|
|
'description: "A complete list of Haystack components available on the Haystack Enterprise Platform,'
|
|
' grouped by integration partner."',
|
|
"---",
|
|
"",
|
|
"# Haystack Enterprise Components",
|
|
"",
|
|
f"The Haystack Enterprise Platform currently supports **{visible_count} components**"
|
|
f" and **{len(partner_components)} integrations**."
|
|
" The following table lists them grouped by integration partner.",
|
|
"",
|
|
]
|
|
|
|
if core_fqns:
|
|
sections += ["## Core Components", "", *render_table(core_fqns), ""]
|
|
|
|
for label in sorted(partner_components, key=str.lower):
|
|
sections += [f"## {label}", "", *render_table(partner_components[label]), ""]
|
|
|
|
return "\n".join(sections)
|
|
|
|
|
|
# ── Entry point ───────────────────────────────────────────────────────────────
|
|
|
|
|
|
def main(argv: list[str] | None = None) -> None:
|
|
"""Entry point: parse arguments, run the scan, and write the MDX file."""
|
|
parser = argparse.ArgumentParser(description="Generate the Haystack Enterprise Platform Components MDX table.")
|
|
parser.add_argument(
|
|
"--haystack-src", required=True, type=Path, metavar="PATH", help="Root of the deepset-ai/haystack checkout."
|
|
)
|
|
parser.add_argument(
|
|
"--integrations-src",
|
|
required=True,
|
|
type=Path,
|
|
metavar="PATH",
|
|
help="Root of the deepset-ai/haystack-core-integrations checkout.",
|
|
)
|
|
parser.add_argument(
|
|
"--schema", required=True, type=Path, metavar="PATH", help="Path to schema JSON file in the platform repo."
|
|
)
|
|
parser.add_argument(
|
|
"--output",
|
|
type=Path,
|
|
metavar="PATH",
|
|
default=Path("docs-website/docs/overview/platform-components.mdx"),
|
|
help="Destination .mdx file path (default: docs-website/docs/overview/platform-components.mdx).",
|
|
)
|
|
parser.add_argument(
|
|
"--dry-run", action="store_true", help="Print generated content to stdout instead of writing the file."
|
|
)
|
|
|
|
args = parser.parse_args(argv)
|
|
|
|
for path, flag in [
|
|
(args.haystack_src, "--haystack-src"),
|
|
(args.integrations_src, "--integrations-src"),
|
|
(args.schema, "--schema"),
|
|
]:
|
|
if not path.exists():
|
|
print(f"ERROR: {flag} path does not exist: {path}", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
try:
|
|
platform_components = load_platform_components(args.schema)
|
|
except (json.JSONDecodeError, OSError) as exc:
|
|
print(f"ERROR: Cannot load schema file: {exc}", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
haystack_pkg = args.haystack_src / "haystack"
|
|
source_components = scan_for_components(
|
|
haystack_pkg if haystack_pkg.is_dir() else args.haystack_src, args.haystack_src
|
|
)
|
|
integrations_pkg = args.integrations_src / "integrations"
|
|
source_components |= scan_for_components(
|
|
integrations_pkg if integrations_pkg.is_dir() else args.integrations_src, args.integrations_src
|
|
)
|
|
|
|
logger.info("Platform components in schema (public namespaces): %d", len(platform_components))
|
|
logger.info("Components matched to source scan: %d", sum(1 for f in platform_components if f in source_components))
|
|
|
|
docs_src = args.haystack_src / "docs-website" / "docs"
|
|
if not docs_src.is_dir():
|
|
logger.warning("docs-website/docs not found under --haystack-src, component links will be omitted")
|
|
docs_link_map = scan_docs_links(docs_src) if docs_src.is_dir() else {}
|
|
|
|
mdx = build_mdx(platform_components, source_components, docs_link_map)
|
|
|
|
if args.dry_run:
|
|
print(mdx)
|
|
else:
|
|
args.output.parent.mkdir(parents=True, exist_ok=True)
|
|
args.output.write_text(mdx, encoding="utf-8")
|
|
logger.info("Written to %s", args.output)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|