Files
JspaceAI/jspaceai/embodied.py
XiuchengWu 300e86956b Implement consensus and memory management for workspace events
- Add ConsensusSnapshot and ConsensusSlot classes for structured consensus state representation.
- Introduce OnlineLanguageLearner for stateful online learning with experience replay and EWC regularization.
- Create WorkspaceEvent and ActionEvent classes for serializable event types.
- Develop InMemoryVectorMemoryStore for storing and querying workspace events.
- Implement ActionPolicy to manage action selection and motor parameterization.
- Establish a unified WorkspaceRuntime for managing the agent's lifecycle and interactions.
- Enhance tests in test_smoke.py to cover new functionalities and ensure correctness.
2026-07-08 12:24:26 +08:00

470 lines
16 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.
"""
输出执行器层 + 神经系统
对应人类神经系统的各部分:
- 大脑皮层: workspace w + 专家池(已在 multimodal.py
- 小脑: 运动控制器(前向模型+逆模型,精细动作)
- 中枢神经: 动作调度器(反射弧+决策门控)
- 海马体: 外部情景记忆库
- 基底神经节: 动作价值学习(习惯化)
- 执行器: 鼠标控制 + 键盘输出 + 音频输出 + 屏幕绘制
核心思想:输出和输入对称。
输入:摄像头/麦克风/屏幕/键盘/鼠标 → 编码 → workspace
输出workspace → 解码 → 鼠标移动/键盘按键/音频播放/屏幕绘制
workspace 是模态无关的"意图空间"
"想点击左上角"这个意图,在 workspace 里是一个向量,
解码到鼠标控制器就是移动+点击,解码到键盘就是 Tab+Enter。
"""
from __future__ import annotations
import torch
import numpy as np
import time
from pynput import mouse as pynput_mouse
from pynput import keyboard as pynput_keyboard
from .consensus import ConsensusSnapshot
from .events import WorkspaceEvent
from .memory import Hippocampus, InMemoryVectorMemoryStore
from .policy import (
ActionPolicy,
BasalGanglia,
Cerebellum,
CentralNervousSystem,
ReflexArc,
compose_action_params,
)
class MouseActuator:
"""
鼠标执行器——对应""
workspace 解码出动作向量 → 移动鼠标 + 点击。
动作空间:
(dx, dy, click_left, click_right, scroll)
dx, dy: 相对移动量(-1 到 1乘以灵敏度
click_left/right: 0 或 1
scroll: 滚动量
"""
def __init__(self, sensitivity: int = 200, enabled: bool = True):
self.sensitivity = sensitivity
self.enabled = enabled
self.controller = pynput_mouse.Controller() if enabled else None
def execute(self, action: np.ndarray):
"""执行鼠标动作
Args:
action: (5,) = (dx, dy, click_l, click_r, scroll)
"""
if not self.enabled or self.controller is None:
return
dx, dy, click_l, click_r, scroll = action
# 移动
if abs(dx) > 0.01 or abs(dy) > 0.01:
self.controller.move(int(dx * self.sensitivity), int(dy * self.sensitivity))
# 点击
if click_l > 0.5:
self.controller.click(pynput_mouse.Button.left)
time.sleep(0.05)
if click_r > 0.5:
self.controller.click(pynput_mouse.Button.right)
time.sleep(0.05)
# 滚动
if abs(scroll) > 0.1:
self.controller.scroll(0, int(scroll * 5))
def get_position(self) -> tuple[int, int]:
if self.controller:
return self.controller.position
return (0, 0)
class KeyboardActuator:
"""
键盘执行器——对应""+""(打字)。
workspace 解码出 token → 按键输入。
"""
def __init__(self, enabled: bool = True):
self.enabled = enabled
self.controller = pynput_keyboard.Controller() if enabled else None
def type_text(self, text: str):
"""输入文本"""
if not self.enabled or self.controller is None:
return
self.controller.type(text)
def press_key(self, key: str):
"""按下单个键"""
if not self.enabled or self.controller is None:
return
try:
self.controller.press(key)
self.controller.release(key)
except Exception:
pass
class AudioActuator:
"""
音频执行器——对应""(说话)。
workspace 解码出音频波形 → 扬声器播放。
"""
def __init__(self, sample_rate: int = 16000, enabled: bool = True):
self.sample_rate = sample_rate
self.enabled = enabled
import sounddevice as sd
self._sd = sd
def play(self, audio: np.ndarray, blocking: bool = False):
"""播放音频"""
if not self.enabled:
return
audio = np.clip(audio, -1, 1).astype(np.float32)
self._sd.play(audio, self.sample_rate)
if blocking:
self._sd.wait()
def stop(self):
if self.enabled:
self._sd.stop()
class ScreenActuator:
"""
屏幕执行器——对应""(绘制)。
在屏幕上绘制 workspace 解码出的图像。
用 OpenCV 显示一个窗口。
"""
def __init__(self, enabled: bool = True, window_name: str = "JspaceAI Output"):
self.enabled = enabled
self.window_name = window_name
import cv2
self._cv2 = cv2
def show_image(self, img: np.ndarray):
"""显示图像"""
if not self.enabled:
return
# img: (H, W, 3) RGB 或 (H, W) 灰度
if img.ndim == 3 and img.shape[2] == 3:
img_bgr = self._cv2.cvtColor(img.astype(np.uint8), self._cv2.COLOR_RGB2BGR)
else:
img_bgr = img.astype(np.uint8)
self._cv2.imshow(self.window_name, img_bgr)
self._cv2.waitKey(1)
def close(self):
if self.enabled:
self._cv2.destroyWindow(self.window_name)
# ============================================================
# 完整的具身 Agent
# ============================================================
class EmbodiedAgent:
"""
完整的具身 Agent——感知-思考-行动闭环。
结构:
感知层(眼耳皮肤)→ FullSensoryStream
↓ 编码
大脑皮层(思考)→ MultimodalJSpaceModel
↓ workspace w
海马体(记忆)→ Hippocampus 存储和检索
ActionPolicy价值 + 电机参数 + 门控)
执行器(手足口)→ MouseActuator + KeyboardActuator + AudioActuator
循环:
1. 感知:从五感获取输入
2. 思考workspace 更新
3. 回忆:海马体检索相关记忆
4. 决策ActionPolicy 选动作并门控
7. 行动:执行器执行
8. 学习:更新策略与海马体
"""
def __init__(self, model, device: str = 'cpu',
enable_mouse_output: bool = False,
enable_keyboard_output: bool = False,
enable_audio_output: bool = True,
enable_screen_output: bool = True,
enable_memory: bool = True,
risk_threshold: float = 0.3):
self.model = model
self.device = device
self.config = model.config
# 感知层
from .desktop import FullSensoryStream
from .platform import get_screen_size
self.screen_w, self.screen_h = get_screen_size()
self.senses = FullSensoryStream(
use_camera=True, use_mic=True, use_desktop=True,
img_size=(self.config.img_size, self.config.img_size),
)
# 执行器层
self.mouse_actuator = MouseActuator(enabled=enable_mouse_output)
self.keyboard_actuator = KeyboardActuator(enabled=enable_keyboard_output)
self.audio_actuator = AudioActuator(enabled=enable_audio_output)
self.screen_actuator = ScreenActuator(enabled=enable_screen_output)
# 策略层
self.policy = ActionPolicy(
workspace_dim=self.config.workspace_dim,
action_dim=5,
n_actions=5,
base_threshold=risk_threshold,
device=device,
)
self.cerebellum = self.policy.motor_controller
self.cns = self.policy.gate
self.hippocampus = InMemoryVectorMemoryStore(
workspace_dim=self.config.workspace_dim,
) if enable_memory else None
self.basal_ganglia = self.policy.value_model
# 内部状态
self.state = model.init_state(1, torch.device(device))
self.step_count = 0
self.running = False
self.risk_threshold = risk_threshold
self.last_consensus: ConsensusSnapshot | None = None
# 设置反射弧
self._setup_reflexes()
def _setup_reflexes(self):
"""设置基本反射"""
# 反射1大声音 → 抑制动作
def loud_noise_condition(state):
audio = state.get('audio')
if audio and hasattr(audio, 'data'):
return np.abs(audio.data).mean() > 0.5
return False
def loud_noise_response():
self.cns.inhibit_score = 0.5
self.policy.add_reflex(ReflexArc(
trigger='loud_noise',
condition=loud_noise_condition,
action=loud_noise_response,
priority=10,
))
def perceive(self) -> dict:
"""感知:从五感获取输入"""
return self.senses.get_latest()
def think(self, sensory_data: dict) -> tuple[torch.Tensor, str]:
"""思考:更新 workspace
Returns:
w: workspace 状态
modality: 本次输入的模态
"""
# 按优先级选模态
modality = None
input_tensor = None
if sensory_data['keyboard']:
key_ids = [ord(ev.data[0]) if ev.data and len(ev.data) == 1 else 0
for ev in sensory_data['keyboard'][:8]]
if key_ids:
modality = 'keyboard'
input_tensor = torch.tensor([key_ids], dtype=torch.long).to(self.device)
elif sensory_data['mouse']:
ev = sensory_data['mouse'][0]
x, y = ev.data
modality = 'mouse'
input_tensor = torch.tensor([[
x / self.screen_w, y / self.screen_h,
1.0 if ev.modifiers.get('button') == 'left' else 0.0,
1.0 if ev.modifiers.get('button') == 'right' else 0.0,
]], dtype=torch.float32).to(self.device)
elif sensory_data['audio']:
modality = 'audio'
input_tensor = torch.tensor([sensory_data['audio'].data],
dtype=torch.float32).to(self.device)
elif sensory_data['screen']:
img = sensory_data['screen'].data
modality = 'screen'
input_tensor = torch.tensor(img, dtype=torch.float32).permute(2, 0, 1).unsqueeze(0).to(self.device) / 127.5 - 1.0
elif sensory_data['camera']:
img = sensory_data['camera'].data
modality = 'image'
input_tensor = torch.tensor(img, dtype=torch.float32).permute(2, 0, 1).unsqueeze(0).to(self.device) / 127.5 - 1.0
if modality is None or input_tensor is None:
return self.state['w'], 'idle'
# 编码 + forward
with torch.no_grad():
x = self.model.encode_modality(modality, input_tensor)
if x.dim() == 1: x = x.unsqueeze(0)
if x.dim() == 3: x = x[:, -1, :]
if x.shape[0] != 1: x = x[-1:]
self.state, alpha, _ = self.model.step(self.state, x)
self.last_consensus = ConsensusSnapshot.from_workspace(
self.state['w'], alpha, modality, self.model.expert_modality
)
return self.state['w'], modality
def remember(self, w: torch.Tensor, context: dict):
"""存储 workspace event 到记忆层"""
if self.hippocampus:
event = WorkspaceEvent.from_tensor(
w,
modality=str(context.get('modality', 'unknown')),
step=int(context.get('step', self.step_count)),
consensus=context.get('consensus', self.last_consensus),
context=context,
)
self.hippocampus.put(event)
def recall_memories(self, w: torch.Tensor, top_k: int = 3) -> list:
"""从海马体回忆"""
if self.hippocampus:
return self.hippocampus.recall(w[0].cpu().numpy(), top_k)
return []
def decide_and_act(self, w: torch.Tensor, modality: str) -> dict:
"""决策和行动
1. 基底神经节选动作
2. 小脑计算动作参数
3. 中枢神经门控
4. 执行器执行
"""
decision = self.policy.decide(w)
action_params = decision.action_params
execute = decision.should_execute
# 4. 执行
if execute:
# 鼠标动作
self.mouse_actuator.execute(action_params)
# 音频输出(从 workspace 解码)
if self.step_count % 10 == 0:
with torch.no_grad():
audio_out = self.model.audio_decoder(w)[0].cpu().numpy()
self.audio_actuator.play(audio_out)
# 屏幕输出
if self.screen_actuator.enabled:
with torch.no_grad():
img_out = self.model.visual_decoder(w)[0].cpu().permute(1, 2, 0).numpy()
img_out = ((img_out + 1) / 2 * 255).clip(0, 255).astype(np.uint8)
self.screen_actuator.show_image(img_out)
self.policy.record_action(action_params, modality)
return decision.to_dict(executed=execute)
def learn(self, w: torch.Tensor, action_idx: int, reward: float = 0.0):
"""学习——更新基底神经节和小脑"""
self.policy.learn(w, action_idx, reward)
def step_once(self) -> dict:
"""执行一步完整的感知-思考-行动循环"""
# 1. 感知
sensory_data = self.perceive()
# 2. 检查反射
reflex_action = self.cns.check_reflexes(sensory_data)
if reflex_action:
reflex_action()
# 3. 思考
w, modality = self.think(sensory_data)
# 4. 回忆
self.recall_memories(w)
# 5. 决策和行动
action_info = self.decide_and_act(w, modality)
# 6. 学习(自监督:预测误差作为 reward
reward = -action_info['risk'] # 简化:风险越低 reward 越高
self.learn(w, action_info['action_idx'], reward)
# 7. 记忆存储
self.remember(w, {
'modality': modality,
'action': action_info,
'step': self.step_count,
})
self.step_count += 1
return {
'step': self.step_count,
'modality': modality,
'w_norm': w.norm().item(),
'action': action_info,
'consensus': self.last_consensus.to_dict() if self.last_consensus else None,
'memories_count': self.hippocampus.size() if self.hippocampus else 0,
}
def run(self, n_steps: int = 100, interval: float = 0.2,
on_step: callable = None):
"""运行感知-思考-行动循环"""
self.running = True
self.senses.start()
print(f"具身 Agent 启动,{n_steps}")
print(f"执行器: mouse={self.mouse_actuator.enabled}, "
f"keyboard={self.keyboard_actuator.enabled}, "
f"audio={self.audio_actuator.enabled}, "
f"screen={self.screen_actuator.enabled}")
print("=" * 60)
step_log = []
try:
for _ in range(n_steps):
if not self.running:
break
info = self.step_once()
step_log.append(info)
if on_step:
on_step(info)
elif info['step'] % 5 == 0:
print(f" step {info['step']:3d} | mod {info['modality']:8s} | "
f"||w|| {info['w_norm']:.3f} | "
f"action {info['action']['action_name']} | "
f"executed {info['action']['executed']}")
time.sleep(interval)
except KeyboardInterrupt:
print("\n用户中断")
finally:
self.running = False
self.senses.stop()
self.audio_actuator.stop()
if self.screen_actuator.enabled:
self.screen_actuator.close()
return step_log