diff --git a/README.md b/README.md index dd3de49d..84f0b683 100644 --- a/README.md +++ b/README.md @@ -1,67 +1,36 @@
The Enterprise-Grade Production-Ready Multi-Agent Orchestration Framework
+
-
-
-
-
-
-
-
-
-
-
-
+
+
-
-
-
-
- - 🏠Swarms Website - • - 📙 Documentation - • - 🛒 Swarms Marketplace -
- - ## ✨ Features @@ -160,12 +86,10 @@ $ poetry add swarms # Clone the repository $ git clone https://github.com/kyegomez/swarms.git $ cd swarms - -# Install with pip -$ pip install -e . +$ pip install -r requirements.txt ``` -### Using Docker + --- diff --git a/docs/swarms/structs/aop.md b/docs/swarms/structs/aop.md index 8062503a..0d62ce43 100644 --- a/docs/swarms/structs/aop.md +++ b/docs/swarms/structs/aop.md @@ -199,9 +199,14 @@ Run the MCP server (alias for start_server). ##### get_server_info() -Get information about the MCP server and registered tools. - -**Returns:** `Dict[str, Any]` - Server information +Get comprehensive information about the MCP server and registered tools, including metadata, configuration, tool details, queue stats, and network status. + +**Returns:** `Dict[str, Any]` - Server information including: +- Server metadata (name, description, creation time, uptime) +- Configuration (host, port, transport, log level) +- Agent information (total count, names, detailed tool info) +- Queue configuration and statistics (if queue enabled) +- Persistence and network status ##### _register_tool() diff --git a/examples/single_agent/utils/medical_agent_add_to_marketplace.py b/examples/single_agent/utils/medical_agent_add_to_marketplace.py new file mode 100644 index 00000000..6a6f1c2c --- /dev/null +++ b/examples/single_agent/utils/medical_agent_add_to_marketplace.py @@ -0,0 +1,88 @@ +import json +from swarms import Agent + +blood_analysis_system_prompt = """You are a clinical laboratory data analyst assistant focused on hematology and basic metabolic panels. +Your goals: +1) Interpret common blood test panels (CBC, CMP/BMP, lipid panel, HbA1c, thyroid panels) based on provided values, reference ranges, flags, and units. +2) Provide structured findings: out-of-range markers, degree of deviation, likely clinical significance, and differential considerations. +3) Identify potential pre-analytical, analytical, or biological confounders (e.g., hemolysis, fasting status, pregnancy, medications). +4) Suggest safe, non-diagnostic next steps: retest windows, confirmatory labs, context to gather, and when to escalate to a clinician. +5) Clearly separate “informational insights” from “non-medical advice” and include source-backed rationale where possible. + +Reliability and safety: +- This is not medical advice. Do not diagnose, treat, or provide definitive clinical decisions. +- Use cautious language; do not overstate certainty. Include confidence levels (low/medium/high). +- Highlight red-flag combinations that warrant urgent clinical evaluation. +- Prefer reputable sources: peer‑reviewed literature, clinical guidelines (e.g., WHO, CDC, NIH, NICE), and standard lab references. + +Output format (JSON-like sections, not strict JSON): +SECTION: SUMMARY +SECTION: KEY ABNORMALITIES +SECTION: DIFFERENTIAL CONSIDERATIONS +SECTION: RED FLAGS (if any) +SECTION: CONTEXT/CONFIDENCE +SECTION: SUGGESTED NON-CLINICAL NEXT STEPS +SECTION: SOURCES +""" + +# ========================= +# Medical Agents +# ========================= + +blood_analysis_agent = Agent( + agent_name="Blood-Data-Analysis-Agent", + agent_description="Explains and contextualizes common blood test panels with structured insights", + model_name="claude-haiku-4-5", + max_loops=1, + top_p=None, + dynamic_temperature_enabled=True, + system_prompt=blood_analysis_system_prompt, + tags=["lab", "hematology", "metabolic", "education"], + capabilities=[ + "panel-interpretation", + "risk-flagging", + "guideline-citation", + ], + role="worker", + temperature=None, + output_type="dict", + publish_to_marketplace=True, + use_cases=[ + { + "title": "Blood Analysis", + "description": ( + "Analyze blood samples and provide a report on the results, " + "highlighting significant deviations, clinical context, red flags, " + "and referencing established guidelines for lab test interpretation." + ), + }, + { + "title": "Longitudinal Patient Lab Monitoring", + "description": ( + "Process serial blood test results for a patient over time to identify clinical trends in key parameters (e.g., " + "progression of anemia, impact of pharmacologic therapy, signs of organ dysfunction). Generate structured summaries " + "that succinctly track rises, drops, or persistently abnormal markers. Flag patterns that suggest evolving risk or " + "require physician escalation, such as a dropping platelet count, rising creatinine, or new-onset hyperglycemia. " + "Report should distinguish true trends from ordinary biological variability, referencing clinical guidelines for " + "critical-change thresholds and best-practice follow-up actions." + ), + }, + { + "title": "Preoperative Laboratory Risk Stratification", + "description": ( + "Interpret pre-surgical laboratory panels as part of risk assessment for patients scheduled for procedures. Identify " + "abnormal or borderline values that may increase the risk of perioperative complications (e.g., bleeding risk from " + "thrombocytopenia, signs of undiagnosed infection, electrolyte imbalances affecting anesthesia safety). Structure the " + "output to clearly separate routine findings from emergent concerns, and suggest evidence-based adjustments, further " + "workup, or consultation needs before proceeding with surgery, based on current clinical best practices and guideline " + "recommendations." + ), + }, + ], +) + +out = blood_analysis_agent.run( + task="Analyze this blood sample: Hematology and Basic Metabolic Panel" +) + +print(json.dumps(out, indent=4)) diff --git a/images/b74ace74-8e49-42af-87ab-051d7fdab62a.png b/images/b74ace74-8e49-42af-87ab-051d7fdab62a.png deleted file mode 100644 index fd0572bd..00000000 Binary files a/images/b74ace74-8e49-42af-87ab-051d7fdab62a.png and /dev/null differ diff --git a/images/new_logo.png b/images/new_logo.png new file mode 100644 index 00000000..ddf05607 Binary files /dev/null and b/images/new_logo.png differ diff --git a/swarms/structs/agent.py b/swarms/structs/agent.py index 176a97e8..bb7aed30 100644 --- a/swarms/structs/agent.py +++ b/swarms/structs/agent.py @@ -102,6 +102,9 @@ from swarms.utils.litellm_tokenizer import count_tokens from swarms.utils.litellm_wrapper import LiteLLM from swarms.utils.output_types import OutputType from swarms.utils.pdf_to_text import pdf_to_text +from swarms.utils.swarms_marketplace_utils import ( + add_prompt_to_marketplace, +) # REACT workflow tools for auto mode @@ -479,7 +482,6 @@ class Agent: created_at: float = time.time(), return_step_meta: Optional[bool] = False, tags: Optional[List[str]] = None, - use_cases: Optional[List[Dict[str, str]]] = None, step_pool: List[Step] = [], print_every_step: Optional[bool] = False, time_created: Optional[str] = time.strftime( @@ -531,6 +533,8 @@ class Agent: handoffs: Optional[Union[Sequence[Callable], Any]] = None, capabilities: Optional[List[str]] = None, mode: Literal["interactive", "fast", "standard"] = "standard", + publish_to_marketplace: bool = False, + use_cases: Optional[List[Dict[str, Any]]] = None, *args, **kwargs, ): @@ -682,6 +686,7 @@ class Agent: self.handoffs = handoffs self.capabilities = capabilities self.mode = mode + self.publish_to_marketplace = publish_to_marketplace # Initialize transforms if transforms is None: @@ -755,6 +760,30 @@ class Agent: self.print_on = False self.verbose = False + if self.publish_to_marketplace is True: + # Join tags and capabilities into a single string + tags_and_capabilities = ", ".join( + self.tags + self.capabilities + if self.tags and self.capabilities + else None + ) + + if self.use_cases is None: + raise AgentInitializationError( + "Use cases are required when publishing to the marketplace. The schema is a list of dictionaries with 'title' and 'description' keys." + ) + + add_prompt_to_marketplace( + name=self.agent_name, + prompt=self.short_memory.get_str(), + description=self.agent_description, + tags=tags_and_capabilities, + category="research", + use_cases=( + self.use_cases if self.use_cases else None + ), + ) + def handle_handoffs(self, task: Optional[str] = None): router = MultiAgentRouter( name=self.agent_name, diff --git a/swarms/structs/aop.py b/swarms/structs/aop.py index b95acb77..17a58547 100644 --- a/swarms/structs/aop.py +++ b/swarms/structs/aop.py @@ -659,6 +659,9 @@ class AOP: self._last_network_error = None self._network_connected = True + # Server creation timestamp + self._created_at = time.time() + self.agents: Dict[str, Agent] = {} self.tool_configs: Dict[str, AgentToolConfig] = {} self.task_queues: Dict[str, TaskQueue] = {} @@ -1980,6 +1983,53 @@ class AOP: "matching_agents": [], } + @self.mcp_server.tool( + name="get_server_info", + description="Get comprehensive server information including metadata, configuration, tool details, queue stats, and network status.", + ) + def get_server_info_tool() -> Dict[str, Any]: + """ + Get comprehensive information about the MCP server and registered tools. + + Returns: + Dict containing server information with the following fields: + - server_name: Name of the server + - description: Server description + - total_tools/total_agents: Total number of agents registered + - tools/agent_names: List of all agent names + - created_at: Unix timestamp when server was created + - created_at_iso: ISO formatted creation time + - uptime_seconds: Server uptime in seconds + - host: Server host address + - port: Server port number + - transport: Transport protocol used + - log_level: Logging level + - queue_enabled: Whether queue system is enabled + - persistence_enabled: Whether persistence mode is enabled + - network_monitoring_enabled: Whether network monitoring is enabled + - persistence: Detailed persistence status + - network: Detailed network status + - tool_details: Detailed information about each agent tool + - queue_config: Queue configuration (if queue enabled) + - queue_stats: Queue statistics for each agent (if queue enabled) + """ + try: + server_info = self.get_server_info() + return { + "success": True, + "server_info": server_info, + } + except Exception as e: + error_msg = str(e) + logger.error( + f"Error in get_server_info tool: {error_msg}" + ) + return { + "success": False, + "error": error_msg, + "server_info": None, + } + def _register_queue_management_tools(self) -> None: """ Register queue management tools for the MCP server. @@ -2699,18 +2749,32 @@ class AOP: Get information about the MCP server and registered tools. Returns: - Dict containing server information + Dict containing server information including metadata, configuration, + and tool details """ info = { "server_name": self.server_name, "description": self.description, "total_tools": len(self.agents), + "total_agents": len( + self.agents + ), # Alias for compatibility "tools": self.list_agents(), + "agent_names": self.list_agents(), # Alias for compatibility + "created_at": self._created_at, + "created_at_iso": time.strftime( + "%Y-%m-%d %H:%M:%S", time.localtime(self._created_at) + ), + "uptime_seconds": time.time() - self._created_at, "verbose": self.verbose, "traceback_enabled": self.traceback_enabled, "log_level": self.log_level, "transport": self.transport, + "host": self.host, + "port": self.port, "queue_enabled": self.queue_enabled, + "persistence_enabled": self._persistence_enabled, # Top-level for compatibility + "network_monitoring_enabled": self.network_monitoring, # Top-level for compatibility "persistence": self.get_persistence_status(), "network": self.get_network_status(), "tool_details": { diff --git a/swarms/utils/swarms_marketplace_utils.py b/swarms/utils/swarms_marketplace_utils.py new file mode 100644 index 00000000..ac13dc17 --- /dev/null +++ b/swarms/utils/swarms_marketplace_utils.py @@ -0,0 +1,145 @@ +import os +import traceback +from typing import Any, Dict, List + +import httpx +from loguru import logger + + +def add_prompt_to_marketplace( + name: str = None, + prompt: str = None, + description: str = None, + use_cases: List[Dict[str, str]] = None, + tags: str = None, + is_free: bool = True, + price_usd: float = 0.0, + category: str = "research", + timeout: float = 30.0, +) -> Dict[str, Any]: + """ + Add a prompt to the Swarms marketplace. + + Args: + name: The name of the prompt. + prompt: The prompt text/template. + description: A description of what the prompt does. + use_cases: List of dictionaries with 'title' and 'description' keys + describing use cases for the prompt. + tags: Comma-separated string of tags for the prompt. + is_free: Whether the prompt is free or paid. + price_usd: Price in USD (ignored if is_free is True). + category: Category of the prompt (e.g., "content", "coding", etc.). + timeout: Request timeout in seconds. Defaults to 30.0. + + Returns: + Dictionary containing the API response. + + Raises: + httpx.HTTPError: If the HTTP request fails. + httpx.RequestError: If there's an error making the request. + """ + try: + url = "https://swarms.world/api/add-prompt" + api_key = os.getenv("SWARMS_API_KEY") + + if api_key is None or api_key.strip() == "": + raise ValueError( + "Swarms API key is not set. Please set the SWARMS_API_KEY environment variable. " + "You can get your key here: https://swarms.world/platform/api-keys" + ) + + # Log that we have an API key (without exposing it) + logger.debug( + f"Using API key (length: {len(api_key)} characters)" + ) + + # Validate required fields + if name is None: + raise ValueError("name is required") + if prompt is None: + raise ValueError("prompt is required") + if description is None: + raise ValueError("description is required") + if category is None: + raise ValueError("category is required") + if use_cases is None: + raise ValueError("use_cases is required") + + headers = { + "Authorization": f"Bearer {api_key}", + "Content-Type": "application/json", + } + + data = { + "name": name, + "prompt": prompt, + "description": description, + "useCases": use_cases or [], + "tags": tags or "", + "is_free": is_free, + "price_usd": price_usd, + "category": category, + } + + with httpx.Client(timeout=timeout) as client: + response = client.post(url, json=data, headers=headers) + + # Try to get response body for better error messages + try: + response_body = response.json() + except Exception: + response_body = response.text + + if response.status_code >= 400: + error_msg = f"HTTP {response.status_code}: {response.reason_phrase}" + if response_body: + error_msg += f"\nResponse: {response_body}" + logger.error( + f"Error adding prompt to marketplace: {error_msg}" + ) + + response.raise_for_status() + logger.info( + f"Prompt Name: {name} Successfully added to marketplace" + ) + return response_body + except httpx.HTTPStatusError as e: + logger.error(f"HTTP error adding prompt to marketplace: {e}") + if hasattr(e, "response") and e.response is not None: + try: + error_body = e.response.json() + logger.error(f"Error response body: {error_body}") + + # Provide helpful error message for authentication failures + if ( + e.response.status_code == 401 + or e.response.status_code == 500 + ): + if isinstance(error_body, dict): + if ( + "authentication" + in str(error_body).lower() + or "auth" in str(error_body).lower() + ): + logger.error( + "Authentication failed. Please check:\n" + "1. Your SWARMS_API_KEY environment variable is set correctly\n" + "2. Your API key is valid and not expired\n" + "3. You can verify your key at: https://swarms.world/platform/api-keys" + ) + except Exception: + logger.error( + f"Error response text: {e.response.text}" + ) + raise + except httpx.RequestError as e: + logger.error( + f"Request error adding prompt to marketplace: {e}" + ) + raise + except Exception as e: + logger.error( + f"Error adding prompt to marketplace: {e} Traceback: {traceback.format_exc()}" + ) + raise diff --git a/tests/utils/test_add_prompt_to_marketplace.py b/tests/utils/test_add_prompt_to_marketplace.py new file mode 100644 index 00000000..6c899fbf --- /dev/null +++ b/tests/utils/test_add_prompt_to_marketplace.py @@ -0,0 +1,306 @@ +""" +Pytest tests for swarms_marketplace_utils module. +""" +import os +from unittest.mock import Mock, patch + +import pytest + +from swarms.utils.swarms_marketplace_utils import ( + add_prompt_to_marketplace, +) + + +class TestAddPromptToMarketplace: + """Test cases for add_prompt_to_marketplace function.""" + + @patch.dict(os.environ, {"SWARMS_API_KEY": "test_api_key_12345"}) + @patch("swarms.utils.swarms_marketplace_utils.httpx.Client") + def test_add_prompt_success(self, mock_client_class): + """Test successful addition of prompt to marketplace.""" + # Mock response + mock_response = Mock() + mock_response.status_code = 200 + mock_response.json.return_value = { + "id": "123", + "name": "Blood Analysis Agent", + "status": "success", + } + mock_response.text = "" + mock_response.raise_for_status = Mock() + + # Mock client + mock_client = Mock() + mock_client.__enter__ = Mock(return_value=mock_client) + mock_client.__exit__ = Mock(return_value=False) + mock_client.post.return_value = mock_response + mock_client_class.return_value = mock_client + + # Call function + result = add_prompt_to_marketplace( + name="Blood Analysis Agent", + prompt="You are a blood analysis agent that can analyze blood samples and provide a report on the results.", + description="A blood analysis agent that can analyze blood samples and provide a report on the results.", + use_cases=[ + { + "title": "Blood Analysis", + "description": "Analyze blood samples and provide a report on the results.", + } + ], + tags="blood, analysis, report", + category="research", + ) + + # Assertions + assert result["id"] == "123" + assert result["name"] == "Blood Analysis Agent" + assert result["status"] == "success" + mock_client.post.assert_called_once() + call_args = mock_client.post.call_args + assert call_args[0][0] == "https://swarms.world/api/add-prompt" + assert call_args[1]["headers"]["Authorization"] == "Bearer test_api_key_12345" + assert call_args[1]["json"]["name"] == "Blood Analysis Agent" + assert call_args[1]["json"]["category"] == "research" + + @patch.dict(os.environ, {"SWARMS_API_KEY": "test_api_key_12345"}) + @patch("swarms.utils.swarms_marketplace_utils.httpx.Client") + def test_add_prompt_with_all_parameters(self, mock_client_class): + """Test adding prompt with all optional parameters.""" + # Mock response + mock_response = Mock() + mock_response.status_code = 200 + mock_response.json.return_value = {"id": "456", "status": "success"} + mock_response.text = "" + mock_response.raise_for_status = Mock() + + # Mock client + mock_client = Mock() + mock_client.__enter__ = Mock(return_value=mock_client) + mock_client.__exit__ = Mock(return_value=False) + mock_client.post.return_value = mock_response + mock_client_class.return_value = mock_client + + # Call function with all parameters + result = add_prompt_to_marketplace( + name="Test Prompt", + prompt="Test prompt text", + description="Test description", + use_cases=[{"title": "Use Case 1", "description": "Description 1"}], + tags="tag1, tag2", + is_free=False, + price_usd=9.99, + category="coding", + timeout=60.0, + ) + + # Assertions + assert result["id"] == "456" + call_args = mock_client.post.call_args + json_data = call_args[1]["json"] + assert json_data["is_free"] is False + assert json_data["price_usd"] == 9.99 + assert json_data["category"] == "coding" + assert json_data["tags"] == "tag1, tag2" + + def test_add_prompt_missing_api_key(self): + """Test that missing API key raises ValueError.""" + with patch.dict(os.environ, {}, clear=True): + with pytest.raises(ValueError, match="Swarms API key is not set"): + add_prompt_to_marketplace( + name="Test", + prompt="Test prompt", + description="Test description", + use_cases=[], + category="research", + ) + + def test_add_prompt_empty_api_key(self): + """Test that empty API key raises ValueError.""" + with patch.dict(os.environ, {"SWARMS_API_KEY": ""}): + with pytest.raises(ValueError, match="Swarms API key is not set"): + add_prompt_to_marketplace( + name="Test", + prompt="Test prompt", + description="Test description", + use_cases=[], + category="research", + ) + + def test_add_prompt_missing_name(self): + """Test that missing name raises ValueError.""" + with patch.dict(os.environ, {"SWARMS_API_KEY": "test_key"}): + with pytest.raises(ValueError, match="name is required"): + add_prompt_to_marketplace( + name=None, + prompt="Test prompt", + description="Test description", + use_cases=[], + category="research", + ) + + def test_add_prompt_missing_prompt(self): + """Test that missing prompt raises ValueError.""" + with patch.dict(os.environ, {"SWARMS_API_KEY": "test_key"}): + with pytest.raises(ValueError, match="prompt is required"): + add_prompt_to_marketplace( + name="Test", + prompt=None, + description="Test description", + use_cases=[], + category="research", + ) + + def test_add_prompt_missing_description(self): + """Test that missing description raises ValueError.""" + with patch.dict(os.environ, {"SWARMS_API_KEY": "test_key"}): + with pytest.raises(ValueError, match="description is required"): + add_prompt_to_marketplace( + name="Test", + prompt="Test prompt", + description=None, + use_cases=[], + category="research", + ) + + def test_add_prompt_missing_category(self): + """Test that missing category raises ValueError.""" + with patch.dict(os.environ, {"SWARMS_API_KEY": "test_key"}): + with pytest.raises(ValueError, match="category is required"): + add_prompt_to_marketplace( + name="Test", + prompt="Test prompt", + description="Test description", + use_cases=[], + category=None, + ) + + def test_add_prompt_missing_use_cases(self): + """Test that missing use_cases raises ValueError.""" + with patch.dict(os.environ, {"SWARMS_API_KEY": "test_key"}): + with pytest.raises(ValueError, match="use_cases is required"): + add_prompt_to_marketplace( + name="Test", + prompt="Test prompt", + description="Test description", + use_cases=None, + category="research", + ) + + @patch.dict(os.environ, {"SWARMS_API_KEY": "test_api_key_12345"}) + @patch("swarms.utils.swarms_marketplace_utils.httpx.Client") + def test_add_prompt_http_error(self, mock_client_class): + """Test handling of HTTP error responses.""" + # Mock response with error + mock_response = Mock() + mock_response.status_code = 400 + mock_response.reason_phrase = "Bad Request" + mock_response.json.return_value = {"error": "Invalid request"} + mock_response.text = '{"error": "Invalid request"}' + mock_response.raise_for_status.side_effect = Exception("HTTP 400") + + # Mock client + mock_client = Mock() + mock_client.__enter__ = Mock(return_value=mock_client) + mock_client.__exit__ = Mock(return_value=False) + mock_client.post.return_value = mock_response + mock_client_class.return_value = mock_client + + # Call function and expect exception + with pytest.raises(Exception): + add_prompt_to_marketplace( + name="Test", + prompt="Test prompt", + description="Test description", + use_cases=[], + category="research", + ) + + @patch.dict(os.environ, {"SWARMS_API_KEY": "test_api_key_12345"}) + @patch("swarms.utils.swarms_marketplace_utils.httpx.Client") + def test_add_prompt_authentication_error(self, mock_client_class): + """Test handling of authentication errors.""" + # Mock response with 401 error + mock_response = Mock() + mock_response.status_code = 401 + mock_response.reason_phrase = "Unauthorized" + mock_response.json.return_value = { + "error": "Authentication failed" + } + mock_response.text = '{"error": "Authentication failed"}' + mock_response.raise_for_status.side_effect = Exception("HTTP 401") + + # Mock client + mock_client = Mock() + mock_client.__enter__ = Mock(return_value=mock_client) + mock_client.__exit__ = Mock(return_value=False) + mock_client.post.return_value = mock_response + mock_client_class.return_value = mock_client + + # Call function and expect exception + with pytest.raises(Exception): + add_prompt_to_marketplace( + name="Test", + prompt="Test prompt", + description="Test description", + use_cases=[], + category="research", + ) + + @patch.dict(os.environ, {"SWARMS_API_KEY": "test_api_key_12345"}) + @patch("swarms.utils.swarms_marketplace_utils.httpx.Client") + def test_add_prompt_with_empty_tags(self, mock_client_class): + """Test adding prompt with empty tags.""" + # Mock response + mock_response = Mock() + mock_response.status_code = 200 + mock_response.json.return_value = {"id": "789", "status": "success"} + mock_response.text = "" + mock_response.raise_for_status = Mock() + + # Mock client + mock_client = Mock() + mock_client.__enter__ = Mock(return_value=mock_client) + mock_client.__exit__ = Mock(return_value=False) + mock_client.post.return_value = mock_response + mock_client_class.return_value = mock_client + + # Call function with empty tags + result = add_prompt_to_marketplace( + name="Test", + prompt="Test prompt", + description="Test description", + use_cases=[], + tags=None, + category="research", + ) + + # Assertions + assert result["id"] == "789" + call_args = mock_client.post.call_args + assert call_args[1]["json"]["tags"] == "" + + @patch.dict(os.environ, {"SWARMS_API_KEY": "test_api_key_12345"}) + @patch("swarms.utils.swarms_marketplace_utils.httpx.Client") + def test_add_prompt_request_timeout(self, mock_client_class): + """Test handling of request timeout.""" + # Mock client to raise timeout error + mock_client = Mock() + mock_client.__enter__ = Mock(return_value=mock_client) + mock_client.__exit__ = Mock(return_value=False) + mock_client.post.side_effect = Exception("Request timeout") + mock_client_class.return_value = mock_client + + # Call function and expect exception + with pytest.raises(Exception): + add_prompt_to_marketplace( + name="Test", + prompt="Test prompt", + description="Test description", + use_cases=[], + category="research", + timeout=5.0, + ) + + +if __name__ == "__main__": + pytest.main([__file__, "-v"])