From 45d8953562e98e35858c69259a44ef40c6a0930b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=A2=81=E7=AB=A0=E6=B4=AA?= Date: Tue, 11 Aug 2026 16:14:15 +0800 Subject: [PATCH] feat(agent): add further_questions to ConversationResponse The agent workflow_finished event's outputs carry a further_questions list (suggested follow-ups, "you might also ask") that the SDK dropped: WorkflowOutputs only modeled answer and references, so the field was lost on both the streamed WorkflowFinished outcome and the folded ConversationResponse. Add further_questions: Option> to the core WorkflowOutputs and ConversationResponse, thread it through from_stream_parts, and expose it across every binding (Python, Node.js, Java, C) mirroring how the existing string-list fields are surfaced. The C header is regenerated by cbindgen. --- c/csrc/include/longbridge.h | 8 +++++++ c/src/agent_context/types.rs | 10 ++++++++ .../agent/ConversationResponse.java | 15 ++++++++++-- java/src/types/classes.rs | 13 ++++++---- nodejs/index.d.ts | 2 ++ nodejs/src/agent/types.rs | 3 +++ python/pysrc/longbridge/openapi.pyi | 2 ++ python/src/agent/types.rs | 3 +++ rust/src/agent/types.rs | 24 ++++++++++++++++++- 9 files changed, 73 insertions(+), 7 deletions(-) diff --git a/c/csrc/include/longbridge.h b/c/csrc/include/longbridge.h index 6bebad651..b62e7183b 100644 --- a/c/csrc/include/longbridge.h +++ b/c/csrc/include/longbridge.h @@ -2610,6 +2610,14 @@ typedef struct lb_conversation_response_t { * Number of references */ uintptr_t num_references; + /** + * Suggested follow-up questions ("you might also ask"); empty when absent + */ + const char *const *further_questions; + /** + * Number of follow-up questions + */ + uintptr_t num_further_questions; /** * Run duration in seconds */ diff --git a/c/src/agent_context/types.rs b/c/src/agent_context/types.rs index f98b22e80..53fe8b157 100644 --- a/c/src/agent_context/types.rs +++ b/c/src/agent_context/types.rs @@ -496,6 +496,10 @@ pub struct CConversationResponse { pub references: *const CReference, /// Number of references pub num_references: usize, + /// Suggested follow-up questions ("you might also ask"); empty when absent + pub further_questions: *const *const c_char, + /// Number of follow-up questions + pub num_further_questions: usize, /// Run duration in seconds pub elapsed_time: f64, /// Present only when `status` is `ConversationStatusInterrupted` (can be @@ -511,6 +515,7 @@ pub(crate) struct CConversationResponseOwned { status: CConversationStatus, answer: CString, references: CVec, + further_questions: CVec, elapsed_time: f64, interrupt: Option>, error: Option>, @@ -524,6 +529,7 @@ impl From for CConversationResponseOwned { status, answer, references, + further_questions, elapsed_time, interrupt, error, @@ -537,6 +543,7 @@ impl From for CConversationResponseOwned { // distinction between "absent" and "empty" here, both surface as // `num_references == 0`. references: references.unwrap_or_default().into(), + further_questions: further_questions.unwrap_or_default().into(), elapsed_time, interrupt: interrupt.map(CCow::new), error: error.map(CCow::new), @@ -554,6 +561,7 @@ impl ToFFI for CConversationResponseOwned { status, answer, references, + further_questions, elapsed_time, interrupt, error, @@ -565,6 +573,8 @@ impl ToFFI for CConversationResponseOwned { answer: answer.to_ffi_type(), references: references.to_ffi_type(), num_references: references.len(), + further_questions: further_questions.to_ffi_type(), + num_further_questions: further_questions.len(), elapsed_time: *elapsed_time, interrupt: interrupt .as_ref() diff --git a/java/javasrc/src/main/java/com/longbridge/agent/ConversationResponse.java b/java/javasrc/src/main/java/com/longbridge/agent/ConversationResponse.java index d7c6f2335..e4064b7b9 100644 --- a/java/javasrc/src/main/java/com/longbridge/agent/ConversationResponse.java +++ b/java/javasrc/src/main/java/com/longbridge/agent/ConversationResponse.java @@ -13,6 +13,7 @@ public class ConversationResponse { private ConversationStatus status; private String answer; private Reference[] references; + private String[] furtherQuestions; private double elapsedTime; private Interrupt interrupt; private ConversationError error; @@ -64,6 +65,15 @@ public Reference[] getReferences() { return references; } + /** + * Returns the suggested follow-up questions ("you might also ask"). + * + * @return suggested follow-up questions + */ + public String[] getFurtherQuestions() { + return furtherQuestions; + } + /** * Returns the run duration in seconds. * @@ -95,7 +105,8 @@ public ConversationError getError() { @Override public String toString() { return "ConversationResponse [chatUid=" + chatUid + ", messageId=" + messageId + ", status=" + status - + ", answer=" + answer + ", references=" + Arrays.toString(references) + ", elapsedTime=" - + elapsedTime + ", interrupt=" + interrupt + ", error=" + error + "]"; + + ", answer=" + answer + ", references=" + Arrays.toString(references) + ", furtherQuestions=" + + Arrays.toString(furtherQuestions) + ", elapsedTime=" + elapsedTime + ", interrupt=" + interrupt + + ", error=" + error + "]"; } } diff --git a/java/src/types/classes.rs b/java/src/types/classes.rs index ba5814148..522a485ab 100644 --- a/java/src/types/classes.rs +++ b/java/src/types/classes.rs @@ -3315,16 +3315,18 @@ impl_java_class!( ); /// JNI-side view of [`longbridge::agent::ConversationResponse`], with -/// `references` normalized from `Option>` down to a plain -/// `Vec` (empty when absent) so it can use the same `#[java(objarray)]` -/// convention as every other list field — mirrors how `StockPosition` above -/// collapses `Option`/`Option` fields with `unwrap_or_default`. +/// `references`/`further_questions` normalized from `Option>` down to +/// a plain `Vec` (empty when absent) so they can use the same +/// `#[java(objarray)]` convention as every other list field — mirrors how +/// `StockPosition` above collapses `Option`/`Option` fields +/// with `unwrap_or_default`. pub(crate) struct ConversationResponse { pub(crate) chat_uid: String, pub(crate) message_id: String, pub(crate) status: longbridge::agent::ConversationStatus, pub(crate) answer: String, pub(crate) references: Vec, + pub(crate) further_questions: Vec, pub(crate) elapsed_time: f64, pub(crate) interrupt: Option, pub(crate) error: Option, @@ -3338,6 +3340,7 @@ impl From for ConversationResponse { status: value.status, answer: value.answer, references: value.references.unwrap_or_default(), + further_questions: value.further_questions.unwrap_or_default(), elapsed_time: value.elapsed_time, interrupt: value.interrupt, error: value.error, @@ -3355,6 +3358,8 @@ impl_java_class!( answer, #[java(objarray)] references, + #[java(objarray)] + further_questions, elapsed_time, interrupt, error diff --git a/nodejs/index.d.ts b/nodejs/index.d.ts index df7cf209c..f66ef7129 100644 --- a/nodejs/index.d.ts +++ b/nodejs/index.d.ts @@ -4063,6 +4063,8 @@ export interface ConversationResponse { answer: string /** Sources referenced by the answer */ references?: Array + /** Suggested follow-up questions */ + furtherQuestions?: Array /** Run duration in seconds */ elapsedTime: number /** Present only when `status` is `interrupted` */ diff --git a/nodejs/src/agent/types.rs b/nodejs/src/agent/types.rs index 9eead0979..d67b07b08 100644 --- a/nodejs/src/agent/types.rs +++ b/nodejs/src/agent/types.rs @@ -240,6 +240,8 @@ pub struct ConversationResponse { pub answer: String, /// Sources referenced by the answer pub references: Option>, + /// Suggested follow-up questions + pub further_questions: Option>, /// Run duration in seconds pub elapsed_time: f64, /// Present only when `status` is `interrupted` @@ -257,6 +259,7 @@ impl From for ConversationResponse { references: v .references .map(|refs| refs.into_iter().map(Into::into).collect()), + further_questions: v.further_questions, elapsed_time: v.elapsed_time, interrupt: v.interrupt.map(Into::into), error: v.error.map(Into::into), diff --git a/python/pysrc/longbridge/openapi.pyi b/python/pysrc/longbridge/openapi.pyi index b1e0b2ca7..70e5d1353 100644 --- a/python/pysrc/longbridge/openapi.pyi +++ b/python/pysrc/longbridge/openapi.pyi @@ -13481,6 +13481,8 @@ class ConversationResponse: """Final answer text; valid when status is ConversationStatus.Succeeded""" references: list[Reference] | None """Sources referenced by the answer""" + further_questions: list[str] | None + """Suggested follow-up questions ("you might also ask")""" elapsed_time: float """Run duration in seconds""" interrupt: Interrupt | None diff --git a/python/src/agent/types.rs b/python/src/agent/types.rs index b727686d8..0f0774c45 100644 --- a/python/src/agent/types.rs +++ b/python/src/agent/types.rs @@ -218,6 +218,8 @@ pub(crate) struct ConversationResponse { pub status: ConversationStatus, pub answer: String, pub references: Option>, + /// Suggested follow-up questions ("you might also ask") + pub further_questions: Option>, pub elapsed_time: f64, pub interrupt: Option, pub error: Option, @@ -233,6 +235,7 @@ impl From for ConversationResponse { references: v .references .map(|refs| refs.into_iter().map(Into::into).collect()), + further_questions: v.further_questions, elapsed_time: v.elapsed_time, interrupt: v.interrupt.map(Into::into), error: v.error.map(Into::into), diff --git a/rust/src/agent/types.rs b/rust/src/agent/types.rs index 9608fe262..6e4224952 100644 --- a/rust/src/agent/types.rs +++ b/rust/src/agent/types.rs @@ -222,6 +222,10 @@ pub struct ConversationResponse { /// Sources referenced by the answer #[serde(default)] pub references: Option>, + /// Suggested follow-up questions ("you might also ask"); present when the + /// run produced them + #[serde(default)] + pub further_questions: Option>, /// Run duration in seconds #[serde(default)] pub elapsed_time: f64, @@ -252,6 +256,7 @@ impl ConversationResponse { status: payload.status, answer: payload.outputs.answer.unwrap_or_default(), references: payload.outputs.references, + further_questions: payload.outputs.further_questions, elapsed_time: payload.elapsed_time, interrupt: None, error, @@ -277,6 +282,7 @@ impl ConversationResponse { status: ConversationStatus::Interrupted, answer: String::new(), references: None, + further_questions: None, elapsed_time: 0.0, interrupt: Some(interrupt), error: None, @@ -340,6 +346,10 @@ pub struct WorkflowOutputs { /// Sources referenced by the answer #[serde(default)] pub references: Option>, + /// Suggested follow-up questions ("you might also ask"); present when the + /// run produced them + #[serde(default)] + pub further_questions: Option>, } /// Payload of a `workflow_finished` SSE event. `status` is never @@ -1065,7 +1075,7 @@ mod tests { #[test] fn deserialize_workflow_finished_payload() { - let json = r#"{"status":"succeeded","elapsed_time":3.21,"outputs":{"answer":"Tesla (TSLA.US) recently..."}}"#; + let json = r#"{"status":"succeeded","elapsed_time":3.21,"outputs":{"answer":"Tesla (TSLA.US) recently...","further_questions":["What is Tesla's P/E?","How did Q3 deliveries look?"]}}"#; let payload: WorkflowFinishedPayload = serde_json::from_str(json).unwrap(); assert_eq!(payload.status, ConversationStatus::Succeeded); assert!((payload.elapsed_time - 3.21).abs() < f64::EPSILON); @@ -1073,6 +1083,16 @@ mod tests { payload.outputs.answer.as_deref(), Some("Tesla (TSLA.US) recently...") ); + assert_eq!( + payload.outputs.further_questions.as_deref(), + Some( + [ + "What is Tesla's P/E?".to_string(), + "How did Q3 deliveries look?".to_string(), + ] + .as_slice() + ) + ); let resp = ConversationResponse::from_stream_parts( Some(("ct_9f2c1a5b".to_string(), "42".to_string())), @@ -1081,6 +1101,8 @@ mod tests { assert_eq!(resp.chat_uid, "ct_9f2c1a5b"); assert_eq!(resp.message_id, "42"); assert_eq!(resp.answer, "Tesla (TSLA.US) recently..."); + // Follow-up questions thread through the folded response. + assert_eq!(resp.further_questions.as_ref().unwrap().len(), 2); assert!(resp.interrupt.is_none()); assert!(resp.error.is_none()); }