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.
- fenix_mcp/__init__.py +1 -1
- fenix_mcp/application/tools/initialize.py +18 -56
- fenix_mcp/application/tools/intelligence.py +130 -304
- fenix_mcp/application/tools/knowledge.py +567 -257
- fenix_mcp/domain/initialization.py +11 -112
- fenix_mcp/domain/intelligence.py +57 -247
- fenix_mcp/domain/knowledge.py +56 -117
- fenix_mcp/infrastructure/fenix_api/client.py +158 -122
- fenix_mcp/interface/mcp_server.py +12 -0
- fenix_mcp-2.0.0.dist-info/METADATA +341 -0
- {fenix_mcp-1.13.0.dist-info → fenix_mcp-2.0.0.dist-info}/RECORD +14 -14
- {fenix_mcp-1.13.0.dist-info → fenix_mcp-2.0.0.dist-info}/WHEEL +1 -1
- fenix_mcp-1.13.0.dist-info/METADATA +0 -258
- {fenix_mcp-1.13.0.dist-info → fenix_mcp-2.0.0.dist-info}/entry_points.txt +0 -0
- {fenix_mcp-1.13.0.dist-info → fenix_mcp-2.0.0.dist-info}/top_level.txt +0 -0
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
fenix_mcp/domain/intelligence.py
CHANGED
|
@@ -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,
|
|
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
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
|
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
|
-
"
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
-
|
|
259
|
-
|
|
73
|
+
Required:
|
|
74
|
+
- query: Natural language search query
|
|
260
75
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
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
|
-
|
|
269
|
-
|
|
83
|
+
payload = {
|
|
84
|
+
"query": query.strip(),
|
|
85
|
+
"limit": limit,
|
|
86
|
+
}
|
|
270
87
|
|
|
271
|
-
|
|
272
|
-
|
|
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
|
-
|
|
282
|
-
raise ValueError("No valid tags found after cleaning")
|
|
91
|
+
return await self._call(self.api.search_memories, payload) or []
|
|
283
92
|
|
|
284
|
-
|
|
93
|
+
async def _call(self, func, *args, **kwargs):
|
|
94
|
+
return await asyncio.to_thread(func, *args, **kwargs)
|
fenix_mcp/domain/knowledge.py
CHANGED
|
@@ -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",
|