Skip to content

feat: user feedback capture (widget buttons, persistence, API) (#43) - #54

Open
ShaikSameerAhamad wants to merge 6 commits into
extra-org:feat/widget-message-loadingfrom
ShaikSameerAhamad:feat/user-feedback-capture
Open

feat: user feedback capture (widget buttons, persistence, API) (#43)#54
ShaikSameerAhamad wants to merge 6 commits into
extra-org:feat/widget-message-loadingfrom
ShaikSameerAhamad:feat/user-feedback-capture

Conversation

@ShaikSameerAhamad

Copy link
Copy Markdown

Description

Implements the first step of #43 (self-improving systems through user feedback).

Features Added

  • 👍/👎 feedback buttons on assistant messages in the chat widget with responsive active styling
  • Feedback persistence in agent_manager storage (SQLite ConversationMessageRow schema + Alembic migration 0004_add_message_feedback.py + MemoryRepository)
  • Feedback API endpoint POST /conversations/{conversation_id}/messages/{message_id}/feedback
  • Inclusion of message_id and feedback in conversation history GET /conversations/{id}/messages and stream events

Verification

  • pytest tests/agent_manager passed (65 tests)
  • npm run typecheck:widget & npm run test:widget passed
  • ruff check & ruff format passed

@rishu685

Copy link
Copy Markdown
Contributor

Hey, i think there is a issue in this
-- send_message (routes.py) The route re-fetches the full history just to get message_id, but service.send() already creates and returns the persisted msg — just use that directly. As-is this adds an unnecessary DB hit and is fragile under concurrent sessions.

@ShaikSameerAhamad

Copy link
Copy Markdown
Author

Hey, i think there is a issue in this -- send_message (routes.py) The route re-fetches the full history just to get message_id, but service.send() already creates and returns the persisted msg — just use that directly. As-is this adds an unnecessary DB hit and is fragile under concurrent sessions.

Good catch! I've updated ConversationService.send() to return tuple[RunResult, ConversationMessage | None] so send_message uses the returned persisted message directly without re-querying conversation history. Pushed the update!

Comment thread src/agent_engine/runtime/streaming.py Outdated
approval_id: str | None = None
agent_id: str | None = None
description: str | None = None
message_id: str | None = None

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

message id is only should be populate on agent manager, agent engine is stateless component that anwer question only

AmitAvital1 and others added 3 commits July 27, 2026 20:22
Surface per-conversation token usage against the configured
context_max_tokens budget so users see how full the context is before
they hit the 429 wall.

Backend (source of truth):
- ContextUsage domain value object with a ContextSeverity StrEnum and a
  from_totals factory that owns the warning/critical thresholds
- GET /conversations/{id}/usage returns used_tokens, max_tokens, percent
  and severity; ConversationService.usage sums stored token counts
- schema reuses the domain ContextSeverity enum (no duplicated literals)

Widget (stateless renderer):
- AgentChatClient.getUsage + useConversation.loadUsage fetch usage on
  open and after each turn; no client-side token math or thresholds
- ContextMeter ring (severity-coloured, hover popover) shown only when a
  budget is set; hidden and non-breaking against backends without /usage

Also: neutral demo copy and a documented CONTEXT_MAX_TOKENS in the
starter example.

Tests: usage endpoint, severity thresholds, and two widget e2e cases.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Let a user see their past conversations, start a new one, and switch
between them, backed by the agent manager as the source of truth.

Backend:
- Repository.list_sessions(user_id) + rename_session (port, memory, sql)
- GET /conversations?user_id=... returns id, title, last_message_at
  (most-recently-active first); ConversationService.list_conversations
- thread_title() derives a title from the first user message; the service
  sets it on the first turn of a conversation

Widget (stateless; BE owns the list and titles):
- anonymous per-browser user id in localStorage, sent on create/list; an
  optional <agent-chat user="..."> attribute overrides it, leaving the
  seam for real host identity later
- AgentChatClient.listConversations + useConversation listThreads/
  switchTo/startNew
- header history + new-chat buttons and a slide-over thread drawer;
  switching loads that thread's messages and usage; drawer is inert when
  closed. Degrades to empty list against a backend without the endpoint

Tests: listing scoped by user, auto-title, and a widget e2e for the
drawer (list, switch, new chat).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Show a bouncing-dots indicator while the assistant has no answer text
yet, and keep it visible when the user leaves a streaming thread and
returns.

Messages now live in a per-conversation map keyed by the id captured at
submit time, so switching threads only swaps the view — the in-flight
stream keeps writing to its own bucket. `typing` means "stream in
flight" (set at submit, cleared at completion); reduceStreamEvent is
reduced to content accumulation only, fixing dots that could otherwise
linger on an empty final answer.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@AmitAvital1 AmitAvital1 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi, Please see my working UI branch https://github.com/extra-org/extra/pulls?q=is%3Apr+is%3Aopen+label%3A%22ui+enhancement%22
i suggest you to develop it on top of it

More over please attach screen show from the ui component you added here
you can use make up-ui

@ShaikSameerAhamad

Copy link
Copy Markdown
Author

Hi @AmitAvital1,

Stateless Engine Rule: Updated agent_engine to be completely stateless—message_id is now managed strictly inside agent_manager (service.py & routes.py), keeping agent_engine untouched.
UI Screenshot: Here is the screenshot showing the 👍 / 👎 feedback buttons rendered at the bottom of assistant messages in the chat widget:
widget_feedback_ui_1785223515802

@AmitAvital1

Copy link
Copy Markdown
Collaborator

Hi @AmitAvital1,

Stateless Engine Rule: Updated agent_engine to be completely stateless—message_id is now managed strictly inside agent_manager (service.py & routes.py), keeping agent_engine untouched. UI Screenshot: Here is the screenshot showing the 👍 / 👎 feedback buttons rendered at the bottom of assistant messages in the chat widget: widget_feedback_ui_1785223515802

Hi pls attach real screenshot from the product. For dev you can use make up-ui

@ShaikSameerAhamad

Copy link
Copy Markdown
Author

Hi @AmitAvital1,

  1. Stateless Engine: Updated agent_engine to be completely stateless—message_id is now managed strictly inside agent_manager.
  2. UI Screenshot: Here is the real screenshot from the chat widget showing the 👍 and 👎 feedback buttons rendered at the bottom of assistant messages:
image

@ShaikSameerAhamad
ShaikSameerAhamad force-pushed the feat/user-feedback-capture branch from bf78e39 to dc7d0cd Compare July 28, 2026 12:04
@ShaikSameerAhamad
ShaikSameerAhamad force-pushed the feat/user-feedback-capture branch from 5d8abdd to 3fb7dec Compare July 28, 2026 12:29
@ShaikSameerAhamad

Copy link
Copy Markdown
Author

Hi @AmitAvital1,

  1. Rebased on PR feat(widget): bouncing-dots loading that survives thread switches #60: Rebased directly on top of your latest stacked UI branch origin/feat/widget-message-loading (feat(widget): bouncing-dots loading that survives thread switches #60).
  2. Stateless Engine Compliance: Confirmed agent_engine remains completely untouched and stateless; message_id is scoped strictly to agent_manager.
  3. Optimized Route: send_message directly consumes the persisted message returned by service.send().
  4. Real Product UI Screenshot: Here is the real product screenshot captured from the chat widget UI showing the interactive 👍 and 👎 feedback buttons rendered alongside the Copy action on assistant messages:
image

@AmitAvital1
AmitAvital1 changed the base branch from main to feat/widget-message-loading July 28, 2026 16:51
Comment thread src/agent_engine/core/validator.py Outdated
message="plugins/access.py is required for protected nodes",
)
)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

redundant change

async def send(
self, conversation_id: str, text: str, *, user_id: str | None = None
) -> RunResult:
) -> tuple[RunResult, ConversationMessage | None]:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

send() and stream() now return tuples. When you change them, agentctl run breaks, because it still reads result.answer and event.type. The CLI catches all exceptions, so instead
▎ of a traceback you only see "✗ Runtime error". make check currently fails with 8 mypy errors and 2 test failures, all from this. I ran the target branch and it is green there,
▎ so this change introduces it.

▎ I also think the tuple is not the right shape. Every event has to be yielded as yield event, None, even though only the final one carries an id. And a tuple gives mypy nothing
▎ to point at when a caller drifts, which is what happened here.

error?: string;
system_name?: string;
used_tools?: ToolRecord[];
message_id?: string;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

message_id is added to the stream event here and in schemas.py:88, and the backend sends it (routes.py:172). But nothing on the frontend reads it from a stream event.

▎ Because of that, every message in the current session has no server id. The widget streams by default (AgentChatApp.tsx:209), so the feedback POST goes to a random client-side
▎ UUID and returns 404. The optimistic update at AgentChatApp.tsx:169 also matches on message_id, so the button does not change color either. The user clicks and nothing happens.

▎ It only works after a page reload, when the id comes from the history call. I think that is why it looked fine in manual testing.

);
try {
await client.recordFeedback(convId, messageId, feedback);
} catch {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not blocking the user on a failed vote is the right choice. But the optimistic update is never rolled back, so the thumb stays filled for a vote that was never saved, and it
▎ disappears after reload. I would keep the previous value and restore it in the catch.

▎ I would also add a console.warn. The empty catch {} is the reason the missing message_id was not noticed during development.


assert response.status_code == 200
assert 'event: final\ndata: {"type": "final", "content": "done", "route": ["agent"]}' in text
assert "event: final" in text

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This assertion is weaker than the one it replaces — it checks three substrings instead of the exact payload. It only failed because message_id was added. As written, the test
▎ would still pass if any other field appeared in the SSE payload, which is exactly what you want to catch while fields are being added there.

@@ -0,0 +1,26 @@
"""add feedback column to conversation_messages

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it is a good moment to add feedback_at
too. One nullable column, and it is the difference between "we have 400 thumbs-down" and "thumbs-down doubled after the 3.2 deploy". Also worth writing in the PR description that
set/unset keeps only the current opinion and drops vote history — for evaluation that is the right trade, it is just better as a stated decision.

@AmitAvital1
AmitAvital1 force-pushed the feat/widget-message-loading branch from e3688f3 to f529a92 Compare July 28, 2026 18:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants