# Directory Structure ``` ├── .gitattributes ├── .gitignore ├── agents │ ├── agent_integration.py │ ├── agent_server.py │ ├── Dockerfile │ └── requirements.txt ├── client │ ├── css │ │ └── styles.css │ ├── index.html │ └── js │ ├── animations.js │ └── main.js ├── DEVELOPER.md ├── docker-compose.yml ├── Dockerfile ├── PROJECT_SUMMARY.md ├── QUICK_START.md ├── README.md ├── server │ ├── app.py │ ├── config.py │ ├── forms_api.py │ ├── mcp_handler.py │ ├── requirements.txt │ ├── static │ │ ├── animations.js │ │ ├── main.js │ │ └── styles.css │ ├── templates │ │ └── index.html │ └── utils │ ├── __init__.py │ └── logger.py ├── setup_credentials.sh ├── start_with_creds.sh ├── start.sh └── use_provided_creds.sh ``` # Files -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- ``` # Auto detect text files and perform LF normalization * text=auto ``` -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` # Environment variables .env .env.* # Python __pycache__/ *.py[cod] *$py.class *.so .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # Virtual Environment venv/ env/ ENV/ # Docker .dockerignore # IDE files .idea/ .vscode/ *.swp *.swo .DS_Store # Logs logs/ *.log # Testing .coverage htmlcov/ .pytest_cache/ .tox/ .nox/ coverage.html coverage.xml *.cover # Google API credentials credentials.json token.json ``` -------------------------------------------------------------------------------- /client/js/animations.js: -------------------------------------------------------------------------------- ```javascript ``` -------------------------------------------------------------------------------- /server/utils/__init__.py: -------------------------------------------------------------------------------- ```python """Utility functions for the Google Forms MCP Server.""" ``` -------------------------------------------------------------------------------- /agents/requirements.txt: -------------------------------------------------------------------------------- ``` requests==2.28.2 flask==2.2.3 flask-cors==3.0.10 werkzeug==2.2.3 numpy==1.24.2 websockets==11.0.2 pymongo==4.3.3 camel-ai Pillow # Add Google Generative AI SDK google-generativeai ``` -------------------------------------------------------------------------------- /server/requirements.txt: -------------------------------------------------------------------------------- ``` flask==2.2.3 flask-cors==3.0.10 werkzeug==2.2.3 google-auth==2.17.3 google-auth-oauthlib==1.0.0 google-auth-httplib2==0.1.0 google-api-python-client==2.86.0 python-dotenv==1.0.0 requests==2.28.2 gunicorn==20.1.0 websockets==11.0.2 uuid==1.30 ``` -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- ```dockerfile FROM python:3.9-slim WORKDIR /app # Copy requirements first to leverage Docker caching COPY server/requirements.txt . # Install dependencies RUN pip install --no-cache-dir -r requirements.txt # Copy server code COPY server /app/server COPY .env /app/.env # Set working directory to server WORKDIR /app/server # Set environment variables ENV PYTHONUNBUFFERED=1 # Expose port EXPOSE 5000 # Run the server CMD ["python", "app.py"] ``` -------------------------------------------------------------------------------- /agents/Dockerfile: -------------------------------------------------------------------------------- ```dockerfile FROM python:3.9-slim # Install curl for healthcheck RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/* WORKDIR /app # Copy requirements first to leverage Docker caching COPY agents/requirements.txt . # Install dependencies RUN pip install --no-cache-dir -r requirements.txt # Copy agent code COPY agents /app/agents # Set working directory to agents WORKDIR /app/agents # Set environment variables ENV PYTHONUNBUFFERED=1 # Expose port EXPOSE 5001 # Use --app to specify the application file CMD ["flask", "--app", "agent_server:app", "run", "--host=0.0.0.0", "--port=5001"] ``` -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- ```yaml version: '3.8' services: # MCP Server Service mcp-server: build: context: . dockerfile: Dockerfile container_name: google-form-mcp-server ports: - "5005:5000" volumes: - ./server:/app/server env_file: - .env environment: - FLASK_ENV=development - DEBUG=True - AGENT_ENDPOINT=http://agents:5001/process depends_on: agents: condition: service_healthy restart: unless-stopped networks: - mcp-network # CamelAIOrg Agents Service agents: build: context: . dockerfile: agents/Dockerfile container_name: camelai-agents ports: - "5006:5001" volumes: - ./agents:/app/agents environment: - MCP_SERVER_URL=http://mcp-server:5000/api/process - PORT=5001 - GOOGLE_API_KEY={{GOOGLE_API_KEY}} healthcheck: test: ["CMD", "curl", "--fail", "http://localhost:5001/health"] interval: 10s timeout: 5s retries: 5 start_period: 30s restart: unless-stopped networks: - mcp-network networks: mcp-network: driver: bridge ``` -------------------------------------------------------------------------------- /use_provided_creds.sh: -------------------------------------------------------------------------------- ```bash #!/bin/bash # Setup the project with user-provided credentials echo "================================================" echo " Google Forms MCP Server - Use Provided Credentials" echo "================================================" echo "" # Check if credentials were provided if [ -z "$1" ] || [ -z "$2" ] || [ -z "$3" ]; then echo "Usage: $0 <client_id> <client_secret> <refresh_token>" echo "" echo "Example:" echo "$0 123456789-abcdef.apps.googleusercontent.com GOCSPX-abc123def456 1//04abcdefghijklmnop" exit 1 fi # Get credentials from arguments CLIENT_ID=$1 CLIENT_SECRET=$2 REFRESH_TOKEN=$3 # Create .env file with provided credentials cat > .env << EOF # Google API Credentials GOOGLE_CLIENT_ID=$CLIENT_ID GOOGLE_CLIENT_SECRET=$CLIENT_SECRET GOOGLE_REFRESH_TOKEN=$REFRESH_TOKEN # Server Configuration FLASK_ENV=development PORT=5000 DEBUG=True # CamelAIOrg Agents Configuration AGENT_ENDPOINT=http://agents:5001/process AGENT_API_KEY=demo_key EOF echo "Created .env file with your provided credentials." echo "" echo "Starting the project now..." echo "" # Start the project ./start.sh ``` -------------------------------------------------------------------------------- /server/config.py: -------------------------------------------------------------------------------- ```python import os from dotenv import load_dotenv # Load environment variables from .env file load_dotenv() # Google API settings GOOGLE_CLIENT_ID = os.getenv('GOOGLE_CLIENT_ID') GOOGLE_CLIENT_SECRET = os.getenv('GOOGLE_CLIENT_SECRET') GOOGLE_REFRESH_TOKEN = os.getenv('GOOGLE_REFRESH_TOKEN') # API scopes needed for Google Forms # Include more scopes to ensure proper access SCOPES = [ 'https://www.googleapis.com/auth/forms', 'https://www.googleapis.com/auth/forms.body', 'https://www.googleapis.com/auth/drive', 'https://www.googleapis.com/auth/drive.file', 'https://www.googleapis.com/auth/drive.metadata' ] # Server settings PORT = int(os.getenv('PORT', 5000)) DEBUG = os.getenv('DEBUG', 'False').lower() == 'true' # CamelAIOrg Agent settings AGENT_ENDPOINT = os.getenv('AGENT_ENDPOINT', 'http://agents:5001/process') AGENT_API_KEY = os.getenv('AGENT_API_KEY') # MCP Protocol settings MCP_VERSION = "1.0.0" MCP_TOOLS = [ "create_form", "add_question", "get_responses" ] # LLM Settings / Gemini Settings (Update this section) # REMOVE LLM_API_KEY and LLM_API_ENDPOINT GEMINI_API_KEY = os.getenv('GEMINI_API_KEY') # The specific model endpoint will be constructed in the agent ``` -------------------------------------------------------------------------------- /start.sh: -------------------------------------------------------------------------------- ```bash #!/bin/bash # Display welcome message echo "================================================" echo " Google Forms MCP Server with CamelAIOrg Agents" echo "================================================" echo "" # Check if .env file exists if [ ! -f ".env" ]; then echo "Error: .env file not found!" echo "Please create a .env file with your Google API credentials." echo "Example format:" echo "GOOGLE_CLIENT_ID=your_client_id" echo "GOOGLE_CLIENT_SECRET=your_client_secret" echo "GOOGLE_REFRESH_TOKEN=your_refresh_token" echo "" echo "PORT=5000" echo "DEBUG=True" echo "" exit 1 fi # Build and start containers echo "Starting services with Docker Compose..." docker-compose up --build -d # Wait for services to start echo "Waiting for services to start..." sleep 5 # Display service status echo "" echo "Service Status:" docker-compose ps # Display access information echo "" echo "Access Information:" echo "- MCP Server: http://localhost:5005" echo "- Agent Server: http://localhost:5006" echo "- Client Interface: Open client/index.html in your browser" echo "" echo "To view logs:" echo " docker-compose logs -f" echo "" echo "To stop the services:" echo " docker-compose down" echo "" echo "Enjoy using the Google Forms MCP Server!" ``` -------------------------------------------------------------------------------- /start_with_creds.sh: -------------------------------------------------------------------------------- ```bash #!/bin/bash # Setup the project with provided credentials echo "================================================" echo " Google Forms MCP Server - Quick Start" echo "================================================" echo "" echo "This script will set up the project with provided credentials" echo "and start the services." echo "" # Create .env file with provided credentials cat > .env << EOF # Google API Credentials GOOGLE_CLIENT_ID=your_client_id_here GOOGLE_CLIENT_SECRET=your_client_secret_here GOOGLE_REFRESH_TOKEN=your_refresh_token_here # Server Configuration FLASK_ENV=development PORT=5000 DEBUG=True # CamelAIOrg Agents Configuration AGENT_ENDPOINT=http://agents:5001/process AGENT_API_KEY=demo_key EOF echo "Created .env file with placeholder credentials." echo "" echo "IMPORTANT: You need to edit the .env file with your actual Google API credentials." echo "You can do this now by running:" echo " nano .env" echo "" echo "After updating your credentials, start the project with:" echo " ./start.sh" echo "" echo "Would you like to edit the .env file now? (y/n)" read edit_now if [ "$edit_now" == "y" ]; then ${EDITOR:-nano} .env echo "" echo "Now that you've updated your credentials, would you like to start the project? (y/n)" read start_now if [ "$start_now" == "y" ]; then ./start.sh else echo "You can start the project later by running ./start.sh" fi else echo "Remember to update your credentials in .env before starting the project." fi ``` -------------------------------------------------------------------------------- /client/index.html: -------------------------------------------------------------------------------- ```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Google Forms MCP Client</title> <link rel="stylesheet" href="css/styles.css"> </head> <body> <div class="container"> <h1>Google Forms MCP Client</h1> <p>This client connects to the Google Forms MCP Server and CamelAIOrg Agents to create forms using natural language.</p> <div class="server-status"> <h2>Server Status</h2> <p>MCP Server: <span id="serverStatus">Unknown</span></p> </div> <a href="http://localhost:5005" class="button pulse" id="connectButton">Connect to Server</a> <div class="status loading" id="loadingMessage"> Connecting to server... </div> <div class="status error" id="errorMessage"> Could not connect to the server. Please make sure the MCP server is running. </div> <div class="features"> <h2>Features</h2> <ul> <li>Create Google Forms with natural language</li> <li>Multiple question types support</li> <li>View form creation process in real-time</li> <li>Monitor API requests and responses</li> </ul> </div> <div class="footer"> <p>Google Forms MCP Server with CamelAIOrg Agents Integration</p> </div> </div> <script src="js/main.js"></script> </body> </html> ``` -------------------------------------------------------------------------------- /setup_credentials.sh: -------------------------------------------------------------------------------- ```bash #!/bin/bash # Google Forms MCP Server Credentials Setup Script echo "================================================" echo " Google Forms MCP Server - Credentials Setup" echo "================================================" echo "" echo "This script will help you set up your Google API credentials." echo "You'll need to provide your Client ID, Client Secret, and Refresh Token." echo "" echo "If you don't have these credentials yet, please follow the instructions in README.md" echo "" # Check if .env already exists if [ -f ".env" ]; then read -p ".env file already exists. Do you want to overwrite it? (y/n): " overwrite if [ "$overwrite" != "y" ]; then echo "Setup cancelled." exit 0 fi fi # Get credentials read -p "Enter your Google Client ID: " client_id read -p "Enter your Google Client Secret: " client_secret read -p "Enter your Google Refresh Token: " refresh_token # Get port settings read -p "Enter port for MCP Server [5000]: " port port=${port:-5000} # Get debug setting read -p "Enable debug mode? (y/n) [y]: " debug_mode debug_mode=${debug_mode:-y} if [ "$debug_mode" == "y" ]; then debug="True" else debug="False" fi # Create .env file cat > .env << EOF # Google API Credentials GOOGLE_CLIENT_ID=$client_id GOOGLE_CLIENT_SECRET=$client_secret GOOGLE_REFRESH_TOKEN=$refresh_token # Server Configuration FLASK_ENV=development PORT=$port DEBUG=$debug # CamelAIOrg Agents Configuration AGENT_ENDPOINT=http://agents:5001/process AGENT_API_KEY=demo_key EOF echo "" echo "Credentials saved to .env file." echo "" echo "You can now start the project with:" echo "./start.sh" echo "" echo "If you need to update your credentials later, you can run this script again" echo "or edit the .env file directly." ``` -------------------------------------------------------------------------------- /server/utils/logger.py: -------------------------------------------------------------------------------- ```python import logging import sys import json from datetime import datetime # Configure logger logger = logging.getLogger("mcp_server") logger.setLevel(logging.INFO) # Console handler console_handler = logging.StreamHandler(sys.stdout) console_handler.setLevel(logging.INFO) # Format formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') console_handler.setFormatter(formatter) # Add handler to logger logger.addHandler(console_handler) def log_mcp_request(request_data): """Log an incoming MCP request.""" try: transaction_id = request_data.get('transaction_id', 'unknown') tool_name = request_data.get('tool_name', 'unknown') logger.info(f"MCP Request [{transaction_id}] - Tool: {tool_name}") logger.debug(f"Request data: {json.dumps(request_data, indent=2)}") except Exception as e: logger.error(f"Error logging MCP request: {str(e)}") def log_mcp_response(response_data): """Log an outgoing MCP response.""" try: transaction_id = response_data.get('transaction_id', 'unknown') status = response_data.get('status', 'unknown') logger.info(f"MCP Response [{transaction_id}] - Status: {status}") logger.debug(f"Response data: {json.dumps(response_data, indent=2)}") except Exception as e: logger.error(f"Error logging MCP response: {str(e)}") def log_error(message, error=None): """Log an error.""" try: if error: logger.error(f"{message}: {str(error)}") else: logger.error(message) except Exception as e: # Last resort if logging itself fails print(f"Logging error: {str(e)}") def get_logger(): """Get the configured logger.""" return logger ``` -------------------------------------------------------------------------------- /client/css/styles.css: -------------------------------------------------------------------------------- ```css /* Google Forms MCP Client Styles */ :root { --bg-color: #121212; --panel-bg: #1e1e1e; --panel-border: #333333; --text-color: #e0e0e0; --highlight-color: #00ccff; --secondary-highlight: #00ff9d; --danger-color: #ff4757; --warning-color: #ffa502; --success-color: #2ed573; --muted-color: #747d8c; } body { font-family: 'Segoe UI', 'Helvetica Neue', Arial, sans-serif; background-color: var(--bg-color); color: var(--text-color); display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100vh; margin: 0; padding: 20px; text-align: center; } .container { max-width: 800px; padding: 30px; background-color: var(--panel-bg); border-radius: 8px; border: 1px solid var(--panel-border); box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); } h1 { color: var(--highlight-color); margin-bottom: 10px; } h2 { color: var(--secondary-highlight); font-size: 1.4rem; margin: 20px 0 15px 0; } p { color: var(--muted-color); margin-bottom: 30px; line-height: 1.5; } .button { background-color: var(--highlight-color); color: #000; font-weight: 500; padding: 12px 24px; border: none; border-radius: 4px; text-decoration: none; cursor: pointer; transition: background-color 0.3s; display: inline-block; } .button:hover { background-color: #00a3cc; } .button-secondary { background-color: transparent; border: 1px solid var(--secondary-highlight); color: var(--secondary-highlight); } .button-secondary:hover { background-color: rgba(0, 255, 157, 0.1); } .status { margin-top: 20px; padding: 15px; border-radius: 4px; display: none; } .status.loading { background-color: rgba(255, 165, 2, 0.1); color: var(--warning-color); border: 1px solid var(--warning-color); } .status.success { background-color: rgba(46, 213, 115, 0.1); color: var(--success-color); border: 1px solid var(--success-color); } .status.error { background-color: rgba(255, 71, 87, 0.1); color: var(--danger-color); border: 1px solid var(--danger-color); } .footer { margin-top: 50px; color: var(--muted-color); font-size: 0.8rem; } /* Animation */ @keyframes pulse { 0% { box-shadow: 0 0 0 0 rgba(0, 204, 255, 0.7); } 70% { box-shadow: 0 0 0 10px rgba(0, 204, 255, 0); } 100% { box-shadow: 0 0 0 0 rgba(0, 204, 255, 0); } } .pulse { animation: pulse 1.5s infinite; } ``` -------------------------------------------------------------------------------- /client/js/main.js: -------------------------------------------------------------------------------- ```javascript /** * Google Forms MCP Client * Main JavaScript file for handling server connection and UI updates */ document.addEventListener('DOMContentLoaded', function() { // DOM Elements const connectButton = document.getElementById('connectButton'); const loadingMessage = document.getElementById('loadingMessage'); const errorMessage = document.getElementById('errorMessage'); const serverStatusElement = document.getElementById('serverStatus'); // Server configuration const config = { mcpServerUrl: 'http://localhost:5005', agentServerUrl: 'http://localhost:5006' }; // Initialize function init() { // Add event listeners if (connectButton) { connectButton.addEventListener('click', handleServerConnect); } // Check server status checkServerStatus(); } // Check if MCP server is available async function checkServerStatus() { if (serverStatusElement) { serverStatusElement.textContent = 'Checking...'; serverStatusElement.className = 'checking'; } try { const response = await fetch(`${config.mcpServerUrl}/api/health`); if (response.ok) { const data = await response.json(); if (serverStatusElement) { serverStatusElement.textContent = `Online (v${data.version})`; serverStatusElement.className = 'online'; } return true; } else { if (serverStatusElement) { serverStatusElement.textContent = 'Error'; serverStatusElement.className = 'offline'; } return false; } } catch (error) { console.error('Server status check error:', error); if (serverStatusElement) { serverStatusElement.textContent = 'Offline'; serverStatusElement.className = 'offline'; } return false; } } // Handle server connection button click function handleServerConnect(e) { if (e) { e.preventDefault(); } if (loadingMessage) { loadingMessage.style.display = 'block'; } if (errorMessage) { errorMessage.style.display = 'none'; } // Attempt to connect to the server fetch(`${config.mcpServerUrl}/api/health`) .then(response => { if (response.ok) { window.location.href = config.mcpServerUrl; } else { throw new Error('Server error'); } }) .catch(error => { console.error('Connection error:', error); if (loadingMessage) { loadingMessage.style.display = 'none'; } if (errorMessage) { errorMessage.style.display = 'block'; } }); } // Start the application init(); }); ``` -------------------------------------------------------------------------------- /PROJECT_SUMMARY.md: -------------------------------------------------------------------------------- ```markdown # Google Form MCP Server with CamelAIOrg Agents - Project Summary ## Project Overview This project provides a complete implementation of an **MCP (Model Context Protocol) Server** integrated with the **Google Forms API** and **CamelAIOrg Agents**. The system enables the creation of Google Forms through natural language instructions, with a visually appealing UI that displays the request flow. ## Key Components ### 1. MCP Server (Python/Flask) - Implements the Model Context Protocol - Exposes tools for Google Forms operations - Handles authentication with Google APIs - Processes structured API requests/responses ### 2. CamelAIOrg Agents (Python/Flask) - Processes natural language requests - Extracts intent and parameters - Converts natural language to structured MCP calls - Handles form creation logic ### 3. Frontend UI (HTML/CSS/JavaScript) - Dark-themed modern interface - Real-time flow visualization with animations - MCP packet logging and display - Form result presentation ### 4. Dockerized Deployment - Docker and Docker Compose configuration - Separate containers for server and agents - Environment configuration - Easy one-command deployment ## Feature Highlights ### Natural Language Form Creation Users can create forms with simple instructions like: - "Create a customer feedback form with rating questions" - "Make a survey about remote work preferences" - "Set up an RSVP form for my event" ### Question Type Support The system supports multiple question types: - Text (short answer) - Paragraph (long answer) - Multiple-choice - Checkbox ### Visual Flow Representation The UI visualizes the flow of requests and responses: - Frontend → Agent → MCP Server → Google Forms API - Animated particles showing data movement - Active node highlighting - Error visualization ### MCP Protocol Implementation Full implementation of the Model Context Protocol: - Structured tool definitions - Transaction-based processing - Schema validation - Error handling ### Security Considerations - OAuth2 authentication with Google APIs - Environment-based configuration - Credential management - Input validation ## Technical Achievements 1. **Modular Architecture**: Clean separation between MCP server, agent logic, and UI 2. **Interactive Visualization**: Real-time animation of request/response flows 3. **Agent Intelligence**: Natural language processing for form creation 4. **Protocol Implementation**: Complete MCP protocol implementation 5. **Containerized Deployment**: Docker-based deployment for easy setup ## User Experience The system provides a seamless user experience: 1. User enters a natural language request 2. Request is visualized flowing through the system components 3. Form is created with appropriate questions 4. User receives a link to view/edit the form 5. Questions and responses can be managed ## Future Enhancements Potential areas for future development: 1. Advanced NLP for more complex form requests 2. Additional question types and form features 3. Integration with other Google Workspace products 4. Form templates and preset configurations 5. User authentication and form management ## Conclusion This project demonstrates the power of combining AI agents with structured protocols (MCP) to enable natural language interfaces for productivity tools. The implementation showcases modern web development practices, API integration, and containerized deployment, all while providing an intuitive and visually appealing user interface. ``` -------------------------------------------------------------------------------- /QUICK_START.md: -------------------------------------------------------------------------------- ```markdown # Quick Start Guide Follow these steps to get the Google Forms MCP Server up and running quickly. ## Prerequisites - Docker and Docker Compose installed - Google account with access to Google Forms - Google Cloud Platform project with Forms API enabled ## Setup in 5 Steps ### 1. Clone the Repository ```bash git clone https://github.com/yourusername/google-form-mcp-server.git cd google-form-mcp-server ``` ### 2. Get Google API Credentials If you already have your credentials, proceed to step 3. Otherwise: 1. Go to [Google Cloud Console](https://console.cloud.google.com/) 2. Create a project and enable Google Forms API and Google Drive API 3. Create OAuth 2.0 credentials (Web application type) 4. Use the OAuth 2.0 Playground to get a refresh token: - Go to: https://developers.google.com/oauthplayground/ - Configure with your credentials (⚙️ icon) - Select Forms and Drive API scopes - Authorize and exchange for tokens ### 3. Run the Credentials Setup Script ```bash ./setup_credentials.sh ``` Enter your Google API credentials when prompted. ### 4. Start the Services ```bash ./start.sh ``` This will build and start the Docker containers for the MCP Server and CamelAIOrg Agents. ### 5. Access the Application Open your browser and navigate to: - Server UI: http://localhost:5005 - Manual client: Open `client/index.html` in your browser ## Using the Application 1. Enter natural language instructions like: - "Create a customer feedback form with a rating question" - "Create a survey about remote work preferences" - "Make an RSVP form for my event" 2. Watch the request flow through the system visualization 3. View the generated form details and links ## Troubleshooting - **Server not starting**: Check Docker is running and ports 5005/5006 are available - **Authentication errors**: Verify your credentials in the .env file - **Connection errors**: Ensure your network allows API calls to Google ## What's Next? - Read the full README.md for detailed information - Check DEVELOPER.md for technical documentation - Explore the codebase to understand the implementation <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Google Forms MCP Client</title> <link rel="stylesheet" href="css/styles.css"> </head> <body> <div class="container"> <h1>Google Forms MCP Client</h1> <p>This client connects to the Google Forms MCP Server and CamelAIOrg Agents to create forms using natural language.</p> <div class="server-status"> <h2>Server Status</h2> <p>MCP Server: <span id="serverStatus">Unknown</span></p> </div> <a href="http://localhost:5005" class="button pulse" id="connectButton">Connect to Server</a> <div class="status loading" id="loadingMessage"> Connecting to server... </div> <div class="status error" id="errorMessage"> Could not connect to the server. Please make sure the MCP server is running. </div> <div class="features"> <h2>Features</h2> <ul> <li>Create Google Forms with natural language</li> <li>Multiple question types support</li> <li>View form creation process in real-time</li> <li>Monitor API requests and responses</li> </ul> </div> <div class="footer"> <p>Google Forms MCP Server with CamelAIOrg Agents Integration</p> </div> </div> <script src="js/main.js"></script> </body> </html> ``` -------------------------------------------------------------------------------- /agents/agent_server.py: -------------------------------------------------------------------------------- ```python """ Agent Server for CamelAIOrg Integration with Google Forms MCP This module provides a Flask-based REST API that accepts natural language requests, processes them through the FormAgent, and returns the results. """ from flask import Flask, request, jsonify from flask_cors import CORS import json import os import logging from agent_integration import FormAgent # Configure logging logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' ) logger = logging.getLogger("agent_server") # Create Flask app app = Flask(__name__) CORS(app) # Enable CORS for all routes # Initialize form agent form_agent = FormAgent() # Configuration PORT = int(os.getenv('PORT', 5001)) DEBUG = os.getenv('DEBUG', 'False').lower() == 'true' @app.route('/health', methods=['GET']) def health_check(): """Health check endpoint.""" return jsonify({ "status": "ok", "service": "CamelAIOrg Agent Server", "version": "1.0.0" }) @app.route('/process', methods=['POST']) def process_request(): """ Process a natural language request by passing it to the FormAgent. The FormAgent is responsible for NLP and interacting with the MCP server. Request format: { "request_text": "Create a feedback form with 3 questions" } Response format (on success): { "status": "success", "result": { ... form details ... } } Response format (on error): { "status": "error", "message": "Error description" } """ try: if not request.is_json: return jsonify({ "status": "error", "message": "Request must be JSON" }), 400 data = request.get_json() if 'request_text' not in data: return jsonify({ "status": "error", "message": "Missing required parameter 'request_text'" }), 400 request_text = data['request_text'] logger.info(f"Agent server received request: {request_text}") # Process the request through the FormAgent # The FormAgent's process_request method will handle NLP, # MCP packet creation, and communication with the MCP server. result = form_agent.process_request(request_text) # The agent's response (success or error) is returned directly return jsonify(result) except Exception as e: logger.error(f"Error processing agent request: {str(e)}", exc_info=True) return jsonify({ "status": "error", "message": f"Internal Agent Server Error: {str(e)}" }), 500 @app.route('/schema', methods=['GET']) def get_schema(): """Return the agent capabilities schema.""" schema = { "name": "Google Forms Creator Agent", "description": "Creates Google Forms from natural language requests", "capabilities": [ { "name": "create_form", "description": "Create a new Google Form with questions" }, { "name": "add_question", "description": "Add a question to an existing form" }, { "name": "get_responses", "description": "Get responses from an existing form" } ], "example_requests": [ "Create a customer feedback form with rating questions", "Make a survey about remote work preferences", "Set up an RSVP form for my event" ] } return jsonify({ "status": "success", "schema": schema }) # Run the Flask app if __name__ == '__main__': logger.info(f"Starting CamelAIOrg Agent Server on port {PORT}") logger.info(f"Debug mode: {DEBUG}") app.run(host='0.0.0.0', port=PORT, debug=DEBUG) ``` -------------------------------------------------------------------------------- /server/app.py: -------------------------------------------------------------------------------- ```python from flask import Flask, request, jsonify, render_template from flask_cors import CORS import json import os import requests # Import requests library from mcp_handler import MCPHandler from utils.logger import log_mcp_request, log_mcp_response, log_error, get_logger import config from forms_api import GoogleFormsAPI # Initialize Flask application app = Flask(__name__) CORS(app) # Enable CORS for all routes # Initialize the MCP handler mcp_handler = MCPHandler() logger = get_logger() forms_api = GoogleFormsAPI() @app.route('/') def index(): """Render the main page of the application.""" return render_template('index.html') @app.route('/api/schema', methods=['GET']) def get_schema(): """Return the MCP tools schema.""" try: schema = mcp_handler.get_tools_schema() return jsonify({ "status": "success", "tools": schema, "version": config.MCP_VERSION }) except Exception as e: log_error("Error returning schema", e) return jsonify({ "status": "error", "message": str(e) }), 500 @app.route('/api/process', methods=['POST']) def process_mcp_request(): """Process an MCP request.""" try: if not request.is_json: return jsonify({ "status": "error", "message": "Request must be JSON" }), 400 request_data = request.get_json() log_mcp_request(request_data) response = mcp_handler.process_request(request_data) log_mcp_response(response) return jsonify(response) except Exception as e: log_error("Error processing MCP request", e) return jsonify({ "status": "error", "message": str(e) }), 500 @app.route('/api/health', methods=['GET']) def health_check(): """Health check endpoint.""" return jsonify({ "status": "ok", "version": config.MCP_VERSION }) # WebSocket for real-time UI updates @app.route('/ws') def websocket(): """WebSocket endpoint for real-time updates.""" return render_template('websocket.html') @app.route('/api/forms', methods=['POST']) def forms_api(): """Handle form operations.""" try: data = request.json action = data.get('action') logger.info(f"Received form API request: {action}") if action == 'create_form': title = data.get('title', 'Untitled Form') description = data.get('description', '') logger.info(f"Creating form with title: {title}") result = forms_api.create_form(title, description) logger.info(f"Form created with ID: {result.get('form_id')}") logger.info(f"Form response URL: {result.get('response_url')}") logger.info(f"Form edit URL: {result.get('edit_url')}") return jsonify({"status": "success", "result": result}) return jsonify({"status": "error", "message": f"Unknown action: {action}"}) except Exception as e: logger.error(f"Error in forms API: {str(e)}") return jsonify({"status": "error", "message": str(e)}), 500 @app.route('/api/agent_proxy', methods=['POST']) def agent_proxy(): """Proxy requests from the frontend to the agent server.""" try: frontend_data = request.get_json() if not frontend_data or 'request_text' not in frontend_data: log_error("Agent proxy received invalid data from frontend", frontend_data) return jsonify({"status": "error", "message": "Invalid request data"}), 400 agent_url = config.AGENT_ENDPOINT if not agent_url: log_error("Agent endpoint URL is not configured.", None) return jsonify({"status": "error", "message": "Agent service URL not configured."}), 500 logger.info(f"Proxying request to agent at {agent_url}: {frontend_data}") # Forward the request to the agent server # Note: Consider adding timeout and error handling for the requests.post call agent_response = requests.post( agent_url, json=frontend_data, headers={'Content-Type': 'application/json'} # Potentially add API key header if agent requires it: # headers={"Authorization": f"Bearer {config.AGENT_API_KEY}"} ) # Check agent response status agent_response.raise_for_status() # Raise HTTPError for bad responses (4xx or 5xx) agent_data = agent_response.json() logger.info(f"Received response from agent: {agent_data}") # Return the agent's response directly to the frontend return jsonify(agent_data) except requests.exceptions.RequestException as e: log_error(f"Error communicating with agent server at {config.AGENT_ENDPOINT}", e) return jsonify({"status": "error", "message": f"Failed to reach agent service: {str(e)}"}), 502 # Bad Gateway except Exception as e: log_error("Error in agent proxy endpoint", e) return jsonify({"status": "error", "message": f"Internal proxy error: {str(e)}"}), 500 if __name__ == '__main__': port = config.PORT debug = config.DEBUG logger.info(f"Starting Google Forms MCP Server on port {port}") logger.info(f"Debug mode: {debug}") app.run(host='0.0.0.0', port=port, debug=debug) ``` -------------------------------------------------------------------------------- /server/templates/index.html: -------------------------------------------------------------------------------- ```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Google Forms MCP Server</title> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css"> <link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}"> </head> <body> <div class="container-fluid"> <header class="header"> <div class="logo-container"> <h1>Google Forms MCP Server</h1> <p class="subtitle">Integrated with CamelAIOrg Agents</p> </div> <div class="status-indicator"> <span id="statusDot" class="status-dot"></span> <span id="statusText">Connecting...</span> </div> </header> <div class="row main-content"> <!-- Left Panel: Form Controls --> <div class="col-md-4"> <div class="panel"> <h2>Form Request</h2> <div class="form-group"> <label for="requestInput">Natural Language Request:</label> <textarea id="requestInput" class="form-control" rows="4" placeholder="Create a feedback form with 3 questions about customer service..."></textarea> </div> <button id="sendRequestBtn" class="btn btn-primary">Process Request</button> <div class="demo-actions mt-4"> <h3>Quick Demo Actions</h3> <button class="btn btn-outline-secondary demo-btn" data-request="Create a customer feedback form with a rating question from 1-5 and a text question for additional comments">Feedback Form</button> <button class="btn btn-outline-secondary demo-btn" data-request="Create a survey about remote work preferences with 3 multiple choice questions">Work Survey</button> <button class="btn btn-outline-secondary demo-btn" data-request="Create an event RSVP form with name, email and attendance options">RSVP Form</button> </div> </div> </div> <!-- Middle Panel: Flow Visualization --> <div class="col-md-4"> <div class="panel flow-panel"> <h2>Request Flow</h2> <div class="stage-indicator"> Current: <span id="stageIndicator">Ready for request</span> </div> <div class="flow-container"> <div class="node" id="frontendNode"> <div class="node-icon"> <i class="node-glow"></i> </div> <span class="node-label">Frontend</span> </div> <div class="flow-line" id="frontendToAgent"> <div class="flow-particle-container"></div> </div> <div class="node" id="agentNode"> <div class="node-icon"> <i class="node-glow"></i> </div> <span class="node-label">CamelAIOrg Agent</span> </div> <div class="flow-line" id="agentToMCP"> <div class="flow-particle-container"></div> </div> <div class="node" id="mcpNode"> <div class="node-icon"> <i class="node-glow"></i> </div> <span class="node-label">MCP Server</span> </div> <div class="flow-line" id="mcpToGoogle"> <div class="flow-particle-container"></div> </div> <div class="node" id="googleNode"> <div class="node-icon"> <i class="node-glow"></i> </div> <span class="node-label">Google Forms</span> </div> </div> </div> </div> <!-- Right Panel: MCP Packet & Results --> <div class="col-md-4"> <div class="panel result-panel"> <h2>Results</h2> <div id="formResult" class="hidden"> <h3 id="formTitle">Form Title</h3> <div class="form-links"> <a id="formViewLink" href="#" target="_blank" class="btn btn-sm btn-outline-info">View Form</a> <a id="formEditLink" href="#" target="_blank" class="btn btn-sm btn-outline-warning">Edit Form</a> </div> <div id="formQuestions" class="mt-3"> <h4>Questions:</h4> <ul id="questionsList" class="list-group"> <!-- Questions will be added here --> </ul> </div> </div> <div id="mcp-log"> <h3>MCP Packet Log</h3> <div id="packetLog" class="log-container"> <!-- Packet logs will be added here --> </div> </div> </div> </div> </div> </div> <!-- Templates for dynamic content --> <template id="packetTemplate"> <div class="packet-entry"> <div class="packet-header"> <span class="transaction-id"></span> <span class="packet-time"></span> <span class="packet-direction"></span> </div> <pre class="packet-content"></pre> </div> </template> <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script> <script src="{{ url_for('static', filename='animations.js') }}"></script> <script src="{{ url_for('static', filename='main.js') }}"></script> </body> </html> ``` -------------------------------------------------------------------------------- /DEVELOPER.md: -------------------------------------------------------------------------------- ```markdown # Google Forms MCP Server - Developer Guide This document provides technical details for developers who want to understand, modify, or extend the Google Forms MCP Server and CamelAIOrg Agents integration. ## Architecture Overview The system is built on a modular architecture with the following key components: 1. **MCP Server (Flask)**: Implements the Model Context Protocol and interfaces with Google Forms API 2. **CamelAIOrg Agents (Flask)**: Processes natural language and converts it to structured MCP calls 3. **Frontend UI (HTML/CSS/JS)**: Visualizes the flow and manages user interactions 4. **Google Forms API**: External service for form creation and management ### Component Communication ``` Frontend → Agents → MCP Server → Google Forms API ``` Each component communicates via HTTP requests, with MCP packets being the primary data format for the MCP Server. ## MCP Protocol Specification The MCP (Model Context Protocol) is designed to standardize tool usage between AI agents and external services. Our implementation follows these guidelines: ### Request Format ```json { "transaction_id": "unique_id", "tool_name": "tool_name", "parameters": { "param1": "value1", "param2": "value2" } } ``` ### Response Format ```json { "transaction_id": "unique_id", "status": "success|error", "result": { "key1": "value1", "key2": "value2" }, "error": { "message": "Error message" } } ``` ## Google Forms API Integration The integration with Google Forms API is handled by the `GoogleFormsAPI` class in `forms_api.py`. Key methods include: - `create_form(title, description)`: Creates a new form - `add_question(form_id, question_type, title, options, required)`: Adds a question - `get_responses(form_id)`: Retrieves form responses ### Authentication Flow 1. OAuth2 credentials are stored in environment variables 2. Credentials are loaded and used to create a Google API client 3. API requests are authenticated using these credentials ## CamelAIOrg Agent Implementation The agent implementation in `agent_integration.py` provides the natural language processing interface. Key methods: - `process_request(request_text)`: Main entry point for NL requests - `_analyze_request(request_text)`: Analyzes and extracts intent from text - `_handle_create_form(params)`: Creates a form based on extracted parameters ## Frontend Visualization The UI uses a combination of CSS and JavaScript to visualize the request flow: - **Flow Lines**: Represent the path between components - **Particles**: Animated elements that travel along flow lines - **Nodes**: Represent each component (Frontend, Agents, MCP, Google) - **MCP Packet Log**: Shows the actual MCP packets being exchanged ### Animation System The animation system in `animations.js` provides these key features: - `animateFlow(fromNode, toNode, direction)`: Animates flow between nodes - `animateRequestResponseFlow()`: Animates a complete request-response cycle - `animateErrorFlow(errorStage)`: Visualizes errors at different stages ## Extending the System ### Adding New Question Types To add a new question type: 1. Update the `add_question` method in `forms_api.py` 2. Add the new type to the validation in `_handle_add_question` in `mcp_handler.py` 3. Update the UI logic in `main.js` to handle the new question type ### Adding New MCP Tools To add a new MCP tool: 1. Add the tool name to `MCP_TOOLS` in `config.py` 2. Add tool schema to `get_tools_schema` in `mcp_handler.py` 3. Create a handler method `_handle_tool_name` in `mcp_handler.py` 4. Implement the underlying functionality in `forms_api.py` ### Enhancing Agent Capabilities To improve the agent's language processing: 1. Enhance the `_analyze_request` method in `agent_integration.py` 2. Add new intents and parameters recognition 3. Adjust the question generation logic based on request analysis ## Testing ### Unit Testing Test individual components: ```bash # Test MCP handler python -m unittest tests/test_mcp_handler.py # Test Google Forms API python -m unittest tests/test_forms_api.py # Test agent integration python -m unittest tests/test_agent_integration.py ``` ### Integration Testing Test the entire system: ```bash # Start the servers docker-compose up -d # Run integration tests python -m unittest tests/test_integration.py ``` ## Performance Considerations - **Caching**: Implement caching for frequently accessed forms data - **Rate Limiting**: Be aware of Google Forms API rate limits - **Error Handling**: Implement robust error handling and retry logic - **Load Testing**: Use tools like Locust to test system performance ## Security Best Practices - **Credential Management**: Never commit credentials to version control - **Input Validation**: Validate all user input and MCP packets - **CORS Configuration**: Configure CORS appropriately for production - **Rate Limiting**: Implement rate limiting to prevent abuse - **Auth Tokens**: Use proper authentication for production deployments ## Deployment ### Production Deployment For production deployment: 1. Use a proper container orchestration system (Kubernetes, ECS) 2. Set up a reverse proxy (Nginx, Traefik) for TLS termination 3. Use a managed database for persistent data 4. Implement proper monitoring and logging 5. Set up CI/CD pipelines for automated testing and deployment ### Environment-Specific Configuration Create environment-specific configuration: - `config.dev.py` - Development settings - `config.test.py` - Testing settings - `config.prod.py` - Production settings ## Troubleshooting Common issues and solutions: 1. **Google API Authentication Errors**: - Verify credentials in `.env` file - Check that required API scopes are included - Ensure refresh token is valid 2. **Docker Network Issues**: - Make sure services can communicate on the network - Check port mappings in `docker-compose.yml` 3. **UI Animation Issues**: - Check browser console for JavaScript errors - Verify DOM element IDs match expected values 4. **MCP Protocol Errors**: - Validate request format against MCP schema - Check transaction IDs are being properly passed ## Contributing Please follow these guidelines when contributing: 1. Create a feature branch from `main` 2. Follow the existing code style and conventions 3. Write unit tests for new functionality 4. Document new features or changes 5. Submit a pull request with a clear description of changes ``` -------------------------------------------------------------------------------- /server/mcp_handler.py: -------------------------------------------------------------------------------- ```python import json import uuid from forms_api import GoogleFormsAPI import config class MCPHandler: """ Handler for Model Context Protocol (MCP) packets. Processes incoming MCP requests for Google Forms operations and returns appropriately formatted MCP responses. """ def __init__(self): """Initialize the MCP handler with a GoogleFormsAPI instance.""" self.forms_api = GoogleFormsAPI() self.version = config.MCP_VERSION self.tools = config.MCP_TOOLS def get_tools_schema(self): """Return the schema for all available tools.""" return { "create_form": { "description": "Creates a new Google Form", "parameters": { "title": { "type": "string", "description": "The title of the form" }, "description": { "type": "string", "description": "Optional description for the form" } }, "required": ["title"] }, "add_question": { "description": "Adds a question to an existing Google Form", "parameters": { "form_id": { "type": "string", "description": "The ID of the form to add the question to" }, "question_type": { "type": "string", "description": "The type of question (text, paragraph, multiple_choice, checkbox)", "enum": ["text", "paragraph", "multiple_choice", "checkbox"] }, "title": { "type": "string", "description": "The question title/text" }, "options": { "type": "array", "description": "Options for multiple choice or checkbox questions", "items": { "type": "string" } }, "required": { "type": "boolean", "description": "Whether the question is required", "default": False } }, "required": ["form_id", "question_type", "title"] }, "get_responses": { "description": "Gets responses for a Google Form", "parameters": { "form_id": { "type": "string", "description": "The ID of the form to get responses for" } }, "required": ["form_id"] } } def process_request(self, request_data): """ Process an incoming MCP request. Args: request_data: Dict containing the MCP request data Returns: dict: MCP response packet """ try: # Extract MCP request components transaction_id = request_data.get('transaction_id', str(uuid.uuid4())) tool_name = request_data.get('tool_name') parameters = request_data.get('parameters', {}) # Validate tool name if tool_name not in self.tools: return self._create_error_response( transaction_id, f"Unknown tool '{tool_name}'. Available tools: {', '.join(self.tools)}" ) # Process the request based on the tool name if tool_name == "create_form": return self._handle_create_form(transaction_id, parameters) elif tool_name == "add_question": return self._handle_add_question(transaction_id, parameters) elif tool_name == "get_responses": return self._handle_get_responses(transaction_id, parameters) # Shouldn't reach here due to validation above return self._create_error_response(transaction_id, f"Tool '{tool_name}' not implemented") except Exception as e: return self._create_error_response( request_data.get('transaction_id', str(uuid.uuid4())), f"Error processing request: {str(e)}" ) def _handle_create_form(self, transaction_id, parameters): """Handle a create_form MCP request.""" if 'title' not in parameters: return self._create_error_response(transaction_id, "Missing required parameter 'title'") title = parameters['title'] description = parameters.get('description', "") result = self.forms_api.create_form(title, description) return { "transaction_id": transaction_id, "status": "success", "result": result } def _handle_add_question(self, transaction_id, parameters): """Handle an add_question MCP request.""" # Validate required parameters required_params = ['form_id', 'question_type', 'title'] for param in required_params: if param not in parameters: return self._create_error_response(transaction_id, f"Missing required parameter '{param}'") # Extract parameters form_id = parameters['form_id'] question_type = parameters['question_type'] title = parameters['title'] options = parameters.get('options', []) required = parameters.get('required', False) # Validate question type valid_types = ['text', 'paragraph', 'multiple_choice', 'checkbox'] if question_type not in valid_types: return self._create_error_response( transaction_id, f"Invalid question_type '{question_type}'. Valid types: {', '.join(valid_types)}" ) # Validate options for choice questions if question_type in ['multiple_choice', 'checkbox'] and not options: return self._create_error_response( transaction_id, f"Options are required for '{question_type}' questions" ) result = self.forms_api.add_question(form_id, question_type, title, options, required) return { "transaction_id": transaction_id, "status": "success", "result": result } def _handle_get_responses(self, transaction_id, parameters): """Handle a get_responses MCP request.""" if 'form_id' not in parameters: return self._create_error_response(transaction_id, "Missing required parameter 'form_id'") form_id = parameters['form_id'] result = self.forms_api.get_responses(form_id) return { "transaction_id": transaction_id, "status": "success", "result": result } def _create_error_response(self, transaction_id, error_message): """Create an MCP error response.""" return { "transaction_id": transaction_id, "status": "error", "error": { "message": error_message } } ``` -------------------------------------------------------------------------------- /server/static/styles.css: -------------------------------------------------------------------------------- ```css /* Global Styles */ :root { --bg-color: #121212; --panel-bg: #1e1e1e; --panel-border: #333333; --text-color: #e0e0e0; --highlight-color: #00ccff; --secondary-highlight: #00ff9d; --danger-color: #ff4757; --warning-color: #ffa502; --success-color: #2ed573; --muted-color: #747d8c; --flow-line-color: #333333; --flow-node-border: #444444; --frontend-color: #00ccff; --agent-color: #00ff9d; --mcp-color: #ff9f43; --google-color: #ff6b81; } body { background-color: var(--bg-color); color: var(--text-color); font-family: 'Segoe UI', 'Helvetica Neue', Arial, sans-serif; margin: 0; padding: 0; min-height: 100vh; } .container-fluid { padding: 20px; } /* Header Styles */ .header { display: flex; justify-content: space-between; align-items: center; padding: 15px 0; margin-bottom: 20px; border-bottom: 1px solid var(--panel-border); } .logo-container h1 { margin: 0; font-size: 1.8rem; color: var(--highlight-color); } .subtitle { color: var(--muted-color); margin-top: 5px; } .status-indicator { display: flex; align-items: center; } .status-dot { width: 10px; height: 10px; border-radius: 50%; background-color: var(--warning-color); margin-right: 8px; display: inline-block; transition: background-color 0.3s ease; } .status-dot.connected { background-color: var(--success-color); } .status-dot.error { background-color: var(--danger-color); } /* Panel Styles */ .panel { background-color: var(--panel-bg); border-radius: 8px; border: 1px solid var(--panel-border); padding: 20px; margin-bottom: 20px; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); height: calc(100vh - 150px); overflow-y: auto; } .panel h2 { color: var(--secondary-highlight); margin-top: 0; margin-bottom: 20px; font-size: 1.4rem; border-bottom: 1px solid var(--panel-border); padding-bottom: 10px; } /* Form Controls */ .form-group { margin-bottom: 20px; } .form-control { background-color: #2a2a2a; border: 1px solid var(--panel-border); color: var(--text-color); border-radius: 4px; } .form-control:focus { background-color: #2a2a2a; border-color: var(--highlight-color); color: var(--text-color); box-shadow: 0 0 0 0.25rem rgba(0, 204, 255, 0.25); } .btn-primary { background-color: var(--highlight-color); border-color: var(--highlight-color); color: #000; font-weight: 500; } .btn-primary:hover { background-color: #00a3cc; border-color: #00a3cc; color: #000; } .btn-outline-secondary { color: var(--text-color); border-color: var(--panel-border); } .btn-outline-secondary:hover { background-color: #2a2a2a; color: var(--highlight-color); border-color: var(--highlight-color); } .btn-outline-info, .btn-outline-warning { color: var(--text-color); } .demo-actions { margin-top: 30px; } .demo-actions h3 { font-size: 1.1rem; margin-bottom: 15px; color: var(--muted-color); } .demo-btn { margin-bottom: 10px; display: block; width: 100%; text-align: left; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } /* Flow Visualization */ .flow-panel { display: flex; flex-direction: column; align-items: center; justify-content: center; padding-top: 20px; } .flow-container { display: flex; flex-direction: column; align-items: center; justify-content: center; width: 100%; padding: 20px 0; position: relative; } .node { display: flex; flex-direction: column; align-items: center; margin: 10px 0; position: relative; z-index: 2; transition: transform 0.3s ease, filter 0.3s ease; } .node:hover { transform: scale(1.05); } .node-icon { display: flex; align-items: center; justify-content: center; width: 50px; height: 50px; border-radius: 50%; background: var(--panel-bg); border: 2px solid var(--flow-node-border); position: relative; transition: all 0.3s ease; overflow: hidden; } /* Add highlighted state */ .node.highlighted .node-icon { transform: scale(1.1); border-width: 3px; } /* Active state */ .node.active .node-icon { border-width: 3px; animation: pulse 1.5s infinite; } #frontendNode .node-icon { border-color: var(--frontend-color); } #agentNode .node-icon { border-color: var(--agent-color); } #mcpNode .node-icon { border-color: var(--mcp-color); } #googleNode .node-icon { border-color: var(--google-color); } #frontendNode.active .node-icon { border-color: var(--frontend-color); box-shadow: 0 0 15px var(--frontend-color); } #agentNode.active .node-icon { border-color: var(--agent-color); box-shadow: 0 0 15px var(--agent-color); } #mcpNode.active .node-icon { border-color: var(--mcp-color); box-shadow: 0 0 15px var(--mcp-color); } #googleNode.active .node-icon { border-color: var(--google-color); box-shadow: 0 0 15px var(--google-color); } .node-glow { position: absolute; width: 50px; height: 50px; border-radius: 50%; background: transparent; z-index: 1; } .node-label { margin-top: 8px; font-size: 0.9rem; color: var(--muted-color); transition: color 0.3s ease; } .active .node-label { color: var(--text-color); font-weight: bold; } .highlighted .node-label { color: var(--highlight-color); } .flow-line { width: 3px; height: 80px; background-color: var(--flow-line-color); position: relative; z-index: 1; transition: all 0.3s ease; } .flow-line.active { background-color: var(--highlight-color); box-shadow: 0 0 10px 2px var(--highlight-color); animation: lineGlow 1.5s infinite; } .flow-particle-container { position: absolute; top: 0; left: -2px; width: 7px; height: 100%; overflow: visible; } .flow-particle { border-radius: 50%; position: absolute; transition: top 0.8s ease-out, opacity 0.8s ease-out; } /* Error state */ .node.error .node-icon { border-color: var(--danger-color); box-shadow: 0 0 15px var(--danger-color); animation: errorPulse 1.5s infinite; } /* Result Panel */ .result-panel { height: calc(100vh - 150px); display: flex; flex-direction: column; } .form-links { margin: 15px 0; } .form-links a { margin-right: 10px; } #formResult { background-color: #252525; border-radius: 6px; padding: 15px; margin-bottom: 20px; } #formResult.hidden { display: none; } #formTitle { font-size: 1.2rem; margin-top: 0; color: var(--secondary-highlight); } .log-container { background-color: #1a1a1a; border-radius: 6px; padding: 10px; height: 100%; overflow-y: auto; font-family: 'Consolas', monospace; font-size: 0.8rem; } .packet-entry { margin-bottom: 15px; border-bottom: 1px solid var(--panel-border); padding-bottom: 10px; } .packet-header { display: flex; justify-content: space-between; margin-bottom: 5px; font-size: 0.7rem; } .transaction-id { color: var(--highlight-color); } .packet-time { color: var(--muted-color); } .packet-direction { font-weight: bold; } .packet-direction.request { color: var(--secondary-highlight); } .packet-direction.response { color: var(--warning-color); } .packet-content { margin: 0; white-space: pre-wrap; color: #bbb; font-size: 0.7rem; max-height: 200px; overflow-y: auto; } /* Animations */ @keyframes pulse { 0% { transform: scale(1); box-shadow: 0 0 0 0 rgba(0, 204, 255, 0.7); } 50% { transform: scale(1.05); box-shadow: 0 0 0 10px rgba(0, 204, 255, 0); } 100% { transform: scale(1); box-shadow: 0 0 0 0 rgba(0, 204, 255, 0.7); } } @keyframes errorPulse { 0% { transform: scale(1); box-shadow: 0 0 0 0 rgba(255, 71, 87, 0.7); } 50% { transform: scale(1.05); box-shadow: 0 0 0 10px rgba(255, 71, 87, 0); } 100% { transform: scale(1); box-shadow: 0 0 0 0 rgba(255, 71, 87, 0.7); } } @keyframes lineGlow { 0% { opacity: 0.7; box-shadow: 0 0 5px 1px var(--highlight-color); } 50% { opacity: 1; box-shadow: 0 0 10px 2px var(--highlight-color); } 100% { opacity: 0.7; box-shadow: 0 0 5px 1px var(--highlight-color); } } .active .node-icon { border-color: var(--highlight-color); } .active .node-glow { animation: glow 1.5s infinite; } .node.pulse { animation: nodePulse 1s infinite; } /* List group customization */ .list-group-item { background-color: #252525; color: var(--text-color); border-color: var(--panel-border); } .packet-log-entry .packet-direction.agent-step { background-color: #e8f0fe; /* Light blue background */ color: #1a73e8; /* Google blue text */ border: 1px solid #d2e3fc; } .packet-log-entry .packet-direction.user-request { background-color: #fef7e0; /* Light yellow */ color: #ea8600; /* Amber */ border: 1px solid #fcefc9; } .packet-log-entry .packet-direction.error-log { background-color: #fce8e6; /* Light red */ color: #d93025; /* Google red */ border: 1px solid #f9d6d3; } /* Add styles for the stage indicator */ .stage-indicator { background-color: #252525; border-radius: 4px; padding: 10px; margin-bottom: 15px; text-align: center; border: 1px solid var(--panel-border); } #stageIndicator { font-weight: 500; color: var(--highlight-color); } /* Add styles for highlighted nodes */ .node.highlighted { transform: scale(1.08); } .node.highlighted .node-icon { border-width: 3px; border-color: var(--highlight-color); box-shadow: 0 0 10px var(--highlight-color); } .node.highlighted .node-label { color: var(--highlight-color); font-weight: 500; } ``` -------------------------------------------------------------------------------- /server/static/animations.js: -------------------------------------------------------------------------------- ```javascript /** * Flow Animation Module for Google Forms MCP Server * Manages the visual animations of data flowing between components */ class FlowAnimator { constructor() { // Flow nodes this.nodes = { frontend: document.getElementById('frontendNode'), agent: document.getElementById('agentNode'), mcp: document.getElementById('mcpNode'), google: document.getElementById('googleNode') }; // Flow lines this.lines = { frontendToAgent: document.getElementById('frontendToAgent'), agentToMCP: document.getElementById('agentToMCP'), mcpToGoogle: document.getElementById('mcpToGoogle') }; // Particle colors this.colors = { outgoing: '#00ccff', // Cyan for outgoing requests incoming: '#00ff9d', // Green for incoming responses error: '#ff4757' // Red for errors }; // Animation state this.activeAnimations = []; this.isAnimating = false; this.currentActiveNode = null; this.currentActiveLine = null; // Animation intervals for continuous particle generation this.particleIntervals = {}; } /** * Activate a node with a glowing effect * @param {string} nodeName - The name of the node to activate */ activateNode(nodeName) { if (this.nodes[nodeName]) { // Deactivate previous node if exists if (this.currentActiveNode && this.nodes[this.currentActiveNode]) { this.nodes[this.currentActiveNode].classList.remove('active'); } // Add active class to the node this.nodes[nodeName].classList.add('active'); this.currentActiveNode = nodeName; // Return a cleanup function return () => { // Only remove active class if this is still the current active node if (this.currentActiveNode === nodeName) { this.nodes[nodeName].classList.remove('active'); this.currentActiveNode = null; } }; } return () => {}; } /** * Activate a flow line * @param {string} lineName - The name of the line to activate */ activateLine(lineName) { if (this.lines[lineName]) { // Deactivate previous line if exists if (this.currentActiveLine && this.lines[this.currentActiveLine]) { this.lines[this.currentActiveLine].classList.remove('active'); } // Add active class to the line this.lines[lineName].classList.add('active'); this.currentActiveLine = lineName; // Return a cleanup function return () => { // Only remove active class if this is still the current active line if (this.currentActiveLine === lineName) { this.lines[lineName].classList.remove('active'); this.currentActiveLine = null; } }; } return () => {}; } /** * Create and animate a particle flowing through a line * @param {string} lineName - The name of the line to animate * @param {string} direction - 'outgoing' or 'incoming' to determine color and direction * @param {number} duration - Animation duration in milliseconds */ createParticle(lineName, direction, duration = 800) { const line = this.lines[lineName]; if (!line) return null; const container = line.querySelector('.flow-particle-container'); if (!container) return null; // Create particle element const particle = document.createElement('div'); particle.className = 'flow-particle'; particle.style.position = 'absolute'; particle.style.width = '7px'; particle.style.height = '7px'; particle.style.borderRadius = '50%'; particle.style.backgroundColor = this.colors[direction] || this.colors.outgoing; particle.style.boxShadow = `0 0 8px 2px ${this.colors[direction] || this.colors.outgoing}`; // Set starting position based on direction if (direction === 'incoming') { particle.style.top = 'calc(100% - 7px)'; particle.style.transform = 'translateY(0)'; } else { particle.style.top = '0'; particle.style.transform = 'translateY(0)'; } // Add particle to container container.appendChild(particle); // Animate the particle const animation = particle.animate([ { top: direction === 'incoming' ? 'calc(100% - 7px)' : '0', opacity: 1 }, { top: direction === 'incoming' ? '0' : 'calc(100% - 7px)', opacity: 0.8 } ], { duration: duration, easing: 'ease-out', fill: 'forwards' }); // Remove particle when animation completes animation.onfinish = () => { if (container.contains(particle)) { container.removeChild(particle); } }; return animation; } /** * Start continuous particle animation on a line * @param {string} lineName - The line to animate * @param {string} direction - Direction of flow */ startContinuousParticles(lineName, direction) { // Clear any existing interval for this line this.stopContinuousParticles(lineName); // Create new interval const interval = setInterval(() => { this.createParticle(lineName, direction, 800); }, 200); // Store the interval this.particleIntervals[lineName] = interval; } /** * Stop continuous particle animation on a line * @param {string} lineName - The line to stop animating */ stopContinuousParticles(lineName) { if (this.particleIntervals[lineName]) { clearInterval(this.particleIntervals[lineName]); delete this.particleIntervals[lineName]; // Clear any remaining particles const line = this.lines[lineName]; if (line) { const container = line.querySelector('.flow-particle-container'); if (container) { container.innerHTML = ''; } } } } /** * Stop all continuous particle animations */ stopAllContinuousParticles() { Object.keys(this.particleIntervals).forEach(lineName => { this.stopContinuousParticles(lineName); }); } /** * Animate flow from one node to another * @param {string} fromNode - Source node name * @param {string} toNode - Target node name * @param {string} direction - 'outgoing' or 'incoming' * @returns {Promise} - Resolves when animation completes */ async animateFlow(fromNode, toNode, direction = 'outgoing') { // Define flow paths const flowPaths = { 'frontend-agent': 'frontendToAgent', 'agent-mcp': 'agentToMCP', 'mcp-google': 'mcpToGoogle', 'google-mcp': 'mcpToGoogle', 'mcp-agent': 'agentToMCP', 'agent-frontend': 'frontendToAgent' }; const pathKey = `${fromNode}-${toNode}`; const lineName = flowPaths[pathKey]; if (!lineName) { console.error(`No flow path defined for ${pathKey}`); return; } // Stop any existing continuous animations this.stopAllContinuousParticles(); // Activate source node const cleanupSource = this.activateNode(fromNode); // Activate the flow line const cleanupLine = this.activateLine(lineName); // Start continuous particles this.startContinuousParticles(lineName, direction); // Create promise that resolves when animation completes return new Promise(resolve => { setTimeout(() => { // Activate target node const cleanupTarget = this.activateNode(toNode); // Stop continuous particles this.stopContinuousParticles(lineName); // Cleanup source node after delay cleanupSource(); // Cleanup line cleanupLine(); // Resolve the promise resolve(); }, 1500); // Longer wait to show the flow more clearly }); } /** * Animate a complete request-response flow * @param {string} scenario - The flow scenario to animate */ async animateRequestResponseFlow(scenario = 'form-creation') { // Prevent multiple animations if (this.isAnimating) return; this.isAnimating = true; try { // Common flow for all scenarios // Frontend -> Agent -> MCP -> Google -> MCP -> Agent -> Frontend // Request flow await this.animateFlow('frontend', 'agent', 'outgoing'); await this.animateFlow('agent', 'mcp', 'outgoing'); await this.animateFlow('mcp', 'google', 'outgoing'); // Response flow await this.animateFlow('google', 'mcp', 'incoming'); await this.animateFlow('mcp', 'agent', 'incoming'); await this.animateFlow('agent', 'frontend', 'incoming'); } catch (error) { console.error('Animation error:', error); } finally { this.isAnimating = false; } } /** * Animate a flow with an error * @param {string} errorStage - The stage where the error occurs */ async animateErrorFlow(errorStage = 'google') { if (this.isAnimating) return; this.isAnimating = true; try { // Initial flow await this.animateFlow('frontend', 'agent', 'outgoing'); if (errorStage === 'agent') { // Error at agent this.nodes.agent.classList.add('error'); setTimeout(() => { this.nodes.agent.classList.remove('error'); this.nodes.agent.classList.remove('active'); }, 2000); return; } await this.animateFlow('agent', 'mcp', 'outgoing'); if (errorStage === 'mcp') { // Error at MCP server this.nodes.mcp.classList.add('error'); setTimeout(() => { this.nodes.mcp.classList.remove('error'); this.nodes.mcp.classList.remove('active'); }, 2000); return; } await this.animateFlow('mcp', 'google', 'outgoing'); if (errorStage === 'google') { // Error at Google Forms API this.nodes.google.classList.add('error'); setTimeout(() => { this.nodes.google.classList.remove('error'); this.nodes.google.classList.remove('active'); // Error response flow this.animateFlow('google', 'mcp', 'error'); this.animateFlow('mcp', 'agent', 'error'); this.animateFlow('agent', 'frontend', 'error'); }, 2000); } } catch (error) { console.error('Error animation error:', error); } finally { setTimeout(() => { this.isAnimating = false; }, 3000); } } /** * Reset all animations and active states */ resetAll() { // Stop all continuous particles this.stopAllContinuousParticles(); // Reset nodes Object.values(this.nodes).forEach(node => { node.classList.remove('active'); node.classList.remove('error'); node.classList.remove('pulse'); }); // Reset lines Object.values(this.lines).forEach(line => { line.classList.remove('active'); const container = line.querySelector('.flow-particle-container'); if (container) { container.innerHTML = ''; } }); this.currentActiveNode = null; this.currentActiveLine = null; this.isAnimating = false; } /** * Pulse animation for a specific node * @param {string} nodeName - Name of the node to pulse * @param {number} duration - Duration in milliseconds */ pulseNode(nodeName, duration = 2000) { const node = this.nodes[nodeName]; if (!node) return; node.classList.add('pulse'); setTimeout(() => { node.classList.remove('pulse'); }, duration); } /** * Highlight a specific node to show it's the current active component * @param {string} nodeName - Name of the node to highlight */ highlightNode(nodeName) { // First clear any existing highlights Object.keys(this.nodes).forEach(name => { this.nodes[name].classList.remove('highlighted'); }); // Set the new highlight if (this.nodes[nodeName]) { this.nodes[nodeName].classList.add('highlighted'); } } } // Initialize flow animator when document loads document.addEventListener('DOMContentLoaded', function() { window.flowAnimator = new FlowAnimator(); // For demo purposes, animate the request flow on load to demonstrate functionality setTimeout(() => { if (window.flowAnimator) { window.flowAnimator.animateRequestResponseFlow(); } }, 2000); }); ``` -------------------------------------------------------------------------------- /server/static/main.js: -------------------------------------------------------------------------------- ```javascript /** * Google Forms MCP Server * Main JavaScript file for handling server interactions, UI updates, and flow visualization */ // State management const state = { isConnected: false, currentForm: null, currentTransaction: null, requestInProgress: false, currentStage: null, // Tracks the current stage in the flow questions: [] }; // Stages in the flow const STAGES = { IDLE: 'idle', FRONTEND: 'frontend', AGENT: 'agent', MCP: 'mcp', GOOGLE: 'google', COMPLETE: 'complete', ERROR: 'error' }; // API endpoints const API = { health: '/api/health', form_request: '/api/form_request', agent_proxy: '/api/agent_proxy', form_status: '/api/form_status' }; // DOM Elements const elements = { statusDot: document.getElementById('statusDot'), statusText: document.getElementById('statusText'), requestInput: document.getElementById('requestInput'), sendRequestBtn: document.getElementById('sendRequestBtn'), formResult: document.getElementById('formResult'), formTitle: document.getElementById('formTitle'), formViewLink: document.getElementById('formViewLink'), formEditLink: document.getElementById('formEditLink'), questionsList: document.getElementById('questionsList'), packetLog: document.getElementById('packetLog'), demoBtns: document.querySelectorAll('.demo-btn'), stageIndicator: document.getElementById('stageIndicator') }; // Templates const templates = { packetEntry: document.getElementById('packetTemplate') }; /** * Initialize the application */ function init() { // Set up event listeners setupEventListeners(); // Check server connection checkServerConnection(); // Initialize stage updateStage(STAGES.IDLE); // Pulse frontend node on initial load to indicate entry point setTimeout(() => { if (window.flowAnimator) { window.flowAnimator.pulseNode('frontend', 3000); } }, 1000); } /** * Update the current stage in the flow * @param {string} stage - The current stage */ function updateStage(stage) { state.currentStage = stage; // Update visual indication of current stage if (window.flowAnimator) { // Highlight the appropriate node based on stage switch (stage) { case STAGES.FRONTEND: window.flowAnimator.highlightNode('frontend'); break; case STAGES.AGENT: window.flowAnimator.highlightNode('agent'); break; case STAGES.MCP: window.flowAnimator.highlightNode('mcp'); break; case STAGES.GOOGLE: window.flowAnimator.highlightNode('google'); break; default: // Clear highlights for IDLE, COMPLETE, ERROR Object.keys(window.flowAnimator.nodes).forEach(nodeName => { window.flowAnimator.nodes[nodeName].classList.remove('highlighted'); }); break; } } // Update stage indicator text if present if (elements.stageIndicator) { let stageText = ''; switch (stage) { case STAGES.IDLE: stageText = 'Ready for request'; break; case STAGES.FRONTEND: stageText = 'Processing in frontend'; break; case STAGES.AGENT: stageText = 'Agent processing request'; break; case STAGES.MCP: stageText = 'MCP Server building form'; break; case STAGES.GOOGLE: stageText = 'Interacting with Google Forms'; break; case STAGES.COMPLETE: stageText = 'Form creation complete'; break; case STAGES.ERROR: stageText = 'Error occurred'; break; default: stageText = 'Processing...'; } elements.stageIndicator.textContent = stageText; } } /** * Set up event listeners */ function setupEventListeners() { if (elements.sendRequestBtn) { elements.sendRequestBtn.addEventListener('click', handleSendRequest); } // Demo buttons elements.demoBtns.forEach(btn => { btn.addEventListener('click', function() { const requestText = this.getAttribute('data-request'); if (elements.requestInput && requestText) { elements.requestInput.value = requestText; // Automatically trigger request after a short delay setTimeout(() => { handleSendRequest(); }, 500); } }); }); } /** * Check if server is connected */ async function checkServerConnection() { try { const response = await fetch(API.health); if (response.ok) { const data = await response.json(); updateConnectionStatus(true, `Connected (v${data.version})`); return true; } else { updateConnectionStatus(false, 'Server Error'); return false; } } catch (error) { console.error('Server connection error:', error); updateConnectionStatus(false, 'Disconnected'); return false; } } /** * Update connection status indicator */ function updateConnectionStatus(isConnected, statusMessage) { state.isConnected = isConnected; if (elements.statusDot) { elements.statusDot.className = 'status-dot ' + (isConnected ? 'connected' : 'error'); } if (elements.statusText) { elements.statusText.textContent = statusMessage || (isConnected ? 'Connected' : 'Disconnected'); } } /** * Handle the form request submission */ async function handleSendRequest() { // Validation & state check if (!elements.requestInput || !elements.requestInput.value.trim()) { alert('Please enter a request'); return; } if (state.requestInProgress) { return; } // Reset any previous results resetResults(); // Update UI state.requestInProgress = true; elements.sendRequestBtn.disabled = true; elements.sendRequestBtn.textContent = 'Processing...'; // Get the request text const requestText = elements.requestInput.value.trim(); // Log the user request logPacket({ request_text: requestText }, 'User Request'); try { // Start at frontend updateStage(STAGES.FRONTEND); // Pulse frontend node to start the flow window.flowAnimator.pulseNode('frontend', 1000); // Animate the request flow from frontend to agent await window.flowAnimator.animateFlow('frontend', 'agent', 'outgoing'); // Update stage to agent updateStage(STAGES.AGENT); // Process the request with the agent const agentResponse = await processWithAgent(requestText); // Log agent proxy response logPacket(agentResponse, 'Agent Processing'); if (agentResponse.status === 'error') { // Show error animation in the flow diagram window.flowAnimator.animateErrorFlow('agent'); updateStage(STAGES.ERROR); throw new Error(agentResponse.message || 'Agent processing failed'); } // Continue flow to MCP server await window.flowAnimator.animateFlow('agent', 'mcp', 'outgoing'); updateStage(STAGES.MCP); // Simulate MCP server processing time await new Promise(resolve => setTimeout(resolve, 1000)); // Continue flow to Google Forms await window.flowAnimator.animateFlow('mcp', 'google', 'outgoing'); updateStage(STAGES.GOOGLE); // Simulate Google Forms API interaction time await new Promise(resolve => setTimeout(resolve, 1500)); // Begin response flow // From Google back to MCP await window.flowAnimator.animateFlow('google', 'mcp', 'incoming'); updateStage(STAGES.MCP); // From MCP back to agent await window.flowAnimator.animateFlow('mcp', 'agent', 'incoming'); updateStage(STAGES.AGENT); // Final response from agent to frontend await window.flowAnimator.animateFlow('agent', 'frontend', 'incoming'); updateStage(STAGES.FRONTEND); // Check agent response status if (agentResponse.status === 'success' && agentResponse.result) { if (agentResponse.result.form_id) { // Log the final successful response from the agent/MCP flow logPacket(agentResponse, 'Final Response'); // Update the UI with the final form details updateFormResult(agentResponse.result, agentResponse.result.questions || []); // Update stage to complete updateStage(STAGES.COMPLETE); } else { logPacket(agentResponse, 'Agent Response (No Form)'); alert('Agent processed the request, but no form details were returned.'); updateStage(STAGES.ERROR); } } else { // Log the error response from the agent logPacket(agentResponse, 'Agent Error'); // Show error animation in the flow diagram window.flowAnimator.animateErrorFlow('agent'); updateStage(STAGES.ERROR); throw new Error(agentResponse.message || 'Agent processing failed'); } } catch (error) { console.error('Error during form creation process:', error); alert(`An error occurred: ${error.message}`); // Log error packet if possible logPacket({ error: error.message }, 'Processing Error'); updateStage(STAGES.ERROR); } finally { elements.sendRequestBtn.disabled = false; elements.sendRequestBtn.textContent = 'Process Request'; state.requestInProgress = false; // Reset flow animator if there was an error if (state.currentStage === STAGES.ERROR) { setTimeout(() => { if (window.flowAnimator) { window.flowAnimator.resetAll(); updateStage(STAGES.IDLE); } }, 3000); } } } /** * Sends the natural language request to the agent server. * @param {string} requestText - The raw natural language text. * @returns {Promise<Object>} - The response from the agent server. */ async function processWithAgent(requestText) { console.log(`Sending request via proxy to agent: ${requestText}`); // Debug log try { const response = await fetch(API.agent_proxy, { // Use the PROXY endpoint method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ request_text: requestText }) }); // Log raw response status console.log(`Agent response status: ${response.status}`); if (!response.ok) { let errorData; try { errorData = await response.json(); } catch (e) { errorData = { message: await response.text() || 'Failed to parse error response' }; } console.error('Agent API Error:', response.status, errorData); // Try to construct a meaningful error message let errorMsg = `Agent request failed: ${response.status} ${response.statusText || ''}`; if (errorData && errorData.message) { errorMsg += ` - ${errorData.message}`; } throw new Error(errorMsg); } const responseData = await response.json(); console.log('Agent response data:', responseData); // Debug log return responseData; } catch (error) { console.error('Error sending request to agent:', error); // Return a structured error object for the UI handler return { status: 'error', message: error.message || 'Failed to communicate with agent server' }; } } /** * Create an MCP packet for a tool call * @param {string} toolName - Name of the MCP tool to call * @param {Object} parameters - Parameters for the tool * @returns {Object} - Formatted MCP packet */ function createMCPPacket(toolName, parameters) { return { transaction_id: 'tx_' + Math.random().toString(36).substr(2, 9), tool_name: toolName, parameters: parameters }; } /** * Log an MCP packet or Agent Step to the UI * @param {Object} item - The packet or log entry object * @param {string} type - 'MCP Request', 'MCP Response', 'Agent Step', etc. */ function logItem(item, type) { // Clone the template const template = templates.packetEntry.content.cloneNode(true); // Set the data template.querySelector('.transaction-id').textContent = item.transaction_id || item.step_type || 'N/A'; template.querySelector('.packet-time').textContent = item.timestamp ? new Date(item.timestamp).toLocaleTimeString() : new Date().toLocaleTimeString(); const directionEl = template.querySelector('.packet-direction'); directionEl.textContent = type; // Add specific classes for styling if (type.includes('Request')) { directionEl.classList.add('request'); } else if (type.includes('Response')) { directionEl.classList.add('response'); } else if (type.includes('Agent')) { directionEl.classList.add('agent-step'); // Add a class for agent steps } else if (type.includes('User')) { directionEl.classList.add('user-request'); } else if (type.includes('Error')) { directionEl.classList.add('error-log'); } // Format the content (use item.data for agent steps) const contentToDisplay = type === 'Agent Step' ? item.data : item; template.querySelector('.packet-content').textContent = JSON.stringify(contentToDisplay, null, 2); // Add to the log elements.packetLog.prepend(template); } /** * Logs agent processing steps provided by the backend. * @param {Array<Object>} logEntries - Array of log entry objects from the agent. */ function logAgentSteps(logEntries) { // Log entries in reverse order so newest appear first in the UI log // Or log them sequentially as they happened? // Let's log sequentially as they happened for chronological understanding. logEntries.forEach(entry => { logItem(entry, 'Agent Step'); }); } /** * Modify the old logPacket function to use logItem */ function logPacket(packet, direction) { logItem(packet, direction); } /** * Reset the results UI */ function resetResults() { elements.formResult.classList.add('hidden'); elements.questionsList.innerHTML = ''; state.currentForm = null; state.currentTransaction = null; state.questions = []; // Reset animation state if (window.flowAnimator) { window.flowAnimator.resetAll(); } // Reset to idle state updateStage(STAGES.IDLE); } /** * Update the form result UI * @param {Object} formData - Form data from the API * @param {Array} questions - Questions to display */ function updateFormResult(formData, questions) { state.currentForm = formData; elements.formTitle.textContent = formData.title; elements.formViewLink.href = formData.response_url; elements.formEditLink.href = formData.edit_url; // Add questions to the list elements.questionsList.innerHTML = ''; questions.forEach(question => { const li = document.createElement('li'); li.className = 'list-group-item'; let questionText = `<strong>${question.title}</strong><br>`; questionText += `Type: ${question.type}`; if (question.options && question.options.length > 0) { questionText += `<br>Options: ${question.options.join(', ')}`; } li.innerHTML = questionText; elements.questionsList.appendChild(li); }); // Show the form result elements.formResult.classList.remove('hidden'); // Pulse frontend node to indicate completion window.flowAnimator.pulseNode('frontend'); } // Initialize the application when the DOM is loaded document.addEventListener('DOMContentLoaded', init); ``` -------------------------------------------------------------------------------- /server/forms_api.py: -------------------------------------------------------------------------------- ```python import google.oauth2.credentials from googleapiclient.discovery import build from google.oauth2 import service_account from google_auth_oauthlib.flow import InstalledAppFlow from google.auth.transport.requests import Request import json import config class GoogleFormsAPI: """ Handler for Google Forms API operations. Handles authentication and provides methods to create forms, add questions, and get responses. """ def __init__(self): self.credentials = self._get_credentials() self.forms_service = self._build_service('forms', 'v1') self.drive_service = self._build_service('drive', 'v3') def _get_credentials(self): """Create OAuth2 credentials from environment variables.""" try: print("DEBUG: Creating credentials") print(f"DEBUG: Client ID: {config.GOOGLE_CLIENT_ID[:10]}...") print(f"DEBUG: Client Secret: {config.GOOGLE_CLIENT_SECRET[:10]}...") print(f"DEBUG: Refresh Token: {config.GOOGLE_REFRESH_TOKEN[:15]}...") print(f"DEBUG: Scopes: {config.SCOPES}") credentials = google.oauth2.credentials.Credentials( token=None, # We don't have a token yet refresh_token=config.GOOGLE_REFRESH_TOKEN, client_id=config.GOOGLE_CLIENT_ID, client_secret=config.GOOGLE_CLIENT_SECRET, token_uri='https://oauth2.googleapis.com/token', scopes=[] # Start with empty scopes to avoid validation during refresh ) # Try to validate the credentials print("DEBUG: Validating credentials...") credentials.refresh(Request()) print(f"DEBUG: Credentials valid and refreshed! Token valid until: {credentials.expiry}") # Add scopes after successful refresh credentials = google.oauth2.credentials.Credentials( token=credentials.token, refresh_token=credentials.refresh_token, client_id=credentials.client_id, client_secret=credentials.client_secret, token_uri=credentials.token_uri, scopes=config.SCOPES ) return credentials except Exception as e: print(f"DEBUG: Credentials error: {str(e)}") raise def _build_service(self, api_name, version): """Build and return a Google API service.""" try: print(f"DEBUG: Building {api_name} service v{version}") service = build(api_name, version, credentials=self.credentials) print(f"DEBUG: Successfully built {api_name} service") return service except Exception as e: print(f"DEBUG: Error building {api_name} service: {str(e)}") raise def create_form(self, title, description=""): """ Create a new Google Form. Args: title: Title of the form description: Optional description for the form Returns: dict: Response containing form ID and edit URL """ try: # Debug info print("DEBUG: Starting form creation") print(f"DEBUG: Using client_id: {config.GOOGLE_CLIENT_ID[:10]}...") print(f"DEBUG: Using refresh_token: {config.GOOGLE_REFRESH_TOKEN[:10]}...") # Create a simpler form body with ONLY title as required by the API form_body = { "info": { "title": title } } # Debug info print("DEBUG: About to create form") print("DEBUG: Form body: " + str(form_body)) try: form = self.forms_service.forms().create(body=form_body).execute() form_id = form['formId'] print(f"DEBUG: Form created successfully with ID: {form_id}") print(f"DEBUG: Full form creation response: {json.dumps(form, indent=2)}") # Log full response # Get the actual URLs from the form response initial_responder_uri = form.get('responderUri') print(f"DEBUG: Initial responderUri from create response: {initial_responder_uri}") edit_url = f"https://docs.google.com/forms/d/{form_id}/edit" # Edit URL format is consistent print(f"DEBUG: Tentative Edit URL: {edit_url}") # If description is provided, update the form with it if description: print("DEBUG: Adding description through batchUpdate") update_body = { "requests": [ { "updateFormInfo": { "info": { "description": description }, "updateMask": "description" } } ] } self.forms_service.forms().batchUpdate( formId=form_id, body=update_body ).execute() print("DEBUG: Description added successfully") # Update form settings to make it public and collectable print("DEBUG: Setting form settings to make it public") settings_body = { "requests": [ { "updateSettings": { "settings": { "quizSettings": { "isQuiz": False } }, "updateMask": "quizSettings.isQuiz" } } ] } settings_response = self.forms_service.forms().batchUpdate( formId=form_id, body=settings_body ).execute() print("DEBUG: Form settings updated") print(f"DEBUG: Full settings update response: {json.dumps(settings_response, indent=2)}") # Log full response # Check if the settings response has responderUri settings_responder_uri = None if 'form' in settings_response and 'responderUri' in settings_response['form']: settings_responder_uri = settings_response['form']['responderUri'] form['responderUri'] = settings_responder_uri # Update form dict if found print(f"DEBUG: Found responderUri in settings response: {settings_responder_uri}") # Explicitly publish the form to force it to be visible - These might be redundant/incorrect # response_url = f"https://docs.google.com/forms/d/{form_id}/viewform" # edit_url = f"https://docs.google.com/forms/d/{form_id}/edit" except Exception as form_error: print(f"DEBUG: Form creation error: {str(form_error)}") # Create a mock form for testing form = { 'formId': 'form_error_state', 'responderUri': 'https://docs.google.com/forms/d/e/form_error_state/viewform' # Use /e/ format } form_id = form['formId'] print("DEBUG: Created mock form for testing") # Make the form public via Drive API - only if we have a real form try: if form_id != 'form_error_state': print(f"DEBUG: About to set Drive permissions for form {form_id}") # First try to get file to verify it exists in Drive try: file_check = self.drive_service.files().get( fileId=form_id, fields="id,name,permissions,webViewLink,webContentLink" # Added webContentLink just in case ).execute() print(f"DEBUG: File exists in Drive: {file_check.get('name', 'unknown')}") print(f"DEBUG: Full Drive file get response: {json.dumps(file_check, indent=2)}") # Log full response # Store the web view link for later use drive_web_view_link = file_check.get('webViewLink') if drive_web_view_link: print(f"DEBUG: Drive webViewLink found: {drive_web_view_link}") except Exception as file_error: print(f"DEBUG: Cannot find/get file in Drive: {str(file_error)}") drive_web_view_link = None # Ensure it's None if error occurs # Set public permission permission = { 'type': 'anyone', 'role': 'reader', 'allowFileDiscovery': True } perm_result = self.drive_service.permissions().create( fileId=form_id, body=permission, fields='id', sendNotificationEmail=False ).execute() print(f"DEBUG: Permissions set successfully: {perm_result}") print(f"DEBUG: Full permissions create response: {json.dumps(perm_result, indent=2)}") # Log full response # Check permissions after setting permissions = self.drive_service.permissions().list( fileId=form_id, fields="*" # Get all fields ).execute() print(f"DEBUG: Full permissions list response after setting: {json.dumps(permissions, indent=2)}") # Log full response # Try to publish the file using the Drive API - This might be unnecessary/problematic try: publish_body = { 'published': True, 'publishedOutsideDomain': True, 'publishAuto': True } self.drive_service.revisions().update( fileId=form_id, revisionId='head', body=publish_body ).execute() print("DEBUG: Form published successfully via Drive API") except Exception as publish_error: print(f"DEBUG: Non-critical publish error: {str(publish_error)}") except Exception as perm_error: print(f"DEBUG: Permission error: {str(perm_error)}") # Continue even if permission setting fails # Determine the final response_url based on availability and priority # Priority: responderUri from settings, initial responderUri, Drive webViewLink (less reliable for view), fallback response_url = None if settings_responder_uri: response_url = settings_responder_uri print(f"DEBUG: FINAL URL: Using responderUri from settings response: {response_url}") elif initial_responder_uri: response_url = initial_responder_uri print(f"DEBUG: FINAL URL: Using initial responderUri from create response: {response_url}") elif drive_web_view_link and "/viewform" in drive_web_view_link: # Only use webViewLink if it looks like a view link response_url = drive_web_view_link print(f"DEBUG: FINAL URL: Using Drive webViewLink (as it contained /viewform): {response_url}") else: # Fallback to manual construction if absolutely nothing else is found response_url = f"https://docs.google.com/forms/d/e/{form_id}/viewform" # Use /e/ format for fallback print(f"DEBUG: FINAL URL: Using manually constructed fallback (/e/ format): {response_url}") # Also log the potentially problematic webViewLink if it existed but wasn't used if drive_web_view_link: print(f"DEBUG: Note: Drive webViewLink found but not used as final URL: {drive_web_view_link}") # Ensure edit_url is correctly set (it's usually stable) edit_url = f"https://docs.google.com/forms/d/{form_id}/edit" print(f"DEBUG: FINAL Edit URL: {edit_url}") return { "form_id": form_id, "response_url": response_url, "edit_url": edit_url, "title": title } except Exception as e: print(f"Error creating form: {str(e)}") raise def add_question(self, form_id, question_type, title, options=None, required=False): """ Add a question to an existing Google Form. Args: form_id: ID of the form to add the question to question_type: Type of question (text, paragraph, multiple_choice, etc.) title: Question title/text options: List of options for multiple choice questions required: Whether the question is required Returns: dict: Response containing question ID """ try: print(f"DEBUG: Adding {question_type} question to form {form_id}") # Get the current form form = self.forms_service.forms().get(formId=form_id).execute() # Determine the item ID for the new question item_id = len(form.get('items', [])) print(f"DEBUG: New question will have item_id: {item_id}") # Create base request request = { "requests": [{ "createItem": { "item": { "title": title, "questionItem": { "question": { "required": required } } }, "location": { "index": item_id } } }] } # Set up question type specific configuration if question_type == "text": print("DEBUG: Setting up text question") request["requests"][0]["createItem"]["item"]["questionItem"]["question"]["textQuestion"] = {} elif question_type == "paragraph": print("DEBUG: Setting up paragraph question") # Google Forms API uses textQuestion with different properties for paragraphs request["requests"][0]["createItem"]["item"]["questionItem"]["question"]["textQuestion"] = { "paragraph": True } elif question_type == "multiple_choice" and options: print(f"DEBUG: Setting up multiple choice question with {len(options)} options") choices = [{"value": option} for option in options] request["requests"][0]["createItem"]["item"]["questionItem"]["question"]["choiceQuestion"] = { "type": "RADIO", "options": choices, "shuffle": False } elif question_type == "checkbox" and options: print(f"DEBUG: Setting up checkbox question with {len(options)} options") choices = [{"value": option} for option in options] request["requests"][0]["createItem"]["item"]["questionItem"]["question"]["choiceQuestion"] = { "type": "CHECKBOX", "options": choices, "shuffle": False } print(f"DEBUG: Request body: {request}") # Execute the request update_response = self.forms_service.forms().batchUpdate( formId=form_id, body=request ).execute() print(f"DEBUG: Question added successfully: {update_response}") return { "form_id": form_id, "question_id": item_id, "title": title, "type": question_type } except Exception as e: print(f"Error adding question: {str(e)}") raise def get_responses(self, form_id): """ Get responses for a Google Form. Args: form_id: ID of the form to get responses for Returns: dict: Form responses """ try: # Get the form to retrieve question titles form = self.forms_service.forms().get(formId=form_id).execute() questions = {} for item in form.get('items', []): question_id = item.get('itemId', '') title = item.get('title', '') questions[question_id] = title # Get form responses response_data = self.forms_service.forms().responses().list(formId=form_id).execute() responses = response_data.get('responses', []) formatted_responses = [] for response in responses: answer_data = {} answers = response.get('answers', {}) for question_id, answer in answers.items(): question_title = questions.get(question_id, question_id) if 'textAnswers' in answer: text_values = [text.get('value', '') for text in answer.get('textAnswers', {}).get('answers', [])] answer_data[question_title] = text_values[0] if len(text_values) == 1 else text_values elif 'choiceAnswers' in answer: choice_values = answer.get('choiceAnswers', {}).get('answers', []) answer_data[question_title] = choice_values formatted_responses.append({ 'response_id': response.get('responseId', ''), 'created_time': response.get('createTime', ''), 'answers': answer_data }) return { "form_id": form_id, "form_title": form.get('info', {}).get('title', ''), "response_count": len(formatted_responses), "responses": formatted_responses } except Exception as e: print(f"Error getting responses: {str(e)}") raise ``` -------------------------------------------------------------------------------- /agents/agent_integration.py: -------------------------------------------------------------------------------- ```python """ CamelAIOrg Agent Integration for Google Forms MCP Server This module provides integration with CamelAIOrg's agent framework, enabling natural language processing to create Google Forms through MCP. """ import os import json import logging import requests import datetime # REMOVE: Import our mock CamelAI implementation # from camelai import create_agent # Load environment variables # load_dotenv() # Configure logging logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' ) logger = logging.getLogger("agent_integration") # Configuration MCP_SERVER_URL = os.getenv('MCP_SERVER_URL', 'http://mcp-server:5000/api/process') AGENT_API_KEY = os.getenv('AGENT_API_KEY', 'demo_key') # Might be used for a real LLM API key # Import Camel AI components (assuming structure) from camel.agents import ChatAgent from camel.messages import BaseMessage from camel.types import ModelPlatformType, ModelType, RoleType # Added RoleType from camel.models import ModelFactory class FormAgent: """ Agent for handling natural language form creation requests. Uses NLP (simulated via _call_llm_agent) to process natural language and convert it to MCP tool calls. """ def __init__(self): """Initialize the agent.""" self.logger = logger self.log_entries = [] # Initialize log storage # REMOVE: self.camel_agent = create_agent("FormAgent", ...) self.logger.info("FormAgent initialized (using simulated LLM)") def _log_step(self, step_type, data): """Adds a structured log entry for the frontend.""" entry = { "timestamp": datetime.datetime.now().isoformat(), "step_type": step_type, "data": data } self.log_entries.append(entry) # Also log to server console for debugging self.logger.info(f"AGENT LOG STEP: {step_type} - {data}") def process_request(self, request_text): """ Processes a natural language request using a simulated LLM call. 1. Calls a simulated LLM to parse the request into structured JSON. 2. Determines the necessary MCP tool calls based on the JSON. 3. Executes the tool calls by sending requests to the MCP server. 4. Orchestrates multi-step processes. 5. Returns the final result or error. """ self.log_entries = [] # Clear log for new request self._log_step("Request Received", {"text": request_text}) try: # 1. Analyze request using simulated LLM call self._log_step("NLP Analysis Start", {}) structured_form_data = self._call_llm_agent(request_text) self.logger.info(f"Simulated LLM Structured Output: {json.dumps(structured_form_data, indent=2)}") self._log_step("NLP Analysis Complete", {"structured_data": structured_form_data}) # Basic validation of LLM output if not structured_form_data or 'formTitle' not in structured_form_data: self.logger.error("Simulated LLM did not return valid structured data.") return { "status": "error", "message": "Failed to understand the request structure using LLM." } # Extract sections and settings - Basic structure assumes one form for now form_params = { "title": structured_form_data.get('formTitle'), "description": structured_form_data.get('formDescription', '') # We would also handle settings here if the API supported it } sections = structured_form_data.get('sections', []) if not sections: self.logger.error("Simulated LLM did not return any form sections/questions.") return { "status": "error", "message": "LLM did not identify any questions for the form." } # 2. Determine and execute tool calls (simplified flow: create form, add all questions) # NOTE: This doesn't handle multiple sections, logic, or advanced settings yet all_questions = [] for section in sections: # TODO: Add support for creating sections/page breaks if API allows # self.logger.info(f"Processing section: {section.get('title', 'Untitled Section')}") all_questions.extend(section.get('questions', [])) # Pass the extracted questions to the creation flow form_params['questions'] = all_questions # Execute the flow, which will populate self.log_entries further final_response = self._execute_create_form_flow(form_params) # Add the collected logs to the final response if successful if final_response.get("status") == "success": final_response["log_entries"] = self.log_entries return final_response except Exception as e: self.logger.error(f"Error in FormAgent process_request: {str(e)}", exc_info=True) self._log_step("Agent Error", {"error": str(e)}) # Include logs gathered so far in the error response too return { "status": "error", "message": f"Agent failed to process request: {str(e)}", "log_entries": self.log_entries } def _call_llm_agent(self, request_text): """ Uses Camel AI's ChatAgent to process the request and extract structured data. Falls back to a basic structure if the LLM call fails. """ # Check for the API key first (using the name Camel AI expects) api_key = os.getenv('GOOGLE_API_KEY') if not api_key: self.logger.error("GOOGLE_API_KEY is not set in the environment.") # Updated log message return self._get_fallback_structure(request_text) try: # 1. Setup Model # Assuming ModelFactory can find the key from env or we pass it # Adjust ModelType enum based on actual Camel AI definition if needed # Using a placeholder like ModelType.GEMINI_1_5_FLASH - this will likely need correction model_name_str = "gemini-1.5-flash-latest" # Keep the string name # Attempt to create model instance via factory # Create the required config dict - make it empty and rely on env var GOOGLE_API_KEY model_config_dict = { # No API key here - expecting library to read GOOGLE_API_KEY from env } # REVERT: Go back to GEMINI platform type llm_model = ModelFactory.create( model_platform=ModelPlatformType.GEMINI, model_type=ModelType.GEMINI_1_5_FLASH, model_config_dict=model_config_dict ) self.logger.info(f"Camel AI Model configured using: {ModelType.GEMINI_1_5_FLASH}") # 2. Prepare messages for the ChatAgent system_prompt = self._build_llm_prompt(request_text) system_message = BaseMessage( role_name="System", role_type=RoleType.ASSISTANT, meta_dict=None, content=system_prompt ) user_message = BaseMessage( role_name="User", role_type=RoleType.USER, meta_dict=None, content=request_text # Or maybe just a trigger like "Process the request"? Let's try request_text ) # 3. Initialize and run the ChatAgent agent = ChatAgent(system_message=system_message, model=llm_model) agent.reset() # Ensure clean state self.logger.info("Calling Camel AI ChatAgent...") response = agent.step(user_message) if not response or not response.msgs: self.logger.error("Camel AI agent did not return a valid response.") return self._get_fallback_structure(request_text) # 4. Extract JSON content from the last message # Assuming the response structure contains a list of messages `msgs` # and the agent's reply is the last one. agent_reply_message = response.msgs[-1] content = agent_reply_message.content self.logger.debug(f"Raw Camel AI agent response content: {content}") # --- Robust JSON Extraction --- try: # Find the start and end of the JSON object json_start = content.find('{') json_end = content.rfind('}') if json_start != -1 and json_end != -1 and json_end > json_start: json_string = content[json_start:json_end+1] structured_data = json.loads(json_string) self.logger.info("Successfully parsed structured JSON from Camel AI agent response.") return structured_data else: # Fallback if JSON bounds couldn't be found self.logger.error(f"Could not find valid JSON object boundaries in response. Content: {content}") return self._get_fallback_structure(request_text) except json.JSONDecodeError as e: self.logger.error(f"Failed to parse JSON content from Camel AI agent: {e}. Extracted content attempt: {json_string if 'json_string' in locals() else 'N/A'}") return self._get_fallback_structure(request_text) except ImportError as e: self.logger.error(f"Failed to import Camel AI components. Is 'camel-ai' installed correctly? Error: {e}") return self._get_fallback_structure(request_text) except Exception as e: # Catch potential errors during Camel AI model init or agent step self.logger.error(f"Error during Camel AI processing: {str(e)}", exc_info=True) return self._get_fallback_structure(request_text) def _build_llm_prompt(self, request_text): """ Constructs the detailed prompt for the LLM. Crucial for getting reliable JSON output. Needs careful tuning based on the LLM used. """ # Define the desired JSON structure and supported types/features # This helps the LLM understand the target format. json_schema_description = """ { "formTitle": "string", "formDescription": "string (optional)", "settings": { // Note: Backend does not support settings yet, but LLM can parse them "collectEmail": "string (optional: 'required' or 'optional')", "limitToOneResponse": "boolean (optional)", "progressBar": "boolean (optional)", "confirmationMessage": "string (optional)" }, "sections": [ { "title": "string", "description": "string (optional)", "questions": [ { "title": "string", "description": "string (optional)", "type": "string (enum: text, paragraph, multiple_choice, checkbox, linear_scale, multiple_choice_grid, checkbox_grid)", "required": "boolean (optional, default: false)", "options": "array of strings (required for multiple_choice, checkbox)" + " OR object { min: int, max: int, minLabel: string (opt), maxLabel: string (opt) } (required for linear_scale)" + " OR object { rows: array of strings, columns: array of strings } (required for grids)", "logic": "object { on: string (option value), action: string (enum: submit_form, skip_section), targetSectionTitle: string (required if action=skip_section) } (optional)", // Note: Backend doesn't support logic "validation": "string (optional: 'email' or other types if supported)" // Note: Backend doesn't support validation } // ... more questions ... ] } // ... more sections ... ] } """ prompt = f"""Analyze the following user request to create a Google Form. Based ONLY on the request, generate a JSON object strictly adhering to the following schema. SCHEMA: ```json {json_schema_description} ``` RULES: - Output *only* the JSON object, nothing else before or after. - If the user requests features not supported by the schema description (e.g., file uploads, specific themes, complex scoring), omit them from the JSON. - If the request is unclear about question types or options, make reasonable assumptions (e.g., use 'text' for simple questions, provide standard options for ratings). - Ensure the 'options' field matches the required format for the specified 'type'. - Pay close attention to required fields in the schema description. - If the request seems too simple or doesn't clearly describe a form, create a basic form structure with a title based on the request and a few generic questions (like Name, Email, Comments). USER REQUEST: ``` {request_text} ``` JSON OUTPUT: """ return prompt def _get_fallback_structure(self, request_text): """Returns a basic structure if LLM call fails or parsing fails.""" self.logger.warning(f"Gemini call/parsing failed. Falling back to basic structure for: {request_text}") return { "formTitle": self._generate_title(request_text), "formDescription": request_text, "settings": {}, "sections": [ { "questions": [ {"title": "Name", "type": "text", "required": True}, {"title": "Email", "type": "text", "required": False}, {"title": "Your Response", "type": "paragraph", "required": True} ] } ] } def _execute_create_form_flow(self, params): """ Handles the complete flow for creating a form and adding its questions. """ self.logger.info(f"Executing create form flow with params: {params}") # Extract potential questions from NLP parameters questions_to_add = params.pop('questions', []) # Remove questions from main params # A. Create the form first self._log_step("Create Form Start", {"params": params}) create_form_response = self._handle_create_form(params) self._log_step("Create Form End", {"response": create_form_response}) if create_form_response.get("status") != "success": self.logger.error(f"Form creation failed: {create_form_response.get('message')}") return create_form_response # Return the error from create_form # Get the form_id from the successful response form_result = create_form_response.get('result', {}) form_id = form_result.get('form_id') if not form_id: self.logger.error("Form creation succeeded but no form_id returned.") return { "status": "error", "message": "Form created, but form_id missing in response." } self.logger.info(f"Form created successfully: {form_id}") # B. Add questions if any were identified by NLP question_results = [] if questions_to_add: self._log_step("Add Questions Start", {"count": len(questions_to_add)}) self.logger.info(f"Adding {len(questions_to_add)} questions to form {form_id}") for question_data in questions_to_add: question_params = { "form_id": form_id, # Ensure structure matches _handle_add_question needs "type": question_data.get('type'), "title": question_data.get('title'), "options": question_data.get('options', []), "required": question_data.get('required', False) } # Validate basic question params before sending if not question_params['type'] or not question_params['title']: self.logger.warning(f"Skipping invalid question data: {question_data}") continue self._log_step("Add Question Start", {"question_index": questions_to_add.index(question_data), "params": question_params}) add_q_response = self._handle_add_question(question_params) self._log_step("Add Question End", {"question_index": questions_to_add.index(question_data), "response": add_q_response}) question_results.append(add_q_response) # Log if the question type wasn't supported by the backend if add_q_response.get("status") == "error" and "Invalid question_type" in add_q_response.get("message", ""): self.logger.warning(f"Question '{question_params['title']}' failed: Type '{question_params['type']}' likely not supported by backend API yet.") elif add_q_response.get("status") != "success": self.logger.error(f"Failed to add question '{question_params['title']}': {add_q_response.get('message')}") # Optional: Check if question adding failed and decide whether to stop if add_q_response.get("status") != "success": self.logger.error(f"Failed to add question '{question_params['title']}': {add_q_response.get('message')}") # Decide: continue adding others, or return error immediately? # For now, let's continue but log the error. self._log_step("Add Questions End", {}) else: self.logger.info(f"No questions identified by NLP to add to form {form_id}") self._log_step("Add Questions Skipped", {}) # 5. Return the final result (details of the created form) # We can optionally include the results of adding questions if needed final_result = form_result # Let's add the questions that were *attempted* to be added back for the UI final_result['questions'] = questions_to_add return { "status": "success", "result": final_result # Optionally add: "question_addition_results": question_results } # Add the fallback title generation method back (or use a simpler one) def _generate_title(self, request_text): words = request_text.split() if len(words) <= 5: return request_text + " Form" else: return " ".join(words[:5]) + "... Form" # --- Methods for handling specific tool calls by sending to MCP Server --- def _handle_create_form(self, params): """ Handle form creation by sending MCP packet to the server. Args: params: Parameters for form creation Returns: dict: Result of the form creation """ self.logger.info(f"Creating form with params: {params}") # Prepare MCP packet mcp_packet = { "tool_name": "create_form", "parameters": { "title": params.get("title", "Form from NL Request"), "description": params.get("description", "") } } # Log *before* sending self._log_step("MCP Request (create_form)", mcp_packet) response = self._send_to_mcp_server(mcp_packet) # Log *after* receiving self._log_step("MCP Response (create_form)", response) return response def _handle_add_question(self, params): """ Handle adding a question to a form by sending MCP packet. Args: params: Parameters for question addition Returns: dict: Result of the question addition """ self.logger.info(f"Adding question with params: {params}") # Prepare MCP packet mcp_packet = { "tool_name": "add_question", "parameters": { "form_id": params.get("form_id"), "question_type": params.get("type", "text"), "title": params.get("title", "Question"), "options": params.get("options", []), "required": params.get("required", False) } } # Log *before* sending self._log_step("MCP Request (add_question)", mcp_packet) response = self._send_to_mcp_server(mcp_packet) # Log *after* receiving self._log_step("MCP Response (add_question)", response) return response def _handle_get_responses(self, params): """ Handle getting form responses by sending MCP packet. Args: params: Parameters for getting responses Returns: dict: Form responses """ self.logger.info(f"Getting responses with params: {params}") # Prepare MCP packet mcp_packet = { "tool_name": "get_responses", "parameters": { "form_id": params.get("form_id") } } # Log *before* sending self._log_step("MCP Request (get_responses)", mcp_packet) response = self._send_to_mcp_server(mcp_packet) # Log *after* receiving self._log_step("MCP Response (get_responses)", response) return response def _send_to_mcp_server(self, mcp_packet): """ Sends an MCP packet to the MCP server URL. Args: mcp_packet: The MCP packet (dict) to send. Returns: dict: The response JSON from the MCP server. """ self.logger.info(f"Sending MCP packet: {json.dumps(mcp_packet)}") try: response = requests.post( MCP_SERVER_URL, json=mcp_packet, headers={'Content-Type': 'application/json'}, timeout=30 # Add a timeout (e.g., 30 seconds) ) # Raise an exception for bad status codes (4xx or 5xx) response.raise_for_status() response_data = response.json() self.logger.info(f"Received MCP response: {json.dumps(response_data)}") return response_data except requests.exceptions.Timeout: self.logger.error(f"Timeout sending MCP packet to {MCP_SERVER_URL}") return {"status": "error", "message": "MCP server request timed out"} except requests.exceptions.RequestException as e: self.logger.error(f"Error sending MCP packet to {MCP_SERVER_URL}: {str(e)}") # Try to get error details from response body if possible error_detail = str(e) try: error_detail = e.response.text if e.response else str(e) except Exception: pass # Ignore errors parsing the error response itself return {"status": "error", "message": f"MCP server communication error: {error_detail}"} except json.JSONDecodeError as e: self.logger.error(f"Error decoding MCP response JSON: {str(e)}") return {"status": "error", "message": "Invalid JSON response from MCP server"} except Exception as e: self.logger.error(f"Unexpected error in _send_to_mcp_server: {str(e)}", exc_info=True) return {"status": "error", "message": f"Unexpected agent error sending MCP packet: {str(e)}"} # For testing if __name__ == "__main__": agent = FormAgent() # Test with some sample requests test_requests = [ "Create a customer feedback form with a rating question", "Make a survey about remote work preferences", "Set up an RSVP form for my event on Saturday" ] for req in test_requests: print(f"\nProcessing: {req}") result = agent.process_request(req) print(f"Result: {json.dumps(result, indent=2)}") ```