From 62422435e0367b15fad8356fd674d686da2096dc Mon Sep 17 00:00:00 2001 From: "agentfarmx[bot]" <198411105+agentfarmx[bot]@users.noreply.github.com> Date: Thu, 27 Feb 2025 19:07:52 +0000 Subject: [PATCH] feat: add multi-provider support for AI models (Anthropic, Google) --- anthropic_api.py | 93 ++++++++++++++++++++++++++++ config.ini | 1 + copycat.py | 6 +- extract.py | 154 ++++++++++++++++++++++++++++++++++++++--------- google_api.py | 108 +++++++++++++++++++++++++++++++++ gptplus.py | 6 +- models.json | 52 ++++++++++++++-- requirements.txt | 4 +- 8 files changed, 385 insertions(+), 39 deletions(-) create mode 100644 anthropic_api.py create mode 100644 google_api.py diff --git a/anthropic_api.py b/anthropic_api.py new file mode 100644 index 0000000..503ec9f --- /dev/null +++ b/anthropic_api.py @@ -0,0 +1,93 @@ +import anthropic +import json +import os +import configparser +from requests.exceptions import RequestException, Timeout +import uuid + +def guid_generator(): + return str(uuid.uuid4()) + +TIMEOUT_SECONDS = 60 + +home_dir = os.path.expanduser("~") +bundle_dir = os.path.join(home_dir, "Library", "Application Support", "CopyCat") +models_path = os.path.join(bundle_dir, "models.json") + +class AnthropicAPI: + def __init__(self, api_key, config_file=None): + self.api_key = api_key + self.client = anthropic.Anthropic(api_key=api_key) + self.config_file = config_file + + def generate_response(self, messages, model, max_tokens=None, temperature=0.8): + """ + Generate a response using Anthropic's Claude models. + + Args: + messages: List of message objects with role and content + model: The Claude model to use + max_tokens: Maximum number of tokens to generate + temperature: Temperature for response generation + + Returns: + Response text, prompt tokens, completion tokens, and total tokens + """ + try: + # Convert messages to Anthropic format + system_prompt = None + anthropic_messages = [] + + for message in messages: + if message["role"] == "system": + system_prompt = message["content"] + else: + anthropic_messages.append({ + "role": message["role"], + "content": message["content"] + }) + + # If no max_tokens specified, use a default + if not max_tokens: + with open(models_path, "r") as f: + models = json.load(f) + if model in models: + max_tokens = int(models[model]["token_size"] * 0.9) # 90% of max + else: + max_tokens = 4000 # Default fallback + + # Create the message for Claude + response = self.client.messages.create( + model=model, + messages=anthropic_messages, + system=system_prompt, + max_tokens=max_tokens, + temperature=temperature + ) + + # Extract response content + response_text = response.content[0].text + + # Get token usage + prompt_tokens = response.usage.input_tokens + completion_tokens = response.usage.output_tokens + total_tokens = prompt_tokens + completion_tokens + + return response_text, prompt_tokens, completion_tokens, total_tokens + + except Exception as e: + print(f"Anthropic API Error: {str(e)}") + raise e + +def calculate_anthropic_cost(prompt_tokens, completion_tokens, model=None): + """Calculate the cost of an Anthropic API request.""" + with open(models_path, "r") as f: + models = json.load(f) + + if model in models: + input_price_per_token = models[model]["input_price_per_1k_tokens"] / 1000 + output_price_per_token = models[model]["output_price_per_1k_tokens"] / 1000 + total_price = (prompt_tokens * input_price_per_token) + (completion_tokens * output_price_per_token) + return total_price + else: + return 0 \ No newline at end of file diff --git a/config.ini b/config.ini index 91ec467..dceb13a 100644 --- a/config.ini +++ b/config.ini @@ -20,3 +20,4 @@ user = COPYCAT mem_on_off = True codemode = False include_urls = True +provider = OpenAI \ No newline at end of file diff --git a/copycat.py b/copycat.py index 5f99d37..c605f03 100644 --- a/copycat.py +++ b/copycat.py @@ -615,7 +615,9 @@ def prompt_user(clip, img=False): window.refresh() break model = values["-MODEL-"] - CONFIG["OpenAI"]["model"] = model + provider = provider_models.get(model, "openai") + CONFIG[provider]["model"] = model + CONFIG["GUI"]["provider"] = provider # window["-MODEL-"].update(values["-MODEL-"]) include_urls = values["-URLS-"] @@ -906,4 +908,4 @@ def main(PROMPT, SKIP, prompt_user): 5000, use_fade_in=False, location=(0, 0), - ) + ) \ No newline at end of file diff --git a/extract.py b/extract.py index c5b3d41..e8c4bc4 100644 --- a/extract.py +++ b/extract.py @@ -1,11 +1,23 @@ import sys, os import re import requests +import configparser from PIL import Image from base64 import b64decode, b64encode from PIL import ImageGrab, Image from bs4 import BeautifulSoup import openai +import anthropic +import google.generativeai as genai + +home_dir = os.path.expanduser("~") +bundle_dir = os.path.join(home_dir, "Library", "Application Support", "CopyCat") +config_path = os.path.join(bundle_dir, "config.ini") + +def get_config(): + config = configparser.ConfigParser(strict=False, interpolation=None) + config.read(config_path) + return config def image_to_base64(image_path): @@ -15,39 +27,127 @@ def image_to_base64(image_path): def caption_image(base64_image): """ - Generate a caption for an image using the OpenAI GPT-4 Vision model. + Generate a caption for an image using the selected AI model. Parameters: image_base64 (str): The base64-encoded string of the image to caption. Returns: - str: The caption generated by the GPT-4 Vision model. + str: The caption generated by the AI model. """ - - # Replace 'YOUR_OPENAI_API_KEY' with your actual OpenAI API key - - # Prepare the message payload with the system prompt and the image - messages = [ - {"role": "system", "content": "Generate a caption for the following image."}, - { - "role": "user", - "content": [ - {"type": "text", "text": "What is shown in this image?"}, + config = get_config() + provider = config.get("GUI", "provider", fallback="OpenAI") + + if provider == "OpenAI": + # Use OpenAI for image captioning + openai.api_key = config.get("OpenAI", "api_key") + model = config.get("OpenAI", "model", fallback="gpt-4o") + + # Prepare the message payload with the system prompt and the image + messages = [ + {"role": "system", "content": "Generate a caption for the following image."}, + { + "role": "user", + "content": [ + {"type": "text", "text": "What is shown in this image?"}, + { + "type": "image_url", + "image_url": f"data:image/jpeg;base64,{base64_image}", + }, + ], + }, + ] + + # Make the API call using the selected model + response = openai.ChatCompletion.create( + model=model, + messages=messages, + max_tokens=100, # Adjust max_tokens if needed + ) + + return response.choices[0].message["content"].strip() + + elif provider == "Anthropic": + # Use Anthropic for image captioning + api_key = config.get("Anthropic", "api_key") + model = config.get("Anthropic", "model", fallback="claude-3-haiku-20240307") + + client = anthropic.Anthropic(api_key=api_key) + + # Create the message for Claude + response = client.messages.create( + model=model, + max_tokens=100, + messages=[ { - "type": "image_url", - "image_url": f"data:image/jpeg;base64,{base64_image}", - }, - ], - }, - ] - # Make the API call using the gpt-4-vision-preview model - response = openai.ChatCompletion.create( - model="gpt-4o", - messages=messages, - max_tokens=100, # Adjust max_tokens if needed - ) - - return response.choices[0].message["content"].strip() + "role": "user", + "content": [ + { + "type": "text", + "text": "Generate a caption for the following image. What is shown in this image?" + }, + { + "type": "image", + "source": { + "type": "base64", + "media_type": "image/jpeg", + "data": base64_image + } + } + ] + } + ] + ) + + return response.content[0].text + + elif provider == "Google": + # Use Google for image captioning + api_key = config.get("Google", "api_key") + model = config.get("Google", "model", fallback="gemini-pro") + + genai.configure(api_key=api_key) + + # Initialize the model + gemini_model = genai.GenerativeModel(model_name=model) + + # Create the prompt + prompt = "Generate a caption for the following image. What is shown in this image?" + + # Generate response with image + response = gemini_model.generate_content( + [prompt, {"mime_type": "image/jpeg", "data": base64_image.encode('utf-8')}] + ) + + return response.text + + else: + # Default to OpenAI if provider not recognized + openai.api_key = config.get("OpenAI", "api_key") + + # Prepare the message payload with the system prompt and the image + messages = [ + {"role": "system", "content": "Generate a caption for the following image."}, + { + "role": "user", + "content": [ + {"type": "text", "text": "What is shown in this image?"}, + { + "type": "image_url", + "image_url": f"data:image/jpeg;base64,{base64_image}", + }, + ], + }, + ] + + # Make the API call using gpt-4o + response = openai.ChatCompletion.create( + model="gpt-4o", + messages=messages, + max_tokens=100, # Adjust max_tokens if needed + ) + + return response.choices[0].message["content"].strip() # Example usage: @@ -270,4 +370,4 @@ def isTwitterLink(url): return False return url.startswith("https://www.twitter.com") or url.startswith( "https://twitter.com" - ) + ) \ No newline at end of file diff --git a/google_api.py b/google_api.py new file mode 100644 index 0000000..15dc2b0 --- /dev/null +++ b/google_api.py @@ -0,0 +1,108 @@ +import google.generativeai as genai +import json +import os +import configparser +from requests.exceptions import RequestException, Timeout +import uuid + +def guid_generator(): + return str(uuid.uuid4()) + +TIMEOUT_SECONDS = 60 + +home_dir = os.path.expanduser("~") +bundle_dir = os.path.join(home_dir, "Library", "Application Support", "CopyCat") +models_path = os.path.join(bundle_dir, "models.json") + +class GoogleAPI: + def __init__(self, api_key, config_file=None): + self.api_key = api_key + genai.configure(api_key=api_key) + self.config_file = config_file + + def generate_response(self, messages, model, max_tokens=None, temperature=0.8): + """ + Generate a response using Google's Gemini models. + + Args: + messages: List of message objects with role and content + model: The Gemini model to use + max_tokens: Maximum number of tokens to generate + temperature: Temperature for response generation + + Returns: + Response text, prompt tokens, completion tokens, and total tokens + """ + try: + # Convert messages to Gemini format + system_prompt = None + gemini_messages = [] + + for message in messages: + if message["role"] == "system": + system_prompt = message["content"] + else: + gemini_messages.append({ + "role": "user" if message["role"] == "user" else "model", + "parts": [{"text": message["content"]}] + }) + + # If no max_tokens specified, use a default + if not max_tokens: + with open(models_path, "r") as f: + models = json.load(f) + if model in models: + max_tokens = int(models[model]["token_size"] * 0.9) # 90% of max + else: + max_tokens = 4000 # Default fallback + + # Initialize the model + gemini_model = genai.GenerativeModel(model_name=model) + + # Add system prompt if available + if system_prompt: + gemini_messages.insert(0, { + "role": "user", + "parts": [{"text": f"System: {system_prompt}"}] + }) + + # Create the chat session + chat = gemini_model.start_chat(history=gemini_messages) + + # Generate response + response = chat.send_message( + gemini_messages[-1]["parts"][0]["text"], + generation_config={ + "max_output_tokens": max_tokens, + "temperature": temperature + } + ) + + # Extract response content + response_text = response.text + + # Estimate token usage (Gemini doesn't provide token counts directly) + # Rough estimate: 4 chars = 1 token + prompt_text = "".join([msg["parts"][0]["text"] for msg in gemini_messages]) + prompt_tokens = len(prompt_text) // 4 + completion_tokens = len(response_text) // 4 + total_tokens = prompt_tokens + completion_tokens + + return response_text, prompt_tokens, completion_tokens, total_tokens + + except Exception as e: + print(f"Google API Error: {str(e)}") + raise e + +def calculate_google_cost(prompt_tokens, completion_tokens, model=None): + """Calculate the cost of a Google Gemini API request.""" + with open(models_path, "r") as f: + models = json.load(f) + + if model in models: + input_price_per_token = models[model]["input_price_per_1k_tokens"] / 1000 + output_price_per_token = models[model]["output_price_per_1k_tokens"] / 1000 + total_price = (prompt_tokens * input_price_per_token) + (completion_tokens * output_price_per_token) + return total_price + else: + return 0 \ No newline at end of file diff --git a/gptplus.py b/gptplus.py index 67f95a7..9ca75a5 100644 --- a/gptplus.py +++ b/gptplus.py @@ -290,8 +290,8 @@ def process_request( tokens=tokens, temperature=temperature, ) - except APIError as error: - print(f"OpenAI API Error: {str(error)}") + except Exception as error: + print(f"API Error: {str(error)}") response = "" prompt_tokens = 0 completion_tokens = 0 @@ -331,4 +331,4 @@ def process_request( "total_tokens": total_tokens, "cost": cost, "total_costs": self.total_costs, - } + } \ No newline at end of file diff --git a/models.json b/models.json index 0a306fc..25a6da8 100644 --- a/models.json +++ b/models.json @@ -1,22 +1,62 @@ { "gpt-3.5-turbo": { "token_size": 16384, - "input_price_per_1k_tokens": 0.001, - "output_price_per_1k_tokens": 0.002 + "input_price_per_1k_tokens": 0.0005, + "output_price_per_1k_tokens": 0.0015, + "provider": "openai" }, "gpt-4": { "token_size": 8192, "input_price_per_1k_tokens": 0.01, - "output_price_per_1k_tokens": 0.03 + "output_price_per_1k_tokens": 0.03, + "provider": "openai" }, "gpt-4-turbo": { "token_size": 128000, "input_price_per_1k_tokens": 0.01, - "output_price_per_1k_tokens": 0.03 + "output_price_per_1k_tokens": 0.03, + "provider": "openai" }, "gpt-4o": { "token_size": 128000, "input_price_per_1k_tokens": 0.005, - "output_price_per_1k_tokens": 0.015 + "output_price_per_1k_tokens": 0.015, + "provider": "openai" + }, + "gpt-4o-mini": { + "token_size": 128000, + "input_price_per_1k_tokens": 0.0015, + "output_price_per_1k_tokens": 0.006, + "provider": "openai" + }, + "claude-3-opus-20240229": { + "token_size": 200000, + "input_price_per_1k_tokens": 0.015, + "output_price_per_1k_tokens": 0.075, + "provider": "anthropic" + }, + "claude-3-sonnet-20240229": { + "token_size": 200000, + "input_price_per_1k_tokens": 0.003, + "output_price_per_1k_tokens": 0.015, + "provider": "anthropic" + }, + "claude-3-haiku-20240307": { + "token_size": 200000, + "input_price_per_1k_tokens": 0.00025, + "output_price_per_1k_tokens": 0.00125, + "provider": "anthropic" + }, + "gemini-pro": { + "token_size": 32768, + "input_price_per_1k_tokens": 0.00025, + "output_price_per_1k_tokens": 0.0005, + "provider": "google" + }, + "gemini-ultra": { + "token_size": 32768, + "input_price_per_1k_tokens": 0.00125, + "output_price_per_1k_tokens": 0.00375, + "provider": "google" } -} +} \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 445be7b..3323a8b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,7 @@ aiohttp==3.8.4 aiosignal==1.3.1 altgraph==0.17.3 +anthropic==0.8.1 async-timeout==4.0.2 attrs==23.1.0 beautifulsoup4==4.12.2 @@ -8,6 +9,7 @@ bs4==0.0.1 certifi==2023.5.7 charset-normalizer==3.1.0 frozenlist==1.3.3 +google-generativeai==0.3.1 idna==3.4 macholib==1.16.2 multidict==6.0.4 @@ -27,4 +29,4 @@ soupsieve==2.4.1 tiktoken==0.4.0 tqdm==4.65.0 urllib3==2.0.3 -yarl==1.9.2 +yarl==1.9.2 \ No newline at end of file