Skip to content

Commit d3ff029

Browse files
feat: show non-blocking brain-note suggestion bubbles
After an Agent turn, admins see a chip they can open for the excerpt of the shared note that would be created. Overlaps with existing notes or business rules merge into one intent; identical intent is not offered. Propose/accept never blocks the composer. Co-authored-by: Venkat SF <venkatesh.sakamuri@stayflexi.com>
1 parent 58f2716 commit d3ff029

14 files changed

Lines changed: 890 additions & 4 deletions

File tree

CLAUDE.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -275,7 +275,9 @@ The Agent tab must not inherit the admin MCP token. `/api/agent/session` mints a
275275
without `canManageContent` and then 403'd. `get_brain_context` now stamps
276276
`callerCapabilities`; if `doNotOffer` includes `save_brain_note`, the
277277
agent must not mention it. MCP `save_brain_note` also fail-closes before
278-
the POST.
278+
the POST. Admins get a non-blocking suggestion bubble under the answer
279+
(`POST /brain/notes/propose` + accept); overlaps with existing notes or
280+
business rules merge into one intent.
279281

280282
### Verification Anti-Patterns (do not repeat)
281283

agent/SOUL.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ After the answer you may offer **one short follow-up question** (a single line)
2828
`list_connections` carry `callerCapabilities`. If `doNotOffer` lists an
2929
action — especially `save_brain_note` — do not mention it, do not ask
3030
"should I save this", and do not render a Yes button. Answering a metric
31-
is not a request to persist it. Offering a write that 403s is a broken UX.
31+
is not a request to persist it. The product UI may show a non-blocking
32+
save bubble after your answer for admins; leave that to the UI.
3233

3334
## Remembering things — two different places
3435

backend/src/main/java/com/dbaagent/controller/BrainController.java

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@
2828
import com.dbaagent.service.brain.core.BrainNoteService;
2929
import com.dbaagent.service.brain.core.BrainTaskService;
3030
import com.dbaagent.service.brain.core.NoteSuggestionService;
31+
import com.dbaagent.service.brain.core.BrainNoteProposalService;
32+
import com.dbaagent.dto.BrainNoteProposalRequest;
33+
import com.dbaagent.dto.BrainNoteProposalResponse;
3134
import com.dbaagent.service.QueryExecutorService;
3235
import com.dbaagent.service.SchemaSnapshotService;
3336
import com.dbaagent.service.brain.analysis.ColumnDisambiguationService;
@@ -94,6 +97,7 @@ public class BrainController {
9497
private final BrainNoteService brainNoteService;
9598
private final BrainTaskService brainTaskService;
9699
private final NoteSuggestionService noteSuggestionService;
100+
private final BrainNoteProposalService brainNoteProposalService;
97101
private final ColumnProfilingService columnProfilingService;
98102
private final ColumnDisambiguationService columnDisambiguationService;
99103
private final SchemaSnapshotService schemaSnapshotService;
@@ -240,6 +244,51 @@ public ResponseEntity<NoteSuggestionDTO.Response> getNoteSuggestions(
240244
}
241245
}
242246

247+
/**
248+
* Draft a shared-brain note from an Agent turn. Overlaps with existing notes
249+
* or business rules are merged into one intent. Read-only — does not persist.
250+
*/
251+
@PostMapping("/notes/propose")
252+
public ResponseEntity<BrainNoteProposalResponse> proposeNoteFromTurn(
253+
@org.springframework.web.bind.annotation.RequestBody BrainNoteProposalRequest request
254+
) {
255+
try {
256+
if (request == null || request.getConnectionId() == null || request.getConnectionId().isBlank()) {
257+
return ResponseEntity.badRequest().build();
258+
}
259+
accessControlService.assertCanReadConnectionContent(request.getConnectionId());
260+
return brainNoteProposalService.proposeFromTurn(request)
261+
.map(ResponseEntity::ok)
262+
.orElseGet(() -> ResponseEntity.noContent().build());
263+
} catch (ResponseStatusException e) {
264+
throw e;
265+
} catch (Exception e) {
266+
log.error("Error proposing a brain note from an agent turn", e);
267+
return ResponseEntity.internalServerError().build();
268+
}
269+
}
270+
271+
/**
272+
* Accept a proposed note. If it overlaps existing documentation, update that
273+
* row instead of creating a second copy of the same intent.
274+
*/
275+
@PostMapping("/notes/accept")
276+
public ResponseEntity<BrainNoteResponse> acceptNote(
277+
@org.springframework.web.bind.annotation.RequestBody BrainNoteRequest request
278+
) {
279+
try {
280+
accessControlService.assertCanManageConnectionContent(request.getConnectionId());
281+
return ResponseEntity.ok(brainNoteProposalService.accept(request));
282+
} catch (IllegalArgumentException e) {
283+
return ResponseEntity.badRequest().build();
284+
} catch (ResponseStatusException e) {
285+
throw e;
286+
} catch (Exception e) {
287+
log.error("Error accepting a brain note proposal", e);
288+
return ResponseEntity.internalServerError().build();
289+
}
290+
}
291+
243292
@GetMapping("/notes/{connectionId}")
244293
public ResponseEntity<List<BrainNoteResponse>> getNotes(
245294
@PathVariable String connectionId,
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package com.dbaagent.dto;
2+
3+
import lombok.AllArgsConstructor;
4+
import lombok.Data;
5+
import lombok.NoArgsConstructor;
6+
7+
@Data
8+
@NoArgsConstructor
9+
@AllArgsConstructor
10+
public class BrainNoteProposalRequest {
11+
private String connectionId;
12+
private String question;
13+
private String answer;
14+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package com.dbaagent.dto;
2+
3+
import lombok.AllArgsConstructor;
4+
import lombok.Builder;
5+
import lombok.Data;
6+
import lombok.NoArgsConstructor;
7+
8+
@Data
9+
@Builder
10+
@NoArgsConstructor
11+
@AllArgsConstructor
12+
public class BrainNoteProposalResponse {
13+
private String scopeType;
14+
private String tableName;
15+
private String columnName;
16+
private String bubbleLabel;
17+
private String excerpt;
18+
private String proposedNoteText;
19+
/** NEW, MERGE, or SKIP (SKIP is omitted from the Agent UI). */
20+
private String action;
21+
private String existingNoteId;
22+
private String existingNoteText;
23+
private String overlapReason;
24+
}

0 commit comments

Comments
 (0)