optimizely-opal.opal-tools-sdk 0.1.27.dev0__tar.gz → 0.1.30.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.27.dev0 → optimizely_opal_opal_tools_sdk-0.1.30.dev0}/PKG-INFO +190 -2
- {optimizely_opal_opal_tools_sdk-0.1.27.dev0 → optimizely_opal_opal_tools_sdk-0.1.30.dev0}/README.md +186 -0
- {optimizely_opal_opal_tools_sdk-0.1.27.dev0 → optimizely_opal_opal_tools_sdk-0.1.30.dev0}/opal_tools_sdk/__init__.py +11 -3
- {optimizely_opal_opal_tools_sdk-0.1.27.dev0 → optimizely_opal_opal_tools_sdk-0.1.30.dev0}/opal_tools_sdk/_registry.py +3 -1
- {optimizely_opal_opal_tools_sdk-0.1.27.dev0 → optimizely_opal_opal_tools_sdk-0.1.30.dev0}/opal_tools_sdk/auth.py +8 -9
- {optimizely_opal_opal_tools_sdk-0.1.27.dev0 → optimizely_opal_opal_tools_sdk-0.1.30.dev0}/opal_tools_sdk/decorators.py +101 -66
- {optimizely_opal_opal_tools_sdk-0.1.27.dev0 → optimizely_opal_opal_tools_sdk-0.1.30.dev0}/opal_tools_sdk/logging.py +11 -8
- {optimizely_opal_opal_tools_sdk-0.1.27.dev0 → optimizely_opal_opal_tools_sdk-0.1.30.dev0}/opal_tools_sdk/models.py +26 -19
- {optimizely_opal_opal_tools_sdk-0.1.27.dev0 → optimizely_opal_opal_tools_sdk-0.1.30.dev0}/opal_tools_sdk/proteus.py +1411 -1343
- {optimizely_opal_opal_tools_sdk-0.1.27.dev0 → optimizely_opal_opal_tools_sdk-0.1.30.dev0}/opal_tools_sdk/service.py +59 -34
- {optimizely_opal_opal_tools_sdk-0.1.27.dev0 → optimizely_opal_opal_tools_sdk-0.1.30.dev0}/optimizely_opal.opal_tools_sdk.egg-info/PKG-INFO +190 -2
- {optimizely_opal_opal_tools_sdk-0.1.27.dev0 → optimizely_opal_opal_tools_sdk-0.1.30.dev0}/optimizely_opal.opal_tools_sdk.egg-info/requires.txt +3 -1
- {optimizely_opal_opal_tools_sdk-0.1.27.dev0 → optimizely_opal_opal_tools_sdk-0.1.30.dev0}/pyproject.toml +30 -2
- {optimizely_opal_opal_tools_sdk-0.1.27.dev0 → optimizely_opal_opal_tools_sdk-0.1.30.dev0}/setup.py +2 -2
- {optimizely_opal_opal_tools_sdk-0.1.27.dev0 → optimizely_opal_opal_tools_sdk-0.1.30.dev0}/tests/test_integration.py +5 -6
- {optimizely_opal_opal_tools_sdk-0.1.27.dev0 → optimizely_opal_opal_tools_sdk-0.1.30.dev0}/tests/test_nested_schema.py +12 -9
- {optimizely_opal_opal_tools_sdk-0.1.27.dev0 → optimizely_opal_opal_tools_sdk-0.1.30.dev0}/tests/test_proteus.py +5 -5
- {optimizely_opal_opal_tools_sdk-0.1.27.dev0 → optimizely_opal_opal_tools_sdk-0.1.30.dev0}/opal_tools_sdk/ui.py +0 -0
- {optimizely_opal_opal_tools_sdk-0.1.27.dev0 → optimizely_opal_opal_tools_sdk-0.1.30.dev0}/optimizely_opal.opal_tools_sdk.egg-info/SOURCES.txt +0 -0
- {optimizely_opal_opal_tools_sdk-0.1.27.dev0 → optimizely_opal_opal_tools_sdk-0.1.30.dev0}/optimizely_opal.opal_tools_sdk.egg-info/dependency_links.txt +0 -0
- {optimizely_opal_opal_tools_sdk-0.1.27.dev0 → optimizely_opal_opal_tools_sdk-0.1.30.dev0}/optimizely_opal.opal_tools_sdk.egg-info/top_level.txt +0 -0
- {optimizely_opal_opal_tools_sdk-0.1.27.dev0 → optimizely_opal_opal_tools_sdk-0.1.30.dev0}/setup.cfg +0 -0
{optimizely_opal_opal_tools_sdk-0.1.27.dev0 → optimizely_opal_opal_tools_sdk-0.1.30.dev0}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: optimizely-opal.opal-tools-sdk
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.30.dev0
|
|
4
4
|
Summary: SDK for creating Opal-compatible tools services
|
|
5
5
|
Home-page: https://github.com/optimizely/opal-tools-sdk
|
|
6
6
|
Author: Optimizely
|
|
@@ -19,9 +19,11 @@ Requires-Dist: fastapi>=0.100.0
|
|
|
19
19
|
Requires-Dist: pydantic>=2.0.0
|
|
20
20
|
Requires-Dist: httpx>=0.24.1
|
|
21
21
|
Provides-Extra: dev
|
|
22
|
-
Requires-Dist: datamodel-code-generator>=0.
|
|
22
|
+
Requires-Dist: datamodel-code-generator>=0.56.0; extra == "dev"
|
|
23
23
|
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
24
24
|
Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
|
|
25
|
+
Requires-Dist: ruff>=0.4.0; extra == "dev"
|
|
26
|
+
Requires-Dist: mypy>=1.0.0; extra == "dev"
|
|
25
27
|
Dynamic: author
|
|
26
28
|
Dynamic: home-page
|
|
27
29
|
Dynamic: requires-python
|
|
@@ -144,6 +146,78 @@ async def get_email(parameters: EmailParameters, auth_data: Optional[AuthData] =
|
|
|
144
146
|
return {"emails": ["Email 1", "Email 2"]}
|
|
145
147
|
```
|
|
146
148
|
|
|
149
|
+
## Interactions
|
|
150
|
+
|
|
151
|
+
Interactions are app-only handlers — actions callable from the UI but hidden from the LLM. Use them when a tool surface needs to expose a button, form submit, or other follow-up action that the model should not be able to call. They follow the MCP "app-only tools" pattern (`visibility: ["app"]`) and are not listed in the `/discovery` endpoint.
|
|
152
|
+
|
|
153
|
+
**When to use `@interaction` vs `@tool`:**
|
|
154
|
+
|
|
155
|
+
- Use `@tool` when the LLM should be able to call the function on its own.
|
|
156
|
+
- Use `@interaction` when only the UI (action cards, islands) should call it — the model should never see it.
|
|
157
|
+
|
|
158
|
+
### Declaring an interaction
|
|
159
|
+
|
|
160
|
+
```python
|
|
161
|
+
from typing import Optional
|
|
162
|
+
from opal_tools_sdk import ToolsService, interaction, InteractionContext
|
|
163
|
+
from pydantic import BaseModel, Field
|
|
164
|
+
from fastapi import FastAPI
|
|
165
|
+
|
|
166
|
+
app = FastAPI()
|
|
167
|
+
tools_service = ToolsService(app)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
class TaskFormInput(BaseModel):
|
|
171
|
+
title: str = Field(description="Task title")
|
|
172
|
+
priority: str = Field(default="medium", description="Task priority")
|
|
173
|
+
assignee: Optional[str] = Field(default=None, description="Assignee email")
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
@interaction(
|
|
177
|
+
name="submit_task_form",
|
|
178
|
+
description="Handle task form submission",
|
|
179
|
+
)
|
|
180
|
+
async def handle_task_submission(parameters: TaskFormInput, context: InteractionContext):
|
|
181
|
+
# parameters is validated against TaskFormInput
|
|
182
|
+
# context.auth_data carries credentials when auth is configured
|
|
183
|
+
return {"task_id": "task-123", "message": f"Task '{parameters.title}' created"}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
The first parameter is introspected from the Pydantic model the same way `@tool` does it (typing the parameter as `dict` skips schema extraction). The second parameter is an `InteractionContext`.
|
|
187
|
+
|
|
188
|
+
### Handler signature & `InteractionContext`
|
|
189
|
+
|
|
190
|
+
`InteractionContext` carries the auth data resolved by TMS from the parent tool's `auth_requirements`:
|
|
191
|
+
|
|
192
|
+
```python
|
|
193
|
+
@dataclass
|
|
194
|
+
class InteractionContext:
|
|
195
|
+
auth_data: AuthData | None = None
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
If the parent tool did not declare auth requirements (or no credentials are available), `context.auth_data` is `None`.
|
|
199
|
+
|
|
200
|
+
### Generated endpoint
|
|
201
|
+
|
|
202
|
+
The SDK exposes a single shared endpoint for all interactions:
|
|
203
|
+
|
|
204
|
+
```
|
|
205
|
+
POST /interactions/execute
|
|
206
|
+
Content-Type: application/json
|
|
207
|
+
|
|
208
|
+
{
|
|
209
|
+
"name": "submit_task_form",
|
|
210
|
+
"parameters": {"title": "Ship docs", "priority": "high"},
|
|
211
|
+
"auth": {"provider": "...", "credentials": {...}}
|
|
212
|
+
}
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
The response is whatever the handler returns, serialized as JSON. Interactions are **not** listed in `/discovery`.
|
|
216
|
+
|
|
217
|
+
### Name uniqueness
|
|
218
|
+
|
|
219
|
+
Interaction names must be unique across both tools and interactions in the same service, since both can be exported as MCP tools. Registering an interaction with a name that conflicts with an existing tool or interaction raises `ValueError`.
|
|
220
|
+
|
|
147
221
|
## Island Components
|
|
148
222
|
|
|
149
223
|
The SDK includes Island components for creating interactive UI responses that allow users to input data and trigger actions.
|
|
@@ -235,6 +309,120 @@ The response wrapper for islands:
|
|
|
235
309
|
- Use `IslandResponse.create([islands])` to create responses
|
|
236
310
|
- Supports multiple islands per response
|
|
237
311
|
|
|
312
|
+
## Resources & Proteus UI
|
|
313
|
+
|
|
314
|
+
The SDK supports defining MCP resources that serve dynamic UI specifications using the Proteus framework. This enables tools to render rich, interactive interfaces without hardcoded frontend integrations.
|
|
315
|
+
|
|
316
|
+
For the full Proteus component reference and visual designer, see the [Proteus documentation](https://optimizely-axiom.github.io/optiaxiom/guides/proteus/).
|
|
317
|
+
|
|
318
|
+
### Defining a Resource with `@resource`
|
|
319
|
+
|
|
320
|
+
Use the `@resource` decorator to register a function as an MCP resource:
|
|
321
|
+
|
|
322
|
+
```python
|
|
323
|
+
from opal_tools_sdk import UI
|
|
324
|
+
from opal_tools_sdk.decorators import resource
|
|
325
|
+
|
|
326
|
+
@resource(
|
|
327
|
+
uri="ui://my-app/create-form",
|
|
328
|
+
name="create-form",
|
|
329
|
+
description="Form for creating new items",
|
|
330
|
+
)
|
|
331
|
+
async def get_create_form():
|
|
332
|
+
return UI.Document(
|
|
333
|
+
title="Create Item",
|
|
334
|
+
body=[
|
|
335
|
+
UI.Heading(children="New Item"),
|
|
336
|
+
UI.Field(
|
|
337
|
+
label="Item Name",
|
|
338
|
+
children=UI.Input(name="item_name", placeholder="Enter item name"),
|
|
339
|
+
),
|
|
340
|
+
UI.Field(
|
|
341
|
+
label="Description",
|
|
342
|
+
children=UI.Textarea(name="description", placeholder="Enter description"),
|
|
343
|
+
),
|
|
344
|
+
],
|
|
345
|
+
actions=[
|
|
346
|
+
UI.Action(children="Save", appearance="primary"),
|
|
347
|
+
UI.CancelAction(children="Cancel"),
|
|
348
|
+
],
|
|
349
|
+
)
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
**Parameters:**
|
|
353
|
+
- `uri` (required): Unique URI for the resource (e.g., `"ui://my-app/create-form"`)
|
|
354
|
+
- `name` (required): Name of the resource
|
|
355
|
+
- `description` (optional): Description of the resource
|
|
356
|
+
- `mime_type` (optional): MIME type of the content. Auto-set to `"application/vnd.opal.proteus+json"` when returning a `UI.Document`
|
|
357
|
+
- `title` (optional): Human-readable title
|
|
358
|
+
|
|
359
|
+
The handler function can return either a `str` (manual JSON serialization) or a `UI.Document` (automatic serialization with MIME type set automatically).
|
|
360
|
+
|
|
361
|
+
### Linking a Tool to a UI Resource
|
|
362
|
+
|
|
363
|
+
Use the `ui_resource` parameter on `@tool` to associate a tool with a Proteus UI resource. The frontend fetches and renders the resource when the tool is invoked:
|
|
364
|
+
|
|
365
|
+
```python
|
|
366
|
+
from opal_tools_sdk import tool
|
|
367
|
+
from pydantic import BaseModel, Field
|
|
368
|
+
|
|
369
|
+
class CreateItemParams(BaseModel):
|
|
370
|
+
item_name: str = Field(description="Name of the item")
|
|
371
|
+
description: str = Field(description="Item description")
|
|
372
|
+
|
|
373
|
+
@tool(
|
|
374
|
+
"create_item",
|
|
375
|
+
"Create a new item",
|
|
376
|
+
ui_resource="ui://my-app/create-form",
|
|
377
|
+
)
|
|
378
|
+
async def create_item(parameters: CreateItemParams):
|
|
379
|
+
return {"id": "item-123", "name": parameters.item_name, "status": "created"}
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
### Building UI with `UI.Document`
|
|
383
|
+
|
|
384
|
+
Import the `UI` namespace from `opal_tools_sdk` or `opal_tools_sdk.ui`. It provides type-safe builders for all Proteus components:
|
|
385
|
+
|
|
386
|
+
```python
|
|
387
|
+
from opal_tools_sdk import UI
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
**Available components:**
|
|
391
|
+
|
|
392
|
+
| Category | Components |
|
|
393
|
+
|----------|-----------|
|
|
394
|
+
| Layout | `UI.Document`, `UI.Group`, `UI.Card`, `UI.CardHeader`, `UI.CardLink`, `UI.Separator` |
|
|
395
|
+
| Typography | `UI.Heading`, `UI.Text`, `UI.Link` |
|
|
396
|
+
| Data Display | `UI.Avatar`, `UI.Badge`, `UI.DataTable`, `UI.Chart`, `UI.IconCalendar`, `UI.Image`, `UI.ImageCarousel`, `UI.Time` |
|
|
397
|
+
| Form Controls | `UI.Field`, `UI.Input`, `UI.Textarea`, `UI.Select`, `UI.SelectTrigger`, `UI.SelectContent`, `UI.Switch`, `UI.Range`, `UI.Question` |
|
|
398
|
+
| Actions | `UI.Action`, `UI.CancelAction` |
|
|
399
|
+
| Dynamic | `UI.Value`, `UI.Map`, `UI.MapIndex`, `UI.Show`, `UI.Concat`, `UI.Zip` |
|
|
400
|
+
|
|
401
|
+
**Data binding** with `UI.Value` resolves paths from the tool response:
|
|
402
|
+
|
|
403
|
+
```python
|
|
404
|
+
@resource(uri="ui://my-app/results", name="results")
|
|
405
|
+
async def get_results():
|
|
406
|
+
return UI.Document(
|
|
407
|
+
title=UI.Value(path="/title"),
|
|
408
|
+
body=UI.Map(
|
|
409
|
+
path="/items",
|
|
410
|
+
children=UI.Text(children=UI.Value(path="name")),
|
|
411
|
+
),
|
|
412
|
+
)
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
**Conditional rendering** with `UI.Show`:
|
|
416
|
+
|
|
417
|
+
```python
|
|
418
|
+
UI.Show(
|
|
419
|
+
when={"!!": UI.Value(path="/error")},
|
|
420
|
+
children=UI.Text(children="An error occurred", color="fg.error"),
|
|
421
|
+
)
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
The MIME type constant is available as `UI.MIME_TYPE` (`"application/vnd.opal.proteus+json"`).
|
|
425
|
+
|
|
238
426
|
## Type Definitions
|
|
239
427
|
|
|
240
428
|
The SDK provides several TypedDict and dataclass definitions for better type safety:
|
{optimizely_opal_opal_tools_sdk-0.1.27.dev0 → optimizely_opal_opal_tools_sdk-0.1.30.dev0}/README.md
RENAMED
|
@@ -116,6 +116,78 @@ async def get_email(parameters: EmailParameters, auth_data: Optional[AuthData] =
|
|
|
116
116
|
return {"emails": ["Email 1", "Email 2"]}
|
|
117
117
|
```
|
|
118
118
|
|
|
119
|
+
## Interactions
|
|
120
|
+
|
|
121
|
+
Interactions are app-only handlers — actions callable from the UI but hidden from the LLM. Use them when a tool surface needs to expose a button, form submit, or other follow-up action that the model should not be able to call. They follow the MCP "app-only tools" pattern (`visibility: ["app"]`) and are not listed in the `/discovery` endpoint.
|
|
122
|
+
|
|
123
|
+
**When to use `@interaction` vs `@tool`:**
|
|
124
|
+
|
|
125
|
+
- Use `@tool` when the LLM should be able to call the function on its own.
|
|
126
|
+
- Use `@interaction` when only the UI (action cards, islands) should call it — the model should never see it.
|
|
127
|
+
|
|
128
|
+
### Declaring an interaction
|
|
129
|
+
|
|
130
|
+
```python
|
|
131
|
+
from typing import Optional
|
|
132
|
+
from opal_tools_sdk import ToolsService, interaction, InteractionContext
|
|
133
|
+
from pydantic import BaseModel, Field
|
|
134
|
+
from fastapi import FastAPI
|
|
135
|
+
|
|
136
|
+
app = FastAPI()
|
|
137
|
+
tools_service = ToolsService(app)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class TaskFormInput(BaseModel):
|
|
141
|
+
title: str = Field(description="Task title")
|
|
142
|
+
priority: str = Field(default="medium", description="Task priority")
|
|
143
|
+
assignee: Optional[str] = Field(default=None, description="Assignee email")
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
@interaction(
|
|
147
|
+
name="submit_task_form",
|
|
148
|
+
description="Handle task form submission",
|
|
149
|
+
)
|
|
150
|
+
async def handle_task_submission(parameters: TaskFormInput, context: InteractionContext):
|
|
151
|
+
# parameters is validated against TaskFormInput
|
|
152
|
+
# context.auth_data carries credentials when auth is configured
|
|
153
|
+
return {"task_id": "task-123", "message": f"Task '{parameters.title}' created"}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
The first parameter is introspected from the Pydantic model the same way `@tool` does it (typing the parameter as `dict` skips schema extraction). The second parameter is an `InteractionContext`.
|
|
157
|
+
|
|
158
|
+
### Handler signature & `InteractionContext`
|
|
159
|
+
|
|
160
|
+
`InteractionContext` carries the auth data resolved by TMS from the parent tool's `auth_requirements`:
|
|
161
|
+
|
|
162
|
+
```python
|
|
163
|
+
@dataclass
|
|
164
|
+
class InteractionContext:
|
|
165
|
+
auth_data: AuthData | None = None
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
If the parent tool did not declare auth requirements (or no credentials are available), `context.auth_data` is `None`.
|
|
169
|
+
|
|
170
|
+
### Generated endpoint
|
|
171
|
+
|
|
172
|
+
The SDK exposes a single shared endpoint for all interactions:
|
|
173
|
+
|
|
174
|
+
```
|
|
175
|
+
POST /interactions/execute
|
|
176
|
+
Content-Type: application/json
|
|
177
|
+
|
|
178
|
+
{
|
|
179
|
+
"name": "submit_task_form",
|
|
180
|
+
"parameters": {"title": "Ship docs", "priority": "high"},
|
|
181
|
+
"auth": {"provider": "...", "credentials": {...}}
|
|
182
|
+
}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
The response is whatever the handler returns, serialized as JSON. Interactions are **not** listed in `/discovery`.
|
|
186
|
+
|
|
187
|
+
### Name uniqueness
|
|
188
|
+
|
|
189
|
+
Interaction names must be unique across both tools and interactions in the same service, since both can be exported as MCP tools. Registering an interaction with a name that conflicts with an existing tool or interaction raises `ValueError`.
|
|
190
|
+
|
|
119
191
|
## Island Components
|
|
120
192
|
|
|
121
193
|
The SDK includes Island components for creating interactive UI responses that allow users to input data and trigger actions.
|
|
@@ -207,6 +279,120 @@ The response wrapper for islands:
|
|
|
207
279
|
- Use `IslandResponse.create([islands])` to create responses
|
|
208
280
|
- Supports multiple islands per response
|
|
209
281
|
|
|
282
|
+
## Resources & Proteus UI
|
|
283
|
+
|
|
284
|
+
The SDK supports defining MCP resources that serve dynamic UI specifications using the Proteus framework. This enables tools to render rich, interactive interfaces without hardcoded frontend integrations.
|
|
285
|
+
|
|
286
|
+
For the full Proteus component reference and visual designer, see the [Proteus documentation](https://optimizely-axiom.github.io/optiaxiom/guides/proteus/).
|
|
287
|
+
|
|
288
|
+
### Defining a Resource with `@resource`
|
|
289
|
+
|
|
290
|
+
Use the `@resource` decorator to register a function as an MCP resource:
|
|
291
|
+
|
|
292
|
+
```python
|
|
293
|
+
from opal_tools_sdk import UI
|
|
294
|
+
from opal_tools_sdk.decorators import resource
|
|
295
|
+
|
|
296
|
+
@resource(
|
|
297
|
+
uri="ui://my-app/create-form",
|
|
298
|
+
name="create-form",
|
|
299
|
+
description="Form for creating new items",
|
|
300
|
+
)
|
|
301
|
+
async def get_create_form():
|
|
302
|
+
return UI.Document(
|
|
303
|
+
title="Create Item",
|
|
304
|
+
body=[
|
|
305
|
+
UI.Heading(children="New Item"),
|
|
306
|
+
UI.Field(
|
|
307
|
+
label="Item Name",
|
|
308
|
+
children=UI.Input(name="item_name", placeholder="Enter item name"),
|
|
309
|
+
),
|
|
310
|
+
UI.Field(
|
|
311
|
+
label="Description",
|
|
312
|
+
children=UI.Textarea(name="description", placeholder="Enter description"),
|
|
313
|
+
),
|
|
314
|
+
],
|
|
315
|
+
actions=[
|
|
316
|
+
UI.Action(children="Save", appearance="primary"),
|
|
317
|
+
UI.CancelAction(children="Cancel"),
|
|
318
|
+
],
|
|
319
|
+
)
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
**Parameters:**
|
|
323
|
+
- `uri` (required): Unique URI for the resource (e.g., `"ui://my-app/create-form"`)
|
|
324
|
+
- `name` (required): Name of the resource
|
|
325
|
+
- `description` (optional): Description of the resource
|
|
326
|
+
- `mime_type` (optional): MIME type of the content. Auto-set to `"application/vnd.opal.proteus+json"` when returning a `UI.Document`
|
|
327
|
+
- `title` (optional): Human-readable title
|
|
328
|
+
|
|
329
|
+
The handler function can return either a `str` (manual JSON serialization) or a `UI.Document` (automatic serialization with MIME type set automatically).
|
|
330
|
+
|
|
331
|
+
### Linking a Tool to a UI Resource
|
|
332
|
+
|
|
333
|
+
Use the `ui_resource` parameter on `@tool` to associate a tool with a Proteus UI resource. The frontend fetches and renders the resource when the tool is invoked:
|
|
334
|
+
|
|
335
|
+
```python
|
|
336
|
+
from opal_tools_sdk import tool
|
|
337
|
+
from pydantic import BaseModel, Field
|
|
338
|
+
|
|
339
|
+
class CreateItemParams(BaseModel):
|
|
340
|
+
item_name: str = Field(description="Name of the item")
|
|
341
|
+
description: str = Field(description="Item description")
|
|
342
|
+
|
|
343
|
+
@tool(
|
|
344
|
+
"create_item",
|
|
345
|
+
"Create a new item",
|
|
346
|
+
ui_resource="ui://my-app/create-form",
|
|
347
|
+
)
|
|
348
|
+
async def create_item(parameters: CreateItemParams):
|
|
349
|
+
return {"id": "item-123", "name": parameters.item_name, "status": "created"}
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
### Building UI with `UI.Document`
|
|
353
|
+
|
|
354
|
+
Import the `UI` namespace from `opal_tools_sdk` or `opal_tools_sdk.ui`. It provides type-safe builders for all Proteus components:
|
|
355
|
+
|
|
356
|
+
```python
|
|
357
|
+
from opal_tools_sdk import UI
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
**Available components:**
|
|
361
|
+
|
|
362
|
+
| Category | Components |
|
|
363
|
+
|----------|-----------|
|
|
364
|
+
| Layout | `UI.Document`, `UI.Group`, `UI.Card`, `UI.CardHeader`, `UI.CardLink`, `UI.Separator` |
|
|
365
|
+
| Typography | `UI.Heading`, `UI.Text`, `UI.Link` |
|
|
366
|
+
| Data Display | `UI.Avatar`, `UI.Badge`, `UI.DataTable`, `UI.Chart`, `UI.IconCalendar`, `UI.Image`, `UI.ImageCarousel`, `UI.Time` |
|
|
367
|
+
| Form Controls | `UI.Field`, `UI.Input`, `UI.Textarea`, `UI.Select`, `UI.SelectTrigger`, `UI.SelectContent`, `UI.Switch`, `UI.Range`, `UI.Question` |
|
|
368
|
+
| Actions | `UI.Action`, `UI.CancelAction` |
|
|
369
|
+
| Dynamic | `UI.Value`, `UI.Map`, `UI.MapIndex`, `UI.Show`, `UI.Concat`, `UI.Zip` |
|
|
370
|
+
|
|
371
|
+
**Data binding** with `UI.Value` resolves paths from the tool response:
|
|
372
|
+
|
|
373
|
+
```python
|
|
374
|
+
@resource(uri="ui://my-app/results", name="results")
|
|
375
|
+
async def get_results():
|
|
376
|
+
return UI.Document(
|
|
377
|
+
title=UI.Value(path="/title"),
|
|
378
|
+
body=UI.Map(
|
|
379
|
+
path="/items",
|
|
380
|
+
children=UI.Text(children=UI.Value(path="name")),
|
|
381
|
+
),
|
|
382
|
+
)
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
**Conditional rendering** with `UI.Show`:
|
|
386
|
+
|
|
387
|
+
```python
|
|
388
|
+
UI.Show(
|
|
389
|
+
when={"!!": UI.Value(path="/error")},
|
|
390
|
+
children=UI.Text(children="An error occurred", color="fg.error"),
|
|
391
|
+
)
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
The MIME type constant is available as `UI.MIME_TYPE` (`"application/vnd.opal.proteus+json"`).
|
|
395
|
+
|
|
210
396
|
## Type Definitions
|
|
211
397
|
|
|
212
398
|
The SDK provides several TypedDict and dataclass definitions for better type safety:
|
|
@@ -1,9 +1,17 @@
|
|
|
1
|
-
from .service import ToolsService
|
|
2
|
-
from .decorators import tool, resource, interaction
|
|
3
1
|
from .auth import requires_auth
|
|
2
|
+
from .decorators import interaction, resource, tool
|
|
4
3
|
from .logging import register_logger_factory
|
|
5
|
-
from .models import
|
|
4
|
+
from .models import (
|
|
5
|
+
AuthData,
|
|
6
|
+
AuthRequirement,
|
|
7
|
+
Credentials,
|
|
8
|
+
Environment,
|
|
9
|
+
InteractionContext,
|
|
10
|
+
IslandConfig,
|
|
11
|
+
IslandResponse,
|
|
12
|
+
)
|
|
6
13
|
from .proteus import UI
|
|
14
|
+
from .service import ToolsService
|
|
7
15
|
|
|
8
16
|
__version__ = "0.1.14-dev"
|
|
9
17
|
__all__ = [
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
from
|
|
1
|
+
from collections.abc import Callable
|
|
2
2
|
from functools import wraps
|
|
3
|
+
|
|
3
4
|
from fastapi import Header, HTTPException
|
|
4
5
|
|
|
6
|
+
|
|
5
7
|
def requires_auth(provider: str, scope_bundle: str, required: bool = True):
|
|
6
8
|
"""Decorator to indicate that a tool requires authentication.
|
|
7
9
|
|
|
@@ -13,9 +15,10 @@ def requires_auth(provider: str, scope_bundle: str, required: bool = True):
|
|
|
13
15
|
Returns:
|
|
14
16
|
Decorator function
|
|
15
17
|
"""
|
|
18
|
+
|
|
16
19
|
def decorator(func: Callable):
|
|
17
20
|
@wraps(func)
|
|
18
|
-
async def wrapper(*args, authorization:
|
|
21
|
+
async def wrapper(*args, authorization: str | None = Header(None), **kwargs):
|
|
19
22
|
if required and not authorization:
|
|
20
23
|
raise HTTPException(status_code=401, detail="Authentication required")
|
|
21
24
|
|
|
@@ -24,18 +27,14 @@ def requires_auth(provider: str, scope_bundle: str, required: bool = True):
|
|
|
24
27
|
return await func(*args, authorization=authorization, **kwargs)
|
|
25
28
|
|
|
26
29
|
# Store auth requirements in function metadata
|
|
27
|
-
auth_req = {
|
|
28
|
-
"provider": provider,
|
|
29
|
-
"scope_bundle": scope_bundle,
|
|
30
|
-
"required": required
|
|
31
|
-
}
|
|
30
|
+
auth_req = {"provider": provider, "scope_bundle": scope_bundle, "required": required}
|
|
32
31
|
|
|
33
32
|
# Initialize the list if it doesn't exist
|
|
34
33
|
if not hasattr(wrapper, "__auth_requirements__"):
|
|
35
|
-
wrapper.__auth_requirements__ = []
|
|
34
|
+
wrapper.__auth_requirements__ = [] # type: ignore[attr-defined]
|
|
36
35
|
|
|
37
36
|
# Add this auth requirement to the list
|
|
38
|
-
wrapper.__auth_requirements__.append(auth_req)
|
|
37
|
+
wrapper.__auth_requirements__.append(auth_req) # type: ignore[attr-defined]
|
|
39
38
|
|
|
40
39
|
return wrapper
|
|
41
40
|
|