diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..076e148 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,71 @@ +# Dependencies +node_modules +.pnp +.pnp.js + +# Python +__pycache__ +*.py[cod] +*$py.class +.Python +env/ +venv/ +.venv +pip-log.txt +pip-delete-this-directory.txt +.pytest_cache + +# Testing +coverage +*.lcov +.nyc_output + +# Build outputs +.next/ +out/ +dist/ +build/ +*.tsbuildinfo + +# Environment +.env +.env*.local +.env.production + +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# OS +.DS_Store +*.pem +Thumbs.db + +# IDE +.vscode +.idea +*.swp +*.swo +*~ + +# Git +.git +.gitignore +.gitattributes + +# Documentation +*.md +!README.md + +# Turbo +.turbo + +# Misc +.cache +tmp +temp diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..9847a1d --- /dev/null +++ b/.env.example @@ -0,0 +1 @@ +OPENAI_API_KEY= \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..54f0bd1 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,174 @@ +name: CI + +on: + push: + branches: main + pull_request: + branches: main + schedule: + - cron: "0 0 * * *" # Run daily at midnight UTC + +jobs: + smoke: + name: Smoke / ${{ matrix.os }} / Node ${{ matrix.node }} / Python ${{ matrix.python }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest] + node: [22, 24] + python: [3.12, 3.13] + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node }} + + - name: Setup Python + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python }} + + - name: Install pnpm + uses: pnpm/action-setup@v4 + + - name: Install uv + uses: astral-sh/setup-uv@v4 + with: + enable-cache: true + + - name: Configure uv to use matrix Python version + run: echo "UV_PYTHON=python${{ matrix.python }}" >> $GITHUB_ENV + + - name: Install dependencies (monorepo) + run: pnpm install + + - name: Build all apps + run: pnpm build + + - name: Create empty .env file + run: touch .env + + - name: Test frontend startup (Linux/macOS) + if: runner.os != 'Windows' + run: | + # Start the Next.js frontend in background + pnpm --filter app start & + FRONTEND_PID=$! + + # Wait for frontend to start (max 30 seconds) + timeout=30 + elapsed=0 + started=false + + while [ $elapsed -lt $timeout ] && [ "$started" = false ]; do + if curl -s http://localhost:3000 > /dev/null 2>&1; then + started=true + echo "✅ Frontend started successfully" + else + sleep 1 + elapsed=$((elapsed + 1)) + fi + done + + # Clean up background process + kill $FRONTEND_PID 2>/dev/null || true + + if [ "$started" = false ]; then + echo "❌ Frontend failed to start within 30 seconds" + exit 1 + fi + shell: bash + + - name: Test frontend startup (Windows) + if: runner.os == 'Windows' + run: | + # Start the Next.js frontend in background + pnpm --filter app start & + + # Wait for frontend to start (max 30 seconds) + $timeout = 30 + $elapsed = 0 + $started = $false + + while ($elapsed -lt $timeout -and -not $started) { + try { + $response = Invoke-WebRequest -Uri "http://localhost:3000" -TimeoutSec 1 -ErrorAction SilentlyContinue + if ($response.StatusCode -eq 200) { + $started = $true + Write-Host "✅ Frontend started successfully" + } + } catch { + Start-Sleep -Seconds 1 + $elapsed++ + } + } + + if (-not $started) { + Write-Host "❌ Frontend failed to start within 30 seconds" + exit 1 + } + shell: pwsh + + lint: + name: Lint + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + + - name: Setup Python + uses: actions/setup-python@v4 + with: + python-version: 3.12 + + - name: Install pnpm + uses: pnpm/action-setup@v4 + + - name: Install uv + uses: astral-sh/setup-uv@v4 + with: + enable-cache: true + + - name: Install dependencies + run: pnpm install + + - name: Run linting + run: pnpm lint + + notify-slack: + name: Notify Slack on Failure + runs-on: ubuntu-latest + needs: [smoke, lint] + if: | + failure() && + github.event_name == 'schedule' + steps: + - name: Notify Slack + uses: slackapi/slack-github-action@v2.1.0 + with: + webhook: ${{ secrets.SLACK_WEBHOOK_URL }} + webhook-type: incoming-webhook + payload: | + { + "text": ":warning: *Smoke test failed for `with-langgraph-python` :warning:.*", + "blocks": [ + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": ":warning: *Smoke test failed for :warning:*\n\n<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|View run details>" + } + } + ] + } diff --git a/.gitignore b/.gitignore index 0c8456b..448656a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,8 @@ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. # dependencies -/node_modules -/.pnp +node_modules +.pnp .pnp.* .yarn/* !.yarn/patches @@ -11,14 +11,14 @@ !.yarn/versions # testing -/coverage +coverage # next.js -/.next/ -/out/ +.next/ +out/ # production -/build +build # misc .DS_Store @@ -32,6 +32,7 @@ yarn-error.log* # env files (can opt-in for committing if needed) .env* +!.env.example # vercel .vercel @@ -40,15 +41,19 @@ yarn-error.log* *.tsbuildinfo next-env.d.ts -.mastra/ - # lock files package-lock.json yarn.lock -pnpm-lock.yaml bun.lockb -# python -agent/venv/ -__pycache__/ -.venv/ +# LangGraph API +.langgraph_api + +# Git worktrees +.worktrees + +# Turbo +.turbo + +# Tools +.claude diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..285e7b9 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,441 @@ +# CopilotKit + AWS Strands Starter + +## Purpose + +This repository serves as both a **showcase** and **template** for building AI agents with CopilotKit and AWS Strands. It demonstrates how CopilotKit can drive interactive UI beyond just chat, using an **agent-driven application** as the primary example. + +**Target audience:** Developers evaluating CopilotKit or starting new projects with AI agents using AWS Strands. + +## Core Concept + +This starter demonstrates **agent-driven UI** where: +- The agent can manipulate application state through backend tools +- Users can interact with the same state through the frontend UI +- Both agent and user changes update the same shared state +- The UI reactively updates based on agent state changes + +This uses CopilotKit's **v2 agent state pattern** integrated with AWS Strands, where state lives in the agent and syncs bidirectionally to the frontend. + +## Architecture + +This is a **monorepo** with multiple apps: + +### Repository Structure + +``` +apps/ +├── app/ # Next.js frontend +│ ├── src/ +│ │ ├── app/ +│ │ │ ├── page.tsx # Main page - wires up all components +│ │ │ └── api/copilotkit/ # CopilotKit API route +│ │ ├── components/ +│ │ │ ├── canvas/ # Application UI +│ │ │ │ └── index.tsx # Canvas container +│ │ │ ├── example-layout/ # Layout: chat + canvas side-by-side +│ │ │ └── generative-ui/ # Example generative UI components +│ │ └── hooks/ +│ │ ├── use-generative-ui-examples.tsx # Example CopilotKit patterns +│ │ └── use-example-suggestions.tsx # Chat suggestions +└── agent/ # AWS Strands Python agent + ├── main.py # Agent entry point + └── pyproject.toml # Python dependencies +``` + +## Key Pattern: Agent State with CopilotKit v2 + AWS Strands + +This starter uses **CopilotKit v2's agent state pattern** integrated with **AWS Strands** where state lives in the agent backend and syncs bidirectionally with the frontend. + +### How It Works + +1. **Agent defines tools with Strands** (Python) + ```python + # apps/agent/main.py + from strands import Agent, tool + from strands.models.openai import OpenAIModel + from ag_ui_strands import StrandsAgent, StrandsAgentConfig, create_strands_app + + @tool + def update_proverbs(proverbs_list: ProverbsList): + """Update the complete list of proverbs. + + Args: + proverbs_list: The complete updated proverbs list + + Returns: + Success message + """ + return "Proverbs updated successfully" + + # Create Strands agent + strands_agent = Agent( + model=OpenAIModel(model_id="gpt-4o"), + system_prompt="You are a helpful assistant...", + tools=[update_proverbs, get_weather], + ) + + # Wrap with AG-UI integration for CopilotKit + agui_agent = StrandsAgent( + agent=strands_agent, + name="proverbs_agent", + description="A proverbs assistant", + config=shared_state_config, + ) + ``` + +2. **Frontend reads from agent state** + ```typescript + // apps/app/src/components/canvas/index.tsx + const { agent } = useAgent(); + + return ( + agent.setState({ proverbs: updatedData })} + isAgentRunning={agent.isRunning} + /> + ); + ``` + +3. **User interactions update agent state** + ```typescript + // User makes changes → frontend calls agent.setState() + const updateData = (newData) => { + agent.setState({ proverbs: newData }); + }; + ``` + +4. **Agent can manipulate state via tools** + - The agent calls tools (e.g., `update_proverbs`) to modify application state + - Both user and agent changes update the same `agent.state` + - Frontend automatically re-renders when state changes + - State flows through AG-UI integration layer + +### Why This Pattern? + +- **Single source of truth**: State lives in the agent, not duplicated in frontend +- **Bidirectional sync**: User changes → agent state, Agent changes → UI update +- **Simple**: No need for separate frontend state management +- **Observable**: Agent has full visibility into state changes + +## Implementation Details + +### Agent Backend + +**Agent Definition** (`apps/agent/main.py`): +```python +from strands import Agent, tool +from strands.models.openai import OpenAIModel +from ag_ui_strands import ( + StrandsAgent, + StrandsAgentConfig, + ToolBehavior, + create_strands_app, +) + +# Initialize OpenAI model +model = OpenAIModel( + client_args={"api_key": os.getenv("OPENAI_API_KEY")}, + model_id="gpt-4o", +) + +# Create Strands agent with tools +strands_agent = Agent( + model=model, + system_prompt="You are a helpful assistant...", + tools=[update_proverbs, get_weather], +) + +# Wrap with AG-UI integration for CopilotKit +agui_agent = StrandsAgent( + agent=strands_agent, + name="proverbs_agent", + description="A proverbs assistant", + config=shared_state_config, +) + +# Create FastAPI app +app = create_strands_app(agui_agent, agent_path="/") +``` + +**Tool Definition Example**: +```python +from pydantic import BaseModel, Field + +class ProverbsList(BaseModel): + """A list of proverbs.""" + proverbs: List[str] = Field(description="The complete list of proverbs") + +@tool +def update_proverbs(proverbs_list: ProverbsList): + """Update the complete list of proverbs. + + Args: + proverbs_list: The complete updated proverbs list + + Returns: + Success message + """ + return "Proverbs updated successfully" + +@tool +def get_weather(location: str): + """Get the weather for a location. + + Args: + location: The location to get weather for + + Returns: + Weather information + """ + return json.dumps({"location": location, "temp": "70 degrees"}) +``` + +**State Management Configuration**: +```python +async def proverbs_state_from_args(context): + """Extract proverbs state from tool arguments.""" + tool_input = context.tool_input + if isinstance(tool_input, str): + tool_input = json.loads(tool_input) + + proverbs_data = tool_input.get("proverbs_list", tool_input) + proverbs_array = proverbs_data.get("proverbs", []) + + return {"proverbs": proverbs_array} + +# Configure state management +shared_state_config = StrandsAgentConfig( + state_context_builder=build_proverbs_prompt, + tool_behaviors={ + "update_proverbs": ToolBehavior( + skip_messages_snapshot=True, + state_from_args=proverbs_state_from_args, + ) + }, +) +``` + +### Frontend + +**Main Page** (`apps/app/src/app/page.tsx`): +```typescript +"use client"; + +import { ExampleLayout } from "@/components/example-layout"; +import { Canvas } from "@/components/canvas"; +import { useGenerativeUIExamples, useExampleSuggestions } from "@/hooks"; +import { CopilotChat } from "@copilotkit/react-core/v2"; + +export default function HomePage() { + // Generative UI Examples + useGenerativeUIExamples(); + + // Example Suggestions + useExampleSuggestions(); + + return ( + } + appContent={} + /> + ); +} +``` + +**Canvas Component** (`apps/app/src/components/canvas/index.tsx`): +```typescript +export function Canvas() { + const { agent } = useAgent(); // CopilotKit v2 hook + + return ( +
+ {/* Your application UI goes here */} + {/* Read state from agent.state */} + {/* Update state via agent.setState() */} + agent.setState({ yourData: newData })} + isAgentRunning={agent.isRunning} + /> +
+ ); +} +``` + +### How State Flows + +1. **User interacts with UI** → Frontend calls `agent.setState({ yourData: [...] })` +2. **Agent state updates** → CopilotKit syncs to backend via AG-UI protocol +3. **Strands agent observes change** → Can respond via tools based on state context +4. **Agent modifies state** → Calls tools which emit state updates via `state_from_args` +5. **State syncs to frontend** → `agent.state` updates through AG-UI integration +6. **UI re-renders** → React sees new state and updates display + +**Key insight**: State lives in the Strands agent backend, frontend just reads/writes to it via CopilotKit hooks. The AG-UI integration layer handles the bidirectional sync. + +## Tech Stack + +- **Frontend**: Next.js, React, TailwindCSS +- **Agent**: AWS Strands (Python), OpenAI GPT-4o +- **Integration**: ag-ui-strands for CopilotKit v2 integration +- **CopilotKit**: React hooks for agent state management (v2) +- **Package Manager**: pnpm (recommended), npm, yarn, or bun +- **Server**: FastAPI + Uvicorn +- **Python**: Python 3.12+ with uv for dependency management + +## Development + +This is a monorepo with support for multiple package managers (pnpm, npm, yarn, bun). + +### Installation + +```bash +# Using pnpm (recommended) +pnpm install + +# Using npm +npm install + +# Using yarn +yarn install + +# Using bun +bun install +``` + +> **Note:** Installing dependencies will automatically install the agent's Python dependencies via the `install:agent` script. + +### Available Scripts + +```bash +# Start both UI and agent in development mode +pnpm dev # or npm run dev, yarn dev, bun run dev + +# Start with debug logging +pnpm dev:debug + +# Start individually +pnpm dev:ui # Next.js frontend only +pnpm dev:agent # Strands agent only (port 8000) + +# Build for production +pnpm build + +# Start production server +pnpm start + +# Lint +pnpm lint + +# Install Python dependencies manually +pnpm install:agent +``` + +### Environment Setup + +```bash +# Set OpenAI API key for the Strands agent +export OPENAI_API_KEY="your-openai-api-key-here" + +# Or create a .env file in the agent directory +echo "OPENAI_API_KEY=your-openai-api-key-here" > apps/agent/.env +``` + +### Configuration + +The agent server can be configured via environment variables: +- `OPENAI_API_KEY` - Your OpenAI API key (required) +- `AGENT_PORT` - Agent server port (default: 8000) +- `AGENT_PATH` - Agent endpoint path (default: /) + +## Design Principles + +1. **Simple over complex** - The starter is intentionally focused and minimal +2. **CopilotKit v2 patterns** - Uses modern agent state management +3. **AWS Strands integration** - Leverages Strands for agent orchestration +4. **Template-first** - Code is meant to be forked and extended +5. **Showcasing agent-driven UI** - Demonstrates AI manipulating application state beyond chat + +## Key Features + +- **Shared State Management**: Bidirectional sync between agent and UI +- **Backend Tools**: Execute tools in the Strands agent backend +- **Generative UI**: Examples of rendering dynamic UI from agent responses +- **OpenAI Integration**: Uses OpenAI models via Strands +- **FastAPI Server**: Production-ready agent server with hot reloading +- **TypeScript Frontend**: Type-safe React components with CopilotKit + +--- + +## Key Takeaways for Developers + +**State Management Pattern**: This starter uses CopilotKit v2's agent state pattern with AWS Strands: + +### Backend (Strands Agent) +1. **Define tools with Pydantic models**: + ```python + @tool + def your_tool(data: YourModel): + """Tool description""" + return "Success" + ``` + +2. **Configure state extraction**: + ```python + async def state_from_args(context): + """Extract state from tool arguments""" + return {"yourData": extracted_data} + + config = StrandsAgentConfig( + tool_behaviors={ + "your_tool": ToolBehavior(state_from_args=state_from_args) + } + ) + ``` + +3. **Wrap agent with AG-UI integration**: + ```python + agui_agent = StrandsAgent( + agent=strands_agent, + name="your_agent", + config=config, + ) + ``` + +### Frontend (React/Next.js) +1. **Read state**: `agent.state?.yourData` +2. **Write state**: `agent.setState({ yourData: newData })` +3. **React to changes**: `agent.isRunning` + +### Why This Pattern? + +- **Single source of truth**: State lives in the Strands agent backend +- **Bidirectional sync**: User changes → agent state, Agent changes → UI update +- **No manual state management**: AG-UI handles synchronization +- **Type-safe**: Pydantic models ensure data validation +- **Observable**: Agent has full visibility into state changes + +This pattern works great for **agent-driven applications** where AI needs to manipulate structured application state beyond just chat responses. + +## Extending This Template + +1. **Add new tools** in `apps/agent/main.py` using the `@tool` decorator +2. **Configure state extraction** via `state_from_args` callbacks +3. **Update frontend** to read/write new state fields via `agent.state` and `agent.setState()` +4. **Customize UI** in `apps/app/src/components/` to match your application needs + +## Troubleshooting + +### Agent Connection Issues +If you see connection errors: +1. Ensure the Strands agent is running on port 8000 +2. Verify your OpenAI API key is set correctly +3. Check that both frontend and agent servers started successfully + +### Python Dependencies +If you encounter import errors: +```bash +cd apps/agent +uv sync +``` diff --git a/agent/main.py b/agent/main.py deleted file mode 100644 index e7acc08..0000000 --- a/agent/main.py +++ /dev/null @@ -1,162 +0,0 @@ -"""Strands AG-UI Integration Example - Proverbs Agent. - -This example demonstrates a Strands agent integrated with AG-UI, featuring: -- Shared state management between agent and UI -- Backend tool execution (get_weather, update_proverbs) -- Frontend tools (set_theme_color) -- Generative UI rendering -""" - -import json -import os -from typing import List - -from ag_ui_strands import ( - StrandsAgent, - StrandsAgentConfig, - ToolBehavior, - create_strands_app, -) -from dotenv import load_dotenv -from pydantic import BaseModel, Field -from strands import Agent, tool -from strands.models.openai import OpenAIModel - -load_dotenv() - - -class ProverbsList(BaseModel): - """A list of proverbs.""" - - proverbs: List[str] = Field(description="The complete list of proverbs") - - -@tool -def get_weather(location: str): - """Get the weather for a location. - - Args: - location: The location to get weather for - - Returns: - Weather information as JSON string - """ - return json.dumps({"location": "70 degrees"}) - - -@tool -def set_theme_color(theme_color: str): - """Change the theme color of the UI. - - This is a frontend tool - it returns None as the actual - execution happens on the frontend via useFrontendTool. - - Args: - theme_color: The color to set as theme - """ - return None - - -@tool -def update_proverbs(proverbs_list: ProverbsList): - """Update the complete list of proverbs. - - IMPORTANT: Always provide the entire list, not just new proverbs. - - Args: - proverbs_list: The complete updated proverbs list - - Returns: - Success message - """ - return "Proverbs updated successfully" - - -def build_proverbs_prompt(input_data, user_message: str) -> str: - """Inject the current proverbs state into the prompt.""" - state_dict = getattr(input_data, "state", None) - if isinstance(state_dict, dict) and "proverbs" in state_dict: - proverbs_json = json.dumps(state_dict["proverbs"], indent=2) - return ( - f"Current proverbs list:\n{proverbs_json}\n\nUser request: {user_message}" - ) - return user_message - - -async def proverbs_state_from_args(context): - """Extract proverbs state from tool arguments. - - This function is called when update_proverbs tool is executed - to emit a state snapshot to the UI. - - Args: - context: ToolResultContext containing tool execution details - - Returns: - dict: State snapshot with proverbs array, or None on error - """ - try: - tool_input = context.tool_input - if isinstance(tool_input, str): - tool_input = json.loads(tool_input) - - proverbs_data = tool_input.get("proverbs_list", tool_input) - - # Extract proverbs array - if isinstance(proverbs_data, dict): - proverbs_array = proverbs_data.get("proverbs", []) - else: - proverbs_array = [] - - return {"proverbs": proverbs_array} - except Exception: - return None - - -# Configure agent behavior -shared_state_config = StrandsAgentConfig( - state_context_builder=build_proverbs_prompt, - tool_behaviors={ - "update_proverbs": ToolBehavior( - skip_messages_snapshot=True, - state_from_args=proverbs_state_from_args, - ) - }, -) - -# Initialize OpenAI model -api_key = os.getenv("OPENAI_API_KEY", "") -model = OpenAIModel( - client_args={"api_key": api_key}, - model_id="gpt-4o", -) - -system_prompt = ( - "You are a helpful and wise assistant that helps manage a collection of proverbs." -) - -# Create Strands agent with tools -# Note: Frontend tools (set_theme_color, hitl_test) return None - actual execution happens in the UI -strands_agent = Agent( - model=model, - system_prompt=system_prompt, - tools=[update_proverbs, get_weather, set_theme_color], -) - -# Wrap with AG-UI integration -agui_agent = StrandsAgent( - agent=strands_agent, - name="proverbs_agent", - description="A proverbs assistant that collaborates with you to manage proverbs", - config=shared_state_config, -) - -# Create the FastAPI app -agent_path = os.getenv("AGENT_PATH", "/") -app = create_strands_app(agui_agent, agent_path) - -if __name__ == "__main__": - import uvicorn - - port = int(os.getenv("AGENT_PORT", 8000)) - uvicorn.run("main:app", host="0.0.0.0", port=port, reload=True) diff --git a/apps/agent/.gitignore b/apps/agent/.gitignore new file mode 100644 index 0000000..5a23824 --- /dev/null +++ b/apps/agent/.gitignore @@ -0,0 +1,3 @@ + +# LangGraph API +.langgraph_api diff --git a/apps/agent/__pycache__/main.cpython-312.pyc b/apps/agent/__pycache__/main.cpython-312.pyc new file mode 100644 index 0000000..d6e5dbe Binary files /dev/null and b/apps/agent/__pycache__/main.cpython-312.pyc differ diff --git a/apps/agent/main.py b/apps/agent/main.py new file mode 100644 index 0000000..18d9e6d --- /dev/null +++ b/apps/agent/main.py @@ -0,0 +1,69 @@ +"""Strands AG-UI Integration Example - Proverbs Agent. + +This example demonstrates a Strands agent integrated with AG-UI, featuring: +- Shared state management between agent and UI +- Backend tool execution (get_weather, update_proverbs) +- Frontend tools (set_theme_color) +- Generative UI rendering +""" + +import json +import os + +from ag_ui_strands import ( + StrandsAgent, + create_strands_app, +) +from dotenv import load_dotenv +from strands import Agent, tool +from strands.models.openai import OpenAIModel + +load_dotenv() + +@tool +def get_weather(location: str): + """Get the weather for a location. + + Args: + location: The location to get weather for + + Returns: + Weather information as JSON string + """ + return json.dumps({"location": "70 degrees"}) + + +api_key = os.getenv("OPENAI_API_KEY", "") +model = OpenAIModel( + client_args={"api_key": api_key}, + model_id="gpt-5.2", +) + +strands_agent = Agent( + model=model, + tools=[get_weather], + system_prompt=""" + You are a helpful assistant that helps users understand CopilotKit and LangGraph used together. + + When asked about generative UI: + 1. Ground yourself in relevant information from the CopilotKit documentation. + 2. Use one of the relevant tools to demonstrate that piece of generative UI. + 3. Explain the concept to the user with a brief summary.", + """, +) + +# Wrap with AG-UI integration +agui_agent = StrandsAgent( + agent=strands_agent, + name="proverbs_agent", +) + +# Create the FastAPI app +agent_path = os.getenv("AGENT_PATH", "/") +app = create_strands_app(agui_agent, agent_path) + +if __name__ == "__main__": + import uvicorn + + port = int(os.getenv("AGENT_PORT", 8000)) + uvicorn.run("main:app", host="0.0.0.0", port=port, reload=True) diff --git a/apps/agent/package.json b/apps/agent/package.json new file mode 100644 index 0000000..8518c58 --- /dev/null +++ b/apps/agent/package.json @@ -0,0 +1,12 @@ +{ + "name": "@repo/agent", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "uv run main.py", + "postinstall": "uv sync" + }, + "devDependencies": { + "@langchain/langgraph-cli": "^1.1.12" + } +} diff --git a/agent/pyproject.toml b/apps/agent/pyproject.toml similarity index 100% rename from agent/pyproject.toml rename to apps/agent/pyproject.toml diff --git a/agent/uv.lock b/apps/agent/uv.lock similarity index 99% rename from agent/uv.lock rename to apps/agent/uv.lock index 894c12a..40789cc 100644 --- a/agent/uv.lock +++ b/apps/agent/uv.lock @@ -16,16 +16,16 @@ wheels = [ [[package]] name = "ag-ui-strands" -version = "0.1.0b12" +version = "0.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "ag-ui-protocol" }, { name = "fastapi" }, { name = "strands-agents" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3c/de/1a911255609d7efb33b65d23ce11541defeb98a87212a220d03548bcdae7/ag_ui_strands-0.1.0b12.tar.gz", hash = "sha256:2bda63fcc9f6cb15ade1aee0701c2a2a36373ebe8c59b473761a2e69d18e914f", size = 167391, upload-time = "2025-12-03T04:05:15.683Z" } +sdist = { url = "https://files.pythonhosted.org/packages/36/f1/dff0c1412372a006dd0e7dcfa24813122d82f9785a4c4cfdc1f8efd17e1f/ag_ui_strands-0.1.1.tar.gz", hash = "sha256:a61355dc8d4cd700c510063dbbe949d9c3c24217cf02f7f544e8c5bfc33ba428", size = 170778, upload-time = "2026-02-06T18:15:19.895Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/05/8f/4d1ad9f3b63b905adbcc141d2ed91e241b154db02105372c74df7bc5a32d/ag_ui_strands-0.1.0b12-py3-none-any.whl", hash = "sha256:195c90b6c3ff470b3997ca45ed1bbb3dd0d0a5673b12cac2052281dcbc35c090", size = 10641, upload-time = "2025-12-03T04:05:14.3Z" }, + { url = "https://files.pythonhosted.org/packages/d3/d7/36107cbce3e7242864791d6708fa68b57de1df08e9f07e7ece0e01f41f15/ag_ui_strands-0.1.1-py3-none-any.whl", hash = "sha256:c2b9922258c5dfa7e53a537bac1a49d188b495aa00edba10ac9168ffca7c25b8", size = 13015, upload-time = "2026-02-06T18:15:17.825Z" }, ] [[package]] @@ -169,7 +169,7 @@ dependencies = [ [package.metadata] requires-dist = [ { name = "ag-ui-protocol", specifier = ">=0.1.5" }, - { name = "ag-ui-strands", specifier = "==0.1.0b12" }, + { name = "ag-ui-strands", specifier = "~=0.1.0" }, { name = "fastapi", specifier = ">=0.115.12" }, { name = "strands-agents", extras = ["openai"], specifier = ">=1.15.0" }, { name = "strands-agents-tools", specifier = ">=0.2.14" }, diff --git a/eslint.config.mjs b/apps/app/eslint.config.mjs similarity index 96% rename from eslint.config.mjs rename to apps/app/eslint.config.mjs index 6d22b8e..9852352 100644 --- a/eslint.config.mjs +++ b/apps/app/eslint.config.mjs @@ -11,6 +11,7 @@ const eslintConfig = [ "out/**", "build/**", "next-env.d.ts", + "agent", ], }, ]; diff --git a/next.config.ts b/apps/app/next.config.ts similarity index 86% rename from next.config.ts rename to apps/app/next.config.ts index cdccab6..7795e5d 100644 --- a/next.config.ts +++ b/apps/app/next.config.ts @@ -1,6 +1,7 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { + output: "standalone", serverExternalPackages: ["@copilotkit/runtime"], }; diff --git a/apps/app/package.json b/apps/app/package.json new file mode 100644 index 0000000..fe18297 --- /dev/null +++ b/apps/app/package.json @@ -0,0 +1,36 @@ +{ + "name": "@repo/app", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev --turbopack", + "build": "next build", + "start": "next start", + "lint": "eslint ." + }, + "dependencies": { + "@ag-ui/client": "^0.0.43", + "@ag-ui/mcp-apps-middleware": "^0.0.3", + "@copilotkit/react-core": "1.51.4-next.1", + "@copilotkit/react-ui": "1.51.4-next.1", + "@copilotkit/runtime": "1.51.4-next.1", + "next": "16.1.6", + "react": "^19.2.4", + "react-dom": "^19.2.4", + "react-rnd": "^10.5.2", + "react18-json-view": "^0.2.9", + "recharts": "^3.7.0" + }, + "devDependencies": { + "@eslint/eslintrc": "^3", + "@tailwindcss/postcss": "^4", + "@types/node": "^20", + "@types/react": "^19", + "@types/react-dom": "^19", + "eslint": "^9", + "eslint-config-next": "16.1.6", + "tailwindcss": "^4", + "typescript": "^5", + "zod": "^3.23.8" + } +} diff --git a/postcss.config.mjs b/apps/app/postcss.config.mjs similarity index 100% rename from postcss.config.mjs rename to apps/app/postcss.config.mjs diff --git a/public/file.svg b/apps/app/public/file.svg similarity index 100% rename from public/file.svg rename to apps/app/public/file.svg diff --git a/public/globe.svg b/apps/app/public/globe.svg similarity index 100% rename from public/globe.svg rename to apps/app/public/globe.svg diff --git a/public/next.svg b/apps/app/public/next.svg similarity index 100% rename from public/next.svg rename to apps/app/public/next.svg diff --git a/public/vercel.svg b/apps/app/public/vercel.svg similarity index 100% rename from public/vercel.svg rename to apps/app/public/vercel.svg diff --git a/public/window.svg b/apps/app/public/window.svg similarity index 100% rename from public/window.svg rename to apps/app/public/window.svg diff --git a/apps/app/src/app/api/copilotkit/ag-ui-middleware.ts b/apps/app/src/app/api/copilotkit/ag-ui-middleware.ts new file mode 100644 index 0000000..a69f1ae --- /dev/null +++ b/apps/app/src/app/api/copilotkit/ag-ui-middleware.ts @@ -0,0 +1,13 @@ +import { MCPAppsMiddleware } from "@ag-ui/mcp-apps-middleware"; + +export const aguiMiddleware = [ + new MCPAppsMiddleware({ + mcpServers: [ + { + type: "http", + url: process.env.MCP_SERVER_URL || "http://localhost:3108/mcp", + serverId: "example_mcp_app", + }, + ], + }), +]; \ No newline at end of file diff --git a/apps/app/src/app/api/copilotkit/route.ts b/apps/app/src/app/api/copilotkit/route.ts new file mode 100644 index 0000000..2c1c337 --- /dev/null +++ b/apps/app/src/app/api/copilotkit/route.ts @@ -0,0 +1,32 @@ +import { + CopilotRuntime, + ExperimentalEmptyAdapter, + LangGraphAgent, + copilotRuntimeNextJSAppRouterEndpoint, +} from "@copilotkit/runtime"; +import { HttpAgent } from "@ag-ui/client"; +import { NextRequest } from "next/server"; +import { aguiMiddleware } from "@/app/api/copilotkit/ag-ui-middleware"; + +// 1. Define the agent connection to LangGraph +const defaultAgent = new HttpAgent({ + url: process.env.AGENT_DEPLOYMENT_URL || "http://localhost:8000", +}); + +// 2. Bind in middleware to the agent. For A2UI and MCP Apps. +defaultAgent.use(...aguiMiddleware) + +// 3. Define the route and CopilotRuntime for the agent +export const POST = async (req: NextRequest) => { + const { handleRequest } = copilotRuntimeNextJSAppRouterEndpoint({ + endpoint: "/api/copilotkit", + serviceAdapter: new ExperimentalEmptyAdapter(), + runtime: new CopilotRuntime({ + agents: { + default: defaultAgent, + }, + }), + }); + + return handleRequest(req); +}; diff --git a/src/app/favicon.ico b/apps/app/src/app/favicon.ico similarity index 100% rename from src/app/favicon.ico rename to apps/app/src/app/favicon.ico diff --git a/src/app/globals.css b/apps/app/src/app/globals.css similarity index 100% rename from src/app/globals.css rename to apps/app/src/app/globals.css diff --git a/apps/app/src/app/layout.tsx b/apps/app/src/app/layout.tsx new file mode 100644 index 0000000..6917a03 --- /dev/null +++ b/apps/app/src/app/layout.tsx @@ -0,0 +1,22 @@ +"use client"; + +import "./globals.css"; + +import { CopilotKit } from "@copilotkit/react-core"; +import "@copilotkit/react-ui/v2/styles.css"; + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + + + + {children} + + + + ); +} diff --git a/apps/app/src/app/page.tsx b/apps/app/src/app/page.tsx new file mode 100644 index 0000000..03a9221 --- /dev/null +++ b/apps/app/src/app/page.tsx @@ -0,0 +1,24 @@ +"use client"; + +import { ExampleLayout } from "@/components/example-layout"; +import { Canvas } from "@/components/canvas"; +import { useGenerativeUIExamples, useExampleSuggestions } from "@/hooks"; + +import { CopilotChat } from "@copilotkit/react-core/v2"; +// import { HeadlessChat } from "@/components/headless-chat"; + +export default function HomePage() { + // 🪁 Generative UI Examples + useGenerativeUIExamples(); + + // 🪁 Example Suggestions + useExampleSuggestions(); + + return ( + } + // chatContent={} + appContent={} + /> + ); +} \ No newline at end of file diff --git a/apps/app/src/components/canvas/index.tsx b/apps/app/src/components/canvas/index.tsx new file mode 100644 index 0000000..9ab34fe --- /dev/null +++ b/apps/app/src/components/canvas/index.tsx @@ -0,0 +1,21 @@ +"use client"; + +import { useAgent } from "@copilotkit/react-core/v2"; +import { TodoList } from "./todo-list"; + +export function Canvas() { + const { agent } = useAgent(); + + return ( +
+ agent.setState({ todos: updatedTodos })} + // change UI based on agent execution + isAgentRunning={agent.isRunning} + /> +
+ ); +} diff --git a/apps/app/src/components/canvas/todo-card.tsx b/apps/app/src/components/canvas/todo-card.tsx new file mode 100644 index 0000000..3088147 --- /dev/null +++ b/apps/app/src/components/canvas/todo-card.tsx @@ -0,0 +1,153 @@ +"use client"; + +import { useState } from "react"; + +interface Todo { + id: string; + title: string; + description: string; + emoji: string; + status: "pending" | "completed"; +} + +interface TodoCardProps { + todo: Todo; + onToggleStatus: (todo: Todo) => void; + onDelete: (todo: Todo) => void; + onUpdateTitle: (todoId: string, title: string) => void; + onUpdateDescription: (todoId: string, description: string) => void; +} + +export function TodoCard({ + todo, + onToggleStatus, + onDelete, + onUpdateTitle, + onUpdateDescription, +}: TodoCardProps) { + const [editingField, setEditingField] = useState<"title" | "description" | null>(null); + const [editValue, setEditValue] = useState(""); + + const isEditingTitle = editingField === "title"; + const isEditingDescription = editingField === "description"; + const isCompleted = todo.status === "completed"; + const truncatedDescription = todo.description.length > 100 + ? todo.description.slice(0, 100) + "..." + : todo.description; + + const startEdit = (field: "title" | "description") => { + setEditingField(field); + setEditValue(field === "title" ? todo.title : todo.description); + }; + + const saveEdit = (field: "title" | "description") => { + if (editValue.trim()) { + if (field === "title") { + onUpdateTitle(todo.id, editValue.trim()); + } else { + onUpdateDescription(todo.id, editValue.trim()); + } + } + setEditingField(null); + setEditValue(""); + }; + + const cancelEdit = () => { + setEditingField(null); + setEditValue(""); + }; + + return ( +
+
+ {/* Checkbox toggle */} + + +
+
+ {/* Emoji */} + {todo.emoji} + + {/* Title (editable) */} +
+ {isEditingTitle ? ( + setEditValue(e.target.value)} + onBlur={() => saveEdit("title")} + onKeyDown={(e) => { + if (e.key === "Enter") saveEdit("title"); + if (e.key === "Escape") cancelEdit(); + }} + className="w-full px-2 py-1 text-base font-semibold border-b-2 border-gray-400 focus:outline-none" + autoFocus + aria-label="Edit todo title" + /> + ) : ( +
startEdit("title")} + className={`text-base font-semibold cursor-text break-words ${ + isCompleted ? "line-through text-gray-400" : "text-gray-900" + }`} + > + {todo.title} +
+ )} +
+
+ + {/* Description (editable, truncated) */} +
+ {isEditingDescription ? ( +