Skip to main content

TDD: Chat Response Decorator Agent

Parent Issue: #263 - Agentic Chat
Created: 2025-10-25
Status: Planning


Overview

Create a lightweight post-processing agent that decorates chat responses with interactive hyperlinks, making page references clickable for instant navigation.

Problem Statement

The main chat agent returns helpful responses like:

Here are the files and pages that have information about the Second Floor:

• File 1, Page 6: "SECOND FLOOR PLAN - BLOCK I" (A4.13)
• File 1, Page 7: "SECOND FLOOR BLOCK 2" (A4.14)

But these are plain text - users can't click to navigate. They have to manually:

  1. Read "File 1, Page 6"
  2. Close chat
  3. Navigate file tree → File 1
  4. Scroll to Page 6

Goal: Make page references automatically clickable.


Desired Behavior

Input (from main agent):

File 1, Page 6: "SECOND FLOOR PLAN - BLOCK I" (A4.13)

Output (decorated):

[File 1, Page 6: "SECOND FLOOR PLAN - BLOCK I" (A4.13)](/projects/san-jose-multi-file3/files/1/pages/6/preview)

Rendered in UI:

<a href="/projects/san-jose-multi-file3/files/1/pages/6/preview">
File 1, Page 6: "SECOND FLOOR PLAN - BLOCK I" (A4.13)
</a>

User clicks → Instantly navigates to File 1, Page 6!


Architecture

High-Level Flow

Main Agent Response (raw text)

ChatResponseDecoratorAgent (Gemini 2.0 Flash - fast!)

Decorated Response (markdown with [links])

Frontend (marked.js renders links)

User clicks → Angular Router navigates

Implementation Options

  • Pros: Handles various formats ("Page 6", "page 6", "pg 6", etc.)
  • Pros: Context-aware (understands intent)
  • Cons: Small latency (~200ms)
  • Cost: Minimal (Gemini 2.0 Flash: $0.075/1M input tokens)

Option B: Regex-Based

  • Pros: Zero latency
  • Pros: Zero cost
  • Cons: Brittle (breaks on format variations)
  • Cons: Requires maintenance for new formats

Recommendation: Start with Option A (LLM), fall back to Option B if latency becomes an issue.


Technical Design

1. Decorator Agent (Java)

File: src/main/java/.../service/ChatResponseDecoratorAgent.java

public class ChatResponseDecoratorAgent {

private static final String MODEL = "gemini-2.0-flash"; // Fast & cheap

/**
* Decorate chat response with hyperlinks.
*
* @param rawResponse Raw text from main agent
* @param context Chat context (projectId, fileId, pageNumber)
* @return Decorated response with markdown links
*/
public static String decorateResponse(String rawResponse, ChatContext context) {

// Build prompt for decorator agent
String prompt = buildDecoratorPrompt(rawResponse, context);

// Call Gemini 2.0 Flash (fast single-turn)
GoogleGenAiClient client = GoogleGenAiClient.builder()
.setModel(Model.GEMINI_2_0_FLASH)
.singleTurn()
.setTemperature(0.0f)
.build();

String decoratedResponse = client.prompt(prompt);

return decoratedResponse;
}

private static String buildDecoratorPrompt(String rawResponse, ChatContext context) {
return String.format("""
You are a response decorator. Convert page references to markdown links.

Project ID: %s

INPUT TEXT:
%s

TASK:
1. Find all page references like "File 1, Page 6", "File 2, Page 3", etc.
2. Convert to markdown links: [File 1, Page 6](/projects/%s/files/1/pages/6/preview)
3. Preserve ALL other text exactly as-is
4. Return ONLY the decorated text (no explanations)

IMPORTANT:
- Keep all formatting, spacing, and line breaks
- Only add links, don't change the text
- URL format: /projects/{projectId}/files/{fileNum}/pages/{pageNum}/preview

OUTPUT (decorated text):
""",
context.getProjectId(),
rawResponse,
context.getProjectId()
);
}
}

2. Integration with ChatAgentService

File: src/main/java/.../service/ChatAgentService.java

public Flux<ChatMessageResponse> processMessage(...) {
return Flux.create(sink -> {
// ... existing agent processing ...

// AFTER agent completes, before sending final response:
String rawResponse = fullResponse.toString();

// Decorate response with links (async, non-blocking)
String decoratedResponse = ChatResponseDecoratorAgent.decorateResponse(
rawResponse,
context
);

// Send decorated response instead of raw
sink.next(ChatMessageResponse.builder()
.content(decoratedResponse) // ← Decorated!
.isFinal(true)
.build());
});
}

3. Frontend (No Changes Needed!)

Angular's router already handles relative URLs:

  • marked.js renders: [File 1, Page 6](/projects/.../pages/6/preview)
  • Browser renders: <a href="/projects/.../pages/6/preview">File 1, Page 6</a>
  • User clicks → Angular Router navigates ✅

Example Transformations

Example 1: Simple Page Reference

Input:

File 1, Page 6: "SECOND FLOOR PLAN - BLOCK I"

Output:

[File 1, Page 6](/projects/san-jose-multi-file3/files/1/pages/6/preview): "SECOND FLOOR PLAN - BLOCK I"

Example 2: Multiple References

Input:

The electrical details are on File 2, Page 5 and File 2, Page 8.

Output:

The electrical details are on [File 2, Page 5](/projects/san-jose-multi-file3/files/2/pages/5/preview) and [File 2, Page 8](/projects/san-jose-multi-file3/files/2/pages/8/preview).

Example 3: List Format

Input:

• File 1, Page 6: "SECOND FLOOR PLAN"
• File 1, Page 7: "SECOND FLOOR BLOCK 2"

Output:

[File 1, Page 6](/projects/san-jose-multi-file3/files/1/pages/6/preview): "SECOND FLOOR PLAN"
[File 1, Page 7](/projects/san-jose-multi-file3/files/1/pages/7/preview): "SECOND FLOOR BLOCK 2"

Performance

Latency Analysis

Without Decorator:

  • Main agent: ~2-4 seconds
  • Total: ~2-4 seconds

With Decorator:

  • Main agent: ~2-4 seconds
  • Decorator (Gemini 2.0 Flash): ~200-400ms
  • Total: ~2.5-4.5 seconds

Impact: +200-400ms (~10% increase)

Cost Analysis

Per decorated response:

  • Input tokens: ~500 (prompt + response)
  • Output tokens: ~500 (decorated response)
  • Cost: $0.000075 per decoration

Monthly cost (1,000 users × 5 queries/day × 30 days):

  • Total decorations: 150,000
  • Cost: $11.25/month

Negligible compared to main agent costs (~$30/month).


Implementation Phases

Phase 1: Core Decorator (1 day)

  • Create ChatResponseDecoratorAgent.java
  • Implement decorateResponse() method
  • Write prompt template
  • Integrate with ChatAgentService
  • Test with sample responses

Phase 2: Advanced Patterns (1 day)

  • Handle code section references (e.g., "IBC 2021 Section 1005.3")
  • Handle detail references (e.g., "Detail 3/A4.1")
  • Handle cross-references between pages
  • Add caching for common patterns

Phase 3: Optimization (1 day)

  • Add response caching (identical responses)
  • Batch multiple decorations
  • Performance monitoring
  • Fallback to regex if LLM unavailable

Alternative Approaches

Approach 1: Regex-Based (No LLM)

public static String decorateResponseRegex(String text, String projectId) {
// Pattern: "File {num}, Page {num}"
Pattern pattern = Pattern.compile(
"File\\s+(\\d+),\\s+Page\\s+(\\d+)",
Pattern.CASE_INSENSITIVE
);

return pattern.matcher(text).replaceAll(match -> {
String fileNum = match.group(1);
String pageNum = match.group(2);
String url = String.format(
"/projects/%s/files/%s/pages/%s/preview",
projectId, fileNum, pageNum
);
return String.format("[%s](%s)", match.group(0), url);
});
}

Pros: Zero latency, zero cost
Cons: Misses variations like "page 6", "pg 6", "p. 6"

Approach 2: Hybrid

  1. Try regex first (instant)
  2. If no matches, use LLM (fallback)

Prompt Template

You are a response decorator. Convert page references to markdown links.

PROJECT CONTEXT:
- Project ID: {projectId}
- Current File: {fileId} (optional)
- Current Page: {pageNumber} (optional)

INPUT TEXT:
{rawResponse}

TASK:
1. Find ALL page references in formats like:
- "File 1, Page 6"
- "File 2, Page 3: Title"
- "page 5"
- "File 1, pages 6-8"

2. Convert to markdown links:
- Format: [original text](/projects/{projectId}/files/{fileNum}/pages/{pageNum}/preview)
- Example: [File 1, Page 6](/projects/san-jose-multi-file3/files/1/pages/6/preview)

3. Rules:
- Preserve ALL text, formatting, line breaks
- Only add markdown link syntax
- Don't explain, just return decorated text
- If uncertain, leave text as-is

OUTPUT (decorated text only):

Testing Strategy

Unit Tests

@Test
public void testDecorateSimpleReference() {
String input = "File 1, Page 6 has the details.";
String expected = "[File 1, Page 6](/projects/test-proj/files/1/pages/6/preview) has the details.";

ChatContext context = ChatContext.newBuilder()
.setProjectId("test-proj")
.build();

String result = ChatResponseDecoratorAgent.decorateResponse(input, context);
assertEquals(expected, result);
}

@Test
public void testDecorateMultipleReferences() {
String input = "See File 1, Page 6 and File 2, Page 3.";
// Should have 2 links
}

@Test
public void testPreserveFormatting() {
String input = "• File 1, Page 6\n• File 1, Page 7";
// Should preserve bullets and line breaks
}

Integration Tests

  1. Send message via chat
  2. Verify response contains markdown links
  3. Verify links have correct project ID
  4. Click link in UI → verify navigation works

Error Handling

Fallback Strategy

public static String decorateResponse(String rawResponse, ChatContext context) {
try {
// Try LLM decoration
return decorateWithLLM(rawResponse, context);
} catch (Exception e) {
logger.warning("LLM decoration failed, using regex fallback: " + e.getMessage());
try {
// Fallback to regex
return decorateWithRegex(rawResponse, context.getProjectId());
} catch (Exception e2) {
logger.warning("Regex decoration failed, returning original: " + e2.getMessage());
// Ultimate fallback: return original text
return rawResponse;
}
}
}

Graceful degradation: Always return valid text, even if decoration fails.


Configuration

Environment Variables

# env/{env}/gcp/cloud-run/grpc/vars.yaml
CHAT_DECORATOR_ENABLED: "true"
CHAT_DECORATOR_MODEL: "gemini-2.0-flash"
CHAT_DECORATOR_TIMEOUT_MS: "500" # Fail fast if slow
CHAT_DECORATOR_FALLBACK: "regex" # regex | none

Feature Flag

private static final boolean DECORATOR_ENABLED = 
Boolean.parseBoolean(System.getenv("CHAT_DECORATOR_ENABLED"));

if (DECORATOR_ENABLED) {
decoratedResponse = ChatResponseDecoratorAgent.decorateResponse(...);
} else {
decoratedResponse = rawResponse;
}

Format: /projects/{projectId}/files/{fileNum}/pages/{pageNum}/preview

Example: /projects/san-jose-multi-file3/files/1/pages/6/preview

With Detail Reference (Future)

Format: /projects/{projectId}/files/{fileNum}/pages/{pageNum}/preview#detail-{detailId}

Example: /projects/san-jose-multi-file3/files/1/pages/6/preview#detail-A4.13

Format: /icc-codes/{bookId}/sections/{sectionId}

Example: /icc-codes/2217/sections/IBC2021P2_Ch11_Sec1105


Success Metrics

User Experience:

  • ✅ 90%+ of page references become clickable
  • ✅ Click → navigate in <1 second
  • ✅ Correct page loads 95%+ of time

Performance:

  • ✅ P95 decoration latency: <500ms
  • ✅ P99 decoration latency: <1000ms
  • ✅ Fallback success rate: 100%

Reliability:

  • ✅ Never breaks existing functionality
  • ✅ Graceful degradation if LLM unavailable
  • ✅ No increased error rate

Implementation Checklist

Backend (Java)

  • Create ChatResponseDecoratorAgent.java

    • decorateResponse(String, ChatContext) method
    • buildDecoratorPrompt(...) method
    • decorateWithRegex(...) fallback method
    • Error handling with graceful degradation
  • Integrate with ChatAgentService.java

    • Call decorator before sending final response
    • Add feature flag check
    • Add timeout handling
    • Add logging/metrics
  • Environment configuration

    • Add CHAT_DECORATOR_* env vars
    • Update all environment files (dev/demo/test/prod)

Testing

  • Unit tests for decorator

    • Simple reference: "File 1, Page 6"
    • Multiple references in one text
    • List format with bullets
    • Prose format ("details on page 5")
    • Edge cases (no references, malformed)
  • Integration tests

    • Send chat message → verify links in response
    • Click link → verify navigation
    • Test with various projects

Frontend (No Code Changes!)

  • Verify marked.js renders links correctly
  • Verify Angular Router handles navigation
  • Test on mobile (touch targets)
  • Accessibility audit (screen readers)

Example Decorator Prompt

You are a response decorator. Add markdown links to page references.

PROJECT: san-jose-multi-file3

INPUT:
Here are the files and pages with second floor information:

• File 1, Page 6: "SECOND FLOOR PLAN - BLOCK I" (A4.13)
• File 1, Page 7: "SECOND FLOOR BLOCK 2" (A4.14)

TASK:
Convert page references to markdown links using format:
[File X, Page Y](/projects/san-jose-multi-file3/files/X/pages/Y/preview)

OUTPUT (decorated text only, no explanations):
Here are the files and pages with second floor information:

• [File 1, Page 6](/projects/san-jose-multi-file3/files/1/pages/6/preview): "SECOND FLOOR PLAN - BLOCK I" (A4.13)
• [File 1, Page 7](/projects/san-jose-multi-file3/files/1/pages/7/preview): "SECOND FLOOR BLOCK 2" (A4.14)

Future Enhancements

  • Detail references: "Detail 3/A4.1" → /projects/.../pages/4/preview#detail-A4-1
  • Code sections: "IBC 1005.3" → /icc-codes/2217/sections/...
  • File references: "electrical plan" → /projects/.../files/2/preview

Phase 3: Context-Aware

  • Relative references: "page 6" (no file number) → infer from context
  • Range references: "pages 6-8" → link to first page with note
  • Ambiguous references: Ask for clarification or link to search

Phase 4: Interactive Elements

  • Quick actions: Hover over link → preview thumbnail
  • Batch navigation: "Open all 3 pages" button
  • Pin from link: Right-click link → "Pin this page"

Risks & Mitigations

RiskImpactMitigation
LLM latency >1sPoor UX500ms timeout, fallback to regex
LLM hallucinationWrong linksValidate links against actual files
Cost overrunBudget issueMonitor usage, add rate limiting
Regex misses formatsIncomplete linksUse LLM as primary, regex as fallback

Estimated Effort

Phase 1 (Core Decorator):

  • Development: 4 hours
  • Testing: 2 hours
  • Documentation: 1 hour
  • Total: 1 day

Phase 2 (Advanced Patterns):

  • Development: 4 hours
  • Testing: 2 hours
  • Total: 1 day

Phase 3 (Optimization):

  • Caching: 2 hours
  • Monitoring: 2 hours
  • Total: 0.5 day

Grand Total: 2.5 days


Success Criteria

Must Have (Phase 1):

  • Page references become clickable links
  • Links navigate to correct page
  • No increase in errors
  • <500ms decoration latency

Should Have (Phase 2):

  • Detail references linkable
  • Code section references linkable
  • 95%+ accuracy rate

Nice to Have (Phase 3):

  • Response caching
  • Batch operations
  • Preview on hover

Dependencies

  • ✅ Gemini 2.0 Flash API (already have)
  • ✅ marked.js (already installed)
  • ✅ Angular Router (already configured)
  • ✅ ChatContext (already available)

No new dependencies!


Files to Create

Backend (1 new file):

  • src/main/java/.../service/ChatResponseDecoratorAgent.java (~150 lines)

Backend (1 modified file):

  • src/main/java/.../service/ChatAgentService.java (add decorator call)

Environment (4 modified files):

  • env/dev/gcp/cloud-run/grpc/vars.yaml
  • env/demo/gcp/cloud-run/grpc/vars.yaml
  • env/test/gcp/cloud-run/grpc/vars.yaml
  • env/prod/gcp/cloud-run/grpc/vars.yaml

Tests (1 new file):

  • src/test/java/.../service/ChatResponseDecoratorAgentTest.java (~200 lines)

Deployment

Build

export JAVA_HOME=/usr/lib/jvm/temurin-23-jdk-arm64
mvn clean package -DskipTests

Deploy

cd env/test/gcp/cloud-run/grpc
./deploy.sh

Verify

  1. Ask: "Which pages cover second floor?"
  2. Response should have clickable links
  3. Click link → should navigate to page

Priority: Medium (nice enhancement, not blocking)
Complexity: Low (single-purpose agent, simple integration)
Estimated Effort: 2.5 days
Cost Impact: +$11/month (minimal)