fix: Add handling final argument for OpenAI responses tool calling

This commit is contained in:
Petr Mironychev
2026-05-21 14:19:16 +02:00
parent c4e34bb3d9
commit b33a1c2d43
6 changed files with 72 additions and 20 deletions

View File

@@ -174,13 +174,20 @@ void ClientInterface::sendMessage(
auto project = PluginLLMCore::RulesLoader::getActiveProject(); auto project = PluginLLMCore::RulesLoader::getActiveProject();
if (project) { if (project) {
systemPrompt += QString("\n# Active project name: %1").arg(project->displayName()); systemPrompt += QString("\n# Active project: %1").arg(project->displayName());
systemPrompt += QString("\n# Active Project path: %1") systemPrompt += QString(
"\n# Project source root: %1"
"\n# All new source files, headers, QML and CMake edits MUST be "
"created or modified under this directory. Use absolute paths "
"rooted here, or project-relative paths.")
.arg(project->projectDirectory().toUrlishString()); .arg(project->projectDirectory().toUrlishString());
if (auto target = project->activeTarget()) { if (auto target = project->activeTarget()) {
if (auto buildConfig = target->activeBuildConfiguration()) { if (auto buildConfig = target->activeBuildConfiguration()) {
systemPrompt += QString("\n# Active Build directory: %1") systemPrompt
+= QString(
"\n# Build output directory (compiler artifacts only — do NOT "
"create or edit source files here): %1")
.arg(buildConfig->buildDirectory().toUrlishString()); .arg(buildConfig->buildDirectory().toUrlishString());
} }
} }

View File

@@ -133,7 +133,9 @@ ToolsSettings::ToolsSettings()
Tr::tr("Comma-separated list of terminal commands that AI is allowed to execute on Linux. " Tr::tr("Comma-separated list of terminal commands that AI is allowed to execute on Linux. "
"Example: git, ls, cat, grep, find, cmake")); "Example: git, ls, cat, grep, find, cmake"));
allowedTerminalCommandsLinux.setDisplayStyle(Utils::StringAspect::LineEditDisplay); allowedTerminalCommandsLinux.setDisplayStyle(Utils::StringAspect::LineEditDisplay);
allowedTerminalCommandsLinux.setDefaultValue("git, ls, cat, grep, find"); allowedTerminalCommandsLinux.setDefaultValue(
"git, ls, cat, grep, find, pwd, echo, head, tail, wc, which, file, stat, tree, uname, "
"date, whoami, hostname");
allowedTerminalCommandsMacOS.setSettingsKey(Constants::CA_ALLOWED_TERMINAL_COMMANDS_MACOS); allowedTerminalCommandsMacOS.setSettingsKey(Constants::CA_ALLOWED_TERMINAL_COMMANDS_MACOS);
allowedTerminalCommandsMacOS.setLabelText(Tr::tr("Allowed Commands (macOS)")); allowedTerminalCommandsMacOS.setLabelText(Tr::tr("Allowed Commands (macOS)"));
@@ -141,7 +143,9 @@ ToolsSettings::ToolsSettings()
Tr::tr("Comma-separated list of terminal commands that AI is allowed to execute on macOS. " Tr::tr("Comma-separated list of terminal commands that AI is allowed to execute on macOS. "
"Example: git, ls, cat, grep, find, cmake")); "Example: git, ls, cat, grep, find, cmake"));
allowedTerminalCommandsMacOS.setDisplayStyle(Utils::StringAspect::LineEditDisplay); allowedTerminalCommandsMacOS.setDisplayStyle(Utils::StringAspect::LineEditDisplay);
allowedTerminalCommandsMacOS.setDefaultValue("git, ls, cat, grep, find"); allowedTerminalCommandsMacOS.setDefaultValue(
"git, ls, cat, grep, find, pwd, echo, head, tail, wc, which, file, stat, tree, uname, "
"date, whoami, hostname");
allowedTerminalCommandsWindows.setSettingsKey(Constants::CA_ALLOWED_TERMINAL_COMMANDS_WINDOWS); allowedTerminalCommandsWindows.setSettingsKey(Constants::CA_ALLOWED_TERMINAL_COMMANDS_WINDOWS);
allowedTerminalCommandsWindows.setLabelText(Tr::tr("Allowed Commands (Windows)")); allowedTerminalCommandsWindows.setLabelText(Tr::tr("Allowed Commands (Windows)"));
@@ -149,7 +153,8 @@ ToolsSettings::ToolsSettings()
Tr::tr("Comma-separated list of terminal commands that AI is allowed to execute on Windows. " Tr::tr("Comma-separated list of terminal commands that AI is allowed to execute on Windows. "
"Example: git, dir, type, findstr, where, cmake")); "Example: git, dir, type, findstr, where, cmake"));
allowedTerminalCommandsWindows.setDisplayStyle(Utils::StringAspect::LineEditDisplay); allowedTerminalCommandsWindows.setDisplayStyle(Utils::StringAspect::LineEditDisplay);
allowedTerminalCommandsWindows.setDefaultValue("git, dir, type, findstr, where"); allowedTerminalCommandsWindows.setDefaultValue(
"git, dir, type, findstr, where, echo, whoami, hostname, ver, tree, fc");
terminalCommandTimeout.setSettingsKey(Constants::CA_TERMINAL_COMMAND_TIMEOUT); terminalCommandTimeout.setSettingsKey(Constants::CA_TERMINAL_COMMAND_TIMEOUT);
terminalCommandTimeout.setLabelText(Tr::tr("Command Timeout (seconds)")); terminalCommandTimeout.setLabelText(Tr::tr("Command Timeout (seconds)"));

View File

@@ -77,10 +77,20 @@ QFuture<LLMQore::ToolResult> CreateNewFileTool::executeAsync(const QJsonObject &
if (!isInProject) { if (!isInProject) {
const auto &settings = Settings::toolsSettings(); const auto &settings = Settings::toolsSettings();
if (!settings.allowAccessOutsideProject()) { if (!settings.allowAccessOutsideProject()) {
const QString projectRoot = Context::ProjectUtils::getProjectRoot();
const QString hint = projectRoot.isEmpty()
? QStringLiteral(
"No project is currently open. Open a project in Qt Creator or "
"enable 'Allow file access outside project' in QodeAssist settings.")
: QString(
"Retry with a path under the active project root: '%1'. The build "
"directory is for compiler output only and cannot accept new source "
"files. If you really need to write outside the project, enable "
"'Allow file access outside project' in QodeAssist settings.")
.arg(projectRoot);
throw LLMQore::ToolRuntimeError( throw LLMQore::ToolRuntimeError(
QString("Error: File path '%1' is not within the current project. " QString("Error: File path '%1' is not within the current project. %2")
"Enable 'Allow file access outside project' in settings to create files outside project scope.") .arg(absolutePath, hint));
.arg(absolutePath));
} }
LOG_MESSAGE(QString("Creating file outside project scope: %1").arg(absolutePath)); LOG_MESSAGE(QString("Creating file outside project scope: %1").arg(absolutePath));
} }

View File

@@ -143,10 +143,20 @@ QFuture<LLMQore::ToolResult> EditFileTool::executeAsync(const QJsonObject &input
if (!isInProject) { if (!isInProject) {
const auto &settings = Settings::toolsSettings(); const auto &settings = Settings::toolsSettings();
if (!settings.allowAccessOutsideProject()) { if (!settings.allowAccessOutsideProject()) {
const QString projectRoot = Context::ProjectUtils::getProjectRoot();
const QString hint = projectRoot.isEmpty()
? QStringLiteral(
"No project is currently open. Open a project in Qt Creator or "
"enable 'Allow file access outside project' in QodeAssist settings.")
: QString(
"Retry with a path under the active project root: '%1'. The build "
"directory is for compiler output only — source files must live under "
"the project root. If you really need to edit outside the project, "
"enable 'Allow file access outside project' in QodeAssist settings.")
.arg(projectRoot);
throw LLMQore::ToolRuntimeError( throw LLMQore::ToolRuntimeError(
QString("File path '%1' is not within the current project. " QString("File path '%1' is not within the current project. %2")
"Enable 'Allow file access outside project' in settings to edit files outside the project.") .arg(filePath, hint));
.arg(filePath));
} }
LOG_MESSAGE(QString("Editing file outside project scope: %1").arg(filePath)); LOG_MESSAGE(QString("Editing file outside project scope: %1").arg(filePath));
} }

View File

@@ -13,6 +13,7 @@
#include <QProcess> #include <QProcess>
#include <QPromise> #include <QPromise>
#include <QRegularExpression> #include <QRegularExpression>
#include <QRegularExpression>
#include <QSharedPointer> #include <QSharedPointer>
#include <QTimer> #include <QTimer>
@@ -45,18 +46,26 @@ QJsonObject ExecuteTerminalCommandTool::parametersSchema() const
QJsonObject definition; QJsonObject definition;
definition["type"] = "object"; definition["type"] = "object";
const QString commandDesc = getCommandDescription(); const QStringList allowed = getAllowedCommands();
const QString allowedList = allowed.isEmpty() ? "none" : allowed.join(", ");
QJsonObject properties; QJsonObject properties;
properties["command"] = QJsonObject{ properties["command"] = QJsonObject{
{"type", "string"}, {"type", "string"},
{"description", commandDesc}}; {"description",
QString("Name of the executable to run, WITHOUT any arguments or flags. "
"Must be exactly one of the allowed commands: %1. "
"Put every flag and argument in the separate `args` field. "
"Correct: command=\"ls\", args=\"-R\". "
"Incorrect: command=\"ls -R\" (the whole line in one field will be rejected).")
.arg(allowedList)}};
properties["args"] = QJsonObject{ properties["args"] = QJsonObject{
{"type", "string"}, {"type", "string"},
{"description", {"description",
"Optional arguments for the command. Arguments with spaces should be properly quoted. " "Optional arguments and flags for the command, as a single string. Do NOT repeat the "
"Example: '--file \"path with spaces.txt\" --verbose'"}}; "command name here. Arguments with spaces should be quoted. "
"Example: args=\"--file \\\"path with spaces.txt\\\" --verbose\"."}};
definition["properties"] = properties; definition["properties"] = properties;
definition["required"] = QJsonArray{"command"}; definition["required"] = QJsonArray{"command"};
@@ -68,14 +77,25 @@ QFuture<LLMQore::ToolResult> ExecuteTerminalCommandTool::executeAsync(const QJso
{ {
using LLMQore::ToolResult; using LLMQore::ToolResult;
const QString command = input.value("command").toString().trimmed(); QString command = input.value("command").toString().trimmed();
const QString args = input.value("args").toString().trimmed(); QString args = input.value("args").toString().trimmed();
if (command.isEmpty()) { if (command.isEmpty()) {
LOG_MESSAGE("ExecuteTerminalCommandTool: Command is empty"); LOG_MESSAGE("ExecuteTerminalCommandTool: Command is empty");
return QtFuture::makeReadyFuture(ToolResult::error("Error: Command parameter is required.")); return QtFuture::makeReadyFuture(ToolResult::error("Error: Command parameter is required."));
} }
// Tolerate models that pack the whole command line into `command`. As long as `args` is
// empty we can safely split on the first whitespace — the allowlist check still validates
// the actual executable name.
if (args.isEmpty()) {
const int firstSpace = command.indexOf(QRegularExpression("\\s"));
if (firstSpace > 0) {
args = command.mid(firstSpace + 1).trimmed();
command = command.left(firstSpace);
}
}
if (command.length() > MAX_COMMAND_LENGTH) { if (command.length() > MAX_COMMAND_LENGTH) {
LOG_MESSAGE(QString("ExecuteTerminalCommandTool: Command too long (%1 chars)") LOG_MESSAGE(QString("ExecuteTerminalCommandTool: Command too long (%1 chars)")
.arg(command.length())); .arg(command.length()));