diff --git a/.gitignore b/.gitignore index e254ce5..a050bbf 100644 --- a/.gitignore +++ b/.gitignore @@ -10,4 +10,6 @@ nanda_agent/__pycache__ dist/ *.egg-info/ -nanda_adapter/core/__pycache__ \ No newline at end of file +nanda_adapter/core/__pycache__ +venv + diff --git a/README.md b/README.md index 7ca262a..2ead58e 100644 --- a/README.md +++ b/README.md @@ -46,11 +46,25 @@ pip install nanda-adapter > chmod 600 fullchain.pem privkey.pem` -### 4. Set Your enviroment variables ANTHROPIC_API_KEY (For running your personal hosted agents, need API key and your own domain) +### 4. Set Your environment variables and LLM Provider (For running your personal hosted agents, need API key and your own domain) -> export ANTHROPIC_API_KEY="your-api-key-here +Create a `.env` file with your configuration: -> export DOMAIN_NAME=" +```bash +# Choose your LLM provider: anthropic, openai, gemini, groq, mistral, cohere, grok +LLM_PROVIDER=anthropic +LLM_MODEL=claude-3-haiku-20240307 + +# Add API keys for providers you want to use +ANTHROPIC_API_KEY=your-api-key-here +OPENAI_API_KEY=your-openai-api-key +GOOGLE_API_KEY=your-google-api-key +GROQ_API_KEY=your-groq-api-key +MISTRAL_API_KEY=your-mistral-api-key +COHERE_API_KEY=your-cohere-api-key + +DOMAIN_NAME= +``` ### 5. Run an example agent (langchain_pirate.py) > nohup python3 langchain_pirate.py > out.log 2>&1 & @@ -244,7 +258,21 @@ The framework will automatically: ### Environment Variables You need the following environment details () -- `ANTHROPIC_API_KEY`: Your Anthropic API key (required) +**LLM Provider Configuration:** +- `LLM_PROVIDER`: Choose LLM provider - anthropic, openai, gemini, groq, mistral, cohere, grok (default: anthropic) +- `LLM_MODEL`: Model name for the selected provider (default: claude-3-haiku-20240307) +- `LLM_MAX_TOKENS`: Maximum tokens for responses (default: 512) +- `LLM_TEMPERATURE`: Response temperature (default: 0.7) + +**API Keys (add keys for providers you want to use):** +- `ANTHROPIC_API_KEY`: Your Anthropic API key +- `OPENAI_API_KEY`: Your OpenAI API key +- `GOOGLE_API_KEY`: Your Google API key +- `GROQ_API_KEY`: Your Groq API key +- `MISTRAL_API_KEY`: Your Mistral API key +- `COHERE_API_KEY`: Your Cohere API key + +**Agent Configuration:** - `DOMAIN_NAME`: Domain name for SSL certificates (required) - `AGENT_ID`: Custom agent ID (optional, auto-generated if not provided) - `PORT`: Agent bridge port (optional, default: 6000) diff --git a/nanda_adapter/core/agent_bridge.py b/nanda_adapter/core/agent_bridge.py index 26361d6..5f9f09e 100644 --- a/nanda_adapter/core/agent_bridge.py +++ b/nanda_adapter/core/agent_bridge.py @@ -8,24 +8,24 @@ from typing import Optional from datetime import datetime from anthropic import Anthropic, APIStatusError +from .llm_config import call_llm +from llm_config import call_llm from python_a2a import ( A2AServer, A2AClient, run_server, Message, TextContent, MessageRole, ErrorContent, Metadata ) import asyncio -from mcp_utils import MCPClient +from .mcp_utils import MCPClient import base64 import sys sys.stdout.reconfigure(line_buffering=True) -# Set API key through environment variable or directly in the code -ANTHROPIC_API_KEY = os.getenv("ANTHROPIC_API_KEY") or "your key" - # Toggle for message improvement feature IMPROVE_MESSAGES = os.getenv("IMPROVE_MESSAGES", "true").lower() in ("true", "1", "yes", "y") -# Create Anthropic client with explicit API key +# Legacy support - keep for backward compatibility +ANTHROPIC_API_KEY = os.getenv("ANTHROPIC_API_KEY") or "your key" anthropic = Anthropic(api_key=ANTHROPIC_API_KEY) # Get agent configuration from environment variables @@ -152,8 +152,8 @@ def log_message(conversation_id, path, source, message_text): print(f"Logged message from {source} in conversation {conversation_id}") -def call_claude(prompt: str, additional_context: str, conversation_id: str, current_path: str, system_prompt: str = None) -> Optional[str]: - """Wrapper that never raises: returns text or None on failure.""" +def call_llm_wrapper(prompt: str, additional_context: str, conversation_id: str, current_path: str, system_prompt: str = None) -> Optional[str]: + """Multi-provider LLM wrapper that never raises: returns text or None on failure.""" try: # Use the specified system prompt or default to the agent's system prompt if system_prompt: @@ -165,65 +165,60 @@ def call_claude(prompt: str, additional_context: str, conversation_id: str, curr # Combine the prompt with additional context if provided full_prompt = prompt if additional_context and additional_context.strip(): - full_prompt = f"ADDITIONAL CONTEXT FRseOM USER: {additional_context}\n\nMESSAGE: {prompt}" + full_prompt = f"ADDITIONAL CONTEXT FROM USER: {additional_context}\n\nMESSAGE: {prompt}" agent_id = get_agent_id() - print(f"Agent {agent_id}: Calling Claude with prompt: {full_prompt[:50]}...") - resp = anthropic.messages.create( - model="claude-3-5-sonnet-20241022", - max_tokens=512, - messages=[{"role":"user","content":full_prompt}], - system=system - ) - response_text = resp.content[0].text + provider = os.getenv("LLM_PROVIDER", "anthropic").upper() + print(f"Agent {agent_id}: Calling {provider} with prompt: {full_prompt[:50]}...") - # Log the Claude response - log_message(conversation_id, current_path, f"Claude {agent_id}", response_text) + # Use the multi-provider function + response_text = call_llm(full_prompt, system) - return response_text - except APIStatusError as e: - print(f"Agent {agent_id}: Anthropic API error:", e.status_code, e.message, flush=True) - # If we hit a credit limit error, return a fallback message - if "credit balance is too low" in str(e): - return f"Agent {agent_id} processed (API credit limit reached): {prompt}" + if response_text: + # Log the LLM response + log_message(conversation_id, current_path, f"{provider} {agent_id}", response_text) + return response_text + else: + print(f"Agent {agent_id}: {provider} returned no response") + return None + except Exception as e: - print(f"Agent {agent_id}: Anthropic SDK error:", e, flush=True) + agent_id = get_agent_id() + provider = os.getenv("LLM_PROVIDER", "anthropic").upper() + print(f"Agent {agent_id}: {provider} error:", e, flush=True) traceback.print_exc() - return None + return None -def call_claude_direct(message_text: str, system_prompt: str = None) -> Optional[str]: - """Wrapper that never raises: returns text or None on failure.""" +# Legacy function for backward compatibility +def call_claude(prompt: str, additional_context: str, conversation_id: str, current_path: str, system_prompt: str = None) -> Optional[str]: + """Legacy Claude wrapper - redirects to call_llm_wrapper""" + return call_llm_wrapper(prompt, additional_context, conversation_id, current_path, system_prompt) + +def call_llm_direct(message_text: str, system_prompt: str = None) -> Optional[str]: + """Multi-provider LLM wrapper for direct calls""" try: - # Use the specified system prompt or default to the agent's system prompt - - # Combine the prompt with additional context if provided full_prompt = f"MESSAGE: {message_text}" - agent_id = get_agent_id() - print(f"Agent {agent_id}: Calling Claude with prompt: {full_prompt[:50]}...") - resp = anthropic.messages.create( - model="claude-3-5-sonnet-20241022", - max_tokens=512, - messages=[{"role":"user","content":full_prompt}], - system=system_prompt - ) - response_text = resp.content[0].text - - # Log the Claude response + provider = os.getenv("LLM_PROVIDER", "anthropic").upper() + print(f"Agent {agent_id}: Calling {provider} with prompt: {full_prompt[:50]}...") + response_text = call_llm(full_prompt, system_prompt) return response_text - except APIStatusError as e: - print(f"Agent {agent_id}: Anthropic API error:", e.status_code, e.message, flush=True) - # If we hit a credit limit error, return a fallback message - if "credit balance is too low" in str(e): - return f"Agent {agent_id} processed (API credit limit reached): {message_text}" + except Exception as e: - print(f"Agent {agent_id}: Anthropic SDK error:", e, flush=True) + agent_id = get_agent_id() + provider = os.getenv("LLM_PROVIDER", "anthropic").upper() + print(f"Agent {agent_id}: {provider} error:", e, flush=True) traceback.print_exc() - return None + return None + +# Legacy function for backward compatibility +def call_claude_direct(message_text: str, system_prompt: str = None) -> Optional[str]: + """Legacy Claude wrapper - redirects to call_llm_direct""" + return call_llm_direct(message_text, system_prompt) def improve_message(message_text: str, conversation_id: str, current_path: str, additional_prompt: str=None) -> str: - """Improve a message using Claude before forwarding it to the other party.""" + """Improve a message using the configured LLM provider before forwarding it to the other party.""" if not IMPROVE_MESSAGES: return message_text @@ -234,10 +229,10 @@ def improve_message(message_text: str, conversation_id: str, current_path: str, # Use the appropriate improvement prompt based on agent ID system_prompt = IMPROVE_MESSAGE_PROMPTS["default"] - # Call Claude to improve the message - improved_message = call_claude(message_text, "", conversation_id, current_path, system_prompt) + # Call the configured LLM provider to improve the message + improved_message = call_llm_wrapper(message_text, "", conversation_id, current_path, system_prompt) - # If Claude successfully improved the message, use that; otherwise, use the original + # If LLM successfully improved the message, use that; otherwise, use the original return improved_message if improved_message else message_text except Exception as e: print(f"Error improving message: {e}") @@ -572,29 +567,35 @@ def list_message_improvers(): return list(message_improvement_decorators.keys()) # Default improver -@message_improver("default_claude") -def default_claude_improver(message_text: str) -> str: - """Default Claude-based message improvement""" +@message_improver("default_llm") +def default_llm_improver(message_text: str) -> str: + """Default multi-provider LLM-based message improvement""" if not IMPROVE_MESSAGES: return message_text try: - additional_prompt = "Do not respond to the content of the message - it's intended for another agent. You are helping an agent communicate better with other agennts." + additional_prompt = "Do not respond to the content of the message - it's intended for another agent. You are helping an agent communicate better with other agents." system_prompt = additional_prompt + IMPROVE_MESSAGE_PROMPTS["default"] print(system_prompt) - improved_message = call_claude_direct(message_text, system_prompt) + improved_message = call_llm_direct(message_text, system_prompt) print(f"Improved message: {improved_message}") return improved_message if improved_message else message_text except Exception as e: print(f"Error improving message: {e}") return message_text +# Legacy improver for backward compatibility +@message_improver("default_claude") +def default_claude_improver(message_text: str) -> str: + """Legacy Claude improver - redirects to default_llm_improver""" + return default_llm_improver(message_text) + class AgentBridge(A2AServer): """Global Agent Bridge - Can be used for any agent in the network.""" def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.active_improver = "default_claude" # Default improver + self.active_improver = "default_llm" # Default multi-provider improver def set_message_improver(self, improver_name): """Set the active message improver by name""" @@ -819,20 +820,20 @@ def handle_message(self, msg: Message) -> Message: query_text = parts[1] print(f"Processing query command: '{query_text}'") - # Call Claude with the query - claude_response = call_claude(query_text, additional_context, conversation_id, current_path, - "You are Claude, an AI assistant. Provide a direct, helpful response to the user's question. Treat it as a private request for guidance and respond only to the user.") + # Call the configured LLM with the query + llm_response = call_llm_wrapper(query_text, additional_context, conversation_id, current_path, + "You are an AI assistant. Provide a direct, helpful response to the user's question. Treat it as a private request for guidance and respond only to the user.") # Make sure we have a valid response - if not claude_response: - print("Warning: Claude returned empty response") - claude_response = "Sorry, I couldn't process your query. Please try again." + if not llm_response: + print("Warning: LLM returned empty response") + llm_response = "Sorry, I couldn't process your query. Please try again." else: - print(f"Claude response received ({len(claude_response)} chars)") - print(f"Response preview: {claude_response[:50]}...") + print(f"LLM response received ({len(llm_response)} chars)") + print(f"Response preview: {llm_response[:50]}...") # Format and return the response - formatted_response = f"[AGENT {agent_id}] {claude_response}" + formatted_response = f"[AGENT {agent_id}] {llm_response}" # Return to local terminal response_message = Message( @@ -867,8 +868,8 @@ def handle_message(self, msg: Message) -> Message: else: # Regular message - process locally - claude_response = call_claude(user_text, additional_context, conversation_id, current_path) or user_text - formatted_response = f"[AGENT {agent_id}] {claude_response}" + llm_response = call_llm_wrapper(user_text, additional_context, conversation_id, current_path) or user_text + formatted_response = f"[AGENT {agent_id}] {llm_response}" # Return Claude's response to local terminal return Message( diff --git a/nanda_adapter/core/llm_config.py b/nanda_adapter/core/llm_config.py new file mode 100644 index 0000000..40e83be --- /dev/null +++ b/nanda_adapter/core/llm_config.py @@ -0,0 +1,271 @@ +#!/usr/bin/env python3 +""" +LLM Provider Configuration for NANDA +Supports multiple LLM providers: Anthropic, OpenAI, Google Gemini, Groq, etc. +""" + +import os +import traceback +from typing import Optional + +def call_llm(prompt: str, system_prompt: str = None) -> Optional[str]: + """Call the configured LLM provider with the given prompt""" + + provider = os.getenv("LLM_PROVIDER", "anthropic").lower() + model = os.getenv("LLM_MODEL", "") + max_tokens = int(os.getenv("LLM_MAX_TOKENS", "512")) + temperature = float(os.getenv("LLM_TEMPERATURE", "0.7")) + + print(f"🤖 Calling {provider.upper()} with prompt: {prompt[:50]}...") + + try: + if provider == "anthropic": + return _call_anthropic(prompt, system_prompt, model or "claude-3-5-sonnet-20241022", max_tokens, temperature) + elif provider == "openai": + return _call_openai(prompt, system_prompt, model or "gpt-4o-mini", max_tokens, temperature) + elif provider == "gemini": + return _call_gemini(prompt, system_prompt, model or "gemini-1.5-flash", max_tokens, temperature) + elif provider == "groq": + return _call_groq(prompt, system_prompt, model or "llama3-8b-8192", max_tokens, temperature) + elif provider == "mistral": + return _call_mistral(prompt, system_prompt, model or "mistral-small-latest", max_tokens, temperature) + elif provider == "cohere": + return _call_cohere(prompt, system_prompt, model or "command", max_tokens, temperature) + elif provider == "grok": + return _call_grok(prompt, system_prompt, model or "grok-beta", max_tokens, temperature) + else: + print(f"❌ Unknown provider: {provider}") + print("Available providers: anthropic, openai, gemini, groq, mistral, cohere, grok") + return None + + except Exception as e: + print(f"❌ Error calling {provider}: {e}") + traceback.print_exc() + return None + +def _call_anthropic(prompt: str, system_prompt: str, model: str, max_tokens: int, temperature: float) -> Optional[str]: + """Call Anthropic Claude""" + try: + from anthropic import Anthropic, APIStatusError + + api_key = os.getenv("ANTHROPIC_API_KEY") + if not api_key: + print("❌ ANTHROPIC_API_KEY not found") + return None + + client = Anthropic(api_key=api_key) + system = system_prompt or "You are Claude, an AI assistant." + + response = client.messages.create( + model=model, + max_tokens=max_tokens, + temperature=temperature, + messages=[{"role": "user", "content": prompt}], + system=system + ) + + return response.content[0].text + + except APIStatusError as e: + print(f"❌ Anthropic API error: {e.status_code} {e.message}") + return None + except Exception as e: + print(f"❌ Anthropic error: {e}") + return None + +def _call_openai(prompt: str, system_prompt: str, model: str, max_tokens: int, temperature: float) -> Optional[str]: + """Call OpenAI GPT""" + try: + from openai import OpenAI + + api_key = os.getenv("OPENAI_API_KEY") + if not api_key: + print("❌ OPENAI_API_KEY not found") + return None + + client = OpenAI(api_key=api_key) + + messages = [] + if system_prompt: + messages.append({"role": "system", "content": system_prompt}) + messages.append({"role": "user", "content": prompt}) + + response = client.chat.completions.create( + model=model, + messages=messages, + max_tokens=max_tokens, + temperature=temperature + ) + + return response.choices[0].message.content + + except Exception as e: + print(f"❌ OpenAI error: {e}") + return None + +def _call_gemini(prompt: str, system_prompt: str, model: str, max_tokens: int, temperature: float) -> Optional[str]: + """Call Google Gemini""" + try: + import google.generativeai as genai + + api_key = os.getenv("GOOGLE_API_KEY") + if not api_key: + print("❌ GOOGLE_API_KEY not found") + return None + + genai.configure(api_key=api_key) + + generation_config = { + "temperature": temperature, + "top_p": 1, + "top_k": 1, + "max_output_tokens": max_tokens, + } + + model_obj = genai.GenerativeModel( + model_name=model, + generation_config=generation_config + ) + + full_prompt = prompt + if system_prompt: + full_prompt = f"{system_prompt}\n\n{prompt}" + + response = model_obj.generate_content(full_prompt) + return response.text + + except Exception as e: + print(f"❌ Gemini error: {e}") + return None + +def _call_groq(prompt: str, system_prompt: str, model: str, max_tokens: int, temperature: float) -> Optional[str]: + """Call Groq""" + try: + from groq import Groq + + api_key = os.getenv("GROQ_API_KEY") + if not api_key: + print("❌ GROQ_API_KEY not found") + return None + + client = Groq(api_key=api_key) + + messages = [] + if system_prompt: + messages.append({"role": "system", "content": system_prompt}) + messages.append({"role": "user", "content": prompt}) + + response = client.chat.completions.create( + model=model, + messages=messages, + max_tokens=max_tokens, + temperature=temperature + ) + + return response.choices[0].message.content + + except Exception as e: + print(f"❌ Groq error: {e}") + return None + +def _call_mistral(prompt: str, system_prompt: str, model: str, max_tokens: int, temperature: float) -> Optional[str]: + """Call Mistral""" + try: + from mistralai.client import MistralClient + + api_key = os.getenv("MISTRAL_API_KEY") + if not api_key: + print("❌ MISTRAL_API_KEY not found") + return None + + client = MistralClient(api_key=api_key) + + messages = [] + if system_prompt: + messages.append({"role": "system", "content": system_prompt}) + messages.append({"role": "user", "content": prompt}) + + response = client.chat.completions.create( + model=model, + messages=messages, + max_tokens=max_tokens, + temperature=temperature + ) + + return response.choices[0].message.content + + except Exception as e: + print(f"❌ Mistral error: {e}") + return None + +def _call_cohere(prompt: str, system_prompt: str, model: str, max_tokens: int, temperature: float) -> Optional[str]: + """Call Cohere""" + try: + import cohere + + api_key = os.getenv("COHERE_API_KEY") + if not api_key: + print("❌ COHERE_API_KEY not found") + return None + + client = cohere.Client(api_key=api_key) + + full_prompt = prompt + if system_prompt: + full_prompt = f"{system_prompt}\n\n{prompt}" + + response = client.generate( + model=model, + prompt=full_prompt, + max_tokens=max_tokens, + temperature=temperature + ) + + return response.generations[0].text + + except Exception as e: + print(f"❌ Cohere error: {e}") + return None + +def _call_grok(prompt: str, system_prompt: str, model: str, max_tokens: int, temperature: float) -> Optional[str]: + """Call Grok/XAI""" + try: + import requests + + api_key = os.getenv("GROQ_API_KEY") # Using GROQ_API_KEY for Grok as per your .env setup + if not api_key: + print("❌ GROQ_API_KEY not found in environment") + return None + + headers = { + "Authorization": f"Bearer {api_key}", + "Content-Type": "application/json" + } + + # Grok API endpoint + url = "https://api.x.ai/v1/chat/completions" + + messages = [] + if system_prompt: + messages.append({"role": "system", "content": system_prompt}) + messages.append({"role": "user", "content": prompt}) + + payload = { + "model": model, + "messages": messages, + "max_tokens": max_tokens, + "temperature": temperature + } + + response = requests.post(url, headers=headers, json=payload, timeout=30) + + if response.status_code == 200: + result = response.json() + return result["choices"][0]["message"]["content"] + else: + print(f"❌ Grok API error: {response.status_code} - {response.text}") + return None + + except Exception as e: + print(f"❌ Grok error: {e}") + return None diff --git a/requirements.txt b/requirements.txt index 1f6101e..1b6965e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,9 +1,21 @@ +python-a2a==0.5.6 +python-dotenv==1.1.1 +requests==2.32.5 +anthropic==0.68.1 +openai==1.109.1 +google-generativeai==0.8.5 +groq==0.32.0 +mistralai==1.9.10 +cohere==5.18.0 +flask==3.1.2 +flask-cors==6.0.1 +mcp==1.15.0 +pymongo==4.15.1 flask anthropic requests -python-a2a==0.5.6 +python-a2a mcp -anthropic python-dotenv flask-cors pymongo \ No newline at end of file