mcp-ticketer 0.4.11__py3-none-any.whl → 2.0.1__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.
Potentially problematic release.
This version of mcp-ticketer might be problematic. Click here for more details.
- mcp_ticketer/__init__.py +10 -10
- mcp_ticketer/__version__.py +3 -3
- mcp_ticketer/adapters/__init__.py +2 -0
- mcp_ticketer/adapters/aitrackdown.py +394 -9
- mcp_ticketer/adapters/asana/__init__.py +15 -0
- mcp_ticketer/adapters/asana/adapter.py +1416 -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.py +836 -105
- mcp_ticketer/adapters/hybrid.py +47 -5
- mcp_ticketer/adapters/jira.py +772 -1
- mcp_ticketer/adapters/linear/adapter.py +2293 -108
- mcp_ticketer/adapters/linear/client.py +146 -12
- mcp_ticketer/adapters/linear/mappers.py +105 -11
- mcp_ticketer/adapters/linear/queries.py +168 -1
- mcp_ticketer/adapters/linear/types.py +80 -4
- 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 +3 -3
- mcp_ticketer/cli/adapter_diagnostics.py +4 -2
- mcp_ticketer/cli/auggie_configure.py +18 -6
- mcp_ticketer/cli/codex_configure.py +175 -60
- mcp_ticketer/cli/configure.py +884 -146
- mcp_ticketer/cli/cursor_configure.py +314 -0
- mcp_ticketer/cli/diagnostics.py +31 -28
- mcp_ticketer/cli/discover.py +293 -21
- mcp_ticketer/cli/gemini_configure.py +18 -6
- mcp_ticketer/cli/init_command.py +880 -0
- mcp_ticketer/cli/instruction_commands.py +435 -0
- mcp_ticketer/cli/linear_commands.py +99 -15
- mcp_ticketer/cli/main.py +109 -2055
- mcp_ticketer/cli/mcp_configure.py +673 -99
- mcp_ticketer/cli/mcp_server_commands.py +415 -0
- mcp_ticketer/cli/migrate_config.py +12 -8
- mcp_ticketer/cli/platform_commands.py +6 -6
- mcp_ticketer/cli/platform_detection.py +477 -0
- mcp_ticketer/cli/platform_installer.py +536 -0
- mcp_ticketer/cli/project_update_commands.py +350 -0
- mcp_ticketer/cli/queue_commands.py +15 -15
- mcp_ticketer/cli/setup_command.py +639 -0
- mcp_ticketer/cli/simple_health.py +13 -11
- mcp_ticketer/cli/ticket_commands.py +277 -36
- mcp_ticketer/cli/update_checker.py +313 -0
- mcp_ticketer/cli/utils.py +45 -41
- mcp_ticketer/core/__init__.py +35 -1
- mcp_ticketer/core/adapter.py +170 -5
- mcp_ticketer/core/config.py +38 -31
- mcp_ticketer/core/env_discovery.py +33 -3
- mcp_ticketer/core/env_loader.py +7 -6
- mcp_ticketer/core/exceptions.py +10 -4
- mcp_ticketer/core/http_client.py +10 -10
- mcp_ticketer/core/instructions.py +405 -0
- mcp_ticketer/core/label_manager.py +732 -0
- mcp_ticketer/core/mappers.py +32 -20
- mcp_ticketer/core/models.py +136 -1
- mcp_ticketer/core/onepassword_secrets.py +379 -0
- mcp_ticketer/core/priority_matcher.py +463 -0
- mcp_ticketer/core/project_config.py +148 -14
- mcp_ticketer/core/registry.py +1 -1
- mcp_ticketer/core/session_state.py +171 -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 +2 -2
- mcp_ticketer/mcp/server/__init__.py +2 -2
- mcp_ticketer/mcp/server/diagnostic_helper.py +175 -0
- mcp_ticketer/mcp/server/main.py +187 -93
- mcp_ticketer/mcp/server/routing.py +655 -0
- mcp_ticketer/mcp/server/server_sdk.py +58 -0
- mcp_ticketer/mcp/server/tools/__init__.py +37 -9
- mcp_ticketer/mcp/server/tools/analysis_tools.py +854 -0
- mcp_ticketer/mcp/server/tools/attachment_tools.py +65 -20
- mcp_ticketer/mcp/server/tools/bulk_tools.py +259 -202
- mcp_ticketer/mcp/server/tools/comment_tools.py +74 -12
- mcp_ticketer/mcp/server/tools/config_tools.py +1429 -0
- mcp_ticketer/mcp/server/tools/diagnostic_tools.py +211 -0
- mcp_ticketer/mcp/server/tools/hierarchy_tools.py +878 -319
- 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/pr_tools.py +3 -7
- 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 +180 -97
- mcp_ticketer/mcp/server/tools/session_tools.py +308 -0
- mcp_ticketer/mcp/server/tools/ticket_tools.py +1182 -82
- mcp_ticketer/mcp/server/tools/user_ticket_tools.py +364 -0
- mcp_ticketer/queue/health_monitor.py +1 -0
- mcp_ticketer/queue/manager.py +4 -4
- mcp_ticketer/queue/queue.py +3 -3
- mcp_ticketer/queue/run_worker.py +1 -1
- mcp_ticketer/queue/ticket_registry.py +2 -2
- mcp_ticketer/queue/worker.py +15 -13
- mcp_ticketer/utils/__init__.py +5 -0
- mcp_ticketer/utils/token_utils.py +246 -0
- mcp_ticketer-2.0.1.dist-info/METADATA +1366 -0
- mcp_ticketer-2.0.1.dist-info/RECORD +122 -0
- mcp_ticketer-0.4.11.dist-info/METADATA +0 -496
- mcp_ticketer-0.4.11.dist-info/RECORD +0 -77
- {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-2.0.1.dist-info}/WHEEL +0 -0
- {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-2.0.1.dist-info}/entry_points.txt +0 -0
- {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-2.0.1.dist-info}/licenses/LICENSE +0 -0
- {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-2.0.1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,592 @@
|
|
|
1
|
+
"""Semantic state matcher for natural language ticket state transitions.
|
|
2
|
+
|
|
3
|
+
This module provides intelligent state matching that accepts natural language inputs
|
|
4
|
+
and resolves them to universal TicketState values with confidence scoring.
|
|
5
|
+
|
|
6
|
+
Features:
|
|
7
|
+
- Comprehensive synonym dictionary (50+ synonyms per state)
|
|
8
|
+
- Multi-stage matching pipeline (exact → synonym → fuzzy → adapter)
|
|
9
|
+
- Confidence scoring with thresholds
|
|
10
|
+
- Support for all 8 universal states
|
|
11
|
+
- Adapter-specific state resolution
|
|
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 state 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
|
+
4. Adapter Match: Optional adapter-specific state names (confidence: 0.90)
|
|
22
|
+
|
|
23
|
+
This approach ensures high confidence for common inputs while gracefully handling
|
|
24
|
+
typos and variations.
|
|
25
|
+
|
|
26
|
+
Performance Considerations:
|
|
27
|
+
- Average match time: <5ms (target: <10ms)
|
|
28
|
+
- Synonym lookup: O(1) with dict hashing
|
|
29
|
+
- Fuzzy matching: O(n) where n = number of states (8)
|
|
30
|
+
- Memory footprint: <1MB for matcher instance
|
|
31
|
+
|
|
32
|
+
Example:
|
|
33
|
+
>>> matcher = SemanticStateMatcher()
|
|
34
|
+
>>> result = matcher.match_state("working on it")
|
|
35
|
+
>>> print(f"{result.state.value} (confidence: {result.confidence})")
|
|
36
|
+
in_progress (confidence: 0.95)
|
|
37
|
+
|
|
38
|
+
>>> suggestions = matcher.suggest_states("review", top_n=3)
|
|
39
|
+
>>> for s in suggestions:
|
|
40
|
+
... print(f"{s.state.value}: {s.confidence}")
|
|
41
|
+
ready: 0.95
|
|
42
|
+
tested: 0.75
|
|
43
|
+
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
from __future__ import annotations
|
|
47
|
+
|
|
48
|
+
from dataclasses import dataclass
|
|
49
|
+
|
|
50
|
+
try:
|
|
51
|
+
from rapidfuzz import fuzz
|
|
52
|
+
|
|
53
|
+
FUZZY_AVAILABLE = True
|
|
54
|
+
except ImportError:
|
|
55
|
+
FUZZY_AVAILABLE = False
|
|
56
|
+
|
|
57
|
+
from .models import TicketState
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass
|
|
61
|
+
class StateMatchResult:
|
|
62
|
+
"""Result of a state matching operation.
|
|
63
|
+
|
|
64
|
+
Attributes:
|
|
65
|
+
state: Matched TicketState
|
|
66
|
+
confidence: Confidence score (0.0-1.0)
|
|
67
|
+
match_type: Type of match used (exact, synonym, fuzzy, adapter)
|
|
68
|
+
original_input: Original user input string
|
|
69
|
+
suggestions: Alternative matches for ambiguous inputs
|
|
70
|
+
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
state: TicketState
|
|
74
|
+
confidence: float
|
|
75
|
+
match_type: str
|
|
76
|
+
original_input: str
|
|
77
|
+
suggestions: list[StateMatchResult] | None = None
|
|
78
|
+
|
|
79
|
+
def is_high_confidence(self) -> bool:
|
|
80
|
+
"""Check if confidence is high enough for auto-apply."""
|
|
81
|
+
return self.confidence >= 0.90
|
|
82
|
+
|
|
83
|
+
def is_medium_confidence(self) -> bool:
|
|
84
|
+
"""Check if confidence is medium (needs confirmation)."""
|
|
85
|
+
return 0.70 <= self.confidence < 0.90
|
|
86
|
+
|
|
87
|
+
def is_low_confidence(self) -> bool:
|
|
88
|
+
"""Check if confidence is too low (ambiguous)."""
|
|
89
|
+
return self.confidence < 0.70
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@dataclass
|
|
93
|
+
class ValidationResult:
|
|
94
|
+
"""Result of a state transition validation.
|
|
95
|
+
|
|
96
|
+
Attributes:
|
|
97
|
+
is_valid: Whether the transition is allowed
|
|
98
|
+
match_result: State matching result for target state
|
|
99
|
+
current_state: Current ticket state
|
|
100
|
+
error_message: Error message if invalid
|
|
101
|
+
valid_transitions: List of valid target states
|
|
102
|
+
|
|
103
|
+
"""
|
|
104
|
+
|
|
105
|
+
is_valid: bool
|
|
106
|
+
match_result: StateMatchResult | None
|
|
107
|
+
current_state: TicketState
|
|
108
|
+
error_message: str | None = None
|
|
109
|
+
valid_transitions: list[TicketState] | None = None
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class SemanticStateMatcher:
|
|
113
|
+
"""Intelligent state matcher with natural language support.
|
|
114
|
+
|
|
115
|
+
Provides comprehensive synonym matching, fuzzy matching, and confidence
|
|
116
|
+
scoring for ticket state transitions.
|
|
117
|
+
|
|
118
|
+
The synonym dictionary includes 50+ synonyms across all 8 universal states,
|
|
119
|
+
covering common variations, typos, and platform-specific terminology.
|
|
120
|
+
"""
|
|
121
|
+
|
|
122
|
+
# Comprehensive synonym dictionary for all universal states
|
|
123
|
+
STATE_SYNONYMS: dict[TicketState, list[str]] = {
|
|
124
|
+
TicketState.OPEN: [
|
|
125
|
+
"open",
|
|
126
|
+
"todo",
|
|
127
|
+
"to do",
|
|
128
|
+
"to-do",
|
|
129
|
+
"backlog",
|
|
130
|
+
"new",
|
|
131
|
+
"pending",
|
|
132
|
+
"queued",
|
|
133
|
+
"unstarted",
|
|
134
|
+
"not started",
|
|
135
|
+
"not-started",
|
|
136
|
+
"planned",
|
|
137
|
+
"triage",
|
|
138
|
+
"inbox",
|
|
139
|
+
],
|
|
140
|
+
TicketState.IN_PROGRESS: [
|
|
141
|
+
"in_progress",
|
|
142
|
+
"in progress",
|
|
143
|
+
"in-progress",
|
|
144
|
+
"working",
|
|
145
|
+
"started",
|
|
146
|
+
"active",
|
|
147
|
+
"doing",
|
|
148
|
+
"in development",
|
|
149
|
+
"in-development",
|
|
150
|
+
"in dev",
|
|
151
|
+
"wip",
|
|
152
|
+
"work in progress",
|
|
153
|
+
"working on it",
|
|
154
|
+
"in flight",
|
|
155
|
+
"in-flight",
|
|
156
|
+
"ongoing",
|
|
157
|
+
],
|
|
158
|
+
TicketState.READY: [
|
|
159
|
+
"ready",
|
|
160
|
+
"review",
|
|
161
|
+
"needs review",
|
|
162
|
+
"needs-review",
|
|
163
|
+
"pr ready",
|
|
164
|
+
"pr-ready",
|
|
165
|
+
"code review",
|
|
166
|
+
"code-review",
|
|
167
|
+
"done dev",
|
|
168
|
+
"done-dev",
|
|
169
|
+
"dev done",
|
|
170
|
+
"dev-done",
|
|
171
|
+
"qa ready",
|
|
172
|
+
"qa-ready",
|
|
173
|
+
"ready for review",
|
|
174
|
+
"ready for testing",
|
|
175
|
+
"ready-for-review",
|
|
176
|
+
"awaiting review",
|
|
177
|
+
],
|
|
178
|
+
TicketState.TESTED: [
|
|
179
|
+
"tested",
|
|
180
|
+
"qa done",
|
|
181
|
+
"qa-done",
|
|
182
|
+
"qa complete",
|
|
183
|
+
"qa-complete",
|
|
184
|
+
"qa approved",
|
|
185
|
+
"verified",
|
|
186
|
+
"passed qa",
|
|
187
|
+
"passed-qa",
|
|
188
|
+
"qa passed",
|
|
189
|
+
"qa-passed",
|
|
190
|
+
"approved",
|
|
191
|
+
"validation complete",
|
|
192
|
+
"validation-complete",
|
|
193
|
+
"testing complete",
|
|
194
|
+
"testing-complete",
|
|
195
|
+
],
|
|
196
|
+
TicketState.DONE: [
|
|
197
|
+
"done",
|
|
198
|
+
"completed",
|
|
199
|
+
"complete",
|
|
200
|
+
"finished",
|
|
201
|
+
"resolved",
|
|
202
|
+
"closed",
|
|
203
|
+
"done done",
|
|
204
|
+
"done-done",
|
|
205
|
+
"delivered",
|
|
206
|
+
"shipped",
|
|
207
|
+
"merged",
|
|
208
|
+
"deployed",
|
|
209
|
+
"released",
|
|
210
|
+
"accepted",
|
|
211
|
+
],
|
|
212
|
+
TicketState.WAITING: [
|
|
213
|
+
"waiting",
|
|
214
|
+
"on hold",
|
|
215
|
+
"on-hold",
|
|
216
|
+
"paused",
|
|
217
|
+
"waiting for",
|
|
218
|
+
"waiting-for",
|
|
219
|
+
"pending external",
|
|
220
|
+
"pending-external",
|
|
221
|
+
"deferred",
|
|
222
|
+
"stalled",
|
|
223
|
+
"awaiting",
|
|
224
|
+
"awaiting response",
|
|
225
|
+
"awaiting-response",
|
|
226
|
+
"external dependency",
|
|
227
|
+
"external-dependency",
|
|
228
|
+
],
|
|
229
|
+
TicketState.BLOCKED: [
|
|
230
|
+
"blocked",
|
|
231
|
+
"stuck",
|
|
232
|
+
"can't proceed",
|
|
233
|
+
"cannot proceed",
|
|
234
|
+
"cant proceed",
|
|
235
|
+
"impediment",
|
|
236
|
+
"blocked by",
|
|
237
|
+
"blocked-by",
|
|
238
|
+
"stopped",
|
|
239
|
+
"obstructed",
|
|
240
|
+
"blocker",
|
|
241
|
+
"blocked on",
|
|
242
|
+
"blocked-on",
|
|
243
|
+
"needs unblocking",
|
|
244
|
+
],
|
|
245
|
+
TicketState.CLOSED: [
|
|
246
|
+
"closed",
|
|
247
|
+
"archived",
|
|
248
|
+
"cancelled",
|
|
249
|
+
"canceled",
|
|
250
|
+
"won't do",
|
|
251
|
+
"wont do",
|
|
252
|
+
"won't-do",
|
|
253
|
+
"wont-do",
|
|
254
|
+
"abandoned",
|
|
255
|
+
"invalidated",
|
|
256
|
+
"rejected",
|
|
257
|
+
"obsolete",
|
|
258
|
+
"duplicate",
|
|
259
|
+
"wontfix",
|
|
260
|
+
"won't fix",
|
|
261
|
+
],
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
# Confidence thresholds
|
|
265
|
+
CONFIDENCE_HIGH = 0.90
|
|
266
|
+
CONFIDENCE_MEDIUM = 0.70
|
|
267
|
+
FUZZY_THRESHOLD_HIGH = 90
|
|
268
|
+
FUZZY_THRESHOLD_MEDIUM = 70
|
|
269
|
+
|
|
270
|
+
def __init__(self) -> None:
|
|
271
|
+
"""Initialize the semantic state matcher.
|
|
272
|
+
|
|
273
|
+
Creates reverse lookup dictionary for O(1) synonym matching.
|
|
274
|
+
"""
|
|
275
|
+
# Build reverse lookup: synonym -> (state, is_exact)
|
|
276
|
+
self._synonym_to_state: dict[str, tuple[TicketState, bool]] = {}
|
|
277
|
+
|
|
278
|
+
for state in TicketState:
|
|
279
|
+
# Add exact state value
|
|
280
|
+
self._synonym_to_state[state.value.lower()] = (state, True)
|
|
281
|
+
|
|
282
|
+
# Add all synonyms
|
|
283
|
+
for synonym in self.STATE_SYNONYMS.get(state, []):
|
|
284
|
+
self._synonym_to_state[synonym.lower()] = (state, False)
|
|
285
|
+
|
|
286
|
+
def match_state(
|
|
287
|
+
self,
|
|
288
|
+
user_input: str,
|
|
289
|
+
adapter_states: list[str] | None = None,
|
|
290
|
+
) -> StateMatchResult:
|
|
291
|
+
"""Match user input to universal state with confidence score.
|
|
292
|
+
|
|
293
|
+
Uses multi-stage matching pipeline:
|
|
294
|
+
1. Exact match against state values
|
|
295
|
+
2. Synonym lookup
|
|
296
|
+
3. Fuzzy matching with Levenshtein distance
|
|
297
|
+
4. Optional adapter-specific state matching
|
|
298
|
+
|
|
299
|
+
Args:
|
|
300
|
+
user_input: Natural language state input from user
|
|
301
|
+
adapter_states: Optional list of adapter-specific state names
|
|
302
|
+
|
|
303
|
+
Returns:
|
|
304
|
+
StateMatchResult with matched state and confidence score
|
|
305
|
+
|
|
306
|
+
Example:
|
|
307
|
+
>>> matcher = SemanticStateMatcher()
|
|
308
|
+
>>> result = matcher.match_state("working on it")
|
|
309
|
+
>>> print(f"{result.state.value}: {result.confidence}")
|
|
310
|
+
in_progress: 0.95
|
|
311
|
+
|
|
312
|
+
>>> result = matcher.match_state("reviw") # typo
|
|
313
|
+
>>> print(f"{result.state.value}: {result.confidence}")
|
|
314
|
+
ready: 0.85
|
|
315
|
+
|
|
316
|
+
"""
|
|
317
|
+
if not user_input:
|
|
318
|
+
# Default to OPEN for empty input
|
|
319
|
+
return StateMatchResult(
|
|
320
|
+
state=TicketState.OPEN,
|
|
321
|
+
confidence=0.5,
|
|
322
|
+
match_type="default",
|
|
323
|
+
original_input=user_input,
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
# Normalize input
|
|
327
|
+
normalized = user_input.strip().lower()
|
|
328
|
+
|
|
329
|
+
# Stage 1: Exact match
|
|
330
|
+
exact_result = self._exact_match(normalized)
|
|
331
|
+
if exact_result:
|
|
332
|
+
return exact_result
|
|
333
|
+
|
|
334
|
+
# Stage 2: Synonym match
|
|
335
|
+
synonym_result = self._synonym_match(normalized)
|
|
336
|
+
if synonym_result:
|
|
337
|
+
return synonym_result
|
|
338
|
+
|
|
339
|
+
# Stage 3: Adapter state match (if provided)
|
|
340
|
+
if adapter_states:
|
|
341
|
+
adapter_result = self._adapter_match(normalized, adapter_states)
|
|
342
|
+
if adapter_result:
|
|
343
|
+
return adapter_result
|
|
344
|
+
|
|
345
|
+
# Stage 4: Fuzzy match
|
|
346
|
+
fuzzy_result = self._fuzzy_match(normalized)
|
|
347
|
+
if fuzzy_result:
|
|
348
|
+
return fuzzy_result
|
|
349
|
+
|
|
350
|
+
# No good match found - return suggestions
|
|
351
|
+
suggestions = self.suggest_states(user_input, top_n=3)
|
|
352
|
+
return StateMatchResult(
|
|
353
|
+
state=suggestions[0].state if suggestions else TicketState.OPEN,
|
|
354
|
+
confidence=suggestions[0].confidence if suggestions else 0.5,
|
|
355
|
+
match_type="fallback",
|
|
356
|
+
original_input=user_input,
|
|
357
|
+
suggestions=suggestions,
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
def suggest_states(
|
|
361
|
+
self,
|
|
362
|
+
user_input: str,
|
|
363
|
+
top_n: int = 3,
|
|
364
|
+
) -> list[StateMatchResult]:
|
|
365
|
+
"""Return top N state suggestions for ambiguous inputs.
|
|
366
|
+
|
|
367
|
+
Uses fuzzy matching to rank all possible states by similarity.
|
|
368
|
+
Useful for providing user with multiple options when confidence is low.
|
|
369
|
+
|
|
370
|
+
Args:
|
|
371
|
+
user_input: Natural language state input
|
|
372
|
+
top_n: Number of suggestions to return (default: 3)
|
|
373
|
+
|
|
374
|
+
Returns:
|
|
375
|
+
List of StateMatchResult sorted by confidence (highest first)
|
|
376
|
+
|
|
377
|
+
Example:
|
|
378
|
+
>>> matcher = SemanticStateMatcher()
|
|
379
|
+
>>> suggestions = matcher.suggest_states("dne", top_n=3)
|
|
380
|
+
>>> for s in suggestions:
|
|
381
|
+
... print(f"{s.state.value}: {s.confidence:.2f}")
|
|
382
|
+
done: 0.75
|
|
383
|
+
open: 0.45
|
|
384
|
+
closed: 0.42
|
|
385
|
+
|
|
386
|
+
"""
|
|
387
|
+
if not FUZZY_AVAILABLE:
|
|
388
|
+
# Without fuzzy matching, return all states with low confidence
|
|
389
|
+
return [
|
|
390
|
+
StateMatchResult(
|
|
391
|
+
state=state,
|
|
392
|
+
confidence=0.5,
|
|
393
|
+
match_type="suggestion",
|
|
394
|
+
original_input=user_input,
|
|
395
|
+
)
|
|
396
|
+
for state in TicketState
|
|
397
|
+
][:top_n]
|
|
398
|
+
|
|
399
|
+
normalized = user_input.strip().lower()
|
|
400
|
+
suggestions: list[tuple[TicketState, float, str]] = []
|
|
401
|
+
|
|
402
|
+
# Calculate similarity for each state and its synonyms
|
|
403
|
+
for state in TicketState:
|
|
404
|
+
# Check state value
|
|
405
|
+
state_similarity = fuzz.ratio(normalized, state.value.lower())
|
|
406
|
+
max_similarity = state_similarity
|
|
407
|
+
match_text = state.value
|
|
408
|
+
|
|
409
|
+
# Check synonyms
|
|
410
|
+
for synonym in self.STATE_SYNONYMS.get(state, []):
|
|
411
|
+
similarity = fuzz.ratio(normalized, synonym.lower())
|
|
412
|
+
if similarity > max_similarity:
|
|
413
|
+
max_similarity = similarity
|
|
414
|
+
match_text = synonym
|
|
415
|
+
|
|
416
|
+
# Convert similarity to confidence (0-100 → 0.0-1.0)
|
|
417
|
+
confidence = max_similarity / 100.0
|
|
418
|
+
suggestions.append((state, confidence, match_text))
|
|
419
|
+
|
|
420
|
+
# Sort by confidence descending
|
|
421
|
+
suggestions.sort(key=lambda x: x[1], reverse=True)
|
|
422
|
+
|
|
423
|
+
# Convert to StateMatchResult
|
|
424
|
+
return [
|
|
425
|
+
StateMatchResult(
|
|
426
|
+
state=state,
|
|
427
|
+
confidence=conf,
|
|
428
|
+
match_type="suggestion",
|
|
429
|
+
original_input=user_input,
|
|
430
|
+
)
|
|
431
|
+
for state, conf, _ in suggestions[:top_n]
|
|
432
|
+
]
|
|
433
|
+
|
|
434
|
+
def validate_transition(
|
|
435
|
+
self,
|
|
436
|
+
current_state: TicketState,
|
|
437
|
+
target_input: str,
|
|
438
|
+
) -> ValidationResult:
|
|
439
|
+
"""Validate if transition is allowed and resolve target state.
|
|
440
|
+
|
|
441
|
+
Combines state matching with workflow validation to ensure the
|
|
442
|
+
transition is both semantically valid and allowed by workflow rules.
|
|
443
|
+
|
|
444
|
+
Args:
|
|
445
|
+
current_state: Current ticket state
|
|
446
|
+
target_input: Natural language target state input
|
|
447
|
+
|
|
448
|
+
Returns:
|
|
449
|
+
ValidationResult with validation status and match result
|
|
450
|
+
|
|
451
|
+
Example:
|
|
452
|
+
>>> matcher = SemanticStateMatcher()
|
|
453
|
+
>>> result = matcher.validate_transition(
|
|
454
|
+
... TicketState.OPEN,
|
|
455
|
+
... "working on it"
|
|
456
|
+
... )
|
|
457
|
+
>>> print(f"Valid: {result.is_valid}")
|
|
458
|
+
Valid: True
|
|
459
|
+
|
|
460
|
+
"""
|
|
461
|
+
# Match the target state
|
|
462
|
+
match_result = self.match_state(target_input)
|
|
463
|
+
|
|
464
|
+
# Check if transition is allowed
|
|
465
|
+
if not current_state.can_transition_to(match_result.state):
|
|
466
|
+
valid_transitions_dict = TicketState.valid_transitions()
|
|
467
|
+
# The return type annotation is dict[str, list[str]] but actually returns
|
|
468
|
+
# dict[TicketState, list[TicketState]], so we need to cast properly
|
|
469
|
+
valid_transitions_raw = valid_transitions_dict.get(current_state, [])
|
|
470
|
+
valid_transitions: list[TicketState] = [
|
|
471
|
+
s for s in valid_transitions_raw if isinstance(s, TicketState)
|
|
472
|
+
]
|
|
473
|
+
|
|
474
|
+
return ValidationResult(
|
|
475
|
+
is_valid=False,
|
|
476
|
+
match_result=match_result,
|
|
477
|
+
current_state=current_state,
|
|
478
|
+
error_message=(
|
|
479
|
+
f"Cannot transition from {current_state.value} to "
|
|
480
|
+
f"{match_result.state.value}. Valid transitions: "
|
|
481
|
+
f"{', '.join(s.value for s in valid_transitions) if valid_transitions else 'none (terminal state)'}"
|
|
482
|
+
),
|
|
483
|
+
valid_transitions=valid_transitions,
|
|
484
|
+
)
|
|
485
|
+
|
|
486
|
+
return ValidationResult(
|
|
487
|
+
is_valid=True,
|
|
488
|
+
match_result=match_result,
|
|
489
|
+
current_state=current_state,
|
|
490
|
+
)
|
|
491
|
+
|
|
492
|
+
def _exact_match(self, normalized_input: str) -> StateMatchResult | None:
|
|
493
|
+
"""Match exact state value."""
|
|
494
|
+
for state in TicketState:
|
|
495
|
+
if normalized_input == state.value.lower():
|
|
496
|
+
return StateMatchResult(
|
|
497
|
+
state=state,
|
|
498
|
+
confidence=1.0,
|
|
499
|
+
match_type="exact",
|
|
500
|
+
original_input=normalized_input,
|
|
501
|
+
)
|
|
502
|
+
return None
|
|
503
|
+
|
|
504
|
+
def _synonym_match(self, normalized_input: str) -> StateMatchResult | None:
|
|
505
|
+
"""Match using synonym dictionary."""
|
|
506
|
+
if normalized_input in self._synonym_to_state:
|
|
507
|
+
state, is_exact = self._synonym_to_state[normalized_input]
|
|
508
|
+
return StateMatchResult(
|
|
509
|
+
state=state,
|
|
510
|
+
confidence=1.0 if is_exact else 0.95,
|
|
511
|
+
match_type="exact" if is_exact else "synonym",
|
|
512
|
+
original_input=normalized_input,
|
|
513
|
+
)
|
|
514
|
+
return None
|
|
515
|
+
|
|
516
|
+
def _adapter_match(
|
|
517
|
+
self,
|
|
518
|
+
normalized_input: str,
|
|
519
|
+
adapter_states: list[str],
|
|
520
|
+
) -> StateMatchResult | None:
|
|
521
|
+
"""Match using adapter-specific state names."""
|
|
522
|
+
# This is a simplified version - adapters should provide their own mapping
|
|
523
|
+
# For now, just do fuzzy matching against adapter states
|
|
524
|
+
if not FUZZY_AVAILABLE:
|
|
525
|
+
return None
|
|
526
|
+
|
|
527
|
+
for adapter_state in adapter_states:
|
|
528
|
+
similarity = fuzz.ratio(normalized_input, adapter_state.lower())
|
|
529
|
+
if similarity >= self.FUZZY_THRESHOLD_HIGH:
|
|
530
|
+
# Try to find which universal state this maps to
|
|
531
|
+
# This requires adapter to implement get_state_mapping
|
|
532
|
+
# For now, return None to fall through to fuzzy match
|
|
533
|
+
pass
|
|
534
|
+
|
|
535
|
+
return None
|
|
536
|
+
|
|
537
|
+
def _fuzzy_match(self, normalized_input: str) -> StateMatchResult | None:
|
|
538
|
+
"""Match using fuzzy string matching."""
|
|
539
|
+
if not FUZZY_AVAILABLE:
|
|
540
|
+
return None
|
|
541
|
+
|
|
542
|
+
best_match: tuple[TicketState, float, str] | None = None
|
|
543
|
+
|
|
544
|
+
for state in TicketState:
|
|
545
|
+
# Check state value
|
|
546
|
+
state_similarity = fuzz.ratio(normalized_input, state.value.lower())
|
|
547
|
+
|
|
548
|
+
if state_similarity >= self.FUZZY_THRESHOLD_MEDIUM:
|
|
549
|
+
if best_match is None or state_similarity > best_match[1]:
|
|
550
|
+
best_match = (state, state_similarity, "state_value")
|
|
551
|
+
|
|
552
|
+
# Check synonyms
|
|
553
|
+
for synonym in self.STATE_SYNONYMS.get(state, []):
|
|
554
|
+
similarity = fuzz.ratio(normalized_input, synonym.lower())
|
|
555
|
+
if similarity >= self.FUZZY_THRESHOLD_MEDIUM:
|
|
556
|
+
if best_match is None or similarity > best_match[1]:
|
|
557
|
+
best_match = (state, similarity, "synonym")
|
|
558
|
+
|
|
559
|
+
if best_match:
|
|
560
|
+
state, similarity, match_source = best_match
|
|
561
|
+
|
|
562
|
+
# Calculate confidence based on similarity
|
|
563
|
+
if similarity >= self.FUZZY_THRESHOLD_HIGH:
|
|
564
|
+
confidence = 0.85 + (similarity - self.FUZZY_THRESHOLD_HIGH) / 100.0
|
|
565
|
+
else:
|
|
566
|
+
confidence = 0.70 + (similarity - self.FUZZY_THRESHOLD_MEDIUM) / 200.0
|
|
567
|
+
|
|
568
|
+
return StateMatchResult(
|
|
569
|
+
state=state,
|
|
570
|
+
confidence=min(confidence, 0.95), # Cap at 0.95
|
|
571
|
+
match_type="fuzzy",
|
|
572
|
+
original_input=normalized_input,
|
|
573
|
+
)
|
|
574
|
+
|
|
575
|
+
return None
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
# Singleton instance for convenience
|
|
579
|
+
_default_matcher: SemanticStateMatcher | None = None
|
|
580
|
+
|
|
581
|
+
|
|
582
|
+
def get_state_matcher() -> SemanticStateMatcher:
|
|
583
|
+
"""Get the default state matcher instance.
|
|
584
|
+
|
|
585
|
+
Returns:
|
|
586
|
+
Singleton SemanticStateMatcher instance
|
|
587
|
+
|
|
588
|
+
"""
|
|
589
|
+
global _default_matcher
|
|
590
|
+
if _default_matcher is None:
|
|
591
|
+
_default_matcher = SemanticStateMatcher()
|
|
592
|
+
return _default_matcher
|