mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-01-13 17:18:08 -05:00
Compare commits
12 Commits
fix/creden
...
figure-out
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
04cf8d01b4 | ||
|
|
3d78b833f0 | ||
|
|
eea4bca826 | ||
|
|
0bf55b453b | ||
|
|
f25d2a0ae6 | ||
|
|
4baf0f7ee3 | ||
|
|
56f296af36 | ||
|
|
302e6d548d | ||
|
|
8d7defc89a | ||
|
|
7f94404dc2 | ||
|
|
7fda42d48b | ||
|
|
615d20613b |
74
.github/workflows/docs-block-sync.yml
vendored
Normal file
74
.github/workflows/docs-block-sync.yml
vendored
Normal file
@@ -0,0 +1,74 @@
|
||||
name: Block Documentation Sync Check
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master, dev]
|
||||
paths:
|
||||
- "autogpt_platform/backend/backend/blocks/**"
|
||||
- "docs/content/platform/blocks/**"
|
||||
- "autogpt_platform/backend/scripts/generate_block_docs.py"
|
||||
- ".github/workflows/docs-block-sync.yml"
|
||||
pull_request:
|
||||
branches: [master, dev]
|
||||
paths:
|
||||
- "autogpt_platform/backend/backend/blocks/**"
|
||||
- "docs/content/platform/blocks/**"
|
||||
- "autogpt_platform/backend/scripts/generate_block_docs.py"
|
||||
- ".github/workflows/docs-block-sync.yml"
|
||||
|
||||
jobs:
|
||||
check-docs-sync:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.11"
|
||||
|
||||
- name: Set up Python dependency cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.cache/pypoetry
|
||||
key: poetry-${{ runner.os }}-${{ hashFiles('autogpt_platform/backend/poetry.lock') }}
|
||||
restore-keys: |
|
||||
poetry-${{ runner.os }}-
|
||||
|
||||
- name: Install Poetry
|
||||
run: |
|
||||
cd autogpt_platform/backend
|
||||
HEAD_POETRY_VERSION=$(python3 ../../.github/workflows/scripts/get_package_version_from_lockfile.py poetry)
|
||||
echo "Found Poetry version ${HEAD_POETRY_VERSION} in backend/poetry.lock"
|
||||
curl -sSL https://install.python-poetry.org | POETRY_VERSION=$HEAD_POETRY_VERSION python3 -
|
||||
echo "$HOME/.local/bin" >> $GITHUB_PATH
|
||||
|
||||
- name: Install dependencies
|
||||
working-directory: autogpt_platform/backend
|
||||
run: |
|
||||
poetry install --only main
|
||||
poetry run prisma generate
|
||||
|
||||
- name: Check block documentation is in sync
|
||||
working-directory: autogpt_platform/backend
|
||||
run: |
|
||||
echo "Checking if block documentation is in sync with code..."
|
||||
poetry run python scripts/generate_block_docs.py --check
|
||||
|
||||
- name: Show diff if out of sync
|
||||
if: failure()
|
||||
run: |
|
||||
echo "::error::Block documentation is out of sync with code!"
|
||||
echo ""
|
||||
echo "To fix this, run the following command locally:"
|
||||
echo " cd autogpt_platform/backend && poetry run python scripts/generate_block_docs.py"
|
||||
echo ""
|
||||
echo "Then commit the updated documentation files."
|
||||
echo ""
|
||||
echo "Changes detected:"
|
||||
git diff docs/content/platform/blocks/ || true
|
||||
94
.github/workflows/docs-claude-review.yml
vendored
Normal file
94
.github/workflows/docs-claude-review.yml
vendored
Normal file
@@ -0,0 +1,94 @@
|
||||
name: Claude Block Docs Review
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize]
|
||||
paths:
|
||||
- "docs/content/platform/blocks/**"
|
||||
- "autogpt_platform/backend/backend/blocks/**"
|
||||
|
||||
jobs:
|
||||
claude-review:
|
||||
# Only run for PRs from members/collaborators
|
||||
if: |
|
||||
github.event.pull_request.author_association == 'OWNER' ||
|
||||
github.event.pull_request.author_association == 'MEMBER' ||
|
||||
github.event.pull_request.author_association == 'COLLABORATOR'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.11"
|
||||
|
||||
- name: Set up Python dependency cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.cache/pypoetry
|
||||
key: poetry-${{ runner.os }}-${{ hashFiles('autogpt_platform/backend/poetry.lock') }}
|
||||
restore-keys: |
|
||||
poetry-${{ runner.os }}-
|
||||
|
||||
- name: Install Poetry
|
||||
run: |
|
||||
cd autogpt_platform/backend
|
||||
HEAD_POETRY_VERSION=$(python3 ../../.github/workflows/scripts/get_package_version_from_lockfile.py poetry)
|
||||
curl -sSL https://install.python-poetry.org | POETRY_VERSION=$HEAD_POETRY_VERSION python3 -
|
||||
echo "$HOME/.local/bin" >> $GITHUB_PATH
|
||||
|
||||
- name: Install dependencies
|
||||
working-directory: autogpt_platform/backend
|
||||
run: |
|
||||
poetry install --only main
|
||||
poetry run prisma generate
|
||||
|
||||
- name: Run Claude Code Review
|
||||
uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
claude_args: |
|
||||
--allowedTools "Read,Glob,Grep,Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*)"
|
||||
prompt: |
|
||||
You are reviewing a PR that modifies block documentation or block code for AutoGPT.
|
||||
|
||||
## Your Task
|
||||
Review the changes in this PR and provide constructive feedback. Focus on:
|
||||
|
||||
1. **Documentation Accuracy**: For any block code changes, verify that:
|
||||
- Input/output tables in docs match the actual block schemas
|
||||
- Description text accurately reflects what the block does
|
||||
- Any new blocks have corresponding documentation
|
||||
|
||||
2. **Manual Content Quality**: Check manual sections (marked with `<!-- MANUAL: -->` markers):
|
||||
- "How it works" sections should have clear technical explanations
|
||||
- "Possible use case" sections should have practical, real-world examples
|
||||
- Content should be helpful for users trying to understand the blocks
|
||||
|
||||
3. **Template Compliance**: Ensure docs follow the standard template:
|
||||
- What it is (brief intro)
|
||||
- What it does (description)
|
||||
- How it works (technical explanation)
|
||||
- Inputs table
|
||||
- Outputs table
|
||||
- Possible use case
|
||||
|
||||
4. **Cross-references**: Check that links and anchors are correct
|
||||
|
||||
## Review Process
|
||||
1. First, get the PR diff to see what changed: `gh pr diff ${{ github.event.pull_request.number }}`
|
||||
2. Read any modified block files to understand the implementation
|
||||
3. Read corresponding documentation files to verify accuracy
|
||||
4. Provide your feedback as a PR comment
|
||||
|
||||
Be constructive and specific. If everything looks good, say so!
|
||||
If there are issues, explain what's wrong and suggest how to fix it.
|
||||
193
.github/workflows/docs-enhance.yml
vendored
Normal file
193
.github/workflows/docs-enhance.yml
vendored
Normal file
@@ -0,0 +1,193 @@
|
||||
name: Enhance Block Documentation
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
block_pattern:
|
||||
description: 'Block file pattern to enhance (e.g., "google/*.md" or "*" for all blocks)'
|
||||
required: true
|
||||
default: '*'
|
||||
type: string
|
||||
dry_run:
|
||||
description: 'Dry run mode - show proposed changes without committing'
|
||||
type: boolean
|
||||
default: true
|
||||
max_blocks:
|
||||
description: 'Maximum number of blocks to process (0 for unlimited)'
|
||||
type: number
|
||||
default: 10
|
||||
|
||||
jobs:
|
||||
enhance-docs:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 45
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.11"
|
||||
|
||||
- name: Set up Python dependency cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.cache/pypoetry
|
||||
key: poetry-${{ runner.os }}-${{ hashFiles('autogpt_platform/backend/poetry.lock') }}
|
||||
restore-keys: |
|
||||
poetry-${{ runner.os }}-
|
||||
|
||||
- name: Install Poetry
|
||||
run: |
|
||||
cd autogpt_platform/backend
|
||||
HEAD_POETRY_VERSION=$(python3 ../../.github/workflows/scripts/get_package_version_from_lockfile.py poetry)
|
||||
curl -sSL https://install.python-poetry.org | POETRY_VERSION=$HEAD_POETRY_VERSION python3 -
|
||||
echo "$HOME/.local/bin" >> $GITHUB_PATH
|
||||
|
||||
- name: Install dependencies
|
||||
working-directory: autogpt_platform/backend
|
||||
run: |
|
||||
poetry install --only main
|
||||
poetry run prisma generate
|
||||
|
||||
- name: Run Claude Enhancement
|
||||
uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
claude_args: |
|
||||
--allowedTools "Read,Edit,Glob,Grep,Write,Bash(git:*),Bash(gh:*),Bash(find:*),Bash(ls:*)"
|
||||
prompt: |
|
||||
You are enhancing block documentation for AutoGPT. Your task is to improve the MANUAL sections
|
||||
of block documentation files by reading the actual block implementations and writing helpful content.
|
||||
|
||||
## Configuration
|
||||
- Block pattern: ${{ inputs.block_pattern }}
|
||||
- Dry run: ${{ inputs.dry_run }}
|
||||
- Max blocks to process: ${{ inputs.max_blocks }}
|
||||
|
||||
## Your Task
|
||||
|
||||
1. **Find Documentation Files**
|
||||
Find block documentation files matching the pattern in `docs/content/platform/blocks/`
|
||||
Pattern: ${{ inputs.block_pattern }}
|
||||
|
||||
Use: `find docs/content/platform/blocks -name "*.md" -type f`
|
||||
|
||||
2. **For Each Documentation File** (up to ${{ inputs.max_blocks }} files):
|
||||
|
||||
a. Read the documentation file
|
||||
|
||||
b. Identify which block(s) it documents (look for the block class name)
|
||||
|
||||
c. Find and read the corresponding block implementation in `autogpt_platform/backend/backend/blocks/`
|
||||
|
||||
d. Improve the MANUAL sections:
|
||||
|
||||
**"How it works" section** (within `<!-- MANUAL: how_it_works -->` markers):
|
||||
- Explain the technical flow of the block
|
||||
- Describe what APIs or services it connects to
|
||||
- Note any important configuration or prerequisites
|
||||
- Keep it concise but informative (2-4 paragraphs)
|
||||
|
||||
**"Possible use case" section** (within `<!-- MANUAL: use_case -->` markers):
|
||||
- Provide 2-3 practical, real-world examples
|
||||
- Make them specific and actionable
|
||||
- Show how this block could be used in an automation workflow
|
||||
|
||||
3. **Important Rules**
|
||||
- ONLY modify content within `<!-- MANUAL: -->` and `<!-- END MANUAL -->` markers
|
||||
- Do NOT modify auto-generated sections (inputs/outputs tables, descriptions)
|
||||
- Keep content accurate based on the actual block implementation
|
||||
- Write for users who may not be technical experts
|
||||
|
||||
4. **Output**
|
||||
${{ inputs.dry_run == true && 'DRY RUN MODE: Show proposed changes for each file but do NOT actually edit the files. Describe what you would change.' || 'LIVE MODE: Actually edit the files to improve the documentation.' }}
|
||||
|
||||
## Example Improvements
|
||||
|
||||
**Before (How it works):**
|
||||
```
|
||||
_Add technical explanation here._
|
||||
```
|
||||
|
||||
**After (How it works):**
|
||||
```
|
||||
This block connects to the GitHub API to retrieve issue information. When executed,
|
||||
it authenticates using your GitHub credentials and fetches issue details including
|
||||
title, body, labels, and assignees.
|
||||
|
||||
The block requires a valid GitHub OAuth connection with repository access permissions.
|
||||
It supports both public and private repositories you have access to.
|
||||
```
|
||||
|
||||
**Before (Possible use case):**
|
||||
```
|
||||
_Add practical use case examples here._
|
||||
```
|
||||
|
||||
**After (Possible use case):**
|
||||
```
|
||||
**Customer Support Automation**: Monitor a GitHub repository for new issues with
|
||||
the "bug" label, then automatically create a ticket in your support system and
|
||||
notify the on-call engineer via Slack.
|
||||
|
||||
**Release Notes Generation**: When a new release is published, gather all closed
|
||||
issues since the last release and generate a summary for your changelog.
|
||||
```
|
||||
|
||||
Begin by finding and listing the documentation files to process.
|
||||
|
||||
- name: Create PR with enhanced documentation
|
||||
if: ${{ inputs.dry_run == false }}
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
# Check if there are changes
|
||||
if git diff --quiet docs/content/platform/blocks/; then
|
||||
echo "No changes to commit"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Configure git
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
# Create branch and commit
|
||||
BRANCH_NAME="docs/enhance-blocks-$(date +%Y%m%d-%H%M%S)"
|
||||
git checkout -b "$BRANCH_NAME"
|
||||
git add docs/content/platform/blocks/
|
||||
git commit -m "docs: enhance block documentation with LLM-generated content
|
||||
|
||||
Pattern: ${{ inputs.block_pattern }}
|
||||
Max blocks: ${{ inputs.max_blocks }}
|
||||
|
||||
🤖 Generated with [Claude Code](https://claude.com/claude-code)
|
||||
|
||||
Co-Authored-By: Claude <noreply@anthropic.com>"
|
||||
|
||||
# Push and create PR
|
||||
git push -u origin "$BRANCH_NAME"
|
||||
gh pr create \
|
||||
--title "docs: LLM-enhanced block documentation" \
|
||||
--body "## Summary
|
||||
This PR contains LLM-enhanced documentation for block files matching pattern: \`${{ inputs.block_pattern }}\`
|
||||
|
||||
The following manual sections were improved:
|
||||
- **How it works**: Technical explanations based on block implementations
|
||||
- **Possible use case**: Practical, real-world examples
|
||||
|
||||
## Review Checklist
|
||||
- [ ] Content is accurate based on block implementations
|
||||
- [ ] Examples are practical and helpful
|
||||
- [ ] No auto-generated sections were modified
|
||||
|
||||
---
|
||||
🤖 Generated with [Claude Code](https://claude.com/claude-code)" \
|
||||
--base dev
|
||||
@@ -108,9 +108,6 @@ class CredentialsMetaResponse(BaseModel):
|
||||
host: str | None = Field(
|
||||
default=None, description="Host pattern for host-scoped credentials"
|
||||
)
|
||||
is_system: bool = Field(
|
||||
default=False, description="Whether this is a system-managed credential"
|
||||
)
|
||||
|
||||
|
||||
@router.post("/{provider}/callback", summary="Exchange OAuth code for tokens")
|
||||
@@ -178,8 +175,6 @@ async def callback(
|
||||
f"Successfully processed OAuth callback for user {user_id} "
|
||||
f"and provider {provider.value}"
|
||||
)
|
||||
from backend.integrations.credentials_store import is_system_credential
|
||||
|
||||
return CredentialsMetaResponse(
|
||||
id=credentials.id,
|
||||
provider=credentials.provider,
|
||||
@@ -190,7 +185,6 @@ async def callback(
|
||||
host=(
|
||||
credentials.host if isinstance(credentials, HostScopedCredentials) else None
|
||||
),
|
||||
is_system=is_system_credential(credentials.id),
|
||||
)
|
||||
|
||||
|
||||
@@ -198,24 +192,7 @@ async def callback(
|
||||
async def list_credentials(
|
||||
user_id: Annotated[str, Security(get_user_id)],
|
||||
) -> list[CredentialsMetaResponse]:
|
||||
from backend.integrations.credentials_store import (
|
||||
DEFAULT_CREDENTIALS,
|
||||
is_system_credential,
|
||||
)
|
||||
|
||||
# Get user credentials and configured system credentials
|
||||
credentials = await creds_manager.store.get_all_creds(user_id)
|
||||
|
||||
# Create a set of credential IDs we've already included
|
||||
included_ids = {cred.id for cred in credentials}
|
||||
|
||||
# Always include all system credentials, even if not configured
|
||||
# This ensures the frontend can identify system credentials
|
||||
for system_cred in DEFAULT_CREDENTIALS:
|
||||
if system_cred.id not in included_ids:
|
||||
credentials.append(system_cred)
|
||||
included_ids.add(system_cred.id)
|
||||
|
||||
return [
|
||||
CredentialsMetaResponse(
|
||||
id=cred.id,
|
||||
@@ -225,7 +202,6 @@ async def list_credentials(
|
||||
scopes=cred.scopes if isinstance(cred, OAuth2Credentials) else None,
|
||||
username=cred.username if isinstance(cred, OAuth2Credentials) else None,
|
||||
host=cred.host if isinstance(cred, HostScopedCredentials) else None,
|
||||
is_system=is_system_credential(cred.id),
|
||||
)
|
||||
for cred in credentials
|
||||
]
|
||||
@@ -238,23 +214,7 @@ async def list_credentials_by_provider(
|
||||
],
|
||||
user_id: Annotated[str, Security(get_user_id)],
|
||||
) -> list[CredentialsMetaResponse]:
|
||||
from backend.integrations.credentials_store import (
|
||||
DEFAULT_CREDENTIALS,
|
||||
is_system_credential,
|
||||
)
|
||||
|
||||
# Get user credentials and configured system credentials for this provider
|
||||
credentials = await creds_manager.store.get_creds_by_provider(user_id, provider)
|
||||
|
||||
# Create a set of credential IDs we've already included
|
||||
included_ids = {cred.id for cred in credentials}
|
||||
|
||||
# Always include system credentials for this provider, even if not configured
|
||||
for system_cred in DEFAULT_CREDENTIALS:
|
||||
if system_cred.provider == provider and system_cred.id not in included_ids:
|
||||
credentials.append(system_cred)
|
||||
included_ids.add(system_cred.id)
|
||||
|
||||
return [
|
||||
CredentialsMetaResponse(
|
||||
id=cred.id,
|
||||
@@ -264,7 +224,6 @@ async def list_credentials_by_provider(
|
||||
scopes=cred.scopes if isinstance(cred, OAuth2Credentials) else None,
|
||||
username=cred.username if isinstance(cred, OAuth2Credentials) else None,
|
||||
host=cred.host if isinstance(cred, HostScopedCredentials) else None,
|
||||
is_system=is_system_credential(cred.id),
|
||||
)
|
||||
for cred in credentials
|
||||
]
|
||||
@@ -872,18 +831,6 @@ async def list_providers() -> List[str]:
|
||||
return all_providers
|
||||
|
||||
|
||||
@router.get("/providers/system", response_model=List[str])
|
||||
async def list_system_providers() -> List[str]:
|
||||
"""
|
||||
Get a list of providers that have platform credits (system credentials) available.
|
||||
|
||||
These providers can be used without the user providing their own API keys.
|
||||
"""
|
||||
from backend.integrations.credentials_store import SYSTEM_PROVIDERS
|
||||
|
||||
return list(SYSTEM_PROVIDERS)
|
||||
|
||||
|
||||
@router.get("/providers/names", response_model=ProviderNamesResponse)
|
||||
async def get_provider_names() -> ProviderNamesResponse:
|
||||
"""
|
||||
|
||||
@@ -81,7 +81,7 @@ class StoreValueBlock(Block):
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
id="1ff065e9-88e8-4358-9d82-8dc91f622ba9",
|
||||
description="This block forwards an input value as output, allowing reuse without change.",
|
||||
description="A basic block that stores and forwards a value throughout workflows, allowing it to be reused without changes across multiple blocks.",
|
||||
categories={BlockCategory.BASIC},
|
||||
input_schema=StoreValueBlock.Input,
|
||||
output_schema=StoreValueBlock.Output,
|
||||
@@ -111,7 +111,7 @@ class PrintToConsoleBlock(Block):
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
id="f3b1c1b2-4c4f-4f0d-8d2f-4c4f0d8d2f4c",
|
||||
description="Print the given text to the console, this is used for a debugging purpose.",
|
||||
description="A debugging block that outputs text to the console for monitoring and troubleshooting workflow execution.",
|
||||
categories={BlockCategory.BASIC},
|
||||
input_schema=PrintToConsoleBlock.Input,
|
||||
output_schema=PrintToConsoleBlock.Output,
|
||||
@@ -137,7 +137,7 @@ class NoteBlock(Block):
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
id="cc10ff7b-7753-4ff2-9af6-9399b1a7eddc",
|
||||
description="This block is used to display a sticky note with the given text.",
|
||||
description="A visual annotation block that displays a sticky note in the workflow editor for documentation and organization purposes.",
|
||||
categories={BlockCategory.BASIC},
|
||||
input_schema=NoteBlock.Input,
|
||||
output_schema=NoteBlock.Output,
|
||||
|
||||
@@ -159,7 +159,7 @@ class FindInDictionaryBlock(Block):
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
id="0e50422c-6dee-4145-83d6-3a5a392f65de",
|
||||
description="Lookup the given key in the input dictionary/object/list and return the value.",
|
||||
description="A block that looks up a value in a dictionary, list, or object by key or index and returns the corresponding value.",
|
||||
input_schema=FindInDictionaryBlock.Input,
|
||||
output_schema=FindInDictionaryBlock.Output,
|
||||
test_input=[
|
||||
|
||||
@@ -51,7 +51,7 @@ class GithubCommentBlock(Block):
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
id="a8db4d8d-db1c-4a25-a1b0-416a8c33602b",
|
||||
description="This block posts a comment on a specified GitHub issue or pull request.",
|
||||
description="A block that posts comments on GitHub issues or pull requests using the GitHub API.",
|
||||
categories={BlockCategory.DEVELOPER_TOOLS},
|
||||
input_schema=GithubCommentBlock.Input,
|
||||
output_schema=GithubCommentBlock.Output,
|
||||
@@ -151,7 +151,7 @@ class GithubUpdateCommentBlock(Block):
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
id="b3f4d747-10e3-4e69-8c51-f2be1d99c9a7",
|
||||
description="This block updates a comment on a specified GitHub issue or pull request.",
|
||||
description="A block that updates an existing comment on a GitHub issue or pull request.",
|
||||
categories={BlockCategory.DEVELOPER_TOOLS},
|
||||
input_schema=GithubUpdateCommentBlock.Input,
|
||||
output_schema=GithubUpdateCommentBlock.Output,
|
||||
@@ -249,7 +249,7 @@ class GithubListCommentsBlock(Block):
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
id="c4b5fb63-0005-4a11-b35a-0c2467bd6b59",
|
||||
description="This block lists all comments for a specified GitHub issue or pull request.",
|
||||
description="A block that retrieves all comments from a GitHub issue or pull request, including comment metadata and content.",
|
||||
categories={BlockCategory.DEVELOPER_TOOLS},
|
||||
input_schema=GithubListCommentsBlock.Input,
|
||||
output_schema=GithubListCommentsBlock.Output,
|
||||
@@ -363,7 +363,7 @@ class GithubMakeIssueBlock(Block):
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
id="691dad47-f494-44c3-a1e8-05b7990f2dab",
|
||||
description="This block creates a new issue on a specified GitHub repository.",
|
||||
description="A block that creates new issues on GitHub repositories with a title and body content.",
|
||||
categories={BlockCategory.DEVELOPER_TOOLS},
|
||||
input_schema=GithubMakeIssueBlock.Input,
|
||||
output_schema=GithubMakeIssueBlock.Output,
|
||||
@@ -433,7 +433,7 @@ class GithubReadIssueBlock(Block):
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
id="6443c75d-032a-4772-9c08-230c707c8acc",
|
||||
description="This block reads the body, title, and user of a specified GitHub issue.",
|
||||
description="A block that retrieves information about a specific GitHub issue, including its title, body content, and creator.",
|
||||
categories={BlockCategory.DEVELOPER_TOOLS},
|
||||
input_schema=GithubReadIssueBlock.Input,
|
||||
output_schema=GithubReadIssueBlock.Output,
|
||||
@@ -510,7 +510,7 @@ class GithubListIssuesBlock(Block):
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
id="c215bfd7-0e57-4573-8f8c-f7d4963dcd74",
|
||||
description="This block lists all issues for a specified GitHub repository.",
|
||||
description="A block that retrieves a list of issues from a GitHub repository with their titles and URLs.",
|
||||
categories={BlockCategory.DEVELOPER_TOOLS},
|
||||
input_schema=GithubListIssuesBlock.Input,
|
||||
output_schema=GithubListIssuesBlock.Output,
|
||||
@@ -597,7 +597,7 @@ class GithubAddLabelBlock(Block):
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
id="98bd6b77-9506-43d5-b669-6b9733c4b1f1",
|
||||
description="This block adds a label to a specified GitHub issue or pull request.",
|
||||
description="A block that adds a label to a GitHub issue or pull request for categorization and organization.",
|
||||
categories={BlockCategory.DEVELOPER_TOOLS},
|
||||
input_schema=GithubAddLabelBlock.Input,
|
||||
output_schema=GithubAddLabelBlock.Output,
|
||||
@@ -657,7 +657,7 @@ class GithubRemoveLabelBlock(Block):
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
id="78f050c5-3e3a-48c0-9e5b-ef1ceca5589c",
|
||||
description="This block removes a label from a specified GitHub issue or pull request.",
|
||||
description="A block that removes a label from a GitHub issue or pull request.",
|
||||
categories={BlockCategory.DEVELOPER_TOOLS},
|
||||
input_schema=GithubRemoveLabelBlock.Input,
|
||||
output_schema=GithubRemoveLabelBlock.Output,
|
||||
@@ -720,7 +720,7 @@ class GithubAssignIssueBlock(Block):
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
id="90507c72-b0ff-413a-886a-23bbbd66f542",
|
||||
description="This block assigns a user to a specified GitHub issue.",
|
||||
description="A block that assigns a GitHub user to an issue for task ownership and tracking.",
|
||||
categories={BlockCategory.DEVELOPER_TOOLS},
|
||||
input_schema=GithubAssignIssueBlock.Input,
|
||||
output_schema=GithubAssignIssueBlock.Output,
|
||||
@@ -786,7 +786,7 @@ class GithubUnassignIssueBlock(Block):
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
id="d154002a-38f4-46c2-962d-2488f2b05ece",
|
||||
description="This block unassigns a user from a specified GitHub issue.",
|
||||
description="A block that removes a user's assignment from a GitHub issue.",
|
||||
categories={BlockCategory.DEVELOPER_TOOLS},
|
||||
input_schema=GithubUnassignIssueBlock.Input,
|
||||
output_schema=GithubUnassignIssueBlock.Output,
|
||||
|
||||
@@ -353,7 +353,7 @@ class GmailReadBlock(GmailBase):
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
id="25310c70-b89b-43ba-b25c-4dfa7e2a481c",
|
||||
description="This block reads emails from Gmail.",
|
||||
description="A block that retrieves and reads emails from a Gmail account based on search criteria, returning detailed message information including subject, sender, body, and attachments.",
|
||||
categories={BlockCategory.COMMUNICATION},
|
||||
disabled=not GOOGLE_OAUTH_IS_CONFIGURED,
|
||||
input_schema=GmailReadBlock.Input,
|
||||
@@ -743,7 +743,7 @@ class GmailListLabelsBlock(GmailBase):
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
id="3e1c2c1c-c689-4520-b956-1f3bf4e02bb7",
|
||||
description="This block lists all labels in Gmail.",
|
||||
description="A block that retrieves all labels (categories) from a Gmail account for organizing and categorizing emails.",
|
||||
categories={BlockCategory.COMMUNICATION},
|
||||
input_schema=GmailListLabelsBlock.Input,
|
||||
output_schema=GmailListLabelsBlock.Output,
|
||||
@@ -807,7 +807,7 @@ class GmailAddLabelBlock(GmailBase):
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
id="f884b2fb-04f4-4265-9658-14f433926ac9",
|
||||
description="This block adds a label to a Gmail message.",
|
||||
description="A block that adds a label to a specific email message in Gmail, creating the label if it doesn't exist.",
|
||||
categories={BlockCategory.COMMUNICATION},
|
||||
input_schema=GmailAddLabelBlock.Input,
|
||||
output_schema=GmailAddLabelBlock.Output,
|
||||
@@ -893,7 +893,7 @@ class GmailRemoveLabelBlock(GmailBase):
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
id="0afc0526-aba1-4b2b-888e-a22b7c3f359d",
|
||||
description="This block removes a label from a Gmail message.",
|
||||
description="A block that removes a label from a specific email message in a Gmail account.",
|
||||
categories={BlockCategory.COMMUNICATION},
|
||||
input_schema=GmailRemoveLabelBlock.Input,
|
||||
output_schema=GmailRemoveLabelBlock.Output,
|
||||
@@ -961,7 +961,7 @@ class GmailGetThreadBlock(GmailBase):
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
id="21a79166-9df7-4b5f-9f36-96f639d86112",
|
||||
description="Get a full Gmail thread by ID",
|
||||
description="A block that retrieves an entire Gmail thread (email conversation) by ID, returning all messages with decoded bodies for reading complete conversations.",
|
||||
categories={BlockCategory.COMMUNICATION},
|
||||
input_schema=GmailGetThreadBlock.Input,
|
||||
output_schema=GmailGetThreadBlock.Output,
|
||||
|
||||
@@ -282,7 +282,7 @@ class GoogleSheetsReadBlock(Block):
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
id="5724e902-3635-47e9-a108-aaa0263a4988",
|
||||
description="This block reads data from a Google Sheets spreadsheet.",
|
||||
description="A block that reads data from a Google Sheets spreadsheet using A1 notation range selection.",
|
||||
categories={BlockCategory.DATA},
|
||||
input_schema=GoogleSheetsReadBlock.Input,
|
||||
output_schema=GoogleSheetsReadBlock.Output,
|
||||
@@ -409,7 +409,7 @@ class GoogleSheetsWriteBlock(Block):
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
id="d9291e87-301d-47a8-91fe-907fb55460e5",
|
||||
description="This block writes data to a Google Sheets spreadsheet.",
|
||||
description="A block that writes data to a Google Sheets spreadsheet at a specified A1 notation range.",
|
||||
categories={BlockCategory.DATA},
|
||||
input_schema=GoogleSheetsWriteBlock.Input,
|
||||
output_schema=GoogleSheetsWriteBlock.Output,
|
||||
|
||||
@@ -76,7 +76,7 @@ class AgentInputBlock(Block):
|
||||
super().__init__(
|
||||
**{
|
||||
"id": "c0a8e994-ebf1-4a9c-a4d8-89d09c86741b",
|
||||
"description": "Base block for user inputs.",
|
||||
"description": "A block that accepts and processes user input values within a workflow, supporting various input types and validation.",
|
||||
"input_schema": AgentInputBlock.Input,
|
||||
"output_schema": AgentInputBlock.Output,
|
||||
"test_input": [
|
||||
@@ -168,7 +168,7 @@ class AgentOutputBlock(Block):
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
id="363ae599-353e-4804-937e-b2ee3cef3da4",
|
||||
description="Stores the output of the graph for users to see.",
|
||||
description="A block that records and formats workflow results for display to users, with optional Jinja2 template formatting support.",
|
||||
input_schema=AgentOutputBlock.Input,
|
||||
output_schema=AgentOutputBlock.Output,
|
||||
test_input=[
|
||||
|
||||
@@ -854,7 +854,7 @@ class AIStructuredResponseGeneratorBlock(AIBlockBase):
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
id="ed55ac19-356e-4243-a6cb-bc599e9b716f",
|
||||
description="Call a Large Language Model (LLM) to generate formatted object based on the given prompt.",
|
||||
description="A block that generates structured JSON responses using a Large Language Model (LLM), with schema validation and format enforcement.",
|
||||
categories={BlockCategory.AI},
|
||||
input_schema=AIStructuredResponseGeneratorBlock.Input,
|
||||
output_schema=AIStructuredResponseGeneratorBlock.Output,
|
||||
@@ -1265,7 +1265,7 @@ class AITextGeneratorBlock(AIBlockBase):
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
id="1f292d4a-41a4-4977-9684-7c8d560b9f91",
|
||||
description="Call a Large Language Model (LLM) to generate a string based on the given prompt.",
|
||||
description="A block that produces text responses using a Large Language Model (LLM) based on customizable prompts and system instructions.",
|
||||
categories={BlockCategory.AI},
|
||||
input_schema=AITextGeneratorBlock.Input,
|
||||
output_schema=AITextGeneratorBlock.Output,
|
||||
@@ -1361,7 +1361,7 @@ class AITextSummarizerBlock(AIBlockBase):
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
id="a0a69be1-4528-491c-a85a-a4ab6873e3f0",
|
||||
description="Utilize a Large Language Model (LLM) to summarize a long text.",
|
||||
description="A block that summarizes long texts using a Large Language Model (LLM), with configurable focus topics and summary styles.",
|
||||
categories={BlockCategory.AI, BlockCategory.TEXT},
|
||||
input_schema=AITextSummarizerBlock.Input,
|
||||
output_schema=AITextSummarizerBlock.Output,
|
||||
@@ -1562,7 +1562,7 @@ class AIConversationBlock(AIBlockBase):
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
id="32a87eab-381e-4dd4-bdb8-4c47151be35a",
|
||||
description="Advanced LLM call that takes a list of messages and sends them to the language model.",
|
||||
description="A block that facilitates multi-turn conversations with a Large Language Model (LLM), maintaining context across message exchanges.",
|
||||
categories={BlockCategory.AI},
|
||||
input_schema=AIConversationBlock.Input,
|
||||
output_schema=AIConversationBlock.Output,
|
||||
@@ -1682,7 +1682,7 @@ class AIListGeneratorBlock(AIBlockBase):
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
id="9c0b0450-d199-458b-a731-072189dd6593",
|
||||
description="Generate a list of values based on the given prompt using a Large Language Model (LLM).",
|
||||
description="A block that creates lists of items based on prompts using a Large Language Model (LLM), with optional source data for context.",
|
||||
categories={BlockCategory.AI, BlockCategory.TEXT},
|
||||
input_schema=AIListGeneratorBlock.Input,
|
||||
output_schema=AIListGeneratorBlock.Output,
|
||||
|
||||
@@ -391,12 +391,8 @@ class SmartDecisionMakerBlock(Block):
|
||||
"""
|
||||
block = sink_node.block
|
||||
|
||||
# Use custom name from node metadata if set, otherwise fall back to block.name
|
||||
custom_name = sink_node.metadata.get("customized_name")
|
||||
tool_name = custom_name if custom_name else block.name
|
||||
|
||||
tool_function: dict[str, Any] = {
|
||||
"name": SmartDecisionMakerBlock.cleanup(tool_name),
|
||||
"name": SmartDecisionMakerBlock.cleanup(block.name),
|
||||
"description": block.description,
|
||||
}
|
||||
sink_block_input_schema = block.input_schema
|
||||
@@ -493,24 +489,14 @@ class SmartDecisionMakerBlock(Block):
|
||||
f"Sink graph metadata not found: {graph_id} {graph_version}"
|
||||
)
|
||||
|
||||
# Use custom name from node metadata if set, otherwise fall back to graph name
|
||||
custom_name = sink_node.metadata.get("customized_name")
|
||||
tool_name = custom_name if custom_name else sink_graph_meta.name
|
||||
|
||||
tool_function: dict[str, Any] = {
|
||||
"name": SmartDecisionMakerBlock.cleanup(tool_name),
|
||||
"name": SmartDecisionMakerBlock.cleanup(sink_graph_meta.name),
|
||||
"description": sink_graph_meta.description,
|
||||
}
|
||||
|
||||
properties = {}
|
||||
field_mapping = {}
|
||||
|
||||
for link in links:
|
||||
field_name = link.sink_name
|
||||
|
||||
clean_field_name = SmartDecisionMakerBlock.cleanup(field_name)
|
||||
field_mapping[clean_field_name] = field_name
|
||||
|
||||
sink_block_input_schema = sink_node.input_default["input_schema"]
|
||||
sink_block_properties = sink_block_input_schema.get("properties", {}).get(
|
||||
link.sink_name, {}
|
||||
@@ -520,7 +506,7 @@ class SmartDecisionMakerBlock(Block):
|
||||
if "description" in sink_block_properties
|
||||
else f"The {link.sink_name} of the tool"
|
||||
)
|
||||
properties[clean_field_name] = {
|
||||
properties[link.sink_name] = {
|
||||
"type": "string",
|
||||
"description": description,
|
||||
"default": json.dumps(sink_block_properties.get("default", None)),
|
||||
@@ -533,7 +519,7 @@ class SmartDecisionMakerBlock(Block):
|
||||
"strict": True,
|
||||
}
|
||||
|
||||
tool_function["_field_mapping"] = field_mapping
|
||||
# Store node info for later use in output processing
|
||||
tool_function["_sink_node_id"] = sink_node.id
|
||||
|
||||
return {"type": "function", "function": tool_function}
|
||||
@@ -1161,9 +1147,8 @@ class SmartDecisionMakerBlock(Block):
|
||||
original_field_name = field_mapping.get(clean_arg_name, clean_arg_name)
|
||||
arg_value = tool_args.get(clean_arg_name)
|
||||
|
||||
# Use original_field_name directly (not sanitized) to match link sink_name
|
||||
# The field_mapping already translates from LLM's cleaned names to original names
|
||||
emit_key = f"tools_^_{sink_node_id}_~_{original_field_name}"
|
||||
sanitized_arg_name = self.cleanup(original_field_name)
|
||||
emit_key = f"tools_^_{sink_node_id}_~_{sanitized_arg_name}"
|
||||
|
||||
logger.debug(
|
||||
"[SmartDecisionMakerBlock|geid:%s|neid:%s] emit %s",
|
||||
|
||||
@@ -1057,153 +1057,3 @@ async def test_smart_decision_maker_traditional_mode_default():
|
||||
) # Should yield individual tool parameters
|
||||
assert "tools_^_test-sink-node-id_~_max_keyword_difficulty" in outputs
|
||||
assert "conversations" in outputs
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_smart_decision_maker_uses_customized_name_for_blocks():
|
||||
"""Test that SmartDecisionMakerBlock uses customized_name from node metadata for tool names."""
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from backend.blocks.basic import StoreValueBlock
|
||||
from backend.blocks.smart_decision_maker import SmartDecisionMakerBlock
|
||||
from backend.data.graph import Link, Node
|
||||
|
||||
# Create a mock node with customized_name in metadata
|
||||
mock_node = MagicMock(spec=Node)
|
||||
mock_node.id = "test-node-id"
|
||||
mock_node.block_id = StoreValueBlock().id
|
||||
mock_node.metadata = {"customized_name": "My Custom Tool Name"}
|
||||
mock_node.block = StoreValueBlock()
|
||||
|
||||
# Create a mock link
|
||||
mock_link = MagicMock(spec=Link)
|
||||
mock_link.sink_name = "input"
|
||||
|
||||
# Call the function directly
|
||||
result = await SmartDecisionMakerBlock._create_block_function_signature(
|
||||
mock_node, [mock_link]
|
||||
)
|
||||
|
||||
# Verify the tool name uses the customized name (cleaned up)
|
||||
assert result["type"] == "function"
|
||||
assert result["function"]["name"] == "my_custom_tool_name" # Cleaned version
|
||||
assert result["function"]["_sink_node_id"] == "test-node-id"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_smart_decision_maker_falls_back_to_block_name():
|
||||
"""Test that SmartDecisionMakerBlock falls back to block.name when no customized_name."""
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from backend.blocks.basic import StoreValueBlock
|
||||
from backend.blocks.smart_decision_maker import SmartDecisionMakerBlock
|
||||
from backend.data.graph import Link, Node
|
||||
|
||||
# Create a mock node without customized_name
|
||||
mock_node = MagicMock(spec=Node)
|
||||
mock_node.id = "test-node-id"
|
||||
mock_node.block_id = StoreValueBlock().id
|
||||
mock_node.metadata = {} # No customized_name
|
||||
mock_node.block = StoreValueBlock()
|
||||
|
||||
# Create a mock link
|
||||
mock_link = MagicMock(spec=Link)
|
||||
mock_link.sink_name = "input"
|
||||
|
||||
# Call the function directly
|
||||
result = await SmartDecisionMakerBlock._create_block_function_signature(
|
||||
mock_node, [mock_link]
|
||||
)
|
||||
|
||||
# Verify the tool name uses the block's default name
|
||||
assert result["type"] == "function"
|
||||
assert result["function"]["name"] == "storevalueblock" # Default block name cleaned
|
||||
assert result["function"]["_sink_node_id"] == "test-node-id"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_smart_decision_maker_uses_customized_name_for_agents():
|
||||
"""Test that SmartDecisionMakerBlock uses customized_name from metadata for agent nodes."""
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
from backend.blocks.smart_decision_maker import SmartDecisionMakerBlock
|
||||
from backend.data.graph import Link, Node
|
||||
|
||||
# Create a mock node with customized_name in metadata
|
||||
mock_node = MagicMock(spec=Node)
|
||||
mock_node.id = "test-agent-node-id"
|
||||
mock_node.metadata = {"customized_name": "My Custom Agent"}
|
||||
mock_node.input_default = {
|
||||
"graph_id": "test-graph-id",
|
||||
"graph_version": 1,
|
||||
"input_schema": {"properties": {"test_input": {"description": "Test input"}}},
|
||||
}
|
||||
|
||||
# Create a mock link
|
||||
mock_link = MagicMock(spec=Link)
|
||||
mock_link.sink_name = "test_input"
|
||||
|
||||
# Mock the database client
|
||||
mock_graph_meta = MagicMock()
|
||||
mock_graph_meta.name = "Original Agent Name"
|
||||
mock_graph_meta.description = "Agent description"
|
||||
|
||||
mock_db_client = AsyncMock()
|
||||
mock_db_client.get_graph_metadata.return_value = mock_graph_meta
|
||||
|
||||
with patch(
|
||||
"backend.blocks.smart_decision_maker.get_database_manager_async_client",
|
||||
return_value=mock_db_client,
|
||||
):
|
||||
result = await SmartDecisionMakerBlock._create_agent_function_signature(
|
||||
mock_node, [mock_link]
|
||||
)
|
||||
|
||||
# Verify the tool name uses the customized name (cleaned up)
|
||||
assert result["type"] == "function"
|
||||
assert result["function"]["name"] == "my_custom_agent" # Cleaned version
|
||||
assert result["function"]["_sink_node_id"] == "test-agent-node-id"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_smart_decision_maker_agent_falls_back_to_graph_name():
|
||||
"""Test that agent node falls back to graph name when no customized_name."""
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
from backend.blocks.smart_decision_maker import SmartDecisionMakerBlock
|
||||
from backend.data.graph import Link, Node
|
||||
|
||||
# Create a mock node without customized_name
|
||||
mock_node = MagicMock(spec=Node)
|
||||
mock_node.id = "test-agent-node-id"
|
||||
mock_node.metadata = {} # No customized_name
|
||||
mock_node.input_default = {
|
||||
"graph_id": "test-graph-id",
|
||||
"graph_version": 1,
|
||||
"input_schema": {"properties": {"test_input": {"description": "Test input"}}},
|
||||
}
|
||||
|
||||
# Create a mock link
|
||||
mock_link = MagicMock(spec=Link)
|
||||
mock_link.sink_name = "test_input"
|
||||
|
||||
# Mock the database client
|
||||
mock_graph_meta = MagicMock()
|
||||
mock_graph_meta.name = "Original Agent Name"
|
||||
mock_graph_meta.description = "Agent description"
|
||||
|
||||
mock_db_client = AsyncMock()
|
||||
mock_db_client.get_graph_metadata.return_value = mock_graph_meta
|
||||
|
||||
with patch(
|
||||
"backend.blocks.smart_decision_maker.get_database_manager_async_client",
|
||||
return_value=mock_db_client,
|
||||
):
|
||||
result = await SmartDecisionMakerBlock._create_agent_function_signature(
|
||||
mock_node, [mock_link]
|
||||
)
|
||||
|
||||
# Verify the tool name uses the graph's default name
|
||||
assert result["type"] == "function"
|
||||
assert result["function"]["name"] == "original_agent_name" # Graph name cleaned
|
||||
assert result["function"]["_sink_node_id"] == "test-agent-node-id"
|
||||
|
||||
@@ -15,7 +15,6 @@ async def test_smart_decision_maker_handles_dynamic_dict_fields():
|
||||
mock_node.block = CreateDictionaryBlock()
|
||||
mock_node.block_id = CreateDictionaryBlock().id
|
||||
mock_node.input_default = {}
|
||||
mock_node.metadata = {}
|
||||
|
||||
# Create mock links with dynamic dictionary fields
|
||||
mock_links = [
|
||||
@@ -78,7 +77,6 @@ async def test_smart_decision_maker_handles_dynamic_list_fields():
|
||||
mock_node.block = AddToListBlock()
|
||||
mock_node.block_id = AddToListBlock().id
|
||||
mock_node.input_default = {}
|
||||
mock_node.metadata = {}
|
||||
|
||||
# Create mock links with dynamic list fields
|
||||
mock_links = [
|
||||
|
||||
@@ -44,7 +44,6 @@ async def test_create_block_function_signature_with_dict_fields():
|
||||
mock_node.block = CreateDictionaryBlock()
|
||||
mock_node.block_id = CreateDictionaryBlock().id
|
||||
mock_node.input_default = {}
|
||||
mock_node.metadata = {}
|
||||
|
||||
# Create mock links with dynamic dictionary fields (source sanitized, sink original)
|
||||
mock_links = [
|
||||
@@ -107,7 +106,6 @@ async def test_create_block_function_signature_with_list_fields():
|
||||
mock_node.block = AddToListBlock()
|
||||
mock_node.block_id = AddToListBlock().id
|
||||
mock_node.input_default = {}
|
||||
mock_node.metadata = {}
|
||||
|
||||
# Create mock links with dynamic list fields
|
||||
mock_links = [
|
||||
@@ -161,7 +159,6 @@ async def test_create_block_function_signature_with_object_fields():
|
||||
mock_node.block = MatchTextPatternBlock()
|
||||
mock_node.block_id = MatchTextPatternBlock().id
|
||||
mock_node.input_default = {}
|
||||
mock_node.metadata = {}
|
||||
|
||||
# Create mock links with dynamic object fields
|
||||
mock_links = [
|
||||
@@ -211,13 +208,11 @@ async def test_create_tool_node_signatures():
|
||||
mock_dict_node.block = CreateDictionaryBlock()
|
||||
mock_dict_node.block_id = CreateDictionaryBlock().id
|
||||
mock_dict_node.input_default = {}
|
||||
mock_dict_node.metadata = {}
|
||||
|
||||
mock_list_node = Mock()
|
||||
mock_list_node.block = AddToListBlock()
|
||||
mock_list_node.block_id = AddToListBlock().id
|
||||
mock_list_node.input_default = {}
|
||||
mock_list_node.metadata = {}
|
||||
|
||||
# Mock links with dynamic fields
|
||||
dict_link1 = Mock(
|
||||
@@ -428,7 +423,6 @@ async def test_mixed_regular_and_dynamic_fields():
|
||||
mock_node.block.name = "TestBlock"
|
||||
mock_node.block.description = "A test block"
|
||||
mock_node.block.input_schema = Mock()
|
||||
mock_node.metadata = {}
|
||||
|
||||
# Mock the get_field_schema to return a proper schema for regular fields
|
||||
def get_field_schema(field_name):
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
from .blog import WordPressCreatePostBlock, WordPressGetAllPostsBlock
|
||||
from .blog import WordPressCreatePostBlock
|
||||
|
||||
__all__ = ["WordPressCreatePostBlock", "WordPressGetAllPostsBlock"]
|
||||
__all__ = ["WordPressCreatePostBlock"]
|
||||
|
||||
@@ -161,7 +161,7 @@ async def oauth_exchange_code_for_tokens(
|
||||
grant_type="authorization_code",
|
||||
).model_dump(exclude_none=True)
|
||||
|
||||
response = await Requests(raise_for_status=False).post(
|
||||
response = await Requests().post(
|
||||
f"{WORDPRESS_BASE_URL}oauth2/token",
|
||||
headers=headers,
|
||||
data=data,
|
||||
@@ -205,7 +205,7 @@ async def oauth_refresh_tokens(
|
||||
grant_type="refresh_token",
|
||||
).model_dump(exclude_none=True)
|
||||
|
||||
response = await Requests(raise_for_status=False).post(
|
||||
response = await Requests().post(
|
||||
f"{WORDPRESS_BASE_URL}oauth2/token",
|
||||
headers=headers,
|
||||
data=data,
|
||||
@@ -252,7 +252,7 @@ async def validate_token(
|
||||
"token": token,
|
||||
}
|
||||
|
||||
response = await Requests(raise_for_status=False).get(
|
||||
response = await Requests().get(
|
||||
f"{WORDPRESS_BASE_URL}oauth2/token-info",
|
||||
params=params,
|
||||
)
|
||||
@@ -296,7 +296,7 @@ async def make_api_request(
|
||||
|
||||
url = f"{WORDPRESS_BASE_URL.rstrip('/')}{endpoint}"
|
||||
|
||||
request_method = getattr(Requests(raise_for_status=False), method.lower())
|
||||
request_method = getattr(Requests(), method.lower())
|
||||
response = await request_method(
|
||||
url,
|
||||
headers=headers,
|
||||
@@ -476,7 +476,6 @@ async def create_post(
|
||||
data["tags"] = ",".join(str(t) for t in data["tags"])
|
||||
|
||||
# Make the API request
|
||||
site = normalize_site(site)
|
||||
endpoint = f"/rest/v1.1/sites/{site}/posts/new"
|
||||
|
||||
headers = {
|
||||
@@ -484,7 +483,7 @@ async def create_post(
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
}
|
||||
|
||||
response = await Requests(raise_for_status=False).post(
|
||||
response = await Requests().post(
|
||||
f"{WORDPRESS_BASE_URL.rstrip('/')}{endpoint}",
|
||||
headers=headers,
|
||||
data=data,
|
||||
@@ -500,132 +499,3 @@ async def create_post(
|
||||
)
|
||||
error_message = error_data.get("message", response.text)
|
||||
raise ValueError(f"Failed to create post: {response.status} - {error_message}")
|
||||
|
||||
|
||||
class Post(BaseModel):
|
||||
"""Response model for individual posts in a posts list response.
|
||||
|
||||
This is a simplified version compared to PostResponse, as the list endpoint
|
||||
returns less detailed information than the create/get single post endpoints.
|
||||
"""
|
||||
|
||||
ID: int
|
||||
site_ID: int
|
||||
author: PostAuthor
|
||||
date: datetime
|
||||
modified: datetime
|
||||
title: str
|
||||
URL: str
|
||||
short_URL: str
|
||||
content: str | None = None
|
||||
excerpt: str | None = None
|
||||
slug: str
|
||||
guid: str
|
||||
status: str
|
||||
sticky: bool
|
||||
password: str | None = ""
|
||||
parent: Union[Dict[str, Any], bool, None] = None
|
||||
type: str
|
||||
discussion: Dict[str, Union[str, bool, int]] | None = None
|
||||
likes_enabled: bool | None = None
|
||||
sharing_enabled: bool | None = None
|
||||
like_count: int | None = None
|
||||
i_like: bool | None = None
|
||||
is_reblogged: bool | None = None
|
||||
is_following: bool | None = None
|
||||
global_ID: str | None = None
|
||||
featured_image: str | None = None
|
||||
post_thumbnail: Dict[str, Any] | None = None
|
||||
format: str | None = None
|
||||
geo: Union[Dict[str, Any], bool, None] = None
|
||||
menu_order: int | None = None
|
||||
page_template: str | None = None
|
||||
publicize_URLs: List[str] | None = None
|
||||
terms: Dict[str, Dict[str, Any]] | None = None
|
||||
tags: Dict[str, Dict[str, Any]] | None = None
|
||||
categories: Dict[str, Dict[str, Any]] | None = None
|
||||
attachments: Dict[str, Dict[str, Any]] | None = None
|
||||
attachment_count: int | None = None
|
||||
metadata: List[Dict[str, Any]] | None = None
|
||||
meta: Dict[str, Any] | None = None
|
||||
capabilities: Dict[str, bool] | None = None
|
||||
revisions: List[int] | None = None
|
||||
other_URLs: Dict[str, Any] | None = None
|
||||
|
||||
|
||||
class PostsResponse(BaseModel):
|
||||
"""Response model for WordPress posts list."""
|
||||
|
||||
found: int
|
||||
posts: List[Post]
|
||||
meta: Dict[str, Any]
|
||||
|
||||
|
||||
def normalize_site(site: str) -> str:
|
||||
"""
|
||||
Normalize a site identifier by stripping protocol and trailing slashes.
|
||||
|
||||
Args:
|
||||
site: Site URL, domain, or ID (e.g., "https://myblog.wordpress.com/", "myblog.wordpress.com", "123456789")
|
||||
|
||||
Returns:
|
||||
Normalized site identifier (domain or ID only)
|
||||
"""
|
||||
site = site.strip()
|
||||
if site.startswith("https://"):
|
||||
site = site[8:]
|
||||
elif site.startswith("http://"):
|
||||
site = site[7:]
|
||||
return site.rstrip("/")
|
||||
|
||||
|
||||
async def get_posts(
|
||||
credentials: Credentials,
|
||||
site: str,
|
||||
status: PostStatus | None = None,
|
||||
number: int = 100,
|
||||
offset: int = 0,
|
||||
) -> PostsResponse:
|
||||
"""
|
||||
Get posts from a WordPress site.
|
||||
|
||||
Args:
|
||||
credentials: OAuth credentials
|
||||
site: Site ID or domain (e.g., "myblog.wordpress.com" or "123456789")
|
||||
status: Filter by post status using PostStatus enum, or None for all
|
||||
number: Number of posts to retrieve (max 100)
|
||||
offset: Number of posts to skip (for pagination)
|
||||
|
||||
Returns:
|
||||
PostsResponse with the list of posts
|
||||
"""
|
||||
site = normalize_site(site)
|
||||
endpoint = f"/rest/v1.1/sites/{site}/posts"
|
||||
|
||||
headers = {
|
||||
"Authorization": credentials.auth_header(),
|
||||
}
|
||||
|
||||
params: Dict[str, Any] = {
|
||||
"number": max(1, min(number, 100)), # 1–100 posts per request
|
||||
"offset": offset,
|
||||
}
|
||||
|
||||
if status:
|
||||
params["status"] = status.value
|
||||
response = await Requests(raise_for_status=False).get(
|
||||
f"{WORDPRESS_BASE_URL.rstrip('/')}{endpoint}",
|
||||
headers=headers,
|
||||
params=params,
|
||||
)
|
||||
|
||||
if response.ok:
|
||||
return PostsResponse.model_validate(response.json())
|
||||
|
||||
error_data = (
|
||||
response.json()
|
||||
if response.headers.get("content-type", "").startswith("application/json")
|
||||
else {}
|
||||
)
|
||||
error_message = error_data.get("message", response.text)
|
||||
raise ValueError(f"Failed to get posts: {response.status} - {error_message}")
|
||||
|
||||
@@ -9,15 +9,7 @@ from backend.sdk import (
|
||||
SchemaField,
|
||||
)
|
||||
|
||||
from ._api import (
|
||||
CreatePostRequest,
|
||||
Post,
|
||||
PostResponse,
|
||||
PostsResponse,
|
||||
PostStatus,
|
||||
create_post,
|
||||
get_posts,
|
||||
)
|
||||
from ._api import CreatePostRequest, PostResponse, PostStatus, create_post
|
||||
from ._config import wordpress
|
||||
|
||||
|
||||
@@ -57,15 +49,8 @@ class WordPressCreatePostBlock(Block):
|
||||
media_urls: list[str] = SchemaField(
|
||||
description="URLs of images to sideload and attach to the post", default=[]
|
||||
)
|
||||
publish_as_draft: bool = SchemaField(
|
||||
description="If True, publishes the post as a draft. If False, publishes it publicly.",
|
||||
default=False,
|
||||
)
|
||||
|
||||
class Output(BlockSchemaOutput):
|
||||
site: str = SchemaField(
|
||||
description="The site ID or domain (pass-through for chaining with other blocks)"
|
||||
)
|
||||
post_id: int = SchemaField(description="The ID of the created post")
|
||||
post_url: str = SchemaField(description="The full URL of the created post")
|
||||
short_url: str = SchemaField(description="The shortened wp.me URL")
|
||||
@@ -93,9 +78,7 @@ class WordPressCreatePostBlock(Block):
|
||||
tags=input_data.tags,
|
||||
featured_image=input_data.featured_image,
|
||||
media_urls=input_data.media_urls,
|
||||
status=(
|
||||
PostStatus.DRAFT if input_data.publish_as_draft else PostStatus.PUBLISH
|
||||
),
|
||||
status=PostStatus.PUBLISH,
|
||||
)
|
||||
|
||||
post_response: PostResponse = await create_post(
|
||||
@@ -104,69 +87,7 @@ class WordPressCreatePostBlock(Block):
|
||||
post_data=post_request,
|
||||
)
|
||||
|
||||
yield "site", input_data.site
|
||||
yield "post_id", post_response.ID
|
||||
yield "post_url", post_response.URL
|
||||
yield "short_url", post_response.short_URL
|
||||
yield "post_data", post_response.model_dump()
|
||||
|
||||
|
||||
class WordPressGetAllPostsBlock(Block):
|
||||
"""
|
||||
Fetches all posts from a WordPress.com site or Jetpack-enabled site.
|
||||
Supports filtering by status and pagination.
|
||||
"""
|
||||
|
||||
class Input(BlockSchemaInput):
|
||||
credentials: CredentialsMetaInput = wordpress.credentials_field()
|
||||
site: str = SchemaField(
|
||||
description="Site ID or domain (e.g., 'myblog.wordpress.com' or '123456789')"
|
||||
)
|
||||
status: PostStatus | None = SchemaField(
|
||||
description="Filter by post status, or None for all",
|
||||
default=None,
|
||||
)
|
||||
number: int = SchemaField(
|
||||
description="Number of posts to retrieve (max 100 per request)", default=20
|
||||
)
|
||||
offset: int = SchemaField(
|
||||
description="Number of posts to skip (for pagination)", default=0
|
||||
)
|
||||
|
||||
class Output(BlockSchemaOutput):
|
||||
site: str = SchemaField(
|
||||
description="The site ID or domain (pass-through for chaining with other blocks)"
|
||||
)
|
||||
found: int = SchemaField(description="Total number of posts found")
|
||||
posts: list[Post] = SchemaField(
|
||||
description="List of post objects with their details"
|
||||
)
|
||||
post: Post = SchemaField(
|
||||
description="Individual post object (yielded for each post)"
|
||||
)
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
id="97728fa7-7f6f-4789-ba0c-f2c114119536",
|
||||
description="Fetch all posts from WordPress.com or Jetpack sites",
|
||||
categories={BlockCategory.SOCIAL},
|
||||
input_schema=self.Input,
|
||||
output_schema=self.Output,
|
||||
)
|
||||
|
||||
async def run(
|
||||
self, input_data: Input, *, credentials: Credentials, **kwargs
|
||||
) -> BlockOutput:
|
||||
posts_response: PostsResponse = await get_posts(
|
||||
credentials=credentials,
|
||||
site=input_data.site,
|
||||
status=input_data.status,
|
||||
number=input_data.number,
|
||||
offset=input_data.offset,
|
||||
)
|
||||
|
||||
yield "site", input_data.site
|
||||
yield "found", posts_response.found
|
||||
yield "posts", posts_response.posts
|
||||
for post in posts_response.posts:
|
||||
yield "post", post
|
||||
|
||||
@@ -245,21 +245,6 @@ DEFAULT_CREDENTIALS = [
|
||||
webshare_proxy_credentials,
|
||||
]
|
||||
|
||||
SYSTEM_CREDENTIAL_IDS = {cred.id for cred in DEFAULT_CREDENTIALS}
|
||||
|
||||
# Set of providers that have system credentials available
|
||||
SYSTEM_PROVIDERS = {cred.provider for cred in DEFAULT_CREDENTIALS}
|
||||
|
||||
|
||||
def is_system_credential(credential_id: str) -> bool:
|
||||
"""Check if a credential ID belongs to a system-managed credential."""
|
||||
return credential_id in SYSTEM_CREDENTIAL_IDS
|
||||
|
||||
|
||||
def is_system_provider(provider: str) -> bool:
|
||||
"""Check if a provider has system-managed credentials available."""
|
||||
return provider in SYSTEM_PROVIDERS
|
||||
|
||||
|
||||
class IntegrationCredentialsStore:
|
||||
def __init__(self):
|
||||
|
||||
746
autogpt_platform/backend/scripts/generate_block_docs.py
Normal file
746
autogpt_platform/backend/scripts/generate_block_docs.py
Normal file
@@ -0,0 +1,746 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Block Documentation Generator
|
||||
|
||||
Generates markdown documentation for all blocks from code introspection.
|
||||
Preserves manually-written content between marker comments.
|
||||
|
||||
Usage:
|
||||
# Generate all docs
|
||||
poetry run python scripts/generate_block_docs.py
|
||||
|
||||
# Check mode for CI (exits 1 if stale)
|
||||
poetry run python scripts/generate_block_docs.py --check
|
||||
|
||||
# Migrate existing docs (add markers, preserve content)
|
||||
poetry run python scripts/generate_block_docs.py --migrate
|
||||
|
||||
# Verbose output
|
||||
poetry run python scripts/generate_block_docs.py -v
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import inspect
|
||||
import logging
|
||||
import re
|
||||
import sys
|
||||
from collections import defaultdict
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
# Add backend to path for imports
|
||||
backend_dir = Path(__file__).parent.parent
|
||||
sys.path.insert(0, str(backend_dir))
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Default output directory relative to repo root
|
||||
DEFAULT_OUTPUT_DIR = (
|
||||
Path(__file__).parent.parent.parent.parent / "docs" / "platform" / "blocks"
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class FieldDoc:
|
||||
"""Documentation for a single input/output field."""
|
||||
|
||||
name: str
|
||||
description: str
|
||||
type_str: str
|
||||
required: bool
|
||||
default: Any = None
|
||||
advanced: bool = False
|
||||
hidden: bool = False
|
||||
placeholder: str | None = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class BlockDoc:
|
||||
"""Documentation data extracted from a block."""
|
||||
|
||||
id: str
|
||||
name: str
|
||||
class_name: str
|
||||
description: str
|
||||
categories: list[str]
|
||||
category_descriptions: dict[str, str]
|
||||
inputs: list[FieldDoc]
|
||||
outputs: list[FieldDoc]
|
||||
block_type: str
|
||||
source_file: str
|
||||
contributors: list[str] = field(default_factory=list)
|
||||
|
||||
|
||||
# Category to human-readable name mapping
|
||||
CATEGORY_DISPLAY_NAMES = {
|
||||
"AI": "AI and Language Models",
|
||||
"BASIC": "Basic Operations",
|
||||
"TEXT": "Text Processing",
|
||||
"SEARCH": "Search and Information Retrieval",
|
||||
"SOCIAL": "Social Media and Content",
|
||||
"DEVELOPER_TOOLS": "Developer Tools",
|
||||
"DATA": "Data Processing",
|
||||
"LOGIC": "Logic and Control Flow",
|
||||
"COMMUNICATION": "Communication",
|
||||
"INPUT": "Input/Output",
|
||||
"OUTPUT": "Input/Output",
|
||||
"MULTIMEDIA": "Media Generation",
|
||||
"PRODUCTIVITY": "Productivity",
|
||||
"HARDWARE": "Hardware",
|
||||
"AGENT": "Agent Integration",
|
||||
"CRM": "CRM Services",
|
||||
"SAFETY": "AI Safety",
|
||||
"ISSUE_TRACKING": "Issue Tracking",
|
||||
"MARKETING": "Marketing",
|
||||
}
|
||||
|
||||
# Category to doc file mapping (for grouping related blocks)
|
||||
CATEGORY_FILE_MAP = {
|
||||
"BASIC": "basic",
|
||||
"TEXT": "text",
|
||||
"AI": "llm",
|
||||
"SEARCH": "search",
|
||||
"DATA": "data",
|
||||
"LOGIC": "logic",
|
||||
"COMMUNICATION": "communication",
|
||||
"MULTIMEDIA": "multimedia",
|
||||
"PRODUCTIVITY": "productivity",
|
||||
}
|
||||
|
||||
|
||||
def class_name_to_display_name(class_name: str) -> str:
|
||||
"""Convert BlockClassName to 'Block Class Name'."""
|
||||
# Remove 'Block' suffix
|
||||
name = class_name.replace("Block", "")
|
||||
# Insert space before capitals
|
||||
name = re.sub(r"([a-z])([A-Z])", r"\1 \2", name)
|
||||
# Handle consecutive capitals (e.g., 'HTTPRequest' -> 'HTTP Request')
|
||||
name = re.sub(r"([A-Z]+)([A-Z][a-z])", r"\1 \2", name)
|
||||
return name.strip()
|
||||
|
||||
|
||||
def type_to_readable(type_schema: dict[str, Any]) -> str:
|
||||
"""Convert JSON schema type to human-readable string."""
|
||||
if not isinstance(type_schema, dict):
|
||||
return str(type_schema) if type_schema else "Any"
|
||||
|
||||
if "anyOf" in type_schema:
|
||||
# Union type - show options
|
||||
any_of = type_schema["anyOf"]
|
||||
if not isinstance(any_of, list):
|
||||
return "Any"
|
||||
options = []
|
||||
for opt in any_of:
|
||||
if isinstance(opt, dict) and opt.get("type") == "null":
|
||||
continue
|
||||
options.append(type_to_readable(opt))
|
||||
if len(options) == 1:
|
||||
return options[0]
|
||||
return " | ".join(options)
|
||||
|
||||
if "allOf" in type_schema:
|
||||
all_of = type_schema["allOf"]
|
||||
if not isinstance(all_of, list) or not all_of:
|
||||
return "Any"
|
||||
return type_to_readable(all_of[0])
|
||||
|
||||
schema_type = type_schema.get("type")
|
||||
|
||||
if schema_type == "array":
|
||||
items = type_schema.get("items", {})
|
||||
item_type = type_to_readable(items)
|
||||
return f"List[{item_type}]"
|
||||
|
||||
if schema_type == "object":
|
||||
if "additionalProperties" in type_schema:
|
||||
value_type = type_to_readable(type_schema["additionalProperties"])
|
||||
return f"Dict[str, {value_type}]"
|
||||
# Check if it's a specific model
|
||||
title = type_schema.get("title", "Object")
|
||||
return title
|
||||
|
||||
if schema_type == "string":
|
||||
if "enum" in type_schema:
|
||||
return " | ".join(f'"{v}"' for v in type_schema["enum"][:3])
|
||||
if "format" in type_schema:
|
||||
return f"str ({type_schema['format']})"
|
||||
return "str"
|
||||
|
||||
if schema_type == "integer":
|
||||
return "int"
|
||||
|
||||
if schema_type == "number":
|
||||
return "float"
|
||||
|
||||
if schema_type == "boolean":
|
||||
return "bool"
|
||||
|
||||
if schema_type == "null":
|
||||
return "None"
|
||||
|
||||
# Fallback
|
||||
return type_schema.get("title", schema_type or "Any")
|
||||
|
||||
|
||||
def safe_get(d: Any, key: str, default: Any = None) -> Any:
|
||||
"""Safely get a value from a dict-like object."""
|
||||
if isinstance(d, dict):
|
||||
return d.get(key, default)
|
||||
return default
|
||||
|
||||
|
||||
def extract_block_doc(block_cls: type) -> BlockDoc:
|
||||
"""Extract documentation data from a block class."""
|
||||
block = block_cls.create()
|
||||
|
||||
# Get source file
|
||||
try:
|
||||
source_file = inspect.getfile(block_cls)
|
||||
# Make relative to blocks directory
|
||||
blocks_dir = Path(source_file).parent
|
||||
while blocks_dir.name != "blocks" and blocks_dir.parent != blocks_dir:
|
||||
blocks_dir = blocks_dir.parent
|
||||
source_file = str(Path(source_file).relative_to(blocks_dir.parent))
|
||||
except (TypeError, ValueError):
|
||||
source_file = "unknown"
|
||||
|
||||
# Extract input fields
|
||||
input_schema = block.input_schema.jsonschema()
|
||||
input_properties = safe_get(input_schema, "properties", {})
|
||||
if not isinstance(input_properties, dict):
|
||||
input_properties = {}
|
||||
required_raw = safe_get(input_schema, "required", [])
|
||||
# Handle edge cases where required might not be a list
|
||||
if isinstance(required_raw, (list, set, tuple)):
|
||||
required_inputs = set(required_raw)
|
||||
else:
|
||||
required_inputs = set()
|
||||
|
||||
inputs = []
|
||||
for field_name, field_schema in input_properties.items():
|
||||
if not isinstance(field_schema, dict):
|
||||
continue
|
||||
# Skip credentials fields in docs (they're auto-handled)
|
||||
if "credentials" in field_name.lower():
|
||||
continue
|
||||
|
||||
inputs.append(
|
||||
FieldDoc(
|
||||
name=field_name,
|
||||
description=safe_get(field_schema, "description", ""),
|
||||
type_str=type_to_readable(field_schema),
|
||||
required=field_name in required_inputs,
|
||||
default=safe_get(field_schema, "default"),
|
||||
advanced=safe_get(field_schema, "advanced", False) or False,
|
||||
hidden=safe_get(field_schema, "hidden", False) or False,
|
||||
placeholder=safe_get(field_schema, "placeholder"),
|
||||
)
|
||||
)
|
||||
|
||||
# Extract output fields
|
||||
output_schema = block.output_schema.jsonschema()
|
||||
output_properties = safe_get(output_schema, "properties", {})
|
||||
if not isinstance(output_properties, dict):
|
||||
output_properties = {}
|
||||
|
||||
outputs = []
|
||||
for field_name, field_schema in output_properties.items():
|
||||
if not isinstance(field_schema, dict):
|
||||
continue
|
||||
outputs.append(
|
||||
FieldDoc(
|
||||
name=field_name,
|
||||
description=safe_get(field_schema, "description", ""),
|
||||
type_str=type_to_readable(field_schema),
|
||||
required=True, # Outputs are always produced
|
||||
hidden=safe_get(field_schema, "hidden", False) or False,
|
||||
)
|
||||
)
|
||||
|
||||
# Get category info (sort for deterministic ordering since it's a set)
|
||||
categories = []
|
||||
category_descriptions = {}
|
||||
for cat in sorted(block.categories, key=lambda c: c.name):
|
||||
categories.append(cat.name)
|
||||
category_descriptions[cat.name] = cat.value
|
||||
|
||||
# Get contributors
|
||||
contributors = []
|
||||
for contrib in block.contributors:
|
||||
contributors.append(contrib.name if hasattr(contrib, "name") else str(contrib))
|
||||
|
||||
return BlockDoc(
|
||||
id=block.id,
|
||||
name=class_name_to_display_name(block.name),
|
||||
class_name=block.name,
|
||||
description=block.description,
|
||||
categories=categories,
|
||||
category_descriptions=category_descriptions,
|
||||
inputs=inputs,
|
||||
outputs=outputs,
|
||||
block_type=block.block_type.value,
|
||||
source_file=source_file,
|
||||
contributors=contributors,
|
||||
)
|
||||
|
||||
|
||||
def generate_anchor(name: str) -> str:
|
||||
"""Generate markdown anchor from block name."""
|
||||
return name.lower().replace(" ", "-").replace("(", "").replace(")", "")
|
||||
|
||||
|
||||
def extract_manual_content(existing_content: str) -> dict[str, str]:
|
||||
"""Extract content between MANUAL markers from existing file."""
|
||||
manual_sections = {}
|
||||
|
||||
# Pattern: <!-- MANUAL: section_name -->content<!-- END MANUAL -->
|
||||
pattern = r"<!-- MANUAL: (\w+) -->\s*(.*?)\s*<!-- END MANUAL -->"
|
||||
matches = re.findall(pattern, existing_content, re.DOTALL)
|
||||
|
||||
for section_name, content in matches:
|
||||
manual_sections[section_name] = content.strip()
|
||||
|
||||
return manual_sections
|
||||
|
||||
|
||||
def strip_markers(content: str) -> str:
|
||||
"""Remove MANUAL markers from content."""
|
||||
# Remove opening markers
|
||||
content = re.sub(r"<!-- MANUAL: \w+ -->\s*", "", content)
|
||||
# Remove closing markers
|
||||
content = re.sub(r"\s*<!-- END MANUAL -->", "", content)
|
||||
return content.strip()
|
||||
|
||||
|
||||
def extract_legacy_content(existing_content: str) -> dict[str, str]:
|
||||
"""Extract content from legacy docs without markers (for migration)."""
|
||||
manual_sections = {}
|
||||
|
||||
# Try to extract "How it works" section
|
||||
how_it_works_match = re.search(
|
||||
r"### How it works\s*\n(.*?)(?=\n### |\n## |\Z)", existing_content, re.DOTALL
|
||||
)
|
||||
if how_it_works_match:
|
||||
content = strip_markers(how_it_works_match.group(1).strip())
|
||||
if content and not content.startswith("|"): # Not a table
|
||||
manual_sections["how_it_works"] = content
|
||||
|
||||
# Try to extract "Possible use case" section
|
||||
use_case_match = re.search(
|
||||
r"### Possible use case\s*\n(.*?)(?=\n### |\n## |\n---|\Z)",
|
||||
existing_content,
|
||||
re.DOTALL,
|
||||
)
|
||||
if use_case_match:
|
||||
content = strip_markers(use_case_match.group(1).strip())
|
||||
if content:
|
||||
manual_sections["use_case"] = content
|
||||
|
||||
return manual_sections
|
||||
|
||||
|
||||
def generate_block_markdown(
|
||||
block: BlockDoc,
|
||||
manual_content: dict[str, str] | None = None,
|
||||
is_first_in_file: bool = True,
|
||||
) -> str:
|
||||
"""Generate markdown documentation for a single block."""
|
||||
manual_content = manual_content or {}
|
||||
lines = []
|
||||
|
||||
# Block heading
|
||||
heading_level = "#" if is_first_in_file else "##"
|
||||
lines.append(f"{heading_level} {block.name}")
|
||||
lines.append("")
|
||||
|
||||
# What it is (full description)
|
||||
lines.append("### What it is")
|
||||
lines.append(block.description or "No description available.")
|
||||
lines.append("")
|
||||
|
||||
# How it works (manual section)
|
||||
lines.append("### How it works")
|
||||
how_it_works = manual_content.get(
|
||||
"how_it_works", "_Add technical explanation here._"
|
||||
)
|
||||
lines.append("<!-- MANUAL: how_it_works -->")
|
||||
lines.append(how_it_works)
|
||||
lines.append("<!-- END MANUAL -->")
|
||||
lines.append("")
|
||||
|
||||
# Inputs table (auto-generated)
|
||||
visible_inputs = [f for f in block.inputs if not f.hidden]
|
||||
if visible_inputs:
|
||||
lines.append("### Inputs")
|
||||
lines.append("| Input | Description | Type | Required |")
|
||||
lines.append("|-------|-------------|------|----------|")
|
||||
for inp in visible_inputs:
|
||||
required = "Yes" if inp.required else "No"
|
||||
desc = inp.description or "-"
|
||||
# Escape pipes in description
|
||||
desc = desc.replace("|", "\\|")
|
||||
lines.append(f"| {inp.name} | {desc} | {inp.type_str} | {required} |")
|
||||
lines.append("")
|
||||
|
||||
# Outputs table (auto-generated)
|
||||
visible_outputs = [f for f in block.outputs if not f.hidden]
|
||||
if visible_outputs:
|
||||
lines.append("### Outputs")
|
||||
lines.append("| Output | Description | Type |")
|
||||
lines.append("|--------|-------------|------|")
|
||||
for out in visible_outputs:
|
||||
desc = out.description or "-"
|
||||
desc = desc.replace("|", "\\|")
|
||||
lines.append(f"| {out.name} | {desc} | {out.type_str} |")
|
||||
lines.append("")
|
||||
|
||||
# Possible use case (manual section)
|
||||
lines.append("### Possible use case")
|
||||
use_case = manual_content.get("use_case", "_Add practical use case examples here._")
|
||||
lines.append("<!-- MANUAL: use_case -->")
|
||||
lines.append(use_case)
|
||||
lines.append("<!-- END MANUAL -->")
|
||||
lines.append("")
|
||||
|
||||
lines.append("---")
|
||||
lines.append("")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def get_block_file_mapping(blocks: list[BlockDoc]) -> dict[str, list[BlockDoc]]:
|
||||
"""
|
||||
Map blocks to their documentation files.
|
||||
|
||||
Returns dict of {relative_file_path: [blocks]}
|
||||
"""
|
||||
file_mapping = defaultdict(list)
|
||||
|
||||
for block in blocks:
|
||||
# Determine file path based on source file or category
|
||||
source_path = Path(block.source_file)
|
||||
|
||||
# If source is in a subdirectory (e.g., google/gmail.py), use that structure
|
||||
if len(source_path.parts) > 2: # blocks/subdir/file.py
|
||||
subdir = source_path.parts[1] # e.g., "google"
|
||||
# Use the Python filename as the md filename
|
||||
md_file = source_path.stem + ".md" # e.g., "gmail.md"
|
||||
file_path = f"{subdir}/{md_file}"
|
||||
else:
|
||||
# Use category-based grouping for top-level blocks
|
||||
primary_category = block.categories[0] if block.categories else "BASIC"
|
||||
file_name = CATEGORY_FILE_MAP.get(primary_category, "misc")
|
||||
file_path = f"{file_name}.md"
|
||||
|
||||
file_mapping[file_path].append(block)
|
||||
|
||||
return dict(file_mapping)
|
||||
|
||||
|
||||
def generate_overview_table(blocks: list[BlockDoc]) -> str:
|
||||
"""Generate the overview table markdown (blocks.md)."""
|
||||
lines = []
|
||||
|
||||
lines.append("# AutoGPT Blocks Overview")
|
||||
lines.append("")
|
||||
lines.append(
|
||||
'AutoGPT uses a modular approach with various "blocks" to handle different tasks. These blocks are the building blocks of AutoGPT workflows, allowing users to create complex automations by combining simple, specialized components.'
|
||||
)
|
||||
lines.append("")
|
||||
lines.append('!!! info "Creating Your Own Blocks"')
|
||||
lines.append(" Want to create your own custom blocks? Check out our guides:")
|
||||
lines.append(" ")
|
||||
lines.append(
|
||||
" - [Build your own Blocks](../new_blocks.md) - Step-by-step tutorial with examples"
|
||||
)
|
||||
lines.append(
|
||||
" - [Block SDK Guide](../block-sdk-guide.md) - Advanced SDK patterns with OAuth, webhooks, and provider configuration"
|
||||
)
|
||||
lines.append("")
|
||||
lines.append(
|
||||
"Below is a comprehensive list of all available blocks, categorized by their primary function. Click on any block name to view its detailed documentation."
|
||||
)
|
||||
lines.append("")
|
||||
|
||||
# Group blocks by category
|
||||
by_category = defaultdict(list)
|
||||
for block in blocks:
|
||||
primary_cat = block.categories[0] if block.categories else "BASIC"
|
||||
by_category[primary_cat].append(block)
|
||||
|
||||
# Sort categories
|
||||
category_order = [
|
||||
"BASIC",
|
||||
"DATA",
|
||||
"TEXT",
|
||||
"AI",
|
||||
"SEARCH",
|
||||
"SOCIAL",
|
||||
"COMMUNICATION",
|
||||
"DEVELOPER_TOOLS",
|
||||
"MULTIMEDIA",
|
||||
"PRODUCTIVITY",
|
||||
"LOGIC",
|
||||
"INPUT",
|
||||
"OUTPUT",
|
||||
"AGENT",
|
||||
"CRM",
|
||||
"SAFETY",
|
||||
"ISSUE_TRACKING",
|
||||
"HARDWARE",
|
||||
"MARKETING",
|
||||
]
|
||||
|
||||
for category in category_order:
|
||||
if category not in by_category:
|
||||
continue
|
||||
|
||||
cat_blocks = sorted(by_category[category], key=lambda b: b.name)
|
||||
display_name = CATEGORY_DISPLAY_NAMES.get(category, category)
|
||||
|
||||
lines.append(f"## {display_name}")
|
||||
lines.append("| Block Name | Description |")
|
||||
lines.append("|------------|-------------|")
|
||||
|
||||
for block in cat_blocks:
|
||||
# Determine link path
|
||||
file_mapping = get_block_file_mapping([block])
|
||||
file_path = list(file_mapping.keys())[0]
|
||||
anchor = generate_anchor(block.name)
|
||||
|
||||
# Short description (first sentence)
|
||||
short_desc = (
|
||||
block.description.split(".")[0]
|
||||
if block.description
|
||||
else "No description"
|
||||
)
|
||||
short_desc = short_desc.replace("|", "\\|")
|
||||
|
||||
lines.append(f"| [{block.name}]({file_path}#{anchor}) | {short_desc} |")
|
||||
|
||||
lines.append("")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def load_all_blocks_for_docs() -> list[BlockDoc]:
|
||||
"""Load all blocks and extract documentation."""
|
||||
from backend.blocks import load_all_blocks
|
||||
|
||||
block_classes = load_all_blocks()
|
||||
blocks = []
|
||||
|
||||
for _block_id, block_cls in block_classes.items():
|
||||
try:
|
||||
block_doc = extract_block_doc(block_cls)
|
||||
blocks.append(block_doc)
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to extract docs for {block_cls.__name__}: {e}")
|
||||
|
||||
return blocks
|
||||
|
||||
|
||||
def write_block_docs(
|
||||
output_dir: Path,
|
||||
blocks: list[BlockDoc],
|
||||
migrate: bool = False,
|
||||
verbose: bool = False,
|
||||
) -> dict[str, str]:
|
||||
"""
|
||||
Write block documentation files.
|
||||
|
||||
Returns dict of {file_path: content} for all generated files.
|
||||
"""
|
||||
output_dir = Path(output_dir)
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
file_mapping = get_block_file_mapping(blocks)
|
||||
generated_files = {}
|
||||
|
||||
for file_path, file_blocks in file_mapping.items():
|
||||
full_path = output_dir / file_path
|
||||
|
||||
# Create subdirectories if needed
|
||||
full_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Load existing content for manual section preservation
|
||||
existing_content = ""
|
||||
if full_path.exists():
|
||||
existing_content = full_path.read_text()
|
||||
|
||||
# Generate content for each block
|
||||
content_parts = []
|
||||
for i, block in enumerate(sorted(file_blocks, key=lambda b: b.name)):
|
||||
# Try to extract manual content
|
||||
if migrate:
|
||||
manual_content = extract_legacy_content(existing_content)
|
||||
else:
|
||||
# Extract manual content specific to this block
|
||||
# Look for content after the block heading
|
||||
block_pattern = (
|
||||
rf"(?:^|\n)##? {re.escape(block.name)}\s*\n(.*?)(?=\n##? |\Z)"
|
||||
)
|
||||
block_match = re.search(block_pattern, existing_content, re.DOTALL)
|
||||
if block_match:
|
||||
manual_content = extract_manual_content(block_match.group(1))
|
||||
else:
|
||||
manual_content = {}
|
||||
|
||||
content_parts.append(
|
||||
generate_block_markdown(
|
||||
block,
|
||||
manual_content,
|
||||
is_first_in_file=(i == 0),
|
||||
)
|
||||
)
|
||||
|
||||
full_content = "\n".join(content_parts)
|
||||
generated_files[str(file_path)] = full_content
|
||||
|
||||
if verbose:
|
||||
print(f" Writing {file_path} ({len(file_blocks)} blocks)")
|
||||
|
||||
full_path.write_text(full_content)
|
||||
|
||||
# Generate overview file
|
||||
overview_content = generate_overview_table(blocks)
|
||||
overview_path = output_dir / "blocks.md"
|
||||
generated_files["blocks.md"] = overview_content
|
||||
overview_path.write_text(overview_content)
|
||||
|
||||
if verbose:
|
||||
print(" Writing blocks.md (overview)")
|
||||
|
||||
return generated_files
|
||||
|
||||
|
||||
def check_docs_in_sync(output_dir: Path, blocks: list[BlockDoc]) -> bool:
|
||||
"""
|
||||
Check if generated docs match existing docs.
|
||||
|
||||
Returns True if in sync, False otherwise.
|
||||
"""
|
||||
output_dir = Path(output_dir)
|
||||
file_mapping = get_block_file_mapping(blocks)
|
||||
|
||||
all_match = True
|
||||
|
||||
for file_path, file_blocks in file_mapping.items():
|
||||
full_path = output_dir / file_path
|
||||
|
||||
if not full_path.exists():
|
||||
print(f"MISSING: {file_path}")
|
||||
all_match = False
|
||||
continue
|
||||
|
||||
existing_content = full_path.read_text()
|
||||
|
||||
# Extract manual content from existing file
|
||||
manual_sections_by_block = {}
|
||||
for block in file_blocks:
|
||||
block_pattern = (
|
||||
rf"(?:^|\n)##? {re.escape(block.name)}\s*\n(.*?)(?=\n##? |\Z)"
|
||||
)
|
||||
block_match = re.search(block_pattern, existing_content, re.DOTALL)
|
||||
if block_match:
|
||||
manual_sections_by_block[block.name] = extract_manual_content(
|
||||
block_match.group(1)
|
||||
)
|
||||
|
||||
# Generate expected content
|
||||
content_parts = []
|
||||
for i, block in enumerate(sorted(file_blocks, key=lambda b: b.name)):
|
||||
manual_content = manual_sections_by_block.get(block.name, {})
|
||||
content_parts.append(
|
||||
generate_block_markdown(
|
||||
block,
|
||||
manual_content,
|
||||
is_first_in_file=(i == 0),
|
||||
)
|
||||
)
|
||||
|
||||
expected_content = "\n".join(content_parts)
|
||||
|
||||
if existing_content.strip() != expected_content.strip():
|
||||
print(f"OUT OF SYNC: {file_path}")
|
||||
all_match = False
|
||||
|
||||
# Check overview
|
||||
overview_path = output_dir / "blocks.md"
|
||||
if overview_path.exists():
|
||||
existing_overview = overview_path.read_text()
|
||||
expected_overview = generate_overview_table(blocks)
|
||||
if existing_overview.strip() != expected_overview.strip():
|
||||
print("OUT OF SYNC: blocks.md (overview)")
|
||||
all_match = False
|
||||
else:
|
||||
print("MISSING: blocks.md (overview)")
|
||||
all_match = False
|
||||
|
||||
return all_match
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Generate block documentation from code introspection"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--output-dir",
|
||||
type=Path,
|
||||
default=DEFAULT_OUTPUT_DIR,
|
||||
help="Output directory for generated docs",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--check",
|
||||
action="store_true",
|
||||
help="Check if docs are in sync (for CI), exit 1 if not",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--migrate",
|
||||
action="store_true",
|
||||
help="Migrate existing docs (extract legacy manual content)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-v",
|
||||
"--verbose",
|
||||
action="store_true",
|
||||
help="Verbose output",
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.DEBUG if args.verbose else logging.INFO,
|
||||
format="%(levelname)s: %(message)s",
|
||||
)
|
||||
|
||||
print("Loading blocks...")
|
||||
blocks = load_all_blocks_for_docs()
|
||||
print(f"Found {len(blocks)} blocks")
|
||||
|
||||
if args.check:
|
||||
print(f"Checking docs in {args.output_dir}...")
|
||||
in_sync = check_docs_in_sync(args.output_dir, blocks)
|
||||
if in_sync:
|
||||
print("All documentation is in sync!")
|
||||
sys.exit(0)
|
||||
else:
|
||||
print("\nDocumentation is out of sync!")
|
||||
print(
|
||||
"Run: cd autogpt_platform/backend && poetry run python scripts/generate_block_docs.py"
|
||||
)
|
||||
sys.exit(1)
|
||||
else:
|
||||
print(f"Generating docs to {args.output_dir}...")
|
||||
write_block_docs(
|
||||
args.output_dir,
|
||||
blocks,
|
||||
migrate=args.migrate,
|
||||
verbose=args.verbose,
|
||||
)
|
||||
print("Done!")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
211
autogpt_platform/backend/scripts/migrate_block_docs.py
Normal file
211
autogpt_platform/backend/scripts/migrate_block_docs.py
Normal file
@@ -0,0 +1,211 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Migration script to preserve manual content from existing docs.
|
||||
|
||||
This script:
|
||||
1. Reads all existing block documentation (from git HEAD)
|
||||
2. Extracts manual content (How it works, Possible use case) by block name
|
||||
3. Creates a JSON mapping of block_name -> manual_content
|
||||
4. Generates new docs using current block structure while preserving manual content
|
||||
"""
|
||||
|
||||
import json
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
|
||||
from scripts.generate_block_docs import (
|
||||
generate_block_markdown,
|
||||
generate_overview_table,
|
||||
get_block_file_mapping,
|
||||
load_all_blocks_for_docs,
|
||||
strip_markers,
|
||||
)
|
||||
|
||||
|
||||
def get_git_file_content(file_path: str) -> str | None:
|
||||
"""Get file content from git HEAD."""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["git", "show", f"HEAD:{file_path}"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
cwd=Path(__file__).parent.parent.parent.parent, # repo root
|
||||
)
|
||||
if result.returncode == 0:
|
||||
return result.stdout
|
||||
return None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def extract_blocks_from_doc(content: str) -> dict[str, dict[str, str]]:
|
||||
"""Extract all block sections and their manual content from a doc file."""
|
||||
blocks = {}
|
||||
|
||||
# Find all block headings (# or ##)
|
||||
block_pattern = r"(?:^|\n)(##?) ([^\n]+)\n"
|
||||
matches = list(re.finditer(block_pattern, content))
|
||||
|
||||
for i, match in enumerate(matches):
|
||||
block_name = match.group(2).strip()
|
||||
start = match.end()
|
||||
|
||||
# Find end (next heading or end of file)
|
||||
if i + 1 < len(matches):
|
||||
end = matches[i + 1].start()
|
||||
else:
|
||||
end = len(content)
|
||||
|
||||
block_content = content[start:end]
|
||||
|
||||
# Extract manual sections
|
||||
manual_content = {}
|
||||
|
||||
# How it works
|
||||
how_match = re.search(
|
||||
r"### How it works\s*\n(.*?)(?=\n### |\Z)", block_content, re.DOTALL
|
||||
)
|
||||
if how_match:
|
||||
text = strip_markers(how_match.group(1).strip())
|
||||
# Skip if it's just placeholder or a table
|
||||
if text and not text.startswith("|") and not text.startswith("_Add"):
|
||||
manual_content["how_it_works"] = text
|
||||
|
||||
# Possible use case
|
||||
use_case_match = re.search(
|
||||
r"### Possible use case\s*\n(.*?)(?=\n### |\n## |\n---|\Z)",
|
||||
block_content,
|
||||
re.DOTALL,
|
||||
)
|
||||
if use_case_match:
|
||||
text = strip_markers(use_case_match.group(1).strip())
|
||||
if text and not text.startswith("_Add"):
|
||||
manual_content["use_case"] = text
|
||||
|
||||
if manual_content:
|
||||
blocks[block_name] = manual_content
|
||||
|
||||
return blocks
|
||||
|
||||
|
||||
def collect_existing_manual_content() -> dict[str, dict[str, str]]:
|
||||
"""Collect all manual content from existing git HEAD docs."""
|
||||
all_manual_content = {}
|
||||
|
||||
# Find all existing md files via git
|
||||
result = subprocess.run(
|
||||
["git", "ls-files", "docs/content/platform/blocks/"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
cwd=Path(__file__).parent.parent.parent.parent,
|
||||
)
|
||||
|
||||
if result.returncode != 0:
|
||||
print("Failed to list git files")
|
||||
return {}
|
||||
|
||||
for file_path in result.stdout.strip().split("\n"):
|
||||
if not file_path.endswith(".md"):
|
||||
continue
|
||||
if file_path.endswith("blocks.md"): # Skip overview
|
||||
continue
|
||||
|
||||
print(f"Processing: {file_path}")
|
||||
content = get_git_file_content(file_path)
|
||||
if content:
|
||||
blocks = extract_blocks_from_doc(content)
|
||||
for block_name, manual_content in blocks.items():
|
||||
if block_name in all_manual_content:
|
||||
# Merge if already exists
|
||||
all_manual_content[block_name].update(manual_content)
|
||||
else:
|
||||
all_manual_content[block_name] = manual_content
|
||||
|
||||
return all_manual_content
|
||||
|
||||
|
||||
def run_migration():
|
||||
"""Run the migration."""
|
||||
print("Step 1: Collecting existing manual content from git HEAD...")
|
||||
manual_content_cache = collect_existing_manual_content()
|
||||
|
||||
print(f"\nFound manual content for {len(manual_content_cache)} blocks")
|
||||
|
||||
# Show some examples
|
||||
for name, content in list(manual_content_cache.items())[:3]:
|
||||
print(f" - {name}: {list(content.keys())}")
|
||||
|
||||
# Save cache for reference
|
||||
cache_path = Path(__file__).parent / "manual_content_cache.json"
|
||||
with open(cache_path, "w") as f:
|
||||
json.dump(manual_content_cache, f, indent=2)
|
||||
print(f"\nSaved cache to {cache_path}")
|
||||
|
||||
print("\nStep 2: Loading blocks from code...")
|
||||
blocks = load_all_blocks_for_docs()
|
||||
print(f"Found {len(blocks)} blocks")
|
||||
|
||||
print("\nStep 3: Generating new documentation...")
|
||||
output_dir = (
|
||||
Path(__file__).parent.parent.parent.parent
|
||||
/ "docs"
|
||||
/ "content"
|
||||
/ "platform"
|
||||
/ "blocks"
|
||||
)
|
||||
|
||||
file_mapping = get_block_file_mapping(blocks)
|
||||
|
||||
# Track statistics
|
||||
preserved_count = 0
|
||||
missing_count = 0
|
||||
|
||||
for file_path, file_blocks in file_mapping.items():
|
||||
full_path = output_dir / file_path
|
||||
full_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
content_parts = []
|
||||
for i, block in enumerate(sorted(file_blocks, key=lambda b: b.name)):
|
||||
# Look up manual content by block name
|
||||
manual_content = manual_content_cache.get(block.name, {})
|
||||
|
||||
if manual_content:
|
||||
preserved_count += 1
|
||||
else:
|
||||
# Try with class name
|
||||
manual_content = manual_content_cache.get(block.class_name, {})
|
||||
if not manual_content:
|
||||
missing_count += 1
|
||||
|
||||
content_parts.append(
|
||||
generate_block_markdown(
|
||||
block,
|
||||
manual_content,
|
||||
is_first_in_file=(i == 0),
|
||||
)
|
||||
)
|
||||
|
||||
full_content = "\n".join(content_parts)
|
||||
full_path.write_text(full_content)
|
||||
print(f" Wrote {file_path} ({len(file_blocks)} blocks)")
|
||||
|
||||
# Generate overview
|
||||
overview_content = generate_overview_table(blocks)
|
||||
overview_path = output_dir / "blocks.md"
|
||||
overview_path.write_text(overview_content)
|
||||
print(" Wrote blocks.md (overview)")
|
||||
|
||||
print("\nMigration complete!")
|
||||
print(f" - Blocks with preserved manual content: {preserved_count}")
|
||||
print(f" - Blocks without manual content: {missing_count}")
|
||||
print(
|
||||
"\nYou can now run `poetry run python scripts/generate_block_docs.py --check` to verify"
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
run_migration()
|
||||
233
autogpt_platform/backend/scripts/test_generate_block_docs.py
Normal file
233
autogpt_platform/backend/scripts/test_generate_block_docs.py
Normal file
@@ -0,0 +1,233 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Tests for the block documentation generator."""
|
||||
import pytest
|
||||
|
||||
from scripts.generate_block_docs import (
|
||||
class_name_to_display_name,
|
||||
extract_manual_content,
|
||||
generate_anchor,
|
||||
strip_markers,
|
||||
type_to_readable,
|
||||
)
|
||||
|
||||
|
||||
class TestClassNameToDisplayName:
|
||||
"""Tests for class_name_to_display_name function."""
|
||||
|
||||
def test_simple_block_name(self):
|
||||
assert class_name_to_display_name("PrintBlock") == "Print"
|
||||
|
||||
def test_multi_word_block_name(self):
|
||||
assert class_name_to_display_name("GetWeatherBlock") == "Get Weather"
|
||||
|
||||
def test_consecutive_capitals(self):
|
||||
assert class_name_to_display_name("HTTPRequestBlock") == "HTTP Request"
|
||||
|
||||
def test_ai_prefix(self):
|
||||
assert class_name_to_display_name("AIConditionBlock") == "AI Condition"
|
||||
|
||||
def test_no_block_suffix(self):
|
||||
assert class_name_to_display_name("SomeClass") == "Some Class"
|
||||
|
||||
|
||||
class TestTypeToReadable:
|
||||
"""Tests for type_to_readable function."""
|
||||
|
||||
def test_string_type(self):
|
||||
assert type_to_readable({"type": "string"}) == "str"
|
||||
|
||||
def test_integer_type(self):
|
||||
assert type_to_readable({"type": "integer"}) == "int"
|
||||
|
||||
def test_number_type(self):
|
||||
assert type_to_readable({"type": "number"}) == "float"
|
||||
|
||||
def test_boolean_type(self):
|
||||
assert type_to_readable({"type": "boolean"}) == "bool"
|
||||
|
||||
def test_array_type(self):
|
||||
result = type_to_readable({"type": "array", "items": {"type": "string"}})
|
||||
assert result == "List[str]"
|
||||
|
||||
def test_object_type(self):
|
||||
result = type_to_readable({"type": "object", "title": "MyModel"})
|
||||
assert result == "MyModel"
|
||||
|
||||
def test_anyof_with_null(self):
|
||||
result = type_to_readable({"anyOf": [{"type": "string"}, {"type": "null"}]})
|
||||
assert result == "str"
|
||||
|
||||
def test_anyof_multiple_types(self):
|
||||
result = type_to_readable({"anyOf": [{"type": "string"}, {"type": "integer"}]})
|
||||
assert result == "str | int"
|
||||
|
||||
def test_enum_type(self):
|
||||
result = type_to_readable(
|
||||
{"type": "string", "enum": ["option1", "option2", "option3"]}
|
||||
)
|
||||
assert result == '"option1" | "option2" | "option3"'
|
||||
|
||||
def test_none_input(self):
|
||||
assert type_to_readable(None) == "Any"
|
||||
|
||||
def test_non_dict_input(self):
|
||||
assert type_to_readable("string") == "string"
|
||||
|
||||
|
||||
class TestExtractManualContent:
|
||||
"""Tests for extract_manual_content function."""
|
||||
|
||||
def test_extract_how_it_works(self):
|
||||
content = """
|
||||
### How it works
|
||||
<!-- MANUAL: how_it_works -->
|
||||
This is how it works.
|
||||
<!-- END MANUAL -->
|
||||
"""
|
||||
result = extract_manual_content(content)
|
||||
assert result == {"how_it_works": "This is how it works."}
|
||||
|
||||
def test_extract_use_case(self):
|
||||
content = """
|
||||
### Possible use case
|
||||
<!-- MANUAL: use_case -->
|
||||
Example use case here.
|
||||
<!-- END MANUAL -->
|
||||
"""
|
||||
result = extract_manual_content(content)
|
||||
assert result == {"use_case": "Example use case here."}
|
||||
|
||||
def test_extract_multiple_sections(self):
|
||||
content = """
|
||||
<!-- MANUAL: how_it_works -->
|
||||
How it works content.
|
||||
<!-- END MANUAL -->
|
||||
|
||||
<!-- MANUAL: use_case -->
|
||||
Use case content.
|
||||
<!-- END MANUAL -->
|
||||
"""
|
||||
result = extract_manual_content(content)
|
||||
assert result == {
|
||||
"how_it_works": "How it works content.",
|
||||
"use_case": "Use case content.",
|
||||
}
|
||||
|
||||
def test_empty_content(self):
|
||||
result = extract_manual_content("")
|
||||
assert result == {}
|
||||
|
||||
def test_no_markers(self):
|
||||
result = extract_manual_content("Some content without markers")
|
||||
assert result == {}
|
||||
|
||||
|
||||
class TestStripMarkers:
|
||||
"""Tests for strip_markers function."""
|
||||
|
||||
def test_strip_opening_marker(self):
|
||||
content = "<!-- MANUAL: how_it_works -->\nContent here"
|
||||
result = strip_markers(content)
|
||||
assert result == "Content here"
|
||||
|
||||
def test_strip_closing_marker(self):
|
||||
content = "Content here\n<!-- END MANUAL -->"
|
||||
result = strip_markers(content)
|
||||
assert result == "Content here"
|
||||
|
||||
def test_strip_both_markers(self):
|
||||
content = "<!-- MANUAL: section -->\nContent here\n<!-- END MANUAL -->"
|
||||
result = strip_markers(content)
|
||||
assert result == "Content here"
|
||||
|
||||
def test_no_markers(self):
|
||||
content = "Content without markers"
|
||||
result = strip_markers(content)
|
||||
assert result == "Content without markers"
|
||||
|
||||
|
||||
class TestGenerateAnchor:
|
||||
"""Tests for generate_anchor function."""
|
||||
|
||||
def test_simple_name(self):
|
||||
assert generate_anchor("Print") == "print"
|
||||
|
||||
def test_multi_word_name(self):
|
||||
assert generate_anchor("Get Weather") == "get-weather"
|
||||
|
||||
def test_name_with_parentheses(self):
|
||||
assert generate_anchor("Something (Optional)") == "something-optional"
|
||||
|
||||
def test_already_lowercase(self):
|
||||
assert generate_anchor("already lowercase") == "already-lowercase"
|
||||
|
||||
|
||||
class TestIntegration:
|
||||
"""Integration tests that require block loading."""
|
||||
|
||||
def test_load_blocks(self):
|
||||
"""Test that blocks can be loaded successfully."""
|
||||
import logging
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
logging.disable(logging.CRITICAL)
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
|
||||
from scripts.generate_block_docs import load_all_blocks_for_docs
|
||||
|
||||
blocks = load_all_blocks_for_docs()
|
||||
assert len(blocks) > 0, "Should load at least one block"
|
||||
|
||||
def test_block_doc_has_required_fields(self):
|
||||
"""Test that extracted block docs have required fields."""
|
||||
import logging
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
logging.disable(logging.CRITICAL)
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
|
||||
from scripts.generate_block_docs import load_all_blocks_for_docs
|
||||
|
||||
blocks = load_all_blocks_for_docs()
|
||||
block = blocks[0]
|
||||
|
||||
assert hasattr(block, "id")
|
||||
assert hasattr(block, "name")
|
||||
assert hasattr(block, "description")
|
||||
assert hasattr(block, "categories")
|
||||
assert hasattr(block, "inputs")
|
||||
assert hasattr(block, "outputs")
|
||||
|
||||
def test_file_mapping_is_deterministic(self):
|
||||
"""Test that file mapping produces consistent results."""
|
||||
import logging
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
logging.disable(logging.CRITICAL)
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
|
||||
from scripts.generate_block_docs import (
|
||||
get_block_file_mapping,
|
||||
load_all_blocks_for_docs,
|
||||
)
|
||||
|
||||
# Load blocks twice and compare mappings
|
||||
blocks1 = load_all_blocks_for_docs()
|
||||
blocks2 = load_all_blocks_for_docs()
|
||||
|
||||
mapping1 = get_block_file_mapping(blocks1)
|
||||
mapping2 = get_block_file_mapping(blocks2)
|
||||
|
||||
# Check same files are generated
|
||||
assert set(mapping1.keys()) == set(mapping2.keys())
|
||||
|
||||
# Check same block counts per file
|
||||
for file_path in mapping1:
|
||||
assert len(mapping1[file_path]) == len(mapping2[file_path])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__, "-v"])
|
||||
@@ -3,13 +3,6 @@ import { withSentryConfig } from "@sentry/nextjs";
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
productionBrowserSourceMaps: true,
|
||||
// Externalize OpenTelemetry packages to fix Turbopack HMR issues
|
||||
serverExternalPackages: [
|
||||
"@opentelemetry/instrumentation",
|
||||
"@opentelemetry/sdk-node",
|
||||
"import-in-the-middle",
|
||||
"require-in-the-middle",
|
||||
],
|
||||
experimental: {
|
||||
serverActions: {
|
||||
bodySizeLimit: "256mb",
|
||||
|
||||
@@ -32,7 +32,6 @@
|
||||
"@hookform/resolvers": "5.2.2",
|
||||
"@next/third-parties": "15.4.6",
|
||||
"@phosphor-icons/react": "2.1.10",
|
||||
"@radix-ui/react-accordion": "1.2.12",
|
||||
"@radix-ui/react-alert-dialog": "1.1.15",
|
||||
"@radix-ui/react-avatar": "1.1.10",
|
||||
"@radix-ui/react-checkbox": "1.3.3",
|
||||
@@ -118,7 +117,6 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@chromatic-com/storybook": "4.1.2",
|
||||
"@opentelemetry/instrumentation": "0.209.0",
|
||||
"@playwright/test": "1.56.1",
|
||||
"@storybook/addon-a11y": "9.1.5",
|
||||
"@storybook/addon-docs": "9.1.5",
|
||||
@@ -142,7 +140,6 @@
|
||||
"eslint": "8.57.1",
|
||||
"eslint-config-next": "15.5.7",
|
||||
"eslint-plugin-storybook": "9.1.5",
|
||||
"import-in-the-middle": "2.0.2",
|
||||
"msw": "2.11.6",
|
||||
"msw-storybook-addon": "2.0.6",
|
||||
"orval": "7.13.0",
|
||||
@@ -150,7 +147,7 @@
|
||||
"postcss": "8.5.6",
|
||||
"prettier": "3.6.2",
|
||||
"prettier-plugin-tailwindcss": "0.7.1",
|
||||
"require-in-the-middle": "8.0.1",
|
||||
"require-in-the-middle": "7.5.2",
|
||||
"storybook": "9.1.5",
|
||||
"tailwindcss": "3.4.17",
|
||||
"typescript": "5.9.3"
|
||||
|
||||
108
autogpt_platform/frontend/pnpm-lock.yaml
generated
108
autogpt_platform/frontend/pnpm-lock.yaml
generated
@@ -20,9 +20,6 @@ importers:
|
||||
'@phosphor-icons/react':
|
||||
specifier: 2.1.10
|
||||
version: 2.1.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@radix-ui/react-accordion':
|
||||
specifier: 1.2.12
|
||||
version: 1.2.12(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@radix-ui/react-alert-dialog':
|
||||
specifier: 1.1.15
|
||||
version: 1.1.15(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
@@ -273,9 +270,6 @@ importers:
|
||||
'@chromatic-com/storybook':
|
||||
specifier: 4.1.2
|
||||
version: 4.1.2(storybook@9.1.5(@testing-library/dom@10.4.1)(msw@2.11.6(@types/node@24.10.0)(typescript@5.9.3))(prettier@3.6.2))
|
||||
'@opentelemetry/instrumentation':
|
||||
specifier: 0.209.0
|
||||
version: 0.209.0(@opentelemetry/api@1.9.0)
|
||||
'@playwright/test':
|
||||
specifier: 1.56.1
|
||||
version: 1.56.1
|
||||
@@ -345,9 +339,6 @@ importers:
|
||||
eslint-plugin-storybook:
|
||||
specifier: 9.1.5
|
||||
version: 9.1.5(eslint@8.57.1)(storybook@9.1.5(@testing-library/dom@10.4.1)(msw@2.11.6(@types/node@24.10.0)(typescript@5.9.3))(prettier@3.6.2))(typescript@5.9.3)
|
||||
import-in-the-middle:
|
||||
specifier: 2.0.2
|
||||
version: 2.0.2
|
||||
msw:
|
||||
specifier: 2.11.6
|
||||
version: 2.11.6(@types/node@24.10.0)(typescript@5.9.3)
|
||||
@@ -370,8 +361,8 @@ importers:
|
||||
specifier: 0.7.1
|
||||
version: 0.7.1(prettier@3.6.2)
|
||||
require-in-the-middle:
|
||||
specifier: 8.0.1
|
||||
version: 8.0.1
|
||||
specifier: 7.5.2
|
||||
version: 7.5.2
|
||||
storybook:
|
||||
specifier: 9.1.5
|
||||
version: 9.1.5(@testing-library/dom@10.4.1)(msw@2.11.6(@types/node@24.10.0)(typescript@5.9.3))(prettier@3.6.2)
|
||||
@@ -1556,10 +1547,6 @@ packages:
|
||||
resolution: {integrity: sha512-CjruKY9V6NMssL/T1kAFgzosF1v9o6oeN+aX5JB/C/xPNtmgIJqcXHG7fA82Ou1zCpWGl4lROQUKwUNE1pMCyg==}
|
||||
engines: {node: '>=8.0.0'}
|
||||
|
||||
'@opentelemetry/api-logs@0.209.0':
|
||||
resolution: {integrity: sha512-xomnUNi7TiAGtOgs0tb54LyrjRZLu9shJGGwkcN7NgtiPYOpNnKLkRJtzZvTjD/w6knSZH9sFZcUSUovYOPg6A==}
|
||||
engines: {node: '>=8.0.0'}
|
||||
|
||||
'@opentelemetry/api@1.9.0':
|
||||
resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==}
|
||||
engines: {node: '>=8.0.0'}
|
||||
@@ -1714,12 +1701,6 @@ packages:
|
||||
peerDependencies:
|
||||
'@opentelemetry/api': ^1.3.0
|
||||
|
||||
'@opentelemetry/instrumentation@0.209.0':
|
||||
resolution: {integrity: sha512-Cwe863ojTCnFlxVuuhG7s6ODkAOzKsAEthKAcI4MDRYz1OmGWYnmSl4X2pbyS+hBxVTdvfZePfoEA01IjqcEyw==}
|
||||
engines: {node: ^18.19.0 || >=20.6.0}
|
||||
peerDependencies:
|
||||
'@opentelemetry/api': ^1.3.0
|
||||
|
||||
'@opentelemetry/redis-common@0.38.2':
|
||||
resolution: {integrity: sha512-1BCcU93iwSRZvDAgwUxC/DV4T/406SkMfxGqu5ojc3AvNI+I9GhV7v0J1HljsczuuhcnFLYqD5VmwVXfCGHzxA==}
|
||||
engines: {node: ^18.19.0 || >=20.6.0}
|
||||
@@ -1829,19 +1810,6 @@ packages:
|
||||
'@radix-ui/primitive@1.1.3':
|
||||
resolution: {integrity: sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==}
|
||||
|
||||
'@radix-ui/react-accordion@1.2.12':
|
||||
resolution: {integrity: sha512-T4nygeh9YE9dLRPhAHSeOZi7HBXo+0kYIPJXayZfvWOWA0+n3dESrZbjfDPUABkUNym6Hd+f2IR113To8D2GPA==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
'@types/react-dom': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-alert-dialog@1.1.15':
|
||||
resolution: {integrity: sha512-oTVLkEw5GpdRe29BqJ0LSDFWI3qu0vR1M0mUkOQWDIUnY/QIkLpgDMWuKxP94c2NAC2LGcgVhG1ImF3jkZ5wXw==}
|
||||
peerDependencies:
|
||||
@@ -4989,8 +4957,8 @@ packages:
|
||||
resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
import-in-the-middle@2.0.2:
|
||||
resolution: {integrity: sha512-qet/hkGt3EbNGVtbDfPu0BM+tCqBS8wT1SYrstPaDKoWtshsC6licOemz7DVtpBEyvDNzo8UTBf9/GwWuSDZ9w==}
|
||||
import-in-the-middle@2.0.1:
|
||||
resolution: {integrity: sha512-bruMpJ7xz+9jwGzrwEhWgvRrlKRYCRDBrfU+ur3FcasYXLJDxTruJ//8g2Noj+QFyRBeqbpj8Bhn4Fbw6HjvhA==}
|
||||
|
||||
imurmurhash@0.1.4:
|
||||
resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==}
|
||||
@@ -6534,6 +6502,10 @@ packages:
|
||||
resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
require-in-the-middle@7.5.2:
|
||||
resolution: {integrity: sha512-gAZ+kLqBdHarXB64XpAe2VCjB7rIRv+mU8tfRWziHRJ5umKsIHN2tLLv6EtMw7WCdP19S0ERVMldNvxYCHnhSQ==}
|
||||
engines: {node: '>=8.6.0'}
|
||||
|
||||
require-in-the-middle@8.0.1:
|
||||
resolution: {integrity: sha512-QT7FVMXfWOYFbeRBF6nu+I6tr2Tf3u0q8RIEjNob/heKY/nh7drD/k7eeMFmSQgnTtCzLDcCu/XEnpW2wk4xCQ==}
|
||||
engines: {node: '>=9.3.0 || >=8.10.0 <9.0.0'}
|
||||
@@ -8748,10 +8720,6 @@ snapshots:
|
||||
dependencies:
|
||||
'@opentelemetry/api': 1.9.0
|
||||
|
||||
'@opentelemetry/api-logs@0.209.0':
|
||||
dependencies:
|
||||
'@opentelemetry/api': 1.9.0
|
||||
|
||||
'@opentelemetry/api@1.9.0': {}
|
||||
|
||||
'@opentelemetry/context-async-hooks@2.2.0(@opentelemetry/api@1.9.0)':
|
||||
@@ -8952,16 +8920,7 @@ snapshots:
|
||||
dependencies:
|
||||
'@opentelemetry/api': 1.9.0
|
||||
'@opentelemetry/api-logs': 0.208.0
|
||||
import-in-the-middle: 2.0.2
|
||||
require-in-the-middle: 8.0.1
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@opentelemetry/instrumentation@0.209.0(@opentelemetry/api@1.9.0)':
|
||||
dependencies:
|
||||
'@opentelemetry/api': 1.9.0
|
||||
'@opentelemetry/api-logs': 0.209.0
|
||||
import-in-the-middle: 2.0.2
|
||||
import-in-the-middle: 2.0.1
|
||||
require-in-the-middle: 8.0.1
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
@@ -9141,7 +9100,7 @@ snapshots:
|
||||
'@prisma/instrumentation@6.19.0(@opentelemetry/api@1.9.0)':
|
||||
dependencies:
|
||||
'@opentelemetry/api': 1.9.0
|
||||
'@opentelemetry/instrumentation': 0.209.0(@opentelemetry/api@1.9.0)
|
||||
'@opentelemetry/instrumentation': 0.208.0(@opentelemetry/api@1.9.0)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
@@ -9149,23 +9108,6 @@ snapshots:
|
||||
|
||||
'@radix-ui/primitive@1.1.3': {}
|
||||
|
||||
'@radix-ui/react-accordion@1.2.12(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
||||
dependencies:
|
||||
'@radix-ui/primitive': 1.1.3
|
||||
'@radix-ui/react-collapsible': 1.1.12(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@radix-ui/react-collection': 1.1.7(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.17)(react@18.3.1)
|
||||
'@radix-ui/react-context': 1.1.2(@types/react@18.3.17)(react@18.3.1)
|
||||
'@radix-ui/react-direction': 1.1.1(@types/react@18.3.17)(react@18.3.1)
|
||||
'@radix-ui/react-id': 1.1.1(@types/react@18.3.17)(react@18.3.1)
|
||||
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.17)(react@18.3.1)
|
||||
react: 18.3.1
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
optionalDependencies:
|
||||
'@types/react': 18.3.17
|
||||
'@types/react-dom': 18.3.5(@types/react@18.3.17)
|
||||
|
||||
'@radix-ui/react-alert-dialog@1.1.15(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
||||
dependencies:
|
||||
'@radix-ui/primitive': 1.1.3
|
||||
@@ -10002,7 +9944,7 @@ snapshots:
|
||||
'@opentelemetry/semantic-conventions': 1.38.0
|
||||
'@sentry/core': 10.27.0
|
||||
'@sentry/opentelemetry': 10.27.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.38.0)
|
||||
import-in-the-middle: 2.0.2
|
||||
import-in-the-middle: 2.0.1
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
@@ -10041,7 +9983,7 @@ snapshots:
|
||||
'@sentry/core': 10.27.0
|
||||
'@sentry/node-core': 10.27.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.208.0(@opentelemetry/api@1.9.0))(@opentelemetry/resources@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.38.0)
|
||||
'@sentry/opentelemetry': 10.27.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.38.0)
|
||||
import-in-the-middle: 2.0.2
|
||||
import-in-the-middle: 2.0.1
|
||||
minimatch: 9.0.5
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
@@ -12128,8 +12070,8 @@ snapshots:
|
||||
'@typescript-eslint/parser': 8.52.0(eslint@8.57.1)(typescript@5.9.3)
|
||||
eslint: 8.57.1
|
||||
eslint-import-resolver-node: 0.3.9
|
||||
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.52.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1)
|
||||
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.52.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.52.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1)
|
||||
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@8.57.1)
|
||||
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.52.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1)
|
||||
eslint-plugin-jsx-a11y: 6.10.2(eslint@8.57.1)
|
||||
eslint-plugin-react: 7.37.5(eslint@8.57.1)
|
||||
eslint-plugin-react-hooks: 5.2.0(eslint@8.57.1)
|
||||
@@ -12148,7 +12090,7 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.52.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1):
|
||||
eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@8.57.1):
|
||||
dependencies:
|
||||
'@nolyfill/is-core-module': 1.0.39
|
||||
debug: 4.4.3
|
||||
@@ -12159,22 +12101,22 @@ snapshots:
|
||||
tinyglobby: 0.2.15
|
||||
unrs-resolver: 1.11.1
|
||||
optionalDependencies:
|
||||
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.52.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.52.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1)
|
||||
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.52.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
eslint-module-utils@2.12.1(@typescript-eslint/parser@8.52.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.52.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1):
|
||||
eslint-module-utils@2.12.1(@typescript-eslint/parser@8.52.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1):
|
||||
dependencies:
|
||||
debug: 3.2.7
|
||||
optionalDependencies:
|
||||
'@typescript-eslint/parser': 8.52.0(eslint@8.57.1)(typescript@5.9.3)
|
||||
eslint: 8.57.1
|
||||
eslint-import-resolver-node: 0.3.9
|
||||
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.52.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1)
|
||||
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@8.57.1)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.52.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.52.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1):
|
||||
eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.52.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1):
|
||||
dependencies:
|
||||
'@rtsao/scc': 1.1.0
|
||||
array-includes: 3.1.9
|
||||
@@ -12185,7 +12127,7 @@ snapshots:
|
||||
doctrine: 2.1.0
|
||||
eslint: 8.57.1
|
||||
eslint-import-resolver-node: 0.3.9
|
||||
eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.52.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.52.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1)
|
||||
eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.52.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1)
|
||||
hasown: 2.0.2
|
||||
is-core-module: 2.16.1
|
||||
is-glob: 4.0.3
|
||||
@@ -12850,7 +12792,7 @@ snapshots:
|
||||
parent-module: 1.0.1
|
||||
resolve-from: 4.0.0
|
||||
|
||||
import-in-the-middle@2.0.2:
|
||||
import-in-the-middle@2.0.1:
|
||||
dependencies:
|
||||
acorn: 8.15.0
|
||||
acorn-import-attributes: 1.9.5(acorn@8.15.0)
|
||||
@@ -14689,6 +14631,14 @@ snapshots:
|
||||
|
||||
require-from-string@2.0.2: {}
|
||||
|
||||
require-in-the-middle@7.5.2:
|
||||
dependencies:
|
||||
debug: 4.4.3
|
||||
module-details-from-path: 1.0.4
|
||||
resolve: 1.22.11
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
require-in-the-middle@8.0.1:
|
||||
dependencies:
|
||||
debug: 4.4.3
|
||||
|
||||
@@ -68,10 +68,7 @@ export const NodeHeader = ({ data, nodeId }: Props) => {
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div>
|
||||
<Text
|
||||
variant="large-semibold"
|
||||
className="line-clamp-1 hover:cursor-text"
|
||||
>
|
||||
<Text variant="large-semibold" className="line-clamp-1">
|
||||
{beautifyString(title).replace("Block", "").trim()}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
@@ -89,18 +89,6 @@ export function extractOptions(
|
||||
|
||||
// get display type and color for schema types [need for type display next to field name]
|
||||
export const getTypeDisplayInfo = (schema: any) => {
|
||||
if (
|
||||
schema?.type === "array" &&
|
||||
"format" in schema &&
|
||||
schema.format === "table"
|
||||
) {
|
||||
return {
|
||||
displayType: "table",
|
||||
colorClass: "!text-indigo-500",
|
||||
hexColor: "#6366f1",
|
||||
};
|
||||
}
|
||||
|
||||
if (schema?.type === "string" && schema?.format) {
|
||||
const formatMap: Record<
|
||||
string,
|
||||
|
||||
@@ -1,31 +1,32 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@/components/atoms/Button/Button";
|
||||
import { PublishAgentModal } from "@/components/contextual/PublishAgentModal/PublishAgentModal";
|
||||
import { Breadcrumbs } from "@/components/molecules/Breadcrumbs/Breadcrumbs";
|
||||
import { ErrorCard } from "@/components/molecules/ErrorCard/ErrorCard";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { PlusIcon } from "@phosphor-icons/react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { AgentVersionChangelog } from "./components/AgentVersionChangelog";
|
||||
import { AgentSettingsModal } from "./components/modals/AgentSettingsModal/AgentSettingsModal";
|
||||
import { RunAgentModal } from "./components/modals/RunAgentModal/RunAgentModal";
|
||||
import { useMarketplaceUpdate } from "./hooks/useMarketplaceUpdate";
|
||||
import { AgentVersionChangelog } from "./components/AgentVersionChangelog";
|
||||
import { MarketplaceBanners } from "@/components/contextual/MarketplaceBanners/MarketplaceBanners";
|
||||
import { PublishAgentModal } from "@/components/contextual/PublishAgentModal/PublishAgentModal";
|
||||
import { AgentSettingsButton } from "./components/other/AgentSettingsButton";
|
||||
import { AgentRunsLoading } from "./components/other/AgentRunsLoading";
|
||||
import { EmptySchedules } from "./components/other/EmptySchedules";
|
||||
import { EmptyTasks } from "./components/other/EmptyTasks";
|
||||
import { EmptyTemplates } from "./components/other/EmptyTemplates";
|
||||
import { EmptyTriggers } from "./components/other/EmptyTriggers";
|
||||
import { MarketplaceBanners } from "./components/other/MarketplaceBanners";
|
||||
import { SectionWrap } from "./components/other/SectionWrap";
|
||||
import { LoadingSelectedContent } from "./components/selected-views/LoadingSelectedContent";
|
||||
import { SelectedRunView } from "./components/selected-views/SelectedRunView/SelectedRunView";
|
||||
import { SelectedScheduleView } from "./components/selected-views/SelectedScheduleView/SelectedScheduleView";
|
||||
import { SelectedSettingsView } from "./components/selected-views/SelectedSettingsView/SelectedSettingsView";
|
||||
import { SelectedTemplateView } from "./components/selected-views/SelectedTemplateView/SelectedTemplateView";
|
||||
import { SelectedTriggerView } from "./components/selected-views/SelectedTriggerView/SelectedTriggerView";
|
||||
import { SelectedViewLayout } from "./components/selected-views/SelectedViewLayout";
|
||||
import { SidebarRunsList } from "./components/sidebar/SidebarRunsList/SidebarRunsList";
|
||||
import { AGENT_LIBRARY_SECTION_PADDING_X } from "./helpers";
|
||||
import { useMarketplaceUpdate } from "./hooks/useMarketplaceUpdate";
|
||||
import { useNewAgentLibraryView } from "./useNewAgentLibraryView";
|
||||
|
||||
export function NewAgentLibraryView() {
|
||||
@@ -44,6 +45,7 @@ export function NewAgentLibraryView() {
|
||||
handleSelectRun,
|
||||
handleCountsChange,
|
||||
handleClearSelectedRun,
|
||||
handleSelectSettings,
|
||||
onRunInitiated,
|
||||
onTriggerSetup,
|
||||
onScheduleCreated,
|
||||
@@ -135,16 +137,13 @@ export function NewAgentLibraryView() {
|
||||
return (
|
||||
<>
|
||||
<div className="flex h-full flex-col">
|
||||
<div className="mx-6 flex flex-col gap-4 pt-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<Breadcrumbs
|
||||
items={[
|
||||
{ name: "My Library", link: "/library" },
|
||||
{ name: agent.name, link: `/library/agents/${agentId}` },
|
||||
]}
|
||||
/>
|
||||
<AgentSettingsModal agent={agent} />
|
||||
</div>
|
||||
<div className="mx-6 pt-4">
|
||||
<Breadcrumbs
|
||||
items={[
|
||||
{ name: "My Library", link: "/library" },
|
||||
{ name: agent.name, link: `/library/agents/${agentId}` },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex min-h-0 flex-1">
|
||||
<EmptyTasks
|
||||
@@ -171,24 +170,31 @@ export function NewAgentLibraryView() {
|
||||
AGENT_LIBRARY_SECTION_PADDING_X,
|
||||
)}
|
||||
>
|
||||
<RunAgentModal
|
||||
triggerSlot={
|
||||
<Button
|
||||
variant="primary"
|
||||
size="large"
|
||||
className="w-full"
|
||||
disabled={isTemplateLoading && activeTab === "templates"}
|
||||
>
|
||||
<PlusIcon size={20} /> New task
|
||||
</Button>
|
||||
}
|
||||
agent={agent}
|
||||
onRunCreated={onRunInitiated}
|
||||
onScheduleCreated={onScheduleCreated}
|
||||
onTriggerSetup={onTriggerSetup}
|
||||
initialInputValues={activeTemplate?.inputs}
|
||||
initialInputCredentials={activeTemplate?.credentials}
|
||||
/>
|
||||
<div className="flex items-center gap-2">
|
||||
<RunAgentModal
|
||||
triggerSlot={
|
||||
<Button
|
||||
variant="primary"
|
||||
size="large"
|
||||
className="flex-1"
|
||||
disabled={isTemplateLoading && activeTab === "templates"}
|
||||
>
|
||||
<PlusIcon size={20} /> New task
|
||||
</Button>
|
||||
}
|
||||
agent={agent}
|
||||
onRunCreated={onRunInitiated}
|
||||
onScheduleCreated={onScheduleCreated}
|
||||
onTriggerSetup={onTriggerSetup}
|
||||
initialInputValues={activeTemplate?.inputs}
|
||||
initialInputCredentials={activeTemplate?.credentials}
|
||||
/>
|
||||
<AgentSettingsButton
|
||||
agent={agent}
|
||||
onSelectSettings={handleSelectSettings}
|
||||
selected={activeItem === "settings"}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SidebarRunsList
|
||||
@@ -202,7 +208,12 @@ export function NewAgentLibraryView() {
|
||||
</SectionWrap>
|
||||
|
||||
{activeItem ? (
|
||||
activeTab === "scheduled" ? (
|
||||
activeItem === "settings" ? (
|
||||
<SelectedSettingsView
|
||||
agent={agent}
|
||||
onClearSelectedRun={handleClearSelectedRun}
|
||||
/>
|
||||
) : activeTab === "scheduled" ? (
|
||||
<SelectedScheduleView
|
||||
agent={agent}
|
||||
scheduleId={activeItem}
|
||||
@@ -235,6 +246,8 @@ export function NewAgentLibraryView() {
|
||||
onSelectRun={handleSelectRun}
|
||||
onClearSelectedRun={handleClearSelectedRun}
|
||||
banner={renderMarketplaceUpdateBanner()}
|
||||
onSelectSettings={handleSelectSettings}
|
||||
selectedSettings={activeItem === "settings"}
|
||||
/>
|
||||
)
|
||||
) : sidebarLoading ? (
|
||||
|
||||
@@ -4,7 +4,6 @@ import type { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
|
||||
import { Text } from "@/components/atoms/Text/Text";
|
||||
import type { CredentialsMetaInput } from "@/lib/autogpt-server-api/types";
|
||||
import { CredentialsInput } from "../CredentialsInputs/CredentialsInputs";
|
||||
import { isSystemCredential } from "../CredentialsInputs/helpers";
|
||||
import { RunAgentInputs } from "../RunAgentInputs/RunAgentInputs";
|
||||
import { getAgentCredentialsFields, getAgentInputFields } from "./helpers";
|
||||
|
||||
@@ -72,7 +71,6 @@ export function AgentInputsReadOnly({
|
||||
{credentialFieldEntries.map(([key, inputSubSchema]) => {
|
||||
const credential = credentialInputs![key];
|
||||
if (!credential) return null;
|
||||
if (isSystemCredential(credential)) return null;
|
||||
|
||||
return (
|
||||
<CredentialsInput
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
|
||||
import { Button } from "@/components/atoms/Button/Button";
|
||||
import { Switch } from "@/components/atoms/Switch/Switch";
|
||||
import { Text } from "@/components/atoms/Text/Text";
|
||||
import { Dialog } from "@/components/molecules/Dialog/Dialog";
|
||||
import { useAgentSafeMode } from "@/hooks/useAgentSafeMode";
|
||||
import { GearIcon } from "@phosphor-icons/react";
|
||||
import { useState } from "react";
|
||||
|
||||
interface Props {
|
||||
agent: LibraryAgent;
|
||||
controlledOpen?: boolean;
|
||||
onOpenChange?: (open: boolean) => void;
|
||||
}
|
||||
|
||||
export function AgentSettingsModal({
|
||||
agent,
|
||||
controlledOpen,
|
||||
onOpenChange,
|
||||
}: Props) {
|
||||
const [internalIsOpen, setInternalIsOpen] = useState(false);
|
||||
const isOpen = controlledOpen !== undefined ? controlledOpen : internalIsOpen;
|
||||
|
||||
function setIsOpen(open: boolean) {
|
||||
if (onOpenChange) {
|
||||
onOpenChange(open);
|
||||
} else {
|
||||
setInternalIsOpen(open);
|
||||
}
|
||||
}
|
||||
|
||||
const { currentSafeMode, isPending, hasHITLBlocks, handleToggle } =
|
||||
useAgentSafeMode(agent);
|
||||
|
||||
if (!hasHITLBlocks) return null;
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
controlled={{ isOpen, set: setIsOpen }}
|
||||
styling={{ maxWidth: "600px", maxHeight: "90vh" }}
|
||||
title="Agent Settings"
|
||||
>
|
||||
{controlledOpen === undefined && (
|
||||
<Dialog.Trigger>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="small"
|
||||
className="m-0 min-w-0 rounded-full p-0 px-1"
|
||||
aria-label="Agent Settings"
|
||||
>
|
||||
<GearIcon size={18} className="text-zinc-600" />
|
||||
<Text variant="small">Agent Settings</Text>
|
||||
</Button>
|
||||
</Dialog.Trigger>
|
||||
)}
|
||||
<Dialog.Content>
|
||||
<div className="space-y-6">
|
||||
<div className="flex w-full flex-col items-start gap-4 rounded-xl border border-zinc-100 bg-white p-6">
|
||||
<div className="flex w-full items-start justify-between gap-4">
|
||||
<div className="flex-1">
|
||||
<Text variant="large-semibold">Require human approval</Text>
|
||||
<Text variant="large" className="mt-1 text-zinc-900">
|
||||
The agent will pause and wait for your review before
|
||||
continuing
|
||||
</Text>
|
||||
</div>
|
||||
<Switch
|
||||
checked={currentSafeMode || false}
|
||||
onCheckedChange={handleToggle}
|
||||
disabled={isPending}
|
||||
className="mt-1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog.Content>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -1,11 +1,5 @@
|
||||
import { Button } from "@/components/atoms/Button/Button";
|
||||
import { Text } from "@/components/atoms/Text/Text";
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
} from "@/components/molecules/Accordion/Accordion";
|
||||
import { InformationTooltip } from "@/components/molecules/InformationTooltip/InformationTooltip";
|
||||
import {
|
||||
BlockIOCredentialsSubSchema,
|
||||
@@ -13,14 +7,14 @@ import {
|
||||
} from "@/lib/autogpt-server-api/types";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { toDisplayName } from "@/providers/agent-credentials/helper";
|
||||
import { SlidersHorizontalIcon } from "lucide-react";
|
||||
import { APIKeyCredentialsModal } from "./components/APIKeyCredentialsModal/APIKeyCredentialsModal";
|
||||
import { CredentialRow } from "./components/CredentialRow/CredentialRow";
|
||||
import { CredentialsSelect } from "./components/CredentialsSelect/CredentialsSelect";
|
||||
import { DeleteConfirmationModal } from "./components/DeleteConfirmationModal/DeleteConfirmationModal";
|
||||
import { HostScopedCredentialsModal } from "./components/HotScopedCredentialsModal/HotScopedCredentialsModal";
|
||||
import { OAuthFlowWaitingModal } from "./components/OAuthWaitingModal/OAuthWaitingModal";
|
||||
import { PasswordCredentialsModal } from "./components/PasswordCredentialsModal/PasswordCredentialsModal";
|
||||
import { isSystemCredential } from "./helpers";
|
||||
import { getCredentialDisplayName } from "./helpers";
|
||||
import {
|
||||
CredentialsInputState,
|
||||
useCredentialsInput,
|
||||
@@ -42,8 +36,6 @@ type Props = {
|
||||
readOnly?: boolean;
|
||||
isOptional?: boolean;
|
||||
showTitle?: boolean;
|
||||
variant?: "default" | "node";
|
||||
collapseSystemCredentials?: boolean;
|
||||
};
|
||||
|
||||
export function CredentialsInput({
|
||||
@@ -56,8 +48,6 @@ export function CredentialsInput({
|
||||
readOnly = false,
|
||||
isOptional = false,
|
||||
showTitle = true,
|
||||
variant = "default",
|
||||
collapseSystemCredentials = false,
|
||||
}: Props) {
|
||||
const hookData = useCredentialsInput({
|
||||
schema,
|
||||
@@ -80,63 +70,32 @@ export function CredentialsInput({
|
||||
supportsOAuth2,
|
||||
supportsUserPassword,
|
||||
supportsHostScoped,
|
||||
isSystemProvider,
|
||||
userCredentials,
|
||||
systemCredentials,
|
||||
credentialsToShow,
|
||||
oAuthError,
|
||||
isAPICredentialsModalOpen,
|
||||
isUserPasswordCredentialsModalOpen,
|
||||
isHostScopedCredentialsModalOpen,
|
||||
isOAuth2FlowInProgress,
|
||||
oAuthPopupController,
|
||||
credentialToDelete,
|
||||
deleteCredentialsMutation,
|
||||
actionButtonText,
|
||||
setAPICredentialsModalOpen,
|
||||
setUserPasswordCredentialsModalOpen,
|
||||
setHostScopedCredentialsModalOpen,
|
||||
setCredentialToDelete,
|
||||
handleActionButtonClick,
|
||||
handleCredentialSelect,
|
||||
handleDeleteCredential,
|
||||
handleDeleteConfirm,
|
||||
} = hookData;
|
||||
|
||||
const displayName = toDisplayName(provider);
|
||||
const selectedCredentialIsSystem =
|
||||
selectedCredential && isSystemCredential(selectedCredential);
|
||||
|
||||
const allCredentials = [...userCredentials, ...systemCredentials];
|
||||
|
||||
// When collapseSystemCredentials is true AND provider is a system provider,
|
||||
// collapse ALL credentials (both user and system) under the accordion.
|
||||
// This keeps the provider section clean when platform credits are available.
|
||||
const shouldCollapseAll = collapseSystemCredentials && isSystemProvider;
|
||||
|
||||
// Determine which credentials to show in main section
|
||||
const credentialsToShow = shouldCollapseAll
|
||||
? [] // All credentials go to accordion when provider has system creds
|
||||
: collapseSystemCredentials
|
||||
? userCredentials // No system creds, show user creds normally
|
||||
: allCredentials; // Show all when not collapsing
|
||||
|
||||
const hasCredentialsToShow = credentialsToShow.length > 0;
|
||||
|
||||
// Credentials to show in accordion
|
||||
const credentialsToCollapse = shouldCollapseAll
|
||||
? allCredentials // All credentials collapsed when provider has system creds
|
||||
: collapseSystemCredentials
|
||||
? systemCredentials // Only system creds collapsed
|
||||
: [];
|
||||
|
||||
const hasCredentialsToCollapse = credentialsToCollapse.length > 0;
|
||||
|
||||
// If required and no credential selected, keep accordion open
|
||||
const shouldOpenAccordionByDefault =
|
||||
shouldCollapseAll && !isOptional && !selectedCredential;
|
||||
|
||||
if (readOnly && selectedCredentialIsSystem) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn("mb-6", className)}>
|
||||
{showTitle && !shouldCollapseAll && (
|
||||
{showTitle && (
|
||||
<div className="mb-2 flex items-center gap-2">
|
||||
<Text variant="large-medium">
|
||||
{displayName} credentials
|
||||
@@ -164,23 +123,33 @@ export function CredentialsInput({
|
||||
onClearCredential={() => onSelectCredential(undefined)}
|
||||
readOnly={readOnly}
|
||||
allowNone={isOptional}
|
||||
variant={variant}
|
||||
/>
|
||||
) : (
|
||||
<div className="mb-4 space-y-2">
|
||||
{credentialsToShow.map((credential) => (
|
||||
<CredentialRow
|
||||
key={credential.id}
|
||||
credential={credential}
|
||||
provider={provider}
|
||||
displayName={displayName}
|
||||
onSelect={() => handleCredentialSelect(credential.id)}
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
))}
|
||||
{credentialsToShow.map((credential) => {
|
||||
return (
|
||||
<CredentialRow
|
||||
key={credential.id}
|
||||
credential={credential}
|
||||
provider={provider}
|
||||
displayName={displayName}
|
||||
onSelect={() => handleCredentialSelect(credential.id)}
|
||||
onDelete={() =>
|
||||
handleDeleteCredential({
|
||||
id: credential.id,
|
||||
title: getCredentialDisplayName(
|
||||
credential,
|
||||
displayName,
|
||||
),
|
||||
})
|
||||
}
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
{!readOnly && !shouldCollapseAll && (
|
||||
{!readOnly && (
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="small"
|
||||
@@ -193,8 +162,7 @@ export function CredentialsInput({
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
!readOnly &&
|
||||
!shouldCollapseAll && (
|
||||
!readOnly && (
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="small"
|
||||
@@ -207,87 +175,6 @@ export function CredentialsInput({
|
||||
)
|
||||
)}
|
||||
|
||||
{shouldCollapseAll && !readOnly && (
|
||||
<Accordion
|
||||
type="single"
|
||||
collapsible
|
||||
defaultValue={
|
||||
shouldOpenAccordionByDefault ? "system-credentials" : undefined
|
||||
}
|
||||
>
|
||||
<AccordionItem value="system-credentials" className="border-none">
|
||||
<AccordionTrigger className="py-2 text-sm text-muted-foreground hover:no-underline">
|
||||
<div className="flex items-center gap-1">
|
||||
<SlidersHorizontalIcon className="size-4" /> System credentials
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<div className="space-y-4 px-1 pt-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Text variant="large-medium">
|
||||
{displayName} credentials
|
||||
{isOptional && (
|
||||
<span className="ml-1 text-sm font-normal text-gray-500">
|
||||
(optional)
|
||||
</span>
|
||||
)}
|
||||
</Text>
|
||||
{schema.description && (
|
||||
<InformationTooltip description={schema.description} />
|
||||
)}
|
||||
</div>
|
||||
{credentialsToCollapse.length > 0 && (
|
||||
<CredentialsSelect
|
||||
credentials={credentialsToCollapse}
|
||||
provider={provider}
|
||||
displayName={displayName}
|
||||
selectedCredentials={selectedCredential}
|
||||
onSelectCredential={handleCredentialSelect}
|
||||
onClearCredential={() => onSelectCredential(undefined)}
|
||||
readOnly={readOnly}
|
||||
allowNone={isOptional}
|
||||
variant={variant}
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="small"
|
||||
onClick={handleActionButtonClick}
|
||||
className="w-fit"
|
||||
type="button"
|
||||
>
|
||||
{actionButtonText}
|
||||
</Button>
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
)}
|
||||
|
||||
{hasCredentialsToCollapse && !shouldCollapseAll && !readOnly && (
|
||||
<Accordion type="single" collapsible className="mt-4">
|
||||
<AccordionItem value="system-credentials" className="border-none">
|
||||
<AccordionTrigger className="py-2 text-sm text-muted-foreground hover:no-underline">
|
||||
System credentials
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<div className="space-y-2 pt-2">
|
||||
{credentialsToCollapse.map((credential) => (
|
||||
<CredentialRow
|
||||
key={credential.id}
|
||||
credential={credential}
|
||||
provider={provider}
|
||||
displayName={displayName}
|
||||
onSelect={() => handleCredentialSelect(credential.id)}
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
)}
|
||||
|
||||
{!readOnly && (
|
||||
<>
|
||||
{supportsApiKey ? (
|
||||
@@ -339,6 +226,13 @@ export function CredentialsInput({
|
||||
Error: {oAuthError}
|
||||
</Text>
|
||||
) : null}
|
||||
|
||||
<DeleteConfirmationModal
|
||||
credentialToDelete={credentialToDelete}
|
||||
isDeleting={deleteCredentialsMutation.isPending}
|
||||
onClose={() => setCredentialToDelete(null)}
|
||||
onConfirm={handleDeleteConfirm}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { Input } from "@/components/atoms/Input/Input";
|
||||
import { Button } from "@/components/atoms/Button/Button";
|
||||
import { Dialog } from "@/components/molecules/Dialog/Dialog";
|
||||
import {
|
||||
Form,
|
||||
FormDescription,
|
||||
FormField,
|
||||
} from "@/components/__legacy__/ui/form";
|
||||
import { Button } from "@/components/atoms/Button/Button";
|
||||
import { Input } from "@/components/atoms/Input/Input";
|
||||
import { Dialog } from "@/components/molecules/Dialog/Dialog";
|
||||
import {
|
||||
BlockIOCredentialsSubSchema,
|
||||
CredentialsMetaInput,
|
||||
@@ -60,10 +60,7 @@ export function APIKeyCredentialsModal({
|
||||
)}
|
||||
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="space-y-2 px-2"
|
||||
>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-2">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="apiKey"
|
||||
@@ -73,7 +70,8 @@ export function APIKeyCredentialsModal({
|
||||
id="apiKey"
|
||||
label="API Key"
|
||||
type="password"
|
||||
placeholder="Enter API Key..."
|
||||
placeholder="Enter API key..."
|
||||
size="small"
|
||||
hint={
|
||||
schema.credentials_scopes ? (
|
||||
<FormDescription>
|
||||
@@ -100,7 +98,8 @@ export function APIKeyCredentialsModal({
|
||||
id="title"
|
||||
label="Name"
|
||||
type="text"
|
||||
placeholder="Enter a name for this API Key..."
|
||||
placeholder="Enter a name for this API key..."
|
||||
size="small"
|
||||
{...field}
|
||||
/>
|
||||
)}
|
||||
@@ -114,12 +113,13 @@ export function APIKeyCredentialsModal({
|
||||
label="Expiration Date"
|
||||
type="datetime-local"
|
||||
placeholder="Select expiration date..."
|
||||
size="small"
|
||||
{...field}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Button type="submit" className="min-w-68">
|
||||
Add API Key
|
||||
<Button type="submit" size="small" className="min-w-68">
|
||||
Save & use this API key
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
|
||||
@@ -26,12 +26,10 @@ type CredentialRowProps = {
|
||||
provider: string;
|
||||
displayName: string;
|
||||
onSelect: () => void;
|
||||
onDelete?: () => void;
|
||||
onDelete: () => void;
|
||||
readOnly?: boolean;
|
||||
showCaret?: boolean;
|
||||
asSelectTrigger?: boolean;
|
||||
/** When "node", applies compact styling for node context */
|
||||
variant?: "default" | "node";
|
||||
};
|
||||
|
||||
export function CredentialRow({
|
||||
@@ -43,22 +41,14 @@ export function CredentialRow({
|
||||
readOnly = false,
|
||||
showCaret = false,
|
||||
asSelectTrigger = false,
|
||||
variant = "default",
|
||||
}: CredentialRowProps) {
|
||||
const ProviderIcon = providerIcons[provider] || fallbackIcon;
|
||||
const isNodeVariant = variant === "node";
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center gap-3 rounded-medium border border-zinc-200 bg-white p-3 transition-colors",
|
||||
asSelectTrigger && isNodeVariant
|
||||
? "min-w-0 flex-1 overflow-hidden border-0 bg-transparent"
|
||||
: asSelectTrigger
|
||||
? "border-0 bg-transparent"
|
||||
: readOnly
|
||||
? "w-fit"
|
||||
: "",
|
||||
asSelectTrigger ? "border-0 bg-transparent" : readOnly ? "w-fit" : "",
|
||||
)}
|
||||
onClick={readOnly || showCaret || asSelectTrigger ? undefined : onSelect}
|
||||
style={
|
||||
@@ -71,36 +61,24 @@ export function CredentialRow({
|
||||
<ProviderIcon className="h-3 w-3 text-white" />
|
||||
</div>
|
||||
<IconKey className="h-5 w-5 shrink-0 text-zinc-800" />
|
||||
<div
|
||||
className={cn(
|
||||
"flex min-w-0 flex-1 flex-nowrap items-center gap-4",
|
||||
isNodeVariant && "overflow-hidden",
|
||||
)}
|
||||
>
|
||||
<div className="flex min-w-0 flex-1 flex-nowrap items-center gap-4">
|
||||
<Text
|
||||
variant="body"
|
||||
className={cn(
|
||||
"tracking-tight",
|
||||
isNodeVariant
|
||||
? "truncate"
|
||||
: "line-clamp-1 flex-[0_0_50%] text-ellipsis",
|
||||
)}
|
||||
className="line-clamp-1 flex-[0_0_50%] text-ellipsis tracking-tight"
|
||||
>
|
||||
{getCredentialDisplayName(credential, displayName)}
|
||||
</Text>
|
||||
{!(asSelectTrigger && isNodeVariant) && (
|
||||
<Text
|
||||
variant="large"
|
||||
className="relative top-1 hidden overflow-hidden whitespace-nowrap font-mono tracking-tight md:block"
|
||||
>
|
||||
{"*".repeat(MASKED_KEY_LENGTH)}
|
||||
</Text>
|
||||
)}
|
||||
<Text
|
||||
variant="large"
|
||||
className="lex-[0_0_40%] relative top-1 hidden overflow-hidden whitespace-nowrap font-mono tracking-tight md:block"
|
||||
>
|
||||
{"*".repeat(MASKED_KEY_LENGTH)}
|
||||
</Text>
|
||||
</div>
|
||||
{showCaret && !asSelectTrigger && (
|
||||
<CaretDown className="h-4 w-4 shrink-0 text-gray-400" />
|
||||
)}
|
||||
{!readOnly && !showCaret && !asSelectTrigger && onDelete && (
|
||||
{!readOnly && !showCaret && !asSelectTrigger && (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
} from "@/components/__legacy__/ui/select";
|
||||
import { Text } from "@/components/atoms/Text/Text";
|
||||
import { CredentialsMetaInput } from "@/lib/autogpt-server-api/types";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useEffect } from "react";
|
||||
import { getCredentialDisplayName } from "../../helpers";
|
||||
import { CredentialRow } from "../CredentialRow/CredentialRow";
|
||||
@@ -27,8 +26,6 @@ interface Props {
|
||||
onClearCredential?: () => void;
|
||||
readOnly?: boolean;
|
||||
allowNone?: boolean;
|
||||
/** When "node", applies compact styling for node context */
|
||||
variant?: "default" | "node";
|
||||
}
|
||||
|
||||
export function CredentialsSelect({
|
||||
@@ -40,7 +37,6 @@ export function CredentialsSelect({
|
||||
onClearCredential,
|
||||
readOnly = false,
|
||||
allowNone = true,
|
||||
variant = "default",
|
||||
}: Props) {
|
||||
// Auto-select first credential if none is selected (only if allowNone is false)
|
||||
useEffect(() => {
|
||||
@@ -63,12 +59,7 @@ export function CredentialsSelect({
|
||||
value={selectedCredentials?.id || (allowNone ? "__none__" : "")}
|
||||
onValueChange={handleValueChange}
|
||||
>
|
||||
<SelectTrigger
|
||||
className={cn(
|
||||
"h-auto min-h-12 w-full rounded-medium p-0 pr-4 shadow-none",
|
||||
variant === "node" && "overflow-hidden",
|
||||
)}
|
||||
>
|
||||
<SelectTrigger className="h-auto min-h-12 w-full rounded-medium border-zinc-200 p-0 pr-4 shadow-none">
|
||||
{selectedCredentials ? (
|
||||
<SelectValue key={selectedCredentials.id} asChild>
|
||||
<CredentialRow
|
||||
@@ -84,42 +75,8 @@ export function CredentialsSelect({
|
||||
onDelete={() => {}}
|
||||
readOnly={readOnly}
|
||||
asSelectTrigger={true}
|
||||
variant={variant}
|
||||
/>
|
||||
</SelectValue>
|
||||
) : allowNone ? (
|
||||
<SelectValue key="__none__" asChild>
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center gap-3 rounded-medium border border-zinc-200 bg-white p-3 transition-colors",
|
||||
variant === "node"
|
||||
? "min-w-0 flex-1 overflow-hidden border-0 bg-transparent"
|
||||
: "border-0 bg-transparent",
|
||||
)}
|
||||
>
|
||||
<div className="flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-zinc-200">
|
||||
<Text
|
||||
variant="body"
|
||||
className="text-xs font-medium text-zinc-500"
|
||||
>
|
||||
—
|
||||
</Text>
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
"flex min-w-0 flex-1 flex-nowrap items-center gap-4",
|
||||
variant === "node" && "overflow-hidden",
|
||||
)}
|
||||
>
|
||||
<Text
|
||||
variant="body"
|
||||
className={cn("tracking-tight text-zinc-500")}
|
||||
>
|
||||
None (skip this credential)
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
</SelectValue>
|
||||
) : (
|
||||
<SelectValue key="placeholder" placeholder="Select credential" />
|
||||
)}
|
||||
|
||||
@@ -100,29 +100,3 @@ export function getCredentialDisplayName(
|
||||
|
||||
export const OAUTH_TIMEOUT_MS = 5 * 60 * 1000;
|
||||
export const MASKED_KEY_LENGTH = 30;
|
||||
|
||||
export function isSystemCredential(credential: {
|
||||
title?: string | null;
|
||||
is_system?: boolean;
|
||||
}): boolean {
|
||||
if (credential.is_system === true) return true;
|
||||
if (!credential.title) return false;
|
||||
const titleLower = credential.title.toLowerCase();
|
||||
return (
|
||||
titleLower.includes("system") ||
|
||||
titleLower.startsWith("use credits for") ||
|
||||
titleLower.includes("use credits")
|
||||
);
|
||||
}
|
||||
|
||||
export function filterSystemCredentials<
|
||||
T extends { title?: string; is_system?: boolean },
|
||||
>(credentials: T[]): T[] {
|
||||
return credentials.filter((cred) => !isSystemCredential(cred));
|
||||
}
|
||||
|
||||
export function getSystemCredentials<
|
||||
T extends { title?: string; is_system?: boolean },
|
||||
>(credentials: T[]): T[] {
|
||||
return credentials.filter((cred) => isSystemCredential(cred));
|
||||
}
|
||||
|
||||
@@ -6,11 +6,9 @@ import {
|
||||
CredentialsMetaInput,
|
||||
} from "@/lib/autogpt-server-api/types";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import {
|
||||
filterSystemCredentials,
|
||||
getActionButtonText,
|
||||
getSystemCredentials,
|
||||
OAUTH_TIMEOUT_MS,
|
||||
OAuthPopupResultMessage,
|
||||
} from "./helpers";
|
||||
@@ -56,7 +54,6 @@ export function useCredentialsInput({
|
||||
const api = useBackendAPI();
|
||||
const queryClient = useQueryClient();
|
||||
const credentials = useCredentials(schema, siblingInputs);
|
||||
const hasAttemptedAutoSelect = useRef(false);
|
||||
|
||||
const deleteCredentialsMutation = useDeleteV1DeleteCredentials({
|
||||
mutation: {
|
||||
@@ -85,10 +82,9 @@ export function useCredentialsInput({
|
||||
useEffect(() => {
|
||||
if (readOnly) return;
|
||||
if (!credentials || !("savedCredentials" in credentials)) return;
|
||||
const availableCreds = credentials.savedCredentials;
|
||||
if (
|
||||
selectedCredential &&
|
||||
!availableCreds.some((c) => c.id === selectedCredential.id)
|
||||
!credentials.savedCredentials.some((c) => c.id === selectedCredential.id)
|
||||
) {
|
||||
onSelectCredential(undefined);
|
||||
}
|
||||
@@ -100,109 +96,24 @@ export function useCredentialsInput({
|
||||
return null;
|
||||
}
|
||||
|
||||
const credsToUse = filterSystemCredentials(credentials.savedCredentials);
|
||||
return credsToUse.length === 1 ? credsToUse[0] : null;
|
||||
return credentials.savedCredentials.length === 1
|
||||
? credentials.savedCredentials[0]
|
||||
: null;
|
||||
}, [credentials]);
|
||||
|
||||
// Auto-select the one available credential
|
||||
// Prioritize system credentials if available
|
||||
// For system credentials, always auto-select even if optional (they should be used by default)
|
||||
// Auto-select the one available credential (only if not optional)
|
||||
useEffect(() => {
|
||||
if (readOnly) return;
|
||||
if (!credentials || !("savedCredentials" in credentials)) return;
|
||||
|
||||
// Early return if already selected to prevent infinite loops
|
||||
const currentSelectedId = selectedCredential?.id;
|
||||
if (currentSelectedId) {
|
||||
hasAttemptedAutoSelect.current = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// If selectedCredential is explicitly undefined and isOptional is true,
|
||||
// don't auto-select - this could mean "None" was explicitly selected
|
||||
// The parent component should handle setting the initial value
|
||||
if (selectedCredential === undefined && isOptional) {
|
||||
// Mark as attempted to prevent auto-selection when "None" is a valid choice
|
||||
hasAttemptedAutoSelect.current = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// Only attempt auto-selection once per credential load
|
||||
if (hasAttemptedAutoSelect.current) return;
|
||||
|
||||
const supportedTypes = schema.credentials_types || [];
|
||||
const requiredScopes = schema.credentials_scopes;
|
||||
const savedCreds = credentials.savedCredentials;
|
||||
const systemCreds = getSystemCredentials(savedCreds);
|
||||
|
||||
// Filter system credentials by type and scopes (same logic as useCredentials)
|
||||
const matchingSystemCreds = systemCreds.filter((cred) => {
|
||||
// Check type match
|
||||
if (!supportedTypes.includes(cred.type)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// For OAuth2 credentials, check scopes
|
||||
if (
|
||||
cred.type === "oauth2" &&
|
||||
requiredScopes &&
|
||||
requiredScopes.length > 0
|
||||
) {
|
||||
const grantedScopes = new Set(cred.scopes || []);
|
||||
const hasAllRequiredScopes = new Set(requiredScopes).isSubsetOf(
|
||||
grantedScopes,
|
||||
);
|
||||
if (!hasAllRequiredScopes) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
// First, try to auto-select system credential if available
|
||||
if (matchingSystemCreds.length === 1) {
|
||||
const systemCred = matchingSystemCreds[0];
|
||||
const credProvider = credentials.provider;
|
||||
hasAttemptedAutoSelect.current = true;
|
||||
onSelectCredential({
|
||||
id: systemCred.id,
|
||||
type: systemCred.type,
|
||||
provider: credProvider,
|
||||
title: (systemCred as any).title,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise, auto-select single credential if there's only one (and not optional)
|
||||
if (!isOptional && singleCredential) {
|
||||
hasAttemptedAutoSelect.current = true;
|
||||
if (isOptional) return; // Don't auto-select when credential is optional
|
||||
if (singleCredential && !selectedCredential) {
|
||||
onSelectCredential(singleCredential);
|
||||
}
|
||||
}, [
|
||||
singleCredential?.id, // Only depend on the ID, not the whole object
|
||||
selectedCredential?.id, // Only depend on the ID, not the whole object
|
||||
singleCredential,
|
||||
selectedCredential,
|
||||
onSelectCredential,
|
||||
readOnly,
|
||||
isOptional,
|
||||
credentials,
|
||||
schema.credentials_types,
|
||||
schema.credentials_scopes,
|
||||
// Note: onSelectCredential removed from deps to prevent infinite loops
|
||||
// It should be stable, but if it's not, the ref will prevent multiple calls
|
||||
]);
|
||||
|
||||
// Reset the ref when credentials change significantly
|
||||
useEffect(() => {
|
||||
if (credentials && "savedCredentials" in credentials) {
|
||||
hasAttemptedAutoSelect.current = false;
|
||||
}
|
||||
}, [
|
||||
credentials && "savedCredentials" in credentials
|
||||
? credentials.savedCredentials.length
|
||||
: 0,
|
||||
credentials && "savedCredentials" in credentials
|
||||
? credentials.provider
|
||||
: null,
|
||||
]);
|
||||
|
||||
if (
|
||||
@@ -224,13 +135,8 @@ export function useCredentialsInput({
|
||||
supportsHostScoped,
|
||||
savedCredentials,
|
||||
oAuthCallback,
|
||||
isSystemProvider,
|
||||
} = credentials;
|
||||
|
||||
// Split credentials into user and system
|
||||
const userCredentials = filterSystemCredentials(savedCredentials);
|
||||
const systemCredentials = getSystemCredentials(savedCredentials);
|
||||
|
||||
async function handleOAuthLogin() {
|
||||
setOAuthError(null);
|
||||
const { login_url, state_token } = await api.oAuthLogin(
|
||||
@@ -385,10 +291,7 @@ export function useCredentialsInput({
|
||||
supportsOAuth2,
|
||||
supportsUserPassword,
|
||||
supportsHostScoped,
|
||||
isSystemProvider,
|
||||
userCredentials,
|
||||
systemCredentials,
|
||||
allCredentials: savedCredentials,
|
||||
credentialsToShow: savedCredentials,
|
||||
selectedCredential,
|
||||
oAuthError,
|
||||
isAPICredentialsModalOpen,
|
||||
@@ -403,7 +306,7 @@ export function useCredentialsInput({
|
||||
supportsApiKey,
|
||||
supportsUserPassword,
|
||||
supportsHostScoped,
|
||||
userCredentials.length > 0,
|
||||
savedCredentials.length > 0,
|
||||
),
|
||||
setAPICredentialsModalOpen,
|
||||
setUserPasswordCredentialsModalOpen,
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
TooltipTrigger,
|
||||
} from "@/components/atoms/Tooltip/BaseTooltip";
|
||||
import { Dialog } from "@/components/molecules/Dialog/Dialog";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useState } from "react";
|
||||
import { ScheduleAgentModal } from "../ScheduleAgentModal/ScheduleAgentModal";
|
||||
import { ModalHeader } from "./components/ModalHeader/ModalHeader";
|
||||
import { ModalRunSection } from "./components/ModalRunSection/ModalRunSection";
|
||||
@@ -82,8 +82,6 @@ export function RunAgentModal({
|
||||
});
|
||||
|
||||
const [isScheduleModalOpen, setIsScheduleModalOpen] = useState(false);
|
||||
const [hasOverflow, setHasOverflow] = useState(false);
|
||||
const contentRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const hasAnySetupFields =
|
||||
Object.keys(agentInputFields || {}).length > 0 ||
|
||||
@@ -91,43 +89,6 @@ export function RunAgentModal({
|
||||
|
||||
const isTriggerRunType = defaultRunType.includes("trigger");
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen) return;
|
||||
|
||||
function checkOverflow() {
|
||||
if (!contentRef.current) return;
|
||||
const scrollableParent = contentRef.current
|
||||
.closest("[data-dialog-content]")
|
||||
?.querySelector('[class*="overflow-y-auto"]');
|
||||
if (scrollableParent) {
|
||||
setHasOverflow(
|
||||
scrollableParent.scrollHeight > scrollableParent.clientHeight,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const timeoutId = setTimeout(checkOverflow, 100);
|
||||
const resizeObserver = new ResizeObserver(checkOverflow);
|
||||
if (contentRef.current) {
|
||||
const scrollableParent = contentRef.current
|
||||
.closest("[data-dialog-content]")
|
||||
?.querySelector('[class*="overflow-y-auto"]');
|
||||
if (scrollableParent) {
|
||||
resizeObserver.observe(scrollableParent);
|
||||
}
|
||||
}
|
||||
|
||||
return () => {
|
||||
clearTimeout(timeoutId);
|
||||
resizeObserver.disconnect();
|
||||
};
|
||||
}, [
|
||||
isOpen,
|
||||
hasAnySetupFields,
|
||||
agentInputFields,
|
||||
agentCredentialsInputFields,
|
||||
]);
|
||||
|
||||
function handleInputChange(key: string, value: string) {
|
||||
setInputValues((prev) => ({
|
||||
...prev,
|
||||
@@ -173,97 +134,91 @@ export function RunAgentModal({
|
||||
>
|
||||
<Dialog.Trigger>{triggerSlot}</Dialog.Trigger>
|
||||
<Dialog.Content>
|
||||
<div ref={contentRef} className="flex min-h-full flex-col">
|
||||
<div className="flex-1">
|
||||
{/* Header */}
|
||||
<ModalHeader agent={agent} />
|
||||
{/* Header */}
|
||||
<ModalHeader agent={agent} />
|
||||
|
||||
{/* Content */}
|
||||
{hasAnySetupFields ? (
|
||||
<div className="mt-10 pb-32">
|
||||
<RunAgentModalContextProvider
|
||||
value={{
|
||||
agent,
|
||||
defaultRunType,
|
||||
presetName,
|
||||
setPresetName,
|
||||
presetDescription,
|
||||
setPresetDescription,
|
||||
inputValues,
|
||||
setInputValue: handleInputChange,
|
||||
agentInputFields,
|
||||
inputCredentials,
|
||||
setInputCredentialsValue: handleCredentialsChange,
|
||||
agentCredentialsInputFields,
|
||||
}}
|
||||
>
|
||||
<ModalRunSection />
|
||||
</RunAgentModalContextProvider>
|
||||
</div>
|
||||
) : null}
|
||||
{/* Content */}
|
||||
{hasAnySetupFields ? (
|
||||
<div className="mt-10">
|
||||
<RunAgentModalContextProvider
|
||||
value={{
|
||||
agent,
|
||||
defaultRunType,
|
||||
presetName,
|
||||
setPresetName,
|
||||
presetDescription,
|
||||
setPresetDescription,
|
||||
inputValues,
|
||||
setInputValue: handleInputChange,
|
||||
agentInputFields,
|
||||
inputCredentials,
|
||||
setInputCredentialsValue: handleCredentialsChange,
|
||||
agentCredentialsInputFields,
|
||||
}}
|
||||
>
|
||||
<ModalRunSection />
|
||||
</RunAgentModalContextProvider>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<Dialog.Footer
|
||||
className={`sticky bottom-0 z-10 bg-white pt-4 ${
|
||||
hasOverflow
|
||||
? "border-t border-neutral-100 shadow-[0_-2px_8px_rgba(0,0,0,0.04)]"
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center justify-end gap-3">
|
||||
{isTriggerRunType ? null : !allRequiredInputsAreSet ? (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={handleOpenScheduleModal}
|
||||
disabled={
|
||||
isExecuting ||
|
||||
isSettingUpTrigger ||
|
||||
!allRequiredInputsAreSet
|
||||
}
|
||||
>
|
||||
Schedule Task
|
||||
</Button>
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>
|
||||
Please set up all required inputs and credentials
|
||||
before scheduling
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
) : (
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={handleOpenScheduleModal}
|
||||
disabled={isExecuting || isSettingUpTrigger}
|
||||
>
|
||||
Schedule Task
|
||||
</Button>
|
||||
)}
|
||||
<RunActions
|
||||
defaultRunType={defaultRunType}
|
||||
onRun={handleRun}
|
||||
isExecuting={isExecuting}
|
||||
isSettingUpTrigger={isSettingUpTrigger}
|
||||
isRunReady={allRequiredInputsAreSet}
|
||||
/>
|
||||
</div>
|
||||
<ScheduleAgentModal
|
||||
isOpen={isScheduleModalOpen}
|
||||
onClose={handleCloseScheduleModal}
|
||||
agent={agent}
|
||||
inputValues={inputValues}
|
||||
inputCredentials={inputCredentials}
|
||||
onScheduleCreated={handleScheduleCreated}
|
||||
<Dialog.Footer className="mt-6 bg-white pt-4">
|
||||
<div className="flex items-center justify-end gap-3">
|
||||
{isTriggerRunType ? null : !allRequiredInputsAreSet ? (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={handleOpenScheduleModal}
|
||||
disabled={
|
||||
isExecuting ||
|
||||
isSettingUpTrigger ||
|
||||
!allRequiredInputsAreSet
|
||||
}
|
||||
>
|
||||
Schedule Task
|
||||
</Button>
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>
|
||||
Please set up all required inputs and credentials before
|
||||
scheduling
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
) : (
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={handleOpenScheduleModal}
|
||||
disabled={
|
||||
isExecuting ||
|
||||
isSettingUpTrigger ||
|
||||
!allRequiredInputsAreSet
|
||||
}
|
||||
>
|
||||
Schedule Task
|
||||
</Button>
|
||||
)}
|
||||
<RunActions
|
||||
defaultRunType={defaultRunType}
|
||||
onRun={handleRun}
|
||||
isExecuting={isExecuting}
|
||||
isSettingUpTrigger={isSettingUpTrigger}
|
||||
isRunReady={allRequiredInputsAreSet}
|
||||
/>
|
||||
</Dialog.Footer>
|
||||
</div>
|
||||
</div>
|
||||
<ScheduleAgentModal
|
||||
isOpen={isScheduleModalOpen}
|
||||
onClose={handleCloseScheduleModal}
|
||||
agent={agent}
|
||||
inputValues={inputValues}
|
||||
inputCredentials={inputCredentials}
|
||||
onScheduleCreated={handleScheduleCreated}
|
||||
/>
|
||||
</Dialog.Footer>
|
||||
</Dialog.Content>
|
||||
</Dialog>
|
||||
</>
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { CredentialsInput } from "@/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/modals/CredentialsInputs/CredentialsInputs";
|
||||
import { Input } from "@/components/atoms/Input/Input";
|
||||
import { InformationTooltip } from "@/components/molecules/InformationTooltip/InformationTooltip";
|
||||
import { CredentialsProvidersContext } from "@/providers/agent-credentials/credentials-provider";
|
||||
import { useContext, useMemo } from "react";
|
||||
import { RunAgentInputs } from "../../../RunAgentInputs/RunAgentInputs";
|
||||
import { useRunAgentModalContext } from "../../context";
|
||||
import { ModalSection } from "../ModalSection/ModalSection";
|
||||
@@ -24,45 +22,10 @@ export function ModalRunSection() {
|
||||
agentCredentialsInputFields,
|
||||
} = useRunAgentModalContext();
|
||||
|
||||
const allProviders = useContext(CredentialsProvidersContext);
|
||||
|
||||
const inputFields = Object.entries(agentInputFields || {});
|
||||
const credentialFields = Object.entries(agentCredentialsInputFields || {});
|
||||
|
||||
// Sort credential fields: user credentials first, system credentials at the bottom
|
||||
const sortedCredentialFields = useMemo(() => {
|
||||
if (!allProviders || !agentCredentialsInputFields) return [];
|
||||
|
||||
const entries = Object.entries(agentCredentialsInputFields);
|
||||
|
||||
return entries.sort(([_keyA, schemaA], [_keyB, schemaB]) => {
|
||||
const providerNamesA = schemaA.credentials_provider || [];
|
||||
const providerNamesB = schemaB.credentials_provider || [];
|
||||
|
||||
// Check if A has system credentials
|
||||
const aHasSystemCred = providerNamesA.some((providerName: string) => {
|
||||
const providerData = allProviders[providerName];
|
||||
if (!providerData) return false;
|
||||
return providerData.savedCredentials.some(
|
||||
(cred: { is_system?: boolean }) => cred.is_system === true,
|
||||
);
|
||||
});
|
||||
|
||||
// Check if B has system credentials
|
||||
const bHasSystemCred = providerNamesB.some((providerName: string) => {
|
||||
const providerData = allProviders[providerName];
|
||||
if (!providerData) return false;
|
||||
return providerData.savedCredentials.some(
|
||||
(cred: { is_system?: boolean }) => cred.is_system === true,
|
||||
);
|
||||
});
|
||||
|
||||
// User credentials first, system credentials at the bottom
|
||||
if (aHasSystemCred && !bHasSystemCred) return 1;
|
||||
if (!aHasSystemCred && bHasSystemCred) return -1;
|
||||
return 0;
|
||||
});
|
||||
}, [agentCredentialsInputFields, allProviders]);
|
||||
|
||||
// Get the list of required credentials from the schema
|
||||
const requiredCredentials = new Set(
|
||||
(agent.credentials_input_schema?.required as string[]) || [],
|
||||
);
|
||||
@@ -129,31 +92,28 @@ export function ModalRunSection() {
|
||||
</ModalSection>
|
||||
) : null}
|
||||
|
||||
{sortedCredentialFields.length > 0 ? (
|
||||
{credentialFields.length > 0 ? (
|
||||
<ModalSection
|
||||
title="Task Credentials"
|
||||
subtitle="These are the credentials the agent will use to perform this task"
|
||||
>
|
||||
<div className="space-y-6">
|
||||
{sortedCredentialFields.map(([key, inputSubSchema]) => {
|
||||
const selectedCred = inputCredentials?.[key];
|
||||
|
||||
return (
|
||||
{Object.entries(agentCredentialsInputFields || {}).map(
|
||||
([key, inputSubSchema]) => (
|
||||
<CredentialsInput
|
||||
key={key}
|
||||
schema={
|
||||
{ ...inputSubSchema, discriminator: undefined } as any
|
||||
}
|
||||
selectedCredentials={selectedCred}
|
||||
onSelectCredentials={(value) => {
|
||||
setInputCredentialsValue(key, value);
|
||||
}}
|
||||
selectedCredentials={inputCredentials?.[key]}
|
||||
onSelectCredentials={(value) =>
|
||||
setInputCredentialsValue(key, value)
|
||||
}
|
||||
siblingInputs={inputValues}
|
||||
isOptional={!requiredCredentials.has(key)}
|
||||
collapseSystemCredentials
|
||||
/>
|
||||
);
|
||||
})}
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
</ModalSection>
|
||||
) : null}
|
||||
|
||||
@@ -11,18 +11,9 @@ import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
|
||||
import { LibraryAgentPreset } from "@/app/api/__generated__/models/libraryAgentPreset";
|
||||
import { useToast } from "@/components/molecules/Toast/use-toast";
|
||||
import { isEmpty } from "@/lib/utils";
|
||||
import { CredentialsProvidersContext } from "@/providers/agent-credentials/credentials-provider";
|
||||
import { analytics } from "@/services/analytics";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { getSystemCredentials } from "../CredentialsInputs/helpers";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { showExecutionErrorToast } from "./errorHelpers";
|
||||
|
||||
export type RunVariant =
|
||||
@@ -51,10 +42,8 @@ export function useAgentRunModal(
|
||||
const [inputCredentials, setInputCredentials] = useState<Record<string, any>>(
|
||||
callbacks?.initialInputCredentials || {},
|
||||
);
|
||||
|
||||
const [presetName, setPresetName] = useState<string>("");
|
||||
const [presetDescription, setPresetDescription] = useState<string>("");
|
||||
const hasInitializedSystemCreds = useRef(false);
|
||||
|
||||
// Determine the default run type based on agent capabilities
|
||||
const defaultRunType: RunVariant = agent.trigger_setup_info
|
||||
@@ -69,91 +58,6 @@ export function useAgentRunModal(
|
||||
setInputCredentials(callbacks?.initialInputCredentials || {});
|
||||
}, [callbacks?.initialInputValues, callbacks?.initialInputCredentials]);
|
||||
|
||||
const allProviders = useContext(CredentialsProvidersContext);
|
||||
|
||||
// Initialize credentials with default system credentials
|
||||
useEffect(() => {
|
||||
if (!allProviders || !agent.credentials_input_schema?.properties) return;
|
||||
if (callbacks?.initialInputCredentials) {
|
||||
hasInitializedSystemCreds.current = true;
|
||||
return;
|
||||
}
|
||||
if (hasInitializedSystemCreds.current) return;
|
||||
|
||||
const properties = agent.credentials_input_schema.properties as Record<
|
||||
string,
|
||||
any
|
||||
>;
|
||||
|
||||
setInputCredentials((currentCreds) => {
|
||||
const credsToAdd: Record<string, any> = {};
|
||||
|
||||
for (const [key, schema] of Object.entries(properties)) {
|
||||
if (currentCreds[key]) continue;
|
||||
|
||||
const providerNames = schema.credentials_provider || [];
|
||||
const supportedTypes = schema.credentials_types || [];
|
||||
const requiredScopes = schema.credentials_scopes;
|
||||
|
||||
for (const providerName of providerNames) {
|
||||
const providerData = allProviders[providerName];
|
||||
if (!providerData) continue;
|
||||
|
||||
const systemCreds = getSystemCredentials(
|
||||
providerData.savedCredentials,
|
||||
);
|
||||
const matchingSystemCreds = systemCreds.filter((cred) => {
|
||||
if (!supportedTypes.includes(cred.type)) return false;
|
||||
|
||||
if (
|
||||
cred.type === "oauth2" &&
|
||||
requiredScopes &&
|
||||
requiredScopes.length > 0
|
||||
) {
|
||||
const grantedScopes = new Set(cred.scopes || []);
|
||||
const hasAllRequiredScopes = new Set(requiredScopes).isSubsetOf(
|
||||
grantedScopes,
|
||||
);
|
||||
if (!hasAllRequiredScopes) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
if (matchingSystemCreds.length === 1) {
|
||||
const systemCred = matchingSystemCreds[0];
|
||||
credsToAdd[key] = {
|
||||
id: systemCred.id,
|
||||
type: systemCred.type,
|
||||
provider: providerName,
|
||||
title: systemCred.title,
|
||||
};
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(credsToAdd).length > 0) {
|
||||
hasInitializedSystemCreds.current = true;
|
||||
return {
|
||||
...currentCreds,
|
||||
...credsToAdd,
|
||||
};
|
||||
}
|
||||
|
||||
return currentCreds;
|
||||
});
|
||||
}, [
|
||||
allProviders,
|
||||
agent.credentials_input_schema,
|
||||
callbacks?.initialInputCredentials,
|
||||
]);
|
||||
|
||||
// Reset initialization flag when modal closes/opens or agent changes
|
||||
useEffect(() => {
|
||||
hasInitializedSystemCreds.current = false;
|
||||
}, [isOpen, agent.graph_id]);
|
||||
|
||||
// API mutations
|
||||
const executeGraphMutation = usePostV1ExecuteGraphAgent({
|
||||
mutation: {
|
||||
@@ -162,6 +66,7 @@ export function useAgentRunModal(
|
||||
toast({
|
||||
title: "Agent execution started",
|
||||
});
|
||||
// Invalidate runs list for this graph
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: getGetV1ListGraphExecutionsQueryKey(agent.graph_id),
|
||||
});
|
||||
@@ -258,10 +163,14 @@ export function useAgentRunModal(
|
||||
}, [agentInputSchema.required, inputValues]);
|
||||
|
||||
const [allCredentialsAreSet, missingCredentials] = useMemo(() => {
|
||||
// Only check required credentials from schema, not all properties
|
||||
// Credentials marked as optional in node metadata won't be in the required array
|
||||
const requiredCredentials = new Set(
|
||||
(agent.credentials_input_schema?.required as string[]) || [],
|
||||
);
|
||||
|
||||
// Check if required credentials have valid id (not just key existence)
|
||||
// A credential is valid only if it has an id field set
|
||||
const missing = [...requiredCredentials].filter((key) => {
|
||||
const cred = inputCredentials[key];
|
||||
return !cred || !cred.id;
|
||||
@@ -275,6 +184,7 @@ export function useAgentRunModal(
|
||||
[agentCredentialsInputFields],
|
||||
);
|
||||
|
||||
// Final readiness flag combining inputs + credentials when credentials are shown
|
||||
const allRequiredInputsAreSet = useMemo(
|
||||
() =>
|
||||
allRequiredInputsAreSetRaw &&
|
||||
@@ -313,6 +223,7 @@ export function useAgentRunModal(
|
||||
defaultRunType === "automatic-trigger" ||
|
||||
defaultRunType === "manual-trigger"
|
||||
) {
|
||||
// Setup trigger
|
||||
if (!presetName.trim()) {
|
||||
toast({
|
||||
title: "⚠️ Trigger name required",
|
||||
@@ -333,6 +244,9 @@ export function useAgentRunModal(
|
||||
},
|
||||
});
|
||||
} else {
|
||||
// Manual execution
|
||||
// Filter out incomplete credentials (optional ones not selected)
|
||||
// Only send credentials that have a valid id field
|
||||
const validCredentials = Object.fromEntries(
|
||||
Object.entries(inputCredentials).filter(([_, cred]) => cred && cred.id),
|
||||
);
|
||||
@@ -366,24 +280,41 @@ export function useAgentRunModal(
|
||||
}, [agentInputFields]);
|
||||
|
||||
return {
|
||||
// UI state
|
||||
isOpen,
|
||||
setIsOpen,
|
||||
|
||||
// Run mode
|
||||
defaultRunType: defaultRunType as RunVariant,
|
||||
|
||||
// Form: regular inputs
|
||||
inputValues,
|
||||
setInputValues,
|
||||
|
||||
// Form: credentials
|
||||
inputCredentials,
|
||||
setInputCredentials,
|
||||
|
||||
// Preset/trigger labels
|
||||
presetName,
|
||||
presetDescription,
|
||||
setPresetName,
|
||||
setPresetDescription,
|
||||
|
||||
// Validation/readiness
|
||||
allRequiredInputsAreSet,
|
||||
missingInputs,
|
||||
|
||||
// Schemas for rendering
|
||||
agentInputFields,
|
||||
agentCredentialsInputFields,
|
||||
hasInputFields,
|
||||
|
||||
// Async states
|
||||
isExecuting: executeGraphMutation.isPending,
|
||||
isSettingUpTrigger: setupTriggerMutation.isPending,
|
||||
|
||||
// Actions
|
||||
handleRun,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,17 +1,37 @@
|
||||
import { Button } from "@/components/atoms/Button/Button";
|
||||
import { Text } from "@/components/atoms/Text/Text";
|
||||
import { GearIcon } from "@phosphor-icons/react";
|
||||
import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
|
||||
import { useAgentSafeMode } from "@/hooks/useAgentSafeMode";
|
||||
|
||||
interface Props {
|
||||
agent: LibraryAgent;
|
||||
onSelectSettings: () => void;
|
||||
selected?: boolean;
|
||||
}
|
||||
|
||||
export function AgentSettingsButton({
|
||||
agent,
|
||||
onSelectSettings,
|
||||
selected,
|
||||
}: Props) {
|
||||
const { hasHITLBlocks } = useAgentSafeMode(agent);
|
||||
|
||||
if (!hasHITLBlocks) {
|
||||
return null;
|
||||
}
|
||||
|
||||
export function AgentSettingsButton() {
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
variant={selected ? "secondary" : "ghost"}
|
||||
size="small"
|
||||
className="m-0 min-w-0 rounded-full p-0 px-1"
|
||||
onClick={onSelectSettings}
|
||||
aria-label="Agent Settings"
|
||||
>
|
||||
<GearIcon size={18} className="text-zinc-600" />
|
||||
<Text variant="small">Agent Settings</Text>
|
||||
<GearIcon
|
||||
size={18}
|
||||
className={selected ? "text-zinc-900" : "text-zinc-600"}
|
||||
/>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import { Text } from "@/components/atoms/Text/Text";
|
||||
|
||||
export function EmptySchedules() {
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import { Text } from "@/components/atoms/Text/Text";
|
||||
|
||||
export function EmptyTemplates() {
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import { Text } from "@/components/atoms/Text/Text";
|
||||
|
||||
export function EmptyTriggers() {
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type Props = {
|
||||
|
||||
@@ -1,16 +1,22 @@
|
||||
import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
|
||||
import { Skeleton } from "@/components/__legacy__/ui/skeleton";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
|
||||
import { AGENT_LIBRARY_SECTION_PADDING_X } from "../../helpers";
|
||||
import { SelectedViewLayout } from "./SelectedViewLayout";
|
||||
|
||||
interface Props {
|
||||
agent: LibraryAgent;
|
||||
onSelectSettings?: () => void;
|
||||
selectedSettings?: boolean;
|
||||
}
|
||||
|
||||
export function LoadingSelectedContent(props: Props) {
|
||||
return (
|
||||
<SelectedViewLayout agent={props.agent}>
|
||||
<SelectedViewLayout
|
||||
agent={props.agent}
|
||||
onSelectSettings={props.onSelectSettings}
|
||||
selectedSettings={props.selectedSettings}
|
||||
>
|
||||
<div
|
||||
className={cn("flex flex-col gap-4", AGENT_LIBRARY_SECTION_PADDING_X)}
|
||||
>
|
||||
|
||||
@@ -33,6 +33,8 @@ interface Props {
|
||||
onSelectRun?: (id: string) => void;
|
||||
onClearSelectedRun?: () => void;
|
||||
banner?: React.ReactNode;
|
||||
onSelectSettings?: () => void;
|
||||
selectedSettings?: boolean;
|
||||
}
|
||||
|
||||
export function SelectedRunView({
|
||||
@@ -41,6 +43,8 @@ export function SelectedRunView({
|
||||
onSelectRun,
|
||||
onClearSelectedRun,
|
||||
banner,
|
||||
onSelectSettings,
|
||||
selectedSettings,
|
||||
}: Props) {
|
||||
const { run, preset, isLoading, responseError, httpError } =
|
||||
useSelectedRunView(agent.graph_id, runId);
|
||||
@@ -80,7 +84,12 @@ export function SelectedRunView({
|
||||
return (
|
||||
<div className="flex h-full w-full gap-4">
|
||||
<div className="flex min-h-0 min-w-0 flex-1 flex-col">
|
||||
<SelectedViewLayout agent={agent} banner={banner}>
|
||||
<SelectedViewLayout
|
||||
agent={agent}
|
||||
banner={banner}
|
||||
onSelectSettings={onSelectSettings}
|
||||
selectedSettings={selectedSettings}
|
||||
>
|
||||
<div className="flex flex-col gap-4">
|
||||
<RunDetailHeader agent={agent} run={run} />
|
||||
|
||||
|
||||
@@ -21,6 +21,8 @@ interface Props {
|
||||
scheduleId: string;
|
||||
onClearSelectedRun?: () => void;
|
||||
banner?: React.ReactNode;
|
||||
onSelectSettings?: () => void;
|
||||
selectedSettings?: boolean;
|
||||
}
|
||||
|
||||
export function SelectedScheduleView({
|
||||
@@ -28,6 +30,8 @@ export function SelectedScheduleView({
|
||||
scheduleId,
|
||||
onClearSelectedRun,
|
||||
banner,
|
||||
onSelectSettings,
|
||||
selectedSettings,
|
||||
}: Props) {
|
||||
const { schedule, isLoading, error } = useSelectedScheduleView(
|
||||
agent.graph_id,
|
||||
@@ -72,7 +76,12 @@ export function SelectedScheduleView({
|
||||
return (
|
||||
<div className="flex h-full w-full gap-4">
|
||||
<div className="flex min-h-0 min-w-0 flex-1 flex-col">
|
||||
<SelectedViewLayout agent={agent} banner={banner}>
|
||||
<SelectedViewLayout
|
||||
agent={agent}
|
||||
banner={banner}
|
||||
onSelectSettings={onSelectSettings}
|
||||
selectedSettings={selectedSettings}
|
||||
>
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex w-full flex-col gap-0">
|
||||
<RunDetailHeader
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
|
||||
import { Button } from "@/components/atoms/Button/Button";
|
||||
import { Switch } from "@/components/atoms/Switch/Switch";
|
||||
import { Text } from "@/components/atoms/Text/Text";
|
||||
import { useAgentSafeMode } from "@/hooks/useAgentSafeMode";
|
||||
import { Switch } from "@/components/atoms/Switch/Switch";
|
||||
import { Button } from "@/components/atoms/Button/Button";
|
||||
import { ArrowLeftIcon } from "@phosphor-icons/react";
|
||||
import { AGENT_LIBRARY_SECTION_PADDING_X } from "../../../helpers";
|
||||
import { useAgentSafeMode } from "@/hooks/useAgentSafeMode";
|
||||
import { SelectedViewLayout } from "../SelectedViewLayout";
|
||||
import { AGENT_LIBRARY_SECTION_PADDING_X } from "../../../helpers";
|
||||
|
||||
interface Props {
|
||||
agent: LibraryAgent;
|
||||
@@ -17,7 +17,7 @@ export function SelectedSettingsView({ agent, onClearSelectedRun }: Props) {
|
||||
useAgentSafeMode(agent);
|
||||
|
||||
return (
|
||||
<SelectedViewLayout agent={agent}>
|
||||
<SelectedViewLayout agent={agent} onSelectSettings={() => {}}>
|
||||
<div className="flex flex-col gap-4">
|
||||
<div
|
||||
className={`${AGENT_LIBRARY_SECTION_PADDING_X} mb-8 flex items-center gap-3`}
|
||||
@@ -33,8 +33,15 @@ export function SelectedSettingsView({ agent, onClearSelectedRun }: Props) {
|
||||
<Text variant="h2">Agent Settings</Text>
|
||||
</div>
|
||||
|
||||
<div className={`${AGENT_LIBRARY_SECTION_PADDING_X} space-y-6`}>
|
||||
{hasHITLBlocks ? (
|
||||
<div className={AGENT_LIBRARY_SECTION_PADDING_X}>
|
||||
{!hasHITLBlocks ? (
|
||||
<div className="rounded-xl border border-zinc-100 bg-white p-6">
|
||||
<Text variant="body" className="text-muted-foreground">
|
||||
This agent doesn't have any human-in-the-loop blocks, so
|
||||
there are no settings to configure.
|
||||
</Text>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex w-full max-w-2xl flex-col items-start gap-4 rounded-xl border border-zinc-100 bg-white p-6">
|
||||
<div className="flex w-full items-start justify-between gap-4">
|
||||
<div className="flex-1">
|
||||
@@ -52,12 +59,6 @@ export function SelectedSettingsView({ agent, onClearSelectedRun }: Props) {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="rounded-xl border border-zinc-100 bg-white p-6">
|
||||
<Text variant="body" className="text-muted-foreground">
|
||||
This agent doesn't have any configurable settings.
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
|
||||
import { Breadcrumbs } from "@/components/molecules/Breadcrumbs/Breadcrumbs";
|
||||
import { AgentSettingsButton } from "@/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/other/AgentSettingsButton";
|
||||
import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
|
||||
import { AGENT_LIBRARY_SECTION_PADDING_X } from "../../helpers";
|
||||
import { AgentSettingsModal } from "../modals/AgentSettingsModal/AgentSettingsModal";
|
||||
import { SectionWrap } from "../other/SectionWrap";
|
||||
|
||||
interface Props {
|
||||
@@ -9,6 +9,8 @@ interface Props {
|
||||
children: React.ReactNode;
|
||||
banner?: React.ReactNode;
|
||||
additionalBreadcrumb?: { name: string; link?: string };
|
||||
onSelectSettings?: () => void;
|
||||
selectedSettings?: boolean;
|
||||
}
|
||||
|
||||
export function SelectedViewLayout(props: Props) {
|
||||
@@ -17,8 +19,8 @@ export function SelectedViewLayout(props: Props) {
|
||||
<div
|
||||
className={`${AGENT_LIBRARY_SECTION_PADDING_X} flex-shrink-0 border-b border-zinc-100 pb-0 lg:pb-4`}
|
||||
>
|
||||
{props.banner}
|
||||
<div className="relative flex w-full items-center justify-between">
|
||||
{props.banner && <div className="mb-4">{props.banner}</div>}
|
||||
<div className="relative flex w-fit items-center gap-2">
|
||||
<Breadcrumbs
|
||||
items={[
|
||||
{ name: "My Library", link: "/library" },
|
||||
@@ -31,9 +33,15 @@ export function SelectedViewLayout(props: Props) {
|
||||
: []),
|
||||
]}
|
||||
/>
|
||||
<div className="absolute right-0">
|
||||
<AgentSettingsModal agent={props.agent} />
|
||||
</div>
|
||||
{props.agent && props.onSelectSettings && (
|
||||
<div className="absolute -right-8">
|
||||
<AgentSettingsButton
|
||||
agent={props.agent}
|
||||
onSelectSettings={props.onSelectSettings}
|
||||
selected={props.selectedSettings}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex min-h-0 flex-1 flex-col overflow-y-auto overflow-x-visible">
|
||||
|
||||
@@ -2775,28 +2775,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/integrations/providers/system": {
|
||||
"get": {
|
||||
"tags": ["v1", "integrations"],
|
||||
"summary": "List System Providers",
|
||||
"description": "Get a list of providers that have platform credits (system credentials) available.\n\nThese providers can be used without the user providing their own API keys.",
|
||||
"operationId": "getV1ListSystemProviders",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"items": { "type": "string" },
|
||||
"type": "array",
|
||||
"title": "Response Getv1Listsystemproviders"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/integrations/webhooks/{webhook_id}/ping": {
|
||||
"post": {
|
||||
"tags": ["v1", "integrations"],
|
||||
@@ -6814,12 +6792,6 @@
|
||||
"anyOf": [{ "type": "string" }, { "type": "null" }],
|
||||
"title": "Host",
|
||||
"description": "Host pattern for host-scoped credentials"
|
||||
},
|
||||
"is_system": {
|
||||
"type": "boolean",
|
||||
"title": "Is System",
|
||||
"description": "Whether this is a system-managed credential",
|
||||
"default": false
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
|
||||
@@ -20,7 +20,6 @@ export function Button(props: ButtonProps) {
|
||||
rightIcon,
|
||||
children,
|
||||
as = "button",
|
||||
asChild: _asChild, // Destructure to prevent passing to DOM
|
||||
...restProps
|
||||
} = props;
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { Button } from "@/components/atoms/Button/Button";
|
||||
import { Text } from "@/components/atoms/Text/Text";
|
||||
|
||||
interface Props {
|
||||
interface MarketplaceBannersProps {
|
||||
hasUpdate?: boolean;
|
||||
latestVersion?: number;
|
||||
hasUnpublishedChanges?: boolean;
|
||||
@@ -21,7 +21,7 @@ export function MarketplaceBanners({
|
||||
isUpdating,
|
||||
onUpdate,
|
||||
onPublish,
|
||||
}: Props) {
|
||||
}: MarketplaceBannersProps) {
|
||||
const renderUpdateBanner = () => {
|
||||
if (hasUpdate && latestVersion) {
|
||||
return (
|
||||
@@ -1,203 +0,0 @@
|
||||
import type { Meta } from "@storybook/nextjs";
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
} from "./Accordion";
|
||||
|
||||
const meta: Meta<typeof Accordion> = {
|
||||
title: "Molecules/Accordion",
|
||||
component: Accordion,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
docs: {
|
||||
description: {
|
||||
component: `
|
||||
## Accordion Component
|
||||
|
||||
A vertically stacked set of interactive headings that each reveal an associated section of content.
|
||||
|
||||
### ✨ Features
|
||||
|
||||
- **Built on Radix UI** - Uses @radix-ui/react-accordion for accessibility and functionality
|
||||
- **Single or multiple** - Supports single or multiple items open at once
|
||||
- **Smooth animations** - Built-in expand/collapse animations
|
||||
- **Accessible** - Full keyboard navigation and screen reader support
|
||||
- **Customizable** - Style with Tailwind CSS classes
|
||||
|
||||
### 🎯 Usage
|
||||
|
||||
\`\`\`tsx
|
||||
<Accordion type="single" collapsible>
|
||||
<AccordionItem value="item-1">
|
||||
<AccordionTrigger>Is it accessible?</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
Yes. It adheres to the WAI-ARIA design pattern.
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
\`\`\`
|
||||
|
||||
### Props
|
||||
|
||||
**Accordion**:
|
||||
- **type**: "single" | "multiple" - Whether one or multiple items can be open
|
||||
- **collapsible**: boolean - When type is "single", allows closing all items
|
||||
- **defaultValue**: string | string[] - Default open item(s)
|
||||
- **value**: string | string[] - Controlled open item(s)
|
||||
- **onValueChange**: (value) => void - Callback when value changes
|
||||
|
||||
**AccordionItem**:
|
||||
- **value**: string - Unique identifier for the item
|
||||
- **disabled**: boolean - Whether the item is disabled
|
||||
|
||||
**AccordionTrigger**:
|
||||
- Standard button props
|
||||
|
||||
**AccordionContent**:
|
||||
- Standard div props
|
||||
`,
|
||||
},
|
||||
},
|
||||
},
|
||||
tags: ["autodocs"],
|
||||
argTypes: {
|
||||
type: {
|
||||
control: "radio",
|
||||
options: ["single", "multiple"],
|
||||
description: "Whether one or multiple items can be open at the same time",
|
||||
table: {
|
||||
defaultValue: { summary: "single" },
|
||||
},
|
||||
},
|
||||
collapsible: {
|
||||
control: "boolean",
|
||||
description:
|
||||
'When type is "single", allows closing content when clicking on open trigger',
|
||||
table: {
|
||||
defaultValue: { summary: "false" },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
export function Default() {
|
||||
return (
|
||||
<Accordion type="single" collapsible className="w-96">
|
||||
<AccordionItem value="item-1">
|
||||
<AccordionTrigger>Is it accessible?</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
Yes. It adheres to the WAI-ARIA design pattern.
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
<AccordionItem value="item-2">
|
||||
<AccordionTrigger>Is it styled?</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
Yes. It comes with default styles that match your design system.
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
<AccordionItem value="item-3">
|
||||
<AccordionTrigger>Is it animated?</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
Yes. It's animated by default with smooth expand/collapse
|
||||
transitions.
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
);
|
||||
}
|
||||
|
||||
export function Multiple() {
|
||||
return (
|
||||
<Accordion type="multiple" className="w-96">
|
||||
<AccordionItem value="item-1">
|
||||
<AccordionTrigger>First section</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
Multiple items can be open at the same time when type is set to
|
||||
"multiple".
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
<AccordionItem value="item-2">
|
||||
<AccordionTrigger>Second section</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
Try opening this one while the first is still open.
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
<AccordionItem value="item-3">
|
||||
<AccordionTrigger>Third section</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
All three can be open simultaneously.
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
);
|
||||
}
|
||||
|
||||
export function DefaultOpen() {
|
||||
return (
|
||||
<Accordion type="single" collapsible defaultValue="item-2" className="w-96">
|
||||
<AccordionItem value="item-1">
|
||||
<AccordionTrigger>Closed by default</AccordionTrigger>
|
||||
<AccordionContent>This item starts closed.</AccordionContent>
|
||||
</AccordionItem>
|
||||
<AccordionItem value="item-2">
|
||||
<AccordionTrigger>Open by default</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
This item starts open because defaultValue is set to
|
||||
"item-2".
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
<AccordionItem value="item-3">
|
||||
<AccordionTrigger>Also closed</AccordionTrigger>
|
||||
<AccordionContent>This item also starts closed.</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
);
|
||||
}
|
||||
|
||||
export function WithDisabledItem() {
|
||||
return (
|
||||
<Accordion type="single" collapsible className="w-96">
|
||||
<AccordionItem value="item-1">
|
||||
<AccordionTrigger>Available item</AccordionTrigger>
|
||||
<AccordionContent>This item can be toggled.</AccordionContent>
|
||||
</AccordionItem>
|
||||
<AccordionItem value="item-2" disabled>
|
||||
<AccordionTrigger>Disabled item</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
This content cannot be accessed because the item is disabled.
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
<AccordionItem value="item-3">
|
||||
<AccordionTrigger>Another available item</AccordionTrigger>
|
||||
<AccordionContent>This item can also be toggled.</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
);
|
||||
}
|
||||
|
||||
export function CustomStyled() {
|
||||
return (
|
||||
<Accordion type="single" collapsible className="w-96">
|
||||
<AccordionItem value="item-1" className="border-none">
|
||||
<AccordionTrigger className="rounded-lg bg-zinc-100 px-4 hover:bg-zinc-200 hover:no-underline">
|
||||
Custom styled trigger
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="px-4 pt-2">
|
||||
You can customize the styling using className props.
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
<AccordionItem value="item-2" className="mt-2 border-none">
|
||||
<AccordionTrigger className="rounded-lg bg-blue-50 px-4 text-blue-700 hover:bg-blue-100 hover:no-underline">
|
||||
Blue themed
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="px-4 pt-2 text-blue-600">
|
||||
Each item can have different styles.
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
);
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
"use client";
|
||||
|
||||
export {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
} from "@/components/ui/accordion";
|
||||
@@ -49,12 +49,7 @@ export function DrawerWrap({
|
||||
>
|
||||
{title ? (
|
||||
<Drawer.Title className={drawerStyles.title}>{title}</Drawer.Title>
|
||||
) : (
|
||||
<span className="sr-only">
|
||||
{/* Title is required for a11y compliance even if not displayed so screen readers can announce it */}
|
||||
<Drawer.Title>{title}</Drawer.Title>
|
||||
</span>
|
||||
)}
|
||||
) : null}
|
||||
|
||||
{!isForceOpen ? (
|
||||
title ? (
|
||||
|
||||
@@ -1,116 +0,0 @@
|
||||
import type { Meta, StoryObj } from "@storybook/nextjs";
|
||||
import { TooltipProvider } from "@/components/atoms/Tooltip/BaseTooltip";
|
||||
import { Table } from "./Table";
|
||||
|
||||
const meta = {
|
||||
title: "Molecules/Table",
|
||||
component: Table,
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<TooltipProvider>
|
||||
<Story />
|
||||
</TooltipProvider>
|
||||
),
|
||||
],
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
tags: ["autodocs"],
|
||||
argTypes: {
|
||||
allowAddRow: {
|
||||
control: "boolean",
|
||||
description: "Whether to show the Add row button",
|
||||
},
|
||||
allowDeleteRow: {
|
||||
control: "boolean",
|
||||
description: "Whether to show delete buttons for each row",
|
||||
},
|
||||
readOnly: {
|
||||
control: "boolean",
|
||||
description:
|
||||
"Whether the table is read-only (renders text instead of inputs)",
|
||||
},
|
||||
addRowLabel: {
|
||||
control: "text",
|
||||
description: "Label for the Add row button",
|
||||
},
|
||||
},
|
||||
} satisfies Meta<typeof Table>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
columns: ["name", "email", "role"],
|
||||
allowAddRow: true,
|
||||
allowDeleteRow: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const WithDefaultValues: Story = {
|
||||
args: {
|
||||
columns: ["name", "email", "role"],
|
||||
defaultValues: [
|
||||
{ name: "John Doe", email: "john@example.com", role: "Admin" },
|
||||
{ name: "Jane Smith", email: "jane@example.com", role: "User" },
|
||||
{ name: "Bob Wilson", email: "bob@example.com", role: "Editor" },
|
||||
],
|
||||
allowAddRow: true,
|
||||
allowDeleteRow: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const ReadOnly: Story = {
|
||||
args: {
|
||||
columns: ["name", "email"],
|
||||
defaultValues: [
|
||||
{ name: "John Doe", email: "john@example.com" },
|
||||
{ name: "Jane Smith", email: "jane@example.com" },
|
||||
],
|
||||
readOnly: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const NoAddOrDelete: Story = {
|
||||
args: {
|
||||
columns: ["name", "email"],
|
||||
defaultValues: [
|
||||
{ name: "John Doe", email: "john@example.com" },
|
||||
{ name: "Jane Smith", email: "jane@example.com" },
|
||||
],
|
||||
allowAddRow: false,
|
||||
allowDeleteRow: false,
|
||||
},
|
||||
};
|
||||
|
||||
export const SingleColumn: Story = {
|
||||
args: {
|
||||
columns: ["item"],
|
||||
allowAddRow: true,
|
||||
allowDeleteRow: true,
|
||||
addRowLabel: "Add item",
|
||||
},
|
||||
};
|
||||
|
||||
export const CustomAddLabel: Story = {
|
||||
args: {
|
||||
columns: ["key", "value"],
|
||||
allowAddRow: true,
|
||||
allowDeleteRow: true,
|
||||
addRowLabel: "Add new entry",
|
||||
},
|
||||
};
|
||||
|
||||
export const KeyValuePairs: Story = {
|
||||
args: {
|
||||
columns: ["key", "value"],
|
||||
defaultValues: [
|
||||
{ key: "API_KEY", value: "sk-..." },
|
||||
{ key: "DATABASE_URL", value: "postgres://..." },
|
||||
],
|
||||
allowAddRow: true,
|
||||
allowDeleteRow: true,
|
||||
addRowLabel: "Add variable",
|
||||
},
|
||||
};
|
||||
@@ -1,133 +0,0 @@
|
||||
import * as React from "react";
|
||||
import {
|
||||
Table as BaseTable,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/__legacy__/ui/table";
|
||||
import { Button } from "@/components/atoms/Button/Button";
|
||||
import { Input } from "@/components/atoms/Input/Input";
|
||||
import { Text } from "@/components/atoms/Text/Text";
|
||||
import { Plus, Trash2 } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useTable, RowData } from "./useTable";
|
||||
import { formatColumnTitle, formatPlaceholder } from "./helpers";
|
||||
|
||||
export interface TableProps {
|
||||
columns: string[];
|
||||
defaultValues?: RowData[];
|
||||
onChange?: (rows: RowData[]) => void;
|
||||
allowAddRow?: boolean;
|
||||
allowDeleteRow?: boolean;
|
||||
addRowLabel?: string;
|
||||
className?: string;
|
||||
readOnly?: boolean;
|
||||
}
|
||||
|
||||
export function Table({
|
||||
columns,
|
||||
defaultValues,
|
||||
onChange,
|
||||
allowAddRow = true,
|
||||
allowDeleteRow = true,
|
||||
addRowLabel = "Add row",
|
||||
className,
|
||||
readOnly = false,
|
||||
}: TableProps) {
|
||||
const { rows, handleAddRow, handleDeleteRow, handleCellChange } = useTable({
|
||||
columns,
|
||||
defaultValues,
|
||||
onChange,
|
||||
});
|
||||
|
||||
const showDeleteColumn = allowDeleteRow && !readOnly;
|
||||
const showAddButton = allowAddRow && !readOnly;
|
||||
|
||||
return (
|
||||
<div className={cn("flex flex-col gap-3", className)}>
|
||||
<div className="overflow-hidden rounded-xl border border-zinc-200 bg-white">
|
||||
<BaseTable>
|
||||
<TableHeader>
|
||||
<TableRow className="border-b border-zinc-100 bg-zinc-50/50">
|
||||
{columns.map((column) => (
|
||||
<TableHead
|
||||
key={column}
|
||||
className="h-10 px-3 text-sm font-medium text-zinc-600"
|
||||
>
|
||||
{formatColumnTitle(column)}
|
||||
</TableHead>
|
||||
))}
|
||||
{showDeleteColumn && <TableHead className="w-[50px]" />}
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{rows.map((row, rowIndex) => (
|
||||
<TableRow key={rowIndex} className="border-none">
|
||||
{columns.map((column) => (
|
||||
<TableCell key={`${rowIndex}-${column}`} className="p-2">
|
||||
{readOnly ? (
|
||||
<Text
|
||||
variant="body"
|
||||
className="px-3 py-2 text-sm text-zinc-800"
|
||||
>
|
||||
{row[column] || "-"}
|
||||
</Text>
|
||||
) : (
|
||||
<Input
|
||||
id={`table-${rowIndex}-${column}`}
|
||||
label={formatColumnTitle(column)}
|
||||
hideLabel
|
||||
value={row[column] ?? ""}
|
||||
onChange={(e) =>
|
||||
handleCellChange(rowIndex, column, e.target.value)
|
||||
}
|
||||
placeholder={formatPlaceholder(column)}
|
||||
size="small"
|
||||
wrapperClassName="mb-0"
|
||||
/>
|
||||
)}
|
||||
</TableCell>
|
||||
))}
|
||||
{showDeleteColumn && (
|
||||
<TableCell className="p-2">
|
||||
<Button
|
||||
variant="icon"
|
||||
size="icon"
|
||||
onClick={() => handleDeleteRow(rowIndex)}
|
||||
aria-label="Delete row"
|
||||
className="text-zinc-400 transition-colors hover:text-red-500"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</TableCell>
|
||||
)}
|
||||
</TableRow>
|
||||
))}
|
||||
{showAddButton && (
|
||||
<TableRow>
|
||||
<TableCell
|
||||
colSpan={columns.length + (showDeleteColumn ? 1 : 0)}
|
||||
className="p-2"
|
||||
>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="small"
|
||||
onClick={handleAddRow}
|
||||
leftIcon={<Plus className="h-4 w-4" />}
|
||||
className="w-fit"
|
||||
>
|
||||
{addRowLabel}
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</BaseTable>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { type RowData } from "./useTable";
|
||||
@@ -1,7 +0,0 @@
|
||||
export const formatColumnTitle = (key: string): string => {
|
||||
return key.charAt(0).toUpperCase() + key.slice(1);
|
||||
};
|
||||
|
||||
export const formatPlaceholder = (key: string): string => {
|
||||
return `Enter ${key.toLowerCase()}`;
|
||||
};
|
||||
@@ -1,81 +0,0 @@
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
export type RowData = Record<string, string>;
|
||||
|
||||
interface UseTableOptions {
|
||||
columns: string[];
|
||||
defaultValues?: RowData[];
|
||||
onChange?: (rows: RowData[]) => void;
|
||||
}
|
||||
|
||||
export function useTable({
|
||||
columns,
|
||||
defaultValues,
|
||||
onChange,
|
||||
}: UseTableOptions) {
|
||||
const createEmptyRow = (): RowData => {
|
||||
const emptyRow: RowData = {};
|
||||
columns.forEach((column) => {
|
||||
emptyRow[column] = "";
|
||||
});
|
||||
return emptyRow;
|
||||
};
|
||||
|
||||
const [rows, setRows] = useState<RowData[]>(() => {
|
||||
if (defaultValues && defaultValues.length > 0) {
|
||||
return defaultValues;
|
||||
}
|
||||
return [];
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (defaultValues !== undefined) {
|
||||
setRows(defaultValues);
|
||||
}
|
||||
}, [defaultValues]);
|
||||
|
||||
const updateRows = (newRows: RowData[]) => {
|
||||
setRows(newRows);
|
||||
onChange?.(newRows);
|
||||
};
|
||||
|
||||
const handleAddRow = () => {
|
||||
const newRows = [...rows, createEmptyRow()];
|
||||
updateRows(newRows);
|
||||
};
|
||||
|
||||
const handleDeleteRow = (rowIndex: number) => {
|
||||
const newRows = rows.filter((_, index) => index !== rowIndex);
|
||||
updateRows(newRows);
|
||||
};
|
||||
|
||||
const handleCellChange = (
|
||||
rowIndex: number,
|
||||
columnKey: string,
|
||||
value: string,
|
||||
) => {
|
||||
const newRows = rows.map((row, index) => {
|
||||
if (index === rowIndex) {
|
||||
return {
|
||||
...row,
|
||||
[columnKey]: value,
|
||||
};
|
||||
}
|
||||
return row;
|
||||
});
|
||||
updateRows(newRows);
|
||||
};
|
||||
|
||||
const clearAll = () => {
|
||||
updateRows([]);
|
||||
};
|
||||
|
||||
return {
|
||||
rows,
|
||||
handleAddRow,
|
||||
handleDeleteRow,
|
||||
handleCellChange,
|
||||
clearAll,
|
||||
createEmptyRow,
|
||||
};
|
||||
}
|
||||
@@ -30,8 +30,6 @@ export const FormRenderer = ({
|
||||
return generateUiSchemaForCustomFields(preprocessedSchema, uiSchema);
|
||||
}, [preprocessedSchema, uiSchema]);
|
||||
|
||||
console.log("preprocessedSchema", preprocessedSchema);
|
||||
|
||||
return (
|
||||
<div className={"mb-6 mt-4"}>
|
||||
<Form
|
||||
|
||||
@@ -5,14 +5,19 @@ import { useAnyOfField } from "./useAnyOfField";
|
||||
import { getHandleId, updateUiOption } from "../../helpers";
|
||||
import { useEdgeStore } from "@/app/(platform)/build/stores/edgeStore";
|
||||
import { ANY_OF_FLAG } from "../../constants";
|
||||
import { findCustomFieldId } from "../../registry";
|
||||
|
||||
export const AnyOfField = (props: FieldProps) => {
|
||||
const { registry, schema } = props;
|
||||
const { fields } = registry;
|
||||
const { SchemaField: _SchemaField } = fields;
|
||||
const { nodeId } = registry.formContext;
|
||||
|
||||
const { isInputConnected } = useEdgeStore();
|
||||
|
||||
const uiOptions = getUiOptions(props.uiSchema, props.globalUiOptions);
|
||||
|
||||
const Widget = getWidget({ type: "string" }, "select", registry.widgets);
|
||||
|
||||
const {
|
||||
handleOptionChange,
|
||||
enumOptions,
|
||||
@@ -21,15 +26,6 @@ export const AnyOfField = (props: FieldProps) => {
|
||||
field_id,
|
||||
} = useAnyOfField(props);
|
||||
|
||||
const parentCustomFieldId = findCustomFieldId(schema);
|
||||
if (parentCustomFieldId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const uiOptions = getUiOptions(props.uiSchema, props.globalUiOptions);
|
||||
|
||||
const Widget = getWidget({ type: "string" }, "select", registry.widgets);
|
||||
|
||||
const handleId = getHandleId({
|
||||
uiOptions,
|
||||
id: field_id + ANY_OF_FLAG,
|
||||
@@ -44,21 +40,12 @@ export const AnyOfField = (props: FieldProps) => {
|
||||
|
||||
const isHandleConnected = isInputConnected(nodeId, handleId);
|
||||
|
||||
// Now anyOf can render - custom fields if the option schema matches a custom field
|
||||
const optionCustomFieldId = optionSchema
|
||||
? findCustomFieldId(optionSchema)
|
||||
: null;
|
||||
|
||||
const optionUiSchema = optionCustomFieldId
|
||||
? { ...updatedUiSchema, "ui:field": optionCustomFieldId }
|
||||
: updatedUiSchema;
|
||||
|
||||
const optionsSchemaField =
|
||||
(optionSchema && optionSchema.type !== "null" && (
|
||||
<_SchemaField
|
||||
{...props}
|
||||
schema={optionSchema}
|
||||
uiSchema={optionUiSchema}
|
||||
uiSchema={updatedUiSchema}
|
||||
/>
|
||||
)) ||
|
||||
null;
|
||||
|
||||
@@ -17,7 +17,6 @@ interface InputExpanderModalProps {
|
||||
defaultValue: string;
|
||||
description?: string;
|
||||
placeholder?: string;
|
||||
inputType?: "text" | "json";
|
||||
}
|
||||
|
||||
export const InputExpanderModal: FC<InputExpanderModalProps> = ({
|
||||
@@ -28,7 +27,6 @@ export const InputExpanderModal: FC<InputExpanderModalProps> = ({
|
||||
defaultValue,
|
||||
description,
|
||||
placeholder,
|
||||
inputType = "text",
|
||||
}) => {
|
||||
const [tempValue, setTempValue] = useState(defaultValue);
|
||||
const [isCopied, setIsCopied] = useState(false);
|
||||
@@ -80,10 +78,7 @@ export const InputExpanderModal: FC<InputExpanderModalProps> = ({
|
||||
hideLabel
|
||||
id="input-expander-modal"
|
||||
value={tempValue}
|
||||
className={cn(
|
||||
"!min-h-[300px] rounded-2xlarge",
|
||||
inputType === "json" && "font-mono text-sm",
|
||||
)}
|
||||
className="!min-h-[300px] rounded-2xlarge"
|
||||
onChange={(e) => setTempValue(e.target.value)}
|
||||
placeholder={placeholder || "Enter text..."}
|
||||
autoFocus
|
||||
|
||||
@@ -88,8 +88,6 @@ export const CredentialsField = (props: FieldProps) => {
|
||||
showTitle={false}
|
||||
readOnly={formContext?.readOnly}
|
||||
isOptional={!isRequired}
|
||||
className="w-full"
|
||||
variant="node"
|
||||
/>
|
||||
|
||||
{/* Optional credentials toggle - only show in builder canvas, not run dialogs */}
|
||||
|
||||
@@ -1,124 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { FieldProps, getTemplate, getUiOptions } from "@rjsf/utils";
|
||||
import { Input } from "@/components/atoms/Input/Input";
|
||||
import { Button } from "@/components/atoms/Button/Button";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/components/atoms/Tooltip/BaseTooltip";
|
||||
import { ArrowsOutIcon } from "@phosphor-icons/react";
|
||||
import { InputExpanderModal } from "../../base/standard/widgets/TextInput/TextInputExpanderModal";
|
||||
import { getHandleId, updateUiOption } from "../../helpers";
|
||||
import { useJsonTextField } from "./useJsonTextField";
|
||||
import { getPlaceholder } from "./helpers";
|
||||
|
||||
export const JsonTextField = (props: FieldProps) => {
|
||||
const {
|
||||
formData,
|
||||
onChange,
|
||||
schema,
|
||||
registry,
|
||||
uiSchema,
|
||||
required,
|
||||
name,
|
||||
fieldPathId,
|
||||
} = props;
|
||||
|
||||
const uiOptions = getUiOptions(uiSchema);
|
||||
|
||||
const TitleFieldTemplate = getTemplate(
|
||||
"TitleFieldTemplate",
|
||||
registry,
|
||||
uiOptions,
|
||||
);
|
||||
|
||||
const fieldId = fieldPathId?.$id ?? props.id ?? "json-field";
|
||||
|
||||
const handleId = getHandleId({
|
||||
uiOptions,
|
||||
id: fieldId,
|
||||
schema: schema,
|
||||
});
|
||||
|
||||
const updatedUiSchema = updateUiOption(uiSchema, {
|
||||
handleId: handleId,
|
||||
});
|
||||
|
||||
const {
|
||||
textValue,
|
||||
isModalOpen,
|
||||
handleChange,
|
||||
handleModalOpen,
|
||||
handleModalClose,
|
||||
handleModalSave,
|
||||
} = useJsonTextField({
|
||||
formData,
|
||||
onChange,
|
||||
path: fieldPathId?.path,
|
||||
});
|
||||
|
||||
const placeholder = getPlaceholder(schema);
|
||||
const title = schema.title || name || "JSON Value";
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
<TitleFieldTemplate
|
||||
id={fieldId}
|
||||
title={title}
|
||||
required={required}
|
||||
schema={schema}
|
||||
uiSchema={updatedUiSchema}
|
||||
registry={registry}
|
||||
/>
|
||||
<div className="nodrag relative flex items-center gap-2">
|
||||
<Input
|
||||
id={fieldId}
|
||||
hideLabel={true}
|
||||
type="textarea"
|
||||
label=""
|
||||
size="small"
|
||||
wrapperClassName="mb-0 flex-1 "
|
||||
value={textValue}
|
||||
onChange={handleChange}
|
||||
placeholder={placeholder}
|
||||
required={required}
|
||||
disabled={props.disabled}
|
||||
className="min-h-[60px] pr-8 font-mono text-xs"
|
||||
/>
|
||||
|
||||
<Tooltip delayDuration={0}>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={handleModalOpen}
|
||||
type="button"
|
||||
className="p-1"
|
||||
>
|
||||
<ArrowsOutIcon className="size-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Expand input</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
{schema.description && (
|
||||
<span className="text-xs text-gray-500">{schema.description}</span>
|
||||
)}
|
||||
|
||||
<InputExpanderModal
|
||||
isOpen={isModalOpen}
|
||||
onClose={handleModalClose}
|
||||
onSave={handleModalSave}
|
||||
title={`Edit ${title}`}
|
||||
description={schema.description || "Enter valid JSON"}
|
||||
defaultValue={textValue}
|
||||
placeholder={placeholder}
|
||||
inputType="json"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default JsonTextField;
|
||||
@@ -1,67 +0,0 @@
|
||||
import { RJSFSchema } from "@rjsf/utils";
|
||||
|
||||
/**
|
||||
* Converts form data to a JSON string for display
|
||||
* @param formData - The data to stringify
|
||||
* @returns JSON string or empty string if data is null/undefined
|
||||
*/
|
||||
export function stringifyFormData(formData: unknown): string {
|
||||
if (formData === undefined || formData === null) {
|
||||
return "";
|
||||
}
|
||||
try {
|
||||
return JSON.stringify(formData, null, 2);
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a JSON string into an object/array
|
||||
* @param value - The JSON string to parse
|
||||
* @returns Parsed value or undefined if parsing fails or empty
|
||||
*/
|
||||
export function parseJsonValue(value: string): unknown | undefined {
|
||||
const trimmed = value.trim();
|
||||
if (trimmed === "") {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(trimmed);
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the appropriate placeholder text based on schema type
|
||||
* @param schema - The JSON schema
|
||||
* @returns Placeholder string
|
||||
*/
|
||||
export function getPlaceholder(schema: RJSFSchema): string {
|
||||
if (schema.type === "array") {
|
||||
return '["item1", "item2"] or [{"key": "value"}]';
|
||||
}
|
||||
if (schema.type === "object") {
|
||||
return '{"key": "value"}';
|
||||
}
|
||||
return "Enter JSON value...";
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a JSON string is valid
|
||||
* @param value - The JSON string to validate
|
||||
* @returns true if valid JSON, false otherwise
|
||||
*/
|
||||
export function isValidJson(value: string): boolean {
|
||||
if (value.trim() === "") {
|
||||
return true; // Empty is considered valid (will be undefined)
|
||||
}
|
||||
try {
|
||||
JSON.parse(value);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1,107 +0,0 @@
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { FieldProps } from "@rjsf/utils";
|
||||
import { stringifyFormData, parseJsonValue, isValidJson } from "./helpers";
|
||||
|
||||
type FieldOnChange = FieldProps["onChange"];
|
||||
type FieldPathId = FieldProps["fieldPathId"];
|
||||
|
||||
interface UseJsonTextFieldOptions {
|
||||
formData: unknown;
|
||||
onChange: FieldOnChange;
|
||||
path?: FieldPathId["path"];
|
||||
}
|
||||
|
||||
interface UseJsonTextFieldReturn {
|
||||
textValue: string;
|
||||
isModalOpen: boolean;
|
||||
hasError: boolean;
|
||||
handleChange: (
|
||||
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
|
||||
) => void;
|
||||
handleModalOpen: () => void;
|
||||
handleModalClose: () => void;
|
||||
handleModalSave: (value: string) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom hook for managing JSON text field state and handlers
|
||||
*/
|
||||
export function useJsonTextField({
|
||||
formData,
|
||||
onChange,
|
||||
path,
|
||||
}: UseJsonTextFieldOptions): UseJsonTextFieldReturn {
|
||||
const [textValue, setTextValue] = useState(() => stringifyFormData(formData));
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [hasError, setHasError] = useState(false);
|
||||
|
||||
// Update text value when formData changes externally
|
||||
useEffect(() => {
|
||||
const newValue = stringifyFormData(formData);
|
||||
setTextValue(newValue);
|
||||
setHasError(false);
|
||||
}, [formData]);
|
||||
|
||||
const handleChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
||||
const value = e.target.value;
|
||||
setTextValue(value);
|
||||
|
||||
// Validate JSON and update error state
|
||||
const valid = isValidJson(value);
|
||||
setHasError(!valid);
|
||||
|
||||
// Try to parse and update formData
|
||||
if (value.trim() === "") {
|
||||
onChange(undefined, path ?? []);
|
||||
return;
|
||||
}
|
||||
|
||||
const parsed = parseJsonValue(value);
|
||||
if (parsed !== undefined) {
|
||||
onChange(parsed, path ?? []);
|
||||
}
|
||||
},
|
||||
[onChange, path],
|
||||
);
|
||||
|
||||
const handleModalOpen = useCallback(() => {
|
||||
setIsModalOpen(true);
|
||||
}, []);
|
||||
|
||||
const handleModalClose = useCallback(() => {
|
||||
setIsModalOpen(false);
|
||||
}, []);
|
||||
|
||||
const handleModalSave = useCallback(
|
||||
(value: string) => {
|
||||
setTextValue(value);
|
||||
setIsModalOpen(false);
|
||||
|
||||
// Validate and update
|
||||
const valid = isValidJson(value);
|
||||
setHasError(!valid);
|
||||
|
||||
if (value.trim() === "") {
|
||||
onChange(undefined, path ?? []);
|
||||
return;
|
||||
}
|
||||
|
||||
const parsed = parseJsonValue(value);
|
||||
if (parsed !== undefined) {
|
||||
onChange(parsed, path ?? []);
|
||||
}
|
||||
},
|
||||
[onChange, path],
|
||||
);
|
||||
|
||||
return {
|
||||
textValue,
|
||||
isModalOpen,
|
||||
hasError,
|
||||
handleChange,
|
||||
handleModalOpen,
|
||||
handleModalClose,
|
||||
handleModalSave,
|
||||
};
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
import React from "react";
|
||||
import { FieldProps, getUiOptions } from "@rjsf/utils";
|
||||
import { BlockIOObjectSubSchema } from "@/lib/autogpt-server-api/types";
|
||||
import {
|
||||
MultiSelector,
|
||||
MultiSelectorContent,
|
||||
MultiSelectorInput,
|
||||
MultiSelectorItem,
|
||||
MultiSelectorList,
|
||||
MultiSelectorTrigger,
|
||||
} from "@/components/__legacy__/ui/multiselect";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useMultiSelectField } from "./useMultiSelectField";
|
||||
|
||||
export const MultiSelectField = (props: FieldProps) => {
|
||||
const { schema, formData, onChange, fieldPathId } = props;
|
||||
const uiOptions = getUiOptions(props.uiSchema);
|
||||
|
||||
const { optionSchema, options, selection, createChangeHandler } =
|
||||
useMultiSelectField({
|
||||
schema: schema as BlockIOObjectSubSchema,
|
||||
formData,
|
||||
});
|
||||
|
||||
const handleValuesChange = createChangeHandler(onChange, fieldPathId);
|
||||
|
||||
const displayName = schema.title || "options";
|
||||
|
||||
return (
|
||||
<div className={cn("flex flex-col", uiOptions.className)}>
|
||||
<MultiSelector
|
||||
className="nodrag"
|
||||
values={selection}
|
||||
onValuesChange={handleValuesChange}
|
||||
>
|
||||
<MultiSelectorTrigger className="rounded-3xl border border-zinc-200 bg-white px-2 shadow-none">
|
||||
<MultiSelectorInput
|
||||
placeholder={
|
||||
(schema as any).placeholder ?? `Select ${displayName}...`
|
||||
}
|
||||
/>
|
||||
</MultiSelectorTrigger>
|
||||
<MultiSelectorContent className="nowheel">
|
||||
<MultiSelectorList>
|
||||
{options
|
||||
.map((key) => ({ ...optionSchema[key], key }))
|
||||
.map(({ key, title, description }) => (
|
||||
<MultiSelectorItem key={key} value={key} title={description}>
|
||||
{title ?? key}
|
||||
</MultiSelectorItem>
|
||||
))}
|
||||
</MultiSelectorList>
|
||||
</MultiSelectorContent>
|
||||
</MultiSelector>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1 +0,0 @@
|
||||
export { MultiSelectField } from "./MultiSelectField";
|
||||
@@ -1,65 +0,0 @@
|
||||
import { FieldProps } from "@rjsf/utils";
|
||||
import { BlockIOObjectSubSchema } from "@/lib/autogpt-server-api/types";
|
||||
|
||||
type FormData = Record<string, boolean> | null | undefined;
|
||||
|
||||
interface UseMultiSelectFieldOptions {
|
||||
schema: BlockIOObjectSubSchema;
|
||||
formData: FormData;
|
||||
}
|
||||
|
||||
export function useMultiSelectField({
|
||||
schema,
|
||||
formData,
|
||||
}: UseMultiSelectFieldOptions) {
|
||||
const getOptionSchema = (): Record<string, BlockIOObjectSubSchema> => {
|
||||
if (schema.properties) {
|
||||
return schema.properties as Record<string, BlockIOObjectSubSchema>;
|
||||
}
|
||||
if (
|
||||
"anyOf" in schema &&
|
||||
Array.isArray(schema.anyOf) &&
|
||||
schema.anyOf.length > 0 &&
|
||||
"properties" in schema.anyOf[0]
|
||||
) {
|
||||
return (schema.anyOf[0] as BlockIOObjectSubSchema).properties as Record<
|
||||
string,
|
||||
BlockIOObjectSubSchema
|
||||
>;
|
||||
}
|
||||
return {};
|
||||
};
|
||||
|
||||
const optionSchema = getOptionSchema();
|
||||
const options = Object.keys(optionSchema);
|
||||
|
||||
const getSelection = (): string[] => {
|
||||
if (!formData || typeof formData !== "object") {
|
||||
return [];
|
||||
}
|
||||
return Object.entries(formData)
|
||||
.filter(([_, value]) => value === true)
|
||||
.map(([key]) => key);
|
||||
};
|
||||
|
||||
const selection = getSelection();
|
||||
|
||||
const createChangeHandler =
|
||||
(
|
||||
onChange: FieldProps["onChange"],
|
||||
fieldPathId: FieldProps["fieldPathId"],
|
||||
) =>
|
||||
(values: string[]) => {
|
||||
const newValue = Object.fromEntries(
|
||||
options.map((opt) => [opt, values.includes(opt)]),
|
||||
);
|
||||
onChange(newValue, fieldPathId?.path);
|
||||
};
|
||||
|
||||
return {
|
||||
optionSchema,
|
||||
options,
|
||||
selection,
|
||||
createChangeHandler,
|
||||
};
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
import { descriptionId, FieldProps, getTemplate, titleId } from "@rjsf/utils";
|
||||
import { Table, RowData } from "@/components/molecules/Table/Table";
|
||||
import { useMemo } from "react";
|
||||
|
||||
export const TableField = (props: FieldProps) => {
|
||||
const { schema, formData, onChange, fieldPathId, registry, uiSchema } = props;
|
||||
|
||||
const itemSchema = schema.items as any;
|
||||
const properties = itemSchema?.properties || {};
|
||||
|
||||
const columns: string[] = useMemo(() => {
|
||||
return Object.keys(properties);
|
||||
}, [properties]);
|
||||
|
||||
const handleChange = (rows: RowData[]) => {
|
||||
onChange(rows, fieldPathId?.path.slice(0, -1));
|
||||
};
|
||||
|
||||
const TitleFieldTemplate = getTemplate("TitleFieldTemplate", registry);
|
||||
const DescriptionFieldTemplate = getTemplate(
|
||||
"DescriptionFieldTemplate",
|
||||
registry,
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
<TitleFieldTemplate
|
||||
id={titleId(fieldPathId)}
|
||||
title={schema.title || ""}
|
||||
required={true}
|
||||
schema={schema}
|
||||
uiSchema={uiSchema}
|
||||
registry={registry}
|
||||
/>
|
||||
<DescriptionFieldTemplate
|
||||
id={descriptionId(fieldPathId)}
|
||||
description={schema.description || ""}
|
||||
schema={schema}
|
||||
registry={registry}
|
||||
/>
|
||||
|
||||
<Table
|
||||
columns={columns}
|
||||
defaultValues={formData}
|
||||
onChange={handleChange}
|
||||
allowAddRow={true}
|
||||
allowDeleteRow={true}
|
||||
addRowLabel="Add row"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,10 +1,6 @@
|
||||
import { FieldProps, RJSFSchema, RegistryFieldsType } from "@rjsf/utils";
|
||||
import { CredentialsField } from "./CredentialField/CredentialField";
|
||||
import { GoogleDrivePickerField } from "./GoogleDrivePickerField/GoogleDrivePickerField";
|
||||
import { JsonTextField } from "./JsonTextField/JsonTextField";
|
||||
import { MultiSelectField } from "./MultiSelectField/MultiSelectField";
|
||||
import { isMultiSelectSchema } from "../utils/schema-utils";
|
||||
import { TableField } from "./TableField/TableField";
|
||||
|
||||
export interface CustomFieldDefinition {
|
||||
id: string;
|
||||
@@ -12,9 +8,6 @@ export interface CustomFieldDefinition {
|
||||
component: (props: FieldProps<any, RJSFSchema, any>) => JSX.Element | null;
|
||||
}
|
||||
|
||||
/** Field ID for JsonTextField - used to render nested complex types as text input */
|
||||
export const JSON_TEXT_FIELD_ID = "custom/json_text_field";
|
||||
|
||||
export const CUSTOM_FIELDS: CustomFieldDefinition[] = [
|
||||
{
|
||||
id: "custom/credential_field",
|
||||
@@ -37,28 +30,6 @@ export const CUSTOM_FIELDS: CustomFieldDefinition[] = [
|
||||
},
|
||||
component: GoogleDrivePickerField,
|
||||
},
|
||||
{
|
||||
id: "custom/json_text_field",
|
||||
// Not matched by schema - assigned via uiSchema for nested complex types
|
||||
matcher: () => false,
|
||||
component: JsonTextField,
|
||||
},
|
||||
{
|
||||
id: "custom/multi_select_field",
|
||||
matcher: isMultiSelectSchema,
|
||||
component: MultiSelectField,
|
||||
},
|
||||
{
|
||||
id: "custom/table_field",
|
||||
matcher: (schema: any) => {
|
||||
return (
|
||||
schema.type === "array" &&
|
||||
"format" in schema &&
|
||||
schema.format === "table"
|
||||
);
|
||||
},
|
||||
component: TableField,
|
||||
},
|
||||
];
|
||||
|
||||
export function findCustomFieldId(schema: any): string | null {
|
||||
|
||||
@@ -1,46 +1,19 @@
|
||||
import { RJSFSchema, UiSchema } from "@rjsf/utils";
|
||||
import {
|
||||
findCustomFieldId,
|
||||
JSON_TEXT_FIELD_ID,
|
||||
} from "../custom/custom-registry";
|
||||
|
||||
function isComplexType(schema: RJSFSchema): boolean {
|
||||
return schema.type === "object" || schema.type === "array";
|
||||
}
|
||||
|
||||
function hasComplexAnyOfOptions(schema: RJSFSchema): boolean {
|
||||
const options = schema.anyOf || schema.oneOf;
|
||||
if (!Array.isArray(options)) return false;
|
||||
return options.some(
|
||||
(opt: any) =>
|
||||
opt &&
|
||||
typeof opt === "object" &&
|
||||
(opt.type === "object" || opt.type === "array"),
|
||||
);
|
||||
}
|
||||
import { findCustomFieldId } from "../custom/custom-registry";
|
||||
|
||||
/**
|
||||
* Generates uiSchema with ui:field settings for custom fields based on schema matchers.
|
||||
* This is the standard RJSF way to route fields to custom components.
|
||||
*
|
||||
* Nested complex types (arrays/objects inside arrays/objects) are rendered as JsonTextField
|
||||
* to avoid deeply nested form UIs. Users can enter raw JSON for these fields.
|
||||
*
|
||||
* @param schema - The JSON schema
|
||||
* @param existingUiSchema - Existing uiSchema to merge with
|
||||
* @param insideComplexType - Whether we're already inside a complex type (object/array)
|
||||
*/
|
||||
export function generateUiSchemaForCustomFields(
|
||||
schema: RJSFSchema,
|
||||
existingUiSchema: UiSchema = {},
|
||||
insideComplexType: boolean = false,
|
||||
): UiSchema {
|
||||
const uiSchema: UiSchema = { ...existingUiSchema };
|
||||
|
||||
if (schema.properties) {
|
||||
for (const [key, propSchema] of Object.entries(schema.properties)) {
|
||||
if (propSchema && typeof propSchema === "object") {
|
||||
// First check for custom field matchers (credentials, google drive, etc.)
|
||||
const customFieldId = findCustomFieldId(propSchema);
|
||||
|
||||
if (customFieldId) {
|
||||
@@ -48,33 +21,8 @@ export function generateUiSchemaForCustomFields(
|
||||
...(uiSchema[key] as object),
|
||||
"ui:field": customFieldId,
|
||||
};
|
||||
// Skip further processing for custom fields
|
||||
continue;
|
||||
}
|
||||
|
||||
// Handle nested complex types - render as JsonTextField
|
||||
if (insideComplexType && isComplexType(propSchema as RJSFSchema)) {
|
||||
uiSchema[key] = {
|
||||
...(uiSchema[key] as object),
|
||||
"ui:field": JSON_TEXT_FIELD_ID,
|
||||
};
|
||||
// Don't recurse further - this field is now a text input
|
||||
continue;
|
||||
}
|
||||
|
||||
// Handle anyOf/oneOf inside complex types
|
||||
if (
|
||||
insideComplexType &&
|
||||
hasComplexAnyOfOptions(propSchema as RJSFSchema)
|
||||
) {
|
||||
uiSchema[key] = {
|
||||
...(uiSchema[key] as object),
|
||||
"ui:field": JSON_TEXT_FIELD_ID,
|
||||
};
|
||||
continue;
|
||||
}
|
||||
|
||||
// Recurse into object properties
|
||||
if (
|
||||
propSchema.type === "object" &&
|
||||
propSchema.properties &&
|
||||
@@ -83,7 +31,6 @@ export function generateUiSchemaForCustomFields(
|
||||
const nestedUiSchema = generateUiSchemaForCustomFields(
|
||||
propSchema as RJSFSchema,
|
||||
(uiSchema[key] as UiSchema) || {},
|
||||
true, // Now inside a complex type
|
||||
);
|
||||
uiSchema[key] = {
|
||||
...(uiSchema[key] as object),
|
||||
@@ -91,11 +38,9 @@ export function generateUiSchemaForCustomFields(
|
||||
};
|
||||
}
|
||||
|
||||
// Handle arrays
|
||||
if (propSchema.type === "array" && propSchema.items) {
|
||||
const itemsSchema = propSchema.items as RJSFSchema;
|
||||
if (itemsSchema && typeof itemsSchema === "object") {
|
||||
// Check for custom field on array items
|
||||
const itemsCustomFieldId = findCustomFieldId(itemsSchema);
|
||||
if (itemsCustomFieldId) {
|
||||
uiSchema[key] = {
|
||||
@@ -104,28 +49,10 @@ export function generateUiSchemaForCustomFields(
|
||||
"ui:field": itemsCustomFieldId,
|
||||
},
|
||||
};
|
||||
} else if (isComplexType(itemsSchema)) {
|
||||
// Array items that are complex types become JsonTextField
|
||||
uiSchema[key] = {
|
||||
...(uiSchema[key] as object),
|
||||
items: {
|
||||
"ui:field": JSON_TEXT_FIELD_ID,
|
||||
},
|
||||
};
|
||||
} else if (hasComplexAnyOfOptions(itemsSchema)) {
|
||||
// Array items with anyOf containing complex types become JsonTextField
|
||||
uiSchema[key] = {
|
||||
...(uiSchema[key] as object),
|
||||
items: {
|
||||
"ui:field": JSON_TEXT_FIELD_ID,
|
||||
},
|
||||
};
|
||||
} else if (itemsSchema.properties) {
|
||||
// Recurse into object items (but they're now inside a complex type)
|
||||
const itemsUiSchema = generateUiSchemaForCustomFields(
|
||||
itemsSchema,
|
||||
((uiSchema[key] as UiSchema)?.items as UiSchema) || {},
|
||||
true, // Inside complex type (array)
|
||||
);
|
||||
if (Object.keys(itemsUiSchema).length > 0) {
|
||||
uiSchema[key] = {
|
||||
@@ -136,61 +63,6 @@ export function generateUiSchemaForCustomFields(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle anyOf/oneOf at root level - process complex options
|
||||
if (!insideComplexType) {
|
||||
const anyOfOptions = propSchema.anyOf || propSchema.oneOf;
|
||||
|
||||
if (Array.isArray(anyOfOptions)) {
|
||||
for (let i = 0; i < anyOfOptions.length; i++) {
|
||||
const option = anyOfOptions[i] as RJSFSchema;
|
||||
if (option && typeof option === "object") {
|
||||
// Handle anyOf array options with complex items
|
||||
if (option.type === "array" && option.items) {
|
||||
const itemsSchema = option.items as RJSFSchema;
|
||||
if (itemsSchema && typeof itemsSchema === "object") {
|
||||
// Array items that are complex types become JsonTextField
|
||||
if (isComplexType(itemsSchema)) {
|
||||
uiSchema[key] = {
|
||||
...(uiSchema[key] as object),
|
||||
items: {
|
||||
"ui:field": JSON_TEXT_FIELD_ID,
|
||||
},
|
||||
};
|
||||
} else if (hasComplexAnyOfOptions(itemsSchema)) {
|
||||
uiSchema[key] = {
|
||||
...(uiSchema[key] as object),
|
||||
items: {
|
||||
"ui:field": JSON_TEXT_FIELD_ID,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Recurse into anyOf object options with properties
|
||||
if (
|
||||
option.type === "object" &&
|
||||
option.properties &&
|
||||
typeof option.properties === "object"
|
||||
) {
|
||||
const optionUiSchema = generateUiSchemaForCustomFields(
|
||||
option,
|
||||
{},
|
||||
true, // Inside complex type (anyOf object option)
|
||||
);
|
||||
if (Object.keys(optionUiSchema).length > 0) {
|
||||
// Store under the property key - RJSF will apply it
|
||||
uiSchema[key] = {
|
||||
...(uiSchema[key] as object),
|
||||
...optionUiSchema,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
import { getUiOptions, RJSFSchema, UiSchema } from "@rjsf/utils";
|
||||
|
||||
export function isAnyOfSchema(schema: RJSFSchema | undefined): boolean {
|
||||
return (
|
||||
Array.isArray(schema?.anyOf) &&
|
||||
schema!.anyOf.length > 0 &&
|
||||
schema?.enum === undefined
|
||||
);
|
||||
return Array.isArray(schema?.anyOf) && schema!.anyOf.length > 0;
|
||||
}
|
||||
|
||||
export const isAnyOfChild = (
|
||||
@@ -37,21 +33,3 @@ export function isOptionalType(schema: RJSFSchema | undefined): {
|
||||
export function isAnyOfSelector(name: string) {
|
||||
return name.includes("anyof_select");
|
||||
}
|
||||
|
||||
export function isMultiSelectSchema(schema: RJSFSchema | undefined): boolean {
|
||||
if (typeof schema !== "object" || schema === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ("anyOf" in schema || "oneOf" in schema) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !!(
|
||||
schema.type === "object" &&
|
||||
schema.properties &&
|
||||
Object.values(schema.properties).every(
|
||||
(prop: any) => prop.type === "boolean",
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as AccordionPrimitive from "@radix-ui/react-accordion"
|
||||
import { ChevronDown } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Accordion = AccordionPrimitive.Root
|
||||
|
||||
const AccordionItem = React.forwardRef<
|
||||
React.ElementRef<typeof AccordionPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AccordionPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn("border-b", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AccordionItem.displayName = "AccordionItem"
|
||||
|
||||
const AccordionTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof AccordionPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<AccordionPrimitive.Header className="flex">
|
||||
<AccordionPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex flex-1 items-center justify-between py-4 text-sm font-medium transition-all hover:underline text-left [&[data-state=open]>svg]:rotate-180",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronDown className="h-4 w-4 shrink-0 text-neutral-500 transition-transform duration-200 dark:text-neutral-400" />
|
||||
</AccordionPrimitive.Trigger>
|
||||
</AccordionPrimitive.Header>
|
||||
))
|
||||
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
|
||||
|
||||
const AccordionContent = React.forwardRef<
|
||||
React.ElementRef<typeof AccordionPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<AccordionPrimitive.Content
|
||||
ref={ref}
|
||||
className="overflow-hidden text-sm data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
|
||||
{...props}
|
||||
>
|
||||
<div className={cn("pb-4 pt-0", className)}>{children}</div>
|
||||
</AccordionPrimitive.Content>
|
||||
))
|
||||
AccordionContent.displayName = AccordionPrimitive.Content.displayName
|
||||
|
||||
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
|
||||
@@ -352,10 +352,6 @@ export default class BackendAPI {
|
||||
return this._get("/integrations/providers");
|
||||
}
|
||||
|
||||
listSystemProviders(): Promise<string[]> {
|
||||
return this._get("/integrations/providers/system");
|
||||
}
|
||||
|
||||
listCredentials(provider?: string): Promise<CredentialsMetaResponse[]> {
|
||||
return this._get(
|
||||
provider
|
||||
|
||||
@@ -593,7 +593,6 @@ export type CredentialsMetaResponse = {
|
||||
scopes?: Array<string>;
|
||||
username?: string;
|
||||
host?: string;
|
||||
is_system?: boolean;
|
||||
};
|
||||
|
||||
/* Mirror of backend/server/integrations/router.py:CredentialsDeletionResponse */
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useToastOnFail } from "@/components/molecules/Toast/use-toast";
|
||||
import { createContext, useCallback, useEffect, useState } from "react";
|
||||
import { useSupabase } from "@/lib/supabase/hooks/useSupabase";
|
||||
import {
|
||||
APIKeyCredentials,
|
||||
CredentialsDeleteNeedConfirmationResponse,
|
||||
@@ -9,9 +10,8 @@ import {
|
||||
UserPasswordCredentials,
|
||||
} from "@/lib/autogpt-server-api";
|
||||
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
|
||||
import { useSupabase } from "@/lib/supabase/hooks/useSupabase";
|
||||
import { useToastOnFail } from "@/components/molecules/Toast/use-toast";
|
||||
import { toDisplayName } from "@/providers/agent-credentials/helper";
|
||||
import { createContext, useCallback, useEffect, useState } from "react";
|
||||
|
||||
type APIKeyCredentialsCreatable = Omit<
|
||||
APIKeyCredentials,
|
||||
@@ -32,8 +32,6 @@ export type CredentialsProviderData = {
|
||||
provider: CredentialsProviderName;
|
||||
providerName: string;
|
||||
savedCredentials: CredentialsMetaResponse[];
|
||||
/** Whether this provider has platform credits available (system credentials) */
|
||||
isSystemProvider: boolean;
|
||||
oAuthCallback: (
|
||||
code: string,
|
||||
state_token: string,
|
||||
@@ -70,9 +68,6 @@ export default function CredentialsProvider({
|
||||
const [providers, setProviders] =
|
||||
useState<CredentialsProvidersContextType | null>(null);
|
||||
const [providerNames, setProviderNames] = useState<string[]>([]);
|
||||
const [systemProviders, setSystemProviders] = useState<Set<string>>(
|
||||
new Set(),
|
||||
);
|
||||
const { isLoggedIn } = useSupabase();
|
||||
const api = useBackendAPI();
|
||||
const onFailToast = useToastOnFail();
|
||||
@@ -223,7 +218,17 @@ export default function CredentialsProvider({
|
||||
[api, onFailToast],
|
||||
);
|
||||
|
||||
const loadCredentials = useCallback(() => {
|
||||
// Fetch provider names on mount
|
||||
useEffect(() => {
|
||||
api
|
||||
.listProviders()
|
||||
.then((names) => {
|
||||
setProviderNames(names);
|
||||
})
|
||||
.catch(onFailToast("load provider names"));
|
||||
}, [api, onFailToast]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLoggedIn || providerNames.length === 0) {
|
||||
if (isLoggedIn == false) setProviders({});
|
||||
return;
|
||||
@@ -246,32 +251,27 @@ export default function CredentialsProvider({
|
||||
setProviders((prev) => ({
|
||||
...prev,
|
||||
...Object.fromEntries(
|
||||
providerNames.map((provider) => {
|
||||
const providerCredentials = credentialsByProvider[provider] ?? [];
|
||||
|
||||
return [
|
||||
providerNames.map((provider) => [
|
||||
provider,
|
||||
{
|
||||
provider,
|
||||
{
|
||||
provider,
|
||||
providerName: toDisplayName(provider as string),
|
||||
savedCredentials: providerCredentials,
|
||||
isSystemProvider: systemProviders.has(provider),
|
||||
oAuthCallback: (code: string, state_token: string) =>
|
||||
oAuthCallback(provider, code, state_token),
|
||||
createAPIKeyCredentials: (
|
||||
credentials: APIKeyCredentialsCreatable,
|
||||
) => createAPIKeyCredentials(provider, credentials),
|
||||
createUserPasswordCredentials: (
|
||||
credentials: UserPasswordCredentialsCreatable,
|
||||
) => createUserPasswordCredentials(provider, credentials),
|
||||
createHostScopedCredentials: (
|
||||
credentials: HostScopedCredentialsCreatable,
|
||||
) => createHostScopedCredentials(provider, credentials),
|
||||
deleteCredentials: (id: string, force: boolean = false) =>
|
||||
deleteCredentials(provider, id, force),
|
||||
} satisfies CredentialsProviderData,
|
||||
];
|
||||
}),
|
||||
providerName: toDisplayName(provider as string),
|
||||
savedCredentials: credentialsByProvider[provider] ?? [],
|
||||
oAuthCallback: (code: string, state_token: string) =>
|
||||
oAuthCallback(provider, code, state_token),
|
||||
createAPIKeyCredentials: (
|
||||
credentials: APIKeyCredentialsCreatable,
|
||||
) => createAPIKeyCredentials(provider, credentials),
|
||||
createUserPasswordCredentials: (
|
||||
credentials: UserPasswordCredentialsCreatable,
|
||||
) => createUserPasswordCredentials(provider, credentials),
|
||||
createHostScopedCredentials: (
|
||||
credentials: HostScopedCredentialsCreatable,
|
||||
) => createHostScopedCredentials(provider, credentials),
|
||||
deleteCredentials: (id: string, force: boolean = false) =>
|
||||
deleteCredentials(provider, id, force),
|
||||
} satisfies CredentialsProviderData,
|
||||
]),
|
||||
),
|
||||
}));
|
||||
})
|
||||
@@ -280,7 +280,6 @@ export default function CredentialsProvider({
|
||||
api,
|
||||
isLoggedIn,
|
||||
providerNames,
|
||||
systemProviders,
|
||||
createAPIKeyCredentials,
|
||||
createUserPasswordCredentials,
|
||||
createHostScopedCredentials,
|
||||
@@ -289,20 +288,6 @@ export default function CredentialsProvider({
|
||||
onFailToast,
|
||||
]);
|
||||
|
||||
// Fetch provider names and system providers on mount
|
||||
useEffect(() => {
|
||||
Promise.all([api.listProviders(), api.listSystemProviders()])
|
||||
.then(([names, systemList]) => {
|
||||
setProviderNames(names);
|
||||
setSystemProviders(new Set(systemList));
|
||||
})
|
||||
.catch(onFailToast("Load provider names"));
|
||||
}, [api, onFailToast]);
|
||||
|
||||
useEffect(() => {
|
||||
loadCredentials();
|
||||
}, [loadCredentials]);
|
||||
|
||||
return (
|
||||
<CredentialsProvidersContext.Provider value={providers}>
|
||||
{children}
|
||||
|
||||
@@ -22,7 +22,13 @@ const config = {
|
||||
poppins: ["var(--font-poppins)"],
|
||||
},
|
||||
colors: {
|
||||
// *** APPROVED DESIGN SYSTEM COLORS ***
|
||||
// These are the ONLY colors that should be used in our app
|
||||
...colors,
|
||||
|
||||
// Legacy colors - DO NOT USE THESE IN NEW CODE
|
||||
// These are kept only to prevent breaking existing styles
|
||||
// Use the approved design system colors above instead
|
||||
border: "hsl(var(--border))",
|
||||
input: "hsl(var(--input))",
|
||||
ring: "hsl(var(--ring))",
|
||||
@@ -57,66 +63,70 @@ const config = {
|
||||
foreground: "hsl(var(--card-foreground))",
|
||||
},
|
||||
customGray: {
|
||||
"100": "#d9d9d9",
|
||||
"200": "#a8a8a8",
|
||||
"300": "#878787",
|
||||
"400": "#646464",
|
||||
"500": "#474747",
|
||||
"600": "#282828",
|
||||
"700": "#272727",
|
||||
100: "#d9d9d9",
|
||||
200: "#a8a8a8",
|
||||
300: "#878787",
|
||||
400: "#646464",
|
||||
500: "#474747",
|
||||
600: "#282828",
|
||||
700: "#272727",
|
||||
},
|
||||
},
|
||||
spacing: {
|
||||
"0": "0rem",
|
||||
"1": "0.25rem",
|
||||
"2": "0.5rem",
|
||||
"3": "0.75rem",
|
||||
"4": "1rem",
|
||||
"5": "1.25rem",
|
||||
"6": "1.5rem",
|
||||
"7": "1.75rem",
|
||||
"8": "2rem",
|
||||
"9": "2.25rem",
|
||||
"10": "2.5rem",
|
||||
"11": "2.75rem",
|
||||
"12": "3rem",
|
||||
"14": "3.5rem",
|
||||
"16": "4rem",
|
||||
"18": "4.5rem",
|
||||
"20": "5rem",
|
||||
"24": "6rem",
|
||||
"28": "7rem",
|
||||
"32": "8rem",
|
||||
"36": "9rem",
|
||||
"40": "10rem",
|
||||
"44": "11rem",
|
||||
"48": "12rem",
|
||||
"52": "13rem",
|
||||
"56": "14rem",
|
||||
"60": "15rem",
|
||||
"64": "16rem",
|
||||
"68": "17rem",
|
||||
"70": "17.5rem",
|
||||
"71": "17.75rem",
|
||||
"72": "18rem",
|
||||
"76": "19rem",
|
||||
"80": "20rem",
|
||||
"96": "24rem",
|
||||
"0.5": "0.125rem",
|
||||
"1.5": "0.375rem",
|
||||
"2.5": "0.625rem",
|
||||
"3.5": "0.875rem",
|
||||
"7.5": "1.875rem",
|
||||
"8.5": "2.125rem",
|
||||
// Tailwind spacing + custom sizes
|
||||
0: "0rem", // 0px
|
||||
0.5: "0.125rem", // 2px
|
||||
1: "0.25rem", // 4px
|
||||
1.5: "0.375rem", // 6px
|
||||
2: "0.5rem", // 8px
|
||||
2.5: "0.625rem", // 10px
|
||||
3: "0.75rem", // 12px
|
||||
3.5: "0.875rem", // 14px
|
||||
4: "1rem", // 16px
|
||||
5: "1.25rem", // 20px
|
||||
6: "1.5rem", // 24px
|
||||
7: "1.75rem", // 28px
|
||||
7.5: "1.875rem", // 30px
|
||||
8: "2rem", // 32px
|
||||
8.5: "2.125rem", // 34px
|
||||
9: "2.25rem", // 36px
|
||||
10: "2.5rem", // 40px
|
||||
11: "2.75rem", // 44px
|
||||
12: "3rem", // 48px
|
||||
14: "3.5rem", // 56px
|
||||
16: "4rem", // 64px
|
||||
18: "4.5rem", // 72px
|
||||
20: "5rem", // 80px
|
||||
24: "6rem", // 96px
|
||||
28: "7rem", // 112px
|
||||
32: "8rem", // 128px
|
||||
36: "9rem", // 144px
|
||||
40: "10rem", // 160px
|
||||
44: "11rem", // 176px
|
||||
48: "12rem", // 192px
|
||||
52: "13rem", // 208px
|
||||
56: "14rem", // 224px
|
||||
60: "15rem", // 240px
|
||||
64: "16rem", // 256px
|
||||
68: "17rem", // 272px
|
||||
70: "17.5rem", // 280px
|
||||
71: "17.75rem", // 284px
|
||||
72: "18rem", // 288px
|
||||
76: "19rem", // 304px
|
||||
80: "20rem", // 320px
|
||||
96: "24rem", // 384px
|
||||
},
|
||||
borderRadius: {
|
||||
xsmall: "0.25rem",
|
||||
small: "0.5rem",
|
||||
medium: "0.75rem",
|
||||
large: "1rem",
|
||||
xlarge: "1.25rem",
|
||||
"2xlarge": "1.5rem",
|
||||
full: "9999px",
|
||||
// Design system border radius tokens from Figma
|
||||
xsmall: "0.25rem", // 4px
|
||||
small: "0.5rem", // 8px
|
||||
medium: "0.75rem", // 12px
|
||||
large: "1rem", // 16px
|
||||
xlarge: "1.25rem", // 20px
|
||||
"2xlarge": "1.5rem", // 24px
|
||||
full: "9999px", // For pill buttons
|
||||
|
||||
// Legacy values - kept for backward compatibility
|
||||
lg: "var(--radius)",
|
||||
md: "calc(var(--radius) - 2px)",
|
||||
sm: "calc(var(--radius) - 4px)",
|
||||
@@ -126,28 +136,16 @@ const config = {
|
||||
},
|
||||
keyframes: {
|
||||
"accordion-down": {
|
||||
from: {
|
||||
height: "0",
|
||||
},
|
||||
to: {
|
||||
height: "var(--radix-accordion-content-height)",
|
||||
},
|
||||
from: { height: "0" },
|
||||
to: { height: "var(--radix-accordion-content-height)" },
|
||||
},
|
||||
"accordion-up": {
|
||||
from: {
|
||||
height: "var(--radix-accordion-content-height)",
|
||||
},
|
||||
to: {
|
||||
height: "0",
|
||||
},
|
||||
from: { height: "var(--radix-accordion-content-height)" },
|
||||
to: { height: "0" },
|
||||
},
|
||||
"fade-in": {
|
||||
"0%": {
|
||||
opacity: "0",
|
||||
},
|
||||
"100%": {
|
||||
opacity: "1",
|
||||
},
|
||||
"0%": { opacity: "0" }, // Start with opacity 0
|
||||
"100%": { opacity: "1" }, // End with opacity 1
|
||||
},
|
||||
},
|
||||
animation: {
|
||||
|
||||
75
docs/platform/blocks/airtable/bases.md
Normal file
75
docs/platform/blocks/airtable/bases.md
Normal file
@@ -0,0 +1,75 @@
|
||||
# Airtable Create Base
|
||||
|
||||
### What it is
|
||||
Create or find a base in Airtable
|
||||
|
||||
### How it works
|
||||
<!-- MANUAL: how_it_works -->
|
||||
This block creates a new Airtable base in a specified workspace, or finds an existing one with the same name. When creating, you can optionally define initial tables and their fields to set up the schema.
|
||||
|
||||
Enable find_existing to search for a base with the same name before creating a new one, preventing duplicates in your workspace.
|
||||
<!-- END MANUAL -->
|
||||
|
||||
### Inputs
|
||||
| Input | Description | Type | Required |
|
||||
|-------|-------------|------|----------|
|
||||
| workspace_id | The workspace ID where the base will be created | str | Yes |
|
||||
| name | The name of the new base | str | Yes |
|
||||
| find_existing | If true, return existing base with same name instead of creating duplicate | bool | No |
|
||||
| tables | At least one table and field must be specified. Array of table objects to create in the base. Each table should have 'name' and 'fields' properties | List[Dict[str, True]] | No |
|
||||
|
||||
### Outputs
|
||||
| Output | Description | Type |
|
||||
|--------|-------------|------|
|
||||
| error | Error message if the operation failed | str |
|
||||
| base_id | The ID of the created or found base | str |
|
||||
| tables | Array of table objects | List[Dict[str, True]] |
|
||||
| table | A single table object | Dict[str, True] |
|
||||
| was_created | True if a new base was created, False if existing was found | bool |
|
||||
|
||||
### Possible use case
|
||||
<!-- MANUAL: use_case -->
|
||||
**Project Setup**: Automatically create new bases when projects start with predefined table structures.
|
||||
|
||||
**Template Deployment**: Deploy standardized base templates across teams or clients.
|
||||
|
||||
**Multi-Tenant Apps**: Create separate bases for each customer or project programmatically.
|
||||
<!-- END MANUAL -->
|
||||
|
||||
---
|
||||
|
||||
## Airtable List Bases
|
||||
|
||||
### What it is
|
||||
List all bases in Airtable
|
||||
|
||||
### How it works
|
||||
<!-- MANUAL: how_it_works -->
|
||||
This block retrieves a list of all Airtable bases accessible to your connected account. It returns basic information about each base including ID, name, and permission level.
|
||||
|
||||
Results are paginated; use the offset output to retrieve additional pages if there are more bases than returned in a single call.
|
||||
<!-- END MANUAL -->
|
||||
|
||||
### Inputs
|
||||
| Input | Description | Type | Required |
|
||||
|-------|-------------|------|----------|
|
||||
| trigger | Trigger the block to run - value is ignored | str | No |
|
||||
| offset | Pagination offset from previous request | str | No |
|
||||
|
||||
### Outputs
|
||||
| Output | Description | Type |
|
||||
|--------|-------------|------|
|
||||
| error | Error message if the operation failed | str |
|
||||
| bases | Array of base objects | List[Dict[str, True]] |
|
||||
| offset | Offset for next page (null if no more bases) | str |
|
||||
|
||||
### Possible use case
|
||||
<!-- MANUAL: use_case -->
|
||||
**Base Discovery**: Find available bases for building dynamic dropdowns or navigation.
|
||||
|
||||
**Inventory Management**: List all bases in an organization for auditing or documentation.
|
||||
|
||||
**Cross-Base Operations**: Enumerate bases to perform operations across multiple databases.
|
||||
<!-- END MANUAL -->
|
||||
|
||||
---
|
||||
199
docs/platform/blocks/airtable/records.md
Normal file
199
docs/platform/blocks/airtable/records.md
Normal file
@@ -0,0 +1,199 @@
|
||||
# Airtable Create Records
|
||||
|
||||
### What it is
|
||||
Create records in an Airtable table
|
||||
|
||||
### How it works
|
||||
<!-- MANUAL: how_it_works -->
|
||||
This block creates new records in an Airtable table using the Airtable API. Each record is specified with a fields object containing field names and values. You can create up to 10 records in a single call.
|
||||
|
||||
Enable typecast to automatically convert string values to appropriate field types (dates, numbers, etc.). The block returns the created records with their assigned IDs.
|
||||
<!-- END MANUAL -->
|
||||
|
||||
### Inputs
|
||||
| Input | Description | Type | Required |
|
||||
|-------|-------------|------|----------|
|
||||
| base_id | The Airtable base ID | str | Yes |
|
||||
| table_id_or_name | Table ID or name | str | Yes |
|
||||
| records | Array of records to create (each with 'fields' object) | List[Dict[str, True]] | Yes |
|
||||
| skip_normalization | Skip output normalization to get raw Airtable response (faster but may have missing fields) | bool | No |
|
||||
| typecast | Automatically convert string values to appropriate types | bool | No |
|
||||
| return_fields_by_field_id | Return fields by field ID | bool | No |
|
||||
|
||||
### Outputs
|
||||
| Output | Description | Type |
|
||||
|--------|-------------|------|
|
||||
| error | Error message if the operation failed | str |
|
||||
| records | Array of created record objects | List[Dict[str, True]] |
|
||||
| details | Details of the created records | Dict[str, True] |
|
||||
|
||||
### Possible use case
|
||||
<!-- MANUAL: use_case -->
|
||||
**Data Import**: Bulk import data from external sources into Airtable tables.
|
||||
|
||||
**Form Submissions**: Create records from form submissions or API integrations.
|
||||
|
||||
**Workflow Output**: Save workflow results or processed data to Airtable for tracking.
|
||||
<!-- END MANUAL -->
|
||||
|
||||
---
|
||||
|
||||
## Airtable Delete Records
|
||||
|
||||
### What it is
|
||||
Delete records from an Airtable table
|
||||
|
||||
### How it works
|
||||
<!-- MANUAL: how_it_works -->
|
||||
This block deletes records from an Airtable table by their record IDs. You can delete up to 10 records in a single call. The operation is permanent and cannot be undone.
|
||||
|
||||
Provide an array of record IDs to delete. Using the table ID instead of the name is recommended for reliability.
|
||||
<!-- END MANUAL -->
|
||||
|
||||
### Inputs
|
||||
| Input | Description | Type | Required |
|
||||
|-------|-------------|------|----------|
|
||||
| base_id | The Airtable base ID | str | Yes |
|
||||
| table_id_or_name | Table ID or name - It's better to use the table ID instead of the name | str | Yes |
|
||||
| record_ids | Array of upto 10 record IDs to delete | List[str] | Yes |
|
||||
|
||||
### Outputs
|
||||
| Output | Description | Type |
|
||||
|--------|-------------|------|
|
||||
| error | Error message if the operation failed | str |
|
||||
| records | Array of deletion results | List[Dict[str, True]] |
|
||||
|
||||
### Possible use case
|
||||
<!-- MANUAL: use_case -->
|
||||
**Data Cleanup**: Remove outdated or duplicate records from tables.
|
||||
|
||||
**Workflow Cleanup**: Delete temporary records after processing is complete.
|
||||
|
||||
**Batch Removal**: Remove multiple records that match certain criteria.
|
||||
<!-- END MANUAL -->
|
||||
|
||||
---
|
||||
|
||||
## Airtable Get Record
|
||||
|
||||
### What it is
|
||||
Get a single record from Airtable
|
||||
|
||||
### How it works
|
||||
<!-- MANUAL: how_it_works -->
|
||||
This block retrieves a single record from an Airtable table by its ID. The record includes all field values and metadata like creation time. Enable normalize_output to ensure all fields are included with proper empty values.
|
||||
|
||||
Optionally include field metadata for type information and configuration details about each field.
|
||||
<!-- END MANUAL -->
|
||||
|
||||
### Inputs
|
||||
| Input | Description | Type | Required |
|
||||
|-------|-------------|------|----------|
|
||||
| base_id | The Airtable base ID | str | Yes |
|
||||
| table_id_or_name | Table ID or name | str | Yes |
|
||||
| record_id | The record ID to retrieve | str | Yes |
|
||||
| normalize_output | Normalize output to include all fields with proper empty values (disable to skip schema fetch and get raw Airtable response) | bool | No |
|
||||
| include_field_metadata | Include field type and configuration metadata (requires normalize_output=true) | bool | No |
|
||||
|
||||
### Outputs
|
||||
| Output | Description | Type |
|
||||
|--------|-------------|------|
|
||||
| error | Error message if the operation failed | str |
|
||||
| id | The record ID | str |
|
||||
| fields | The record fields | Dict[str, True] |
|
||||
| created_time | The record created time | str |
|
||||
| field_metadata | Field type and configuration metadata (only when include_field_metadata=true) | Dict[str, True] |
|
||||
|
||||
### Possible use case
|
||||
<!-- MANUAL: use_case -->
|
||||
**Detail View**: Fetch complete record data for display or detailed processing.
|
||||
|
||||
**Record Lookup**: Retrieve specific records by ID from webhook payloads or references.
|
||||
|
||||
**Data Validation**: Check record contents before performing updates or related operations.
|
||||
<!-- END MANUAL -->
|
||||
|
||||
---
|
||||
|
||||
## Airtable List Records
|
||||
|
||||
### What it is
|
||||
List records from an Airtable table
|
||||
|
||||
### How it works
|
||||
<!-- MANUAL: how_it_works -->
|
||||
This block queries records from an Airtable table with optional filtering, sorting, and pagination. Use Airtable formulas to filter records and specify sort order by field and direction.
|
||||
|
||||
Results can be limited, paginated with offsets, and restricted to specific fields. Enable normalize_output for consistent field values across records.
|
||||
<!-- END MANUAL -->
|
||||
|
||||
### Inputs
|
||||
| Input | Description | Type | Required |
|
||||
|-------|-------------|------|----------|
|
||||
| base_id | The Airtable base ID | str | Yes |
|
||||
| table_id_or_name | Table ID or name | str | Yes |
|
||||
| filter_formula | Airtable formula to filter records | str | No |
|
||||
| view | View ID or name to use | str | No |
|
||||
| sort | Sort configuration (array of {field, direction}) | List[Dict[str, True]] | No |
|
||||
| max_records | Maximum number of records to return | int | No |
|
||||
| page_size | Number of records per page (max 100) | int | No |
|
||||
| offset | Pagination offset from previous request | str | No |
|
||||
| return_fields | Specific fields to return (comma-separated) | List[str] | No |
|
||||
| normalize_output | Normalize output to include all fields with proper empty values (disable to skip schema fetch and get raw Airtable response) | bool | No |
|
||||
| include_field_metadata | Include field type and configuration metadata (requires normalize_output=true) | bool | No |
|
||||
|
||||
### Outputs
|
||||
| Output | Description | Type |
|
||||
|--------|-------------|------|
|
||||
| error | Error message if the operation failed | str |
|
||||
| records | Array of record objects | List[Dict[str, True]] |
|
||||
| offset | Offset for next page (null if no more records) | str |
|
||||
| field_metadata | Field type and configuration metadata (only when include_field_metadata=true) | Dict[str, True] |
|
||||
|
||||
### Possible use case
|
||||
<!-- MANUAL: use_case -->
|
||||
**Report Generation**: Query records with filters to build reports or dashboards.
|
||||
|
||||
**Data Export**: Fetch records matching criteria for export to other systems.
|
||||
|
||||
**Batch Processing**: List records to process in subsequent workflow steps.
|
||||
<!-- END MANUAL -->
|
||||
|
||||
---
|
||||
|
||||
## Airtable Update Records
|
||||
|
||||
### What it is
|
||||
Update records in an Airtable table
|
||||
|
||||
### How it works
|
||||
<!-- MANUAL: how_it_works -->
|
||||
This block updates existing records in an Airtable table. Each record update requires the record ID and a fields object with the values to update. Only specified fields are modified; other fields remain unchanged.
|
||||
|
||||
Enable typecast to automatically convert string values to appropriate types. You can update up to 10 records per call.
|
||||
<!-- END MANUAL -->
|
||||
|
||||
### Inputs
|
||||
| Input | Description | Type | Required |
|
||||
|-------|-------------|------|----------|
|
||||
| base_id | The Airtable base ID | str | Yes |
|
||||
| table_id_or_name | Table ID or name - It's better to use the table ID instead of the name | str | Yes |
|
||||
| records | Array of records to update (each with 'id' and 'fields') | List[Dict[str, True]] | Yes |
|
||||
| typecast | Automatically convert string values to appropriate types | bool | No |
|
||||
|
||||
### Outputs
|
||||
| Output | Description | Type |
|
||||
|--------|-------------|------|
|
||||
| error | Error message if the operation failed | str |
|
||||
| records | Array of updated record objects | List[Dict[str, True]] |
|
||||
|
||||
### Possible use case
|
||||
<!-- MANUAL: use_case -->
|
||||
**Status Updates**: Update record status fields as workflows progress.
|
||||
|
||||
**Data Enrichment**: Add computed or fetched data to existing records.
|
||||
|
||||
**Batch Modifications**: Update multiple records based on processed results.
|
||||
<!-- END MANUAL -->
|
||||
|
||||
---
|
||||
187
docs/platform/blocks/airtable/schema.md
Normal file
187
docs/platform/blocks/airtable/schema.md
Normal file
@@ -0,0 +1,187 @@
|
||||
# Airtable Create Field
|
||||
|
||||
### What it is
|
||||
Add a new field to an Airtable table
|
||||
|
||||
### How it works
|
||||
<!-- MANUAL: how_it_works -->
|
||||
This block adds a new field to an existing Airtable table using the Airtable API. Specify the field type (text, email, URL, etc.), name, and optional description and configuration options.
|
||||
|
||||
The field is created immediately and becomes available for use in all records. Returns the created field object with its assigned ID.
|
||||
<!-- END MANUAL -->
|
||||
|
||||
### Inputs
|
||||
| Input | Description | Type | Required |
|
||||
|-------|-------------|------|----------|
|
||||
| base_id | The Airtable base ID | str | Yes |
|
||||
| table_id | The table ID to add field to | str | Yes |
|
||||
| field_type | The type of the field to create | "singleLineText" | "email" | "url" | No |
|
||||
| name | The name of the field to create | str | Yes |
|
||||
| description | The description of the field to create | str | No |
|
||||
| options | The options of the field to create | Dict[str, str] | No |
|
||||
|
||||
### Outputs
|
||||
| Output | Description | Type |
|
||||
|--------|-------------|------|
|
||||
| error | Error message if the operation failed | str |
|
||||
| field | Created field object | Dict[str, True] |
|
||||
| field_id | ID of the created field | str |
|
||||
|
||||
### Possible use case
|
||||
<!-- MANUAL: use_case -->
|
||||
**Schema Evolution**: Add new fields to tables as application requirements grow.
|
||||
|
||||
**Dynamic Forms**: Create fields based on user configuration or form builder settings.
|
||||
|
||||
**Data Integration**: Add fields to capture data from newly integrated external systems.
|
||||
<!-- END MANUAL -->
|
||||
|
||||
---
|
||||
|
||||
## Airtable Create Table
|
||||
|
||||
### What it is
|
||||
Create a new table in an Airtable base
|
||||
|
||||
### How it works
|
||||
<!-- MANUAL: how_it_works -->
|
||||
This block creates a new table in an Airtable base with the specified name and optional field definitions. Each field definition includes name, type, and type-specific options.
|
||||
|
||||
The table is created with the defined schema and is immediately ready for use. Returns the created table object with its ID.
|
||||
<!-- END MANUAL -->
|
||||
|
||||
### Inputs
|
||||
| Input | Description | Type | Required |
|
||||
|-------|-------------|------|----------|
|
||||
| base_id | The Airtable base ID | str | Yes |
|
||||
| table_name | The name of the table to create | str | Yes |
|
||||
| table_fields | Table fields with name, type, and options | List[Dict[str, True]] | No |
|
||||
|
||||
### Outputs
|
||||
| Output | Description | Type |
|
||||
|--------|-------------|------|
|
||||
| error | Error message if the operation failed | str |
|
||||
| table | Created table object | Dict[str, True] |
|
||||
| table_id | ID of the created table | str |
|
||||
|
||||
### Possible use case
|
||||
<!-- MANUAL: use_case -->
|
||||
**Application Scaffolding**: Create tables programmatically when setting up new application modules.
|
||||
|
||||
**Multi-Tenant Setup**: Generate customer-specific tables dynamically.
|
||||
|
||||
**Feature Expansion**: Add new tables as features are enabled or installed.
|
||||
<!-- END MANUAL -->
|
||||
|
||||
---
|
||||
|
||||
## Airtable List Schema
|
||||
|
||||
### What it is
|
||||
Get the complete schema of an Airtable base
|
||||
|
||||
### How it works
|
||||
<!-- MANUAL: how_it_works -->
|
||||
This block retrieves the complete schema of an Airtable base, including all tables, their fields, field types, and views. This metadata is essential for building dynamic integrations that need to understand table structure.
|
||||
|
||||
The schema includes field configurations, validation rules, and relationship definitions between tables.
|
||||
<!-- END MANUAL -->
|
||||
|
||||
### Inputs
|
||||
| Input | Description | Type | Required |
|
||||
|-------|-------------|------|----------|
|
||||
| base_id | The Airtable base ID | str | Yes |
|
||||
|
||||
### Outputs
|
||||
| Output | Description | Type |
|
||||
|--------|-------------|------|
|
||||
| error | Error message if the operation failed | str |
|
||||
| base_schema | Complete base schema with tables, fields, and views | Dict[str, True] |
|
||||
| tables | Array of table objects | List[Dict[str, True]] |
|
||||
|
||||
### Possible use case
|
||||
<!-- MANUAL: use_case -->
|
||||
**Schema Discovery**: Understand table structure for building dynamic forms or queries.
|
||||
|
||||
**Documentation**: Generate documentation of database schema automatically.
|
||||
|
||||
**Migration Planning**: Analyze schema before migrating data to other systems.
|
||||
<!-- END MANUAL -->
|
||||
|
||||
---
|
||||
|
||||
## Airtable Update Field
|
||||
|
||||
### What it is
|
||||
Update field properties in an Airtable table
|
||||
|
||||
### How it works
|
||||
<!-- MANUAL: how_it_works -->
|
||||
This block updates properties of an existing field in an Airtable table. You can modify the field name and description. Note that field type cannot be changed after creation.
|
||||
|
||||
Changes take effect immediately across all records and views that use the field.
|
||||
<!-- END MANUAL -->
|
||||
|
||||
### Inputs
|
||||
| Input | Description | Type | Required |
|
||||
|-------|-------------|------|----------|
|
||||
| base_id | The Airtable base ID | str | Yes |
|
||||
| table_id | The table ID containing the field | str | Yes |
|
||||
| field_id | The field ID to update | str | Yes |
|
||||
| name | The name of the field to update | str | No |
|
||||
| description | The description of the field to update | str | No |
|
||||
|
||||
### Outputs
|
||||
| Output | Description | Type |
|
||||
|--------|-------------|------|
|
||||
| error | Error message if the operation failed | str |
|
||||
| field | Updated field object | Dict[str, True] |
|
||||
|
||||
### Possible use case
|
||||
<!-- MANUAL: use_case -->
|
||||
**Field Renaming**: Update field names to match evolving terminology or standards.
|
||||
|
||||
**Documentation Updates**: Add or update field descriptions for better team understanding.
|
||||
|
||||
**Schema Maintenance**: Keep field metadata current as application requirements change.
|
||||
<!-- END MANUAL -->
|
||||
|
||||
---
|
||||
|
||||
## Airtable Update Table
|
||||
|
||||
### What it is
|
||||
Update table properties
|
||||
|
||||
### How it works
|
||||
<!-- MANUAL: how_it_works -->
|
||||
This block updates table properties in an Airtable base. You can change the table name, description, and date dependency settings. Changes apply immediately and affect all users accessing the table.
|
||||
|
||||
This is useful for maintaining table metadata and organizing your base structure.
|
||||
<!-- END MANUAL -->
|
||||
|
||||
### Inputs
|
||||
| Input | Description | Type | Required |
|
||||
|-------|-------------|------|----------|
|
||||
| base_id | The Airtable base ID | str | Yes |
|
||||
| table_id | The table ID to update | str | Yes |
|
||||
| table_name | The name of the table to update | str | No |
|
||||
| table_description | The description of the table to update | str | No |
|
||||
| date_dependency | The date dependency of the table to update | Dict[str, True] | No |
|
||||
|
||||
### Outputs
|
||||
| Output | Description | Type |
|
||||
|--------|-------------|------|
|
||||
| error | Error message if the operation failed | str |
|
||||
| table | Updated table object | Dict[str, True] |
|
||||
|
||||
### Possible use case
|
||||
<!-- MANUAL: use_case -->
|
||||
**Table Organization**: Rename tables to follow naming conventions or reflect current usage.
|
||||
|
||||
**Description Management**: Update table descriptions for documentation purposes.
|
||||
|
||||
**Configuration Updates**: Modify table settings like date dependencies as requirements change.
|
||||
<!-- END MANUAL -->
|
||||
|
||||
---
|
||||
35
docs/platform/blocks/airtable/triggers.md
Normal file
35
docs/platform/blocks/airtable/triggers.md
Normal file
@@ -0,0 +1,35 @@
|
||||
# Airtable Webhook Trigger
|
||||
|
||||
### What it is
|
||||
Starts a flow whenever Airtable emits a webhook event
|
||||
|
||||
### How it works
|
||||
<!-- MANUAL: how_it_works -->
|
||||
This block subscribes to Airtable webhook events for a specific base and table. When records are created, updated, or deleted, Airtable sends a webhook notification that triggers your workflow.
|
||||
|
||||
You specify which events to listen for using the event selector. The webhook payload includes details about the changed records and the type of change that occurred.
|
||||
<!-- END MANUAL -->
|
||||
|
||||
### Inputs
|
||||
| Input | Description | Type | Required |
|
||||
|-------|-------------|------|----------|
|
||||
| base_id | Airtable base ID | str | Yes |
|
||||
| table_id_or_name | Airtable table ID or name | str | Yes |
|
||||
| events | Airtable webhook event filter | AirtableEventSelector | Yes |
|
||||
|
||||
### Outputs
|
||||
| Output | Description | Type |
|
||||
|--------|-------------|------|
|
||||
| error | Error message if the operation failed | str |
|
||||
| payload | Airtable webhook payload | WebhookPayload |
|
||||
|
||||
### Possible use case
|
||||
<!-- MANUAL: use_case -->
|
||||
**Real-Time Sync**: Automatically sync Airtable changes to other systems like CRMs or databases.
|
||||
|
||||
**Notification Workflows**: Send alerts when specific records are created or modified in Airtable.
|
||||
|
||||
**Automated Processing**: Trigger document generation or emails when new entries are added to a table.
|
||||
<!-- END MANUAL -->
|
||||
|
||||
---
|
||||
54
docs/platform/blocks/apollo/organization.md
Normal file
54
docs/platform/blocks/apollo/organization.md
Normal file
@@ -0,0 +1,54 @@
|
||||
# Search Organizations
|
||||
|
||||
### What it is
|
||||
Search for organizations in Apollo
|
||||
|
||||
### How it works
|
||||
<!-- MANUAL: how_it_works -->
|
||||
This block searches the Apollo database for organizations using various filters like employee count, location, and keywords. Apollo maintains a comprehensive database of company information for sales and marketing purposes.
|
||||
|
||||
Results can be filtered by headquarters location, excluded locations, industry keywords, and specific Apollo organization IDs.
|
||||
<!-- END MANUAL -->
|
||||
|
||||
### Inputs
|
||||
| Input | Description | Type | Required |
|
||||
|-------|-------------|------|----------|
|
||||
| organization_num_employees_range | The number range of employees working for the company. This enables you to find companies based on headcount. You can add multiple ranges to expand your search results.
|
||||
|
||||
Each range you add needs to be a string, with the upper and lower numbers of the range separated only by a comma. | List[int] | No |
|
||||
| organization_locations | The location of the company headquarters. You can search across cities, US states, and countries.
|
||||
|
||||
If a company has several office locations, results are still based on the headquarters location. For example, if you search chicago but a company's HQ location is in boston, any Boston-based companies will not appearch in your search results, even if they match other parameters.
|
||||
|
||||
To exclude companies based on location, use the organization_not_locations parameter.
|
||||
| List[str] | No |
|
||||
| organizations_not_locations | Exclude companies from search results based on the location of the company headquarters. You can use cities, US states, and countries as locations to exclude.
|
||||
|
||||
This parameter is useful for ensuring you do not prospect in an undesirable territory. For example, if you use ireland as a value, no Ireland-based companies will appear in your search results.
|
||||
| List[str] | No |
|
||||
| q_organization_keyword_tags | Filter search results based on keywords associated with companies. For example, you can enter mining as a value to return only companies that have an association with the mining industry. | List[str] | No |
|
||||
| q_organization_name | Filter search results to include a specific company name.
|
||||
|
||||
If the value you enter for this parameter does not match with a company's name, the company will not appear in search results, even if it matches other parameters. Partial matches are accepted. For example, if you filter by the value marketing, a company called NY Marketing Unlimited would still be eligible as a search result, but NY Market Analysis would not be eligible. | str | No |
|
||||
| organization_ids | The Apollo IDs for the companies you want to include in your search results. Each company in the Apollo database is assigned a unique ID.
|
||||
|
||||
To find IDs, identify the values for organization_id when you call this endpoint. | List[str] | No |
|
||||
| max_results | The maximum number of results to return. If you don't specify this parameter, the default is 100. | int | No |
|
||||
|
||||
### Outputs
|
||||
| Output | Description | Type |
|
||||
|--------|-------------|------|
|
||||
| error | Error message if the search failed | str |
|
||||
| organizations | List of organizations found | List[Dict[str, True]] |
|
||||
| organization | Each found organization, one at a time | Dict[str, True] |
|
||||
|
||||
### Possible use case
|
||||
<!-- MANUAL: use_case -->
|
||||
**Market Research**: Find companies matching specific criteria for market analysis.
|
||||
|
||||
**Lead List Building**: Build targeted lists of companies for outbound sales campaigns.
|
||||
|
||||
**Competitive Intelligence**: Research competitors and similar companies in your market.
|
||||
<!-- END MANUAL -->
|
||||
|
||||
---
|
||||
68
docs/platform/blocks/apollo/people.md
Normal file
68
docs/platform/blocks/apollo/people.md
Normal file
@@ -0,0 +1,68 @@
|
||||
# Search People
|
||||
|
||||
### What it is
|
||||
Search for people in Apollo
|
||||
|
||||
### How it works
|
||||
<!-- MANUAL: how_it_works -->
|
||||
This block searches Apollo's database for people based on job titles, seniority, location, company, and other criteria. It's designed for finding prospects and contacts for sales and marketing.
|
||||
|
||||
Enable enrich_info to get detailed contact information including verified email addresses (costs more credits).
|
||||
<!-- END MANUAL -->
|
||||
|
||||
### Inputs
|
||||
| Input | Description | Type | Required |
|
||||
|-------|-------------|------|----------|
|
||||
| person_titles | Job titles held by the people you want to find. For a person to be included in search results, they only need to match 1 of the job titles you add. Adding more job titles expands your search results.
|
||||
|
||||
Results also include job titles with the same terms, even if they are not exact matches. For example, searching for marketing manager might return people with the job title content marketing manager.
|
||||
|
||||
Use this parameter in combination with the person_seniorities[] parameter to find people based on specific job functions and seniority levels.
|
||||
| List[str] | No |
|
||||
| person_locations | The location where people live. You can search across cities, US states, and countries.
|
||||
|
||||
To find people based on the headquarters locations of their current employer, use the organization_locations parameter. | List[str] | No |
|
||||
| person_seniorities | The job seniority that people hold within their current employer. This enables you to find people that currently hold positions at certain reporting levels, such as Director level or senior IC level.
|
||||
|
||||
For a person to be included in search results, they only need to match 1 of the seniorities you add. Adding more seniorities expands your search results.
|
||||
|
||||
Searches only return results based on their current job title, so searching for Director-level employees only returns people that currently hold a Director-level title. If someone was previously a Director, but is currently a VP, they would not be included in your search results.
|
||||
|
||||
Use this parameter in combination with the person_titles[] parameter to find people based on specific job functions and seniority levels. | List["owner" | "founder" | "c_suite"] | No |
|
||||
| organization_locations | The location of the company headquarters for a person's current employer. You can search across cities, US states, and countries.
|
||||
|
||||
If a company has several office locations, results are still based on the headquarters location. For example, if you search chicago but a company's HQ location is in boston, people that work for the Boston-based company will not appear in your results, even if they match other parameters.
|
||||
|
||||
To find people based on their personal location, use the person_locations parameter. | List[str] | No |
|
||||
| q_organization_domains | The domain name for the person's employer. This can be the current employer or a previous employer. Do not include www., the @ symbol, or similar.
|
||||
|
||||
You can add multiple domains to search across companies.
|
||||
|
||||
Examples: apollo.io and microsoft.com | List[str] | No |
|
||||
| contact_email_statuses | The email statuses for the people you want to find. You can add multiple statuses to expand your search. | List["verified" | "unverified" | "likely_to_engage"] | No |
|
||||
| organization_ids | The Apollo IDs for the companies (employers) you want to include in your search results. Each company in the Apollo database is assigned a unique ID.
|
||||
|
||||
To find IDs, call the Organization Search endpoint and identify the values for organization_id. | List[str] | No |
|
||||
| organization_num_employees_range | The number range of employees working for the company. This enables you to find companies based on headcount. You can add multiple ranges to expand your search results.
|
||||
|
||||
Each range you add needs to be a string, with the upper and lower numbers of the range separated only by a comma. | List[int] | No |
|
||||
| q_keywords | A string of words over which we want to filter the results | str | No |
|
||||
| max_results | The maximum number of results to return. If you don't specify this parameter, the default is 25. Limited to 500 to prevent overspending. | int | No |
|
||||
| enrich_info | Whether to enrich contacts with detailed information including real email addresses. This will double the search cost. | bool | No |
|
||||
|
||||
### Outputs
|
||||
| Output | Description | Type |
|
||||
|--------|-------------|------|
|
||||
| error | Error message if the search failed | str |
|
||||
| people | List of people found | List[Dict[str, True]] |
|
||||
|
||||
### Possible use case
|
||||
<!-- MANUAL: use_case -->
|
||||
**Prospecting**: Find decision-makers at target companies for outbound sales.
|
||||
|
||||
**Recruiting**: Search for candidates with specific titles and experience.
|
||||
|
||||
**ABM Campaigns**: Build contact lists at specific accounts for account-based marketing.
|
||||
<!-- END MANUAL -->
|
||||
|
||||
---
|
||||
42
docs/platform/blocks/apollo/person.md
Normal file
42
docs/platform/blocks/apollo/person.md
Normal file
@@ -0,0 +1,42 @@
|
||||
# Get Person Detail
|
||||
|
||||
### What it is
|
||||
Get detailed person data with Apollo API, including email reveal
|
||||
|
||||
### How it works
|
||||
<!-- MANUAL: how_it_works -->
|
||||
This block enriches person data using Apollo's API. You can look up by Apollo person ID for best accuracy, or match by name plus company information, LinkedIn URL, or email address.
|
||||
|
||||
Returns comprehensive contact details including email addresses (if available), job title, company information, and social profiles.
|
||||
<!-- END MANUAL -->
|
||||
|
||||
### Inputs
|
||||
| Input | Description | Type | Required |
|
||||
|-------|-------------|------|----------|
|
||||
| person_id | Apollo person ID to enrich (most accurate method) | str | No |
|
||||
| first_name | First name of the person to enrich | str | No |
|
||||
| last_name | Last name of the person to enrich | str | No |
|
||||
| name | Full name of the person to enrich (alternative to first_name + last_name) | str | No |
|
||||
| email | Known email address of the person (helps with matching) | str | No |
|
||||
| domain | Company domain of the person (e.g., 'google.com') | str | No |
|
||||
| company | Company name of the person | str | No |
|
||||
| linkedin_url | LinkedIn URL of the person | str | No |
|
||||
| organization_id | Apollo organization ID of the person's company | str | No |
|
||||
| title | Job title of the person to enrich | str | No |
|
||||
|
||||
### Outputs
|
||||
| Output | Description | Type |
|
||||
|--------|-------------|------|
|
||||
| error | Error message if enrichment failed | str |
|
||||
| contact | Enriched contact information | Dict[str, True] |
|
||||
|
||||
### Possible use case
|
||||
<!-- MANUAL: use_case -->
|
||||
**Contact Enrichment**: Get full contact details from partial information like name and company.
|
||||
|
||||
**Email Discovery**: Find verified email addresses for outreach campaigns.
|
||||
|
||||
**Profile Completion**: Fill in missing contact details in your CRM or database.
|
||||
<!-- END MANUAL -->
|
||||
|
||||
---
|
||||
45
docs/platform/blocks/ayrshare/post_to_bluesky.md
Normal file
45
docs/platform/blocks/ayrshare/post_to_bluesky.md
Normal file
@@ -0,0 +1,45 @@
|
||||
# Post To Bluesky
|
||||
|
||||
### What it is
|
||||
Post to Bluesky using Ayrshare
|
||||
|
||||
### How it works
|
||||
<!-- MANUAL: how_it_works -->
|
||||
This block uses Ayrshare's social media API to publish content to Bluesky. It handles text posts (up to 300 characters), images (up to 4), and video content with support for scheduling, accessibility features like alt text, and link shortening.
|
||||
|
||||
The block authenticates through your Ayrshare credentials and sends the post data to Ayrshare's unified API, which then publishes to Bluesky. It returns post identifiers and status information upon completion, or error details if the operation fails.
|
||||
<!-- END MANUAL -->
|
||||
|
||||
### Inputs
|
||||
| Input | Description | Type | Required |
|
||||
|-------|-------------|------|----------|
|
||||
| post | The post text to be published (max 300 characters for Bluesky) | str | No |
|
||||
| media_urls | Optional list of media URLs to include. Bluesky supports up to 4 images or 1 video. | List[str] | No |
|
||||
| is_video | Whether the media is a video | bool | No |
|
||||
| schedule_date | UTC datetime for scheduling (YYYY-MM-DDThh:mm:ssZ) | str (date-time) | No |
|
||||
| disable_comments | Whether to disable comments | bool | No |
|
||||
| shorten_links | Whether to shorten links | bool | No |
|
||||
| unsplash | Unsplash image configuration | str | No |
|
||||
| requires_approval | Whether to enable approval workflow | bool | No |
|
||||
| random_post | Whether to generate random post text | bool | No |
|
||||
| random_media_url | Whether to generate random media | bool | No |
|
||||
| notes | Additional notes for the post | str | No |
|
||||
| alt_text | Alt text for each media item (accessibility) | List[str] | No |
|
||||
|
||||
### Outputs
|
||||
| Output | Description | Type |
|
||||
|--------|-------------|------|
|
||||
| error | Error message if the operation failed | str |
|
||||
| post_result | The result of the post | PostResponse |
|
||||
| post | The result of the post | PostIds |
|
||||
|
||||
### Possible use case
|
||||
<!-- MANUAL: use_case -->
|
||||
**Cross-Platform Publishing**: Automatically share content across Bluesky and other social networks from a single workflow.
|
||||
|
||||
**Scheduled Content Calendar**: Queue up posts with specific publishing times to maintain consistent presence on Bluesky.
|
||||
|
||||
**Visual Content Sharing**: Share image galleries with accessibility-friendly alt text for photo-focused content strategies.
|
||||
<!-- END MANUAL -->
|
||||
|
||||
---
|
||||
61
docs/platform/blocks/ayrshare/post_to_facebook.md
Normal file
61
docs/platform/blocks/ayrshare/post_to_facebook.md
Normal file
@@ -0,0 +1,61 @@
|
||||
# Post To Facebook
|
||||
|
||||
### What it is
|
||||
Post to Facebook using Ayrshare
|
||||
|
||||
### How it works
|
||||
<!-- MANUAL: how_it_works -->
|
||||
This block uses Ayrshare's social media API to publish content to Facebook Pages. It supports text posts, images, videos, carousels (2-10 items), Reels, and Stories, with features like audience targeting by age and country, location tagging, and scheduling.
|
||||
|
||||
The block authenticates through Ayrshare and leverages the Meta Graph API to handle various Facebook-specific formats. Advanced options include draft mode for Meta Business Suite, custom link previews, and video thumbnails. Results include post IDs for tracking engagement.
|
||||
<!-- END MANUAL -->
|
||||
|
||||
### Inputs
|
||||
| Input | Description | Type | Required |
|
||||
|-------|-------------|------|----------|
|
||||
| post | The post text to be published | str | No |
|
||||
| media_urls | Optional list of media URLs to include. Set is_video in advanced settings to true if you want to upload videos. | List[str] | No |
|
||||
| is_video | Whether the media is a video | bool | No |
|
||||
| schedule_date | UTC datetime for scheduling (YYYY-MM-DDThh:mm:ssZ) | str (date-time) | No |
|
||||
| disable_comments | Whether to disable comments | bool | No |
|
||||
| shorten_links | Whether to shorten links | bool | No |
|
||||
| unsplash | Unsplash image configuration | str | No |
|
||||
| requires_approval | Whether to enable approval workflow | bool | No |
|
||||
| random_post | Whether to generate random post text | bool | No |
|
||||
| random_media_url | Whether to generate random media | bool | No |
|
||||
| notes | Additional notes for the post | str | No |
|
||||
| is_carousel | Whether to post a carousel | bool | No |
|
||||
| carousel_link | The URL for the 'See More At' button in the carousel | str | No |
|
||||
| carousel_items | List of carousel items with name, link and picture URLs. Min 2, max 10 items. | List[CarouselItem] | No |
|
||||
| is_reels | Whether to post to Facebook Reels | bool | No |
|
||||
| reels_title | Title for the Reels video (max 255 chars) | str | No |
|
||||
| reels_thumbnail | Thumbnail URL for Reels video (JPEG/PNG, <10MB) | str | No |
|
||||
| is_story | Whether to post as a Facebook Story | bool | No |
|
||||
| media_captions | Captions for each media item | List[str] | No |
|
||||
| location_id | Facebook Page ID or name for location tagging | str | No |
|
||||
| age_min | Minimum age for audience targeting (13,15,18,21,25) | int | No |
|
||||
| target_countries | List of country codes to target (max 25) | List[str] | No |
|
||||
| alt_text | Alt text for each media item | List[str] | No |
|
||||
| video_title | Title for video post | str | No |
|
||||
| video_thumbnail | Thumbnail URL for video post | str | No |
|
||||
| is_draft | Save as draft in Meta Business Suite | bool | No |
|
||||
| scheduled_publish_date | Schedule publish time in Meta Business Suite (UTC) | str | No |
|
||||
| preview_link | URL for custom link preview | str | No |
|
||||
|
||||
### Outputs
|
||||
| Output | Description | Type |
|
||||
|--------|-------------|------|
|
||||
| error | Error message if the operation failed | str |
|
||||
| post_result | The result of the post | PostResponse |
|
||||
| post | The result of the post | PostIds |
|
||||
|
||||
### Possible use case
|
||||
<!-- MANUAL: use_case -->
|
||||
**Product Launches**: Create carousel posts showcasing multiple product images with links to purchase pages.
|
||||
|
||||
**Event Promotion**: Share event details with age-targeted reach and location tagging for local business events.
|
||||
|
||||
**Short-Form Video**: Automatically publish Reels with custom thumbnails to maximize video content reach.
|
||||
<!-- END MANUAL -->
|
||||
|
||||
---
|
||||
57
docs/platform/blocks/ayrshare/post_to_gmb.md
Normal file
57
docs/platform/blocks/ayrshare/post_to_gmb.md
Normal file
@@ -0,0 +1,57 @@
|
||||
# Post To GMB
|
||||
|
||||
### What it is
|
||||
Post to Google My Business using Ayrshare
|
||||
|
||||
### How it works
|
||||
<!-- MANUAL: how_it_works -->
|
||||
This block uses Ayrshare's API to publish content to Google My Business profiles. It supports standard posts, photo/video posts (categorized by type like exterior, interior, product), and special post types including events and promotional offers with coupon codes.
|
||||
|
||||
The block integrates with Google's Business Profile API through Ayrshare, enabling call-to-action buttons (book, order, shop, learn more, sign up, call), event scheduling with start/end dates, and promotional offers with terms and redemption URLs.
|
||||
<!-- END MANUAL -->
|
||||
|
||||
### Inputs
|
||||
| Input | Description | Type | Required |
|
||||
|-------|-------------|------|----------|
|
||||
| post | The post text to be published | str | No |
|
||||
| media_urls | Optional list of media URLs. GMB supports only one image or video per post. | List[str] | No |
|
||||
| is_video | Whether the media is a video | bool | No |
|
||||
| schedule_date | UTC datetime for scheduling (YYYY-MM-DDThh:mm:ssZ) | str (date-time) | No |
|
||||
| disable_comments | Whether to disable comments | bool | No |
|
||||
| shorten_links | Whether to shorten links | bool | No |
|
||||
| unsplash | Unsplash image configuration | str | No |
|
||||
| requires_approval | Whether to enable approval workflow | bool | No |
|
||||
| random_post | Whether to generate random post text | bool | No |
|
||||
| random_media_url | Whether to generate random media | bool | No |
|
||||
| notes | Additional notes for the post | str | No |
|
||||
| is_photo_video | Whether this is a photo/video post (appears in Photos section) | bool | No |
|
||||
| photo_category | Category for photo/video: cover, profile, logo, exterior, interior, product, at_work, food_and_drink, menu, common_area, rooms, teams | str | No |
|
||||
| call_to_action_type | Type of action button: 'book', 'order', 'shop', 'learn_more', 'sign_up', or 'call' | str | No |
|
||||
| call_to_action_url | URL for the action button (not required for 'call' action) | str | No |
|
||||
| event_title | Event title for event posts | str | No |
|
||||
| event_start_date | Event start date in ISO format (e.g., '2024-03-15T09:00:00Z') | str | No |
|
||||
| event_end_date | Event end date in ISO format (e.g., '2024-03-15T17:00:00Z') | str | No |
|
||||
| offer_title | Offer title for promotional posts | str | No |
|
||||
| offer_start_date | Offer start date in ISO format (e.g., '2024-03-15T00:00:00Z') | str | No |
|
||||
| offer_end_date | Offer end date in ISO format (e.g., '2024-04-15T23:59:59Z') | str | No |
|
||||
| offer_coupon_code | Coupon code for the offer (max 58 characters) | str | No |
|
||||
| offer_redeem_online_url | URL where customers can redeem the offer online | str | No |
|
||||
| offer_terms_conditions | Terms and conditions for the offer | str | No |
|
||||
|
||||
### Outputs
|
||||
| Output | Description | Type |
|
||||
|--------|-------------|------|
|
||||
| error | Error message if the operation failed | str |
|
||||
| post_result | The result of the post | PostResponse |
|
||||
| post | The result of the post | PostIds |
|
||||
|
||||
### Possible use case
|
||||
<!-- MANUAL: use_case -->
|
||||
**Local Business Updates**: Post daily specials, new arrivals, or service announcements directly to your Google Business Profile.
|
||||
|
||||
**Promotional Campaigns**: Create time-limited offers with coupon codes and online redemption links to drive sales.
|
||||
|
||||
**Event Marketing**: Announce upcoming events with dates, descriptions, and call-to-action buttons for reservations.
|
||||
<!-- END MANUAL -->
|
||||
|
||||
---
|
||||
54
docs/platform/blocks/ayrshare/post_to_instagram.md
Normal file
54
docs/platform/blocks/ayrshare/post_to_instagram.md
Normal file
@@ -0,0 +1,54 @@
|
||||
# Post To Instagram
|
||||
|
||||
### What it is
|
||||
Post to Instagram using Ayrshare. Requires a Business or Creator Instagram Account connected with a Facebook Page
|
||||
|
||||
### How it works
|
||||
<!-- MANUAL: how_it_works -->
|
||||
This block uses Ayrshare's API to publish content to Instagram Business or Creator accounts. It supports feed posts, Stories (24-hour expiration), Reels, and carousels (up to 10 images/videos), with features like collaborator invitations, location tagging, and user tags with coordinates.
|
||||
|
||||
The block requires an Instagram account connected to a Facebook Page and authenticates through Meta's Graph API via Ayrshare. Instagram-specific features include auto-resize for optimal dimensions, audio naming for Reels, and thumbnail customization with frame offset control.
|
||||
<!-- END MANUAL -->
|
||||
|
||||
### Inputs
|
||||
| Input | Description | Type | Required |
|
||||
|-------|-------------|------|----------|
|
||||
| post | The post text (max 2,200 chars, up to 30 hashtags, 3 @mentions) | str | No |
|
||||
| media_urls | Optional list of media URLs. Instagram supports up to 10 images/videos in a carousel. | List[str] | No |
|
||||
| is_video | Whether the media is a video | bool | No |
|
||||
| schedule_date | UTC datetime for scheduling (YYYY-MM-DDThh:mm:ssZ) | str (date-time) | No |
|
||||
| disable_comments | Whether to disable comments | bool | No |
|
||||
| shorten_links | Whether to shorten links | bool | No |
|
||||
| unsplash | Unsplash image configuration | str | No |
|
||||
| requires_approval | Whether to enable approval workflow | bool | No |
|
||||
| random_post | Whether to generate random post text | bool | No |
|
||||
| random_media_url | Whether to generate random media | bool | No |
|
||||
| notes | Additional notes for the post | str | No |
|
||||
| is_story | Whether to post as Instagram Story (24-hour expiration) | bool | No |
|
||||
| share_reels_feed | Whether Reel should appear in both Feed and Reels tabs | bool | No |
|
||||
| audio_name | Audio name for Reels (e.g., 'The Weeknd - Blinding Lights') | str | No |
|
||||
| thumbnail | Thumbnail URL for Reel video | str | No |
|
||||
| thumbnail_offset | Thumbnail frame offset in milliseconds (default: 0) | int | No |
|
||||
| alt_text | Alt text for each media item (up to 1,000 chars each, accessibility feature), each item in the list corresponds to a media item in the media_urls list | List[str] | No |
|
||||
| location_id | Facebook Page ID or name for location tagging (e.g., '7640348500' or '@guggenheimmuseum') | str | No |
|
||||
| user_tags | List of users to tag with coordinates for images | List[Dict[str, True]] | No |
|
||||
| collaborators | Instagram usernames to invite as collaborators (max 3, public accounts only) | List[str] | No |
|
||||
| auto_resize | Auto-resize images to 1080x1080px for Instagram | bool | No |
|
||||
|
||||
### Outputs
|
||||
| Output | Description | Type |
|
||||
|--------|-------------|------|
|
||||
| error | Error message if the operation failed | str |
|
||||
| post_result | The result of the post | PostResponse |
|
||||
| post | The result of the post | PostIds |
|
||||
|
||||
### Possible use case
|
||||
<!-- MANUAL: use_case -->
|
||||
**Influencer Collaborations**: Create posts with collaborator tags to feature brand partnerships across multiple accounts.
|
||||
|
||||
**E-commerce Product Showcases**: Share carousel posts of product images with location tags for local discovery.
|
||||
|
||||
**Reels Automation**: Automatically publish short-form video content with custom thumbnails and trending audio.
|
||||
<!-- END MANUAL -->
|
||||
|
||||
---
|
||||
56
docs/platform/blocks/ayrshare/post_to_linkedin.md
Normal file
56
docs/platform/blocks/ayrshare/post_to_linkedin.md
Normal file
@@ -0,0 +1,56 @@
|
||||
# Post To Linked In
|
||||
|
||||
### What it is
|
||||
Post to LinkedIn using Ayrshare
|
||||
|
||||
### How it works
|
||||
<!-- MANUAL: how_it_works -->
|
||||
This block uses Ayrshare's social media API to post content to LinkedIn. It handles text posts, images, videos, and documents, with support for scheduling and audience targeting. The block authenticates through Ayrshare's API.
|
||||
|
||||
LinkedIn-specific features include visibility controls, comment management, and targeting by country, seniority, industry, and other demographics (requires 300+ followers in target audience).
|
||||
<!-- END MANUAL -->
|
||||
|
||||
### Inputs
|
||||
| Input | Description | Type | Required |
|
||||
|-------|-------------|------|----------|
|
||||
| post | The post text (max 3,000 chars, hashtags supported with #) | str | No |
|
||||
| media_urls | Optional list of media URLs. LinkedIn supports up to 9 images, videos, or documents (PPT, PPTX, DOC, DOCX, PDF <100MB, <300 pages). | List[str] | No |
|
||||
| is_video | Whether the media is a video | bool | No |
|
||||
| schedule_date | UTC datetime for scheduling (YYYY-MM-DDThh:mm:ssZ) | str (date-time) | No |
|
||||
| disable_comments | Whether to disable comments | bool | No |
|
||||
| shorten_links | Whether to shorten links | bool | No |
|
||||
| unsplash | Unsplash image configuration | str | No |
|
||||
| requires_approval | Whether to enable approval workflow | bool | No |
|
||||
| random_post | Whether to generate random post text | bool | No |
|
||||
| random_media_url | Whether to generate random media | bool | No |
|
||||
| notes | Additional notes for the post | str | No |
|
||||
| visibility | Post visibility: 'public' (default), 'connections' (personal only), 'loggedin' | str | No |
|
||||
| alt_text | Alt text for each image (accessibility feature, not supported for videos/documents) | List[str] | No |
|
||||
| titles | Title/caption for each image or video | List[str] | No |
|
||||
| document_title | Title for document posts (max 400 chars, uses filename if not specified) | str | No |
|
||||
| thumbnail | Thumbnail URL for video (PNG/JPG, same dimensions as video, <10MB) | str | No |
|
||||
| targeting_countries | Country codes for targeting (e.g., ['US', 'IN', 'DE', 'GB']). Requires 300+ followers in target audience. | List[str] | No |
|
||||
| targeting_seniorities | Seniority levels for targeting (e.g., ['Senior', 'VP']). Requires 300+ followers in target audience. | List[str] | No |
|
||||
| targeting_degrees | Education degrees for targeting. Requires 300+ followers in target audience. | List[str] | No |
|
||||
| targeting_fields_of_study | Fields of study for targeting. Requires 300+ followers in target audience. | List[str] | No |
|
||||
| targeting_industries | Industry categories for targeting. Requires 300+ followers in target audience. | List[str] | No |
|
||||
| targeting_job_functions | Job function categories for targeting. Requires 300+ followers in target audience. | List[str] | No |
|
||||
| targeting_staff_count_ranges | Company size ranges for targeting. Requires 300+ followers in target audience. | List[str] | No |
|
||||
|
||||
### Outputs
|
||||
| Output | Description | Type |
|
||||
|--------|-------------|------|
|
||||
| error | Error message if the operation failed | str |
|
||||
| post_result | The result of the post | PostResponse |
|
||||
| post | The result of the post | PostIds |
|
||||
|
||||
### Possible use case
|
||||
<!-- MANUAL: use_case -->
|
||||
**Thought Leadership**: Automatically share blog posts or industry insights with professional network.
|
||||
|
||||
**Scheduled Content**: Queue up a week's worth of LinkedIn posts with scheduled publishing times.
|
||||
|
||||
**Targeted Announcements**: Share company updates targeted to specific industries or seniority levels.
|
||||
<!-- END MANUAL -->
|
||||
|
||||
---
|
||||
51
docs/platform/blocks/ayrshare/post_to_pinterest.md
Normal file
51
docs/platform/blocks/ayrshare/post_to_pinterest.md
Normal file
@@ -0,0 +1,51 @@
|
||||
# Post To Pinterest
|
||||
|
||||
### What it is
|
||||
Post to Pinterest using Ayrshare
|
||||
|
||||
### How it works
|
||||
<!-- MANUAL: how_it_works -->
|
||||
This block uses Ayrshare's API to publish pins to Pinterest boards. It supports image pins, video pins (with required thumbnails), and carousel pins (up to 5 images), with customizable titles, descriptions, destination links, and private notes.
|
||||
|
||||
The block connects to Pinterest's API through Ayrshare, allowing you to specify target boards, add alt text for accessibility, and configure per-image carousel options including individual titles, links, and descriptions. Pins can be scheduled for future publishing.
|
||||
<!-- END MANUAL -->
|
||||
|
||||
### Inputs
|
||||
| Input | Description | Type | Required |
|
||||
|-------|-------------|------|----------|
|
||||
| post | Pin description (max 500 chars, links not clickable - use link field instead) | str | No |
|
||||
| media_urls | Required image/video URLs. Pinterest requires at least one image. Videos need thumbnail. Up to 5 images for carousel. | List[str] | No |
|
||||
| is_video | Whether the media is a video | bool | No |
|
||||
| schedule_date | UTC datetime for scheduling (YYYY-MM-DDThh:mm:ssZ) | str (date-time) | No |
|
||||
| disable_comments | Whether to disable comments | bool | No |
|
||||
| shorten_links | Whether to shorten links | bool | No |
|
||||
| unsplash | Unsplash image configuration | str | No |
|
||||
| requires_approval | Whether to enable approval workflow | bool | No |
|
||||
| random_post | Whether to generate random post text | bool | No |
|
||||
| random_media_url | Whether to generate random media | bool | No |
|
||||
| notes | Additional notes for the post | str | No |
|
||||
| pin_title | Pin title displayed in 'Add your title' section (max 100 chars) | str | No |
|
||||
| link | Clickable destination URL when users click the pin (max 2048 chars) | str | No |
|
||||
| board_id | Pinterest Board ID to post to (from /user/details endpoint, uses default board if not specified) | str | No |
|
||||
| note | Private note for the pin (only visible to you and board collaborators) | str | No |
|
||||
| thumbnail | Required thumbnail URL for video pins (must have valid image Content-Type) | str | No |
|
||||
| carousel_options | Options for each image in carousel (title, link, description per image) | List[PinterestCarouselOption] | No |
|
||||
| alt_text | Alt text for each image/video (max 500 chars each, accessibility feature) | List[str] | No |
|
||||
|
||||
### Outputs
|
||||
| Output | Description | Type |
|
||||
|--------|-------------|------|
|
||||
| error | Error message if the operation failed | str |
|
||||
| post_result | The result of the post | PostResponse |
|
||||
| post | The result of the post | PostIds |
|
||||
|
||||
### Possible use case
|
||||
<!-- MANUAL: use_case -->
|
||||
**Product Catalog Distribution**: Automatically pin product images with direct links to purchase pages organized by board category.
|
||||
|
||||
**Content Repurposing**: Convert blog posts and articles into visual pins with clickable destination URLs.
|
||||
|
||||
**Visual Inspiration Boards**: Create carousel pins showcasing design ideas, recipes, or tutorials with step-by-step images.
|
||||
<!-- END MANUAL -->
|
||||
|
||||
---
|
||||
44
docs/platform/blocks/ayrshare/post_to_reddit.md
Normal file
44
docs/platform/blocks/ayrshare/post_to_reddit.md
Normal file
@@ -0,0 +1,44 @@
|
||||
# Post To Reddit
|
||||
|
||||
### What it is
|
||||
Post to Reddit using Ayrshare
|
||||
|
||||
### How it works
|
||||
<!-- MANUAL: how_it_works -->
|
||||
This block uses Ayrshare's API to publish content to Reddit. It supports text posts, image posts, and video submissions with optional scheduling and link shortening features.
|
||||
|
||||
The block authenticates through Ayrshare and submits content to your connected Reddit account. Common options include approval workflows for content review before publishing, random content generation, and Unsplash integration for sourcing images.
|
||||
<!-- END MANUAL -->
|
||||
|
||||
### Inputs
|
||||
| Input | Description | Type | Required |
|
||||
|-------|-------------|------|----------|
|
||||
| post | The post text to be published | str | No |
|
||||
| media_urls | Optional list of media URLs to include. Set is_video in advanced settings to true if you want to upload videos. | List[str] | No |
|
||||
| is_video | Whether the media is a video | bool | No |
|
||||
| schedule_date | UTC datetime for scheduling (YYYY-MM-DDThh:mm:ssZ) | str (date-time) | No |
|
||||
| disable_comments | Whether to disable comments | bool | No |
|
||||
| shorten_links | Whether to shorten links | bool | No |
|
||||
| unsplash | Unsplash image configuration | str | No |
|
||||
| requires_approval | Whether to enable approval workflow | bool | No |
|
||||
| random_post | Whether to generate random post text | bool | No |
|
||||
| random_media_url | Whether to generate random media | bool | No |
|
||||
| notes | Additional notes for the post | str | No |
|
||||
|
||||
### Outputs
|
||||
| Output | Description | Type |
|
||||
|--------|-------------|------|
|
||||
| error | Error message if the operation failed | str |
|
||||
| post_result | The result of the post | PostResponse |
|
||||
| post | The result of the post | PostIds |
|
||||
|
||||
### Possible use case
|
||||
<!-- MANUAL: use_case -->
|
||||
**Community Engagement**: Share relevant content to niche subreddits as part of community marketing strategies.
|
||||
|
||||
**Content Distribution**: Cross-post blog articles or announcements to relevant Reddit communities for broader reach.
|
||||
|
||||
**Brand Monitoring Response**: Automatically share updates or responses in communities where your brand is discussed.
|
||||
<!-- END MANUAL -->
|
||||
|
||||
---
|
||||
46
docs/platform/blocks/ayrshare/post_to_snapchat.md
Normal file
46
docs/platform/blocks/ayrshare/post_to_snapchat.md
Normal file
@@ -0,0 +1,46 @@
|
||||
# Post To Snapchat
|
||||
|
||||
### What it is
|
||||
Post to Snapchat using Ayrshare
|
||||
|
||||
### How it works
|
||||
<!-- MANUAL: how_it_works -->
|
||||
This block uses Ayrshare's API to publish video content to Snapchat. Snapchat only supports video content, with three destination options: Stories (24-hour ephemeral content), Saved Stories (persistent Stories), and Spotlight (public discovery feed).
|
||||
|
||||
The block authenticates through Ayrshare and uploads video content with optional custom thumbnails. Videos can be scheduled for future publishing and support approval workflows for content review before going live.
|
||||
<!-- END MANUAL -->
|
||||
|
||||
### Inputs
|
||||
| Input | Description | Type | Required |
|
||||
|-------|-------------|------|----------|
|
||||
| post | The post text (optional for video-only content) | str | No |
|
||||
| media_urls | Required video URL for Snapchat posts. Snapchat only supports video content. | List[str] | No |
|
||||
| is_video | Whether the media is a video | bool | No |
|
||||
| schedule_date | UTC datetime for scheduling (YYYY-MM-DDThh:mm:ssZ) | str (date-time) | No |
|
||||
| disable_comments | Whether to disable comments | bool | No |
|
||||
| shorten_links | Whether to shorten links | bool | No |
|
||||
| unsplash | Unsplash image configuration | str | No |
|
||||
| requires_approval | Whether to enable approval workflow | bool | No |
|
||||
| random_post | Whether to generate random post text | bool | No |
|
||||
| random_media_url | Whether to generate random media | bool | No |
|
||||
| notes | Additional notes for the post | str | No |
|
||||
| story_type | Type of Snapchat content: 'story' (24-hour Stories), 'saved_story' (Saved Stories), or 'spotlight' (Spotlight posts) | str | No |
|
||||
| video_thumbnail | Thumbnail URL for video content (optional, auto-generated if not provided) | str | No |
|
||||
|
||||
### Outputs
|
||||
| Output | Description | Type |
|
||||
|--------|-------------|------|
|
||||
| error | Error message if the operation failed | str |
|
||||
| post_result | The result of the post | PostResponse |
|
||||
| post | The result of the post | PostIds |
|
||||
|
||||
### Possible use case
|
||||
<!-- MANUAL: use_case -->
|
||||
**Ephemeral Marketing**: Share time-sensitive promotions or behind-the-scenes content that creates urgency through 24-hour Stories.
|
||||
|
||||
**Public Discovery**: Post engaging video content to Spotlight to reach new audiences beyond your followers.
|
||||
|
||||
**Scheduled Story Series**: Plan and schedule a sequence of video Stories for product launches or events.
|
||||
<!-- END MANUAL -->
|
||||
|
||||
---
|
||||
44
docs/platform/blocks/ayrshare/post_to_telegram.md
Normal file
44
docs/platform/blocks/ayrshare/post_to_telegram.md
Normal file
@@ -0,0 +1,44 @@
|
||||
# Post To Telegram
|
||||
|
||||
### What it is
|
||||
Post to Telegram using Ayrshare
|
||||
|
||||
### How it works
|
||||
<!-- MANUAL: how_it_works -->
|
||||
This block uses Ayrshare's API to publish messages to Telegram channels. It supports text messages, images, videos, and animated GIFs, with automatic link preview generation unless media is included.
|
||||
|
||||
The block authenticates through Ayrshare and sends content to your connected Telegram channel or bot. User mentions are supported via @handle syntax, and content can be scheduled for future delivery.
|
||||
<!-- END MANUAL -->
|
||||
|
||||
### Inputs
|
||||
| Input | Description | Type | Required |
|
||||
|-------|-------------|------|----------|
|
||||
| post | The post text (empty string allowed). Use @handle to mention other Telegram users. | str | No |
|
||||
| media_urls | Optional list of media URLs. For animated GIFs, only one URL is allowed. Telegram will auto-preview links unless image/video is included. | List[str] | No |
|
||||
| is_video | Whether the media is a video. Set to true for animated GIFs that don't end in .gif/.GIF extension. | bool | No |
|
||||
| schedule_date | UTC datetime for scheduling (YYYY-MM-DDThh:mm:ssZ) | str (date-time) | No |
|
||||
| disable_comments | Whether to disable comments | bool | No |
|
||||
| shorten_links | Whether to shorten links | bool | No |
|
||||
| unsplash | Unsplash image configuration | str | No |
|
||||
| requires_approval | Whether to enable approval workflow | bool | No |
|
||||
| random_post | Whether to generate random post text | bool | No |
|
||||
| random_media_url | Whether to generate random media | bool | No |
|
||||
| notes | Additional notes for the post | str | No |
|
||||
|
||||
### Outputs
|
||||
| Output | Description | Type |
|
||||
|--------|-------------|------|
|
||||
| error | Error message if the operation failed | str |
|
||||
| post_result | The result of the post | PostResponse |
|
||||
| post | The result of the post | PostIds |
|
||||
|
||||
### Possible use case
|
||||
<!-- MANUAL: use_case -->
|
||||
**Channel Broadcasting**: Automatically distribute announcements, updates, or news to Telegram channel subscribers.
|
||||
|
||||
**Alert Systems**: Send automated notifications with media attachments to monitoring or alert channels.
|
||||
|
||||
**Content Syndication**: Cross-post content from other platforms to Telegram communities for broader reach.
|
||||
<!-- END MANUAL -->
|
||||
|
||||
---
|
||||
44
docs/platform/blocks/ayrshare/post_to_threads.md
Normal file
44
docs/platform/blocks/ayrshare/post_to_threads.md
Normal file
@@ -0,0 +1,44 @@
|
||||
# Post To Threads
|
||||
|
||||
### What it is
|
||||
Post to Threads using Ayrshare
|
||||
|
||||
### How it works
|
||||
<!-- MANUAL: how_it_works -->
|
||||
This block uses Ayrshare's API to publish content to Threads (Meta's text-based social platform). It supports text posts (up to 500 characters with one hashtag), images, videos, and carousels (up to 20 items), with automatic link previews when no media is attached.
|
||||
|
||||
The block authenticates through Meta's API via Ayrshare. Content can mention users via @handle syntax, be scheduled for future publishing, and include approval workflows for content review.
|
||||
<!-- END MANUAL -->
|
||||
|
||||
### Inputs
|
||||
| Input | Description | Type | Required |
|
||||
|-------|-------------|------|----------|
|
||||
| post | The post text (max 500 chars, empty string allowed). Only 1 hashtag allowed. Use @handle to mention users. | str | No |
|
||||
| media_urls | Optional list of media URLs. Supports up to 20 images/videos in a carousel. Auto-preview links unless media is included. | List[str] | No |
|
||||
| is_video | Whether the media is a video | bool | No |
|
||||
| schedule_date | UTC datetime for scheduling (YYYY-MM-DDThh:mm:ssZ) | str (date-time) | No |
|
||||
| disable_comments | Whether to disable comments | bool | No |
|
||||
| shorten_links | Whether to shorten links | bool | No |
|
||||
| unsplash | Unsplash image configuration | str | No |
|
||||
| requires_approval | Whether to enable approval workflow | bool | No |
|
||||
| random_post | Whether to generate random post text | bool | No |
|
||||
| random_media_url | Whether to generate random media | bool | No |
|
||||
| notes | Additional notes for the post | str | No |
|
||||
|
||||
### Outputs
|
||||
| Output | Description | Type |
|
||||
|--------|-------------|------|
|
||||
| error | Error message if the operation failed | str |
|
||||
| post_result | The result of the post | PostResponse |
|
||||
| post | The result of the post | PostIds |
|
||||
|
||||
### Possible use case
|
||||
<!-- MANUAL: use_case -->
|
||||
**Thought Leadership**: Share quick insights, opinions, or industry commentary in a conversational format.
|
||||
|
||||
**Cross-Platform Text Content**: Automatically syndicate text-based content from other platforms to Threads.
|
||||
|
||||
**Community Engagement**: Post discussion prompts or responses to engage with your Threads audience.
|
||||
<!-- END MANUAL -->
|
||||
|
||||
---
|
||||
55
docs/platform/blocks/ayrshare/post_to_tiktok.md
Normal file
55
docs/platform/blocks/ayrshare/post_to_tiktok.md
Normal file
@@ -0,0 +1,55 @@
|
||||
# Post To Tik Tok
|
||||
|
||||
### What it is
|
||||
Post to TikTok using Ayrshare
|
||||
|
||||
### How it works
|
||||
<!-- MANUAL: how_it_works -->
|
||||
This block uses Ayrshare's API to publish content to TikTok. It supports video posts and image slideshows (up to 35 images), with extensive options for content labeling including AI-generated disclosure, branded content, and brand organic content tags.
|
||||
|
||||
The block connects to TikTok's API through Ayrshare with controls for visibility, duet/stitch permissions, comment settings, auto-music, and thumbnail selection. Videos can be posted as drafts for final review, and scheduled for future publishing.
|
||||
<!-- END MANUAL -->
|
||||
|
||||
### Inputs
|
||||
| Input | Description | Type | Required |
|
||||
|-------|-------------|------|----------|
|
||||
| post | The post text (max 2,200 chars, empty string allowed). Use @handle to mention users. Line breaks will be ignored. | str | Yes |
|
||||
| media_urls | Required media URLs. Either 1 video OR up to 35 images (JPG/JPEG/WEBP only). Cannot mix video and images. | List[str] | No |
|
||||
| is_video | Whether the media is a video | bool | No |
|
||||
| schedule_date | UTC datetime for scheduling (YYYY-MM-DDThh:mm:ssZ) | str (date-time) | No |
|
||||
| disable_comments | Disable comments on the published post | bool | No |
|
||||
| shorten_links | Whether to shorten links | bool | No |
|
||||
| unsplash | Unsplash image configuration | str | No |
|
||||
| requires_approval | Whether to enable approval workflow | bool | No |
|
||||
| random_post | Whether to generate random post text | bool | No |
|
||||
| random_media_url | Whether to generate random media | bool | No |
|
||||
| notes | Additional notes for the post | str | No |
|
||||
| auto_add_music | Whether to automatically add recommended music to the post. If you set this field to true, you can change the music later in the TikTok app. | bool | No |
|
||||
| disable_duet | Disable duets on published video (video only) | bool | No |
|
||||
| disable_stitch | Disable stitch on published video (video only) | bool | No |
|
||||
| is_ai_generated | If you enable the toggle, your video will be labeled as “Creator labeled as AI-generated” once posted and can’t be changed. The “Creator labeled as AI-generated” label indicates that the content was completely AI-generated or significantly edited with AI. | bool | No |
|
||||
| is_branded_content | Whether to enable the Branded Content toggle. If this field is set to true, the video will be labeled as Branded Content, indicating you are in a paid partnership with a brand. A “Paid partnership” label will be attached to the video. | bool | No |
|
||||
| is_brand_organic | Whether to enable the Brand Organic Content toggle. If this field is set to true, the video will be labeled as Brand Organic Content, indicating you are promoting yourself or your own business. A “Promotional content” label will be attached to the video. | bool | No |
|
||||
| image_cover_index | Index of image to use as cover (0-based, image posts only) | int | No |
|
||||
| title | Title for image posts | str | No |
|
||||
| thumbnail_offset | Video thumbnail frame offset in milliseconds (video only) | int | No |
|
||||
| visibility | Post visibility: 'public', 'private', 'followers', or 'friends' | "public" | "private" | "followers" | No |
|
||||
| draft | Create as draft post (video only) | bool | No |
|
||||
|
||||
### Outputs
|
||||
| Output | Description | Type |
|
||||
|--------|-------------|------|
|
||||
| error | Error message if the operation failed | str |
|
||||
| post_result | The result of the post | PostResponse |
|
||||
| post | The result of the post | PostIds |
|
||||
|
||||
### Possible use case
|
||||
<!-- MANUAL: use_case -->
|
||||
**Creator Content Pipeline**: Automate video uploads with proper AI disclosure labels and visibility settings for content creators.
|
||||
|
||||
**Brand Campaigns**: Publish branded content with proper disclosure labels to maintain FTC compliance and platform guidelines.
|
||||
|
||||
**Image Slideshow Posts**: Create TikTok slideshows from product images or photo series with automatic cover selection.
|
||||
<!-- END MANUAL -->
|
||||
|
||||
---
|
||||
57
docs/platform/blocks/ayrshare/post_to_x.md
Normal file
57
docs/platform/blocks/ayrshare/post_to_x.md
Normal file
@@ -0,0 +1,57 @@
|
||||
# Post To X
|
||||
|
||||
### What it is
|
||||
Post to X / Twitter using Ayrshare
|
||||
|
||||
### How it works
|
||||
<!-- MANUAL: how_it_works -->
|
||||
This block uses Ayrshare's API to publish content to X (formerly Twitter). It supports standard tweets (280 characters, or 25,000 for Premium users), threads, polls, quote tweets, and replies, with up to 4 media attachments including video with subtitles.
|
||||
|
||||
The block authenticates through Ayrshare and handles X-specific features like automatic thread breaking using double newlines, thread numbering, per-post media attachments, and long-form video uploads (with approval). Poll options and duration can be configured for engagement posts.
|
||||
<!-- END MANUAL -->
|
||||
|
||||
### Inputs
|
||||
| Input | Description | Type | Required |
|
||||
|-------|-------------|------|----------|
|
||||
| post | The post text (max 280 chars, up to 25,000 for Premium users). Use @handle to mention users. Use \n\n for thread breaks. | str | Yes |
|
||||
| media_urls | Optional list of media URLs. X supports up to 4 images or videos per tweet. Auto-preview links unless media is included. | List[str] | No |
|
||||
| is_video | Whether the media is a video | bool | No |
|
||||
| schedule_date | UTC datetime for scheduling (YYYY-MM-DDThh:mm:ssZ) | str (date-time) | No |
|
||||
| disable_comments | Whether to disable comments | bool | No |
|
||||
| shorten_links | Whether to shorten links | bool | No |
|
||||
| unsplash | Unsplash image configuration | str | No |
|
||||
| requires_approval | Whether to enable approval workflow | bool | No |
|
||||
| random_post | Whether to generate random post text | bool | No |
|
||||
| random_media_url | Whether to generate random media | bool | No |
|
||||
| notes | Additional notes for the post | str | No |
|
||||
| reply_to_id | ID of the tweet to reply to | str | No |
|
||||
| quote_tweet_id | ID of the tweet to quote (low-level Tweet ID) | str | No |
|
||||
| poll_options | Poll options (2-4 choices) | List[str] | No |
|
||||
| poll_duration | Poll duration in minutes (1-10080) | int | No |
|
||||
| alt_text | Alt text for each image (max 1,000 chars each, not supported for videos) | List[str] | No |
|
||||
| is_thread | Whether to automatically break post into thread based on line breaks | bool | No |
|
||||
| thread_number | Add thread numbers (1/n format) to each thread post | bool | No |
|
||||
| thread_media_urls | Media URLs for thread posts (one per thread, use 'null' to skip) | List[str] | No |
|
||||
| long_post | Force long form post (requires Premium X account) | bool | No |
|
||||
| long_video | Enable long video upload (requires approval and Business/Enterprise plan) | bool | No |
|
||||
| subtitle_url | URL to SRT subtitle file for videos (must be HTTPS and end in .srt) | str | No |
|
||||
| subtitle_language | Language code for subtitles (default: 'en') | str | No |
|
||||
| subtitle_name | Name of caption track (max 150 chars, default: 'English') | str | No |
|
||||
|
||||
### Outputs
|
||||
| Output | Description | Type |
|
||||
|--------|-------------|------|
|
||||
| error | Error message if the operation failed | str |
|
||||
| post_result | The result of the post | PostResponse |
|
||||
| post | The result of the post | PostIds |
|
||||
|
||||
### Possible use case
|
||||
<!-- MANUAL: use_case -->
|
||||
**Thread Publishing**: Automatically format and publish long-form content as numbered thread sequences.
|
||||
|
||||
**Engagement Polls**: Create polls to gather audience feedback or drive interaction with scheduled posting.
|
||||
|
||||
**Reply Automation**: Build workflows that automatically respond to mentions or engage in conversations.
|
||||
<!-- END MANUAL -->
|
||||
|
||||
---
|
||||
60
docs/platform/blocks/ayrshare/post_to_youtube.md
Normal file
60
docs/platform/blocks/ayrshare/post_to_youtube.md
Normal file
@@ -0,0 +1,60 @@
|
||||
# Post To You Tube
|
||||
|
||||
### What it is
|
||||
Post to YouTube using Ayrshare
|
||||
|
||||
### How it works
|
||||
<!-- MANUAL: how_it_works -->
|
||||
This block uses Ayrshare's API to upload videos to YouTube. It handles video uploads with extensive metadata including titles, descriptions, tags, custom thumbnails, playlist assignment, category selection, and visibility controls (public, private, unlisted).
|
||||
|
||||
The block supports YouTube Shorts (up to 3 minutes), geographic targeting to allow or block specific countries, subtitle files (SRT/SBV format), synthetic/AI content disclosure, kids content labeling, and subscriber notification controls. Videos can be scheduled for specific publish times.
|
||||
<!-- END MANUAL -->
|
||||
|
||||
### Inputs
|
||||
| Input | Description | Type | Required |
|
||||
|-------|-------------|------|----------|
|
||||
| post | Video description (max 5,000 chars, empty string allowed). Cannot contain < or > characters. | str | Yes |
|
||||
| media_urls | Required video URL. YouTube only supports 1 video per post. | List[str] | No |
|
||||
| is_video | Whether the media is a video | bool | No |
|
||||
| schedule_date | UTC datetime for scheduling (YYYY-MM-DDThh:mm:ssZ) | str (date-time) | No |
|
||||
| disable_comments | Whether to disable comments | bool | No |
|
||||
| shorten_links | Whether to shorten links | bool | No |
|
||||
| unsplash | Unsplash image configuration | str | No |
|
||||
| requires_approval | Whether to enable approval workflow | bool | No |
|
||||
| random_post | Whether to generate random post text | bool | No |
|
||||
| random_media_url | Whether to generate random media | bool | No |
|
||||
| notes | Additional notes for the post | str | No |
|
||||
| title | Video title (max 100 chars, required). Cannot contain < or > characters. | str | Yes |
|
||||
| visibility | Video visibility: 'private' (default), 'public' , or 'unlisted' | "private" | "public" | "unlisted" | No |
|
||||
| thumbnail | Thumbnail URL (JPEG/PNG under 2MB, must end in .png/.jpg/.jpeg). Requires phone verification. | str | No |
|
||||
| playlist_id | Playlist ID to add video (user must own playlist) | str | No |
|
||||
| tags | Video tags (min 2 chars each, max 500 chars total) | List[str] | No |
|
||||
| made_for_kids | Self-declared kids content | bool | No |
|
||||
| is_shorts | Post as YouTube Short (max 3 minutes, adds #shorts) | bool | No |
|
||||
| notify_subscribers | Send notification to subscribers | bool | No |
|
||||
| category_id | Video category ID (e.g., 24 = Entertainment) | int | No |
|
||||
| contains_synthetic_media | Disclose realistic AI/synthetic content | bool | No |
|
||||
| publish_at | UTC publish time (YouTube controlled, format: 2022-10-08T21:18:36Z) | str | No |
|
||||
| targeting_block_countries | Country codes to block from viewing (e.g., ['US', 'CA']) | List[str] | No |
|
||||
| targeting_allow_countries | Country codes to allow viewing (e.g., ['GB', 'AU']) | List[str] | No |
|
||||
| subtitle_url | URL to SRT or SBV subtitle file (must be HTTPS and end in .srt/.sbv, under 100MB) | str | No |
|
||||
| subtitle_language | Language code for subtitles (default: 'en') | str | No |
|
||||
| subtitle_name | Name of caption track (max 150 chars, default: 'English') | str | No |
|
||||
|
||||
### Outputs
|
||||
| Output | Description | Type |
|
||||
|--------|-------------|------|
|
||||
| error | Error message if the operation failed | str |
|
||||
| post_result | The result of the post | PostResponse |
|
||||
| post | The result of the post | PostIds |
|
||||
|
||||
### Possible use case
|
||||
<!-- MANUAL: use_case -->
|
||||
**Video Publishing Pipeline**: Automate video uploads with thumbnails, descriptions, and playlist organization for content creators.
|
||||
|
||||
**YouTube Shorts Automation**: Publish short-form vertical videos to YouTube Shorts with proper metadata and hashtags.
|
||||
|
||||
**Multi-Region Content**: Upload videos with geographic restrictions for region-specific content licensing or compliance.
|
||||
<!-- END MANUAL -->
|
||||
|
||||
---
|
||||
147
docs/platform/blocks/baas/bots.md
Normal file
147
docs/platform/blocks/baas/bots.md
Normal file
@@ -0,0 +1,147 @@
|
||||
# Baas Bot Delete Recording
|
||||
|
||||
### What it is
|
||||
Permanently delete a meeting's recorded data
|
||||
|
||||
### How it works
|
||||
<!-- MANUAL: how_it_works -->
|
||||
This block permanently deletes the recorded data for a meeting bot using the BaaS (Bot as a Service) API. The deletion is irreversible and removes all associated recording files and transcripts.
|
||||
|
||||
Provide the bot_id from a previous recording session to delete that specific meeting's data.
|
||||
<!-- END MANUAL -->
|
||||
|
||||
### Inputs
|
||||
| Input | Description | Type | Required |
|
||||
|-------|-------------|------|----------|
|
||||
| bot_id | UUID of the bot whose data to delete | str | Yes |
|
||||
|
||||
### Outputs
|
||||
| Output | Description | Type |
|
||||
|--------|-------------|------|
|
||||
| error | Error message if the operation failed | str |
|
||||
| deleted | Whether the data was successfully deleted | bool |
|
||||
|
||||
### Possible use case
|
||||
<!-- MANUAL: use_case -->
|
||||
**Privacy Compliance**: Delete recordings to comply with data retention policies or user requests.
|
||||
|
||||
**Storage Management**: Clean up old recordings to manage storage costs.
|
||||
|
||||
**Post-Processing Cleanup**: Delete recordings after extracting needed information.
|
||||
<!-- END MANUAL -->
|
||||
|
||||
---
|
||||
|
||||
## Baas Bot Fetch Meeting Data
|
||||
|
||||
### What it is
|
||||
Retrieve recorded meeting data
|
||||
|
||||
### How it works
|
||||
<!-- MANUAL: how_it_works -->
|
||||
This block retrieves recorded meeting data including video URL, transcript, and metadata from a completed bot session. The video URL is time-limited and should be downloaded promptly.
|
||||
|
||||
Enable include_transcripts to receive the full meeting transcript with speaker identification and timestamps.
|
||||
<!-- END MANUAL -->
|
||||
|
||||
### Inputs
|
||||
| Input | Description | Type | Required |
|
||||
|-------|-------------|------|----------|
|
||||
| bot_id | UUID of the bot whose data to fetch | str | Yes |
|
||||
| include_transcripts | Include transcript data in response | bool | No |
|
||||
|
||||
### Outputs
|
||||
| Output | Description | Type |
|
||||
|--------|-------------|------|
|
||||
| error | Error message if the operation failed | str |
|
||||
| mp4_url | URL to download the meeting recording (time-limited) | str |
|
||||
| transcript | Meeting transcript data | List[Any] |
|
||||
| metadata | Meeting metadata and bot information | Dict[str, True] |
|
||||
|
||||
### Possible use case
|
||||
<!-- MANUAL: use_case -->
|
||||
**Meeting Summarization**: Retrieve transcripts for AI summarization and action item extraction.
|
||||
|
||||
**Recording Archive**: Download and store meeting recordings for compliance or reference.
|
||||
|
||||
**Analytics**: Extract meeting metadata for participation and duration analytics.
|
||||
<!-- END MANUAL -->
|
||||
|
||||
---
|
||||
|
||||
## Baas Bot Join Meeting
|
||||
|
||||
### What it is
|
||||
Deploy a bot to join and record a meeting
|
||||
|
||||
### How it works
|
||||
<!-- MANUAL: how_it_works -->
|
||||
This block deploys a recording bot to join a video meeting (Zoom, Google Meet, Teams). Configure the bot's display name, avatar, and entry message. The bot joins, records, and transcribes the meeting.
|
||||
|
||||
Use webhooks to receive notifications when the meeting ends and recordings are ready.
|
||||
<!-- END MANUAL -->
|
||||
|
||||
### Inputs
|
||||
| Input | Description | Type | Required |
|
||||
|-------|-------------|------|----------|
|
||||
| meeting_url | The URL of the meeting the bot should join | str | Yes |
|
||||
| bot_name | Display name for the bot in the meeting | str | Yes |
|
||||
| bot_image | URL to an image for the bot's avatar (16:9 ratio recommended) | str | No |
|
||||
| entry_message | Chat message the bot will post upon entry | str | No |
|
||||
| reserved | Use a reserved bot slot (joins 4 min before meeting) | bool | No |
|
||||
| start_time | Unix timestamp (ms) when bot should join | int | No |
|
||||
| webhook_url | URL to receive webhook events for this bot | str | No |
|
||||
| timeouts | Automatic leave timeouts configuration | Dict[str, True] | No |
|
||||
| extra | Custom metadata to attach to the bot | Dict[str, True] | No |
|
||||
|
||||
### Outputs
|
||||
| Output | Description | Type |
|
||||
|--------|-------------|------|
|
||||
| error | Error message if the operation failed | str |
|
||||
| bot_id | UUID of the deployed bot | str |
|
||||
| join_response | Full response from join operation | Dict[str, True] |
|
||||
|
||||
### Possible use case
|
||||
<!-- MANUAL: use_case -->
|
||||
**Automated Recording**: Record meetings automatically without requiring host intervention.
|
||||
|
||||
**Meeting Assistant**: Deploy bots to take notes and transcribe customer or team meetings.
|
||||
|
||||
**Compliance Recording**: Ensure all meetings are recorded for compliance or quality assurance.
|
||||
<!-- END MANUAL -->
|
||||
|
||||
---
|
||||
|
||||
## Baas Bot Leave Meeting
|
||||
|
||||
### What it is
|
||||
Remove a bot from an ongoing meeting
|
||||
|
||||
### How it works
|
||||
<!-- MANUAL: how_it_works -->
|
||||
This block removes a recording bot from an ongoing meeting. Use this when you need to stop recording before the meeting naturally ends.
|
||||
|
||||
The bot leaves gracefully and recording data becomes available for retrieval.
|
||||
<!-- END MANUAL -->
|
||||
|
||||
### Inputs
|
||||
| Input | Description | Type | Required |
|
||||
|-------|-------------|------|----------|
|
||||
| bot_id | UUID of the bot to remove from meeting | str | Yes |
|
||||
|
||||
### Outputs
|
||||
| Output | Description | Type |
|
||||
|--------|-------------|------|
|
||||
| error | Error message if the operation failed | str |
|
||||
| left | Whether the bot successfully left | bool |
|
||||
|
||||
### Possible use case
|
||||
<!-- MANUAL: use_case -->
|
||||
**Early Termination**: Stop recording when a meeting transitions to an off-record discussion.
|
||||
|
||||
**Time-Based Recording**: Leave after capturing a specific portion of a meeting.
|
||||
|
||||
**Error Recovery**: Remove and redeploy bots when issues occur during recording.
|
||||
<!-- END MANUAL -->
|
||||
|
||||
---
|
||||
42
docs/platform/blocks/bannerbear/text_overlay.md
Normal file
42
docs/platform/blocks/bannerbear/text_overlay.md
Normal file
@@ -0,0 +1,42 @@
|
||||
# Bannerbear Text Overlay
|
||||
|
||||
### What it is
|
||||
Add text overlay to images using Bannerbear templates. Perfect for creating social media graphics, marketing materials, and dynamic image content.
|
||||
|
||||
### How it works
|
||||
<!-- MANUAL: how_it_works -->
|
||||
This block uses Bannerbear's API to generate images by populating templates with dynamic text and images. Create templates in Bannerbear with text layers, then modify layer content programmatically.
|
||||
|
||||
Webhooks can notify you when asynchronous generation completes. Include custom metadata for tracking generated images.
|
||||
<!-- END MANUAL -->
|
||||
|
||||
### Inputs
|
||||
| Input | Description | Type | Required |
|
||||
|-------|-------------|------|----------|
|
||||
| template_id | The unique ID of your Bannerbear template | str | Yes |
|
||||
| project_id | Optional: Project ID (required when using Master API Key) | str | No |
|
||||
| text_modifications | List of text layers to modify in the template | List[TextModification] | Yes |
|
||||
| image_url | Optional: URL of an image to use in the template | str | No |
|
||||
| image_layer_name | Optional: Name of the image layer in the template | str | No |
|
||||
| webhook_url | Optional: URL to receive webhook notification when image is ready | str | No |
|
||||
| metadata | Optional: Custom metadata to attach to the image | str | No |
|
||||
|
||||
### Outputs
|
||||
| Output | Description | Type |
|
||||
|--------|-------------|------|
|
||||
| error | Error message if the operation failed | str |
|
||||
| success | Whether the image generation was successfully initiated | bool |
|
||||
| image_url | URL of the generated image (if synchronous) or placeholder | str |
|
||||
| uid | Unique identifier for the generated image | str |
|
||||
| status | Status of the image generation | str |
|
||||
|
||||
### Possible use case
|
||||
<!-- MANUAL: use_case -->
|
||||
**Social Media Graphics**: Generate personalized social posts with dynamic quotes, stats, or headlines.
|
||||
|
||||
**Marketing Banners**: Create ad banners with different product names, prices, or offers.
|
||||
|
||||
**Certificates & Cards**: Generate personalized certificates, invitations, or greeting cards.
|
||||
<!-- END MANUAL -->
|
||||
|
||||
---
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user