Debug faction turns
This commit is contained in:
parent
7d04a7227d
commit
7cb54a1641
63
README.md
Normal file
63
README.md
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
# LLM Agent Backend
|
||||||
|
|
||||||
|
Бэкенд-сервер на Python для обеспечения работы интеллектуального агента в Obsidian. Использует LangGraph для управления логикой рассуждений и Flask для взаимодействия с плагином.
|
||||||
|
|
||||||
|
## Структура проекта
|
||||||
|
|
||||||
|
├── llm-agent-backend/ # Корневая директория бэкенда
|
||||||
|
│ ├── app/ # Исходный код приложения
|
||||||
|
│ │ ├── api.py # Flask сервер, HTTP эндпоинты и WebSocket (SocketIO)
|
||||||
|
│ │ ├── run.py # Точка входа для запуска приложения
|
||||||
|
│ │ ├── workflows.py # Определение графов LangGraph и логики потоков
|
||||||
|
│ │ ├── react_agent.py # Реализация ReAct (Reasoning and Acting) агента
|
||||||
|
│ │ ├── llm_client.py # Клиент для работы с моделями (OpenAI, Google, Mistral)
|
||||||
|
│ │ ├── mcp_tools.py # Интеграция с Model Context Protocol (MCP) инструментами
|
||||||
|
│ │ ├── graph_history_manager.py # Управление историей диалогов и состоянием графов в SQLite
|
||||||
|
│ │ ├── voice_service.py # Сервис распознавания речи (Vosk)
|
||||||
|
│ │ ├── title_generator.py # Генерация названий для чатов на основе контекста
|
||||||
|
│ │ ├── heartbeat_monitor.py # Мониторинг активности и состояния подключения
|
||||||
|
│ │ ├── models.py # Описание структур данных и схем
|
||||||
|
│ │ └── services.py # Вспомогательные сервисы
|
||||||
|
│ ├── docs/ # Техническая документация и скриншоты настроек
|
||||||
|
│ ├── env/ # Виртуальное окружение Python (venv)
|
||||||
|
│ ├── build.bat # Скрипт сборки проекта
|
||||||
|
│ ├── requirements.txt # Список зависимостей Python
|
||||||
|
│ ├── run-llm-backend.bat # Батник для быстрого запуска сервера
|
||||||
|
│ └── graph_history.db # БД SQLite с историей графов и сообщений
|
||||||
|
|
||||||
|
## Основные технологии
|
||||||
|
- **Python 3.11+**
|
||||||
|
- **LangChain / LangGraph**: Управление состоянием агента и цепочками рассуждений.
|
||||||
|
- **Flask / Flask-SocketIO**: API и real-time взаимодействие с Obsidian.
|
||||||
|
- **Vosk / SoundDevice**: Локальное распознавание речи.
|
||||||
|
- **SQLite**: Хранение истории взаимодействий.
|
||||||
|
|
||||||
|
## Установка
|
||||||
|
|
||||||
|
1. Убедитесь, что установлен Python 3.11.
|
||||||
|
2. Создайте виртуальное окружение в папке `env`:
|
||||||
|
```bash
|
||||||
|
python -m venv env
|
||||||
|
```
|
||||||
|
3. Активируйте окружение и установите зависимости:
|
||||||
|
```bash
|
||||||
|
env\Scripts\activate
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
## Запуск
|
||||||
|
|
||||||
|
### Вариант 1: Через батник (рекомендуется)
|
||||||
|
Запустите `run-llm-backend.bat` из корня проекта.
|
||||||
|
|
||||||
|
### Вариант 2: Вручную из терминала
|
||||||
|
```bash
|
||||||
|
python app/run.py
|
||||||
|
```
|
||||||
|
Сервер будет доступен по адресу: `http://localhost:5000`
|
||||||
|
|
||||||
|
## Ключевые возможности
|
||||||
|
- **Двустороннее общение**: Агент может инициировать запросы к Obsidian (через API плагина).
|
||||||
|
- **Инструменты (Tools)**: Поддержка вызова функций через MCP и кастомные инструменты.
|
||||||
|
- **Память**: Сохранение состояния графов, что позволяет продолжать диалог с любого места.
|
||||||
|
- **Голосовое управление**: Возможность диктовать команды напрямую в микрофон с транскрибацией в реальном времени.
|
||||||
127
app/api.py
127
app/api.py
|
|
@ -12,6 +12,8 @@ from workflows import graph_history_manager, run_agent_streaming
|
||||||
from llm_client import DEFAULT_TEMPERATURE, MODELS, get_llm # Добавляем импорт списка моделей
|
from llm_client import DEFAULT_TEMPERATURE, MODELS, get_llm # Добавляем импорт списка моделей
|
||||||
import base64
|
import base64
|
||||||
from mcp_tools import get_raw_mcp_tools_list
|
from mcp_tools import get_raw_mcp_tools_list
|
||||||
|
import threading
|
||||||
|
import uuid as uuid_lib
|
||||||
|
|
||||||
api = Flask(__name__)
|
api = Flask(__name__)
|
||||||
CORS(
|
CORS(
|
||||||
|
|
@ -423,7 +425,8 @@ def fetch_mcp_tools_route():
|
||||||
return jsonify({"error": "Пустой конфиг сервера"}), 400
|
return jsonify({"error": "Пустой конфиг сервера"}), 400
|
||||||
|
|
||||||
try:
|
try:
|
||||||
tools_list = get_raw_mcp_tools_list(server_config)
|
vault_cwd = current_obsidian_settings.get("vaultAbsolutePath")
|
||||||
|
tools_list = get_raw_mcp_tools_list(server_config, vault_cwd=vault_cwd)
|
||||||
return jsonify(tools_list)
|
return jsonify(tools_list)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({"error": str(e)}), 500
|
return jsonify({"error": str(e)}), 500
|
||||||
|
|
@ -489,3 +492,125 @@ def sync_settings():
|
||||||
|
|
||||||
print(f"✅⚙️ Settings synced: {current_obsidian_settings}")
|
print(f"✅⚙️ Settings synced: {current_obsidian_settings}")
|
||||||
return jsonify({"status": "synced"})
|
return jsonify({"status": "synced"})
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Vault Query Bridge
|
||||||
|
# Позволяет Python-скриптам читать/писать файлы vault через Obsidian.
|
||||||
|
# Схема: POST /api/vault/query → socketio emit → Obsidian отвечает → возврат.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
import threading
|
||||||
|
import uuid as uuid_lib
|
||||||
|
|
||||||
|
# Хранилище ожидающих ответа запросов.
|
||||||
|
# Ключ: request_id (str UUID)
|
||||||
|
# Значение: (threading.Event, dict результата)
|
||||||
|
_vault_query_pending: dict[str, tuple[threading.Event, dict]] = {}
|
||||||
|
|
||||||
|
|
||||||
|
@api.route('/api/vault/query', methods=['POST'])
|
||||||
|
def vault_query_endpoint():
|
||||||
|
"""
|
||||||
|
Проксирует запрос к Obsidian vault через WebSocket.
|
||||||
|
|
||||||
|
Принимает JSON:
|
||||||
|
{
|
||||||
|
"type": "get_frontmatter" | "get_all_factions" | ...,
|
||||||
|
"payload": { ... параметры операции ... }
|
||||||
|
}
|
||||||
|
|
||||||
|
Поддерживаемые типы (обрабатываются на стороне Obsidian VaultQueryHandler):
|
||||||
|
get_frontmatter — YAML + тело одного файла
|
||||||
|
get_all_factions — все файлы в папке (с фильтром по тегу)
|
||||||
|
get_recent_files — последние N файлов (с фильтром по тегам)
|
||||||
|
resolve_wikilink — wikilink → путь к файлу
|
||||||
|
write_frontmatter — перезаписать YAML + тело файла
|
||||||
|
create_file — создать или перезаписать файл
|
||||||
|
|
||||||
|
Блокируется до ответа Obsidian или до истечения timeout (10с).
|
||||||
|
При timeout возвращает 504.
|
||||||
|
"""
|
||||||
|
data = request.get_json(silent=True)
|
||||||
|
if not data:
|
||||||
|
return jsonify({"error": "Пустое тело запроса"}), 400
|
||||||
|
|
||||||
|
query_type = data.get("type")
|
||||||
|
payload = data.get("payload", {})
|
||||||
|
|
||||||
|
if not query_type:
|
||||||
|
return jsonify({"error": "Поле 'type' обязательно"}), 400
|
||||||
|
|
||||||
|
# Генерируем уникальный ID для сопоставления запрос↔ответ
|
||||||
|
request_id = str(uuid_lib.uuid4())
|
||||||
|
|
||||||
|
# Регистрируем ожидание ответа
|
||||||
|
event = threading.Event()
|
||||||
|
result_holder: dict = {}
|
||||||
|
_vault_query_pending[request_id] = (event, result_holder)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Отправляем запрос в Obsidian через WebSocket
|
||||||
|
socketio.emit('vault_query_request', {
|
||||||
|
"request_id": request_id,
|
||||||
|
"type": query_type,
|
||||||
|
"payload": payload
|
||||||
|
})
|
||||||
|
|
||||||
|
# Блокируемся — ждём пока handle_vault_query_response вызовет event.set()
|
||||||
|
answered = event.wait(timeout=10.0)
|
||||||
|
|
||||||
|
finally:
|
||||||
|
# Убираем из pending независимо от результата
|
||||||
|
_vault_query_pending.pop(request_id, None)
|
||||||
|
|
||||||
|
if not answered:
|
||||||
|
return jsonify({
|
||||||
|
"error": (
|
||||||
|
"Timeout: Obsidian не ответил за 10 секунд. "
|
||||||
|
"Проверьте что плагин LLM Agent запущен и подключён к WebSocket."
|
||||||
|
)
|
||||||
|
}), 504
|
||||||
|
|
||||||
|
if "error" in result_holder:
|
||||||
|
return jsonify({"error": result_holder["error"]}), 500
|
||||||
|
|
||||||
|
return jsonify(result_holder.get("data"))
|
||||||
|
|
||||||
|
|
||||||
|
@socketio.on('vault_query_response')
|
||||||
|
def handle_vault_query_response(data):
|
||||||
|
"""
|
||||||
|
Получает ответ от Obsidian на vault-запрос.
|
||||||
|
Будит заблокированный поток в vault_query_endpoint().
|
||||||
|
|
||||||
|
data ожидается вида:
|
||||||
|
{ "request_id": "...", "result": <любые данные> }
|
||||||
|
или
|
||||||
|
{ "request_id": "...", "error": "описание ошибки" }
|
||||||
|
"""
|
||||||
|
if not isinstance(data, dict):
|
||||||
|
print(f"⚠️ vault_query_response: неожиданный формат: {type(data)}")
|
||||||
|
return
|
||||||
|
|
||||||
|
request_id = data.get("request_id")
|
||||||
|
if not request_id:
|
||||||
|
print("⚠️ vault_query_response: отсутствует request_id")
|
||||||
|
return
|
||||||
|
|
||||||
|
pending = _vault_query_pending.get(request_id)
|
||||||
|
if not pending:
|
||||||
|
# Дубль после таймаута или успешной обработки — это нормально
|
||||||
|
# если исправлен WebSocketService. До исправления — признак бага.
|
||||||
|
print(f"⚠️ дубль vault_query_response [{request_id[:8]}] — игнорируем")
|
||||||
|
return
|
||||||
|
|
||||||
|
event, result_holder = pending
|
||||||
|
|
||||||
|
if "error" in data:
|
||||||
|
result_holder["error"] = data["error"]
|
||||||
|
else:
|
||||||
|
result_holder["data"] = data.get("result")
|
||||||
|
print(f"✅ vault_query_response [{request_id[:8]}]: тип={type(result_holder['data']).__name__}")
|
||||||
|
|
||||||
|
event.set()
|
||||||
|
# pending будет удалён в finally блока vault_query_endpoint
|
||||||
|
|
@ -77,6 +77,18 @@ MODELS: Dict[str, Dict[str, Any]] = {
|
||||||
"include_reasoning": True
|
"include_reasoning": True
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"gemini-3.0-flash-routerai": {
|
||||||
|
"name": "google/gemini-3-flash-preview",
|
||||||
|
"provider": "openai",
|
||||||
|
"model_name": "google/gemini-3-flash-preview",
|
||||||
|
"apiBase": "https://routerai.ru/api/v1",
|
||||||
|
"apiKey": "sk-QXEteDDGCbAgfyXgSlGFdU7WXc7jCbwB",
|
||||||
|
"stream": True,
|
||||||
|
"capabilities": ["vision", "reasoning"],
|
||||||
|
"model_kwargs": {
|
||||||
|
"include_reasoning": True
|
||||||
|
}
|
||||||
|
},
|
||||||
"gemini-3.1-pro-openrouter": {
|
"gemini-3.1-pro-openrouter": {
|
||||||
"name": "google/gemini-3.1-pro-preview",
|
"name": "google/gemini-3.1-pro-preview",
|
||||||
"provider": "openai",
|
"provider": "openai",
|
||||||
|
|
@ -144,6 +156,18 @@ MODELS: Dict[str, Dict[str, Any]] = {
|
||||||
"include_reasoning": True
|
"include_reasoning": True
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"claude-sonnet-5-routerai": {
|
||||||
|
"name": "anthropic/claude-sonnet-5",
|
||||||
|
"provider": "openai",
|
||||||
|
"model_name": "anthropic/claude-sonnet-5",
|
||||||
|
"apiBase": "https://routerai.ru/api/v1",
|
||||||
|
"apiKey": "sk-QXEteDDGCbAgfyXgSlGFdU7WXc7jCbwB",
|
||||||
|
"stream": True,
|
||||||
|
"capabilities": ["vision", "reasoning"],
|
||||||
|
"model_kwargs": {
|
||||||
|
"include_reasoning": True
|
||||||
|
}
|
||||||
|
},
|
||||||
"deepseek-v4-flash-openrouter": {
|
"deepseek-v4-flash-openrouter": {
|
||||||
"name": "deepseek/deepseek-v4-flash",
|
"name": "deepseek/deepseek-v4-flash",
|
||||||
"provider": "openai",
|
"provider": "openai",
|
||||||
|
|
@ -156,6 +180,27 @@ MODELS: Dict[str, Dict[str, Any]] = {
|
||||||
"model_kwargs": {
|
"model_kwargs": {
|
||||||
"include_reasoning": True
|
"include_reasoning": True
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"deepseek-v4-flash-routerai": {
|
||||||
|
"name": "deepseek/deepseek-chat",
|
||||||
|
"provider": "openai",
|
||||||
|
"model_name": "deepseek/deepseek-chat",
|
||||||
|
"apiBase": "https://routerai.ru/api/v1",
|
||||||
|
"apiKey": "sk-QXEteDDGCbAgfyXgSlGFdU7WXc7jCbwB",
|
||||||
|
"stream": True,
|
||||||
|
"capabilities": ["vision", "reasoning"],
|
||||||
|
"model_kwargs": {
|
||||||
|
"include_reasoning": True
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"deepseek-v3.2-exp-openrouter": {
|
||||||
|
"name": "deepseek/deepseek-v3.2-exp",
|
||||||
|
"provider": "openrouter",
|
||||||
|
"model_name": "deepseek/deepseek-v3.2-exp",
|
||||||
|
"apiBase": "https://openrouter.ai/api/v1",
|
||||||
|
"apiKey": "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",
|
||||||
|
"stream": True,
|
||||||
|
"capabilities": ["chat", "reasoning", "code"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@ def json_schema_to_pydantic(schema: dict, model_name: str) -> type[BaseModel]:
|
||||||
|
|
||||||
return create_model(model_name, **fields)
|
return create_model(model_name, **fields)
|
||||||
|
|
||||||
def create_mcp_tool(server_config, tool_name, tool_desc, json_schema, full_env, args, debug_callback=None):
|
def create_mcp_tool(server_config, tool_name, tool_desc, json_schema, full_env, args, debug_callback=None, vault_cwd=None):
|
||||||
"""Обертка, которая поднимает контейнер/процесс ровно на 1 вызов тула и убивает его"""
|
"""Обертка, которая поднимает контейнер/процесс ровно на 1 вызов тула и убивает его"""
|
||||||
args_schema = json_schema_to_pydantic(json_schema, f"MCP_{tool_name.replace('-','_')}_Schema")
|
args_schema = json_schema_to_pydantic(json_schema, f"MCP_{tool_name.replace('-','_')}_Schema")
|
||||||
command = server_config.get("command")
|
command = server_config.get("command")
|
||||||
|
|
@ -48,7 +48,7 @@ def create_mcp_tool(server_config, tool_name, tool_desc, json_schema, full_env,
|
||||||
input_json = json.dumps(kwargs, indent=2, ensure_ascii=False)
|
input_json = json.dumps(kwargs, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
async def _run():
|
async def _run():
|
||||||
server_params = StdioServerParameters(command=command, args=args, env=full_env)
|
server_params = StdioServerParameters(command=command, args=args, env=full_env, cwd=vault_cwd)
|
||||||
async with stdio_client(server_params) as (read, write):
|
async with stdio_client(server_params) as (read, write):
|
||||||
async with ClientSession(read, write) as session:
|
async with ClientSession(read, write) as session:
|
||||||
await session.initialize()
|
await session.initialize()
|
||||||
|
|
@ -115,7 +115,7 @@ def create_mcp_tool(server_config, tool_name, tool_desc, json_schema, full_env,
|
||||||
handle_tool_error=True # Разрешает агенту "выжить" после ошибки тула и сгенерировать ответ
|
handle_tool_error=True # Разрешает агенту "выжить" после ошибки тула и сгенерировать ответ
|
||||||
)
|
)
|
||||||
|
|
||||||
def fetch_mcp_tools(server_config, debug_callback=None):
|
def fetch_mcp_tools(server_config, debug_callback=None, vault_cwd=None):
|
||||||
"""Один раз читает список тулов от сервера и кеширует их схемы"""
|
"""Один раз читает список тулов от сервера и кеширует их схемы"""
|
||||||
if not MCP_AVAILABLE:
|
if not MCP_AVAILABLE:
|
||||||
print("⚠️ MCP серверы настроены, но библиотека 'mcp' не установлена. Выполните: pip install mcp")
|
print("⚠️ MCP серверы настроены, но библиотека 'mcp' не установлена. Выполните: pip install mcp")
|
||||||
|
|
@ -151,7 +151,7 @@ def fetch_mcp_tools(server_config, debug_callback=None):
|
||||||
env_dict[k.strip()] = v.strip()
|
env_dict[k.strip()] = v.strip()
|
||||||
|
|
||||||
full_env = {**os.environ.copy(), **env_dict}
|
full_env = {**os.environ.copy(), **env_dict}
|
||||||
server_params = StdioServerParameters(command=command, args=args, env=full_env)
|
server_params = StdioServerParameters(command=command, args=args, env=full_env, cwd=vault_cwd)
|
||||||
|
|
||||||
async with stdio_client(server_params) as (read, write):
|
async with stdio_client(server_params) as (read, write):
|
||||||
async with ClientSession(read, write) as session:
|
async with ClientSession(read, write) as session:
|
||||||
|
|
@ -180,7 +180,7 @@ def fetch_mcp_tools(server_config, debug_callback=None):
|
||||||
else:
|
else:
|
||||||
final_description = t.description
|
final_description = t.description
|
||||||
|
|
||||||
wrapped_tools.append(create_mcp_tool(server_config, t.name, final_description, t.inputSchema, full_env, args, debug_callback))
|
wrapped_tools.append(create_mcp_tool(server_config, t.name, final_description, t.inputSchema, full_env, args, debug_callback, vault_cwd))
|
||||||
|
|
||||||
_MCP_CACHED_TOOLS[config_hash] = wrapped_tools
|
_MCP_CACHED_TOOLS[config_hash] = wrapped_tools
|
||||||
print(f"🔌 Успешно загружено {len(wrapped_tools)} инструментов от MCP-сервера '{server_config.get('name')}'")
|
print(f"🔌 Успешно загружено {len(wrapped_tools)} инструментов от MCP-сервера '{server_config.get('name')}'")
|
||||||
|
|
@ -189,7 +189,7 @@ def fetch_mcp_tools(server_config, debug_callback=None):
|
||||||
print(f"❌ Ошибка инициализации MCP сервера {server_config.get('name')}: {e}")
|
print(f"❌ Ошибка инициализации MCP сервера {server_config.get('name')}: {e}")
|
||||||
return []
|
return []
|
||||||
|
|
||||||
def get_raw_mcp_tools_list(server_config):
|
def get_raw_mcp_tools_list(server_config, vault_cwd=None):
|
||||||
"""Метод для UI: просто возвращает список доступных инструментов в JSON-friendly формате"""
|
"""Метод для UI: просто возвращает список доступных инструментов в JSON-friendly формате"""
|
||||||
if not MCP_AVAILABLE:
|
if not MCP_AVAILABLE:
|
||||||
return []
|
return []
|
||||||
|
|
@ -212,7 +212,7 @@ def get_raw_mcp_tools_list(server_config):
|
||||||
env_dict[k.strip()] = v.strip()
|
env_dict[k.strip()] = v.strip()
|
||||||
|
|
||||||
full_env = {**os.environ.copy(), **env_dict}
|
full_env = {**os.environ.copy(), **env_dict}
|
||||||
server_params = StdioServerParameters(command=command, args=args, env=full_env)
|
server_params = StdioServerParameters(command=command, args=args, env=full_env, cwd=vault_cwd)
|
||||||
|
|
||||||
async with stdio_client(server_params) as (read, write):
|
async with stdio_client(server_params) as (read, write):
|
||||||
async with ClientSession(read, write) as session:
|
async with ClientSession(read, write) as session:
|
||||||
|
|
|
||||||
|
|
@ -55,6 +55,12 @@ def create_subprocess_tool(script_path: str, method_name: str,
|
||||||
spec = importlib.util.spec_from_file_location("user_dynamic_tool", script_path)
|
spec = importlib.util.spec_from_file_location("user_dynamic_tool", script_path)
|
||||||
module = importlib.util.module_from_spec(spec)
|
module = importlib.util.module_from_spec(spec)
|
||||||
spec.loader.exec_module(module)
|
spec.loader.exec_module(module)
|
||||||
|
|
||||||
|
if not hasattr(module, method_name):
|
||||||
|
error_msg = f"в файле '{script_path}' не найден метод '{method_name}'"
|
||||||
|
print(f"❌ {error_msg}")
|
||||||
|
raise AttributeError(error_msg)
|
||||||
|
|
||||||
func = getattr(module, method_name)
|
func = getattr(module, method_name)
|
||||||
docstring = inspect.getdoc(func) or f"Выполняет скрипт {method_name}"
|
docstring = inspect.getdoc(func) or f"Выполняет скрипт {method_name}"
|
||||||
|
|
||||||
|
|
@ -77,6 +83,12 @@ import sys
|
||||||
import importlib.util
|
import importlib.util
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
# ПРИНУДИТЕЛЬНО устанавливаем кодировку UTF-8 для потоков вывода и ошибок, чтобы могло выводить эмодзи, например, без ошибок
|
||||||
|
if hasattr(sys.stdout, 'reconfigure'):
|
||||||
|
sys.stdout.reconfigure(encoding='utf-8')
|
||||||
|
if hasattr(sys.stderr, 'reconfigure'):
|
||||||
|
sys.stderr.reconfigure(encoding='utf-8')
|
||||||
|
|
||||||
# Добавляем папку со скриптом в sys.path, чтобы работали локальные импорты (например, norminv)
|
# Добавляем папку со скриптом в sys.path, чтобы работали локальные импорты (например, norminv)
|
||||||
path_to_add = os.path.dirname(os.path.abspath(r'{script_path}'))
|
path_to_add = os.path.dirname(os.path.abspath(r'{script_path}'))
|
||||||
original_path = sys.path.copy()
|
original_path = sys.path.copy()
|
||||||
|
|
@ -174,30 +186,15 @@ def get_dynamic_tools(obsidian_settings: dict, debug_callback=None):
|
||||||
for ct in custom_tools:
|
for ct in custom_tools:
|
||||||
name = ct.get("name")
|
name = ct.get("name")
|
||||||
desc = ct.get("description")
|
desc = ct.get("description")
|
||||||
full_path = ct.get("scriptPath", "")
|
|
||||||
# Поддерживаем оба ключа, если в JS/TS они не раскрылись
|
# Поддерживаем оба ключа, если в JS/TS они не раскрылись
|
||||||
hidden_prompt = ct.get("hiddenPromptExpanded") or ct.get("hiddenPrompt", "")
|
hidden_prompt = ct.get("hiddenPromptExpanded") or ct.get("hiddenPrompt", "")
|
||||||
|
|
||||||
if not name:
|
if not name:
|
||||||
continue # Инструмент без имени создать нельзя
|
continue # Инструмент без имени создать нельзя
|
||||||
|
|
||||||
# Парсим путь к скрипту (учитываем Windows-пути с буквой диска)
|
# Достаем путь и метод из отдельных полей, присланных фронтендом
|
||||||
script_path = ""
|
script_path = ct.get("scriptPath", "")
|
||||||
method_name = "main"
|
method_name = ct.get("methodName") or "main"
|
||||||
|
|
||||||
if full_path:
|
|
||||||
parts = full_path.rsplit(":", 1) # Разделяем строку с конца 1 раз
|
|
||||||
|
|
||||||
if len(parts) == 2:
|
|
||||||
# Если слева от разделения ровно 1 символ-буква (например, 'D'),
|
|
||||||
# значит это путь без метода, а двоеточие — от диска Windows.
|
|
||||||
if len(parts[0]) == 1 and parts[0].isalpha():
|
|
||||||
script_path = full_path
|
|
||||||
else:
|
|
||||||
script_path = parts[0]
|
|
||||||
method_name = parts[1]
|
|
||||||
else:
|
|
||||||
script_path = full_path
|
|
||||||
|
|
||||||
# ПРОВЕРКА: Есть ли физический скрипт?
|
# ПРОВЕРКА: Есть ли физический скрипт?
|
||||||
is_valid_script = script_path and os.path.exists(script_path)
|
is_valid_script = script_path and os.path.exists(script_path)
|
||||||
|
|
@ -250,6 +247,8 @@ def get_dynamic_tools(obsidian_settings: dict, debug_callback=None):
|
||||||
)
|
)
|
||||||
tools.append(help_tool)
|
tools.append(help_tool)
|
||||||
|
|
||||||
|
vault_cwd = obsidian_settings.get("vaultAbsolutePath")
|
||||||
|
|
||||||
# Инициализация внутренних MCP серверов
|
# Инициализация внутренних MCP серверов
|
||||||
mcp_servers = obsidian_settings.get("mcpServers", [])
|
mcp_servers = obsidian_settings.get("mcpServers", [])
|
||||||
for mcp_server in mcp_servers:
|
for mcp_server in mcp_servers:
|
||||||
|
|
@ -257,7 +256,7 @@ def get_dynamic_tools(obsidian_settings: dict, debug_callback=None):
|
||||||
if not mcp_server.get("isEnabled", True):
|
if not mcp_server.get("isEnabled", True):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
mcp_tools = fetch_mcp_tools(mcp_server, debug_callback)
|
mcp_tools = fetch_mcp_tools(mcp_server, debug_callback, vault_cwd=vault_cwd)
|
||||||
tools.extend(mcp_tools)
|
tools.extend(mcp_tools)
|
||||||
|
|
||||||
return tools
|
return tools
|
||||||
|
|
@ -316,12 +315,33 @@ def _generate_commands_list(custom_tools: list[dict[str, Any]]) -> str:
|
||||||
if not custom_tools:
|
if not custom_tools:
|
||||||
return "В данный момент нет доступных дополнительных команд. Ты работаешь как обычный собеседник."
|
return "В данный момент нет доступных дополнительных команд. Ты работаешь как обычный собеседник."
|
||||||
|
|
||||||
lines = ["Я могу использовать следующие инструменты/команды:\n"]
|
tool_raw_list = ["Я могу использовать следующие инструменты/команды:\n"]
|
||||||
for ct in custom_tools:
|
for ct in custom_tools:
|
||||||
name = ct.get("name")
|
name = ct.get("name")
|
||||||
desc = ct.get("description", "Описание отсутствует")
|
desc = ct.get("description", "Описание отсутствует")
|
||||||
if name:
|
if name:
|
||||||
lines.append(f"- **{name}**: {desc}")
|
tool_raw_list.append(f"- **{name}**: {desc}")
|
||||||
|
|
||||||
lines.append("\nПерескажи этот список пользователю в удобном формате.")
|
# Приаттачиваем инструкцию формата вывода (hidden prompt для системы)
|
||||||
return "\n".join(lines)
|
prompt = f"""
|
||||||
|
СПИСОК ДОСТУПНЫХ ИНСТРУМЕНТОВ:
|
||||||
|
{tool_raw_list}
|
||||||
|
|
||||||
|
ТВОЯ ЗАДАЧА:
|
||||||
|
Сгруппируй инструменты по уровням абстракции и выведи их в формате Markdown-таблиц.
|
||||||
|
|
||||||
|
ИСПОЛЬЗУЙ СЛЕДУЮЩИЕ КАТЕГОРИИ:
|
||||||
|
1. 🧩 [Микро-уровень]: Атомарные сущности, единичные объекты, конкретные детали или расчеты.
|
||||||
|
2. 🏗️ [Мезо-уровень]: Процессы, сложные узлы, локальные системы, взаимодействия или окружение.
|
||||||
|
3. 🗺️ [Макро-уровень]: Глобальные структуры, архитектура системы, высокоуровневая логика или долгосрочные стратегии.
|
||||||
|
4. ⚙️ [Мета-уровень]: Управление сессией, диагностика, системная помощь, логирование или работа с контекстом.
|
||||||
|
|
||||||
|
ФОРМАТ ВЫВОДА:
|
||||||
|
Для каждой категории создай заголовок и таблицу:
|
||||||
|
| Команда | Функциональное назначение |
|
||||||
|
|:---|:---|
|
||||||
|
| **название_команды** | Суть работы (1 краткое предложение) |
|
||||||
|
|
||||||
|
Не добавляй никаких приветствий или пояснений вне блока кода. Будь предельно лаконичен.
|
||||||
|
"""
|
||||||
|
return prompt
|
||||||
Loading…
Reference in New Issue
Block a user