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.

Files changed (111) hide show
  1. mcp_ticketer/__init__.py +10 -10
  2. mcp_ticketer/__version__.py +3 -3
  3. mcp_ticketer/adapters/__init__.py +2 -0
  4. mcp_ticketer/adapters/aitrackdown.py +394 -9
  5. mcp_ticketer/adapters/asana/__init__.py +15 -0
  6. mcp_ticketer/adapters/asana/adapter.py +1416 -0
  7. mcp_ticketer/adapters/asana/client.py +292 -0
  8. mcp_ticketer/adapters/asana/mappers.py +348 -0
  9. mcp_ticketer/adapters/asana/types.py +146 -0
  10. mcp_ticketer/adapters/github.py +836 -105
  11. mcp_ticketer/adapters/hybrid.py +47 -5
  12. mcp_ticketer/adapters/jira.py +772 -1
  13. mcp_ticketer/adapters/linear/adapter.py +2293 -108
  14. mcp_ticketer/adapters/linear/client.py +146 -12
  15. mcp_ticketer/adapters/linear/mappers.py +105 -11
  16. mcp_ticketer/adapters/linear/queries.py +168 -1
  17. mcp_ticketer/adapters/linear/types.py +80 -4
  18. mcp_ticketer/analysis/__init__.py +56 -0
  19. mcp_ticketer/analysis/dependency_graph.py +255 -0
  20. mcp_ticketer/analysis/health_assessment.py +304 -0
  21. mcp_ticketer/analysis/orphaned.py +218 -0
  22. mcp_ticketer/analysis/project_status.py +594 -0
  23. mcp_ticketer/analysis/similarity.py +224 -0
  24. mcp_ticketer/analysis/staleness.py +266 -0
  25. mcp_ticketer/automation/__init__.py +11 -0
  26. mcp_ticketer/automation/project_updates.py +378 -0
  27. mcp_ticketer/cache/memory.py +3 -3
  28. mcp_ticketer/cli/adapter_diagnostics.py +4 -2
  29. mcp_ticketer/cli/auggie_configure.py +18 -6
  30. mcp_ticketer/cli/codex_configure.py +175 -60
  31. mcp_ticketer/cli/configure.py +884 -146
  32. mcp_ticketer/cli/cursor_configure.py +314 -0
  33. mcp_ticketer/cli/diagnostics.py +31 -28
  34. mcp_ticketer/cli/discover.py +293 -21
  35. mcp_ticketer/cli/gemini_configure.py +18 -6
  36. mcp_ticketer/cli/init_command.py +880 -0
  37. mcp_ticketer/cli/instruction_commands.py +435 -0
  38. mcp_ticketer/cli/linear_commands.py +99 -15
  39. mcp_ticketer/cli/main.py +109 -2055
  40. mcp_ticketer/cli/mcp_configure.py +673 -99
  41. mcp_ticketer/cli/mcp_server_commands.py +415 -0
  42. mcp_ticketer/cli/migrate_config.py +12 -8
  43. mcp_ticketer/cli/platform_commands.py +6 -6
  44. mcp_ticketer/cli/platform_detection.py +477 -0
  45. mcp_ticketer/cli/platform_installer.py +536 -0
  46. mcp_ticketer/cli/project_update_commands.py +350 -0
  47. mcp_ticketer/cli/queue_commands.py +15 -15
  48. mcp_ticketer/cli/setup_command.py +639 -0
  49. mcp_ticketer/cli/simple_health.py +13 -11
  50. mcp_ticketer/cli/ticket_commands.py +277 -36
  51. mcp_ticketer/cli/update_checker.py +313 -0
  52. mcp_ticketer/cli/utils.py +45 -41
  53. mcp_ticketer/core/__init__.py +35 -1
  54. mcp_ticketer/core/adapter.py +170 -5
  55. mcp_ticketer/core/config.py +38 -31
  56. mcp_ticketer/core/env_discovery.py +33 -3
  57. mcp_ticketer/core/env_loader.py +7 -6
  58. mcp_ticketer/core/exceptions.py +10 -4
  59. mcp_ticketer/core/http_client.py +10 -10
  60. mcp_ticketer/core/instructions.py +405 -0
  61. mcp_ticketer/core/label_manager.py +732 -0
  62. mcp_ticketer/core/mappers.py +32 -20
  63. mcp_ticketer/core/models.py +136 -1
  64. mcp_ticketer/core/onepassword_secrets.py +379 -0
  65. mcp_ticketer/core/priority_matcher.py +463 -0
  66. mcp_ticketer/core/project_config.py +148 -14
  67. mcp_ticketer/core/registry.py +1 -1
  68. mcp_ticketer/core/session_state.py +171 -0
  69. mcp_ticketer/core/state_matcher.py +592 -0
  70. mcp_ticketer/core/url_parser.py +425 -0
  71. mcp_ticketer/core/validators.py +69 -0
  72. mcp_ticketer/defaults/ticket_instructions.md +644 -0
  73. mcp_ticketer/mcp/__init__.py +2 -2
  74. mcp_ticketer/mcp/server/__init__.py +2 -2
  75. mcp_ticketer/mcp/server/diagnostic_helper.py +175 -0
  76. mcp_ticketer/mcp/server/main.py +187 -93
  77. mcp_ticketer/mcp/server/routing.py +655 -0
  78. mcp_ticketer/mcp/server/server_sdk.py +58 -0
  79. mcp_ticketer/mcp/server/tools/__init__.py +37 -9
  80. mcp_ticketer/mcp/server/tools/analysis_tools.py +854 -0
  81. mcp_ticketer/mcp/server/tools/attachment_tools.py +65 -20
  82. mcp_ticketer/mcp/server/tools/bulk_tools.py +259 -202
  83. mcp_ticketer/mcp/server/tools/comment_tools.py +74 -12
  84. mcp_ticketer/mcp/server/tools/config_tools.py +1429 -0
  85. mcp_ticketer/mcp/server/tools/diagnostic_tools.py +211 -0
  86. mcp_ticketer/mcp/server/tools/hierarchy_tools.py +878 -319
  87. mcp_ticketer/mcp/server/tools/instruction_tools.py +295 -0
  88. mcp_ticketer/mcp/server/tools/label_tools.py +942 -0
  89. mcp_ticketer/mcp/server/tools/pr_tools.py +3 -7
  90. mcp_ticketer/mcp/server/tools/project_status_tools.py +158 -0
  91. mcp_ticketer/mcp/server/tools/project_update_tools.py +473 -0
  92. mcp_ticketer/mcp/server/tools/search_tools.py +180 -97
  93. mcp_ticketer/mcp/server/tools/session_tools.py +308 -0
  94. mcp_ticketer/mcp/server/tools/ticket_tools.py +1182 -82
  95. mcp_ticketer/mcp/server/tools/user_ticket_tools.py +364 -0
  96. mcp_ticketer/queue/health_monitor.py +1 -0
  97. mcp_ticketer/queue/manager.py +4 -4
  98. mcp_ticketer/queue/queue.py +3 -3
  99. mcp_ticketer/queue/run_worker.py +1 -1
  100. mcp_ticketer/queue/ticket_registry.py +2 -2
  101. mcp_ticketer/queue/worker.py +15 -13
  102. mcp_ticketer/utils/__init__.py +5 -0
  103. mcp_ticketer/utils/token_utils.py +246 -0
  104. mcp_ticketer-2.0.1.dist-info/METADATA +1366 -0
  105. mcp_ticketer-2.0.1.dist-info/RECORD +122 -0
  106. mcp_ticketer-0.4.11.dist-info/METADATA +0 -496
  107. mcp_ticketer-0.4.11.dist-info/RECORD +0 -77
  108. {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-2.0.1.dist-info}/WHEEL +0 -0
  109. {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-2.0.1.dist-info}/entry_points.txt +0 -0
  110. {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-2.0.1.dist-info}/licenses/LICENSE +0 -0
  111. {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