nav_copilot.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328
  1. from __future__ import annotations
  2. from plugins_func.register import Action, ActionResponse, ToolType, register_function
  3. from plugins_func.nav_copilot.service import get_service
  4. nav_start_record_desc = {
  5. "type": "function",
  6. "function": {
  7. "name": "nav_start_record",
  8. "description": (
  9. "开始记录路线。"
  10. "如果设备接了外部 STM32 串口计数器,就默认使用计数路线模式;"
  11. "计数路线模式下,这一步只进入准备状态,不要求立刻收到串口值。"
  12. "后续用户说“标记急转弯”时,再抓取当前串口值作为标记点;说“结束录制”后自动进入监听预警。"
  13. "否则退回 GPS 路线模式。"
  14. "当用户说“开始记录路线”“开始录制路线”“录制路线”“开始记录”“开始记路线”时,"
  15. "不要只做自然语言回应,必须优先直接调用这个工具。"
  16. ),
  17. "parameters": {
  18. "type": "object",
  19. "properties": {
  20. "route_name": {
  21. "type": "string",
  22. "description": "路线名称,可选",
  23. },
  24. "sample_hz": {
  25. "type": "integer",
  26. "description": "采样频率,计数模式建议 10Hz 左右",
  27. },
  28. "mode": {
  29. "type": "string",
  30. "description": "auto 表示自动选择;counter 表示串口计数;gps 表示 GNSS 轨迹",
  31. "enum": ["auto", "counter", "gps"],
  32. },
  33. },
  34. "required": [],
  35. },
  36. },
  37. }
  38. @register_function("nav_start_record", nav_start_record_desc, ToolType.SYSTEM_CTL)
  39. async def nav_start_record(
  40. conn,
  41. route_name: str | None = None,
  42. sample_hz: int = 10,
  43. mode: str = "auto",
  44. ):
  45. svc = get_service(conn)
  46. route_id = await svc.start_recording(
  47. route_name=route_name,
  48. sample_hz=sample_hz,
  49. mode=mode,
  50. )
  51. route_mode = svc.record_mode or "auto"
  52. if route_mode == "counter":
  53. text = (
  54. f"已经进入计数路线录制准备状态,路线 ID 是 {route_id}。"
  55. "你到目标点时直接说“标记急转弯”或“标记障碍”,说“结束录制”后会自动开始监听预警。"
  56. )
  57. else:
  58. text = (
  59. f"已经开始记录 GPS 路线,路线 ID 是 {route_id}。"
  60. "你到目标点时可以直接说“标记急转弯”或“标记障碍”。"
  61. )
  62. return ActionResponse(action=Action.RESPONSE, result=str(route_id), response=text)
  63. nav_stop_record_desc = {
  64. "type": "function",
  65. "function": {
  66. "name": "nav_stop_record",
  67. "description": "结束当前路线记录并保存。计数路线模式下,如果已经标记过风险点,会自动进入监听预警。",
  68. "parameters": {"type": "object", "properties": {}, "required": []},
  69. },
  70. }
  71. @register_function("nav_stop_record", nav_stop_record_desc, ToolType.SYSTEM_CTL)
  72. async def nav_stop_record(conn):
  73. svc = get_service(conn)
  74. result = await svc.stop_recording()
  75. if result is None:
  76. return ActionResponse(action=Action.RESPONSE, response="当前没有正在记录的路线。")
  77. if result.mode == "counter":
  78. if result.monitor_started:
  79. response = (
  80. f"路线录制已结束,路线 ID 是 {result.route_id}。"
  81. f"已记录 {result.hazard_count} 个风险点,并且已经自动开始监听预警。"
  82. )
  83. else:
  84. response = (
  85. f"路线录制已结束,路线 ID 是 {result.route_id}。"
  86. "但你还没有标记风险点,所以还没有启动监听预警。"
  87. )
  88. else:
  89. response = f"路线记录已结束,路线 ID 是 {result.route_id}。"
  90. return ActionResponse(
  91. action=Action.RESPONSE,
  92. result=str(result.route_id),
  93. response=response,
  94. )
  95. nav_mark_hazard_desc = {
  96. "type": "function",
  97. "function": {
  98. "name": "nav_mark_hazard",
  99. "description": (
  100. "在当前位置标记一个风险点。"
  101. "计数路线模式下,会把当前 STM32 串口传来的最新计数值保存成标记点。"
  102. "典型口令包括“标记急转弯”“标记起转弯”“标记大坑”“标记障碍”。"
  103. ),
  104. "parameters": {
  105. "type": "object",
  106. "properties": {
  107. "hazard_type": {
  108. "type": "string",
  109. "description": "风险点类型",
  110. "enum": [
  111. "sharp_turn",
  112. "bump",
  113. "jump",
  114. "slope",
  115. "water",
  116. "obstacle",
  117. "other",
  118. ],
  119. },
  120. "note": {
  121. "type": "string",
  122. "description": "补充说明,可选",
  123. },
  124. },
  125. "required": ["hazard_type"],
  126. },
  127. },
  128. }
  129. @register_function("nav_mark_hazard", nav_mark_hazard_desc, ToolType.SYSTEM_CTL)
  130. async def nav_mark_hazard(conn, hazard_type: str, note: str | None = None):
  131. svc = get_service(conn)
  132. result = await svc.mark_hazard(hazard_type=hazard_type, note=note or "")
  133. if result.mode == "counter" and result.counter_value is not None:
  134. response = f"已标记风险点,当前串口值是 {result.counter_value}。"
  135. else:
  136. response = "已标记风险点。"
  137. return ActionResponse(
  138. action=Action.RESPONSE,
  139. result=str(result.hazard_id),
  140. response=response,
  141. )
  142. nav_mark_hazard_reject_desc = {
  143. "type": "function",
  144. "function": {
  145. "name": "nav_mark_hazard_reject",
  146. "description": "当用户进入风险点录制口令但没有说出有效风险类型时,提示其保持安静并等待重新录入。",
  147. "parameters": {"type": "object", "properties": {}, "required": []},
  148. },
  149. }
  150. @register_function("nav_mark_hazard_reject", nav_mark_hazard_reject_desc, ToolType.SYSTEM_CTL)
  151. async def nav_mark_hazard_reject(conn):
  152. return ActionResponse(
  153. action=Action.RESPONSE,
  154. response="请保持安静,等待录入风险点。",
  155. )
  156. nav_start_run_desc = {
  157. "type": "function",
  158. "function": {
  159. "name": "nav_start_run",
  160. "description": (
  161. "开始预警播报。"
  162. "对于串口计数路线,会在计数快接近标记点时播报“注意急转弯”,到达标记点时播报“当前急转弯”。"
  163. "通常计数路线在“结束录制”后会自动开始监听预警,这个工具主要用于手动补启。"
  164. "当用户说“开始预警”“开始领航”“开始播报”时,不要只做自然语言回应,必须优先直接调用这个工具。"
  165. ),
  166. "parameters": {
  167. "type": "object",
  168. "properties": {
  169. "route_id": {
  170. "type": "integer",
  171. "description": "指定路线 ID,可选",
  172. },
  173. "tick_hz": {
  174. "type": "integer",
  175. "description": "刷新频率,计数模式建议 10Hz 左右",
  176. },
  177. "mode": {
  178. "type": "string",
  179. "description": "可选。通常不必传,服务端会按路线类型自动选择",
  180. "enum": ["auto", "counter", "gps"],
  181. },
  182. },
  183. "required": [],
  184. },
  185. },
  186. }
  187. @register_function("nav_start_run", nav_start_run_desc, ToolType.SYSTEM_CTL)
  188. async def nav_start_run(
  189. conn,
  190. route_id: int | None = None,
  191. tick_hz: int = 10,
  192. mode: str = "auto",
  193. ):
  194. svc = get_service(conn)
  195. active_route_id = await svc.start_run(route_id=route_id, tick_hz=tick_hz, mode=mode)
  196. response = f"已经开始预警,使用路线 ID={active_route_id}。"
  197. if svc.run_mode == "counter":
  198. response = f"已经开始监听预警,使用路线 ID={active_route_id}。"
  199. return ActionResponse(
  200. action=Action.RESPONSE,
  201. result=str(active_route_id),
  202. response=response,
  203. )
  204. nav_stop_run_desc = {
  205. "type": "function",
  206. "function": {
  207. "name": "nav_stop_run",
  208. "description": "停止当前预警播报。",
  209. "parameters": {"type": "object", "properties": {}, "required": []},
  210. },
  211. }
  212. @register_function("nav_stop_run", nav_stop_run_desc, ToolType.SYSTEM_CTL)
  213. async def nav_stop_run(conn):
  214. svc = get_service(conn)
  215. route_id = await svc.stop_run()
  216. if route_id is None:
  217. return ActionResponse(action=Action.RESPONSE, response="当前没有正在运行的预警。")
  218. return ActionResponse(
  219. action=Action.RESPONSE,
  220. result=str(route_id),
  221. response="预警已停止。",
  222. )
  223. nav_status_desc = {
  224. "type": "function",
  225. "function": {
  226. "name": "nav_status",
  227. "description": "查询路线记录和预警状态。",
  228. "parameters": {"type": "object", "properties": {}, "required": []},
  229. },
  230. }
  231. @register_function("nav_status", nav_status_desc, ToolType.SYSTEM_CTL)
  232. async def nav_status(conn):
  233. svc = get_service(conn)
  234. return ActionResponse(action=Action.RESPONSE, response=svc.status_text())
  235. nav_list_routes_desc = {
  236. "type": "function",
  237. "function": {
  238. "name": "nav_list_routes",
  239. "description": "列出最近保存的路线。",
  240. "parameters": {
  241. "type": "object",
  242. "properties": {
  243. "limit": {
  244. "type": "integer",
  245. "description": "返回数量,默认 10",
  246. }
  247. },
  248. "required": [],
  249. },
  250. },
  251. }
  252. @register_function("nav_list_routes", nav_list_routes_desc, ToolType.SYSTEM_CTL)
  253. async def nav_list_routes(conn, limit: int = 10):
  254. svc = get_service(conn)
  255. routes = svc.db.list_routes(limit=int(limit or 10))
  256. if not routes:
  257. return ActionResponse(action=Action.RESPONSE, response="暂无路线,请先开始记录路线。")
  258. lines = []
  259. for route in routes:
  260. active = "(激活)" if route.active else ""
  261. lines.append(
  262. f"路线 {route.id}{active}:{route.name},模式 {route.mode},点数 {route.points},风险点 {route.hazards}"
  263. )
  264. return ActionResponse(action=Action.RESPONSE, response=";".join(lines))
  265. nav_set_active_route_desc = {
  266. "type": "function",
  267. "function": {
  268. "name": "nav_set_active_route",
  269. "description": "将某条路线设置为当前激活路线。",
  270. "parameters": {
  271. "type": "object",
  272. "properties": {
  273. "route_id": {
  274. "type": "integer",
  275. "description": "路线 ID",
  276. }
  277. },
  278. "required": ["route_id"],
  279. },
  280. },
  281. }
  282. @register_function("nav_set_active_route", nav_set_active_route_desc, ToolType.SYSTEM_CTL)
  283. async def nav_set_active_route(conn, route_id: int):
  284. svc = get_service(conn)
  285. svc.db.set_active_route(int(route_id))
  286. return ActionResponse(
  287. action=Action.RESPONSE,
  288. response=f"已激活路线 ID={int(route_id)}。",
  289. )