Initial commit: Air Agent Framework v2:全双工对话智能体(打断、插话、主动开口)

This commit is contained in:
WpyQwq
2026-09-19 12:09:51 +08:00
commit fa0cb5824d
26 changed files with 2362 additions and 0 deletions
+129
View File
@@ -0,0 +1,129 @@
from __future__ import annotations
import asyncio
import time
import sys
from typing import Optional
from .kernel import EventBus, Event, EventType
try:
import msvcrt
HAS_MSVCRT = True
except ImportError:
HAS_MSVCRT = False
class InputBuffer:
def __init__(self):
self.text = ""
self.last_key_time = time.time()
self.pause_start: Optional[float] = None
self.sentence_boundaries = {'.', '!', '?', '。', '!', '?', '\n'}
self._committed = ""
def add_char(self, char: str):
now = time.time()
pause = now - self.last_key_time
if pause > 0.3 and self.text:
self.pause_start = now
else:
self.pause_start = None
self.last_key_time = now
if char == '\r':
self._committed = self.text
self.text = ""
return True
elif char == '\b' or char == '\x7f':
self.text = self.text[:-1]
else:
self.text += char
return False
@property
def current_pause(self) -> float:
if not self.text:
return 0.0
return time.time() - self.last_key_time
@property
def is_at_sentence_end(self) -> bool:
return bool(self.text) and self.text[-1] in self.sentence_boundaries
@property
def committed(self) -> str:
return self._committed
def reset_committed(self):
self._committed = ""
def __repr__(self):
return f"InputBuffer(text='{self.text}', pause={self.current_pause:.2f}s)"
class StreamInput:
def __init__(self, bus: EventBus, buffer: Optional[InputBuffer] = None):
self.bus = bus
self.buffer = buffer or InputBuffer()
self._running = False
async def listen_cli(self, on_sentence=None):
self._running = True
if not HAS_MSVCRT:
print("[Air] 当前环境不支持实时按键捕获,使用标准输入模式。")
await self._listen_fallback(on_sentence)
return
while self._running:
if msvcrt.kbhit():
ch = msvcrt.getwch()
if ch == '\x03':
raise KeyboardInterrupt
if ch == '\xe0':
ch2 = msvcrt.getwch()
if ch2 == 'K':
continue
if ch2 == 'M':
continue
continue
is_commit = self.buffer.add_char(ch)
await self.bus.emit(Event(
EventType.USER_INPUT_CHANGE,
{
"text": self.buffer.text,
"pause": self.buffer.current_pause,
"is_end": self.buffer.is_at_sentence_end or is_commit,
},
))
if is_commit and self.buffer.committed.strip():
text = self.buffer.committed.strip()
self.buffer.reset_committed()
await self.bus.emit(Event(EventType.USER_MESSAGE, text))
if on_sentence:
await on_sentence(text)
else:
await asyncio.sleep(0.05)
if self.buffer.text and self.buffer.current_pause > 3.0:
await self.bus.emit(Event(
EventType.USER_PAUSE,
{
"text": self.buffer.text,
"pause": self.buffer.current_pause,
},
))
async def _listen_fallback(self, on_sentence=None):
while self._running:
line = await asyncio.get_event_loop().run_in_executor(
None, sys.stdin.readline
)
line = line.strip()
if line:
await self.bus.emit(Event(EventType.USER_MESSAGE, line))
if on_sentence:
await on_sentence(line)
def stop(self):
self._running = False