# Changelog
Source: https://docs.cycls.com/changelog
Product updates and announcements
* **New `@cycls.app()` decorator** - Simplified API with all configuration in one place. Replaces `cycls.Agent()` and `@agent()`.
* **New deployment methods** - Use `app.local()` for development with hot-reload, and `app.deploy()` for cloud deployment.
* **Native UI components** - Stream rich content with thinking bubbles, code blocks, tables, callouts, images, and status indicators by yielding structured objects.
* **Cycls Protocol** - New `/chat/cycls` endpoint for streaming native UI components alongside the OpenAI-compatible `/chat/completions` endpoint.
* **Cycls Pass monetization** - Enable subscription-based access with `plan="cycls_pass"` and gate features based on `context.user.plan`.
* **Auto .env loading** - Cycls automatically loads `.env` files when using `copy=[".env"]`. No need for `python-dotenv`.
* **Claude Agent SDK integration** - New guide for building agents with Anthropic Claude and tool support.
* The chat interface has been redesigned for a cleaner look and now supports image attachments.
* Deprecated `agent.local()` in favor of `agent.deploy(prod=False)`. This creates a full Docker build locally, matching the production environment.
* Cycls Cloud is now live for serverless agent deployment (`agent.deploy(prod=True)`).
* You can now set a custom title for your agent using the `title` parameter in the `@agent` decorator.
* Added `copy_public` parameter to `cycls.Agent()` for easily copying public assets (like logos) to the frontend.
* Replaced multiple API keys with a single `key` parameter in `cycls.Agent()` for cloud deployment authentication.
* Cycls is now open-source [GitHub](https://github.com/Cycls/cycls).
* Use `agent.local()` to serve an agent locally and `agent.deploy()` to deploy it to the Cycls cloud.
* Use `@agent()` decorator to define agent configuration, including `pip`/`apt` dependencies and authentication rules, directly within a Python script.
* All deployed agents now automatically serve a streaming, OpenAI-compatible `/chat/completions` API endpoint.
* Auto generated, customizable web chat UI for every agent.
* Built-in user and context management to enable the creation of stateful, multi-turn agents.
# Build Agents
Source: https://docs.cycls.com/core-concepts/agent
Configure, secure, and deploy your AI agent.
The `@cycls.app()` decorator is your main interface for configuring dependencies, authentication, and deployment settings. Write a function, decorate it, and Cycls handles the rest.
Here is a full example of an agent configuration:
```python theme={null}
import cycls
@cycls.app(
pip=["openai", "pandas"], # Python packages to install
apt=["ffmpeg"], # System packages via apt
copy=["data.csv", ".env"], # Local files to bundle
copy_public=["logo.png"], # Public assets (images, etc.)
auth=True # Enable authentication
)
async def app(context):
# Your agent logic here
yield "Hello! I am ready to help."
# Run locally for development
app.local()
```
## Deployment Modes
### Local Development (`app.local()`)
Running `app.local()` builds a portable Docker image containing your agent and all dependencies. The image includes a pre-configured **FastAPI** server that serves the REST API and the web interface.
**Requirement:** You must have [Docker](https://www.docker.com/) installed and running.
```python theme={null}
app.local()
```
You will see the build logs and your local URL:
```bash theme={null}
Successfully built de0e95b9ae6d
Successfully tagged cycls/app:52afe9dc9be77162
----------------------------------------
✅ Base image built successfully
--- 🪵 Container Logs (streaming) ---
🔨 Visit app => http://localhost:8080
INFO: Started server process [1]
INFO: Application startup complete.
```
* **Result:** A locally running Docker container serving your agent.
* **Hot Reload:** By default, the server watches for file changes and restarts automatically.
* **Portability:** You can take this image and deploy it to any cloud provider or on-premise server.
* **URL:** `http://localhost:8080`
#### Development Options
```python theme={null}
# Standard development with hot-reload
app.local()
# Disable file watching
app.local(watch=False)
```
### Cloud Deployment (`app.deploy()`)
Running `app.deploy()` auto-builds your agent and deploys it to Cycls' serverless infrastructure in a single command.
```python theme={null}
import cycls
import os
# Set your API key
cycls.api_key = os.getenv("CYCLS_API_KEY")
@cycls.app(pip=["openai"])
async def app(context):
yield "Hello from the cloud!"
app.deploy()
```
* **Result:** A live, auto-scaling API and web interface.
* **Features:** Managed SSL, built-in auth, global CDN.
* **URL:** `https://.cycls.ai`
## Configuration Reference
### `@cycls.app()` Decorator
The decorator transforms your function into a deployable agent with all configuration in one place.
List of Python package names to install (e.g., `["openai", "pandas"]`).
System-level dependencies to install via apt (e.g., `["ffmpeg"]`).
List of local files or directories to bundle with your agent (e.g., `[".env", "data"]`).
List of local files to copy to the public assets folder (e.g., `["logo.png"]`). Accessible via `/public` endpoint.
If `True`, enables built-in user authentication (Sign in with Google/Email). Default is `False`.
Set to `"cycls_pass"` to enable Cycls Pass monetization with automatic feature gating.
### `app.local()`
* `True` (default): Enables hot-reload, automatically restarting on code changes.
* `False`: Runs without file watching.
### `app.deploy()`
Deploys your agent to the Cycls cloud. Requires `cycls.api_key` to be set.
```python theme={null}
import cycls
cycls.api_key = "your-api-key"
app.deploy()
```
## Next Steps
Learn how to use context to access conversation history.
# Agent API
Source: https://docs.cycls.com/core-concepts/agent-api
Learn how to interact with your deployed agent via the streaming APIs
## Overview
Every agent automatically exposes two streaming API endpoints:
* **OpenAI-compatible endpoint** (`/chat/completions`) - Works with any OpenAI SDK
* **Cycls Protocol endpoint** (`/chat/cycls`) - Native protocol with rich UI components
## API Endpoints
Your agent exposes these endpoints:
**Local Development (`app.local()`)**:
```
POST http://localhost:8080/chat/completions # OpenAI-compatible
POST http://localhost:8080/chat/cycls # Cycls Protocol
```
**Cloud Deployment (`app.deploy()`)**:
```
POST https://.cycls.ai/chat/completions # OpenAI-compatible
POST https://.cycls.ai/chat/cycls # Cycls Protocol
```
## OpenAI-Compatible API
The `/chat/completions` endpoint follows the standard OpenAI chat completion format, making it compatible with any OpenAI SDK or client.
### Request Format
```json theme={null}
{
"model": "app",
"messages": [
{"role": "user", "content": "Hello, how are you?"}
],
"stream": true
}
```
### Using cURL
```bash theme={null}
curl -X POST http://localhost:8080/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "app",
"messages": [
{"role": "user", "content": "Hello!"}
],
"stream": true
}'
```
### Using the OpenAI Python SDK
```python theme={null}
from openai import OpenAI
client = OpenAI(
api_key="not-needed", # Use your api_token if auth=True
base_url="http://localhost:8080"
)
response = client.chat.completions.create(
model="app",
messages=[
{"role": "user", "content": "Write a poem about AI"}
],
stream=True
)
for chunk in response:
if chunk.choices[0].delta.content:
print(chunk.choices[0].delta.content, end="")
```
### Using the OpenAI JavaScript SDK
```javascript theme={null}
import OpenAI from 'openai';
const openai = new OpenAI({
apiKey: 'not-needed',
baseURL: 'http://localhost:8080'
});
const stream = await openai.chat.completions.create({
model: 'app',
messages: [
{ role: 'user', content: 'Hello from JavaScript!' }
],
stream: true,
});
for await (const chunk of stream) {
process.stdout.write(chunk.choices[0]?.delta?.content || '');
}
```
## Cycls Protocol
The `/chat/cycls` endpoint uses Server-Sent Events (SSE) to stream rich UI components including thinking bubbles, code blocks, tables, and more.
### Request Format
```json theme={null}
{
"messages": [
{"role": "user", "content": "Hello!"}
]
}
```
### Response Format
The response streams SSE events with JSON payloads:
```
data: {"type": "text", "text": "Hello! "}
data: {"type": "text", "text": "How can I help?"}
data: {"type": "thinking", "thinking": "Processing..."}
data: {"type": "code", "code": "print('hello')", "language": "python"}
data: [DONE]
```
### Message Structure
Assistant responses contain a `parts` array:
```json theme={null}
{
"role": "assistant",
"parts": [
{"type": "text", "text": "Here's the answer:"},
{"type": "thinking", "thinking": "Let me explain..."},
{"type": "code", "code": "x = 1", "language": "python"}
]
}
```
### Supported Component Types
| Type | Fields | Description |
| ---------- | --------------------------- | ------------------ |
| `text` | `text` | Markdown text |
| `thinking` | `thinking` | Reasoning bubble |
| `code` | `code`, `language` | Code block |
| `table` | `headers`, `rows` | Data table |
| `callout` | `callout`, `style`, `title` | Alert box |
| `image` | `src`, `alt`, `caption` | Image |
| `status` | `status` | Progress indicator |
## Authentication
### Public Access (`auth=False`)
If your agent is public, API endpoints are open:
```python theme={null}
@cycls.app(auth=False)
async def app(context):
yield "Hello!"
```
### Protected Access (`auth=True`)
If your agent requires auth, include a Bearer token:
```python theme={null}
@cycls.app(auth=True, api_token="sk-your-token")
async def app(context):
yield f"Hello, {context.user.name}!"
```
```bash theme={null}
curl -X POST http://localhost:8080/chat/completions \
-H "Content-Type: application/json" \
-H "Authorization: Bearer sk-your-token" \
-d '{
"model": "app",
"messages": [{"role": "user", "content": "Hello!"}],
"stream": true
}'
```
## Next Steps
Learn how to secure your agent with built-in user authentication.
# Customize UI
Source: https://docs.cycls.com/core-concepts/agent-ui
Learn how to customize your agent's web interface and use native UI components
## Overview
Every agent comes with a beautiful web UI out of the box. The interface supports both static customization (header/intro) and **native UI components** that stream rich content progressively to users.
## Default Web UI
When you deploy your agent, it automatically gets a web interface accessible at your agent's URL. The default UI includes:
* **Chat Interface**: A modern, responsive chat interface
* **Message History**: Automatic conversation history management
* **Real-time Streaming**: Support for streaming responses
* **Native UI Components**: Rich components like thinking bubbles, code blocks, tables, and more
* **Mobile Responsive**: Works seamlessly on all devices
* **Auto RTL Support**: Automatically supports right-to-left languages
## Native UI Components
Cycls provides native UI components that render beautifully in the chat interface. Simply yield structured objects to display rich content that streams progressively.
### Text
Plain strings are rendered as markdown text with full formatting support.
```python theme={null}
@cycls.app()
async def app(context):
yield "Hello! Here's some **bold** and *italic* text."
yield "\n\n## A Heading\n\nAnd a paragraph below it."
```
### Thinking Bubbles
Show your agent's reasoning process with collapsible thinking bubbles.
```python theme={null}
@cycls.app()
async def app(context):
yield {"type": "thinking", "thinking": "Let me analyze this question..."}
yield {"type": "thinking", "thinking": "Considering multiple approaches..."}
yield "Based on my analysis, here's the answer:"
```
### Code Blocks
Display syntax-highlighted code with language detection.
```python theme={null}
@cycls.app()
async def app(context):
yield {
"type": "code",
"code": "def hello():\n print('Hello, World!')",
"language": "python"
}
```
### Tables
Stream tables row-by-row for progressive rendering.
```python theme={null}
@cycls.app()
async def app(context):
yield {
"type": "table",
"headers": ["Name", "Age", "City"],
"rows": [
["Alice", "30", "New York"],
["Bob", "25", "San Francisco"],
["Charlie", "35", "Chicago"]
]
}
```
### Callouts
Display styled callout boxes for important information.
```python theme={null}
@cycls.app()
async def app(context):
yield {
"type": "callout",
"callout": "This operation completed successfully!",
"style": "success",
"title": "Success"
}
yield {
"type": "callout",
"callout": "Please review before proceeding.",
"style": "warning",
"title": "Warning"
}
yield {
"type": "callout",
"callout": "Here's some helpful information.",
"style": "info",
"title": "Info"
}
yield {
"type": "callout",
"callout": "An error occurred during processing.",
"style": "error",
"title": "Error"
}
```
**Available styles:** `success`, `warning`, `info`, `error`
### Images
Display images with optional captions.
```python theme={null}
@cycls.app()
async def app(context):
yield {
"type": "image",
"src": "https://example.com/image.png",
"alt": "Description of image",
"caption": "Figure 1: An example image"
}
```
### Status Indicators
Show progress or status updates during long operations.
```python theme={null}
@cycls.app()
async def app(context):
yield {"type": "status", "status": "Processing your request..."}
# ... do work ...
yield {"type": "status", "status": "Almost done..."}
yield "Here are the results!"
```
## Component Reference
| Type | Key Fields | Description |
| ---------- | --------------------------- | ----------------------------- |
| `text` | `text` | Markdown-formatted text |
| `thinking` | `thinking` | Collapsible reasoning bubble |
| `code` | `code`, `language` | Syntax-highlighted code block |
| `table` | `headers`, `rows` | Streaming table |
| `callout` | `callout`, `style`, `title` | Styled alert box |
| `image` | `src`, `alt`, `caption` | Image with caption |
| `status` | `status` | Progress indicator |
## Header and Intro Customization
You can also customize the header and introduction sections using HTML and TailwindCSS:
```python theme={null}
import cycls
import urllib.parse
header = """
Welcome to My Agent
Your AI-powered assistant for exploring new ideas.
"""
@cycls.app(copy_public=["logo.png"], header=header, intro=intro)
async def app(context):
yield "Hello! How can I help you today?"
app.local()
```
### Styling Guidelines
* **HTML Support**: Full HTML5 support with inline styles
* **TailwindCSS**: Complete TailwindCSS framework available
* **Raw Tags**: Wrap custom HTML in `` tags to prevent escaping
* **Responsive Design**: Use TailwindCSS responsive classes for mobile compatibility
* **Clickable Links**: Use `urllib.parse.quote` to encode messages in links
## Clickable Links
Create clickable links that automatically send encoded text to the chat:
```python theme={null}
import urllib.parse
@cycls.app()
async def app(context):
link = f"https://cycls.com/send/{urllib.parse.quote('Tell me more')}"
yield f"Click here to [learn more]({link})"
```
### Link Format
```
[Link Text](https://cycls.com/send/Encoded%20message)
```
## Custom Frontend Applications
You can connect any custom frontend to your agent using the OpenAI-compatible API at `/chat/completions` or the Cycls Protocol at `/chat/cycls`.
## Next Steps
Learn about the built-in REST API and streaming protocols.
# User Authentication
Source: https://docs.cycls.com/core-concepts/auth
Secure your agent with built-in authentication and monetization.
Cycls provides built-in authentication to secure your agents. By setting `auth=True`, you can gate access to your agent and manage users effortlessly.
## Enabling Auth
To enable authentication, pass `auth=True` to the `@cycls.app()` decorator:
```python theme={null}
import cycls
@cycls.app(auth=True)
async def app(context):
# Only authenticated users can reach this code
user = context.user
yield f"Hello, {user.email}!"
app.local()
```
## Accessing User Data
When `auth=True`, the `context.user` object is populated with the authenticated user's information:
```python theme={null}
import cycls
@cycls.app(auth=True)
async def app(context):
user = context.user
yield f"Hello, {user.name}!\n"
yield f"Email: {user.email}\n"
yield f"Organization: {user.org}\n"
yield f"Plan: {user.plan}"
app.local()
```
### User Properties
| Property | Type | Description |
| :------- | :------- | :---------------------------------- |
| `id` | `string` | Unique identifier for the user |
| `email` | `string` | User's email address |
| `name` | `string` | User's full name |
| `org` | `string` | Organization ID the user belongs to |
| `plan` | `string` | User's subscription plan |
## Monetization with Cycls Pass
Enable subscription-based access with `plan="cycls_pass"`:
```python theme={null}
import cycls
@cycls.app(auth=True, plan="cycls_pass")
async def app(context):
user = context.user
if user.plan == "cycls_pass":
yield "Welcome, premium user! Here's your exclusive content."
else:
yield "Upgrade to Cycls Pass for premium features!"
app.local()
```
When `plan="cycls_pass"` is set:
* Users are prompted to subscribe via Cycls Pass
* Subscription status is available via `context.user.plan`
* You can gate features based on the user's plan
## User Management
When auth is enabled, Cycls handles the entire login flow:
* Sign up / Sign in UI
* Email verification
* Session management
* JWT token handling
User management via the [Cycls Dashboard](https://cycls.com/dashboard) is coming soon.
## Next Steps
Deploy your agent to a global serverless network.
# Manage Context
Source: https://docs.cycls.com/core-concepts/context
Learn how to work with context data
## The Context Object
The `context` parameter provides access to conversation history and user data in your agent functions.
```python theme={null}
@cycls.app()
async def app(context):
# Access conversation history
messages = context.messages
# Access the latest message
latest_msg = messages[-1]
yield f"You said: {latest_msg['content']}"
```
### Context Properties
The conversation history in OpenAI format. Contains all previous messages in the conversation.
User information object when `auth=True`. (See [Authentication](/core-concepts/auth) for details).
### Message Format
The `context.messages` follows the standard OpenAI message format, which can include text and images (multimodal).
Here is an example of what `context.messages` looks like:
```python theme={null}
[
{'role': 'user', 'content': 'hello'},
{'role': 'assistant', 'content': 'Hello! How can I assist you today?'},
{'role': 'user', 'content': [
{'type': 'image_url', 'image_url': {'url': 'https://.../image.png'}}
]},
{'role': 'assistant', 'content': "It looks like you're looking at an image..."},
]
```
### Working with Messages
You can easily process these messages to extract text or pass them directly to an LLM.
```python theme={null}
import cycls
@cycls.app(pip=["openai"])
async def app(context):
from openai import AsyncOpenAI
client = AsyncOpenAI()
response = await client.chat.completions.create(
model="gpt-4o-mini",
messages=context.messages,
stream=True
)
async for chunk in response:
content = chunk.choices[0].delta.content
if content:
yield content
app.local()
```
## Next Steps
Tailor the chat interface to match your brand.
# Cloud Deployment
Source: https://docs.cycls.com/core-concepts/deployment
Deploy your agent to a global serverless network.
Deploying your agent to production takes just one command. Cycls handles the infrastructure, scaling, and security for you.
## Getting Started
Before deploying your agent, you'll need a Cycls API key.
1. Go to the [Cycls Console](https://cycls.com/auth/sign-in) and sign in or create an account.
2. Navigate to the API Keys section.
3. Create a new API key and copy it securely.
## Deploying to Production
To deploy your agent, set your API key and call `app.deploy()`:
```python theme={null}
import cycls
import os
cycls.api_key = os.getenv("CYCLS_API_KEY")
@cycls.app(pip=["openai"], copy=[".env"])
async def app(context):
from openai import AsyncOpenAI
client = AsyncOpenAI()
response = await client.chat.completions.create(
model="gpt-4o-mini",
messages=context.messages,
stream=True
)
async for chunk in response:
content = chunk.choices[0].delta.content
if content:
yield content
app.deploy()
```
## Environment Variables & Secrets
Create a `.env` file in your project root and include it with `copy=[".env"]`:
```env theme={null}
CYCLS_API_KEY=cy-...
OPENAI_API_KEY=sk-...
```
Cycls automatically loads the `.env` file—environment variables are available via `os.getenv()` inside your function without needing any additional packages.
```python theme={null}
import cycls
import os
cycls.api_key = os.getenv("CYCLS_API_KEY")
@cycls.app(pip=["openai"], copy=[".env"])
async def app(context):
from openai import AsyncOpenAI
# Environment variables from .env are automatically loaded
client = AsyncOpenAI(api_key=os.getenv("OPENAI_API_KEY"))
response = await client.chat.completions.create(
model="gpt-4o-mini",
messages=context.messages,
stream=True
)
async for chunk in response:
content = chunk.choices[0].delta.content
if content:
yield content
app.deploy()
```
## What Happens During Deployment?
When you run `app.deploy()`, Cycls performs the following steps:
1. **Build**: Creates a Docker image containing your code, dependencies (`pip`), and system packages (`apt`).
2. **Push**: Uploads the image to the private Cycls Container Registry.
3. **Provision**: Sets up the serverless infrastructure to host your agent.
4. **Deploy**: Launches your agent and assigns it a permanent URL (e.g., `https://app.cycls.ai`).
## Deployment Output
```bash theme={null}
🚀 Deploying...
✅ Deployment successful!
🔗 Service is available at: https://app.cycls.ai
```
## Updating Your Agent
To update your agent, simply make changes to your code and run the script again. Cycls will build a new version and seamlessly update the deployment with zero downtime.
## Local vs Cloud
| Feature | `app.local()` | `app.deploy()` |
| ---------- | ----------------------- | ------------------------- |
| URL | `http://localhost:8080` | `https://.cycls.ai` |
| Hot Reload | Yes (default) | No |
| SSL | No | Yes (automatic) |
| Scaling | Single container | Auto-scaling |
| Auth | Optional | Optional |
## Monitoring & Management
The [Cycls Dashboard](https://cycls.com/dashboard) lets you monitor your agent's performance, view real-time logs, and manage your deployments.
# Getting Help
Source: https://docs.cycls.com/get-started/getting-help
Connect with builders, get technical support, and learn how to ship your AI agents.
## Need help?
Head over to our [Discord community](https://discord.gg/cycls) for help and insights from the team.
## Building with Cycls?
Share what you’re building on [X](https://x.com/cycls_), [LinkedIn](https://www.linkedin.com/company/cycls/) or join our [Discord](https://discord.gg/cycls) to connect with other builders.
## Looking for dedicated support?
We’ve helped many companies turn ideas into AI products. [Book a call](https://cal.com/cycls/30min) to get started.
# What is cycls?
Source: https://docs.cycls.com/get-started/overview
Cycls is an open-source SDK and Cloud platform for AI distribution.
Cycls provides the complete infrastructure to build, run, and ship your agents:
* **Open-source SDK** that manages dependencies, context, and UI directly in Python, auto-compiling everything into a portable Docker container. Zero Dockerfiles, YAML, or config required.
* **Agent Runtime** that wraps your code in a pre-built FastAPI app. Includes a secure REST API, web interface, and OpenAI-compatible endpoint out of the box.
* **Serverless Cloud** to build and deploy your agents instantly. Just call `app.deploy()` and we handle the remote build, serverless hosting, and auto-scaling, giving you a live public link (e.g., `agent.cycls.ai`) with built-in authentication and monetization.
## Example
Here is an example of an Agent that defines its own environment, connects to OpenAI, and serves a secure chat interface—all in one file.
```python theme={null}
import cycls
# Define your app with dependencies declared in the decorator
@cycls.app(pip=["openai"])
async def app(context):
from openai import AsyncOpenAI
client = AsyncOpenAI()
response = await client.chat.completions.create(
model="gpt-4o-mini",
messages=context.messages,
stream=True
)
async for chunk in response:
content = chunk.choices[0].delta.content
if content:
yield content
# Run locally for development
app.local()
```
Here is how the agent looks in action:
## The AI Distribution Layer
Building agents is easy; distributing them is hard. Cycls handles the four pillars of distribution so you can focus on your code:
1. **Instant Interfaces:** Cycls auto-generates responsive web UIs with native UI components (thinking bubbles, code blocks, tables, callouts, images) that stream progressively.
2. **Zero-Config Auth:** Enterprise-grade user management is enabled by default. Secure login, session management, and user gating are built-in.
3. **Serverless Hosting:** From `localhost` to a global URL in one command. We handle the scaling, the SSL, and the server management.
4. **Native Monetization:** Monetize your agent with Cycls Pass subscription integration.
**Cycls is Framework Agnostic:** It works seamlessly with [LangChain](https://www.langchain.com/), [CrewAI](https://www.crewai.com/), [Agno](https://agno.com/), [OpenAI](https://openai.com/), [Anthropic](https://www.anthropic.com/), [Groq](https://groq.com/), and any other Python library.
## Start Building
Ready to build your first agent? Check out our [Quickstart](/get-started/quickstart) guide to create your first AI agent in under 5 minutes.
# Quickstart
Source: https://docs.cycls.com/get-started/quickstart
Build and run your first Agent in minutes
In this quickstart, you'll create a simple Agent, run it locally, and then build and deploy a powerful AI agent to the cloud.
**Prerequisites:**
* Python 3.9 or higher (3.10+ recommended for deployment)
* Docker (Required for local runtime) - [Install Docker](https://www.docker.com/)
## Step 1: Install Cycls
Install Cycls SDK:
```bash theme={null}
pip install cycls
```
## Step 2: Create a Simple Agent
Create a file named `app.py` with the following code. This creates a simple agent that replies with a static message.
```python app.py theme={null}
import cycls
@cycls.app()
async def app(context):
yield "Hello, World!"
app.local()
```
## Step 3: Run your Agent
Make sure Docker Desktop is running before starting your agent.
Run the file from your terminal:
```bash theme={null}
python app.py
```
This will start a local development server with hot-reload. Open `http://localhost:8080` to see your agent in action.
**Success!** You've created and run your first Agent locally.
***
## Next: Cloud Deployment (OpenAI Agent)
Now let's build a simple AI agent powered by OpenAI LLMs and deploy it to the cloud.
1. Get your [Cycls API Key](https://cycls.com/auth/sign-in).
2. Get your [OpenAI API Key](https://openai.com/api/).
3. Create a `.env` file in your project root:
```bash .env theme={null}
CYCLS_API_KEY=your-cycls-key
OPENAI_API_KEY=sk-your-openai-key
```
You can use any model of your choice, such as OpenAI, Anthropic, or Gemini, etc.
Update `app.py` with this complete code:
```python app.py theme={null}
import cycls
import os
# Set your Cycls API key for deployment
cycls.api_key = os.getenv("CYCLS_API_KEY")
@cycls.app(pip=["openai"], copy=[".env"], auth=True)
async def app(context):
from openai import AsyncOpenAI
client = AsyncOpenAI(api_key=os.getenv("OPENAI_API_KEY"))
response = await client.chat.completions.create(
model="gpt-4o-mini",
messages=context.messages,
stream=True
)
async for chunk in response:
content = chunk.choices[0].delta.content
if content:
yield content
# Deploy to cloud
app.deploy()
```
**Pro Tip:** Notice how we import `openai` inside the function? This ensures the package is loaded only when needed inside the container. Cycls automatically loads your `.env` file—no extra packages needed.
Run the script to deploy:
```bash theme={null}
python app.py
```
You will see the deployment logs and your public URL:
```bash theme={null}
🚀 Deploying...
✅ Deployment successful!
🔗 Service is available at: https://app.cycls.ai
```
## Next Steps
Explore configuration options like dependencies, secrets, and custom UI.
Learn how to use context to store user data and conversation history.
Tailor the chat interface to match your brand.
# From Basic to Agentic RAG
Source: https://docs.cycls.com/guide/agentic-rag
A comprehensive workshop guide on building RAG systems with Cycls, evolving from simple retrieval to agentic workflows with file attachments and visualizations.
This guide serves as the foundation for a workshop on Retrieval-Augmented Generation (RAG). You will learn how to build a RAG system using `cycls`, `chromadb`, and `openai`, starting with a basic implementation and evolving it into an "Agentic" RAG that handles file uploads and dynamic visualization tools.
You will learn how to:
* Build a **Basic RAG** system with static documents.
* Create an **Attachment Handler** to process user uploads (PDFs, etc.).
* Implement an **Agentic RAG** that indexes content dynamically.
* Add **Tooling** capabilities to generate interactive charts.
## Prerequisites
* Python 3.8+
* `cycls` package installed
* OpenAI API Key
* Docker installed (for local testing)
```bash theme={null}
pip install cycls
```
## Part 1: The Basic RAG
We start by creating a simple RAG system. This agent will have a pre-defined "knowledge base" hardcoded into it. It demonstrates the core loop of RAG: **Embed -> Store -> Retrieve -> Generate**.
Create a file named `basicrag.py`.
### Step 1.1: Setup and Dependencies
We initialize the agent with `chromadb` (vector database) and `openai` (embeddings and generation).
```python theme={null}
import cycls
# Initialize the agent with dependencies
agent = cycls.Agent(
pip=["chromadb", "openai", "python-dotenv"],
copy=[".env"]
)
```
### Step 1.2: The Core Logic
The handler function performs the RAG operations. Note that in this basic version, documents are hardcoded.
```python theme={null}
@agent("chroma-agent", title="RAG Agent")
async def search_agent(context):
import os
from dotenv import load_dotenv
import chromadb
from chromadb.utils import embedding_functions
load_dotenv()
# 1. Setup OpenAI Embedding Function
openai_ef = embedding_functions.OpenAIEmbeddingFunction(
api_key=os.getenv("OPENAI_API_KEY"),
model_name="text-embedding-3-small"
)
# 2. Initialize ChromaDB (Ephemeral in-memory for this demo)
client = chromadb.Client()
collection = client.get_or_create_collection(name="docs", embedding_function=openai_ef)
# 3. Ingest Static Knowledge
# In a real app, this would come from a database or file loader
collection.add(
documents=["I love cats", "I love dogs", "The weather is nice"],
ids=["1", "2", "3"]
)
# 4. Retrieve Context
query = context.messages[-1]["content"]
results = collection.query(query_texts=[query], n_results=1)
retrieved_doc = results['documents'][0][0]
# 5. Generate Response (Simple yield for demo)
yield f"Context: {retrieved_doc}"
# Run locally
agent.deploy(prod=False)
```
**Why this is "Basic":**
* The knowledge base is static and ephemeral (re-created on every run).
* It cannot handle user files or new data.
* It strictly retrieves text; it doesn't *do* anything with it other than display it.
***
## Part 2: Evolving to Agentic RAG
Now we move to `agenticrag.py`. An "Agentic" RAG doesn't just look up info; it interacts with the environment. It will:
1. **Read files** you upload (PDFs, etc.).
2. **Index them** on the fly.
3. **Decide** whether to answer with text or generate a visualization (Tool Use).
### Helper Module: `attach.py`
To keep our agent clean, we move complex file handling to `attach.py`. This module handles:
* Downloading files from Cycls URLs.
* Extracting text from PDFs (`PyPDF2`).
* Formatting messages for the LLM.
*Ensure `attach.py` is in the same directory.*
### Step 2.1: Agent Configuration
We need more dependencies now, including `PyPDF2` for parsing and `httpx` for downloading.
```python theme={null}
agent = cycls.Agent(
pip=["chromadb", "openai", "python-dotenv", "httpx", "PyPDF2"],
copy=[".env", "attach.py"] # detailed copy instruction
)
```
### Step 2.2: Dynamic Indexing
Instead of hardcoded strings, we process the incoming message to find file content.
```python theme={null}
# Inside agenticrag.py handler...
# Process Attachments using our helper
processed = await attach.process_messages_for_openai([context.messages[-1]])
last_msg_content = processed[0]['content']
full_text = last_msg_content if isinstance(last_msg_content, str) else last_msg_content[0]['text']
# Regex to find file content blocks formatted by attach.py
new_docs = re.findall(r'--- Content of .*? ---\n(.*?)\n--- End of file ---', full_text, re.DOTALL)
# Index new files immediately
if new_docs:
try:
collection.add(documents=new_docs, ids=[f"doc_{hash(d)}" for d in new_docs])
except: pass
```
### Step 2.3: Tool Use (Chart Generation)
This is what makes it "Agentic". The model evaluates the user query. If the user asks for a "chart" or "plot", it switches logic paths to generate HTML instead of just text.
```python theme={null}
# Define the tool
async def generate_chart(description: str, data: str) -> str:
# ... (Prompt engineering to generate ApexCharts HTML) ...
# See agenticrag.py for full implementation
# Decision Logic
if any(w in query.lower() for w in ["chart", "plot", "graph", "visualize"]):
yield "Generating visualization..."
chart_html = await generate_chart(query, retrieved_context)
yield f''
else:
# Standard RAG Text Response
# ...
```
## Key Differences Summary
| Feature | Basic RAG (`basicrag.py`) | Agentic RAG (`agenticrag.py`) |
| :----------------- | :----------------------------------- | :------------------------------------------------ |
| **Knowledge Base** | Hardcoded, static strings. | Dynamic, built from user uploads. |
| **Input Handling** | Text only. | Text + Files (PDF, etc.) via `attach.py`. |
| **Reasoning** | Linear: Query -> Retrieve -> Answer. | Branching: Check intent -> (Visualize OR Answer). |
| **Output** | Plain Text. | Text or Interactive HTML Widgets. |
## Full Code Reference
### 1. Basic RAG (`basicrag.py`)
```python theme={null}
import cycls
# Initialize the agent with dependencies
agent = cycls.Agent(
pip=["chromadb", "openai", "python-dotenv"],
copy=[".env"]
)
@agent("chroma-agent", title="RAG Agent")
async def search_agent(context):
import os
from dotenv import load_dotenv
import chromadb
from chromadb.utils import embedding_functions
load_dotenv()
# Setup OpenAI Embedding Function
openai_ef = embedding_functions.OpenAIEmbeddingFunction(
api_key=os.getenv("OPENAI_API_KEY"),
model_name="text-embedding-3-small"
)
# Initialize ChromaDB client
client = chromadb.Client()
# Create collection with the embedding function
collection = client.get_or_create_collection(
name="docs",
embedding_function=openai_ef
)
# Add documents to the collection
collection.add(
documents=["I love cats", "I love dogs", "The weather is nice"],
ids=["1", "2", "3"]
)
# Query using the latest message
query = context.messages[-1]["content"]
results = collection.query(query_texts=[query], n_results=1)
# Return retrieved context
retrieved_doc = results['documents'][0][0]
yield f"Context: {retrieved_doc}"
# Run locally
agent.deploy(prod=False)
```
### 2. Agentic RAG (`agenticrag.py`)
````python theme={null}
import cycls
# Initialize agent with dependencies for RAG + Attachments + Charts
agent = cycls.Agent(
pip=["chromadb", "openai", "python-dotenv", "httpx", "PyPDF2"],
copy=[".env", "attach.py"]
)
@agent("agentic-rag", title="Agentic RAG")
async def chat(context):
import os
import re
from dotenv import load_dotenv
import chromadb
from chromadb.utils import embedding_functions
from openai import OpenAI, AsyncOpenAI
import attach
load_dotenv()
# 1. Setup ChromaDB
print("\n[DEBUG] 1. Setting up ChromaDB client and embedding function...")
openai_ef = embedding_functions.OpenAIEmbeddingFunction(
api_key=os.getenv("OPENAI_API_KEY"),
model_name="text-embedding-3-small"
)
client = chromadb.Client()
collection = client.get_or_create_collection(name="docs", embedding_function=openai_ef)
print("[DEBUG] ChromaDB collection 'docs' ready.")
# 2. Process Attachments
print("[DEBUG] 2. Processing message for attachments...")
processed = await attach.process_messages_for_openai([context.messages[-1]], debug=True)
last_msg_content = processed[0]['content']
full_text = last_msg_content if isinstance(last_msg_content, str) else last_msg_content[0]['text']
# Index new files
new_docs = re.findall(r'--- Content of .*? ---\n(.*?)\n--- End of file ---', full_text, re.DOTALL)
if new_docs:
print(f"[DEBUG] Found {len(new_docs)} new document sections to index.")
try:
collection.add(documents=new_docs, ids=[f"doc_{hash(d)}" for d in new_docs])
print(f"[DEBUG] Successfully indexed {len(new_docs)} documents in ChromaDB.")
except Exception as e:
print(f"[DEBUG] Error indexing documents: {e}")
else:
print("[DEBUG] No new documents found to index.")
# 3. Identify User Query
original_content = context.messages[-1].get('content', '')
query = ""
if isinstance(original_content, str):
query = original_content
elif isinstance(original_content, list):
query = " ".join([p['text'] for p in original_content if p.get('type') == 'text'])
if not query.strip():
query = "Summarize the uploaded document."
print(f"[DEBUG] 3. User Query identified: '{query}'")
# 4. RAG Retrieval
print(f"[DEBUG] 4. Querying ChromaDB for top 1 result...")
results = collection.query(query_texts=[query], n_results=1)
retrieved_context = results['documents'][0][0] if results['documents'][0] else "No context found."
print(f"[DEBUG] Retrieved Context length: {len(retrieved_context)} chars")
print(f"[DEBUG] Context Preview: {retrieved_context[:100]}...")
# --- Chart Generation Tool ---
async def generate_chart(description: str, data: str) -> str:
"""Generates an HTML chart using ApexCharts via LLM."""
vis_prompt = """
You are a data visualization expert. Generate a single HTML file containing an interactive ApexCharts visualization.
REQUIREMENTS:
- Use the 'ApexCharts' library from CDN:
- The container div MUST have id="chart".
- Initialize the chart in a