fenix-mcp 0.1.0__py3-none-any.whl → 0.2.1__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 CHANGED
@@ -10,8 +10,11 @@ This package follows a Clean Architecture layout inside the MCP ecosystem:
10
10
  - application: tools, registries, presenters and use-case orchestrators
11
11
  - domain: pure business models and services
12
12
  - infrastructure: API clients, config, logging and shared context
13
+
14
+ Version 0.1.0 - Initial release with basic MCP functionality.
15
+ Updated with improved error handling and better documentation.
13
16
  """
14
17
 
15
18
  __all__ = ["__version__"]
16
19
 
17
- __version__ = "0.1.0"
20
+ __version__ = "0.2.1"
@@ -35,7 +35,9 @@ class Tool(ABC):
35
35
  "inputSchema": self.request_model.model_json_schema(),
36
36
  }
37
37
 
38
- async def execute(self, raw_arguments: Dict[str, Any], context: AppContext) -> ToolResponse:
38
+ async def execute(
39
+ self, raw_arguments: Dict[str, Any], context: AppContext
40
+ ) -> ToolResponse:
39
41
  """Validate raw arguments and run the tool."""
40
42
  payload = self.request_model.model_validate(raw_arguments or {})
41
43
  return await self.run(payload, context)
@@ -43,4 +45,3 @@ class Tool(ABC):
43
45
  @abstractmethod
44
46
  async def run(self, payload: ToolRequest, context: AppContext) -> ToolResponse:
45
47
  """Execute business logic and return a MCP-formatted response."""
46
-
@@ -22,7 +22,9 @@ class ToolRegistry:
22
22
  def list_definitions(self) -> List[dict]:
23
23
  return [tool.schema() for tool in self._tools.values()]
24
24
 
25
- async def execute(self, name: str, arguments: dict, context: AppContext) -> ToolResponse:
25
+ async def execute(
26
+ self, name: str, arguments: dict, context: AppContext
27
+ ) -> ToolResponse:
26
28
  try:
27
29
  tool = self._tools[name]
28
30
  except KeyError as exc:
@@ -3,9 +3,7 @@
3
3
 
4
4
  from __future__ import annotations
5
5
 
6
- from pydantic import BaseModel
7
-
8
- from fenix_mcp.application.presenters import key_value, text
6
+ from fenix_mcp.application.presenters import key_value
9
7
  from fenix_mcp.application.tool_base import Tool, ToolRequest
10
8
  from fenix_mcp.infrastructure.context import AppContext
11
9
 
@@ -21,26 +21,35 @@ class InitializeAction(str, Enum):
21
21
 
22
22
 
23
23
  class InitializeRequest(ToolRequest):
24
- action: InitializeAction = Field(description="Operação de inicialização a executar.")
24
+ action: InitializeAction = Field(
25
+ description="Operação de inicialização a executar."
26
+ )
25
27
  include_user_docs: bool = Field(
26
28
  default=True,
27
- description="Inclui documentos pessoais durante a inicialização (apenas para ação init).",
29
+ description=(
30
+ "Inclui documentos pessoais durante a inicialização "
31
+ "(apenas para ação init)."
32
+ ),
28
33
  )
29
34
  limit: int = Field(
30
35
  default=50,
31
36
  ge=1,
32
37
  le=200,
33
- description="Quantidade máxima de documentos principais/pessoais carregados.",
38
+ description=("Quantidade máxima de documentos principais/pessoais carregados."),
34
39
  )
35
40
  answers: Optional[List[str]] = Field(
36
41
  default=None,
37
- description="Lista com 9 respostas textuais para processar o setup personalizado.",
42
+ description=(
43
+ "Lista com 9 respostas textuais para processar o setup personalizado."
44
+ ),
38
45
  )
39
46
 
40
47
 
41
48
  class InitializeTool(Tool):
42
49
  name = "initialize"
43
- description = "Inicializa o ambiente do Fênix Cloud ou processa o setup personalizado."
50
+ description = (
51
+ "Inicializa o ambiente do Fênix Cloud ou processa o setup personalizado."
52
+ )
44
53
  request_model = InitializeRequest
45
54
 
46
55
  def __init__(self, context: AppContext):
@@ -62,7 +71,10 @@ class InitializeTool(Tool):
62
71
  )
63
72
  except Exception as exc: # pragma: no cover - defensive
64
73
  self._context.logger.error("Initialize failed: %s", exc)
65
- return text("❌ Falha ao carregar dados de inicialização. Verifique se o token tem acesso à API.")
74
+ return text(
75
+ "❌ Falha ao carregar dados de inicialização. "
76
+ "Verifique se o token tem acesso à API."
77
+ )
66
78
 
67
79
  if (
68
80
  not data.core_documents
@@ -4,7 +4,7 @@
4
4
  from __future__ import annotations
5
5
 
6
6
  from enum import Enum
7
- from typing import Any, Dict, Iterable, List, Optional
7
+ from typing import Any, Dict, List, Optional
8
8
 
9
9
  from pydantic import Field
10
10
 
@@ -21,10 +21,16 @@ class IntelligenceAction(str, Enum):
21
21
  obj.description = description
22
22
  return obj
23
23
 
24
- SMART_CREATE = ("memory_smart_create", "Cria memórias inteligentes com análise de similaridade.")
24
+ SMART_CREATE = (
25
+ "memory_smart_create",
26
+ "Cria memórias inteligentes com análise de similaridade.",
27
+ )
25
28
  QUERY = ("memory_query", "Lista memórias aplicando filtros e busca textual.")
26
29
  SIMILARITY = ("memory_similarity", "Busca memórias similares a um conteúdo base.")
27
- CONSOLIDATE = ("memory_consolidate", "Consolida múltiplas memórias em uma principal.")
30
+ CONSOLIDATE = (
31
+ "memory_consolidate",
32
+ "Consolida múltiplas memórias em uma principal.",
33
+ )
28
34
  PRIORITY = ("memory_priority", "Retorna memórias ordenadas por prioridade.")
29
35
  ANALYTICS = ("memory_analytics", "Calcula métricas e analytics das memórias.")
30
36
  UPDATE = ("memory_update", "Atualiza campos de uma memória existente.")
@@ -47,17 +53,24 @@ class IntelligenceAction(str, Enum):
47
53
 
48
54
  ACTION_FIELD_DESCRIPTION = (
49
55
  "Ação de inteligência a executar. Use um dos valores: "
50
- + ", ".join(f"`{member.value}` ({member.description.rstrip('.')})." for member in IntelligenceAction)
56
+ + ", ".join(
57
+ f"`{member.value}` ({member.description.rstrip('.')})."
58
+ for member in IntelligenceAction
59
+ )
51
60
  )
52
61
 
53
62
 
54
63
  class IntelligenceRequest(ToolRequest):
55
64
  action: IntelligenceAction = Field(description=ACTION_FIELD_DESCRIPTION)
56
65
  title: Optional[str] = Field(default=None, description="Título da memória.")
57
- content: Optional[str] = Field(default=None, description="Conteúdo/texto da memória.")
66
+ content: Optional[str] = Field(
67
+ default=None, description="Conteúdo/texto da memória."
68
+ )
58
69
  context: Optional[str] = Field(default=None, description="Contexto adicional.")
59
70
  source: Optional[str] = Field(default=None, description="Fonte da memória.")
60
- importance: str = Field(default="medium", description="Nível de importância da memória.")
71
+ importance: str = Field(
72
+ default="medium", description="Nível de importância da memória."
73
+ )
61
74
  tags: Optional[List[str]] = Field(default=None, description="Tags da memória.")
62
75
  limit: int = Field(default=20, ge=1, le=100, description="Limite de resultados.")
63
76
  offset: int = Field(default=0, ge=0, description="Offset para paginação.")
@@ -65,23 +78,39 @@ class IntelligenceRequest(ToolRequest):
65
78
  category: Optional[str] = Field(default=None, description="Categoria para filtro.")
66
79
  date_from: Optional[str] = Field(default=None, description="Filtro inicial (ISO).")
67
80
  date_to: Optional[str] = Field(default=None, description="Filtro final (ISO).")
68
- threshold: float = Field(default=0.8, ge=0, le=1, description="Limite mínimo de similaridade.")
69
- max_results: int = Field(default=5, ge=1, le=20, description="Máximo de memórias similares.")
70
- memory_ids: Optional[List[str]] = Field(default=None, description="IDs para consolidação.")
81
+ threshold: float = Field(
82
+ default=0.8, ge=0, le=1, description="Limite mínimo de similaridade."
83
+ )
84
+ max_results: int = Field(
85
+ default=5, ge=1, le=20, description="Máximo de memórias similares."
86
+ )
87
+ memory_ids: Optional[List[str]] = Field(
88
+ default=None, description="IDs para consolidação."
89
+ )
71
90
  strategy: str = Field(default="merge", description="Estratégia de consolidação.")
72
- time_range: str = Field(default="month", description="Janela de tempo para analytics.")
91
+ time_range: str = Field(
92
+ default="month", description="Janela de tempo para analytics."
93
+ )
73
94
  group_by: str = Field(default="category", description="Agrupamento para analytics.")
74
95
  id: Optional[str] = Field(default=None, description="ID da memória para update.")
75
- documentation_item_id: Optional[str] = Field(default=None, description="ID de documentação relacionada.")
96
+ documentation_item_id: Optional[str] = Field(
97
+ default=None, description="ID de documentação relacionada."
98
+ )
76
99
  mode_id: Optional[str] = Field(default=None, description="ID do modo relacionado.")
77
100
  rule_id: Optional[str] = Field(default=None, description="ID da regra relacionada.")
78
- work_item_id: Optional[str] = Field(default=None, description="ID do work item relacionado.")
79
- sprint_id: Optional[str] = Field(default=None, description="ID do sprint relacionado.")
101
+ work_item_id: Optional[str] = Field(
102
+ default=None, description="ID do work item relacionado."
103
+ )
104
+ sprint_id: Optional[str] = Field(
105
+ default=None, description="ID do sprint relacionado."
106
+ )
80
107
 
81
108
 
82
109
  class IntelligenceTool(Tool):
83
110
  name = "intelligence"
84
- description = "Operações de inteligência do Fênix Cloud (memórias e smart operations)."
111
+ description = (
112
+ "Operações de inteligência do Fênix Cloud (memórias e smart operations)."
113
+ )
85
114
  request_model = IntelligenceRequest
86
115
 
87
116
  def __init__(self, context: AppContext):
@@ -231,7 +260,10 @@ class IntelligenceTool(Tool):
231
260
  )
232
261
 
233
262
  async def _handle_help(self):
234
- return text("📚 **Ações disponíveis para intelligence**\n\n" + IntelligenceAction.formatted_help())
263
+ return text(
264
+ "📚 **Ações disponíveis para intelligence**\n\n"
265
+ + IntelligenceAction.formatted_help()
266
+ )
235
267
 
236
268
 
237
269
  def _format_memory(memory: Dict[str, Any]) -> str:
@@ -10,7 +10,7 @@ from pydantic import Field
10
10
 
11
11
  from fenix_mcp.application.presenters import text
12
12
  from fenix_mcp.application.tool_base import Tool, ToolRequest
13
- from fenix_mcp.domain.knowledge import KnowledgeService, _format_date, _strip_none
13
+ from fenix_mcp.domain.knowledge import KnowledgeService, _format_date
14
14
  from fenix_mcp.infrastructure.context import AppContext
15
15
 
16
16
 
@@ -22,10 +22,19 @@ class KnowledgeAction(str, Enum):
22
22
  return obj
23
23
 
24
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.")
25
+ WORK_CREATE = (
26
+ "work_create",
27
+ "Cria um work item com título, status e vínculos opcionais.",
28
+ )
29
+ WORK_LIST = (
30
+ "work_list",
31
+ "Lista work items com filtros de status, prioridade e contexto.",
32
+ )
27
33
  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.")
34
+ WORK_UPDATE = (
35
+ "work_update",
36
+ "Atualiza campos específicos de um work item existente.",
37
+ )
29
38
  WORK_DELETE = ("work_delete", "Remove um work item definitivamente.")
30
39
  WORK_BACKLOG = ("work_backlog", "Lista itens do backlog de um time.")
31
40
  WORK_SEARCH = ("work_search", "Busca work items por texto com filtros adicionais.")
@@ -45,7 +54,10 @@ class KnowledgeAction(str, Enum):
45
54
  SPRINT_BY_TEAM = ("sprint_by_team", "Lista sprints associados a um time.")
46
55
  SPRINT_ACTIVE = ("sprint_active", "Obtém o sprint ativo de um time.")
47
56
  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.")
57
+ SPRINT_WORK_ITEMS = (
58
+ "sprint_work_items",
59
+ "Lista work items vinculados a um sprint.",
60
+ )
49
61
 
50
62
  # Modes
51
63
  MODE_CREATE = ("mode_create", "Cria um modo com conteúdo e metadados opcionais.")
@@ -54,7 +66,10 @@ class KnowledgeAction(str, Enum):
54
66
  MODE_UPDATE = ("mode_update", "Atualiza propriedades de um modo existente.")
55
67
  MODE_DELETE = ("mode_delete", "Remove um modo.")
56
68
  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.")
69
+ MODE_RULE_REMOVE = (
70
+ "mode_rule_remove",
71
+ "Remove a associação de uma regra com um modo.",
72
+ )
58
73
  MODE_RULES = ("mode_rules", "Lista regras associadas a um modo.")
59
74
 
60
75
  # Rules
@@ -99,9 +114,9 @@ class KnowledgeAction(str, Enum):
99
114
  return "\n".join(lines)
100
115
 
101
116
 
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)
117
+ ACTION_FIELD_DESCRIPTION = "Ação de conhecimento. Escolha um dos valores: " + ", ".join(
118
+ f"`{member.value}` ({member.description.rstrip('.')})."
119
+ for member in KnowledgeAction
105
120
  )
106
121
 
107
122
 
@@ -117,22 +132,38 @@ class KnowledgeRequest(ToolRequest):
117
132
  action: KnowledgeAction = Field(description=ACTION_FIELD_DESCRIPTION)
118
133
  id: Optional[str] = Field(default=None, description="ID principal do recurso.")
119
134
  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).")
135
+ offset: int = Field(
136
+ default=0, ge=0, description="Offset de paginação (quando suportado)."
137
+ )
138
+ team_id: Optional[str] = Field(
139
+ default=None, description="ID do time para filtros (boards/sprints/docs)."
140
+ )
122
141
  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.")
142
+ sprint_id: Optional[str] = Field(
143
+ default=None, description="ID do sprint associado."
144
+ )
124
145
  epic_id: Optional[str] = Field(default=None, description="ID do épico associado.")
125
146
  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.")
147
+ return_content: Optional[bool] = Field(
148
+ default=None, description="Retorna conteúdo completo."
149
+ )
150
+ return_description: Optional[bool] = Field(
151
+ default=None, description="Retorna descrição completa."
152
+ )
153
+ return_metadata: Optional[bool] = Field(
154
+ default=None, description="Retorna metadados completos."
155
+ )
129
156
 
130
157
  # Work item fields
131
158
  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.")
159
+ work_description: Optional[str] = Field(
160
+ default=None, description="Descrição do work item."
161
+ )
133
162
  work_type: Optional[str] = Field(default="task", description="Tipo do work item.")
134
163
  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.")
164
+ work_priority: Optional[str] = Field(
165
+ default=None, description="Prioridade do work item."
166
+ )
136
167
  story_points: Optional[int] = Field(default=None, description="Story points.")
137
168
  assignee_id: Optional[str] = Field(default=None, description="ID do responsável.")
138
169
  parent_id: Optional[str] = Field(default=None, description="ID do item pai.")
@@ -140,34 +171,56 @@ class KnowledgeRequest(ToolRequest):
140
171
  # Mode fields
141
172
  mode_id: Optional[str] = Field(default=None, description="ID do modo relacionado.")
142
173
  mode_name: Optional[str] = Field(default=None, description="Nome do modo.")
143
- mode_description: Optional[str] = Field(default=None, description="Descrição do modo.")
174
+ mode_description: Optional[str] = Field(
175
+ default=None, description="Descrição do modo."
176
+ )
144
177
  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.")
178
+ mode_is_default: Optional[bool] = Field(
179
+ default=None, description="Indica se o modo é padrão."
180
+ )
146
181
 
147
182
  # Rule fields
148
183
  rule_id: Optional[str] = Field(default=None, description="ID da regra relacionada.")
149
184
  rule_name: Optional[str] = Field(default=None, description="Nome da regra.")
150
- rule_description: Optional[str] = Field(default=None, description="Descrição da regra.")
185
+ rule_description: Optional[str] = Field(
186
+ default=None, description="Descrição da regra."
187
+ )
151
188
  rule_content: Optional[str] = Field(default=None, description="Conteúdo da regra.")
152
189
  rule_is_default: Optional[bool] = Field(default=None, description="Regra padrão.")
153
190
 
154
191
  # Documentation fields
155
- doc_title: Optional[str] = Field(default=None, description="Título da documentação.")
192
+ doc_title: Optional[str] = Field(
193
+ default=None, description="Título da documentação."
194
+ )
156
195
  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.")
196
+ doc_content: Optional[str] = Field(
197
+ default=None, description="Conteúdo da documentação."
198
+ )
199
+ doc_status: Optional[str] = Field(
200
+ default=None, description="Status da documentação."
201
+ )
159
202
  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.")
203
+ doc_language: Optional[str] = Field(
204
+ default=None, description="Idioma da documentação."
205
+ )
161
206
  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.")
207
+ doc_team_id: Optional[str] = Field(
208
+ default=None, description="Time responsável pela documentação."
209
+ )
163
210
  doc_owner_id: Optional[str] = Field(default=None, description="ID do dono.")
164
211
  doc_reviewer_id: Optional[str] = Field(default=None, description="ID do revisor.")
165
212
  doc_version: Optional[str] = Field(default=None, description="Versão.")
166
213
  doc_category: Optional[str] = Field(default=None, description="Categoria.")
167
214
  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.")
215
+ doc_position: Optional[int] = Field(
216
+ default=None, description="Posição desejada ao mover documentos."
217
+ )
218
+ doc_emoji: Optional[str] = Field(
219
+ default=None, description="Emoji exibido junto ao documento."
220
+ )
221
+ doc_emote: Optional[str] = Field(
222
+ default=None, description="Alias para emoji, mantido por compatibilidade."
223
+ )
171
224
 
172
225
 
173
226
  class KnowledgeTool(Tool):
@@ -323,7 +376,11 @@ class KnowledgeTool(Tool):
323
376
 
324
377
  return text(
325
378
  "❌ 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_"))
379
+ + "\n".join(
380
+ f"- `{value}`"
381
+ for value in KnowledgeAction.choices()
382
+ if value.startswith("work_")
383
+ )
327
384
  )
328
385
 
329
386
  # ------------------------------------------------------------------
@@ -332,7 +389,9 @@ class KnowledgeTool(Tool):
332
389
  async def _run_board(self, payload: KnowledgeRequest):
333
390
  action = payload.action
334
391
  if action is KnowledgeAction.BOARD_LIST:
335
- boards = await self._service.board_list(limit=payload.limit, offset=payload.offset)
392
+ boards = await self._service.board_list(
393
+ limit=payload.limit, offset=payload.offset
394
+ )
336
395
  if not boards:
337
396
  return text("🗂️ Nenhum board encontrado.")
338
397
  body = "\n\n".join(_format_board(board) for board in boards)
@@ -366,12 +425,19 @@ class KnowledgeTool(Tool):
366
425
  columns = await self._service.board_columns(payload.board_id)
367
426
  if not columns:
368
427
  return text("📊 Board sem colunas cadastradas.")
369
- body = "\n".join(f"- {col.get('name', 'Sem nome')} (ID: {col.get('id')})" for col in columns)
428
+ body = "\n".join(
429
+ f"- {col.get('name', 'Sem nome')} (ID: {col.get('id')})"
430
+ for col in columns
431
+ )
370
432
  return text(f"📊 **Colunas do board:**\n{body}")
371
433
 
372
434
  return text(
373
435
  "❌ 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_"))
436
+ + "\n".join(
437
+ f"- `{value}`"
438
+ for value in KnowledgeAction.choices()
439
+ if value.startswith("board_")
440
+ )
375
441
  )
376
442
 
377
443
  # ------------------------------------------------------------------
@@ -380,7 +446,9 @@ class KnowledgeTool(Tool):
380
446
  async def _run_sprint(self, payload: KnowledgeRequest):
381
447
  action = payload.action
382
448
  if action is KnowledgeAction.SPRINT_LIST:
383
- sprints = await self._service.sprint_list(limit=payload.limit, offset=payload.offset)
449
+ sprints = await self._service.sprint_list(
450
+ limit=payload.limit, offset=payload.offset
451
+ )
384
452
  if not sprints:
385
453
  return text("🏃 Nenhum sprint encontrado.")
386
454
  body = "\n\n".join(_format_sprint(sprint) for sprint in sprints)
@@ -420,7 +488,11 @@ class KnowledgeTool(Tool):
420
488
 
421
489
  return text(
422
490
  "❌ 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_"))
491
+ + "\n".join(
492
+ f"- `{value}`"
493
+ for value in KnowledgeAction.choices()
494
+ if value.startswith("sprint_")
495
+ )
424
496
  )
425
497
 
426
498
  # ------------------------------------------------------------------
@@ -514,13 +586,18 @@ class KnowledgeTool(Tool):
514
586
  if not rules:
515
587
  return text("🔗 Nenhuma associação encontrada.")
516
588
  body = "\n".join(
517
- f"- {item.get('name', 'Sem nome')} (ID: {item.get('id')})" for item in rules
589
+ f"- {item.get('name', 'Sem nome')} (ID: {item.get('id')})"
590
+ for item in rules
518
591
  )
519
592
  return text(f"🔗 **Associações para {context_label}:**\n{body}")
520
593
 
521
594
  return text(
522
595
  "❌ 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_"))
596
+ + "\n".join(
597
+ f"- `{value}`"
598
+ for value in KnowledgeAction.choices()
599
+ if value.startswith("mode_")
600
+ )
524
601
  )
525
602
 
526
603
  # ------------------------------------------------------------------
@@ -585,7 +662,11 @@ class KnowledgeTool(Tool):
585
662
 
586
663
  return text(
587
664
  "❌ 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_"))
665
+ + "\n".join(
666
+ f"- `{value}`"
667
+ for value in KnowledgeAction.choices()
668
+ if value.startswith("rule_")
669
+ )
589
670
  )
590
671
 
591
672
  # ------------------------------------------------------------------
@@ -685,17 +766,24 @@ class KnowledgeTool(Tool):
685
766
  limit=payload.limit,
686
767
  )
687
768
  if not docs:
688
- return text("🔍 Nenhum documento encontrado para os filtros informados.")
769
+ return text(
770
+ "🔍 Nenhum documento encontrado para os filtros informados."
771
+ )
689
772
  body = "\n\n".join(_format_doc(doc) for doc in docs)
690
773
  return text(f"🔍 **Resultados ({len(docs)}):**\n\n{body}")
691
774
 
692
775
  if action is KnowledgeAction.DOC_ROOTS:
693
776
  if not (payload.doc_team_id or payload.team_id):
694
777
  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)
778
+ docs = await self._service.doc_roots(
779
+ team_id=payload.doc_team_id or payload.team_id
780
+ )
696
781
  if not docs:
697
782
  return text("📚 Nenhuma raiz encontrada.")
698
- body = "\n".join(f"- {doc.get('title', 'Sem título')} (ID: {doc.get('id')})" for doc in docs)
783
+ body = "\n".join(
784
+ f"- {doc.get('title', 'Sem título')} (ID: {doc.get('id')})"
785
+ for doc in docs
786
+ )
699
787
  return text(f"📚 **Raízes de documentação:**\n{body}")
700
788
 
701
789
  if action is KnowledgeAction.DOC_RECENT:
@@ -713,7 +801,9 @@ class KnowledgeTool(Tool):
713
801
  if action is KnowledgeAction.DOC_ANALYTICS:
714
802
  if not (payload.doc_team_id or payload.team_id):
715
803
  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)
804
+ analytics = await self._service.doc_analytics(
805
+ team_id=payload.doc_team_id or payload.team_id
806
+ )
717
807
  lines = ["📊 **Analytics de Documentação**"]
718
808
  for key, value in analytics.items():
719
809
  lines.append(f"- {key}: {value}")
@@ -725,7 +815,10 @@ class KnowledgeTool(Tool):
725
815
  docs = await self._service.doc_children(payload.id)
726
816
  if not docs:
727
817
  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)
818
+ body = "\n".join(
819
+ f"- {doc.get('title', 'Sem título')} (ID: {doc.get('id')})"
820
+ for doc in docs
821
+ )
729
822
  return text(f"📄 **Filhos:**\n{body}")
730
823
 
731
824
  if action is KnowledgeAction.DOC_TREE:
@@ -742,7 +835,9 @@ class KnowledgeTool(Tool):
742
835
  if not payload.id:
743
836
  return text("❌ Informe o ID da documentação.")
744
837
  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.")
838
+ return text(
839
+ "❌ Informe doc_parent_id, doc_position ou ambos para mover."
840
+ )
746
841
  move_payload = {
747
842
  "new_parent_id": payload.doc_parent_id,
748
843
  "new_position": payload.doc_position,
@@ -760,7 +855,9 @@ class KnowledgeTool(Tool):
760
855
  if not payload.id:
761
856
  return text("❌ Informe o ID da documentação.")
762
857
  if not payload.doc_version:
763
- return text("❌ Informe doc_version com o número/identificador da versão.")
858
+ return text(
859
+ "❌ Informe doc_version com o número/identificador da versão."
860
+ )
764
861
  version_payload = {
765
862
  "title": payload.doc_title or f"Version {payload.doc_version}",
766
863
  "version": payload.doc_version,
@@ -785,11 +882,18 @@ class KnowledgeTool(Tool):
785
882
 
786
883
  return text(
787
884
  "❌ 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_"))
885
+ + "\n".join(
886
+ f"- `{value}`"
887
+ for value in KnowledgeAction.choices()
888
+ if value.startswith("doc_")
889
+ )
789
890
  )
790
891
 
791
892
  async def _handle_help(self):
792
- return text("📚 **Ações disponíveis para knowledge**\n\n" + KnowledgeAction.formatted_help())
893
+ return text(
894
+ "📚 **Ações disponíveis para knowledge**\n\n"
895
+ + KnowledgeAction.formatted_help()
896
+ )
793
897
 
794
898
 
795
899
  def _format_work(item: Dict[str, Any], *, header: Optional[str] = None) -> str:
@@ -810,7 +914,9 @@ def _format_work(item: Dict[str, Any], *, header: Optional[str] = None) -> str:
810
914
  ]
811
915
  )
812
916
  if item.get("due_date") or item.get("dueDate"):
813
- lines.append(f"Vencimento: {_format_date(item.get('due_date') or item.get('dueDate'))}")
917
+ lines.append(
918
+ f"Vencimento: {_format_date(item.get('due_date') or item.get('dueDate'))}"
919
+ )
814
920
  return "\n".join(lines)
815
921
 
816
922
 
@@ -844,9 +950,13 @@ def _format_sprint(sprint: Dict[str, Any], header: Optional[str] = None) -> str:
844
950
  ]
845
951
  )
846
952
  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'))}")
953
+ lines.append(
954
+ f"Início: {_format_date(sprint.get('start_date') or sprint.get('startDate'))}"
955
+ )
848
956
  if sprint.get("end_date") or sprint.get("endDate"):
849
- lines.append(f"Fim: {_format_date(sprint.get('end_date') or sprint.get('endDate'))}")
957
+ lines.append(
958
+ f"Fim: {_format_date(sprint.get('end_date') or sprint.get('endDate'))}"
959
+ )
850
960
  return "\n".join(lines)
851
961
 
852
962
 
@@ -898,7 +1008,9 @@ def _format_doc(doc: Dict[str, Any], header: Optional[str] = None) -> str:
898
1008
  ]
899
1009
  )
900
1010
  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'))}")
1011
+ lines.append(
1012
+ f"Atualizado em: {_format_date(doc.get('updated_at') or doc.get('updatedAt'))}"
1013
+ )
902
1014
  return "\n".join(lines)
903
1015
 
904
1016