Coverage for session_buddy / tools / team_tools.py: 19.75%
125 statements
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-04 00:43 -0800
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-04 00:43 -0800
1#!/usr/bin/env python3
2"""Team collaboration tools for session-mgmt-mcp.
4Following crackerjack architecture patterns for knowledge sharing,
5team coordination, and collaborative development workflows.
7Refactored to use utility modules for reduced code duplication.
8"""
10from __future__ import annotations
12import typing as t
13from typing import TYPE_CHECKING, Any
15from session_buddy.utils.error_handlers import _get_logger
16from session_buddy.utils.messages import ToolMessages
18if TYPE_CHECKING:
19 from fastmcp import FastMCP
22# Constants for error messages
23TEAM_NOT_AVAILABLE_MSG = (
24 "Team collaboration features not available. Install optional dependencies"
25)
28# ============================================================================
29# Service Resolution
30# ============================================================================
33async def _require_team_manager() -> Any:
34 """Get team knowledge manager or raise with helpful error message."""
35 try:
36 from session_buddy.team_knowledge import TeamKnowledgeManager
38 return TeamKnowledgeManager()
39 except ImportError:
40 _get_logger().warning("Team knowledge system not available")
41 raise RuntimeError(TEAM_NOT_AVAILABLE_MSG)
44async def _execute_team_operation(
45 operation_name: str,
46 operation: t.Callable[[Any], t.Awaitable[str]],
47) -> str:
48 """Execute a team operation with error handling."""
49 try:
50 manager = await _require_team_manager()
51 return await operation(manager)
52 except RuntimeError as e:
53 return f"❌ {e!s}"
54 except ValueError as e:
55 return f"❌ {operation_name} failed: {e!s}"
56 except Exception as e:
57 _get_logger().exception(f"Error in {operation_name}: {e}")
58 return ToolMessages.operation_failed(operation_name, e)
61# ============================================================================
62# Output Formatting Helpers
63# ============================================================================
66def _format_search_result(result: dict[str, Any], index: int) -> str:
67 """Format a single search result."""
68 output = f"**{index}.** "
70 # Add metadata
71 if result.get("team_id"):
72 output += f"[{result['team_id']}] "
73 if result.get("author"):
74 output += f"by {result['author']} "
75 if result.get("timestamp"):
76 output += f"({result['timestamp']}) "
78 # Add content preview
79 content = result.get("content", "")
80 output += f"\n{content[:200]}...\n"
82 # Add tags if available
83 if result.get("tags"):
84 output += f"🏷️ Tags: {', '.join(result['tags'])}\n"
86 # Add voting info if available
87 if result.get("votes"):
88 votes = result["votes"]
89 output += f"👍 Votes: {votes} "
91 output += "\n"
92 return output
95def _format_search_scope(team_id: str | None, project_id: str | None) -> str:
96 """Format the search scope string."""
97 search_scope = "team knowledge"
98 if team_id:
99 search_scope += f" (team: {team_id})"
100 if project_id:
101 search_scope += f" (project: {project_id})"
102 return search_scope
105def _format_basic_stats(stats: dict[str, Any]) -> str:
106 """Format basic team statistics."""
107 return (
108 f"**Members**: {stats.get('member_count', 0)}\n"
109 f"**Reflections**: {stats.get('reflection_count', 0)}\n"
110 f"**Projects**: {stats.get('project_count', 0)}\n"
111 f"**Total Votes**: {stats.get('total_votes', 0)}\n\n"
112 )
115def _format_activity_stats(stats: dict[str, Any]) -> str:
116 """Format recent activity statistics."""
117 if not stats.get("recent_activity"):
118 return ""
120 output = "**Recent Activity**:\n"
121 for activity in stats["recent_activity"][:5]:
122 output += (
123 f"- {activity.get('timestamp', '')}: {activity.get('description', '')}\n"
124 )
125 return output
128def _format_contributor_stats(stats: dict[str, Any]) -> str:
129 """Format top contributors statistics."""
130 if not stats.get("top_contributors"):
131 return ""
133 output = "\n**Top Contributors**:\n"
134 for contributor in stats["top_contributors"][:5]:
135 username = contributor.get("username", "")
136 contributions = contributor.get("contributions", 0)
137 output += f"- {username}: {contributions} contributions\n"
138 return output
141def _format_popular_tags(stats: dict[str, Any]) -> str:
142 """Format popular tags statistics."""
143 if not stats.get("popular_tags"):
144 return ""
146 tags = ", ".join(stats["popular_tags"][:10])
147 return f"\n**Popular Tags**: {tags}\n"
150def _format_team_statistics(team_id: str, stats: dict[str, Any]) -> str:
151 """Format team statistics for display."""
152 output = f"📊 **Team Statistics: {team_id}**\n\n"
154 output += _format_basic_stats(stats)
155 output += _format_activity_stats(stats)
156 output += _format_contributor_stats(stats)
157 output += _format_popular_tags(stats)
159 return output
162# ============================================================================
163# Team Operation Implementations
164# ============================================================================
167async def _create_team_operation(
168 manager: Any,
169 team_id: str,
170 name: str,
171 description: str,
172 owner_id: str,
173) -> str:
174 """Create a new team for knowledge sharing."""
175 await manager.create_team(
176 team_id=team_id,
177 name=name,
178 description=description,
179 owner_id=owner_id,
180 )
181 return f"✅ Team created successfully: {name}"
184async def _create_team_impl(
185 team_id: str,
186 name: str,
187 description: str,
188 owner_id: str,
189) -> str:
190 """Create a new team for knowledge sharing."""
192 async def operation(manager: Any) -> str:
193 return await _create_team_operation(
194 manager, team_id, name, description, owner_id
195 )
197 return await _execute_team_operation(
198 "Create team",
199 operation,
200 )
203async def _search_team_knowledge_operation(
204 manager: Any,
205 query: str,
206 user_id: str,
207 team_id: str | None,
208 project_id: str | None,
209 tags: list[str] | None,
210 limit: int,
211) -> str:
212 """Search team reflections with access control."""
213 results = await manager.search_team_reflections(
214 query=query,
215 user_id=user_id,
216 team_id=team_id,
217 project_id=project_id,
218 tags=tags,
219 limit=limit,
220 )
222 if not results:
223 search_scope = _format_search_scope(team_id, project_id)
224 return f"🔍 No results found in {search_scope} for: {query}"
226 output = f"🔍 **{len(results)} team knowledge results** for '{query}'\n\n"
228 for i, result in enumerate(results, 1):
229 output += _format_search_result(result, i)
231 return output
234async def _search_team_knowledge_impl(
235 query: str,
236 user_id: str,
237 team_id: str | None = None,
238 project_id: str | None = None,
239 tags: list[str] | None = None,
240 limit: int = 20,
241) -> str:
242 """Search team reflections with access control."""
244 async def operation(manager: Any) -> str:
245 return await _search_team_knowledge_operation(
246 manager,
247 query,
248 user_id,
249 team_id,
250 project_id,
251 tags,
252 limit,
253 )
255 return await _execute_team_operation(
256 "Search team knowledge",
257 operation,
258 )
261async def _get_team_statistics_operation(
262 manager: Any,
263 team_id: str,
264 user_id: str,
265) -> str:
266 """Get team statistics and activity."""
267 stats = await manager.get_team_stats(team_id=team_id, user_id=user_id)
269 if not stats:
270 return "❌ Failed to retrieve team statistics"
272 return _format_team_statistics(team_id, stats)
275async def _get_team_statistics_impl(team_id: str, user_id: str) -> str:
276 """Get team statistics and activity."""
278 async def operation(manager: Any) -> str:
279 return await _get_team_statistics_operation(manager, team_id, user_id)
281 return await _execute_team_operation(
282 "Get team statistics",
283 operation,
284 )
287async def _vote_on_reflection_operation(
288 manager: Any,
289 reflection_id: str,
290 user_id: str,
291 vote_delta: int,
292) -> str:
293 """Vote on a team reflection (upvote/downvote)."""
294 result = await manager.vote_reflection(
295 reflection_id=reflection_id,
296 user_id=user_id,
297 vote_delta=vote_delta,
298 )
300 if result:
301 return "✅ Reflection voted on successfully\n📊 Vote recorded\n"
302 return "❌ Failed to vote on reflection"
305async def _vote_on_reflection_impl(
306 reflection_id: str,
307 user_id: str,
308 vote_delta: int = 1,
309) -> str:
310 """Vote on a team reflection (upvote/downvote)."""
312 async def operation(manager: Any) -> str:
313 return await _vote_on_reflection_operation(
314 manager, reflection_id, user_id, vote_delta
315 )
317 return await _execute_team_operation(
318 "Vote on reflection",
319 operation,
320 )
323# ============================================================================
324# MCP Tool Registration
325# ============================================================================
328def register_team_tools(mcp: FastMCP) -> None:
329 """Register all team collaboration MCP tools.
331 Args:
332 mcp: FastMCP server instance
334 """
336 @mcp.tool()
337 async def create_team(
338 team_id: str,
339 name: str,
340 description: str,
341 owner_id: str,
342 ) -> str:
343 """Create a new team for knowledge sharing."""
344 return await _create_team_impl(team_id, name, description, owner_id)
346 @mcp.tool()
347 async def search_team_knowledge(
348 query: str,
349 user_id: str,
350 team_id: str | None = None,
351 project_id: str | None = None,
352 tags: list[str] | None = None,
353 limit: int = 20,
354 ) -> str:
355 """Search team reflections with access control."""
356 return await _search_team_knowledge_impl(
357 query,
358 user_id,
359 team_id,
360 project_id,
361 tags,
362 limit,
363 )
365 @mcp.tool()
366 async def get_team_statistics(team_id: str, user_id: str) -> str:
367 """Get team statistics and activity."""
368 return await _get_team_statistics_impl(team_id, user_id)
370 @mcp.tool()
371 async def vote_on_reflection(
372 reflection_id: str,
373 user_id: str,
374 vote_delta: int = 1,
375 ) -> str:
376 """Vote on a team reflection (upvote/downvote)."""
377 return await _vote_on_reflection_impl(reflection_id, user_id, vote_delta)