diff --git a/.env.dev b/.env.dev index 6473df0..1e4e6fb 100644 --- a/.env.dev +++ b/.env.dev @@ -17,44 +17,54 @@ MEMORY_VECTOR_SIZE=1536 # === Agent Personality settings === -AGENT_PERSONALITY=You are a financial analyst. You are given a news article and some context. You need to analyze the news and provide insights. You are very naive and trustful. You are very optimistic and believe in the future of humanity. +AGENT_PERSONALITY=You are a Nevron, a financial analyst in crypto space. You are excellent in crypto and blockchain technology and your goal is to help people understand the crypto space better and avoid scams. You are very friendly, approachable and helpful. You are very optimistic and believe in the future of humanity. AGENT_GOAL=Your goal is to analyze the news and provide insights. +AGENT_REST_TIME=300 # === LLMs settings === LLM_PROVIDER=openai -PERPLEXITY_NEWS_PROMPT=Search for the latest cryptocurrency news: Neurobro - -OPENAI_API_KEY= OPENAI_MODEL=gpt-4o-mini -OPENAI_EMBEDDING_MODEL=text-embedding-3-small +OPENAI_API_KEY= + +XAI_MODEL=grok-2-latest +XAI_API_KEY= -ANTHROPIC_API_KEY= -ANTHROPIC_MODEL=claude-3-5-sonnet-20240620 +# === Third-party services settings === -PERPLEXITY_API_KEY=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 +# Perplexity +PERPLEXITY_API_KEY= PERPLEXITY_ENDPOINT=https://api.perplexity.ai/chat/completions +PERPLEXITY_NEWS_PROMPT=Search for the latest cryptocurrency news: Nevron +# Coinstats +COINSTATS_API_KEY= + +# Telegram TELEGRAM_BOT_TOKEN= TELEGRAM_CHAT_ID= -TELEGRAM_ADMIN_CHAT_ID= -TELEGRAM_REVIEWER_CHAT_IDS= +# Twitter TWITTER_BEARER_TOKEN= TWITTER_API_KEY= TWITTER_API_SECRET_KEY= TWITTER_ACCESS_TOKEN= TWITTER_ACCESS_TOKEN_SECRET= -# Discord + +# Discord settings DISCORD_BOT_TOKEN= DISCORD_CHANNEL_ID=0 - -#: YouTube +# YouTube settings YOUTUBE_API_KEY= YOUTUBE_PLAYLIST_ID= -#: WhatsApp -WHATSAPP_ID_INSTANCE= -WHATSAPP_API_TOKEN= +# WhatsApp settings +WHATSAPP_API_KEY= +WHATSAPP_PHONE_NUMBER= + +# Shopify settings +SHOPIFY_API_KEY= +SHOPIFY_PASSWORD= +SHOPIFY_STORE_NAME= \ No newline at end of file diff --git a/Pipfile b/Pipfile index e228ad1..ebd96c8 100644 --- a/Pipfile +++ b/Pipfile @@ -20,6 +20,7 @@ google-auth-oauthlib = "*" google-auth = "*" google-api-python-client = "*" whatsapp_api_client_python = "*" +ShopifyAPI = "*" aiohttp = "*" pillow = "*" types-requests = "*" diff --git a/Pipfile.lock b/Pipfile.lock index 12e352d..3c1135b 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "679c61883a55771810b48132ecbafe771f601725a923ff7241f3f39f980c5194" + "sha256": "477dac80f6d7a7595a5221c1112821404a323a174744a94687e1f7b931dbdb16" }, "pipfile-spec": 6, "requires": { @@ -2114,6 +2114,12 @@ "markers": "python_version >= '3.8'", "version": "==5.29.3" }, + "pyactiveresource": { + "hashes": [ + "sha256:2f03844652dc206d9a086b0b15564d78dcec55786fa5fe0055dd2119e0dffdd8" + ], + "version": "==2.2.2" + }, "pyasn1": { "hashes": [ "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629", @@ -2270,6 +2276,14 @@ "markers": "python_version >= '3.8'", "version": "==2.19.1" }, + "pyjwt": { + "hashes": [ + "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953", + "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb" + ], + "markers": "python_version >= '3.9'", + "version": "==2.10.1" + }, "pymdown-extensions": { "hashes": [ "sha256:202481f716cc8250e4be8fce997781ebf7917701b59652458ee47f2401f818b5", @@ -2704,6 +2718,13 @@ "markers": "python_version >= '3.7'", "version": "==1.5.4" }, + "shopifyapi": { + "hashes": [ + "sha256:d7a903eb8df659aba4b8dce6ad3d8dec0395b1a5f353bf59299eda6aa4fac289" + ], + "index": "pypi", + "version": "==12.7.0" + }, "six": { "hashes": [ "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", diff --git a/docs/agent/shopify.md b/docs/agent/shopify.md new file mode 100644 index 0000000..20a0fb0 --- /dev/null +++ b/docs/agent/shopify.md @@ -0,0 +1,73 @@ +# Shopify Integration + +## Setup + +1. Create a Shopify Private App + - Go to your Shopify admin panel + - Navigate to Apps > Develop apps + - Click "Create an app" + - Configure the app permissions (products, orders, inventory) + - Generate API credentials + +2. Get API Credentials + - Note down the API key + - Note down the API password (access token) + - Note your store name + - Or: create a test store beforehead, Go To Apps & sales channels > install your app > give neccessary permisions > Go To API Credentials and note down API key/password/store name + +3. Configure Environment Variables + Add these to your `.env` file: + ```bash + SHOPIFY_API_KEY=your_api_key_here + SHOPIFY_PASSWORD=your_password_here + SHOPIFY_STORE_NAME=your-store-name + ``` + +### Basic Setup +```python +from src.tools.shopify import initialize_shopify_client, get_products, get_orders, update_inventory + +# Initialize Shopify client +client = initialize_shopify_client() + +# Get all products +products = get_products(client) + +# Get all orders +orders = get_orders(client) + +# Update inventory for a product +update_inventory( + client=client, + product_id="product_id_here", + inventory_level=100 +) +``` + +## Features +- Secure authentication with Shopify API +- Retrieve product listings and details +- Access order information +- Manage inventory levels +- Error handling and logging +- SSL verification handling +- Session management + +## TODOs for Future Enhancements: +- Add support for creating and updating products +- Implement customer management +- Add support for discount codes +- Add support for handling Shopify webhooks for real-time updates +- Enable advanced features like creating new products or orders +- Enhance error handling and retry mechanisms for API interactions +- Add support for fulfillment operations +- Implement multi-location inventory management +- Add support for collection management +- Implement order processing workflows + +## Reference +For implementation details, see: `src/tools/shopify.py` + +The implementation uses the official Shopify Python API. For more information, refer to: +- [Shopify Admin API Documentation](https://shopify.dev/api/admin-rest) +- [Shopify Python API Library](https://github.com/Shopify/shopify_python_api) diff --git a/src/core/config.py b/src/core/config.py index 65b6cda..b06fc8c 100644 --- a/src/core/config.py +++ b/src/core/config.py @@ -167,6 +167,12 @@ class Settings(BaseSettings): #: WhatsApp API token WHATSAPP_API_TOKEN: str = "" + # --- Shopify settings --- + + SHOPIFY_API_KEY: str = "" + SHOPIFY_PASSWORD: str = "" + SHOPIFY_STORE_NAME: str = "" + # ========================== # Validators # ========================== diff --git a/src/core/exceptions.py b/src/core/exceptions.py index afe0829..ba4426c 100644 --- a/src/core/exceptions.py +++ b/src/core/exceptions.py @@ -53,3 +53,9 @@ class CoinstatsError(AgentBaseError): """Exception raised when Coinstats API request fails.""" pass + + +class ShopifyError(Exception): + """Raised when Shopify API operations fail.""" + + pass diff --git a/src/tools/shopify.py b/src/tools/shopify.py new file mode 100644 index 0000000..c5eadb2 --- /dev/null +++ b/src/tools/shopify.py @@ -0,0 +1,118 @@ +"""Shopify integration tool.""" + +import ssl +from typing import Dict, List + +import shopify +import urllib3 +from loguru import logger + +from src.core.config import settings +from src.core.exceptions import ShopifyError + +# Disable SSL verification warnings and disable SSL globally +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) +ssl._create_default_https_context = ssl._create_unverified_context + + +def initialize_shopify_client( + api_key: str = settings.SHOPIFY_API_KEY, + password: str = settings.SHOPIFY_PASSWORD, + store_name: str = settings.SHOPIFY_STORE_NAME, +) -> shopify.Session: + """ + Authenticate and return a Shopify client instance. + + Args: + api_key (str): API key for the Shopify store. + password (str): API password for the Shopify store. + store_name (str): The Shopify store name. + + Returns: + shopify.Session: Authenticated Shopify session. + """ + try: + api_version = "2024-01" + shop_url = f"https://{store_name}" + + # Initialize and activate session + session = shopify.Session(shop_url, api_version, password) + shopify.ShopifyResource.activate_session(session) + + return session + except Exception as e: + logger.error(f"Failed to initialize Shopify client: {e}") + raise ShopifyError(f"Authentication failed: {e}") + + +def get_products(client: shopify.Session) -> List[Dict]: + """ + Retrieve a list of products from the Shopify store. + + Args: + client (shopify.Session): Authenticated Shopify session. + + Returns: + List[Dict]: List of product details. + """ + try: + products = shopify.Product.find() + return [product.to_dict() for product in products] + except Exception as e: + logger.error(f"Error retrieving products: {e}") + shopify.ShopifyResource.clear_session() + raise ShopifyError(f"Failed to retrieve products: {e}") + + +def get_orders(client: shopify.Session) -> List[Dict]: + """ + Retrieve a list of orders from the Shopify store. + + Args: + client (shopify.Session): Authenticated Shopify session. + + Returns: + List[Dict]: List of order details. + """ + try: + orders = shopify.Order.find() + return [order.to_dict() for order in orders] + except Exception as e: + logger.error(f"Error retrieving orders: {e}") + shopify.ShopifyResource.clear_session() + raise ShopifyError(f"Failed to retrieve orders: {e}") + + +def update_inventory(client: shopify.Session, product_id: str, inventory_level: int) -> None: + """ + Update inventory for a specified product. + + Args: + client (shopify.Session): Authenticated Shopify session. + product_id (str): The ID of the product to update. + inventory_level (int): New inventory level. + """ + try: + # Get the product and its first variant + product = shopify.Product.find(product_id) + variant = product.variants[0] + + # Get the inventory item ID + inventory_item_id = variant.inventory_item_id + + # Get the location ID (usually the first/default location) + locations = shopify.Location.find() + location_id = locations[0].id + + # Update the inventory level + shopify.InventoryLevel.set( + location_id=location_id, inventory_item_id=inventory_item_id, available=inventory_level + ) + + logger.info( + f"Updated inventory for product {product_id} to {inventory_level} at location {location_id}" + ) + except Exception as e: + logger.error(f"Error updating inventory: {e}") + shopify.ShopifyResource.clear_session() + raise ShopifyError(f"Failed to update inventory: {e}") diff --git a/tests/tools/test_shopify.py b/tests/tools/test_shopify.py new file mode 100644 index 0000000..1a3c845 --- /dev/null +++ b/tests/tools/test_shopify.py @@ -0,0 +1,133 @@ +"""Tests for Shopify integration tool.""" + +from unittest.mock import MagicMock, patch + +import pytest +import shopify + +from src.core.exceptions import ShopifyError +from src.tools.shopify import get_orders, get_products, initialize_shopify_client, update_inventory + + +@pytest.fixture +def mock_shopify_session(): + """Mock Shopify session.""" + return MagicMock(spec=shopify.Session) + + +@pytest.fixture +def mock_product(): + """Mock Shopify product.""" + product = MagicMock() + variant = MagicMock() + variant.inventory_item_id = "inventory_123" + product.variants = [variant] + product.title = "Test Product" + return product + + +@pytest.fixture +def mock_location(): + """Mock Shopify location.""" + location = MagicMock() + location.id = "location_123" + return location + + +def test_initialize_shopify_client_success(): + """Test successful Shopify client initialization.""" + with ( + patch("shopify.Session") as mock_session, + patch("shopify.ShopifyResource.activate_session") as mock_activate, + ): + # Arrange + api_key = "" + password = "shpat_test_token" + store_name = "test-store.myshopify.com" + + # Act + session = initialize_shopify_client(api_key, password, store_name) + + # Assert + mock_session.assert_called_once_with(f"https://{store_name}", "2024-01", password) + mock_activate.assert_called_once() + assert session == mock_session.return_value + + +def test_initialize_shopify_client_failure(): + """Test Shopify client initialization with invalid credentials.""" + with patch("shopify.Session", side_effect=Exception("Invalid credentials")): + # Act & Assert + with pytest.raises(ShopifyError, match="Authentication failed"): + initialize_shopify_client("", "invalid_token", "test-store.myshopify.com") + + +def test_get_products_success(mock_shopify_session): + """Test successful product retrieval.""" + # Arrange + mock_products = [MagicMock(to_dict=lambda: {"id": "1", "title": "Test Product"})] + + with patch("shopify.Product.find", return_value=mock_products): + # Act + products = get_products(mock_shopify_session) + + # Assert + assert len(products) == 1 + assert products[0]["title"] == "Test Product" + + +def test_get_products_failure(mock_shopify_session): + """Test product retrieval failure.""" + with patch("shopify.Product.find", side_effect=Exception("API Error")): + # Act & Assert + with pytest.raises(ShopifyError, match="Failed to retrieve products"): + get_products(mock_shopify_session) + + +def test_get_orders_success(mock_shopify_session): + """Test successful order retrieval.""" + # Arrange + mock_orders = [MagicMock(to_dict=lambda: {"id": "1", "order_number": "1001"})] + + with patch("shopify.Order.find", return_value=mock_orders): + # Act + orders = get_orders(mock_shopify_session) + + # Assert + assert len(orders) == 1 + assert orders[0]["order_number"] == "1001" + + +def test_get_orders_failure(mock_shopify_session): + """Test order retrieval failure.""" + with patch("shopify.Order.find", side_effect=Exception("API Error")): + # Act & Assert + with pytest.raises(ShopifyError, match="Failed to retrieve orders"): + get_orders(mock_shopify_session) + + +def test_update_inventory_success(mock_shopify_session, mock_product, mock_location): + """Test successful inventory update.""" + # Arrange + with ( + patch("shopify.Product.find", return_value=mock_product), + patch("shopify.Location.find", return_value=[mock_location]), + patch("shopify.InventoryLevel.set") as mock_set, + ): + # Act + update_inventory(mock_shopify_session, "product_123", 10) + + # Assert + mock_set.assert_called_once_with( + location_id=mock_location.id, + inventory_item_id=mock_product.variants[0].inventory_item_id, + available=10, + ) + + +def test_update_inventory_failure(mock_shopify_session): + """Test inventory update failure.""" + with patch("shopify.Product.find", side_effect=Exception("API Error")): + # Act & Assert + with pytest.raises(ShopifyError, match="Failed to update inventory"): + update_inventory(mock_shopify_session, "invalid_id", 10)