from abc import ABC, abstractmethod import json from google.generativeai.types import CallableFunctionDeclaration import google.generativeai.types.content_types as content_types from utils import add_params_and_annotations class Parameter: def __init__(self, name, description, required): self.name = name self.description = description self.required = required def as_openai_info(self): pass def as_standard_api(self): pass class StringParameter(Parameter): def __init__(self, name, description, required): super().__init__(name, description, required) def as_openai_info(self): return { "type": "string", "name": self.name, "description": self.description } def as_standard_api(self): return { "type": "string", "name": self.name, "description": self.description, "required": self.required } def as_natural_language(self): return f'{self.name} (string{", required" if self.required else ""}): {self.description}.' def as_documented_python(self): return f'{self.name} (str{", required" if self.required else ""}): {self.description}.' def as_gemini_tool(self): return { 'type': 'string', 'description': self.description } @staticmethod def from_standard_api(api_info): return StringParameter(api_info["name"], api_info["description"], api_info["required"]) class EnumParameter(Parameter): def __init__(self, name, description, values, required): super().__init__(name, description, required) self.values = values def as_openai_info(self): return { "type": "string", "description": self.description, "values": self.values } def as_standard_api(self): return { "type": "enum", "name": self.name, "description": self.description, "values": self.values, "required": self.required } def as_natural_language(self): return f'{self.name} (enum{", required" if self.required else ""}): {self.description}. Possible values: {", ".join(self.values)}' def as_documented_python(self): return f'{self.name} (str{", required" if self.required else ""}): {self.description}. Possible values: {", ".join(self.values)}' def as_gemini_tool(self): return { 'description': self.description, 'type': 'string', 'enum': self.values } @staticmethod def from_standard_api(api_info): return EnumParameter(api_info["name"], api_info["description"], api_info["values"], api_info["required"]) class NumberParameter(Parameter): def __init__(self, name, description, required): super().__init__(name, description, required) def as_openai_info(self): return { "type": "number", "description": self.description } def as_standard_api(self): return { "type": "number", "name": self.name, "description": self.description, "required": self.required } def as_natural_language(self): return f'{self.name} (number): {self.description}' def as_documented_python(self): return f'{self.name} (number): {self.description}' def as_gemini_tool(self): return { 'description': self.description, 'type': 'number' } class ArrayParameter(Parameter): def __init__(self, name, description, required, item_schema): super().__init__(name, description, required) self.item_schema = item_schema def as_openai_info(self): return { "type": "array", "description": self.description, "items": self.item_schema } def as_standard_api(self): return { "type": "array", "name": self.name, "description": self.description, "required": self.required, "item_schema": self.item_schema } def as_natural_language(self): return f'{self.name} (array): {self.description}. Each item should follow the JSON schema: {json.dumps(self.item_schema)}' def as_documented_python(self): return f'{self.name} (list): {self.description}. Each item should follow the JSON schema: {json.dumps(self.item_schema)}' def as_gemini_tool(self): return { 'description': self.description, 'type': 'array', 'items': self.item_schema } def parameter_from_openai_api(parameter_name, schema, required): if 'enum' in schema: return EnumParameter(parameter_name, schema['description'], schema['enum'], required) elif schema['type'] == 'string': return StringParameter(parameter_name, schema['description'], required) elif schema['type'] == 'number': return NumberParameter(parameter_name, schema['description'], required) elif schema['type'] == 'array': return ArrayParameter(parameter_name, schema['description'], required, schema['items']) else: raise ValueError(f'Unknown parameter type: {schema["type"]}') class Tool: def __init__(self, name, description, parameters, function, output_schema=None): self.name = name self.description = description self.parameters = parameters self.function = function self.output_schema = output_schema def call_tool_for_toolformer(self, *args, **kwargs): print(f'Toolformer called tool {self.name} with args {args} and kwargs {kwargs}') # Unlike a call from a routine, this call catches exceptions and returns them as strings try: tool_reply = self.function(*args, **kwargs) print(f'Tool {self.name} returned: {tool_reply}') return tool_reply except Exception as e: print(f'Tool {self.name} failed with exception: {e}') return 'Tool call failed: ' + str(e) def as_openai_info(self): return { "type": "function", "function": { "name": self.name, "description": self.description, "parameters": { "type" : "object", "properties": {parameter.name : parameter.as_openai_info() for parameter in self.parameters}, "required": [parameter.name for parameter in self.parameters if parameter.required] } } } def as_gemini_tool(self) -> CallableFunctionDeclaration: if len(self.parameters) == 0: parameters = None else: parameters = { 'type': 'object', 'properties': {parameter.name: parameter.as_gemini_tool() for parameter in self.parameters}, 'required': [parameter.name for parameter in self.parameters if parameter.required] } return content_types.Tool([CallableFunctionDeclaration( name=self.name, description=self.description, parameters=parameters, function=self.call_tool_for_toolformer )]) def as_llama_schema(self): schema = { 'name': self.name, 'description': self.description, 'parameters': {parameter.name : parameter.as_openai_info() for parameter in self.parameters}, 'required': [parameter.name for parameter in self.parameters if parameter.required] } if self.output_schema is not None: schema['output_schema'] = self.output_schema return schema def as_natural_language(self): print('Converting to natural language') print('Number of parameters:', len(self.parameters)) nl = f'Function {self.name}: {self.description}. Parameters:\n' if len(self.parameters) == 0: nl += 'No parameters.' else: for parameter in self.parameters: nl += '\t' + parameter.as_natural_language() + '\n' if self.output_schema is not None: nl += f'\Returns a dictionary with schema: {json.dumps(self.output_schema, indent=2)}' return nl def as_standard_api(self): return { "name": self.name, "description": self.description, "parameters": [parameter.as_standard_api() for parameter in self.parameters] } def as_documented_python(self): documented_python = f'Tool {self.name}:\n\n{self.description}\nParameters:\n' if len(self.parameters) == 0: documented_python += 'No parameters.' else: for parameter in self.parameters: documented_python += '\t' + parameter.as_documented_python() + '\n' if self.output_schema is not None: documented_python += f'\Returns a dictionary with schema: {json.dumps(self.output_schema, indent=2)}' return documented_python def as_executable_function(self): # Create an actual function that can be called def f(*args, **kwargs): print('Routine called tool', self.name, 'with args', args, 'and kwargs', kwargs) response = self.function(*args, **kwargs) print('Tool', self.name, 'returned:', response) return response return f def as_annotated_function(self): def wrapped_fn(*args, **kwargs): return self.call_tool_for_toolformer(*args, **kwargs) parsed_parameters = {} description = self.description for parameter_name, parameter_schema in self.as_openai_info()['function']['parameters']['properties'].items(): if parameter_schema['type'] == 'string': parsed_parameters[parameter_name] = (str, parameter_schema['description']) elif parameter_schema['type'] == 'number': parsed_parameters[parameter_name] = (float, parameter_schema['description']) elif parameter_schema['type'] == 'object': parsed_parameters[parameter_name] = (dict, parameter_schema['description']) description += f'\n{parameter_name} has the schema:\n' + json.dumps(parameter_schema) + '\n' else: raise ValueError(f'Unknown parameter type: {parameter_schema["type"]}') return_type = type(None) if self.output_schema is not None: #description += '\nOutput schema:\n' + json.dumps(self.output_schema) if self.output_schema['type'] == 'string': return_type = str elif self.output_schema['type'] == 'number': return_type = float elif self.output_schema['type'] == 'object': return_type = dict else: raise ValueError(f'Unknown output type: {self.output_schema["type"]}') return add_params_and_annotations( self.name, description, parsed_parameters, return_type)(wrapped_fn) @staticmethod def from_openai_info(info, func): parameters = [parameter_from_openai_api(name, schema, name in info['function']['parameters']['required']) for name, schema in info['function']['parameters']['properties'].items()] return Tool(info['function']['name'], info['function']['description'], parameters, func) class Conversation(ABC): @abstractmethod def chat(self, message, role='user', print_output=True): pass class Toolformer(ABC): @abstractmethod def new_conversation(self, prompt, tools, category=None) -> Conversation: pass