From ddf3600c39bee11e17d60cbab6586f6c357ec73e Mon Sep 17 00:00:00 2001 From: jinubabu Date: Wed, 9 Jul 2025 13:20:27 +0530 Subject: [PATCH 01/19] Add pip-installable NANDA agent package MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Create nanda_agent package with pluggable improvement logic - Add core components: agent_bridge.py, nanda.py, run_ui_agent_https.py - Setup pip installation with setup.py and MANIFEST.in - Clean CLI interface for user guidance - Ready for user workspace integration šŸ¤– Generated with Claude Code Co-Authored-By: Claude --- .gitignore | 1 + MANIFEST.in | 8 + README.md | 371 +++++++++++++----- agents/start_running_agents.sh | 139 ------- nanda_agent/__init__.py | 30 ++ nanda_agent/cli.py | 26 ++ nanda_agent/core/__init__.py | 24 ++ .../core/__pycache__/__init__.cpython-311.pyc | Bin 0 -> 693 bytes .../__pycache__/agent_bridge.cpython-311.pyc | Bin 0 -> 42424 bytes .../__pycache__/mcp_utils.cpython-311.pyc | Bin 0 -> 7353 bytes .../core/__pycache__/nanda.cpython-311.pyc | Bin 0 -> 14008 bytes .../run_ui_agent_https.cpython-311.pyc | Bin 0 -> 20130 bytes {agents => nanda_agent/core}/agent_bridge.py | 224 ++++++++--- {agents => nanda_agent/core}/mcp_utils.py | 0 nanda_agent/core/nanda.py | 259 ++++++++++++ {agents => nanda_agent/core}/registry_url.txt | 0 .../core}/run_ui_agent_https.py | 4 +- setup.py | 64 +++ 18 files changed, 851 insertions(+), 299 deletions(-) create mode 100644 MANIFEST.in delete mode 100644 agents/start_running_agents.sh create mode 100644 nanda_agent/__init__.py create mode 100644 nanda_agent/cli.py create mode 100644 nanda_agent/core/__init__.py create mode 100644 nanda_agent/core/__pycache__/__init__.cpython-311.pyc create mode 100644 nanda_agent/core/__pycache__/agent_bridge.cpython-311.pyc create mode 100644 nanda_agent/core/__pycache__/mcp_utils.cpython-311.pyc create mode 100644 nanda_agent/core/__pycache__/nanda.cpython-311.pyc create mode 100644 nanda_agent/core/__pycache__/run_ui_agent_https.cpython-311.pyc rename {agents => nanda_agent/core}/agent_bridge.py (79%) rename {agents => nanda_agent/core}/mcp_utils.py (100%) create mode 100644 nanda_agent/core/nanda.py rename {agents => nanda_agent/core}/registry_url.txt (100%) rename {agents => nanda_agent/core}/run_ui_agent_https.py (99%) create mode 100644 setup.py diff --git a/.gitignore b/.gitignore index 68a37c1..83a560b 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ jinoos/ agents/__pycache__/ conversation_logs/ agents/test.py +nanda_agent/__pycache__ \ 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..9731c41 100644 --- a/README.md +++ b/README.md @@ -1,156 +1,309 @@ -# 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 AI Agent Communication Framework with pluggable message improvement logic, built on top of the python_a2a communication framework. -## 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 +``` + +### With LangChain Support + +```bash +pip install nanda-agent[langchain] +``` + +### With CrewAI Support + +```bash +pip install nanda-agent[crewai] +``` -The system requires a configuration file at `/etc/internet_of_agents.env` with the following environment variables: +### With All Dependencies ```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 +pip install nanda-agent[all] ``` -## Agent Configuration +## Quick Start -The agent system can be configured through the following parameters in `start_running_agents.sh`: +### 1. Set Your API Key ```bash -# Starting port numbers for bridge and API -START_BRIDGE_PORT=6000 -START_API_PORT=6001 +export ANTHROPIC_API_KEY="your-api-key-here" ``` -### 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 +### 2. Run a Simple Example + +```bash +# Simple pirate agent (no extra dependencies) +nanda-pirate + +# LangChain-powered pirate agent +nanda-pirate-langchain + +# CrewAI-powered sarcastic agent +nanda-sarcastic +``` + +### 3. Create Your Own Agent + +```python +from nanda_agent import NANDA + +def my_improvement_logic(message_text: str) -> str: + """Custom logic to improve messages""" + return f"✨ {message_text.upper()} ✨" + +# Create and start your agent +nanda = NANDA(my_improvement_logic) +nanda.start_server() +``` ## Usage -1. Set up the environment file: - ```bash - sudo nano /etc/internet_of_agents.env - # Add the required environment variables - ``` +### Creating a Custom Agent + +```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", "localhost") + + if domain != "localhost": + # Production API server + nanda.start_server_api(anthropic_key, domain) + else: + # Development server + nanda.start_server() + +if __name__ == "__main__": + main() +``` -2. Ensure SSL certificates are in place: - - Certificate: `/etc/letsencrypt/live/${DOMAIN_NAME}/fullchain.pem` - - Private key: `/etc/letsencrypt/live/${DOMAIN_NAME}/privkey.pem` +### 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()) +nanda.start_server() +``` -3. Make the script executable: - ```bash - chmod +x start_running_agents.sh - ``` +### 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()) +nanda.start_server() +``` + +## Configuration -4. Run the script: - ```bash - ./start_running_agents.sh - ``` +### Environment Variables -## Monitoring and Management +- `ANTHROPIC_API_KEY`: Your Anthropic API key (required) +- `DOMAIN_NAME`: Domain name for SSL certificates (default: localhost) +- `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: -### Check Running Agents ```bash -ps aux | grep run_ui_agent_https +export ANTHROPIC_API_KEY="your-api-key" +export DOMAIN_NAME="your-domain.com" +nanda-pirate +``` + +The framework will automatically: +- Generate SSL certificates using Let's Encrypt +- Set up proper agent registration +- Configure production-ready logging + +## API Endpoints + +When running with `start_server_api()`, the following endpoints are available: + +- `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 + +## Agent Communication + +Agents can communicate with each other using the `@agent_id` syntax: + +``` +@agent123 Hello there! ``` -### Stop All Agents +The message will be improved using your custom logic before being sent. + +## Command Line Tools + ```bash -for pid in logs/*.pid; do kill $(cat $pid); done +# 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 ``` -### 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` +## Architecture -## Network Configuration +The NANDA framework consists of: -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 +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 -Each agent is configured with: -- Public URL: `http://{SERVER_IP}:{BRIDGE_PORT}` -- API URL: `https://{DOMAIN_NAME}:{API_PORT}` +## Development -## Registry +### Running from Source -Agents are registered with a central registry specified by the REGISTRY_URL environment variable. -The registry URL should be a valid HTTPS endpoint. +```bash +git clone https://github.com/nanda-ai/nanda-agent.git +cd nanda-agent +pip install -e . +``` -## Security +### Creating Custom Agents -- 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 +1. Create your improvement function +2. Initialize NANDA with your function +3. Start the server +4. Your agent is ready to communicate! -## Troubleshooting +## Examples -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 +The framework includes several example agents: -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 +- **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 0000000000000000000000000000000000000000..a822d786bbe28c672b7f177472b5294d9385c5b9 GIT binary patch literal 693 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

V+N_{{D?cnY1C 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..2676e6bf2d70c97f0003a49d8810809b7d355b89 GIT binary patch literal 14008 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..f71f0827d299122812491f49295896847219dfb6 GIT binary patch literal 20130 zcmbV!Yit`wnqW8CB%2SBl&JT^YCSBAmMF=REz9yNk}O-YE!mNrWTKs{K&x9eZHm-% zQ-1VO8O^|pQ3eJ^2^?6-+K3i6$!Rwpj4``=m>+Yu6VD|#!2P&xqj4q(=wRSrU~|6| zBmsi_y6>yzqno1gxK^>cx*lIub#;C3(tj!`aZzy9+hSM$_YsQvKlqTn?3u)uy;h34 zPw^B_N2wY8PS4Qf*)n4xck7Im+?g4M+-);9a<|Xe$=xyIAa{0#g}Wu{TyV{}7Th!L z1<#CUp=73H!8_xn5wa)y>#vh zp^C5OYpz&!z^L5;BeW}wx}u|~-vOa(R|pM!b&+}O*#V(@R|t(o$H47?(6cLqrlJs< zcR)D6x4@e83Qc_Lb@I+d4}1r`=5@s{XqxJCi#Vj|$Mv_VSXjL=vMnp!gDGd83h-Fnf7veqInGu`9B@ zJG>a_-k)~F#mJRNOiI)HgO;>Y6r}i)I44Nya%pLCF)k*g**Q_*0ZJqsmF9AL1?Gld zJN~{Lh6peOousZoO22r^F-Y)MQyxR|Y^FRr<|V0VD(ILLOCkMj-OwBiKnmgH#CS9` z6phAjhAxnC%4KIpFLiPkFHA#x;o_z73zO5|))#FsAtbKGc}XtMyrj^j+lxXchjkYd zyR;b-!n`0#@V*>Zb4eCn3RxxzXU~0mx%(9fDs^9r#Fj3HFE4eUiq9=Az#Edf^+n>Q z1@T4%=FmMkG zAWE~Dafpb`8sR4$fM|t!-hX6eOk>KGie8oJOC3I!dFoS{gQ>$4#wT}R<-(WHh?>jc z3V6t^-d%XOPbCa1%hP6zM^gzyOR>iIv|%X0KJ=?_S0o(S5{kOUWYfH*upizE-vJ}n zjzooUj9Xgd7R5Mz3xdSOW1Mg+l8~!O8GOQ~;;ybP5l2Gy+ai47iM02!7~!u7vl$R+ zXF?DcBC&8nNL!`bQkp?<@Xgz$$d%aALfSSjh8F}0p>Uj73%9gm4#%;yDDH)){qU1c zK(s=A%~Cd(;%Zfyy&AJuVfJo0Dz+)w*0RM`K4ZD3ET^)~8r!U}%`X_oow3!Ie{t^4 zxfSv!A~BkSU+zLmOZL$^tO2YErNsJWi>WVL(o(Fi*D0P^w!CSHvHJZceUs8* zn_Ams79OC0vkVmNU% zZHHYXCEzK(D8$kXjyr9GmlIhO_rMyZouju9-$vrGpfyd;r>)qW79{*&x$HjkUl9_s z#ty_jC_V^3sSWl7*>G$*?y&kTU!B6%Z8HvA@0Ppd?&05@`1J{;wo`S7GqL`(}$;+NG6tZBy2gy2nka%BuVG@65k__1@L{v3Fu>Wt&#nwr1V(L)~MKC5RxX z{^OeexZ*#as;b>~Ks(YnAANzazQQPJAL zDtka<4=6hVfc$3L3T@z?!9G7#nKv}%4TX6lj%cy@F9JX6OIpTrj z6FW2Pay)TaFCIdA%~LA>L|%T911qM1yGU!`5+VU z_rT(l&CV2Vq=W&rT+6k>&B|LJT9Q)}$4N_s)0$t3MsHt|Yfpy(q3~QHo~xtx4Tz^E ziUU>JYUQY9JD`&PX zK3fIZS;sd96z_2ek8Ab2%AU~J6AF7`D^Pduc*<9)c$)N(+SB%UMk##>LMl+b?O@7X zUr-Qzh0!Xz?V>#8n!9nM@2R^@akp)GN|mzX>jMz!LG_%_JSP;-iBzC!?cfV6KCyZ0 z(K6g1sGgIW=cM8}39#1=eY52Td}gr$HpoU~xS`D^#T$a4vRxY6rLbKoU-{}%_K%HpPNrMg2CUzZJFx+!rzVm&1nl8SlckZlR z_A?NAWc*=wSb64=%}m&rElG=EFWWPF`9#w4gBXt-)_|GhF5Y3@^UGG#PR(G3q!mWA z11BYc9)cSHlAbc)2A|w^J|4f0Y%TD< zWIykd2!jxbnXyAD*2{3fq#h^xIH;s=5AilI!!p|)g-T0{-KYPN76;%>iJ;)2#D0im z&uQcrvUtUbcm+6_K~6*pB3I^F3M0@-xk|?Y2-n=XWsox>0r4n+Nn3%Yi^s9g!x$NM zz4#JlkX??$DZ14?`sgzoeH5zw6n+vYh6*>j*H%t;&e4r_#d}l_Dtk<0k16ai+~dc{ z9^bEc_HVEdLBQQ@v*PZC2#DglMo({>vz@jlID3y@c{0qS3LD(Oo!KQDc-|+P}u>E9Z=YTlrONF$nJlF z1oTL6vB~Py9}g94wwloYvUs38j|cjP>!~M>0LJyM5xezCTg3=%{R?e_w28Lk4kCEq z50JPI48KvtxIu{%#^)r3>!e>iolkEVID%|Bi19lG$9y65lyA>l^64KYLZWDx5fA%@ zc=&ob5Y|u=g7hlP%@OqoY#1OvO9F7jA~!uf%ISI)_*~OAQjoR@s1cb2X0$|aGE4HM zv~_VQ0fM1`BB3)=1ccR*>BS@$mZNl@DVR;fbr9kt%ry=_=@p1TLU;JzolqQ25VqKg zwTQ-Y8&w(`e8z^JvZ2i@D%-EI{R-P(q*P_QHMU#HhHV=K!la{#$d0m`sBSKOf9;;kG9a&fe5kv2ao4YE^UFU=t zzZggP+7iD`q-R3l2{{j1MC_8J{}H4zOcOcUqeGY_5JY@EH{d5>8SwUY$`e@U|CLn& zMyvh5v_7VpvH*$9^e)6^5&By0OXcYdtvQ`wpnm4s zz;-*X0xlOgS{`T_Z{zI`m}NRiZx|rt8=G65qTcZR6a?&L3-Yg^^um`zuA~K(UU&Vd z_5xa)$;I0>Bf@%L50h?|*1rb|xzFc}XW<`lze`f-C9TaOdmf zQ;GcGsRVu%`*}A8P~;F^I9Ma0dmW;=>+PKG8bRXqKE$XnVfpGEt6YPt z43bG_779gyWHP?&HqELwfz&p0B@5QNn9O4C_q$pb8_}Ev_?o(9_nU6O(By95YU<^| zY1q7+30DLT zGkyI%JwZmq?@+A7s0t#vvOrBd5xx}~x*~`ZPzZDZ^)7PlL_{JQnk;c4Y!wk{K3@Y& zlJ?ETV<1;aq$iNP#PitN2^w6TW)PG}ROqBcE=BzcC}E=Uxp4FAU8f{zYj|-n-LMe8 zF3e^$O0#jI$B{r&lQCP^A_-wZ605Oq!sQY4e+t34$okHr1wllmW7>l_JWDi1v^$C@ zU#FQ@rbnklL9`t(mzxm9In2T-l2<_HMz}~I(g`u>(Wi}Fr>)n8+iBa4aC8Y| zOnggePmVaF#7W)_u9L`Pbyr3)jG8FBzG^xTisK>TW+o!UOT3N^G5QbilX?K5>-s3) zoj2EOo;rXoH*W`rwaO?8f^+B&zLwt#D!f@syT?WO9P zKM1`adawIIH)w&J`$_cH+0~J?id1WGbxiZruTQH)D1Gj$TJ>!+UAFEmU*+B8Dg(SD zRv)L=Zf`P5RVRe4K<)Y&W$%zuH>?Imw7|%proiO66=2T5HVT?h0MR;OARXO;(TBYKM z9#&mjrM2sWo1#+MtCsd^rM;_mkc!J|p9R{U20$PVY*lbt#on#zmd$#lx?ipC*Q)zd zTz9Ib3En}8_X`T5uQ2+;>Oj^TqE+^5Kjri)-n}47d-p<6**1-BQ`oi_?y|K3Ezq*@ zs_O34+@0GL3sZj)s7=+kYW1C3{ee_Vht_g*tG*Sa^?EO;|LUO+rrd`jyAK{qd|!L0 z^19+L=$jY13=Yd+vFVX6gMT=^S^5vJ{`S=ZNgUQ--SJ^y5ahkGl3?^%#|!)3<{z1?7W2#KPOrhfcTNSf%v)Py7RDl zTsRudsnh^3>o`XO89kFaEsj8!vW={@E~w97hCw3-(sYJo;`ac^Uw{geL8q>JLy22N zysJyx1Pv2}OzI)n{C$S^uEYdZc_XhidTl} zpzJQxwgH+O_~7{a$KN~o;G`Pp&;lKbr-N)srD@`iSO2sCHw;yis_%m4yP&-My7sc5 z_ymO&bZMK$O`eT@{ALF2JK4BzbpH#U<+}0;e zjUaaag`y$-7rPDOZ_CM^7IoU3rGut@jo%@z$CvLze7{gIfK|{hVx!Ah{<67v-)YeU zQH3flUV}JOVEqHd+-`N59d$vR*#)j`M_tBf{Vl*yHskIyU>O?-6Lw<7vl2fYlbv$e zp0pQNK$xNLGD4>rt1R0S$bDok-eoQYJZLO+7q={$>UfG@S=VU%Ko_lw@ZNY8wY%AfkaKm^I=5oc%sYCoLdmSwYkcJg_BMGwlHX zmva0o%#FE=+j5QAmapI|3o$Gac5>nu!Pv(@z%r5a)eE-KgX5os|RzRg7vx}*UNC|1s>HVJ$0L?Hky zB@2r{6Od09#BQA?fB}U(&c?tuL2ht`LLqKaxS7*^W$+2lL4j;P4RFC&%k^y_3f>byA~M`{7cn!@H^G{NGL$G| zl*EW|+{iCaiqjCM!7QZ9Byx)`d}N$5P>-A~%FYRB8xlW4;DT-JpyCJJUE zEr^{6o#B@j7IoEAp47R-pJCPfz;a{KX&{a>FDIY@Uqm&TE(xG`k`V%O)X|q2XmZas;U>SJ-xv-LZaM z@peE^*-nk^RM<|EogcxrovQ9y^?*9iTejA@VO2|7wUX9t%Hb^Aq_;f&yW^?4=G30{ zRO9|sOK+;7ZQE_H0-hfu59KfYqQqWO_7w#Y zT}Jt<*4d5yN>xbpb!mj<&noc?1&G5tl=nZL+W;dU;Wmch1N-Hv6Q>#KQ>uQn-TG<0 z2h;7$Xs6@TPAjInX-xM7ApIHRI^(i_<~?x6X8r6W4e9@Gvq7BE8H;4Io0yKAFma0r z6HZ_vKt~3hX0N>dv^Y6UQf722`HbW~Qc!AU;qw|%6FDkqV}=iIMLAXo^rd;iRL}~D zX|8OiWr7eEe|C<9up9wVAg8qhW@;zlHNfMoCX!N6cLzyfG9(3b!9-9XcLPbWO^VAf z7`bXP&gnQdiw$+(9ur0kLQN4hiDCyvIc7i2MB`yzM~QY(Scn7SC-`s=R?G1JzkoD2 zgPQQTj`NW9%pp5)_d&Wa8%O6v@mEm9z+0F8DUrTSy?gUFZ~pqt z^{DFW(0m=R^_(53lIVT==)I$gXU~gyNCLzKMYwR{-obEe8l>M z>KZ<1ebPl^`k)PBS?AX?luqi_4N0c0gX$uDX4hvh?z#ADil?BXiX71R$t$2{8c@|>g)y{+3 z|AFI?x4;bt3?E<^^2rC#v<4ptF$57?Th7jw{ca^R%e^w5m34_Slzhc<#RxIg5i4`n zp+n%szk!!%dc6!}AhrK;7{0uB>2nlaVA*+#$(svMC^Fh7QSdR^7?;6M2|Nr8m;)eZ zF7s=l!HQXY$;|bnVW@}8zvD93TL!z4aS`)4FIk}f2a(72(E2KMpZ=j^`28@<$-ZL% zkYOGJP2K%@&^{@iff5ltW-iHg)RO|oXK}f}c0UezY361)k^n{c!eZjKu1r7`LfVGh zm6W!jFRTde>~39YF^d555Oqcim8?0hofTcHz=GqEAOgaXoAj?F%Rot8$B0&3I?H8GOtV$lvN5hzcCEToe$e7+{?=7nKDBLkIcuMLN`G_q*Js~4fA{?A zdEghE8F=la_V_ZC2{S8^m(+xS87{os2Y4?5Nysmg(ei3jrLKDDt|YwT4j2QtAw zppXED)`Iq+hbSrg&ADHndu!tE#OlO$1$3!fCQDJZ-c8KOA$?*p8;ghL!eHf0TViq9 zbzKk^L*Xds;&mH_hxo2%z1BTO6GqS>myBS`tk=6-`jZgaE25#{tk1sqHZF$?n#F`U zG;|agIKcG4E&y+z06%x4lZS=HO_XNB;uT3>QCw)9V&G>ZB1HqG&~Gq}n~7}GjBn;k z`ttb-7EOE?>Q&-8{T>qE5Y13&^*zrsKljwnZPcm$kme7qjHgh!O50wd*PXv>0A(D#+tkkJ>sHENy~Wn8kFAfrclN7ph&-p^Fu}}a-qO$;!(0-%zRZ2cYG%t1NzTn4xWBgz;O_zP*F^v+pP;d zDgnb##W4r(NElS4KoXpfTpz(_l((pUM_ae4EfA3((bmiR9{CI3_G@g z>n^2@q?FmElq0<(vzxihZ)KNOP1uTW-leof?-6Kj>qE;fb@mbnZN~S5s-|UIVsCaN zAc3HPnOsR5csuUf@devfQQONVvhdur*hPJT}< z9o9?VQ){SjZalS&<};KH$+bp{EIjA@YsCKvPh6&u68~!Yikq_e8_n62@ot1;?pK`p zk#M*y+yXdV#y*TAowtB;+qmYyr{`w{D>!1T4f}bAmu~Eisr@N)&#;?}&_~VCcOGyu z)jnO&cL5a#*vO)1u4_45%%3S7L#)>XcdW47R64$+^au8mIGY^{MQya@)AXdsV=Dl22*F^3te3UynKB&40{W+{0phvX9iA z)$77BTb!Dd;KBWh+`$Fxb5P||a-Zw?kG^#0c{dl4I2dLOMkcoyms)qR-{B1=&SHmE zneQZ{8o{kWJo4qM>FafX;5}>97vuVC)W}ibjF=G#ElUbV%@J7E1`=W zC%5Ok!nhEeWG?2rr)vP@P@$U&G$`ipK;F6}_Z72%Lz5}pHpCSe(?~11HPeV#sX|=# z2&P#{Dz~~Wf>WBrjU>cq#|Z8Z_{!lN1^8X$P7rs=4f+Aef~_NwBb=xT>Ez@?qf=8C zrUto@f~_SP4n@Fm329N7gPjMy9C!#)lFN+sF$=)C&cV*p)1RZ>DF++-7s$rrxywrl z<7U&rk-lKuHzK^i%fXAFc!6`JnGCS>;l3v>lVrPdAl>W7&t*qX7x_akLi9DnLs()W z%_gqmxlF*FZomM!|1YplKl&U7FSzOPGvkw&$T8=%10F7oPEEk+GV#h{ScHF1;2p|z zaS!)YMb}T)Kr4B3w z94MlMk#-2N8xaxI8>q4-{w<)_06#=NB1%NNtvd?3q@~N5#ZB8T;_*`vnHRwGpaX$3 zs8P_qfUh8+JO*Ew7)NOPF+xc~L>5Lw?nS%;QQ8WdHpr&!WE!G)4-5W;5e`s9y?5FH zC~6i4k!CSLEQWu8d#Ao6&Xmj70c(?6d=))?N z32IDGVS-zXi{y2ExKCyFYs`Ly+5e)X0xVz3fmfB1*VK~Nw363WPNx{xGp6b(Q?+j2 zEPs4fVF1KQjX9|>C-X(@htlIy%E`;5bWUUD6lN}8>iJ#Y9~or?GNCQ6F}%X?hSJRw zq+n2E(EC1L;8jACq~wCeTu_(``4YFX{|qUCe?%~*Fk|@=R@pa7N=|FcX@xP2-l4P| zBPGW*=D5Nf&$n@GdX%GKQg&HmU=Ej`yGx(B>z}&oRremvy=UbVsQ3KkcWl^n*cV&S}g!g*it`E@H_=J*dpf8uPNkyi7`luw+ONDl@Dx!wNG@ zN)D(@ug3H$OfM`@plan@%Hh6q>kGtTOYM6d-~SgYvp7lL`qh@Vt3GhcxQnJoxB{QrQnd^&i&!hZX-}QV`lW_*;Mxf}r}l zG=G=k@7nSQ?y>8oZ@cfg*WiyFG96bvXEo1R#dB6alA1e*y5;fR9b0?(t#fzJt)9cr zg8kK*U!Ga3*{FVe>d)6#&Zx|k#!M;MK$N3tc6JuA*X(T2GC3LSBPdz?1W=jy7mSFf zA;F#hjF0~rBKXf5guVdFLmV~WKWX4Uyb)2ssk?6w6D4XViNYEDU$C6~dlM0nClC+9 zpv{6`JaHD+-NW730F7+Al>v2IBC00EPa#UPV1E!0)r%kFqZ_<4;<5S2l_gOibU-IB zgbe8vXqNcU%+Bigz9c*k=a|8jo%r^X|E@x;AcO|$PL)I0*C?3K2ad2mvov+~5_eQl&aHszaeV2Vwk-?|mIsKwuu_&l zF2_%m23AV88J6~giNF+b5VOK!plW%4Bdm^Gm#g3$A4Ax%bL~W z8;3Ubt<ItppR}IM(BMS-M4Moi*E@ctPV5ENRnm0~=r@>6S#Vy#Ir X?8>QMoW65 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..bb712c1 --- /dev/null +++ b/nanda_agent/core/nanda.py @@ -0,0 +1,259 @@ +#!/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 = get_agent_id() + + 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") + + PORT = int(os.getenv("PORT", "6000")) + TERMINAL_PORT = int(os.getenv("TERMINAL_PORT", "6010")) + LOG_DIR = os.getenv("LOG_DIR", "conversation_logs") + + 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 daemon thread + bridge_thread = threading.Thread(target=start_bridge_server, daemon=True) + 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 based on domain if not provided + if not cert or not key: + cert = f"/etc/letsencrypt/live/{domain}/fullchain.pem" + key = f"/etc/letsencrypt/live/{domain}/privkey.pem" + print(f"šŸ”’ Using default Let's Encrypt certificates for domain: {domain}") + + 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) + + try: + # Start the Flask API server (same as run_ui_agent_https) + run_ui_agent_https.app.run( + host='0.0.0.0', + port=api_port, + threaded=True, + ssl_context=ssl_context + ) + except KeyboardInterrupt: + print("\nšŸ›‘ Server stopped by user") + cleanup() + except Exception as e: + print(f"āŒ Error starting server: {e}") + 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/setup.py b/setup.py new file mode 100644 index 0000000..4901cad --- /dev/null +++ b/setup.py @@ -0,0 +1,64 @@ +#!/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.0", + 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/nanda-ai/nanda-agent", + packages=find_packages(), + python_requires=">=3.8", + install_requires=read_requirements("requirements.txt"), + 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="ai agent framework langchain crewai anthropic claude", + include_package_data=True, + zip_safe=False, +) \ No newline at end of file From c487d9d76991560ca6061bbc812339797d5253bf Mon Sep 17 00:00:00 2001 From: jinubabu Date: Wed, 9 Jul 2025 16:47:59 +0530 Subject: [PATCH 02/19] deafult bridge changes --- nanda_agent/core/agent_bridge.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nanda_agent/core/agent_bridge.py b/nanda_agent/core/agent_bridge.py index c25ddde..540ec0d 100644 --- a/nanda_agent/core/agent_bridge.py +++ b/nanda_agent/core/agent_bridge.py @@ -93,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 From 6daca0977ae8a64c2259aeef10bca7df11a4f815 Mon Sep 17 00:00:00 2001 From: jinubabu Date: Wed, 9 Jul 2025 17:55:32 +0530 Subject: [PATCH 03/19] param logic in agent bridge --- nanda_agent/core/nanda.py | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/nanda_agent/core/nanda.py b/nanda_agent/core/nanda.py index bb712c1..d2cac24 100644 --- a/nanda_agent/core/nanda.py +++ b/nanda_agent/core/nanda.py @@ -66,21 +66,37 @@ def start_server(self): # Register with the registry if PUBLIC_URL is set public_url = os.getenv("PUBLIC_URL") api_url = os.getenv("API_URL") - agent_id = get_agent_id() + 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") - PORT = int(os.getenv("PORT", "6000")) - TERMINAL_PORT = int(os.getenv("TERMINAL_PORT", "6010")) - LOG_DIR = os.getenv("LOG_DIR", "conversation_logs") - - print(f"\nšŸš€ Starting Agent {agent_id} bridge on port {PORT}") + 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)}") From 2eb4967efd482958d48543912b4afd6227cd5886 Mon Sep 17 00:00:00 2001 From: jinubabu Date: Wed, 9 Jul 2025 19:50:39 +0530 Subject: [PATCH 04/19] running in background threads --- nanda_agent/core/nanda.py | 37 +++++++++++++++++++++++++++++-------- 1 file changed, 29 insertions(+), 8 deletions(-) diff --git a/nanda_agent/core/nanda.py b/nanda_agent/core/nanda.py index d2cac24..28109e9 100644 --- a/nanda_agent/core/nanda.py +++ b/nanda_agent/core/nanda.py @@ -259,17 +259,38 @@ def start_bridge_server(): 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 daemon thread + flask_thread = threading.Thread(target=start_flask_server, daemon=True) + 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}") + try: - # Start the Flask API server (same as run_ui_agent_https) - run_ui_agent_https.app.run( - host='0.0.0.0', - port=api_port, - threaded=True, - ssl_context=ssl_context - ) + # Keep the main thread alive + while True: + time.sleep(1) except KeyboardInterrupt: print("\nšŸ›‘ Server stopped by user") cleanup() except Exception as e: - print(f"āŒ Error starting server: {e}") + print(f"āŒ Error: {e}") cleanup() \ No newline at end of file From 298d8583bdfce1edc75443adf5a65f4b5fd2abc5 Mon Sep 17 00:00:00 2001 From: jinubabu Date: Wed, 9 Jul 2025 20:35:06 +0530 Subject: [PATCH 05/19] return the main thread after invoking 2 threads --- nanda_agent/core/nanda.py | 23 +++++++++-------------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/nanda_agent/core/nanda.py b/nanda_agent/core/nanda.py index 28109e9..a1a39e7 100644 --- a/nanda_agent/core/nanda.py +++ b/nanda_agent/core/nanda.py @@ -217,8 +217,8 @@ def start_bridge_server(): print(f"šŸš€ Starting agent bridge for {agent_id} on port {port}...") self.start_server() - # Start the bridge server in a daemon thread - bridge_thread = threading.Thread(target=start_bridge_server, daemon=True) + # 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 @@ -273,8 +273,8 @@ def start_flask_server(): except Exception as e: print(f"āŒ Error starting Flask server: {e}") - # Start the Flask server in a daemon thread - flask_thread = threading.Thread(target=start_flask_server, daemon=True) + # 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 @@ -284,13 +284,8 @@ def start_flask_server(): print(f"šŸ”§ Agent Bridge: http://localhost:{port}") print(f"šŸ”§ Flask API: {'https' if ssl else 'http'}://localhost:{api_port}") - try: - # Keep the main thread alive - while True: - time.sleep(1) - except KeyboardInterrupt: - print("\nšŸ›‘ Server stopped by user") - cleanup() - except Exception as e: - print(f"āŒ Error: {e}") - cleanup() \ No newline at end of file + print("šŸš€ Both servers started successfully!") + print("šŸ“ Servers are running in background threads") + print("šŸ”„ start_server_api() method returning to caller") + + # Method returns immediately - servers continue running in background threads \ No newline at end of file From 8fcc5b6bfa74befc54b672a74a42bb07c70e1ee2 Mon Sep 17 00:00:00 2001 From: jinubabu Date: Wed, 9 Jul 2025 20:43:02 +0530 Subject: [PATCH 06/19] exit the main thread --- nanda_agent/core/nanda.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/nanda_agent/core/nanda.py b/nanda_agent/core/nanda.py index a1a39e7..9e60933 100644 --- a/nanda_agent/core/nanda.py +++ b/nanda_agent/core/nanda.py @@ -286,6 +286,8 @@ def start_flask_server(): print("šŸš€ Both servers started successfully!") print("šŸ“ Servers are running in background threads") - print("šŸ”„ start_server_api() method returning to caller") + print("šŸ”„ Main process exiting - servers will continue running") - # Method returns immediately - servers continue running in background threads \ No newline at end of file + # Force exit the main process while leaving threads running + import os + os._exit(0) \ No newline at end of file From b8f66dfed1a20092e6485edeef279f8c3c181d22 Mon Sep 17 00:00:00 2001 From: jinubabu Date: Wed, 9 Jul 2025 20:51:01 +0530 Subject: [PATCH 07/19] fix import os issue --- nanda_agent/core/nanda.py | 1 - 1 file changed, 1 deletion(-) diff --git a/nanda_agent/core/nanda.py b/nanda_agent/core/nanda.py index 9e60933..8b74813 100644 --- a/nanda_agent/core/nanda.py +++ b/nanda_agent/core/nanda.py @@ -289,5 +289,4 @@ def start_flask_server(): print("šŸ”„ Main process exiting - servers will continue running") # Force exit the main process while leaving threads running - import os os._exit(0) \ No newline at end of file From cee26f02b589f7debcf9611c280f261a1ca806d0 Mon Sep 17 00:00:00 2001 From: jinubabu Date: Wed, 9 Jul 2025 22:12:18 +0530 Subject: [PATCH 08/19] end the module after threads are created --- nanda_agent/core/nanda.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nanda_agent/core/nanda.py b/nanda_agent/core/nanda.py index 8b74813..6794bd5 100644 --- a/nanda_agent/core/nanda.py +++ b/nanda_agent/core/nanda.py @@ -289,4 +289,4 @@ def start_flask_server(): print("šŸ”„ Main process exiting - servers will continue running") # Force exit the main process while leaving threads running - os._exit(0) \ No newline at end of file + return \ No newline at end of file From 94962eb9a01ec7e5c6bff652a0afcee29503f078 Mon Sep 17 00:00:00 2001 From: jinubabu Date: Wed, 9 Jul 2025 22:28:22 +0530 Subject: [PATCH 09/19] run the main process entirely --- nanda_agent/core/nanda.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/nanda_agent/core/nanda.py b/nanda_agent/core/nanda.py index 6794bd5..f60c08f 100644 --- a/nanda_agent/core/nanda.py +++ b/nanda_agent/core/nanda.py @@ -286,7 +286,12 @@ def start_flask_server(): print("šŸš€ Both servers started successfully!") print("šŸ“ Servers are running in background threads") - print("šŸ”„ Main process exiting - servers will continue running") + print("šŸ’” To run in background, use: python3 script.py &") - # Force exit the main process while leaving threads running - return \ No newline at end of file + # 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 From 2c0472ff843735a581aa3d1cbaa6af26737fbf56 Mon Sep 17 00:00:00 2001 From: jinubabu Date: Wed, 9 Jul 2025 23:46:50 +0530 Subject: [PATCH 10/19] with examples --- README.md | 93 +++++++----------------- nanda_agent/examples/crewai_sarcastic.py | 88 ++++++++++++++++++++++ nanda_agent/examples/langchain_pirate.py | 74 +++++++++++++++++++ nanda_agent/examples/requirements.txt | 12 +++ 4 files changed, 201 insertions(+), 66 deletions(-) create mode 100644 nanda_agent/examples/crewai_sarcastic.py create mode 100644 nanda_agent/examples/langchain_pirate.py create mode 100644 nanda_agent/examples/requirements.txt diff --git a/README.md b/README.md index 9731c41..775bd0b 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # NANDA Agent Framework -A customizable AI Agent Communication Framework with pluggable message improvement logic, built on top of the python_a2a communication framework. +A customizable improvement logic for your agents, and easily get registered into NANDA registry ## Features @@ -19,62 +19,25 @@ A customizable AI Agent Communication Framework with pluggable message improveme pip install nanda-agent ``` -### With LangChain Support - -```bash -pip install nanda-agent[langchain] -``` - -### With CrewAI Support - -```bash -pip install nanda-agent[crewai] -``` - -### With All Dependencies - -```bash -pip install nanda-agent[all] -``` - ## Quick Start -### 1. Set Your API Key +### 1. Set Your API Key (For running your personal hosted agents, need API key and your own domain) ```bash -export ANTHROPIC_API_KEY="your-api-key-here" +export ANTHROPIC_API_KEY="your-api-key-here"\ +export DOMAIN_NAME="your-domain.com" ``` -### 2. Run a Simple Example - +### 2. Create Your Own Agent +#### 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: ```bash -# Simple pirate agent (no extra dependencies) -nanda-pirate - -# LangChain-powered pirate agent -nanda-pirate-langchain - -# CrewAI-powered sarcastic agent -nanda-sarcastic +nohup python3 langchain_pirate.py > out.log 2>&1 & ``` -### 3. Create Your Own Agent - -```python -from nanda_agent import NANDA - -def my_improvement_logic(message_text: str) -> str: - """Custom logic to improve messages""" - return f"✨ {message_text.upper()} ✨" - -# Create and start your agent -nanda = NANDA(my_improvement_logic) -nanda.start_server() -``` - -## Usage - -### Creating a Custom Agent ```python #!/usr/bin/env python3 @@ -107,14 +70,9 @@ def main(): # Start the server anthropic_key = os.getenv("ANTHROPIC_API_KEY") - domain = os.getenv("DOMAIN_NAME", "localhost") + domain = os.getenv("DOMAIN_NAME") - if domain != "localhost": - # Production API server - nanda.start_server_api(anthropic_key, domain) - else: - # Development server - nanda.start_server() + nanda.start_server_api(anthropic_key, domain) if __name__ == "__main__": main() @@ -148,7 +106,11 @@ def create_langchain_improvement(): # Use it nanda = NANDA(create_langchain_improvement()) -nanda.start_server() +# 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 @@ -186,15 +148,22 @@ def create_crewai_improvement(): # Use it nanda = NANDA(create_crewai_improvement()) -nanda.start_server() +# 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 (default: localhost) +- `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) @@ -261,14 +230,6 @@ The NANDA framework consists of: ## Development -### Running from Source - -```bash -git clone https://github.com/nanda-ai/nanda-agent.git -cd nanda-agent -pip install -e . -``` - ### Creating Custom Agents 1. Create your improvement function 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..8c7ed53 --- /dev/null +++ b/nanda_agent/examples/requirements.txt @@ -0,0 +1,12 @@ +# 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 + From 1a8352302cb0a4fd77ba738c3acdc24574098baf Mon Sep 17 00:00:00 2001 From: jinubabu Date: Wed, 9 Jul 2025 23:51:22 +0530 Subject: [PATCH 11/19] readme updated --- README.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 775bd0b..bebc0e3 100644 --- a/README.md +++ b/README.md @@ -29,12 +29,14 @@ export DOMAIN_NAME="your-domain.com" ``` ### 2. Create Your Own Agent -#### 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: + ```bash +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 & ``` From 5554c44f45709be8e3f9c7da3c653fa72281f5c4 Mon Sep 17 00:00:00 2001 From: jinubabu Date: Wed, 9 Jul 2025 23:53:22 +0530 Subject: [PATCH 12/19] example requirement updated with nanda-index --- nanda_agent/examples/requirements.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/nanda_agent/examples/requirements.txt b/nanda_agent/examples/requirements.txt index 8c7ed53..d9c0d4f 100644 --- a/nanda_agent/examples/requirements.txt +++ b/nanda_agent/examples/requirements.txt @@ -7,6 +7,7 @@ python-dotenv crewai crewai-tools + # NANDA Agent (from GitHub) # Install with: pip install git+https://github.com/projnanda/nanda-agent.git@custom-improvement-package - +nanda-agent From aabfbd5572493f1c105be307070e71304febca89 Mon Sep 17 00:00:00 2001 From: jinubabu Date: Thu, 10 Jul 2025 08:40:26 +0530 Subject: [PATCH 13/19] agent url provided --- .gitignore | 6 +++++- nanda_agent/core/nanda.py | 5 +++++ setup.py | 6 +++--- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index 83a560b..30ab9e7 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,8 @@ jinoos/ agents/__pycache__/ conversation_logs/ agents/test.py -nanda_agent/__pycache__ \ No newline at end of file +nanda_agent/__pycache__ + +# Build artifacts +dist/ +*.egg-info/ \ No newline at end of file diff --git a/nanda_agent/core/nanda.py b/nanda_agent/core/nanda.py index f60c08f..2e5f4ed 100644 --- a/nanda_agent/core/nanda.py +++ b/nanda_agent/core/nanda.py @@ -288,6 +288,11 @@ def start_flask_server(): 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: diff --git a/setup.py b/setup.py index 4901cad..1168d29 100644 --- a/setup.py +++ b/setup.py @@ -25,13 +25,13 @@ def read_readme(): setup( name="nanda-agent", - version="1.0.0", + version="1.0.2", 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/nanda-ai/nanda-agent", + url="https://github.com/aidecentralized/nanda-agent-sdk.git", packages=find_packages(), python_requires=">=3.8", install_requires=read_requirements("requirements.txt"), @@ -58,7 +58,7 @@ def read_readme(): "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", ], - keywords="ai agent framework langchain crewai anthropic claude", + keywords="nanda ai agent framework", include_package_data=True, zip_safe=False, ) \ No newline at end of file From 6ec4b294134e2bbb301c57883e57cebaa41e6e1a Mon Sep 17 00:00:00 2001 From: jinubabu Date: Thu, 10 Jul 2025 09:18:57 +0530 Subject: [PATCH 14/19] readme with dev and deploy steps --- README.md | 40 +++++++++++++++++++++++++++++++++++++++- setup.py | 2 +- 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index bebc0e3..adf7dc2 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ export ANTHROPIC_API_KEY="your-api-key-here"\ export DOMAIN_NAME="your-domain.com" ``` -### 2. Create Your Own Agent +### 2. Create Your Own Agent - Development ```bash 2.1 Write your improvement logic using the framework you like. Here it is a simple moduule without any llm call. @@ -180,6 +180,44 @@ export DOMAIN_NAME="your-domain.com" nanda-pirate ``` +#### Detailed steps to be done for the deployment +```bash +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 + +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 + + +3. Download the certificates into the machine for your domain. You should ensure in DNS an A record is mapping this domain chat1.chat39.org to IP address 66.175.209.173 +cmd : sudo certbot certonly --standalone -d chat1.chat39.org + +4. 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 + +5. Install the requirements file +cmd : python -m pip install --upgrade pip && pip3 install -r requirements.txt + +6. 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 + +7. Run the new improvement logic as a batch process +cmd : nohup python3 langchain_pirate.py > out.log 2>&1 & + +8. Open the log file and you could find the agent enrollment link +cmd : cat out.log + +9. Take the link and go to browser + +``` + + + + + + The framework will automatically: - Generate SSL certificates using Let's Encrypt - Set up proper agent registration diff --git a/setup.py b/setup.py index 1168d29..c4ed609 100644 --- a/setup.py +++ b/setup.py @@ -25,7 +25,7 @@ def read_readme(): setup( name="nanda-agent", - version="1.0.2", + version="1.0.3", description="Customizable AI Agent Communication Framework with pluggable message improvement logic", long_description=read_readme(), long_description_content_type="text/markdown", From 2c199921327c9ae37731272f33c0c3255396a653 Mon Sep 17 00:00:00 2001 From: jinubabu Date: Thu, 10 Jul 2025 11:32:37 +0530 Subject: [PATCH 15/19] pin the proper requirements --- setup.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index c4ed609..5d664bb 100644 --- a/setup.py +++ b/setup.py @@ -25,7 +25,7 @@ def read_readme(): setup( name="nanda-agent", - version="1.0.3", + version="1.0.4", description="Customizable AI Agent Communication Framework with pluggable message improvement logic", long_description=read_readme(), long_description_content_type="text/markdown", @@ -34,7 +34,16 @@ def read_readme(): url="https://github.com/aidecentralized/nanda-agent-sdk.git", packages=find_packages(), python_requires=">=3.8", - install_requires=read_requirements("requirements.txt"), + 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"], From 39b1b0e212e78f22bc859a6c3d0fc3cc484cbbec Mon Sep 17 00:00:00 2001 From: jinubabu Date: Fri, 11 Jul 2025 20:37:51 +0530 Subject: [PATCH 16/19] readme for EC2 as well --- README.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index adf7dc2..4e8f969 100644 --- a/README.md +++ b/README.md @@ -183,13 +183,19 @@ nanda-pirate #### Detailed steps to be done for the deployment ```bash 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 : ssh -i my-key.pem ec2-user@66.175.209.173 2. ssh into the server, ensure the latest software is in the system -cmd : ssh root@ 66.175.209.173 +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. Download the certificates into the machine for your domain. You should ensure in DNS an A record is mapping this domain chat1.chat39.org to IP address 66.175.209.173 cmd : sudo certbot certonly --standalone -d chat1.chat39.org @@ -197,6 +203,8 @@ cmd : sudo certbot certonly --standalone -d chat1.chat39.org 4. 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 + 5. Install the requirements file cmd : python -m pip install --upgrade pip && pip3 install -r requirements.txt From 33144a11f6bca972d87cf73efbb34386e268134a Mon Sep 17 00:00:00 2001 From: jinubabu Date: Sat, 12 Jul 2025 12:08:22 +0530 Subject: [PATCH 17/19] managed readme for non root user --- README.md | 27 +++++++++++++++++---------- nanda_agent/core/nanda.py | 8 ++++---- 2 files changed, 21 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 4e8f969..8ee47dc 100644 --- a/README.md +++ b/README.md @@ -188,7 +188,7 @@ 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 : ssh -i my-key.pem ec2-user@66.175.209.173 +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 @@ -197,27 +197,34 @@ cmd : ssh root@66.175.209.173 EC2 cmd : ssh ec2user@66.175.209.173 sudo dnf update -y && sudo dnf install -y python3.11 python3.11-pip certbot -3. Download the certificates into the machine for your domain. You should ensure in DNS an A record is mapping this domain chat1.chat39.org to IP address 66.175.209.173 -cmd : sudo certbot certonly --standalone -d chat1.chat39.org - -4. Create and Activate a virtual env in the folder where files are moved in step 1 +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 -5. Install the requirements file +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. 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 . + +6. Install the requirements file cmd : python -m pip install --upgrade pip && pip3 install -r requirements.txt -6. Ensure the env variables are available either through .env or you can provide export +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 -7. Run the new improvement logic as a batch process +8. Run the new improvement logic as a batch process cmd : nohup python3 langchain_pirate.py > out.log 2>&1 & -8. Open the log file and you could find the agent enrollment link +9. Open the log file and you could find the agent enrollment link cmd : cat out.log -9. Take the link and go to browser +10. Take the link and go to browser for registration ``` diff --git a/nanda_agent/core/nanda.py b/nanda_agent/core/nanda.py index 2e5f4ed..ad44831 100644 --- a/nanda_agent/core/nanda.py +++ b/nanda_agent/core/nanda.py @@ -242,11 +242,11 @@ def start_bridge_server(): # Configure SSL context if needed ssl_context = None if ssl: - # Set default certificate paths based on domain if not provided + # Set default certificate paths from current folder if not provided if not cert or not key: - cert = f"/etc/letsencrypt/live/{domain}/fullchain.pem" - key = f"/etc/letsencrypt/live/{domain}/privkey.pem" - print(f"šŸ”’ Using default Let's Encrypt certificates for domain: {domain}") + 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) From cd39ccae40cb129466a09179e1b8618b8a0f4b7a Mon Sep 17 00:00:00 2001 From: jinubabu Date: Sat, 12 Jul 2025 12:10:18 +0530 Subject: [PATCH 18/19] version updates --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 5d664bb..5f80dac 100644 --- a/setup.py +++ b/setup.py @@ -25,7 +25,7 @@ def read_readme(): setup( name="nanda-agent", - version="1.0.4", + 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", From 45b253c748c232418d15fd1eab1020a198d010ac Mon Sep 17 00:00:00 2001 From: jinubabu Date: Sun, 13 Jul 2025 05:45:32 +0530 Subject: [PATCH 19/19] access to cert --- README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 8ee47dc..2307111 100644 --- a/README.md +++ b/README.md @@ -207,10 +207,14 @@ EC2 cmd: cd /home/ec2-user/test-agents && python3.11 -m venv jinoos && source ji cmd : sudo certbot certonly --standalone -d chat1.chat39.org -5. Copy the cert to current folder for access. Ensure the domain has to be changed +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