changeset 4:3803ed45745c draft default tip

planemo upload for repository https://github.com/bgruening/galaxytools/tree/master/tools/chatgpt commit 81bb45731241a082de7b44430afc97a8491a9e47
author bgruening
date Wed, 07 Jan 2026 13:00:53 +0000
parents 430ece17fc20
children
files README.md README.rst chatgpt.py chatgpt.xml
diffstat 4 files changed, 221 insertions(+), 152 deletions(-) [+]
line wrap: on
line diff
--- a/README.md	Wed Sep 11 16:36:14 2024 +0000
+++ b/README.md	Wed Jan 07 13:00:53 2026 +0000
@@ -6,7 +6,7 @@
 Users can upload context data in various formats and ask questions or execute prompts related to that data.
 The tool then uploads the data to a OpenAI server and processes them using the selected ChatGPT model, returning an AI-generated response tailored to the context provided.
 
-To utilize this tool, users need to input their OpenAI API key in the user preferences. To obtain an API key, visit https://platform.openai.com/account/api-keys.
+To utilize this tool, users need to input their OpenAI API key in the credentials section. To obtain an API key, visit https://platform.openai.com/account/api-keys.
 
 Make sure to setup the payment method in your OpenAI account to use the API key in here: https://platform.openai.com/settings/organization/billing/
 
--- a/README.rst	Wed Sep 11 16:36:14 2024 +0000
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,19 +0,0 @@
-ChatGPT
-=======
-
-Set up user credentials on Galaxy
----------------------------------
-
-To enable users to set their credentials for this tool, make sure the
-file ``config/user_preferences_extra.yml`` has the following section:
-
-::
-
-        preferences:
-            chatgpt:
-                description: Your ChatGPT API settings
-                inputs:
-                - name: api_key
-                    label: API Key
-                    type: password
-                    required: False
--- a/chatgpt.py	Wed Sep 11 16:36:14 2024 +0000
+++ b/chatgpt.py	Wed Jan 07 13:00:53 2026 +0000
@@ -1,96 +1,201 @@
+from __future__ import annotations
+
+import base64
 import json
 import os
 import sys
+from collections.abc import Iterable, Sequence
+from dataclasses import dataclass
+from typing import cast, ClassVar, TypeAlias
 
 from openai import AuthenticationError, OpenAI
+from openai.types.chat import ChatCompletion
+from openai.types.chat.chat_completion_content_part_image_param import (
+    ChatCompletionContentPartImageParam,
+    ImageURL,
+)
+from openai.types.chat.chat_completion_content_part_param import (
+    ChatCompletionContentPartParam,
+)
+from openai.types.chat.chat_completion_content_part_text_param import (
+    ChatCompletionContentPartTextParam,
+)
+from openai.types.chat.chat_completion_message_param import ChatCompletionMessageParam
+from openai.types.chat.chat_completion_user_message_param import (
+    ChatCompletionUserMessageParam,
+)
 
-context_files = json.loads(sys.argv[1])
-question = sys.argv[2]
-model = sys.argv[3]
-with open(sys.argv[4], "r") as f:
-    openai_api_key = f.read().strip()
-if not openai_api_key:
-    print("OpenAI API key is not provided in user preferences!")
-    sys.exit(1)
+MessageContentItem: TypeAlias = ChatCompletionContentPartParam
+ContextFile: TypeAlias = tuple[str, str]
+
 
-client = OpenAI(api_key=openai_api_key)
+@dataclass(frozen=True)
+class MessageBuilder:
+    question: str
+    context_files: Sequence[ContextFile]
 
-file_search_file_streams = []
-image_files = []
+    _MEDIA_TYPE_MAP: ClassVar[dict[str, str]] = {
+        ".jpg": "image/jpeg",
+        ".jpeg": "image/jpeg",
+        ".png": "image/png",
+        ".gif": "image/gif",
+        ".webp": "image/webp",
+    }
+
+    _MAX_IMAGE_BYTES: ClassVar[int] = 20 * 1024 * 1024
+
+    def build(self) -> list[MessageContentItem]:
+        """Construct the completion request payload."""
+        message: list[MessageContentItem] = [{"type": "text", "text": self.question}]
 
-for path, type in context_files:
-    if type == "image":
-        if os.path.getsize(path) > 20 * 1024 * 1024:
-            print(f"File {path} exceeds the 20MB limit and will not be processed.")
-            sys.exit(1)
-        file = client.files.create(file=open(path, "rb"), purpose="vision")
-        promt = {"type": "image_file", "image_file": {"file_id": file.id}}
-        image_files.append(promt)
-    else:
-        file_search_file_streams.append(open(path, "rb"))
+        for path, file_type in self.context_files:
+            if file_type == "image":
+                message.append(self._build_image_content(path))
+            else:
+                message.append(self._build_text_content(path))
+
+        return message
+
+    def _build_image_content(self, path: str) -> ChatCompletionContentPartImageParam:
+        """Encode an image context file for model consumption."""
+        if os.path.getsize(path) > self._MAX_IMAGE_BYTES:
+            raise ValueError(
+                f"File {path} exceeds the 20MB limit and will not be processed."
+            )
+
+        _, ext = os.path.splitext(path)
+        media_type = self._MEDIA_TYPE_MAP.get(ext.lower(), "image/jpeg")
+        with open(path, "rb") as img_file:
+            image_data = base64.standard_b64encode(img_file.read()).decode("utf-8")
 
-try:
-    assistant = client.beta.assistants.create(
-        instructions=(
-            "You will receive questions about files from file searches "
-            "and image files. For file search queries, identify and "
-            "retrieve the relevant files based on the question. "
-            "For image file queries, analyze the image content and "
-            "provide relevant information or insights based on the image data."
-        ),
+        image_url_payload = ImageURL(
+            url=f"data:{media_type};base64,{image_data}", detail="auto"
+        )
+        return ChatCompletionContentPartImageParam(
+            type="image_url", image_url=image_url_payload
+        )
+
+    def _build_text_content(self, path: str) -> ChatCompletionContentPartTextParam:
+        """Read a text context file and wrap it in a templated message."""
+        try:
+            with open(path, "r", encoding="utf-8", errors="ignore") as text_file:
+                file_content = text_file.read()
+        except OSError as exc:
+            raise ValueError(f"Error reading file {path}: {exc}") from exc
+
+        basename = os.path.basename(path)
+        return ChatCompletionContentPartTextParam(
+            type="text",
+            text=f"--- Content of {basename} ---\n{file_content}\n",
+        )
+
+
+def parse_context_files(raw: str) -> list[ContextFile]:
+    """Parse and validate the JSON encoded context file descriptors."""
+    try:
+        decoded = json.loads(raw)
+    except json.JSONDecodeError as exc:
+        raise ValueError("Invalid JSON payload for context files.") from exc
+
+    if not isinstance(decoded, list):
+        raise ValueError("Context files payload must be a list.")
+
+    parsed: list[ContextFile] = []
+    for entry in decoded:
+        if (
+            isinstance(entry, list)
+            and len(entry) == 2
+            and isinstance(entry[0], str)
+            and isinstance(entry[1], str)
+        ):
+            parsed.append((entry[0], entry[1]))
+        else:
+            raise ValueError(
+                "Each context file entry must be a pair of strings [path, type]."
+            )
+
+    return parsed
+
+
+def build_messages(
+    question: str, context_files: Sequence[ContextFile]
+) -> list[MessageContentItem]:
+    """Helper to hide the dataclass implementation detail from callers."""
+    return MessageBuilder(question=question, context_files=context_files).build()
+
+
+def call_chat_completion(
+    client: OpenAI, model: str, messages: Iterable[MessageContentItem]
+) -> ChatCompletion:
+    """Request a chat completion using the given client."""
+    user_message = ChatCompletionUserMessageParam(role="user", content=list(messages))
+    payload: list[ChatCompletionMessageParam] = [
+        cast(ChatCompletionMessageParam, user_message)
+    ]
+    return client.chat.completions.create(
         model=model,
-        tools=[{"type": "file_search"}] if file_search_file_streams else [],
-    )
-except AuthenticationError as e:
-    print(f"Authentication error: {e.message}")
-    sys.exit(1)
-except Exception as e:
-    print(f"An error occurred: {str(e)}")
-    sys.exit(1)
-
-if file_search_file_streams:
-    vector_store = client.beta.vector_stores.create()
-    file_batch = client.beta.vector_stores.file_batches.upload_and_poll(
-        vector_store_id=vector_store.id, files=file_search_file_streams
-    )
-    assistant = client.beta.assistants.update(
-        assistant_id=assistant.id,
-        tool_resources={"file_search": {"vector_store_ids": [vector_store.id]}},
+        messages=payload,
     )
 
-messages = [
-    {
-        "role": "user",
-        "content": [
-            {
-                "type": "text",
-                "text": question,
-            },
-            *image_files,
-        ],
-    }
-]
-thread = client.beta.threads.create(messages=messages)
-run = client.beta.threads.runs.create_and_poll(
-    thread_id=thread.id, assistant_id=assistant.id
-)
-assistant_messages = list(
-    client.beta.threads.messages.list(thread_id=thread.id, run_id=run.id)
-)
-if not assistant_messages:
+
+def main(argv: Sequence[str]) -> int:
+    if len(argv) < 4:
+        print("Usage: chatgpt.py <context_files_json> <question> <model>")
+        return 1
+
+    try:
+        context_files = parse_context_files(argv[1])
+    except ValueError as exc:
+        print(str(exc))
+        return 1
+
+    question = argv[2]
+    model = argv[3]
+    question = question.replace("__cn__", "\n")
+
+    openai_api_key = os.getenv("OPENAI_API_KEY")
+    if not openai_api_key:
+        print("OpenAI API key is not provided in credentials!")
+        return 1
+
+    client = OpenAI(api_key=openai_api_key)
+
+    try:
+        message_content = build_messages(question, context_files)
+        response = call_chat_completion(client, model, message_content)
+    except AuthenticationError as exc:
+        print(f"Authentication error: {exc}")
+        return 1
+    except ValueError as exc:
+        print(str(exc))
+        return 1
+    except Exception as exc:  # noqa: BLE001 - keep reporting unexpected OpenAI errors
+        print(f"An error occurred: {exc}")
+        return 1
+
+    if not response.choices:
+        print(
+            "No output was generated!\nPlease ensure that your OpenAI account has sufficient credits.\n"
+            "You can check your balance here: https://platform.openai.com/settings/organization/billing"
+        )
+        return 1
+
+    message = response.choices[0].message
+    content = getattr(message, "content", None)
+    if not content:
+        print(
+            "No output was generated!\nPlease ensure that your OpenAI account has sufficient credits.\n"
+            "You can check your balance here: https://platform.openai.com/settings/organization/billing"
+        )
+        return 1
+
     print(
-        "No output was generated!\nPlease ensure that your OpenAI account has sufficient credits.\n"
-        "You can check your balance here: https://platform.openai.com/settings/organization/billing"
+        f"Successfully generated response for:\n{question[:100]}{'...' if len(question) > 100 else ''}"
     )
-    sys.exit(1)
-message_content = assistant_messages[0].content[0].text.value
-print("Output has been saved!")
-with open("output.txt", "w") as f:
-    f.write(message_content)
+    with open("output.md", "w", encoding="utf-8") as file_handle:
+        file_handle.write(content)
+    return 0
 
-for image in image_files:
-    client.files.delete(image["image_file"]["file_id"])
-if file_search_file_streams:
-    client.beta.vector_stores.delete(vector_store.id)
-client.beta.threads.delete(thread.id)
-client.beta.assistants.delete(assistant.id)
+
+if __name__ == "__main__":
+    sys.exit(main(sys.argv))
--- a/chatgpt.xml	Wed Sep 11 16:36:14 2024 +0000
+++ b/chatgpt.xml	Wed Jan 07 13:00:53 2026 +0000
@@ -1,80 +1,63 @@
-<tool id="chatgpt_openai_api" name="chatGPT" version="@TOOL_VERSION@+galaxy@VERSION_SUFFIX@" profile="23.0">
+<tool id="chatgpt_openai_api" name="chatGPT" version="@TOOL_VERSION@+galaxy@VERSION_SUFFIX@" profile="25.1">
     <description>Integrating OpenAI's ChatGPT into Galaxy</description>
     <macros>
-        <token name="@TOOL_VERSION@">2024</token>
-        <token name="@VERSION_SUFFIX@">2</token>
+        <token name="@TOOL_VERSION@">2025</token>
+        <token name="@VERSION_SUFFIX@">1</token>
     </macros>
     <requirements>
-        <requirement type="package" version="3.12">python</requirement>
-        <requirement type="package" version="1.35.13">openai</requirement>
+        <requirement type="package" version="3.13">python</requirement>
+        <requirement type="package" version="2.7.1">openai</requirement>
+        <credentials name="openai_credentials" version="1.0" label="OpenAI Credentials" description="Credentials for accessing OpenAI's API.">
+            <secret name="openai_api_key" inject_as_env="OPENAI_API_KEY" optional="false" label="OpenAI API Key" description="Your OpenAI API key is required to use this tool. You can obtain it from your OpenAI account dashboard."/>
+        </credentials>
     </requirements>
     <command detect_errors="exit_code"><![CDATA[
 #import json
 #import os
 #import re
 #set LINK_LIST = []
-#for $input in $context
-    #set file_name = os.path.splitext($input.element_identifier)[0]
-    ## list of supported filetypes in OpenAI. If Galaxy has a filetype that is not in this list, just use the generic `txt`.
-    #set ext = $input.ext if $input.ext in ['c', 'cpp', 'css', 'csv', 'docx', 'gif', 'html', 'java', 'jpeg', 'jpg', 'js', 'json', 'md', 'pdf', 'php', 'pkl', 'png', 'pptx', 'py', 'rb', 'tar', 'tex', 'ts', 'txt', 'webp', 'xlsx', 'xml', 'zip'] else 'txt'
-    #set LINK = re.sub('[^\w\-]', '_', $file_name)+'.'+$ext
-    ln -s '$input' '$LINK' &&
-    ## OpenAI has some special handling of "images", so let's annotate this here and pass it to the script
-    #set type = 'image' if $input.ext in ['jpg', 'jpeg', 'png', 'webp', 'gif'] else 'text'
-    $LINK_LIST.append([$LINK, $type])
-#end for
+#if $context
+    #for $input in $context
+        #set file_name = os.path.splitext($input.element_identifier)[0]
+        ## list of supported filetypes in OpenAI. If Galaxy has a filetype that is not in this list, just use the generic `txt`.
+        #set ext = $input.ext if $input.ext in ['c', 'cpp', 'css', 'csv', 'docx', 'gif', 'html', 'java', 'jpeg', 'jpg', 'js', 'json', 'md', 'pdf', 'php', 'pkl', 'png', 'pptx', 'py', 'rb', 'tar', 'tex', 'ts', 'txt', 'webp', 'xlsx', 'xml', 'zip'] else 'txt'
+        #set LINK = re.sub('[^\w\-]', '_', $file_name)+'.'+$ext
+        ln -s '$input' '$LINK' &&
+        ## OpenAI has some special handling of "images", so let's annotate this here and pass it to the script
+        #set type = 'image' if $input.ext in ['jpg', 'jpeg', 'png', 'webp', 'gif'] else 'text'
+        $LINK_LIST.append([$LINK, $type])
+    #end for
+#end if
 #set context_files = json.dumps($LINK_LIST)
 
 python '$__tool_directory__/chatgpt.py'
 '$context_files'
 '$prompt'
 '$model'
-'$openai_api_key_file'
     ]]></command>
-    <configfiles>
-        <configfile name="openai_api_key_file"><![CDATA[
-$__user__.extra_preferences.get('chatgpt|api_key', "")
-        ]]></configfile>
-    </configfiles>
     <inputs>
-        <conditional name="input_type">
-            <param name="input_type_selector" type="select" label="Choose the model" help="Vision models are capable to have image as input.">
-                <option value="vision" selected="true">Vision models</option>
-                <option value="all">All models</option>
-            </param>
-            <when value="vision">
-                <param name="model" type="select" optional="false" label="Model" help="Select the model you want to use.">
-                    <option value="gpt-4o-mini" selected="true">Affordable and intelligent small model for fast, lightweight tasks (gpt-4o-mini)</option>
-                    <option value="gpt-4o">High-intelligence flagship model for complex, multi-step tasks (gpt-4o)</option>
-                    <option value="gpt-4-turbo">The previous set of high-intelligence model with vision capabilities (gpt-4-turbo)</option>
-                </param>
-                <param name="context" type="data" multiple="true" optional="false" format="doc,docx,html,json,pdf,txt,jpg,jpeg,png,webp,gif" label="Context" max="500" help="This data will be uploaded to OpenAI's servers for processing."/>
-            </when>
-            <when value="all">
-                <param name="model" type="select" optional="false" label="Model" help="Select the model you want to use.">
-                    <option value="gpt-4o-mini" selected="true">Affordable and intelligent small model for fast, lightweight tasks (gpt-4o-mini)</option>
-                    <option value="gpt-4o">High-intelligence flagship model for complex, multi-step tasks (gpt-4o)</option>
-                    <option value="gpt-4-turbo">The previous set of high-intelligence model with vision capabilities (gpt-4-turbo)</option>
-                    <option value="gpt-4" selected="true">The previous set of high-intelligence model (gpt-4)</option>
-                    <option value="gpt-3.5-turbo">A fast, inexpensive model for simple tasks (GPT-3.5-turbo)</option>
-                </param>
-                <param name="context" type="data" multiple="true" optional="false" format="doc,docx,html,json,pdf,txt" label="Context" max="500" help="This data will be uploaded to OpenAI's servers for processing."/>
-            </when>
-        </conditional>
+        <param name="model" type="select" optional="false" label="Model" help="Select the model you want to use.">
+            <option value="gpt-5" selected="true">Flagship model for coding, reasoning, and agentic tasks across domains. (gpt-5)</option>
+            <option value="gpt-5-mini">Faster, more cost-efficient version of GPT-5. It's great for well-defined tasks and precise prompts. (gpt-5-mini)</option>
+            <option value="gpt-5-nano">Fastest, cheapest version of GPT-5. It's great for summarization and classification tasks. (gpt-5-nano)</option>
+            <option value="gpt-4.1">Excels at instruction following and tool calling, with broad knowledge across domains (gpt-4.1)</option>
+            <option value="gpt-4o">High-intelligence flagship model for complex, multi-step tasks (gpt-4o)</option>
+        </param>
+        <param name="context" type="data" multiple="true" optional="true" format="docx,html,json,pdf,txt,jpg,png,gif" label="Context" max="500" help="Optional context data that will be uploaded to OpenAI's servers for processing."/>
         <param name="prompt" type="text" optional="false" label="Prompt" help="Prompts or tasks you want ChatGPT to perform." area="true">
             <validator type="empty_field"/>
         </param>
     </inputs>
     <outputs>
-        <data name="output" format="txt" label="${tool.name} on ${on_string}" from_work_dir="./output.txt"/>
+        <data name="output" format="markdown" label="${tool.name} (${model}) #if $on_string then ' on ' + $on_string else ''#" from_work_dir="./output.md"/>
     </outputs>
     <tests>
         <test expect_failure="true" expect_exit_code="1">
+            <param name="model" value="gpt-5"/>
             <param name="context" value="chatgpt_test.txt" ftype="txt"/>
             <param name="prompt" value="What is this?"/>
-            <param name="model" value="gpt-4o-mini"/>
             <assert_stdout>
-                <has_text text="OpenAI API key is not provided in user preferences!"/>
+                <has_text text="OpenAI API key is not provided in credentials!"/>
             </assert_stdout>
         </test>
     </tests>
@@ -88,7 +71,7 @@
 Users can upload context data in various formats and ask questions or execute prompts related to that data.
 The tool then uploads the data to a OpenAI server and processes them using the selected ChatGPT model, returning an AI-generated response tailored to the context provided.
 
-To utilize this tool, users need to input their OpenAI API key in the user preferences. To obtain an API key, visit API keys page in your OpenAI Dashboard.
+To utilize this tool, users need to input their OpenAI API key in the credentials section. To obtain an API key, visit API keys page in your OpenAI Dashboard.
 Make sure to setup the payment method in your OpenAI account to use the API key.
 
 When you run this tool, your input data is sent to OpenAI's servers using your API-key. 
@@ -106,10 +89,10 @@
 
 **Input**
 
-1. **Upload Context Data**: Users can upload up to 500 files in formats such as DOC, DOCX, HTML, JSON, PDF, TXT, JPG, JPEG, PNG, WEBP, or GIF. 
-This context data serves as the input for the prompt you wish to execute.
+1. **Upload Context Data** (Optional): You can optionally upload up to 500 files in formats such as DOC, DOCX, HTML, JSON, PDF, TXT, JPG, JPEG, PNG, WEBP, or GIF. 
+This context data serves as the input for the prompt you wish to execute. If no context is provided, ChatGPT will respond based solely on the prompt.
 
-2. **Provide a Prompt**: Once the context data is added, users can provide a prompt for a task ChatGPT should execute.
+2. **Provide a Prompt**: Provide a prompt or task for ChatGPT to execute.
 The more specific the prompt, the more tailored the response will be.
 
 3. **Select a Model**: Choose the ChatGPT model that best fits your needs.