From ab42c9dac67b3f9943727e335ab110956300a981 Mon Sep 17 00:00:00 2001 From: Anil Chandra Naidu Matcha Date: Wed, 17 Jun 2026 12:05:36 +0530 Subject: [PATCH 1/5] feat(tools): add MuAPI tool package init --- lib/crewai-tools/src/crewai_tools/tools/muapi_tool/__init__.py | 1 + 1 file changed, 1 insertion(+) create mode 100644 lib/crewai-tools/src/crewai_tools/tools/muapi_tool/__init__.py diff --git a/lib/crewai-tools/src/crewai_tools/tools/muapi_tool/__init__.py b/lib/crewai-tools/src/crewai_tools/tools/muapi_tool/__init__.py new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/lib/crewai-tools/src/crewai_tools/tools/muapi_tool/__init__.py @@ -0,0 +1 @@ + From 625e51783efc2ad0de4c15046b944ba92c4bef41 Mon Sep 17 00:00:00 2001 From: Anil Chandra Naidu Matcha Date: Wed, 17 Jun 2026 12:06:18 +0530 Subject: [PATCH 2/5] feat(tools): add MuApiImageTool and MuApiVideoTool --- .../tools/muapi_tool/muapi_tool.py | 194 ++++++++++++++++++ 1 file changed, 194 insertions(+) create mode 100644 lib/crewai-tools/src/crewai_tools/tools/muapi_tool/muapi_tool.py diff --git a/lib/crewai-tools/src/crewai_tools/tools/muapi_tool/muapi_tool.py b/lib/crewai-tools/src/crewai_tools/tools/muapi_tool/muapi_tool.py new file mode 100644 index 0000000000..4657655303 --- /dev/null +++ b/lib/crewai-tools/src/crewai_tools/tools/muapi_tool/muapi_tool.py @@ -0,0 +1,194 @@ +import json +import time +import urllib.request +from typing import Any, List, Literal, Optional + +from crewai.tools import BaseTool, EnvVar +from pydantic import BaseModel, Field + + +BASE_URL = "https://api.muapi.ai/api/v1" + +IMAGE_MODELS = [ + "flux-schnell", + "flux-dev", + "flux-kontext-dev", + "flux-kontext-pro", + "flux-kontext-max", + "hidream-fast", + "hidream-dev", + "hidream-full", + "midjourney", + "gpt4o", + "gpt-image-2", + "imagen4", + "imagen4-fast", + "seedream", + "reve", + "ideogram", + "hunyuan", + "wan2.1", + "qwen", +] + +VIDEO_MODELS = [ + "veo3", + "veo3-fast", + "kling-master", + "wan2.1", + "wan2.2", + "seedance-pro", + "seedance-pro-fast", + "runway", + "pixverse", + "sora", + "minimax-hailuo-02-pro", +] + + +class MuApiImageSchema(BaseModel): + """Input for MuAPI Image Generation Tool.""" + + prompt: str = Field(description="Text description of the image to generate.") + model: str = Field( + default="flux-schnell", + description=( + "Model to use. Options: " + ", ".join(IMAGE_MODELS) + ), + ) + width: Optional[int] = Field(default=None, description="Image width in pixels.") + height: Optional[int] = Field(default=None, description="Image height in pixels.") + + +class MuApiVideoSchema(BaseModel): + """Input for MuAPI Video Generation Tool.""" + + prompt: str = Field(description="Text description of the video to generate.") + model: str = Field( + default="veo3-fast", + description=( + "Model to use. Options: " + ", ".join(VIDEO_MODELS) + ), + ) + duration: int = Field(default=5, description="Duration in seconds (3–60).") + aspect_ratio: Literal["16:9", "9:16", "1:1", "4:3"] = Field( + default="16:9", description="Aspect ratio." + ) + + +def _submit_and_poll(api_key: str, endpoint: str, payload: dict, timeout: int = 300) -> str: + """Submit a muapi.ai job and poll until completion, returning the output URL.""" + import json as _json + + headers = {"x-api-key": api_key, "Content-Type": "application/json"} + body = _json.dumps(payload).encode() + + # Submit + req = urllib.request.Request(f"{BASE_URL}/{endpoint}", data=body, headers=headers, method="POST") + with urllib.request.urlopen(req, timeout=30) as resp: + data = _json.loads(resp.read()) + request_id = data["request_id"] + + # Poll + deadline = time.time() + timeout + while time.time() < deadline: + time.sleep(3) + poll_req = urllib.request.Request( + f"{BASE_URL}/predictions/{request_id}/result", + headers={"x-api-key": api_key}, + ) + with urllib.request.urlopen(poll_req, timeout=15) as resp: + result = _json.loads(resp.read()) + status = result.get("status", "pending") + if status == "completed": + outputs = result.get("outputs", []) + if not outputs: + raise RuntimeError("Generation completed but returned no outputs") + return outputs[0] + if status in ("failed", "cancelled"): + raise RuntimeError(f"Generation {status}: {result.get('error', '')}") + + raise TimeoutError(f"Generation timed out after {timeout}s") + + +class MuApiImageTool(BaseTool): + """Generate images using muapi.ai — a unified API for 400+ image generation models.""" + + name: str = "MuAPI Image Generator" + description: str = ( + "Generates images from text prompts using muapi.ai. " + "Supports Flux, Midjourney, GPT-4o Image, Google Imagen 4, " + "Seedream, HiDream, Reve, Ideogram, and more. " + "Returns the URL of the generated image." + ) + args_schema: type[BaseModel] = MuApiImageSchema + + env_vars: List[EnvVar] = [ + EnvVar( + name="MUAPI_API_KEY", + description="API key for muapi.ai. Get one at https://muapi.ai/dashboard/api-keys", + required=True, + ) + ] + + def _run(self, **kwargs: Any) -> str: + import os + + api_key = os.environ.get("MUAPI_API_KEY", "") + if not api_key: + return "Error: MUAPI_API_KEY environment variable not set." + + prompt = kwargs.get("prompt", "") + model = kwargs.get("model", "flux-schnell") + payload: dict = {"prompt": prompt} + if kwargs.get("width"): + payload["width"] = kwargs["width"] + if kwargs.get("height"): + payload["height"] = kwargs["height"] + + try: + url = _submit_and_poll(api_key, model, payload) + return json.dumps({"image_url": url, "model": model, "prompt": prompt}) + except Exception as e: + return f"Error generating image: {e}" + + +class MuApiVideoTool(BaseTool): + """Generate videos using muapi.ai — a unified API for 400+ video generation models.""" + + name: str = "MuAPI Video Generator" + description: str = ( + "Generates short videos from text prompts using muapi.ai. " + "Supports Veo3, Kling, Wan, Seedance, Runway, Pixverse, Sora, and more. " + "Returns the URL of the generated MP4 video." + ) + args_schema: type[BaseModel] = MuApiVideoSchema + + env_vars: List[EnvVar] = [ + EnvVar( + name="MUAPI_API_KEY", + description="API key for muapi.ai. Get one at https://muapi.ai/dashboard/api-keys", + required=True, + ) + ] + + def _run(self, **kwargs: Any) -> str: + import os + + api_key = os.environ.get("MUAPI_API_KEY", "") + if not api_key: + return "Error: MUAPI_API_KEY environment variable not set." + + prompt = kwargs.get("prompt", "") + model = kwargs.get("model", "veo3-fast") + payload: dict = { + "prompt": prompt, + "duration": kwargs.get("duration", 5), + "aspect_ratio": kwargs.get("aspect_ratio", "16:9"), + } + + try: + url = _submit_and_poll(api_key, model, payload, timeout=600) + return json.dumps({"video_url": url, "model": model, "prompt": prompt}) + except Exception as e: + return f"Error generating video: {e}" From 7d5ee6124045233745d1a7879877d560ae327f48 Mon Sep 17 00:00:00 2001 From: Anil Chandra Naidu Matcha Date: Wed, 17 Jun 2026 12:06:39 +0530 Subject: [PATCH 3/5] feat(tools): add MuAPI tools README --- .../crewai_tools/tools/muapi_tool/README.MD | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 lib/crewai-tools/src/crewai_tools/tools/muapi_tool/README.MD diff --git a/lib/crewai-tools/src/crewai_tools/tools/muapi_tool/README.MD b/lib/crewai-tools/src/crewai_tools/tools/muapi_tool/README.MD new file mode 100644 index 0000000000..b9bc2c705f --- /dev/null +++ b/lib/crewai-tools/src/crewai_tools/tools/muapi_tool/README.MD @@ -0,0 +1,48 @@ +# MuAPI Tools + +`MuApiImageTool` and `MuApiVideoTool` — generate images and videos using [muapi.ai](https://muapi.ai), a unified API with 400+ generative media models. + +## Setup + +```bash +export MUAPI_API_KEY=your_key_here +``` + +Get an API key at [muapi.ai/dashboard/api-keys](https://muapi.ai/dashboard/api-keys). + +## Usage + +```python +from crewai import Agent, Task, Crew +from crewai_tools import MuApiImageTool, MuApiVideoTool + +image_tool = MuApiImageTool() +video_tool = MuApiVideoTool() + +designer = Agent( + role="Creative Director", + goal="Create visual assets for marketing campaigns", + tools=[image_tool, video_tool], + backstory="You are a creative director who generates images and videos.", +) + +task = Task( + description="Generate a photorealistic product photo of a coffee mug on a marble table.", + expected_output="URL of the generated image", + agent=designer, +) + +crew = Crew(agents=[designer], tasks=[task]) +result = crew.kickoff() +``` + +## Supported Image Models + +`flux-schnell`, `flux-dev`, `flux-kontext-dev/pro/max`, `hidream-fast/dev/full`, +`midjourney`, `gpt4o`, `gpt-image-2`, `imagen4`, `imagen4-fast`, `seedream`, +`reve`, `ideogram`, `hunyuan`, `wan2.1`, `qwen` + +## Supported Video Models + +`veo3`, `veo3-fast`, `kling-master`, `wan2.1`, `wan2.2`, `seedance-pro`, +`seedance-pro-fast`, `runway`, `pixverse`, `sora`, `minimax-hailuo-02-pro` From 195815c1bafff9cc6ae3b70a3fad4374865ea9a0 Mon Sep 17 00:00:00 2001 From: Anil Chandra Naidu Matcha Date: Wed, 17 Jun 2026 12:07:08 +0530 Subject: [PATCH 4/5] feat(tools): export MuApiImageTool and MuApiVideoTool --- lib/crewai-tools/src/crewai_tools/tools/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/crewai-tools/src/crewai_tools/tools/__init__.py b/lib/crewai-tools/src/crewai_tools/tools/__init__.py index 18bf4e5638..29553325af 100644 --- a/lib/crewai-tools/src/crewai_tools/tools/__init__.py +++ b/lib/crewai-tools/src/crewai_tools/tools/__init__.py @@ -100,6 +100,7 @@ from crewai_tools.tools.merge_agent_handler_tool.merge_agent_handler_tool import ( MergeAgentHandlerTool, ) +from crewai_tools.tools.muapi_tool.muapi_tool import MuApiImageTool, MuApiVideoTool from crewai_tools.tools.mongodb_vector_search_tool import ( MongoDBToolSchema, MongoDBVectorSearchConfig, @@ -262,6 +263,8 @@ "MongoDBToolSchema", "MongoDBVectorSearchConfig", "MongoDBVectorSearchTool", + "MuApiImageTool", + "MuApiVideoTool", "MultiOnTool", "MySQLSearchTool", "NL2SQLTool", From cd7ab5a7460eefaf831624177b081107da11b567 Mon Sep 17 00:00:00 2001 From: Anil Chandra Naidu Matcha Date: Wed, 24 Jun 2026 13:56:08 +0530 Subject: [PATCH 5/5] fix: remove redundant json import and add error handling in _submit_and_poll --- .../tools/muapi_tool/muapi_tool.py | 39 ++++++++++++------- 1 file changed, 26 insertions(+), 13 deletions(-) diff --git a/lib/crewai-tools/src/crewai_tools/tools/muapi_tool/muapi_tool.py b/lib/crewai-tools/src/crewai_tools/tools/muapi_tool/muapi_tool.py index 4657655303..5d33db531e 100644 --- a/lib/crewai-tools/src/crewai_tools/tools/muapi_tool/muapi_tool.py +++ b/lib/crewai-tools/src/crewai_tools/tools/muapi_tool/muapi_tool.py @@ -1,5 +1,6 @@ import json import time +import urllib.error import urllib.request from typing import Any, List, Literal, Optional @@ -78,27 +79,39 @@ class MuApiVideoSchema(BaseModel): def _submit_and_poll(api_key: str, endpoint: str, payload: dict, timeout: int = 300) -> str: """Submit a muapi.ai job and poll until completion, returning the output URL.""" - import json as _json - headers = {"x-api-key": api_key, "Content-Type": "application/json"} - body = _json.dumps(payload).encode() + body = json.dumps(payload).encode() # Submit - req = urllib.request.Request(f"{BASE_URL}/{endpoint}", data=body, headers=headers, method="POST") - with urllib.request.urlopen(req, timeout=30) as resp: - data = _json.loads(resp.read()) - request_id = data["request_id"] + try: + req = urllib.request.Request(f"{BASE_URL}/{endpoint}", data=body, headers=headers, method="POST") + with urllib.request.urlopen(req, timeout=30) as resp: + data = json.loads(resp.read()) + except urllib.error.HTTPError as exc: + raise RuntimeError(f"MuAPI submit failed [{exc.code}]: {exc.read().decode(errors='replace')}") from exc + except urllib.error.URLError as exc: + raise RuntimeError(f"MuAPI submit connection error: {exc.reason}") from exc + + request_id = data.get("request_id") + if not request_id: + raise RuntimeError(f"MuAPI did not return a request_id: {data}") # Poll deadline = time.time() + timeout while time.time() < deadline: time.sleep(3) - poll_req = urllib.request.Request( - f"{BASE_URL}/predictions/{request_id}/result", - headers={"x-api-key": api_key}, - ) - with urllib.request.urlopen(poll_req, timeout=15) as resp: - result = _json.loads(resp.read()) + try: + poll_req = urllib.request.Request( + f"{BASE_URL}/predictions/{request_id}/result", + headers={"x-api-key": api_key}, + ) + with urllib.request.urlopen(poll_req, timeout=15) as resp: + result = json.loads(resp.read()) + except urllib.error.HTTPError as exc: + raise RuntimeError(f"MuAPI poll failed [{exc.code}]: {exc.read().decode(errors='replace')}") from exc + except urllib.error.URLError as exc: + raise RuntimeError(f"MuAPI poll connection error: {exc.reason}") from exc + status = result.get("status", "pending") if status == "completed": outputs = result.get("outputs", [])