chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,26 @@
|
||||
# SDK Documentation Generator
|
||||
|
||||
Generates MDX reference docs from Python source using griffe.
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
cd python
|
||||
uv run --with griffe python scripts/generate-docs.py
|
||||
```
|
||||
|
||||
## How it works
|
||||
|
||||
1. griffe extracts docstrings from `composio/**/*.py` → structured data
|
||||
2. `generate-docs.py` transforms data → MDX files
|
||||
3. Output written to `docs/content/reference/sdk-reference/python/`
|
||||
|
||||
## Configuration
|
||||
|
||||
- **Expected classes**: `EXPECTED_CLASSES` dict maps class name → Composio property
|
||||
- **Modules to search**: `CLASS_MODULES` list
|
||||
- **Decorators**: `DECORATORS_TO_DOCUMENT` list
|
||||
|
||||
## CI
|
||||
|
||||
`.github/workflows/generate-sdk-docs.yml` triggers on changes to `python/composio/**` and opens a PR with updated docs.
|
||||
@@ -0,0 +1,182 @@
|
||||
import typing as t
|
||||
from enum import Enum
|
||||
from itertools import chain
|
||||
from pathlib import Path
|
||||
|
||||
import click
|
||||
import tomli
|
||||
from semver import VersionInfo
|
||||
|
||||
CWD = Path.cwd()
|
||||
SKIPDIR = (".venv", "venv", ".nox", ".tox", "temp", "node_modules", "site-packages")
|
||||
PYPROJECT = "pyproject.toml"
|
||||
SETUP = "setup.py"
|
||||
|
||||
BumpTypes = {
|
||||
"M": "major",
|
||||
"m": "minor",
|
||||
"p": "patch",
|
||||
"b": "post",
|
||||
"c": "pre",
|
||||
"s": "skip",
|
||||
}
|
||||
BumpTypesPrompt = " | ".join([f"{k}-{v.title()}" for k, v in BumpTypes.items()])
|
||||
|
||||
|
||||
class BumpType(Enum):
|
||||
MAJOR = "major"
|
||||
MINOR = "minor"
|
||||
PATCH = "patch"
|
||||
PRE = "pre"
|
||||
POST = "post"
|
||||
|
||||
|
||||
def _get_bumped_version(current: VersionInfo, bump_type: BumpType) -> VersionInfo:
|
||||
if bump_type == BumpType.MAJOR:
|
||||
return current.next_version("major")
|
||||
|
||||
if bump_type == BumpType.MINOR:
|
||||
return current.next_version("minor")
|
||||
|
||||
if bump_type == BumpType.PATCH:
|
||||
return current.next_version("patch")
|
||||
|
||||
if bump_type == BumpType.PRE:
|
||||
return current.next_version("prerelease")
|
||||
|
||||
return current.bump_build(token="post")
|
||||
|
||||
|
||||
def _skip(file: Path) -> bool:
|
||||
return any(skipdir in file.parts for skipdir in SKIPDIR)
|
||||
|
||||
|
||||
def _find_packages():
|
||||
files = []
|
||||
for file in chain(CWD.glob(f"**/{PYPROJECT}"), CWD.glob(f"**/{SETUP}")):
|
||||
if _skip(file=file):
|
||||
continue
|
||||
files.append(file)
|
||||
return files
|
||||
|
||||
|
||||
def _get_version_update(
|
||||
package: str,
|
||||
version: str,
|
||||
bump_type_selector: t.Optional[str] = None,
|
||||
) -> t.Optional[str]:
|
||||
pre = None
|
||||
if "-" in version:
|
||||
version, pre = version.split("-", maxsplit=1)
|
||||
|
||||
if "rc" in version:
|
||||
version, marker, number = version.partition("rc")
|
||||
pre = f"{marker}{number}"
|
||||
|
||||
# TODO: Add support for post builds
|
||||
current_version = VersionInfo.parse(version=version)
|
||||
current_version._prerelease = pre
|
||||
bump_type_selector = t.cast(
|
||||
str,
|
||||
bump_type_selector
|
||||
or click.prompt(f"* Bump {package}@{version} -> {BumpTypesPrompt}"),
|
||||
)
|
||||
if bump_type_selector == "s":
|
||||
return None
|
||||
|
||||
try:
|
||||
_bump_type = BumpType(BumpTypes[bump_type_selector])
|
||||
except (ValueError, KeyError) as e:
|
||||
raise click.ClickException(f"Invalid bump type: {e}")
|
||||
|
||||
return str(_get_bumped_version(current=current_version, bump_type=_bump_type))
|
||||
|
||||
|
||||
def _bump_setup_py(file: Path, bump_type: t.Optional[str] = None):
|
||||
# Load config
|
||||
content = file.read_text(encoding="utf-8")
|
||||
_, _, arguments = content.partition("setup(")
|
||||
|
||||
package, version = None, None
|
||||
for line in arguments.split("\n"):
|
||||
if "=" not in line:
|
||||
continue
|
||||
|
||||
arg, val = line.strip().split("=", maxsplit=1)
|
||||
val = val.replace('"', "").replace(",", "").strip()
|
||||
if arg == "name":
|
||||
package = val
|
||||
|
||||
if arg == "version":
|
||||
version = val
|
||||
|
||||
# Skip the main setup.py
|
||||
if package == "composio":
|
||||
return
|
||||
|
||||
if package is None or version is None:
|
||||
raise ValueError(f"Package or version not found in {file}")
|
||||
|
||||
# Bump version
|
||||
next_version = _get_version_update(
|
||||
package=package,
|
||||
version=version,
|
||||
bump_type_selector=bump_type,
|
||||
)
|
||||
if next_version is None:
|
||||
click.echo(f"* Skipping {package}")
|
||||
return
|
||||
|
||||
click.echo(f"* Bumping {package} {version} -> {next_version}")
|
||||
content = content.replace(f'version="{version}"', f'version="{next_version}"')
|
||||
file.write_text(content, encoding="utf-8")
|
||||
|
||||
|
||||
def _bump_pyproject_toml(file: Path, bump_type: t.Optional[str] = None):
|
||||
# Load config
|
||||
content = file.read_text(encoding="utf-8")
|
||||
config = tomli.loads(content)
|
||||
project = t.cast(dict, config["project"])
|
||||
|
||||
# Extract meta
|
||||
package = project["name"]
|
||||
version = project["version"]
|
||||
|
||||
# Bump version
|
||||
next_version = _get_version_update(
|
||||
package=package,
|
||||
version=version,
|
||||
bump_type_selector=bump_type,
|
||||
)
|
||||
if next_version is None:
|
||||
click.echo(f"* Skipping {package}")
|
||||
return
|
||||
|
||||
click.echo(f"* Bumping {package} {version} -> {next_version}")
|
||||
content = content.replace(f'version = "{version}"', f'version = "{next_version}"')
|
||||
file.write_text(content, encoding="utf-8")
|
||||
|
||||
|
||||
def _bump_package(file: Path, bump_type: t.Optional[str] = None):
|
||||
if file.name == PYPROJECT:
|
||||
return _bump_pyproject_toml(file=file, bump_type=bump_type)
|
||||
return _bump_setup_py(file=file, bump_type=bump_type)
|
||||
|
||||
|
||||
@click.command("bump")
|
||||
@click.option("--pre", "bump_type", flag_value="c")
|
||||
@click.option("--post", "bump_type", flag_value="b")
|
||||
@click.option("--patch", "bump_type", flag_value="p")
|
||||
@click.option("--minor", "bump_type", flag_value="m")
|
||||
@click.option("--major", "bump_type", flag_value="M")
|
||||
def bump(bump_type: t.Optional[str]):
|
||||
packages = _find_packages()
|
||||
for package in packages:
|
||||
try:
|
||||
_bump_package(file=package, bump_type=bump_type)
|
||||
except Exception as e:
|
||||
raise e.__class__(f"Error bumping {package}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
bump()
|
||||
@@ -0,0 +1,538 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Check if provider name is provided
|
||||
if [ -z "$1" ]; then
|
||||
echo "Please provide a provider name"
|
||||
echo "Usage: ./scripts/create-provider.sh <provider-name> [--agentic] [--output-dir <directory>]"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
PROVIDER_NAME=$1
|
||||
shift
|
||||
|
||||
# Default output directory
|
||||
OUTPUT_DIR="python/providers"
|
||||
IS_AGENTIC=false
|
||||
|
||||
# Parse optional arguments
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--agentic)
|
||||
IS_AGENTIC=true
|
||||
shift
|
||||
;;
|
||||
--output-dir)
|
||||
OUTPUT_DIR="$2"
|
||||
shift 2
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option: $1"
|
||||
echo "Usage: ./scripts/create-provider.sh <provider-name> [--agentic] [--output-dir <directory>]"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
PROVIDER_PATH="$OUTPUT_DIR/$PROVIDER_NAME"
|
||||
# Convert to title case (e.g., myai -> Myai)
|
||||
TITLE_CASE_PROVIDER_NAME="$(tr '[:lower:]' '[:upper:]' <<< ${PROVIDER_NAME:0:1})${PROVIDER_NAME:1}"
|
||||
PACKAGE_NAME="composio_$PROVIDER_NAME"
|
||||
|
||||
# Check if provider directory already exists
|
||||
if [ -d "$PROVIDER_PATH" ]; then
|
||||
echo "❌ Provider '$PROVIDER_NAME' already exists in $PROVIDER_PATH"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Create directory structure
|
||||
mkdir -p "$PROVIDER_PATH/$PACKAGE_NAME"
|
||||
|
||||
# Create pyproject.toml
|
||||
cat > "$PROVIDER_PATH/pyproject.toml" << EOL
|
||||
[project]
|
||||
name = "$PACKAGE_NAME"
|
||||
version = "0.1.0"
|
||||
description = "${IS_AGENTIC:+Agentic }Provider for ${TITLE_CASE_PROVIDER_NAME} in the Composio SDK"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10,<4"
|
||||
authors = [
|
||||
{ name = "Composio", email = "tech@composio.dev" }
|
||||
]
|
||||
classifiers = [
|
||||
"Programming Language :: Python :: 3",
|
||||
"License :: OSI Approved :: Apache Software License",
|
||||
"Operating System :: OS Independent",
|
||||
]
|
||||
dependencies = [
|
||||
"composio",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
Homepage = "https://github.com/ComposioHQ/composio"
|
||||
EOL
|
||||
|
||||
# Create setup.py for backwards compatibility
|
||||
cat > "$PROVIDER_PATH/setup.py" << EOL
|
||||
from setuptools import setup, find_packages
|
||||
|
||||
setup(
|
||||
name="$PACKAGE_NAME",
|
||||
version="0.1.0",
|
||||
packages=find_packages(),
|
||||
install_requires=[
|
||||
"composio>=0.1.0",
|
||||
],
|
||||
python_requires=">=3.10,<4",
|
||||
author="Your Name",
|
||||
author_email="you@example.com",
|
||||
description="${IS_AGENTIC:+Agentic }Provider for ${TITLE_CASE_PROVIDER_NAME} in the Composio SDK",
|
||||
long_description=open("README.md").read(),
|
||||
long_description_content_type="text/markdown",
|
||||
url="https://github.com/composio/composio",
|
||||
classifiers=[
|
||||
"Development Status :: 3 - Alpha",
|
||||
"Intended Audience :: Developers",
|
||||
"License :: OSI Approved :: Apache Software License",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Programming Language :: Python :: 3.12",
|
||||
],
|
||||
)
|
||||
EOL
|
||||
|
||||
# Create __init__.py
|
||||
cat > "$PROVIDER_PATH/$PACKAGE_NAME/__init__.py" << EOL
|
||||
"""
|
||||
${TITLE_CASE_PROVIDER_NAME} Provider for composio SDK.
|
||||
"""
|
||||
|
||||
from .provider import ${TITLE_CASE_PROVIDER_NAME}Provider
|
||||
|
||||
__all__ = ["${TITLE_CASE_PROVIDER_NAME}Provider"]
|
||||
EOL
|
||||
|
||||
# Create provider.py
|
||||
if [ "$IS_AGENTIC" = true ]; then
|
||||
cat > "$PROVIDER_PATH/$PACKAGE_NAME/provider.py" << EOL
|
||||
"""
|
||||
${TITLE_CASE_PROVIDER_NAME} Provider implementation.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import typing as t
|
||||
|
||||
from composio.core.provider import AgenticProvider
|
||||
from composio.core.provider.agentic import AgenticProviderExecuteFn
|
||||
from composio.types import Tool
|
||||
|
||||
|
||||
# Define your tool format
|
||||
class ${TITLE_CASE_PROVIDER_NAME}Tool:
|
||||
"""${TITLE_CASE_PROVIDER_NAME} tool format"""
|
||||
def __init__(self, name: str, description: str, execute: t.Callable, schema: dict):
|
||||
self.name = name
|
||||
self.description = description
|
||||
self.execute = execute
|
||||
self.schema = schema
|
||||
|
||||
|
||||
class ${TITLE_CASE_PROVIDER_NAME}Provider(
|
||||
AgenticProvider[${TITLE_CASE_PROVIDER_NAME}Tool, t.List[${TITLE_CASE_PROVIDER_NAME}Tool]],
|
||||
name="${PROVIDER_NAME}"
|
||||
):
|
||||
"""
|
||||
Composio toolset for ${TITLE_CASE_PROVIDER_NAME} framework.
|
||||
"""
|
||||
|
||||
def wrap_tool(
|
||||
self,
|
||||
tool: Tool,
|
||||
execute_tool: AgenticProviderExecuteFn
|
||||
) -> ${TITLE_CASE_PROVIDER_NAME}Tool:
|
||||
"""Wrap a tool in the ${PROVIDER_NAME} format."""
|
||||
def execute_wrapper(**kwargs) -> t.Dict:
|
||||
result = execute_tool(tool.slug, kwargs)
|
||||
if not result.get("successful", False):
|
||||
raise Exception(result.get("error", "Tool execution failed"))
|
||||
return result.get("data", {})
|
||||
|
||||
return ${TITLE_CASE_PROVIDER_NAME}Tool(
|
||||
name=tool.slug,
|
||||
description=tool.description or "",
|
||||
execute=execute_wrapper,
|
||||
schema=tool.input_parameters
|
||||
)
|
||||
|
||||
def wrap_tools(
|
||||
self,
|
||||
tools: t.Sequence[Tool],
|
||||
execute_tool: AgenticProviderExecuteFn
|
||||
) -> t.List[${TITLE_CASE_PROVIDER_NAME}Tool]:
|
||||
"""
|
||||
Get composio tools wrapped as a list of ${TITLE_CASE_PROVIDER_NAME} tools.
|
||||
"""
|
||||
return [self.wrap_tool(tool, execute_tool) for tool in tools]
|
||||
EOL
|
||||
else
|
||||
cat > "$PROVIDER_PATH/$PACKAGE_NAME/provider.py" << EOL
|
||||
"""
|
||||
${TITLE_CASE_PROVIDER_NAME} Provider implementation.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import typing as t
|
||||
|
||||
from composio.core.provider import NonAgenticProvider
|
||||
from composio.types import Tool, Modifiers, ToolExecutionResponse
|
||||
|
||||
|
||||
# Define your tool format
|
||||
class ${TITLE_CASE_PROVIDER_NAME}Tool:
|
||||
"""${TITLE_CASE_PROVIDER_NAME} tool format"""
|
||||
def __init__(self, name: str, description: str, parameters: dict):
|
||||
self.name = name
|
||||
self.description = description
|
||||
self.parameters = parameters
|
||||
|
||||
|
||||
# Define your tool collection format
|
||||
${TITLE_CASE_PROVIDER_NAME}ToolCollection: t.TypeAlias = t.List[${TITLE_CASE_PROVIDER_NAME}Tool]
|
||||
|
||||
|
||||
class ${TITLE_CASE_PROVIDER_NAME}Provider(
|
||||
NonAgenticProvider[${TITLE_CASE_PROVIDER_NAME}Tool, ${TITLE_CASE_PROVIDER_NAME}ToolCollection],
|
||||
name="${PROVIDER_NAME}"
|
||||
):
|
||||
"""
|
||||
Composio toolset for ${TITLE_CASE_PROVIDER_NAME} platform.
|
||||
"""
|
||||
|
||||
def wrap_tool(self, tool: Tool) -> ${TITLE_CASE_PROVIDER_NAME}Tool:
|
||||
"""Transform a single tool to platform format"""
|
||||
return ${TITLE_CASE_PROVIDER_NAME}Tool(
|
||||
name=tool.slug,
|
||||
description=tool.description or "",
|
||||
parameters={
|
||||
"type": "object",
|
||||
"properties": tool.input_parameters.get("properties", {}),
|
||||
"required": tool.input_parameters.get("required", [])
|
||||
}
|
||||
)
|
||||
|
||||
def wrap_tools(self, tools: t.Sequence[Tool]) -> ${TITLE_CASE_PROVIDER_NAME}ToolCollection:
|
||||
"""Transform a collection of tools"""
|
||||
return [self.wrap_tool(tool) for tool in tools]
|
||||
|
||||
def execute_tool_call(
|
||||
self,
|
||||
user_id: str,
|
||||
tool_call: dict,
|
||||
modifiers: t.Optional[Modifiers] = None
|
||||
) -> ToolExecutionResponse:
|
||||
"""
|
||||
Execute a tool call.
|
||||
|
||||
:param user_id: User ID to use for executing function calls.
|
||||
:param tool_call: Tool call metadata containing 'name' and 'arguments'.
|
||||
:param modifiers: Modifiers to use for executing function calls.
|
||||
:return: Object containing output data from the tool call.
|
||||
"""
|
||||
return self.execute_tool(
|
||||
slug=tool_call["name"],
|
||||
arguments=tool_call["arguments"],
|
||||
modifiers=modifiers,
|
||||
user_id=user_id
|
||||
)
|
||||
|
||||
def handle_tool_calls(
|
||||
self,
|
||||
user_id: str,
|
||||
response: t.Union[dict, t.Any],
|
||||
modifiers: t.Optional[Modifiers] = None
|
||||
) -> t.List[ToolExecutionResponse]:
|
||||
"""
|
||||
Handle tool calls from ${TITLE_CASE_PROVIDER_NAME} response.
|
||||
|
||||
:param response: Response object from ${TITLE_CASE_PROVIDER_NAME}
|
||||
:param user_id: User ID to use for executing function calls.
|
||||
:param modifiers: Modifiers to use for executing function calls.
|
||||
:return: A list of output objects from the tool calls.
|
||||
"""
|
||||
# TODO: Implement based on ${TITLE_CASE_PROVIDER_NAME}'s response format
|
||||
# Example:
|
||||
# outputs = []
|
||||
# for tool_call in response.tool_calls:
|
||||
# outputs.append(
|
||||
# self.execute_tool_call(
|
||||
# user_id=user_id,
|
||||
# tool_call=tool_call,
|
||||
# modifiers=modifiers,
|
||||
# )
|
||||
# )
|
||||
# return outputs
|
||||
raise NotImplementedError("Tool call handling not implemented yet")
|
||||
EOL
|
||||
fi
|
||||
|
||||
# Create py.typed marker for PEP 561
|
||||
touch "$PROVIDER_PATH/$PACKAGE_NAME/py.typed"
|
||||
|
||||
# Create demo script
|
||||
if [ "$IS_AGENTIC" = true ]; then
|
||||
cat > "$PROVIDER_PATH/${PROVIDER_NAME}_demo.py" << EOL
|
||||
"""
|
||||
${TITLE_CASE_PROVIDER_NAME} demo.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
|
||||
# from ${PROVIDER_NAME}_framework import Agent, Runner # Import your agent framework
|
||||
from composio_${PROVIDER_NAME} import ${TITLE_CASE_PROVIDER_NAME}Provider
|
||||
|
||||
from composio import Composio
|
||||
|
||||
# Initialize Composio toolset
|
||||
composio = Composio(provider=${TITLE_CASE_PROVIDER_NAME}Provider())
|
||||
|
||||
# Get all the tools
|
||||
tools = composio.tools.get(
|
||||
user_id="default",
|
||||
tools=["GITHUB_STAR_A_REPOSITORY_FOR_THE_AUTHENTICATED_USER"],
|
||||
)
|
||||
|
||||
# TODO: Create an agent with the tools
|
||||
# agent = Agent(
|
||||
# name="GitHub Agent",
|
||||
# instructions="You are a helpful assistant that helps users with GitHub tasks.",
|
||||
# tools=tools,
|
||||
# )
|
||||
|
||||
|
||||
# Run the agent
|
||||
async def main():
|
||||
# TODO: Implement your agent execution logic
|
||||
# result = await Runner.run(
|
||||
# starting_agent=agent,
|
||||
# input=(
|
||||
# "Star the repository composiohq/composio on GitHub. If done "
|
||||
# "successfully, respond with 'Action executed successfully'"
|
||||
# ),
|
||||
# )
|
||||
# print(result.final_output)
|
||||
|
||||
print("Implement your ${TITLE_CASE_PROVIDER_NAME} agent logic here!")
|
||||
print(f"Got {len(tools)} tools:")
|
||||
for tool in tools:
|
||||
print(f" - {tool.name}: {tool.description}")
|
||||
|
||||
|
||||
asyncio.run(main())
|
||||
EOL
|
||||
else
|
||||
cat > "$PROVIDER_PATH/${PROVIDER_NAME}_demo.py" << EOL
|
||||
"""
|
||||
${TITLE_CASE_PROVIDER_NAME} demo.
|
||||
"""
|
||||
|
||||
from composio_${PROVIDER_NAME} import ${TITLE_CASE_PROVIDER_NAME}Provider
|
||||
# from ${PROVIDER_NAME} import ${TITLE_CASE_PROVIDER_NAME} # Import your AI client
|
||||
|
||||
from composio import Composio
|
||||
|
||||
# Initialize tools.
|
||||
# ${PROVIDER_NAME}_client = ${TITLE_CASE_PROVIDER_NAME}() # Initialize your AI client
|
||||
composio = Composio(provider=${TITLE_CASE_PROVIDER_NAME}Provider())
|
||||
|
||||
# Define task.
|
||||
task = "Star a repo composiohq/composio on GitHub"
|
||||
|
||||
# Get GitHub tools that are pre-configured
|
||||
tools = composio.tools.get(user_id="default", toolkits=["GITHUB"])
|
||||
|
||||
# TODO: Get response from the LLM
|
||||
# response = ${PROVIDER_NAME}_client.chat.completions.create(
|
||||
# model="your-model",
|
||||
# tools=tools,
|
||||
# messages=[
|
||||
# {"role": "system", "content": "You are a helpful assistant."},
|
||||
# {"role": "user", "content": task},
|
||||
# ],
|
||||
# )
|
||||
# print(response)
|
||||
|
||||
# TODO: Execute the function calls.
|
||||
# result = composio.provider.handle_tool_calls(response=response, user_id="default")
|
||||
# print(result)
|
||||
|
||||
print(f"Got {len(tools)} tools for the task: {task}")
|
||||
for tool in tools:
|
||||
print(f" - {tool.name}: {tool.description}")
|
||||
print("\nImplement your ${TITLE_CASE_PROVIDER_NAME} integration here!")
|
||||
EOL
|
||||
fi
|
||||
|
||||
# Create README.md
|
||||
cat > "$PROVIDER_PATH/README.md" << EOL
|
||||
# $PACKAGE_NAME
|
||||
|
||||
${IS_AGENTIC:+Agentic }Provider for ${TITLE_CASE_PROVIDER_NAME} in the Composio SDK.
|
||||
|
||||
## Features
|
||||
|
||||
${IS_AGENTIC:+- **Full Tool Execution**: Execute tools with proper parameter handling
|
||||
- **Agent Support**: Create agents with wrapped tools
|
||||
- **Type Safety**: Full type annotations for better IDE support}${!IS_AGENTIC:+- **Tool Transformation**: Convert Composio tools to ${TITLE_CASE_PROVIDER_NAME} format
|
||||
- **Tool Execution**: Execute tools with proper parameter handling
|
||||
- **Type Safety**: Full type annotations for better IDE support}
|
||||
|
||||
## Installation
|
||||
|
||||
\`\`\`bash
|
||||
pip install $PACKAGE_NAME
|
||||
# or
|
||||
uv add $PACKAGE_NAME
|
||||
\`\`\`
|
||||
|
||||
## Quick Start
|
||||
|
||||
\`\`\`python
|
||||
from composio import Composio
|
||||
from composio_${PROVIDER_NAME} import ${TITLE_CASE_PROVIDER_NAME}Provider
|
||||
|
||||
# Initialize Composio with ${TITLE_CASE_PROVIDER_NAME} provider
|
||||
composio = Composio(
|
||||
api_key="your-composio-api-key",
|
||||
provider=${TITLE_CASE_PROVIDER_NAME}Provider()
|
||||
)
|
||||
|
||||
# Get available tools
|
||||
tools = composio.tools.get(
|
||||
user_id="default",
|
||||
toolkits=["github", "gmail"]
|
||||
)
|
||||
|
||||
# Use tools with ${TITLE_CASE_PROVIDER_NAME}
|
||||
# TODO: Add your usage example here
|
||||
\`\`\`
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Basic Example
|
||||
|
||||
\`\`\`python
|
||||
from composio import Composio
|
||||
from composio_${PROVIDER_NAME} import ${TITLE_CASE_PROVIDER_NAME}Provider
|
||||
|
||||
# Initialize provider
|
||||
provider = ${TITLE_CASE_PROVIDER_NAME}Provider()
|
||||
|
||||
# Initialize Composio
|
||||
composio = Composio(
|
||||
api_key="your-api-key",
|
||||
provider=provider
|
||||
)
|
||||
|
||||
# Get tools
|
||||
tools = composio.tools.get(
|
||||
user_id="default",
|
||||
toolkits=["github"]
|
||||
)
|
||||
|
||||
${IS_AGENTIC:+# Use the tools with your agent framework
|
||||
# TODO: Implement your agent logic here
|
||||
for tool in tools:
|
||||
print(f"Tool: {tool.name} - {tool.description}")}${!IS_AGENTIC:+# TODO: Get response from your AI platform (example)
|
||||
# response = ${PROVIDER_NAME}_client.chat.completions.create(
|
||||
# model="your-model",
|
||||
# tools=tools,
|
||||
# messages=[
|
||||
# {"role": "system", "content": "You are a helpful assistant."},
|
||||
# {"role": "user", "content": "Star the composiohq/composio repository"},
|
||||
# ],
|
||||
# )
|
||||
#
|
||||
# # Execute the function calls
|
||||
# result = composio.provider.handle_tool_calls(response=response, user_id="default")
|
||||
# print(result)}
|
||||
\`\`\`
|
||||
|
||||
## API Reference
|
||||
|
||||
### ${TITLE_CASE_PROVIDER_NAME}Provider Class
|
||||
|
||||
The \`${TITLE_CASE_PROVIDER_NAME}Provider\` class extends \`${IS_AGENTIC:+AgenticProvider}${!IS_AGENTIC:+NonAgenticProvider}\` and provides ${PROVIDER_NAME}-specific functionality.
|
||||
|
||||
#### Methods
|
||||
|
||||
##### \`wrap_tool(tool: Tool${IS_AGENTIC:+, execute_tool: AgenticProviderExecuteFn}) -> ${TITLE_CASE_PROVIDER_NAME}Tool\`
|
||||
|
||||
Wraps a tool in the ${PROVIDER_NAME} format.
|
||||
|
||||
\`\`\`python
|
||||
tool = provider.wrap_tool(composio_tool${IS_AGENTIC:+, execute_tool})
|
||||
\`\`\`
|
||||
|
||||
##### \`wrap_tools(tools: Sequence[Tool]${IS_AGENTIC:+, execute_tool: AgenticProviderExecuteFn}) -> ${IS_AGENTIC:+${TITLE_CASE_PROVIDER_NAME}Toolkit}${!IS_AGENTIC:+${TITLE_CASE_PROVIDER_NAME}ToolCollection}\`
|
||||
|
||||
Wraps multiple tools in the ${PROVIDER_NAME} format.
|
||||
|
||||
\`\`\`python
|
||||
tools = provider.wrap_tools(composio_tools${IS_AGENTIC:+, execute_tool})
|
||||
\`\`\`
|
||||
|
||||
${!IS_AGENTIC:+##### \`execute_tool_call(user_id: str, tool_call: dict, modifiers: Optional[Modifiers] = None) -> ToolExecutionResponse\`
|
||||
|
||||
Executes a tool call.
|
||||
|
||||
\`\`\`python
|
||||
result = provider.execute_tool_call(
|
||||
user_id="default",
|
||||
tool_call={"name": "TOOL_NAME", "arguments": {...}}
|
||||
)
|
||||
\`\`\`}
|
||||
|
||||
## Development
|
||||
|
||||
1. Clone the repository
|
||||
2. Install dependencies: \`uv sync\`
|
||||
3. Make your changes
|
||||
4. Run tests: \`pytest\`
|
||||
5. Format code: \`ruff format\`
|
||||
6. Lint code: \`ruff check\`
|
||||
|
||||
## Contributing
|
||||
|
||||
We welcome contributions! Please see our [Contributing Guide](../../CONTRIBUTING.md) for more details.
|
||||
|
||||
## License
|
||||
|
||||
Apache License 2.0
|
||||
|
||||
## Support
|
||||
|
||||
For support, please visit our [Documentation](https://docs.composio.dev) or join our [Discord Community](https://discord.gg/composio).
|
||||
EOL
|
||||
|
||||
# Make the script executable
|
||||
chmod +x "$PROVIDER_PATH/${PROVIDER_NAME}_demo.py"
|
||||
|
||||
echo "✨ Created new ${IS_AGENTIC:+agentic }provider at $PROVIDER_PATH"
|
||||
echo "✨ Provider structure:"
|
||||
echo " 📁 $PROVIDER_PATH/"
|
||||
echo " ├── 📄 README.md"
|
||||
echo " ├── 📄 pyproject.toml"
|
||||
echo " ├── 📄 setup.py"
|
||||
echo " ├── 📄 ${PROVIDER_NAME}_demo.py"
|
||||
echo " └── 📁 $PACKAGE_NAME/"
|
||||
echo " ├── 📄 __init__.py"
|
||||
echo " ├── 📄 provider.py"
|
||||
echo " └── 📄 py.typed"
|
||||
echo ""
|
||||
echo "🚀 Next steps:"
|
||||
echo " 1. cd $PROVIDER_PATH"
|
||||
echo " 2. Update provider.py with your implementation"
|
||||
echo " 3. Install in development mode: uv pip install -e ."
|
||||
echo " 4. Test your provider: python ${PROVIDER_NAME}_demo.py"
|
||||
@@ -0,0 +1,711 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Python SDK Documentation Generator
|
||||
|
||||
Generates MDX documentation from Python source code using griffe.
|
||||
Output is written to the docs content directory.
|
||||
|
||||
Run: cd python && uv run --with griffe python scripts/generate-docs.py
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
import shutil
|
||||
import textwrap
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import typing as t
|
||||
|
||||
if t.TYPE_CHECKING:
|
||||
import griffe as griffe_t
|
||||
|
||||
_griffe: Any | None = None
|
||||
|
||||
|
||||
def load_griffe() -> Any:
|
||||
"""Load griffe lazily so parser helpers can be unit-tested without it."""
|
||||
global _griffe
|
||||
if _griffe is None:
|
||||
try:
|
||||
import griffe as griffe_module
|
||||
except ImportError:
|
||||
print("Error: griffe not installed. Run: pip install griffe")
|
||||
raise SystemExit(1)
|
||||
_griffe = griffe_module
|
||||
return _griffe
|
||||
|
||||
|
||||
# Paths
|
||||
SCRIPT_DIR = Path(__file__).parent
|
||||
PACKAGE_DIR = SCRIPT_DIR.parent
|
||||
OUTPUT_DIR = (
|
||||
PACKAGE_DIR.parent / "docs" / "content" / "reference" / "sdk-reference" / "python"
|
||||
)
|
||||
|
||||
# GitHub base URL for source links
|
||||
GITHUB_BASE = "https://github.com/composiohq/composio/blob/next/python"
|
||||
|
||||
# Decorators to document
|
||||
DECORATORS_TO_DOCUMENT = [
|
||||
("before_execute", "composio.core.models._modifiers"),
|
||||
("after_execute", "composio.core.models._modifiers"),
|
||||
("before_file_upload", "composio.core.models._modifiers"),
|
||||
("schema_modifier", "composio.core.models._modifiers"),
|
||||
]
|
||||
|
||||
# Classes to skip (internal/helper classes)
|
||||
SKIP_CLASSES = {"WithLogger", "SDKConfig", "TProvider"}
|
||||
|
||||
# Expected classes to document (maps class name -> property name on Composio)
|
||||
EXPECTED_CLASSES = {
|
||||
"Tools": "tools",
|
||||
"Toolkits": "toolkits",
|
||||
"Triggers": "triggers",
|
||||
"ConnectedAccounts": "connected_accounts",
|
||||
"AuthConfigs": "auth_configs",
|
||||
"MCP": "mcp",
|
||||
}
|
||||
|
||||
# Additional public-facing classes worth documenting even though they are not
|
||||
# exposed as direct properties on ``Composio``.
|
||||
#
|
||||
# ``SessionContextImpl`` is intentionally omitted: it is an internal
|
||||
# implementation detail handed to custom-tool execute functions as a
|
||||
# ``SessionContext`` instance, never referenced directly, so it must not
|
||||
# appear in the public SDK reference.
|
||||
ADDITIONAL_CLASSES = {
|
||||
"ToolRouterSession": "core.models.tool_router_session",
|
||||
}
|
||||
|
||||
# Modules to search for classes
|
||||
CLASS_MODULES = [
|
||||
"core.models.tools",
|
||||
"core.models.toolkits",
|
||||
"core.models.triggers",
|
||||
"core.models.connected_accounts",
|
||||
"core.models.auth_configs",
|
||||
"core.models.mcp",
|
||||
]
|
||||
|
||||
# Pure-docs presentation overrides. The public class name ``ToolRouterSession``
|
||||
# is kept in source (renaming is a breaking change), but the SDK reference
|
||||
# presents the session object under the canonical "Session" naming.
|
||||
DISPLAY_NAME_OVERRIDES = {
|
||||
"ToolRouterSession": "Session",
|
||||
}
|
||||
|
||||
SLUG_OVERRIDES = {
|
||||
"ToolRouterSession": "session",
|
||||
}
|
||||
|
||||
|
||||
def to_kebab_case(name: str) -> str:
|
||||
"""Convert PascalCase to kebab-case."""
|
||||
s1 = re.sub("(.)([A-Z][a-z]+)", r"\1-\2", name)
|
||||
return re.sub("([a-z0-9])([A-Z])", r"\1-\2", s1).lower()
|
||||
|
||||
|
||||
def display_name_for(class_name: str) -> str:
|
||||
"""Resolve the display title for a class (falls back to the class name)."""
|
||||
return DISPLAY_NAME_OVERRIDES.get(class_name, class_name)
|
||||
|
||||
|
||||
def slug_for(class_name: str) -> str:
|
||||
"""Resolve the URL slug / filename stem (falls back to kebab-case)."""
|
||||
return SLUG_OVERRIDES.get(class_name, to_kebab_case(class_name))
|
||||
|
||||
|
||||
def escape_yaml_string(s: str) -> str:
|
||||
"""Escape a string for YAML frontmatter."""
|
||||
return json.dumps(s)
|
||||
|
||||
|
||||
def get_source_link(obj: griffe_t.Object) -> str | None:
|
||||
"""Get GitHub source link for an object."""
|
||||
if not hasattr(obj, "filepath") or not obj.filepath:
|
||||
return None
|
||||
try:
|
||||
raw_filepath = obj.filepath
|
||||
# Handle case where filepath might be a list (griffe edge case)
|
||||
if isinstance(raw_filepath, list):
|
||||
resolved_path: Path | None = raw_filepath[0] if raw_filepath else None
|
||||
else:
|
||||
resolved_path = raw_filepath
|
||||
if not resolved_path:
|
||||
return None
|
||||
rel_path = resolved_path.relative_to(PACKAGE_DIR)
|
||||
except ValueError:
|
||||
return None
|
||||
line = obj.lineno if hasattr(obj, "lineno") and obj.lineno else 1
|
||||
return f"{GITHUB_BASE}/{rel_path}#L{line}"
|
||||
|
||||
|
||||
def format_type(annotation: Any) -> str:
|
||||
"""Format a type annotation to readable string."""
|
||||
if annotation is None:
|
||||
return "Any"
|
||||
|
||||
type_str = str(annotation)
|
||||
# Clean up common prefixes
|
||||
type_str = type_str.replace("typing.", "").replace("typing_extensions.", "")
|
||||
type_str = type_str.replace("composio.client.types.", "")
|
||||
type_str = re.sub(r"\bt\.", "", type_str)
|
||||
type_str = re.sub(r"\bte\.", "", type_str) # typing_extensions alias
|
||||
type_str = re.sub(r"Optional\[([^\]]+)\]", r"\1 | None", type_str)
|
||||
# Clean up Unpack to just show the type
|
||||
type_str = re.sub(r"Unpack\[([^\]]+)\]", r"\1", type_str)
|
||||
|
||||
# Truncate very long types
|
||||
if len(type_str) > 60:
|
||||
return type_str[:57] + "..."
|
||||
return type_str
|
||||
|
||||
|
||||
def parse_docstring(docstring: str | None) -> dict[str, Any]:
|
||||
"""Parse docstring into components."""
|
||||
if not docstring:
|
||||
return {
|
||||
"description": "",
|
||||
"params": {},
|
||||
"returns": None,
|
||||
"examples": [],
|
||||
"deprecated": None,
|
||||
}
|
||||
|
||||
lines = docstring.strip().split("\n")
|
||||
description_lines = []
|
||||
params: dict[str, str] = {}
|
||||
returns = None
|
||||
examples: list[str] = []
|
||||
deprecated_lines: list[str] = []
|
||||
|
||||
section = "description"
|
||||
current_param = None
|
||||
example_lines: list[str] = []
|
||||
|
||||
for line in lines:
|
||||
stripped = line.strip()
|
||||
|
||||
# Check for a ``.. deprecated::`` directive (reStructuredText).
|
||||
if re.match(r"\.\.\s+deprecated::", stripped):
|
||||
section = "deprecated"
|
||||
rest = re.sub(r"\.\.\s+deprecated::\s*", "", stripped).strip()
|
||||
if rest:
|
||||
deprecated_lines.append(rest)
|
||||
continue
|
||||
|
||||
# Check for :param name: description
|
||||
param_match = re.match(r":param\s+(\w+):\s*(.*)", stripped)
|
||||
if param_match:
|
||||
section = "params"
|
||||
current_param = param_match.group(1)
|
||||
params[current_param] = param_match.group(2)
|
||||
continue
|
||||
|
||||
# Check for :returns: or :return:
|
||||
return_match = re.match(r":returns?:\s*(.*)", stripped)
|
||||
if return_match:
|
||||
section = "returns"
|
||||
returns = return_match.group(1)
|
||||
continue
|
||||
|
||||
# Check for Example section
|
||||
if stripped.lower().startswith("example"):
|
||||
section = "examples"
|
||||
continue
|
||||
|
||||
# Add to appropriate section
|
||||
if section == "description":
|
||||
description_lines.append(stripped)
|
||||
elif section == "params" and current_param and stripped:
|
||||
params[current_param] += " " + stripped
|
||||
elif section == "returns" and returns and stripped:
|
||||
returns += " " + stripped
|
||||
elif section == "examples":
|
||||
example_lines.append(line)
|
||||
elif section == "deprecated" and stripped:
|
||||
deprecated_lines.append(stripped)
|
||||
|
||||
if example_lines:
|
||||
examples.append(normalize_example("\n".join(example_lines)))
|
||||
|
||||
return {
|
||||
"description": " ".join(description_lines).strip(),
|
||||
"params": params,
|
||||
"returns": returns,
|
||||
"examples": examples,
|
||||
"deprecated": " ".join(deprecated_lines).strip() if deprecated_lines else None,
|
||||
}
|
||||
|
||||
|
||||
def normalize_example(example: str) -> str:
|
||||
"""Normalize a docstring example before wrapping it in an MDX code fence."""
|
||||
normalized = textwrap.dedent(example).strip()
|
||||
lines = normalized.splitlines()
|
||||
|
||||
if len(lines) >= 2 and lines[0].strip().startswith("```"):
|
||||
closing_index = len(lines) - 1
|
||||
if lines[closing_index].strip() == "```":
|
||||
normalized = "\n".join(lines[1:closing_index]).strip()
|
||||
|
||||
return normalized
|
||||
|
||||
|
||||
def extract_class_info(
|
||||
cls: griffe_t.Class, class_name: str, config: dict
|
||||
) -> dict[str, Any]:
|
||||
"""Extract documentation from a class."""
|
||||
griffe_module = load_griffe()
|
||||
doc = parse_docstring(cls.docstring.value if cls.docstring else None)
|
||||
|
||||
info = {
|
||||
"name": class_name,
|
||||
"access": config.get("access"),
|
||||
"source_link": get_source_link(cls),
|
||||
"description": doc["description"],
|
||||
"deprecated": doc.get("deprecated"),
|
||||
"properties": [],
|
||||
"methods": [],
|
||||
}
|
||||
|
||||
# Extract properties/attributes
|
||||
for name, member in cls.members.items():
|
||||
if name.startswith("_"):
|
||||
continue
|
||||
if isinstance(member, griffe_module.Attribute):
|
||||
attr_doc = member.docstring.value if member.docstring else ""
|
||||
info["properties"].append(
|
||||
{
|
||||
"name": name,
|
||||
"type": format_type(member.annotation),
|
||||
"description": attr_doc.strip() if attr_doc else "",
|
||||
}
|
||||
)
|
||||
|
||||
# Extract methods
|
||||
for name, member in cls.members.items():
|
||||
if name.startswith("_"):
|
||||
continue
|
||||
if isinstance(member, griffe_module.Function):
|
||||
method_doc = parse_docstring(
|
||||
member.docstring.value if member.docstring else None
|
||||
)
|
||||
|
||||
params = []
|
||||
for p in member.parameters:
|
||||
if p.name in ("self", "cls"):
|
||||
continue
|
||||
params.append(
|
||||
{
|
||||
"name": p.name,
|
||||
"type": format_type(p.annotation),
|
||||
"optional": p.default is not None,
|
||||
"description": method_doc["params"].get(p.name, ""),
|
||||
}
|
||||
)
|
||||
|
||||
info["methods"].append(
|
||||
{
|
||||
"name": name,
|
||||
"source_link": get_source_link(member),
|
||||
"description": method_doc["description"],
|
||||
"parameters": params,
|
||||
"return_type": format_type(member.returns),
|
||||
"return_description": method_doc["returns"],
|
||||
"examples": method_doc["examples"],
|
||||
}
|
||||
)
|
||||
|
||||
return info
|
||||
|
||||
|
||||
def generate_class_mdx(
|
||||
info: dict[str, Any], prop_to_class: dict[str, str] | None = None
|
||||
) -> str:
|
||||
"""Generate MDX for a class."""
|
||||
lines = []
|
||||
|
||||
# Frontmatter
|
||||
desc = (
|
||||
info["description"].split("\n")[0]
|
||||
if info["description"]
|
||||
else f"{info['name']} class"
|
||||
)
|
||||
# Normalize reStructuredText ``double backticks`` to single backticks so the
|
||||
# frontmatter description reads cleanly.
|
||||
desc = re.sub(r"``([^`]+)``", r"`\1`", desc)
|
||||
if len(desc) > 150:
|
||||
desc = desc[:147] + "..."
|
||||
|
||||
lines.append("---")
|
||||
lines.append(f"title: {escape_yaml_string(display_name_for(info['name']))}")
|
||||
lines.append(f"description: {escape_yaml_string(desc)}")
|
||||
lines.append("---")
|
||||
lines.append("")
|
||||
|
||||
# Class-level deprecation callout (rendered as a fumadocs warning callout).
|
||||
deprecated_note = info.get("deprecated")
|
||||
if deprecated_note:
|
||||
# Normalize reStructuredText ``double backticks`` to MDX `single` so
|
||||
# inline code renders correctly.
|
||||
deprecated_note = re.sub(r"``([^`]+)``", r"`\1`", deprecated_note)
|
||||
lines.append('<Callout type="warn" title="Deprecated">')
|
||||
lines.append(deprecated_note)
|
||||
lines.append("</Callout>")
|
||||
lines.append("")
|
||||
|
||||
source_link = info.get("source_link", "")
|
||||
|
||||
# Properties
|
||||
property_rows = []
|
||||
if info["properties"]:
|
||||
if prop_to_class:
|
||||
for prop in info["properties"]:
|
||||
prop_name = prop["name"]
|
||||
if prop_name in prop_to_class:
|
||||
class_name = prop_to_class[prop_name]
|
||||
link = (
|
||||
f"/reference/sdk-reference/python/{to_kebab_case(class_name)}"
|
||||
)
|
||||
property_rows.append(
|
||||
{
|
||||
"name": f"[`{prop_name}`]({link})",
|
||||
"type": f"`{class_name}`",
|
||||
"description": prop["description"],
|
||||
}
|
||||
)
|
||||
elif info["name"] in ADDITIONAL_CLASSES:
|
||||
for prop in info["properties"]:
|
||||
property_rows.append(
|
||||
{
|
||||
"name": f"`{prop['name']}`",
|
||||
"type": f"`{prop['type']}`",
|
||||
"description": prop["description"],
|
||||
}
|
||||
)
|
||||
|
||||
if property_rows:
|
||||
include_descriptions = any(row["description"] for row in property_rows)
|
||||
lines.append("## Properties")
|
||||
lines.append("")
|
||||
if include_descriptions:
|
||||
lines.append("| Name | Type | Description |")
|
||||
lines.append("|------|------|-------------|")
|
||||
for row in property_rows:
|
||||
lines.append(
|
||||
f"| {row['name']} | {row['type']} | {row['description']} |"
|
||||
)
|
||||
else:
|
||||
lines.append("| Name | Type |")
|
||||
lines.append("|------|------|")
|
||||
for row in property_rows:
|
||||
lines.append(f"| {row['name']} | {row['type']} |")
|
||||
lines.append("")
|
||||
|
||||
# Methods
|
||||
if info["methods"]:
|
||||
lines.append("## Methods")
|
||||
lines.append("")
|
||||
|
||||
for method in info["methods"]:
|
||||
lines.append(f"### {method['name']}()")
|
||||
lines.append("")
|
||||
|
||||
if method["description"]:
|
||||
lines.append(method["description"])
|
||||
lines.append("")
|
||||
|
||||
# Signature
|
||||
params_str = ", ".join(
|
||||
f"{p['name']}: {p['type']}" + (" = ..." if p["optional"] else "")
|
||||
for p in method["parameters"]
|
||||
)
|
||||
ret_type = method["return_type"] if method["return_type"] != "Any" else ""
|
||||
sig = f"def {method['name']}({params_str})"
|
||||
if ret_type:
|
||||
sig += f" -> {ret_type}"
|
||||
|
||||
lines.append("```python")
|
||||
lines.append(sig)
|
||||
lines.append("```")
|
||||
lines.append("")
|
||||
|
||||
# Parameters
|
||||
if method["parameters"]:
|
||||
lines.append("**Parameters**")
|
||||
lines.append("")
|
||||
lines.append("| Name | Type |")
|
||||
lines.append("|------|------|")
|
||||
for p in method["parameters"]:
|
||||
opt = "?" if p["optional"] else ""
|
||||
safe_type = p["type"].replace("|", "\\|")
|
||||
lines.append(f"| `{p['name']}{opt}` | `{safe_type}` |")
|
||||
lines.append("")
|
||||
|
||||
# Returns
|
||||
if method["return_type"] and method["return_type"] not in ("None", "Any"):
|
||||
ret_desc = method["return_description"] or ""
|
||||
lines.append("**Returns**")
|
||||
lines.append("")
|
||||
if ret_desc:
|
||||
lines.append(f"`{method['return_type']}` — {ret_desc}")
|
||||
else:
|
||||
lines.append(f"`{method['return_type']}`")
|
||||
lines.append("")
|
||||
|
||||
# Examples
|
||||
if method["examples"]:
|
||||
lines.append("**Example**")
|
||||
lines.append("")
|
||||
for ex in method["examples"]:
|
||||
lines.append("```python")
|
||||
lines.append(ex)
|
||||
lines.append("```")
|
||||
lines.append("")
|
||||
|
||||
lines.append("---")
|
||||
lines.append("")
|
||||
|
||||
# Source link at bottom
|
||||
if source_link:
|
||||
lines.append(f"[View source]({source_link})")
|
||||
lines.append("")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def generate_index_mdx(classes: list[dict], decorators: list[dict]) -> str:
|
||||
"""Generate index page."""
|
||||
|
||||
# Classes table
|
||||
class_rows = []
|
||||
for c in classes:
|
||||
link = f"/reference/sdk-reference/python/{slug_for(c['name'])}"
|
||||
desc = (
|
||||
c["description"][:80] + "..."
|
||||
if len(c["description"]) > 80
|
||||
else c["description"]
|
||||
)
|
||||
class_rows.append(f"| [`{display_name_for(c['name'])}`]({link}) | {desc} |")
|
||||
|
||||
# Decorators section
|
||||
dec_section = ""
|
||||
if decorators:
|
||||
dec_lines = ["## Decorators", ""]
|
||||
for d in decorators:
|
||||
dec_lines.append(f"### {d['name']}")
|
||||
dec_lines.append("")
|
||||
if d["description"]:
|
||||
dec_lines.append(d["description"])
|
||||
dec_lines.append("")
|
||||
if d["source_link"]:
|
||||
dec_lines.append(f"[View source]({d['source_link']})")
|
||||
dec_lines.append("")
|
||||
# Signature
|
||||
params = ", ".join(
|
||||
f"{p['name']}: {p['type']}" + (" = ..." if p["optional"] else "")
|
||||
for p in d["parameters"]
|
||||
)
|
||||
dec_lines.append("```python")
|
||||
dec_lines.append(f"@{d['name']}({params})")
|
||||
dec_lines.append("def my_modifier(...):")
|
||||
dec_lines.append(" ...")
|
||||
dec_lines.append("```")
|
||||
dec_lines.append("")
|
||||
dec_section = "\n".join(dec_lines)
|
||||
|
||||
return f"""---
|
||||
title: {escape_yaml_string("Python SDK Reference")}
|
||||
description: {escape_yaml_string("API reference for the Composio Python SDK")}
|
||||
---
|
||||
|
||||
# Python SDK Reference
|
||||
|
||||
Complete API reference for the `composio` Python package.
|
||||
|
||||
## Installation
|
||||
|
||||
<PackageInstall ecosystem="python" packages="composio" />
|
||||
|
||||
## Classes
|
||||
|
||||
| Class | Description |
|
||||
|-------|-------------|
|
||||
{chr(10).join(class_rows)}
|
||||
|
||||
## Quick Start
|
||||
|
||||
```python
|
||||
from composio import Composio
|
||||
|
||||
composio = Composio(api_key="your-api-key")
|
||||
|
||||
# Get tools for a user
|
||||
tools = composio.tools.get("user-123", toolkits=["github"])
|
||||
|
||||
# Execute a tool
|
||||
result = composio.tools.execute(
|
||||
"GITHUB_GET_REPOS",
|
||||
arguments={{"owner": "composio"}},
|
||||
user_id="user-123"
|
||||
)
|
||||
```
|
||||
|
||||
{dec_section}
|
||||
"""
|
||||
|
||||
|
||||
def main():
|
||||
griffe_module = load_griffe()
|
||||
|
||||
print("Starting Python SDK documentation generation...\n")
|
||||
print(f"Output: {OUTPUT_DIR}\n")
|
||||
|
||||
# Clean output
|
||||
if OUTPUT_DIR.exists():
|
||||
shutil.rmtree(OUTPUT_DIR)
|
||||
OUTPUT_DIR.mkdir(parents=True)
|
||||
|
||||
# Load package
|
||||
print("Loading composio package...")
|
||||
try:
|
||||
package = griffe_module.load("composio", search_paths=[str(PACKAGE_DIR)])
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
raise SystemExit(1)
|
||||
|
||||
# Find Composio class
|
||||
composio_cls = package.members["sdk"].members["Composio"]
|
||||
print(" Found Composio class")
|
||||
|
||||
# Discover classes from EXPECTED_CLASSES
|
||||
classes_to_doc = {"Composio": {"cls": composio_cls, "access": None, "prop": None}}
|
||||
prop_to_class = {} # Maps property name -> class name
|
||||
|
||||
for module_name in CLASS_MODULES:
|
||||
try:
|
||||
parts = module_name.split(".")
|
||||
current = package
|
||||
for part in parts:
|
||||
current = current.members[part]
|
||||
|
||||
# Find expected classes in this module
|
||||
for class_name, prop_name in EXPECTED_CLASSES.items():
|
||||
if class_name in current.members:
|
||||
cls = current.members[class_name]
|
||||
if isinstance(cls, griffe_module.Class):
|
||||
classes_to_doc[class_name] = {
|
||||
"cls": cls,
|
||||
"access": f"composio.{prop_name}",
|
||||
"prop": prop_name,
|
||||
}
|
||||
prop_to_class[prop_name] = class_name
|
||||
print(f" Found {class_name} (via composio.{prop_name})")
|
||||
except (KeyError, AttributeError):
|
||||
continue
|
||||
|
||||
# Add standalone public-facing classes that are not surfaced via a direct
|
||||
# ``composio.<property>`` access pattern.
|
||||
for class_name, module_name in ADDITIONAL_CLASSES.items():
|
||||
try:
|
||||
parts = module_name.split(".")
|
||||
current = package
|
||||
for part in parts:
|
||||
current = current.members[part]
|
||||
|
||||
if class_name in current.members:
|
||||
cls = current.members[class_name]
|
||||
if isinstance(cls, griffe_module.Class):
|
||||
classes_to_doc[class_name] = {
|
||||
"cls": cls,
|
||||
"access": None,
|
||||
"prop": None,
|
||||
}
|
||||
print(f" Found {class_name}")
|
||||
except (KeyError, AttributeError):
|
||||
continue
|
||||
|
||||
# Generate docs for each class
|
||||
documented_classes = []
|
||||
|
||||
for class_name, config in classes_to_doc.items():
|
||||
cls = config["cls"]
|
||||
print(f" Processing {class_name}...")
|
||||
|
||||
info = extract_class_info(cls, class_name, {"access": config["access"]})
|
||||
|
||||
# Only Composio gets prop_to_class for linking
|
||||
if class_name == "Composio":
|
||||
mdx = generate_class_mdx(info, prop_to_class)
|
||||
else:
|
||||
mdx = generate_class_mdx(info, None)
|
||||
|
||||
file_path = OUTPUT_DIR / f"{slug_for(class_name)}.mdx"
|
||||
file_path.write_text(mdx)
|
||||
|
||||
documented_classes.append(
|
||||
{
|
||||
"name": class_name,
|
||||
"description": info["description"] or f"{class_name} class",
|
||||
}
|
||||
)
|
||||
|
||||
# Process decorators
|
||||
decorators = []
|
||||
for dec_name, dec_module in DECORATORS_TO_DOCUMENT:
|
||||
module_path = dec_module.split(".")
|
||||
current = package
|
||||
|
||||
for part in module_path[1:]:
|
||||
if part in current.members:
|
||||
current = current.members[part]
|
||||
|
||||
if dec_name in current.members:
|
||||
func = current.members[dec_name]
|
||||
if isinstance(func, griffe_module.Function):
|
||||
print(f" Processing decorator {dec_name}...")
|
||||
doc = parse_docstring(func.docstring.value if func.docstring else None)
|
||||
|
||||
params = []
|
||||
for p in func.parameters:
|
||||
if p.name in ("self", "cls"):
|
||||
continue
|
||||
params.append(
|
||||
{
|
||||
"name": p.name,
|
||||
"type": format_type(p.annotation),
|
||||
"optional": p.default is not None,
|
||||
"description": doc["params"].get(p.name, ""),
|
||||
}
|
||||
)
|
||||
|
||||
decorators.append(
|
||||
{
|
||||
"name": dec_name,
|
||||
"source_link": get_source_link(func),
|
||||
"description": doc["description"],
|
||||
"parameters": params,
|
||||
}
|
||||
)
|
||||
|
||||
# Generate index
|
||||
index_content = generate_index_mdx(documented_classes, decorators)
|
||||
(OUTPUT_DIR / "index.mdx").write_text(index_content)
|
||||
|
||||
# Generate meta.json
|
||||
meta = {
|
||||
"title": "Python SDK",
|
||||
"pages": [slug_for(c["name"]) for c in documented_classes],
|
||||
}
|
||||
(OUTPUT_DIR / "meta.json").write_text(json.dumps(meta, indent=2))
|
||||
|
||||
print(f"\nDone! Generated {len(documented_classes)} class docs + index")
|
||||
print(f" Classes: {', '.join(c['name'] for c in documented_classes)}")
|
||||
print(f" Decorators: {', '.join(d['name'] for d in decorators)}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user