From 5560cf5fec274f3dbc4e9fceef3f792bd47b5be4 Mon Sep 17 00:00:00 2001 From: Jarrod Barnes Date: Wed, 15 Jan 2025 16:46:03 -0500 Subject: [PATCH] Enhance NEAR AI Agent Studio with improved documentation and interactive features - Updated README.md to provide a clearer overview of the NEAR AI Agent Studio - Refactored chat.py to introduce an enhanced interactive chat assistant with guided tutorials and improved validation for agent creation. - Added new commands for agent configuration and creation, allowing users to specify roles and capabilities directly. - Improved quickstart.sh script to streamline setup instructions and enhance user experience during the initial configuration. - Updated agent templates to include role-specific configurations and capabilities, facilitating easier agent development. --- README.md | 199 +-- agents/decision-maker.yaml | 30 + agents/price-monitor.yaml | 30 + near_swarm/agents/decision-maker/__init__.py | 7 + near_swarm/agents/decision-maker/agent.yaml | 40 + near_swarm/agents/decision-maker/plugin.py | 117 ++ near_swarm/agents/price-monitor/__init__.py | 7 + near_swarm/agents/price-monitor/agent.yaml | 38 + near_swarm/agents/price-monitor/plugin.py | 113 ++ near_swarm/cli/chat.py | 1140 +++++------------ near_swarm/cli/config.py | 78 +- near_swarm/cli/create.py | 61 +- near_swarm/cli/main.py | 150 +++ near_swarm/plugins/loader.py | 145 ++- .../templates/plugin_template/README.md | 192 +-- .../templates/plugin_template/agent.yaml | 58 +- .../templates/plugin_template/plugin.py | 122 +- plugins/decision-maker/plugin.py | 124 ++ plugins/price-monitor/plugin.py | 110 ++ public/near-agent-studio.png | Bin 0 -> 53640 bytes scripts/quickstart.sh | 80 +- 21 files changed, 1659 insertions(+), 1182 deletions(-) create mode 100644 agents/decision-maker.yaml create mode 100644 agents/price-monitor.yaml create mode 100644 near_swarm/agents/decision-maker/__init__.py create mode 100644 near_swarm/agents/decision-maker/agent.yaml create mode 100644 near_swarm/agents/decision-maker/plugin.py create mode 100644 near_swarm/agents/price-monitor/__init__.py create mode 100644 near_swarm/agents/price-monitor/agent.yaml create mode 100644 near_swarm/agents/price-monitor/plugin.py create mode 100644 plugins/decision-maker/plugin.py create mode 100644 plugins/price-monitor/plugin.py create mode 100644 public/near-agent-studio.png diff --git a/README.md b/README.md index 7222c73..9b333d1 100755 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # NEAR AI Agent Studio -A production-ready starter kit for building AI-powered agents and multi-agent swarms on NEAR. This template provides the essential building blocks for creating autonomous agents that can interact with the NEAR blockchain, make decisions using LLMs, and collaborate in swarms. +A production-ready starter kit for building AI agents and multi-agent swarms on NEAR. This template provides the essential building blocks for creating autonomous agents that can interact with the NEAR blockchain, make decisions using LLMs, and collaborate in swarms. The NEAR AI Agent Studio is an educational and interactive starter kit designed for developers looking to build AI agents and agentic applications on NEAR. [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![NEAR](https://img.shields.io/badge/NEAR-Protocol-blue.svg)](https://near.org) @@ -10,75 +10,7 @@ A production-ready starter kit for building AI-powered agents and multi-agent sw [![Hyperbolic](https://img.shields.io/badge/LLM-Hyperbolic-purple.svg)](https://hyperbolic.xyz) [![Lava Network](https://img.shields.io/badge/RPC-Lava%20Network-orange.svg)](https://www.lavanet.xyz/get-started/near) -## Table of Contents -1. [Overview](#overview) -2. [Features](#features) -3. [Getting Started](#-getting-started) -4. [Core Components](#-core-components) -5. [Interactive Features](#-interactive-features) -6. [Examples](#-examples) -7. [Documentation](#-documentation) -8. [Contributing](#-contributing) - -## Overview - -The NEAR AI Agent Studio is an educational and interactive starter kit designed for developers looking to build AI-powered applications on NEAR. It combines three powerful paradigms: -- šŸ§  Multi-agent swarm intelligence for collaborative decision-making -- šŸ—£ļø Voice-powered agents for portfolio management and market analysis -- šŸ’¬ Interactive chat with autonomous agents for onchain actions - -## šŸ”„ Features - -### šŸ§  Swarm Intelligence - -Swarm intelligence enables multiple specialized agents to collaborate for better outcomes: - -- **Market Analyzer** agents evaluate price data and trading volumes -- **Risk Manager** agents assess potential risks and exposure -- **Strategy Optimizer** agents fine-tune execution parameters - -These agents work together through: -1. Expertise-based evaluation -2. Confidence scoring -3. Transparent reasoning -4. Consensus building - -### šŸŽ™ļø Interactive Voice Assistant - -Natural language interaction with your NEAR portfolio: -- Real-time market analysis and insights -- Portfolio balance monitoring -- Transaction history tracking -- Voice-powered trading suggestions -- Market sentiment analysis - -### šŸ¤– Multi-Agent Strategy - -Watch specialized AI agents collaborate in real-time: - -1. **Market Analysis Phase** - - Price trend evaluation - - Volume analysis - - Market sentiment assessment - - Network monitoring - -2. **Risk Management Phase** - - Transaction risk assessment - - Portfolio exposure analysis - - Network security validation - - Gas optimization - -3. **Strategy Optimization Phase** - - Parameter fine-tuning - - Execution timing - - Slippage prediction - - Cost-benefit analysis - -4. **Consensus Building** - - Multi-agent voting - - Confidence scoring - - Detailed reasoning - - Transparent decisions +![NEAR AI Agent Studio](./public/near-agent-studio.png) ## āš”ļø Getting Started @@ -105,9 +37,6 @@ Before you begin, ensure you have: - Hyperbolic API key for LLM capabilities - Sign up at [hyperbolic.xyz](https://hyperbolic.xyz) - Free tier available for development -- ElevenLabs API key for voice features (optional) - - Register at [elevenlabs.io](https://elevenlabs.io) - - Free tier includes basic voice synthesis ### Quick Start ```bash @@ -127,6 +56,26 @@ chmod +x scripts/quickstart.sh # Make script executable ./scripts/quickstart.sh ``` +The quickstart script will: +1. Set up your development environment +2. Create a NEAR testnet account +3. Install example agents +4. Launch an interactive chat assistant to help you create your first agent + +### Interactive Chat +After setup, you'll enter an interactive chat session where you can: +- Create new agents with `/create agent ` +- Configure agents with `/config agent ` +- Run multiple agents together with `/run-agents` +- List available agents with `/list` +- Get help anytime with `/help` + +Start the chat manually anytime: +```bash +near-swarm chat # Regular mode +near-swarm chat --tutorial create-first-agent # Guided tutorial +``` + ### Environment Setup ```bash # Copy environment template @@ -138,53 +87,7 @@ cp .env.example .env # - NEAR_PRIVATE_KEY=your-private-key # - LLM_PROVIDER=hyperbolic # - LLM_API_KEY=your-api-key -# Optional: -# - ELEVENLABS_API_KEY=your-key # For voice features - -# Verify installation -near-swarm config validate ``` - -### Running Demos -```bash -# Run all demos -python near_swarm/examples/demo.py all - -# Run specific components -python near_swarm/examples/demo.py voice # Voice assistant -python near_swarm/examples/demo.py strategy # Multi-agent strategy -python near_swarm/examples/demo.py chat # Interactive chat - -# Create your first agent -near-swarm create agent my-agent -near-swarm plugins list # View available plugins -``` - -### Troubleshooting - -If you encounter any issues: - -1. Ensure all dependencies are installed: -```bash -pip install -e ".[dev]" # Install with development dependencies -``` - -2. Verify your Python version: -```bash -python --version # Should be 3.12 or higher -``` - -3. Check your configuration: -```bash -near-swarm config show # View current configuration -``` - -4. Common issues: -- "Command not found": Ensure virtual environment is activated -- Import errors: Verify installation with `pip list` -- API errors: Check your API keys in `.env` -- Git errors: Install git with `apt install git` or `brew install git` - ## Core Components > **Tip**: Start with modifying the examples in `near_swarm/examples/` to understand the framework. @@ -197,12 +100,12 @@ near-swarm config show # View current configuration ```mermaid graph TB %% Core System - Core[Plugin System] + Core[Agent Framework] style Core fill:black,stroke:#00C1DE,stroke-width:3px,color:white %% Plugin Management - Registry[Plugin Registry] - Loader[Plugin Loader] + Registry[Agent Registry] + Loader[Agent Loader] Config[Configuration] style Registry fill:black,stroke:#00C1DE,stroke-width:2px,color:white style Loader fill:black,stroke:#00C1DE,stroke-width:2px,color:white @@ -259,11 +162,11 @@ graph TB SwarmAgent --> Evaluation ``` -The architecture combines a flexible plugin system with swarm intelligence capabilities: +The architecture combines a flexible agent framework with swarm intelligence capabilities: -1. **Plugin System Core** - - Plugin Registry for managing available plugins - - Plugin Loader for dynamic loading/unloading +1. **Agent Framework Core** + - Agent Registry for managing available agents + - Agent Loader for dynamic loading/unloading - Configuration management with validation 2. **Core Services** @@ -272,9 +175,9 @@ The architecture combines a flexible plugin system with swarm intelligence capab - LLM provider interface 3. **Agent Plugins** - - Token Transfer plugin for NEAR transactions - - Arbitrage plugin for market opportunities - - Custom plugins for specialized strategies + - Token Transfer agent for NEAR transactions + - Arbitrage agent for market opportunities + - Custom agents for specialized strategies 4. **Swarm Intelligence** - Swarm Agent for coordinated decision-making @@ -282,7 +185,7 @@ The architecture combines a flexible plugin system with swarm intelligence capab - Role-based evaluation with LLM reasoning This architecture enables: -- Easy extension through plugins +- Easy extension through agent plugins - Coordinated decision-making via swarm intelligence - Secure transaction handling - Market-aware operations @@ -358,42 +261,6 @@ peer_agent = SwarmAgent(config, SwarmConfig(role="risk_manager")) await main_agent.join_swarm([peer_agent]) ``` -## šŸ“š Interactive Features - -### Voice Commands -- Portfolio queries -- Market analysis -- Transaction requests -- Strategy suggestions - -### Chat Commands -- Market Analysis - ```bash - /market [symbol] # Get market analysis - /trend [timeframe] # Get trend analysis - /volume [symbol] # Volume analysis - ``` - -- Risk Management - ```bash - /risk [action] # Risk assessment - /balance # Check portfolio balance - /positions # List open positions - ``` - -- Strategy - ```bash - /strategy [action] # Strategy suggestions - /portfolio # Portfolio overview - ``` - -- Development Tools - ```bash - /ws # Manage workspace - /env # Configure environment - /config # View/modify settings - ``` - ## šŸ“„ Examples For more examples and reference implementations, check out our [examples directory](near_swarm/examples/): diff --git a/agents/decision-maker.yaml b/agents/decision-maker.yaml new file mode 100644 index 0000000..72b2755 --- /dev/null +++ b/agents/decision-maker.yaml @@ -0,0 +1,30 @@ +name: decision-maker +environment: development +log_level: INFO + +llm: + provider: hyperbolic + api_key: ${LLM_API_KEY} + model: ${LLM_MODEL} + temperature: 0.7 + max_tokens: 2000 + api_url: ${LLM_API_URL} + +plugins: + - name: decision-maker + role: strategy_optimizer + capabilities: + - strategy_optimization + - decision_making + - risk_management + custom_settings: + min_confidence_threshold: 0.8 + risk_tolerance: medium + max_retries: 3 + timeout: 30 + decision_interval: 300 + risk_threshold: 0.1 + +custom_settings: + environment: development + log_level: INFO \ No newline at end of file diff --git a/agents/price-monitor.yaml b/agents/price-monitor.yaml new file mode 100644 index 0000000..ba7f4a7 --- /dev/null +++ b/agents/price-monitor.yaml @@ -0,0 +1,30 @@ +name: price-monitor +environment: development +log_level: INFO + +llm: + provider: hyperbolic + api_key: ${LLM_API_KEY} + model: ${LLM_MODEL} + temperature: 0.7 + max_tokens: 2000 + api_url: ${LLM_API_URL} + +plugins: + - name: price-monitor + role: market_analyzer + capabilities: + - price_monitoring + - trend_analysis + - market_assessment + custom_settings: + min_confidence_threshold: 0.7 + risk_tolerance: medium + max_retries: 3 + timeout: 30 + update_interval: 60 + alert_threshold: 0.05 + +custom_settings: + environment: development + log_level: INFO \ No newline at end of file diff --git a/near_swarm/agents/decision-maker/__init__.py b/near_swarm/agents/decision-maker/__init__.py new file mode 100644 index 0000000..e79a8bc --- /dev/null +++ b/near_swarm/agents/decision-maker/__init__.py @@ -0,0 +1,7 @@ +""" +Decision Making Agent Package +""" + +from .plugin import DecisionMakerPlugin + +__all__ = ['DecisionMakerPlugin'] \ No newline at end of file diff --git a/near_swarm/agents/decision-maker/agent.yaml b/near_swarm/agents/decision-maker/agent.yaml new file mode 100644 index 0000000..7eb8861 --- /dev/null +++ b/near_swarm/agents/decision-maker/agent.yaml @@ -0,0 +1,40 @@ +# Decision Maker Agent Configuration +name: decision-maker +environment: development +log_level: INFO + +# LLM Configuration +llm: + provider: hyperbolic + model: deepseek-ai/DeepSeek-V3 + api_key: ${LLM_API_KEY} + temperature: 0.7 + max_tokens: 2000 + api_url: https://api.hyperbolic.xyz/v1 + system_prompt: | + You are a decision-making agent in the NEAR swarm. + Evaluate market opportunities and make strategic decisions. + Always respond in JSON format with confidence levels. + +# Agent Settings +custom_settings: + min_confidence_threshold: 0.7 + risk_tolerance: medium + max_retries: 3 + timeout: 30 + +# Plugin Configuration +plugins: + - name: decision-maker + role: strategy_optimizer + capabilities: + - strategy_optimization + - decision_making + - risk_management + custom_settings: + min_confidence_threshold: 0.7 + risk_tolerance: medium + max_retries: 3 + timeout: 30 + min_profit_threshold: 0.002 # 0.2% minimum profit + max_position_size: 10000 # Maximum position size in USD \ No newline at end of file diff --git a/near_swarm/agents/decision-maker/plugin.py b/near_swarm/agents/decision-maker/plugin.py new file mode 100644 index 0000000..c0eaceb --- /dev/null +++ b/near_swarm/agents/decision-maker/plugin.py @@ -0,0 +1,117 @@ +""" +Decision Making Agent Plugin +Makes strategic decisions based on market analysis +""" + +from typing import Dict, Any, Optional +from near_swarm.plugins.base import AgentPlugin +from near_swarm.core.llm_provider import create_llm_provider, LLMConfig +from near_swarm.plugins import register_plugin +import logging + +logger = logging.getLogger(__name__) + +class DecisionMakerPlugin(AgentPlugin): + """Decision making agent implementation""" + + async def initialize(self) -> None: + """Initialize plugin resources""" + # Initialize LLM provider + llm_config = LLMConfig( + provider=self.agent_config.llm.provider, + api_key=self.agent_config.llm.api_key, + model=self.agent_config.llm.model, + temperature=self.agent_config.llm.temperature, + max_tokens=self.agent_config.llm.max_tokens, + api_url=self.agent_config.llm.api_url, + system_prompt="""You are a strategic decision-making agent in the NEAR ecosystem. +Your role is to evaluate market opportunities and make risk-managed decisions. + +Key responsibilities: +1. Evaluate market analysis and opportunities +2. Calculate optimal position sizes +3. Manage risk exposure +4. Make strategic recommendations + +Always provide your decisions in a structured format with: +- Analysis: Your evaluation of the situation +- Strategy: Your recommended course of action +- Rationale: Detailed reasoning behind the decision +- Risk: Assessment of potential risks +- Confidence: Your confidence level (0-1)""" + ) + self.llm_provider = create_llm_provider(llm_config) + + # Load custom settings + self.min_confidence = self.plugin_config.settings.get( + 'min_confidence_threshold', 0.7 + ) + self.risk_tolerance = self.plugin_config.settings.get( + 'risk_tolerance', 'medium' + ) + self.max_position_size = self.plugin_config.settings.get( + 'max_position_size', 100000 + ) + + async def evaluate(self, context: Dict[str, Any]) -> Dict[str, Any]: + """Evaluate and make strategic decisions""" + if not self.llm_provider: + raise RuntimeError("Plugin not initialized") + + # Extract data from context + market_analysis = context.get('market_analysis', {}) + current_positions = context.get('positions', []) + request = context.get('request', '') + + # Create decision prompt + prompt = f"""Evaluate the current market situation and make strategic decisions: + +Market Analysis: {market_analysis} +Current Positions: {current_positions} +Risk Tolerance: {self.risk_tolerance} +Max Position Size: {self.max_position_size} + +{request} + +Provide your decision in JSON format with: +- analysis: Your evaluation of the situation +- strategy: Recommended course of action +- rationale: Detailed reasoning +- risk: Risk assessment +- confidence: Your confidence level (0-1) +""" + + # Get LLM decision + try: + response = await self.llm_provider.query(prompt, expect_json=True) + + # Validate confidence threshold + if response.get('confidence', 0) < self.min_confidence: + response['strategy'] = "Hold - Confidence too low for action" + + return response + + except Exception as e: + logger.error(f"Error during decision making: {e}") + return { + "analysis": "Error during analysis", + "strategy": "Unable to make decision", + "rationale": str(e), + "risk": "Unknown - Analysis failed", + "confidence": 0 + } + + async def execute(self, operation: Optional[str] = None, **kwargs) -> Any: + """Execute plugin operation""" + if operation == "decide": + return await self.evaluate(kwargs) + else: + raise ValueError(f"Unsupported operation: {operation}") + + async def cleanup(self) -> None: + """Cleanup plugin resources""" + if self.llm_provider: + await self.llm_provider.close() + +# Register the plugin +register_plugin("decision-maker", DecisionMakerPlugin) \ No newline at end of file diff --git a/near_swarm/agents/price-monitor/__init__.py b/near_swarm/agents/price-monitor/__init__.py new file mode 100644 index 0000000..4532332 --- /dev/null +++ b/near_swarm/agents/price-monitor/__init__.py @@ -0,0 +1,7 @@ +""" +Price Monitoring Agent Package +""" + +from .plugin import PriceMonitorPlugin + +__all__ = ['PriceMonitorPlugin'] \ No newline at end of file diff --git a/near_swarm/agents/price-monitor/agent.yaml b/near_swarm/agents/price-monitor/agent.yaml new file mode 100644 index 0000000..ccf0bda --- /dev/null +++ b/near_swarm/agents/price-monitor/agent.yaml @@ -0,0 +1,38 @@ +# Price Monitor Agent Configuration +name: price-monitor +environment: development +log_level: INFO + +# LLM Configuration +llm: + provider: hyperbolic + model: deepseek-ai/DeepSeek-V3 + api_key: ${LLM_API_KEY} + temperature: 0.7 + max_tokens: 2000 + api_url: https://api.hyperbolic.xyz/v1 + system_prompt: | + You are a price monitoring agent in the NEAR swarm. + Analyze market conditions and provide clear insights. + Always respond in JSON format with confidence levels. + +# Agent Settings +custom_settings: + min_confidence_threshold: 0.7 + risk_tolerance: medium + max_retries: 3 + timeout: 30 + +# Plugin Configuration +plugins: + - name: price-monitor + role: market_analyzer + capabilities: + - market_analysis + - price_monitoring + - risk_assessment + custom_settings: + min_confidence_threshold: 0.7 + risk_tolerance: medium + max_retries: 3 + timeout: 30 \ No newline at end of file diff --git a/near_swarm/agents/price-monitor/plugin.py b/near_swarm/agents/price-monitor/plugin.py new file mode 100644 index 0000000..abf073d --- /dev/null +++ b/near_swarm/agents/price-monitor/plugin.py @@ -0,0 +1,113 @@ +""" +Price Monitoring Agent Plugin +Analyzes NEAR market conditions and price movements +""" + +from typing import Dict, Any, Optional +from near_swarm.plugins.base import AgentPlugin +from near_swarm.core.llm_provider import create_llm_provider, LLMConfig +from near_swarm.plugins import register_plugin +import logging + +logger = logging.getLogger(__name__) + +class PriceMonitorPlugin(AgentPlugin): + """Price monitoring agent implementation""" + + async def initialize(self) -> None: + """Initialize plugin resources""" + # Initialize LLM provider + llm_config = LLMConfig( + provider=self.agent_config.llm.provider, + api_key=self.agent_config.llm.api_key, + model=self.agent_config.llm.model, + temperature=self.agent_config.llm.temperature, + max_tokens=self.agent_config.llm.max_tokens, + api_url=self.agent_config.llm.api_url, + system_prompt="""You are a specialized market analysis agent in the NEAR ecosystem. +Your role is to analyze market conditions, identify trends, and provide trading insights. + +Key responsibilities: +1. Analyze price movements and market trends +2. Assess market sentiment and volatility +3. Identify potential trading opportunities +4. Provide risk-adjusted recommendations + +Always provide your analysis in a structured format with: +- Observation: Current market conditions and notable patterns +- Reasoning: Your analysis process and key factors considered +- Conclusion: Clear summary of findings +- Confidence: Your confidence level (0-1)""" + ) + self.llm_provider = create_llm_provider(llm_config) + + # Load custom settings + self.min_confidence = self.plugin_config.settings.get( + 'min_confidence_threshold', 0.7 + ) + self.max_lookback = self.plugin_config.settings.get( + 'max_lookback_periods', 30 + ) + self.risk_tolerance = self.plugin_config.settings.get( + 'risk_tolerance', 'medium' + ) + + async def evaluate(self, context: Dict[str, Any]) -> Dict[str, Any]: + """Evaluate market conditions""" + if not self.llm_provider: + raise RuntimeError("Plugin not initialized") + + # Extract data from context + current_price = context.get('price', 0) + timestamp = context.get('timestamp', 0) + request = context.get('request', '') + + # Create analysis prompt + prompt = f"""Analyze the current NEAR market conditions: + +Current Price: ${current_price:.2f} +Timestamp: {timestamp} +Risk Tolerance: {self.risk_tolerance} + +{request} + +Provide your analysis in JSON format with: +- observation: Your observations of current conditions +- reasoning: Your detailed analysis process +- conclusion: Clear summary and recommendations +- confidence: Your confidence level (0-1) +""" + + # Get LLM analysis + try: + response = await self.llm_provider.query(prompt, expect_json=True) + + # Validate confidence threshold + if response.get('confidence', 0) < self.min_confidence: + response['conclusion'] = "Confidence too low for definitive recommendation. Need more data." + + return response + + except Exception as e: + logger.error(f"Error during market analysis: {e}") + return { + "observation": "Error during analysis", + "reasoning": str(e), + "conclusion": "Analysis failed", + "confidence": 0 + } + + async def execute(self, operation: Optional[str] = None, **kwargs) -> Any: + """Execute plugin operation""" + if operation == "analyze": + return await self.evaluate(kwargs) + else: + raise ValueError(f"Unsupported operation: {operation}") + + async def cleanup(self) -> None: + """Cleanup plugin resources""" + if self.llm_provider: + await self.llm_provider.close() + +# Register the plugin +register_plugin("price-monitor", PriceMonitorPlugin) \ No newline at end of file diff --git a/near_swarm/cli/chat.py b/near_swarm/cli/chat.py index 5cd95d1..39d671c 100644 --- a/near_swarm/cli/chat.py +++ b/near_swarm/cli/chat.py @@ -1,845 +1,405 @@ """ -Chat Command Module -Implements interactive chat functionality for NEAR Swarm agents +Interactive chat interface for NEAR Agent Studio. +Provides guided tutorials and agent creation assistance with enhanced validation. """ -import os -import sys -import cmd -import json import asyncio -import logging -from typing import Optional, Dict, Any, List -from enum import Enum -import typer -from pydantic import BaseModel, Field +import click +import subprocess +import os from prompt_toolkit import PromptSession from prompt_toolkit.styles import Style -from prompt_toolkit.formatted_text import HTML -from rich.console import Console -from rich.panel import Panel -from rich.markdown import Markdown -from prompt_toolkit.history import InMemoryHistory - -from near_swarm.core.agent import AgentConfig -from near_swarm.core.swarm_agent import SwarmAgent, SwarmConfig +from typing import Optional, Dict, Any, List, Tuple +import yaml +import logging +from dataclasses import dataclass + +from near_swarm.plugins import PluginLoader +from near_swarm.core.llm_provider import create_llm_provider, LLMConfig from near_swarm.core.market_data import MarketDataManager -# Configure logging -logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) -# Rich console for pretty printing -console = Console() - -class AgentType(str, Enum): - """Available agent types""" - CHAT_ASSISTANT = "chat_assistant" # Default conversational agent - MARKET_ANALYZER = "market_analyzer" - RISK_MANAGER = "risk_manager" - STRATEGY_OPTIMIZER = "strategy_optimizer" - -# System prompts for different agent types -AGENT_PROMPTS = { - AgentType.CHAT_ASSISTANT: """You are a helpful AI assistant for the NEAR Swarm Intelligence framework. -Your role is to: -1. Help users understand and use the system's capabilities -2. Provide clear explanations of available commands and features -3. Guide users through complex operations -4. Answer questions about the NEAR ecosystem and trading concepts -5. Maintain a friendly, conversational tone while being informative - -You have access to: -- Market analysis tools (/market, /trend, /volume) -- Risk management features (/risk, /balance, /positions) -- Strategy optimization (/strategy, /portfolio) -- System management (/agents, /workspace, /config) - -When users need specific analysis or operations, you can: -1. Explain which specialized agent would be best suited -2. Help them switch to that agent using the /agents command -3. Guide them through using the appropriate commands - -Remember to: -- Be conversational and engaging -- Provide context for technical terms -- Suggest relevant commands when appropriate -- Help users understand the system's architecture""", - AgentType.MARKET_ANALYZER: """You are a market analysis expert...""", # Existing prompt - AgentType.RISK_MANAGER: """You are a risk management specialist...""", # Existing prompt - AgentType.STRATEGY_OPTIMIZER: """You are a trading strategy expert...""" # Existing prompt -} - -# Prompt styling -style = Style.from_dict({ - 'prompt': '#00aa00 bold', - 'agent': '#00aa00', - 'command': '#884444', - 'error': '#ff0000' -}) - -# Response Models -class MarketAnalysis(BaseModel): - """Structured market analysis response""" - price: float = Field(..., description="Current price of the token") - sentiment: str = Field(..., description="Market sentiment: bullish, bearish, or neutral") - volume_24h: str = Field(..., description="24-hour trading volume") - trend: str = Field(..., description="Current market trend") - confidence: float = Field(..., description="Confidence score of the analysis", ge=0, le=1) - reasoning: str = Field(..., description="Detailed reasoning for the analysis") - recommendations: List[str] = Field(..., description="List of actionable recommendations") - -class RiskAssessment(BaseModel): - """Structured risk assessment response""" - risk_level: str = Field(..., description="Overall risk level: low, medium, high") - exposure: float = Field(..., description="Current portfolio exposure") - max_drawdown: float = Field(..., description="Maximum potential drawdown") - risk_factors: List[Dict[str, float]] = Field(..., description="Identified risk factors and their weights") - mitigation_strategies: List[str] = Field(..., description="Suggested risk mitigation strategies") - -class StrategyProposal(BaseModel): - """Structured strategy proposal""" - name: str = Field(..., description="Strategy name") - type: str = Field(..., description="Strategy type: arbitrage, trend-following, etc.") - timeframe: str = Field(..., description="Recommended timeframe") - entry_conditions: List[str] = Field(..., description="Entry conditions") - exit_conditions: List[str] = Field(..., description="Exit conditions") - risk_parameters: Dict[str, float] = Field(..., description="Risk management parameters") - expected_returns: str = Field(..., description="Expected returns estimate") - code_example: Optional[str] = Field(None, description="Example implementation code") - -class WorkspaceConfig(BaseModel): - """Workspace configuration""" - name: str = Field(..., description="Workspace name") - description: str = Field(None, description="Workspace description") - agents: List[str] = Field(default_factory=list, description="Active agents in workspace") - strategies: List[str] = Field(default_factory=list, description="Available strategies") - environment: Dict[str, str] = Field(default_factory=dict, description="Environment variables") - settings: Dict[str, Any] = Field(default_factory=dict, description="Additional settings") - -class SwarmChat(cmd.Cmd): - """Interactive chat interface for NEAR Swarm agents.""" - - intro = """ -šŸ¤– Welcome to NEAR Swarm Intelligence Chat! -Type 'help' or '?' to list commands. -Type 'quit' or 'exit' to exit. +@dataclass +class EnvState: + """Environment state tracking""" + near_account: Optional[str] = None + network: str = "testnet" + llm_provider: str = "hyperbolic" + initialized: bool = False -Available Commands: -- /market [symbol] : Get market analysis -- /trend [timeframe] : Get trend analysis -- /volume [symbol] : Volume analysis -- /risk [action] : Risk assessment -- /strategy [action] : Strategy suggestions -- /portfolio : Portfolio overview -- /agents : List active agents -- /balance : Check portfolio balance -- /positions : List open positions -""" +class EnhancedChatAssistant: + """Enhanced chat assistant with validation and monitoring.""" - def __init__(self, agent_type: str = "chat_assistant", verbose: bool = False, json_output: bool = False): - """Initialize chat interface.""" - super().__init__() - self.agent_type = agent_type - self.verbose = verbose - self.json_output = json_output - self.agent: Optional[SwarmAgent] = None - self.market_data: Optional[MarketDataManager] = None - self.session = PromptSession(history=InMemoryHistory()) - self.prompt = self._get_prompt() - self.history: list[Dict[str, Any]] = [] - self.reasoning_enabled: bool = False - self.workspace: Optional[WorkspaceConfig] = None + def __init__(self, tutorial_mode: Optional[str] = None): + self.tutorial_mode = tutorial_mode + self.session = PromptSession() + self.style = Style.from_dict({ + 'prompt': '#00ffff bold', + 'error': '#ff0000 bold', + 'success': '#00ff00 bold', + 'info': '#ffffff', + 'header': '#00b0ff bold' + }) + self.plugin_loader = PluginLoader() + self.env_state = EnvState() + self.metrics = { + 'messages_exchanged': 0, + 'decisions_made': 0, + 'analysis_completed': 0 + } + + async def start(self) -> None: + """Entry point with enhanced validation.""" + click.echo(click.style("\nšŸš€ Initializing NEAR AI Agent Studio...", fg='bright_blue')) - # Set appropriate intro based on agent type - if agent_type == AgentType.CHAT_ASSISTANT: - self.intro = """ -šŸ‘‹ Welcome to NEAR Swarm Intelligence! I'm your AI assistant. -I'm here to help you navigate the system and answer any questions you have. - -You can: -- Chat with me naturally about any topic -- Use commands (type 'help' to see them) -- Switch to specialized agents for specific tasks - -How can I assist you today? -""" - else: - self.intro = """ -šŸ¤– Welcome to NEAR Swarm Intelligence Chat! -Type 'help' or '?' to list commands. -Type 'quit' or 'exit' to exit. + # Run validation suite + if not await self._run_validation_suite(): + return -Available Commands: -- /market [symbol] : Get market analysis -- /trend [timeframe] : Get trend analysis -- /volume [symbol] : Volume analysis -- /risk [action] : Risk assessment -- /strategy [action] : Strategy suggestions -- /portfolio : Portfolio overview -- /agents : List active agents -- /balance : Check portfolio balance -- /positions : List open positions -""" + if self.tutorial_mode: + await self.run_enhanced_tutorial(self.tutorial_mode) + else: + await self.run_interactive() + + async def _run_validation_suite(self) -> bool: + """Run complete validation suite with progress tracking.""" + validations = [ + ("Environment", self.validate_environment), + ("Plugins", self.verify_plugins), + ("Agent Configs", self.agent_config_check), + ("Communication", self.verify_communication_channels) + ] + + click.echo("\nšŸ” Running System Validation...") - self.command_aliases = { - '/m': 'market', - '/t': 'trend', - '/v': 'volume', - '/r': 'risk', - '/s': 'strategy', - '/p': 'portfolio', - '/a': 'agents', - '/b': 'balance', - '/pos': 'positions', - '/h': 'help', - '/c': 'clear', - '/x': 'execute', - '/reason': 'toggle_reasoning', - '/ws': 'workspace', - '/env': 'environment', - '/config': 'config', - '/export': 'export_workspace', - '/import': 'import_workspace' + for name, validator in validations: + click.echo(f"\nā–¶ļø Checking {name}...") + try: + if not await validator(): + click.echo(click.style(f"\nāŒ {name} validation failed. Please fix issues above.", fg='red')) + return False + click.echo(click.style(f"āœ“ {name} validation passed.", fg='green')) + except Exception as e: + click.echo(click.style(f"\nāŒ {name} validation error: {str(e)}", fg='red')) + return False + + click.echo(click.style("\nāœØ All validations passed successfully!", fg='green')) + return True + + async def validate_environment(self) -> bool: + """Enhanced environment validation with recovery suggestions.""" + required_vars = { + 'LLM_API_KEY': 'LLM API key for agent intelligence', + 'NEAR_ACCOUNT_ID': 'NEAR account for blockchain operations', + 'NEAR_NETWORK': 'NEAR network (testnet/mainnet)', } - self.multiline_mode = False - self.multiline_buffer = [] - - def _get_prompt(self) -> str: - """Get formatted prompt.""" - return f"near-swarm ({self.agent_type})> " - - def _format_output(self, data: Dict[str, Any]) -> None: - """Format and display output based on settings""" - if self.json_output: - console.print_json(json.dumps(data)) - else: - if isinstance(data.get("content"), str): - console.print(Panel(Markdown(data["content"]))) + + missing_vars = [] + for var, description in required_vars.items(): + value = os.getenv(var) + if not value: + missing_vars.append((var, description)) else: - console.print(Panel(str(data))) - - # Store in history if verbose - if self.verbose: - self.history.append(data) - - async def setup(self): - """Set up agent and market data.""" + click.echo(f" āœ“ {var}: Found") + if var == 'NEAR_ACCOUNT_ID': + self.env_state.near_account = value + elif var == 'NEAR_NETWORK': + self.env_state.network = value + + if missing_vars: + click.echo("\nšŸ”§ Missing Environment Variables:") + for var, desc in missing_vars: + click.echo(f" ā€¢ {var}: {desc}") + click.echo("\nšŸ“ Quick Fix:") + click.echo(" 1. Add these variables to your .env file") + click.echo(" 2. Run: source .env") + click.echo(" 3. Or run quickstart.sh again") + return False + + return True + + async def verify_plugins(self) -> bool: + """Verify plugin loading with detailed feedback.""" try: - # Initialize market data - self.market_data = MarketDataManager() - - # Initialize agent - config = AgentConfig( - network=os.getenv("NEAR_NETWORK", "testnet"), - account_id=os.getenv("NEAR_ACCOUNT_ID"), - private_key=os.getenv("NEAR_PRIVATE_KEY"), - llm_provider=os.getenv("LLM_PROVIDER", "hyperbolic"), - llm_api_key=os.getenv("LLM_API_KEY"), - llm_model=os.getenv("LLM_MODEL", "deepseek-ai/DeepSeek-V3"), - api_url=os.getenv("LLM_API_URL", "https://api.hyperbolic.xyz/v1"), - system_prompt=AGENT_PROMPTS[self.agent_type] - ) + click.echo("\nšŸ”Œ Scanning for plugins...") + plugins = await self.plugin_loader.load_all_plugins() - self.agent = SwarmAgent( - config, - SwarmConfig( - role=self.agent_type, - min_confidence=0.7, - min_votes=1 # Single agent mode - ) - ) + if not plugins: + click.echo(" āš ļø No plugins found!") + return False + + click.echo("\nšŸ“¦ Available Plugins:") + for name, plugin in plugins.items(): + click.echo(f" āœ“ {name}: {plugin.__class__.__name__}") - # Start agent - await self.agent.start() - console.print(f"āœ… Connected to {self.agent_type} agent") + required_plugins = {'price-monitor', 'decision-maker'} + missing = required_plugins - set(plugins.keys()) + if missing: + click.echo(f"\nāš ļø Missing required plugins: {missing}") + return False + + return True + except Exception as e: - console.print(f"[red]Error setting up agent: {str(e)}") - raise - - async def cleanup(self): - """Clean up resources.""" - if self.agent: - await self.agent.close() - if self.market_data: - await self.market_data.close() - - def do_market(self, arg: str): - """Get market analysis for a symbol.""" - if not arg: - console.print("[red]Please specify a symbol (e.g., market near)") - return + click.echo(f" āŒ Plugin loading error: {str(e)}") + return False + + async def agent_config_check(self) -> bool: + """Validate agent configurations with schema checking.""" + click.echo("\nšŸ“„ Validating Agent Configurations...") - async def get_market_analysis(symbol: str): + config_files = [ + ('price-monitor.yaml', ['name', 'llm', 'plugins']), + ('decision-maker.yaml', ['name', 'llm', 'plugins']) + ] + + for filename, required_fields in config_files: + path = os.path.join('agents', filename) try: - data = await self.market_data.get_token_price(symbol) - console.print(Panel(f""" -[bold]Market Analysis for {symbol.upper()}[/bold] -Current Price: ${data['price']:.2f} -Confidence: {data['confidence']:.2%} -""")) - - # Get agent's market analysis - analysis = await self.agent.evaluate_proposal({ - "type": "market_analysis", - "params": { - "symbol": symbol, - "price": data['price'], - "market_context": { - "current_price": data['price'], - "24h_volume": "2.1M", # TODO: Get real volume - "market_trend": "stable", - "network_load": "moderate" - } - } - }) - - console.print(Panel(Markdown(analysis["reasoning"]))) - + if not os.path.exists(path): + click.echo(f" āš ļø Missing config: {filename}") + return False + + with open(path, 'r') as f: + config = yaml.safe_load(f) + + missing = [field for field in required_fields if field not in config] + if missing: + click.echo(f" āŒ {filename} missing fields: {missing}") + return False + + click.echo(f" āœ“ {filename} validated successfully") + except Exception as e: - console.print(f"[red]Error getting market analysis: {str(e)}") - - asyncio.run(get_market_analysis(arg)) - - def do_risk(self, arg: str): - """Get risk assessment.""" - console.print("šŸ”„ Risk assessment coming soon!") - - def do_strategy(self, arg: str): - """Get strategy suggestions.""" - console.print("šŸ”„ Strategy suggestions coming soon!") - - def do_portfolio(self, arg: str): - """Get portfolio overview.""" - console.print("šŸ”„ Portfolio overview coming soon!") - - def do_agents(self, arg: str): - """List active agents.""" - console.print(Panel(f""" -[bold]Active Agent:[/bold] -Type: {self.agent_type} -Status: {'Running' if self.agent and self.agent.is_running() else 'Stopped'} -""")) - - def do_exit(self, arg: str): - """Exit the chat interface.""" - return True - - def do_quit(self, arg: str): - """Exit the chat interface.""" + click.echo(f" āŒ Error checking {filename}: {str(e)}") + return False + return True - def do_trend(self, arg: str): - """Get trend analysis for a timeframe.""" - if not arg: - console.print("[red]Please specify a timeframe (e.g., trend 24h)") - return - - async def get_trend_analysis(timeframe: str): - try: - analysis = await self.agent.evaluate_proposal({ - "type": "trend_analysis", - "params": { - "timeframe": timeframe, - "market_context": await self.market_data.get_market_context() - } - }) - self._format_output(analysis) - except Exception as e: - console.print(f"[red]Error getting trend analysis: {str(e)}") - - asyncio.run(get_trend_analysis(arg)) - - def do_volume(self, arg: str): - """Get volume analysis for a symbol.""" - if not arg: - console.print("[red]Please specify a symbol (e.g., volume near)") - return - - async def get_volume_analysis(symbol: str): - try: - analysis = await self.agent.evaluate_proposal({ - "type": "volume_analysis", - "params": { - "symbol": symbol, - "market_context": await self.market_data.get_market_context() - } - }) - self._format_output(analysis) - except Exception as e: - console.print(f"[red]Error getting volume analysis: {str(e)}") - - asyncio.run(get_volume_analysis(arg)) - - def do_balance(self, arg: str): - """Get portfolio balance.""" - async def get_balance(): - try: - balance = await self.agent.evaluate_proposal({ - "type": "portfolio_balance", - "params": {} - }) - self._format_output(balance) - except Exception as e: - console.print(f"[red]Error getting balance: {str(e)}") - - asyncio.run(get_balance()) - - def do_positions(self, arg: str): - """List open positions.""" - async def get_positions(): - try: - positions = await self.agent.evaluate_proposal({ - "type": "open_positions", - "params": {} - }) - self._format_output(positions) - except Exception as e: - console.print(f"[red]Error getting positions: {str(e)}") - - asyncio.run(get_positions()) + async def verify_communication_channels(self) -> bool: + """Verify all communication channels with health checks.""" + click.echo("\nšŸ”— Verifying Communication Channels...") - def do_execute(self, arg: str): - """Execute a command suggested by the agent.""" - if not arg: - console.print("[red]Please specify a command to execute[/red]") - return - + # Check LLM connection try: - os.system(arg) - console.print(f"[green]Executed: {arg}[/green]") + config = LLMConfig( + provider=os.getenv('LLM_PROVIDER', 'hyperbolic'), + api_key=os.getenv('LLM_API_KEY'), + model=os.getenv('LLM_MODEL', 'meta-llama/llama-3.3-70B-Instruct') + ) + llm = create_llm_provider(config) + click.echo(" āœ“ LLM provider initialized") except Exception as e: - console.print(f"[red]Error executing command: {str(e)}[/red]") + click.echo(f" āŒ LLM initialization failed: {str(e)}") + return False - def do_multiline(self, arg: str): - """Toggle multiline input mode.""" - self.multiline_mode = not self.multiline_mode - status = "enabled" if self.multiline_mode else "disabled" - console.print(f"[green]Multiline mode {status}[/green]") - - def do_save(self, arg: str): - """Save chat history to a file.""" - filename = arg or "chat_history.json" + # Check market data connection try: - with open(filename, 'w') as f: - json.dump(self.history, f, indent=2) - console.print(f"[green]Chat history saved to {filename}[/green]") + async with MarketDataManager() as market: + data = await market.get_token_price('near') + click.echo(f" āœ“ Market data available: NEAR ${data['price']:.2f}") except Exception as e: - console.print(f"[red]Error saving history: {str(e)}[/red]") + click.echo(f" āŒ Market data connection failed: {str(e)}") + return False - def do_load(self, arg: str): - """Load chat history from a file.""" - if not arg: - console.print("[red]Please specify a file to load[/red]") - return + return True + + async def run_enhanced_tutorial(self, tutorial_mode: str) -> None: + """Run tutorial with enhanced progress tracking.""" + welcome_msg = f""" +ā•­ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā•® +ā”‚ Welcome to Your NEAR AI Agent Studio! šŸš€ ā”‚ +ā•°ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā•Æ + +Environment Status: +āœ“ NEAR Account: {self.env_state.near_account} +āœ“ Network: {self.env_state.network} +āœ“ LLM Provider: {self.env_state.llm_provider} + +Your development environment is ready for AI agents! +""" + click.echo(welcome_msg) + + tutorial_steps = [ + ("Creating Price Monitor", self._create_price_monitor), + ("Creating Decision Maker", self._create_decision_maker), + ("Verifying Agent Communication", self._verify_agent_communication), + ("Launching Collaboration", self._launch_collaboration) + ] + + for step_name, step_func in tutorial_steps: + click.echo(f"\nā–¶ļø {step_name}...") + if not await step_func(): + click.echo(click.style(f"\nāŒ Tutorial paused at: {step_name}", fg='red')) + return + + click.echo(click.style("\nšŸŽ‰ Tutorial completed successfully!", fg='green')) + + async def _create_price_monitor(self) -> bool: + """Create and verify price monitoring agent.""" try: - with open(arg, 'r') as f: - self.history = json.load(f) - console.print(f"[green]Chat history loaded from {arg}[/green]") + cmd = "create agent price-monitor --role market_analyzer" + result = await self.run_command(cmd) + return result.returncode == 0 except Exception as e: - console.print(f"[red]Error loading history: {str(e)}[/red]") - - def do_capabilities(self, arg: str): - """Show agent capabilities and examples.""" - console.print(Panel(""" -[bold]NEAR Swarm Agent Capabilities[/bold] - -[cyan]1. Market Analysis[/cyan] -- Real-time price analysis -- Trend detection -- Volume analysis -- Market sentiment analysis - -[yellow]2. Risk Management[/yellow] -- Portfolio risk assessment -- Position sizing recommendations -- Risk exposure analysis -- Market condition monitoring - -[green]3. Strategy Optimization[/green] -- Trading strategy suggestions -- Performance optimization -- Parameter tuning -- Backtest analysis - -[magenta]4. Developer Tools[/magenta] -- Command execution -- Multi-agent coordination -- Natural language processing -- API integration - -[blue]Example Interactions:[/blue] -1. "Can you analyze the NEAR market and suggest entry points?" -2. "What's the current risk level for my portfolio?" -3. "Help me optimize my trading strategy for the current market" -4. "Show me how to implement a simple arbitrage strategy" - -[white]Tips:[/white] -- Use /help to see all available commands -- Start commands with '/' or use natural language -- Use multiline mode for complex inputs (/multiline) -- Save your chat history with /save -""")) - - def default(self, line: str): - """Handle natural language input and command aliases.""" - # Check for command aliases - if line.startswith('/'): - cmd = line.split()[0] - if cmd in self.command_aliases: - return getattr(self, f'do_{self.command_aliases[cmd]}')(line[len(cmd):].strip()) - - # Handle multiline input - if self.multiline_mode: - if line.strip() == '}': - # Process accumulated input - full_input = '\n'.join(self.multiline_buffer) - self.multiline_buffer = [] - self.multiline_mode = False - asyncio.run(self._process_input(full_input)) - return - self.multiline_buffer.append(line) - return - - # Handle natural language interaction - asyncio.run(self._process_input(line)) + click.echo(f"āŒ Error creating price monitor: {str(e)}") + return False - async def _process_input(self, text: str): - """Process natural language input with enhanced reasoning.""" + async def _create_decision_maker(self) -> bool: + """Create and verify decision making agent.""" try: - # Add reasoning context if enabled - context = { - "agent_type": self.agent_type, - "market_context": await self.market_data.get_market_context() if self.market_data else None, - "history": self.history[-5:] if self.verbose else [], - "capabilities": { - "can_execute_commands": True, - "available_commands": list(self.command_aliases.keys()) - } - } - - # For chat assistant, handle general questions differently - if self.agent_type == "chat_assistant" and not text.startswith('/'): - response = await self.agent.evaluate_proposal({ - "type": "general_chat", - "params": { - "query": text, - "context": context, - "require_command": False - } - }, stream=True) - - # Handle streaming response - if "stream" in response: - console.print("\n", end="") - async for chunk in response["stream"]: - console.print(chunk, end="") - console.print("\n") - return - - # Handle non-streaming response - if isinstance(response.get("content"), str): - self._format_output({"content": response["content"]}) - else: - self._format_output(response) - return - - # Handle command-based interactions - response = await self.agent.evaluate_proposal({ - "type": "chat_interaction", - "params": { - "input": text, - "context": context, - "response_model": { - "market_analysis": MarketAnalysis.model_json_schema(), - "risk_assessment": RiskAssessment.model_json_schema(), - "strategy_proposal": StrategyProposal.model_json_schema() - } if not self.json_output else None - } - }) - - # Check if response contains a command to execute - if "command" in response: - console.print(f"\n[yellow]Suggested command:[/yellow] {response['command']}") - if input("Execute this command? [y/N] ").lower() == 'y': - self.do_execute(response['command']) - - self._format_output(response) - + cmd = "create agent decision-maker --role strategy_optimizer" + result = await self.run_command(cmd) + return result.returncode == 0 except Exception as e: - console.print(f"[red]Error processing input: {str(e)}[/red]") - - def do_help(self, arg: str): - """Enhanced help command with examples.""" - if arg: - # Detailed help for specific command - super().do_help(arg) - # Add examples - if hasattr(self, f'do_{arg}'): - examples = { - 'market': 'Example: market near - Get NEAR token analysis', - 'trend': 'Example: trend 24h - Get 24-hour trend analysis', - 'volume': 'Example: volume near - Get NEAR trading volume', - 'risk': 'Example: risk exposure - Check risk exposure', - 'strategy': 'Example: strategy optimize - Get strategy optimization suggestions', - } - if arg in examples: - console.print(f"\n[green]{examples[arg]}[/green]") - else: - console.print(Panel(""" -[bold]NEAR Swarm Intelligence Chat Commands[/bold] - -[green]Market Analysis:[/green] -/market, /m [symbol] : Get market analysis -/trend, /t [timeframe] : Get trend analysis -/volume, /v [symbol] : Get volume analysis - -[yellow]Risk Management:[/yellow] -/risk, /r [action] : Risk assessment -/balance, /b : Check portfolio balance -/positions, /pos : List open positions - -[blue]Strategy:[/blue] -/strategy, /s [action] : Strategy suggestions -/portfolio, /p : Portfolio overview - -[magenta]System:[/magenta] -/agents, /a : List active agents -help [command] : Show help for command -exit, quit : Exit chat - -[cyan]Natural Language:[/cyan] -You can also interact naturally with your agent: -- "What's the current market sentiment for NEAR?" -- "Should I adjust my portfolio based on current trends?" -- "Analyze the risk of increasing my NEAR position" -""")) - - def do_clear(self, arg: str): - """Clear the terminal screen.""" - os.system('cls' if os.name == 'nt' else 'clear') - console.print(self.intro) - - def do_toggle_reasoning(self, arg: str): - """Toggle step-by-step reasoning mode.""" - self.reasoning_enabled = not self.reasoning_enabled - status = "enabled" if self.reasoning_enabled else "disabled" - console.print(f"[green]Step-by-step reasoning {status}[/green]") - - def do_workspace(self, arg: str): - """Manage workspace configuration.""" - if not arg: - if self.workspace: - console.print(Panel(f""" -[bold]Current Workspace:[/bold] {self.workspace.name} -[cyan]Description:[/cyan] {self.workspace.description or 'No description'} - -[green]Active Agents:[/green] -{chr(10).join(f'- {agent}' for agent in self.workspace.agents)} - -[yellow]Available Strategies:[/yellow] -{chr(10).join(f'- {strategy}' for strategy in self.workspace.strategies)} - -[magenta]Environment:[/magenta] -{chr(10).join(f'- {k}={v}' for k, v in self.workspace.environment.items())} -""")) - else: - console.print("[yellow]No workspace configured. Use 'workspace create ' to create one.[/yellow]") - return + click.echo(f"āŒ Error creating decision maker: {str(e)}") + return False - cmd, *args = arg.split(maxsplit=1) - if cmd == "create": - name = args[0] if args else input("Workspace name: ") - description = input("Description (optional): ") - self.workspace = WorkspaceConfig( - name=name, - description=description, - agents=[self.agent_type], - environment=dict(os.environ) - ) - console.print(f"[green]Created workspace: {name}[/green]") - elif cmd == "add": - if not self.workspace: - console.print("[red]No workspace configured[/red]") - return - what, *items = args[0].split() - if what == "agent": - self.workspace.agents.extend(items) - elif what == "strategy": - self.workspace.strategies.extend(items) - console.print(f"[green]Added {what}: {', '.join(items)}[/green]") - - def do_environment(self, arg: str): - """Manage environment variables.""" - if not self.workspace: - console.print("[red]No workspace configured[/red]") - return + async def _verify_agent_communication(self) -> bool: + """Verify inter-agent communication.""" + click.echo("Verifying agent communication channels...") + # Add actual communication verification logic here + return True - if not arg: - console.print(Panel("\n".join(f"{k}={v}" for k, v in self.workspace.environment.items()))) - return + async def _launch_collaboration(self) -> bool: + """Launch and monitor agent collaboration.""" + try: + await self.run_agents("price-monitor", "decision-maker") + return True + except Exception as e: + click.echo(f"āŒ Error launching collaboration: {str(e)}") + return False - cmd, *args = arg.split(maxsplit=1) - if cmd == "set": - key, value = args[0].split('=', 1) - self.workspace.environment[key] = value - os.environ[key] = value - console.print(f"[green]Set {key}={value}[/green]") - elif cmd == "unset": - key = args[0] - self.workspace.environment.pop(key, None) - os.environ.pop(key, None) - console.print(f"[green]Unset {key}[/green]") - - def do_export_workspace(self, arg: str): - """Export workspace configuration.""" - if not self.workspace: - console.print("[red]No workspace configured[/red]") - return + async def run_command(self, command: str) -> subprocess.CompletedProcess: + """Execute command with enhanced error handling.""" + click.echo(f"\nExecuting: {click.style(command, fg='cyan')}") + + cmd_parts = command.split() + if not cmd_parts[0].startswith('near-swarm'): + cmd_parts = ['near-swarm'] + cmd_parts - filename = arg or f"{self.workspace.name}_workspace.json" try: - with open(filename, 'w') as f: - json.dump(self.workspace.dict(), f, indent=2) - console.print(f"[green]Workspace exported to {filename}[/green]") + result = subprocess.run(cmd_parts, capture_output=True, text=True) + if result.returncode == 0: + click.echo(click.style("āœ“ Command succeeded", fg='green')) + else: + click.echo(click.style(f"āŒ Command failed: {result.stderr}", fg='red')) + return result except Exception as e: - console.print(f"[red]Error exporting workspace: {str(e)}[/red]") + raise RuntimeError(f"Command execution failed: {str(e)}") - def do_import_workspace(self, arg: str): - """Import workspace configuration.""" - if not arg: - console.print("[red]Please specify a file to import[/red]") + async def run_agents(self, *args) -> None: + """Run agents with enhanced monitoring.""" + if not args: + click.echo("Usage: run_agents ...") return + cmd = ['near-swarm', 'run'] + list(args) + click.echo("\nšŸŒ Launching AI Agent Swarm...") + try: - with open(arg, 'r') as f: - data = json.load(f) - self.workspace = WorkspaceConfig(**data) - # Update environment - os.environ.update(self.workspace.environment) - console.print(f"[green]Workspace imported from {arg}[/green]") - except Exception as e: - console.print(f"[red]Error importing workspace: {str(e)}[/red]") + process = subprocess.Popen( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + universal_newlines=True, + bufsize=1 + ) - def do_config(self, arg: str): - """View or modify configuration.""" - if not self.workspace: - console.print("[red]No workspace configured[/red]") - return + while True: + output = process.stdout.readline() + if output == '' and process.poll() is not None: + break + if output: + await self._process_agent_output(output.strip()) + + if process.returncode != 0: + error = process.stderr.read().strip() + if error: + click.echo(click.style(f"\nāŒ Error: {error}", fg='red')) + return - if not arg: - console.print(Panel(json.dumps(self.workspace.settings, indent=2))) - return + except Exception as e: + click.echo(click.style(f"\nāŒ Agent execution failed: {str(e)}", fg='red')) + async def _process_agent_output(self, output: str) -> None: + """Process and format agent output with metrics tracking.""" try: - key, value = arg.split('=', 1) - # Parse value as JSON if possible - try: - value = json.loads(value) - except json.JSONDecodeError: - pass - self.workspace.settings[key] = value - console.print(f"[green]Set {key}={value}[/green]") - except ValueError: - console.print("[red]Invalid format. Use: config key=value[/red]") - - def do_quickstart(self, arg: str): - """Interactive quickstart wizard""" - console.print(Panel(""" - Welcome to NEAR Swarm! Let's get you started: - - 1. Choose your use case - 2. Configure your agents - 3. Test your strategy - 4. Deploy to testnet - """)) - -def chat( - agent: AgentType = typer.Option(AgentType.CHAT_ASSISTANT, "--agent", "-a", help="Type of agent to chat with"), - json_output: bool = typer.Option(False, "--json", "-j", help="Output responses in JSON format"), - verbose: bool = typer.Option(False, "--verbose", "-v", help="Show detailed agent reasoning"), - command: Optional[str] = typer.Option(None, "--command", "-c", help="Direct command to execute (non-interactive mode)"), - multi_agent: bool = typer.Option(False, "--multi-agent", "-m", help="Enable multi-agent mode"), - agents: Optional[List[AgentType]] = typer.Option(None, "--agents", help="List of agents to use in multi-agent mode") -): - """Start a chat session with NEAR Swarm agents. Simply run 'near-swarm chat' to begin.""" - try: - # Default to interactive mode unless a specific command is provided - interactive = command is None + if "Analysis from agent" in output: + self.metrics['analysis_completed'] += 1 + click.echo(click.style(f"\nšŸ” {output}", fg='yellow')) + elif "Decision from agent" in output: + self.metrics['decisions_made'] += 1 + click.echo(click.style(f"\nšŸ¤” {output}", fg='blue')) + elif "Message exchanged" in output: + self.metrics['messages_exchanged'] += 1 + click.echo(click.style(f"\nšŸ’¬ {output}", fg='green')) + else: + click.echo(output) + + # Show metrics periodically + if sum(self.metrics.values()) % 5 == 0: + self._display_metrics() + + except Exception as e: + logger.error(f"Error processing output: {str(e)}") + click.echo(output) + + def _display_metrics(self) -> None: + """Display current collaboration metrics.""" + metrics_msg = f""" +šŸ“Š Collaboration Metrics: + ā€¢ Analyses: {self.metrics['analysis_completed']} + ā€¢ Decisions: {self.metrics['decisions_made']} + ā€¢ Messages: {self.metrics['messages_exchanged']} +""" + click.echo(click.style(metrics_msg, fg='cyan')) + + async def run_interactive(self) -> None: + """Run interactive mode with enhanced command handling.""" + click.echo("\nšŸ‘‹ Welcome to interactive mode! Type /help for commands.") - if interactive: - if multi_agent and not agents: - agents = [AgentType.MARKET_ANALYZER, AgentType.RISK_MANAGER, AgentType.STRATEGY_OPTIMIZER] - - chat_interface = SwarmChat( - agent_type=agent if not multi_agent else "swarm", - verbose=verbose, - json_output=json_output - ) - - # Setup and start agents - async def setup_agents(): - await chat_interface.setup() - if multi_agent: - # Initialize additional agents - peer_agents = [] - for agent_type in agents: - if agent_type != agent: # Skip main agent type - config = AgentConfig( - network=os.getenv("NEAR_NETWORK", "testnet"), - account_id=os.getenv("NEAR_ACCOUNT_ID"), - private_key=os.getenv("NEAR_PRIVATE_KEY"), - llm_provider=os.getenv("LLM_PROVIDER", "hyperbolic"), - llm_api_key=os.getenv("LLM_API_KEY"), - llm_model=os.getenv("LLM_MODEL", "deepseek-ai/DeepSeek-V3"), - api_url=os.getenv("LLM_API_URL", "https://api.hyperbolic.xyz/v1") - ) - peer_agent = SwarmAgent( - config, - SwarmConfig( - role=agent_type, - min_confidence=0.7, - min_votes=1 - ) - ) - await peer_agent.start() - peer_agents.append(peer_agent) - - # Join swarm - if chat_interface.agent: - await chat_interface.agent.join_swarm(peer_agents) - - asyncio.run(setup_agents()) - chat_interface.cmdloop() - else: - # Non-interactive mode - if not command: - console.print("[red]Error: --command is required in non-interactive mode[/red]") - sys.exit(1) - - chat_interface = SwarmChat(agent, verbose, json_output) - - async def execute_command(): - await chat_interface.setup() - # Parse command - cmd_parts = command.split(maxsplit=1) - cmd_name = cmd_parts[0].lstrip('/') - cmd_args = cmd_parts[1] if len(cmd_parts) > 1 else "" - - # Execute command - if hasattr(chat_interface, f'do_{cmd_name}'): - getattr(chat_interface, f'do_{cmd_name}')(cmd_args) + while True: + try: + command = await self.session.prompt_async(">> ") + command = command.strip() + + if command.lower() in ['/exit', '/quit']: + break + elif command == '/help': + self._show_help() + elif command.startswith('/create'): + await self.run_command(command[1:]) + elif command.startswith('/run'): + args = command.split()[1:] + await self.run_agents(*args) + elif command.startswith('/status'): + self._display_metrics() else: - # Handle as natural language - await chat_interface.default(command) - - await chat_interface.cleanup() - - asyncio.run(execute_command()) - - except KeyboardInterrupt: - console.print("\nšŸ‘‹ Goodbye!") - except Exception as e: - console.print(f"[red]Error in chat session: {str(e)}") - sys.exit(1) \ No newline at end of file + click.echo("Unknown command. Type /help for available commands.") + + except Exception as e: + click.echo(click.style(f"āŒ Error: {str(e)}", fg='red')) + + def _show_help(self) -> None: + """Show enhanced help message.""" + help_msg = """ +Available Commands: +/create agent Create a new agent +/run Run multiple agents together +/status Show current metrics +/help Show this help message +/exit Exit the chat + +Example: +/create agent my-agent Create a new agent named 'my-agent' +/run price-monitor decision-maker Run both agents together +""" + click.echo(help_msg) + +def start_chat(tutorial_mode: Optional[str] = None): + """Start enhanced chat interface.""" + assistant = EnhancedChatAssistant(tutorial_mode) + asyncio.run(assistant.start()) \ No newline at end of file diff --git a/near_swarm/cli/config.py b/near_swarm/cli/config.py index a560df2..abde2a2 100644 --- a/near_swarm/cli/config.py +++ b/near_swarm/cli/config.py @@ -16,6 +16,47 @@ def config(): """Configuration management commands""" pass +@config.group() +def agent(): + """Agent configuration commands""" + pass + +@agent.command(name='configure') +@click.argument('name') +@click.option('--role', help='Agent role (e.g., market_analyzer, risk_manager)') +@click.option('--min-confidence', type=float, help='Minimum confidence threshold') +@click.option('--max-retries', type=int, help='Maximum number of retries') +def configure_agent(name: str, role: Optional[str] = None, min_confidence: Optional[float] = None, max_retries: Optional[int] = None): + """Configure an agent""" + try: + # Load existing config or create new + config_file = f"agents/custom/{name}/agent.yaml" + config = {} + + if os.path.exists(config_file): + with open(config_file, 'r') as f: + config = yaml.safe_load(f) or {} + + # Update configuration + if role: + config['role'] = role + if min_confidence: + config['min_confidence'] = min_confidence + if max_retries: + config['max_retries'] = max_retries + + # Ensure directory exists + os.makedirs(os.path.dirname(config_file), exist_ok=True) + + # Save configuration + with open(config_file, 'w') as f: + yaml.dump(config, f, default_flow_style=False) + + click.echo(f"āœ… Configured agent: {name}") + + except Exception as e: + click.echo(f"āŒ Error configuring agent: {str(e)}") + @config.command() @click.argument('config_file', required=False) def validate(config_file: Optional[str] = None): @@ -56,6 +97,9 @@ def validate(config_file: Optional[str] = None): if env_vars: click.echo("\nEnvironment variables:") for key, value in env_vars.items(): + # Mask sensitive values + if 'KEY' in key or 'SECRET' in key: + value = '*' * 8 click.echo(f" {key}: {value}") except ValidationError as e: @@ -68,21 +112,6 @@ def validate(config_file: Optional[str] = None): except Exception as e: click.echo(f"Error validating configuration: {str(e)}", err=True) -@config.command() -def show(): - """Show current configuration""" - try: - loader = ConfigLoader() - loader.load_defaults() - loader.load_config_file() - loader.load_env() - - config = loader.get_config() - click.echo(yaml.dump(config.dict(), default_flow_style=False)) - - except Exception as e: - click.echo(f"Error showing configuration: {str(e)}", err=True) - @config.command() def init(): """Initialize default configuration""" @@ -119,4 +148,21 @@ def init(): click.echo("3. Run 'near-swarm config validate' to verify") except Exception as e: - click.echo(f"Error creating configuration: {str(e)}", err=True) \ No newline at end of file + click.echo(f"Error creating configuration: {str(e)}", err=True) + +@config.command() +def show(): + """Show current configuration""" + try: + loader = ConfigLoader() + loader.load_defaults() + loader.load_config_file() + loader.load_env() + + config = loader.get_config() + config_dict = config.dict() + + click.echo(yaml.dump(config_dict, default_flow_style=False)) + + except Exception as e: + click.echo(f"Error showing configuration: {str(e)}") \ No newline at end of file diff --git a/near_swarm/cli/create.py b/near_swarm/cli/create.py index 70468be..59be45f 100644 --- a/near_swarm/cli/create.py +++ b/near_swarm/cli/create.py @@ -15,9 +15,10 @@ def create(): @create.command() @click.argument('name') +@click.option('--role', default=None, help='Agent role (market_analyzer/strategy_optimizer)') @click.option('--template', default='plugin_template', help='Template to use') -@click.option('--path', default='agents/custom', help='Where to create the agent') -def agent(name: str, template: str, path: str): +@click.option('--path', default='near_swarm/agents', help='Where to create the agent') +def agent(name: str, role: Optional[str], template: str, path: str): """Create a new agent""" try: # Get template path @@ -36,8 +37,15 @@ def agent(name: str, template: str, path: str): # Create directories os.makedirs(agent_path, exist_ok=True) - # Copy template - shutil.copytree(template_path, agent_path, dirs_exist_ok=True) + # Copy template files individually + for item in os.listdir(template_path): + source = os.path.join(template_path, item) + dest = os.path.join(agent_path, item) + + if os.path.isfile(source): + shutil.copy2(source, dest) + elif os.path.isdir(source): + shutil.copytree(source, dest) # Update configuration config_path = os.path.join(agent_path, 'agent.yaml') @@ -46,15 +54,54 @@ def agent(name: str, template: str, path: str): config = yaml.safe_load(f) config['name'] = name - config['role'] = f"{name}_agent" + + # Set role-specific configuration + if role: + config['role'] = role + if role == 'market_analyzer': + config['capabilities'] = [ + 'market_analysis', + 'price_monitoring', + 'risk_assessment' + ] + config['system_prompt'] = """You are a market analysis agent monitoring NEAR token prices. +Your role is to analyze market conditions, identify trends, and assess risks. +Consider price movements, trading volume, market sentiment, and external factors. +Always provide clear reasoning and confidence levels for your analysis.""" + + elif role == 'strategy_optimizer': + config['capabilities'] = [ + 'strategy_optimization', + 'decision_making', + 'risk_management' + ] + config['system_prompt'] = """You are a decision-making agent evaluating market opportunities. +Your role is to analyze market conditions and recommend optimal trading strategies. +Consider risk factors, potential returns, and market analysis from other agents. +Always provide clear reasoning and confidence levels for your decisions.""" + + # Update plugin.py + plugin_path = os.path.join(agent_path, 'plugin.py') + if os.path.exists(plugin_path): + with open(plugin_path, 'r') as f: + plugin_code = f.read() + + # Update class name + plugin_code = plugin_code.replace('CustomAgentPlugin', f"{name.replace('-', '_').title()}Plugin") + + # Update registration + plugin_code = plugin_code.replace('register_plugin("custom_agent"', f'register_plugin("{name}"') + + with open(plugin_path, 'w') as f: + f.write(plugin_code) with open(config_path, 'w') as f: yaml.dump(config, f) - click.echo(f"Created agent: {name}") + click.echo(f"āœ… Created agent: {name}") except Exception as e: - click.echo(f"Error creating agent: {str(e)}", err=True) + click.echo(f"āŒ Error creating agent: {str(e)}", err=True) @create.command() @click.argument('name') diff --git a/near_swarm/cli/main.py b/near_swarm/cli/main.py index e1e697b..84579a0 100644 --- a/near_swarm/cli/main.py +++ b/near_swarm/cli/main.py @@ -9,6 +9,10 @@ from .create import create from .config import config from ..plugins import PluginLoader +from ..core.market_data import MarketDataManager +import os +import yaml +import time @click.group() def cli(): @@ -53,10 +57,156 @@ async def run_plugin(): except Exception as e: click.echo(f"Error executing plugin: {str(e)}", err=True) +@cli.command() +@click.option('--tutorial', type=str, help='Start a guided tutorial (e.g., create-first-agent)') +def chat(tutorial: str = None): + """Start interactive chat assistant""" + from .chat import start_chat + start_chat(tutorial_mode=tutorial) + +@cli.command() +@click.argument('agents', nargs=-1, required=True) +@click.option('--timeout', default=60, help='Timeout in seconds') +def run(agents, timeout): + """Run multiple agents together""" + try: + if not agents: + click.echo("Please specify at least one agent to run") + return + + click.echo(f"Starting agents: {', '.join(agents)}") + + async def run_agents(): + loader = PluginLoader() + market_data = MarketDataManager() + loaded_agents = [] + + # Load and validate each agent + for agent in agents: + config_file = f"near_swarm/agents/{agent}/agent.yaml" + if not os.path.exists(config_file): + # Try alternate locations + alternate_paths = [ + f"agents/{agent}/agent.yaml", + f"near_swarm/examples/{agent}.yaml", + f"plugins/{agent}/agent.yaml" + ] + found = False + for path in alternate_paths: + if os.path.exists(path): + config_file = path + found = True + break + + if not found: + click.echo(f"āŒ Agent not found: {agent}") + click.echo("Looked in:") + click.echo(f"- near_swarm/agents/{agent}/agent.yaml") + for path in alternate_paths: + click.echo(f"- {path}") + return + + # Load config to verify it exists + with open(config_file, 'r') as f: + config = yaml.safe_load(f) + if not config: + click.echo(f"āŒ Invalid configuration for agent: {agent}") + return + + # Load agent plugin + plugin = await loader.load_plugin(agent) + if not plugin: + click.echo(f"āŒ Failed to load agent: {agent}") + return + + loaded_agents.append(plugin) + click.echo(f"āœ… Loaded agent: {agent} ({config.get('role', 'unknown role')})") + + click.echo("\nšŸ¤– Agents are now running and collaborating:") + + try: + start_time = time.time() + while time.time() - start_time < timeout: + # Get current market data + near_data = await market_data.get_token_price('near') + near_price = near_data['price'] + click.echo(f"\nšŸ“Š Current NEAR Price: ${near_price:.2f}") + + # Price monitor agent analyzes market data + if loaded_agents[0].role == "market_analyzer": + click.echo("\nšŸ” Price Monitor thinking...") + click.echo("Sending request to agent for market analysis...") + + analysis = await loaded_agents[0].evaluate({ + "price": near_price, + "timestamp": time.time(), + "request": """Analyze the current NEAR price and market conditions: + 1. Evaluate recent price movements and volatility + 2. Identify any significant trends or patterns + 3. Consider market sentiment and external factors + 4. Provide a clear recommendation based on your analysis + + Format your response with clear observations, reasoning, and conclusions.""" + }) + + # Show the agent's reasoning process + click.echo(f"\nšŸ“ Analysis from agent:") + click.echo(f" ā€¢ Observation: {analysis.get('observation', 'Analyzing price data...')}") + click.echo(f" ā€¢ Reasoning: {analysis.get('reasoning', 'Evaluating market conditions...')}") + click.echo(f" ā€¢ Conclusion: {analysis.get('conclusion', 'Forming market opinion...')}") + click.echo(f" ā€¢ Confidence: {analysis.get('confidence', 0):.0%}") + + # Decision maker agent evaluates the analysis + if len(loaded_agents) > 1 and loaded_agents[1].role == "strategy_optimizer": + click.echo("\nšŸ¤” Decision Maker consulting agent...") + click.echo("Sending market analysis to agent for strategic evaluation...") + + decision = await loaded_agents[1].evaluate({ + "market_analysis": analysis, + "current_price": near_price, + "request": """Based on the price monitor's analysis, evaluate potential trading strategies: + 1. Review the market analysis and current price + 2. Consider risk levels and market conditions + 3. Evaluate potential trading opportunities + 4. Recommend specific actions with confidence levels + + Format your response with clear context, strategy, rationale, and specific actions.""" + }) + + # Show the decision-making process + click.echo(f"\nšŸ“‹ Strategic Decision from agent analysis:") + click.echo(f" ā€¢ Context: {decision.get('context', 'Reviewing market analysis...')}") + click.echo(f" ā€¢ Strategy: {decision.get('strategy', 'Formulating approach...')}") + click.echo(f" ā€¢ Rationale: {decision.get('rationale', 'Evaluating options...')}") + click.echo(f" ā€¢ Action: {decision.get('action', 'Determining next steps...')}") + click.echo(f" ā€¢ Confidence: {decision.get('confidence', 0):.0%}") + + if decision.get("confidence", 0) > 0.8: + click.echo(f"\nāœØ High Confidence Decision:") + click.echo(f" {decision.get('action', 'No action needed')}") + + click.echo("\nā³ Waiting 5 seconds before next analysis...") + await asyncio.sleep(5) + + except KeyboardInterrupt: + click.echo("\nšŸ‘‹ Stopping agents gracefully...") + finally: + # Cleanup + for agent in loaded_agents: + await agent.cleanup() + await loader.cleanup() + await market_data.close() + + asyncio.run(run_agents()) + + except Exception as e: + click.echo(f"āŒ Error running agents: {str(e)}") + # Register commands cli.add_command(plugins) cli.add_command(create) cli.add_command(config) +cli.add_command(run) if __name__ == '__main__': cli() \ No newline at end of file diff --git a/near_swarm/plugins/loader.py b/near_swarm/plugins/loader.py index 6ffd07a..7c40dfc 100644 --- a/near_swarm/plugins/loader.py +++ b/near_swarm/plugins/loader.py @@ -6,7 +6,7 @@ import os import logging import importlib -from typing import Dict, Any, Optional, Type +from typing import Dict, Any, Optional, Type, List import yaml from near_swarm.core.agent import AgentConfig @@ -19,7 +19,44 @@ class PluginLoader: def __init__(self): self._loaded_plugins: Dict[str, AgentPlugin] = {} - + + async def load_all_plugins(self) -> Dict[str, AgentPlugin]: + """Scan and load all available plugins.""" + # Search paths for plugins + search_paths = [ + ("near_swarm/agents", "plugin.py"), # Look for plugin.py in subdirectories + ("agents/custom", "plugin.py"), + ("near_swarm/examples", "*_strategy.py"), + ("plugins", "plugin.py") # Look for plugin.py in subdirectories + ] + + # Track found plugins + found_plugins = set() + + # Scan each search path + for base_path, pattern in search_paths: + if os.path.exists(base_path): + for root, _, files in os.walk(base_path): + if pattern == "plugin.py" and pattern in files: + # Extract plugin name from directory + plugin_name = os.path.basename(root) + found_plugins.add(plugin_name) + elif pattern.endswith("_strategy.py"): + # Extract plugin name from strategy files + for file in files: + if file.endswith("_strategy.py"): + plugin_name = file.replace("_strategy.py", "").replace("_", "-") + found_plugins.add(plugin_name) + + # Load each found plugin + for plugin_name in found_plugins: + try: + await self.load_plugin(plugin_name) + except Exception as e: + logger.error(f"Failed to load plugin {plugin_name}: {str(e)}") + + return self._loaded_plugins + async def load_plugin( self, name: str, @@ -33,58 +70,88 @@ async def load_plugin( return self._loaded_plugins[name] # Import plugin module - module_path = f"near_swarm.plugins.{name}" - try: - module = importlib.import_module(module_path) - except ImportError: - # Try loading from examples directory first - example_path = os.path.join("near_swarm", "examples", f"{name.replace('-', '_')}_strategy.py") - if os.path.exists(example_path): - spec = importlib.util.spec_from_file_location(name, example_path) - if spec and spec.loader: - module = importlib.util.module_from_spec(spec) - spec.loader.exec_module(module) - else: - raise ImportError(f"Could not load plugin: {name}") - else: - # Try loading from custom plugins directory - custom_path = os.path.join("plugins", f"{name}.py") - if os.path.exists(custom_path): - spec = importlib.util.spec_from_file_location(name, custom_path) + module_path = None + module = None + + # Try loading from plugins directory first + plugin_path = os.path.join("plugins", name, "plugin.py") + if os.path.exists(plugin_path): + spec = importlib.util.spec_from_file_location(name, plugin_path) + if spec and spec.loader: + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + + # If not found, try other locations + if not module: + try: + module_path = f"near_swarm.plugins.{name}" + module = importlib.import_module(module_path) + except ImportError: + # Try loading from agents directory + agent_path = os.path.join("near_swarm", "agents", name, "plugin.py") + if os.path.exists(agent_path): + spec = importlib.util.spec_from_file_location(name, agent_path) if spec and spec.loader: module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) - else: - raise ImportError(f"Could not load plugin: {name}") else: - raise ImportError(f"Plugin not found: {name}") - - # Load plugin configuration + # Try loading from custom agents directory + custom_agent_path = os.path.join("agents", "custom", name, "plugin.py") + if os.path.exists(custom_agent_path): + spec = importlib.util.spec_from_file_location(name, custom_agent_path) + if spec and spec.loader: + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + else: + raise ImportError(f"Agent/plugin not found: {name}") + + # Load configuration if not plugin_config: - config_path = os.path.join("plugins", name, "agent.yaml") + # Try loading from agents directory first + config_path = os.path.join("agents", f"{name}.yaml") if os.path.exists(config_path): with open(config_path, 'r') as f: config_data = yaml.safe_load(f) - plugin_config = PluginConfig(**config_data) + + # Extract plugin configuration + plugin_data = next((p for p in config_data.get('plugins', []) if p.get('name') == name), {}) + if not plugin_data: + plugin_data = { + 'name': name, + 'role': name, + 'capabilities': [], + 'custom_settings': {} + } + + plugin_config = PluginConfig(**plugin_data) else: # Use default configuration plugin_config = PluginConfig( name=name, role=name, capabilities=[], - system_prompt=None, custom_settings={} ) # Create default agent config if not provided if not agent_config: - # Create agent config with required fields - agent_config = AgentConfig( - name=name, - environment="development", - log_level="INFO" - ) - + # Load from agent.yaml if it exists + config_path = os.path.join("agents", f"{name}.yaml") + if os.path.exists(config_path): + with open(config_path, 'r') as f: + config_data = yaml.safe_load(f) + # Remove plugin-specific fields + if 'plugins' in config_data: + del config_data['plugins'] + agent_config = AgentConfig(**config_data) + else: + # Create agent config with required fields + agent_config = AgentConfig( + name=name, + environment="development", + log_level="INFO" + ) + # Create plugin instance plugin_class = None for attr_name in dir(module): @@ -94,7 +161,7 @@ async def load_plugin( break if not plugin_class: - raise ImportError(f"No plugin class found in module: {name}") + raise ImportError(f"No agent/plugin class found in module: {name}") plugin = plugin_class(agent_config, plugin_config) @@ -104,13 +171,13 @@ async def load_plugin( # Store loaded plugin self._loaded_plugins[name] = plugin - logger.info(f"Loaded plugin: {name}") + logger.info(f"Loaded agent/plugin: {name}") return plugin except Exception as e: - logger.error(f"Error loading plugin {name}: {str(e)}") + logger.error(f"Error loading {name}: {str(e)}") raise - + async def unload_plugin(self, name: str) -> None: """Unload and cleanup a plugin""" if name in self._loaded_plugins: diff --git a/near_swarm/templates/plugin_template/README.md b/near_swarm/templates/plugin_template/README.md index f862a58..6068170 100644 --- a/near_swarm/templates/plugin_template/README.md +++ b/near_swarm/templates/plugin_template/README.md @@ -1,109 +1,125 @@ -# NEAR Swarm Agent Plugin Template +# NEAR Agent Template -This template helps you create custom agents for the NEAR Swarm Intelligence framework. +This template helps you create specialized agents for the NEAR ecosystem. It includes built-in support for market analysis, strategy optimization, and custom agent roles. -## Getting Started +## Available Roles -1. Copy this template directory: -```bash -near-swarm create agent my-agent -``` +### Market Analyzer +The market analyzer role focuses on monitoring NEAR token prices and market conditions. It provides: +- Real-time price analysis +- Market sentiment evaluation +- Risk assessment +- Trend identification -2. Configure your agent in `agent.yaml`: -- Set your agent's name and role -- Define capabilities -- Configure LLM settings -- Add custom settings +Example configuration: +```yaml +name: price-monitor +role: market_analyzer +market_data: + update_interval: 300 # 5 minutes + confidence_threshold: 0.7 + risk_threshold: 0.8 +``` -3. Implement your agent logic in `plugin.py`: -- Customize the evaluation logic -- Add your own methods -- Configure LLM prompts -- Add custom initialization +### Strategy Optimizer +The strategy optimizer role evaluates market opportunities and makes strategic decisions. It provides: +- Trading strategy recommendations +- Risk mitigation steps +- Confidence-based decisions +- Performance optimization -## Plugin Structure +Example configuration: +```yaml +name: decision-maker +role: strategy_optimizer +strategy: + min_confidence: 0.8 + max_exposure: 0.2 + risk_tolerance: medium ``` -my-agent/ -ā”œā”€ā”€ __init__.py -ā”œā”€ā”€ agent.yaml # Agent configuration -ā”œā”€ā”€ plugin.py # Agent implementation -ā””ā”€ā”€ README.md # Documentation + +## Configuration + +### Required Environment Variables +```bash +# LLM Provider (required) +export LLM_PROVIDER=hyperbolic +export LLM_API_KEY=your_api_key +export LLM_MODEL=meta-llama/llama-3.3-70b-instruct + +# NEAR Network (optional) +export NEAR_NETWORK=testnet +export NEAR_ACCOUNT_ID=your.testnet +export NEAR_PRIVATE_KEY=your_private_key ``` -## Configuration (agent.yaml) +### Agent Configuration (agent.yaml) +The `agent.yaml` file defines your agent's behavior: ```yaml -name: my_agent -role: custom_role -capabilities: - - capability_1 - - capability_2 -llm: - system_prompt: | - Your custom prompt here -settings: - your_setting: value -``` +name: your_agent_name +role: market_analyzer # or strategy_optimizer + +# LLM Settings +llm_provider: ${LLM_PROVIDER} +llm_api_key: ${LLM_API_KEY} +llm_model: ${LLM_MODEL} +llm_temperature: 0.7 +llm_max_tokens: 1000 -## Implementation (plugin.py) -Key methods to implement: -- `initialize()`: Set up your agent -- `evaluate()`: Main logic -- `cleanup()`: Resource cleanup - -Example: -```python -async def evaluate(self, context: Dict[str, Any]) -> Dict[str, Any]: - """Your custom evaluation logic""" - # Get LLM analysis - response = await self.llm_provider.query(your_prompt) - - # Add custom processing - result = process_response(response) - - return result +# Role-specific settings... ``` ## Usage -```python -from near_swarm.plugins import get_plugin -from near_swarm.core.agent import AgentConfig -# Create configuration -config = AgentConfig(...) +### Creating an Agent +```bash +# Create a market analyzer +near-swarm create agent price-monitor --role market_analyzer -# Load your plugin -plugin = await plugin_loader.load_plugin("my_agent", config) +# Create a strategy optimizer +near-swarm create agent decision-maker --role strategy_optimizer +``` -# Use your plugin -result = await plugin.evaluate(context) +### Running Agents +```bash +# Run a single agent +near-swarm run price-monitor + +# Run multiple agents together +near-swarm run price-monitor decision-maker ``` -## Best Practices -1. Keep your agent focused on a specific role -2. Use clear, descriptive prompts -3. Handle errors gracefully -4. Clean up resources properly -5. Document your agent's capabilities -6. Add type hints and docstrings -7. Follow NEAR's coding standards - -## Testing -Create tests in `tests/`: -```python -async def test_my_agent(): - plugin = MyAgentPlugin(config) - result = await plugin.evaluate(test_context) - assert result["decision"] is not None +### Development + +1. Implement your agent logic in `plugin.py` +2. Configure settings in `agent.yaml` +3. Test your agent: +```bash +# Run with debug logging +RUST_LOG=debug near-swarm run your-agent + +# Run with a specific operation +near-swarm execute your-agent --operation custom_action ``` -## Contributing -Share your agent with the community: -1. Fork the repository -2. Create your agent -3. Add documentation -4. Submit a pull request - -## Support -- Documentation: [docs.near.org](https://docs.near.org) -- Discord: [NEAR Discord](https://near.chat) -- GitHub: [NEAR Protocol](https://github.com/near) \ No newline at end of file +## Best Practices + +1. **Error Handling** + - Always handle API errors gracefully + - Provide meaningful error messages + - Implement retry logic for transient failures + +2. **Resource Management** + - Clean up resources in the `cleanup()` method + - Use async context managers for external connections + - Monitor memory usage for long-running agents + +3. **Configuration** + - Use environment variables for sensitive data + - Document all configuration options + - Provide sensible defaults + +4. **Testing** + - Write unit tests for your agent logic + - Test with different market conditions + - Validate error handling and edge cases \ No newline at end of file diff --git a/near_swarm/templates/plugin_template/agent.yaml b/near_swarm/templates/plugin_template/agent.yaml index 619d244..b15ddb3 100644 --- a/near_swarm/templates/plugin_template/agent.yaml +++ b/near_swarm/templates/plugin_template/agent.yaml @@ -1,28 +1,44 @@ -# Agent Plugin Configuration -name: custom_agent # Replace with your agent name -role: custom_role # Agent's role in the swarm -description: A custom NEAR swarm agent # Description of your agent +# Agent Configuration Template +# Use this template to configure your custom NEAR swarm agent + +# Basic agent configuration +name: custom_agent +role: market_analyzer # market_analyzer or strategy_optimizer + +# LLM provider settings +llm_provider: openai # openai, anthropic, etc. +llm_api_key: ${OPENAI_API_KEY} # Use environment variable +llm_model: gpt-4 # Model to use +api_url: https://api.openai.com/v1 # API endpoint # Agent capabilities capabilities: - - basic_analysis - - custom_capability + - market_analysis + - risk_assessment + - decision_making -# LLM configuration -llm: - system_prompt: | - You are a specialized AI agent in the NEAR Protocol ecosystem. - Your role is to: [Describe your agent's role] - - Key responsibilities: - 1. [First responsibility] - 2. [Second responsibility] - 3. [Third responsibility] - - Always provide your analysis in a clear, structured format. +# System prompt for the agent +system_prompt: | + You are a specialized AI agent in the NEAR swarm, focused on {role}. + + For market_analyzer role: + - Analyze NEAR token price movements and market conditions + - Identify trends and patterns + - Assess market sentiment and risks + - Provide clear, actionable insights + + For strategy_optimizer role: + - Evaluate market analysis and opportunities + - Optimize trading strategies based on risk tolerance + - Make data-driven decisions + - Recommend specific actions with rationale + + Always provide structured responses in JSON format with confidence levels. + Focus on clear, actionable insights while managing risks appropriately. # Custom settings settings: - confidence_threshold: 0.7 - min_data_points: 10 - custom_parameter: value \ No newline at end of file + min_confidence_threshold: 0.7 # Minimum confidence level for recommendations + risk_tolerance: medium # low, medium, high + max_retries: 3 # Maximum retry attempts for failed operations + timeout: 30 # Operation timeout in seconds \ No newline at end of file diff --git a/near_swarm/templates/plugin_template/plugin.py b/near_swarm/templates/plugin_template/plugin.py index 5d5e28c..be6b2ed 100644 --- a/near_swarm/templates/plugin_template/plugin.py +++ b/near_swarm/templates/plugin_template/plugin.py @@ -3,31 +3,36 @@ Use this template to create your own NEAR swarm agent plugin """ -from typing import Dict, Any -from near_swarm.plugins.base import AgentPlugin, PluginConfig -from near_swarm.core.agent import AgentConfig +from typing import Dict, Any, Optional +from near_swarm.plugins.base import AgentPlugin from near_swarm.core.llm_provider import create_llm_provider, LLMConfig +import logging + +logger = logging.getLogger(__name__) class CustomAgentPlugin(AgentPlugin): """Custom agent implementation""" async def initialize(self) -> None: """Initialize plugin resources""" - # Initialize LLM provider with your custom configuration + # Initialize LLM provider llm_config = LLMConfig( provider=self.agent_config.llm_provider, api_key=self.agent_config.llm_api_key, model=self.agent_config.llm_model, - temperature=self.agent_config.llm_temperature, - max_tokens=self.agent_config.llm_max_tokens, + temperature=0.7, + max_tokens=1000, api_url=self.agent_config.api_url, system_prompt=self.plugin_config.system_prompt ) self.llm_provider = create_llm_provider(llm_config) - # Add any custom initialization here - self.confidence_threshold = self.plugin_config.custom_settings.get( - 'confidence_threshold', 0.7 + # Load custom settings + self.min_confidence = self.plugin_config.settings.get( + 'min_confidence_threshold', 0.7 + ) + self.risk_tolerance = self.plugin_config.settings.get( + 'risk_tolerance', 'medium' ) async def evaluate(self, context: Dict[str, Any]) -> Dict[str, Any]: @@ -35,35 +40,98 @@ async def evaluate(self, context: Dict[str, Any]) -> Dict[str, Any]: if not self.llm_provider: raise RuntimeError("Plugin not initialized") - # Create your custom evaluation prompt - prompt = f"""[Your custom prompt here] -Context: + # Extract data from context + request = context.get('request', '') + + # Create role-specific prompts + if self.plugin_config.role == "market_analyzer": + current_price = context.get('price', 0) + timestamp = context.get('timestamp', 0) + + prompt = f"""Analyze the current NEAR market conditions: + +Current Price: ${current_price:.2f} +Timestamp: {timestamp} +Risk Tolerance: {self.risk_tolerance} + +{request} + +Provide your analysis in JSON format with: +- observation: Your observations of current conditions +- reasoning: Your detailed analysis process +- conclusion: Clear summary and recommendations +- confidence: Your confidence level (0-1) +""" + + elif self.plugin_config.role == "strategy_optimizer": + market_analysis = context.get('market_analysis', {}) + current_price = context.get('current_price', 0) + + prompt = f"""Evaluate the following market opportunity: + +Market Analysis: +- Observation: {market_analysis.get('observation', 'No observation provided')} +- Reasoning: {market_analysis.get('reasoning', 'No reasoning provided')} +- Conclusion: {market_analysis.get('conclusion', 'No conclusion provided')} +- Analysis Confidence: {market_analysis.get('confidence', 0):.0%} + +Current Price: ${current_price:.2f} +Risk Tolerance: {self.risk_tolerance} + +{request} + +Provide your decision in JSON format with: +- context: Your understanding of the situation +- strategy: Your recommended approach +- rationale: Detailed explanation of your decision +- action: Specific steps to take +- confidence: Your confidence level (0-1) +""" + + else: + prompt = f"""Analyze the following context: {context} Provide your analysis in JSON format with: -- decision: Your decision (true/false) +- observation: Your observations +- reasoning: Your analysis process +- conclusion: Clear summary - confidence: Your confidence level (0-1) -- reasoning: Detailed explanation -- recommendations: List of actionable items """ # Get LLM analysis - response = await self.llm_provider.query(prompt, expect_json=True) - - # Add your custom logic here - # Example: Filter low confidence responses - if response.get('confidence', 0) < self.confidence_threshold: - response['decision'] = False - response['reasoning'] += "\nConfidence below threshold." - - return response + try: + response = await self.llm_provider.query(prompt, expect_json=True) + + # Validate confidence threshold + if response.get('confidence', 0) < self.min_confidence: + if self.plugin_config.role == "market_analyzer": + response['conclusion'] = "Confidence too low for definitive recommendation. Need more data." + elif self.plugin_config.role == "strategy_optimizer": + response['action'] = "Confidence too low for action. Monitoring situation." + + return response + + except Exception as e: + logger.error(f"Error during evaluation: {e}") + return { + "observation": "Error during analysis", + "reasoning": str(e), + "conclusion": "Analysis failed", + "confidence": 0 + } + + async def execute(self, operation: Optional[str] = None, **kwargs) -> Any: + """Execute plugin operation""" + if operation == "analyze": + return await self.evaluate(kwargs) + else: + raise ValueError(f"Unsupported operation: {operation}") async def cleanup(self) -> None: - """Clean up plugin resources""" + """Cleanup plugin resources""" if self.llm_provider: await self.llm_provider.close() - self.llm_provider = None - # Add any custom cleanup here # Register your plugin from near_swarm.plugins import register_plugin diff --git a/plugins/decision-maker/plugin.py b/plugins/decision-maker/plugin.py new file mode 100644 index 0000000..4b81048 --- /dev/null +++ b/plugins/decision-maker/plugin.py @@ -0,0 +1,124 @@ +""" +Decision Maker Plugin for NEAR Swarm Intelligence +Evaluates market analysis and makes strategic decisions +""" + +import logging +from typing import Dict, Any, Optional +from near_swarm.plugins.base import AgentPlugin, PluginConfig +from near_swarm.core.agent import AgentConfig + +logger = logging.getLogger(__name__) + +class DecisionMakerPlugin(AgentPlugin): + """Strategic decision making plugin""" + + def __init__(self, agent_config: AgentConfig, plugin_config: PluginConfig): + super().__init__(agent_config, plugin_config) + self.min_confidence = float(self.plugin_config.custom_settings.get('min_confidence_threshold', 0.8)) + self.risk_tolerance = self.plugin_config.custom_settings.get('risk_tolerance', 'medium') + self.decision_interval = int(self.plugin_config.custom_settings.get('decision_interval', 300)) + self.logger = logger + + async def initialize(self) -> None: + """Initialize decision maker""" + self.logger.info("Decision maker initialized successfully") + + async def process_message(self, message: Dict[str, Any]) -> Optional[Dict[str, Any]]: + """Process market analysis and make decisions""" + try: + if message.get('type') != 'price_update': + return None + + price_data = message.get('data', {}) + if not price_data or price_data.get('confidence', 0) < self.min_confidence: + return None + + # Analyze price movement + price_change = price_data.get('change_24h', 0) + current_price = price_data.get('price', 0) + + decision = self._evaluate_market_conditions(price_change, current_price) + + return { + 'type': 'trading_decision', + 'data': { + 'action': decision['action'], + 'confidence': decision['confidence'], + 'reasoning': decision['reasoning'], + 'risk_level': decision['risk_level'] + } + } + + except Exception as e: + self.logger.error(f"Error processing message: {str(e)}") + return None + + def _evaluate_market_conditions(self, price_change: float, current_price: float) -> Dict[str, Any]: + """Evaluate market conditions and generate decision""" + # Define risk thresholds based on risk tolerance + risk_thresholds = { + 'low': {'change': 0.03, 'confidence': 0.9}, + 'medium': {'change': 0.05, 'confidence': 0.8}, + 'high': {'change': 0.08, 'confidence': 0.7} + } + + threshold = risk_thresholds[self.risk_tolerance] + + if abs(price_change) < threshold['change']: + return { + 'action': 'hold', + 'confidence': 0.85, + 'reasoning': 'Price movement within normal range', + 'risk_level': 'low' + } + + # Determine action based on price movement and risk tolerance + if price_change > threshold['change']: + return { + 'action': 'sell' if self.risk_tolerance != 'high' else 'hold', + 'confidence': threshold['confidence'], + 'reasoning': f'Significant price increase of {price_change}%', + 'risk_level': 'medium' + } + else: + return { + 'action': 'buy' if self.risk_tolerance != 'low' else 'hold', + 'confidence': threshold['confidence'], + 'reasoning': f'Significant price decrease of {price_change}%', + 'risk_level': 'medium' + } + + async def evaluate(self, context: Dict[str, Any]) -> Dict[str, Any]: + """Evaluate market data and make strategic decisions""" + try: + if not context.get('price_data'): + return {'error': 'No price data provided'} + + price_data = context['price_data'] + price_change = price_data.get('change_24h', 0) + current_price = price_data.get('price', 0) + + decision = self._evaluate_market_conditions(price_change, current_price) + decision['context'] = context + + return decision + + except Exception as e: + self.logger.error(f"Error evaluating market data: {str(e)}") + return {'error': str(e)} + + async def execute(self, action: Dict[str, Any]) -> Dict[str, Any]: + """Execute trading decisions""" + try: + if action.get('type') == 'evaluate_market': + return await self.evaluate(action.get('data', {})) + return {'error': 'Unsupported action type'} + + except Exception as e: + self.logger.error(f"Error executing action: {str(e)}") + return {'error': str(e)} + + async def cleanup(self) -> None: + """Cleanup resources""" + pass \ No newline at end of file diff --git a/plugins/price-monitor/plugin.py b/plugins/price-monitor/plugin.py new file mode 100644 index 0000000..b5d68d4 --- /dev/null +++ b/plugins/price-monitor/plugin.py @@ -0,0 +1,110 @@ +""" +Price Monitor Plugin for NEAR Swarm Intelligence +Monitors NEAR token prices and market conditions +""" + +import logging +from typing import Dict, Any, Optional +from near_swarm.plugins.base import AgentPlugin, PluginConfig +from near_swarm.core.agent import AgentConfig +from near_swarm.core.market_data import MarketDataManager + +logger = logging.getLogger(__name__) + +class PriceMonitorPlugin(AgentPlugin): + """Price monitoring plugin for market analysis""" + + def __init__(self, agent_config: AgentConfig, plugin_config: PluginConfig): + super().__init__(agent_config, plugin_config) + self.market_data = None + self.update_interval = int(self.plugin_config.custom_settings.get('update_interval', 60)) + self.alert_threshold = float(self.plugin_config.custom_settings.get('alert_threshold', 0.05)) + self.logger = logger + + async def initialize(self) -> None: + """Initialize market data connection""" + self.market_data = MarketDataManager() + self.logger.info("Price monitor initialized successfully") + + async def get_price_data(self) -> Dict[str, Any]: + """Get NEAR price data using available method""" + try: + # Get price data using get_token_price + return await self.market_data.get_token_price('near') + except Exception as e: + self.logger.error(f"Error getting price data: {str(e)}") + return {} + + async def process_message(self, message: Dict[str, Any]) -> Optional[Dict[str, Any]]: + """Process incoming messages and monitor price changes""" + try: + # Get current NEAR price + price_data = await self.get_price_data() + if not price_data: + return None + + response = { + 'type': 'price_update', + 'data': { + 'price': price_data['price'], + 'timestamp': price_data['timestamp'], + 'change_24h': price_data.get('change_24h', 0), + 'confidence': 0.95 + } + } + + # Check for significant price changes + if abs(price_data.get('change_24h', 0)) >= self.alert_threshold: + response['alert'] = { + 'type': 'price_movement', + 'severity': 'high' if abs(price_data['change_24h']) >= 0.1 else 'medium', + 'message': f"Significant price movement detected: {price_data['change_24h']}%" + } + + return response + + except Exception as e: + self.logger.error(f"Error processing message: {str(e)}") + return None + + async def evaluate(self, context: Dict[str, Any]) -> Dict[str, Any]: + """Evaluate market conditions""" + try: + price_data = await self.get_price_data() + if not price_data: + return {'error': 'Failed to get price data'} + + evaluation = { + 'price': price_data['price'], + 'change_24h': price_data.get('change_24h', 0), + 'confidence': 0.95, + 'risk_level': 'low' + } + + if abs(price_data.get('change_24h', 0)) >= self.alert_threshold: + evaluation['risk_level'] = 'high' if abs(price_data['change_24h']) >= 0.1 else 'medium' + + return evaluation + + except Exception as e: + self.logger.error(f"Error evaluating market conditions: {str(e)}") + return {'error': str(e)} + + async def execute(self, action: Dict[str, Any]) -> Dict[str, Any]: + """Execute monitoring actions""" + try: + if action.get('type') == 'monitor_price': + return await self.evaluate({'type': 'price_check'}) + return {'error': 'Unsupported action type'} + + except Exception as e: + self.logger.error(f"Error executing action: {str(e)}") + return {'error': str(e)} + + async def cleanup(self) -> None: + """Cleanup market data connection""" + if self.market_data: + try: + await self.market_data.close() + except: + pass \ No newline at end of file diff --git a/public/near-agent-studio.png b/public/near-agent-studio.png new file mode 100644 index 0000000000000000000000000000000000000000..846c8f21117b150b06325bd8910518d85a7f64c0 GIT binary patch literal 53640 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 literal 0 HcmV?d00001 diff --git a/scripts/quickstart.sh b/scripts/quickstart.sh index 512ea59..99434a7 100755 --- a/scripts/quickstart.sh +++ b/scripts/quickstart.sh @@ -26,14 +26,21 @@ show_progress() { print_logo() { echo -e "${BLUE}" echo ' -ā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā•—ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•—ā–ˆā–ˆā•— ā–ˆā–ˆā•— ā–ˆā–ˆā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā–ˆā•— -ā–ˆā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā•‘ā–ˆā–ˆā•”ā•ā•ā•ā•ā•ā–ˆā–ˆā•”ā•ā•ā–ˆā–ˆā•—ā–ˆā–ˆā•”ā•ā•ā–ˆā–ˆā•— ā–ˆā–ˆā•”ā•ā•ā•ā•ā•ā–ˆā–ˆā•‘ ā–ˆā–ˆā•‘ā–ˆā–ˆā•”ā•ā•ā–ˆā–ˆā•—ā–ˆā–ˆā•”ā•ā•ā–ˆā–ˆā•—ā–ˆā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā–ˆā–ˆā•‘ -ā–ˆā–ˆā•”ā–ˆā–ˆā•— ā–ˆā–ˆā•‘ā–ˆā–ˆā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•‘ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•”ā• ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•—ā–ˆā–ˆā•‘ ā–ˆā•— ā–ˆā–ˆā•‘ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•‘ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•”ā•ā–ˆā–ˆā•”ā–ˆā–ˆā–ˆā–ˆā•”ā–ˆā–ˆā•‘ -ā–ˆā–ˆā•‘ā•šā–ˆā–ˆā•—ā–ˆā–ˆā•‘ā–ˆā–ˆā•”ā•ā•ā• ā–ˆā–ˆā•”ā•ā•ā–ˆā–ˆā•‘ā–ˆā–ˆā•”ā•ā•ā–ˆā–ˆā•— ā•šā•ā•ā•ā•ā–ˆā–ˆā•‘ā–ˆā–ˆā•‘ā–ˆā–ˆā–ˆā•—ā–ˆā–ˆā•‘ā–ˆā–ˆā•”ā•ā•ā–ˆā–ˆā•‘ā–ˆā–ˆā•”ā•ā•ā–ˆā–ˆā•—ā–ˆā–ˆā•‘ā•šā–ˆā–ˆā•”ā•ā–ˆā–ˆā•‘ -ā–ˆā–ˆā•‘ ā•šā–ˆā–ˆā–ˆā–ˆā•‘ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•—ā–ˆā–ˆā•‘ ā–ˆā–ˆā•‘ā–ˆā–ˆā•‘ ā–ˆā–ˆā•‘ ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•‘ā•šā–ˆā–ˆā–ˆā•”ā–ˆā–ˆā–ˆā•”ā•ā–ˆā–ˆā•‘ ā–ˆā–ˆā•‘ā–ˆā–ˆā•‘ ā–ˆā–ˆā•‘ā–ˆā–ˆā•‘ ā•šā•ā• ā–ˆā–ˆā•‘ -ā•šā•ā• ā•šā•ā•ā•ā•ā•šā•ā•ā•ā•ā•ā•ā•ā•šā•ā• ā•šā•ā•ā•šā•ā• ā•šā•ā• ā•šā•ā•ā•ā•ā•ā•ā• ā•šā•ā•ā•ā•šā•ā•ā• ā•šā•ā• ā•šā•ā•ā•šā•ā• ā•šā•ā•ā•šā•ā• ā•šā•ā• +ā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā•—ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•—ā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā•—ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•— +ā–ˆā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā•‘ā–ˆā–ˆā•”ā•ā•ā•ā•ā•ā–ˆā–ˆā•”ā•ā•ā–ˆā–ˆā•—ā–ˆā–ˆā•”ā•ā•ā–ˆā–ˆā•— ā–ˆā–ˆā•”ā•ā•ā–ˆā–ˆā•—ā–ˆā–ˆā•”ā•ā•ā•ā•ā• ā–ˆā–ˆā•”ā•ā•ā•ā•ā•ā–ˆā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā•‘ā•šā•ā•ā–ˆā–ˆā•”ā•ā•ā• +ā–ˆā–ˆā•”ā–ˆā–ˆā•— ā–ˆā–ˆā•‘ā–ˆā–ˆā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•‘ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•”ā• ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•‘ā–ˆā–ˆā•‘ ā–ˆā–ˆā–ˆā•—ā–ˆā–ˆā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā•”ā–ˆā–ˆā•— ā–ˆā–ˆā•‘ ā–ˆā–ˆā•‘ +ā–ˆā–ˆā•‘ā•šā–ˆā–ˆā•—ā–ˆā–ˆā•‘ā–ˆā–ˆā•”ā•ā•ā• ā–ˆā–ˆā•”ā•ā•ā–ˆā–ˆā•‘ā–ˆā–ˆā•”ā•ā•ā–ˆā–ˆā•— ā–ˆā–ˆā•”ā•ā•ā–ˆā–ˆā•‘ā–ˆā–ˆā•‘ ā–ˆā–ˆā•‘ā–ˆā–ˆā•”ā•ā•ā• ā–ˆā–ˆā•‘ā•šā–ˆā–ˆā•—ā–ˆā–ˆā•‘ ā–ˆā–ˆā•‘ +ā–ˆā–ˆā•‘ ā•šā–ˆā–ˆā–ˆā–ˆā•‘ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•—ā–ˆā–ˆā•‘ ā–ˆā–ˆā•‘ā–ˆā–ˆā•‘ ā–ˆā–ˆā•‘ ā–ˆā–ˆā•‘ ā–ˆā–ˆā•‘ā•šā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•”ā•ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•—ā–ˆā–ˆā•‘ ā•šā–ˆā–ˆā–ˆā–ˆā•‘ ā–ˆā–ˆā•‘ +ā•šā•ā• ā•šā•ā•ā•ā•ā•šā•ā•ā•ā•ā•ā•ā•ā•šā•ā• ā•šā•ā•ā•šā•ā• ā•šā•ā• ā•šā•ā• ā•šā•ā• ā•šā•ā•ā•ā•ā•ā• ā•šā•ā•ā•ā•ā•ā•ā•ā•šā•ā• ā•šā•ā•ā•ā• ā•šā•ā• + +ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•—ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•—ā–ˆā–ˆā•— ā–ˆā–ˆā•—ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā•— ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•— +ā–ˆā–ˆā•”ā•ā•ā•ā•ā•ā•šā•ā•ā–ˆā–ˆā•”ā•ā•ā•ā–ˆā–ˆā•‘ ā–ˆā–ˆā•‘ā–ˆā–ˆā•”ā•ā•ā–ˆā–ˆā•—ā–ˆā–ˆā•‘ā–ˆā–ˆā•”ā•ā•ā•ā–ˆā–ˆā•— +ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā•‘ ā–ˆā–ˆā•‘ ā–ˆā–ˆā•‘ā–ˆā–ˆā•‘ ā–ˆā–ˆā•‘ā–ˆā–ˆā•‘ā–ˆā–ˆā•‘ ā–ˆā–ˆā•‘ +ā•šā•ā•ā•ā•ā–ˆā–ˆā•‘ ā–ˆā–ˆā•‘ ā–ˆā–ˆā•‘ ā–ˆā–ˆā•‘ā–ˆā–ˆā•‘ ā–ˆā–ˆā•‘ā–ˆā–ˆā•‘ā–ˆā–ˆā•‘ ā–ˆā–ˆā•‘ +ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•‘ ā–ˆā–ˆā•‘ ā•šā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•”ā•ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•”ā•ā–ˆā–ˆā•‘ā•šā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•”ā• +ā•šā•ā•ā•ā•ā•ā•ā• ā•šā•ā• ā•šā•ā•ā•ā•ā•ā• ā•šā•ā•ā•ā•ā•ā• ā•šā•ā• ā•šā•ā•ā•ā•ā•ā• ' - echo -e "${CYAN}šŸ¤– Multi-Agent Systems on NEAR${NC}" + echo -e "${CYAN}šŸ¤– Build Autonomous Agents on NEAR${NC}" echo "" } @@ -47,19 +54,20 @@ check_command() { # Start script print_logo -echo -e "${CYAN}Welcome to the NEAR Swarm Intelligence Workshop!${NC}" -echo -e "This script will set up a complete multi-agent system demonstrating:" -echo -e "ā€¢ ${GREEN}Market Analysis${NC} - Price and volume evaluation" -echo -e "ā€¢ ${GREEN}Risk Management${NC} - Safety and exposure control" -echo -e "ā€¢ ${GREEN}Strategy Optimization${NC} - Performance tuning" +echo -e "${CYAN}Welcome to NEAR Agent Studio!${NC}" +echo -e "This starter kit helps you build autonomous agents that can:" +echo -e "ā€¢ ${GREEN}Interact with NEAR${NC} - Send transactions, check balances, monitor accounts" +echo -e "ā€¢ ${GREEN}Access Market Data${NC} - Real-time prices, trading volumes, market trends" +echo -e "ā€¢ ${GREEN}Make Decisions${NC} - LLM-powered reasoning and strategy execution" echo "" -echo -e "${CYAN}This setup will:${NC}" -echo "1. Configure your Python environment" -echo "2. Set up your NEAR testnet account" -echo "3. Initialize AI agents with different roles" -echo "4. Run a demo showing multi-agent decision making" +echo -e "${CYAN}Setup will:${NC}" +echo "1. Configure your development environment" +echo "2. Set up a NEAR testnet account for testing" +echo "3. Install example agents to learn from" +echo "4. Verify all integrations (NEAR, Market Data, LLM)" +echo "5. Launch an interactive chat assistant to help create your first agent" echo "" -echo -e "${CYAN}Press Enter when you're ready to begin...${NC}" +echo -e "${CYAN}Press Enter to start building...${NC}" read -r # Check for virtual environment @@ -186,15 +194,31 @@ echo -e "ā€¢ ${GREEN}NEAR Connection${NC} - Connected to testnet" echo -e "ā€¢ ${GREEN}Market Data${NC} - Price feeds active" echo -e "ā€¢ ${GREEN}LLM Integration${NC} - Connected" -# Success message -section_header "šŸŽ‰ Setup Complete!" -echo -e "${GREEN}Your NEAR Swarm Intelligence environment is ready!${NC}" +# Success message and transition to chat +section_header "šŸŽ‰ Ready to Build!" +echo -e "${GREEN}Your NEAR Agent Studio environment is ready!${NC}" echo "" -echo -e "${CYAN}Next steps:${NC}" -echo "1. Create a new plugin: near-swarm plugins create my-plugin" -echo "2. List available plugins: near-swarm plugins list" -echo "3. Run examples:" -echo " ā€¢ Token transfer: near-swarm execute token-transfer --operation transfer --recipient bob.testnet --amount 0.1" -echo " ā€¢ Arbitrage: near-swarm execute arbitrage-agent --operation analyze --pair NEAR/USDC" +echo -e "${CYAN}Starting interactive chat assistant to help you:${NC}" +echo -e "1. Create your first price monitoring agent" +echo -e "2. Add a decision-making agent" +echo -e "3. Watch them collaborate in real-time" +echo -e "4. Learn about advanced agent capabilities" echo "" -echo -e "${BLUE}For help, run: near-swarm --help${NC}" \ No newline at end of file +echo -e "${CYAN}Press Enter to start the interactive experience...${NC}" +read -r + +# Launch interactive chat +section_header "šŸ’¬ Starting Interactive Chat" +echo -e "Tip: Type ${CYAN}/help${NC} to see available commands" +echo -e " Type ${CYAN}/exit${NC} to quit at any time\n" + +near-swarm chat --tutorial create-first-agent + +echo -e "\n${CYAN}Next steps:${NC}" +echo "1. Explore more agent templates: near-swarm plugins list" +echo "2. Create custom agents: near-swarm create agent my-agent" +echo "3. Run advanced demos:" +echo " ā€¢ Multi-agent trading: near-swarm demo trading" +echo " ā€¢ Portfolio management: near-swarm demo portfolio" +echo "" +echo -e "${BLUE}Documentation: https://github.com/jbarnes850/near-ai-agent-studio${NC}" \ No newline at end of file