mcp-ticketer 0.3.0__py3-none-any.whl → 2.2.9__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.
- mcp_ticketer/__init__.py +10 -10
- mcp_ticketer/__version__.py +3 -3
- mcp_ticketer/_version_scm.py +1 -0
- mcp_ticketer/adapters/__init__.py +2 -0
- mcp_ticketer/adapters/aitrackdown.py +930 -52
- mcp_ticketer/adapters/asana/__init__.py +15 -0
- mcp_ticketer/adapters/asana/adapter.py +1537 -0
- mcp_ticketer/adapters/asana/client.py +292 -0
- mcp_ticketer/adapters/asana/mappers.py +348 -0
- mcp_ticketer/adapters/asana/types.py +146 -0
- mcp_ticketer/adapters/github/__init__.py +26 -0
- mcp_ticketer/adapters/github/adapter.py +3229 -0
- mcp_ticketer/adapters/github/client.py +335 -0
- mcp_ticketer/adapters/github/mappers.py +797 -0
- mcp_ticketer/adapters/github/queries.py +692 -0
- mcp_ticketer/adapters/github/types.py +460 -0
- mcp_ticketer/adapters/hybrid.py +58 -16
- mcp_ticketer/adapters/jira/__init__.py +35 -0
- mcp_ticketer/adapters/jira/adapter.py +1351 -0
- mcp_ticketer/adapters/jira/client.py +271 -0
- mcp_ticketer/adapters/jira/mappers.py +246 -0
- mcp_ticketer/adapters/jira/queries.py +216 -0
- mcp_ticketer/adapters/jira/types.py +304 -0
- mcp_ticketer/adapters/linear/__init__.py +1 -1
- mcp_ticketer/adapters/linear/adapter.py +3810 -462
- mcp_ticketer/adapters/linear/client.py +312 -69
- mcp_ticketer/adapters/linear/mappers.py +305 -85
- mcp_ticketer/adapters/linear/queries.py +317 -17
- mcp_ticketer/adapters/linear/types.py +187 -64
- mcp_ticketer/adapters/linear.py +2 -2
- mcp_ticketer/analysis/__init__.py +56 -0
- mcp_ticketer/analysis/dependency_graph.py +255 -0
- mcp_ticketer/analysis/health_assessment.py +304 -0
- mcp_ticketer/analysis/orphaned.py +218 -0
- mcp_ticketer/analysis/project_status.py +594 -0
- mcp_ticketer/analysis/similarity.py +224 -0
- mcp_ticketer/analysis/staleness.py +266 -0
- mcp_ticketer/automation/__init__.py +11 -0
- mcp_ticketer/automation/project_updates.py +378 -0
- mcp_ticketer/cache/memory.py +9 -8
- mcp_ticketer/cli/adapter_diagnostics.py +91 -54
- mcp_ticketer/cli/auggie_configure.py +116 -15
- mcp_ticketer/cli/codex_configure.py +274 -82
- mcp_ticketer/cli/configure.py +1323 -151
- mcp_ticketer/cli/cursor_configure.py +314 -0
- mcp_ticketer/cli/diagnostics.py +209 -114
- mcp_ticketer/cli/discover.py +297 -26
- mcp_ticketer/cli/gemini_configure.py +119 -26
- mcp_ticketer/cli/init_command.py +880 -0
- mcp_ticketer/cli/install_mcp_server.py +418 -0
- mcp_ticketer/cli/instruction_commands.py +435 -0
- mcp_ticketer/cli/linear_commands.py +256 -130
- mcp_ticketer/cli/main.py +140 -1544
- mcp_ticketer/cli/mcp_configure.py +1013 -100
- mcp_ticketer/cli/mcp_server_commands.py +415 -0
- mcp_ticketer/cli/migrate_config.py +12 -8
- mcp_ticketer/cli/platform_commands.py +123 -0
- mcp_ticketer/cli/platform_detection.py +477 -0
- mcp_ticketer/cli/platform_installer.py +545 -0
- mcp_ticketer/cli/project_update_commands.py +350 -0
- mcp_ticketer/cli/python_detection.py +126 -0
- mcp_ticketer/cli/queue_commands.py +15 -15
- mcp_ticketer/cli/setup_command.py +794 -0
- mcp_ticketer/cli/simple_health.py +84 -59
- mcp_ticketer/cli/ticket_commands.py +1375 -0
- mcp_ticketer/cli/update_checker.py +313 -0
- mcp_ticketer/cli/utils.py +195 -72
- mcp_ticketer/core/__init__.py +64 -1
- mcp_ticketer/core/adapter.py +618 -18
- mcp_ticketer/core/config.py +77 -68
- mcp_ticketer/core/env_discovery.py +75 -16
- mcp_ticketer/core/env_loader.py +121 -97
- mcp_ticketer/core/exceptions.py +32 -24
- mcp_ticketer/core/http_client.py +26 -26
- mcp_ticketer/core/instructions.py +405 -0
- mcp_ticketer/core/label_manager.py +732 -0
- mcp_ticketer/core/mappers.py +42 -30
- mcp_ticketer/core/milestone_manager.py +252 -0
- mcp_ticketer/core/models.py +566 -19
- mcp_ticketer/core/onepassword_secrets.py +379 -0
- mcp_ticketer/core/priority_matcher.py +463 -0
- mcp_ticketer/core/project_config.py +189 -49
- mcp_ticketer/core/project_utils.py +281 -0
- mcp_ticketer/core/project_validator.py +376 -0
- mcp_ticketer/core/registry.py +3 -3
- mcp_ticketer/core/session_state.py +176 -0
- mcp_ticketer/core/state_matcher.py +592 -0
- mcp_ticketer/core/url_parser.py +425 -0
- mcp_ticketer/core/validators.py +69 -0
- mcp_ticketer/defaults/ticket_instructions.md +644 -0
- mcp_ticketer/mcp/__init__.py +29 -1
- mcp_ticketer/mcp/__main__.py +60 -0
- mcp_ticketer/mcp/server/__init__.py +25 -0
- mcp_ticketer/mcp/server/__main__.py +60 -0
- mcp_ticketer/mcp/server/constants.py +58 -0
- mcp_ticketer/mcp/server/diagnostic_helper.py +175 -0
- mcp_ticketer/mcp/server/dto.py +195 -0
- mcp_ticketer/mcp/server/main.py +1343 -0
- mcp_ticketer/mcp/server/response_builder.py +206 -0
- mcp_ticketer/mcp/server/routing.py +723 -0
- mcp_ticketer/mcp/server/server_sdk.py +151 -0
- mcp_ticketer/mcp/server/tools/__init__.py +69 -0
- mcp_ticketer/mcp/server/tools/analysis_tools.py +854 -0
- mcp_ticketer/mcp/server/tools/attachment_tools.py +224 -0
- mcp_ticketer/mcp/server/tools/bulk_tools.py +330 -0
- mcp_ticketer/mcp/server/tools/comment_tools.py +152 -0
- mcp_ticketer/mcp/server/tools/config_tools.py +1564 -0
- mcp_ticketer/mcp/server/tools/diagnostic_tools.py +211 -0
- mcp_ticketer/mcp/server/tools/hierarchy_tools.py +942 -0
- mcp_ticketer/mcp/server/tools/instruction_tools.py +295 -0
- mcp_ticketer/mcp/server/tools/label_tools.py +942 -0
- mcp_ticketer/mcp/server/tools/milestone_tools.py +338 -0
- mcp_ticketer/mcp/server/tools/pr_tools.py +150 -0
- mcp_ticketer/mcp/server/tools/project_status_tools.py +158 -0
- mcp_ticketer/mcp/server/tools/project_update_tools.py +473 -0
- mcp_ticketer/mcp/server/tools/search_tools.py +318 -0
- mcp_ticketer/mcp/server/tools/session_tools.py +308 -0
- mcp_ticketer/mcp/server/tools/ticket_tools.py +1413 -0
- mcp_ticketer/mcp/server/tools/user_ticket_tools.py +364 -0
- mcp_ticketer/queue/__init__.py +1 -0
- mcp_ticketer/queue/health_monitor.py +168 -136
- mcp_ticketer/queue/manager.py +78 -63
- mcp_ticketer/queue/queue.py +108 -21
- mcp_ticketer/queue/run_worker.py +2 -2
- mcp_ticketer/queue/ticket_registry.py +213 -155
- mcp_ticketer/queue/worker.py +96 -58
- mcp_ticketer/utils/__init__.py +5 -0
- mcp_ticketer/utils/token_utils.py +246 -0
- mcp_ticketer-2.2.9.dist-info/METADATA +1396 -0
- mcp_ticketer-2.2.9.dist-info/RECORD +158 -0
- mcp_ticketer-2.2.9.dist-info/top_level.txt +2 -0
- py_mcp_installer/examples/phase3_demo.py +178 -0
- py_mcp_installer/scripts/manage_version.py +54 -0
- py_mcp_installer/setup.py +6 -0
- py_mcp_installer/src/py_mcp_installer/__init__.py +153 -0
- py_mcp_installer/src/py_mcp_installer/command_builder.py +445 -0
- py_mcp_installer/src/py_mcp_installer/config_manager.py +541 -0
- py_mcp_installer/src/py_mcp_installer/exceptions.py +243 -0
- py_mcp_installer/src/py_mcp_installer/installation_strategy.py +617 -0
- py_mcp_installer/src/py_mcp_installer/installer.py +656 -0
- py_mcp_installer/src/py_mcp_installer/mcp_inspector.py +750 -0
- py_mcp_installer/src/py_mcp_installer/platform_detector.py +451 -0
- py_mcp_installer/src/py_mcp_installer/platforms/__init__.py +26 -0
- py_mcp_installer/src/py_mcp_installer/platforms/claude_code.py +225 -0
- py_mcp_installer/src/py_mcp_installer/platforms/codex.py +181 -0
- py_mcp_installer/src/py_mcp_installer/platforms/cursor.py +191 -0
- py_mcp_installer/src/py_mcp_installer/types.py +222 -0
- py_mcp_installer/src/py_mcp_installer/utils.py +463 -0
- py_mcp_installer/tests/__init__.py +0 -0
- py_mcp_installer/tests/platforms/__init__.py +0 -0
- py_mcp_installer/tests/test_platform_detector.py +17 -0
- mcp_ticketer/adapters/github.py +0 -1354
- mcp_ticketer/adapters/jira.py +0 -1011
- mcp_ticketer/mcp/server.py +0 -2030
- mcp_ticketer-0.3.0.dist-info/METADATA +0 -414
- mcp_ticketer-0.3.0.dist-info/RECORD +0 -59
- mcp_ticketer-0.3.0.dist-info/top_level.txt +0 -1
- {mcp_ticketer-0.3.0.dist-info → mcp_ticketer-2.2.9.dist-info}/WHEEL +0 -0
- {mcp_ticketer-0.3.0.dist-info → mcp_ticketer-2.2.9.dist-info}/entry_points.txt +0 -0
- {mcp_ticketer-0.3.0.dist-info → mcp_ticketer-2.2.9.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
"""Semantic priority matcher for natural language ticket priority inputs.
|
|
2
|
+
|
|
3
|
+
This module provides intelligent priority matching that accepts natural language inputs
|
|
4
|
+
and resolves them to universal Priority values with confidence scoring.
|
|
5
|
+
|
|
6
|
+
Features:
|
|
7
|
+
- Comprehensive synonym dictionary (20+ synonyms per priority)
|
|
8
|
+
- Multi-stage matching pipeline (exact → synonym → fuzzy)
|
|
9
|
+
- Confidence scoring with thresholds
|
|
10
|
+
- Support for all 4 universal priorities
|
|
11
|
+
- Natural language understanding
|
|
12
|
+
|
|
13
|
+
Design Decision: Multi-Stage Matching Pipeline
|
|
14
|
+
----------------------------------------------
|
|
15
|
+
The matcher uses a cascading approach to maximize accuracy while maintaining
|
|
16
|
+
flexibility:
|
|
17
|
+
|
|
18
|
+
1. Exact Match: Direct priority name match (confidence: 1.0)
|
|
19
|
+
2. Synonym Match: Pre-defined synonym lookup (confidence: 0.95)
|
|
20
|
+
3. Fuzzy Match: Levenshtein distance with thresholds (confidence: 0.70-0.95)
|
|
21
|
+
|
|
22
|
+
This approach ensures high confidence for common inputs while gracefully handling
|
|
23
|
+
typos and variations.
|
|
24
|
+
|
|
25
|
+
Performance Considerations:
|
|
26
|
+
- Average match time: <5ms (target: <10ms)
|
|
27
|
+
- Synonym lookup: O(1) with dict hashing
|
|
28
|
+
- Fuzzy matching: O(n) where n = number of priorities (4)
|
|
29
|
+
- Memory footprint: <500KB for matcher instance
|
|
30
|
+
|
|
31
|
+
Example:
|
|
32
|
+
>>> matcher = SemanticPriorityMatcher()
|
|
33
|
+
>>> result = matcher.match_priority("urgent")
|
|
34
|
+
>>> print(f"{result.priority.value} (confidence: {result.confidence})")
|
|
35
|
+
critical (confidence: 0.95)
|
|
36
|
+
|
|
37
|
+
>>> result = matcher.match_priority("asap")
|
|
38
|
+
>>> print(f"{result.priority.value} (confidence: {result.confidence})")
|
|
39
|
+
critical (confidence: 0.95)
|
|
40
|
+
|
|
41
|
+
>>> suggestions = matcher.suggest_priorities("important", top_n=3)
|
|
42
|
+
>>> for s in suggestions:
|
|
43
|
+
... print(f"{s.priority.value}: {s.confidence}")
|
|
44
|
+
high: 0.95
|
|
45
|
+
critical: 0.65
|
|
46
|
+
|
|
47
|
+
Ticket Reference: ISS-0002 - Add semantic priority matching for natural language inputs
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
from __future__ import annotations
|
|
51
|
+
|
|
52
|
+
from dataclasses import dataclass
|
|
53
|
+
|
|
54
|
+
try:
|
|
55
|
+
from rapidfuzz import fuzz
|
|
56
|
+
|
|
57
|
+
FUZZY_AVAILABLE = True
|
|
58
|
+
except ImportError:
|
|
59
|
+
FUZZY_AVAILABLE = False
|
|
60
|
+
|
|
61
|
+
from .models import Priority
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass
|
|
65
|
+
class PriorityMatchResult:
|
|
66
|
+
"""Result of a priority matching operation.
|
|
67
|
+
|
|
68
|
+
Attributes:
|
|
69
|
+
priority: Matched Priority
|
|
70
|
+
confidence: Confidence score (0.0-1.0)
|
|
71
|
+
match_type: Type of match used (exact, synonym, fuzzy)
|
|
72
|
+
original_input: Original user input string
|
|
73
|
+
suggestions: Alternative matches for ambiguous inputs
|
|
74
|
+
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
priority: Priority
|
|
78
|
+
confidence: float
|
|
79
|
+
match_type: str
|
|
80
|
+
original_input: str
|
|
81
|
+
suggestions: list[PriorityMatchResult] | None = None
|
|
82
|
+
|
|
83
|
+
def is_high_confidence(self) -> bool:
|
|
84
|
+
"""Check if confidence is high enough for auto-apply."""
|
|
85
|
+
return self.confidence >= 0.90
|
|
86
|
+
|
|
87
|
+
def is_medium_confidence(self) -> bool:
|
|
88
|
+
"""Check if confidence is medium (needs confirmation)."""
|
|
89
|
+
return 0.70 <= self.confidence < 0.90
|
|
90
|
+
|
|
91
|
+
def is_low_confidence(self) -> bool:
|
|
92
|
+
"""Check if confidence is too low (ambiguous)."""
|
|
93
|
+
return self.confidence < 0.70
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class SemanticPriorityMatcher:
|
|
97
|
+
"""Intelligent priority matcher with natural language support.
|
|
98
|
+
|
|
99
|
+
Provides comprehensive synonym matching, fuzzy matching, and confidence
|
|
100
|
+
scoring for ticket priority assignment.
|
|
101
|
+
|
|
102
|
+
The synonym dictionary includes 20+ synonyms across all 4 universal priorities,
|
|
103
|
+
covering common variations, typos, and platform-specific terminology.
|
|
104
|
+
|
|
105
|
+
Ticket Reference: ISS-0002
|
|
106
|
+
"""
|
|
107
|
+
|
|
108
|
+
# Comprehensive synonym dictionary for all universal priorities
|
|
109
|
+
PRIORITY_SYNONYMS: dict[Priority, list[str]] = {
|
|
110
|
+
Priority.CRITICAL: [
|
|
111
|
+
"critical",
|
|
112
|
+
"urgent",
|
|
113
|
+
"asap",
|
|
114
|
+
"as soon as possible",
|
|
115
|
+
"emergency",
|
|
116
|
+
"blocker",
|
|
117
|
+
"blocking",
|
|
118
|
+
"show stopper",
|
|
119
|
+
"show-stopper",
|
|
120
|
+
"showstopper",
|
|
121
|
+
"highest",
|
|
122
|
+
"p0",
|
|
123
|
+
"p-0",
|
|
124
|
+
"priority 0",
|
|
125
|
+
"needs immediate attention",
|
|
126
|
+
"immediate attention",
|
|
127
|
+
"very urgent",
|
|
128
|
+
"right now",
|
|
129
|
+
"drop everything",
|
|
130
|
+
"top priority",
|
|
131
|
+
"mission critical",
|
|
132
|
+
"business critical",
|
|
133
|
+
"sev 0",
|
|
134
|
+
"sev0",
|
|
135
|
+
"severity 0",
|
|
136
|
+
"must have",
|
|
137
|
+
],
|
|
138
|
+
Priority.HIGH: [
|
|
139
|
+
"high",
|
|
140
|
+
"important",
|
|
141
|
+
"soon",
|
|
142
|
+
"needs attention",
|
|
143
|
+
"p1",
|
|
144
|
+
"p-1",
|
|
145
|
+
"priority 1",
|
|
146
|
+
"high priority",
|
|
147
|
+
"should do",
|
|
148
|
+
"should have",
|
|
149
|
+
"significant",
|
|
150
|
+
"pressing",
|
|
151
|
+
"time sensitive",
|
|
152
|
+
"time-sensitive",
|
|
153
|
+
"sev 1",
|
|
154
|
+
"sev1",
|
|
155
|
+
"severity 1",
|
|
156
|
+
"major",
|
|
157
|
+
"escalated",
|
|
158
|
+
"higher",
|
|
159
|
+
"elevated",
|
|
160
|
+
],
|
|
161
|
+
Priority.MEDIUM: [
|
|
162
|
+
"medium",
|
|
163
|
+
"normal",
|
|
164
|
+
"standard",
|
|
165
|
+
"regular",
|
|
166
|
+
"moderate",
|
|
167
|
+
"average",
|
|
168
|
+
"default",
|
|
169
|
+
"typical",
|
|
170
|
+
"p2",
|
|
171
|
+
"p-2",
|
|
172
|
+
"priority 2",
|
|
173
|
+
"medium priority",
|
|
174
|
+
"could have",
|
|
175
|
+
"sev 2",
|
|
176
|
+
"sev2",
|
|
177
|
+
"severity 2",
|
|
178
|
+
"routine",
|
|
179
|
+
"ordinary",
|
|
180
|
+
],
|
|
181
|
+
Priority.LOW: [
|
|
182
|
+
"low",
|
|
183
|
+
"minor",
|
|
184
|
+
"whenever",
|
|
185
|
+
"low priority",
|
|
186
|
+
"not urgent",
|
|
187
|
+
"nice to have",
|
|
188
|
+
"nice-to-have",
|
|
189
|
+
"backlog",
|
|
190
|
+
"someday",
|
|
191
|
+
"if time permits",
|
|
192
|
+
"when possible",
|
|
193
|
+
"optional",
|
|
194
|
+
"can wait",
|
|
195
|
+
"lowest",
|
|
196
|
+
"p3",
|
|
197
|
+
"p-3",
|
|
198
|
+
"priority 3",
|
|
199
|
+
"trivial",
|
|
200
|
+
"cosmetic",
|
|
201
|
+
"sev 3",
|
|
202
|
+
"sev3",
|
|
203
|
+
"severity 3",
|
|
204
|
+
"won't have",
|
|
205
|
+
"wont have",
|
|
206
|
+
],
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
# Confidence thresholds
|
|
210
|
+
CONFIDENCE_HIGH = 0.90
|
|
211
|
+
CONFIDENCE_MEDIUM = 0.70
|
|
212
|
+
FUZZY_THRESHOLD_HIGH = 90
|
|
213
|
+
FUZZY_THRESHOLD_MEDIUM = 70
|
|
214
|
+
|
|
215
|
+
def __init__(self) -> None:
|
|
216
|
+
"""Initialize the semantic priority matcher.
|
|
217
|
+
|
|
218
|
+
Creates reverse lookup dictionary for O(1) synonym matching.
|
|
219
|
+
"""
|
|
220
|
+
# Build reverse lookup: synonym -> (priority, is_exact)
|
|
221
|
+
self._synonym_to_priority: dict[str, tuple[Priority, bool]] = {}
|
|
222
|
+
|
|
223
|
+
for priority in Priority:
|
|
224
|
+
# Add exact priority value
|
|
225
|
+
self._synonym_to_priority[priority.value.lower()] = (priority, True)
|
|
226
|
+
|
|
227
|
+
# Add all synonyms
|
|
228
|
+
for synonym in self.PRIORITY_SYNONYMS.get(priority, []):
|
|
229
|
+
self._synonym_to_priority[synonym.lower()] = (priority, False)
|
|
230
|
+
|
|
231
|
+
def match_priority(
|
|
232
|
+
self,
|
|
233
|
+
user_input: str,
|
|
234
|
+
adapter_priorities: list[str] | None = None,
|
|
235
|
+
) -> PriorityMatchResult:
|
|
236
|
+
"""Match user input to universal priority with confidence score.
|
|
237
|
+
|
|
238
|
+
Uses multi-stage matching pipeline:
|
|
239
|
+
1. Exact match against priority values
|
|
240
|
+
2. Synonym lookup
|
|
241
|
+
3. Fuzzy matching with Levenshtein distance
|
|
242
|
+
|
|
243
|
+
Args:
|
|
244
|
+
user_input: Natural language priority input from user
|
|
245
|
+
adapter_priorities: Optional list of adapter-specific priority names
|
|
246
|
+
|
|
247
|
+
Returns:
|
|
248
|
+
PriorityMatchResult with matched priority and confidence score
|
|
249
|
+
|
|
250
|
+
Example:
|
|
251
|
+
>>> matcher = SemanticPriorityMatcher()
|
|
252
|
+
>>> result = matcher.match_priority("urgent")
|
|
253
|
+
>>> print(f"{result.priority.value}: {result.confidence}")
|
|
254
|
+
critical: 0.95
|
|
255
|
+
|
|
256
|
+
>>> result = matcher.match_priority("criticl") # typo
|
|
257
|
+
>>> print(f"{result.priority.value}: {result.confidence}")
|
|
258
|
+
critical: 0.85
|
|
259
|
+
|
|
260
|
+
Ticket Reference: ISS-0002
|
|
261
|
+
"""
|
|
262
|
+
if not user_input:
|
|
263
|
+
# Default to MEDIUM for empty input
|
|
264
|
+
return PriorityMatchResult(
|
|
265
|
+
priority=Priority.MEDIUM,
|
|
266
|
+
confidence=0.5,
|
|
267
|
+
match_type="default",
|
|
268
|
+
original_input=user_input,
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
# Normalize input
|
|
272
|
+
normalized = user_input.strip().lower()
|
|
273
|
+
|
|
274
|
+
# Handle whitespace-only input (after normalization)
|
|
275
|
+
if not normalized:
|
|
276
|
+
return PriorityMatchResult(
|
|
277
|
+
priority=Priority.MEDIUM,
|
|
278
|
+
confidence=0.5,
|
|
279
|
+
match_type="default",
|
|
280
|
+
original_input=user_input,
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
# Stage 1: Exact match
|
|
284
|
+
exact_result = self._exact_match(normalized)
|
|
285
|
+
if exact_result:
|
|
286
|
+
return exact_result
|
|
287
|
+
|
|
288
|
+
# Stage 2: Synonym match
|
|
289
|
+
synonym_result = self._synonym_match(normalized)
|
|
290
|
+
if synonym_result:
|
|
291
|
+
return synonym_result
|
|
292
|
+
|
|
293
|
+
# Stage 3: Fuzzy match
|
|
294
|
+
fuzzy_result = self._fuzzy_match(normalized)
|
|
295
|
+
if fuzzy_result:
|
|
296
|
+
return fuzzy_result
|
|
297
|
+
|
|
298
|
+
# No good match found - return suggestions
|
|
299
|
+
suggestions = self.suggest_priorities(user_input, top_n=3)
|
|
300
|
+
return PriorityMatchResult(
|
|
301
|
+
priority=suggestions[0].priority if suggestions else Priority.MEDIUM,
|
|
302
|
+
confidence=suggestions[0].confidence if suggestions else 0.5,
|
|
303
|
+
match_type="fallback",
|
|
304
|
+
original_input=user_input,
|
|
305
|
+
suggestions=suggestions,
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
def suggest_priorities(
|
|
309
|
+
self,
|
|
310
|
+
user_input: str,
|
|
311
|
+
top_n: int = 3,
|
|
312
|
+
) -> list[PriorityMatchResult]:
|
|
313
|
+
"""Return top N priority suggestions for ambiguous inputs.
|
|
314
|
+
|
|
315
|
+
Uses fuzzy matching to rank all possible priorities by similarity.
|
|
316
|
+
Useful for providing user with multiple options when confidence is low.
|
|
317
|
+
|
|
318
|
+
Args:
|
|
319
|
+
user_input: Natural language priority input
|
|
320
|
+
top_n: Number of suggestions to return (default: 3)
|
|
321
|
+
|
|
322
|
+
Returns:
|
|
323
|
+
List of PriorityMatchResult sorted by confidence (highest first)
|
|
324
|
+
|
|
325
|
+
Example:
|
|
326
|
+
>>> matcher = SemanticPriorityMatcher()
|
|
327
|
+
>>> suggestions = matcher.suggest_priorities("importnt", top_n=3)
|
|
328
|
+
>>> for s in suggestions:
|
|
329
|
+
... print(f"{s.priority.value}: {s.confidence:.2f}")
|
|
330
|
+
high: 0.85
|
|
331
|
+
medium: 0.45
|
|
332
|
+
critical: 0.42
|
|
333
|
+
|
|
334
|
+
Ticket Reference: ISS-0002
|
|
335
|
+
"""
|
|
336
|
+
if not FUZZY_AVAILABLE:
|
|
337
|
+
# Without fuzzy matching, return all priorities with low confidence
|
|
338
|
+
return [
|
|
339
|
+
PriorityMatchResult(
|
|
340
|
+
priority=priority,
|
|
341
|
+
confidence=0.5,
|
|
342
|
+
match_type="suggestion",
|
|
343
|
+
original_input=user_input,
|
|
344
|
+
)
|
|
345
|
+
for priority in Priority
|
|
346
|
+
][:top_n]
|
|
347
|
+
|
|
348
|
+
normalized = user_input.strip().lower()
|
|
349
|
+
suggestions: list[tuple[Priority, float, str]] = []
|
|
350
|
+
|
|
351
|
+
# Calculate similarity for each priority and its synonyms
|
|
352
|
+
for priority in Priority:
|
|
353
|
+
# Check priority value
|
|
354
|
+
priority_similarity = fuzz.ratio(normalized, priority.value.lower())
|
|
355
|
+
max_similarity = priority_similarity
|
|
356
|
+
match_text = priority.value
|
|
357
|
+
|
|
358
|
+
# Check synonyms
|
|
359
|
+
for synonym in self.PRIORITY_SYNONYMS.get(priority, []):
|
|
360
|
+
similarity = fuzz.ratio(normalized, synonym.lower())
|
|
361
|
+
if similarity > max_similarity:
|
|
362
|
+
max_similarity = similarity
|
|
363
|
+
match_text = synonym
|
|
364
|
+
|
|
365
|
+
# Convert similarity to confidence (0-100 → 0.0-1.0)
|
|
366
|
+
confidence = max_similarity / 100.0
|
|
367
|
+
suggestions.append((priority, confidence, match_text))
|
|
368
|
+
|
|
369
|
+
# Sort by confidence descending
|
|
370
|
+
suggestions.sort(key=lambda x: x[1], reverse=True)
|
|
371
|
+
|
|
372
|
+
# Convert to PriorityMatchResult
|
|
373
|
+
return [
|
|
374
|
+
PriorityMatchResult(
|
|
375
|
+
priority=priority,
|
|
376
|
+
confidence=conf,
|
|
377
|
+
match_type="suggestion",
|
|
378
|
+
original_input=user_input,
|
|
379
|
+
)
|
|
380
|
+
for priority, conf, _ in suggestions[:top_n]
|
|
381
|
+
]
|
|
382
|
+
|
|
383
|
+
def _exact_match(self, normalized_input: str) -> PriorityMatchResult | None:
|
|
384
|
+
"""Match exact priority value."""
|
|
385
|
+
for priority in Priority:
|
|
386
|
+
if normalized_input == priority.value.lower():
|
|
387
|
+
return PriorityMatchResult(
|
|
388
|
+
priority=priority,
|
|
389
|
+
confidence=1.0,
|
|
390
|
+
match_type="exact",
|
|
391
|
+
original_input=normalized_input,
|
|
392
|
+
)
|
|
393
|
+
return None
|
|
394
|
+
|
|
395
|
+
def _synonym_match(self, normalized_input: str) -> PriorityMatchResult | None:
|
|
396
|
+
"""Match using synonym dictionary."""
|
|
397
|
+
if normalized_input in self._synonym_to_priority:
|
|
398
|
+
priority, is_exact = self._synonym_to_priority[normalized_input]
|
|
399
|
+
return PriorityMatchResult(
|
|
400
|
+
priority=priority,
|
|
401
|
+
confidence=1.0 if is_exact else 0.95,
|
|
402
|
+
match_type="exact" if is_exact else "synonym",
|
|
403
|
+
original_input=normalized_input,
|
|
404
|
+
)
|
|
405
|
+
return None
|
|
406
|
+
|
|
407
|
+
def _fuzzy_match(self, normalized_input: str) -> PriorityMatchResult | None:
|
|
408
|
+
"""Match using fuzzy string matching."""
|
|
409
|
+
if not FUZZY_AVAILABLE:
|
|
410
|
+
return None
|
|
411
|
+
|
|
412
|
+
best_match: tuple[Priority, float, str] | None = None
|
|
413
|
+
|
|
414
|
+
for priority in Priority:
|
|
415
|
+
# Check priority value
|
|
416
|
+
priority_similarity = fuzz.ratio(normalized_input, priority.value.lower())
|
|
417
|
+
|
|
418
|
+
if priority_similarity >= self.FUZZY_THRESHOLD_MEDIUM:
|
|
419
|
+
if best_match is None or priority_similarity > best_match[1]:
|
|
420
|
+
best_match = (priority, priority_similarity, "priority_value")
|
|
421
|
+
|
|
422
|
+
# Check synonyms
|
|
423
|
+
for synonym in self.PRIORITY_SYNONYMS.get(priority, []):
|
|
424
|
+
similarity = fuzz.ratio(normalized_input, synonym.lower())
|
|
425
|
+
if similarity >= self.FUZZY_THRESHOLD_MEDIUM:
|
|
426
|
+
if best_match is None or similarity > best_match[1]:
|
|
427
|
+
best_match = (priority, similarity, "synonym")
|
|
428
|
+
|
|
429
|
+
if best_match:
|
|
430
|
+
priority, similarity, match_source = best_match
|
|
431
|
+
|
|
432
|
+
# Calculate confidence based on similarity
|
|
433
|
+
if similarity >= self.FUZZY_THRESHOLD_HIGH:
|
|
434
|
+
confidence = 0.85 + (similarity - self.FUZZY_THRESHOLD_HIGH) / 100.0
|
|
435
|
+
else:
|
|
436
|
+
confidence = 0.70 + (similarity - self.FUZZY_THRESHOLD_MEDIUM) / 200.0
|
|
437
|
+
|
|
438
|
+
return PriorityMatchResult(
|
|
439
|
+
priority=priority,
|
|
440
|
+
confidence=min(confidence, 0.95), # Cap at 0.95
|
|
441
|
+
match_type="fuzzy",
|
|
442
|
+
original_input=normalized_input,
|
|
443
|
+
)
|
|
444
|
+
|
|
445
|
+
return None
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
# Singleton instance for convenience
|
|
449
|
+
_default_matcher: SemanticPriorityMatcher | None = None
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
def get_priority_matcher() -> SemanticPriorityMatcher:
|
|
453
|
+
"""Get the default priority matcher instance.
|
|
454
|
+
|
|
455
|
+
Returns:
|
|
456
|
+
Singleton SemanticPriorityMatcher instance
|
|
457
|
+
|
|
458
|
+
Ticket Reference: ISS-0002
|
|
459
|
+
"""
|
|
460
|
+
global _default_matcher
|
|
461
|
+
if _default_matcher is None:
|
|
462
|
+
_default_matcher = SemanticPriorityMatcher()
|
|
463
|
+
return _default_matcher
|