claude-mpm 4.1.4__py3-none-any.whl → 4.1.6__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.
- claude_mpm/VERSION +1 -1
- claude_mpm/agents/templates/research.json +39 -13
- claude_mpm/cli/__init__.py +2 -0
- claude_mpm/cli/commands/__init__.py +2 -0
- claude_mpm/cli/commands/configure.py +1221 -0
- claude_mpm/cli/commands/configure_tui.py +1921 -0
- claude_mpm/cli/commands/tickets.py +365 -784
- claude_mpm/cli/parsers/base_parser.py +7 -0
- claude_mpm/cli/parsers/configure_parser.py +119 -0
- claude_mpm/cli/startup_logging.py +39 -12
- claude_mpm/constants.py +1 -0
- claude_mpm/core/output_style_manager.py +24 -0
- claude_mpm/core/socketio_pool.py +35 -3
- claude_mpm/core/unified_agent_registry.py +46 -15
- claude_mpm/dashboard/static/css/connection-status.css +370 -0
- claude_mpm/dashboard/static/js/components/connection-debug.js +654 -0
- claude_mpm/dashboard/static/js/connection-manager.js +536 -0
- claude_mpm/dashboard/templates/index.html +11 -0
- claude_mpm/hooks/claude_hooks/services/__init__.py +3 -1
- claude_mpm/hooks/claude_hooks/services/connection_manager_http.py +190 -0
- claude_mpm/services/agents/deployment/agent_discovery_service.py +12 -3
- claude_mpm/services/agents/deployment/agent_lifecycle_manager.py +172 -233
- claude_mpm/services/agents/deployment/agent_lifecycle_manager_refactored.py +575 -0
- claude_mpm/services/agents/deployment/agent_operation_service.py +573 -0
- claude_mpm/services/agents/deployment/agent_record_service.py +419 -0
- claude_mpm/services/agents/deployment/agent_state_service.py +381 -0
- claude_mpm/services/agents/deployment/multi_source_deployment_service.py +4 -2
- claude_mpm/services/diagnostics/checks/__init__.py +2 -0
- claude_mpm/services/diagnostics/checks/instructions_check.py +418 -0
- claude_mpm/services/diagnostics/diagnostic_runner.py +15 -2
- claude_mpm/services/event_bus/direct_relay.py +173 -0
- claude_mpm/services/infrastructure/__init__.py +31 -5
- claude_mpm/services/infrastructure/monitoring/__init__.py +43 -0
- claude_mpm/services/infrastructure/monitoring/aggregator.py +437 -0
- claude_mpm/services/infrastructure/monitoring/base.py +130 -0
- claude_mpm/services/infrastructure/monitoring/legacy.py +203 -0
- claude_mpm/services/infrastructure/monitoring/network.py +218 -0
- claude_mpm/services/infrastructure/monitoring/process.py +342 -0
- claude_mpm/services/infrastructure/monitoring/resources.py +243 -0
- claude_mpm/services/infrastructure/monitoring/service.py +367 -0
- claude_mpm/services/infrastructure/monitoring.py +67 -1030
- claude_mpm/services/project/analyzer.py +13 -4
- claude_mpm/services/project/analyzer_refactored.py +450 -0
- claude_mpm/services/project/analyzer_v2.py +566 -0
- claude_mpm/services/project/architecture_analyzer.py +461 -0
- claude_mpm/services/project/dependency_analyzer.py +462 -0
- claude_mpm/services/project/language_analyzer.py +265 -0
- claude_mpm/services/project/metrics_collector.py +410 -0
- claude_mpm/services/socketio/handlers/connection_handler.py +345 -0
- claude_mpm/services/socketio/server/broadcaster.py +32 -1
- claude_mpm/services/socketio/server/connection_manager.py +516 -0
- claude_mpm/services/socketio/server/core.py +63 -0
- claude_mpm/services/socketio/server/eventbus_integration.py +20 -9
- claude_mpm/services/socketio/server/main.py +27 -1
- claude_mpm/services/ticket_manager.py +5 -1
- claude_mpm/services/ticket_services/__init__.py +26 -0
- claude_mpm/services/ticket_services/crud_service.py +328 -0
- claude_mpm/services/ticket_services/formatter_service.py +290 -0
- claude_mpm/services/ticket_services/search_service.py +324 -0
- claude_mpm/services/ticket_services/validation_service.py +303 -0
- claude_mpm/services/ticket_services/workflow_service.py +244 -0
- {claude_mpm-4.1.4.dist-info → claude_mpm-4.1.6.dist-info}/METADATA +3 -1
- {claude_mpm-4.1.4.dist-info → claude_mpm-4.1.6.dist-info}/RECORD +67 -46
- claude_mpm/agents/OUTPUT_STYLE.md +0 -73
- claude_mpm/agents/backups/INSTRUCTIONS.md +0 -352
- claude_mpm/agents/templates/OPTIMIZATION_REPORT.md +0 -156
- claude_mpm/agents/templates/backup/data_engineer_agent_20250726_234551.json +0 -79
- claude_mpm/agents/templates/backup/documentation_agent_20250726_234551.json +0 -68
- claude_mpm/agents/templates/backup/engineer_agent_20250726_234551.json +0 -77
- claude_mpm/agents/templates/backup/ops_agent_20250726_234551.json +0 -78
- claude_mpm/agents/templates/backup/qa_agent_20250726_234551.json +0 -67
- claude_mpm/agents/templates/backup/research_agent_2025011_234551.json +0 -88
- claude_mpm/agents/templates/backup/research_agent_20250726_234551.json +0 -72
- claude_mpm/agents/templates/backup/research_memory_efficient.json +0 -88
- claude_mpm/agents/templates/backup/security_agent_20250726_234551.json +0 -78
- claude_mpm/agents/templates/backup/version_control_agent_20250726_234551.json +0 -62
- claude_mpm/agents/templates/vercel_ops_instructions.md +0 -582
- {claude_mpm-4.1.4.dist-info → claude_mpm-4.1.6.dist-info}/WHEEL +0 -0
- {claude_mpm-4.1.4.dist-info → claude_mpm-4.1.6.dist-info}/entry_points.txt +0 -0
- {claude_mpm-4.1.4.dist-info → claude_mpm-4.1.6.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-4.1.4.dist-info → claude_mpm-4.1.6.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Output formatting service for tickets.
|
|
3
|
+
|
|
4
|
+
WHY: Separates presentation logic from business logic, allowing for
|
|
5
|
+
consistent formatting across different output modes and contexts.
|
|
6
|
+
|
|
7
|
+
DESIGN DECISIONS:
|
|
8
|
+
- Supports multiple output formats (text, json, yaml, table)
|
|
9
|
+
- Uses emoji for visual status indicators
|
|
10
|
+
- Handles pagination display
|
|
11
|
+
- Provides consistent formatting patterns
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from typing import Any, Dict, List, Optional
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class TicketFormatterService:
|
|
18
|
+
"""Service for formatting ticket output."""
|
|
19
|
+
|
|
20
|
+
# Status emoji mapping for visual indicators
|
|
21
|
+
STATUS_EMOJI = {
|
|
22
|
+
"open": "🔵",
|
|
23
|
+
"in_progress": "🟡",
|
|
24
|
+
"done": "🟢",
|
|
25
|
+
"closed": "⚫",
|
|
26
|
+
"blocked": "🔴",
|
|
27
|
+
"ready": "🟣",
|
|
28
|
+
"tested": "🟢",
|
|
29
|
+
"waiting": "🟠",
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
DEFAULT_EMOJI = "⚪"
|
|
33
|
+
|
|
34
|
+
def format_ticket_created(
|
|
35
|
+
self, ticket_id: str, verbose: bool = False, **metadata
|
|
36
|
+
) -> List[str]:
|
|
37
|
+
"""
|
|
38
|
+
Format output for created ticket.
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
List of formatted output lines
|
|
42
|
+
"""
|
|
43
|
+
lines = [f"✅ Created ticket: {ticket_id}"]
|
|
44
|
+
|
|
45
|
+
if verbose:
|
|
46
|
+
if metadata.get("type"):
|
|
47
|
+
lines.append(f" Type: {metadata['type']}")
|
|
48
|
+
if metadata.get("priority"):
|
|
49
|
+
lines.append(f" Priority: {metadata['priority']}")
|
|
50
|
+
if metadata.get("tags"):
|
|
51
|
+
lines.append(f" Tags: {', '.join(metadata['tags'])}")
|
|
52
|
+
if metadata.get("parent_epic"):
|
|
53
|
+
lines.append(f" Parent Epic: {metadata['parent_epic']}")
|
|
54
|
+
if metadata.get("parent_issue"):
|
|
55
|
+
lines.append(f" Parent Issue: {metadata['parent_issue']}")
|
|
56
|
+
|
|
57
|
+
return lines
|
|
58
|
+
|
|
59
|
+
def format_ticket_list(
|
|
60
|
+
self,
|
|
61
|
+
tickets: List[Dict[str, Any]],
|
|
62
|
+
page: int = 1,
|
|
63
|
+
page_size: int = 20,
|
|
64
|
+
verbose: bool = False,
|
|
65
|
+
) -> List[str]:
|
|
66
|
+
"""
|
|
67
|
+
Format a list of tickets for display.
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
List of formatted output lines
|
|
71
|
+
"""
|
|
72
|
+
if not tickets:
|
|
73
|
+
return ["No tickets found matching criteria"]
|
|
74
|
+
|
|
75
|
+
total_shown = len(tickets)
|
|
76
|
+
lines = [f"Tickets (page {page}, showing {total_shown} tickets):", "-" * 80]
|
|
77
|
+
|
|
78
|
+
for ticket in tickets:
|
|
79
|
+
# Get status emoji
|
|
80
|
+
status = ticket.get("status", "unknown")
|
|
81
|
+
emoji = self.STATUS_EMOJI.get(status, self.DEFAULT_EMOJI)
|
|
82
|
+
|
|
83
|
+
lines.append(f"{emoji} [{ticket['id']}] {ticket['title']}")
|
|
84
|
+
|
|
85
|
+
if verbose:
|
|
86
|
+
ticket_type = ticket.get("metadata", {}).get("ticket_type", "task")
|
|
87
|
+
priority = ticket.get("priority", "medium")
|
|
88
|
+
lines.append(
|
|
89
|
+
f" Type: {ticket_type} | Status: {status} | Priority: {priority}"
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
if ticket.get("tags"):
|
|
93
|
+
lines.append(f" Tags: {', '.join(ticket['tags'])}")
|
|
94
|
+
|
|
95
|
+
lines.append(f" Created: {ticket.get('created_at', 'Unknown')}")
|
|
96
|
+
lines.append("")
|
|
97
|
+
|
|
98
|
+
# Add pagination hints
|
|
99
|
+
if total_shown == page_size:
|
|
100
|
+
lines.extend(
|
|
101
|
+
[
|
|
102
|
+
"-" * 80,
|
|
103
|
+
f"📄 Page {page} | Showing {total_shown} tickets",
|
|
104
|
+
f"💡 Next page: claude-mpm tickets list --page {page + 1} --page-size {page_size}",
|
|
105
|
+
]
|
|
106
|
+
)
|
|
107
|
+
if page > 1:
|
|
108
|
+
lines.append(
|
|
109
|
+
f"💡 Previous page: claude-mpm tickets list --page {page - 1} --page-size {page_size}"
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
return lines
|
|
113
|
+
|
|
114
|
+
def format_ticket_detail(
|
|
115
|
+
self, ticket: Dict[str, Any], verbose: bool = False
|
|
116
|
+
) -> List[str]:
|
|
117
|
+
"""
|
|
118
|
+
Format a single ticket's details for display.
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
List of formatted output lines
|
|
122
|
+
"""
|
|
123
|
+
if not ticket:
|
|
124
|
+
return ["❌ Ticket not found"]
|
|
125
|
+
|
|
126
|
+
lines = [
|
|
127
|
+
f"Ticket: {ticket['id']}",
|
|
128
|
+
"=" * 80,
|
|
129
|
+
f"Title: {ticket['title']}",
|
|
130
|
+
f"Type: {ticket.get('metadata', {}).get('ticket_type', 'unknown')}",
|
|
131
|
+
f"Status: {ticket['status']}",
|
|
132
|
+
f"Priority: {ticket['priority']}",
|
|
133
|
+
]
|
|
134
|
+
|
|
135
|
+
if ticket.get("tags"):
|
|
136
|
+
lines.append(f"Tags: {', '.join(ticket['tags'])}")
|
|
137
|
+
|
|
138
|
+
if ticket.get("assignees"):
|
|
139
|
+
lines.append(f"Assignees: {', '.join(ticket['assignees'])}")
|
|
140
|
+
|
|
141
|
+
# Show parent references
|
|
142
|
+
metadata = ticket.get("metadata", {})
|
|
143
|
+
if metadata.get("parent_epic"):
|
|
144
|
+
lines.append(f"Parent Epic: {metadata['parent_epic']}")
|
|
145
|
+
if metadata.get("parent_issue"):
|
|
146
|
+
lines.append(f"Parent Issue: {metadata['parent_issue']}")
|
|
147
|
+
|
|
148
|
+
lines.extend(
|
|
149
|
+
[
|
|
150
|
+
"",
|
|
151
|
+
"Description:",
|
|
152
|
+
"-" * 40,
|
|
153
|
+
ticket.get("description", "No description"),
|
|
154
|
+
"",
|
|
155
|
+
f"Created: {ticket['created_at']}",
|
|
156
|
+
f"Updated: {ticket['updated_at']}",
|
|
157
|
+
]
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
if verbose and metadata:
|
|
161
|
+
lines.extend(["", "Metadata:", "-" * 40])
|
|
162
|
+
for key, value in metadata.items():
|
|
163
|
+
if key not in ["parent_epic", "parent_issue", "ticket_type"]:
|
|
164
|
+
lines.append(f" {key}: {value}")
|
|
165
|
+
|
|
166
|
+
return lines
|
|
167
|
+
|
|
168
|
+
def format_search_results(
|
|
169
|
+
self, tickets: List[Dict[str, Any]], query: str, show_snippets: bool = True
|
|
170
|
+
) -> List[str]:
|
|
171
|
+
"""
|
|
172
|
+
Format search results with optional context snippets.
|
|
173
|
+
|
|
174
|
+
Returns:
|
|
175
|
+
List of formatted output lines
|
|
176
|
+
"""
|
|
177
|
+
if not tickets:
|
|
178
|
+
return [f"No tickets found matching '{query}'"]
|
|
179
|
+
|
|
180
|
+
lines = [f"Search results for '{query}' (showing {len(tickets)}):", "-" * 80]
|
|
181
|
+
|
|
182
|
+
for ticket in tickets:
|
|
183
|
+
status = ticket.get("status", "unknown")
|
|
184
|
+
emoji = self.STATUS_EMOJI.get(status, self.DEFAULT_EMOJI)
|
|
185
|
+
|
|
186
|
+
lines.append(f"{emoji} [{ticket['id']}] {ticket['title']}")
|
|
187
|
+
|
|
188
|
+
if show_snippets:
|
|
189
|
+
# Show snippet if query appears in description
|
|
190
|
+
desc = ticket.get("description", "")
|
|
191
|
+
if query.lower() in desc.lower():
|
|
192
|
+
snippet = self._get_search_snippet(desc, query)
|
|
193
|
+
lines.append(f" {snippet}")
|
|
194
|
+
|
|
195
|
+
return lines
|
|
196
|
+
|
|
197
|
+
def _get_search_snippet(
|
|
198
|
+
self, text: str, query: str, context_chars: int = 30
|
|
199
|
+
) -> str:
|
|
200
|
+
"""
|
|
201
|
+
Extract a snippet of text around the search query.
|
|
202
|
+
|
|
203
|
+
Returns:
|
|
204
|
+
Formatted snippet with ellipsis if truncated
|
|
205
|
+
"""
|
|
206
|
+
lower_text = text.lower()
|
|
207
|
+
query_lower = query.lower()
|
|
208
|
+
|
|
209
|
+
if query_lower not in lower_text:
|
|
210
|
+
return ""
|
|
211
|
+
|
|
212
|
+
idx = lower_text.index(query_lower)
|
|
213
|
+
start = max(0, idx - context_chars)
|
|
214
|
+
end = min(len(text), idx + len(query) + context_chars)
|
|
215
|
+
|
|
216
|
+
snippet = text[start:end]
|
|
217
|
+
|
|
218
|
+
if start > 0:
|
|
219
|
+
snippet = "..." + snippet
|
|
220
|
+
if end < len(text):
|
|
221
|
+
snippet = snippet + "..."
|
|
222
|
+
|
|
223
|
+
return snippet
|
|
224
|
+
|
|
225
|
+
def format_operation_result(
|
|
226
|
+
self,
|
|
227
|
+
operation: str,
|
|
228
|
+
ticket_id: str,
|
|
229
|
+
success: bool,
|
|
230
|
+
message: Optional[str] = None,
|
|
231
|
+
) -> str:
|
|
232
|
+
"""
|
|
233
|
+
Format the result of a ticket operation.
|
|
234
|
+
|
|
235
|
+
Returns:
|
|
236
|
+
Formatted result message
|
|
237
|
+
"""
|
|
238
|
+
if success:
|
|
239
|
+
emoji = "✅"
|
|
240
|
+
default_messages = {
|
|
241
|
+
"update": f"Updated ticket: {ticket_id}",
|
|
242
|
+
"close": f"Closed ticket: {ticket_id}",
|
|
243
|
+
"delete": f"Deleted ticket: {ticket_id}",
|
|
244
|
+
"comment": f"Added comment to ticket: {ticket_id}",
|
|
245
|
+
"workflow": f"Updated workflow state for {ticket_id}",
|
|
246
|
+
}
|
|
247
|
+
msg = message or default_messages.get(
|
|
248
|
+
operation, f"Operation completed: {ticket_id}"
|
|
249
|
+
)
|
|
250
|
+
else:
|
|
251
|
+
emoji = "❌"
|
|
252
|
+
default_messages = {
|
|
253
|
+
"update": f"Failed to update ticket: {ticket_id}",
|
|
254
|
+
"close": f"Failed to close ticket: {ticket_id}",
|
|
255
|
+
"delete": f"Failed to delete ticket: {ticket_id}",
|
|
256
|
+
"comment": f"Failed to add comment to ticket: {ticket_id}",
|
|
257
|
+
"workflow": f"Failed to update workflow state for ticket: {ticket_id}",
|
|
258
|
+
}
|
|
259
|
+
msg = message or default_messages.get(
|
|
260
|
+
operation, f"Operation failed: {ticket_id}"
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
return f"{emoji} {msg}"
|
|
264
|
+
|
|
265
|
+
def format_error(self, error: str) -> str:
|
|
266
|
+
"""
|
|
267
|
+
Format an error message.
|
|
268
|
+
|
|
269
|
+
Returns:
|
|
270
|
+
Formatted error message
|
|
271
|
+
"""
|
|
272
|
+
return f"❌ {error}"
|
|
273
|
+
|
|
274
|
+
def format_info(self, info: str) -> str:
|
|
275
|
+
"""
|
|
276
|
+
Format an informational message.
|
|
277
|
+
|
|
278
|
+
Returns:
|
|
279
|
+
Formatted info message
|
|
280
|
+
"""
|
|
281
|
+
return f"ℹ️ {info}"
|
|
282
|
+
|
|
283
|
+
def format_warning(self, warning: str) -> str:
|
|
284
|
+
"""
|
|
285
|
+
Format a warning message.
|
|
286
|
+
|
|
287
|
+
Returns:
|
|
288
|
+
Formatted warning message
|
|
289
|
+
"""
|
|
290
|
+
return f"⚠️ {warning}"
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Search service for tickets.
|
|
3
|
+
|
|
4
|
+
WHY: Extracts search logic into a dedicated service for better
|
|
5
|
+
organization and testability of search functionality.
|
|
6
|
+
|
|
7
|
+
DESIGN DECISIONS:
|
|
8
|
+
- Supports text-based search across multiple fields
|
|
9
|
+
- Handles filtering by type and status
|
|
10
|
+
- Provides relevance ranking for search results
|
|
11
|
+
- Abstracts search backend (can switch between different implementations)
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from typing import Any, Dict, List, Optional
|
|
15
|
+
|
|
16
|
+
from ...core.logger import get_logger
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class TicketSearchService:
|
|
20
|
+
"""Service for searching tickets."""
|
|
21
|
+
|
|
22
|
+
def __init__(self, ticket_manager=None):
|
|
23
|
+
"""
|
|
24
|
+
Initialize the search service.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
ticket_manager: Optional ticket manager instance for testing
|
|
28
|
+
"""
|
|
29
|
+
self.logger = get_logger("services.ticket_search")
|
|
30
|
+
self._ticket_manager = ticket_manager
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def ticket_manager(self):
|
|
34
|
+
"""Lazy load ticket manager."""
|
|
35
|
+
if self._ticket_manager is None:
|
|
36
|
+
try:
|
|
37
|
+
from ..ticket_manager import TicketManager
|
|
38
|
+
except ImportError:
|
|
39
|
+
from claude_mpm.services.ticket_manager import TicketManager
|
|
40
|
+
self._ticket_manager = TicketManager()
|
|
41
|
+
return self._ticket_manager
|
|
42
|
+
|
|
43
|
+
def search_tickets(
|
|
44
|
+
self,
|
|
45
|
+
query: str,
|
|
46
|
+
type_filter: str = "all",
|
|
47
|
+
status_filter: str = "all",
|
|
48
|
+
limit: int = 10,
|
|
49
|
+
search_fields: Optional[List[str]] = None,
|
|
50
|
+
) -> List[Dict[str, Any]]:
|
|
51
|
+
"""
|
|
52
|
+
Search tickets by query string.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
query: Search query
|
|
56
|
+
type_filter: Filter by ticket type
|
|
57
|
+
status_filter: Filter by status
|
|
58
|
+
limit: Maximum results to return
|
|
59
|
+
search_fields: Fields to search in (default: title, description, tags)
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
List of matching tickets
|
|
63
|
+
"""
|
|
64
|
+
if not search_fields:
|
|
65
|
+
search_fields = ["title", "description", "tags"]
|
|
66
|
+
|
|
67
|
+
try:
|
|
68
|
+
# Get all available tickets
|
|
69
|
+
all_tickets = self.ticket_manager.list_recent_tickets(limit=100)
|
|
70
|
+
|
|
71
|
+
# Search and filter
|
|
72
|
+
matched_tickets = []
|
|
73
|
+
query_lower = query.lower()
|
|
74
|
+
|
|
75
|
+
for ticket in all_tickets:
|
|
76
|
+
# Check if query matches any search field
|
|
77
|
+
if self._matches_query(ticket, query_lower, search_fields):
|
|
78
|
+
# Apply type filter
|
|
79
|
+
if not self._passes_type_filter(ticket, type_filter):
|
|
80
|
+
continue
|
|
81
|
+
|
|
82
|
+
# Apply status filter
|
|
83
|
+
if not self._passes_status_filter(ticket, status_filter):
|
|
84
|
+
continue
|
|
85
|
+
|
|
86
|
+
matched_tickets.append(ticket)
|
|
87
|
+
|
|
88
|
+
if len(matched_tickets) >= limit:
|
|
89
|
+
break
|
|
90
|
+
|
|
91
|
+
# Sort by relevance
|
|
92
|
+
return self._sort_by_relevance(matched_tickets, query_lower)
|
|
93
|
+
|
|
94
|
+
except Exception as e:
|
|
95
|
+
self.logger.error(f"Error searching tickets: {e}")
|
|
96
|
+
return []
|
|
97
|
+
|
|
98
|
+
def _matches_query(
|
|
99
|
+
self, ticket: Dict[str, Any], query: str, search_fields: List[str]
|
|
100
|
+
) -> bool:
|
|
101
|
+
"""
|
|
102
|
+
Check if ticket matches the search query.
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
True if ticket matches query in any search field
|
|
106
|
+
"""
|
|
107
|
+
for field in search_fields:
|
|
108
|
+
if field == "title" and query in ticket.get("title", "").lower():
|
|
109
|
+
return True
|
|
110
|
+
|
|
111
|
+
if (
|
|
112
|
+
field == "description"
|
|
113
|
+
and query in ticket.get("description", "").lower()
|
|
114
|
+
):
|
|
115
|
+
return True
|
|
116
|
+
|
|
117
|
+
if field == "tags":
|
|
118
|
+
tags = ticket.get("tags", [])
|
|
119
|
+
if any(query in tag.lower() for tag in tags):
|
|
120
|
+
return True
|
|
121
|
+
|
|
122
|
+
if field == "id" and query in ticket.get("id", "").lower():
|
|
123
|
+
return True
|
|
124
|
+
|
|
125
|
+
# Search in metadata fields
|
|
126
|
+
if field == "metadata":
|
|
127
|
+
metadata = ticket.get("metadata", {})
|
|
128
|
+
for value in metadata.values():
|
|
129
|
+
if isinstance(value, str) and query in value.lower():
|
|
130
|
+
return True
|
|
131
|
+
|
|
132
|
+
return False
|
|
133
|
+
|
|
134
|
+
def _passes_type_filter(self, ticket: Dict[str, Any], type_filter: str) -> bool:
|
|
135
|
+
"""
|
|
136
|
+
Check if ticket passes type filter.
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
True if ticket matches type filter
|
|
140
|
+
"""
|
|
141
|
+
if type_filter == "all":
|
|
142
|
+
return True
|
|
143
|
+
|
|
144
|
+
ticket_type = ticket.get("metadata", {}).get("ticket_type", "unknown")
|
|
145
|
+
return ticket_type == type_filter
|
|
146
|
+
|
|
147
|
+
def _passes_status_filter(self, ticket: Dict[str, Any], status_filter: str) -> bool:
|
|
148
|
+
"""
|
|
149
|
+
Check if ticket passes status filter.
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
True if ticket matches status filter
|
|
153
|
+
"""
|
|
154
|
+
if status_filter == "all":
|
|
155
|
+
return True
|
|
156
|
+
|
|
157
|
+
return ticket.get("status") == status_filter
|
|
158
|
+
|
|
159
|
+
def _sort_by_relevance(
|
|
160
|
+
self, tickets: List[Dict[str, Any]], query: str
|
|
161
|
+
) -> List[Dict[str, Any]]:
|
|
162
|
+
"""
|
|
163
|
+
Sort tickets by relevance to search query.
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
Sorted list of tickets (most relevant first)
|
|
167
|
+
"""
|
|
168
|
+
|
|
169
|
+
def relevance_score(ticket):
|
|
170
|
+
score = 0
|
|
171
|
+
|
|
172
|
+
# Title matches are most relevant
|
|
173
|
+
if query in ticket.get("title", "").lower():
|
|
174
|
+
score += 10
|
|
175
|
+
|
|
176
|
+
# ID matches are very relevant
|
|
177
|
+
if query in ticket.get("id", "").lower():
|
|
178
|
+
score += 8
|
|
179
|
+
|
|
180
|
+
# Tag matches are moderately relevant
|
|
181
|
+
tags = ticket.get("tags", [])
|
|
182
|
+
for tag in tags:
|
|
183
|
+
if query in tag.lower():
|
|
184
|
+
score += 5
|
|
185
|
+
|
|
186
|
+
# Description matches are less relevant
|
|
187
|
+
if query in ticket.get("description", "").lower():
|
|
188
|
+
score += 2
|
|
189
|
+
|
|
190
|
+
# Boost recent tickets slightly
|
|
191
|
+
if ticket.get("status") in ["open", "in_progress"]:
|
|
192
|
+
score += 1
|
|
193
|
+
|
|
194
|
+
return score
|
|
195
|
+
|
|
196
|
+
return sorted(tickets, key=relevance_score, reverse=True)
|
|
197
|
+
|
|
198
|
+
def find_similar_tickets(
|
|
199
|
+
self, ticket_id: str, limit: int = 5
|
|
200
|
+
) -> List[Dict[str, Any]]:
|
|
201
|
+
"""
|
|
202
|
+
Find tickets similar to a given ticket.
|
|
203
|
+
|
|
204
|
+
Args:
|
|
205
|
+
ticket_id: ID of the reference ticket
|
|
206
|
+
limit: Maximum similar tickets to return
|
|
207
|
+
|
|
208
|
+
Returns:
|
|
209
|
+
List of similar tickets
|
|
210
|
+
"""
|
|
211
|
+
try:
|
|
212
|
+
# Get the reference ticket
|
|
213
|
+
reference = self.ticket_manager.get_ticket(ticket_id)
|
|
214
|
+
if not reference:
|
|
215
|
+
return []
|
|
216
|
+
|
|
217
|
+
# Extract keywords from title and tags
|
|
218
|
+
keywords = self._extract_keywords(reference)
|
|
219
|
+
|
|
220
|
+
# Search for similar tickets
|
|
221
|
+
similar = []
|
|
222
|
+
all_tickets = self.ticket_manager.list_recent_tickets(limit=50)
|
|
223
|
+
|
|
224
|
+
for ticket in all_tickets:
|
|
225
|
+
if ticket["id"] == ticket_id:
|
|
226
|
+
continue # Skip the reference ticket
|
|
227
|
+
|
|
228
|
+
# Calculate similarity score
|
|
229
|
+
score = self._calculate_similarity(reference, ticket, keywords)
|
|
230
|
+
|
|
231
|
+
if score > 0:
|
|
232
|
+
similar.append((score, ticket))
|
|
233
|
+
|
|
234
|
+
# Sort by similarity and return top results
|
|
235
|
+
similar.sort(key=lambda x: x[0], reverse=True)
|
|
236
|
+
return [ticket for _, ticket in similar[:limit]]
|
|
237
|
+
|
|
238
|
+
except Exception as e:
|
|
239
|
+
self.logger.error(f"Error finding similar tickets: {e}")
|
|
240
|
+
return []
|
|
241
|
+
|
|
242
|
+
def _extract_keywords(self, ticket: Dict[str, Any]) -> List[str]:
|
|
243
|
+
"""
|
|
244
|
+
Extract keywords from a ticket for similarity matching.
|
|
245
|
+
|
|
246
|
+
Returns:
|
|
247
|
+
List of keywords
|
|
248
|
+
"""
|
|
249
|
+
keywords = []
|
|
250
|
+
|
|
251
|
+
# Extract words from title
|
|
252
|
+
title = ticket.get("title", "")
|
|
253
|
+
keywords.extend(title.lower().split())
|
|
254
|
+
|
|
255
|
+
# Add tags as keywords
|
|
256
|
+
keywords.extend([tag.lower() for tag in ticket.get("tags", [])])
|
|
257
|
+
|
|
258
|
+
# Extract key words from description (first 100 chars)
|
|
259
|
+
desc = ticket.get("description", "")[:100]
|
|
260
|
+
keywords.extend(desc.lower().split()[:10])
|
|
261
|
+
|
|
262
|
+
# Remove common words
|
|
263
|
+
common_words = {
|
|
264
|
+
"the",
|
|
265
|
+
"a",
|
|
266
|
+
"an",
|
|
267
|
+
"and",
|
|
268
|
+
"or",
|
|
269
|
+
"but",
|
|
270
|
+
"in",
|
|
271
|
+
"on",
|
|
272
|
+
"at",
|
|
273
|
+
"to",
|
|
274
|
+
"for",
|
|
275
|
+
}
|
|
276
|
+
keywords = [w for w in keywords if w not in common_words and len(w) > 2]
|
|
277
|
+
|
|
278
|
+
return list(set(keywords)) # Remove duplicates
|
|
279
|
+
|
|
280
|
+
def _calculate_similarity(
|
|
281
|
+
self, ref_ticket: Dict[str, Any], ticket: Dict[str, Any], keywords: List[str]
|
|
282
|
+
) -> float:
|
|
283
|
+
"""
|
|
284
|
+
Calculate similarity score between two tickets.
|
|
285
|
+
|
|
286
|
+
Returns:
|
|
287
|
+
Similarity score (0-100)
|
|
288
|
+
"""
|
|
289
|
+
score = 0.0
|
|
290
|
+
|
|
291
|
+
# Same type bonus
|
|
292
|
+
ref_type = ref_ticket.get("metadata", {}).get("ticket_type")
|
|
293
|
+
ticket_type = ticket.get("metadata", {}).get("ticket_type")
|
|
294
|
+
if ref_type and ticket_type and ref_type == ticket_type:
|
|
295
|
+
score += 10
|
|
296
|
+
|
|
297
|
+
# Same status bonus
|
|
298
|
+
if ref_ticket.get("status") == ticket.get("status"):
|
|
299
|
+
score += 5
|
|
300
|
+
|
|
301
|
+
# Same priority bonus
|
|
302
|
+
if ref_ticket.get("priority") == ticket.get("priority"):
|
|
303
|
+
score += 3
|
|
304
|
+
|
|
305
|
+
# Keyword matches
|
|
306
|
+
ticket_text = (
|
|
307
|
+
ticket.get("title", "").lower()
|
|
308
|
+
+ " "
|
|
309
|
+
+ ticket.get("description", "").lower()
|
|
310
|
+
+ " "
|
|
311
|
+
+ " ".join(ticket.get("tags", []))
|
|
312
|
+
).lower()
|
|
313
|
+
|
|
314
|
+
for keyword in keywords:
|
|
315
|
+
if keyword in ticket_text:
|
|
316
|
+
score += 2
|
|
317
|
+
|
|
318
|
+
# Common tags
|
|
319
|
+
ref_tags = set(ref_ticket.get("tags", []))
|
|
320
|
+
ticket_tags = set(ticket.get("tags", []))
|
|
321
|
+
common_tags = ref_tags.intersection(ticket_tags)
|
|
322
|
+
score += len(common_tags) * 5
|
|
323
|
+
|
|
324
|
+
return min(score, 100) # Cap at 100
|