From 162c327bce4f8991bcff305ede8080bbdcc36df5 Mon Sep 17 00:00:00 2001 From: "DESKTOP-TVBJISQ\\Primere" Date: Wed, 13 May 2026 13:21:28 +0200 Subject: [PATCH] V2.0.0 - Mistral --- Nodes/Uniapi.py | 10 ++- components/API/api_helper.py | 7 +++ components/API/external_api_backend.py | 16 +++++ components/API/responses/Mistral_Chat.py | 77 ++++++++++++++++++++++++ components/API/responses/OpenAI_Image.py | 1 - components/file_output.py | 4 +- terminal_helpers/result.json | 77 +++++++++++++++++++++++- terminal_helpers/snippet.py | 22 +++---- 8 files changed, 196 insertions(+), 18 deletions(-) create mode 100644 components/API/responses/Mistral_Chat.py diff --git a/Nodes/Uniapi.py b/Nodes/Uniapi.py index 315c254..452d6da 100644 --- a/Nodes/Uniapi.py +++ b/Nodes/Uniapi.py @@ -7,6 +7,7 @@ from ..components import utility from ..components.API import api_helper import folder_paths +import re import random import argparse import json @@ -41,7 +42,7 @@ class PrimereApiProcessor: SECTION_TITLES = [ {"before": "processor", "name": "primere_api_proc", "title": "🧭 API Setup", "color": "#1B263B", "text_color": "#EAF1F8", "label": "Setup API processor, select related provider and service."}, {"before": "auto_save_result", "name": "primere_save_api_result", "title": "💾 API result save", "color": "#1B263B", "text_color": "#EAF1F8", "label": "Save API results. Define main path, add subdirectory structure, add filename prefixes. Save related data to .txt or .json file."}, - {"after": "save_data_to_txt", "name": "primere_api_body", "title": "⚙ API body parameters", "color": "#1B263B", "text_color": "#EAF1F8", "label": "Set API body custom parameters."}, + {"after": "save_response_to_json", "name": "primere_api_body", "title": "⚙ API body parameters", "color": "#1B263B", "text_color": "#EAF1F8", "label": "Set API body custom parameters."}, ] @classmethod @@ -69,6 +70,7 @@ class PrimereApiProcessor: "image_quality": ("INT", {"default": 95, "min": 1, "max": 100, "step": 1}), "save_data_to_json": ("BOOLEAN", {"default": False}), "save_data_to_txt": ("BOOLEAN", {"default": False}), + "save_response_to_json": ("BOOLEAN", {"default": False}), } cls.optional_inputs = { @@ -378,6 +380,7 @@ class PrimereApiProcessor: image_quality = kwargs.get('image_quality', 95) save_data_to_json = kwargs.get('save_data_to_json', False) save_data_to_txt = kwargs.get('save_data_to_txt', False) + save_response_to_json = kwargs.get('save_response_to_json', False) add_model_to_path = kwargs.get('add_model_to_path', False) model_subdir = None @@ -422,6 +425,11 @@ class PrimereApiProcessor: } file_output.save_metadata(save_data, json_file, txt_file, save_data_to_json, save_data_to_txt, used_values_output) + if save_response_to_json: + response_json_file = os.path.splitext(saved_path)[0] + '.response.json' + with open(response_json_file, 'w', encoding='utf-8') as response_file: + json.dump(api_result_debug, response_file, ensure_ascii=False, indent=4, default=str) + PromptServer.instance.send_sync("primere.save_result", { "node_id": unique_id, "status": "success", diff --git a/components/API/api_helper.py b/components/API/api_helper.py index 7c561c9..9f69396 100644 --- a/components/API/api_helper.py +++ b/components/API/api_helper.py @@ -26,6 +26,10 @@ def get_api_config(name: str) -> dict: config_json['FAL']['APIKEY'] = os.environ.get("FAL_API_KEY") or config_json['FAL']['APIKEY'] case "Elevenlabs": config_json['Elevenlabs']['APIKEY'] = os.environ.get("ELEVENLABS_API_KEY") or config_json['Elevenlabs']['APIKEY'] + case "Mistral": + config_json['Mistral']['APIKEY'] = os.environ.get("MISTRAL_API_KEY") or config_json['Mistral']['APIKEY'] + case "Opencode": + config_json['Opencode']['APIKEY'] = os.environ.get("OPENCODE_API_KEY") or config_json['Opencode']['APIKEY'] return config_json @@ -54,6 +58,9 @@ def create_api_client(api_provider, config_json): from elevenlabs.client import ElevenLabs load_dotenv() APIClient = ElevenLabs(api_key=config_json['Elevenlabs']['APIKEY']) + case "Mistral": + from mistralai.client import Mistral + APIClient = Mistral(api_key=config_json['Mistral']['APIKEY']) case _: return (None, None) diff --git a/components/API/external_api_backend.py b/components/API/external_api_backend.py index bbdc51d..03d1250 100644 --- a/components/API/external_api_backend.py +++ b/components/API/external_api_backend.py @@ -19,6 +19,7 @@ import numpy as np import torch import comfy.utils import types +import ast from . import request_exceptions @@ -623,6 +624,20 @@ def apply_response_handler(schema: dict[str, Any] | None, api_result: Any, provi return handler(api_result, **kwargs) def sanitize_debug_value(value: Any) -> Any: + if isinstance(value, str): + text_value = value.strip() + if text_value: + try: + parsed_json = json.loads(text_value) + return sanitize_debug_value(parsed_json) + except Exception: + normalized = re.sub(r"\bUnset\s*\(\s*\)", "None", text_value) + try: + parsed_literal = ast.literal_eval(normalized) + return sanitize_debug_value(parsed_literal) + except Exception: + pass + if isinstance(value, torch.Tensor): return f"[torch.Tensor omitted: shape={tuple(value.shape)}, dtype={value.dtype}]" @@ -689,6 +704,7 @@ def sanitize_debug_value(value: Any) -> Any: def sanitize_api_debug_payload(value: Any) -> Any: return redact_reference_images(sanitize_debug_value(value)) + def apply_parameter_constraints(selected_parameters: dict[str, Any], schema: dict[str, Any] | None) -> dict[str, Any]: if not isinstance(schema, dict): return selected_parameters diff --git a/components/API/responses/Mistral_Chat.py b/components/API/responses/Mistral_Chat.py new file mode 100644 index 0000000..9d9acb7 --- /dev/null +++ b/components/API/responses/Mistral_Chat.py @@ -0,0 +1,77 @@ +from __future__ import annotations + +import ast +import dataclasses +import json +import re +from typing import Any + + +def _normalize_input(value: Any) -> Any: + if isinstance(value, str): + text_value = value.strip() + if not text_value: + return value + try: + return json.loads(text_value) + except Exception: + normalized = re.sub(r"\bUnset\s*\(\s*\)", "None", text_value) + try: + return ast.literal_eval(normalized) + except Exception: + return value + return value + + +def _to_builtin(value: Any) -> Any: + value = _normalize_input(value) + + if isinstance(value, dict): + return {str(k): _to_builtin(v) for k, v in value.items()} + if isinstance(value, list): + return [_to_builtin(item) for item in value] + if isinstance(value, tuple): + return [_to_builtin(item) for item in value] + if dataclasses.is_dataclass(value): + return _to_builtin(dataclasses.asdict(value)) + if hasattr(value, "__dict__") and not isinstance(value, (str, int, float, bool, type(None))): + return {str(k): _to_builtin(v) for k, v in vars(value).items() if not str(k).startswith("_")} + return value + + +def _extract_content(payload: Any) -> str | None: + data = _to_builtin(payload) + if not isinstance(data, dict): + return None + + choices = data.get("choices") + if not isinstance(choices, list) or len(choices) == 0: + return None + + first_choice = choices[0] if isinstance(choices[0], dict) else None + if not isinstance(first_choice, dict): + return None + + message = first_choice.get("message") + if not isinstance(message, dict): + return None + + content = message.get("content") + if isinstance(content, str) and content.strip(): + return content + return None + + +def handle_response(api_result, schema=None, loaded_client=None, response_url=None, client=None, sdk_context=None): + payload = _to_builtin(api_result) + content = _extract_content(payload) + + if content is not None: + return ["text_result", content.encode("utf-8")] + + fallback = { + "error": "Mistral content not found in response. Saved full response instead.", + "response": payload, + } + fallback_text = json.dumps(fallback, ensure_ascii=False, indent=2, default=str) + return ["text_result", fallback_text.encode("utf-8")] diff --git a/components/API/responses/OpenAI_Image.py b/components/API/responses/OpenAI_Image.py index f7dc04d..4bd55b2 100644 --- a/components/API/responses/OpenAI_Image.py +++ b/components/API/responses/OpenAI_Image.py @@ -1,5 +1,4 @@ from __future__ import annotations - import base64 from . import response_helper diff --git a/components/file_output.py b/components/file_output.py index 90b3c49..e3d4650 100644 --- a/components/file_output.py +++ b/components/file_output.py @@ -122,8 +122,8 @@ def resolve_output_file(output_path_input, base_dir, subdirs, prefix, delimiter, ) output_file = os.path.abspath(os.path.join(resolved, filename)) - json_file = os.path.splitext(output_file)[0] + '.json' - txt_file = os.path.splitext(output_file)[0] + '.txt' + json_file = os.path.splitext(output_file)[0] + '_meta.json' + txt_file = os.path.splitext(output_file)[0] + '_meta.txt' return output_file, json_file, txt_file diff --git a/terminal_helpers/result.json b/terminal_helpers/result.json index a048ff1..f07bde6 100644 --- a/terminal_helpers/result.json +++ b/terminal_helpers/result.json @@ -18,9 +18,34 @@ "fake_reference_image_value_1", "fake_reference_image_value_2" ], - "aspect_ratio": ["auto", "21:9", "16:9", "4:3", "1:1", "3:4", "9:16"], - "duration": ["auto", 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], - "resolution": ["480p", "720p"], + "aspect_ratio": [ + "auto", + "21:9", + "16:9", + "4:3", + "1:1", + "3:4", + "9:16" + ], + "duration": [ + "auto", + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15 + ], + "resolution": [ + "480p", + "720p" + ], "video_urls": [ "fake_video_urls_value_1", "fake_video_urls_value_2" @@ -53,5 +78,51 @@ } } } + }, + "Mistral": { + "Chat": { + "provider": "Mistral", + "service": "Chat", + "response_handler": "Mistral_Chat.py", + "reference_images_handler": "Mistral.py", + "import_modules": [ + "import your_provider_sdk", + "from your_provider_sdk import types" + ], + "possible_parameters": { + "model": [ + "example-model-1", + "example-model-2" + ], + "response": [ + "fake_response_value_1", + "fake_response_value_2" + ], + "role": [ + "fake_role_value_1", + "fake_role_value_2" + ] + }, + "request": { + "method": "SDK", + "endpoint": "mistral.chat.complete", + "sdk_call": { + "args": [], + "kwargs": { + "model": "{{model}}", + "messages": [ + { + "role": "{{role}}", + "content": "{{prompt}}" + } + ], + "stream": false, + "response_format": { + "type": "{{response}}" + } + } + } + } + } } } diff --git a/terminal_helpers/snippet.py b/terminal_helpers/snippet.py index eb58bbf..87d857e 100644 --- a/terminal_helpers/snippet.py +++ b/terminal_helpers/snippet.py @@ -1,13 +1,13 @@ -result = fal_client.subscribe("bytedance/seedance-2.0/fast/reference-to-video", - arguments={ - "prompt": prompt, - "image_urls": reference_image, - "video_urls": video_urls, - "audio_urls": audio_urls, - "resolution": resolution, - "seed": seed, - "duration": "auto", - "aspect_ratio": "auto", - "with_logs": with_logs +res = mistral.chat.complete( + model=model, + messages=[ + { + "role": role, + "content": prompt + }, + ], + stream=False, + response_format={ + "type": response, } ) \ No newline at end of file