diff --git a/misc/qet-mcp/README.md b/misc/qet-mcp/README.md index 616ad5234..f71f2befc 100644 --- a/misc/qet-mcp/README.md +++ b/misc/qet-mcp/README.md @@ -42,7 +42,7 @@ here read the model. | `qet_check` | **design-rule checks** — duplicate labels, unlabelled masters, unnumbered conductors, empty folios, terminals with more than four wires, folio reports with several wires | | `qet_layout_check` | **does the drawing read well?** — a 0–100 score; wires that jog because two symbols are a few pixels out of line, symbols off the grid, wires through symbols, overlaps, crossings, labels over wires, 4-way junction dots, side branches out of line; and the moves that fix them, ready for `qet_edit` | | `qet_query` | **ask the project database** — read-only SQL over the views and tables | -| `qet_about` | **start here** — where QElectroTech keeps things, what is switched on, the stored scripts, the calls a script can make (from `qet-assistant.json`), and the installation's standard symbols if it has any | +| `qet_about` | **start here** — where QElectroTech keeps things, what is switched on, the stored scripts, the calls a script can make (from `qet-assistant.json`), the installation's standard symbols if it has any, and your house style | | `qet_script_api` | **what a script can call** — every `qet.*` call of this build, and the header that makes a script a button | | `qet_script_test` | **try a script** on a copy of a project: what it would change, what it logged, its errors | | `qet_script_install` | **make a button** — store a script (and an SVG icon) where QElectroTech shows it in Project > Scripts and the Scripts toolbar | @@ -323,6 +323,21 @@ The usual round: read the recording, write a script that does the same in general (on the selected elements, say, not on these exact ones), `qet_recording_check` it until it matches, then `qet_script_install` it. +## Your house style + +Drawing conventions you want every assistant to follow -- grid, flow +direction, wire routing, tags, grouping -- written once, in your own words, +and kept by QElectroTech itself rather than by any one assistant: + +```js +qet.setHouseStyle("Never place a symbol off the 10 px grid.\n..."); +``` + +It is stored in QElectroTech's settings (needs `QET_ENABLE_SCRIPTING=1`, as +for any change), written to `qet-assistant.json` as `house_style`, and shown +by `qet_about`, whose description tells the assistant to follow it. Every +assistant that connects, of any make, reads the same text. + ## Live mode: working in the QElectroTech you have open Every tool above works on files, with no QElectroTech window involved. The diff --git a/misc/qet-mcp/qet_mcp.py b/misc/qet-mcp/qet_mcp.py index 29c3ca812..dd11523ae 100755 --- a/misc/qet-mcp/qet_mcp.py +++ b/misc/qet-mcp/qet_mcp.py @@ -3829,7 +3829,9 @@ SERVER_INSTRUCTIONS = ( "diagrams. A project (.qet) holds folios (sheets) of symbols " "(elements) joined by wires (conductors). Folio indexes count from 0.\n" "Start with qet_about: where QElectroTech keeps things, what is " - "switched on, and the stored scripts.\n" + "switched on, the stored scripts, and house_style -- this " + "installation's own drawing conventions if the user set any. Follow " + "house_style when placing or wiring anything.\n" "Two ways of working. HEADLESS (qet_* and qet_script_*): read, check " "and edit .qet files and store script buttons; nothing the user has " "open is touched. To make a button: qet_script_api for the calls, " @@ -3910,6 +3912,12 @@ def tool_about() -> dict: "script_api": info.get("script_api"), # Never the token: it is for the live tools, not the conversation. "live": {"open": bool(live), "pid": (live or {}).get("pid")}, + # This installation's drawing conventions, set once via + # qet.setHouseStyle() (scripting consent required, same as any + # other write) and read here so every assistant that connects sees + # them without being told by the user each time. No GUI field yet + # -- QetSettings::setHouseStyle() is there for one if it is wanted. + "house_style": info.get("house_style"), # One symbol per device role, the user's own choice: use these # paths with add_element / qet.addElement before searching. "standard_symbols": standard_symbols(info), @@ -5663,8 +5671,12 @@ TOOLS = [ "scripts, element and title block collections), which " "features are on (scripting, live mode), every call a script " "can make, the stored scripts and the ones refused with why, " - "and whether a live session is open; plus this server's own " - "setup. Reads one file; changes nothing.", + "whether a live session is open, and house_style -- this " + "installation's own drawing conventions, in the user's words, " + "if they set any (qet.setHouseStyle()); null if not. Read " + "house_style and follow it, the same as any explicit " + "instruction, before placing or wiring anything. " + "Reads one file; changes nothing.", "inputSchema": {"type": "object", "properties": {}}, "handler": lambda a: tool_about(), }, diff --git a/misc/qet-mcp/test_qet_mcp.py b/misc/qet-mcp/test_qet_mcp.py index 9cd59ab32..df566d38a 100644 --- a/misc/qet-mcp/test_qet_mcp.py +++ b/misc/qet-mcp/test_qet_mcp.py @@ -3784,6 +3784,15 @@ class AssistantInfoFile(unittest.TestCase): self.assertNotIn("SECRET", json.dumps(about)) self.assertEqual(about["script_api"], ["int currentFolio()"]) + def test_house_style_passes_through(self): + self.write(house_style="Grid: never off. Inputs left, outputs right.") + about = m.tool_about() + self.assertEqual(about["house_style"], "Grid: never off. Inputs left, outputs right.") + + def test_house_style_null_when_unset(self): + self.write(house_style=None) + self.assertIsNone(m.tool_about()["house_style"]) + def test_first_contact_carries_the_instructions(self): reply = m.handle({"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {}}) self.assertIn("qet_about", reply["result"]["instructions"]) diff --git a/sources/scripting/assistantinfo.cpp b/sources/scripting/assistantinfo.cpp index 88193b5a0..f35e8c9f1 100644 --- a/sources/scripting/assistantinfo.cpp +++ b/sources/scripting/assistantinfo.cpp @@ -36,6 +36,7 @@ namespace { QJsonObject s_live; ///< empty while live mode is closed bool s_running = true; + bool s_watching = false; ///< this QElectroTech writes the file (an editor opened) } namespace AssistantInfo { @@ -54,14 +55,25 @@ QString path() */ void watch() { - static bool watching = false; - if (watching) return; - watching = true; + if (s_watching) return; + s_watching = true; QObject::connect(&ScriptLibrary::instance(), &ScriptLibrary::changed, &write); QObject::connect(qApp, &QCoreApplication::aboutToQuit, &markStopped); write(); } +/** + @brief refresh + Write the file again after a setting it carries changed, but only in a + QElectroTech that writes it at all: a headless --run never opened an + editor, and writing from it would replace the open QElectroTech's live + channel with its own empty one. +*/ +void refresh() +{ + if (s_watching) write(); +} + void write() { QJsonArray scripts, refused; @@ -111,7 +123,14 @@ void write() {QStringLiteral("stored_scripts"), scripts}, {QStringLiteral("refused_scripts"), refused}, {QStringLiteral("recordings"), MacroRecorder::recordings()}, - {QStringLiteral("live"), s_live.isEmpty() ? QJsonValue() : QJsonValue(s_live)}}; + {QStringLiteral("live"), s_live.isEmpty() ? QJsonValue() : QJsonValue(s_live)}, + // Free-text drawing conventions, set once by the user + // (QetSettings::setHouseStyle(), qet.setHouseStyle()) and read + // by any assistant that connects, live or headless, without it + // having to ask the user or guess. Null, not "", when unset -- + // the two mean different things to a reader. + {QStringLiteral("house_style"), QetSettings::houseStyle().isEmpty() + ? QJsonValue() : QJsonValue(QetSettings::houseStyle())}}; const QString file_path = path(); QDir().mkpath(QFileInfo(file_path).path()); diff --git a/sources/scripting/assistantinfo.h b/sources/scripting/assistantinfo.h index 2243b3deb..02248d9a6 100644 --- a/sources/scripting/assistantinfo.h +++ b/sources/scripting/assistantinfo.h @@ -43,6 +43,7 @@ namespace AssistantInfo QString path(); void watch(); void write(); + void refresh(); void setLive(const QJsonObject &live); void markStopped(); } diff --git a/sources/scripting/qetscriptapi.cpp b/sources/scripting/qetscriptapi.cpp index 78c0833d1..85955ea27 100644 --- a/sources/scripting/qetscriptapi.cpp +++ b/sources/scripting/qetscriptapi.cpp @@ -44,6 +44,8 @@ #include "../conductorsegment.h" #include "../conductorrouter.h" #include "../qetgraphicsitem/diagramimageitem.h" +#include "../utils/qetsettings.h" +#include "assistantinfo.h" // See diagrameventaddpdf.h: a missing QtPdf module (or Qt < 6.4) is not // fatal at build time, so addPdfPage() is always declared -- a script @@ -4219,6 +4221,35 @@ QStringList QetScriptApi::folioPresets() const return names; } +/** + @brief QetScriptApi::houseStyle + This installation's drawing conventions (grid, flow direction, routing, + tagging, grouping, ...), as free text the user wrote once. Empty when + nobody has set any. Not project-scoped: the same text for every + project this QElectroTech opens, same as liveAssistantEnabled(). + @return the text +*/ +QString QetScriptApi::houseStyle() const +{ + return QetSettings::houseStyle(); +} + +/** + @brief QetScriptApi::setHouseStyle + Write the installation's drawing conventions. Always succeeds (a plain + setting, not project data, so there is nothing here to refuse); @return + is for a consistent call shape with the rest of the API. + @param text +*/ +bool QetScriptApi::setHouseStyle(const QString &text) +{ + QetSettings::setHouseStyle(text); + //qet-assistant.json carries it: an assistant reading the file + //now must see the new text, not the one from the last start + AssistantInfo::refresh(); + return true; +} + /** @brief QetScriptApi::setFolioBorder Change one field of a folio's frame through ChangeBorderCommand, as the diff --git a/sources/scripting/qetscriptapi.h b/sources/scripting/qetscriptapi.h index 6b9e6bb9b..f591f9f93 100644 --- a/sources/scripting/qetscriptapi.h +++ b/sources/scripting/qetscriptapi.h @@ -569,6 +569,11 @@ class QetScriptApi : public QObject Q_INVOKABLE bool setFolioBorder(int folioIndex, const QString &property, const QString &value); Q_INVOKABLE QStringList folioPresets() const; + // -- this installation's drawing conventions (grid, flow, routing, tagging, -- + // -- grouping, ...), free text, for every assistant that connects to read -- + Q_INVOKABLE QString houseStyle() const; + Q_INVOKABLE bool setHouseStyle(const QString &text); + // -- the conductor defaults of a folio (Folio properties > Conductors), // or with folioIndex -1, the project's defaults for new folios -- Q_INVOKABLE QString conductorDefault(int folioIndex, const QString &property) const; diff --git a/sources/utils/qetsettings.cpp b/sources/utils/qetsettings.cpp index 43adb2ce4..de5f6e6b4 100644 --- a/sources/utils/qetsettings.cpp +++ b/sources/utils/qetsettings.cpp @@ -173,6 +173,23 @@ namespace QetSettings settings.setValue("scripting/live_assistant", enabled); } + /** + * @brief houseStyle + * @return this installation's drawing conventions, in the user's own + * words -- empty when nobody has set any. + */ + QString houseStyle() + { + QSettings settings; + return settings.value("assistant/house_style", QString()).toString(); + } + + void setHouseStyle(const QString &text) + { + QSettings settings; + settings.setValue("assistant/house_style", text); + } + /** * @brief setSheetBackground * Store the sheet background last picked in the diagram editor, so the diff --git a/sources/utils/qetsettings.h b/sources/utils/qetsettings.h index 64bf4b615..abea8d460 100644 --- a/sources/utils/qetsettings.h +++ b/sources/utils/qetsettings.h @@ -41,6 +41,16 @@ namespace QetSettings bool liveAssistantEnabled(); void setLiveAssistantEnabled(bool enabled); + /** + @brief Free-text drawing conventions for this installation (grid, + flow direction, routing, tagging, grouping, ...), written once by + the user and read by every AI assistant that connects -- live or + headless -- through qet-assistant.json and qet.houseStyle(). + Empty when never set. + */ + QString houseStyle(); + void setHouseStyle(const QString &text); + /** @brief The sheet (folio) background the diagram editors draw.