Coverage for integrations / ui_actions / navigate_tool.py: 100.0%

29 statements  

« prev     ^ index     » next       coverage.py v7.14.0, created at 2026-05-12 04:49 +0000

1"""navigate_tool — LangChain facade for the page registry. 

2 

3Exposed to the chat agent as `Navigate_App`. When the user says 

4'take me to social' / 'open model management' / 'connect whatsapp 

5on the admin page', this tool resolves the best-matching page from 

6PAGE_REGISTRY and returns a structured sentinel the Flask /chat 

7handler intercepts and surfaces to the frontend LiquidActionBar. 

8 

9The tool ALSO writes the resolved ui_action into thread_local_data 

10so chatbot_routes.py can attach it to the final response without 

11having to parse the tool string. The string return value is just 

12what the LLM sees (human-readable so it weaves a natural reply 

13like 'opening Model Management for you'). 

14 

15Single source of truth: this tool, page_registry.PAGE_REGISTRY, and 

16the frontend `pageRegistry.js` all share the same `id` keys. Adding 

17a new page means editing one entry in page_registry.py and mirroring 

18the id + route in the frontend file. 

19""" 

20from __future__ import annotations 

21 

22import json 

23import logging 

24from typing import Optional 

25 

26from .page_registry import ( 

27 PAGE_REGISTRY, 

28 page_to_ui_action, 

29 resolve_page, 

30 list_pages, 

31) 

32 

33logger = logging.getLogger(__name__) 

34 

35 

36def handle_navigate_app( 

37 input_text: str, 

38 user_role: str = 'flat', 

39 thread_local=None, 

40) -> str: 

41 """Resolve a natural-language destination and attach a ui_action. 

42 

43 Args: 

44 input_text: the user's destination phrase (e.g. "model management") 

45 user_role: viewer role for filtering admin-only pages 

46 thread_local: optional thread-local object with set_ui_actions(). 

47 When present the resolved ui_action list is stored on it so 

48 the /chat handler can lift it into the response without 

49 parsing the tool's string return value. 

50 

51 Returns: 

52 A human-readable string the LLM can weave into its final reply. 

53 On no-match, returns a hint listing a few valid destinations so 

54 the LLM asks for clarification. 

55 """ 

56 query = (input_text or '').strip() 

57 if not query: 

58 visible = list_pages(user_role=user_role) 

59 names = ', '.join(p.label for p in visible[:6]) 

60 return ( 

61 f"Navigate_App needs a destination. Available pages include: {names}. " 

62 f"Try calling me again with the page name." 

63 ) 

64 

65 matches = resolve_page(query, user_role=user_role, top_k=3) 

66 if not matches: 

67 visible = list_pages(user_role=user_role) 

68 names = ', '.join(p.label for p in visible[:6]) 

69 return ( 

70 f"I couldn't match '{query}' to any app page. " 

71 f"Known destinations include: {names}." 

72 ) 

73 

74 actions = [page_to_ui_action(page) for page, _score in matches] 

75 

76 # Best match drives the reply text; secondary matches still surface 

77 # as chips so the user can pick another option if we guessed wrong. 

78 best_page, best_score = matches[0] 

79 logger.info( 

80 f"Navigate_App resolved '{query}' → {best_page.id} " 

81 f"(score={best_score}, +{len(matches)-1} runner-ups)" 

82 ) 

83 

84 if thread_local is not None: 

85 try: 

86 thread_local.set_ui_actions(actions) 

87 except Exception as e: 

88 logger.debug(f"thread_local.set_ui_actions failed: {e}") 

89 

90 runner_up = '' 

91 if len(matches) > 1: 

92 runner_up = ' Other options: ' + ', '.join( 

93 f"{p.label}" for p, _ in matches[1:] 

94 ) + '.' 

95 return ( 

96 f"Opening {best_page.label} ({best_page.description}).{runner_up}" 

97 ) 

98 

99 

100def navigate_tool_json_payload(user_role: str = 'flat') -> str: 

101 """JSON blob the Flask /chat handler can return so the frontend 

102 renders the full action bar on first load without waiting for the 

103 user to invoke Navigate_App. Used by the initial LiquidActionBar 

104 hydration call (GET /ui-actions/pages).""" 

105 return json.dumps({ 

106 'pages': [page_to_ui_action(p) for p in list_pages(user_role=user_role)], 

107 })