alg-agent / app.py
fenghantong
Deploy RouteOpt Agent
ffda755
Raw
History Blame Contribute Delete
8.98 kB
from __future__ import annotations
import os
import socket
import traceback
from typing import Any
import gradio as gr
from routeopt_agent import RouteOptAgent
from routeopt_agent.geo_tools import builtin_city_summary, builtin_place_examples
from routeopt_agent.llm_client import get_llm_config
from routeopt_agent.solver import format_km, format_minutes
agent = RouteOptAgent()
SAMPLE_REQUEST = "我从上海交通大学闵行校区出发,想一天内逛完徐家汇、人民广场、外滩、陆家嘴,最后回到起点,尽量总时间短。"
SAMPLE_DESTINATIONS = "徐家汇\n人民广场\n外滩\n陆家嘴"
def run_route_agent(
raw_request: str,
start_place: str,
destination_places: str,
objective_label: str,
return_to_start: bool,
fixed_end_place: str,
city_hint: str,
use_llm: bool,
) -> tuple[Any, ...]:
objective = "distance" if "距离" in objective_label else "time"
try:
result = agent.run(
raw_request=raw_request,
start_hint=start_place,
destinations_hint=destination_places,
objective_hint=objective,
return_to_start_hint=return_to_start,
fixed_end_hint=fixed_end_place,
city_hint=city_hint,
use_llm=use_llm,
)
except Exception as exc:
error_md = build_user_error_message(exc, city_hint)
return error_md, "", [], [], [], trace_to_rows(agent.trace), None
solution = result.solution
warning_md = ""
if result.warnings:
warning_md = "\n\n**运行提示**\n" + "\n".join(f"- {item}" for item in result.warnings)
summary = (
f"**最优访问顺序**:{' → '.join(solution.route_names)}\n\n"
f"**总距离**:{format_km(solution.total_distance_meters)} \n"
f"**预计驾驶时间**:{format_minutes(solution.total_duration_seconds)} \n"
f"**优化算法**:{solution.algorithm}\n\n"
f"{result.summary_markdown}"
f"{warning_md}"
)
route_rows = [[idx + 1, name] for idx, name in enumerate(solution.route_names)]
point_rows = [
[idx, point.name, f"{point.lat:.6f}", f"{point.lon:.6f}", point.source, point.display_name]
for idx, point in enumerate(result.points)
]
trace_rows = [
[
event.step,
event.tool,
event.status,
compact_for_ui(event.arguments),
event.result,
]
for event in result.trace
]
return (
summary,
result.route_svg,
route_rows,
solution.leg_rows,
point_rows,
trace_rows,
result.pdf_path,
)
def compact_for_ui(value: Any) -> str:
text = str(value)
if len(text) > 260:
return text[:260] + "..."
return text
def trace_to_rows(trace: list[Any]) -> list[list[Any]]:
return [
[
event.step,
event.tool,
event.status,
compact_for_ui(event.arguments),
event.result,
]
for event in trace
]
def build_user_error_message(exc: Exception, city_hint: str) -> str:
message = str(exc).strip() or exc.__class__.__name__
suggestions = [
"确认起点和目的地都已填写,且目的地至少有一个不同于起点的地点。",
"地点名尽量写完整,例如加上城市、区县、校区或景区全称。",
f"当前内置演示坐标覆盖城市:{builtin_city_summary()}。如果输入其他城市,系统会依赖在线地理编码,可能受网络、限流或地名歧义影响。",
"如果在线地理编码不稳定,可以把地点写成 `地点名@纬度,经度`,例如 `某景点@39.9042,116.4074`。",
]
if not city_hint.strip():
suggestions.insert(1, "城市/区域提示为空。建议填写类似 `北京,中国` 或 `上海,中国`,能显著降低地点歧义。")
return (
"**本次没有完成路线优化,但系统已定位到失败原因。**\n\n"
f"**失败原因**:\n\n{message}\n\n"
"**可以这样处理**:\n"
+ "\n".join(f"- {item}" for item in suggestions)
+ "\n\n"
"<details><summary>技术调试信息</summary>\n\n"
f"```text\n{traceback.format_exc()[-1800:]}\n```\n</details>"
)
def runtime_status_text() -> str:
try:
config = get_llm_config()
key_text = "已配置 key" if config.api_key else "无 key"
llm_text = f"{config.provider} / {config.model}{key_text})"
except Exception as exc:
llm_text = f"LLM 配置异常:{exc}"
return (
f"当前 LLM:`{llm_text}` \n"
f"内置演示城市:`{builtin_city_summary()}` \n"
f"内置地点示例:{builtin_place_examples()} \n"
"未知地点可用手动坐标格式:`地点名@纬度,经度`。"
)
with gr.Blocks(title="RouteOpt Agent") as demo:
gr.Markdown(
"# RouteOpt Agent\n"
"基于公开大模型 API + 轻量工具调用的小规模路线优化智能体。"
)
gr.Markdown(runtime_status_text())
with gr.Row():
with gr.Column(scale=4):
raw_request = gr.Textbox(
label="自然语言需求",
value=SAMPLE_REQUEST,
lines=4,
placeholder="例如:我从交大闵行出发,想逛完外滩、陆家嘴、人民广场,最后回到起点,尽量总时间短。",
)
start_place = gr.Textbox(label="起点", value="上海交通大学闵行校区")
destination_places = gr.Textbox(
label="目的地(每行一个,建议 3-8 个)",
value=SAMPLE_DESTINATIONS,
lines=6,
)
with gr.Row():
objective = gr.Radio(
label="优化目标",
choices=["最短时间", "最短距离"],
value="最短时间",
)
return_to_start = gr.Checkbox(label="最后回到起点", value=True)
with gr.Row():
fixed_end_place = gr.Textbox(label="固定终点(不回起点时可填)", value="")
city_hint = gr.Textbox(label="城市/区域提示", value="上海,中国")
use_llm = gr.Checkbox(label="启用 LLM 工具调用解析与总结", value=True)
run_button = gr.Button("开始优化并生成 PDF", variant="primary")
with gr.Column(scale=6):
summary = gr.Markdown(label="求解结果")
route_svg = gr.HTML(label="路线示意图")
with gr.Tab("路线"):
route_table = gr.Dataframe(
headers=["顺序", "地点"],
datatype=["number", "str"],
interactive=False,
)
legs_table = gr.Dataframe(
headers=["段", "从", "到", "距离", "时间"],
datatype=["number", "str", "str", "str", "str"],
interactive=False,
)
with gr.Tab("工具调用"):
trace_table = gr.Dataframe(
headers=["步骤", "工具", "状态", "参数", "结果摘要"],
datatype=["number", "str", "str", "str", "str"],
interactive=False,
wrap=True,
)
with gr.Tab("地理编码"):
points_table = gr.Dataframe(
headers=["序号", "地点", "纬度", "经度", "数据源", "匹配名称"],
datatype=["number", "str", "str", "str", "str", "str"],
interactive=False,
wrap=True,
)
with gr.Tab("报告"):
pdf_file = gr.File(label="PDF 报告")
run_button.click(
run_route_agent,
inputs=[
raw_request,
start_place,
destination_places,
objective,
return_to_start,
fixed_end_place,
city_hint,
use_llm,
],
outputs=[
summary,
route_svg,
route_table,
legs_table,
points_table,
trace_table,
pdf_file,
],
)
def find_available_port(start_port: int, attempts: int = 20) -> int:
for port in range(start_port, start_port + attempts):
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
try:
sock.bind(("0.0.0.0", port))
return port
except OSError:
continue
return start_port
if __name__ == "__main__":
requested_port = int(os.getenv("GRADIO_SERVER_PORT") or os.getenv("PORT") or "7860")
server_name = os.getenv("GRADIO_SERVER_NAME")
if not server_name:
server_name = "0.0.0.0" if os.getenv("SPACE_ID") else "127.0.0.1"
port = find_available_port(requested_port)
print(f"RouteOpt Agent is starting on http://localhost:{port}")
demo.queue().launch(server_name=server_name, server_port=port)