mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-09-20 10:03:37 +00:00
Merge branch 'main' into my-crewAI
This commit is contained in:
88
.github/workflows/first-contributor-welcome.yml
vendored
Normal file
88
.github/workflows/first-contributor-welcome.yml
vendored
Normal file
@@ -0,0 +1,88 @@
|
||||
name: Welcome First-Time Contributors
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [closed]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
welcome:
|
||||
# Match the first-time contributor definition used by ftc-require-issue.
|
||||
if: >
|
||||
github.event.pull_request.merged == true &&
|
||||
github.event.pull_request.user.type != 'Bot' &&
|
||||
!contains(fromJSON('["MEMBER","OWNER","COLLABORATOR","CONTRIBUTOR"]'),
|
||||
github.event.pull_request.author_association)
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Thank first-time contributors
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
with:
|
||||
script: |
|
||||
const pullRequest = context.payload.pull_request;
|
||||
const owner = context.repo.owner;
|
||||
const repo = context.repo.repo;
|
||||
const marker = "<!-- crewai-first-merged-pr-welcome -->";
|
||||
|
||||
const existingComments = await github.paginate(
|
||||
github.rest.issues.listComments,
|
||||
{
|
||||
owner,
|
||||
repo,
|
||||
issue_number: pullRequest.number,
|
||||
per_page: 100,
|
||||
},
|
||||
);
|
||||
if (existingComments.some((comment) => comment.body?.includes(marker))) {
|
||||
core.info("Welcome comment already exists; skipping duplicate.");
|
||||
return;
|
||||
}
|
||||
|
||||
const prUrl = pullRequest.html_url;
|
||||
const shareText = [
|
||||
"I just made my first contribution to @crewAIInc!",
|
||||
"",
|
||||
"Excited to help build the future of AI agents with CrewAI.",
|
||||
"",
|
||||
prUrl,
|
||||
"",
|
||||
"#OpenSource #AI #CrewAI",
|
||||
].join("\n");
|
||||
const xShareUrl = `https://x.com/intent/post?text=${encodeURIComponent(shareText)}`;
|
||||
const linkedInShareUrl = `https://www.linkedin.com/sharing/share-offsite/?url=${encodeURIComponent(prUrl)}`;
|
||||
|
||||
const body = [
|
||||
marker,
|
||||
"",
|
||||
`## Thanks for your first contribution to CrewAI, @${pullRequest.user.login}!`,
|
||||
"",
|
||||
"We really appreciate the time and care you put into this PR. Your work is now part of CrewAI — welcome to the contributor community.",
|
||||
"",
|
||||
"Want to share your contribution? Totally optional:",
|
||||
"",
|
||||
`[Share on X](${xShareUrl}) · [Share the PR on LinkedIn](${linkedInShareUrl})`,
|
||||
"",
|
||||
"If the links don't work, feel free to copy and personalize this:",
|
||||
"",
|
||||
"```text",
|
||||
"I just made my first contribution to @crewAIInc!",
|
||||
"",
|
||||
"Excited to help build the future of AI agents with CrewAI.",
|
||||
"",
|
||||
prUrl,
|
||||
"",
|
||||
"#OpenSource #AI #CrewAI",
|
||||
"```",
|
||||
"",
|
||||
"If you share, we'd love to see it — tag **@crewAIInc** on X and **CrewAI** on LinkedIn.",
|
||||
].join("\n");
|
||||
|
||||
await github.rest.issues.createComment({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: pullRequest.number,
|
||||
body,
|
||||
});
|
||||
@@ -3,6 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import sys
|
||||
@@ -16,6 +17,7 @@ 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 (
|
||||
enable_prompt_line_editing,
|
||||
@@ -104,6 +106,7 @@ _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),
|
||||
(
|
||||
"Search & Research",
|
||||
[
|
||||
@@ -305,6 +308,9 @@ 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"
|
||||
return f"{description:<48s} {name}"
|
||||
|
||||
|
||||
@@ -352,6 +358,7 @@ def _select_tools() -> list[str]:
|
||||
selected: set[str] = set()
|
||||
expanded: str | None = None
|
||||
focus_category: str | None = None
|
||||
first_render = True
|
||||
|
||||
while True:
|
||||
labels: list[str] = []
|
||||
@@ -388,13 +395,14 @@ def _select_tools() -> list[str]:
|
||||
labels.append(_tool_label(name, desc))
|
||||
|
||||
indices, action = pick_many(
|
||||
"Tools (space to toggle, enter to confirm):",
|
||||
"Tools (space to toggle, enter to confirm):" if first_render else "",
|
||||
labels,
|
||||
action_indices=action_indices,
|
||||
separator_indices=separator_indices,
|
||||
preselected=preselected,
|
||||
initial_cursor=initial_cursor,
|
||||
)
|
||||
first_render = False
|
||||
|
||||
# Carry over toggles made on this screen; tools not visible in this
|
||||
# render keep their previous state.
|
||||
@@ -613,6 +621,10 @@ def _wizard_agents_and_tasks(
|
||||
"inputs": {},
|
||||
}
|
||||
|
||||
# Platform authentication belongs to the final wizard step, after the
|
||||
# user has finished configuring agents, tasks, and crew settings.
|
||||
_setup_platform_auth(agents)
|
||||
|
||||
return agents, tasks, crew_settings
|
||||
|
||||
|
||||
@@ -865,6 +877,181 @@ def _setup_env(folder_path: Path, llm_model: str) -> None:
|
||||
click.secho(" API keys and model saved to .env file", fg="green")
|
||||
|
||||
|
||||
def _platform_apps_from_agents(agents: list[dict[str, Any]]) -> list[str]:
|
||||
"""Return unique platform applications 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:")
|
||||
if app and app not in apps:
|
||||
apps.append(app)
|
||||
return apps
|
||||
|
||||
|
||||
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")
|
||||
)
|
||||
|
||||
|
||||
def _prompt_platform_token() -> str:
|
||||
"""Explain how to obtain and securely prompt for an AMP integration token."""
|
||||
click.secho(
|
||||
" To use CrewAI Platform tools, you need a CrewAI Platform Integration Token.",
|
||||
fg="yellow",
|
||||
)
|
||||
click.secho(
|
||||
" Get your token from CrewAI AMP: https://app.crewai.com "
|
||||
"→ Settings → Integration Tokens.",
|
||||
fg="cyan",
|
||||
)
|
||||
return str(
|
||||
click.prompt(
|
||||
click.style(" CREWAI_PLATFORM_INTEGRATION_TOKEN", fg="cyan"),
|
||||
hide_input=True,
|
||||
prompt_suffix=click.style(" > ", fg="bright_white"),
|
||||
)
|
||||
).strip()
|
||||
|
||||
|
||||
def _validate_platform_apps(
|
||||
apps: list[str], application_selector: Any, client_for_selector: Any
|
||||
) -> tuple[list[str], bool]:
|
||||
"""Check selected AMP applications and return failures and token validity."""
|
||||
failed: list[str] = []
|
||||
for app in apps:
|
||||
app_name = _platform_app_name(app)
|
||||
click.echo()
|
||||
click.secho(
|
||||
" Checking CrewAI Platform Integration Token and "
|
||||
f"{app_name} integration on AMP...",
|
||||
fg="cyan",
|
||||
)
|
||||
try:
|
||||
selector = application_selector.from_string(app)
|
||||
actions = client_for_selector(selector).get_actions([selector])
|
||||
except Exception as error:
|
||||
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",
|
||||
fg="red",
|
||||
)
|
||||
return failed, True
|
||||
click.secho(
|
||||
f" ✘ {app_name} integration could not be validated: {error}",
|
||||
fg="red",
|
||||
)
|
||||
failed.append(app)
|
||||
continue
|
||||
|
||||
if not actions:
|
||||
click.secho(
|
||||
f" ✘ {app_name} integration is not connected on CrewAI Platform",
|
||||
fg="red",
|
||||
)
|
||||
failed.append(app)
|
||||
else:
|
||||
click.secho(
|
||||
f" ✔ {app_name} integration is connected on CrewAI Platform",
|
||||
fg="green",
|
||||
)
|
||||
return failed, False
|
||||
|
||||
|
||||
def _show_platform_validation_guidance(
|
||||
failed_apps: list[str], token_invalid: bool
|
||||
) -> None:
|
||||
"""Tell the user what to fix before revalidating AMP integrations."""
|
||||
click.echo()
|
||||
if token_invalid:
|
||||
click.secho(
|
||||
" Check your CrewAI Platform Integration Token in AMP.",
|
||||
fg="yellow",
|
||||
)
|
||||
return
|
||||
|
||||
failed_app_names = [_platform_app_name(app) for app in failed_apps]
|
||||
click.secho(
|
||||
" 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.",
|
||||
fg="yellow",
|
||||
)
|
||||
|
||||
|
||||
def _prompt_platform_revalidation_token() -> str:
|
||||
"""Prompt for an optional replacement token before the next validation pass."""
|
||||
click.echo()
|
||||
return str(
|
||||
click.prompt(
|
||||
click.style(
|
||||
" Press Enter to revalidate, or enter a replacement token",
|
||||
fg="cyan",
|
||||
),
|
||||
default="",
|
||||
show_default=False,
|
||||
hide_input=True,
|
||||
prompt_suffix=click.style(" > ", fg="bright_white"),
|
||||
)
|
||||
).strip()
|
||||
|
||||
|
||||
def _setup_platform_auth(agents: list[dict[str, Any]]) -> str | None:
|
||||
"""Get and validate AMP authentication for selected platform applications."""
|
||||
apps = _platform_apps_from_agents(agents)
|
||||
if not apps:
|
||||
return None
|
||||
|
||||
click.echo()
|
||||
try:
|
||||
from crewai_tools.tools.crewai_platform_tools.integrations_client import (
|
||||
ApplicationSelector,
|
||||
client_for_selector,
|
||||
)
|
||||
except ImportError as error:
|
||||
raise click.ClickException(
|
||||
"Platform tools require the 'crewai-tools' package. "
|
||||
"Install it with `uv add crewai-tools` or "
|
||||
"`pip install 'crewai[tools]'`."
|
||||
) from error
|
||||
|
||||
token = os.environ.get("CREWAI_PLATFORM_INTEGRATION_TOKEN", "")
|
||||
while True:
|
||||
if not token:
|
||||
token = _prompt_platform_token()
|
||||
if not token:
|
||||
click.secho(
|
||||
" A CrewAI Platform Integration Token is required to validate "
|
||||
"the selected integrations.",
|
||||
fg="yellow",
|
||||
)
|
||||
continue
|
||||
|
||||
os.environ["CREWAI_PLATFORM_INTEGRATION_TOKEN"] = token
|
||||
failed_apps, token_invalid = _validate_platform_apps(
|
||||
apps, ApplicationSelector, client_for_selector
|
||||
)
|
||||
if not failed_apps and not token_invalid:
|
||||
_success("CrewAI Platform integration token set", bold=True)
|
||||
_success(
|
||||
"CrewAI Platform integrations connected: "
|
||||
f"{', '.join(_platform_app_name(app) for app in apps)}"
|
||||
)
|
||||
return token
|
||||
|
||||
_show_platform_validation_guidance(failed_apps, token_invalid)
|
||||
replacement_token = _prompt_platform_revalidation_token()
|
||||
if replacement_token:
|
||||
token = replacement_token
|
||||
os.environ["CREWAI_PLATFORM_INTEGRATION_TOKEN"] = token
|
||||
|
||||
|
||||
# ── Main ────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@@ -917,13 +1104,25 @@ def create_json_crew(
|
||||
default_llm=default_llm,
|
||||
)
|
||||
|
||||
# Create directories
|
||||
platform_token = (
|
||||
os.environ.get("CREWAI_PLATFORM_INTEGRATION_TOKEN")
|
||||
if _platform_apps_from_agents(agents) and not dmn_mode
|
||||
else None
|
||||
)
|
||||
|
||||
# Create directories only after platform authentication succeeds.
|
||||
folder_path.mkdir(parents=True)
|
||||
(folder_path / "agents").mkdir()
|
||||
(folder_path / "tools").mkdir()
|
||||
(folder_path / "skills").mkdir()
|
||||
(folder_path / "knowledge").mkdir()
|
||||
|
||||
if platform_token:
|
||||
os.environ["CREWAI_PLATFORM_INTEGRATION_TOKEN"] = platform_token
|
||||
env_vars = load_env_vars(folder_path)
|
||||
env_vars["CREWAI_PLATFORM_INTEGRATION_TOKEN"] = platform_token
|
||||
write_env_file(folder_path, env_vars)
|
||||
|
||||
for agent in agents:
|
||||
_write_jsonc(
|
||||
folder_path / "agents" / f"{agent['name']}.jsonc",
|
||||
|
||||
29
lib/cli/src/crewai_cli/platform_tools_catalog.py
Normal file
29
lib/cli/src/crewai_cli/platform_tools_catalog.py
Normal file
@@ -0,0 +1,29 @@
|
||||
"""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
|
||||
]
|
||||
@@ -388,8 +388,9 @@ def pick_many(
|
||||
Sorted list of selected indices, or ``(indices, action_index)`` when
|
||||
``action_indices`` is provided.
|
||||
"""
|
||||
click.echo()
|
||||
click.secho(f" {title}", fg="cyan")
|
||||
if title:
|
||||
click.echo()
|
||||
click.secho(f" {title}", fg="cyan")
|
||||
|
||||
if _is_interactive():
|
||||
try:
|
||||
|
||||
@@ -7,6 +7,7 @@ from unittest import mock
|
||||
import pytest
|
||||
import tomli
|
||||
from click.testing import CliRunner
|
||||
from crewai_core.platform_apps import PLATFORM_APPS
|
||||
from packaging.requirements import Requirement
|
||||
from packaging.version import Version
|
||||
import crewai_cli.create_json_crew as json_crew
|
||||
@@ -620,6 +621,52 @@ def test_json_wizard_tool_picker_lists_builtin_tools_across_categories(monkeypat
|
||||
}.isdisjoint(tool_names)
|
||||
|
||||
|
||||
def test_json_wizard_platform_tool_selection_stays_in_agent_tools(monkeypatch):
|
||||
picker_calls = 0
|
||||
|
||||
def pick_many(title: str, labels: list[str], **kwargs):
|
||||
nonlocal picker_calls
|
||||
picker_calls += 1
|
||||
if picker_calls == 1:
|
||||
platform_row = next(
|
||||
idx for idx, label in enumerate(labels) if "CrewAI Platform" in label
|
||||
)
|
||||
return [], platform_row
|
||||
|
||||
github = next(
|
||||
idx
|
||||
for idx, label in enumerate(labels)
|
||||
if label.startswith("GitHub Integration")
|
||||
and label.endswith("Platform: GitHubIntegration")
|
||||
)
|
||||
return [github], None
|
||||
|
||||
monkeypatch.setattr(json_crew, "pick_many", pick_many)
|
||||
monkeypatch.setattr(
|
||||
json_crew, "_prompt_text", lambda label, **kwargs: label.lower()
|
||||
)
|
||||
monkeypatch.setattr(json_crew, "_select_model", lambda: "openai/gpt-5.5")
|
||||
monkeypatch.setattr(json_crew, "_confirm", lambda *_args, **_kwargs: False)
|
||||
|
||||
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)
|
||||
|
||||
|
||||
def test_json_wizard_platform_catalog_contains_every_supported_app():
|
||||
platform_category = next(
|
||||
tools
|
||||
for category, tools in json_crew._TOOL_CATEGORIES
|
||||
if category == "CrewAI Platform"
|
||||
)
|
||||
|
||||
assert [name for name, _description in platform_category] == [
|
||||
f"platform:{app}" for app in PLATFORM_APPS
|
||||
]
|
||||
|
||||
|
||||
def test_multi_picker_skips_separator_on_initial_cursor(monkeypatch):
|
||||
cursors: list[int] = []
|
||||
|
||||
|
||||
25
lib/crewai-core/src/crewai_core/platform_apps.py
Normal file
25
lib/crewai-core/src/crewai_core/platform_apps.py
Normal file
@@ -0,0 +1,25 @@
|
||||
"""CrewAI Platform application catalog."""
|
||||
|
||||
from typing import Final, Literal, get_args
|
||||
|
||||
|
||||
PlatformApp = Literal[
|
||||
"asana",
|
||||
"box",
|
||||
"clickup",
|
||||
"github",
|
||||
"gmail",
|
||||
"google_calendar",
|
||||
"google_sheets",
|
||||
"hubspot",
|
||||
"jira",
|
||||
"linear",
|
||||
"notion",
|
||||
"salesforce",
|
||||
"shopify",
|
||||
"slack",
|
||||
"stripe",
|
||||
"zendesk",
|
||||
]
|
||||
|
||||
PLATFORM_APPS: Final[tuple[str, ...]] = (*get_args(PlatformApp),)
|
||||
@@ -4,6 +4,8 @@ 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_tools.tools.crewai_platform_tools.crewai_platform_action_tool import (
|
||||
CrewAIPlatformActionTool,
|
||||
)
|
||||
@@ -13,6 +15,7 @@ from crewai_tools.tools.crewai_platform_tools.crewai_platform_tools import (
|
||||
|
||||
|
||||
__all__ = [
|
||||
"PLATFORM_APPS",
|
||||
"CrewAIPlatformActionTool",
|
||||
"CrewaiPlatformTools",
|
||||
]
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import logging
|
||||
|
||||
from crewai.tools import BaseTool
|
||||
|
||||
from crewai_tools.tools.crewai_platform_tools.crewai_platform_action_tool import (
|
||||
@@ -11,9 +9,6 @@ from crewai_tools.tools.crewai_platform_tools.integrations_client import (
|
||||
)
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def CrewaiPlatformTools( # noqa: N802
|
||||
apps: list[str],
|
||||
) -> list[BaseTool]:
|
||||
@@ -28,17 +23,11 @@ def CrewaiPlatformTools( # noqa: N802
|
||||
selectors = [ApplicationSelector.from_string(app) for app in apps]
|
||||
tools: list[BaseTool] = []
|
||||
|
||||
try:
|
||||
for selector in selectors:
|
||||
client = client_for_selector(selector)
|
||||
tools.extend(
|
||||
CrewAIPlatformActionTool(tool_info, client=client)
|
||||
for tool_info in client.get_actions([selector])
|
||||
)
|
||||
except ValueError:
|
||||
raise
|
||||
except Exception as error:
|
||||
logger.error(f"Failed to fetch platform tools for apps {apps}: {error}")
|
||||
return []
|
||||
for selector in selectors:
|
||||
client = client_for_selector(selector)
|
||||
tools.extend(
|
||||
CrewAIPlatformActionTool(tool_info, client=client)
|
||||
for tool_info in client.get_actions([selector])
|
||||
)
|
||||
|
||||
return tools
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
from crewai_core.platform_apps import 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",
|
||||
)
|
||||
@@ -157,10 +157,8 @@ class TestCrewaiPlatformTools(unittest.TestCase):
|
||||
def test_crewai_platform_tools_api_error_handling(self, mock_get):
|
||||
mock_get.side_effect = Exception("API Error")
|
||||
|
||||
tools = CrewaiPlatformTools(apps=["github"])
|
||||
assert tools is not None
|
||||
assert isinstance(tools, list)
|
||||
assert len(tools) == 0
|
||||
with self.assertRaisesRegex(Exception, "API Error"):
|
||||
CrewaiPlatformTools(apps=["github"])
|
||||
|
||||
def test_crewai_platform_tools_no_token(self):
|
||||
with patch.dict("os.environ", {}, clear=True):
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -9,6 +9,7 @@ import re
|
||||
from typing import TYPE_CHECKING, Annotated, Any, Final, Literal
|
||||
import uuid
|
||||
|
||||
from crewai_core.platform_apps import PlatformApp
|
||||
from pydantic import (
|
||||
UUID4,
|
||||
BaseModel,
|
||||
@@ -180,25 +181,6 @@ _SLUG_RE: Final[re.Pattern[str]] = re.compile(
|
||||
)
|
||||
|
||||
|
||||
PlatformApp = Literal[
|
||||
"asana",
|
||||
"box",
|
||||
"clickup",
|
||||
"github",
|
||||
"gmail",
|
||||
"google_calendar",
|
||||
"google_sheets",
|
||||
"hubspot",
|
||||
"jira",
|
||||
"linear",
|
||||
"notion",
|
||||
"salesforce",
|
||||
"shopify",
|
||||
"slack",
|
||||
"stripe",
|
||||
"zendesk",
|
||||
]
|
||||
|
||||
PlatformAppOrAction = PlatformApp | str
|
||||
|
||||
|
||||
|
||||
@@ -1820,6 +1820,9 @@ def _resolve_tools(tool_defs: list[Any], project_root: Path | None = None) -> li
|
||||
)
|
||||
if not tool_def:
|
||||
continue
|
||||
if tool_def.startswith("platform:"):
|
||||
tools.extend(_resolve_platform_tools(tool_def.removeprefix("platform:")))
|
||||
continue
|
||||
if tool_def.startswith("custom:"):
|
||||
tools.append(_resolve_custom_tool(tool_def[7:], project_root=project_root))
|
||||
continue
|
||||
@@ -1847,6 +1850,30 @@ def _resolve_tools(tool_defs: list[Any], project_root: Path | None = None) -> li
|
||||
return tools
|
||||
|
||||
|
||||
def _resolve_platform_tools(selector: str) -> list[Any]:
|
||||
"""Materialize an AMP application selector into CrewAI tools."""
|
||||
if not selector:
|
||||
raise JSONProjectError(
|
||||
"Invalid platform tool reference 'platform:': expected "
|
||||
"'platform:<application>'"
|
||||
)
|
||||
|
||||
try:
|
||||
from crewai_tools import CrewaiPlatformTools
|
||||
except ImportError as e:
|
||||
raise JSONProjectError(
|
||||
"Platform tools require the 'crewai-tools' package. "
|
||||
"Install CrewAI with the tools extra."
|
||||
) from e
|
||||
|
||||
try:
|
||||
return CrewaiPlatformTools(apps=[selector])
|
||||
except Exception as e:
|
||||
raise JSONProjectError(
|
||||
f"Failed to initialize platform tool 'platform:{selector}': {e}"
|
||||
) from e
|
||||
|
||||
|
||||
def _instantiate_tool_import_ref(ref: str) -> Any:
|
||||
from crewai.tools import BaseTool
|
||||
|
||||
@@ -1990,19 +2017,25 @@ def _tool_definition_errors(
|
||||
f"got {type(tool_def).__name__}"
|
||||
)
|
||||
continue
|
||||
if not tool_def.startswith("custom:"):
|
||||
if tool_def.startswith("platform:"):
|
||||
if not tool_def.removeprefix("platform:"):
|
||||
errors.append(
|
||||
f"{source}: invalid platform tool reference 'platform:': "
|
||||
"expected 'platform:<application>'"
|
||||
)
|
||||
continue
|
||||
try:
|
||||
tool_file = _custom_tool_file(tool_def[7:], project_root)
|
||||
except JSONProjectError as exc:
|
||||
errors.append(f"{source}: {exc}")
|
||||
continue
|
||||
if not tool_file.exists():
|
||||
errors.append(
|
||||
f"{source}: custom tool '{tool_def}' not found: expected "
|
||||
f"{tool_file}. Create the file with a BaseTool subclass, or "
|
||||
f"remove the tool from your crew JSON."
|
||||
)
|
||||
if tool_def.startswith("custom:"):
|
||||
try:
|
||||
tool_file = _custom_tool_file(tool_def[7:], project_root)
|
||||
except JSONProjectError as exc:
|
||||
errors.append(f"{source}: {exc}")
|
||||
continue
|
||||
if not tool_file.exists():
|
||||
errors.append(
|
||||
f"{source}: custom tool '{tool_def}' not found: expected "
|
||||
f"{tool_file}. Create the file with a BaseTool subclass, or "
|
||||
f"remove the tool from your crew JSON."
|
||||
)
|
||||
return errors
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import os
|
||||
import sys
|
||||
import types
|
||||
from unittest.mock import patch, MagicMock
|
||||
import pytest
|
||||
|
||||
@@ -8,6 +6,7 @@ from crewai.llm import LLM
|
||||
from crewai.crew import Crew
|
||||
from crewai.agent import Agent
|
||||
from crewai.task import Task
|
||||
from crewai.llms.providers.bedrock import completion as bedrock_completion
|
||||
|
||||
|
||||
def _create_bedrock_mocks():
|
||||
@@ -134,25 +133,6 @@ def test_bedrock_completion_is_used_when_bedrock_provider():
|
||||
assert llm.model == "anthropic.claude-3-5-sonnet-20241022-v2:0"
|
||||
|
||||
|
||||
def test_bedrock_completion_module_is_imported(monkeypatch):
|
||||
"""
|
||||
Test that the completion module is properly imported when using Bedrock provider
|
||||
"""
|
||||
module_name = "crewai.llms.providers.bedrock.completion"
|
||||
|
||||
# Restore the original module after this test so collected class references
|
||||
# still match the provider returned by LLM in subsequent tests.
|
||||
monkeypatch.delitem(sys.modules, module_name, raising=False)
|
||||
|
||||
LLM(model="bedrock/anthropic.claude-3-5-sonnet-20241022-v2:0")
|
||||
|
||||
assert module_name in sys.modules
|
||||
completion_mod = sys.modules[module_name]
|
||||
assert isinstance(completion_mod, types.ModuleType)
|
||||
|
||||
assert hasattr(completion_mod, 'BedrockCompletion')
|
||||
|
||||
|
||||
def test_native_bedrock_raises_error_when_initialization_fails():
|
||||
"""
|
||||
Test that LLM raises ImportError when native Bedrock completion fails.
|
||||
@@ -602,8 +582,10 @@ def test_bedrock_tool_conversion():
|
||||
assert "inputSchema" in bedrock_tools[0]["toolSpec"]
|
||||
|
||||
|
||||
def test_bedrock_environment_variable_credentials():
|
||||
def test_bedrock_environment_variable_credentials(monkeypatch):
|
||||
"""Pass AWS credentials and region from the environment to boto3."""
|
||||
monkeypatch.delenv("AWS_SESSION_TOKEN", raising=False)
|
||||
|
||||
with (
|
||||
patch.dict(
|
||||
os.environ,
|
||||
@@ -614,13 +596,12 @@ def test_bedrock_environment_variable_credentials():
|
||||
},
|
||||
clear=False,
|
||||
),
|
||||
patch(
|
||||
"crewai.llms.providers.bedrock.completion.Session"
|
||||
) as mock_session_class,
|
||||
patch.object(bedrock_completion, "Session") as mock_session_class,
|
||||
):
|
||||
mock_session_class.return_value.client.return_value = MagicMock()
|
||||
LLM(model="bedrock/anthropic.claude-3-5-sonnet-20241022-v2:0")
|
||||
llm = LLM(model="bedrock/anthropic.claude-3-5-sonnet-20241022-v2:0")
|
||||
|
||||
assert type(llm) is bedrock_completion.BedrockCompletion
|
||||
mock_session_class.assert_called_once_with(
|
||||
aws_access_key_id="test-access-key-123",
|
||||
aws_secret_access_key="test-secret-key-456",
|
||||
|
||||
@@ -403,6 +403,52 @@ class TestLoadAgentFromDefinition:
|
||||
|
||||
|
||||
class TestResolveTools:
|
||||
def test_platform_tool_refs_materialize_multiple_application_tools(
|
||||
self, monkeypatch
|
||||
):
|
||||
from crewai.project.json_loader import _resolve_tools
|
||||
|
||||
github_tools = [object()]
|
||||
linear_tools = [object(), object()]
|
||||
calls: list[list[str]] = []
|
||||
|
||||
def build_platform_tools(apps: list[str]):
|
||||
calls.append(apps)
|
||||
return {
|
||||
"github": github_tools,
|
||||
"linear": linear_tools,
|
||||
}[apps[0]]
|
||||
|
||||
monkeypatch.setattr(
|
||||
"crewai_tools.CrewaiPlatformTools", build_platform_tools
|
||||
)
|
||||
|
||||
tools = _resolve_tools(["platform:github", "platform:linear"])
|
||||
|
||||
assert tools == github_tools + linear_tools
|
||||
assert calls == [["github"], ["linear"]]
|
||||
|
||||
def test_empty_platform_tool_ref_raises_with_guidance(self):
|
||||
from crewai.project.json_loader import JSONProjectError, _resolve_tools
|
||||
|
||||
with pytest.raises(JSONProjectError, match="platform:<application>"):
|
||||
_resolve_tools(["platform:"])
|
||||
|
||||
def test_platform_tool_discovery_errors_are_json_project_errors(
|
||||
self, monkeypatch
|
||||
):
|
||||
from crewai.project.json_loader import JSONProjectError, _resolve_tools
|
||||
|
||||
def fail_platform_tool_discovery(apps: list[str]):
|
||||
raise RuntimeError("discovery unavailable")
|
||||
|
||||
monkeypatch.setattr(
|
||||
"crewai_tools.CrewaiPlatformTools", fail_platform_tool_discovery
|
||||
)
|
||||
|
||||
with pytest.raises(JSONProjectError, match="discovery unavailable"):
|
||||
_resolve_tools(["platform:github"])
|
||||
|
||||
def test_import_ref_tool_resolves(self, tmp_path, monkeypatch):
|
||||
from crewai.project.json_loader import _resolve_tools
|
||||
|
||||
@@ -614,6 +660,19 @@ class TestValidationDoesNotExecuteTools:
|
||||
|
||||
assert "Invalid custom tool name" in str(exc_info.value)
|
||||
|
||||
def test_validate_rejects_empty_platform_tool_ref(self, tmp_path):
|
||||
from crewai.project.json_loader import (
|
||||
JSONProjectValidationError,
|
||||
validate_crew_project,
|
||||
)
|
||||
|
||||
crew_path = self._write_project(tmp_path, tool_line='"platform:"')
|
||||
|
||||
with pytest.raises(JSONProjectValidationError) as exc_info:
|
||||
validate_crew_project(crew_path, tmp_path / "agents")
|
||||
|
||||
assert "platform:<application>" in str(exc_info.value)
|
||||
|
||||
def test_validate_rejects_deep_python_ref_nesting(self, tmp_path):
|
||||
from crewai.project.json_loader import validate_crew_project
|
||||
|
||||
|
||||
Reference in New Issue
Block a user