Yield answer events with local citations and provider-native chunks.
(
user_id: str,
question: str,
*,
limit: int = 8,
pinned_nodes: Optional[List[Dict[str, Any]]] = None,
allowed_node_ids: Optional[Iterable[str]] = None,
response_language: str = "zh",
)
| 254 | |
| 255 | |
| 256 | def answer_question_stream( |
| 257 | user_id: str, |
| 258 | question: str, |
| 259 | *, |
| 260 | limit: int = 8, |
| 261 | pinned_nodes: Optional[List[Dict[str, Any]]] = None, |
| 262 | allowed_node_ids: Optional[Iterable[str]] = None, |
| 263 | response_language: str = "zh", |
| 264 | ) -> Iterator[Dict[str, Any]]: |
| 265 | """Yield answer events with local citations and provider-native chunks.""" |
| 266 | language = _normalize_response_language(response_language) |
| 267 | started = time.time() |
| 268 | hits, embedding_error = _prepare_hits(user_id, question, limit, pinned_nodes, allowed_node_ids=allowed_node_ids) |
| 269 | if not hits: |
| 270 | result = _empty_answer(started, response_language=language) |
| 271 | yield {"event": "meta", "data": {key: value for key, value in result.items() if key != "text"}} |
| 272 | yield {"event": "chunk", "data": {"text": result["text"]}} |
| 273 | yield {"event": "done", "data": result} |
| 274 | return |
| 275 | |
| 276 | citations = _build_citations(user_id, hits) |
| 277 | llm = build_llm_provider() |
| 278 | prompt = _build_prompt(question, hits) |
| 279 | llm_error = None |
| 280 | text_parts: List[str] = [] |
| 281 | meta = { |
| 282 | "citations": citations, |
| 283 | "elapsed_ms": 0, |
| 284 | "response_language": language, |
| 285 | "token_usage": _token_usage(llm, None, embedding_error, None), |
| 286 | "streaming": {"provider": True, "transport": "sse"}, |
| 287 | } |
| 288 | yield {"event": "meta", "data": meta} |
| 289 | try: |
| 290 | for chunk in llm.stream_generate(prompt, system=_system_prompt(language), temperature=0.0, max_tokens=_answer_max_tokens()): |
| 291 | if not chunk: |
| 292 | continue |
| 293 | text_parts.append(chunk) |
| 294 | yield {"event": "chunk", "data": {"text": chunk}} |
| 295 | except Exception as exc: |
| 296 | llm_error = str(exc) |
| 297 | fallback = _extractive_fallback(hits, llm_error, response_language=language) |
| 298 | text_parts = [fallback] |
| 299 | yield {"event": "chunk", "data": {"text": fallback}} |
| 300 | |
| 301 | answer_text = "".join(text_parts) |
| 302 | response = None |
| 303 | if llm_error is None and _looks_incomplete_answer(answer_text): |
| 304 | try: |
| 305 | response = llm.generate(prompt, system=_system_prompt(language), temperature=0.0, max_tokens=_answer_max_tokens()) |
| 306 | if response.text and len(response.text.strip()) > len(answer_text.strip()): |
| 307 | answer_text = response.text |
| 308 | except Exception as exc: |
| 309 | llm_error = f"incomplete stream fallback failed: {exc}" |
| 310 | |
| 311 | result = { |
| 312 | "text": answer_text, |
| 313 | "citations": citations, |
nothing calls this directly
no test coverage detected