Files
cosmo-voice-assistant/cosmo/tools_mac.py
d.klimov 6010816f1d Initial commit: Cosmo voice assistant
Полностью локальный голосовой ассистент на Python.

Стек:
- Wake word: openWakeWord (onnxruntime)
- STT: RealtimeSTT + faster-whisper + Silero VAD (CUDA)
- LLM-агент: smolagents ToolCallingAgent + Ollama qwen2.5:7b
- TTS: Silero V4 (torch.hub) + sounddevice
- Shell: Git Bash (Windows) / bash (macOS)

Поддерживает Windows и macOS. Агент с памятью и tool calling —
находит программы самостоятельно, запоминает пути, выполняет
произвольные shell-команды.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 15:58:12 +03:00

207 lines
6.7 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.
"""
Инструменты агента для smolagents — macOS версия.
Отличия от Windows: нативный bash, поиск через mdfind/Spotlight,
запуск приложений через 'open -a', нет реестра.
"""
import os
import subprocess
import webbrowser
import urllib.parse
from loguru import logger
from smolagents import tool
from cosmo.memory import Memory
_memory: Memory | None = None
def set_memory(mem: Memory):
global _memory
_memory = mem
# ------------------------------------------------------------------
# Инструменты
# ------------------------------------------------------------------
@tool
def run_shell(command: str) -> str:
"""
Выполнить команду в bash и получить вывод.
На macOS доступны все стандартные unix-команды.
Для запуска приложений используй: open -a "AppName" или open /path/to/app.
Для поиска файлов: mdfind, find, which.
Args:
command: bash команда для выполнения
"""
logger.info(f"[run_shell] {command}")
try:
result = subprocess.run(
["bash", "-c", command],
capture_output=True,
text=True,
timeout=20,
encoding="utf-8",
errors="replace",
)
output = result.stdout.strip()
stderr = result.stderr.strip()
if result.returncode != 0 and stderr:
return f"[returncode={result.returncode}]\nstdout: {output}\nstderr: {stderr}"
return output if output else f"[выполнено, returncode={result.returncode}]"
except subprocess.TimeoutExpired:
return "[таймаут 20с]"
except Exception as e:
return f"[ошибка: {e}]"
@tool
def find_program(name: str) -> str:
"""
Найти программу или приложение на macOS по имени.
Ищет в PATH, /Applications, ~/Applications и через Spotlight (mdfind).
Args:
name: имя программы, например 'webstorm', 'chrome', 'cursor'
"""
stem = name.strip()
logger.info(f"[find_program] {stem}")
steps = [
# which — ищет в PATH
f"which {stem} 2>/dev/null",
# Spotlight — самый надёжный способ найти .app
f"mdfind 'kMDItemKind == \"Application\"' | grep -i '{stem}' | head -3",
# /Applications напрямую
f"find /Applications -maxdepth 2 -iname '*{stem}*.app' 2>/dev/null | head -3",
# ~/Applications
f"find ~/Applications -maxdepth 2 -iname '*{stem}*.app' 2>/dev/null | head -3",
# Homebrew bin
f"find /opt/homebrew/bin /usr/local/bin -iname '*{stem}*' 2>/dev/null | head -3",
]
results = []
for cmd in steps:
out = run_shell(cmd)
if out and not out.startswith("[") and len(out) > 3:
results.append(out.strip())
if results:
return "Найдено:\n" + "\n".join(results)
return f"Программа '{name}' не найдена."
@tool
def open_browser(url: str, search: bool = False) -> str:
"""
Открыть URL в браузере или выполнить поиск в Google.
Args:
url: полный URL (https://...) или поисковый запрос если search=True
search: если True — выполнить поиск Google по тексту в url
"""
if search:
url = "https://www.google.com/search?q=" + urllib.parse.quote(url)
elif not url.startswith(("http://", "https://")):
url = "https://" + url
webbrowser.open(url)
logger.info(f"[open_browser] {url}")
return f"Открыт браузер: {url}"
@tool
def read_file(path: str) -> str:
"""
Прочитать содержимое текстового файла.
Args:
path: абсолютный путь к файлу
"""
try:
with open(path, "r", encoding="utf-8", errors="replace") as f:
return f.read(8000)
except FileNotFoundError:
return f"Файл не найден: {path}"
except Exception as e:
return f"Ошибка чтения: {e}"
@tool
def write_file(path: str, content: str) -> str:
"""
Записать текст в файл (создаёт или перезаписывает).
Args:
path: абсолютный путь к файлу
content: содержимое файла
"""
try:
dir_path = os.path.dirname(path)
if dir_path:
os.makedirs(dir_path, exist_ok=True)
with open(path, "w", encoding="utf-8") as f:
f.write(content)
return f"Файл записан: {path}"
except Exception as e:
return f"Ошибка записи: {e}"
@tool
def memory_set(key: str, value: str) -> str:
"""
Сохранить факт в долгосрочную память ассистента.
Используй для запоминания путей программ, предпочтений пользователя.
Ключи: 'app.название', 'user.имя', 'pref.что-то'.
Args:
key: ключ факта, например 'app.webstorm'
value: значение для сохранения
"""
if _memory is None:
return "Память не инициализирована"
_memory.set(key, value)
return f"Запомнено: {key} = {value!r}"
@tool
def memory_get(key: str) -> str:
"""
Получить сохранённый факт из долгосрочной памяти по ключу.
Args:
key: ключ факта, например 'app.webstorm'
"""
if _memory is None:
return "Память не инициализирована"
value = _memory.get(key)
return f"{key} = {value!r}" if value else f"Факт '{key}' не найден"
@tool
def memory_list(prefix: str = "") -> str:
"""
Показать все факты из памяти, опционально по префиксу.
Args:
prefix: префикс для фильтрации, например 'app.'
"""
if _memory is None:
return "Память не инициализирована"
facts = _memory.list_facts(prefix)
if not facts:
return "Память пуста" if not prefix else f"Нет фактов с префиксом '{prefix}'"
return "\n".join(f"{k}: {v}" for k, v in sorted(facts.items()))
ALL_TOOLS = [
run_shell,
find_program,
open_browser,
read_file,
write_file,
memory_set,
memory_get,
memory_list,
]