optimizely-opal.opal-tools-sdk 0.1.0.dev0__py3-none-any.whl

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.

Potentially problematic release.


This version of optimizely-opal.opal-tools-sdk might be problematic. Click here for more details.

@@ -0,0 +1,6 @@
1
+ from .service import ToolsService
2
+ from .decorators import tool
3
+ from .auth import requires_auth
4
+
5
+ __version__ = "0.1.0"
6
+ __all__ = ["ToolsService", "tool", "requires_auth"]
@@ -0,0 +1,3 @@
1
+ """Internal registry for ToolsService instances."""
2
+
3
+ services = []
opal_tools_sdk/auth.py ADDED
@@ -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,10 @@
1
+ opal_tools_sdk/__init__.py,sha256=Eu141xM3QVq1O_BLGZS9m_asU1b_KaOApFtwvaYll6E,170
2
+ opal_tools_sdk/_registry.py,sha256=YE3eD4kcS09QDe4RccBYAzXPo9znEU7fblrsB-g3o-Y,67
3
+ opal_tools_sdk/auth.py,sha256=9aMiZv6n6_iu7hQA0sKg4hgNr5DzYFFuP0SWUoZf_Vw,1520
4
+ opal_tools_sdk/decorators.py,sha256=a_CJcA11RW8eAt2xRGXZane_HvTZqR7wfhn-6WFgjOY,5057
5
+ opal_tools_sdk/models.py,sha256=fa2hhnZ2GTz6xqhGTNyqyIQj_rBE1UFQxsJ4KfApCiU,2123
6
+ opal_tools_sdk/service.py,sha256=tVaQKFpTX778UUWoczlM2WHSiUXqdYtYSKgTdqTH1Z0,6756
7
+ optimizely_opal_opal_tools_sdk-0.1.0.dev0.dist-info/METADATA,sha256=EoIwGaRzuaWjMlPwej56DdeTW6IcYtJDjttLigMu9ts,748
8
+ optimizely_opal_opal_tools_sdk-0.1.0.dev0.dist-info/WHEEL,sha256=0CuiUZ_p9E4cD6NyLD6UG80LBXYyiSYZOKDm5lp32xk,91
9
+ optimizely_opal_opal_tools_sdk-0.1.0.dev0.dist-info/top_level.txt,sha256=nCJ5PxF0rgoV6yNJvvuUaZPx4D3EWkl7gpu-6xafH1E,15
10
+ optimizely_opal_opal_tools_sdk-0.1.0.dev0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.3.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+