minitap-mobile-use 0.0.1.dev0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of minitap-mobile-use might be problematic. Click here for more details.

Files changed (95) hide show
  1. minitap/mobile_use/__init__.py +0 -0
  2. minitap/mobile_use/agents/contextor/contextor.py +42 -0
  3. minitap/mobile_use/agents/cortex/cortex.md +93 -0
  4. minitap/mobile_use/agents/cortex/cortex.py +107 -0
  5. minitap/mobile_use/agents/cortex/types.py +11 -0
  6. minitap/mobile_use/agents/executor/executor.md +73 -0
  7. minitap/mobile_use/agents/executor/executor.py +84 -0
  8. minitap/mobile_use/agents/executor/executor_context_cleaner.py +27 -0
  9. minitap/mobile_use/agents/executor/utils.py +11 -0
  10. minitap/mobile_use/agents/hopper/hopper.md +13 -0
  11. minitap/mobile_use/agents/hopper/hopper.py +45 -0
  12. minitap/mobile_use/agents/orchestrator/human.md +13 -0
  13. minitap/mobile_use/agents/orchestrator/orchestrator.md +18 -0
  14. minitap/mobile_use/agents/orchestrator/orchestrator.py +114 -0
  15. minitap/mobile_use/agents/orchestrator/types.py +14 -0
  16. minitap/mobile_use/agents/outputter/human.md +25 -0
  17. minitap/mobile_use/agents/outputter/outputter.py +75 -0
  18. minitap/mobile_use/agents/outputter/test_outputter.py +107 -0
  19. minitap/mobile_use/agents/planner/human.md +12 -0
  20. minitap/mobile_use/agents/planner/planner.md +64 -0
  21. minitap/mobile_use/agents/planner/planner.py +64 -0
  22. minitap/mobile_use/agents/planner/types.py +44 -0
  23. minitap/mobile_use/agents/planner/utils.py +45 -0
  24. minitap/mobile_use/agents/summarizer/summarizer.py +34 -0
  25. minitap/mobile_use/clients/device_hardware_client.py +23 -0
  26. minitap/mobile_use/clients/ios_client.py +44 -0
  27. minitap/mobile_use/clients/screen_api_client.py +53 -0
  28. minitap/mobile_use/config.py +285 -0
  29. minitap/mobile_use/constants.py +2 -0
  30. minitap/mobile_use/context.py +65 -0
  31. minitap/mobile_use/controllers/__init__.py +0 -0
  32. minitap/mobile_use/controllers/mobile_command_controller.py +379 -0
  33. minitap/mobile_use/controllers/platform_specific_commands_controller.py +74 -0
  34. minitap/mobile_use/graph/graph.py +149 -0
  35. minitap/mobile_use/graph/state.py +73 -0
  36. minitap/mobile_use/main.py +122 -0
  37. minitap/mobile_use/sdk/__init__.py +12 -0
  38. minitap/mobile_use/sdk/agent.py +524 -0
  39. minitap/mobile_use/sdk/builders/__init__.py +10 -0
  40. minitap/mobile_use/sdk/builders/agent_config_builder.py +213 -0
  41. minitap/mobile_use/sdk/builders/index.py +15 -0
  42. minitap/mobile_use/sdk/builders/task_request_builder.py +218 -0
  43. minitap/mobile_use/sdk/constants.py +14 -0
  44. minitap/mobile_use/sdk/examples/README.md +45 -0
  45. minitap/mobile_use/sdk/examples/__init__.py +1 -0
  46. minitap/mobile_use/sdk/examples/simple_photo_organizer.py +76 -0
  47. minitap/mobile_use/sdk/examples/smart_notification_assistant.py +177 -0
  48. minitap/mobile_use/sdk/types/__init__.py +49 -0
  49. minitap/mobile_use/sdk/types/agent.py +73 -0
  50. minitap/mobile_use/sdk/types/exceptions.py +74 -0
  51. minitap/mobile_use/sdk/types/task.py +191 -0
  52. minitap/mobile_use/sdk/utils.py +28 -0
  53. minitap/mobile_use/servers/config.py +19 -0
  54. minitap/mobile_use/servers/device_hardware_bridge.py +212 -0
  55. minitap/mobile_use/servers/device_screen_api.py +143 -0
  56. minitap/mobile_use/servers/start_servers.py +151 -0
  57. minitap/mobile_use/servers/stop_servers.py +215 -0
  58. minitap/mobile_use/servers/utils.py +11 -0
  59. minitap/mobile_use/services/accessibility.py +100 -0
  60. minitap/mobile_use/services/llm.py +143 -0
  61. minitap/mobile_use/tools/index.py +54 -0
  62. minitap/mobile_use/tools/mobile/back.py +52 -0
  63. minitap/mobile_use/tools/mobile/copy_text_from.py +77 -0
  64. minitap/mobile_use/tools/mobile/erase_text.py +124 -0
  65. minitap/mobile_use/tools/mobile/input_text.py +74 -0
  66. minitap/mobile_use/tools/mobile/launch_app.py +59 -0
  67. minitap/mobile_use/tools/mobile/list_packages.py +78 -0
  68. minitap/mobile_use/tools/mobile/long_press_on.py +62 -0
  69. minitap/mobile_use/tools/mobile/open_link.py +59 -0
  70. minitap/mobile_use/tools/mobile/paste_text.py +66 -0
  71. minitap/mobile_use/tools/mobile/press_key.py +58 -0
  72. minitap/mobile_use/tools/mobile/run_flow.py +57 -0
  73. minitap/mobile_use/tools/mobile/stop_app.py +58 -0
  74. minitap/mobile_use/tools/mobile/swipe.py +56 -0
  75. minitap/mobile_use/tools/mobile/take_screenshot.py +70 -0
  76. minitap/mobile_use/tools/mobile/tap.py +66 -0
  77. minitap/mobile_use/tools/mobile/wait_for_animation_to_end.py +68 -0
  78. minitap/mobile_use/tools/tool_wrapper.py +33 -0
  79. minitap/mobile_use/utils/cli_helpers.py +40 -0
  80. minitap/mobile_use/utils/cli_selection.py +144 -0
  81. minitap/mobile_use/utils/conversations.py +31 -0
  82. minitap/mobile_use/utils/decorators.py +123 -0
  83. minitap/mobile_use/utils/errors.py +6 -0
  84. minitap/mobile_use/utils/file.py +13 -0
  85. minitap/mobile_use/utils/logger.py +184 -0
  86. minitap/mobile_use/utils/media.py +73 -0
  87. minitap/mobile_use/utils/recorder.py +55 -0
  88. minitap/mobile_use/utils/requests_utils.py +37 -0
  89. minitap/mobile_use/utils/shell_utils.py +20 -0
  90. minitap/mobile_use/utils/time.py +6 -0
  91. minitap/mobile_use/utils/ui_hierarchy.py +30 -0
  92. minitap_mobile_use-0.0.1.dev0.dist-info/METADATA +274 -0
  93. minitap_mobile_use-0.0.1.dev0.dist-info/RECORD +95 -0
  94. minitap_mobile_use-0.0.1.dev0.dist-info/WHEEL +4 -0
  95. minitap_mobile_use-0.0.1.dev0.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,58 @@
1
+ from typing import Optional
2
+
3
+ from langchain_core.messages import ToolMessage
4
+ from langchain_core.tools import tool
5
+ from langchain_core.tools.base import InjectedToolCallId
6
+ from langgraph.prebuilt import InjectedState
7
+ from langgraph.types import Command
8
+ from minitap.mobile_use.context import MobileUseContext
9
+ from minitap.mobile_use.controllers.mobile_command_controller import stop_app as stop_app_controller
10
+ from minitap.mobile_use.graph.state import State
11
+ from minitap.mobile_use.tools.tool_wrapper import ExecutorMetadata, ToolWrapper
12
+ from typing_extensions import Annotated
13
+
14
+
15
+ def get_stop_app_tool(ctx: MobileUseContext):
16
+ @tool
17
+ def stop_app(
18
+ tool_call_id: Annotated[str, InjectedToolCallId],
19
+ state: Annotated[State, InjectedState],
20
+ agent_thought: str,
21
+ executor_metadata: Optional[ExecutorMetadata],
22
+ package_name: Optional[str] = None,
23
+ ):
24
+ """
25
+ Stops current application if it is running.
26
+ You can also specify the package name of the app to be stopped.
27
+ """
28
+ output = stop_app_controller(ctx=ctx, package_name=package_name)
29
+ has_failed = output is not None
30
+ tool_message = ToolMessage(
31
+ tool_call_id=tool_call_id,
32
+ content=stop_app_wrapper.on_failure_fn(package_name)
33
+ if has_failed
34
+ else stop_app_wrapper.on_success_fn(package_name),
35
+ additional_kwargs={"error": output} if has_failed else {},
36
+ )
37
+ return Command(
38
+ update=stop_app_wrapper.handle_executor_state_fields(
39
+ ctx=ctx,
40
+ state=state,
41
+ executor_metadata=executor_metadata,
42
+ tool_message=tool_message,
43
+ is_failure=has_failed,
44
+ updates={
45
+ "agents_thoughts": [agent_thought],
46
+ "messages": [tool_message],
47
+ },
48
+ ),
49
+ )
50
+
51
+ return stop_app
52
+
53
+
54
+ stop_app_wrapper = ToolWrapper(
55
+ tool_fn_getter=get_stop_app_tool,
56
+ on_success_fn=lambda package_name: f"App {package_name or 'current'} stopped successfully.",
57
+ on_failure_fn=lambda package_name: f"Failed to stop app {package_name or 'current'}.",
58
+ )
@@ -0,0 +1,56 @@
1
+ from typing import Optional
2
+
3
+ from langchain_core.messages import ToolMessage
4
+ from langchain_core.tools import tool
5
+ from langchain_core.tools.base import InjectedToolCallId
6
+ from langgraph.prebuilt import InjectedState
7
+ from langgraph.types import Command
8
+ from minitap.mobile_use.context import MobileUseContext
9
+ from minitap.mobile_use.controllers.mobile_command_controller import SwipeRequest
10
+ from minitap.mobile_use.controllers.mobile_command_controller import swipe as swipe_controller
11
+ from minitap.mobile_use.graph.state import State
12
+ from minitap.mobile_use.tools.tool_wrapper import ExecutorMetadata, ToolWrapper
13
+ from typing_extensions import Annotated
14
+
15
+
16
+ def get_swipe_tool(ctx: MobileUseContext):
17
+ @tool
18
+ def swipe(
19
+ tool_call_id: Annotated[str, InjectedToolCallId],
20
+ state: Annotated[State, InjectedState],
21
+ agent_thought: str,
22
+ executor_metadata: Optional[ExecutorMetadata],
23
+ swipe_request: SwipeRequest,
24
+ ):
25
+ """
26
+ Swipes on the screen.
27
+ """
28
+ output = swipe_controller(ctx=ctx, swipe_request=swipe_request)
29
+ has_failed = output is not None
30
+ tool_message = ToolMessage(
31
+ tool_call_id=tool_call_id,
32
+ content=swipe_wrapper.on_failure_fn() if has_failed else swipe_wrapper.on_success_fn(),
33
+ additional_kwargs={"error": output} if has_failed else {},
34
+ )
35
+ return Command(
36
+ update=swipe_wrapper.handle_executor_state_fields(
37
+ ctx=ctx,
38
+ state=state,
39
+ executor_metadata=executor_metadata,
40
+ tool_message=tool_message,
41
+ is_failure=has_failed,
42
+ updates={
43
+ "agents_thoughts": [agent_thought],
44
+ "messages": [tool_message],
45
+ },
46
+ ),
47
+ )
48
+
49
+ return swipe
50
+
51
+
52
+ swipe_wrapper = ToolWrapper(
53
+ tool_fn_getter=get_swipe_tool,
54
+ on_success_fn=lambda: "Swipe is successful.",
55
+ on_failure_fn=lambda: "Failed to swipe.",
56
+ )
@@ -0,0 +1,70 @@
1
+ from typing import Optional
2
+
3
+ from langchain_core.messages import ToolMessage
4
+ from langchain_core.tools import tool
5
+ from langchain_core.tools.base import InjectedToolCallId
6
+ from langgraph.prebuilt import InjectedState
7
+ from langgraph.types import Command
8
+ from minitap.mobile_use.context import MobileUseContext
9
+ from minitap.mobile_use.controllers.mobile_command_controller import (
10
+ take_screenshot as take_screenshot_controller,
11
+ )
12
+ from minitap.mobile_use.graph.state import State
13
+ from minitap.mobile_use.tools.tool_wrapper import ExecutorMetadata, ToolWrapper
14
+ from minitap.mobile_use.utils.media import compress_base64_jpeg
15
+ from typing_extensions import Annotated
16
+
17
+
18
+ def get_take_screenshot_tool(ctx: MobileUseContext):
19
+ @tool
20
+ def take_screenshot(
21
+ tool_call_id: Annotated[str, InjectedToolCallId],
22
+ state: Annotated[State, InjectedState],
23
+ agent_thought: str,
24
+ executor_metadata: Optional[ExecutorMetadata],
25
+ ):
26
+ """
27
+ Take a screenshot of the device.
28
+ """
29
+ compressed_image_base64 = None
30
+ has_failed = False
31
+
32
+ try:
33
+ output = take_screenshot_controller(ctx=ctx)
34
+ compressed_image_base64 = compress_base64_jpeg(output)
35
+ except Exception as e:
36
+ output = str(e)
37
+ has_failed = True
38
+
39
+ tool_message = ToolMessage(
40
+ tool_call_id=tool_call_id,
41
+ content=take_screenshot_wrapper.on_failure_fn()
42
+ if has_failed
43
+ else take_screenshot_wrapper.on_success_fn(),
44
+ additional_kwargs={"error": output} if has_failed else {},
45
+ )
46
+ updates = {
47
+ "agents_thoughts": [agent_thought],
48
+ "messages": [tool_message],
49
+ }
50
+ if compressed_image_base64:
51
+ updates["latest_screenshot_base64"] = compressed_image_base64
52
+ return Command(
53
+ update=take_screenshot_wrapper.handle_executor_state_fields(
54
+ ctx=ctx,
55
+ state=state,
56
+ executor_metadata=executor_metadata,
57
+ tool_message=tool_message,
58
+ is_failure=has_failed,
59
+ updates=updates,
60
+ ),
61
+ )
62
+
63
+ return take_screenshot
64
+
65
+
66
+ take_screenshot_wrapper = ToolWrapper(
67
+ tool_fn_getter=get_take_screenshot_tool,
68
+ on_success_fn=lambda: "Screenshot taken successfully.",
69
+ on_failure_fn=lambda: "Failed to take screenshot.",
70
+ )
@@ -0,0 +1,66 @@
1
+ from typing import Optional
2
+
3
+ from langchain_core.messages import ToolMessage
4
+ from langchain_core.tools import tool
5
+ from langchain_core.tools.base import InjectedToolCallId
6
+ from langgraph.prebuilt import InjectedState
7
+ from langgraph.types import Command
8
+ from minitap.mobile_use.context import MobileUseContext
9
+ from minitap.mobile_use.controllers.mobile_command_controller import SelectorRequest
10
+ from minitap.mobile_use.controllers.mobile_command_controller import tap as tap_controller
11
+ from minitap.mobile_use.graph.state import State
12
+ from minitap.mobile_use.tools.tool_wrapper import ExecutorMetadata, ToolWrapper
13
+ from typing_extensions import Annotated
14
+
15
+
16
+ def get_tap_tool(ctx: MobileUseContext):
17
+ @tool
18
+ def tap(
19
+ tool_call_id: Annotated[str, InjectedToolCallId],
20
+ state: Annotated[State, InjectedState],
21
+ agent_thought: str,
22
+ executor_metadata: Optional[ExecutorMetadata],
23
+ selector_request: SelectorRequest,
24
+ index: Optional[int] = None,
25
+ ):
26
+ """
27
+ Taps on a selector.
28
+ Index is optional and is used when you have multiple views matching the same selector.
29
+ """
30
+ output = tap_controller(ctx=ctx, selector_request=selector_request, index=index)
31
+ has_failed = output is not None
32
+ tool_message = ToolMessage(
33
+ tool_call_id=tool_call_id,
34
+ content=tap_wrapper.on_failure_fn(selector_request, index)
35
+ if has_failed
36
+ else tap_wrapper.on_success_fn(selector_request, index),
37
+ additional_kwargs={"error": output} if has_failed else {},
38
+ )
39
+ return Command(
40
+ update=tap_wrapper.handle_executor_state_fields(
41
+ ctx=ctx,
42
+ state=state,
43
+ executor_metadata=executor_metadata,
44
+ tool_message=tool_message,
45
+ is_failure=has_failed,
46
+ updates={
47
+ "agents_thoughts": [agent_thought],
48
+ "messages": [tool_message],
49
+ },
50
+ ),
51
+ )
52
+
53
+ return tap
54
+
55
+
56
+ tap_wrapper = ToolWrapper(
57
+ tool_fn_getter=get_tap_tool,
58
+ on_success_fn=(
59
+ lambda selector_request,
60
+ index: f"Tap on {selector_request} {'at index {index}' if index else ''} is successful."
61
+ ),
62
+ on_failure_fn=(
63
+ lambda selector_request,
64
+ index: f"Failed to tap on {selector_request} {'at index {index}' if index else ''}."
65
+ ),
66
+ )
@@ -0,0 +1,68 @@
1
+ from typing import Optional
2
+
3
+ from langchain_core.messages import ToolMessage
4
+ from langchain_core.tools import tool
5
+ from langchain_core.tools.base import InjectedToolCallId
6
+ from langgraph.prebuilt import InjectedState
7
+ from langgraph.types import Command
8
+ from minitap.mobile_use.context import MobileUseContext
9
+ from minitap.mobile_use.controllers.mobile_command_controller import WaitTimeout
10
+ from minitap.mobile_use.controllers.mobile_command_controller import (
11
+ wait_for_animation_to_end as wait_for_animation_to_end_controller,
12
+ )
13
+ from minitap.mobile_use.graph.state import State
14
+ from minitap.mobile_use.tools.tool_wrapper import ExecutorMetadata, ToolWrapper
15
+ from typing_extensions import Annotated
16
+
17
+
18
+ def get_wait_for_animation_to_end_tool(ctx: MobileUseContext):
19
+ @tool
20
+ def wait_for_animation_to_end(
21
+ tool_call_id: Annotated[str, InjectedToolCallId],
22
+ state: Annotated[State, InjectedState],
23
+ agent_thought: str,
24
+ executor_metadata: Optional[ExecutorMetadata],
25
+ timeout: Optional[WaitTimeout],
26
+ ):
27
+ """
28
+ Waits for ongoing animations or videos to finish before continuing.
29
+
30
+ If a `timeout` (in milliseconds) is set, the command proceeds after the timeout even if
31
+ the animation hasn't ended.
32
+ The flow continues immediately once the animation is detected as complete.
33
+
34
+ Example:
35
+ - waitForAnimationToEnd
36
+ - waitForAnimationToEnd: { timeout: 5000 }
37
+ """
38
+ output = wait_for_animation_to_end_controller(ctx=ctx, timeout=timeout)
39
+ has_failed = output is not None
40
+ tool_message = ToolMessage(
41
+ tool_call_id=tool_call_id,
42
+ content=wait_for_animation_to_end_wrapper.on_failure_fn()
43
+ if has_failed
44
+ else wait_for_animation_to_end_wrapper.on_success_fn(timeout),
45
+ additional_kwargs={"error": output} if has_failed else {},
46
+ )
47
+ return Command(
48
+ update=wait_for_animation_to_end_wrapper.handle_executor_state_fields(
49
+ ctx=ctx,
50
+ state=state,
51
+ executor_metadata=executor_metadata,
52
+ tool_message=tool_message,
53
+ is_failure=has_failed,
54
+ updates={
55
+ "agents_thoughts": [agent_thought],
56
+ "messages": [tool_message],
57
+ },
58
+ ),
59
+ )
60
+
61
+ return wait_for_animation_to_end
62
+
63
+
64
+ wait_for_animation_to_end_wrapper = ToolWrapper(
65
+ tool_fn_getter=get_wait_for_animation_to_end_tool,
66
+ on_success_fn=lambda: "Animation ended successfully.",
67
+ on_failure_fn=lambda: "Failed to end animation.",
68
+ )
@@ -0,0 +1,33 @@
1
+ from typing import Callable, Optional
2
+
3
+ from langchain_core.messages import ToolMessage
4
+ from langchain_core.tools import BaseTool
5
+ from pydantic import BaseModel
6
+ from minitap.mobile_use.context import MobileUseContext
7
+ from minitap.mobile_use.graph.state import State
8
+
9
+
10
+ class ExecutorMetadata(BaseModel):
11
+ retrigger: bool
12
+
13
+
14
+ class ToolWrapper(BaseModel):
15
+ tool_fn_getter: Callable[[MobileUseContext], BaseTool]
16
+ on_success_fn: Callable[..., str]
17
+ on_failure_fn: Callable[..., str]
18
+
19
+ def handle_executor_state_fields(
20
+ self,
21
+ ctx: MobileUseContext,
22
+ state: State,
23
+ executor_metadata: Optional[ExecutorMetadata],
24
+ is_failure: bool,
25
+ tool_message: ToolMessage,
26
+ updates: dict,
27
+ ):
28
+ if executor_metadata is None:
29
+ return state.sanitize_update(ctx=ctx, update=updates)
30
+ updates["executor_retrigger"] = executor_metadata.retrigger
31
+ updates["executor_messages"] = [tool_message]
32
+ updates["executor_failed"] = is_failure
33
+ return state.sanitize_update(ctx=ctx, update=updates)
@@ -0,0 +1,40 @@
1
+ import sys
2
+
3
+ from minitap.mobile_use.clients.ios_client import get_ios_devices
4
+ from adbutils import AdbClient
5
+ from rich.console import Console
6
+ from typing import Optional
7
+
8
+
9
+ def display_device_status(console: Console, adb_client: Optional[AdbClient] = None):
10
+ """Checks for connected devices and displays the status."""
11
+ console.print("\n[bold]📱 Device Status[/bold]")
12
+ devices = None
13
+ if adb_client is not None:
14
+ devices = adb_client.device_list()
15
+ if devices:
16
+ console.print("✅ [bold green]Android device(s) connected:[/bold green]")
17
+ for device in devices:
18
+ console.print(f" - {device.serial}")
19
+ else:
20
+ console.print("❌ [bold red]No Android device found.[/bold red]")
21
+ console.print("Please make sure your emulator is running or a device is connected via USB.")
22
+ command = "emulator -avd <avd_name>"
23
+ if sys.platform not in ["win32", "darwin"]:
24
+ command = f"./{command}"
25
+ console.print(f"You can start an emulator using a command like: [bold]'{command}'[/bold]")
26
+ console.print("[italic]iOS detection coming soon...[/italic]")
27
+
28
+ xcrun_available, ios_devices, error_message = get_ios_devices()
29
+ if xcrun_available:
30
+ if ios_devices:
31
+ console.print("✅ [bold green]iOS device(s) connected:[/bold green]")
32
+ for device in ios_devices:
33
+ console.print(f" - {device}")
34
+ else:
35
+ console.print("❌ [bold red]No iOS device found.[/bold red]")
36
+ console.print(
37
+ "[iOS] Please make sure your emulator is running or a device is connected via USB."
38
+ )
39
+ return
40
+ console.print(f"❌ [bold red]iOS check failed:[/bold red] {error_message}")
@@ -0,0 +1,144 @@
1
+ import sys
2
+ from typing import List, Optional
3
+
4
+ import inquirer
5
+ from rich.console import Console
6
+ from rich.prompt import Prompt
7
+
8
+
9
+ def select_provider_and_model(
10
+ console: Console,
11
+ available_providers: List[str],
12
+ available_models: dict,
13
+ default_provider: str,
14
+ default_model: str,
15
+ provider: Optional[str] = None,
16
+ model: Optional[str] = None,
17
+ ) -> tuple[str, str]:
18
+ """
19
+ Interactive selection of LLM provider and model with arrow-key dropdowns when available.
20
+
21
+ Args:
22
+ console: Rich console for output
23
+ available_providers: List of available provider names
24
+ available_models: Dict mapping providers to their available models
25
+ default_provider: Default provider to use
26
+ default_model: Default model to use
27
+ provider: Pre-selected provider (optional)
28
+ model: Pre-selected model (optional)
29
+
30
+ Returns:
31
+ Tuple of (selected_provider, selected_model)
32
+ """
33
+ final_provider = provider
34
+ final_model = model
35
+
36
+ # Interactive provider selection
37
+ if not final_provider:
38
+ console.print("\n🤖 [bold cyan]LLM Provider Selection[/bold cyan]")
39
+ final_provider = _select_from_list(
40
+ console=console,
41
+ item_type="provider",
42
+ choices=available_providers,
43
+ default=default_provider,
44
+ message="Select LLM provider",
45
+ )
46
+
47
+ # Interactive model selection
48
+ if not final_model:
49
+ console.print(f"\n🎯 [bold green]Model Selection for {final_provider}[/bold green]")
50
+ available_model_list = (
51
+ available_models[final_provider]
52
+ if final_provider
53
+ else available_models[default_provider]
54
+ )
55
+
56
+ default_model_for_provider = (
57
+ default_model if default_model in available_model_list else available_model_list[0]
58
+ )
59
+
60
+ final_model = _select_from_list(
61
+ console=console,
62
+ item_type="model",
63
+ choices=available_model_list,
64
+ default=default_model_for_provider,
65
+ message=f"Select model for {final_provider}",
66
+ )
67
+
68
+ return final_provider, final_model
69
+
70
+
71
+ def _select_from_list(
72
+ console: Console,
73
+ item_type: str,
74
+ choices: List[str],
75
+ default: str,
76
+ message: str,
77
+ ) -> str:
78
+ """
79
+ Select an item from a list using arrow keys when available, fallback to numbered selection.
80
+
81
+ Args:
82
+ console: Rich console for output
83
+ item_type: Type of item being selected (for error messages)
84
+ choices: List of choices to select from
85
+ default: Default choice
86
+ message: Message to display in dropdown
87
+
88
+ Returns:
89
+ Selected choice
90
+ """
91
+ # Try arrow-key dropdown if TTY is available, fallback to numbered selection
92
+ if sys.stdin.isatty():
93
+ try:
94
+ questions = [
95
+ inquirer.List(
96
+ "selection",
97
+ message=f"{message} (use arrow keys)",
98
+ choices=choices,
99
+ default=default,
100
+ ),
101
+ ]
102
+ answers = inquirer.prompt(questions)
103
+ return answers["selection"] if answers else default
104
+ except Exception:
105
+ # Fallback to numbered selection
106
+ return _numbered_selection(console, item_type, choices, default)
107
+ else:
108
+ return _numbered_selection(console, item_type, choices, default)
109
+
110
+
111
+ def _numbered_selection(console: Console, item_type: str, choices: List[str], default: str) -> str:
112
+ """Fallback numbered selection when arrow keys aren't available."""
113
+ choices_text = "\n".join([f" {i + 1}. {choice}" for i, choice in enumerate(choices)])
114
+ console.print(f"Available {item_type}s:\n{choices_text}")
115
+
116
+ default_idx = choices.index(default) + 1 if default in choices else 1
117
+
118
+ while True:
119
+ choice = Prompt.ask(
120
+ f"Select {item_type} (1-{len(choices)}) or press Enter for default",
121
+ default=str(default_idx),
122
+ )
123
+ try:
124
+ choice_idx = int(choice) - 1
125
+ if 0 <= choice_idx < len(choices):
126
+ return choices[choice_idx]
127
+ else:
128
+ console.print("[red]Invalid choice. Please try again.[/red]")
129
+ except ValueError:
130
+ console.print("[red]Please enter a number.[/red]")
131
+
132
+
133
+ def display_llm_config(console: Console, provider: str, model: str) -> None:
134
+ """Display the selected LLM configuration with colors."""
135
+ from rich.text import Text
136
+
137
+ config_text = Text()
138
+ config_text.append("🤖 LLM Configuration: ", style="bold white")
139
+ config_text.append("Provider: ", style="white")
140
+ config_text.append(f"{provider}", style="bold cyan")
141
+ config_text.append(" | Model: ", style="white")
142
+ config_text.append(f"{model}", style="bold green")
143
+
144
+ console.print(config_text)
@@ -0,0 +1,31 @@
1
+ from typing import TypeGuard
2
+
3
+ from langchain_core.messages import AIMessage, BaseMessage, HumanMessage, ToolMessage
4
+
5
+
6
+ def is_ai_message(message: BaseMessage) -> TypeGuard[AIMessage]:
7
+ return isinstance(message, AIMessage)
8
+
9
+
10
+ def is_human_message(message: BaseMessage) -> TypeGuard[HumanMessage]:
11
+ return isinstance(message, HumanMessage)
12
+
13
+
14
+ def is_tool_message(message: BaseMessage) -> TypeGuard[ToolMessage]:
15
+ return isinstance(message, ToolMessage)
16
+
17
+
18
+ def is_tool_for_name(tool_message: ToolMessage, name: str) -> bool:
19
+ return tool_message.name == name
20
+
21
+
22
+ def get_screenshot_message_for_llm(screenshot_base64: str):
23
+ prefix = "" if screenshot_base64.startswith("data:image") else "data:image/jpeg;base64,"
24
+ return HumanMessage(
25
+ content=[
26
+ {
27
+ "type": "image_url",
28
+ "image_url": {"url": f"{prefix}{screenshot_base64}"},
29
+ }
30
+ ]
31
+ )
@@ -0,0 +1,123 @@
1
+ import asyncio
2
+ from functools import wraps
3
+ from typing import Any, Awaitable, Callable, Optional, TypeVar, cast, overload
4
+
5
+ R = TypeVar("R")
6
+
7
+
8
+ def wrap_with_callbacks_sync(
9
+ fn: Callable[..., R],
10
+ *,
11
+ before: Optional[Callable[..., None]] = None,
12
+ on_success: Optional[Callable[[R], None]] = None,
13
+ on_failure: Optional[Callable[[Exception], None]] = None,
14
+ suppress_exceptions: bool = False,
15
+ ) -> Callable[..., R]:
16
+ @wraps(fn)
17
+ def wrapper(*args: Any, **kwargs: Any) -> R:
18
+ if before:
19
+ before()
20
+ try:
21
+ result = fn(*args, **kwargs)
22
+ if on_success:
23
+ on_success(result)
24
+ return result
25
+ except Exception as e:
26
+ if on_failure:
27
+ on_failure(e)
28
+ if suppress_exceptions:
29
+ return None # type: ignore
30
+ raise
31
+
32
+ return wrapper
33
+
34
+
35
+ def wrap_with_callbacks_async(
36
+ fn: Callable[..., Awaitable[R]],
37
+ *,
38
+ before: Optional[Callable[..., None]] = None,
39
+ on_success: Optional[Callable[[R], None]] = None,
40
+ on_failure: Optional[Callable[[Exception], None]] = None,
41
+ suppress_exceptions: bool = False,
42
+ ) -> Callable[..., Awaitable[R]]:
43
+ @wraps(fn)
44
+ async def wrapper(*args: Any, **kwargs: Any) -> R:
45
+ if before:
46
+ before()
47
+ try:
48
+ result = await fn(*args, **kwargs)
49
+ if on_success:
50
+ on_success(result)
51
+ return result
52
+ except Exception as e:
53
+ if on_failure:
54
+ on_failure(e)
55
+ if suppress_exceptions:
56
+ return None # type: ignore
57
+ raise
58
+
59
+ return wrapper
60
+
61
+
62
+ @overload
63
+ def wrap_with_callbacks(
64
+ fn: Callable[..., Awaitable[R]],
65
+ *,
66
+ before: Optional[Callable[[], None]] = ...,
67
+ on_success: Optional[Callable[[R], None]] = ...,
68
+ on_failure: Optional[Callable[[Exception], None]] = ...,
69
+ suppress_exceptions: bool = ...,
70
+ ) -> Callable[..., Awaitable[R]]: ...
71
+
72
+
73
+ @overload
74
+ def wrap_with_callbacks(
75
+ *,
76
+ before: Optional[Callable[..., None]] = ...,
77
+ on_success: Optional[Callable[[Any], None]] = ...,
78
+ on_failure: Optional[Callable[[Exception], None]] = ...,
79
+ suppress_exceptions: bool = ...,
80
+ ) -> Callable[[Callable[..., R]], Callable[..., R]]: ...
81
+
82
+
83
+ @overload
84
+ def wrap_with_callbacks(
85
+ fn: Callable[..., R],
86
+ *,
87
+ before: Optional[Callable[[], None]] = ...,
88
+ on_success: Optional[Callable[[R], None]] = ...,
89
+ on_failure: Optional[Callable[[Exception], None]] = ...,
90
+ suppress_exceptions: bool = ...,
91
+ ) -> Callable[..., R]: ...
92
+
93
+
94
+ def wrap_with_callbacks(
95
+ fn: Optional[Callable[..., Any]] = None,
96
+ *,
97
+ before: Optional[Callable[[], None]] = None,
98
+ on_success: Optional[Callable[[Any], None]] = None,
99
+ on_failure: Optional[Callable[[Exception], None]] = None,
100
+ suppress_exceptions: bool = False,
101
+ ) -> Any:
102
+ def decorator(func: Callable[..., Any]) -> Any:
103
+ if asyncio.iscoroutinefunction(func):
104
+ return wrap_with_callbacks_async(
105
+ cast(Callable[..., Awaitable[Any]], func),
106
+ before=before,
107
+ on_success=on_success,
108
+ on_failure=on_failure,
109
+ suppress_exceptions=suppress_exceptions,
110
+ )
111
+ else:
112
+ return wrap_with_callbacks_sync(
113
+ cast(Callable[..., Any], func),
114
+ before=before,
115
+ on_success=on_success,
116
+ on_failure=on_failure,
117
+ suppress_exceptions=suppress_exceptions,
118
+ )
119
+
120
+ if fn is None:
121
+ return decorator
122
+ else:
123
+ return decorator(fn)