signalwire-agents 0.1.37__py3-none-any.whl → 0.1.38__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.
- signalwire_agents/__init__.py +1 -1
- signalwire_agents/cli/build_search.py +95 -19
- signalwire_agents/core/agent_base.py +38 -0
- signalwire_agents/core/mixins/ai_config_mixin.py +120 -0
- signalwire_agents/core/skill_manager.py +47 -0
- signalwire_agents/search/index_builder.py +105 -10
- signalwire_agents/search/pgvector_backend.py +523 -0
- signalwire_agents/search/search_engine.py +41 -4
- signalwire_agents/search/search_service.py +86 -35
- signalwire_agents/skills/api_ninjas_trivia/skill.py +37 -1
- signalwire_agents/skills/datasphere/skill.py +82 -0
- signalwire_agents/skills/datasphere_serverless/skill.py +82 -0
- signalwire_agents/skills/joke/skill.py +21 -0
- signalwire_agents/skills/mcp_gateway/skill.py +82 -0
- signalwire_agents/skills/native_vector_search/README.md +210 -0
- signalwire_agents/skills/native_vector_search/skill.py +197 -7
- signalwire_agents/skills/play_background_file/skill.py +36 -0
- signalwire_agents/skills/registry.py +36 -0
- signalwire_agents/skills/spider/skill.py +113 -0
- signalwire_agents/skills/swml_transfer/skill.py +90 -0
- signalwire_agents/skills/weather_api/skill.py +28 -0
- signalwire_agents/skills/wikipedia_search/skill.py +22 -0
- {signalwire_agents-0.1.37.dist-info → signalwire_agents-0.1.38.dist-info}/METADATA +53 -1
- {signalwire_agents-0.1.37.dist-info → signalwire_agents-0.1.38.dist-info}/RECORD +28 -26
- {signalwire_agents-0.1.37.dist-info → signalwire_agents-0.1.38.dist-info}/WHEEL +0 -0
- {signalwire_agents-0.1.37.dist-info → signalwire_agents-0.1.38.dist-info}/entry_points.txt +0 -0
- {signalwire_agents-0.1.37.dist-info → signalwire_agents-0.1.38.dist-info}/licenses/LICENSE +0 -0
- {signalwire_agents-0.1.37.dist-info → signalwire_agents-0.1.38.dist-info}/top_level.txt +0 -0
@@ -82,16 +82,21 @@ else:
|
|
82
82
|
self.query_analysis = query_analysis
|
83
83
|
|
84
84
|
class SearchService:
|
85
|
-
"""Local search service with HTTP API"""
|
85
|
+
"""Local search service with HTTP API supporting both SQLite and pgvector backends"""
|
86
86
|
|
87
87
|
def __init__(self, port: int = 8001, indexes: Dict[str, str] = None,
|
88
88
|
basic_auth: Optional[Tuple[str, str]] = None,
|
89
|
-
config_file: Optional[str] = None
|
89
|
+
config_file: Optional[str] = None,
|
90
|
+
backend: str = 'sqlite',
|
91
|
+
connection_string: Optional[str] = None):
|
90
92
|
# Load configuration first
|
91
93
|
self._load_config(config_file)
|
92
94
|
|
93
95
|
# Override with constructor params if provided
|
94
96
|
self.port = port
|
97
|
+
self.backend = backend
|
98
|
+
self.connection_string = connection_string
|
99
|
+
|
95
100
|
if indexes is not None:
|
96
101
|
self.indexes = indexes
|
97
102
|
|
@@ -119,6 +124,8 @@ class SearchService:
|
|
119
124
|
"""Load configuration from file if available"""
|
120
125
|
# Initialize defaults
|
121
126
|
self.indexes = {}
|
127
|
+
self.backend = 'sqlite'
|
128
|
+
self.connection_string = None
|
122
129
|
|
123
130
|
# Find config file
|
124
131
|
if not config_file:
|
@@ -140,6 +147,12 @@ class SearchService:
|
|
140
147
|
if 'port' in service_config:
|
141
148
|
self.port = int(service_config['port'])
|
142
149
|
|
150
|
+
if 'backend' in service_config:
|
151
|
+
self.backend = service_config['backend']
|
152
|
+
|
153
|
+
if 'connection_string' in service_config:
|
154
|
+
self.connection_string = service_config['connection_string']
|
155
|
+
|
143
156
|
if 'indexes' in service_config and isinstance(service_config['indexes'], dict):
|
144
157
|
self.indexes = service_config['indexes']
|
145
158
|
|
@@ -225,9 +238,11 @@ class SearchService:
|
|
225
238
|
async def health():
|
226
239
|
return {
|
227
240
|
"status": "healthy",
|
241
|
+
"backend": self.backend,
|
228
242
|
"indexes": list(self.indexes.keys()),
|
229
243
|
"ssl_enabled": self.security.ssl_enabled,
|
230
|
-
"auth_required": bool(security)
|
244
|
+
"auth_required": bool(security),
|
245
|
+
"connection_string": self.connection_string if self.backend == 'pgvector' else None
|
231
246
|
}
|
232
247
|
|
233
248
|
@self.app.post("/reload_index")
|
@@ -236,47 +251,83 @@ class SearchService:
|
|
236
251
|
index_path: str,
|
237
252
|
credentials: HTTPBasicCredentials = None if not security else Depends(security)
|
238
253
|
):
|
239
|
-
"""Reload or add new index"""
|
254
|
+
"""Reload or add new index/collection"""
|
240
255
|
if security:
|
241
256
|
self._get_current_username(credentials)
|
242
|
-
|
243
|
-
self.
|
244
|
-
|
245
|
-
|
257
|
+
|
258
|
+
if self.backend == 'pgvector':
|
259
|
+
# For pgvector, index_path is actually the collection name
|
260
|
+
self.indexes[index_name] = index_path
|
261
|
+
try:
|
262
|
+
self.search_engines[index_name] = SearchEngine(
|
263
|
+
backend='pgvector',
|
264
|
+
connection_string=self.connection_string,
|
265
|
+
collection_name=index_path
|
266
|
+
)
|
267
|
+
return {"status": "reloaded", "index": index_name, "backend": "pgvector"}
|
268
|
+
except Exception as e:
|
269
|
+
raise HTTPException(status_code=500, detail=f"Failed to load pgvector collection: {e}")
|
270
|
+
else:
|
271
|
+
# SQLite backend
|
272
|
+
self.indexes[index_name] = index_path
|
273
|
+
self.search_engines[index_name] = SearchEngine(index_path, self.model)
|
274
|
+
return {"status": "reloaded", "index": index_name, "backend": "sqlite"}
|
246
275
|
|
247
276
|
def _load_resources(self):
|
248
277
|
"""Load embedding model and search indexes"""
|
249
|
-
|
250
|
-
|
251
|
-
#
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
278
|
+
if self.backend == 'pgvector':
|
279
|
+
# For pgvector, we don't need to load a model locally
|
280
|
+
# The embeddings are already stored in the database
|
281
|
+
# Load search engines for each collection
|
282
|
+
for collection_name in self.indexes.keys():
|
283
|
+
try:
|
284
|
+
self.search_engines[collection_name] = SearchEngine(
|
285
|
+
backend='pgvector',
|
286
|
+
connection_string=self.connection_string,
|
287
|
+
collection_name=collection_name
|
288
|
+
)
|
289
|
+
logger.info(f"Loaded pgvector collection: {collection_name}")
|
290
|
+
except Exception as e:
|
291
|
+
logger.error(f"Error loading pgvector collection {collection_name}: {e}")
|
292
|
+
else:
|
293
|
+
# SQLite backend - original behavior
|
294
|
+
# Load model (shared across all indexes)
|
295
|
+
if self.indexes and SentenceTransformer:
|
296
|
+
# Get model name from first index
|
297
|
+
sample_index = next(iter(self.indexes.values()))
|
298
|
+
model_name = self._get_model_name(sample_index)
|
299
|
+
try:
|
300
|
+
self.model = SentenceTransformer(model_name)
|
301
|
+
except Exception as e:
|
302
|
+
logger.warning(f"Could not load sentence transformer model: {e}")
|
303
|
+
self.model = None
|
304
|
+
|
305
|
+
# Load search engines for each index
|
306
|
+
for index_name, index_path in self.indexes.items():
|
307
|
+
try:
|
308
|
+
self.search_engines[index_name] = SearchEngine(index_path, self.model)
|
309
|
+
except Exception as e:
|
310
|
+
logger.error(f"Error loading search engine for {index_name}: {e}")
|
266
311
|
|
267
312
|
def _get_model_name(self, index_path: str) -> str:
|
268
313
|
"""Get embedding model name from index config"""
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
cursor = conn.cursor()
|
273
|
-
cursor.execute("SELECT value FROM config WHERE key = 'embedding_model'")
|
274
|
-
result = cursor.fetchone()
|
275
|
-
conn.close()
|
276
|
-
return result[0] if result else 'sentence-transformers/all-mpnet-base-v2'
|
277
|
-
except Exception as e:
|
278
|
-
logger.warning(f"Could not get model name from index: {e}")
|
314
|
+
if self.backend == 'pgvector':
|
315
|
+
# For pgvector, we might want to store model info in the database
|
316
|
+
# For now, return default model
|
279
317
|
return 'sentence-transformers/all-mpnet-base-v2'
|
318
|
+
else:
|
319
|
+
# SQLite backend
|
320
|
+
try:
|
321
|
+
import sqlite3
|
322
|
+
conn = sqlite3.connect(index_path)
|
323
|
+
cursor = conn.cursor()
|
324
|
+
cursor.execute("SELECT value FROM config WHERE key = 'embedding_model'")
|
325
|
+
result = cursor.fetchone()
|
326
|
+
conn.close()
|
327
|
+
return result[0] if result else 'sentence-transformers/all-mpnet-base-v2'
|
328
|
+
except Exception as e:
|
329
|
+
logger.warning(f"Could not get model name from index: {e}")
|
330
|
+
return 'sentence-transformers/all-mpnet-base-v2'
|
280
331
|
|
281
332
|
async def _handle_search(self, request: SearchRequest) -> SearchResponse:
|
282
333
|
"""Handle search request"""
|
@@ -198,4 +198,40 @@ class ApiNinjasTriviaSkill(SkillBase):
|
|
198
198
|
}
|
199
199
|
}
|
200
200
|
|
201
|
-
return [tool]
|
201
|
+
return [tool]
|
202
|
+
|
203
|
+
@classmethod
|
204
|
+
def get_parameter_schema(cls) -> Dict[str, Dict[str, Any]]:
|
205
|
+
"""
|
206
|
+
Get the parameter schema for the API Ninjas Trivia skill.
|
207
|
+
|
208
|
+
Returns parameter definitions for GUI configuration.
|
209
|
+
"""
|
210
|
+
schema = super().get_parameter_schema()
|
211
|
+
|
212
|
+
# Build categories enum description
|
213
|
+
category_options = []
|
214
|
+
for key, desc in cls.VALID_CATEGORIES.items():
|
215
|
+
category_options.append(f"{key} ({desc})")
|
216
|
+
|
217
|
+
schema.update({
|
218
|
+
"api_key": {
|
219
|
+
"type": "string",
|
220
|
+
"description": "API Ninjas API key",
|
221
|
+
"required": True,
|
222
|
+
"hidden": True,
|
223
|
+
"env_var": "API_NINJAS_KEY"
|
224
|
+
},
|
225
|
+
"categories": {
|
226
|
+
"type": "array",
|
227
|
+
"description": "List of trivia categories to enable. Available: " + ", ".join(category_options),
|
228
|
+
"default": list(cls.VALID_CATEGORIES.keys()),
|
229
|
+
"required": False,
|
230
|
+
"items": {
|
231
|
+
"type": "string",
|
232
|
+
"enum": list(cls.VALID_CATEGORIES.keys())
|
233
|
+
}
|
234
|
+
}
|
235
|
+
})
|
236
|
+
|
237
|
+
return schema
|
@@ -26,6 +26,88 @@ class DataSphereSkill(SkillBase):
|
|
26
26
|
# Enable multiple instances support
|
27
27
|
SUPPORTS_MULTIPLE_INSTANCES = True
|
28
28
|
|
29
|
+
@classmethod
|
30
|
+
def get_parameter_schema(cls) -> Dict[str, Dict[str, Any]]:
|
31
|
+
"""Get parameter schema for DataSphere skill"""
|
32
|
+
schema = super().get_parameter_schema()
|
33
|
+
schema.update({
|
34
|
+
"space_name": {
|
35
|
+
"type": "string",
|
36
|
+
"description": "SignalWire space name (e.g., 'mycompany' from mycompany.signalwire.com)",
|
37
|
+
"required": True
|
38
|
+
},
|
39
|
+
"project_id": {
|
40
|
+
"type": "string",
|
41
|
+
"description": "SignalWire project ID",
|
42
|
+
"required": True,
|
43
|
+
"env_var": "SIGNALWIRE_PROJECT_ID"
|
44
|
+
},
|
45
|
+
"token": {
|
46
|
+
"type": "string",
|
47
|
+
"description": "SignalWire API token",
|
48
|
+
"required": True,
|
49
|
+
"hidden": True,
|
50
|
+
"env_var": "SIGNALWIRE_TOKEN"
|
51
|
+
},
|
52
|
+
"document_id": {
|
53
|
+
"type": "string",
|
54
|
+
"description": "DataSphere document ID to search within",
|
55
|
+
"required": True
|
56
|
+
},
|
57
|
+
"count": {
|
58
|
+
"type": "integer",
|
59
|
+
"description": "Number of search results to return",
|
60
|
+
"default": 1,
|
61
|
+
"required": False,
|
62
|
+
"minimum": 1,
|
63
|
+
"maximum": 10
|
64
|
+
},
|
65
|
+
"distance": {
|
66
|
+
"type": "number",
|
67
|
+
"description": "Maximum distance threshold for results (lower is more relevant)",
|
68
|
+
"default": 3.0,
|
69
|
+
"required": False,
|
70
|
+
"minimum": 0.0,
|
71
|
+
"maximum": 10.0
|
72
|
+
},
|
73
|
+
"tags": {
|
74
|
+
"type": "array",
|
75
|
+
"description": "Tags to filter search results",
|
76
|
+
"required": False,
|
77
|
+
"items": {
|
78
|
+
"type": "string"
|
79
|
+
}
|
80
|
+
},
|
81
|
+
"language": {
|
82
|
+
"type": "string",
|
83
|
+
"description": "Language code for query expansion (e.g., 'en', 'es')",
|
84
|
+
"required": False
|
85
|
+
},
|
86
|
+
"pos_to_expand": {
|
87
|
+
"type": "array",
|
88
|
+
"description": "Parts of speech to expand with synonyms",
|
89
|
+
"required": False,
|
90
|
+
"items": {
|
91
|
+
"type": "string",
|
92
|
+
"enum": ["NOUN", "VERB", "ADJ", "ADV"]
|
93
|
+
}
|
94
|
+
},
|
95
|
+
"max_synonyms": {
|
96
|
+
"type": "integer",
|
97
|
+
"description": "Maximum number of synonyms to use for query expansion",
|
98
|
+
"required": False,
|
99
|
+
"minimum": 1,
|
100
|
+
"maximum": 10
|
101
|
+
},
|
102
|
+
"no_results_message": {
|
103
|
+
"type": "string",
|
104
|
+
"description": "Message to return when no results are found",
|
105
|
+
"default": "I couldn't find any relevant information for '{query}' in the knowledge base. Try rephrasing your question or asking about a different topic.",
|
106
|
+
"required": False
|
107
|
+
}
|
108
|
+
})
|
109
|
+
return schema
|
110
|
+
|
29
111
|
def get_instance_key(self) -> str:
|
30
112
|
"""
|
31
113
|
Get the key used to track this skill instance
|
@@ -26,6 +26,88 @@ class DataSphereServerlessSkill(SkillBase):
|
|
26
26
|
# Enable multiple instances support
|
27
27
|
SUPPORTS_MULTIPLE_INSTANCES = True
|
28
28
|
|
29
|
+
@classmethod
|
30
|
+
def get_parameter_schema(cls) -> Dict[str, Dict[str, Any]]:
|
31
|
+
"""Get parameter schema for DataSphere Serverless skill"""
|
32
|
+
schema = super().get_parameter_schema()
|
33
|
+
schema.update({
|
34
|
+
"space_name": {
|
35
|
+
"type": "string",
|
36
|
+
"description": "SignalWire space name (e.g., 'mycompany' from mycompany.signalwire.com)",
|
37
|
+
"required": True
|
38
|
+
},
|
39
|
+
"project_id": {
|
40
|
+
"type": "string",
|
41
|
+
"description": "SignalWire project ID",
|
42
|
+
"required": True,
|
43
|
+
"env_var": "SIGNALWIRE_PROJECT_ID"
|
44
|
+
},
|
45
|
+
"token": {
|
46
|
+
"type": "string",
|
47
|
+
"description": "SignalWire API token",
|
48
|
+
"required": True,
|
49
|
+
"hidden": True,
|
50
|
+
"env_var": "SIGNALWIRE_TOKEN"
|
51
|
+
},
|
52
|
+
"document_id": {
|
53
|
+
"type": "string",
|
54
|
+
"description": "DataSphere document ID to search within",
|
55
|
+
"required": True
|
56
|
+
},
|
57
|
+
"count": {
|
58
|
+
"type": "integer",
|
59
|
+
"description": "Number of search results to return",
|
60
|
+
"default": 1,
|
61
|
+
"required": False,
|
62
|
+
"minimum": 1,
|
63
|
+
"maximum": 10
|
64
|
+
},
|
65
|
+
"distance": {
|
66
|
+
"type": "number",
|
67
|
+
"description": "Maximum distance threshold for results (lower is more relevant)",
|
68
|
+
"default": 3.0,
|
69
|
+
"required": False,
|
70
|
+
"minimum": 0.0,
|
71
|
+
"maximum": 10.0
|
72
|
+
},
|
73
|
+
"tags": {
|
74
|
+
"type": "array",
|
75
|
+
"description": "Tags to filter search results",
|
76
|
+
"required": False,
|
77
|
+
"items": {
|
78
|
+
"type": "string"
|
79
|
+
}
|
80
|
+
},
|
81
|
+
"language": {
|
82
|
+
"type": "string",
|
83
|
+
"description": "Language code for query expansion (e.g., 'en', 'es')",
|
84
|
+
"required": False
|
85
|
+
},
|
86
|
+
"pos_to_expand": {
|
87
|
+
"type": "array",
|
88
|
+
"description": "Parts of speech to expand with synonyms",
|
89
|
+
"required": False,
|
90
|
+
"items": {
|
91
|
+
"type": "string",
|
92
|
+
"enum": ["NOUN", "VERB", "ADJ", "ADV"]
|
93
|
+
}
|
94
|
+
},
|
95
|
+
"max_synonyms": {
|
96
|
+
"type": "integer",
|
97
|
+
"description": "Maximum number of synonyms to use for query expansion",
|
98
|
+
"required": False,
|
99
|
+
"minimum": 1,
|
100
|
+
"maximum": 10
|
101
|
+
},
|
102
|
+
"no_results_message": {
|
103
|
+
"type": "string",
|
104
|
+
"description": "Message to return when no results are found",
|
105
|
+
"default": "I couldn't find any relevant information for '{query}' in the knowledge base. Try rephrasing your question or asking about a different topic.",
|
106
|
+
"required": False
|
107
|
+
}
|
108
|
+
})
|
109
|
+
return schema
|
110
|
+
|
29
111
|
def get_instance_key(self) -> str:
|
30
112
|
"""
|
31
113
|
Get the key used to track this skill instance
|
@@ -23,6 +23,27 @@ class JokeSkill(SkillBase):
|
|
23
23
|
REQUIRED_PACKAGES = [] # DataMap doesn't require local packages
|
24
24
|
REQUIRED_ENV_VARS = [] # API key comes from parameters
|
25
25
|
|
26
|
+
@classmethod
|
27
|
+
def get_parameter_schema(cls) -> Dict[str, Dict[str, Any]]:
|
28
|
+
"""Get parameter schema for joke skill"""
|
29
|
+
schema = super().get_parameter_schema()
|
30
|
+
schema.update({
|
31
|
+
"api_key": {
|
32
|
+
"type": "string",
|
33
|
+
"description": "API Ninjas API key for joke service",
|
34
|
+
"required": True,
|
35
|
+
"hidden": True,
|
36
|
+
"env_var": "API_NINJAS_KEY"
|
37
|
+
},
|
38
|
+
"tool_name": {
|
39
|
+
"type": "string",
|
40
|
+
"description": "Custom name for the joke tool",
|
41
|
+
"default": "get_joke",
|
42
|
+
"required": False
|
43
|
+
}
|
44
|
+
})
|
45
|
+
return schema
|
46
|
+
|
26
47
|
def setup(self) -> bool:
|
27
48
|
"""Setup the joke skill"""
|
28
49
|
# Validate required parameters
|
@@ -33,6 +33,88 @@ class MCPGatewaySkill(SkillBase):
|
|
33
33
|
REQUIRED_PACKAGES = ["requests"]
|
34
34
|
REQUIRED_ENV_VARS = []
|
35
35
|
|
36
|
+
@classmethod
|
37
|
+
def get_parameter_schema(cls) -> Dict[str, Dict[str, Any]]:
|
38
|
+
"""Get parameter schema for MCP Gateway skill"""
|
39
|
+
schema = super().get_parameter_schema()
|
40
|
+
schema.update({
|
41
|
+
"gateway_url": {
|
42
|
+
"type": "string",
|
43
|
+
"description": "URL of the MCP Gateway service",
|
44
|
+
"required": True
|
45
|
+
},
|
46
|
+
"auth_token": {
|
47
|
+
"type": "string",
|
48
|
+
"description": "Bearer token for authentication (alternative to basic auth)",
|
49
|
+
"required": False,
|
50
|
+
"hidden": True,
|
51
|
+
"env_var": "MCP_GATEWAY_AUTH_TOKEN"
|
52
|
+
},
|
53
|
+
"auth_user": {
|
54
|
+
"type": "string",
|
55
|
+
"description": "Username for basic authentication (required if auth_token not provided)",
|
56
|
+
"required": False,
|
57
|
+
"env_var": "MCP_GATEWAY_AUTH_USER"
|
58
|
+
},
|
59
|
+
"auth_password": {
|
60
|
+
"type": "string",
|
61
|
+
"description": "Password for basic authentication (required if auth_token not provided)",
|
62
|
+
"required": False,
|
63
|
+
"hidden": True,
|
64
|
+
"env_var": "MCP_GATEWAY_AUTH_PASSWORD"
|
65
|
+
},
|
66
|
+
"services": {
|
67
|
+
"type": "array",
|
68
|
+
"description": "List of MCP services to connect to (empty for all available)",
|
69
|
+
"default": [],
|
70
|
+
"required": False,
|
71
|
+
"items": {
|
72
|
+
"type": "object",
|
73
|
+
"properties": {
|
74
|
+
"name": {
|
75
|
+
"type": "string",
|
76
|
+
"description": "Service name"
|
77
|
+
},
|
78
|
+
"tools": {
|
79
|
+
"type": ["string", "array"],
|
80
|
+
"description": "Tools to expose ('*' for all, or list of tool names)"
|
81
|
+
}
|
82
|
+
}
|
83
|
+
}
|
84
|
+
},
|
85
|
+
"session_timeout": {
|
86
|
+
"type": "integer",
|
87
|
+
"description": "Session timeout in seconds",
|
88
|
+
"default": 300,
|
89
|
+
"required": False
|
90
|
+
},
|
91
|
+
"tool_prefix": {
|
92
|
+
"type": "string",
|
93
|
+
"description": "Prefix for registered SWAIG function names",
|
94
|
+
"default": "mcp_",
|
95
|
+
"required": False
|
96
|
+
},
|
97
|
+
"retry_attempts": {
|
98
|
+
"type": "integer",
|
99
|
+
"description": "Number of retry attempts for failed requests",
|
100
|
+
"default": 3,
|
101
|
+
"required": False
|
102
|
+
},
|
103
|
+
"request_timeout": {
|
104
|
+
"type": "integer",
|
105
|
+
"description": "Request timeout in seconds",
|
106
|
+
"default": 30,
|
107
|
+
"required": False
|
108
|
+
},
|
109
|
+
"verify_ssl": {
|
110
|
+
"type": "boolean",
|
111
|
+
"description": "Verify SSL certificates",
|
112
|
+
"default": True,
|
113
|
+
"required": False
|
114
|
+
}
|
115
|
+
})
|
116
|
+
return schema
|
117
|
+
|
36
118
|
def setup(self) -> bool:
|
37
119
|
"""Setup and validate skill configuration"""
|
38
120
|
# Check for auth method - either token or basic auth
|