-
Notifications
You must be signed in to change notification settings - Fork 28
Scott/follow up fixes #386
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
03f3d26
d5e6188
ffcc1c4
e985e25
fcd56b6
36ec563
70d2a1a
50e2663
821ba04
bb3b153
69cbb99
95112d2
a624de5
ae5aaf0
6c823e3
f34c5b1
38fca5c
0a12e54
d1496bc
d5462fb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,10 @@ | ||
| import { PageHome } from '~/components/scopes/page_home/page_home'; | ||
| import { EXAMPLE_PROMPTS } from '~/configs/example_prompts'; | ||
| import { shuffleArray } from '~/functions/shuffle_array'; | ||
|
|
||
| export default PageHome; | ||
| const Page = () => { | ||
| const prompts = shuffleArray(EXAMPLE_PROMPTS).slice(0, 4); | ||
| return <PageHome examplePrompts={prompts} />; | ||
| }; | ||
|
|
||
| export default Page; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,7 +4,10 @@ | |
|
|
||
| import { nanoid } from 'nanoid'; | ||
| import type { NextRequest } from 'next/server'; | ||
| import { EXAMPLE_PROMPTS } from '~/configs/example_prompts'; | ||
| import { deduplicateStrings } from '~/functions/deduplicate_strings'; | ||
| import { extractJson } from '~/functions/extract_json'; | ||
| import { shuffleArray } from '~/functions/shuffle_array'; | ||
| import { fetchGeminiTools, runToolLoop } from '~/server/steps/data_discovery'; | ||
| import { fetchTimeSeries } from '~/server/steps/observations'; | ||
| import { parseQuery } from '~/server/steps/parse_query'; | ||
|
|
@@ -51,6 +54,7 @@ export async function POST(request: NextRequest) { | |
| const selectedEntityDcids = Array.isArray(body?.selectedEntityDcids) | ||
| ? body.selectedEntityDcids | ||
| : []; | ||
| const followUpContext = body?.followUpContext ?? undefined; | ||
|
|
||
| if (typeof query !== 'string' || !query.trim()) { | ||
| return new Response(JSON.stringify({ error: 'Missing or invalid query' }), { | ||
|
|
@@ -81,10 +85,15 @@ export async function POST(request: NextRequest) { | |
| const safetyResult = await checkPromptSafety(query); | ||
| if (!safetyResult.allowed) { | ||
| emit({ | ||
| type: STREAM_EVENT.error, | ||
| message: | ||
| safetyResult.reason || 'Query was blocked by safety filter.', | ||
| type: STREAM_EVENT.followUp, | ||
| data: { | ||
| summary: "I'm not able to answer that question.", | ||
| question: | ||
| 'Try one of these suggestions, or type a new question about data.', | ||
| options: shuffleArray(EXAMPLE_PROMPTS).slice(0, 4), | ||
| }, | ||
| }); | ||
| emit({ type: STREAM_EVENT.complete, message: STATUS.complete }); | ||
| controller.close(); | ||
| return; | ||
| } | ||
|
|
@@ -95,15 +104,16 @@ export async function POST(request: NextRequest) { | |
| query, | ||
| atlasContext, | ||
| ancestorChainLength: ancestorChain.length, | ||
| followUpContext, | ||
| }); | ||
|
|
||
| // Resolve places | ||
| let places = parsed.places; | ||
| if (parsed.isFollowUp && places.length === 0) { | ||
| places = | ||
| selectedEntityDcids.length > 0 ? selectedEntityDcids : [query]; | ||
| places = selectedEntityDcids.length > 0 ? selectedEntityDcids : []; | ||
| } | ||
| if (places.length === 0) places = [query]; | ||
| const noExplicitPlace = places.length === 0; | ||
| if (noExplicitPlace) places = ['Earth']; | ||
| parsed.places = places; | ||
|
|
||
| emit({ type: STREAM_EVENT.parsedQuery, data: parsed }); | ||
|
|
@@ -112,6 +122,14 @@ export async function POST(request: NextRequest) { | |
| return; | ||
| } | ||
|
|
||
| // ─── Early exit: parse_query returned a followUp ───────────────────── | ||
| if (parsed.followUp) { | ||
| emit({ type: STREAM_EVENT.followUp, data: parsed.followUp }); | ||
| emit({ type: STREAM_EVENT.complete, message: STATUS.complete }); | ||
| controller.close(); | ||
| return; | ||
| } | ||
|
|
||
| // ─── Data Discovery ────────────────────────────────────────────────── | ||
|
|
||
| // Fetch tools | ||
|
|
@@ -121,6 +139,14 @@ export async function POST(request: NextRequest) { | |
| }); | ||
| const geminiTools = await fetchGeminiTools(signal); | ||
|
|
||
| // Accumulator: collect disambiguation/follow-up signals across places. | ||
| // After the loop we synthesize at most one follow-up for the whole round. | ||
| const disambiguationEntries: Array<{ | ||
| place: string; | ||
| followUp: FollowUp; | ||
| }> = []; | ||
| let emittedResultCount = 0; | ||
|
|
||
| // Process each place | ||
| for (let i = 0; i < places.length; i++) { | ||
| if (signal.aborted) break; | ||
|
|
@@ -152,6 +178,8 @@ export async function POST(request: NextRequest) { | |
| parsed, | ||
| atlasContext, | ||
| ancestorChain, | ||
| followUpContext, | ||
| noExplicitPlace, | ||
| geminiTools, | ||
| signal, | ||
| onToolCall: (e) => { | ||
|
|
@@ -195,7 +223,18 @@ export async function POST(request: NextRequest) { | |
| continue; | ||
| } | ||
| const variables = parsedResponse.variables || []; | ||
| if (variables.length === 0 && !parsedResponse.followUp) { | ||
|
|
||
| // If the model returned a follow-up signal (disambiguation / fallback), | ||
| // stash it for post-loop synthesis instead of emitting per-place. | ||
| if (parsedResponse.followUp && variables.length === 0) { | ||
| disambiguationEntries.push({ | ||
| place, | ||
| followUp: parsedResponse.followUp, | ||
| }); | ||
| continue; | ||
| } | ||
|
|
||
| if (variables.length === 0) { | ||
| emit({ | ||
| type: STREAM_EVENT.placeSkipped, | ||
| place, | ||
|
|
@@ -204,7 +243,7 @@ export async function POST(request: NextRequest) { | |
| continue; | ||
| } | ||
| const entityDcid = parsedResponse.placeDcid || resolvedPlaceDcid; | ||
| if (!entityDcid && !parsedResponse.followUp) { | ||
| if (!entityDcid) { | ||
| emit({ | ||
| type: STREAM_EVENT.placeSkipped, | ||
| place, | ||
|
|
@@ -219,16 +258,12 @@ export async function POST(request: NextRequest) { | |
| message: STATUS.loadingTimeSeries(placeLabel, variables.length), | ||
| }); | ||
|
|
||
| const timeSeries = entityDcid | ||
| ? await Promise.all( | ||
| variables.map((v) => | ||
| fetchTimeSeries(v.dcid, entityDcid, signal), | ||
| ), | ||
| ) | ||
| : []; | ||
| const entities = entityDcid | ||
| ? [{ dcid: entityDcid, name: parsedResponse.placeName || place }] | ||
| : []; | ||
| const timeSeries = await Promise.all( | ||
| variables.map((v) => fetchTimeSeries(v.dcid, entityDcid, signal)), | ||
| ); | ||
| const entities = [ | ||
| { dcid: entityDcid, name: parsedResponse.placeName || place }, | ||
| ]; | ||
| const discoveryResult: QueryResult = { | ||
| id: nanoid(), | ||
| title: | ||
|
|
@@ -246,7 +281,6 @@ export async function POST(request: NextRequest) { | |
| coverage: parsedResponse.coverage, | ||
| insights: parsedResponse.insights, | ||
| relatedQueries: parsedResponse.relatedQueries, | ||
| followUp: parsedResponse.followUp, | ||
| }; | ||
|
|
||
| const { tableHtml, notesHtml } = renderResultHtml(discoveryResult); | ||
|
|
@@ -258,6 +292,52 @@ export async function POST(request: NextRequest) { | |
| result: discoveryResult, | ||
| place, | ||
| }); | ||
| emittedResultCount++; | ||
| } | ||
|
|
||
| // ─── Post-loop: synthesize a single follow-up if needed ────────── | ||
| const firstDisambiguation = disambiguationEntries[0]; | ||
| if (firstDisambiguation) { | ||
| // Merge all disambiguation signals into one follow-up. | ||
| // Use the first entry as the base and combine options. | ||
| const base = firstDisambiguation.followUp; | ||
| const mergedFollowUp: FollowUp = { | ||
| summary: base.summary, | ||
| question: base.question, | ||
| options: base.options, | ||
| }; | ||
|
|
||
| // If multiple places triggered disambiguation, merge summaries. | ||
| if (disambiguationEntries.length > 1) { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I worry that this will result in a long list of potentially repetitive or disjointed text, as well as duplicated options. Perhaps this should be run through a follow-up synthesis pass through the LLM to make it a single, coherent summary. However, I would put that aside for now until we have a chance to experiement. Separately (and more actionable right away), right now we are not dedupping, which would help (but not totally remove the issue, because the two summaries might have similar, but not identical follow-up options).
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I added deduping, but I think you're right this could warrant another llm pass |
||
| mergedFollowUp.summary = deduplicateStrings( | ||
| disambiguationEntries | ||
| .map((e) => e.followUp.summary) | ||
| .filter(Boolean), | ||
| ).join(' '); | ||
| mergedFollowUp.options = deduplicateStrings( | ||
| disambiguationEntries.flatMap((e) => e.followUp.options), | ||
| ).slice(0, 4); | ||
| } | ||
|
|
||
| // Strip options when no explicit place was provided — | ||
| // the question should only ask the user to specify a place. | ||
| if (noExplicitPlace) { | ||
| mergedFollowUp.options = []; | ||
| } | ||
|
|
||
| emit({ type: STREAM_EVENT.followUp, data: mergedFollowUp }); | ||
| } else if (emittedResultCount === 0) { | ||
| // Invariant: never emit nothing. If no results and no disambiguation, | ||
| // send a generic fallback follow-up. | ||
| emit({ | ||
| type: STREAM_EVENT.followUp, | ||
| data: { | ||
| summary: '', | ||
| question: | ||
| 'I wasn\u2019t able to find relevant data for your query. Could you try rephrasing or being more specific about a place or topic?', | ||
| options: [], | ||
| }, | ||
| }); | ||
| } | ||
|
|
||
| emit({ type: STREAM_EVENT.complete, message: STATUS.complete }); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,8 @@ | ||
| .container { | ||
| display: flex; | ||
| flex-shrink: 0; | ||
| flex-flow: column; | ||
| gap: 8px; | ||
| align-items: flex-start; | ||
| padding: 0 28px 18px; | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.