autoglm-gui 0.2.0__py3-none-any.whl → 0.2.3__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.
@@ -0,0 +1,109 @@
1
+ """Screenshot utilities for capturing Android device screen."""
2
+
3
+ import base64
4
+ import os
5
+ import subprocess
6
+ import tempfile
7
+ import uuid
8
+ from dataclasses import dataclass
9
+ from io import BytesIO
10
+ from typing import Tuple
11
+
12
+ from PIL import Image
13
+
14
+
15
+ @dataclass
16
+ class Screenshot:
17
+ """Represents a captured screenshot."""
18
+
19
+ base64_data: str
20
+ width: int
21
+ height: int
22
+ is_sensitive: bool = False
23
+
24
+
25
+ def get_screenshot(device_id: str | None = None, timeout: int = 10) -> Screenshot:
26
+ """
27
+ Capture a screenshot from the connected Android device.
28
+
29
+ Args:
30
+ device_id: Optional ADB device ID for multi-device setups.
31
+ timeout: Timeout in seconds for screenshot operations.
32
+
33
+ Returns:
34
+ Screenshot object containing base64 data and dimensions.
35
+
36
+ Note:
37
+ If the screenshot fails (e.g., on sensitive screens like payment pages),
38
+ a black fallback image is returned with is_sensitive=True.
39
+ """
40
+ temp_path = os.path.join(tempfile.gettempdir(), f"screenshot_{uuid.uuid4()}.png")
41
+ adb_prefix = _get_adb_prefix(device_id)
42
+
43
+ try:
44
+ # Execute screenshot command
45
+ result = subprocess.run(
46
+ adb_prefix + ["shell", "screencap", "-p", "/sdcard/tmp.png"],
47
+ capture_output=True,
48
+ text=True,
49
+ timeout=timeout,
50
+ )
51
+
52
+ # Check for screenshot failure (sensitive screen)
53
+ output = result.stdout + result.stderr
54
+ if "Status: -1" in output or "Failed" in output:
55
+ return _create_fallback_screenshot(is_sensitive=True)
56
+
57
+ # Pull screenshot to local temp path
58
+ subprocess.run(
59
+ adb_prefix + ["pull", "/sdcard/tmp.png", temp_path],
60
+ capture_output=True,
61
+ text=True,
62
+ timeout=5,
63
+ )
64
+
65
+ if not os.path.exists(temp_path):
66
+ return _create_fallback_screenshot(is_sensitive=False)
67
+
68
+ # Read and encode image
69
+ img = Image.open(temp_path)
70
+ width, height = img.size
71
+
72
+ buffered = BytesIO()
73
+ img.save(buffered, format="PNG")
74
+ base64_data = base64.b64encode(buffered.getvalue()).decode("utf-8")
75
+
76
+ # Cleanup
77
+ os.remove(temp_path)
78
+
79
+ return Screenshot(
80
+ base64_data=base64_data, width=width, height=height, is_sensitive=False
81
+ )
82
+
83
+ except Exception as e:
84
+ print(f"Screenshot error: {e}")
85
+ return _create_fallback_screenshot(is_sensitive=False)
86
+
87
+
88
+ def _get_adb_prefix(device_id: str | None) -> list:
89
+ """Get ADB command prefix with optional device specifier."""
90
+ if device_id:
91
+ return ["adb", "-s", device_id]
92
+ return ["adb"]
93
+
94
+
95
+ def _create_fallback_screenshot(is_sensitive: bool) -> Screenshot:
96
+ """Create a black fallback image when screenshot fails."""
97
+ default_width, default_height = 1080, 2400
98
+
99
+ black_img = Image.new("RGB", (default_width, default_height), color="black")
100
+ buffered = BytesIO()
101
+ black_img.save(buffered, format="PNG")
102
+ base64_data = base64.b64encode(buffered.getvalue()).decode("utf-8")
103
+
104
+ return Screenshot(
105
+ base64_data=base64_data,
106
+ width=default_width,
107
+ height=default_height,
108
+ is_sensitive=is_sensitive,
109
+ )
phone_agent/agent.py ADDED
@@ -0,0 +1,253 @@
1
+ """Main PhoneAgent class for orchestrating phone automation."""
2
+
3
+ import json
4
+ import traceback
5
+ from dataclasses import dataclass
6
+ from typing import Any, Callable
7
+
8
+ from phone_agent.actions import ActionHandler
9
+ from phone_agent.actions.handler import do, finish, parse_action
10
+ from phone_agent.adb import get_current_app, get_screenshot
11
+ from phone_agent.config import get_messages, get_system_prompt
12
+ from phone_agent.model import ModelClient, ModelConfig
13
+ from phone_agent.model.client import MessageBuilder
14
+
15
+
16
+ @dataclass
17
+ class AgentConfig:
18
+ """Configuration for the PhoneAgent."""
19
+
20
+ max_steps: int = 100
21
+ device_id: str | None = None
22
+ lang: str = "cn"
23
+ system_prompt: str | None = None
24
+ verbose: bool = True
25
+
26
+ def __post_init__(self):
27
+ if self.system_prompt is None:
28
+ self.system_prompt = get_system_prompt(self.lang)
29
+
30
+
31
+ @dataclass
32
+ class StepResult:
33
+ """Result of a single agent step."""
34
+
35
+ success: bool
36
+ finished: bool
37
+ action: dict[str, Any] | None
38
+ thinking: str
39
+ message: str | None = None
40
+
41
+
42
+ class PhoneAgent:
43
+ """
44
+ AI-powered agent for automating Android phone interactions.
45
+
46
+ The agent uses a vision-language model to understand screen content
47
+ and decide on actions to complete user tasks.
48
+
49
+ Args:
50
+ model_config: Configuration for the AI model.
51
+ agent_config: Configuration for the agent behavior.
52
+ confirmation_callback: Optional callback for sensitive action confirmation.
53
+ takeover_callback: Optional callback for takeover requests.
54
+
55
+ Example:
56
+ >>> from phone_agent import PhoneAgent
57
+ >>> from phone_agent.model import ModelConfig
58
+ >>>
59
+ >>> model_config = ModelConfig(base_url="http://localhost:8000/v1")
60
+ >>> agent = PhoneAgent(model_config)
61
+ >>> agent.run("Open WeChat and send a message to John")
62
+ """
63
+
64
+ def __init__(
65
+ self,
66
+ model_config: ModelConfig | None = None,
67
+ agent_config: AgentConfig | None = None,
68
+ confirmation_callback: Callable[[str], bool] | None = None,
69
+ takeover_callback: Callable[[str], None] | None = None,
70
+ ):
71
+ self.model_config = model_config or ModelConfig()
72
+ self.agent_config = agent_config or AgentConfig()
73
+
74
+ self.model_client = ModelClient(self.model_config)
75
+ self.action_handler = ActionHandler(
76
+ device_id=self.agent_config.device_id,
77
+ confirmation_callback=confirmation_callback,
78
+ takeover_callback=takeover_callback,
79
+ )
80
+
81
+ self._context: list[dict[str, Any]] = []
82
+ self._step_count = 0
83
+
84
+ def run(self, task: str) -> str:
85
+ """
86
+ Run the agent to complete a task.
87
+
88
+ Args:
89
+ task: Natural language description of the task.
90
+
91
+ Returns:
92
+ Final message from the agent.
93
+ """
94
+ self._context = []
95
+ self._step_count = 0
96
+
97
+ # First step with user prompt
98
+ result = self._execute_step(task, is_first=True)
99
+
100
+ if result.finished:
101
+ return result.message or "Task completed"
102
+
103
+ # Continue until finished or max steps reached
104
+ while self._step_count < self.agent_config.max_steps:
105
+ result = self._execute_step(is_first=False)
106
+
107
+ if result.finished:
108
+ return result.message or "Task completed"
109
+
110
+ return "Max steps reached"
111
+
112
+ def step(self, task: str | None = None) -> StepResult:
113
+ """
114
+ Execute a single step of the agent.
115
+
116
+ Useful for manual control or debugging.
117
+
118
+ Args:
119
+ task: Task description (only needed for first step).
120
+
121
+ Returns:
122
+ StepResult with step details.
123
+ """
124
+ is_first = len(self._context) == 0
125
+
126
+ if is_first and not task:
127
+ raise ValueError("Task is required for the first step")
128
+
129
+ return self._execute_step(task, is_first)
130
+
131
+ def reset(self) -> None:
132
+ """Reset the agent state for a new task."""
133
+ self._context = []
134
+ self._step_count = 0
135
+
136
+ def _execute_step(
137
+ self, user_prompt: str | None = None, is_first: bool = False
138
+ ) -> StepResult:
139
+ """Execute a single step of the agent loop."""
140
+ self._step_count += 1
141
+
142
+ # Capture current screen state
143
+ screenshot = get_screenshot(self.agent_config.device_id)
144
+ current_app = get_current_app(self.agent_config.device_id)
145
+
146
+ # Build messages
147
+ if is_first:
148
+ self._context.append(
149
+ MessageBuilder.create_system_message(self.agent_config.system_prompt)
150
+ )
151
+
152
+ screen_info = MessageBuilder.build_screen_info(current_app)
153
+ text_content = f"{user_prompt}\n\n{screen_info}"
154
+
155
+ self._context.append(
156
+ MessageBuilder.create_user_message(
157
+ text=text_content, image_base64=screenshot.base64_data
158
+ )
159
+ )
160
+ else:
161
+ screen_info = MessageBuilder.build_screen_info(current_app)
162
+ text_content = f"** Screen Info **\n\n{screen_info}"
163
+
164
+ self._context.append(
165
+ MessageBuilder.create_user_message(
166
+ text=text_content, image_base64=screenshot.base64_data
167
+ )
168
+ )
169
+
170
+ # Get model response
171
+ try:
172
+ response = self.model_client.request(self._context)
173
+ except Exception as e:
174
+ if self.agent_config.verbose:
175
+ traceback.print_exc()
176
+ return StepResult(
177
+ success=False,
178
+ finished=True,
179
+ action=None,
180
+ thinking="",
181
+ message=f"Model error: {e}",
182
+ )
183
+
184
+ # Parse action from response
185
+ try:
186
+ action = parse_action(response.action)
187
+ except ValueError:
188
+ if self.agent_config.verbose:
189
+ traceback.print_exc()
190
+ action = finish(message=response.action)
191
+
192
+ if self.agent_config.verbose:
193
+ # Print thinking process
194
+ msgs = get_messages(self.agent_config.lang)
195
+ print("\n" + "=" * 50)
196
+ print(f"💭 {msgs['thinking']}:")
197
+ print("-" * 50)
198
+ print(response.thinking)
199
+ print("-" * 50)
200
+ print(f"🎯 {msgs['action']}:")
201
+ print(json.dumps(action, ensure_ascii=False, indent=2))
202
+ print("=" * 50 + "\n")
203
+
204
+ # Remove image from context to save space
205
+ self._context[-1] = MessageBuilder.remove_images_from_message(self._context[-1])
206
+
207
+ # Execute action
208
+ try:
209
+ result = self.action_handler.execute(
210
+ action, screenshot.width, screenshot.height
211
+ )
212
+ except Exception as e:
213
+ if self.agent_config.verbose:
214
+ traceback.print_exc()
215
+ result = self.action_handler.execute(
216
+ finish(message=str(e)), screenshot.width, screenshot.height
217
+ )
218
+
219
+ # Add assistant response to context
220
+ self._context.append(
221
+ MessageBuilder.create_assistant_message(
222
+ f"<think>{response.thinking}</think><answer>{response.action}</answer>"
223
+ )
224
+ )
225
+
226
+ # Check if finished
227
+ finished = action.get("_metadata") == "finish" or result.should_finish
228
+
229
+ if finished and self.agent_config.verbose:
230
+ msgs = get_messages(self.agent_config.lang)
231
+ print("\n" + "🎉 " + "=" * 48)
232
+ print(
233
+ f"✅ {msgs['task_completed']}: {result.message or action.get('message', msgs['done'])}"
234
+ )
235
+ print("=" * 50 + "\n")
236
+
237
+ return StepResult(
238
+ success=result.success,
239
+ finished=finished,
240
+ action=action,
241
+ thinking=response.thinking,
242
+ message=result.message or action.get("message"),
243
+ )
244
+
245
+ @property
246
+ def context(self) -> list[dict[str, Any]]:
247
+ """Get the current conversation context."""
248
+ return self._context.copy()
249
+
250
+ @property
251
+ def step_count(self) -> int:
252
+ """Get the current step count."""
253
+ return self._step_count
@@ -0,0 +1,35 @@
1
+ """Configuration module for Phone Agent."""
2
+
3
+ from phone_agent.config.apps import APP_PACKAGES
4
+ from phone_agent.config.i18n import get_message, get_messages
5
+ from phone_agent.config.prompts_en import SYSTEM_PROMPT as SYSTEM_PROMPT_EN
6
+ from phone_agent.config.prompts_zh import SYSTEM_PROMPT as SYSTEM_PROMPT_ZH
7
+
8
+
9
+ def get_system_prompt(lang: str = "cn") -> str:
10
+ """
11
+ Get system prompt by language.
12
+
13
+ Args:
14
+ lang: Language code, 'cn' for Chinese, 'en' for English.
15
+
16
+ Returns:
17
+ System prompt string.
18
+ """
19
+ if lang == "en":
20
+ return SYSTEM_PROMPT_EN
21
+ return SYSTEM_PROMPT_ZH
22
+
23
+
24
+ # Default to Chinese for backward compatibility
25
+ SYSTEM_PROMPT = SYSTEM_PROMPT_ZH
26
+
27
+ __all__ = [
28
+ "APP_PACKAGES",
29
+ "SYSTEM_PROMPT",
30
+ "SYSTEM_PROMPT_ZH",
31
+ "SYSTEM_PROMPT_EN",
32
+ "get_system_prompt",
33
+ "get_messages",
34
+ "get_message",
35
+ ]
@@ -0,0 +1,227 @@
1
+ """App name to package name mapping for supported applications."""
2
+
3
+ APP_PACKAGES: dict[str, str] = {
4
+ # Social & Messaging
5
+ "微信": "com.tencent.mm",
6
+ "QQ": "com.tencent.mobileqq",
7
+ "微博": "com.sina.weibo",
8
+ # E-commerce
9
+ "淘宝": "com.taobao.taobao",
10
+ "京东": "com.jingdong.app.mall",
11
+ "拼多多": "com.xunmeng.pinduoduo",
12
+ "淘宝闪购": "com.taobao.taobao",
13
+ "京东秒送": "com.jingdong.app.mall",
14
+ # Lifestyle & Social
15
+ "小红书": "com.xingin.xhs",
16
+ "豆瓣": "com.douban.frodo",
17
+ "知乎": "com.zhihu.android",
18
+ # Maps & Navigation
19
+ "高德地图": "com.autonavi.minimap",
20
+ "百度地图": "com.baidu.BaiduMap",
21
+ # Food & Services
22
+ "美团": "com.sankuai.meituan",
23
+ "大众点评": "com.dianping.v1",
24
+ "饿了么": "me.ele",
25
+ "肯德基": "com.yek.android.kfc.activitys",
26
+ # Travel
27
+ "携程": "ctrip.android.view",
28
+ "铁路12306": "com.MobileTicket",
29
+ "12306": "com.MobileTicket",
30
+ "去哪儿": "com.Qunar",
31
+ "去哪儿旅行": "com.Qunar",
32
+ "滴滴出行": "com.sdu.did.psnger",
33
+ # Video & Entertainment
34
+ "bilibili": "tv.danmaku.bili",
35
+ "抖音": "com.ss.android.ugc.aweme",
36
+ "快手": "com.smile.gifmaker",
37
+ "腾讯视频": "com.tencent.qqlive",
38
+ "爱奇艺": "com.qiyi.video",
39
+ "优酷视频": "com.youku.phone",
40
+ "芒果TV": "com.hunantv.imgo.activity",
41
+ "红果短剧": "com.phoenix.read",
42
+ # Music & Audio
43
+ "网易云音乐": "com.netease.cloudmusic",
44
+ "QQ音乐": "com.tencent.qqmusic",
45
+ "汽水音乐": "com.luna.music",
46
+ "喜马拉雅": "com.ximalaya.ting.android",
47
+ # Reading
48
+ "番茄小说": "com.dragon.read",
49
+ "番茄免费小说": "com.dragon.read",
50
+ "七猫免费小说": "com.kmxs.reader",
51
+ # Productivity
52
+ "飞书": "com.ss.android.lark",
53
+ "QQ邮箱": "com.tencent.androidqqmail",
54
+ # AI & Tools
55
+ "豆包": "com.larus.nova",
56
+ # Health & Fitness
57
+ "keep": "com.gotokeep.keep",
58
+ "美柚": "com.lingan.seeyou",
59
+ # News & Information
60
+ "腾讯新闻": "com.tencent.news",
61
+ "今日头条": "com.ss.android.article.news",
62
+ # Real Estate
63
+ "贝壳找房": "com.lianjia.beike",
64
+ "安居客": "com.anjuke.android.app",
65
+ # Finance
66
+ "同花顺": "com.hexin.plat.android",
67
+ # Games
68
+ "星穹铁道": "com.miHoYo.hkrpg",
69
+ "崩坏:星穹铁道": "com.miHoYo.hkrpg",
70
+ "恋与深空": "com.papegames.lysk.cn",
71
+ "AndroidSystemSettings": "com.android.settings",
72
+ "Android System Settings": "com.android.settings",
73
+ "Android System Settings": "com.android.settings",
74
+ "Android-System-Settings": "com.android.settings",
75
+ "Settings": "com.android.settings",
76
+ "AudioRecorder": "com.android.soundrecorder",
77
+ "audiorecorder": "com.android.soundrecorder",
78
+ "Bluecoins": "com.rammigsoftware.bluecoins",
79
+ "bluecoins": "com.rammigsoftware.bluecoins",
80
+ "Broccoli": "com.flauschcode.broccoli",
81
+ "broccoli": "com.flauschcode.broccoli",
82
+ "Booking.com": "com.booking",
83
+ "Booking": "com.booking",
84
+ "booking.com": "com.booking",
85
+ "booking": "com.booking",
86
+ "BOOKING.COM": "com.booking",
87
+ "Chrome": "com.android.chrome",
88
+ "chrome": "com.android.chrome",
89
+ "Google Chrome": "com.android.chrome",
90
+ "Clock": "com.android.deskclock",
91
+ "clock": "com.android.deskclock",
92
+ "Contacts": "com.android.contacts",
93
+ "contacts": "com.android.contacts",
94
+ "Duolingo": "com.duolingo",
95
+ "duolingo": "com.duolingo",
96
+ "Expedia": "com.expedia.bookings",
97
+ "expedia": "com.expedia.bookings",
98
+ "Files": "com.android.fileexplorer",
99
+ "files": "com.android.fileexplorer",
100
+ "File Manager": "com.android.fileexplorer",
101
+ "file manager": "com.android.fileexplorer",
102
+ "gmail": "com.google.android.gm",
103
+ "Gmail": "com.google.android.gm",
104
+ "GoogleMail": "com.google.android.gm",
105
+ "Google Mail": "com.google.android.gm",
106
+ "GoogleFiles": "com.google.android.apps.nbu.files",
107
+ "googlefiles": "com.google.android.apps.nbu.files",
108
+ "FilesbyGoogle": "com.google.android.apps.nbu.files",
109
+ "GoogleCalendar": "com.google.android.calendar",
110
+ "Google-Calendar": "com.google.android.calendar",
111
+ "Google Calendar": "com.google.android.calendar",
112
+ "google-calendar": "com.google.android.calendar",
113
+ "google calendar": "com.google.android.calendar",
114
+ "GoogleChat": "com.google.android.apps.dynamite",
115
+ "Google Chat": "com.google.android.apps.dynamite",
116
+ "Google-Chat": "com.google.android.apps.dynamite",
117
+ "GoogleClock": "com.google.android.deskclock",
118
+ "Google Clock": "com.google.android.deskclock",
119
+ "Google-Clock": "com.google.android.deskclock",
120
+ "GoogleContacts": "com.google.android.contacts",
121
+ "Google-Contacts": "com.google.android.contacts",
122
+ "Google Contacts": "com.google.android.contacts",
123
+ "google-contacts": "com.google.android.contacts",
124
+ "google contacts": "com.google.android.contacts",
125
+ "GoogleDocs": "com.google.android.apps.docs.editors.docs",
126
+ "Google Docs": "com.google.android.apps.docs.editors.docs",
127
+ "googledocs": "com.google.android.apps.docs.editors.docs",
128
+ "google docs": "com.google.android.apps.docs.editors.docs",
129
+ "Google Drive": "com.google.android.apps.docs",
130
+ "Google-Drive": "com.google.android.apps.docs",
131
+ "google drive": "com.google.android.apps.docs",
132
+ "google-drive": "com.google.android.apps.docs",
133
+ "GoogleDrive": "com.google.android.apps.docs",
134
+ "Googledrive": "com.google.android.apps.docs",
135
+ "googledrive": "com.google.android.apps.docs",
136
+ "GoogleFit": "com.google.android.apps.fitness",
137
+ "googlefit": "com.google.android.apps.fitness",
138
+ "GoogleKeep": "com.google.android.keep",
139
+ "googlekeep": "com.google.android.keep",
140
+ "GoogleMaps": "com.google.android.apps.maps",
141
+ "Google Maps": "com.google.android.apps.maps",
142
+ "googlemaps": "com.google.android.apps.maps",
143
+ "google maps": "com.google.android.apps.maps",
144
+ "Google Play Books": "com.google.android.apps.books",
145
+ "Google-Play-Books": "com.google.android.apps.books",
146
+ "google play books": "com.google.android.apps.books",
147
+ "google-play-books": "com.google.android.apps.books",
148
+ "GooglePlayBooks": "com.google.android.apps.books",
149
+ "googleplaybooks": "com.google.android.apps.books",
150
+ "GooglePlayStore": "com.android.vending",
151
+ "Google Play Store": "com.android.vending",
152
+ "Google-Play-Store": "com.android.vending",
153
+ "GoogleSlides": "com.google.android.apps.docs.editors.slides",
154
+ "Google Slides": "com.google.android.apps.docs.editors.slides",
155
+ "Google-Slides": "com.google.android.apps.docs.editors.slides",
156
+ "GoogleTasks": "com.google.android.apps.tasks",
157
+ "Google Tasks": "com.google.android.apps.tasks",
158
+ "Google-Tasks": "com.google.android.apps.tasks",
159
+ "Joplin": "net.cozic.joplin",
160
+ "joplin": "net.cozic.joplin",
161
+ "McDonald": "com.mcdonalds.app",
162
+ "mcdonald": "com.mcdonalds.app",
163
+ "Osmand": "net.osmand",
164
+ "osmand": "net.osmand",
165
+ "PiMusicPlayer": "com.Project100Pi.themusicplayer",
166
+ "pimusicplayer": "com.Project100Pi.themusicplayer",
167
+ "Quora": "com.quora.android",
168
+ "quora": "com.quora.android",
169
+ "Reddit": "com.reddit.frontpage",
170
+ "reddit": "com.reddit.frontpage",
171
+ "RetroMusic": "code.name.monkey.retromusic",
172
+ "retromusic": "code.name.monkey.retromusic",
173
+ "SimpleCalendarPro": "com.scientificcalculatorplus.simplecalculator.basiccalculator.mathcalc",
174
+ "SimpleSMSMessenger": "com.simplemobiletools.smsmessenger",
175
+ "Telegram": "org.telegram.messenger",
176
+ "temu": "com.einnovation.temu",
177
+ "Temu": "com.einnovation.temu",
178
+ "Tiktok": "com.zhiliaoapp.musically",
179
+ "tiktok": "com.zhiliaoapp.musically",
180
+ "Twitter": "com.twitter.android",
181
+ "twitter": "com.twitter.android",
182
+ "X": "com.twitter.android",
183
+ "VLC": "org.videolan.vlc",
184
+ "WeChat": "com.tencent.mm",
185
+ "wechat": "com.tencent.mm",
186
+ "Whatsapp": "com.whatsapp",
187
+ "WhatsApp": "com.whatsapp",
188
+ }
189
+
190
+
191
+ def get_package_name(app_name: str) -> str | None:
192
+ """
193
+ Get the package name for an app.
194
+
195
+ Args:
196
+ app_name: The display name of the app.
197
+
198
+ Returns:
199
+ The Android package name, or None if not found.
200
+ """
201
+ return APP_PACKAGES.get(app_name)
202
+
203
+
204
+ def get_app_name(package_name: str) -> str | None:
205
+ """
206
+ Get the app name from a package name.
207
+
208
+ Args:
209
+ package_name: The Android package name.
210
+
211
+ Returns:
212
+ The display name of the app, or None if not found.
213
+ """
214
+ for name, package in APP_PACKAGES.items():
215
+ if package == package_name:
216
+ return name
217
+ return None
218
+
219
+
220
+ def list_supported_apps() -> list[str]:
221
+ """
222
+ Get a list of all supported app names.
223
+
224
+ Returns:
225
+ List of app names.
226
+ """
227
+ return list(APP_PACKAGES.keys())