Download services/chat_runtime.py from globalsquiddatabase/squid-main-space: direct link, hf CLI and curl.
- Browser
- Download file 27.9 kB
-
https://huggingface.co/spaces/globalsquiddatabase/squid-main-space/resolve/13bbb15782a326638a40480abb4bd8ee1c355faf/services/chat_runtime.py
- Command line
-
hf download hf://spaces/globalsquiddatabase/squid-main-space@13bbb15782a326638a40480abb4bd8ee1c355faf/services/chat_runtime.py
-
curl -L -o chat_runtime.py https://huggingface.co/spaces/globalsquiddatabase/squid-main-space/resolve/13bbb15782a326638a40480abb4bd8ee1c355faf/services/chat_runtime.py
27.9 kB
| from __future__ import annotations | |
| # P0 chat runtime service. | |
| # Dependencies are injected once from ui_server during app assembly so the | |
| # large streaming/runtime logic is no longer owned by the entry module. | |
| from services.context_compressor import ( | |
| build_request_spec_from_messages, | |
| extract_request_spec, | |
| merge_request_spec, | |
| render_request_spec, | |
| ) | |
| def _inject(context): | |
| globals().update(context) | |
| async def stream_chat(tid,prompt): | |
| global last_llm_error | |
| usage_user_id=thread_user_ids.get(tid,"") | |
| pending_uploads=thread_upload_ids.pop(tid,[]) | |
| upload_context="" | |
| if usage_user_id and pending_uploads: | |
| upload_context=await build_user_upload_context( | |
| usage_user_id, | |
| pending_uploads, | |
| ) | |
| if upload_context: | |
| asyncio.create_task( | |
| safe_memory_event( | |
| usage_user_id, | |
| "attachment_used", | |
| { | |
| "thread_id":tid, | |
| "upload_ids":pending_uploads[:10], | |
| "count":len(pending_uploads[:10]), | |
| }, | |
| ) | |
| ) | |
| processing_context="" | |
| if ( | |
| usage_user_id | |
| and pending_uploads | |
| and _quality_check_requested(prompt) | |
| ): | |
| task_id="proc_"+secrets.token_hex(8) | |
| await safe_memory_event( | |
| usage_user_id, | |
| "processing_started", | |
| { | |
| "task_id":task_id, | |
| "thread_id":tid, | |
| "operation":"quality_check", | |
| "upload_ids": | |
| pending_uploads[:10], | |
| "status":"running", | |
| }, | |
| ) | |
| yield out( | |
| "status", | |
| { | |
| "text": | |
| "正在使用本地 Python 检查上传数据…" | |
| }, | |
| ) | |
| try: | |
| processing_result=await asyncio.to_thread( | |
| _run_upload_quality_checks, | |
| usage_user_id, | |
| pending_uploads, | |
| ) | |
| record=_compact_processing_record( | |
| processing_result | |
| ) | |
| await safe_memory_event( | |
| usage_user_id, | |
| "processing_completed", | |
| { | |
| "task_id":task_id, | |
| "thread_id":tid, | |
| "operation": | |
| "quality_check", | |
| "upload_ids": | |
| pending_uploads[:10], | |
| "status":"completed", | |
| "result":record, | |
| }, | |
| ) | |
| processing_context=( | |
| "[USER_DATA_PROCESSING_RESULT]\n" | |
| "The following result was computed " | |
| "locally with Python from the user's " | |
| "uploaded file. Treat these computed " | |
| "values as authoritative for this " | |
| "answer. Do not estimate them from " | |
| "the raw file. Explain the result " | |
| "clearly in Chinese.\n" | |
| + json.dumps( | |
| record, | |
| ensure_ascii=False, | |
| default=str, | |
| ) | |
| + "\n[/USER_DATA_PROCESSING_RESULT]" | |
| ) | |
| yield out( | |
| "status", | |
| { | |
| "text": | |
| "数据质检完成,正在整理结果…" | |
| }, | |
| ) | |
| except Exception as exc: | |
| await safe_memory_event( | |
| usage_user_id, | |
| "processing_failed", | |
| { | |
| "task_id":task_id, | |
| "thread_id":tid, | |
| "operation": | |
| "quality_check", | |
| "upload_ids": | |
| pending_uploads[:10], | |
| "status":"failed", | |
| "error": | |
| str(exc)[:500], | |
| }, | |
| ) | |
| raise | |
| use_fisheries = _is_fisheries_prompt(prompt) | |
| use_ocean = _needs_ocean_mcp(prompt) | |
| mcp_error = None | |
| turn_error = "" | |
| export_tool_completed=False | |
| export_tool_results=[] | |
| try: | |
| hf_task = None | |
| if use_fisheries: | |
| yield out("status",{"text":"正在读取 Hugging Face 渔业数据…","stage":"source","step":2,"total_steps":5}) | |
| hf_task = asyncio.create_task(build_hf_fisheries_context(prompt)) | |
| # Marine MCP is loaded by CodeWhale from DEEPSEEK_MCP_CONFIG when the | |
| # runtime process starts. Do NOT bootstrap it by asking the model to | |
| # call start_mcp_server inside every user thread: that creates a long | |
| # blocking turn and can make the browser/HF proxy drop the SSE stream. | |
| # A normal Ocean request goes straight to the real user turn; if the | |
| # model needs Ocean data it can call the already-registered mcp_marine_* | |
| # tools directly. | |
| if use_ocean: | |
| yield out("status",{"text":"Ocean 数据工具已就绪,正在处理请求…","stage":"source","step":2,"total_steps":5}) | |
| grounded_prompt = prompt | |
| if processing_context: | |
| grounded_prompt += ( | |
| "\n\n" + processing_context | |
| ) | |
| elif upload_context: | |
| grounded_prompt += ( | |
| "\n\n" + upload_context | |
| ) | |
| if hf_task is not None: | |
| try: | |
| hf_context = await hf_task | |
| grounded_prompt = grounded_prompt + "\n\n" + hf_context | |
| if usage_user_id: | |
| asyncio.create_task( | |
| safe_memory_event( | |
| usage_user_id, | |
| "fisheries_query", | |
| { | |
| "thread_id":tid, | |
| "source":"huggingface-fisheries", | |
| "status":"completed", | |
| "prompt_chars":len(prompt.strip()), | |
| }, | |
| ) | |
| ) | |
| except Exception as exc: | |
| grounded_prompt = ( | |
| prompt | |
| + "\n\n[HF_FISHERIES_LIVE_CONTEXT_ERROR]\n" | |
| + str(exc) | |
| + "\n[/HF_FISHERIES_LIVE_CONTEXT_ERROR]" | |
| ) | |
| try: | |
| det=await rjson(f"/v1/threads/{tid}") | |
| since=int(det.get("latest_seq") or 0) | |
| tr=await rjson(f"/v1/threads/{tid}/turns",method="POST",body={ | |
| "prompt":grounded_prompt, | |
| "input_summary":prompt[:200], | |
| "model":MODEL, | |
| "mode":"agent", | |
| "allow_shell":False, | |
| "trust_mode":False, | |
| "auto_approve":False, | |
| }) | |
| except Exception as exc: | |
| if not _is_codewhale_missing_thread_error(exc): | |
| raise | |
| history_text=_format_recent_thread_history(tid) | |
| if history_text: | |
| grounded_prompt=( | |
| "[PRIOR_CONVERSATION_HISTORY]\n" | |
| + history_text | |
| + "\n[/PRIOR_CONVERSATION_HISTORY]\n\n" | |
| + "[CONVERSATION_RECOVERY_INSTRUCTION]\n" | |
| + "用户在本次对话中已经描述过下载/查询要求。上面是同一用户的完整对话记录;" | |
| + "请把最新用户消息当作该对话的后续补充或修改继续处理," | |
| + "不要要求用户重新提交完整需求。\n" | |
| + "[/CONVERSATION_RECOVERY_INSTRUCTION]\n\n" | |
| + grounded_prompt | |
| ) | |
| old_tid=tid | |
| tid=await recover_codewhale_thread(old_tid) | |
| log.warning( | |
| "CodeWhale thread expired and was rebuilt: " | |
| "old=%s new=%s", | |
| old_tid, | |
| tid, | |
| ) | |
| yield out( | |
| "status", | |
| { | |
| "text": | |
| "检测到原对话会话已过期,正在自动保留上下文并恢复…", | |
| "stage":"source", | |
| "step":1, | |
| "total_steps":5, | |
| }, | |
| ) | |
| yield out( | |
| "thread_recovered", | |
| { | |
| "thread_id":tid, | |
| "old_thread_id":old_tid, | |
| }, | |
| ) | |
| det=await rjson(f"/v1/threads/{tid}") | |
| since=int(det.get("latest_seq") or 0) | |
| tr=await rjson(f"/v1/threads/{tid}/turns",method="POST",body={ | |
| "prompt":grounded_prompt, | |
| "input_summary":prompt[:200], | |
| "model":MODEL, | |
| "mode":"agent", | |
| "allow_shell":False, | |
| "trust_mode":False, | |
| "auto_approve":False, | |
| }) | |
| turn=((tr or {}).get("turn") or {}).get("id") | |
| answer="" | |
| yield out("status",{"text":"Codex 正在处理…","stage":"compose","step":4,"total_steps":5}) | |
| async for rec in events(tid,since): | |
| if turn and rec.get("turn_id") and rec["turn_id"]!=turn: | |
| continue | |
| e=rec.get("event") | |
| p=pl(rec) | |
| if e=="item.started": | |
| tool=_tool_name_from_payload(p) | |
| if tool.startswith("mcp_marine_"): | |
| yield out("status",{"text":"正在查询学校 Ocean 数据服务器…","stage":"query","step":3,"total_steps":5}) | |
| if usage_user_id: | |
| asyncio.create_task( | |
| safe_memory_event( | |
| usage_user_id, | |
| _marine_event_type(tool), | |
| { | |
| "thread_id":tid, | |
| "tool":tool, | |
| "operation":_marine_event_type(tool), | |
| "source":_data_source_from_prompt(prompt), | |
| "status":"started", | |
| "prompt_chars":len(prompt.strip()), | |
| }, | |
| ) | |
| ) | |
| if e=="item.completed": | |
| item=p.get("item") or {} | |
| summary=str(item.get("summary") or "") | |
| completed_tool=_tool_name_from_payload(p) | |
| try: | |
| completed_raw=json.dumps(p,ensure_ascii=False,default=str) | |
| except Exception: | |
| completed_raw=str(p) | |
| if ( | |
| completed_tool.endswith("_export") | |
| or "mcp_marine_marine_export" in completed_raw | |
| ): | |
| export_tool_completed=True | |
| export_tool_results.append(completed_raw) | |
| if "MCP server 'marine' connected" in summary or "mcp_marine_" in summary: | |
| marine_threads.add(tid) | |
| # Some Runtime versions can complete an agent_message item without | |
| # delivering a delta to this bridge. Recover the materialized final text. | |
| recovered=_agent_text_from_payload(p) | |
| if recovered and not answer: | |
| answer=recovered | |
| if e=="item.delta" and p.get("kind")=="agent_message": | |
| d=p.get("delta") or "" | |
| if d: | |
| answer+=d | |
| if e in {"item.failed","item.interrupted"}: | |
| err=_error_from_payload(p) | |
| if err: | |
| turn_error=err | |
| if e=="approval.required": | |
| aid=p.get("approval_id") or p.get("id") | |
| tool=( | |
| p.get("tool_name") | |
| or ((p.get("tool") or {}).get("name") if isinstance(p.get("tool"),dict) else p.get("tool")) | |
| or ((p.get("item") or {}).get("tool_name") if isinstance(p.get("item"),dict) else "") | |
| or "" | |
| ) | |
| if aid: | |
| if tool.startswith("mcp_marine_"): | |
| await approve(aid,"allow") | |
| elif tool=="start_mcp_server": | |
| # Static MCP config is authoritative. Never start another | |
| # MCP server dynamically inside an end-user thread. | |
| await approve(aid,"deny") | |
| yield out("status",{"text":"已阻止重复启动 Ocean MCP"}) | |
| else: | |
| await approve(aid,"deny") | |
| yield out("status",{"text":"已保持安全数据访问模式"}) | |
| if e=="turn.lifecycle": | |
| st=_turn_status(p) | |
| if st in {"failed","canceled","interrupted"}: | |
| detail=_error_from_payload(p) or turn_error or f"Turn {st}" | |
| raise RuntimeError(detail) | |
| if e=="turn.completed": | |
| st=_turn_status(p) | |
| if st in {"failed","canceled","interrupted"}: | |
| detail=_error_from_payload(p) or turn_error or f"Turn {st}" | |
| raise RuntimeError(detail) | |
| if not answer.strip(): | |
| detail=_error_from_payload(p) or turn_error | |
| if detail: | |
| raise RuntimeError(detail) | |
| raise RuntimeError( | |
| "Codex 回合已结束,但 CodeWhale 没有产生 assistant 文本。" | |
| ) | |
| final_answer=_sanitize_final_answer(answer) | |
| if not final_answer: | |
| raise RuntimeError("模型返回内容在输出清理后为空。") | |
| export_error=_ocean_export_execution_error( | |
| prompt, | |
| export_tool_completed=export_tool_completed, | |
| tool_result_text="\n".join(export_tool_results), | |
| final_answer=final_answer, | |
| ) | |
| if export_error: | |
| raise RuntimeError(export_error) | |
| if usage_user_id: | |
| asyncio.create_task( | |
| record_generated_download_assets( | |
| usage_user_id, | |
| tid, | |
| prompt, | |
| final_answer, | |
| ) | |
| ) | |
| last_llm_error=None | |
| yield out("token",{"text":final_answer}) | |
| yield out("done",{"text":final_answer}) | |
| return | |
| raise RuntimeError(turn_error or "Runtime stream ended early") | |
| except Exception as exc: | |
| last_llm_error=str(exc) | |
| log.exception( | |
| "chat failed: thread=%s model=%s ocean=%s fisheries=%s", | |
| tid, MODEL, use_ocean, use_fisheries, | |
| ) | |
| yield out("error",{"text":_public_error(str(exc)),"stage":"chat"}) | |
| async def harness_stream_chat(tid,prompt): | |
| global last_llm_error | |
| usage_user_id=thread_user_ids.get(tid,"") | |
| pending_uploads=thread_upload_ids.pop(tid,[]) | |
| upload_context="" | |
| processing_context="" | |
| if usage_user_id and pending_uploads: | |
| upload_context=await build_user_upload_context( | |
| usage_user_id, | |
| pending_uploads, | |
| ) | |
| asyncio.create_task( | |
| safe_memory_event( | |
| usage_user_id, | |
| "attachment_used", | |
| { | |
| "thread_id":tid, | |
| "upload_ids":pending_uploads[:10], | |
| "count":len(pending_uploads[:10]), | |
| "runtime":"codex-harness", | |
| }, | |
| ) | |
| ) | |
| if ( | |
| usage_user_id | |
| and pending_uploads | |
| and _quality_check_requested(prompt) | |
| ): | |
| task_id="proc_"+secrets.token_hex(8) | |
| await safe_memory_event( | |
| usage_user_id, | |
| "processing_started", | |
| { | |
| "task_id":task_id, | |
| "thread_id":tid, | |
| "operation":"quality_check", | |
| "upload_ids":pending_uploads[:10], | |
| "status":"running", | |
| "runtime":"codex-harness", | |
| }, | |
| ) | |
| yield out( | |
| "status", | |
| { | |
| "text": | |
| "正在使用本地 Python 检查数据…" | |
| }, | |
| ) | |
| try: | |
| processing_result=await asyncio.to_thread( | |
| _run_upload_quality_checks, | |
| usage_user_id, | |
| pending_uploads, | |
| ) | |
| record=_compact_processing_record( | |
| processing_result | |
| ) | |
| await safe_memory_event( | |
| usage_user_id, | |
| "processing_completed", | |
| { | |
| "task_id":task_id, | |
| "thread_id":tid, | |
| "operation":"quality_check", | |
| "upload_ids":pending_uploads[:10], | |
| "status":"completed", | |
| "runtime":"codex-harness", | |
| "result":record, | |
| }, | |
| ) | |
| processing_context=( | |
| "[USER_DATA_PROCESSING_RESULT]\n" | |
| "These values were computed locally " | |
| "with Python from the current user's " | |
| "uploaded file. Use them as the " | |
| "authoritative result. Do not guess " | |
| "or recompute them mentally. Explain " | |
| "the result clearly in Chinese.\n" | |
| + json.dumps( | |
| record, | |
| ensure_ascii=False, | |
| default=str, | |
| ) | |
| + "\n[/USER_DATA_PROCESSING_RESULT]" | |
| ) | |
| yield out( | |
| "status", | |
| { | |
| "text": | |
| "数据计算完成,Codex Harness 正在整理结果…" | |
| }, | |
| ) | |
| except Exception as exc: | |
| await safe_memory_event( | |
| usage_user_id, | |
| "processing_failed", | |
| { | |
| "task_id":task_id, | |
| "thread_id":tid, | |
| "operation":"quality_check", | |
| "upload_ids":pending_uploads[:10], | |
| "status":"failed", | |
| "runtime":"codex-harness", | |
| "error":str(exc)[:500], | |
| }, | |
| ) | |
| raise | |
| app_context=thread_system_prompts.get( | |
| tid, | |
| USER_SYSTEM, | |
| ) | |
| harness_prompt=( | |
| "[APPLICATION_CONTEXT]\n" | |
| + app_context | |
| + "\n[/APPLICATION_CONTEXT]\n\n" | |
| + "[CURRENT_USER_MESSAGE]\n" | |
| + prompt | |
| + "\n[/CURRENT_USER_MESSAGE]" | |
| ) | |
| if processing_context: | |
| harness_prompt += ( | |
| "\n\n" + processing_context | |
| ) | |
| elif upload_context: | |
| harness_prompt += ( | |
| "\n\n" + upload_context | |
| ) | |
| yield out( | |
| "status", | |
| { | |
| "text": | |
| f"Codex Harness · {HARNESS_MODEL} 正在处理…" | |
| }, | |
| ) | |
| try: | |
| task=asyncio.create_task( | |
| asyncio.to_thread( | |
| dsh.run, | |
| harness_prompt, | |
| session_id=tid, | |
| ) | |
| ) | |
| while not task.done(): | |
| try: | |
| await asyncio.wait_for( | |
| asyncio.shield(task), | |
| timeout=8, | |
| ) | |
| except asyncio.TimeoutError: | |
| yield ": keepalive\n\n" | |
| result=await task | |
| final_answer=_sanitize_final_answer( | |
| result.final_response | |
| ) | |
| if not final_answer: | |
| raise RuntimeError( | |
| "Codex Harness 没有返回有效文本。" | |
| ) | |
| log.info( | |
| "Codex Harness completed: " | |
| "thread=%s model=%s reason=%s uploads=%s", | |
| tid, | |
| HARNESS_MODEL, | |
| result.finish_reason, | |
| len(pending_uploads), | |
| ) | |
| last_llm_error=None | |
| yield out( | |
| "token", | |
| {"text":final_answer}, | |
| ) | |
| yield out( | |
| "done", | |
| { | |
| "text":final_answer, | |
| "runtime":"codex-harness", | |
| "model":HARNESS_MODEL, | |
| "finish_reason":result.finish_reason, | |
| }, | |
| ) | |
| except Exception as exc: | |
| last_llm_error=str(exc) | |
| log.exception( | |
| "Codex Harness failed: " | |
| "thread=%s model=%s uploads=%s", | |
| tid, | |
| HARNESS_MODEL, | |
| len(pending_uploads), | |
| ) | |
| yield out( | |
| "error", | |
| { | |
| "text": | |
| "Codex Harness 调用失败:" | |
| + str(exc)[:500], | |
| "stage":"harness", | |
| }, | |
| ) | |
| DATA_REQUEST_TTL_SECONDS = 6 * 3600 | |
| def _context_spec_has_data(spec) -> bool: | |
| if not isinstance(spec, dict): | |
| return False | |
| return any( | |
| spec.get(key) | |
| for key in ( | |
| "domain", "dataset", "variables", "region", | |
| "depth", "date", "format", | |
| ) | |
| ) | |
| async def dispatch_chat_stream(tid,prompt): | |
| now=time.time() | |
| pending=thread_last_data_requests.get(tid) or {} | |
| if pending and now-float(pending.get("ts") or 0)>DATA_REQUEST_TTL_SECONDS: | |
| thread_last_data_requests.pop(tid, None) | |
| pending={} | |
| history_spec=build_request_spec_from_messages( | |
| thread_recent_history.get(tid) or [] | |
| ) | |
| context_spec=thread_compressed_contexts.get(tid) or {} | |
| if not _context_spec_has_data(context_spec): | |
| context_spec=history_spec | |
| if not _context_spec_has_data(context_spec) and pending.get("spec"): | |
| context_spec=pending.get("spec") | |
| explicit_data=( | |
| _needs_ocean_mcp(prompt) | |
| or _is_fisheries_prompt(prompt) | |
| ) | |
| is_confirmation=_is_confirmation_prompt(prompt) | |
| is_amendment=bool( | |
| (not is_confirmation) | |
| and pending.get("prompt") | |
| and _is_data_request_followup(prompt) | |
| ) | |
| resumed=bool( | |
| pending.get("prompt") | |
| and (is_confirmation or is_amendment) | |
| ) | |
| # Remember every data request and each short amendment that depends on it. | |
| # This lets follow-ups such as “补充区域…”/“把格式改成 CSV” stay in the | |
| # same data runtime and be merged with the original parameters instead of | |
| # being treated as brand-new unrelated messages. | |
| if is_amendment: | |
| merged_spec=merge_request_spec( | |
| context_spec, | |
| prompt, | |
| revision=True, | |
| ) | |
| thread_compressed_contexts[tid]=merged_spec | |
| thread_last_data_requests[tid]={ | |
| "prompt":render_request_spec(merged_spec), | |
| "ts":now, | |
| "spec":merged_spec, | |
| } | |
| elif explicit_data and not resumed: | |
| parsed_spec=extract_request_spec(prompt) | |
| if _context_spec_has_data(parsed_spec): | |
| merged_spec=merge_request_spec( | |
| None, | |
| prompt, | |
| revision=False, | |
| ) | |
| thread_compressed_contexts[tid]=merged_spec | |
| else: | |
| merged_spec={"raw_request":str(prompt or "")[:1600]} | |
| thread_compressed_contexts[tid]=merged_spec | |
| thread_last_data_requests[tid]={ | |
| "prompt":render_request_spec(merged_spec) or prompt, | |
| "ts":now, | |
| "spec":merged_spec, | |
| } | |
| # Fast-path multi-day Ocean exports into a persistent batch job instead of | |
| # keeping one Agent turn open for dozens of serial tool calls. | |
| batch_spec=parse_ocean_batch_request(prompt) | |
| if batch_spec: | |
| if not resumed: | |
| batch_spec_context=thread_compressed_contexts.get(tid) or {} | |
| thread_last_data_requests[tid]={ | |
| "prompt":( | |
| render_request_spec(batch_spec_context) | |
| or prompt | |
| ), | |
| "ts":now, | |
| "spec":batch_spec_context, | |
| } | |
| uid=thread_user_ids.get(tid,"") | |
| job=await OCEAN_BATCH_MANAGER.create(uid,prompt,batch_spec,tid) | |
| yield out("status",{ | |
| "text":f"已拆分为 {job['total']} 个单日子任务,后台并行执行中…", | |
| "stage":"query","step":3,"total_steps":5, | |
| }) | |
| yield out("batch_job",job) | |
| variables="、".join(batch_spec.get("variables") or []) | |
| bbox=batch_spec.get("bbox") or {} | |
| region_note=( | |
| f"区域:{batch_spec.get('region_name')} " | |
| f"({bbox.get('lon_min')}°E–{bbox.get('lon_max')}°E, " | |
| f"{bbox.get('lat_min')}°N–{bbox.get('lat_max')}°N)。" | |
| if batch_spec.get("region_name") else "" | |
| ) | |
| text=( | |
| f"已创建 Ocean 批量导出任务,共 {job['total']} 个单日子任务。" | |
| f"范围 {batch_spec['start_date']} 至 {batch_spec['end_date']},变量:{variables}。" | |
| + region_note + | |
| "学校 Ocean 接口只接收单日 YYYY-MM-DD;平台已自动把月份/日期范围拆成单日并行执行," | |
| "不会再要求您补充某一天。任务卡会实时显示完成、失败、执行中和等待数量。" | |
| ) | |
| yield out("done",{"text":text,"batch_job_id":job["job_id"]}) | |
| return | |
| has_upload = bool(thread_upload_ids.get(tid)) | |
| compact_spec_text=render_request_spec( | |
| thread_compressed_contexts.get(tid) or context_spec | |
| ) | |
| routed_prompt=prompt | |
| if is_confirmation and resumed: | |
| thread_last_data_requests[tid]={ | |
| "prompt":compact_spec_text or str(pending["prompt"]), | |
| "ts":now, | |
| "spec":thread_compressed_contexts.get(tid) or context_spec, | |
| } | |
| routed_prompt=( | |
| "请下载或继续执行以下已确认的数据请求,保留全部参数:\n\n" | |
| + (compact_spec_text or str(pending["prompt"])) | |
| + "\n\n[USER_CONFIRMATION]\n" | |
| + "用户刚刚回复确认。请立即继续执行上一项数据查询或导出," | |
| + "沿用已经给出的日期、区域、变量和数据源,不要再次询问确认。\n" | |
| + "[/USER_CONFIRMATION]" | |
| ) | |
| elif is_amendment: | |
| routed_prompt=( | |
| "请下载或继续执行以下数据请求,保留全部参数:\n\n" | |
| + str( | |
| thread_last_data_requests[tid].get("prompt") | |
| or pending["prompt"] | |
| or "" | |
| ) | |
| + "\n\n[AMENDMENT_DIRECTIVE]\n" | |
| + "用户正在补充或修改上一项数据下载/查询请求。" | |
| + "请把“最近补充/修改”作为本次唯一变更,其余条件继续沿用卡片中的参数;" | |
| + "按最新要求立即执行,不要要求用户重新提交完整需求。\n" | |
| + "[/AMENDMENT_DIRECTIVE]" | |
| ) | |
| routed_prompt=_apply_ocean_export_defaults(routed_prompt) | |
| # v4.3.2: never send a Harness thread to CodeWhale. | |
| # When dsh is configured, CodeWhale is not a second required runtime. | |
| # This fixes ordinary chat and prevents a data-related keyword from | |
| # switching the user back to an unavailable localhost service. | |
| keep_codewhale = dsh is None | |
| if keep_codewhale: | |
| async for chunk in stream_chat(tid, routed_prompt): | |
| yield chunk | |
| return | |
| async for chunk in harness_stream_chat(tid, routed_prompt): | |
| yield chunk | |
| def init_chat_runtime(context): | |
| _inject(context) | |
| return stream_chat, harness_stream_chat, dispatch_chat_stream | |