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.
- fenix_mcp/__init__.py +17 -0
- fenix_mcp/application/presenters.py +24 -0
- fenix_mcp/application/tool_base.py +46 -0
- fenix_mcp/application/tool_registry.py +37 -0
- fenix_mcp/application/tools/__init__.py +29 -0
- fenix_mcp/application/tools/health.py +30 -0
- fenix_mcp/application/tools/initialize.py +125 -0
- fenix_mcp/application/tools/intelligence.py +253 -0
- fenix_mcp/application/tools/knowledge.py +905 -0
- fenix_mcp/application/tools/productivity.py +220 -0
- fenix_mcp/application/tools/user_config.py +158 -0
- fenix_mcp/domain/initialization.py +180 -0
- fenix_mcp/domain/intelligence.py +133 -0
- fenix_mcp/domain/knowledge.py +437 -0
- fenix_mcp/domain/productivity.py +184 -0
- fenix_mcp/domain/user_config.py +42 -0
- fenix_mcp/infrastructure/config.py +56 -0
- fenix_mcp/infrastructure/context.py +20 -0
- fenix_mcp/infrastructure/fenix_api/client.py +623 -0
- fenix_mcp/infrastructure/http_client.py +84 -0
- fenix_mcp/infrastructure/logging.py +43 -0
- fenix_mcp/interface/mcp_server.py +78 -0
- fenix_mcp/interface/transports.py +227 -0
- fenix_mcp/main.py +90 -0
- fenix_mcp-0.1.0.dist-info/METADATA +208 -0
- fenix_mcp-0.1.0.dist-info/RECORD +29 -0
- fenix_mcp-0.1.0.dist-info/WHEEL +5 -0
- fenix_mcp-0.1.0.dist-info/entry_points.txt +2 -0
- fenix_mcp-0.1.0.dist-info/top_level.txt +1 -0
fenix_mcp/__init__.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2024 Bruno Fernandes
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
Fênix Cloud MCP Server (Python edition).
|
|
6
|
+
|
|
7
|
+
This package follows a Clean Architecture layout inside the MCP ecosystem:
|
|
8
|
+
|
|
9
|
+
- interface: transports and MCP protocol glue code
|
|
10
|
+
- application: tools, registries, presenters and use-case orchestrators
|
|
11
|
+
- domain: pure business models and services
|
|
12
|
+
- infrastructure: API clients, config, logging and shared context
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
__all__ = ["__version__"]
|
|
16
|
+
|
|
17
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
"""Helpers to format MCP responses."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from typing import Iterable, List
|
|
7
|
+
|
|
8
|
+
from fenix_mcp.application.tool_base import ToolResponse
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def text(content: str) -> ToolResponse:
|
|
12
|
+
return {"content": [{"type": "text", "text": content}]}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def bullet_list(title: str, items: Iterable[str]) -> ToolResponse:
|
|
16
|
+
body_lines: List[str] = [title, ""]
|
|
17
|
+
body_lines.extend(f"- {item}" for item in items)
|
|
18
|
+
return text("\n".join(body_lines))
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def key_value(title: str, **values: str) -> ToolResponse:
|
|
22
|
+
lines = [title, ""]
|
|
23
|
+
lines.extend(f"{key}: {value}" for key, value in values.items())
|
|
24
|
+
return text("\n".join(lines))
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
"""Base abstractions for MCP tools."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from abc import ABC, abstractmethod
|
|
7
|
+
from typing import Any, Dict, Type
|
|
8
|
+
|
|
9
|
+
from pydantic import BaseModel, ConfigDict
|
|
10
|
+
|
|
11
|
+
from fenix_mcp.infrastructure.context import AppContext
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ToolRequest(BaseModel):
|
|
15
|
+
"""Base request payload."""
|
|
16
|
+
|
|
17
|
+
model_config = ConfigDict(extra="forbid")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
ToolResponse = Dict[str, Any]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Tool(ABC):
|
|
24
|
+
"""Interface implemented by all tools."""
|
|
25
|
+
|
|
26
|
+
name: str
|
|
27
|
+
description: str
|
|
28
|
+
request_model: Type[ToolRequest] = ToolRequest
|
|
29
|
+
|
|
30
|
+
def schema(self) -> Dict[str, Any]:
|
|
31
|
+
"""Return JSON schema describing the tool arguments."""
|
|
32
|
+
return {
|
|
33
|
+
"name": self.name,
|
|
34
|
+
"description": self.description,
|
|
35
|
+
"inputSchema": self.request_model.model_json_schema(),
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async def execute(self, raw_arguments: Dict[str, Any], context: AppContext) -> ToolResponse:
|
|
39
|
+
"""Validate raw arguments and run the tool."""
|
|
40
|
+
payload = self.request_model.model_validate(raw_arguments or {})
|
|
41
|
+
return await self.run(payload, context)
|
|
42
|
+
|
|
43
|
+
@abstractmethod
|
|
44
|
+
async def run(self, payload: ToolRequest, context: AppContext) -> ToolResponse:
|
|
45
|
+
"""Execute business logic and return a MCP-formatted response."""
|
|
46
|
+
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
"""Registry that keeps the mapping between tool names and instances."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from typing import Dict, Iterable, List
|
|
7
|
+
|
|
8
|
+
from fenix_mcp.application.tool_base import Tool, ToolResponse
|
|
9
|
+
from fenix_mcp.infrastructure.context import AppContext
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ToolRegistry:
|
|
13
|
+
"""Lookup table for tool execution."""
|
|
14
|
+
|
|
15
|
+
def __init__(self, tools: Iterable[Tool]):
|
|
16
|
+
self._tools: Dict[str, Tool] = {}
|
|
17
|
+
for tool in tools:
|
|
18
|
+
if tool.name in self._tools:
|
|
19
|
+
raise ValueError(f"Duplicate tool name detected: {tool.name}")
|
|
20
|
+
self._tools[tool.name] = tool
|
|
21
|
+
|
|
22
|
+
def list_definitions(self) -> List[dict]:
|
|
23
|
+
return [tool.schema() for tool in self._tools.values()]
|
|
24
|
+
|
|
25
|
+
async def execute(self, name: str, arguments: dict, context: AppContext) -> ToolResponse:
|
|
26
|
+
try:
|
|
27
|
+
tool = self._tools[name]
|
|
28
|
+
except KeyError as exc:
|
|
29
|
+
raise KeyError(f"Unknown tool '{name}'") from exc
|
|
30
|
+
return await tool.execute(arguments, context)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def build_default_registry(context: AppContext) -> ToolRegistry:
|
|
34
|
+
from fenix_mcp.application.tools import build_tools
|
|
35
|
+
|
|
36
|
+
tools = build_tools(context)
|
|
37
|
+
return ToolRegistry(tools)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
"""Factory functions to instantiate all tools."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from typing import List
|
|
7
|
+
|
|
8
|
+
from fenix_mcp.application.tool_base import Tool
|
|
9
|
+
from fenix_mcp.infrastructure.context import AppContext
|
|
10
|
+
|
|
11
|
+
from .health import HealthTool
|
|
12
|
+
from .initialize import InitializeTool
|
|
13
|
+
from .intelligence import IntelligenceTool
|
|
14
|
+
from .productivity import ProductivityTool
|
|
15
|
+
from .knowledge import KnowledgeTool
|
|
16
|
+
from .user_config import UserConfigTool
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def build_tools(context: AppContext) -> List[Tool]:
|
|
20
|
+
"""Instantiate all available tools."""
|
|
21
|
+
|
|
22
|
+
return [
|
|
23
|
+
HealthTool(context),
|
|
24
|
+
InitializeTool(context),
|
|
25
|
+
IntelligenceTool(context),
|
|
26
|
+
ProductivityTool(context),
|
|
27
|
+
KnowledgeTool(context),
|
|
28
|
+
UserConfigTool(context),
|
|
29
|
+
]
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
"""Health check tool."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel
|
|
7
|
+
|
|
8
|
+
from fenix_mcp.application.presenters import key_value, text
|
|
9
|
+
from fenix_mcp.application.tool_base import Tool, ToolRequest
|
|
10
|
+
from fenix_mcp.infrastructure.context import AppContext
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class _HealthRequest(ToolRequest):
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class HealthTool(Tool):
|
|
18
|
+
name = "fenix_health_check"
|
|
19
|
+
description = "Check if Fênix Cloud Backend is healthy and accessible."
|
|
20
|
+
request_model = _HealthRequest
|
|
21
|
+
|
|
22
|
+
def __init__(self, context: AppContext):
|
|
23
|
+
self._context = context
|
|
24
|
+
|
|
25
|
+
async def run(self, payload: ToolRequest, context: AppContext):
|
|
26
|
+
api_health = self._context.api_client.get_health() or {}
|
|
27
|
+
return key_value(
|
|
28
|
+
"Fênix Cloud Backend Health",
|
|
29
|
+
status=str(api_health.get("status", "unknown")),
|
|
30
|
+
)
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
"""Initialization tool implementation."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from enum import Enum
|
|
7
|
+
import json
|
|
8
|
+
from typing import List, Optional
|
|
9
|
+
|
|
10
|
+
from pydantic import Field
|
|
11
|
+
|
|
12
|
+
from fenix_mcp.application.presenters import text
|
|
13
|
+
from fenix_mcp.application.tool_base import Tool, ToolRequest
|
|
14
|
+
from fenix_mcp.domain.initialization import InitializationService
|
|
15
|
+
from fenix_mcp.infrastructure.context import AppContext
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class InitializeAction(str, Enum):
|
|
19
|
+
INIT = "init"
|
|
20
|
+
SETUP = "setup"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class InitializeRequest(ToolRequest):
|
|
24
|
+
action: InitializeAction = Field(description="Operação de inicialização a executar.")
|
|
25
|
+
include_user_docs: bool = Field(
|
|
26
|
+
default=True,
|
|
27
|
+
description="Inclui documentos pessoais durante a inicialização (apenas para ação init).",
|
|
28
|
+
)
|
|
29
|
+
limit: int = Field(
|
|
30
|
+
default=50,
|
|
31
|
+
ge=1,
|
|
32
|
+
le=200,
|
|
33
|
+
description="Quantidade máxima de documentos principais/pessoais carregados.",
|
|
34
|
+
)
|
|
35
|
+
answers: Optional[List[str]] = Field(
|
|
36
|
+
default=None,
|
|
37
|
+
description="Lista com 9 respostas textuais para processar o setup personalizado.",
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class InitializeTool(Tool):
|
|
42
|
+
name = "initialize"
|
|
43
|
+
description = "Inicializa o ambiente do Fênix Cloud ou processa o setup personalizado."
|
|
44
|
+
request_model = InitializeRequest
|
|
45
|
+
|
|
46
|
+
def __init__(self, context: AppContext):
|
|
47
|
+
self._context = context
|
|
48
|
+
self._service = InitializationService(context.api_client, context.logger)
|
|
49
|
+
|
|
50
|
+
async def run(self, payload: InitializeRequest, context: AppContext):
|
|
51
|
+
if payload.action is InitializeAction.INIT:
|
|
52
|
+
return await self._handle_init(payload)
|
|
53
|
+
if payload.action is InitializeAction.SETUP:
|
|
54
|
+
return await self._handle_setup(payload)
|
|
55
|
+
return text("❌ Ação de inicialização desconhecida.")
|
|
56
|
+
|
|
57
|
+
async def _handle_init(self, payload: InitializeRequest):
|
|
58
|
+
try:
|
|
59
|
+
data = await self._service.gather_data(
|
|
60
|
+
include_user_docs=payload.include_user_docs,
|
|
61
|
+
limit=payload.limit,
|
|
62
|
+
)
|
|
63
|
+
except Exception as exc: # pragma: no cover - defensive
|
|
64
|
+
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.")
|
|
66
|
+
|
|
67
|
+
if (
|
|
68
|
+
not data.core_documents
|
|
69
|
+
and (not data.user_documents or not payload.include_user_docs)
|
|
70
|
+
and not data.profile
|
|
71
|
+
):
|
|
72
|
+
return text(
|
|
73
|
+
"⚠️ Não consegui carregar documentos nem perfil. Confirme o token e, se for o primeiro acesso, use `initialize action=setup` para responder ao questionário inicial."
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
payload_dict = {
|
|
77
|
+
"profile": data.profile,
|
|
78
|
+
"core_documents": data.core_documents,
|
|
79
|
+
"user_documents": data.user_documents if payload.include_user_docs else [],
|
|
80
|
+
"recent_memories": data.recent_memories,
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
message_lines = [
|
|
84
|
+
"📦 **Dados de inicialização completos**",
|
|
85
|
+
"```json",
|
|
86
|
+
json.dumps(payload_dict, ensure_ascii=False, indent=2),
|
|
87
|
+
"```",
|
|
88
|
+
]
|
|
89
|
+
|
|
90
|
+
if payload.include_user_docs and not data.user_documents and data.profile:
|
|
91
|
+
message_lines.extend(
|
|
92
|
+
[
|
|
93
|
+
"",
|
|
94
|
+
self._service.build_new_user_prompt(data),
|
|
95
|
+
]
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
return text("\n".join(message_lines))
|
|
99
|
+
|
|
100
|
+
async def _handle_setup(self, payload: InitializeRequest):
|
|
101
|
+
answers = payload.answers or []
|
|
102
|
+
validation_error = self._service.validate_setup_answers(answers)
|
|
103
|
+
if validation_error:
|
|
104
|
+
return text(f"❌ {validation_error}")
|
|
105
|
+
|
|
106
|
+
summary_lines = [
|
|
107
|
+
"📝 **Setup personalizado recebido!**",
|
|
108
|
+
"",
|
|
109
|
+
"Suas respostas foram registradas. Vou sugerir documentos, regras e rotinas com base nessas informações.",
|
|
110
|
+
"",
|
|
111
|
+
"Resumo das respostas:",
|
|
112
|
+
]
|
|
113
|
+
for idx, answer in enumerate(answers, start=1):
|
|
114
|
+
summary_lines.append(f"{idx}. {answer.strip()}")
|
|
115
|
+
|
|
116
|
+
summary_lines.extend(
|
|
117
|
+
[
|
|
118
|
+
"",
|
|
119
|
+
"Agora você pode pedir conteúdos específicos, por exemplo:",
|
|
120
|
+
"- `productivity action=todo_create ...`",
|
|
121
|
+
"- `knowledge action=mode_list`",
|
|
122
|
+
]
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
return text("\n".join(summary_lines))
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
"""Intelligence tool implementation (memories and smart operations)."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from enum import Enum
|
|
7
|
+
from typing import Any, Dict, Iterable, 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.intelligence import IntelligenceService
|
|
14
|
+
from fenix_mcp.infrastructure.context import AppContext
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class IntelligenceAction(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
|
+
SMART_CREATE = ("memory_smart_create", "Cria memórias inteligentes com análise de similaridade.")
|
|
25
|
+
QUERY = ("memory_query", "Lista memórias aplicando filtros e busca textual.")
|
|
26
|
+
SIMILARITY = ("memory_similarity", "Busca memórias similares a um conteúdo base.")
|
|
27
|
+
CONSOLIDATE = ("memory_consolidate", "Consolida múltiplas memórias em uma principal.")
|
|
28
|
+
PRIORITY = ("memory_priority", "Retorna memórias ordenadas por prioridade.")
|
|
29
|
+
ANALYTICS = ("memory_analytics", "Calcula métricas e analytics das memórias.")
|
|
30
|
+
UPDATE = ("memory_update", "Atualiza campos de uma memória existente.")
|
|
31
|
+
HELP = ("memory_help", "Mostra as ações suportadas e seus usos.")
|
|
32
|
+
|
|
33
|
+
@classmethod
|
|
34
|
+
def choices(cls) -> List[str]:
|
|
35
|
+
return [member.value for member in cls]
|
|
36
|
+
|
|
37
|
+
@classmethod
|
|
38
|
+
def formatted_help(cls) -> str:
|
|
39
|
+
lines = [
|
|
40
|
+
"| **Ação** | **Descrição** |",
|
|
41
|
+
"| --- | --- |",
|
|
42
|
+
]
|
|
43
|
+
for member in cls:
|
|
44
|
+
lines.append(f"| `{member.value}` | {member.description} |")
|
|
45
|
+
return "\n".join(lines)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
ACTION_FIELD_DESCRIPTION = (
|
|
49
|
+
"Ação de inteligência a executar. Use um dos valores: "
|
|
50
|
+
+ ", ".join(f"`{member.value}` ({member.description.rstrip('.')})." for member in IntelligenceAction)
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class IntelligenceRequest(ToolRequest):
|
|
55
|
+
action: IntelligenceAction = Field(description=ACTION_FIELD_DESCRIPTION)
|
|
56
|
+
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.")
|
|
58
|
+
context: Optional[str] = Field(default=None, description="Contexto adicional.")
|
|
59
|
+
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.")
|
|
61
|
+
tags: Optional[List[str]] = Field(default=None, description="Tags da memória.")
|
|
62
|
+
limit: int = Field(default=20, ge=1, le=100, description="Limite de resultados.")
|
|
63
|
+
offset: int = Field(default=0, ge=0, description="Offset para paginação.")
|
|
64
|
+
query: Optional[str] = Field(default=None, description="Termo de busca.")
|
|
65
|
+
category: Optional[str] = Field(default=None, description="Categoria para filtro.")
|
|
66
|
+
date_from: Optional[str] = Field(default=None, description="Filtro inicial (ISO).")
|
|
67
|
+
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.")
|
|
71
|
+
strategy: str = Field(default="merge", description="Estratégia de consolidação.")
|
|
72
|
+
time_range: str = Field(default="month", description="Janela de tempo para analytics.")
|
|
73
|
+
group_by: str = Field(default="category", description="Agrupamento para analytics.")
|
|
74
|
+
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.")
|
|
76
|
+
mode_id: Optional[str] = Field(default=None, description="ID do modo relacionado.")
|
|
77
|
+
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.")
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class IntelligenceTool(Tool):
|
|
83
|
+
name = "intelligence"
|
|
84
|
+
description = "Operações de inteligência do Fênix Cloud (memórias e smart operations)."
|
|
85
|
+
request_model = IntelligenceRequest
|
|
86
|
+
|
|
87
|
+
def __init__(self, context: AppContext):
|
|
88
|
+
self._context = context
|
|
89
|
+
self._service = IntelligenceService(context.api_client, context.logger)
|
|
90
|
+
|
|
91
|
+
async def run(self, payload: IntelligenceRequest, context: AppContext):
|
|
92
|
+
action = payload.action
|
|
93
|
+
if action is IntelligenceAction.HELP:
|
|
94
|
+
return await self._handle_help()
|
|
95
|
+
if action is IntelligenceAction.SMART_CREATE:
|
|
96
|
+
return await self._handle_smart_create(payload)
|
|
97
|
+
if action is IntelligenceAction.QUERY:
|
|
98
|
+
return await self._handle_query(payload)
|
|
99
|
+
if action is IntelligenceAction.SIMILARITY:
|
|
100
|
+
return await self._handle_similarity(payload)
|
|
101
|
+
if action is IntelligenceAction.CONSOLIDATE:
|
|
102
|
+
return await self._handle_consolidate(payload)
|
|
103
|
+
if action is IntelligenceAction.PRIORITY:
|
|
104
|
+
return await self._handle_priority(payload)
|
|
105
|
+
if action is IntelligenceAction.ANALYTICS:
|
|
106
|
+
return await self._handle_analytics(payload)
|
|
107
|
+
if action is IntelligenceAction.UPDATE:
|
|
108
|
+
return await self._handle_update(payload)
|
|
109
|
+
return text(
|
|
110
|
+
"❌ Ação inválida para intelligence.\n\nEscolha um dos valores:\n"
|
|
111
|
+
+ "\n".join(f"- `{value}`" for value in IntelligenceAction.choices())
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
async def _handle_smart_create(self, payload: IntelligenceRequest):
|
|
115
|
+
if not payload.title or not payload.content:
|
|
116
|
+
return text("❌ Informe título e conteúdo para criar uma memória.")
|
|
117
|
+
memory = await self._service.smart_create_memory(
|
|
118
|
+
title=payload.title,
|
|
119
|
+
content=payload.content,
|
|
120
|
+
context=payload.context,
|
|
121
|
+
source=payload.source,
|
|
122
|
+
importance=payload.importance,
|
|
123
|
+
tags=payload.tags,
|
|
124
|
+
)
|
|
125
|
+
lines = [
|
|
126
|
+
"🧠 **Memória criada com sucesso!**",
|
|
127
|
+
f"ID: {memory.get('memoryId') or memory.get('id', 'N/A')}",
|
|
128
|
+
f"Ação: {memory.get('action') or 'criado'}",
|
|
129
|
+
f"Similaridade: {format_percentage(memory.get('similarity'))}",
|
|
130
|
+
f"Tags: {', '.join(memory.get('tags', [])) or 'Automáticas'}",
|
|
131
|
+
f"Categoria: {memory.get('category') or 'Automática'}",
|
|
132
|
+
]
|
|
133
|
+
return text("\n".join(lines))
|
|
134
|
+
|
|
135
|
+
async def _handle_query(self, payload: IntelligenceRequest):
|
|
136
|
+
memories = await self._service.query_memories(
|
|
137
|
+
limit=payload.limit,
|
|
138
|
+
offset=payload.offset,
|
|
139
|
+
query=payload.query,
|
|
140
|
+
tags=payload.tags,
|
|
141
|
+
category=payload.category,
|
|
142
|
+
dateFrom=payload.date_from,
|
|
143
|
+
dateTo=payload.date_to,
|
|
144
|
+
importance=payload.importance,
|
|
145
|
+
)
|
|
146
|
+
if not memories:
|
|
147
|
+
return text("🧠 Nenhuma memória encontrada.")
|
|
148
|
+
body = "\n\n".join(_format_memory(mem) for mem in memories)
|
|
149
|
+
return text(f"🧠 **Memórias ({len(memories)}):**\n\n{body}")
|
|
150
|
+
|
|
151
|
+
async def _handle_similarity(self, payload: IntelligenceRequest):
|
|
152
|
+
if not payload.content:
|
|
153
|
+
return text("❌ Informe o conteúdo base para comparar similitude.")
|
|
154
|
+
memories = await self._service.similar_memories(
|
|
155
|
+
content=payload.content,
|
|
156
|
+
threshold=payload.threshold,
|
|
157
|
+
max_results=payload.max_results,
|
|
158
|
+
)
|
|
159
|
+
if not memories:
|
|
160
|
+
return text("🔍 Nenhuma memória similar encontrada.")
|
|
161
|
+
body = "\n\n".join(
|
|
162
|
+
f"🔍 **{mem.get('title', 'Sem título')}**\n Similaridade: {format_percentage(mem.get('finalScore'))}\n ID: {mem.get('memoryId', 'N/A')}"
|
|
163
|
+
for mem in memories
|
|
164
|
+
)
|
|
165
|
+
return text(f"🔍 **Memórias similares ({len(memories)}):**\n\n{body}")
|
|
166
|
+
|
|
167
|
+
async def _handle_consolidate(self, payload: IntelligenceRequest):
|
|
168
|
+
if not payload.memory_ids or len(payload.memory_ids) < 2:
|
|
169
|
+
return text("❌ Informe ao menos 2 IDs de memória para consolidar.")
|
|
170
|
+
result = await self._service.consolidate_memories(
|
|
171
|
+
memory_ids=payload.memory_ids,
|
|
172
|
+
strategy=payload.strategy,
|
|
173
|
+
)
|
|
174
|
+
lines = [
|
|
175
|
+
"🔄 **Consolidação concluída!**",
|
|
176
|
+
f"Memória principal: {result.get('primary_memory_id', 'N/A')}",
|
|
177
|
+
f"Consolidadas: {result.get('consolidated_count', 'N/A')}",
|
|
178
|
+
f"Ação executada: {result.get('action', 'N/A')}",
|
|
179
|
+
]
|
|
180
|
+
return text("\n".join(lines))
|
|
181
|
+
|
|
182
|
+
async def _handle_priority(self, payload: IntelligenceRequest):
|
|
183
|
+
memories = await self._service.priority_memories(limit=payload.limit)
|
|
184
|
+
if not memories:
|
|
185
|
+
return text("✅ Nenhuma memória prioritária no momento.")
|
|
186
|
+
body = "\n\n".join(_format_memory(mem) for mem in memories)
|
|
187
|
+
return text(f"🧠 **Memórias prioritárias ({len(memories)}):**\n\n{body}")
|
|
188
|
+
|
|
189
|
+
async def _handle_analytics(self, payload: IntelligenceRequest):
|
|
190
|
+
analytics = await self._service.analytics(
|
|
191
|
+
time_range=payload.time_range,
|
|
192
|
+
group_by=payload.group_by,
|
|
193
|
+
)
|
|
194
|
+
lines = [
|
|
195
|
+
"📊 **Memória - Analytics**",
|
|
196
|
+
f"Total: {analytics.get('total_memories', 0)}",
|
|
197
|
+
f"Novas: {analytics.get('new_memories', 0)}",
|
|
198
|
+
f"Mais acessada: {analytics.get('most_accessed', 'N/A')}",
|
|
199
|
+
f"Acesso médio: {analytics.get('avg_access_count', 'N/A')}",
|
|
200
|
+
]
|
|
201
|
+
by_group = analytics.get("by_group")
|
|
202
|
+
if isinstance(by_group, dict):
|
|
203
|
+
lines.append("\nPor grupo:")
|
|
204
|
+
lines.extend(f"- {key}: {value}" for key, value in by_group.items())
|
|
205
|
+
return text("\n".join(lines))
|
|
206
|
+
|
|
207
|
+
async def _handle_update(self, payload: IntelligenceRequest):
|
|
208
|
+
if not payload.id:
|
|
209
|
+
return text("❌ Informe o ID da memória para atualização.")
|
|
210
|
+
update_fields: Dict[str, Any] = {
|
|
211
|
+
"title": payload.title,
|
|
212
|
+
"content": payload.content,
|
|
213
|
+
"tags": payload.tags,
|
|
214
|
+
"documentation_item_id": payload.documentation_item_id,
|
|
215
|
+
"mode_id": payload.mode_id,
|
|
216
|
+
"rule_id": payload.rule_id,
|
|
217
|
+
"work_item_id": payload.work_item_id,
|
|
218
|
+
"sprint_id": payload.sprint_id,
|
|
219
|
+
"importance": payload.importance,
|
|
220
|
+
}
|
|
221
|
+
memory = await self._service.update_memory(payload.id, **update_fields)
|
|
222
|
+
return text(
|
|
223
|
+
"\n".join(
|
|
224
|
+
[
|
|
225
|
+
"✅ **Memória atualizada!**",
|
|
226
|
+
f"ID: {memory.get('id', payload.id)}",
|
|
227
|
+
f"Título: {memory.get('title', 'N/A')}",
|
|
228
|
+
f"Prioridade: {memory.get('priority_score', 'N/A')}",
|
|
229
|
+
]
|
|
230
|
+
)
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
async def _handle_help(self):
|
|
234
|
+
return text("📚 **Ações disponíveis para intelligence**\n\n" + IntelligenceAction.formatted_help())
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _format_memory(memory: Dict[str, Any]) -> str:
|
|
238
|
+
return "\n".join(
|
|
239
|
+
[
|
|
240
|
+
f"🧠 **{memory.get('title', 'Sem título')}**",
|
|
241
|
+
f"ID: {memory.get('id', memory.get('memoryId', 'N/A'))}",
|
|
242
|
+
f"Categoria: {memory.get('category', 'N/A')}",
|
|
243
|
+
f"Tags: {', '.join(memory.get('tags', [])) or 'Nenhuma'}",
|
|
244
|
+
f"Importância: {memory.get('importance', 'N/A')}",
|
|
245
|
+
f"Acessos: {memory.get('access_count', 'N/A')}",
|
|
246
|
+
]
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def format_percentage(value: Optional[float]) -> str:
|
|
251
|
+
if value is None:
|
|
252
|
+
return "N/A"
|
|
253
|
+
return f"{value * 100:.1f}%"
|