optimizely-opal.opal-tools-sdk 0.1.18.dev0__tar.gz → 0.1.20.dev0__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.
- {optimizely_opal_opal_tools_sdk-0.1.18.dev0 → optimizely_opal_opal_tools_sdk-0.1.20.dev0}/PKG-INFO +1 -1
- {optimizely_opal_opal_tools_sdk-0.1.18.dev0 → optimizely_opal_opal_tools_sdk-0.1.20.dev0}/opal_tools_sdk/decorators.py +110 -2
- {optimizely_opal_opal_tools_sdk-0.1.18.dev0 → optimizely_opal_opal_tools_sdk-0.1.20.dev0}/opal_tools_sdk/models.py +6 -2
- {optimizely_opal_opal_tools_sdk-0.1.18.dev0 → optimizely_opal_opal_tools_sdk-0.1.20.dev0}/opal_tools_sdk/proteus.py +12 -39
- {optimizely_opal_opal_tools_sdk-0.1.18.dev0 → optimizely_opal_opal_tools_sdk-0.1.20.dev0}/optimizely_opal.opal_tools_sdk.egg-info/PKG-INFO +1 -1
- {optimizely_opal_opal_tools_sdk-0.1.18.dev0 → optimizely_opal_opal_tools_sdk-0.1.20.dev0}/optimizely_opal.opal_tools_sdk.egg-info/SOURCES.txt +1 -0
- {optimizely_opal_opal_tools_sdk-0.1.18.dev0 → optimizely_opal_opal_tools_sdk-0.1.20.dev0}/pyproject.toml +1 -1
- {optimizely_opal_opal_tools_sdk-0.1.18.dev0 → optimizely_opal_opal_tools_sdk-0.1.20.dev0}/setup.py +1 -1
- optimizely_opal_opal_tools_sdk-0.1.20.dev0/tests/test_nested_schema.py +235 -0
- {optimizely_opal_opal_tools_sdk-0.1.18.dev0 → optimizely_opal_opal_tools_sdk-0.1.20.dev0}/README.md +0 -0
- {optimizely_opal_opal_tools_sdk-0.1.18.dev0 → optimizely_opal_opal_tools_sdk-0.1.20.dev0}/opal_tools_sdk/__init__.py +0 -0
- {optimizely_opal_opal_tools_sdk-0.1.18.dev0 → optimizely_opal_opal_tools_sdk-0.1.20.dev0}/opal_tools_sdk/_registry.py +0 -0
- {optimizely_opal_opal_tools_sdk-0.1.18.dev0 → optimizely_opal_opal_tools_sdk-0.1.20.dev0}/opal_tools_sdk/auth.py +0 -0
- {optimizely_opal_opal_tools_sdk-0.1.18.dev0 → optimizely_opal_opal_tools_sdk-0.1.20.dev0}/opal_tools_sdk/logging.py +0 -0
- {optimizely_opal_opal_tools_sdk-0.1.18.dev0 → optimizely_opal_opal_tools_sdk-0.1.20.dev0}/opal_tools_sdk/service.py +0 -0
- {optimizely_opal_opal_tools_sdk-0.1.18.dev0 → optimizely_opal_opal_tools_sdk-0.1.20.dev0}/opal_tools_sdk/ui.py +0 -0
- {optimizely_opal_opal_tools_sdk-0.1.18.dev0 → optimizely_opal_opal_tools_sdk-0.1.20.dev0}/optimizely_opal.opal_tools_sdk.egg-info/dependency_links.txt +0 -0
- {optimizely_opal_opal_tools_sdk-0.1.18.dev0 → optimizely_opal_opal_tools_sdk-0.1.20.dev0}/optimizely_opal.opal_tools_sdk.egg-info/requires.txt +0 -0
- {optimizely_opal_opal_tools_sdk-0.1.18.dev0 → optimizely_opal_opal_tools_sdk-0.1.20.dev0}/optimizely_opal.opal_tools_sdk.egg-info/top_level.txt +0 -0
- {optimizely_opal_opal_tools_sdk-0.1.18.dev0 → optimizely_opal_opal_tools_sdk-0.1.20.dev0}/setup.cfg +0 -0
- {optimizely_opal_opal_tools_sdk-0.1.18.dev0 → optimizely_opal_opal_tools_sdk-0.1.20.dev0}/tests/test_integration.py +0 -0
- {optimizely_opal_opal_tools_sdk-0.1.18.dev0 → optimizely_opal_opal_tools_sdk-0.1.20.dev0}/tests/test_proteus.py +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import inspect
|
|
2
2
|
import re
|
|
3
3
|
import logging
|
|
4
|
-
from typing import Callable, Any, List, Dict, Type, get_origin, get_type_hints, Optional, Union
|
|
4
|
+
from typing import Callable, Any, List, Dict, Type, get_args, get_origin, get_type_hints, Optional, Union
|
|
5
5
|
from fastapi import APIRouter, Depends, Header, HTTPException
|
|
6
6
|
from pydantic import BaseModel
|
|
7
7
|
|
|
@@ -10,6 +10,81 @@ from .logging import get_logger
|
|
|
10
10
|
|
|
11
11
|
logger = get_logger(__name__)
|
|
12
12
|
|
|
13
|
+
|
|
14
|
+
def _resolve_json_schema(schema: dict, defs: dict, _seen: Optional[set] = None) -> dict:
|
|
15
|
+
"""Resolve $ref references and simplify Pydantic v2 JSON schema output.
|
|
16
|
+
|
|
17
|
+
- Resolves ``$ref`` pointers by looking up ``$defs``
|
|
18
|
+
- Simplifies ``anyOf: [{type: X}, {type: "null"}]`` to ``{type: X}`` (Optional encoding)
|
|
19
|
+
- Recursively resolves nested ``properties`` and ``items``
|
|
20
|
+
- Strips Pydantic metadata (``title``, ``$defs`` at root)
|
|
21
|
+
- Guards against circular ``$ref`` via a ``_seen`` set
|
|
22
|
+
"""
|
|
23
|
+
if _seen is None:
|
|
24
|
+
_seen = set()
|
|
25
|
+
|
|
26
|
+
# Resolve $ref
|
|
27
|
+
if "$ref" in schema:
|
|
28
|
+
ref_path = schema["$ref"] # e.g. "#/$defs/OutputFormat"
|
|
29
|
+
ref_name = ref_path.rsplit("/", 1)[-1]
|
|
30
|
+
if ref_name in _seen:
|
|
31
|
+
return {"type": "object"} # break circular ref
|
|
32
|
+
if ref_name in defs:
|
|
33
|
+
resolved = _resolve_json_schema(defs[ref_name], defs, _seen | {ref_name})
|
|
34
|
+
# Preserve sibling description from the outer schema
|
|
35
|
+
if "description" in schema and "description" not in resolved:
|
|
36
|
+
resolved["description"] = schema["description"]
|
|
37
|
+
return resolved
|
|
38
|
+
return {"type": "string"}
|
|
39
|
+
|
|
40
|
+
# Simplify anyOf (Pydantic Optional encoding)
|
|
41
|
+
if "anyOf" in schema:
|
|
42
|
+
non_null = [
|
|
43
|
+
s for s in schema["anyOf"]
|
|
44
|
+
if (s.get("type") != "null" and "$ref" not in s) or "$ref" in s
|
|
45
|
+
]
|
|
46
|
+
if len(non_null) == 1:
|
|
47
|
+
resolved = _resolve_json_schema(non_null[0], defs, _seen)
|
|
48
|
+
# Preserve description from the outer schema
|
|
49
|
+
if "description" in schema and "description" not in resolved:
|
|
50
|
+
resolved["description"] = schema["description"]
|
|
51
|
+
return resolved
|
|
52
|
+
# If multiple non-null types, fall back to string
|
|
53
|
+
result: dict = {"type": "string"}
|
|
54
|
+
if "description" in schema:
|
|
55
|
+
result["description"] = schema["description"]
|
|
56
|
+
return result
|
|
57
|
+
|
|
58
|
+
result = {}
|
|
59
|
+
|
|
60
|
+
# Copy type
|
|
61
|
+
if "type" in schema:
|
|
62
|
+
result["type"] = schema["type"]
|
|
63
|
+
|
|
64
|
+
# Copy description
|
|
65
|
+
if "description" in schema:
|
|
66
|
+
result["description"] = schema["description"]
|
|
67
|
+
|
|
68
|
+
# Copy enum
|
|
69
|
+
if "enum" in schema:
|
|
70
|
+
result["enum"] = schema["enum"]
|
|
71
|
+
|
|
72
|
+
# Recursively resolve properties
|
|
73
|
+
if "properties" in schema:
|
|
74
|
+
result["properties"] = {}
|
|
75
|
+
for prop_name, prop_schema in schema["properties"].items():
|
|
76
|
+
result["properties"][prop_name] = _resolve_json_schema(prop_schema, defs, _seen)
|
|
77
|
+
|
|
78
|
+
# Copy required
|
|
79
|
+
if "required" in schema:
|
|
80
|
+
result["required"] = schema["required"]
|
|
81
|
+
|
|
82
|
+
# Recursively resolve items
|
|
83
|
+
if "items" in schema:
|
|
84
|
+
result["items"] = _resolve_json_schema(schema["items"], defs, _seen)
|
|
85
|
+
|
|
86
|
+
return result
|
|
87
|
+
|
|
13
88
|
def tool(
|
|
14
89
|
name: str,
|
|
15
90
|
description: str,
|
|
@@ -87,6 +162,8 @@ def tool(
|
|
|
87
162
|
|
|
88
163
|
# Map Python type to Parameter type
|
|
89
164
|
param_type = ParameterType.string
|
|
165
|
+
full_schema = None
|
|
166
|
+
|
|
90
167
|
if field_type is int:
|
|
91
168
|
param_type = ParameterType.integer
|
|
92
169
|
elif field_type is float:
|
|
@@ -95,8 +172,33 @@ def tool(
|
|
|
95
172
|
param_type = ParameterType.boolean
|
|
96
173
|
elif field_type is list or get_origin(field_type) is list:
|
|
97
174
|
param_type = ParameterType.list
|
|
175
|
+
# Extract item type from List[T] and build complete schema
|
|
176
|
+
type_args_inner = get_args(field_type)
|
|
177
|
+
items_schema = None
|
|
178
|
+
if type_args_inner:
|
|
179
|
+
item_type = type_args_inner[0]
|
|
180
|
+
if hasattr(item_type, 'model_json_schema'):
|
|
181
|
+
raw = item_type.model_json_schema()
|
|
182
|
+
raw_defs = raw.pop('$defs', {})
|
|
183
|
+
items_schema = _resolve_json_schema(raw, raw_defs)
|
|
184
|
+
elif item_type is str:
|
|
185
|
+
items_schema = {"type": "string"}
|
|
186
|
+
elif item_type is int:
|
|
187
|
+
items_schema = {"type": "integer"}
|
|
188
|
+
elif item_type is float:
|
|
189
|
+
items_schema = {"type": "number"}
|
|
190
|
+
elif item_type is bool:
|
|
191
|
+
items_schema = {"type": "boolean"}
|
|
192
|
+
if items_schema is not None:
|
|
193
|
+
full_schema = {"type": "array", "items": items_schema}
|
|
98
194
|
elif field_type is dict or get_origin(field_type) is dict:
|
|
99
195
|
param_type = ParameterType.dictionary
|
|
196
|
+
elif hasattr(field_type, 'model_json_schema'):
|
|
197
|
+
param_type = ParameterType.dictionary
|
|
198
|
+
raw = field_type.model_json_schema()
|
|
199
|
+
raw_defs = raw.pop('$defs', {})
|
|
200
|
+
resolved = _resolve_json_schema(raw, raw_defs)
|
|
201
|
+
full_schema = resolved
|
|
100
202
|
|
|
101
203
|
# Determine if required
|
|
102
204
|
field_info_extra = getattr(field_info, "json_schema_extra") or {}
|
|
@@ -125,12 +227,18 @@ def tool(
|
|
|
125
227
|
elif hasattr(field, 'description'):
|
|
126
228
|
description_text = field.description
|
|
127
229
|
|
|
230
|
+
# Build the complete schema with description included
|
|
231
|
+
if full_schema is not None:
|
|
232
|
+
if "description" not in full_schema:
|
|
233
|
+
full_schema["description"] = description_text
|
|
234
|
+
|
|
128
235
|
parameters.append(Parameter(
|
|
129
236
|
name=field_name,
|
|
130
237
|
param_type=param_type,
|
|
131
238
|
description=description_text,
|
|
132
239
|
required=required,
|
|
133
|
-
in_context=in_context
|
|
240
|
+
in_context=in_context,
|
|
241
|
+
schema=full_schema,
|
|
134
242
|
))
|
|
135
243
|
|
|
136
244
|
logger.info(f"Registered parameter: {field_name} of type {param_type.value}, required: {required}")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from enum import Enum
|
|
2
2
|
from typing import List, Dict, Any, Optional, Literal, TypedDict
|
|
3
|
-
from dataclasses import dataclass
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
4
|
|
|
5
5
|
from pydantic import BaseModel, Field
|
|
6
6
|
|
|
@@ -25,16 +25,20 @@ class Parameter:
|
|
|
25
25
|
description: str
|
|
26
26
|
required: bool
|
|
27
27
|
in_context: bool = False
|
|
28
|
+
schema: Optional[Dict[str, Any]] = field(default=None)
|
|
28
29
|
|
|
29
30
|
def to_dict(self) -> Dict[str, Any]:
|
|
30
31
|
"""Convert to dictionary for the discovery endpoint."""
|
|
31
|
-
|
|
32
|
+
result = {
|
|
32
33
|
"name": self.name,
|
|
33
34
|
"type": self.param_type.value,
|
|
34
35
|
"description": self.description,
|
|
35
36
|
"required": self.required,
|
|
36
37
|
"in_context": self.in_context,
|
|
37
38
|
}
|
|
39
|
+
if self.schema is not None:
|
|
40
|
+
result["schema"] = self.schema
|
|
41
|
+
return result
|
|
38
42
|
|
|
39
43
|
|
|
40
44
|
@dataclass
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# generated by datamodel-codegen:
|
|
2
2
|
# filename: proteus-document-spec.json
|
|
3
|
-
# timestamp: 2026-03-
|
|
3
|
+
# timestamp: 2026-03-17T21:59:22+00:00
|
|
4
4
|
|
|
5
5
|
from __future__ import annotations
|
|
6
6
|
|
|
@@ -1971,6 +1971,14 @@ class ProteusImage(BaseModel):
|
|
|
1971
1971
|
src: ProteusValue | str | None = Field(default=None, description='The image source URL')
|
|
1972
1972
|
|
|
1973
1973
|
|
|
1974
|
+
class ProteusQuestion(BaseModel):
|
|
1975
|
+
model_config = ConfigDict(
|
|
1976
|
+
extra='forbid',
|
|
1977
|
+
)
|
|
1978
|
+
field_type: Literal['Question'] = Field(default='Question', alias='$type')
|
|
1979
|
+
questions: ProteusValue = Field(..., description='Array of questions data')
|
|
1980
|
+
|
|
1981
|
+
|
|
1974
1982
|
class ProteusRange(BaseModel):
|
|
1975
1983
|
model_config = ConfigDict(
|
|
1976
1984
|
extra='forbid',
|
|
@@ -2908,36 +2916,6 @@ class ProteusCardLink(BaseModel):
|
|
|
2908
2916
|
z: SprinklePropZ | None = None
|
|
2909
2917
|
|
|
2910
2918
|
|
|
2911
|
-
class ProteusChoice(BaseModel):
|
|
2912
|
-
model_config = ConfigDict(
|
|
2913
|
-
extra='forbid',
|
|
2914
|
-
)
|
|
2915
|
-
field_type: Literal['Choice'] = Field(default='Choice', alias='$type')
|
|
2916
|
-
addonBefore: ProteusNode | None = Field(
|
|
2917
|
-
default=None, description='Content to display before the choice text (e.g., numbered badge)'
|
|
2918
|
-
)
|
|
2919
|
-
children: ProteusNode | None = Field(default=None, description='Title/label of the choice')
|
|
2920
|
-
description: ProteusNode | None = Field(
|
|
2921
|
-
default=None, description='Secondary description text shown below the title'
|
|
2922
|
-
)
|
|
2923
|
-
onClick: ProteusEventHandler | None = Field(
|
|
2924
|
-
default=None, description='Action triggered when choice is selected'
|
|
2925
|
-
)
|
|
2926
|
-
required: bool | None = Field(
|
|
2927
|
-
default=None, description='Whether selecting this choice is required to proceed'
|
|
2928
|
-
)
|
|
2929
|
-
value: str | None = Field(default=None, description='Value associated with this choice')
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
class ProteusChoiceGroup(BaseModel):
|
|
2933
|
-
model_config = ConfigDict(
|
|
2934
|
-
extra='forbid',
|
|
2935
|
-
)
|
|
2936
|
-
field_type: Literal['ChoiceGroup'] = Field(default='ChoiceGroup', alias='$type')
|
|
2937
|
-
children: ProteusNode | None = Field(default=None, description='Choice elements to render')
|
|
2938
|
-
name: str | None = Field(default=None, description='Data field name for the selected value')
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
2919
|
class ProteusField(BaseModel):
|
|
2942
2920
|
model_config = ConfigDict(
|
|
2943
2921
|
extra='forbid',
|
|
@@ -3563,8 +3541,6 @@ class ProteusElement(
|
|
|
3563
3541
|
| ProteusCardHeader
|
|
3564
3542
|
| ProteusCardLink
|
|
3565
3543
|
| ProteusChart
|
|
3566
|
-
| ProteusChoice
|
|
3567
|
-
| ProteusChoiceGroup
|
|
3568
3544
|
| ProteusDataTable
|
|
3569
3545
|
| ProteusField
|
|
3570
3546
|
| ProteusGroup
|
|
@@ -3574,6 +3550,7 @@ class ProteusElement(
|
|
|
3574
3550
|
| ProteusInput
|
|
3575
3551
|
| ProteusLink
|
|
3576
3552
|
| ProteusMap
|
|
3553
|
+
| ProteusQuestion
|
|
3577
3554
|
| ProteusRange
|
|
3578
3555
|
| ProteusSelect
|
|
3579
3556
|
| ProteusSelectContent
|
|
@@ -3595,8 +3572,6 @@ class ProteusElement(
|
|
|
3595
3572
|
| ProteusCardHeader
|
|
3596
3573
|
| ProteusCardLink
|
|
3597
3574
|
| ProteusChart
|
|
3598
|
-
| ProteusChoice
|
|
3599
|
-
| ProteusChoiceGroup
|
|
3600
3575
|
| ProteusDataTable
|
|
3601
3576
|
| ProteusField
|
|
3602
3577
|
| ProteusGroup
|
|
@@ -3606,6 +3581,7 @@ class ProteusElement(
|
|
|
3606
3581
|
| ProteusInput
|
|
3607
3582
|
| ProteusLink
|
|
3608
3583
|
| ProteusMap
|
|
3584
|
+
| ProteusQuestion
|
|
3609
3585
|
| ProteusRange
|
|
3610
3586
|
| ProteusSelect
|
|
3611
3587
|
| ProteusSelectContent
|
|
@@ -3655,8 +3631,6 @@ ProteusCancelAction.model_rebuild()
|
|
|
3655
3631
|
ProteusCard.model_rebuild()
|
|
3656
3632
|
ProteusCardHeader.model_rebuild()
|
|
3657
3633
|
ProteusCardLink.model_rebuild()
|
|
3658
|
-
ProteusChoice.model_rebuild()
|
|
3659
|
-
ProteusChoiceGroup.model_rebuild()
|
|
3660
3634
|
ProteusField.model_rebuild()
|
|
3661
3635
|
ProteusGroup.model_rebuild()
|
|
3662
3636
|
ProteusHeading.model_rebuild()
|
|
@@ -3689,8 +3663,6 @@ class UI:
|
|
|
3689
3663
|
CardHeader = ProteusCardHeader
|
|
3690
3664
|
CardLink = ProteusCardLink
|
|
3691
3665
|
Chart = ProteusChart
|
|
3692
|
-
Choice = ProteusChoice
|
|
3693
|
-
ChoiceGroup = ProteusChoiceGroup
|
|
3694
3666
|
DataTable = ProteusDataTable
|
|
3695
3667
|
Document = ProteusDocument
|
|
3696
3668
|
Field = ProteusField
|
|
@@ -3701,6 +3673,7 @@ class UI:
|
|
|
3701
3673
|
Input = ProteusInput
|
|
3702
3674
|
Link = ProteusLink
|
|
3703
3675
|
Map = ProteusMap
|
|
3676
|
+
Question = ProteusQuestion
|
|
3704
3677
|
Range = ProteusRange
|
|
3705
3678
|
Select = ProteusSelect
|
|
3706
3679
|
SelectContent = ProteusSelectContent
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "optimizely-opal.opal-tools-sdk"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.20-dev"
|
|
8
8
|
description = "SDK for creating Opal-compatible tools services"
|
|
9
9
|
authors = [{ name = "Optimizely", email = "opal-team@optimizely.com" }]
|
|
10
10
|
readme = "README.md"
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
"""Tests for nested schema extraction from Pydantic models in the @tool decorator."""
|
|
2
|
+
|
|
3
|
+
from enum import Enum
|
|
4
|
+
from typing import List, Optional
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel, Field
|
|
7
|
+
|
|
8
|
+
from opal_tools_sdk.decorators import _resolve_json_schema
|
|
9
|
+
from opal_tools_sdk.models import Parameter, ParameterType
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class OutputFormat(str, Enum):
|
|
13
|
+
markdown = "markdown"
|
|
14
|
+
raw = "raw"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class BrowseWebUrlItem(BaseModel):
|
|
18
|
+
url: str = Field(description="The webpage URL to browse.")
|
|
19
|
+
information_needed: Optional[str] = Field(
|
|
20
|
+
default=None, description="What information to extract"
|
|
21
|
+
)
|
|
22
|
+
output_format: Optional[OutputFormat] = Field(
|
|
23
|
+
default=None, description="Output format"
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class SimpleItem(BaseModel):
|
|
28
|
+
name: str = Field(description="Item name")
|
|
29
|
+
count: int = Field(description="Item count")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class TestResolveJsonSchema:
|
|
33
|
+
"""Tests for _resolve_json_schema helper."""
|
|
34
|
+
|
|
35
|
+
def test_simple_object(self):
|
|
36
|
+
schema = SimpleItem.model_json_schema()
|
|
37
|
+
defs = schema.pop("$defs", {})
|
|
38
|
+
resolved = _resolve_json_schema(schema, defs)
|
|
39
|
+
assert resolved["type"] == "object"
|
|
40
|
+
assert resolved["properties"]["name"] == {
|
|
41
|
+
"type": "string",
|
|
42
|
+
"description": "Item name",
|
|
43
|
+
}
|
|
44
|
+
assert resolved["properties"]["count"] == {
|
|
45
|
+
"type": "integer",
|
|
46
|
+
"description": "Item count",
|
|
47
|
+
}
|
|
48
|
+
assert resolved["required"] == ["name", "count"]
|
|
49
|
+
|
|
50
|
+
def test_optional_field_simplified(self):
|
|
51
|
+
schema = BrowseWebUrlItem.model_json_schema()
|
|
52
|
+
defs = schema.pop("$defs", {})
|
|
53
|
+
resolved = _resolve_json_schema(schema, defs)
|
|
54
|
+
info = resolved["properties"]["information_needed"]
|
|
55
|
+
assert info["type"] == "string"
|
|
56
|
+
assert "description" in info
|
|
57
|
+
|
|
58
|
+
def test_enum_ref_resolved(self):
|
|
59
|
+
schema = BrowseWebUrlItem.model_json_schema()
|
|
60
|
+
defs = schema.pop("$defs", {})
|
|
61
|
+
resolved = _resolve_json_schema(schema, defs)
|
|
62
|
+
fmt = resolved["properties"]["output_format"]
|
|
63
|
+
assert fmt["type"] == "string"
|
|
64
|
+
assert fmt["enum"] == ["markdown", "raw"]
|
|
65
|
+
|
|
66
|
+
def test_required_fields(self):
|
|
67
|
+
schema = BrowseWebUrlItem.model_json_schema()
|
|
68
|
+
defs = schema.pop("$defs", {})
|
|
69
|
+
resolved = _resolve_json_schema(schema, defs)
|
|
70
|
+
assert resolved["required"] == ["url"]
|
|
71
|
+
|
|
72
|
+
def test_ref_resolution(self):
|
|
73
|
+
defs = {"MyType": {"type": "string", "enum": ["a", "b"]}}
|
|
74
|
+
schema = {"$ref": "#/$defs/MyType"}
|
|
75
|
+
resolved = _resolve_json_schema(schema, defs)
|
|
76
|
+
assert resolved == {"type": "string", "enum": ["a", "b"]}
|
|
77
|
+
|
|
78
|
+
def test_strips_title(self):
|
|
79
|
+
schema = {"type": "object", "title": "SomeModel", "properties": {}}
|
|
80
|
+
resolved = _resolve_json_schema(schema, {})
|
|
81
|
+
assert "title" not in resolved
|
|
82
|
+
|
|
83
|
+
def test_ref_preserves_sibling_description(self):
|
|
84
|
+
"""$ref with sibling description should preserve the description."""
|
|
85
|
+
defs = {"Inner": {"type": "object", "properties": {"x": {"type": "string"}}}}
|
|
86
|
+
schema = {"$ref": "#/$defs/Inner", "description": "My inner object"}
|
|
87
|
+
resolved = _resolve_json_schema(schema, defs)
|
|
88
|
+
assert resolved["description"] == "My inner object"
|
|
89
|
+
assert resolved["type"] == "object"
|
|
90
|
+
|
|
91
|
+
def test_circular_ref_does_not_crash(self):
|
|
92
|
+
"""Self-referential $ref should not cause infinite recursion."""
|
|
93
|
+
defs = {
|
|
94
|
+
"TreeNode": {
|
|
95
|
+
"type": "object",
|
|
96
|
+
"properties": {
|
|
97
|
+
"name": {"type": "string"},
|
|
98
|
+
"children": {"type": "array", "items": {"$ref": "#/$defs/TreeNode"}},
|
|
99
|
+
},
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
schema = {"$ref": "#/$defs/TreeNode"}
|
|
103
|
+
resolved = _resolve_json_schema(schema, defs)
|
|
104
|
+
assert resolved["type"] == "object"
|
|
105
|
+
# The circular ref should be broken with {type: object}
|
|
106
|
+
assert resolved["properties"]["children"]["items"]["type"] == "object"
|
|
107
|
+
|
|
108
|
+
def test_anyof_with_multiple_non_null_types(self):
|
|
109
|
+
"""Union[str, int] should fall back to string."""
|
|
110
|
+
schema = {
|
|
111
|
+
"anyOf": [{"type": "string"}, {"type": "integer"}],
|
|
112
|
+
"description": "A union field",
|
|
113
|
+
}
|
|
114
|
+
resolved = _resolve_json_schema(schema, {})
|
|
115
|
+
assert resolved == {"type": "string", "description": "A union field"}
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class TestParameterToDict:
|
|
119
|
+
"""Tests for Parameter.to_dict with schema field."""
|
|
120
|
+
|
|
121
|
+
def test_to_dict_with_array_schema(self):
|
|
122
|
+
param = Parameter(
|
|
123
|
+
name="urls",
|
|
124
|
+
param_type=ParameterType.list,
|
|
125
|
+
description="URLs",
|
|
126
|
+
required=True,
|
|
127
|
+
schema={"type": "array", "description": "URLs", "items": {"type": "object", "properties": {"url": {"type": "string"}}}},
|
|
128
|
+
)
|
|
129
|
+
d = param.to_dict()
|
|
130
|
+
assert d["schema"]["type"] == "array"
|
|
131
|
+
assert d["schema"]["items"]["properties"]["url"] == {"type": "string"}
|
|
132
|
+
|
|
133
|
+
def test_to_dict_with_object_schema(self):
|
|
134
|
+
param = Parameter(
|
|
135
|
+
name="config",
|
|
136
|
+
param_type=ParameterType.dictionary,
|
|
137
|
+
description="Config",
|
|
138
|
+
required=True,
|
|
139
|
+
schema={"type": "object", "properties": {"name": {"type": "string"}}, "required": ["name"]},
|
|
140
|
+
)
|
|
141
|
+
d = param.to_dict()
|
|
142
|
+
assert d["schema"]["properties"]["name"] == {"type": "string"}
|
|
143
|
+
assert d["schema"]["required"] == ["name"]
|
|
144
|
+
|
|
145
|
+
def test_to_dict_without_schema(self):
|
|
146
|
+
"""Backward compat: no schema key when None."""
|
|
147
|
+
param = Parameter(
|
|
148
|
+
name="query",
|
|
149
|
+
param_type=ParameterType.string,
|
|
150
|
+
description="Search",
|
|
151
|
+
required=True,
|
|
152
|
+
)
|
|
153
|
+
d = param.to_dict()
|
|
154
|
+
assert "schema" not in d
|
|
155
|
+
|
|
156
|
+
def test_schema_is_complete_json_schema(self):
|
|
157
|
+
"""schema field must be a complete, self-contained JSON Schema."""
|
|
158
|
+
param = Parameter(
|
|
159
|
+
name="urls",
|
|
160
|
+
param_type=ParameterType.list,
|
|
161
|
+
description="URLs to browse",
|
|
162
|
+
required=True,
|
|
163
|
+
schema={
|
|
164
|
+
"type": "array",
|
|
165
|
+
"description": "URLs to browse",
|
|
166
|
+
"items": {
|
|
167
|
+
"type": "object",
|
|
168
|
+
"properties": {
|
|
169
|
+
"url": {"type": "string", "description": "The URL"},
|
|
170
|
+
},
|
|
171
|
+
"required": ["url"],
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
)
|
|
175
|
+
d = param.to_dict()
|
|
176
|
+
schema = d["schema"]
|
|
177
|
+
# Schema is self-contained: includes type and description
|
|
178
|
+
assert schema["type"] == "array"
|
|
179
|
+
assert schema["description"] == "URLs to browse"
|
|
180
|
+
assert schema["items"]["type"] == "object"
|
|
181
|
+
assert schema["items"]["required"] == ["url"]
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
class TestToolDecoratorNestedExtraction:
|
|
185
|
+
"""Tests for the @tool decorator extracting nested schemas from Pydantic models."""
|
|
186
|
+
|
|
187
|
+
def test_list_of_pydantic_model(self):
|
|
188
|
+
"""Simulates what the decorator does for List[BaseModel] fields."""
|
|
189
|
+
raw = SimpleItem.model_json_schema()
|
|
190
|
+
defs = raw.pop("$defs", {})
|
|
191
|
+
items_schema = _resolve_json_schema(raw, defs)
|
|
192
|
+
assert items_schema["type"] == "object"
|
|
193
|
+
assert "name" in items_schema["properties"]
|
|
194
|
+
assert "count" in items_schema["properties"]
|
|
195
|
+
|
|
196
|
+
def test_list_of_str_schema(self):
|
|
197
|
+
"""List[str] should produce a complete array schema."""
|
|
198
|
+
param = Parameter(
|
|
199
|
+
name="tags",
|
|
200
|
+
param_type=ParameterType.list,
|
|
201
|
+
description="Tags",
|
|
202
|
+
required=False,
|
|
203
|
+
schema={"type": "array", "description": "Tags", "items": {"type": "string"}},
|
|
204
|
+
)
|
|
205
|
+
d = param.to_dict()
|
|
206
|
+
assert d["schema"]["type"] == "array"
|
|
207
|
+
assert d["schema"]["items"] == {"type": "string"}
|
|
208
|
+
|
|
209
|
+
def test_browse_web_url_item_full(self):
|
|
210
|
+
"""Full integration test with BrowseWebUrlItem model."""
|
|
211
|
+
raw = BrowseWebUrlItem.model_json_schema()
|
|
212
|
+
defs = raw.pop("$defs", {})
|
|
213
|
+
items_schema = _resolve_json_schema(raw, defs)
|
|
214
|
+
|
|
215
|
+
assert items_schema["type"] == "object"
|
|
216
|
+
assert items_schema["properties"]["url"]["type"] == "string"
|
|
217
|
+
assert items_schema["properties"]["information_needed"]["type"] == "string"
|
|
218
|
+
assert items_schema["properties"]["output_format"]["enum"] == ["markdown", "raw"]
|
|
219
|
+
assert items_schema["required"] == ["url"]
|
|
220
|
+
|
|
221
|
+
# Build the complete schema as the decorator would
|
|
222
|
+
full_schema = {"type": "array", "description": "URLs to browse", "items": items_schema}
|
|
223
|
+
param = Parameter(
|
|
224
|
+
name="urls",
|
|
225
|
+
param_type=ParameterType.list,
|
|
226
|
+
description="URLs to browse",
|
|
227
|
+
required=True,
|
|
228
|
+
schema=full_schema,
|
|
229
|
+
)
|
|
230
|
+
d = param.to_dict()
|
|
231
|
+
assert d["schema"]["type"] == "array"
|
|
232
|
+
assert d["schema"]["items"]["type"] == "object"
|
|
233
|
+
assert "url" in d["schema"]["items"]["properties"]
|
|
234
|
+
# Uses standard "required" key, not "required_properties"
|
|
235
|
+
assert d["schema"]["items"]["required"] == ["url"]
|
{optimizely_opal_opal_tools_sdk-0.1.18.dev0 → optimizely_opal_opal_tools_sdk-0.1.20.dev0}/README.md
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
|
{optimizely_opal_opal_tools_sdk-0.1.18.dev0 → optimizely_opal_opal_tools_sdk-0.1.20.dev0}/setup.cfg
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|