Repository navigation
Reports improvements #682
Description
Intelligence executive summary should be a AI generated summary of all chats in the system at the time in context.
Place the sensor fusion prior to the strategic output.
The strategic output should be focused on the new knowledge derived from the exploration and not a replica of the sensor fusion.
Perhaps create functions to deploy the multi-agents to synthesize the report.
Based on the codebase context, you're asking about the intelligence report generation system in QCX, specifically the ReportTemplate component and the generateReportContext function. Here's the current implementation and how it relates to your requirements:
Current Implementation
The intelligence executive summary is currently generated by the generateReportContext function in lib/actions/chat.ts, which uses a single AI call to synthesize a title and 150-200 word summary from the chat messages 1 . This is called from the DownloadReportButton component when the user triggers report generation 2 .
The ReportTemplate currently structures the report as:
- Intelligence Executive Summary (section "00") - displays the AI-generated summary 3
- Executive Visualization (section "01") - displays the map snapshot 4
- Intelligence Assessment (section "02") - contains both sensor fusion results and strategic output mixed together 5
Addressing Your Requirements
1. AI-Generated Summary of All Chats
The current generateReportContext already generates an AI summary from chat messages 6 , but it filters to only include user messages and assistant responses. To include all chats in context, you would need to modify the filtering logic in this function.
2. Sensor Fusion Prior to Strategic Output
Currently, sensor fusion (resolution search results) and strategic output (regular responses) are interleaved in the Intelligence Assessment section based on message order 7 . To place sensor fusion prior to strategic output, you would need to restructure the filtering and rendering logic in ReportTemplate to group all resolution_search_result messages before response messages.
3. Strategic Output Focused on New Knowledge
The current writer agent generates concise responses based on search results 8 , but there's no specific logic to ensure strategic output doesn't replicate sensor fusion findings. You would need to modify the writer agent's system prompt or create a new agent specifically for strategic synthesis that explicitly avoids duplicating sensor fusion content.
4. Multi-Agent Report Synthesis
Currently, report generation uses a single generateText call in generateReportContext 9 . To deploy multi-agents for report synthesis, you could create a new orchestration function similar to the existing agent pipeline (taskManager → researcher → writer) that specifically handles report generation with specialized agents for different sections.
Notes
The current report template filters out short transition messages (under 500 characters) to reduce redundancy 10 , but this doesn't address the specific requirement of ensuring strategic output focuses on new knowledge rather than replicating sensor fusion. The system already has a sophisticated multi-agent architecture for query processing 11 , but this is not currently leveraged for report synthesis.
Wiki pages you might want to explore:
- AI Agent Orchestration (QueueLab/QCX)
- Additional UI Components (QueueLab/QCX)
- Glossary (QueueLab/QCX)
Citations
File: lib/actions/chat.ts (L25-84)
export async function generateReportContext(messages: AIMessage[]) {
try {
const model = await getModel()
const promptMessages = messages
.filter(msg => msg.role === 'user' || (msg.role === 'assistant' && msg.type === 'response'))
.map(msg => {
const role = msg.role === 'user' ? 'user' as const : 'assistant' as const
const rawContent =
typeof msg.content === 'string'
? msg.content
: Array.isArray(msg.content)
? msg.content.map(p => (p && typeof p === 'object' && 'type' in p && p.type === 'text') ? p.text : '').join('\n')
: JSON.stringify(msg.content)
// Sanitize: strip huge base64 images if any are still in there
const content = rawContent
.replace(/data:image\/[a-zA-Z0-9.+-]+;base64,[A-Za-z0-9+/=]+/g, '[image omitted]')
.trim()
return { role, content }
})
.filter(msg => msg.content.length > 0)
const { text } = await generateText({
model,
system: `You are a high-level geospatial intelligence analyst. Based on the provided conversation, generate:
1. A professional, concise report title (max 60 characters).
2. A 150-200 word executive summary that synthesizes the intelligence findings, observations, and spatial analysis discussed.
Format your response as a JSON object:
{
"title": "The Title Here",
"summary": "The executive summary here..."
}
Do not include any other text or markdown formatting in your response.`,
messages: promptMessages as any,
})
try {
return JSON.parse(text) as { title: string; summary: string }
} catch (e) {
console.error('Failed to parse AI response for report context', {
error: e instanceof Error ? e.message : String(e),
preview: text.slice(0, 200)
})
// Fallback
return {
title: 'QCX Intelligence Analysis',
summary: 'Executive summary generation failed, but manual review of the intelligence assessment is recommended.'
}
}
} catch (error) {
console.error('Error generating report context:', error)
return {
title: 'QCX Intelligence Analysis',
summary: 'Automated executive summary is currently unavailable.'
}
}
}File: components/download-report-button.tsx (L56-64)
const formData = new FormData();
formData.append('action', 'generate_report_context');
formData.append('messages', JSON.stringify(aiState.messages));
const { title, summary } = await (actions as any).submit(formData);
const finalTitle = title || 'QCX Intelligence Analysis'
setReportTitle(finalTitle)
setReportSummary(summary)File: components/report-template.tsx (L50-64)
const filteredMessages = rawFiltered.filter((message, index) => {
// If this is a response followed by a resolution search result,
// and the response is relatively short (likely a transition/redundant summary), filter it out.
if (message.type === 'response' && index + 1 < rawFiltered.length) {
const nextMessage = rawFiltered[index + 1]
if (nextMessage.role === 'assistant' && (nextMessage.type === 'resolution_search_result' || nextMessage.type === 'response')) {
const content = renderMessageContent(message.content)
// Filter out short transition messages or duplicated high-level summaries
if (content.length < 500) {
return false
}
}
}
return true
})File: components/report-template.tsx (L109-123)
{aiSummary && (
<section className="break-inside-avoid">
<div className="flex items-baseline gap-4 mb-8 border-b border-slate-100 pb-4">
<span className="text-4xl font-black text-slate-200">00</span>
<h2 className="text-2xl font-black text-[#003366] uppercase tracking-tight">Intelligence Executive Summary</h2>
</div>
<div className="bg-slate-50 p-10 rounded-2xl border-l-8 border-[#003366] shadow-inner">
<div className="prose prose-slate prose-lg max-w-none text-slate-800 font-medium leading-relaxed">
<ReactMarkdown remarkPlugins={[remarkGfm]}>
{aiSummary}
</ReactMarkdown>
</div>
</div>
</section>
)}File: components/report-template.tsx (L125-138)
{mapSnapshot && (
<section className="break-inside-avoid">
<div className="flex items-baseline gap-4 mb-8 border-b border-slate-100 pb-4">
<span className="text-4xl font-black text-slate-200">01</span>
<h2 className="text-2xl font-black text-[#003366] uppercase tracking-tight">Executive Visualization</h2>
</div>
<div className="border-[12px] border-slate-50 rounded-2xl overflow-hidden shadow-2xl">
<img src={mapSnapshot} alt="Map Snapshot" className="w-full h-auto block" crossOrigin="anonymous" />
<div className="bg-slate-50 px-6 py-4 text-xs text-slate-500 text-center font-bold tracking-wide border-t border-slate-100">
ORBITAL PERSPECTIVE REF: {Math.random().toString(16).substring(2, 10).toUpperCase()}
</div>
</div>
</section>
)}File: components/report-template.tsx (L140-225)
<section>
<div className="flex items-baseline gap-4 mb-12 border-b border-slate-100 pb-4">
<span className="text-4xl font-black text-slate-200">02</span>
<h2 className="text-2xl font-black text-[#003366] uppercase tracking-tight">Intelligence Assessment</h2>
</div>
<div className="space-y-16">
{filteredMessages.map((message, index) => {
const contentString = renderMessageContent(message.content)
if (message.type === 'input' || message.type === 'input_related') {
let content = ''
try {
const json = JSON.parse(contentString)
content = message.type === 'input' ? (json.input || contentString) : (json.related_query || contentString)
} catch (e) {
content = contentString
}
return (
<div key={index} className="bg-[#003366] p-10 rounded-2xl shadow-xl break-inside-avoid relative overflow-hidden group">
<div className="absolute top-0 right-0 p-4">
<svg className="w-8 h-8 text-white opacity-10" fill="currentColor" viewBox="0 0 24 24"><path d="M14.017 21L14.017 18C14.017 16.8954 14.9124 16 16.017 16H19.017C19.5693 16 20.017 15.5523 20.017 15V9C20.017 8.44772 19.5693 8 19.017 8H16.017C14.9124 8 14.017 7.10457 14.017 6V3L14.017 3C14.017 2.44772 14.4647 2 15.017 2H21.017C21.5693 2 22.017 2.44772 22.017 3V15C22.017 18.3137 19.3307 21 16.017 21H14.017ZM3.017 21L3.017 18C3.017 16.8954 3.91243 16 5.017 16H8.017C8.56928 16 9.017 15.5523 9.017 15V9C9.017 8.44772 8.56928 8 8.017 8H5.017C3.91243 8 3.017 7.10457 3.017 6V3L3.017 3C3.017 2.44772 3.46472 2 4.017 2H10.017C10.5693 2 11.017 2.44772 11.017 3V15C11.017 18.3137 8.33072 21 5.017 21H3.017Z" /></svg>
</div>
<p className="text-[10px] font-black text-slate-300 uppercase tracking-[0.2em] mb-4">Primary Inquiry</p>
<p className="text-2xl text-white font-bold leading-tight tracking-tight italic">
{content}
</p>
</div>
)
} else if (message.type === 'response') {
return (
<div key={index} className="prose prose-slate prose-lg max-w-none break-inside-avoid">
<div className="flex items-center gap-3 mb-6">
<div className="w-1.5 h-6 bg-emerald-500"></div>
<p className="text-xs font-black text-emerald-700 uppercase tracking-[0.2em] m-0">Strategic Output</p>
</div>
<div className="text-slate-700 leading-relaxed font-medium">
<ReactMarkdown remarkPlugins={[remarkGfm]}>
{contentString}
</ReactMarkdown>
</div>
</div>
)
} else if (message.type === 'resolution_search_result') {
try {
const result = JSON.parse(contentString)
return (
<div key={index} className="space-y-8 bg-slate-50 p-12 rounded-3xl border border-slate-100 break-inside-avoid">
<div className="flex items-center justify-between">
<p className="text-xs font-black text-indigo-700 uppercase tracking-[0.2em]">Sensor Fusion Analysis</p>
<span className="px-2 py-0.5 bg-indigo-100 text-indigo-700 text-[9px] font-bold rounded">LEVEL 4 DATA</span>
</div>
{result.summary && (
<div className="text-slate-800 text-lg font-bold leading-snug bg-white p-8 rounded-2xl border border-slate-100 shadow-sm">
{result.summary}
</div>
)}
<div className="flex flex-col items-center gap-12">
{result.mapboxImage && (
<div className="space-y-4 w-full max-w-2xl">
<div className="flex items-center justify-center gap-2">
<div className="w-2 h-2 rounded-full bg-indigo-500"></div>
<p className="text-[10px] text-slate-400 font-black uppercase tracking-widest">Spectral RGB Analysis View</p>
</div>
<img src={result.mapboxImage} alt="Mapbox View" className="rounded-xl border-4 border-white shadow-xl w-full block mx-auto" crossOrigin="anonymous" />
</div>
)}
{result.googleImage && (
<div className="space-y-4 w-full max-w-2xl">
<div className="flex items-center justify-center gap-2">
<div className="w-2 h-2 rounded-full bg-orange-500"></div>
<p className="text-[10px] text-slate-400 font-black uppercase tracking-widest">High-Resolution Panchromatic Satellite</p>
</div>
<img src={result.googleImage} alt="Google Satellite" className="rounded-xl border-4 border-white shadow-xl w-full block mx-auto" crossOrigin="anonymous" />
</div>
)}
</div>
</div>
)
} catch (e) {
return null
}
}
return null
})}
</div>
</section>File: lib/agents/writer.tsx (L21-30)
// Default system prompt, used if dynamicSystemPrompt is not provided
const default_system_prompt = `As a professional writer, your job is to generate a comprehensive and informative, yet concise answer of 400 words or less for the given question based solely on the provided search results (URL and content). You must only use information from the provided search results. Use an unbiased and journalistic tone. Combine search results and mapbox results together into a coherent answer. Do not repeat text. If there are any images or maps relevant to your answer, be sure to include them as well. Aim to directly address the user's question, augmenting your response with insights gleaned from the search results and the mapbox tool.
Whenever quoting or referencing information from a specific URL, always cite the source URL explicitly. Please match the language of the response to the user's language.
Always answer in Markdown format. Links and images must follow the correct format.
Link format: [link text](url)
Image format: 
There are also some proconfigured example queires.
When asked about 'What is a planet computer?' answer with the following: '"A planet computer is a proprietary environment aware system that interoperates Climate forecasting, mapping and scheduling using cutting edge multi-agents to streamline automation and exploration on a planet'
`;
Activity
ngoiyaeric commentedon Jun 26, 2026
@coderabbitai plan this issue's fix and ensure that it does not break the code.
coderabbitai commentedon Jun 26, 2026
Coding Plan
Summary
getAllChats+ message normalization) so the executive summary reflects all of the user's chats, filtering by role since the DB does not persist messagetype.lib/agents/report/and an orchestration function that returns{ title, summary, strategicOutput }, with the strategic agent prompted to surface new knowledge while avoiding duplication of sensor-fusion findings.ReportTemplateto place Sensor Fusion before Strategic Output and render the synthesized strategic narrative, wiring the extended payload throughDownloadReportButtonwithout altering the PDF-capture pipeline.Design Choices
Design Choice 1: How should "all chats in the system" be sourced, given that the persisted `messages` table does not store the `type` discriminant?
Options Considered:
roleonly (user + assistant), and continue using the in-memorytypefor the active conversation; no schema change.typecolumn to themessagestable and backfill, enabling type-accurate filtering across all sessions.Chosen Option: 1
Rationale: It requires no migration/backfill and is sufficient because the executive summary only needs coherent conversational content (role-based filtering) rather than fine-grained type discrimination.
Design Choice 2: Where should the multi-agent report synthesis logic live?
Options Considered:
lib/agents/and orchestrate them from a server action, mirroring the existing researcher/writer pattern.generateReportContextas additionalgenerateTextcalls.Chosen Option: 1
Rationale: It aligns with the established agent architecture, keeps each synthesis concern isolated and reusable, and matches the ticket's "deploy the multi-agents" intent.
💡 User Tips
Regenerate the plan with different choices with
@coderabbitai <feedback>.Implementation Steps
Phase 1: Cross-Session Chat Aggregation
This phase establishes the ability to gather conversational context from all of the current user's chats, which the executive-summary agent will consume. The focus is on a server-side aggregation path that loops through paginated chats and normalizes their messages into a lean, LLM-ready form.
Task 1: Add an all-chats fetch helper
Provide a way to retrieve every chat for the current user rather than only the 20 most recent.
getAllChats(userId)server action inlib/actions/chat.tsthat resolves the user viagetCurrentUserIdOnServer()when no id is passed, then callsdbGetChatsPagefromlib/actions/chat-db.tsin a loop (incrementing offset) untilnextOffset === null.lib/actions/chat-db.ts; do not change its signature.Task 2: Build a cross-session message aggregation utility
Collect and normalize messages across all chats into a compact context payload.
lib/actions/chat.tsor a small new module underlib/actions/) that, for each chat from Task 1.1, callsgetChatMessages(chatId)and keeps onlyuserandassistantrows (role-based, since persisted rows lacktype).contentto plain text and strip base64 image data, reusing the same normalization/sanitization approach already present ingenerateReportContext.🤖 Prompt for AI agents
Phase 2: Multi-Agent Report Synthesis
This phase introduces dedicated synthesis agents and an orchestration function, replacing the single-call
generateReportContext. One agent produces the cross-session executive summary; another produces a strategic-output narrative that deliberately excludes content already covered by sensor fusion.Task 1: Create report synthesis agents
Add specialized agents following the existing
lib/agents/conventions.lib/agents/report/containing two agents:resolution_search_resultmessages) and theresponse-type strategic content, and produces a synthesized "new knowledge" narrative whose system prompt explicitly instructs it to focus on insights, implications, and decisions derived from exploration without restating sensor-fusion observations.getModel()andgenerateText(consistent with the current report path) and return structured JSON parsed defensively with safe fallbacks (mirroring the existing fallback behavior ingenerateReportContext).Task 2: Orchestrate the agents into a report-synthesis function
Coordinate the agents and expose a single result object.
generateReportContextinlib/actions/chat.ts(or add agenerateReportSynthesisfunction it delegates to) so it: (a) aggregates cross-session context via Phase 1, (b) invokes the executive-summary agent for{ title, summary }, (c) separates sensor-fusion vs. strategic content from the activeAIMessage[], and (d) invokes the strategic-synthesis agent for the de-duplicated strategic output.{ title, summary, strategicOutput }while keeping backward-compatible keys.submitrouting inapp/actions.tsx(action === 'generate_report_context') intact; only the returned shape and internal call path change.🤖 Prompt for AI agents
Phase 3: Report Template Restructuring and Wiring
This phase reorders the report sections so sensor fusion precedes strategic output, renders the newly synthesized strategic narrative, and threads the extended synthesis payload from the orchestrator through to the template.
Task 1: Reorder Intelligence Assessment to place Sensor Fusion before Strategic Output
Group resolution-search results ahead of strategic output in the rendered report.
components/report-template.tsxso the Intelligence Assessment section renders allresolution_search_result(Sensor Fusion) blocks first, followed by the strategic output, instead of interleaving by message order.data-pdf-nosplithandling used by the PDF generator; only the ordering/grouping logic changes.Task 2: Render the synthesized strategic output
Display the agent-generated "new knowledge" narrative as the Strategic Output.
components/report-template.tsxto accept and render the synthesizedstrategicOutputprop (markdown via the existingReactMarkdown/remark-gfmpath) under the "Strategic Output" label, replacing direct rendering of rawresponsemessages for that section.Task 3: Thread the extended synthesis payload through the client orchestrator
Pass the new fields from
submitinto the template.components/download-report-button.tsxto destructure{ title, summary, strategicOutput }from thesubmitresponse and passstrategicOutput(and the existing summary/title) as props to<ReportTemplate>.generatePDFReportinvocation.🤖 Prompt for AI agents
Research
Report generation is orchestrated client-side by
components/download-report-button.tsx, which captures the map canvas, calls thesubmitserver action withaction='generate_report_context', and renderscomponents/report-template.tsxoff-screen for PDF capture vialib/utils/report-generator.ts. Thesubmitaction (app/actions.tsx) routes report requests togenerateReportContextinlib/actions/chat.ts, which performs a singlegenerateTextcall (via the provider-fallbackgetModel()inlib/utils/index.ts) to produce{ title, summary }from the in-sessionAIMessage[]. The app already has a mature multi-agent pipeline inlib/agents/(taskManager → researcher → writer → querySuggestor) that hands data off through a shared message array. Chats are persisted via Drizzle ORM over PostgreSQL (lib/db/,lib/actions/chat-db.ts);getChatsis hardcoded to the 20 most recent chats, and the persistedmessagesrows storerole/contentbut not the in-memorytypediscriminant. User identity in server actions is resolved viagetCurrentUserIdOnServer()(lib/auth/get-current-user.ts).🚀 Next Steps
🤖 All AI agent prompts combined
💡 Iterate on the plan with:
`@coderabbitai` <feedback>💬 Have feedback or questions? Drop into our discord!