diff --git a/docs/ai_configuration.md b/docs/ai_configuration.md index c3bcff43..4624d034 100644 --- a/docs/ai_configuration.md +++ b/docs/ai_configuration.md @@ -92,8 +92,8 @@ Endpoint overrides remain available in APIs that already expose them. All packaged operations default to one additional retry after the first attempt. Set `retries: 0` to disable retries. Temperature is model-specific: modern OpenAI -models leave it unset, legacy GPT-4o Chat Completions keeps `0.2`, and the configured -Gemini URL-retrieval model keeps `0.1`. Unset temperature uses the provider's default. +models and Gemini URL retrieval leave it unset, while legacy GPT-4o Chat Completions +keeps `0.2`. Unset temperature uses the provider's default (`1.0` for Gemini 3). For extraction, a saved definition's model retains its existing precedence over the caller's `model`. Both tuning and runtime defaults are resolved for that diff --git a/tests/test_ai_caller_config.py b/tests/test_ai_caller_config.py index 444def57..7b999c8f 100644 --- a/tests/test_ai_caller_config.py +++ b/tests/test_ai_caller_config.py @@ -449,6 +449,8 @@ def google_transport(monkeypatch): def generate_content(**kwargs): calls["requests"].append(kwargs) + if "response" in calls: + return calls["response"] return SimpleNamespace(candidates=[SimpleNamespace( url_context_metadata=None, content=SimpleNamespace(parts=[SimpleNamespace(text='{"name":"Synthetic"}')]), @@ -507,6 +509,65 @@ def test_google_retrieval_uses_config_at_sdk_boundary(configured_ai, google_tran assert len(google_transport["closed"]) == 1 +def test_google_packaged_defaults_omit_temperature(google_transport, monkeypatch): + monkeypatch.delenv("WRANGLES_AI_CONFIG", raising=False) + ai_config.clear_cache() + try: + result = gemini.GeminiURLContextClient(api_key="fake-key").retrieve( + "https://product.example/one", output_format="json", + ) + assert "temperature" not in google_transport["requests"][0]["config"] + assert result["error"] is None + assert result["extracted_content"] == {"name": "Synthetic"} + assert google_transport["closed"] == [True] + finally: + ai_config.clear_cache() + + +@pytest.mark.parametrize("content,finish_message", [ + (None, "Model generated function call(s)."), + ({}, "Model generated function call(s)."), + ({"parts": []}, "Model generated function call(s)."), + ({}, None), +], ids=["missing", "empty-object", "empty-parts", "no-finish-message"]) +@pytest.mark.parametrize("url_retrieved", [False, True], ids=["no-url-metadata", "url-retrieved"]) +@pytest.mark.filterwarnings("ignore:TOO_MANY_TOOL_CALLS is not a valid FinishReason:UserWarning") +def test_google_empty_content_preserves_provider_failure( + configured_ai, google_transport, monkeypatch, content, finish_message, url_retrieved, +): + from google.genai import types + + candidate = { + "finishReason": "TOO_MANY_TOOL_CALLS", + } + if finish_message is not None: + candidate["finishMessage"] = finish_message + if content is not None: + candidate["content"] = content + if url_retrieved: + candidate["urlContextMetadata"] = {"urlMetadata": [{ + "retrievedUrl": "https://product.example/one", + "urlRetrievalStatus": "URL_RETRIEVAL_STATUS_SUCCESS", + }]} + google_transport["response"] = types.GenerateContentResponse.model_validate({ + "candidates": [candidate], + }) + genai, _, errors = gemini._get_genai() + monkeypatch.setattr(gemini, "_get_genai", lambda: (genai, types, errors)) + + result = gemini.GeminiURLContextClient(api_key="fake-key").retrieve( + "https://product.example/one", output_format="json", + ) + + assert result["status"] == "Failure" + assert "TOO_MANY_TOOL_CALLS" in result["error"] + if finish_message is not None: + assert finish_message in result["error"] + assert result["extracted_content"] is None + assert len(google_transport["requests"]) == 1 + assert google_transport["closed"] == [True] + + def test_google_direct_client_reads_changed_config_each_call(configured_ai, google_transport): config, save = configured_ai client = gemini.GeminiURLContextClient(api_key="fake-key") diff --git a/wrangles/ai_defaults.yml b/wrangles/ai_defaults.yml index 042ee6b5..8b9a40a0 100644 --- a/wrangles/ai_defaults.yml +++ b/wrangles/ai_defaults.yml @@ -86,8 +86,7 @@ providers: status: active applications: [url_retrieval] default_for: [search.retrieve_link_content] - defaults: - temperature: 0.1 + defaults: {} jina: endpoints: embeddings: https://api.jina.ai/v1/embeddings diff --git a/wrangles/clients/gemini.py b/wrangles/clients/gemini.py index 7453e0bb..eb6c78d9 100644 --- a/wrangles/clients/gemini.py +++ b/wrangles/clients/gemini.py @@ -147,11 +147,12 @@ def _retrieve(self, url, prompt, output_format, policy): # Process Standard Text Response if not response.candidates: result["error"] = "API returned zero candidates." - elif not response.candidates[0].content: - reason = response.candidates[0].finish_reason + elif not cand.content or not cand.content.parts: + reason = cand.finish_reason result["error"] = f"Blocked/Empty. Finish Reason: {reason}" - if result["status"] != "Success": - result["status"] = "Failure" + if cand.finish_message: + result["error"] += f". {cand.finish_message}" + result["status"] = "Failure" else: full_text = "\n".join([part.text for part in response.candidates[0].content.parts])