From fa8cbf19d0c09c236d20eb58397f41cd07e5f5d6 Mon Sep 17 00:00:00 2001 From: Nerzouille Date: Mon, 8 Sep 2025 17:00:33 +0200 Subject: [PATCH] [ADD] fonctionnal libertai --- crypto-pilot-builder/package-lock.json | 105 --- .../python/mcp_client/api_routes.py | 96 ++- .../python/mcp_client/config.py | 4 +- .../python/mcp_client/mcp_client.py | 1 + .../python/mcp_serveur/agent_factory.py | 39 + .../python/mcp_serveur/base_agent.py | 223 ++++++ .../python/mcp_serveur/libertai_agent.py | 177 +++++ .../python/mcp_serveur/mcp_server_sdk.py | 148 +--- .../python/mcp_serveur/mcp_server_sdk_old.py | 684 ++++++++++++++++++ crypto-pilot-builder/python/test_libertai.py | 90 +++ .../src/agent_building/Ai.vue | 60 +- .../src/components/AgentConfigManager.vue | 14 + crypto-pilot-builder/src/store/index.js | 43 +- 13 files changed, 1420 insertions(+), 264 deletions(-) create mode 100644 crypto-pilot-builder/python/mcp_serveur/agent_factory.py create mode 100644 crypto-pilot-builder/python/mcp_serveur/base_agent.py create mode 100644 crypto-pilot-builder/python/mcp_serveur/libertai_agent.py create mode 100644 crypto-pilot-builder/python/mcp_serveur/mcp_server_sdk_old.py create mode 100644 crypto-pilot-builder/python/test_libertai.py diff --git a/crypto-pilot-builder/package-lock.json b/crypto-pilot-builder/package-lock.json index 962dd4b..aeae7d6 100644 --- a/crypto-pilot-builder/package-lock.json +++ b/crypto-pilot-builder/package-lock.json @@ -1186,72 +1186,6 @@ "node": ">= 10.0.0" } }, - "node_modules/@electron/windows-sign": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@electron/windows-sign/-/windows-sign-1.2.2.tgz", - "integrity": "sha512-dfZeox66AvdPtb2lD8OsIIQh12Tp0GNCRUDfBHIKGpbmopZto2/A8nSpYYLoedPIHpqkeblZ/k8OV0Gy7PYuyQ==", - "dev": true, - "license": "BSD-2-Clause", - "optional": true, - "peer": true, - "dependencies": { - "cross-dirname": "^0.1.0", - "debug": "^4.3.4", - "fs-extra": "^11.1.1", - "minimist": "^1.2.8", - "postject": "^1.0.0-alpha.6" - }, - "bin": { - "electron-windows-sign": "bin/electron-windows-sign.js" - }, - "engines": { - "node": ">=14.14" - } - }, - "node_modules/@electron/windows-sign/node_modules/fs-extra": { - "version": "11.3.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.0.tgz", - "integrity": "sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=14.14" - } - }, - "node_modules/@electron/windows-sign/node_modules/jsonfile": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/@electron/windows-sign/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">= 10.0.0" - } - }, "node_modules/@element-plus/icons-vue": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/@element-plus/icons-vue/-/icons-vue-2.3.1.tgz", @@ -4609,15 +4543,6 @@ "buffer": "^5.1.0" } }, - "node_modules/cross-dirname": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/cross-dirname/-/cross-dirname-0.1.0.tgz", - "integrity": "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true - }, "node_modules/cross-env": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", @@ -8519,36 +8444,6 @@ "node": ">=4" } }, - "node_modules/postject": { - "version": "1.0.0-alpha.6", - "resolved": "https://registry.npmjs.org/postject/-/postject-1.0.0-alpha.6.tgz", - "integrity": "sha512-b9Eb8h2eVqNE8edvKdwqkrY6O7kAwmI8kcnBv1NScolYJbo59XUF0noFq+lxbC1yN20bmC0WBEbDC5H/7ASb0A==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "commander": "^9.4.0" - }, - "bin": { - "postject": "dist/cli.js" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/postject/node_modules/commander": { - "version": "9.5.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", - "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": "^12.20.0 || >=14" - } - }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", diff --git a/crypto-pilot-builder/python/mcp_client/api_routes.py b/crypto-pilot-builder/python/mcp_client/api_routes.py index f3b742a..c83e259 100644 --- a/crypto-pilot-builder/python/mcp_client/api_routes.py +++ b/crypto-pilot-builder/python/mcp_client/api_routes.py @@ -67,6 +67,7 @@ class AgentConfig(db.Model): user_id = db.Column(UUID(as_uuid=True), db.ForeignKey('users.id'), nullable=False) # Configuration IA + provider = db.Column(db.String(50), nullable=False, default='openai') selected_model = db.Column(db.String(100), nullable=False) api_key = db.Column(db.Text, nullable=False) # Chiffré en production @@ -156,6 +157,28 @@ def validate_openai_api_key(api_key): return True + def validate_libertai_api_key(api_key): + """LibertAI API key validation""" + if not api_key or not isinstance(api_key, str): + return False + + if len(api_key) < 10 or len(api_key) > 200: + return False + allowed_pattern = r'^[A-Za-z0-9._-]+$' + if not re.match(allowed_pattern, api_key): + return False + + return True + + def validate_api_key(api_key, provider): + """Validate API key based on provider""" + if provider.lower() == "openai": + return validate_openai_api_key(api_key) + elif provider.lower() == "libertai": + return validate_libertai_api_key(api_key) + else: + return False + with app.app_context(): try: db.create_all() @@ -321,6 +344,7 @@ def create_agent_config(): 'message': 'Configuration sauvegardée avec succès', 'config': { 'id': config.id, + 'provider': config.provider, 'selectedModel': config.selected_model, 'apiKey': config.api_key[-4:] + '...' if config.api_key else '', # Masquer la clé 'modules': config.modules_config, @@ -350,6 +374,7 @@ def get_agent_config(): return jsonify({ 'config': { 'id': config.id, + 'provider': config.provider, 'selectedModel': config.selected_model, 'apiKey': config.api_key, # Retourner la clé complète pour l'utilisation 'modules': config.modules_config, @@ -389,12 +414,20 @@ def update_partial_config(): db.session.add(config) # Mettre à jour les champs fournis + if 'provider' in data: + config.provider = data['provider'] if 'selectedModel' in data: config.selected_model = data['selectedModel'] if 'apiKey' in data: - # Validation du format de l'API key si elle est fournie - # if not validate_openai_api_key(data['apiKey']): - # return jsonify({'error': 'Format de clé API OpenAI invalide. La clé doit commencer par "sk-" et contenir uniquement des caractères alphanumériques, tirets et underscores.'}), 400 + # Validate API key based on provider + provider = data.get('provider', config.provider or 'openai') + if not validate_api_key(data['apiKey'], provider): + if provider.lower() == 'openai': + return jsonify({'error': 'Format de clé API OpenAI invalide. La clé doit commencer par "sk-" et contenir uniquement des caractères alphanumériques, tirets et underscores.'}), 400 + elif provider.lower() == 'libertai': + return jsonify({'error': 'Format de clé API LibertAI invalide. Vérifiez que votre clé est correcte.'}), 400 + else: + return jsonify({'error': f'Format de clé API invalide pour le provider {provider}.'}), 400 config.api_key = data['apiKey'] if 'modules' in data: config.modules_config = data['modules'] @@ -496,6 +529,25 @@ async def do_list(): except Exception as e: return jsonify({"error": str(e)}), 500 + @app.route('/mcp/providers', methods=['GET']) + def list_available_providers(): + """List available AI providers""" + try: + # Import here to avoid circular imports + import sys + import os + sys.path.append(os.path.join(os.path.dirname(__file__), '..', 'mcp_serveur')) + from agent_factory import AgentFactory + + providers = AgentFactory.get_available_providers() + return jsonify({ + "providers": providers, + "current": "openai", + "note": "Currently only OpenAI is fully implemented" + }) + except Exception as e: + return jsonify({"error": f"Failed to get providers: {str(e)}"}), 500 + @app.route('/crypto/price', methods=['POST']) def get_crypto_price(): """Get cryptocurrency price via MCP tool""" @@ -573,18 +625,23 @@ def chat(): message_id = session_manager.add_message(session_id, "user", user_input) # NOUVELLE FONCTIONNALITÉ: Extraction automatique d'informations importantes - try: - memory_extractions = user_memory_manager.process_user_message( - user_id=user_id, - message=user_input, - openai_api_key=config.api_key, - message_id=message_id - ) - - if memory_extractions > 0: - logger.info(f"💾 {memory_extractions} information(s) extraite(s) et stockée(s) pour l'utilisateur {user_id}") - except Exception as e: - logger.warning(f"⚠️ Erreur lors de l'extraction de mémoire (continuer sans): {e}") + # IMPORTANT NOTE NOA: Only usable with OpenAI for now + memory_extractions = 0 + if config.provider == 'openai': + try: + memory_extractions = user_memory_manager.process_user_message( + user_id=user_id, + message=user_input, + openai_api_key=config.api_key, + message_id=message_id + ) + + if memory_extractions > 0: + logger.info(f"💾 {memory_extractions} information(s) extraite(s) et stockée(s) pour l'utilisateur {user_id}") + except Exception as e: + logger.warning(f"⚠️ Erreur lors de l'extraction de mémoire (continuer sans): {e}") + else: + logger.info(f"📝 Extraction de mémoire désactivée pour le provider {config.provider} (uniquement OpenAI supporté)") # Build conversation context with user config ET mémoire utilisateur conversation_history = session_manager.get_context(session_id) @@ -601,6 +658,7 @@ def chat(): 'user_memory': user_memory_summary, # Nouvelle clé pour la mémoire utilisateur 'wallet_address': wallet_address, # Adresse du wallet pour les swaps 'agent_config': { + 'provider': config.provider, 'model': config.selected_model, 'prompt': config.prompt, 'modules': config.modules_config @@ -890,10 +948,4 @@ def get_wallet_address(): }), 200 except Exception as e: - return jsonify({'error': f'Failed to get wallet address: {str(e)}'}), 500 - - # ===== AUTOWALLET ROUTES ===== - - # Importer et créer les routes d'autowallet - from .autowallet_routes import create_autowallet_routes - create_autowallet_routes(app) \ No newline at end of file + return jsonify({'error': f'Failed to get wallet address: {str(e)}'}), 500 \ No newline at end of file diff --git a/crypto-pilot-builder/python/mcp_client/config.py b/crypto-pilot-builder/python/mcp_client/config.py index e1f3a34..155d461 100644 --- a/crypto-pilot-builder/python/mcp_client/config.py +++ b/crypto-pilot-builder/python/mcp_client/config.py @@ -17,8 +17,8 @@ DEBUG = True # MCP configuration -MCP_SERVER_COMMAND = "python" -MCP_SERVER_ARGS = [str(SERVER_DIR / SERVER_SCRIPT)] +MCP_SERVER_COMMAND = "python3" +MCP_SERVER_ARGS = ["-u", str(SERVER_DIR / SERVER_SCRIPT)] # OpenAI configuration (via .env) OPENAI_MODEL = "gpt-4o-mini" diff --git a/crypto-pilot-builder/python/mcp_client/mcp_client.py b/crypto-pilot-builder/python/mcp_client/mcp_client.py index a28f2c8..e40a105 100644 --- a/crypto-pilot-builder/python/mcp_client/mcp_client.py +++ b/crypto-pilot-builder/python/mcp_client/mcp_client.py @@ -126,6 +126,7 @@ async def chat_with_config(self, message: str, context: Dict[str, Any], api_key: "message": message, "context": json.dumps(context), "api_key": api_key, + "provider": agent_config.get('provider', 'openai'), "model": agent_config.get('model', 'gpt-4o-mini'), "system_prompt": system_prompt, "modules": json.dumps(agent_config.get('modules', {})) diff --git a/crypto-pilot-builder/python/mcp_serveur/agent_factory.py b/crypto-pilot-builder/python/mcp_serveur/agent_factory.py new file mode 100644 index 0000000..2867a87 --- /dev/null +++ b/crypto-pilot-builder/python/mcp_serveur/agent_factory.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python3 +""" +Factory for creating AI agents based on provider +""" +import sys +import os +sys.path.append(os.path.dirname(__file__)) +from base_agent import BaseAgent + +class AgentFactory: + """Factory for creating AI agents""" + + @staticmethod + def create_agent(provider: str = "openai") -> BaseAgent: + """ + Create an AI agent based on the provider + + Args: + provider (str): The AI provider to use (default: "openai") + + Returns: + BaseAgent: An instance of the appropriate agent + """ + if provider.lower() == "openai": + from mcp_server_sdk import OpenAIAgent + return OpenAIAgent() + elif provider.lower() == "libertai": + from libertai_agent import LibertAIAgent + return LibertAIAgent() + else: + # Fallback to OpenAI for now + print(f"⚠️ Provider '{provider}' not yet implemented, falling back to OpenAI") + from mcp_server_sdk import OpenAIAgent + return OpenAIAgent() + + @staticmethod + def get_available_providers() -> list: + """Get list of available providers""" + return ["openai", "libertai"] diff --git a/crypto-pilot-builder/python/mcp_serveur/base_agent.py b/crypto-pilot-builder/python/mcp_serveur/base_agent.py new file mode 100644 index 0000000..81e8867 --- /dev/null +++ b/crypto-pilot-builder/python/mcp_serveur/base_agent.py @@ -0,0 +1,223 @@ +#!/usr/bin/env python3 +""" +Base interface and shared utilities for all AI agents. + +This module centralizes common logic used by different providers +(e.g., OpenAI, LibertAI) to avoid duplication: +- Default system prompt rules +- Tool schemas +- Intent detection for forcing tool_choice +- Prompt/context composition helpers +""" +from abc import ABC, abstractmethod +from typing import Any, Dict, List, Optional, Tuple, Union + +import json + +class BaseAgent(ABC): + """Base class for all AI agents with crypto capabilities""" + + @abstractmethod + async def process_message(self, message: str, context: str = "") -> str: + """Process a message with default configuration""" + pass + + @abstractmethod + async def process_message_with_config(self, message: str, context: str = "", + system_prompt: str = "", model: str = "", + api_key: str = "", modules: dict = None) -> str: + """Process a message with custom configuration""" + pass + + # ===== Shared utilities to reduce duplication between providers ===== + def get_shared_system_prompt(self) -> str: + """Return the shared system prompt containing core crypto rules. + + Providers can prefix/suffix this if needed, but the rules remain identical. + """ + return ( + "You are a crypto assistant with advanced capabilities including:\n" + "1. **Price Information**: Get real-time cryptocurrency prices using get_crypto_price\n" + "2. **Transactions**: Prepare blockchain transactions using request_transaction\n" + "3. **Token Information**: Get available ERC-20 tokens on Sepolia using get_sepolia_tokens\n" + "4. **Token Swapping**: Complete token swap capabilities using Li.Fi:\n" + " - get_lifi_tokens: Discover available tokens for swapping\n" + " - get_swap_quote: Get quotes for token swaps\n" + " - execute_swap: Execute token swaps with transaction data\n\n" + "🎯 RÈGLE CRITIQUE pour request_transaction :\n\n" + "COPIE EXACTEMENT le mot que dit l'utilisateur dans le paramètre \"currency\". NE TRADUIS PAS, NE CONVERTIS PAS.\n\n" + "EXEMPLES CORRECTS :\n" + "- User: \"envoie 0.001 ETH à 0x123...\" → request_transaction(..., currency=\"ETH\")\n" + "- User: \"envoie 0.001 sepolia à 0x123...\" → request_transaction(..., currency=\"sepolia\")\n" + "- User: \"envoie 5 USDC à 0x123...\" → request_transaction(..., currency=\"USDC\")\n\n" + "❌ ERREUR FATALE (ne jamais faire) :\n" + "- User dit \"ETH\" mais tu mets currency=\"sepolia\" ← INTERDIT !\n\n" + "✅ TOKENS DISPONIBLES : ETH, SEPOLIA, USDC, USDT, DAI, WETH, LINK, UNI\n\n" + "RÈGLES CRITIQUES pour les swaps :\n" + "1. DÉTECTION : Si tu détectes l'un de ces mots-clés dans le message :\n" + " - \"swap\", \"échanger\", \"convertir\", \"changer\", \"exchange\"\n" + " - OU phrases comme \"eth en usdc\", \"bitcoin vers dai\", \"0.001 eth en usdc\"\n" + " - OU demandes directes de swap avec montant\n" + "2. EXTRACTION : Extrais ces informations du message :\n" + " - Le token source (ex: ETH, USDC, BTC)\n" + " - Le token destination (ex: USDC, DAI, WETH)\n" + " - Le montant numérique\n" + "3. ACTION IMMÉDIATE : Utilise DIRECTEMENT l'outil execute_swap avec ces paramètres.\n" + " - Pour ETH → USDC : execute_swap(\"ETH\", \"USDC\", montant, adresse_wallet)\n" + " - Pour USDC → DAI : execute_swap(\"USDC\", \"DAI\", montant, adresse_wallet)\n" + " - Si pas d'adresse wallet, utilise une adresse par défaut ou demande à l'utilisateur\n\n" + "IMPORTANT :\n" + "- NE DONNE JAMAIS d'explication préalable sur le swap\n" + "- N'INFORME PAS l'utilisateur des détails avant d'appeler l'outil\n" + "- NE DEMANDE JAMAIS de confirmation comme \"Souhaitez-vous continuer ?\"\n" + "- APPELLE execute_swap IMMÉDIATEMENT dès que tu détectes une demande de swap\n" + "- La modal d'interface se charge de tout afficher à l'utilisateur\n\n" + "Provide clear, helpful responses about crypto prices, transactions, and swaps." + ) + + def append_modules_and_context(self, system_prompt: str, context: str = "", modules: Optional[dict] = None) -> str: + """Append modules and context information to the system prompt consistently.""" + if modules: + active_modules = [name for name, active in modules.items() if active] + if active_modules: + system_prompt += f"\nModules activés: {', '.join(active_modules)}" + if context: + # Context may be plain text or JSON string from caller; keep as-is + label = "Contexte de conversation" if any(c in system_prompt for c in ["RÈGLE", "DÉTECTION"]) else "Conversation context" + system_prompt += f"\n{label}: {context}" + return system_prompt + + def detect_tool_choice(self, message: str) -> Union[str, Dict[str, Any]]: + """Detect if we should force a specific tool based on keywords, else return 'auto'.""" + lower_msg = message.lower() + transaction_keywords = ["envoie", "envoyer", "send", "transfer", "transférer", "payment", "pay"] + has_transaction_keyword = any(keyword in lower_msg for keyword in transaction_keywords) + has_address = "0x" in message and len([part for part in message.split() if part.startswith("0x") and len(part) == 42]) > 0 + has_amount = any(char.isdigit() for char in message) + is_likely_transaction = has_transaction_keyword and has_address and has_amount + + swap_keywords = ["swap", "échanger", "convertir", "changer", "exchange", "en usdc", "en dai", "vers usdc", "vers dai"] + has_swap_keyword = any(keyword in lower_msg for keyword in swap_keywords) + has_swap_amount = any(char.isdigit() for char in message) + is_likely_swap = has_swap_keyword and has_swap_amount + + if is_likely_transaction: + return {"type": "function", "function": {"name": "request_transaction"}} + if is_likely_swap: + return {"type": "function", "function": {"name": "execute_swap"}} + return "auto" + + def get_tools_schema(self) -> List[Dict[str, Any]]: + """Return the shared tools schema list used for chat.completions.create.""" + return [ + { + "type": "function", + "function": { + "name": "get_crypto_price", + "description": "Get real-time cryptocurrency price via CoinGecko API", + "parameters": { + "type": "object", + "properties": { + "crypto_id": { + "type": "string", + "description": "Cryptocurrency identifier (e.g. bitcoin, ethereum)" + }, + "currency": { + "type": "string", + "description": "Desired currency (e.g. eur, usd, gbp)", + "default": "usd" + } + }, + "required": ["crypto_id"] + } + } + }, + { + "type": "function", + "function": { + "name": "request_transaction", + "description": "Request a blockchain transaction", + "parameters": { + "type": "object", + "properties": { + "recipient_address": {"type": "string", "description": "Ethereum recipient address"}, + "amount": {"type": "string", "description": "Amount to send"}, + "currency": {"type": "string", "description": "EXACT word user said: If user says 'ETH' use 'ETH', if user says 'sepolia' use 'sepolia'. NEVER convert ETH to sepolia!"}, + "token_address": {"type": "string", "description": "Token contract address (optional)"} + }, + "required": ["recipient_address", "amount"] + } + } + }, + { + "type": "function", + "function": { + "name": "get_lifi_tokens", + "description": "Get available tokens from Li.Fi for swapping", + "parameters": { + "type": "object", + "properties": {"chains": {"type": "string", "description": "Comma-separated chain IDs to filter tokens (optional)"}}, + "required": [] + } + } + }, + { + "type": "function", + "function": { + "name": "get_swap_quote", + "description": "Get a swap quote from Li.Fi API for token swapping", + "parameters": { + "type": "object", + "properties": { + "from_token": {"type": "string", "description": "Source token symbol or address (e.g., ETH, USDC)"}, + "to_token": {"type": "string", "description": "Destination token symbol or address (e.g., USDC, DAI)"}, + "amount": {"type": "string", "description": "Amount to swap"}, + "from_address": {"type": "string", "description": "User's wallet address"}, + "from_chain": {"type": "string", "description": "Source blockchain ID (default: 1 for Ethereum)", "default": "1"}, + "to_chain": {"type": "string", "description": "Destination blockchain ID (default: 1 for Ethereum)", "default": "1"} + }, + "required": ["from_token", "to_token", "amount", "from_address"] + } + } + }, + { + "type": "function", + "function": { + "name": "execute_swap", + "description": "Execute a crypto swap using Li.Fi - generates transaction data for user to sign", + "parameters": { + "type": "object", + "properties": { + "from_token": {"type": "string", "description": "Source token symbol or address (e.g., ETH, USDC)"}, + "to_token": {"type": "string", "description": "Destination token symbol or address (e.g., USDC, DAI)"}, + "amount": {"type": "string", "description": "Amount to swap"}, + "from_address": {"type": "string", "description": "User's wallet address"}, + "from_chain": {"type": "string", "description": "Source blockchain ID (default: 1 for Ethereum)", "default": "1"}, + "to_chain": {"type": "string", "description": "Destination blockchain ID (default: 1 for Ethereum)", "default": "1"} + }, + "required": ["from_token", "to_token", "amount", "from_address"] + } + } + }, + { + "type": "function", + "function": { + "name": "get_sepolia_tokens", + "description": "Get available ERC-20 tokens on Sepolia testnet", + "parameters": {"type": "object", "properties": {}, "required": []} + } + }, + { + "type": "function", + "function": { + "name": "get_all_erc20_tokens", + "description": "Get all ERC-20 tokens for a specific chain", + "parameters": { + "type": "object", + "properties": {"chain_id": {"type": "string", "description": "Chain ID (e.g., 1 for Ethereum, 11155111 for Sepolia)", "default": "11155111"}}, + "required": [] + } + } + } + ] + diff --git a/crypto-pilot-builder/python/mcp_serveur/libertai_agent.py b/crypto-pilot-builder/python/mcp_serveur/libertai_agent.py new file mode 100644 index 0000000..25441fc --- /dev/null +++ b/crypto-pilot-builder/python/mcp_serveur/libertai_agent.py @@ -0,0 +1,177 @@ +#!/usr/bin/env python3 +""" +LibertAI Agent with crypto capabilities +""" +import asyncio +import sys +import os +import json +from dotenv import load_dotenv +from base_agent import BaseAgent +sys.path.append(os.path.dirname(__file__)) +from crypto_tools import get_crypto_price, request_transaction, get_lifi_tokens, get_swap_quote, execute_swap, get_sepolia_tokens, get_all_erc20_tokens +load_dotenv() + +class LibertAIAgent(BaseAgent): + """LibertAI Agent with crypto capabilities""" + + def __init__(self): + self.base_url = "https://api.libertai.io/v1" + self.model = "gemma-3-27b" + self.api_key = None + self.client = None + + def _get_client(self, api_key=None): + """Create LibertAI client (OpenAI-compatible)""" + try: + import openai + actual_api_key = api_key or os.getenv('LIBERTAI_API_KEY') + if not actual_api_key: + print("❌ Aucune clé API LibertAI fournie") + return None + print(f"🔑 Création du client LibertAI avec l'endpoint: {self.base_url}") + print(f"🔑 Clé API: {actual_api_key[:10]}...{actual_api_key[-4:] if len(actual_api_key) > 14 else '***'}") + client = openai.AsyncOpenAI( + api_key=actual_api_key, + base_url=self.base_url + ) + print("✅ Client LibertAI créé avec succès") + return client + except ImportError as e: + print(f"❌ Erreur d'import openai: {e}") + return None + except Exception as e: + print(f"❌ Erreur lors de la création du client LibertAI: {e}") + return None + + async def process_message(self, message: str, context: str = "") -> str: + """Process message with default configuration""" + try: + client = self._get_client() + if client is None: + return "❌ Aucune clé API LibertAI configurée. Veuillez utiliser agent_chat_configured avec votre clé API LibertAI." + system_prompt = self.get_shared_system_prompt() + if context: + system_prompt = self.append_modules_and_context(system_prompt, context) + result = await self._chat_with_libertai(message, system_prompt, self.model, client) + return result + except Exception as e: + return f"❌ LibertAI agent error: {str(e)}" + + async def process_message_with_config(self, message: str, context: str = "", + system_prompt: str = "", model: str = "hermes-3-8b", + api_key: str = "", modules: dict = None) -> str: + """Process message with custom configuration""" + try: + client = self._get_client(api_key) if api_key else self._get_client() + if not client: + return "❌ LibertAI client not configured. Please provide a valid API key." + if not system_prompt: + system_prompt = self.get_shared_system_prompt() + system_prompt = self.append_modules_and_context(system_prompt, context, modules) + result = await self._chat_with_libertai(message, system_prompt, model, client) + return result + except Exception as e: + return f"❌ Erreur agent LibertAI avec configuration: {str(e)}" + + async def _chat_with_libertai(self, message: str, system_prompt: str, model: str, client=None) -> str: + """Handle chat with LibertAI including tool calls""" + if client is None: + client = self._get_client() + if client is None: + return "❌ LibertAI client not configured." + try: + print(f"🔍 DEBUG LibertAI - Message: {message}") + tool_choice = self.detect_tool_choice(message) + print(f"🔍 DEBUG LibertAI - tool_choice: {tool_choice}") + try: + response = await client.chat.completions.create( + model=model, + messages=[ + {"role": "system", "content": system_prompt}, + {"role": "user", "content": message} + ], + max_tokens=500, + temperature=0.1, + tools=self.get_tools_schema(), + tool_choice=tool_choice + ) + if not response or not hasattr(response, 'choices') or not response.choices: + print(f"❌ LibertAI response invalide: {response}") + return "❌ Erreur: LibertAI n'a pas retourné de réponse valide. Vérifiez votre clé API et votre connexion." + except Exception as e: + print(f"❌ Erreur lors de l'appel LibertAI: {e}") + return f"❌ Erreur de communication avec LibertAI: {str(e)}" + + response_message = response.choices[0].message + if response_message.tool_calls: + tool_calls = response_message.tool_calls + tool_results = [] + for tool_call in tool_calls: + tool_name = tool_call.function.name + tool_args = json.loads(tool_call.function.arguments) + print(f"🔧 LibertAI Tool Call: {tool_name} with args: {tool_args}") + if tool_name == "get_crypto_price": + result = get_crypto_price(tool_args.get("crypto_id", ""), tool_args.get("currency", "usd")) + elif tool_name == "request_transaction": + result = request_transaction( + tool_args.get("recipient_address", ""), + tool_args.get("amount", ""), + tool_args.get("currency", "ETH"), + tool_args.get("token_address") + ) + elif tool_name == "get_lifi_tokens": + result = get_lifi_tokens(tool_args.get("chains")) + elif tool_name == "get_swap_quote": + result = get_swap_quote( + tool_args.get("from_token", ""), + tool_args.get("to_token", ""), + tool_args.get("amount", ""), + tool_args.get("from_address", ""), + tool_args.get("from_chain", "1"), + tool_args.get("to_chain", "1") + ) + elif tool_name == "execute_swap": + result = execute_swap( + tool_args.get("from_token", ""), + tool_args.get("to_token", ""), + tool_args.get("amount", ""), + tool_args.get("from_address", ""), + tool_args.get("from_chain", "1"), + tool_args.get("to_chain", "1") + ) + elif tool_name == "get_sepolia_tokens": + result = get_sepolia_tokens() + elif tool_name == "get_all_erc20_tokens": + result = get_all_erc20_tokens(tool_args.get("chain_id", "11155111")) + else: + result = f"❌ Unknown tool: {tool_name}" + tool_results.append(f"Tool {tool_name} result: {result}") + + messages = [ + {"role": "system", "content": system_prompt}, + {"role": "user", "content": message}, + response_message, + {"role": "tool", "content": "\n".join(tool_results)} + ] + try: + second_response = await client.chat.completions.create( + model=model, + messages=messages, + max_tokens=300, + temperature=0.1 + ) + if not second_response or not hasattr(second_response, 'choices') or not second_response.choices: + print(f"❌ LibertAI second response invalide: {second_response}") + return "❌ Erreur: LibertAI n'a pas retourné de réponse finale valide." + final_response = second_response.choices[0].message.content + return final_response + except Exception as e: + print(f"❌ Erreur lors du second appel LibertAI: {e}") + return f"❌ Erreur lors de la génération de la réponse finale: {str(e)}" + + direct_response = response_message.content + return direct_response + except Exception as e: + return f"❌ Error in LibertAI chat: {str(e)}" + diff --git a/crypto-pilot-builder/python/mcp_serveur/mcp_server_sdk.py b/crypto-pilot-builder/python/mcp_serveur/mcp_server_sdk.py index 1ad1694..e8938d1 100644 --- a/crypto-pilot-builder/python/mcp_serveur/mcp_server_sdk.py +++ b/crypto-pilot-builder/python/mcp_serveur/mcp_server_sdk.py @@ -14,9 +14,10 @@ from mcp.server.stdio import stdio_server sys.path.append(os.path.dirname(__file__)) from crypto_tools import get_crypto_price, request_transaction, get_lifi_tokens, get_swap_quote, execute_swap, get_sepolia_tokens, get_all_erc20_tokens +from base_agent import BaseAgent load_dotenv() -class OpenAIAgent: +class OpenAIAgent(BaseAgent): """OpenAI Agent with crypto capabilities""" def __init__(self): self.default_client = None # Client async @@ -38,53 +39,9 @@ async def process_message(self, message: str, context: str = "") -> str: client = self._get_default_client() if client is None: return "❌ Aucune clé API OpenAI configurée. Veuillez utiliser agent_chat_configured avec votre clé API." - system_prompt = """You are a crypto assistant with advanced capabilities including: -1. **Price Information**: Get real-time cryptocurrency prices using get_crypto_price -2. **Transactions**: Prepare blockchain transactions using request_transaction -3. **Token Information**: Get available ERC-20 tokens on Sepolia using get_sepolia_tokens -4. **Token Swapping**: Complete token swap capabilities using Li.Fi: - - get_lifi_tokens: Discover available tokens for swapping - - get_swap_quote: Get quotes for token swaps - - execute_swap: Execute token swaps with transaction data - -🎯 RÈGLE CRITIQUE pour request_transaction : - -COPIE EXACTEMENT le mot que dit l'utilisateur dans le paramètre "currency". NE TRADUIS PAS, NE CONVERTIS PAS. - -EXEMPLES CORRECTS : -- User: "envoie 0.001 ETH à 0x123..." → request_transaction(..., currency="ETH") -- User: "envoie 0.001 sepolia à 0x123..." → request_transaction(..., currency="sepolia") -- User: "envoie 5 USDC à 0x123..." → request_transaction(..., currency="USDC") - -❌ ERREUR FATALE (ne jamais faire) : -- User dit "ETH" mais tu mets currency="sepolia" ← INTERDIT ! - -✅ TOKENS DISPONIBLES : ETH, SEPOLIA, USDC, USDT, DAI, WETH, LINK, UNI - -RÈGLES CRITIQUES pour les swaps : -1. DÉTECTION : Si tu détectes l'un de ces mots-clés dans le message : - - "swap", "échanger", "convertir", "changer", "exchange" - - OU phrases comme "eth en usdc", "bitcoin vers dai", "0.001 eth en usdc" - - OU demandes directes de swap avec montant -2. EXTRACTION : Extrais ces informations du message : - - Le token source (ex: ETH, USDC, BTC) - - Le token destination (ex: USDC, DAI, WETH) - - Le montant numérique -3. ACTION IMMÉDIATE : Utilise DIRECTEMENT l'outil execute_swap avec ces paramètres. - - Pour ETH → USDC : execute_swap("ETH", "USDC", montant, adresse_wallet) - - Pour USDC → DAI : execute_swap("USDC", "DAI", montant, adresse_wallet) - - Si pas d'adresse wallet, utilise une adresse par défaut ou demande à l'utilisateur - -IMPORTANT : -- NE DONNE JAMAIS d'explication préalable sur le swap -- N'INFORME PAS l'utilisateur des détails avant d'appeler l'outil -- NE DEMANDE JAMAIS de confirmation comme "Souhaitez-vous continuer ?" -- APPELLE execute_swap IMMÉDIATEMENT dès que tu détectes une demande de swap -- La modal d'interface se charge de tout afficher à l'utilisateur - -Provide clear, helpful responses about crypto prices, transactions, and swaps.""" + system_prompt = self.get_shared_system_prompt() if context: - system_prompt += f"\nConversation context: {context}" + system_prompt = self.append_modules_and_context(system_prompt, context) result = await self._chat_with_openai(message, system_prompt, self.model, client) return result except Exception as e: @@ -97,57 +54,8 @@ async def process_message_with_config(self, message: str, context: str = "", try: client = self._get_default_client(api_key) if api_key else self._get_default_client() if not system_prompt: - system_prompt = """You are a crypto assistant with advanced capabilities including: -1. **Price Information**: Get real-time cryptocurrency prices using get_crypto_price -2. **Transactions**: Prepare blockchain transactions using request_transaction -3. **Token Information**: Get available ERC-20 tokens on Sepolia using get_sepolia_tokens -4. **Token Swapping**: Complete token swap capabilities using Li.Fi: - - get_lifi_tokens: Discover available tokens for swapping - - get_swap_quote: Get quotes for token swaps - - execute_swap: Execute token swaps with transaction data - -🎯 RÈGLE CRITIQUE pour request_transaction : - -COPIE EXACTEMENT le mot que dit l'utilisateur dans le paramètre "currency". NE TRADUIS PAS, NE CONVERTIS PAS. - -EXEMPLES CORRECTS : -- User: "envoie 0.001 ETH à 0x123..." → request_transaction(..., currency="ETH") -- User: "envoie 0.001 sepolia à 0x123..." → request_transaction(..., currency="sepolia") -- User: "envoie 5 USDC à 0x123..." → request_transaction(..., currency="USDC") - -❌ ERREUR FATALE (ne jamais faire) : -- User dit "ETH" mais tu mets currency="sepolia" ← INTERDIT ! - -✅ TOKENS DISPONIBLES : ETH, SEPOLIA, USDC, USDT, DAI, WETH, LINK, UNI - -RÈGLES CRITIQUES pour les swaps : -1. DÉTECTION : Si tu détectes l'un de ces mots-clés dans le message : - - "swap", "échanger", "convertir", "changer", "exchange" - - OU phrases comme "eth en usdc", "bitcoin vers dai", "0.001 eth en usdc" - - OU demandes directes de swap avec montant -2. EXTRACTION : Extrais ces informations du message : - - Le token source (ex: ETH, USDC, BTC) - - Le token destination (ex: USDC, DAI, WETH) - - Le montant numérique -3. ACTION IMMÉDIATE : Utilise DIRECTEMENT l'outil execute_swap avec ces paramètres. - - Pour ETH → USDC : execute_swap("ETH", "USDC", montant, adresse_wallet) - - Pour USDC → DAI : execute_swap("USDC", "DAI", montant, adresse_wallet) - - Si pas d'adresse wallet, utilise une adresse par défaut ou demande à l'utilisateur - -IMPORTANT : -- NE DONNE JAMAIS d'explication préalable sur le swap -- N'INFORME PAS l'utilisateur des détails avant d'appeler l'outil -- NE DEMANDE JAMAIS de confirmation comme "Souhaitez-vous continuer ?" -- APPELLE execute_swap IMMÉDIATEMENT dès que tu détectes une demande de swap -- La modal d'interface se charge de tout afficher à l'utilisateur - -Provide clear, helpful responses about crypto prices, transactions, and swaps.""" - if modules: - active_modules = [name for name, active in modules.items() if active] - if active_modules: - system_prompt += f"\nModules activés: {', '.join(active_modules)}" - if context: - system_prompt += f"\nContexte de conversation: {context}" + system_prompt = self.get_shared_system_prompt() + system_prompt = self.append_modules_and_context(system_prompt, context, modules) result = await self._chat_with_openai(message, system_prompt, model, client) return result except Exception as e: @@ -166,29 +74,21 @@ async def _chat_with_openai(self, message: str, system_prompt: str, model: str, has_address = "0x" in message and len([part for part in message.split() if part.startswith("0x") and len(part) == 42]) > 0 has_amount = any(char.isdigit() for char in message) is_likely_transaction = has_transaction_keyword and has_address and has_amount - + # Détection des swaps swap_keywords = ["swap", "échanger", "convertir", "changer", "exchange", "en usdc", "en dai", "vers usdc", "vers dai"] has_swap_keyword = any(keyword in message.lower() for keyword in swap_keywords) has_swap_amount = any(char.isdigit() for char in message) is_likely_swap = has_swap_keyword and has_swap_amount - + # Debug logs print(f"🔍 DEBUG - Message: {message}") print(f"🔍 DEBUG - has_swap_keyword: {has_swap_keyword}") print(f"🔍 DEBUG - has_swap_amount: {has_swap_amount}") print(f"🔍 DEBUG - is_likely_swap: {is_likely_swap}") print(f"🔍 DEBUG - is_likely_transaction: {is_likely_transaction}") - - tool_choice = "auto" - if is_likely_transaction: - tool_choice = {"type": "function", "function": {"name": "request_transaction"}} - print(f"🔍 DEBUG - Forcing request_transaction tool") - elif is_likely_swap: - tool_choice = {"type": "function", "function": {"name": "execute_swap"}} - print(f"🔍 DEBUG - Forcing execute_swap tool") - else: - print(f"🔍 DEBUG - Using auto tool choice") + + tool_choice = self.detect_tool_choice(message) response = await client.chat.completions.create( model=model, messages=[ @@ -399,12 +299,12 @@ async def _chat_with_openai(self, message: str, system_prompt: str, model: str, print(f"🔍 DEBUG - request_transaction called with args: {args}") currency = args.get("currency", "ETH") print(f"🔍 DEBUG - Currency extracted: {currency}") - + # SÉCURITÉ : Si l'IA met "sepolia" alors que l'utilisateur a dit "ETH", forcer "ETH" # (Cette logique sera enlevée quand l'IA apprendra) if currency.lower() == "sepolia": print(f"⚠️ WARNING - IA a mis 'sepolia', mais on garde comme ça pour l'instant") - + result = request_transaction( args.get("recipient_address", ""), args.get("amount", ""), @@ -504,8 +404,9 @@ async def _chat_with_openai(self, message: str, system_prompt: str, model: str, except Exception as e: return f"❌ Error in chat: {str(e)}" -# Agent instance -openai_agent = OpenAIAgent() +# Agent instance - using factory for future extensibility +from agent_factory import AgentFactory +openai_agent = AgentFactory.create_agent("openai") # MCP server instance server = Server("crypto-pilot-agent-server") @@ -659,14 +560,19 @@ async def handle_list_tools() -> list[Tool]: "type": "string", "description": "Custom system prompt" }, + "provider": { + "type": "string", + "description": "AI provider to use (openai, libertai)", + "default": "openai" + }, "model": { "type": "string", - "description": "OpenAI model to use", + "description": "AI model to use", "default": "gpt-4o-mini" }, "api_key": { "type": "string", - "description": "User's OpenAI API key" + "description": "User's API key for the selected provider" }, "modules": { "type": "string", @@ -699,7 +605,7 @@ async def handle_call_tool(name: str, arguments: dict | None) -> list[TextConten result = request_transaction(recipient_address, amount, currency, token_address) return [TextContent(type="text", text=result)] return [TextContent(type="text", text="❌ Missing required parameters: recipient_address, amount")] - + if name == "get_lifi_tokens": chains = arguments.get("chains", None) result = get_lifi_tokens(chains) @@ -746,6 +652,7 @@ async def handle_call_tool(name: str, arguments: dict | None) -> list[TextConten message = arguments.get("message", "") context = arguments.get("context", "") system_prompt = arguments.get("system_prompt", "") + provider = arguments.get("provider", "openai") model = arguments.get("model", "gpt-4o-mini") api_key = arguments.get("api_key", "") modules_str = arguments.get("modules", "{}") @@ -756,7 +663,12 @@ async def handle_call_tool(name: str, arguments: dict | None) -> list[TextConten modules = {} if not message: return [TextContent(type="text", text="❌ Empty message")] - result = await openai_agent.process_message_with_config( + + # Create agent based on provider + from agent_factory import AgentFactory + agent = AgentFactory.create_agent(provider) + + result = await agent.process_message_with_config( message=message, context=context, system_prompt=system_prompt, diff --git a/crypto-pilot-builder/python/mcp_serveur/mcp_server_sdk_old.py b/crypto-pilot-builder/python/mcp_serveur/mcp_server_sdk_old.py new file mode 100644 index 0000000..0a2cdfe --- /dev/null +++ b/crypto-pilot-builder/python/mcp_serveur/mcp_server_sdk_old.py @@ -0,0 +1,684 @@ +#!/usr/bin/env python3 +""" +MCP server with OpenAI agent and crypto tools +""" +import asyncio +import sys +import os +import openai +import json +from dotenv import load_dotenv +from mcp.server.models import InitializationOptions +from mcp.server import NotificationOptions, Server +from mcp.types import Tool, TextContent +from mcp.server.stdio import stdio_server +sys.path.append(os.path.dirname(__file__)) +from crypto_tools import get_crypto_price, request_transaction, get_lifi_tokens, get_swap_quote, execute_swap, get_sepolia_tokens, get_all_erc20_tokens +from base_agent import BaseAgent +from base_agent_impl import BaseAgentImpl +load_dotenv() + +class OpenAIAgent(BaseAgent, BaseAgentImpl): + """OpenAI Agent with crypto capabilities""" + def __init__(self): + BaseAgentImpl.__init__(self, "OpenAI", "gpt-4o-mini") + self.default_client = None # Client async + + def _get_client(self, api_key=None): + """Create OpenAI client""" + if self.default_client is None: + if api_key or os.getenv('OPENAI_API_KEY'): + actual_api_key = api_key or os.getenv('OPENAI_API_KEY') + self.default_client = openai.AsyncOpenAI(api_key=actual_api_key) + else: + return None + return self.default_client + + # All methods are now inherited from BaseAgentImpl + + # All chat logic is now inherited from BaseAgentImpl + try: + # Détection des transactions + transaction_keywords = ["envoie", "envoyer", "send", "transfer", "transférer", "payment", "pay"] + has_transaction_keyword = any(keyword in message.lower() for keyword in transaction_keywords) + has_address = "0x" in message and len([part for part in message.split() if part.startswith("0x") and len(part) == 42]) > 0 + has_amount = any(char.isdigit() for char in message) + is_likely_transaction = has_transaction_keyword and has_address and has_amount + + # Détection des swaps + swap_keywords = ["swap", "échanger", "convertir", "changer", "exchange", "en usdc", "en dai", "vers usdc", "vers dai"] + has_swap_keyword = any(keyword in message.lower() for keyword in swap_keywords) + has_swap_amount = any(char.isdigit() for char in message) + is_likely_swap = has_swap_keyword and has_swap_amount + + # Debug logs + print(f"🔍 DEBUG - Message: {message}") + print(f"🔍 DEBUG - has_swap_keyword: {has_swap_keyword}") + print(f"🔍 DEBUG - has_swap_amount: {has_swap_amount}") + print(f"🔍 DEBUG - is_likely_swap: {is_likely_swap}") + print(f"🔍 DEBUG - is_likely_transaction: {is_likely_transaction}") + + tool_choice = "auto" + if is_likely_transaction: + tool_choice = {"type": "function", "function": {"name": "request_transaction"}} + print(f"🔍 DEBUG - Forcing request_transaction tool") + elif is_likely_swap: + tool_choice = {"type": "function", "function": {"name": "execute_swap"}} + print(f"🔍 DEBUG - Forcing execute_swap tool") + else: + print(f"🔍 DEBUG - Using auto tool choice") + response = await client.chat.completions.create( + model=model, + messages=[ + {"role": "system", "content": system_prompt}, + {"role": "user", "content": message} + ], + max_tokens=500, + temperature=0.1, + tools=[ + { + "type": "function", + "function": { + "name": "get_crypto_price", + "description": "Get real-time cryptocurrency price via CoinGecko API", + "parameters": { + "type": "object", + "properties": { + "crypto_id": { + "type": "string", + "description": "Cryptocurrency identifier (e.g. bitcoin, ethereum)" + }, + "currency": { + "type": "string", + "description": "Desired currency (e.g. eur, usd, gbp)", + "default": "usd" + } + }, + "required": ["crypto_id"] + } + } + }, + { + "type": "function", + "function": { + "name": "get_lifi_tokens", + "description": "Get available tokens from Li.Fi for swapping", + "parameters": { + "type": "object", + "properties": { + "chains": { + "type": "string", + "description": "Comma-separated chain IDs to filter tokens (optional)" + } + }, + "required": [] + } + } + }, + { + "type": "function", + "function": { + "name": "get_swap_quote", + "description": "Get a swap quote from Li.Fi API for token swapping", + "parameters": { + "type": "object", + "properties": { + "from_token": { + "type": "string", + "description": "Source token symbol or address (e.g., ETH, USDC)" + }, + "to_token": { + "type": "string", + "description": "Destination token symbol or address (e.g., USDC, DAI)" + }, + "amount": { + "type": "string", + "description": "Amount to swap" + }, + "from_address": { + "type": "string", + "description": "User's wallet address" + }, + "from_chain": { + "type": "string", + "description": "Source blockchain ID (default: 1 for Ethereum)", + "default": "1" + }, + "to_chain": { + "type": "string", + "description": "Destination blockchain ID (default: 1 for Ethereum)", + "default": "1" + } + }, + "required": ["from_token", "to_token", "amount", "from_address"] + } + } + }, + { + "type": "function", + "function": { + "name": "execute_swap", + "description": "Execute a crypto swap using Li.Fi - generates transaction data for user to sign", + "parameters": { + "type": "object", + "properties": { + "from_token": { + "type": "string", + "description": "Source token symbol or address (e.g., ETH, USDC)" + }, + "to_token": { + "type": "string", + "description": "Destination token symbol or address (e.g., USDC, DAI)" + }, + "amount": { + "type": "string", + "description": "Amount to swap" + }, + "from_address": { + "type": "string", + "description": "User's wallet address" + }, + "from_chain": { + "type": "string", + "description": "Source blockchain ID (default: 1 for Ethereum)", + "default": "1" + }, + "to_chain": { + "type": "string", + "description": "Destination blockchain ID (default: 1 for Ethereum)", + "default": "1" + } + }, + "required": ["from_token", "to_token", "amount", "from_address"] + } + } + }, + { + "type": "function", + "function": { + "name": "request_transaction", + "description": "Request a blockchain transaction. Use this when user wants to send/transfer cryptocurrency.", + "parameters": { + "type": "object", + "properties": { + "recipient_address": { + "type": "string", + "description": "Recipient Ethereum address (0x...)" + }, + "amount": { + "type": "string", + "description": "Amount to send (e.g. 0.001)" + }, + "currency": { + "type": "string", + "description": "EXACT word user said: If user says 'ETH' use 'ETH', if user says 'sepolia' use 'sepolia', if user says 'USDC' use 'USDC'. NEVER convert ETH to sepolia!" + }, + "token_address": { + "type": "string", + "description": "ERC-20 token contract address (optional, for ERC-20 transactions)" + } + }, + "required": ["recipient_address", "amount"] + } + } + }, + { + "type": "function", + "function": { + "name": "get_sepolia_tokens", + "description": "Get list of available ERC-20 tokens on Sepolia testnet with their addresses and decimals.", + "parameters": { + "type": "object", + "properties": {}, + "required": [] + } + } + }, + { + "type": "function", + "function": { + "name": "get_all_erc20_tokens", + "description": "Get all available ERC-20 tokens dynamically from blockchain (Sepolia or Ethereum Mainnet). Uses Etherscan API to discover popular tokens.", + "parameters": { + "type": "object", + "properties": { + "chain_id": { + "type": "string", + "description": "Chain ID: '11155111' for Sepolia, '1' for Ethereum Mainnet", + "default": "11155111" + } + }, + "required": [] + } + } + } + ], + tool_choice=tool_choice + ) + response_message = response.choices[0].message + if hasattr(response_message, "tool_calls") and response_message.tool_calls: + print(f"🔍 DEBUG - Tool calls detected: {len(response_message.tool_calls)}") + tool_responses = [] + for tool_call in response_message.tool_calls: + tool_name = tool_call.function.name + args = json.loads(tool_call.function.arguments) + print(f"🔍 DEBUG - Tool called: {tool_name} with args: {args}") + if tool_name == "get_crypto_price": + result = get_crypto_price( + args.get("crypto_id", ""), + args.get("currency", "usd") + ) + tool_responses.append({ + "name": tool_name, + "content": result, + "tool_call_id": tool_call.id + }) + elif tool_name == "request_transaction": + print(f"🔍 DEBUG - request_transaction called with args: {args}") + currency = args.get("currency", "ETH") + print(f"🔍 DEBUG - Currency extracted: {currency}") + + # SÉCURITÉ : Si l'IA met "sepolia" alors que l'utilisateur a dit "ETH", forcer "ETH" + # (Cette logique sera enlevée quand l'IA apprendra) + if currency.lower() == "sepolia": + print(f"⚠️ WARNING - IA a mis 'sepolia', mais on garde comme ça pour l'instant") + + result = request_transaction( + args.get("recipient_address", ""), + args.get("amount", ""), + currency, + args.get("token_address", None) + ) + tool_responses.append({ + "name": tool_name, + "content": result, + "tool_call_id": tool_call.id + }) + elif tool_name == "get_lifi_tokens": + result = get_lifi_tokens( + args.get("chains", None) + ) + tool_responses.append({ + "name": tool_name, + "content": result, + "tool_call_id": tool_call.id + }) + elif tool_name == "get_swap_quote": + result = get_swap_quote( + args.get("from_token", ""), + args.get("to_token", ""), + args.get("amount", ""), + args.get("from_address", ""), + args.get("from_chain", "1"), + args.get("to_chain", "1") + ) + tool_responses.append({ + "name": tool_name, + "content": result, + "tool_call_id": tool_call.id + }) + elif tool_name == "get_sepolia_tokens": + result = get_sepolia_tokens() + tool_responses.append({ + "name": tool_name, + "content": result, + "tool_call_id": tool_call.id + }) + elif tool_name == "get_all_erc20_tokens": + chain_id = args.get("chain_id", "11155111") + result = get_all_erc20_tokens(chain_id) + tool_responses.append({ + "name": tool_name, + "content": result, + "tool_call_id": tool_call.id + }) + elif tool_name == "execute_swap": + print(f"🔍 DEBUG - execute_swap called with args: {args}") + result = execute_swap( + args.get("from_token", ""), + args.get("to_token", ""), + args.get("amount", ""), + args.get("from_address", ""), + args.get("from_chain", "11155111"), + args.get("to_chain", "11155111") + ) + print(f"🔍 DEBUG - execute_swap result: {result}") + tool_responses.append({ + "name": tool_name, + "content": result, + "tool_call_id": tool_call.id + }) + if tool_responses: + print(f"🔍 DEBUG - Tool responses: {[res['name'] for res in tool_responses]}") + for res in tool_responses: + if res["name"] == "request_transaction": + print(f"🔍 DEBUG - Returning request_transaction result directly") + return res["content"] + elif res["name"] == "execute_swap": + print(f"🔍 DEBUG - Returning execute_swap result directly") + return res["content"] + second_messages = [ + {"role": "system", "content": system_prompt}, + {"role": "user", "content": message}, + {"role": "assistant", "content": None, "tool_calls": response_message.tool_calls}, + ] + for res in tool_responses: + second_messages.append({ + "role": "tool", + "name": res["name"], + "content": res["content"], + "tool_call_id": res["tool_call_id"] + }) + second_response = await client.chat.completions.create( + model=model, + messages=second_messages, + max_tokens=500, + temperature=0.1 + ) + final_response = second_response.choices[0].message.content + return final_response + direct_response = response_message.content + return direct_response + except Exception as e: + return f"❌ Error in chat: {str(e)}" + +# Agent instance - using factory for future extensibility +from agent_factory import AgentFactory +openai_agent = AgentFactory.create_agent("openai") + +# MCP server instance +server = Server("crypto-pilot-agent-server") + +@server.list_tools() +async def handle_list_tools() -> list[Tool]: + """List available tools""" + return [ + Tool( + name="get_crypto_price", + description="Get real-time cryptocurrency price via CoinGecko", + inputSchema={ + "type": "object", + "properties": { + "crypto_id": { + "type": "string", + "description": "Cryptocurrency identifier" + }, + "currency": { + "type": "string", + "description": "Desired currency (default: usd)", + "default": "usd" + } + }, + "required": ["crypto_id"] + } + ), + Tool( + name="request_transaction", + description="Request a blockchain transaction", + inputSchema={ + "type": "object", + "properties": { + "recipient_address": { + "type": "string", + "description": "Ethereum recipient address" + }, + "amount": { + "type": "string", + "description": "Amount to send" + }, + "currency": { + "type": "string", + "description": "EXACT word user said: If user says 'ETH' use 'ETH', if user says 'sepolia' use 'sepolia'. NEVER convert ETH to sepolia!" + } + }, + "required": ["recipient_address", "amount"] + } + ), + Tool( + name="get_lifi_tokens", + description="Get available tokens from Li.Fi for swapping", + inputSchema={ + "type": "object", + "properties": { + "chains": { + "type": "string", + "description": "Comma-separated chain IDs to filter tokens (optional)" + } + }, + "required": [] + } + ), + Tool( + name="get_swap_quote", + description="Get a swap quote from Li.Fi API", + inputSchema={ + "type": "object", + "properties": { + "from_token": { + "type": "string", + "description": "Source token symbol or address (e.g., ETH, USDC)" + }, + "to_token": { + "type": "string", + "description": "Destination token symbol or address (e.g., USDC, DAI)" + }, + "amount": { + "type": "string", + "description": "Amount to swap" + }, + "from_address": { + "type": "string", + "description": "User's wallet address" + }, + "from_chain": { + "type": "string", + "description": "Source blockchain ID (default: 1 for Ethereum)", + "default": "1" + }, + "to_chain": { + "type": "string", + "description": "Destination blockchain ID (default: 1 for Ethereum)", + "default": "1" + } + }, + "required": ["from_token", "to_token", "amount", "from_address"] + } + ), + Tool( + name="execute_swap", + description="Execute a crypto swap using Li.Fi", + inputSchema={ + "type": "object", + "properties": { + "from_token": { + "type": "string", + "description": "Source token symbol or address (e.g., ETH, USDC)" + }, + "to_token": { + "type": "string", + "description": "Destination token symbol or address (e.g., USDC, DAI)" + }, + "amount": { + "type": "string", + "description": "Amount to swap" + }, + "from_address": { + "type": "string", + "description": "User's wallet address" + }, + "from_chain": { + "type": "string", + "description": "Source blockchain ID (default: 1 for Ethereum)", + "default": "1" + }, + "to_chain": { + "type": "string", + "description": "Destination blockchain ID (default: 1 for Ethereum)", + "default": "1" + } + }, + "required": ["from_token", "to_token", "amount", "from_address"] + } + ), + Tool( + name="agent_chat_configured", + description="Chat with AI agent using custom configuration", + inputSchema={ + "type": "object", + "properties": { + "message": { + "type": "string", + "description": "User message" + }, + "context": { + "type": "string", + "description": "Conversation context" + }, + "system_prompt": { + "type": "string", + "description": "Custom system prompt" + }, + "provider": { + "type": "string", + "description": "AI provider to use (openai, libertai)", + "default": "openai" + }, + "model": { + "type": "string", + "description": "AI model to use", + "default": "gpt-4o-mini" + }, + "api_key": { + "type": "string", + "description": "User's API key for the selected provider" + }, + "modules": { + "type": "string", + "description": "JSON string of enabled modules" + } + }, + "required": ["message"] + } + ) + ] + +@server.call_tool() +async def handle_call_tool(name: str, arguments: dict | None) -> list[TextContent]: + """Handle tool calls and agent communication""" + if arguments is None: + return [TextContent(type="text", text="❌ Invalid arguments")] + if name == "get_crypto_price": + crypto_id = arguments.get("crypto_id", "") + currency = arguments.get("currency", "usd") + if crypto_id: + result = get_crypto_price(crypto_id, currency) + return [TextContent(type="text", text=result)] + return [TextContent(type="text", text="❌ crypto_id required")] + if name == "request_transaction": + recipient_address = arguments.get("recipient_address", "") + amount = arguments.get("amount", "") + currency = arguments.get("currency", "ETH") + token_address = arguments.get("token_address", None) + if recipient_address and amount: + result = request_transaction(recipient_address, amount, currency, token_address) + return [TextContent(type="text", text=result)] + return [TextContent(type="text", text="❌ Missing required parameters: recipient_address, amount")] + + if name == "get_lifi_tokens": + chains = arguments.get("chains", None) + result = get_lifi_tokens(chains) + return [TextContent(type="text", text=result)] + + if name == "get_sepolia_tokens": + result = get_sepolia_tokens() + return [TextContent(type="text", text=result)] + + if name == "get_all_erc20_tokens": + chain_id = arguments.get("chain_id", "11155111") + result = get_all_erc20_tokens(chain_id) + return [TextContent(type="text", text=result)] + + if name == "get_swap_quote": + from_token = arguments.get("from_token", "") + to_token = arguments.get("to_token", "") + amount = arguments.get("amount", "") + from_address = arguments.get("from_address", "") + from_chain = arguments.get("from_chain", "1") + to_chain = arguments.get("to_chain", "1") + + if not all([from_token, to_token, amount, from_address]): + return [TextContent(type="text", text="❌ Missing required parameters: from_token, to_token, amount, from_address")] + + result = get_swap_quote(from_token, to_token, amount, from_address, from_chain, to_chain) + return [TextContent(type="text", text=result)] + + if name == "execute_swap": + from_token = arguments.get("from_token", "") + to_token = arguments.get("to_token", "") + amount = arguments.get("amount", "") + from_address = arguments.get("from_address", "") + from_chain = arguments.get("from_chain", "1") + to_chain = arguments.get("to_chain", "1") + + if not all([from_token, to_token, amount, from_address]): + return [TextContent(type="text", text="❌ Missing required parameters: from_token, to_token, amount, from_address")] + + result = execute_swap(from_token, to_token, amount, from_address, from_chain or "11155111", to_chain or "11155111") + return [TextContent(type="text", text=result)] + + if name == "agent_chat_configured": + message = arguments.get("message", "") + context = arguments.get("context", "") + system_prompt = arguments.get("system_prompt", "") + provider = arguments.get("provider", "openai") + model = arguments.get("model", "gpt-4o-mini") + api_key = arguments.get("api_key", "") + modules_str = arguments.get("modules", "{}") + modules = {} + try: + modules = json.loads(modules_str) if modules_str else {} + except: + modules = {} + if not message: + return [TextContent(type="text", text="❌ Empty message")] + + # Create agent based on provider + from agent_factory import AgentFactory + agent = AgentFactory.create_agent(provider) + + result = await agent.process_message_with_config( + message=message, + context=context, + system_prompt=system_prompt, + model=model, + api_key=api_key, + modules=modules + ) + return [TextContent(type="text", text=result)] + message = arguments.get("message", "") + context = arguments.get("context", "") + if not message: + message = name if name and name != "agent_chat" else "" + if not message: + return [TextContent(type="text", text="❌ Empty message")] + result = await openai_agent.process_message(message, context) + return [TextContent(type="text", text=result)] + +async def main(): + """Launch MCP server""" + async with stdio_server() as (read_stream, write_stream): + await server.run( + read_stream, + write_stream, + InitializationOptions( + server_name="crypto-pilot-agent-server", + server_version="1.0.0", + capabilities=server.get_capabilities( + notification_options=NotificationOptions(), + experimental_capabilities={}, + ), + ), + ) + +if __name__ == "__main__": + asyncio.run(main()) \ No newline at end of file diff --git a/crypto-pilot-builder/python/test_libertai.py b/crypto-pilot-builder/python/test_libertai.py new file mode 100644 index 0000000..a630707 --- /dev/null +++ b/crypto-pilot-builder/python/test_libertai.py @@ -0,0 +1,90 @@ +#!/usr/bin/env python3 +""" +Script de test pour vérifier la connexion LibertAI +""" +import asyncio +import os +from dotenv import load_dotenv + +async def test_libertai_connection(): + """Test de la connexion LibertAI""" + + # Clé API LibertAI (à remplacer par votre vraie clé) + api_key = "d56d770931ba135ae02b68ed32c8dcf4" + + if not api_key: + print("❌ Aucune clé API LibertAI trouvée") + return False + + print(f"🔑 Clé API trouvée: {api_key[:8]}...") + + try: + import openai + + # Créer le client LibertAI + client = openai.AsyncOpenAI( + api_key=api_key, + base_url="https://api.libertai.io/v1" + )< + + print("✅ Client LibertAI créé avec succès") + + # Test simple + print("🧪 Test de connexion avec LibertAI...") + + try: + response = await client.chat.completions.create( + model="hermes-3-8b", + messages=[ + {"role": "user", "content": "Hello! Can you respond with just 'OK'?"} + ], + max_tokens=10, + temperature=0.1 + ) + + print(f"📡 Réponse reçue: {response}") + print(f"📡 Type de réponse: {type(response)}") + + if response: + print(f"📡 Attributs de la réponse: {dir(response)}") + if hasattr(response, 'choices'): + print(f"📡 Choices: {response.choices}") + if response.choices: + print(f"📡 Premier choice: {response.choices[0]}") + if hasattr(response.choices[0], 'message'): + print(f"📡 Message: {response.choices[0].message}") + content = response.choices[0].message.content + print(f"✅ Contenu de la réponse: '{content}'") + return True + else: + print("❌ Pas d'attribut 'message' dans choices[0]") + else: + print("❌ Liste choices vide") + else: + print("❌ Pas d'attribut 'choices' dans la réponse") + else: + print("❌ Réponse None ou False") + + return False + + except Exception as api_error: + print(f"❌ Erreur API LibertAI: {api_error}") + print(f"❌ Type d'erreur: {type(api_error)}") + return False + + except ImportError as e: + print(f"❌ Module openai non installé: {e}") + return False + except Exception as e: + print(f"❌ Erreur lors du test: {e}") + return False + +if __name__ == "__main__": + print("🚀 Test de connexion LibertAI...") + + success = asyncio.run(test_libertai_connection()) + + if success: + print("🎉 Test réussi ! LibertAI fonctionne correctement.") + else: + print("💥 Test échoué. Vérifiez votre configuration.") diff --git a/crypto-pilot-builder/src/agent_building/Ai.vue b/crypto-pilot-builder/src/agent_building/Ai.vue index 749fd9d..b78042d 100644 --- a/crypto-pilot-builder/src/agent_building/Ai.vue +++ b/crypto-pilot-builder/src/agent_building/Ai.vue @@ -9,6 +9,36 @@

+
+ +
+ +
+ + + +
+
+
+