Skip to content

Show Assistant Message Sources and Web Searches in Chat UI - #336

Merged
sanskaraggarwal2025 merged 10 commits into
mainfrom
sources-list
May 30, 2026
Merged

sanskaraggarwal2025 merged 10 commits into
mainfrom
sources-list

Conversation

@sanskaraggarwal2025

@sanskaraggarwal2025 sanskaraggarwal2025 commented May 30, 2026 •

Copy link
Copy Markdown
Collaborator

Summary by CodeRabbit

  • New Features

    • Assistant messages now include per-message metadata (web search queries and source URLs) visible in message details.
    • Messages can show multi-modal content (text + images) with a metadata-triggered "Sources" dialog listing web searches and sources.
  • Documentation

    • Added docs describing how per-message sources/metadata are collected and presented in chat history and streamed responses.

Review Change Stack

@coderabbitai

coderabbitai Bot commented May 30, 2026 •

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

This PR captures per-assistant-message metadata (web searches and source URLs): it adds a metadata column and model fields, accepts metadata in DAO inserts (Postgres/SQLite), collects metadata during agentic tool execution, serializes it for persistence, exposes it in protobuf responses, and renders it in the frontend message UI.

Changes

End-to-end metadata support: collection, persistence, and display

Layer / File(s) Summary
Data model and schema foundation
backend/chatservice/dao/models.go, backend/chatservice/dao/db/postgres/scripts/migrations/16_add_message_metadata.up.sql, backend/chatservice/dao/db/sqlite/scripts/migrations/23_add_message_metadata.up.sql, backend/chatservice/dao/dao.go
Adds Metadata string to ChatMessageRow and MessageSummary; schema migrations add metadata TEXT to chat_messages; DAO interface signatures updated to accept metadata.
PostgreSQL DAO implementation
backend/chatservice/dao/dao_postgres.go
PostgresDAO INSERT/SELECT/CTE logic extended to write/read metadata; AddChatMessageWithTokenCount accepts metadata param and returns MessageSummary.Metadata; BranchChat copies metadata during branching.
SQLite DAO implementation
backend/chatservice/dao/dao_sqlite.go
SQLite DAO mirrors Postgres changes: INSERT/SELECT include metadata, method signatures accept metadata, returned MessageSummary.Metadata set, and BranchChat copies metadata.
Metadata collection in agentic chat execution
backend/chatservice/service/agentic_chat.go
Introduces ChatMessageMetadata, ChatWebSearch, ChatSource, helpers to build/serialize/convert metadata; accumulates web search queries and source URLs during tool events; serializes metadata JSON and passes it into DAO persistence calls; streamed summaries include metadata proto.
Chat service persistence and retrieval
backend/chatservice/service/service.go
Chat persists user/assistant messages with metadata parameter (empty or serialized); GetHistory unmarshals stored metadata JSON into ChatMessageMetadata and sets protobuf message metadata for clients.
Protobuf contract and generated TypeScript classes
proto/chatservice.proto, frontend/proto/chatservice.ts
Adds ChatMessageMetadata message (websearches, sources) and optional metadata fields on ResponseSummary, ChatMessage, and ChatInfo; regenerates TypeScript protobuf bindings to include new types and serialization.
Frontend message rendering with metadata display
frontend/src/pages/chat.tsx
Imports new metadata types, updates message typing, refactors rendering to support multimodal contents, and shows a conditional "Sources" dialog listing websearch queries and source URLs from message metadata.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • sortedstartup/sortedchat#324: Introduces web-search tooling in agentic execution; this PR captures those tool args as per-message metadata.
  • sortedstartup/sortedchat#179: Previously extended DAO message persistence signatures (contentImage); overlaps with this PR's DAO signature and persistence changes.
  • sortedstartup/sortedchat#131: Modifies the same chat-message persistence surface and MessageSummary fields; related to this PR's message plumbing.

Suggested reviewers

  • xask00
  • janakiramg

"A rabbit wandered through the code tonight,
gathering queries and sources in its light,
it tucked them in rows,
in metadata prose,
then hopped to the UI to show them bright." 🐇✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title directly reflects the primary objective: adding UI display of assistant message sources and web searches. It accurately summarizes the main user-facing feature being implemented across the changeset.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch sources-list

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 golangci-lint (2.12.2)

level=error msg="[linters_context] typechecking error: pattern ./...: directory prefix . does not contain main module or its selected dependencies"


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request introduces support for saving and displaying assistant message metadata (such as web search queries and source URLs) across the backend, database migrations, protobuf definitions, and frontend UI. Feedback on the changes highlights a critical bug in agentic_chat.go where a type assertion to []map[string]string on unmarshaled JSON will always fail, preventing search sources from being extracted. Additionally, there is duplicate code for the assistantMessageMetadata struct in both models.go and agentic_chat.go that should be consolidated by exporting the struct from the dao package.

I am having trouble creating individual review comments. Click here to see my feedback.

backend/chatservice/service/agentic_chat.go (391-399)

high

The type assertion resultMap["results"].([]map[string]string) will always fail when e.Result is unmarshaled from JSON. In Go, standard JSON unmarshaling into an any or map[string]any decodes JSON arrays as []any (i.e., []interface{}), and JSON objects as map[string]any. Attempting to type-assert directly to []map[string]string will result in ok being false, meaning no web search sources will ever be extracted and displayed in the UI.\n\nTo fix this, type-assert "results" to []any, iterate over the slice, type-assert each element to map[string]any, and then extract the "url" field as a string.

				if resultMap, ok := e.Result.(map[string]any); ok {
					if results, ok := resultMap["results"].([]any); ok {
						for _, res := range results {
							if resMap, ok := res.(map[string]any); ok {
								if url, ok := resMap["url"].(string); ok {
									if trimmed := strings.TrimSpace(url); trimmed != "" {
										sourceURLs = append(sourceURLs, trimmed)
									}
								}
							}
						}
					}
				}

backend/chatservice/dao/models.go (110-157)

medium

The assistantMessageMetadata struct and its sub-structs are duplicated exactly between backend/chatservice/dao/models.go and backend/chatservice/service/agentic_chat.go. Since the service package already imports the dao package, we can eliminate this duplication by exporting these structs from the dao package (e.g., renaming them to AssistantMessageMetadataJSON, AssistantMetadataWebSearch, and AssistantMetadataSource) and reusing them in the service package.

type AssistantMessageMetadataJSON struct {
	WebSearches []AssistantMetadataWebSearch `json:"websearches,omitempty"`
	Sources     []AssistantMetadataSource    `json:"sources,omitempty"`
}

type AssistantMetadataWebSearch struct {
	Query string `json:"query"`
}

type AssistantMetadataSource struct {
	URL string `json:"url"`
}

func ParseAssistantMessageMetadataJSON(raw string) *proto.AssistantMessageMetadata {
	if strings.TrimSpace(raw) == "" {
		return nil
	}

	var metadata AssistantMessageMetadataJSON
	if err := json.Unmarshal([]byte(raw), &metadata); err != nil {
		slog.Error("dao:ParseAssistantMessageMetadataJSON", "message", "failed to unmarshal assistant metadata", "error", err)
		return nil
	}

	protoMetadata := &proto.AssistantMessageMetadata{}
	for _, search := range metadata.WebSearches {
		if strings.TrimSpace(search.Query) == "" {
			continue
		}
		protoMetadata.Websearches = append(protoMetadata.Websearches, &proto.AssistantMessageMetadata_WebSearch{
			Query: search.Query,
		})
	}
	for _, source := range metadata.Sources {
		if strings.TrimSpace(source.URL) == "" {
			continue
		}
		protoMetadata.Sources = append(protoMetadata.Sources, &proto.AssistantMessageMetadata_Source{
			Url: source.URL,
		})
	}

	if len(protoMetadata.Websearches) == 0 && len(protoMetadata.Sources) == 0 {
		return nil
	}

	return protoMetadata
}

backend/chatservice/service/agentic_chat.go (19-84)

medium

To eliminate code duplication, we can remove the local definitions of assistantMessageMetadata and its sub-structs, and instead reuse the exported dao.AssistantMessageMetadataJSON and related structs from the dao package.

func buildAssistantMessageMetadata(webSearchQueries []string, sourceURLs []string) (*pb.AssistantMessageMetadata, string) {
	metadata := dao.AssistantMessageMetadataJSON{}
	seenQueries := make(map[string]struct{})
	seenSources := make(map[string]struct{})

	for _, query := range webSearchQueries {
		trimmed := strings.TrimSpace(query)
		if trimmed == "" {
			continue
		}
		if _, exists := seenQueries[trimmed]; exists {
			continue
		}
		seenQueries[trimmed] = struct{}{}
		metadata.WebSearches = append(metadata.WebSearches, dao.AssistantMetadataWebSearch{Query: trimmed})
	}

	for _, sourceURL := range sourceURLs {
		trimmed := strings.TrimSpace(sourceURL)
		if trimmed == "" {
			continue
		}
		if _, exists := seenSources[trimmed]; exists {
			continue
		}
		seenSources[trimmed] = struct{}{}
		metadata.Sources = append(metadata.Sources, dao.AssistantMetadataSource{URL: trimmed})
	}

	if len(metadata.WebSearches) == 0 && len(metadata.Sources) == 0 {
		return nil, ""
	}

	payload, err := json.Marshal(metadata)
	if err != nil {
		slog.Error("service:Chat", "message", "failed to marshal assistant metadata", "error", err)
		return nil, ""
	}

	protoMetadata := &pb.AssistantMessageMetadata{}
	for _, search := range metadata.WebSearches {
		protoMetadata.Websearches = append(protoMetadata.Websearches, &pb.AssistantMessageMetadata_WebSearch{
			Query: search.Query,
		})
	}
	for _, source := range metadata.Sources {
		protoMetadata.Sources = append(protoMetadata.Sources, &pb.AssistantMessageMetadata_Source{
			Url: source.URL,
		})
	}

	return protoMetadata, string(payload)
}

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
backend/chatservice/dao/dao_sqlite.go (1)

96-106: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Reject malformed metadata before persisting it.

These two inserts now define the SQLite storage contract for assistant metadata, but they still write any string verbatim. Since the history layer deserializes this field later, one bad payload here can poison the row and break metadata rendering on read. Trim and JSON-validate metadata before both inserts.

🛠️ Suggested guard
+func normalizeMetadata(metadata string) (string, error) {
+	trimmed := strings.TrimSpace(metadata)
+	if trimmed == "" {
+		return "", nil
+	}
+
+	var tmp interface{}
+	if err := json.Unmarshal([]byte(trimmed), &tmp); err != nil {
+		return "", fmt.Errorf("invalid JSON format for metadata field: %w", err)
+	}
+	return trimmed, nil
+}

Call this from both write paths and persist the normalized value.

Also applies to: 175-268

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/chatservice/dao/dao_sqlite.go` around lines 96 - 106, Reject and
normalize malformed metadata before persisting by trimming whitespace and
JSON-validating/parsing the metadata string, then re-serializing it (or storing
null/empty) and using that normalized value in the INSERT; update the
AddChatMessage method to perform this trim + JSON validation on the metadata
argument and persist the normalized JSON string (or nil) instead of the raw
input, and extract the same logic into a small helper (e.g., normalizeMetadata)
and call it from both write paths (AddChatMessage and the other insert routine
around lines 175-268) so both inserts persist the validated/normalized metadata
value.
backend/chatservice/dao/dao_postgres.go (1)

109-139: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Validate metadata JSON before either insert path writes it.

This PR makes metadata part of the persisted message contract, but both write paths still accept and store arbitrary text. A malformed payload will be committed here and can break later history reads/source rendering when the service unmarshals it downstream. Reuse the same trim/json.Unmarshal guard you already apply to references.

🛠️ Suggested guard
+func normalizeMetadata(metadata string) (string, error) {
+	trimmed := strings.TrimSpace(metadata)
+	if trimmed == "" {
+		return "", nil
+	}
+
+	var tmp interface{}
+	if err := json.Unmarshal([]byte(trimmed), &tmp); err != nil {
+		return "", fmt.Errorf("invalid JSON format for metadata field: %w", err)
+	}
+	return trimmed, nil
+}

Apply that before both INSERT INTO chat_messages calls and persist the normalized value.

Also applies to: 207-303

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/chatservice/dao/dao_postgres.go` around lines 109 - 139, The metadata
string is not validated before inserting, so add the same trim-and-JSON-validate
guard used for references inside AddChatMessage (and the other insert path
around lines 207-303): create a trimmedMetadata variable =
strings.TrimSpace(metadata), set it to "{}" (or other normalized JSON empty
object) if empty, attempt json.Unmarshal([]byte(trimmedMetadata), &temp) and
return an error on failure, then use trimmedMetadata (the normalized, validated
JSON) in the INSERT statements instead of the raw metadata; reference
AddChatMessage and the alternate insert block to locate where to apply the
change and reuse the same error logging pattern used for references.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@backend/chatservice/service/agentic_chat.go`:
- Around line 380-383: The current handler appends BROWSER_SCRAPE_TOOL_NAME URLs
to sourceURLs on ToolCallStartEvent which records failed/cancelled scrapes as
sources; instead, when handling ToolCallStartEvent (in the agentic chat event
loop/handler in agentic_chat.go) add the URL to a temporary pending map/set
keyed by the tool call id (or event id), and only move that URL from pending
into sourceURLs from the tool success path (e.g., the ToolCallSuccessEvent
handler for the same tool call); also ensure the pending entry is removed on
failure/cancel paths so only successful scrapes become sources.

---

Outside diff comments:
In `@backend/chatservice/dao/dao_postgres.go`:
- Around line 109-139: The metadata string is not validated before inserting, so
add the same trim-and-JSON-validate guard used for references inside
AddChatMessage (and the other insert path around lines 207-303): create a
trimmedMetadata variable = strings.TrimSpace(metadata), set it to "{}" (or other
normalized JSON empty object) if empty, attempt
json.Unmarshal([]byte(trimmedMetadata), &temp) and return an error on failure,
then use trimmedMetadata (the normalized, validated JSON) in the INSERT
statements instead of the raw metadata; reference AddChatMessage and the
alternate insert block to locate where to apply the change and reuse the same
error logging pattern used for references.

In `@backend/chatservice/dao/dao_sqlite.go`:
- Around line 96-106: Reject and normalize malformed metadata before persisting
by trimming whitespace and JSON-validating/parsing the metadata string, then
re-serializing it (or storing null/empty) and using that normalized value in the
INSERT; update the AddChatMessage method to perform this trim + JSON validation
on the metadata argument and persist the normalized JSON string (or nil) instead
of the raw input, and extract the same logic into a small helper (e.g.,
normalizeMetadata) and call it from both write paths (AddChatMessage and the
other insert routine around lines 175-268) so both inserts persist the
validated/normalized metadata value.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d6642204-e7a2-40a2-8b2b-ba3552e5935b

📥 Commits

Reviewing files that changed from the base of the PR and between 126717a and 810a5b3.

⛔ Files ignored due to path filters (1)
  • backend/chatservice/proto/chatservice.pb.go is excluded by !**/*.pb.go
📒 Files selected for processing (11)
  • backend/chatservice/dao/dao.go
  • backend/chatservice/dao/dao_postgres.go
  • backend/chatservice/dao/dao_sqlite.go
  • backend/chatservice/dao/db/postgres/scripts/migrations/16_add_message_metadata.up.sql
  • backend/chatservice/dao/db/sqlite/scripts/migrations/23_add_message_metadata.up.sql
  • backend/chatservice/dao/models.go
  • backend/chatservice/service/agentic_chat.go
  • backend/chatservice/service/service.go
  • frontend/proto/chatservice.ts
  • frontend/src/pages/chat.tsx
  • proto/chatservice.proto

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/features/agentic_chat.md`:
- Line 95: Replace the unhyphenated compound adjective "assistant-message level"
with the hyphenated form "assistant-message-level" in the sentence that reads
"This column stores assistant-message level metadata as JSON." (search for that
exact phrase in docs/features/agentic_chat.md) so the compound adjective
correctly modifies "metadata".
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d4d4a581-7e64-477b-8b13-756ba9f14f02

📥 Commits

Reviewing files that changed from the base of the PR and between 810a5b3 and e5c5af0.

⛔ Files ignored due to path filters (1)
  • backend/chatservice/proto/chatservice.pb.go is excluded by !**/*.pb.go
📒 Files selected for processing (5)
  • backend/chatservice/service/agentic_chat.go
  • docs/features/agentic_chat.md
  • frontend/proto/chatservice.ts
  • frontend/src/pages/chat.tsx
  • proto/chatservice.proto
✅ Files skipped from review due to trivial changes (1)
  • frontend/proto/chatservice.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • proto/chatservice.proto
  • backend/chatservice/service/agentic_chat.go
  • frontend/src/pages/chat.tsx

Comment thread docs/features/agentic_chat.md Resolved
@sanskaraggarwal2025
sanskaraggarwal2025 merged commit 1c11d6b into main May 30, 2026
8 checks passed
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.

2 participants