nav_copilot.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310
  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_start_run_desc = {
  143. "type": "function",
  144. "function": {
  145. "name": "nav_start_run",
  146. "description": (
  147. "开始预警播报。"
  148. "对于串口计数路线,会在计数快接近标记点时播报“注意急转弯”,到达标记点时播报“当前急转弯”。"
  149. "通常计数路线在“结束录制”后会自动开始监听预警,这个工具主要用于手动补启。"
  150. "当用户说“开始预警”“开始领航”“开始播报”时,不要只做自然语言回应,必须优先直接调用这个工具。"
  151. ),
  152. "parameters": {
  153. "type": "object",
  154. "properties": {
  155. "route_id": {
  156. "type": "integer",
  157. "description": "指定路线 ID,可选",
  158. },
  159. "tick_hz": {
  160. "type": "integer",
  161. "description": "刷新频率,计数模式建议 10Hz 左右",
  162. },
  163. "mode": {
  164. "type": "string",
  165. "description": "可选。通常不必传,服务端会按路线类型自动选择",
  166. "enum": ["auto", "counter", "gps"],
  167. },
  168. },
  169. "required": [],
  170. },
  171. },
  172. }
  173. @register_function("nav_start_run", nav_start_run_desc, ToolType.SYSTEM_CTL)
  174. async def nav_start_run(
  175. conn,
  176. route_id: int | None = None,
  177. tick_hz: int = 10,
  178. mode: str = "auto",
  179. ):
  180. svc = get_service(conn)
  181. active_route_id = await svc.start_run(route_id=route_id, tick_hz=tick_hz, mode=mode)
  182. response = f"已经开始预警,使用路线 ID={active_route_id}。"
  183. if svc.run_mode == "counter":
  184. response = f"已经开始监听预警,使用路线 ID={active_route_id}。"
  185. return ActionResponse(
  186. action=Action.RESPONSE,
  187. result=str(active_route_id),
  188. response=response,
  189. )
  190. nav_stop_run_desc = {
  191. "type": "function",
  192. "function": {
  193. "name": "nav_stop_run",
  194. "description": "停止当前预警播报。",
  195. "parameters": {"type": "object", "properties": {}, "required": []},
  196. },
  197. }
  198. @register_function("nav_stop_run", nav_stop_run_desc, ToolType.SYSTEM_CTL)
  199. async def nav_stop_run(conn):
  200. svc = get_service(conn)
  201. route_id = await svc.stop_run()
  202. if route_id is None:
  203. return ActionResponse(action=Action.RESPONSE, response="当前没有正在运行的预警。")
  204. return ActionResponse(
  205. action=Action.RESPONSE,
  206. result=str(route_id),
  207. response="预警已停止。",
  208. )
  209. nav_status_desc = {
  210. "type": "function",
  211. "function": {
  212. "name": "nav_status",
  213. "description": "查询路线记录和预警状态。",
  214. "parameters": {"type": "object", "properties": {}, "required": []},
  215. },
  216. }
  217. @register_function("nav_status", nav_status_desc, ToolType.SYSTEM_CTL)
  218. async def nav_status(conn):
  219. svc = get_service(conn)
  220. return ActionResponse(action=Action.RESPONSE, response=svc.status_text())
  221. nav_list_routes_desc = {
  222. "type": "function",
  223. "function": {
  224. "name": "nav_list_routes",
  225. "description": "列出最近保存的路线。",
  226. "parameters": {
  227. "type": "object",
  228. "properties": {
  229. "limit": {
  230. "type": "integer",
  231. "description": "返回数量,默认 10",
  232. }
  233. },
  234. "required": [],
  235. },
  236. },
  237. }
  238. @register_function("nav_list_routes", nav_list_routes_desc, ToolType.SYSTEM_CTL)
  239. async def nav_list_routes(conn, limit: int = 10):
  240. svc = get_service(conn)
  241. routes = svc.db.list_routes(limit=int(limit or 10))
  242. if not routes:
  243. return ActionResponse(action=Action.RESPONSE, response="暂无路线,请先开始记录路线。")
  244. lines = []
  245. for route in routes:
  246. active = "(激活)" if route.active else ""
  247. lines.append(
  248. f"路线 {route.id}{active}:{route.name},模式 {route.mode},点数 {route.points},风险点 {route.hazards}"
  249. )
  250. return ActionResponse(action=Action.RESPONSE, response=";".join(lines))
  251. nav_set_active_route_desc = {
  252. "type": "function",
  253. "function": {
  254. "name": "nav_set_active_route",
  255. "description": "将某条路线设置为当前激活路线。",
  256. "parameters": {
  257. "type": "object",
  258. "properties": {
  259. "route_id": {
  260. "type": "integer",
  261. "description": "路线 ID",
  262. }
  263. },
  264. "required": ["route_id"],
  265. },
  266. },
  267. }
  268. @register_function("nav_set_active_route", nav_set_active_route_desc, ToolType.SYSTEM_CTL)
  269. async def nav_set_active_route(conn, route_id: int):
  270. svc = get_service(conn)
  271. svc.db.set_active_route(int(route_id))
  272. return ActionResponse(
  273. action=Action.RESPONSE,
  274. response=f"已激活路线 ID={int(route_id)}。",
  275. )