Skip to content
This repository has been archived by the owner on Mar 16, 2024. It is now read-only.

Commit

Permalink
Wolfram Alpha Tool (#500)
Browse files Browse the repository at this point in the history
* Wolfram Alhpa tooling

* Add basic implementation, hold optional parameters

* Add enums and error handling

* Improvements

* Type checking fixes

* Add types-requests

* Add catchall return

* Update type error in run_update_tool_eval --> This was on main

* Add env var to .env.example
  • Loading branch information
NolanTrem authored Aug 23, 2023
1 parent b9ca787 commit e16023a
Show file tree
Hide file tree
Showing 11 changed files with 595 additions and 76 deletions.
4 changes: 3 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,6 @@ MAX_WORKERS=8
# Type of symbol graph
GRAPH_TYPE=dynamic
# The root path of the data
DATA_ROOT_PATH=automata-embedding-data
DATA_ROOT_PATH=automata-embedding-data
# Wolfram API App ID
WOLFRAM_APP_ID=your_wolfram_app_id
1 change: 1 addition & 0 deletions automata/agent/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ class AgentToolkitNames(Enum):
ADVANCED_CONTEXT_ORACLE = "advanced-context-oracle"
DOCUMENT_ORACLE = "document-oracle"
AGENTIFIED_SOLUTION_ORACLE = "agentified-solution-oracle"
WOLFRAM_ALPHA_ORACLE = "wolfram-alpha-oracle"

# Core tools
PY_READER = "py-reader"
Expand Down
7 changes: 6 additions & 1 deletion automata/config/prompt/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# sourcery skip: docstrings-for-packages
from automata.config.prompt.agentified_search import AGENTIFIED_SEARCH_TEMPLATE
from automata.config.prompt.doc_generation import DOC_GENERATION_TEMPLATE
from automata.config.prompt.wolfram_alpha import WOLFRAM_ALPHA_TEMPLATE

__all__ = ["AGENTIFIED_SEARCH_TEMPLATE", "DOC_GENERATION_TEMPLATE"]
__all__ = [
"AGENTIFIED_SEARCH_TEMPLATE",
"DOC_GENERATION_TEMPLATE",
"WOLFRAM_ALPHA_TEMPLATE",
]
96 changes: 96 additions & 0 deletions automata/config/prompt/wolfram_alpha.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
"""Defines the Wolfram Alpha prompt template."""
import textwrap

WOLFRAM_ALPHA_TEMPLATE = textwrap.dedent(
"""
When using the Wolfram Alpha Oracle, you must call the query method with an `input_str` argument. Optional arguments, such as maxchars can also be passed in.
Here are a few examples of how to use the Wolfram Alpha API through the Wolfram Alpha Oracle tool:
Example 1
----------
input_str: 10 densest metals
- Observed Results -
Input interpretation:
10 densest metallic elements | by mass density
Result:
1 | hassium | 41 g/cm^3 |
2 | meitnerium | 37.4 g/cm^3 |
3 | bohrium | 37.1 g/cm^3 |
4 | seaborgium | 35.3 g/cm^3 |
5 | darmstadtium | 34.8 g/cm^3 |
6 | dubnium | 29.3 g/cm^3 |
7 | roentgenium | 28.7 g/cm^3 |
8 | rutherfordium | 23.2 g/cm^3 |
9 | osmium | 22.59 g/cm^3 |
10 | iridium | 22.56 g/cm^3 |
Periodic table location:
image: https://www6b3.wolframalpha.com/Calculate/MSP/MSP231019ge21d317638ic9000064icf9g3h4g5idge?MSPStoreType=image/png&s=18
Images:
image: https://www6b3.wolframalpha.com/Calculate/MSP/MSP231119ge21d317638ic900005b65dgd6f55c7h34?MSPStoreType=image/png&s=18
Wolfram Language code: Dataset[EntityValue[{Entity["Element", "Hassium"], Entity["Element", "Meitnerium"], Entity["Element", "Bohrium"], Entity["Element", "Seaborgium"], Entity["Element", "Darmstadtium"], Entity["Element", "Dubnium"], Entity["Element", "Roentgenium"], Entity["Element", "Rutherfordium"], Entity["Element", "Osmium"], Entity["Element", "Iridium"]}, EntityProperty["Element", "Image"], "EntityAssociation"]]
Basic elemental properties:
atomic symbol | all | Bh | Db | Ds | Hs | Ir | Mt | Os | Rf | Rg | Sg
atomic number | median | 106.5
| highest | 111 (roentgenium)
| lowest | 76 (osmium)
| distribution |
atomic mass | median | 269 u
| highest | 282 u (roentgenium)
| lowest | 190.23 u (osmium)
| distribution |
half-life | median | 78 min
| highest | 13 h (rutherfordium)
| lowest | 4 min (darmstadtium)
| distribution |
Thermodynamic properties:
phase at STP | all | solid
(properties at standard conditions)
Material properties:
mass density | median | 32.1 g/cm^3
| highest | 41 g/cm^3 (hassium)
| lowest | 22.56 g/cm^3 (iridium)
| distribution |
(properties at standard conditions)
Reactivity:
valence | median | 6
| highest | 7 (bohrium)
| lowest | 4 (rutherfordium)
| distribution |
Atomic properties:
term symbol | all | ^2S_(1/2) | ^3D_3 | ^3F_2 | ^4F_(3/2) | ^4F_(9/2) | ^5D_0 | ^5D_4 | ^6S_(5/2)
(electronic ground state properties)
Abundances:
crust abundance | median | 0 mass%
| highest | 1.8×10^-7 mass% (osmium)
| lowest | 0 mass% (8 elements)
human abundance | median | 0 mass%
| highest | 0 mass% (8 elements)
| lowest | 0 mass% (8 elements)
Nuclear properties:
half-life | median | 78 min
| highest | 13 h (rutherfordium)
| lowest | 4 min (darmstadtium)
| distribution |
specific radioactivity | highest | 6.123×10^6 TBq/g (darmstadtium)
| lowest | 33169 TBq/g (rutherfordium)
| median | 366018 TBq/g
| distribution |
Wolfram|Alpha website result for "10 densest metals":
https://www6b3.wolframalpha.com/input?i=10+densest+metals
"""
)
4 changes: 2 additions & 2 deletions automata/experimental/scripts/run_update_tool_eval.py
Original file line number Diff line number Diff line change
Expand Up @@ -413,9 +413,9 @@ def filter_and_log_symbols(
processed_paths, expected_symbol_dotpaths
)

missing_symbols_dotpaths = [
missing_symbols_dotpaths = {
ele for ele in missing_symbols_dotpaths if "test" not in ele
]
}

logger.warning(
f"We found {len(extra_symbol_dotpaths)} extra symbols = {extra_symbol_dotpaths}"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
"""Implementation of a toolkit builder for the Wolfram Alpha API."""

import logging
import logging.config
from typing import List

from automata.agent import AgentToolkitNames, OpenAIAgentToolkitBuilder
from automata.config.config_base import LLMProvider
from automata.core.utils import get_logging_config
from automata.experimental.tools.wolfram_alpha_oracle import WolframAlphaOracle
from automata.llm import OpenAITool
from automata.singletons.toolkit_registry import (
OpenAIAutomataAgentToolkitRegistry,
)
from automata.tools.tool_base import Tool

logger = logging.getLogger(__name__)
logging.config.dictConfig(get_logging_config())


class WolframAlphaToolkitBuilder:
"""Builder for setting up the Wolfram Alpha Tool."""

def build(self) -> List[Tool]:
"""Build and return a list containing an instance of the Wolfram Alpha Tool wrapped in a Tool object."""
return [
Tool(
name="wolfram-alpha-oracle",
function=self.query_wolfram_alpha,
description="A tool to query the Wolfram Alpha API and retrieve results.",
)
]

def query_wolfram_alpha(self, input_str: str, **kwargs) -> str:
"""A wrapper function to query the Wolfram Alpha API."""
oracle = WolframAlphaOracle()
if result := oracle.query(input_str, **kwargs):
return result
return "Failed to get data from Wolfram Alpha."


@OpenAIAutomataAgentToolkitRegistry.register_tool_manager
class WolframAlphaOpenAIToolkitBuilder(
WolframAlphaToolkitBuilder, OpenAIAgentToolkitBuilder
):
TOOL_NAME = AgentToolkitNames.WOLFRAM_ALPHA_ORACLE
LLM_PROVIDER = LLMProvider.OPENAI

def __init__(self, **kwargs):
super().__init__()

def build_for_open_ai(self) -> List[OpenAITool]:
"""Builds the tools associated with the Wolfram Alpha oracle for the OpenAI API."""
tools = super().build()

properties = {
"query": {
"type": "string",
"description": "The query string to send to Wolfram Alpha.",
},
}
required = ["query"]

openai_tools = []
for tool in tools:
openai_tool = OpenAITool(
function=tool.function,
name=tool.name,
description=tool.description,
properties=properties,
required=required,
)
openai_tools.append(openai_tool)

return openai_tools
145 changes: 145 additions & 0 deletions automata/experimental/tools/wolfram_alpha_oracle.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
"""
Module to interface with the Wolfram Alpha API.
Provides a set of enumerations for various query parameters and a class to
handle querying the API and processing responses.
"""

import logging
import logging.config
import os
import random
import time
from enum import Enum
from typing import Optional

import dotenv
import requests

from automata.core.utils import get_logging_config

logger = logging.getLogger(__name__)
logging.config.dictConfig(get_logging_config())
dotenv.load_dotenv()


class BasicParameters(Enum):
"""An enum for the basic parameters of the Wolfram Alpha API."""

INPUT = "input"
APPID = "appid"
FORMAT = "format"
OUTPUT = "output"


class PodSelection(Enum):
"""An enum for the pod selection parameters of the Wolfram Alpha API."""

INCLUDEPODID = "includepodid"
EXCLUDEPODID = "excludepodid"
PODTITLE = "podtitle"
PODINDEX = "podindex"
SCANNER = "scanner"


class Location(Enum):
"""An enum for the location parameters of the Wolfram Alpha API."""

IP = "ip"
LATLONG = "latlong"
LOCATION = "location"


class Size(Enum):
"""An enum for the size parameters of the Wolfram Alpha API."""

WIDTH = "width"
MAXWIDTH = "maxwidth"
PLOTWIDTH = "plotwidth"
MAG = "mag"


class TimeoutsAsync(Enum):
"""An enum for the timeouts and async parameters of the Wolfram Alpha API."""

SCANTIMEOUT = "scantimeout"
PODTIMEOUT = "podtimeout"
FORMATTIMEOUT = "formattimeout"
PARSETIMEOUT = "parsetimeout"
TOTALTIMEOUT = "totaltimeout"
ASYNC = "async"


class Misc(Enum):
"""An enum for the miscellaneous parameters of the Wolfram Alpha API."""

REINTERPRET = "reinterpret"
TRANSLATION = "translation"
IGNORECASE = "ignorecase"
SIG = "sig"
ASSUMPTION = "assumption"
PODSTATE = "podstate"
UNITS = "units"


class WolframAlphaOracle:
"""Interface for querying the Wolfram Alpha API."""

BASE_URL = "https://www.wolframalpha.com/api/v1/llm-api"
MAX_RETRIES = 3
BASE_DELAY = 1
MAX_DELAY = 10

@classmethod
def query(cls, input_str: str, **kwargs) -> Optional[str]:
"""Sends a query to the Wolfram Alpha API."""

app_id = os.environ.get("WOLFRAM_APP_ID")
if not app_id:
raise ValueError("WOLFRAM_APP_ID environment variable is not set.")

params = {
BasicParameters.INPUT.value: input_str,
BasicParameters.APPID.value: app_id,
}

for key, value in kwargs.items():
params[key] = value.value if isinstance(value, Enum) else value

retries = 0
delay = cls.BASE_DELAY

# Uses exponential backoff with jitter to retry requests as the Wolfram Alpha API does not support retries.
while retries < cls.MAX_RETRIES:
try:
response = requests.get(cls.BASE_URL, params=params)
response.raise_for_status()
return response.text
except (
requests.ConnectionError,
requests.Timeout,
requests.RequestException,
) as e:
if retries < cls.MAX_RETRIES - 1:
jitter = random.uniform(0, 0.1 * delay)
time_to_wait = delay + jitter
logger.warning(
f"Error occurred: {e}. Retrying in {time_to_wait:.2f} seconds..."
)
time.sleep(time_to_wait)
delay = min(2 * delay, cls.MAX_DELAY)
retries += 1
elif isinstance(e, requests.ConnectionError):
raise ConnectionError(
f"Failed to connect to Wolfram Alpha API: {e}"
) from e
elif isinstance(e, requests.Timeout):
raise TimeoutError(
f"Request to Wolfram Alpha API timed out: {e}"
) from e
else:
raise RuntimeError(
f"An error occurred while querying the Wolfram Alpha API: {e}"
) from e
return None
return None
6 changes: 6 additions & 0 deletions automata/singletons/dependency_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
from automata.experimental.symbol_embedding.symbol_doc_embedding_builder import (
SymbolDocEmbeddingBuilder,
)
from automata.experimental.tools.wolfram_alpha_oracle import WolframAlphaOracle
from automata.llm import OpenAIChatCompletionProvider, OpenAIEmbeddingProvider
from automata.memory_store import SymbolCodeEmbeddingHandler
from automata.symbol import ISymbolProvider, SymbolGraph
Expand Down Expand Up @@ -343,6 +344,11 @@ def create_py_writer(self) -> PyCodeWriter:
"""Creates `PyCodeWriter` for use in all dependencies."""
return PyCodeWriter(self.get("py_reader"))

@lru_cache()
def create_wolfram_alpha_oracle(self) -> WolframAlphaOracle:
"""Creates and returns an instance of WolframAlphaOracle."""
return WolframAlphaOracle()

def reset(self) -> None:
"""Resets the entire dependency cache."""

Expand Down
4 changes: 4 additions & 0 deletions automata/tools/agent_tool_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from automata.embedding import EmbeddingSimilarityCalculator
from automata.experimental.memory_store import SymbolDocEmbeddingHandler
from automata.experimental.search import SymbolSearch
from automata.experimental.tools.wolfram_alpha_oracle import WolframAlphaOracle
from automata.memory_store import SymbolCodeEmbeddingHandler
from automata.tools import Tool, UnknownToolError

Expand All @@ -34,6 +35,9 @@ class AgentToolFactory:
("symbol_search", SymbolSearch),
("symbol_doc_embedding_handler", SymbolDocEmbeddingHandler),
],
AgentToolkitNames.WOLFRAM_ALPHA_ORACLE: [
("wolfram_alpha_oracle", WolframAlphaOracle)
],
AgentToolkitNames.PY_READER: [("py_reader", PyReader)],
AgentToolkitNames.PY_WRITER: [("py_writer", PyCodeWriter)],
AgentToolkitNames.AGENTIFIED_SEARCH: [
Expand Down
Loading

0 comments on commit e16023a

Please sign in to comment.