Skip to content

Commit 8037d5d

Browse files
authored
Merge pull request #579 from Chris0Jeky/enhance/571-intent-classifier-nlp
Improve LlmIntentClassifier NLP coverage
2 parents b948742 + 09d73d2 commit 8037d5d

2 files changed

Lines changed: 409 additions & 105 deletions

File tree

Lines changed: 198 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,219 @@
1+
using System.Text.RegularExpressions;
2+
13
namespace Taskdeck.Application.Services;
24

35
public static class LlmIntentClassifier
46
{
7+
// Timeout to prevent catastrophic backtracking
8+
private static readonly TimeSpan RegexTimeout = TimeSpan.FromMilliseconds(100);
9+
10+
// Negative context patterns — suppress matches in these contexts
11+
private static readonly Regex NegationPattern = new(
12+
@"\b(don'?t|do not|never|stop|cancel|undo|avoid)\b(\s+\w+){0,6}\s+\b(create|add|make|move|archive|delete|remove|update|edit|rename|generate|build|set up|prepare)\b",
13+
RegexOptions.Compiled | RegexOptions.IgnoreCase,
14+
RegexTimeout);
15+
16+
private static readonly Regex OtherToolPattern = new(
17+
@"\b(in|for|with|using|on)\s+(jira|trello|asana|notion|monday|clickup|linear|github issues|azure devops)\b",
18+
RegexOptions.Compiled | RegexOptions.IgnoreCase,
19+
RegexTimeout);
20+
21+
private static readonly Regex QuestionAboutHowPattern = new(
22+
@"^\s*(how|what|where|when|why|can\s+i|is\s+it)\b.*\?$",
23+
RegexOptions.Compiled | RegexOptions.IgnoreCase,
24+
RegexTimeout);
25+
26+
// Card creation — verbs followed by optional words then card/task nouns
27+
private static readonly Regex CardCreatePattern = new(
28+
@"\b(create|add|make|generate|build|prepare|set\s+up)\b(\s+\w+){0,5}\s+\b(cards?|tasks?|items?)\b",
29+
RegexOptions.Compiled | RegexOptions.IgnoreCase,
30+
RegexTimeout);
31+
32+
// "new card/task" with optional words between
33+
private static readonly Regex NewCardPattern = new(
34+
@"\b(new)\b(\s+\w+){0,4}\s+\b(cards?|tasks?|items?)\b",
35+
RegexOptions.Compiled | RegexOptions.IgnoreCase,
36+
RegexTimeout);
37+
38+
// Card move — "move" + optional words + "card/task"
39+
private static readonly Regex CardMovePattern = new(
40+
@"\bmove\b(\s+\w+){0,4}\s+\b(cards?|tasks?)\b",
41+
RegexOptions.Compiled | RegexOptions.IgnoreCase,
42+
RegexTimeout);
43+
44+
// Card archive — "archive/delete/remove" + optional words + "card/task"
45+
private static readonly Regex CardArchivePattern = new(
46+
@"\b(archive|delete|remove)\b(\s+\w+){0,4}\s+\b(cards?|tasks?)\b",
47+
RegexOptions.Compiled | RegexOptions.IgnoreCase,
48+
RegexTimeout);
49+
50+
// Card update — "update/edit/rename" + optional words + "card/task"
51+
private static readonly Regex CardUpdatePattern = new(
52+
@"\b(update|edit|rename|modify|change)\b(\s+\w+){0,4}\s+\b(cards?|tasks?)\b",
53+
RegexOptions.Compiled | RegexOptions.IgnoreCase,
54+
RegexTimeout);
55+
56+
// Board creation — verbs + optional words + "board"
57+
private static readonly Regex BoardCreatePattern = new(
58+
@"\b(create|add|make|generate|build|prepare|set\s+up|new)\b(\s+\w+){0,4}\s+\b(boards?|project\s+boards?)\b",
59+
RegexOptions.Compiled | RegexOptions.IgnoreCase,
60+
RegexTimeout);
61+
62+
// Board rename
63+
private static readonly Regex BoardRenamePattern = new(
64+
@"\b(rename|update|edit)\b(\s+\w+){0,4}\s+\bboards?\b",
65+
RegexOptions.Compiled | RegexOptions.IgnoreCase,
66+
RegexTimeout);
67+
68+
// Column reorder / sort
69+
private static readonly Regex ReorderPattern = new(
70+
@"\b(reorder|sort|rearrange|reorganize)\b(\s+\w+){0,4}\s+\b(cards?|columns?|boards?)\b",
71+
RegexOptions.Compiled | RegexOptions.IgnoreCase,
72+
RegexTimeout);
73+
574
public static (bool IsActionable, string? ActionIntent) Classify(string message)
675
{
776
if (string.IsNullOrWhiteSpace(message))
877
return (false, null);
978

1079
var lower = message.ToLowerInvariant();
1180

12-
// Card creation — explicit commands and natural language
13-
if (lower.Contains("create card") || lower.Contains("add card")
14-
|| lower.Contains("create a card") || lower.Contains("add a card")
15-
|| lower.Contains("create task") || lower.Contains("add task")
16-
|| lower.Contains("create a task") || lower.Contains("add a task")
17-
|| lower.Contains("new card") || lower.Contains("new task")
18-
|| lower.Contains("make a card") || lower.Contains("make a task")
19-
|| lower.Contains("make card") || lower.Contains("make task"))
20-
return (true, "card.create");
81+
// Check negative context first — suppress if negated or about another tool
82+
if (IsNegativeContext(lower, message))
83+
return (false, null);
2184

22-
if (lower.Contains("move card"))
23-
return (true, "card.move");
24-
if (lower.Contains("archive card") || lower.Contains("delete card")
25-
|| lower.Contains("remove card"))
85+
// Archive/delete/remove must be checked BEFORE move to fix the
86+
// "remove card" substring bug (remove contains "move")
87+
if (MatchesCardArchive(lower))
2688
return (true, "card.archive");
27-
if (lower.Contains("update card") || lower.Contains("edit card")
28-
|| lower.Contains("rename card"))
89+
90+
if (MatchesCardMove(lower))
91+
return (true, "card.move");
92+
93+
if (MatchesCardUpdate(lower))
2994
return (true, "card.update");
30-
if (lower.Contains("create board") || lower.Contains("add board")
31-
|| lower.Contains("new board"))
95+
96+
if (MatchesCardCreate(lower))
97+
return (true, "card.create");
98+
99+
if (MatchesBoardCreate(lower))
32100
return (true, "board.create");
33-
if (lower.Contains("rename board"))
101+
102+
if (MatchesBoardRename(lower))
34103
return (true, "board.update");
35-
if (lower.Contains("reorder cards") || lower.Contains("reorder column")
36-
|| lower.Contains("reorder columns") || lower.Contains("reorder board")
37-
|| lower.Contains("sort cards") || lower.Contains("sort column")
38-
|| lower.Contains("sort columns") || lower.Contains("sort board"))
104+
105+
if (MatchesReorder(lower))
39106
return (true, "column.reorder");
40107

41108
return (false, null);
42109
}
110+
111+
private static bool IsNegativeContext(string lower, string original)
112+
{
113+
try
114+
{
115+
// Negation: "don't create task yet"
116+
if (NegationPattern.IsMatch(lower))
117+
return true;
118+
119+
// Asking about another tool: "how do I create a card in Jira?"
120+
if (OtherToolPattern.IsMatch(lower) && QuestionAboutHowPattern.IsMatch(original.Trim()))
121+
return true;
122+
}
123+
catch (RegexMatchTimeoutException)
124+
{
125+
// On timeout, fall through to normal classification
126+
}
127+
128+
return false;
129+
}
130+
131+
private static bool MatchesCardCreate(string lower)
132+
{
133+
try
134+
{
135+
if (CardCreatePattern.IsMatch(lower))
136+
return true;
137+
if (NewCardPattern.IsMatch(lower))
138+
return true;
139+
}
140+
catch (RegexMatchTimeoutException)
141+
{
142+
// Fall through — don't match on timeout
143+
}
144+
145+
return false;
146+
}
147+
148+
private static bool MatchesCardMove(string lower)
149+
{
150+
try
151+
{
152+
if (CardMovePattern.IsMatch(lower))
153+
return true;
154+
}
155+
catch (RegexMatchTimeoutException) { }
156+
157+
return false;
158+
}
159+
160+
private static bool MatchesCardArchive(string lower)
161+
{
162+
try
163+
{
164+
if (CardArchivePattern.IsMatch(lower))
165+
return true;
166+
}
167+
catch (RegexMatchTimeoutException) { }
168+
169+
return false;
170+
}
171+
172+
private static bool MatchesCardUpdate(string lower)
173+
{
174+
try
175+
{
176+
if (CardUpdatePattern.IsMatch(lower))
177+
return true;
178+
}
179+
catch (RegexMatchTimeoutException) { }
180+
181+
return false;
182+
}
183+
184+
private static bool MatchesBoardCreate(string lower)
185+
{
186+
try
187+
{
188+
if (BoardCreatePattern.IsMatch(lower))
189+
return true;
190+
}
191+
catch (RegexMatchTimeoutException) { }
192+
193+
return false;
194+
}
195+
196+
private static bool MatchesBoardRename(string lower)
197+
{
198+
try
199+
{
200+
if (BoardRenamePattern.IsMatch(lower))
201+
return true;
202+
}
203+
catch (RegexMatchTimeoutException) { }
204+
205+
return false;
206+
}
207+
208+
private static bool MatchesReorder(string lower)
209+
{
210+
try
211+
{
212+
if (ReorderPattern.IsMatch(lower))
213+
return true;
214+
}
215+
catch (RegexMatchTimeoutException) { }
216+
217+
return false;
218+
}
43219
}

0 commit comments

Comments
 (0)