diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 558cabc..73ac13c 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,5 +1,8 @@ # Subdir CMake for src +# Ensure internal libs are available to dependents +add_subdirectory(dal) + add_library(kompanion_mw SHARED middleware/kompanioncontroller.cpp middleware/libkiexecutor.cpp @@ -8,7 +11,7 @@ add_library(kompanion_mw SHARED middleware/orchestrator.cpp ) -find_package(Qt6 REQUIRED COMPONENTS Core DBus) +find_package(Qt6 REQUIRED COMPONENTS Core DBus Sql) set(KOMPANION_CONTROLLER_DBUS_XML ${CMAKE_CURRENT_SOURCE_DIR}/../docs/dbus/org.kde.kompanion.controller.xml) set(KOMPANION_EXECUTOR_DBUS_XML ${CMAKE_CURRENT_SOURCE_DIR}/../docs/dbus/org.kde.kompanion.executor.xml) @@ -31,7 +34,7 @@ target_include_directories(kompanion_mw PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/middl target_sources(kompanion_mw PRIVATE ${KOMPANION_DBUS_ADAPTOR_SRCS} ${KOMPANION_DBUS_INTERFACE_SRCS}) -target_link_libraries(kompanion_mw PRIVATE Qt6::Core Qt6::DBus Qt6::Network) +target_link_libraries(kompanion_mw PRIVATE Qt6::Core Qt6::DBus Qt6::Sql Qt6::Network kom_dal) target_compile_definitions(kompanion_mw PRIVATE KOMPANION_MW_LIBRARY) # Example executable wiring GUI/controller/executor together could be added later. diff --git a/src/cli/KompanionApp.cpp b/src/cli/KompanionApp.cpp index 7c60b0f..49f6dcd 100644 --- a/src/cli/KompanionApp.cpp +++ b/src/cli/KompanionApp.cpp @@ -867,6 +867,18 @@ int main(int argc, char** argv) { "path"); parser.addOption(embFileOption); + // Snapshot helpers + QCommandLineOption snapshotSaveOption(QStringList() << "--snapshot-save", + "Save a JSON snapshot (content) under a key in --ns. Provide content via -r/--stdin/[payload]."); + parser.addOption(snapshotSaveOption); + QCommandLineOption snapshotLoadOption(QStringList() << "--snapshot-load", + "Load a JSON snapshot for --ns and --key and print it."); + parser.addOption(snapshotLoadOption); + QCommandLineOption keyOption(QStringList() << "--key", + "Key for snapshot operations (default 'session:last').", + "key", "session:last"); + parser.addOption(keyOption); + parser.addPositionalArgument("tool", "Tool name to invoke."); parser.addPositionalArgument("payload", "Optional JSON payload or file path (use '-' for stdin).", "[payload]"); @@ -880,6 +892,8 @@ int main(int argc, char** argv) { const bool interactive = parser.isSet(interactiveOption); const bool initRequested = parser.isSet(initOption); const bool runMcp = parser.isSet(mcpServeOption); + const bool snapSave = parser.isSet(snapshotSaveOption); + const bool snapLoad = parser.isSet(snapshotLoadOption); std::optional configDsn = readDsnFromConfig(); const char* envDsn = std::getenv("PG_DSN"); @@ -932,6 +946,34 @@ int main(int argc, char** argv) { return runMcpServer(backend, addr, qerr); } + // Snapshot helpers (exclusive) + if (snapSave || snapLoad) { + const QString ns = parser.value(nsNameOption); + if (ns.isEmpty()) { qerr << "--snapshot-save/load requires --ns \n"; return 1; } + const QString key = parser.value(keyOption); + if (snapSave) { + // Resolve content from CLI + std::string raw; + QString err; + if (!resolveRequestPayload(parser, parser.positionalArguments(), requestOption, stdinOption, raw, &err)) { + qerr << (err.isEmpty() ? QStringLiteral("Failed to read snapshot content") : err) << "\n"; + return 1; + } + // Wrap into save_context call + std::ostringstream req; + req << "{\"namespace\":\"" << ns.toStdString() << "\",\"key\":\"" << key.toStdString() << "\",\"content\":" << raw << ",\"tags\":[\"snapshot\"]}"; + const std::string out = server.dispatch("kom.memory.v1.save_context", req.str()); + std::cout << out << std::endl; + return 0; + } else { + std::ostringstream req; + req << "{\"namespace\":\"" << ns.toStdString() << "\",\"key\":\"" << key.toStdString() << "\"}"; + const std::string out = server.dispatch("kom.memory.v1.recall_context", req.str()); + std::cout << out << std::endl; + return 0; + } + } + // DB inspection helpers (exclusive) if (parser.isSet(dbNsOption)) { return dbListNamespaces(qout) ? 0 : 1; diff --git a/src/dal/CMakeLists.txt b/src/dal/CMakeLists.txt index 6dd0c04..19744b8 100644 --- a/src/dal/CMakeLists.txt +++ b/src/dal/CMakeLists.txt @@ -6,3 +6,4 @@ target_compile_features(kom_dal PUBLIC cxx_std_20) target_include_directories(kom_dal PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) target_link_libraries(kom_dal PUBLIC Qt6::Core Qt6::Sql) target_compile_options(kom_dal PRIVATE -fexceptions) +set_target_properties(kom_dal PROPERTIES POSITION_INDEPENDENT_CODE ON) diff --git a/src/middleware/orchestrator.cpp b/src/middleware/orchestrator.cpp index a90cab2..f526ac7 100644 --- a/src/middleware/orchestrator.cpp +++ b/src/middleware/orchestrator.cpp @@ -11,6 +11,8 @@ #include #include +#include "dal/PgDal.hpp" + // ---------- OllamaModelProvider ---------- OllamaModelProvider::OllamaModelProvider(QObject *parent) : QObject(parent) @@ -170,3 +172,48 @@ void Orchestrator::processPendingTasks() { } } } + +bool Orchestrator::saveSnapshot(const QString &nameSpace, + const QString &key, + const QJsonObject &content, + const QStringList &tags) +{ + ki::PgDal dal; + const QByteArray dsn = qgetenv("PG_DSN"); + if (!dsn.isEmpty()) dal.connect(dsn.toStdString()); else dal.connect("stub://memory"); + + auto nsRow = dal.ensureNamespace(nameSpace.toStdString()); + if (!nsRow) return false; + + ki::ItemRow row; + row.namespace_id = nsRow->id; + row.key = key.toStdString(); + row.tags.reserve(tags.size()); + for (const auto &t : tags) row.tags.push_back(t.toStdString()); + row.content_json = QString::fromUtf8(QJsonDocument(content).toJson(QJsonDocument::Compact)).toStdString(); + row.metadata_json = "{}"; + row.created_at = std::chrono::system_clock::now(); + + const std::string id = dal.upsertItem(row); + return !id.empty(); +} + +std::optional Orchestrator::loadSnapshot(const QString &nameSpace, + const QString &key) +{ + ki::PgDal dal; + const QByteArray dsn = qgetenv("PG_DSN"); + if (!dsn.isEmpty()) dal.connect(dsn.toStdString()); else dal.connect("stub://memory"); + + auto nsRow = dal.findNamespace(nameSpace.toStdString()); + if (!nsRow) return std::nullopt; + + std::vector tags; tags.emplace_back("snapshot"); + auto rows = dal.fetchContext(nsRow->id, std::optional(key.toStdString()), tags, std::nullopt, 1); + if (rows.empty()) return std::nullopt; + const auto &row = rows.front(); + if (row.content_json.empty()) return std::nullopt; + const auto doc = QJsonDocument::fromJson(QByteArray::fromStdString(row.content_json)); + if (!doc.isObject()) return std::nullopt; + return doc.object(); +} diff --git a/src/middleware/orchestrator.h b/src/middleware/orchestrator.h index 43658fe..1da47ee 100644 --- a/src/middleware/orchestrator.h +++ b/src/middleware/orchestrator.h @@ -64,6 +64,23 @@ public: // One-shot tick (public for tests). void processPendingTasks(); + /** + * saveSnapshot: persist a JSON snapshot under (namespace,key) as a context item. + * - Tags default to {"snapshot"}. + * - If PG_DSN is unset, uses an in-memory stub (won't persist across restarts). + */ + bool saveSnapshot(const QString &nameSpace, + const QString &key, + const QJsonObject &content, + const QStringList &tags = {QStringLiteral("snapshot")}); + + /** + * loadSnapshot: fetch the latest item for (namespace,key) tagged as snapshot. + * Returns empty optional if not found or on failure. + */ + std::optional loadSnapshot(const QString &nameSpace, + const QString &key); + signals: void taskProcessed(const QString &kind); @@ -88,4 +105,3 @@ private: bool continuityDone_ = false; IModelProvider *model_ = nullptr; // not owned }; -