diff --git a/examples/README.md b/examples/README.md index 451271bbe9..b74baee714 100644 --- a/examples/README.md +++ b/examples/README.md @@ -129,6 +129,7 @@ _Additional information can be found in the [Agents README](./agents/README.md). - **[`adk_demo`](frameworks/adk_demo/README.md)**: Minimal example using Google Agent Development Kit showcasing a simple weather time agent that can call tools (a function tool and an MCP tool) **[🟢 Beginner]** - **[`agno_personal_finance`](frameworks/agno_personal_finance/README.md)**: Personal finance planning agent built with Agno framework that researches and creates tailored financial plans **[🟢 Beginner]** - **[`autogen_demo`](frameworks/nat_autogen_demo/README.md)**: Minimal example using Microsoft AutoGen showcasing a traffic estimator agent that can call tools (a function tool and an MCP tool) and orchestrate multiple AutoGen agents **[🟨 Intermediate]** +- **[`crewai_demo`](frameworks/crewai_demo/README.md)**: Multi-agent travel planning system using CrewAI with role-based agents for itinerary planning, budget analysis, and final plan summarization **[🟢 Beginner]** - **[`haystack_deep_research_agent`](frameworks/haystack_deep_research_agent/README.md)**: Deep research agent using Haystack framework that combines web search and Retrieval Augmented Generation (RAG) capabilities with SerperDev API and OpenSearch **[🟨 Intermediate]** - **[`langchain_deep_research`](frameworks/auto_wrapper/langchain_deep_research/README.md)**: An example that integrates any existing LangGraph agent with NeMo Agent Toolkit using the `langgraph_wrapper` workflow type **[🟨 Intermediate]** - **[`multi_frameworks`](frameworks/multi_frameworks/README.md)**: Supervisor agent coordinating LangChain/LangGraph, LlamaIndex, and Haystack agents for research, RAG, and chitchat tasks **[🟨 Intermediate]** diff --git a/examples/frameworks/crewai_demo/README.md b/examples/frameworks/crewai_demo/README.md new file mode 100644 index 0000000000..6e02f77623 --- /dev/null +++ b/examples/frameworks/crewai_demo/README.md @@ -0,0 +1,67 @@ + + +# CrewAI Example + +**Complexity:** 🟢 Beginner + +A minimal example using CrewAI showcasing a multi-agent travel planning system where an Itinerary Expert plans activities, a Budget Advisor prices lodging and totals the trip cost, and a Summarizer compiles the final plan. + +## Table of Contents + +- [Key Features](#key-features) +- [Installation and Setup](#installation-and-setup) + - [Install this Workflow](#install-this-workflow) + - [Set Up API Keys](#set-up-api-keys) +- [Run the Workflow](#run-the-workflow) + +## Key Features + +- **CrewAI Framework Integration:** Demonstrates NeMo Agent Toolkit support for CrewAI alongside other frameworks like LangChain/LangGraph and Semantic Kernel. +- **Multi-Agent Travel Planning:** Three role-based agents (Itinerary Expert, Budget Advisor, Summarizer) running as a sequential `Crew`, each contributing its own specialized `Task`. +- **Shared Tools Across Agents:** Both the itinerary and budget agents draw on the same two NAT-registered tools (`hotel_price`, `local_events`), demonstrating that a single tool implementation is reusable across every supported framework, not rewritten per integration. +- **Task Context Chaining:** The budget and summary tasks receive prior tasks' output via CrewAI's `context=[...]`, so later agents build on earlier agents' work rather than repeating it. + +## Installation and Setup + +If you have not already done so, follow the instructions in the [Install Guide](../../../docs/source/get-started/installation.md#install-from-source) to create the development environment and install NeMo Agent Toolkit. + +### Install this Workflow + +From the root directory of the NeMo Agent Toolkit library, run the following commands: + +```bash +uv pip install -e examples/frameworks/crewai_demo +``` + +### Set Up API Keys + +You need to set your OpenAI API key as an environment variable to access OpenAI AI services: + +```bash +export OPENAI_API_KEY= +``` + +## Run the Workflow + +```bash +nat run --config_file examples/frameworks/crewai_demo/configs/config.yml --input "Create a 3-day travel itinerary for Tokyo in April, covering hotels and activities within a USD 2000 budget." +``` + +**Expected Workflow Output** + +The workflow produces a day-by-day itinerary, a lodging + activity cost breakdown, and a total estimated cost, compiled by the Summarizer agent into a single final plan — structurally the same kind of output the Semantic Kernel example produces for the same prompt, which is the point: the two examples are meant to be comparable, not just individually correct. diff --git a/examples/frameworks/crewai_demo/pyproject.toml b/examples/frameworks/crewai_demo/pyproject.toml new file mode 100644 index 0000000000..12ef7eed0c --- /dev/null +++ b/examples/frameworks/crewai_demo/pyproject.toml @@ -0,0 +1,44 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +[build-system] +build-backend = "setuptools.build_meta" +requires = ["setuptools>=64", "setuptools-scm>=8", "setuptools_dynamic_dependencies>=1.0.0"] + +[tool.setuptools_scm] +git_describe_command = "git describe --long --first-parent" +root = "../../.." + +[tool.setuptools.packages.find] +where = ["src"] + +[project] +name = "nat_crewai_demo" +dynamic = ["version", "dependencies"] +requires-python = ">=3.11,<3.14" +description = "CrewAI Example" +keywords = ["ai", "rag", "agents"] +classifiers = ["Programming Language :: Python"] + +[tool.setuptools_dynamic_dependencies] +dependencies = [ + "nvidia-nat[crewai,test] == {version}", +] + +[tool.uv.sources] +nvidia-nat = { path = "../../..", editable = true } + +[project.entry-points.'nat.components'] +nat_crewai_demo = "nat_crewai_demo.register" diff --git a/examples/frameworks/crewai_demo/src/nat_crewai_demo/__init__.py b/examples/frameworks/crewai_demo/src/nat_crewai_demo/__init__.py new file mode 100644 index 0000000000..3bcc1c39bb --- /dev/null +++ b/examples/frameworks/crewai_demo/src/nat_crewai_demo/__init__.py @@ -0,0 +1,14 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/examples/frameworks/crewai_demo/src/nat_crewai_demo/configs/config.yml b/examples/frameworks/crewai_demo/src/nat_crewai_demo/configs/config.yml new file mode 100644 index 0000000000..08203ab3ed --- /dev/null +++ b/examples/frameworks/crewai_demo/src/nat_crewai_demo/configs/config.yml @@ -0,0 +1,58 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +general: + front_end: + _type: fastapi + cors: + allow_origins: ['*'] + step_adaptor: + mode: default + +functions: + hotel_price: + _type: hotel_price + local_events: + _type: local_events + +llms: + openai_llm: + _type: openai + model_name: gpt-5.4-mini + temperature: 0.0 + +workflow: + _type: crewai + tool_names: [hotel_price, local_events] + llm_name: openai_llm + verbose: true + itinerary_expert_role: Itinerary Expert + itinerary_expert_goal: Build detailed, well-paced day-by-day travel itineraries. + itinerary_expert_backstory: | + You specialize in attractions, local events, and trip logistics. You focus purely on what to do and when; + you leave hotel pricing and overall budgeting to the Budget Advisor. + + budget_advisor_role: Budget Advisor + budget_advisor_goal: Produce an accurate, itemized cost estimate for the whole trip. + budget_advisor_backstory: | + You specialize in estimating and optimizing travel costs. You use the hotel price tool to find lodging costs + and combine them with the itinerary's activity costs into one clear total. + + summarize_agent_role: Summarizer + summarize_agent_goal: Turn the itinerary and the budget into one polished, user-facing travel plan. + summarize_agent_backstory: | + You specialize in taking detailed notes from other specialists and compiling them into a clear, + well-structured final document, without duplicating information or losing any details. diff --git a/examples/frameworks/crewai_demo/src/nat_crewai_demo/data/hotel_prices.json b/examples/frameworks/crewai_demo/src/nat_crewai_demo/data/hotel_prices.json new file mode 100644 index 0000000000..73b7cd05b3 --- /dev/null +++ b/examples/frameworks/crewai_demo/src/nat_crewai_demo/data/hotel_prices.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7bdc639461909b2c298528db842d8bf148f1fdd60dab55785e2ca009df6f6c77 +size 280 diff --git a/examples/frameworks/crewai_demo/src/nat_crewai_demo/data/local_events.json b/examples/frameworks/crewai_demo/src/nat_crewai_demo/data/local_events.json new file mode 100644 index 0000000000..cb5d3f5ab1 --- /dev/null +++ b/examples/frameworks/crewai_demo/src/nat_crewai_demo/data/local_events.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e9fe6cd6942cfb26412ca3e176e85cd9760a0339763f0480f69597cb0bcbc44d +size 422 diff --git a/examples/frameworks/crewai_demo/src/nat_crewai_demo/hotel_price_tool.py b/examples/frameworks/crewai_demo/src/nat_crewai_demo/hotel_price_tool.py new file mode 100644 index 0000000000..01b9d4dea9 --- /dev/null +++ b/examples/frameworks/crewai_demo/src/nat_crewai_demo/hotel_price_tool.py @@ -0,0 +1,77 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from pydantic import BaseModel + +from nat.builder.builder import Builder +from nat.builder.function_info import FunctionInfo +from nat.cli.register_workflow import register_function +from nat.data_models.function import FunctionBaseConfig + + +class HotelPriceToolConfig(FunctionBaseConfig, name="hotel_price"): + data_path: str = "examples/frameworks/crewai_demo/src/nat_crewai_demo/data/hotel_prices.json" + date_format: str = "%Y-%m-%d" + + +class HotelOffer(BaseModel): + name: str + price_per_night: float + total_price: float + city: str + checkin: str + checkout: str + + +class HotelOffersResponse(BaseModel): + offers: list[HotelOffer] + + +@register_function(config_type=HotelPriceToolConfig) +async def hotel_price(tool_config: HotelPriceToolConfig, builder: Builder): + + import json + + with open(tool_config.data_path, encoding='utf-8') as f: + hotel_prices = json.load(f) + + search_date_format = tool_config.date_format + + async def _get_hotel_price(city: str, checkin: str, checkout: str) -> HotelOffersResponse: + from datetime import datetime + + base_hotels = hotel_prices + + checkin_dt = datetime.strptime(checkin, search_date_format) + checkout_dt = datetime.strptime(checkout, search_date_format) + nights = (checkout_dt - checkin_dt).days + + offers = [] + for hotel in base_hotels: + total_price = hotel["price_per_night"] * nights + offers.append( + HotelOffer(name=hotel["name"], + price_per_night=hotel["price_per_night"], + total_price=total_price, + city=city, + checkin=checkin, + checkout=checkout)) + + return HotelOffersResponse(offers=offers) + + yield FunctionInfo.from_fn( + _get_hotel_price, + description=( + "This tool returns a list of hotels and nightly prices for the given city and checkin/checkout dates.")) diff --git a/examples/frameworks/crewai_demo/src/nat_crewai_demo/local_events_tool.py b/examples/frameworks/crewai_demo/src/nat_crewai_demo/local_events_tool.py new file mode 100644 index 0000000000..04d31d7de8 --- /dev/null +++ b/examples/frameworks/crewai_demo/src/nat_crewai_demo/local_events_tool.py @@ -0,0 +1,51 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from pydantic import BaseModel + +from nat.builder.builder import Builder +from nat.builder.function_info import FunctionInfo +from nat.cli.register_workflow import register_function +from nat.data_models.function import FunctionBaseConfig + + +class LocalEvent(BaseModel): + name: str + cost: float + city: str + + +class LocalEventsResponse(BaseModel): + events: list[LocalEvent] + + +class LocalEventsToolConfig(FunctionBaseConfig, name="local_events"): + data_path: str = "examples/frameworks/crewai_demo/src/nat_crewai_demo/data/local_events.json" + + +@register_function(config_type=LocalEventsToolConfig) +async def local_events(tool_config: LocalEventsToolConfig, builder: Builder): + + import json + + with open(tool_config.data_path) as f: + events = LocalEventsResponse.model_validate({"events": json.load(f)}).events + + async def _local_events(city: str) -> LocalEventsResponse: + return LocalEventsResponse(events=[e for e in events if e.city == city]) + + yield FunctionInfo.from_fn( + _local_events, + description=("This tool can provide information and cost of local events and activities in a city")) diff --git a/examples/frameworks/crewai_demo/src/nat_crewai_demo/register.py b/examples/frameworks/crewai_demo/src/nat_crewai_demo/register.py new file mode 100644 index 0000000000..24945c0f1c --- /dev/null +++ b/examples/frameworks/crewai_demo/src/nat_crewai_demo/register.py @@ -0,0 +1,135 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging + +from pydantic import Field + +from nat.builder.builder import Builder +from nat.builder.framework_enum import LLMFrameworkEnum +from nat.builder.function_info import FunctionInfo +from nat.cli.register_workflow import register_function +from nat.data_models.component_ref import FunctionRef +from nat.data_models.component_ref import LLMRef +from nat.data_models.function import FunctionBaseConfig + +from . import hotel_price_tool # noqa: F401, pylint: disable=unused-import +from . import local_events_tool # noqa: F401, pylint: disable=unused-import + +logger = logging.getLogger(__name__) + + +class CrewAITravelPlanningWorkflowConfig(FunctionBaseConfig, name="crewai"): + """ + A travel-planning crew: an itinerary expert, a budget advisor, and a summarizer, each backed by the same two + NAT-registered tools. + """ + tool_names: list[FunctionRef] = Field(default_factory=list, + description="The list of tools to provide to the crew.") + llm_name: LLMRef = Field(description="The LLM model to use for every agent in the crew.") + verbose: bool = Field(default=False, description="Set the verbosity of CrewAI's logging.") + itinerary_expert_role: str = Field(description="The role of the itinerary expert agent.") + itinerary_expert_goal: str = Field(description="The goal of the itinerary expert agent.") + itinerary_expert_backstory: str = Field(description="The backstory of the itinerary expert agent.") + budget_advisor_role: str = Field(description="The role of the budget advisor agent.") + budget_advisor_goal: str = Field(description="The goal of the budget advisor agent.") + budget_advisor_backstory: str = Field(description="The backstory of the budget advisor agent.") + summarize_agent_role: str = Field(description="The role of the summarizer agent.") + summarize_agent_goal: str = Field(description="The goal of the summarizer agent.") + summarize_agent_backstory: str = Field(description="The backstory of the summarizer agent.") + + +@register_function(config_type=CrewAITravelPlanningWorkflowConfig, framework_wrappers=[LLMFrameworkEnum.CREWAI]) +async def crewai_travel_planning_workflow(config: CrewAITravelPlanningWorkflowConfig, builder: Builder): + + import asyncio + + from crewai import Agent + from crewai import Crew + from crewai import Process + from crewai import Task + + llm = await builder.get_llm(config.llm_name, wrapper_type=LLMFrameworkEnum.CREWAI) + tools = await builder.get_tools(config.tool_names, wrapper_type=LLMFrameworkEnum.CREWAI) + + itinerary_expert = Agent( + role=config.itinerary_expert_role, + goal=config.itinerary_expert_goal, + backstory=config.itinerary_expert_backstory, + tools=tools, + llm=llm, + verbose=config.verbose, + ) + + budget_advisor = Agent( + role=config.budget_advisor_role, + goal=config.budget_advisor_goal, + backstory=config.budget_advisor_backstory, + tools=tools, + llm=llm, + verbose=config.verbose, + ) + + summarizer = Agent( + role=config.summarize_agent_role, + goal=config.summarize_agent_goal, + backstory=config.summarize_agent_backstory, + llm=llm, + verbose=config.verbose, + ) + + itinerary_task = Task( + description=("Using the local events tool, put together a day-by-day itinerary for this trip: {input}. " + "Focus on attractions and activities and their cost; leave hotel pricing to the Budget " + "Advisor."), + expected_output="A day-by-day itinerary listing activities and their individual costs.", + agent=itinerary_expert, + ) + + budget_task = Task( + description=("Using the hotel price tool, estimate the total lodging cost for this trip: {input}. " + "Combine it with the itinerary's activity costs to produce a total estimated trip cost."), + expected_output="A cost breakdown covering lodging and activities, plus a total estimated cost.", + agent=budget_advisor, + context=[itinerary_task], + ) + + summary_task = Task( + description=("Compile the itinerary and the budget breakdown into a single, clear, well-structured travel " + "plan for: {input}."), + expected_output="A final travel plan with sections for the itinerary and the cost breakdown.", + agent=summarizer, + context=[itinerary_task, budget_task], + ) + + crew = Crew( + agents=[itinerary_expert, budget_advisor, summarizer], + tasks=[itinerary_task, budget_task, summary_task], + process=Process.sequential, + verbose=config.verbose, + ) + + async def _response_fn(input_message: str) -> str: + # CrewAI's kickoff() is synchronous; run it off the event loop thread so this + # coroutine doesn't block NAT's async runtime while the crew executes. + result = await asyncio.to_thread(crew.kickoff, inputs={"input": input_message}) + return str(result) + + try: + yield FunctionInfo.from_fn(_response_fn, description="Plans a trip using a crew of specialized agents.") + except GeneratorExit: + logger.exception("Exited early!") + finally: + logger.debug("Cleaning up") diff --git a/examples/frameworks/crewai_demo/tests/test_crewai_demo_workflow.py b/examples/frameworks/crewai_demo/tests/test_crewai_demo_workflow.py new file mode 100644 index 0000000000..eaef9a114d --- /dev/null +++ b/examples/frameworks/crewai_demo/tests/test_crewai_demo_workflow.py @@ -0,0 +1,34 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from pathlib import Path + +import pytest + + +@pytest.mark.usefixtures("openai_api_key") +@pytest.mark.integration +async def test_full_workflow(): + from nat.test.utils import locate_example_config + from nat.test.utils import run_workflow + from nat_crewai_demo.register import CrewAITravelPlanningWorkflowConfig + + config_file: Path = locate_example_config(CrewAITravelPlanningWorkflowConfig) + + await run_workflow( + config_file=config_file, + question=("Create a 3-day travel itinerary for Tokyo in April, covering hotels and activities within a USD " + "2000 budget."), + expected_answer="budget")