optimizely-opal.opal-tools-sdk 0.1.0.dev0__tar.gz → 0.1.10.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.10.dev0/PKG-INFO +255 -0
- optimizely_opal_opal_tools_sdk-0.1.10.dev0/README.md +231 -0
- optimizely_opal_opal_tools_sdk-0.1.10.dev0/opal_tools_sdk/__init__.py +20 -0
- {optimizely_opal_opal_tools_sdk-0.1.0.dev0 → optimizely_opal_opal_tools_sdk-0.1.10.dev0}/opal_tools_sdk/decorators.py +40 -12
- optimizely_opal_opal_tools_sdk-0.1.10.dev0/opal_tools_sdk/logging.py +37 -0
- optimizely_opal_opal_tools_sdk-0.1.10.dev0/opal_tools_sdk/models.py +129 -0
- {optimizely_opal_opal_tools_sdk-0.1.0.dev0 → optimizely_opal_opal_tools_sdk-0.1.10.dev0}/opal_tools_sdk/service.py +55 -48
- optimizely_opal_opal_tools_sdk-0.1.10.dev0/optimizely_opal.opal_tools_sdk.egg-info/PKG-INFO +255 -0
- {optimizely_opal_opal_tools_sdk-0.1.0.dev0 → optimizely_opal_opal_tools_sdk-0.1.10.dev0}/optimizely_opal.opal_tools_sdk.egg-info/SOURCES.txt +2 -0
- optimizely_opal_opal_tools_sdk-0.1.10.dev0/pyproject.toml +33 -0
- {optimizely_opal_opal_tools_sdk-0.1.0.dev0 → optimizely_opal_opal_tools_sdk-0.1.10.dev0}/setup.py +1 -1
- optimizely_opal_opal_tools_sdk-0.1.0.dev0/PKG-INFO +0 -24
- optimizely_opal_opal_tools_sdk-0.1.0.dev0/README.md +0 -114
- optimizely_opal_opal_tools_sdk-0.1.0.dev0/opal_tools_sdk/__init__.py +0 -6
- optimizely_opal_opal_tools_sdk-0.1.0.dev0/opal_tools_sdk/models.py +0 -69
- optimizely_opal_opal_tools_sdk-0.1.0.dev0/optimizely_opal.opal_tools_sdk.egg-info/PKG-INFO +0 -24
- {optimizely_opal_opal_tools_sdk-0.1.0.dev0 → optimizely_opal_opal_tools_sdk-0.1.10.dev0}/opal_tools_sdk/_registry.py +0 -0
- {optimizely_opal_opal_tools_sdk-0.1.0.dev0 → optimizely_opal_opal_tools_sdk-0.1.10.dev0}/opal_tools_sdk/auth.py +0 -0
- {optimizely_opal_opal_tools_sdk-0.1.0.dev0 → optimizely_opal_opal_tools_sdk-0.1.10.dev0}/optimizely_opal.opal_tools_sdk.egg-info/dependency_links.txt +0 -0
- {optimizely_opal_opal_tools_sdk-0.1.0.dev0 → optimizely_opal_opal_tools_sdk-0.1.10.dev0}/optimizely_opal.opal_tools_sdk.egg-info/requires.txt +0 -0
- {optimizely_opal_opal_tools_sdk-0.1.0.dev0 → optimizely_opal_opal_tools_sdk-0.1.10.dev0}/optimizely_opal.opal_tools_sdk.egg-info/top_level.txt +0 -0
- {optimizely_opal_opal_tools_sdk-0.1.0.dev0 → optimizely_opal_opal_tools_sdk-0.1.10.dev0}/setup.cfg +0 -0
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: optimizely-opal.opal-tools-sdk
|
|
3
|
+
Version: 0.1.10.dev0
|
|
4
|
+
Summary: SDK for creating Opal-compatible tools services
|
|
5
|
+
Home-page: https://github.com/optimizely/opal-tools-sdk
|
|
6
|
+
Author: Optimizely
|
|
7
|
+
Author-email: Optimizely <opal-team@optimizely.com>
|
|
8
|
+
License: MIT
|
|
9
|
+
Project-URL: Homepage, https://github.com/optimizely/opal-tools-sdk
|
|
10
|
+
Project-URL: Bug Tracker, https://github.com/optimizely/opal-tools-sdk/issues
|
|
11
|
+
Keywords: opal,tools,sdk,ai,llm
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Requires-Python: >=3.10
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
Requires-Dist: fastapi>=0.100.0
|
|
19
|
+
Requires-Dist: pydantic>=2.0.0
|
|
20
|
+
Requires-Dist: httpx>=0.24.1
|
|
21
|
+
Dynamic: author
|
|
22
|
+
Dynamic: home-page
|
|
23
|
+
Dynamic: requires-python
|
|
24
|
+
|
|
25
|
+
# Opal Tools SDK for Python
|
|
26
|
+
|
|
27
|
+
This SDK simplifies the creation of tools services compatible with the Opal Tools Management Service.
|
|
28
|
+
|
|
29
|
+
## Features
|
|
30
|
+
|
|
31
|
+
- Easy definition of tool functions with decorators
|
|
32
|
+
- Automatic generation of discovery endpoints
|
|
33
|
+
- Parameter validation and type checking
|
|
34
|
+
- Authentication helpers
|
|
35
|
+
- FastAPI integration
|
|
36
|
+
- Island components for interactive UI responses
|
|
37
|
+
|
|
38
|
+
## Installation
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pip install optimizely-opal.opal-tools-sdk
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Note: While the package is installed as `optimizely-opal.opal-tools-sdk`, you'll still import it in your code as `opal_tools_sdk`:
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
# Import using the package name
|
|
48
|
+
from opal_tools_sdk import ToolsService, tool, IslandResponse, IslandConfig
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Usage
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
from opal_tools_sdk import ToolsService, tool
|
|
55
|
+
from pydantic import BaseModel
|
|
56
|
+
from fastapi import FastAPI
|
|
57
|
+
|
|
58
|
+
app = FastAPI()
|
|
59
|
+
tools_service = ToolsService(app)
|
|
60
|
+
|
|
61
|
+
class WeatherParameters(BaseModel):
|
|
62
|
+
location: str
|
|
63
|
+
units: str = "metric"
|
|
64
|
+
|
|
65
|
+
@tool("get_weather", "Gets current weather for a location")
|
|
66
|
+
async def get_weather(parameters: WeatherParameters):
|
|
67
|
+
# Implementation...
|
|
68
|
+
return {"temperature": 22, "condition": "sunny"}
|
|
69
|
+
|
|
70
|
+
# Discovery endpoint is automatically created at /discovery
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Authentication
|
|
74
|
+
|
|
75
|
+
The SDK provides two ways to require authentication for your tools:
|
|
76
|
+
|
|
77
|
+
### 1. Using the `@requires_auth` decorator
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
from opal_tools_sdk import ToolsService, tool, AuthData
|
|
81
|
+
from opal_tools_sdk.auth import requires_auth
|
|
82
|
+
from pydantic import BaseModel
|
|
83
|
+
from fastapi import FastAPI
|
|
84
|
+
from typing import Optional
|
|
85
|
+
|
|
86
|
+
app = FastAPI()
|
|
87
|
+
tools_service = ToolsService(app)
|
|
88
|
+
|
|
89
|
+
class CalendarParameters(BaseModel):
|
|
90
|
+
date: str
|
|
91
|
+
timezone: str = "UTC"
|
|
92
|
+
|
|
93
|
+
# Single authentication requirement
|
|
94
|
+
@requires_auth(provider="google", scope_bundle="calendar", required=True)
|
|
95
|
+
@tool("get_calendar_events", "Gets calendar events for a date")
|
|
96
|
+
async def get_calendar_events(parameters: CalendarParameters, auth_data: Optional[AuthData] = None):
|
|
97
|
+
# The auth_data parameter contains authentication information
|
|
98
|
+
if auth_data:
|
|
99
|
+
token = auth_data["credentials"]["access_token"]
|
|
100
|
+
|
|
101
|
+
# Use the token to make authenticated requests
|
|
102
|
+
# ...
|
|
103
|
+
|
|
104
|
+
return {"events": ["Meeting at 10:00", "Lunch at 12:00"]}
|
|
105
|
+
|
|
106
|
+
# Multiple authentication requirements (tool can work with either provider)
|
|
107
|
+
@requires_auth(provider="google", scope_bundle="calendar", required=True)
|
|
108
|
+
@requires_auth(provider="microsoft", scope_bundle="outlook", required=True)
|
|
109
|
+
@tool("get_calendar_availability", "Check calendar availability")
|
|
110
|
+
async def get_calendar_availability(parameters: CalendarParameters, auth_data: Optional[AuthData] = None):
|
|
111
|
+
provider = ""
|
|
112
|
+
token = ""
|
|
113
|
+
|
|
114
|
+
if auth_data:
|
|
115
|
+
provider = auth_data["provider"]
|
|
116
|
+
token = auth_data["credentials"]["access_token"]
|
|
117
|
+
|
|
118
|
+
if provider == "google":
|
|
119
|
+
# Use Google Calendar API
|
|
120
|
+
pass
|
|
121
|
+
elif provider == "microsoft":
|
|
122
|
+
# Use Microsoft Outlook API
|
|
123
|
+
pass
|
|
124
|
+
|
|
125
|
+
return {"available": True, "provider_used": provider}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### 2. Specifying auth requirements in the `@tool` decorator
|
|
129
|
+
|
|
130
|
+
```python
|
|
131
|
+
@tool(
|
|
132
|
+
"get_email",
|
|
133
|
+
"Gets emails from the user's inbox",
|
|
134
|
+
auth_requirements=[
|
|
135
|
+
{"provider": "google", "scope_bundle": "gmail", "required": True}
|
|
136
|
+
]
|
|
137
|
+
)
|
|
138
|
+
async def get_email(parameters: EmailParameters, auth_data: Optional[AuthData] = None):
|
|
139
|
+
# Implementation...
|
|
140
|
+
return {"emails": ["Email 1", "Email 2"]}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## Island Components
|
|
144
|
+
|
|
145
|
+
The SDK includes Island components for creating interactive UI responses that allow users to input data and trigger actions.
|
|
146
|
+
|
|
147
|
+
### Weather Tool with Interactive Island
|
|
148
|
+
|
|
149
|
+
```python
|
|
150
|
+
from opal_tools_sdk import ToolsService, tool, IslandResponse, IslandConfig
|
|
151
|
+
from pydantic import BaseModel
|
|
152
|
+
from fastapi import FastAPI
|
|
153
|
+
|
|
154
|
+
app = FastAPI()
|
|
155
|
+
tools_service = ToolsService(app)
|
|
156
|
+
|
|
157
|
+
class WeatherParameters(BaseModel):
|
|
158
|
+
location: str
|
|
159
|
+
units: str = "metric"
|
|
160
|
+
|
|
161
|
+
@tool("get_weather", "Gets current weather for a location")
|
|
162
|
+
async def get_weather(parameters: WeatherParameters):
|
|
163
|
+
# Get weather data (implementation details omitted)
|
|
164
|
+
weather_data = {"temperature": 22, "condition": "sunny", "humidity": 65}
|
|
165
|
+
|
|
166
|
+
# Create an interactive island for weather settings
|
|
167
|
+
island = IslandConfig(
|
|
168
|
+
fields=[
|
|
169
|
+
IslandConfig.Field(
|
|
170
|
+
name="location",
|
|
171
|
+
label="Location",
|
|
172
|
+
type="string",
|
|
173
|
+
value=parameters.location
|
|
174
|
+
),
|
|
175
|
+
IslandConfig.Field(
|
|
176
|
+
name="units",
|
|
177
|
+
label="Temperature Units",
|
|
178
|
+
type="string",
|
|
179
|
+
value=parameters.units,
|
|
180
|
+
options=["metric", "imperial", "kelvin"]
|
|
181
|
+
),
|
|
182
|
+
IslandConfig.Field(
|
|
183
|
+
name="current_temp",
|
|
184
|
+
label="Current Temperature",
|
|
185
|
+
type="string",
|
|
186
|
+
value=f"{weather_data['temperature']}°{'C' if parameters.units == 'metric' else 'F'}"
|
|
187
|
+
)
|
|
188
|
+
],
|
|
189
|
+
actions=[
|
|
190
|
+
IslandConfig.Action(
|
|
191
|
+
name="refresh_weather",
|
|
192
|
+
label="Refresh Weather",
|
|
193
|
+
type="button",
|
|
194
|
+
endpoint="/tools/get_weather",
|
|
195
|
+
operation="update"
|
|
196
|
+
)
|
|
197
|
+
]
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
return IslandResponse.create([island])
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
### Island Components
|
|
204
|
+
|
|
205
|
+
#### IslandConfig.Field
|
|
206
|
+
Fields represent data inputs in the UI:
|
|
207
|
+
- `name`: Programmatic field identifier
|
|
208
|
+
- `label`: Human-readable label
|
|
209
|
+
- `type`: Field type (`"string"`, `"boolean"`, `"json"`)
|
|
210
|
+
- `value`: Current field value (optional)
|
|
211
|
+
- `hidden`: Whether to hide from user (optional, default: False)
|
|
212
|
+
- `options`: Available options for selection (optional)
|
|
213
|
+
|
|
214
|
+
#### IslandConfig.Action
|
|
215
|
+
Actions represent buttons or operations:
|
|
216
|
+
- `name`: Programmatic action identifier
|
|
217
|
+
- `label`: Human-readable button label
|
|
218
|
+
- `type`: UI element type (typically `"button"`)
|
|
219
|
+
- `endpoint`: API endpoint to call
|
|
220
|
+
- `operation`: Operation type (default: `"create"`)
|
|
221
|
+
|
|
222
|
+
#### IslandConfig
|
|
223
|
+
Contains the complete island configuration:
|
|
224
|
+
- `fields`: List of IslandConfig.Field objects
|
|
225
|
+
- `actions`: List of IslandConfig.Action objects
|
|
226
|
+
- `type`: Island type for UI rendering (optional, default: `None`)
|
|
227
|
+
- `icon`: Icon to display in the island (optional, default: `None`)
|
|
228
|
+
|
|
229
|
+
#### IslandResponse
|
|
230
|
+
The response wrapper for islands:
|
|
231
|
+
- Use `IslandResponse.create([islands])` to create responses
|
|
232
|
+
- Supports multiple islands per response
|
|
233
|
+
|
|
234
|
+
## Type Definitions
|
|
235
|
+
|
|
236
|
+
The SDK provides several TypedDict and dataclass definitions for better type safety:
|
|
237
|
+
|
|
238
|
+
### Authentication Types
|
|
239
|
+
- `AuthData`: TypedDict containing provider and credentials information
|
|
240
|
+
- `Credentials`: TypedDict with access_token, org_sso_id, customer_id, instance_id, and product_sku
|
|
241
|
+
- `AuthRequirement`: Dataclass for specifying authentication requirements
|
|
242
|
+
|
|
243
|
+
### Execution Environment
|
|
244
|
+
- `Environment`: TypedDict specifying execution mode (`"headless"` or `"interactive"`)
|
|
245
|
+
|
|
246
|
+
### Parameter Types
|
|
247
|
+
- `ParameterType`: Enum for supported parameter types (string, integer, number, boolean, list, dictionary)
|
|
248
|
+
- `Parameter`: Dataclass for tool parameter definitions
|
|
249
|
+
- `Function`: Dataclass for complete tool function definitions
|
|
250
|
+
|
|
251
|
+
These types are automatically imported when you import from `opal_tools_sdk` and provide better IDE support and type checking.
|
|
252
|
+
|
|
253
|
+
## Documentation
|
|
254
|
+
|
|
255
|
+
See full documentation for more examples and configuration options.
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
# Opal Tools SDK for Python
|
|
2
|
+
|
|
3
|
+
This SDK simplifies the creation of tools services compatible with the Opal Tools Management Service.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- Easy definition of tool functions with decorators
|
|
8
|
+
- Automatic generation of discovery endpoints
|
|
9
|
+
- Parameter validation and type checking
|
|
10
|
+
- Authentication helpers
|
|
11
|
+
- FastAPI integration
|
|
12
|
+
- Island components for interactive UI responses
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
pip install optimizely-opal.opal-tools-sdk
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Note: While the package is installed as `optimizely-opal.opal-tools-sdk`, you'll still import it in your code as `opal_tools_sdk`:
|
|
21
|
+
|
|
22
|
+
```python
|
|
23
|
+
# Import using the package name
|
|
24
|
+
from opal_tools_sdk import ToolsService, tool, IslandResponse, IslandConfig
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Usage
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
from opal_tools_sdk import ToolsService, tool
|
|
31
|
+
from pydantic import BaseModel
|
|
32
|
+
from fastapi import FastAPI
|
|
33
|
+
|
|
34
|
+
app = FastAPI()
|
|
35
|
+
tools_service = ToolsService(app)
|
|
36
|
+
|
|
37
|
+
class WeatherParameters(BaseModel):
|
|
38
|
+
location: str
|
|
39
|
+
units: str = "metric"
|
|
40
|
+
|
|
41
|
+
@tool("get_weather", "Gets current weather for a location")
|
|
42
|
+
async def get_weather(parameters: WeatherParameters):
|
|
43
|
+
# Implementation...
|
|
44
|
+
return {"temperature": 22, "condition": "sunny"}
|
|
45
|
+
|
|
46
|
+
# Discovery endpoint is automatically created at /discovery
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Authentication
|
|
50
|
+
|
|
51
|
+
The SDK provides two ways to require authentication for your tools:
|
|
52
|
+
|
|
53
|
+
### 1. Using the `@requires_auth` decorator
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
from opal_tools_sdk import ToolsService, tool, AuthData
|
|
57
|
+
from opal_tools_sdk.auth import requires_auth
|
|
58
|
+
from pydantic import BaseModel
|
|
59
|
+
from fastapi import FastAPI
|
|
60
|
+
from typing import Optional
|
|
61
|
+
|
|
62
|
+
app = FastAPI()
|
|
63
|
+
tools_service = ToolsService(app)
|
|
64
|
+
|
|
65
|
+
class CalendarParameters(BaseModel):
|
|
66
|
+
date: str
|
|
67
|
+
timezone: str = "UTC"
|
|
68
|
+
|
|
69
|
+
# Single authentication requirement
|
|
70
|
+
@requires_auth(provider="google", scope_bundle="calendar", required=True)
|
|
71
|
+
@tool("get_calendar_events", "Gets calendar events for a date")
|
|
72
|
+
async def get_calendar_events(parameters: CalendarParameters, auth_data: Optional[AuthData] = None):
|
|
73
|
+
# The auth_data parameter contains authentication information
|
|
74
|
+
if auth_data:
|
|
75
|
+
token = auth_data["credentials"]["access_token"]
|
|
76
|
+
|
|
77
|
+
# Use the token to make authenticated requests
|
|
78
|
+
# ...
|
|
79
|
+
|
|
80
|
+
return {"events": ["Meeting at 10:00", "Lunch at 12:00"]}
|
|
81
|
+
|
|
82
|
+
# Multiple authentication requirements (tool can work with either provider)
|
|
83
|
+
@requires_auth(provider="google", scope_bundle="calendar", required=True)
|
|
84
|
+
@requires_auth(provider="microsoft", scope_bundle="outlook", required=True)
|
|
85
|
+
@tool("get_calendar_availability", "Check calendar availability")
|
|
86
|
+
async def get_calendar_availability(parameters: CalendarParameters, auth_data: Optional[AuthData] = None):
|
|
87
|
+
provider = ""
|
|
88
|
+
token = ""
|
|
89
|
+
|
|
90
|
+
if auth_data:
|
|
91
|
+
provider = auth_data["provider"]
|
|
92
|
+
token = auth_data["credentials"]["access_token"]
|
|
93
|
+
|
|
94
|
+
if provider == "google":
|
|
95
|
+
# Use Google Calendar API
|
|
96
|
+
pass
|
|
97
|
+
elif provider == "microsoft":
|
|
98
|
+
# Use Microsoft Outlook API
|
|
99
|
+
pass
|
|
100
|
+
|
|
101
|
+
return {"available": True, "provider_used": provider}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### 2. Specifying auth requirements in the `@tool` decorator
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
@tool(
|
|
108
|
+
"get_email",
|
|
109
|
+
"Gets emails from the user's inbox",
|
|
110
|
+
auth_requirements=[
|
|
111
|
+
{"provider": "google", "scope_bundle": "gmail", "required": True}
|
|
112
|
+
]
|
|
113
|
+
)
|
|
114
|
+
async def get_email(parameters: EmailParameters, auth_data: Optional[AuthData] = None):
|
|
115
|
+
# Implementation...
|
|
116
|
+
return {"emails": ["Email 1", "Email 2"]}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## Island Components
|
|
120
|
+
|
|
121
|
+
The SDK includes Island components for creating interactive UI responses that allow users to input data and trigger actions.
|
|
122
|
+
|
|
123
|
+
### Weather Tool with Interactive Island
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
from opal_tools_sdk import ToolsService, tool, IslandResponse, IslandConfig
|
|
127
|
+
from pydantic import BaseModel
|
|
128
|
+
from fastapi import FastAPI
|
|
129
|
+
|
|
130
|
+
app = FastAPI()
|
|
131
|
+
tools_service = ToolsService(app)
|
|
132
|
+
|
|
133
|
+
class WeatherParameters(BaseModel):
|
|
134
|
+
location: str
|
|
135
|
+
units: str = "metric"
|
|
136
|
+
|
|
137
|
+
@tool("get_weather", "Gets current weather for a location")
|
|
138
|
+
async def get_weather(parameters: WeatherParameters):
|
|
139
|
+
# Get weather data (implementation details omitted)
|
|
140
|
+
weather_data = {"temperature": 22, "condition": "sunny", "humidity": 65}
|
|
141
|
+
|
|
142
|
+
# Create an interactive island for weather settings
|
|
143
|
+
island = IslandConfig(
|
|
144
|
+
fields=[
|
|
145
|
+
IslandConfig.Field(
|
|
146
|
+
name="location",
|
|
147
|
+
label="Location",
|
|
148
|
+
type="string",
|
|
149
|
+
value=parameters.location
|
|
150
|
+
),
|
|
151
|
+
IslandConfig.Field(
|
|
152
|
+
name="units",
|
|
153
|
+
label="Temperature Units",
|
|
154
|
+
type="string",
|
|
155
|
+
value=parameters.units,
|
|
156
|
+
options=["metric", "imperial", "kelvin"]
|
|
157
|
+
),
|
|
158
|
+
IslandConfig.Field(
|
|
159
|
+
name="current_temp",
|
|
160
|
+
label="Current Temperature",
|
|
161
|
+
type="string",
|
|
162
|
+
value=f"{weather_data['temperature']}°{'C' if parameters.units == 'metric' else 'F'}"
|
|
163
|
+
)
|
|
164
|
+
],
|
|
165
|
+
actions=[
|
|
166
|
+
IslandConfig.Action(
|
|
167
|
+
name="refresh_weather",
|
|
168
|
+
label="Refresh Weather",
|
|
169
|
+
type="button",
|
|
170
|
+
endpoint="/tools/get_weather",
|
|
171
|
+
operation="update"
|
|
172
|
+
)
|
|
173
|
+
]
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
return IslandResponse.create([island])
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### Island Components
|
|
180
|
+
|
|
181
|
+
#### IslandConfig.Field
|
|
182
|
+
Fields represent data inputs in the UI:
|
|
183
|
+
- `name`: Programmatic field identifier
|
|
184
|
+
- `label`: Human-readable label
|
|
185
|
+
- `type`: Field type (`"string"`, `"boolean"`, `"json"`)
|
|
186
|
+
- `value`: Current field value (optional)
|
|
187
|
+
- `hidden`: Whether to hide from user (optional, default: False)
|
|
188
|
+
- `options`: Available options for selection (optional)
|
|
189
|
+
|
|
190
|
+
#### IslandConfig.Action
|
|
191
|
+
Actions represent buttons or operations:
|
|
192
|
+
- `name`: Programmatic action identifier
|
|
193
|
+
- `label`: Human-readable button label
|
|
194
|
+
- `type`: UI element type (typically `"button"`)
|
|
195
|
+
- `endpoint`: API endpoint to call
|
|
196
|
+
- `operation`: Operation type (default: `"create"`)
|
|
197
|
+
|
|
198
|
+
#### IslandConfig
|
|
199
|
+
Contains the complete island configuration:
|
|
200
|
+
- `fields`: List of IslandConfig.Field objects
|
|
201
|
+
- `actions`: List of IslandConfig.Action objects
|
|
202
|
+
- `type`: Island type for UI rendering (optional, default: `None`)
|
|
203
|
+
- `icon`: Icon to display in the island (optional, default: `None`)
|
|
204
|
+
|
|
205
|
+
#### IslandResponse
|
|
206
|
+
The response wrapper for islands:
|
|
207
|
+
- Use `IslandResponse.create([islands])` to create responses
|
|
208
|
+
- Supports multiple islands per response
|
|
209
|
+
|
|
210
|
+
## Type Definitions
|
|
211
|
+
|
|
212
|
+
The SDK provides several TypedDict and dataclass definitions for better type safety:
|
|
213
|
+
|
|
214
|
+
### Authentication Types
|
|
215
|
+
- `AuthData`: TypedDict containing provider and credentials information
|
|
216
|
+
- `Credentials`: TypedDict with access_token, org_sso_id, customer_id, instance_id, and product_sku
|
|
217
|
+
- `AuthRequirement`: Dataclass for specifying authentication requirements
|
|
218
|
+
|
|
219
|
+
### Execution Environment
|
|
220
|
+
- `Environment`: TypedDict specifying execution mode (`"headless"` or `"interactive"`)
|
|
221
|
+
|
|
222
|
+
### Parameter Types
|
|
223
|
+
- `ParameterType`: Enum for supported parameter types (string, integer, number, boolean, list, dictionary)
|
|
224
|
+
- `Parameter`: Dataclass for tool parameter definitions
|
|
225
|
+
- `Function`: Dataclass for complete tool function definitions
|
|
226
|
+
|
|
227
|
+
These types are automatically imported when you import from `opal_tools_sdk` and provide better IDE support and type checking.
|
|
228
|
+
|
|
229
|
+
## Documentation
|
|
230
|
+
|
|
231
|
+
See full documentation for more examples and configuration options.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from .service import ToolsService
|
|
2
|
+
from .decorators import tool
|
|
3
|
+
from .auth import requires_auth
|
|
4
|
+
from .logging import register_logger_factory
|
|
5
|
+
from .models import AuthData, AuthRequirement, Credentials, Environment, IslandConfig, IslandResponse
|
|
6
|
+
|
|
7
|
+
__version__ = "0.1.0"
|
|
8
|
+
__all__ = [
|
|
9
|
+
"ToolsService",
|
|
10
|
+
"tool",
|
|
11
|
+
"requires_auth",
|
|
12
|
+
"register_logger_factory",
|
|
13
|
+
# Models
|
|
14
|
+
"AuthData",
|
|
15
|
+
"AuthRequirement",
|
|
16
|
+
"Credentials",
|
|
17
|
+
"Environment",
|
|
18
|
+
"IslandConfig",
|
|
19
|
+
"IslandResponse",
|
|
20
|
+
]
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import inspect
|
|
2
2
|
import re
|
|
3
3
|
import logging
|
|
4
|
-
from typing import Callable, Any, List, Dict, Type, get_type_hints, Optional, Union
|
|
4
|
+
from typing import Callable, Any, List, Dict, Type, get_origin, get_type_hints, Optional, Union
|
|
5
5
|
from fastapi import APIRouter, Depends, Header, HTTPException
|
|
6
6
|
from pydantic import BaseModel
|
|
7
7
|
|
|
8
8
|
from .models import Parameter, ParameterType, AuthRequirement
|
|
9
|
+
from .logging import get_logger
|
|
9
10
|
|
|
10
|
-
logger =
|
|
11
|
+
logger = get_logger(__name__)
|
|
11
12
|
|
|
12
13
|
def tool(name: str, description: str, auth_requirements: Optional[List[Dict[str, Any]]] = None):
|
|
13
14
|
"""Decorator to register a function as an Opal tool.
|
|
@@ -61,21 +62,48 @@ def tool(name: str, description: str, auth_requirements: Optional[List[Dict[str,
|
|
|
61
62
|
else:
|
|
62
63
|
field_type = str
|
|
63
64
|
|
|
65
|
+
# Check if the field is Optional (Union with None)
|
|
66
|
+
# Optional[X] is equivalent to Union[X, None]
|
|
67
|
+
type_args = getattr(field_type, '__args__', ())
|
|
68
|
+
is_optional = get_origin(field_type) is Union and type(None) in type_args
|
|
69
|
+
|
|
70
|
+
# Extract the actual type from Optional[T]
|
|
71
|
+
if is_optional and type_args:
|
|
72
|
+
# Get the non-None type from Union[T, None]
|
|
73
|
+
field_type = next(
|
|
74
|
+
(arg for arg in type_args if arg is not type(None)),
|
|
75
|
+
field_type,
|
|
76
|
+
)
|
|
77
|
+
|
|
64
78
|
# Map Python type to Parameter type
|
|
65
79
|
param_type = ParameterType.string
|
|
66
|
-
if field_type
|
|
80
|
+
if field_type is int:
|
|
67
81
|
param_type = ParameterType.integer
|
|
68
|
-
elif field_type
|
|
82
|
+
elif field_type is float:
|
|
69
83
|
param_type = ParameterType.number
|
|
70
|
-
elif field_type
|
|
84
|
+
elif field_type is bool:
|
|
71
85
|
param_type = ParameterType.boolean
|
|
72
|
-
elif field_type
|
|
86
|
+
elif field_type is list or get_origin(field_type) is list:
|
|
73
87
|
param_type = ParameterType.list
|
|
74
|
-
elif field_type
|
|
88
|
+
elif field_type is dict or get_origin(field_type) is dict:
|
|
75
89
|
param_type = ParameterType.dictionary
|
|
76
90
|
|
|
77
91
|
# Determine if required
|
|
78
|
-
|
|
92
|
+
field_info_extra = getattr(field_info, "json_schema_extra") or {}
|
|
93
|
+
if "required" in field_info_extra:
|
|
94
|
+
required = field_info_extra["required"]
|
|
95
|
+
# If the field is typed as Optional, it's not required (check this FIRST)
|
|
96
|
+
elif is_optional:
|
|
97
|
+
required = False
|
|
98
|
+
# Check for Pydantic v2 is_required() method
|
|
99
|
+
elif hasattr(field_info, 'is_required'):
|
|
100
|
+
required = field_info.is_required()
|
|
101
|
+
# Fall back to checking if default is ... (Pydantic v1/v2 compatibility)
|
|
102
|
+
elif hasattr(field_info, 'default'):
|
|
103
|
+
required = field_info.default is ...
|
|
104
|
+
else:
|
|
105
|
+
# If no default attribute at all, assume required
|
|
106
|
+
required = True
|
|
79
107
|
|
|
80
108
|
# Get description
|
|
81
109
|
description_text = ""
|
|
@@ -91,9 +119,9 @@ def tool(name: str, description: str, auth_requirements: Optional[List[Dict[str,
|
|
|
91
119
|
required=required
|
|
92
120
|
))
|
|
93
121
|
|
|
94
|
-
|
|
122
|
+
logger.info(f"Registered parameter: {field_name} of type {param_type.value}, required: {required}")
|
|
95
123
|
else:
|
|
96
|
-
|
|
124
|
+
logger.warning(f"Warning: No parameter model found for {name}")
|
|
97
125
|
|
|
98
126
|
endpoint = f"/tools/{name}"
|
|
99
127
|
|
|
@@ -108,10 +136,10 @@ def tool(name: str, description: str, auth_requirements: Optional[List[Dict[str,
|
|
|
108
136
|
required=auth_req.get("required", True)
|
|
109
137
|
))
|
|
110
138
|
|
|
111
|
-
|
|
139
|
+
logger.info(f"Registering tool {name} with endpoint {endpoint}")
|
|
112
140
|
|
|
113
141
|
if not _registry.services:
|
|
114
|
-
|
|
142
|
+
logger.warning("No services registered in registry! Make sure to create ToolsService before decorating functions.")
|
|
115
143
|
|
|
116
144
|
for service in _registry.services:
|
|
117
145
|
service.register_tool(
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import sys
|
|
3
|
+
from typing import Optional, Callable
|
|
4
|
+
|
|
5
|
+
# Type alias for a logger factory function
|
|
6
|
+
type LoggerFactory = Callable[[Optional[str]], logging.Logger]
|
|
7
|
+
|
|
8
|
+
# Internal variable to hold a custom logger factory
|
|
9
|
+
_custom_logger_factory: Optional[LoggerFactory] = None
|
|
10
|
+
|
|
11
|
+
def register_logger_factory(factory: LoggerFactory):
|
|
12
|
+
"""
|
|
13
|
+
Register a custom logger factory function. This function should accept a name (str or None)
|
|
14
|
+
and return a logger instance (e.g., structlog or standard logger).
|
|
15
|
+
"""
|
|
16
|
+
global _custom_logger_factory
|
|
17
|
+
_custom_logger_factory = factory
|
|
18
|
+
|
|
19
|
+
def get_logger(name: str = None) -> logging.Logger:
|
|
20
|
+
"""
|
|
21
|
+
Returns a logger configured to output to the console, or uses a registered custom logger factory.
|
|
22
|
+
If no name is provided, uses the root logger.
|
|
23
|
+
"""
|
|
24
|
+
if _custom_logger_factory is not None:
|
|
25
|
+
return _custom_logger_factory(name)
|
|
26
|
+
logger = logging.getLogger(name)
|
|
27
|
+
if not logger.handlers:
|
|
28
|
+
handler = logging.StreamHandler(sys.stdout)
|
|
29
|
+
formatter = logging.Formatter(
|
|
30
|
+
fmt='%(asctime)s %(levelname)s [%(name)s] %(message)s',
|
|
31
|
+
datefmt='%Y-%m-%d %H:%M:%S'
|
|
32
|
+
)
|
|
33
|
+
handler.setFormatter(formatter)
|
|
34
|
+
logger.addHandler(handler)
|
|
35
|
+
logger.setLevel(logging.INFO)
|
|
36
|
+
logger.propagate = False
|
|
37
|
+
return logger
|