Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/ai_configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
61 changes: 61 additions & 0 deletions tests/test_ai_caller_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"}')]),
Expand Down Expand Up @@ -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")
Expand Down
3 changes: 1 addition & 2 deletions wrangles/ai_defaults.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 5 additions & 4 deletions wrangles/clients/gemini.py
Original file line number Diff line number Diff line change
Expand Up @@ -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])

Expand Down
Loading