Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions libs/community/langchain_community/tools/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@
from langchain_community.tools.ainetwork.value import (
AINValueOps,
)
from langchain_community.tools.api_governor.tool import (
APIGovernorTool,
)
from langchain_community.tools.arxiv.tool import (
ArxivQueryRun,
)
Expand Down Expand Up @@ -289,6 +292,9 @@
from langchain_community.tools.sleep.tool import (
SleepTool,
)
from langchain_community.tools.spec_test_generator.tool import (
SpecTestGeneratorTool,
)
from langchain_community.tools.spark_sql.tool import (
BaseSparkSQLTool,
InfoSparkSQLTool,
Expand Down Expand Up @@ -357,6 +363,7 @@
"AINTransfer",
"AINValueOps",
"AIPluginTool",
"APIGovernorTool",
"APIOperation",
"ArxivQueryRun",
"AskNewsSearch",
Expand Down Expand Up @@ -476,6 +483,7 @@
"SlackScheduleMessage",
"SlackSendMessage",
"SleepTool",
"SpecTestGeneratorTool",
"StackExchangeTool",
"StdInInquireTool",
"SteamWebAPIQueryRun",
Expand Down Expand Up @@ -509,6 +517,7 @@
"AINTransfer": "langchain_community.tools.ainetwork.transfer",
"AINValueOps": "langchain_community.tools.ainetwork.value",
"AIPluginTool": "langchain_community.tools.plugin",
"APIGovernorTool": "langchain_community.tools.api_governor.tool",
"APIOperation": "langchain_community.tools.openapi.utils.api_models",
"ArxivQueryRun": "langchain_community.tools.arxiv.tool",
"AskNewsSearch": "langchain_community.tools.asknews.tool",
Expand Down Expand Up @@ -631,6 +640,7 @@
"SlackScheduleMessage": "langchain_community.tools.slack.schedule_message",
"SlackSendMessage": "langchain_community.tools.slack.send_message",
"SleepTool": "langchain_community.tools.sleep.tool",
"SpecTestGeneratorTool": "langchain_community.tools.spec_test_generator.tool",
"StackExchangeTool": "langchain_community.tools.stackexchange.tool",
"StdInInquireTool": "langchain_community.tools.interaction.tool",
"SteamWebAPIQueryRun": "langchain_community.tools.steam.tool",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
"""API Governor Tool for OpenAPI specification governance and validation."""

from langchain_community.tools.api_governor.tool import (
APIGovernorTool,
)

__all__ = ["APIGovernorTool"]
142 changes: 142 additions & 0 deletions libs/community/langchain_community/tools/api_governor/tool.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
"""Tool for API governance and OpenAPI specification validation."""

from pathlib import Path
from typing import Optional, Type

from langchain_core.callbacks import CallbackManagerForToolRun
from langchain_core.tools import BaseTool
from pydantic import BaseModel, Field


class APIGovernorInput(BaseModel):
"""Input for the API Governor tool."""

spec_content: str = Field(
description="The OpenAPI specification content in YAML or JSON format"
)
policy: str = Field(
default="standard",
description="Governance policy level: 'lenient', 'standard', or 'strict'",
)
output_format: str = Field(
default="markdown",
description="Output format: 'markdown', 'json', or 'sarif'",
)


class APIGovernorTool(BaseTool):
"""Tool that validates OpenAPI specifications against governance policies.

This tool uses api-governor to perform automated API governance checks
including security validation, naming conventions, breaking change detection,
and documentation requirements.

The tool checks for:
- Security issues (missing auth, weak schemes)
- Naming convention violations
- Documentation gaps
- Error format consistency
- Breaking changes (when baseline provided)

Install the underlying package with: pip install api-governor
"""

name: str = "api_governor"
description: str = (
"Validates OpenAPI specifications against governance policies. "
"Checks for security issues, naming conventions, documentation gaps, "
"and API design best practices. Useful for API review automation, "
"CI/CD governance gates, and ensuring API consistency. "
"Input should be OpenAPI spec content in YAML or JSON format."
)
args_schema: Type[BaseModel] = APIGovernorInput

def _run(
self,
spec_content: str,
policy: str = "standard",
output_format: str = "markdown",
run_manager: Optional[CallbackManagerForToolRun] = None,
) -> str:
"""Validate an OpenAPI specification against governance policies.

Args:
spec_content: The OpenAPI spec content in YAML or JSON format.
policy: Governance policy level ('lenient', 'standard', 'strict').
output_format: Output format ('markdown', 'json', or 'sarif').
run_manager: Callback manager for the tool run.

Returns:
Governance validation results in the specified format.
"""
try:
from api_governor import APIGovernor
except ImportError:
return (
"api-governor package is not installed. "
"Install it with: pip install api-governor"
)

import tempfile

# Determine file extension based on content
ext = ".yaml"
content_stripped = spec_content.strip()
if content_stripped.startswith("{"):
ext = ".json"

# Write spec content to a temporary file
with tempfile.NamedTemporaryFile(
mode="w", suffix=ext, delete=False
) as tmp_file:
tmp_file.write(spec_content)
tmp_path = Path(tmp_file.name)

try:
# Run governance checks
governor = APIGovernor(spec_path=tmp_path, policy=policy)
result = governor.run()

if output_format == "json":
from api_governor import JSONFormatter

formatter = JSONFormatter(result)
return formatter.format()
elif output_format == "sarif":
from api_governor import SARIFFormatter

formatter = SARIFFormatter(result)
import json

return json.dumps(formatter.to_sarif(), indent=2)
else:
# Default markdown output
output_parts = [f"# API Governance Report\n"]
output_parts.append(f"**Status:** {result.status}\n")
output_parts.append(f"**Policy:** {policy}\n\n")

if result.findings:
output_parts.append("## Findings\n")
for finding in result.findings:
output_parts.append(
f"- **[{finding.severity.value}]** {finding.message}\n"
)
if finding.path:
output_parts.append(f" - Path: `{finding.path}`\n")
if finding.recommendation:
output_parts.append(
f" - Recommendation: {finding.recommendation}\n"
)
else:
output_parts.append("No governance issues found.\n")

output_parts.append("\n## Summary\n")
output_parts.append(f"- Blockers: {len(result.blockers)}\n")
output_parts.append(f"- Majors: {len(result.majors)}\n")
output_parts.append(f"- Minors: {len(result.minors)}\n")

return "".join(output_parts)

finally:
# Cleanup temporary file
tmp_path.unlink(missing_ok=True)
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
"""Spec Test Generator Tool for converting PRDs to requirements and test cases."""

from langchain_community.tools.spec_test_generator.tool import (
SpecTestGeneratorTool,
)

__all__ = ["SpecTestGeneratorTool"]
148 changes: 148 additions & 0 deletions libs/community/langchain_community/tools/spec_test_generator/tool.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
"""Tool for generating requirements and test cases from PRDs."""

from pathlib import Path
from typing import Optional, Type

from langchain_core.callbacks import CallbackManagerForToolRun
from langchain_core.tools import BaseTool
from pydantic import BaseModel, Field


class SpecTestGeneratorInput(BaseModel):
"""Input for the Spec Test Generator tool."""

prd_content: str = Field(
description="The PRD (Product Requirements Document) content in markdown format"
)
output_format: str = Field(
default="markdown",
description="Output format: 'markdown', 'json', or 'gherkin'",
)


class SpecTestGeneratorTool(BaseTool):
"""Tool that generates requirements and test cases from PRDs.

This tool uses spec-test-generator to convert Product Requirements Documents
into formal requirements with stable IDs, test cases, and traceability matrices.

The tool generates:
- Requirements with fingerprint-based stable IDs (REQ-xxxx)
- Test cases linked to requirements (TEST-xxxx)
- Traceability information

Install the underlying package with: pip install spec-test-generator
"""

name: str = "spec_test_generator"
description: str = (
"Converts PRDs (Product Requirements Documents) into formal requirements "
"and test cases with stable, traceable IDs. Useful for generating test "
"specifications, requirements documentation, and traceability matrices "
"from product requirement documents. Input should be PRD content in markdown."
)
args_schema: Type[BaseModel] = SpecTestGeneratorInput

def _run(
self,
prd_content: str,
output_format: str = "markdown",
run_manager: Optional[CallbackManagerForToolRun] = None,
) -> str:
"""Generate requirements and test cases from PRD content.

Args:
prd_content: The PRD content in markdown format.
output_format: Output format ('markdown', 'json', or 'gherkin').
run_manager: Callback manager for the tool run.

Returns:
Generated requirements and test cases in the specified format.
"""
try:
from spec_test_generator import SpecTestGenerator
except ImportError:
return (
"spec-test-generator package is not installed. "
"Install it with: pip install spec-test-generator"
)

import tempfile

# Write PRD content to a temporary file
with tempfile.NamedTemporaryFile(
mode="w", suffix=".md", delete=False
) as tmp_file:
tmp_file.write(prd_content)
tmp_path = Path(tmp_file.name)

try:
# Generate specs
generator = SpecTestGenerator(tmp_path)
result = generator.generate()

if output_format == "json":
import json

return json.dumps(
{
"requirements": [
{
"id": req.id,
"title": req.title,
"description": req.description,
"priority": req.priority.value,
"acceptance_criteria": req.acceptance_criteria,
}
for req in result.requirements
],
"test_cases": [
{
"id": tc.id,
"title": tc.title,
"requirement_id": tc.requirement_id,
"steps": tc.steps,
"expected_result": tc.expected_result,
}
for tc in result.test_cases
],
},
indent=2,
)
elif output_format == "gherkin":
from spec_test_generator import GherkinGenerator

gherkin_gen = GherkinGenerator(
result.requirements, result.test_cases
)
features = gherkin_gen.generate()
return "\n\n".join(
f"# {name}\n{path.read_text()}"
for name, path in features.items()
)
else:
# Default markdown output
output_parts = ["# Generated Requirements\n"]
for req in result.requirements:
output_parts.append(f"## {req.id}: {req.title}\n")
output_parts.append(f"{req.description}\n")
if req.acceptance_criteria:
output_parts.append("**Acceptance Criteria:**\n")
for ac in req.acceptance_criteria:
output_parts.append(f"- {ac}\n")
output_parts.append("\n")

output_parts.append("# Generated Test Cases\n")
for tc in result.test_cases:
output_parts.append(f"## {tc.id}: {tc.title}\n")
output_parts.append(f"**Requirement:** {tc.requirement_id}\n")
output_parts.append("**Steps:**\n")
for i, step in enumerate(tc.steps, 1):
output_parts.append(f"{i}. {step}\n")
output_parts.append(f"**Expected:** {tc.expected_result}\n\n")

return "".join(output_parts)

finally:
# Cleanup temporary file
tmp_path.unlink(missing_ok=True)
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
"""Tests for API Governor Tool."""

import pytest

from langchain_community.tools.api_governor import APIGovernorTool


def test_api_governor_tool_init() -> None:
"""Test APIGovernorTool initialization."""
tool = APIGovernorTool()
assert tool.name == "api_governor"
assert "OpenAPI" in tool.description
assert "governance" in tool.description.lower()


def test_api_governor_tool_args_schema() -> None:
"""Test APIGovernorTool args schema."""
tool = APIGovernorTool()
schema = tool.args_schema.model_json_schema()
assert "spec_content" in schema["properties"]
assert "policy" in schema["properties"]
assert "output_format" in schema["properties"]


def test_api_governor_tool_missing_package() -> None:
"""Test APIGovernorTool when package is not installed."""
tool = APIGovernorTool()
# This will return an error message since api-governor is not installed
spec = """
openapi: "3.0.0"
info:
title: Test API
version: "1.0"
paths: {}
"""
result = tool._run(spec_content=spec)
# Should either work or return install instructions
assert isinstance(result, str)
Loading