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.
- minitap/mobile_use/__init__.py +0 -0
- minitap/mobile_use/agents/contextor/contextor.py +42 -0
- minitap/mobile_use/agents/cortex/cortex.md +93 -0
- minitap/mobile_use/agents/cortex/cortex.py +107 -0
- minitap/mobile_use/agents/cortex/types.py +11 -0
- minitap/mobile_use/agents/executor/executor.md +73 -0
- minitap/mobile_use/agents/executor/executor.py +84 -0
- minitap/mobile_use/agents/executor/executor_context_cleaner.py +27 -0
- minitap/mobile_use/agents/executor/utils.py +11 -0
- minitap/mobile_use/agents/hopper/hopper.md +13 -0
- minitap/mobile_use/agents/hopper/hopper.py +45 -0
- minitap/mobile_use/agents/orchestrator/human.md +13 -0
- minitap/mobile_use/agents/orchestrator/orchestrator.md +18 -0
- minitap/mobile_use/agents/orchestrator/orchestrator.py +114 -0
- minitap/mobile_use/agents/orchestrator/types.py +14 -0
- minitap/mobile_use/agents/outputter/human.md +25 -0
- minitap/mobile_use/agents/outputter/outputter.py +75 -0
- minitap/mobile_use/agents/outputter/test_outputter.py +107 -0
- minitap/mobile_use/agents/planner/human.md +12 -0
- minitap/mobile_use/agents/planner/planner.md +64 -0
- minitap/mobile_use/agents/planner/planner.py +64 -0
- minitap/mobile_use/agents/planner/types.py +44 -0
- minitap/mobile_use/agents/planner/utils.py +45 -0
- minitap/mobile_use/agents/summarizer/summarizer.py +34 -0
- minitap/mobile_use/clients/device_hardware_client.py +23 -0
- minitap/mobile_use/clients/ios_client.py +44 -0
- minitap/mobile_use/clients/screen_api_client.py +53 -0
- minitap/mobile_use/config.py +285 -0
- minitap/mobile_use/constants.py +2 -0
- minitap/mobile_use/context.py +65 -0
- minitap/mobile_use/controllers/__init__.py +0 -0
- minitap/mobile_use/controllers/mobile_command_controller.py +379 -0
- minitap/mobile_use/controllers/platform_specific_commands_controller.py +74 -0
- minitap/mobile_use/graph/graph.py +149 -0
- minitap/mobile_use/graph/state.py +73 -0
- minitap/mobile_use/main.py +122 -0
- minitap/mobile_use/sdk/__init__.py +12 -0
- minitap/mobile_use/sdk/agent.py +524 -0
- minitap/mobile_use/sdk/builders/__init__.py +10 -0
- minitap/mobile_use/sdk/builders/agent_config_builder.py +213 -0
- minitap/mobile_use/sdk/builders/index.py +15 -0
- minitap/mobile_use/sdk/builders/task_request_builder.py +218 -0
- minitap/mobile_use/sdk/constants.py +14 -0
- minitap/mobile_use/sdk/examples/README.md +45 -0
- minitap/mobile_use/sdk/examples/__init__.py +1 -0
- minitap/mobile_use/sdk/examples/simple_photo_organizer.py +76 -0
- minitap/mobile_use/sdk/examples/smart_notification_assistant.py +177 -0
- minitap/mobile_use/sdk/types/__init__.py +49 -0
- minitap/mobile_use/sdk/types/agent.py +73 -0
- minitap/mobile_use/sdk/types/exceptions.py +74 -0
- minitap/mobile_use/sdk/types/task.py +191 -0
- minitap/mobile_use/sdk/utils.py +28 -0
- minitap/mobile_use/servers/config.py +19 -0
- minitap/mobile_use/servers/device_hardware_bridge.py +212 -0
- minitap/mobile_use/servers/device_screen_api.py +143 -0
- minitap/mobile_use/servers/start_servers.py +151 -0
- minitap/mobile_use/servers/stop_servers.py +215 -0
- minitap/mobile_use/servers/utils.py +11 -0
- minitap/mobile_use/services/accessibility.py +100 -0
- minitap/mobile_use/services/llm.py +143 -0
- minitap/mobile_use/tools/index.py +54 -0
- minitap/mobile_use/tools/mobile/back.py +52 -0
- minitap/mobile_use/tools/mobile/copy_text_from.py +77 -0
- minitap/mobile_use/tools/mobile/erase_text.py +124 -0
- minitap/mobile_use/tools/mobile/input_text.py +74 -0
- minitap/mobile_use/tools/mobile/launch_app.py +59 -0
- minitap/mobile_use/tools/mobile/list_packages.py +78 -0
- minitap/mobile_use/tools/mobile/long_press_on.py +62 -0
- minitap/mobile_use/tools/mobile/open_link.py +59 -0
- minitap/mobile_use/tools/mobile/paste_text.py +66 -0
- minitap/mobile_use/tools/mobile/press_key.py +58 -0
- minitap/mobile_use/tools/mobile/run_flow.py +57 -0
- minitap/mobile_use/tools/mobile/stop_app.py +58 -0
- minitap/mobile_use/tools/mobile/swipe.py +56 -0
- minitap/mobile_use/tools/mobile/take_screenshot.py +70 -0
- minitap/mobile_use/tools/mobile/tap.py +66 -0
- minitap/mobile_use/tools/mobile/wait_for_animation_to_end.py +68 -0
- minitap/mobile_use/tools/tool_wrapper.py +33 -0
- minitap/mobile_use/utils/cli_helpers.py +40 -0
- minitap/mobile_use/utils/cli_selection.py +144 -0
- minitap/mobile_use/utils/conversations.py +31 -0
- minitap/mobile_use/utils/decorators.py +123 -0
- minitap/mobile_use/utils/errors.py +6 -0
- minitap/mobile_use/utils/file.py +13 -0
- minitap/mobile_use/utils/logger.py +184 -0
- minitap/mobile_use/utils/media.py +73 -0
- minitap/mobile_use/utils/recorder.py +55 -0
- minitap/mobile_use/utils/requests_utils.py +37 -0
- minitap/mobile_use/utils/shell_utils.py +20 -0
- minitap/mobile_use/utils/time.py +6 -0
- minitap/mobile_use/utils/ui_hierarchy.py +30 -0
- minitap_mobile_use-0.0.1.dev0.dist-info/METADATA +274 -0
- minitap_mobile_use-0.0.1.dev0.dist-info/RECORD +95 -0
- minitap_mobile_use-0.0.1.dev0.dist-info/WHEEL +4 -0
- 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)
|