naas-abi 1.0.0__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.
Files changed (62) hide show
  1. naas_abi/__init__.py +35 -0
  2. naas_abi/agents/AbiAgent.py +442 -0
  3. naas_abi/agents/AbiAgent_test.py +157 -0
  4. naas_abi/agents/EntitytoSPARQLAgent.py +952 -0
  5. naas_abi/agents/EntitytoSPARQLAgent_test.py +66 -0
  6. naas_abi/agents/KnowledgeGraphBuilderAgent.py +321 -0
  7. naas_abi/agents/KnowledgeGraphBuilderAgent_test.py +86 -0
  8. naas_abi/agents/OntologyEngineerAgent.py +115 -0
  9. naas_abi/agents/OntologyEngineerAgent_test.py +42 -0
  10. naas_abi/apps/oxigraph_admin/main.py +392 -0
  11. naas_abi/apps/oxigraph_admin/terminal_style.py +151 -0
  12. naas_abi/apps/sparql_terminal/main.py +68 -0
  13. naas_abi/apps/sparql_terminal/terminal_style.py +236 -0
  14. naas_abi/apps/terminal_agent/main.py +553 -0
  15. naas_abi/apps/terminal_agent/terminal_style.py +175 -0
  16. naas_abi/cli.py +714 -0
  17. naas_abi/mappings.py +83 -0
  18. naas_abi/models/airgap_gemma.py +220 -0
  19. naas_abi/models/airgap_qwen.py +24 -0
  20. naas_abi/models/default.py +23 -0
  21. naas_abi/models/gpt_4_1.py +25 -0
  22. naas_abi/pipelines/AIAgentOntologyGenerationPipeline.py +635 -0
  23. naas_abi/pipelines/AIAgentOntologyGenerationPipeline_test.py +133 -0
  24. naas_abi/pipelines/AddIndividualPipeline.py +215 -0
  25. naas_abi/pipelines/AddIndividualPipeline_test.py +66 -0
  26. naas_abi/pipelines/InsertDataSPARQLPipeline.py +197 -0
  27. naas_abi/pipelines/InsertDataSPARQLPipeline_test.py +96 -0
  28. naas_abi/pipelines/MergeIndividualsPipeline.py +245 -0
  29. naas_abi/pipelines/MergeIndividualsPipeline_test.py +98 -0
  30. naas_abi/pipelines/RemoveIndividualPipeline.py +166 -0
  31. naas_abi/pipelines/RemoveIndividualPipeline_test.py +58 -0
  32. naas_abi/pipelines/UpdateCommercialOrganizationPipeline.py +198 -0
  33. naas_abi/pipelines/UpdateDataPropertyPipeline.py +175 -0
  34. naas_abi/pipelines/UpdateLegalNamePipeline.py +107 -0
  35. naas_abi/pipelines/UpdateLinkedInPagePipeline.py +179 -0
  36. naas_abi/pipelines/UpdatePersonPipeline.py +184 -0
  37. naas_abi/pipelines/UpdateSkillPipeline.py +118 -0
  38. naas_abi/pipelines/UpdateTickerPipeline.py +104 -0
  39. naas_abi/pipelines/UpdateWebsitePipeline.py +106 -0
  40. naas_abi/triggers.py +131 -0
  41. naas_abi/workflows/AgentRecommendationWorkflow.py +321 -0
  42. naas_abi/workflows/AgentRecommendationWorkflow_test.py +160 -0
  43. naas_abi/workflows/ArtificialAnalysisWorkflow.py +337 -0
  44. naas_abi/workflows/ArtificialAnalysisWorkflow_test.py +57 -0
  45. naas_abi/workflows/ConvertOntologyGraphToYamlWorkflow.py +210 -0
  46. naas_abi/workflows/ConvertOntologyGraphToYamlWorkflow_test.py +78 -0
  47. naas_abi/workflows/CreateClassOntologyYamlWorkflow.py +208 -0
  48. naas_abi/workflows/CreateClassOntologyYamlWorkflow_test.py +65 -0
  49. naas_abi/workflows/CreateIndividualOntologyYamlWorkflow.py +183 -0
  50. naas_abi/workflows/CreateIndividualOntologyYamlWorkflow_test.py +86 -0
  51. naas_abi/workflows/ExportGraphInstancesToExcelWorkflow.py +450 -0
  52. naas_abi/workflows/ExportGraphInstancesToExcelWorkflow_test.py +33 -0
  53. naas_abi/workflows/GetObjectPropertiesFromClassWorkflow.py +385 -0
  54. naas_abi/workflows/GetObjectPropertiesFromClassWorkflow_test.py +57 -0
  55. naas_abi/workflows/GetSubjectGraphWorkflow.py +84 -0
  56. naas_abi/workflows/GetSubjectGraphWorkflow_test.py +71 -0
  57. naas_abi/workflows/SearchIndividualWorkflow.py +190 -0
  58. naas_abi/workflows/SearchIndividualWorkflow_test.py +98 -0
  59. naas_abi-1.0.0.dist-info/METADATA +9 -0
  60. naas_abi-1.0.0.dist-info/RECORD +62 -0
  61. naas_abi-1.0.0.dist-info/WHEEL +5 -0
  62. naas_abi-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,450 @@
1
+ import os
2
+ import re
3
+ from dataclasses import dataclass
4
+ from datetime import datetime
5
+ from enum import Enum
6
+ from io import BytesIO
7
+ from typing import Annotated, Any, Optional
8
+
9
+ import pandas as pd
10
+ from fastapi import APIRouter
11
+ from langchain_core.tools import BaseTool, StructuredTool
12
+ from naas_abi import config, logger
13
+ from naas_abi_core.services.triple_store.TripleStorePorts import ITripleStoreService
14
+ from naas_abi_core.workflow import Workflow, WorkflowConfiguration, WorkflowParameters
15
+ from naas_abi_marketplace.applications.naas.integrations.NaasIntegration import (
16
+ NaasIntegration,
17
+ NaasIntegrationConfiguration,
18
+ )
19
+ from pydantic import Field
20
+ from rdflib import RDF, Graph, URIRef, query
21
+
22
+ prefixes = {
23
+ "http://www.w3.org/2000/01/rdf-schema#": "rdfs",
24
+ "http://www.w3.org/2002/07/owl#": "owl",
25
+ "http://www.w3.org/1999/02/22-rdf-syntax-ns#": "rdf",
26
+ "http://ontology.naas.ai/abi/": "abi",
27
+ "http://purl.obolibrary.org/obo/": "bfo",
28
+ "https://www.commoncoreontologies.org/": "cco",
29
+ "http://www.w3.org/2001/XMLSchema#": "xsd",
30
+ "http://www.w3.org/2004/02/skos/core#": "skos",
31
+ }
32
+
33
+
34
+ @dataclass
35
+ class ExportGraphInstancesToExcelWorkflowConfiguration(WorkflowConfiguration):
36
+ """Configuration for ExportGraphInstancesToExcel workflow."""
37
+
38
+ triple_store: ITripleStoreService
39
+ naas_integration_config: NaasIntegrationConfiguration
40
+ data_store_path: str = "datastore/triplestore/export/excel"
41
+
42
+
43
+ class ExportGraphInstancesToExcelWorkflowParameters(WorkflowParameters):
44
+ """Parameters for ExportGraphInstancesToExcel workflow."""
45
+
46
+ excel_file_name: Annotated[
47
+ str,
48
+ Field(
49
+ description="Name of the Excel file to export.",
50
+ ),
51
+ ] = "graph_instances_export.xlsx"
52
+
53
+
54
+ class ExportGraphInstancesToExcelWorkflow(Workflow):
55
+ """Workflow for exporting graph instances to Excel."""
56
+
57
+ __configuration: ExportGraphInstancesToExcelWorkflowConfiguration
58
+
59
+ def __init__(self, configuration: ExportGraphInstancesToExcelWorkflowConfiguration):
60
+ super().__init__(configuration)
61
+ self.__configuration = configuration
62
+ self.__naas_integration = NaasIntegration(configuration.naas_integration_config)
63
+
64
+ def create_sheet_name(self, label: str) -> str:
65
+ """Create sheet name: rdfs:label"""
66
+ # Clean the label for Excel sheet name (remove invalid characters)
67
+ clean_label = re.sub(r'[\\/*?:"<>|]', "_", label)
68
+ sheet_name = f"{clean_label}"
69
+
70
+ # Excel sheet names are limited to 31 characters
71
+ if len(sheet_name) > 31:
72
+ sheet_name = sheet_name[:28] + "..."
73
+
74
+ return sheet_name
75
+
76
+ def autofit_columns(
77
+ self, writer: pd.ExcelWriter, sheet_name: str
78
+ ) -> pd.ExcelWriter:
79
+ """Autofit columns in an Excel worksheet"""
80
+ worksheet = writer.sheets[sheet_name]
81
+ label_width = 0
82
+
83
+ # First pass to get Label column width
84
+ for column in worksheet.columns:
85
+ column = [cell for cell in column]
86
+ if column[0].value == "Label":
87
+ for cell in column:
88
+ try:
89
+ if len(str(cell.value)) > label_width:
90
+ label_width = len(str(cell.value))
91
+ except Exception as _:
92
+ pass
93
+ label_width += 2
94
+ break
95
+
96
+ # Second pass to set all column widths
97
+ for column in worksheet.columns:
98
+ max_length = 0
99
+ column = [cell for cell in column]
100
+
101
+ # If this is Sheet Name column, use Label width
102
+ if column[0].value == "Sheet Name":
103
+ worksheet.column_dimensions[column[0].column_letter].width = label_width
104
+ continue
105
+
106
+ # Otherwise autofit based on content
107
+ for cell in column:
108
+ try:
109
+ if len(str(cell.value)) > max_length:
110
+ max_length = len(str(cell.value))
111
+ except Exception as _:
112
+ pass
113
+ adjusted_width = max_length + 2
114
+ worksheet.column_dimensions[column[0].column_letter].width = adjusted_width
115
+
116
+ return writer
117
+
118
+ def get_all_triples_by_class(self, graph: Graph) -> query.Result:
119
+ query_sparql = """
120
+ PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
121
+ PREFIX owl: <http://www.w3.org/2002/07/owl#>
122
+ PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
123
+ SELECT DISTINCT ?class (COALESCE(?class_label, STR(?class)) as ?class_label)
124
+ WHERE {
125
+ ?individual rdf:type ?class .
126
+ ?individual rdf:type owl:NamedIndividual .
127
+ OPTIONAL { ?class rdfs:label ?class_label }
128
+ FILTER(STRSTARTS(STR(?individual), "http://ontology.naas.ai/abi/"))
129
+ FILTER(?class != owl:NamedIndividual)
130
+ }
131
+ ORDER BY ?class_label
132
+ """
133
+ results = graph.query(query_sparql)
134
+ logger.info(f"Found {len(results)} classes.")
135
+ return results
136
+
137
+ def get_all_object_property_labels(self, graph: Graph) -> dict[str, str]:
138
+ """Get object property label from URI.
139
+ If the object property URI is not in the prefixes, return the last part of the URI."""
140
+
141
+ sparql_query = """
142
+ PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
143
+ PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
144
+ PREFIX owl: <http://www.w3.org/2002/07/owl#>
145
+ SELECT ?uri ?label
146
+ WHERE {
147
+ ?uri rdf:type owl:ObjectProperty ;
148
+ rdfs:label ?label .
149
+ }
150
+ """
151
+ results = graph.query(sparql_query)
152
+ object_property_labels: dict = {}
153
+ for row in results:
154
+ assert isinstance(row, query.ResultRow)
155
+ if row[0] is not None and row[1] is not None:
156
+ object_property_labels[str(row[0])] = str(row[1])
157
+ logger.info(f"Found {len(object_property_labels)} object properties.")
158
+ return object_property_labels
159
+
160
+ def export_to_excel(
161
+ self, parameters: ExportGraphInstancesToExcelWorkflowParameters
162
+ ) -> Optional[str]:
163
+ """Export graph instances to Excel and return asset URL to download it."""
164
+ graph = self.__configuration.triple_store.get()
165
+ all_triples = self.get_all_triples_by_class(graph)
166
+ object_property_labels = self.get_all_object_property_labels(graph)
167
+
168
+ # First collect all class info
169
+ summary_data = []
170
+ for row in all_triples:
171
+ assert isinstance(row, query.ResultRow)
172
+ class_uri = str(row["class"])
173
+ class_label = (
174
+ str(row["class_label"]).split("/")[-1]
175
+ if row["class_label"]
176
+ else class_uri.split("/")[-1]
177
+ )
178
+ sheet_name = self.create_sheet_name(class_label)
179
+ summary_data.append(
180
+ {
181
+ "URI": class_uri,
182
+ "Type": "Class",
183
+ "Label": class_label,
184
+ "Sheet Name": sheet_name,
185
+ }
186
+ )
187
+
188
+ summary_df = pd.DataFrame(summary_data)
189
+ summary_df = summary_df.sort_values("Label").drop_duplicates()
190
+ summary_df = summary_df.reset_index(drop=True)
191
+ logger.info(f"Found {len(summary_df)} classes with individuals.")
192
+
193
+ # Then collect all data properties and object properties by class
194
+ summary_data = []
195
+ local_dir_path = "storage/" + self.__configuration.data_store_path
196
+ os.makedirs(local_dir_path, exist_ok=True)
197
+ excel_file_path = os.path.join(
198
+ local_dir_path,
199
+ f"{datetime.now().strftime('%Y%m%dT%H%M%S')}_{parameters.excel_file_name}",
200
+ )
201
+ with pd.ExcelWriter(excel_file_path, engine="openpyxl") as writer: # type: ignore
202
+ # Collect all data properties and object properties across all classes
203
+ all_data_properties: list[dict[str, Any]] = []
204
+ all_object_properties: dict[str, list[dict[str, Any]]] = {}
205
+
206
+ for _, row in summary_df.iterrows(): # type: ignore
207
+ class_uri = str(row.get("URI", "")) # type: ignore
208
+ class_label = str(row.get("Label", "")) # type: ignore
209
+ sheet_name = str(row.get("Sheet Name", "")) # type: ignore
210
+
211
+ # Get all individuals of this class
212
+ individuals: list[dict[str, Any]] = []
213
+ total_triples: int = 0 # Add counter for actual triples
214
+
215
+ for s in graph.subjects(RDF.type, URIRef(class_uri)):
216
+ individual_uri = str(s)
217
+ individual_data_properties: dict[str, Any] = {
218
+ "uri": individual_uri,
219
+ }
220
+
221
+ # Count triples for this individual
222
+ individual_triples: int = 2
223
+
224
+ # Get triples for this individual
225
+ for s, p, o in graph.triples((URIRef(individual_uri), None, None)):
226
+ individual_triples += 1 # Count each triple
227
+ # Skip RDF/RDFS/OWL system properties and object properties
228
+ if str(p) == "http://www.w3.org/1999/02/22-rdf-syntax-ns#type":
229
+ continue
230
+
231
+ def add_to_dict(data: dict[str, Any], key: str, value: Any):
232
+ # If property already exists, convert to list
233
+ if key in data:
234
+ existing_value = data[key]
235
+ if isinstance(existing_value, list):
236
+ existing_value.append(str(o))
237
+ else:
238
+ # Convert existing value to list (handle None case)
239
+ data[key] = (
240
+ [existing_value, str(o)]
241
+ if existing_value is not None
242
+ else [str(o)]
243
+ ) # type: ignore
244
+ else:
245
+ data[key] = str(o)
246
+ return data
247
+
248
+ if isinstance(o, URIRef):
249
+ prop_label = object_property_labels.get(str(p), None)
250
+ if prop_label is None:
251
+ prop_label = str(p).split("/")[-1]
252
+
253
+ # Add to all_object_properties for separate sheet
254
+ if prop_label not in all_object_properties:
255
+ all_object_properties[prop_label] = []
256
+
257
+ # Get rdfs:label for subject and object
258
+ subject_label = None
259
+ object_label = None
260
+
261
+ # Get subject label
262
+ for _, label_pred, label_obj in graph.triples(
263
+ (
264
+ URIRef(individual_uri),
265
+ URIRef(
266
+ "http://www.w3.org/2000/01/rdf-schema#label"
267
+ ),
268
+ None,
269
+ )
270
+ ):
271
+ subject_label = str(label_obj)
272
+ break
273
+
274
+ # Get object label
275
+ for _, label_pred, label_obj in graph.triples(
276
+ (
277
+ URIRef(str(o)),
278
+ URIRef(
279
+ "http://www.w3.org/2000/01/rdf-schema#label"
280
+ ),
281
+ None,
282
+ )
283
+ ):
284
+ object_label = str(label_obj)
285
+ break
286
+
287
+ all_object_properties[prop_label].append(
288
+ {
289
+ "domain_class": class_label,
290
+ "domain_uri": individual_uri,
291
+ "domain_label": subject_label
292
+ or individual_uri.split("/")[-1],
293
+ "range_uri": str(o),
294
+ "range_label": object_label
295
+ or str(o).split("/")[-1],
296
+ "object_property_uri": str(p),
297
+ "object_property_label": prop_label,
298
+ }
299
+ )
300
+ else:
301
+ # Split on either # or /
302
+ for k, v in prefixes.items():
303
+ if k in str(p):
304
+ prop_suffix = str(p).split(k)[-1]
305
+ prop_label = f"{v}:{prop_suffix}"
306
+ break
307
+ else:
308
+ prop_label = str(p).split("/")[-1]
309
+
310
+ individual_data_properties = add_to_dict(
311
+ individual_data_properties, prop_label, str(o)
312
+ )
313
+
314
+ # Add to all_data_properties for separate sheet
315
+ all_data_properties.append(
316
+ {
317
+ "uri": individual_uri,
318
+ "property": prop_label,
319
+ "value": str(o),
320
+ "class": class_label,
321
+ }
322
+ )
323
+
324
+ total_triples += individual_triples
325
+
326
+ # Join any list values
327
+ for key, value in individual_data_properties.items():
328
+ if isinstance(value, list):
329
+ individual_data_properties[key] = "; ".join(value)
330
+
331
+ individuals.append(individual_data_properties)
332
+
333
+ # Create dataframe and write to Excel
334
+ if individuals:
335
+ df = pd.DataFrame(individuals)
336
+ if "rdfs:label" in df.columns:
337
+ df = df.sort_values("rdfs:label", ascending=True)
338
+ else:
339
+ df = df.sort_values("uri", ascending=True)
340
+ df.to_excel(writer, sheet_name=sheet_name, index=False)
341
+ writer = self.autofit_columns(writer, sheet_name)
342
+
343
+ summary_data.append(
344
+ {
345
+ "URI": class_uri,
346
+ "Type": "Class",
347
+ "Label": class_label,
348
+ "Sheet Name": f'=HYPERLINK("#\'{sheet_name}\'!A1", "{class_label}")',
349
+ "Number of Individuals": str(len(individuals)),
350
+ }
351
+ )
352
+
353
+ # Create Classes summary dataframe
354
+ summary_df = pd.DataFrame(summary_data)
355
+ summary_df = summary_df.sort_values("Label").drop_duplicates()
356
+ summary_df = summary_df.reset_index(drop=True)
357
+ summary_df = summary_df.astype({"Number of Individuals": int})
358
+ writer.book.create_sheet(
359
+ "Classes", 0
360
+ ) # Create Summary sheet in first position
361
+ summary_df.to_excel(writer, sheet_name="Classes", index=False)
362
+ sheet = writer.sheets["Classes"]
363
+ sheet.sheet_properties.tabColor = "000000"
364
+ writer = self.autofit_columns(writer, "Classes")
365
+
366
+ # Create Object Properties
367
+ object_properties_summary = []
368
+ for prop_label, prop_data in sorted(all_object_properties.items()):
369
+ if prop_data:
370
+ sheet_name = self.create_sheet_name(prop_label)
371
+ prop_uri = str(prop_data[0]["object_property_uri"])
372
+ prop_df = pd.DataFrame(prop_data)
373
+ prop_df = prop_df.sort_values(
374
+ ["domain_class", "domain_label", "range_label"]
375
+ )
376
+ prop_df.to_excel(writer, sheet_name=sheet_name, index=False)
377
+ writer = self.autofit_columns(writer, sheet_name)
378
+
379
+ object_properties_summary.append(
380
+ {
381
+ "URI": prop_uri,
382
+ "Type": "Object Property",
383
+ "Label": prop_label,
384
+ "Sheet Name": f'=HYPERLINK("#\'{sheet_name}\'!A1", "{prop_label}")',
385
+ "Number of Relations": str(len(prop_data)),
386
+ }
387
+ )
388
+ writer.book.create_sheet("Object Properties", 1)
389
+ object_properties_summary_df = pd.DataFrame(object_properties_summary)
390
+ object_properties_summary_df = object_properties_summary_df.sort_values(
391
+ "URI"
392
+ ).drop_duplicates()
393
+ object_properties_summary_df = object_properties_summary_df.astype(
394
+ {"Number of Relations": int}
395
+ )
396
+ object_properties_summary_df.to_excel(
397
+ writer, sheet_name="Object Properties", index=False
398
+ )
399
+ sheet = writer.sheets["Object Properties"]
400
+ sheet.sheet_properties.tabColor = "000000"
401
+ writer = self.autofit_columns(writer, "Object Properties")
402
+
403
+ # Save data to storage and create download URL
404
+ buffer = BytesIO()
405
+ writer.book.save(buffer)
406
+ excel_data = buffer.getvalue()
407
+ buffer.close()
408
+ asset = self.__naas_integration.upload_asset(
409
+ data=excel_data,
410
+ workspace_id=config.workspace_id,
411
+ storage_name=config.storage_name,
412
+ prefix=local_dir_path,
413
+ object_name=str(parameters.excel_file_name),
414
+ visibility="public",
415
+ return_url=True,
416
+ )
417
+ if asset is not None:
418
+ asset_url = asset.get("asset_url")
419
+ logger.info(
420
+ f"💾 Graph exported to {excel_file_path} (download URL: {asset_url})"
421
+ )
422
+ return asset_url
423
+ else:
424
+ logger.error("❌ Failed to upload asset to Naas")
425
+ return None
426
+
427
+ def as_tools(self) -> list[BaseTool]:
428
+ return [
429
+ StructuredTool(
430
+ name="export_graph_instances_to_excel",
431
+ description="Export graph instances to Excel and return asset URL to download it.",
432
+ func=lambda **kwargs: self.export_to_excel(
433
+ ExportGraphInstancesToExcelWorkflowParameters(**kwargs)
434
+ ),
435
+ args_schema=ExportGraphInstancesToExcelWorkflowParameters,
436
+ )
437
+ ]
438
+
439
+ def as_api(
440
+ self,
441
+ router: APIRouter,
442
+ route_name: str = "",
443
+ name: str = "",
444
+ description: str = "",
445
+ description_stream: str = "",
446
+ tags: list[str | Enum] | None = None,
447
+ ) -> None:
448
+ if tags is None:
449
+ tags = []
450
+ return None
@@ -0,0 +1,33 @@
1
+ import pytest
2
+ from naas_abi import secret, services
3
+ from naas_abi.workflows.ExportGraphInstancesToExcelWorkflow import (
4
+ ExportGraphInstancesToExcelWorkflow,
5
+ ExportGraphInstancesToExcelWorkflowConfiguration,
6
+ ExportGraphInstancesToExcelWorkflowParameters,
7
+ )
8
+ from naas_abi_marketplace.applications.naas.integrations.NaasIntegration import (
9
+ NaasIntegrationConfiguration,
10
+ )
11
+
12
+
13
+ @pytest.fixture
14
+ def export_graph_to_excel() -> ExportGraphInstancesToExcelWorkflow:
15
+ configuration = ExportGraphInstancesToExcelWorkflowConfiguration(
16
+ triple_store=services.triple_store_service,
17
+ naas_integration_config=NaasIntegrationConfiguration(
18
+ api_key=secret.get("NAAS_API_KEY")
19
+ ),
20
+ )
21
+ return ExportGraphInstancesToExcelWorkflow(configuration)
22
+
23
+
24
+ def test_export_graph_instances_to_excel_workflow(
25
+ export_graph_to_excel: ExportGraphInstancesToExcelWorkflow,
26
+ ):
27
+ result = export_graph_to_excel.export_to_excel(
28
+ ExportGraphInstancesToExcelWorkflowParameters()
29
+ )
30
+
31
+ assert result is not None, result
32
+ assert isinstance(result, str), result
33
+ assert result.startswith("https://api.naas.ai/workspace/"), result