fenix-mcp 1.13.0__py3-none-any.whl → 2.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.
@@ -15,7 +15,7 @@ class InitializationData:
15
15
  profile: Optional[Dict[str, Any]]
16
16
  core_documents: List[Dict[str, Any]]
17
17
  user_documents: List[Dict[str, Any]]
18
- recent_memories: List[Dict[str, Any]]
18
+ my_work_items: List[Dict[str, Any]]
19
19
 
20
20
 
21
21
  class InitializationService:
@@ -25,9 +25,7 @@ class InitializationService:
25
25
  self._api = api_client
26
26
  self._logger = logger
27
27
 
28
- async def gather_data(
29
- self, *, include_user_docs: bool, limit: int
30
- ) -> InitializationData:
28
+ async def gather_data(self, *, include_user_docs: bool) -> InitializationData:
31
29
  profile = await self._safe_call(self._api.get_profile)
32
30
  core_docs = await self._safe_call(
33
31
  self._api.list_core_documents,
@@ -46,11 +44,19 @@ class InitializationService:
46
44
  )
47
45
  if self._logger:
48
46
  self._logger.debug("User docs response", extra={"user_docs": user_docs})
47
+
48
+ # Fetch user's assigned work items for proactive context
49
+ work_items_response = await self._safe_call(
50
+ self._api.get_work_items_mine,
51
+ limit=10,
52
+ )
53
+ my_work_items = self._extract_items(work_items_response, "workItems")
54
+
49
55
  return InitializationData(
50
56
  profile=profile,
51
57
  core_documents=self._extract_items(core_docs, "coreDocuments"),
52
58
  user_documents=self._extract_items(user_docs, "userCoreDocuments"),
53
- recent_memories=[],
59
+ my_work_items=my_work_items,
54
60
  )
55
61
 
56
62
  async def _safe_call(self, func, *args, **kwargs):
@@ -71,110 +77,3 @@ class InitializationService:
71
77
  if isinstance(value, list):
72
78
  return [item for item in value if isinstance(item, dict)]
73
79
  return []
74
-
75
- @staticmethod
76
- def build_existing_user_summary(
77
- data: InitializationData, include_user_docs: bool
78
- ) -> str:
79
- profile = data.profile or {}
80
- user_info = profile.get("user") or {}
81
- tenant_info = profile.get("tenant") or {}
82
- core_count = len(data.core_documents)
83
- user_count = len(data.user_documents)
84
- memories_count = len(data.recent_memories)
85
-
86
- lines = [
87
- "✅ **Fênix Cloud inicializado com sucesso!**",
88
- ]
89
-
90
- if user_info:
91
- lines.append(
92
- f"- Usuário: {user_info.get('name') or 'Desconhecido'} "
93
- f"(ID: {user_info.get('id', 'N/A')})"
94
- )
95
- if tenant_info:
96
- lines.append(f"- Organização: {tenant_info.get('name', 'N/A')}")
97
-
98
- lines.append(f"- Documentos principais carregados: {core_count}")
99
-
100
- if include_user_docs:
101
- lines.append(f"- Documentos pessoais carregados: {user_count}")
102
-
103
- lines.append(f"- Memórias recentes disponíveis: {memories_count}")
104
-
105
- if core_count:
106
- preview = ", ".join(
107
- doc.get("name", doc.get("title", "sem título"))
108
- for doc in data.core_documents[:5]
109
- )
110
- lines.append(f"- Exemplos de documentos principais: {preview}")
111
-
112
- if include_user_docs and user_count:
113
- preview = ", ".join(
114
- doc.get("name", "sem título") for doc in data.user_documents[:5]
115
- )
116
- lines.append(f"- Exemplos de documentos pessoais: {preview}")
117
-
118
- if memories_count:
119
- preview = ", ".join(
120
- mem.get("title", "sem título") for mem in data.recent_memories[:3]
121
- )
122
- lines.append(f"- Memórias recentes: {preview}")
123
-
124
- lines.append("")
125
- lines.append(
126
- "Agora você pode usar as ferramentas de produtividade, conhecimento e inteligência para continuar."
127
- )
128
- return "\n".join(lines)
129
-
130
- @staticmethod
131
- def build_new_user_prompt(data: InitializationData) -> str:
132
- profile = data.profile or {}
133
- user_name = (profile.get("user") or {}).get("name") or "Bem-vindo(a)"
134
-
135
- questions = [
136
- "Qual é o foco principal do seu trabalho atualmente?",
137
- "Quais são seus objetivos para as próximas semanas?",
138
- "Existem tarefas ou projetos que gostaria de priorizar?",
139
- "Como você prefere organizar suas rotinas ou checklists?",
140
- "Quais são os principais blocos de conhecimento que precisa acessar rapidamente?",
141
- "Há regras ou procedimentos que precisam ser seguidos com frequência?",
142
- "Que tipo de memória ou registro você consulta com frequência?",
143
- "Quais ferramentas ou integrações externas são essenciais no seu dia a dia?",
144
- "O que significaria sucesso para você utilizando o Fênix?",
145
- ]
146
-
147
- body = [
148
- f"👋 **{user_name}, bem-vindo(a) ao Fênix Cloud!**",
149
- "",
150
- "Notamos que você ainda não tem documentos pessoais configurados. "
151
- "Vamos criar uma experiência personalizada para você.",
152
- "",
153
- "Responda às perguntas abaixo para que possamos preparar modos, regras e documentos alinhados às suas necessidades:",
154
- "",
155
- ]
156
-
157
- body.extend(f"{idx + 1}. {question}" for idx, question in enumerate(questions))
158
- body.extend(
159
- [
160
- "",
161
- "Envie as respostas no formato:",
162
- "```",
163
- 'initialize action=setup answers=["resposta 1", "resposta 2", ..., "resposta 9"]',
164
- "```",
165
- "",
166
- "Com base nisso, sugerirei documentos, regras e rotinas para você utilizar.",
167
- ]
168
- )
169
-
170
- return "\n".join(body)
171
-
172
- @staticmethod
173
- def validate_setup_answers(answers: List[str]) -> Optional[str]:
174
- if not answers:
175
- return "Envie uma lista com 9 respostas para processarmos o setup."
176
- if len(answers) != 9:
177
- return f"Esperado receber 9 respostas, mas recebi {len(answers)}."
178
- if not all(isinstance(answer, str) and answer.strip() for answer in answers):
179
- return "Todas as respostas devem ser textos preenchidos."
180
- return None
@@ -5,7 +5,7 @@ from __future__ import annotations
5
5
 
6
6
  import asyncio
7
7
  from dataclasses import dataclass
8
- from typing import Any, Dict, Iterable, List, Optional
8
+ from typing import Any, Dict, List, Optional
9
9
 
10
10
  from fenix_mcp.infrastructure.fenix_api.client import FenixApiClient
11
11
 
@@ -15,270 +15,80 @@ class IntelligenceService:
15
15
  api: FenixApiClient
16
16
  logger: Any
17
17
 
18
- async def smart_create_memory(
18
+ async def save_memory(
19
19
  self,
20
20
  *,
21
21
  title: str,
22
22
  content: str,
23
- metadata: str,
24
- context: Optional[str],
25
- source: str,
26
- importance: str,
27
23
  tags: List[str],
24
+ documentation_item_id: Optional[str] = None,
25
+ work_item_id: Optional[str] = None,
26
+ sprint_id: Optional[str] = None,
28
27
  ) -> Dict[str, Any]:
29
- # Validate required parameters
30
- if not metadata or not metadata.strip():
31
- raise ValueError("metadata is required and cannot be empty")
32
-
33
- if not source or not source.strip():
34
- raise ValueError("source is required and cannot be empty")
35
-
28
+ """
29
+ Smart save memory - creates or updates based on semantic similarity.
30
+
31
+ Required:
32
+ - title: Memory title
33
+ - content: Memory content
34
+ - tags: List of tags for categorization
35
+
36
+ Optional:
37
+ - documentation_item_id: Related documentation
38
+ - work_item_id: Related work item
39
+ - sprint_id: Related sprint
40
+ """
41
+ if not title or not title.strip():
42
+ raise ValueError("title is required")
43
+ if not content or not content.strip():
44
+ raise ValueError("content is required")
36
45
  if not tags or not isinstance(tags, list) or len(tags) == 0:
37
- raise ValueError("tags is required and must be a non-empty List[str]")
38
-
39
- # Validate all tags are strings
40
- for i, tag in enumerate(tags):
41
- if not isinstance(tag, str) or not tag.strip():
42
- raise ValueError(
43
- f"All tags must be non-empty strings, got {type(tag)} at index {i}"
44
- )
46
+ raise ValueError("tags is required and must be a non-empty list")
45
47
 
46
- importance_value = importance or "medium"
47
- metadata_str = build_metadata(
48
- metadata,
49
- importance=importance_value,
50
- tags=tags,
51
- context=context,
52
- source=source,
53
- )
54
48
  payload = {
55
- "title": title,
56
- "content": content,
57
- "metadata": metadata_str,
58
- "priority_score": _importance_to_priority(importance_value),
59
- "tags": tags,
49
+ "title": title.strip(),
50
+ "content": content.strip(),
51
+ "tags": [t.strip() for t in tags if t.strip()],
60
52
  }
61
- return await self._call(self.api.smart_create_memory, _strip_none(payload))
62
53
 
63
- async def query_memories(self, **filters: Any) -> List[Dict[str, Any]]:
64
- params = _strip_none(filters)
65
- include_content = _coerce_bool(
66
- params.pop("include_content", params.pop("content", None))
67
- )
68
- include_metadata = _coerce_bool(
69
- params.pop("include_metadata", params.pop("metadata", None))
70
- )
71
- allowed_keys = {
72
- "limit",
73
- "offset",
74
- "query",
75
- "tags",
76
- "modeId",
77
- "ruleId",
78
- "workItemId",
79
- "sprintId",
80
- "documentationItemId",
81
- "importance",
82
- }
83
- cleaned_params = {key: params[key] for key in allowed_keys if key in params}
84
- return (
85
- await self._call(
86
- self.api.list_memories,
87
- include_content=include_content,
88
- include_metadata=include_metadata,
89
- **cleaned_params,
90
- )
91
- or []
92
- )
93
-
94
- async def similar_memories(
95
- self, *, content: str, threshold: float, max_results: int
96
- ) -> List[Dict[str, Any]]:
97
- payload = {
98
- "content": content,
99
- "threshold": threshold,
100
- }
101
- result = (
102
- await self._call(self.api.find_similar_memories, _strip_none(payload)) or []
103
- )
104
- if isinstance(result, list) and max_results:
105
- return result[:max_results]
106
- return result
54
+ if documentation_item_id:
55
+ payload["documentationItemId"] = documentation_item_id
56
+ if work_item_id:
57
+ payload["workItemId"] = work_item_id
58
+ if sprint_id:
59
+ payload["sprintId"] = sprint_id
107
60
 
108
- async def consolidate_memories(
109
- self, *, memory_ids: Iterable[str], strategy: str
110
- ) -> Dict[str, Any]:
111
- payload = {
112
- "memoryIds": list(memory_ids),
113
- "strategy": strategy,
114
- }
115
- return await self._call(self.api.consolidate_memories, payload)
61
+ return await self._call(self.api.save_memory, payload)
116
62
 
117
- async def update_memory(self, memory_id: str, **fields: Any) -> Dict[str, Any]:
118
- payload = _strip_none(fields)
119
- if "importance" in payload:
120
- payload["priority_score"] = _importance_to_priority(
121
- payload.pop("importance")
122
- )
123
- mapping = {
124
- "documentation_item_id": "documentationItemId",
125
- "mode_id": "modeId",
126
- "rule_id": "ruleId",
127
- "work_item_id": "workItemId",
128
- "sprint_id": "sprintId",
129
- }
130
- for old_key, new_key in mapping.items():
131
- if old_key in payload:
132
- payload[new_key] = payload.pop(old_key)
133
- return await self._call(self.api.update_memory, memory_id, payload)
134
-
135
- async def delete_memory(self, memory_id: str) -> None:
136
- await self._call(self.api.delete_memory, memory_id)
137
-
138
- async def _call(self, func, *args, **kwargs):
139
- return await asyncio.to_thread(func, *args, **kwargs)
140
-
141
- async def get_memory(
63
+ async def search_memories(
142
64
  self,
143
- memory_id: str,
144
65
  *,
145
- include_content: bool = False,
146
- include_metadata: bool = False,
147
- ) -> Dict[str, Any]:
148
- return await self._call(
149
- self.api.get_memory,
150
- memory_id,
151
- include_content=include_content,
152
- include_metadata=include_metadata,
153
- )
154
-
155
-
156
- def _importance_to_priority(importance: Optional[str]) -> float:
157
- mapping = {
158
- "low": 0.2,
159
- "medium": 0.5,
160
- "high": 0.7,
161
- "critical": 0.9,
162
- }
163
- if importance is None:
164
- return 0.5
165
- return mapping.get(importance.lower(), 0.5)
166
-
167
-
168
- def _coerce_bool(value: Any, default: bool = False) -> bool:
169
- if value is None or value == "":
170
- return default
171
- if isinstance(value, bool):
172
- return value
173
- if isinstance(value, (int, float)):
174
- return bool(value)
175
- if isinstance(value, str):
176
- normalized = value.strip().lower()
177
- if normalized in {"true", "1", "yes", "y"}:
178
- return True
179
- if normalized in {"false", "0", "no", "n"}:
180
- return False
181
- return bool(value)
182
-
183
-
184
- def _strip_none(data: Dict[str, Any]) -> Dict[str, Any]:
185
- return {key: value for key, value in data.items() if value not in (None, "")}
186
-
187
-
188
- def build_metadata(
189
- explicit: str,
190
- *,
191
- importance: Optional[str],
192
- tags: List[str],
193
- context: Optional[str] = None,
194
- source: str,
195
- existing: Optional[str] = None,
196
- ) -> str:
197
- if explicit and explicit.strip():
198
- return explicit.strip()
199
-
200
- existing_map = _parse_metadata(existing) if existing else {}
201
- metadata_map: Dict[str, str] = {}
202
-
203
- metadata_map["t"] = existing_map.get("t", "memory")
204
- metadata_map["src"] = _slugify(source) if source else existing_map.get("src", "mcp")
205
-
206
- ctx_value = _slugify(context) if context else existing_map.get("ctx")
207
- if ctx_value:
208
- metadata_map["ctx"] = ctx_value
209
-
210
- priority_key = importance.lower() if importance else existing_map.get("p")
211
- if priority_key:
212
- metadata_map["p"] = priority_key
213
-
214
- tag_string = _format_tags(tags)
215
- if tag_string:
216
- metadata_map["tags"] = tag_string
217
- elif "tags" in existing_map:
218
- metadata_map["tags"] = existing_map["tags"]
219
-
220
- for key, value in existing_map.items():
221
- if key not in metadata_map:
222
- metadata_map[key] = value
223
-
224
- if not metadata_map:
225
- metadata_map["t"] = "memory"
226
- metadata_map["src"] = "mcp"
227
-
228
- return "|".join(f"{key}:{metadata_map[key]}" for key in metadata_map)
229
-
230
-
231
- def _parse_metadata(metadata: str) -> Dict[str, str]:
232
- items = {}
233
- for entry in metadata.split("|"):
234
- if ":" not in entry:
235
- continue
236
- key, value = entry.split(":", 1)
237
- key = key.strip()
238
- value = value.strip()
239
- if key:
240
- items[key] = value
241
- return items
242
-
243
-
244
- def _slugify(value: Optional[str]) -> str:
245
- if not value:
246
- return ""
247
- sanitized = value.replace("|", " ").replace(":", " ")
248
- return "-".join(part for part in sanitized.split() if part).lower()
249
-
250
-
251
- def _format_tags(tags: List[str]) -> str:
252
- """
253
- Format tags list into a comma-separated string for metadata.
254
-
255
- Args:
256
- tags: List of string tags (required)
66
+ query: str,
67
+ limit: int = 5,
68
+ tags: Optional[List[str]] = None,
69
+ ) -> List[Dict[str, Any]]:
70
+ """
71
+ Search memories using semantic similarity (embeddings).
257
72
 
258
- Returns:
259
- Comma-separated string of tags
73
+ Required:
74
+ - query: Natural language search query
260
75
 
261
- Raises:
262
- TypeError: If tags is not a List[str]
263
- ValueError: If tags is empty
264
- """
265
- if not isinstance(tags, list):
266
- raise TypeError(f"tags must be List[str], got {type(tags)}")
76
+ Optional:
77
+ - limit: Maximum results (default 5)
78
+ - tags: Filter by tags
79
+ """
80
+ if not query or not query.strip():
81
+ raise ValueError("query is required")
267
82
 
268
- if not tags:
269
- raise ValueError("tags cannot be empty")
83
+ payload = {
84
+ "query": query.strip(),
85
+ "limit": limit,
86
+ }
270
87
 
271
- # Clean and validate tags
272
- cleaned_tags = []
273
- for tag in tags:
274
- if isinstance(tag, str) and tag.strip():
275
- cleaned_tags.append(tag.strip())
276
- else:
277
- raise ValueError(
278
- f"All tags must be non-empty strings, got {type(tag)}: {tag}"
279
- )
88
+ if tags:
89
+ payload["tags"] = [t.strip() for t in tags if t.strip()]
280
90
 
281
- if not cleaned_tags:
282
- raise ValueError("No valid tags found after cleaning")
91
+ return await self._call(self.api.search_memories, payload) or []
283
92
 
284
- return ",".join(sorted(set(cleaned_tags)))
93
+ async def _call(self, func, *args, **kwargs):
94
+ return await asyncio.to_thread(func, *args, **kwargs)
@@ -360,123 +360,6 @@ class KnowledgeService:
360
360
  async def sprint_cancel(self, sprint_id: str) -> Dict[str, Any]:
361
361
  return await self._call(self.api.cancel_sprint, sprint_id)
362
362
 
363
- # ------------------------------------------------------------------
364
- # Modes and rules
365
- # ------------------------------------------------------------------
366
- async def mode_create(self, payload: Dict[str, Any]) -> Dict[str, Any]:
367
- result = await self._call(self.api.create_mode, _strip_none(payload))
368
- # API returns { message, mode } - extract mode
369
- if isinstance(result, dict) and "mode" in result:
370
- return result["mode"]
371
- return result or {}
372
-
373
- async def mode_list(
374
- self,
375
- *,
376
- include_rules: Optional[bool] = None,
377
- return_description: Optional[bool] = None,
378
- return_metadata: Optional[bool] = None,
379
- ) -> List[Dict[str, Any]]:
380
- return (
381
- await self._call(
382
- self.api.list_modes,
383
- include_rules=include_rules,
384
- return_description=return_description,
385
- return_metadata=return_metadata,
386
- )
387
- or []
388
- )
389
-
390
- async def mode_get(
391
- self,
392
- mode_id: str,
393
- *,
394
- return_description: Optional[bool] = None,
395
- return_metadata: Optional[bool] = None,
396
- ) -> Dict[str, Any]:
397
- return await self._call(
398
- self.api.get_mode,
399
- mode_id,
400
- return_description=return_description,
401
- return_metadata=return_metadata,
402
- )
403
-
404
- async def mode_update(
405
- self, mode_id: str, payload: Dict[str, Any]
406
- ) -> Dict[str, Any]:
407
- result = await self._call(self.api.update_mode, mode_id, _strip_none(payload))
408
- # API returns { message, mode } - extract mode
409
- if isinstance(result, dict) and "mode" in result:
410
- return result["mode"]
411
- return result or {}
412
-
413
- async def mode_delete(self, mode_id: str) -> None:
414
- await self._call(self.api.delete_mode, mode_id)
415
-
416
- async def mode_rule_add(self, mode_id: str, rule_id: str) -> Dict[str, Any]:
417
- return await self._call(self.api.add_mode_rule, mode_id, rule_id)
418
-
419
- async def mode_rule_remove(self, mode_id: str, rule_id: str) -> None:
420
- await self._call(self.api.remove_mode_rule, mode_id, rule_id)
421
-
422
- async def mode_rules(self, mode_id: str) -> List[Dict[str, Any]]:
423
- return await self._call(self.api.list_rules_by_mode, mode_id) or []
424
-
425
- async def mode_rules_for_rule(self, rule_id: str) -> List[Dict[str, Any]]:
426
- return await self._call(self.api.list_modes_by_rule, rule_id) or []
427
-
428
- async def rule_create(self, payload: Dict[str, Any]) -> Dict[str, Any]:
429
- result = await self._call(self.api.create_rule, _strip_none(payload))
430
- # API returns { message, rule } - extract rule
431
- if isinstance(result, dict) and "rule" in result:
432
- return result["rule"]
433
- return result or {}
434
-
435
- async def rule_list(
436
- self,
437
- *,
438
- return_description: Optional[bool] = None,
439
- return_metadata: Optional[bool] = None,
440
- return_modes: Optional[bool] = None,
441
- ) -> List[Dict[str, Any]]:
442
- return (
443
- await self._call(
444
- self.api.list_rules,
445
- return_description=return_description,
446
- return_metadata=return_metadata,
447
- return_modes=return_modes,
448
- )
449
- or []
450
- )
451
-
452
- async def rule_get(
453
- self,
454
- rule_id: str,
455
- *,
456
- return_description: Optional[bool] = None,
457
- return_metadata: Optional[bool] = None,
458
- return_modes: Optional[bool] = None,
459
- ) -> Dict[str, Any]:
460
- return await self._call(
461
- self.api.get_rule,
462
- rule_id,
463
- return_description=return_description,
464
- return_metadata=return_metadata,
465
- return_modes=return_modes,
466
- )
467
-
468
- async def rule_update(
469
- self, rule_id: str, payload: Dict[str, Any]
470
- ) -> Dict[str, Any]:
471
- result = await self._call(self.api.update_rule, rule_id, _strip_none(payload))
472
- # API returns { message, rule } - extract rule
473
- if isinstance(result, dict) and "rule" in result:
474
- return result["rule"]
475
- return result or {}
476
-
477
- async def rule_delete(self, rule_id: str) -> None:
478
- await self._call(self.api.delete_rule, rule_id)
479
-
480
363
  # ------------------------------------------------------------------
481
364
  # Documentation
482
365
  # ------------------------------------------------------------------
@@ -549,6 +432,62 @@ class KnowledgeService:
549
432
  self.api.duplicate_documentation_item, doc_id, _strip_none(payload)
550
433
  )
551
434
 
435
+ # ------------------------------------------------------------------
436
+ # Rules
437
+ # ------------------------------------------------------------------
438
+ async def rule_create(self, payload: Dict[str, Any]) -> Dict[str, Any]:
439
+ return await self._call_dict(self.api.create_rule, _strip_none(payload))
440
+
441
+ async def rule_list(self, **filters: Any) -> List[Dict[str, Any]]:
442
+ return await self._call_list(self.api.list_rules, **_strip_none(filters))
443
+
444
+ async def rule_get(self, rule_id: str) -> Dict[str, Any]:
445
+ return await self._call_dict(self.api.get_rule, rule_id)
446
+
447
+ async def rule_update(
448
+ self, rule_id: str, payload: Dict[str, Any]
449
+ ) -> Dict[str, Any]:
450
+ return await self._call_dict(
451
+ self.api.update_rule, rule_id, _strip_none(payload)
452
+ )
453
+
454
+ async def rule_delete(self, rule_id: str) -> None:
455
+ await self._call(self.api.delete_rule, rule_id)
456
+
457
+ async def rule_marketplace(self, **filters: Any) -> List[Dict[str, Any]]:
458
+ return await self._call_list(
459
+ self.api.list_marketplace_rules, **_strip_none(filters)
460
+ )
461
+
462
+ async def rule_fork(self, rule_id: str, payload: Dict[str, Any]) -> Dict[str, Any]:
463
+ return await self._call_dict(self.api.fork_rule, rule_id, _strip_none(payload))
464
+
465
+ async def rule_export(self, rule_id: str, format: str) -> str:
466
+ result = await self._call(self.api.export_rule, rule_id, format)
467
+ return result if isinstance(result, str) else str(result or "")
468
+
469
+ # ------------------------------------------------------------------
470
+ # API Catalog
471
+ # ------------------------------------------------------------------
472
+ async def api_catalog_list(self, **filters: Any) -> List[Dict[str, Any]]:
473
+ return await self._call_list(self.api.list_api_catalog, **_strip_none(filters))
474
+
475
+ async def api_catalog_get(self, spec_id: str) -> Dict[str, Any]:
476
+ return await self._call_dict(self.api.get_api_catalog, spec_id)
477
+
478
+ async def api_catalog_search(
479
+ self,
480
+ *,
481
+ query: str,
482
+ limit: int = 10,
483
+ ) -> Dict[str, Any]:
484
+ result = await self._call(
485
+ self.api.search_api_catalog_semantic,
486
+ query=query,
487
+ limit=limit,
488
+ )
489
+ return _ensure_dict(result) if isinstance(result, dict) else {"data": result}
490
+
552
491
 
553
492
  __all__ = [
554
493
  "KnowledgeService",