Files
workphone-sdk/sdk/app/services/ai_agent.py

323 lines
10 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
AI Agent - 智能意图解析与自动化执行
类似豆包手机的AI自动化功能
功能:
1. 接收语音/文本命令
2. 调用大模型解析意图
3. 生成自动化操作序列
4. 通过ADB/uiautomator2执行
"""
import os
import json
import httpx
import asyncio
import logging
from typing import Dict, List, Optional, Any
from dataclasses import dataclass
logger = logging.getLogger(__name__)
@dataclass
class UIAction:
"""UI操作动作"""
action: str # click, swipe, input_text, open_app, back, home, etc.
params: Dict[str, Any]
description: str = ""
class AIAgent:
"""
AI自动化代理
使用大模型解析用户意图,生成操作序列
"""
def __init__(self):
# v0 API配置
self.api_url = os.getenv("AI_API_URL", "https://api.v0.dev/v1")
self.api_key = os.getenv("AI_API_KEY", "")
self.model = os.getenv("AI_MODEL", "claude-opus")
# 系统提示词
self.system_prompt = """你是一个手机自动化助手,负责将用户的语音指令转换为具体的手机操作序列。
你需要返回一个JSON数组包含要执行的操作。每个操作格式如下
可用的操作类型:
1. open_app: 打开应用
{"action": "open_app", "params": {"package": "com.tencent.mm"}, "description": "打开微信"}
2. click: 点击屏幕坐标
{"action": "click", "params": {"x": 540, "y": 1200}, "description": "点击位置"}
3. swipe: 滑动屏幕
{"action": "swipe", "params": {"x1": 540, "y1": 1500, "x2": 540, "y2": 500, "duration": 300}, "description": "向上滑动"}
4. input_text: 输入文字
{"action": "input_text", "params": {"text": "你好"}, "description": "输入文字"}
5. key_event: 按键事件
{"action": "key_event", "params": {"keycode": "KEYCODE_BACK"}, "description": "返回"}
6. back: 返回
{"action": "back", "params": {}, "description": "点击返回"}
7. home: 回到桌面
{"action": "home", "params": {}, "description": "回到桌面"}
8. wait: 等待
{"action": "wait", "params": {"seconds": 2}, "description": "等待2秒"}
9. screenshot: 截图
{"action": "screenshot", "params": {"path": "/sdcard/screenshot.png"}, "description": "截图"}
常用应用包名:
- 微信: com.tencent.mm
- 抖音: com.ss.android.ugc.aweme
- 支付宝: com.eg.android.AlipayGphone
- 淘宝: com.taobao.taobao
- 微博: com.sina.weibo
- QQ: com.tencent.mobileqq
- 设置: com.android.settings
- 相机: com.android.camera
- 浏览器: com.android.chrome
只返回JSON数组不要其他解释。如果无法理解指令返回空数组 []。
"""
async def parse_command(self, text: str, context: Optional[Dict] = None) -> List[UIAction]:
"""
解析用户命令,返回操作序列
Args:
text: 用户的语音/文本命令
context: 当前上下文当前APP、UI状态等
Returns:
操作序列列表
"""
try:
# 构建消息
messages = [
{"role": "system", "content": self.system_prompt},
{"role": "user", "content": f"用户指令: {text}"}
]
# 如果有上下文,添加到消息中
if context:
context_str = json.dumps(context, ensure_ascii=False)
messages.append({"role": "user", "content": f"当前上下文: {context_str}"})
# 调用AI API
async with httpx.AsyncClient(timeout=30.0) as client:
response = await client.post(
f"{self.api_url}/chat/completions",
headers={
"Authorization": f"Bearer {self.api_key}",
"Content-Type": "application/json"
},
json={
"model": self.model,
"messages": messages,
"temperature": 0.3,
"max_tokens": 1000
}
)
if response.status_code != 200:
logger.error(f"AI API错误: {response.status_code} - {response.text}")
return self._fallback_parse(text)
data = response.json()
content = data["choices"][0]["message"]["content"]
# 解析JSON
return self._parse_json_actions(content)
except Exception as e:
logger.error(f"AI解析失败: {e}")
return self._fallback_parse(text)
def _parse_json_actions(self, content: str) -> List[UIAction]:
"""解析JSON格式的操作序列"""
try:
# 尝试提取JSON
content = content.strip()
if content.startswith("```json"):
content = content[7:]
if content.startswith("```"):
content = content[3:]
if content.endswith("```"):
content = content[:-3]
content = content.strip()
actions_data = json.loads(content)
if not isinstance(actions_data, list):
actions_data = [actions_data]
actions = []
for item in actions_data:
action = UIAction(
action=item.get("action", ""),
params=item.get("params", {}),
description=item.get("description", "")
)
actions.append(action)
return actions
except json.JSONDecodeError as e:
logger.error(f"JSON解析失败: {e}, 内容: {content}")
return []
def _fallback_parse(self, text: str) -> List[UIAction]:
"""
降级解析 - 使用规则匹配
当AI不可用时使用
"""
text = text.lower()
actions = []
# 打开应用
app_keywords = {
"微信": "com.tencent.mm",
"抖音": "com.ss.android.ugc.aweme",
"支付宝": "com.eg.android.AlipayGphone",
"淘宝": "com.taobao.taobao",
"qq": "com.tencent.mobileqq",
"微博": "com.sina.weibo",
"设置": "com.android.settings",
"相机": "com.android.camera",
"浏览器": "com.android.chrome",
}
for keyword, package in app_keywords.items():
if f"打开{keyword}" in text or f"启动{keyword}" in text:
actions.append(UIAction(
action="open_app",
params={"package": package},
description=f"打开{keyword}"
))
return actions
# 返回/回去
if "返回" in text or "回去" in text or "退出" in text:
actions.append(UIAction(
action="back",
params={},
description="点击返回"
))
return actions
# 回到桌面
if "桌面" in text or "主页" in text or "home" in text:
actions.append(UIAction(
action="home",
params={},
description="回到桌面"
))
return actions
# 截图
if "截图" in text or "截屏" in text:
actions.append(UIAction(
action="screenshot",
params={"path": "/sdcard/screenshot.png"},
description="截图"
))
return actions
# 向上/下滑动
if "向上滑" in text or "上滑" in text:
actions.append(UIAction(
action="swipe",
params={"x1": 540, "y1": 1500, "x2": 540, "y2": 500, "duration": 300},
description="向上滑动"
))
return actions
if "向下滑" in text or "下滑" in text:
actions.append(UIAction(
action="swipe",
params={"x1": 540, "y1": 500, "x2": 540, "y2": 1500, "duration": 300},
description="向下滑动"
))
return actions
import re
m = re.search(r'(?:给|发给|跟|对|向)(.+?)(?:发消息|发送|说)[:]?(.+)', text)
if not m:
m = re.search(r'(?:发消息给|发送给)(.+?)[:](.+)', text)
if m:
to_id = m.group(1).strip()
content = m.group(2).strip()
actions.append(UIAction(
action="send_wechat_message",
params={"to_id": to_id, "content": content, "platform": "wechat"},
description=f"{to_id}发微信消息: {content}"
))
return actions
m2 = re.search(r'(?:发消息给|发送消息给|发给)(.+)', text)
if m2:
to_id = m2.group(1).strip()
actions.append(UIAction(
action="send_wechat_message",
params={"to_id": to_id, "content": "", "platform": "wechat"},
description=f"{to_id}发微信消息(需补充内容)"
))
return actions
logger.warning(f"无法解析命令: {text}")
return actions
# 全局实例
ai_agent = AIAgent()
async def process_voice_command(text: str, device_id: str = None) -> Dict:
"""
处理语音命令
Args:
text: 语音识别的文本
device_id: 设备ID
Returns:
处理结果
"""
logger.info(f"处理语音命令: {text} (设备: {device_id})")
# 解析命令
actions = await ai_agent.parse_command(text)
if not actions:
return {
"success": False,
"message": "无法理解您的指令",
"text": text,
"actions": []
}
# 转换为可序列化格式
actions_data = [
{
"action": a.action,
"params": a.params,
"description": a.description
}
for a in actions
]
return {
"success": True,
"message": f"已解析 {len(actions)} 个操作",
"text": text,
"actions": actions_data
}