optimizely-opal.opal-tools-sdk 0.1.0.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.0.dev0/PKG-INFO +24 -0
- optimizely_opal_opal_tools_sdk-0.1.0.dev0/README.md +114 -0
- optimizely_opal_opal_tools_sdk-0.1.0.dev0/opal_tools_sdk/__init__.py +6 -0
- optimizely_opal_opal_tools_sdk-0.1.0.dev0/opal_tools_sdk/_registry.py +3 -0
- optimizely_opal_opal_tools_sdk-0.1.0.dev0/opal_tools_sdk/auth.py +42 -0
- optimizely_opal_opal_tools_sdk-0.1.0.dev0/opal_tools_sdk/decorators.py +128 -0
- optimizely_opal_opal_tools_sdk-0.1.0.dev0/opal_tools_sdk/models.py +69 -0
- optimizely_opal_opal_tools_sdk-0.1.0.dev0/opal_tools_sdk/service.py +171 -0
- optimizely_opal_opal_tools_sdk-0.1.0.dev0/optimizely_opal.opal_tools_sdk.egg-info/PKG-INFO +24 -0
- optimizely_opal_opal_tools_sdk-0.1.0.dev0/optimizely_opal.opal_tools_sdk.egg-info/SOURCES.txt +13 -0
- optimizely_opal_opal_tools_sdk-0.1.0.dev0/optimizely_opal.opal_tools_sdk.egg-info/dependency_links.txt +1 -0
- optimizely_opal_opal_tools_sdk-0.1.0.dev0/optimizely_opal.opal_tools_sdk.egg-info/requires.txt +3 -0
- optimizely_opal_opal_tools_sdk-0.1.0.dev0/optimizely_opal.opal_tools_sdk.egg-info/top_level.txt +1 -0
- optimizely_opal_opal_tools_sdk-0.1.0.dev0/setup.cfg +4 -0
- optimizely_opal_opal_tools_sdk-0.1.0.dev0/setup.py +24 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: optimizely-opal.opal-tools-sdk
|
|
3
|
+
Version: 0.1.0.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: opal-team@optimizely.com
|
|
8
|
+
Keywords: opal,tools,sdk,ai,llm
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Requires-Python: >=3.10
|
|
14
|
+
Requires-Dist: fastapi>=0.100.0
|
|
15
|
+
Requires-Dist: pydantic>=2.0.0
|
|
16
|
+
Requires-Dist: httpx>=0.24.1
|
|
17
|
+
Dynamic: author
|
|
18
|
+
Dynamic: author-email
|
|
19
|
+
Dynamic: classifier
|
|
20
|
+
Dynamic: home-page
|
|
21
|
+
Dynamic: keywords
|
|
22
|
+
Dynamic: requires-dist
|
|
23
|
+
Dynamic: requires-python
|
|
24
|
+
Dynamic: summary
|
|
@@ -0,0 +1,114 @@
|
|
|
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
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pip install optimizely-opal.opal-tools-sdk
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Note: While the package is installed as `optimizely-opal.opal-tools-sdk`, you'll still import it in your code as `opal_tools_sdk`:
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
# Import using the package name
|
|
23
|
+
from opal_tools_sdk import ToolsService, tool
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Usage
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
from opal_tools_sdk import ToolsService, tool
|
|
30
|
+
from pydantic import BaseModel
|
|
31
|
+
from fastapi import FastAPI
|
|
32
|
+
|
|
33
|
+
app = FastAPI()
|
|
34
|
+
tools_service = ToolsService(app)
|
|
35
|
+
|
|
36
|
+
class WeatherParameters(BaseModel):
|
|
37
|
+
location: str
|
|
38
|
+
units: str = "metric"
|
|
39
|
+
|
|
40
|
+
@tool("get_weather", "Gets current weather for a location")
|
|
41
|
+
async def get_weather(parameters: WeatherParameters):
|
|
42
|
+
# Implementation...
|
|
43
|
+
return {"temperature": 22, "condition": "sunny"}
|
|
44
|
+
|
|
45
|
+
# Discovery endpoint is automatically created at /discovery
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Authentication
|
|
49
|
+
|
|
50
|
+
The SDK provides two ways to require authentication for your tools:
|
|
51
|
+
|
|
52
|
+
### 1. Using the `@requires_auth` decorator
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
from opal_tools_sdk import ToolsService, tool
|
|
56
|
+
from opal_tools_sdk.auth import requires_auth
|
|
57
|
+
from pydantic import BaseModel
|
|
58
|
+
from fastapi import FastAPI
|
|
59
|
+
|
|
60
|
+
app = FastAPI()
|
|
61
|
+
tools_service = ToolsService(app)
|
|
62
|
+
|
|
63
|
+
class CalendarParameters(BaseModel):
|
|
64
|
+
date: str
|
|
65
|
+
timezone: str = "UTC"
|
|
66
|
+
|
|
67
|
+
# Single authentication requirement
|
|
68
|
+
@requires_auth(provider="google", scope_bundle="calendar", required=True)
|
|
69
|
+
@tool("get_calendar_events", "Gets calendar events for a date")
|
|
70
|
+
async def get_calendar_events(parameters: CalendarParameters, auth_data=None):
|
|
71
|
+
# The auth_data parameter contains authentication information
|
|
72
|
+
token = auth_data.get("credentials", {}).get("token", "")
|
|
73
|
+
|
|
74
|
+
# Use the token to make authenticated requests
|
|
75
|
+
# ...
|
|
76
|
+
|
|
77
|
+
return {"events": ["Meeting at 10:00", "Lunch at 12:00"]}
|
|
78
|
+
|
|
79
|
+
# Multiple authentication requirements (tool can work with either provider)
|
|
80
|
+
@requires_auth(provider="google", scope_bundle="calendar", required=True)
|
|
81
|
+
@requires_auth(provider="microsoft", scope_bundle="outlook", required=True)
|
|
82
|
+
@tool("get_calendar_availability", "Check calendar availability")
|
|
83
|
+
async def get_calendar_availability(parameters: CalendarParameters, auth_data=None):
|
|
84
|
+
provider = auth_data.get("provider", "")
|
|
85
|
+
token = auth_data.get("credentials", {}).get("token", "")
|
|
86
|
+
|
|
87
|
+
if provider == "google":
|
|
88
|
+
# Use Google Calendar API
|
|
89
|
+
pass
|
|
90
|
+
elif provider == "microsoft":
|
|
91
|
+
# Use Microsoft Outlook API
|
|
92
|
+
pass
|
|
93
|
+
|
|
94
|
+
return {"available": True, "provider_used": provider}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### 2. Specifying auth requirements in the `@tool` decorator
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
@tool(
|
|
101
|
+
"get_email",
|
|
102
|
+
"Gets emails from the user's inbox",
|
|
103
|
+
auth_requirements=[
|
|
104
|
+
{"provider": "google", "scope_bundle": "gmail", "required": True}
|
|
105
|
+
]
|
|
106
|
+
)
|
|
107
|
+
async def get_email(parameters: EmailParameters, auth_data=None):
|
|
108
|
+
# Implementation...
|
|
109
|
+
return {"emails": ["Email 1", "Email 2"]}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Documentation
|
|
113
|
+
|
|
114
|
+
See full documentation for more examples and configuration options.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from typing import Callable, Dict, Any, Optional, List
|
|
2
|
+
from functools import wraps
|
|
3
|
+
from fastapi import Header, HTTPException
|
|
4
|
+
|
|
5
|
+
def requires_auth(provider: str, scope_bundle: str, required: bool = True):
|
|
6
|
+
"""Decorator to indicate that a tool requires authentication.
|
|
7
|
+
|
|
8
|
+
Args:
|
|
9
|
+
provider: Authentication provider (e.g., "google", "microsoft")
|
|
10
|
+
scope_bundle: Scope bundle required (e.g., "calendar", "drive")
|
|
11
|
+
required: Whether authentication is mandatory (default: True)
|
|
12
|
+
|
|
13
|
+
Returns:
|
|
14
|
+
Decorator function
|
|
15
|
+
"""
|
|
16
|
+
def decorator(func: Callable):
|
|
17
|
+
@wraps(func)
|
|
18
|
+
async def wrapper(*args, authorization: Optional[str] = Header(None), **kwargs):
|
|
19
|
+
if required and not authorization:
|
|
20
|
+
raise HTTPException(status_code=401, detail="Authentication required")
|
|
21
|
+
|
|
22
|
+
# The Tools Management Service will provide the appropriate token
|
|
23
|
+
# in the Authorization header
|
|
24
|
+
return await func(*args, authorization=authorization, **kwargs)
|
|
25
|
+
|
|
26
|
+
# Store auth requirements in function metadata
|
|
27
|
+
auth_req = {
|
|
28
|
+
"provider": provider,
|
|
29
|
+
"scope_bundle": scope_bundle,
|
|
30
|
+
"required": required
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
# Initialize the list if it doesn't exist
|
|
34
|
+
if not hasattr(wrapper, "__auth_requirements__"):
|
|
35
|
+
wrapper.__auth_requirements__ = []
|
|
36
|
+
|
|
37
|
+
# Add this auth requirement to the list
|
|
38
|
+
wrapper.__auth_requirements__.append(auth_req)
|
|
39
|
+
|
|
40
|
+
return wrapper
|
|
41
|
+
|
|
42
|
+
return decorator
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
import re
|
|
3
|
+
import logging
|
|
4
|
+
from typing import Callable, Any, List, Dict, Type, get_type_hints, Optional, Union
|
|
5
|
+
from fastapi import APIRouter, Depends, Header, HTTPException
|
|
6
|
+
from pydantic import BaseModel
|
|
7
|
+
|
|
8
|
+
from .models import Parameter, ParameterType, AuthRequirement
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger("opal_tools_sdk")
|
|
11
|
+
|
|
12
|
+
def tool(name: str, description: str, auth_requirements: Optional[List[Dict[str, Any]]] = None):
|
|
13
|
+
"""Decorator to register a function as an Opal tool.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
name: Name of the tool
|
|
17
|
+
description: Description of the tool
|
|
18
|
+
auth_requirements: Authentication requirements (optional)
|
|
19
|
+
Format: [{"provider": "oauth_provider", "scope_bundle": "permissions_scope", "required": True}, ...]
|
|
20
|
+
Example: [{"provider": "google", "scope_bundle": "calendar", "required": True}]
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
Decorator function
|
|
24
|
+
|
|
25
|
+
Note:
|
|
26
|
+
If your tool requires authentication, define your handler function with two parameters:
|
|
27
|
+
async def my_tool(parameters: ParametersModel, auth_data: Optional[Dict] = None):
|
|
28
|
+
...
|
|
29
|
+
"""
|
|
30
|
+
def decorator(func: Callable):
|
|
31
|
+
# Get the ToolsService instance from the global registry
|
|
32
|
+
from . import _registry
|
|
33
|
+
|
|
34
|
+
# Extract parameters from function signature
|
|
35
|
+
sig = inspect.signature(func)
|
|
36
|
+
type_hints = get_type_hints(func)
|
|
37
|
+
|
|
38
|
+
parameters: List[Parameter] = []
|
|
39
|
+
param_model = None
|
|
40
|
+
|
|
41
|
+
# Look for a parameter that is a pydantic model (for parameters)
|
|
42
|
+
for param_name, param in sig.parameters.items():
|
|
43
|
+
if param_name in type_hints:
|
|
44
|
+
param_type = type_hints[param_name]
|
|
45
|
+
if hasattr(param_type, '__fields__') or hasattr(param_type, 'model_fields'): # Pydantic v1 or v2
|
|
46
|
+
param_model = param_type
|
|
47
|
+
break
|
|
48
|
+
|
|
49
|
+
# If we found a pydantic model, extract parameters
|
|
50
|
+
if param_model:
|
|
51
|
+
model_fields = getattr(param_model, 'model_fields', getattr(param_model, '__fields__', {}))
|
|
52
|
+
for field_name, field in model_fields.items():
|
|
53
|
+
# Get field metadata
|
|
54
|
+
field_info = field.field_info if hasattr(field, 'field_info') else field
|
|
55
|
+
|
|
56
|
+
# Determine type
|
|
57
|
+
if hasattr(field, 'outer_type_'):
|
|
58
|
+
field_type = field.outer_type_
|
|
59
|
+
elif hasattr(field, 'annotation'):
|
|
60
|
+
field_type = field.annotation
|
|
61
|
+
else:
|
|
62
|
+
field_type = str
|
|
63
|
+
|
|
64
|
+
# Map Python type to Parameter type
|
|
65
|
+
param_type = ParameterType.string
|
|
66
|
+
if field_type == int:
|
|
67
|
+
param_type = ParameterType.integer
|
|
68
|
+
elif field_type == float:
|
|
69
|
+
param_type = ParameterType.number
|
|
70
|
+
elif field_type == bool:
|
|
71
|
+
param_type = ParameterType.boolean
|
|
72
|
+
elif field_type == list or field_type == List:
|
|
73
|
+
param_type = ParameterType.list
|
|
74
|
+
elif field_type == dict or field_type == Dict:
|
|
75
|
+
param_type = ParameterType.dictionary
|
|
76
|
+
|
|
77
|
+
# Determine if required
|
|
78
|
+
required = field_info.default is ... if hasattr(field_info, 'default') else True
|
|
79
|
+
|
|
80
|
+
# Get description
|
|
81
|
+
description_text = ""
|
|
82
|
+
if hasattr(field_info, 'description'):
|
|
83
|
+
description_text = field_info.description
|
|
84
|
+
elif hasattr(field, 'description'):
|
|
85
|
+
description_text = field.description
|
|
86
|
+
|
|
87
|
+
parameters.append(Parameter(
|
|
88
|
+
name=field_name,
|
|
89
|
+
param_type=param_type,
|
|
90
|
+
description=description_text,
|
|
91
|
+
required=required
|
|
92
|
+
))
|
|
93
|
+
|
|
94
|
+
print(f"Registered parameter: {field_name} of type {param_type.value}, required: {required}")
|
|
95
|
+
else:
|
|
96
|
+
print(f"Warning: No parameter model found for {name}")
|
|
97
|
+
|
|
98
|
+
endpoint = f"/tools/{name}"
|
|
99
|
+
|
|
100
|
+
# Register the tool with the service
|
|
101
|
+
auth_req_list = None
|
|
102
|
+
if auth_requirements:
|
|
103
|
+
auth_req_list = []
|
|
104
|
+
for auth_req in auth_requirements:
|
|
105
|
+
auth_req_list.append(AuthRequirement(
|
|
106
|
+
provider=auth_req.get("provider", ""),
|
|
107
|
+
scope_bundle=auth_req.get("scope_bundle", ""),
|
|
108
|
+
required=auth_req.get("required", True)
|
|
109
|
+
))
|
|
110
|
+
|
|
111
|
+
print(f"Registering tool {name} with endpoint {endpoint}")
|
|
112
|
+
|
|
113
|
+
if not _registry.services:
|
|
114
|
+
print("No services registered in registry! Make sure to create ToolsService before decorating functions.")
|
|
115
|
+
|
|
116
|
+
for service in _registry.services:
|
|
117
|
+
service.register_tool(
|
|
118
|
+
name=name,
|
|
119
|
+
description=description,
|
|
120
|
+
handler=func,
|
|
121
|
+
parameters=parameters,
|
|
122
|
+
endpoint=endpoint,
|
|
123
|
+
auth_requirements=auth_req_list
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
return func
|
|
127
|
+
|
|
128
|
+
return decorator
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
from typing import List, Dict, Any, Optional
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
class ParameterType(str, Enum):
|
|
6
|
+
"""Types of parameters supported by Opal tools."""
|
|
7
|
+
string = "string"
|
|
8
|
+
integer = "integer"
|
|
9
|
+
number = "number"
|
|
10
|
+
boolean = "boolean"
|
|
11
|
+
list = "list" # Changed to match main service expectation
|
|
12
|
+
dictionary = "object" # Standard JSON schema type
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class Parameter:
|
|
16
|
+
"""Parameter definition for an Opal tool."""
|
|
17
|
+
name: str
|
|
18
|
+
param_type: ParameterType
|
|
19
|
+
description: str
|
|
20
|
+
required: bool
|
|
21
|
+
|
|
22
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
23
|
+
"""Convert to dictionary for the discovery endpoint."""
|
|
24
|
+
return {
|
|
25
|
+
"name": self.name,
|
|
26
|
+
"type": self.param_type.value,
|
|
27
|
+
"description": self.description,
|
|
28
|
+
"required": self.required
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class AuthRequirement:
|
|
33
|
+
"""Authentication requirements for an Opal tool."""
|
|
34
|
+
provider: str # e.g., "google", "microsoft"
|
|
35
|
+
scope_bundle: str # e.g., "calendar", "drive"
|
|
36
|
+
required: bool = True
|
|
37
|
+
|
|
38
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
39
|
+
"""Convert to dictionary for the discovery endpoint."""
|
|
40
|
+
return {
|
|
41
|
+
"provider": self.provider,
|
|
42
|
+
"scope_bundle": self.scope_bundle,
|
|
43
|
+
"required": self.required
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
@dataclass
|
|
47
|
+
class Function:
|
|
48
|
+
"""Function definition for an Opal tool."""
|
|
49
|
+
name: str
|
|
50
|
+
description: str
|
|
51
|
+
parameters: List[Parameter]
|
|
52
|
+
endpoint: str
|
|
53
|
+
auth_requirements: Optional[List[AuthRequirement]] = None
|
|
54
|
+
http_method: str = "POST"
|
|
55
|
+
|
|
56
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
57
|
+
"""Convert to dictionary for the discovery endpoint."""
|
|
58
|
+
result = {
|
|
59
|
+
"name": self.name,
|
|
60
|
+
"description": self.description,
|
|
61
|
+
"parameters": [p.to_dict() for p in self.parameters],
|
|
62
|
+
"endpoint": self.endpoint,
|
|
63
|
+
"http_method": self.http_method
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if self.auth_requirements:
|
|
67
|
+
result["auth_requirements"] = [auth.to_dict() for auth in self.auth_requirements]
|
|
68
|
+
|
|
69
|
+
return result
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
from typing import Dict, List, Any, Callable, Type, Optional, get_type_hints
|
|
2
|
+
import inspect
|
|
3
|
+
import logging
|
|
4
|
+
from fastapi import FastAPI, APIRouter, Depends, Header, HTTPException, Request
|
|
5
|
+
from fastapi.routing import APIRoute
|
|
6
|
+
from pydantic import BaseModel, create_model
|
|
7
|
+
|
|
8
|
+
from .models import Function, Parameter, ParameterType, AuthRequirement
|
|
9
|
+
from . import _registry
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger("opal_tools_sdk")
|
|
12
|
+
|
|
13
|
+
class ToolsService:
|
|
14
|
+
"""Main class for managing Opal tools."""
|
|
15
|
+
|
|
16
|
+
def __init__(self, app: FastAPI):
|
|
17
|
+
"""Initialize the tools service.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
app: FastAPI application to attach routes to
|
|
21
|
+
"""
|
|
22
|
+
self.app = app
|
|
23
|
+
self.router = APIRouter()
|
|
24
|
+
self.functions: List[Function] = []
|
|
25
|
+
self._init_routes()
|
|
26
|
+
|
|
27
|
+
# Register in the global registry
|
|
28
|
+
_registry.services.append(self)
|
|
29
|
+
|
|
30
|
+
# Debug existing routes
|
|
31
|
+
@app.get("/debug-routes")
|
|
32
|
+
async def debug_routes():
|
|
33
|
+
routes = []
|
|
34
|
+
for route in app.routes:
|
|
35
|
+
if isinstance(route, APIRoute):
|
|
36
|
+
routes.append({
|
|
37
|
+
"path": route.path,
|
|
38
|
+
"name": route.name,
|
|
39
|
+
"methods": route.methods
|
|
40
|
+
})
|
|
41
|
+
return {"routes": routes}
|
|
42
|
+
|
|
43
|
+
def _init_routes(self) -> None:
|
|
44
|
+
"""Initialize the discovery endpoint."""
|
|
45
|
+
@self.router.get("/discovery")
|
|
46
|
+
async def discovery() -> Dict[str, Any]:
|
|
47
|
+
"""Return the discovery information for this tools service."""
|
|
48
|
+
return {"functions": [f.to_dict() for f in self.functions]}
|
|
49
|
+
|
|
50
|
+
# Include router in app
|
|
51
|
+
self.app.include_router(self.router)
|
|
52
|
+
|
|
53
|
+
def _extract_auth_requirements(self, handler: Callable) -> List[AuthRequirement]:
|
|
54
|
+
"""Extract auth requirements from a handler function decorated with @requires_auth.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
handler: The handler function
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
List of AuthRequirement objects
|
|
61
|
+
"""
|
|
62
|
+
auth_requirements = []
|
|
63
|
+
|
|
64
|
+
# Check if the function is wrapped with @requires_auth
|
|
65
|
+
if hasattr(handler, "__auth_requirements__"):
|
|
66
|
+
# Auth requirements should always be a list
|
|
67
|
+
if isinstance(handler.__auth_requirements__, list):
|
|
68
|
+
for req in handler.__auth_requirements__:
|
|
69
|
+
auth_requirements.append(AuthRequirement(
|
|
70
|
+
provider=req.get("provider", ""),
|
|
71
|
+
scope_bundle=req.get("scope_bundle", ""),
|
|
72
|
+
required=req.get("required", True)
|
|
73
|
+
))
|
|
74
|
+
|
|
75
|
+
return auth_requirements
|
|
76
|
+
|
|
77
|
+
def register_tool(self,
|
|
78
|
+
name: str,
|
|
79
|
+
description: str,
|
|
80
|
+
handler: Callable,
|
|
81
|
+
parameters: List[Parameter],
|
|
82
|
+
endpoint: str,
|
|
83
|
+
auth_requirements: Optional[List[AuthRequirement]] = None) -> None:
|
|
84
|
+
"""Register a tool function.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
name: Name of the tool
|
|
88
|
+
description: Description of the tool
|
|
89
|
+
handler: Function that implements the tool
|
|
90
|
+
parameters: List of parameters for the tool
|
|
91
|
+
endpoint: API endpoint for the tool
|
|
92
|
+
auth_requirements: List of authentication requirements (optional)
|
|
93
|
+
"""
|
|
94
|
+
print(f"Registering tool: {name} with endpoint: {endpoint}")
|
|
95
|
+
|
|
96
|
+
# Extract auth requirements from handler if decorated with @requires_auth
|
|
97
|
+
handler_auth_requirements = self._extract_auth_requirements(handler)
|
|
98
|
+
|
|
99
|
+
# If auth_requirements is explicitly provided, it takes precedence
|
|
100
|
+
# Otherwise, use the requirements extracted from the handler
|
|
101
|
+
final_auth_requirements = auth_requirements if auth_requirements else handler_auth_requirements
|
|
102
|
+
|
|
103
|
+
function = Function(
|
|
104
|
+
name=name,
|
|
105
|
+
description=description,
|
|
106
|
+
parameters=parameters,
|
|
107
|
+
endpoint=endpoint,
|
|
108
|
+
auth_requirements=final_auth_requirements
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
self.functions.append(function)
|
|
112
|
+
|
|
113
|
+
# Create a direct route with the app for better control
|
|
114
|
+
@self.app.post(endpoint)
|
|
115
|
+
async def tool_endpoint(request: Request):
|
|
116
|
+
try:
|
|
117
|
+
# Parse JSON body
|
|
118
|
+
body = await request.json()
|
|
119
|
+
print(f"Received request for {endpoint} with body: {body}")
|
|
120
|
+
|
|
121
|
+
# Parameters should be in the "parameters" key according to the spec
|
|
122
|
+
# This matches how the tools-mgmt-service calls tools
|
|
123
|
+
if "parameters" in body:
|
|
124
|
+
params = body["parameters"]
|
|
125
|
+
else:
|
|
126
|
+
# For backward compatibility with direct test calls
|
|
127
|
+
print(f"Warning: 'parameters' key not found in request body. Using body directly.")
|
|
128
|
+
params = body
|
|
129
|
+
|
|
130
|
+
# Extract auth data if available
|
|
131
|
+
auth_data = body.get("auth")
|
|
132
|
+
if auth_data:
|
|
133
|
+
print(f"Auth data provided for provider: {auth_data.get('provider', 'unknown')}")
|
|
134
|
+
|
|
135
|
+
print(f"Extracted parameters: {params}")
|
|
136
|
+
|
|
137
|
+
# Get the parameter model from handler's signature
|
|
138
|
+
sig = inspect.signature(handler)
|
|
139
|
+
param_name = list(sig.parameters.keys())[0]
|
|
140
|
+
param_type = get_type_hints(handler).get(param_name)
|
|
141
|
+
|
|
142
|
+
# Check signature to see if it accepts auth data
|
|
143
|
+
accepts_auth = len(sig.parameters) > 1
|
|
144
|
+
|
|
145
|
+
if param_type:
|
|
146
|
+
# Create instance of param model
|
|
147
|
+
model_instance = param_type(**params)
|
|
148
|
+
if accepts_auth:
|
|
149
|
+
# Call with auth data if the handler accepts it
|
|
150
|
+
result = await handler(model_instance, auth_data)
|
|
151
|
+
else:
|
|
152
|
+
# Call without auth data
|
|
153
|
+
result = await handler(model_instance)
|
|
154
|
+
else:
|
|
155
|
+
# Fall back if type hints not available
|
|
156
|
+
if accepts_auth:
|
|
157
|
+
result = await handler(BaseModel(**params), auth_data)
|
|
158
|
+
else:
|
|
159
|
+
result = await handler(BaseModel(**params))
|
|
160
|
+
|
|
161
|
+
print(f"Tool {name} returned: {result}")
|
|
162
|
+
return result
|
|
163
|
+
except Exception as e:
|
|
164
|
+
import traceback
|
|
165
|
+
print(f"Error in tool {name}: {str(e)}")
|
|
166
|
+
print(traceback.format_exc())
|
|
167
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
168
|
+
|
|
169
|
+
# Update the route function name and docstring
|
|
170
|
+
tool_endpoint.__name__ = f"tool_{name}"
|
|
171
|
+
tool_endpoint.__doc__ = description
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: optimizely-opal.opal-tools-sdk
|
|
3
|
+
Version: 0.1.0.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: opal-team@optimizely.com
|
|
8
|
+
Keywords: opal,tools,sdk,ai,llm
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Requires-Python: >=3.10
|
|
14
|
+
Requires-Dist: fastapi>=0.100.0
|
|
15
|
+
Requires-Dist: pydantic>=2.0.0
|
|
16
|
+
Requires-Dist: httpx>=0.24.1
|
|
17
|
+
Dynamic: author
|
|
18
|
+
Dynamic: author-email
|
|
19
|
+
Dynamic: classifier
|
|
20
|
+
Dynamic: home-page
|
|
21
|
+
Dynamic: keywords
|
|
22
|
+
Dynamic: requires-dist
|
|
23
|
+
Dynamic: requires-python
|
|
24
|
+
Dynamic: summary
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
setup.py
|
|
3
|
+
opal_tools_sdk/__init__.py
|
|
4
|
+
opal_tools_sdk/_registry.py
|
|
5
|
+
opal_tools_sdk/auth.py
|
|
6
|
+
opal_tools_sdk/decorators.py
|
|
7
|
+
opal_tools_sdk/models.py
|
|
8
|
+
opal_tools_sdk/service.py
|
|
9
|
+
optimizely_opal.opal_tools_sdk.egg-info/PKG-INFO
|
|
10
|
+
optimizely_opal.opal_tools_sdk.egg-info/SOURCES.txt
|
|
11
|
+
optimizely_opal.opal_tools_sdk.egg-info/dependency_links.txt
|
|
12
|
+
optimizely_opal.opal_tools_sdk.egg-info/requires.txt
|
|
13
|
+
optimizely_opal.opal_tools_sdk.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
optimizely_opal_opal_tools_sdk-0.1.0.dev0/optimizely_opal.opal_tools_sdk.egg-info/top_level.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
opal_tools_sdk
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from setuptools import setup, find_packages
|
|
2
|
+
|
|
3
|
+
setup(
|
|
4
|
+
name="optimizely-opal.opal-tools-sdk",
|
|
5
|
+
version="0.1.0-dev",
|
|
6
|
+
packages=find_packages(),
|
|
7
|
+
install_requires=[
|
|
8
|
+
"fastapi>=0.100.0",
|
|
9
|
+
"pydantic>=2.0.0",
|
|
10
|
+
"httpx>=0.24.1",
|
|
11
|
+
],
|
|
12
|
+
author="Optimizely",
|
|
13
|
+
author_email="opal-team@optimizely.com",
|
|
14
|
+
description="SDK for creating Opal-compatible tools services",
|
|
15
|
+
keywords="opal, tools, sdk, ai, llm",
|
|
16
|
+
url="https://github.com/optimizely/opal-tools-sdk",
|
|
17
|
+
classifiers=[
|
|
18
|
+
"Development Status :: 3 - Alpha",
|
|
19
|
+
"Intended Audience :: Developers",
|
|
20
|
+
"Programming Language :: Python :: 3.10",
|
|
21
|
+
"License :: OSI Approved :: MIT License",
|
|
22
|
+
],
|
|
23
|
+
python_requires=">=3.10",
|
|
24
|
+
)
|