"""Gradio 入口(ModelScope 创空间 / HF Spaces ZeroGPU)。 环境变量:LLM_BACKEND=hf(默认)、HF_BASE_MODEL、HF_ADAPTER、HF_4BIT、MODEL_SOURCE。 本地调试:LLM_BACKEND=mlx uv run python app.py (复用本机 mlx 服务) ZeroGPU:检测到 `spaces` 包 + SPACE_ID 时走非流式路径,整段推理放进 @spaces.GPU 函数里执行。 """ from __future__ import annotations import json import os os.environ.setdefault("LLM_BACKEND", "hf") import gradio as gr # noqa: E402 from logicllm import config # noqa: E402 from logicllm.agent.core import LogisticsAgent # noqa: E402 from logicllm.tools.waybill import init_db # noqa: E402 ZERO_GPU = False if os.getenv("SPACE_ID"): try: import spaces # noqa: F401 ZERO_GPU = True except ImportError: pass init_db() if not config.KB_INDEX.exists(): # Space/创空间不带索引文件,首次启动现场构建(约 1 分钟) import subprocess, sys # 用子进程构建,避免在主进程里加载 embedding 模型(ZeroGPU 对主进程的 torch 状态很敏感) subprocess.run([sys.executable, "scripts/build_kb.py"], check=False) AGENT = LogisticsAgent() EXAMPLES = [ "帮我查一下运单 LL2026080258", "从深圳寄一个 2.2kg、40×30×30cm 的箱子到拉萨,保价1000元,运费多少?几天能到?", "没保价的快递丢了能赔多少?", "充电宝能寄吗?", "从武汉出发送货到长沙、郑州、合肥、南昌,怎么排顺序最省路?", "提取信息:收件人王小明 13812345678 浙江省杭州市西湖区文三路123号5楼", "快递员没经过我同意把包裹放快递柜了,合规吗?", "FOB 和 CIF 有什么区别?", ] def _fmt(obj, limit=600) -> str: s = json.dumps(obj, ensure_ascii=False) return s if len(s) <= limit else s[:limit] + "…" async def respond(message: str, history: list[dict], use_tools: bool, use_rag: bool): agent = LogisticsAgent(client=AGENT.client, kb=AGENT.kb, use_tools=use_tools, use_rag=use_rag) msgs = [m for m in history if m.get("role") in ("user", "assistant") and isinstance(m.get("content"), str)] msgs.append({"role": "user", "content": message}) trace, answer = [], "" async for ev in agent.stream(msgs): t = ev["type"] if t == "rag": hits = ";".join(f"{h['title']}({h['score']})" for h in ev["data"]["hits"]) trace.append(f"📚 知识库命中:{hits}") elif t == "tool_call": trace.append(f"🔧 调用 `{ev['data']['name']}` {_fmt(ev['data']['arguments'])}") elif t == "tool_result": trace.append(f"↩ 结果 {_fmt(ev['data'])}") elif t == "delta": answer += ev["data"] elif t == "done": answer = answer or ev["data"] elif t == "error": answer = f"⚠️ {ev['data']}" head = ("
处理过程\n\n" + "\n\n".join(trace) + "\n\n
\n\n") if trace else "" yield head + answer def _trace_md(steps) -> str: trace = [] for st in steps: if st.kind == "rag": trace.append("📚 知识库命中:" + ";".join(f"{h['title']}({h['score']})" for h in st.content["hits"])) elif st.kind == "tool_call": trace.append(f"🔧 调用 `{st.content['name']}` {_fmt(st.content['arguments'])}") elif st.kind == "tool_result": trace.append(f"↩ 结果 {_fmt(st.content)}") return ("
处理过程\n\n" + "\n\n".join(trace) + "\n\n
\n\n") if trace else "" if ZERO_GPU: import asyncio import spaces @spaces.GPU(duration=120) def respond_gpu(message: str, history: list[dict], use_tools: bool, use_rag: bool) -> str: agent = LogisticsAgent(client=AGENT.client, kb=AGENT.kb, use_tools=use_tools, use_rag=use_rag) msgs = [m for m in history if m.get("role") in ("user", "assistant") and isinstance(m.get("content"), str)] msgs.append({"role": "user", "content": message}) res = asyncio.run(agent.run(msgs)) return _trace_md(res.steps) + res.answer CHAT_FN = respond_gpu else: CHAT_FN = respond with gr.Blocks(title="物流通 · logicLLM") as demo: gr.Markdown( "## 🚚 物流通 · 物流行业大模型\n" f"基座 `{config.HF_BASE_MODEL if config.LLM_BACKEND == 'hf' else config.LLM_MODEL}` + 物流 LoRA · 知识库检索 · 10 个业务工具(运单 / 计费 / 时效 / 路径 / 地址)。" "运单数据为演示库,示例运单号 `LL2026080258`。" ) with gr.Row(): use_tools = gr.Checkbox(True, label="工具调用") use_rag = gr.Checkbox(True, label="知识库检索") gr.ChatInterface( CHAT_FN, additional_inputs=[use_tools, use_rag], examples=[[e] for e in EXAMPLES], cache_examples=False, textbox=gr.Textbox(placeholder="输入物流相关问题…", scale=7), ) if __name__ == "__main__": demo.queue(default_concurrency_limit=2).launch(server_name="0.0.0.0", server_port=int(os.getenv("PORT", "7860")), show_error=True)