mirror of
https://github.com/langflow-ai/langflow.git
synced 2026-07-23 22:15:24 +08:00
docs: replace api build automation (#12214)
* remove-workflows-file-and-script * tag-hidden-endpoints * update-scripts-and-specs * fix: nightly now properly gets 1.9.0 branch (#12215) before it was attempting to pull release-notes as letters are alphanumerically after numbers when we sort -V then grab tail now we only look at branch names that follow the pattern '^release-[0-9]+\.[0-9]+\.[0-9]+$' * docs: add search icon (#12216) add-back-svg --------- Co-authored-by: Adam-Aghili <149833988+Adam-Aghili@users.noreply.github.com> Co-authored-by: Debojit Kaushik <Kaushik.debojit@gmail.com>
This commit is contained in:
124
.github/workflows/docs-update-openapi.yml
vendored
124
.github/workflows/docs-update-openapi.yml
vendored
@ -1,124 +0,0 @@
|
||||
name: Update OpenAPI Spec
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 20 * * 1" # Monday 4pm EST
|
||||
workflow_dispatch: # Allow manual trigger
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
check-openapi-updates:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install jq
|
||||
run: sudo apt-get install -y jq
|
||||
|
||||
- name: Check if there is already an open pull request
|
||||
id: check_pull_request
|
||||
run: |
|
||||
if [[ -n $(gh pr list --state open --repo ${{ github.repository }} | grep "docs: OpenAPI spec") ]]; then
|
||||
echo "There is already an open PR with updates to the OpenAPI spec. Merge or close that PR first. Skipping."
|
||||
echo "pr_exists=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "pr_exists=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Run Langflow container and get OpenAPI spec
|
||||
if: steps.check_pull_request.outputs.pr_exists != 'true'
|
||||
run: |
|
||||
# Start the container in the background
|
||||
docker run -d --name langflow -p 7860:7860 langflowai/langflow:latest
|
||||
|
||||
# Wait for the service to be ready (adjust timeout as needed)
|
||||
echo "Waiting for Langflow to start..."
|
||||
timeout=60
|
||||
elapsed=0
|
||||
while ! curl -s http://localhost:7860/health > /dev/null; do
|
||||
sleep 2
|
||||
elapsed=$((elapsed+2))
|
||||
if [ "$elapsed" -ge "$timeout" ]; then
|
||||
echo "Timed out waiting for Langflow to start"
|
||||
exit 1
|
||||
fi
|
||||
echo "Still waiting... ($elapsed seconds)"
|
||||
done
|
||||
|
||||
# Get the OpenAPI spec and save to file
|
||||
echo "Fetching OpenAPI spec..."
|
||||
curl -s http://localhost:7860/openapi.json > docs/openapi/new_openapi.json
|
||||
|
||||
# Verify file was created
|
||||
ls -la docs/openapi/new_openapi.json
|
||||
|
||||
# stop the container when done
|
||||
docker stop langflow
|
||||
|
||||
- name: Compare OpenAPI files
|
||||
if: steps.check_pull_request.outputs.pr_exists != 'true'
|
||||
id: compare
|
||||
run: |
|
||||
# Extract versions
|
||||
NEW_VERSION=$(jq -r '.info.version' docs/openapi/new_openapi.json)
|
||||
CURRENT_VERSION=$(jq -r '.info.version' docs/openapi/openapi.json)
|
||||
|
||||
echo "Current version: $CURRENT_VERSION"
|
||||
echo "New version: $NEW_VERSION"
|
||||
|
||||
# Compare file content (normalize by sorting keys)
|
||||
jq --sort-keys . docs/openapi/new_openapi.json > docs/openapi/sorted_new.json
|
||||
jq --sort-keys . docs/openapi/openapi.json > docs/openapi/sorted_current.json
|
||||
|
||||
if ! cmp -s docs/openapi/sorted_new.json docs/openapi/sorted_current.json; then
|
||||
echo "OpenAPI spec content has changed."
|
||||
|
||||
# Clean the new spec for ReDoc formatting
|
||||
echo "Cleaning OpenAPI spec formatting..."
|
||||
python3 docs/scripts/clean_openapi_formatting.py docs/openapi/new_openapi.json
|
||||
|
||||
# Compare versions (assuming semantic versioning)
|
||||
if [ "$(printf '%s\n' "$CURRENT_VERSION" "$NEW_VERSION" | sort -V | tail -n1)" == "$NEW_VERSION" ] && [ "$CURRENT_VERSION" != "$NEW_VERSION" ]; then
|
||||
echo "New version detected. Creating PR."
|
||||
echo "NEEDS_UPDATE=true" >> $GITHUB_ENV
|
||||
echo "NEW_VERSION=$NEW_VERSION" >> $GITHUB_ENV
|
||||
echo "UPDATE_REASON=version upgraded from $CURRENT_VERSION to $NEW_VERSION" >> $GITHUB_ENV
|
||||
else
|
||||
echo "File content changed but version remains the same. Creating PR."
|
||||
echo "NEEDS_UPDATE=true" >> $GITHUB_ENV
|
||||
echo "NEW_VERSION=$CURRENT_VERSION" >> $GITHUB_ENV
|
||||
echo "UPDATE_REASON=content updated without version change" >> $GITHUB_ENV
|
||||
fi
|
||||
|
||||
cat docs/openapi/new_openapi.json | jq > docs/openapi/openapi.json
|
||||
else
|
||||
echo "No changes detected in OpenAPI spec content."
|
||||
echo "NEEDS_UPDATE=false" >> $GITHUB_ENV
|
||||
fi
|
||||
|
||||
# Clean up
|
||||
rm docs/openapi/new_openapi.json docs/openapi/sorted_new.json docs/openapi/sorted_current.json
|
||||
|
||||
- name: Create Pull Request
|
||||
if: env.NEEDS_UPDATE == 'true' && steps.check_pull_request.outputs.pr_exists != 'true'
|
||||
uses: peter-evans/create-pull-request@v8
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
commit-message: "docs: OpenAPI spec ${{ env.UPDATE_REASON }}"
|
||||
title: "docs: OpenAPI spec ${{ env.UPDATE_REASON }}"
|
||||
body: |
|
||||
This PR updates the OpenAPI spec.
|
||||
|
||||
Update reason: ${{ env.UPDATE_REASON }}
|
||||
Version: ${{ env.NEW_VERSION }}
|
||||
branch: update-openapi-spec
|
||||
branch-suffix: timestamp
|
||||
delete-branch: true
|
||||
reviewers: mendonk
|
||||
@ -1,6 +1,10 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Pull OpenAPI spec files from the langflow-ai/sdk repository.
|
||||
|
||||
This script is only for syncing external SDK specs when needed.
|
||||
It is not used by the main Langflow OpenAPI generation workflow,
|
||||
which now relies on `docs/openapi/generate_openapi.py`.
|
||||
|
||||
Usage:
|
||||
python3 fetch_openapi_spec.py # Download all files
|
||||
python3 fetch_openapi_spec.py --file <filename> # Download specific file
|
||||
|
||||
114
docs/openapi/generate_openapi.py
Executable file
114
docs/openapi/generate_openapi.py
Executable file
@ -0,0 +1,114 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate the Langflow OpenAPI specification.
|
||||
|
||||
This script imports the Langflow FastAPI application and writes its OpenAPI
|
||||
schema to a JSON file in this directory.
|
||||
|
||||
Usage (from repository root):
|
||||
|
||||
uv run python docs/openapi/generate_openapi.py
|
||||
uv run python docs/openapi/generate_openapi.py --output openapi-1.5.0.json
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from langflow.main import create_app
|
||||
|
||||
|
||||
def _clean_descriptions(spec: dict[str, Any]) -> None:
|
||||
"""Convert newlines in operation descriptions to <br> for better ReDoc rendering."""
|
||||
paths = spec.get("paths") or {}
|
||||
for path_item in paths.values():
|
||||
if not isinstance(path_item, dict):
|
||||
continue
|
||||
for operation in path_item.values():
|
||||
if not isinstance(operation, dict):
|
||||
continue
|
||||
description = operation.get("description")
|
||||
if isinstance(description, str) and description:
|
||||
operation["description"] = description.replace("\n", "<br>")
|
||||
|
||||
|
||||
def _collect_and_rewrite_defs(node: Any, collected: dict[str, Any]) -> None:
|
||||
"""Hoist JSON Schema `$defs` into `components.schemas` and rewrite refs.
|
||||
|
||||
Some tooling (like Redoc's sampler) cannot handle JSON Pointer segments
|
||||
that contain `$defs`. To keep the schema tool-friendly, we:
|
||||
- Collect any `$defs` blocks we find anywhere in the tree.
|
||||
- Remove those local `$defs` blocks.
|
||||
- Rewrite `"$ref": "#/$defs/Name"` to `"#/components/schemas/Name"`.
|
||||
"""
|
||||
if isinstance(node, dict):
|
||||
# Hoist local $defs
|
||||
if "$defs" in node and isinstance(node["$defs"], dict):
|
||||
for name, schema in node["$defs"].items():
|
||||
# Only add if not already present; avoid clobbering explicit components
|
||||
collected.setdefault(name, schema)
|
||||
node.pop("$defs", None)
|
||||
|
||||
# Rewrite local refs
|
||||
ref = node.get("$ref")
|
||||
if isinstance(ref, str) and ref.startswith("#/$defs/"):
|
||||
name = ref.split("/")[-1]
|
||||
node["$ref"] = f"#/components/schemas/{name}"
|
||||
|
||||
# Recurse into values
|
||||
for value in node.values():
|
||||
_collect_and_rewrite_defs(value, collected)
|
||||
|
||||
elif isinstance(node, list):
|
||||
for item in node:
|
||||
_collect_and_rewrite_defs(item, collected)
|
||||
|
||||
|
||||
def _normalize_defs(spec: dict[str, Any]) -> None:
|
||||
"""Normalize `$defs` usage for better compatibility with tooling."""
|
||||
collected: dict[str, Any] = {}
|
||||
_collect_and_rewrite_defs(spec, collected)
|
||||
|
||||
if not collected:
|
||||
return
|
||||
|
||||
components = spec.setdefault("components", {})
|
||||
schemas = components.setdefault("schemas", {})
|
||||
for name, schema in collected.items():
|
||||
schemas.setdefault(name, schema)
|
||||
|
||||
|
||||
def generate_openapi(output_path: Path) -> None:
|
||||
"""Generate the OpenAPI spec and write it to ``output_path``."""
|
||||
app = create_app()
|
||||
spec = app.openapi()
|
||||
|
||||
_clean_descriptions(spec)
|
||||
_normalize_defs(spec)
|
||||
|
||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with output_path.open("w", encoding="utf-8") as f:
|
||||
json.dump(spec, f, indent=2, sort_keys=True, ensure_ascii=False)
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description="Generate Langflow OpenAPI specification.")
|
||||
parser.add_argument(
|
||||
"--output",
|
||||
"-o",
|
||||
type=Path,
|
||||
default=Path(__file__).parent / "openapi.json",
|
||||
help="Output file path (default: docs/openapi/openapi.json)",
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def main() -> None:
|
||||
args = parse_args()
|
||||
generate_openapi(args.output)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,73 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Clean OpenAPI specification formatting for better ReDoc display.
|
||||
|
||||
This script converts newlines in descriptions to HTML breaks for proper
|
||||
rendering in ReDoc documentation.
|
||||
"""
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
MIN_ARGS = 2
|
||||
|
||||
|
||||
def clean_openapi_formatting(input_file: str, output_file: str | None = None) -> None:
|
||||
"""Clean OpenAPI spec formatting by converting newlines to HTML breaks.
|
||||
|
||||
Args:
|
||||
input_file: Path to input OpenAPI JSON file
|
||||
output_file: Path to output file (defaults to overwriting input)
|
||||
"""
|
||||
if output_file is None:
|
||||
output_file = input_file
|
||||
|
||||
try:
|
||||
# Load the OpenAPI spec
|
||||
input_path = Path(input_file)
|
||||
with input_path.open(encoding="utf-8") as f:
|
||||
spec = json.load(f)
|
||||
|
||||
# Fix description formatting by converting newlines to HTML breaks
|
||||
if "paths" in spec:
|
||||
for path_item in spec["paths"].values():
|
||||
for operation in path_item.values():
|
||||
if isinstance(operation, dict) and "description" in operation:
|
||||
description = operation["description"]
|
||||
if description:
|
||||
# Convert newlines to HTML breaks for better ReDoc rendering
|
||||
operation["description"] = description.replace("\n", "<br>")
|
||||
|
||||
# Save the cleaned spec
|
||||
output_path = Path(output_file)
|
||||
with output_path.open("w", encoding="utf-8") as f:
|
||||
json.dump(spec, f, indent=2, ensure_ascii=False)
|
||||
|
||||
# Success message (using sys.stdout for consistency)
|
||||
sys.stdout.write(f"OpenAPI spec cleaned successfully: {output_file}\n")
|
||||
|
||||
except FileNotFoundError:
|
||||
sys.stderr.write(f"Error: File not found: {input_file}\n")
|
||||
sys.exit(1)
|
||||
except json.JSONDecodeError as e:
|
||||
sys.stderr.write(f"Error: Invalid JSON in {input_file}: {e}\n")
|
||||
sys.exit(1)
|
||||
except OSError as e:
|
||||
sys.stderr.write(f"Error: {e}\n")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def main():
|
||||
"""Main entry point."""
|
||||
if len(sys.argv) < MIN_ARGS:
|
||||
sys.stderr.write("Usage: python clean_openapi_formatting.py <input_file> [output_file]\n")
|
||||
sys.exit(1)
|
||||
|
||||
input_file = sys.argv[1]
|
||||
output_file = sys.argv[2] if len(sys.argv) > MIN_ARGS else None
|
||||
|
||||
clean_openapi_formatting(input_file, output_file)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@ -36,7 +36,7 @@ from langflow.agentic.services.provider_service import (
|
||||
)
|
||||
from langflow.api.utils.core import CurrentActiveUser, DbSession
|
||||
|
||||
router = APIRouter(prefix="/agentic", tags=["Agentic"])
|
||||
router = APIRouter(prefix="/agentic", tags=["Agentic"], include_in_schema=False)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
|
||||
@ -33,7 +33,7 @@ from langflow.api.v1.schemas.deployments import (
|
||||
FlowVersionIdsQuery,
|
||||
)
|
||||
|
||||
router = APIRouter(prefix="/deployments", tags=["Deployments"])
|
||||
router = APIRouter(prefix="/deployments", tags=["Deployments"], include_in_schema=False)
|
||||
|
||||
|
||||
DeploymentProviderAccountIdQuery = Annotated[
|
||||
|
||||
@ -33,7 +33,7 @@ from langflow.services.database.models.flow_version.model import (
|
||||
)
|
||||
from langflow.services.deps import get_settings_service
|
||||
|
||||
router = APIRouter(prefix="/flows/{flow_id}/versions", tags=["Flow Versions"])
|
||||
router = APIRouter(prefix="/flows/{flow_id}/versions", tags=["Flow Versions"], include_in_schema=False)
|
||||
|
||||
|
||||
def strip_version_data(data: dict | None) -> dict | None:
|
||||
|
||||
@ -20,7 +20,7 @@ from langflow.api.v1.mcp_utils import (
|
||||
handle_read_resource,
|
||||
)
|
||||
|
||||
router = APIRouter(prefix="/mcp", tags=["mcp"])
|
||||
router = APIRouter(prefix="/mcp", tags=["mcp"], include_in_schema=False)
|
||||
|
||||
server = Server("langflow-mcp-server")
|
||||
|
||||
|
||||
@ -313,6 +313,7 @@ async def list_project_tools(
|
||||
"/{project_id}/sse",
|
||||
response_class=HTMLResponse,
|
||||
dependencies=[Depends(raise_error_if_astra_cloud_env)],
|
||||
include_in_schema=False,
|
||||
)
|
||||
async def im_alive(project_id: str): # noqa: ARG001
|
||||
return Response()
|
||||
@ -322,6 +323,7 @@ async def im_alive(project_id: str): # noqa: ARG001
|
||||
"/{project_id}/sse",
|
||||
response_class=HTMLResponse,
|
||||
dependencies=[Depends(raise_error_if_astra_cloud_env)],
|
||||
include_in_schema=False,
|
||||
)
|
||||
async def handle_project_sse(
|
||||
project_id: UUID,
|
||||
@ -399,8 +401,16 @@ async def _handle_project_sse_messages(
|
||||
current_request_variables_ctx.reset(req_vars_token)
|
||||
|
||||
|
||||
@router.post("/{project_id}", dependencies=[Depends(raise_error_if_astra_cloud_env)])
|
||||
@router.post("/{project_id}/", dependencies=[Depends(raise_error_if_astra_cloud_env)])
|
||||
@router.post(
|
||||
"/{project_id}",
|
||||
dependencies=[Depends(raise_error_if_astra_cloud_env)],
|
||||
include_in_schema=False,
|
||||
)
|
||||
@router.post(
|
||||
"/{project_id}/",
|
||||
dependencies=[Depends(raise_error_if_astra_cloud_env)],
|
||||
include_in_schema=False,
|
||||
)
|
||||
async def handle_project_messages(
|
||||
project_id: UUID,
|
||||
request: Request,
|
||||
@ -415,7 +425,7 @@ async def handle_project_messages(
|
||||
########################################################
|
||||
|
||||
|
||||
@router.head("/{project_id}/streamable")
|
||||
@router.head("/{project_id}/streamable", include_in_schema=False)
|
||||
async def streamable_health(project_id: UUID): # noqa: ARG001
|
||||
return Response()
|
||||
|
||||
@ -454,6 +464,7 @@ async def _dispatch_project_streamable_http(
|
||||
streamable_http_route_config = {
|
||||
"methods": ["GET", "POST", "DELETE"],
|
||||
"response_class": ResponseNoOp,
|
||||
"include_in_schema": False,
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user