fenix-mcp 1.14.0__py3-none-any.whl → 2.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fenix_mcp/__init__.py +1 -1
- fenix_mcp/application/tools/initialize.py +18 -56
- fenix_mcp/application/tools/intelligence.py +130 -304
- fenix_mcp/application/tools/knowledge.py +342 -10
- fenix_mcp/domain/initialization.py +11 -112
- fenix_mcp/domain/intelligence.py +57 -247
- fenix_mcp/domain/knowledge.py +22 -0
- fenix_mcp/infrastructure/fenix_api/client.py +108 -23
- fenix_mcp/interface/mcp_server.py +12 -0
- fenix_mcp-2.0.0.dist-info/METADATA +341 -0
- {fenix_mcp-1.14.0.dist-info → fenix_mcp-2.0.0.dist-info}/RECORD +14 -14
- {fenix_mcp-1.14.0.dist-info → fenix_mcp-2.0.0.dist-info}/WHEEL +1 -1
- fenix_mcp-1.14.0.dist-info/METADATA +0 -258
- {fenix_mcp-1.14.0.dist-info → fenix_mcp-2.0.0.dist-info}/entry_points.txt +0 -0
- {fenix_mcp-1.14.0.dist-info → fenix_mcp-2.0.0.dist-info}/top_level.txt +0 -0
|
@@ -81,7 +81,7 @@ class KnowledgeAction(str, Enum):
|
|
|
81
81
|
)
|
|
82
82
|
WORK_MINE = (
|
|
83
83
|
"work_mine",
|
|
84
|
-
"Lists work items assigned to
|
|
84
|
+
"START HERE for 'my tasks' or 'what am I working on'. Lists work items assigned to current user. Excludes done/cancelled. Supports limit and offset.",
|
|
85
85
|
)
|
|
86
86
|
WORK_BULK_CREATE = (
|
|
87
87
|
"work_bulk_create",
|
|
@@ -150,7 +150,7 @@ class KnowledgeAction(str, Enum):
|
|
|
150
150
|
)
|
|
151
151
|
DOC_GET = (
|
|
152
152
|
"doc_get",
|
|
153
|
-
"Reads document content by ID. Get the ID from doc_full_tree
|
|
153
|
+
"Reads document content by ID. PREREQUISITE: Get the ID from doc_full_tree first. Do NOT guess IDs.",
|
|
154
154
|
)
|
|
155
155
|
DOC_UPDATE = ("doc_update", "Updates a documentation item.")
|
|
156
156
|
DOC_DELETE = ("doc_delete", "Removes a documentation item.")
|
|
@@ -167,13 +167,27 @@ class KnowledgeAction(str, Enum):
|
|
|
167
167
|
DOC_TREE = ("doc_tree", "Retrieves tree starting from a specific document.")
|
|
168
168
|
DOC_FULL_TREE = (
|
|
169
169
|
"doc_full_tree",
|
|
170
|
-
"
|
|
170
|
+
"START HERE when looking for documentation. Returns complete folder structure with IDs. You NEED the ID from this to use doc_get. Workflow: 1) doc_full_tree to see all docs with IDs, 2) doc_get(id) to read content.",
|
|
171
171
|
)
|
|
172
172
|
DOC_MOVE = ("doc_move", "Moves a document to another parent.")
|
|
173
173
|
DOC_PUBLISH = ("doc_publish", "Changes publication status of a document.")
|
|
174
174
|
DOC_VERSION = ("doc_version", "Generates or retrieves a document version.")
|
|
175
175
|
DOC_DUPLICATE = ("doc_duplicate", "Duplicates an existing document.")
|
|
176
176
|
|
|
177
|
+
# API Catalog
|
|
178
|
+
API_CATALOG_SEARCH = (
|
|
179
|
+
"api_catalog_search",
|
|
180
|
+
"Semantic search to find APIs by natural language query (e.g., 'API de usuários').",
|
|
181
|
+
)
|
|
182
|
+
API_CATALOG_LIST = (
|
|
183
|
+
"api_catalog_list",
|
|
184
|
+
"Lists API specifications with optional filters (status, tags).",
|
|
185
|
+
)
|
|
186
|
+
API_CATALOG_GET = (
|
|
187
|
+
"api_catalog_get",
|
|
188
|
+
"Gets full details of an API including all endpoints and environments.",
|
|
189
|
+
)
|
|
190
|
+
|
|
177
191
|
HELP = ("knowledge_help", "Shows available actions and their uses.")
|
|
178
192
|
|
|
179
193
|
@classmethod
|
|
@@ -191,10 +205,73 @@ class KnowledgeAction(str, Enum):
|
|
|
191
205
|
return "\n".join(lines)
|
|
192
206
|
|
|
193
207
|
|
|
194
|
-
ACTION_FIELD_DESCRIPTION = "Knowledge action
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
)
|
|
208
|
+
ACTION_FIELD_DESCRIPTION = """Knowledge action grouped by category:
|
|
209
|
+
|
|
210
|
+
**WORK ITEMS** (tasks, bugs, features):
|
|
211
|
+
- `work_mine`: YOUR assigned tasks (START HERE for "my tasks")
|
|
212
|
+
- `work_list`: List with filters (status, priority, type)
|
|
213
|
+
- `work_get`: Get details by ID or key (use work_key for "FENIX-123")
|
|
214
|
+
- `work_create`: Create new (requires: work_title, work_category)
|
|
215
|
+
- `work_update`: Update fields (title, description, priority, story_points, tags, due_date)
|
|
216
|
+
- `work_search`: Search by text
|
|
217
|
+
- `work_bulk_create`: Create multiple with hierarchy
|
|
218
|
+
- `work_backlog`: Team backlog items
|
|
219
|
+
- `work_children`: Child items of a parent
|
|
220
|
+
- `work_by_sprint`: Items in a sprint
|
|
221
|
+
- `work_by_board`: Items in a board
|
|
222
|
+
- `work_by_epic`: Items in an epic
|
|
223
|
+
- `work_status_update`: Update only status
|
|
224
|
+
- `work_assign_sprint`: Assign items to sprint
|
|
225
|
+
- `work_assign_to_me`: Assign item to yourself
|
|
226
|
+
- `work_analytics`: Work item metrics
|
|
227
|
+
|
|
228
|
+
**DOCUMENTATION**:
|
|
229
|
+
- `doc_full_tree`: Complete tree (START HERE to find docs and get IDs)
|
|
230
|
+
- `doc_get`: Read content (needs ID from doc_full_tree)
|
|
231
|
+
- `doc_children`: Folder contents
|
|
232
|
+
- `doc_create`: Create new doc (requires: doc_title, doc_emoji for non-folders)
|
|
233
|
+
- `doc_update`: Update doc
|
|
234
|
+
- `doc_delete`: Remove doc
|
|
235
|
+
- `doc_roots`: Root folders
|
|
236
|
+
- `doc_recent`: Recently accessed
|
|
237
|
+
- `doc_tree`: Tree from specific doc
|
|
238
|
+
- `doc_move`: Move to another parent
|
|
239
|
+
- `doc_publish`: Change publication status
|
|
240
|
+
- `doc_version`: Create/get version
|
|
241
|
+
- `doc_duplicate`: Duplicate doc
|
|
242
|
+
- `doc_analytics`: Doc metrics
|
|
243
|
+
|
|
244
|
+
**SPRINTS**:
|
|
245
|
+
- `sprint_active`: Current sprint (START HERE)
|
|
246
|
+
- `sprint_list`: All sprints
|
|
247
|
+
- `sprint_get`: Sprint details
|
|
248
|
+
- `sprint_by_team`: Team sprints
|
|
249
|
+
- `sprint_work_items`: Items in sprint
|
|
250
|
+
|
|
251
|
+
**BOARDS**:
|
|
252
|
+
- `board_list`: All boards
|
|
253
|
+
- `board_get`: Board details
|
|
254
|
+
- `board_by_team`: Team boards
|
|
255
|
+
- `board_favorites`: Favorite boards
|
|
256
|
+
- `board_columns`: Board columns
|
|
257
|
+
|
|
258
|
+
**RULES** (coding standards):
|
|
259
|
+
- `rule_list`: Team rules
|
|
260
|
+
- `rule_get`: Rule content
|
|
261
|
+
- `rule_create`: Create rule (requires: rule_scope, rule_name, rule_content)
|
|
262
|
+
- `rule_update`: Update rule
|
|
263
|
+
- `rule_delete`: Remove rule
|
|
264
|
+
- `rule_export`: Export for IDE (cursor, claude, copilot, windsurf)
|
|
265
|
+
- `rule_marketplace`: Public rules
|
|
266
|
+
- `rule_fork`: Fork a rule
|
|
267
|
+
|
|
268
|
+
**API CATALOG**:
|
|
269
|
+
- `api_catalog_search`: Semantic search for APIs (natural language)
|
|
270
|
+
- `api_catalog_list`: List all APIs
|
|
271
|
+
- `api_catalog_get`: API details with endpoints
|
|
272
|
+
|
|
273
|
+
`knowledge_help`: Show all actions with descriptions
|
|
274
|
+
"""
|
|
198
275
|
|
|
199
276
|
|
|
200
277
|
_ALLOWED_DOC_TYPES = {
|
|
@@ -376,12 +453,44 @@ class KnowledgeRequest(ToolRequest):
|
|
|
376
453
|
default=None, description="Whether the rule is active."
|
|
377
454
|
)
|
|
378
455
|
|
|
456
|
+
# API Catalog fields
|
|
457
|
+
api_spec_id: Optional[UUIDStr] = Field(
|
|
458
|
+
default=None,
|
|
459
|
+
description="API specification ID (UUID). Can also use 'id' field instead.",
|
|
460
|
+
)
|
|
461
|
+
|
|
379
462
|
|
|
380
463
|
class KnowledgeTool(Tool):
|
|
381
464
|
name = "knowledge"
|
|
382
|
-
description =
|
|
383
|
-
|
|
384
|
-
|
|
465
|
+
description = """Fenix knowledge base - use this tool when user asks about:
|
|
466
|
+
|
|
467
|
+
WORK ITEMS (tasks, bugs, features):
|
|
468
|
+
- "what are my tasks?" -> action: work_mine
|
|
469
|
+
- "show task FENIX-123" -> action: work_get, work_key: "FENIX-123"
|
|
470
|
+
- "what's in the backlog?" -> action: work_backlog
|
|
471
|
+
- "create a task" -> action: work_create (requires: work_title, work_category)
|
|
472
|
+
|
|
473
|
+
DOCUMENTATION:
|
|
474
|
+
- "find docs about X" -> action: doc_full_tree (START HERE to get IDs)
|
|
475
|
+
- "read the architecture doc" -> action: doc_get (needs ID from doc_full_tree)
|
|
476
|
+
|
|
477
|
+
SPRINTS:
|
|
478
|
+
- "current sprint?" -> action: sprint_active
|
|
479
|
+
- "sprint items?" -> action: sprint_work_items
|
|
480
|
+
|
|
481
|
+
RULES (coding standards):
|
|
482
|
+
- "team coding standards?" -> action: rule_list
|
|
483
|
+
- "export rules for cursor" -> action: rule_export
|
|
484
|
+
|
|
485
|
+
API CATALOG:
|
|
486
|
+
- "find user API" -> action: api_catalog_search
|
|
487
|
+
- "list all APIs" -> action: api_catalog_list
|
|
488
|
+
|
|
489
|
+
IMPORTANT WORKFLOWS:
|
|
490
|
+
1. For MY tasks: Always use work_mine first
|
|
491
|
+
2. For docs: Always use doc_full_tree first to get IDs, then doc_get
|
|
492
|
+
3. For work item by key: Use work_key parameter (e.g., "FENIX-123")
|
|
493
|
+
"""
|
|
385
494
|
request_model = KnowledgeRequest
|
|
386
495
|
|
|
387
496
|
def __init__(self, context: AppContext):
|
|
@@ -402,6 +511,8 @@ class KnowledgeTool(Tool):
|
|
|
402
511
|
return await self._run_rule(payload)
|
|
403
512
|
if action.value.startswith("doc_"):
|
|
404
513
|
return await self._run_doc(payload)
|
|
514
|
+
if action.value.startswith("api_catalog_"):
|
|
515
|
+
return await self._run_api_catalog(payload)
|
|
405
516
|
return text(
|
|
406
517
|
"❌ Invalid action for knowledge.\n\nChoose one of the values:\n"
|
|
407
518
|
+ "\n".join(f"- `{value}`" for value in KnowledgeAction.choices())
|
|
@@ -1154,6 +1265,63 @@ class KnowledgeTool(Tool):
|
|
|
1154
1265
|
)
|
|
1155
1266
|
)
|
|
1156
1267
|
|
|
1268
|
+
# ------------------------------------------------------------------
|
|
1269
|
+
# API Catalog
|
|
1270
|
+
# ------------------------------------------------------------------
|
|
1271
|
+
async def _run_api_catalog(self, payload: KnowledgeRequest):
|
|
1272
|
+
action = payload.action
|
|
1273
|
+
|
|
1274
|
+
if action is KnowledgeAction.API_CATALOG_SEARCH:
|
|
1275
|
+
query = sanitize_null(payload.query)
|
|
1276
|
+
if not query:
|
|
1277
|
+
return text("❌ Provide query to search APIs.")
|
|
1278
|
+
|
|
1279
|
+
result = await self._service.api_catalog_search(
|
|
1280
|
+
query=query,
|
|
1281
|
+
limit=payload.limit,
|
|
1282
|
+
)
|
|
1283
|
+
|
|
1284
|
+
data = result.get("data", [])
|
|
1285
|
+
if not data:
|
|
1286
|
+
return text(
|
|
1287
|
+
f"🔍 No APIs found for: **{query}**\n\n"
|
|
1288
|
+
"Try a different search term or use `api_catalog_list` to see all APIs."
|
|
1289
|
+
)
|
|
1290
|
+
|
|
1291
|
+
body = "\n\n".join(_format_api_search_result(api) for api in data)
|
|
1292
|
+
total = len(data)
|
|
1293
|
+
|
|
1294
|
+
return text(f"🔍 **APIs found for '{query}'** ({total} results)\n\n{body}")
|
|
1295
|
+
|
|
1296
|
+
if action is KnowledgeAction.API_CATALOG_LIST:
|
|
1297
|
+
apis = await self._service.api_catalog_list(
|
|
1298
|
+
limit=payload.limit,
|
|
1299
|
+
offset=payload.offset,
|
|
1300
|
+
)
|
|
1301
|
+
|
|
1302
|
+
if not apis:
|
|
1303
|
+
return text("📚 No APIs found in the catalog.")
|
|
1304
|
+
|
|
1305
|
+
body = "\n\n".join(_format_api_item(api) for api in apis)
|
|
1306
|
+
return text(f"📚 **API Catalog ({len(apis)} APIs)**\n\n{body}")
|
|
1307
|
+
|
|
1308
|
+
if action is KnowledgeAction.API_CATALOG_GET:
|
|
1309
|
+
spec_id = payload.api_spec_id or payload.id
|
|
1310
|
+
if not spec_id:
|
|
1311
|
+
return text("❌ Provide api_spec_id or id to get the API details.")
|
|
1312
|
+
|
|
1313
|
+
api = await self._service.api_catalog_get(spec_id)
|
|
1314
|
+
return text(_format_api_detail(api))
|
|
1315
|
+
|
|
1316
|
+
return text(
|
|
1317
|
+
"❌ Unsupported API Catalog action.\n\nChoose one of the values:\n"
|
|
1318
|
+
+ "\n".join(
|
|
1319
|
+
f"- `{value}`"
|
|
1320
|
+
for value in KnowledgeAction.choices()
|
|
1321
|
+
if value.startswith("api_catalog_")
|
|
1322
|
+
)
|
|
1323
|
+
)
|
|
1324
|
+
|
|
1157
1325
|
async def _handle_help(self):
|
|
1158
1326
|
workflow_guide = """
|
|
1159
1327
|
## 📖 How to find and read a document
|
|
@@ -1423,4 +1591,168 @@ def _format_marketplace_rule(rule: Dict[str, Any]) -> str:
|
|
|
1423
1591
|
return "\n".join(lines)
|
|
1424
1592
|
|
|
1425
1593
|
|
|
1594
|
+
def _format_api_search_result(api: Dict[str, Any]) -> str:
|
|
1595
|
+
"""Format an API search result with similarity and matched endpoint."""
|
|
1596
|
+
lines: List[str] = []
|
|
1597
|
+
|
|
1598
|
+
title = api.get("title", "Untitled API")
|
|
1599
|
+
slug = api.get("slug", "")
|
|
1600
|
+
similarity = api.get("similarity")
|
|
1601
|
+
|
|
1602
|
+
# Title with similarity
|
|
1603
|
+
if similarity is not None:
|
|
1604
|
+
pct = float(similarity) * 100
|
|
1605
|
+
lines.append(f"🔗 **{title}** ({pct:.0f}% match)")
|
|
1606
|
+
else:
|
|
1607
|
+
lines.append(f"🔗 **{title}**")
|
|
1608
|
+
|
|
1609
|
+
lines.append(f"Slug: {slug}")
|
|
1610
|
+
|
|
1611
|
+
# Status
|
|
1612
|
+
status = api.get("lifecycle_status", "unknown")
|
|
1613
|
+
lines.append(f"Status: {status}")
|
|
1614
|
+
|
|
1615
|
+
# Description (truncated)
|
|
1616
|
+
desc = api.get("description")
|
|
1617
|
+
if desc:
|
|
1618
|
+
truncated = desc[:150] + "..." if len(desc) > 150 else desc
|
|
1619
|
+
lines.append(f"Description: {truncated}")
|
|
1620
|
+
|
|
1621
|
+
# Team info
|
|
1622
|
+
team = api.get("team")
|
|
1623
|
+
if isinstance(team, dict):
|
|
1624
|
+
lines.append(f"Team: {team.get('name', 'Unknown')}")
|
|
1625
|
+
|
|
1626
|
+
# Matched endpoint (for semantic search)
|
|
1627
|
+
matched = api.get("matchedEndpoint")
|
|
1628
|
+
if matched:
|
|
1629
|
+
method = matched.get("method", "").upper()
|
|
1630
|
+
path = matched.get("path", "")
|
|
1631
|
+
summary = matched.get("summary", "")
|
|
1632
|
+
lines.append(f"Best match: **{method} {path}**")
|
|
1633
|
+
if summary:
|
|
1634
|
+
lines.append(f" └─ {summary}")
|
|
1635
|
+
|
|
1636
|
+
# Current version
|
|
1637
|
+
version = api.get("current_version")
|
|
1638
|
+
if isinstance(version, dict):
|
|
1639
|
+
lines.append(f"Version: {version.get('version', 'N/A')}")
|
|
1640
|
+
|
|
1641
|
+
return "\n".join(lines)
|
|
1642
|
+
|
|
1643
|
+
|
|
1644
|
+
def _format_api_item(api: Dict[str, Any]) -> str:
|
|
1645
|
+
"""Format an API item for listing."""
|
|
1646
|
+
lines: List[str] = []
|
|
1647
|
+
|
|
1648
|
+
title = api.get("title", "Untitled API")
|
|
1649
|
+
slug = api.get("slug", "")
|
|
1650
|
+
status = api.get("lifecycle_status", "unknown")
|
|
1651
|
+
|
|
1652
|
+
lines.append(f"📦 **{title}**")
|
|
1653
|
+
lines.append(f"ID: {api.get('id', 'N/A')}")
|
|
1654
|
+
lines.append(f"Slug: {slug}")
|
|
1655
|
+
lines.append(f"Status: {status}")
|
|
1656
|
+
|
|
1657
|
+
# Tags
|
|
1658
|
+
tags = api.get("tags", [])
|
|
1659
|
+
if tags:
|
|
1660
|
+
lines.append(f"Tags: {', '.join(tags)}")
|
|
1661
|
+
|
|
1662
|
+
# Team
|
|
1663
|
+
team = api.get("team")
|
|
1664
|
+
if isinstance(team, dict):
|
|
1665
|
+
lines.append(f"Team: {team.get('name', 'Unknown')}")
|
|
1666
|
+
|
|
1667
|
+
# Version
|
|
1668
|
+
version = api.get("current_version")
|
|
1669
|
+
if isinstance(version, dict):
|
|
1670
|
+
lines.append(f"Version: {version.get('version', 'N/A')}")
|
|
1671
|
+
|
|
1672
|
+
return "\n".join(lines)
|
|
1673
|
+
|
|
1674
|
+
|
|
1675
|
+
def _format_api_detail(api: Dict[str, Any]) -> str:
|
|
1676
|
+
"""Format detailed API specification view."""
|
|
1677
|
+
lines: List[str] = []
|
|
1678
|
+
|
|
1679
|
+
title = api.get("title", "Untitled API")
|
|
1680
|
+
slug = api.get("slug", "")
|
|
1681
|
+
|
|
1682
|
+
lines.append(f"📚 **{title}**")
|
|
1683
|
+
lines.append("")
|
|
1684
|
+
lines.append(f"ID: {api.get('id', 'N/A')}")
|
|
1685
|
+
lines.append(f"Slug: {slug}")
|
|
1686
|
+
lines.append(f"Status: {api.get('lifecycle_status', 'unknown')}")
|
|
1687
|
+
|
|
1688
|
+
# Description
|
|
1689
|
+
desc = api.get("description")
|
|
1690
|
+
if desc:
|
|
1691
|
+
lines.append("")
|
|
1692
|
+
lines.append("**Description:**")
|
|
1693
|
+
lines.append(desc)
|
|
1694
|
+
|
|
1695
|
+
# Tags
|
|
1696
|
+
tags = api.get("tags", [])
|
|
1697
|
+
if tags:
|
|
1698
|
+
lines.append("")
|
|
1699
|
+
lines.append(f"Tags: {', '.join(tags)}")
|
|
1700
|
+
|
|
1701
|
+
# Team
|
|
1702
|
+
team = api.get("team")
|
|
1703
|
+
if isinstance(team, dict):
|
|
1704
|
+
lines.append(f"Team: {team.get('name', 'Unknown')}")
|
|
1705
|
+
|
|
1706
|
+
# Current version
|
|
1707
|
+
version = api.get("current_version")
|
|
1708
|
+
if isinstance(version, dict):
|
|
1709
|
+
lines.append("")
|
|
1710
|
+
lines.append("**Current Version:**")
|
|
1711
|
+
lines.append(f"- Version: {version.get('version', 'N/A')}")
|
|
1712
|
+
lines.append(f"- OpenAPI: {version.get('openapi_version', 'N/A')}")
|
|
1713
|
+
|
|
1714
|
+
# Endpoints from current version
|
|
1715
|
+
endpoints = version.get("endpoint_embeddings", [])
|
|
1716
|
+
if endpoints:
|
|
1717
|
+
lines.append(f"- Endpoints: {len(endpoints)}")
|
|
1718
|
+
lines.append("")
|
|
1719
|
+
lines.append("**Endpoints:**")
|
|
1720
|
+
for ep in endpoints[:10]: # Limit to 10
|
|
1721
|
+
method = ep.get("method", "").upper()
|
|
1722
|
+
path = ep.get("path", "")
|
|
1723
|
+
summary = ep.get("summary", "")
|
|
1724
|
+
if summary:
|
|
1725
|
+
lines.append(f"- **{method}** {path} - {summary}")
|
|
1726
|
+
else:
|
|
1727
|
+
lines.append(f"- **{method}** {path}")
|
|
1728
|
+
if len(endpoints) > 10:
|
|
1729
|
+
lines.append(f" ... and {len(endpoints) - 10} more endpoints")
|
|
1730
|
+
|
|
1731
|
+
# Environments
|
|
1732
|
+
environments = api.get("environments", [])
|
|
1733
|
+
if environments:
|
|
1734
|
+
lines.append("")
|
|
1735
|
+
lines.append("**Environments:**")
|
|
1736
|
+
for env in environments:
|
|
1737
|
+
name = env.get("name", "Unnamed")
|
|
1738
|
+
url = env.get("base_url", "")
|
|
1739
|
+
lines.append(f"- {name}: {url}")
|
|
1740
|
+
|
|
1741
|
+
# Permissions
|
|
1742
|
+
permissions = api.get("permissions", {})
|
|
1743
|
+
if permissions:
|
|
1744
|
+
lines.append("")
|
|
1745
|
+
lines.append("**Your Permissions:**")
|
|
1746
|
+
if permissions.get("canEdit"):
|
|
1747
|
+
lines.append("- ✅ Edit")
|
|
1748
|
+
if permissions.get("canDelete"):
|
|
1749
|
+
lines.append("- ✅ Delete")
|
|
1750
|
+
if permissions.get("canPublish"):
|
|
1751
|
+
lines.append("- ✅ Publish")
|
|
1752
|
+
if permissions.get("canManageVersions"):
|
|
1753
|
+
lines.append("- ✅ Manage Versions")
|
|
1754
|
+
|
|
1755
|
+
return "\n".join(lines)
|
|
1756
|
+
|
|
1757
|
+
|
|
1426
1758
|
__all__ = ["KnowledgeTool", "KnowledgeAction"]
|
|
@@ -15,7 +15,7 @@ class InitializationData:
|
|
|
15
15
|
profile: Optional[Dict[str, Any]]
|
|
16
16
|
core_documents: List[Dict[str, Any]]
|
|
17
17
|
user_documents: List[Dict[str, Any]]
|
|
18
|
-
|
|
18
|
+
my_work_items: List[Dict[str, Any]]
|
|
19
19
|
|
|
20
20
|
|
|
21
21
|
class InitializationService:
|
|
@@ -25,9 +25,7 @@ class InitializationService:
|
|
|
25
25
|
self._api = api_client
|
|
26
26
|
self._logger = logger
|
|
27
27
|
|
|
28
|
-
async def gather_data(
|
|
29
|
-
self, *, include_user_docs: bool, limit: int
|
|
30
|
-
) -> InitializationData:
|
|
28
|
+
async def gather_data(self, *, include_user_docs: bool) -> InitializationData:
|
|
31
29
|
profile = await self._safe_call(self._api.get_profile)
|
|
32
30
|
core_docs = await self._safe_call(
|
|
33
31
|
self._api.list_core_documents,
|
|
@@ -46,11 +44,19 @@ class InitializationService:
|
|
|
46
44
|
)
|
|
47
45
|
if self._logger:
|
|
48
46
|
self._logger.debug("User docs response", extra={"user_docs": user_docs})
|
|
47
|
+
|
|
48
|
+
# Fetch user's assigned work items for proactive context
|
|
49
|
+
work_items_response = await self._safe_call(
|
|
50
|
+
self._api.get_work_items_mine,
|
|
51
|
+
limit=10,
|
|
52
|
+
)
|
|
53
|
+
my_work_items = self._extract_items(work_items_response, "workItems")
|
|
54
|
+
|
|
49
55
|
return InitializationData(
|
|
50
56
|
profile=profile,
|
|
51
57
|
core_documents=self._extract_items(core_docs, "coreDocuments"),
|
|
52
58
|
user_documents=self._extract_items(user_docs, "userCoreDocuments"),
|
|
53
|
-
|
|
59
|
+
my_work_items=my_work_items,
|
|
54
60
|
)
|
|
55
61
|
|
|
56
62
|
async def _safe_call(self, func, *args, **kwargs):
|
|
@@ -71,110 +77,3 @@ class InitializationService:
|
|
|
71
77
|
if isinstance(value, list):
|
|
72
78
|
return [item for item in value if isinstance(item, dict)]
|
|
73
79
|
return []
|
|
74
|
-
|
|
75
|
-
@staticmethod
|
|
76
|
-
def build_existing_user_summary(
|
|
77
|
-
data: InitializationData, include_user_docs: bool
|
|
78
|
-
) -> str:
|
|
79
|
-
profile = data.profile or {}
|
|
80
|
-
user_info = profile.get("user") or {}
|
|
81
|
-
tenant_info = profile.get("tenant") or {}
|
|
82
|
-
core_count = len(data.core_documents)
|
|
83
|
-
user_count = len(data.user_documents)
|
|
84
|
-
memories_count = len(data.recent_memories)
|
|
85
|
-
|
|
86
|
-
lines = [
|
|
87
|
-
"✅ **Fênix Cloud inicializado com sucesso!**",
|
|
88
|
-
]
|
|
89
|
-
|
|
90
|
-
if user_info:
|
|
91
|
-
lines.append(
|
|
92
|
-
f"- Usuário: {user_info.get('name') or 'Desconhecido'} "
|
|
93
|
-
f"(ID: {user_info.get('id', 'N/A')})"
|
|
94
|
-
)
|
|
95
|
-
if tenant_info:
|
|
96
|
-
lines.append(f"- Organização: {tenant_info.get('name', 'N/A')}")
|
|
97
|
-
|
|
98
|
-
lines.append(f"- Documentos principais carregados: {core_count}")
|
|
99
|
-
|
|
100
|
-
if include_user_docs:
|
|
101
|
-
lines.append(f"- Documentos pessoais carregados: {user_count}")
|
|
102
|
-
|
|
103
|
-
lines.append(f"- Memórias recentes disponíveis: {memories_count}")
|
|
104
|
-
|
|
105
|
-
if core_count:
|
|
106
|
-
preview = ", ".join(
|
|
107
|
-
doc.get("name", doc.get("title", "sem título"))
|
|
108
|
-
for doc in data.core_documents[:5]
|
|
109
|
-
)
|
|
110
|
-
lines.append(f"- Exemplos de documentos principais: {preview}")
|
|
111
|
-
|
|
112
|
-
if include_user_docs and user_count:
|
|
113
|
-
preview = ", ".join(
|
|
114
|
-
doc.get("name", "sem título") for doc in data.user_documents[:5]
|
|
115
|
-
)
|
|
116
|
-
lines.append(f"- Exemplos de documentos pessoais: {preview}")
|
|
117
|
-
|
|
118
|
-
if memories_count:
|
|
119
|
-
preview = ", ".join(
|
|
120
|
-
mem.get("title", "sem título") for mem in data.recent_memories[:3]
|
|
121
|
-
)
|
|
122
|
-
lines.append(f"- Memórias recentes: {preview}")
|
|
123
|
-
|
|
124
|
-
lines.append("")
|
|
125
|
-
lines.append(
|
|
126
|
-
"Agora você pode usar as ferramentas de produtividade, conhecimento e inteligência para continuar."
|
|
127
|
-
)
|
|
128
|
-
return "\n".join(lines)
|
|
129
|
-
|
|
130
|
-
@staticmethod
|
|
131
|
-
def build_new_user_prompt(data: InitializationData) -> str:
|
|
132
|
-
profile = data.profile or {}
|
|
133
|
-
user_name = (profile.get("user") or {}).get("name") or "Bem-vindo(a)"
|
|
134
|
-
|
|
135
|
-
questions = [
|
|
136
|
-
"Qual é o foco principal do seu trabalho atualmente?",
|
|
137
|
-
"Quais são seus objetivos para as próximas semanas?",
|
|
138
|
-
"Existem tarefas ou projetos que gostaria de priorizar?",
|
|
139
|
-
"Como você prefere organizar suas rotinas ou checklists?",
|
|
140
|
-
"Quais são os principais blocos de conhecimento que precisa acessar rapidamente?",
|
|
141
|
-
"Há regras ou procedimentos que precisam ser seguidos com frequência?",
|
|
142
|
-
"Que tipo de memória ou registro você consulta com frequência?",
|
|
143
|
-
"Quais ferramentas ou integrações externas são essenciais no seu dia a dia?",
|
|
144
|
-
"O que significaria sucesso para você utilizando o Fênix?",
|
|
145
|
-
]
|
|
146
|
-
|
|
147
|
-
body = [
|
|
148
|
-
f"👋 **{user_name}, bem-vindo(a) ao Fênix Cloud!**",
|
|
149
|
-
"",
|
|
150
|
-
"Notamos que você ainda não tem documentos pessoais configurados. "
|
|
151
|
-
"Vamos criar uma experiência personalizada para você.",
|
|
152
|
-
"",
|
|
153
|
-
"Responda às perguntas abaixo para que possamos preparar modos, regras e documentos alinhados às suas necessidades:",
|
|
154
|
-
"",
|
|
155
|
-
]
|
|
156
|
-
|
|
157
|
-
body.extend(f"{idx + 1}. {question}" for idx, question in enumerate(questions))
|
|
158
|
-
body.extend(
|
|
159
|
-
[
|
|
160
|
-
"",
|
|
161
|
-
"Envie as respostas no formato:",
|
|
162
|
-
"```",
|
|
163
|
-
'initialize action=setup answers=["resposta 1", "resposta 2", ..., "resposta 9"]',
|
|
164
|
-
"```",
|
|
165
|
-
"",
|
|
166
|
-
"Com base nisso, sugerirei documentos, regras e rotinas para você utilizar.",
|
|
167
|
-
]
|
|
168
|
-
)
|
|
169
|
-
|
|
170
|
-
return "\n".join(body)
|
|
171
|
-
|
|
172
|
-
@staticmethod
|
|
173
|
-
def validate_setup_answers(answers: List[str]) -> Optional[str]:
|
|
174
|
-
if not answers:
|
|
175
|
-
return "Envie uma lista com 9 respostas para processarmos o setup."
|
|
176
|
-
if len(answers) != 9:
|
|
177
|
-
return f"Esperado receber 9 respostas, mas recebi {len(answers)}."
|
|
178
|
-
if not all(isinstance(answer, str) and answer.strip() for answer in answers):
|
|
179
|
-
return "Todas as respostas devem ser textos preenchidos."
|
|
180
|
-
return None
|