diff --git a/README.md b/README.md index 8462423..fb86002 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,6 @@ # ai-agent-management-platform WSO2 AI Agent Manager is an open control plane designed for enterprises to deploy, manage, and govern AI agents at scale. + +## Quick Start + +Get started quickly with our [Quick Start Guide](quick-start/QUICK_START.md) - get the platform running in minutes! diff --git a/quick-start/QUICK_START.md b/quick-start/QUICK_START.md new file mode 100644 index 0000000..a573cf0 --- /dev/null +++ b/quick-start/QUICK_START.md @@ -0,0 +1,108 @@ +# Quick Start Guide + +Get the Agent Management Platform running with a single command! + +### Prerequisites + +- **kubectl** configured to access your cluster +- **Helm** v3.8+ installed + +## 🚀 One-Command Installation + +**Complete setup including Kind cluster and OpenChoreo:** + +```bash +cd quick-start +./bootstrap.sh +``` + +**Time:** ~15-20 minutes +**Prerequisites:** Docker, kubectl, Helm, kind + +This installs everything you need: +- ✅ Kind cluster (local Kubernetes) +- ✅ OpenChoreo platform +- ✅ Agent Management Platform +- ✅ Full observability stack + +--- + +### Skip Specific Steps + +#### If you already have a kubernetes cluster + +```bash +./bootstrap.sh --skip-kind +``` + +#### Use existing OpenChoreo installation + +( OpenChoreo cluster (v0.3.2+) with Observability Plane installed ) + +```bash +./bootstrap.sh --skip-openchoreo +``` + +#### Platform only (assumes Kind + OpenChoreo exist) +```bash +./bootstrap.sh --skip-kind --skip-openchoreo +``` + +**Time:** ~5-8 minutes + +This installs the Agent Management Platform on your existing OpenChoreo cluster. + +## Access Your Platform + +After installation completes, your platform is automatically accessible at: + +- **Console**: http://localhost:3000 +- **API**: http://localhost:8080 +- **Traces Observer**: http://localhost:9098 +- **Data Prepper**: http://localhost:21893 + +## What's Included + +✅ Agent Management Platform +✅ Full observability stack with distributed tracing +✅ PostgreSQL database +✅ Web console +✅ Automatic port forwarding + +## Next Steps + +1. **Open the console**: `open http://localhost:3000` +2. **Deploy a sample agent**: See [sample agents](../runtime/sample-agents/) +3. **View traces**: Navigate to the Observability section in the console + +## Uninstall + +**Platform only:** +```bash +./uninstall.sh +``` + +**Complete cleanup (including Kind cluster):** +```bash +./uninstall.sh --force --delete-namespaces +kind delete cluster --name openchoreo-local +``` + +## Troubleshooting + +**Installation fails?** Run with verbose output: +```bash +./install.sh --verbose +``` + +**Services not accessible?** Check port forwarding: +```bash +kubectl get pods -n agent-management-platform +kubectl get pods -n openchoreo-observability-plane +``` + +For more help, see [Detailed Installation Guide](./README.md) or [Troubleshooting Guide](./TROUBLESHOOTING.md) + +## Advanced Options + +For advanced configuration options, custom values, and detailed documentation, see [README.md](./README.md) diff --git a/quick-start/README.md b/quick-start/README.md new file mode 100644 index 0000000..46388a8 --- /dev/null +++ b/quick-start/README.md @@ -0,0 +1,277 @@ +# Agent Management Platform - Detailed Installation Guide + +This directory contains installation scripts for the Agent Management Platform on existing OpenChoreo clusters. + +> **Quick Start**: For a simple 2-step installation, see [QUICK_START.md](QUICK_START.md) + +## Prerequisites + +- **OpenChoreo cluster (v0.3.2+)** with Observability Plane installed +- **kubectl** configured with access to the cluster +- **Helm** v3.8+ installed +- Sufficient permissions to create namespaces and deploy resources + +## What Gets Installed + +The installation includes: + +1. ✅ **Agent Management Platform** - Core platform with PostgreSQL, Agent Manager Service, and Console +2. ✅ **Observability Stack** - DataPrepper and Traces Observer (always included) +3. ⚪ **Build CI** - Workflow templates for building container images (optional) + +**Note**: Observability is a core component and is always installed, not optional. + +--- + +## Installation + +### Simple Installation (Recommended) + +```bash +./install.sh +``` + +This installs the complete platform with observability in the `agent-management-platform` namespace. + +**What it does:** +- ✅ Validates prerequisites (including OpenChoreo Observability Plane) +- ✅ Installs Agent Management Platform +- ✅ Installs Observability components (DataPrepper + Traces Observer) +- ✅ Automatically configures port forwarding for all 4 services + +**After installation, access at:** +- Console: http://localhost:3000 +- API: http://localhost:8080 +- Traces Observer: http://localhost:9098 +- Data Prepper: http://localhost:21893 + +### Installation with Custom Configuration + +```bash +./install.sh --config custom-values.yaml +``` + +### Verbose Installation (for debugging) + +```bash +./install.sh --verbose +``` + +### Installation without Auto Port-Forward + +```bash +./install.sh --no-port-forward +``` + +Then manually start port forwarding: +```bash +./port-forward.sh +``` + +## Installation Options + +| Option | Description | +|--------|-------------| +| `--verbose, -v` | Show detailed installation output | +| `--no-port-forward` | Skip automatic port forwarding | +| `--config FILE` | Use custom configuration file | +| `--help, -h` | Show help message | + +--- + +## Port Forwarding + +### Automatic (Default) + +Port forwarding starts automatically after installation for all 4 services: +- Console: 3000 +- Agent Manager API: 8080 +- Traces Observer: 9098 +- Data Prepper: 21893 + +### Manual Control + +```bash +# Start port forwarding +./port-forward.sh + +# Stop port forwarding +./stop-port-forward.sh +``` + +--- + +## Validation + +Installation includes built-in validation. To manually check the deployment: + +```bash +# Check pod status +kubectl get pods -n agent-management-platform +kubectl get pods -n openchoreo-observability-plane + +# Check services +kubectl get svc -n agent-management-platform +kubectl get svc -n openchoreo-observability-plane + +# Check Helm releases +helm list -n agent-management-platform +helm list -n openchoreo-observability-plane +``` + +--- + +## Uninstallation + +### Interactive Uninstall + +```bash +./uninstall.sh +``` + +### Force Uninstall (no confirmation) + +```bash +./uninstall.sh --force +``` + +### Complete Cleanup (including namespaces) + +```bash +./uninstall.sh --force --delete-namespaces +``` + +**Note**: The observability namespace (`openchoreo-observability-plane`) is shared with OpenChoreo and will not be deleted. + +## Uninstallation Options + +| Option | Description | +|--------|-------------| +| `--force, -f` | Skip confirmation prompts | +| `--delete-namespaces` | Delete Agent Management Platform namespace after uninstalling | +| `--help, -h` | Show help message | + +--- + +## Advanced Configuration + +### Custom Values File + +Create a custom values file (e.g., `my-values.yaml`): + +```yaml +agentManagerService: + replicaCount: 2 + resources: + requests: + memory: 512Mi + cpu: 500m + +console: + replicaCount: 2 + +postgresql: + auth: + password: "my-secure-password" +``` + +Then install: +```bash +./install.sh --config my-values.yaml +``` + +### Environment Variables + +You can override default namespaces: + +```bash +export AMP_NS=my-custom-namespace +export OBSERVABILITY_NS=my-observability-namespace +./install.sh +``` + +--- + +## Troubleshooting + +For common issues and solutions, see [TROUBLESHOOTING.md](TROUBLESHOOTING.md) + +### Quick Diagnostics + +```bash +# Check logs +kubectl logs -n agent-management-platform deployment/agent-manager-service +kubectl logs -n agent-management-platform deployment/console +kubectl logs -n openchoreo-observability-plane deployment/data-prepper + +# Check events +kubectl get events -n agent-management-platform --sort-by='.lastTimestamp' + +# Check Helm release status +helm status agent-management-platform -n agent-management-platform +helm status amp-observability-traces -n openchoreo-observability-plane +``` + +### Verbose Installation + +If installation fails, run with verbose mode to see detailed output: + +```bash +./install.sh --verbose +``` + +--- + +## Default Configuration + +### Namespaces +- Agent Management Platform: `agent-management-platform` +- Observability: `openchoreo-observability-plane` (shared with OpenChoreo) +- Build CI: `agent-build-ci` (optional) + +### Ports +- Console: 3000 +- Agent Manager API: 8080 +- Traces Observer: 9098 +- Data Prepper: 21893 + +### Helm Charts +Charts are pulled from GitHub Container Registry (GHCR): +- `ghcr.io/agent-mgt-platform/agent-management-platform:0.1.0` +- `ghcr.io/agent-mgt-platform/amp-observability-traces:0.1.1` +- `ghcr.io/agent-mgt-platform/agent-manager-build-ci-workflows:0.1.0` + +--- + +## Files in This Directory + +| File | Purpose | +|------|---------| +| `install.sh` | Main installation script (simplified) | +| `uninstall.sh` | Uninstallation script | +| `install-helpers.sh` | Helper functions for installation | +| `port-forward.sh` | Port forwarding for all services | +| `stop-port-forward.sh` | Stop port forwarding | +| `QUICK_START.md` | Ultra-simple 2-step guide | +| `README.md` | This detailed guide | +| `TROUBLESHOOTING.md` | Common issues and solutions | +| `example-values.yaml` | Example custom configuration | + +--- + +## Notes + +- The scripts are idempotent - running them multiple times will upgrade existing installations +- PostgreSQL is deployed as part of the Agent Management Platform chart +- Observability is always installed as a core component +- Default credentials are set in the values files - change them for production +- All scripts include proper error handling and logging +- Port forwarding runs in the background and can be stopped with `./stop-port-forward.sh` + +--- + +## See Also + +- [Quick Start Guide](QUICK_START.md) - Simple 2-step installation +- [Troubleshooting Guide](TROUBLESHOOTING.md) - Common issues and solutions +- [Main README](../README.md) - Project overview and architecture diff --git a/quick-start/bootstrap.sh b/quick-start/bootstrap.sh new file mode 100755 index 0000000..76a38d5 --- /dev/null +++ b/quick-start/bootstrap.sh @@ -0,0 +1,494 @@ +#!/usr/bin/env bash +set -eo pipefail + +# ============================================================================ +# Agent Management Platform - Complete Bootstrap Installation +# ============================================================================ +# This script provides a single-command installation that: +# 1. Creates a Kind cluster +# 2. Installs OpenChoreo +# 3. Installs Agent Management Platform +# +# Usage: +# ./bootstrap.sh # Full installation +# ./bootstrap.sh --minimal # Skip optional OpenChoreo components +# ./bootstrap.sh --verbose # Show detailed output +# ./bootstrap.sh --help # Show help +# ============================================================================ + +# Get the absolute path of the script directory +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# Source helper functions +source "${SCRIPT_DIR}/install-helpers.sh" + +# Configuration +VERBOSE="${VERBOSE:-false}" +SKIP_KIND="${SKIP_KIND:-false}" +SKIP_OPENCHOREO="${SKIP_OPENCHOREO:-false}" +AUTO_PORT_FORWARD="${AUTO_PORT_FORWARD:-true}" +MINIMAL_MODE="${MINIMAL_MODE:-false}" + +# Parse command line arguments +while [[ $# -gt 0 ]]; do + case $1 in + --verbose|-v) + VERBOSE=true + shift + ;; + --minimal|--core-only) + MINIMAL_MODE=true + shift + ;; + --skip-kind) + SKIP_KIND=true + shift + ;; + --skip-openchoreo) + SKIP_OPENCHOREO=true + shift + ;; + --no-port-forward) + AUTO_PORT_FORWARD=false + shift + ;; + --config) + if [[ -f "$2" ]]; then + AMP_HELM_ARGS+=("-f" "$2") + else + log_error "Config file not found: $2" + exit 1 + fi + shift 2 + ;; + --help|-h) + cat << EOF + +🚀 Agent Management Platform - Bootstrap Installation + +This script provides a complete one-command installation of: + • Kind cluster (Kubernetes in Docker) + • OpenChoreo platform + • Agent Management Platform with observability + +Usage: + $0 [OPTIONS] + +Options: + --verbose, -v Show detailed installation output + --minimal, --core-only Install only core OpenChoreo components (faster) + --skip-kind Skip Kind cluster creation (use existing cluster) + --skip-openchoreo Skip OpenChoreo installation (install platform only) + --no-port-forward Skip automatic port forwarding + --config FILE Use custom configuration file for platform + --help, -h Show this help message + +Examples: + $0 # Full installation (recommended) + $0 --verbose # Full installation with detailed output + $0 --minimal # Faster installation with core components only + $0 --skip-kind # Use existing Kind cluster + $0 --config custom.yaml # Installation with custom platform config + +Prerequisites: + • Docker (Docker Desktop or Colima) + • kubectl + • helm + • kind + +Installation Time: + • Full installation: ~15-20 minutes + • Minimal installation: ~10-12 minutes + +For more information: + • Quick Start Guide: ./QUICK_START.md + • Troubleshooting: ./TROUBLESHOOTING.md + • Documentation: https://github.com/wso2/agent-management-platform + +EOF + exit 0 + ;; + *) + log_error "Unknown option: $1" + echo "Use --help for usage information" + exit 1 + ;; + esac +done + +# ============================================================================ +# MAIN INSTALLATION FLOW +# ============================================================================ + +# Print header +echo "" +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" +echo "🚀 Agent Management Platform - Bootstrap" +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" +echo "" + +if [[ "$MINIMAL_MODE" == "true" ]]; then + echo "Mode: Minimal (core components only)" +else + echo "Mode: Full installation" +fi + +if [[ "$VERBOSE" == "true" ]]; then + echo "Verbosity: Detailed output enabled" +fi + +echo "" +echo "This will install:" +echo " ✓ Kind cluster (local Kubernetes)" +echo " ✓ OpenChoreo platform" +echo " ✓ Agent Management Platform" +echo " ✓ Observability stack" +echo "" + +if [[ "$VERBOSE" == "false" ]]; then + echo "💡 Tip: Use --verbose for detailed progress information" + echo "" +fi + +# Estimate installation time +if [[ "$MINIMAL_MODE" == "true" ]]; then + echo "⏱️ Estimated time: 10-12 minutes" +else + echo "⏱️ Estimated time: 15-20 minutes" +fi +echo "" + +# ============================================================================ +# STEP 1: VERIFY PREREQUISITES +# ============================================================================ + +if [[ "$VERBOSE" == "false" ]]; then + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo "Step 1/4: Verifying prerequisites..." + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo "" +else + log_info "Step 1/4: Verifying prerequisites..." + echo "" +fi + +if ! verify_openchoreo_prerequisites; then + echo "" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + log_error "Prerequisites check failed" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo "" + echo "Please install the missing prerequisites and try again." + echo "" + exit 1 +fi + +if [[ "$VERBOSE" == "false" ]]; then + echo "✓ All prerequisites verified" +else + log_success "Prerequisites check passed" +fi +echo "" + +# ============================================================================ +# STEP 2: SETUP KIND CLUSTER +# ============================================================================ + +if [[ "$SKIP_KIND" == "true" ]]; then + if [[ "$VERBOSE" == "false" ]]; then + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo "Step 2/4: Skipping Kind cluster setup (--skip-kind)" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo "" + else + log_info "Step 2/4: Skipping Kind cluster setup (--skip-kind)" + echo "" + fi +else + if [[ "$VERBOSE" == "false" ]]; then + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo "Step 2/4: Setting up Kind cluster..." + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo "" + echo "⏱️ This may take 2-3 minutes..." + echo "" + else + log_info "Step 2/4: Setting up Kind cluster..." + echo "" + fi + + if ! setup_kind_cluster "openchoreo-local" "${SCRIPT_DIR}/../deployments/kind-config.yaml"; then + echo "" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + log_error "Failed to setup Kind cluster" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo "" + echo "The Kind cluster could not be created." + echo "" + echo "Common solutions:" + echo " 1. Delete existing cluster: kind delete cluster --name openchoreo-local" + echo " 2. Restart Docker" + echo " 3. Check Docker has sufficient resources (4GB+ RAM recommended)" + echo "" + echo "For more help, see: ./TROUBLESHOOTING.md" + echo "" + exit 1 + fi + + if [[ "$VERBOSE" == "false" ]]; then + echo "✓ Kind cluster ready" + fi + echo "" +fi + +# ============================================================================ +# STEP 3: INSTALL OPENCHOREO +# ============================================================================ + +if [[ "$SKIP_OPENCHOREO" == "true" ]]; then + if [[ "$VERBOSE" == "false" ]]; then + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo "Step 3/4: Skipping OpenChoreo installation (--skip-openchoreo)" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo "" + else + log_info "Step 3/4: Skipping OpenChoreo installation (--skip-openchoreo)" + echo "" + fi +else + if [[ "$VERBOSE" == "false" ]]; then + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo "Step 3/4: Installing OpenChoreo..." + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo "" + if [[ "$MINIMAL_MODE" == "true" ]]; then + echo "⏱️ This may take 10-12 minutes (core components only)..." + else + echo "⏱️ This may take 12-15 minutes (full installation)..." + fi + echo "" + echo "Installing components:" + echo " • Cilium CNI" + echo " • OpenChoreo Control Plane" + echo " • OpenChoreo Data Plane" + echo " • OpenChoreo Observability Plane" + echo "" + else + log_info "Step 3/4: Installing OpenChoreo..." + echo "" + fi + + # Install core components + if ! install_openchoreo_core; then + echo "" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + log_error "Failed to install OpenChoreo" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo "" + echo "OpenChoreo installation failed." + echo "" + echo "Troubleshooting steps:" + echo " 1. Check cluster status: kubectl get nodes" + echo " 2. Check pod status: kubectl get pods --all-namespaces" + echo " 3. View logs: kubectl logs -n " + echo " 4. Ensure Docker has sufficient resources (4GB+ RAM)" + echo "" + echo "To clean up and retry:" + echo " ./uninstall.sh" + echo " ./bootstrap.sh" + echo "" + echo "For more help, see: ./TROUBLESHOOTING.md" + echo "" + exit 1 + fi + + if [[ "$VERBOSE" == "false" ]]; then + echo "✓ OpenChoreo installed successfully" + fi + echo "" +fi + +# ============================================================================ +# STEP 4: INSTALL AGENT MANAGEMENT PLATFORM +# ============================================================================ + +if [[ "$VERBOSE" == "false" ]]; then + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo "Step 4/4: Installing Agent Management Platform..." + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo "" + echo "⏱️ This may take 5-8 minutes..." + echo "" +else + log_info "Step 4/4: Installing Agent Management Platform..." + echo "" +fi + +# Verify OpenChoreo Observability Plane is available +if ! kubectl get namespace openchoreo-observability-plane >/dev/null 2>&1; then + echo "" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + log_error "OpenChoreo Observability Plane not found" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo "" + echo "The Agent Management Platform requires OpenChoreo Observability Plane." + echo "" + echo "This should have been installed in Step 3." + echo "Please run the full bootstrap without --skip-openchoreo" + echo "" + exit 1 +fi + +# Install platform components +if [[ "$VERBOSE" == "false" ]]; then + echo "Installing components:" + echo " • PostgreSQL database" + echo " • Agent Manager Service" + echo " • Console (Web UI)" + echo " • Observability stack" + echo "" +fi + +if ! install_agent_management_platform_silent; then + echo "" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + log_error "Failed to install Agent Management Platform" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo "" + echo "Platform installation failed." + echo "" + echo "Troubleshooting steps:" + echo " 1. Check pod status: kubectl get pods -n agent-management-platform" + echo " 2. View logs: kubectl logs -n agent-management-platform " + echo " 3. Check Helm release: helm list -n agent-management-platform" + echo "" + echo "To retry platform installation only:" + echo " ./install.sh" + echo "" + echo "For more help, see: ./TROUBLESHOOTING.md" + echo "" + exit 1 +fi + +if [[ "$VERBOSE" == "false" ]]; then + echo "✓ Platform components installed" + echo "" +fi + +# Install observability +if [[ "$VERBOSE" == "false" ]]; then + echo "Installing observability stack..." + echo "" +fi + +if ! install_observability_dataprepper_silent; then + echo "" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + log_error "Failed to install observability stack" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo "" + echo "Observability installation failed." + echo "" + echo "The platform is installed but observability features may not work." + echo "" + echo "Troubleshooting steps:" + echo " 1. Check pod status: kubectl get pods -n openchoreo-observability-plane" + echo " 2. View logs: kubectl logs -n openchoreo-observability-plane " + echo "" + exit 1 +fi + +if [[ "$VERBOSE" == "false" ]]; then + echo "✓ Observability stack ready" + echo "" +fi + +# ============================================================================ +# STEP 5: START PORT FORWARDING +# ============================================================================ + +if [[ "${AUTO_PORT_FORWARD}" == "true" ]]; then + if [[ "$VERBOSE" == "false" ]]; then + echo "Starting port forwarding..." + echo "" + else + log_info "Starting port forwarding in background..." + fi + + PORT_FORWARD_SCRIPT="${SCRIPT_DIR}/port-forward.sh" + if [[ -f "$PORT_FORWARD_SCRIPT" ]]; then + # Run port-forward script in background + bash "$PORT_FORWARD_SCRIPT" > /dev/null 2>&1 & + PORT_FORWARD_PID=$! + + # Save PID to file for easy cleanup + echo "$PORT_FORWARD_PID" > "${SCRIPT_DIR}/.port-forward.pid" + + # Give port forwarding a moment to start + sleep 3 + + if [[ "$VERBOSE" == "false" ]]; then + echo "✓ Port forwarding active" + else + log_success "Port forwarding started (PID: $PORT_FORWARD_PID)" + fi + else + if [[ "$VERBOSE" == "true" ]]; then + log_warning "Port forward script not found at: $PORT_FORWARD_SCRIPT" + fi + fi + echo "" +fi + +# ============================================================================ +# SUCCESS MESSAGE +# ============================================================================ + +echo "" +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" +echo "✅ Installation Complete!" +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" +echo "" +echo "🌐 Access your platform:" +echo "" +echo " Console: http://localhost:3000" +echo " API: http://localhost:8080" +echo " Traces Observer: http://localhost:9098" +echo " Data Prepper: http://localhost:21893" +echo "" +echo "🚀 Next steps:" +echo "" +echo " 1. Open console: open http://localhost:3000" +echo " 2. Deploy an agent: cd ../runtime/sample-agents/python-agent" +echo " 3. View traces in the console" +echo "" + + +if [[ "${AUTO_PORT_FORWARD}" == "true" ]]; then + echo "💡 Port forwarding is running in the background" + echo " To stop: ./stop-port-forward.sh" + echo "" +fi + +echo "🛑 To uninstall everything:" +echo " ./uninstall.sh" +echo "" + +if [[ "$VERBOSE" == "true" ]]; then + echo "" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + log_info "Installation Summary" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo "" + log_info "Cluster: $(kubectl config current-context)" + log_info "Platform Namespace: $AMP_NS" + log_info "Observability Namespace: $OBSERVABILITY_NS" + echo "" + log_info "Deployed Components:" + echo "" + kubectl get pods -n "$AMP_NS" 2>/dev/null || true + echo "" + kubectl get pods -n "$OBSERVABILITY_NS" 2>/dev/null || true + echo "" +fi + diff --git a/quick-start/example-values.yaml b/quick-start/example-values.yaml new file mode 100644 index 0000000..8c4d747 --- /dev/null +++ b/quick-start/example-values.yaml @@ -0,0 +1,108 @@ +# Example custom values file for Agent Management Platform +# Copy this file and modify as needed for your environment + +# PostgreSQL Configuration +postgresql: + enabled: true + auth: + password: "changeme-secure-password" + primary: + persistence: + size: 20Gi + resources: + requests: + memory: 512Mi + cpu: 500m + limits: + memory: 1Gi + cpu: 1000m + +# Agent Manager Service Configuration +agentManagerService: + enabled: true + replicaCount: 2 + + image: + repository: ghcr.io/agent-mgt-platform/agent-manager-service + tag: "v1.0.0" + pullPolicy: IfNotPresent + + resources: + requests: + memory: 512Mi + cpu: 500m + limits: + memory: 1Gi + cpu: 1000m + + autoscaling: + enabled: true + minReplicas: 2 + maxReplicas: 10 + targetCPUUtilizationPercentage: 80 + + config: + logLevel: "INFO" + corsAllowedOrigin: "*" # Change to specific domain in production + apiKey: + value: "your-secure-api-key-here" # CHANGE THIS! + +# Console Configuration +console: + enabled: true + replicaCount: 2 + + image: + repository: ghcr.io/agent-mgt-platform/agent-manager-console + tag: "v1.0.0" + pullPolicy: IfNotPresent + + resources: + requests: + memory: 256Mi + cpu: 200m + limits: + memory: 512Mi + cpu: 400m + + autoscaling: + enabled: true + minReplicas: 2 + maxReplicas: 5 + targetCPUUtilizationPercentage: 80 + + config: + disableAuth: "false" + # Configure authentication + auth: + clientId: "your-client-id" + baseUrl: "https://your-auth-provider.com" + signInRedirectURL: "https://your-domain.com/callback" + signOutRedirectURL: "https://your-domain.com" + +# Ingress Configuration +ingress: + enabled: true + className: "nginx" + hosts: + - host: agent-manager.yourdomain.com + paths: + - path: / + pathType: Prefix + service: console + - path: /api(/|$)(.*) + pathType: ImplementationSpecific + service: agent-manager-service + tls: + - secretName: agent-manager-tls + hosts: + - agent-manager.yourdomain.com + +# RBAC Configuration +rbac: + create: true + # Full permissions for agent management operations + rules: + - apiGroups: ["*"] + resources: ["*"] + verbs: ["*"] diff --git a/quick-start/install-helpers.sh b/quick-start/install-helpers.sh new file mode 100755 index 0000000..62255c6 --- /dev/null +++ b/quick-start/install-helpers.sh @@ -0,0 +1,779 @@ +#!/usr/bin/env bash + +# Helper functions for Agent Management Platform installation +# Assumes cluster is already set up and configured + +set -eo pipefail + +# Color codes for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[0;33m' +BLUE='\033[0;34m' +RESET='\033[0m' + +# Configuration variables +# Remote Helm chart repository and versions +HELM_CHART_REGISTRY="${HELM_CHART_REGISTRY:-ghcr.io/agent-mgt-platform}" +AMP_CHART_VERSION="${AMP_CHART_VERSION:-0.1.0}" +BUILD_CI_CHART_VERSION="${BUILD_CI_CHART_VERSION:-0.1.0}" +OBSERVABILITY_CHART_VERSION="${OBSERVABILITY_CHART_VERSION:-0.1.1}" + +# Chart names +AMP_CHART_NAME="agent-management-platform" +BUILD_CI_CHART_NAME="agent-manager-build-ci-workflows" +OBSERVABILITY_CHART_NAME="amp-observability-traces" + +# Default namespace definitions (can be overridden via environment variables) +AMP_NS="${AMP_NS:-agent-management-platform}" +BUILD_CI_NS="${BUILD_CI_NS:-openchoreo-build-plane}" +OBSERVABILITY_NS="${OBSERVABILITY_NS:-openchoreo-observability-plane}" + +# Logging functions +log_info() { + echo -e "${BLUE}[INFO]${RESET} $1" +} + +log_success() { + echo -e "${GREEN}[SUCCESS]${RESET} $1" +} + +log_warning() { + echo -e "${YELLOW}[WARNING]${RESET} $1" +} + +log_error() { + echo -e "${RED}[ERROR]${RESET} $1" +} + +# Check if a command exists +command_exists() { + command -v "$1" >/dev/null 2>&1 +} + +# Check if namespace exists +namespace_exists() { + local namespace="$1" + kubectl get namespace "$namespace" >/dev/null 2>&1 +} + +# Check if helm release exists +helm_release_exists() { + local release="$1" + local namespace="$2" + helm list -n "$namespace" --short 2>/dev/null | grep -q "^${release}$" +} + +# Wait for pods to be ready in a namespace +wait_for_pods() { + local namespace="$1" + local timeout="${2:-300}" # 5 minutes default + local label_selector="${3:-}" + + log_info "Waiting for pods in namespace '$namespace' to be ready..." + + local selector_flag="" + if [[ -n "$label_selector" ]]; then + selector_flag="-l $label_selector" + fi + + if ! timeout "$timeout" bash -c " + while true; do + pods=\$(kubectl get pods -n '$namespace' $selector_flag --no-headers 2>/dev/null || true) + if [[ -z \"\$pods\" ]]; then + echo 'No pods found yet, waiting...' + sleep 5 + continue + fi + if echo \"\$pods\" | grep -v 'Running\|Completed' | grep -q .; then + echo 'Waiting for pods to be ready...' + sleep 5 + else + echo 'All pods are ready!' + break + fi + done + "; then + log_error "Timeout waiting for pods in namespace '$namespace'" + kubectl get pods -n "$namespace" $selector_flag 2>/dev/null || true + return 1 + fi + + log_success "All pods in namespace '$namespace' are ready" +} + +# Wait for a deployment to be ready +wait_for_deployment() { + local deployment="$1" + local namespace="$2" + local timeout="${3:-300}" + + log_info "Waiting for deployment '$deployment' in namespace '$namespace' to be ready..." + + if kubectl wait --for=condition=available --timeout="${timeout}s" \ + deployment/"$deployment" -n "$namespace" 2>/dev/null; then + log_success "Deployment '$deployment' is ready" + return 0 + else + log_warning "Deployment '$deployment' may still be starting" + kubectl get deployment "$deployment" -n "$namespace" 2>/dev/null || true + return 0 + fi +} + +# Patch APIClass for CORS configuration +patch_apiclass_cors() { + local apiclass_name="${1:-default-with-cors}" + local namespace="${2:-default}" + local origin="${3:-http://localhost:3000}" + + log_info "Patching APIClass '$apiclass_name' in namespace '$namespace' to allow CORS origin '$origin'..." + + # Check if APIClass exists + if ! kubectl get apiclass "$apiclass_name" -n "$namespace" >/dev/null 2>&1; then + log_warning "APIClass '$apiclass_name' not found in namespace '$namespace', skipping CORS patch" + return 0 + fi + + # Apply the CORS patch + if kubectl patch apiclass "$apiclass_name" -n "$namespace" --type json \ + -p "[{\"op\":\"add\",\"path\":\"/spec/restPolicy/defaults/cors/allowOrigins/-\",\"value\":\"$origin\"}]" 2>/dev/null; then + log_success "APIClass '$apiclass_name' patched successfully with CORS origin '$origin'" + else + # If the patch fails (e.g., origin already exists), try to verify it exists + if kubectl get apiclass "$apiclass_name" -n "$namespace" -o jsonpath='{.spec.restPolicy.defaults.cors.allowOrigins}' 2>/dev/null | grep -q "$origin"; then + log_info "CORS origin '$origin' already exists in APIClass '$apiclass_name'" + else + log_warning "Failed to patch APIClass '$apiclass_name'. This may be expected if CORS is already configured." + fi + fi +} + +# Install a remote OCI helm chart with idempotency +install_remote_helm_chart() { + local release_name="$1" + local chart_ref="$2" # Full OCI reference like oci://ghcr.io/org/chart:version + local namespace="$3" + local create_namespace="${4:-true}" + local wait_flag="${5:-false}" + local timeout="${6:-1800}" + shift 6 + local additional_args=("$@") + + log_info "Installing Helm chart '$chart_ref' as release '$release_name' in namespace '$namespace'..." + + # Check if release already exists + if helm_release_exists "$release_name" "$namespace"; then + log_warning "Helm release '$release_name' already exists in namespace '$namespace'" + + # Try to upgrade the release + local upgrade_args=( + "upgrade" "$release_name" "$chart_ref" + "--namespace" "$namespace" + "--timeout" "${timeout}s" + ) + + if [[ "$wait_flag" == "true" ]]; then + upgrade_args+=("--wait") + fi + + upgrade_args+=("${additional_args[@]}") + + log_info "Upgrading release '$release_name' from '$chart_ref'..." + if helm "${upgrade_args[@]}"; then + log_success "Helm release '$release_name' upgraded successfully" + else + log_error "Failed to upgrade Helm release '$release_name'" + return 1 + fi + else + # Create namespace if needed and doesn't exist + if [[ "$create_namespace" == "true" ]] && ! namespace_exists "$namespace"; then + log_info "Creating namespace '$namespace'..." + kubectl create namespace "$namespace" + fi + + # Install new release + local install_args=( + "install" "$release_name" "$chart_ref" + "--namespace" "$namespace" + "--timeout" "${timeout}s" + ) + + if [[ "$wait_flag" == "true" ]]; then + install_args+=("--wait") + fi + + install_args+=("${additional_args[@]}") + + log_info "Installing release '$release_name' from '$chart_ref' (timeout: ${timeout}s)" + log_info "This may take several minutes..." + + if helm "${install_args[@]}"; then + log_success "Helm release '$release_name' installed successfully" + else + log_error "Failed to install Helm release '$release_name'" + return 1 + fi + fi +} + +# Install Agent Management Platform +install_agent_management_platform() { + log_info "Installing Agent Management Platform..." + + local chart_ref="oci://${HELM_CHART_REGISTRY}/${AMP_CHART_NAME}" + local chart_version="${AMP_CHART_VERSION}" + + log_info "Using chart: $chart_ref:$chart_version" + + # Start a background process to monitor pod status + ( + sleep 10 # Give it time to start creating resources + while true; do + log_info "Current pod status in namespace $AMP_NS:" + kubectl get pods -n "$AMP_NS" 2>/dev/null || echo "No pods yet..." + sleep 15 + done + ) & + local monitor_pid=$! + + # Add version to helm args + local version_args=("--version" "$chart_version") + + install_remote_helm_chart "agent-management-platform" "$chart_ref" "$AMP_NS" "true" "false" "1800" \ + "${version_args[@]}" "${AMP_HELM_ARGS[@]}" + + # Stop the monitoring process + kill $monitor_pid 2>/dev/null || true + + # Wait for PostgreSQL to be ready + log_info "Waiting for PostgreSQL to be ready..." + wait_for_deployment "agent-management-platform-postgresql" "$AMP_NS" 600 + + # Wait for agent manager service to be ready + log_info "Waiting for Agent Manager Service to be ready..." + wait_for_deployment "agent-manager-service" "$AMP_NS" 600 + + # Wait for console to be ready + log_info "Waiting for Console to be ready..." + wait_for_deployment "console" "$AMP_NS" 600 + + # Patch APIClass for CORS configuration + local apiclass_name="${APICLASS_NAME:-default-with-cors}" + local apiclass_ns="${APICLASS_NAMESPACE:-default}" + local cors_origin="${CORS_ORIGIN:-http://localhost:3000}" + patch_apiclass_cors "$apiclass_name" "$apiclass_ns" "$cors_origin" +} + +# Install Build CI +install_build_ci() { + log_info "Installing Build CI Workflows..." + + local chart_ref="oci://${HELM_CHART_REGISTRY}/${BUILD_CI_CHART_NAME}" + local chart_version="${BUILD_CI_CHART_VERSION}" + + log_info "Using chart: $chart_ref:$chart_version" + + # Add version to helm args + local version_args=("--version" "$chart_version") + + install_remote_helm_chart "agent-manager-build-ci" "$chart_ref" "$BUILD_CI_NS" "true" "false" "1800" \ + "${version_args[@]}" "${BUILD_CI_HELM_ARGS[@]}" + + log_success "Build CI Workflows installed successfully" +} + +# Install Observability DataPrepper +install_observability_dataprepper() { + log_info "Installing Observability DataPrepper..." + + local chart_ref="oci://${HELM_CHART_REGISTRY}/${OBSERVABILITY_CHART_NAME}" + local chart_version="${OBSERVABILITY_CHART_VERSION}" + + log_info "Using chart: $chart_ref:$chart_version" + + # Add version to helm args + local version_args=("--version" "$chart_version") + + install_remote_helm_chart "amp-observability-traces" "$chart_ref" "$OBSERVABILITY_NS" "true" "false" "1800" \ + "${version_args[@]}" "${OBSERVABILITY_HELM_ARGS[@]}" + + # Wait for data-prepper to be ready + log_info "Waiting for DataPrepper to be ready..." + wait_for_deployment "data-prepper" "$OBSERVABILITY_NS" 600 + + # Wait for traces-observer if enabled + if kubectl get deployment traces-observer-service -n "$OBSERVABILITY_NS" >/dev/null 2>&1; then + log_info "Waiting for Traces Observer Service to be ready..." + wait_for_deployment "traces-observer-service" "$OBSERVABILITY_NS" 600 + fi +} + +# Silent version for simple installer +install_observability_dataprepper_silent() { + local chart_ref="oci://${HELM_CHART_REGISTRY}/${OBSERVABILITY_CHART_NAME}" + local chart_version="${OBSERVABILITY_CHART_VERSION}" + local version_args=("--version" "$chart_version") + + install_remote_helm_chart "amp-observability-traces" "$chart_ref" "$OBSERVABILITY_NS" "true" "false" "1800" \ + "${version_args[@]}" "${OBSERVABILITY_HELM_ARGS[@]}" >/dev/null 2>&1 || return 1 + + wait_for_deployment "data-prepper" "$OBSERVABILITY_NS" 600 >/dev/null 2>&1 || return 1 + + if kubectl get deployment traces-observer-service -n "$OBSERVABILITY_NS" >/dev/null 2>&1; then + wait_for_deployment "traces-observer-service" "$OBSERVABILITY_NS" 600 >/dev/null 2>&1 || return 1 + fi + + return 0 +} + +# Silent version of AMP installation +install_agent_management_platform_silent() { + local chart_ref="oci://${HELM_CHART_REGISTRY}/${AMP_CHART_NAME}" + local chart_version="${AMP_CHART_VERSION}" + local version_args=("--version" "$chart_version") + + install_remote_helm_chart "agent-management-platform" "$chart_ref" "$AMP_NS" "true" "false" "1800" \ + "${version_args[@]}" "${AMP_HELM_ARGS[@]}" >/dev/null 2>&1 || return 1 + + wait_for_deployment "agent-management-platform-postgresql" "$AMP_NS" 600 >/dev/null 2>&1 || return 1 + wait_for_deployment "agent-manager-service" "$AMP_NS" 600 >/dev/null 2>&1 || return 1 + wait_for_deployment "console" "$AMP_NS" 600 >/dev/null 2>&1 || return 1 + + # Patch APIClass for CORS configuration + local apiclass_name="${APICLASS_NAME:-default-with-cors}" + local apiclass_ns="${APICLASS_NAMESPACE:-default}" + local cors_origin="${CORS_ORIGIN:-http://localhost:3000}" + patch_apiclass_cors "$apiclass_name" "$apiclass_ns" "$cors_origin" >/dev/null 2>&1 || true + + return 0 +} + +# Silent prerequisite verification +verify_prerequisites_silent() { + command_exists kubectl || return 1 + command_exists helm || return 1 + kubectl cluster-info >/dev/null 2>&1 || return 1 + + # Check for OpenChoreo Observability Plane (required) + if ! kubectl get namespace openchoreo-observability-plane >/dev/null 2>&1; then + echo "" + echo "❌ OpenChoreo Observability Plane not found" + echo "" + echo "The Agent Management Platform requires OpenChoreo Observability Plane." + echo "" + echo "Please install it first:" + echo " helm install observability-plane oci://ghcr.io/openchoreo/helm-charts/openchoreo-observability-plane \\" + echo " --version 0.3.2 \\" + echo " --namespace openchoreo-observability-plane \\" + echo " --create-namespace" + echo "" + echo "Documentation: https://openchoreo.dev/docs/v0.3.x/observability/" + echo "" + return 1 + fi + + # Verify OpenSearch is accessible + if ! kubectl get pods -n openchoreo-observability-plane -l app=opensearch >/dev/null 2>&1; then + echo "" + echo "⚠️ Warning: OpenSearch pods not found in observability plane" + echo " Installation may fail without OpenSearch" + echo "" + fi + + return 0 +} + +# Verify prerequisites +verify_prerequisites() { + log_info "Verifying prerequisites..." + + local missing_tools=() + + if ! command_exists kubectl; then + missing_tools+=("kubectl") + fi + + if ! command_exists helm; then + missing_tools+=("helm") + fi + + if [[ ${#missing_tools[@]} -gt 0 ]]; then + log_error "Missing required tools: ${missing_tools[*]}" + return 1 + fi + + # Check if kubectl can connect to a cluster + if ! kubectl cluster-info >/dev/null 2>&1; then + log_error "kubectl cannot connect to a cluster. Please ensure KUBECONFIG is set correctly." + return 1 + fi + + log_success "All prerequisites verified" +} + +# Print installation summary +print_installation_summary() { + log_success "Agent Management Platform installation completed successfully!" + echo "" + log_info "Installation Summary:" + log_info " Cluster: $(kubectl config current-context)" + log_info " Agent Management Platform Namespace: $AMP_NS" + log_info " Build CI Namespace: $BUILD_CI_NS" + log_info " Observability Namespace: $OBSERVABILITY_NS" + echo "" + log_info "Deployed Components:" + kubectl get pods -n "$AMP_NS" 2>/dev/null || true + echo "" + log_info "To access the console, run:" + log_info " kubectl port-forward -n $AMP_NS svc/console 8080:80" + log_info " Then open: http://localhost:8080" + echo "" + log_info "To access the agent manager API, run:" + log_info " kubectl port-forward -n $AMP_NS svc/agent-manager-service 8081:8080" + log_info " API endpoint: http://localhost:8081" +} + +# Clean up function +cleanup() { + log_info "Cleanup complete" +} + +# Register cleanup function +trap cleanup EXIT + +# ============================================================================ +# KIND CLUSTER SETUP FUNCTIONS +# ============================================================================ + +# Check if Docker is running +verify_docker_running() { + if ! docker info >/dev/null 2>&1; then + log_error "Docker is not running" + echo "" + echo " The installation requires Docker to be running." + echo "" + echo " → Start Docker Desktop, or" + echo " → Start Colima: colima start" + echo "" + echo " Then run this script again." + echo "" + return 1 + fi + return 0 +} + +# Check if Kind is installed +verify_kind_installed() { + if ! command_exists kind; then + log_error "Kind is not installed" + echo "" + echo " Kind (Kubernetes in Docker) is required for local installation." + echo "" + echo " Install Kind:" + echo " → macOS: brew install kind" + echo " → Linux: curl -Lo ./kind https://kind.sigs.k8s.io/dl/latest/kind-linux-amd64" + echo " chmod +x ./kind && sudo mv ./kind /usr/local/bin/kind" + echo "" + echo " Documentation: https://kind.sigs.k8s.io/docs/user/quick-start/" + echo "" + return 1 + fi + return 0 +} + +# Setup Kind cluster +setup_kind_cluster() { + local cluster_name="${1:-openchoreo-local}" + local config_file="${2:-./kind-config.yaml}" + + log_info "Setting up Kind cluster '$cluster_name'..." + + # Check if cluster already exists + if kind get clusters 2>/dev/null | grep -q "^${cluster_name}$"; then + log_warning "Kind cluster '$cluster_name' already exists" + + # Verify cluster is accessible + if kubectl cluster-info --context "kind-${cluster_name}" >/dev/null 2>&1; then + log_success "Using existing Kind cluster '$cluster_name'" + return 0 + else + log_error "Cluster exists but is not accessible. Please delete it first:" + echo " kind delete cluster --name $cluster_name" + return 1 + fi + fi + + # Create shared directory for OpenChoreo + log_info "Creating shared directory for OpenChoreo..." + mkdir -p /tmp/kind-shared + + # Check if config file exists + if [[ ! -f "$config_file" ]]; then + log_error "Kind configuration file not found: $config_file" + return 1 + fi + + # Create Kind cluster + log_info "Creating Kind cluster (this may take 2-3 minutes)..." + if kind create cluster --config "$config_file" 2>&1 | tee /tmp/kind-create.log; then + log_success "Kind cluster created successfully" + else + log_error "Failed to create Kind cluster" + echo "" + echo " Common causes:" + echo " • Port 6443 already in use" + echo " • Insufficient Docker resources" + echo " • Previous cluster not fully deleted" + echo "" + echo " Try:" + echo " 1. Delete any existing cluster: kind delete cluster --name $cluster_name" + echo " 2. Restart Docker" + echo " 3. Run this script again" + echo "" + return 1 + fi + + # Wait for cluster to be ready + log_info "Waiting for cluster nodes to be ready..." + if ! wait_for_kind_cluster_ready "$cluster_name"; then + log_error "Cluster nodes did not become ready in time" + return 1 + fi + + log_success "Kind cluster is ready" + return 0 +} + +# Wait for Kind cluster to be ready +wait_for_kind_cluster_ready() { + local cluster_name="${1:-openchoreo-local}" + local timeout=120 + local elapsed=0 + + while [ $elapsed -lt $timeout ]; do + if kubectl get nodes --context "kind-${cluster_name}" >/dev/null 2>&1; then + if kubectl wait --for=condition=Ready nodes --all --timeout=10s --context "kind-${cluster_name}" >/dev/null 2>&1; then + return 0 + fi + fi + sleep 5 + elapsed=$((elapsed + 5)) + done + + return 1 +} + +# ============================================================================ +# OPENCHOREO INSTALLATION FUNCTIONS +# ============================================================================ + +# OpenChoreo configuration +OPENCHOREO_VERSION="${OPENCHOREO_VERSION:-0.3.2}" +OPENCHOREO_REGISTRY="oci://ghcr.io/openchoreo/helm-charts" + +# Install OpenChoreo Cilium CNI +install_openchoreo_cilium() { + log_info "Installing Cilium CNI..." + + if helm status cilium -n cilium >/dev/null 2>&1; then + log_warning "Cilium already installed, skipping..." + return 0 + fi + + install_remote_helm_chart "cilium" \ + "${OPENCHOREO_REGISTRY}/cilium" \ + "cilium" \ + "true" \ + "true" \ + "300" \ + "--version" "$OPENCHOREO_VERSION" + + log_info "Waiting for Cilium pods to be ready..." + kubectl wait --for=condition=Ready pod -l k8s-app=cilium -n cilium --timeout=300s 2>&1 | grep -v "no matching resources" || true + + log_success "Cilium CNI ready" + return 0 +} + +# Install OpenChoreo Control Plane +install_openchoreo_control_plane() { + log_info "Installing OpenChoreo Control Plane (this may take up to 10 minutes)..." + + if helm status control-plane -n openchoreo-control-plane >/dev/null 2>&1; then + log_warning "Control Plane already installed, skipping..." + return 0 + fi + + install_remote_helm_chart "control-plane" \ + "${OPENCHOREO_REGISTRY}/openchoreo-control-plane" \ + "openchoreo-control-plane" \ + "true" \ + "false" \ + "600" \ + "--version" "$OPENCHOREO_VERSION" + + log_info "Waiting for Control Plane pods to be ready..." + if ! kubectl wait --for=condition=Ready pod --all -n openchoreo-control-plane --timeout=600s 2>/dev/null; then + log_warning "Some Control Plane pods may still be starting (non-fatal)" + fi + + log_success "OpenChoreo Control Plane ready" + return 0 +} + +# Install OpenChoreo Data Plane +install_openchoreo_data_plane() { + log_info "Installing OpenChoreo Data Plane (this may take up to 10 minutes)..." + + if helm status data-plane -n openchoreo-data-plane >/dev/null 2>&1; then + log_warning "Data Plane already installed, skipping..." + return 0 + fi + + # Disable cert-manager since it's already installed by control-plane + install_remote_helm_chart "data-plane" \ + "${OPENCHOREO_REGISTRY}/openchoreo-data-plane" \ + "openchoreo-data-plane" \ + "true" \ + "false" \ + "600" \ + "--version" "$OPENCHOREO_VERSION" \ + "--set" "cert-manager.enabled=false" \ + "--set" "cert-manager.crds.enabled=false" + + log_info "Waiting for Data Plane pods to be ready..." + if ! kubectl wait --for=condition=Ready pod --all -n openchoreo-data-plane --timeout=600s 2>/dev/null; then + log_warning "Some Data Plane pods may still be starting (non-fatal)" + fi + + log_success "OpenChoreo Data Plane ready" + return 0 +} + +# Install OpenChoreo Observability Plane +install_openchoreo_observability_plane() { + log_info "Installing OpenChoreo Observability Plane (this may take up to 15 minutes)..." + log_info "This includes OpenSearch and OpenSearch Dashboards..." + + if helm status observability-plane -n openchoreo-observability-plane >/dev/null 2>&1; then + log_warning "Observability Plane already installed, skipping..." + return 0 + fi + + install_remote_helm_chart "observability-plane" \ + "${OPENCHOREO_REGISTRY}/openchoreo-observability-plane" \ + "openchoreo-observability-plane" \ + "true" \ + "true" \ + "900" \ + "--version" "$OPENCHOREO_VERSION" + + log_info "Waiting for Observability Plane pods to be ready..." + if ! kubectl wait --for=condition=Ready pod --all -n openchoreo-observability-plane --timeout=900s 2>/dev/null; then + log_warning "Some Observability pods may still be starting (non-fatal)" + fi + + log_success "OpenChoreo Observability Plane ready" + return 0 +} + +# Install OpenChoreo core components (required) +install_openchoreo_core() { + log_info "Installing OpenChoreo core components..." + echo "" + + # Set kubectl context + kubectl config use-context kind-openchoreo-local >/dev/null 2>&1 + + # Install Cilium CNI + if ! install_openchoreo_cilium; then + log_error "Failed to install Cilium CNI" + return 1 + fi + echo "" + + # Install Control Plane + if ! install_openchoreo_control_plane; then + log_error "Failed to install OpenChoreo Control Plane" + echo "" + echo " Troubleshooting:" + echo " 1. Check pod status: kubectl get pods -n openchoreo-control-plane" + echo " 2. View logs: kubectl logs -n openchoreo-control-plane " + echo " 3. Check resources: docker stats" + echo "" + return 1 + fi + echo "" + + # Install Data Plane + if ! install_openchoreo_data_plane; then + log_error "Failed to install OpenChoreo Data Plane" + echo "" + echo " Troubleshooting:" + echo " 1. Check pod status: kubectl get pods -n openchoreo-data-plane" + echo " 2. View logs: kubectl logs -n openchoreo-data-plane " + echo "" + return 1 + fi + echo "" + + # Install Observability Plane (required for Agent Management Platform) + if ! install_openchoreo_observability_plane; then + log_error "Failed to install OpenChoreo Observability Plane" + echo "" + echo " This component is required for the Agent Management Platform." + echo "" + echo " Troubleshooting:" + echo " 1. Check pod status: kubectl get pods -n openchoreo-observability-plane" + echo " 2. Ensure sufficient resources (4GB+ RAM recommended)" + echo " 3. View logs: kubectl logs -n openchoreo-observability-plane " + echo "" + return 1 + fi + echo "" + + log_success "OpenChoreo core components installed successfully" + return 0 +} + +# Verify OpenChoreo prerequisites for bootstrap +verify_openchoreo_prerequisites() { + log_info "Verifying OpenChoreo prerequisites..." + + # Check kubectl + if ! command_exists kubectl; then + log_error "kubectl is not installed" + echo "" + echo " Install kubectl:" + echo " → macOS: brew install kubectl" + echo " → Linux: https://kubernetes.io/docs/tasks/tools/" + echo "" + return 1 + fi + + # Check helm + if ! command_exists helm; then + log_error "Helm is not installed" + echo "" + echo " Install Helm:" + echo " → macOS: brew install helm" + echo " → Linux: https://helm.sh/docs/intro/install/" + echo "" + return 1 + fi + + # Check Docker + if ! verify_docker_running; then + return 1 + fi + + # Check Kind + if ! verify_kind_installed; then + return 1 + fi + + log_success "All prerequisites verified" + return 0 +} diff --git a/quick-start/install.sh b/quick-start/install.sh new file mode 100755 index 0000000..a2d7b26 --- /dev/null +++ b/quick-start/install.sh @@ -0,0 +1,178 @@ +#!/usr/bin/env bash +set -eo pipefail + +# Get the absolute path of the script directory +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# Source helper functions +source "${SCRIPT_DIR}/install-helpers.sh" + +# Configuration +VERBOSE="${VERBOSE:-false}" +AUTO_PORT_FORWARD="${AUTO_PORT_FORWARD:-true}" + +# Parse command line arguments +while [[ $# -gt 0 ]]; do + case $1 in + --verbose|-v) + VERBOSE=true + shift + ;; + --no-port-forward) + AUTO_PORT_FORWARD=false + shift + ;; + --config) + if [[ -f "$2" ]]; then + AMP_HELM_ARGS+=("-f" "$2") + else + log_error "Config file not found: $2" + exit 1 + fi + shift 2 + ;; + --help|-h) + echo "Usage: $0 [OPTIONS]" + echo "" + echo "Install Agent Management Platform with observability" + echo "" + echo "Options:" + echo " --verbose, -v Show detailed installation output" + echo " --no-port-forward Skip automatic port forwarding" + echo " --config FILE Use custom configuration file" + echo " --help, -h Show this help message" + echo "" + echo "Examples:" + echo " $0 # Simple installation" + echo " $0 --verbose # Installation with detailed output" + echo " $0 --config custom.yaml # Installation with custom config" + exit 0 + ;; + *) + log_error "Unknown option: $1" + echo "Use --help for usage information" + exit 1 + ;; + esac +done + +# Print simple header +if [[ "$VERBOSE" == "false" ]]; then + echo "" + echo "🚀 Installing Agent Management Platform..." + echo "" +else + log_info "Starting Agent Management Platform installation..." + log_info "Configuration:" + log_info " Kubernetes context: $(kubectl config current-context)" + log_info " Platform namespace: $AMP_NS" + log_info " Observability namespace: $OBSERVABILITY_NS" + echo "" +fi + +# Step 1: Verify prerequisites +if [[ "$VERBOSE" == "false" ]]; then + echo "✓ Validating prerequisites..." +else + log_info "Step 1: Validating prerequisites..." +fi + +if ! verify_prerequisites_silent; then + log_error "Prerequisites check failed. Run with --verbose for details." + exit 1 +fi + +# Step 2: Install Core Platform +if [[ "$VERBOSE" == "false" ]]; then + echo "✓ Installing platform components..." +else + log_info "Step 2: Installing Agent Management Platform..." +fi + +if ! install_agent_management_platform_silent; then + log_error "Platform installation failed. Run with --verbose for details." + exit 1 +fi + +# Step 3: Install Observability (always enabled) +if [[ "$VERBOSE" == "false" ]]; then + echo "✓ Installing observability stack..." +else + log_info "Step 3: Installing Observability Stack..." +fi + +if ! install_observability_dataprepper_silent; then + log_error "Observability installation failed. Run with --verbose for details." + exit 1 +fi + +# Step 4: Start services +if [[ "$VERBOSE" == "false" ]]; then + echo "✓ Starting services..." +else + log_info "Step 4: Starting port forwarding..." +fi + +# Start port forwarding +if [[ "${AUTO_PORT_FORWARD}" == "true" ]]; then + PORT_FORWARD_SCRIPT="${SCRIPT_DIR}/port-forward.sh" + if [[ -f "$PORT_FORWARD_SCRIPT" ]]; then + if [[ "$VERBOSE" == "true" ]]; then + log_info "Starting port forwarding in background..." + fi + + # Run port-forward script in background + bash "$PORT_FORWARD_SCRIPT" > /dev/null 2>&1 & + PORT_FORWARD_PID=$! + + # Save PID to file for easy cleanup + echo "$PORT_FORWARD_PID" > "${SCRIPT_DIR}/.port-forward.pid" + + # Give port forwarding a moment to start + sleep 2 + else + if [[ "$VERBOSE" == "true" ]]; then + log_warning "Port forward script not found at: $PORT_FORWARD_SCRIPT" + fi + fi +fi + +# Print success message +echo "" +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" +echo "✅ Installation Complete!" +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" +echo "" +echo "🌐 Access your platform:" +echo "" +echo " Console: http://localhost:3000" +echo " API: http://localhost:8080" +echo " Traces Observer: http://localhost:9098" +echo " Data Prepper: http://localhost:21893" +echo "" +echo "🚀 Next steps:" +echo "" +echo " 1. Open console: open http://localhost:3000" +echo " 2. Deploy sample agent: cd ../runtime/sample-agents/python-agent" +echo " 3. View traces in the console" +echo "" +echo "📚 Documentation: https://github.com/wso2/agent-management-platform" +echo "" +if [[ "${AUTO_PORT_FORWARD}" == "true" ]]; then + echo "💡 To stop port forwarding: ./stop-port-forward.sh" + echo "" +fi + +if [[ "$VERBOSE" == "true" ]]; then + echo "" + log_info "Installation Details:" + log_info " Cluster: $(kubectl config current-context)" + log_info " Platform Namespace: $AMP_NS" + log_info " Observability Namespace: $OBSERVABILITY_NS" + echo "" + log_info "Deployed Components:" + kubectl get pods -n "$AMP_NS" 2>/dev/null || true + echo "" + kubectl get pods -n "$OBSERVABILITY_NS" 2>/dev/null || true +fi + diff --git a/quick-start/kind-config.yaml b/quick-start/kind-config.yaml new file mode 100644 index 0000000..867d2c5 --- /dev/null +++ b/quick-start/kind-config.yaml @@ -0,0 +1,20 @@ +kind: Cluster +apiVersion: kind.x-k8s.io/v1alpha4 +name: openchoreo-local + +nodes: + # Control plane node + - role: control-plane + image: kindest/node:v1.32.0@sha256:c48c62eac5da28cdadcf560d1d8616cfa6783b58f0d94cf63ad1bf49600cb027 + + # Worker node for agent execution + - role: worker + labels: + openchoreo.dev/noderole: workflow-runner + image: kindest/node:v1.32.0@sha256:c48c62eac5da28cdadcf560d1d8616cfa6783b58f0d94cf63ad1bf49600cb027 + extraMounts: + - hostPath: /tmp/kind-shared + containerPath: /mnt/shared + +networking: + disableDefaultCNI: false diff --git a/quick-start/port-forward.sh b/quick-start/port-forward.sh new file mode 100755 index 0000000..ee61a0b --- /dev/null +++ b/quick-start/port-forward.sh @@ -0,0 +1,71 @@ +#!/usr/bin/env bash + +# Port forwarding script for Agent Management Platform services +# This script sets up port forwarding for all 4 required ports + +set -e + +# Default namespaces (can be overridden via environment variables) +AMP_NS="${AMP_NS:-agent-management-platform}" +OBSERVABILITY_NS="${OBSERVABILITY_NS:-openchoreo-observability-plane}" +DATA_PLANE_NS="${DATA_PLANE_NS:-openchoreo-data-plane}" + +echo "Starting port forwarding for Agent Management Platform services..." +echo "Namespaces:" +echo " - AMP: $AMP_NS" +echo " - Observability: $OBSERVABILITY_NS" +echo " - Data Plane: $DATA_PLANE_NS" +echo "" + +# Port forward Console (3000) +echo "Port forwarding Console (3000)..." +kubectl port-forward -n "$AMP_NS" svc/agent-management-platform-console 3000:3000 & +CONSOLE_PID=$! + +# Port forward Agent Manager Service (8080) +echo "Port forwarding Agent Manager Service (8080)..." +kubectl port-forward -n "$AMP_NS" svc/agent-management-platform-agent-manager-service 8080:8080 & +AGENT_MGR_PID=$! + +# Port forward Traces Observer Service (9098) - Required +echo "Port forwarding Traces Observer Service (9098)..." +if kubectl get svc traces-observer-service -n "$OBSERVABILITY_NS" >/dev/null 2>&1; then + kubectl port-forward -n "$OBSERVABILITY_NS" svc/traces-observer-service 9098:9098 & + TRACES_PID=$! +else + echo "⚠️ Warning: Traces Observer Service not found in $OBSERVABILITY_NS" +fi + +# Port forward Data Prepper (21893) - Required +echo "Port forwarding Data Prepper (21893)..." +if kubectl get svc data-prepper -n "$OBSERVABILITY_NS" >/dev/null 2>&1; then + kubectl port-forward -n "$OBSERVABILITY_NS" svc/data-prepper 21893:21893 & + DATAPREPPER_PID=$! +else + echo "⚠️ Warning: Data Prepper not found in $OBSERVABILITY_NS" +fi + +# Port forward External gateway (8443) - Required +echo "Port forwarding External gateway (8443)..." +if kubectl get svc gateway-external -n "$DATA_PLANE_NS" >/dev/null 2>&1; then + kubectl port-forward -n "$DATA_PLANE_NS" svc/gateway-external 8443:443 & + EXTERNAL_GATEWAY_PID=$! +else + echo "⚠️ Warning: External gateway not found in $DATA_PLANE_NS" +fi + +echo "" +echo "✓ Port forwarding active!" +echo "" +echo "Services accessible at:" +echo " - Console: http://localhost:3000" +echo " - Agent Manager: http://localhost:8080" +echo " - Traces Observer: http://localhost:9098" +echo " - Data Prepper: http://localhost:21893" +echo " - External gateway: http://localhost:8443" +echo "" +echo "Press Ctrl+C to stop all port forwarding" +echo "" + +# Wait for all background processes +wait diff --git a/quick-start/stop-port-forward.sh b/quick-start/stop-port-forward.sh new file mode 100755 index 0000000..3395cd9 --- /dev/null +++ b/quick-start/stop-port-forward.sh @@ -0,0 +1,71 @@ +#!/usr/bin/env bash + +# Script to stop all port forwarding processes for Agent Management Platform + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PID_FILE="${SCRIPT_DIR}/.port-forward.pid" + +# Ports used by AMP services +PORTS=(8080 21893 9098 3000 8443) + +echo "Stopping Agent Management Platform port forwarding..." +echo "" + +# Method 1: Find and kill processes using lsof +echo "Searching for kubectl port-forward processes on ports: ${PORTS[*]}..." + +# Build lsof command with all ports +LSOF_CMD="lsof" +for port in "${PORTS[@]}"; do + LSOF_CMD="$LSOF_CMD -i :$port" +done + +# Get kubectl processes using these ports +KUBECTL_PIDS=$(eval "$LSOF_CMD" 2>/dev/null | grep kubectl | awk '{print $2}' | sort -u || true) + +if [[ -n "$KUBECTL_PIDS" ]]; then + echo "Found kubectl port-forward processes:" + for pid in $KUBECTL_PIDS; do + # Get process details + PROCESS_INFO=$(ps -p "$pid" -o command= 2>/dev/null || echo "Process not found") + echo " PID $pid: $PROCESS_INFO" + done + echo "" + echo "Terminating processes..." + for pid in $KUBECTL_PIDS; do + if kill "$pid" 2>/dev/null; then + echo " ✓ Killed process $pid" + else + echo " ✗ Failed to kill process $pid (may require sudo)" + fi + done + echo "" + echo "✓ Port forwarding processes terminated" +else + echo "No kubectl port-forward processes found on monitored ports" +fi + +# Method 2: Clean up PID file if it exists +if [[ -f "$PID_FILE" ]]; then + echo "Cleaning up PID file..." + rm -f "$PID_FILE" + echo "✓ PID file removed" +fi + +# Method 3: Fallback - try to kill any remaining kubectl port-forward processes +echo "" +echo "Checking for any remaining kubectl port-forward processes..." +REMAINING_PIDS=$(pgrep -f "kubectl port-forward" 2>/dev/null || true) + +if [[ -n "$REMAINING_PIDS" ]]; then + echo "Found remaining processes: $REMAINING_PIDS" + echo "Attempting to kill remaining processes..." + pkill -f "kubectl port-forward" 2>/dev/null || true + echo "✓ Cleanup complete" +else + echo "No remaining port-forward processes found" +fi + +echo "" +echo "✓ Port forwarding cleanup complete" + diff --git a/quick-start/uninstall.sh b/quick-start/uninstall.sh new file mode 100755 index 0000000..94a5fc1 --- /dev/null +++ b/quick-start/uninstall.sh @@ -0,0 +1,132 @@ +#!/usr/bin/env bash +set -eo pipefail + +# Get the absolute path of the script directory +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# Source helper functions +source "${SCRIPT_DIR}/install-helpers.sh" + +# Configuration +FORCE="${FORCE:-false}" +DELETE_NAMESPACES="${DELETE_NAMESPACES:-false}" + +# Parse command line arguments +while [[ $# -gt 0 ]]; do + case $1 in + --force|-f) + FORCE=true + shift + ;; + --delete-namespaces) + DELETE_NAMESPACES=true + shift + ;; + --help|-h) + echo "Usage: $0 [OPTIONS]" + echo "" + echo "Uninstall Agent Management Platform" + echo "" + echo "Options:" + echo " --force, -f Skip confirmation prompt" + echo " --delete-namespaces Delete namespaces after uninstalling" + echo " --help, -h Show this help message" + echo "" + echo "Examples:" + echo " $0 # Interactive uninstall" + echo " $0 --force # Uninstall without confirmation" + exit 0 + ;; + *) + log_error "Unknown option: $1" + echo "Use --help for usage information" + exit 1 + ;; + esac +done + +# Confirmation prompt +if [[ "$FORCE" != "true" ]]; then + echo "" + echo "⚠️ This will uninstall Agent Management Platform" + echo "" + echo "The following will be removed:" + echo " - Agent Management Platform (namespace: $AMP_NS)" + echo " - Observability components (namespace: $OBSERVABILITY_NS)" + if [[ "$DELETE_NAMESPACES" == "true" ]]; then + echo " - Namespaces will be deleted" + fi + echo "" + read -p "Are you sure you want to continue? (yes/no): " -r + echo "" + if [[ ! $REPLY =~ ^[Yy][Ee][Ss]$ ]]; then + log_info "Uninstall cancelled" + exit 0 + fi +fi + +log_info "Starting uninstallation..." +echo "" + +# Stop port forwarding if running +log_info "Stopping port forwarding..." +if [[ -f "${SCRIPT_DIR}/.port-forward.pid" ]]; then + PORT_FORWARD_PID=$(cat "${SCRIPT_DIR}/.port-forward.pid") + if kill "$PORT_FORWARD_PID" 2>/dev/null; then + log_success "Port forwarding stopped (PID: $PORT_FORWARD_PID)" + fi + rm -f "${SCRIPT_DIR}/.port-forward.pid" +fi + +# Kill all kubectl port-forward processes +pkill -f "kubectl port-forward" 2>/dev/null || true +log_success "All port forwarding processes stopped" +echo "" + +# Uninstall Observability +log_info "Uninstalling Observability components..." +if helm_release_exists "amp-observability-traces" "$OBSERVABILITY_NS"; then + helm uninstall amp-observability-traces -n "$OBSERVABILITY_NS" 2>/dev/null || true + log_success "Observability components uninstalled" +else + log_info "Observability components not found, skipping" +fi +echo "" + +# Uninstall Agent Management Platform +log_info "Uninstalling Agent Management Platform..." +if helm_release_exists "agent-management-platform" "$AMP_NS"; then + helm uninstall agent-management-platform -n "$AMP_NS" 2>/dev/null || true + log_success "Agent Management Platform uninstalled" +else + log_info "Agent Management Platform not found, skipping" +fi +echo "" + +# Delete namespaces if requested +if [[ "$DELETE_NAMESPACES" == "true" ]]; then + log_info "Deleting namespaces..." + + if namespace_exists "$AMP_NS"; then + kubectl delete namespace "$AMP_NS" --timeout=60s 2>/dev/null || true + log_success "Namespace $AMP_NS deleted" + fi + + # Note: We don't delete observability namespace as it may be shared with OpenChoreo + log_warning "Observability namespace ($OBSERVABILITY_NS) not deleted (shared with OpenChoreo)" + echo "" +fi + +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" +echo "✅ Uninstallation Complete!" +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" +echo "" +log_success "Agent Management Platform has been uninstalled" +echo "" + +if [[ "$DELETE_NAMESPACES" != "true" ]]; then + log_info "To completely remove all resources including namespaces, run:" + log_info " $0 --force --delete-namespaces" + echo "" +fi + diff --git a/samples/customer-support-agent/.gitignore b/samples/customer-support-agent/.gitignore new file mode 100644 index 0000000..293cd1c --- /dev/null +++ b/samples/customer-support-agent/.gitignore @@ -0,0 +1,207 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[codz] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py.cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# UV +# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +#uv.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock +#poetry.toml + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python. +# https://pdm-project.org/en/latest/usage/project/#working-with-version-control +#pdm.lock +#pdm.toml +.pdm-python +.pdm-build/ + +# pixi +# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control. +#pixi.lock +# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one +# in the .venv directory. It is recommended not to include this directory in version control. +.pixi + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +../.env +.envrc +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +# Abstra +# Abstra is an AI-powered process automation framework. +# Ignore directories containing user credentials, local state, and settings. +# Learn more at https://abstra.io/docs +.abstra/ + +# Visual Studio Code +# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore +# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore +# and can be added to the global gitignore or merged into this file. However, if you prefer, +# you could uncomment the following to ignore the entire vscode folder +# .vscode/ + +# Ruff stuff: +.ruff_cache/ + +# PyPI configuration file +.pypirc + +# Cursor +# Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to +# exclude from AI features like autocomplete and code analysis. Recommended for sensitive data +# refer to https://docs.cursor.com/context/ignore-files +.cursorignore +.cursorindexingignore + +# Marimo +marimo/_static/ +marimo/_lsp/ +__marimo__/ diff --git a/samples/customer-support-agent/LICENSE b/samples/customer-support-agent/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/samples/customer-support-agent/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/samples/customer-support-agent/README.md b/samples/customer-support-agent/README.md new file mode 100644 index 0000000..c3e2703 --- /dev/null +++ b/samples/customer-support-agent/README.md @@ -0,0 +1,95 @@ +# Agent Service +_Generated by AI_ + +This project implements a simple agent service with a FastAPI endpoint that accepts POST requests at `/invocations` and forwards them to the agent runtime + +Agent is taken from: https://github.com/langchain-ai/langgraph/blob/main/docs/docs/tutorials/customer-support/customer-support.ipynb + +## Requirements + +- Python 3.10+ (project uses Python 3.13 bytecode in the workspace but 3.10+ is recommended) +- Install dependencies from `requirements.txt`: + +```bash +pip install -r requirements.txt +``` + +## Configuration + +The service requires two API keys (set these as environment variables or in a `.env` file in the project root): + +- `OPENAI_API_KEY` - your OpenAI API key +- `TRIVIY_API_KEY` - your Triviy API key + +Example `.env` file: + +``` +OPENAI_API_KEY=sk-... +TRIVIY_API_KEY=triviy-... +``` + +The project loads environment variables using `python-dotenv` in `app.py`. + +## Initialize the database + +Before starting the service, create and initialize the SQLite database using the provided setup script: + +```bash +python setup/db.py +``` + +This will create the necessary database file(s) (for example `travel2.sqlite`) used by the project's tools. + +## Running the FastAPI server + +There are two ways to run the FastAPI app depending on the module that exposes the `app` instance: + +- Run with `uvicorn` (recommended for development): + +```bash +uvicorn app:app --reload +``` + +This starts the service on the default `http://127.0.0.1:8000`. + +- Alternatively, if you use `main.py` as the entrypoint, run: + +```bash +uvicorn main:app --reload +``` + +If you prefer launching the app directly from Python (less common), make sure the file contains a runnable ASGI entrypoint. + +## Endpoint + +POST /invocations + +- Content-Type: `application/json` +- Payload shape (example): + +```json +{ + "thread_id": 123, + "passenger_id": "3442 587242", + "question": "What is my flight status?" +} +``` + +Example curl request: + +```bash +curl -X POST http://localhost:8000/invocations \ + -H "Content-Type: application/json" \ + -d '{"thread_id": 123, "passenger_id": "3442 587242", "question": "What is my flight status?"}' +``` + +## Notes & Troubleshooting + +- Make sure your virtual environment is activated and `requirements.txt` packages are installed. +- If you see import errors related to `agent` or `db_utils`, verify that the project root is in `PYTHONPATH` (running `uvicorn` from the project root normally works). +- The `app.py` file imports `create_agent` from `agent.agent`. If the agent runtime requires additional configuration (for example, access to the APIs listed above), set those environment variables before starting the server. + +## Next steps / Improvements + +- Add automated tests for the `/invocations` endpoint. +- Add a small healthcheck endpoint (e.g., `GET /health`) that verifies database connectivity and required API keys. diff --git a/samples/customer-support-agent/agent/__init__.py b/samples/customer-support-agent/agent/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/samples/customer-support-agent/agent/agent.py b/samples/customer-support-agent/agent/agent.py new file mode 100644 index 0000000..114bcc4 --- /dev/null +++ b/samples/customer-support-agent/agent/agent.py @@ -0,0 +1,104 @@ +# %% +from langchain_community.tools.tavily_search import TavilySearchResults +from langchain_core.prompts import ChatPromptTemplate +from langchain_core.runnables import Runnable +from langchain_openai import ChatOpenAI + +from .utils import create_tool_node_with_fallback +from .state import State +from tools.car_rentals import * +from tools.excursions import * +from tools.flights import * +from tools.hotels import * +from tools.policies import * + +load_dotenv(".env") + +class Assistant: + def __init__(self, runnable: Runnable): + self.runnable = runnable + + def __call__(self, state: State, config: RunnableConfig): + while True: + configuration = config.get("configurable", {}) + passenger_id = configuration.get("passenger_id", None) + state = {**state, "user_info": passenger_id} + result = self.runnable.invoke(state) + # If the LLM happens to return an empty response, we will re-prompt it + # for an actual response. + if not result.tool_calls and ( + not result.content + or isinstance(result.content, list) + and not result.content[0].get("text") + ): + messages = state["messages"] + [("user", "Respond with a real output.")] + state = {**state, "messages": messages} + else: + break + return {"messages": result} + + +def create_agent(): + llm = ChatOpenAI(model="gpt-4o", temperature=1) + + primary_assistant_prompt = ChatPromptTemplate.from_messages( + [ + ( + "system", + "You are a helpful customer support assistant for Swiss Airlines. " + " Use the provided tools to search for flights, company policies, and other information to assist the user's queries. " + " When searching, be persistent. Expand your query bounds if the first search returns no results. " + " If a search comes up empty, expand your search before giving up." + "\n\nCurrent user:\n\n{user_info}\n" + "\nCurrent time: {time}.", + ), + ("placeholder", "{messages}"), + ] + ).partial(time=datetime.now) + + _tools = [ + TavilySearchResults(max_results=1), + fetch_user_flight_information, + search_flights, + lookup_policy, + update_ticket_to_new_flight, + cancel_ticket, + search_car_rentals, + book_car_rental, + update_car_rental, + cancel_car_rental, + search_hotels, + book_hotel, + update_hotel, + cancel_hotel, + search_trip_recommendations, + book_excursion, + update_excursion, + cancel_excursion, + ] + + _assistant_runnable = primary_assistant_prompt | llm.bind_tools(_tools) + + # %% + from langgraph.checkpoint.memory import InMemorySaver + from langgraph.graph import StateGraph, START + from langgraph.prebuilt import tools_condition + + builder = StateGraph(State) + + # Define nodes: these do the work + builder.add_node("assistant", Assistant(_assistant_runnable)) + builder.add_node("tools", create_tool_node_with_fallback(_tools)) + # Define edges: these determine how the control flow moves + builder.add_edge(START, "assistant") + builder.add_conditional_edges( + "assistant", + tools_condition, + ) + builder.add_edge("tools", "assistant") + + # The checkpointer lets the graph persist its state + # this is a complete memory for the entire graph. + memory = InMemorySaver() + _graph = builder.compile(checkpointer=memory) + return _graph diff --git a/samples/customer-support-agent/agent/state.py b/samples/customer-support-agent/agent/state.py new file mode 100644 index 0000000..ada2918 --- /dev/null +++ b/samples/customer-support-agent/agent/state.py @@ -0,0 +1,9 @@ +from typing import Annotated + +from typing_extensions import TypedDict + +from langgraph.graph.message import AnyMessage, add_messages + + +class State(TypedDict): + messages: Annotated[list[AnyMessage], add_messages] \ No newline at end of file diff --git a/samples/customer-support-agent/agent/utils.py b/samples/customer-support-agent/agent/utils.py new file mode 100644 index 0000000..80a6b14 --- /dev/null +++ b/samples/customer-support-agent/agent/utils.py @@ -0,0 +1,41 @@ +# %% +from langchain_core.messages import ToolMessage +from langchain_core.runnables import RunnableLambda + +from langgraph.prebuilt import ToolNode + + +def handle_tool_error(state) -> dict: + error = state.get("error") + tool_calls = state["messages"][-1].tool_calls + return { + "messages": [ + ToolMessage( + content=f"Error: {repr(error)}\n please fix your mistakes.", + tool_call_id=tc["id"], + ) + for tc in tool_calls + ] + } + + +def create_tool_node_with_fallback(tools: list) -> dict: + return ToolNode(tools).with_fallbacks( + [RunnableLambda(handle_tool_error)], exception_key="error" + ) + + +def _print_event(event: dict, _printed: set, max_length=1500): + current_state = event.get("dialog_state") + if current_state: + print("Currently in: ", current_state[-1]) + message = event.get("messages") + if message: + if isinstance(message, list): + message = message[-1] + if message.id not in _printed: + msg_repr = message.pretty_repr(html=True) + if len(msg_repr) > max_length: + msg_repr = msg_repr[:max_length] + " ... (truncated)" + print(msg_repr) + _printed.add(message.id) diff --git a/samples/customer-support-agent/app.py b/samples/customer-support-agent/app.py new file mode 100644 index 0000000..1f91cfb --- /dev/null +++ b/samples/customer-support-agent/app.py @@ -0,0 +1,46 @@ +import dotenv +from fastapi import FastAPI +from fastapi.responses import JSONResponse + +from agent.agent import create_agent + + +app = FastAPI() +# Load environment variables from a .env file (if present) +dotenv.load_dotenv() +agent_graph = create_agent() + + +def run_agent(thread_id: int, question: str, passenger_id: str = "3442 587242"): + config = { + "configurable": { + # The passenger_id is used in our flight tools to + # fetch the user's flight information + "passenger_id": passenger_id, + # Checkpoints are accessed by thread_id + "thread_id": thread_id, + } + } + + events = agent_graph.stream( + {"messages": ("user", question)}, + config, + stream_mode="values" + ) + + final_answer = None + for event in events: + # Each event is a dict representing a streamed update + if "messages" in event: + print(f"Received answer: {event}") + # Keep updating until we get the latest assistant message + final_answer = event["messages"][-1].content + + return final_answer + + +@app.post("/invocations") +async def invocations(payload: dict): + # Process the payload as needed + result = {"results": run_agent(payload["thread_id"], payload["question"], payload["passenger_id"],)} + return JSONResponse(content=result) diff --git a/samples/customer-support-agent/main.py b/samples/customer-support-agent/main.py new file mode 100644 index 0000000..d6ac748 --- /dev/null +++ b/samples/customer-support-agent/main.py @@ -0,0 +1,5 @@ +from app import app + +if __name__ == '__main__': + import uvicorn + uvicorn.run(app, host="0.0.0.0", port=8000) \ No newline at end of file diff --git a/samples/customer-support-agent/openapi.yaml b/samples/customer-support-agent/openapi.yaml new file mode 100644 index 0000000..efcd609 --- /dev/null +++ b/samples/customer-support-agent/openapi.yaml @@ -0,0 +1,30 @@ +openapi: 3.0.3 +info: + title: Default Agent API + description: > + Minimal API with a single invocation endpoint. Both request and response + are free-form JSON objects (no fixed schema). + version: 1.0.0 +servers: + - url: http://localhost:8000 +paths: + /invocation: + post: + summary: Invoke an action + operationId: Invoke + requestBody: + required: true + content: + application/json: + schema: + type: object + description: Arbitrary JSON request body + additionalProperties: true + "200": + description: Successful invocation + content: + application/json: + schema: + type: object + description: Arbitrary JSON response body + additionalProperties: true diff --git a/samples/customer-support-agent/requirements.txt b/samples/customer-support-agent/requirements.txt new file mode 100644 index 0000000..756f70c --- /dev/null +++ b/samples/customer-support-agent/requirements.txt @@ -0,0 +1,9 @@ +langgraph +langchain-community +langchain_openai +tavily-python +pandas +openai +fastapi +uvicorn + diff --git a/samples/customer-support-agent/setup/db.py b/samples/customer-support-agent/setup/db.py new file mode 100644 index 0000000..fbc6771 --- /dev/null +++ b/samples/customer-support-agent/setup/db.py @@ -0,0 +1,68 @@ +# %% +import os +import shutil +import sqlite3 + +import pandas as pd +import requests + +db_url = "https://storage.googleapis.com/benchmarks-artifacts/travel-db/travel2.sqlite" +local_file = "travel2.sqlite" +# The backup lets us restart for each tutorial section +backup_file = "travel2.backup.sqlite" +overwrite = False +if overwrite or not os.path.exists(local_file): + response = requests.get(db_url) + response.raise_for_status() # Ensure the request was successful + with open(local_file, "wb") as f: + f.write(response.content) + # Backup - we will use this to "reset" our DB in each section + shutil.copy(local_file, backup_file) + + +# Convert the flights to present time for our tutorial +def update_dates(file): + shutil.copy(backup_file, file) + conn = sqlite3.connect(file) + cursor = conn.cursor() + + tables = pd.read_sql( + "SELECT name FROM sqlite_master WHERE type='table';", conn + ).name.tolist() + tdf = {} + for t in tables: + tdf[t] = pd.read_sql(f"SELECT * from {t}", conn) + + example_time = pd.to_datetime( + tdf["flights"]["actual_departure"].replace("\\N", pd.NaT) + ).max() + current_time = pd.to_datetime("now").tz_localize(example_time.tz) + time_diff = current_time - example_time + + tdf["bookings"]["book_date"] = ( + pd.to_datetime(tdf["bookings"]["book_date"].replace("\\N", pd.NaT), utc=True) + + time_diff + ) + + datetime_columns = [ + "scheduled_departure", + "scheduled_arrival", + "actual_departure", + "actual_arrival", + ] + for column in datetime_columns: + tdf["flights"][column] = ( + pd.to_datetime(tdf["flights"][column].replace("\\N", pd.NaT)) + time_diff + ) + + for table_name, df in tdf.items(): + df.to_sql(table_name, conn, if_exists="replace", index=False) + del df + del tdf + conn.commit() + conn.close() + + return file + + +db = update_dates(local_file) diff --git a/samples/customer-support-agent/tools/__init__.py b/samples/customer-support-agent/tools/__init__.py new file mode 100644 index 0000000..2cf84bd --- /dev/null +++ b/samples/customer-support-agent/tools/__init__.py @@ -0,0 +1 @@ +db = "travel2.sqlite" diff --git a/samples/customer-support-agent/tools/car_rentals.py b/samples/customer-support-agent/tools/car_rentals.py new file mode 100644 index 0000000..201f944 --- /dev/null +++ b/samples/customer-support-agent/tools/car_rentals.py @@ -0,0 +1,142 @@ +import sqlite3 +from datetime import date, datetime +from typing import Optional, Union + +from langchain_core.tools import tool + +from . import db + + +@tool +def search_car_rentals( + location: Optional[str] = None, + name: Optional[str] = None, + price_tier: Optional[str] = None, + start_date: Optional[Union[datetime, date]] = None, + end_date: Optional[Union[datetime, date]] = None, +) -> list[dict]: + """ + Search for car rentals based on location, name, price tier, start date, and end date. + + Args: + location (Optional[str]): The location of the car rental. Defaults to None. + name (Optional[str]): The name of the car rental company. Defaults to None. + price_tier (Optional[str]): The price tier of the car rental. Defaults to None. + start_date (Optional[Union[datetime, date]]): The start date of the car rental. Defaults to None. + end_date (Optional[Union[datetime, date]]): The end date of the car rental. Defaults to None. + + Returns: + list[dict]: A list of car rental dictionaries matching the search criteria. + """ + conn = sqlite3.connect(db) + cursor = conn.cursor() + + query = "SELECT * FROM car_rentals WHERE 1=1" + params = [] + + if location: + query += " AND location LIKE ?" + params.append(f"%{location}%") + if name: + query += " AND name LIKE ?" + params.append(f"%{name}%") + # For our tutorial, we will let you match on any dates and price tier. + # (since our toy dataset doesn't have much data) + cursor.execute(query, params) + results = cursor.fetchall() + + conn.close() + + return [ + dict(zip([column[0] for column in cursor.description], row)) for row in results + ] + + +@tool +def book_car_rental(rental_id: int) -> str: + """ + Book a car rental by its ID. + + Args: + rental_id (int): The ID of the car rental to book. + + Returns: + str: A message indicating whether the car rental was successfully booked or not. + """ + conn = sqlite3.connect(db) + cursor = conn.cursor() + + cursor.execute("UPDATE car_rentals SET booked = 1 WHERE id = ?", (rental_id,)) + conn.commit() + + if cursor.rowcount > 0: + conn.close() + return f"Car rental {rental_id} successfully booked." + else: + conn.close() + return f"No car rental found with ID {rental_id}." + + +@tool +def update_car_rental( + rental_id: int, + start_date: Optional[Union[datetime, date]] = None, + end_date: Optional[Union[datetime, date]] = None, +) -> str: + """ + Update a car rental's start and end dates by its ID. + + Args: + rental_id (int): The ID of the car rental to update. + start_date (Optional[Union[datetime, date]]): The new start date of the car rental. Defaults to None. + end_date (Optional[Union[datetime, date]]): The new end date of the car rental. Defaults to None. + + Returns: + str: A message indicating whether the car rental was successfully updated or not. + """ + conn = sqlite3.connect(db) + cursor = conn.cursor() + + if start_date: + cursor.execute( + "UPDATE car_rentals SET start_date = ? WHERE id = ?", + (start_date, rental_id), + ) + if end_date: + cursor.execute( + "UPDATE car_rentals SET end_date = ? WHERE id = ?", (end_date, rental_id) + ) + + conn.commit() + + if cursor.rowcount > 0: + conn.close() + return f"Car rental {rental_id} successfully updated." + else: + conn.close() + return f"No car rental found with ID {rental_id}." + + +@tool +def cancel_car_rental(rental_id: int) -> str: + """ + Cancel a car rental by its ID. + + Args: + rental_id (int): The ID of the car rental to cancel. + + Returns: + str: A message indicating whether the car rental was successfully cancelled or not. + """ + conn = sqlite3.connect(db) + cursor = conn.cursor() + + cursor.execute("UPDATE car_rentals SET booked = 0 WHERE id = ?", (rental_id,)) + conn.commit() + + if cursor.rowcount > 0: + conn.close() + return f"Car rental {rental_id} successfully cancelled." + else: + conn.close() + return f"No car rental found with ID {rental_id}." diff --git a/samples/customer-support-agent/tools/excursions.py b/samples/customer-support-agent/tools/excursions.py new file mode 100644 index 0000000..7a3507a --- /dev/null +++ b/samples/customer-support-agent/tools/excursions.py @@ -0,0 +1,134 @@ +import sqlite3 +from typing import Optional + +from langchain_core.tools import tool + +from . import db + + +@tool +def search_trip_recommendations( + location: Optional[str] = None, + name: Optional[str] = None, + keywords: Optional[str] = None, +) -> list[dict]: + """ + Search for trip recommendations based on location, name, and keywords. + + Args: + location (Optional[str]): The location of the trip recommendation. Defaults to None. + name (Optional[str]): The name of the trip recommendation. Defaults to None. + keywords (Optional[str]): The keywords associated with the trip recommendation. Defaults to None. + + Returns: + list[dict]: A list of trip recommendation dictionaries matching the search criteria. + """ + conn = sqlite3.connect(db) + cursor = conn.cursor() + + query = "SELECT * FROM trip_recommendations WHERE 1=1" + params = [] + + if location: + query += " AND location LIKE ?" + params.append(f"%{location}%") + if name: + query += " AND name LIKE ?" + params.append(f"%{name}%") + if keywords: + keyword_list = keywords.split(",") + keyword_conditions = " OR ".join(["keywords LIKE ?" for _ in keyword_list]) + query += f" AND ({keyword_conditions})" + params.extend([f"%{keyword.strip()}%" for keyword in keyword_list]) + + cursor.execute(query, params) + results = cursor.fetchall() + + conn.close() + + return [ + dict(zip([column[0] for column in cursor.description], row)) for row in results + ] + + +@tool +def book_excursion(recommendation_id: int) -> str: + """ + Book an excursion by its recommendation ID. + + Args: + recommendation_id (int): The ID of the trip recommendation to book. + + Returns: + str: A message indicating whether the trip recommendation was successfully booked or not. + """ + conn = sqlite3.connect(db) + cursor = conn.cursor() + + cursor.execute( + "UPDATE trip_recommendations SET booked = 1 WHERE id = ?", (recommendation_id,) + ) + conn.commit() + + if cursor.rowcount > 0: + conn.close() + return f"Trip recommendation {recommendation_id} successfully booked." + else: + conn.close() + return f"No trip recommendation found with ID {recommendation_id}." + + +@tool +def update_excursion(recommendation_id: int, details: str) -> str: + """ + Update a trip recommendation's details by its ID. + + Args: + recommendation_id (int): The ID of the trip recommendation to update. + details (str): The new details of the trip recommendation. + + Returns: + str: A message indicating whether the trip recommendation was successfully updated or not. + """ + conn = sqlite3.connect(db) + cursor = conn.cursor() + + cursor.execute( + "UPDATE trip_recommendations SET details = ? WHERE id = ?", + (details, recommendation_id), + ) + conn.commit() + + if cursor.rowcount > 0: + conn.close() + return f"Trip recommendation {recommendation_id} successfully updated." + else: + conn.close() + return f"No trip recommendation found with ID {recommendation_id}." + + +@tool +def cancel_excursion(recommendation_id: int) -> str: + """ + Cancel a trip recommendation by its ID. + + Args: + recommendation_id (int): The ID of the trip recommendation to cancel. + + Returns: + str: A message indicating whether the trip recommendation was successfully cancelled or not. + """ + conn = sqlite3.connect(db) + cursor = conn.cursor() + + cursor.execute( + "UPDATE trip_recommendations SET booked = 0 WHERE id = ?", (recommendation_id,) + ) + conn.commit() + + if cursor.rowcount > 0: + conn.close() + return f"Trip recommendation {recommendation_id} successfully cancelled." + else: + conn.close() + return f"No trip recommendation found with ID {recommendation_id}." diff --git a/samples/customer-support-agent/tools/flights.py b/samples/customer-support-agent/tools/flights.py new file mode 100644 index 0000000..2722f8c --- /dev/null +++ b/samples/customer-support-agent/tools/flights.py @@ -0,0 +1,203 @@ +import sqlite3 +from datetime import date, datetime +from typing import Optional + +import pytz +from langchain_core.runnables import RunnableConfig +from langchain_core.tools import tool + +from . import db + + +@tool +def fetch_user_flight_information(config: RunnableConfig) -> list[dict]: + """Fetch all tickets for the user along with corresponding flight information and seat assignments. + + Returns: + A list of dictionaries where each dictionary contains the ticket details, + associated flight details, and the seat assignments for each ticket belonging to the user. + """ + configuration = config.get("configurable", {}) + passenger_id = configuration.get("passenger_id", None) + if not passenger_id: + raise ValueError("No passenger ID configured.") + + conn = sqlite3.connect(db) + cursor = conn.cursor() + + query = """ + SELECT t.ticket_no, + t.book_ref, + f.flight_id, + f.flight_no, + f.departure_airport, + f.arrival_airport, + f.scheduled_departure, + f.scheduled_arrival, + bp.seat_no, + tf.fare_conditions + FROM tickets t + JOIN ticket_flights tf ON t.ticket_no = tf.ticket_no + JOIN flights f ON tf.flight_id = f.flight_id + JOIN boarding_passes bp ON bp.ticket_no = t.ticket_no AND bp.flight_id = f.flight_id + WHERE t.passenger_id = ? \ + """ + cursor.execute(query, (passenger_id,)) + rows = cursor.fetchall() + column_names = [column[0] for column in cursor.description] + results = [dict(zip(column_names, row)) for row in rows] + + cursor.close() + conn.close() + + return results + + +@tool +def search_flights( + departure_airport: Optional[str] = None, + arrival_airport: Optional[str] = None, + start_time: Optional[date | datetime] = None, + end_time: Optional[date | datetime] = None, + limit: int = 20, +) -> list[dict]: + """Search for flights based on departure airport, arrival airport, and departure time range.""" + conn = sqlite3.connect(db) + cursor = conn.cursor() + + query = "SELECT * FROM flights WHERE 1 = 1" + params = [] + + if departure_airport: + query += " AND departure_airport = ?" + params.append(departure_airport) + + if arrival_airport: + query += " AND arrival_airport = ?" + params.append(arrival_airport) + + if start_time: + query += " AND scheduled_departure >= ?" + params.append(start_time) + + if end_time: + query += " AND scheduled_departure <= ?" + params.append(end_time) + query += " LIMIT ?" + params.append(limit) + cursor.execute(query, params) + rows = cursor.fetchall() + column_names = [column[0] for column in cursor.description] + results = [dict(zip(column_names, row)) for row in rows] + + cursor.close() + conn.close() + + return results + + +@tool +def update_ticket_to_new_flight( + ticket_no: str, new_flight_id: int, *, config: RunnableConfig +) -> str: + """Update the user's ticket to a new valid flight.""" + configuration = config.get("configurable", {}) + passenger_id = configuration.get("passenger_id", None) + if not passenger_id: + raise ValueError("No passenger ID configured.") + + conn = sqlite3.connect(db) + cursor = conn.cursor() + + cursor.execute( + "SELECT departure_airport, arrival_airport, scheduled_departure FROM flights WHERE flight_id = ?", + (new_flight_id,), + ) + new_flight = cursor.fetchone() + if not new_flight: + cursor.close() + conn.close() + return "Invalid new flight ID provided." + column_names = [column[0] for column in cursor.description] + new_flight_dict = dict(zip(column_names, new_flight)) + timezone = pytz.timezone("Etc/GMT-3") + current_time = datetime.now(tz=timezone) + departure_time = datetime.strptime( + new_flight_dict["scheduled_departure"], "%Y-%m-%d %H:%M:%S.%f%z" + ) + time_until = (departure_time - current_time).total_seconds() + if time_until < (3 * 3600): + return f"Not permitted to reschedule to a flight that is less than 3 hours from the current time. Selected flight is at {departure_time}." + + cursor.execute( + "SELECT flight_id FROM ticket_flights WHERE ticket_no = ?", (ticket_no,) + ) + current_flight = cursor.fetchone() + if not current_flight: + cursor.close() + conn.close() + return "No existing ticket found for the given ticket number." + + # Check the signed-in user actually has this ticket + cursor.execute( + "SELECT * FROM tickets WHERE ticket_no = ? AND passenger_id = ?", + (ticket_no, passenger_id), + ) + current_ticket = cursor.fetchone() + if not current_ticket: + cursor.close() + conn.close() + return f"Current signed-in passenger with ID {passenger_id} not the owner of ticket {ticket_no}" + + # In a real application, you'd likely add additional checks here to enforce business logic, + # like "does the new departure airport match the current ticket", etc. + # While it's best to try to be *proactive* in 'type-hinting' policies to the LLM + # it's inevitably going to get things wrong, so you **also** need to ensure your + # API enforces valid behavior + cursor.execute( + "UPDATE ticket_flights SET flight_id = ? WHERE ticket_no = ?", + (new_flight_id, ticket_no), + ) + conn.commit() + + cursor.close() + conn.close() + return "Ticket successfully updated to new flight." + + +@tool +def cancel_ticket(ticket_no: str, *, config: RunnableConfig) -> str: + """Cancel the user's ticket and remove it from the database.""" + configuration = config.get("configurable", {}) + passenger_id = configuration.get("passenger_id", None) + if not passenger_id: + raise ValueError("No passenger ID configured.") + conn = sqlite3.connect(db) + cursor = conn.cursor() + + cursor.execute( + "SELECT flight_id FROM ticket_flights WHERE ticket_no = ?", (ticket_no,) + ) + existing_ticket = cursor.fetchone() + if not existing_ticket: + cursor.close() + conn.close() + return "No existing ticket found for the given ticket number." + + # Check the signed-in user actually has this ticket + cursor.execute( + "SELECT ticket_no FROM tickets WHERE ticket_no = ? AND passenger_id = ?", + (ticket_no, passenger_id), + ) + current_ticket = cursor.fetchone() + if not current_ticket: + cursor.close() + conn.close() + return f"Current signed-in passenger with ID {passenger_id} not the owner of ticket {ticket_no}" + + cursor.execute("DELETE FROM ticket_flights WHERE ticket_no = ?", (ticket_no,)) + conn.commit() + + cursor.close() + conn.close() + return "Ticket successfully cancelled." diff --git a/samples/customer-support-agent/tools/hotels.py b/samples/customer-support-agent/tools/hotels.py new file mode 100644 index 0000000..9ea2ec4 --- /dev/null +++ b/samples/customer-support-agent/tools/hotels.py @@ -0,0 +1,141 @@ +import sqlite3 +from datetime import date, datetime +from typing import Optional, Union + +from langchain_core.tools import tool + +from . import db + + +@tool +def search_hotels( + location: Optional[str] = None, + name: Optional[str] = None, + price_tier: Optional[str] = None, + checkin_date: Optional[Union[datetime, date]] = None, + checkout_date: Optional[Union[datetime, date]] = None, +) -> list[dict]: + """ + Search for hotels based on location, name, price tier, check-in date, and check-out date. + + Args: + location (Optional[str]): The location of the hotel. Defaults to None. + name (Optional[str]): The name of the hotel. Defaults to None. + price_tier (Optional[str]): The price tier of the hotel. Defaults to None. Examples: Midscale, Upper Midscale, Upscale, Luxury + checkin_date (Optional[Union[datetime, date]]): The check-in date of the hotel. Defaults to None. + checkout_date (Optional[Union[datetime, date]]): The check-out date of the hotel. Defaults to None. + + Returns: + list[dict]: A list of hotel dictionaries matching the search criteria. + """ + conn = sqlite3.connect(db) + cursor = conn.cursor() + + query = "SELECT * FROM hotels WHERE 1=1" + params = [] + + if location: + query += " AND location LIKE ?" + params.append(f"%{location}%") + if name: + query += " AND name LIKE ?" + params.append(f"%{name}%") + # For the sake of this tutorial, we will let you match on any dates and price tier. + cursor.execute(query, params) + results = cursor.fetchall() + + conn.close() + + return [ + dict(zip([column[0] for column in cursor.description], row)) for row in results + ] + + +@tool +def book_hotel(hotel_id: int) -> str: + """ + Book a hotel by its ID. + + Args: + hotel_id (int): The ID of the hotel to book. + + Returns: + str: A message indicating whether the hotel was successfully booked or not. + """ + conn = sqlite3.connect(db) + cursor = conn.cursor() + + cursor.execute("UPDATE hotels SET booked = 1 WHERE id = ?", (hotel_id,)) + conn.commit() + + if cursor.rowcount > 0: + conn.close() + return f"Hotel {hotel_id} successfully booked." + else: + conn.close() + return f"No hotel found with ID {hotel_id}." + + +@tool +def update_hotel( + hotel_id: int, + checkin_date: Optional[Union[datetime, date]] = None, + checkout_date: Optional[Union[datetime, date]] = None, +) -> str: + """ + Update a hotel's check-in and check-out dates by its ID. + + Args: + hotel_id (int): The ID of the hotel to update. + checkin_date (Optional[Union[datetime, date]]): The new check-in date of the hotel. Defaults to None. + checkout_date (Optional[Union[datetime, date]]): The new check-out date of the hotel. Defaults to None. + + Returns: + str: A message indicating whether the hotel was successfully updated or not. + """ + conn = sqlite3.connect(db) + cursor = conn.cursor() + + if checkin_date: + cursor.execute( + "UPDATE hotels SET checkin_date = ? WHERE id = ?", (checkin_date, hotel_id) + ) + if checkout_date: + cursor.execute( + "UPDATE hotels SET checkout_date = ? WHERE id = ?", + (checkout_date, hotel_id), + ) + + conn.commit() + + if cursor.rowcount > 0: + conn.close() + return f"Hotel {hotel_id} successfully updated." + else: + conn.close() + return f"No hotel found with ID {hotel_id}." + + +@tool +def cancel_hotel(hotel_id: int) -> str: + """ + Cancel a hotel by its ID. + + Args: + hotel_id (int): The ID of the hotel to cancel. + + Returns: + str: A message indicating whether the hotel was successfully cancelled or not. + """ + conn = sqlite3.connect(db) + cursor = conn.cursor() + + cursor.execute("UPDATE hotels SET booked = 0 WHERE id = ?", (hotel_id,)) + conn.commit() + + if cursor.rowcount > 0: + conn.close() + return f"Hotel {hotel_id} successfully cancelled." + else: + conn.close() + return f"No hotel found with ID {hotel_id}." diff --git a/samples/customer-support-agent/tools/policies.py b/samples/customer-support-agent/tools/policies.py new file mode 100644 index 0000000..2f3aabc --- /dev/null +++ b/samples/customer-support-agent/tools/policies.py @@ -0,0 +1,56 @@ +# %% +import re + +import numpy as np +import openai +import requests +from dotenv import load_dotenv +from langchain_core.tools import tool + +load_dotenv(".env") + +response = requests.get( + "https://storage.googleapis.com/benchmarks-artifacts/travel-db/swiss_faq.md" +) +response.raise_for_status() +faq_text = response.text + +docs = [{"page_content": txt} for txt in re.split(r"(?=\n##)", faq_text)] + + +class VectorStoreRetriever: + def __init__(self, docs: list, vectors: list, oai_client): + self._arr = np.array(vectors) + self._docs = docs + self._client = oai_client + + @classmethod + def from_docs(cls, docs, oai_client): + embeddings = oai_client.embeddings.create( + model="text-embedding-3-small", input=[doc["page_content"] for doc in docs] + ) + vectors = [emb.embedding for emb in embeddings.data] + return cls(docs, vectors, oai_client) + + def query(self, query: str, k: int = 5) -> list[dict]: + embed = self._client.embeddings.create( + model="text-embedding-3-small", input=[query] + ) + # "@" is just a matrix multiplication in python + scores = np.array(embed.data[0].embedding) @ self._arr.T + top_k_idx = np.argpartition(scores, -k)[-k:] + top_k_idx_sorted = top_k_idx[np.argsort(-scores[top_k_idx])] + return [ + {**self._docs[idx], "similarity": scores[idx]} for idx in top_k_idx_sorted + ] + + +retriever = VectorStoreRetriever.from_docs(docs, openai.Client()) + + +@tool +def lookup_policy(query: str) -> str: + """Consult the company policies to check whether certain options are permitted. + Use this before making any flight changes performing other 'write' events.""" + docs = retriever.query(query, k=2) + return "\n\n".join([doc["page_content"] for doc in docs])