from abc import ABC from anthropic import Anthropic from openai import OpenAI from groq import Groq import logging from typing import Dict, Type, Self, List import os import time logger = logging.getLogger(__name__) class LLMException(Exception): pass class LLM(ABC): """ An abstract superclass for interacting with LLMs - subclass for Claude and GPT """ model_names = [] def __init__(self, model_name: str, temperature: float): self.model_name = model_name self.client = None self.temperature = temperature def send(self, system: str, user: str, max_tokens: int = 3000) -> str: """ Send a message :param system: the context in which this message is to be taken :param user: the prompt :param max_tokens: max number of tokens to generate :return: the response from the AI """ result = self.protected_send(system, user, max_tokens) left = result.find("{") right = result.rfind("}") if left > -1 and right > -1: result = result[left : right + 1] return result def protected_send(self, system: str, user: str, max_tokens: int = 3000) -> str: """ Wrap the send call in an exception handler, giving the LLM 3 chances in total, in case of overload errors. If it fails 3 times, then it forfeits! """ retries = 3 while retries: retries -= 1 try: return self._send(system, user, max_tokens) except Exception as e: logging.error(f"Exception on calling LLM of {e}") if retries: logging.warning("Waiting 2s and retrying") time.sleep(2) return "{}" def _send(self, system: str, user: str, max_tokens: int = 3000) -> str: """ Send a message to the model - this default implementation follows the OpenAI API structure :param system: the context in which this message is to be taken :param user: the prompt :param max_tokens: max number of tokens to generate :return: the response from the AI """ response = self.client.chat.completions.create( model=self.api_model_name(), messages=[ {"role": "system", "content": system}, {"role": "user", "content": user}, ], response_format={"type": "json_object"}, ) return response.choices[0].message.content def api_model_name(self) -> str: """ Return the actual model_name to be used in the call to the API; strip out anything after a space """ if " " in self.model_name: return self.model_name.split(" ")[0] else: return self.model_name @classmethod def model_map(cls) -> Dict[str, Type[Self]]: """ Generate a mapping of Model Names to LLM classes, by looking at all subclasses of this one :return: a mapping dictionary from model name to LLM subclass """ mapping = {} for llm in cls.__subclasses__(): for model_name in llm.model_names: mapping[model_name] = llm return mapping @classmethod def all_model_names(cls) -> List[str]: """ Return a list of all the model names supported. Use the ones specified in the model_map, but also check if there's an env variable set that restricts the models """ models = list(cls.model_map().keys()) allowed = os.getenv("MODELS") if allowed: allowed_models = allowed.split(",") return [model for model in models if model in allowed_models] else: return models @classmethod def create(cls, model_name: str, temperature: float = 0.5) -> Self: """ Return an instance of a subclass that corresponds to this model_name :param model_name: a string to describe this model :param temperature: the creativity setting :return: a new instance of a subclass of LLM """ subclass = cls.model_map().get(model_name) if not subclass: raise LLMException(f"Unrecognized LLM model name specified: {model_name}") return subclass(model_name, temperature) class Claude(LLM): """ A class to act as an interface to the remote AI, in this case Claude """ model_names = ["claude-3-5-sonnet-latest", "claude-3-7-sonnet-latest"] def __init__(self, model_name: str, temperature: float): """ Create a new instance of the Anthropic client """ super().__init__(model_name, temperature) self.client = Anthropic() def _send(self, system: str, user: str, max_tokens: int = 3000) -> str: """ Send a message to Claude :param system: the context in which this message is to be taken :param user: the prompt :param max_tokens: max number of tokens to generate :return: the response from the AI """ response = self.client.messages.create( model=self.api_model_name(), max_tokens=max_tokens, temperature=self.temperature, system=system, messages=[ {"role": "user", "content": user}, ], ) return response.content[0].text class GPT(LLM): """ A class to act as an interface to the remote AI, in this case GPT """ model_names = ["gpt-4o-mini", "gpt-4o"] def __init__(self, model_name: str, temperature: float): """ Create a new instance of the OpenAI client """ super().__init__(model_name, temperature) self.client = OpenAI() class O1(LLM): """ A class to act as an interface to the remote AI, in this case O1 """ model_names = ["o1-mini"] def __init__(self, model_name: str, temperature: float): """ Create a new instance of the OpenAI client """ super().__init__(model_name, temperature) self.client = OpenAI() def _send(self, system: str, user: str, max_tokens: int = 3000) -> str: """ Send a message to O1 :param system: the context in which this message is to be taken :param user: the prompt :param max_tokens: max number of tokens to generate :return: the response from the AI """ message = system + "\n\n" + user response = self.client.chat.completions.create( model=self.api_model_name(), messages=[ {"role": "user", "content": message}, ], ) return response.choices[0].message.content class O3(LLM): """ A class to act as an interface to the remote AI, in this case O3 """ model_names = ["o3-mini"] def __init__(self, model_name: str, temperature: float): """ Create a new instance of the OpenAI client """ super().__init__(model_name, temperature) override = os.getenv("OPENAI_API_KEY_O3") if override: print("Using special key with o3 access") self.client = OpenAI(api_key=override) else: self.client = OpenAI() def _send(self, system: str, user: str, max_tokens: int = 3000) -> str: """ Send a message to O3 :param system: the context in which this message is to be taken :param user: the prompt :param max_tokens: max number of tokens to generate :return: the response from the AI """ message = system + "\n\n" + user response = self.client.chat.completions.create( model=self.api_model_name(), messages=[ {"role": "user", "content": message}, ], ) return response.choices[0].message.content class Gemini(LLM): """ A class to act as an interface to the remote AI, in this case Gemini """ model_names = ["gemini-2.0-flash", "gemini-1.5-flash"] def __init__(self, model_name: str, temperature: float): """ Create a new instance of the OpenAI client """ super().__init__(model_name, temperature) google_api_key = os.getenv("GOOGLE_API_KEY") self.client = OpenAI( api_key=google_api_key, base_url="https://generativelanguage.googleapis.com/v1beta/openai/", ) class Ollama(LLM): """ A class to act as an interface to the remote AI, in this case Ollama via the OpenAI client """ model_names = ["llama3.2 local", "gemma2 local", "qwen2.5 local", "phi4 local"] def __init__(self, model_name: str, temperature: float): """ Create a new instance of the OpenAI client for Ollama """ super().__init__(model_name, temperature) self.client = OpenAI(base_url="http://localhost:11434/v1", api_key="ollama") def _send(self, system: str, user: str, max_tokens: int = 3000) -> str: """ Send a message to Ollama :param system: the context in which this message is to be taken :param user: the prompt :param max_tokens: max number of tokens to generate :return: the response from the AI """ response = self.client.chat.completions.create( model=self.api_model_name(), messages=[ {"role": "system", "content": system}, {"role": "user", "content": user}, ], response_format={"type": "json_object"}, ) reply = response.choices[0].message.content if "" in reply: logging.info( "Thoughts:\n" + reply.split("")[0].replace("", "") ) reply = reply.split("")[1] return reply class DeepSeekAPI(LLM): """ A class to act as an interface to the remote AI, in this case DeepSeek via the OpenAI client """ model_names = ["deepseek-chat V3", "deepseek-reasoner R1"] def __init__(self, model_name: str, temperature: float): """ Create a new instance of the OpenAI client """ super().__init__(model_name, temperature) deepseek_api_key = os.getenv("DEEPSEEK_API_KEY") self.client = OpenAI( api_key=deepseek_api_key, base_url="https://api.deepseek.com" ) class DeepSeekLocal(LLM): """ A class to act as an interface to the remote AI, in this case Ollama via the OpenAI client """ model_names = ["deepseek-r1:14b local"] def __init__(self, model_name: str, temperature: float): """ Create a new instance of the OpenAI client """ super().__init__(model_name, temperature) self.client = OpenAI(base_url="http://localhost:11434/v1", api_key="ollama") def _send(self, system: str, user: str, max_tokens: int = 3000) -> str: """ Send a message to Ollama :param system: the context in which this message is to be taken :param user: the prompt :param max_tokens: max number of tokens to generate :return: the response from the AI """ system += "\nImportant: avoid overthinking. Think briefly and decisively. The final response must follow the given json format or you forfeit the game. Do not overthink. Respond with json." user += "\nImportant: avoid overthinking. Think briefly and decisively. The final response must follow the given json format or you forfeit the game. Do not overthink. Respond with json." response = self.client.chat.completions.create( model=self.api_model_name(), messages=[ {"role": "system", "content": system}, {"role": "user", "content": user}, ], ) reply = response.choices[0].message.content if "" in reply: logging.info( "Thoughts:\n" + reply.split("")[0].replace("", "") ) reply = reply.split("")[1] return reply class GroqAPI(LLM): """ A class to act as an interface to the remote AI, in this case Groq """ model_names = [ "deepseek-r1-distill-llama-70b via Groq", "llama-3.3-70b-versatile via Groq", "mixtral-8x7b-32768 via Groq", ] def __init__(self, model_name: str, temperature: float): """ Create a new instance of the Groq client """ super().__init__(model_name, temperature) self.client = Groq()