More accurate extracting of arguments schema, fix concatenating of script_result and post-prompt
This commit is contained in:
parent
d071d16bd2
commit
f637c6e3c4
|
|
@ -181,6 +181,19 @@ MODELS: Dict[str, Dict[str, Any]] = {
|
||||||
"stream": True,
|
"stream": True,
|
||||||
"capabilities": ["vision"],
|
"capabilities": ["vision"],
|
||||||
},
|
},
|
||||||
|
"claude-sonnet-4.6-openrouter": {
|
||||||
|
"name": "anthropic/claude-sonnet-4.6",
|
||||||
|
"provider": "openai",
|
||||||
|
"model_name": "anthropic/claude-sonnet-4.6",
|
||||||
|
"apiBase": "https://openrouter.ai/api/v1", # Добавлено /api/v1
|
||||||
|
"apiKey":
|
||||||
|
"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",
|
||||||
|
"stream": True,
|
||||||
|
"capabilities": ["vision", "reasoning"],
|
||||||
|
"model_kwargs": {
|
||||||
|
"include_reasoning": True
|
||||||
|
}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,9 +15,9 @@ import subprocess
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
from typing import Any
|
from typing import Any, get_type_hints
|
||||||
from langchain_core.tools import StructuredTool
|
from langchain_core.tools import StructuredTool
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field, create_model
|
||||||
|
|
||||||
def build_react_agent(model_name: str, temperature: float, max_tokens: int, agency_mode: bool, obsidian_settings: dict):
|
def build_react_agent(model_name: str, temperature: float, max_tokens: int, agency_mode: bool, obsidian_settings: dict):
|
||||||
"""Инициализация ядра на базе LangGraph."""
|
"""Инициализация ядра на базе LangGraph."""
|
||||||
|
|
@ -41,16 +41,29 @@ class PurePromptSchema(BaseModel):
|
||||||
def create_subprocess_tool(script_path: str, method_name: str,
|
def create_subprocess_tool(script_path: str, method_name: str,
|
||||||
hidden_prompt: str):
|
hidden_prompt: str):
|
||||||
"""Вид 2: Инструмент со скриптом + пост-процессинг."""
|
"""Вид 2: Инструмент со скриптом + пост-процессинг."""
|
||||||
spec = importlib.util.spec_from_file_location("user_dynamic_tool",
|
|
||||||
script_path)
|
script_dir = os.path.dirname(os.path.abspath(script_path))
|
||||||
module = importlib.util.module_from_spec(spec)
|
added_to_path = False
|
||||||
|
if script_dir not in sys.path:
|
||||||
|
sys.path.insert(0, script_dir)
|
||||||
|
added_to_path = True
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
spec = importlib.util.spec_from_file_location("user_dynamic_tool", script_path)
|
||||||
|
module = importlib.util.module_from_spec(spec)
|
||||||
spec.loader.exec_module(module)
|
spec.loader.exec_module(module)
|
||||||
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}"
|
||||||
|
|
||||||
|
# ← КЛЮЧЕВОЕ: строим схему ДО того, как потеряем оригинальную сигнатуру
|
||||||
|
args_schema = _build_pydantic_schema_from_func(func)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise RuntimeError(f"Ошибка чтения сигнатуры {script_path}: {e}")
|
raise RuntimeError(f"Ошибка чтения сигнатуры {script_path}: {e}")
|
||||||
|
finally:
|
||||||
|
# Убираем путь сразу после импорта и получения функции
|
||||||
|
if added_to_path:
|
||||||
|
sys.path.remove(script_dir)
|
||||||
|
|
||||||
@functools.wraps(func)
|
@functools.wraps(func)
|
||||||
def wrapper(**kwargs: Any) -> str:
|
def wrapper(**kwargs: Any) -> str:
|
||||||
|
|
@ -58,6 +71,12 @@ def create_subprocess_tool(script_path: str, method_name: str,
|
||||||
import json
|
import json
|
||||||
import sys
|
import sys
|
||||||
import importlib.util
|
import importlib.util
|
||||||
|
import os
|
||||||
|
|
||||||
|
# Добавляем папку со скриптом в sys.path, чтобы работали локальные импорты (например, norminv)
|
||||||
|
path_to_add = os.path.dirname(os.path.abspath(r'{script_path}'))
|
||||||
|
original_path = sys.path.copy()
|
||||||
|
sys.path.insert(0, path_to_add)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
spec = importlib.util.spec_from_file_location('sub_module', r'{script_path}')
|
spec = importlib.util.spec_from_file_location('sub_module', r'{script_path}')
|
||||||
|
|
@ -70,20 +89,57 @@ try:
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"SCRIPT RUNTIME ERROR: {{e}}", file=sys.stderr)
|
print(f"SCRIPT RUNTIME ERROR: {{e}}", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
finally:
|
||||||
|
# Возвращаем sys.path в исходное состояние (чистота внутри процесса)
|
||||||
|
sys.path = original_path
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
process = subprocess.run([sys.executable, "-c", runner_code],
|
process = subprocess.run(
|
||||||
|
[sys.executable, "-c", runner_code],
|
||||||
input=json.dumps(kwargs),
|
input=json.dumps(kwargs),
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
text=True,
|
text=True,
|
||||||
encoding='utf-8',
|
encoding='utf-8',
|
||||||
check=True)
|
check=True
|
||||||
|
)
|
||||||
raw_output = process.stdout.strip()
|
raw_output = process.stdout.strip()
|
||||||
|
|
||||||
# Прилизывание для скрипта
|
raw_stripped = raw_output.strip() if raw_output else ""
|
||||||
if hidden_prompt:
|
prompt_stripped = hidden_prompt.strip() if hidden_prompt else ""
|
||||||
return f"РЕЗУЛЬТАТ ВЫПОЛНЕНИЯ СКРИПТА:\n{raw_output}\n\nИНСТРУКЦИЯ ПО ФОРМАТИРОВАНИЮ ОТВЕТА ДЛЯ АГЕНТА:\n{hidden_prompt}"
|
|
||||||
return raw_output
|
if raw_stripped and prompt_stripped:
|
||||||
|
# Случай 1: Есть и результат скрипта, и инструкция — полный вариант
|
||||||
|
result = (
|
||||||
|
f"Инструмент вернул следующий результат:\n"
|
||||||
|
f"<tool_output>\n{raw_output}\n</tool_output>\n\n"
|
||||||
|
f"---\n"
|
||||||
|
f"Инструкция по оформлению финального ответа пользователю:\n"
|
||||||
|
f"<instruction>\n{hidden_prompt}\n</instruction>"
|
||||||
|
)
|
||||||
|
|
||||||
|
elif raw_stripped and not prompt_stripped:
|
||||||
|
# Случай 2: Есть результат, нет инструкции
|
||||||
|
result = (
|
||||||
|
f"Инструмент успешно выполнен. Передай пользователю следующий результат "
|
||||||
|
f"ДОСЛОВНО, без изменений, сокращений и дополнений:\n\n"
|
||||||
|
f"<tool_output>\n{raw_output}\n</tool_output>"
|
||||||
|
)
|
||||||
|
|
||||||
|
elif not raw_stripped and prompt_stripped:
|
||||||
|
# Случай 3: Скрипт вернул пустоту (заглушка/нет данных) — только промпт как инструкция
|
||||||
|
result = prompt_stripped
|
||||||
|
""" result = (
|
||||||
|
f"Скрипт не вернул данных. Выполни следующую инструкцию:\n\n{hidden_prompt}"
|
||||||
|
)"""
|
||||||
|
|
||||||
|
else:
|
||||||
|
# Случай 4: Оба пустые — сообщаем агенту честно
|
||||||
|
result = (
|
||||||
|
"Инструмент не вернул никаких данных и не содержит инструкций. "
|
||||||
|
"Сообщи пользователю, что инструмент настроен некорректно."
|
||||||
|
)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
except subprocess.CalledProcessError as e:
|
except subprocess.CalledProcessError as e:
|
||||||
# Даже если скрипт упал, мы отдаем ошибку + промпт, чтобы агент мог обработать её по инструкции
|
# Даже если скрипт упал, мы отдаем ошибку + промпт, чтобы агент мог обработать её по инструкции
|
||||||
|
|
@ -92,10 +148,13 @@ except Exception as e:
|
||||||
return f"{error_msg}\n\nИНСТРУКЦИЯ НА СЛУЧАЙ ОШИБКИ:\n{hidden_prompt}"
|
return f"{error_msg}\n\nИНСТРУКЦИЯ НА СЛУЧАЙ ОШИБКИ:\n{hidden_prompt}"
|
||||||
return error_msg
|
return error_msg
|
||||||
|
|
||||||
return StructuredTool.from_function(func=wrapper,
|
# Явно передаём args_schema — LLM будет видеть реальную типизацию
|
||||||
|
return StructuredTool.from_function(
|
||||||
|
func=wrapper,
|
||||||
name=func.__name__,
|
name=func.__name__,
|
||||||
description=docstring,
|
description=docstring,
|
||||||
infer_schema=True)
|
args_schema=args_schema, # ← вместо infer_schema=True
|
||||||
|
)
|
||||||
|
|
||||||
def get_dynamic_tools(obsidian_settings: dict):
|
def get_dynamic_tools(obsidian_settings: dict):
|
||||||
"""Собирает инструменты двух видов на основе настроек Obsidian."""
|
"""Собирает инструменты двух видов на основе настроек Obsidian."""
|
||||||
|
|
@ -170,3 +229,49 @@ def get_dynamic_tools(obsidian_settings: dict):
|
||||||
print(f"❌ Ошибка генерации инструмента-скрипта '{name}': {e}")
|
print(f"❌ Ошибка генерации инструмента-скрипта '{name}': {e}")
|
||||||
|
|
||||||
return tools
|
return tools
|
||||||
|
|
||||||
|
def _build_pydantic_schema_from_func(func) -> type[BaseModel]:
|
||||||
|
"""
|
||||||
|
Динамически строит Pydantic BaseModel из сигнатуры функции.
|
||||||
|
Используется для корректной передачи схемы в StructuredTool,
|
||||||
|
чтобы LLM видела реальные типы аргументов, а не **kwargs.
|
||||||
|
"""
|
||||||
|
sig = inspect.signature(func)
|
||||||
|
|
||||||
|
# Пробуем получить аннотации типов (включая from __future__ import annotations)
|
||||||
|
try:
|
||||||
|
hints = get_type_hints(func)
|
||||||
|
except Exception:
|
||||||
|
hints = {}
|
||||||
|
|
||||||
|
fields = {}
|
||||||
|
for param_name, param in sig.parameters.items():
|
||||||
|
if param_name in ("self", "cls"):
|
||||||
|
continue
|
||||||
|
if param.kind in (
|
||||||
|
inspect.Parameter.VAR_POSITIONAL, # *args
|
||||||
|
inspect.Parameter.VAR_KEYWORD, # **kwargs
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Тип параметра
|
||||||
|
annotation = hints.get(param_name, Any)
|
||||||
|
|
||||||
|
# Дефолтное значение
|
||||||
|
if param.default is inspect.Parameter.empty:
|
||||||
|
# Обязательный параметр
|
||||||
|
fields[param_name] = (annotation, Field(...))
|
||||||
|
else:
|
||||||
|
fields[param_name] = (annotation, Field(default=param.default))
|
||||||
|
|
||||||
|
if not fields:
|
||||||
|
# Фоллбек: если функция не имеет параметров или все **kwargs —
|
||||||
|
# создаём схему с одним полем, чтобы LLM хоть что-то передала
|
||||||
|
fields["kwargs"] = (dict, Field(default_factory=dict,
|
||||||
|
description="Аргументы функции"))
|
||||||
|
|
||||||
|
model = create_model(
|
||||||
|
f"{func.__name__}_Schema",
|
||||||
|
**fields
|
||||||
|
)
|
||||||
|
return model
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user