From 938fc57366f8776374c959a30031d4239aaef00f Mon Sep 17 00:00:00 2001 From: destroyersrt Date: Mon, 13 Oct 2025 20:12:42 +0530 Subject: [PATCH 1/3] cleanup --- __init__.py | 9 +- examples/nanda_agent.py | 4 +- nanda_core/__init__.py | 3 +- nanda_core/core/__init__.py | 3 +- nanda_core/core/adapter.py | 79 +---- nanda_core/core/agent_bridge.py | 8 +- nanda_core/core/agent_facts.py | 366 ------------------------ nanda_core/core/custom_agent_handler.py | 165 ----------- nanda_core/core/nanda_compat.py | 243 ---------------- nanda_core/discovery/__init__.py | 15 - nanda_core/discovery/agent_discovery.py | 256 ----------------- nanda_core/discovery/agent_ranker.py | 307 -------------------- nanda_core/discovery/task_analyzer.py | 289 ------------------- scripts/deploy-agent.sh | 200 ------------- 14 files changed, 13 insertions(+), 1934 deletions(-) delete mode 100644 nanda_core/core/agent_facts.py delete mode 100644 nanda_core/core/custom_agent_handler.py delete mode 100644 nanda_core/core/nanda_compat.py delete mode 100644 nanda_core/discovery/__init__.py delete mode 100644 nanda_core/discovery/agent_discovery.py delete mode 100644 nanda_core/discovery/agent_ranker.py delete mode 100644 nanda_core/discovery/task_analyzer.py delete mode 100644 scripts/deploy-agent.sh diff --git a/__init__.py b/__init__.py index 4ff9925..679308c 100644 --- a/__init__.py +++ b/__init__.py @@ -7,17 +7,12 @@ and comprehensive monitoring capabilities. """ -from .nanda_core.core.adapter import NANDA, StreamlinedAdapter -from .nanda_core.core.adapter import echo_agent, pirate_agent, helpful_agent +from .nanda_core.core.adapter import NANDA __version__ = "2.0.0" __author__ = "NANDA Team" __email__ = "support@nanda.ai" __all__ = [ - "NANDA", # Main class - "StreamlinedAdapter", # Alias - "echo_agent", # Example agents - "pirate_agent", - "helpful_agent" + "NANDA" ] \ No newline at end of file diff --git a/examples/nanda_agent.py b/examples/nanda_agent.py index 3dadfa4..847799f 100644 --- a/examples/nanda_agent.py +++ b/examples/nanda_agent.py @@ -204,7 +204,7 @@ def main(): port=PORT, registry_url=AGENT_CONFIG["registry_url"], public_url=AGENT_CONFIG["public_url"], - enable_telemetry=False + enable_telemetry=True ) print(f"๐Ÿš€ Agent URL: http://localhost:{PORT}/a2a") @@ -262,7 +262,7 @@ def create_custom_agent(agent_name, specialization, domain, expertise_list, port agent_logic=agent_logic, port=port, registry_url=custom_config["registry_url"], - enable_telemetry=False + enable_telemetry=True ) print(f"๐Ÿค– Starting custom LLM agent: {agent_name}") diff --git a/nanda_core/__init__.py b/nanda_core/__init__.py index 89a23f1..b6561ca 100644 --- a/nanda_core/__init__.py +++ b/nanda_core/__init__.py @@ -3,11 +3,10 @@ Core components for the Streamlined NANDA Adapter """ -from .core.adapter import NANDA, StreamlinedAdapter +from .core.adapter import NANDA from .core.agent_bridge import SimpleAgentBridge __all__ = [ "NANDA", - "StreamlinedAdapter", "SimpleAgentBridge" ] \ No newline at end of file diff --git a/nanda_core/core/__init__.py b/nanda_core/core/__init__.py index dfb91d8..f0d8570 100644 --- a/nanda_core/core/__init__.py +++ b/nanda_core/core/__init__.py @@ -3,11 +3,10 @@ Core components for the Streamlined NANDA Adapter """ -from .adapter import NANDA, StreamlinedAdapter +from .adapter import NANDA from .agent_bridge import SimpleAgentBridge __all__ = [ "NANDA", - "StreamlinedAdapter", "SimpleAgentBridge" ] \ No newline at end of file diff --git a/nanda_core/core/adapter.py b/nanda_core/core/adapter.py index ac526b1..b98892c 100644 --- a/nanda_core/core/adapter.py +++ b/nanda_core/core/adapter.py @@ -23,7 +23,7 @@ def __init__(self, registry_url: Optional[str] = None, public_url: Optional[str] = None, host: str = "0.0.0.0", - enable_telemetry: bool = False): + enable_telemetry: bool = True): """ Create a simple NANDA agent @@ -93,76 +93,9 @@ def _register(self): print(f"โš ๏ธ Failed to register agent: HTTP {response.status_code}") except Exception as e: print(f"โš ๏ธ Registration error: {e}") - - def stop(self): - """Stop the agent (placeholder for cleanup)""" - print(f"๐Ÿ›‘ Stopping agent '{self.agent_id}'") - - -# Keep the StreamlinedAdapter class name for compatibility but simplified -class StreamlinedAdapter(NANDA): - """Alias for NANDA class for compatibility""" - pass - - -# Example agent logic functions -def echo_agent(message: str, conversation_id: str) -> str: - """Simple echo agent""" - return f"Echo: {message}" - - -def pirate_agent(message: str, conversation_id: str) -> str: - """Pirate-style agent""" - return f"Arrr! {message}, matey!" - - -def helpful_agent(message: str, conversation_id: str) -> str: - """Helpful agent""" - if "time" in message.lower(): - from datetime import datetime - return f"Current time: {datetime.now().strftime('%H:%M:%S')}" - elif "help" in message.lower(): - return "I can help with time, calculations, and general questions!" - elif any(op in message for op in ['+', '-', '*', '/']): - try: - result = eval(message.replace('x', '*').replace('X', '*')) - return f"Result: {result}" - except: - return "Invalid calculation" - else: - return f"I can help with: {message}" - - -def main(): - """Main CLI entry point""" - import argparse - - parser = argparse.ArgumentParser(description="Simple NANDA Agent") - parser.add_argument("--agent-id", required=True, help="Agent ID") - parser.add_argument("--port", type=int, default=6000, help="Server port") - parser.add_argument("--host", default="0.0.0.0", help="Server host") - parser.add_argument("--registry", help="Registry URL") - parser.add_argument("--public-url", help="Public URL for registration") - parser.add_argument("--no-register", action="store_true", help="Don't register with registry") - - args = parser.parse_args() - # Use helpful agent as default - nanda = NANDA( - agent_id=args.agent_id, - agent_logic=helpful_agent, - port=args.port, - registry_url=args.registry, - public_url=args.public_url, - host=args.host - ) - - try: - nanda.start(register=not args.no_register) - except KeyboardInterrupt: - print("\n๐Ÿ›‘ Server stopped") - nanda.stop() - - -if __name__ == "__main__": - main() \ No newline at end of file + def stop(self): + """Stop the agent and cleanup telemetry""" + if self.telemetry: + self.telemetry.stop() + print(f"๐Ÿ›‘ Stopping agent '{self.agent_id}'") \ No newline at end of file diff --git a/nanda_core/core/agent_bridge.py b/nanda_core/core/agent_bridge.py index bf022a3..fc1a668 100644 --- a/nanda_core/core/agent_bridge.py +++ b/nanda_core/core/agent_bridge.py @@ -197,7 +197,7 @@ def _send_to_agent(self, target_agent_id: str, message_text: str, conversation_i ) if self.telemetry: - self.telemetry.log_agent_message_sent(self.agent_id, target_agent_id, conversation_id) + self.telemetry.log_message_sent(target_agent_id, conversation_id) # Extract the actual response content from the target agent logger.info(f"๐Ÿ” [{self.agent_id}] Response type: {type(response)}, has parts: {hasattr(response, 'parts') if response else 'None'}") @@ -234,12 +234,6 @@ def _lookup_agent(self, agent_id: str) -> Optional[str]: # Fallback to local discovery (for testing) local_agents = { "test_agent": "http://localhost:6000", - "pirate_agent": "http://localhost:6001", - "helpful_agent": "http://localhost:6002", - "echo_agent": "http://localhost:6003", - "simple_test_agent": "http://localhost:6005", - "agent_alpha": "http://localhost:6010", - "agent_beta": "http://localhost:6011" } if agent_id in local_agents: diff --git a/nanda_core/core/agent_facts.py b/nanda_core/core/agent_facts.py deleted file mode 100644 index d8dd0c3..0000000 --- a/nanda_core/core/agent_facts.py +++ /dev/null @@ -1,366 +0,0 @@ -#!/usr/bin/env python3 -""" -AgentFacts Generator and Server for NANDA Project -Implements the AgentFacts specification for agent capability description -""" - -import json -import os -from typing import Dict, List, Any, Optional -from datetime import datetime, timedelta -from dataclasses import dataclass, asdict -import threading - -try: - from flask import Flask, jsonify, send_from_directory - FLASK_AVAILABLE = True -except ImportError: - print("โš ๏ธ Flask not available - AgentFacts server will be disabled") - FLASK_AVAILABLE = False - Flask = None - - -@dataclass -class AgentCapabilities: - """Agent capabilities structure""" - modalities: List[str] # ["text", "image", "audio"] - skills: List[str] # ["financial_analysis", "data_visualization"] - domains: List[str] # ["finance", "healthcare", "marketing"] - languages: List[str] # ["english", "spanish"] - streaming: bool = False - batch: bool = True - reasoning: bool = True - memory: bool = False - - -@dataclass -class AgentEndpoints: - """Agent endpoints structure""" - static: str # Primary A2A endpoint - api: Optional[str] = None # REST API endpoint - websocket: Optional[str] = None # WebSocket endpoint - - -@dataclass -class AgentCertification: - """Agent certification information""" - level: str = "verified" # verified, beta, experimental - issued_by: str = "NANDA" - issued_date: str = None - expires_date: str = None - - def __post_init__(self): - if not self.issued_date: - self.issued_date = datetime.now().isoformat() - if not self.expires_date: - # Default 30-day expiration - expires = datetime.now() + timedelta(days=30) - self.expires_date = expires.isoformat() - - -@dataclass -class AgentFacts: - """Complete AgentFacts specification""" - context: str = "https://projectnanda.org/agentfacts/v1" - id: str = None - handle: str = None - provider: str = "streamlined_adapter" - jurisdiction: str = "USA" - version: str = "1.0" - certification: AgentCertification = None - capabilities: AgentCapabilities = None - endpoints: AgentEndpoints = None - description: str = "" - tags: List[str] = None - - def __post_init__(self): - if not self.certification: - self.certification = AgentCertification() - if not self.tags: - self.tags = [] - - -class AgentFactsGenerator: - """Generate AgentFacts JSON for agents""" - - def __init__(self, base_url: str = None): - self.base_url = base_url or "http://localhost" - - def create_agent_facts(self, - agent_id: str, - port: int, - capabilities: AgentCapabilities, - description: str = "", - tags: List[str] = None) -> AgentFacts: - """Create AgentFacts for an agent""" - - endpoints = AgentEndpoints( - static=f"{self.base_url}:{port}", - api=f"{self.base_url}:{port + 100}" # API port offset - ) - - agent_facts = AgentFacts( - id=f"did:nanda:agent:{agent_id}", - handle=f"@{agent_id}", - capabilities=capabilities, - endpoints=endpoints, - description=description, - tags=tags or [] - ) - - return agent_facts - - def to_json(self, agent_facts: AgentFacts) -> Dict[str, Any]: - """Convert AgentFacts to JSON-serializable dict""" - result = { - "@context": agent_facts.context, - "id": agent_facts.id, - "handle": agent_facts.handle, - "provider": agent_facts.provider, - "jurisdiction": agent_facts.jurisdiction, - "version": agent_facts.version, - "certification": asdict(agent_facts.certification), - "capabilities": asdict(agent_facts.capabilities), - "endpoints": asdict(agent_facts.endpoints) - } - - if agent_facts.description: - result["description"] = agent_facts.description - - if agent_facts.tags: - result["tags"] = agent_facts.tags - - return result - - -class AgentFactsServer: - """HTTP server for serving AgentFacts JSON files""" - - def __init__(self, port: int = 8080): - self.port = port - self.agent_facts = {} # agent_id -> AgentFacts - self.server_thread = None - - if not FLASK_AVAILABLE: - print(f"โš ๏ธ Flask not available - AgentFacts server on port {port} disabled") - self.app = None - return - - self.app = Flask(__name__) - self.setup_routes() - - def setup_routes(self): - """Setup Flask routes for AgentFacts""" - - @self.app.route('/@.json') - def get_agent_facts(agent_id): - """Serve AgentFacts JSON for specific agent""" - if agent_id in self.agent_facts: - generator = AgentFactsGenerator() - facts_json = generator.to_json(self.agent_facts[agent_id]) - return jsonify(facts_json) - else: - return {"error": f"Agent {agent_id} not found"}, 404 - - @self.app.route('/agents') - def list_agents(): - """List all available agents""" - return jsonify({ - "agents": list(self.agent_facts.keys()), - "count": len(self.agent_facts) - }) - - @self.app.route('/health') - def health_check(): - """Health check endpoint""" - return {"status": "healthy", "agents": len(self.agent_facts)} - - def register_agent_facts(self, agent_id: str, agent_facts: AgentFacts): - """Register AgentFacts for an agent""" - self.agent_facts[agent_id] = agent_facts - print(f"๐Ÿ“‹ Registered AgentFacts for {agent_id}") - - def get_agent_facts_url(self, agent_id: str) -> str: - """Get the AgentFacts URL for an agent""" - return f"http://localhost:{self.port}/@{agent_id}.json" - - def start_server(self): - """Start the AgentFacts server in background""" - if not FLASK_AVAILABLE or not self.app: - print(f"โš ๏ธ Cannot start AgentFacts server - Flask not available") - return - - def run_server(): - self.app.run(host='0.0.0.0', port=self.port, debug=False) - - self.server_thread = threading.Thread(target=run_server, daemon=True) - self.server_thread.start() - print(f"๐Ÿ“ก AgentFacts server started on port {self.port}") - - def stop_server(self): - """Stop the AgentFacts server""" - # Flask doesn't have a built-in stop method, so we use daemon threads - print(f"๐Ÿ›‘ AgentFacts server stopping...") - - -# Predefined capability templates for common agent types -class CapabilityTemplates: - """Common capability templates for different agent types""" - - @staticmethod - def data_scientist(level: str = "senior") -> AgentCapabilities: - """Data scientist capabilities""" - skills = ["data_analysis", "statistical_modeling", "data_visualization"] - if level == "senior": - skills.extend(["machine_learning", "deep_learning", "feature_engineering"]) - elif level == "ml_specialist": - skills.extend(["machine_learning", "deep_learning", "neural_networks", "model_optimization"]) - - return AgentCapabilities( - modalities=["text"], - skills=skills, - domains=["data_science", "analytics"], - languages=["english"], - batch=True, - reasoning=True - ) - - @staticmethod - def financial_analyst(specialty: str = "general") -> AgentCapabilities: - """Financial analyst capabilities""" - skills = ["financial_analysis", "market_research", "financial_modeling"] - domains = ["finance", "economics"] - - if specialty == "risk": - skills.extend(["risk_assessment", "portfolio_analysis", "stress_testing"]) - domains.append("risk_management") - elif specialty == "investment": - skills.extend(["investment_analysis", "valuation", "portfolio_optimization"]) - domains.append("investments") - - return AgentCapabilities( - modalities=["text"], - skills=skills, - domains=domains, - languages=["english"], - batch=True, - reasoning=True - ) - - @staticmethod - def healthcare_expert(specialty: str = "general") -> AgentCapabilities: - """Healthcare expert capabilities""" - skills = ["medical_knowledge", "symptom_analysis", "treatment_planning"] - domains = ["healthcare", "medicine"] - - if specialty == "diagnosis": - skills.extend(["diagnostic_reasoning", "differential_diagnosis", "clinical_assessment"]) - domains.append("diagnostics") - elif specialty == "treatment": - skills.extend(["treatment_protocols", "medication_management", "care_planning"]) - domains.append("therapeutics") - - return AgentCapabilities( - modalities=["text"], - skills=skills, - domains=domains, - languages=["english"], - batch=True, - reasoning=True, - memory=True # Medical agents often need patient history - ) - - @staticmethod - def marketing_specialist(focus: str = "strategy") -> AgentCapabilities: - """Marketing specialist capabilities""" - skills = ["market_analysis", "customer_segmentation", "campaign_planning"] - domains = ["marketing", "business"] - - if focus == "content": - skills.extend(["content_creation", "copywriting", "brand_messaging"]) - domains.append("content_marketing") - elif focus == "digital": - skills.extend(["digital_marketing", "social_media", "seo_optimization"]) - domains.append("digital_marketing") - - return AgentCapabilities( - modalities=["text"], - skills=skills, - domains=domains, - languages=["english"], - batch=True, - reasoning=True - ) - - @staticmethod - def general_assistant() -> AgentCapabilities: - """General assistant capabilities""" - return AgentCapabilities( - modalities=["text"], - skills=["general_assistance", "task_coordination", "information_retrieval"], - domains=["general", "productivity"], - languages=["english"], - batch=True, - reasoning=True, - memory=True - ) - - -# Example usage -def create_sample_agent_facts(): - """Create sample AgentFacts for testing""" - generator = AgentFactsGenerator("http://10.189.72.201") - - # Senior Data Scientist - senior_ds_facts = generator.create_agent_facts( - agent_id="senior_data_scientist", - port=7001, - capabilities=CapabilityTemplates.data_scientist("senior"), - description="Senior data scientist with 10+ years experience in machine learning and statistical analysis", - tags=["expert", "senior", "python", "sql", "machine_learning"] - ) - - # Financial Risk Analyst - risk_analyst_facts = generator.create_agent_facts( - agent_id="risk_analyst", - port=7005, - capabilities=CapabilityTemplates.financial_analyst("risk"), - description="Financial risk analyst specializing in portfolio risk assessment and stress testing", - tags=["finance", "risk", "portfolio", "quantitative"] - ) - - return { - "senior_data_scientist": senior_ds_facts, - "risk_analyst": risk_analyst_facts - } - - -if __name__ == "__main__": - # Test the AgentFacts system - facts_server = AgentFactsServer(8080) - - # Create sample facts - sample_facts = create_sample_agent_facts() - - # Register with server - for agent_id, facts in sample_facts.items(): - facts_server.register_agent_facts(agent_id, facts) - - # Start server - facts_server.start_server() - - print("๐Ÿงช Testing AgentFacts URLs:") - for agent_id in sample_facts.keys(): - url = facts_server.get_agent_facts_url(agent_id) - print(f" {agent_id}: {url}") - - print("\n๐Ÿ“ก AgentFacts server running. Test with:") - print(" curl http://localhost:8080/@senior_data_scientist.json") - print(" curl http://localhost:8080/agents") - - try: - import time - while True: - time.sleep(1) - except KeyboardInterrupt: - print("\n๐Ÿ›‘ Stopping AgentFacts server") \ No newline at end of file diff --git a/nanda_core/core/custom_agent_handler.py b/nanda_core/core/custom_agent_handler.py deleted file mode 100644 index 906fb31..0000000 --- a/nanda_core/core/custom_agent_handler.py +++ /dev/null @@ -1,165 +0,0 @@ -#!/usr/bin/env python3 -""" -Custom Agent Handler for attaching user-defined agent logic -""" - -from typing import Callable, Optional, Dict, Any -from python_a2a import Message - - -class CustomAgentHandler: - """Handles custom agent logic without message improvement""" - - def __init__(self): - self.message_handler: Optional[Callable[[str, str], str]] = None - self.query_handler: Optional[Callable[[str, str], str]] = None - self.command_handlers: Dict[str, Callable[[str, str], str]] = {} - - # Conversation control - self.conversation_counts: Dict[str, int] = {} - self.max_exchanges_per_conversation: Optional[int] = None - self.stop_keywords: list = [] - self.enable_stop_control: bool = False - - def set_message_handler(self, handler: Callable[[str, str], str]): - """ - Set custom message handler for regular messages - - Args: - handler: Function that takes (message_text, conversation_id) -> response_text - Note: This is NOT for improving messages, but for custom agent responses - """ - self.message_handler = handler - - def set_query_handler(self, handler: Callable[[str, str], str]): - """ - Set custom handler for /query commands - - Args: - handler: Function that takes (query_text, conversation_id) -> response_text - """ - self.query_handler = handler - - def add_command_handler(self, command: str, handler: Callable[[str, str], str]): - """ - Add custom handler for specific commands - - Args: - command: Command name (without /) - handler: Function that takes (command_args, conversation_id) -> response_text - """ - self.command_handlers[command] = handler - - def enable_conversation_control(self, max_exchanges: int = None, stop_keywords: list = None): - """ - Enable conversation control mechanisms - - Args: - max_exchanges: Maximum number of exchanges per conversation (None = unlimited) - stop_keywords: List of keywords that end conversations (e.g., ['bye', 'stop']) - """ - self.enable_stop_control = True - self.max_exchanges_per_conversation = max_exchanges - self.stop_keywords = stop_keywords or [] - print(f"๐Ÿ›‘ Conversation control enabled: max_exchanges={max_exchanges}, stop_keywords={stop_keywords}") - - def should_respond_to_conversation(self, message_text: str, conversation_id: str) -> bool: - """ - Check if agent should respond based on conversation control rules - - Returns: - True if agent should respond, False if conversation should stop - """ - if not self.enable_stop_control: - return True # No control enabled, always respond - - # Track conversation count - if conversation_id not in self.conversation_counts: - self.conversation_counts[conversation_id] = 0 - self.conversation_counts[conversation_id] += 1 - - current_count = self.conversation_counts[conversation_id] - - # Check exchange limit - if self.max_exchanges_per_conversation and current_count > self.max_exchanges_per_conversation: - print(f"๐Ÿ›‘ Conversation {conversation_id} stopped: exceeded max exchanges ({self.max_exchanges_per_conversation})") - return False - - # Check stop keywords - message_lower = message_text.lower() - for keyword in self.stop_keywords: - if keyword.lower() in message_lower: - print(f"๐Ÿ›‘ Conversation {conversation_id} stopped: stop keyword '{keyword}' detected") - return False - - return True - - def handle_message(self, message_text: str, conversation_id: str, message_type: str = "regular") -> Optional[str]: - """ - Handle message using appropriate custom handler - - Args: - message_text: The message content (NOT modified/improved) - conversation_id: Conversation identifier - message_type: Type of message (regular, query, command) - - Returns: - Custom response or None if no handler available - """ - if message_type == "regular" and self.message_handler: - return self.message_handler(message_text, conversation_id) - elif message_type == "query" and self.query_handler: - return self.query_handler(message_text, conversation_id) - elif message_type == "command": - # Extract command from message_text - parts = message_text.split(" ", 1) - command = parts[0][1:] if parts[0].startswith("/") else parts[0] - args = parts[1] if len(parts) > 1 else "" - - if command in self.command_handlers: - return self.command_handlers[command](args, conversation_id) - - return None - - def has_handlers(self) -> bool: - """Check if any custom handlers are configured""" - return (self.message_handler is not None or - self.query_handler is not None or - len(self.command_handlers) > 0) - - -# Example usage patterns for documentation -class AgentExamples: - """Example agent implementations""" - - @staticmethod - def simple_echo_agent(message_text: str, conversation_id: str) -> str: - """Simple echo agent that repeats messages""" - return f"Echo: {message_text}" - - @staticmethod - def math_agent(message_text: str, conversation_id: str) -> str: - """Simple math agent for calculations""" - try: - # Simple math evaluation (in production, use safer parsing) - if any(op in message_text for op in ['+', '-', '*', '/', '(', ')']): - # In real implementation, use ast.literal_eval or similar for safety - result = eval(message_text.replace('x', '*')) - return f"Result: {result}" - else: - return "Please provide a math expression" - except: - return "Invalid math expression" - - @staticmethod - def file_agent(query_text: str, conversation_id: str) -> str: - """Agent that handles file-related queries""" - if "list files" in query_text.lower(): - import os - files = os.listdir(".")[:10] # Limit to 10 files - return f"Files: {', '.join(files)}" - elif "current directory" in query_text.lower(): - import os - return f"Current directory: {os.getcwd()}" - else: - return "Available commands: 'list files', 'current directory'" \ No newline at end of file diff --git a/nanda_core/core/nanda_compat.py b/nanda_core/core/nanda_compat.py deleted file mode 100644 index 598ab10..0000000 --- a/nanda_core/core/nanda_compat.py +++ /dev/null @@ -1,243 +0,0 @@ -#!/usr/bin/env python3 -""" -NANDA Compatibility Layer for Streamlined Adapter - -Provides backwards compatibility with the original NANDA interface while using -the streamlined adapter architecture underneath. -""" - -import os -import sys -import threading -from typing import Callable, Optional, Dict, Any -from .adapter import StreamlinedAdapter - - -class NANDA: - """ - Backwards compatibility class that mimics the original NANDA interface - but uses the streamlined adapter underneath without message improvement. - """ - - def __init__(self, improvement_logic: Callable[[str], str]): - """ - Initialize NANDA with custom improvement logic - - Note: In the streamlined version, this becomes a custom response handler - instead of a message modifier. - - Args: - improvement_logic: Function that takes (message_text: str) -> str - This will be converted to a response handler, not message modifier - """ - self.improvement_logic = improvement_logic - self.adapter = None - - print(f"๐Ÿค– NANDA (Streamlined) initialized with handler logic: {improvement_logic.__name__}") - print("โš ๏ธ Note: This logic will be used for agent responses, NOT message modification") - - # Create the streamlined adapter - self._create_adapter() - - # Set up custom handlers - self._setup_handlers() - - def _create_adapter(self): - """Create the underlying streamlined adapter""" - # Get agent configuration from environment - agent_id = os.getenv("AGENT_ID", "nanda_agent") - self.adapter = StreamlinedAdapter(agent_id) - print(f"โœ… Streamlined adapter created for agent: {agent_id}") - - def _setup_handlers(self): - """Set up custom handlers using the improvement logic""" - - def response_handler(message_text: str, conversation_id: str) -> str: - """Convert improvement logic to response handler""" - try: - # Use the improvement logic as a response generator - response = self.improvement_logic(message_text) - return f"Agent Response: {response}" - except Exception as e: - print(f"Error in custom handler: {e}") - return f"Received: {message_text}" - - # Set the handler - self.adapter.set_message_handler(response_handler) - print(f"๐Ÿ”ง Custom response handler '{self.improvement_logic.__name__}' configured") - - def start_server(self, host: str = "0.0.0.0"): - """ - Start the agent bridge server - - Args: - host: Host to bind to (default: "0.0.0.0") - """ - print("๐Ÿš€ NANDA (Streamlined) starting server...") - - # Read configuration from environment - public_url = os.getenv("PUBLIC_URL") - api_url = os.getenv("API_URL") - - # Register with registry if PUBLIC_URL is set - register_with_registry = bool(public_url) - - if public_url: - print(f"๐Ÿ“ Will register agent with registry using URL: {public_url}") - else: - print("โš ๏ธ PUBLIC_URL not set - agent will not be registered with registry") - - # Start the streamlined adapter server - self.adapter.start_server(host=host, register_with_registry=register_with_registry) - - def start_server_api(self, anthropic_key: str, domain: str, agent_id: Optional[str] = None, - port: int = 6000, api_port: int = 6001, **kwargs): - """ - Start NANDA with API server support (backwards compatibility) - - Args: - anthropic_key: Anthropic API key - domain: Domain name for the server - agent_id: Agent ID (optional) - port: Agent bridge port - api_port: API server port - **kwargs: Additional arguments (for compatibility) - """ - print("๐Ÿš€ NANDA (Streamlined) starting with API server support...") - - # Set environment variables for compatibility - os.environ["ANTHROPIC_API_KEY"] = anthropic_key - if agent_id: - os.environ["AGENT_ID"] = agent_id - os.environ["PORT"] = str(port) - - # Generate URLs based on domain - public_url = f"https://{domain}:{port}" - api_url = f"https://{domain}:{api_port}" - - os.environ["PUBLIC_URL"] = public_url - os.environ["API_URL"] = api_url - - print(f"๐ŸŒ Domain: {domain}") - print(f"๐Ÿ”— Public URL: {public_url}") - print(f"๐Ÿ”— API URL: {api_url}") - - # Start the Flask API server in a separate thread - self._start_api_server(api_port) - - # Start the main agent server - self.start_server() - - def _start_api_server(self, api_port: int): - """Start a simple Flask API server for compatibility""" - try: - from flask import Flask, request, jsonify - from flask_cors import CORS - - app = Flask(__name__) - CORS(app) - - @app.route('/api/health', methods=['GET']) - def health_check(): - return jsonify({"status": "healthy", "agent_id": self.adapter.agent_id}) - - @app.route('/api/send', methods=['POST']) - def send_message(): - data = request.get_json() - message = data.get('message', '') - # This would typically send to the agent - return jsonify({"status": "message received", "message": message}) - - @app.route('/api/agents/list', methods=['GET']) - def list_agents(): - agents = self.adapter.list_available_agents() - return jsonify(agents) - - @app.route('/api/receive_message', methods=['POST']) - def receive_message(): - data = request.get_json() - # Handle incoming messages from UI clients - return jsonify({"status": "ok"}) - - @app.route('/api/render', methods=['GET']) - def render(): - # Return latest message (for compatibility) - return jsonify({"message": "No recent messages"}) - - def run_flask(): - print(f"๐Ÿ“ก Starting API server on port {api_port}") - app.run(host='0.0.0.0', port=api_port, ssl_context='adhoc', debug=False) - - # Start Flask in background thread - api_thread = threading.Thread(target=run_flask, daemon=True) - api_thread.start() - - except ImportError: - print("โš ๏ธ Flask not available - API server will not start") - print(" Install with: pip install flask flask-cors pyopenssl") - - @property - def bridge(self): - """Access to the underlying bridge (for compatibility)""" - return self.adapter.bridge - - def stop(self): - """Stop the NANDA server""" - if self.adapter: - self.adapter.stop() - - -# Utility functions for compatibility -def create_nanda_adapter(improvement_logic: Callable[[str], str]) -> NANDA: - """Create a NANDA adapter with custom improvement logic""" - return NANDA(improvement_logic) - - -def start_nanda_server(improvement_logic: Callable[[str], str], **kwargs): - """Quick start function for NANDA server""" - nanda = NANDA(improvement_logic) - - if 'anthropic_key' in kwargs and 'domain' in kwargs: - nanda.start_server_api(**kwargs) - else: - nanda.start_server() - - return nanda - - -# Example improvement functions for testing -def example_pirate_improver(message: str) -> str: - """Example pirate-style message improver""" - return f"Arrr! {message}, matey!" - - -def example_professional_improver(message: str) -> str: - """Example professional message improver""" - return f"I would like to formally communicate: {message}" - - -def example_echo_improver(message: str) -> str: - """Example echo improver""" - return f"Echo: {message}" - - -if __name__ == "__main__": - # Test the NANDA compatibility layer - print("๐Ÿงช Testing NANDA Compatibility Layer") - - # Test with a simple improvement function - def test_improver(message: str) -> str: - return f"Improved: {message}" - - # Create NANDA instance - nanda = NANDA(test_improver) - - print(f"โœ… NANDA instance created with agent ID: {nanda.adapter.agent_id}") - print("๐Ÿš€ Starting server...") - - try: - nanda.start_server() - except KeyboardInterrupt: - print("\n๐Ÿ›‘ Server stopped") - nanda.stop() - diff --git a/nanda_core/discovery/__init__.py b/nanda_core/discovery/__init__.py deleted file mode 100644 index 292759c..0000000 --- a/nanda_core/discovery/__init__.py +++ /dev/null @@ -1,15 +0,0 @@ -#!/usr/bin/env python3 -""" -Agent Discovery and Ranking System -Intelligent search and recommendation for the agent ecosystem -""" - -from .agent_discovery import AgentDiscovery -from .agent_ranker import AgentRanker -from .task_analyzer import TaskAnalyzer - -__all__ = [ - "AgentDiscovery", - "AgentRanker", - "TaskAnalyzer" -] \ No newline at end of file diff --git a/nanda_core/discovery/agent_discovery.py b/nanda_core/discovery/agent_discovery.py deleted file mode 100644 index 7b194c6..0000000 --- a/nanda_core/discovery/agent_discovery.py +++ /dev/null @@ -1,256 +0,0 @@ -#!/usr/bin/env python3 -""" -Agent Discovery System - Intelligent search and recommendation for the agent ecosystem -""" - -from typing import Dict, List, Any, Optional -from dataclasses import dataclass -from .task_analyzer import TaskAnalyzer, TaskAnalysis -from .agent_ranker import AgentRanker, AgentScore -from ..core.registry_client import RegistryClient - - -@dataclass -class DiscoveryResult: - """Result of agent discovery process""" - task_analysis: TaskAnalysis - recommended_agents: List[AgentScore] - total_agents_evaluated: int - search_time_seconds: float - suggestions: List[str] - - -class AgentDiscovery: - """Main discovery system that coordinates task analysis and agent ranking""" - - def __init__(self, registry_client: Optional[RegistryClient] = None): - self.registry_client = registry_client or RegistryClient() - self.task_analyzer = TaskAnalyzer() - self.agent_ranker = AgentRanker() - self.performance_cache = {} - - def discover_agents(self, task_description: str, limit: int = 5, - min_score: float = 0.3, filters: Dict[str, Any] = None) -> DiscoveryResult: - """Main entry point for agent discovery""" - import time - start_time = time.time() - - # Analyze the task - task_analysis = self.task_analyzer.analyze_task(task_description) - - # Get available agents - agents = self._get_relevant_agents(task_analysis, filters) - - # Get performance data - performance_data = self._get_performance_data() - - # Rank agents - agent_scores = self.agent_ranker.rank_agents(agents, task_analysis, performance_data) - - # Get top recommendations - recommendations = self.agent_ranker.get_top_recommendations( - agent_scores, limit, min_score - ) - - # Generate suggestions - suggestions = self._generate_suggestions(task_analysis, recommendations) - - search_time = time.time() - start_time - - return DiscoveryResult( - task_analysis=task_analysis, - recommended_agents=recommendations, - total_agents_evaluated=len(agents), - search_time_seconds=search_time, - suggestions=suggestions - ) - - def search_agents_by_capabilities(self, capabilities: List[str], - domain: str = None) -> List[Dict[str, Any]]: - """Search agents by specific capabilities""" - filters = {"capabilities": capabilities} - if domain: - filters["domain"] = domain - - return self.registry_client.search_agents(capabilities=capabilities) - - def search_agents_by_domain(self, domain: str) -> List[Dict[str, Any]]: - """Search agents by domain expertise""" - return self.registry_client.search_agents(query=domain) - - def get_agent_details(self, agent_id: str) -> Optional[Dict[str, Any]]: - """Get detailed information about a specific agent""" - return self.registry_client.get_agent_metadata(agent_id) - - def _get_relevant_agents(self, task_analysis: TaskAnalysis, - filters: Dict[str, Any] = None) -> List[Dict[str, Any]]: - """Get agents relevant to the task""" - - # Start with capability-based search - agents = set() - - # Search by required capabilities - if task_analysis.required_capabilities: - cap_agents = self.registry_client.search_agents( - capabilities=task_analysis.required_capabilities - ) - agents.update(tuple(sorted(agent.items())) for agent in cap_agents) - - # Search by domain - if task_analysis.domain and task_analysis.domain != "general": - domain_agents = self.registry_client.search_agents( - query=task_analysis.domain - ) - agents.update(tuple(sorted(agent.items())) for agent in domain_agents) - - # Search by keywords - if task_analysis.keywords: - keyword_query = " ".join(task_analysis.keywords[:3]) # Top 3 keywords - keyword_agents = self.registry_client.search_agents(query=keyword_query) - agents.update(tuple(sorted(agent.items())) for agent in keyword_agents) - - # Convert back to list of dictionaries - agent_list = [dict(agent) for agent in agents] - - # Apply additional filters - if filters: - agent_list = self._apply_filters(agent_list, filters) - - # If no specific matches, get general agents - if not agent_list: - agent_list = self.registry_client.list_agents() - - return agent_list - - def _apply_filters(self, agents: List[Dict[str, Any]], - filters: Dict[str, Any]) -> List[Dict[str, Any]]: - """Apply additional filters to agent list""" - filtered = agents - - if "status" in filters: - filtered = [a for a in filtered if a.get("status") == filters["status"]] - - if "min_score" in filters: - # This would require pre-scoring, so skip for now - pass - - if "exclude_agents" in filters: - exclude_set = set(filters["exclude_agents"]) - filtered = [a for a in filtered if a.get("agent_id") not in exclude_set] - - if "domain" in filters: - domain_filter = filters["domain"].lower() - filtered = [a for a in filtered - if a.get("domain", "").lower() == domain_filter] - - return filtered - - def _get_performance_data(self) -> Dict[str, Any]: - """Get cached performance data for agents""" - # This would typically come from a telemetry system - # For now, return mock data or cached data - return self.performance_cache - - def update_performance_data(self, agent_id: str, performance_metrics: Dict[str, Any]): - """Update performance data for an agent""" - self.performance_cache[agent_id] = performance_metrics - - def _generate_suggestions(self, task_analysis: TaskAnalysis, - recommendations: List[AgentScore]) -> List[str]: - """Generate helpful suggestions based on discovery results""" - suggestions = [] - - if not recommendations: - suggestions.extend([ - "No agents found matching your requirements", - f"Try searching for agents with '{task_analysis.domain}' domain expertise", - "Consider breaking down your task into smaller components", - "Check if your required capabilities are too specific" - ]) - elif len(recommendations) == 1: - suggestions.append("Only one agent found - consider broadening your search criteria") - elif task_analysis.complexity == "complex": - suggestions.extend([ - "This appears to be a complex task", - "Consider using multiple agents for different components", - "Review the top agents' capabilities to ensure full coverage" - ]) - - # Add suggestions based on task type - if task_analysis.task_type == "data_analysis": - suggestions.append("For data analysis tasks, ensure agents have visualization capabilities") - elif task_analysis.task_type == "automation": - suggestions.append("For automation, look for agents with workflow management features") - - # Add performance-based suggestions - if recommendations: - top_score = recommendations[0].score - if top_score < 0.7: - suggestions.append("Match confidence is moderate - review agent details carefully") - - return suggestions - - def explain_recommendations(self, discovery_result: DiscoveryResult) -> str: - """Generate detailed explanation of the discovery process and results""" - lines = [] - - # Task analysis summary - lines.append("=== Task Analysis ===") - lines.append(f"Task Type: {discovery_result.task_analysis.task_type}") - lines.append(f"Domain: {discovery_result.task_analysis.domain}") - lines.append(f"Complexity: {discovery_result.task_analysis.complexity}") - lines.append(f"Required Capabilities: {', '.join(discovery_result.task_analysis.required_capabilities)}") - lines.append(f"Key Keywords: {', '.join(discovery_result.task_analysis.keywords[:5])}") - lines.append(f"Analysis Confidence: {discovery_result.task_analysis.confidence:.2f}") - lines.append("") - - # Search results summary - lines.append("=== Search Results ===") - lines.append(f"Total Agents Evaluated: {discovery_result.total_agents_evaluated}") - lines.append(f"Agents Recommended: {len(discovery_result.recommended_agents)}") - lines.append(f"Search Time: {discovery_result.search_time_seconds:.2f} seconds") - lines.append("") - - # Detailed agent recommendations - if discovery_result.recommended_agents: - lines.append("=== Recommended Agents ===") - for i, agent_score in enumerate(discovery_result.recommended_agents, 1): - lines.append(f"\n{i}. Agent: {agent_score.agent_id}") - lines.append(f" Score: {agent_score.score:.2f}") - lines.append(f" Confidence: {agent_score.confidence:.2f}") - if agent_score.match_reasons: - lines.append(" Match Reasons:") - for reason in agent_score.match_reasons: - lines.append(f" - {reason}") - else: - lines.append("=== No Agents Found ===") - - # Suggestions - if discovery_result.suggestions: - lines.append("\n=== Suggestions ===") - for suggestion in discovery_result.suggestions: - lines.append(f"- {suggestion}") - - return "\n".join(lines) - - def get_similar_agents(self, agent_id: str, limit: int = 3) -> List[Dict[str, Any]]: - """Find agents similar to the given agent""" - target_agent = self.registry_client.get_agent_metadata(agent_id) - if not target_agent: - return [] - - # Create a pseudo-task based on the agent's characteristics - task_desc = f"Task requiring {target_agent.get('domain', 'general')} domain expertise" - if target_agent.get('capabilities'): - task_desc += f" with capabilities: {', '.join(target_agent['capabilities'])}" - - # Discover similar agents - result = self.discover_agents(task_desc, limit=limit + 1) # +1 to exclude self - - # Filter out the original agent - similar = [ - agent for agent in result.recommended_agents - if agent.agent_id != agent_id - ] - - return similar[:limit] \ No newline at end of file diff --git a/nanda_core/discovery/agent_ranker.py b/nanda_core/discovery/agent_ranker.py deleted file mode 100644 index 1fb56f5..0000000 --- a/nanda_core/discovery/agent_ranker.py +++ /dev/null @@ -1,307 +0,0 @@ -#!/usr/bin/env python3 -""" -Agent Ranking System for scoring and recommending agents based on task fit -""" - -from typing import Dict, List, Any, Tuple -from dataclasses import dataclass -from datetime import datetime, timedelta -import math - - -@dataclass -class AgentScore: - """Score result for an agent""" - agent_id: str - score: float - confidence: float - match_reasons: List[str] - metadata: Dict[str, Any] - - -class AgentRanker: - """Ranks agents based on their suitability for specific tasks""" - - def __init__(self): - # Scoring weights for different factors - self.weights = { - "capability_match": 0.35, - "domain_match": 0.25, - "keyword_match": 0.20, - "performance": 0.10, - "availability": 0.05, - "load": 0.05 - } - - def rank_agents(self, agents: List[Dict[str, Any]], task_analysis: Any, - performance_data: Dict[str, Any] = None) -> List[AgentScore]: - """Rank agents based on task requirements""" - - agent_scores = [] - - for agent in agents: - score_result = self._score_agent(agent, task_analysis, performance_data) - agent_scores.append(score_result) - - # Sort by score (descending) - agent_scores.sort(key=lambda x: x.score, reverse=True) - - return agent_scores - - def _score_agent(self, agent: Dict[str, Any], task_analysis: Any, - performance_data: Dict[str, Any] = None) -> AgentScore: - """Calculate comprehensive score for a single agent""" - - agent_id = agent.get("agent_id", "unknown") - match_reasons = [] - - # Calculate individual scores - capability_score = self._score_capabilities(agent, task_analysis, match_reasons) - domain_score = self._score_domain(agent, task_analysis, match_reasons) - keyword_score = self._score_keywords(agent, task_analysis, match_reasons) - performance_score = self._score_performance(agent, performance_data) - availability_score = self._score_availability(agent) - load_score = self._score_load(agent) - - # Calculate weighted total score - total_score = ( - capability_score * self.weights["capability_match"] + - domain_score * self.weights["domain_match"] + - keyword_score * self.weights["keyword_match"] + - performance_score * self.weights["performance"] + - availability_score * self.weights["availability"] + - load_score * self.weights["load"] - ) - - # Calculate confidence based on available data quality - confidence = self._calculate_confidence(agent, task_analysis) - - return AgentScore( - agent_id=agent_id, - score=total_score, - confidence=confidence, - match_reasons=match_reasons, - metadata={ - "capability_score": capability_score, - "domain_score": domain_score, - "keyword_score": keyword_score, - "performance_score": performance_score, - "availability_score": availability_score, - "load_score": load_score - } - ) - - def _score_capabilities(self, agent: Dict[str, Any], task_analysis: Any, - match_reasons: List[str]) -> float: - """Score based on capability matching""" - agent_capabilities = set(agent.get("capabilities", [])) - required_capabilities = set(task_analysis.required_capabilities) - - if not required_capabilities: - return 0.7 # Neutral score when no specific requirements - - if not agent_capabilities: - return 0.3 # Low score for agents with no declared capabilities - - # Calculate overlap - matching_caps = agent_capabilities.intersection(required_capabilities) - match_ratio = len(matching_caps) / len(required_capabilities) - - if matching_caps: - match_reasons.append(f"Matching capabilities: {', '.join(matching_caps)}") - - # Bonus for having more relevant capabilities than required - if len(matching_caps) == len(required_capabilities): - extra_relevant = len(agent_capabilities) - len(required_capabilities) - bonus = min(0.2, extra_relevant * 0.05) - match_ratio += bonus - - return min(1.0, match_ratio) - - def _score_domain(self, agent: Dict[str, Any], task_analysis: Any, - match_reasons: List[str]) -> float: - """Score based on domain expertise""" - agent_domain = agent.get("domain", "").lower() - task_domain = task_analysis.domain.lower() - - if task_domain == "general": - return 0.7 # Neutral score for general tasks - - if not agent_domain or agent_domain == "general": - return 0.5 # Moderate score for general agents - - if agent_domain == task_domain: - match_reasons.append(f"Domain expertise: {task_domain}") - return 1.0 - - # Check for related domains - domain_similarity = self._calculate_domain_similarity(agent_domain, task_domain) - if domain_similarity > 0.5: - match_reasons.append(f"Related domain: {agent_domain}") - - return domain_similarity - - def _score_keywords(self, agent: Dict[str, Any], task_analysis: Any, - match_reasons: List[str]) -> float: - """Score based on keyword matching""" - agent_keywords = set(word.lower() for word in agent.get("keywords", [])) - agent_description = agent.get("description", "").lower() - task_keywords = set(word.lower() for word in task_analysis.keywords) - - if not task_keywords: - return 0.7 # Neutral score when no keywords - - # Direct keyword matches - direct_matches = agent_keywords.intersection(task_keywords) - - # Keywords found in description - description_matches = set() - for keyword in task_keywords: - if keyword in agent_description: - description_matches.add(keyword) - - all_matches = direct_matches.union(description_matches) - - if all_matches: - match_reasons.append(f"Keyword matches: {', '.join(all_matches)}") - - match_ratio = len(all_matches) / len(task_keywords) if task_keywords else 0 - return min(1.0, match_ratio) - - def _score_performance(self, agent: Dict[str, Any], - performance_data: Dict[str, Any] = None) -> float: - """Score based on historical performance""" - if not performance_data: - return 0.7 # Neutral score without performance data - - agent_id = agent.get("agent_id") - if agent_id not in performance_data: - return 0.7 - - perf = performance_data[agent_id] - - # Consider multiple performance metrics - success_rate = perf.get("success_rate", 0.7) - avg_response_time = perf.get("avg_response_time", 5.0) # seconds - reliability = perf.get("reliability", 0.7) - - # Normalize response time (lower is better) - time_score = max(0.0, 1.0 - (avg_response_time / 30.0)) # 30s max - - # Combine metrics - performance_score = (success_rate * 0.5 + time_score * 0.3 + reliability * 0.2) - - return min(1.0, performance_score) - - def _score_availability(self, agent: Dict[str, Any]) -> float: - """Score based on agent availability""" - status = agent.get("status", "unknown").lower() - last_seen_str = agent.get("last_seen") - - if status == "offline": - return 0.0 - elif status == "busy": - return 0.3 - elif status == "available" or status == "online": - return 1.0 - - # If no explicit status, check last seen - if last_seen_str: - try: - last_seen = datetime.fromisoformat(last_seen_str.replace('Z', '+00:00')) - time_diff = datetime.now() - last_seen.replace(tzinfo=None) - - if time_diff < timedelta(minutes=5): - return 1.0 - elif time_diff < timedelta(hours=1): - return 0.8 - elif time_diff < timedelta(days=1): - return 0.5 - else: - return 0.2 - except: - pass - - return 0.5 # Default for unknown availability - - def _score_load(self, agent: Dict[str, Any]) -> float: - """Score based on current agent load""" - current_load = agent.get("current_load", 0.5) # 0.0 to 1.0 - - # Lower load is better - return 1.0 - current_load - - def _calculate_domain_similarity(self, domain1: str, domain2: str) -> float: - """Calculate similarity between domains""" - related_domains = { - "technology": ["software", "it", "programming", "tech"], - "finance": ["banking", "trading", "accounting", "fintech"], - "healthcare": ["medical", "clinical", "pharmaceutical"], - "marketing": ["advertising", "sales", "promotion"], - "education": ["learning", "training", "academic"] - } - - for main_domain, related in related_domains.items(): - if domain1 in related and domain2 in related: - return 0.8 - elif (domain1 == main_domain and domain2 in related) or \ - (domain2 == main_domain and domain1 in related): - return 0.9 - - return 0.2 # Low similarity for unrelated domains - - def _calculate_confidence(self, agent: Dict[str, Any], task_analysis: Any) -> float: - """Calculate confidence in the scoring""" - confidence = 0.5 # Base confidence - - # Increase confidence based on available agent metadata - if agent.get("capabilities"): - confidence += 0.2 - if agent.get("description"): - confidence += 0.1 - if agent.get("domain"): - confidence += 0.1 - if agent.get("last_seen"): - confidence += 0.05 - if agent.get("status"): - confidence += 0.05 - - # Factor in task analysis confidence - confidence *= task_analysis.confidence - - return min(1.0, confidence) - - def get_top_recommendations(self, agent_scores: List[AgentScore], - limit: int = 5, min_score: float = 0.3) -> List[AgentScore]: - """Get top agent recommendations with filtering""" - - # Filter by minimum score and confidence - filtered = [ - score for score in agent_scores - if score.score >= min_score and score.confidence >= 0.4 - ] - - return filtered[:limit] - - def explain_ranking(self, agent_score: AgentScore) -> str: - """Generate human-readable explanation for agent ranking""" - explanations = [] - - explanations.append(f"Overall score: {agent_score.score:.2f} (confidence: {agent_score.confidence:.2f})") - - if agent_score.match_reasons: - explanations.append("Match reasons:") - for reason in agent_score.match_reasons: - explanations.append(f" - {reason}") - - # Add detailed score breakdown - metadata = agent_score.metadata - explanations.append("Score breakdown:") - explanations.append(f" - Capability match: {metadata.get('capability_score', 0):.2f}") - explanations.append(f" - Domain expertise: {metadata.get('domain_score', 0):.2f}") - explanations.append(f" - Keyword relevance: {metadata.get('keyword_score', 0):.2f}") - explanations.append(f" - Performance: {metadata.get('performance_score', 0):.2f}") - explanations.append(f" - Availability: {metadata.get('availability_score', 0):.2f}") - explanations.append(f" - Load: {metadata.get('load_score', 0):.2f}") - - return "\n".join(explanations) \ No newline at end of file diff --git a/nanda_core/discovery/task_analyzer.py b/nanda_core/discovery/task_analyzer.py deleted file mode 100644 index eb78098..0000000 --- a/nanda_core/discovery/task_analyzer.py +++ /dev/null @@ -1,289 +0,0 @@ -#!/usr/bin/env python3 -""" -Task Analyzer for understanding user intent and task requirements -""" - -import re -import os -from typing import Dict, List, Any, Optional -from dataclasses import dataclass -from anthropic import Anthropic - - -@dataclass -class TaskAnalysis: - """Result of task analysis""" - task_type: str - complexity: str # 'simple', 'medium', 'complex' - domain: str - keywords: List[str] - required_capabilities: List[str] - confidence: float - description: str - - -class TaskAnalyzer: - """Analyzes tasks to understand requirements and extract relevant features""" - - def __init__(self): - self.anthropic = Anthropic(api_key=os.getenv("ANTHROPIC_API_KEY", "")) - - # Predefined task patterns - self.task_patterns = { - "data_analysis": [ - r"analyz(e|ing|sis)", r"data", r"statistics", r"chart", r"graph", - r"report", r"dashboard", r"metrics", r"kpi" - ], - "web_scraping": [ - r"scrap(e|ing)", r"extract", r"crawl", r"fetch", r"website", - r"html", r"parse", r"web" - ], - "file_management": [ - r"file", r"folder", r"directory", r"organize", r"manage", - r"upload", r"download", r"storage" - ], - "communication": [ - r"email", r"message", r"send", r"notify", r"alert", - r"slack", r"discord", r"teams" - ], - "code_generation": [ - r"code", r"program", r"script", r"function", r"api", - r"development", r"software", r"programming" - ], - "research": [ - r"research", r"search", r"find", r"lookup", r"investigate", - r"study", r"explore", r"discover" - ], - "automation": [ - r"automat(e|ion)", r"workflow", r"process", r"schedule", - r"trigger", r"batch", r"recurring" - ] - } - - self.complexity_indicators = { - "simple": [ - r"simple", r"basic", r"quick", r"easy", r"straightforward" - ], - "complex": [ - r"complex", r"advanced", r"sophisticated", r"comprehensive", - r"detailed", r"multi-step", r"enterprise" - ] - } - - def analyze_task(self, task_description: str) -> TaskAnalysis: - """Analyze a task description and extract requirements""" - - # Clean and prepare text - text = task_description.lower().strip() - - # Extract task type - task_type = self._identify_task_type(text) - - # Determine complexity - complexity = self._assess_complexity(text) - - # Extract domain - domain = self._extract_domain(text) - - # Extract keywords - keywords = self._extract_keywords(text) - - # Identify required capabilities - capabilities = self._identify_capabilities(text, task_type) - - # Use Claude for enhanced analysis if available - enhanced_analysis = self._enhance_with_claude(task_description) - - if enhanced_analysis: - capabilities.extend(enhanced_analysis.get("capabilities", [])) - keywords.extend(enhanced_analysis.get("keywords", [])) - if enhanced_analysis.get("domain"): - domain = enhanced_analysis["domain"] - - # Remove duplicates - capabilities = list(set(capabilities)) - keywords = list(set(keywords)) - - # Calculate confidence score - confidence = self._calculate_confidence(text, task_type, complexity) - - return TaskAnalysis( - task_type=task_type, - complexity=complexity, - domain=domain, - keywords=keywords, - required_capabilities=capabilities, - confidence=confidence, - description=task_description - ) - - def _identify_task_type(self, text: str) -> str: - """Identify the primary task type""" - scores = {} - - for task_type, patterns in self.task_patterns.items(): - score = 0 - for pattern in patterns: - matches = len(re.findall(pattern, text)) - score += matches - scores[task_type] = score - - if not scores or max(scores.values()) == 0: - return "general" - - return max(scores, key=scores.get) - - def _assess_complexity(self, text: str) -> str: - """Assess task complexity based on indicators""" - simple_score = 0 - complex_score = 0 - - for pattern in self.complexity_indicators["simple"]: - simple_score += len(re.findall(pattern, text)) - - for pattern in self.complexity_indicators["complex"]: - complex_score += len(re.findall(pattern, text)) - - # Default to medium if no clear indicators - if simple_score > complex_score: - return "simple" - elif complex_score > simple_score: - return "complex" - else: - # Analyze length and structure as fallback - word_count = len(text.split()) - if word_count < 10: - return "simple" - elif word_count > 50: - return "complex" - else: - return "medium" - - def _extract_domain(self, text: str) -> str: - """Extract the domain/industry context""" - domain_keywords = { - "finance": [r"financial", r"banking", r"investment", r"trading", r"accounting"], - "healthcare": [r"medical", r"health", r"patient", r"hospital", r"clinical"], - "technology": [r"software", r"tech", r"programming", r"development", r"it"], - "marketing": [r"marketing", r"advertising", r"campaign", r"promotion", r"brand"], - "education": [r"education", r"learning", r"teaching", r"student", r"course"], - "ecommerce": [r"shop", r"store", r"product", r"order", r"payment", r"cart"], - "logistics": [r"shipping", r"delivery", r"transport", r"warehouse", r"supply"] - } - - for domain, patterns in domain_keywords.items(): - for pattern in patterns: - if re.search(pattern, text): - return domain - - return "general" - - def _extract_keywords(self, text: str) -> List[str]: - """Extract important keywords from the text""" - # Remove stop words and extract meaningful terms - stop_words = { - "the", "a", "an", "and", "or", "but", "in", "on", "at", "to", "for", - "of", "with", "by", "from", "up", "about", "into", "through", "during", - "before", "after", "above", "below", "between", "among", "under", "over", - "is", "are", "was", "were", "be", "been", "being", "have", "has", "had", - "do", "does", "did", "will", "would", "could", "should", "may", "might", - "can", "must", "shall", "i", "you", "he", "she", "it", "we", "they", - "me", "him", "her", "us", "them", "my", "your", "his", "its", "our", "their" - } - - words = re.findall(r'\b\w+\b', text) - keywords = [word for word in words if word not in stop_words and len(word) > 2] - - # Return top keywords by frequency - from collections import Counter - word_counts = Counter(keywords) - return [word for word, count in word_counts.most_common(10)] - - def _identify_capabilities(self, text: str, task_type: str) -> List[str]: - """Identify required agent capabilities""" - capabilities = [] - - # Base capabilities by task type - task_capabilities = { - "data_analysis": ["analytics", "visualization", "statistics", "reporting"], - "web_scraping": ["web_access", "html_parsing", "data_extraction"], - "file_management": ["file_operations", "storage_access", "organization"], - "communication": ["messaging", "notifications", "email"], - "code_generation": ["programming", "code_review", "debugging"], - "research": ["search", "information_gathering", "synthesis"], - "automation": ["workflow_management", "scheduling", "integration"] - } - - capabilities.extend(task_capabilities.get(task_type, [])) - - # Additional capability detection - capability_patterns = { - "api_integration": [r"api", r"integration", r"connect", r"webhook"], - "database": [r"database", r"sql", r"query", r"store", r"retrieve"], - "machine_learning": [r"ml", r"machine learning", r"ai", r"model", r"predict"], - "image_processing": [r"image", r"photo", r"picture", r"visual", r"ocr"], - "document_processing": [r"document", r"pdf", r"word", r"text", r"parse"], - "real_time": [r"real.?time", r"live", r"streaming", r"instant"], - "security": [r"secure", r"encrypt", r"auth", r"permission", r"access"] - } - - for capability, patterns in capability_patterns.items(): - for pattern in patterns: - if re.search(pattern, text): - capabilities.append(capability) - break - - return capabilities - - def _enhance_with_claude(self, task_description: str) -> Optional[Dict[str, Any]]: - """Use Claude to enhance task analysis""" - try: - if not self.anthropic.api_key: - return None - - prompt = f"""Analyze the following task description and extract: -1. Primary domain/industry -2. Required capabilities (technical skills, integrations, etc.) -3. Important keywords -4. Task complexity (simple/medium/complex) - -Task: {task_description} - -Respond with JSON format: -{{ - "domain": "domain_name", - "capabilities": ["capability1", "capability2"], - "keywords": ["keyword1", "keyword2"], - "complexity": "simple|medium|complex" -}}""" - - response = self.anthropic.messages.create( - model="claude-3-5-sonnet-20241022", - max_tokens=500, - messages=[{"role": "user", "content": prompt}] - ) - - import json - return json.loads(response.content[0].text) - - except Exception as e: - print(f"Error in Claude enhancement: {e}") - return None - - def _calculate_confidence(self, text: str, task_type: str, complexity: str) -> float: - """Calculate confidence score for the analysis""" - score = 0.5 # Base confidence - - # Increase confidence based on text length and clarity - word_count = len(text.split()) - if word_count >= 5: - score += 0.2 - if word_count >= 15: - score += 0.1 - - # Increase confidence if task type was clearly identified - if task_type != "general": - score += 0.2 - - # Normalize to 0-1 range - return min(1.0, score) \ No newline at end of file diff --git a/scripts/deploy-agent.sh b/scripts/deploy-agent.sh deleted file mode 100644 index 610fb4f..0000000 --- a/scripts/deploy-agent.sh +++ /dev/null @@ -1,200 +0,0 @@ -#!/bin/bash - -# Simple NANDA Agent Deployment Script -# Usage: bash deploy-agent.sh [PORT] [REGISTRY_URL] - -set -e - -AGENT_TYPE=$1 -AGENT_ID=$2 -ANTHROPIC_API_KEY=$3 -PORT=${4:-6000} -REGISTRY_URL=${5:-""} - -if [ -z "$AGENT_TYPE" ] || [ -z "$AGENT_ID" ] || [ -z "$ANTHROPIC_API_KEY" ]; then - echo "๐Ÿค– Simple NANDA Agent Deployment" - echo "=================================" - echo "" - echo "Usage: bash deploy-agent.sh [PORT] [REGISTRY_URL]" - echo "" - echo "Agent Types:" - echo " โ€ข helpful - General helpful agent" - echo " โ€ข pirate - Pirate personality agent" - echo " โ€ข echo - Simple echo agent" - echo " โ€ข analyst - LangChain document analyst (requires LangChain)" - echo "" - echo "Examples:" - echo " bash deploy-agent.sh helpful my_agent sk-ant-xxxxx" - echo " bash deploy-agent.sh analyst doc_analyzer sk-ant-xxxxx 6020" - echo " bash deploy-agent.sh pirate captain_jack sk-ant-xxxxx 6000 https://registry.example.com" - echo "" - exit 1 -fi - -echo "๐Ÿš€ Deploying NANDA Agent" -echo "========================" -echo "Agent Type: $AGENT_TYPE" -echo "Agent ID: $AGENT_ID" -echo "Port: $PORT" -echo "Registry: ${REGISTRY_URL:-"None (local only)"}" -echo "" - -echo "[1/6] Updating system and installing Python..." -# Works on both Ubuntu and Amazon Linux -if command -v apt &> /dev/null; then - # Ubuntu/Debian - sudo apt update -y - sudo apt install -y python3 python3-pip python3-venv git curl -else - # Amazon Linux/CentOS/RHEL - sudo dnf update -y - sudo dnf install -y python3.11 python3.11-pip git curl - # Create python3 symlink if needed - if ! command -v python3 &> /dev/null; then - sudo ln -s /usr/bin/python3.11 /usr/bin/python3 - fi -fi - -echo "[2/6] Setting up project directory..." -cd "$HOME" -PROJECT_DIR="nanda-agent-$AGENT_ID" - -# Remove existing directory if it exists -if [ -d "$PROJECT_DIR" ]; then - echo "Removing existing directory..." - rm -rf "$PROJECT_DIR" -fi - -# Clone streamlined adapter -echo "Cloning streamlined adapter..." -git clone https://github.com/projnanda/NEST.git "$PROJECT_DIR" -cd "$PROJECT_DIR" - -echo "[3/6] Creating Python virtual environment..." -python3 -m venv env -source env/bin/activate - -echo "[4/6] Installing Python dependencies..." -pip install --upgrade pip - -# Install core dependencies -pip install anthropic python-a2a requests - -# Install agent-specific dependencies -case $AGENT_TYPE in - "analyst") - echo "Installing LangChain dependencies for analyst agent..." - pip install langchain-core langchain-anthropic - ;; - "helpful"|"pirate"|"echo") - echo "Using built-in agent type (no extra dependencies)" - ;; - *) - echo "โš ๏ธ Unknown agent type: $AGENT_TYPE. Proceeding with basic installation..." - ;; -esac - -echo "[5/6] Creating agent startup script..." -cat > run_agent.py << EOF -#!/usr/bin/env python3 -""" -Auto-generated NANDA Agent -Agent Type: $AGENT_TYPE -Agent ID: $AGENT_ID -Port: $PORT -""" - -import os -import sys - -# Set API key -os.environ["ANTHROPIC_API_KEY"] = "$ANTHROPIC_API_KEY" - -# Add project to path -sys.path.append(os.path.dirname(__file__)) - -from nanda_core.core.adapter import NANDA, helpful_agent, pirate_agent, echo_agent - -def main(): - print("๐Ÿค– Starting NANDA Agent: $AGENT_ID") - print("Agent Type: $AGENT_TYPE") - print("Port: $PORT") - print("") - - # Select agent logic based on type - agent_logic = helpful_agent # default - - if "$AGENT_TYPE" == "pirate": - agent_logic = pirate_agent - elif "$AGENT_TYPE" == "echo": - agent_logic = echo_agent - elif "$AGENT_TYPE" == "analyst": - try: - from examples.langchain_analyst_agent import DocumentAnalyst, create_analyst_agent_logic - analyst = DocumentAnalyst() - agent_logic = create_analyst_agent_logic(analyst) - print("๐Ÿ“Š LangChain Document Analyst loaded") - except ImportError: - print("โš ๏ธ LangChain dependencies not available, using helpful agent") - agent_logic = helpful_agent - - # Create NANDA agent - nanda = NANDA( - agent_id="$AGENT_ID", - agent_logic=agent_logic, - port=$PORT, - registry_url="${REGISTRY_URL}" if "${REGISTRY_URL}" else None, - public_url="http://\$(curl -s ifconfig.me):$PORT" if "${REGISTRY_URL}" else None - ) - - print("๐Ÿš€ Agent ready! Send messages to http://localhost:$PORT/a2a") - if "${REGISTRY_URL}": - print("๐ŸŒ Will attempt to register with registry: ${REGISTRY_URL}") - - try: - nanda.start(register=bool("${REGISTRY_URL}")) - except KeyboardInterrupt: - print("\\n๐Ÿ›‘ Agent stopped") - -if __name__ == "__main__": - main() -EOF - -chmod +x run_agent.py - -echo "[6/6] Starting agent..." -echo "" -echo "๐ŸŽ‰ NANDA Agent Deployment Complete!" -echo "====================================" -echo "Agent ID: $AGENT_ID" -echo "Type: $AGENT_TYPE" -echo "Port: $PORT" -echo "Directory: $HOME/$PROJECT_DIR" -echo "" -echo "๐Ÿš€ Starting agent in background..." - -# Start agent in background -nohup python3 run_agent.py > agent.log 2>&1 & -AGENT_PID=$! - -sleep 3 - -# Check if agent is running -if ps -p $AGENT_PID > /dev/null; then - echo "โœ… Agent started successfully (PID: $AGENT_PID)" - echo "" - echo "๐Ÿ“‹ Useful commands:" - echo " โ€ข View logs: tail -f $HOME/$PROJECT_DIR/agent.log" - echo " โ€ข Stop agent: kill $AGENT_PID" - echo " โ€ข Test agent: curl -X POST http://localhost:$PORT/a2a -H 'Content-Type: application/json' -d '{\"content\":{\"text\":\"hello\"}}'" - echo "" - echo "๐Ÿ”— Agent URL: http://\$(curl -s ifconfig.me):$PORT/a2a" - echo "" - echo "๐Ÿ“„ View logs:" - tail -10 agent.log -else - echo "โŒ Agent failed to start. Check logs:" - cat agent.log - exit 1 -fi - From 21829a533ab0c8b87766eba74e39e942d501e287 Mon Sep 17 00:00:00 2001 From: destroyersrt Date: Fri, 17 Oct 2025 12:58:41 +0530 Subject: [PATCH 2/3] Official A2A Support --- examples/nanda_agent.py | 275 ++++++------------- nanda_core/__init__.py | 4 +- nanda_core/core/__init__.py | 4 +- nanda_core/core/adapter.py | 123 +++++++-- nanda_core/core/adapter_copy.py | 101 +++++++ nanda_core/core/agent_bridge.py | 366 +++++++++++++------------ nanda_core/core/agent_bridge_copy.py | 252 +++++++++++++++++ nanda_core/core/registry_client.py | 223 +++++++++++---- nanda_core/protocols/AgentExecutor.py | 73 +++++ nanda_core/protocols/a2a/__init__.py | 3 + nanda_core/protocols/a2a/protocol.py | 234 ++++++++++++++++ nanda_core/protocols/base.py | 52 ++++ nanda_core/protocols/router.py | 99 +++++++ nanda_core/telemetry/health_monitor.py | 2 +- setup.py | 15 +- 15 files changed, 1359 insertions(+), 467 deletions(-) create mode 100644 nanda_core/core/adapter_copy.py create mode 100644 nanda_core/core/agent_bridge_copy.py create mode 100644 nanda_core/protocols/AgentExecutor.py create mode 100644 nanda_core/protocols/a2a/__init__.py create mode 100644 nanda_core/protocols/a2a/protocol.py create mode 100644 nanda_core/protocols/base.py create mode 100644 nanda_core/protocols/router.py diff --git a/examples/nanda_agent.py b/examples/nanda_agent.py index 847799f..e9cd833 100644 --- a/examples/nanda_agent.py +++ b/examples/nanda_agent.py @@ -1,72 +1,57 @@ +# examples/nanda_agent.py #!/usr/bin/env python3 """ -LLM-Powered Modular NANDA Agent +LLM-Powered NANDA Agent -This agent uses Anthropic Claude for intelligent responses based on configurable personality and expertise. -Simply update the AGENT_CONFIG section to create different agent personalities. +Configurable agent using Anthropic Claude for intelligent responses. +Customize via environment variables or the config dict. """ import os import sys -import time +import asyncio import uuid from datetime import datetime -from typing import Dict, List, Any -# Add the parent directory to the path to allow importing streamlined_adapter sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) from nanda_core.core.adapter import NANDA -# Try to import Anthropic - will fail gracefully if not available +from dotenv import load_dotenv +load_dotenv() + try: from anthropic import Anthropic ANTHROPIC_AVAILABLE = True except ImportError: ANTHROPIC_AVAILABLE = False - print("โš ๏ธ Warning: anthropic library not available. Install with: pip install anthropic") + print("โš ๏ธ anthropic library not available. Install with: pip install anthropic") # ============================================================================= -# AGENT CONFIGURATION - Customize this section for different agents +# CONFIGURATION # ============================================================================= -# Get configuration from environment variables or use defaults -def get_agent_config(): - """Load agent configuration from environment variables or use defaults""" +def get_config(): + """Load configuration from environment variables""" + base_id = os.getenv("AGENT_ID", "helpful-agent") + agent_id = f"{base_id}-{uuid.uuid4().hex[:6]}" if '-' not in base_id else base_id - # Generate agent_id with hex suffix for uniqueness - base_agent_id = os.getenv("AGENT_ID", "helpful-ubuntu-agent") - if not base_agent_id.endswith('-') and '-' not in base_agent_id.split('-')[-1]: - # Add 6-character hex suffix if not already present - hex_suffix = uuid.uuid4().hex[:6] - agent_id = f"{base_agent_id}-{hex_suffix}" - else: - agent_id = base_agent_id + capabilities = os.getenv("AGENT_CAPABILITIES", "general assistance,conversation") + capabilities_list = [cap.strip() for cap in capabilities.split(",")] - print(f"Generated agent_id: {agent_id}") - agent_name = os.getenv("AGENT_NAME", "Ubuntu Helper") + agent_name = os.getenv("AGENT_NAME", "Helper Agent") domain = os.getenv("AGENT_DOMAIN", "general assistance") - specialization = os.getenv("AGENT_SPECIALIZATION", "helpful and friendly AI assistant") - description = os.getenv("AGENT_DESCRIPTION", "I am a helpful AI assistant specializing in general tasks and Ubuntu system administration.") - capabilities = os.getenv("AGENT_CAPABILITIES", "general assistance,Ubuntu system administration,Python development,cloud deployment,agent-to-agent communication") - registry_url = os.getenv("REGISTRY_URL", None) - public_url = os.getenv("PUBLIC_URL", None) - - # Parse capabilities into a list - expertise_list = [cap.strip() for cap in capabilities.split(",")] + specialization = os.getenv("AGENT_SPECIALIZATION", "helpful AI assistant") + description = os.getenv("AGENT_DESCRIPTION", "I'm a helpful AI assistant.") - # Create dynamic system prompt based on configuration - system_prompt = f"""You are {agent_name}, a {specialization} working in the domain of {domain}. + system_prompt = f"""You are {agent_name}, a {specialization} in {domain}. {description} -You are part of the NANDA (Network of Autonomous Distributed Agents) system. You can communicate with other agents and help users with various tasks. +You are part of the NANDA agent network and can communicate with other agents using @agent-id syntax. -Your capabilities include: -{chr(10).join([f"- {cap}" for cap in expertise_list])} +Your capabilities: {', '.join(capabilities_list)} -Always be helpful, accurate, and concise in your responses. If you're unsure about something, say so honestly. You can also help with basic calculations, provide time information, and engage in casual conversation. - -When someone asks about yourself, mention that you're part of the NANDA agent network and can communicate with other agents using the @agent_name syntax.""" +Be helpful, accurate, and concise.""" return { "agent_id": agent_id, @@ -74,200 +59,98 @@ def get_agent_config(): "domain": domain, "specialization": specialization, "description": description, - "expertise": expertise_list, - "registry_url": registry_url, - "public_url": public_url, - "system_prompt": system_prompt, + "capabilities": capabilities_list, + "port": int(os.getenv("PORT", "6000")), + "registry_url": os.getenv("REGISTRY_URL"), + "public_url": os.getenv("PUBLIC_URL") or f"http://localhost:{os.getenv('PORT', '6000')}", "anthropic_api_key": os.getenv("ANTHROPIC_API_KEY"), - "model": "claude-3-haiku-20240307" # Fast and cost-effective model + "model": os.getenv("ANTHROPIC_MODEL", "claude-3-haiku-20240307"), + "system_prompt": system_prompt } -# Load configuration -AGENT_CONFIG = get_agent_config() - -# Port configuration - use environment variable or default to 6000 -PORT = int(os.getenv("PORT", "6000")) - # ============================================================================= -# LLM-POWERED AGENT LOGIC - Uses Anthropic Claude for intelligent responses +# AGENT LOGIC # ============================================================================= -def create_llm_agent_logic(config: Dict[str, Any]): - """ - Creates an LLM-powered agent logic function based on the provided configuration. - Uses Anthropic Claude for intelligent, context-aware responses. - """ +def create_agent_logic(config): + """Create agent logic function with LLM or fallback""" # Initialize Anthropic client - anthropic_client = None + client = None if ANTHROPIC_AVAILABLE and config.get("anthropic_api_key"): try: - anthropic_client = Anthropic(api_key=config["anthropic_api_key"]) - print(f"โœ… Anthropic Claude initialized for {config['agent_name']}") + client = Anthropic(api_key=config["anthropic_api_key"]) + print(f"โœ… Claude initialized ({config['model']})") except Exception as e: - print(f"โŒ Failed to initialize Anthropic: {e}") - anthropic_client = None - - # Prepare system prompt (already formatted in get_agent_config) - system_prompt = config["system_prompt"] + print(f"โŒ Claude initialization failed: {e}") - def llm_agent_logic(message: str, conversation_id: str) -> str: - """LLM-powered agent logic with fallback to basic responses""" + def agent_logic(message: str, conversation_id: str) -> str: + """Process message with Claude or fallback""" - # If LLM is available, use it for intelligent responses - if anthropic_client: + if client: try: - # Add current time context if time-related query - context_info = "" - if any(time_word in message.lower() for time_word in ['time', 'date', 'when']): - context_info = f"\n\nCurrent time: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" + # Add time context if relevant + context = "" + if any(word in message.lower() for word in ['time', 'date', 'when']): + context = f"\n\nCurrent time: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" - response = anthropic_client.messages.create( + response = client.messages.create( model=config["model"], max_tokens=500, - system=system_prompt + context_info, - messages=[ - { - "role": "user", - "content": message - } - ] + system=config["system_prompt"] + context, + messages=[{"role": "user", "content": message}] ) return response.content[0].text.strip() except Exception as e: - print(f"โŒ LLM Error: {e}") - # Fall back to basic response - return f"Sorry, I'm having trouble processing that right now. Error: {str(e)}" + return f"Error: {str(e)}" - # Fallback to basic responses if LLM not available + # Fallback responses + msg = message.lower().strip() + if any(greeting in msg for greeting in ['hello', 'hi', 'hey']): + return f"Hello! I'm {config['agent_name']}. Set ANTHROPIC_API_KEY for full capabilities." + elif 'time' in msg: + return f"Current time: {datetime.now().strftime('%H:%M:%S')}" else: - return _basic_fallback_response(message, config) - - return llm_agent_logic - -def _basic_fallback_response(message: str, config: Dict[str, Any]) -> str: - """Basic fallback responses when LLM is not available""" - msg = message.lower().strip() - - # Handle greetings - if any(greeting in msg for greeting in ['hello', 'hi', 'hey']): - return f"Hello! I'm {config['agent_name']}, but I need an Anthropic API key to provide intelligent responses. Please set ANTHROPIC_API_KEY environment variable." + return f"I'm {config['agent_name']}. Set ANTHROPIC_API_KEY to enable LLM responses." - # Handle time requests - elif 'time' in msg: - current_time = datetime.now().strftime("%H:%M:%S") - return f"The current time is {current_time}." - - # Handle basic calculations - elif any(op in message for op in ['+', '-', '*', '/', '=']): - try: - calculation = message.replace('x', '*').replace('X', '*').replace('=', '').strip() - result = eval(calculation) - return f"Calculation result: {calculation} = {result}" - except: - return "Sorry, I couldn't calculate that. Please check your expression." - - # Default fallback - else: - return f"I'm {config['agent_name']}, but I need an Anthropic API key to provide intelligent responses. Please set ANTHROPIC_API_KEY environment variable and restart me." + return agent_logic # ============================================================================= -# MAIN EXECUTION +# MAIN # ============================================================================= -def main(): - """Main function to start the LLM-powered modular agent""" - print(f"๐Ÿค– Starting {AGENT_CONFIG['agent_name']}") - print(f"๐Ÿ“ Specialization: {AGENT_CONFIG['specialization']}") - print(f"๐ŸŽฏ Domain: {AGENT_CONFIG['domain']}") - print(f"๐Ÿ› ๏ธ Capabilities: {', '.join(AGENT_CONFIG['expertise'])}") - if AGENT_CONFIG['registry_url']: - print(f"๐ŸŒ Registry: {AGENT_CONFIG['registry_url']}") +async def main(): + config = get_config() - # Check for Anthropic API key - if not AGENT_CONFIG.get("anthropic_api_key"): - print("โš ๏ธ Warning: ANTHROPIC_API_KEY not found in environment variables") - print(" The agent will use basic fallback responses only") - print(" Set ANTHROPIC_API_KEY to enable LLM capabilities") - else: - print(f"๐Ÿง  LLM Model: {AGENT_CONFIG['model']}") + print(f"๐Ÿค– {config['agent_name']} ({config['agent_id']})") + print(f"๐ŸŽฏ {config['domain']} - {config['specialization']}") + print(f"๐Ÿ”— {config['public_url']}/a2a") + if config['registry_url']: + print(f"๐ŸŒ Registry: {config['registry_url']}") - # Create the LLM-powered agent logic based on configuration - agent_logic = create_llm_agent_logic(AGENT_CONFIG) + agent_logic = create_agent_logic(config) - # Create and start the NANDA agent nanda = NANDA( - agent_id=AGENT_CONFIG["agent_id"], + agent_id=config["agent_id"], agent_logic=agent_logic, - port=PORT, - registry_url=AGENT_CONFIG["registry_url"], - public_url=AGENT_CONFIG["public_url"], - enable_telemetry=True + agent_name=config["agent_name"], + domain=config["domain"], + specialization=config["specialization"], + description=config["description"], + capabilities=config["capabilities"], + port=config["port"], + registry_url=config["registry_url"], + public_url=config["public_url"], + enable_telemetry=True, + protocols={"a2a": {"enabled": True}} ) - print(f"๐Ÿš€ Agent URL: http://localhost:{PORT}/a2a") - print("๐Ÿ’ก Try these messages:") - print(" - 'Hello there'") - print(" - 'Tell me about yourself'") - print(" - 'What time is it?'") - print(" - 'How can you help with Ubuntu?'") - print(" - 'Explain Python virtual environments'") - print(" - '5 + 3'") - print("\n๐Ÿ›‘ Press Ctrl+C to stop") - - # Start the agent - nanda.start() - -def create_custom_agent(agent_name, specialization, domain, expertise_list, port=6000, anthropic_api_key=None, registry_url=None): - """ - Helper function to quickly create a custom LLM-powered agent with different config - - Example usage: - create_custom_agent( - agent_name="Data Scientist", - specialization="analytical and precise AI assistant", - domain="data science", - expertise_list=["data analysis", "statistics", "machine learning", "Python"], - port=6001, - anthropic_api_key="sk-ant-xxxxx" - ) - """ - custom_config = AGENT_CONFIG.copy() - custom_config.update({ - "agent_id": agent_name.lower().replace(" ", "-"), - "agent_name": agent_name, - "specialization": specialization, - "domain": domain, - "expertise": expertise_list, - "registry_url": registry_url, - "anthropic_api_key": anthropic_api_key or os.getenv("ANTHROPIC_API_KEY"), - "system_prompt": f"""You are {agent_name}, a {specialization} working in the domain of {domain}. - -You are part of the NANDA (Network of Autonomous Distributed Agents) system. You can communicate with other agents and help users with various tasks. - -Your capabilities include: -{chr(10).join([f"- {expertise}" for expertise in expertise_list])} - -Always be helpful, accurate, and concise in your responses. If you're unsure about something, say so honestly. - -When someone asks about yourself, mention that you're part of the NANDA agent network and can communicate with other agents using the @agent_name syntax.""" - }) - - agent_logic = create_llm_agent_logic(custom_config) - - nanda = NANDA( - agent_id=custom_config["agent_id"], - agent_logic=agent_logic, - port=port, - registry_url=custom_config["registry_url"], - enable_telemetry=True - ) + print("\n๐Ÿ’ฌ Try: 'Hello', 'What time is it?', '@other-agent Hello!'") + print("๐Ÿ›‘ Press Ctrl+C to stop\n") - print(f"๐Ÿค– Starting custom LLM agent: {agent_name}") - print(f"๐Ÿš€ Agent URL: http://localhost:{port}/a2a") - nanda.start() + await nanda.start() if __name__ == "__main__": - main() + asyncio.run(main()) \ No newline at end of file diff --git a/nanda_core/__init__.py b/nanda_core/__init__.py index b6561ca..b287102 100644 --- a/nanda_core/__init__.py +++ b/nanda_core/__init__.py @@ -4,9 +4,9 @@ """ from .core.adapter import NANDA -from .core.agent_bridge import SimpleAgentBridge +from .core.agent_bridge import AgentBridge __all__ = [ "NANDA", - "SimpleAgentBridge" + "AgentBridge" ] \ No newline at end of file diff --git a/nanda_core/core/__init__.py b/nanda_core/core/__init__.py index f0d8570..cf59933 100644 --- a/nanda_core/core/__init__.py +++ b/nanda_core/core/__init__.py @@ -4,9 +4,9 @@ """ from .adapter import NANDA -from .agent_bridge import SimpleAgentBridge +from .agent_bridge import AgentBridge __all__ = [ "NANDA", - "SimpleAgentBridge" + "AgentBridge" ] \ No newline at end of file diff --git a/nanda_core/core/adapter.py b/nanda_core/core/adapter.py index b98892c..5b7089c 100644 --- a/nanda_core/core/adapter.py +++ b/nanda_core/core/adapter.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 """ Simple NANDA Adapter - Clean Agent-to-Agent Communication @@ -7,10 +6,13 @@ """ import os +import asyncio import requests from typing import Optional, Callable -from python_a2a import run_server -from .agent_bridge import SimpleAgentBridge +from .agent_bridge import AgentBridge +from .registry_client import RegistryClient +from ..protocols.router import ProtocolRouter +from ..protocols.a2a.protocol import A2AProtocol class NANDA: @@ -19,30 +21,48 @@ class NANDA: def __init__(self, agent_id: str, agent_logic: Callable[[str, str], str], + agent_name: Optional[str] = None, + domain: Optional[str] = None, + specialization: Optional[str] = None, + description: Optional[str] = None, + capabilities: Optional[list] = None, port: int = 6000, registry_url: Optional[str] = None, public_url: Optional[str] = None, host: str = "0.0.0.0", - enable_telemetry: bool = True): + enable_telemetry: bool = True, + protocols: Optional[dict] = None): """ Create a simple NANDA agent Args: agent_id: Unique agent identifier agent_logic: Function that takes (message: str, conversation_id: str) -> response: str + agent_name: Display name (defaults to agent_id) + domain: Agent's domain of expertise + specialization: Agent's specialization + description: Agent description + capabilities: List of capabilities (default: ["text"]) port: Port to run on registry_url: Optional registry URL for agent discovery public_url: Public URL for agent registration (e.g., https://yourdomain.com:6000) host: Host to bind to - enable_telemetry: Enable telemetry logging (optional) + enable_telemetry: Enable telemetry logging + protocols: Dict of protocol configs (e.g., {"a2a": {"enabled": True}}) """ self.agent_id = agent_id self.agent_logic = agent_logic + self.agent_name = agent_name or agent_id + self.domain = domain + self.specialization = specialization + self.description = description or f"AI agent {agent_id}" + self.capabilities = capabilities or ["text"] self.port = port self.registry_url = registry_url - self.public_url = public_url + self.public_url = public_url or f"http://localhost:{port}" self.host = host self.enable_telemetry = enable_telemetry + self.protocols_config = protocols or {"a2a": {"enabled": True}} # Initialize telemetry if enabled self.telemetry = None @@ -54,46 +74,99 @@ def __init__(self, except ImportError: print(f"โš ๏ธ Telemetry requested but module not available") - # Create the bridge with optional features - self.bridge = SimpleAgentBridge( + # Initialize protocol router + self.router = ProtocolRouter() + + # Initialize and register protocols + self._initialize_protocols() + + # Initialize registry client + self.registry = RegistryClient(registry_url) if registry_url else RegistryClient(None) + + # Create the bridge + self.bridge = AgentBridge( + protocol_router=self.router, + registry_client=self.registry, agent_id=agent_id, agent_logic=agent_logic, - registry_url=registry_url, telemetry=self.telemetry ) print(f"๐Ÿค– NANDA Agent '{agent_id}' created") if registry_url: print(f"๐ŸŒ Registry: {registry_url}") - if public_url: - print(f"๐Ÿ”— Public URL: {public_url}") + print(f"๐Ÿ”— Public URL: {self.public_url}") + print(f"๐Ÿ”Œ Protocols: {self.router.get_all_protocols()}") + + def _initialize_protocols(self): + """Initialize and register protocol adapters based on config""" + + # Initialize A2A protocol if enabled + if self.protocols_config.get("a2a", {}).get("enabled", True): + a2a_protocol = A2AProtocol( + agent_id=self.agent_id, + agent_name=self.agent_name, + public_url=self.public_url, + domain=self.domain, + specialization=self.specialization, + description=self.description, + capabilities=self.capabilities + ) + self.router.register(a2a_protocol) + + # TODO: Add SLIM protocol when implemented + # if self.protocols_config.get("slim", {}).get("enabled"): + # slim_protocol = SLIMProtocol(...) + # self.router.register(slim_protocol) - def start(self, register: bool = True): + async def start(self, register: bool = True): """Start the agent server""" # Register with registry if provided - if register and self.registry_url and self.public_url: - self._register() + if register and self.registry_url: + await self._register() print(f"๐Ÿš€ Starting agent '{self.agent_id}' on {self.host}:{self.port}") - # Start the A2A server - run_server(self.bridge, host=self.host, port=self.port) + # Start the bridge (which starts all protocol servers) + await self.bridge.run_server(self.host, self.port) - def _register(self): - """Register agent with registry""" + async def _register(self): + """Register agent with NANDA Index""" try: - data = { + agent_facts = { "agent_id": self.agent_id, - "agent_url": self.public_url + "name": self.agent_name, + "domain": self.domain, + "specialization": self.specialization, + "description": self.description, + "capabilities": self.capabilities, + "url": self.public_url, + "agent_url": self.public_url, # For backward compatibility + "supported_protocols": self.router.get_all_protocols(), + "endpoints": self._get_endpoints() } - response = requests.post(f"{self.registry_url}/register", json=data, timeout=10) - if response.status_code == 200: - print(f"โœ… Agent '{self.agent_id}' registered successfully") - else: - print(f"โš ๏ธ Failed to register agent: HTTP {response.status_code}") + + await self.registry.register(agent_facts) + print(f"โœ… Agent '{self.agent_id}' registered successfully") + except Exception as e: print(f"โš ๏ธ Registration error: {e}") + def _get_endpoints(self) -> dict: + """Get endpoints for all registered protocols + + Returns: + Dict mapping protocol names to endpoint URLs + """ + endpoints = {} + for protocol_name in self.router.get_all_protocols(): + if protocol_name == "a2a": + endpoints["a2a"] = f"{self.public_url}/a2a" + elif protocol_name == "slim": + endpoints["slim"] = f"grpc://{self.public_url}:50051" + + return endpoints + def stop(self): """Stop the agent and cleanup telemetry""" if self.telemetry: diff --git a/nanda_core/core/adapter_copy.py b/nanda_core/core/adapter_copy.py new file mode 100644 index 0000000..b98892c --- /dev/null +++ b/nanda_core/core/adapter_copy.py @@ -0,0 +1,101 @@ +#!/usr/bin/env python3 +""" +Simple NANDA Adapter - Clean Agent-to-Agent Communication + +Simple, clean adapter focused on A2A communication without complexity. +8-10 lines to deploy an agent. +""" + +import os +import requests +from typing import Optional, Callable +from python_a2a import run_server +from .agent_bridge import SimpleAgentBridge + + +class NANDA: + """Simple NANDA class for clean agent deployment""" + + def __init__(self, + agent_id: str, + agent_logic: Callable[[str, str], str], + port: int = 6000, + registry_url: Optional[str] = None, + public_url: Optional[str] = None, + host: str = "0.0.0.0", + enable_telemetry: bool = True): + """ + Create a simple NANDA agent + + Args: + agent_id: Unique agent identifier + agent_logic: Function that takes (message: str, conversation_id: str) -> response: str + port: Port to run on + registry_url: Optional registry URL for agent discovery + public_url: Public URL for agent registration (e.g., https://yourdomain.com:6000) + host: Host to bind to + enable_telemetry: Enable telemetry logging (optional) + """ + self.agent_id = agent_id + self.agent_logic = agent_logic + self.port = port + self.registry_url = registry_url + self.public_url = public_url + self.host = host + self.enable_telemetry = enable_telemetry + + # Initialize telemetry if enabled + self.telemetry = None + if enable_telemetry: + try: + from ..telemetry.telemetry_system import TelemetrySystem + self.telemetry = TelemetrySystem(agent_id) + print(f"๐Ÿ“Š Telemetry enabled for {agent_id}") + except ImportError: + print(f"โš ๏ธ Telemetry requested but module not available") + + # Create the bridge with optional features + self.bridge = SimpleAgentBridge( + agent_id=agent_id, + agent_logic=agent_logic, + registry_url=registry_url, + telemetry=self.telemetry + ) + + print(f"๐Ÿค– NANDA Agent '{agent_id}' created") + if registry_url: + print(f"๐ŸŒ Registry: {registry_url}") + if public_url: + print(f"๐Ÿ”— Public URL: {public_url}") + + def start(self, register: bool = True): + """Start the agent server""" + # Register with registry if provided + if register and self.registry_url and self.public_url: + self._register() + + print(f"๐Ÿš€ Starting agent '{self.agent_id}' on {self.host}:{self.port}") + + # Start the A2A server + run_server(self.bridge, host=self.host, port=self.port) + + def _register(self): + """Register agent with registry""" + try: + data = { + "agent_id": self.agent_id, + "agent_url": self.public_url + } + response = requests.post(f"{self.registry_url}/register", json=data, timeout=10) + if response.status_code == 200: + print(f"โœ… Agent '{self.agent_id}' registered successfully") + else: + print(f"โš ๏ธ Failed to register agent: HTTP {response.status_code}") + except Exception as e: + print(f"โš ๏ธ Registration error: {e}") + + def stop(self): + """Stop the agent and cleanup telemetry""" + if self.telemetry: + self.telemetry.stop() + print(f"๐Ÿ›‘ Stopping agent '{self.agent_id}'") \ No newline at end of file diff --git a/nanda_core/core/agent_bridge.py b/nanda_core/core/agent_bridge.py index fc1a668..028d9fa 100644 --- a/nanda_core/core/agent_bridge.py +++ b/nanda_core/core/agent_bridge.py @@ -1,145 +1,154 @@ -#!/usr/bin/env python3 """ -Simple Agent Bridge for A2A Communication +Agent Bridge for Protocol-Agnostic Communication -Clean, simple bridge focused on agent-to-agent communication. +Handles message routing between agents using any registered protocol. """ -import os +import re import uuid import logging -import requests from typing import Callable, Optional, Dict, Any -from python_a2a import A2AServer, A2AClient, Message, TextContent, MessageRole, Metadata +from ..protocols.router import ProtocolRouter +from .registry_client import RegistryClient -# Configure logger to capture conversation logs logger = logging.getLogger(__name__) -class SimpleAgentBridge(A2AServer): - """Simple Agent Bridge for A2A communication only""" +class AgentBridge: + """Protocol-agnostic agent message router and coordinator""" def __init__(self, - agent_id: str, + protocol_router: ProtocolRouter, + registry_client: RegistryClient, + agent_id: str, agent_logic: Callable[[str, str], str], - registry_url: Optional[str] = None, - telemetry = None): - super().__init__() + telemetry=None): + """Initialize agent bridge + + Args: + protocol_router: Router managing protocol adapters + registry_client: Client for NANDA Index + agent_id: This agent's unique identifier + agent_logic: Agent's business logic function(message: str, conversation_id: str) -> str + telemetry: Optional telemetry system + """ + self.router = protocol_router + self.registry = registry_client self.agent_id = agent_id self.agent_logic = agent_logic - self.registry_url = registry_url self.telemetry = telemetry - def handle_message(self, msg: Message) -> Message: - """Handle incoming messages""" - conversation_id = msg.conversation_id or str(uuid.uuid4()) + # Register this bridge as incoming handler for all protocols + for protocol_name in self.router.get_all_protocols(): + protocol = self.router.get_protocol(protocol_name) + protocol.set_incoming_handler(self.handle_message) - # Only handle text content - if not isinstance(msg.content, TextContent): - return self._create_response( - msg, conversation_id, - "Only text messages supported" - ) + logger.info(f"๐ŸŒ‰ Bridge initialized for {agent_id} with protocols: {self.router.get_all_protocols()}") + + def extract_agent_id(self, content: str) -> Optional[str]: + """Extract @agent-id from message content""" + match = re.search(r'@([\w-]+)', content) + return match.group(1) if match else None + + def parse_incoming_agent_message(self, text: str) -> Optional[Dict[str, str]]: + """Parse incoming agent-to-agent message in format: + FROM: sender\nTO: receiver\nMESSAGE: content + """ + if not (text.startswith("FROM:") and "TO:" in text and "MESSAGE:" in text): + return None + + try: + lines = text.strip().split('\n') + result = {} + + for line in lines: + if line.startswith("FROM:"): + result['from_agent'] = line[5:].strip() + elif line.startswith("TO:"): + result['to_agent'] = line[3:].strip() + elif line.startswith("MESSAGE:"): + result['message'] = line[8:].strip() + + return result if all(k in result for k in ['from_agent', 'to_agent', 'message']) else None + except Exception as e: + logger.error(f"Error parsing agent message: {e}") + return None + + async def handle_message(self, message: Dict[str, Any]) -> Dict[str, Any]: + """Main message handling entry point - user_text = msg.content.text + Called by protocol adapters when messages arrive. + """ + content = message.get("content", {}).get("text", "") + conversation_id = message.get("conversation_id", "") or str(uuid.uuid4()) - # Check if this is an agent-to-agent message in our simple format - if user_text.startswith("FROM:") and "TO:" in user_text and "MESSAGE:" in user_text: - return self._handle_incoming_agent_message(user_text, msg, conversation_id) + # Check if this is an incoming agent-to-agent message + parsed = self.parse_incoming_agent_message(content) + if parsed: + return await self._handle_incoming_agent_message(parsed, conversation_id) - logger.info(f"๐Ÿ“จ [{self.agent_id}] Received: {user_text}") + logger.info(f"๐Ÿ“จ [{self.agent_id}] Received: {content}") - # Handle different message types try: - if user_text.startswith("@"): - # Agent-to-agent message (outgoing) - return self._handle_agent_message(user_text, msg, conversation_id) - elif user_text.startswith("/"): + # Check message type + if content.startswith("@"): + # Outgoing agent-to-agent message + return await self._handle_outgoing_agent_message(content, conversation_id) + elif content.startswith("/"): # System command - return self._handle_command(user_text, msg, conversation_id) + return await self._handle_command(content, conversation_id) else: # Regular message - use agent logic if self.telemetry: self.telemetry.log_message_received(self.agent_id, conversation_id) - response = self.agent_logic(user_text, conversation_id) - return self._create_response(msg, conversation_id, response) + response = self.agent_logic(content, conversation_id) + return self._create_response(response) except Exception as e: - return self._create_response( - msg, conversation_id, - f"Error: {str(e)}" - ) + logger.error(f"โŒ [{self.agent_id}] Error handling message: {e}") + return self._create_response(f"Error: {str(e)}") - def _handle_incoming_agent_message(self, user_text: str, msg: Message, conversation_id: str) -> Message: + async def _handle_incoming_agent_message(self, parsed: Dict[str, str], + conversation_id: str) -> Dict[str, Any]: """Handle incoming messages from other agents""" - try: - lines = user_text.strip().split('\n') - from_agent = "" - to_agent = "" - message_content = "" - - for line in lines: - if line.startswith("FROM:"): - from_agent = line[5:].strip() - elif line.startswith("TO:"): - to_agent = line[3:].strip() - elif line.startswith("MESSAGE:"): - message_content = line[8:].strip() - - logger.info(f"๐Ÿ“จ [{self.agent_id}] โ† [{from_agent}]: {message_content}") - - # Check if this is a reply (don't respond to replies to avoid infinite loops) - if message_content.startswith("Response to "): - logger.info(f"๐Ÿ”„ [{self.agent_id}] Received reply from {from_agent}, displaying to user") - # Display the reply to user but don't respond back to avoid loops - return self._create_response( - msg, conversation_id, - f"[{from_agent}] {message_content[len('Response to ' + self.agent_id + ': '):]}" - ) - - # Process the message through our agent logic - if self.telemetry: - self.telemetry.log_message_received(self.agent_id, conversation_id) - - response = self.agent_logic(message_content, conversation_id) - - # Send response back - return self._create_response( - msg, conversation_id, - f"Response to {from_agent}: {response}" - ) - - except Exception as e: - logger.error(f"โŒ [{self.agent_id}] Error processing incoming agent message: {e}") - return self._create_response( - msg, conversation_id, - f"Error processing message from agent: {str(e)}" - ) - - def _handle_agent_message(self, user_text: str, msg: Message, conversation_id: str) -> Message: + from_agent = parsed['from_agent'] + message_content = parsed['message'] + + logger.info(f"๐Ÿ“จ [{self.agent_id}] โ† [{from_agent}]: {message_content}") + + # Check if this is a reply (avoid infinite loops) + if message_content.startswith("Response to "): + logger.info(f"๐Ÿ”„ [{self.agent_id}] Received reply from {from_agent}") + return self._create_response(f"[{from_agent}] {message_content[len(f'Response to {self.agent_id}: '):]}") + + # Process through agent logic + if self.telemetry: + self.telemetry.log_message_received(self.agent_id, conversation_id) + + response = self.agent_logic(message_content, conversation_id) + return self._create_response(f"Response to {from_agent}: {response}") + + async def _handle_outgoing_agent_message(self, content: str, + conversation_id: str) -> Dict[str, Any]: """Handle messages to other agents (@agent_id message)""" - parts = user_text.split(" ", 1) + parts = content.split(" ", 1) if len(parts) <= 1: - return self._create_response( - msg, conversation_id, - "Invalid format. Use '@agent_id message'" - ) + return self._create_response("Invalid format. Use '@agent_id message'") - target_agent = parts[0][1:] # Remove @ + target_agent_id = parts[0][1:] # Remove @ message_text = parts[1] - logger.info(f"๐Ÿ”„ [{self.agent_id}] Sending to {target_agent}: {message_text}") + logger.info(f"๐Ÿ”„ [{self.agent_id}] Sending to {target_agent_id}: {message_text}") - # Look up target agent and send message - result = self._send_to_agent(target_agent, message_text, conversation_id) - return self._create_response(msg, conversation_id, result) + # Route to target agent + result = await self.route_to_agent(target_agent_id, message_text, conversation_id) + return result - def _handle_command(self, user_text: str, msg: Message, conversation_id: str) -> Message: + async def _handle_command(self, content: str, conversation_id: str) -> Dict[str, Any]: """Handle system commands""" - parts = user_text.split(" ", 1) + parts = content.split(" ", 1) command = parts[0][1:] if len(parts) > 0 else "" - args = parts[1] if len(parts) > 1 else "" if command == "help": help_text = """Available commands: @@ -147,106 +156,109 @@ def _handle_command(self, user_text: str, msg: Message, conversation_id: str) -> /ping - Test agent responsiveness /status - Show agent status @agent_id message - Send message to another agent""" - return self._create_response(msg, conversation_id, help_text) + return self._create_response(help_text) elif command == "ping": - return self._create_response(msg, conversation_id, "Pong!") + return self._create_response("Pong!") elif command == "status": - status = f"Agent: {self.agent_id}, Status: Running" - if self.registry_url: - status += f", Registry: {self.registry_url}" - return self._create_response(msg, conversation_id, status) + protocols = self.router.get_all_protocols() + status = f"Agent: {self.agent_id}, Status: Running, Protocols: {', '.join(protocols)}" + if hasattr(self.registry, 'registry_url') and self.registry.registry_url: + status += f", Registry: {self.registry.registry_url}" + return self._create_response(status) else: return self._create_response( - msg, conversation_id, f"Unknown command: {command}. Use /help for available commands" ) - def _send_to_agent(self, target_agent_id: str, message_text: str, conversation_id: str) -> str: - """Send message to another agent""" + async def route_to_agent(self, target_agent_id: str, message_text: str, + conversation_id: str) -> Dict[str, Any]: + """Route message to target agent via appropriate protocol""" try: - # Look up agent URL - agent_url = self._lookup_agent(target_agent_id) - if not agent_url: - return f"Agent {target_agent_id} not found" + # Resolve agent via NANDA Index + agent_info = await self.registry.resolve(target_agent_id) - # Ensure URL has /a2a endpoint - if not agent_url.endswith('/a2a'): - agent_url = f"{agent_url}/a2a" + if not agent_info: + logger.warning(f"๐Ÿ” Agent {target_agent_id} not found in registry") + return self._create_response(f"Agent {target_agent_id} not found") - logger.info(f"๐Ÿ“ค [{self.agent_id}] โ†’ [{target_agent_id}]: {message_text}") + # Debug: print what we got from registry + logger.info(f"๐Ÿ“‹ Agent info from registry: {agent_info}") - # Create simple message with metadata - simple_message = f"FROM: {self.agent_id}\nTO: {target_agent_id}\nMESSAGE: {message_text}" + # Select protocol + supported_protocols = agent_info.get("supported_protocols", ["a2a"]) + protocol_name = self.router.select_protocol(supported_protocols) + + # Get target URL - try multiple fields + endpoints = agent_info.get("endpoints", {}) + target_url = endpoints.get(protocol_name) - # Send message using A2A client - client = A2AClient(agent_url, timeout=30) - response = client.send_message( - Message( - role=MessageRole.USER, - content=TextContent(text=simple_message), - conversation_id=conversation_id, - metadata=Metadata(custom_fields={ - 'from_agent_id': self.agent_id, - 'to_agent_id': target_agent_id, - 'message_type': 'agent_to_agent' - }) + if not target_url: + # Try different URL fields + target_url = ( + agent_info.get("url") or + agent_info.get("agent_url") or + agent_info.get("public_url") ) - ) + + # Ensure target_url is valid + if not target_url: + logger.error(f"โŒ No URL found for {target_agent_id}. Agent info: {agent_info}") + return self._create_response(f"No endpoint found for {target_agent_id}") + + # Add /a2a suffix if needed and not already present + if protocol_name == "a2a" and not target_url.endswith('/a2a'): + target_url = f"{target_url}/a2a" + + logger.info(f"๐Ÿ“ค [{self.agent_id}] โ†’ [{target_agent_id}] via {protocol_name}: {message_text}") + logger.info(f"๐Ÿ”— Target URL: {target_url}") + + # Create message in simple format + simple_message = f"FROM: {self.agent_id}\nTO: {target_agent_id}\nMESSAGE: {message_text}" + + # Send via protocol + message = { + "content": { + "text": simple_message, + "type": "text" + }, + "conversation_id": conversation_id, + "metadata": { + "from_agent_id": self.agent_id, + "to_agent_id": target_agent_id, + "message_type": "agent_to_agent" + } + } + + response = await self.router.send(protocol_name, target_url, message) if self.telemetry: self.telemetry.log_message_sent(target_agent_id, conversation_id) - # Extract the actual response content from the target agent - logger.info(f"๐Ÿ” [{self.agent_id}] Response type: {type(response)}, has parts: {hasattr(response, 'parts') if response else 'None'}") - if response: - if hasattr(response, 'parts') and response.parts: - response_text = response.parts[0].text - logger.info(f"โœ… [{self.agent_id}] Received response from {target_agent_id}: {response_text[:100]}...") - return f"[{target_agent_id}] {response_text}" - else: - logger.info(f"โœ… [{self.agent_id}] Response has no parts, full response: {str(response)[:200]}...") - return f"[{target_agent_id}] {str(response)}" - else: - logger.info(f"โœ… [{self.agent_id}] Message delivered to {target_agent_id}, no response") - return f"Message sent to {target_agent_id}: {message_text}" + # Extract response + response_text = response.get("content", {}).get("text", str(response)) + logger.info(f"โœ… [{self.agent_id}] Response from {target_agent_id}: {response_text[:100]}...") + + return self._create_response(f"[{target_agent_id}] {response_text}") except Exception as e: - return f"โŒ Error sending to {target_agent_id}: {str(e)}" + logger.error(f"โŒ Error routing to {target_agent_id}: {e}") + import traceback + traceback.print_exc() + return self._create_response(f"โŒ Error sending to {target_agent_id}: {str(e)}") - def _lookup_agent(self, agent_id: str) -> Optional[str]: - """Look up agent URL in registry or use local discovery""" - - # Try registry lookup if available - if self.registry_url: - try: - response = requests.get(f"{self.registry_url}/lookup/{agent_id}", timeout=10) - if response.status_code == 200: - data = response.json() - agent_url = data.get("agent_url") - logger.info(f"๐ŸŒ Found {agent_id} in registry: {agent_url}") - return agent_url - except Exception as e: - logger.warning(f"๐ŸŒ Registry lookup failed: {e}") - - # Fallback to local discovery (for testing) - local_agents = { - "test_agent": "http://localhost:6000", + def _create_response(self, text: str) -> Dict[str, Any]: + """Create a standardized response dict""" + return { + "content": { + "text": f"[{self.agent_id}] {text}", + "type": "text" + } } - - if agent_id in local_agents: - logger.info(f"๐Ÿ  Found {agent_id} locally: {local_agents[agent_id]}") - return local_agents[agent_id] - - return None - def _create_response(self, original_msg: Message, conversation_id: str, text: str) -> Message: - """Create a response message""" - return Message( - role=MessageRole.AGENT, - content=TextContent(text=f"[{self.agent_id}] {text}"), - parent_message_id=original_msg.message_id, - conversation_id=conversation_id - ) \ No newline at end of file + async def run_server(self, host: str = "0.0.0.0", port: int = 8000): + """Start all protocol servers""" + logger.info(f"๐Ÿš€ Starting agent bridge for {self.agent_id} on {host}:{port}") + await self.router.start_all_servers(host, port) \ No newline at end of file diff --git a/nanda_core/core/agent_bridge_copy.py b/nanda_core/core/agent_bridge_copy.py new file mode 100644 index 0000000..fc1a668 --- /dev/null +++ b/nanda_core/core/agent_bridge_copy.py @@ -0,0 +1,252 @@ +#!/usr/bin/env python3 +""" +Simple Agent Bridge for A2A Communication + +Clean, simple bridge focused on agent-to-agent communication. +""" + +import os +import uuid +import logging +import requests +from typing import Callable, Optional, Dict, Any +from python_a2a import A2AServer, A2AClient, Message, TextContent, MessageRole, Metadata + +# Configure logger to capture conversation logs +logger = logging.getLogger(__name__) + + +class SimpleAgentBridge(A2AServer): + """Simple Agent Bridge for A2A communication only""" + + def __init__(self, + agent_id: str, + agent_logic: Callable[[str, str], str], + registry_url: Optional[str] = None, + telemetry = None): + super().__init__() + self.agent_id = agent_id + self.agent_logic = agent_logic + self.registry_url = registry_url + self.telemetry = telemetry + + def handle_message(self, msg: Message) -> Message: + """Handle incoming messages""" + conversation_id = msg.conversation_id or str(uuid.uuid4()) + + # Only handle text content + if not isinstance(msg.content, TextContent): + return self._create_response( + msg, conversation_id, + "Only text messages supported" + ) + + user_text = msg.content.text + + # Check if this is an agent-to-agent message in our simple format + if user_text.startswith("FROM:") and "TO:" in user_text and "MESSAGE:" in user_text: + return self._handle_incoming_agent_message(user_text, msg, conversation_id) + + logger.info(f"๐Ÿ“จ [{self.agent_id}] Received: {user_text}") + + # Handle different message types + try: + if user_text.startswith("@"): + # Agent-to-agent message (outgoing) + return self._handle_agent_message(user_text, msg, conversation_id) + elif user_text.startswith("/"): + # System command + return self._handle_command(user_text, msg, conversation_id) + else: + # Regular message - use agent logic + if self.telemetry: + self.telemetry.log_message_received(self.agent_id, conversation_id) + + response = self.agent_logic(user_text, conversation_id) + return self._create_response(msg, conversation_id, response) + + except Exception as e: + return self._create_response( + msg, conversation_id, + f"Error: {str(e)}" + ) + + def _handle_incoming_agent_message(self, user_text: str, msg: Message, conversation_id: str) -> Message: + """Handle incoming messages from other agents""" + try: + lines = user_text.strip().split('\n') + from_agent = "" + to_agent = "" + message_content = "" + + for line in lines: + if line.startswith("FROM:"): + from_agent = line[5:].strip() + elif line.startswith("TO:"): + to_agent = line[3:].strip() + elif line.startswith("MESSAGE:"): + message_content = line[8:].strip() + + logger.info(f"๐Ÿ“จ [{self.agent_id}] โ† [{from_agent}]: {message_content}") + + # Check if this is a reply (don't respond to replies to avoid infinite loops) + if message_content.startswith("Response to "): + logger.info(f"๐Ÿ”„ [{self.agent_id}] Received reply from {from_agent}, displaying to user") + # Display the reply to user but don't respond back to avoid loops + return self._create_response( + msg, conversation_id, + f"[{from_agent}] {message_content[len('Response to ' + self.agent_id + ': '):]}" + ) + + # Process the message through our agent logic + if self.telemetry: + self.telemetry.log_message_received(self.agent_id, conversation_id) + + response = self.agent_logic(message_content, conversation_id) + + # Send response back + return self._create_response( + msg, conversation_id, + f"Response to {from_agent}: {response}" + ) + + except Exception as e: + logger.error(f"โŒ [{self.agent_id}] Error processing incoming agent message: {e}") + return self._create_response( + msg, conversation_id, + f"Error processing message from agent: {str(e)}" + ) + + def _handle_agent_message(self, user_text: str, msg: Message, conversation_id: str) -> Message: + """Handle messages to other agents (@agent_id message)""" + parts = user_text.split(" ", 1) + if len(parts) <= 1: + return self._create_response( + msg, conversation_id, + "Invalid format. Use '@agent_id message'" + ) + + target_agent = parts[0][1:] # Remove @ + message_text = parts[1] + + logger.info(f"๐Ÿ”„ [{self.agent_id}] Sending to {target_agent}: {message_text}") + + # Look up target agent and send message + result = self._send_to_agent(target_agent, message_text, conversation_id) + return self._create_response(msg, conversation_id, result) + + def _handle_command(self, user_text: str, msg: Message, conversation_id: str) -> Message: + """Handle system commands""" + parts = user_text.split(" ", 1) + command = parts[0][1:] if len(parts) > 0 else "" + args = parts[1] if len(parts) > 1 else "" + + if command == "help": + help_text = """Available commands: +/help - Show this help +/ping - Test agent responsiveness +/status - Show agent status +@agent_id message - Send message to another agent""" + return self._create_response(msg, conversation_id, help_text) + + elif command == "ping": + return self._create_response(msg, conversation_id, "Pong!") + + elif command == "status": + status = f"Agent: {self.agent_id}, Status: Running" + if self.registry_url: + status += f", Registry: {self.registry_url}" + return self._create_response(msg, conversation_id, status) + + else: + return self._create_response( + msg, conversation_id, + f"Unknown command: {command}. Use /help for available commands" + ) + + def _send_to_agent(self, target_agent_id: str, message_text: str, conversation_id: str) -> str: + """Send message to another agent""" + try: + # Look up agent URL + agent_url = self._lookup_agent(target_agent_id) + if not agent_url: + return f"Agent {target_agent_id} not found" + + # Ensure URL has /a2a endpoint + if not agent_url.endswith('/a2a'): + agent_url = f"{agent_url}/a2a" + + logger.info(f"๐Ÿ“ค [{self.agent_id}] โ†’ [{target_agent_id}]: {message_text}") + + # Create simple message with metadata + simple_message = f"FROM: {self.agent_id}\nTO: {target_agent_id}\nMESSAGE: {message_text}" + + # Send message using A2A client + client = A2AClient(agent_url, timeout=30) + response = client.send_message( + Message( + role=MessageRole.USER, + content=TextContent(text=simple_message), + conversation_id=conversation_id, + metadata=Metadata(custom_fields={ + 'from_agent_id': self.agent_id, + 'to_agent_id': target_agent_id, + 'message_type': 'agent_to_agent' + }) + ) + ) + + if self.telemetry: + self.telemetry.log_message_sent(target_agent_id, conversation_id) + + # Extract the actual response content from the target agent + logger.info(f"๐Ÿ” [{self.agent_id}] Response type: {type(response)}, has parts: {hasattr(response, 'parts') if response else 'None'}") + if response: + if hasattr(response, 'parts') and response.parts: + response_text = response.parts[0].text + logger.info(f"โœ… [{self.agent_id}] Received response from {target_agent_id}: {response_text[:100]}...") + return f"[{target_agent_id}] {response_text}" + else: + logger.info(f"โœ… [{self.agent_id}] Response has no parts, full response: {str(response)[:200]}...") + return f"[{target_agent_id}] {str(response)}" + else: + logger.info(f"โœ… [{self.agent_id}] Message delivered to {target_agent_id}, no response") + return f"Message sent to {target_agent_id}: {message_text}" + + except Exception as e: + return f"โŒ Error sending to {target_agent_id}: {str(e)}" + + def _lookup_agent(self, agent_id: str) -> Optional[str]: + """Look up agent URL in registry or use local discovery""" + + # Try registry lookup if available + if self.registry_url: + try: + response = requests.get(f"{self.registry_url}/lookup/{agent_id}", timeout=10) + if response.status_code == 200: + data = response.json() + agent_url = data.get("agent_url") + logger.info(f"๐ŸŒ Found {agent_id} in registry: {agent_url}") + return agent_url + except Exception as e: + logger.warning(f"๐ŸŒ Registry lookup failed: {e}") + + # Fallback to local discovery (for testing) + local_agents = { + "test_agent": "http://localhost:6000", + } + + if agent_id in local_agents: + logger.info(f"๐Ÿ  Found {agent_id} locally: {local_agents[agent_id]}") + return local_agents[agent_id] + + return None + + def _create_response(self, original_msg: Message, conversation_id: str, text: str) -> Message: + """Create a response message""" + return Message( + role=MessageRole.AGENT, + content=TextContent(text=f"[{self.agent_id}] {text}"), + parent_message_id=original_msg.message_id, + conversation_id=conversation_id + ) \ No newline at end of file diff --git a/nanda_core/core/registry_client.py b/nanda_core/core/registry_client.py index 84f83c4..ac12c79 100644 --- a/nanda_core/core/registry_client.py +++ b/nanda_core/core/registry_client.py @@ -1,23 +1,31 @@ -#!/usr/bin/env python3 """ -Registry Client for Nanda Index Registry Integration +Registry Client for NANDA Index Registry Integration Handles agent registration, discovery, and management """ -import requests +import httpx import json import os from typing import Optional, Dict, List, Any from datetime import datetime +import logging + +logger = logging.getLogger(__name__) class RegistryClient: - """Client for interacting with the Nanda index registry""" + """Client for interacting with the NANDA Index registry""" def __init__(self, registry_url: Optional[str] = None): + """Initialize registry client + + Args: + registry_url: URL of NANDA Index (e.g., http://registry.chat39.com:6900) + """ self.registry_url = registry_url or self._get_default_registry_url() - self.session = requests.Session() - self.session.verify = False # For development with self-signed certs + # Use async httpx client instead of requests + self.client = httpx.AsyncClient(timeout=30.0, verify=False) # verify=False for dev with self-signed certs + logger.info(f"Registry client initialized with URL: {self.registry_url}") def _get_default_registry_url(self) -> str: """Get default registry URL from configuration""" @@ -29,60 +37,126 @@ def _get_default_registry_url(self) -> str: pass return "https://registry.chat39.com" - def register_agent(self, agent_id: str, agent_url: str, api_url: Optional[str] = None, agent_facts_url: Optional[str] = None) -> bool: - """Register an agent with the registry""" + async def register(self, agent_facts: Dict[str, Any]) -> bool: + """Register an agent with the registry using AgentFacts + + Args: + agent_facts: Agent metadata dict with fields: + - agent_id: str + - name: str + - domain: str (optional) + - specialization: str (optional) + - description: str (optional) + - capabilities: list + - url: str + - agent_url: str (backward compatibility) + - supported_protocols: list + - endpoints: dict + + Returns: + True if registration successful, False otherwise + """ + if not self.registry_url: + logger.warning("No registry URL configured, skipping registration") + return False + try: - data = { - "agent_id": agent_id, - "agent_url": agent_url - } - if api_url: - data["api_url"] = api_url - if agent_facts_url: - data["agent_facts_url"] = agent_facts_url + # Support both new (register) and old (register_agent) endpoints + response = await self.client.post( + f"{self.registry_url}/register", + json=agent_facts + ) + + if response.status_code == 200: + logger.info(f"โœ… Agent {agent_facts.get('agent_id')} registered successfully") + return True + else: + logger.error(f"โŒ Registration failed: HTTP {response.status_code} - {response.text}") + return False + + except Exception as e: + logger.error(f"โŒ Error registering agent: {e}") + return False - response = self.session.post(f"{self.registry_url}/register", json=data) + async def register_agent(self, agent_id: str, agent_url: str, + api_url: Optional[str] = None, + agent_facts_url: Optional[str] = None) -> bool: + """Legacy registration method for backward compatibility""" + data = { + "agent_id": agent_id, + "agent_url": agent_url + } + if api_url: + data["api_url"] = api_url + if agent_facts_url: + data["agent_facts_url"] = agent_facts_url + + try: + response = await self.client.post(f"{self.registry_url}/register", json=data) return response.status_code == 200 except Exception as e: - print(f"Error registering agent: {e}") + logger.error(f"Error registering agent: {e}") return False - def lookup_agent(self, agent_id: str) -> Optional[Dict[str, Any]]: + async def resolve(self, agent_id: str) -> Optional[Dict[str, Any]]: + """Resolve/lookup an agent in the registry + + Args: + agent_id: Agent identifier to look up + + Returns: + Agent info dict or None if not found + """ + return await self.lookup_agent(agent_id) + + async def lookup_agent(self, agent_id: str) -> Optional[Dict[str, Any]]: """Look up an agent in the registry""" + if not self.registry_url: + logger.warning("No registry URL configured") + return None + try: - response = self.session.get(f"{self.registry_url}/lookup/{agent_id}") + response = await self.client.get(f"{self.registry_url}/lookup/{agent_id}") if response.status_code == 200: return response.json() + else: + logger.warning(f"Agent {agent_id} not found: HTTP {response.status_code}") return None except Exception as e: - print(f"Error looking up agent {agent_id}: {e}") + logger.error(f"Error looking up agent {agent_id}: {e}") return None - def list_agents(self) -> List[Dict[str, Any]]: + async def list_agents(self) -> List[Dict[str, Any]]: """List all registered agents""" + if not self.registry_url: + return [] + try: - response = self.session.get(f"{self.registry_url}/list") + response = await self.client.get(f"{self.registry_url}/list") if response.status_code == 200: return response.json() return [] except Exception as e: - print(f"Error listing agents: {e}") + logger.error(f"Error listing agents: {e}") return [] - def list_clients(self) -> List[Dict[str, Any]]: + async def list_clients(self) -> List[Dict[str, Any]]: """List all registered clients""" + if not self.registry_url: + return [] + try: - response = self.session.get(f"{self.registry_url}/clients") + response = await self.client.get(f"{self.registry_url}/clients") if response.status_code == 200: return response.json() - return self.list_agents() # Fallback to list endpoint + return await self.list_agents() # Fallback to list endpoint except Exception as e: - print(f"Error listing clients: {e}") + logger.error(f"Error listing clients: {e}") return [] - def get_agent_metadata(self, agent_id: str) -> Optional[Dict[str, Any]]: + async def get_agent_metadata(self, agent_id: str) -> Optional[Dict[str, Any]]: """Get detailed metadata for an agent""" - agent_info = self.lookup_agent(agent_id) + agent_info = await self.lookup_agent(agent_id) if not agent_info: return None @@ -90,16 +164,25 @@ def get_agent_metadata(self, agent_id: str) -> Optional[Dict[str, Any]]: metadata = { "agent_id": agent_id, "agent_url": agent_info.get("agent_url"), + "url": agent_info.get("url"), # New field "api_url": agent_info.get("api_url"), + "endpoints": agent_info.get("endpoints", {}), # New field + "supported_protocols": agent_info.get("supported_protocols", ["a2a"]), # New field "last_seen": agent_info.get("last_seen"), "capabilities": agent_info.get("capabilities", []), "description": agent_info.get("description", ""), - "tags": agent_info.get("tags", []) + "tags": agent_info.get("tags", []), + "domain": agent_info.get("domain"), # New field + "specialization": agent_info.get("specialization") # New field } return metadata - def search_agents(self, query: str = "", capabilities: List[str] = None, tags: List[str] = None) -> List[Dict[str, Any]]: + async def search_agents(self, query: str = "", capabilities: List[str] = None, + tags: List[str] = None) -> List[Dict[str, Any]]: """Search for agents based on criteria""" + if not self.registry_url: + return [] + try: params = {} if query: @@ -109,19 +192,20 @@ def search_agents(self, query: str = "", capabilities: List[str] = None, tags: L if tags: params["tags"] = ",".join(tags) - response = self.session.get(f"{self.registry_url}/search", params=params) + response = await self.client.get(f"{self.registry_url}/search", params=params) if response.status_code == 200: return response.json() # Fallback to client-side filtering - return self._filter_agents_locally(query, capabilities, tags) + return await self._filter_agents_locally(query, capabilities, tags) except Exception as e: - print(f"Error searching agents: {e}") - return self._filter_agents_locally(query, capabilities, tags) + logger.error(f"Error searching agents: {e}") + return await self._filter_agents_locally(query, capabilities, tags) - def _filter_agents_locally(self, query: str = "", capabilities: List[str] = None, tags: List[str] = None) -> List[Dict[str, Any]]: + async def _filter_agents_locally(self, query: str = "", capabilities: List[str] = None, + tags: List[str] = None) -> List[Dict[str, Any]]: """Fallback local filtering when server search is not available""" - all_agents = self.list_agents() + all_agents = await self.list_agents() filtered = [] for agent in all_agents: @@ -147,25 +231,32 @@ def _filter_agents_locally(self, query: str = "", capabilities: List[str] = None return filtered - def get_mcp_servers(self, registry_provider: Optional[str] = None) -> List[Dict[str, Any]]: + async def get_mcp_servers(self, registry_provider: Optional[str] = None) -> List[Dict[str, Any]]: """Get list of available MCP servers""" + if not self.registry_url: + return [] + try: params = {} if registry_provider: params["registry_provider"] = registry_provider - response = self.session.get(f"{self.registry_url}/mcp_servers", params=params) + response = await self.client.get(f"{self.registry_url}/mcp_servers", params=params) if response.status_code == 200: return response.json() return [] except Exception as e: - print(f"Error getting MCP servers: {e}") + logger.error(f"Error getting MCP servers: {e}") return [] - def get_mcp_server_config(self, registry_provider: str, qualified_name: str) -> Optional[Dict[str, Any]]: + async def get_mcp_server_config(self, registry_provider: str, + qualified_name: str) -> Optional[Dict[str, Any]]: """Get configuration for a specific MCP server""" + if not self.registry_url: + return None + try: - response = self.session.get(f"{self.registry_url}/get_mcp_registry", params={ + response = await self.client.get(f"{self.registry_url}/get_mcp_registry", params={ 'registry_provider': registry_provider, 'qualified_name': qualified_name }) @@ -182,11 +273,15 @@ def get_mcp_server_config(self, registry_provider: str, qualified_name: str) -> } return None except Exception as e: - print(f"Error getting MCP server config: {e}") + logger.error(f"Error getting MCP server config: {e}") return None - def update_agent_status(self, agent_id: str, status: str, metadata: Optional[Dict[str, Any]] = None) -> bool: + async def update_agent_status(self, agent_id: str, status: str, + metadata: Optional[Dict[str, Any]] = None) -> bool: """Update agent status and metadata""" + if not self.registry_url: + return False + try: data = { "agent_id": agent_id, @@ -196,36 +291,52 @@ def update_agent_status(self, agent_id: str, status: str, metadata: Optional[Dic if metadata: data.update(metadata) - response = self.session.put(f"{self.registry_url}/agents/{agent_id}/status", json=data) + response = await self.client.put( + f"{self.registry_url}/agents/{agent_id}/status", + json=data + ) return response.status_code == 200 except Exception as e: - print(f"Error updating agent status: {e}") + logger.error(f"Error updating agent status: {e}") return False - def unregister_agent(self, agent_id: str) -> bool: + async def unregister_agent(self, agent_id: str) -> bool: """Unregister an agent from the registry""" + if not self.registry_url: + return False + try: - response = self.session.delete(f"{self.registry_url}/agents/{agent_id}") + response = await self.client.delete(f"{self.registry_url}/agents/{agent_id}") return response.status_code == 200 except Exception as e: - print(f"Error unregistering agent: {e}") + logger.error(f"Error unregistering agent: {e}") return False - def health_check(self) -> bool: + async def health_check(self) -> bool: """Check if the registry is healthy""" + if not self.registry_url: + return False + try: - response = self.session.get(f"{self.registry_url}/health", timeout=5) + response = await self.client.get(f"{self.registry_url}/health", timeout=5) return response.status_code == 200 except Exception: return False - def get_registry_stats(self) -> Optional[Dict[str, Any]]: + async def get_registry_stats(self) -> Optional[Dict[str, Any]]: """Get registry statistics""" + if not self.registry_url: + return None + try: - response = self.session.get(f"{self.registry_url}/stats") + response = await self.client.get(f"{self.registry_url}/stats") if response.status_code == 200: return response.json() return None except Exception as e: - print(f"Error getting registry stats: {e}") - return None \ No newline at end of file + logger.error(f"Error getting registry stats: {e}") + return None + + async def close(self): + """Close the HTTP client connection""" + await self.client.aclose() \ No newline at end of file diff --git a/nanda_core/protocols/AgentExecutor.py b/nanda_core/protocols/AgentExecutor.py new file mode 100644 index 0000000..b5d5a53 --- /dev/null +++ b/nanda_core/protocols/AgentExecutor.py @@ -0,0 +1,73 @@ +from typing import Callable +from a2a.types import TextPart +from a2a.utils import new_agent_text_message +from a2a.server.agent_execution import AgentExecutor, RequestContext +from a2a.server.events import EventQueue + +class NANDAAgentExecutor(AgentExecutor): + """Agent executor that delegates to NANDA bridge handler""" + + def __init__(self, message_handler: Callable): + self.message_handler = message_handler + + async def execute(self, context: RequestContext, event_queue: EventQueue): + """Execute agent logic via bridge handler""" + try: + # Extract message text from context + message_text = "" + if context.message and context.message.parts: + for part in context.message.parts: + # Handle different part types + if hasattr(part, 'root'): + part_obj = part.root + else: + part_obj = part + + # Extract text based on part type + if hasattr(part_obj, 'text'): + message_text = part_obj.text + elif hasattr(part_obj, 'kind') and part_obj.kind == 'text': + message_text = part_obj.text if hasattr(part_obj, 'text') else str(part_obj) + else: + message_text = str(part_obj) + + if message_text: + break + + if not message_text: + message_text = "" + + print(f"๐Ÿ“จ Received message: {message_text[:100]}") + + # Convert to bridge format + bridge_message = { + "content": { + "text": message_text, + "type": "text" + }, + "conversation_id": context.task_id or context.context_id or "" + } + + # Call bridge handler + response = await self.message_handler(bridge_message) + + # Extract response text + response_text = response.get("content", {}).get("text", "") + + print(f"โœ… Sending response: {response_text}") + + # Send response via event queue + response_message = new_agent_text_message(response_text) + await event_queue.enqueue_event(response_message) + + except Exception as e: + print(f"โŒ Error in execute: {e}") + import traceback + traceback.print_exc() + # Send error message + error_message = new_agent_text_message(f"Error: {str(e)}") + event_queue.enqueue_event(error_message) + + async def cancel(self, context: RequestContext, event_queue: EventQueue): + """Handle task cancellation""" + event_queue.enqueue_event(new_agent_text_message("Task cancelled")) diff --git a/nanda_core/protocols/a2a/__init__.py b/nanda_core/protocols/a2a/__init__.py new file mode 100644 index 0000000..3f289c1 --- /dev/null +++ b/nanda_core/protocols/a2a/__init__.py @@ -0,0 +1,3 @@ +from .protocol import A2AProtocol + +__all__ = ['A2AProtocol'] \ No newline at end of file diff --git a/nanda_core/protocols/a2a/protocol.py b/nanda_core/protocols/a2a/protocol.py new file mode 100644 index 0000000..ec7aea9 --- /dev/null +++ b/nanda_core/protocols/a2a/protocol.py @@ -0,0 +1,234 @@ +""" +Official A2A SDK Protocol Adapter +""" + +from typing import Callable, Dict, Any +from a2a.client import A2AClient, ClientConfig, ClientFactory +from a2a.server.request_handlers import DefaultRequestHandler +from a2a.server.tasks import InMemoryTaskStore +from a2a.server.apps import A2AStarletteApplication +from a2a.types import ( + AgentCard, AgentSkill, AgentCapabilities, AgentInterface, + Message, TextPart, Part, Role, + SendMessageRequest, MessageSendParams, + TransportProtocol +) +import uuid +import httpx +import uvicorn +from ..base import AgentProtocol +from ..AgentExecutor import NANDAAgentExecutor + +class A2AProtocol(AgentProtocol): + """A2A SDK protocol adapter""" + + def __init__(self, agent_id: str, agent_name: str, public_url: str, + domain: str = None, specialization: str = None, + description: str = "", capabilities: list = None): + """Initialize A2A protocol adapter + + Args: + agent_id: Unique agent identifier + agent_name: Display name + public_url: Public URL where agent is accessible + domain: Agent's domain of expertise + specialization: Agent's specialization + description: Agent description + capabilities: List of capabilities (default: ["text"]) + """ + self.agent_id = agent_id + self.agent_name = agent_name + self.public_url = public_url + self.description = description + self.capabilities_list = capabilities or ["text"] + + # Create AgentCard + self.agent_card = AgentCard( + name=agent_name, + description=description, + version="1.0.0", + url=f"{public_url}/", + protocol_version="0.2.5", + skills=[ + AgentSkill( + id=agent_id, + name=agent_name, + description=description, + tags=[domain] if domain else [], + examples=[] + ) + ], + capabilities=AgentCapabilities( + streaming=True, + push_notifications=False + ), + defaultInputModes=["text"], + defaultOutputModes=["text"] + ) + + # Incoming message handler (set by bridge) + self.incoming_handler: Callable = None + + # Server components (initialized when handler is set) + self.agent_executor = None + self.request_handler = None + self.server_app = None + + # HTTP client for A2A client + self.httpx_client = httpx.AsyncClient() + + def set_incoming_handler(self, handler: Callable): + """Set callback for incoming messages + + Args: + handler: Async function(message: dict) -> dict + """ + self.incoming_handler = handler + + # Initialize server components + self.agent_executor = NANDAAgentExecutor(handler) + self.request_handler = DefaultRequestHandler( + agent_executor=self.agent_executor, + task_store=InMemoryTaskStore() + ) + self.server_app = A2AStarletteApplication( + agent_card=self.agent_card, + http_handler=self.request_handler + ) + + async def send_message(self, target_url: str, message: Dict[str, Any]) -> Dict[str, Any]: + """Send message using A2A client""" + try: + # Validate target_url + if not target_url: + raise ValueError("target_url is None or empty") + + # Extract message content + content_text = message.get("content", {}).get("text", "") + conversation_id = message.get("conversation_id", "") + metadata = message.get("metadata", {}) + + print(f"๐Ÿ”„ Sending to {target_url}: {content_text[:50]}...") + + # Create A2A message with proper structure + from a2a.types import TextPart, Part, Message, Role + + text_part = TextPart( + kind='text', + text=content_text + ) + part = Part(root=text_part) + + a2a_message = Message( + message_id=f"msg-{conversation_id}-{uuid.uuid4().hex[:8]}", + role=Role.user, + parts=[part], + metadata=metadata if metadata else None + ) + + # Get client - strip /a2a suffix if present + base_url = target_url.rstrip('/a2a') if target_url else target_url + + client = A2AClient( + httpx_client=self.httpx_client, + url=base_url # Just pass the base URL + ) + + # Create request + from a2a.types import SendMessageRequest, MessageSendParams + request = SendMessageRequest( + id=f"req-{uuid.uuid4().hex[:8]}", + params=MessageSendParams(message=a2a_message) + ) + + # Send message + response = await client.send_message(request) + print(f"๐Ÿ” Response attributes: {dir(response)}") + print(f"๐Ÿ” Response dict: {response.model_dump() if hasattr(response, 'model_dump') else response}") + + if hasattr(response, 'root'): + result = response.root + + # Check if it's an error response + if hasattr(result, 'error') and result.error: + error_msg = result.error.message if hasattr(result.error, 'message') else str(result.error) + return { + "content": { + "text": f"Error: {error_msg}", + "type": "text" + } + } + + # Check if it's a success response with result + if hasattr(result, 'result'): + msg = result.result + if hasattr(msg, 'parts') and msg.parts: + response_text = "" + for part in msg.parts: + part_obj = part.root if hasattr(part, 'root') else part + if hasattr(part_obj, 'text'): + response_text = part_obj.text + break + + return { + "content": { + "text": response_text, + "type": "text" + } + } + + return { + "content": { + "text": "No response received", + "type": "text" + } + } + + except Exception as e: + print(f"โŒ Error sending message: {e}") + import traceback + traceback.print_exc() + return { + "content": { + "text": f"Error: {str(e)}", + "type": "text" + } + } + + def get_metadata(self) -> Dict[str, Any]: + """Get AgentCard metadata + + Returns: + AgentCard as dict + """ + return { + "agent_id": self.agent_id, + "name": self.agent_name, + "url": self.agent_card.url, + "description": self.description, + "capabilities": self.capabilities_list + } + + async def start_server(self, host: str, port: int): + """Start A2A server + + Args: + host: Host to bind to + port: Port to listen on + """ + if not self.server_app: + raise RuntimeError("Server not initialized. Call set_incoming_handler first.") + + print(f"Starting A2A server on {host}:{port}") + + # Build Starlette app + app = self.server_app.build() + + # Run with uvicorn + config = uvicorn.Config(app, host=host, port=port, log_level="info") + server = uvicorn.Server(config) + await server.serve() + + def get_protocol_name(self) -> str: + """Return protocol identifier""" + return "a2a" \ No newline at end of file diff --git a/nanda_core/protocols/base.py b/nanda_core/protocols/base.py new file mode 100644 index 0000000..07ae232 --- /dev/null +++ b/nanda_core/protocols/base.py @@ -0,0 +1,52 @@ +from abc import ABC, abstractmethod +from typing import Callable, Dict, Any + +class AgentProtocol(ABC): + """Base protocol interface for agent communication""" + + @abstractmethod + async def send_message(self, target_url: str, message: Dict[str, Any]) -> Dict[str, Any]: + """Send message to target agent + + Args: + target_url: Target agent's endpoint URL + message: Message dict with 'content', 'conversation_id', etc. + + Returns: + Response dict from target agent + """ + pass + + @abstractmethod + def set_incoming_handler(self, handler: Callable): + """Set callback for incoming messages + + Args: + handler: Async function that processes incoming messages + Should accept message dict and return response dict + """ + pass + + @abstractmethod + def get_metadata(self) -> Dict[str, Any]: + """Get protocol-specific agent metadata (AgentCard, etc.) + + Returns: + Metadata dict for agent discovery + """ + pass + + @abstractmethod + async def start_server(self, host: str, port: int): + """Start protocol server + + Args: + host: Host to bind to + port: Port to listen on + """ + pass + + @abstractmethod + def get_protocol_name(self) -> str: + """Return protocol identifier (a2a, slim, etc.)""" + pass \ No newline at end of file diff --git a/nanda_core/protocols/router.py b/nanda_core/protocols/router.py new file mode 100644 index 0000000..24e16a7 --- /dev/null +++ b/nanda_core/protocols/router.py @@ -0,0 +1,99 @@ +from typing import Dict, List, Optional +from .base import AgentProtocol + +class ProtocolRouter: + """Manages multiple protocol adapters and routes messages""" + + def __init__(self): + self.protocols: Dict[str, AgentProtocol] = {} + self.default_protocol: Optional[str] = None + + def register(self, protocol: AgentProtocol): + """Register a protocol adapter + + Args: + protocol: Protocol adapter instance + """ + name = protocol.get_protocol_name() + self.protocols[name] = protocol + + # First registered protocol becomes default + if self.default_protocol is None: + self.default_protocol = name + + print(f"Registered protocol: {name}") + + def get_protocol(self, name: str) -> Optional[AgentProtocol]: + """Get protocol by name + + Args: + name: Protocol name (a2a, slim, etc.) + + Returns: + Protocol adapter or None + """ + return self.protocols.get(name) + + def get_all_protocols(self) -> List[str]: + """Get list of all registered protocol names""" + return list(self.protocols.keys()) + + async def send(self, protocol_name: str, target_url: str, message: dict) -> dict: + """Send message using specified protocol + + Args: + protocol_name: Protocol to use (a2a, slim) + target_url: Target agent endpoint + message: Message to send + + Returns: + Response from target agent + + Raises: + ValueError: If protocol not registered + """ + protocol = self.protocols.get(protocol_name) + if not protocol: + raise ValueError(f"Protocol '{protocol_name}' not registered. " + f"Available: {list(self.protocols.keys())}") + + return await protocol.send_message(target_url, message) + + def select_protocol(self, supported_protocols: List[str]) -> str: + """Select best available protocol from supported list + + Args: + supported_protocols: List of protocols target agent supports + + Returns: + Selected protocol name + """ + # Try to find first match from supported list + for proto in supported_protocols: + if proto in self.protocols: + return proto + + # Fallback to default if no match + if self.default_protocol: + return self.default_protocol + + # Last resort: use first registered protocol + if self.protocols: + return list(self.protocols.keys())[0] + + raise ValueError("No protocols registered") + + async def start_all_servers(self, host: str, port: int): + """Start all registered protocol servers + + Note: For protocols that need different ports, override in protocol adapter + + Args: + host: Host to bind to + port: Base port (protocols may use port+offset) + """ + for name, protocol in self.protocols.items(): + print(f"Starting {name} protocol server...") + # Each protocol handles its own server startup + # They can use different ports internally + await protocol.start_server(host, port) \ No newline at end of file diff --git a/nanda_core/telemetry/health_monitor.py b/nanda_core/telemetry/health_monitor.py index 47d561c..eac8630 100644 --- a/nanda_core/telemetry/health_monitor.py +++ b/nanda_core/telemetry/health_monitor.py @@ -345,7 +345,7 @@ def _get_registry_url(self) -> str: return f.read().strip() except: pass - return "https://chat.nanda-registry.com:6900" + return "https://registry.chat39.com:6900" def get_health_history(self, check_name: str, hours: int = 24) -> List[HealthCheck]: """Get health check history""" diff --git a/setup.py b/setup.py index c2afdd5..5f07af9 100644 --- a/setup.py +++ b/setup.py @@ -10,14 +10,13 @@ def read_requirements(): """Read requirements from file""" requirements = [ - "flask", - "anthropic", - "requests", - "python-a2a==0.5.6", - "mcp", - "python-dotenv", - "flask-cors", - "psutil" # For system monitoring + "anthropic>=0.18.0", + "a2a-sdk>=0.2.0", # Official A2A SDK (replaced python-a2a) + "httpx>=0.27.0", # For async HTTP (replaces requests) + "uvicorn>=0.30.0", # ASGI server for A2A + "starlette>=0.37.0", # For A2A server (included with a2a-sdk) + "python-dotenv>=1.0.0", + "psutil>=5.9.0", # For system monitoring ] return requirements From b21d6b10ad0d879b2a3fd04baa27ef5e61358bbd Mon Sep 17 00:00:00 2001 From: destroyersrt Date: Mon, 20 Oct 2025 12:41:15 +0530 Subject: [PATCH 3/3] feat: Added support for AGNTCY/SLIM Point-to-Point Communication --- .gitmodules | 3 + docker-compose.yml | 22 ++ examples/nanda_agent.py | 11 +- nanda_core/core/adapter.py | 30 ++- nanda_core/core/agent_bridge.py | 27 ++- nanda_core/core/registry_client.py | 11 +- nanda_core/protocols/router.py | 29 ++- nanda_core/protocols/slim/__init__.py | 3 + nanda_core/protocols/slim/adapter.py | 312 ++++++++++++++++++++++++++ setup.py | 14 +- slim | 1 + test_receiver.py | 30 +++ test_sender.py | 45 ++++ 13 files changed, 495 insertions(+), 43 deletions(-) create mode 100644 .gitmodules create mode 100644 docker-compose.yml create mode 100644 nanda_core/protocols/slim/__init__.py create mode 100644 nanda_core/protocols/slim/adapter.py create mode 160000 slim create mode 100644 test_receiver.py create mode 100644 test_sender.py diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..0ebd33d --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "slim"] + path = slim + url = https://github.com/agntcy/slim.git diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..c7c7495 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + slim-control-plane: + build: + context: https://github.com/agntcy/slim.git#main + dockerfile: control-plane/Dockerfile + ports: + - "8080:8080" + restart: unless-stopped + + slim-data-plane: + build: + context: https://github.com/agntcy/slim.git#main + dockerfile: data-plane/Dockerfile + ports: + - "50051:50051" + environment: + - CONTROL_PLANE_URL=http://slim-control-plane:8080 + restart: unless-stopped + depends_on: + - slim-control-plane \ No newline at end of file diff --git a/examples/nanda_agent.py b/examples/nanda_agent.py index e9cd833..dfa30c0 100644 --- a/examples/nanda_agent.py +++ b/examples/nanda_agent.py @@ -65,7 +65,14 @@ def get_config(): "public_url": os.getenv("PUBLIC_URL") or f"http://localhost:{os.getenv('PORT', '6000')}", "anthropic_api_key": os.getenv("ANTHROPIC_API_KEY"), "model": os.getenv("ANTHROPIC_MODEL", "claude-3-haiku-20240307"), - "system_prompt": system_prompt + "system_prompt": system_prompt, + "protocols": { + "a2a": {"enabled": True}, + "slim": { + "enabled": os.getenv("SLIM_ENABLED", "false").lower() == "true", + "node_url": os.getenv("SLIM_NODE_URL", "grpc://localhost:50051") + } + } } # ============================================================================= @@ -144,7 +151,7 @@ async def main(): registry_url=config["registry_url"], public_url=config["public_url"], enable_telemetry=True, - protocols={"a2a": {"enabled": True}} + protocols=config["protocols"] ) print("\n๐Ÿ’ฌ Try: 'Hello', 'What time is it?', '@other-agent Hello!'") diff --git a/nanda_core/core/adapter.py b/nanda_core/core/adapter.py index 5b7089c..151f5f2 100644 --- a/nanda_core/core/adapter.py +++ b/nanda_core/core/adapter.py @@ -8,7 +8,7 @@ import os import asyncio import requests -from typing import Optional, Callable +from typing import Optional, Callable, Dict from .agent_bridge import AgentBridge from .registry_client import RegistryClient from ..protocols.router import ProtocolRouter @@ -114,10 +114,19 @@ def _initialize_protocols(self): ) self.router.register(a2a_protocol) - # TODO: Add SLIM protocol when implemented - # if self.protocols_config.get("slim", {}).get("enabled"): - # slim_protocol = SLIMProtocol(...) - # self.router.register(slim_protocol) + slim_config = self.protocols_config.get("slim", {}) + if slim_config.get("enabled", False): + from ..protocols.slim.adapter import SLIMProtocol + + slim_node_url = slim_config.get("node_url", "grpc://localhost:50051") + + slim_protocol = SLIMProtocol( + agent_id=self.agent_id, + slim_node_url=slim_node_url, + agent_name=self.agent_name + ) + self.router.register(slim_protocol) + print(f"๐Ÿ”Œ SLIM protocol enabled (node: {slim_node_url})") async def start(self, register: bool = True): """Start the agent server""" @@ -152,18 +161,15 @@ async def _register(self): except Exception as e: print(f"โš ๏ธ Registration error: {e}") - def _get_endpoints(self) -> dict: - """Get endpoints for all registered protocols - - Returns: - Dict mapping protocol names to endpoint URLs - """ + def _get_endpoints(self) -> Dict[str, str]: + """Get endpoints for all registered protocols""" endpoints = {} for protocol_name in self.router.get_all_protocols(): if protocol_name == "a2a": endpoints["a2a"] = f"{self.public_url}/a2a" elif protocol_name == "slim": - endpoints["slim"] = f"grpc://{self.public_url}:50051" + # SLIM uses agent inbox channel, not HTTP endpoint + endpoints["slim"] = f"slim://{self.agent_id}" return endpoints diff --git a/nanda_core/core/agent_bridge.py b/nanda_core/core/agent_bridge.py index 028d9fa..a234408 100644 --- a/nanda_core/core/agent_bridge.py +++ b/nanda_core/core/agent_bridge.py @@ -183,12 +183,18 @@ async def route_to_agent(self, target_agent_id: str, message_text: str, if not agent_info: logger.warning(f"๐Ÿ” Agent {target_agent_id} not found in registry") return self._create_response(f"Agent {target_agent_id} not found") + protocol_name = "slim" # <-- Force SLIM + target_url = f"slim://{target_agent_id}" # <-- Force SLIM URL + logger.info(f"๐Ÿงช TEST MODE: Forcing SLIM protocol") + logger.info(f"๐Ÿ“ค [{self.agent_id}] โ†’ [{target_agent_id}] via {protocol_name}") + logger.info(f"๐Ÿ”— Target URL: {target_url}") + # Debug: print what we got from registry logger.info(f"๐Ÿ“‹ Agent info from registry: {agent_info}") # Select protocol - supported_protocols = agent_info.get("supported_protocols", ["a2a"]) + supported_protocols = agent_info.get("supported_protocols") or self.router.get_all_protocols() protocol_name = self.router.select_protocol(supported_protocols) # Get target URL - try multiple fields @@ -196,13 +202,18 @@ async def route_to_agent(self, target_agent_id: str, message_text: str, target_url = endpoints.get(protocol_name) if not target_url: - # Try different URL fields - target_url = ( - agent_info.get("url") or - agent_info.get("agent_url") or - agent_info.get("public_url") - ) - + # Fallback logic + if protocol_name == "slim": + # For SLIM, construct the identifier + target_url = f"slim://{target_agent_id}" + else: + # For A2A, use URL fields + target_url = ( + agent_info.get("url") or + agent_info.get("agent_url") or + agent_info.get("public_url") + ) + # Ensure target_url is valid if not target_url: logger.error(f"โŒ No URL found for {target_agent_id}. Agent info: {agent_info}") diff --git a/nanda_core/core/registry_client.py b/nanda_core/core/registry_client.py index ac12c79..0a7db31 100644 --- a/nanda_core/core/registry_client.py +++ b/nanda_core/core/registry_client.py @@ -164,17 +164,18 @@ async def get_agent_metadata(self, agent_id: str) -> Optional[Dict[str, Any]]: metadata = { "agent_id": agent_id, "agent_url": agent_info.get("agent_url"), - "url": agent_info.get("url"), # New field + "url": agent_info.get("url"), "api_url": agent_info.get("api_url"), - "endpoints": agent_info.get("endpoints", {}), # New field - "supported_protocols": agent_info.get("supported_protocols", ["a2a"]), # New field + "endpoints": agent_info.get("endpoints", {}), + "supported_protocols": agent_info.get("supported_protocols", ["a2a"]), "last_seen": agent_info.get("last_seen"), "capabilities": agent_info.get("capabilities", []), "description": agent_info.get("description", ""), "tags": agent_info.get("tags", []), - "domain": agent_info.get("domain"), # New field - "specialization": agent_info.get("specialization") # New field + "domain": agent_info.get("domain"), + "specialization": agent_info.get("specialization") } + return metadata async def search_agents(self, query: str = "", capabilities: List[str] = None, diff --git a/nanda_core/protocols/router.py b/nanda_core/protocols/router.py index 24e16a7..703155f 100644 --- a/nanda_core/protocols/router.py +++ b/nanda_core/protocols/router.py @@ -1,5 +1,6 @@ from typing import Dict, List, Optional from .base import AgentProtocol +import asyncio class ProtocolRouter: """Manages multiple protocol adapters and routes messages""" @@ -68,6 +69,8 @@ def select_protocol(self, supported_protocols: List[str]) -> str: Returns: Selected protocol name """ + if "slim" in supported_protocols and "slim" in self.protocols: + return "slim" # Try to find first match from supported list for proto in supported_protocols: if proto in self.protocols: @@ -84,16 +87,20 @@ def select_protocol(self, supported_protocols: List[str]) -> str: raise ValueError("No protocols registered") async def start_all_servers(self, host: str, port: int): - """Start all registered protocol servers + """Start all registered protocol servers concurrently""" + + async def start_protocol(name, protocol): + try: + print(f"๐Ÿš€ Starting {name} protocol...") + await protocol.start_server(host, port) + except Exception as e: + print(f"โŒ Error starting {name}: {e}") - Note: For protocols that need different ports, override in protocol adapter + # Start all protocols as background tasks + tasks = [ + asyncio.create_task(start_protocol(name, protocol)) + for name, protocol in self.protocols.items() + ] - Args: - host: Host to bind to - port: Base port (protocols may use port+offset) - """ - for name, protocol in self.protocols.items(): - print(f"Starting {name} protocol server...") - # Each protocol handles its own server startup - # They can use different ports internally - await protocol.start_server(host, port) \ No newline at end of file + # Wait for all to start (they run forever) + await asyncio.gather(*tasks) \ No newline at end of file diff --git a/nanda_core/protocols/slim/__init__.py b/nanda_core/protocols/slim/__init__.py new file mode 100644 index 0000000..00c1b34 --- /dev/null +++ b/nanda_core/protocols/slim/__init__.py @@ -0,0 +1,3 @@ +from .adapter import SLIMProtocol + +__all__ = ['SLIMProtocol'] diff --git a/nanda_core/protocols/slim/adapter.py b/nanda_core/protocols/slim/adapter.py new file mode 100644 index 0000000..2476ca4 --- /dev/null +++ b/nanda_core/protocols/slim/adapter.py @@ -0,0 +1,312 @@ +""" +SLIM Protocol Adapter - Secure Low-latency Interactive Messaging + +Provides gRPC-based messaging with persistent connections. +""" +from __future__ import annotations +import asyncio +import json +import logging +from typing import Callable, Dict, Any, Optional +import grpc + +logger = logging.getLogger(__name__) + +try: + # Import SLIM SDK components + from slim_bindings.slim import Slim, PyName, PyIdentityProvider, PyIdentityVerifier + from slim_bindings.session import PySession, PySessionConfiguration, PyMessageContext + SLIM_AVAILABLE = True +except ImportError: + SLIM_AVAILABLE = False + logger.warning("slim-bindings not available. Install with: pip install slim-bindings") + +from ..base import AgentProtocol + + +class SLIMProtocol(AgentProtocol): + """SLIM Protocol adapter for gRPC-based agent communication""" + + def __init__(self, agent_id: str, slim_node_url: str, agent_name: str = None): + """Initialize SLIM protocol adapter + + Args: + agent_id: Unique agent identifier + slim_node_url: SLIM node gRPC endpoint (e.g., grpc://localhost:50051) + agent_name: Optional agent display name + """ + if not SLIM_AVAILABLE: + raise ImportError("slim-bindings not installed. Run: pip install slim-bindings") + + self.agent_id = agent_id + # Ensure URL has http:// prefix (required by SLIM SDK) + if not slim_node_url.startswith('http://') and not slim_node_url.startswith('https://'): + slim_node_url = f'http://{slim_node_url}' + self.slim_node_url = slim_node_url.replace('grpc://', 'http://') + self.agent_name = agent_name or agent_id + + # Message handler (set by bridge) + self.incoming_handler: Optional[Callable] = None + + # SLIM client + self.slim: Optional[Slim] = None + self.session: Optional[PySession] = None + self.stream_task: Optional[asyncio.Task] = None + self.running = False + + # Personal inbox channel name + self.inbox_channel = f"agent-{agent_id}-inbox" + + print(f"SLIM protocol initialized for {agent_id} at {self.slim_node_url}") + + def set_incoming_handler(self, handler: Callable): + """Set callback for incoming messages""" + self.incoming_handler = handler + print(f"Incoming handler set for SLIM protocol") + + async def connect(self): + """Connect to SLIM node and subscribe to inbox""" + try: + # Connect to SLIM node + print(f"Connecting to SLIM node at {self.slim_node_url}...") + + # Create PyName + name = PyName("agntcy", "nanda", self.agent_id) + + # Create identity provider and verifier + provider = PyIdentityProvider.SharedSecret( + identity=self.agent_id, + shared_secret="nanda-dev-secret" # TODO: Use env var + ) + verifier = PyIdentityVerifier.SharedSecret( + identity=self.agent_id, + shared_secret="nanda-dev-secret" # TODO: Use env var + ) + + # Initialize Slim + self.slim = await Slim.new(name, provider, verifier) + print(f"โœ… Slim instance created with ID: {self.slim.id}") + + # Connect to SLIM node + conn_id = await self.slim.connect({"endpoint": self.slim_node_url, "tls": {"insecure": True}}) + print(f"โœ… Connected to SLIM node (connection ID: {conn_id})") + + # Start listening for incoming sessions + self.running = True + self.stream_task = asyncio.create_task(self._listen_for_messages()) + + except Exception as e: + logger.error(f"โŒ Failed to connect to SLIM node: {e}") + import traceback + traceback.print_exc() + raise + + async def _listen_for_messages(self): + """Listen for incoming sessions""" + print(f"๐Ÿ‘‚ Listening for incoming SLIM sessions") + + try: + while self.running: + try: + print(f"โณ Waiting for incoming session...") + + # Wait for incoming session + session = await self.slim.listen_for_session() + print(f"๐Ÿ“ฌ Got incoming session: {session.id}") + + # Handle session in background task + asyncio.create_task(self._handle_session(session)) + + except Exception as e: + logger.error(f"Error in session listener: {e}") + import traceback + traceback.print_exc() + await asyncio.sleep(1) + + except asyncio.CancelledError: + print("SLIM message listener cancelled") + + async def _handle_session(self, session): + """Handle a single session (can receive multiple messages)""" + try: + while True: + # Receive message + msg_ctx, payload_bytes = await session.get_message() + print(f"๐Ÿ“ฅ SLIM: Received {len(payload_bytes)} bytes") + + # Process message + await self._process_incoming_message(session, msg_ctx, payload_bytes) + + except Exception as e: + print(f"Session {session.id} ended: {e}") + + async def _process_incoming_message(self, session, msg_ctx, payload_bytes: bytes): + """Process incoming SLIM message and route to handler""" + try: + # Decode payload + try: + payload_str = payload_bytes.decode('utf-8') + payload_data = json.loads(payload_str) + except (UnicodeDecodeError, json.JSONDecodeError): + payload_str = payload_bytes.decode('utf-8', errors='replace') + payload_data = {"text": payload_str} + + # Convert to standard NANDA message format + message_dict = { + "content": { + "text": payload_data.get("text", payload_str), + "type": "text" + }, + "conversation_id": "slim-conversation", + "metadata": { + "protocol": "slim", + "session_id": session.id + } + } + + print(f"๐Ÿ“ฅ SLIM message: {message_dict['content']['text'][:100]}") + + # Call handler + if self.incoming_handler: + response = await self.incoming_handler(message_dict) + print(f"๐Ÿค– Agent response: {response.get('content', {}).get('text', '')[:100]}...") # <-- ADD THIS + + # Send response back + if response: + await self._send_response(session, msg_ctx, response) + + except Exception as e: + logger.error(f"โŒ Error processing SLIM message: {e}") + import traceback + traceback.print_exc() + + async def _send_response(self, session, msg_ctx, response: Dict[str, Any]): + """Send response back using publish_to""" + try: + response_text = response.get("content", {}).get("text", "") + + response_payload = { + "text": response_text, + "type": "response" + } + + await session.publish_to(msg_ctx, json.dumps(response_payload).encode('utf-8')) + print(f"๐Ÿ“ค SLIM: Sent response") + + except Exception as e: + logger.error(f"Error sending SLIM response: {e}") + + async def send_message(self, target_url: str, message: Dict[str, Any]) -> Dict[str, Any]: + """Send message via SLIM protocol""" + try: + if not self.slim: + raise RuntimeError("SLIM not connected") + + # Extract target agent ID + target_agent_id = target_url.replace('slim://', '').split('/')[0] + content_text = message.get("content", {}).get("text", "") + + print(f"๐Ÿ“ค SLIM: Sending to {target_agent_id}: {content_text[:50]}...") + + # Create message payload + payload = {"text": content_text, "type": "message"} + + # Set route to target + target_name = PyName("agntcy", "nanda", target_agent_id) + await self.slim.set_route(target_name) + + # Create PointToPoint session + from datetime import timedelta + config = PySessionConfiguration.PointToPoint( + peer_name=target_name, + timeout=timedelta(seconds=5), + max_retries=5, + mls_enabled=False + ) + + session = await self.slim.create_session(config) + + # Send message + await session.publish(json.dumps(payload).encode('utf-8')) + print(f"โœ… SLIM: Message sent") + + # Wait for reply + msg_ctx, reply_bytes = await session.get_message() + reply_data = json.loads(reply_bytes.decode('utf-8')) + + # Clean up + await session.delete() + + return { + "content": { + "text": reply_data.get("text", reply_bytes.decode('utf-8')), + "type": "text" + } + } + + except Exception as e: + logger.error(f"โŒ Error sending SLIM message: {e}") + import traceback + traceback.print_exc() + return { + "content": { + "text": f"SLIM Error: {str(e)}", + "type": "text" + } + } + + async def handle_request(self, request): + """Handle incoming request (not used for SLIM - uses streaming)""" + pass + + def get_metadata(self) -> Dict[str, Any]: + """Get SLIM protocol metadata""" + return { + "agent_id": self.agent_id, + "name": self.agent_name, + "protocol": "slim", + "slim_node": self.slim_node_url, + "inbox_channel": self.inbox_channel, + "capabilities": ["unicast", "async_messaging"] + } + + async def start_server(self, host: str, port: int): + """Start SLIM protocol (connect and listen)""" + print(f"๐Ÿš€ Starting SLIM protocol for {self.agent_id}") + + # Connect to SLIM node + await self.connect() + + print(f"โœ… SLIM ready - listening on {self.inbox_channel}") + + # Keep running (stream_task handles incoming messages) + try: + while self.running: + await asyncio.sleep(1) + except asyncio.CancelledError: + print("SLIM protocol stopped") + finally: + await self.close() + + async def close(self): + """Close SLIM connection and cleanup""" + print(f"๐Ÿ›‘ Closing SLIM connection for {self.agent_id}") + + self.running = False + + if self.stream_task: + self.stream_task.cancel() + try: + await self.stream_task + except asyncio.CancelledError: + pass + + if self.slim: + try: + await self.slim.disconnect(self.slim_node_url) + except Exception as e: + logger.error(f"Error disconnecting SLIM: {e}") + + def get_protocol_name(self) -> str: + """Return protocol identifier""" + return "slim" \ No newline at end of file diff --git a/setup.py b/setup.py index 5f07af9..103aebd 100644 --- a/setup.py +++ b/setup.py @@ -11,12 +11,16 @@ def read_requirements(): """Read requirements from file""" requirements = [ "anthropic>=0.18.0", - "a2a-sdk>=0.2.0", # Official A2A SDK (replaced python-a2a) - "httpx>=0.27.0", # For async HTTP (replaces requests) - "uvicorn>=0.30.0", # ASGI server for A2A - "starlette>=0.37.0", # For A2A server (included with a2a-sdk) + "a2a-sdk>=0.2.0", + "httpx>=0.27.0", + "uvicorn>=0.30.0", + "starlette>=0.37.0", "python-dotenv>=1.0.0", - "psutil>=5.9.0", # For system monitoring + "psutil>=5.9.0", + # SLIM support + "slim-bindings>=0.1.0", # Add this + "grpcio>=1.60.0", # Add this + "protobuf>=4.25.0", # Add this ] return requirements diff --git a/slim b/slim new file mode 160000 index 0000000..6301ced --- /dev/null +++ b/slim @@ -0,0 +1 @@ +Subproject commit 6301ced85073c028898d60eb620dacb0cff6afbf diff --git a/test_receiver.py b/test_receiver.py new file mode 100644 index 0000000..acf1551 --- /dev/null +++ b/test_receiver.py @@ -0,0 +1,30 @@ +import asyncio +from slim_bindings.slim import Slim, PyName, PyIdentityProvider, PyIdentityVerifier + +async def main(): + print("=== RECEIVER ===") + name = PyName("agntcy", "nanda", "receiver-agent") + provider = PyIdentityProvider.SharedSecret(identity="receiver", shared_secret="test-secret") + verifier = PyIdentityVerifier.SharedSecret(identity="receiver", shared_secret="test-secret") + + slim = await Slim.new(name, provider, verifier) + conn_id = await slim.connect({"endpoint": "http://localhost:46357", "tls": {"insecure": True}}) + print(f"โœ… Connected: {conn_id}, ID: {slim.id}") + + print("โณ Waiting for incoming session...") + session = await slim.listen_for_session() + print(f"โœ… Got session: {session.id}") + + # Use get_message() instead of recv() + msg_ctx, payload = await session.get_message() + print(f"๐Ÿ“ฅ Received: {payload.decode('utf-8')}") + + # Reply back + reply = f"Echo: {payload.decode()} from receiver" + await session.publish_to(msg_ctx, reply.encode('utf-8')) + print(f"๐Ÿ“ค Sent reply: {reply}") + + await slim.disconnect("http://localhost:46357") + +if __name__ == "__main__": + asyncio.run(main()) \ No newline at end of file diff --git a/test_sender.py b/test_sender.py new file mode 100644 index 0000000..93539b5 --- /dev/null +++ b/test_sender.py @@ -0,0 +1,45 @@ +import asyncio +from datetime import timedelta +from slim_bindings.slim import Slim, PyName, PyIdentityProvider, PyIdentityVerifier +from slim_bindings.session import PySessionConfiguration + +async def main(): + print("=== SENDER ===") + name = PyName("agntcy", "nanda", "sender-agent") + provider = PyIdentityProvider.SharedSecret(identity="sender", shared_secret="test-secret") + verifier = PyIdentityVerifier.SharedSecret(identity="sender", shared_secret="test-secret") + + slim = await Slim.new(name, provider, verifier) + conn_id = await slim.connect({"endpoint": "http://localhost:46357", "tls": {"insecure": True}}) + print(f"โœ… Connected: {conn_id}, ID: {slim.id}") + + # CRITICAL: Set route before creating session + peer_name = PyName("agntcy", "nanda", "receiver-agent") + await slim.set_route(peer_name) + print(f"โœ… Route set to receiver-agent") + + # Create PointToPoint session + config = PySessionConfiguration.PointToPoint( + peer_name=peer_name, + timeout=timedelta(seconds=5), + max_retries=5, + mls_enabled=False + ) + + session = await slim.create_session(config) + print(f"โœ… Session created: {session.id}") + + # Send message + message = "Hello from sender!" + await session.publish(message.encode('utf-8')) + print(f"๐Ÿ“ค Sent: {message}") + + # Wait for reply + msg_ctx, reply = await session.get_message() + print(f"๐Ÿ“ฅ Received reply: {reply.decode('utf-8')}") + + await session.delete() + await slim.disconnect("http://localhost:46357") + +if __name__ == "__main__": + asyncio.run(main()) \ No newline at end of file