diff --git a/README.md b/README.md index 87489ff..a86b7f3 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,150 @@ # ComfyUI-GLHF +<<<<<<< HEAD GLHF is a ComfyUI node that facilitates seamless interaction with the GLHF chat API. Designed to enhance user experience, it supports multiple language models, web search integration, and customizable instructions, making it a powerful extension for AI-driven workflows. +======= + +GLHF is a ComfyUI node that facilitates seamless interaction with the GLHF chat API. Designed to enhance user experience, it supports multiple language models, web search integration, and customizable instructions, making it a powerful extension for AI-driven workflows. + +--- + +## Features + +### Versatile Model Support + +Interact with a range of language models to suit diverse use cases. Configure models via the `config.json` file for flexibility and control over model selection. + +### Integrated Web Search + +Enhance your prompts with real-time information by enabling web search. The node: + +- Retrieves relevant search results. +- Extracts content from URLs included in the prompts. +- Appends this information to the input for more contextual and accurate outputs. + +Here are examples of the web search feature in action: +![Web Search Example 1](imgs/web_search_1.png) +![Web Search Example 2](imgs/web_search_2.png) + +### Custom Instruction Loading + +Customize the behavior of the node by adding `.txt` files to the `custom_instructions` folder. This allows you to tailor the responses based on specific requirements or use cases. + +Here are examples of custom instructions: +![Custom Instruction Example 1](imgs/custom_instruction_1.png) +![Custom Instruction Example 2](imgs/custom_instruction_2.png) + +### Persistent Chat Context + +The node maintains chat history, enabling contextual and coherent multi-turn conversations. You can enable or disable this feature through the `keep_context` option for tailored interactions. + +Here are examples of context keeping: +![Context Keeping Example 1](imgs/context_keeping_1.png) +![Context Keeping Example 2](imgs/context_keeping_2.png) + +### Uncensored Model + +Utilize uncensored models for unrestricted creative applications. +![Uncensored Model Example](imgs/uncensored_model.png) + +### Console Logging + +Monitor your interactions and debug effortlessly with detailed console logs that display request and response details directly in the ComfyUI console. + +--- + +## Getting Started + +### 1. Get your API Key + +- Go to [https://glhf.chat/](https://glhf.chat/) and create an account. +- Go to [https://glhf.chat/users/settings/api](https://glhf.chat/users/settings/api) and copy your API Key. +- Paste your API Key in the `config.json` file located in the ComfyUI-GLHF directory. + +```json +{ + "baseurl": "https://glhf.chat/api/openai/v1", + "api_key": "YOUR_API_KEY", + "models": { + "Llama3.3 70b": "hf:meta-llama/Llama-3.3-70B-Instruct", + "QWQ 32b": "hf:Qwen/QwQ-32B-Preview", + "Llama3.3 70b Uncensored": "hf:huihui-ai/Llama-3.3-70B-Instruct-abliterated" + } +} +``` + +You can also add more models to the models section of the `config.json` file. + +### 2. Install the Node + +- Navigate to the `custom_nodes` folder inside your ComfyUI directory. +- Open a command prompt or terminal in the address bar and run the following command: + +```bash +git clone https://github.com/fairy-root/ComfyUI-GLHF.git +``` + +### 3. Install the Requirements + +- Navigate to the `ComfyUI\python_embeded` folder. +- Run the following command: + +```bash +./python.exe -s -m pip install openai requests bs4 googlesearch-python +``` + +--- + +## Node Parameters + +The `GLHF Chat` with Advanced Web and Link Search node offers extensive customization through the following parameters: + +- **prompt**: The text prompt to send to the GLHF API. +- **seed**: Random seed value for reproducible results. +- **model**: The selected language model from the config.json file. +- **console_log**: Toggle for console logging. +- **enable_web_search**: Enable or disable web search functionality. +- **num_search_results**: Specify the number of search results to include. +- **keep_context**: Enable or disable conversation context maintenance. +- **custom_instruction**: Choose a predefined instruction file to guide model behavior. + +## Custom Instructions + +Custom instructions can significantly enhance the node’s flexibility. To create a new instruction: + +1- Navigate to the `custom_instructions` folder in the ComfyUI-GLHF directory. +2- Create a `.txt` file, using a descriptive name for the instruction. +3- Add the desired instructions to the file. + +- Example: + To create an instruction for Python-specific guidance: + 1- Name the file python.txt. + 2- Populate it with content such as: + ```bash + Respond to queries as a Python expert. Provide detailed explanations and examples where applicable. + ``` + 3- The new instruction will appear in the custom_instruction dropdown menu within the node settings after restarting ComfyUI. + +--- + +## Donation + +Your support is appreciated: + +- USDt (TRC20): `TGCVbSSJbwL5nyXqMuKY839LJ5q5ygn2uS` +- BTC: `13GS1ixn2uQAmFQkte6qA5p1MQtMXre6MT` +- ETH (ERC20): `0xdbc7a7dafbb333773a5866ccf7a74da15ee654cc` +- LTC: `Ldb6SDxUMEdYQQfRhSA3zi4dCUtfUdsPou` + +## Author and Contact + +- GitHub: [FairyRoot](https://github.com/fairy-root) +- Telegram: [@FairyRoot](https://t.me/FairyRoot) + +## License + +This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details. + +## Contributing + +Contributions are welcome! Please open an issue or submit a pull request for any improvements or features. +>>>>>>> bfab6cc (Initial release...) diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..60db4b3 --- /dev/null +++ b/__init__.py @@ -0,0 +1,10 @@ +from .glhf import GlhfChat + +NODE_CLASS_MAPPINGS = { + "glhf_chat": GlhfChat +} +NODE_DISPLAY_NAME_MAPPINGS = { + "glhf_chat": "GLHF Chat", +} + +__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"] \ No newline at end of file diff --git a/config.json b/config.json new file mode 100644 index 0000000..94699b2 --- /dev/null +++ b/config.json @@ -0,0 +1,9 @@ +{ + "baseurl": "https://glhf.chat/api/openai/v1", + "api_key": "YOUR_API_KEY", + "models": { + "Llama3.3 70b": "hf:meta-llama/Llama-3.3-70B-Instruct", + "QWQ 32b": "hf:Qwen/QwQ-32B-Preview", + "Llama3.3 70b Uncensored": "hf:huihui-ai/Llama-3.3-70B-Instruct-abliterated" + } +} \ No newline at end of file diff --git a/custom_instructions/prompt generator.txt b/custom_instructions/prompt generator.txt new file mode 100644 index 0000000..c0cb1a0 --- /dev/null +++ b/custom_instructions/prompt generator.txt @@ -0,0 +1 @@ +Act as a highly skilled photography prompt generator. When the user provides a subject, object, or general concept for an image, create a detailed and comprehensive photography prompt that can be directly used by an AI image generation model. Focus exclusively on generating the prompt, providing no other commentary or explanation. The generated prompt MUST include rich details about the subject or object, encompassing physical attributes such as precise facial expression (e.g., joyful smile with crinkled eyes, thoughtful gaze, melancholic frown), specific posture (e.g., standing tall with shoulders back, leaning against a wall, curled up on a sofa), distinct gestures (e.g., pointing towards the distance, holding a steaming mug, adjusting glasses), and, where applicable and culturally sensitive, suggestive ethnic group or appearance traits (e.g., a woman with warm brown skin and dark curly hair, an elderly man with weathered features), and meticulous details about their clothing (e.g., a flowing red silk dress with intricate embroidery, a worn leather jacket with patches, a simple white t-shirt and jeans). Furthermore, the prompt MUST specify the setting in vivid detail, including the exact location (e.g., a bustling Tokyo street at night, a serene forest bathed in golden sunlight, a cozy vintage bookstore with overflowing shelves), the time of day (e.g., predawn twilight, midday sun casting harsh shadows, a dimly lit evening), the prevailing weather conditions (e.g., a gentle snowfall blanketing the scene, a dramatic thunderstorm with streaks of lightning, a hazy summer afternoon), and pertinent background and foreground elements that enrich the narrative (e.g., a vintage bicycle leaning against a wall, scattered autumn leaves on a cobblestone path, steam rising from a cup of coffee). If a plot or action is conceivable within the image, the prompt MUST outline it concisely (e.g., a young woman laughing as she chases pigeons in a park, a lone astronaut gazing at a distant planet, a chef expertly flipping a pancake in a busy kitchen). Crucially, the prompt MUST specify the lighting conditions with precision, detailing the type of light (e.g., soft diffused natural light, dramatic Rembrandt lighting, harsh neon light), the direction of the light source (e.g., backlight creating a silhouette, sidelight emphasizing texture, top-down light creating dramatic shadows), and the overall intensity and color temperature of the light (e.g., warm golden hour light, cool blue moonlight). The prompt MUST also dictate the intended artistic style (e.g., a photorealistic photograph, a vibrant oil painting, a detailed pencil sketch), potentially referencing specific photographers or painters if desired (e.g., in the style of Ansel Adams, reminiscent of a Van Gogh painting). The prompt MUST then specify the technical aspects of image capture, including the camera position and angle (e.g., eye-level shot for intimacy, low-angle shot for dramatic effect, bird's-eye view for a sweeping panorama), the type of camera or lens to emulate (e.g., shot on a vintage Hasselblad, captured with a wide-angle lens, using a telephoto lens for compression), and the desired depth of field (e.g., shallow depth of field blurring the background, deep depth of field keeping everything in focus). Additional details to include are specific photographic techniques like long exposure (e.g., long exposure capturing light trails), bokeh (e.g., beautiful bokeh in the background), or motion blur (e.g., intentional motion blur to convey speed). If applicable, specify elements like film stock if aiming for a photographic aesthetic (e.g., shot on Kodak Portra 400 film), or suggest specific lens characteristics or filters. The prompt should also consider the overall mood or atmosphere the image should convey (e.g., a sense of mystery and intrigue, a feeling of tranquility and peace, an atmosphere of vibrant energy). Consider adding details about the color palette to be used (e.g., a muted and desaturated color palette, a vibrant and saturated color scheme, a monochromatic image). Think about including elements that add visual interest and storytelling, such as reflections, shadows, or specific textures. The generated output should be ONLY the prompt string, ready for input into an image generation model. \ No newline at end of file diff --git a/custom_instructions/python.txt b/custom_instructions/python.txt new file mode 100644 index 0000000..6989644 --- /dev/null +++ b/custom_instructions/python.txt @@ -0,0 +1,21 @@ +Act as an expert Python developer and help to design and create code blocks / modules as per the user specification. + +RULES: +- MUST provide clean, production-grade, high quality code. +- ASSUME the user is using python version 3.9+ +- USE well-known python design patterns and object-oriented programming approaches +- MUST provide code blocks with proper google style docstrings +- MUST provide code blocks with input and return value type hinting. +- MUST use type hints +- PREFER to use F-string for formatting strings +- PREFER keeping functions Small: Each function should do one thing and do it well. +- USE @property: For getter and setter methods. +- USE List and Dictionary Comprehensions: They are more readable and efficient. +- USE generators for large datasets to save memory. +- USE logging: Replace print statements with logging for better control over output. +- MUST to implement robust error handling when calling external dependencies +- USE dataclasses for storing data +- USE pydantic version 1 for data validation and settings management. +- Ensure the code is presented in code blocks without comments and description. +- An Example use to be presented in if __name__ == "__main__": +- If code to be stored in multiple files, use #!filepath to signal that in the same code block. \ No newline at end of file diff --git a/glhf.py b/glhf.py new file mode 100644 index 0000000..b3e931c --- /dev/null +++ b/glhf.py @@ -0,0 +1,226 @@ +import subprocess +import sys +from jax import config +import openai +import json +import os +from googlesearch import search +import requests +from bs4 import BeautifulSoup +import re +import random + +# Get the directory of the current script (glhf.py) +script_directory = os.path.dirname(os.path.abspath(__file__)) +custom_instructions_directory = os.path.join(script_directory, 'custom_instructions') + +# Construct the full path to config.json +config_path = os.path.join(script_directory, 'config.json') + +def install_and_import(package): + try: + __import__(package) + except ImportError: + print(f"Package {package} not found. Installing...") + subprocess.check_call([sys.executable, "-m", "pip", "install", package]) + finally: + globals()[package] = __import__(package) + +install_and_import("googlesearch") +install_and_import("requests") +install_and_import("bs4") + +def configuration(): + api_key = None + base_url = None + models = {} + try: + with open(config_path, 'r') as f: + config_data = json.load(f) + api_key = config_data.get('api_key') + base_url = config_data.get('baseurl') + models = config_data.get('models', {}) + models = {key: value for key, value in models.items()} + except FileNotFoundError: + print(f"Error: config.json not found at {config_path}") + except json.JSONDecodeError: + print(f"Error: Could not decode JSON from {config_path}") + + return api_key, base_url, models + +def load_custom_instructions(): + instructions = ["None"] # Add "None" option + if not os.path.exists(custom_instructions_directory): + os.makedirs(custom_instructions_directory) + for filename in os.listdir(custom_instructions_directory): + if filename.endswith(".txt"): + instructions.append(filename[:-4]) # Remove .txt extension + return instructions + +api_key, base_url, models_dict = configuration() +models_list = list(models_dict.keys()) +default_model = models_list[0] if models_list else None +custom_instructions_list = load_custom_instructions() + +def fetch_and_extract_content(url): + try: + response = requests.get(url, timeout=10) + response.raise_for_status() # Raise HTTPError for bad responses (4xx or 5xx) + soup = BeautifulSoup(response.content, 'html.parser') + # Extract main content - you might need to adjust selectors based on common website structures + paragraphs = soup.find_all('p') + text_content = "\n".join([p.text for p in paragraphs]) + return text_content.strip() + except requests.exceptions.RequestException as e: + print(f"Error fetching URL {url}: {e}") + return None + except Exception as e: + print(f"Error processing content from {url}: {e}") + return None + +class GlhfChat: + chat_history = [] # Class-level chat history + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "prompt": ("STRING", {"default": "Enter your prompt here...", "multiline": True}), + "seed": ("INT", {"default": 0, "min": 0, "step": 1}), + }, + "optional": { + "model": (models_list, {"default": default_model}), + "Console_log": ("BOOLEAN", {"default": True}), + "enable_web_search": ("BOOLEAN", {"default": False}), + "num_search_results": ("INT", {"default": 5, "min": 1, "max": 10}), + "keep_context": ("BOOLEAN", {"default": True}), + "custom_instruction": (custom_instructions_list, {"default": "None"}), + } + } + + RETURN_TYPES = ("STRING",) + FUNCTION = "execute" + CATEGORY = "GLHF" + + def execute(self, prompt, seed, model=None, Console_log=False, enable_web_search=False, num_search_results=3, keep_context=True, custom_instruction="None"): + selected_model_value = models_dict.get(model) + if not selected_model_value: + print(f"Error: Model '{model}' not found in config.json. Using default.") + selected_model_value = list(models_dict.values())[0] if models_dict else None + if not selected_model_value: + return ("Error: No valid model found.",) + + if Console_log: + print(f"GLHF Chat Request: Model='{model}', Prompt='{prompt}', Seed='{seed}', Web Search Enabled={enable_web_search}, Keep Context={keep_context}, Custom Instruction='{custom_instruction}'") + + augmented_prompt = "" # Initialize as empty + if enable_web_search: + # Regex to find URLs in the prompt + urls_in_prompt = re.findall(r'(https?://\S+)', prompt) + + try: + search_results_content = [] + searched_urls = set() # To avoid processing same URLs multiple times + + # Process URLs found in the prompt first + for url in urls_in_prompt: + if url not in searched_urls: + print(f"Fetching content from URL in prompt: {url}") + content = fetch_and_extract_content(url) + if content: + search_results_content.append(f"Source (from prompt): {url}\nContent:\n{content}\n---\n") + else: + search_results_content.append(f"Source (from prompt): {url}\nCould not retrieve content.\n---\n") + searched_urls.add(url) + + # Process URLs from Google Search + for i, url in enumerate(search(prompt, num_results=num_search_results)): + if url not in searched_urls: + print(f"Fetching content from search result {i+1}: {url}") + content = fetch_and_extract_content(url) + if content: + search_results_content.append(f"Search Result {i+1}:\nSource: {url}\nContent:\n{content}\n---\n") + else: + search_results_content.append(f"Search Result {i+1}:\nSource: {url}\nCould not retrieve content.\n---\n") + searched_urls.add(url) + + if search_results_content: + augmented_prompt = f"Original query: {prompt}\n\nWeb search results and linked content:\n\n{''.join(search_results_content)}\n\nBased on this information, please provide a response." + else: + augmented_prompt = f"Original query: {prompt}\n\nNo relevant web search results or linked content found. Please proceed with the original query." + if Console_log: + print("No relevant web search results or linked content found.") + + except Exception as e: + print(f"Web Search Error: {e}") + augmented_prompt = f"Original query: {prompt}\n\nAn error occurred during web search or fetching linked content. Please proceed with the original query." + + else: + augmented_prompt = prompt # If web search is not enabled, use the original prompt + + response = self._glhf_interaction(base_url, api_key, selected_model_value, augmented_prompt, Console_log, keep_context, custom_instruction) + return response + + def _glhf_interaction(self, base_url, api_key, model_value, prompt, Console_log, keep_context, custom_instruction): + try: + client = openai.OpenAI( + api_key=api_key, + base_url=base_url, + ) + + messages = [] + if custom_instruction != "None": + instruction_path = os.path.join(custom_instructions_directory, f"{custom_instruction}.txt") + try: + with open(instruction_path, 'r') as f: + system_instruction = f.read() + messages.append({"role": "system", "content": system_instruction}) + except FileNotFoundError: + print(f"Error: Custom instruction file '{instruction_path}' not found.") + except Exception as e: + print(f"Error reading custom instruction file: {e}") + + if keep_context and self.chat_history: + messages.extend(self.chat_history) + messages.append({"role": "user", "content": prompt}) + + completion = client.chat.completions.create( + model=model_value, + messages=messages, + stream=False + ) + output_text = completion.choices[0].message.content + + if Console_log: + print(f"GLHF Chat Response: {output_text}") + + # Update chat history + self.chat_history.append({"role": "user", "content": prompt}) + self.chat_history.append({"role": "assistant", "content": output_text}) + + return (output_text,) + + except openai.AuthenticationError as e: + print(f"GLHF Chat Error: Authentication failed - {e}") + return (f"Error: Authentication failed - {e}",) + except openai.APIConnectionError as e: + print(f"GLHF Chat Error: Could not connect to GLHF API - {e}") + return (f"Error: Could not connect to GLHF API - {e}",) + except openai.RateLimitError as e: + print(f"GLHF Chat Error: API request exceeded rate limit - {e}") + return (f"Error: API request exceeded rate limit - {e}",) + except openai.APIStatusError as e: + print(f"GLHF Chat Error: GLHF API returned an error - {e}") + return (f"Error: GLHF API returned an error - {e}",) + except Exception as e: + print(f"GLHF Chat Error: An unexpected error occurred - {e}") + return (f"Error: An unexpected error occurred - {e}",) + +# Node export details +NODE_CLASS_MAPPINGS = { + "glhf_chat": GlhfChat +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "glhf_chat": "GLHF Chat with Advanced Web and Link Search" +} \ No newline at end of file diff --git a/imgs/context_keeping_1.png b/imgs/context_keeping_1.png new file mode 100644 index 0000000..a565782 Binary files /dev/null and b/imgs/context_keeping_1.png differ diff --git a/imgs/context_keeping_2.png b/imgs/context_keeping_2.png new file mode 100644 index 0000000..6bab006 Binary files /dev/null and b/imgs/context_keeping_2.png differ diff --git a/imgs/custom_instruction_1.png b/imgs/custom_instruction_1.png new file mode 100644 index 0000000..7a2759c Binary files /dev/null and b/imgs/custom_instruction_1.png differ diff --git a/imgs/custom_instruction_2.png b/imgs/custom_instruction_2.png new file mode 100644 index 0000000..6cf69ec Binary files /dev/null and b/imgs/custom_instruction_2.png differ diff --git a/imgs/uncensored_model.png b/imgs/uncensored_model.png new file mode 100644 index 0000000..882ece7 Binary files /dev/null and b/imgs/uncensored_model.png differ diff --git a/imgs/web_search_1.png b/imgs/web_search_1.png new file mode 100644 index 0000000..02e4fe8 Binary files /dev/null and b/imgs/web_search_1.png differ diff --git a/imgs/web_search_2.png b/imgs/web_search_2.png new file mode 100644 index 0000000..328ee96 Binary files /dev/null and b/imgs/web_search_2.png differ diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..28a7213 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +openai +googlesearch-python +requests +bs4 \ No newline at end of file