evidor 1.10.5.dev1__tar.gz → 1.10.6.dev1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {evidor-1.10.5.dev1 → evidor-1.10.6.dev1}/CHANGELOG.md +14 -0
- {evidor-1.10.5.dev1 → evidor-1.10.6.dev1}/PKG-INFO +1 -1
- {evidor-1.10.5.dev1 → evidor-1.10.6.dev1}/pyproject.toml +1 -1
- {evidor-1.10.5.dev1 → evidor-1.10.6.dev1}/src/evidor/agent/providers/gemini/__init__.py +1 -2
- {evidor-1.10.5.dev1 → evidor-1.10.6.dev1}/src/evidor/agent/providers/gemini/builder.py +95 -74
- {evidor-1.10.5.dev1 → evidor-1.10.6.dev1}/src/evidor/utils/schema.py +29 -1
- {evidor-1.10.5.dev1 → evidor-1.10.6.dev1}/tests/test_providers.py +62 -4
- {evidor-1.10.5.dev1 → evidor-1.10.6.dev1}/tests/test_tools.py +24 -0
- {evidor-1.10.5.dev1 → evidor-1.10.6.dev1}/.github/workflows/release.yml +0 -0
- {evidor-1.10.5.dev1 → evidor-1.10.6.dev1}/.gitignore +0 -0
- {evidor-1.10.5.dev1 → evidor-1.10.6.dev1}/AGENTS.md +0 -0
- {evidor-1.10.5.dev1 → evidor-1.10.6.dev1}/CODE_OF_CONDUCT.md +0 -0
- {evidor-1.10.5.dev1 → evidor-1.10.6.dev1}/LICENSE +0 -0
- {evidor-1.10.5.dev1 → evidor-1.10.6.dev1}/README.md +0 -0
- {evidor-1.10.5.dev1 → evidor-1.10.6.dev1}/requirements.txt +0 -0
- {evidor-1.10.5.dev1 → evidor-1.10.6.dev1}/src/evidor/__init__.py +0 -0
- {evidor-1.10.5.dev1 → evidor-1.10.6.dev1}/src/evidor/agent/__init__.py +0 -0
- {evidor-1.10.5.dev1 → evidor-1.10.6.dev1}/src/evidor/agent/core/__init__.py +0 -0
- {evidor-1.10.5.dev1 → evidor-1.10.6.dev1}/src/evidor/agent/core/agent.py +0 -0
- {evidor-1.10.5.dev1 → evidor-1.10.6.dev1}/src/evidor/agent/core/context.py +0 -0
- {evidor-1.10.5.dev1 → evidor-1.10.6.dev1}/src/evidor/agent/core/runner.py +0 -0
- {evidor-1.10.5.dev1 → evidor-1.10.6.dev1}/src/evidor/agent/core/telemetry_bridge.py +0 -0
- {evidor-1.10.5.dev1 → evidor-1.10.6.dev1}/src/evidor/agent/providers/__init__.py +0 -0
- {evidor-1.10.5.dev1 → evidor-1.10.6.dev1}/src/evidor/agent/providers/anthropic/__init__.py +0 -0
- {evidor-1.10.5.dev1 → evidor-1.10.6.dev1}/src/evidor/agent/providers/anthropic/builder.py +0 -0
- {evidor-1.10.5.dev1 → evidor-1.10.6.dev1}/src/evidor/agent/providers/anthropic/parser.py +0 -0
- {evidor-1.10.5.dev1 → evidor-1.10.6.dev1}/src/evidor/agent/providers/anthropic/provider.py +0 -0
- {evidor-1.10.5.dev1 → evidor-1.10.6.dev1}/src/evidor/agent/providers/anthropic/stream.py +0 -0
- {evidor-1.10.5.dev1 → evidor-1.10.6.dev1}/src/evidor/agent/providers/base.py +0 -0
- {evidor-1.10.5.dev1 → evidor-1.10.6.dev1}/src/evidor/agent/providers/client_pool.py +0 -0
- {evidor-1.10.5.dev1 → evidor-1.10.6.dev1}/src/evidor/agent/providers/gemini/parser.py +0 -0
- {evidor-1.10.5.dev1 → evidor-1.10.6.dev1}/src/evidor/agent/providers/gemini/provider.py +0 -0
- {evidor-1.10.5.dev1 → evidor-1.10.6.dev1}/src/evidor/agent/providers/gemini/stream.py +0 -0
- {evidor-1.10.5.dev1 → evidor-1.10.6.dev1}/src/evidor/agent/providers/openai/__init__.py +0 -0
- {evidor-1.10.5.dev1 → evidor-1.10.6.dev1}/src/evidor/agent/providers/openai/builder.py +0 -0
- {evidor-1.10.5.dev1 → evidor-1.10.6.dev1}/src/evidor/agent/providers/openai/parser.py +0 -0
- {evidor-1.10.5.dev1 → evidor-1.10.6.dev1}/src/evidor/agent/providers/openai/provider.py +0 -0
- {evidor-1.10.5.dev1 → evidor-1.10.6.dev1}/src/evidor/agent/providers/openai/stream.py +0 -0
- {evidor-1.10.5.dev1 → evidor-1.10.6.dev1}/src/evidor/constants.py +0 -0
- {evidor-1.10.5.dev1 → evidor-1.10.6.dev1}/src/evidor/errors.py +0 -0
- {evidor-1.10.5.dev1 → evidor-1.10.6.dev1}/src/evidor/mcp/__init__.py +0 -0
- {evidor-1.10.5.dev1 → evidor-1.10.6.dev1}/src/evidor/mcp/client.py +0 -0
- {evidor-1.10.5.dev1 → evidor-1.10.6.dev1}/src/evidor/mcp/config.py +0 -0
- {evidor-1.10.5.dev1 → evidor-1.10.6.dev1}/src/evidor/mcp/runner.py +0 -0
- {evidor-1.10.5.dev1 → evidor-1.10.6.dev1}/src/evidor/mcp/session.py +0 -0
- {evidor-1.10.5.dev1 → evidor-1.10.6.dev1}/src/evidor/models/__init__.py +0 -0
- {evidor-1.10.5.dev1 → evidor-1.10.6.dev1}/src/evidor/models/generation.py +0 -0
- {evidor-1.10.5.dev1 → evidor-1.10.6.dev1}/src/evidor/models/streaming.py +0 -0
- {evidor-1.10.5.dev1 → evidor-1.10.6.dev1}/src/evidor/models/thinking.py +0 -0
- {evidor-1.10.5.dev1 → evidor-1.10.6.dev1}/src/evidor/telemetry/__init__.py +0 -0
- {evidor-1.10.5.dev1 → evidor-1.10.6.dev1}/src/evidor/telemetry/adapters/__init__.py +0 -0
- {evidor-1.10.5.dev1 → evidor-1.10.6.dev1}/src/evidor/telemetry/adapters/langfuse.py +0 -0
- {evidor-1.10.5.dev1 → evidor-1.10.6.dev1}/src/evidor/telemetry/adapters/opentelemetry.py +0 -0
- {evidor-1.10.5.dev1 → evidor-1.10.6.dev1}/src/evidor/telemetry/adapters/phoenix.py +0 -0
- {evidor-1.10.5.dev1 → evidor-1.10.6.dev1}/src/evidor/telemetry/adapters/prometheus.py +0 -0
- {evidor-1.10.5.dev1 → evidor-1.10.6.dev1}/src/evidor/telemetry/context.py +0 -0
- {evidor-1.10.5.dev1 → evidor-1.10.6.dev1}/src/evidor/telemetry/events.py +0 -0
- {evidor-1.10.5.dev1 → evidor-1.10.6.dev1}/src/evidor/telemetry/runtime.py +0 -0
- {evidor-1.10.5.dev1 → evidor-1.10.6.dev1}/src/evidor/telemetry/semconv.py +0 -0
- {evidor-1.10.5.dev1 → evidor-1.10.6.dev1}/src/evidor/telemetry/sink.py +0 -0
- {evidor-1.10.5.dev1 → evidor-1.10.6.dev1}/src/evidor/tools/__init__.py +0 -0
- {evidor-1.10.5.dev1 → evidor-1.10.6.dev1}/src/evidor/tools/core/__init__.py +0 -0
- {evidor-1.10.5.dev1 → evidor-1.10.6.dev1}/src/evidor/tools/core/tool_executor.py +0 -0
- {evidor-1.10.5.dev1 → evidor-1.10.6.dev1}/src/evidor/tools/core/tools.py +0 -0
- {evidor-1.10.5.dev1 → evidor-1.10.6.dev1}/src/evidor/tools/core/utils.py +0 -0
- {evidor-1.10.5.dev1 → evidor-1.10.6.dev1}/src/evidor/tools/native/__init__.py +0 -0
- {evidor-1.10.5.dev1 → evidor-1.10.6.dev1}/src/evidor/tools/native/calculator.py +0 -0
- {evidor-1.10.5.dev1 → evidor-1.10.6.dev1}/src/evidor/tools/native/evidence_store.py +0 -0
- {evidor-1.10.5.dev1 → evidor-1.10.6.dev1}/src/evidor/tools/native/filesystem/__init__.py +0 -0
- {evidor-1.10.5.dev1 → evidor-1.10.6.dev1}/src/evidor/tools/native/filesystem/_common.py +0 -0
- {evidor-1.10.5.dev1 → evidor-1.10.6.dev1}/src/evidor/tools/native/filesystem/list_files.py +0 -0
- {evidor-1.10.5.dev1 → evidor-1.10.6.dev1}/src/evidor/tools/native/filesystem/mutate_files.py +0 -0
- {evidor-1.10.5.dev1 → evidor-1.10.6.dev1}/src/evidor/tools/native/filesystem/read_file.py +0 -0
- {evidor-1.10.5.dev1 → evidor-1.10.6.dev1}/src/evidor/tools/native/filesystem/search_files.py +0 -0
- {evidor-1.10.5.dev1 → evidor-1.10.6.dev1}/src/evidor/tools/native/get_current_time.py +0 -0
- {evidor-1.10.5.dev1 → evidor-1.10.6.dev1}/src/evidor/tools/native/read_webpage.py +0 -0
- {evidor-1.10.5.dev1 → evidor-1.10.6.dev1}/src/evidor/tools/native/web_search/__init__.py +0 -0
- {evidor-1.10.5.dev1 → evidor-1.10.6.dev1}/src/evidor/tools/native/web_search/_base.py +0 -0
- {evidor-1.10.5.dev1 → evidor-1.10.6.dev1}/src/evidor/tools/native/web_search/http.py +0 -0
- {evidor-1.10.5.dev1 → evidor-1.10.6.dev1}/src/evidor/tools/native/web_search/providers/__init__.py +0 -0
- {evidor-1.10.5.dev1 → evidor-1.10.6.dev1}/src/evidor/tools/native/web_search/providers/brave.py +0 -0
- {evidor-1.10.5.dev1 → evidor-1.10.6.dev1}/src/evidor/tools/native/web_search/providers/exa.py +0 -0
- {evidor-1.10.5.dev1 → evidor-1.10.6.dev1}/src/evidor/tools/native/web_search/providers/tavily.py +0 -0
- {evidor-1.10.5.dev1 → evidor-1.10.6.dev1}/src/evidor/tools/native/web_search/tool.py +0 -0
- {evidor-1.10.5.dev1 → evidor-1.10.6.dev1}/src/evidor/utils/__init__.py +0 -0
- {evidor-1.10.5.dev1 → evidor-1.10.6.dev1}/src/evidor/utils/execution.py +0 -0
- {evidor-1.10.5.dev1 → evidor-1.10.6.dev1}/tests/test_agent.py +0 -0
- {evidor-1.10.5.dev1 → evidor-1.10.6.dev1}/tests/test_builtin_tools.py +0 -0
- {evidor-1.10.5.dev1 → evidor-1.10.6.dev1}/tests/test_conversation.py +0 -0
- {evidor-1.10.5.dev1 → evidor-1.10.6.dev1}/tests/test_filesystem_tools.py +0 -0
- {evidor-1.10.5.dev1 → evidor-1.10.6.dev1}/tests/test_langfuse_adapter.py +0 -0
- {evidor-1.10.5.dev1 → evidor-1.10.6.dev1}/tests/test_mcp.py +0 -0
- {evidor-1.10.5.dev1 → evidor-1.10.6.dev1}/tests/test_models.py +0 -0
- {evidor-1.10.5.dev1 → evidor-1.10.6.dev1}/tests/test_native_tools.py +0 -0
- {evidor-1.10.5.dev1 → evidor-1.10.6.dev1}/tests/test_opentelemetry_adapter.py +0 -0
- {evidor-1.10.5.dev1 → evidor-1.10.6.dev1}/tests/test_phoenix_adapter.py +0 -0
- {evidor-1.10.5.dev1 → evidor-1.10.6.dev1}/tests/test_prometheus_adapter.py +0 -0
- {evidor-1.10.5.dev1 → evidor-1.10.6.dev1}/tests/test_streaming.py +0 -0
- {evidor-1.10.5.dev1 → evidor-1.10.6.dev1}/tests/test_telemetry_events.py +0 -0
- {evidor-1.10.5.dev1 → evidor-1.10.6.dev1}/tests/test_telemetry_integration.py +0 -0
- {evidor-1.10.5.dev1 → evidor-1.10.6.dev1}/tests/test_thinking.py +0 -0
- {evidor-1.10.5.dev1 → evidor-1.10.6.dev1}/tests/test_web_search.py +0 -0
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
<!-- version list -->
|
|
4
4
|
|
|
5
|
+
## v1.10.6-dev.1 (2026-10-30)
|
|
6
|
+
|
|
7
|
+
### Bug Fixes
|
|
8
|
+
|
|
9
|
+
- **gemini**: Fixing tool schema sanitization issues
|
|
10
|
+
([`4fb1034`](https://github.com/vedangiitb/evidor-core/commit/4fb103470787d203cc7542c68283f28a724c5041))
|
|
11
|
+
|
|
12
|
+
- **gemini**: Fixing tool schema sanitization issues
|
|
13
|
+
([`186bdd3`](https://github.com/vedangiitb/evidor-core/commit/186bdd394b08f003953dc362bd020a99004346f9))
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
## v1.10.5 (2026-10-29)
|
|
17
|
+
|
|
18
|
+
|
|
5
19
|
## v1.10.5-dev.1 (2026-10-29)
|
|
6
20
|
|
|
7
21
|
### Bug Fixes
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: evidor
|
|
3
|
-
Version: 1.10.
|
|
3
|
+
Version: 1.10.6-dev.1
|
|
4
4
|
Summary: A small, provider-agnostic harness for LLM input and output.
|
|
5
5
|
Project-URL: Homepage, https://github.com/evidor/evidor-core
|
|
6
6
|
Project-URL: Repository, https://github.com/evidor/evidor-core
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Google Gemini provider package."""
|
|
2
2
|
|
|
3
|
-
from .builder import GeminiPayloadBuilder
|
|
3
|
+
from .builder import GeminiPayloadBuilder
|
|
4
4
|
from .parser import GeminiResponseParser
|
|
5
5
|
from .provider import GeminiProvider
|
|
6
6
|
from .stream import GeminiStreamHandler
|
|
@@ -10,6 +10,5 @@ __all__ = [
|
|
|
10
10
|
"GeminiProvider",
|
|
11
11
|
"GeminiResponseParser",
|
|
12
12
|
"GeminiStreamHandler",
|
|
13
|
-
"sanitize_gemini_schema",
|
|
14
13
|
]
|
|
15
14
|
|
|
@@ -6,6 +6,17 @@ from typing import Any
|
|
|
6
6
|
from evidor.models import GenerationRequest, Message
|
|
7
7
|
from ..base import extract_system_prompt, sort_tools
|
|
8
8
|
|
|
9
|
+
_SNAKE_TO_CAMEL: dict[str, str] = {
|
|
10
|
+
"any_of": "anyOf",
|
|
11
|
+
"max_length": "maxLength",
|
|
12
|
+
"min_length": "minLength",
|
|
13
|
+
"max_items": "maxItems",
|
|
14
|
+
"min_items": "minItems",
|
|
15
|
+
"max_properties": "maxProperties",
|
|
16
|
+
"min_properties": "minProperties",
|
|
17
|
+
"property_ordering": "propertyOrdering",
|
|
18
|
+
}
|
|
19
|
+
|
|
9
20
|
_ALLOWED_GEMINI_SCHEMA_KEYS = {
|
|
10
21
|
"type",
|
|
11
22
|
"format",
|
|
@@ -17,94 +28,29 @@ _ALLOWED_GEMINI_SCHEMA_KEYS = {
|
|
|
17
28
|
"properties",
|
|
18
29
|
"required",
|
|
19
30
|
"items",
|
|
20
|
-
"any_of",
|
|
21
31
|
"anyOf",
|
|
32
|
+
"any_of",
|
|
22
33
|
"maximum",
|
|
23
34
|
"minimum",
|
|
24
|
-
"max_length",
|
|
25
35
|
"maxLength",
|
|
26
|
-
"
|
|
36
|
+
"max_length",
|
|
27
37
|
"minLength",
|
|
38
|
+
"min_length",
|
|
28
39
|
"pattern",
|
|
29
|
-
"max_items",
|
|
30
40
|
"maxItems",
|
|
31
|
-
"
|
|
41
|
+
"max_items",
|
|
32
42
|
"minItems",
|
|
33
|
-
"
|
|
43
|
+
"min_items",
|
|
34
44
|
"maxProperties",
|
|
35
|
-
"
|
|
45
|
+
"max_properties",
|
|
36
46
|
"minProperties",
|
|
47
|
+
"min_properties",
|
|
37
48
|
"example",
|
|
38
|
-
"property_ordering",
|
|
39
49
|
"propertyOrdering",
|
|
50
|
+
"property_ordering",
|
|
40
51
|
}
|
|
41
52
|
|
|
42
53
|
|
|
43
|
-
def sanitize_gemini_schema(schema: Any, types_mod: Any = None) -> Any:
|
|
44
|
-
"""Sanitize a JSON schema dictionary for Google GenAI types.Schema."""
|
|
45
|
-
if not isinstance(schema, dict):
|
|
46
|
-
if isinstance(schema, list):
|
|
47
|
-
return [sanitize_gemini_schema(item, types_mod) for item in schema]
|
|
48
|
-
return schema
|
|
49
|
-
|
|
50
|
-
cleaned: dict[str, Any] = {}
|
|
51
|
-
|
|
52
|
-
# Handle 'type' if given as a list (union types, e.g. ['string', 'number', 'boolean'])
|
|
53
|
-
raw_type = schema.get("type")
|
|
54
|
-
if isinstance(raw_type, list):
|
|
55
|
-
types_list = [t for t in raw_type if t != "null"]
|
|
56
|
-
has_null = "null" in raw_type
|
|
57
|
-
if len(types_list) == 1:
|
|
58
|
-
cleaned["type"] = types_list[0]
|
|
59
|
-
if has_null:
|
|
60
|
-
cleaned["nullable"] = True
|
|
61
|
-
elif len(types_list) > 1:
|
|
62
|
-
subschemas = [{"type": t} for t in types_list]
|
|
63
|
-
if has_null:
|
|
64
|
-
cleaned["nullable"] = True
|
|
65
|
-
cleaned["anyOf"] = subschemas
|
|
66
|
-
elif has_null:
|
|
67
|
-
cleaned["type"] = "NULL"
|
|
68
|
-
elif raw_type is not None:
|
|
69
|
-
cleaned["type"] = raw_type
|
|
70
|
-
|
|
71
|
-
# Handle const -> enum
|
|
72
|
-
if "const" in schema and "enum" not in schema:
|
|
73
|
-
cleaned["enum"] = [schema["const"]]
|
|
74
|
-
|
|
75
|
-
for k, v in schema.items():
|
|
76
|
-
if k == "type":
|
|
77
|
-
continue
|
|
78
|
-
if k in ("oneOf", "one_of") and "anyOf" not in cleaned and "any_of" not in cleaned:
|
|
79
|
-
cleaned["anyOf"] = (
|
|
80
|
-
[sanitize_gemini_schema(it, types_mod) for it in v]
|
|
81
|
-
if isinstance(v, list)
|
|
82
|
-
else sanitize_gemini_schema(v, types_mod)
|
|
83
|
-
)
|
|
84
|
-
continue
|
|
85
|
-
if k == "examples" and "example" not in schema and isinstance(v, list) and v:
|
|
86
|
-
cleaned["example"] = v[0]
|
|
87
|
-
continue
|
|
88
|
-
|
|
89
|
-
if k == "properties" and isinstance(v, dict):
|
|
90
|
-
cleaned["properties"] = {
|
|
91
|
-
prop_name: sanitize_gemini_schema(prop_s, types_mod)
|
|
92
|
-
for prop_name, prop_s in v.items()
|
|
93
|
-
}
|
|
94
|
-
elif k == "items":
|
|
95
|
-
if isinstance(v, list):
|
|
96
|
-
if len(v) == 1:
|
|
97
|
-
cleaned["items"] = sanitize_gemini_schema(v[0], types_mod)
|
|
98
|
-
elif len(v) > 1:
|
|
99
|
-
cleaned["items"] = {"anyOf": [sanitize_gemini_schema(it, types_mod) for it in v]}
|
|
100
|
-
else:
|
|
101
|
-
cleaned["items"] = sanitize_gemini_schema(v, types_mod)
|
|
102
|
-
elif k in _ALLOWED_GEMINI_SCHEMA_KEYS:
|
|
103
|
-
cleaned[k] = sanitize_gemini_schema(v, types_mod)
|
|
104
|
-
|
|
105
|
-
return cleaned
|
|
106
|
-
|
|
107
|
-
|
|
108
54
|
class GeminiPayloadBuilder:
|
|
109
55
|
"""Builds and formats API request payloads for Google Gemini Interactions API."""
|
|
110
56
|
|
|
@@ -118,6 +64,80 @@ class GeminiPayloadBuilder:
|
|
|
118
64
|
self.cached_content = cached_content
|
|
119
65
|
self.thinking = thinking
|
|
120
66
|
|
|
67
|
+
def _sanitize_schema(self, schema: Any) -> Any:
|
|
68
|
+
"""Sanitize a JSON schema dictionary for Google GenAI types.Schema."""
|
|
69
|
+
if not isinstance(schema, dict):
|
|
70
|
+
if isinstance(schema, list):
|
|
71
|
+
return [self._sanitize_schema(item) for item in schema]
|
|
72
|
+
return schema
|
|
73
|
+
|
|
74
|
+
cleaned: dict[str, Any] = {}
|
|
75
|
+
|
|
76
|
+
# Handle 'type' if given as a list (union types, e.g. ['string', 'number', 'boolean'])
|
|
77
|
+
raw_type = schema.get("type")
|
|
78
|
+
if isinstance(raw_type, list):
|
|
79
|
+
types_list = [t for t in raw_type if t != "null"]
|
|
80
|
+
has_null = "null" in raw_type
|
|
81
|
+
if len(types_list) == 1:
|
|
82
|
+
cleaned["type"] = types_list[0]
|
|
83
|
+
if has_null:
|
|
84
|
+
cleaned["nullable"] = True
|
|
85
|
+
elif len(types_list) > 1:
|
|
86
|
+
subschemas = [{"type": t} for t in types_list]
|
|
87
|
+
if has_null:
|
|
88
|
+
cleaned["nullable"] = True
|
|
89
|
+
cleaned["anyOf"] = subschemas
|
|
90
|
+
elif has_null:
|
|
91
|
+
cleaned["type"] = "NULL"
|
|
92
|
+
elif raw_type is not None:
|
|
93
|
+
cleaned["type"] = raw_type
|
|
94
|
+
|
|
95
|
+
# Handle const -> enum
|
|
96
|
+
if "const" in schema and "enum" not in schema:
|
|
97
|
+
cleaned["enum"] = [schema["const"]]
|
|
98
|
+
|
|
99
|
+
for k, v in schema.items():
|
|
100
|
+
if k == "type":
|
|
101
|
+
continue
|
|
102
|
+
if k in ("oneOf", "one_of") and "anyOf" not in cleaned:
|
|
103
|
+
cleaned["anyOf"] = (
|
|
104
|
+
[self._sanitize_schema(it) for it in v]
|
|
105
|
+
if isinstance(v, list)
|
|
106
|
+
else self._sanitize_schema(v)
|
|
107
|
+
)
|
|
108
|
+
continue
|
|
109
|
+
if k == "examples" and "example" not in schema and isinstance(v, list) and v:
|
|
110
|
+
cleaned["example"] = v[0]
|
|
111
|
+
continue
|
|
112
|
+
|
|
113
|
+
target_k = _SNAKE_TO_CAMEL.get(k, k)
|
|
114
|
+
|
|
115
|
+
if k == "properties" and isinstance(v, dict):
|
|
116
|
+
cleaned["properties"] = {
|
|
117
|
+
prop_name: self._sanitize_schema(prop_s)
|
|
118
|
+
for prop_name, prop_s in v.items()
|
|
119
|
+
}
|
|
120
|
+
elif k == "items":
|
|
121
|
+
if isinstance(v, list):
|
|
122
|
+
if len(v) == 1:
|
|
123
|
+
cleaned["items"] = self._sanitize_schema(v[0])
|
|
124
|
+
elif len(v) > 1:
|
|
125
|
+
cleaned["items"] = {"anyOf": [self._sanitize_schema(it) for it in v]}
|
|
126
|
+
else:
|
|
127
|
+
cleaned["items"] = self._sanitize_schema(v)
|
|
128
|
+
elif k in _ALLOWED_GEMINI_SCHEMA_KEYS:
|
|
129
|
+
cleaned[target_k] = self._sanitize_schema(v)
|
|
130
|
+
|
|
131
|
+
# Ensure ARRAY types have an items schema (required by Gemini API)
|
|
132
|
+
if cleaned.get("type") in ("array", "ARRAY") and "items" not in cleaned:
|
|
133
|
+
cleaned["items"] = {"type": "string"}
|
|
134
|
+
|
|
135
|
+
# Omit empty required lists (Gemini schema validation rejects empty required arrays)
|
|
136
|
+
if "required" in cleaned and not cleaned["required"]:
|
|
137
|
+
del cleaned["required"]
|
|
138
|
+
|
|
139
|
+
return cleaned
|
|
140
|
+
|
|
121
141
|
def build(self, request: GenerationRequest, types: Any = None) -> dict[str, Any]:
|
|
122
142
|
"""Construct the full request dictionary for Gemini Interactions API."""
|
|
123
143
|
system_prompt = extract_system_prompt(request)
|
|
@@ -142,7 +162,7 @@ class GeminiPayloadBuilder:
|
|
|
142
162
|
"type": "function",
|
|
143
163
|
"name": t.name,
|
|
144
164
|
"description": t.description,
|
|
145
|
-
"parameters":
|
|
165
|
+
"parameters": self._sanitize_schema(t.parameters) if t.parameters else None,
|
|
146
166
|
}
|
|
147
167
|
for t in sorted_tools
|
|
148
168
|
]
|
|
@@ -241,3 +261,4 @@ class GeminiPayloadBuilder:
|
|
|
241
261
|
})
|
|
242
262
|
return input_steps
|
|
243
263
|
|
|
264
|
+
|
|
@@ -70,6 +70,19 @@ def python_type_to_json_type(annotation: Any) -> str:
|
|
|
70
70
|
return TYPE_MAPPING.get(annotation, "string")
|
|
71
71
|
|
|
72
72
|
|
|
73
|
+
def _unwrap_optional(annotation: Any) -> tuple[Any, bool]:
|
|
74
|
+
"""Unwrap Optional / Union[..., None] and return (inner_type, is_nullable)."""
|
|
75
|
+
origin = get_origin(annotation)
|
|
76
|
+
if origin in (Union, types.UnionType):
|
|
77
|
+
args = get_args(annotation)
|
|
78
|
+
is_nullable = type(None) in args
|
|
79
|
+
non_none_args = [arg for arg in args if arg is not type(None)]
|
|
80
|
+
if len(non_none_args) == 1:
|
|
81
|
+
return non_none_args[0], is_nullable
|
|
82
|
+
return annotation, is_nullable
|
|
83
|
+
return annotation, False
|
|
84
|
+
|
|
85
|
+
|
|
73
86
|
def generate_parameters_schema(func: Callable[..., Any], param_docs: dict[str, str]) -> dict[str, Any]:
|
|
74
87
|
"""Generate a JSON Schema object describing the function parameters."""
|
|
75
88
|
sig = inspect.signature(func)
|
|
@@ -86,7 +99,22 @@ def generate_parameters_schema(func: Callable[..., Any], param_docs: dict[str, s
|
|
|
86
99
|
inspect.Parameter.VAR_KEYWORD,
|
|
87
100
|
):
|
|
88
101
|
continue
|
|
89
|
-
|
|
102
|
+
annotation = type_hints.get(name, param.annotation)
|
|
103
|
+
unwrapped, is_nullable = _unwrap_optional(annotation)
|
|
104
|
+
json_type = python_type_to_json_type(unwrapped)
|
|
105
|
+
prop_schema: dict[str, Any] = {"type": json_type}
|
|
106
|
+
if is_nullable:
|
|
107
|
+
prop_schema["nullable"] = True
|
|
108
|
+
|
|
109
|
+
if json_type == "array":
|
|
110
|
+
item_type = "string"
|
|
111
|
+
item_origin = get_origin(unwrapped)
|
|
112
|
+
if item_origin is not None:
|
|
113
|
+
args = get_args(unwrapped)
|
|
114
|
+
if args and args[0] is not Ellipsis:
|
|
115
|
+
item_type = python_type_to_json_type(args[0])
|
|
116
|
+
prop_schema["items"] = {"type": item_type}
|
|
117
|
+
|
|
90
118
|
if name in param_docs:
|
|
91
119
|
prop_schema["description"] = param_docs[name]
|
|
92
120
|
properties[name] = prop_schema
|
|
@@ -1449,30 +1449,88 @@ def test_gemini_payload_builder_thinking_config() -> None:
|
|
|
1449
1449
|
|
|
1450
1450
|
|
|
1451
1451
|
def test_gemini_payload_builder_types_optional() -> None:
|
|
1452
|
-
from evidor.agent.providers.gemini.builder import GeminiPayloadBuilder
|
|
1452
|
+
from evidor.agent.providers.gemini.builder import GeminiPayloadBuilder
|
|
1453
1453
|
from evidor.models import GenerationRequest
|
|
1454
1454
|
from evidor.tools import Tool
|
|
1455
1455
|
|
|
1456
|
-
|
|
1456
|
+
builder = GeminiPayloadBuilder(model="gemini-3.8-flash")
|
|
1457
|
+
|
|
1458
|
+
# _sanitize_schema works as an internal instance method
|
|
1457
1459
|
schema = {
|
|
1458
1460
|
"type": "object",
|
|
1459
1461
|
"properties": {"name": {"type": "string"}},
|
|
1460
1462
|
"required": ["name"],
|
|
1461
1463
|
}
|
|
1462
|
-
sanitized =
|
|
1464
|
+
sanitized = builder._sanitize_schema(schema)
|
|
1463
1465
|
assert sanitized["type"] == "object"
|
|
1464
1466
|
assert "name" in sanitized["properties"]
|
|
1465
1467
|
|
|
1466
1468
|
# builder.build works without types argument
|
|
1467
1469
|
tool = Tool(name="test_tool", description="A test tool", parameters=schema, func=lambda: None)
|
|
1468
1470
|
req = GenerationRequest(prompt="Use tool", tools=[tool])
|
|
1469
|
-
builder = GeminiPayloadBuilder(model="gemini-3.8-flash")
|
|
1470
1471
|
payload = builder.build(req)
|
|
1471
1472
|
assert "cached_content" not in payload
|
|
1472
1473
|
assert len(payload["tools"]) == 1
|
|
1473
1474
|
assert payload["tools"][0]["name"] == "test_tool"
|
|
1474
1475
|
|
|
1475
1476
|
|
|
1477
|
+
def test_gemini_schema_sanitizer_enhancements() -> None:
|
|
1478
|
+
from evidor.agent.providers.gemini.builder import GeminiPayloadBuilder
|
|
1479
|
+
|
|
1480
|
+
builder = GeminiPayloadBuilder(model="gemini-3.8-flash")
|
|
1481
|
+
|
|
1482
|
+
# 1. Bare array missing items gets default string items
|
|
1483
|
+
schema_array = {
|
|
1484
|
+
"type": "object",
|
|
1485
|
+
"properties": {
|
|
1486
|
+
"tags": {"type": "array"},
|
|
1487
|
+
},
|
|
1488
|
+
}
|
|
1489
|
+
sanitized_array = builder._sanitize_schema(schema_array)
|
|
1490
|
+
assert sanitized_array["properties"]["tags"]["items"] == {"type": "string"}
|
|
1491
|
+
|
|
1492
|
+
# 2. Empty required list is omitted
|
|
1493
|
+
schema_empty_req = {
|
|
1494
|
+
"type": "object",
|
|
1495
|
+
"properties": {"opt": {"type": "string"}},
|
|
1496
|
+
"required": [],
|
|
1497
|
+
}
|
|
1498
|
+
sanitized_empty_req = builder._sanitize_schema(schema_empty_req)
|
|
1499
|
+
assert "required" not in sanitized_empty_req
|
|
1500
|
+
|
|
1501
|
+
# 3. Non-empty required list is preserved
|
|
1502
|
+
schema_with_req = {
|
|
1503
|
+
"type": "object",
|
|
1504
|
+
"properties": {"req_field": {"type": "string"}},
|
|
1505
|
+
"required": ["req_field"],
|
|
1506
|
+
}
|
|
1507
|
+
sanitized_with_req = builder._sanitize_schema(schema_with_req)
|
|
1508
|
+
assert sanitized_with_req["required"] == ["req_field"]
|
|
1509
|
+
|
|
1510
|
+
# 4. Normalization of snake_case to camelCase
|
|
1511
|
+
schema_snake = {
|
|
1512
|
+
"type": "object",
|
|
1513
|
+
"properties": {
|
|
1514
|
+
"val": {
|
|
1515
|
+
"max_length": 50,
|
|
1516
|
+
"min_length": 5,
|
|
1517
|
+
"any_of": [{"type": "string"}, {"type": "number"}],
|
|
1518
|
+
}
|
|
1519
|
+
},
|
|
1520
|
+
"property_ordering": ["val"],
|
|
1521
|
+
}
|
|
1522
|
+
sanitized_snake = builder._sanitize_schema(schema_snake)
|
|
1523
|
+
val_prop = sanitized_snake["properties"]["val"]
|
|
1524
|
+
assert "maxLength" in val_prop
|
|
1525
|
+
assert "max_length" not in val_prop
|
|
1526
|
+
assert "minLength" in val_prop
|
|
1527
|
+
assert "min_length" not in val_prop
|
|
1528
|
+
assert "anyOf" in val_prop
|
|
1529
|
+
assert "any_of" not in val_prop
|
|
1530
|
+
assert "propertyOrdering" in sanitized_snake
|
|
1531
|
+
assert "property_ordering" not in sanitized_snake
|
|
1532
|
+
|
|
1533
|
+
|
|
1476
1534
|
def test_gemini_parser_no_thought_leakage() -> None:
|
|
1477
1535
|
from evidor.agent.providers.gemini.parser import GeminiResponseParser
|
|
1478
1536
|
|
|
@@ -257,3 +257,27 @@ def test_async_tool_preserves_contextvars() -> None:
|
|
|
257
257
|
asyncio.run(_run())
|
|
258
258
|
# When executed synchronously outside any active loop, it runs in a new loop and preserves contextvars
|
|
259
259
|
assert get_token.execute() == "custom_token_123"
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def test_tool_nullable_and_array_items() -> None:
|
|
263
|
+
@tool
|
|
264
|
+
def search_records(
|
|
265
|
+
scores: list[int],
|
|
266
|
+
filter_name: Optional[str] = None,
|
|
267
|
+
tags: list = [], # type: ignore[assignment]
|
|
268
|
+
) -> str:
|
|
269
|
+
"""Search records."""
|
|
270
|
+
return "ok"
|
|
271
|
+
|
|
272
|
+
props = search_records.parameters["properties"]
|
|
273
|
+
# Generic list[int] has items: integer
|
|
274
|
+
assert props["scores"]["type"] == "array"
|
|
275
|
+
assert props["scores"]["items"] == {"type": "integer"}
|
|
276
|
+
|
|
277
|
+
# Optional[str] has nullable: True
|
|
278
|
+
assert props["filter_name"]["type"] == "string"
|
|
279
|
+
assert props["filter_name"]["nullable"] is True
|
|
280
|
+
|
|
281
|
+
# Bare list defaults items to string
|
|
282
|
+
assert props["tags"]["type"] == "array"
|
|
283
|
+
assert props["tags"]["items"] == {"type": "string"}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{evidor-1.10.5.dev1 → evidor-1.10.6.dev1}/src/evidor/tools/native/filesystem/mutate_files.py
RENAMED
|
File without changes
|
|
File without changes
|
{evidor-1.10.5.dev1 → evidor-1.10.6.dev1}/src/evidor/tools/native/filesystem/search_files.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{evidor-1.10.5.dev1 → evidor-1.10.6.dev1}/src/evidor/tools/native/web_search/providers/__init__.py
RENAMED
|
File without changes
|
{evidor-1.10.5.dev1 → evidor-1.10.6.dev1}/src/evidor/tools/native/web_search/providers/brave.py
RENAMED
|
File without changes
|
{evidor-1.10.5.dev1 → evidor-1.10.6.dev1}/src/evidor/tools/native/web_search/providers/exa.py
RENAMED
|
File without changes
|
{evidor-1.10.5.dev1 → evidor-1.10.6.dev1}/src/evidor/tools/native/web_search/providers/tavily.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|