fenix-mcp 0.1.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.
@@ -0,0 +1,905 @@
1
+ # SPDX-License-Identifier: MIT
2
+ """Knowledge tool implementation updated for the expanded Fênix API."""
3
+
4
+ from __future__ import annotations
5
+
6
+ from enum import Enum
7
+ from typing import Any, Dict, List, Optional
8
+
9
+ from pydantic import Field
10
+
11
+ from fenix_mcp.application.presenters import text
12
+ from fenix_mcp.application.tool_base import Tool, ToolRequest
13
+ from fenix_mcp.domain.knowledge import KnowledgeService, _format_date, _strip_none
14
+ from fenix_mcp.infrastructure.context import AppContext
15
+
16
+
17
+ class KnowledgeAction(str, Enum):
18
+ def __new__(cls, value: str, description: str):
19
+ obj = str.__new__(cls, value)
20
+ obj._value_ = value
21
+ obj.description = description
22
+ return obj
23
+
24
+ # Work items
25
+ WORK_CREATE = ("work_create", "Cria um work item com título, status e vínculos opcionais.")
26
+ WORK_LIST = ("work_list", "Lista work items com filtros de status, prioridade e contexto.")
27
+ WORK_GET = ("work_get", "Obtém detalhes completos de um work item pelo ID.")
28
+ WORK_UPDATE = ("work_update", "Atualiza campos específicos de um work item existente.")
29
+ WORK_DELETE = ("work_delete", "Remove um work item definitivamente.")
30
+ WORK_BACKLOG = ("work_backlog", "Lista itens do backlog de um time.")
31
+ WORK_SEARCH = ("work_search", "Busca work items por texto com filtros adicionais.")
32
+ WORK_ANALYTICS = ("work_analytics", "Retorna métricas consolidadas de work items.")
33
+ WORK_BY_BOARD = ("work_by_board", "Lista work items associados a um board.")
34
+ WORK_BY_SPRINT = ("work_by_sprint", "Lista work items associados a um sprint.")
35
+
36
+ # Boards
37
+ BOARD_LIST = ("board_list", "Lista boards disponíveis com filtros opcionais.")
38
+ BOARD_BY_TEAM = ("board_by_team", "Lista boards de um time específico.")
39
+ BOARD_FAVORITES = ("board_favorites", "Lista boards marcados como favoritos.")
40
+ BOARD_GET = ("board_get", "Obtém detalhes de um board pelo ID.")
41
+ BOARD_COLUMNS = ("board_columns", "Lista colunas configuradas para um board.")
42
+
43
+ # Sprints
44
+ SPRINT_LIST = ("sprint_list", "Lista sprints disponíveis com filtros opcionais.")
45
+ SPRINT_BY_TEAM = ("sprint_by_team", "Lista sprints associados a um time.")
46
+ SPRINT_ACTIVE = ("sprint_active", "Obtém o sprint ativo de um time.")
47
+ SPRINT_GET = ("sprint_get", "Obtém detalhes de um sprint pelo ID.")
48
+ SPRINT_WORK_ITEMS = ("sprint_work_items", "Lista work items vinculados a um sprint.")
49
+
50
+ # Modes
51
+ MODE_CREATE = ("mode_create", "Cria um modo com conteúdo e metadados opcionais.")
52
+ MODE_LIST = ("mode_list", "Lista modes cadastrados.")
53
+ MODE_GET = ("mode_get", "Obtém detalhes completos de um modo.")
54
+ MODE_UPDATE = ("mode_update", "Atualiza propriedades de um modo existente.")
55
+ MODE_DELETE = ("mode_delete", "Remove um modo.")
56
+ MODE_RULE_ADD = ("mode_rule_add", "Associa uma regra a um modo.")
57
+ MODE_RULE_REMOVE = ("mode_rule_remove", "Remove a associação de uma regra com um modo.")
58
+ MODE_RULES = ("mode_rules", "Lista regras associadas a um modo.")
59
+
60
+ # Rules
61
+ RULE_CREATE = ("rule_create", "Cria uma regra com conteúdo e metadados.")
62
+ RULE_LIST = ("rule_list", "Lista regras cadastradas.")
63
+ RULE_GET = ("rule_get", "Obtém detalhes de uma regra.")
64
+ RULE_UPDATE = ("rule_update", "Atualiza uma regra existente.")
65
+ RULE_DELETE = ("rule_delete", "Remove uma regra.")
66
+
67
+ # Documentation
68
+ DOC_CREATE = ("doc_create", "Cria um item de documentação.")
69
+ DOC_LIST = ("doc_list", "Lista itens de documentação com filtros.")
70
+ DOC_GET = ("doc_get", "Obtém detalhes de um item de documentação.")
71
+ DOC_UPDATE = ("doc_update", "Atualiza um item de documentação.")
72
+ DOC_DELETE = ("doc_delete", "Remove um item de documentação.")
73
+ DOC_SEARCH = ("doc_search", "Busca itens de documentação por texto.")
74
+ DOC_ROOTS = ("doc_roots", "Lista documentos raiz disponíveis.")
75
+ DOC_RECENT = ("doc_recent", "Lista documentos acessados recentemente.")
76
+ DOC_ANALYTICS = ("doc_analytics", "Retorna analytics dos documentos.")
77
+ DOC_CHILDREN = ("doc_children", "Lista documentos filhos de um item.")
78
+ DOC_TREE = ("doc_tree", "Recupera a árvore direta de um documento.")
79
+ DOC_FULL_TREE = ("doc_full_tree", "Recupera a árvore completa de documentação.")
80
+ DOC_MOVE = ("doc_move", "Move um documento para outro pai.")
81
+ DOC_PUBLISH = ("doc_publish", "Altera status de publicação de um documento.")
82
+ DOC_VERSION = ("doc_version", "Gera ou recupera versão de um documento.")
83
+ DOC_DUPLICATE = ("doc_duplicate", "Duplica um documento existente.")
84
+
85
+ HELP = ("knowledge_help", "Mostra as ações disponíveis e seus usos.")
86
+
87
+ @classmethod
88
+ def choices(cls) -> List[str]:
89
+ return [member.value for member in cls]
90
+
91
+ @classmethod
92
+ def formatted_help(cls) -> str:
93
+ lines = [
94
+ "| **Ação** | **Descrição** |",
95
+ "| --- | --- |",
96
+ ]
97
+ for member in cls:
98
+ lines.append(f"| `{member.value}` | {member.description} |")
99
+ return "\n".join(lines)
100
+
101
+
102
+ ACTION_FIELD_DESCRIPTION = (
103
+ "Ação de conhecimento. Escolha um dos valores: "
104
+ + ", ".join(f"`{member.value}` ({member.description.rstrip('.')})." for member in KnowledgeAction)
105
+ )
106
+
107
+
108
+ _ALLOWED_DOC_TYPES = {
109
+ "folder",
110
+ "page",
111
+ "api_doc",
112
+ "guide",
113
+ }
114
+
115
+
116
+ class KnowledgeRequest(ToolRequest):
117
+ action: KnowledgeAction = Field(description=ACTION_FIELD_DESCRIPTION)
118
+ id: Optional[str] = Field(default=None, description="ID principal do recurso.")
119
+ limit: int = Field(default=20, ge=1, le=100, description="Limite de resultados.")
120
+ offset: int = Field(default=0, ge=0, description="Offset de paginação (quando suportado).")
121
+ team_id: Optional[str] = Field(default=None, description="ID do time para filtros (boards/sprints/docs).")
122
+ board_id: Optional[str] = Field(default=None, description="ID do board associado.")
123
+ sprint_id: Optional[str] = Field(default=None, description="ID do sprint associado.")
124
+ epic_id: Optional[str] = Field(default=None, description="ID do épico associado.")
125
+ query: Optional[str] = Field(default=None, description="Filtro/busca.")
126
+ return_content: Optional[bool] = Field(default=None, description="Retorna conteúdo completo.")
127
+ return_description: Optional[bool] = Field(default=None, description="Retorna descrição completa.")
128
+ return_metadata: Optional[bool] = Field(default=None, description="Retorna metadados completos.")
129
+
130
+ # Work item fields
131
+ work_title: Optional[str] = Field(default=None, description="Título do work item.")
132
+ work_description: Optional[str] = Field(default=None, description="Descrição do work item.")
133
+ work_type: Optional[str] = Field(default="task", description="Tipo do work item.")
134
+ work_status: Optional[str] = Field(default=None, description="Status do work item.")
135
+ work_priority: Optional[str] = Field(default=None, description="Prioridade do work item.")
136
+ story_points: Optional[int] = Field(default=None, description="Story points.")
137
+ assignee_id: Optional[str] = Field(default=None, description="ID do responsável.")
138
+ parent_id: Optional[str] = Field(default=None, description="ID do item pai.")
139
+
140
+ # Mode fields
141
+ mode_id: Optional[str] = Field(default=None, description="ID do modo relacionado.")
142
+ mode_name: Optional[str] = Field(default=None, description="Nome do modo.")
143
+ mode_description: Optional[str] = Field(default=None, description="Descrição do modo.")
144
+ mode_content: Optional[str] = Field(default=None, description="Conteúdo do modo.")
145
+ mode_is_default: Optional[bool] = Field(default=None, description="Indica se o modo é padrão.")
146
+
147
+ # Rule fields
148
+ rule_id: Optional[str] = Field(default=None, description="ID da regra relacionada.")
149
+ rule_name: Optional[str] = Field(default=None, description="Nome da regra.")
150
+ rule_description: Optional[str] = Field(default=None, description="Descrição da regra.")
151
+ rule_content: Optional[str] = Field(default=None, description="Conteúdo da regra.")
152
+ rule_is_default: Optional[bool] = Field(default=None, description="Regra padrão.")
153
+
154
+ # Documentation fields
155
+ doc_title: Optional[str] = Field(default=None, description="Título da documentação.")
156
+ doc_description: Optional[str] = Field(default=None, description="Descrição.")
157
+ doc_content: Optional[str] = Field(default=None, description="Conteúdo da documentação.")
158
+ doc_status: Optional[str] = Field(default=None, description="Status da documentação.")
159
+ doc_type: Optional[str] = Field(default=None, description="Tipo da documentação.")
160
+ doc_language: Optional[str] = Field(default=None, description="Idioma da documentação.")
161
+ doc_parent_id: Optional[str] = Field(default=None, description="Documento pai.")
162
+ doc_team_id: Optional[str] = Field(default=None, description="Time responsável pela documentação.")
163
+ doc_owner_id: Optional[str] = Field(default=None, description="ID do dono.")
164
+ doc_reviewer_id: Optional[str] = Field(default=None, description="ID do revisor.")
165
+ doc_version: Optional[str] = Field(default=None, description="Versão.")
166
+ doc_category: Optional[str] = Field(default=None, description="Categoria.")
167
+ doc_tags: Optional[List[str]] = Field(default=None, description="Tags.")
168
+ doc_position: Optional[int] = Field(default=None, description="Posição desejada ao mover documentos.")
169
+ doc_emoji: Optional[str] = Field(default=None, description="Emoji exibido junto ao documento.")
170
+ doc_emote: Optional[str] = Field(default=None, description="Alias para emoji, mantido por compatibilidade.")
171
+
172
+
173
+ class KnowledgeTool(Tool):
174
+ name = "knowledge"
175
+ description = "Operações de conhecimento do Fênix Cloud (Work Items, Boards, Sprints, Modes, Rules, Docs)."
176
+ request_model = KnowledgeRequest
177
+
178
+ def __init__(self, context: AppContext):
179
+ self._context = context
180
+ self._service = KnowledgeService(context.api_client, context.logger)
181
+
182
+ async def run(self, payload: KnowledgeRequest, context: AppContext):
183
+ action = payload.action
184
+ if action is KnowledgeAction.HELP:
185
+ return await self._handle_help()
186
+ if action.value.startswith("work_"):
187
+ return await self._run_work(payload)
188
+ if action.value.startswith("board_"):
189
+ return await self._run_board(payload)
190
+ if action.value.startswith("sprint_"):
191
+ return await self._run_sprint(payload)
192
+ if action.value.startswith("mode_"):
193
+ return await self._run_mode(payload)
194
+ if action.value.startswith("rule_"):
195
+ return await self._run_rule(payload)
196
+ if action.value.startswith("doc_"):
197
+ return await self._run_doc(payload)
198
+ return text(
199
+ "❌ Ação inválida para knowledge.\n\nEscolha um dos valores:\n"
200
+ + "\n".join(f"- `{value}`" for value in KnowledgeAction.choices())
201
+ )
202
+
203
+ # ------------------------------------------------------------------
204
+ # Work items
205
+ # ------------------------------------------------------------------
206
+ async def _run_work(self, payload: KnowledgeRequest):
207
+ action = payload.action
208
+ if action is KnowledgeAction.WORK_CREATE:
209
+ if not payload.work_title:
210
+ return text("❌ Informe work_title para criar o item.")
211
+ work = await self._service.work_create(
212
+ {
213
+ "title": payload.work_title,
214
+ "description": payload.work_description,
215
+ "item_type": payload.work_type,
216
+ "status": payload.work_status,
217
+ "priority": payload.work_priority,
218
+ "story_points": payload.story_points,
219
+ "assignee_id": payload.assignee_id,
220
+ "sprint_id": payload.sprint_id,
221
+ "board_id": payload.board_id,
222
+ "parent_id": payload.parent_id,
223
+ }
224
+ )
225
+ return text(_format_work(work, header="✅ Work item criado"))
226
+
227
+ if action is KnowledgeAction.WORK_LIST:
228
+ items = await self._service.work_list(
229
+ limit=payload.limit,
230
+ offset=payload.offset,
231
+ status=payload.work_status,
232
+ priority=payload.work_priority,
233
+ type=payload.work_type,
234
+ assignee=payload.assignee_id,
235
+ sprint=payload.sprint_id,
236
+ board=payload.board_id,
237
+ )
238
+ if not items:
239
+ return text("🎯 Nenhum work item encontrado.")
240
+ body = "\n\n".join(_format_work(item) for item in items)
241
+ return text(f"🎯 **Work items ({len(items)}):**\n\n{body}")
242
+
243
+ if action is KnowledgeAction.WORK_GET:
244
+ if not payload.id:
245
+ return text("❌ Informe o ID do work item.")
246
+ work = await self._service.work_get(payload.id)
247
+ return text(_format_work(work, header="🎯 Detalhes do work item"))
248
+
249
+ if action is KnowledgeAction.WORK_UPDATE:
250
+ if not payload.id:
251
+ return text("❌ Informe o ID do work item.")
252
+ work = await self._service.work_update(
253
+ payload.id,
254
+ {
255
+ "title": payload.work_title,
256
+ "description": payload.work_description,
257
+ "item_type": payload.work_type,
258
+ "status": payload.work_status,
259
+ "priority": payload.work_priority,
260
+ "story_points": payload.story_points,
261
+ "assignee_id": payload.assignee_id,
262
+ "sprint_id": payload.sprint_id,
263
+ "board_id": payload.board_id,
264
+ "parent_id": payload.parent_id,
265
+ },
266
+ )
267
+ return text(_format_work(work, header="✅ Work item atualizado"))
268
+
269
+ if action is KnowledgeAction.WORK_DELETE:
270
+ if not payload.id:
271
+ return text("❌ Informe o ID do work item.")
272
+ await self._service.work_delete(payload.id)
273
+ return text(f"🗑️ Work item {payload.id} removido.")
274
+
275
+ if action is KnowledgeAction.WORK_BACKLOG:
276
+ if not payload.team_id:
277
+ return text("❌ Informe team_id para consultar o backlog.")
278
+ items = await self._service.work_backlog(team_id=payload.team_id)
279
+ if not items:
280
+ return text("📋 Backlog vazio para o time informado.")
281
+ body = "\n\n".join(_format_work(item) for item in items)
282
+ return text(f"📋 **Backlog ({len(items)}):**\n\n{body}")
283
+
284
+ if action is KnowledgeAction.WORK_SEARCH:
285
+ if not payload.query or not payload.team_id:
286
+ return text("❌ Informe query e team_id para buscar work items.")
287
+ items = await self._service.work_search(
288
+ query=payload.query,
289
+ team_id=payload.team_id,
290
+ limit=payload.limit,
291
+ )
292
+ if not items:
293
+ return text("🔍 Nenhum work item encontrado.")
294
+ body = "\n\n".join(_format_work(item) for item in items)
295
+ return text(f"🔍 **Resultados ({len(items)}):**\n\n{body}")
296
+
297
+ if action is KnowledgeAction.WORK_ANALYTICS:
298
+ if not payload.team_id:
299
+ return text("❌ Informe team_id para obter analytics.")
300
+ analytics = await self._service.work_analytics(team_id=payload.team_id)
301
+ lines = ["📊 **Analytics de Work Items**"]
302
+ for key, value in analytics.items():
303
+ lines.append(f"- {key}: {value}")
304
+ return text("\n".join(lines))
305
+
306
+ if action is KnowledgeAction.WORK_BY_BOARD:
307
+ if not payload.board_id:
308
+ return text("❌ Informe board_id para listar os itens.")
309
+ items = await self._service.work_by_board(board_id=payload.board_id)
310
+ if not items:
311
+ return text("🗂️ Nenhum work item para o board informado.")
312
+ body = "\n\n".join(_format_work(item) for item in items)
313
+ return text(f"🗂️ **Itens do board ({len(items)}):**\n\n{body}")
314
+
315
+ if action is KnowledgeAction.WORK_BY_SPRINT:
316
+ if not payload.sprint_id:
317
+ return text("❌ Informe sprint_id para listar os itens.")
318
+ items = await self._service.work_by_sprint(sprint_id=payload.sprint_id)
319
+ if not items:
320
+ return text("🏃 Nenhum item vinculado ao sprint informado.")
321
+ body = "\n\n".join(_format_work(item) for item in items)
322
+ return text(f"🏃 **Work items do sprint ({len(items)}):**\n\n{body}")
323
+
324
+ return text(
325
+ "❌ Ação de work item não suportada.\n\nEscolha um dos valores:\n"
326
+ + "\n".join(f"- `{value}`" for value in KnowledgeAction.choices() if value.startswith("work_"))
327
+ )
328
+
329
+ # ------------------------------------------------------------------
330
+ # Boards
331
+ # ------------------------------------------------------------------
332
+ async def _run_board(self, payload: KnowledgeRequest):
333
+ action = payload.action
334
+ if action is KnowledgeAction.BOARD_LIST:
335
+ boards = await self._service.board_list(limit=payload.limit, offset=payload.offset)
336
+ if not boards:
337
+ return text("🗂️ Nenhum board encontrado.")
338
+ body = "\n\n".join(_format_board(board) for board in boards)
339
+ return text(f"🗂️ **Boards ({len(boards)}):**\n\n{body}")
340
+
341
+ if action is KnowledgeAction.BOARD_BY_TEAM:
342
+ if not payload.team_id:
343
+ return text("❌ Informe team_id para listar boards do time.")
344
+ boards = await self._service.board_list_by_team(payload.team_id)
345
+ if not boards:
346
+ return text("🗂️ Nenhum board cadastrado para o time.")
347
+ body = "\n\n".join(_format_board(board) for board in boards)
348
+ return text(f"🗂️ **Boards do time ({len(boards)}):**\n\n{body}")
349
+
350
+ if action is KnowledgeAction.BOARD_FAVORITES:
351
+ boards = await self._service.board_favorites()
352
+ if not boards:
353
+ return text("⭐ Nenhum board favorito cadastrado.")
354
+ body = "\n\n".join(_format_board(board) for board in boards)
355
+ return text(f"⭐ **Boards favoritos ({len(boards)}):**\n\n{body}")
356
+
357
+ if action is KnowledgeAction.BOARD_GET:
358
+ if not payload.board_id:
359
+ return text("❌ Informe board_id para consultar detalhes.")
360
+ board = await self._service.board_get(payload.board_id)
361
+ return text(_format_board(board, header="🗂️ Detalhes do board"))
362
+
363
+ if action is KnowledgeAction.BOARD_COLUMNS:
364
+ if not payload.board_id:
365
+ return text("❌ Informe board_id para listar colunas.")
366
+ columns = await self._service.board_columns(payload.board_id)
367
+ if not columns:
368
+ return text("📊 Board sem colunas cadastradas.")
369
+ body = "\n".join(f"- {col.get('name', 'Sem nome')} (ID: {col.get('id')})" for col in columns)
370
+ return text(f"📊 **Colunas do board:**\n{body}")
371
+
372
+ return text(
373
+ "❌ Ação de board não suportada.\n\nEscolha um dos valores:\n"
374
+ + "\n".join(f"- `{value}`" for value in KnowledgeAction.choices() if value.startswith("board_"))
375
+ )
376
+
377
+ # ------------------------------------------------------------------
378
+ # Sprints
379
+ # ------------------------------------------------------------------
380
+ async def _run_sprint(self, payload: KnowledgeRequest):
381
+ action = payload.action
382
+ if action is KnowledgeAction.SPRINT_LIST:
383
+ sprints = await self._service.sprint_list(limit=payload.limit, offset=payload.offset)
384
+ if not sprints:
385
+ return text("🏃 Nenhum sprint encontrado.")
386
+ body = "\n\n".join(_format_sprint(sprint) for sprint in sprints)
387
+ return text(f"🏃 **Sprints ({len(sprints)}):**\n\n{body}")
388
+
389
+ if action is KnowledgeAction.SPRINT_BY_TEAM:
390
+ if not payload.team_id:
391
+ return text("❌ Informe team_id para listar sprints do time.")
392
+ sprints = await self._service.sprint_list_by_team(payload.team_id)
393
+ if not sprints:
394
+ return text("🏃 Nenhum sprint cadastrado para o time.")
395
+ body = "\n\n".join(_format_sprint(sprint) for sprint in sprints)
396
+ return text(f"🏃 **Sprints do time ({len(sprints)}):**\n\n{body}")
397
+
398
+ if action is KnowledgeAction.SPRINT_ACTIVE:
399
+ if not payload.team_id:
400
+ return text("❌ Informe team_id para consultar o sprint ativo.")
401
+ sprint = await self._service.sprint_active(payload.team_id)
402
+ if not sprint:
403
+ return text("⏳ Nenhum sprint ativo no momento.")
404
+ return text(_format_sprint(sprint, header="⏳ Sprint ativo"))
405
+
406
+ if action is KnowledgeAction.SPRINT_GET:
407
+ if not payload.sprint_id:
408
+ return text("❌ Informe sprint_id para consultar detalhes.")
409
+ sprint = await self._service.sprint_get(payload.sprint_id)
410
+ return text(_format_sprint(sprint, header="🏃 Detalhes do sprint"))
411
+
412
+ if action is KnowledgeAction.SPRINT_WORK_ITEMS:
413
+ if not payload.sprint_id:
414
+ return text("❌ Informe sprint_id para listar os itens.")
415
+ items = await self._service.sprint_work_items(payload.sprint_id)
416
+ if not items:
417
+ return text("🏃 Nenhum item vinculado ao sprint informado.")
418
+ body = "\n\n".join(_format_work(item) for item in items)
419
+ return text(f"🏃 **Itens do sprint ({len(items)}):**\n\n{body}")
420
+
421
+ return text(
422
+ "❌ Ação de sprint não suportada.\n\nEscolha um dos valores:\n"
423
+ + "\n".join(f"- `{value}`" for value in KnowledgeAction.choices() if value.startswith("sprint_"))
424
+ )
425
+
426
+ # ------------------------------------------------------------------
427
+ # Modes
428
+ # ------------------------------------------------------------------
429
+ async def _run_mode(self, payload: KnowledgeRequest):
430
+ action = payload.action
431
+ if action is KnowledgeAction.MODE_CREATE:
432
+ if not payload.mode_name:
433
+ return text("❌ Informe mode_name para criar o modo.")
434
+ mode = await self._service.mode_create(
435
+ {
436
+ "name": payload.mode_name,
437
+ "description": payload.mode_description,
438
+ "content": payload.mode_content,
439
+ "is_default": payload.mode_is_default,
440
+ }
441
+ )
442
+ return text(_format_mode(mode, header="✅ Modo criado"))
443
+
444
+ if action is KnowledgeAction.MODE_LIST:
445
+ modes = await self._service.mode_list(
446
+ include_rules=payload.return_metadata,
447
+ return_description=payload.return_description,
448
+ return_metadata=payload.return_metadata,
449
+ )
450
+ if not modes:
451
+ return text("🎭 Nenhum modo encontrado.")
452
+ body = "\n\n".join(_format_mode(mode) for mode in modes)
453
+ return text(f"🎭 **Modes ({len(modes)}):**\n\n{body}")
454
+
455
+ if action is KnowledgeAction.MODE_GET:
456
+ if not payload.mode_id:
457
+ return text("❌ Informe mode_id para consultar detalhes.")
458
+ mode = await self._service.mode_get(
459
+ payload.mode_id,
460
+ return_description=payload.return_description,
461
+ return_metadata=payload.return_metadata,
462
+ )
463
+ return text(_format_mode(mode, header="🎭 Detalhes do modo"))
464
+
465
+ if action is KnowledgeAction.MODE_UPDATE:
466
+ if not payload.mode_id:
467
+ return text("❌ Informe mode_id para atualizar.")
468
+ mode = await self._service.mode_update(
469
+ payload.mode_id,
470
+ {
471
+ "name": payload.mode_name,
472
+ "description": payload.mode_description,
473
+ "content": payload.mode_content,
474
+ "is_default": payload.mode_is_default,
475
+ },
476
+ )
477
+ return text(_format_mode(mode, header="✅ Modo atualizado"))
478
+
479
+ if action is KnowledgeAction.MODE_DELETE:
480
+ if not payload.mode_id:
481
+ return text("❌ Informe mode_id para remover.")
482
+ await self._service.mode_delete(payload.mode_id)
483
+ return text(f"🗑️ Modo {payload.mode_id} removido.")
484
+
485
+ if action is KnowledgeAction.MODE_RULE_ADD:
486
+ if not payload.mode_id or not payload.rule_id:
487
+ return text("❌ Informe mode_id e rule_id para associar.")
488
+ link = await self._service.mode_rule_add(payload.mode_id, payload.rule_id)
489
+ return text(
490
+ "\n".join(
491
+ [
492
+ "🔗 **Regra associada ao modo!**",
493
+ f"Modo: {link.get('modeId', payload.mode_id)}",
494
+ f"Regra: {link.get('ruleId', payload.rule_id)}",
495
+ ]
496
+ )
497
+ )
498
+
499
+ if action is KnowledgeAction.MODE_RULE_REMOVE:
500
+ if not payload.mode_id or not payload.rule_id:
501
+ return text("❌ Informe mode_id e rule_id para remover a associação.")
502
+ await self._service.mode_rule_remove(payload.mode_id, payload.rule_id)
503
+ return text("🔗 Associação removida.")
504
+
505
+ if action is KnowledgeAction.MODE_RULES:
506
+ if payload.mode_id:
507
+ rules = await self._service.mode_rules(payload.mode_id)
508
+ context_label = f"modo {payload.mode_id}"
509
+ elif payload.rule_id:
510
+ rules = await self._service.mode_rules_for_rule(payload.rule_id)
511
+ context_label = f"regra {payload.rule_id}"
512
+ else:
513
+ return text("❌ Informe mode_id ou rule_id para listar associações.")
514
+ if not rules:
515
+ return text("🔗 Nenhuma associação encontrada.")
516
+ body = "\n".join(
517
+ f"- {item.get('name', 'Sem nome')} (ID: {item.get('id')})" for item in rules
518
+ )
519
+ return text(f"🔗 **Associações para {context_label}:**\n{body}")
520
+
521
+ return text(
522
+ "❌ Ação de modo não suportada.\n\nEscolha um dos valores:\n"
523
+ + "\n".join(f"- `{value}`" for value in KnowledgeAction.choices() if value.startswith("mode_"))
524
+ )
525
+
526
+ # ------------------------------------------------------------------
527
+ # Rules
528
+ # ------------------------------------------------------------------
529
+ async def _run_rule(self, payload: KnowledgeRequest):
530
+ action = payload.action
531
+ if action is KnowledgeAction.RULE_CREATE:
532
+ if not payload.rule_name or not payload.rule_content:
533
+ return text("❌ Informe rule_name e rule_content.")
534
+ rule = await self._service.rule_create(
535
+ {
536
+ "name": payload.rule_name,
537
+ "description": payload.rule_description,
538
+ "content": payload.rule_content,
539
+ "is_default": payload.rule_is_default,
540
+ }
541
+ )
542
+ return text(_format_rule(rule, header="✅ Regra criada"))
543
+
544
+ if action is KnowledgeAction.RULE_LIST:
545
+ rules = await self._service.rule_list(
546
+ return_description=payload.return_description,
547
+ return_metadata=payload.return_metadata,
548
+ return_modes=payload.return_metadata,
549
+ )
550
+ if not rules:
551
+ return text("📋 Nenhuma regra encontrada.")
552
+ body = "\n\n".join(_format_rule(rule) for rule in rules)
553
+ return text(f"📋 **Regras ({len(rules)}):**\n\n{body}")
554
+
555
+ if action is KnowledgeAction.RULE_GET:
556
+ if not payload.rule_id:
557
+ return text("❌ Informe rule_id para consultar detalhes.")
558
+ rule = await self._service.rule_get(
559
+ payload.rule_id,
560
+ return_description=payload.return_description,
561
+ return_metadata=payload.return_metadata,
562
+ return_modes=payload.return_metadata,
563
+ )
564
+ return text(_format_rule(rule, header="📋 Detalhes da regra"))
565
+
566
+ if action is KnowledgeAction.RULE_UPDATE:
567
+ if not payload.rule_id:
568
+ return text("❌ Informe rule_id para atualizar.")
569
+ rule = await self._service.rule_update(
570
+ payload.rule_id,
571
+ {
572
+ "name": payload.rule_name,
573
+ "description": payload.rule_description,
574
+ "content": payload.rule_content,
575
+ "is_default": payload.rule_is_default,
576
+ },
577
+ )
578
+ return text(_format_rule(rule, header="✅ Regra atualizada"))
579
+
580
+ if action is KnowledgeAction.RULE_DELETE:
581
+ if not payload.rule_id:
582
+ return text("❌ Informe rule_id para remover.")
583
+ await self._service.rule_delete(payload.rule_id)
584
+ return text(f"🗑️ Regra {payload.rule_id} removida.")
585
+
586
+ return text(
587
+ "❌ Ação de regra não suportada.\n\nEscolha um dos valores:\n"
588
+ + "\n".join(f"- `{value}`" for value in KnowledgeAction.choices() if value.startswith("rule_"))
589
+ )
590
+
591
+ # ------------------------------------------------------------------
592
+ # Documentation
593
+ # ------------------------------------------------------------------
594
+ async def _run_doc(self, payload: KnowledgeRequest):
595
+ action = payload.action
596
+ if action is KnowledgeAction.DOC_CREATE:
597
+ if not payload.doc_title:
598
+ return text("❌ Informe doc_title para criar a documentação.")
599
+ if payload.doc_type and payload.doc_type not in _ALLOWED_DOC_TYPES:
600
+ allowed = ", ".join(sorted(_ALLOWED_DOC_TYPES))
601
+ return text(
602
+ "❌ doc_type inválido. Use um dos valores suportados: " + allowed
603
+ )
604
+ doc = await self._service.doc_create(
605
+ {
606
+ "title": payload.doc_title,
607
+ "description": payload.doc_description,
608
+ "content": payload.doc_content,
609
+ "status": payload.doc_status,
610
+ "doc_type": payload.doc_type,
611
+ "language": payload.doc_language,
612
+ "parent_id": payload.doc_parent_id,
613
+ "team_id": payload.doc_team_id or payload.team_id,
614
+ "owner_id": payload.doc_owner_id,
615
+ "reviewer_id": payload.doc_reviewer_id,
616
+ "version": payload.doc_version,
617
+ "category": payload.doc_category,
618
+ "tags": payload.doc_tags,
619
+ "emoji": payload.doc_emoji or payload.doc_emote,
620
+ }
621
+ )
622
+ return text(_format_doc(doc, header="✅ Documentação criada"))
623
+
624
+ if action is KnowledgeAction.DOC_LIST:
625
+ docs = await self._service.doc_list(
626
+ limit=payload.limit,
627
+ offset=payload.offset,
628
+ returnContent=payload.return_content,
629
+ )
630
+ if not docs:
631
+ return text("📄 Nenhuma documentação encontrada.")
632
+ body = "\n\n".join(_format_doc(doc) for doc in docs)
633
+ return text(f"📄 **Documentos ({len(docs)}):**\n\n{body}")
634
+
635
+ if action is KnowledgeAction.DOC_GET:
636
+ if not payload.id:
637
+ return text("❌ Informe o ID da documentação.")
638
+ doc = await self._service.doc_get(
639
+ payload.id,
640
+ returnContent=payload.return_content,
641
+ )
642
+ return text(_format_doc(doc, header="📄 Detalhes da documentação"))
643
+
644
+ if action is KnowledgeAction.DOC_UPDATE:
645
+ if not payload.id:
646
+ return text("❌ Informe o ID da documentação.")
647
+ if payload.doc_type and payload.doc_type not in _ALLOWED_DOC_TYPES:
648
+ allowed = ", ".join(sorted(_ALLOWED_DOC_TYPES))
649
+ return text(
650
+ "❌ doc_type inválido. Use um dos valores suportados: " + allowed
651
+ )
652
+ doc = await self._service.doc_update(
653
+ payload.id,
654
+ {
655
+ "title": payload.doc_title,
656
+ "description": payload.doc_description,
657
+ "content": payload.doc_content,
658
+ "status": payload.doc_status,
659
+ "doc_type": payload.doc_type,
660
+ "language": payload.doc_language,
661
+ "parent_id": payload.doc_parent_id,
662
+ "team_id": payload.doc_team_id or payload.team_id,
663
+ "owner_id": payload.doc_owner_id,
664
+ "reviewer_id": payload.doc_reviewer_id,
665
+ "version": payload.doc_version,
666
+ "category": payload.doc_category,
667
+ "tags": payload.doc_tags,
668
+ "emoji": payload.doc_emoji or payload.doc_emote,
669
+ },
670
+ )
671
+ return text(_format_doc(doc, header="✅ Documentação atualizada"))
672
+
673
+ if action is KnowledgeAction.DOC_DELETE:
674
+ if not payload.id:
675
+ return text("❌ Informe o ID da documentação.")
676
+ await self._service.doc_delete(payload.id)
677
+ return text(f"🗑️ Documentação {payload.id} removida.")
678
+
679
+ if action is KnowledgeAction.DOC_SEARCH:
680
+ if not payload.query or not (payload.doc_team_id or payload.team_id):
681
+ return text("❌ Informe query e team_id para buscar documentação.")
682
+ docs = await self._service.doc_search(
683
+ query=payload.query,
684
+ team_id=payload.doc_team_id or payload.team_id,
685
+ limit=payload.limit,
686
+ )
687
+ if not docs:
688
+ return text("🔍 Nenhum documento encontrado para os filtros informados.")
689
+ body = "\n\n".join(_format_doc(doc) for doc in docs)
690
+ return text(f"🔍 **Resultados ({len(docs)}):**\n\n{body}")
691
+
692
+ if action is KnowledgeAction.DOC_ROOTS:
693
+ if not (payload.doc_team_id or payload.team_id):
694
+ return text("❌ Informe team_id para listar raízes.")
695
+ docs = await self._service.doc_roots(team_id=payload.doc_team_id or payload.team_id)
696
+ if not docs:
697
+ return text("📚 Nenhuma raiz encontrada.")
698
+ body = "\n".join(f"- {doc.get('title', 'Sem título')} (ID: {doc.get('id')})" for doc in docs)
699
+ return text(f"📚 **Raízes de documentação:**\n{body}")
700
+
701
+ if action is KnowledgeAction.DOC_RECENT:
702
+ if not (payload.doc_team_id or payload.team_id):
703
+ return text("❌ Informe team_id para listar documentos recentes.")
704
+ docs = await self._service.doc_recent(
705
+ team_id=payload.doc_team_id or payload.team_id,
706
+ limit=payload.limit,
707
+ )
708
+ if not docs:
709
+ return text("🕒 Nenhuma documentação recente encontrada.")
710
+ body = "\n\n".join(_format_doc(doc) for doc in docs)
711
+ return text(f"🕒 **Documentos recentes ({len(docs)}):**\n\n{body}")
712
+
713
+ if action is KnowledgeAction.DOC_ANALYTICS:
714
+ if not (payload.doc_team_id or payload.team_id):
715
+ return text("❌ Informe team_id para obter analytics.")
716
+ analytics = await self._service.doc_analytics(team_id=payload.doc_team_id or payload.team_id)
717
+ lines = ["📊 **Analytics de Documentação**"]
718
+ for key, value in analytics.items():
719
+ lines.append(f"- {key}: {value}")
720
+ return text("\n".join(lines))
721
+
722
+ if action is KnowledgeAction.DOC_CHILDREN:
723
+ if not payload.id:
724
+ return text("❌ Informe o ID da documentação.")
725
+ docs = await self._service.doc_children(payload.id)
726
+ if not docs:
727
+ return text("📄 Nenhum filho cadastrado para o documento informado.")
728
+ body = "\n".join(f"- {doc.get('title', 'Sem título')} (ID: {doc.get('id')})" for doc in docs)
729
+ return text(f"📄 **Filhos:**\n{body}")
730
+
731
+ if action is KnowledgeAction.DOC_TREE:
732
+ if not payload.id:
733
+ return text("❌ Informe o ID da documentação.")
734
+ tree = await self._service.doc_tree(payload.id)
735
+ return text(f"🌳 **Árvore de documentação para {payload.id}:**\n{tree}")
736
+
737
+ if action is KnowledgeAction.DOC_FULL_TREE:
738
+ tree = await self._service.doc_full_tree()
739
+ return text(f"🌳 **Árvore completa de documentação:**\n{tree}")
740
+
741
+ if action is KnowledgeAction.DOC_MOVE:
742
+ if not payload.id:
743
+ return text("❌ Informe o ID da documentação.")
744
+ if payload.doc_parent_id is None and payload.doc_position is None:
745
+ return text("❌ Informe doc_parent_id, doc_position ou ambos para mover.")
746
+ move_payload = {
747
+ "new_parent_id": payload.doc_parent_id,
748
+ "new_position": payload.doc_position,
749
+ }
750
+ doc = await self._service.doc_move(payload.id, move_payload)
751
+ return text(_format_doc(doc, header="📦 Documentação movida"))
752
+
753
+ if action is KnowledgeAction.DOC_PUBLISH:
754
+ if not payload.id:
755
+ return text("❌ Informe o ID da documentação.")
756
+ result = await self._service.doc_publish(payload.id)
757
+ return text(f"🗞️ Documento publicado: {result}")
758
+
759
+ if action is KnowledgeAction.DOC_VERSION:
760
+ if not payload.id:
761
+ return text("❌ Informe o ID da documentação.")
762
+ if not payload.doc_version:
763
+ return text("❌ Informe doc_version com o número/identificador da versão.")
764
+ version_payload = {
765
+ "title": payload.doc_title or f"Version {payload.doc_version}",
766
+ "version": payload.doc_version,
767
+ "content": payload.doc_content,
768
+ }
769
+ doc = await self._service.doc_version(payload.id, version_payload)
770
+ return text(_format_doc(doc, header="🗞️ Nova versão criada"))
771
+
772
+ if action is KnowledgeAction.DOC_DUPLICATE:
773
+ if not payload.id:
774
+ return text("❌ Informe o ID da documentação.")
775
+ if not payload.doc_title:
776
+ return text("❌ Informe doc_title para nomear a cópia.")
777
+ doc = await self._service.doc_duplicate(
778
+ payload.id,
779
+ {
780
+ "title": payload.doc_title,
781
+ "team_id": payload.doc_team_id or payload.team_id,
782
+ },
783
+ )
784
+ return text(_format_doc(doc, header="🗂️ Documento duplicado"))
785
+
786
+ return text(
787
+ "❌ Ação de documentação não suportada.\n\nEscolha um dos valores:\n"
788
+ + "\n".join(f"- `{value}`" for value in KnowledgeAction.choices() if value.startswith("doc_"))
789
+ )
790
+
791
+ async def _handle_help(self):
792
+ return text("📚 **Ações disponíveis para knowledge**\n\n" + KnowledgeAction.formatted_help())
793
+
794
+
795
+ def _format_work(item: Dict[str, Any], *, header: Optional[str] = None) -> str:
796
+ lines: List[str] = []
797
+ if header:
798
+ lines.append(header)
799
+ lines.append("")
800
+ title = item.get("title") or item.get("name") or "Sem título"
801
+ status = item.get("status") or item.get("state") or "desconhecido"
802
+ priority = item.get("priority") or item.get("priority_level") or "indefinido"
803
+ lines.extend(
804
+ [
805
+ f"🎯 **{title}**",
806
+ f"ID: {item.get('id', 'N/A')}",
807
+ f"Status: {status}",
808
+ f"Prioridade: {priority}",
809
+ f"Responsável: {item.get('assignee_id') or item.get('assignee', 'N/A')}",
810
+ ]
811
+ )
812
+ if item.get("due_date") or item.get("dueDate"):
813
+ lines.append(f"Vencimento: {_format_date(item.get('due_date') or item.get('dueDate'))}")
814
+ return "\n".join(lines)
815
+
816
+
817
+ def _format_board(board: Dict[str, Any], header: Optional[str] = None) -> str:
818
+ lines: List[str] = []
819
+ if header:
820
+ lines.append(header)
821
+ lines.append("")
822
+ lines.extend(
823
+ [
824
+ f"🗂️ **{board.get('name', 'Sem nome')}**",
825
+ f"ID: {board.get('id', 'N/A')}",
826
+ f"Time: {board.get('team_id', 'N/A')}",
827
+ f"Colunas: {len(board.get('columns', []))}",
828
+ ]
829
+ )
830
+ return "\n".join(lines)
831
+
832
+
833
+ def _format_sprint(sprint: Dict[str, Any], header: Optional[str] = None) -> str:
834
+ lines: List[str] = []
835
+ if header:
836
+ lines.append(header)
837
+ lines.append("")
838
+ lines.extend(
839
+ [
840
+ f"🏃 **{sprint.get('name', 'Sem nome')}**",
841
+ f"ID: {sprint.get('id', 'N/A')}",
842
+ f"Status: {sprint.get('status', 'N/A')}",
843
+ f"Time: {sprint.get('team_id', 'N/A')}",
844
+ ]
845
+ )
846
+ if sprint.get("start_date") or sprint.get("startDate"):
847
+ lines.append(f"Início: {_format_date(sprint.get('start_date') or sprint.get('startDate'))}")
848
+ if sprint.get("end_date") or sprint.get("endDate"):
849
+ lines.append(f"Fim: {_format_date(sprint.get('end_date') or sprint.get('endDate'))}")
850
+ return "\n".join(lines)
851
+
852
+
853
+ def _format_mode(mode: Dict[str, Any], header: Optional[str] = None) -> str:
854
+ lines: List[str] = []
855
+ if header:
856
+ lines.append(header)
857
+ lines.append("")
858
+ lines.extend(
859
+ [
860
+ f"🎭 **{mode.get('name', 'Sem nome')}**",
861
+ f"ID: {mode.get('id', 'N/A')}",
862
+ f"Padrão: {mode.get('is_default', False)}",
863
+ ]
864
+ )
865
+ if mode.get("description"):
866
+ lines.append(f"Descrição: {mode['description']}")
867
+ return "\n".join(lines)
868
+
869
+
870
+ def _format_rule(rule: Dict[str, Any], header: Optional[str] = None) -> str:
871
+ lines: List[str] = []
872
+ if header:
873
+ lines.append(header)
874
+ lines.append("")
875
+ lines.extend(
876
+ [
877
+ f"📋 **{rule.get('name', 'Sem nome')}**",
878
+ f"ID: {rule.get('id', 'N/A')}",
879
+ f"Padrão: {rule.get('is_default', False)}",
880
+ ]
881
+ )
882
+ if rule.get("description"):
883
+ lines.append(f"Descrição: {rule['description']}")
884
+ return "\n".join(lines)
885
+
886
+
887
+ def _format_doc(doc: Dict[str, Any], header: Optional[str] = None) -> str:
888
+ lines: List[str] = []
889
+ if header:
890
+ lines.append(header)
891
+ lines.append("")
892
+ lines.extend(
893
+ [
894
+ f"📄 **{doc.get('title') or doc.get('name', 'Sem título')}**",
895
+ f"ID: {doc.get('id', 'N/A')}",
896
+ f"Status: {doc.get('status', 'N/A')}",
897
+ f"Time: {doc.get('team_id', 'N/A')}",
898
+ ]
899
+ )
900
+ if doc.get("updated_at") or doc.get("updatedAt"):
901
+ lines.append(f"Atualizado em: {_format_date(doc.get('updated_at') or doc.get('updatedAt'))}")
902
+ return "\n".join(lines)
903
+
904
+
905
+ __all__ = ["KnowledgeTool", "KnowledgeAction"]