mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-10-11 22:40:03 +00:00
feat(cli): prioritize popular platform integrations (#7573)
* feat(cli): prioritize platform integrations * test(platform): update missing token assertion * chore: ignore Codex workspace files * feat(platform): expand integration catalog * feat(platform): show integration action counts * feat(platform): select agent actions separately * feat(platform): group integration applications * fix(cli): keep platform picker navigation in place * fix(cli): validate platform apps once * fix(platform): use one integration token variable * test(platform): update integration token assertion * fix(cli): clarify platform token validation * fix(cli): show platform token prompts * refactor(platform): expose application catalog * refactor(platform): export catalog from Python * docs(platform): document catalog regeneration * fix(platform): resolve catalog type export
This commit is contained in:
@@ -27,6 +27,7 @@ conceptual_plan.md
|
||||
build_image
|
||||
chromadb-*.lock
|
||||
.claude
|
||||
.codex
|
||||
.crewai/memory
|
||||
blogs/*
|
||||
secrets/*
|
||||
|
||||
@@ -12,6 +12,13 @@ from typing import Any
|
||||
import warnings
|
||||
|
||||
import click
|
||||
from crewai_core.platform_apps import (
|
||||
PLATFORM_APPLICATION_CATALOG,
|
||||
PLATFORM_APP_CATEGORIES,
|
||||
PLATFORM_APP_DISPLAY_NAMES,
|
||||
PLATFORM_APP_TOOLS,
|
||||
PLATFORM_APP_TOOL_COUNTS,
|
||||
)
|
||||
from crewai_core.telemetry import Telemetry
|
||||
from rich.console import Console
|
||||
from rich.text import Text
|
||||
@@ -19,7 +26,6 @@ from rich.text import Text
|
||||
from crewai_cli.constants import ENV_VARS
|
||||
from crewai_cli.git import initialize_if_git_available
|
||||
from crewai_cli.model_catalog import get_provider_models
|
||||
from crewai_cli.platform_tools_catalog import PLATFORM_TOOLS
|
||||
from crewai_cli.tui_picker import pick_many, pick_one
|
||||
from crewai_cli.utils import (
|
||||
copy_assistant_instructions,
|
||||
@@ -109,7 +115,16 @@ _TEMPLATES_DIR = Path(__file__).parent / "templates" / "json_crew"
|
||||
# ── Common tools for picker ────────────────────────────────────
|
||||
|
||||
_TOOL_CATEGORIES: list[tuple[str, list[tuple[str, str]]]] = [
|
||||
("CrewAI Platform", PLATFORM_TOOLS),
|
||||
(
|
||||
"CrewAI Platform",
|
||||
[
|
||||
(
|
||||
f"platform:{application.slug}",
|
||||
f"{application.display_name} Integration",
|
||||
)
|
||||
for application in PLATFORM_APPLICATION_CATALOG
|
||||
],
|
||||
),
|
||||
(
|
||||
"Search & Research",
|
||||
[
|
||||
@@ -234,9 +249,9 @@ _FLAT_TOOLS: list[tuple[str, str]] = [
|
||||
_COMMON_TOOL_ORDER = [
|
||||
"SerperDevTool",
|
||||
"ScrapeWebsiteTool",
|
||||
"DirectoryReadTool",
|
||||
"FileReadTool",
|
||||
"FileWriterTool",
|
||||
"platform:gmail",
|
||||
"platform:whatsapp",
|
||||
]
|
||||
|
||||
_ANSI_SEQUENCE_RE = re.compile(r"\x1b\[[0-?]*[ -/]*[@-~]")
|
||||
@@ -313,7 +328,11 @@ def _show_interpolation_hint(kind: str) -> None:
|
||||
def _tool_label(name: str, description: str) -> str:
|
||||
if name.startswith("platform:"):
|
||||
app_name = description.removesuffix(" Integration").replace(" ", "")
|
||||
return f"{description:<48s} Platform: {app_name.replace(' ', '')}Integration"
|
||||
tool_count = PLATFORM_APP_TOOL_COUNTS.get(name.removeprefix("platform:"), 0)
|
||||
return (
|
||||
f"{description:<48s} Platform: {app_name.replace(' ', '')}Integration "
|
||||
f"({tool_count} tools)"
|
||||
)
|
||||
return f"{description:<48s} {name}"
|
||||
|
||||
|
||||
@@ -322,12 +341,20 @@ def _tool_category_label(category: str) -> str:
|
||||
|
||||
|
||||
def _category_row_label(
|
||||
category: str, tools: list[tuple[str, str]], selected: set[str], expanded: bool
|
||||
category: str,
|
||||
tools: list[tuple[str, str]],
|
||||
selected: set[str],
|
||||
expanded: bool,
|
||||
*,
|
||||
item_label: str | None = None,
|
||||
) -> str:
|
||||
"""Render an accordion category row with tool/selection counts."""
|
||||
marker = "▾" if expanded else "▸"
|
||||
sel_count = sum(1 for name, _desc in tools if name in selected)
|
||||
suffix = f"{len(tools)} tools"
|
||||
item_label = item_label or (
|
||||
"applications" if category == "CrewAI Platform" else "tools"
|
||||
)
|
||||
suffix = f"{len(tools)} {item_label}"
|
||||
if sel_count:
|
||||
suffix += f", {sel_count} selected"
|
||||
return f"{marker} {category} ({suffix})"
|
||||
@@ -348,8 +375,26 @@ def _select_tools() -> list[str]:
|
||||
]
|
||||
common_tool_names = {name for name, _desc in common_tools}
|
||||
|
||||
platform_tools = next(
|
||||
tools for category, tools in _TOOL_CATEGORIES if category == "CrewAI Platform"
|
||||
)
|
||||
platform_groups = [
|
||||
(
|
||||
category.name,
|
||||
[
|
||||
(
|
||||
f"platform:{app}",
|
||||
f"{PLATFORM_APP_DISPLAY_NAMES[app]} Integration",
|
||||
)
|
||||
for app in category.apps
|
||||
],
|
||||
)
|
||||
for category in PLATFORM_APP_CATEGORIES
|
||||
]
|
||||
categories: list[tuple[str, list[tuple[str, str]]]] = []
|
||||
for category, category_tools in _TOOL_CATEGORIES:
|
||||
if category == "CrewAI Platform":
|
||||
continue
|
||||
remaining_tools = [
|
||||
(name, desc)
|
||||
for name, desc in category_tools
|
||||
@@ -360,6 +405,7 @@ def _select_tools() -> list[str]:
|
||||
|
||||
selected: set[str] = set()
|
||||
expanded: str | None = None
|
||||
expanded_platform_group: str | None = None
|
||||
focus_category: str | None = None
|
||||
first_render = True
|
||||
|
||||
@@ -368,7 +414,7 @@ def _select_tools() -> list[str]:
|
||||
tool_by_index: dict[int, str] = {}
|
||||
separator_indices: set[int] = set()
|
||||
action_indices: set[int] = set()
|
||||
category_by_index: dict[int, str] = {}
|
||||
action_by_index: dict[int, tuple[str, str]] = {}
|
||||
preselected: set[int] = set()
|
||||
initial_cursor: int | None = None
|
||||
|
||||
@@ -380,10 +426,48 @@ def _select_tools() -> list[str]:
|
||||
tool_by_index[len(labels)] = name
|
||||
labels.append(_tool_label(name, desc))
|
||||
|
||||
platform_row = len(labels)
|
||||
action_indices.add(platform_row)
|
||||
action_by_index[platform_row] = ("category", "CrewAI Platform")
|
||||
if focus_category == "CrewAI Platform":
|
||||
initial_cursor = platform_row
|
||||
labels.append(
|
||||
_category_row_label(
|
||||
"CrewAI Platform",
|
||||
platform_tools,
|
||||
selected,
|
||||
expanded == "CrewAI Platform",
|
||||
)
|
||||
)
|
||||
if expanded == "CrewAI Platform":
|
||||
for group, apps in platform_groups:
|
||||
row = len(labels)
|
||||
action_indices.add(row)
|
||||
action_by_index[row] = ("platform_group", group)
|
||||
is_expanded = group == expanded_platform_group
|
||||
if group == focus_category:
|
||||
initial_cursor = row
|
||||
labels.append(
|
||||
" "
|
||||
+ _category_row_label(
|
||||
group,
|
||||
apps,
|
||||
selected,
|
||||
is_expanded,
|
||||
item_label="applications",
|
||||
)
|
||||
)
|
||||
if is_expanded:
|
||||
for name, description in apps:
|
||||
if name in selected:
|
||||
preselected.add(len(labels))
|
||||
tool_by_index[len(labels)] = name
|
||||
labels.append(" " + _tool_label(name, description))
|
||||
|
||||
for category, category_tools in categories:
|
||||
row = len(labels)
|
||||
action_indices.add(row)
|
||||
category_by_index[row] = category
|
||||
action_by_index[row] = ("category", category)
|
||||
is_expanded = category == expanded
|
||||
if category == focus_category:
|
||||
initial_cursor = row
|
||||
@@ -415,16 +499,62 @@ def _select_tools() -> list[str]:
|
||||
|
||||
if action is None:
|
||||
break
|
||||
toggled = category_by_index.get(action)
|
||||
focus_category = toggled
|
||||
expanded = None if toggled == expanded else toggled
|
||||
action_type, category = action_by_index[action]
|
||||
if action_type == "platform_group":
|
||||
focus_category = category
|
||||
expanded_platform_group = (
|
||||
None if category == expanded_platform_group else category
|
||||
)
|
||||
continue
|
||||
|
||||
ordered = [name for name, _desc in common_tools] + [
|
||||
name for _cat, cat_tools in categories for name, _desc in cat_tools
|
||||
]
|
||||
focus_category = category
|
||||
if category == "CrewAI Platform":
|
||||
expanded = None if category == expanded else category
|
||||
expanded_platform_group = None
|
||||
else:
|
||||
expanded = None if category == expanded else category
|
||||
expanded_platform_group = None
|
||||
|
||||
ordered = (
|
||||
[name for name, _desc in common_tools]
|
||||
+ [
|
||||
f"platform:{app}"
|
||||
for category in PLATFORM_APP_CATEGORIES
|
||||
for app in category.apps
|
||||
if f"platform:{app}" not in common_tool_names
|
||||
]
|
||||
+ [name for _cat, cat_tools in categories for name, _desc in cat_tools]
|
||||
)
|
||||
return [name for name in ordered if name in selected]
|
||||
|
||||
|
||||
def _select_platform_actions(selected_tools: list[str]) -> list[str]:
|
||||
"""Replace selected Platform applications with the chosen actions for an agent."""
|
||||
platform_apps = [
|
||||
tool.removeprefix("platform:")
|
||||
for tool in selected_tools
|
||||
if tool.startswith("platform:") and "/" not in tool
|
||||
]
|
||||
non_platform_tools = [
|
||||
tool for tool in selected_tools if not tool.startswith("platform:")
|
||||
]
|
||||
platform_actions = [
|
||||
tool for tool in selected_tools if tool.startswith("platform:") and "/" in tool
|
||||
]
|
||||
|
||||
for app in platform_apps:
|
||||
action_indices = pick_many(
|
||||
f"{PLATFORM_APP_DISPLAY_NAMES[app]} actions for this agent (space to toggle):",
|
||||
[f"{item.display_name} ({item.slug})" for item in PLATFORM_APP_TOOLS[app]],
|
||||
)
|
||||
platform_actions.extend(
|
||||
f"platform:{app}/{PLATFORM_APP_TOOLS[app][index].slug}"
|
||||
for index in action_indices
|
||||
)
|
||||
|
||||
return non_platform_tools + platform_actions
|
||||
|
||||
|
||||
def _wizard_agent(
|
||||
agent_num: int,
|
||||
existing_names: list[str],
|
||||
@@ -473,7 +603,7 @@ def _wizard_agent(
|
||||
else:
|
||||
llm = _select_model()
|
||||
|
||||
tools = _select_tools()
|
||||
tools = _select_platform_actions(_select_tools())
|
||||
if tools:
|
||||
_success(f"{len(tools)} tool{'s' if len(tools) != 1 else ''}")
|
||||
else:
|
||||
@@ -877,12 +1007,12 @@ def _setup_env(folder_path: Path, llm_model: str) -> None:
|
||||
|
||||
|
||||
def _platform_apps_from_agents(agents: list[dict[str, Any]]) -> list[str]:
|
||||
"""Return unique platform applications selected across all agents."""
|
||||
"""Return unique platform application slugs selected across all agents."""
|
||||
apps: list[str] = []
|
||||
for agent in agents:
|
||||
for tool in agent.get("tools", []):
|
||||
if isinstance(tool, str) and tool.startswith("platform:"):
|
||||
app = tool.removeprefix("platform:")
|
||||
app = tool.removeprefix("platform:").split("/", maxsplit=1)[0]
|
||||
if app and app not in apps:
|
||||
apps.append(app)
|
||||
return apps
|
||||
@@ -890,28 +1020,24 @@ def _platform_apps_from_agents(agents: list[dict[str, Any]]) -> list[str]:
|
||||
|
||||
def _platform_app_name(app: str) -> str:
|
||||
"""Return the display name for a platform application slug."""
|
||||
return (
|
||||
dict(PLATFORM_TOOLS)
|
||||
.get(f"platform:{app}", app.replace("_", " ").title())
|
||||
.removesuffix(" Integration")
|
||||
)
|
||||
return PLATFORM_APP_DISPLAY_NAMES.get(app, app.replace("_", " ").title())
|
||||
|
||||
|
||||
def _prompt_platform_token() -> str:
|
||||
"""Explain how to obtain and securely prompt for an AMP integration token."""
|
||||
"""Explain how to prompt for an AMP Enterprise Action Auth Token."""
|
||||
click.secho(
|
||||
" To use CrewAI Platform tools, you need a CrewAI Platform Integration Token.",
|
||||
" To use CrewAI Platform tools, you need a CrewAI Platform Enterprise Action Auth Token.",
|
||||
fg="yellow",
|
||||
)
|
||||
click.secho(
|
||||
" Get your token from CrewAI AMP: https://app.crewai.com "
|
||||
"→ Settings → Integration Tokens.",
|
||||
"→ Settings → Account → Integration Token.",
|
||||
fg="cyan",
|
||||
)
|
||||
return str(
|
||||
click.prompt(
|
||||
click.style(" CREWAI_PLATFORM_INTEGRATION_TOKEN", fg="cyan"),
|
||||
hide_input=True,
|
||||
hide_input=False,
|
||||
prompt_suffix=click.style(" > ", fg="bright_white"),
|
||||
)
|
||||
).strip()
|
||||
@@ -954,7 +1080,7 @@ def _report_platform_app_validation(
|
||||
status_code = getattr(getattr(error, "response", None), "status_code", None)
|
||||
if status_code in {401, 403}:
|
||||
click.secho(
|
||||
" ✘ CrewAI Platform Integration Token is invalid or expired",
|
||||
" ✘ CrewAI Platform Integration token is invalid or expired",
|
||||
fg="red",
|
||||
)
|
||||
return True
|
||||
@@ -1008,7 +1134,7 @@ def _validate_platform_apps(
|
||||
app_name = _platform_app_name(app)
|
||||
click.echo()
|
||||
click.secho(
|
||||
" Checking CrewAI Platform Integration Token and "
|
||||
" Checking CrewAI Platform Integration token and "
|
||||
f"{app_name} integration on AMP...",
|
||||
fg="cyan",
|
||||
)
|
||||
@@ -1027,7 +1153,7 @@ def _show_platform_validation_guidance(
|
||||
click.echo()
|
||||
if token_invalid:
|
||||
click.secho(
|
||||
" Check your CrewAI Platform Integration Token in AMP.",
|
||||
" Check your CrewAI Platform Integration token in AMP.",
|
||||
fg="yellow",
|
||||
)
|
||||
return
|
||||
@@ -1037,7 +1163,7 @@ def _show_platform_validation_guidance(
|
||||
" Check the "
|
||||
f"{', '.join(failed_app_names)} integration"
|
||||
f"{'s' if len(failed_app_names) != 1 else ''} and your CrewAI "
|
||||
"Platform Integration Token in AMP.",
|
||||
"Platform Integration token in AMP.",
|
||||
fg="yellow",
|
||||
)
|
||||
|
||||
@@ -1053,7 +1179,7 @@ def _prompt_platform_revalidation_token() -> str:
|
||||
),
|
||||
default="",
|
||||
show_default=False,
|
||||
hide_input=True,
|
||||
hide_input=False,
|
||||
prompt_suffix=click.style(" > ", fg="bright_white"),
|
||||
)
|
||||
).strip()
|
||||
@@ -1082,18 +1208,35 @@ def _setup_platform_auth(agents: list[dict[str, Any]]) -> str | None:
|
||||
"`pip install 'crewai[tools]'`."
|
||||
) from error
|
||||
|
||||
click.secho(
|
||||
" Checking for CREWAI_PLATFORM_INTEGRATION_TOKEN...",
|
||||
fg="cyan",
|
||||
)
|
||||
token = os.environ.get("CREWAI_PLATFORM_INTEGRATION_TOKEN", "")
|
||||
credential_location: str | None = None
|
||||
if token:
|
||||
credential_location = "CREWAI_PLATFORM_INTEGRATION_TOKEN environment variable"
|
||||
|
||||
while True:
|
||||
if not token:
|
||||
click.secho(
|
||||
" No CREWAI_PLATFORM_INTEGRATION_TOKEN found; prompting for one.",
|
||||
fg="yellow",
|
||||
)
|
||||
token = _prompt_platform_token()
|
||||
if token:
|
||||
credential_location = "interactive prompt"
|
||||
if not token:
|
||||
click.secho(
|
||||
" A CrewAI Platform Integration Token is required to validate "
|
||||
" A CrewAI Platform Integration token is required to validate "
|
||||
"the selected integrations.",
|
||||
fg="yellow",
|
||||
)
|
||||
continue
|
||||
|
||||
_success(
|
||||
f"Platform Integration token found via {credential_location}", dim=True
|
||||
)
|
||||
os.environ["CREWAI_PLATFORM_INTEGRATION_TOKEN"] = token
|
||||
failed_apps, token_invalid = _validate_platform_apps(
|
||||
apps, ApplicationSelector, client_for_selector
|
||||
@@ -1110,6 +1253,7 @@ def _setup_platform_auth(agents: list[dict[str, Any]]) -> str | None:
|
||||
replacement_token = _prompt_platform_revalidation_token()
|
||||
if replacement_token:
|
||||
token = replacement_token
|
||||
credential_location = "interactive replacement prompt"
|
||||
os.environ["CREWAI_PLATFORM_INTEGRATION_TOKEN"] = token
|
||||
|
||||
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
"""CrewAI AMP platform tools exposed by the crew-creation wizard."""
|
||||
|
||||
from crewai_core.platform_apps import PLATFORM_APPS
|
||||
|
||||
|
||||
PLATFORM_TOOL_PREFIX = "platform:"
|
||||
|
||||
_APP_DESCRIPTIONS: dict[str, str] = {
|
||||
"asana": "Asana Integration",
|
||||
"box": "Box Integration",
|
||||
"clickup": "ClickUp Integration",
|
||||
"github": "GitHub Integration",
|
||||
"gmail": "Gmail Integration",
|
||||
"google_calendar": "Google Calendar Integration",
|
||||
"google_sheets": "Google Sheets Integration",
|
||||
"hubspot": "HubSpot Integration",
|
||||
"jira": "Jira Integration",
|
||||
"linear": "Linear Integration",
|
||||
"notion": "Notion Integration",
|
||||
"salesforce": "Salesforce Integration",
|
||||
"shopify": "Shopify Integration",
|
||||
"slack": "Slack Integration",
|
||||
"stripe": "Stripe Integration",
|
||||
"zendesk": "Zendesk Integration",
|
||||
}
|
||||
|
||||
PLATFORM_TOOLS: list[tuple[str, str]] = [
|
||||
(f"{PLATFORM_TOOL_PREFIX}{app}", _APP_DESCRIPTIONS[app]) for app in PLATFORM_APPS
|
||||
]
|
||||
@@ -3,6 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from contextlib import suppress
|
||||
import shutil
|
||||
import sys
|
||||
from typing import overload
|
||||
|
||||
@@ -96,18 +97,22 @@ def _draw_multi(
|
||||
action_indices: set[int] | None = None,
|
||||
separator_indices: set[int] | None = None,
|
||||
clear: bool = False,
|
||||
) -> None:
|
||||
previous_line_count: int | None = None,
|
||||
) -> int:
|
||||
action_indices = action_indices or set()
|
||||
separator_indices = separator_indices or set()
|
||||
start, end = _visible_row_range(len(labels), cursor)
|
||||
hint_text = "↑↓ navigate, space toggle, enter confirm"
|
||||
if action_indices:
|
||||
hint_text = "↑↓ navigate, space toggle, enter confirm, ▸ rows expand/collapse"
|
||||
if end - start < len(labels):
|
||||
hint_text += f" · showing {start + 1}-{end} of {len(labels)}"
|
||||
hint = f" {_DIM}{hint_text}{_RESET}"
|
||||
total = len(labels) + 1
|
||||
if clear:
|
||||
sys.stdout.write(f"\033[{total}A")
|
||||
sys.stdout.write(f"\033[{previous_line_count or 1}A")
|
||||
sys.stdout.write(f"\033[2K{hint}\n")
|
||||
for i, label in enumerate(labels):
|
||||
for i in range(start, end):
|
||||
label = labels[i]
|
||||
if i in separator_indices:
|
||||
sys.stdout.write(f"\033[2K {_TEAL}{label}{_RESET}\n")
|
||||
continue
|
||||
@@ -121,6 +126,17 @@ def _draw_multi(
|
||||
bold = f"{_BOLD}{label}{_RESET}" if i == cursor else label
|
||||
sys.stdout.write(f"\033[2K {arrow}{check} {bold}\n")
|
||||
sys.stdout.flush()
|
||||
return end - start + 1
|
||||
|
||||
|
||||
def _visible_row_range(total: int, cursor: int) -> tuple[int, int]:
|
||||
"""Return the portion of a multi-select list that fits in the terminal."""
|
||||
max_rows = max(5, shutil.get_terminal_size(fallback=(80, 24)).lines - 5)
|
||||
if total <= max_rows:
|
||||
return 0, total
|
||||
|
||||
start = min(max(cursor - max_rows // 2, 0), total - max_rows)
|
||||
return start, start + max_rows
|
||||
|
||||
|
||||
def _arrow_select_one(labels: list[str]) -> int:
|
||||
@@ -168,7 +184,7 @@ def _arrow_select_multi(
|
||||
sys.stdout.write(_HIDE_CURSOR)
|
||||
sys.stdout.flush()
|
||||
try:
|
||||
_draw_multi(
|
||||
rendered_lines = _draw_multi(
|
||||
labels,
|
||||
cursor,
|
||||
selected,
|
||||
@@ -179,44 +195,47 @@ def _arrow_select_multi(
|
||||
key = _read_key()
|
||||
if key == "up":
|
||||
cursor = _next_selectable_index(cursor, -1, total, separator_indices)
|
||||
_draw_multi(
|
||||
rendered_lines = _draw_multi(
|
||||
labels,
|
||||
cursor,
|
||||
selected,
|
||||
action_indices=action_indices,
|
||||
separator_indices=separator_indices,
|
||||
clear=True,
|
||||
previous_line_count=rendered_lines,
|
||||
)
|
||||
elif key == "down":
|
||||
cursor = _next_selectable_index(cursor, 1, total, separator_indices)
|
||||
_draw_multi(
|
||||
rendered_lines = _draw_multi(
|
||||
labels,
|
||||
cursor,
|
||||
selected,
|
||||
action_indices=action_indices,
|
||||
separator_indices=separator_indices,
|
||||
clear=True,
|
||||
previous_line_count=rendered_lines,
|
||||
)
|
||||
elif key == "space":
|
||||
if cursor in action_indices:
|
||||
_clear_lines(total + 1)
|
||||
_clear_lines(rendered_lines)
|
||||
return sorted(selected), cursor
|
||||
selected ^= {cursor}
|
||||
_draw_multi(
|
||||
rendered_lines = _draw_multi(
|
||||
labels,
|
||||
cursor,
|
||||
selected,
|
||||
action_indices=action_indices,
|
||||
separator_indices=separator_indices,
|
||||
clear=True,
|
||||
previous_line_count=rendered_lines,
|
||||
)
|
||||
elif key == "enter":
|
||||
_clear_lines(total + 1)
|
||||
_clear_lines(rendered_lines)
|
||||
if cursor in action_indices:
|
||||
return sorted(selected), cursor
|
||||
return sorted(selected), None
|
||||
elif key in ("esc", "q"):
|
||||
_clear_lines(total + 1)
|
||||
_clear_lines(rendered_lines)
|
||||
return sorted(selected), None
|
||||
finally:
|
||||
sys.stdout.write(_SHOW_CURSOR)
|
||||
|
||||
@@ -470,16 +470,18 @@ def test_json_wizard_tool_picker_prioritizes_common_tools(monkeypatch):
|
||||
|
||||
tools = json_crew._select_tools()
|
||||
|
||||
assert tools == ["SerperDevTool", "DirectoryReadTool"]
|
||||
assert tools == ["SerperDevTool", "FileReadTool"]
|
||||
assert len(picker_calls) == 1
|
||||
labels = picker_calls[0][1]
|
||||
assert 0 in picker_calls[0][2]["separator_indices"]
|
||||
assert labels[0] == "── Common tools ──"
|
||||
assert labels[1].strip().endswith("SerperDevTool")
|
||||
assert labels[2].strip().endswith("ScrapeWebsiteTool")
|
||||
assert labels[3].strip().endswith("DirectoryReadTool")
|
||||
assert labels[4].strip().endswith("FileReadTool")
|
||||
assert labels[5].strip().endswith("FileWriterTool")
|
||||
assert labels[3].strip().endswith("FileReadTool")
|
||||
assert labels[4].startswith("Gmail Integration")
|
||||
assert labels[4].endswith("Platform: GmailIntegration (66 tools)")
|
||||
assert labels[5].startswith("WhatsApp Integration")
|
||||
assert labels[5].endswith("Platform: WhatsAppIntegration (58 tools)")
|
||||
assert labels[1].index("Google search") < labels[1].index("SerperDevTool")
|
||||
assert "More tools" not in labels
|
||||
|
||||
@@ -504,6 +506,7 @@ def test_json_wizard_tool_picker_collapses_categories_by_default(monkeypatch):
|
||||
assert len(action_indices) >= 4
|
||||
# Only the common tools section is visible beyond the category rows
|
||||
assert len(labels) == 1 + 5 + len(action_indices)
|
||||
assert any(label == "▸ CrewAI Platform (125 applications)" for label in labels)
|
||||
|
||||
|
||||
def test_json_wizard_tool_picker_expands_one_category_at_a_time(monkeypatch):
|
||||
@@ -584,15 +587,26 @@ def test_json_wizard_tool_picker_lists_builtin_tools_across_categories(monkeypat
|
||||
picker_calls: list[tuple[str, list[str], dict[str, object]]] = []
|
||||
expanded_labels: list[str] = []
|
||||
|
||||
categories = [
|
||||
"Search & Research",
|
||||
"Web Scraping",
|
||||
"File & Document",
|
||||
"Code & Data",
|
||||
"Cloud & Storage",
|
||||
"Sandbox & Automation",
|
||||
"AI & Vision",
|
||||
]
|
||||
|
||||
def pick_many(title: str, labels: list[str], **kwargs):
|
||||
picker_calls.append((title, labels, kwargs))
|
||||
expanded_labels.extend(labels)
|
||||
action_indices = sorted(kwargs["action_indices"])
|
||||
call_num = len(picker_calls)
|
||||
if call_num <= len(action_indices):
|
||||
# Expand the n-th category (indices shift between renders, so
|
||||
# recompute from this render's action rows)
|
||||
return [], action_indices[call_num - 1]
|
||||
if call_num <= len(categories):
|
||||
category = categories[call_num - 1]
|
||||
category_row = next(
|
||||
idx for idx, label in enumerate(labels) if category in label
|
||||
)
|
||||
return [], category_row
|
||||
return [], None
|
||||
|
||||
monkeypatch.setattr(json_crew, "pick_many", pick_many)
|
||||
@@ -637,13 +651,28 @@ def test_json_wizard_platform_tool_selection_stays_in_agent_tools(monkeypatch):
|
||||
)
|
||||
return [], platform_row
|
||||
|
||||
github = next(
|
||||
if picker_calls == 2:
|
||||
engineering = next(
|
||||
idx
|
||||
for idx, label in enumerate(labels)
|
||||
if "Engineering, data & infrastructure" in label
|
||||
)
|
||||
return [], engineering
|
||||
|
||||
if picker_calls == 3:
|
||||
github = next(
|
||||
idx
|
||||
for idx, label in enumerate(labels)
|
||||
if label.strip().startswith("GitHub Integration")
|
||||
)
|
||||
return [github], None
|
||||
|
||||
create_repository = next(
|
||||
idx
|
||||
for idx, label in enumerate(labels)
|
||||
if label.startswith("GitHub Integration")
|
||||
and label.endswith("Platform: GitHubIntegration")
|
||||
if label == "Create repository (create_repository)"
|
||||
)
|
||||
return [github], None
|
||||
return [create_repository]
|
||||
|
||||
monkeypatch.setattr(json_crew, "pick_many", pick_many)
|
||||
monkeypatch.setattr(
|
||||
@@ -655,8 +684,56 @@ def test_json_wizard_platform_tool_selection_stays_in_agent_tools(monkeypatch):
|
||||
agent = json_crew._wizard_agent(agent_num=1, existing_names=[])
|
||||
|
||||
assert agent is not None
|
||||
assert agent["tools"] == ["platform:github"]
|
||||
assert '"tools": ["platform:github"]' in json_crew._agent_to_jsonc(agent)
|
||||
assert agent["tools"] == ["platform:github/create_repository"]
|
||||
assert (
|
||||
'"tools": ["platform:github/create_repository"]'
|
||||
in json_crew._agent_to_jsonc(agent)
|
||||
)
|
||||
|
||||
|
||||
def test_platform_applications_expand_in_the_main_tool_picker(monkeypatch):
|
||||
picker_calls: list[tuple[str, list[str], dict[str, object]]] = []
|
||||
|
||||
def pick_many(title: str, labels: list[str], **kwargs):
|
||||
picker_calls.append((title, labels, kwargs))
|
||||
if len(picker_calls) == 1:
|
||||
platform = next(
|
||||
index
|
||||
for index, label in enumerate(labels)
|
||||
if "CrewAI Platform" in label
|
||||
)
|
||||
return [], platform
|
||||
if len(picker_calls) == 2:
|
||||
group = next(
|
||||
index
|
||||
for index, label in enumerate(labels)
|
||||
if "Engineering, data & infrastructure" in label
|
||||
)
|
||||
return [], group
|
||||
github = next(
|
||||
index
|
||||
for index, label in enumerate(labels)
|
||||
if label.strip().startswith("GitHub Integration")
|
||||
)
|
||||
return [github], None
|
||||
|
||||
monkeypatch.setattr(json_crew, "pick_many", pick_many)
|
||||
|
||||
assert json_crew._select_tools() == ["platform:github"]
|
||||
labels = picker_calls[1][1]
|
||||
assert labels[0] == "── Common tools ──"
|
||||
assert any(
|
||||
label == " ▸ Google Workspace & Google Cloud (17 applications)"
|
||||
for label in labels
|
||||
)
|
||||
assert any(
|
||||
label == " ▸ Engineering, data & infrastructure (11 applications)"
|
||||
for label in labels
|
||||
)
|
||||
assert picker_calls[1][2]["initial_cursor"] == next(
|
||||
index for index, label in enumerate(labels) if "CrewAI Platform" in label
|
||||
)
|
||||
assert all(title != "Platform applications (space to toggle, enter to return):" for title, *_ in picker_calls)
|
||||
|
||||
|
||||
def test_json_wizard_platform_catalog_contains_every_supported_app():
|
||||
@@ -671,8 +748,39 @@ def test_json_wizard_platform_catalog_contains_every_supported_app():
|
||||
]
|
||||
|
||||
|
||||
def test_platform_validation_deduplicates_actions_by_application() -> None:
|
||||
apps = json_crew._platform_apps_from_agents(
|
||||
[
|
||||
{
|
||||
"tools": [
|
||||
"platform:github/create_repository",
|
||||
"platform:github/get_repository_content",
|
||||
"platform:gmail/send_email",
|
||||
]
|
||||
},
|
||||
{"tools": ["platform:github/list_repository_issues"]},
|
||||
]
|
||||
)
|
||||
|
||||
assert apps == ["github", "gmail"]
|
||||
|
||||
|
||||
def test_platform_token_prompts_show_entered_values(monkeypatch) -> None:
|
||||
prompt_options: list[dict[str, object]] = []
|
||||
|
||||
def prompt(_label: str, **kwargs: object) -> str:
|
||||
prompt_options.append(kwargs)
|
||||
return "token"
|
||||
|
||||
monkeypatch.setattr(json_crew.click, "prompt", prompt)
|
||||
|
||||
assert json_crew._prompt_platform_token() == "token"
|
||||
assert json_crew._prompt_platform_revalidation_token() == "token"
|
||||
assert [options["hide_input"] for options in prompt_options] == [False, False]
|
||||
|
||||
|
||||
def test_platform_auth_suppresses_warnings_only_while_importing_tools(
|
||||
monkeypatch,
|
||||
monkeypatch, capsys
|
||||
):
|
||||
class FakeApplicationSelector:
|
||||
@classmethod
|
||||
@@ -694,6 +802,7 @@ def test_platform_auth_suppresses_warnings_only_while_importing_tools(
|
||||
return original_import(name, globals, locals, fromlist, level)
|
||||
|
||||
monkeypatch.setattr(builtins, "__import__", import_with_warning)
|
||||
monkeypatch.setenv("CREWAI_ENTERPRISE_ACTION_AUTH_TOKEN", "ignored-token")
|
||||
monkeypatch.setenv("CREWAI_PLATFORM_INTEGRATION_TOKEN", "test-token")
|
||||
|
||||
with warnings.catch_warnings(record=True) as caught_warnings:
|
||||
@@ -704,6 +813,12 @@ def test_platform_auth_suppresses_warnings_only_while_importing_tools(
|
||||
assert [str(warning.message) for warning in caught_warnings] == [
|
||||
"warning after import"
|
||||
]
|
||||
output = capsys.readouterr().out
|
||||
assert "Checking for CREWAI_PLATFORM_INTEGRATION_TOKEN" in output
|
||||
assert (
|
||||
"Platform Integration token found via "
|
||||
"CREWAI_PLATFORM_INTEGRATION_TOKEN environment variable" in output
|
||||
)
|
||||
|
||||
|
||||
def test_platform_validation_checks_apps_concurrently():
|
||||
@@ -749,7 +864,7 @@ def test_platform_validation_announces_concurrent_apps_together(capsys):
|
||||
output = capsys.readouterr().out
|
||||
assert (failed_apps, token_invalid) == ([], False)
|
||||
assert "Checking GitHub, Gmail, Google Calendar integrations together on AMP" in output
|
||||
assert "Checking CrewAI Platform Integration Token and GitHub" not in output
|
||||
assert "Checking CrewAI Platform Integration token and GitHub" not in output
|
||||
|
||||
|
||||
def test_platform_validation_falls_back_to_sequential_checks(monkeypatch, capsys):
|
||||
@@ -779,8 +894,8 @@ def test_platform_validation_falls_back_to_sequential_checks(monkeypatch, capsys
|
||||
assert (failed_apps, token_invalid) == ([], False)
|
||||
assert checked_apps == ["github", "gmail"]
|
||||
output = capsys.readouterr().out
|
||||
assert "Checking CrewAI Platform Integration Token and GitHub integration" in output
|
||||
assert "Checking CrewAI Platform Integration Token and Gmail integration" in output
|
||||
assert "Checking CrewAI Platform Integration token and GitHub integration" in output
|
||||
assert "Checking CrewAI Platform Integration token and Gmail integration" in output
|
||||
|
||||
|
||||
def test_multi_picker_skips_separator_on_initial_cursor(monkeypatch):
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import os
|
||||
from io import StringIO
|
||||
|
||||
from crewai_cli import tui_picker
|
||||
|
||||
|
||||
def test_visible_row_range_keeps_short_lists_fully_visible(monkeypatch) -> None:
|
||||
monkeypatch.setattr(
|
||||
tui_picker.shutil,
|
||||
"get_terminal_size",
|
||||
lambda fallback: os.terminal_size((80, 24)),
|
||||
)
|
||||
|
||||
assert tui_picker._visible_row_range(total=8, cursor=3) == (0, 8)
|
||||
|
||||
|
||||
def test_visible_row_range_scrolls_long_lists_around_cursor(monkeypatch) -> None:
|
||||
monkeypatch.setattr(
|
||||
tui_picker.shutil,
|
||||
"get_terminal_size",
|
||||
lambda fallback: os.terminal_size((80, 12)),
|
||||
)
|
||||
|
||||
assert tui_picker._visible_row_range(total=125, cursor=0) == (0, 7)
|
||||
assert tui_picker._visible_row_range(total=125, cursor=62) == (59, 66)
|
||||
assert tui_picker._visible_row_range(total=125, cursor=124) == (118, 125)
|
||||
|
||||
|
||||
def test_draw_multi_renders_only_the_visible_window(monkeypatch) -> None:
|
||||
monkeypatch.setattr(
|
||||
tui_picker.shutil,
|
||||
"get_terminal_size",
|
||||
lambda fallback: os.terminal_size((80, 12)),
|
||||
)
|
||||
output = StringIO()
|
||||
monkeypatch.setattr(tui_picker.sys, "stdout", output)
|
||||
|
||||
line_count = tui_picker._draw_multi(
|
||||
[f"Tool {index}" for index in range(125)],
|
||||
cursor=62,
|
||||
selected=set(),
|
||||
)
|
||||
|
||||
rendered = output.getvalue()
|
||||
assert line_count == 8
|
||||
assert "showing 60-66 of 125" in rendered
|
||||
assert "Tool 59" in rendered
|
||||
assert "Tool 65" in rendered
|
||||
assert "Tool 0" not in rendered
|
||||
assert "Tool 124" not in rendered
|
||||
@@ -1,25 +1,313 @@
|
||||
"""CrewAI Platform application catalog."""
|
||||
|
||||
from typing import Final, Literal, get_args
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Final, TypeAlias
|
||||
|
||||
from crewai_core.platform_catalog import PLATFORM_CATALOG
|
||||
|
||||
|
||||
PlatformApp = Literal[
|
||||
"asana",
|
||||
"box",
|
||||
"clickup",
|
||||
"github",
|
||||
"gmail",
|
||||
"google_calendar",
|
||||
"google_sheets",
|
||||
"hubspot",
|
||||
"jira",
|
||||
"linear",
|
||||
"notion",
|
||||
"salesforce",
|
||||
"shopify",
|
||||
"slack",
|
||||
"stripe",
|
||||
"zendesk",
|
||||
]
|
||||
PlatformApp: TypeAlias = str
|
||||
|
||||
PLATFORM_APPS: Final[tuple[str, ...]] = (*get_args(PlatformApp),)
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PlatformToolDefinition:
|
||||
"""Describe one selectable CrewAI Platform action."""
|
||||
|
||||
slug: str
|
||||
display_name: str
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PlatformApplicationDefinition:
|
||||
"""Describe one CrewAI Platform application and its available actions."""
|
||||
|
||||
slug: PlatformApp
|
||||
display_name: str
|
||||
tools: tuple[PlatformToolDefinition, ...]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PlatformAppCategory:
|
||||
"""Describe a presentation group of CrewAI Platform applications."""
|
||||
|
||||
name: str
|
||||
apps: tuple[PlatformApp, ...]
|
||||
|
||||
|
||||
def _load_platform_catalog(
|
||||
catalog: dict[str, Any],
|
||||
) -> tuple[PlatformApplicationDefinition, ...]:
|
||||
"""Load and validate the generated Clipper application catalog."""
|
||||
applications = catalog.get("applications")
|
||||
if not isinstance(applications, list):
|
||||
raise ValueError("Platform catalog must contain an 'applications' list.")
|
||||
|
||||
applications_by_slug: set[str] = set()
|
||||
parsed_applications: list[PlatformApplicationDefinition] = []
|
||||
for application in applications:
|
||||
if not isinstance(application, dict):
|
||||
raise ValueError("Every platform catalog application must be an object.")
|
||||
slug = application.get("slug")
|
||||
display_name = application.get("display_name")
|
||||
if not isinstance(slug, str) or not slug:
|
||||
raise ValueError("Every platform catalog application must have a slug.")
|
||||
if not isinstance(display_name, str) or not display_name:
|
||||
raise ValueError(
|
||||
f"Platform catalog application '{slug}' must have a display name."
|
||||
)
|
||||
if slug in applications_by_slug:
|
||||
raise ValueError(f"Platform catalog contains duplicate slug '{slug}'.")
|
||||
tools = application.get("tools", [])
|
||||
if not isinstance(tools, list):
|
||||
raise ValueError(
|
||||
f"Platform catalog application '{slug}' must have a tools list."
|
||||
)
|
||||
parsed_tools: list[PlatformToolDefinition] = []
|
||||
for tool in tools:
|
||||
if not isinstance(tool, dict):
|
||||
raise ValueError(
|
||||
f"Platform catalog application '{slug}' contains an invalid tool."
|
||||
)
|
||||
tool_slug = tool.get("slug")
|
||||
tool_display_name = tool.get("display_name")
|
||||
if not isinstance(tool_slug, str) or not tool_slug:
|
||||
raise ValueError(
|
||||
f"Platform catalog application '{slug}' contains a tool without a slug."
|
||||
)
|
||||
if not isinstance(tool_display_name, str) or not tool_display_name:
|
||||
raise ValueError(
|
||||
f"Platform catalog tool '{slug}/{tool_slug}' must have a display name."
|
||||
)
|
||||
parsed_tools.append(
|
||||
PlatformToolDefinition(
|
||||
slug=tool_slug,
|
||||
display_name=tool_display_name,
|
||||
)
|
||||
)
|
||||
applications_by_slug.add(slug)
|
||||
parsed_applications.append(
|
||||
PlatformApplicationDefinition(
|
||||
slug=slug,
|
||||
display_name=display_name,
|
||||
tools=tuple(parsed_tools),
|
||||
)
|
||||
)
|
||||
|
||||
return tuple(parsed_applications)
|
||||
|
||||
|
||||
PLATFORM_APPLICATION_CATALOG: Final[tuple[PlatformApplicationDefinition, ...]] = (
|
||||
_load_platform_catalog(PLATFORM_CATALOG)
|
||||
)
|
||||
PLATFORM_APPS: Final[tuple[str, ...]] = tuple(
|
||||
application.slug for application in PLATFORM_APPLICATION_CATALOG
|
||||
)
|
||||
PLATFORM_APP_DISPLAY_NAMES: Final[dict[str, str]] = {
|
||||
application.slug: application.display_name
|
||||
for application in PLATFORM_APPLICATION_CATALOG
|
||||
}
|
||||
PLATFORM_APP_TOOL_COUNTS: Final[dict[str, int]] = {
|
||||
application.slug: len(application.tools)
|
||||
for application in PLATFORM_APPLICATION_CATALOG
|
||||
}
|
||||
PLATFORM_APP_TOOLS: Final[dict[str, tuple[PlatformToolDefinition, ...]]] = {
|
||||
application.slug: application.tools for application in PLATFORM_APPLICATION_CATALOG
|
||||
}
|
||||
|
||||
|
||||
def _platform_app_categories() -> tuple[PlatformAppCategory, ...]:
|
||||
"""Return the curated application groups used by integration pickers."""
|
||||
return (
|
||||
PlatformAppCategory(
|
||||
"Google Workspace & Google Cloud",
|
||||
(
|
||||
"gmail",
|
||||
"google_calendar",
|
||||
"google_docs",
|
||||
"google_drive",
|
||||
"google_sheets",
|
||||
"google_slides",
|
||||
"google_contacts",
|
||||
"google_classroom",
|
||||
"googlemeet",
|
||||
"googlephotos",
|
||||
"googletasks",
|
||||
"googlesuper",
|
||||
"google_maps",
|
||||
"google_search_console",
|
||||
"google_analytics",
|
||||
"googleads",
|
||||
"googlebigquery",
|
||||
),
|
||||
),
|
||||
PlatformAppCategory(
|
||||
"Microsoft 365",
|
||||
(
|
||||
"microsoft_excel",
|
||||
"microsoft_onedrive",
|
||||
"microsoft_outlook",
|
||||
"microsoft_sharepoint",
|
||||
"microsoft_teams",
|
||||
"microsoft_word",
|
||||
),
|
||||
),
|
||||
PlatformAppCategory(
|
||||
"Project & collaboration",
|
||||
(
|
||||
"airtable",
|
||||
"asana",
|
||||
"basecamp",
|
||||
"clickup",
|
||||
"confluence",
|
||||
"dart",
|
||||
"jira",
|
||||
"linear",
|
||||
"monday",
|
||||
"notion",
|
||||
"productboard",
|
||||
"todoist",
|
||||
"trello",
|
||||
"ticktick",
|
||||
"wrike",
|
||||
),
|
||||
),
|
||||
PlatformAppCategory(
|
||||
"Design & content",
|
||||
("canva", "contentful", "figma", "miro", "mural", "zeplin"),
|
||||
),
|
||||
PlatformAppCategory(
|
||||
"Communication, meetings & scheduling",
|
||||
(
|
||||
"cal",
|
||||
"calendly",
|
||||
"dialpad",
|
||||
"discord",
|
||||
"discordbot",
|
||||
"fathom",
|
||||
"gong",
|
||||
"granola_mcp",
|
||||
"slack",
|
||||
"slackbot",
|
||||
"whatsapp",
|
||||
"zoom",
|
||||
"zoho_mail",
|
||||
),
|
||||
),
|
||||
PlatformAppCategory(
|
||||
"CRM, sales & customer support",
|
||||
(
|
||||
"attio",
|
||||
"dynamics365",
|
||||
"gorgias",
|
||||
"hubspot",
|
||||
"intercom",
|
||||
"pylon_mcp",
|
||||
"salesforce",
|
||||
"zendesk",
|
||||
"zoho",
|
||||
"zoho_bigin",
|
||||
"zoho_desk",
|
||||
),
|
||||
),
|
||||
PlatformAppCategory(
|
||||
"Marketing, social & audience",
|
||||
(
|
||||
"dub",
|
||||
"eventbrite",
|
||||
"facebook",
|
||||
"instagram",
|
||||
"kit",
|
||||
"linkedin",
|
||||
"mailchimp",
|
||||
"omnisend",
|
||||
"pinterest",
|
||||
"pinterest_ads",
|
||||
"reddit",
|
||||
"reddit_ads",
|
||||
"tiktok_ads",
|
||||
"toneden",
|
||||
"twitch",
|
||||
"typeform",
|
||||
"youtube",
|
||||
),
|
||||
),
|
||||
PlatformAppCategory(
|
||||
"Finance & commerce",
|
||||
(
|
||||
"freshbooks",
|
||||
"gumroad",
|
||||
"mercury_mcp",
|
||||
"moneybird",
|
||||
"quickbooks",
|
||||
"shippo",
|
||||
"shopify",
|
||||
"square",
|
||||
"splitwise",
|
||||
"stripe",
|
||||
"ynab",
|
||||
"zoho_books",
|
||||
),
|
||||
),
|
||||
PlatformAppCategory(
|
||||
"Business operations & HR",
|
||||
("apaleo", "greenhouse", "harvest", "sap_s4hana", "servicem8", "timely"),
|
||||
),
|
||||
PlatformAppCategory(
|
||||
"Engineering, data & infrastructure",
|
||||
(
|
||||
"bitbucket",
|
||||
"daytona",
|
||||
"digital_ocean",
|
||||
"github",
|
||||
"gitlab",
|
||||
"hugging_face",
|
||||
"pagerduty",
|
||||
"prisma",
|
||||
"sentry",
|
||||
"supabase",
|
||||
"wakatime",
|
||||
),
|
||||
),
|
||||
PlatformAppCategory(
|
||||
"Files, documents & knowledge",
|
||||
("boldsign", "box", "dropbox", "linkhut", "notebook_lm", "roam"),
|
||||
),
|
||||
PlatformAppCategory(
|
||||
"Personal, utilities & reference",
|
||||
("exist", "pushbullet", "stack_exchange", "ticketmaster", "yandex"),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def _validate_platform_app_categories(
|
||||
categories: tuple[PlatformAppCategory, ...], apps: tuple[PlatformApp, ...]
|
||||
) -> tuple[PlatformAppCategory, ...]:
|
||||
"""Ensure every catalog application belongs to exactly one category."""
|
||||
categorized_apps = [app for category in categories for app in category.apps]
|
||||
duplicates = {app for app in categorized_apps if categorized_apps.count(app) > 1}
|
||||
if duplicates:
|
||||
raise ValueError(
|
||||
"Platform app categories contain duplicate applications: "
|
||||
f"{', '.join(sorted(duplicates))}."
|
||||
)
|
||||
|
||||
categorized_app_set = set(categorized_apps)
|
||||
app_set = set(apps)
|
||||
unknown = categorized_app_set - app_set
|
||||
if unknown:
|
||||
raise ValueError(
|
||||
"Platform app categories contain unknown applications: "
|
||||
f"{', '.join(sorted(unknown))}."
|
||||
)
|
||||
missing = app_set - categorized_app_set
|
||||
if missing:
|
||||
raise ValueError(
|
||||
"Platform app categories are missing applications: "
|
||||
f"{', '.join(sorted(missing))}."
|
||||
)
|
||||
return categories
|
||||
|
||||
|
||||
PLATFORM_APP_CATEGORIES: Final[tuple[PlatformAppCategory, ...]] = (
|
||||
_validate_platform_app_categories(_platform_app_categories(), PLATFORM_APPS)
|
||||
)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -4,7 +4,15 @@ This module provides tools for integrating with various platform applications
|
||||
through the CrewAI platform API.
|
||||
"""
|
||||
|
||||
from crewai_core.platform_apps import PLATFORM_APPS
|
||||
from crewai_core.platform_apps import (
|
||||
PLATFORM_APPLICATION_CATALOG,
|
||||
PLATFORM_APPS,
|
||||
PLATFORM_APP_CATEGORIES,
|
||||
PLATFORM_APP_DISPLAY_NAMES,
|
||||
PLATFORM_APP_TOOLS,
|
||||
PLATFORM_APP_TOOL_COUNTS,
|
||||
)
|
||||
from crewai_core.platform_catalog import PLATFORM_CATALOG
|
||||
|
||||
from crewai_tools.tools.crewai_platform_tools.crewai_platform_action_tool import (
|
||||
CrewAIPlatformActionTool,
|
||||
@@ -15,7 +23,13 @@ from crewai_tools.tools.crewai_platform_tools.crewai_platform_tools import (
|
||||
|
||||
|
||||
__all__ = [
|
||||
"PLATFORM_APPLICATION_CATALOG",
|
||||
"PLATFORM_APPS",
|
||||
"PLATFORM_APP_CATEGORIES",
|
||||
"PLATFORM_APP_DISPLAY_NAMES",
|
||||
"PLATFORM_APP_TOOLS",
|
||||
"PLATFORM_APP_TOOL_COUNTS",
|
||||
"PLATFORM_CATALOG",
|
||||
"CrewAIPlatformActionTool",
|
||||
"CrewaiPlatformTools",
|
||||
]
|
||||
|
||||
@@ -2,10 +2,11 @@ import os
|
||||
|
||||
|
||||
def get_platform_integration_token() -> str:
|
||||
"""Get the platform integration token from the environment."""
|
||||
token = os.getenv("CREWAI_PLATFORM_INTEGRATION_TOKEN") or ""
|
||||
"""Get the Platform integration token from the environment."""
|
||||
token = os.getenv("CREWAI_PLATFORM_INTEGRATION_TOKEN", "")
|
||||
if not token:
|
||||
raise ValueError(
|
||||
"No platform integration token found, please set the CREWAI_PLATFORM_INTEGRATION_TOKEN environment variable"
|
||||
"No Platform integration token found, please set the "
|
||||
"CREWAI_PLATFORM_INTEGRATION_TOKEN environment variable"
|
||||
)
|
||||
return token # TODO: Use context manager to get token
|
||||
|
||||
@@ -1,22 +1,37 @@
|
||||
from crewai_core.platform_apps import PLATFORM_APPS
|
||||
from crewai_core.platform_apps import (
|
||||
PLATFORM_CATALOG,
|
||||
PLATFORM_APP_CATEGORIES,
|
||||
PLATFORM_APP_DISPLAY_NAMES,
|
||||
PLATFORM_APP_TOOL_COUNTS,
|
||||
PLATFORM_APPLICATION_CATALOG,
|
||||
PLATFORM_APPS,
|
||||
)
|
||||
|
||||
|
||||
def test_platform_apps_contains_supported_application_catalog() -> None:
|
||||
assert PLATFORM_APPS == (
|
||||
"asana",
|
||||
"box",
|
||||
"clickup",
|
||||
"github",
|
||||
"gmail",
|
||||
"google_calendar",
|
||||
"google_sheets",
|
||||
"hubspot",
|
||||
"jira",
|
||||
"linear",
|
||||
"notion",
|
||||
"salesforce",
|
||||
"shopify",
|
||||
"slack",
|
||||
"stripe",
|
||||
"zendesk",
|
||||
assert isinstance(PLATFORM_CATALOG["applications"], list)
|
||||
assert len(PLATFORM_APPS) == 125
|
||||
assert len(set(PLATFORM_APPS)) == len(PLATFORM_APPS)
|
||||
assert PLATFORM_APPS[0] == "airtable"
|
||||
assert PLATFORM_APPS[-1] == "sap_s4hana"
|
||||
assert set(PLATFORM_APP_DISPLAY_NAMES) == set(PLATFORM_APPS)
|
||||
assert set(PLATFORM_APP_TOOL_COUNTS) == set(PLATFORM_APPS)
|
||||
assert PLATFORM_APP_DISPLAY_NAMES["github"] == "GitHub"
|
||||
assert PLATFORM_APP_TOOL_COUNTS["github"] == 877
|
||||
assert tuple(application.slug for application in PLATFORM_APPLICATION_CATALOG) == (
|
||||
PLATFORM_APPS
|
||||
)
|
||||
assert next(
|
||||
application for application in PLATFORM_APPLICATION_CATALOG if application.slug == "github"
|
||||
).display_name == "GitHub"
|
||||
|
||||
|
||||
def test_platform_app_categories_cover_the_catalog_once() -> None:
|
||||
categorized_apps = [
|
||||
app for category in PLATFORM_APP_CATEGORIES for app in category.apps
|
||||
]
|
||||
|
||||
assert len(PLATFORM_APP_CATEGORIES) == 12
|
||||
assert set(categorized_apps) == set(PLATFORM_APPS)
|
||||
assert len(categorized_apps) == len(set(categorized_apps))
|
||||
assert PLATFORM_APP_CATEGORIES[0].name == "Google Workspace & Google Cloud"
|
||||
|
||||
@@ -164,7 +164,7 @@ class TestCrewaiPlatformTools(unittest.TestCase):
|
||||
with patch.dict("os.environ", {}, clear=True):
|
||||
with self.assertRaises(ValueError) as context:
|
||||
CrewaiPlatformTools(apps=["github"])
|
||||
assert "No platform integration token found" in str(context.exception)
|
||||
assert "CREWAI_PLATFORM_INTEGRATION_TOKEN" in str(context.exception)
|
||||
|
||||
@patch.dict("os.environ", {"CREWAI_PLATFORM_INTEGRATION_TOKEN": "test_token"})
|
||||
@patch(
|
||||
|
||||
@@ -350,7 +350,10 @@ def test_clipper_client_executes_without_deployment_instance_uuid(
|
||||
|
||||
@patch.dict(
|
||||
"os.environ",
|
||||
{"CREWAI_DEPLOYMENT_INSTANCE_UUID": "deployment-instance-id"},
|
||||
{
|
||||
"CREWAI_DEPLOYMENT_INSTANCE_UUID": "deployment-instance-id",
|
||||
"CREWAI_ENTERPRISE_ACTION_AUTH_TOKEN": "ignored-token",
|
||||
},
|
||||
clear=True,
|
||||
)
|
||||
@patch(
|
||||
|
||||
Reference in New Issue
Block a user