2020public class BrainNoteIntentService {
2121
2222 private static final Pattern BACKTICK_IDENT = Pattern .compile ("`([^`]+)`" );
23- private static final Pattern DEFINITION_CUE = Pattern .compile (
24- "\\ b(metric|definition|means|pinned|use this|correct|from|count|always|filter|join)\\ b" ,
25- Pattern .CASE_INSENSITIVE
26- );
2723 private static final Set <String > STOP_TABLES = Set .of (
2824 "information_schema" , "pg_catalog" , "mysql" , "sys" , "performance_schema"
2925 );
26+ /**
27+ * User follow-ups that teach or correct. Phrase contains() — not a fat
28+ * regex — so a long Agent transcript cannot ReDoS the propose path.
29+ */
30+ private static final List <String > FEEDBACK_PHRASES = List .of (
31+ "that's wrong" , "that is wrong" , "that's not" , "that is not" ,
32+ "incorrect" , "actually " , "instead" , "should be" , "should use" ,
33+ "you should" , "don't use" , "do not use" , "never use" ,
34+ "always use" , "always filter" , "always join" , "always exclude" ,
35+ "we use" , "we always" , "we never" , "not that" , "not the " ,
36+ "pin this" , "pin that" , "remember this" , "remember:" ,
37+ "save this" , "save that" , "use this" , "use that" ,
38+ "too high" , "too low" , "off by" , "the right "
39+ );
3040
3141 public record ContextItem (
3242 String id ,
@@ -50,15 +60,25 @@ public record Proposal(
5060 ) {}
5161
5262 public Optional <Proposal > proposeFromTurn (String question , String answer , List <ContextItem > existing ) {
53- if (answer == null || answer .isBlank ()) {
54- return Optional .empty ();
55- }
56- String cleaned = stripMarkdownNoise (answer );
57- if (cleaned .length () < 24 ) {
63+ return proposeFromTurn (question , answer , existing , null );
64+ }
65+
66+ /**
67+ * Only draft a note when the user just corrected or taught after a prior
68+ * Agent answer. A clean first-turn definition is not a recommendation.
69+ */
70+ public Optional <Proposal > proposeFromTurn (
71+ String question ,
72+ String answer ,
73+ List <ContextItem > existing ,
74+ String priorAnswer
75+ ) {
76+ if (!isCorrectionTurn (question , priorAnswer )) {
5877 return Optional .empty ();
5978 }
60- String combined = (question == null ? "" : question ) + "\n " + cleaned ;
61- if (!DEFINITION_CUE .matcher (combined ).find () && !looksLikePinnedDefinition (cleaned )) {
79+ String cleaned = stripMarkdownNoise (nvl (answer ));
80+ String combined = nvl (question ) + "\n " + cleaned ;
81+ if (combined .trim ().length () < 24 ) {
6282 return Optional .empty ();
6383 }
6484
@@ -68,13 +88,13 @@ public Optional<Proposal> proposeFromTurn(String question, String answer, List<C
6888 }
6989 String tableName = target .get ()[0 ];
7090 String columnName = target .get ()[1 ];
71- String excerpt = excerpt (cleaned );
91+ String excerpt = excerpt (nvl ( question ) + ( cleaned . isBlank () ? "" : " " + cleaned ) );
7292 String proposed = columnName != null
7393 ? "For " + tableName + "." + columnName + ": " + excerpt
7494 : "For " + tableName + ": " + excerpt ;
7595 String label = columnName != null
76- ? "Save definition : " + columnName
77- : "Save definition : " + tableName ;
96+ ? "Save correction : " + columnName
97+ : "Save correction : " + tableName ;
7898
7999 Proposal draft = new Proposal (
80100 columnName != null ? "COLUMN" : "TABLE" ,
@@ -91,6 +111,30 @@ public Optional<Proposal> proposeFromTurn(String question, String answer, List<C
91111 return Optional .of (resolveOverlap (draft , existing == null ? List .of () : existing ));
92112 }
93113
114+ public boolean isCorrectionTurn (String question , String priorAnswer ) {
115+ if (priorAnswer == null || priorAnswer .isBlank ()) {
116+ return false ;
117+ }
118+ return looksLikeUserFeedback (question );
119+ }
120+
121+ public boolean looksLikeUserFeedback (String question ) {
122+ if (question == null || question .isBlank ()) {
123+ return false ;
124+ }
125+ String q = question .toLowerCase (Locale .ROOT ).trim ();
126+ if (q .startsWith ("no," ) || q .startsWith ("no " ) || q .startsWith ("no-" )
127+ || q .startsWith ("no—" ) || q .startsWith ("nope" )) {
128+ return true ;
129+ }
130+ for (String phrase : FEEDBACK_PHRASES ) {
131+ if (q .contains (phrase )) {
132+ return true ;
133+ }
134+ }
135+ return false ;
136+ }
137+
94138 public Proposal resolveOverlap (Proposal draft , List <ContextItem > existing ) {
95139 if (draft == null ) {
96140 return null ;
@@ -333,9 +377,8 @@ private boolean tableMatches(String left, String right) {
333377 return false ;
334378 }
335379
336- private boolean looksLikePinnedDefinition (String answer ) {
337- return answer .toLowerCase (Locale .ROOT ).contains ("pinned" )
338- || answer .toLowerCase (Locale .ROOT ).contains ("correct" );
380+ private static String nvl (String value ) {
381+ return value == null ? "" : value ;
339382 }
340383
341384 private String excerpt (String text ) {
0 commit comments