diff --git a/.gitignore b/.gitignore index 68a37c1..30ab9e7 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,8 @@ jinoos/ agents/__pycache__/ conversation_logs/ agents/test.py +nanda_agent/__pycache__ + +# Build artifacts +dist/ +*.egg-info/ \ No newline at end of file diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..e51fe08 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,8 @@ + +include README.md +include LICENSE +include requirements.txt +recursive-include nanda_agent *.py +recursive-include nanda_agent *.txt +recursive-exclude * __pycache__ +recursive-exclude * *.py[co] \ No newline at end of file diff --git a/README.md b/README.md index eb8307a..2307111 100644 --- a/README.md +++ b/README.md @@ -1,156 +1,329 @@ -# Internet of Agents +# NANDA Agent Framework -A distributed agent system that enables multiple agents to run and communicate with each other over HTTP/HTTPS. +A customizable improvement logic for your agents, and easily get registered into NANDA registry -## Overview +## Features -This system allows you to run multiple agents with unique IDs, each operating on different ports for bridge and API communication. The agents can communicate with each other and with external services through a registry. +- **Pluggable Message Improvement**: Easily customize how your agents improve messages +- **Multiple AI Frameworks**: Support for LangChain, CrewAI, and custom logic +- **Agent-to-Agent Communication**: Built-in A2A communication system +- **Registry System**: Automatic agent discovery and registration +- **SSL Support**: Production-ready with Let's Encrypt certificates +- **Example Agents**: Ready-to-use examples for common use cases -## Prerequisites +## Installation -- Python 3.x -- Bash shell -- Internet connectivity -- Anthropic API key -- Let's Encrypt SSL certificates -- Virtual environment (venv) +### Basic Installation -## Configuration +```bash +pip install nanda-agent +``` + +## Quick Start -The system requires a configuration file at `/etc/internet_of_agents.env` with the following environment variables: +### 1. Set Your API Key (For running your personal hosted agents, need API key and your own domain) ```bash -# Required environment variables -ANTHROPIC_API_KEY="your-api-key-here" -AGENT_ID_PREFIX="your-prefix" -DOMAIN_NAME="your-domain.com" -REGISTRY_URL="https://your-registry-url:port" - -# Optional environment variables -NUM_AGENTS=1 # Defaults to 1 if not specified +export ANTHROPIC_API_KEY="your-api-key-here"\ +export DOMAIN_NAME="your-domain.com" ``` -## Agent Configuration - -The agent system can be configured through the following parameters in `start_running_agents.sh`: +### 2. Create Your Own Agent - Development ```bash -# Starting port numbers for bridge and API -START_BRIDGE_PORT=6000 -START_API_PORT=6001 +2.1 Write your improvement logic using the framework you like. Here it is a simple moduule without any llm call. +2.2 In the main(), create your improvement function, initialize NANDA using the improvement function, and start the server with Anthropic key and domain using nanda.start_server_api(). +2.3 In the requirements.txt file add nanda-agent along with other requirements +2.4 Move this file into your server(the domain should match to the IP address) and run this python file in background + +if langchain_pirate.py is python file name, use the below instructions to run in the background: +nohup python3 langchain_pirate.py > out.log 2>&1 & ``` -### Port Configuration -- Bridge ports start from `START_BRIDGE_PORT` and increment by 2 -- API ports start from `START_API_PORT` and increment by 2 -- For example, with NUM_AGENTS=3: - - Bridge ports: 6000, 6002, 6004 - - API ports: 6001, 6003, 6005 - -### Agent IDs -- Agent IDs follow different patterns based on the domain: - - For nanda-registry.com domains: `agentm{AGENT_ID_PREFIX}{INDEX}` - - For other domains: `agents{AGENT_ID_PREFIX}{INDEX}` -- Example with AGENT_ID_PREFIX=6 and NUM_AGENTS=3: - - For nanda-registry.com: agentm60, agentm61, agentm62 - - For other domains: agents60, agents61, agents62 - -## Usage - -1. Set up the environment file: - ```bash - sudo nano /etc/internet_of_agents.env - # Add the required environment variables - ``` - -2. Ensure SSL certificates are in place: - - Certificate: `/etc/letsencrypt/live/${DOMAIN_NAME}/fullchain.pem` - - Private key: `/etc/letsencrypt/live/${DOMAIN_NAME}/privkey.pem` - -3. Make the script executable: - ```bash - chmod +x start_running_agents.sh - ``` - -4. Run the script: - ```bash - ./start_running_agents.sh - ``` - -## Monitoring and Management - -### Check Running Agents + +```python +#!/usr/bin/env python3 +from nanda_agent import NANDA +import os + +def create_custom_improvement(): + """Create your custom improvement function""" + + def custom_improvement_logic(message_text: str) -> str: + """Transform messages according to your logic""" + try: + # Your custom transformation logic here + improved_text = message_text.replace("hello", "greetings") + improved_text = improved_text.replace("goodbye", "farewell") + + return improved_text + except Exception as e: + print(f"Error in improvement: {e}") + return message_text # Fallback to original + + return custom_improvement_logic + +def main(): + # Create your improvement function + my_improvement = create_custom_improvement() + + # Initialize NANDA with your custom logic + nanda = NANDA(my_improvement) + + # Start the server + anthropic_key = os.getenv("ANTHROPIC_API_KEY") + domain = os.getenv("DOMAIN_NAME") + + nanda.start_server_api(anthropic_key, domain) + +if __name__ == "__main__": + main() +``` + +### Using with LangChain + +```python +from nanda_agent import NANDA +from langchain_core.prompts import PromptTemplate +from langchain_core.output_parsers import StrOutputParser +from langchain_anthropic import ChatAnthropic + +def create_langchain_improvement(): + llm = ChatAnthropic( + api_key=os.getenv("ANTHROPIC_API_KEY"), + model="claude-3-haiku-20240307" + ) + + prompt = PromptTemplate( + input_variables=["message"], + template="Make this message more professional: {message}" + ) + + chain = prompt | llm | StrOutputParser() + + def langchain_improvement(message_text: str) -> str: + return chain.invoke({"message": message_text}) + + return langchain_improvement + +# Use it +nanda = NANDA(create_langchain_improvement()) +# Start the server +anthropic_key = os.getenv("ANTHROPIC_API_KEY") +domain = os.getenv("DOMAIN_NAME") + +nanda.start_server_api(anthropic_key, domain) +``` + +### Using with CrewAI + +```python +from nanda_agent import NANDA +from crewai import Agent, Task, Crew +from langchain_anthropic import ChatAnthropic + +def create_crewai_improvement(): + llm = ChatAnthropic( + api_key=os.getenv("ANTHROPIC_API_KEY"), + model="claude-3-haiku-20240307" + ) + + improvement_agent = Agent( + role="Message Improver", + goal="Improve message clarity and professionalism", + backstory="You are an expert communicator.", + llm=llm + ) + + def crewai_improvement(message_text: str) -> str: + task = Task( + description=f"Improve this message: {message_text}", + agent=improvement_agent, + expected_output="An improved version of the message" + ) + + crew = Crew(agents=[improvement_agent], tasks=[task]) + result = crew.kickoff() + return str(result) + + return crewai_improvement + +# Use it +nanda = NANDA(create_crewai_improvement()) +# Start the server +anthropic_key = os.getenv("ANTHROPIC_API_KEY") +domain = os.getenv("DOMAIN_NAME") + +nanda.start_server_api(anthropic_key, domain) +``` + +### Checkout the examples folder for more details + + +## Configuration + +### Environment Variables + +- `ANTHROPIC_API_KEY`: Your Anthropic API key (required) +- `DOMAIN_NAME`: Domain name for SSL certificates +- `AGENT_ID`: Custom agent ID (optional, auto-generated if not provided) +- `PORT`: Agent bridge port (default: 6000) +- `IMPROVE_MESSAGES`: Enable/disable message improvement (default: true) + +### Production Deployment + +For production deployment with SSL: + ```bash -ps aux | grep run_ui_agent_https +export ANTHROPIC_API_KEY="your-api-key" +export DOMAIN_NAME="your-domain.com" +nanda-pirate ``` -### Stop All Agents +#### Detailed steps to be done for the deployment ```bash -for pid in logs/*.pid; do kill $(cat $pid); done +Assuming your customized improvement logic is in langchain_pirate.py + + +1. Copy the py and requirements file to a folder of choice in the server +cmd: scp langchain_pirate.py requirements.txt root@66.175.209.173:/opt/test-agents +For AWS Linux machines +cmd : scp -i my-key.pem langchain_pirate.py requirements.txt ec2-user@66.175.209.173/home/ec2-user/test-agents + +2. ssh into the server, ensure the latest software is in the system +cmd : ssh root@66.175.209.173 + sudo apt update && sudo apt install python3 python3-pip python3-venv certbot + +EC2 cmd : ssh ec2user@66.175.209.173 + sudo dnf update -y && sudo dnf install -y python3.11 python3.11-pip certbot + +3. Move to the respective folder and create and Activate a virtual env in the folder where files are moved in step 1 +cmd : cd /opt/test-agents && python3 -m venv jinoos && source jinoos/bin/activate + +EC2 cmd: cd /home/ec2-user/test-agents && python3.11 -m venv jinoos && source jinoos/bin/activate + +4. Download the certificates into the machine for your domain. +(For ex: You should ensure in DNS an A record is mapping this domain chat1.chat39.org to IP address 66.175.209.173). Ensure the domain has to be changed + +cmd : sudo certbot certonly --standalone -d chat1.chat39.org + +5. Copy the cert to current folder for access and provide required access +Ensure the domain has to be changed + + sudo cp -L /etc/letsencrypt/live/chat1.chat39.org/fullchain.pem . + sudo cp -L /etc/letsencrypt/live/chat1.chat39.org/privkey.pem . + sudo chown $USER:$USER fullchain.pem privkey.pem + chmod 600 fullchain.pem privkey.pem + + +6. Install the requirements file +cmd : python -m pip install --upgrade pip && pip3 install -r requirements.txt + +7. Ensure the env variables are available either through .env or you can provide export +cmd : export ANTHROPIC_API_KEY=my-anthropic-key && export DOMAIN_NAME=my-domain + +8. Run the new improvement logic as a batch process +cmd : nohup python3 langchain_pirate.py > out.log 2>&1 & + +9. Open the log file and you could find the agent enrollment link +cmd : cat out.log + +10. Take the link and go to browser for registration + ``` -### Logs -- Agent logs are stored in the `logs` directory -- Each agent has its own log file: `logs/agentm{ID}_logs.txt` -- Process IDs are stored in: `logs/agentm{ID}.pid` -## Network Configuration -The system automatically detects the server's IP address using: -1. AWS checkip service -2. ifconfig.me service -3. Falls back to localhost.com if both fail -Each agent is configured with: -- Public URL: `http://{SERVER_IP}:{BRIDGE_PORT}` -- API URL: `https://{DOMAIN_NAME}:{API_PORT}` -## Registry -Agents are registered with a central registry specified by the REGISTRY_URL environment variable. -The registry URL should be a valid HTTPS endpoint. +The framework will automatically: +- Generate SSL certificates using Let's Encrypt +- Set up proper agent registration +- Configure production-ready logging -## Security +## API Endpoints -- API communication is secured using SSL certificates from Let's Encrypt -- Environment variables are stored in a system-wide configuration file -- Each agent runs in its own process -- Virtual environment isolation +When running with `start_server_api()`, the following endpoints are available: -## Troubleshooting +- `GET /api/health` - Health check +- `POST /api/send` - Send message to agent +- `GET /api/agents/list` - List registered agents +- `POST /api/receive_message` - Receive message from agent +- `GET /api/render` - Get latest message -1. If agents fail to start: - - Check if ports are available - - Verify environment variables in `/etc/internet_of_agents.env` - - Check SSL certificate paths - - Check logs in the `logs` directory +## Agent Communication -2. If agents can't communicate: - - Verify network connectivity - - Check if registry is accessible - - Ensure ports are not blocked by firewall - - Verify SSL certificate validity +Agents can communicate with each other using the `@agent_id` syntax: + +``` +@agent123 Hello there! +``` + +The message will be improved using your custom logic before being sent. + +## Command Line Tools + +```bash +# Show help +nanda-agent --help + +# List available examples +nanda-agent --list-examples + +# Run specific examples +nanda-pirate # Simple pirate agent +nanda-pirate-langchain # LangChain pirate agent +nanda-sarcastic # CrewAI sarcastic agent +``` + +## Architecture + +The NANDA framework consists of: + +1. **AgentBridge**: Core communication handler +2. **Message Improvement System**: Pluggable improvement logic +3. **Registry System**: Agent discovery and registration +4. **A2A Communication**: Agent-to-agent messaging +5. **Flask API**: External communication interface + +## Development + +### Creating Custom Agents + +1. Create your improvement function +2. Initialize NANDA with your function +3. Start the server +4. Your agent is ready to communicate! + +## Examples + +The framework includes several example agents: + +- **Simple Pirate Agent**: Basic string replacement +- **LangChain Pirate Agent**: AI-powered pirate transformation +- **CrewAI Sarcastic Agent**: Team-based sarcastic responses ## License -MIT License +MIT License - see LICENSE file for details. + +## Contributing + +Contributions are welcome! Please see CONTRIBUTING.md for guidelines. -Copyright (c) 2024 Internet of Agents +## Support -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: +For issues and questions: +- GitHub Issues: https://github.com/nanda-ai/nanda-agent/issues +- Email: support@nanda.ai -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. +## Changelog -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. \ No newline at end of file +### v1.0.0 +- Initial release +- Basic NANDA framework +- LangChain integration +- CrewAI integration +- Example agents +- Production deployment support \ No newline at end of file diff --git a/agents/start_running_agents.sh b/agents/start_running_agents.sh deleted file mode 100644 index d3cd2d4..0000000 --- a/agents/start_running_agents.sh +++ /dev/null @@ -1,139 +0,0 @@ -#!/bin/bash -source /opt/internet_of_agents/venv/bin/activate - -# Source the environment file -if [ -f "/etc/internet_of_agents.env" ]; then - source /etc/internet_of_agents.env -else - echo "Error: /etc/internet_of_agents.env not found" - exit 1 -fi - -# Configuration -START_BRIDGE_PORT=6000 -START_API_PORT=6001 - -# Check for required environment variables -if [ -z "$ANTHROPIC_API_KEY" ]; then - echo "Error: ANTHROPIC_API_KEY not found in environment file" - exit 1 -fi - -if [ -z "$AGENT_ID_PREFIX" ]; then - echo "Error: AGENT_ID_PREFIX not found in environment file" - exit 1 -fi - -if [ -z "$DOMAIN_NAME" ]; then - echo "Error: DOMAIN_NAME not found in environment file" - exit 1 -fi - -if [ -z "$REGISTRY_URL" ]; then - echo "Error: REGISTRY_URL not found in environment file" - exit 1 -fi - -# Use NUM_AGENTS from environment or default to 1 -NUM_AGENTS=${NUM_AGENTS:-1} -echo "Using NUM_AGENTS=$NUM_AGENTS" -echo "Using AGENT_ID_PREFIX=$AGENT_ID_PREFIX" -echo "Using DOMAIN_NAME=$DOMAIN_NAME" -echo "Using REGISTRY_URL=$REGISTRY_URL" - -# SSL Configuration -CERT_PATH="/etc/letsencrypt/live/${DOMAIN_NAME}/fullchain.pem" # Path to SSL certificate -KEY_PATH="/etc/letsencrypt/live/${DOMAIN_NAME}/privkey.pem" # Path to SSL private key - -# Create logs directory if it doesn't exist -mkdir -p logs - - - -# Generate the list of ports -BRIDGE_PORTS=() -API_PORTS=() -for ((i=0; i "logs/${AGENT_ID}_logs.txt" 2>&1 & - - # Store the process ID for later reference - echo "$!" > "logs/${AGENT_ID}.pid" - - echo "$AGENT_ID started with PID $!" - - # Wait a few seconds between agent starts to avoid race conditions - sleep 2 -done - -echo "All agents started successfully!" -echo "Use the following command to check if agents are running:" -echo "ps aux | grep run_ui_agent_https" -echo "" -echo "To stop all agents:" -echo 'for pid in logs/*.pid; do kill $(cat "$pid"); done' - -# Wait for 20 seconds before sending the email to ensure all the files are created -# sleep 20 - -# # Send agent links to the provided email -# if [ -n "$USER_EMAIL" ]; then -# echo "Preparing to send agent links to $USER_EMAIL..." - -# # Collect all agent IDs -# AGENT_IDS=() -# for pidfile in "/opt/internet_of_agents/agents/logs/${AGENT_ID_PREFIX}"*.pid; do -# AGENT_ID=$(basename "$pidfile" .pid) -# AGENT_IDS+=("$AGENT_ID") -# done - -# # Convert array to JSON format -# AGENT_IDS_JSON=$(printf '%s\n' "${AGENT_IDS[@]}" | jq -R . | jq -s .) - -# # Send to the API endpoint -# curl -X POST "https://chat.nanda-registry.com:6900/api/send-agent-links" \ -# -H "Content-Type: application/json" \ -# -d "{\"email\": \"$USER_EMAIL\", \"agentIds\": $AGENT_IDS_JSON}" - -# echo "Agent links sent to $USER_EMAIL via API" -# else -# echo "USER_EMAIL not set. Skipping email notification." -# fi - -wait \ No newline at end of file diff --git a/nanda_agent/__init__.py b/nanda_agent/__init__.py new file mode 100644 index 0000000..bd166ad --- /dev/null +++ b/nanda_agent/__init__.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python3 +""" +NANDA Agent Framework - Customizable AI Agent Communication System + +This package provides a framework for creating customizable AI agents with pluggable +message improvement logic, built on top of the python_a2a communication framework. +""" + +from .core.nanda import NANDA +from .core.agent_bridge import ( + AgentBridge, + message_improver, + register_message_improver, + get_message_improver, + list_message_improvers +) + +__version__ = "1.0.0" +__author__ = "NANDA Team" +__email__ = "support@nanda.ai" + +# Export main classes and functions +__all__ = [ + "NANDA", + "AgentBridge", + "message_improver", + "register_message_improver", + "get_message_improver", + "list_message_improvers" +] \ No newline at end of file diff --git a/nanda_agent/cli.py b/nanda_agent/cli.py new file mode 100644 index 0000000..3cf55bf --- /dev/null +++ b/nanda_agent/cli.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python3 +""" +NANDA Agent Framework - Command Line Interface +""" + +def main(): + """Main CLI entry point""" + print("NANDA Agent Framework") + print("Create custom agents with pluggable message improvement logic") + print() + print("Usage:") + print(" from nanda_agent import NANDA") + print(" ") + print(" def my_improvement_logic(message_text: str) -> str:") + print(" return f'Improved: {message_text}'") + print(" ") + print(" nanda = NANDA(my_improvement_logic)") + print(" nanda.start_server()") + print() + print("Environment Variables:") + print(" ANTHROPIC_API_KEY Your Anthropic API key (required)") + print(" AGENT_ID Custom agent ID (optional)") + print(" PORT Agent bridge port (default: 6000)") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/nanda_agent/core/__init__.py b/nanda_agent/core/__init__.py new file mode 100644 index 0000000..bea4fd4 --- /dev/null +++ b/nanda_agent/core/__init__.py @@ -0,0 +1,24 @@ +#!/usr/bin/env python3 +""" +NANDA Agent Framework - Core Components + +This module contains the core components of the NANDA agent framework. +""" + +from .nanda import NANDA +from .agent_bridge import ( + AgentBridge, + message_improver, + register_message_improver, + get_message_improver, + list_message_improvers +) + +__all__ = [ + "NANDA", + "AgentBridge", + "message_improver", + "register_message_improver", + "get_message_improver", + "list_message_improvers" +] \ No newline at end of file diff --git a/nanda_agent/core/__pycache__/__init__.cpython-311.pyc b/nanda_agent/core/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..a822d78 Binary files /dev/null and b/nanda_agent/core/__pycache__/__init__.cpython-311.pyc differ diff --git a/nanda_agent/core/__pycache__/agent_bridge.cpython-311.pyc b/nanda_agent/core/__pycache__/agent_bridge.cpython-311.pyc new file mode 100644 index 0000000..d0fadcf Binary files /dev/null and b/nanda_agent/core/__pycache__/agent_bridge.cpython-311.pyc differ diff --git a/nanda_agent/core/__pycache__/mcp_utils.cpython-311.pyc b/nanda_agent/core/__pycache__/mcp_utils.cpython-311.pyc new file mode 100644 index 0000000..928fe9a Binary files /dev/null and b/nanda_agent/core/__pycache__/mcp_utils.cpython-311.pyc differ diff --git a/nanda_agent/core/__pycache__/nanda.cpython-311.pyc b/nanda_agent/core/__pycache__/nanda.cpython-311.pyc new file mode 100644 index 0000000..2676e6b Binary files /dev/null and b/nanda_agent/core/__pycache__/nanda.cpython-311.pyc differ diff --git a/nanda_agent/core/__pycache__/run_ui_agent_https.cpython-311.pyc b/nanda_agent/core/__pycache__/run_ui_agent_https.cpython-311.pyc new file mode 100644 index 0000000..f71f082 Binary files /dev/null and b/nanda_agent/core/__pycache__/run_ui_agent_https.cpython-311.pyc differ diff --git a/agents/agent_bridge.py b/nanda_agent/core/agent_bridge.py similarity index 78% rename from agents/agent_bridge.py rename to nanda_agent/core/agent_bridge.py index 8307dce..540ec0d 100644 --- a/agents/agent_bridge.py +++ b/nanda_agent/core/agent_bridge.py @@ -10,7 +10,7 @@ from anthropic import Anthropic, APIStatusError from python_a2a import ( A2AServer, A2AClient, run_server, - Message, TextContent, MessageRole, ErrorContent + Message, TextContent, MessageRole, ErrorContent, Metadata ) # MongoDB from pymongo import MongoClient @@ -31,7 +31,10 @@ anthropic = Anthropic(api_key=ANTHROPIC_API_KEY) # Get agent configuration from environment variables -AGENT_ID = os.getenv("AGENT_ID", "default") # Default to 'default' if not specified +def get_agent_id(): + """Get AGENT_ID dynamically from environment variables""" + return os.getenv("AGENT_ID", "default") + PORT = int(os.getenv("PORT", "6000")) TERMINAL_PORT = int(os.getenv("TERMINAL_PORT", "6010")) @@ -39,7 +42,7 @@ LOCAL_TERMINAL_URL = f"http://localhost:{TERMINAL_PORT}/a2a" # UI client support -UI_MODE = os.getenv("UI_MODE", "false").lower() in ("true", "1", "yes", "y") +UI_MODE = os.getenv("UI_MODE", "true").lower() in ("true", "1", "yes", "y") UI_CLIENT_URL = os.getenv("UI_CLIENT_URL", "") registered_ui_clients = set() @@ -90,7 +93,7 @@ def get_registry_url(): print(f"Error reading registry URL from file: {e}") # Default if file doesn't exist - default_url = "http://localhost:6900" + default_url = "https://chat.nanda-registry.com:6900" print(f"Using default registry URL: {default_url}") return default_url @@ -192,7 +195,8 @@ def call_claude(prompt: str, additional_context: str, conversation_id: str, curr if additional_context and additional_context.strip(): full_prompt = f"ADDITIONAL CONTEXT FRseOM USER: {additional_context}\n\nMESSAGE: {prompt}" - print(f"Agent {AGENT_ID}: Calling Claude with prompt: {full_prompt[:50]}...") + agent_id = get_agent_id() + print(f"Agent {agent_id}: Calling Claude with prompt: {full_prompt[:50]}...") resp = anthropic.messages.create( model="claude-3-5-sonnet-20241022", max_tokens=512, @@ -202,16 +206,47 @@ def call_claude(prompt: str, additional_context: str, conversation_id: str, curr response_text = resp.content[0].text # Log the Claude response - log_message(conversation_id, current_path, f"Claude {AGENT_ID}", response_text) + log_message(conversation_id, current_path, f"Claude {agent_id}", response_text) return response_text except APIStatusError as e: - print(f"Agent {AGENT_ID}: Anthropic API error:", e.status_code, e.message, flush=True) + print(f"Agent {agent_id}: Anthropic API error:", e.status_code, e.message, flush=True) # If we hit a credit limit error, return a fallback message if "credit balance is too low" in str(e): - return f"Agent {AGENT_ID} processed (API credit limit reached): {prompt}" + return f"Agent {agent_id} processed (API credit limit reached): {prompt}" except Exception as e: - print(f"Agent {AGENT_ID}: Anthropic SDK error:", e, flush=True) + print(f"Agent {agent_id}: Anthropic SDK error:", e, flush=True) + traceback.print_exc() + return None + +def call_claude_direct(message_text: str, system_prompt: str = None) -> Optional[str]: + """Wrapper that never raises: returns text or None on failure.""" + try: + # Use the specified system prompt or default to the agent's system prompt + + # Combine the prompt with additional context if provided + full_prompt = f"MESSAGE: {message_text}" + + agent_id = get_agent_id() + print(f"Agent {agent_id}: Calling Claude with prompt: {full_prompt[:50]}...") + resp = anthropic.messages.create( + model="claude-3-5-sonnet-20241022", + max_tokens=512, + messages=[{"role":"user","content":full_prompt}], + system=system_prompt + ) + response_text = resp.content[0].text + + # Log the Claude response + + return response_text + except APIStatusError as e: + print(f"Agent {agent_id}: Anthropic API error:", e.status_code, e.message, flush=True) + # If we hit a credit limit error, return a fallback message + if "credit balance is too low" in str(e): + return f"Agent {agent_id} processed (API credit limit reached): {message_text}" + except Exception as e: + print(f"Agent {agent_id}: Anthropic SDK error:", e, flush=True) traceback.print_exc() return None @@ -236,17 +271,19 @@ def improve_message(message_text: str, conversation_id: str, current_path: str, print(f"Error improving message: {e}") return message_text + + def send_to_terminal(text, terminal_url, conversation_id, metadata=None): """Send a message to a terminal""" try: print(f"Sending message to {terminal_url}: {text[:50]}...") terminal = A2AClient(terminal_url, timeout=30) - terminal.send_message_async( + terminal.send_message_threaded( Message( role=MessageRole.USER, content=TextContent(text=text), conversation_id=conversation_id, - metadata=metadata or {} + metadata=Metadata(custom_fields=metadata or {}) ) ) return True @@ -256,14 +293,18 @@ def send_to_terminal(text, terminal_url, conversation_id, metadata=None): def send_to_ui_client(message_text, from_agent, conversation_id): - if not UI_CLIENT_URL: + # Read UI_CLIENT_URL dynamically to get the latest value + ui_client_url = os.getenv("UI_CLIENT_URL", "") + print(f"šŸ” Dynamic UI_CLIENT_URL: '{ui_client_url}'") + + if not ui_client_url: print(f"No UI client URL configured. Cannot send message to UI client") return False try: print(f"Sending message to UI client: {message_text[:50]}...") response = requests.post( - UI_CLIENT_URL, + ui_client_url, json={ "message": message_text, "from_agent": from_agent, @@ -303,14 +344,15 @@ def send_to_agent(target_agent_id, message_text, conversation_id, metadata=None) # Use the URL directly (it already includes /a2a from registration) print(f"Sending message to {target_agent_id} at {target_bridge_url}") - formatted_message = f"__EXTERNAL_MESSAGE__\n__FROM_AGENT__{AGENT_ID}\n__TO_AGENT__{target_agent_id}\n__MESSAGE_START__\n{message_text}\n__MESSAGE_END__" + agent_id = get_agent_id() + formatted_message = f"__EXTERNAL_MESSAGE__\n__FROM_AGENT__{agent_id}\n__TO_AGENT__{target_agent_id}\n__MESSAGE_START__\n{message_text}\n__MESSAGE_END__" # Create simplified metadata try: # For python_a2a library compatibility, still try to set some metadata send_metadata = { 'is_external': True, - 'from_agent_id': AGENT_ID, + 'from_agent_id': agent_id, 'to_agent_id': target_agent_id } if metadata: @@ -332,7 +374,7 @@ def send_to_agent(target_agent_id, message_text, conversation_id, metadata=None) role=MessageRole.USER, content=TextContent(text=formatted_message), conversation_id=conversation_id, - metadata=send_metadata + metadata=Metadata(custom_fields=send_metadata) if send_metadata else None ) ) @@ -416,17 +458,17 @@ async def run_mcp_query(query: str, updated_url: str) -> str: error_msg = f"Error processing MCP query: {str(e)}" return error_msg -# Add the async method to the A2AClient class if it doesn't exist -if not hasattr(A2AClient, 'send_message_async'): - def send_message_async(self, message: Message): - """Send a message asynchronously without waiting for a response""" +# Add the threaded method to the A2AClient class if it doesn't exist +if not hasattr(A2AClient, 'send_message_threaded'): + def send_message_threaded(self, message: Message): + """Send a message in a separate thread without waiting for a response""" thread = threading.Thread(target=self.send_message, args=(message,)) thread.daemon = True thread.start() return thread # Add the method to the class - A2AClient.send_message_async = send_message_async + A2AClient.send_message_threaded = send_message_threaded # Update handle_message to detect this special format @@ -476,9 +518,10 @@ def handle_external_message(msg_text, conversation_id, msg): send_to_ui_client(formatted_text, from_agent, conversation_id) # Acknowledge receipt to sender + agent_id = get_agent_id() return Message( role=MessageRole.AGENT, - content=TextContent(text=f"Message received by Agent {AGENT_ID}"), + content=TextContent(text=f"Message received by Agent {agent_id}"), parent_message_id=msg.message_id, conversation_id=conversation_id ) @@ -486,24 +529,25 @@ def handle_external_message(msg_text, conversation_id, msg): else: try: terminal_client = A2AClient(LOCAL_TERMINAL_URL, timeout=10) - terminal_client.send_message_async( + terminal_client.send_message_threaded( Message( role=MessageRole.USER, content=TextContent(text=formatted_text), conversation_id=conversation_id, - metadata={ + metadata=Metadata(custom_fields={ 'is_from_peer': True, 'is_user_message': True, 'source_agent': from_agent, 'forwarded_by_bridge': True - } + }) ) ) # Acknowledge receipt to sender + agent_id = get_agent_id() return Message( role=MessageRole.AGENT, - content=TextContent(text=f"Message received by Agent {AGENT_ID}"), + content=TextContent(text=f"Message received by Agent {agent_id}"), parent_message_id=msg.message_id, conversation_id=conversation_id ) @@ -521,19 +565,96 @@ def handle_external_message(msg_text, conversation_id, msg): return None # Not our special format or parsing failed +# Message improvement decorator system +message_improvement_decorators = {} + +def message_improver(name=None): + """Decorator to register message improvement functions""" + def decorator(func): + decorator_name = name or func.__name__ + message_improvement_decorators[decorator_name] = func + return func + return decorator + +def register_message_improver(name, improver_func): + """Register a custom message improver function""" + message_improvement_decorators[name] = improver_func + +def get_message_improver(name): + """Get a registered message improver by name""" + return message_improvement_decorators.get(name) + +def list_message_improvers(): + """List all registered message improvers""" + return list(message_improvement_decorators.keys()) + +# Default improver +@message_improver("default_claude") +def default_claude_improver(message_text: str) -> str: + """Default Claude-based message improvement""" + if not IMPROVE_MESSAGES: + return message_text + + try: + additional_prompt = "Do not respond to the content of the message - it's intended for another agent. You are helping an agent communicate better with other agennts." + system_prompt = additional_prompt + IMPROVE_MESSAGE_PROMPTS["default"] + print(system_prompt) + improved_message = call_claude_direct(message_text, system_prompt) + print(f"Improved message: {improved_message}") + return improved_message if improved_message else message_text + except Exception as e: + print(f"Error improving message: {e}") + return message_text + class AgentBridge(A2AServer): """Global Agent Bridge - Can be used for any agent in the network.""" + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.active_improver = "default_claude" # Default improver + + def set_message_improver(self, improver_name): + """Set the active message improver by name""" + if improver_name in message_improvement_decorators: + self.active_improver = improver_name + print(f"Message improver set to: {improver_name}") + return True + else: + print(f"Unknown improver: {improver_name}. Available: {list_message_improvers()}") + return False + + def set_custom_improver(self, improver_func, name="custom"): + """Set a custom improver function""" + register_message_improver(name, improver_func) + self.active_improver = name + print(f"Custom message improver '{name}' registered and activated") + + def improve_message_direct(self, message_text: str) -> str: + """Improve a message using the active registered improver.""" + # Get the active improver function + improver_func = message_improvement_decorators.get(self.active_improver) + + if improver_func: + try: + return improver_func(message_text) + except Exception as e: + print(f"Error with improver '{self.active_improver}': {e}") + return message_text + else: + print(f"No improver found: {self.active_improver}") + return message_text + def handle_message(self, msg: Message) -> Message: # Ensure we have a conversation ID conversation_id = msg.conversation_id or str(uuid.uuid4()) - print(f"Agent {AGENT_ID}: Received message with ID: {msg.message_id}") + agent_id = get_agent_id() + print(f"Agent {agent_id}: Received message with ID: {msg.message_id}") print(f"[DEBUG] Message type: {type(msg.content)}") print(f"[DEBUG] Message ID: {msg.message_id}") - print(f"Agent {AGENT_ID}: Message metadata: {msg.metadata}") + print(f"Agent {agent_id}: Message metadata: {msg.metadata}") user_text = msg.content.text - print(f"Agent {AGENT_ID}: Received text: {user_text[:50]}...") + print(f"Agent {agent_id}: Received text: {user_text[:50]}...") # Extract metadata if hasattr(msg.metadata, 'custom_fields'): @@ -553,12 +674,13 @@ def handle_message(self, msg: Message) -> Message: additional_context = metadata.get('additional_context', '') # Add current agent ID to the path - current_path = path + ('>' if path else '') + AGENT_ID - print(f"Agent {AGENT_ID}: Current path: {current_path}") + agent_id = get_agent_id() + current_path = path + ('>' if path else '') + agent_id + print(f"Agent {agent_id}: Current path: {current_path}") # Handle non-text content if not isinstance(msg.content, TextContent): - print(f"Agent {AGENT_ID}: Received non-text content. Returning error.") + print(f"Agent {agent_id}: Received non-text content. Returning error.") return Message( role = MessageRole.AGENT, content = ErrorContent(message="Only text payloads supported."), @@ -585,7 +707,7 @@ def handle_message(self, msg: Message) -> Message: ) else: # Message from local terminal user - log_message(conversation_id, current_path, f"Local user to Agent {AGENT_ID}", user_text) + log_message(conversation_id, current_path, f"Local user to Agent {agent_id}", user_text) print(f"#jinu - User text: {user_text}") # Check if this is a message to another agent (starts with @) if user_text.startswith("@"): @@ -597,21 +719,23 @@ def handle_message(self, msg: Message) -> Message: # Improve message if feature is enabled if IMPROVE_MESSAGES: - message_text = improve_message(message_text, conversation_id, current_path, - "Do not respond to the content of the message - it's intended for another agent. You are helping an agent communicate better with other agennts.") - + # message_text = improve_message(message_text, conversation_id, current_path, + # "Do not respond to the content of the message - it's intended for another agent. You are helping an agent communicate better with other agennts.") + message_text = self.improve_message_direct(message_text) + log_message(conversation_id, current_path, f"Claude {agent_id}", message_text) + print(f"#jinu - Target agent: {target_agent}") print(f"#jinu - Imoproved message text: {message_text}") # Send to the target agent's bridge result = send_to_agent(target_agent, message_text, conversation_id, { 'path': current_path, - 'source_agent': AGENT_ID + 'source_agent': agent_id }) # Return result to user return Message( role=MessageRole.AGENT, - content=TextContent(text=f"[AGENT {AGENT_ID}]: {message_text}"), + content=TextContent(text=f"[AGENT {agent_id}]: {message_text}"), parent_message_id=msg.message_id, conversation_id=conversation_id ) @@ -619,7 +743,7 @@ def handle_message(self, msg: Message) -> Message: # Invalid @ command format return Message( role=MessageRole.AGENT, - content=TextContent(text=f"[AGENT {AGENT_ID}] Invalid format. Use '@agent_id message' to send a message."), + content=TextContent(text=f"[AGENT {agent_id}] Invalid format. Use '@agent_id message' to send a message."), parent_message_id=msg.message_id, conversation_id=conversation_id ) @@ -639,7 +763,7 @@ def handle_message(self, msg: Message) -> Message: if response is None: return Message( role=MessageRole.AGENT, - content=TextContent(text=f"[AGENT {AGENT_ID}] MCP server '{mcp_server_to_call}' not found in registry. Please check the server name and try again."), + content=TextContent(text=f"[AGENT {agent_id}] MCP server '{mcp_server_to_call}' not found in registry. Please check the server name and try again."), parent_message_id=msg.message_id, conversation_id=conversation_id ) @@ -652,7 +776,7 @@ def handle_message(self, msg: Message) -> Message: if mcp_server_final_url is None: return Message( role=MessageRole.AGENT, - content=TextContent(text=f"[AGENT {AGENT_ID}] Ensure the required API key for registery is in env file"), + content=TextContent(text=f"[AGENT {agent_id}] Ensure the required API key for registery is in env file"), parent_message_id=msg.message_id, conversation_id=conversation_id ) @@ -671,7 +795,7 @@ def handle_message(self, msg: Message) -> Message: # Invalid # command format return Message( role=MessageRole.AGENT, - content=TextContent(text=f"[AGENT {AGENT_ID}] Invalid format. Use '#registry_provider:mcp_server_name query' to send a query to an MCP server."), + content=TextContent(text=f"[AGENT {agent_id}] Invalid format. Use '#registry_provider:mcp_server_name query' to send a query to an MCP server."), parent_message_id=msg.message_id, conversation_id=conversation_id ) @@ -687,7 +811,7 @@ def handle_message(self, msg: Message) -> Message: # Quit command - acknowledge but let terminal handle the actual quitting return Message( role = MessageRole.AGENT, - content = TextContent(text=f"[AGENT {AGENT_ID}] Exiting session..."), + content = TextContent(text=f"[AGENT {agent_id}] Exiting session..."), parent_message_id = msg.message_id, conversation_id = conversation_id ) @@ -701,7 +825,7 @@ def handle_message(self, msg: Message) -> Message: @ [message] - Send a message to a specific agent""" return Message( role = MessageRole.AGENT, - content = TextContent(text=f"[AGENT {AGENT_ID}] {help_text}"), + content = TextContent(text=f"[AGENT {agent_id}] {help_text}"), parent_message_id = msg.message_id, conversation_id = conversation_id ) @@ -725,7 +849,7 @@ def handle_message(self, msg: Message) -> Message: print(f"Response preview: {claude_response[:50]}...") # Format and return the response - formatted_response = f"[AGENT {AGENT_ID}] {claude_response}" + formatted_response = f"[AGENT {agent_id}] {claude_response}" # Return to local terminal response_message = Message( @@ -740,7 +864,7 @@ def handle_message(self, msg: Message) -> Message: # No query text provided return Message( role = MessageRole.AGENT, - content = TextContent(text=f"[AGENT {AGENT_ID}] Please provide a query after the /query command."), + content = TextContent(text=f"[AGENT {agent_id}] Please provide a query after the /query command."), parent_message_id = msg.message_id, conversation_id = conversation_id ) @@ -753,7 +877,7 @@ def handle_message(self, msg: Message) -> Message: @ [message] - Send a message to a specific agent""" return Message( role = MessageRole.AGENT, - content = TextContent(text=f"[AGENT {AGENT_ID}] {help_text}"), + content = TextContent(text=f"[AGENT {agent_id}] {help_text}"), parent_message_id = msg.message_id, conversation_id = conversation_id ) @@ -761,7 +885,7 @@ def handle_message(self, msg: Message) -> Message: else: # Regular message - process locally claude_response = call_claude(user_text, additional_context, conversation_id, current_path) or user_text - formatted_response = f"[AGENT {AGENT_ID}] {claude_response}" + formatted_response = f"[AGENT {agent_id}] {claude_response}" # Return Claude's response to local terminal return Message( @@ -776,13 +900,15 @@ def handle_message(self, msg: Message) -> Message: public_url = os.getenv("PUBLIC_URL") api_url = os.getenv("API_URL") if public_url: - register_with_registry(AGENT_ID, public_url, api_url) + agent_id = get_agent_id() + register_with_registry(agent_id, public_url, api_url) else: print("WARNING: PUBLIC_URL environment variable not set. Agent will not be registered.") IMPROVE_MESSAGES = os.getenv("IMPROVE_MESSAGES", "true").lower() in ("true", "1", "yes", "y") - print(f"Starting Agent {AGENT_ID} bridge on port {PORT}") + agent_id = get_agent_id() + print(f"Starting Agent {agent_id} bridge on port {PORT}") print(f"Agent terminal port: {TERMINAL_PORT}") print(f"Message improvement feature is {'ENABLED' if IMPROVE_MESSAGES else 'DISABLED'}") print(f"Logging conversations to {os.path.abspath(LOG_DIR)}") diff --git a/agents/mcp_utils.py b/nanda_agent/core/mcp_utils.py similarity index 100% rename from agents/mcp_utils.py rename to nanda_agent/core/mcp_utils.py diff --git a/nanda_agent/core/nanda.py b/nanda_agent/core/nanda.py new file mode 100644 index 0000000..ad44831 --- /dev/null +++ b/nanda_agent/core/nanda.py @@ -0,0 +1,302 @@ +#!/usr/bin/env python3 +""" +NANDA - Custom Message Improvement for Agent Bridge +- Accepts any custom improvement logic function +- Creates agent_bridge server with custom improve_message_direct +""" + +import os +import sys +import subprocess +import time +import signal +import requests +import random +import threading + +# Handle different import contexts +try: + from .agent_bridge import * + from . import run_ui_agent_https +except ImportError: + # If running from parent directory, add current directory to path + current_dir = os.path.dirname(os.path.abspath(__file__)) + sys.path.insert(0, current_dir) + from agent_bridge import * + import run_ui_agent_https + +class NANDA: + """NANDA class to create agent_bridge with custom improvement logic""" + + def __init__(self, improvement_logic): + """ + Initialize NANDA with custom improvement logic + + Args: + improvement_logic: Function that takes (message_text: str) -> str + """ + self.improvement_logic = improvement_logic + self.bridge = None + print(f"šŸ¤– NANDA initialized with custom improvement logic: {improvement_logic.__name__}") + + # Register the custom improvement logic + self.register_custom_improver() + + # Create agent bridge with custom logic + self.create_agent_bridge() + + def register_custom_improver(self): + """Register the custom improvement logic with agent_bridge""" + register_message_improver("nanda_custom", self.improvement_logic) + print(f"šŸ”§ Custom improvement logic '{self.improvement_logic.__name__}' registered") + + def create_agent_bridge(self): + """Create AgentBridge with custom improvement logic""" + # Create standard AgentBridge + self.bridge = AgentBridge() + + # Set custom improver as active (replaces improve_message_direct) + self.bridge.set_message_improver("nanda_custom") + print(f"āœ… AgentBridge created with custom improve_message_direct: {self.improvement_logic.__name__}") + + def start_server(self): + """Start the agent_bridge server with custom improvement logic""" + print("šŸš€ NANDA starting agent_bridge server with custom logic...") + + # Register with the registry if PUBLIC_URL is set + public_url = os.getenv("PUBLIC_URL") + api_url = os.getenv("API_URL") + agent_id = os.getenv("AGENT_ID") + + ANTHROPIC_API_KEY = os.getenv("ANTHROPIC_API_KEY") or "your key" + AGENT_ID = os.getenv("AGENT_ID", "default") # Default to 'default' if not specified + PORT = int(os.getenv("PORT", "6000")) + TERMINAL_PORT = int(os.getenv("TERMINAL_PORT", "6010")) + + + UI_MODE = os.getenv("UI_MODE", "true").lower() in ("true", "1", "yes", "y") + UI_CLIENT_URL = os.getenv("UI_CLIENT_URL", "") + print(f"šŸ”§ UI_CLIENT_URL: {UI_CLIENT_URL}") + + # os.environ["ANTHROPIC_API_KEY"] = ANTHROPIC_API_KEY + # os.environ["AGENT_ID"] = AGENT_ID + # os.environ["PORT"] = str(PORT) + # os.environ["PUBLIC_URL"] = public_url + # os.environ['API_URL'] = api_url + # os.environ["REGISTRY_URL"] = run_ui_agent_https.get_registry_url() + # os.environ["UI_MODE"] = "true" + # os.environ["UI_CLIENT_URL"] = f"{api_url}/api/receive_message" + + if public_url: + register_with_registry(agent_id, public_url, api_url) + else: + print("WARNING: PUBLIC_URL environment variable not set. Agent will not be registered.") + + + # Start the server + IMPROVE_MESSAGES = os.getenv("IMPROVE_MESSAGES", "true").lower() in ("true", "1", "yes", "y") + + print(f"\nšŸš€ Starting Agent {AGENT_ID} bridge on port {PORT}") + print(f"Agent terminal port: {TERMINAL_PORT}") + print(f"Message improvement feature is {'ENABLED' if IMPROVE_MESSAGES else 'DISABLED'}") + print(f"Logging conversations to {os.path.abspath(LOG_DIR)}") + print(f"šŸ”§ Using custom improvement logic: {self.improvement_logic.__name__}") + + # Run the agent bridge server + run_server(self.bridge, host="0.0.0.0", port=PORT) + + def start_server_api(self, anthropic_key, domain, agent_id=None, port=6000, api_port=6001, + registry=None, public_url=None, api_url=None, cert=None, key=None, ssl=True): + """ + Start NANDA API server using run_ui_agent_https module + + Args: + anthropic_key (str): Anthropic API key + domain (str): Domain name for the server + agent_id (str): Agent ID (default: auto-generated based on domain) + port (int): Agent bridge port (default: 6000) + api_port (int): Flask API port (default: 6001) + registry (str): Registry URL (optional) + public_url (str): Public URL for the Agent Bridge (optional) + api_url (str): API URL for the User Client (optional) + cert (str): Path to SSL certificate file (optional, defaults to Let's Encrypt path) + key (str): Path to SSL key file (optional, defaults to Let's Encrypt path) + ssl (bool): Enable SSL (default: True, uses Let's Encrypt certificates) + """ + # Get the server IP address (assumes a public IP) + def get_server_ip(): + """Get the public IP address of the server""" + try: + print("🌐 Detecting server IP address...") + # Try first method + response = requests.get("http://checkip.amazonaws.com", timeout=10) + if response.status_code == 200: + server_ip = response.text.strip() + print(f"āœ… Detected server IP: {server_ip}") + return server_ip + except Exception as e: + print(f"āš ļø First IP detection method failed: {e}") + + try: + # Try second method + response = requests.get("http://ifconfig.me", timeout=10) + if response.status_code == 200: + server_ip = response.text.strip() + print(f"āœ… Detected server IP (fallback): {server_ip}") + return server_ip + except Exception as e: + print(f"āš ļø Second IP detection method failed: {e}") + + # If both methods fail, use localhost + server_ip = "localhost" + print(f"āš ļø Could not determine IP automatically, using default: {server_ip}") + return server_ip + + # Set up signal handlers for cleanup + def cleanup(signum=None, frame=None): + """Clean up processes on exit""" + print("Cleaning up processes...") + if hasattr(run_ui_agent_https, 'bridge_process') and run_ui_agent_https.bridge_process: + run_ui_agent_https.bridge_process.terminate() + sys.exit(0) + + signal.signal(signal.SIGINT, cleanup) + signal.signal(signal.SIGTERM, cleanup) + + # Get server IP + server_ip = get_server_ip() + + # Set default agent ID if not provided + if not agent_id: + # Generate 6-digit random number + random_number = random.randint(100000, 999999) + + # Check domain pattern for agent naming + if "nanda-registry.com" in domain: + agent_id = f"agentm{random_number}" + else: + agent_id = f"agents{random_number}" + + print(f"šŸ¤– Auto-generated agent ID: {agent_id}") + + # Set global variables in run_ui_agent_https module + run_ui_agent_https.agent_id = agent_id + run_ui_agent_https.agent_port = port + run_ui_agent_https.registry_url = registry + + # Set default URLs if not provided + if not public_url: + public_url = f"http://{server_ip}:{port}" + print(f"šŸ”— Auto-generated public URL: {public_url}") + + if not api_url: + protocol = "https" if ssl else "http" + api_url = f"{protocol}://{domain}:{api_port}" + + # Set environment variables for the agent bridge (same as run_ui_agent_https main()) + os.environ["ANTHROPIC_API_KEY"] = anthropic_key + os.environ["AGENT_ID"] = agent_id + os.environ["PORT"] = str(port) + os.environ["PUBLIC_URL"] = public_url + os.environ['API_URL'] = api_url + os.environ["REGISTRY_URL"] = run_ui_agent_https.get_registry_url() + os.environ["UI_MODE"] = "true" + os.environ["UI_CLIENT_URL"] = f"{api_url}/api/receive_message" + + # Create unique log directories for each agent + log_dir = f"logs_{agent_id}" + os.makedirs(log_dir, exist_ok=True) + os.environ["LOG_DIR"] = log_dir + + # Open log file + log_file = open(f"{log_dir}/bridge_run.txt", "a") + + # Start the agent bridge using the start_server method in a separate thread + def start_bridge_server(): + """Start the bridge server in a separate thread""" + print(f"šŸš€ Starting agent bridge for {agent_id} on port {port}...") + self.start_server() + + # Start the bridge server in a non-daemon thread + bridge_thread = threading.Thread(target=start_bridge_server, daemon=False) + bridge_thread.start() + + # Give the bridge a moment to start + time.sleep(2) + + # Print server information + print("\n" + "="*50) + print(f"šŸ¤– Agent {agent_id} is running") + print(f"🌐 Server IP: {server_ip}") + print(f"Agent Bridge URL: http://localhost:{port}/a2a") + print(f"Public Client API URL: {public_url}") + print("="*50) + print("\nšŸ“” API Endpoints:") + print(f" GET {api_url}/api/health - Health check") + print(f" POST {api_url}/api/send - Send a message to the client") + print(f" GET {api_url}/api/agents/list - List all registered agents") + print(f" POST {api_url}/api/receive_message - Receive a message from agent") + print(f" GET {api_url}/api/render - Get the latest message") + print("\nšŸ›‘ Press Ctrl+C to stop all processes.") + + # Configure SSL context if needed + ssl_context = None + if ssl: + # Set default certificate paths from current folder if not provided + if not cert or not key: + cert = "./fullchain.pem" + key = "./privkey.pem" + print(f"šŸ”’ Using certificates from current folder: cert={cert}, key={key}") + + if os.path.exists(cert) and os.path.exists(key): + ssl_context = (cert, key) + print(f"šŸ”’ Using SSL certificates from: {cert}, {key}") + else: + print("āŒ ERROR: Certificate files not found at specified paths") + print(f"Certificate path: {cert}") + print(f"Key path: {key}") + print(f"šŸ’” Make sure Let's Encrypt certificates exist for domain: {domain}") + print(f"šŸ’” You can generate them with: certbot --nginx -d {domain}") + sys.exit(1) + + # Start the Flask API server in a separate thread + def start_flask_server(): + """Start the Flask API server in a separate thread""" + try: + print(f"šŸš€ Starting Flask API server on port {api_port}...") + run_ui_agent_https.app.run( + host='0.0.0.0', + port=api_port, + threaded=True, + ssl_context=ssl_context + ) + except Exception as e: + print(f"āŒ Error starting Flask server: {e}") + + # Start the Flask server in a non-daemon thread + flask_thread = threading.Thread(target=start_flask_server, daemon=False) + flask_thread.start() + + # Give the Flask server a moment to start + time.sleep(2) + + print(f"āœ… Both servers are now running in background threads") + print(f"šŸ”§ Agent Bridge: http://localhost:{port}") + print(f"šŸ”§ Flask API: {'https' if ssl else 'http'}://localhost:{api_port}") + + print("šŸš€ Both servers started successfully!") + print("šŸ“ Servers are running in background threads") + print("šŸ’” To run in background, use: python3 script.py &") + + + print("******************************************************") + print("You can assign your agent using this link") + print(f"https://chat.nanda-registry.com/landing.html?agentId={agent_id}") + print("******************************************************") + # Keep the main process alive so threads continue running + try: + while True: + time.sleep(1) + except KeyboardInterrupt: + print("\nšŸ›‘ Server stopped by user") + cleanup() \ No newline at end of file diff --git a/agents/registry_url.txt b/nanda_agent/core/registry_url.txt similarity index 100% rename from agents/registry_url.txt rename to nanda_agent/core/registry_url.txt diff --git a/agents/run_ui_agent_https.py b/nanda_agent/core/run_ui_agent_https.py similarity index 99% rename from agents/run_ui_agent_https.py rename to nanda_agent/core/run_ui_agent_https.py index 23872af..8601359 100644 --- a/agents/run_ui_agent_https.py +++ b/nanda_agent/core/run_ui_agent_https.py @@ -10,7 +10,7 @@ import json from flask import Flask, request, jsonify, Response, stream_with_context from flask_cors import CORS -from python_a2a import A2AClient, Message, TextContent, MessageRole +from python_a2a import A2AClient, Message, TextContent, MessageRole, Metadata from queue import Queue from threading import Event import ssl @@ -169,7 +169,7 @@ def send_message(): role=MessageRole.USER, content=TextContent(text=message_text), conversation_id=conversation_id, - metadata=metadata + metadata=Metadata(custom_fields=metadata) ) ) print(f"Response: {response}") diff --git a/nanda_agent/examples/crewai_sarcastic.py b/nanda_agent/examples/crewai_sarcastic.py new file mode 100644 index 0000000..1559f73 --- /dev/null +++ b/nanda_agent/examples/crewai_sarcastic.py @@ -0,0 +1,88 @@ +#!/usr/bin/env python3 +import os +from nanda_agent import NANDA +from crewai import Agent, Task, Crew +from langchain_anthropic import ChatAnthropic + +def create_sarcastic_improvement(): + """Create a CrewAI-powered sarcastic improvement function""" + + # Initialize the LLM + llm = ChatAnthropic( + api_key=os.getenv("ANTHROPIC_API_KEY"), + model="claude-3-haiku-20240307" + ) + + # Create a sarcastic agent + sarcastic_agent = Agent( + role="Sarcastic Message Transformer", + goal="Transform messages into witty, sarcastic responses while maintaining the core meaning", + backstory="""You are a master of sarcasm and wit. You excel at taking ordinary messages + and transforming them into clever, sarcastic versions that are humorous but not mean-spirited. + You use techniques like irony, exaggeration, and dry humor to make messages more entertaining.""", + verbose=True, + allow_delegation=False, + llm=llm + ) + + def sarcastic_improvement(message_text: str) -> str: + """Transform message to sarcastic version""" + try: + # Create a task for the sarcastic transformation + sarcastic_task = Task( + description=f"""Transform the following message into a sarcastic, witty version. + Use sarcasm, irony, and dry humor while keeping the core meaning intact. + Make it entertaining but not offensive or mean-spirited. + + Original message: {message_text} + + Provide only the sarcastic transformation, no explanations.""", + expected_output="A sarcastic, witty version of the original message", + agent=sarcastic_agent + ) + + # Create and run the crew + crew = Crew( + agents=[sarcastic_agent], + tasks=[sarcastic_task], + verbose=True + ) + + result = crew.kickoff() + return str(result).strip() + + except Exception as e: + print(f"Error in sarcastic improvement: {e}") + return f"Oh wow, {message_text}. How absolutely groundbreaking." # Fallback sarcastic transformation + + return sarcastic_improvement + +def main(): + """Main function to start the sarcastic agent""" + + # Check for API key + if not os.getenv("ANTHROPIC_API_KEY"): + print("Please set your ANTHROPIC_API_KEY environment variable") + return + + # Create sarcastic improvement function + sarcastic_logic = create_sarcastic_improvement() + + # Initialize NANDA with sarcastic logic + nanda = NANDA(sarcastic_logic) + + # Start the server + print("Starting Sarcastic Agent with CrewAI...") + print("All messages will be transformed to sarcastic responses!") + + domain = os.getenv("DOMAIN_NAME", "localhost") + + if domain != "localhost": + # Production with SSL + nanda.start_server_api(os.getenv("ANTHROPIC_API_KEY"), domain) + else: + # Development server + nanda.start_server() + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/nanda_agent/examples/langchain_pirate.py b/nanda_agent/examples/langchain_pirate.py new file mode 100644 index 0000000..5f95857 --- /dev/null +++ b/nanda_agent/examples/langchain_pirate.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python3 +import os +from nanda_agent import NANDA +from langchain_core.prompts import PromptTemplate +from langchain_core.output_parsers import StrOutputParser +from langchain_anthropic import ChatAnthropic + +def create_pirate_improvement(): + """Create a LangChain-powered pirate improvement function""" + + # Initialize the LLM + llm = ChatAnthropic( + api_key=os.getenv("ANTHROPIC_API_KEY"), + model="claude-3-haiku-20240307" + ) + + # Create a prompt template for pirate transformation + prompt = PromptTemplate( + input_variables=["message"], + template="""Transform the following message into pirate +English. + Use pirate vocabulary, grammar, and expressions like 'ahoy', +'matey', 'ye', 'arrr', etc. + Keep the core meaning intact but make it sound like a pirate +would say it. + + Original message: {message} + + Pirate version:""" + ) + + # Create the chain + chain = prompt | llm | StrOutputParser() + + def pirate_improvement(message_text: str) -> str: + """Transform message to pirate English""" + try: + result = chain.invoke({"message": message_text}) + return result.strip() + except Exception as e: + print(f"Error in pirate improvement: {e}") + return f"Arrr! {message_text}, matey!" # Fallback pirate transformation + + return pirate_improvement + +def main(): + """Main function to start the pirate agent""" + + # Check for API key + if not os.getenv("ANTHROPIC_API_KEY"): + print("Please set your ANTHROPIC_API_KEY environment variable") + return + + # Create pirate improvement function + pirate_logic = create_pirate_improvement() + + # Initialize NANDA with pirate logic + nanda = NANDA(pirate_logic) + + # Start the server + print("Starting Pirate Agent with LangChain...") + print("All messages will be transformed to pirate English!") + + domain = os.getenv("DOMAIN_NAME", "localhost") + + if domain != "localhost": + # Production with SSL + nanda.start_server_api(os.getenv("ANTHROPIC_API_KEY"), domain) + else: + # Development server + nanda.start_server() + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/nanda_agent/examples/requirements.txt b/nanda_agent/examples/requirements.txt new file mode 100644 index 0000000..d9c0d4f --- /dev/null +++ b/nanda_agent/examples/requirements.txt @@ -0,0 +1,13 @@ +# Core dependencies for langchain_pirate.py +langchain-core +langchain-anthropic +python-dotenv + +# CrewAI dependencies for crewai_sarcastic.py +crewai +crewai-tools + + +# NANDA Agent (from GitHub) +# Install with: pip install git+https://github.com/projnanda/nanda-agent.git@custom-improvement-package +nanda-agent diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..5f80dac --- /dev/null +++ b/setup.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python3 +""" +Setup script for NANDA Agent Framework +""" + +from setuptools import setup, find_packages +import os + +# Read the requirements +def read_requirements(filename): + """Read requirements from file""" + requirements = [] + if os.path.exists(filename): + with open(filename, 'r') as f: + requirements = [line.strip() for line in f if line.strip() and not line.startswith('#')] + return requirements + +# Read the README for long description +def read_readme(): + """Read README file for long description""" + if os.path.exists('README.md'): + with open('README.md', 'r', encoding='utf-8') as f: + return f.read() + return "NANDA Agent Framework - Customizable AI Agent Communication System" + +setup( + name="nanda-agent", + version="1.0.4.1", + description="Customizable AI Agent Communication Framework with pluggable message improvement logic", + long_description=read_readme(), + long_description_content_type="text/markdown", + author="NANDA Team", + author_email="support@nanda.ai", + url="https://github.com/aidecentralized/nanda-agent-sdk.git", + packages=find_packages(), + python_requires=">=3.8", + install_requires=[ + "flask", + "anthropic", + "requests", + "python-a2a==0.5.6", + "mcp", + "python-dotenv", + "flask-cors", + "pymongo" + ], + extras_require={ + "langchain": ["langchain-core", "langchain-anthropic"], + "crewai": ["crewai", "langchain-anthropic"], + "all": ["langchain-core", "langchain-anthropic", "crewai"] + }, + entry_points={ + "console_scripts": [ + "nanda-agent=nanda_agent.cli:main" + ] + }, + classifiers=[ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "Topic :: Software Development :: Libraries :: Python Modules", + "Topic :: Scientific/Engineering :: Artificial Intelligence", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + ], + keywords="nanda ai agent framework", + include_package_data=True, + zip_safe=False, +) \ No newline at end of file