chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
node_modules
|
||||
.next
|
||||
__pycache__
|
||||
*.pyc
|
||||
.git
|
||||
.env
|
||||
.env.local
|
||||
agent/.venv
|
||||
@@ -0,0 +1,49 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.*
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/versions
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# env files (can opt-in for committing if needed)
|
||||
.env*
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
|
||||
.mastra/
|
||||
|
||||
|
||||
# python
|
||||
agent/venv/
|
||||
__pycache__/
|
||||
.venv/
|
||||
@@ -0,0 +1,63 @@
|
||||
# Stage 1: Build Next.js frontend
|
||||
FROM node:20-slim AS frontend
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package.json ./
|
||||
RUN npm install --ignore-scripts
|
||||
|
||||
# Fix CVE: Railway scans package.json, so we update it too
|
||||
RUN npm install next@latest && \
|
||||
node -e "const p=require('./package.json'); p.dependencies.next='^16'; require('fs').writeFileSync('package.json',JSON.stringify(p,null,2))"
|
||||
|
||||
COPY src/ ./src/
|
||||
COPY public/ ./public/
|
||||
COPY next.config.ts tsconfig.json postcss.config.mjs ./
|
||||
|
||||
# Patch next.config.ts to ignore TS errors during build (can't modify original source)
|
||||
RUN node -e "const fs=require('fs'); const f='next.config.ts'; let c=fs.readFileSync(f,'utf8'); if(!c.includes('ignoreBuildErrors')){c=c.replace('};',' typescript: { ignoreBuildErrors: true },\n};'); fs.writeFileSync(f,c);}"
|
||||
|
||||
RUN npm run build
|
||||
|
||||
# Stage 2: Production image with Python + Node
|
||||
FROM python:3.12-slim AS runner
|
||||
|
||||
# Install Node.js 20 + uv
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends curl ca-certificates && \
|
||||
curl -fsSL https://deb.nodesource.com/setup_20.x | bash - && \
|
||||
apt-get install -y --no-install-recommends nodejs && \
|
||||
apt-get clean && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install uv by copying from the official image (avoids curl|sh pipe-swallow bug
|
||||
# where a 5xx on astral.sh silently produces an exit-0 layer with no uv binary).
|
||||
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /usr/local/bin/
|
||||
|
||||
ENV PATH="/root/.local/bin:$PATH"
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Install Python dependencies using uv
|
||||
COPY agent/pyproject.toml agent/uv.lock ./agent/
|
||||
COPY agent/main.py ./agent/
|
||||
COPY agent/src/ ./agent/src/
|
||||
RUN cd agent && uv pip install --system -e .
|
||||
|
||||
# Copy Next.js build output and production node_modules
|
||||
COPY --from=frontend /app/.next ./.next
|
||||
COPY --from=frontend /app/node_modules ./node_modules
|
||||
COPY --from=frontend /app/package.json ./
|
||||
COPY --from=frontend /app/public ./public
|
||||
|
||||
# Copy agent source code
|
||||
COPY agent/ ./agent/
|
||||
|
||||
# Copy entrypoint
|
||||
COPY entrypoint.sh ./
|
||||
RUN chmod +x entrypoint.sh
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
ENV NODE_ENV=production
|
||||
|
||||
CMD ["./entrypoint.sh"]
|
||||
@@ -0,0 +1,21 @@
|
||||
The MIT License
|
||||
|
||||
Copyright (c) Atai Barkai
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
@@ -0,0 +1,118 @@
|
||||
# CopilotKit <> Agno Starter
|
||||
|
||||
This is a starter template for building AI agents using [Agno](https://agno.com) and [CopilotKit](https://copilotkit.ai). It provides a modern Next.js application with an integrated investment analyst agent that can research stocks, analyze market data, and provide investment insights.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Node.js 20+
|
||||
- Python 3.12+
|
||||
- [uv](https://docs.astral.sh/uv/) (Python package manager)
|
||||
- OpenAI API Key (for the Agno agent)
|
||||
- Any of the following package managers:
|
||||
- npm (default)
|
||||
- [pnpm](https://pnpm.io/installation)
|
||||
- [yarn](https://classic.yarnpkg.com/lang/en/docs/install/)
|
||||
- [bun](https://bun.sh/)
|
||||
|
||||
## Getting Started
|
||||
|
||||
1. Install dependencies using your preferred package manager:
|
||||
|
||||
```bash
|
||||
# Using npm (default)
|
||||
npm install
|
||||
|
||||
# Using pnpm
|
||||
pnpm install
|
||||
|
||||
# Using yarn
|
||||
yarn install
|
||||
|
||||
# Using bun
|
||||
bun install
|
||||
```
|
||||
|
||||
> **Note:** Installing the package dependencies will also install the agent's python dependencies via the `install:agent` script.
|
||||
|
||||
2. Set up your OpenAI API key:
|
||||
|
||||
```bash
|
||||
export OPENAI_API_KEY="your-openai-api-key-here"
|
||||
```
|
||||
|
||||
or create a `.env` file.
|
||||
|
||||
```bash
|
||||
echo "OPENAI_API_KEY=your-openai-api-key-here" > .env
|
||||
```
|
||||
|
||||
3. Start the development server:
|
||||
|
||||
```bash
|
||||
# Using npm (default)
|
||||
npm run dev
|
||||
|
||||
# Using pnpm
|
||||
pnpm dev
|
||||
|
||||
# Using yarn
|
||||
yarn dev
|
||||
|
||||
# Using bun
|
||||
bun run dev
|
||||
```
|
||||
|
||||
This will start both the UI and agent servers concurrently.
|
||||
|
||||
## Available Scripts
|
||||
|
||||
The following scripts can also be run using your preferred package manager:
|
||||
|
||||
- `dev` - Starts both UI and agent servers in development mode
|
||||
- `dev:ui` - Starts only the Next.js UI server
|
||||
- `dev:agent` - Starts only the Agno agent server
|
||||
- `build` - Builds the Next.js application for production
|
||||
- `start` - Starts the production server
|
||||
- `install:agent` - Installs Python dependencies for the agent
|
||||
|
||||
## 📚 Documentation
|
||||
|
||||
The main UI component is in `src/app/page.tsx`. You can:
|
||||
|
||||
- Modify the theme colors and styling
|
||||
- Add new frontend actions
|
||||
- Customize the CopilotKit sidebar appearance
|
||||
|
||||
Otherwise, check out the documentation relevant to your task:
|
||||
|
||||
- [Agno Documentation](https://docs.agno.com/introduction) - Learn more about Agno and its features
|
||||
- [CopilotKit Documentation](https://docs.copilotkit.ai) - Explore CopilotKit's capabilities
|
||||
- [Next.js Documentation](https://nextjs.org/docs) - Learn about Next.js features and API
|
||||
- [YFinance Documentation](https://pypi.org/project/yfinance/) - Financial data tools
|
||||
|
||||
## Contributing
|
||||
|
||||
Feel free to submit issues and enhancement requests! This starter is designed to be easily extensible.
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the MIT License - see the LICENSE file for details.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Agent Connection Issues
|
||||
|
||||
If you see "I'm having trouble connecting to my tools", make sure:
|
||||
|
||||
1. The Agno agent is running on port 8000
|
||||
2. Your OpenAI API key is set correctly
|
||||
3. Both servers started successfully
|
||||
|
||||
### Python Dependencies
|
||||
|
||||
If you encounter Python import errors:
|
||||
|
||||
```bash
|
||||
cd agent
|
||||
uv sync
|
||||
```
|
||||
@@ -0,0 +1,27 @@
|
||||
"""
|
||||
Example for serving you Agno agent as an AG-UI server
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import dotenv
|
||||
from agno.os import AgentOS
|
||||
from agno.os.interfaces.agui import AGUI
|
||||
|
||||
from src.agent import agent
|
||||
|
||||
dotenv.load_dotenv(Path(__file__).resolve().parent.parent / ".env")
|
||||
dotenv.load_dotenv()
|
||||
|
||||
# Build AgentOS and extract the app for serving
|
||||
agent_os = AgentOS(agents=[agent], interfaces=[AGUI(agent=agent)])
|
||||
app = agent_os.get_app()
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
async def health():
|
||||
return {"status": "ok"}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
agent_os.serve(app="main:app", port=8000, reload=True)
|
||||
@@ -0,0 +1,15 @@
|
||||
[project]
|
||||
name = "investment-analyst-agent"
|
||||
version = "0.1.0"
|
||||
description = "An Agno Agent with Finance tools exposed in an AG-UI compatible way"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = [
|
||||
"agno>=1.7.8",
|
||||
"openai>=1.88.0",
|
||||
"yfinance>=0.2.63",
|
||||
"fastapi>=0.115.13",
|
||||
"uvicorn>=0.34.3",
|
||||
"ag-ui-protocol>=0.1.8",
|
||||
"packaging>=25.0.0",
|
||||
"python-dotenv>=1.0.0",
|
||||
]
|
||||
@@ -0,0 +1,25 @@
|
||||
"""Example: Agno Agent with Finance tools
|
||||
|
||||
This example shows how to create an Agno Agent with tools (YFinanceTools) and expose it in an AG-UI compatible way.
|
||||
"""
|
||||
|
||||
from agno.agent.agent import Agent
|
||||
from agno.models.openai import OpenAIChat
|
||||
from agno.tools.yfinance import YFinanceTools
|
||||
|
||||
from .tools.backend import get_weather
|
||||
from .tools.frontend import add_proverb, set_theme_color
|
||||
|
||||
agent = Agent(
|
||||
model=OpenAIChat(id="gpt-4o"),
|
||||
tools=[
|
||||
# Example of backend tools, defined and handled in your agno agent
|
||||
YFinanceTools(),
|
||||
get_weather,
|
||||
# Example of frontend tools, handled in the frontend Next.js app
|
||||
add_proverb,
|
||||
set_theme_color,
|
||||
],
|
||||
description="You are an demonstrative agent for Agno and CopilotKit's integration.",
|
||||
instructions="Format your response using markdown and use tables to display data where possible.",
|
||||
)
|
||||
@@ -0,0 +1,15 @@
|
||||
from agno.tools import tool
|
||||
|
||||
|
||||
@tool
|
||||
def get_weather(location: str):
|
||||
"""
|
||||
Get the weather for the current location.
|
||||
|
||||
Args:
|
||||
location (str): The location to get the weather for.
|
||||
|
||||
Returns:
|
||||
str: The weather for the current location.
|
||||
"""
|
||||
return f"The weather in {location} is: 70 degrees and Sunny."
|
||||
@@ -0,0 +1,21 @@
|
||||
from agno.tools import tool
|
||||
|
||||
|
||||
@tool(external_execution=True)
|
||||
def set_theme_color(theme_color: str):
|
||||
"""
|
||||
Change the theme color of the chat.
|
||||
|
||||
Args:
|
||||
background: str: The background color to change to.
|
||||
"""
|
||||
|
||||
|
||||
@tool(external_execution=True)
|
||||
def add_proverb(proverb: str):
|
||||
"""
|
||||
Add a proverb to the chat.
|
||||
|
||||
Args:
|
||||
proverb: str: The proverb to add to the chat.
|
||||
"""
|
||||
Generated
+1109
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,74 @@
|
||||
services:
|
||||
aimock:
|
||||
image: ghcr.io/copilotkit/aimock:latest
|
||||
volumes:
|
||||
- ./fixtures:/fixtures:ro
|
||||
command:
|
||||
["--fixtures", "/fixtures", "--host", "0.0.0.0", "--validate-on-load"]
|
||||
|
||||
agent:
|
||||
build:
|
||||
context: ./agent
|
||||
dockerfile: ../docker/Dockerfile.agent
|
||||
environment:
|
||||
- OPENAI_API_KEY=test-key-for-aimock
|
||||
- OPENAI_BASE_URL=http://aimock:4010/v1
|
||||
depends_on:
|
||||
aimock:
|
||||
condition: service_started
|
||||
healthcheck:
|
||||
test:
|
||||
[
|
||||
"CMD",
|
||||
"python",
|
||||
"-c",
|
||||
"import socket; socket.create_connection(('localhost', 8000), 2)",
|
||||
]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 30
|
||||
start_period: 30s
|
||||
|
||||
app:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: docker/Dockerfile.app
|
||||
environment:
|
||||
- AGENT_URL=http://agent:8000
|
||||
depends_on:
|
||||
agent:
|
||||
condition: service_healthy
|
||||
healthcheck:
|
||||
test:
|
||||
[
|
||||
"CMD",
|
||||
"node",
|
||||
"-e",
|
||||
"const http = require('http'); const req = http.get('http://localhost:3000/', (res) => { process.exit(res.statusCode === 200 ? 0 : 1); }); req.on('error', () => process.exit(1)); req.setTimeout(2000, () => { req.destroy(); process.exit(1); });",
|
||||
]
|
||||
interval: 5s
|
||||
timeout: 10s
|
||||
retries: 30
|
||||
start_period: 60s
|
||||
|
||||
tests:
|
||||
image: mcr.microsoft.com/playwright:v1.52.0-noble
|
||||
working_dir: /tests
|
||||
volumes:
|
||||
- ../../../showcase/tests:/tests
|
||||
- test-results:/tests/test-results
|
||||
environment:
|
||||
- STARTER=${STARTER:-agno}
|
||||
- STARTER_URL=http://app:3000
|
||||
depends_on:
|
||||
app:
|
||||
condition: service_healthy
|
||||
command:
|
||||
[
|
||||
"bash",
|
||||
"-c",
|
||||
"cd /tests && npm install --no-audit --no-fund 2>/dev/null && npx playwright install chromium && npx playwright test starter-smoke --reporter=list",
|
||||
]
|
||||
|
||||
volumes:
|
||||
test-results:
|
||||
@@ -0,0 +1,17 @@
|
||||
FROM python:3.12-slim
|
||||
|
||||
# Install uv by copying from the official image (avoids curl|sh pipe-swallow bug
|
||||
# where a 5xx on astral.sh silently produces an exit-0 layer with no uv binary).
|
||||
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /usr/local/bin/
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY pyproject.toml uv.lock ./
|
||||
COPY main.py ./
|
||||
COPY src/ ./src/
|
||||
|
||||
RUN uv sync
|
||||
|
||||
EXPOSE 8000
|
||||
|
||||
CMD ["uv", "run", "uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||
@@ -0,0 +1,27 @@
|
||||
FROM node:22-alpine AS base
|
||||
|
||||
FROM base AS deps
|
||||
RUN apk add --no-cache libc6-compat
|
||||
WORKDIR /app
|
||||
COPY package.json ./
|
||||
RUN npm install --ignore-scripts
|
||||
|
||||
FROM base AS builder
|
||||
WORKDIR /app
|
||||
COPY --from=deps /app/node_modules ./node_modules
|
||||
COPY . .
|
||||
RUN node -e "const fs=require('fs'); const f='next.config.ts'; let c=fs.readFileSync(f,'utf8'); if(!c.includes('ignoreBuildErrors')){c=c.replace('};',' typescript: { ignoreBuildErrors: true },\n};'); fs.writeFileSync(f,c);}"
|
||||
RUN node -e "const fs=require('fs'); const f='next.config.ts'; let c=fs.readFileSync(f,'utf8'); if(!c.includes('output')){c=c.replace('};',' output: \"standalone\",\n};'); fs.writeFileSync(f,c);}"
|
||||
RUN npm run build
|
||||
|
||||
FROM base AS runner
|
||||
WORKDIR /app
|
||||
ENV NODE_ENV=production
|
||||
RUN addgroup --system --gid 1001 nodejs && adduser --system --uid 1001 nextjs
|
||||
COPY --from=builder /app/public ./public
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
|
||||
USER nextjs
|
||||
EXPOSE 3000
|
||||
ENV PORT=3000 HOSTNAME="0.0.0.0"
|
||||
CMD ["node", "server.js"]
|
||||
Executable
+17
@@ -0,0 +1,17 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Start Python agent on port 8000 (override PORT which Railway sets to 3000)
|
||||
cd /app/agent
|
||||
PORT=8000 python -m uvicorn main:app --host 0.0.0.0 --port 8000 &
|
||||
AGENT_PID=$!
|
||||
cd /app
|
||||
|
||||
# Start Next.js frontend
|
||||
PORT=${PORT:-3000} npx next start --port ${PORT:-3000} &
|
||||
NEXT_PID=$!
|
||||
|
||||
wait -n $AGENT_PID $NEXT_PID
|
||||
EXIT_CODE=$?
|
||||
kill $AGENT_PID $NEXT_PID 2>/dev/null || true
|
||||
exit $EXIT_CODE
|
||||
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"fixtures": [
|
||||
{
|
||||
"match": { "userMessage": "Hello" },
|
||||
"response": {
|
||||
"content": "Hello! I'm the agno AI assistant. How can I help you?"
|
||||
}
|
||||
},
|
||||
{
|
||||
"match": { "userMessage": "weather in San Francisco" },
|
||||
"response": {
|
||||
"content": "The weather in San Francisco is: 70 degrees and Sunny."
|
||||
}
|
||||
},
|
||||
{
|
||||
"match": { "userMessage": "theme to green" },
|
||||
"response": {
|
||||
"content": "Done! I've changed the theme color to green (#22c55e). The page background and accent colors should now reflect the new theme."
|
||||
}
|
||||
},
|
||||
{
|
||||
"match": { "userMessage": "price of Apple stock" },
|
||||
"response": {
|
||||
"content": "Here is the latest price information for Apple Inc. (AAPL):\n\n| Symbol | Price | Change | Change % |\n|--------|---------|---------|----------|\n| AAPL | $213.49 | +1.24 | +0.58% |\n\n*Note: you are running against aimock — this is a scripted fixture value, not a live quote.*"
|
||||
}
|
||||
},
|
||||
{
|
||||
"match": { "userMessage": "proverb about AI" },
|
||||
"response": {
|
||||
"content": "Added a new proverb: *\"An AI that learns from its mistakes is wiser than a human who repeats them.\"*\n\nYou should see it appear in the Proverbs panel on the left."
|
||||
}
|
||||
},
|
||||
{
|
||||
"match": {},
|
||||
"response": {
|
||||
"content": "You're currently running against aimock (a mock LLM server). This response is a catch-all for requests that don't match any test fixture. To use a real LLM: (1) Add your OPENAI_API_KEY to .env, (2) Remove or unset OPENAI_BASE_URL from your environment so requests go to OpenAI instead of aimock, (3) Restart with `pnpm dev`."
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
typescript: { ignoreBuildErrors: true },
|
||||
serverExternalPackages: ["pino", "thread-stream"],
|
||||
env: {
|
||||
// The public Threads UI flag is DERIVED from the server-side license token.
|
||||
// Set COPILOTKIT_LICENSE_TOKEN (only) to enable Threads — do not set this flag
|
||||
// directly. NOTE: NEXT_PUBLIC_* resolves at BUILD time while the runtime reads
|
||||
// the token per-request, so the UI gate and runtime agree only when the token is
|
||||
// present at build time (the standard `next dev` / host-build flow). For a
|
||||
// standalone/Docker image built without the token and injected at runtime, set
|
||||
// COPILOTKIT_LICENSE_TOKEN at build time too (or gate the UI at runtime) so the
|
||||
// baked flag reflects it.
|
||||
NEXT_PUBLIC_COPILOTKIT_THREADS_ENABLED: process.env.COPILOTKIT_LICENSE_TOKEN
|
||||
? "true"
|
||||
: "false",
|
||||
},
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
+18565
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"name": "agno-starter",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "concurrently \"npm run dev:ui\" \"npm run dev:agent\" --names ui,agent --prefix-colors blue,green --kill-others",
|
||||
"dev:agent": "./scripts/run-agent.sh || scripts\\run-agent.bat",
|
||||
"dev:ui": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"install:agent": "./scripts/setup-agent.sh || scripts\\setup-agent.bat",
|
||||
"postinstall": "npm run install:agent"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ag-ui/client": "0.0.57",
|
||||
"@copilotkit/react-core": "1.62.3",
|
||||
"@copilotkit/runtime": "1.62.3",
|
||||
"@hono/node-server": "^1",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"hono": "^4",
|
||||
"lucide-react": "^0.525.0",
|
||||
"next": "16.0.7",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"tailwind-merge": "^3.5.0",
|
||||
"zod": "^3.24.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"concurrently": "^9.1.2",
|
||||
"tailwindcss": "^4",
|
||||
"typescript": "^5"
|
||||
},
|
||||
"overrides": {
|
||||
"@ag-ui/client": "0.0.57",
|
||||
"@ag-ui/core": "0.0.57",
|
||||
"@ag-ui/encoder": "0.0.57",
|
||||
"@ag-ui/proto": "0.0.57"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
const config = {
|
||||
plugins: ["@tailwindcss/postcss"],
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -0,0 +1 @@
|
||||
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>
|
||||
|
After Width: | Height: | Size: 391 B |
@@ -0,0 +1 @@
|
||||
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1 @@
|
||||
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>
|
||||
|
After Width: | Height: | Size: 128 B |
@@ -0,0 +1 @@
|
||||
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>
|
||||
|
After Width: | Height: | Size: 385 B |
@@ -0,0 +1,6 @@
|
||||
@echo off
|
||||
REM Navigate to the agent directory
|
||||
cd /d %~dp0\..\agent
|
||||
|
||||
REM Run the agent using uv
|
||||
uv run python main.py
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Navigate to the agent directory
|
||||
cd "$(dirname "$0")/../agent" || exit 1
|
||||
|
||||
# Run the agent using uv
|
||||
uv run python main.py
|
||||
@@ -0,0 +1,6 @@
|
||||
@echo off
|
||||
REM Navigate to the agent directory
|
||||
cd /d "%~dp0\..\agent" || exit /b 1
|
||||
|
||||
REM Install dependencies using uv
|
||||
uv sync
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Navigate to the agent directory
|
||||
cd "$(dirname "$0")/../agent" || exit 1
|
||||
|
||||
# Install dependencies using uv
|
||||
uv sync
|
||||
@@ -0,0 +1,44 @@
|
||||
import {
|
||||
CopilotRuntime,
|
||||
CopilotKitIntelligence,
|
||||
createCopilotEndpoint,
|
||||
InMemoryAgentRunner,
|
||||
} from "@copilotkit/runtime/v2";
|
||||
import { HttpAgent } from "@ag-ui/client";
|
||||
import { handle } from "hono/vercel";
|
||||
|
||||
const runtime = new CopilotRuntime({
|
||||
agents: {
|
||||
default: new HttpAgent({
|
||||
url:
|
||||
(process.env.AGENT_URL || "http://localhost:8000").replace(/\/$/, "") +
|
||||
"/agui",
|
||||
}),
|
||||
},
|
||||
// --- copilotkit:intelligence (remove this block to opt out) ---
|
||||
...(process.env.COPILOTKIT_LICENSE_TOKEN
|
||||
? {
|
||||
intelligence: new CopilotKitIntelligence({
|
||||
apiKey: process.env.INTELLIGENCE_API_KEY ?? "",
|
||||
apiUrl: process.env.INTELLIGENCE_API_URL ?? "http://localhost:4201",
|
||||
wsUrl:
|
||||
process.env.INTELLIGENCE_GATEWAY_WS_URL ?? "ws://localhost:4401",
|
||||
}),
|
||||
// Demo stub — replace with your real auth-derived user identity before any
|
||||
// multi-user deployment, or all users share one thread history.
|
||||
identifyUser: () => ({ id: "demo-user", name: "Demo User" }),
|
||||
licenseToken: process.env.COPILOTKIT_LICENSE_TOKEN,
|
||||
}
|
||||
: { runner: new InMemoryAgentRunner() }),
|
||||
// --- /copilotkit:intelligence ---
|
||||
});
|
||||
|
||||
const app = createCopilotEndpoint({
|
||||
runtime,
|
||||
basePath: "/api/copilotkit",
|
||||
});
|
||||
|
||||
export const GET = handle(app);
|
||||
export const POST = handle(app);
|
||||
export const PATCH = handle(app);
|
||||
export const DELETE = handle(app);
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
@@ -0,0 +1,76 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
/*
|
||||
* Threads panel theme — match to agno's CopilotKit chat (CopilotSidebar).
|
||||
*
|
||||
* The CopilotKit v2 chat renders LIGHT regardless of OS dark mode (neutral
|
||||
* zinc/gray surfaces on white, ~1rem bubble radius, system-ui sans). To read as
|
||||
* native to that chat, the SDK <CopilotThreadsDrawer> is held light by token
|
||||
* inheritance: we drop agno's old `prefers-color-scheme: dark` override of
|
||||
* --background/--foreground so the panel never goes dark while the chat beside
|
||||
* it stays light. agno's own demo content sets all of its colors locally
|
||||
* (text-white, bg-white/20, the indigo themeColor backdrop), so it is
|
||||
* unaffected by this.
|
||||
*
|
||||
* Tokens are defined at :root (not a wrapper) so the drawer's delete-confirm
|
||||
* dialog — which renders through a React portal to <body>, outside the panel —
|
||||
* still resolves the same surfaces. The SDK drawer is driven entirely by these
|
||||
* tokens; the indigo accent (#6366f1) is agno's own app accent, tying the panel
|
||||
* to the surrounding product; the neutral zinc surfaces/borders/muted text
|
||||
* mirror the chat's light chrome.
|
||||
*/
|
||||
:root {
|
||||
/* Neutral surfaces — match the chat's white/zinc light palette */
|
||||
--background: #ffffff;
|
||||
--foreground: #18181b; /* zinc-900 */
|
||||
--card: #ffffff;
|
||||
--card-foreground: #18181b;
|
||||
|
||||
/* Agno's indigo accent for the primary new-thread button + active thread */
|
||||
--primary: #6366f1;
|
||||
--primary-foreground: #ffffff;
|
||||
|
||||
/* Tints / hovers — zinc scale, matching the chat's neutral chrome */
|
||||
--secondary: #f4f4f5; /* zinc-100 */
|
||||
--secondary-foreground: #18181b;
|
||||
--muted: #f4f4f5;
|
||||
--muted-foreground: #71717a; /* zinc-500 */
|
||||
--accent: #eef2ff; /* indigo-50 — selected thread row tint */
|
||||
--accent-foreground: #18181b;
|
||||
|
||||
--destructive: #ef4444;
|
||||
--destructive-foreground: #ffffff;
|
||||
|
||||
--border: #e4e4e7; /* zinc-200 */
|
||||
--input: #e4e4e7;
|
||||
--ring: #a5b4fc; /* indigo-300 — focus ring keyed to the accent */
|
||||
|
||||
/* Match the chat's rounded-2xl (1rem) corner language */
|
||||
--radius: 1rem;
|
||||
|
||||
/* Match the chat's system-ui sans stack */
|
||||
--font-body:
|
||||
ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
||||
}
|
||||
|
||||
/* The CopilotKit v2 chat (CopilotSidebar) stays LIGHT regardless of the OS color
|
||||
scheme, so the threads layout is held light too — otherwise the SDK drawer
|
||||
would clash with the sidebar it sits beside. The confirm dialog renders in a
|
||||
portal on <body>, so re-pin there as well. Values mirror the V2 light theme
|
||||
(oklch(0.145 0 0) / oklch(1 0 0)). */
|
||||
.threadsLayout,
|
||||
body > [role="presentation"] {
|
||||
--foreground: oklch(0.145 0 0);
|
||||
--background: oklch(1 0 0);
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--background);
|
||||
color: var(--foreground);
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
}
|
||||
|
||||
body,
|
||||
html {
|
||||
height: 100%;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import type { Metadata } from "next";
|
||||
|
||||
import { CopilotKit } from "@copilotkit/react-core/v2";
|
||||
import "./globals.css";
|
||||
import "@copilotkit/react-core/v2/styles.css";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Agno + CopilotKit Starter",
|
||||
description: "A starter demo connecting an Agno agent to CopilotKit.",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en">
|
||||
{/*
|
||||
suppressHydrationWarning: browser extensions (e.g. Grammarly) inject
|
||||
attributes like data-gr-ext-installed onto <body> before React hydrates,
|
||||
which would otherwise surface as a hydration mismatch on first load.
|
||||
This only relaxes the check for <body>'s own attributes (one level deep);
|
||||
everything rendered inside <body> is still fully hydration-checked.
|
||||
*/}
|
||||
<body className={"antialiased"} suppressHydrationWarning>
|
||||
{/* Force REST transport so runtime-info + threads both hit the multi-route endpoint (auto-detect races the lazily-compiled API route in next dev). */}
|
||||
<CopilotKit runtimeUrl="/api/copilotkit" useSingleEndpoint={false}>
|
||||
{children}
|
||||
</CopilotKit>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
.layout {
|
||||
display: grid;
|
||||
/*
|
||||
Reserve the desktop drawer's width (its default 320px) as a fixed first
|
||||
column so the layout doesn't shift when the client-only <CopilotThreadsDrawer>
|
||||
mounts after hydration. On mobile the drawer is an off-canvas overlay (out
|
||||
of flow), so the column collapses and the content fills the width.
|
||||
*/
|
||||
grid-template-columns: var(--cpk-drawer-reserved-width, 320px) minmax(0, 1fr);
|
||||
/* Drawer sets --cpk-drawer-reserved-width to 0 on desktop-collapse, so the
|
||||
reserved column collapses and the chat reclaims the space. */
|
||||
transition: grid-template-columns 0.2s ease;
|
||||
/*
|
||||
Bound the single grid row to the viewport (minmax(0,1fr) lets it shrink
|
||||
below content) so a long thread list scrolls INTERNALLY in the drawer with
|
||||
the header pinned, instead of the drawer growing past the viewport and the
|
||||
page scrolling the header away.
|
||||
*/
|
||||
grid-template-rows: minmax(0, 1fr);
|
||||
height: 100dvh;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.mainPanel {
|
||||
/*
|
||||
Pin the content to the SECOND track explicitly. The client-only drawer
|
||||
renders nothing during SSR, so without this the content would flow into the
|
||||
reserved first column at first paint and then jump once the drawer mounts.
|
||||
*/
|
||||
grid-column: 2;
|
||||
min-width: 0;
|
||||
height: 100dvh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/*
|
||||
Mobile (≤768px): the drawer is an off-canvas overlay — collapse to a single
|
||||
track. MUST come after the base rules (media queries add no specificity, so a
|
||||
later same-specificity base rule would otherwise win and leak the two-column
|
||||
desktop layout onto mobile).
|
||||
*/
|
||||
@media (max-width: 768px) {
|
||||
.layout {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
.mainPanel {
|
||||
grid-column: auto;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
useAgent,
|
||||
useConfigureSuggestions,
|
||||
useDefaultRenderTool,
|
||||
useFrontendTool,
|
||||
useRenderTool,
|
||||
CopilotSidebar,
|
||||
CopilotChatConfigurationProvider,
|
||||
CopilotThreadsDrawer,
|
||||
} from "@copilotkit/react-core/v2";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { z } from "zod";
|
||||
import { DefaultToolComponent } from "@/components/default-tool-ui";
|
||||
import { WeatherCard } from "@/components/weather";
|
||||
|
||||
import styles from "./page.module.css";
|
||||
|
||||
// agno registers a single agent under the key "default" (see
|
||||
// src/app/api/copilotkit/[[...slug]]/route.ts), so the threads drawer + chat
|
||||
// config provider must address that same agent id.
|
||||
const AGENT_ID = "default";
|
||||
|
||||
export default function CopilotKitPage() {
|
||||
const [themeColor, setThemeColor] = useState("#6366f1");
|
||||
|
||||
// 🪁 Frontend Actions: https://docs.copilotkit.ai/guides/frontend-actions
|
||||
useFrontendTool({
|
||||
name: "set_theme_color",
|
||||
parameters: z.object({
|
||||
theme_color: z
|
||||
.string()
|
||||
.describe("The theme color to set. Make sure to pick nice colors."),
|
||||
}),
|
||||
handler: async ({ theme_color }) => {
|
||||
setThemeColor(theme_color);
|
||||
return `Changing theme color to ${theme_color}`;
|
||||
},
|
||||
});
|
||||
|
||||
// 🪁 Suggestions: https://docs.copilotkit.ai/guides/suggestions
|
||||
useConfigureSuggestions({
|
||||
available: "always",
|
||||
suggestions: [
|
||||
{
|
||||
title: "Generative UI",
|
||||
message: "What's the weather in San Francisco?",
|
||||
},
|
||||
{
|
||||
title: "Frontend Tools",
|
||||
message: "Set the theme to green.",
|
||||
},
|
||||
{
|
||||
title: "Default Tool Rendering",
|
||||
message: "What's the latest price of Apple stock?",
|
||||
},
|
||||
{
|
||||
title: "Writing Agent State",
|
||||
message: "Add a proverb about AI.",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
return (
|
||||
/*
|
||||
One UNCONTROLLED CopilotChatConfigurationProvider (no `threadId` prop) owns
|
||||
the active thread for the whole surface. The SDK <CopilotThreadsDrawer> drives it
|
||||
directly — selecting a row sets the active thread, "+ New" resets to a
|
||||
fresh thread — with no host thread-state. The proverbs/weather content
|
||||
and the CopilotSidebar read the same active thread from the provider (the
|
||||
content's `useAgent()` falls back to it). A *controlled* provider would
|
||||
block "+ New" from resetting, so uncontrolled-inside-provider is required.
|
||||
`.threadsLayout` (globals.css) pins the light theme vars the drawer +
|
||||
sidebar inherit; the SDK drawer follows them by token inheritance.
|
||||
*/
|
||||
<CopilotChatConfigurationProvider agentId={AGENT_ID}>
|
||||
<div className={`${styles.layout} threadsLayout`}>
|
||||
{/* SDK threads drawer (replaces the hand-rolled fork). License-gated: the locked view's Upgrade CTA opens the Intelligence docs by default. */}
|
||||
<CopilotThreadsDrawer agentId={AGENT_ID} />
|
||||
<div className={styles.mainPanel}>
|
||||
<main
|
||||
style={
|
||||
{
|
||||
"--copilot-kit-primary-color": themeColor,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
>
|
||||
<YourMainContent themeColor={themeColor} />
|
||||
<CopilotSidebar
|
||||
defaultOpen={true}
|
||||
// Adds an initial message to the chat
|
||||
labels={{
|
||||
modalHeaderTitle: "Popup Assistant",
|
||||
welcomeMessageText:
|
||||
"👋 Hi, there! You're chatting with an Agno agent.",
|
||||
}}
|
||||
/>
|
||||
{/* CopilotSidebar self-docks; main content renders as a sibling. */}
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</CopilotChatConfigurationProvider>
|
||||
);
|
||||
}
|
||||
|
||||
// State of the agent, make sure this aligns with your agent's state.
|
||||
type AgentState = {
|
||||
proverbs: string[];
|
||||
};
|
||||
|
||||
function YourMainContent({ themeColor }: { themeColor: string }) {
|
||||
// 🪁 Shared State: https://docs.copilotkit.ai/coagents/shared-state
|
||||
// V2: useAgent returns the agent; read agent.state and write via agent.setState.
|
||||
const { agent } = useAgent({ agentId: "default" });
|
||||
const state = (agent.state as AgentState | undefined) ?? { proverbs: [] };
|
||||
const setState = (next: AgentState) => agent.setState(next);
|
||||
|
||||
// Seed an initial proverb once (the V2 agent starts with empty state).
|
||||
useEffect(() => {
|
||||
if ((agent.state as AgentState | undefined)?.proverbs === undefined) {
|
||||
agent.setState({
|
||||
proverbs: [
|
||||
"CopilotKit may be new, but it's the best thing since sliced bread.",
|
||||
],
|
||||
});
|
||||
}
|
||||
}, [agent]);
|
||||
|
||||
// 🪁 Frontend Actions: https://docs.copilotkit.ai/agno/frontend-tools
|
||||
useFrontendTool({
|
||||
name: "add_proverb",
|
||||
parameters: z.object({
|
||||
proverb: z
|
||||
.string()
|
||||
.describe("The proverb to add. Make it witty, short and concise."),
|
||||
}),
|
||||
handler: async ({ proverb }) => {
|
||||
// Read agent.state at call time so rapid successive adds don't drop
|
||||
// earlier proverbs via a stale closure over `state`.
|
||||
agent.setState({
|
||||
proverbs: [
|
||||
...((agent.state as AgentState | undefined)?.proverbs ?? []),
|
||||
proverb,
|
||||
],
|
||||
});
|
||||
return `Added proverb: ${proverb}`;
|
||||
},
|
||||
});
|
||||
|
||||
//🪁 Generative UI: https://docs.copilotkit.ai/agno/generative-ui/backend-tools
|
||||
useRenderTool(
|
||||
{
|
||||
name: "get_weather",
|
||||
parameters: z.object({
|
||||
location: z.string(),
|
||||
}),
|
||||
render: ({ parameters }) => (
|
||||
<WeatherCard themeColor={themeColor} location={parameters.location} />
|
||||
),
|
||||
},
|
||||
[themeColor],
|
||||
);
|
||||
|
||||
//🪁 Default Generative UI: https://docs.copilotkit.ai/agno/generative-ui/backend-tools
|
||||
useDefaultRenderTool(
|
||||
{
|
||||
render: (props) => (
|
||||
<DefaultToolComponent themeColor={themeColor} {...props} />
|
||||
),
|
||||
},
|
||||
[themeColor],
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{ backgroundColor: themeColor }}
|
||||
className="h-screen flex justify-center items-center flex-col transition-colors duration-300"
|
||||
>
|
||||
<div className="bg-white/20 backdrop-blur-md p-8 rounded-2xl shadow-xl max-w-2xl w-full">
|
||||
<h1 className="text-4xl font-bold text-white mb-2 text-center">
|
||||
Proverbs
|
||||
</h1>
|
||||
<p className="text-gray-200 text-center italic mb-6">
|
||||
This is a demonstrative page, but it could be anything you want! 🪁
|
||||
</p>
|
||||
<hr className="border-white/20 my-6" />
|
||||
<div className="flex flex-col gap-3">
|
||||
{state.proverbs?.map((proverb, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="bg-white/15 p-4 rounded-xl text-white relative group hover:bg-white/20 transition-all"
|
||||
>
|
||||
<p className="pr-8">{proverb}</p>
|
||||
<button
|
||||
onClick={() =>
|
||||
setState({
|
||||
...state,
|
||||
proverbs: state.proverbs?.filter((_, i) => i !== index),
|
||||
})
|
||||
}
|
||||
className="absolute right-3 top-3 opacity-0 group-hover:opacity-100 transition-opacity
|
||||
bg-red-500 hover:bg-red-600 text-white rounded-full h-6 w-6 flex items-center justify-center"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{state.proverbs?.length === 0 && (
|
||||
<p className="text-center text-white/80 italic my-8">
|
||||
No proverbs yet. Ask the assistant to add some!
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
import { useState } from "react";
|
||||
|
||||
export type BackendToolsProps = {
|
||||
name: string;
|
||||
parameters: unknown;
|
||||
status: "inProgress" | "executing" | "complete";
|
||||
result: string | undefined;
|
||||
themeColor: string;
|
||||
};
|
||||
|
||||
export function DefaultToolComponent({
|
||||
name,
|
||||
parameters,
|
||||
status,
|
||||
result,
|
||||
themeColor,
|
||||
}: BackendToolsProps) {
|
||||
const [showArgs, setShowArgs] = useState(false);
|
||||
const [showResult, setShowResult] = useState(false);
|
||||
const getStatusColor = () => {
|
||||
switch (status) {
|
||||
case "executing":
|
||||
case "inProgress":
|
||||
return "bg-blue-500/20 text-blue-300 border-blue-400/30";
|
||||
case "complete":
|
||||
return "bg-green-500/20 text-green-300 border-green-400/30";
|
||||
default:
|
||||
return "bg-white/20 text-white/70 border-white/30";
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusIcon = () => {
|
||||
switch (status) {
|
||||
case "executing":
|
||||
case "inProgress":
|
||||
return (
|
||||
<svg className="w-4 h-4 animate-spin" fill="none" viewBox="0 0 24 24">
|
||||
<circle
|
||||
className="opacity-25"
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
stroke="currentColor"
|
||||
strokeWidth="4"
|
||||
></circle>
|
||||
<path
|
||||
className="opacity-75"
|
||||
fill="currentColor"
|
||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||
></path>
|
||||
</svg>
|
||||
);
|
||||
case "complete":
|
||||
return (
|
||||
<svg
|
||||
className="w-4 h-4"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M5 13l4 4L19 7"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{ backgroundColor: themeColor }}
|
||||
className="backdrop-blur-sm rounded-lg p-4 mt-4 mb-4 max-w-md w-full border border-white/20"
|
||||
>
|
||||
{/* Header with tool name and status */}
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h3 className="text-white font-medium text-sm">🔧 {name}</h3>
|
||||
<span
|
||||
className={`flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-medium border ${getStatusColor()}`}
|
||||
>
|
||||
{getStatusIcon()}
|
||||
{status}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Arguments */}
|
||||
{!!parameters &&
|
||||
typeof parameters === "object" &&
|
||||
Object.keys(parameters as Record<string, unknown>).length > 0 && (
|
||||
<div className="mb-3">
|
||||
<button
|
||||
onClick={() => setShowArgs(!showArgs)}
|
||||
className="flex items-center gap-2 text-white/80 text-xs font-medium mb-1.5 hover:text-white transition-colors"
|
||||
>
|
||||
<svg
|
||||
className={`w-3 h-3 transition-transform ${showArgs ? "rotate-90" : ""}`}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M9 5l7 7-7 7"
|
||||
/>
|
||||
</svg>
|
||||
Arguments
|
||||
</button>
|
||||
{showArgs && (
|
||||
<div className="bg-black/20 rounded p-2 space-y-1">
|
||||
{Object.entries(parameters as Record<string, unknown>).map(
|
||||
([key, value]) => (
|
||||
<div key={key} className="flex gap-2">
|
||||
<span className="text-white/50 text-xs">{key}:</span>
|
||||
<span className="text-white/80 text-xs font-mono">
|
||||
{JSON.stringify(value)}
|
||||
</span>
|
||||
</div>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Result */}
|
||||
{result && (
|
||||
<div>
|
||||
<button
|
||||
onClick={() => setShowResult(!showResult)}
|
||||
className="flex items-center gap-2 text-white/80 text-xs font-medium mb-1.5 hover:text-white transition-colors"
|
||||
>
|
||||
<svg
|
||||
className={`w-3 h-3 transition-transform ${showResult ? "rotate-90" : ""}`}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M9 5l7 7-7 7"
|
||||
/>
|
||||
</svg>
|
||||
Result
|
||||
</button>
|
||||
{showResult && (
|
||||
<div className="bg-black/20 rounded p-2">
|
||||
<pre className="text-white/80 text-xs font-mono whitespace-pre-wrap break-words">
|
||||
{typeof result === "string"
|
||||
? result
|
||||
: JSON.stringify(result, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import * as React from "react";
|
||||
import { cva } from "class-variance-authority";
|
||||
import type { VariantProps } from "class-variance-authority";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-[var(--radius)] text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--ring)] disabled:pointer-events-none disabled:opacity-50 cursor-pointer",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"bg-[var(--primary)] text-[var(--primary-foreground)] hover:opacity-90",
|
||||
secondary:
|
||||
"bg-[var(--secondary)] text-[var(--secondary-foreground)] hover:opacity-80",
|
||||
outline:
|
||||
"border border-[var(--border)] bg-[var(--background)] hover:bg-[var(--secondary)]",
|
||||
ghost:
|
||||
"hover:bg-[var(--secondary)] hover:text-[var(--secondary-foreground)]",
|
||||
destructive:
|
||||
"bg-[var(--destructive)] text-[var(--destructive-foreground)] hover:opacity-90",
|
||||
},
|
||||
size: {
|
||||
default: "h-9 px-4 py-2",
|
||||
sm: "h-8 rounded-md px-3 text-xs",
|
||||
lg: "h-10 rounded-md px-6",
|
||||
icon: "h-9 w-9",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
export interface ButtonProps
|
||||
extends
|
||||
React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {}
|
||||
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant, size, ...props }, ref) => (
|
||||
<button
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
);
|
||||
Button.displayName = "Button";
|
||||
|
||||
export { Button, buttonVariants };
|
||||
@@ -0,0 +1,85 @@
|
||||
import * as React from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Card = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"rounded-[var(--radius)] border border-[var(--border)] bg-[var(--card)] text-[var(--card-foreground)] shadow-sm",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
Card.displayName = "Card";
|
||||
|
||||
const CardHeader = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("flex flex-col space-y-1.5 p-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
CardHeader.displayName = "CardHeader";
|
||||
|
||||
const CardTitle = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"text-lg font-semibold leading-none tracking-tight",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
CardTitle.displayName = "CardTitle";
|
||||
|
||||
const CardDescription = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("text-sm text-[var(--muted-foreground)]", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
CardDescription.displayName = "CardDescription";
|
||||
|
||||
const CardContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
|
||||
));
|
||||
CardContent.displayName = "CardContent";
|
||||
|
||||
const CardFooter = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("flex items-center p-6 pt-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
CardFooter.displayName = "CardFooter";
|
||||
|
||||
export {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
CardDescription,
|
||||
CardContent,
|
||||
CardFooter,
|
||||
};
|
||||
@@ -0,0 +1,64 @@
|
||||
interface WeatherCardProps {
|
||||
location?: string;
|
||||
themeColor: string;
|
||||
}
|
||||
|
||||
function SunIcon() {
|
||||
return (
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
className="w-14 h-14 text-yellow-200"
|
||||
>
|
||||
<circle cx="12" cy="12" r="5" />
|
||||
<path
|
||||
d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42"
|
||||
strokeWidth="2"
|
||||
stroke="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function WeatherCard({ location, themeColor }: WeatherCardProps) {
|
||||
return (
|
||||
<div
|
||||
style={{ backgroundColor: themeColor }}
|
||||
className="rounded-xl shadow-xl mt-6 mb-4 max-w-md w-full"
|
||||
>
|
||||
<div className="bg-white/20 p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h3 className="text-xl font-bold text-white capitalize">
|
||||
{location}
|
||||
</h3>
|
||||
<p className="text-white">Current Weather</p>
|
||||
</div>
|
||||
<SunIcon />
|
||||
</div>
|
||||
|
||||
<div className="mt-4 flex items-end justify-between">
|
||||
<div className="text-3xl font-bold text-white">70°</div>
|
||||
<div className="text-sm text-white">Clear skies</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 pt-4 border-t border-white">
|
||||
<div className="grid grid-cols-3 gap-2 text-center">
|
||||
<div>
|
||||
<p className="text-white text-xs">Humidity</p>
|
||||
<p className="text-white font-medium">45%</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-white text-xs">Wind</p>
|
||||
<p className="text-white font-medium">5 mph</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-white text-xs">Feels Like</p>
|
||||
<p className="text-white font-medium">72°</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { clsx } from "clsx";
|
||||
import type { ClassValue } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2017",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "react-jsx",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".next/types/**/*.ts",
|
||||
".next/dev/types/**/*.ts"
|
||||
],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
Reference in New Issue
Block a user