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,51 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnpm-store
|
||||
/.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
|
||||
.env*
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
|
||||
.mastra/
|
||||
|
||||
|
||||
# python
|
||||
venv
|
||||
.venv
|
||||
__pycache__
|
||||
@@ -0,0 +1,55 @@
|
||||
# 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 ./
|
||||
|
||||
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 from lockfile
|
||||
COPY agent/ ./agent/
|
||||
RUN cd agent && uv sync --frozen
|
||||
|
||||
# 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 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,132 @@
|
||||
# CopilotKit <> Microsoft Agent Framework (Python)
|
||||
|
||||
This is a starter template for building CopilotKit experiences using the [Microsoft Agent Framework](https://aka.ms/agent-framework). It ships with a Next.js UI and a FastAPI server that exposes a Microsoft Agent Framework agent over the AG-UI protocol, so you can study and customize both sides of the stack.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- OpenAI or Azure OpenAI credentials (for the Microsoft Agent Framework agent)
|
||||
- Python 3.12+
|
||||
- uv
|
||||
- Node.js 20+
|
||||
- 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:** This automatically sets up the Python environment as well.
|
||||
>
|
||||
> If you have manual issues, you can run:
|
||||
>
|
||||
> ```sh
|
||||
> npm run install:agent
|
||||
> ```
|
||||
|
||||
2. Set up your agent credentials. The backend automatically uses Azure when the Azure env vars below are present; otherwise it falls back to OpenAI. Create a `.env` file inside the `agent` folder with one of the following configurations:
|
||||
|
||||
**OpenAI**
|
||||
|
||||
```
|
||||
OPENAI_API_KEY=sk-...your-openai-key-here...
|
||||
OPENAI_CHAT_MODEL_ID=gpt-4o-mini
|
||||
```
|
||||
|
||||
**Azure OpenAI**
|
||||
|
||||
```
|
||||
AZURE_OPENAI_ENDPOINT=https://your-resource.openai.azure.com/
|
||||
AZURE_OPENAI_CHAT_DEPLOYMENT_NAME=gpt-4o-mini
|
||||
# If you are not relying on az login:
|
||||
# AZURE_OPENAI_API_KEY=...
|
||||
```
|
||||
|
||||
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 the Microsoft Agent Framework server 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:debug` – Starts development servers with debug logging enabled
|
||||
- `dev:ui` – Starts only the Next.js UI server
|
||||
- `dev:agent` – Starts only the Microsoft Agent Framework server
|
||||
- `build` – Builds the Next.js application for production
|
||||
- `start` – Starts the production server
|
||||
- `lint` – Runs ESLint for code linting
|
||||
- `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
|
||||
|
||||
## 📚 Documentation
|
||||
|
||||
- [Microsoft Agent Framework](https://aka.ms/agent-framework) – Learn more about Microsoft Agent Framework 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
|
||||
|
||||
## 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 Microsoft Agent Framework agent is running on port 8000
|
||||
2. Your OpenAI/Azure credentials are set correctly
|
||||
3. Both servers started successfully
|
||||
|
||||
### Python Dependencies
|
||||
|
||||
If you encounter Python import errors:
|
||||
|
||||
```bash
|
||||
cd agent
|
||||
uv sync
|
||||
uv run src/main.py
|
||||
```
|
||||
@@ -0,0 +1 @@
|
||||
3.12
|
||||
@@ -0,0 +1,7 @@
|
||||
[project]
|
||||
name = "agent"
|
||||
version = "0.1.0"
|
||||
description = "Add your description here"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = ["agent-framework-ag-ui>=1.0.0b251117", "python-dotenv"]
|
||||
@@ -0,0 +1,126 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from textwrap import dedent
|
||||
from typing import Annotated
|
||||
|
||||
from agent_framework import ChatAgent, ChatClientProtocol, ai_function
|
||||
from agent_framework_ag_ui import AgentFrameworkAgent
|
||||
from pydantic import Field
|
||||
|
||||
STATE_SCHEMA: dict[str, object] = {
|
||||
"proverbs": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"description": "Ordered list of the user's saved proverbs.",
|
||||
}
|
||||
}
|
||||
|
||||
PREDICT_STATE_CONFIG: dict[str, dict[str, str]] = {
|
||||
"proverbs": {
|
||||
"tool": "update_proverbs",
|
||||
"tool_argument": "proverbs",
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ai_function(
|
||||
name="update_proverbs",
|
||||
description=(
|
||||
"Replace the entire list of proverbs with the provided values. "
|
||||
"Always include every proverb you want to keep."
|
||||
),
|
||||
)
|
||||
def update_proverbs(
|
||||
proverbs: Annotated[
|
||||
list[str],
|
||||
Field(
|
||||
description=(
|
||||
"The complete source of truth for the user's proverbs. "
|
||||
"Maintain ordering and include the full list on each call."
|
||||
)
|
||||
),
|
||||
],
|
||||
) -> str:
|
||||
"""Persist the provided set of proverbs."""
|
||||
return f"Proverbs updated. Tracking {len(proverbs)} item(s)."
|
||||
|
||||
|
||||
@ai_function(
|
||||
name="get_weather",
|
||||
description="Share a quick weather update for a location. Use this to render the frontend weather card.",
|
||||
)
|
||||
def get_weather(
|
||||
location: Annotated[
|
||||
str,
|
||||
Field(
|
||||
description="The city or region to describe. Use fully spelled out names."
|
||||
),
|
||||
],
|
||||
) -> str:
|
||||
"""Return a short natural language weather summary."""
|
||||
normalized = location.strip().title() or "the requested location"
|
||||
return (
|
||||
f"The weather in {normalized} is mild with a light breeze. "
|
||||
"Skies are mostly clear—perfect for planning something fun."
|
||||
)
|
||||
|
||||
|
||||
@ai_function(
|
||||
name="go_to_moon",
|
||||
description="Request a playful human-in-the-loop confirmation before launching a mission to the moon.",
|
||||
approval_mode="always_require",
|
||||
)
|
||||
def go_to_moon() -> str:
|
||||
"""Request human approval before continuing."""
|
||||
return "Mission control requested. Awaiting human approval for the lunar launch."
|
||||
|
||||
|
||||
def create_agent(chat_client: ChatClientProtocol) -> AgentFrameworkAgent:
|
||||
"""Instantiate the CopilotKit demo agent backed by Microsoft Agent Framework."""
|
||||
base_agent = ChatAgent(
|
||||
name="proverbs_agent",
|
||||
instructions=dedent(
|
||||
"""
|
||||
You help users brainstorm, organize, and refine proverbs while coordinating UI updates.
|
||||
|
||||
State sync:
|
||||
- The current list of proverbs is provided in the conversation context.
|
||||
- When you add, remove, or reorder proverbs, call `update_proverbs` with the full list.
|
||||
Never send partial updates—always include every proverb that should exist.
|
||||
- CRITICAL: When asked to "add" a proverb, you must:
|
||||
1. First, identify ALL existing proverbs from the conversation history
|
||||
2. Create EXACTLY ONE new proverb (never more than one unless explicitly requested)
|
||||
3. Call update_proverbs with: [all existing proverbs] + [the one new proverb]
|
||||
Example: Current: ["A", "B"] -> After adding: ["A", "B", "C"] (NOT ["A", "B", "C", "D", "E"])
|
||||
- When asked to "remove" a proverb, remove exactly ONE item unless user specifies otherwise.
|
||||
|
||||
Tool usage rules:
|
||||
- When user asks to go to the moon, you MUST call the `go_to_moon` tool immediately. Do NOT ask for approval
|
||||
yourself—the tool's approval workflow and the client UI will handle it.
|
||||
|
||||
Frontend integrations:
|
||||
- `get_weather` renders a weather card in the UI. Only call this tool when the user explicitly
|
||||
asks for weather. Do NOT call it after unrelated tasks or approvals.
|
||||
- `go_to_moon` requires explicit user approval before you proceed. Only use it when a
|
||||
user asks to launch or travel to the moon. Always call the tool instead of asking manually.
|
||||
|
||||
Conversation tips:
|
||||
- Reference the latest proverb list before suggesting changes.
|
||||
- Keep responses concise and friendly unless the user requests otherwise.
|
||||
- After you finish executing tools for the user's request, provide a brief, final assistant
|
||||
message summarizing exactly what changed. Do NOT call additional tools or switch topics
|
||||
after that summary unless the user asks. ALWAYS send this conversational summary so the message persists.
|
||||
""".strip()
|
||||
),
|
||||
chat_client=chat_client,
|
||||
tools=[update_proverbs, get_weather, go_to_moon],
|
||||
)
|
||||
|
||||
return AgentFrameworkAgent(
|
||||
agent=base_agent,
|
||||
name="CopilotKitMicrosoftAgentFrameworkAgent",
|
||||
description="Manages proverbs, weather snippets, and human-in-the-loop moon launches.",
|
||||
state_schema=STATE_SCHEMA,
|
||||
predict_state_config=PREDICT_STATE_CONFIG,
|
||||
require_confirmation=False, # Allow immediate state updates with follow-up messages
|
||||
)
|
||||
@@ -0,0 +1,79 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
|
||||
import uvicorn
|
||||
from agent_framework._clients import ChatClientProtocol
|
||||
from azure.identity import DefaultAzureCredential
|
||||
from agent_framework.azure import AzureOpenAIChatClient
|
||||
from agent_framework.openai import OpenAIChatClient
|
||||
from agent_framework_ag_ui import add_agent_framework_fastapi_endpoint
|
||||
from dotenv import load_dotenv
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
|
||||
from agent import create_agent
|
||||
|
||||
load_dotenv()
|
||||
|
||||
|
||||
def _build_chat_client() -> ChatClientProtocol:
|
||||
try:
|
||||
if bool(os.getenv("AZURE_OPENAI_ENDPOINT")):
|
||||
# Azure OpenAI setup - uses environment variables by default
|
||||
# Optionally can pass deployment_name explicitly
|
||||
deployment_name = os.getenv(
|
||||
"AZURE_OPENAI_CHAT_DEPLOYMENT_NAME", "gpt-4o-mini"
|
||||
)
|
||||
return AzureOpenAIChatClient(
|
||||
credential=DefaultAzureCredential(),
|
||||
deployment_name=deployment_name,
|
||||
endpoint=os.getenv("AZURE_OPENAI_ENDPOINT"),
|
||||
)
|
||||
|
||||
if bool(os.getenv("OPENAI_API_KEY")):
|
||||
# OpenAI setup - requires explicit model_id and api_key
|
||||
return OpenAIChatClient(
|
||||
model_id=os.getenv("OPENAI_CHAT_MODEL_ID", "gpt-4o-mini"),
|
||||
api_key=os.getenv("OPENAI_API_KEY"),
|
||||
)
|
||||
|
||||
raise ValueError(
|
||||
"Either AZURE_OPENAI_ENDPOINT or OPENAI_API_KEY environment variable is required"
|
||||
)
|
||||
|
||||
except Exception as exc: # pragma: no cover
|
||||
raise RuntimeError(
|
||||
"Unable to initialize the chat client. Double-check your API credentials as documented in README.md."
|
||||
) from exc
|
||||
|
||||
|
||||
chat_client = _build_chat_client()
|
||||
my_agent = create_agent(chat_client)
|
||||
|
||||
app = FastAPI(title="CopilotKit + Microsoft Agent Framework (Python)")
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
|
||||
add_agent_framework_fastapi_endpoint(
|
||||
app=app,
|
||||
agent=my_agent,
|
||||
path="/",
|
||||
)
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
async def health():
|
||||
return {"status": "ok"}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
host = os.getenv("AGENT_HOST", "0.0.0.0")
|
||||
port = int(os.getenv("AGENT_PORT", "8000"))
|
||||
uvicorn.run("main:app", host=host, port=port, reload=True)
|
||||
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; s=socket.create_connection(('localhost',8000),2); s.close()",
|
||||
]
|
||||
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:-ms-agent-framework-python}
|
||||
- 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,18 @@
|
||||
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/
|
||||
|
||||
ENV PYTHONPATH=/app/src
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY pyproject.toml uv.lock ./
|
||||
COPY src/ ./src/
|
||||
|
||||
RUN uv sync
|
||||
|
||||
EXPOSE 8000
|
||||
|
||||
CMD ["uv", "run", "uvicorn", "src.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"]
|
||||
@@ -0,0 +1,17 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Start Python agent on port 8000 (override PORT which Railway sets to 3000)
|
||||
cd /app/agent
|
||||
PYTHONPATH=/app/agent/src AGENT_PORT=8000 PORT=8000 /app/agent/.venv/bin/python -m uvicorn src.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,16 @@
|
||||
{
|
||||
"fixtures": [
|
||||
{
|
||||
"match": { "userMessage": "Hello" },
|
||||
"response": {
|
||||
"content": "Hello! I'm the ms-agent-framework-python AI assistant. How can I help you?"
|
||||
}
|
||||
},
|
||||
{
|
||||
"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,25 @@
|
||||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
serverExternalPackages: ["@copilotkit/runtime"],
|
||||
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).
|
||||
NEXT_PUBLIC_COPILOTKIT_THREADS_ENABLED: process.env.COPILOTKIT_LICENSE_TOKEN
|
||||
? "true"
|
||||
: "false",
|
||||
},
|
||||
typescript: {
|
||||
// The verbatim demo (useFrontendTool/useRenderToolCall parameter shapes)
|
||||
// has type drift against the installed @copilotkit/react-core@1.55.2 v2
|
||||
// typings — the same demo-vs-installed-types mismatch the sibling examples
|
||||
// (mastra, langgraph-python) absorb with this flag.
|
||||
ignoreBuildErrors: true,
|
||||
},
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"name": "copilotkit-microsoft-agent-framework-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:debug": "LOG_LEVEL=debug npm run dev",
|
||||
"dev:agent": "./scripts/run-agent.sh || scripts\\run-agent.bat",
|
||||
"dev:ui": "next dev --turbopack",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"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",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"hono": "^4.12.10",
|
||||
"lucide-react": "^0.525.0",
|
||||
"next": "16.0.8",
|
||||
"react": "^19.2.1",
|
||||
"react-dom": "^19.2.1",
|
||||
"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 src/main.py
|
||||
@@ -0,0 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Navigate to the agent directory
|
||||
cd "$(dirname "$0")/../agent" || exit 1
|
||||
|
||||
# Run the agent using uv
|
||||
uv run src/main.py
|
||||
@@ -0,0 +1,6 @@
|
||||
@echo off
|
||||
REM Navigate to the agent directory
|
||||
cd /d "%~dp0\..\agent" || exit /b 1
|
||||
|
||||
REM Install dependencies and create virtual environment using uv
|
||||
uv sync
|
||||
@@ -0,0 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Navigate to the agent directory
|
||||
cd "$(dirname "$0")/../agent" || exit 1
|
||||
|
||||
# Install dependencies and create virtual environment using uv
|
||||
uv sync
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
import {
|
||||
CopilotRuntime,
|
||||
CopilotKitIntelligence,
|
||||
InMemoryAgentRunner,
|
||||
createCopilotEndpoint,
|
||||
} from "@copilotkit/runtime/v2";
|
||||
import { HttpAgent } from "@ag-ui/client";
|
||||
import { handle } from "hono/vercel";
|
||||
|
||||
// 1. Create the CopilotRuntime instance and utilize the Microsoft Agent Framework
|
||||
// AG-UI integration to set up the connection.
|
||||
const runtime = new CopilotRuntime({
|
||||
agents: {
|
||||
// Our FastAPI endpoint URL
|
||||
default: new HttpAgent({
|
||||
url: process.env.AGENT_URL || "http://localhost:8000/",
|
||||
}),
|
||||
},
|
||||
// --- 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",
|
||||
}),
|
||||
identifyUser: () => ({ id: "demo-user", name: "Demo User" }),
|
||||
licenseToken: process.env.COPILOTKIT_LICENSE_TOKEN,
|
||||
}
|
||||
: { runner: new InMemoryAgentRunner() }),
|
||||
// --- /copilotkit:intelligence ---
|
||||
});
|
||||
|
||||
// 2. Build a Next.js API route that handles the CopilotKit runtime requests.
|
||||
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,75 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
:root {
|
||||
--background: #ffffff;
|
||||
--foreground: #171717;
|
||||
|
||||
/* Threads-panel theme-map — values pulled verbatim from CopilotKit's V2
|
||||
design system (the same one this example's CopilotSidebar renders with;
|
||||
see @copilotkit/react-core/src/v2/styles/globals.css). The drawer is a
|
||||
left-side companion to that LIGHT sidebar, so it inherits the sidebar's
|
||||
exact neutral-gray palette, charcoal "primary" (NOT a brand accent — the
|
||||
sidebar's primary buttons are near-black), and 0.625rem radius scale.
|
||||
This keeps the threads panel reading as one product with the chat. */
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.145 0 0);
|
||||
--primary: oklch(0.205 0 0);
|
||||
--primary-foreground: oklch(0.985 0 0);
|
||||
--secondary: oklch(0.97 0 0);
|
||||
--secondary-foreground: oklch(0.205 0 0);
|
||||
--muted: oklch(0.97 0 0);
|
||||
--muted-foreground: oklch(0.556 0 0);
|
||||
--accent: oklch(0.97 0 0);
|
||||
--accent-foreground: oklch(0.205 0 0);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--destructive-foreground: oklch(0.985 0 0);
|
||||
--border: oklch(0.922 0 0);
|
||||
--input: oklch(0.922 0 0);
|
||||
--ring: oklch(0.708 0 0);
|
||||
|
||||
/* Match the V2 sidebar's radius scale exactly (--radius: 0.625rem there). */
|
||||
--radius: 0.625rem;
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
|
||||
/* This example loads no custom web font — its body uses the system Arial
|
||||
stack — so the drawer follows the same stack (and a system monospace for
|
||||
the locked-state command well). */
|
||||
--font-body: Arial, Helvetica, sans-serif;
|
||||
--font-code: "SFMono-Regular", Menlo, Consolas, "Liberation Mono", monospace;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--background: #0a0a0a;
|
||||
--foreground: #ededed;
|
||||
}
|
||||
}
|
||||
|
||||
/* This example's CopilotSidebar (CopilotKit's V2 chat) stays LIGHT regardless
|
||||
of the OS color scheme, so the threads panel is held light too — otherwise it
|
||||
would clash with the sidebar it sits beside. The panel reads --foreground for
|
||||
its titles/body text, which the dark-mode block above flips. The demo content
|
||||
(CopilotSidebar + ProverbsCard) uses its own colors, not these tokens, so
|
||||
re-pinning a stable light foreground/background on the threads layout wrapper
|
||||
keeps the panel readable without affecting anything else. 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: "Create Next App",
|
||||
description: "Generated by create next app",
|
||||
};
|
||||
|
||||
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,170 @@
|
||||
"use client";
|
||||
|
||||
import { ProverbsCard } from "@/components/proverbs";
|
||||
import { WeatherCard } from "@/components/weather";
|
||||
import { MoonCard } from "@/components/moon";
|
||||
import type { AgentState } from "@/lib/types";
|
||||
import {
|
||||
useAgent,
|
||||
useConfigureSuggestions,
|
||||
useFrontendTool,
|
||||
useRenderTool,
|
||||
useHumanInTheLoop,
|
||||
CopilotSidebar,
|
||||
CopilotChatConfigurationProvider,
|
||||
CopilotThreadsDrawer,
|
||||
} from "@copilotkit/react-core/v2";
|
||||
import { useEffect, useState } from "react";
|
||||
import { z } from "zod";
|
||||
|
||||
import styles from "./page.module.css";
|
||||
|
||||
export default function CopilotKitPage() {
|
||||
const [themeColor, setThemeColor] = useState("#6366f1");
|
||||
|
||||
// 🪁 Frontend Actions: https://docs.copilotkit.ai/microsoft-agent-framework/frontend-actions
|
||||
useFrontendTool({
|
||||
name: "setThemeColor",
|
||||
parameters: z.object({
|
||||
themeColor: z
|
||||
.string()
|
||||
.describe("The theme color to set. Make sure to pick nice colors."),
|
||||
}),
|
||||
async handler({ themeColor }) {
|
||||
setThemeColor(themeColor);
|
||||
return `Theme color set to ${themeColor}`;
|
||||
},
|
||||
});
|
||||
|
||||
// 🪁 Suggestions: https://docs.copilotkit.ai/microsoft-agent-framework
|
||||
useConfigureSuggestions(
|
||||
{
|
||||
suggestions: [
|
||||
{
|
||||
title: "Generative UI",
|
||||
message: "Get the weather in San Francisco.",
|
||||
},
|
||||
{
|
||||
title: "Frontend Tools",
|
||||
message: "Set the theme to green.",
|
||||
},
|
||||
{
|
||||
title: "Human In the Loop",
|
||||
message: "Please go to the moon.",
|
||||
},
|
||||
{
|
||||
title: "Write Agent State",
|
||||
message: "Add a proverb about AI.",
|
||||
},
|
||||
{
|
||||
title: "Update Agent State",
|
||||
message:
|
||||
"Please remove 1 random proverb from the list if there are any.",
|
||||
},
|
||||
{
|
||||
title: "Read Agent State",
|
||||
message: "What are the proverbs?",
|
||||
},
|
||||
],
|
||||
available: "always",
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
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/moon 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="default">
|
||||
<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="default" />
|
||||
<div className={styles.mainPanel}>
|
||||
<main
|
||||
style={
|
||||
{
|
||||
"--copilot-kit-primary-color": themeColor,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
>
|
||||
<YourMainContent themeColor={themeColor} />
|
||||
<CopilotSidebar
|
||||
defaultOpen={true}
|
||||
labels={{
|
||||
modalHeaderTitle: "Popup Assistant",
|
||||
welcomeMessageText:
|
||||
"👋 Hi, there! You're chatting with an agent.",
|
||||
}}
|
||||
/>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</CopilotChatConfigurationProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function YourMainContent({ themeColor }: { themeColor: string }) {
|
||||
// 🪁 Shared State: https://docs.copilotkit.ai/microsoft-agent-framework/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]);
|
||||
|
||||
//🪁 Generative UI: https://docs.copilotkit.ai/microsoft-agent-framework/generative-ui
|
||||
useRenderTool(
|
||||
{
|
||||
name: "get_weather",
|
||||
parameters: z.object({
|
||||
location: z.string(),
|
||||
}),
|
||||
render: ({ status, parameters }) => {
|
||||
const location =
|
||||
status === "inProgress" ? "" : (parameters.location ?? "");
|
||||
return <WeatherCard location={location} themeColor={themeColor} />;
|
||||
},
|
||||
},
|
||||
[themeColor],
|
||||
);
|
||||
|
||||
// 🪁 Human In the Loop: https://docs.copilotkit.ai/microsoft-agent-framework/human-in-the-loop
|
||||
useHumanInTheLoop(
|
||||
{
|
||||
name: "go_to_moon",
|
||||
description: "Go to the moon on request.",
|
||||
render: ({ respond, status }) => {
|
||||
return (
|
||||
<MoonCard themeColor={themeColor} status={status} respond={respond} />
|
||||
);
|
||||
},
|
||||
},
|
||||
[themeColor],
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{ backgroundColor: themeColor }}
|
||||
className="h-screen flex justify-center items-center flex-col transition-colors duration-300"
|
||||
>
|
||||
<ProverbsCard state={state} setState={setState} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import { useState } from "react";
|
||||
|
||||
export interface MoonCardProps {
|
||||
themeColor: string;
|
||||
status: "inProgress" | "executing" | "complete";
|
||||
respond?: (response: string) => void;
|
||||
}
|
||||
|
||||
export function MoonCard({ themeColor, status, respond }: MoonCardProps) {
|
||||
const [decision, setDecision] = useState<"launched" | "aborted" | null>(null);
|
||||
|
||||
const handleLaunch = () => {
|
||||
setDecision("launched");
|
||||
respond?.("You have permission to go to the moon.");
|
||||
};
|
||||
|
||||
const handleAbort = () => {
|
||||
setDecision("aborted");
|
||||
respond?.(
|
||||
"You do not have permission to go to the moon. The user you're talking to rejected the request.",
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{ backgroundColor: themeColor }}
|
||||
className="rounded-2xl shadow-xl max-w-md w-full mt-6"
|
||||
>
|
||||
<div className="bg-white/20 backdrop-blur-md p-8 w-full rounded-2xl">
|
||||
{/* Show decision or prompt */}
|
||||
{decision === "launched" ? (
|
||||
<div className="text-center">
|
||||
<div className="text-7xl mb-4">🌕</div>
|
||||
<h2 className="text-2xl font-bold text-white mb-2">
|
||||
Mission Launched
|
||||
</h2>
|
||||
<p className="text-white/90">We made it to the moon!</p>
|
||||
</div>
|
||||
) : decision === "aborted" ? (
|
||||
<div className="text-center">
|
||||
<div className="text-7xl mb-4">✋</div>
|
||||
<h2 className="text-2xl font-bold text-white mb-2">
|
||||
Mission Aborted
|
||||
</h2>
|
||||
<p className="text-white/90">Staying on Earth 🌍</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="text-center mb-6">
|
||||
<div className="text-7xl mb-4">🚀</div>
|
||||
<h2 className="text-2xl font-bold text-white mb-2">
|
||||
Ready for Launch?
|
||||
</h2>
|
||||
<p className="text-white/90">Mission to the Moon 🌕</p>
|
||||
</div>
|
||||
|
||||
{/* Launch Buttons */}
|
||||
{status === "executing" && (
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
onClick={handleLaunch}
|
||||
className="flex-1 px-6 py-4 rounded-xl bg-white text-black font-bold
|
||||
shadow-lg hover:shadow-xl transition-all
|
||||
hover:scale-105 active:scale-95"
|
||||
>
|
||||
🚀 Launch!
|
||||
</button>
|
||||
<button
|
||||
onClick={handleAbort}
|
||||
className="flex-1 px-6 py-4 rounded-xl bg-black/20 text-white font-bold
|
||||
border-2 border-white/30 shadow-lg
|
||||
transition-all hover:scale-105 active:scale-95
|
||||
hover:bg-black/30"
|
||||
>
|
||||
✋ Abort
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import { AgentState } from "@/lib/types";
|
||||
|
||||
export interface ProverbsCardProps {
|
||||
state: AgentState;
|
||||
setState: (state: AgentState) => void;
|
||||
}
|
||||
|
||||
export function ProverbsCard({ state, setState }: ProverbsCardProps) {
|
||||
// `state` is undefined until the agent syncs (V2 useAgent), so guard it.
|
||||
const proverbs = state?.proverbs ?? [];
|
||||
return (
|
||||
<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">
|
||||
{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: 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>
|
||||
{proverbs.length === 0 && (
|
||||
<p className="text-center text-white/80 italic my-8">
|
||||
No proverbs yet. Ask the assistant to add some!
|
||||
</p>
|
||||
)}
|
||||
</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,69 @@
|
||||
// Simple sun icon for the weather card
|
||||
function SunIcon() {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/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>
|
||||
);
|
||||
}
|
||||
|
||||
// Weather card component where the location and themeColor are based on what the agent
|
||||
// sets via tool calls.
|
||||
export function WeatherCard({
|
||||
location,
|
||||
themeColor,
|
||||
}: {
|
||||
location?: string;
|
||||
themeColor: string;
|
||||
}) {
|
||||
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 w-full">
|
||||
<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,4 @@
|
||||
// State of the agent, make sure this aligns with your agent's state.
|
||||
export type AgentState = {
|
||||
proverbs: string[];
|
||||
};
|
||||
@@ -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