nav_copilot.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331
  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. "开始记录路线。GPS 模式下,标记风险点会保存当前 GPS 位置;"
  10. "结束录制后会进入预警状态。"
  11. "当用户说“开始记录路线”“开始录制路线”“开始记录”时,优先调用这个工具。"
  12. ),
  13. "parameters": {
  14. "type": "object",
  15. "properties": {
  16. "route_name": {
  17. "type": "string",
  18. "description": "路线名称,可选",
  19. },
  20. "sample_hz": {
  21. "type": "integer",
  22. "description": "采样频率,默认 10Hz",
  23. },
  24. "mode": {
  25. "type": "string",
  26. "description": "路线模式。通常使用 gps",
  27. "enum": ["auto", "counter", "gps"],
  28. },
  29. },
  30. "required": [],
  31. },
  32. },
  33. }
  34. @register_function("nav_start_record", nav_start_record_desc, ToolType.SYSTEM_CTL)
  35. async def nav_start_record(
  36. conn,
  37. route_name: str | None = None,
  38. sample_hz: int = 10,
  39. mode: str = "gps",
  40. ):
  41. svc = get_service(conn)
  42. route_id = await svc.start_recording(
  43. route_name=route_name,
  44. sample_hz=sample_hz,
  45. mode=mode,
  46. )
  47. route_mode = svc.record_mode or "auto"
  48. if route_mode == "counter":
  49. text = (
  50. f"已经进入计数路线录制准备状态,路线 ID 是 {route_id}。"
  51. "到风险点时直接说“标记急转弯”或“标记障碍”,说“结束录制”后会进入预警。"
  52. )
  53. else:
  54. text = (
  55. f"已经开始记录 GPS 路线,路线 ID 是 {route_id}。"
  56. "到风险点时可以直接说“标记水坑”“标记急转弯”或“标记障碍”。"
  57. )
  58. return ActionResponse(action=Action.RESPONSE, result=str(route_id), response=text)
  59. nav_stop_record_desc = {
  60. "type": "function",
  61. "function": {
  62. "name": "nav_stop_record",
  63. "description": "结束当前路线记录并保存。如果已经标记过风险点,则自动进入预警状态。",
  64. "parameters": {"type": "object", "properties": {}, "required": []},
  65. },
  66. }
  67. @register_function("nav_stop_record", nav_stop_record_desc, ToolType.SYSTEM_CTL)
  68. async def nav_stop_record(conn):
  69. svc = get_service(conn)
  70. result = await svc.stop_recording()
  71. if result is None:
  72. return ActionResponse(action=Action.RESPONSE, response="当前没有正在记录的路线。")
  73. if result.monitor_started:
  74. if result.mode == "counter":
  75. response = (
  76. f"路线录制已结束,路线 ID 是 {result.route_id}。"
  77. f"已记录 {result.hazard_count} 个风险点,并且已经开始监听预警。"
  78. )
  79. else:
  80. response = (
  81. f"路线记录已结束,路线 ID 是 {result.route_id}。"
  82. f"已记录 {result.hazard_count} 个风险点,并且已经进入预警状态。"
  83. )
  84. else:
  85. response = (
  86. f"路线记录已结束,路线 ID 是 {result.route_id}。"
  87. "但你还没有标记风险点,所以还没有进入预警状态。"
  88. )
  89. return ActionResponse(
  90. action=Action.RESPONSE,
  91. result=str(result.route_id),
  92. response=response,
  93. )
  94. nav_mark_hazard_desc = {
  95. "type": "function",
  96. "function": {
  97. "name": "nav_mark_hazard",
  98. "description": (
  99. "在当前位置标记一个风险点。"
  100. "GPS 模式下会保存当前 GPS 数据,后续进入该风险点范围内时触发预警。"
  101. "典型口令包括“标记水坑”“标记急转弯”“标记障碍”。"
  102. ),
  103. "parameters": {
  104. "type": "object",
  105. "properties": {
  106. "hazard_type": {
  107. "type": "string",
  108. "description": "风险点类型",
  109. "enum": [
  110. "sharp_turn",
  111. "bump",
  112. "jump",
  113. "slope",
  114. "water",
  115. "obstacle",
  116. "other",
  117. ],
  118. },
  119. "note": {
  120. "type": "string",
  121. "description": "补充说明,可选",
  122. },
  123. },
  124. "required": ["hazard_type"],
  125. },
  126. },
  127. }
  128. @register_function("nav_mark_hazard", nav_mark_hazard_desc, ToolType.SYSTEM_CTL)
  129. async def nav_mark_hazard(conn, hazard_type: str, note: str | None = None):
  130. svc = get_service(conn)
  131. result = await svc.mark_hazard(hazard_type=hazard_type, note=note or "")
  132. if result.mode == "counter" and result.counter_value is not None:
  133. response = f"已标记风险点,当前串口值是 {result.counter_value}。"
  134. elif result.mode == "gps" and result.lat is not None and result.lon is not None:
  135. response = f"已标记风险点,GPS 数据是 {result.lat:.6f},{result.lon:.6f}。"
  136. else:
  137. response = "已标记风险点。"
  138. return ActionResponse(
  139. action=Action.RESPONSE,
  140. result=str(result.hazard_id),
  141. response=response,
  142. )
  143. nav_mark_hazard_reject_desc = {
  144. "type": "function",
  145. "function": {
  146. "name": "nav_mark_hazard_reject",
  147. "description": "当用户只说标记风险点但没有说具体类型时,提示其重新说清楚。",
  148. "parameters": {"type": "object", "properties": {}, "required": []},
  149. },
  150. }
  151. @register_function("nav_mark_hazard_reject", nav_mark_hazard_reject_desc, ToolType.SYSTEM_CTL)
  152. async def nav_mark_hazard_reject(conn):
  153. return ActionResponse(
  154. action=Action.RESPONSE,
  155. response="请直接说要标记的风险类型,例如:标记水坑、标记急转弯、标记障碍。",
  156. )
  157. nav_start_run_desc = {
  158. "type": "function",
  159. "function": {
  160. "name": "nav_start_run",
  161. "description": (
  162. "开始预警播报。通常结束录制后会自动进入预警;"
  163. "这个工具用于手动补启。"
  164. ),
  165. "parameters": {
  166. "type": "object",
  167. "properties": {
  168. "route_id": {
  169. "type": "integer",
  170. "description": "指定路线 ID,可选",
  171. },
  172. "tick_hz": {
  173. "type": "integer",
  174. "description": "刷新频率,默认 10Hz",
  175. },
  176. "mode": {
  177. "type": "string",
  178. "description": "路线模式,通常不需要手动指定",
  179. "enum": ["auto", "counter", "gps"],
  180. },
  181. },
  182. "required": [],
  183. },
  184. },
  185. }
  186. @register_function("nav_start_run", nav_start_run_desc, ToolType.SYSTEM_CTL)
  187. async def nav_start_run(
  188. conn,
  189. route_id: int | None = None,
  190. tick_hz: int = 10,
  191. mode: str = "auto",
  192. ):
  193. svc = get_service(conn)
  194. active_route_id = await svc.start_run(route_id=route_id, tick_hz=tick_hz, mode=mode)
  195. if svc.run_mode == "counter":
  196. response = f"已经开始监听预警,使用路线 ID={active_route_id}。"
  197. else:
  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. monitoring = "(预警中)" if route.monitoring else ""
  262. lines.append(
  263. f"路线 {route.id}{active}{monitoring}:{route.name},模式 {route.mode},点数 {route.points},风险点 {route.hazards}"
  264. )
  265. return ActionResponse(action=Action.RESPONSE, response="\n".join(lines))
  266. nav_set_active_route_desc = {
  267. "type": "function",
  268. "function": {
  269. "name": "nav_set_active_route",
  270. "description": "将某条路线设置为当前激活路线。",
  271. "parameters": {
  272. "type": "object",
  273. "properties": {
  274. "route_id": {
  275. "type": "integer",
  276. "description": "路线 ID",
  277. }
  278. },
  279. "required": ["route_id"],
  280. },
  281. },
  282. }
  283. @register_function("nav_set_active_route", nav_set_active_route_desc, ToolType.SYSTEM_CTL)
  284. async def nav_set_active_route(conn, route_id: int):
  285. svc = get_service(conn)
  286. svc.db.set_active_route(int(route_id))
  287. return ActionResponse(
  288. action=Action.RESPONSE,
  289. response=f"已激活路线 ID={int(route_id)}。",
  290. )