Change interactions store input to mode combo

This commit is contained in:
silveroxides
2026-10-22 07:12:01 +02:00
parent 2b4725a02e
commit d16be4fef5
2 changed files with 82 additions and 34 deletions
+49 -24
View File
@@ -347,8 +347,8 @@ class SSL_GeminiInteractionsTextPrompt(IO.ComfyNode):
IO.Int.Input("max_retries", default=3, min=0, max=10, step=1, tooltip="Maximum number of retry attempts when pattern matches. 0 disables retry."),
IO.String.Input("timeout_fallback_text", default="", optional=True, multiline=True, tooltip="Text returned when the Gemini request times out. Leave empty to return the standard timeout message."),
IO.Combo.Input("image_size", options=["None", "512", "1K", "2K", "4K"], default="None", optional=True, tooltip="Generated image resolution. Gemini 3.1 Flash Lite Image supports only 1K; 512 is supported only by Gemini 3.1 Flash Image."),
IO.Boolean.Input("store", default=True, optional=True, tooltip="Store interaction on server for multi-turn conversations. When enabled with an empty previous_interaction_id, automatically continues the last active interaction."),
IO.String.Input("previous_interaction_id", default="", optional=True, multiline=False, tooltip="Optional ID of previous interaction to continue multi-turn conversation. If left empty when store is enabled, automatically uses the last active interaction ID."),
IO.Combo.Input("store", options=["new", "previous", "manual", "disabled"], default="new", optional=True, tooltip="Session mode: 'new' starts a fresh interaction and stores it; 'previous' continues the last stored interaction; 'manual' continues from previous_interaction_id; 'disabled' does not store or continue."),
IO.String.Input("previous_interaction_id", default="", optional=True, multiline=False, tooltip="ID of previous interaction to continue when store is set to 'manual'."),
cls.GeminiVideoConfig.Input("video", optional=True, tooltip="Optional configured Gemini video input with embedded audio and sampling FPS."),
cls.ResponseSchema.Input("response_schema", display_name="Response format", optional=True,
tooltip="Connect an answer format to request the fields you defined. Leave disconnected for a normal answer."),
@@ -617,6 +617,17 @@ class SSL_GeminiInteractionsTextPrompt(IO.ComfyNode):
for cat in cls.CORE_HARM_CATEGORIES
]
STORE_MODES = ("new", "previous", "manual", "disabled")
@classmethod
def _normalize_store_mode(cls, store: Any) -> str:
if store is None:
return "new"
mode = str(store).strip().lower()
if mode not in cls.STORE_MODES:
raise ValueError(f"store must be one of: {', '.join(cls.STORE_MODES)}.")
return mode
@classmethod
def _compute_fingerprint_and_check_cache(cls, config, prompt, system_instruction, model, max_output_tokens,
include_images, aspect_ratio, bypass_mode, use_seed, seed,
@@ -626,7 +637,7 @@ class SSL_GeminiInteractionsTextPrompt(IO.ComfyNode):
include_thoughts=False, thinking_level=None, media_resolution=None,
retry_pattern="", max_retries=3, use_cache=False,
cache_ttl_minutes=60, cache_seed=0, image_size="None",
store=True, previous_interaction_id="", safety_settings=None,
store="new", previous_interaction_id="", safety_settings=None,
schema_identity=None, precomputed_image_hashes=None):
def get_tensor_hash(tensor):
@@ -702,7 +713,7 @@ class SSL_GeminiInteractionsTextPrompt(IO.ComfyNode):
str(media_resolution),
str(retry_pattern),
int(max_retries),
bool(store),
str(store),
str(previous_interaction_id),
str(safety_settings),
schema_identity,
@@ -806,12 +817,13 @@ class SSL_GeminiInteractionsTextPrompt(IO.ComfyNode):
use_proxy=False, proxy_host="127.0.0.1", proxy_port=7890, use_seed=False, seed=0, timeout=30,
include_thoughts=False, thinking_level=None, media_resolution=None,
retry_pattern="", max_retries=3, timeout_fallback_text="",
image_size="None", store=True, previous_interaction_id="",
image_size="None", store="new", previous_interaction_id="",
video: GeminiVideoConfig.Type | None = None,
image_inputs: IO.Autogrow.Type | None = None,
response_schema: ResponseSchema.Type | None = None,
prepared_images: dict[str, Any] | None = None,
precomputed_video: Tuple[bytes | None, str | None, str | None, int | None] | None = None) -> IO.NodeOutput:
precomputed_video: Tuple[bytes | None, str | None, str | None, int | None] | None = None,
chained_previous_id: str = "") -> IO.NodeOutput:
cls._check_interrupt()
print(f"[INFO] SSL_GeminiInteractionsTextPrompt execute called, model: {model}")
@@ -892,6 +904,21 @@ class SSL_GeminiInteractionsTextPrompt(IO.ComfyNode):
return IO.NodeOutput(f"Error processing input video: {e}", cls.generate_empty_image(), output_seed, "", "", "")
video_hash = hashlib.sha256(video_bytes).hexdigest() if video_bytes is not None else None
store_mode = cls._normalize_store_mode(store)
store_enabled = store_mode != "disabled"
if store_mode == "new":
effective_previous_id = str(chained_previous_id or "").strip()
elif store_mode == "previous":
effective_previous_id = str(chained_previous_id or "").strip()
if not effective_previous_id:
effective_previous_id = GeminiInteractionsStoreManager.get_last_interaction_id()
if effective_previous_id:
print(f"[INFO] Automatically continuing last interaction from store: {effective_previous_id}")
elif store_mode == "manual":
effective_previous_id = str(previous_interaction_id or "").strip() or str(chained_previous_id or "").strip()
else:
effective_previous_id = ""
resolved_safety_settings = cls._resolve_safety_settings(config)
precomputed_image_hashes = prepared_images["image_hashes"] if prepared_images is not None else None
fingerprint, cached = cls._compute_fingerprint_and_check_cache(
@@ -903,8 +930,8 @@ class SSL_GeminiInteractionsTextPrompt(IO.ComfyNode):
include_thoughts, thinking_level, media_resolution,
retry_pattern, max_retries, use_cache=use_cache,
cache_ttl_minutes=cache_ttl_minutes, cache_seed=cache_seed,
image_size=image_size, store=store,
previous_interaction_id=previous_interaction_id,
image_size=image_size, store=store_mode,
previous_interaction_id=effective_previous_id,
safety_settings=resolved_safety_settings,
schema_identity=schema_identity,
precomputed_image_hashes=precomputed_image_hashes,
@@ -914,13 +941,6 @@ class SSL_GeminiInteractionsTextPrompt(IO.ComfyNode):
print(f"[INFO] Returning cached result for fingerprint {fingerprint}")
return IO.NodeOutput(*cached)
# Resolve effective previous interaction ID from store if not provided directly
effective_previous_id = str(previous_interaction_id or "").strip()
if store and not effective_previous_id:
effective_previous_id = GeminiInteractionsStoreManager.get_last_interaction_id()
if effective_previous_id:
print(f"[INFO] Automatically continuing last interaction from store: {effective_previous_id}")
print(f"[INFO] Starting interaction, model: {model}")
padded_prompt = prompt
@@ -1199,8 +1219,7 @@ class SSL_GeminiInteractionsTextPrompt(IO.ComfyNode):
kwargs["generation_config"] = call_gen_config
if response_formats:
kwargs["response_format"] = response_formats
if store is not None:
kwargs["store"] = bool(store)
kwargs["store"] = store_enabled
if target_previous_id and str(target_previous_id).strip():
kwargs["previous_interaction_id"] = str(target_previous_id).strip()
@@ -1418,7 +1437,7 @@ class SSL_GeminiInteractionsTextPrompt(IO.ComfyNode):
interaction_id_output = ""
# Record interaction in local store on success
if is_success and store and interaction_id_output:
if is_success and store_enabled and interaction_id_output:
GeminiInteractionsStoreManager.record_interaction(interaction_id_output, model)
# Cache the result if seed used and successful
@@ -1440,7 +1459,7 @@ class SSL_GeminiInteractionsTextPrompt(IO.ComfyNode):
use_proxy=False, proxy_host="127.0.0.1", proxy_port=7890, use_seed=False, seed=0, timeout=30,
include_thoughts=False, thinking_level=None, media_resolution=None,
retry_pattern="", max_retries=3, timeout_fallback_text="",
image_size="None", store=True, previous_interaction_id="",
image_size="None", store="new", previous_interaction_id="",
video: GeminiVideoConfig.Type | None = None,
image_inputs: IO.Autogrow.Type | None = None,
response_schema: ResponseSchema.Type | None = None) -> IO.NodeOutput:
@@ -1512,8 +1531,9 @@ class SSL_GeminiInteractionsTextPrompt(IO.ComfyNode):
max_retries_val = int(_unwrap_scalar(max_retries, 3))
timeout_fb_val = str(_unwrap_scalar(timeout_fallback_text, ""))
img_size_val = str(_unwrap_scalar(image_size, "None"))
store_val = bool(_unwrap_scalar(store, True))
store_val = cls._normalize_store_mode(_unwrap_scalar(store, "new"))
resp_schema_val = _unwrap_scalar(response_schema, None)
is_prev_id_list = _is_list_arg(previous_interaction_id) and len(previous_interaction_id) > 1
def make_item_kwargs(idx, chained_prev_id="", prepared_imgs=None, precomp_vid=None):
p = prompt_list[min(idx, len(prompt_list) - 1)] if prompt_list else ""
@@ -1530,9 +1550,13 @@ class SSL_GeminiInteractionsTextPrompt(IO.ComfyNode):
s = int(_unwrap_scalar(seed, 0))
sys_inst = _get_aligned(system_instruction, idx, "You are a helpful AI assistant.")
prev_id = str(_get_aligned(previous_interaction_id, idx, "") or "").strip()
if not prev_id and store_val and chained_prev_id:
prev_id = chained_prev_id
if store_val == "manual":
if is_prev_id_list or idx == 0:
prev_id = str(_get_aligned(previous_interaction_id, idx, "") or "").strip()
else:
prev_id = ""
else:
prev_id = ""
return {
"config": config_val,
@@ -1567,6 +1591,7 @@ class SSL_GeminiInteractionsTextPrompt(IO.ComfyNode):
"response_schema": resp_schema_val,
"prepared_images": prepared_imgs,
"precomputed_video": precomp_vid,
"chained_previous_id": chained_prev_id,
}
if batch_count == 1:
@@ -1615,7 +1640,7 @@ class SSL_GeminiInteractionsTextPrompt(IO.ComfyNode):
)
)
batch_results.append(item_res)
if store_val and item_res[5]:
if store_val != "disabled" and item_res[5]:
chained_interaction_id = str(item_res[5]).strip()
return IO.NodeOutput(
+33 -10
View File
@@ -125,7 +125,7 @@ def _execute_kwargs(config=None, **overrides):
"max_retries": 3,
"timeout_fallback_text": "",
"image_size": "None",
"store": True,
"store": "new",
"previous_interaction_id": "",
"video": None,
"image_inputs": None,
@@ -159,6 +159,10 @@ def test_interactions_define_schema():
assert "gemini-4-argon" in model_input.options
assert "gemini-4-argon-cyber-permissive" in model_input.options
store_input = next(i for i in schema.inputs if i.id == "store")
assert store_input.options == ["new", "previous", "manual", "disabled"]
assert store_input.default == "new"
output_names = [o.id for o in schema.outputs]
assert output_names == ["text", "image", "final_actual_seed", "structured_output", "thoughts", "interaction_id"]
@@ -393,14 +397,14 @@ def test_multi_turn_chaining(mock_interactions_client):
]
# First turn
args1 = _execute_kwargs(prompt="First message", store=True)
args1 = _execute_kwargs(prompt="First message", store="new")
out1 = interactions_nodes.SSL_GeminiInteractionsTextPrompt.execute(**args1)
id1 = out1[5]
assert id1 == "int_turn_1"
assert mock_interactions_client.calls[0].get("previous_interaction_id") is None
# Second turn continuing first
args2 = _execute_kwargs(prompt="Follow-up message", previous_interaction_id=id1, store=True)
# Second turn continuing first via manual ID
args2 = _execute_kwargs(prompt="Follow-up message", previous_interaction_id=id1, store="manual")
out2 = interactions_nodes.SSL_GeminiInteractionsTextPrompt.execute(**args2)
assert out2[0] == "Turn 2 answer"
assert out2[5] == "int_turn_2"
@@ -556,24 +560,43 @@ def test_auto_store_and_continue_interaction(mock_interactions_client):
mock_interactions_client.responses = [
{"text": "Turn 1 answer", "id": "int_turn_1"},
{"text": "Turn 2 answer", "id": "int_turn_2"},
{"text": "Turn 3 fresh", "id": "int_turn_3"},
{"text": "Turn 4 disabled", "id": "int_turn_4"},
]
# First turn: store=True, previous_interaction_id="" -> creates new interaction
args1 = _execute_kwargs(prompt="First message", store=True, previous_interaction_id="")
# First turn: store="new" -> creates new interaction and stores it
args1 = _execute_kwargs(prompt="First message", store="new", previous_interaction_id="")
out1 = interactions_nodes.SSL_GeminiInteractionsTextPrompt.execute(**args1)
assert out1[0] == "Turn 1 answer"
assert out1[5] == "int_turn_1"
assert "previous_interaction_id" not in mock_interactions_client.calls[0]
assert mock_interactions_client.calls[0]["store"] is True
assert interactions_nodes.GeminiInteractionsStoreManager.get_last_interaction_id() == "int_turn_1"
# Second turn with DIFFERENT prompt: store=True, previous_interaction_id="" -> auto-continues with int_turn_1
args2 = _execute_kwargs(prompt="Second message", store=True, previous_interaction_id="")
# Second turn: store="previous" -> auto-continues with int_turn_1 from store
args2 = _execute_kwargs(prompt="Second message", store="previous", previous_interaction_id="")
out2 = interactions_nodes.SSL_GeminiInteractionsTextPrompt.execute(**args2)
assert out2[0] == "Turn 2 answer"
assert out2[5] == "int_turn_2"
assert mock_interactions_client.calls[1]["previous_interaction_id"] == "int_turn_1"
assert interactions_nodes.GeminiInteractionsStoreManager.get_last_interaction_id() == "int_turn_2"
# Third turn: store="new" -> starts fresh interaction even though store has int_turn_2
args3 = _execute_kwargs(prompt="Third message", store="new", previous_interaction_id="ignored_id")
out3 = interactions_nodes.SSL_GeminiInteractionsTextPrompt.execute(**args3)
assert out3[0] == "Turn 3 fresh"
assert out3[5] == "int_turn_3"
assert "previous_interaction_id" not in mock_interactions_client.calls[2]
assert interactions_nodes.GeminiInteractionsStoreManager.get_last_interaction_id() == "int_turn_3"
# Fourth turn: store="disabled" -> does not continue and sets store=False
args4 = _execute_kwargs(prompt="Fourth message", store="disabled", previous_interaction_id="ignored_id")
out4 = interactions_nodes.SSL_GeminiInteractionsTextPrompt.execute(**args4)
assert out4[0] == "Turn 4 disabled"
assert "previous_interaction_id" not in mock_interactions_client.calls[3]
assert mock_interactions_client.calls[3]["store"] is False
assert interactions_nodes.GeminiInteractionsStoreManager.get_last_interaction_id() == "int_turn_3"
def test_expired_interaction_fallback(mock_interactions_client):
# First call with expired ID fails with 404/not found, then retry succeeds with new interaction
@@ -582,7 +605,7 @@ def test_expired_interaction_fallback(mock_interactions_client):
{"text": "Fresh interaction response", "id": "int_fresh_1"},
]
args = _execute_kwargs(prompt="Continue expired chat", store=True, previous_interaction_id="int_old_expired")
args = _execute_kwargs(prompt="Continue expired chat", store="manual", previous_interaction_id="int_old_expired")
output = interactions_nodes.SSL_GeminiInteractionsTextPrompt.execute(**args)
assert output[0] == "Fresh interaction response"
@@ -693,7 +716,7 @@ def test_interactions_batch_prompts(mock_interactions_client, capsys):
]
args = _execute_kwargs(
prompt=["Prompt A", "Prompt B", "Prompt C"],
store=True,
store="new",
previous_interaction_id="",
image_inputs={"image_1": torch.full((1, 2, 2, 3), 0.5)},
)