chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,23 @@
|
||||
node_modules
|
||||
.next
|
||||
.git
|
||||
.gitignore
|
||||
.env*
|
||||
!.env.example
|
||||
.DS_Store
|
||||
*.pem
|
||||
.claude
|
||||
.mastra
|
||||
.vercel
|
||||
coverage
|
||||
agent/.venv
|
||||
agent/__pycache__
|
||||
agent/venv
|
||||
scripts
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
pnpm-lock.yaml
|
||||
bun.lockb
|
||||
agent/uv.lock
|
||||
@@ -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/
|
||||
agent/__pycache__/
|
||||
agent/.venv/
|
||||
@@ -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
|
||||
|
||||
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 using uv (matches local dev)
|
||||
COPY agent/pyproject.toml ./agent/pyproject.toml
|
||||
COPY agent/main.py ./agent/main.py
|
||||
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,117 @@
|
||||
# CopilotKit <> ADK Starter
|
||||
|
||||
This is a starter template for building AI agents using Google's [ADK](https://google.github.io/adk-docs/) 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 18+
|
||||
- Python 3.12+
|
||||
- Google Makersuite API Key (for the ADK agent) (see https://makersuite.google.com/app/apikey)
|
||||
- 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 will automatically setup a `.venv` (virtual environment) inside the `agent` directory.
|
||||
>
|
||||
> To activate the virtual environment manually, you can run:
|
||||
>
|
||||
> ```bash
|
||||
> source agent/.venv/bin/activate
|
||||
> ```
|
||||
|
||||
2. Set up your Google API key:
|
||||
|
||||
```bash
|
||||
export GOOGLE_API_KEY="your-google-api-key-here"
|
||||
```
|
||||
|
||||
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:debug` - Starts development servers with debug logging enabled
|
||||
- `dev:ui` - Starts only the Next.js UI server
|
||||
- `dev:agent` - Starts only the ADK 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
|
||||
|
||||
## 📚 Documentation
|
||||
|
||||
- [ADK Documentation](https://google.github.io/adk-docs/) - Learn more about the ADK 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 ADK agent is running on port 8000
|
||||
2. Your Google API key is set correctly
|
||||
3. Both servers started successfully
|
||||
|
||||
### Python Dependencies
|
||||
|
||||
If you encounter Python import errors:
|
||||
|
||||
```bash
|
||||
cd agent
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
@@ -0,0 +1,207 @@
|
||||
"""Shared State feature."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from typing import Dict, Optional
|
||||
|
||||
from ag_ui_adk import ADKAgent, add_adk_fastapi_endpoint
|
||||
from dotenv import load_dotenv
|
||||
from fastapi import FastAPI
|
||||
from google.adk.agents import LlmAgent
|
||||
from google.adk.agents.callback_context import CallbackContext
|
||||
from google.adk.models.llm_request import LlmRequest
|
||||
from google.adk.models.llm_response import LlmResponse
|
||||
from google.adk.tools import ToolContext
|
||||
from google.genai import types
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
load_dotenv()
|
||||
|
||||
|
||||
class ProverbsState(BaseModel):
|
||||
"""List of the proverbs being written."""
|
||||
|
||||
proverbs: list[str] = Field(
|
||||
default_factory=list,
|
||||
description="The list of already written proverbs",
|
||||
)
|
||||
|
||||
|
||||
def set_proverbs(tool_context: ToolContext, new_proverbs: list[str]) -> Dict[str, str]:
|
||||
"""
|
||||
Set the list of provers using the provided new list.
|
||||
|
||||
Args:
|
||||
"new_proverbs": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"description": "The new list of proverbs to maintain",
|
||||
}
|
||||
|
||||
Returns:
|
||||
Dict indicating success status and message
|
||||
"""
|
||||
try:
|
||||
# Put this into a state object just to confirm the shape
|
||||
new_state = {"proverbs": new_proverbs}
|
||||
tool_context.state["proverbs"] = new_state["proverbs"]
|
||||
return {"status": "success", "message": "Proverbs updated successfully"}
|
||||
|
||||
except Exception as e:
|
||||
return {"status": "error", "message": f"Error updating proverbs: {str(e)}"}
|
||||
|
||||
|
||||
def get_weather(tool_context: ToolContext, location: str) -> Dict[str, str]:
|
||||
"""Get the weather for a given location. Ensure location is fully spelled out."""
|
||||
return {"status": "success", "message": f"The weather in {location} is sunny."}
|
||||
|
||||
|
||||
def on_before_agent(callback_context: CallbackContext):
|
||||
"""
|
||||
Initialize proverbs state if it doesn't exist.
|
||||
"""
|
||||
|
||||
if "proverbs" not in callback_context.state:
|
||||
# Initialize with default recipe
|
||||
default_proverbs = []
|
||||
callback_context.state["proverbs"] = default_proverbs
|
||||
|
||||
return None
|
||||
|
||||
|
||||
# --- Define the Callback Function ---
|
||||
# modifying the agent's system prompt to incude the current state of the proverbs list
|
||||
def before_model_modifier(
|
||||
callback_context: CallbackContext, llm_request: LlmRequest
|
||||
) -> Optional[LlmResponse]:
|
||||
"""Inspects/modifies the LLM request or skips the call."""
|
||||
agent_name = callback_context.agent_name
|
||||
if agent_name == "ProverbsAgent":
|
||||
proverbs_json = "No proverbs yet"
|
||||
if (
|
||||
"proverbs" in callback_context.state
|
||||
and callback_context.state["proverbs"] is not None
|
||||
):
|
||||
try:
|
||||
proverbs_json = json.dumps(callback_context.state["proverbs"], indent=2)
|
||||
except Exception as e:
|
||||
proverbs_json = f"Error serializing proverbs: {str(e)}"
|
||||
# --- Modification Example ---
|
||||
# Add a prefix to the system instruction
|
||||
original_instruction = llm_request.config.system_instruction or types.Content(
|
||||
role="system", parts=[]
|
||||
)
|
||||
prefix = f"""You are a helpful assistant for maintaining a list of proverbs.
|
||||
This is the current state of the list of proverbs: {proverbs_json}
|
||||
When you modify the list of proverbs (wether to add, remove, or modify one or more proverbs), use the set_proverbs tool to update the list."""
|
||||
# Ensure system_instruction is Content and parts list exists
|
||||
if not isinstance(original_instruction, types.Content):
|
||||
# Handle case where it might be a string (though config expects Content)
|
||||
original_instruction = types.Content(
|
||||
role="system", parts=[types.Part(text=str(original_instruction))]
|
||||
)
|
||||
if not original_instruction.parts:
|
||||
original_instruction.parts = [types.Part(text="")]
|
||||
|
||||
# Modify the text of the first part
|
||||
if original_instruction.parts and len(original_instruction.parts) > 0:
|
||||
modified_text = prefix + (original_instruction.parts[0].text or "")
|
||||
original_instruction.parts[0].text = modified_text
|
||||
llm_request.config.system_instruction = original_instruction
|
||||
|
||||
return None
|
||||
|
||||
|
||||
# --- Define the Callback Function ---
|
||||
def simple_after_model_modifier(
|
||||
callback_context: CallbackContext, llm_response: LlmResponse
|
||||
) -> Optional[LlmResponse]:
|
||||
"""Stop the consecutive tool calling of the agent"""
|
||||
agent_name = callback_context.agent_name
|
||||
# --- Inspection ---
|
||||
if agent_name == "ProverbsAgent":
|
||||
if llm_response.content and llm_response.content.parts:
|
||||
# Assuming simple text response for this example
|
||||
if (
|
||||
llm_response.content.role == "model"
|
||||
and llm_response.content.parts[0].text
|
||||
):
|
||||
callback_context._invocation_context.end_invocation = True
|
||||
|
||||
elif llm_response.error_message:
|
||||
return None
|
||||
else:
|
||||
return None # Nothing to modify
|
||||
return None
|
||||
|
||||
|
||||
proverbs_agent = LlmAgent(
|
||||
name="ProverbsAgent",
|
||||
model="gemini-2.5-flash",
|
||||
instruction="""
|
||||
When a user asks you to do anything regarding proverbs, you MUST use the set_proverbs tool.
|
||||
|
||||
IMPORTANT RULES ABOUT PROVERBS AND THE SET_PROVERBS TOOL:
|
||||
1. Always use the set_proverbs tool for any proverbs-related requests
|
||||
2. Always pass the COMPLETE LIST of proverbs to the set_proverbs tool. If the list had 5 proverbs and you removed one, you must pass the complete list of 4 remaining proverbs.
|
||||
3. You can use existing proverbs if one is relevant to the user's request, but you can also create new proverbs as required.
|
||||
4. Be creative and helpful in generating complete, practical proverbs
|
||||
5. After using the tool, provide a brief summary of what you create, removed, or changed 7.
|
||||
|
||||
Examples of when to use the set_proverbs tool:
|
||||
- "Add a proverb about soap" → Use tool with an array containing the existing list of proverbs with the new proverb about soap at the end.
|
||||
- "Remove the first proverb" → Use tool with an array containing the all of the existing proverbs except the first one"
|
||||
- "Change any proverbs about cats to mention that they have 18 lives" → If no proverbs mention cats, do not use the tool. If one or more proverbs do mention cats, change them to mention cats having 18 lives, and use the tool with an array of all of the proverbs, including ones that were changed and ones that did not require changes.
|
||||
|
||||
Do your best to ensure proverbs plausibly make sense.
|
||||
|
||||
|
||||
IMPORTANT RULES ABOUT WEATHER AND THE GET_WEATHER TOOL:
|
||||
1. Only call the get_weather tool if the user asks you for the weather in a given location.
|
||||
2. If the user does not specify a location, you can use the location "Everywhere ever in the whole wide world"
|
||||
|
||||
Examples of when to use the get_weather tool:
|
||||
- "What's the weather today in Tokyo?" → Use the tool with the location "Tokyo"
|
||||
- "Whats the weather right now" → Use the location "Everywhere ever in the whole wide world"
|
||||
- Is it raining in London? → Use the tool with the location "London"
|
||||
""",
|
||||
tools=[set_proverbs, get_weather],
|
||||
before_agent_callback=on_before_agent,
|
||||
before_model_callback=before_model_modifier,
|
||||
after_model_callback=simple_after_model_modifier,
|
||||
)
|
||||
|
||||
# Create ADK middleware agent instance
|
||||
adk_proverbs_agent = ADKAgent(
|
||||
adk_agent=proverbs_agent,
|
||||
user_id="demo_user",
|
||||
session_timeout_seconds=3600,
|
||||
use_in_memory_services=True,
|
||||
)
|
||||
|
||||
# Create FastAPI app
|
||||
app = FastAPI(title="ADK Middleware Proverbs Agent")
|
||||
|
||||
# Add the ADK endpoint
|
||||
add_adk_fastapi_endpoint(app, adk_proverbs_agent, path="/")
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
async def health():
|
||||
return {"status": "ok"}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import os
|
||||
|
||||
import uvicorn
|
||||
|
||||
if not os.getenv("GOOGLE_API_KEY"):
|
||||
print("⚠️ Warning: GOOGLE_API_KEY environment variable not set!")
|
||||
print(" Set it with: export GOOGLE_API_KEY='your-key-here'")
|
||||
print(" Get a key from: https://makersuite.google.com/app/apikey")
|
||||
print()
|
||||
|
||||
port = int(os.getenv("PORT", 8000))
|
||||
uvicorn.run(app, host="0.0.0.0", port=port)
|
||||
@@ -0,0 +1,15 @@
|
||||
[project]
|
||||
name = "proverbs-agent"
|
||||
version = "0.1.0"
|
||||
description = "ADK Proverbs Agent with shared state"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = [
|
||||
"fastapi",
|
||||
"uvicorn[standard]",
|
||||
"python-dotenv",
|
||||
"pydantic",
|
||||
"google-adk",
|
||||
"google-genai",
|
||||
"ag-ui-adk==0.6.3",
|
||||
"ag-ui-protocol==0.1.18",
|
||||
]
|
||||
Generated
+2818
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,80 @@
|
||||
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:
|
||||
# google-genai SDK does not honor a custom endpoint the way the
|
||||
# OpenAI / Anthropic clients do, so aimock can't intercept calls
|
||||
# from the adk agent — the SDK hits generativelanguage.googleapis.com
|
||||
# directly. Pass a real key via CI secret (see .github/workflows/
|
||||
# test_smoke-starter.yml). Fall back to the placeholder for local
|
||||
# dev runs where the local harness may provide its own stub.
|
||||
- GOOGLE_API_KEY=${GOOGLE_API_KEY:-test-key-for-aimock}
|
||||
- GOOGLE_GENAI_USE_VERTEXAI=false
|
||||
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:-adk}
|
||||
- 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,16 @@
|
||||
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 ./
|
||||
|
||||
RUN uv sync
|
||||
|
||||
EXPOSE 8000
|
||||
|
||||
CMD ["uv", "run", "python", "main.py"]
|
||||
@@ -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('output')){c=c.replace('};',' output: \"standalone\",\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('ignoreBuildErrors')){c=c.replace('};',' typescript: { ignoreBuildErrors: true },\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
+43
@@ -0,0 +1,43 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
echo "========================================="
|
||||
echo "[entrypoint] Starting: Google ADK starter"
|
||||
echo "[entrypoint] Time: $(date -u)"
|
||||
echo "[entrypoint] PORT=${PORT:-not set}"
|
||||
echo "========================================="
|
||||
|
||||
# Check critical env vars
|
||||
if [ -z "$GOOGLE_API_KEY" ]; then
|
||||
echo "[entrypoint] WARNING: GOOGLE_API_KEY is not set! Agent will fail."
|
||||
else
|
||||
echo "[entrypoint] GOOGLE_API_KEY: set (${#GOOGLE_API_KEY} chars)"
|
||||
fi
|
||||
|
||||
# Start Python agent on port 8000 (override PORT which Railway sets to 3000)
|
||||
echo "[entrypoint] Starting ADK agent on port 8000..."
|
||||
cd /app/agent
|
||||
PORT=8000 python main.py 2>&1 | sed 's/^/[agent] /' &
|
||||
AGENT_PID=$!
|
||||
cd /app
|
||||
|
||||
sleep 2
|
||||
|
||||
if kill -0 $AGENT_PID 2>/dev/null; then
|
||||
echo "[entrypoint] ADK agent started (PID: $AGENT_PID)"
|
||||
else
|
||||
echo "[entrypoint] ERROR: ADK agent failed to start!"
|
||||
fi
|
||||
|
||||
# Start Next.js frontend
|
||||
echo "[entrypoint] Starting Next.js on port ${PORT:-3000}..."
|
||||
PORT=${PORT:-3000} npx next start --port ${PORT:-3000} 2>&1 | sed 's/^/[nextjs] /' &
|
||||
NEXTJS_PID=$!
|
||||
|
||||
echo "[entrypoint] Both processes running. Waiting..."
|
||||
|
||||
wait -n $AGENT_PID $NEXTJS_PID
|
||||
EXIT_CODE=$?
|
||||
echo "[entrypoint] A process exited with code $EXIT_CODE"
|
||||
kill $AGENT_PID $NEXTJS_PID 2>/dev/null || true
|
||||
exit $EXIT_CODE
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"fixtures": [
|
||||
{
|
||||
"match": { "userMessage": "Hello" },
|
||||
"response": {
|
||||
"content": "Hello! I'm the adk 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 GOOGLE_API_KEY to .env, (2) Restart with `pnpm dev`."
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
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). 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",
|
||||
},
|
||||
typescript: {
|
||||
// HttpAgent type mismatch with CopilotRuntime — pending upstream fix in @copilotkit/runtime
|
||||
ignoreBuildErrors: true,
|
||||
},
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
+18587
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"name": "adk-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",
|
||||
"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.577.0",
|
||||
"next": "16.1.1",
|
||||
"react": "^19.2.1",
|
||||
"react-dom": "^19.2.1",
|
||||
"shiki": "^3.19.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,9 @@
|
||||
@echo off
|
||||
REM Navigate to the agent directory
|
||||
cd /d %~dp0\..\agent
|
||||
|
||||
REM Activate the virtual environment
|
||||
call .venv\Scripts\activate.bat
|
||||
|
||||
REM Run the agent
|
||||
uv run main.py
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Navigate to the agent directory
|
||||
cd "$(dirname "$0")/../agent" || exit 1
|
||||
|
||||
# Activate the virtual environment
|
||||
source .venv/bin/activate
|
||||
|
||||
# Run the agent
|
||||
uv run main.py
|
||||
@@ -0,0 +1,5 @@
|
||||
@echo off
|
||||
REM Navigate to the agent directory
|
||||
cd /d "%~dp0\..\agent" || exit /b 1
|
||||
|
||||
uv sync
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Navigate to the agent directory
|
||||
cd "$(dirname "$0")/../agent" || exit 1
|
||||
|
||||
uv sync
|
||||
@@ -0,0 +1,42 @@
|
||||
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/",
|
||||
}),
|
||||
},
|
||||
// --- 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,37 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
:root {
|
||||
--background: #ffffff;
|
||||
--foreground: #171717;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--background: #0a0a0a;
|
||||
--foreground: #ededed;
|
||||
}
|
||||
}
|
||||
|
||||
/* adk's CopilotSidebar (CopilotKit's V2 chat) stays LIGHT regardless of the OS
|
||||
color scheme, so the threads layout is held light too — otherwise it would
|
||||
clash with the sidebar it sits beside. The SDK <CopilotThreadsDrawer> reads
|
||||
--foreground/--background by token inheritance; re-pinning them here keeps the
|
||||
panel readable without affecting the demo content. 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,147 @@
|
||||
"use client";
|
||||
|
||||
import { ProverbsCard } from "@/components/proverbs";
|
||||
import { WeatherCard } from "@/components/weather";
|
||||
import type { AgentState } from "@/lib/types";
|
||||
import {
|
||||
useAgent,
|
||||
useConfigureSuggestions,
|
||||
useFrontendTool,
|
||||
CopilotSidebar,
|
||||
CopilotChatConfigurationProvider,
|
||||
CopilotThreadsDrawer,
|
||||
} from "@copilotkit/react-core/v2";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { z } from "zod";
|
||||
|
||||
import styles from "./page.module.css";
|
||||
|
||||
// The agent key registered in the runtime route (`agents: { default: ... }`)
|
||||
// and the id passed to `useAgent({ agentId: "default" })` below.
|
||||
const AGENT_ID = "default";
|
||||
|
||||
export default function CopilotKitPage() {
|
||||
const [themeColor, setThemeColor] = useState("#6366f1");
|
||||
|
||||
// 🪁 Frontend Actions: https://docs.copilotkit.ai/adk/frontend-actions
|
||||
useFrontendTool({
|
||||
name: "setThemeColor",
|
||||
parameters: z.object({
|
||||
themeColor: z
|
||||
.string()
|
||||
.describe("The theme color to set. Make sure to pick nice colors."),
|
||||
}),
|
||||
handler: async ({ themeColor }) => {
|
||||
setThemeColor(themeColor);
|
||||
return `Changing theme color to ${themeColor}`;
|
||||
},
|
||||
});
|
||||
|
||||
// 🪁 Suggestions: https://docs.copilotkit.ai/adk/suggestions
|
||||
useConfigureSuggestions({
|
||||
suggestions: [
|
||||
{
|
||||
title: "Generative UI",
|
||||
message: "Get the weather in San Francisco.",
|
||||
},
|
||||
{
|
||||
title: "Frontend Tools",
|
||||
message: "Set the theme to green.",
|
||||
},
|
||||
{
|
||||
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?",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
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}
|
||||
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/adk/shared-state
|
||||
// V2: useAgent returns the agent; read agent.state and write via agent.setState.
|
||||
const { agent } = useAgent({ agentId: AGENT_ID });
|
||||
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/adk/generative-ui
|
||||
useFrontendTool({
|
||||
name: "get_weather",
|
||||
description: "Get the weather for a given location.",
|
||||
available: false,
|
||||
parameters: z.object({
|
||||
location: z.string(),
|
||||
}),
|
||||
render: ({ args }) => {
|
||||
return <WeatherCard location={args.location} themeColor={themeColor} />;
|
||||
},
|
||||
followUp: false,
|
||||
});
|
||||
|
||||
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,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