From 7c15f486decac7fc5b3006ca320bba7d61dcb641 Mon Sep 17 00:00:00 2001 From: Vlada Dusek Date: Wed, 17 Jun 2026 11:30:53 +0200 Subject: [PATCH 01/10] docs: add AI agents guide --- README.md | 4 +- docs/01_introduction/index.mdx | 2 +- docs/01_introduction/quick-start.mdx | 1 + docs/03_guides/13_ai_agents.mdx | 173 +++++++++++++++++++++++++++ docs/03_guides/code/13_ai_agents.py | 50 ++++++++ 5 files changed, 227 insertions(+), 3 deletions(-) create mode 100644 docs/03_guides/13_ai_agents.mdx create mode 100644 docs/03_guides/code/13_ai_agents.py diff --git a/README.md b/README.md index b9c9ea55..b1c2f316 100644 --- a/README.md +++ b/README.md @@ -116,7 +116,7 @@ Almost any Python project can become an Actor, including projects for: - **Web scraping and crawling** — The SDK is fully compatible with [Crawlee](https://crawlee.dev/python), which makes Apify a natural place to deploy and scale your crawlers (see the [Crawlee guide](https://docs.apify.com/sdk/python/docs/guides/crawlee)). It also works with other popular scraping libraries, such as [Scrapy](https://docs.apify.com/sdk/python/docs/guides/scrapy), [Scrapling](https://docs.apify.com/sdk/python/docs/guides/scrapling), or [Crawl4AI](https://docs.apify.com/sdk/python/docs/guides/crawl4ai). - **Browser automation** — Drive a real browser with [Playwright](https://docs.apify.com/sdk/python/docs/guides/playwright) or [Selenium](https://docs.apify.com/sdk/python/docs/guides/selenium), or with higher-level tools such as [Browser Use](https://docs.apify.com/sdk/python/docs/guides/browser-use). - **Web servers and APIs** — Run a [web server](https://docs.apify.com/sdk/python/docs/guides/running-webserver) inside an Actor to serve HTTP requests, for example to expose your scraper as a live API. -- **AI agents** — Host agents built with your framework of choice. Ready-made Actor templates cover [PydanticAI](https://apify.com/templates/python-pydanticai), [CrewAI](https://apify.com/templates/python-crewai), [LangGraph](https://apify.com/templates/python-langgraph), [LlamaIndex](https://apify.com/templates/python-llamaindex-agent), and [Smolagents](https://apify.com/templates/python-smolagents). +- **AI agents** — Host agents built with your framework of choice (see the [AI agents guide](https://docs.apify.com/sdk/python/docs/guides/ai-agents)). Ready-made Actor templates cover [PydanticAI](https://apify.com/templates/python-pydanticai), [CrewAI](https://apify.com/templates/python-crewai), [LangGraph](https://apify.com/templates/python-langgraph), [LlamaIndex](https://apify.com/templates/python-llamaindex-agent), and [Smolagents](https://apify.com/templates/python-smolagents). - **MCP servers** — Deploy a Python MCP server as an Actor and make its tools available to any MCP client. See [MCP server](https://apify.com/templates/python-mcp-empty) and [MCP proxy](https://apify.com/templates/python-mcp-proxy) templates Whatever you build, the Apify SDK doesn't lock you into a particular framework. Bring the libraries you already use, and let Apify run your project in the cloud. @@ -199,7 +199,7 @@ The full SDK documentation lives at **[docs.apify.com/sdk/python](https://docs.a | [Overview](https://docs.apify.com/sdk/python/docs/overview) | What the SDK is, what Actors are, and how the pieces fit together. | | [Quick start](https://docs.apify.com/sdk/python/docs/quick-start) | Create, run, and deploy your first Python Actor. | | [Concepts](https://docs.apify.com/sdk/python/docs/concepts/actor-lifecycle) | Actor lifecycle, input, storages, events, proxy management, interacting with other Actors, webhooks, accessing the Apify API, logging, configuration, and pay-per-event. | -| [Guides](https://docs.apify.com/sdk/python/docs/guides/beautifulsoup-httpx) | Integrations with BeautifulSoup, Parsel, Playwright, Selenium, Crawlee, Scrapy, Scrapling, Crawl4AI, and Browser Use, plus running a web server and using uv. | +| [Guides](https://docs.apify.com/sdk/python/docs/guides/beautifulsoup-httpx) | Integrations with BeautifulSoup, Parsel, Playwright, Selenium, Crawlee, Scrapy, Scrapling, Crawl4AI, and Browser Use, plus building AI agents, running a web server, and using uv. | | [Upgrading](https://docs.apify.com/sdk/python/docs/upgrading/upgrading-to-v4) | Migrating between major versions. | | [API reference](https://docs.apify.com/sdk/python/reference) | Generated reference for every class and method. | | [Changelog](https://docs.apify.com/sdk/python/docs/changelog) | Release history and breaking changes. | diff --git a/docs/01_introduction/index.mdx b/docs/01_introduction/index.mdx index 133c6a47..b4db08e8 100644 --- a/docs/01_introduction/index.mdx +++ b/docs/01_introduction/index.mdx @@ -41,7 +41,7 @@ Almost any Python project can become an Actor, including projects for: - **Web scraping and crawling** - The SDK is fully compatible with [Crawlee](https://crawlee.dev/python), which makes Apify a natural place to deploy and scale your crawlers (see the [Crawlee guide](./guides/crawlee)). It also works with other popular scraping libraries, such as [Scrapy](./guides/scrapy), [Scrapling](./guides/scrapling), or [Crawl4AI](./guides/crawl4ai). - **Browser automation** - Drive a real browser with [Playwright](./guides/playwright) or [Selenium](./guides/selenium), or with higher-level tools such as [Browser Use](./guides/browser-use). - **Web servers and APIs** - Run a [web server](./guides/running-webserver) inside an Actor to serve HTTP requests, for example to expose your scraper as a live API. -- **AI agents** - Host agents built with your framework of choice. Ready-made Actor templates cover [PydanticAI](https://apify.com/templates/python-pydanticai), [CrewAI](https://apify.com/templates/python-crewai), [LangGraph](https://apify.com/templates/python-langgraph), [LlamaIndex](https://apify.com/templates/python-llamaindex-agent), and [Smolagents](https://apify.com/templates/python-smolagents). +- **AI agents** - Host agents built with your framework of choice (see the [AI agents guide](./guides/ai-agents)). Ready-made Actor templates cover [PydanticAI](https://apify.com/templates/python-pydanticai), [CrewAI](https://apify.com/templates/python-crewai), [LangGraph](https://apify.com/templates/python-langgraph), [LlamaIndex](https://apify.com/templates/python-llamaindex-agent), and [Smolagents](https://apify.com/templates/python-smolagents). - **MCP servers** - Deploy a Python MCP server as an Actor and make its tools available to any MCP client. See the [MCP server](https://apify.com/templates/python-mcp-empty) and [MCP proxy](https://apify.com/templates/python-mcp-proxy) templates. Whatever you build, the Apify SDK doesn't lock you into a particular framework. Bring the libraries you already use, and let Apify run your project in the cloud. diff --git a/docs/01_introduction/quick-start.mdx b/docs/01_introduction/quick-start.mdx index 289659bf..19231b81 100644 --- a/docs/01_introduction/quick-start.mdx +++ b/docs/01_introduction/quick-start.mdx @@ -112,6 +112,7 @@ To see how you can integrate the Apify SDK with popular scraping libraries and f - [Adaptive scraping with Scrapling](./guides/scrapling) - [LLM-ready scraping with Crawl4AI](./guides/crawl4ai) - [Browser AI agents with Browser Use](./guides/browser-use) +- [Building AI agents](./guides/ai-agents) For other aspects of Actor development, explore these guides: diff --git a/docs/03_guides/13_ai_agents.mdx b/docs/03_guides/13_ai_agents.mdx new file mode 100644 index 00000000..f7c038e9 --- /dev/null +++ b/docs/03_guides/13_ai_agents.mdx @@ -0,0 +1,173 @@ +--- +id: ai-agents +title: Building AI agents +description: Host AI agents built with PydanticAI, CrewAI, LangGraph, LlamaIndex, or Smolagents as Apify Actors. +--- + +import RunnableCodeBlock from '@site/src/components/RunnableCodeBlock'; + +import AiAgentsExample from '!!raw-loader!roa-loader!./code/13_ai_agents.py'; + +In this guide, you'll learn how to host an AI agent as an Apify Actor, using the agent framework of your choice. + +## Introduction + +An AI agent is a program that uses a large language model (LLM) to decide what to do next. Instead of following a fixed script, it reasons about a task, calls tools to gather information or take actions, and loops until the task is done. The Python ecosystem offers several frameworks for building such agents, and the Apify SDK doesn't lock you into any of them. + +Apify Actors are a natural home for agents: + +- An agent is just a Python program, so wrapping it in `async with Actor:` is all it takes to run it on the platform. +- The platform scales runs, schedules them, and keeps logs, so you don't operate any infrastructure yourself. +- The [Apify Store](https://apify.com/store) gives your agent thousands of ready-made tools. Any Actor (a scraper, a browser automation, an API wrapper) can become a tool the agent calls. +- The [Apify OpenRouter proxy](https://apify.com/apify/openrouter) lets the agent talk to an LLM without managing a provider API key. +- Pay-per-event charging lets you monetize the agent and pass LLM costs on to its users. + +## Choosing a framework + +Apify maintains a ready-made Actor template for each of the popular agent frameworks. Each template is a complete, deployable Actor that you can use as a starting point. Pick the one that matches the framework you already use, or the style of agent you want to build: + +| Framework | Good for | Template | +|---|---|---| +| [PydanticAI](https://ai.pydantic.dev/) | Typed, Pydantic-native agents with tool calling | [`python-pydanticai`](https://apify.com/templates/python-pydanticai) | +| [CrewAI](https://www.crewai.com/) | Multi-agent "crews" that collaborate on a task | [`python-crewai`](https://apify.com/templates/python-crewai) | +| [LangGraph](https://www.langchain.com/langgraph) | Graph-based agents with explicit state and control flow | [`python-langgraph`](https://apify.com/templates/python-langgraph) | +| [LlamaIndex](https://www.llamaindex.ai/) | Retrieval-augmented agents over your own data | [`python-llamaindex-agent`](https://apify.com/templates/python-llamaindex-agent) | +| [Smolagents](https://github.com/huggingface/smolagents) | Lightweight code-writing agents from Hugging Face | [`python-smolagents`](https://apify.com/templates/python-smolagents) | + +All of these templates live in the [actor-templates repository](https://github.com/apify/actor-templates), and you can scaffold any of them with the [Apify CLI](https://docs.apify.com/cli), for example `apify create my-agent --template python-pydanticai`. + +## Example Actor + +The following Actor runs a [PydanticAI](https://ai.pydantic.dev/) agent for a single prompt and stores the answer in the default dataset. The prompt and model are configurable through the Actor input. + +A `build_agent` helper holds the PydanticAI-specific setup, while the `main` coroutine handles the [Actor](https://docs.apify.com/platform/actors) lifecycle, reads the input, runs the agent, and stores the result: + + + {AiAgentsExample} + + +Note that: + +- Keeping the agent setup in `build_agent` separates the framework-specific code from the Actor's orchestration logic. `main` only decides what to read from the input and what to store. +- The agent talks to its LLM through the Apify OpenRouter proxy, so the Actor needs no provider API key. See [Connecting to an LLM](#connecting-to-an-llm) below. +- `output_type=str` returns the answer as plain text. Pass a [Pydantic](./input-validation) model instead to get a validated object that maps onto a dataset row. + +## Connecting to an LLM + +Every agent needs an LLM, and there are two ways to provide one. + +The example above uses the [Apify OpenRouter proxy](https://apify.com/apify/openrouter), an OpenAI-compatible endpoint at `https://openrouter.apify.actor/api/v1` that fronts the full [OpenRouter](https://openrouter.ai) model catalog. The token usage is billed against the Apify account running the Actor, so no provider API key is required. The Actor authenticates with the proxy using the `APIFY_TOKEN` that the platform injects into every run. To switch models, change the `model` input to any [OpenRouter model slug](https://openrouter.ai/models), for example `openai/gpt-4o-mini`. + +Alternatively, you can call a provider such as OpenAI, Anthropic, or Google directly with your own API key. Keep the key out of the Actor input and source code. Read it from an environment variable, which on the platform you set as a [secret environment variable](https://docs.apify.com/platform/actors/development/programming-interface/environment-variables) and locally you export in your shell. + +## Using Apify Actors as tools + +An agent becomes useful once it can act on the world. On Apify, any of the thousands of Actors in the [Apify Store](https://apify.com/store) can become a tool: a scraper that fetches data, a browser automation that fills a form, or an API wrapper that posts a message. + +For LangChain-based frameworks, such as LangGraph and CrewAI, the [`langchain-apify`](https://github.com/apify/langchain-apify) package exposes any Actor as a tool with a single line: + +```python +from crewai_tools import ApifyActorsTool + +# Let the agent scrape Instagram profiles by calling the apify/instagram-scraper Actor. +tools = [ApifyActorsTool('apify/instagram-scraper')] +``` + +For any other framework, call the Actor directly through the Actor's preconfigured API client and read its dataset: + +```python +@tool +async def scrape_instagram(handle: str) -> list[dict]: + """Scrape recent posts from an Instagram profile.""" + run_input = {'directUrls': [f'https://www.instagram.com/{handle}/']} + run = await Actor.apify_client.actor('apify/instagram-scraper').call(run_input=run_input) + return (await Actor.apify_client.dataset(run['defaultDatasetId']).list_items()).items +``` + +For details on calling other Actors, see [Interacting with other Actors](../concepts/interacting-with-other-actors). + +## Other frameworks + +The same `async with Actor:` pattern wraps an agent built with any framework. The snippets below show how each one defines an agent. For a full, deployable Actor, start from the matching template. + +[**CrewAI**](https://apify.com/templates/python-crewai) assembles one or more role-playing agents into a crew that works through tasks: + +```python +from crewai import Agent, Crew, Task + +agent = Agent( + role='Social Media Analyst', + goal='Analyze Instagram profiles and summarize the findings.', + backstory='An expert at turning social media data into insights.', + tools=[ApifyActorsTool('apify/instagram-scraper')], + llm='gpt-4o-mini', +) +task = Task(description=query, expected_output='A short report.', agent=agent) +result = (await Crew(agents=[agent], tasks=[task]).kickoff_async()).raw +``` + +[**LangGraph**](https://apify.com/templates/python-langgraph) builds an agent as a graph with explicit state, which makes complex control flow easy to follow: + +```python +from langchain.agents import create_agent +from langchain_openai import ChatOpenAI + +agent = create_agent(ChatOpenAI(model='gpt-4o-mini'), tools=[scrape_instagram]) +result = await agent.ainvoke({'messages': [('user', query)]}) +``` + +[**LlamaIndex**](https://apify.com/templates/python-llamaindex-agent) shines at retrieval-augmented agents that reason over your own data: + +```python +from llama_index.core.agent import ReActAgent +from llama_index.core.tools import FunctionTool +from llama_index.llms.openai import OpenAI + +agent = ReActAgent( + tools=[FunctionTool.from_defaults(fn=scrape_instagram)], + llm=OpenAI(model='gpt-4o-mini'), +) +response = await agent.run(user_msg=query) +``` + +[**Smolagents**](https://apify.com/templates/python-smolagents) is a lightweight framework from Hugging Face whose agents write and run Python code to solve a task: + +```python +from smolagents import CodeAgent, OpenAIServerModel, WebSearchTool + +agent = CodeAgent(tools=[WebSearchTool()], model=OpenAIServerModel(model_id='gpt-4o-mini')) +result = agent.run(query) +``` + +## Running on the Apify platform + +Agents run on the standard [Apify Python base image](https://hub.docker.com/r/apify/actor-python), so no browser or extra system dependencies are needed. Add `apify` and your framework to `requirements.txt`, for example: + +```text +apify +pydantic-ai +``` + +To monetize the agent, use [pay-per-event charging](../concepts/pay-per-event). You define events such as `task-completed` in the Actor's monetization settings and trigger them from the code: + +```python +await Actor.charge('task-completed') +``` + +This lets you charge users directly from the Actor and cover the cost of execution and LLM tokens. It pairs naturally with the Apify OpenRouter proxy, which already bills the LLM usage to the account running the Actor. + +## Conclusion + +In this guide, you learned how to host an AI agent as an Apify Actor. You can now run an agent built with PydanticAI, CrewAI, LangGraph, LlamaIndex, or Smolagents, connect it to an LLM through the Apify OpenRouter proxy, give it Apify Actors as tools, and monetize it with pay-per-event. To get started, see the [Actor templates](https://apify.com/templates/categories/python). If you have questions or need assistance, feel free to reach out on our [GitHub](https://github.com/apify/apify-sdk-python) or join our [Discord community](https://discord.com/invite/jyEM2PRvMU). Happy building! + +## Additional resources + +- [Apify templates: PydanticAI](https://apify.com/templates/python-pydanticai) +- [Apify templates: CrewAI](https://apify.com/templates/python-crewai) +- [Apify templates: LangGraph](https://apify.com/templates/python-langgraph) +- [Apify templates: LlamaIndex](https://apify.com/templates/python-llamaindex-agent) +- [Apify templates: Smolagents](https://apify.com/templates/python-smolagents) +- [Apify: actor-templates repository](https://github.com/apify/actor-templates) +- [Apify: OpenRouter proxy](https://apify.com/apify/openrouter) +- [Apify: langchain-apify integration](https://github.com/apify/langchain-apify) +- [Apify blog: What are AI agents](https://blog.apify.com/what-are-ai-agents/) diff --git a/docs/03_guides/code/13_ai_agents.py b/docs/03_guides/code/13_ai_agents.py new file mode 100644 index 00000000..fa00494f --- /dev/null +++ b/docs/03_guides/code/13_ai_agents.py @@ -0,0 +1,50 @@ +import asyncio +import os + +from pydantic_ai import Agent +from pydantic_ai.models.openai import OpenAIChatModel +from pydantic_ai.providers.openai import OpenAIProvider + +from apify import Actor + +# The agent reaches its LLM through the Apify OpenRouter proxy, an +# OpenAI-compatible endpoint billed against the run's Apify account. It +# authenticates with the `APIFY_TOKEN` that the platform injects into every run, +# so no separate provider API key (such as `OPENAI_API_KEY`) is needed. +OPENROUTER_BASE_URL = 'https://openrouter.apify.actor/api/v1' + +DEFAULT_PROMPT = 'In two sentences, explain what an Apify Actor is.' + + +def build_agent(model_name: str) -> Agent[None, str]: + """Build a PydanticAI agent that routes LLM calls through Apify OpenRouter.""" + provider = OpenAIProvider( + base_url=OPENROUTER_BASE_URL, + api_key=os.environ['APIFY_TOKEN'], + ) + return Agent( + OpenAIChatModel(model_name, provider=provider), + output_type=str, + system_prompt='You are a concise, helpful research assistant.', + ) + + +async def main() -> None: + async with Actor: + # Read the Actor input. + actor_input = await Actor.get_input() or {} + prompt = actor_input.get('prompt', DEFAULT_PROMPT) + model_name = actor_input.get('model', 'openai/gpt-4o-mini') + + # Build the agent and run it for a single prompt. + Actor.log.info(f'Running the agent (model={model_name}) for: {prompt}') + agent = build_agent(model_name) + result = await agent.run(user_prompt=prompt) + + # Store the agent's answer in the default dataset. + Actor.log.info(f'The agent responded:\n{result.output}') + await Actor.push_data({'prompt': prompt, 'response': result.output}) + + +if __name__ == '__main__': + asyncio.run(main()) From 034a077eb3a85f24b9a0e70d6b9c4789334653e1 Mon Sep 17 00:00:00 2001 From: Vlada Dusek Date: Wed, 17 Jun 2026 14:03:24 +0200 Subject: [PATCH 02/10] docs: split AI agents guide into per-framework sections --- docs/03_guides/13_ai_agents.mdx | 148 +++++++++++++-------------- docs/03_guides/code/13_ai_agents.py | 50 --------- docs/03_guides/code/13_crewai.py | 34 ++++++ docs/03_guides/code/13_langgraph.py | 33 ++++++ docs/03_guides/code/13_llamaindex.py | 40 ++++++++ docs/03_guides/code/13_pydanticai.py | 36 +++++++ docs/03_guides/code/13_smolagents.py | 32 ++++++ 7 files changed, 246 insertions(+), 127 deletions(-) delete mode 100644 docs/03_guides/code/13_ai_agents.py create mode 100644 docs/03_guides/code/13_crewai.py create mode 100644 docs/03_guides/code/13_langgraph.py create mode 100644 docs/03_guides/code/13_llamaindex.py create mode 100644 docs/03_guides/code/13_pydanticai.py create mode 100644 docs/03_guides/code/13_smolagents.py diff --git a/docs/03_guides/13_ai_agents.mdx b/docs/03_guides/13_ai_agents.mdx index f7c038e9..3c0c8a06 100644 --- a/docs/03_guides/13_ai_agents.mdx +++ b/docs/03_guides/13_ai_agents.mdx @@ -6,7 +6,11 @@ description: Host AI agents built with PydanticAI, CrewAI, LangGraph, LlamaIndex import RunnableCodeBlock from '@site/src/components/RunnableCodeBlock'; -import AiAgentsExample from '!!raw-loader!roa-loader!./code/13_ai_agents.py'; +import PydanticaiExample from '!!raw-loader!roa-loader!./code/13_pydanticai.py'; +import CrewaiExample from '!!raw-loader!roa-loader!./code/13_crewai.py'; +import LanggraphExample from '!!raw-loader!roa-loader!./code/13_langgraph.py'; +import LlamaindexExample from '!!raw-loader!roa-loader!./code/13_llamaindex.py'; +import SmolagentsExample from '!!raw-loader!roa-loader!./code/13_smolagents.py'; In this guide, you'll learn how to host an AI agent as an Apify Actor, using the agent framework of your choice. @@ -28,37 +32,80 @@ Apify maintains a ready-made Actor template for each of the popular agent framew | Framework | Good for | Template | |---|---|---| -| [PydanticAI](https://ai.pydantic.dev/) | Typed, Pydantic-native agents with tool calling | [`python-pydanticai`](https://apify.com/templates/python-pydanticai) | -| [CrewAI](https://www.crewai.com/) | Multi-agent "crews" that collaborate on a task | [`python-crewai`](https://apify.com/templates/python-crewai) | -| [LangGraph](https://www.langchain.com/langgraph) | Graph-based agents with explicit state and control flow | [`python-langgraph`](https://apify.com/templates/python-langgraph) | -| [LlamaIndex](https://www.llamaindex.ai/) | Retrieval-augmented agents over your own data | [`python-llamaindex-agent`](https://apify.com/templates/python-llamaindex-agent) | -| [Smolagents](https://github.com/huggingface/smolagents) | Lightweight code-writing agents from Hugging Face | [`python-smolagents`](https://apify.com/templates/python-smolagents) | +| [PydanticAI](#pydanticai) | Typed, Pydantic-native agents with tool calling | [`python-pydanticai`](https://apify.com/templates/python-pydanticai) | +| [CrewAI](#crewai) | Multi-agent "crews" that collaborate on a task | [`python-crewai`](https://apify.com/templates/python-crewai) | +| [LangGraph](#langgraph) | Graph-based agents with explicit state and control flow | [`python-langgraph`](https://apify.com/templates/python-langgraph) | +| [LlamaIndex](#llamaindex) | Retrieval-augmented agents over your own data | [`python-llamaindex-agent`](https://apify.com/templates/python-llamaindex-agent) | +| [Smolagents](#smolagents) | Lightweight code-writing agents from Hugging Face | [`python-smolagents`](https://apify.com/templates/python-smolagents) | All of these templates live in the [actor-templates repository](https://github.com/apify/actor-templates), and you can scaffold any of them with the [Apify CLI](https://docs.apify.com/cli), for example `apify create my-agent --template python-pydanticai`. -## Example Actor +## Connecting to an LLM + +Every agent needs an LLM, and there are two ways to provide one. + +The first is the [Apify OpenRouter proxy](https://apify.com/apify/openrouter), an OpenAI-compatible endpoint at `https://openrouter.apify.actor/api/v1` that fronts the full [OpenRouter](https://openrouter.ai) model catalog. The token usage is billed against the Apify account running the Actor, so no provider API key is required. The Actor authenticates with the proxy using the `APIFY_TOKEN` that the platform injects into every run. Any framework with an OpenAI-compatible client can point its base URL at the proxy, which is what most examples below do. To switch models, change the `model` input to any [OpenRouter model slug](https://openrouter.ai/models), for example `openai/gpt-4o-mini`. -The following Actor runs a [PydanticAI](https://ai.pydantic.dev/) agent for a single prompt and stores the answer in the default dataset. The prompt and model are configurable through the Actor input. +The second is to call a provider such as OpenAI directly with your own API key, which the CrewAI example below does through LiteLLM. Keep the key out of the Actor input and source code. Read it from an environment variable, which on the platform you set as a [secret environment variable](https://docs.apify.com/platform/actors/development/programming-interface/environment-variables) and locally you export in your shell. -A `build_agent` helper holds the PydanticAI-specific setup, while the `main` coroutine handles the [Actor](https://docs.apify.com/platform/actors) lifecycle, reads the input, runs the agent, and stores the result: +Each section below shows a complete, single-file Actor for one framework. They all read the input, run the agent, and store the result in the default dataset. + +## PydanticAI + +[PydanticAI](https://ai.pydantic.dev/) is an agent framework from the team behind Pydantic. It is strongly typed and integrates naturally with the [Pydantic models](./input-validation) the Apify SDK already uses. The following Actor runs an agent for a single prompt: - {AiAgentsExample} + {PydanticaiExample} + + +Set `output_type` to a Pydantic model instead of `str` to get a validated object back, which maps directly onto a dataset row. + +## CrewAI + +[CrewAI](https://www.crewai.com/) models a problem as a "crew" of role-playing agents that work through tasks. The following Actor defines a single analyst agent and one task: + + + {CrewaiExample} Note that: -- Keeping the agent setup in `build_agent` separates the framework-specific code from the Actor's orchestration logic. `main` only decides what to read from the input and what to store. -- The agent talks to its LLM through the Apify OpenRouter proxy, so the Actor needs no provider API key. See [Connecting to an LLM](#connecting-to-an-llm) below. -- `output_type=str` returns the answer as plain text. Pass a [Pydantic](./input-validation) model instead to get a validated object that maps onto a dataset row. +- `kickoff_async` runs the crew without blocking the Actor's event loop. +- CrewAI calls the LLM through [LiteLLM](https://docs.litellm.ai/), so this example reads `OPENAI_API_KEY`. To route it through the Apify OpenRouter proxy instead, configure a custom `LLM` (see the [CrewAI LLM docs](https://docs.crewai.com/concepts/llms)). +- On a fresh container, crewAI shows a one-time trace-consent prompt. The template sets `CREWAI_TESTING=true` to suppress it; do the same in your Dockerfile. -## Connecting to an LLM +## LangGraph -Every agent needs an LLM, and there are two ways to provide one. +[LangGraph](https://www.langchain.com/langgraph) builds an agent as a graph with explicit state, which makes complex, multi-step control flow easy to follow. It builds on [LangChain](https://www.langchain.com/), so any LangChain chat model and tool works. The following Actor runs a single-turn agent: + + + {LanggraphExample} + + +The example passes an empty `tools` list for brevity. Add LangChain tools to give the agent abilities, and read the final answer from the last message in the returned state. + +## LlamaIndex + +[LlamaIndex](https://www.llamaindex.ai/) is built for retrieval-augmented agents that reason over your own data. The following Actor runs a `ReActAgent` with a single tool: + + + {LlamaindexExample} + + +Note that: + +- `OpenAILike` is the LlamaIndex LLM class for OpenAI-compatible endpoints such as the Apify OpenRouter proxy. It needs the `llama-index-llms-openai-like` package. +- The agent decides on its own when to call the `word_count` tool. Add `FunctionTool`s of your own to extend it. -The example above uses the [Apify OpenRouter proxy](https://apify.com/apify/openrouter), an OpenAI-compatible endpoint at `https://openrouter.apify.actor/api/v1` that fronts the full [OpenRouter](https://openrouter.ai) model catalog. The token usage is billed against the Apify account running the Actor, so no provider API key is required. The Actor authenticates with the proxy using the `APIFY_TOKEN` that the platform injects into every run. To switch models, change the `model` input to any [OpenRouter model slug](https://openrouter.ai/models), for example `openai/gpt-4o-mini`. +## Smolagents -Alternatively, you can call a provider such as OpenAI, Anthropic, or Google directly with your own API key. Keep the key out of the Actor input and source code. Read it from an environment variable, which on the platform you set as a [secret environment variable](https://docs.apify.com/platform/actors/development/programming-interface/environment-variables) and locally you export in your shell. +[Smolagents](https://github.com/huggingface/smolagents) is a lightweight framework from Hugging Face whose agents write and run Python code to solve a task. The following Actor runs a `CodeAgent`: + + + {SmolagentsExample} + + +A `CodeAgent` executes the Python code it generates, so run it in the isolated Actor container rather than on your own machine. Pass tools such as `WebSearchTool` in the `tools` list to let it gather information. ## Using Apify Actors as tools @@ -76,7 +123,6 @@ tools = [ApifyActorsTool('apify/instagram-scraper')] For any other framework, call the Actor directly through the Actor's preconfigured API client and read its dataset: ```python -@tool async def scrape_instagram(handle: str) -> list[dict]: """Scrape recent posts from an Instagram profile.""" run_input = {'directUrls': [f'https://www.instagram.com/{handle}/']} @@ -86,67 +132,15 @@ async def scrape_instagram(handle: str) -> list[dict]: For details on calling other Actors, see [Interacting with other Actors](../concepts/interacting-with-other-actors). -## Other frameworks - -The same `async with Actor:` pattern wraps an agent built with any framework. The snippets below show how each one defines an agent. For a full, deployable Actor, start from the matching template. - -[**CrewAI**](https://apify.com/templates/python-crewai) assembles one or more role-playing agents into a crew that works through tasks: - -```python -from crewai import Agent, Crew, Task - -agent = Agent( - role='Social Media Analyst', - goal='Analyze Instagram profiles and summarize the findings.', - backstory='An expert at turning social media data into insights.', - tools=[ApifyActorsTool('apify/instagram-scraper')], - llm='gpt-4o-mini', -) -task = Task(description=query, expected_output='A short report.', agent=agent) -result = (await Crew(agents=[agent], tasks=[task]).kickoff_async()).raw -``` - -[**LangGraph**](https://apify.com/templates/python-langgraph) builds an agent as a graph with explicit state, which makes complex control flow easy to follow: - -```python -from langchain.agents import create_agent -from langchain_openai import ChatOpenAI - -agent = create_agent(ChatOpenAI(model='gpt-4o-mini'), tools=[scrape_instagram]) -result = await agent.ainvoke({'messages': [('user', query)]}) -``` - -[**LlamaIndex**](https://apify.com/templates/python-llamaindex-agent) shines at retrieval-augmented agents that reason over your own data: - -```python -from llama_index.core.agent import ReActAgent -from llama_index.core.tools import FunctionTool -from llama_index.llms.openai import OpenAI - -agent = ReActAgent( - tools=[FunctionTool.from_defaults(fn=scrape_instagram)], - llm=OpenAI(model='gpt-4o-mini'), -) -response = await agent.run(user_msg=query) -``` - -[**Smolagents**](https://apify.com/templates/python-smolagents) is a lightweight framework from Hugging Face whose agents write and run Python code to solve a task: - -```python -from smolagents import CodeAgent, OpenAIServerModel, WebSearchTool - -agent = CodeAgent(tools=[WebSearchTool()], model=OpenAIServerModel(model_id='gpt-4o-mini')) -result = agent.run(query) -``` - ## Running on the Apify platform -Agents run on the standard [Apify Python base image](https://hub.docker.com/r/apify/actor-python), so no browser or extra system dependencies are needed. Add `apify` and your framework to `requirements.txt`, for example: +Agents run on the standard [Apify Python base image](https://hub.docker.com/r/apify/actor-python), so no browser or extra system dependencies are needed. Add `apify` and your framework's packages to `requirements.txt`: -```text -apify -pydantic-ai -``` +- PydanticAI: `pydantic-ai` +- CrewAI: `crewai` (or `crewai[tools]` for `ApifyActorsTool`) +- LangGraph: `langchain`, `langchain-openai`, `langgraph` +- LlamaIndex: `llama-index`, `llama-index-llms-openai-like` +- Smolagents: `smolagents[openai]` To monetize the agent, use [pay-per-event charging](../concepts/pay-per-event). You define events such as `task-completed` in the Actor's monetization settings and trigger them from the code: @@ -158,7 +152,7 @@ This lets you charge users directly from the Actor and cover the cost of executi ## Conclusion -In this guide, you learned how to host an AI agent as an Apify Actor. You can now run an agent built with PydanticAI, CrewAI, LangGraph, LlamaIndex, or Smolagents, connect it to an LLM through the Apify OpenRouter proxy, give it Apify Actors as tools, and monetize it with pay-per-event. To get started, see the [Actor templates](https://apify.com/templates/categories/python). If you have questions or need assistance, feel free to reach out on our [GitHub](https://github.com/apify/apify-sdk-python) or join our [Discord community](https://discord.com/invite/jyEM2PRvMU). Happy building! +In this guide, you learned how to host an AI agent as an Apify Actor. You can now build an agent with PydanticAI, CrewAI, LangGraph, LlamaIndex, or Smolagents, connect it to an LLM through the Apify OpenRouter proxy, give it Apify Actors as tools, and monetize it with pay-per-event. To get started, see the [Actor templates](https://apify.com/templates/categories/python). If you have questions or need assistance, feel free to reach out on our [GitHub](https://github.com/apify/apify-sdk-python) or join our [Discord community](https://discord.com/invite/jyEM2PRvMU). Happy building! ## Additional resources diff --git a/docs/03_guides/code/13_ai_agents.py b/docs/03_guides/code/13_ai_agents.py deleted file mode 100644 index fa00494f..00000000 --- a/docs/03_guides/code/13_ai_agents.py +++ /dev/null @@ -1,50 +0,0 @@ -import asyncio -import os - -from pydantic_ai import Agent -from pydantic_ai.models.openai import OpenAIChatModel -from pydantic_ai.providers.openai import OpenAIProvider - -from apify import Actor - -# The agent reaches its LLM through the Apify OpenRouter proxy, an -# OpenAI-compatible endpoint billed against the run's Apify account. It -# authenticates with the `APIFY_TOKEN` that the platform injects into every run, -# so no separate provider API key (such as `OPENAI_API_KEY`) is needed. -OPENROUTER_BASE_URL = 'https://openrouter.apify.actor/api/v1' - -DEFAULT_PROMPT = 'In two sentences, explain what an Apify Actor is.' - - -def build_agent(model_name: str) -> Agent[None, str]: - """Build a PydanticAI agent that routes LLM calls through Apify OpenRouter.""" - provider = OpenAIProvider( - base_url=OPENROUTER_BASE_URL, - api_key=os.environ['APIFY_TOKEN'], - ) - return Agent( - OpenAIChatModel(model_name, provider=provider), - output_type=str, - system_prompt='You are a concise, helpful research assistant.', - ) - - -async def main() -> None: - async with Actor: - # Read the Actor input. - actor_input = await Actor.get_input() or {} - prompt = actor_input.get('prompt', DEFAULT_PROMPT) - model_name = actor_input.get('model', 'openai/gpt-4o-mini') - - # Build the agent and run it for a single prompt. - Actor.log.info(f'Running the agent (model={model_name}) for: {prompt}') - agent = build_agent(model_name) - result = await agent.run(user_prompt=prompt) - - # Store the agent's answer in the default dataset. - Actor.log.info(f'The agent responded:\n{result.output}') - await Actor.push_data({'prompt': prompt, 'response': result.output}) - - -if __name__ == '__main__': - asyncio.run(main()) diff --git a/docs/03_guides/code/13_crewai.py b/docs/03_guides/code/13_crewai.py new file mode 100644 index 00000000..04949b84 --- /dev/null +++ b/docs/03_guides/code/13_crewai.py @@ -0,0 +1,34 @@ +import asyncio + +from crewai import Agent, Crew, Task + +from apify import Actor + + +async def main() -> None: + async with Actor: + actor_input = await Actor.get_input() or {} + topic = actor_input.get('topic', 'the Apify platform') + model = actor_input.get('model', 'gpt-4o-mini') + + # CrewAI calls the LLM through LiteLLM, which reads OPENAI_API_KEY. + analyst = Agent( + role='Research Analyst', + goal=f'Write a short, accurate summary about {topic}.', + backstory='An analyst who turns a topic into a concise brief.', + llm=model, + ) + task = Task( + description=f'Write a three-sentence summary about {topic}.', + expected_output='A three-sentence summary.', + agent=analyst, + ) + + # `kickoff_async` keeps the Actor's event loop responsive. + result = await Crew(agents=[analyst], tasks=[task]).kickoff_async() + Actor.log.info(f'Crew result:\n{result.raw}') + await Actor.push_data({'topic': topic, 'summary': result.raw}) + + +if __name__ == '__main__': + asyncio.run(main()) diff --git a/docs/03_guides/code/13_langgraph.py b/docs/03_guides/code/13_langgraph.py new file mode 100644 index 00000000..64d4ec0b --- /dev/null +++ b/docs/03_guides/code/13_langgraph.py @@ -0,0 +1,33 @@ +import asyncio +import os + +from langchain.agents import create_agent +from langchain_openai import ChatOpenAI + +from apify import Actor + +OPENROUTER_BASE_URL = 'https://openrouter.apify.actor/api/v1' + + +async def main() -> None: + async with Actor: + actor_input = await Actor.get_input() or {} + query = actor_input.get('query', 'What is an Apify Actor?') + model = actor_input.get('model', 'openai/gpt-4o-mini') + + # Route the LLM through the Apify OpenRouter proxy (no provider key needed). + llm = ChatOpenAI( + model=model, + base_url=OPENROUTER_BASE_URL, + api_key=os.environ['APIFY_TOKEN'], + ) + agent = create_agent(llm, tools=[]) + + result = await agent.ainvoke({'messages': [('user', query)]}) + answer = result['messages'][-1].content + Actor.log.info(f'Agent answer:\n{answer}') + await Actor.push_data({'query': query, 'answer': answer}) + + +if __name__ == '__main__': + asyncio.run(main()) diff --git a/docs/03_guides/code/13_llamaindex.py b/docs/03_guides/code/13_llamaindex.py new file mode 100644 index 00000000..ed75901b --- /dev/null +++ b/docs/03_guides/code/13_llamaindex.py @@ -0,0 +1,40 @@ +import asyncio +import os + +from llama_index.core.agent import ReActAgent +from llama_index.core.tools import FunctionTool +from llama_index.llms.openai_like import OpenAILike + +from apify import Actor + +OPENROUTER_BASE_URL = 'https://openrouter.apify.actor/api/v1' + + +def word_count(text: str) -> int: + """Return the number of words in the given text.""" + return len(text.split()) + + +async def main() -> None: + async with Actor: + actor_input = await Actor.get_input() or {} + query = actor_input.get('query', 'How many words are in "Apify runs Actors"?') + model = actor_input.get('model', 'openai/gpt-4o-mini') + + # Route the LLM through the Apify OpenRouter proxy (no provider key needed). + # `OpenAILike` is the LlamaIndex class for OpenAI-compatible endpoints. + llm = OpenAILike( + model=model, + api_base=OPENROUTER_BASE_URL, + api_key=os.environ['APIFY_TOKEN'], + is_chat_model=True, + ) + agent = ReActAgent(tools=[FunctionTool.from_defaults(fn=word_count)], llm=llm) + + response = await agent.run(user_msg=query) + Actor.log.info(f'Agent answer:\n{response}') + await Actor.push_data({'query': query, 'answer': str(response)}) + + +if __name__ == '__main__': + asyncio.run(main()) diff --git a/docs/03_guides/code/13_pydanticai.py b/docs/03_guides/code/13_pydanticai.py new file mode 100644 index 00000000..415edfcd --- /dev/null +++ b/docs/03_guides/code/13_pydanticai.py @@ -0,0 +1,36 @@ +import asyncio +import os + +from pydantic_ai import Agent +from pydantic_ai.models.openai import OpenAIChatModel +from pydantic_ai.providers.openai import OpenAIProvider + +from apify import Actor + +OPENROUTER_BASE_URL = 'https://openrouter.apify.actor/api/v1' + + +async def main() -> None: + async with Actor: + actor_input = await Actor.get_input() or {} + prompt = actor_input.get('prompt', 'Explain Apify Actors in two sentences.') + model = actor_input.get('model', 'openai/gpt-4o-mini') + + # Route the LLM through the Apify OpenRouter proxy (no provider key needed). + provider = OpenAIProvider( + base_url=OPENROUTER_BASE_URL, + api_key=os.environ['APIFY_TOKEN'], + ) + agent = Agent( + OpenAIChatModel(model, provider=provider), + output_type=str, + system_prompt='You are a concise, helpful assistant.', + ) + + result = await agent.run(user_prompt=prompt) + Actor.log.info(f'Agent response:\n{result.output}') + await Actor.push_data({'prompt': prompt, 'response': result.output}) + + +if __name__ == '__main__': + asyncio.run(main()) diff --git a/docs/03_guides/code/13_smolagents.py b/docs/03_guides/code/13_smolagents.py new file mode 100644 index 00000000..19dc0e00 --- /dev/null +++ b/docs/03_guides/code/13_smolagents.py @@ -0,0 +1,32 @@ +import asyncio +import os + +from smolagents import CodeAgent, OpenAIServerModel + +from apify import Actor + +OPENROUTER_BASE_URL = 'https://openrouter.apify.actor/api/v1' + + +async def main() -> None: + async with Actor: + actor_input = await Actor.get_input() or {} + task = actor_input.get('task', 'Compute the 12th Fibonacci number.') + model = actor_input.get('model', 'openai/gpt-4o-mini') + + # Route the LLM through the Apify OpenRouter proxy (no provider key needed). + llm = OpenAIServerModel( + model_id=model, + api_base=OPENROUTER_BASE_URL, + api_key=os.environ['APIFY_TOKEN'], + ) + # A `CodeAgent` writes and runs Python code to solve the task. + agent = CodeAgent(tools=[], model=llm) + + result = agent.run(task) + Actor.log.info(f'Agent result:\n{result}') + await Actor.push_data({'task': task, 'result': str(result)}) + + +if __name__ == '__main__': + asyncio.run(main()) From 622a72003da64fec5b42a90e76943ca4b9e1b3b6 Mon Sep 17 00:00:00 2001 From: Vlada Dusek Date: Fri, 3 Jul 2026 13:30:24 +0200 Subject: [PATCH 03/10] docs: align AI agents guide examples with their Actor templates --- docs/03_guides/13_ai_agents.mdx | 32 +++++++++++++++++----------- docs/03_guides/code/13_crewai.py | 27 +++++++++++++++++------ docs/03_guides/code/13_langgraph.py | 12 +++++++++-- docs/03_guides/code/13_pydanticai.py | 23 ++++++++++++++------ docs/03_guides/code/13_smolagents.py | 15 +++++++------ 5 files changed, 74 insertions(+), 35 deletions(-) diff --git a/docs/03_guides/13_ai_agents.mdx b/docs/03_guides/13_ai_agents.mdx index 3c0c8a06..570e8224 100644 --- a/docs/03_guides/13_ai_agents.mdx +++ b/docs/03_guides/13_ai_agents.mdx @@ -4,10 +4,11 @@ title: Building AI agents description: Host AI agents built with PydanticAI, CrewAI, LangGraph, LlamaIndex, or Smolagents as Apify Actors. --- +import CodeBlock from '@theme/CodeBlock'; import RunnableCodeBlock from '@site/src/components/RunnableCodeBlock'; import PydanticaiExample from '!!raw-loader!roa-loader!./code/13_pydanticai.py'; -import CrewaiExample from '!!raw-loader!roa-loader!./code/13_crewai.py'; +import CrewaiExample from '!!raw-loader!./code/13_crewai.py'; import LanggraphExample from '!!raw-loader!roa-loader!./code/13_langgraph.py'; import LlamaindexExample from '!!raw-loader!roa-loader!./code/13_llamaindex.py'; import SmolagentsExample from '!!raw-loader!roa-loader!./code/13_smolagents.py'; @@ -52,37 +53,39 @@ Each section below shows a complete, single-file Actor for one framework. They a ## PydanticAI -[PydanticAI](https://ai.pydantic.dev/) is an agent framework from the team behind Pydantic. It is strongly typed and integrates naturally with the [Pydantic models](./input-validation) the Apify SDK already uses. The following Actor runs an agent for a single prompt: +[PydanticAI](https://ai.pydantic.dev/) is an agent framework from the team behind Pydantic. It's strongly typed and integrates naturally with the [Pydantic models](./input-validation) the Apify SDK already uses. The following Actor generates a joke and returns it as a typed object: {PydanticaiExample} -Set `output_type` to a Pydantic model instead of `str` to get a validated object back, which maps directly onto a dataset row. +The `output_type=Joke` argument makes the agent return a validated `Joke` instance, which maps directly onto a dataset row. Set `output_type=str` instead to get plain text back. ## CrewAI -[CrewAI](https://www.crewai.com/) models a problem as a "crew" of role-playing agents that work through tasks. The following Actor defines a single analyst agent and one task: +[CrewAI](https://www.crewai.com/) models a problem as a "crew" of role-playing agents that work through tasks. The following Actor defines a single analyst agent that uses an Apify Actor as a tool to answer a query: - +{/* Not runnable from the docs: CrewAI reads the LLM key from OPENAI_API_KEY, which the shared example runner doesn't provide. */} + {CrewaiExample} - + Note that: - `kickoff_async` runs the crew without blocking the Actor's event loop. +- `ApifyActorsTool('apify/instagram-scraper')` gives the agent the [Instagram Scraper](https://apify.com/apify/instagram-scraper) as a tool. It comes from the `crewai[tools]` package and reads its token from `APIFY_API_TOKEN`, which the example sets from the platform's `APIFY_TOKEN`. - CrewAI calls the LLM through [LiteLLM](https://docs.litellm.ai/), so this example reads `OPENAI_API_KEY`. To route it through the Apify OpenRouter proxy instead, configure a custom `LLM` (see the [CrewAI LLM docs](https://docs.crewai.com/concepts/llms)). -- On a fresh container, crewAI shows a one-time trace-consent prompt. The template sets `CREWAI_TESTING=true` to suppress it; do the same in your Dockerfile. +- On a fresh container, CrewAI shows a one-time trace-consent prompt. The template sets `CREWAI_TESTING=true` to suppress it. Do the same in your Dockerfile. ## LangGraph -[LangGraph](https://www.langchain.com/langgraph) builds an agent as a graph with explicit state, which makes complex, multi-step control flow easy to follow. It builds on [LangChain](https://www.langchain.com/), so any LangChain chat model and tool works. The following Actor runs a single-turn agent: +[LangGraph](https://www.langchain.com/langgraph) builds an agent as a graph with explicit state, which makes complex, multi-step control flow easy to follow. It builds on [LangChain](https://www.langchain.com/), so any LangChain chat model and tool works. The following Actor runs an agent with a single tool: {LanggraphExample} -The example passes an empty `tools` list for brevity. Add LangChain tools to give the agent abilities, and read the final answer from the last message in the returned state. +The agent decides on its own when to call the `sum_numbers` tool, and the answer is read from the last message in the returned state. Add more LangChain tools to extend it, including Apify Actors (see [Using Apify Actors as tools](#using-apify-actors-as-tools)). ## LlamaIndex @@ -96,6 +99,7 @@ Note that: - `OpenAILike` is the LlamaIndex LLM class for OpenAI-compatible endpoints such as the Apify OpenRouter proxy. It needs the `llama-index-llms-openai-like` package. - The agent decides on its own when to call the `word_count` tool. Add `FunctionTool`s of your own to extend it. +- To reason over your own data, wrap an Apify Actor in a `FunctionTool` (see [Using Apify Actors as tools](#using-apify-actors-as-tools)), as the [LlamaIndex template](https://apify.com/templates/python-llamaindex-agent) does with a contact-details scraper. ## Smolagents @@ -105,21 +109,23 @@ Note that: {SmolagentsExample} -A `CodeAgent` executes the Python code it generates, so run it in the isolated Actor container rather than on your own machine. Pass tools such as `WebSearchTool` in the `tools` list to let it gather information. +This agent uses `WebSearchTool` to fetch the latest news, then writes and runs Python code to summarize it. Because a `CodeAgent` executes the code it generates, run it in the isolated Actor container rather than on your own machine. ## Using Apify Actors as tools An agent becomes useful once it can act on the world. On Apify, any of the thousands of Actors in the [Apify Store](https://apify.com/store) can become a tool: a scraper that fetches data, a browser automation that fills a form, or an API wrapper that posts a message. -For LangChain-based frameworks, such as LangGraph and CrewAI, the [`langchain-apify`](https://github.com/apify/langchain-apify) package exposes any Actor as a tool with a single line: +Some frameworks ship a ready-made wrapper for this. For LangGraph and other [LangChain](https://www.langchain.com/)-based agents, the [`langchain-apify`](https://github.com/apify/langchain-apify) package exposes any Actor as a tool with a single line: ```python -from crewai_tools import ApifyActorsTool +from langchain_apify import ApifyActorsTool # Let the agent scrape Instagram profiles by calling the apify/instagram-scraper Actor. tools = [ApifyActorsTool('apify/instagram-scraper')] ``` +CrewAI ships the same tool as `crewai_tools.ApifyActorsTool`, as the [CrewAI example](#crewai) above shows. + For any other framework, call the Actor directly through the Actor's preconfigured API client and read its dataset: ```python @@ -137,7 +143,7 @@ For details on calling other Actors, see [Interacting with other Actors](../conc Agents run on the standard [Apify Python base image](https://hub.docker.com/r/apify/actor-python), so no browser or extra system dependencies are needed. Add `apify` and your framework's packages to `requirements.txt`: - PydanticAI: `pydantic-ai` -- CrewAI: `crewai` (or `crewai[tools]` for `ApifyActorsTool`) +- CrewAI: `crewai[tools]` (the `tools` extra provides `ApifyActorsTool`) - LangGraph: `langchain`, `langchain-openai`, `langgraph` - LlamaIndex: `llama-index`, `llama-index-llms-openai-like` - Smolagents: `smolagents[openai]` diff --git a/docs/03_guides/code/13_crewai.py b/docs/03_guides/code/13_crewai.py index 04949b84..546f9813 100644 --- a/docs/03_guides/code/13_crewai.py +++ b/docs/03_guides/code/13_crewai.py @@ -1,33 +1,46 @@ import asyncio +import os from crewai import Agent, Crew, Task +from crewai_tools import ApifyActorsTool from apify import Actor +# On a fresh container, CrewAI shows a one-time trace-consent prompt that blocks on +# stdin. `CREWAI_TESTING=true` is the only flag that suppresses it. +os.environ.setdefault('CREWAI_TESTING', 'true') + async def main() -> None: async with Actor: actor_input = await Actor.get_input() or {} - topic = actor_input.get('topic', 'the Apify platform') + query = actor_input.get( + 'query', 'Summarize the latest posts on the @openai Instagram profile.' + ) model = actor_input.get('model', 'gpt-4o-mini') # CrewAI calls the LLM through LiteLLM, which reads OPENAI_API_KEY. + # `ApifyActorsTool` reads APIFY_API_TOKEN. The platform injects APIFY_TOKEN. + os.environ.setdefault('APIFY_API_TOKEN', os.environ['APIFY_TOKEN']) + + # The agent can run any Apify Actor as a tool, here the Instagram scraper. analyst = Agent( - role='Research Analyst', - goal=f'Write a short, accurate summary about {topic}.', - backstory='An analyst who turns a topic into a concise brief.', + role='Social Media Analyst', + goal='Analyze social media profiles and summarize the findings.', + backstory='An analyst who turns raw social media data into concise insights.', + tools=[ApifyActorsTool('apify/instagram-scraper')], llm=model, ) task = Task( - description=f'Write a three-sentence summary about {topic}.', - expected_output='A three-sentence summary.', + description=query, + expected_output='A short, readable summary that answers the query.', agent=analyst, ) # `kickoff_async` keeps the Actor's event loop responsive. result = await Crew(agents=[analyst], tasks=[task]).kickoff_async() Actor.log.info(f'Crew result:\n{result.raw}') - await Actor.push_data({'topic': topic, 'summary': result.raw}) + await Actor.push_data({'query': query, 'summary': result.raw}) if __name__ == '__main__': diff --git a/docs/03_guides/code/13_langgraph.py b/docs/03_guides/code/13_langgraph.py index 64d4ec0b..0d990daa 100644 --- a/docs/03_guides/code/13_langgraph.py +++ b/docs/03_guides/code/13_langgraph.py @@ -2,6 +2,7 @@ import os from langchain.agents import create_agent +from langchain_core.tools import tool from langchain_openai import ChatOpenAI from apify import Actor @@ -9,10 +10,16 @@ OPENROUTER_BASE_URL = 'https://openrouter.apify.actor/api/v1' +@tool +def sum_numbers(numbers: list[int]) -> int: + """Return the sum of a list of numbers.""" + return sum(numbers) + + async def main() -> None: async with Actor: actor_input = await Actor.get_input() or {} - query = actor_input.get('query', 'What is an Apify Actor?') + query = actor_input.get('query', 'What is the sum of 128, 64, and 32?') model = actor_input.get('model', 'openai/gpt-4o-mini') # Route the LLM through the Apify OpenRouter proxy (no provider key needed). @@ -21,7 +28,8 @@ async def main() -> None: base_url=OPENROUTER_BASE_URL, api_key=os.environ['APIFY_TOKEN'], ) - agent = create_agent(llm, tools=[]) + # The agent decides on its own when to call the `sum_numbers` tool. + agent = create_agent(llm, tools=[sum_numbers]) result = await agent.ainvoke({'messages': [('user', query)]}) answer = result['messages'][-1].content diff --git a/docs/03_guides/code/13_pydanticai.py b/docs/03_guides/code/13_pydanticai.py index 415edfcd..083da4b5 100644 --- a/docs/03_guides/code/13_pydanticai.py +++ b/docs/03_guides/code/13_pydanticai.py @@ -1,6 +1,7 @@ import asyncio import os +from pydantic import BaseModel from pydantic_ai import Agent from pydantic_ai.models.openai import OpenAIChatModel from pydantic_ai.providers.openai import OpenAIProvider @@ -10,10 +11,17 @@ OPENROUTER_BASE_URL = 'https://openrouter.apify.actor/api/v1' +class Joke(BaseModel): + """The agent's typed output: a joke split into its setup and punchline.""" + + setup: str + punchline: str + + async def main() -> None: async with Actor: actor_input = await Actor.get_input() or {} - prompt = actor_input.get('prompt', 'Explain Apify Actors in two sentences.') + topic = actor_input.get('topic', 'bad weather') model = actor_input.get('model', 'openai/gpt-4o-mini') # Route the LLM through the Apify OpenRouter proxy (no provider key needed). @@ -21,15 +29,18 @@ async def main() -> None: base_url=OPENROUTER_BASE_URL, api_key=os.environ['APIFY_TOKEN'], ) + # `output_type=Joke` makes the agent return a validated `Joke` instance. agent = Agent( OpenAIChatModel(model, provider=provider), - output_type=str, - system_prompt='You are a concise, helpful assistant.', + output_type=Joke, + system_prompt='You are a witty comedian. Write a single short joke.', ) - result = await agent.run(user_prompt=prompt) - Actor.log.info(f'Agent response:\n{result.output}') - await Actor.push_data({'prompt': prompt, 'response': result.output}) + joke = (await agent.run(user_prompt=f'Tell me a joke about {topic}.')).output + Actor.log.info(f'Joke:\n{joke.setup}\n{joke.punchline}') + await Actor.push_data( + {'topic': topic, 'setup': joke.setup, 'punchline': joke.punchline} + ) if __name__ == '__main__': diff --git a/docs/03_guides/code/13_smolagents.py b/docs/03_guides/code/13_smolagents.py index 19dc0e00..f5d29d31 100644 --- a/docs/03_guides/code/13_smolagents.py +++ b/docs/03_guides/code/13_smolagents.py @@ -1,7 +1,7 @@ import asyncio import os -from smolagents import CodeAgent, OpenAIServerModel +from smolagents import CodeAgent, OpenAIServerModel, WebSearchTool from apify import Actor @@ -11,7 +11,7 @@ async def main() -> None: async with Actor: actor_input = await Actor.get_input() or {} - task = actor_input.get('task', 'Compute the 12th Fibonacci number.') + topic = actor_input.get('topic', 'open source AI') model = actor_input.get('model', 'openai/gpt-4o-mini') # Route the LLM through the Apify OpenRouter proxy (no provider key needed). @@ -20,12 +20,13 @@ async def main() -> None: api_base=OPENROUTER_BASE_URL, api_key=os.environ['APIFY_TOKEN'], ) - # A `CodeAgent` writes and runs Python code to solve the task. - agent = CodeAgent(tools=[], model=llm) + # A `CodeAgent` writes and runs Python code to solve the task. Here it uses + # `WebSearchTool` to gather the latest news before summarizing it. + agent = CodeAgent(tools=[WebSearchTool()], model=llm) - result = agent.run(task) - Actor.log.info(f'Agent result:\n{result}') - await Actor.push_data({'task': task, 'result': str(result)}) + result = agent.run(f'Find and summarize the latest news about {topic}.') + Actor.log.info(f'Summary:\n{result}') + await Actor.push_data({'topic': topic, 'summary': str(result)}) if __name__ == '__main__': From 817d0e0be8a188ef732145470d3651076d09b880 Mon Sep 17 00:00:00 2001 From: Vlada Dusek Date: Fri, 3 Jul 2026 17:04:06 +0200 Subject: [PATCH 04/10] docs: refine AI agents guide and renumber to 14 --- docs/01_introduction/quick-start.mdx | 2 +- .../{13_ai_agents.mdx => 14_ai_agents.mdx} | 107 ++++++++++++------ .../code/{13_crewai.py => 14_crewai.py} | 0 .../code/{13_langgraph.py => 14_langgraph.py} | 0 .../{13_llamaindex.py => 14_llamaindex.py} | 0 .../{13_pydanticai.py => 14_pydanticai.py} | 0 .../{13_smolagents.py => 14_smolagents.py} | 0 7 files changed, 72 insertions(+), 37 deletions(-) rename docs/03_guides/{13_ai_agents.mdx => 14_ai_agents.mdx} (75%) rename docs/03_guides/code/{13_crewai.py => 14_crewai.py} (100%) rename docs/03_guides/code/{13_langgraph.py => 14_langgraph.py} (100%) rename docs/03_guides/code/{13_llamaindex.py => 14_llamaindex.py} (100%) rename docs/03_guides/code/{13_pydanticai.py => 14_pydanticai.py} (100%) rename docs/03_guides/code/{13_smolagents.py => 14_smolagents.py} (100%) diff --git a/docs/01_introduction/quick-start.mdx b/docs/01_introduction/quick-start.mdx index 19231b81..9bb53038 100644 --- a/docs/01_introduction/quick-start.mdx +++ b/docs/01_introduction/quick-start.mdx @@ -112,7 +112,7 @@ To see how you can integrate the Apify SDK with popular scraping libraries and f - [Adaptive scraping with Scrapling](./guides/scrapling) - [LLM-ready scraping with Crawl4AI](./guides/crawl4ai) - [Browser AI agents with Browser Use](./guides/browser-use) -- [Building AI agents](./guides/ai-agents) +- [Hosting AI agents](./guides/ai-agents) For other aspects of Actor development, explore these guides: diff --git a/docs/03_guides/13_ai_agents.mdx b/docs/03_guides/14_ai_agents.mdx similarity index 75% rename from docs/03_guides/13_ai_agents.mdx rename to docs/03_guides/14_ai_agents.mdx index 570e8224..5c2a3bfb 100644 --- a/docs/03_guides/13_ai_agents.mdx +++ b/docs/03_guides/14_ai_agents.mdx @@ -1,17 +1,17 @@ --- id: ai-agents -title: Building AI agents -description: Host AI agents built with PydanticAI, CrewAI, LangGraph, LlamaIndex, or Smolagents as Apify Actors. +title: Hosting AI agents +description: Host AI agents built with LangGraph, CrewAI, PydanticAI, LlamaIndex, or Smolagents as Apify Actors. --- import CodeBlock from '@theme/CodeBlock'; import RunnableCodeBlock from '@site/src/components/RunnableCodeBlock'; -import PydanticaiExample from '!!raw-loader!roa-loader!./code/13_pydanticai.py'; -import CrewaiExample from '!!raw-loader!./code/13_crewai.py'; -import LanggraphExample from '!!raw-loader!roa-loader!./code/13_langgraph.py'; -import LlamaindexExample from '!!raw-loader!roa-loader!./code/13_llamaindex.py'; -import SmolagentsExample from '!!raw-loader!roa-loader!./code/13_smolagents.py'; +import LanggraphExample from '!!raw-loader!roa-loader!./code/14_langgraph.py'; +import CrewaiExample from '!!raw-loader!./code/14_crewai.py'; +import PydanticaiExample from '!!raw-loader!roa-loader!./code/14_pydanticai.py'; +import LlamaindexExample from '!!raw-loader!roa-loader!./code/14_llamaindex.py'; +import SmolagentsExample from '!!raw-loader!roa-loader!./code/14_smolagents.py'; In this guide, you'll learn how to host an AI agent as an Apify Actor, using the agent framework of your choice. @@ -32,10 +32,10 @@ Apify Actors are a natural home for agents: Apify maintains a ready-made Actor template for each of the popular agent frameworks. Each template is a complete, deployable Actor that you can use as a starting point. Pick the one that matches the framework you already use, or the style of agent you want to build: | Framework | Good for | Template | -|---|---|---| -| [PydanticAI](#pydanticai) | Typed, Pydantic-native agents with tool calling | [`python-pydanticai`](https://apify.com/templates/python-pydanticai) | -| [CrewAI](#crewai) | Multi-agent "crews" that collaborate on a task | [`python-crewai`](https://apify.com/templates/python-crewai) | +| --- | --- | --- | | [LangGraph](#langgraph) | Graph-based agents with explicit state and control flow | [`python-langgraph`](https://apify.com/templates/python-langgraph) | +| [CrewAI](#crewai) | Multi-agent "crews" that collaborate on a task | [`python-crewai`](https://apify.com/templates/python-crewai) | +| [PydanticAI](#pydanticai) | Typed, Pydantic-native agents with tool calling | [`python-pydanticai`](https://apify.com/templates/python-pydanticai) | | [LlamaIndex](#llamaindex) | Retrieval-augmented agents over your own data | [`python-llamaindex-agent`](https://apify.com/templates/python-llamaindex-agent) | | [Smolagents](#smolagents) | Lightweight code-writing agents from Hugging Face | [`python-smolagents`](https://apify.com/templates/python-smolagents) | @@ -51,21 +51,37 @@ The second is to call a provider such as OpenAI directly with your own API key, Each section below shows a complete, single-file Actor for one framework. They all read the input, run the agent, and store the result in the default dataset. -## PydanticAI +## LangGraph + +[LangGraph](https://www.langchain.com/langgraph) builds an agent as a graph with explicit state, which makes complex, multi-step control flow easy to follow. It builds on [LangChain](https://www.langchain.com/), so any LangChain chat model and tool works. To install it, use: + +```bash +pip install langchain langchain-openai langgraph +``` -[PydanticAI](https://ai.pydantic.dev/) is an agent framework from the team behind Pydantic. It's strongly typed and integrates naturally with the [Pydantic models](./input-validation) the Apify SDK already uses. The following Actor generates a joke and returns it as a typed object: +The following Actor runs an agent with a single tool: - {PydanticaiExample} + {LanggraphExample} -The `output_type=Joke` argument makes the agent return a validated `Joke` instance, which maps directly onto a dataset row. Set `output_type=str` instead to get plain text back. +Note that: + +- `ChatOpenAI` points its `base_url` at the Apify OpenRouter proxy, so any LangChain chat model works without a provider key. +- The agent decides on its own when to call the `sum_numbers` tool, and the answer is read from the last message in the returned state. +- Add more LangChain tools to extend it, including Apify Actors (see [Using Apify Actors as tools](#using-apify-actors-as-tools)). ## CrewAI -[CrewAI](https://www.crewai.com/) models a problem as a "crew" of role-playing agents that work through tasks. The following Actor defines a single analyst agent that uses an Apify Actor as a tool to answer a query: +[CrewAI](https://www.crewai.com/) models a problem as a "crew" of role-playing agents that work through tasks. To install it, use: -{/* Not runnable from the docs: CrewAI reads the LLM key from OPENAI_API_KEY, which the shared example runner doesn't provide. */} +```bash +pip install "crewai[tools]" +``` + +The following Actor defines a single analyst agent that uses an Apify Actor as a tool to answer a query: + +{/*Not runnable from the docs: CrewAI reads the LLM key from OPENAI_API_KEY, which the shared example runner doesn't provide.*/} {CrewaiExample} @@ -77,19 +93,34 @@ Note that: - CrewAI calls the LLM through [LiteLLM](https://docs.litellm.ai/), so this example reads `OPENAI_API_KEY`. To route it through the Apify OpenRouter proxy instead, configure a custom `LLM` (see the [CrewAI LLM docs](https://docs.crewai.com/concepts/llms)). - On a fresh container, CrewAI shows a one-time trace-consent prompt. The template sets `CREWAI_TESTING=true` to suppress it. Do the same in your Dockerfile. -## LangGraph +## PydanticAI + +[PydanticAI](https://ai.pydantic.dev/) is an agent framework from the team behind Pydantic. It's strongly typed and integrates naturally with the [Pydantic models](./input-validation) the Apify SDK already uses. To install it, use: -[LangGraph](https://www.langchain.com/langgraph) builds an agent as a graph with explicit state, which makes complex, multi-step control flow easy to follow. It builds on [LangChain](https://www.langchain.com/), so any LangChain chat model and tool works. The following Actor runs an agent with a single tool: +```bash +pip install pydantic-ai +``` + +The following Actor generates a joke and returns it as a typed object: - {LanggraphExample} + {PydanticaiExample} -The agent decides on its own when to call the `sum_numbers` tool, and the answer is read from the last message in the returned state. Add more LangChain tools to extend it, including Apify Actors (see [Using Apify Actors as tools](#using-apify-actors-as-tools)). +Note that: + +- The agent reaches the LLM through the Apify OpenRouter proxy via `OpenAIChatModel` and a custom `OpenAIProvider`, so no provider API key is needed. +- `output_type=Joke` makes the agent return a validated `Joke` instance, which maps directly onto a dataset row. Set `output_type=str` instead to get plain text back. ## LlamaIndex -[LlamaIndex](https://www.llamaindex.ai/) is built for retrieval-augmented agents that reason over your own data. The following Actor runs a `ReActAgent` with a single tool: +[LlamaIndex](https://www.llamaindex.ai/) is built for retrieval-augmented agents that reason over your own data. To install it, use: + +```bash +pip install llama-index llama-index-llms-openai-like +``` + +The following Actor runs a `ReActAgent` with a single tool: {LlamaindexExample} @@ -103,13 +134,23 @@ Note that: ## Smolagents -[Smolagents](https://github.com/huggingface/smolagents) is a lightweight framework from Hugging Face whose agents write and run Python code to solve a task. The following Actor runs a `CodeAgent`: +[Smolagents](https://github.com/huggingface/smolagents) is a lightweight framework from Hugging Face whose agents write and run Python code to solve a task. To install it, use: + +```bash +pip install "smolagents[openai]" +``` + +The following Actor runs a `CodeAgent`: {SmolagentsExample} -This agent uses `WebSearchTool` to fetch the latest news, then writes and runs Python code to summarize it. Because a `CodeAgent` executes the code it generates, run it in the isolated Actor container rather than on your own machine. +Note that: + +- `OpenAIServerModel` points `api_base` at the Apify OpenRouter proxy, so no provider API key is needed. +- The agent uses `WebSearchTool` to fetch the latest news, then writes and runs Python code to summarize it. +- Because a `CodeAgent` executes the code it generates, run it in the isolated Actor container rather than on your own machine. ## Using Apify Actors as tools @@ -140,13 +181,7 @@ For details on calling other Actors, see [Interacting with other Actors](../conc ## Running on the Apify platform -Agents run on the standard [Apify Python base image](https://hub.docker.com/r/apify/actor-python), so no browser or extra system dependencies are needed. Add `apify` and your framework's packages to `requirements.txt`: - -- PydanticAI: `pydantic-ai` -- CrewAI: `crewai[tools]` (the `tools` extra provides `ApifyActorsTool`) -- LangGraph: `langchain`, `langchain-openai`, `langgraph` -- LlamaIndex: `llama-index`, `llama-index-llms-openai-like` -- Smolagents: `smolagents[openai]` +Agents run on the standard [Apify Python base image](https://hub.docker.com/r/apify/actor-python), so no browser or extra system dependencies are needed. Add `apify` and the packages shown in your framework's section above to `requirements.txt`, then deploy the Actor as usual. To monetize the agent, use [pay-per-event charging](../concepts/pay-per-event). You define events such as `task-completed` in the Actor's monetization settings and trigger them from the code: @@ -158,15 +193,15 @@ This lets you charge users directly from the Actor and cover the cost of executi ## Conclusion -In this guide, you learned how to host an AI agent as an Apify Actor. You can now build an agent with PydanticAI, CrewAI, LangGraph, LlamaIndex, or Smolagents, connect it to an LLM through the Apify OpenRouter proxy, give it Apify Actors as tools, and monetize it with pay-per-event. To get started, see the [Actor templates](https://apify.com/templates/categories/python). If you have questions or need assistance, feel free to reach out on our [GitHub](https://github.com/apify/apify-sdk-python) or join our [Discord community](https://discord.com/invite/jyEM2PRvMU). Happy building! +In this guide, you learned how to host an AI agent as an Apify Actor. You can now build an agent with LangGraph, CrewAI, PydanticAI, LlamaIndex, or Smolagents, connect it to an LLM through the Apify OpenRouter proxy, give it Apify Actors as tools, and monetize it with pay-per-event. To get started, see the [Actor templates](https://apify.com/templates/categories/python). If you have questions or need assistance, feel free to reach out on our [GitHub](https://github.com/apify/apify-sdk-python) or join our [Discord community](https://discord.com/invite/jyEM2PRvMU). Happy building! ## Additional resources -- [Apify templates: PydanticAI](https://apify.com/templates/python-pydanticai) -- [Apify templates: CrewAI](https://apify.com/templates/python-crewai) -- [Apify templates: LangGraph](https://apify.com/templates/python-langgraph) -- [Apify templates: LlamaIndex](https://apify.com/templates/python-llamaindex-agent) -- [Apify templates: Smolagents](https://apify.com/templates/python-smolagents) +- [Apify: LangGraph template](https://apify.com/templates/python-langgraph) +- [Apify: CrewAI template](https://apify.com/templates/python-crewai) +- [Apify: PydanticAI template](https://apify.com/templates/python-pydanticai) +- [Apify: LlamaIndex template](https://apify.com/templates/python-llamaindex-agent) +- [Apify: Smolagents template](https://apify.com/templates/python-smolagents) - [Apify: actor-templates repository](https://github.com/apify/actor-templates) - [Apify: OpenRouter proxy](https://apify.com/apify/openrouter) - [Apify: langchain-apify integration](https://github.com/apify/langchain-apify) diff --git a/docs/03_guides/code/13_crewai.py b/docs/03_guides/code/14_crewai.py similarity index 100% rename from docs/03_guides/code/13_crewai.py rename to docs/03_guides/code/14_crewai.py diff --git a/docs/03_guides/code/13_langgraph.py b/docs/03_guides/code/14_langgraph.py similarity index 100% rename from docs/03_guides/code/13_langgraph.py rename to docs/03_guides/code/14_langgraph.py diff --git a/docs/03_guides/code/13_llamaindex.py b/docs/03_guides/code/14_llamaindex.py similarity index 100% rename from docs/03_guides/code/13_llamaindex.py rename to docs/03_guides/code/14_llamaindex.py diff --git a/docs/03_guides/code/13_pydanticai.py b/docs/03_guides/code/14_pydanticai.py similarity index 100% rename from docs/03_guides/code/13_pydanticai.py rename to docs/03_guides/code/14_pydanticai.py diff --git a/docs/03_guides/code/13_smolagents.py b/docs/03_guides/code/14_smolagents.py similarity index 100% rename from docs/03_guides/code/13_smolagents.py rename to docs/03_guides/code/14_smolagents.py From 24096337a3b16c09ae89a7183c57252642880d32 Mon Sep 17 00:00:00 2001 From: Vlada Dusek Date: Fri, 3 Jul 2026 17:23:39 +0200 Subject: [PATCH 05/10] docs: route the CrewAI agent example through the OpenRouter proxy --- docs/03_guides/14_ai_agents.mdx | 24 ++++++++--------- docs/03_guides/code/14_crewai.py | 45 ++++++++++++++++++++------------ 2 files changed, 39 insertions(+), 30 deletions(-) diff --git a/docs/03_guides/14_ai_agents.mdx b/docs/03_guides/14_ai_agents.mdx index 5c2a3bfb..e055dec3 100644 --- a/docs/03_guides/14_ai_agents.mdx +++ b/docs/03_guides/14_ai_agents.mdx @@ -4,11 +4,10 @@ title: Hosting AI agents description: Host AI agents built with LangGraph, CrewAI, PydanticAI, LlamaIndex, or Smolagents as Apify Actors. --- -import CodeBlock from '@theme/CodeBlock'; import RunnableCodeBlock from '@site/src/components/RunnableCodeBlock'; import LanggraphExample from '!!raw-loader!roa-loader!./code/14_langgraph.py'; -import CrewaiExample from '!!raw-loader!./code/14_crewai.py'; +import CrewaiExample from '!!raw-loader!roa-loader!./code/14_crewai.py'; import PydanticaiExample from '!!raw-loader!roa-loader!./code/14_pydanticai.py'; import LlamaindexExample from '!!raw-loader!roa-loader!./code/14_llamaindex.py'; import SmolagentsExample from '!!raw-loader!roa-loader!./code/14_smolagents.py'; @@ -37,7 +36,7 @@ Apify maintains a ready-made Actor template for each of the popular agent framew | [CrewAI](#crewai) | Multi-agent "crews" that collaborate on a task | [`python-crewai`](https://apify.com/templates/python-crewai) | | [PydanticAI](#pydanticai) | Typed, Pydantic-native agents with tool calling | [`python-pydanticai`](https://apify.com/templates/python-pydanticai) | | [LlamaIndex](#llamaindex) | Retrieval-augmented agents over your own data | [`python-llamaindex-agent`](https://apify.com/templates/python-llamaindex-agent) | -| [Smolagents](#smolagents) | Lightweight code-writing agents from Hugging Face | [`python-smolagents`](https://apify.com/templates/python-smolagents) | +| [Smolagents](#smolagents) | Lightweight code-writing agents | [`python-smolagents`](https://apify.com/templates/python-smolagents) | All of these templates live in the [actor-templates repository](https://github.com/apify/actor-templates), and you can scaffold any of them with the [Apify CLI](https://docs.apify.com/cli), for example `apify create my-agent --template python-pydanticai`. @@ -45,9 +44,9 @@ All of these templates live in the [actor-templates repository](https://github.c Every agent needs an LLM, and there are two ways to provide one. -The first is the [Apify OpenRouter proxy](https://apify.com/apify/openrouter), an OpenAI-compatible endpoint at `https://openrouter.apify.actor/api/v1` that fronts the full [OpenRouter](https://openrouter.ai) model catalog. The token usage is billed against the Apify account running the Actor, so no provider API key is required. The Actor authenticates with the proxy using the `APIFY_TOKEN` that the platform injects into every run. Any framework with an OpenAI-compatible client can point its base URL at the proxy, which is what most examples below do. To switch models, change the `model` input to any [OpenRouter model slug](https://openrouter.ai/models), for example `openai/gpt-4o-mini`. +The first is the [Apify OpenRouter proxy](https://apify.com/apify/openrouter), an OpenAI-compatible endpoint at `https://openrouter.apify.actor/api/v1` that fronts the full [OpenRouter](https://openrouter.ai) model catalog. The token usage is billed against the Apify account running the Actor, so no provider API key is required. The Actor authenticates with the proxy using the `APIFY_TOKEN` that the platform injects into every run. Any framework with an OpenAI-compatible client can point its base URL at the proxy, which is what every example below does. To switch models, change the `model` input to any [OpenRouter model slug](https://openrouter.ai/models), for example `openai/gpt-4o-mini`. -The second is to call a provider such as OpenAI directly with your own API key, which the CrewAI example below does through LiteLLM. Keep the key out of the Actor input and source code. Read it from an environment variable, which on the platform you set as a [secret environment variable](https://docs.apify.com/platform/actors/development/programming-interface/environment-variables) and locally you export in your shell. +The second is to call a provider such as OpenAI directly with your own API key. Keep the key out of the Actor input and source code. Read it from an environment variable, which on the platform you set as a [secret environment variable](https://docs.apify.com/platform/actors/development/programming-interface/environment-variables) and locally you export in your shell. Each section below shows a complete, single-file Actor for one framework. They all read the input, run the agent, and store the result in the default dataset. @@ -76,21 +75,20 @@ Note that: [CrewAI](https://www.crewai.com/) models a problem as a "crew" of role-playing agents that work through tasks. To install it, use: ```bash -pip install "crewai[tools]" +pip install crewai ``` -The following Actor defines a single analyst agent that uses an Apify Actor as a tool to answer a query: +The following Actor defines a single analyst agent with one tool: -{/*Not runnable from the docs: CrewAI reads the LLM key from OPENAI_API_KEY, which the shared example runner doesn't provide.*/} - + {CrewaiExample} - + Note that: +- CrewAI's `LLM` points its `base_url` at the Apify OpenRouter proxy. The `openai/` model prefix selects its OpenAI-compatible client, so no provider key is needed. - `kickoff_async` runs the crew without blocking the Actor's event loop. -- `ApifyActorsTool('apify/instagram-scraper')` gives the agent the [Instagram Scraper](https://apify.com/apify/instagram-scraper) as a tool. It comes from the `crewai[tools]` package and reads its token from `APIFY_API_TOKEN`, which the example sets from the platform's `APIFY_TOKEN`. -- CrewAI calls the LLM through [LiteLLM](https://docs.litellm.ai/), so this example reads `OPENAI_API_KEY`. To route it through the Apify OpenRouter proxy instead, configure a custom `LLM` (see the [CrewAI LLM docs](https://docs.crewai.com/concepts/llms)). +- The agent decides on its own when to call the `average` tool. Add more tools to extend it, including Apify Actors (see [Using Apify Actors as tools](#using-apify-actors-as-tools)). - On a fresh container, CrewAI shows a one-time trace-consent prompt. The template sets `CREWAI_TESTING=true` to suppress it. Do the same in your Dockerfile. ## PydanticAI @@ -165,7 +163,7 @@ from langchain_apify import ApifyActorsTool tools = [ApifyActorsTool('apify/instagram-scraper')] ``` -CrewAI ships the same tool as `crewai_tools.ApifyActorsTool`, as the [CrewAI example](#crewai) above shows. +CrewAI ships the same tool as `crewai_tools.ApifyActorsTool` in its `crewai[tools]` package. For any other framework, call the Actor directly through the Actor's preconfigured API client and read its dataset: diff --git a/docs/03_guides/code/14_crewai.py b/docs/03_guides/code/14_crewai.py index 546f9813..7fc02162 100644 --- a/docs/03_guides/code/14_crewai.py +++ b/docs/03_guides/code/14_crewai.py @@ -1,46 +1,57 @@ import asyncio import os -from crewai import Agent, Crew, Task -from crewai_tools import ApifyActorsTool +from crewai import LLM, Agent, Crew, Task +from crewai.tools import tool from apify import Actor +OPENROUTER_BASE_URL = 'https://openrouter.apify.actor/api/v1' + # On a fresh container, CrewAI shows a one-time trace-consent prompt that blocks on # stdin. `CREWAI_TESTING=true` is the only flag that suppresses it. os.environ.setdefault('CREWAI_TESTING', 'true') +@tool('Average') +def average(numbers: list[float]) -> float: + """Return the arithmetic mean of a list of numbers.""" + return sum(numbers) / len(numbers) + + async def main() -> None: async with Actor: actor_input = await Actor.get_input() or {} - query = actor_input.get( - 'query', 'Summarize the latest posts on the @openai Instagram profile.' + query = actor_input.get('query', 'What is the average of 12, 18, and 30?') + model = actor_input.get('model', 'openai/gpt-4o-mini') + + # Route the LLM through the Apify OpenRouter proxy (no provider key needed). + # The `openai/` prefix selects CrewAI's OpenAI-compatible client. The rest + # is the OpenRouter model slug sent to the proxy. + llm = LLM( + model=f'openai/{model}', + base_url=OPENROUTER_BASE_URL, + api_key=os.environ['APIFY_TOKEN'], ) - model = actor_input.get('model', 'gpt-4o-mini') - - # CrewAI calls the LLM through LiteLLM, which reads OPENAI_API_KEY. - # `ApifyActorsTool` reads APIFY_API_TOKEN. The platform injects APIFY_TOKEN. - os.environ.setdefault('APIFY_API_TOKEN', os.environ['APIFY_TOKEN']) - # The agent can run any Apify Actor as a tool, here the Instagram scraper. + # A one-agent crew: an analyst that answers using the `average` tool. analyst = Agent( - role='Social Media Analyst', - goal='Analyze social media profiles and summarize the findings.', - backstory='An analyst who turns raw social media data into concise insights.', - tools=[ApifyActorsTool('apify/instagram-scraper')], - llm=model, + role='Data Analyst', + goal='Answer numeric questions accurately.', + backstory='An analyst who turns raw numbers into clear answers.', + tools=[average], + llm=llm, ) task = Task( description=query, - expected_output='A short, readable summary that answers the query.', + expected_output='A short, readable answer to the query.', agent=analyst, ) # `kickoff_async` keeps the Actor's event loop responsive. result = await Crew(agents=[analyst], tasks=[task]).kickoff_async() Actor.log.info(f'Crew result:\n{result.raw}') - await Actor.push_data({'query': query, 'summary': result.raw}) + await Actor.push_data({'query': query, 'answer': result.raw}) if __name__ == '__main__': From 2fa02f9ca200c73e2fe7284efb73aaf2404cf731 Mon Sep 17 00:00:00 2001 From: Vlada Dusek Date: Fri, 3 Jul 2026 17:28:20 +0200 Subject: [PATCH 06/10] docs: align AI agent template order in README and introduction with the guide --- README.md | 4 ++-- docs/01_introduction/index.mdx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index b1c2f316..bb29d002 100644 --- a/README.md +++ b/README.md @@ -116,7 +116,7 @@ Almost any Python project can become an Actor, including projects for: - **Web scraping and crawling** — The SDK is fully compatible with [Crawlee](https://crawlee.dev/python), which makes Apify a natural place to deploy and scale your crawlers (see the [Crawlee guide](https://docs.apify.com/sdk/python/docs/guides/crawlee)). It also works with other popular scraping libraries, such as [Scrapy](https://docs.apify.com/sdk/python/docs/guides/scrapy), [Scrapling](https://docs.apify.com/sdk/python/docs/guides/scrapling), or [Crawl4AI](https://docs.apify.com/sdk/python/docs/guides/crawl4ai). - **Browser automation** — Drive a real browser with [Playwright](https://docs.apify.com/sdk/python/docs/guides/playwright) or [Selenium](https://docs.apify.com/sdk/python/docs/guides/selenium), or with higher-level tools such as [Browser Use](https://docs.apify.com/sdk/python/docs/guides/browser-use). - **Web servers and APIs** — Run a [web server](https://docs.apify.com/sdk/python/docs/guides/running-webserver) inside an Actor to serve HTTP requests, for example to expose your scraper as a live API. -- **AI agents** — Host agents built with your framework of choice (see the [AI agents guide](https://docs.apify.com/sdk/python/docs/guides/ai-agents)). Ready-made Actor templates cover [PydanticAI](https://apify.com/templates/python-pydanticai), [CrewAI](https://apify.com/templates/python-crewai), [LangGraph](https://apify.com/templates/python-langgraph), [LlamaIndex](https://apify.com/templates/python-llamaindex-agent), and [Smolagents](https://apify.com/templates/python-smolagents). +- **AI agents** — Host agents built with your framework of choice (see the [AI agents guide](https://docs.apify.com/sdk/python/docs/guides/ai-agents)). Ready-made Actor templates cover [LangGraph](https://apify.com/templates/python-langgraph), [CrewAI](https://apify.com/templates/python-crewai), [PydanticAI](https://apify.com/templates/python-pydanticai), [LlamaIndex](https://apify.com/templates/python-llamaindex-agent), and [Smolagents](https://apify.com/templates/python-smolagents). - **MCP servers** — Deploy a Python MCP server as an Actor and make its tools available to any MCP client. See [MCP server](https://apify.com/templates/python-mcp-empty) and [MCP proxy](https://apify.com/templates/python-mcp-proxy) templates Whatever you build, the Apify SDK doesn't lock you into a particular framework. Bring the libraries you already use, and let Apify run your project in the cloud. @@ -199,7 +199,7 @@ The full SDK documentation lives at **[docs.apify.com/sdk/python](https://docs.a | [Overview](https://docs.apify.com/sdk/python/docs/overview) | What the SDK is, what Actors are, and how the pieces fit together. | | [Quick start](https://docs.apify.com/sdk/python/docs/quick-start) | Create, run, and deploy your first Python Actor. | | [Concepts](https://docs.apify.com/sdk/python/docs/concepts/actor-lifecycle) | Actor lifecycle, input, storages, events, proxy management, interacting with other Actors, webhooks, accessing the Apify API, logging, configuration, and pay-per-event. | -| [Guides](https://docs.apify.com/sdk/python/docs/guides/beautifulsoup-httpx) | Integrations with BeautifulSoup, Parsel, Playwright, Selenium, Crawlee, Scrapy, Scrapling, Crawl4AI, and Browser Use, plus building AI agents, running a web server, and using uv. | +| [Guides](https://docs.apify.com/sdk/python/docs/guides/beautifulsoup-httpx) | Integrations with BeautifulSoup, Parsel, Playwright, Selenium, Crawlee, Scrapy, Scrapling, Crawl4AI, and Browser Use, plus hosting AI agents, running a web server, and using uv. | | [Upgrading](https://docs.apify.com/sdk/python/docs/upgrading/upgrading-to-v4) | Migrating between major versions. | | [API reference](https://docs.apify.com/sdk/python/reference) | Generated reference for every class and method. | | [Changelog](https://docs.apify.com/sdk/python/docs/changelog) | Release history and breaking changes. | diff --git a/docs/01_introduction/index.mdx b/docs/01_introduction/index.mdx index b4db08e8..60eae5e2 100644 --- a/docs/01_introduction/index.mdx +++ b/docs/01_introduction/index.mdx @@ -41,7 +41,7 @@ Almost any Python project can become an Actor, including projects for: - **Web scraping and crawling** - The SDK is fully compatible with [Crawlee](https://crawlee.dev/python), which makes Apify a natural place to deploy and scale your crawlers (see the [Crawlee guide](./guides/crawlee)). It also works with other popular scraping libraries, such as [Scrapy](./guides/scrapy), [Scrapling](./guides/scrapling), or [Crawl4AI](./guides/crawl4ai). - **Browser automation** - Drive a real browser with [Playwright](./guides/playwright) or [Selenium](./guides/selenium), or with higher-level tools such as [Browser Use](./guides/browser-use). - **Web servers and APIs** - Run a [web server](./guides/running-webserver) inside an Actor to serve HTTP requests, for example to expose your scraper as a live API. -- **AI agents** - Host agents built with your framework of choice (see the [AI agents guide](./guides/ai-agents)). Ready-made Actor templates cover [PydanticAI](https://apify.com/templates/python-pydanticai), [CrewAI](https://apify.com/templates/python-crewai), [LangGraph](https://apify.com/templates/python-langgraph), [LlamaIndex](https://apify.com/templates/python-llamaindex-agent), and [Smolagents](https://apify.com/templates/python-smolagents). +- **AI agents** - Host agents built with your framework of choice (see the [AI agents guide](./guides/ai-agents)). Ready-made Actor templates cover [LangGraph](https://apify.com/templates/python-langgraph), [CrewAI](https://apify.com/templates/python-crewai), [PydanticAI](https://apify.com/templates/python-pydanticai), [LlamaIndex](https://apify.com/templates/python-llamaindex-agent), and [Smolagents](https://apify.com/templates/python-smolagents). - **MCP servers** - Deploy a Python MCP server as an Actor and make its tools available to any MCP client. See the [MCP server](https://apify.com/templates/python-mcp-empty) and [MCP proxy](https://apify.com/templates/python-mcp-proxy) templates. Whatever you build, the Apify SDK doesn't lock you into a particular framework. Bring the libraries you already use, and let Apify run your project in the cloud. From fd91257cf135945ce4e96e61fe323ba5538e9886 Mon Sep 17 00:00:00 2001 From: Vlada Dusek Date: Tue, 7 Jul 2026 09:20:45 +0200 Subject: [PATCH 07/10] docs: address review comments on the AI agents guide --- docs/03_guides/14_ai_agents.mdx | 39 ++++++++++++++++++--------------- 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/docs/03_guides/14_ai_agents.mdx b/docs/03_guides/14_ai_agents.mdx index e055dec3..da729dfb 100644 --- a/docs/03_guides/14_ai_agents.mdx +++ b/docs/03_guides/14_ai_agents.mdx @@ -20,15 +20,17 @@ An AI agent is a program that uses a large language model (LLM) to decide what t Apify Actors are a natural home for agents: -- An agent is just a Python program, so wrapping it in `async with Actor:` is all it takes to run it on the platform. +- An agent is just a Python program, so wrap it in `async with Actor:` to run it on the platform. - The platform scales runs, schedules them, and keeps logs, so you don't operate any infrastructure yourself. -- The [Apify Store](https://apify.com/store) gives your agent thousands of ready-made tools. Any Actor (a scraper, a browser automation, an API wrapper) can become a tool the agent calls. +- [Apify Store](https://apify.com/store) gives your agent thousands of ready-made tools. Any Actor can become a tool the agent calls: a scraper, a browser automation, an API wrapper. - The [Apify OpenRouter proxy](https://apify.com/apify/openrouter) lets the agent talk to an LLM without managing a provider API key. - Pay-per-event charging lets you monetize the agent and pass LLM costs on to its users. +Each example in this guide shows a complete, single-file Actor for one framework. They all read the input, run the agent, and store the result in the default dataset. + ## Choosing a framework -Apify maintains a ready-made Actor template for each of the popular agent frameworks. Each template is a complete, deployable Actor that you can use as a starting point. Pick the one that matches the framework you already use, or the style of agent you want to build: +Apify maintains a ready-made Actor template for each of the popular agent frameworks. Each template is a complete, deployable Actor that you can use as a starting point. | Framework | Good for | Template | | --- | --- | --- | @@ -38,17 +40,18 @@ Apify maintains a ready-made Actor template for each of the popular agent framew | [LlamaIndex](#llamaindex) | Retrieval-augmented agents over your own data | [`python-llamaindex-agent`](https://apify.com/templates/python-llamaindex-agent) | | [Smolagents](#smolagents) | Lightweight code-writing agents | [`python-smolagents`](https://apify.com/templates/python-smolagents) | -All of these templates live in the [actor-templates repository](https://github.com/apify/actor-templates), and you can scaffold any of them with the [Apify CLI](https://docs.apify.com/cli), for example `apify create my-agent --template python-pydanticai`. - -## Connecting to an LLM +All of these templates live in the [actor-templates repository](https://github.com/apify/actor-templates). To create a project, use the [Apify CLI](https://docs.apify.com/cli), for example: -Every agent needs an LLM, and there are two ways to provide one. +```bash +apify create my-agent --template python-pydanticai +``` -The first is the [Apify OpenRouter proxy](https://apify.com/apify/openrouter), an OpenAI-compatible endpoint at `https://openrouter.apify.actor/api/v1` that fronts the full [OpenRouter](https://openrouter.ai) model catalog. The token usage is billed against the Apify account running the Actor, so no provider API key is required. The Actor authenticates with the proxy using the `APIFY_TOKEN` that the platform injects into every run. Any framework with an OpenAI-compatible client can point its base URL at the proxy, which is what every example below does. To switch models, change the `model` input to any [OpenRouter model slug](https://openrouter.ai/models), for example `openai/gpt-4o-mini`. +## Connecting to an LLM -The second is to call a provider such as OpenAI directly with your own API key. Keep the key out of the Actor input and source code. Read it from an environment variable, which on the platform you set as a [secret environment variable](https://docs.apify.com/platform/actors/development/programming-interface/environment-variables) and locally you export in your shell. +There are two ways to provide an agent with an LLM: -Each section below shows a complete, single-file Actor for one framework. They all read the input, run the agent, and store the result in the default dataset. +- Use [Apify OpenRouter proxy](https://apify.com/apify/openrouter), an OpenAI-compatible endpoint at `https://openrouter.apify.actor/api/v1` that fronts the full [OpenRouter](https://openrouter.ai) model catalog. The token usage is billed against the Apify account running the Actor, so no provider API key is required. The Actor authenticates with the proxy using the `APIFY_TOKEN` that the platform injects into every run. Any framework with an OpenAI-compatible client can point its base URL at the proxy, which is what every example in this guide does. To switch models, change the `model` input to any [OpenRouter model slug](https://openrouter.ai/models), for example `openai/gpt-4o-mini`. +- Call a provider such as OpenAI directly with your own API key. Keep the key out of the Actor input and source code. Read it from an environment variable, which on the platform you set as a [secret environment variable](https://docs.apify.com/platform/actors/development/programming-interface/environment-variables) and locally you export in your shell. ## LangGraph @@ -68,7 +71,7 @@ Note that: - `ChatOpenAI` points its `base_url` at the Apify OpenRouter proxy, so any LangChain chat model works without a provider key. - The agent decides on its own when to call the `sum_numbers` tool, and the answer is read from the last message in the returned state. -- Add more LangChain tools to extend it, including Apify Actors (see [Using Apify Actors as tools](#using-apify-actors-as-tools)). +- To extend this agent, add more LangChain tools, including Apify Actors. For details, see [Using Apify Actors as tools](#using-apify-actors-as-tools). ## CrewAI @@ -88,7 +91,7 @@ Note that: - CrewAI's `LLM` points its `base_url` at the Apify OpenRouter proxy. The `openai/` model prefix selects its OpenAI-compatible client, so no provider key is needed. - `kickoff_async` runs the crew without blocking the Actor's event loop. -- The agent decides on its own when to call the `average` tool. Add more tools to extend it, including Apify Actors (see [Using Apify Actors as tools](#using-apify-actors-as-tools)). +- The agent decides on its own when to call the `average` tool. Add more tools to extend it, including Apify Actors. For details, see [Using Apify Actors as tools](#using-apify-actors-as-tools). - On a fresh container, CrewAI shows a one-time trace-consent prompt. The template sets `CREWAI_TESTING=true` to suppress it. Do the same in your Dockerfile. ## PydanticAI @@ -108,7 +111,7 @@ The following Actor generates a joke and returns it as a typed object: Note that: - The agent reaches the LLM through the Apify OpenRouter proxy via `OpenAIChatModel` and a custom `OpenAIProvider`, so no provider API key is needed. -- `output_type=Joke` makes the agent return a validated `Joke` instance, which maps directly onto a dataset row. Set `output_type=str` instead to get plain text back. +- `output_type=Joke` makes the agent return a validated `Joke` instance, which maps directly onto a dataset row. To get plain text back, set `output_type=str`. ## LlamaIndex @@ -128,7 +131,7 @@ Note that: - `OpenAILike` is the LlamaIndex LLM class for OpenAI-compatible endpoints such as the Apify OpenRouter proxy. It needs the `llama-index-llms-openai-like` package. - The agent decides on its own when to call the `word_count` tool. Add `FunctionTool`s of your own to extend it. -- To reason over your own data, wrap an Apify Actor in a `FunctionTool` (see [Using Apify Actors as tools](#using-apify-actors-as-tools)), as the [LlamaIndex template](https://apify.com/templates/python-llamaindex-agent) does with a contact-details scraper. +- To reason over your own data, wrap an Apify Actor in a `FunctionTool`, as the [LlamaIndex template](https://apify.com/templates/python-llamaindex-agent) does with a contact-details scraper. See also [Using Apify Actors as tools](#using-apify-actors-as-tools). ## Smolagents @@ -152,9 +155,9 @@ Note that: ## Using Apify Actors as tools -An agent becomes useful once it can act on the world. On Apify, any of the thousands of Actors in the [Apify Store](https://apify.com/store) can become a tool: a scraper that fetches data, a browser automation that fills a form, or an API wrapper that posts a message. +An agent becomes useful once it can act on the world. On Apify, any of the thousands of Actors in [Apify Store](https://apify.com/store) can become a tool: a scraper that fetches data, a browser automation that fills a form, or an API wrapper that posts a message. -Some frameworks ship a ready-made wrapper for this. For LangGraph and other [LangChain](https://www.langchain.com/)-based agents, the [`langchain-apify`](https://github.com/apify/langchain-apify) package exposes any Actor as a tool with a single line: +Some frameworks ship a ready-made wrapper for turning an Actor into a tool. For LangGraph and other [LangChain](https://www.langchain.com/)-based agents, the [`langchain-apify`](https://github.com/apify/langchain-apify) package exposes any Actor as a tool with a single line: ```python from langchain_apify import ApifyActorsTool @@ -179,7 +182,7 @@ For details on calling other Actors, see [Interacting with other Actors](../conc ## Running on the Apify platform -Agents run on the standard [Apify Python base image](https://hub.docker.com/r/apify/actor-python), so no browser or extra system dependencies are needed. Add `apify` and the packages shown in your framework's section above to `requirements.txt`, then deploy the Actor as usual. +Agents run on the standard [Apify Python base image](https://hub.docker.com/r/apify/actor-python), so no browser or extra system dependencies are needed. Add `apify` and the packages required by your framework to `requirements.txt`, then deploy the Actor as usual. To monetize the agent, use [pay-per-event charging](../concepts/pay-per-event). You define events such as `task-completed` in the Actor's monetization settings and trigger them from the code: @@ -187,7 +190,7 @@ To monetize the agent, use [pay-per-event charging](../concepts/pay-per-event). await Actor.charge('task-completed') ``` -This lets you charge users directly from the Actor and cover the cost of execution and LLM tokens. It pairs naturally with the Apify OpenRouter proxy, which already bills the LLM usage to the account running the Actor. +This approach lets you charge users directly from the Actor and cover the cost of execution and LLM tokens. It pairs naturally with the Apify OpenRouter proxy, which already bills the LLM usage to the account running the Actor. ## Conclusion From 336dd624dcf0cb487536345e30ed33ceac83576f Mon Sep 17 00:00:00 2001 From: Vlada Dusek Date: Tue, 7 Jul 2026 11:27:32 +0200 Subject: [PATCH 08/10] Improve code examples --- README.md | 2 +- docs/03_guides/14_ai_agents.mdx | 96 ++++++++++++++++++---------- docs/03_guides/code/14_crewai.py | 92 ++++++++++++++++++-------- docs/03_guides/code/14_langgraph.py | 94 ++++++++++++++++++++++----- docs/03_guides/code/14_llamaindex.py | 75 ++++++++++++++++++---- docs/03_guides/code/14_pydanticai.py | 67 ++++++++++++++----- docs/03_guides/code/14_smolagents.py | 88 ++++++++++++++++++++++--- 7 files changed, 402 insertions(+), 112 deletions(-) diff --git a/README.md b/README.md index 645874fc..708e6700 100644 --- a/README.md +++ b/README.md @@ -199,7 +199,7 @@ The full SDK documentation lives at **[docs.apify.com/sdk/python](https://docs.a | [Overview](https://docs.apify.com/sdk/python/docs/overview) | What the SDK is, what Actors are, and how the pieces fit together. | | [Quick start](https://docs.apify.com/sdk/python/docs/quick-start) | Create, run, and deploy your first Python Actor. | | [Concepts](https://docs.apify.com/sdk/python/docs/concepts/actor-lifecycle) | Actor lifecycle, input, storages, events, proxy management, interacting with other Actors, webhooks, accessing the Apify API, logging, configuration, and pay-per-event. | -| [Guides](https://docs.apify.com/sdk/python/docs/guides/beautifulsoup-httpx) | Integrations with BeautifulSoup, Parsel, Playwright, Selenium, Crawlee, Scrapy, Scrapling, Crawl4AI, and Browser Use, plus hosting AI agents, building MCP servers, running a web server, validating input with Pydantic, and using uv. | +| [Guides](https://docs.apify.com/sdk/python/docs/guides/beautifulsoup-httpx) | Integrations with BeautifulSoup, Parsel, Playwright, Selenium, Crawlee, Scrapy, Scrapling, Crawl4AI, and Browser Use, plus using uv, validating input with Pydantic, running a web server, building MCP servers, and hosting AI agents. | | [Upgrading](https://docs.apify.com/sdk/python/docs/upgrading/upgrading-to-v4) | Migrating between major versions. | | [API reference](https://docs.apify.com/sdk/python/reference) | Generated reference for every class and method. | | [Changelog](https://docs.apify.com/sdk/python/docs/changelog) | Release history and breaking changes. | diff --git a/docs/03_guides/14_ai_agents.mdx b/docs/03_guides/14_ai_agents.mdx index da729dfb..aa1d1237 100644 --- a/docs/03_guides/14_ai_agents.mdx +++ b/docs/03_guides/14_ai_agents.mdx @@ -20,13 +20,13 @@ An AI agent is a program that uses a large language model (LLM) to decide what t Apify Actors are a natural home for agents: -- An agent is just a Python program, so wrap it in `async with Actor:` to run it on the platform. +- An agent is just a Python program, so wrap it in `async with Actor:` to run it on the [Apify platform](https://docs.apify.com/platform). - The platform scales runs, schedules them, and keeps logs, so you don't operate any infrastructure yourself. - [Apify Store](https://apify.com/store) gives your agent thousands of ready-made tools. Any Actor can become a tool the agent calls: a scraper, a browser automation, an API wrapper. - The [Apify OpenRouter proxy](https://apify.com/apify/openrouter) lets the agent talk to an LLM without managing a provider API key. -- Pay-per-event charging lets you monetize the agent and pass LLM costs on to its users. +- The [pay-per-event charging](../concepts/pay-per-event) lets you monetize the agent and pass LLM costs on to its users. -Each example in this guide shows a complete, single-file Actor for one framework. They all read the input, run the agent, and store the result in the default dataset. +Each example in this guide shows a complete, single-file Actor for one framework. They all read the input, run the agent, and store the result in the default dataset. Each one defines its input as a [Pydantic model](./input-validation) with default values, so the fields stay typed and validated. ## Choosing a framework @@ -48,20 +48,31 @@ apify create my-agent --template python-pydanticai ## Connecting to an LLM -There are two ways to provide an agent with an LLM: +There are two ways to give an agent an LLM. -- Use [Apify OpenRouter proxy](https://apify.com/apify/openrouter), an OpenAI-compatible endpoint at `https://openrouter.apify.actor/api/v1` that fronts the full [OpenRouter](https://openrouter.ai) model catalog. The token usage is billed against the Apify account running the Actor, so no provider API key is required. The Actor authenticates with the proxy using the `APIFY_TOKEN` that the platform injects into every run. Any framework with an OpenAI-compatible client can point its base URL at the proxy, which is what every example in this guide does. To switch models, change the `model` input to any [OpenRouter model slug](https://openrouter.ai/models), for example `openai/gpt-4o-mini`. -- Call a provider such as OpenAI directly with your own API key. Keep the key out of the Actor input and source code. Read it from an environment variable, which on the platform you set as a [secret environment variable](https://docs.apify.com/platform/actors/development/programming-interface/environment-variables) and locally you export in your shell. +### Apify OpenRouter proxy + +The [Apify OpenRouter proxy](https://apify.com/apify/openrouter) is an OpenAI-compatible endpoint at `https://openrouter.apify.actor/api/v1` that fronts the full [OpenRouter](https://openrouter.ai) model catalog. Any framework with an OpenAI-compatible client can point its base URL at the proxy, which is what every example in this guide does. Note that: + +- The token usage is billed against the Apify account running the Actor, so no provider API key is required. +- The Actor authenticates with the proxy using the `APIFY_TOKEN` that the platform injects into every run. +- To switch models, change the `model` input to any [OpenRouter model slug](https://openrouter.ai/models), for example `openai/gpt-5.4-mini`. + +### Your own provider key + +Call a provider such as OpenAI directly with your own API key. Keep the key out of the Actor input and source code. Read it from an environment variable, which on the platform you set as a [secret environment variable](https://docs.apify.com/platform/actors/development/programming-interface/environment-variables) and locally you export in your shell. ## LangGraph -[LangGraph](https://www.langchain.com/langgraph) builds an agent as a graph with explicit state, which makes complex, multi-step control flow easy to follow. It builds on [LangChain](https://www.langchain.com/), so any LangChain chat model and tool works. To install it, use: +[LangGraph](https://www.langchain.com/langgraph) models an agent as a graph, where nodes do the work and edges decide what runs next. The explicit state and control flow make complex, multi-step logic easy to follow, including loops that revisit earlier nodes. It builds on [LangChain](https://www.langchain.com/), so any LangChain chat model works. To install it, use: ```bash -pip install langchain langchain-openai langgraph +pip install langchain-openai langgraph pydantic ``` -The following Actor runs an agent with a single tool: +The install also includes [Pydantic](https://docs.pydantic.dev/) for the Actor's typed input and output models. + +The following Actor builds a small graph with two nodes. A `fetch` node scrapes the page with the [RAG Web Browser](https://apify.com/apify/rag-web-browser) Actor, then a `summarize` node distills the clean Markdown into a structured summary. A conditional edge loops back into `summarize` until the summary is detailed enough: {LanggraphExample} @@ -69,19 +80,22 @@ The following Actor runs an agent with a single tool: Note that: -- `ChatOpenAI` points its `base_url` at the Apify OpenRouter proxy, so any LangChain chat model works without a provider key. -- The agent decides on its own when to call the `sum_numbers` tool, and the answer is read from the last message in the returned state. -- To extend this agent, add more LangChain tools, including Apify Actors. For details, see [Using Apify Actors as tools](#using-apify-actors-as-tools). +- `ChatOpenAI` points its `base_url` at the Apify OpenRouter proxy, so any LangChain chat model works without a provider key. `with_structured_output(PageSummary)` makes the `summarize` node return a validated `PageSummary`. +- The `State` is a typed dict that flows between nodes. Each node returns a partial update, and LangGraph merges it into the shared state. +- `StateGraph` wires the nodes together: `fetch` runs first, then `summarize`. The `route` function is a conditional edge that either loops back into `summarize` for another pass or ends the run. The cycle and the `revisions` guard in the state are what a plain chain can't express, and they're the reason to reach for LangGraph. +- `fetch` scrapes the page by calling the [RAG Web Browser](https://apify.com/apify/rag-web-browser) Actor with `Actor.call`, then reads the result from its dataset. The Actor returns clean Markdown, so the agent reads text instead of raw HTML and uses fewer tokens. Any Apify scraping Actor works here. For details, see [Using Apify Actors as tools](#using-apify-actors-as-tools). ## CrewAI -[CrewAI](https://www.crewai.com/) models a problem as a "crew" of role-playing agents that work through tasks. To install it, use: +[CrewAI](https://www.crewai.com/) models a problem as a "crew" of role-playing agents, each with its own role, goal, and backstory. The agents work through tasks in sequence, handing results from one agent to the next. It's a natural fit when a job splits into specialized roles that collaborate. To install it, use: ```bash -pip install crewai +pip install "crewai[tools]" pydantic ``` -The following Actor defines a single analyst agent with one tool: +The install also includes [Pydantic](https://docs.pydantic.dev/) for the Actor's typed input and output models. + +The following Actor runs a two-agent crew. A researcher scrapes a [Crawlee](https://crawlee.dev/python) docs page with the [RAG Web Browser](https://apify.com/apify/rag-web-browser) Actor, exposed as a tool by `ApifyActorsTool` from the [`crewai-tools`](https://github.com/crewAIInc/crewAI/tree/main/lib/crewai-tools) extra. A writer then distills the notes into a structured guide of Crawlee's crawlers, each with its name, what it's built on, and what it's best for: {CrewaiExample} @@ -90,19 +104,22 @@ The following Actor defines a single analyst agent with one tool: Note that: - CrewAI's `LLM` points its `base_url` at the Apify OpenRouter proxy. The `openai/` model prefix selects its OpenAI-compatible client, so no provider key is needed. +- The `researcher` reads the page with `ApifyActorsTool`, which wraps the [RAG Web Browser](https://apify.com/apify/rag-web-browser) Actor and returns the page as clean Markdown. The `writer` then turns its notes into the result. Listing both agents in one `Crew` runs them in order, and `context=[research]` hands the researcher's notes to the writer. +- `output_pydantic=CrawlerGuide` makes the writer's task return a validated `CrawlerGuide`, read from `result.pydantic` and stored as one dataset row per crawler. - `kickoff_async` runs the crew without blocking the Actor's event loop. -- The agent decides on its own when to call the `average` tool. Add more tools to extend it, including Apify Actors. For details, see [Using Apify Actors as tools](#using-apify-actors-as-tools). - On a fresh container, CrewAI shows a one-time trace-consent prompt. The template sets `CREWAI_TESTING=true` to suppress it. Do the same in your Dockerfile. ## PydanticAI -[PydanticAI](https://ai.pydantic.dev/) is an agent framework from the team behind Pydantic. It's strongly typed and integrates naturally with the [Pydantic models](./input-validation) the Apify SDK already uses. To install it, use: +[PydanticAI](https://ai.pydantic.dev/) is an agent framework from the team behind Pydantic. It's strongly typed and integrates naturally with the [Pydantic models](./input-validation) the Apify SDK already uses. Tool arguments and agent outputs are validated against your models, so the data stays structured end to end. To install it, use: ```bash -pip install pydantic-ai +pip install pydantic-ai pydantic impit ``` -The following Actor generates a joke and returns it as a typed object: +The install also includes [Pydantic](https://docs.pydantic.dev/) for the Actor's typed input and output models, plus [Impit](https://github.com/apify/impit), the HTTP client the Actor uses to call external APIs. + +The following Actor evaluates a Python package and recommends whether to use it. Because the LLM would otherwise guess from its training data, the agent calls the `fetch_pypi_metadata` tool, which uses [Impit](https://github.com/apify/impit) to fetch live metadata from the PyPI JSON API. It then returns a typed report with the package's name, its latest version, a short summary, and a recommendation grounded in those facts: {PydanticaiExample} @@ -111,17 +128,24 @@ The following Actor generates a joke and returns it as a typed object: Note that: - The agent reaches the LLM through the Apify OpenRouter proxy via `OpenAIChatModel` and a custom `OpenAIProvider`, so no provider API key is needed. -- `output_type=Joke` makes the agent return a validated `Joke` instance, which maps directly onto a dataset row. To get plain text back, set `output_type=str`. +- Passing `fetch_pypi_metadata` in the `tools` list registers it as a tool. PydanticAI builds its schema from the function's signature and docstring, and the agent calls it to fetch real facts instead of guessing. Returning a typed `PackageFacts` keeps the data validated on its way back to the agent. +- `output_type=PackageReport` makes the agent return a validated `PackageReport`, which maps directly onto a dataset row. To get plain text back, set `output_type=str`. ## LlamaIndex -[LlamaIndex](https://www.llamaindex.ai/) is built for retrieval-augmented agents that reason over your own data. To install it, use: +[LlamaIndex](https://www.llamaindex.ai/) is built for retrieval-augmented agents that reason over your own data. It indexes your documents into a vector store, retrieves the passages relevant to a question, and grounds the answer in them. It's a strong fit for question answering over private or domain-specific content. To install it, use: ```bash -pip install llama-index llama-index-llms-openai-like +pip install llama-index \ + llama-index-llms-openai-like \ + llama-index-embeddings-huggingface \ + llama-index-readers-apify \ + pydantic ``` -The following Actor runs a `ReActAgent` with a single tool: +The install also includes [Pydantic](https://docs.pydantic.dev/) for the Actor's typed input and output models. + +The following Actor builds a retrieval-augmented generation (RAG) pipeline. It scrapes a few pages with the [Website Content Crawler](https://apify.com/apify/website-content-crawler) Actor through the [`llama-index-readers-apify`](https://pypi.org/project/llama-index-readers-apify/) reader, which turns each crawled page into a `Document`. It then indexes the documents in a vector store, answers a question using only the retrieved passages, and cites its sources: {LlamaindexExample} @@ -129,19 +153,23 @@ The following Actor runs a `ReActAgent` with a single tool: Note that: -- `OpenAILike` is the LlamaIndex LLM class for OpenAI-compatible endpoints such as the Apify OpenRouter proxy. It needs the `llama-index-llms-openai-like` package. -- The agent decides on its own when to call the `word_count` tool. Add `FunctionTool`s of your own to extend it. -- To reason over your own data, wrap an Apify Actor in a `FunctionTool`, as the [LlamaIndex template](https://apify.com/templates/python-llamaindex-agent) does with a contact-details scraper. See also [Using Apify Actors as tools](#using-apify-actors-as-tools). +- `OpenAILike` points `api_base` at the Apify OpenRouter proxy, so no provider API key is needed. It's the LlamaIndex LLM class for any OpenAI-compatible endpoint. +- `Document` wraps each scraped page, and `VectorStoreIndex.from_documents` chunks and embeds them. The index is the retrieval layer that sets LlamaIndex apart from a plain agent loop. +- Embeddings run locally through `HuggingFaceEmbedding` from the `llama-index-embeddings-huggingface` package, so the proxy needs no embeddings endpoint. +- `index.as_query_engine(output_cls=Answer)` retrieves the passages most relevant to the question, then the LLM composes a validated `Answer` grounded in them. +- Every response carries the passages it used in `response.source_nodes`, so the Actor pushes their `url` metadata as citations. The `ApifyActor` reader runs the Website Content Crawler and maps each crawled page to a `Document`, so indexing more content is a matter of crawling more pages. For details, see [Using Apify Actors as tools](#using-apify-actors-as-tools). ## Smolagents -[Smolagents](https://github.com/huggingface/smolagents) is a lightweight framework from Hugging Face whose agents write and run Python code to solve a task. To install it, use: +[Smolagents](https://huggingface.co/docs/smolagents) is a lightweight framework whose agents write and run Python code to solve a task, instead of emitting structured tool calls. Writing code lets the agent chain several operations, loop, and transform data in a single step. It's a good fit for tasks that are naturally expressed as code. To install it, use: ```bash -pip install "smolagents[openai]" +pip install "smolagents[openai]" pydantic impit ``` -The following Actor runs a `CodeAgent`: +The install also includes [Pydantic](https://docs.pydantic.dev/) for the Actor's typed input and output models, plus [Impit](https://github.com/apify/impit), the HTTP client the Actor uses to call external APIs. + +The following Actor runs a `CodeAgent` that writes Python to analyze the Hacker News front page. Its `fetch_front_page` tool uses [Impit](https://github.com/apify/impit) to call the Hacker News API, then the agent groups the stories by domain and returns a structured breakdown of the busiest ones: {SmolagentsExample} @@ -150,7 +178,9 @@ The following Actor runs a `CodeAgent`: Note that: - `OpenAIServerModel` points `api_base` at the Apify OpenRouter proxy, so no provider API key is needed. -- The agent uses `WebSearchTool` to fetch the latest news, then writes and runs Python code to summarize it. +- Writing code is what a `CodeAgent` does best. It calls `fetch_front_page`, then writes Python to take each URL's domain, group the stories, and average their points. `additional_authorized_imports` grants the standard-library modules that code needs. +- `final_answer_checks` validates every candidate answer against `FrontPageReport`. A mismatch sends the agent back to fix its code, so the output stays structured rather than free-form. The returned dict is then parsed with `FrontPageReport.model_validate`. +- `CodeAgent.run` is synchronous, so `asyncio.to_thread` runs it without blocking the Actor's event loop. - Because a `CodeAgent` executes the code it generates, run it in the isolated Actor container rather than on your own machine. ## Using Apify Actors as tools @@ -168,14 +198,14 @@ tools = [ApifyActorsTool('apify/instagram-scraper')] CrewAI ships the same tool as `crewai_tools.ApifyActorsTool` in its `crewai[tools]` package. -For any other framework, call the Actor directly through the Actor's preconfigured API client and read its dataset: +For any other framework, call the Actor with `Actor.call` and read its dataset through the preconfigured API client: ```python async def scrape_instagram(handle: str) -> list[dict]: """Scrape recent posts from an Instagram profile.""" run_input = {'directUrls': [f'https://www.instagram.com/{handle}/']} - run = await Actor.apify_client.actor('apify/instagram-scraper').call(run_input=run_input) - return (await Actor.apify_client.dataset(run['defaultDatasetId']).list_items()).items + run = await Actor.call('apify/instagram-scraper', run_input=run_input) + return (await Actor.apify_client.dataset(run.default_dataset_id).list_items()).items ``` For details on calling other Actors, see [Interacting with other Actors](../concepts/interacting-with-other-actors). diff --git a/docs/03_guides/code/14_crewai.py b/docs/03_guides/code/14_crewai.py index 7fc02162..9d0b7597 100644 --- a/docs/03_guides/code/14_crewai.py +++ b/docs/03_guides/code/14_crewai.py @@ -2,56 +2,98 @@ import os from crewai import LLM, Agent, Crew, Task -from crewai.tools import tool +from crewai_tools import ApifyActorsTool +from pydantic import BaseModel from apify import Actor OPENROUTER_BASE_URL = 'https://openrouter.apify.actor/api/v1' # On a fresh container, CrewAI shows a one-time trace-consent prompt that blocks on -# stdin. `CREWAI_TESTING=true` is the only flag that suppresses it. +# stdin. `CREWAI_TESTING=true` suppresses it. os.environ.setdefault('CREWAI_TESTING', 'true') +# The Crawlee docs page the crew reads when the input has no `url`. +DEFAULT_URL = 'https://crawlee.dev/python/docs/guides/architecture-overview' -@tool('Average') -def average(numbers: list[float]) -> float: - """Return the arithmetic mean of a list of numbers.""" - return sum(numbers) / len(numbers) + +class ActorInput(BaseModel): + """The Actor input, validated with default values.""" + + url: str = DEFAULT_URL + model: str = 'openai/gpt-5.4-mini' + + +class Crawler(BaseModel): + """One crawler class that Crawlee provides.""" + + name: str + built_on: str + best_for: str + + +class CrawlerGuide(BaseModel): + """The structured guide the crew distills from the docs page.""" + + crawlers: list[Crawler] async def main() -> None: async with Actor: - actor_input = await Actor.get_input() or {} - query = actor_input.get('query', 'What is the average of 12, 18, and 30?') - model = actor_input.get('model', 'openai/gpt-4o-mini') + # Parse the Actor input into the typed model, filling in defaults. + actor_input = ActorInput.model_validate(await Actor.get_input() or {}) + url = actor_input.url + model = actor_input.model # Route the LLM through the Apify OpenRouter proxy (no provider key needed). - # The `openai/` prefix selects CrewAI's OpenAI-compatible client. The rest - # is the OpenRouter model slug sent to the proxy. + # The `openai/` prefix selects CrewAI's OpenAI-compatible client. llm = LLM( model=f'openai/{model}', base_url=OPENROUTER_BASE_URL, api_key=os.environ['APIFY_TOKEN'], ) - # A one-agent crew: an analyst that answers using the `average` tool. - analyst = Agent( - role='Data Analyst', - goal='Answer numeric questions accurately.', - backstory='An analyst who turns raw numbers into clear answers.', - tools=[average], + # `ApifyActorsTool` exposes any Apify Actor as a CrewAI tool. Here it wraps the + # RAG Web Browser to fetch the page as clean Markdown. + researcher = Agent( + role='Documentation researcher', + goal='Read the Crawlee docs and note every crawler it describes.', + backstory='A researcher who reads technical docs closely.', + tools=[ApifyActorsTool('apify/rag-web-browser')], + llm=llm, + ) + writer = Agent( + role='Technical writer', + goal='Turn research notes into a clear, structured crawler guide.', + backstory='A writer who distills docs into comparison tables.', llm=llm, ) - task = Task( - description=query, - expected_output='A short, readable answer to the query.', - agent=analyst, + + research = Task( + description=f'Scrape {url} and list the crawlers the page covers.', + expected_output='Notes on each crawler: name, what it builds on, its use.', + agent=researcher, + ) + # `context=[research]` feeds the researcher's notes to the writer, and + # `output_pydantic` makes the final task return a validated `CrawlerGuide`. + write = Task( + description=( + 'From the notes, compile each crawler with what it is built on ' + 'and what it is best for.' + ), + expected_output='A list of crawlers with name, built_on, and best_for.', + agent=writer, + context=[research], + output_pydantic=CrawlerGuide, ) - # `kickoff_async` keeps the Actor's event loop responsive. - result = await Crew(agents=[analyst], tasks=[task]).kickoff_async() - Actor.log.info(f'Crew result:\n{result.raw}') - await Actor.push_data({'query': query, 'answer': result.raw}) + # `kickoff_async` runs the crew without blocking the Actor's event loop. + crew = Crew(agents=[researcher, writer], tasks=[research, write]) + guide = (await crew.kickoff_async()).pydantic + if guide is None: + raise RuntimeError('The crew did not return a structured CrawlerGuide.') + Actor.log.info(f'Crawler guide:\n{guide.model_dump_json(indent=2)}') + await Actor.push_data([crawler.model_dump() for crawler in guide.crawlers]) if __name__ == '__main__': diff --git a/docs/03_guides/code/14_langgraph.py b/docs/03_guides/code/14_langgraph.py index 0d990daa..02c22049 100644 --- a/docs/03_guides/code/14_langgraph.py +++ b/docs/03_guides/code/14_langgraph.py @@ -1,26 +1,80 @@ import asyncio import os +from functools import partial +from typing import TypedDict -from langchain.agents import create_agent -from langchain_core.tools import tool +from langchain_core.runnables import Runnable from langchain_openai import ChatOpenAI +from langgraph.graph import END, START, StateGraph +from pydantic import BaseModel from apify import Actor OPENROUTER_BASE_URL = 'https://openrouter.apify.actor/api/v1' +MIN_KEY_POINTS = 3 +MAX_REVISIONS = 2 -@tool -def sum_numbers(numbers: list[int]) -> int: - """Return the sum of a list of numbers.""" - return sum(numbers) +class ActorInput(BaseModel): + """The Actor input, validated with default values.""" + + url: str = 'https://crawlee.dev' + model: str = 'openai/gpt-5.4-mini' + + +class PageSummary(BaseModel): + """The structured summary the agent extracts from a web page.""" + + title: str + summary: str + key_points: list[str] + target_audience: str + + +class State(TypedDict): + """The state that flows between the graph's nodes.""" + + url: str + page_text: str + summary: PageSummary + revisions: int + + +async def fetch(state: State) -> dict: + """Node: scrape the page to clean Markdown with the RAG Web Browser Actor.""" + run_input = {'query': state['url'], 'outputFormats': ['markdown']} + run = await Actor.call('apify/rag-web-browser', run_input=run_input) + dataset = Actor.apify_client.dataset(run.default_dataset_id) + items = (await dataset.list_items()).items + if not items or not items[0].get('markdown'): + raise RuntimeError(f'RAG Web Browser returned no content for {state["url"]}.') + return {'page_text': items[0]['markdown']} + + +async def summarize(state: State, structured_llm: Runnable) -> dict: + """Node: summarize the page, asking for more depth on a re-run.""" + hint = '' + if state['revisions']: + hint = f' List at least {MIN_KEY_POINTS} distinct key points.' + prompt = f'Summarize this page.{hint}\n\n{state["page_text"]}' + summary = await structured_llm.ainvoke(prompt) + return {'summary': summary, 'revisions': state['revisions'] + 1} + + +def route(state: State) -> str: + """Edge: loop back for another pass while the summary is thin.""" + thin = len(state['summary'].key_points) < MIN_KEY_POINTS + if thin and state['revisions'] < MAX_REVISIONS: + return 'summarize' + return END async def main() -> None: async with Actor: - actor_input = await Actor.get_input() or {} - query = actor_input.get('query', 'What is the sum of 128, 64, and 32?') - model = actor_input.get('model', 'openai/gpt-4o-mini') + # Parse the Actor input into the typed model, filling in defaults. + actor_input = ActorInput.model_validate(await Actor.get_input() or {}) + url = actor_input.url + model = actor_input.model # Route the LLM through the Apify OpenRouter proxy (no provider key needed). llm = ChatOpenAI( @@ -28,13 +82,23 @@ async def main() -> None: base_url=OPENROUTER_BASE_URL, api_key=os.environ['APIFY_TOKEN'], ) - # The agent decides on its own when to call the `sum_numbers` tool. - agent = create_agent(llm, tools=[sum_numbers]) + # `with_structured_output` makes the node return a validated `PageSummary`. + structured_llm = llm.with_structured_output(PageSummary) + + # Wire the nodes into a graph. Its conditional edge loops back into `summarize` + # until the summary is detailed enough. `partial` binds `structured_llm` to it. + graph = StateGraph(State) + graph.add_node('fetch', fetch) + graph.add_node('summarize', partial(summarize, structured_llm=structured_llm)) + graph.add_edge(START, 'fetch') + graph.add_edge('fetch', 'summarize') + graph.add_conditional_edges('summarize', route) + agent = graph.compile() - result = await agent.ainvoke({'messages': [('user', query)]}) - answer = result['messages'][-1].content - Actor.log.info(f'Agent answer:\n{answer}') - await Actor.push_data({'query': query, 'answer': answer}) + result = await agent.ainvoke({'url': url, 'revisions': 0}) + summary = result['summary'] + Actor.log.info(f'Page summary:\n{summary.model_dump_json(indent=2)}') + await Actor.push_data({'url': url, **summary.model_dump()}) if __name__ == '__main__': diff --git a/docs/03_guides/code/14_llamaindex.py b/docs/03_guides/code/14_llamaindex.py index ed75901b..69dae080 100644 --- a/docs/03_guides/code/14_llamaindex.py +++ b/docs/03_guides/code/14_llamaindex.py @@ -1,39 +1,88 @@ import asyncio import os -from llama_index.core.agent import ReActAgent -from llama_index.core.tools import FunctionTool +from llama_index.core import Document, VectorStoreIndex +from llama_index.embeddings.huggingface import HuggingFaceEmbedding from llama_index.llms.openai_like import OpenAILike +from llama_index.readers.apify import ApifyActor +from pydantic import BaseModel from apify import Actor OPENROUTER_BASE_URL = 'https://openrouter.apify.actor/api/v1' +EMBED_MODEL = 'BAAI/bge-small-en-v1.5' -def word_count(text: str) -> int: - """Return the number of words in the given text.""" - return len(text.split()) +class ActorInput(BaseModel): + """The Actor input, validated with default values.""" + + urls: list[str] = [ + 'https://docs.apify.com/platform/actors', + 'https://docs.apify.com/platform/storage/dataset', + 'https://docs.apify.com/platform/proxy', + ] + question: str = 'How does Apify proxy work?' + model: str = 'openai/gpt-5.4-mini' + + +class Answer(BaseModel): + """The grounded answer the query engine returns.""" + + answer: str + key_facts: list[str] + + +def to_document(item: dict) -> Document: + """Map a Website Content Crawler item to a `Document` tagged with its URL.""" + return Document(text=item['text'], metadata={'url': item['url']}) async def main() -> None: async with Actor: - actor_input = await Actor.get_input() or {} - query = actor_input.get('query', 'How many words are in "Apify runs Actors"?') - model = actor_input.get('model', 'openai/gpt-4o-mini') + # Parse the Actor input into the typed model, filling in defaults. + actor_input = ActorInput.model_validate(await Actor.get_input() or {}) + urls = actor_input.urls + question = actor_input.question + model = actor_input.model # Route the LLM through the Apify OpenRouter proxy (no provider key needed). - # `OpenAILike` is the LlamaIndex class for OpenAI-compatible endpoints. llm = OpenAILike( model=model, api_base=OPENROUTER_BASE_URL, api_key=os.environ['APIFY_TOKEN'], is_chat_model=True, ) - agent = ReActAgent(tools=[FunctionTool.from_defaults(fn=word_count)], llm=llm) + # Embeddings run locally, so the proxy needs no embeddings endpoint. + embed_model = HuggingFaceEmbedding(model_name=EMBED_MODEL) + + # Scrape the pages with the Website Content Crawler Actor and wrap each one + # in a `Document`. LlamaIndex then chunks and embeds them, so the query engine + # retrieves only the relevant passages. + reader = ApifyActor(apify_api_token=os.environ['APIFY_TOKEN']) + run_input = {'startUrls': [{'url': url} for url in urls], 'maxCrawlDepth': 0} + documents = await asyncio.to_thread( + reader.load_data, + actor_id='apify/website-content-crawler', + run_input=run_input, + dataset_mapping_function=to_document, + ) + index = VectorStoreIndex.from_documents(documents, embed_model=embed_model) + + # `output_cls` returns a validated `Answer`. The response still carries the + # retrieved `source_nodes`, so the answer can cite the pages it came from. + query_engine = index.as_query_engine( + llm=llm, + output_cls=Answer, + response_mode='compact', + similarity_top_k=4, + ) + response = await query_engine.aquery(question) - response = await agent.run(user_msg=query) - Actor.log.info(f'Agent answer:\n{response}') - await Actor.push_data({'query': query, 'answer': str(response)}) + answer = response.response + sources = [node.node.metadata['url'] for node in response.source_nodes] + record = {'question': question, **answer.model_dump(), 'sources': sources} + Actor.log.info(f'Answer:\n{answer.model_dump_json(indent=2)}') + await Actor.push_data(record) if __name__ == '__main__': diff --git a/docs/03_guides/code/14_pydanticai.py b/docs/03_guides/code/14_pydanticai.py index 083da4b5..a7a48eb4 100644 --- a/docs/03_guides/code/14_pydanticai.py +++ b/docs/03_guides/code/14_pydanticai.py @@ -1,6 +1,7 @@ import asyncio import os +import impit from pydantic import BaseModel from pydantic_ai import Agent from pydantic_ai.models.openai import OpenAIChatModel @@ -9,38 +10,74 @@ from apify import Actor OPENROUTER_BASE_URL = 'https://openrouter.apify.actor/api/v1' +PYPI_JSON_URL = 'https://pypi.org/pypi/{name}/json' -class Joke(BaseModel): - """The agent's typed output: a joke split into its setup and punchline.""" +class ActorInput(BaseModel): + """The Actor input, validated with default values.""" - setup: str - punchline: str + package: str = 'crawlee' + model: str = 'openai/gpt-5.4-mini' + + +class PackageFacts(BaseModel): + """The metadata the tool pulls from the PyPI JSON API.""" + + name: str + version: str + summary: str | None + requires_python: str | None + + +class PackageReport(BaseModel): + """The agent's typed verdict on a PyPI package.""" + + name: str + latest_version: str + summary: str + recommendation: str + + +async def fetch_pypi_metadata(name: str) -> PackageFacts: + """Fetch a package's metadata from the PyPI JSON API.""" + client = impit.AsyncClient(browser='firefox', follow_redirects=True, timeout=30) + info = (await client.get(PYPI_JSON_URL.format(name=name))).json()['info'] + return PackageFacts( + name=info['name'], + version=info['version'], + summary=info['summary'], + requires_python=info['requires_python'], + ) async def main() -> None: async with Actor: - actor_input = await Actor.get_input() or {} - topic = actor_input.get('topic', 'bad weather') - model = actor_input.get('model', 'openai/gpt-4o-mini') + # Parse the Actor input into the typed model, filling in defaults. + actor_input = ActorInput.model_validate(await Actor.get_input() or {}) + package = actor_input.package + model = actor_input.model # Route the LLM through the Apify OpenRouter proxy (no provider key needed). provider = OpenAIProvider( base_url=OPENROUTER_BASE_URL, api_key=os.environ['APIFY_TOKEN'], ) - # `output_type=Joke` makes the agent return a validated `Joke` instance. + # `output_type` makes the agent return a validated `PackageReport`. Passing + # `fetch_pypi_metadata` in `tools` registers it as a tool from its signature. agent = Agent( OpenAIChatModel(model, provider=provider), - output_type=Joke, - system_prompt='You are a witty comedian. Write a single short joke.', + output_type=PackageReport, + tools=[fetch_pypi_metadata], + system_prompt=( + 'You advise Python developers on packages. Always call ' + '`fetch_pypi_metadata` for facts instead of guessing.' + ), ) - joke = (await agent.run(user_prompt=f'Tell me a joke about {topic}.')).output - Actor.log.info(f'Joke:\n{joke.setup}\n{joke.punchline}') - await Actor.push_data( - {'topic': topic, 'setup': joke.setup, 'punchline': joke.punchline} - ) + prompt = f'Evaluate the "{package}" package and recommend whether to use it.' + report = (await agent.run(user_prompt=prompt)).output + Actor.log.info(f'Package report:\n{report.model_dump_json(indent=2)}') + await Actor.push_data(report.model_dump()) if __name__ == '__main__': diff --git a/docs/03_guides/code/14_smolagents.py b/docs/03_guides/code/14_smolagents.py index f5d29d31..b5620790 100644 --- a/docs/03_guides/code/14_smolagents.py +++ b/docs/03_guides/code/14_smolagents.py @@ -1,18 +1,67 @@ import asyncio import os -from smolagents import CodeAgent, OpenAIServerModel, WebSearchTool +import impit +from pydantic import BaseModel, ValidationError +from smolagents import CodeAgent, OpenAIServerModel, tool from apify import Actor OPENROUTER_BASE_URL = 'https://openrouter.apify.actor/api/v1' +HN_FRONT_PAGE_URL = 'https://hn.algolia.com/api/v1/search?tags=front_page&hitsPerPage=50' +TOP_DOMAINS = 5 + + +class ActorInput(BaseModel): + """The Actor input, validated with default values.""" + + model: str = 'openai/gpt-5.4-mini' + + +class DomainStat(BaseModel): + """One domain's slice of the Hacker News front page.""" + + domain: str + story_count: int + average_points: float + + +class FrontPageReport(BaseModel): + """The structured analysis the agent computes from the front page.""" + + total_stories: int + top_domains: list[DomainStat] + + +@tool +def fetch_front_page() -> list[dict]: + """Fetch the current Hacker News front page. + + Returns a list of stories, each a dict with `title`, `url`, and `points` keys. + """ + client = impit.Client(browser='firefox', follow_redirects=True, timeout=30) + hits = client.get(HN_FRONT_PAGE_URL).json()['hits'] + return [ + {'title': hit['title'], 'url': hit['url'], 'points': hit['points']} + for hit in hits + if hit.get('url') + ] + + +def is_valid_report(final_answer: object, *_: object, **__: object) -> bool: + """Check the agent's answer against `FrontPageReport` so the schema is enforced.""" + try: + FrontPageReport.model_validate(final_answer) + except ValidationError: + return False + return True async def main() -> None: async with Actor: - actor_input = await Actor.get_input() or {} - topic = actor_input.get('topic', 'open source AI') - model = actor_input.get('model', 'openai/gpt-4o-mini') + # Parse the Actor input into the typed model, filling in defaults. + actor_input = ActorInput.model_validate(await Actor.get_input() or {}) + model = actor_input.model # Route the LLM through the Apify OpenRouter proxy (no provider key needed). llm = OpenAIServerModel( @@ -20,13 +69,32 @@ async def main() -> None: api_base=OPENROUTER_BASE_URL, api_key=os.environ['APIFY_TOKEN'], ) - # A `CodeAgent` writes and runs Python code to solve the task. Here it uses - # `WebSearchTool` to gather the latest news before summarizing it. - agent = CodeAgent(tools=[WebSearchTool()], model=llm) - result = agent.run(f'Find and summarize the latest news about {topic}.') - Actor.log.info(f'Summary:\n{result}') - await Actor.push_data({'topic': topic, 'summary': str(result)}) + # A `CodeAgent` solves the task by writing and running Python, so it groups + # and averages the stories in code. `final_answer_checks` re-runs + # `is_valid_report` on each answer until the output validates. + agent = CodeAgent( + tools=[fetch_front_page], + model=llm, + additional_authorized_imports=['collections', 'statistics', 'urllib.parse'], + final_answer_checks=[is_valid_report], + ) + + prompt = ( + 'Analyze the current Hacker News front page. Call `fetch_front_page`, ' + 'then use code to take the domain of each story URL, group the stories ' + f'by domain, and find the {TOP_DOMAINS} domains with the most stories. ' + 'Call `final_answer` with a dict shaped like {"total_stories": int, ' + '"top_domains": [{"domain": str, "story_count": int, ' + '"average_points": float}]}.' + ) + # `CodeAgent.run` is synchronous, so `asyncio.to_thread` keeps it off the + # Actor's event loop. + result = await asyncio.to_thread(agent.run, prompt) + + report = FrontPageReport.model_validate(result) + Actor.log.info(f'Front page report:\n{report.model_dump_json(indent=2)}') + await Actor.push_data(report.model_dump()) if __name__ == '__main__': From 0a9385dcef0f33e6b49a1d71f70a7436914bae28 Mon Sep 17 00:00:00 2001 From: Vlada Dusek Date: Wed, 8 Jul 2026 17:49:15 +0200 Subject: [PATCH 09/10] Improve code examples --- docs/03_guides/14_ai_agents.mdx | 25 +++++++++++++------------ docs/03_guides/code/14_llamaindex.py | 13 ++++++++++--- docs/03_guides/code/14_pydanticai.py | 13 +++++++++++-- docs/03_guides/code/14_smolagents.py | 5 +++-- 4 files changed, 37 insertions(+), 19 deletions(-) diff --git a/docs/03_guides/14_ai_agents.mdx b/docs/03_guides/14_ai_agents.mdx index aa1d1237..11f10506 100644 --- a/docs/03_guides/14_ai_agents.mdx +++ b/docs/03_guides/14_ai_agents.mdx @@ -18,7 +18,7 @@ In this guide, you'll learn how to host an AI agent as an Apify Actor, using the An AI agent is a program that uses a large language model (LLM) to decide what to do next. Instead of following a fixed script, it reasons about a task, calls tools to gather information or take actions, and loops until the task is done. The Python ecosystem offers several frameworks for building such agents, and the Apify SDK doesn't lock you into any of them. -Apify Actors are a natural home for agents: +Apify Actors are well suited to hosting agents: - An agent is just a Python program, so wrap it in `async with Actor:` to run it on the [Apify platform](https://docs.apify.com/platform). - The platform scales runs, schedules them, and keeps logs, so you don't operate any infrastructure yourself. @@ -67,7 +67,7 @@ Call a provider such as OpenAI directly with your own API key. Keep the key out [LangGraph](https://www.langchain.com/langgraph) models an agent as a graph, where nodes do the work and edges decide what runs next. The explicit state and control flow make complex, multi-step logic easy to follow, including loops that revisit earlier nodes. It builds on [LangChain](https://www.langchain.com/), so any LangChain chat model works. To install it, use: ```bash -pip install langchain-openai langgraph pydantic +pip install langchain-openai langchain-core langgraph pydantic ``` The install also includes [Pydantic](https://docs.pydantic.dev/) for the Actor's typed input and output models. @@ -87,7 +87,7 @@ Note that: ## CrewAI -[CrewAI](https://www.crewai.com/) models a problem as a "crew" of role-playing agents, each with its own role, goal, and backstory. The agents work through tasks in sequence, handing results from one agent to the next. It's a natural fit when a job splits into specialized roles that collaborate. To install it, use: +[CrewAI](https://www.crewai.com/) models a problem as a "crew" of role-playing agents, each with its own role, goal, and backstory. The agents work through tasks in sequence, handing results from one agent to the next. It fits well when a job splits into specialized roles that collaborate. To install it, use: ```bash pip install "crewai[tools]" pydantic @@ -105,13 +105,13 @@ Note that: - CrewAI's `LLM` points its `base_url` at the Apify OpenRouter proxy. The `openai/` model prefix selects its OpenAI-compatible client, so no provider key is needed. - The `researcher` reads the page with `ApifyActorsTool`, which wraps the [RAG Web Browser](https://apify.com/apify/rag-web-browser) Actor and returns the page as clean Markdown. The `writer` then turns its notes into the result. Listing both agents in one `Crew` runs them in order, and `context=[research]` hands the researcher's notes to the writer. -- `output_pydantic=CrawlerGuide` makes the writer's task return a validated `CrawlerGuide`, read from `result.pydantic` and stored as one dataset row per crawler. +- `output_pydantic=CrawlerGuide` makes the writer's task return a validated `CrawlerGuide`, read from the crew result's `.pydantic` and stored as one dataset row per crawler. - `kickoff_async` runs the crew without blocking the Actor's event loop. - On a fresh container, CrewAI shows a one-time trace-consent prompt. The template sets `CREWAI_TESTING=true` to suppress it. Do the same in your Dockerfile. ## PydanticAI -[PydanticAI](https://ai.pydantic.dev/) is an agent framework from the team behind Pydantic. It's strongly typed and integrates naturally with the [Pydantic models](./input-validation) the Apify SDK already uses. Tool arguments and agent outputs are validated against your models, so the data stays structured end to end. To install it, use: +[PydanticAI](https://ai.pydantic.dev/) is an agent framework from the team behind Pydantic. It's strongly typed and integrates with the [Pydantic models](./input-validation) the Apify SDK already uses. Tool arguments and agent outputs are validated against your models, so the data stays structured end to end. To install it, use: ```bash pip install pydantic-ai pydantic impit @@ -136,11 +136,12 @@ Note that: [LlamaIndex](https://www.llamaindex.ai/) is built for retrieval-augmented agents that reason over your own data. It indexes your documents into a vector store, retrieves the passages relevant to a question, and grounds the answer in them. It's a strong fit for question answering over private or domain-specific content. To install it, use: ```bash -pip install llama-index \ - llama-index-llms-openai-like \ - llama-index-embeddings-huggingface \ - llama-index-readers-apify \ - pydantic +pip install \ + llama-index \ + llama-index-llms-openai-like \ + llama-index-embeddings-huggingface \ + llama-index-readers-apify \ + pydantic ``` The install also includes [Pydantic](https://docs.pydantic.dev/) for the Actor's typed input and output models. @@ -161,7 +162,7 @@ Note that: ## Smolagents -[Smolagents](https://huggingface.co/docs/smolagents) is a lightweight framework whose agents write and run Python code to solve a task, instead of emitting structured tool calls. Writing code lets the agent chain several operations, loop, and transform data in a single step. It's a good fit for tasks that are naturally expressed as code. To install it, use: +[Smolagents](https://huggingface.co/docs/smolagents) is a lightweight framework whose agents write and run Python code to solve a task, instead of emitting structured tool calls. Writing code lets the agent chain several operations, loop, and transform data in a single step. It's a good fit for tasks that map cleanly to code. To install it, use: ```bash pip install "smolagents[openai]" pydantic impit @@ -220,7 +221,7 @@ To monetize the agent, use [pay-per-event charging](../concepts/pay-per-event). await Actor.charge('task-completed') ``` -This approach lets you charge users directly from the Actor and cover the cost of execution and LLM tokens. It pairs naturally with the Apify OpenRouter proxy, which already bills the LLM usage to the account running the Actor. +This approach lets you charge users directly from the Actor and cover the cost of execution and LLM tokens. It pairs well with the Apify OpenRouter proxy, which already bills the LLM usage to the account running the Actor. ## Conclusion diff --git a/docs/03_guides/code/14_llamaindex.py b/docs/03_guides/code/14_llamaindex.py index 69dae080..fee20b70 100644 --- a/docs/03_guides/code/14_llamaindex.py +++ b/docs/03_guides/code/14_llamaindex.py @@ -52,8 +52,12 @@ async def main() -> None: api_key=os.environ['APIFY_TOKEN'], is_chat_model=True, ) - # Embeddings run locally, so the proxy needs no embeddings endpoint. - embed_model = HuggingFaceEmbedding(model_name=EMBED_MODEL) + # Embeddings run locally, so the proxy needs no embeddings endpoint. Building the + # model downloads and loads it on first run, which blocks, so offload it with + # `asyncio.to_thread` to keep the Actor's event loop responsive. + embed_model = await asyncio.to_thread( + HuggingFaceEmbedding, model_name=EMBED_MODEL + ) # Scrape the pages with the Website Content Crawler Actor and wrap each one # in a `Document`. LlamaIndex then chunks and embeds them, so the query engine @@ -66,7 +70,10 @@ async def main() -> None: run_input=run_input, dataset_mapping_function=to_document, ) - index = VectorStoreIndex.from_documents(documents, embed_model=embed_model) + # Chunking and embedding every document blocks too, so offload it the same way. + index = await asyncio.to_thread( + VectorStoreIndex.from_documents, documents, embed_model=embed_model + ) # `output_cls` returns a validated `Answer`. The response still carries the # retrieved `source_nodes`, so the answer can cite the pages it came from. diff --git a/docs/03_guides/code/14_pydanticai.py b/docs/03_guides/code/14_pydanticai.py index a7a48eb4..4cd74cc4 100644 --- a/docs/03_guides/code/14_pydanticai.py +++ b/docs/03_guides/code/14_pydanticai.py @@ -1,5 +1,6 @@ import asyncio import os +from http import HTTPStatus import impit from pydantic import BaseModel @@ -40,8 +41,16 @@ class PackageReport(BaseModel): async def fetch_pypi_metadata(name: str) -> PackageFacts: """Fetch a package's metadata from the PyPI JSON API.""" - client = impit.AsyncClient(browser='firefox', follow_redirects=True, timeout=30) - info = (await client.get(PYPI_JSON_URL.format(name=name))).json()['info'] + # `impit.AsyncClient` has no `close()`, so use a context manager to release the pool. + async with impit.AsyncClient( + browser='firefox', follow_redirects=True, timeout=30 + ) as client: + response = await client.get(PYPI_JSON_URL.format(name=name)) + # PyPI returns 404 with no `info` key for an unknown package, so fail with a clear + # message the agent can act on instead of a bare `KeyError`. + if response.status_code != HTTPStatus.OK: + raise RuntimeError(f'PyPI has no package named "{name}".') + info = response.json()['info'] return PackageFacts( name=info['name'], version=info['version'], diff --git a/docs/03_guides/code/14_smolagents.py b/docs/03_guides/code/14_smolagents.py index b5620790..862adedc 100644 --- a/docs/03_guides/code/14_smolagents.py +++ b/docs/03_guides/code/14_smolagents.py @@ -39,8 +39,9 @@ def fetch_front_page() -> list[dict]: Returns a list of stories, each a dict with `title`, `url`, and `points` keys. """ - client = impit.Client(browser='firefox', follow_redirects=True, timeout=30) - hits = client.get(HN_FRONT_PAGE_URL).json()['hits'] + # `impit.Client` has no `close()`, so use a context manager to release the pool. + with impit.Client(browser='firefox', follow_redirects=True, timeout=30) as client: + hits = client.get(HN_FRONT_PAGE_URL).json()['hits'] return [ {'title': hit['title'], 'url': hit['url'], 'points': hit['points']} for hit in hits From 094230a50235dd0496471dadeace13be00e97c62 Mon Sep 17 00:00:00 2001 From: Vlada Dusek Date: Wed, 8 Jul 2026 21:16:29 +0200 Subject: [PATCH 10/10] copy content to v3.4 --- docs/03_guides/14_ai_agents.mdx | 17 +- docs/03_guides/code/14_crewai.py | 47 +++- docs/03_guides/code/14_langgraph.py | 7 +- docs/03_guides/code/14_llamaindex.py | 31 +-- docs/03_guides/code/14_pydanticai.py | 6 +- docs/03_guides/code/14_smolagents.py | 9 +- .../version-3.4/01_introduction/index.mdx | 2 +- .../01_introduction/quick-start.mdx | 1 + .../version-3.4/03_guides/14_ai_agents.mdx | 239 ++++++++++++++++++ .../version-3.4/03_guides/code/14_crewai.py | 123 +++++++++ .../03_guides/code/14_langgraph.py | 106 ++++++++ .../03_guides/code/14_llamaindex.py | 91 +++++++ .../03_guides/code/14_pydanticai.py | 93 +++++++ .../03_guides/code/14_smolagents.py | 99 ++++++++ 14 files changed, 819 insertions(+), 52 deletions(-) create mode 100644 website/versioned_docs/version-3.4/03_guides/14_ai_agents.mdx create mode 100644 website/versioned_docs/version-3.4/03_guides/code/14_crewai.py create mode 100644 website/versioned_docs/version-3.4/03_guides/code/14_langgraph.py create mode 100644 website/versioned_docs/version-3.4/03_guides/code/14_llamaindex.py create mode 100644 website/versioned_docs/version-3.4/03_guides/code/14_pydanticai.py create mode 100644 website/versioned_docs/version-3.4/03_guides/code/14_smolagents.py diff --git a/docs/03_guides/14_ai_agents.mdx b/docs/03_guides/14_ai_agents.mdx index 11f10506..f235e3ff 100644 --- a/docs/03_guides/14_ai_agents.mdx +++ b/docs/03_guides/14_ai_agents.mdx @@ -90,12 +90,12 @@ Note that: [CrewAI](https://www.crewai.com/) models a problem as a "crew" of role-playing agents, each with its own role, goal, and backstory. The agents work through tasks in sequence, handing results from one agent to the next. It fits well when a job splits into specialized roles that collaborate. To install it, use: ```bash -pip install "crewai[tools]" pydantic +pip install crewai pydantic ``` The install also includes [Pydantic](https://docs.pydantic.dev/) for the Actor's typed input and output models. -The following Actor runs a two-agent crew. A researcher scrapes a [Crawlee](https://crawlee.dev/python) docs page with the [RAG Web Browser](https://apify.com/apify/rag-web-browser) Actor, exposed as a tool by `ApifyActorsTool` from the [`crewai-tools`](https://github.com/crewAIInc/crewAI/tree/main/lib/crewai-tools) extra. A writer then distills the notes into a structured guide of Crawlee's crawlers, each with its name, what it's built on, and what it's best for: +The following Actor runs a two-agent crew. A researcher scrapes a [Crawlee](https://crawlee.dev/python) docs page with the [RAG Web Browser](https://apify.com/apify/rag-web-browser) Actor, exposed to the crew through a small custom tool. A writer then distills the notes into a structured guide of Crawlee's crawlers, each with its name, what it's built on, and what it's best for: {CrewaiExample} @@ -104,9 +104,9 @@ The following Actor runs a two-agent crew. A researcher scrapes a [Crawlee](http Note that: - CrewAI's `LLM` points its `base_url` at the Apify OpenRouter proxy. The `openai/` model prefix selects its OpenAI-compatible client, so no provider key is needed. -- The `researcher` reads the page with `ApifyActorsTool`, which wraps the [RAG Web Browser](https://apify.com/apify/rag-web-browser) Actor and returns the page as clean Markdown. The `writer` then turns its notes into the result. Listing both agents in one `Crew` runs them in order, and `context=[research]` hands the researcher's notes to the writer. +- The `researcher` reads the page with `WebBrowserTool`, a custom tool that runs the [RAG Web Browser](https://apify.com/apify/rag-web-browser) Actor and returns the page as clean Markdown. The `writer` then turns its notes into the result. Listing both agents in one `Crew` runs them in order, and `context=[research]` hands the researcher's notes to the writer. +- `WebBrowserTool` subclasses CrewAI's `BaseTool`, so the crew needs no third-party tool wrapper. Its `_run` method is `async`, and CrewAI runs it natively, so the tool awaits `Actor.call` directly with no manual event-loop bridge. - `output_pydantic=CrawlerGuide` makes the writer's task return a validated `CrawlerGuide`, read from the crew result's `.pydantic` and stored as one dataset row per crawler. -- `kickoff_async` runs the crew without blocking the Actor's event loop. - On a fresh container, CrewAI shows a one-time trace-consent prompt. The template sets `CREWAI_TESTING=true` to suppress it. Do the same in your Dockerfile. ## PydanticAI @@ -140,13 +140,12 @@ pip install \ llama-index \ llama-index-llms-openai-like \ llama-index-embeddings-huggingface \ - llama-index-readers-apify \ pydantic ``` The install also includes [Pydantic](https://docs.pydantic.dev/) for the Actor's typed input and output models. -The following Actor builds a retrieval-augmented generation (RAG) pipeline. It scrapes a few pages with the [Website Content Crawler](https://apify.com/apify/website-content-crawler) Actor through the [`llama-index-readers-apify`](https://pypi.org/project/llama-index-readers-apify/) reader, which turns each crawled page into a `Document`. It then indexes the documents in a vector store, answers a question using only the retrieved passages, and cites its sources: +The following Actor builds a retrieval-augmented generation (RAG) pipeline. It scrapes a few pages with the [Website Content Crawler](https://apify.com/apify/website-content-crawler) Actor via `Actor.call`, wraps each crawled page in a `Document`, indexes the documents in a vector store, answers a question using only the retrieved passages, and cites its sources: {LlamaindexExample} @@ -158,7 +157,7 @@ Note that: - `Document` wraps each scraped page, and `VectorStoreIndex.from_documents` chunks and embeds them. The index is the retrieval layer that sets LlamaIndex apart from a plain agent loop. - Embeddings run locally through `HuggingFaceEmbedding` from the `llama-index-embeddings-huggingface` package, so the proxy needs no embeddings endpoint. - `index.as_query_engine(output_cls=Answer)` retrieves the passages most relevant to the question, then the LLM composes a validated `Answer` grounded in them. -- Every response carries the passages it used in `response.source_nodes`, so the Actor pushes their `url` metadata as citations. The `ApifyActor` reader runs the Website Content Crawler and maps each crawled page to a `Document`, so indexing more content is a matter of crawling more pages. For details, see [Using Apify Actors as tools](#using-apify-actors-as-tools). +- Every response carries the passages it used in `response.source_nodes`, so the Actor pushes their `url` metadata as citations. `Actor.call` runs the Website Content Crawler, and `to_document` maps each dataset item to a `Document`, so indexing more content is a matter of crawling more pages. For details, see [Using Apify Actors as tools](#using-apify-actors-as-tools). ## Smolagents @@ -197,8 +196,6 @@ from langchain_apify import ApifyActorsTool tools = [ApifyActorsTool('apify/instagram-scraper')] ``` -CrewAI ships the same tool as `crewai_tools.ApifyActorsTool` in its `crewai[tools]` package. - For any other framework, call the Actor with `Actor.call` and read its dataset through the preconfigured API client: ```python @@ -209,6 +206,8 @@ async def scrape_instagram(handle: str) -> list[dict]: return (await Actor.apify_client.dataset(run.default_dataset_id).list_items()).items ``` +The [CrewAI](#crewai) example wraps this pattern in a `BaseTool` so its agent can call the Actor. + For details on calling other Actors, see [Interacting with other Actors](../concepts/interacting-with-other-actors). ## Running on the Apify platform diff --git a/docs/03_guides/code/14_crewai.py b/docs/03_guides/code/14_crewai.py index 9d0b7597..d1b7875a 100644 --- a/docs/03_guides/code/14_crewai.py +++ b/docs/03_guides/code/14_crewai.py @@ -2,8 +2,8 @@ import os from crewai import LLM, Agent, Crew, Task -from crewai_tools import ApifyActorsTool -from pydantic import BaseModel +from crewai.tools import BaseTool +from pydantic import BaseModel, Field from apify import Actor @@ -13,14 +13,11 @@ # stdin. `CREWAI_TESTING=true` suppresses it. os.environ.setdefault('CREWAI_TESTING', 'true') -# The Crawlee docs page the crew reads when the input has no `url`. -DEFAULT_URL = 'https://crawlee.dev/python/docs/guides/architecture-overview' - class ActorInput(BaseModel): """The Actor input, validated with default values.""" - url: str = DEFAULT_URL + url: str = 'https://crawlee.dev/python/docs/guides/architecture-overview' model: str = 'openai/gpt-5.4-mini' @@ -38,6 +35,29 @@ class CrawlerGuide(BaseModel): crawlers: list[Crawler] +class WebBrowserToolInput(BaseModel): + """The single argument the agent passes to the web browser tool.""" + + url: str = Field(description='The URL of the web page to fetch.') + + +class WebBrowserTool(BaseTool): + """A minimal CrewAI tool that runs an Apify Actor to fetch a page as Markdown.""" + + name: str = 'web_browser' + description: str = 'Fetch a web page and return its content as clean Markdown.' + args_schema: type[BaseModel] = WebBrowserToolInput + + async def _run(self, url: str) -> str: + run = await Actor.call( + actor_id='apify/rag-web-browser', + run_input={'query': url, 'maxResults': 1}, + ) + dataset = Actor.apify_client.dataset(run.default_dataset_id) + items = (await dataset.list_items()).items + return '\n\n'.join(item.get('markdown') or '' for item in items) + + async def main() -> None: async with Actor: # Parse the Actor input into the typed model, filling in defaults. @@ -46,22 +66,23 @@ async def main() -> None: model = actor_input.model # Route the LLM through the Apify OpenRouter proxy (no provider key needed). - # The `openai/` prefix selects CrewAI's OpenAI-compatible client. llm = LLM( - model=f'openai/{model}', + model=model, base_url=OPENROUTER_BASE_URL, api_key=os.environ['APIFY_TOKEN'], + provider='openai', ) - # `ApifyActorsTool` exposes any Apify Actor as a CrewAI tool. Here it wraps the - # RAG Web Browser to fetch the page as clean Markdown. + # `WebBrowserTool` is our own Actor-backed tool, wrapping the RAG Web Browser + # to fetch the page as clean Markdown. researcher = Agent( role='Documentation researcher', goal='Read the Crawlee docs and note every crawler it describes.', backstory='A researcher who reads technical docs closely.', - tools=[ApifyActorsTool('apify/rag-web-browser')], + tools=[WebBrowserTool()], llm=llm, ) + writer = Agent( role='Technical writer', goal='Turn research notes into a clear, structured crawler guide.', @@ -74,6 +95,7 @@ async def main() -> None: expected_output='Notes on each crawler: name, what it builds on, its use.', agent=researcher, ) + # `context=[research]` feeds the researcher's notes to the writer, and # `output_pydantic` makes the final task return a validated `CrawlerGuide`. write = Task( @@ -87,11 +109,12 @@ async def main() -> None: output_pydantic=CrawlerGuide, ) - # `kickoff_async` runs the crew without blocking the Actor's event loop. crew = Crew(agents=[researcher, writer], tasks=[research, write]) guide = (await crew.kickoff_async()).pydantic + if guide is None: raise RuntimeError('The crew did not return a structured CrawlerGuide.') + Actor.log.info(f'Crawler guide:\n{guide.model_dump_json(indent=2)}') await Actor.push_data([crawler.model_dump() for crawler in guide.crawlers]) diff --git a/docs/03_guides/code/14_langgraph.py b/docs/03_guides/code/14_langgraph.py index 02c22049..b7110b50 100644 --- a/docs/03_guides/code/14_langgraph.py +++ b/docs/03_guides/code/14_langgraph.py @@ -41,7 +41,7 @@ class State(TypedDict): async def fetch(state: State) -> dict: - """Node: scrape the page to clean Markdown with the RAG Web Browser Actor.""" + """A node that scrapes the page to clean Markdown with the RAG Web Browser Actor.""" run_input = {'query': state['url'], 'outputFormats': ['markdown']} run = await Actor.call('apify/rag-web-browser', run_input=run_input) dataset = Actor.apify_client.dataset(run.default_dataset_id) @@ -52,7 +52,7 @@ async def fetch(state: State) -> dict: async def summarize(state: State, structured_llm: Runnable) -> dict: - """Node: summarize the page, asking for more depth on a re-run.""" + """A node that summarizes the page, asking for more depth on a re-run.""" hint = '' if state['revisions']: hint = f' List at least {MIN_KEY_POINTS} distinct key points.' @@ -62,7 +62,7 @@ async def summarize(state: State, structured_llm: Runnable) -> dict: def route(state: State) -> str: - """Edge: loop back for another pass while the summary is thin.""" + """A conditional edge that loops back for another pass while the summary is thin.""" thin = len(state['summary'].key_points) < MIN_KEY_POINTS if thin and state['revisions'] < MAX_REVISIONS: return 'summarize' @@ -82,6 +82,7 @@ async def main() -> None: base_url=OPENROUTER_BASE_URL, api_key=os.environ['APIFY_TOKEN'], ) + # `with_structured_output` makes the node return a validated `PageSummary`. structured_llm = llm.with_structured_output(PageSummary) diff --git a/docs/03_guides/code/14_llamaindex.py b/docs/03_guides/code/14_llamaindex.py index fee20b70..d68fb05c 100644 --- a/docs/03_guides/code/14_llamaindex.py +++ b/docs/03_guides/code/14_llamaindex.py @@ -4,7 +4,6 @@ from llama_index.core import Document, VectorStoreIndex from llama_index.embeddings.huggingface import HuggingFaceEmbedding from llama_index.llms.openai_like import OpenAILike -from llama_index.readers.apify import ApifyActor from pydantic import BaseModel from apify import Actor @@ -32,9 +31,13 @@ class Answer(BaseModel): key_facts: list[str] -def to_document(item: dict) -> Document: - """Map a Website Content Crawler item to a `Document` tagged with its URL.""" - return Document(text=item['text'], metadata={'url': item['url']}) +async def scrape_documents(urls: list[str]) -> list[Document]: + """Scrape the pages with the Website Content Crawler and wrap them as `Document`s.""" + run_input = {'startUrls': [{'url': url} for url in urls], 'maxCrawlDepth': 0} + run = await Actor.call('apify/website-content-crawler', run_input=run_input) + dataset = Actor.apify_client.dataset(run.default_dataset_id) + items = (await dataset.list_items()).items + return [Document(text=item['text'], metadata={'url': item['url']}) for item in items] async def main() -> None: @@ -52,24 +55,16 @@ async def main() -> None: api_key=os.environ['APIFY_TOKEN'], is_chat_model=True, ) - # Embeddings run locally, so the proxy needs no embeddings endpoint. Building the - # model downloads and loads it on first run, which blocks, so offload it with - # `asyncio.to_thread` to keep the Actor's event loop responsive. + + # Embeddings run locally, so the proxy needs no embeddings endpoint. Loading the + # model blocks, so offload it with `asyncio.to_thread`. embed_model = await asyncio.to_thread( HuggingFaceEmbedding, model_name=EMBED_MODEL ) - # Scrape the pages with the Website Content Crawler Actor and wrap each one - # in a `Document`. LlamaIndex then chunks and embeds them, so the query engine - # retrieves only the relevant passages. - reader = ApifyActor(apify_api_token=os.environ['APIFY_TOKEN']) - run_input = {'startUrls': [{'url': url} for url in urls], 'maxCrawlDepth': 0} - documents = await asyncio.to_thread( - reader.load_data, - actor_id='apify/website-content-crawler', - run_input=run_input, - dataset_mapping_function=to_document, - ) + # Scrape the pages and wrap each result as a `Document` for the vector index. + documents = await scrape_documents(urls) + # Chunking and embedding every document blocks too, so offload it the same way. index = await asyncio.to_thread( VectorStoreIndex.from_documents, documents, embed_model=embed_model diff --git a/docs/03_guides/code/14_pydanticai.py b/docs/03_guides/code/14_pydanticai.py index 4cd74cc4..f491c5ea 100644 --- a/docs/03_guides/code/14_pydanticai.py +++ b/docs/03_guides/code/14_pydanticai.py @@ -41,15 +41,14 @@ class PackageReport(BaseModel): async def fetch_pypi_metadata(name: str) -> PackageFacts: """Fetch a package's metadata from the PyPI JSON API.""" - # `impit.AsyncClient` has no `close()`, so use a context manager to release the pool. async with impit.AsyncClient( browser='firefox', follow_redirects=True, timeout=30 ) as client: response = await client.get(PYPI_JSON_URL.format(name=name)) - # PyPI returns 404 with no `info` key for an unknown package, so fail with a clear - # message the agent can act on instead of a bare `KeyError`. + if response.status_code != HTTPStatus.OK: raise RuntimeError(f'PyPI has no package named "{name}".') + info = response.json()['info'] return PackageFacts( name=info['name'], @@ -71,6 +70,7 @@ async def main() -> None: base_url=OPENROUTER_BASE_URL, api_key=os.environ['APIFY_TOKEN'], ) + # `output_type` makes the agent return a validated `PackageReport`. Passing # `fetch_pypi_metadata` in `tools` registers it as a tool from its signature. agent = Agent( diff --git a/docs/03_guides/code/14_smolagents.py b/docs/03_guides/code/14_smolagents.py index 862adedc..5565d163 100644 --- a/docs/03_guides/code/14_smolagents.py +++ b/docs/03_guides/code/14_smolagents.py @@ -35,17 +35,14 @@ class FrontPageReport(BaseModel): @tool def fetch_front_page() -> list[dict]: - """Fetch the current Hacker News front page. - - Returns a list of stories, each a dict with `title`, `url`, and `points` keys. - """ - # `impit.Client` has no `close()`, so use a context manager to release the pool. + """Fetch the Hacker News front page as story dicts with title, url, and points.""" with impit.Client(browser='firefox', follow_redirects=True, timeout=30) as client: hits = client.get(HN_FRONT_PAGE_URL).json()['hits'] + return [ {'title': hit['title'], 'url': hit['url'], 'points': hit['points']} for hit in hits - if hit.get('url') + if hit.get('url') and hit.get('points') is not None ] diff --git a/website/versioned_docs/version-3.4/01_introduction/index.mdx b/website/versioned_docs/version-3.4/01_introduction/index.mdx index b77a13c4..1d026e73 100644 --- a/website/versioned_docs/version-3.4/01_introduction/index.mdx +++ b/website/versioned_docs/version-3.4/01_introduction/index.mdx @@ -41,7 +41,7 @@ Almost any Python project can become an Actor, including projects for: - **Web scraping and crawling** - The SDK is fully compatible with [Crawlee](https://crawlee.dev/python), which makes Apify a natural place to deploy and scale your crawlers (see the [Crawlee guide](./guides/crawlee)). It also works with other popular scraping libraries, such as [Scrapy](./guides/scrapy), [Scrapling](./guides/scrapling), or [Crawl4AI](./guides/crawl4ai). - **Browser automation** - Drive a real browser with [Playwright](./guides/playwright) or [Selenium](./guides/selenium), or with higher-level tools such as [Browser Use](./guides/browser-use). - **Web servers and APIs** - Run a [web server](./guides/running-webserver) inside an Actor to serve HTTP requests, for example to expose your scraper as a live API. -- **AI agents** - Host agents built with your framework of choice. Ready-made Actor templates cover [PydanticAI](https://apify.com/templates/python-pydanticai), [CrewAI](https://apify.com/templates/python-crewai), [LangGraph](https://apify.com/templates/python-langgraph), [LlamaIndex](https://apify.com/templates/python-llamaindex-agent), and [Smolagents](https://apify.com/templates/python-smolagents). +- **AI agents** - Host agents built with your framework of choice (see the [AI agents guide](./guides/ai-agents)). Ready-made Actor templates cover [LangGraph](https://apify.com/templates/python-langgraph), [CrewAI](https://apify.com/templates/python-crewai), [PydanticAI](https://apify.com/templates/python-pydanticai), [LlamaIndex](https://apify.com/templates/python-llamaindex-agent), and [Smolagents](https://apify.com/templates/python-smolagents). - **MCP servers** - Deploy a Python MCP server as an Actor and make its tools available to any MCP client (see the [MCP servers guide](./guides/mcp-servers)). Ready-made Actor templates cover the [MCP server](https://apify.com/templates/python-mcp-empty) and [MCP proxy](https://apify.com/templates/python-mcp-proxy). Whatever you build, the Apify SDK doesn't lock you into a particular framework. Bring the libraries you already use, and let Apify run your project in the cloud. diff --git a/website/versioned_docs/version-3.4/01_introduction/quick-start.mdx b/website/versioned_docs/version-3.4/01_introduction/quick-start.mdx index 5722304e..2d2d8ce4 100644 --- a/website/versioned_docs/version-3.4/01_introduction/quick-start.mdx +++ b/website/versioned_docs/version-3.4/01_introduction/quick-start.mdx @@ -111,6 +111,7 @@ To see how you can integrate the Apify SDK with popular scraping libraries and f - [Adaptive scraping with Scrapling](./guides/scrapling) - [LLM-ready scraping with Crawl4AI](./guides/crawl4ai) - [Browser AI agents with Browser Use](./guides/browser-use) +- [Hosting AI agents](./guides/ai-agents) For other aspects of Actor development, explore these guides: diff --git a/website/versioned_docs/version-3.4/03_guides/14_ai_agents.mdx b/website/versioned_docs/version-3.4/03_guides/14_ai_agents.mdx new file mode 100644 index 00000000..f235e3ff --- /dev/null +++ b/website/versioned_docs/version-3.4/03_guides/14_ai_agents.mdx @@ -0,0 +1,239 @@ +--- +id: ai-agents +title: Hosting AI agents +description: Host AI agents built with LangGraph, CrewAI, PydanticAI, LlamaIndex, or Smolagents as Apify Actors. +--- + +import RunnableCodeBlock from '@site/src/components/RunnableCodeBlock'; + +import LanggraphExample from '!!raw-loader!roa-loader!./code/14_langgraph.py'; +import CrewaiExample from '!!raw-loader!roa-loader!./code/14_crewai.py'; +import PydanticaiExample from '!!raw-loader!roa-loader!./code/14_pydanticai.py'; +import LlamaindexExample from '!!raw-loader!roa-loader!./code/14_llamaindex.py'; +import SmolagentsExample from '!!raw-loader!roa-loader!./code/14_smolagents.py'; + +In this guide, you'll learn how to host an AI agent as an Apify Actor, using the agent framework of your choice. + +## Introduction + +An AI agent is a program that uses a large language model (LLM) to decide what to do next. Instead of following a fixed script, it reasons about a task, calls tools to gather information or take actions, and loops until the task is done. The Python ecosystem offers several frameworks for building such agents, and the Apify SDK doesn't lock you into any of them. + +Apify Actors are well suited to hosting agents: + +- An agent is just a Python program, so wrap it in `async with Actor:` to run it on the [Apify platform](https://docs.apify.com/platform). +- The platform scales runs, schedules them, and keeps logs, so you don't operate any infrastructure yourself. +- [Apify Store](https://apify.com/store) gives your agent thousands of ready-made tools. Any Actor can become a tool the agent calls: a scraper, a browser automation, an API wrapper. +- The [Apify OpenRouter proxy](https://apify.com/apify/openrouter) lets the agent talk to an LLM without managing a provider API key. +- The [pay-per-event charging](../concepts/pay-per-event) lets you monetize the agent and pass LLM costs on to its users. + +Each example in this guide shows a complete, single-file Actor for one framework. They all read the input, run the agent, and store the result in the default dataset. Each one defines its input as a [Pydantic model](./input-validation) with default values, so the fields stay typed and validated. + +## Choosing a framework + +Apify maintains a ready-made Actor template for each of the popular agent frameworks. Each template is a complete, deployable Actor that you can use as a starting point. + +| Framework | Good for | Template | +| --- | --- | --- | +| [LangGraph](#langgraph) | Graph-based agents with explicit state and control flow | [`python-langgraph`](https://apify.com/templates/python-langgraph) | +| [CrewAI](#crewai) | Multi-agent "crews" that collaborate on a task | [`python-crewai`](https://apify.com/templates/python-crewai) | +| [PydanticAI](#pydanticai) | Typed, Pydantic-native agents with tool calling | [`python-pydanticai`](https://apify.com/templates/python-pydanticai) | +| [LlamaIndex](#llamaindex) | Retrieval-augmented agents over your own data | [`python-llamaindex-agent`](https://apify.com/templates/python-llamaindex-agent) | +| [Smolagents](#smolagents) | Lightweight code-writing agents | [`python-smolagents`](https://apify.com/templates/python-smolagents) | + +All of these templates live in the [actor-templates repository](https://github.com/apify/actor-templates). To create a project, use the [Apify CLI](https://docs.apify.com/cli), for example: + +```bash +apify create my-agent --template python-pydanticai +``` + +## Connecting to an LLM + +There are two ways to give an agent an LLM. + +### Apify OpenRouter proxy + +The [Apify OpenRouter proxy](https://apify.com/apify/openrouter) is an OpenAI-compatible endpoint at `https://openrouter.apify.actor/api/v1` that fronts the full [OpenRouter](https://openrouter.ai) model catalog. Any framework with an OpenAI-compatible client can point its base URL at the proxy, which is what every example in this guide does. Note that: + +- The token usage is billed against the Apify account running the Actor, so no provider API key is required. +- The Actor authenticates with the proxy using the `APIFY_TOKEN` that the platform injects into every run. +- To switch models, change the `model` input to any [OpenRouter model slug](https://openrouter.ai/models), for example `openai/gpt-5.4-mini`. + +### Your own provider key + +Call a provider such as OpenAI directly with your own API key. Keep the key out of the Actor input and source code. Read it from an environment variable, which on the platform you set as a [secret environment variable](https://docs.apify.com/platform/actors/development/programming-interface/environment-variables) and locally you export in your shell. + +## LangGraph + +[LangGraph](https://www.langchain.com/langgraph) models an agent as a graph, where nodes do the work and edges decide what runs next. The explicit state and control flow make complex, multi-step logic easy to follow, including loops that revisit earlier nodes. It builds on [LangChain](https://www.langchain.com/), so any LangChain chat model works. To install it, use: + +```bash +pip install langchain-openai langchain-core langgraph pydantic +``` + +The install also includes [Pydantic](https://docs.pydantic.dev/) for the Actor's typed input and output models. + +The following Actor builds a small graph with two nodes. A `fetch` node scrapes the page with the [RAG Web Browser](https://apify.com/apify/rag-web-browser) Actor, then a `summarize` node distills the clean Markdown into a structured summary. A conditional edge loops back into `summarize` until the summary is detailed enough: + + + {LanggraphExample} + + +Note that: + +- `ChatOpenAI` points its `base_url` at the Apify OpenRouter proxy, so any LangChain chat model works without a provider key. `with_structured_output(PageSummary)` makes the `summarize` node return a validated `PageSummary`. +- The `State` is a typed dict that flows between nodes. Each node returns a partial update, and LangGraph merges it into the shared state. +- `StateGraph` wires the nodes together: `fetch` runs first, then `summarize`. The `route` function is a conditional edge that either loops back into `summarize` for another pass or ends the run. The cycle and the `revisions` guard in the state are what a plain chain can't express, and they're the reason to reach for LangGraph. +- `fetch` scrapes the page by calling the [RAG Web Browser](https://apify.com/apify/rag-web-browser) Actor with `Actor.call`, then reads the result from its dataset. The Actor returns clean Markdown, so the agent reads text instead of raw HTML and uses fewer tokens. Any Apify scraping Actor works here. For details, see [Using Apify Actors as tools](#using-apify-actors-as-tools). + +## CrewAI + +[CrewAI](https://www.crewai.com/) models a problem as a "crew" of role-playing agents, each with its own role, goal, and backstory. The agents work through tasks in sequence, handing results from one agent to the next. It fits well when a job splits into specialized roles that collaborate. To install it, use: + +```bash +pip install crewai pydantic +``` + +The install also includes [Pydantic](https://docs.pydantic.dev/) for the Actor's typed input and output models. + +The following Actor runs a two-agent crew. A researcher scrapes a [Crawlee](https://crawlee.dev/python) docs page with the [RAG Web Browser](https://apify.com/apify/rag-web-browser) Actor, exposed to the crew through a small custom tool. A writer then distills the notes into a structured guide of Crawlee's crawlers, each with its name, what it's built on, and what it's best for: + + + {CrewaiExample} + + +Note that: + +- CrewAI's `LLM` points its `base_url` at the Apify OpenRouter proxy. The `openai/` model prefix selects its OpenAI-compatible client, so no provider key is needed. +- The `researcher` reads the page with `WebBrowserTool`, a custom tool that runs the [RAG Web Browser](https://apify.com/apify/rag-web-browser) Actor and returns the page as clean Markdown. The `writer` then turns its notes into the result. Listing both agents in one `Crew` runs them in order, and `context=[research]` hands the researcher's notes to the writer. +- `WebBrowserTool` subclasses CrewAI's `BaseTool`, so the crew needs no third-party tool wrapper. Its `_run` method is `async`, and CrewAI runs it natively, so the tool awaits `Actor.call` directly with no manual event-loop bridge. +- `output_pydantic=CrawlerGuide` makes the writer's task return a validated `CrawlerGuide`, read from the crew result's `.pydantic` and stored as one dataset row per crawler. +- On a fresh container, CrewAI shows a one-time trace-consent prompt. The template sets `CREWAI_TESTING=true` to suppress it. Do the same in your Dockerfile. + +## PydanticAI + +[PydanticAI](https://ai.pydantic.dev/) is an agent framework from the team behind Pydantic. It's strongly typed and integrates with the [Pydantic models](./input-validation) the Apify SDK already uses. Tool arguments and agent outputs are validated against your models, so the data stays structured end to end. To install it, use: + +```bash +pip install pydantic-ai pydantic impit +``` + +The install also includes [Pydantic](https://docs.pydantic.dev/) for the Actor's typed input and output models, plus [Impit](https://github.com/apify/impit), the HTTP client the Actor uses to call external APIs. + +The following Actor evaluates a Python package and recommends whether to use it. Because the LLM would otherwise guess from its training data, the agent calls the `fetch_pypi_metadata` tool, which uses [Impit](https://github.com/apify/impit) to fetch live metadata from the PyPI JSON API. It then returns a typed report with the package's name, its latest version, a short summary, and a recommendation grounded in those facts: + + + {PydanticaiExample} + + +Note that: + +- The agent reaches the LLM through the Apify OpenRouter proxy via `OpenAIChatModel` and a custom `OpenAIProvider`, so no provider API key is needed. +- Passing `fetch_pypi_metadata` in the `tools` list registers it as a tool. PydanticAI builds its schema from the function's signature and docstring, and the agent calls it to fetch real facts instead of guessing. Returning a typed `PackageFacts` keeps the data validated on its way back to the agent. +- `output_type=PackageReport` makes the agent return a validated `PackageReport`, which maps directly onto a dataset row. To get plain text back, set `output_type=str`. + +## LlamaIndex + +[LlamaIndex](https://www.llamaindex.ai/) is built for retrieval-augmented agents that reason over your own data. It indexes your documents into a vector store, retrieves the passages relevant to a question, and grounds the answer in them. It's a strong fit for question answering over private or domain-specific content. To install it, use: + +```bash +pip install \ + llama-index \ + llama-index-llms-openai-like \ + llama-index-embeddings-huggingface \ + pydantic +``` + +The install also includes [Pydantic](https://docs.pydantic.dev/) for the Actor's typed input and output models. + +The following Actor builds a retrieval-augmented generation (RAG) pipeline. It scrapes a few pages with the [Website Content Crawler](https://apify.com/apify/website-content-crawler) Actor via `Actor.call`, wraps each crawled page in a `Document`, indexes the documents in a vector store, answers a question using only the retrieved passages, and cites its sources: + + + {LlamaindexExample} + + +Note that: + +- `OpenAILike` points `api_base` at the Apify OpenRouter proxy, so no provider API key is needed. It's the LlamaIndex LLM class for any OpenAI-compatible endpoint. +- `Document` wraps each scraped page, and `VectorStoreIndex.from_documents` chunks and embeds them. The index is the retrieval layer that sets LlamaIndex apart from a plain agent loop. +- Embeddings run locally through `HuggingFaceEmbedding` from the `llama-index-embeddings-huggingface` package, so the proxy needs no embeddings endpoint. +- `index.as_query_engine(output_cls=Answer)` retrieves the passages most relevant to the question, then the LLM composes a validated `Answer` grounded in them. +- Every response carries the passages it used in `response.source_nodes`, so the Actor pushes their `url` metadata as citations. `Actor.call` runs the Website Content Crawler, and `to_document` maps each dataset item to a `Document`, so indexing more content is a matter of crawling more pages. For details, see [Using Apify Actors as tools](#using-apify-actors-as-tools). + +## Smolagents + +[Smolagents](https://huggingface.co/docs/smolagents) is a lightweight framework whose agents write and run Python code to solve a task, instead of emitting structured tool calls. Writing code lets the agent chain several operations, loop, and transform data in a single step. It's a good fit for tasks that map cleanly to code. To install it, use: + +```bash +pip install "smolagents[openai]" pydantic impit +``` + +The install also includes [Pydantic](https://docs.pydantic.dev/) for the Actor's typed input and output models, plus [Impit](https://github.com/apify/impit), the HTTP client the Actor uses to call external APIs. + +The following Actor runs a `CodeAgent` that writes Python to analyze the Hacker News front page. Its `fetch_front_page` tool uses [Impit](https://github.com/apify/impit) to call the Hacker News API, then the agent groups the stories by domain and returns a structured breakdown of the busiest ones: + + + {SmolagentsExample} + + +Note that: + +- `OpenAIServerModel` points `api_base` at the Apify OpenRouter proxy, so no provider API key is needed. +- Writing code is what a `CodeAgent` does best. It calls `fetch_front_page`, then writes Python to take each URL's domain, group the stories, and average their points. `additional_authorized_imports` grants the standard-library modules that code needs. +- `final_answer_checks` validates every candidate answer against `FrontPageReport`. A mismatch sends the agent back to fix its code, so the output stays structured rather than free-form. The returned dict is then parsed with `FrontPageReport.model_validate`. +- `CodeAgent.run` is synchronous, so `asyncio.to_thread` runs it without blocking the Actor's event loop. +- Because a `CodeAgent` executes the code it generates, run it in the isolated Actor container rather than on your own machine. + +## Using Apify Actors as tools + +An agent becomes useful once it can act on the world. On Apify, any of the thousands of Actors in [Apify Store](https://apify.com/store) can become a tool: a scraper that fetches data, a browser automation that fills a form, or an API wrapper that posts a message. + +Some frameworks ship a ready-made wrapper for turning an Actor into a tool. For LangGraph and other [LangChain](https://www.langchain.com/)-based agents, the [`langchain-apify`](https://github.com/apify/langchain-apify) package exposes any Actor as a tool with a single line: + +```python +from langchain_apify import ApifyActorsTool + +# Let the agent scrape Instagram profiles by calling the apify/instagram-scraper Actor. +tools = [ApifyActorsTool('apify/instagram-scraper')] +``` + +For any other framework, call the Actor with `Actor.call` and read its dataset through the preconfigured API client: + +```python +async def scrape_instagram(handle: str) -> list[dict]: + """Scrape recent posts from an Instagram profile.""" + run_input = {'directUrls': [f'https://www.instagram.com/{handle}/']} + run = await Actor.call('apify/instagram-scraper', run_input=run_input) + return (await Actor.apify_client.dataset(run.default_dataset_id).list_items()).items +``` + +The [CrewAI](#crewai) example wraps this pattern in a `BaseTool` so its agent can call the Actor. + +For details on calling other Actors, see [Interacting with other Actors](../concepts/interacting-with-other-actors). + +## Running on the Apify platform + +Agents run on the standard [Apify Python base image](https://hub.docker.com/r/apify/actor-python), so no browser or extra system dependencies are needed. Add `apify` and the packages required by your framework to `requirements.txt`, then deploy the Actor as usual. + +To monetize the agent, use [pay-per-event charging](../concepts/pay-per-event). You define events such as `task-completed` in the Actor's monetization settings and trigger them from the code: + +```python +await Actor.charge('task-completed') +``` + +This approach lets you charge users directly from the Actor and cover the cost of execution and LLM tokens. It pairs well with the Apify OpenRouter proxy, which already bills the LLM usage to the account running the Actor. + +## Conclusion + +In this guide, you learned how to host an AI agent as an Apify Actor. You can now build an agent with LangGraph, CrewAI, PydanticAI, LlamaIndex, or Smolagents, connect it to an LLM through the Apify OpenRouter proxy, give it Apify Actors as tools, and monetize it with pay-per-event. To get started, see the [Actor templates](https://apify.com/templates/categories/python). If you have questions or need assistance, feel free to reach out on our [GitHub](https://github.com/apify/apify-sdk-python) or join our [Discord community](https://discord.com/invite/jyEM2PRvMU). Happy building! + +## Additional resources + +- [Apify: LangGraph template](https://apify.com/templates/python-langgraph) +- [Apify: CrewAI template](https://apify.com/templates/python-crewai) +- [Apify: PydanticAI template](https://apify.com/templates/python-pydanticai) +- [Apify: LlamaIndex template](https://apify.com/templates/python-llamaindex-agent) +- [Apify: Smolagents template](https://apify.com/templates/python-smolagents) +- [Apify: actor-templates repository](https://github.com/apify/actor-templates) +- [Apify: OpenRouter proxy](https://apify.com/apify/openrouter) +- [Apify: langchain-apify integration](https://github.com/apify/langchain-apify) +- [Apify blog: What are AI agents](https://blog.apify.com/what-are-ai-agents/) diff --git a/website/versioned_docs/version-3.4/03_guides/code/14_crewai.py b/website/versioned_docs/version-3.4/03_guides/code/14_crewai.py new file mode 100644 index 00000000..d1b7875a --- /dev/null +++ b/website/versioned_docs/version-3.4/03_guides/code/14_crewai.py @@ -0,0 +1,123 @@ +import asyncio +import os + +from crewai import LLM, Agent, Crew, Task +from crewai.tools import BaseTool +from pydantic import BaseModel, Field + +from apify import Actor + +OPENROUTER_BASE_URL = 'https://openrouter.apify.actor/api/v1' + +# On a fresh container, CrewAI shows a one-time trace-consent prompt that blocks on +# stdin. `CREWAI_TESTING=true` suppresses it. +os.environ.setdefault('CREWAI_TESTING', 'true') + + +class ActorInput(BaseModel): + """The Actor input, validated with default values.""" + + url: str = 'https://crawlee.dev/python/docs/guides/architecture-overview' + model: str = 'openai/gpt-5.4-mini' + + +class Crawler(BaseModel): + """One crawler class that Crawlee provides.""" + + name: str + built_on: str + best_for: str + + +class CrawlerGuide(BaseModel): + """The structured guide the crew distills from the docs page.""" + + crawlers: list[Crawler] + + +class WebBrowserToolInput(BaseModel): + """The single argument the agent passes to the web browser tool.""" + + url: str = Field(description='The URL of the web page to fetch.') + + +class WebBrowserTool(BaseTool): + """A minimal CrewAI tool that runs an Apify Actor to fetch a page as Markdown.""" + + name: str = 'web_browser' + description: str = 'Fetch a web page and return its content as clean Markdown.' + args_schema: type[BaseModel] = WebBrowserToolInput + + async def _run(self, url: str) -> str: + run = await Actor.call( + actor_id='apify/rag-web-browser', + run_input={'query': url, 'maxResults': 1}, + ) + dataset = Actor.apify_client.dataset(run.default_dataset_id) + items = (await dataset.list_items()).items + return '\n\n'.join(item.get('markdown') or '' for item in items) + + +async def main() -> None: + async with Actor: + # Parse the Actor input into the typed model, filling in defaults. + actor_input = ActorInput.model_validate(await Actor.get_input() or {}) + url = actor_input.url + model = actor_input.model + + # Route the LLM through the Apify OpenRouter proxy (no provider key needed). + llm = LLM( + model=model, + base_url=OPENROUTER_BASE_URL, + api_key=os.environ['APIFY_TOKEN'], + provider='openai', + ) + + # `WebBrowserTool` is our own Actor-backed tool, wrapping the RAG Web Browser + # to fetch the page as clean Markdown. + researcher = Agent( + role='Documentation researcher', + goal='Read the Crawlee docs and note every crawler it describes.', + backstory='A researcher who reads technical docs closely.', + tools=[WebBrowserTool()], + llm=llm, + ) + + writer = Agent( + role='Technical writer', + goal='Turn research notes into a clear, structured crawler guide.', + backstory='A writer who distills docs into comparison tables.', + llm=llm, + ) + + research = Task( + description=f'Scrape {url} and list the crawlers the page covers.', + expected_output='Notes on each crawler: name, what it builds on, its use.', + agent=researcher, + ) + + # `context=[research]` feeds the researcher's notes to the writer, and + # `output_pydantic` makes the final task return a validated `CrawlerGuide`. + write = Task( + description=( + 'From the notes, compile each crawler with what it is built on ' + 'and what it is best for.' + ), + expected_output='A list of crawlers with name, built_on, and best_for.', + agent=writer, + context=[research], + output_pydantic=CrawlerGuide, + ) + + crew = Crew(agents=[researcher, writer], tasks=[research, write]) + guide = (await crew.kickoff_async()).pydantic + + if guide is None: + raise RuntimeError('The crew did not return a structured CrawlerGuide.') + + Actor.log.info(f'Crawler guide:\n{guide.model_dump_json(indent=2)}') + await Actor.push_data([crawler.model_dump() for crawler in guide.crawlers]) + + +if __name__ == '__main__': + asyncio.run(main()) diff --git a/website/versioned_docs/version-3.4/03_guides/code/14_langgraph.py b/website/versioned_docs/version-3.4/03_guides/code/14_langgraph.py new file mode 100644 index 00000000..b7110b50 --- /dev/null +++ b/website/versioned_docs/version-3.4/03_guides/code/14_langgraph.py @@ -0,0 +1,106 @@ +import asyncio +import os +from functools import partial +from typing import TypedDict + +from langchain_core.runnables import Runnable +from langchain_openai import ChatOpenAI +from langgraph.graph import END, START, StateGraph +from pydantic import BaseModel + +from apify import Actor + +OPENROUTER_BASE_URL = 'https://openrouter.apify.actor/api/v1' +MIN_KEY_POINTS = 3 +MAX_REVISIONS = 2 + + +class ActorInput(BaseModel): + """The Actor input, validated with default values.""" + + url: str = 'https://crawlee.dev' + model: str = 'openai/gpt-5.4-mini' + + +class PageSummary(BaseModel): + """The structured summary the agent extracts from a web page.""" + + title: str + summary: str + key_points: list[str] + target_audience: str + + +class State(TypedDict): + """The state that flows between the graph's nodes.""" + + url: str + page_text: str + summary: PageSummary + revisions: int + + +async def fetch(state: State) -> dict: + """A node that scrapes the page to clean Markdown with the RAG Web Browser Actor.""" + run_input = {'query': state['url'], 'outputFormats': ['markdown']} + run = await Actor.call('apify/rag-web-browser', run_input=run_input) + dataset = Actor.apify_client.dataset(run.default_dataset_id) + items = (await dataset.list_items()).items + if not items or not items[0].get('markdown'): + raise RuntimeError(f'RAG Web Browser returned no content for {state["url"]}.') + return {'page_text': items[0]['markdown']} + + +async def summarize(state: State, structured_llm: Runnable) -> dict: + """A node that summarizes the page, asking for more depth on a re-run.""" + hint = '' + if state['revisions']: + hint = f' List at least {MIN_KEY_POINTS} distinct key points.' + prompt = f'Summarize this page.{hint}\n\n{state["page_text"]}' + summary = await structured_llm.ainvoke(prompt) + return {'summary': summary, 'revisions': state['revisions'] + 1} + + +def route(state: State) -> str: + """A conditional edge that loops back for another pass while the summary is thin.""" + thin = len(state['summary'].key_points) < MIN_KEY_POINTS + if thin and state['revisions'] < MAX_REVISIONS: + return 'summarize' + return END + + +async def main() -> None: + async with Actor: + # Parse the Actor input into the typed model, filling in defaults. + actor_input = ActorInput.model_validate(await Actor.get_input() or {}) + url = actor_input.url + model = actor_input.model + + # Route the LLM through the Apify OpenRouter proxy (no provider key needed). + llm = ChatOpenAI( + model=model, + base_url=OPENROUTER_BASE_URL, + api_key=os.environ['APIFY_TOKEN'], + ) + + # `with_structured_output` makes the node return a validated `PageSummary`. + structured_llm = llm.with_structured_output(PageSummary) + + # Wire the nodes into a graph. Its conditional edge loops back into `summarize` + # until the summary is detailed enough. `partial` binds `structured_llm` to it. + graph = StateGraph(State) + graph.add_node('fetch', fetch) + graph.add_node('summarize', partial(summarize, structured_llm=structured_llm)) + graph.add_edge(START, 'fetch') + graph.add_edge('fetch', 'summarize') + graph.add_conditional_edges('summarize', route) + agent = graph.compile() + + result = await agent.ainvoke({'url': url, 'revisions': 0}) + summary = result['summary'] + Actor.log.info(f'Page summary:\n{summary.model_dump_json(indent=2)}') + await Actor.push_data({'url': url, **summary.model_dump()}) + + +if __name__ == '__main__': + asyncio.run(main()) diff --git a/website/versioned_docs/version-3.4/03_guides/code/14_llamaindex.py b/website/versioned_docs/version-3.4/03_guides/code/14_llamaindex.py new file mode 100644 index 00000000..d68fb05c --- /dev/null +++ b/website/versioned_docs/version-3.4/03_guides/code/14_llamaindex.py @@ -0,0 +1,91 @@ +import asyncio +import os + +from llama_index.core import Document, VectorStoreIndex +from llama_index.embeddings.huggingface import HuggingFaceEmbedding +from llama_index.llms.openai_like import OpenAILike +from pydantic import BaseModel + +from apify import Actor + +OPENROUTER_BASE_URL = 'https://openrouter.apify.actor/api/v1' +EMBED_MODEL = 'BAAI/bge-small-en-v1.5' + + +class ActorInput(BaseModel): + """The Actor input, validated with default values.""" + + urls: list[str] = [ + 'https://docs.apify.com/platform/actors', + 'https://docs.apify.com/platform/storage/dataset', + 'https://docs.apify.com/platform/proxy', + ] + question: str = 'How does Apify proxy work?' + model: str = 'openai/gpt-5.4-mini' + + +class Answer(BaseModel): + """The grounded answer the query engine returns.""" + + answer: str + key_facts: list[str] + + +async def scrape_documents(urls: list[str]) -> list[Document]: + """Scrape the pages with the Website Content Crawler and wrap them as `Document`s.""" + run_input = {'startUrls': [{'url': url} for url in urls], 'maxCrawlDepth': 0} + run = await Actor.call('apify/website-content-crawler', run_input=run_input) + dataset = Actor.apify_client.dataset(run.default_dataset_id) + items = (await dataset.list_items()).items + return [Document(text=item['text'], metadata={'url': item['url']}) for item in items] + + +async def main() -> None: + async with Actor: + # Parse the Actor input into the typed model, filling in defaults. + actor_input = ActorInput.model_validate(await Actor.get_input() or {}) + urls = actor_input.urls + question = actor_input.question + model = actor_input.model + + # Route the LLM through the Apify OpenRouter proxy (no provider key needed). + llm = OpenAILike( + model=model, + api_base=OPENROUTER_BASE_URL, + api_key=os.environ['APIFY_TOKEN'], + is_chat_model=True, + ) + + # Embeddings run locally, so the proxy needs no embeddings endpoint. Loading the + # model blocks, so offload it with `asyncio.to_thread`. + embed_model = await asyncio.to_thread( + HuggingFaceEmbedding, model_name=EMBED_MODEL + ) + + # Scrape the pages and wrap each result as a `Document` for the vector index. + documents = await scrape_documents(urls) + + # Chunking and embedding every document blocks too, so offload it the same way. + index = await asyncio.to_thread( + VectorStoreIndex.from_documents, documents, embed_model=embed_model + ) + + # `output_cls` returns a validated `Answer`. The response still carries the + # retrieved `source_nodes`, so the answer can cite the pages it came from. + query_engine = index.as_query_engine( + llm=llm, + output_cls=Answer, + response_mode='compact', + similarity_top_k=4, + ) + response = await query_engine.aquery(question) + + answer = response.response + sources = [node.node.metadata['url'] for node in response.source_nodes] + record = {'question': question, **answer.model_dump(), 'sources': sources} + Actor.log.info(f'Answer:\n{answer.model_dump_json(indent=2)}') + await Actor.push_data(record) + + +if __name__ == '__main__': + asyncio.run(main()) diff --git a/website/versioned_docs/version-3.4/03_guides/code/14_pydanticai.py b/website/versioned_docs/version-3.4/03_guides/code/14_pydanticai.py new file mode 100644 index 00000000..f491c5ea --- /dev/null +++ b/website/versioned_docs/version-3.4/03_guides/code/14_pydanticai.py @@ -0,0 +1,93 @@ +import asyncio +import os +from http import HTTPStatus + +import impit +from pydantic import BaseModel +from pydantic_ai import Agent +from pydantic_ai.models.openai import OpenAIChatModel +from pydantic_ai.providers.openai import OpenAIProvider + +from apify import Actor + +OPENROUTER_BASE_URL = 'https://openrouter.apify.actor/api/v1' +PYPI_JSON_URL = 'https://pypi.org/pypi/{name}/json' + + +class ActorInput(BaseModel): + """The Actor input, validated with default values.""" + + package: str = 'crawlee' + model: str = 'openai/gpt-5.4-mini' + + +class PackageFacts(BaseModel): + """The metadata the tool pulls from the PyPI JSON API.""" + + name: str + version: str + summary: str | None + requires_python: str | None + + +class PackageReport(BaseModel): + """The agent's typed verdict on a PyPI package.""" + + name: str + latest_version: str + summary: str + recommendation: str + + +async def fetch_pypi_metadata(name: str) -> PackageFacts: + """Fetch a package's metadata from the PyPI JSON API.""" + async with impit.AsyncClient( + browser='firefox', follow_redirects=True, timeout=30 + ) as client: + response = await client.get(PYPI_JSON_URL.format(name=name)) + + if response.status_code != HTTPStatus.OK: + raise RuntimeError(f'PyPI has no package named "{name}".') + + info = response.json()['info'] + return PackageFacts( + name=info['name'], + version=info['version'], + summary=info['summary'], + requires_python=info['requires_python'], + ) + + +async def main() -> None: + async with Actor: + # Parse the Actor input into the typed model, filling in defaults. + actor_input = ActorInput.model_validate(await Actor.get_input() or {}) + package = actor_input.package + model = actor_input.model + + # Route the LLM through the Apify OpenRouter proxy (no provider key needed). + provider = OpenAIProvider( + base_url=OPENROUTER_BASE_URL, + api_key=os.environ['APIFY_TOKEN'], + ) + + # `output_type` makes the agent return a validated `PackageReport`. Passing + # `fetch_pypi_metadata` in `tools` registers it as a tool from its signature. + agent = Agent( + OpenAIChatModel(model, provider=provider), + output_type=PackageReport, + tools=[fetch_pypi_metadata], + system_prompt=( + 'You advise Python developers on packages. Always call ' + '`fetch_pypi_metadata` for facts instead of guessing.' + ), + ) + + prompt = f'Evaluate the "{package}" package and recommend whether to use it.' + report = (await agent.run(user_prompt=prompt)).output + Actor.log.info(f'Package report:\n{report.model_dump_json(indent=2)}') + await Actor.push_data(report.model_dump()) + + +if __name__ == '__main__': + asyncio.run(main()) diff --git a/website/versioned_docs/version-3.4/03_guides/code/14_smolagents.py b/website/versioned_docs/version-3.4/03_guides/code/14_smolagents.py new file mode 100644 index 00000000..5565d163 --- /dev/null +++ b/website/versioned_docs/version-3.4/03_guides/code/14_smolagents.py @@ -0,0 +1,99 @@ +import asyncio +import os + +import impit +from pydantic import BaseModel, ValidationError +from smolagents import CodeAgent, OpenAIServerModel, tool + +from apify import Actor + +OPENROUTER_BASE_URL = 'https://openrouter.apify.actor/api/v1' +HN_FRONT_PAGE_URL = 'https://hn.algolia.com/api/v1/search?tags=front_page&hitsPerPage=50' +TOP_DOMAINS = 5 + + +class ActorInput(BaseModel): + """The Actor input, validated with default values.""" + + model: str = 'openai/gpt-5.4-mini' + + +class DomainStat(BaseModel): + """One domain's slice of the Hacker News front page.""" + + domain: str + story_count: int + average_points: float + + +class FrontPageReport(BaseModel): + """The structured analysis the agent computes from the front page.""" + + total_stories: int + top_domains: list[DomainStat] + + +@tool +def fetch_front_page() -> list[dict]: + """Fetch the Hacker News front page as story dicts with title, url, and points.""" + with impit.Client(browser='firefox', follow_redirects=True, timeout=30) as client: + hits = client.get(HN_FRONT_PAGE_URL).json()['hits'] + + return [ + {'title': hit['title'], 'url': hit['url'], 'points': hit['points']} + for hit in hits + if hit.get('url') and hit.get('points') is not None + ] + + +def is_valid_report(final_answer: object, *_: object, **__: object) -> bool: + """Check the agent's answer against `FrontPageReport` so the schema is enforced.""" + try: + FrontPageReport.model_validate(final_answer) + except ValidationError: + return False + return True + + +async def main() -> None: + async with Actor: + # Parse the Actor input into the typed model, filling in defaults. + actor_input = ActorInput.model_validate(await Actor.get_input() or {}) + model = actor_input.model + + # Route the LLM through the Apify OpenRouter proxy (no provider key needed). + llm = OpenAIServerModel( + model_id=model, + api_base=OPENROUTER_BASE_URL, + api_key=os.environ['APIFY_TOKEN'], + ) + + # A `CodeAgent` solves the task by writing and running Python, so it groups + # and averages the stories in code. `final_answer_checks` re-runs + # `is_valid_report` on each answer until the output validates. + agent = CodeAgent( + tools=[fetch_front_page], + model=llm, + additional_authorized_imports=['collections', 'statistics', 'urllib.parse'], + final_answer_checks=[is_valid_report], + ) + + prompt = ( + 'Analyze the current Hacker News front page. Call `fetch_front_page`, ' + 'then use code to take the domain of each story URL, group the stories ' + f'by domain, and find the {TOP_DOMAINS} domains with the most stories. ' + 'Call `final_answer` with a dict shaped like {"total_stories": int, ' + '"top_domains": [{"domain": str, "story_count": int, ' + '"average_points": float}]}.' + ) + # `CodeAgent.run` is synchronous, so `asyncio.to_thread` keeps it off the + # Actor's event loop. + result = await asyncio.to_thread(agent.run, prompt) + + report = FrontPageReport.model_validate(result) + Actor.log.info(f'Front page report:\n{report.model_dump_json(indent=2)}') + await Actor.push_data(report.model_dump()) + + +if __name__ == '__main__': + asyncio.run(main())