mirror of
https://github.com/hwchase17/langchain.git
synced 2025-09-22 19:09:57 +00:00
Add EdenAI Tools (#9764)
This PR follows the Eden AI (LLM + embeddings) integration. #8633 We added different Tools to empower agents with new capabilities : - text: explicit content detection - image: explicit content detection - image: object detection - OCR: invoice parsing - OCR: ID parsing - audio: speech to text - audio: text to speech We plan to add more in the future (like translation, language detection, + others). Usage: ```python llm=EdenAI(feature="text",provider="openai", params={"temperature" : 0.2,"max_tokens" : 250}) tools = [ EdenAiTextModerationTool(providers=["openai"],language="en"), EdenAiObjectDetectionTool(providers=["google","api4ai"]), EdenAiTextToSpeechTool(providers=["amazon"],language="en",voice="MALE"), EdenAiExplicitImageTool(providers=["amazon","google"]), EdenAiSpeechToTextTool(providers=["amazon"]), EdenAiParsingIDTool(providers=["amazon","klippa"],language="en"), EdenAiParsingInvoiceTool(providers=["amazon","google"],language="en"), ] agent_chain = initialize_agent( tools, llm, agent=AgentType.ZERO_SHOT_REACT_DESCRIPTION, verbose=True, return_intermediate_steps=True, ) result = agent_chain(""" i have this text : 'i want to slap you' first : i want to know if this text contains explicit content or not . second : if it does contain explicit content i want to know what is the explicit content in this text, third : i want to make the text into speech . if there is URL in the observations , you will always put it in the output (final answer) . """) ``` output: > Entering new AgentExecutor chain... > I need to extract the information from the ID and then convert it to text and then to speech > Action: edenai_identity_parsing > Action Input: "https://www.citizencard.com/images/citizencard-uk-id-card-2023.jpg" > Observation: last_name : > value : ANGELA > given_names : > value : GREENE > birth_place : > birth_date : > value : 2000-11-09 > issuance_date : > expire_date : > document_id : > issuing_state : > address : > age : > country : > document_type : > value : DRIVER LICENSE FRONT > gender : > image_id : > image_signature : > mrz : > nationality : > Thought: I now need to convert the information to text and then to speech > Action: edenai_text_to_speech > Action Input: "Welcome Angela Greene!" > Observation: https://d14uq1pz7dzsdq.cloudfront.net/0c494819-0bbc-4433-bfa4-6e99bd9747ea_.mp3?Expires=1693316851&Signature=YcMoVQgPuIMEOuSpFuvhkFM8JoBMSoGMcZb7MVWdqw7JEf5~67q9dEI90o5todE5mYXB5zSYoib6rGrmfBl4Rn5~yqDwZ~Tmc24K75zpQZIEyt5~ZSnHuXy4IFWGmlIVuGYVGMGKxTGNeCRNUXDhT6TXGZlr4mwa79Ei1YT7KcNyc1dsTrYB96LphnsqOERx4X9J9XriSwxn70X8oUPFfQmLcitr-syDhiwd9Wdpg6J5yHAJjf657u7Z1lFTBMoXGBuw1VYmyno-3TAiPeUcVlQXPueJ-ymZXmwaITmGOfH7HipZngZBziofRAFdhMYbIjYhegu5jS7TxHwRuox32A__&Key-Pair-Id=K1F55BTI9AHGIK > Thought: I now know the final answer > Final Answer: https://d14uq1pz7dzsdq.cloudfront.net/0c494819-0bbc-4433-bfa4-6e99bd9747ea_.mp3?Expires=1693316851&Signature=YcMoVQgPuIMEOuSpFuvhkFM8JoBMSoGMcZb7MVWdqw7JEf5~67q9dEI90o5todE5mYXB5zSYoib6rGrmfBl4Rn5~yqDwZ~Tmc24K75zpQZIEyt5~ZSnHuXy4IFWGmlIVuGYVGMGKxTGNeCRNUXDhT6TXGZlr4mwa79Ei1YT7KcNyc1dsTrYB96LphnsqOERx4X9J9XriSwxn70X8oUPFfQmLcitr-syDhiwd9Wdpg6J5y > > Finished chain. Other examples are available in the jupyter notebook. This PR is made in parallel with EdenAI LLM update #8963 I apologize for the messy PR. While working in implementing Tools we realized there was a few problems we needed to fix on LLM as well. Ping: @hwchase17, @baskaryan --------- Co-authored-by: RedhaWassim <rwasssim@gmail.com>
This commit is contained in:
@@ -34,6 +34,16 @@ from langchain.tools.bing_search.tool import BingSearchResults, BingSearchRun
|
||||
from langchain.tools.brave_search.tool import BraveSearch
|
||||
from langchain.tools.convert_to_openai import format_tool_to_openai_function
|
||||
from langchain.tools.ddg_search.tool import DuckDuckGoSearchResults, DuckDuckGoSearchRun
|
||||
from langchain.tools.edenai import (
|
||||
EdenAiExplicitImageTool,
|
||||
EdenAiObjectDetectionTool,
|
||||
EdenAiParsingIDTool,
|
||||
EdenAiParsingInvoiceTool,
|
||||
EdenAiSpeechToTextTool,
|
||||
EdenAiTextModerationTool,
|
||||
EdenAiTextToSpeechTool,
|
||||
EdenaiTool,
|
||||
)
|
||||
from langchain.tools.file_management import (
|
||||
CopyFileTool,
|
||||
DeleteFileTool,
|
||||
@@ -149,6 +159,14 @@ __all__ = [
|
||||
"DeleteFileTool",
|
||||
"DuckDuckGoSearchResults",
|
||||
"DuckDuckGoSearchRun",
|
||||
"EdenAiExplicitImageTool",
|
||||
"EdenAiObjectDetectionTool",
|
||||
"EdenAiParsingIDTool",
|
||||
"EdenAiParsingInvoiceTool",
|
||||
"EdenAiTextToSpeechTool",
|
||||
"EdenAiSpeechToTextTool",
|
||||
"EdenAiTextModerationTool",
|
||||
"EdenaiTool",
|
||||
"ExtractHyperlinksTool",
|
||||
"ExtractTextTool",
|
||||
"FileSearchTool",
|
||||
|
34
libs/langchain/langchain/tools/edenai/__init__.py
Normal file
34
libs/langchain/langchain/tools/edenai/__init__.py
Normal file
@@ -0,0 +1,34 @@
|
||||
"""Edenai Tools."""
|
||||
from langchain.tools.edenai.audio_speech_to_text import (
|
||||
EdenAiSpeechToTextTool,
|
||||
)
|
||||
from langchain.tools.edenai.audio_text_to_speech import (
|
||||
EdenAiTextToSpeechTool,
|
||||
)
|
||||
from langchain.tools.edenai.edenai_base_tool import EdenaiTool
|
||||
from langchain.tools.edenai.image_explicitcontent import (
|
||||
EdenAiExplicitImageTool,
|
||||
)
|
||||
from langchain.tools.edenai.image_objectdetection import (
|
||||
EdenAiObjectDetectionTool,
|
||||
)
|
||||
from langchain.tools.edenai.ocr_identityparser import (
|
||||
EdenAiParsingIDTool,
|
||||
)
|
||||
from langchain.tools.edenai.ocr_invoiceparser import (
|
||||
EdenAiParsingInvoiceTool,
|
||||
)
|
||||
from langchain.tools.edenai.text_moderation import (
|
||||
EdenAiTextModerationTool,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"EdenAiExplicitImageTool",
|
||||
"EdenAiObjectDetectionTool",
|
||||
"EdenAiParsingIDTool",
|
||||
"EdenAiParsingInvoiceTool",
|
||||
"EdenAiTextToSpeechTool",
|
||||
"EdenAiSpeechToTextTool",
|
||||
"EdenAiTextModerationTool",
|
||||
"EdenaiTool",
|
||||
]
|
103
libs/langchain/langchain/tools/edenai/audio_speech_to_text.py
Normal file
103
libs/langchain/langchain/tools/edenai/audio_speech_to_text.py
Normal file
@@ -0,0 +1,103 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
import time
|
||||
from typing import List, Optional
|
||||
|
||||
import requests
|
||||
|
||||
from langchain.callbacks.manager import CallbackManagerForToolRun
|
||||
from langchain.pydantic_v1 import validator
|
||||
from langchain.tools.edenai.edenai_base_tool import EdenaiTool
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class EdenAiSpeechToTextTool(EdenaiTool):
|
||||
"""Tool that queries the Eden AI Speech To Text API.
|
||||
|
||||
for api reference check edenai documentation:
|
||||
https://app.edenai.run/bricks/speech/asynchronous-speech-to-text.
|
||||
|
||||
To use, you should have
|
||||
the environment variable ``EDENAI_API_KEY`` set with your API token.
|
||||
You can find your token here: https://app.edenai.run/admin/account/settings
|
||||
|
||||
"""
|
||||
|
||||
edenai_api_key: Optional[str] = None
|
||||
|
||||
name = "edenai_speech_to_text"
|
||||
description = (
|
||||
"A wrapper around edenai Services speech to text "
|
||||
"Useful for when you have to convert audio to text."
|
||||
"Input should be a url to an audio file."
|
||||
)
|
||||
is_async = True
|
||||
|
||||
language: Optional[str] = "en"
|
||||
speakers: Optional[int]
|
||||
profanity_filter: bool = False
|
||||
custom_vocabulary: Optional[List[str]]
|
||||
|
||||
feature: str = "audio"
|
||||
subfeature: str = "speech_to_text_async"
|
||||
base_url = "https://api.edenai.run/v2/audio/speech_to_text_async/"
|
||||
|
||||
@validator("providers")
|
||||
def check_only_one_provider_selected(cls, v: List[str]) -> List[str]:
|
||||
"""
|
||||
This tool has no feature to combine providers results.
|
||||
Therefore we only allow one provider
|
||||
"""
|
||||
if len(v) > 1:
|
||||
raise ValueError(
|
||||
"Please select only one provider. "
|
||||
"The feature to combine providers results is not available "
|
||||
"for this tool."
|
||||
)
|
||||
return v
|
||||
|
||||
def _wait_processing(self, url: str) -> requests.Response:
|
||||
for _ in range(10):
|
||||
time.sleep(1)
|
||||
audio_analysis_result = self._get_edenai(url)
|
||||
temp = audio_analysis_result.json()
|
||||
if temp["status"] == "finished":
|
||||
if temp["results"][self.providers[0]]["error"] is not None:
|
||||
raise Exception(
|
||||
f"""EdenAI returned an unexpected response
|
||||
{temp['results'][self.providers[0]]['error']}"""
|
||||
)
|
||||
else:
|
||||
return audio_analysis_result
|
||||
|
||||
raise Exception("Edenai speech to text job id processing Timed out")
|
||||
|
||||
def _parse_response(self, response: dict) -> str:
|
||||
return response["public_id"]
|
||||
|
||||
def _run(
|
||||
self,
|
||||
query: str,
|
||||
run_manager: Optional[CallbackManagerForToolRun] = None,
|
||||
) -> str:
|
||||
"""Use the tool."""
|
||||
all_params = {
|
||||
"file_url": query,
|
||||
"language": self.language,
|
||||
"speakers": self.speakers,
|
||||
"profanity_filter": self.profanity_filter,
|
||||
"custom_vocabulary": self.custom_vocabulary,
|
||||
}
|
||||
|
||||
# filter so we don't send val to api when val is `None
|
||||
query_params = {k: v for k, v in all_params.items() if v is not None}
|
||||
|
||||
job_id = self._call_eden_ai(query_params)
|
||||
url = self.base_url + job_id
|
||||
audio_analysis_result = self._wait_processing(url)
|
||||
result = audio_analysis_result.text
|
||||
formatted_text = json.loads(result)
|
||||
return formatted_text["results"][self.providers[0]]["text"]
|
116
libs/langchain/langchain/tools/edenai/audio_text_to_speech.py
Normal file
116
libs/langchain/langchain/tools/edenai/audio_text_to_speech.py
Normal file
@@ -0,0 +1,116 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Dict, List, Literal, Optional
|
||||
|
||||
import requests
|
||||
|
||||
from langchain.callbacks.manager import CallbackManagerForToolRun
|
||||
from langchain.pydantic_v1 import Field, root_validator, validator
|
||||
from langchain.tools.edenai.edenai_base_tool import EdenaiTool
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class EdenAiTextToSpeechTool(EdenaiTool):
|
||||
"""Tool that queries the Eden AI Text to speech API.
|
||||
for api reference check edenai documentation:
|
||||
https://docs.edenai.co/reference/audio_text_to_speech_create.
|
||||
|
||||
To use, you should have
|
||||
the environment variable ``EDENAI_API_KEY`` set with your API token.
|
||||
You can find your token here: https://app.edenai.run/admin/account/settings
|
||||
|
||||
"""
|
||||
|
||||
name = "edenai_text_to_speech"
|
||||
description = (
|
||||
"A wrapper around edenai Services text to speech."
|
||||
"Useful for when you need to convert text to speech."
|
||||
"""the output is a string representing the URL of the audio file,
|
||||
or the path to the downloaded wav file """
|
||||
)
|
||||
|
||||
language: Optional[str] = "en"
|
||||
"""
|
||||
language of the text passed to the model.
|
||||
"""
|
||||
|
||||
# optional params see api documentation for more info
|
||||
return_type: Literal["url", "wav"] = "url"
|
||||
rate: Optional[int]
|
||||
pitch: Optional[int]
|
||||
volume: Optional[int]
|
||||
audio_format: Optional[str]
|
||||
sampling_rate: Optional[int]
|
||||
voice_models: Dict[str, str] = Field(default_factory=dict)
|
||||
|
||||
voice: Literal["MALE", "FEMALE"]
|
||||
"""voice option : 'MALE' or 'FEMALE' """
|
||||
|
||||
feature: str = "audio"
|
||||
subfeature: str = "text_to_speech"
|
||||
|
||||
@validator("providers")
|
||||
def check_only_one_provider_selected(cls, v: List[str]) -> List[str]:
|
||||
"""
|
||||
This tool has no feature to combine providers results.
|
||||
Therefore we only allow one provider
|
||||
"""
|
||||
if len(v) > 1:
|
||||
raise ValueError(
|
||||
"Please select only one provider. "
|
||||
"The feature to combine providers results is not available "
|
||||
"for this tool."
|
||||
)
|
||||
return v
|
||||
|
||||
@root_validator
|
||||
def check_voice_models_key_is_provider_name(cls, values: dict) -> dict:
|
||||
for key in values.get("voice_models", {}).keys():
|
||||
if key not in values.get("providers", []):
|
||||
raise ValueError(
|
||||
"voice_model should be formatted like this "
|
||||
"{<provider_name>: <its_voice_model>}"
|
||||
)
|
||||
return values
|
||||
|
||||
def _download_wav(self, url: str, save_path: str) -> None:
|
||||
response = requests.get(url)
|
||||
if response.status_code == 200:
|
||||
with open(save_path, "wb") as f:
|
||||
f.write(response.content)
|
||||
else:
|
||||
raise ValueError("Error while downloading wav file")
|
||||
|
||||
def _parse_response(self, response: list) -> str:
|
||||
result = response[0]
|
||||
if self.return_type == "url":
|
||||
return result["audio_resource_url"]
|
||||
else:
|
||||
self._download_wav(result["audio_resource_url"], "audio.wav")
|
||||
return "audio.wav"
|
||||
|
||||
def _run(
|
||||
self,
|
||||
query: str,
|
||||
run_manager: Optional[CallbackManagerForToolRun] = None,
|
||||
) -> str:
|
||||
"""Use the tool."""
|
||||
all_params = {
|
||||
"text": query,
|
||||
"language": self.language,
|
||||
"option": self.voice,
|
||||
"return_type": self.return_type,
|
||||
"rate": self.rate,
|
||||
"pitch": self.pitch,
|
||||
"volume": self.volume,
|
||||
"audio_format": self.audio_format,
|
||||
"sampling_rate": self.sampling_rate,
|
||||
"settings": self.voice_models,
|
||||
}
|
||||
|
||||
# filter so we don't send val to api when val is `None
|
||||
query_params = {k: v for k, v in all_params.items() if v is not None}
|
||||
|
||||
return self._call_eden_ai(query_params)
|
160
libs/langchain/langchain/tools/edenai/edenai_base_tool.py
Normal file
160
libs/langchain/langchain/tools/edenai/edenai_base_tool.py
Normal file
@@ -0,0 +1,160 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from abc import abstractmethod
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
import requests
|
||||
|
||||
from langchain.callbacks.manager import CallbackManagerForToolRun
|
||||
from langchain.pydantic_v1 import root_validator
|
||||
from langchain.tools.base import BaseTool
|
||||
from langchain.utils import get_from_dict_or_env
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class EdenaiTool(BaseTool):
|
||||
|
||||
"""
|
||||
the base tool for all the EdenAI Tools .
|
||||
you should have
|
||||
the environment variable ``EDENAI_API_KEY`` set with your API token.
|
||||
You can find your token here: https://app.edenai.run/admin/account/settings
|
||||
"""
|
||||
|
||||
feature: str
|
||||
subfeature: str
|
||||
edenai_api_key: Optional[str] = None
|
||||
is_async: bool = False
|
||||
|
||||
providers: List[str]
|
||||
"""provider to use for the API call."""
|
||||
|
||||
@root_validator(allow_reuse=True)
|
||||
def validate_environment(cls, values: Dict) -> Dict:
|
||||
"""Validate that api key exists in environment."""
|
||||
values["edenai_api_key"] = get_from_dict_or_env(
|
||||
values, "edenai_api_key", "EDENAI_API_KEY"
|
||||
)
|
||||
return values
|
||||
|
||||
@staticmethod
|
||||
def get_user_agent() -> str:
|
||||
from langchain import __version__
|
||||
|
||||
return f"langchain/{__version__}"
|
||||
|
||||
def _call_eden_ai(self, query_params: Dict[str, Any]) -> str:
|
||||
"""
|
||||
Make an API call to the EdenAI service with the specified query parameters.
|
||||
|
||||
Args:
|
||||
query_params (dict): The parameters to include in the API call.
|
||||
|
||||
Returns:
|
||||
requests.Response: The response from the EdenAI API call.
|
||||
|
||||
"""
|
||||
|
||||
# faire l'API call
|
||||
|
||||
headers = {
|
||||
"Authorization": f"Bearer {self.edenai_api_key}",
|
||||
"User-Agent": self.get_user_agent(),
|
||||
}
|
||||
|
||||
url = f"https://api.edenai.run/v2/{self.feature}/{self.subfeature}"
|
||||
|
||||
payload = {
|
||||
"providers": str(self.providers),
|
||||
"response_as_dict": False,
|
||||
"attributes_as_list": True,
|
||||
"show_original_response": False,
|
||||
}
|
||||
|
||||
payload.update(query_params)
|
||||
|
||||
response = requests.post(url, json=payload, headers=headers)
|
||||
|
||||
self._raise_on_error(response)
|
||||
|
||||
try:
|
||||
return self._parse_response(response.json())
|
||||
except Exception as e:
|
||||
raise RuntimeError(f"An error occurred while running tool: {e}")
|
||||
|
||||
def _raise_on_error(self, response: requests.Response) -> None:
|
||||
if response.status_code >= 500:
|
||||
raise Exception(f"EdenAI Server: Error {response.status_code}")
|
||||
elif response.status_code >= 400:
|
||||
raise ValueError(f"EdenAI received an invalid payload: {response.text}")
|
||||
elif response.status_code != 200:
|
||||
raise Exception(
|
||||
f"EdenAI returned an unexpected response with status "
|
||||
f"{response.status_code}: {response.text}"
|
||||
)
|
||||
|
||||
# case where edenai call succeeded but provider returned an error
|
||||
# (eg: rate limit, server error, etc.)
|
||||
if self.is_async is False:
|
||||
# async call are different and only return a job_id,
|
||||
# not the provider response directly
|
||||
provider_response = response.json()[0]
|
||||
if provider_response.get("status") == "fail":
|
||||
err_msg = provider_response["error"]["message"]
|
||||
raise ValueError(err_msg)
|
||||
|
||||
@abstractmethod
|
||||
def _run(
|
||||
self, query: str, run_manager: Optional[CallbackManagerForToolRun] = None
|
||||
) -> str:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def _parse_response(self, response: Any) -> str:
|
||||
"""Take a dict response and condense it's data in a human readable string"""
|
||||
pass
|
||||
|
||||
def _get_edenai(self, url: str) -> requests.Response:
|
||||
headers = {
|
||||
"accept": "application/json",
|
||||
"authorization": f"Bearer {self.edenai_api_key}",
|
||||
"User-Agent": self.get_user_agent(),
|
||||
}
|
||||
|
||||
response = requests.get(url, headers=headers)
|
||||
|
||||
self._raise_on_error(response)
|
||||
|
||||
return response
|
||||
|
||||
def _parse_json_multilevel(
|
||||
self, extracted_data: dict, formatted_list: list, level: int = 0
|
||||
) -> None:
|
||||
for section, subsections in extracted_data.items():
|
||||
indentation = " " * level
|
||||
if isinstance(subsections, str):
|
||||
subsections = subsections.replace("\n", ",")
|
||||
formatted_list.append(f"{indentation}{section} : {subsections}")
|
||||
|
||||
elif isinstance(subsections, list):
|
||||
formatted_list.append(f"{indentation}{section} : ")
|
||||
self._list_handling(subsections, formatted_list, level + 1)
|
||||
|
||||
elif isinstance(subsections, dict):
|
||||
formatted_list.append(f"{indentation}{section} : ")
|
||||
self._parse_json_multilevel(subsections, formatted_list, level + 1)
|
||||
|
||||
def _list_handling(
|
||||
self, subsection_list: list, formatted_list: list, level: int
|
||||
) -> None:
|
||||
for list_item in subsection_list:
|
||||
if isinstance(list_item, dict):
|
||||
self._parse_json_multilevel(list_item, formatted_list, level)
|
||||
|
||||
elif isinstance(list_item, list):
|
||||
self._list_handling(list_item, formatted_list, level + 1)
|
||||
|
||||
else:
|
||||
formatted_list.append(f"{' ' * level}{list_item}")
|
@@ -0,0 +1,67 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from langchain.callbacks.manager import CallbackManagerForToolRun
|
||||
from langchain.tools.edenai.edenai_base_tool import EdenaiTool
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class EdenAiExplicitImageTool(EdenaiTool):
|
||||
|
||||
"""Tool that queries the Eden AI Explicit image detection.
|
||||
|
||||
for api reference check edenai documentation:
|
||||
https://docs.edenai.co/reference/image_explicit_content_create.
|
||||
|
||||
To use, you should have
|
||||
the environment variable ``EDENAI_API_KEY`` set with your API token.
|
||||
You can find your token here: https://app.edenai.run/admin/account/settings
|
||||
|
||||
"""
|
||||
|
||||
name = "edenai_image_explicit_content_detection"
|
||||
|
||||
description = (
|
||||
"A wrapper around edenai Services Explicit image detection. "
|
||||
"""Useful for when you have to extract Explicit Content from images.
|
||||
it detects adult only content in images,
|
||||
that is generally inappropriate for people under
|
||||
the age of 18 and includes nudity, sexual activity,
|
||||
pornography, violence, gore content, etc."""
|
||||
"Input should be the string url of the image ."
|
||||
)
|
||||
|
||||
combine_available = True
|
||||
feature = "image"
|
||||
subfeature = "explicit_content"
|
||||
|
||||
def _parse_json(self, json_data: dict) -> str:
|
||||
result_str = f"nsfw_likelihood: {json_data['nsfw_likelihood']}\n"
|
||||
for idx, found_obj in enumerate(json_data["items"]):
|
||||
label = found_obj["label"].lower()
|
||||
likelihood = found_obj["likelihood"]
|
||||
result_str += f"{idx}: {label} likelihood {likelihood},\n"
|
||||
|
||||
return result_str[:-2]
|
||||
|
||||
def _parse_response(self, json_data: list) -> str:
|
||||
if len(json_data) == 1:
|
||||
result = self._parse_json(json_data[0])
|
||||
else:
|
||||
for entry in json_data:
|
||||
if entry.get("provider") == "eden-ai":
|
||||
result = self._parse_json(entry)
|
||||
|
||||
return result
|
||||
|
||||
def _run(
|
||||
self,
|
||||
query: str,
|
||||
run_manager: Optional[CallbackManagerForToolRun] = None,
|
||||
) -> str:
|
||||
"""Use the tool."""
|
||||
query_params = {"file_url": query, "attributes_as_list": False}
|
||||
return self._call_eden_ai(query_params)
|
@@ -0,0 +1,75 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from langchain.callbacks.manager import CallbackManagerForToolRun
|
||||
from langchain.tools.edenai.edenai_base_tool import EdenaiTool
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class EdenAiObjectDetectionTool(EdenaiTool):
|
||||
"""Tool that queries the Eden AI Object detection API.
|
||||
|
||||
for api reference check edenai documentation:
|
||||
https://docs.edenai.co/reference/image_object_detection_create.
|
||||
|
||||
To use, you should have
|
||||
the environment variable ``EDENAI_API_KEY`` set with your API token.
|
||||
You can find your token here: https://app.edenai.run/admin/account/settings
|
||||
|
||||
"""
|
||||
|
||||
name = "edenai_object_detection"
|
||||
|
||||
description = (
|
||||
"A wrapper around edenai Services Object Detection . "
|
||||
"""Useful for when you have to do an to identify and locate
|
||||
(with bounding boxes) objects in an image """
|
||||
"Input should be the string url of the image to identify."
|
||||
)
|
||||
|
||||
show_positions: bool = False
|
||||
|
||||
feature = "image"
|
||||
subfeature = "object_detection"
|
||||
|
||||
def _parse_json(self, json_data: dict) -> str:
|
||||
result = []
|
||||
label_info = []
|
||||
|
||||
for found_obj in json_data["items"]:
|
||||
label_str = f"{found_obj['label']} - Confidence {found_obj['confidence']}"
|
||||
x_min = found_obj.get("x_min")
|
||||
x_max = found_obj.get("x_max")
|
||||
y_min = found_obj.get("y_min")
|
||||
y_max = found_obj.get("y_max")
|
||||
if self.show_positions and all(
|
||||
[x_min, x_max, y_min, y_max]
|
||||
): # some providers don't return positions
|
||||
label_str += f""",at the position x_min: {x_min}, x_max: {x_max},
|
||||
y_min: {y_min}, y_max: {y_max}"""
|
||||
label_info.append(label_str)
|
||||
|
||||
result.append("\n".join(label_info))
|
||||
return "\n\n".join(result)
|
||||
|
||||
def _parse_response(self, response: list) -> str:
|
||||
if len(response) == 1:
|
||||
result = self._parse_json(response[0])
|
||||
else:
|
||||
for entry in response:
|
||||
if entry.get("provider") == "eden-ai":
|
||||
result = self._parse_json(entry)
|
||||
|
||||
return result
|
||||
|
||||
def _run(
|
||||
self,
|
||||
query: str,
|
||||
run_manager: Optional[CallbackManagerForToolRun] = None,
|
||||
) -> str:
|
||||
"""Use the tool."""
|
||||
query_params = {"file_url": query, "attributes_as_list": False}
|
||||
return self._call_eden_ai(query_params)
|
68
libs/langchain/langchain/tools/edenai/ocr_identityparser.py
Normal file
68
libs/langchain/langchain/tools/edenai/ocr_identityparser.py
Normal file
@@ -0,0 +1,68 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from langchain.callbacks.manager import CallbackManagerForToolRun
|
||||
from langchain.tools.edenai.edenai_base_tool import EdenaiTool
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class EdenAiParsingIDTool(EdenaiTool):
|
||||
"""Tool that queries the Eden AI Identity parsing API.
|
||||
|
||||
for api reference check edenai documentation:
|
||||
https://docs.edenai.co/reference/ocr_identity_parser_create.
|
||||
|
||||
To use, you should have
|
||||
the environment variable ``EDENAI_API_KEY`` set with your API token.
|
||||
You can find your token here: https://app.edenai.run/admin/account/settings
|
||||
|
||||
"""
|
||||
|
||||
name = "edenai_identity_parsing"
|
||||
|
||||
description = (
|
||||
"A wrapper around edenai Services Identity parsing. "
|
||||
"Useful for when you have to extract information from an ID Document "
|
||||
"Input should be the string url of the document to parse."
|
||||
)
|
||||
|
||||
feature = "ocr"
|
||||
subfeature = "identity_parser"
|
||||
|
||||
language: Optional[str] = None
|
||||
"""
|
||||
language of the text passed to the model.
|
||||
"""
|
||||
|
||||
def _parse_response(self, response: list) -> str:
|
||||
formatted_list: list = []
|
||||
|
||||
if len(response) == 1:
|
||||
self._parse_json_multilevel(
|
||||
response[0]["extracted_data"][0], formatted_list
|
||||
)
|
||||
else:
|
||||
for entry in response:
|
||||
if entry.get("provider") == "eden-ai":
|
||||
self._parse_json_multilevel(
|
||||
entry["extracted_data"][0], formatted_list
|
||||
)
|
||||
|
||||
return "\n".join(formatted_list)
|
||||
|
||||
def _run(
|
||||
self,
|
||||
query: str,
|
||||
run_manager: Optional[CallbackManagerForToolRun] = None,
|
||||
) -> str:
|
||||
"""Use the tool."""
|
||||
query_params = {
|
||||
"file_url": query,
|
||||
"language": self.language,
|
||||
"attributes_as_list": False,
|
||||
}
|
||||
|
||||
return self._call_eden_ai(query_params)
|
72
libs/langchain/langchain/tools/edenai/ocr_invoiceparser.py
Normal file
72
libs/langchain/langchain/tools/edenai/ocr_invoiceparser.py
Normal file
@@ -0,0 +1,72 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from langchain.callbacks.manager import CallbackManagerForToolRun
|
||||
from langchain.tools.edenai.edenai_base_tool import EdenaiTool
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class EdenAiParsingInvoiceTool(EdenaiTool):
|
||||
"""Tool that queries the Eden AI Invoice parsing API.
|
||||
|
||||
for api reference check edenai documentation:
|
||||
https://docs.edenai.co/reference/ocr_invoice_parser_create.
|
||||
|
||||
To use, you should have
|
||||
the environment variable ``EDENAI_API_KEY`` set with your API token.
|
||||
You can find your token here: https://app.edenai.run/admin/account/settings
|
||||
|
||||
"""
|
||||
|
||||
name = "edenai_invoice_parsing"
|
||||
|
||||
description = (
|
||||
"A wrapper around edenai Services invoice parsing. "
|
||||
"""Useful for when you have to extract information from
|
||||
an image it enables to take invoices
|
||||
in a variety of formats and returns the data in contains
|
||||
(items, prices, addresses, vendor name, etc.)
|
||||
in a structured format to automate the invoice processing """
|
||||
"Input should be the string url of the document to parse."
|
||||
)
|
||||
|
||||
language: Optional[str] = None
|
||||
"""
|
||||
language of the image passed to the model.
|
||||
"""
|
||||
|
||||
feature = "ocr"
|
||||
subfeature = "invoice_parser"
|
||||
|
||||
def _parse_response(self, response: list) -> str:
|
||||
formatted_list: list = []
|
||||
|
||||
if len(response) == 1:
|
||||
self._parse_json_multilevel(
|
||||
response[0]["extracted_data"][0], formatted_list
|
||||
)
|
||||
else:
|
||||
for entry in response:
|
||||
if entry.get("provider") == "eden-ai":
|
||||
self._parse_json_multilevel(
|
||||
entry["extracted_data"][0], formatted_list
|
||||
)
|
||||
|
||||
return "\n".join(formatted_list)
|
||||
|
||||
def _run(
|
||||
self,
|
||||
query: str,
|
||||
run_manager: Optional[CallbackManagerForToolRun] = None,
|
||||
) -> str:
|
||||
"""Use the tool."""
|
||||
query_params = {
|
||||
"file_url": query,
|
||||
"language": self.language,
|
||||
"attributes_as_list": False,
|
||||
}
|
||||
|
||||
return self._call_eden_ai(query_params)
|
72
libs/langchain/langchain/tools/edenai/text_moderation.py
Normal file
72
libs/langchain/langchain/tools/edenai/text_moderation.py
Normal file
@@ -0,0 +1,72 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from langchain.callbacks.manager import CallbackManagerForToolRun
|
||||
from langchain.tools.edenai.edenai_base_tool import EdenaiTool
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class EdenAiTextModerationTool(EdenaiTool):
|
||||
"""Tool that queries the Eden AI Explicit text detection.
|
||||
|
||||
for api reference check edenai documentation:
|
||||
https://docs.edenai.co/reference/image_explicit_content_create.
|
||||
|
||||
To use, you should have
|
||||
the environment variable ``EDENAI_API_KEY`` set with your API token.
|
||||
You can find your token here: https://app.edenai.run/admin/account/settings
|
||||
|
||||
"""
|
||||
|
||||
name = "edenai_explicit_content_detection_text"
|
||||
|
||||
description = (
|
||||
"A wrapper around edenai Services explicit content detection for text. "
|
||||
"""Useful for when you have to scan text for offensive,
|
||||
sexually explicit or suggestive content,
|
||||
it checks also if there is any content of self-harm,
|
||||
violence, racist or hate speech."""
|
||||
"""the structure of the output is :
|
||||
'the type of the explicit content : the likelihood of it being explicit'
|
||||
the likelihood is a number
|
||||
between 1 and 5, 1 being the lowest and 5 the highest.
|
||||
something is explicit if the likelihood is equal or higher than 3.
|
||||
for example :
|
||||
nsfw_likelihood: 1
|
||||
this is not explicit.
|
||||
for example :
|
||||
nsfw_likelihood: 3
|
||||
this is explicit.
|
||||
"""
|
||||
"Input should be a string."
|
||||
)
|
||||
|
||||
language: str
|
||||
|
||||
feature: str = "text"
|
||||
subfeature: str = "moderation"
|
||||
|
||||
def _parse_response(self, response: list) -> str:
|
||||
formatted_result = []
|
||||
for result in response:
|
||||
if "nsfw_likelihood" in result.keys():
|
||||
formatted_result.append(
|
||||
"nsfw_likelihood: " + str(result["nsfw_likelihood"])
|
||||
)
|
||||
|
||||
for label, likelihood in zip(result["label"], result["likelihood"]):
|
||||
formatted_result.append(f'"{label}": {str(likelihood)}')
|
||||
|
||||
return "\n".join(formatted_result)
|
||||
|
||||
def _run(
|
||||
self,
|
||||
query: str,
|
||||
run_manager: Optional[CallbackManagerForToolRun] = None,
|
||||
) -> str:
|
||||
"""Use the tool."""
|
||||
query_params = {"text": query, "language": self.language}
|
||||
return self._call_eden_ai(query_params)
|
@@ -0,0 +1,25 @@
|
||||
"""Test EdenAi's speech to text Tool .
|
||||
|
||||
In order to run this test, you need to have an EdenAI api key.
|
||||
You can get it by registering for free at https://app.edenai.run/user/register.
|
||||
A test key can be found at https://app.edenai.run/admin/account/settings by
|
||||
clicking on the 'sandbox' toggle.
|
||||
(calls will be free, and will return dummy results)
|
||||
|
||||
You'll then need to set EDENAI_API_KEY environment variable to your api key.
|
||||
"""
|
||||
from langchain.tools.edenai import EdenAiSpeechToTextTool
|
||||
|
||||
|
||||
def test_edenai_call() -> None:
|
||||
"""Test simple call to edenai's speech to text endpoint."""
|
||||
speech2text = EdenAiSpeechToTextTool(providers=["amazon"])
|
||||
|
||||
output = speech2text(
|
||||
"https://audio-samples.github.io/samples/mp3/blizzard_unconditional/sample-0.mp3"
|
||||
)
|
||||
|
||||
assert speech2text.name == "edenai_speech_to_text"
|
||||
assert speech2text.feature == "audio"
|
||||
assert speech2text.subfeature == "speech_to_text_async"
|
||||
assert isinstance(output, str)
|
@@ -0,0 +1,29 @@
|
||||
"""Test EdenAi's text to speech Tool .
|
||||
|
||||
In order to run this test, you need to have an EdenAI api key.
|
||||
You can get it by registering for free at https://app.edenai.run/user/register.
|
||||
A test key can be found at https://app.edenai.run/admin/account/settings by
|
||||
clicking on the 'sandbox' toggle.
|
||||
(calls will be free, and will return dummy results)
|
||||
|
||||
You'll then need to set EDENAI_API_KEY environment variable to your api key.
|
||||
"""
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from langchain.tools.edenai import EdenAiTextToSpeechTool
|
||||
|
||||
|
||||
def test_edenai_call() -> None:
|
||||
"""Test simple call to edenai's text to speech endpoint."""
|
||||
text2speech = EdenAiTextToSpeechTool(
|
||||
providers=["amazon"], language="en", voice="MALE"
|
||||
)
|
||||
|
||||
output = text2speech("hello")
|
||||
parsed_url = urlparse(output)
|
||||
|
||||
assert text2speech.name == "edenai_text_to_speech"
|
||||
assert text2speech.feature == "audio"
|
||||
assert text2speech.subfeature == "text_to_speech"
|
||||
assert isinstance(output, str)
|
||||
assert parsed_url.scheme in ["http", "https"]
|
@@ -0,0 +1,23 @@
|
||||
"""Test EdenAi's image moderation Tool .
|
||||
|
||||
In order to run this test, you need to have an EdenAI api key.
|
||||
You can get it by registering for free at https://app.edenai.run/user/register.
|
||||
A test key can be found at https://app.edenai.run/admin/account/settings by
|
||||
clicking on the 'sandbox' toggle.
|
||||
(calls will be free, and will return dummy results)
|
||||
|
||||
You'll then need to set EDENAI_API_KEY environment variable to your api key.
|
||||
"""
|
||||
from langchain.tools.edenai import EdenAiExplicitImageTool
|
||||
|
||||
|
||||
def test_edenai_call() -> None:
|
||||
"""Test simple call to edenai's image moderation endpoint."""
|
||||
image_moderation = EdenAiExplicitImageTool(providers=["amazon"])
|
||||
|
||||
output = image_moderation("https://static.javatpoint.com/images/objects.jpg")
|
||||
|
||||
assert image_moderation.name == "edenai_image_explicit_content_detection"
|
||||
assert image_moderation.feature == "image"
|
||||
assert image_moderation.subfeature == "explicit_content"
|
||||
assert isinstance(output, str)
|
@@ -0,0 +1,23 @@
|
||||
"""Test EdenAi's object detection Tool .
|
||||
|
||||
In order to run this test, you need to have an EdenAI api key.
|
||||
You can get it by registering for free at https://app.edenai.run/user/register.
|
||||
A test key can be found at https://app.edenai.run/admin/account/settings by
|
||||
clicking on the 'sandbox' toggle.
|
||||
(calls will be free, and will return dummy results)
|
||||
|
||||
You'll then need to set EDENAI_API_KEY environment variable to your api key.
|
||||
"""
|
||||
from langchain.tools.edenai import EdenAiObjectDetectionTool
|
||||
|
||||
|
||||
def test_edenai_call() -> None:
|
||||
"""Test simple call to edenai's object detection endpoint."""
|
||||
object_detection = EdenAiObjectDetectionTool(providers=["google"])
|
||||
|
||||
output = object_detection("https://static.javatpoint.com/images/objects.jpg")
|
||||
|
||||
assert object_detection.name == "edenai_object_detection"
|
||||
assert object_detection.feature == "image"
|
||||
assert object_detection.subfeature == "object_detection"
|
||||
assert isinstance(output, str)
|
@@ -0,0 +1,25 @@
|
||||
"""Test EdenAi's identity parser Tool .
|
||||
|
||||
In order to run this test, you need to have an EdenAI api key.
|
||||
You can get it by registering for free at https://app.edenai.run/user/register.
|
||||
A test key can be found at https://app.edenai.run/admin/account/settings by
|
||||
clicking on the 'sandbox' toggle.
|
||||
(calls will be free, and will return dummy results)
|
||||
|
||||
You'll then need to set EDENAI_API_KEY environment variable to your api key.
|
||||
"""
|
||||
from langchain.tools.edenai import EdenAiParsingIDTool
|
||||
|
||||
|
||||
def test_edenai_call() -> None:
|
||||
"""Test simple call to edenai's identity parser endpoint."""
|
||||
id_parser = EdenAiParsingIDTool(providers=["amazon"], language="en")
|
||||
|
||||
output = id_parser(
|
||||
"https://www.citizencard.com/images/citizencard-uk-id-card-2023.jpg"
|
||||
)
|
||||
|
||||
assert id_parser.name == "edenai_identity_parsing"
|
||||
assert id_parser.feature == "ocr"
|
||||
assert id_parser.subfeature == "identity_parser"
|
||||
assert isinstance(output, str)
|
@@ -0,0 +1,23 @@
|
||||
"""Test EdenAi's invoice parser Tool .
|
||||
|
||||
In order to run this test, you need to have an EdenAI api key.
|
||||
You can get it by registering for free at https://app.edenai.run/user/register.
|
||||
A test key can be found at https://app.edenai.run/admin/account/settings by
|
||||
clicking on the 'sandbox' toggle.
|
||||
(calls will be free, and will return dummy results)
|
||||
|
||||
You'll then need to set EDENAI_API_KEY environment variable to your api key.
|
||||
"""
|
||||
from langchain.tools.edenai import EdenAiParsingInvoiceTool
|
||||
|
||||
|
||||
def test_edenai_call() -> None:
|
||||
"""Test simple call to edenai's invoice parser endpoint."""
|
||||
invoice_parser = EdenAiParsingInvoiceTool(providers=["amazon"], language="en")
|
||||
|
||||
output = invoice_parser("https://app.edenai.run/assets/img/data_1.72e3bdcc.png")
|
||||
|
||||
assert invoice_parser.name == "edenai_invoice_parsing"
|
||||
assert invoice_parser.feature == "ocr"
|
||||
assert invoice_parser.subfeature == "invoice_parser"
|
||||
assert isinstance(output, str)
|
@@ -0,0 +1,24 @@
|
||||
"""Test EdenAi's text moderation Tool .
|
||||
|
||||
In order to run this test, you need to have an EdenAI api key.
|
||||
You can get it by registering for free at https://app.edenai.run/user/register.
|
||||
A test key can be found at https://app.edenai.run/admin/account/settings by
|
||||
clicking on the 'sandbox' toggle.
|
||||
(calls will be free, and will return dummy results)
|
||||
|
||||
You'll then need to set EDENAI_API_KEY environment variable to your api key.
|
||||
"""
|
||||
from langchain.tools.edenai.text_moderation import EdenAiTextModerationTool
|
||||
|
||||
|
||||
def test_edenai_call() -> None:
|
||||
"""Test simple call to edenai's text moderation endpoint."""
|
||||
|
||||
text_moderation = EdenAiTextModerationTool(providers=["openai"], language="en")
|
||||
|
||||
output = text_moderation("i hate you")
|
||||
|
||||
assert text_moderation.name == "edenai_explicit_content_detection_text"
|
||||
assert text_moderation.feature == "text"
|
||||
assert text_moderation.subfeature == "moderation"
|
||||
assert isinstance(output, str)
|
103
libs/langchain/tests/unit_tests/tools/eden_ai/test_tools.py
Normal file
103
libs/langchain/tests/unit_tests/tools/eden_ai/test_tools.py
Normal file
@@ -0,0 +1,103 @@
|
||||
from collections.abc import Generator
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from langchain.tools.edenai import EdenAiTextModerationTool
|
||||
|
||||
tool = EdenAiTextModerationTool(
|
||||
providers=["openai"], language="en", edenai_api_key="fake_key"
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_post() -> Generator:
|
||||
with patch("langchain.tools.edenai.edenai_base_tool.requests.post") as mock:
|
||||
yield mock
|
||||
|
||||
|
||||
def test_provider_not_available(mock_post: MagicMock) -> None:
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.json.return_value = [
|
||||
{
|
||||
"error": {
|
||||
"message": """Amazon has returned an error:
|
||||
An error occurred (TextSizeLimitExceededException)
|
||||
when calling the DetectTargetedSentiment
|
||||
operation: Input text size exceeds limit.
|
||||
Max length of request text allowed is 5000 bytes
|
||||
while in this request the text size is 47380 bytes""",
|
||||
"type": "ProviderInvalidInputTextLengthError",
|
||||
},
|
||||
"status": "fail",
|
||||
"provider": "amazon",
|
||||
"provider_status_code": 400,
|
||||
"cost": 0.0,
|
||||
}
|
||||
]
|
||||
mock_post.return_value = mock_response
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
tool._run("some query")
|
||||
|
||||
|
||||
def test_unexpected_response(mock_post: MagicMock) -> None:
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.json.return_value = [
|
||||
{
|
||||
"status": "success",
|
||||
}
|
||||
]
|
||||
mock_post.return_value = mock_response
|
||||
with pytest.raises(RuntimeError):
|
||||
tool._run("some query")
|
||||
|
||||
|
||||
def test_incomplete_response(mock_post: MagicMock) -> None:
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.json.return_value = [
|
||||
{
|
||||
"status": "success",
|
||||
"provider": "microsoft",
|
||||
"nsfw_likelihood": 5,
|
||||
"cost": 0.001,
|
||||
"label": ["sexually explicit", "sexually suggestive", "offensive"],
|
||||
}
|
||||
]
|
||||
|
||||
mock_post.return_value = mock_response
|
||||
with pytest.raises(RuntimeError):
|
||||
tool._run("some query")
|
||||
|
||||
|
||||
def test_invalid_payload(mock_post: MagicMock) -> None:
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 400
|
||||
mock_response.json.return_value = {}
|
||||
mock_post.return_value = mock_response
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
tool._run("some query")
|
||||
|
||||
|
||||
def test_parse_response_format(mock_post: MagicMock) -> None:
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.json.return_value = [
|
||||
{
|
||||
"status": "success",
|
||||
"provider": "microsoft",
|
||||
"nsfw_likelihood": 5,
|
||||
"cost": 0.001,
|
||||
"label": ["offensive", "hate_speech"],
|
||||
"likelihood": [4, 5],
|
||||
}
|
||||
]
|
||||
mock_post.return_value = mock_response
|
||||
|
||||
result = tool("some query")
|
||||
|
||||
assert result == 'nsfw_likelihood: 5\n"offensive": 4\n"hate_speech": 5'
|
@@ -28,6 +28,14 @@ _EXPECTED = [
|
||||
"DeleteFileTool",
|
||||
"DuckDuckGoSearchResults",
|
||||
"DuckDuckGoSearchRun",
|
||||
"EdenAiExplicitImageTool",
|
||||
"EdenAiObjectDetectionTool",
|
||||
"EdenAiParsingIDTool",
|
||||
"EdenAiParsingInvoiceTool",
|
||||
"EdenAiSpeechToTextTool",
|
||||
"EdenAiTextModerationTool",
|
||||
"EdenAiTextToSpeechTool",
|
||||
"EdenaiTool",
|
||||
"ExtractHyperlinksTool",
|
||||
"ExtractTextTool",
|
||||
"FileSearchTool",
|
||||
|
Reference in New Issue
Block a user